Files
xiaotutongxue/docs/交接摘要.md
T
lovife 9d7a8ca845 feat(m0): 后端骨架 + 账号体系(M0 验收 36/36 通过)
技术栈(已在 4.1.1 上实测跑通,非纸面选型)
  Java 21 + Spring Boot 4.1.1 + MyBatis-Plus 3.5.17(boot4) + PostgreSQL 16.15
  + Redis 7 + Flyway 12.4 + springdoc 3.1.1;前端定 Vue 3 + Element Plus + uni-app

交付内容
  M0-01 基础设施     deploy/docker-compose.yml(PG + Redis + MinIO,容器名与端口避让本机既有项目)
  M0-02 后端骨架     xt-common + xt-server;统一响应 / 全局异常 / 58 个错误码 /
                     多租户拦截 / 软删除 / 字段自动填充 / AES-GCM / HMAC 索引 / RSA / JWT
  M0-05 租户        开通租户(一次完成建租户 + 发 6 个内置角色 + 建首个管理员)、列表、详情
  M0-06 组织        CRUD + 组织树 + ancestor_path 自动维护 + 占用校验
  M0-07 PC 登录     RSA 解密 + 图形验证码 + 失败锁定 + 等时比较防账号枚举 + 审计日志
  M0-08 登出/当前用户  Token 黑名单(存 SHA-256,TTL 到自然过期)
  M0-01 数据库      Flyway 2 个迁移 → 26 张表(20 业务 + 5 标准 + 1 迁移记录)

安全姿态
  · 租户拦截器【默认拒绝】:匿名请求触及租户表直接抛 40101,不做「不加条件放行」
  · 权限码定义在代码里(PermissionDef / BuiltinRole),不建 sys_permission 表,杜绝前后端漂移
  · 匿名接口只靠 @PublicApi 放行,不做路径白名单,避免两套机制各说各话
  · 账号不存在时也跑一次 BCrypt(假哈希),拉平响应时间防枚举
  · 登录密码 RSA 传输,防止网关/APM 把明文密码记进日志

脚本
  scripts/sync-ddl.py          文档 DDL → Flyway 迁移,带 --check 防漂移
  scripts/e2e-m0.py            M0 验收 36 项,可进 CI
  scripts/verify-docs.py       文档一致性(接口数【逐节】核对、错误码、跨文档数字、表格列数)
  scripts/dev-login.py         RSA 加密登录助手
  scripts/dev-clean-e2e.py     清理 E2E 测试数据

文档修正(实测暴露,非人工审查)
  · DDL 缺陷:sys_tenant.contact_phone / sys_user.phone / collect_roster.phone
    原 varchar(20) 按明文长度建,装不下 AES-GCM 密文(约 52 字符),改 varchar(128)(PRD V2.4)
  · 错误码 40007b/40007c 带字母后缀与「code 为数字」的约定冲突,规范化为 40071/40072
  · 07-开发实施计划 新增 §9 版本兼容踩坑记录(10 条实录)与 §10 M0 完成情况
2026-09-28 17:28:21 +08:00

19 KiB
Raw Blame History

交接摘要

用途:压缩后的唯一状态源。新会话读这一份即可接续,不必回看历史对话。 更新:2026-09-28


一、项目一句话

职业技能等级认定报名材料收集 SaaS。学生在微信小程序填报、班主任在手机初审、管理员在 PC 终审与导出。核心价值:动态表单 + 规则引擎(命名/压缩/识别)+ 门禁核验 + 一键规范导出。

工作目录 D:\IDEA_Project\xiaotutongxue(空 git 仓库,无 commit,全部改动未提交),交付物在 docs/。


一·二、一期范围(2026-09-28 客户确认收窄)

一期只做「学校收集 + 自主认定」一条流程。 详见 PRD「一期范围声明」。

维度 字段 一期 二期后置
收集模式 collect_mode 1 学校收集 2 社会收集
认定类型 cert_type 1 自主认定 2 社会评价 / 3 专项职业能力

一期材料只有 4 类,全部有实测样本:一寸照片 / 身份证 / 学籍表 / 工作证明 二期才做的 2 类材料零样本:XJW_BA 学信网备案表 / OTHER_PROOF 其它佐证材料

数据结构保留不启用(二期直接开,不需 DB 迁移)——因为这三项本来就是配置驱动的 (needClassAudit 是开关、字段隐藏走 visibleWhen、材料差异是材料项配置), 所以省下的主要是测试成本与一个外部依赖,而不是代码量。

