Files
xiaotutongxue/AGENTS.md
T
lovife fdfe21345a feat(roster): 名册导入(M2-01/02/03 后端)—— 表头探测 + 体检报告 + 合并/替换
计划里排第一的一步。顺序不是拍的,是依赖方向:
没有名册,门禁匹配不到任何人 → 学生进不来;而名册此前只能靠 SQL 夹具造。
(做了这一步,下一步的前端一上,任务发布就能在界面上走通。)

一、新增(module/roster/)
   RosterColumnCatalog   官方 21 列(A~U) ↔ 名册字段 对照 + 别名 + 表头文本归一化
   RosterImportService   表头探测 + 行解析 + 体检 + 落库(APPEND / REPLACE)
   RosterService         分页(姓名模糊 / 身份证号精确 / 班级 / 状态)
   RosterImportReport    体检报告(严格照 §8.1 的 JSON)
   RosterVO              名册项(证件号与手机号一律脱敏)
   CollectRosterMapper   批量判重 / 按 hash 定位 / 整表替换(都带显式 tenant_id)
   RosterController      POST /tasks/{id}/roster/import、GET /tasks/{id}/roster
   权限码现成且口径天然合上契约:导入 roster:manage(仅管理员),
   查看 roster:view(管理员+班主任+审核员)

二、先修了一个隐患:规范化被抄成两份就会出事
   门禁是按 id_card_hash(HMAC)等值查名册的,而 HMAC 的输入是**规范化后**的字符串。
   原来 normalizeName 是 MpRosterService 的私有方法,导入再抄一份的话,
   两处只要有一步不同 → 同一号码算出不同 hash → **名册里明明有这个人却查不到**,
   而症状会把人引向"学生是不是填错了"。已抽成 support/IdentityNormalizer
   (name/idCard),门禁与导入共用;并注明:加宽松(本次补的全角转换)安全,
   收紧是破坏性的(已有名册的学生会突然进不来)。
   验证不是"读代码比对",而是**用真门禁去核验导入进来的名册**(E2E 第 3 节)。

三、几个刻意的决定
   · dryRun 默认 true —— 不显式要求就不写库
   · ignoreEmptyRow(false) + 行号与 Excel 对齐 —— EasyExcel 默认跳过空行会让行号错位,
     报告说"第 12 行有问题"而用户翻到第 12 行是空的,整份报告就没人信了
   · 未识别的列进 ext_json(列名做键)—— "备注""推荐人"这类列不该被丢掉
   · 名册上的 phone 存脱敏值(不是密文):名册不需要回放完整号码
   · REPLACE 在已有人核验/提交时直接拒绝(40029)—— 否则那个学生的材料
     会挂到空名单上,他下次扫码进不来
   · APPEND 命中已绑定/已提交的行 → 跳过不覆盖(ROW_LOCKED):
     「报考职业」是锁定字段,盖掉等于把学生已确认的材料变成另一种认定
   · 校验位不通过**阻断且不自动改** —— 与"文件名里的号码"策略相反:
     名册是权威基准,改它等于我们替机构编了个身份证号

四、E2E:scripts/e2e-m2-roster.py → 58/58
   夹具刻意把**表头放在第 6 行**、第一张表全空白、表头带换行、另加一列"备注"。
   逐条验:4 个阻断行 + 2 个警告行的**行号与 code 都对**、dryRun 不落库、
   密文/脱敏值/ext_json、**完整身份证号能精确搜**(密文搜不了 LIKE)、
   响应里不得出现完整证件号、重复导入合并不新增、REPLACE 被拒后名册没被破坏、
   40028/40017 两条兜底出路。
   ⭐ 第 3 节用**真门禁接口**核验导入的名册 —— 导入与门禁的 hash 口径一致才算过。

   **本次 E2E 抓到一个真 bug**:write() 里"已存在则更新"的判断写反了
   (写成"不在 existingHashes 里才去查"),于是重复导入会插重复行,
   撞唯一约束 uk_task_idcard → 50000。也就是"第一次能用、第二次就炸",
   而重导一次正是最自然的动作。已修 + 在代码里点名这个坑。

五、又发现一处文档与代码不一致(以代码为准)
   接口设计 §3.6 把报考三项的"↔列"写成 ext_json,而建表脚本里它们是真实列。
   按 §3.6 实现的话,门禁回填(读真实列)会永远给学生空值。
   已在 RosterVO 类注释写明。

六、回归
   全量 613 条断言、0 失败(新增 58;门禁 29 照旧通过 → 规范化抽取没改变门禁行为)
   verify-docs.py 19 项全通过

七、下一步(第 2 步)
   名册导入前端(frontend-pc/src/views/task/):上传 → 表头确认 → 体检报告
   (错行定位、红黄区分)→ 名册列表。做完它,任务发布就能在界面上走通了。
2026-09-29 17:00:21 +08:00

