4a5790d900
一、dev-clean-e2e.py:加「在用组织保护」后执行
· 先算「仍被存活任务引用的组织」并保留 —— 当前演示态就挂在其中一个
「E2E 24数控1班」上,删了它演示态就坏(保留 3 个,演示态完好:
任务 1449 / 名册 3 人 / 组织 43 都在)
· 已执行 --apply --demo-tasks --orphan-files:
49 个 e2e 租户 + 147 个测试组织 + 84 个【演示·旧】任务,
共 3714 行 / 27 个文件(业务表按 information_schema 动态枚举,不写死清单)
二、dev-clean-qiniu.py(新增):清真实桶里本项目前缀的测试对象
· 只动 xiaotutongxue/ 前缀;默认 dry-run;删前核实库中零引用、
数量异常(>5000)直接停
· 已执行:2036 个对象 / 116 MB 全部删除,删后复验 0 个(F-93「删库不删桶」)
· 签名按官方文档逐字核对 —— 踩了两个 401 才对:旧 QBox 格式(一个换行 +
"QBox " 前缀)不对,是「两个换行 + Qiniu 前缀」;batch 的 Body 必须进
签名串(列举无 Body 能过、批删全 401 —— 同一签名函数一好一坏,线索在 Body)
· 列举在 rsf.qiniuapi.com,不在 rs(打错 host 报「Bad method: list」)
三、xt-minio 容器已删(F-2)—— 代码早已不用 S3,deploy 里也没有它
四、文档核销与记录
· docs/08:F-3/4/7/10(小节)与 F-14/26/29/30/31/38/51/57/64/65/71/72/93
(表格行)全部标「已闭环」;F-5 补执行记录;F-28 备案维持两级(→D-11);
变更记录补两行 —— **F-1~F-100 至此无未决项**,剩余全部等外部输入
· docs/01:新增 E4「国标职业库数据来源待客户确认」(F-34,字典页如实显示 0 项)
· 交接摘要:新增 §一·三十一(17 项决策执行记录)
· AGENTS.md:清理一节写清三个工具与「在用组织保护」
验证:python scripts/verify-all.py --with-e2e →
通过 8 / 失败 0 / 跳过 0(37 个 E2E 脚本 / 1577 条断言 / 0 失败,291s;
prodguard 9/9;test profile 能起)
550 lines
135 KiB
Markdown
550 lines
135 KiB
Markdown
# 完成度盘点与问题清单(施工计划)
|
||
|
||
> 目的:**给"还剩什么、先做哪个、每个问题怎么改"一份可直接照着做的清单。**
|
||
> 口径:以**代码与实测**为准(文档是需求源,但与代码冲突处以代码为准,冲突项在 §五 单列)。
|
||
> 盘点时间: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. 若后续要恢复直传(大文件/弱网),再单独立任务,不要在这轮混做。
|
||
|
||
- ✅ **已闭环(2026-09-30,客户确认后按建议改文档)**:§9.5 重写为「上传前置检查(预检/秒传/压缩建议)」、§9.6 改为「有原始件时的重新规范化」、§9.9 标注一期缓做 —— 口径见契约 §17.2 D-3/D-4/D-12。
|
||
|
||
### F-4【P1 · 文档与代码不一致】契约里没有的接口(首次盘点 19 个,现在 41 个) —— **已闭环**
|
||
|
||
- **现象**(代码有、03 文档 §4 清单没有):最典型的是 `GET /mp/scene-context`、
|
||
`GET /auth/captcha`、`GET /mp/h5/authorize|callback|stub-login`(代替 `/mp/login`)、
|
||
`POST /mp/files/upload`(代替 `upload-token` + 直传)、`GET /material-catalog`……
|
||
完整清单**现跑**:`python scripts/check-api-parity.py --extra`(当前 **41 个**)。
|
||
- **影响**:契约不是"当前系统长什么样",导致每次核对都要重新考古;前端/测试无法以文档为准。
|
||
⚠️ 更要紧的是:**这个数会继续长**(每加一处界面必需接口就 +1),
|
||
所以它必须以命令的形式存在,不能以表格的形式存在 —— 见 §九。
|
||
- **建议**:在 03 文档**新增一节「§17 实现补充与偏差记录」**,逐条列出这些接口(路径 / 权限 / 是否要补进 §4 清单),并在 §4 的合计口径里说明"121 是设计口径,另有 N 个实现补充"。改动后必须重跑 `python scripts/verify-docs.py`(它逐节核对接口数,加节时注意别改动 §4 的声称数)。
|
||
|
||
- ✅ **已闭环(2026-09-30)**:契约新增 **§17 实现补充与偏差记录** —— 41 个接口按六类分组(清单由 `check-api-parity.py --extra` 生成,写明「手抄件会过期,以命令输出为准」),外加 12 条已确认偏离 D-1~D-12(含租户域名一行、`biz_type` 落库字符串、§14 路径前缀)。
|
||
|
||
### F-5【P2 · 数据卫生】E2E 租户残留到"每个请求多一次解析" —— **清理工具已补齐(2026-09-30)**
|
||
|
||
- **现象**:`sys_tenant` 17 行、`sys_user` 300 行,而脚本每次跑都会建测试租户,清理只删自己建的那一个。
|
||
⚠️ 现在回头看,这个数字被低估了:跑了几十轮之后是 **48 个 `e2e*` 租户 / 709 个用户 /
|
||
144 个挂在 demo 租户下的测试组织 / 82 个【演示·旧】任务**。
|
||
- **影响**:本地演示/截图时列表里全是 `e2e-*` 名字;租户域名解析与登录"跨单位重名"分支被无意义数据搅扰。
|
||
- **真正的窟窿(这一轮才看清)**:**删库不删盘**。`backend/data/uploads` 里 **281 个文件 / 276 MB**,
|
||
而库里只剩 16 行文件 —— 压测与 E2E 都只删自己的库行。
|
||
文件的归属**只有库知道**,所以这件事必须由"拿库做对照"的工具来收,不能靠 `rm -rf`。
|
||
- **已做**(`scripts/dev-clean-e2e.py`,**默认 dry-run**):
|
||
① `--only-orphan-files --apply`:删**没有任何库行指向**的磁盘文件 —— **已执行,255 个 / 275.4 MB,
|
||
uploads 从 276 MB 降到 0.3 MB**;
|
||
② `--apply`:清 e2e 租户 + 测试组织 + **它们的业务行**(业务表按 `information_schema` 动态枚举,
|
||
以后新增业务表不会漏);③ `--demo-tasks`:清【演示·旧】任务及其子表与文件。
|
||
- ⚠️ **后两项我没有执行**,原因具体:**当前演示态正挂在其中一个"E2E 24数控1班 645451"组织上**
|
||
(就是导出目录里那个班级名),删了组织等于把演示态弄坏。所以:
|
||
要清就先确认演示态迁到正式组织名下,再跑 `--apply`。
|
||
我也因此把 `--demo-tasks` 的匹配**收窄到只带「旧」的那批** ——
|
||
`【演示】审核台` 是当前在用的,原来那句 `LIKE '【演示】%'` 会把它一起删掉,
|
||
而这种错在预览里只表现为"83 个"(比 82 多一个),**极容易被放过**。
|
||
- **建议**:把 `--only-orphan-files --apply` 写进"跑完压测/E2E 的收尾动作"(见 `AGENTS.md` 第三节)。
|
||
- ✅ **已执行(2026-09-30,含「在用组织保护」)**:`--apply --demo-tasks --orphan-files` 清掉 **49 个 e2e 租户 + 147 个测试组织 + 84 个【演示·旧】任务,共 3714 行、27 个文件**;清理前先算「仍被存活任务引用的组织」并保留(**3 个**,其中就有当前演示态挂着的 E2E 24数控1班 —— 演示态完好:任务 1449 / 名册 3 人 / 组织 43 都在)。另外 **xt-minio 容器已删**(F-2)、**七牛桶已清**(F-93)。
|
||
|
||
### F-6【P2 · 文档误导】`frontend-pc/AGENTS.md` 里的 Mock 说明已过时 —— **已修(2026-09-30)**
|
||
|
||
- **现象**:该文件写「开发默认 Mock(`/mock` 前缀)」,而 `AGENTS.md` 已注明"已过时,没有 Mock,dev 走 vite proxy 到 8080"。
|
||
- **影响**:AI/新人按旧说明写 Mock 代码或排查"为什么没有 `/mock`"。
|
||
- **已修**(按下面的建议做的,但**多修了一处**,见"连带"):
|
||
`frontend-pc/AGENTS.md` 的 `pnpm dev` 注释与"开发默认 Mock"那句,
|
||
改为"`/api` 代理到 `http://localhost:8080`,没有 Mock → **后端不起着就没有数据**",
|
||
并写明我们的接口模块在 `src/apis/xt/`。
|
||
- **连带(这才是根子)**:`frontend-pc/.agents/skills/project-api/SKILL.md` 整篇仍以
|
||
`vite-plugin-mock` / `mock/**` / `defineMock` 为主线 —— 而**该项目里 `mock/` 目录与插件都已删除**,
|
||
照它做会去建一个没人加载的 `mock/` 目录。只改 `AGENTS.md` 那句等于"把指路牌改了、
|
||
但路的尽头还是错的",所以在那份 skill 开头加了 ⚠️ 说明(保留原文,供 Gi 模板升级时做差异比对)。
|
||
核实方式:`frontend-pc/` 下无 `mock/`,`package.json` 与 `vite.config.ts` 里都没有 mock 关键字。
|
||
|
||
### F-7【P2 · 一致性】撤回的动作码与适用状态 —— **已闭环**
|
||
|
||
- **现象**:DDL 注释 `3=批量通过` 与 03 §11.4「写入 `action = 3`(撤回)」冲突,代码取 `7`(已按"时间轴不能混淆"处理);§11.4 的状态表只列状态 3,代码额外允许状态 2。
|
||
- **建议**:客户确认后同步两处文档;若客户不同意状态 2 的撤回,删一个分支即可(代码里有注释标明)。
|
||
|
||
- ✅ **已闭环(2026-09-30,客户确认按代码口径)**:§2.7 `auditAction` 补全 7 个动作(撤回=7),§11.4 约束表补「状态 2 可撤(撤的是初审)」、留痕改 `action=7`。DDL 注释少列 7(迁移已发布不改,以 §2.7 为准)。
|
||
|
||
### 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 步",避免将来重开时找不到入口。
|
||
|
||
---
|
||
|
||
- ✅ **已闭环(留一行入口,按建议)**:契约 §17.2 **D-10** 写明「能力已实现(Host 定位 + token 授权),默认关闭,启用四步见《交接摘要》§一·二十六」。
|
||
|
||
## 六、施工记录(本轮自主完成的事)
|
||
|
||
> 客户要求:**"完成所有的开发计划,中途发现问题自己决定,记录出来让我知道即可。"**
|
||
> 本节的每条都带**验证方式**,不写"已完成"这种没有证据的话。
|
||
|
||
### 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 清空 + 改名挪开"(夹具性质,已注明)。**建议**:给管理员留一个"归档后可删"的口径,否则本地库会一直堆任务 ✅ 已闭环(客户确认「归档后可删」):`DELETE /tasks/{id}` 支持 status=3 —— **级联物理删** 14 张表(刻意不软删:本条的动机就是库会一直堆)+ 尽力删物理文件 + `sys_operation_log` 记 `DELETE_TASK`;收集中/已截止仍拒删(归档是最后的后悔药)。验证:新增 `scripts/e2e-m1-task-delete.py` → **19/19**。 |
|
||
| 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` 的 `<script>` 动态 SQL 里写了裸 `<>`(SQL 的"不等于"),而 `<script>` 块要按 **XML** 解析 —— `<` 必须转义成 `<`。错误是 `SAXParseException: 元素内容必须由格式正确的字符数据或标记组成`,**指不到具体那一行 SQL**,而我第一反应是去查 SQL 逻辑(走了一段弯路)。更危险的是:`mvn package` **照样成功**,只有启动时才炸 | 改成 `<>`(SQL 标准的"不等于"),并顺手扫了 `module/mp/mapper` 下所有动态 SQL,确认没有别的裸 `<`。**教训**:带 `<script>` 的 Mapper **编译期不校验 XML**,改完必须**真启动一次** —— 这类错的表现是"服务起不来",不是"接口报错" |
|
||
| F-24 | ⚠️ **`collect_message` 少了两列审计字段**(`create_by` / `update_by`):实体继承 `TenantEntity`,而 `FillMetaObjectHandler` 会填这两列 → 插入报 `column "create_by" of relation "collect_message" does not exist`,**站内信一条都写不进去**。这个坑在 `collect_task_org` / `collect_export_task` 上都踩过,是第三次 | V8 **还没发布**(本轮才加、且只在开发库跑过一次),所以直接在迁移里补上两列——而不是像前两次那样"去掉实体字段"。**判断依据**:这张表以后要按 `create_by` 查"谁发的消息",少这列以后要再加就麻烦了。**教训**:新建业务表先问一句"它是 `TenantEntity` 还是普通实体",然后照着列清单核对一遍 |
|
||
| F-25 | ⚠️ **`@Update` 里的 `<` 不会被还原成 `<`**:`SubscribeQuotaMapper.consume` 写的是 `AND used < total`,而**没有 `<script>` 包裹的注解 SQL 不经过 XML 解析** → 字面量 `<` 原样发给 PG → `syntax error at or near "&"`。表现更绕:这个异常在 `NotificationService` 里被捕获降级成"仅站内信",所以**用户侧完全正常**,只有 `collect_notify_record` 里一行都没有 | 外面套一层 `<script>`(本意不是为了动态 SQL,就是为了转义),并写进注释。**连带教训**:这类"被吞掉的异常"最难查 —— 我一开始以为是"没被调用",查了三轮日志才看到真正的堆栈。**排查手法**:先把被吞的异常用 `System.err` 打出来,别信日志配置 |
|
||
| F-26 | ⚠️ **`collect_notify_record.biz_type` 是 `varchar(32)`,不是数字**:契约 §6 的字段表把 `bizType` 定义成数字码(1~7),而 V1 的 DDL 建的是 varchar + 注释只写"业务类型"。传 Integer 进去 PG 直接拒绝(`column "biz_type" is of type character varying but expression is of type integer`)—— 同样被降级逻辑吞掉,表现还是"表里一行都没有" | 按 DDL 走:`biz_type` 存**数字码的字符串形式**(`"3"`),接口层仍然是数字(契约不变)。`collect_message.biz_type` 我用的是 `SMALLINT`(新表,我自己定的),两者"A 通过 bizType 关联"依然成立。**建议**:契约 §6 补一句"`collect_notify_record.biz_type` 落库为字符串" ✅ 已闭环:契约 §17.2 **D-8** 写明「落库为数字码的字符串形式(varchar 列),接口层仍是数字」。 |
|
||
| F-27 | `notifySent` 的语义**被收窄了**:以前它表示"有没有通知渠道"(恒 `false`),现在表示"**微信推送**有没有真的发出去"。站内信一定写得进去,所以 `needManualNotice` 仍然可能为 `true`(学生不一定会进小程序) | 已在代码里写清,并且**没有**顺手把 `needManualNotice` 改成恒 `false`。E2E 断言"降级时如实告知:notifySent=false / needManualNotice=true",防止后人把它"优化"成乐观值。**这是对前端契约的语义变更,已在本文档记录** |
|
||
| F-28 | **降级链只做了两级(订阅消息 → 站内信),中间两级没做**:契约 §13.6 写的是「订阅消息 → 公众号模板 → 短信 → 仅站内信」。后两级的缺不是代码问题 —— 公众号模板消息要**已认证服务号 + 用户已关注**(一期没有公众号),短信要买通道与签名(按条计费,是商务动作)。我选择**只实现能真正走通的两级**,而不是写两级"看起来有、其实永远失败"的通道 | 在 `NotificationService` 的类注释里写清了为什么少做,并用 `channel` 字段如实标出这条消息最终怎么到的。**理由**:永远失败的通道会往日志里刷一片失败,把真正的原因(没配通道)淹掉,而运营看到"通知失败率 90%"会去查代码,实际上该去找商务。 ✅ **已决策(2026-09-30 客户确认:一期维持两级)**,备案进契约 §17.2 **D-11**;接公众号模板/短信是商务动作(认证服务号、短信通道采购),不是代码欠账 |
|
||
| F-29 | **催报的分钟级限流还没做**:契约 §3 的表里写了「催报 10 次/分钟/任务」,本轮只做了 Service 层的**去重**(`excludeRecentHours`,默认 24 小时),没有做分钟级限流。**两者的作用不同**:去重防的是"同一个学生被重复催",限流防的是"老师连点把系统打爆"。现在连点 100 次不会造成伤害(第二次起全被去重跳过),但会白跑 100 次全表查询 | 记为 P1 剩余项(与 §3 的其它限流一起做)。**不做的理由**:现在没有真实并发压力,而限流放哪个维度(租户/用户/任务)需要与 §3 的其它限流统一,单独加一个会分叉 ✅ 已闭环:补上**分钟级限流**(10 次/分钟/任务,超限 `42901` 且话说清等多久;Redis 不可用时放行不瘫痪);`e2e-m5-urge.py` → **42/42**(新增「第 11 次被限流」断言)。 |
|
||
|
||
| F-30 | ⚠️ **契约里 `notifyBizType` 的 5/6 是对调的**:§259 那行写「`5` 导出完成 / `6` 催报」,而 §2655 的消息中心示例是「`bizType: 2` 临期提醒、`bizType: 3` 驳回」—— 后者能与代码对上,所以不是我看错行。**后果很具体**:我第一版按契约写 `biz_type = '6'` 做催报去重,而实际写入的是消息中心的 5,于是去重**静默失效**(老师点两下学生收到两条),排查时完全看不出是"码对不上" | 统一按**消息中心的码**(`5` 催报 / `6` 导出完成,见 `CollectMessage` 的常量与 `UrgeService.BIZ_URGE` 的注释),并把不一致记在这里。**建议**:契约 §6 的 `notifyBizType` 与 §5.9/§13.5 对齐成同一套码 —— 改哪一边要客户确认 ✅ 已闭环:客户确认**以 §13.5 为准**(5 催报 / 6 导出完成),§2.7 表已改,全文档统一。 |
|
||
| F-31 | **登录日志只有 PC 账号密码登录**(`login_type` 恒为 1,库里 1101 行全是 `PASSWORD`):学生从小程序/H5 进来走的是**微信授权**(`/mp/h5/*`),那条链路**不写 `sys_login_log`**。所以按 §13.4 查"学生登录记录"会是空的 | 本轮**没有补**(补它要动 MP 授权链路,属于另一处改动),而是在接口与页面的说明里**如实写清**这是已知边界 —— 否则运维会以为日志功能坏了。记为 P1 剩余项。**风险**:合规上"谁在什么时候登录过"对学生侧是缺口 ✅ 已闭环:新增 `WechatLoginLogger`,小程序登录与 H5 授权回调/打桩登录都写 `sys_login_log`(`login_type=2` 与 DDL 注释对齐;H5/小程序共用 2,靠 userAgent 区分);`e2e-m3-h5-auth.py` → **24/24**。 |
|
||
|
||
| F-32 | ⚠️ **"发布"与"恢复"都是 `→收集中`**:我第一版让 `patchStatus` 从 `(原状态, 新状态)` **反推**动作名,于是 `resume` 被记成了 `PUBLISH_TASK` —— 而合规要区分的恰恰是"这个任务是第一次发布,还是被暂停后又恢复的"(恢复意味着它曾经被暂停,那段时间学生的填报是被关掉的) | 改成**由调用方显式给动作名**(`patchStatus(id, status, ACTION_*)`),并补了契约表里没有但必须有的两个动作:`RESUME_TASK` / `CLOSE_TASK`。**教训**:凡是从结果反推原因的地方,遇到"多对一"就一定会错 —— 而这类错通常表现为"日志看起来有、但答案是错的",比缺日志更难发现 |
|
||
|
||
| F-33 | **`publish` 原来没走 `patchStatus`**(自己 `updateById` 设状态 + `published_time`):把留痕集中到 `patchStatus` 之后,才发现"发布"这条**最该留痕的动作**被漏掉了 —— 而它正是任务开始对外收集的时点 | 已改为走 `patchStatus`,并把 `published_time` 的写入一起收进去(否则会出现"状态是收集中但没有发布时间"的行,而没人说得清是怎么来的) |
|
||
| F-34 | **`sys_occupation_ref`(国标职业库)一期是空的**(0 行):契约 §13.3 把 `OCCUPATION` 字典的来源指定为这张表,而它是**国标数据** —— 要客户提供或采购后导入,不是我们能生成的。所以现在字典页里「职业(国标)」显示 0 项 | 在字典页与接口说明里**如实写清**这是已知情况(否则用户会以为功能坏了),并把 `COUNT(*)` 直接当作 `itemCount`(有数据时自动就对了,不需要改代码)。**建议**:正式实施前确认国标职业数据的来源与授权 |
|
||
| F-35 | ⚠️ **F-24 第三次复发**:`collect_export_file`(V11 新建)又漏了 `create_by` / `update_by` —— 实体继承 `TenantEntity`,`FillMetaObjectHandler` 会填这两列,于是插入报 `column "create_by" ... does not exist`。**这个坑已经踩了四次**(`collect_task_org` / `collect_export_task` / `collect_message` / `collect_export_file`),每次的现象都长得像"SQL 写错了",而实际是建表漏列 | 修了 V11 之后,**把这件事变成机械检查**:新增 `scripts/check-entity-columns.py`,从 Java 实体读 `@TableName` 与基类、从迁移 SQL 读建表列,比对"基类要求的列在不在",并额外查"普通实体带 `@TableLogic` 但表里没有 `deleted`"(那是同一类错的另一种形态)。**教训**:一个错犯第二次就该上工具,第三次说明"记住"这条路是走不通的 |
|
||
|
||
| F-36 | **写机器检查本身也会假阳性**:`check-entity-columns.py` 第一版直接搜 `@TableLogic` 字符串,结果把 4 个实体的**类注释**也算进去了 —— 而那四段注释恰恰在解释"为什么不继承基类"(里面当然会出现 `@TableLogic` 这个词)。表现是"4 个实体报错",而我差点据此去改那四个**本来就是对的**实体 | 改成**先去注释再找注解**。**教训**:静态检查一定要能区分"代码"与"解释代码的文字" —— 否则它会指向正好写着"这里不该这么做"的地方 |
|
||
|
||
| F-37 | **`@Scheduled` 的定时任务在测试里等不起**:导出包过期清理每小时跑一次,而验证"过期之后下载会怎样"总不能把过期时间改成 7 天后再等一小时 | 加了 `POST /exports/cleanup-run`(手工触发,权限同导出管理员)。清理本身**可重复执行**(已清理的卷会跳过),所以手工触发没有副作用。**这是本项目第一个定时任务**,所以顺手在 `FillApplication` 上写了"别顺手加更多:定时任务是「没有请求也会发生的事」,每加一个都要能回答「它失败了谁会知道」" |
|
||
| F-38 | ⚠️ **`collect_file.source` 的口径在两份文档里冲突**:PRD 的建表注释(第 1894 行)是「1 本人上传 / 2 班主任代传 / 3 管理员代传 / 4 整理器导入」,而接口设计 §3.6 的字段表(第 464 行)是「1 聊天记录 / 2 相册 / 3 手机文件 / 4 代传」。两者都是 4 个值但含义完全不同 | **以 DDL 为准**(它是"这一列表示什么"的定义;已有数据全为 1 与它一致;PC 端 `FILE_SOURCE` 常量已按它写),并在列注释里写明冲突 —— 见 F-39 的同类处理。**建议**:契约 §3.6 的字段表要改成 DDL 口径,否则下一个人会按字段表写 ✅ 已闭环:客户确认**以建表注释为准(谁传的)**,§3.6/§9.5 字段表已改。 |
|
||
|
||
| F-39 | ⚠️ **`collect_file.source` 从来没有被代码显式写过**:库里那 10 行全是**列默认值 1**,也就是「学生自己传的」与「老师代传的」在数据上**完全区分不出来**。而契约 §10.6 明确要求代传必须留痕(`source = 4`/我按 DDL 用 2) | V12 起写入侧显式赋值:学生上传写 1、代传在插入后改成 2 并记 `proxy_by` + `proxy_reason`。**为什么还加 `proxy_by`**:`source=2` 只说"是班主任代传的",说不出**是哪个**班主任、**为什么**替他传 —— 而契约要求时间轴显示「由李老师代传」,名字只能存在这里 |
|
||
|
||
| F-40 | ⚠️ **`precheck.canExport` 在"没有任何候选"时仍然为 true**:`canExport = blocking.isEmpty()`,而一个"没人提交过"的任务 `blocking` 当然是空的(没有文件就没有不合格文件)→ 按钮被点亮,点下去却报"没有可导出的数据"。**这直接违反契约 §12.4 的铁律**(`canExport=false` 时前端禁用按钮)——反过来说,按钮亮着就该能导出 | 修成"没有阻断项 **且** 有东西可导",并把「还没有人提交填报单」也放进 `blocking`(前端只读它渲染"卡在哪",不给理由会让人以为系统坏了)。E2E 加了回归防线 |
|
||
|
||
| F-41 | ⚠️ **`onlyApproved` 在前置检查里根本没生效**,而阻断项给用户的建议正是「也可以改选『只要已通过的』」—— **照着提示做仍然导不出来**,用户会以为系统坏了。而打包那一步(`pack`)是看 `onlyApproved` 的,所以两处口径原本就不一致 | 让 `precheck` 也认 `onlyApproved`:勾了之后未审完的人不进包、也不再阻断,但**必须留一条 warning 说清"有几个人因此没进包"**(静默少人才是真危险:客户拿到包会以为人齐了)。E2E 加了回归防线 |
|
||
|
||
| F-42 | ⚠️ **E2E 的 cleanup 静默失败**:`R.sql(...)` 的返回值一直没人看,而"已发布的任务删不掉"这件事没被处理 → 残留的 roster 行让后面的脚本(`e2e-m5-teacher-dashboard.py`)算出错误人数(应填 3 变 4),三条断言失败。**排查时完全看不出是"上一个脚本没清干净"** —— 更难的是:单跑那个脚本是通过的,只有全量回归才红 | 把 cleanup 改成**可审计**:按依赖顺序删 → 逐条记结果到 `scripts/_e2e-clean-report.txt` → 末尾**校验一行都没剩** → 有问题就打印出来。**教训**:清理是测试的一部分,"清理失败"必须和"断言失败"一样可见 |
|
||
|
||
| F-43 | 门禁核验**按用户 1 小时 10 次**限流,而演示脚本每次都用同一批演示 uid ——一个小时内跑到第 4 次就撞 `40071`,整脚本 SystemExit。表现极具误导性:调用它的 E2E 报"演示数据没摆成" | 加 `dev-seed-audit.py --reset-gate-limit`(显式开关,**不**默认清 ——限流是生产行为,不该被脚本顺手改掉)。**连带踩到的**:`xt-redis` 开了 `requirepass`,而 `subprocess.run(capture_output=True)` 不看返回码 → 表现为"清了但没清掉" |
|
||
| F-44 | ⚠️ **`TaskService.create` 对缺必填项抛 NPE(对外 50000)而不是校验错误**:`req.getMajorName().trim()` 在 `majorName` 为 null 时抛 NullPointerException,而 `GlobalExceptionHandler` 把它变成「系统繁忙,请稍后再试」—— 真正的原因是"你没填专业名称"。**这类坑每个字段都会踩一次** | 在 `create` 开头集中判 `taskName`/`batchNo`/`majorName` 三个必填项并给出字段名。(发现路径:历史整理器导入时没传 majorName → 500,翻日志才看到 NPE) |
|
||
|
||
| F-45 | ⚠️ **`collect_task.task_code` 是 NOT NULL 且由 `TaskService` 生成** —— 绕过它直接 `taskMapper.insert` 会报 `null value in column "task_code"`,对外同样是 50000 | 历史整理器的导入改成**走 `TaskService.create`**(复用编码生成与校验),而不是自己再写一份编码逻辑 —— 两处各生成一次编码迟早会撞号 |
|
||
|
||
| F-46 | ⚠️ **`FileProcessService.renderName` 的 `ext` 参数必须自带点号** (既有调用点是 `"." + ext`)。传裸扩展名**不报错**,只会产出「身份证jpg」这种文件 —— 文件系统按无扩展名处理,客户双击打不开。**少一个字符的错误不会报错,只会让交付物不可用** | 调用点改成 `"." + ext`,并在注释里写明这个约定 |
|
||
|
||
| F-47 | ⚠️ **异步线程里回查"刚插入的那一行"必然查不到**:`create()` 带 `@Transactional`,插入 job 后立刻投递异步任务,而异步线程与请求线程是**两个事务** —— 请求线程还没提交,异步线程查不到 → 「整理任务不存在」,整次整理白跑。**这类时序问题不能用 sleep 解决**(只是把概率变小) | 让异步方法**直接收 job 对象**(调用方手里就有),去掉"回查"这个动作本身 |
|
||
|
||
| F-48 | ⚠️ **"候选名判定"用 `contains` 会吃掉真名**:解析历史文件名时,噪声词表里只要有「整理」这种 2 字词,`contains` 判定就会把「**整理**甲」当噪声滤掉 —— 表现是**所有文件都归不了位**(报告里一堆 orphan,串档检测也失去基准)。我先后写了两版都栽在同一个地方(第二版留了"≤2 字词可被包含"的例外,而"整理"正好 2 个字) | 判据彻底改成:**噪声词只做精确匹配**;"是不是材料/班级词"分别由材料词表与「**完整**中文串长度 2~4 + 不含数字 + 不含班级词」判断(`{2,4}` 截断也会把「新建文件夹」切成「新建文件」而当成人名)。**教训:中文人名与业务短词无法用"包含"区分** |
|
||
|
||
| F-49 | ⚠️ **参数校验排在幂等判断之后就等于没校验**:整理结果导入的 `importFiles=true`(一期做不到,本该 40044)在**第二次**导入时被安静接受 —— 因为请求先命中了"已经导过了,直接返回原任务"。**一次不合法的请求不该因为"恰好之前导过一次"而变成成功** | 参数校验提到幂等判断之前 |
|
||
| F-50 | ⚠️ **`collect_validation_issue` 与 `collect_rule_template` 两张表 V1 就建好了,却一直没有代码用它们**(连 `expected_val`/`actual_val`/`auto_fixed`/`resolved_by` 都有,与契约 §3.9 逐列一致)。也就是"当初就设计好了",只是 §12.1~12.3 三个接口没做 —— 而盘点工具只比"契约 vs 控制器",**看不到"表建了但没人用"** | 本轮把问题清单做完;`collect_rule_template`(§4.3 的规则模板 2 个接口)仍待做。**教训**:盘点时要连"迁移里建好的表有没有代码在用"一起算,否则会重复设计 |
|
||
|
||
| F-51 | ⚠️ **契约的 `POST /validation/run` 声明 `async` 默认 true 并返回 `jobId`,但整个契约没有"查作业状态"的接口** —— 那个 jobId 查不到任何东西 | 一期**同步跑完**(实测 1 份填报单 <100ms),`jobId` 只表示"本次运行",并**加返回** `checkedSubmissions / issuesFound / unresolvedLeft / elapsedMs`,让界面能立刻显示结果 —— 比返回一个查不到状态的作业号有用(加字段记在 F-52) ✅ 已闭环:客户确认**改同步**并返回统计字段(`checkedSubmissions/issuesFound/unresolvedLeft/elapsedMs`),§12.1 已改。 |
|
||
|
||
| F-52 | ⚠️ **契约 §12.7 的模板接口链断在第一步**:`GET /notify/templates` 返回的内置模板**没有 id**(它不在库里),而 `PUT /notify/templates/{id}` 要 id —— 用户点"编辑"时前端根本拿不出一个 id。文档上看起来是通的 | 补 `POST /notify/templates/{code}/customize`(把内置文案复制成库里一行,幂等)与 `DELETE /notify/templates/{code}`(恢复默认);另补 `GET /notify/templates/variables`(变量白名单必须服务端给,前端抄一份迟早不一致)与 `POST /notify/send` 的`rosterIds` / 整任务发法 |
|
||
|
||
| F-53 | ⚠️ **微信回调必须偏离两条全局约定**:①它是**匿名**入口(微信不带我们的登录态);②响应体是微信格式 `{"errcode":0}` 而不是本项目的 `{code,msg,data}` | 两条都写进注释,并说明权限从哪来:**只写推送状态、不读业务数据** + **必须验签**(缺 `fill.mini-program.callback-token` 时明确拒绝,而不是跳过验签)+ **凭发出时记的 recordId/MsgID 反查租户**(查不到就报错,绝不猜一个租户去写 —— 写错租户等于把数据丢进别人家) |
|
||
|
||
| F-54 | ⚠️ **PostgreSQL 把未加引号的别名折叠成小写**:`rule_code AS ruleCode` 返回的键其实是 `rulecode`,Java 侧 `m.get("ruleCode")` 拿到 null —— **对外表现是"按规则分组"整块全 null**,而 SQL 完全正确、日志里查询成功、行数也对 | 别名写成 `AS "ruleCode"`。这条对任何用 `Map` 接的查询都适用 |
|
||
|
||
| F-55 | ⚠️ **PostgreSQL 推断不出 `? IS NULL` 里参数的类型**:`WHERE (#{taskId} IS NULL AND task_id IS NULL OR task_id = #{taskId})` 报 `could not determine data type of parameter`(对外 50000)| 改用 `<script>` + `<choose>`,让传 null 时那个参数**干脆不出现在 SQL 里**。**教训**:注解 SQL 里的"可选条件"不能用"参数与 NULL 比较"的写法 |
|
||
|
||
| F-56 | ⚠️ **软删除与业务唯一索引打架**:`@TableLogic` 让 `delete()` 变成 `UPDATE deleted=1`,而 `uk_notify_tpl` 唯一索引**不看 deleted** → "恢复默认 → 再自定义"撞唯一约束(500)。**这类冲突在"软删除 + 业务唯一键"的组合下必然出现**,只是要到第二次操作才暴露 | 模板覆盖行改为**物理删除**。**判断依据**:这张表的行是"配置"还是"业务数据" —— 前者的历史价值低、而唯一键冲突的代价高 |
|
||
|
||
| F-57 | ⚠️ **`PUT /tasks/{id}/form-schema` 是有的**(`FormSchemaService.save` 实现了),但**规则模板的 `FORM` 部分没有落地的应用路径** —— `RuleTemplateService` 只能"跳过并说明原因"。所以模板的 FORM 部分现在等于装饰 | 如实 skipped + 原因(不假装套用了)。**建议**:下一步把「应用表单定义」接上,或把模板的 FORM 部分从契约里去掉 ✅ 已闭环:`FormSchemaService.saveRawVersion`(版本化保存只有一份),`RuleTemplateService` 的 FORM 分支真正应用;`e2e-m7-taskconfig.py` → **75/75**。 |
|
||
|
||
| F-58 | ⚠️ **契约 §7.13 只给了「列表」与「应用」,没有「保存为模板」** —— 而没有它,"用户自建模板"永远是一句空话:模板只能靠改库或写迁移产生,而 §7.13 明确说列表要"含公共模板与**租户自建**" | 补 `POST /tasks/{id}/save-as-template`,并加边界:**只有平台管理员能把模板设为公共**(公共模板对所有租户可见,否则任何一个学校都能把自己的配置推给所有人 —— 那不是权限问题,是产品问题) |
|
||
|
||
| F-59 | ⚠️ **公共模板会被租户拦截器挡掉**:规则模板的公共模板是 `tenant_id = 0`,而拦截器会给查询补 `tenant_id = <当前租户>` → 公共模板一条都查不出来,**而表现是"列表空着但不报错"**(功能看起来没做) | `RuleTemplateMapper` 用 `@InterceptorIgnore(tenantLine="true")` + 手写 `(tenant_id = 本租户 OR (is_public = 1 AND tenant_id = 0))`,并把"关掉框架保护之后由这一行保证隔离"写进注释(与 `MpSceneMapper` 同一做法)。**这是"默认拒绝"体系里必须主动开的口子,而不是顺手关掉** |
|
||
|
||
| F-60 | ⚠️ **Excel 索引调色板里同一个红色有两个下标**:`FF0000` 既是 2 也是 10,而 POI 的 `IndexedColors.RED` 用 10、openpyxl 的 `COLOR_INDEX` 第一次出现是 2 —— **写死任何一个都会在另一边红**。这条本身只是测试断言的问题,但它暴露了更普遍的一类:**跨库的"同一个常量"要按值查、不要按位置查** | E2E 改成按索引**反查颜色值**再比较(`COLOR_INDEX[index]`) |
|
||
| F-61 | ⚠️ **初审待办会列出"我批不了"的单子** —— 待办与审批用了**两套范围判据**:待办按 `s.org_id IN orgIds`,而 `orgIds` 在 `taskId` 为空时是「我在**任意任务**里负责过的班级」;审批按「这份单子**所属任务**里这个班归不归我」。于是「我在任务 A 是班级 43 的初审人、在任务 B 不是」时,待办照样列出任务 B 的单子,点通过回 `40301`。**真实场景里"同一个班在不同任务的初审人不同"很常见**(换班主任、分批指定),所以这不是脏数据才有的问题 —— 而表现是"我权限没配好",用户只能一条条点、一条条被拒(最糟糕的那种交互) | 待办改成与审批**同一套判据**:`EXISTS (select 1 from collect_task_org cto where cto.task_id = s.task_id and cto.org_id = s.org_id and 我是它的初审人)`。改动覆盖 `pageTodos` 与 `counts`(含四个档位计数),并在 E2E 里加了**性质断言**(待办里每一条都必须通过同一套 SQL 判据),而不是"待办有几条"这种随夹具变化的断言 |
|
||
| F-62 | ⚠️ **契约 §7.13 只给了「列表」与「应用」,没有「保存为模板」** —— 而没有它,"用户自建模板"永远是一句空话:模板只能靠改库或写迁移产生,而 §7.13 明确说列表要"含公共模板与**租户自建**" | 补 `POST /tasks/{id}/save-as-template`,并加边界:**只有平台管理员能把模板设为公共**(公共模板对所有租户可见,否则任何一个学校都能把自己的配置推给所有人 —— 那不是权限问题,是产品问题) |
|
||
| F-63 | ⚠️ **公共模板会被租户拦截器挡掉**:规则模板的公共模板是 `tenant_id = 0`,而拦截器会给查询补 `tenant_id = <当前租户>` → 公共模板一条都查不出来,**而表现是"列表空着但不报错"**(功能看起来没做) | `RuleTemplateMapper` 用 `@InterceptorIgnore(tenantLine="true")` + 手写 `(tenant_id = 本租户 OR (is_public = 1 AND tenant_id = 0))`,并把"关掉框架保护之后由这一行保证隔离"写进注释(与 `MpSceneMapper` 同一做法)。**这是"默认拒绝"体系里必须主动开的口子,而不是顺手关掉** |
|
||
| F-64 | ⚠️ **`PUT /tasks/{id}/form-schema` 声称实现,但规则模板的 `FORM` 部分没有落地的应用路径**:`RuleTemplateService` 只能"跳过并说明原因"。所以模板的 FORM 部分现在等于装饰 | 如实 skipped + 原因(不假装套用了)。**建议**:下一步把"应用表单定义"接上,或把模板的 FORM 部分从契约里去掉 ✅ 已闭环:同 F-57;顺带补上「存为模板**也存表单**」(此前存模板不存表单、套模板用不上,两头各缺一半)。 |
|
||
| F-65 | ⚠️ **契约 §14 的路径与 §1 的路径约定冲突**:§14 的详情节写 `POST /open/v1/auth/token`(**不带** `/api/v1`),而 §1 声明全部接口在生产是 `https://api.tianbaotong.cn/api/v1/**` | 按 §1 走,实现为 `/api/v1/open/v1/...`(与其它 110 多个接口同一前缀,网关只需一条规则)。`check-api-parity.py` 的 `norm()` 会去掉 `/api/v1` 前缀,两种写法都能对上,**所以工具不会替我们发现这个冲突** —— 它只能靠人读文档发现 ✅ 已闭环:§17.2 **D-7** 写明统一 `/api/v1/open/v1/...` 前缀(网关一条规则)。 |
|
||
|
||
| F-66 | ⚠️ **契约 §14.4 把一个"我方 → 机构"的报文写成了我方的一个路径**:正文说"系统在终审通过后主动推送",请求示例也标注"我方 → 机构",但接口清单里它是 `POST /open/v1/callback/push`(一个我方路径) | 实现为**人工重推入口**(`{submissionId}`),并重置重试次数 —— 它是"人判断过该再试一次",不该被"已经失败 5 次"挡住(那正是需要人工的场景)。自动推送与自动重试分别发生在终审通过的 `afterCommit` 与定时任务里 |
|
||
|
||
| F-67 | ⚠️ **契约 §14 定义了"机构怎么调我们",却没定义 appKey / appSecret 从哪来** —— 没有管理接口,§14 的四个接口在真实流程里**跑不起来**:只能靠改库产生接入方,而 `appSecret` 是密文列(有人得手工加密一个字符串) | 补 `POST/GET /open-apps`、`/{id}/reset-secret`、`/{id}/toggle`、`/push-logs`,并守住两条:**appSecret 只在创建/重置时返回一次**;**重置是显式动作**(它会让对方现有签名立刻失效,要在界面上交代清楚) |
|
||
|
||
| F-68 | ⚠️ **规则模板被我挂错了路径**:契约 §4.3 的清单写 `/rule-templates`(即 `/api/v1/rule-templates`),而我挂在了 `TaskController`(base `/api/v1/tasks`)下 → `/api/v1/tasks/rule-templates`。**是契约对、我错**:模板是独立资源(有 id、可被多个任务套用),不是任务的下级 | 新建 `RuleTemplateController`(base `/api/v1/rule-templates`)。**这个错是 `check-api-parity.py` 报出来的** —— 它按完整路径比对,所以"路径挂错"和"没实现"在它眼里是同一个现象(都算缺口)。这也是它作为验收工具的价值:**只要路径与契约不符就会红** |
|
||
|
||
| F-69 | ⚠️ **开放接口的令牌路径必然 500**:`appOfToken` 里用 `selectById` 回查接入方,而开放接口是匿名的 —— **请求进来时还没有租户上下文**,租户拦截器直接拒绝(`未登录或登录已失效`),对外是 50000「系统繁忙」。HMAC 路径不出现(它走 `findByAppKey`,已忽略租户),所以**只有令牌路径 500**,看起来像"令牌功能没做" | 凡在"还没有租户上下文"时查归属都用忽略租户的专用方法(`findByAppKey` / `findByIdIgnoreTenant` / `findBySubmissionIgnoreTenant`),拿到归属后立刻 `TenantScope.callAs` 摆上下文。**"先找归属、再摆上下文"这个模式里,找归属那一步必须能无租户执行** —— 范围就是那几个方法,逐个写清理由 |
|
||
|
||
| F-70 | ⚠️ **nonce 的占用顺序反了会变成拒绝服务入口**:我原来的顺序是"先占 nonce 再验签",注释还写了一句"反过来的话错误签名能消耗 nonce"—— **这句话恰好说反了**。先占的话,任何人发一个签名错误的请求就能把那个 nonce 消耗掉;先验签则签名错直接拒、nonce 不被碰 | 改成**先验签、后占 nonce**。E2E 有一条专门守它("错误签名不该消耗 nonce")—— 这条断言的价值在于:它测的是一个**副作用**,而不是返回值 |
|
||
| F-71 | ⚠️ **§9.5 的"直传凭证"给的是 S3 风格的签名 PUT URL,而本项目存储走七牛** —— 七牛直传是"POST 到 upload.qiniup.com + form 带 token",而且**必须配回调**(`callbackUrl`)才能让服务端知道"传完了、文件在哪"。没有回调,文件会安静地躺在存储里,而库里没有任何记录指向它 | 一期如实返回 `direct: false` + `uploadUrl` = 本项目的接收地址(学生端 `wx.uploadFile` 直接 POST 它,与 H5 路径**完全一样**)。凭证这一层仍保留它真正的价值:**预检 + 秒传 + 压缩提示** —— 这三件事都能在"不传一个字节"的情况下完成,而那正是它要解决的问题(传了 8MB 才发现格式不对/重复了) ✅ 已闭环:§9.5 重写为 `direct:false` + 服务端接收地址;恢复直传的前置条件(含七牛回调)写进 §17.2 D-3。 |
|
||
| F-72 | ⚠️ **`uploadToken` 签发之后没有"消费"它的地方**:我按契约实现了凭证的签发与预检,但**上传接口没有接它**(`/mp/files/upload` 仍然只认 `submissionId + materialCode`)。于是凭证目前只用于"预检与秒传判断",**不构成一道校验** —— 客户端完全可以跳过凭证直接上传 | 如实记录(不假装它是安全机制)。**建议**:要么让 `/mp/files/upload` 接受并校验凭证(一次性、15 分钟、绑定到人),要么把它定位成"纯性能优化"并在契约里写明 —— 现在两种都不是 ✅ 已闭环(客户确认定位为**预检/秒传/压缩建议**,非安全边界):契约 §9.5 明写「客户端可跳过;服务端上传本身做全部校验」。 |
|
||
| F-73 | ⚠️ **秒传检查排在份数检查之后** —— 而"同一个文件重传"恰恰是秒传要解决的场景(学生传完网络断了、客户端重试)。顺序反了会得到「身份证最多 2 份,你已经传了 2 份。要换请先删掉上一份」,让用户在"我明明刚传的"这件事上原地打转 | 改成**先秒传、后份数**。**顺序体现的是"哪个答案对用户更有用"**:「你已经传过了」永远比「你超了」更有用,因为前者不需要他做任何事 |
|
||
| F-74 | ⚠️ **平台运营账号(`platform`)登录会撞图形验证码**:`LoginGuard` 在连续失败后要求验证码(`xt:login:fail:{account}`),而验证码是图片、脚本给不出来。这不是缺陷(那是防爆破的设计),但它让"验收运营类接口"卡在第一步 —— 而 §6.5/§6.7 的权限正是"运营" | E2E 先清 `xt:login:fail:platform` 再登录,并把这个 key 写进注释(真实验收时运营账号被锁,运维也要知道清哪个)。⚠️ 顺带确认:**平台账号的密码是 `Platform@123`**(在 `docs/07-开发实施计划.md`),不是学校管理员的 `Admin@123` —— 我先按 `Admin@123` 试,得到"账号或密码错误",很容易误判成"平台账号没建" |
|
||
| F-75 | ⚠️ **压缩率会出现负数**:一个 3 个小文件(各 40 字节)的包,`raw=120B`、`final=802B` → 页面显示 `-568.3%`。原因是"压缩"在小包上本来就不适用 —— 里面的文件已经是压过的(jpg),或者重新打 zip 的**包头开销**比省下来的还多。问题不在"不好看",而在**客户会以为系统算错了**(而它没算错) | 服务端对 `ratio < 0` 回 `—`(与"还没算"同一个符号:**都是"这个数说明不了什么"**)。⚠️ 顺带发现 **E2E 的断言太弱**:`"%" in compressRatio` 对 `-568.3%` 也成立 —— 它确实"是个百分比",只是没有意义。已改成"要么形如 `12.3%`(非负),要么是 `—`" |
|
||
| F-76 | ⚠️ **`GiButton` 没有 `text` prop**:它的文案来自 `type` 的预设(`BUTTON_MAP`)或**默认插槽**(`<slot>{{ buttonConfig?.text }}</slot>`)。我写成 `<GiButton type="add" text="上传压缩包整理" />`,**typecheck 过了、控制台没报错、按钮也正常渲染** —— 只是显示的是预设的「新增」 | 改用默认插槽传文案。**教训:「传了没报错」不等于「生效了」** —— 这类错只有真正打开页面看才会发现。所以前端的验证底线是**在浏览器里看一眼**,typecheck/lint 过不代表对(未知属性在 Vue 模板里完全合法) |
|
||
| F-77 | ⚠️ **我自己在验证时用错了选择器,差点去"修"一个不存在的 bug**:我用 `.arco-table-tbody .arco-table-tr` 数行数,得到 0 行、判定"表格没渲染";真相是 **Arco 在无 `scroll.y` 时 `<tbody>` 上不带 `arco-table-tbody` 这个 class**,行一直都在(`document.querySelector(".arco-table tbody").innerText` 立刻能看到) | 记下来是因为它值一条纪律:**"测出来是 0"与"确实是 0"是两件事** —— 断言要打在真实的 DOM/响应上,并且**先确认这个度量本身是有效的**(换一个选择器、或直接读文字,交叉验证一次)。这与之前 F-60(POI 与 openpyxl 对"红色"的索引不同)是同一类:**度量工具的偏差会伪装成产品的缺陷** |
|
||
| F-78 | ⚠️ **「待建」页在说谎,而且说了很久**:`名单管理 / 材料规则 / 表单设计` 的功能早在前面几轮就做完了(作为「收集任务」行内的抽屉),但这一页对它们一律说「这个页面还没做」。原因不是"忘了改文案",而是**这一页从来没读到过菜单定义** —— 它按 `route.meta.menu` 找,而 `formatAsyncRoutes` 构造的 `meta` 里**根本没有 `menu` 这个键**(只有 hidden/keepAlive/title/icon/affix/…)→ `findMenu()` 恒为 `undefined` → 每个菜单项自己的 `note` 从来没显示出来过 | 给 `XtMenu` 加可选的 `entryAt`(真实入口在哪),并**按路径查找**(`findMenuByPath(route.path)`,不动 Gi 模板的共享代码)。待建页据此分两种说法:有 `entryAt` → 「功能已就绪」+ 指路 + 跳转按钮;没有 → 原来的「还没做」。**教训:一个"取值永远失败"的表达式,表现是"文案不对",而根因跟文案毫无关系** —— 与 F-77 一样,先确认取值本身有效,再改下游 |
|
||
| F-79 | ⚠️ **又一处把 Markdown 写进纯文本节点**:菜单 `note` 里的 `**按任务**` 在页面上原样显示成两个星号。这与 F-76(`headlineOf` 里的 `**`)**完全同类** —— 同一天里犯了两次 | 清掉全部 3 处(并**扫了一遍**所有 `note:` 而不是只修看到的那条)。**记这条是因为"同一类错犯两次"说明它不是我手滑,而是缺一条检查**:写文案时先问一句"这段文字最终落在哪里、那里支不支持富文本" |
|
||
| F-80 | ⚠️ **脚本改代码时把"跨行拼接的属性"腰斩了**:`roster` 的 `note` 本来是`note: '第一段'` + 换行 + `+ '第二段'` 两行拼的,我的补丁按"整行替换"处理,只换掉了第一行,于是留下一个孤立的 `+ '导入、体检报告…'`,整个文件**语法错误**(typecheck 报在几百行之外,看着完全无关) | 按多行结构重写这一段。**教训:补丁用"整行替换"时,必须先确认这个属性是不是多行的** —— `find()` 命中一行不等于这个属性只有一行。(这与之前 F-57 那次"`find()` 返回 -1 却继续往下写"是同一族:**基于文本的补丁要有"结构假设",而假设必须被验证**) |
|
||
| F-81 | ⚠️ **平均审核时长返回 17 位浮点数**:`AVG(duration_sec)/3600.0` 的 PostgreSQL 双精度结果直接透出,界面上是 `0.00038888888888888887 小时`。**那不是"精确",是"没处理"** —— 既不可读又让人怀疑系统算错了。而且这个字段**开放接口(§14)也看得到**,对外报文里出现这种东西很显眼 | 服务端 `round2()` 四舍五入到 2 位(`avgAuditHours` 与 `byAuditor.avgHours` 两处);`null` 仍保持 `null`(**"没有样本"与"0 小时"是两件事**)。前端再按量级换单位:`null`→`—`、`0`→`不到 1 分钟`、`<1`→`X 分钟`、否则 `X 小时`. **这不是美化,是别让人读出一个不存在的结论**(`0.0 小时` 会被读成"零小时") |
|
||
| F-82 | ⚠️ **首页在说一件不真的事,而且没人会发现**:工作台底部那张卡写着「M0 后端已完成,界面建设中」+ "界面按里程碑推进:M1→M2→M3→M5→M6"。而实际上契约 121 个接口里 120 个已实现、10 个 PC 页面可点。**它不报错、不影响任何功能、也没有测试会红** —— 所以它能一直错下去,而每个新用户打开首页第一眼看到的就是它 | 整块换成真实看板。**教训:说明性文案比功能代码更容易过期**,因为它没有测试守着。写"进度/现状"类文案时,要么让它**从数据里来**(这次的做法),要么就别写具体数字(写成"详见交接摘要"也行)。本轮同时核销了 §四 里 8 条"已完成但仍挂未完成"的条目 —— 那份清单过期同样属于这一类 |
|
||
| F-83 | ⚠️ **`Map<String, Object>` 结果的别名会被 PostgreSQL 折成小写** —— `SELECT tenant_id AS tenantId` 在结果里是 `tenantid`,于是 `row.get("tenantId")` 拿到 `null`。**同一个写法,两种结果**:返回 POJO 的 mapper(`UrgeMapper`)不受影响(MyBatis 按属性名忽略大小写匹配),**所以这个坑只在 Map 结果上出现** —— 照着别的 mapper 抄一遍就会中招。它的表现极其安静:定时任务照跑、`TenantScope.runAs(null, ...)` 抛了被 catch 吞掉、日志里只有一行 warn,**看起来跑过了,其实什么都没做** | 别名一律 `AS "camelCase"`(与 F-54 同一条规矩,**这是同一个坑的第二次**)。另外在四个任务里都加了候选行自检:缺 `tenantId` 就把"候选行的实际键"打进日志,下次一眼能看到是 `tenantid` 不是 `tenantId` |
|
||
| F-84 | ⚠️ **「HTTP 200 + code 0」不等于业务动作成功**:我在 E2E 里断言提交接口 `code == 0` 就算"学生已提交",而契约 §9.5 明写「**校验不通过也是 HTTP 200**:它是正常业务结果而不是异常」。于是乙(没传必传材料)的 `code=0` 而 `data.submitted=false`、填报单还是草稿、名册还是「草稿」 | 断言改成判据字段 `data.submitted`,并把 `redCount` 与 `issues` 打进失败信息里。**凡是"校验可能不通过"的接口(提交、发布、导入、批量审核…),判据都在 `data` 里** —— 夹具断言"动作成功"时必须断言那个动作自己的结论字段。这个错的表现极具欺骗性:夹具"通过了",后面所有断言全红,看起来像被验的功能坏了 |
|
||
| F-85 | ⚠️ **清理只在"正常结束"时跑,而失败路径才是残留的主要来源**:E2E 里 20 多处 `raise SystemExit`(夹具失败即中止)都会**跳过结尾的 `cleanup()`**,于是上一轮的消息留在库里,把这一轮的"不该发"断言全部污染成失败(看起来像催报去重坏了) | **两头都清**:开跑前先 `preclean()`(保证起点干净),结尾再清一遍。顺带修了一个观测问题:Python 的 stdout 在管道里是块缓冲、stderr 不缓冲,所以 `raise SystemExit` 的消息会出现在日志**最前面**,看起来像"前面某一节就死了" —— 结尾加 `sys.stdout.flush()` |
|
||
| F-86 | ⚠️ **"检查配置"的代码自己先崩在"配置有问题"上**:生产自检原本要在启动时列出"缺哪些环境变量",而我用 `env.getProperty(key)` 去读 —— 它会**解析占位符**,而 prod 配置里写的是 `secret: ${XT_JWT_SECRET}`,环境变量没设时它抛 `PlaceholderResolutionException`。于是精心写的那张清单**一个字都打不出来**,用户看到的还是一段 56 行栈 | 改成**安全解析**(解析失败才算没配)。⚠️ 中间我还修错了一版:改成只读 `PropertySource.getProperty`(完全不解析)之后,读到的永远是字面量 `${XT_JWT_SECRET}`,于是 **env 明明设了值也报"未设置"** —— 这个错更隐蔽("真没设"的场景看起来完全正确)。**一般化:做配置检查的代码必须能区分"没配"与"配了",而且要比被检查的配置更皮实** |
|
||
| F-87 | ⚠️ **自检写成 `ApplicationRunner` 就轮不到它**:`ApplicationRunner` 跑在容器就绪之后,而缺 `XT_JWT_SECRET` 时 **`JwtService` 在构造 bean 时就炸了** —— 自检根本没机会说话 | 挪到 `EnvironmentPostProcessor`(在 `META-INF/spring.factories` 里注册),它跑在任何 bean 之前,只需要 `Environment`。⚠️ **注册错的表现是"检查静默不生效"**(应用照常启动、毫无报错),所以 `scripts/e2e-prodguard.py` 是**真的起一次 jar** 来验证,而不是看代码在不在 |
|
||
| F-88 | ⚠️ **我定义了 CI 的 e2e 模板却没写 `extends` 它的 job**:`.e2e-base` 里写好了"起 PG/Redis 容器、装依赖、生成随机密钥、起后端、循环跑全套 E2E",而 YAML 里**没有任何 job 引用它** —— 流水线解析完全通过、阶段名也都在,唯独**这个项目唯一的主验证手段根本没跑** | 补上 `接口断言(全套 E2E)` job,并在文档里记下这次是怎么发现的:**用 `yaml.safe_load` 把 job 名列出来数一遍**。"看起来有 e2e 阶段"和"真的跑了 e2e"是两件事 —— 配置类文件也需要断言(光"能解析"不够,还要断言"该有的都在") |
|
||
| F-89 | ⚠️ **"造一个合法密钥"这件事我连错两次,而且错法一样**:第一次 AES 假密钥解出来是 30 字节(要 32),第二次 HMAC 那串**根本不是合法 base64**。两次都不是配置的问题,是我在"手写字面量当密钥"上想当然 —— 而它们的表现都是启动失败 + 一个栈,看起来像"配置写错了" | 改成 `base64(os.urandom(n))` 生成。**顺带验了 CI 里那三条 `openssl rand -base64 N` 的长度是对的**(64→88 字符、32→44、48→64,解回都是原字节数)—— 否则流水线会因为密钥长度红掉。⚠️ 同时把"密钥长度"收进 `ProdEnvCheck`:不改的话运维看到的是一个 85 层栈里的 `AES-256 需要 32 字节密钥,实际 30 字节`,而那个信息本该出现在那张清单里 |
|
||
| F-90 | ⚠️ **"起来了"被误报成"起不来"**:`check-test-profile.py` 在 Tomcat 打印 `Tomcat started` 的**同一瞬间**就发健康检查,那时端口还没开始接受连接 → connection refused → 脚本报"起不来",而日志里明明是 `Tomcat started on port 18098`。**误报的方向恰好最危险**:它会让人去查一个不存在的问题 | 健康检查加 20 次重试。⚠️ 同一个脚本还有第二个缺陷:写死端口且 `kill()` 后不等退出,**第二次跑就撞端口**,报的错(`Port xxx already in use`)与"配置对不对"无关 —— **一个只能跑一次的检查等于没有检查**,改成每次 `socket` 要一个空闲端口 + `wait(timeout=15)`。并把它加进 CI(在 e2e 起服务**之前**单独跑一步:失败了就是"配置起不来",而不是"E2E 脚本有问题") |
|
||
| F-91 | ⚠️ **PRD 附录 E 里有一条定时任务,对应的机制在 V1.5 就被废除了**:附录 E 第 6 条写着「待认领名册清理|每日|超时未认领的人员移出应填名单并通知管理员」,而 PRD 正文 §"容错与纠正对照表"明确写着:「📌 **已废除**:原矩阵中"不在名册 → 转待认领队列"—— **"自助认领"机制已在 V1.5 废除**(会绕过门禁)。证件号不在名册时的正确出路是引导走「申请加入名单」通道(FR-509),由管理员逐条审批后入册」 | **不做这一项**,并把 9 项核销为 8 项。⚠️ 为什么这条值得单独记:附录 E 是一张**看起来最像清单的清单**(9 行、有频率、有说明),而它漏跟了正文的一次机制废除 —— 照着它做,会**把一个已被产品明确废除的门禁旁路重新实现出来**。**清单类附录最容易过期,因为它没有"会被读到"的必然性**(正文条款在讨论时一定会被翻) |
|
||
| F-92 | ⚠️⚠️ **门禁限流会把整间教室挡住**(压测抓到的真缺陷):IP 限流 `20 次/分钟`计的是**所有请求**(成功也算),于是 60 个**不同**学生同一出口 IP 同时核验时,**只有 20 个能进,其余 40 个收到 `40071 操作过于频繁`**。而"一个班的学生坐在同一间教室、连同一个 WiFi"正是**首次落地第一分钟就会发生的事** —— 这不是性能问题,是上线即事故 | 拆成两个计数器,因为**目的不同**:失败(按人 10/小时、按 IP 20/分钟)用于防猜与防枚举;成功(按 IP 200/分钟)只用于防"拿这个接口批量取数据"。**关键洞见:枚举攻击产生的是失败,而教室产生的是成功** —— 把成功也算进防枚举额度里,等于用防攻击的代价挡住正常用户,而挡住的量级恰好是"一个班"。⚠️ 另有一条**刻意不计**的失败:`GATE_ALREADY_BOUND`(两个人抢同一个名册项)——开学第一天必然出现几例,算进额度会让整间教室在几秒内被限流,**而没有人做错了什么** |
|
||
| F-93 | ⚠️ **压测会打到真实对象存储**:本机 `application-local.yml` 里 `provider: qiniu`,所以上传压测落的是**线上那个空间**(bucket=tools-matrix)—— 40 张图打过去,七牛回了 `status:614` | 上传压测改成默认不跑(`--with-upload` 才跑),并在脚本里打印"要跑请先 `XT_STORAGE_PROVIDER=local` 重启后端"。两件事都不对:**压测不该依赖外部服务**(网络抖动会让"性能结论"变成对七牛的评价),**也不该往真实桶里灌测试文件**(删库不删桶) ✅ 已闭环:压测改为打**本地存储实例**(docs/09 §9.4 推荐跑法);真实桶里 2036 个测试对象 / 116 MB 已用 `scripts/dev-clean-qiniu.py` 清空(只动 `xiaotutongxue/` 前缀,删前核实库中零引用,删后复验 0 个)。 |
|
||
| F-94 | ⚠️ **我自己在压测里犯了三个度量错误**:① 吞吐算成了 `n / (avg × n)`,化简就是 `1/平均耗时` —— 那不是吞吐,而"一行看起来很像结论的数字"最容易被抄进汇报里;② 只报"混合 p95",看不出**哪个接口**慢(而"该优化哪个"才是压测的目的);③ **用两个并发数字推断出"每行约 2.8ms"**,而单请求复测(20/50/200 页 → 47/36/44ms)直接否掉了它 —— 并发下的差异是排队噪声,不是按行成本 | ①改成"请求数 / 墙钟耗时";②按端点分别统计;③把"单请求中位"也纳入报告,并在 §9 里写明"并发数字不能用来推断每行成本"。**教训:并发压测的数字只能回答"系统在并发下的表现",不能反推单请求的成本结构** —— 要问"哪里慢"就得用"数 SQL 条数 + 单请求计时 + EXPLAIN"这三样 |
|
||
| F-95 | ⚠️ **手册初稿里有两处"按设想的系统写",而不是"按真的系统写"**:① 我写了「同一张图重复传会被自动识别(秒传)」—— 而 `/mp/files/upload` **只计算 `file_hash`、没有去重查询**(秒传逻辑在 `/mp/files/upload-token` 里,而 F-72 已记过:上传接口并不消费它)。手册这么写会让学生以为重复传不占份数,然后撞上份数上限;② 我写了「代传**要**填原因(这是审计要求)」—— 而 `proxyReason` 是**选填**,手册说"必须填"会让老师以为界面坏了(因为不填也能提交) | 两处都改成与实现一致的说法。**并把这件事故化**:新增 `scripts/check-manual.py`,把"手册引用的错误码与提示文案是否真的存在"变成机械检查并进 CI —— 因为**手册是文档、文案在代码里**,改一次文案手册就静默失准,而没有任何东西会红(与 F-82「首页在说一件不真的事」同一类)。⚠️ 但要说清它**查不到什么**:它拦的是"手册写了代码里没有的东西",**拦不住"手册漏了新增的东西"** —— 那仍然要靠人 |
|
||
| F-96 | ⚠️ **本轮我连续犯了三个"没断言就往下走"的错**,形态不同、根因相同:① **文本补丁静默没生效**:`str.replace` 没命中时不报错、不返回失败,脚本照样打印"成功",而文件一个字没改 —— 随后的编译错("方法不存在")看起来像另一个问题;② **夹具构造失败却继续往下走**:`with_checksum` 传错位数后,失败发生在 300 行之外的名册导入报告里("当前 19 位");③ **`R.call` 的返回值没查 `code`**:建草稿失败时 `data` 是 `null`,报的是 `TypeError: 'NoneType' object is not subscriptable`,真正原因一个字都没打 | ①改成"每处替换都断言命中"(`must_replace`),并**回读校验**;②把 helper 改成**参数不合法就抛**(治本,前两次我只改调用点);③每个 `R.call` 先查 `code` 并把 `msg` 带进断言信息。**根因是同一条:把"我以为发生了"当成"确实发生了"** —— 而这三处的共同代价是**把排查引向错误的方向**(比直接报错贵得多) |
|
||
| F-97 | ⚠️ **我断言错了字段名,于是"缩略图功能"看起来没生效**:`FileUrl` 有 `url`(原图)与 `thumbUrl`(缩略图)两个字段,而我的验证脚本去检查 `d2["url"]` 里有没有 `imageView2` —— 按设计它**永远是原图链接**,所以那个断言必然为 false;更糟的是它连着把后面两条断言一起带错了(下载的是原图,于是"没变小") | 改成检查 `thumbUrl`,并**加一条反向断言**:`url` 不该带处理参数、`thumbUrl` 该带。⚠️ 这与 F-77(用错 CSS 选择器,差点修一个不存在的 bug)**是同一类**:**断言之前先确认"你在量的那个东西是对的"**。这次的隐蔽之处是字段名很像(`url` vs `thumbUrl`),而且接口确实返回 200、数据确实存在 —— 一切看起来都正常,只有断言是错的 |
|
||
| F-98 | ⚠️ **"随机端口"这个防撞设计带来一个反直觉的副作用**:`check-test-profile.py` 每次取**随机空闲端口**,于是上一次失败留下的 JVM **永远撞不到**这个端口、也就永远不会被发现 —— 它会一直占着内存与 CPU,直到某一次"怎么都起不来"(实测:连跑几轮后本机攒了 5~7 个 java,启动从 10 秒变成 180 秒超时;而**下一次跑又好了**,这正是"时好时坏"的来源,很容易被归因成"配置有问题")。同时那次失败**没法排查**:子进程输出走 `PIPE` 而没人读,事后才读又已被 kill 截断,最后只剩一句"180 秒内没探活成功" | ①开跑前**按 jar + profile 清残留**(只杀本项目的 jar,且**保留 dev 后端** —— E2E 正打着它,顺手杀掉等于把后面的检查全废掉);②子进程输出**落盘**(`backend/_test-profile.txt`,已被 `.gitignore` 的 `backend/_*.txt` 覆盖),失败时打印"停在哪一行 + 关键字命中 + 尾部 20 行",成功时打印**启动用时**(基线 ~10 秒 / 72 行 / 9.5KB,一旦变成几十秒先怀疑本机负载);③等待期间每 15 秒报一次进度,**卡住时不留沉默**。**教训:给"防撞"加一层机制时,要问一句"那它把什么东西藏起来了"** |
|
||
| F-99 | ⚠️ **补测"没测过的场景"时,压测工具自己连续撒了 8 次谎** —— 它们的共同点是"**度量/夹具出问题时,现象长得像产品缺陷**"。① **夹具前置没断言**:单独跑 `--only upload` 时 40 张图**全部**报「没有草稿」—— 因为上传需要先经门禁绑定,而以前它总是跟在门禁场景后面跑、"恰好"能跑;② **假 JPEG**:夹具图是"JPEG 头 + 随机字节",能过扩展名与大小校验,但服务端**解码即失败**,走的根本不是压缩路径 —— 换上真图后立刻量到 2850KB → 1329KB(**47%**);③ **猜列名**:`collect_export_file` 的列是 `part_no / file_name / file_path`,我按直觉写成 `part / name / path`,SQL 直接报错;④ **判据取错返回值**:`upload_file` 返回的是 `(HTTP 码, body)`,我拿 HTTP 码当业务判据,把 `code: 0` 的成功报成"串行基线失败"(与 F-84 / F-96 同一类,**第三次**);⑤ **跨租户同名账号**:`select id from sys_user where username='admin'` 命中的是**别人家**的 admin(库里有 50 多个同名),于是"初审人"被设成跨租户用户 id —— 正确做法是**从 token 的 `sub` 取 uid**;⑥ **不查接口返回码**:`PUT /tasks/{id}/orgs` 被服务端拒(`headUserId` 必须是**真持班主任角色**的账号)而我没查,"发布"失败也没查,最后学生端整齐地报 `40008 任务已截止、已归档或已暂停`,排查方向整个错了;⑦ **PowerShell `Select-Object -First N` 会提前停止读取上游**:被截断的进程**仍然活着**,看起来就是"卡住",我为此白等了十几分钟 —— 而外层工具又把输出缓冲到命令结束,于是"慢"与"被堵"根本分不开;⑧ **清理只删库行不删磁盘文件**:压测跑几轮就在 `backend/data/uploads` 攒下几百 MB | ①在上传/导出场景里**显式绑定 + 断言前置**(不成立就报"前置不成立",而不是"上传失败");②`make_img` 改用 Pillow 生成**真 JPEG**(噪声图,纯色量不出 CPU 成本);③按 DDL 写列名;④改判 `body["code"]`;⑤新增 `uid_of(token)`(解 JWT 的 `sub`);⑥新增 `call_ok()`:夹具里每个接口调用**都断言业务码为 0**;⑦长跑/压测/起服务**一律把日志落盘**,不用管道截断(与 F-98 同一个手段);⑧`cleanup()` 先取出文件路径再删行,**连磁盘文件一起删**(存储是七牛时那些路径本地不存在,是安全的 no-op)。**教训:补测的价值一半在数字,另一半在"原来夹具一直不成立"** —— 而后者只有在**断言前置**之后才会暴露 |
|
||
| F-100 | ⚠️ **门禁"成功侧上限"卡在年级量级,会挡住全校集中填报**(F-92 的第二次复发,量级更大):契约 §1.6 只规定**失败侧**(同 openid 10 次/小时、同 IP 20 次/分钟),成功侧 200 次/分钟是本项目自己加的(防"拿匹配接口批量取数据"),而当初注释里写的本意是"一个 200 人的年级同时扫码也不该被挡"。实测 **250 个不同学生同时核验(全部正确)→ 213 成功、37 被挡**,提示「当前网络访问过于集中,请稍等 1 分钟再试」。"全校在同一个出口 IP 集中扫码"(开学第一课集体填报)正是会发生的场景,而**这些人没有一个做错了什么** —— 与 F-92「教室被挡住」是同一类,只是从"一个班"放大到"一所学校" | 上调 `IP_OK_LIMIT_PER_MINUTE` **200 → 600**(依据:一所学校一次集中填报的峰值;它仍然是限速,防枚举靠的是失败侧那两个计数器,**原样未动**)。**复测**:250 人 **250/250**、500 人 **500/500**(500 并发 p50 1.4s / p95 2.5s,是排队不是限流),`e2e-m3-gate.py` 仍 **29/29**。已在 `MpRosterService` 常量注释里写明依据与"改之前先量"的命令,并同步 `docs/01` E2、`docs/09` §9.3。**教训:自己加的"防御性限额"也要过一遍真实场景的量级 —— 否则它与它要防的东西一样,都是事故** |
|
||
|
||
---
|
||
|
||
## 七、建议的执行顺序(按依赖,不按重要性)
|
||
|
||
```
|
||
① 审核前端(P0-3)── 打通"学生提交 → 班主任审 → 终审"这条能看见的路
|
||
↓ 审核结论是导出的前置条件
|
||
② 导出与交付(P0-2)── 打包引擎 + 汇总 Excel + 目录树 + 禁用导出铁律
|
||
↓ 交付物能产出后,才谈得上"验收"
|
||
③ B1 加入申请(P1-4)── 客户唯一功能增量,表已建
|
||
↓ 与 ② 互不依赖,可并行
|
||
④ 学生端剩余页(P1-5)+ 班主任端看板(P1-6)
|
||
↓
|
||
⑤ 识别与校验(P1-1/P1-2,等 D1 选型)── 失败一律放行,不阻断主流程
|
||
↓
|
||
⑥ 收尾(P2-1~4):CI/CD + 环境 + 压测 + 手册;小程序资质全程并行推进(P0-1)
|
||
```
|
||
|
||
**工时参照**(沿用 `05-开发任务拆解.md` 的口径,未含缓冲):M6 导出 32 人日、M4 识别校验 34 人日、
|
||
M7 收尾 52 人日、M5 剩余前端约 31 人日。**关键路径仍是"审核前端 → 导出打包"**,
|
||
打包引擎 10 人日不可拆(拆开要重写),所以这一段**加人无效**。
|
||
|
||
---
|
||
|
||
## 八、每步的验收口径(做完就得能跑出这些)
|
||
|
||
| 步骤 | 验收 |
|
||
|---|---|
|
||
| 审核前端 | `e2e-m5-audit.py` 87 条不回归 + 浏览器实测:T2(同是班主任但没被指定)看不到待办;右滑通过后列表少一张;驳回必须点按钮并选原因 |
|
||
| 导出与交付 | 新增 `scripts/e2e-m6-export.py`:有红问题 → `canExport=false`;导出后解压校验目录树与 21 列汇总 Excel;**Windows 解压中文文件名不乱码**;URL 不入库 |
|
||
| B1 加入申请 | 新增 `scripts/e2e-m2-join.py`:门禁失败 → 提交申请 → 管理端审批 → 通过后该生下次扫码能进门禁 |
|
||
| 学生端剩余页 | `e2e-m3-*` 不回归 + H5 真机(硬刷新)走完"我的任务 → 填报 → 提交 → 撤回 → 重交" |
|
||
| 识别与校验 | 识别失败一律放行(断言"识别不可用时提交仍成功");置信度 <0.8 不展示 |
|
||
| 收尾 | CI 里跑 `mvn -DskipTests package` + 全量 E2E + `vue-tsc` + `verify-docs.py`;上线检查清单(F-8)逐项打勾 |
|
||
|
||
---
|
||
|
||
## 九、附录 A · 代码有、契约 §4 清单没有的接口(F-4 的待办底稿)
|
||
|
||
> **这份清单是跑出来的,不是抄出来的:**
|
||
>
|
||
> ```bash
|
||
> python scripts/check-api-parity.py --extra
|
||
> ```
|
||
>
|
||
> 当前口径:代码共注册 **161** 个接口,契约 §4 声称 **121** 个(其中 **120** 个已在代码里),
|
||
> 差集 **41** 个。
|
||
>
|
||
> ⚠️ 上一版这里是"人工做一次差集、手抄成表"(写着 83 / 62 / 21)——
|
||
> 此后每加一个接口它就悄悄过期一次,最后差了近一倍,而且**没人会记得回来改它**。
|
||
> 手抄件会过期,命令不会。所以下面只留"为什么它们不在契约里"的分类结论;
|
||
> **要逐条清单时现跑那条命令。**
|
||
|
||
### 9.1 这 41 个按其「为什么不在契约里」分成六类
|
||
|
||
| 类 | 个数 | 为什么契约里没有 | 该怎么办 |
|
||
|:--|:--:|---|---|
|
||
| **A 材料通道:一步 multipart / 审核端独立一条** | 7 | 契约写的是"先传文件拿 `fileId` 再挂载"与 `upload-token` + OSS 直传;实现是**一步 multipart**,且审核端与学生端**各一条通道**(审核端要看防伪纹,学生端只看自己的) | §17 注明"实现按此写法"。**不要**改实现去迁就文档:直传要把密钥交给前端,反而更不安全 |
|
||
| **B 微信授权链路代替 `POST /mp/login`** | 4 | H5 走网页授权跳转(`authorize` → `callback`,token 放 URL fragment)、匿名落地页要 `scene-context`、dev 打桩登录只用于联调 | §17 注明;`stub-login` 要写明**仅非生产可用**(代码里另有一道 profile 判断) |
|
||
| **C 签名 URL:契约只定义了"取下载信息"** | 1 | `GET /exports/{id}/file` 才是真正下文件那条;§12.6 只定义了取下载信息 | 补进 §17,并写明 **URL 有有效期** —— 不能入库、不能发出去让人明天再打开 |
|
||
| **D 界面与接口必需、清单漏列** | 17 | 按钮要先知道"能不能点"(`exports/precheck`、`exports/preview`、`material-catalog`)、匿名出图、批量通过、教师端原因字典、名册项"标记不参与"这个可逆替代(`roster/{rid}/deactivate`)…… | **直接补进 §17 清单**,这类没有口径争议 |
|
||
| **E 契约链条本身断在第一步(要客户确认)** | 8 | `§12.7` 模板:`GET /notify/templates` 返回的内置模板**没有 id**,而 `PUT …/{id}` 要 id → 用户点"编辑"时根本拿不出 id(F-52);`§14` 的**接入方管理整块没写**(F-67) | 这 8 条**不是补文档能解决的**,要客户定口径(见 F-51 / F-52 / F-67) |
|
||
| **F 运维与调试动作** | 4 | 定时任务在测试里"等不起",所以留了手工入口(F-37):`schedules/run`、`exports/cleanup-run`、`cleanup/jobs/json`、`cleanup/jobs/parse-preview` | 补进 §17,标注"运维/调试用"(权限走运营侧的 `log:view:all`) |
|
||
|
||
> 合计 7 + 4 + 1 + 17 + 8 + 4 = **41**。
|
||
> 分类是按"谁去改什么"分的 —— 需要客户回答的只有 **E** 那一栏,其余三类是文档侧的补记。
|
||
|
||
---
|
||
|
||
## 十、变更记录
|
||
|
||
| 时间 | 变更 |
|
||
|---|---|
|
||
| 2026-09-29 | 首次成文:完成度矩阵(121 接口 / 58 实现)、未完成清单(P0~P2)、问题清单 F-1~F-10 与执行顺序;新增 `scripts/check-api-parity.py` 把接口计数固化成可复现检查 |
|
||
| 2026-09-29 | 附录 A 逐条核对:19 个"代码有、契约清单没有"的接口(用 tool 做差集确认,不靠人工数) |
|
||
| 2026-09-29 | **§六 施工记录**:完成 P0-3 审核前端(PC)与 P0-2 导出与交付(M6 后端),新增 `scripts/e2e-m6-export.py`(54 条)与 `scripts/dev-seed-audit.py`;全量回归 **19 脚本 / 771 断言 / 0 失败**;新增问题 F-11~F-15 与决定 D1~D9 |
|
||
| 2026-09-29 | 导出中心 UI + 目录树预览;**修正 F-16**(导出目录结构按 PRD §5.9 改为「批次号-任务名/班级/材料类型/文件」);新增 F-16~F-18;断言数更正为 **813**(753+60) |
|
||
| 2026-09-29 | **B1 名单加入申请**(后端 + 学生端 H5 + 管理端审批,V7 迁移);新增 `scripts/e2e-m2-join.py`(50 条);新增 F-19(名册 NOT NULL 密文列导致审批走不通)/F-20(自述班级被姓名规范化去掉空格);全量回归 **20 脚本 / 863 断言 / 0 失败**;接口实现度 **67/121(55%)** |
|
||
| 2026-09-29 | **学生端「我的任务 / 撤回 / 进度」**(§9.1/§9.11/§9.12 后端 + H5 两页);新增 `scripts/e2e-m3-mytask.py`(42 条);新增 F-21(进度条因"不适用材料算进分母"永远到不了 100%)/F-22(静态常量上的多余 `@TableField`);全量回归 **21 脚本 / 905 断言 / 0 失败**;接口实现度 **70/121(58%)** |
|
||
| 2026-09-30 | **班主任端(小程序)看板 + 审核卡片流**(§10.1 看板后端 + 3 个新页:看板 / 卡片流 / 详情与驳回);新增 `scripts/e2e-m5-teacher-dashboard.py`(24 条,验证「已交率 ≠ 通过率」与「范围只算我负责的班」);新增 F-23(`<script>` 动态 SQL 里裸 `<` 导致 MyBatis XML 解析失败、**启动即崩**);全量回归 **22 脚本 / 929 断言 / 0 失败**;接口实现度 **71/121(59%)** |
|
||
| 2026-09-30 | **通知与消息中心**(§13.5 站内信 + §13.6 订阅额度;V8 新表 `collect_message`;驳回/终审通过现在真的会通知学生,`notifySent` 语义收窄为「微信推送发出去没有」);小程序新增消息中心页 + 提交后索取订阅授权;新增 `scripts/e2e-m5-notify.py`(48 条);新增 F-24(表缺审计列)/F-25(`@Update` 里 `<` 不转义)/F-26(`biz_type` 是 varchar 不是数字);全量回归 **23 脚本 / 977 断言 / 0 失败**;接口实现度 **94/121(78%)** |
|
||
| 2026-09-30 | **催报**(§8.6 定向/一键催报 + §10.7 教师端催报与历史 + §13.3 通知记录;`/notify/records` 也一并做了);PC 新增通知记录页;新增 `scripts/e2e-m5-urge.py`(40 条);新增 F-29(催报分钟级限流未做)/F-30(契约 `notifyBizType` 5/6 对调,导致去重静默失效);全量回归 **24 脚本 / 1017 断言 / 0 失败**;接口实现度 **94/121(78%)** |
|
||
| 2026-09-30 | **操作日志留痕**(§13.4 操作日志与登录日志;V9 迁移补结构化留痕列)+ **名册改/删**(§8.3/§8.4,证件号变更强制留痕、已绑定不可删并给出可逆替代);PC 新增操作日志页;新增 `scripts/e2e-m5-logs.py`(44 条);新增 F-31(学生微信登录不写登录日志)/F-32(发布与恢复都是→收集中,反推动作名会记错)/F-33(publish 原来漏了留痕);全量回归 **25 脚本 / 1061 断言 / 0 失败**;接口实现度 **94/121(78%)** |
|
||
| 2026-09-30 | **字典管理**(§13.3 四个 + 内置项停用/恢复;V10 迁移新建 `sys_dict_item` 与 `sys_dict_disabled`,**两表分开**才能一键恢复默认);内置字典写在代码里、表单与字典共用同一份口径;PC 新增字典管理页;新增 `scripts/e2e-m1-dict.py`(53 条);新增 F-34(国标职业库一期为空,属外部数据依赖);全量回归 **26 脚本 / 1114 断言 / 0 失败**;接口实现度 **94/121(78%)** |
|
||
| 2026-09-30 | **导出分卷与过期清理**(§12.4 的 `splitSizeMb`;V11 迁移新建 `collect_export_file`);每卷都是完整可解的 ZIP、各带 sha256、各带汇总 Excel;过期后**删文件留记录**(状态 4 已过期,不是 3 失败);新增 `scripts/check-entity-columns.py` 与 `scripts/e2e-m6-export-split.py`(33 条);新增 F-35(F-24 第三次复发 → 上机械检查)/F-36(机器检查自身的假阳性)/F-37(定时任务在测试里等不起 → 加手工入口);全量回归 **27 脚本 / 1147 断言 / 0 失败** |
|
||
| 2026-09-30 | **班主任代传材料 + 教师端文件访问**(§10.6/§4.6;V12 迁移补 `proxy_by`/`proxy_reason`;一步 multipart,三道闸门);新增 `scripts/e2e-m5-proxy.py`(33 条);新增 F-38(`source` 两处文档口径冲突)/F-39(`source` 从来没被写过)/F-40(`canExport` 在无候选时仍为 true)/F-41(`onlyApproved` 在前置检查里没生效)/F-42(E2E cleanup 静默失败污染后续脚本)/F-43(门禁限流打断演示脚本);全量回归 **28 脚本 / 1183 断言 / 0 失败**;接口实现度 **94/121(78%)**;§4.6 班主任移动端缺口清零 |
|
||
| 2026-09-30 | **历史数据整理器**(§13.2,差异化获客功能;V13 补审计列与 `roster_task_id`;`HistoryNameParser` + 体检报告 + 幂等导入);新增 `scripts/e2e-m7-cleanup.py`(54 条);新增 F-44(`TaskService.create` 缺必填抛 NPE)/F-45(`task_code` 必须由 TaskService 生成)/F-46(`renderName` 的 ext 要带点号)/F-47(异步回查跨事务必然查不到)/F-48(候选名判定用 contains 会吃掉真名)/F-49(参数校验被幂等短路);接口实现度 **94/121(78%)**;§13.2 的 5 个缺口清零 |
|
||
| 2026-09-30 | **校验问题清单(§12.1~12.3)+ 通知模板与手工发送(§12.7)+ 微信回调**;V14 迁移;新增 `scripts/e2e-m7-validation.py`(70 条);新增 F-50(表建了没人用)/F-51(契约要 async 却没状态接口)/F-52(模板接口链断在第一步)/F-53(微信回调要偏离两条全局约定)/F-54(PG 折叠别名大小写)/F-55(PG 推断不出 `? IS NULL`)/F-56(软删除与唯一索引打架);接口实现度 **101/121(83%)**;§4.8 缺口清零;`check-api-parity.py` 加 `--missing` |
|
||
| 2026-09-30 | **任务克隆 / 任务统计(§7.11)/ 规则模板(§7.13)/ 名册导入模板(§8.5)/ 审核统计(§11.6)/ PC 端代传(§11.7)**;V15 迁移(`collect_rule_template` 补审计列 + 两条公共模板);新增 `scripts/e2e-m7-taskconfig.py`(69 条);新增 F-61(**初审待办会列出批不了的单子** —— 待办与审批两套判据)/F-62(契约漏了「保存为模板」)/F-63(公共模板被租户拦截器挡掉)/F-64(模板的 FORM 部分没有落地路径);接口实现度 **106/121(88%)**;§4.4 与 §4.7 缺口清零 |
|
||
| 2026-09-30 | **开放接口(§14,供认定机构对接)**;V16 迁移(接入方 + 推送记录);新增 `scripts/e2e-m8-open.py`(60 条);修 `rule-templates` 的路径(F-68);新增 F-65(§14 路径与 §1 冲突)/F-66(§14.4 的报文方向与路径不一致)/F-67(契约漏了接入方管理)/F-68(规则模板挂错路径)/F-69(匿名入口回查接入方必然 500)/F-70(nonce 占用顺序反了 = 拒绝服务入口);接口实现度 **112/121(93%)**;§4.10 缺口清零 |
|
||
| 2026-09-30 | **小程序登录与手机号绑定(§5.3)/ 组织导入(§6.4)/ 租户修改与用量(§6.5、§6.7)/ 文件视图·直传凭证·批量处理(§9.5、§9.7、§9.8)**;新增 `scripts/e2e-m8-orgfile.py`(54 条);新增 F-71(七牛直传缺"上传完成回调")/F-72(`uploadToken` 签发后无人消费)/F-73(**秒传排在份数检查之后**)/F-74(运营账号的验证码与密码);接口实现度 **120/121(99%)** —— **只剩 `/mp/ocr/idcard`**(等 OCR 选型);§4.1 与 §4.2 缺口清零 |
|
||
| 2026-09-30 | **历史整理器页面(PC)**:`apis/xt/cleanup.ts` + `views/cleanup/index.vue`,路由 `ready: true`;修 **F-75**(压缩率负数 → 服务端回 `—`,并强化 E2E 断言)/**F-76**(`GiButton` 没有 `text` prop,"传了没报错≠生效")/**F-77**(我自己验证时用错选择器,差点修一个不存在的 bug);PC 端 `ready:false` 的路由从 **5** 个减到 **4** 个(还剩 名单管理 / 表单设计器 / 材料规则 / 角色与授权) |
|
||
| 2026-09-30 | **「待建」页不再说谎**:`XtMenu` 加 `entryAt`,`名单管理/材料规则/表单设计` 三个菜单项改为「功能已就绪 + 真实入口指路 + 跳转按钮」,删掉会静默过期的"已实现页面"清单;修 **F-78**(**待建页按 `meta.menu` 查找恒为 undefined**,所以每个菜单项的 note 从来没显示过)/**F-79**(Markdown 星号写进纯文本,与 F-76 同类)/**F-80**(文本补丁腰斩跨行拼接的属性 → 文件语法错误);PC 端 `ready:false` 的路由仍是 4 个,但其中 3 个现在**明确指向真实入口** |
|
||
| 2026-09-30 | **工作台数据看板(P1-9)**:`apis/xt/{task,audit}.ts` 加统计封装与`TASK_STATUS_TEXT`;`views/dashboard/index.vue` 换掉过期的「开发进度」卡,改为"选任务 → 进度四格 + 分班进度 + 审核情况 + 驳回原因";修 **F-81**(平均审核时长 17 位浮点数 → 服务端 2 位 + 前端按量级换单位)/**F-82**(**首页在说一件不真的事**:写着"M0 后端已完成、界面建设中");并**核销 §四 里 8 条已完成却仍挂"未完成"的条目**(P0-2/P0-3/P1-5/P1-8/P1-9/P1-10/P2-5/P2-10,每条都附了证据) |
|
||
| 2026-09-30 | **定时任务(P2-3,PRD 附录 E 的四项)**:新建 `module/schedule/`(`ScheduleMapper` + `ScheduleJobs` + `ScheduleController`),补上任务自动截止 / 自动催报 / 审核超时提醒 / 原始件清理;新增 `scripts/e2e-m7-schedule.py`(27 条);新增 F-83(**`Map` 结果的别名被折成小写** → 定时任务静默什么都不做,与 F-54 同一个坑的第二次)/F-84(**「HTTP 200 + code 0」不等于动作成功**)/F-85(**清理只在正常结束时跑,失败路径才是残留来源**) |
|
||
| 2026-09-30 | **CI/CD + test/prod 环境(P2-1)**:`.gitlab-ci.yml`(三阶段 6 job,e2e 阶段跑全套断言且每次随机生成密钥)、`application-test.yml` / `application-prod.yml`(零密钥)、`ProdEnvCheck`(启动前自检 6 条)、`docs/09-部署与运维.md`;删掉模板残留的 GitHub Pages 工作流;新增 `scripts/e2e-prodguard.py`(9 条);新增 F-86(**检查配置的代码自己先崩在配置上**)/F-87(**自检写成 ApplicationRunner 就轮不到它**)/F-88(**CI 的 e2e 模板没写 extends 它的 job**) |
|
||
| 2026-09-30 | **定时任务收尾(P2-3 完成)**:V17 迁移(重试三列 + 用量快照表);`ScheduleJobs` 补用量快照 / 压缩失败重试 / 租户到期提醒;`e2e-m7-schedule.py` 扩到 38 条;新增 F-91(**PRD 附录 E 的「待认领名册清理」对应机制已在 V1.5 废除**,故 9 项核销为 8 项);实体/表计数更新为 **27 实体 / 33 表** |
|
||
| 2026-09-30 | **压测(P2-2)**:新增 `scripts/load-test.py`(四场景、按端点统计、可调规模);**修掉 F-92 —— 门禁 IP 限流曾把整间教室挡住(60 人同 IP 只过 20 个)**,改成"只对失败计数";新增 F-93(压测会打到真实七牛空间)/F-94(**我自己的三个度量错误**:吞吐公式、混合 p95、用并发数字推断每行成本);`docs/09-部署与运维.md` 加 §9 压测记录 |
|
||
| 2026-09-30 | **用户手册与客服话术(P2-4)**:新增 `docs/10-用户手册与客服话术.md`(三角色手册 + 标准回答 + 培训提纲 + 上线检查单);新增 `scripts/check-manual.py`(手册引用的错误码/文案 ↔ 代码,已进 CI);新增 F-95(**手册初稿两处"按设想的系统写"**:秒传不存在、代传原因其实选填);docs 一致性检查从 21 项增至 22 项 |
|
||
| 2026-09-30 | **缩略图(P2-7)+ H5 独立 dev server(P2-9)**:`StorageService.thumbUrl` / `FileUrl.thumbUrl` / 审核端 `?thumb=true`;`frontend-mp` 加 `/api` 代理与 `VITE_API_BASE`;新增 `scripts/e2e-m9-thumb.py`(12 条,打到真实七牛);**同步 AGENTS.md 里"H5 没有独立 dev server"那句过期说明**;新增 F-96(**三个"没断言就往下走"**:补丁静默无效 / 夹具静默失败 /不查 code)/F-97(**断言错了字段名**,与 F-77 同类) |
|
||
| 2026-09-30 | **对外待决事项归拢 + 一键交付自检**:`docs/01` 新增 **E 组·第二轮待确认**(E1 `union_id` 是"H5 与小程序不能同时上线"的阻断项,E2 D3 限流口径更新、E3 二期时点,D3 那行划掉);§四 的每条卡点补上"**找谁 / 编号**";交接摘要新增 §一·二十八(坐标刷新 + 这 20 多轮最该记住的 7 条);新增 `scripts/verify-all.py`(一条命令跑完交付自检,**跳过项如实列出、不算失败**)。全量回归 **36 脚本 / 1548 断言 / 0 失败** |
|
||
| 2026-09-30 | **文档小账清理 + `check-test-profile` 加固**:① **附录 A 改为"可再生成"** —— `check-api-parity.py` 加 `--extra` 现算"代码有、契约 §4 没有"的接口(**41 个**;旧手抄表写 83/62/21,已差近一倍),并按"谁去改什么"分六类(只有 E 类要客户定口径);② **F-6 已修**:`frontend-pc/AGENTS.md` 的 Mock 说明改正,**并连带修 `.agents/skills/project-api/SKILL.md`**(整篇仍以 `vite-plugin-mock` 为主线,而 `mock/` 目录与插件都已删除 —— 只改指路牌不改路的尽头等于没改);③ `docs/03` 给 **§2 `notifyBizType` 的 5/6** 与 **§3.6/§9.5 的 `source`** 加 ⚠️ 注(**不改口径**,只把文档内部冲突摆明 + 指出实现以哪边为准,待客户确认);④ `check-test-profile.py` 改为**日志落盘 + 按 jar/profile 清残留 JVM** —— 随机端口让残留**永远撞不到端口**,于是静静攒着,直到某次"怎么都起不来"(新增 F-98) |
|
||
| 2026-09-30 | **补测 `docs/09` §9.5 的"没测过"项(上传并发 / 名册导入规模 / 导出大包 / 长跑稳定性)**,`scripts/load-test.py` 加 `--base` / `--fixture-only` / `--files-per-user` / `--split-mb` / `--soak` / `--rps`,上传夹具换成**真 JPEG**,并新增 `export-big` 场景(造 N 份已通过的单 → 打包 → 逐卷验证 ZIP)。结果:**上传 20 并发 40/40**(串行 321ms / 并发 p50 620ms、p95 801ms;2850KB → 1329KB = **47%**)、**5000 行名册导入 20.2s**(RSS 782MB)、**导出 150 人 194.6MB 打包 13.2s**、**分卷 4 卷每卷可单独解开**、**长跑 60 分钟**(RSS / PG 连接数趋势见 `docs/09` §9.6)。**同一轮里压测工具自己撒了 8 次谎**(前置未断言 / 假 JPEG / 猜列名 / 判据取错返回值 / 跨租户同名账号 / 不查返回码 / 管道截断 / 清理不删文件)→ 新增 **F-99**,并把结论写进 `docs/09` §9.1 / §9.4 / §9.5 / §9.6 |
|
||
| 2026-09-30 | **17 项决策全部执行(A 组改契约 / B 组补代码 / C 组备案 / D 组清环境)**:① **契约 V2.6**:§2.7 `notifyBizType` 5/6 修正(F-30)、`auditAction` 补全 7 动作(F-7)、§3.6/§9.5 `source` 改建表口径(F-38/39)、§9.5 重写为「上传前置检查」+ §9.6 `process` 改语义 + §9.9 标注缓做(F-3/71/72)、§11.4 撤回约束补状态 2、§12.1 `validation/run` 改同步(F-51)、**新增 §17 实现补充与偏差记录**(41 接口六类 + D-1~D-12,F-4/10/26/65);② **代码**:规则模板 FORM 真正可套用 + 存模板也存表单(F-57/64,`e2e-m7-taskconfig` 75/75)、已归档任务可删——级联**物理删** 14 表 + 留痕(F-14,新增 `e2e-m1-task-delete` 19/19)、催报分钟级限流 10 次/分/任务(F-29,`e2e-m5-urge` 42/42)、学生微信登录写 `sys_login_log`(F-31,`WechatLoginLogger`,`e2e-m3-h5-auth` 24/24)、三个纯函数最小单测 12 条(F-9);③ **备案**:通知降级链一期维持两级(F-28→D-11)、国标职业库待客户提供(`docs/01` 新增 **E4**,F-34);④ **环境**:`xt-minio` 已删(F-2)、本地清 49 e2e 租户/147 测试组织/84 演示旧任务(3714 行 + 27 文件,**在用组织保护**保住演示态,F-5)、七牛桶 `xiaotutongxue/` 前缀 **2036 个对象/116MB 清空**(删前核实库中零引用,F-93,新增 `dev-clean-qiniu.py`)——至此 **F-1~F-100 无未决项,剩余全部等外部输入** |
|
||
| 2026-09-30 | **长跑稳定性测完 + 门禁成功上限 200→600(F-100)+ 磁盘清理(F-5 落地)**:① 长跑 51 个样本 / 15300 请求 / **0 失败**,RSS 前 1/3 均值 603.9MB → 后 1/3 582.9MB(**-3.5% 平稳**)、PG 连接 17.0 → 6.2(**定时任务高峰后正常回落**)、p95 62ms 纹丝不动 → 写进 `docs/09` §9.5.4;② **门禁成功侧上限实测卡在年级量级**(250 人 → 37 被挡)→ 上调 600,复测 250/250、500/500,`e2e-m3-gate` 29/29(**F-100**),同步 `docs/01` E2、`docs/09` §9.3;③ `dev-clean-e2e.py` 扩展(`--demo-tasks` / `--orphan-files` / `--only-orphan-files`,业务表按 `information_schema` 动态枚举),**已清磁盘孤儿文件 255 个 / 275.4MB**(uploads 276MB → 0.3MB);e2e 租户与"E2E 24数控1班"组织**刻意不删**(当前演示态挂在其中一个班下面,已记录原因)——问题清单累计 **F-1~F-100** |
|
||
|
||
|
||
|