工期:392 → 387 人日(−5);关键路径不变,仍 15 周。

⚠️ 二期启动前必做:索取社会评价样本(D4)→ 标定 XJW_BA/OTHER_PROOF → 补社会收集审核流与专项职业能力字段隐藏。


一·三、开发进展(2026-09-28 起)

代码位置:backend/(Java 21 + Spring Boot 4.1.1)、deploy/(基础设施)、scripts/(工具)

项 状态
技术栈定版 ✅ Java 21 + Spring Boot 4.1.1 + MyBatis-Plus 3.5.17(boot4) + PG16 + Redis7
PC: Vue3 + Element Plus;小程序: uni-app
M0 后端骨架 ✅ 统一响应/异常/58 错误码/多租户拦截/软删除/字段填充/AES-GCM/HMAC/RSA/JWT/权限模型
M0 数据库 ✅ Flyway 2 个迁移;26 张表(20 业务 + 5 标准 + 1 迁移记录)
M0 后端接口 ✅ 租户开通与列表、组织 CRUD 与树、PC 登录/登出/当前用户
M0 验收 ✅ python scripts/e2e-m0.py → 35/35 通过
待做 用户 CRUD 与 Excel 导入;PC/小程序前端;CI/CD;H5 预研

关键实现决策(不要在后继开发中改回去)

决策 理由
权限码定义在代码里(PermissionDef / BuiltinRole),不建 sys_permission 表 权限码是前后端契约,放 DB 必然与代码漂移;一期无"权限配置界面"需求
租户拦截器默认拒绝:匿名请求触及租户表直接抛 40101 漏加租户条件 = 越租户泄露全校身份证号,不可接受的失败模式;宁可让开发者立刻撞墙
匿名接口只靠 @PublicApi 放行,不做路径白名单 两套机制会让"哪些接口公开"出现两个来源,改一处漏一处
登录时账号不存在也跑一次 BCrypt(假哈希) 否则响应时间差会泄露"账号是否存在",直接把密码喷洒成功率提高几个数量级
密码 RSA 加密传输 不是替代 HTTPS,而是防止网关/APM/链路追踪把明文密码记进日志
登出黑名单存 token 的 SHA-256,TTL 到自然过期 token 本身是凭证,明文写进 Redis 等于复制了一份到别处

四个必须实测的坑(详见 07-开发实施计划.md §9,10 条实录)

Boot 4 / Flyway 10 / MP 3.5.9 这一批版本都在"把单体拆成模块", 导致**「依赖在、功能不在」的静默失败变多**。典型:只加 flyway-core 时迁移不跑也不报错。 规则:每加一个中间件,必须实测它的核心动作真的发生了,不能只看依赖解析成功。


一·五、客户回执(2026-09-28 · 需求已冻结)

客户全部 23 项按建议方案确认。 唯一功能增量是 B1:名单加入申请通道。

组 需开发 说明
A 认定机构 6 项 0 与现有设计完全一致(min_long_side=1400、min_quality=70、PNG 保留等都是当初按实测设计的)
B 校方业务 9 项 1 B1 已落地:FR-509 系列 + collect_join_request 表 + 5 接口 + 2 屏原型
C 商务法务 5 项 0 C1 小程序资质推进中,H5 保底已列入 M0-13 预研
D 内部技术 7 项 0 D4 已后置为二期(一期不涉及零样本材料);D7 已降级(文件名识别准确率不影响任何交付项,残留只是上传页文案校准,班主任群里问两句即可)。均已清除,无外部依赖

规模变化:FR 115 → 125 / 接口 117 → 121 / 库表 19 → 20 / VAL 20 → 21 / 错误码 50 → 57 / Golden 断言 29 → 36 / 功能验收 85 → 111 / 原型 29 → 31 屏 / 任务 94 → 100 / 人日 380 → 392 → 387(一期收窄 −5)/ 工期 14 → 15 周。

B1 的设计要点(三条边界,不要在后继开发中放水)

# 边界 理由
1 只解决"进名单",不绕过门禁 申请期间仍不能填报;审批通过后才自动绑定放行
2 证件号与手机号仅存 HMAC + 脱敏值 与门禁口径一致
3 驳回必须逐条填原因;批量通过不提供"全选" 身份认定环节误操作代价极高

二、已锁定的铁律(不要重开讨论)

