This commit is contained in:
oneao committed 2026-10-14 17:25:47 +08:00
1 parent 15e0ae5b67
commit e434374db5
71 files changed
+5726 -329

No files matched your search

+141
View File
@@ -0,0 +1,141 @@
# CODEBUDDY.md
给 AI 协作者的项目约定。人看的话直接读 `README.md`(前端)与 `server/README.md`(后端)。
## 项目速览
前后端同仓、**独立构建、独立版本,唯一契约是 HTTP 接口**(前端不读后端源码,后端不管前端怎么调)。
| 目录 | 是什么 | 端口 / 入口 |
|---|---|---|
| `web/` | 前端(Vue 3 + Vite + Pinia),唯一有 `package.json` 的包 | dev server `5090`,`/api` 代理到 `127.0.0.1:8088` |
| `server/` | 后端(Spring Boot 4.1 + Java),多机构 SQL Server | `8088`,context-path `/api` |
| `sql/` | 工作流模块 SQL 脚本与演示数据 | 执行顺序见 `sql/README.md` |
**根目录没有 `package.json`**,前端命令一律在 `web/` 下跑。
## 启动
```bash
# 1) 组件库必须先有产物(@g3soft/ui 的 exports 指向 dist)
# 需要 g3soft-libs 与 g3soft-erp 平级
cd ../g3soft-libs && pnpm --filter @g3soft/tokens build && pnpm --filter @g3soft/ui build
# 2) 前端
cd ../g3soft-erp/web && pnpm dev # http://localhost:5090
# 3) 后端(连只支持 TLS 1.0 的老 SQL Server 时必须这么起,见 server/config/dbconfigs/README.md)
cd ../g3soft-erp/server
mvn "-Dspring-boot.run.jvmArguments=-Djava.security.properties=./config/legacy-tls.security" spring-boot:run
```
改完组件库要重启 dev server(构建会清空 `dist/`,dev server 会崩或白屏),详见根 `README.md`。
## 开发期免登录(省掉每次填账号 + 拖滑块)
`web/.env.development` 里配好了测试账号,`pnpm dev` 打开页面**已是登录态**:
| 变量 | 值 | 说明 |
|---|---|---|
| `VITE_DEV_AUTO_LOGIN` | `1` | 设为 `0` 关闭(例如要调登录页本身) |
| `VITE_DEV_ORG` | `G3HD` | 机构码,需对应 `server/config/dbconfigs/{机构码}.properties` |
| `VITE_DEV_ACCOUNT` | `demo_zhangwei` | 实测可登录,但**只有审批中心权限**(见下) |
| `VITE_DEV_PASSWORD` | `demo_zhangwei` | 演示账号密码=账号名 |
实现见 `web/src/dev/auto-login.ts`:调 `POST /api/auth/login` 拿 token 写进 store,
**只在 `import.meta.env.DEV` 下生效**,生产构建里被 tree-shake。
滑块验证码是纯前端校验(不发请求),所以走接口登录不需要它。
覆盖面:入口是 `/login` 时不自动登录;已有 token 时不重复登录。
个人覆盖用 `web/.env.development.local`(已被 gitignore,优先级更高)。
命令行取 token(例如要用 curl 直接打业务接口):
```bash
node scripts/dev-login.mjs --token-only # 只输出 token 一行
node scripts/dev-login.mjs # 人看的输出 + curl 示例
```
## E2E(Playwright)
```bash
cd web
pnpm test:e2e # 全量,headless
pnpm test:e2e:ui # 带 UI 的调试模式
```
前置条件:**后端已在 8088 起来**(前端 dev server 由配置里的 `webServer` 负责,已经在跑就复用)。
后端没起会在 globalSetup 阶段直接报「连不上后端」,不会跑到用例里。
与 Vitest 的分工:`pnpm test` 跑 `tests/**`(happy-dom 单元测试),`pnpm test:e2e` 跑 `e2e/**`(真浏览器 + 真后端)。
**单元测试放哪儿**:统一放 `web/tests/`,目录**镜像 `src/` 结构**
(`src/components/biz-tree/BizTree.vue` → `tests/components/biz-tree/BizTree.spec.ts`)。
不要贴源放进组件目录;`vitest.config.ts` 的 `include` 只收 `tests/**`,src 下误放的 spec 不会被执行。
测试里引用被测文件一律用 `@/` 别名(如 `@/components/biz-tree/BizTree.vue`),别写相对路径。
`playwright.config.ts` 的 `testDir` 必须限定 `./e2e`,否则 Playwright 会把 vitest 的用例也收进来。
### 免登录是怎么做的
1. `e2e/support/global-setup.ts`:**整轮测试只登录一次**,调 `POST /api/auth/login`,
会话落到 `e2e/.auth/session.json`(已 gitignore,里面有可用 token)。
2. `e2e/support/fixtures.ts`:覆盖内置 `context` fixture,用 `addInitScript` 在应用脚本
执行**之前**把 `sessionStorage['g3erp:login']` 种好,于是守卫直接放行,用例一进站就是登录态。
**为什么不是 `storageState`**:token 存在 `sessionStorage`,而 Playwright 的 `storageState`
只序列化 cookie + localStorage,种不进去。`addInitScript` 是唯一可靠的办法。
**为什么必须只登录一次**:后端 `ActiveSessionRegistry` 按 (机构码, 账号) 只保留最新会话,
重复登录会互踢(业务码 `40101`)。并发 worker 各登录一次就会互相把对方踢下线。
> 所以:**不要在用例里提交登录表单**。要测登录流程本身,得用一个专用账号,
> 并把 `VITE_DEV_AUTO_LOGIN` 设为 `0`(否则应用自己会先把你登录了)。
账号沿用 `web/.env.development` 的 `VITE_DEV_*`,可用 `E2E_ORG` / `E2E_ACCOUNT` / `E2E_PASSWORD`
覆盖;`E2E_BASE_URL` 换前端地址,`E2E_API_TARGET` 换后端地址,`E2E_CHANNEL=msedge|chrome`
改用系统已装的浏览器(Chromium 下载不便时)。
### 写用例时注意
- 落点别选 `/dashboard`:它**没有任何权限点**(`sql/20_registration/14_dashboard_menu.sql`
明确「不建权限点」),只有超管可见;默认账号 `demo_zhangwei` 进它会被守卫送到 `/403`。
`e2e/smoke.spec.ts` 用 `/approval/todo`,演示账号和超管都能进。
- 断言用 `toHaveURL(/\/approval\/todo$/)` 这种**正向**断言,别用 `not.toHaveURL(/\/login/)`:
`goto` 之后 URL 立刻就是目标值,否定断言会在重定向发生之前就通过,等于什么都没断言。
- dev server 是按需转译的,某页**第一次**访问要现编依赖图,实测 5-11s。
配置里已经把 expect 超时放宽到 15s —— 别在用例里写死更短的值。
## 测试账号
| 账号 | 密码 | 权限 | 用途 |
|---|---|---|---|
| `demo_zhangwei` / `demo_liqiang` / `demo_wangfang` / `demo_liumin` | 同账号名 | 只授了 `menu.approval%` 与 `action.v_b_othercompany.%`(`sql/90_mock/02_*.sql:186`) | 已实测可登录;侧栏只有「审批中心」,其他页面会被守卫拦到 `/403` |
| `g3soft` | **未知** | 前后端唯一超管,免查 `s_power`(`permission.ts:61`、`SqlPermissionService.ADMIN_ACCOUNT`) | 测全部页面得用它 |
机构码统一 `G3HD`。
**`g3soft` 的密码待确认**:2026-10-14 实测 `G3HD/g3soft/空密码` 返回 `code=401`
(`AuthServiceTests` 里「超管空密码」是纯 mock,不代表 G3HD 库的真实数据)。
拿到真实密码后填进 `web/.env.development` 的 `VITE_DEV_PASSWORD` 即可——
**一次配置,不必每次查库**。另外 `g3soft` 在 `b_user` 里是否存在也需确认:
本仓库的 `sql/` 没有任何脚本创建它(只建 `demo_*`),假定它来自 G3HD 核心库。
账号密码不用去查库。库里没有的账号靠 `sql/` 下的脚本建(执行顺序见 `sql/README.md`);
要给 `demo_*` 放开更多菜单,用 `sql/README.md` 里「可选:给普通账号授权」那段。
## 踩坑备忘
- **同一账号重复登录会互踢**:每次登录都刷新服务端 session(`ActiveSessionRegistry.activate`),
上一个会话的下一次请求会拿到业务码 `40101` 并被踢回登录页。所以**别和正在用 `g3soft` 的人共用一个账号**——
否则双方会在「登录 → 被踢 → 重新登录」之间来回。需要并行时用 `.env.development.local`
给 AI 换一个账号(`demo_*` 权限窄,测非审批页要另开权限)。
- **鉴权失败是 HTTP 200 + `body.code=401`**,不是 HTTP 401,判状态码会漏。
- **菜单是动态的**(读 `s_menu` 表):新增页面必须同时加前端静态路由 +`s_menu` 记录,
缺一个就表现为「点菜单进 404」。而 Vite 对任意路径都返回 HTTP 200,网络面板里看不出 404,
判据是**路由是否匹配**。
- **权限是白名单语义**:`s_power` 没授权 → 侧栏不显示该菜单 + 守卫拦到 `/403`。
- **token 存 `sessionStorage`**(key `g3erp:login`,pinia-plugin-persistedstate 写的),
所以浏览器每个新会话/新 context 都是未登录态 —— 这正是自动登录要解决的问题。
- **`docs/` 目录当前不存在**(提交 `15e0ae5b` 删掉了)。文档写进 `README.md` / `server/README.md`
或本文件,别再新增指向 `docs/` 的链接。