2ff17cd0c9
用户决定:后端不再对外发静态文件,学生端 H5 交给网关。两者**仍然同源**
(微信只给一个授权回调域名;H5 的接口地址取自 location.origin),
所以前端业务代码、微信后台、application-dev.yml 的 h5-base-url 都不用改。
后端
- 删掉 dev/test/prod 三份 profile 里的 spring.web.resources.static-locations
- 主配置加 spring.web.resources.add-mappings=false —— 用开关而不是"指到空目录":
后者会连带启用 Spring Boot 的默认位置(classpath:/static 等),谁丢个文件进去就悄悄对外可见
- StaticCacheHeaderFilter → ApiNoStoreFilter,只剩 /api/** 的 no-store
(静态资源的缓存策略整体归 nginx,避免"改一处漏一处")
- 补 NoHandlerFoundException → 404:关掉静态映射后未匹配路径不再经过
ResourceHttpRequestHandler,抛的是 NoHandlerFoundException,只处理
NoResourceFoundException 时访问 / 会变成 500「系统繁忙」并刷 ERROR 日志
deploy
- 新增 xt-nginx(80):发 frontend-mp/dist/build/h5 + /api 反代 8080(host.docker.internal,
Linux 靠 extra_hosts host-gateway),端口与产物目录走 .env 的 H5_PORT/H5_DIR
- 三条"删了就会出事"的都已带上理由:client_max_body_size 50m(默认 1m,超了回 413
而后端日志一个字都没有)、/assets 的 immutable 与 *.html 的 no-cache(§一·十 那个
"发版后学生看到旧页面"的坑)、健康检查写 127.0.0.1(容器里 localhost 先解析到 IPv6,
nginx 只听 IPv4 → 永远 starting 而首页是好的)
CI 与文档
- CI:学生端 H5 构建从"e2e 的输入"改为编译检查,不再留 artifact(e2e 不消费它)
- 同步 AGENTS.md / README.md / docs/09(启动步骤、CI 表、发布注意、排障表)/ docs/11
- 交接摘要新增 §一·四十三(含理由、验证数字、要人工做的一步与环境限制)
验证
- mvn clean package 通过(必须 clean:删掉的旧类会留在 target/classes 被组件扫描)
- 后端直连:/ 与 /assets/* → 404({"code":40001,"msg":"接口不存在"})、
/api/.../captcha → 200 + no-store、/doc.html → 302→200、/v3/api-docs 与 /actuator/health → 200
- 经 nginx(80):首页 200 + no-cache、/assets/*.js 200 + immutable、/nope.js 404(无回退)、
/api 200 + no-store、gzip 生效;2MB multipart 上传 → 401(与直连一致 ⇒ 未踩 1m 的坑)、
55MB → 413
- e2e(XT_BASE=http://localhost,全程走 nginx):e2e-m0.py 37/37、e2e-m3-h5-auth.py 24/24、
e2e-m2-scope.py 38/38
- verify-all.py --quick 6/6;check-test-profile.py:test profile 仍能起来
196 lines
14 KiB
Markdown
196 lines
14 KiB
Markdown
# AGENTS.md
|
||
|
||
面向 AI 编码助手的项目指南。**用简体中文交流、用中文写文档。**
|
||
|
||
需求已冻结:**文档是需求源,代码是它的实现**,不要按"更合理"去改业务口径。
|
||
`docs/交接摘要.md` 是**唯一状态源** —— 动手前先读它最近的两节(**§一·四十二、§一·四十三**)
|
||
与 `## 二、已锁定的铁律`;那里面大量决定"读代码看不出来、但都有理由"。
|
||
|
||
> ⚠️ **2026-10 需求收敛(见 §一·三十六)**:只做 **4 个角色**(平台运营 / 学校管理员 / 班主任 / 学生)、
|
||
> **10 个 PC 菜单**、**不设二期**。删掉的东西不要再"顺手加回来":副班主任 / 审核员两个角色、
|
||
> 历史整理器、加入申请(含学生端自助申请入口)、`collect_task_org.deputy_user_ids`。
|
||
|
||
## 一、四个可运行部分
|
||
|
||
| 目录 | 是什么 | 入口 |
|
||
|---|---|---|
|
||
| `backend/` | Java 21 + Spring Boot 4.1.1 + MyBatis-Plus + PG16 + Redis7 | `xt-server/`(`xt-common/` 放响应体/错误码/加解密) |
|
||
| `frontend-pc/` | 管理端 **纯 Vue 3 + Naive UI 2.42**(2026-10 从 Gi Admin Pro + Arco 重搭) | `pnpm dev` → 5173 |
|
||
| `frontend-mp/` | 学生端 **uni-app**(H5 与微信小程序同一套代码) | `pnpm build:h5` / `pnpm dev:mp-weixin` |
|
||
| `deploy/` | 本地基础设施(PG + Redis + **nginx**,**没有对象存储**) | `docker compose up -d` |
|
||
|
||
`docs/prototype/` 是视觉与交互的**唯一基准**(单文件自包含、零外部依赖、**不用 emoji**)。
|
||
|
||
**范围只做「学校收集 + 自主认定」**:`collectMode=2` / `certType=2,3` 一律返回 `40044`。
|
||
**不设二期** —— 与「二期」有关的任何设计(社会收集、社会评价、专项职业能力、数据范围可配等)都**不做**,
|
||
别顺手实现;OCR 与材料识别是「缓做,等供应商选型」。
|
||
|
||
## 二、常用命令(备注里是容易猜错的地方)
|
||
|
||
### 基础设施
|
||
|
||
```bash
|
||
cd deploy && docker compose up -d # xt-pg(5432) + xt-redis(16379) + xt-nginx(80)
|
||
```
|
||
|
||
⚠️ 本机还有别的项目的 `pg-verify`(55432) 与 `redis-local`(6379),**不要动它们**。
|
||
|
||
⚠️ **H5 由 `xt-nginx` 托管(80),不由后端托管**(2026-10 调整,见 §一·四十三):
|
||
它把 `frontend-mp/dist/build/h5` 当静态站发出去,并把 `/api` 反代到后端 8080 ——
|
||
两者**同源**,这是微信授权与 H5 的接口地址(`location.origin`)共同要求的。
|
||
起它之前先 `cd frontend-mp && pnpm build:h5`(挂的是产物目录,空目录 = 页面 404)。
|
||
|
||
### 后端
|
||
|
||
```bash
|
||
cd backend
|
||
mvn -DskipTests package # 改了 Java 必须重新打包
|
||
java -jar xt-server/target/xt-server-1.0.0-SNAPSHOT.jar --spring.profiles.active=dev
|
||
```
|
||
|
||
- ⚠️ **工作目录必须是 `backend/`**:`application-local.yml`(不提交的本地私有配置)在那里,
|
||
是靠相对路径找到的(H5 静态目录那条相对路径已于 2026-10 随托管一起移走,见 §一·四十三)
|
||
- 跑的是 **jar 不是热加载** → 改后端要重新 package(先停进程,否则 jar 被占用)
|
||
- **动过 `db/migration/` 必须 `mvn clean package`**(Flyway 已到 **V19**;迁移文件发布后不许改,只能新加)
|
||
- 本地私有配置机制是 `profiles.active=dev` + `spring.profiles.include: local`
|
||
(模板 `backend/application-local.yml.example`)。
|
||
**不要改用 `spring.config.import`** —— 它优先级低于 profile 文件,会静默失效(见 `application.yml` 注释)
|
||
|
||
### 管理端(PC)
|
||
|
||
```bash
|
||
cd frontend-pc && pnpm install && pnpm dev
|
||
pnpm typecheck # vue-tsc pnpm lint # eslint
|
||
```
|
||
|
||
- 用 **pnpm**(仓库里是 `pnpm-lock.yaml`;别 `npm install`,会多出 package-lock.json)
|
||
- dev 通过 vite proxy 把 `/api` 转发到 `http://localhost:8080`(见 `.env.development`),
|
||
**没有 Mock**(`mock/` 与 `vite-plugin-mock` 都已删除)→ 后端不起着就没有数据
|
||
- 接口模块都在 `src/apis/xt/`(按业务域分文件);**没有 auto-import**,组件与 API 全显式 `import`
|
||
- ⚠️ **2026-10 已从 Gi Admin Pro + Arco 重搭为纯 Vue 3 + Naive UI**,因此:
|
||
- **`pnpm lint:style`(Stylelint)已移除**,只剩 `pnpm lint`(ESLint);`pnpm typecheck` 现在 **0 错**
|
||
- 权限判断统一写 `userStore.has('码')`(**没有 `v-hasPerm` 指令**),字符串必须与后端 `PermissionDef` **逐字一致**
|
||
- `n-tag` 的 `type` 必须过 **`@/utils/tag.ts`** 转换:`apis/xt/*.ts` 里的展示映射仍是 Arco 色词
|
||
(`green`/`arcoblue`/`gold`/`gray`),直接透传会**静默变灰**且不报错
|
||
- 图标:`scripts/extract-pc-icons.py` 生成 `src/icons/icon-data.ts`(37 个);
|
||
`AppIcon` 对缺失图标是**空占位、不报错** → **图标集升级后要重跑该脚本**
|
||
- **PC 前端有独立的规范来源:`frontend-pc/AGENTS.md` + `frontend-pc/.agents/`**(已按 Naive UI 重写)。
|
||
改那里的代码前先读它,不要另起一套写法
|
||
|
||
### 学生端(uni-app)
|
||
|
||
```bash
|
||
cd frontend-mp && pnpm install
|
||
pnpm build:h5 # → dist/build/h5(由 xt-nginx 托管,不由后端托管)
|
||
pnpm dev:mp-weixin # → dist/dev/mp-weixin(导入微信开发者工具)
|
||
```
|
||
|
||
- **H5 现在有独立 dev server 了**(`pnpm dev:h5` → http://localhost:5174):
|
||
`vite.config.ts` 加了 `/api` 代理,`.env.development` 把 `VITE_API_BASE` 设成空串,
|
||
于是请求走相对路径、由代理转发到 8080。**改一行不用再 `build:h5`**。
|
||
⚠️ 三端行为不同,改动这块前先看 `src/config/index.ts` 的注释:
|
||
dev server → 代理;构建产物(由 nginx 托管、与 `/api` 同源)→ `location.origin`;小程序 → 写死地址
|
||
- 改前端后浏览器要**硬刷新**(`/assets/**` 是 immutable 缓存)
|
||
- 构建时 Dart Sass 的 `legacy-js-api` DEPRECATION 属正常
|
||
- `vue-tsc` 有 2 条**已知**报错(`src/config/index.ts` 的 `#ifdef` 重复导出),不是新引入的。
|
||
⚠️ PC 端不同:重搭后 `pnpm typecheck` 是 **0 错** —— 那边一有报错就是新引入的
|
||
- 当前节奏:**先做 H5**(可自动化测试、不依赖小程序资质);小程序端代码同步维护
|
||
- ⚠️ **小程序端目前学生用不了(F-130,P0)**:前端**从来没有调用过 `/mp/login`**(token 永远是空 → 门禁 40101),
|
||
而后端那条路也不通 —— `scene` 带 taskId 时 `/mp/login` 必然 500(`MpLoginService.java:127` 匿名查租户表被租户拦截器拒)。
|
||
实测细节见 `docs/11` §5.4 / §6。**除登录外整条链路(落地页/门禁/表单/压缩上传/红黄校验/提交)都是好的**
|
||
—— 用开发者工具自动化(`wmp_*` 工具族)可以跑,`wx.login()` 在页面上下文里能拿到**真 code**
|
||
|
||
## 三、怎么验证(本仓库最不显然的一点)
|
||
|
||
**后端的单元测试很少**(`backend/*/src/test` 只有 12 条纯函数断言,`mvn test` 跑)。
|
||
主验证手段是 `scripts/e2e-*.py`:打真实接口 + `docker exec` 进 PG 查库断言。
|
||
|
||
```bash
|
||
python scripts/e2e-m3-file.py # 单跑一个;前提是后端 8080 在跑、xt-pg 容器在跑
|
||
```
|
||
|
||
- 全量:把 `scripts/e2e-*.py` 依次跑一遍(**38 个脚本 / 1712 条断言**,以最近一次
|
||
`verify-all --with-e2e` 的输出为准;2026-10 需求收敛删掉了 `e2e-m2-join.py` 与 `e2e-m7-cleanup.py`,
|
||
之后又补了 `e2e-m3-flow.py` —— 填报链路的覆盖缺口,见 `docs/11-填报流程端到端测试计划.md`),
|
||
或一条命令 `python scripts/verify-all.py --with-e2e`
|
||
- 脚本**自己造 MP 的 JWT**:读 `application-dev.yml` 的 `fill.jwt.secret` +
|
||
`application.yml` 的 `issuer` 手搓 HS512 —— 不需要先登录
|
||
- 改了后端**忘了重新打包/重启**,测试会对着旧 jar 通过,这是最容易踩的空转
|
||
- 跑完 `e2e-m3-*` 会**清掉名册绑定**(`collect_roster.bind_user_id`);
|
||
想继续用浏览器验演示态,要先重新绑定(见交接摘要的"演示态"一节)
|
||
- 清理残留:`python scripts/dev-clean-e2e.py`(**默认 dry-run**;
|
||
`--only-orphan-files --apply` 只清磁盘孤儿文件 —— 压测跑几轮就能攒下几百 MB;
|
||
`--apply` 清 e2e 租户与测试组织,**会先算「仍被存活任务引用的组织」并保留**)、
|
||
`python scripts/dev-clean-test-users.py`、`python scripts/dev-clean-qiniu.py`
|
||
(清真实桶里 `xiaotutongxue/` 前缀的测试对象,**默认 dry-run**,删前先核实库中零引用)
|
||
- 文档一致性:`python scripts/verify-docs.py`
|
||
- 接线检查:`python scripts/check-wiring.py`(**"声明了但没接上"** ——
|
||
权限码有没有地方校验、错误码有没有被引用、站内信模板有没有发送点、
|
||
`bizType` 有没有被用过、菜单键 ↔ PC 端 `XtMenu` 是否对得上、
|
||
**PC 端 `user.has('码')` 引用的权限码 ↔ `PermissionDef`**(第 ⑦ 项)、
|
||
**模板里的 `<n-*>` 组件有没有漏 import**(第 ⑧ 项 —— 漏了会被当未知元素渲染,样式整块丢、
|
||
编译与 lint 都不红)、
|
||
**`.svg` 资产是不是合法 XML**(第 ⑨ 项 —— SVG 注释里出现连续两个减号就 not well-formed,
|
||
图标变裂图而控制台一句话都不打;2026-10 换品牌标时连踩两次)、
|
||
**后端会写的日志动作码 ↔ PC 端 `OPERATION_ACTIONS` 的文案**(第 ⑩ 项 —— 缺一个文案,
|
||
日志页会**原样回显英文码**;做这一项时才发现 `AUTO_CLOSE` 是字符串字面量写的,
|
||
所以"提成常量"是让这个检查真正生效的前提)。
|
||
⚠️ 这一族错**编译 / lint / review 都不会红**(类在、表在、常量在、注释还写得很清楚),
|
||
只有"从入口走一遍"才暴露 —— 所以把它固化成命令;有意不接线的写在脚本上方的例外表里(带理由)。
|
||
⚠️ **检查器自己也会失效**:PC 端重搭后权限码的写法从 `permissions.includes` / `v-hasPerm`
|
||
改成了 `user.has`,第 ⑦ 项曾因此"一个码都扫不到却判为通过"。
|
||
加/改这类检查时**必须反向验证一次**(故意写错,它得红)
|
||
- 一条命令跑完上面这些:`python scripts/verify-all.py --with-e2e`
|
||
(9 项;**跳过项会如实列出来**,不是静默跳过)
|
||
|
||
## 四、接口与数据契约(不显然、容易做错)
|
||
|
||
- 响应恒为 `{code,msg,data}`;**业务错误也用 HTTP 200**,只有 401/403/429/5xxxx 用真状态码
|
||
- 软删除字段名是 **`deleted`**(不是 `is_deleted`);业务表都带 `tenant_id`
|
||
- **租户拦截器默认拒绝**:匿名请求触及租户表会直接 `40101`。匿名接口(如扫码落地页)
|
||
必须走专用 Mapper + `@InterceptorIgnore(tenantLine="true")` + **显式带 tenant_id**,
|
||
且集中放(`module/mp/mapper/MpSceneMapper.java`),就是为了"关掉框架保护后由人保证隔离"这件事有地方可审
|
||
- **权限码写在代码里**(`security/PermissionDef`、`security/BuiltinRole`);库里只有
|
||
`sys_role_permission` 做「角色 → 权限码」的分配,**没有** `sys_permission` 定义表 ——
|
||
加权限要改代码,不要想着往表里插一行
|
||
- 动态表单 = 内置字段(固定列、可统计)+ 扩展字段(`extras_json`,只展示与导出)。
|
||
**字段 key 全是 snake_case**,权威来源 `module/form/service/BuiltinFields.java`(21 项)——
|
||
改口径要同步它,别按直觉造 key
|
||
- `collect_task.status`:`0` 草稿 / `1` 收集中 / `2` 已截止 / `3` 已归档 / `4` 暂停
|
||
(接口设计文档曾写反,**以代码为准**)
|
||
- `collect_roster.id_card_no` 存的是 **AES 密文**(另有 `id_card_hash` / `id_card_mask`),
|
||
明文只在导入与门禁时存在 → 提交环节**做不到**身份证校验位校验,别去"补"这个
|
||
- 材料上传是**一步**:`POST /mp/files/upload` 收上来就压缩 + 按模板改名,默认**只存规范件**
|
||
(`fill.storage.keep-raw=false`);`process` 只是"有原始件时的重新规范化"
|
||
- 图片处理有两条**踩过真 bug**的规则,动之前先读交接摘要 §一·二十 / §一·二十一:
|
||
① 服务端必须按 EXIF 转正,否则手机竖拍的身份证会被存成躺倒的(且 E2E 的测试图不带 EXIF,抓不到);
|
||
② 浏览器与小程序对 EXIF 的处理**不一致**,前端必须"量出来再决定",写死必错一半
|
||
|
||
## 五、环境与流程坑
|
||
|
||
- 本地 H5 走花生壳穿透(`http://394nb68658le.vicp.fun` → `192.168.3.139:80`,
|
||
即 `xt-nginx`;**不是后端的 8080** —— 指到 8080 的表现是"接口好用、页面 404"。
|
||
配在 `application-dev.yml` 的 `mp-h5`)。**硬刷新后首屏常要 1~2 分钟**,
|
||
探测要给足,否则会把"还在加载"误判成失败
|
||
- 打桩登录(仅 dev:`fill.mp-h5.stub-enabled`,**生产必须 false**,代码里还会再判一次 profile):
|
||
`/api/v1/mp/h5/stub-login?openid=<openid>&scene=t1-k17` → 302 回 H5,token 放在 **URL fragment** 里
|
||
- `scene` 三段式:`t{tenantId}` / `t{tenantId}-k{taskId}` / `t{tenantId}-k{taskId}-o{orgId}`
|
||
(微信限 32 字符)。它**可穷举**,所以匿名接口只返回最少信息 + 按 IP 限流
|
||
- 存储默认 `provider=local`(落 `backend/data/uploads`,**只适合开发**:多副本必然丢文件);
|
||
七牛用 `provider=qiniu`,AK/SK 只放 `backend/application-local.yml` 或环境变量。
|
||
**MinIO 已删**,本地不再有 S3,别去找
|
||
- 七牛空间是**私有**的 → 下载走签名链接(有有效期):
|
||
**URL 不能入库、不能发出去让人明天再打开**,每次要用现取
|
||
- 改中文文档、或跑任何会碰到中文文本的脚本,**一律写成 `.py` 文件再执行**:
|
||
PowerShell 不支持 heredoc(`<<'PY'` 直接报错);`@"..."@` here-string 会**吃掉双引号**
|
||
(`return "bin";` 变成 `return bin;`)—— 带引号的代码用编辑工具改,别过转义层
|
||
|
||
## 六、提交前
|
||
|
||
- 不要提交:`backend/data/`、`dist/`、`application-local.yml`、`deploy/.env`
|
||
(都已在 `.gitignore`,但 `git add -A` 容易顺手带上 —— 先 `git status` 看一眼)
|
||
- `deploy/` 里被 git 跟踪的只有 `.env.example`、`docker-compose.yml` 与 `nginx/xt.conf`
|
||
(`*.pem` 在 ignore 里,`deploy/rsa-public-key-dev.pem` 是本地开发公钥)
|
||
- 改动接口 / 库表 / FR-VAL 编号 → 重跑 `python scripts/verify-docs.py`,
|
||
并同步 `docs/00-交付计划.md` 与 `docs/交接摘要.md`
|