# 决策 关键理由
1 扫描件强制压到 ≤1M,无例外 认定机构硬要求
2 默认参数 300dpi / q85 实测 28/28 达标,平均 806KB,清晰度零损失
3 材料来源只有 聊天记录 / 相册 / 手机文件,「直接拍照」默认关闭 实测 4 类材料无一来自现场拍照
4 必须先导名单,姓名+身份证匹配不通过只提示、不放行(学校与社会收集皆然) 门禁是产品根基
5 报考范围从名单聚合,不存在独立"计划清单"文件 真实名单验证
6 动态表单 = 内置字段(固定列,可统计)+ 扩展字段(extras_json,仅展示导出) 兼顾统计能力与灵活性
7 数据库 PostgreSQL(纯 PG);软删除字段名为 deleted 客户已确认
8 班主任端只做右滑通过,驳回必须点按钮 误触后果不对称
9 批量通过只带无问题项,红黄一律排除 防"手快全选"
10 有不合格文件时直接禁用导出按钮,不允许带问题打包 铁律
11 识别策略 规则为主 + VLM 兜底(触发率 2%~10%) 成本可控
12 识别失败一律放行,不得阻断提交 可用性优先
13 原型不用 emoji(→ ↔ · 等排版字符保留) 设计约束

三、已完成交付物

文件 版本/大小 状态
docs/数据信息填报系统PRD.md V2.4 / 189KB / 3019 行 ✅ 含 125 条 FR、21 条 VAL、121 个接口、20 表 DDL、实测附录
docs/数据信息填报系统PRD-功能逻辑卷.md V1.9 / 88KB ✅ 纯功能逻辑,不含库表/接口/代码
docs/db/schema-postgresql.sql 37KB ✅ PG 16.15 实测零错误(20 表 / 177 列注释 / 65 索引 / 20 表注释)
docs/prototype/student-app.html 11 屏 / 57KB ✅ 含 STEP 03B「申请加入名单」
docs/prototype/teacher-app.html 9 屏 / 63KB ✅
docs/prototype/admin-web.html 11 屏 / 114KB ✅ 含表单设计器、材料规则配置、历史整理器、申请审批
docs/prototype/index.html 7KB ✅ 三端入口
docs/00-交付计划.md — ✅ 7 项计划与进度记录
docs/01-客户与机构确认函.md 23KB ✅ P1 23 项已全部确认(客户按建议执行,需求冻结)
docs/02-需求追溯矩阵.md 10KB ✅ P2 审计报告,含 10 个接口缺口
docs/03-接口设计.md 115KB / 2883 行 ✅ P3 121 个接口字段级定义 + 枚举全集 + 12 个 VO + 58 个错误码;§4 逐节声称数与表行数一致
docs/06-验收测试清单.md 33KB ✅ P6 36 条 Golden 断言 + 111 条功能验收 + 27 条异常边界
../README.md 10KB ✅ P7 文档导航,按角色读法 + 快速上手
docs/04-核心模块设计说明.md 69KB / 1453 行 ✅ P4 六个模块伪代码级说明 + 依赖图 + Golden 基线
docs/05-开发任务拆解.md 31KB / 506 行 ✅ P5 100 个任务 / 387 人日 / 15 周,含关键路径、一期范围与核算口径

四、待办计划

# 任务 产出 状态
P1 客户与认定机构确认函 01-客户与机构确认函.md ✅
P2 需求追溯矩阵 02-需求追溯矩阵.md ✅
P2.5 管理端补 3 屏原型 并入 admin-web.html ✅
P3 接口设计(OpenAPI) 03-接口设计.md ✅ 已完成(117 个接口)
P4 核心模块设计说明(伪代码级) 04-核心模块设计说明.md ✅ 已完成(6 个模块)
P5 开发任务拆解 WBS(M0–M7) 05-开发任务拆解.md ✅ 已完成(94 任务 / 380 人日 / 14 周)
P6 验收测试清单(Golden Test Set) 06-验收测试清单.md ✅ 已完成(29 条 Golden 断言 + 85 条功能验收)
P7 文档导航 README ../README.md ✅ 已完成

P3 已补齐的缺口(21 个接口,原审计发现 10 项)

