8480f431ba
后端补齐 /users 全套(列表/新增/修改/软删/启停/重置密码/批量导入)与
PUT /auth/password;前端加「组织与账号 → 账号管理」一屏(左组织树 + 右列表、
新增编辑弹窗、导入体检报告、初始密码只显示一次的强确认)。
验收:e2e-m0-user.py 55/55;e2e-m0.py 回归 36/36;typecheck 0 错、build 通过。
账号管理页已在浏览器肉眼确认(首屏与切页均可见、数据与分页正确)。
本轮修掉的 5 个真问题 —— 都不是功能没做完,而是做完了但错的:
1) 学校租户也发放 PLATFORM_ADMIN 角色行
学校管理员在新增账号时传 roleCodes=[PLATFORM_ADMIN] 就能造出带
tenant:manage 的账号,而 /tenants 只校验权限码、不校验是否真在平台租户下
→ 任何学校管理员都能开通租户、拉全平台租户列表(租户级越权到平台级)。
RoleProvisioner 不再给非平台租户发该角色行 + 服务端按租户白名单校验。
2) UserUpsertReq.username 的 @NotBlank 对修改也生效
PUT /users/{id} 永远返回 40001,且明细只说存在阻断性校验问题,
看不出是登录名 —— 改账号功能整体不可用。去掉该注解,
必填性改由 UserService.create 判定。
3) Main.vue 的 <transition appear>
首次渲染被当成一次过渡,元素被加上 zoom-fade-enter-from(opacity:0)后
永不清理 → DOM 内容齐全、接口数据正常、文字可选中,但整块内容区肉眼不可见。
极易误判成页面没渲染而往路由/接口方向排查。去掉 appear。
4) assertAnotherAdminExists 在停用/删除路径上不可达
能调这两个接口的人自己必然是活跃管理员,所以除目标外还有管理员恒成立;
而真正会锁死租户的路径 —— 把管理员改成普通角色(目标可以是操作者自己)——
当时没有拦。补 losingAdminRole() + UserMapper.countAdminRoles(),
并在方法注释里写明哪条路径真生效、哪条只是防御性的。
5) sys_user.user_type 有两套互相冲突的枚举
DDL/实体注释写「1教师/管理员 2填报人(学生) 3社会人员」,而权威契约
(03-接口设计 §2.7)与 BuiltinRole.UserType 是
「1平台运营 2学校管理员 3班主任 4学生」—— 2 和 3 完全错位。
后果:userType=3(班主任,最高频的账号)被当成社会人员(二期)直接拒掉,
建班主任账号会失败;前端还把学校管理员显示成填报人(学生)。
改法:后端从角色反推用户类型再比对;新增 V3 迁移更正库注释
(V1 已应用不能改,Flyway 会校验 checksum);更正 PRD 与 schema DDL 的同一行;
前端「用户类型」从手选改为由角色推导,从根上消除矛盾组合;
E2E 加 3 条回归断言锁住这个语义。
顺带修掉:
· components/index.ts 里 6 个指向已删组件的悬空导出(构建不报错、用到才炸)
· tsconfig 的 lib 停在 ES2020 而代码用了 Array.prototype.at
· 两处 .at() 的空值风险(useBreakpoint / useTabsStore)
· dashboard 的 userStore.menus 拼错,导致平台判断恒为 false
· http.ts 里两个永不命中的错误码常量
· 账号列表的 fixed 列缺 :scroll 配置,会浮在其它列上面
· .gitignore 补 __pycache__
新增脚本:
· scripts/e2e-m0-user.py 账号管理 55 项验收(含越权与自锁的负向断言)
· scripts/dev-clean-test-users.py 清理 E2E 残留的测试账号
366 lines
25 KiB
Markdown
366 lines
25 KiB
Markdown
# 交接摘要
|
||
|
||
> 用途:**压缩后的唯一状态源**。新会话读这一份即可接续,不必回看历史对话。
|
||
> 更新:2026-09-28
|
||
|
||
---
|
||
|
||
## 一、项目一句话
|
||
|
||
职业技能等级认定**报名材料收集 SaaS**。学生在微信小程序填报、班主任在手机初审、管理员在 PC 终审与导出。核心价值:**动态表单 + 规则引擎(命名/压缩/识别)+ 门禁核验 + 一键规范导出**。
|
||
|
||
工作目录 `D:\IDEA_Project\xiaotutongxue`(git 仓库,**已有 3 次提交**:`63aec55` 文档 / `9d7a8ca` M0 后端 / `87ece5a` 前端换框架;**其后又有一批未提交改动**,见 §一·四),交付物在 `docs/`。
|
||
|
||
---
|
||
|
||
## 一·二、一期范围(2026-09-28 客户确认收窄)
|
||
|
||
> **一期只做「学校收集 + 自主认定」一条流程。** 详见 PRD「一期范围声明」。
|
||
|
||
| 维度 | 字段 | 一期 | 二期后置 |
|
||
|---|---|---|---|
|
||
| 收集模式 | `collect_mode` | `1` 学校收集 | `2` 社会收集 |
|
||
| 认定类型 | `cert_type` | `1` 自主认定 | `2` 社会评价 / `3` 专项职业能力 |
|
||
|
||
**一期材料只有 4 类,全部有实测样本**:一寸照片 / 身份证 / 学籍表 / 工作证明
|
||
**二期才做的 2 类材料零样本**:`XJW_BA` 学信网备案表 / `OTHER_PROOF` 其它佐证材料
|
||
|
||
**数据结构保留不启用**(二期直接开,不需 DB 迁移)——因为这三项本来就是**配置驱动**的
|
||
(`needClassAudit` 是开关、字段隐藏走 `visibleWhen`、材料差异是材料项配置),
|
||
所以**省下的主要是测试成本与一个外部依赖,而不是代码量**。
|
||
|
||
**工期**:392 → **387 人日**(−5);**关键路径不变,仍 15 周**。
|
||
|
||
> ⚠️ **二期启动前必做**:索取社会评价样本(D4)→ 标定 `XJW_BA`/`OTHER_PROOF` → 补社会收集审核流与专项职业能力字段隐藏。
|
||
|
||
---
|
||
|
||
|
||
## 一·三、开发进展(2026-09-28 起)
|
||
|
||
> **代码位置**:`backend/`(Java 21 + Spring Boot 4.1.1)、`deploy/`(基础设施)、`scripts/`(工具)
|
||
|
||
| 项 | 状态 |
|
||
|---|---|
|
||
| 技术栈定版 | ✅ Java 21 + **Spring Boot 4.1.1** + MyBatis-Plus 3.5.17(boot4) + PG16 + Redis7<br>PC: Vue3 + **Gi Admin Pro(Arco Design Vue 2.58)**;小程序: uni-app |
|
||
| M0 后端骨架 | ✅ 统一响应/异常/58 错误码/多租户拦截/软删除/字段填充/AES-GCM/HMAC/RSA/JWT/权限模型 |
|
||
| M0 数据库 | ✅ Flyway 2 个迁移;26 张表(20 业务 + 5 标准 + 1 迁移记录) |
|
||
| M0 后端接口 | ✅ 租户开通与列表、组织 CRUD 与树、PC 登录/登出/当前用户、**账号管理全套(含 Excel 批量导入)** |
|
||
| M0 验收 | ✅ **`python scripts/e2e-m0.py` → 36/36**;**`python scripts/e2e-m0-user.py` → 53/53** |
|
||
| PC 前端 | ✅ 已换 Gi Admin Pro + 腾讯云主题;已实现:登录 / 工作台 / 组织架构 / 租户管理 / **账号管理** |
|
||
| 待做 | 小程序(uni-app)未创建;CI/CD;H5 保底预研;`hasActiveTask` 待 M1 补实现 |
|
||
|
||
---
|
||
|
||
## 一·四、账号管理竖切(2026-09-28 晚,未提交)
|
||
|
||
> 范围:`GET/POST/PUT/DELETE /users`、`PUT /users/{id}/status`、`POST /users/{id}/reset-password`、
|
||
> `POST /users/import`、`PUT /auth/password`;前端「组织与账号 → 账号管理」一屏。
|
||
|
||
**验收**:`python scripts/e2e-m0-user.py` → **55/55**;`python scripts/e2e-m0.py` 回归 **36/36**。
|
||
前端 `pnpm typecheck` **0 错**、`pnpm build` 通过、新增文件 eslint/stylelint 干净。
|
||
账号管理页**已在浏览器肉眼确认**(首屏与切页均可见、8→7 列、数据与分页正确)。
|
||
|
||
### 本轮修掉的 5 个真问题(都不是"功能没做完",是"做完了但错的")
|
||
|
||
| # | 问题 | 后果 | 修法 |
|
||
|:--:|---|---|---|
|
||
| 1 | **学校租户也发放 `PLATFORM_ADMIN` 角色行** | 学校管理员在新增账号时传 `roleCodes:["PLATFORM_ADMIN"]` 即可造出带 `tenant:manage` 的账号;而 `/tenants` 只校验权限码、不校验"是否真在平台租户下" → **任何学校管理员都能开通租户、拉全平台租户列表**(租户级越权到平台级) | ① `RoleProvisioner` 不再给非平台租户发该角色行;② `UserService.resolveAssignableRoles` 按租户白名单校验,越界返回 `40301`。两道都留 |
|
||
| 2 | `UserUpsertReq.username` 上的 `@NotBlank` 对**修改**也生效 | `PUT /users/{id}` 永远返回 `40001`,而错误明细只说"存在阻断性校验问题",看不出是登录名 —— 改账号功能整体不可用 | 去掉该注解(`@Pattern` 对 null 是通过的),"新增时必填"改由 `UserService.create` 判定 |
|
||
| 3 | `Main.vue` 的 `<transition appear>` | 首次渲染被当成一次过渡,元素被加上 `zoom-fade-enter-from/-active`(`opacity:0`)后**永不清理** → **DOM 内容齐全、接口数据正常、文字可选中,但整块内容区肉眼不可见**。极易误判成"页面没渲染"而往路由/接口方向排查 | 去掉 `appear`(首次加载本就不需要入场动画)。**已浏览器复验通过** |
|
||
| 4 | 我写的 `assertAnotherAdminExists` **在停用/删除路径上不可达** | 能调这两个接口的人自己必然是活跃管理员,所以"除目标外还有管理员"恒成立。而真正会锁死租户的路径——**把管理员改成普通角色**(目标可以是操作者自己)——当时没有拦 | 新增 `losingAdminRole()` + `UserMapper.countAdminRoles()`,在改角色时判定;方法注释里写明"哪条路径真生效、哪条是防御性的" |
|
||
| 5 | ⭐ **`sys_user.user_type` 有两套互相冲突的枚举** | DDL/实体注释写「1教师/管理员 2填报人(学生) 3社会人员」,而**权威契约**(《03-接口设计》§2.7)与 `BuiltinRole.UserType` 是「1平台运营 2学校管理员 3班主任 4学生」。两套的 **2 和 3 完全错位**。我照错的注释写了新代码,结果是:**`userType=3`(班主任,最高频的账号)被当成"社会人员(二期)"直接拒掉** —— 建班主任账号会失败;前端「类型」列还把学校管理员显示成"填报人(学生)" | ① 后端改成**从角色反推用户类型**再比对(`ROLE_TO_USER_TYPE`),不再逐条 `if`;② 新增 `V3__fix_user_type_comment.sql` 更正库注释(**不能改 V1,Flyway 会校验 checksum**);③ 更正 PRD 与 `schema-postgresql.sql` 的同一行注释;④ 前端「用户类型」从手选改为**由角色推导**,从根上消除矛盾组合;⑤ E2E 加 3 条回归断言锁住这个语义 |
|
||
|
||
> ⚠️ **第 5 条是最值得记住的一条**:不是"漏了个校验",而是**同一份资料里有两套定义,且都对不上**。
|
||
> 教训:看到"枚举注释"时,要去**权威契约**(这里是《03-接口设计》)核对,不能就近抄实体或 DDL 的注释。
|
||
> 这条也解释了为什么 `V1` 迁移里的错误注释只能靠 `V3` 覆盖 —— 已应用的迁移**一个字都不能改**。
|
||
|
||
### 已知缺口(有意留着,别当成漏做)
|
||
|
||
| 项 | 说明 |
|
||
|---|---|
|
||
| **改密后旧 token 仍有效** | 直到自然过期。要立刻踢人需要 JWT 带签发时间 + 与"最近改密时间"比较,涉及 `JwtService`/`AuthInterceptor`,不适合混在这一刀里。已在 `AuthService.changePassword` 注释里写明 |
|
||
| `hasActiveTask()` 恒为 `false` | `collect_task` 是 M1 的表且当前无"任务负责人"字段,`40014` 这条约束**目前不生效**。方法注释已标红,M1 必须补 |
|
||
| 手机号**模糊**搜不了 | AES-GCM 每次密文不同,`LIKE` 在原理上不可行。完整号码走 `phone_hash` 等值匹配;代价是"只记得中间几位"搜不到,属加密的必然取舍。E2E 里有一条专门断言这个行为(避免后人当 bug 改掉) |
|
||
| **浏览器端验证** | ✅ 已完成。账号管理页首屏与切页均 `opacity: 1`(`appear` 修复生效)、7 列、数据与分页正确、组织树正常;demo 租户已清理干净(1 个账号 `admin`、6 个组织节点)。⚠️ 下一个新页面上线时容易忽略的一点:**新增 `views/` 目录后必须重启 dev server**,见下方环境坑 3 |
|
||
|
||
### 本轮踩到的三个环境坑
|
||
|
||
1. **Maven 的本地仓库不在 `C:\Users\<u>\.m2\repository`**,而在 `D:\maven_repositiory`。在那里查依赖才准。
|
||
2. **`easyexcel-4.0.3.jar` 只有 3KB** —— 它是聚合包,真类在 `easyexcel-core`(528KB)+ `poi 5.2.5`。只看聚合包的大小会误判成"依赖是空的"。这正是本项目"依赖在、功能不在"那条教训的又一例。
|
||
|
||
3. **`import.meta.glob('@/views/**/*.vue')` 在 dev server 里对"新增目录"不总失效**:`views/user/` 建好并 HMR 重载后,路由仍拿不到组件(component 为 `undefined` → 内容区空白)。**新增页面目录后要重启 dev server**,不能只靠 HMR。
|
||
|
||
---
|
||
|
||
**关键实现决策(不要在后继开发中改回去)**
|
||
|
||
| 决策 | 理由 |
|
||
|---|---|
|
||
| **权限码定义在代码里**(`PermissionDef` / `BuiltinRole`),不建 `sys_permission` 表 | 权限码是前后端契约,放 DB 必然与代码漂移;一期无"权限配置界面"需求 |
|
||
| **租户拦截器默认拒绝**:匿名请求触及租户表直接抛 40101 | 漏加租户条件 = 越租户泄露全校身份证号,不可接受的失败模式;宁可让开发者立刻撞墙 |
|
||
| 匿名接口只靠 `@PublicApi` 放行,**不做路径白名单** | 两套机制会让"哪些接口公开"出现两个来源,改一处漏一处 |
|
||
| **登录时账号不存在也跑一次 BCrypt**(假哈希) | 否则响应时间差会泄露"账号是否存在",直接把密码喷洒成功率提高几个数量级 |
|
||
| 密码 RSA 加密传输 | 不是替代 HTTPS,而是防止网关/APM/链路追踪把明文密码记进日志 |
|
||
| 登出黑名单**存 token 的 SHA-256**,TTL 到自然过期 | token 本身是凭证,明文写进 Redis 等于复制了一份到别处 |
|
||
|
||
**四个必须实测的坑**(详见 `07-开发实施计划.md` §9,10 条实录)
|
||
|
||
Boot 4 / Flyway 10 / MP 3.5.9 这一批版本都在"把单体拆成模块",
|
||
导致**「依赖在、功能不在」的静默失败变多**。典型:只加 `flyway-core` 时迁移**不跑也不报错**。
|
||
**规则:每加一个中间件,必须实测它的核心动作真的发生了,不能只看依赖解析成功。**
|
||
|
||
---
|
||
|
||
## 一·五、客户回执(2026-09-28 · 需求已冻结)
|
||
|
||
> **客户全部 23 项按建议方案确认。** 唯一功能增量是 **B1:名单加入申请通道**。
|
||
|
||
| 组 | 需开发 | 说明 |
|
||
|---|:--:|---|
|
||
| A 认定机构 6 项 | **0** | 与现有设计完全一致(`min_long_side=1400`、`min_quality=70`、PNG 保留等都是当初按实测设计的) |
|
||
| B 校方业务 9 项 | **1** | **B1** 已落地:FR-509 系列 + `collect_join_request` 表 + 5 接口 + 2 屏原型 |
|
||
| C 商务法务 5 项 | 0 | C1 小程序资质推进中,H5 保底已列入 M0-13 预研 |
|
||
| D 内部技术 7 项 | 0 | **D4 已后置为二期**(一期不涉及零样本材料);**D7 已降级**(文件名识别准确率不影响任何交付项,残留只是上传页文案校准,班主任群里问两句即可)。**均已清除,无外部依赖** |
|
||
|
||
**规模变化**:FR 115 → **125** / 接口 117 → **121** / 库表 19 → **20** / VAL 20 → **21** / 错误码 50 → **57** / Golden 断言 29 → **36** / 功能验收 85 → **111** / 原型 29 → **31** 屏 / 任务 94 → **100** / 人日 380 → **392** → **387**(一期收窄 −5)/ 工期 14 → **15 周**。
|
||
|
||
**B1 的设计要点(三条边界,不要在后继开发中放水)**
|
||
|
||
| # | 边界 | 理由 |
|
||
|:--:|---|---|
|
||
| 1 | **只解决"进名单",不绕过门禁** | 申请期间仍不能填报;审批通过后才自动绑定放行 |
|
||
| 2 | **证件号与手机号仅存 HMAC + 脱敏值** | 与门禁口径一致 |
|
||
| 3 | **驳回必须逐条填原因;批量通过不提供"全选"** | 身份认定环节误操作代价极高 |
|
||
|
||
---
|
||
|
||
## 二、已锁定的铁律(不要重开讨论)
|
||
|
||
| # | 决策 | 关键理由 |
|
||
|---|---|---|
|
||
| 1 | **扫描件强制压到 ≤1M,无例外** | 认定机构硬要求 |
|
||
| 2 | 默认参数 **300dpi / q85** | 实测 28/28 达标,平均 806KB,清晰度零损失 |
|
||
| 3 | 材料来源只有 **聊天记录 / 相册 / 手机文件**,「直接拍照」默认关闭 | 实测 4 类材料无一来自现场拍照 |
|
||
| 4 | **必须先导名单**,姓名+身份证匹配不通过只提示、不放行(学校与社会收集皆然) | 门禁是产品根基 |
|
||
| 5 | 报考范围**从名单聚合**,不存在独立"计划清单"文件 | 真实名单验证 |
|
||
| 6 | 动态表单 = **内置字段(固定列,可统计)+ 扩展字段(extras_json,仅展示导出)** | 兼顾统计能力与灵活性 |
|
||
| 7 | 数据库 **PostgreSQL**(纯 PG);软删除字段名为 **`deleted`** | 客户已确认 |
|
||
| 8 | 班主任端**只做右滑通过**,驳回必须点按钮 | 误触后果不对称 |
|
||
| 9 | 批量通过**只带无问题项**,红黄一律排除 | 防"手快全选" |
|
||
| 10 | 有不合格文件时**直接禁用导出按钮**,不允许带问题打包 | 铁律 |
|
||
| 11 | 识别策略 **规则为主 + VLM 兜底**(触发率 2%~10%) | 成本可控 |
|
||
| 12 | 识别失败**一律放行**,不得阻断提交 | 可用性优先 |
|
||
| 13 | 原型**不用 emoji**(`→` `↔` `·` 等排版字符保留) | 设计约束 |
|
||
|
||
---
|
||
|
||
## 三、已完成交付物
|
||
|
||
| 文件 | 版本/大小 | 状态 |
|
||
|---|---|---|
|
||
| `docs/数据信息填报系统PRD.md` | V2.4 / 189KB / 3019 行 | ✅ 含 **125 条 FR**、**21 条 VAL**、**121 个接口**、**20 表 DDL**、实测附录 |
|
||
| `docs/数据信息填报系统PRD-功能逻辑卷.md` | V1.9 / 88KB | ✅ 纯功能逻辑,**不含库表/接口/代码** |
|
||
| `docs/db/schema-postgresql.sql` | 37KB | ✅ **PG 16.15 实测零错误**(20 表 / 177 列注释 / 65 索引 / 20 表注释) |
|
||
| `docs/prototype/student-app.html` | **11 屏** / 57KB | ✅ 含 STEP 03B「申请加入名单」 |
|
||
| `docs/prototype/teacher-app.html` | 9 屏 / 63KB | ✅ |
|
||
| `docs/prototype/admin-web.html` | **11 屏** / 114KB | ✅ 含表单设计器、材料规则配置、历史整理器、**申请审批** |
|
||
| `docs/prototype/index.html` | 7KB | ✅ 三端入口 |
|
||
| `docs/00-交付计划.md` | — | ✅ 7 项计划与进度记录 |
|
||
| `docs/01-客户与机构确认函.md` | 23KB | ✅ **P1** 23 项**已全部确认**(客户按建议执行,需求冻结) |
|
||
| `docs/02-需求追溯矩阵.md` | 10KB | ✅ **P2** 审计报告,含 10 个接口缺口 |
|
||
| `docs/03-接口设计.md` | **115KB / 2883 行** | ✅ **P3 121 个接口**字段级定义 + 枚举全集 + 12 个 VO + **58 个错误码**;§4 逐节声称数与表行数一致 |
|
||
| `docs/06-验收测试清单.md` | **33KB** | ✅ **P6 36 条 Golden 断言** + **111 条功能验收** + **27 条异常边界** |
|
||
| `../README.md` | **10KB** | ✅ **P7 文档导航**,按角色读法 + 快速上手 |
|
||
| `docs/04-核心模块设计说明.md` | **69KB / 1453 行** | ✅ **P4 六个模块**伪代码级说明 + 依赖图 + Golden 基线 |
|
||
| `docs/05-开发任务拆解.md` | **31KB / 506 行** | ✅ **P5 100 个任务 / 387 人日 / 15 周**,含关键路径、一期范围与核算口径 |
|
||
|
||
---
|
||
|
||
## 四、待办计划
|
||
|
||
| # | 任务 | 产出 | 状态 |
|
||
|---|---|---|---|
|
||
| ~~P1~~ | 客户与认定机构确认函 | `01-客户与机构确认函.md` | ✅ |
|
||
| ~~P2~~ | 需求追溯矩阵 | `02-需求追溯矩阵.md` | ✅ |
|
||
| ~~P2.5~~ | 管理端补 3 屏原型 | 并入 `admin-web.html` | ✅ |
|
||
| **P3** | **接口设计(OpenAPI)** | `03-接口设计.md` | ✅ **已完成**(117 个接口) |
|
||
| P4 | 核心模块设计说明(伪代码级) | `04-核心模块设计说明.md` | ✅ **已完成**(6 个模块) |
|
||
| P5 | 开发任务拆解 WBS(M0–M7) | `05-开发任务拆解.md` | ✅ **已完成**(94 任务 / 380 人日 / 14 周) |
|
||
| P6 | 验收测试清单(Golden Test Set) | `06-验收测试清单.md` | ✅ **已完成**(29 条 Golden 断言 + 85 条功能验收) |
|
||
| P7 | 文档导航 README | `../README.md` | ✅ **已完成** |
|
||
|
||
### P3 已补齐的缺口(21 个接口,原审计发现 10 项)
|
||
|
||
```
|
||
✅ I1 报考范围读取/确认 GET/PUT /api/v1/tasks/{id}/plan-scope
|
||
✅ I2 历史数据整理器 POST /api/v1/cleanup/jobs(共 5 个)
|
||
✅ I3 字典管理 /api/v1/dicts(4 个)
|
||
✅ I4 操作日志 GET /api/v1/logs/operations
|
||
✅ I5 登录日志 GET /api/v1/logs/logins
|
||
✅ I6 消息中心 /mp/messages、/mp/messages/read、/mp/teacher/messages
|
||
✅ I7 订阅额度登记 POST /api/v1/mp/subscribe
|
||
✅ I8 审核撤回 POST /api/v1/audit/submissions/{id}/revoke
|
||
✅ I9 任务暂停/恢复 POST /api/v1/tasks/{id}/pause、/resume
|
||
✅ I10 材料识别结果读取 已并入 GET /api/v1/audit/submissions/{id} 响应
|
||
➕ 文件规范化 process 原仅在 PRD §8.9 详述、未登记进清单,已补登
|
||
```
|
||
|
||
**接口总数:96(PRD 原有)+ 21 = 117**
|
||
|
||
---
|
||
|
||
## 五、关键事实速查
|
||
|
||
### 真实基准数据集(Golden Test Set)
|
||
|
||
`D:\项目\202609机电填报系统\24中技数控1班\` —— **28 人 × 4 类材料 = 112 文件**
|
||
|
||
| 实测项 | 结果 |
|
||
|---|---|
|
||
| 原始总量 | 475 MB |
|
||
| 命名违规 | 59 / 112(52.7%) |
|
||
| 双扩展名 | 56 |
|
||
| 体积超标(>1M) | 31 |
|
||
| 证件号异常 | 5 例(3 可自动、2 待人工) |
|
||
| 疑似串档 | 1(**唯一必须人工判断的**) |
|
||
| 压缩后 | 300dpi/q85 → 平均 806KB、最大 831KB、**0 张超标** |
|
||
| 材料识别 | 文件名层 112/112;形态层 99.1%;OCR 内容层 32/32 |
|
||
|
||
### 官方模板要点
|
||
|
||
- 21 列(A–U):**红=必填 11 项、绿=级联 6 项、黑=选填 4 项**
|
||
- **「报考职业」「报考工种」在 Excel 里没有下拉,是自由填写** ← 我方产品的核心增量
|
||
- 「职业技能等级个人申报表」的规范文件名却是 **「工作证明」**(机构既有习惯)
|
||
- **表头行与工作表名都不固定**(实测一份在第 1 行、一份在第 6 行)
|
||
|
||
### 三个易错实现陷阱
|
||
|
||
1. **「无」是合法业务值,不是空值**
|
||
2. **专项职业能力:无工种 / 无等级 / 无补考科目(0 科)**
|
||
3. 补考约束:三/四/五级最多 1 科,一/二级最多 2 科,专项 0 科
|
||
|
||
### 压缩五档(PRD §6.3)
|
||
|
||
A4 扫描件 300dpi(2480×3508)/q85 · 一寸照片固定 295×413 · 学籍表为 16:9 截图**保持 PNG 不缩放**
|
||
|
||
### 教师端数字口径(原型必须自洽)
|
||
|
||
28 人 = 已通过 15 + 待审 4 + 待核对 3 + 有问题 1 + 未提交 5;**完成率 82% 是已提交率(23/28)**
|
||
|
||
---
|
||
|
||
## 六、环境与工具
|
||
|
||
| 项 | 内容 |
|
||
|---|---|
|
||
| PG 验证容器 | `docker exec -it pg-verify psql -U postgres -d filldb`;清理 `docker rm -f pg-verify` |
|
||
| 原型校验脚本 | `python C:\Users\93292\AppData\Local\Temp\opencode\check2.py <html路径>` |
|
||
| DDL 同步方式 | 从主 PRD 的 ` ```sql ` 代码块自动抽取生成,改文档后重跑抽取脚本 |
|
||
| 原型约束 | 单文件自包含、零外部依赖、内联 SVG(`<symbol>`+`<use>`,1.7px 描边 currentColor)、无 emoji |
|
||
| 原型校验基线 | 四份全通过:标签配对 ✓ / CSS 变量 ✓ / SVG 无冗余 ✓ / emoji 0 ✓ |
|
||
|
||
---
|
||
|
||
## 七、重要教训
|
||
|
||
### 教训一:脚本审计前必须先验证脚本本身
|
||
P2 审计时脚本因未匹配加粗编号 ``| **FR-614** |``,误报 3 个"需求条目问题"(实为假警报);若直接采信并"修复",会引入 3 处新错误。同时 FR 总数由误算的 102 修正为 **115**。
|
||
|
||
> **⚠️ 这条教训在 2026-09-28 写开发实施计划时又复现了一次。**
|
||
> 我为交接摘要重算交付物体量,用了三个正则去数"接口数",分别得到 **74 / 95 / 121**——
|
||
> 三个都"看起来对",但没有一个经过验证。
|
||
>
|
||
> 定位过程:**逐节对比「标题声称数」与「实际表行数」**,发现
|
||
> ① 我漏了 `| ⭐ GET | ... |` 这种 ⭐ 在方法**前面**的行;
|
||
> ② §4.9 / §4.11 是**索引节**(交叉引用别处已计数的接口),本就不该计入。
|
||
> 修正后逐节声称数与表行数**全部为 0 差**,合计 **121**。
|
||
>
|
||
> **加固**:`scripts/verify-docs.py` 现在会做「**逐节声称数 vs 实际行数**」的一致性检查,
|
||
> 而不是只看总数对不对——总数相等也可能是个别节多算、个别节少算恰好抵消。
|
||
>
|
||
> **可复用的规则**:
|
||
> 1. 数字类断言必须**交叉核对至少两种独立方法**;
|
||
> 2. 核对要**分块**(逐节/逐表),不能只看总和;
|
||
> 3. 相互矛盾的中间值**一个都不要先写进文档**——写错再改,比一开始就空着更容易误导人。
|
||
|
||
### 教训二:写新文档必须回查已有编号体系,不能凭记忆
|
||
P4 写 M5 校验引擎时,我凭记忆用了一套 VAL 编号,**与 PRD 权威清单整体错位**:
|
||
|
||
| P4 原写法 | 我以为的含义 | PRD 实际定义 |
|
||
|---|---|---|
|
||
| VAL-01 | 姓名与名册一致 | **证件号格式与校验位** |
|
||
| VAL-03 | 证件号格式 | **姓名与名册一致** |
|
||
| VAL-06 | 双扩展名 | **文件格式白名单** |
|
||
| VAL-07 | 多扩展名冲突 | **文件体积上限** |
|
||
|
||
**后果**:若不修正,开发按 P4 实现会把规则完全接错,且两份文档互相矛盾。
|
||
**修正**:已全部重映射到 PRD 体系(单次扫描替换避免 01↔03 碰撞),并给 PRD 补了 P4 需要的三条新规则 **VAL-18 文件名规范化 / VAL-19 疑似串档 / VAL-20 体积下限保护**。
|
||
**规则**:新增文档引用任何已有编号(FR/VAL/错误码/接口)前,**先脚本抽取权威清单核对**,不凭记忆。
|
||
|
||
### 教训三:系统里的数据不会错,错的一定是「归属」
|
||
客户指出:**"系统应该不存在名称错误的情况。是先录入了有这个姓名和身份证,然后才能填写的。"**
|
||
|
||
这指出一个真实的设计缺陷:
|
||
|
||
| 位置 | 会不会错 | 说明 |
|
||
|---|---|---|
|
||
| **填报单里的姓名/证件号** | **永远不会错** | 门禁已核验 + 字段锁定。客户说得对 |
|
||
| 文件名里的姓名/证件号 | 可能不同 | **但这不是"名称错误",而是"这份文件可能不属于这个人"** |
|
||
|
||
**原设计的错误**:把"文件名姓证件号与名册不符"处理为「自动按名册修正」(信息级),
|
||
这会把"A 传了 B 的材料"**静默洗白**成 A 的材料——审核时完全看不出来,事后无从追溯。
|
||
|
||
```
|
||
学生 A 在微信里误选了同学 B 的材料
|
||
↓ 文件名:李四-3610...(B 的号码,校验位通过)
|
||
错误做法:判"与名册不符" → 改成 张三-3610... → 信息级,A 看不到
|
||
↓ 审核:班主任看到一份名字正确的材料 → 通过
|
||
↓ 最终:B 的工作证明被认定成 A 的 ← 灾难
|
||
```
|
||
|
||
**修正(客户已确认)**:
|
||
1. **判定优先级重排**为 `证件号反查名册 > OCR 内容 > 姓名(仅辅助)`
|
||
2. 新增 **VAL-21 文件归属校验**:归属存疑一律**阻断提交**,且**不提供"仍然继续"**
|
||
3. **姓名不再作为独立规则**——只在证件号佐证时按名册取值,仅留痕不打扰学生
|
||
4. 核心判据:**文件名证件号能否在名册里反查到"另一个人"**(一次查询,走唯一索引,< 1ms)
|
||
|
||
**精髓在于两条的区分**:
|
||
- **校验位失败** = 不是一个真实号码 → 只是写错了 → **可以自动改**
|
||
- **校验位通过** = 一个真实存在的自然人 → 改了就是改了身份 → **必须阻断**
|
||
|
||
### 教训五:联系方式不进系统
|
||
客户指出:**"学生本来就有班主任联系方式,不需要系统里面体现。"**
|
||
|
||
原设计在门禁失败页放了一个「联系老师」按钮(脱敏手机号 + 一键拨打),并为此专门开了一个接口 `GET /mp/roster/contact`。
|
||
已移除:
|
||
|
||
| 移除项 | 原因 |
|
||
|---|---|
|
||
| `GET /mp/roster/contact` 接口 | 不必要(用户已知道怎么找人)+ 有维护成本(换人/换号要同步)+ 多一处隐私暴露面 |
|
||
| 门禁失败页「联系老师」按钮 | 同上。改为只保留两条**系统内动作**:重新填写 / 申请加入名单 |
|
||
| 功能逻辑卷中的联系方式展示描述 | 同步 |
|
||
|
||
**保留**:纯文案提示(如"如有疑问请联系老师")——**不涉及任何联系方式数据**。
|
||
**接口总数**:122 → **121**。
|
||
|
||
### 教训四:「社会评价」与「社会收集」只差一个字
|
||
`cert_type`(认定类型:自主认定 / **社会评价** / 专项职业能力)决定**填什么材料**;
|
||
`collect_mode`(收集模式:学校收集 / **社会收集**)决定**谁审核**。二者可任意组合,已在 PRD §5.3 与接口 §2.3 分别写明。
|
||
另外补全了 `cert_type` 的第三个取值(专项职业能力)在 DDL 注释与附录中的遗漏。
|
||
|
||
### 附带修正
|
||
- **体积口径**:原型导出中心曾写"475MB → 22.4MB(95%)",与 PRD 及历史整理器屏(37MB/92%)矛盾。实为混淆——**444→22MB 是"工作证明"单项,475→37MB 是全班 112 文件合计**。已统一为 37MB / 92%。
|
||
|
||
---
|
||
|
||
## 八、外部依赖(未回)
|
||
|
||
`01-客户与机构确认函.md` 已发出待回,其中**最高优先级 3 项**:
|
||
|
||
- **A1** 认定机构是否书面接受 300dpi 扫描件
|
||
- **B1** 名单外的人怎么处理(建议加"申请加入名单→管理员审批"通道)
|
||
- **B3** 是否允许学生修改老师预填字段(建议:姓名/证件号/报考三项锁定,其余可改留痕)
|
||
|
||
未回复则按文档内建议方案推进。
|