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

9.0 KiB
Raw Blame History

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, 别顺手把二期功能实现出来。

二、常用命令(备注里是容易猜错的地方)

基础设施

cd deploy && docker compose up -d     # xt-pg(5432) + xt-redis(16379)

⚠️ 本机还有别的项目的 pg-verify(55432) 与 redis-local(6379),不要动它们。

后端

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)

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)

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 查库断言。

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