✅ I1  报考范围读取/确认    GET/PUT /api/v1/tasks/{id}/plan-scope
✅ I2  历史数据整理器       POST /api/v1/cleanup/jobs(共 5 个)
✅ I3  字典管理            /api/v1/dicts(4 个)
✅ I4  操作日志            GET /api/v1/logs/operations
✅ I5  登录日志            GET /api/v1/logs/logins
✅ I6  消息中心            /mp/messages、/mp/messages/read、/mp/teacher/messages
✅ I7  订阅额度登记         POST /api/v1/mp/subscribe
✅ I8  审核撤回            POST /api/v1/audit/submissions/{id}/revoke
✅ I9  任务暂停/恢复        POST /api/v1/tasks/{id}/pause、/resume
✅ I10 材料识别结果读取      已并入 GET /api/v1/audit/submissions/{id} 响应
➕    文件规范化 process     原仅在 PRD §8.9 详述、未登记进清单,已补登

接口总数:96(PRD 原有)+ 21 = 117


五、关键事实速查

真实基准数据集(Golden Test Set)

D:\项目\202609机电填报系统\24中技数控1班\ —— 28 人 × 4 类材料 = 112 文件

实测项 结果
原始总量 475 MB
命名违规 59 / 112(52.7%)
双扩展名 56
体积超标(>1M) 31
证件号异常 5 例(3 可自动、2 待人工)
疑似串档 1(唯一必须人工判断的)
压缩后 300dpi/q85 → 平均 806KB、最大 831KB、0 张超标
材料识别 文件名层 112/112;形态层 99.1%;OCR 内容层 32/32

官方模板要点

  • 21 列(A–U):红=必填 11 项、绿=级联 6 项、黑=选填 4 项
  • 「报考职业」「报考工种」在 Excel 里没有下拉,是自由填写 ← 我方产品的核心增量
  • 「职业技能等级个人申报表」的规范文件名却是 「工作证明」(机构既有习惯)
  • 表头行与工作表名都不固定(实测一份在第 1 行、一份在第 6 行)

三个易错实现陷阱

  1. 「无」是合法业务值,不是空值
  2. 专项职业能力:无工种 / 无等级 / 无补考科目(0 科)
  3. 补考约束:三/四/五级最多 1 科,一/二级最多 2 科,专项 0 科

压缩五档(PRD §6.3)

A4 扫描件 300dpi(2480×3508)/q85 · 一寸照片固定 295×413 · 学籍表为 16:9 截图保持 PNG 不缩放

教师端数字口径(原型必须自洽)

28 人 = 已通过 15 + 待审 4 + 待核对 3 + 有问题 1 + 未提交 5;完成率 82% 是已提交率(23/28)


六、环境与工具

