# 完成度盘点与问题清单(施工计划) > 目的:**给"还剩什么、先做哪个、每个问题怎么改"一份可直接照着做的清单。** > 口径:以**代码与实测**为准(文档是需求源,但与代码冲突处以代码为准,冲突项在 §五 单列)。 > 盘点时间:2026-09-29 · 基线提交 `5213bc2` --- ## 一、盘点方式(复现命令) | # | 盘点项 | 命令 / 依据 | 本次结果 | |:--:|---|---|---| | 1 | 后端回归 | 依次跑 `scripts/e2e-*.py`(18 个) | **753 条断言、0 失败** | | 2 | 接口实现度 | `python scripts/check-api-parity.py`(解析 §4 的 121 个接口 ↔ 全部 controller 的 `@*Mapping`) | 首次盘点 **58 / 121**;完成 M6 导出后为 **62 / 121(51%)**;另有 21 个接口在代码里但契约清单没有 | | 3 | 管理端 | `pnpm typecheck` / `pnpm build` | 0 错 / 构建通过 | | 4 | 库表 | `docker exec xt-pg psql -U fill -d filldb` | 26 张表(20 业务 + `sys_*` 业务 5 + flyway 1),其中 4 张业务表**恒为 0 行** | | 5 | 文档一致性 | `python scripts/verify-docs.py` | **20 项全通过**(含本文件) | | 6 | 工作区 | `git status --short` | 干净;`xt-server-1.0.0-SNAPSHOT.jar` 已是最新提交的构建 | > ⚠️ **计数口径的教训**:第一版盘点用「`@GetMapping`」之类的短名匹配,漏掉了 > `FillController` 里用**全限定注解**(`@org.springframework.web.bind.annotation.PostMapping`) > 写的 3 个接口,于是 `/mp/submissions` 系列被误报成"未实现"。 > 结论:**数接口这类事必须按契约清单逐条比对,不能按写法猜** —— 与《交接摘要》§七 > 「脚本审计前先验证脚本本身」是同一类错误。 > > 现在这件事固化成 `scripts/check-api-parity.py`(**可复现**): > 它按 §4 总览逐条比对代码,结束时**自检"契约必须恰好 121 个"**,不等就退出码 1 —— > 这样"文档改了表格格式导致解析漏项"也会当场暴露,而不是给出一份看着对的错报告。 --- ## 二、总体完成度 | 层 | 已完成 | 未完成 | 完成度 | |---|---|---:|---:| | 后端接口 | 120 个(M0/M1/M2/M3 主链路 + M5 审核闭环与班主任看板 + M6 导出、分卷与过期清理 + B1 加入申请 + 学生端我的任务/撤回/进度 + 通知消息与催报 + 名册改删 + 操作/登录日志 + 字典管理 + 代传与教师端文件访问 + 历史数据整理器 + 校验问题清单 / 通知模板与手工发送 / 微信回调 + 任务克隆 / 任务统计 / 规则模板 / 名册导入模板 / 审核统计 / PC 端代传 + 开放接口(认定机构对接) + **小程序登录与绑手机号 / 组织导入 / 租户用量 / 文件视图·直传凭证·批量处理**) | 1 个 | 99% | | 后端里程碑 | **契约 §4 的 121 个接口里 120 个已实现**(M0~M3 主链路、M5 审核闭环、M6 导出与交付、M7 整理器与运维接口、开放接口、小程序登录与文件接口) | **只剩 M4 的 `/mp/ocr/idcard`**(等 OCR 选型)+ 前端页面与 CI/CD | 约 7.5/8 | | 管理端(PC) | 登录/工作台/组织/账号/租户/收集任务 + 3 个配置抽屉 + 名册与报考范围 + 审核台 + 导出中心 / 加入申请 / 通知记录 / 操作日志 / 字典管理 | 角色、**历史整理器页面**(后端已就绪)、表单设计器 | 约 92% | | 学生端(H5/小程序) | 落地页、门禁核验、填报、**我的任务 / 撤回 / 进度 / 消息中心 / 加入申请** | 临期提醒页、催报 | 约 85% | | 班主任端(小程序) | 看板 / 审核卡片流 / 详情与驳回(右滑通过、左滑不绑驳回)+ **代传材料 / 催报 / 学生材料预览**(后端与 MP 页面均已就绪) | 临期提醒入口 | 约 95% | | 基础设施 | PG 16 + Redis 7 + 七牛(`provider=qiniu` 已验)、Flyway **V1~V16**、本地存储兜底、导出产物落盘 + 流式下载 + 分卷与过期清理、**33 个 E2E 脚本(1489 条断言)+ 3 个机械校验脚本** | CI/CD、test/prod 环境、压测、灰度 | 约 58% | **契约接口实现度(按 §4 分类)** | 分类 | 契约 | 已实现 | 缺口 | |---|:--:|:--:|:--:| | 4.1 认证与账号 | 13 | **13** | 0 | | 4.2 组织与租户 | 15 | **15** | 0 | | 4.3 任务与配置 | 30 | **27** | 3 | | 4.4 名册 | 12 | **12** | 0 | | 4.5 填报端 · 小程序 | 16 | **15** | 1 | | 4.6 班主任移动端 | 10 | **10** | 0 | | 4.7 审核 | 9 | **9** | 0 | | 4.8 校验 / 导出 / 通知 | 12 | **12** | 0 | | 4.10 开放接口 | 4 | 0 | 4 | | 合计 | **121** | **120** | **1** | --- ## 三、已完成(可用、有证据) | 里程碑 | 内容 | 证据 | |---|---|---| | M0 | 骨架、58 错误码、多租户默认拒绝、软删除、AES-GCM/HMAC/RSA、JWT、权限码在代码里;租户开通、组织树、账号全套(含 Excel 导入)、PC 登录/验证码/改密 | `e2e-m0` 36、`e2e-m0-user` 55 | | M1 | 任务 CRUD 与状态机(发布 5 项阻断校验,`force` 绕不过)、覆盖班级与班主任(软移除+历史行复用)、材料项定义(3 内置模板)、表单 schema 版本化(40025/40026)、填报二维码 | 71 / 48 / 68 / 48 / 32 | | M1 前端 | 收集任务列表、配置班级抽屉、材料规则界面、表单设计器 | 上述 E2E + 浏览器实测 | | M2 | 名册导入(表头探测/体检报告/APPEND‑REPLACE/dryRun)、名册分页、**报考范围聚合与 4 类体检**、FR‑311 追加告警、门禁匹配引擎(原子绑定)、H5 微信授权、租户域名(默认关闭、能力保留) | 67 / 38 / 29 / 23 / 6 | | M3 | 命名规范化引擎、压缩引擎(五档 + EXIF 转正 + 二分降质)、上传即规范(默认不留原始件)、草稿自动存、提交与 VAL 红黄绿校验 | 39 / 27 / 31 | | 存储 | 七牛私有空间签名链接、本地 `provider=local` 兜底 | 17 | | M5 后端 | 待办/详情/通过/驳回/批量通过(服务端强制只带绿色)/撤回/时间轴/审核端文件通道;「权限≠范围」用 `1 = 0` 守死;红黄计数现场重算并回写 | **87** | | 学生端 | 落地页、门禁核验、填报页(条件级联 + 材料上传 + 前端压缩 + 提交清单 + 材料预览) | H5 浏览器实测 + `e2e-m3-*` | --- ## 四、未完成(按"能不能上线"排序) > ⚠️ **截至 2026-09-30,剩下的全部是外部依赖或二期范围** —— 没有一条是"我们还没写"。 > 所以每条都指向 `docs/01-客户与机构确认函.md` 的具体编号:**卡在谁那里、要谁做什么**。 > > | 剩项 | 卡点 | 找谁 | 编号 | > |---|---|---|---| > | P0-1 小程序类目资质 | 教育类目可能要办学许可 | 法务 / 商务 | `docs/01` **C1**(第一轮已问,等回执) | > | P0-4 H5 与小程序两个 openid | 需微信**开放平台**账号并绑定两个 appid 才有 `union_id` | 商务推动 + 我方 0.5 人日 | `docs/01` **E1**(第二轮新增) | > | P1-1 M4 识别与校验(7 个接口) | 需先定 OCR 供应商(含数据出境合规) | 商务比价 + 我方技术评估 | `docs/01` **D1** | > | P2-8 小程序端真机验证 | 需要真机与体验版 | 客户提供设备 | `docs/01` **D6** | > | P2-6 二期功能 | 一期范围外(已约定) | 客户定启动时点 | `docs/01` **E3** | > > 📌 **`union_id` 那条最容易漏**:字段(`sys_user.union_id`)与方案都已就位、 > 注释也写了,但**全仓库零读写**(683 个用户全为 `null`)—— > 它不是"没设计",而是"**没有它做不了**"。**两条填报腿不能同时上线**,就卡在这一步。 > > 📌 **不要在没有这些输入的情况下去"顺手实现"**:M4 按某家供应商写死,换供应商要重写。 ### 4.1 P0 · 阻断学生开始填报 / 阻断交付 | # | 未完成项 | 对应任务/契约 | 为什么是 P0 | |:--:|---|---|---| | P0-1 | **小程序类目资质**(C1) | M7-10 前置 | 开发版/体验版学生都扫不开,只有正式版能扫 —— 它决定"能不能开始收"(→ `docs/01` C1) | | ~~P0-2~~ | ~~导出打包引擎与交付物 + 导出中心 UI~~ → ✅ **已完成**:`views/export/index.vue` 有前置检查清单(`canExport=false` 时**禁用按钮、不弹警告放行**)、进度、产物下载(带 token 取 blob);`scripts/e2e-m6-export.py` 60 条、`e2e-m6-export-split.py` 35 条 | M6-05、M6-06 | — | | ~~P0-3~~ | ~~审核前端(管理端终审台 + 班主任端卡片流)~~ → ✅ **已完成**:管理端审核台 + `frontend-mp/src/pages/audit/audit.vue` 实现了原型铁律(**右滑通过 / 驳回必须点按钮**,左滑什么都不做),另有 `audit-detail` / `dashboard` / `messages` / `tasks` / `progress` 五个小程序页面 | M5-07~11 | — | | P0-4 | **H5 与小程序两个 openid** | 架构前置 | 两条腿都上线时同一学生会被当成两个人(撞 `40072`),必须先绑开放平台拿 `union_id` 并按它认人(→ `docs/01` **E1**,字段与方案已就位、零读写) | ### 4.2 P1 · 影响验收与可用性 | # | 未完成项 | 对应任务/契约 | |:--:|---|---| | **P1-1** | **M4 识别与校验**(形态 / OCR / VLM / 串档)—— **现在唯一的大块**(§4.8 的 7 个缺口全在这里) | M4-01~07、M4-09(→ `docs/01` **D1**;识别失败一律放行) | | ~~P1-2~~ | ~~校验问题接口(重跑/清单/导出)~~ → ✅ **已完成**(§六 6.1 第 6i 项)。**剩余**:M4 的**识别类**校验本身(等 D1 选型) | §12.1~12.3 | | ~~P1-3~~ | ~~通知与消息中心、订阅额度、催报~~ | ✅ **已完成**(§13.5 三个 + §13.6 一个 + §8.6/§10.7 催报四个 + §13.3 通知记录一个)。**剩余**:§4.6 的教师端文件访问(`/mp/teacher/files/{fileId}`)—— 见 F-28:钉住降级链后两级的不是代码量,而是"短信/公众号通道还没有商务结论" | | P1-4 | ~~B1 加入申请(客户唯一功能增量)~~ → **已完成**(§六 6.1 第 3 项) | §8.7 的 5 个接口 + 学生 STEP 03B + 管理端审批页 **全部落地** | | ~~P1-5~~ | ~~学生端「我的任务列表 / 撤回 / 进度与问题清单」~~ → ✅ **已完成**:`pages/tasks`(含「撤回修改」,`canWithdraw` 由服务端算)+ `pages/progress`;`scripts/e2e-m3-mytask.py` 42 条 | §9.1、§9.11、§9.12 | | ~~P1-6~~ | ~~班主任端看板与文件访问~~ → ✅ **已完成**(§六 6.1 第 6g 项:看板 / §10.6 代传 / §10.7 催报 / §4.6 学生材料预览) | §10.1、§10.6、§10.7 | | ~~P1-7~~ | ~~历史数据整理器(差异化获客功能)~~ → ✅ **后端已完成**(§六 6.1 第 6h 项,含串档检测与体检报告)。**剩余**:PC 端页面 | M7-06/07、§13.2 的 5 个接口 | | ~~P1-8~~ | ~~操作日志与登录日志查询、字典管理~~ → ✅ **已完成**:操作日志**真的在写**(`sys_operation_log` 实测 523 条,最近一条是几分钟前;F-1 已在前面几轮修掉),写入口统一走 `OperationLogService`;`views/log` 与 `views/dict` 均已可用 | M7-03/04/05、§13.3、§13.4 | | ~~P1-9~~ | ~~任务统计与数据看板~~ → ✅ **已完成**(§六 6.1 第 6j 项 + 本轮 6o 的工作台看板页) | §7.11、M7-01/02 | | ~~P1-10~~ | ~~名册改/删、导入模板下载~~ → ✅ **已完成**:`RosterEditService`(改姓名/证件号/删除,都写操作日志)、名册导入模板下载(表头固定、必填列标红、带 9 条规则说明页);`scripts/e2e-m2-roster.py` 67 条 | §8.3、§8.4、§8.5 | | ~~P1-11~~ | ~~M1-05 规则模板;任务克隆~~ → ✅ **已完成**(§六 6.1 第 6j 项;另补了契约漏掉的「保存为模板」,见 F-62)。**剩余**:模板的 FORM 部分(F-64) | §7.13、§4.3 的 `/tasks/{id}/clone` | | ~~P1-12~~ | ~~代传材料(教师端/审核端)~~ → ✅ **已完成**(§六 6.1 第 6g 项) | M5-06、§10.6、§11.7 | | ~~P1-13~~ | ~~开放接口(认定机构对接)~~ → ✅ **已完成**(§六 6.1 第 6k 项;另补了契约漏掉的接入方管理,见 F-67) | §14 的 4 个接口(HMAC 加签) | | ~~P1-14~~ | ~~学生端 `/mp/login`(小程序端登录)+ 手机号绑定~~ → ✅ **已完成**(§六 6.1 第 6l 项) | §5.3、§4.1 | ### 4.3 P2 · 上线收尾与二期 | # | 未完成项 | 说明 | |:--:|---|---| | ~~P2-1~~ | ~~CI/CD + test/prod 环境 + 部署 + 数据迁移预案~~ → ✅ **本轮完成**:`.gitlab-ci.yml`(6 个 job,三阶段;**e2e 阶段跑全套 1516 条断言**,每次随机生成密钥)+ `application-test.yml` / `application-prod.yml`(**零密钥**)+ `ProdEnvCheck`(启动自检,6 条"配错了也能启动"的配置)+ `docs/09-部署与运维.md`(发布 / 密钥轮换 / 迁移与回滚 / 灰度 / 排障 / 首次接入 CI 的检查单) | M0-04、M7-10 | | ~~P2-2~~ | ~~压测与调优~~ → ✅ **本轮完成**:新增 `scripts/load-test.py`(四个场景、按端点分别统计、可调规模);**压测发现并修掉一个会让整间教室被挡住的限流缺陷**(F-92)。结论:**后端没有 N+1、DB 在 2ms 以内**,并发下的延迟是排队效应。尚未测:导出大包、上传并发、5000 人名册、长稳(见 docs/09 §9.5) | M7-09 | | ~~P2-3~~ | ~~定时任务(PRD 附录 E)~~ → ✅ **已完成**:**8 项已实现**(任务自动截止 / 自动催报 / 审核超时提醒 / 原始件清理 / **用量快照** / **压缩失败重试** / **租户到期提醒** / 导出包清理 + 开放接口推送重试),`POST /schedules/run` 可手工触发任意一项。⚠️ 第 9 项「待认领名册清理」**不做** —— 它对应的"自助认领"机制已在 PRD V1.5 废除(见 F-91) | M7-12 | | ~~P2-4~~ | ~~用户手册 / 培训材料 / 客服话术~~ → ✅ **本轮完成**:`docs/10-用户手册与客服话术.md`(三角色操作手册 + 高频问题标准回答 + 30 分钟培训提纲 + 上线首日检查单);并新增 `scripts/check-manual.py`(**机械核对手册引用的错误码与提示文案是否真的存在**,已进 CI) | M7-11 | | ~~P2-5~~ | ~~导出包过期清理~~ → ✅ **已完成**:`ExportCleanupService.cleanupExpired()`(`@Scheduled` 每小时第 17 分跑) | M6-07 | | ~~P2-10~~ | ~~导出分卷(`splitSizeMb > 0`)~~ → ✅ **已完成**:`SplitZipWriter` 会产出 `{base}-part1.zip`、`{base}-part2.zip`…;`scripts/e2e-m6-export-split.py` 35 条 | M6-07 | | P2-6 | 二期:社会收集 / 社会评价 / 专项职业能力 | 一期范围外(`collectMode=2`、`certType=2,3` 返回 `40044`)→ 启动时点见 `docs/01` **E3** | | ~~P2-7~~ | ~~七牛实时样式缩略图~~ → ✅ **本轮完成**:`StorageService.thumbUrl()`(**参数拼在签名之前**,拼错顺序会 401 且看起来像网络问题);`FileUrl` 同时给 `url` 与 `thumbUrl`;审核端 `?thumb=true`。**实测 1454KB → 36KB(2.4%)**,1600×1200 → 400×300 等比不变形 | M7-09 附带 | | P2-8 | 小程序端 `compressImage` 真机验证;D6 三通道体积上限实测 | 需真机(→ `docs/01` D6) | | ~~P2-9~~ | ~~学生端 H5 没有独立 dev server~~ → ✅ **本轮完成**:`frontend-mp` 加 `/api` 代理 + `.env.development` 把 `VITE_API_BASE` 设空串,`pnpm dev:h5` → http://localhost:5174 可直接联调;三端行为(dev server / 构建产物 / 小程序)都验过,且 **AGENTS.md 里那句过期的说明已同步** | 开发体验 | --- ## 五、发现的问题与修改建议 ### F-1【P1 · 真问题】操作日志从没人写 - **现象**:`sys_operation_log` 0 行;`module/log/mapper/OperationLogMapper` 全仓库**只有它自己 import**(`LoginLogMapper` 则在 `AuthService` 里真写,`sys_login_log` 704 行)。 - **影响**:M7-04 的「敏感操作留痕」实际不存在(查看明文证件号、下载、导出、代传、删除都无痕);而 §13.4 的两个查询接口一旦做出来,界面上会永远是空的。 - **建议**: 1. 加 `module/log/service/OperationLogService` + `@OperationLog(module, action)` 注解 + `aspect/OperationLogAspect`,在注解里声明要记的动作; 2. 优先挂 6 处:`VIEW_IDCARD_FULL`(审核详情看完整证件号)、导出创建与下载、代传、名册 REPLACE、账号删除、租户开通; 3. **入库前脱敏**(`SysOperationLog` 类注释已写明:去掉 password、证件号明文); 4. 验收:E2E 新增一节,断言"看完整证件号后 `sys_operation_log` 多了一行且不含明文"。 ### F-2【P2 · 环境遗留】`xt-minio` 容器还在跑 - **现象**:`docker ps` 有 `xt-minio`(9000/9001,已跑 30 小时),而 `deploy/docker-compose.yml` 里**只有 pg 与 redis**,代码里 S3/MinIO 已删。 - **影响**:占用端口 + 误导(新人会以为本地有对象存储可用)。 - **建议**:`docker rm -f xt-minio`;若还有旧数据要留,先 `docker run --rm -v` 导出再删。同时在 `AGENTS.md` 的"环境与流程坑"里保留"MinIO 已删,别去找"这句(已有,够用)。 ### F-3【P1 · 文档与代码不一致】`/mp/files/upload-token` 与 `/mp/ocr/idcard` - **现象**:契约(PRD §8 与 03 §9.5/§9.9)规定上传是「OS 直传 + `upload-token`」,OCR 走 `/mp/ocr/idcard`;实际实现是 `POST /mp/files/upload`(服务端压缩 + 按模板改名,**默认不留原始件**),OCR 完全没做。 - **影响**:前端按契约写会调不存在的接口;`upload-token` 是"防重复建文件记录"的关键设计(重复提交幂等),现在靠 `POST /mp/files/upload` 每次新建记录来承担。 - **建议**(推荐改文档 + 保留实现,别改实现): 1. 在 03 文档 §9.5 标注「**已改为服务端上传**:`POST /mp/files/upload`(multipart,收上来即压缩+改名)」,§9.6 `process` 语义按 `AGENTS.md` 的口径改成"有原始件时的重新规范化"; 2. §9.9 OCR 标注「一期缓做(等 D1 选型)」,不要留着像已实现的样子; 3. 若后续要恢复直传(大文件/弱网),再单独立任务,不要在这轮混做。 ### F-4【P1 · 文档与代码不一致】契约里没有的 19 个接口 - **现象**(代码有、03 文档 §4 清单没有):`GET /mp/scene-context`、`GET /auth/captcha`、`GET /auth/public-key`、`GET /mp/h5/authorize|callback|stub-login`(代替 `/mp/login`)、`PUT /users/{id}/status`、`GET /mp/files/submissions/{submissionId}`、`GET /mp/files/{id}/url|content`、`GET /audit/files/{id}/url|content`、`POST /tasks/{id}/materials/apply-template`、`GET /material-catalog`、`PUT /tasks/{id}/plan-scope`(清单里只列了 GET)。 - **影响**:契约不是"当前系统长什么样",导致每次核对都要重新考古;前端/测试无法以文档为准。 - **建议**:在 03 文档**新增一节「§17 实现补充与偏差记录」**,逐条列出这些接口(路径 / 权限 / 是否要补进 §4 清单),并在 §4 的合计口径里说明"121 是设计口径,另有 N 个实现补充"。改动后必须重跑 `python scripts/verify-docs.py`(它逐节核对接口数,加节时注意别改动 §4 的声称数)。 ### F-5【P2 · 数据卫生】E2E 租户残留到"每个请求多一次解析" - **现象**:`sys_tenant` 17 行、`sys_user` 300 行,而脚本每次跑都会建测试租户,清理只删自己建的那一个。 - **影响**:本地演示/截图时列表里全是 `e2e-*` 名字;租户域名解析与登录"跨单位重名"分支被无意义数据搅扰。 - **建议**:`scripts/dev-clean-e2e.py` 增加一节"按名字前缀删测试租户与账号"(默认 dry-run,`--yes` 才真删),并在 `AGENTS.md` 里把"跑完 E2E 顺手清一次"写成固定动作。 ### F-6【P2 · 文档误导】`frontend-pc/AGENTS.md` 里的 Mock 说明已过时 - **现象**:该文件写「开发默认 Mock(`/mock` 前缀)」,而 `AGENTS.md` 已注明"已过时,没有 Mock,dev 走 vite proxy 到 8080"。 - **影响**:AI/新人按旧说明写 Mock 代码或排查"为什么没有 `/mock`"。 - **建议**:直接改 `frontend-pc/AGENTS.md` 那段为"联调真实后端:`pnpm dev` + vite proxy → `http://localhost:8080`;无 Mock"。 ### F-7【P2 · 一致性】撤回的动作码与适用状态 - **现象**:DDL 注释 `3=批量通过` 与 03 §11.4「写入 `action = 3`(撤回)」冲突,代码取 `7`(已按"时间轴不能混淆"处理);§11.4 的状态表只列状态 3,代码额外允许状态 2。 - **建议**:客户确认后同步两处文档;若客户不同意状态 2 的撤回,删一个分支即可(代码里有注释标明)。 ### F-8【P2 · 安全/上线】打桩登录与存储密钥 - **现象**:`application-dev.yml` 里 `stub-enabled: ${WX_H5_STUB:true}`(**dev 默认开着**),而代码侧默认 `false`;`keep-raw: ${XT_STORAGE_KEEP_RAW:false}` 默认不留原始件;七牛 AK/SK 只在本地 `application-local.yml`。 - **建议**:上线检查清单里固化三条 —— ① 生产**不设** `WX_H5_STUB`(代码里 `fill.mp-h5.stub-enabled` 默认 false + profile 二次判定,两道都留着);② `provider=qiniu` 且 bucket 私有;③ 导出/下载一律走签名链接、**URL 不入库不外发**。 ### F-9【P3 · 工程】后端无单元测试,纯函数没被单测覆盖 - **现象**:`src/test` 为空,验证全靠 E2E(很硬,但每次要起服务 + 连库)。 - **建议**:先给三个**纯函数**补最小单测(不引框架之外的东西):`IdentityNormalizer`、命名规范化引擎、压缩档位推导。理由是这三个"输入输出确定、没有 IO",单测成本最低、回归收益最高;其余继续靠 E2E。 ### F-10【P3 · 一致】租户域名相关文档暂缓 - **现象**:§一·二十五 的能力已实现且默认关闭,PRD 与 03 文档未写(当时有意暂缓)。 - **建议**:保持暂缓,但**在 03 文档 §17 留一行**"租户域名能力:`Host 定位 + token 授权`,默认关闭,启用条件 4 步",避免将来重开时找不到入口。 --- ## 六、施工记录(本轮自主完成的事) > 客户要求:**"完成所有的开发计划,中途发现问题自己决定,记录出来让我知道即可。"** > 本节的每条都带**验证方式**,不写"已完成"这种没有证据的话。 ### 6.1 已完成并验证 | # | 事项 | 交付物 | 验证 | |:--:|---|---|---| | 1 | **P0-3 审核前端(PC)** | `frontend-pc/src/apis/xt/audit.ts`、`views/audit/index.vue`(待办台:四个计数、分级过滤、批量通过、快捷通过/驳回)、`AuditDetailDrawer.vue`(材料/问题清单/改动明细/时间轴/通过·驳回·撤回)、`AuditRejectModal.vue`(附录 C 原因码 + R999 必填说明)、`FilePreviewImage.vue`(签名链接现取);`xt-routes.ts` 的 `audit` 由"待建"改为已实现 | `vue-tsc` 0 错、`eslint` 0 错;dev server 逐模块编译 200(含 4 个新组件);接口层用真数据核过(见下) | | 2 | **P0-2 导出与交付(M6 后端 + 导出中心 UI)** | 后端 `module/export/`:实体 + 两个 Mapper + DTO + `ExportService`(前置检查 / 目录树 / 异步打包 / 进度 / 下载)+ `ExportController`(7 个接口)+ 独立导出线程池;前端 `apis/xt/export-task.ts`、`views/export/index.vue`(范围/包名/阻断清单/目录树预览/按钮禁用/记录表)、`ExportDetailDrawer.vue`(进度轮询 + 产物 + 下载) | **`scripts/e2e-m6-export.py` → 60/60**;`vue-tsc` 0 错、`eslint` 0 错、dev server 模块编译 200 | | 3 | **P1-5 学生端「我的任务 / 撤回 / 进度」** | 后端 `StudentTaskMapper` + `StudentTaskService` + `StudentTaskController`(§9.1 `/mp/tasks`、§9.11 `/mp/submissions/{id}/withdraw`、§9.12 `/mp/submissions/{id}/progress`);学生端新页 `pages/tasks/tasks.vue`(我的填报:待办数/状态/驳回原因/截止提醒/撤回)与 `pages/progress/progress.vue`(材料清单 + 问题 + 时间轴);落地页在"有登录态但没 scene"时改为进「我的填报」 | **`scripts/e2e-m3-mytask.py` → 42/42**;H5 `build:h5` 通过且产物含 `pages-tasks-tasks` / `pages-progress-progress` | | 3b | **P1-4 B1 名单加入申请** | 见 §6.1 上一轮记录(后端 + 学生 H5 + 管理端审批 + V7 迁移) | `scripts/e2e-m2-join.py` → 50/50 | | 4 | **P0-3 班主任端(小程序)看板 + 审核卡片流** | 后端 `TeacherDashboardMapper` + `TeacherDashboardService` + `MpTeacherDashboardController`(§10.1 看板);小程序新页 `pages/dashboard/dashboard.vue`(已交率与已通过**分开呈现**、四个待办角标、班级卡片、截止提醒)、`pages/audit/audit.vue`(卡片流:**右滑通过 / 左滑不绑驳回**、一键通过只带无问题项)、`pages/audit-detail/audit-detail.vue`(材料预览 + 问题清单 + 改动明细 + 驳回弹层:原因快选 + 二次确认);「我的填报」按 `/auth/me` 的角色给出老师入口 | **`scripts/e2e-m5-teacher-dashboard.py` → 24/24**;H5 `build:h5` 通过且产物含 `pages-dashboard-dashboard` / `pages-audit-audit` / `pages-audit-detail-audit-detail` | | 5 | **可复现的接口计数** | `scripts/check-api-parity.py`(契约 §4 的 121 个接口 ↔ 全部 controller,自带"必须恰好 121"的自检) | `python scripts/check-api-parity.py` 退出码 0 | | 6 | **界面验收用的演示数据** | `scripts/dev-seed-audit.py`(走真实接口摆出:任务 + 班级 + 材料 + 名册 + 门禁绑定 + 3 份已提交填报单 + 1 份已初审,含一个**演示班主任**账号) | 脚本自身对每步 `code` 断言;审核接口回读确认(见 6.3) | | 6b | **P1-3 通知与消息中心** | 后端 `module/notify/`:`CollectMessage` 实体 + `CollectMessageMapper`(批量已读)+ `SubscribeQuotaMapper`(额度 UPSERT / 原子扣减)+ `NotifyRecordMapper` + `NotificationService`(降级链)+ `NotifyController`(§13.5 消息中心 3 个 + §13.6 订阅额度 1 个);**V8 迁移**新建 `collect_message`;`AuditService` 的通过/驳回真的会通知学生;小程序新增 `pages/messages/messages.vue` + `utils/subscribe.ts`(提交后索取授权) | **`scripts/e2e-m5-notify.py` → 48/48**;H5 `build:h5` 通过且产物含 `pages-messages-messages` | | 6c | **P1-3 剩余:催报与通知记录** | 后端 `UrgeService` + `UrgeMapper` + `UrgeDtos` + 两个 Controller(§8.6 定向/一键催报、§10.7 教师端催报与历史、§13.3 管理端通知记录);`NotificationService` 的发送记录补上 `task_id`(催报去重要用它);PC 新增 `views/notify/index.vue`(通知记录:状态 3 与 2 分开显示)+ [notify.ts](frontend-pc/src/apis/xt/notify.ts) | **`scripts/e2e-m5-urge.py` → 40/40**;`vue-tsc` 0 错、`eslint` 0 错 | | 6d | **P1-8 操作日志留痕(§13.4,PRD FR-506/FR-1202 的 P0 项)** | **V9 迁移**给 `sys_operation_log` 补 4 列结构化留痕(`target_type/target_id/before_val/after_val`);`OperationLogService`(显式留痕 + **统一强制脱敏**)+ `LogController`(§13.4 操作日志与登录日志两个查询);已接入的敏感动作:名册改证件号 / 改名 / 删名册项(§8.3/§8.4 一并实现,含「标记不参与」的可逆替代)、任务发布/暂停/恢复/截止/归档;PC 新增 `views/log/index.vue` | **`scripts/e2e-m5-logs.py` → 44/44**;`vue-tsc` 0 错、`eslint` 0 错 | | 6e | **P1-8 剩余:字典管理(§13.3)** | **V10 迁移**新建 `sys_dict_item`(客户自定义项)与 `sys_dict_disabled`(内置项停用开关,**两表分开**才能"一键恢复默认");`DictCatalog`(内置字典写在**代码里**,照抄官方表格)+ `DictService` + `DictItemMapper` + `DictController`(§13.3 四个 + 内置项 toggle);顺带给 `BuiltinFields` / `RejectReasonCatalog` 加了访问器 —— **表单下拉与字典是同一份口径**,不是复制两份;PC 新增 `views/dict/index.vue`(内置项只有"停用/恢复",自定义项才可改名删) | **`scripts/e2e-m1-dict.py` → 53/53**;`vue-tsc` 0 错、`eslint` 0 错 | | 6f | **导出分卷 + 过期清理(§12.4 的 `splitSizeMb` / 保留 7 天)** | **V11 迁移**新建 `collect_export_file`(一票一卷一行,替代原来那段 `result_files` JSON);`SplitZipWriter`(边写边轮转,**每一卷都是完整可解的 ZIP**,不是 `.z01`);`ExportCleanupService`(定时任务 + 手工入口:**删文件、留记录**);`ExportService` 的下载改走产物表并**按行判可下载**(过期后回「已被清理、请重新导出」而不是「文件不存在」);新增 `scripts/check-entity-columns.py`(机械校验"实体基类要求的列 vs 迁移建表列") | **`scripts/e2e-m6-export-split.py` → 33/33**;`e2e-m6-export.py` 60/60 未回归 | | 6g | **P1-12 班主任代传材料 + 教师端文件访问** | **V12 迁移**给 `collect_file` 补 `proxy_by` / `proxy_reason`;`TeacherProxyService` + `MpTeacherProxyController`(§10.6 代传、§4.6 教师端看学生材料);**一步 multipart**(不是契约写的"先传文件再挂 fileId");三道闸门:只给自己负责的班 / 只给草稿状态的单 / 必须留痕且学生可见 | **`scripts/e2e-m5-proxy.py` → 33/33** | | 6h | **P2-04 历史数据整理器(§13.2,5 个接口)** | **V13 迁移**给 `collect_cleanup_job` 补审计列(`update_by`/`update_time`/`deleted` —— V1 漏了,**写代码之前**就被 `check-entity-columns.py` 拦住)+ `roster_task_id`;`HistoryNameParser`(从"人长出来的"历史路径里解析姓名/证件号/材料/班级)+ `CleanupService`(解压 → 解析 → 与名册比对 → 按模板重打包 → 体检报告)+ `CleanupImportService`(导入为正式任务,**幂等**);**报告算出 `autoHandled / needManual`**(契约里最重要的一句话)与**疑似串档检测** | **`scripts/e2e-m7-cleanup.py` → 54/54**(含"坏包要明确失败"与"无名单时串档恒为 0") | | 6i | **P1-2 校验问题清单(§12.1~12.3)+ §12.7 通知模板与手工发送 + 微信回调** | **V14 迁移**给 `collect_validation_issue` 补审计列(该表 V1 就建好了、列与契约 §3.9 逐列一致,只是**一直没有代码用它**,见 F-50)并加"同一问题只一行"的表达式唯一索引;新建 `collect_notify_template`、给 `collect_notify_record` 补 `msg_id`;`ValidationService`(**对齐语义**:已有→只更新内容不碰 resolved / 新增→插入 / 消失→标已解决)+ `NotifyTemplateService`(内置模板写在代码里、库里只存改过的;自定义 / 恢复默认)+ `NotifyCallbackService`(验签 + 凭发出时记的 id 反查租户);上传时把"系统改过文件名"记成 VAL-06 自动修复,让 `byRule.autoFixed` 有真实数据 | **`scripts/e2e-m7-validation.py` → 70/70** | | 6j | **P1-11 / P1-9 任务克隆、任务统计、规则模板、名册导入模板、审核统计、PC 端代传** | **V15 迁移**给 `collect_rule_template` 补审计列(该表 V1 就建好、列与契约 §7.13 的 FORM/MATERIALS/NAMING 三部分逐项对应,见 F-50)并随迁移带两条**公共模板**;`TaskCloneService`(克隆**配置**、**刻意不克隆业务数据**)、`TaskStatisticsService`(补上 `abnormalCount` 与 `avgAuditHours` —— 这两个字段以前恒为 null,因为问题表与审核记录还不存在)、`RuleTemplateService`(可按 parts 只应用一部分、材料清单**覆盖式合并**)、`RosterTemplateService`(表头固定第 1 行 + 必填列标红 + 9 条填表规则页)、`AuditStatisticsService`、`TeacherProxyService.proxyUploadAsAdmin`;并修掉 F-61(初审待办会列出**批不了**的单子) | **`scripts/e2e-m7-taskconfig.py` → 69/69**(含待办范围判据的回归防线) | | 7 | **全量回归** | — | **33 个脚本 / 1489 条断言 / 0 失败**(753 + 导出 60 + 加入申请 50 + 学生端 42 + 看板 24 + 通知 48 + 催报 40 + 日志 44 + 字典 53 + 分卷 35 + 代传 33 + 整理器 54 + 校验 70 + 任务配置 69 + 开放接口 60 + 组织与文件 54) | | 6u | **P2-7 缩略图 + P2-9 H5 独立 dev server** | **缩略图**:`StorageService.thumbUrl(path, expire, maxLongSide)`(七牛在**签名之前**拼 `imageView2/2/w/h`;本地存储回退原图,让调用方不必判断"当前是什么存储");`FileUrl` 加 `thumbUrl`/`thumbMaxLongSide`;审核端 `?thumb=true`(**默认 false** —— 详情页要看防伪纹)。**H5 dev server**:`vite.config.ts` 加 `/api` 代理 + `.env.development` 把 `VITE_API_BASE` 设空串(`uni.request` 不能传相对路径,所以空串时仍补 `location.origin`,由代理兜住) | **缩略图实测 1454KB → 36KB(2.4%)**、400×300 等比;新增 `scripts/e2e-m9-thumb.py` → **12/12**(打到真实七牛,含"参数若拼在签名之后会 401"这一条);H5 dev server 实测 5174 可访问且 `/api` 代理到 8080;**H5/小程序两端构建都验过**(小程序产物仍是写死地址) | | 6t | **P2-4 用户手册 / 培训材料 / 客服话术(M7-11)** | 新增 `docs/10-用户手册与客服话术.md`:学生/班主任/管理员三份操作手册 + 「高频问题 → 标准回答」+ 30 分钟培训提纲 + 上线首日检查单。⚠️ 文案与错误码**逐条取自代码**,并在写完后**对着代码核了一遍** —— 抓到两处与实现不符的说法(见 F-95);另把这项核对固化成 `scripts/check-manual.py` 并进 CI | **9 个错误码 + 14 句提示文案全部与代码一致** | | 6s | **P2-2 压测(M7-09)** | 新增 `scripts/load-test.py`(门禁 / 读接口 / 上传 / 导出四场景;**按端点分别统计**;`--rows` 可调规模)。**压测抓到一个真缺陷并修掉**:门禁 IP 限流计的是"所有请求",于是 **60 个不同学生同一出口 IP 同时核验只过了 20 个** —— 而"一个班连同一个 WiFi"正是首次落地第一分钟就会发生的事(见 F-92)。**调优结论(三条独立证据)**:SQL 条数恒为 3(与 pageSize 无关)→ 无 N+1;单请求延迟与页大小无关(20/50/200 → 47/36/44ms);`EXPLAIN ANALYZE` 显示 DB 侧 ≤2ms。所以并发下的 p50 156~375ms 是**排队效应**,要提吞吐得加机器而不是改 SQL | **修前 20/60 → 修后 60/60**;门禁 E2E 仍 29/29 绿 | | 6r | **P2-3 定时任务收尾(PRD 附录 E 的最后三项)** | **V17 迁移**:`collect_file` 加 `process_retry_count` / `process_error` / `last_retry_time`(+ 只索引"还没处理出规范件"的**部分索引**)、新建 `sys_tenant_usage_snapshot`(一个租户一个周期一行,唯一索引保证幂等);`ScheduleJobs` 加三个任务:**用量快照**(每日 01:00,⚠️ `tenant_id` 必须**显式写**—— 平台上下文不会自动填,忘了就会全挤到 0 上,而那看起来像「没有数据」)、**压缩失败重试**(每 10 分钟,候选 = 没有规范件 + 有原始件 + 计数 < 3;**必须封顶**,否则原图损坏的文件会被每 10 分钟重试一次而永远不会成功)、**租户到期提醒**(每日 08:00,30/7/1 天,发给**该租户自己的管理员**);`runNow` 支持 7 个任务名 | **`e2e-m7-schedule.py` 扩到 38/38**(新增快照幂等与一致性、重试累加与封顶、到期提醒的日期窗口与收件人) | | 6q | **P2-1 CI/CD + test/prod 环境(M0-04 / M7-10)** | 新增 `.gitlab-ci.yml`(三阶段 6 个 job:机械校验 / 构建(含 H5)/ **全套 E2E 打真库**;密钥**每次随机生成**,顺带证明"应用没有偷偷依赖 dev 默认值");`application-test.yml`(零密钥、允许 clean 以便每次从空库跑全量迁移)、`application-prod.yml`(零密钥、禁 clean、关 Swagger、日志滚动);`ProdEnvCheck`(`EnvironmentPostProcessor`,**跑在任何 bean 之前**);`docs/09-部署与运维.md`;删掉模板残留的 GitHub Pages 工作流(远端是 GitLab、用 pnpm、且它发布的是没有 API 的静态页) | **新增 `scripts/e2e-prodguard.py` → 9/9**(三种误配形态都拒绝启动)+ `scripts/check-test-profile.py`(test profile 真起一次,可重复跑);本地照跑 CI 的 check/build 各步骤全通过 | | 6p | **P2-3 定时任务(PRD 附录 E 的四项)** | 新建 `module/schedule/`:`ScheduleMapper`(**跨租户的候选查询集中在这一个文件**,全部 `@InterceptorIgnore` + 只读;别名一律带双引号,见 F-83)+ `ScheduleJobs`(四个 `@Scheduled`,每个都是"先以平台身份扫候选 → 再逐个租户摆上下文干活"的两段式)+ `ScheduleController`(`POST /schedules/run` 手工触发,权限用运营侧的 `log:view:all`)。四项:**任务自动截止**(`end_time < CURRENT_DATE`,动作名 `AUTO_CLOSE` 以区分「系统关的」与「管理员关的」)、**自动催报**(7/3/1 天,`end_time = 那天` 而非 `<=`,只催未提交)、**审核超时提醒**(时效与 §11.6 同口径,发给"真的要动手的那个人",**找不到班主任时升级给学校管理员**)、**原始件清理**(先清引用再删文件)。另给 `UrgeService` 加 `autoUrge`、`TaskService` 加 `autoClose` | **新增 `scripts/e2e-m7-schedule.py` → 27/27** | | 6o | **P1-9 工作台数据看板(PC)** | 工作台底部原来是一张「开发进度」卡,写着「M0 后端已完成,界面建设中」—— 而项目已是 120/121 个接口可用、10 个 PC 页面可点。**首页上说一件不真的事,比空着更糟**,所以换成真实看板:`apis/xt/task.ts` 加 `getTaskStatistics` + `TASK_STATUS_TEXT`(状态文案放一处)、`apis/xt/audit.ts` 加 `getAuditStatistics`;`views/dashboard/index.vue` 加"选任务 → 进度四格(应填/已提交/已通过/待审)+ 分班进度 + 审核情况 + 驳回原因分布"。**提交率与通过率分两列**(已提交 ≠ 已通过);平均审核时长按量级换单位显示(`null`=还没有样本 / 0=不足 1 分钟 / 否则分钟或小时) | **浏览器实测**:应填 3 / 已提交 3 / 提交率 100.0% / 已通过 3 / 通过率 100.0% / 待审 0,分班表与审核表都出数;`pnpm typecheck` 通过、`eslint` 0 错 | | 6n | **「待建」页不再说谎(3 个"功能已就绪、入口在别处"的菜单项)** | `名单管理 / 材料规则 / 表单设计` 三个菜单项对应的功能**早就做好了**(`TaskRosterDrawer` / `TaskMaterialDrawer` / `TaskFormSchemaDrawer`,入口在「收集任务」每一行的链接里),但点进菜单看到的一直是「**这个页面还没做**」+ 一句早已过期的"已实现的页面:工作台、组织架构、租户管理"。**说"还没做"而功能其实在隔壁,比找不到更糟 —— 它让人得出错误结论。** 给 `XtMenu` 加可选的 `entryAt`,待建页据此分两种说法:有 `entryAt` → 「功能已就绪」+ 真实入口指路 + 跳转按钮;没有 → 原来的「还没做」(`角色与授权` 就是这个:权限码由代码决定、自定义角色属二期,确实没做)。并删掉那句**会静默过期**的清单 —— 任何写死在代码里的"现状"都会过期,而没人会记得回来改它 | **浏览器实测四个菜单项**:三个显示「功能已就绪」并可一键跳到「收集任务」,`角色与授权` 显示「二期」;`pnpm typecheck` 通过、`eslint` 0 错 | | 6m | **P2-04 历史整理器页面(PC)** | 新增 `frontend-pc/src/apis/xt/cleanup.ts`(含**报告口径**:`headlineOf` 拼"112 个文件里 90 个全自动处理,只有 1 个必须你亲自判断",但它拼的是**服务端算好的两个数**,不是前端减出来的 —— 前端减出来的早晚会与后端分叉;下载走 api 层,与导出中心同一写法)+ `views/cleanup/index.vue`(列表 / 上传 / 进度轮询 / 报告 / 难点清单 / 改名样例 / 证件号冲突 / 下载 / 导入);路由 `ready: true`。**实机验证**(浏览器里真的点了一遍):列表出数据、点详情出报告、压缩率显示 `—`、重点条目带建议、导入按钮可见 | `pnpm typecheck` 通过;`eslint` 0 错;**截图留证** | | 6l | **P1-14 / P1-8 / P1-10 小程序登录、组织导入、租户用量、文件视图·直传凭证·批量处理** | `MpLoginService`(`jscode2session` + **复用** H5 的 `resolveUser`,不复制一份建账号逻辑)+ 手机号绑定(手机号**从客户端到不了服务端**,所以能作凭据;一个号只能绑一个账号);`OrgImportService`(父子关系用**编码 + 上级编码**、两遍扫描支持"子行在父行之前"、补 `ancestor_path`);`TenantUsageService`(周期支持月/季;`ocrCount`/`smsCount` **恒为 0 并带 notes 说明**);`MaterialFileViewService`(§9.5 预检+秒传+压缩提示、§9.7 批量处理、§9.8 预览带 `actualKind`);并修掉 F-73(秒传排在份数检查之后 → 网络重传得到"你超了"而不是"你已经传过了") | **`scripts/e2e-m8-orgfile.py` → 54/54** | | 6k | **P2-05 开放接口(§14,供认定机构对接)** | **V16 迁移**新建 `collect_open_app`(密钥存 AES 密文 —— HMAC 要能还原,不能只存哈希)与 `collect_open_push_log`(`next_retry_at` **落库**,内存定时器在重启后就没了);`OpenSignService`(HMAC-SHA256 + 5 分钟时间窗 + nonce 防重放,**先验签再占 nonce**)、`OpenDataService`(§14.2/§14.3,**一律不返回明文证件号**、签名 URL 30 分钟)、`OpenPushService`(§14.4 推送 + 定时重试 + 人工重推,推送挂在 `afterCommit` 上)、`OpenAppAdminController`(契约漏掉的接入方管理,见 F-67);并把规则模板的两个接口**修回契约路径** `/api/v1/rule-templates`(见 F-68) | **`scripts/e2e-m8-open.py` → 60/60** | ### 6.2 导出模块到底做了什么(客户最关心的那条链) - **铁律 10 落地**:前置检查不通过 → `40035` + 完整清单(谁的哪一项、该怎么办)+ `quickActions`; **没有"警告后放行"这个分支**(代码里就没有 force 参数)。界面上可以直接拿 `GET /exports/precheck` 决定按钮是否可点。 - **只装该装的**:草稿与未绑定的人不进包(但会在 `warnings` 里说清有几个人); 文件只取 `is_current = 1`(历史版本绝不进交付物 —— 否则等于把两份矛盾的身份证一起交给机构)。 - **目录结构按 PRD §5.9**:根目录 `{批次号}-{任务名称}` → 班级 → **材料类型** → 文件, 汇总表(文件名用机构的叫法:`考生线下报名信息导入模板(汇总).xlsx`)放根目录。 ⚠️ 第一版我按直觉做成了"班级/姓名/文件",与本规范不符 —— 已改回并按规范加了断言(见 F-16)。 - **目录树预览**:`GET /exports/preview` 与真正打包**共用同一份构建代码**, 所以"预览"不会说谎;E2E 会把预览的节点名与产物路径**逐项比对**。 - **汇总 Excel**:严格 21 列(对齐官方模板 A~U),证件号用脱敏值。 - **中文文件名**:用 commons-compress 显式设 UTF-8 编码, 并在 E2E 里**断言 ZIP 的 UTF-8 标志位(general purpose bit 11)** —— 这一条只在 Windows 解压时才出错,服务器上怎么看都对,所以必须验在产物里。 - **校验值**:产物带 `sha256:`,下载接口回它,便于客户核对传输完整性。 ### 6.3 本轮自主决定的事(客户只需知道,不需要回复) | # | 决定 | 理由 | |:--:|---|---| | D1 | 审核台**不做**前端侧的范围过滤,只显示服务端返回的待办 | "权限 ≠ 范围"由服务端用 `1 = 0` 守死;前端再过滤一次只会在口径不一致时制造假象 | | D2 | 批量通过被跳过的条目**逐条列原因**(弹窗里列出姓名 + 原因) | 只说"过了 3 个"会让用户反复点那个大按钮 | | D3 | 审核台的"通过"按钮在 `approvePolicy.canApprove=false` 时**禁用并显示原因**,而不是等他点了再弹 `40032` | 点完才被拒会让人以为系统故障 | | D4 | 新增 `GET /exports/precheck`(契约里没有) | 界面必须先知道"能不能导"才能禁用按钮;契约只在 POST 失败时回 40035,那样用户要先点一次才知道 | | D5 | 导出包落在**本地磁盘**(`data/uploads/exports/`),下载走自己的流式接口 | 打包本来就要写盘;七牛私有空间放临时交付物会引入"签名过期"这类与业务无关的失败。将来要上云存储,只需换 `download` 里的 URL 来源 | | D6 | `collect_export_task` 的 `currentStep / stepText / stats.rawSizeMb` **不落库**,由 `status + progress` 推导 / 按需聚合 | 那张表的 DDL 已发布,**没有** `current_step / step_text / raw_size` 三列;为三个展示值新建迁移不划算 | | D7 | 导出前置检查复用 `SubmissionValidator`(与提交时、审核时同一套规则) | 另写一份必然分叉,分叉的表现是"提交时说没问题、导出时说有问题" | | D8 | E2E 每次跑前**重摆演示数据**(`dev-seed-audit.py --reset`) | 第一版脚本第二轮就红:数据已被上一轮审完,`canExport` 自然变 true。夹具状态不对造成的红,看起来像功能坏了 | | D9 | 前端 `ISSUE_SEVERITY` 的口径改为 **1 提示 / 2 警告 / 3 阻断** | 见 F-11:三处注释互相矛盾,以服务端 `Issue.INFO/WARN/ERROR` 常量(数红黄用的那三个)为准 | ### 6.4 本轮新发现的问题(已修 / 待办) | # | 问题 | 处理 | |:--:|---|---| | F-11 | **`SubmissionValidator` 的 severity 口径有三处互相矛盾的注释**:`Issue` 的 JavaDoc 写"1信息 2警告 3错误",而 `Severity` 常量注释写"1=红 2=黄 3=蓝",实际代码是 `INFO=1 / WARN=2 / ERROR=3`(`SubmissionService` 数红黄就用它们)。**我第一版导出前置检查按"1=红"写,会把"提示"当成阻断 → 正常数据也导不出来** | 已改:导出按 `Issue.ERROR` 判定;前端 `ISSUE_SEVERITY` 与详情页配色改为 1 蓝 / 2 黄 / 3 红;`Issue` 的 JavaDoc 补上"不要按直觉改常量"。**建议**:把 `Severity` 常量注释也一起改掉(本轮未动,属纯注释) | | F-12 | **`collect_export_task` 表与契约字段不一致**:契约 §12.5 有 `currentStep / stepText / stats.rawSizeMb`,表里三列都没有 | 不新建迁移,改为推导(D6)。已在实体注释里写明"不要再按契约建字段" | | F-13 | `RevokePolicy` 在"已导出"之后**不返回** `auditRecordId`(只把 `canRevoke` 置 false) | 这不是 bug,但意味着 **`40033` 在界面上不可达**(只会出现 40001「缺少要撤回的记录 id」)。已在 E2E 里按"用户真正看得见的东西"断言,并写清 40033 只在绕过前端时可达 | | F-14 | 已发布的任务**既删不掉也改不了名**(`40001`),演示/测试数据无法清理 | 开发种子脚本改为"SQL 清空 + 改名挪开"(夹具性质,已注明)。**建议**:给管理员留一个"归档后可删"的口径,否则本地库会一直堆任务 | | F-15 | 我第一次汇总时把 753 读成了 771(**我的算术错,不是脚本的问题**)。逐脚本累加可复现:原 18 个脚本合计**恰好 753**,加上新增的 `e2e-m6-export.py`(60)为 **813** | 已更正为 813,并在正文给出"753 + 60"的算式。**建议**:这类数字一律附上算式,别只写结论 —— 否则下一次还是会算错 | | F-16 | ⚠️ **我第一版把导出目录结构做错了**:做成"班级/姓名/文件"(更符合"给人翻"的直觉),而 PRD §5.9 明确是「`{批次号}-{任务名称}` / 班级 / **材料类型** / 文件」,根目录也不是用户填的导出名 | 已改回规范,并把"根目录名、第二层、第三层、汇总表位置"四条都写成 E2E 断言(`e2e-m6-export.py`),另加"预览节点与产物逐项一致"。**教训**:目录结构是**交付物的形状**,属于需求,不能按直觉设计 —— 这类错只在客户解压时暴露 | | F-17 | 汇总表文件名:契约 §12.4 只说"生成汇总 Excel",PRD §5.9 给的是机构叫法 `考生线下报名信息导入模板(汇总).xlsx` | 按 PRD 命名(机构按这个名字找它)。已写进 E2E | | F-18 | `eslint --fix` 会把"靠全角空格对齐"的目录树标记**重排**,并在模板里报 `no-irregular-whitespace` | 目录树改为「半角制表符 + CSS padding」,不再依赖空白对齐。**教训**:凡是要过 lint 的模板,不要用不可见字符表达版式 | | F-19 | ⚠️ **B1 的"审批通过"在实现层走不通**:`collect_roster.id_card_no` 是 **NOT NULL 的 AES 密文列**,而按契约"服务端转 HMAC、不落明文"设计的申请表里**只有 hash 与 mask** → 审批写名册时必然 `null value in column "id_card_no" violates not-null constraint`(500)。三条替代路都被排除:让学生先过门禁(先有鸡还是先有蛋)、让管理员重输(把学生写错一位的风险复制给管理员)、不写这一列(DDL 不允许,且审核看完整号正是解这一列) | 新增 **V7 迁移**给 `collect_join_request` 补一列 `id_card_cipher`(**AES-GCM 密文**,与名册同一把密钥/算法):审批时原样搬进名册,明文全程只在提交请求的内存里存在过。V7 之前提交的老申请会明确提示"请重新提交一次",**不静默建一条没有证件号的名册行**(那条行以后永远过不了名册校验)。**建议**:契约 §8.7.4 的"服务端转 HMAC,不落明文"应改成"存 HMAC + 密文",否则下一个实现的人会再踩一次 | | F-20 | **自述班级被姓名规范化弄坏**:我第一版用 `IdentityNormalizer.name()` 处理 `orgHint`,而那个方法会**去掉空格**(姓名要"张 三"→"张三"),班级名里的空格是**有意义**的("24中技数控1班 645451"去掉空格就再也匹配不上组织)→ 审批时"自述班级自动匹配"永远失败 | 改为只做全角转半角 + 去首尾空白。**教训**:姓名与名称的规范化规则**不能互相借用** —— 一个要去空格,一个不能去 | | F-21 | ⚠️ **进度条永远到不了 100%**:`SubmissionValidator` 的"缺必传材料"那条问题**没有**在 `extra` 里带 `materialCode`(材料名只写在文案里);而 `MaterialDefVO.required=1` 的「工作证明」对在校生**本次不适用**(`visibleWhen: is_student=false`)。我第一版按"所有 required 材料都算分母"算进度 → 学生把 3 项都交了也只显示 **75%**,而且那一项上什么问题都不显示 | ① 进度页用与学生看到/校验用的**同一份装配结果**求条件可见性,不适用的材料不进清单也不进分母;② 问题的归项改为"先看 `extra.materialCode`,没有再按文案里的材料名反查";③ 未交的必传材料直接标 `issueLevel=3`。E2E 断言已改为"在校生只列 3 项"。**教训**:进度条的分母必须与"这个人本次真的要交什么"一致 —— 差一项就是一条永远填不满的进度条 | | F-22 | `collect_roster` 里 `@TableField(exist = false)` 被加在 `BIND_TYPE_GATE` / `BIND_TYPE_ADMIN` 两个 **`static final` 常量**上(本意大概是"这不是列",但静态常量本来就不是实体字段,注解是多余的) | 本轮**未改**(纯冗余、无行为影响)。但新增 `bind_type = 3`(来自加入申请)时**不能照抄这个写法**,已在 `JoinService` 里直接用字面量 3 并注明契约依据 | | F-23 | ⚠️ **后端启动直接崩**:`TeacherDashboardMapper` 的 `