# 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/` 的链接。