项 内容
PG 验证容器 docker exec -it pg-verify psql -U postgres -d filldb;清理 docker rm -f pg-verify
原型校验脚本 python C:\Users\93292\AppData\Local\Temp\opencode\check2.py <html路径>
DDL 同步方式 从主 PRD 的 ```sql 代码块自动抽取生成,改文档后重跑抽取脚本
原型约束 单文件自包含、零外部依赖、内联 SVG(<symbol>+<use>,1.7px 描边 currentColor)、无 emoji
原型校验基线 四份全通过:标签配对 ✓ / CSS 变量 ✓ / SVG 无冗余 ✓ / emoji 0 ✓

七、重要教训

教训一:脚本审计前必须先验证脚本本身

P2 审计时脚本因未匹配加粗编号 | **FR-614** |,误报 3 个"需求条目问题"(实为假警报);若直接采信并"修复",会引入 3 处新错误。同时 FR 总数由误算的 102 修正为 115。

⚠️ 这条教训在 2026-09-28 写开发实施计划时又复现了一次。 我为交接摘要重算交付物体量,用了三个正则去数"接口数",分别得到 74 / 95 / 121—— 三个都"看起来对",但没有一个经过验证。

定位过程:逐节对比「标题声称数」与「实际表行数」,发现 ① 我漏了 | ⭐ GET | ... | 这种 ⭐ 在方法前面的行; ② §4.9 / §4.11 是索引节(交叉引用别处已计数的接口),本就不该计入。 修正后逐节声称数与表行数全部为 0 差,合计 121。

加固:scripts/verify-docs.py 现在会做「逐节声称数 vs 实际行数」的一致性检查, 而不是只看总数对不对——总数相等也可能是个别节多算、个别节少算恰好抵消。

可复用的规则:

  1. 数字类断言必须交叉核对至少两种独立方法;
  2. 核对要分块(逐节/逐表),不能只看总和;
  3. 相互矛盾的中间值一个都不要先写进文档——写错再改,比一开始就空着更容易误导人。

教训二:写新文档必须回查已有编号体系,不能凭记忆

P4 写 M5 校验引擎时,我凭记忆用了一套 VAL 编号,与 PRD 权威清单整体错位:

P4 原写法 我以为的含义 PRD 实际定义
VAL-01 姓名与名册一致 证件号格式与校验位
VAL-03 证件号格式 姓名与名册一致
VAL-06 双扩展名 文件格式白名单
VAL-07 多扩展名冲突 文件体积上限

后果:若不修正,开发按 P4 实现会把规则完全接错,且两份文档互相矛盾。 修正:已全部重映射到 PRD 体系(单次扫描替换避免 01↔03 碰撞),并给 PRD 补了 P4 需要的三条新规则 VAL-18 文件名规范化 / VAL-19 疑似串档 / VAL-20 体积下限保护。 规则:新增文档引用任何已有编号(FR/VAL/错误码/接口)前,先脚本抽取权威清单核对,不凭记忆。

教训三:系统里的数据不会错,错的一定是「归属」

客户指出:"系统应该不存在名称错误的情况。是先录入了有这个姓名和身份证,然后才能填写的。"

这指出一个真实的设计缺陷:

位置 会不会错 说明
填报单里的姓名/证件号 永远不会错 门禁已核验 + 字段锁定。客户说得对
文件名里的姓名/证件号 可能不同 但这不是"名称错误",而是"这份文件可能不属于这个人"

原设计的错误:把"文件名姓证件号与名册不符"处理为「自动按名册修正」(信息级), 这会把"A 传了 B 的材料"静默洗白成 A 的材料——审核时完全看不出来,事后无从追溯。

学生 A 在微信里误选了同学 B 的材料
   ↓ 文件名:李四-3610...(B 的号码,校验位通过)
错误做法:判"与名册不符" → 改成 张三-3610... → 信息级,A 看不到
   ↓ 审核:班主任看到一份名字正确的材料 → 通过
   ↓ 最终:B 的工作证明被认定成 A 的 ← 灾难

修正(客户已确认):

  1. 判定优先级重排为 证件号反查名册 > OCR 内容 > 姓名(仅辅助)
  2. 新增 VAL-21 文件归属校验:归属存疑一律阻断提交,且不提供"仍然继续"
  3. 姓名不再作为独立规则——只在证件号佐证时按名册取值,仅留痕不打扰学生
  4. 核心判据:文件名证件号能否在名册里反查到"另一个人"(一次查询,走唯一索引,< 1ms)

精髓在于两条的区分:

  • 校验位失败 = 不是一个真实号码 → 只是写错了 → 可以自动改
  • 校验位通过 = 一个真实存在的自然人 → 改了就是改了身份 → 必须阻断

教训五:联系方式不进系统

客户指出:"学生本来就有班主任联系方式,不需要系统里面体现。"

原设计在门禁失败页放了一个「联系老师」按钮(脱敏手机号 + 一键拨打),并为此专门开了一个接口 GET /mp/roster/contact。 已移除:

移除项 原因
GET /mp/roster/contact 接口 不必要(用户已知道怎么找人)+ 有维护成本(换人/换号要同步)+ 多一处隐私暴露面
门禁失败页「联系老师」按钮 同上。改为只保留两条系统内动作:重新填写 / 申请加入名单
功能逻辑卷中的联系方式展示描述 同步

保留:纯文案提示(如"如有疑问请联系老师")——不涉及任何联系方式数据。 接口总数:122 → 121。

教训四:「社会评价」与「社会收集」只差一个字

cert_type(认定类型:自主认定 / 社会评价 / 专项职业能力)决定填什么材料; collect_mode(收集模式:学校收集 / 社会收集)决定谁审核。二者可任意组合,已在 PRD §5.3 与接口 §2.3 分别写明。 另外补全了 cert_type 的第三个取值(专项职业能力)在 DDL 注释与附录中的遗漏。

附带修正

  • 体积口径:原型导出中心曾写"475MB → 22.4MB(95%)",与 PRD 及历史整理器屏(37MB/92%)矛盾。实为混淆——444→22MB 是"工作证明"单项,475→37MB 是全班 112 文件合计。已统一为 37MB / 92%。

八、外部依赖(未回)

01-客户与机构确认函.md 已发出待回,其中最高优先级 3 项:

  • A1 认定机构是否书面接受 300dpi 扫描件
  • B1 名单外的人怎么处理(建议加"申请加入名单→管理员审批"通道)
  • B3 是否允许学生修改老师预填字段(建议:姓名/证件号/报考三项锁定,其余可改留痕)

未回复则按文档内建议方案推进。