148 lines
9.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# AGENTS.md
面向 AI 编码助手的项目指南。**用简体中文交流、用中文写文档。**
需求已冻结:**文档是需求源,代码是它的实现**,不要按"更合理"去改业务口径。
`docs/交接摘要.md` 是**唯一状态源** —— 动手前先读它最近的两节(§一·二十、§一·二十一)
与 `## 二、已锁定的铁律`;那里面大量决定"读代码看不出来、但都有理由"。
## 一、四个可运行部分
| 目录 | 是什么 | 入口 |
|---|---|---|
| `backend/` | Java 21 + Spring Boot 4.1.1 + MyBatis-Plus + PG16 + Redis7 | `xt-server/`(`xt-common/` 放响应体/错误码/加解密) |
| `frontend-pc/` | 管理端 Vue3 + Arco Design Vue(基于 **Gi Admin Pro**) | `pnpm dev` → 5173 |
| `frontend-mp/` | 学生端 **uni-app**(H5 与微信小程序同一套代码) | `pnpm build:h5` / `pnpm dev:mp-weixin` |
| `deploy/` | 本地基础设施(PG + Redis,**没有对象存储**) | `docker compose up -d` |
`docs/prototype/` 是视觉与交互的**唯一基准**(单文件自包含、零外部依赖、**不用 emoji**)。
**一期范围只做「学校收集 + 自主认定」**:`collectMode=2` / `certType=2,3` 一律返回 `40044`,
别顺手把二期功能实现出来。
## 二、常用命令(备注里是容易猜错的地方)
### 基础设施
```bash
cd deploy && docker compose up -d # xt-pg(5432) + xt-redis(16379)
```
⚠️ 本机还有别的项目的 `pg-verify`(55432) 与 `redis-local`(6379),**不要动它们**。
### 后端
```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`(不提交的本地私有配置)就在 `backend/`;
H5 静态目录配的是 `file:../frontend-mp/dist/build/h5/`
- 跑的是 **jar 不是热加载** → 改后端要重新 package(先停进程,否则 jar 被占用)
- **动过 `db/migration/` 必须 `mvn clean package`**(Flyway 已到 V6;迁移文件发布后不许改)
- 本地私有配置机制是 `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** —— `frontend-pc/AGENTS.md` 里"开发默认 Mock"已过时
- 我们的接口模块都在 `src/apis/xt/`;`apis/system|user|person|cate|area|test` 是 Gi 模板自带的,别往那儿加
- **PC 前端有独立的规范来源:`frontend-pc/.agents/`**(`AGENTS.md` + `skills/*`)。
改那里的代码前按它的表加载对应 skill,不要另起一套写法
### 学生端(uni-app)
```bash
cd frontend-mp && pnpm install
pnpm build:h5 # → dist/build/h5(由后端托管)
pnpm dev:mp-weixin # → dist/dev/mp-weixin(导入微信开发者工具)
```
- ⚠️ **H5 没有可直接联调的独立 dev server**:`vite.config.ts` 没有 proxy,而
`src/config/index.ts` 的 `API_BASE_URL` 取 `location.origin` → 只能访问
**后端托管的构建产物**(`http://localhost:8080/...`),改完前端要重新 `build:h5`
- 改前端后浏览器要**硬刷新**(`/assets/**` 是 immutable 缓存)
- 构建时 Dart Sass 的 `legacy-js-api` DEPRECATION 属正常
- `vue-tsc` 有 2 条**已知**报错(`src/config/index.ts` 的 `#ifdef` 重复导出),不是新引入的
- 当前节奏:**先做 H5**(可自动化测试、不依赖小程序资质);小程序端代码同步维护
## 三、怎么验证(本仓库最不显然的一点)
**后端没有单元测试**(`src/test` 是空的)。验证靠 `scripts/e2e-*.py`:
打真实接口 + `docker exec` 进 PG 查库断言。
```bash
python scripts/e2e-m3-file.py # 单跑一个;前提是后端 8080 在跑、xt-pg 容器在跑
```
- 全量:把 `scripts/e2e-*.py` 依次跑一遍(`e2e-m0*` / `e2e-m1-*` / `e2e-m2-roster` / `e2e-m3-*`),
目前合计 613 条断言
- 脚本**自己造 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`、`python scripts/dev-clean-test-users.py`
- 文档一致性:`python scripts/verify-docs.py`
## 四、接口与数据契约(不显然、容易做错)
- 响应恒为 `{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:8080`,
配在 `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/` 里**只有** `.env.example` 与 `docker-compose.yml` 被 git 跟踪
(`*.pem` 在 ignore 里,`deploy/rsa-public-key-dev.pem` 是本地开发公钥)
- 改动接口 / 库表 / FR-VAL 编号 → 重跑 `python scripts/verify-docs.py`,
并同步 `docs/00-交付计划.md` 与 `docs/交接摘要.md`