docs: 同步交接摘要 / PRD / 原型,并记录品牌改名「深学在线」

- 交接摘要补 §一·三十五~§一·三十八:角色与授权、需求收敛(4 角色 / 10 菜单 / 取消二期)、
  PC 端重搭后的 UI 修复与卡片式基线、品牌改名与新标识
- PRD 与功能逻辑卷只改标题行与产品代号线;**文件名不动** —— verify-docs.py 按路径读,
  全库还有 20+ 处按文件名引用,改名会连带炸脚本
- 原型 index / admin-web / teacher-app / student-app 的品牌区改「深学在线」,
  admin-web 的 .mark(10 处)换成内嵌的新标 data URI,保持原型"单文件自包含"的约定
- 根 AGENTS.md 与 frontend-pc 的规范同步:接线检查补第 ⑨ 项(SVG 资产是不是合法 XML ——
  SVG 注释里出现连续两个减号就 not well-formed,图标裂开而控制台一句话都不报)、
  「最容易写错的约定」6 -> 7 条(新增品牌标)
- deploy/docker-compose.yml 只改头注释里的项目名(compose 的 name 与七牛对象前缀都没动)
This commit is contained in:
2026-10-10 16:49:04 +08:00
parent 1f3c485ceb
commit 7df078e0ea
22 changed files with 1158 additions and 1093 deletions
+181 -362
View File
@@ -14,16 +14,16 @@
| §1 | 通用约定(域名、鉴权、响应、分页、幂等、限流、脱敏) |
| §2 | 枚举字典全集 |
| §3 | 通用数据结构(Schema 定义) |
| §4 | 接口总览(**121 个**) |
| §4 | 接口总览(**119 个**) |
| §5 | 认证与账号(13) |
| §6 | 组织与租户(15) |
| §7 | 任务与配置(30) |
| §8 | 名册(12) |
| §7 | 任务与配置(25) |
| §8 | 名册(7) |
| §9 | 填报端 · 小程序(16) |
| §10 | 班主任移动端(10) |
| §11 | 审核(9) |
| §12 | 校验 / 导出 / 通知(12) |
| §13 | **本次补齐的缺口接口(21)** |
| §13 | **本次补齐的缺口接口(16)** |
| §14 | 开放接口 · 供认定机构对接(4) |
| §15 | 错误码全集 |
@@ -212,6 +212,12 @@
> ⚠️ **`WORK_PROOF` 的名称陷阱**:按机构模板,**「职业技能等级个人申报表」的规范文件名是「工作证明」**。这是机构既有习惯,不要"修正"。
> ⚠️ **一期实际只有 4 类材料**:`PHOTO1` / `IDCARD` / `SCHOOL_ROLL` / `WORK_PROOF`。
> `XJW_BA` 与 `OTHER_PROOF` 是需求收敛(2026-10)**之前**留下的编码 ——
> **历史数据读得到、但"新增材料"选不到**(`GET /api/v1/material-catalog` 的可选项不含它们),
> 内置材料模板也只剩 `TPL_SCHOOL_ROLL` 一个(原 `TPL_SOCIAL` / `TPL_ZHUANXIANG` 已删)。
> 这样处理是为了**不让历史任务里的材料项悬空**:删编码会让已配的任务读不出材料名。
### 2.2 收集模式 `collectMode`
| 值 | 含义 | 审核流 |
@@ -266,7 +272,7 @@
| 枚举 | 取值 |
|---|---|
| `userType` | `1` 平台运营 / `2` 学校管理员 / `3` 班主任 / `4` 学生 |
| `roleCode` | `PLATFORM_ADMIN` / `SCHOOL_ADMIN` / `HEAD_TEACHER` / `DEPUTY_TEACHER` / `STUDENT` / `AUDITOR` |
| `roleCode` | `PLATFORM_ADMIN` / `SCHOOL_ADMIN` / `HEAD_TEACHER` / `STUDENT` |
| `taskStatus` | `0` 草稿 / `1` 收集中 / `2` 已截止 / `3` 已归档 / `4` **已暂停** (与后端 `CollectTask.STATUS_*` 及 DDL 列注释一致)|
| `rosterSubmitStatus` | `0` 未提交 / `1` 已提交 / `2` 已通过 / `3` 已驳回 |
| `fileValidateStatus` | `0` 待校验 / `1` 通过 / `2` 警告 / `3` 不通过 / `4` 待人工 |
@@ -275,10 +281,8 @@
| `detectSource` | `1` 文件名 / `2` 形态 / `3` OCR 内容 / `4` VLM / `5` 用户指定 |
| `compressProfile` | `P1` 证件照 / `P2` 证件翻拍 / `P3` A4 扫描件 / `P4` 截图 / `P5` 兜底 |
| `notifyChannel` | `1` 微信订阅消息 / `2` 短信 / `3` 公众号模板 |
| `notifyBizType` | `1` 任务发布 / `2` 临期提醒 / `3` 驳回通知 / `4` 审核通过 / `5` **催报** / `6` **导出完成** / `7` 加入名单审批 |
| `notifyBizType` | `1` 任务发布 / `2` 临期提醒 / `3` 驳回通知 / `4` 审核通过 / `5` **催报** / `6` **导出完成** |
| `exportStatus` | `0` 排队中 / `1` 打包中 / `2` 已完成 / `3` 失败 / `4` 已过期 |
| `cleanupSourceType` | `1` 压缩包 / `2` 文件夹 / `3` 云盘链接 |
| `cleanupStatus` | `0` 排队 / `1` 解析中 / `2` 处理中 / `3` 已完成 / `4` 失败 |
> ✅ **`notifyBizType` 的 `5`/`6` 曾经在本文件内部对调过(已修正,2026-09-30 客户确认)**:
> 本表旧版写「`5` 导出完成 / `6` 催报」,与 §13.5 消息中心示例、落库值
@@ -487,7 +491,7 @@
| `vlmCalled` | bool | `vlm_called` | 是否调用过 VLM(**成本监控用**) |
| `validateStatus` | int | `validate_status` | 见 §2.7 `fileValidateStatus` |
| `issues` | array | — | 该文件的 `ValidationIssueVO[]` |
| `source` | int | `source` | `1` 本人上传 / `2` 班主任代传 / `3` 管理员代传 / `4` 整理器导入 |
| `source` | int | `source` | `1` 本人上传 / `2` 班主任代传 / `3` 管理员代传 / `4` 整理器导入(**该模块已取消,仅历史数据**) |
| `rawExpireTime` | string | `raw_expire_time` | **原图过期时间(默认 30 天)** |
> ✅ **`collect_file.source` 的口径曾经有两套,已确认(2026-09-30 客户确认)**:本表旧版写
@@ -573,7 +577,6 @@
| `UserVO` | `id` `username` `realName` `phone` `orgId` `orgName` `roleCodes[]` `userType` `status` `lastLoginTime` |
| `ExportTaskVO` | `id` `taskId` `exportName` `scopeJson` `splitSizeMb` `desensitize` `status` `progress` `fileCount` `fileSizeMb` `resultFiles[]{ name, sizeMb, url }` `errorMsg` `expireTime` `finishTime` |
| `NotifyRecordVO` | `id` `taskId` `channel` `bizType` `receiverId` `receiverName` `receiverRole` `templateCode` `content` `status` `failReason` `sendTime` |
| `CleanupJobVO` | `id` `jobName` `sourceType` `sourcePath` `targetTaskId` `status` `progress` `totalFiles` `resultPath` `reportJson` `finishTime` |
| `DictItemVO` | `id` `dictCode` `dictName` `itemCode` `itemName` `sortNo` `parentItemCode` `status` `builtin` |
| `MessageVO` | `id` `bizType` `title` `content` `read` `linkType` `linkId` `createTime` |
@@ -581,7 +584,10 @@
## §4 接口总览
> 合计 **121 个**:PRD §8 已定义 96 个 + 追溯矩阵补齐 **21 个** + 客户确认新增 **5 个**,减去客户要求移除的联系方式接口 1 个(标 ⭐)。补齐项见 §4.9 索引,客户确认新增见 §8.7。
> 合计 **119 个**:PRD §8 已定义 96 个 + 追溯矩阵补齐 **16 个** + **「角色与授权」8 个**,减去客户要求移除的联系方式接口 1 个(标 ⭐)。
> 补齐项见 §4.9 索引,角色与授权见 §4.12 与 §5.10。
> ⚠️ **需求收敛(2026-10)的减法**:原「客户确认新增 5 个」(加入申请,确认函 B1)**整条通道取消**,
> 原「补齐 21 个」中的「历史整理器 5 个」**随模块删除** → 21 − 5 = 16,故 96 + 16 + 8 − 1 = **119**。
> 「权限」列:`公开` / `学生` / `教师` / `管理` / `运营`(平台方)。
> 「修补」列指该接口是补齐追溯矩阵审计发现的缺口。
@@ -589,14 +595,15 @@
|---|:--:|
| 认证与账号 | 13 |
| 组织与租户 | 15 |
| 任务与配置 | 30 |
| 名册 | 12 |
| 任务与配置 | 25 |
| 名册 | 7 |
| 填报端 · 小程序 | 16 |
| 班主任移动端 | 10 |
| 审核 | 9 |
| 校验 / 导出 / 通知 | 12 |
| 开放接口 | 4 |
| **合计** | **121** |
| ⭐ 角色与授权 | 8 |
| **合计** | **119** |
### 4.1 认证与账号(13)
@@ -636,7 +643,7 @@
| ⭐ GET | `/logs/operations` | **操作日志** | 管理 |
| ⭐ GET | `/logs/logins` | **登录日志** | 管理 |
### 4.3 任务与配置(30)
### 4.3 任务与配置(25)
| | 路径 | 说明 | 权限 |
|---|---|---|---|
@@ -665,13 +672,8 @@
| GET | `/tasks/{id}/qrcode` | 填报二维码 | 管理 |
| GET | `/rule-templates` | 规则模板列表 | 管理 |
| POST | `/rule-templates/{id}/apply` | 应用模板 | 管理 |
| ⭐ POST | `/cleanup/jobs` | **创建整理任务** | 管理 |
| ⭐ GET | `/cleanup/jobs` | **整理任务列表** | 管理 |
| ⭐ GET | `/cleanup/jobs/{id}` | **整理进度** | 管理 |
| ⭐ GET | `/cleanup/jobs/{id}/report` | **体检报告** | 管理 |
| ⭐ POST | `/cleanup/jobs/{id}/import` | **结果导入为正式任务** | 管理 |
### 4.4 名册(12)
### 4.4 名册(7)
| | 路径 | 说明 | 权限 |
|---|---|---|---|
@@ -682,11 +684,6 @@
| GET | `/tasks/{id}/roster/template` | 下载导入模板 | 管理 |
| POST | `/roster/{rid}/notify` | 定向催报 | 管理/教师 |
| POST | `/tasks/{id}/roster/notify-unsubmitted` | 一键催报未提交者 | 管理/教师 |
| ⭐ POST | `/mp/roster/join-request` | **提交加入申请** | 学生 |
| ⭐ GET | `/mp/roster/join-request` | **我的申请状态** | 学生 |
| ⭐ GET | `/roster/join-requests` | **申请待办列表** | 管理 |
| ⭐ POST | `/roster/join-requests/{id}/approve` | **审批通过** | 管理 |
| ⭐ POST | `/roster/join-requests/{id}/reject` | **审批驳回** | 管理 |
### 4.5 填报端 · 小程序(16)
@@ -757,10 +754,11 @@
| GET | `/notify/records` | 通知记录 | 管理 |
| POST | `/callback/wechat/msg` | 微信订阅消息回调 | 公开 |
### 4.9 ⭐ 本次新增接口索引(21)
### 4.9 ⭐ 本次新增接口索引(16)
> 本表是**索引**,不是额外接口——其中 5 个已在上方 §4.3 / §4.7 列出,**不计入总数**。
> 表内 18 行覆盖 **21 个接口**(含 GET/PUT、GET/POST 合并行)。
> 表内 13 行覆盖 **16 个接口**(含 GET/PUT、GET/POST 合并行)。
> ⚠️ 需求收敛(2026-10)后,原表里的**历史整理器 5 个**随模块删除,故 21 − 5 = **16**。
| | 路径 | 说明 | 详情节 |
|---|---|---|---|
@@ -768,11 +766,6 @@
| ⭐ | `/tasks/{id}/pause` `/resume` | 暂停 / 恢复任务 | §7.4 |
| ⭐ | `/audit/submissions/{id}/revoke` | 撤回已审 | §11.4 |
| ⭐ | `/mp/files/{fileId}/process` | **文件规范化总入口**(原仅在 §8.9 详述,未登记进清单) | §9.6 |
| ⭐ | `/cleanup/jobs` | 创建整理任务 | §13.2 |
| ⭐ | `/cleanup/jobs` | 整理任务列表 | §13.2 |
| ⭐ | `/cleanup/jobs/{id}` | 整理进度 | §13.2 |
| ⭐ | `/cleanup/jobs/{id}/report` | 体检报告 | §13.2 |
| ⭐ | `/cleanup/jobs/{id}/import` | 结果导入为正式任务 | §13.2 |
| ⭐ | `/dicts` | 字典列表 | §13.3 |
| ⭐ | `/dicts/{code}/items` | 字典项(GET / POST) | §13.3 |
| ⭐ | `/dicts/items/{id}` | 修改字典项 | §13.3 |
@@ -783,7 +776,7 @@
| ⭐ | `/mp/teacher/messages` | 教师消息中心 | §13.5 |
| ⭐ | `/mp/subscribe` | 订阅消息额度登记 | §13.6 |
**新增 21 个的分布**
**新增 16 个的分布**
| 归属 | 数量 | 明细 |
|---|:--:|---|
@@ -791,7 +784,6 @@
| 任务状态 | 2 | pause / resume |
| 审核 | 1 | revoke |
| 文件处理 | 1 | process(登记遗漏) |
| 历史整理器 | 5 | cleanup 全套 |
| 字典 | 4 | dicts |
| 日志 | 2 | operations / logins |
| 消息中心 | 3 | 学生 2 + 教师 1 |
@@ -806,33 +798,35 @@
| GET | `/open/v1/tasks/{id}/files` | 批量获取文件签名 URL |
| POST | `/open/v1/callback/push` | 推送审核结果到机构系统 |
### 4.11 ⭐ 客户确认后新增(5)
### 4.11 ⭐ 加入申请(已撤销 · 0 个)
> 来源:确认函 **B1**(客户确认采用"申请加入名单 → 管理员审批"通道)。这是本次唯一的功能增量。
> 详述见 **§8.7**。
> ⚠️ **本节原为「客户确认后新增 5 个」**(确认函 **B1**:申请加入名单 → 管理员审批)。
> **需求收敛(2026-10)后整条通道取消**:模块 `module/join`、5 个接口、`join:audit` 权限码、
> `collect_join_request` 表、学生端「申请加入名单」页与 5 个专用错误码
> (`40037` / `40038` / `40039` / `40040` / `40042`)**已全部删除**。
>
> **门禁失败后的口径**:只保留「**重新填写**」一个系统内动作 + 一句纯文案提示
> "如确认已报名,请联系老师把你加入名单"。名单外的人由老师在任务里**手工加人**(`FR-508` 追加导入),
> **不展示任何联系方式**(铁律:联系方式不进系统)。
>
> ⚠️ **治理提示**:B1 是确认函里**唯一的功能增量**,本次属**需求回退**,建议补一张变更确认单。
### 4.12 ⭐ 角色与授权(8)
> 来源:**FR-205「角色自定义与数据权限」**。内置角色 **4 个**(平台运营 / 学校管理员 / 班主任 / 学生,
> 权限由代码 `BuiltinRole` 决定、界面只读),租户可另建**自定义角色**并配置权限码与可见菜单。
> 详述见 **§5.10**。
| | 路径 | 说明 | 权限 |
|---|---|---|---|
| ⭐ | `/mp/roster/join-request` | 提交加入申请 / 查看我的申请(GET / POST) | 学生 |
| ⭐ | `/roster/join-requests` | 申请待办列表 | 管理 |
| ⭐ | `/roster/join-requests/{id}/approve` | 审批通过(自动建名册项 + 自动绑定) | 管理 |
| ⭐ | `/roster/join-requests/{id}/reject` | 审批驳回(原因必填) | 管理 |
**设计边界(三条)**
| # | 边界 | 理由 |
|:--:|---|---|
| 1 | **只解决"进名单",不绕过门禁** | 申请期间仍不能填报;审批通过后学生**仍需重新核验**(或凭已绑定 openid 直接放行) |
| 2 | **证件号仅存 HMAC,不落明文** | 与门禁口径一致;申请记录保留 2 年 |
| 3 | **驳回必须逐条填原因;批量通过不提供"全选"** | 身份认定环节误操作代价极高,与"批量通过只带绿色项"同源 |
**与现有机制的关系**
| 机制 | 区别 |
|---|---|
| `FR-508` 名册追加导入 | 老师批量加人;申请通道是**学生自助 + 逐条审批** |
| `FR-504` 统一失败提示 | 申请通道**不破坏**统一提示——`40039`(已在名单中)不泄露新信息 |
| `FR-504d` 已被他人绑定 | 申请通过后若该证件号已被占用 → `40041`,不覆盖 |
| ⭐ GET | `/roles` | 角色列表(内置 + 自定义) | 管理 |
| ⭐ GET | `/permissions` | 权限码目录(按分组) | 管理(`role:manage`) |
| ⭐ POST | `/roles` | 新增自定义角色 | 管理(`role:manage`) |
| ⭐ PUT | `/roles/{id}` | 修改自定义角色(名称 / 用户类型 / 备注) | 管理(`role:manage`) |
| ⭐ DELETE | `/roles/{id}` | 删除自定义角色(软删) | 管理(`role:manage`) |
| ⭐ PUT | `/roles/{id}/permissions` | 设置权限码(覆盖式) | 管理(`role:manage`) |
| ⭐ PUT | `/roles/{id}/menus` | 设置可见菜单(覆盖式) | 管理(`role:manage`) |
| ⭐ PUT | `/roles/{id}/status` | 启用 / 停用角色 | 管理(`role:manage`) |
---
@@ -988,8 +982,9 @@
> ② 有维护成本——班主任换人、号码变更都要同步;
> ③ 多一处隐私暴露面——即使是脱敏号,也意味着系统里要多存一份联系方式数据。
>
> **替代**:门禁失败页只保留两条**系统内的动作**——「重新填写」与「申请加入名单」(FR-509)。
> 纯文案提示(如"如有疑问请联系老师")保留,因为**不涉及任何联系方式数据**。
> **替代**:门禁失败页只保留「**重新填写**」一个**系统内动作**(原「申请加入名单」通道已于 2026-10 需求收敛时取消)。
> 纯文案提示(如"如确认已报名,请联系老师把你加入名单")保留,因为**不涉及任何联系方式数据**;
> 名单外的人由老师在任务里手工加人(`FR-508` 追加导入)。
### 5.6 `GET /api/v1/users`
@@ -1048,6 +1043,92 @@
---
### 5.10 角色与授权(§4.12 的 8 个接口)
> 对应 **FR-205**。内置角色 **4 个**(`PLATFORM_ADMIN` / `SCHOOL_ADMIN` / `HEAD_TEACHER` / `STUDENT`),
> 权限由代码 `BuiltinRole` 决定、界面**只读**(能看、能当模板,改不了);
> 租户可另建**自定义角色**,于是"角色 → 权限码"与"角色 → 可见菜单"两件事可以由租户自己配。
> 改授权**立即生效**(服务端写授权后清缓存),不需要重新登录。
>
> 权限码本身**仍然定义在代码里**(`PermissionDef`,**28 个**),没有 `sys_permission` 定义表 ——
> 它是前后端契约:后端用它校验、前端用它控制按钮显隐,存 DB 就会漂移。
>
> ⚠️ **需求收敛(2026-10)**:本条不再是"二期功能",它就是一期能力;同时
> **数据范围(本人/本班/本院系/全租户)只展示、不提供修改入口** ——
> 它目前由"拿到了哪些权限码 + 任务里被指派了哪些班"共同决定,做成可配要先把判据收口(**本次不做**)。
**五条口径(实现与文档同源,改一处必须改另一处)**
| # | 口径 | 为什么 |
|:--:|---|---|
| 1 | **内置角色只读** | `roleType=1` 的角色可以看权限矩阵,但改 / 删 / 授权 / 停用一律 `40001`。一次误操作就能把管理员锁在系统外,而这类错误不可逆 |
| 2 | **内置角色的权限以代码为准** | 列表里内置角色的 `permCodes` / `menus` 由代码返回,**不读库**;库里的行只是"看得见、可审计"(每日定时任务把两者对齐,见 §17.2 D-13) |
| 3 | **平台级能力与入口不给学校** | 学校租户授予 `tenant:manage` / `tenant:view` / `log:viewAll` → `40301`;配置平台专用菜单 `tenant` → `40301`;建"平台运营"类角色(`userType=1`)→ `40301`;发 `PLATFORM_ADMIN` 给账号 → `40301` |
| 4 | **防自锁** | 不允许去掉"自己进入本页所依赖的最后一条 `role:manage` 权限 / `role` 菜单",也不允许停用或删除那个角色;改账号角色时同理保护 `user:manage` |
| 5 | **改完立即生效** | 每次写授权都清该角色的缓存,所以**不需要重新登录**;停用角色立刻不再给权限(不是等 token 过期)。⚠️ 改**账号的角色归属**仍是"下次登录生效"(角色码在 token 里) |
**`GET /api/v1/roles`** 权限:`role:manage` 或 `user:view` / `user:manage`(账号页的角色下拉要用它)
| 字段 | 类型 | 说明 |
|---|---|---|
| `id` | long | 角色 ID |
| `roleCode` | string | 角色码(建后不可改) |
| `roleName` | string | 角色名 |
| `roleType` | int | `1` 内置(只读)/ `2` 自定义 |
| `userType` | int | `1` 平台运营 / `2` 学校管理员 / `3` 班主任 / `4` 学生(账号的 `userType` 由它推导) |
| `dataScope` | int | `1` 全平台 / `2` 本租户 / `3` 本组织及下级 / `4` 仅本人(**本期只展示**,见下) |
| `status` | int | `0` 停用 / `1` 正常 |
| `userCount` | int | 有多少账号在用(> 0 时不可删除) |
| `editable` | bool | 能否编辑 / 删除 / 停用(内置恒 `false`) |
| `assignable` | bool | 能否发给账号(内置的「平台运营」在学校租户里 `false`:**看得见、发不出去**) |
| `permCodes` | string[] | 权限码(内置取代码,自定义取库) |
| `menus` | string[] | 可见菜单键(与前端 `XtMenu.key` 一致) |
> ⚠️ **`dataScope` 本期只展示、不提供修改入口**:数据范围(本人 / 本班 / 本院系 / 全租户)目前由
> "拿到了哪些权限码 + 任务里被指派了哪些班"共同决定,散在审核 / 催报 / 代传 / 看板各处;
> 做成可配要先把那些判据收口(单独一批)。让界面出现一个"改了不生效"的开关,比没有它更糟。
**`GET /api/v1/permissions`** 权限:`role:manage`
返回按分组排列的 **28** 个权限码:`[{ group, items: [{ code, label, platform, platformOnly }] }]`。
`platform` 为 `PC` / `MP` / `BOTH`;`platformOnly=true` 的码学校租户不可授予(界面禁用勾选,服务端 `40301`)。
**`POST /api/v1/roles`** 权限:`role:manage`
| 字段 | 必填 | 说明 |
|---|:--:|---|
| `roleCode` | ✅ | 大写字母 / 数字 / 下划线,以字母开头,租户内唯一,**不得占用内置角色码** |
| `roleName` | ✅ | 显示名(可改) |
| `userType` | ✅ | 该角色对应的用户类型(学校租户不能选 `1`) |
| `remark` | — | 备注 |
| `permCodes` | ✅ | 至少一项(服务端校验存在性与平台级白名单) |
| `menus` | ✅ | 至少一个(服务端校验是否为现存菜单键、是否平台专用) |
返回新角色 `id`。建后**不能改 `roleCode`**(它在 JWT 与操作日志里,改了历史留痕就对不上)。
**`PUT /api/v1/roles/{id}`** 权限:`role:manage` 只改 `roleName` / `userType` / `remark`。
⚠️ 角色还有账号在用(`userCount > 0`)时改 `userType` → `40001`:那会让账号的 `user_type` 与角色不一致,
而 `user_type` 决定"他算学生还是老师"。
**`DELETE /api/v1/roles/{id}`** 权限:`role:manage` 软删(含授权与菜单行)。
还有账号在用 → **`40045`**;命中防自锁 → `40001`。
**`PUT /api/v1/roles/{id}/permissions`** / **`.../menus`** 权限:`role:manage`
请求体 `{ permCodes: [] }` / `{ menus: [] }`,**覆盖式**;返回 `void`。写后立即失效该角色的授权缓存。
**`PUT /api/v1/roles/{id}/status?status=0|1`** 权限:`role:manage`
停用 = 该角色立刻不再给权限(**不必等 token 过期**);内置角色不可停用。
**错误码**
| 码 | 场景 |
|---|---|
| `40001` | 角色码不合法 / 重复 / 占用内置码;权限码或菜单键不存在;改内置角色;`userType` 与在用的账号冲突;命中防自锁 |
| `40301` | 无 `role:manage`;授予平台级权限码;配置平台专用菜单;建平台运营类角色;跨租户(平台未选定学校) |
| `40045` | 角色还有账号在用,不可删除 |
---
## §6 组织与租户
### 6.1 `GET /api/v1/orgs/tree`
@@ -1167,7 +1248,7 @@ Excel 列:`层级` `组织名称` `组织编码` `上级编码` `负责人手
**响应**:`{ "id": 2026090001, "taskCode": "2026090001", "status": 0 }`
> **社会收集的审核配置默认值**(二期):`needClassAudit: false`(跳过初审),由服务端强制设置,客户端传 true 会被忽略并返回 `warnings`。
> **社会收集的审核配置默认值**(该模式**本次不做**,仅保留数据结构):`needClassAudit: false`(跳过初审),由服务端强制设置,客户端传 true 会被忽略并返回 `warnings`。
> ⚠️ **一期只支持 `collectMode=1`(学校收集)**。传 `2` 会被拒绝并返回 `40044`。
### 7.3 `POST /api/v1/tasks/{id}/publish`
@@ -1363,8 +1444,10 @@ Excel 列:`层级` `组织名称` `组织编码` `上级编码` `负责人手
| 模板 | 材料项 |
|---|---|
| `TPL_SCHOOL_ROLL` | 一寸照片 + 身份证 + 学籍表 + 工作证明 |
| `TPL_SOCIAL` | 一寸照片 + 身份证 + 工作证明 + 学信网备案表 |
| `TPL_ZHUANXIANG` | 一寸照片 + 身份证 + 工作证明 |
> ⚠️ **需求收敛(2026-10)后模板只剩这一个**:原 `TPL_SOCIAL`(社会收集)与
> `TPL_ZHUANXIANG`(专项职业能力)随"二期取消"一并删除;传这两个码会得到**不认识的模板**,
> 不再有对应的初始化行为。
### 7.11 `GET /api/v1/tasks/{id}/statistics`
@@ -1518,184 +1601,15 @@ Excel 列:`层级` `组织名称` `组织编码` `上级编码` `负责人手
---
### 8.7 名单加入申请(FR-509 · 客户确认后新增)
> **背景**:门禁不通过的人原先只能"联系老师",老师再手工加名单。几百人的社会收集场景下,老师负担极重。
> 本组接口提供一条**受控通道**:学生自助申请 → 管理员审批 → 自动入册。
> ⚠️ **本节已撤销(需求收敛 2026-10)**:原「名单加入申请」通道(`FR-509`,确认函 **B1**)
> 连同 5 个接口(`POST/GET /mp/roster/join-request`、`GET /roster/join-requests`、
> `POST /roster/join-requests/{id}/approve|reject`)、`join:audit` 权限码、`collect_join_request` 表
> 与错误码 `40037` / `40038` / `40039` / `40040` / `40042` **一并删除**。
>
> ⚠️ **关键边界**:这条通道**只解决"进名单",不绕过门禁**——申请期间学生仍不能填报。
#### 8.7.1 `POST /api/v1/mp/roster/join-request`
**权限**:学生(**须已过门禁失败流程**) **限流**:同 openid 每任务 ≤2 次
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `taskId` | int64 | ✅ | — |
| `realName` | string | ✅ | 姓名(明文存储,业务必需) |
| `idCardNo` | string | ✅ | 身份证号(**服务端转 HMAC,不落明文**) |
| `phone` | string | ✅ | 手机号(**仅存 HMAC + 脱敏值**) |
| `orgHint` | string | — | 自述班级 |
| `reason` | string | — | 申请说明,如"我是转班过来的" |
**响应**
```json
{
"code": 0,
"data": {
"requestId": 7001,
"status": 0,
"statusText": "已提交,等待学校审核",
"estimatedHours": 24,
"remainAttempts": 1
}
}
```
**主要错误**
| 码 | 场景 | 提示 |
|---|---|---|
| ⭐ `40037` | 同任务申请次数超限(>2) | "该任务申请次数已达上限,请联系老师" |
| ⭐ `40038` | 该证件号已有待审申请 | "你已提交过申请,请耐心等待" |
| ⭐ `40039` | 该证件号已在名册中 | "你已在名单中,请直接返回重新核验" |
| `40008` | 任务已截止/暂停 | — |
> **`40039` 是必要的**:若证件号已在名册,说明学生**只是输错了姓名**,该引导他回去重填门禁,而不是提交申请。
> 这一条不违反"统一失败提示"原则——因为学生已经主动提供了正确证件号,此时告知"你已在名单中"**不泄露任何新信息**。
#### 8.7.2 `GET /api/v1/mp/roster/join-request`
**Query**:`taskId`
**响应**
```json
{
"code": 0,
"data": {
"list": [
{ "requestId": 7001, "taskId": 2026090001, "taskName": "2026年9月职业技能认定",
"status": 0, "statusText": "待审核",
"realName": "李四", "idCardMask": "3610**********10",
"orgHint": "24中技数控2班",
"createTime": "2026-09-28T18:00:00+08:00",
"auditTime": null, "rejectReason": null,
"rosterId": null }
],
"total": 1
}
}
```
**驳回态**:`status: 2` + `rejectReason` 明文返回(学生需要知道原因)。
**通过态**:`status: 1` + `rosterId`,并提示"已加入名单,请返回重新核验进入填报"。
#### 8.7.3 `GET /api/v1/roster/join-requests`
**权限**:管理 **Query**:`taskId`、`status`(默认 `0` 待审)、`orgId`、`keyword`、分页
**响应**
```json
{
"code": 0,
"data": {
"list": [
{ "requestId": 7001, "taskId": 2026090001,
"realName": "李四", "idCardMask": "3610**********10",
"phoneMask": "138****8888",
"orgHint": "24中技数控2班",
"reason": "我是转班过来的",
"status": 0,
"createTime": "2026-09-28T18:00:00+08:00",
"waitHours": 2.5,
"duplicateHint": null }
],
"total": 1,
"counts": { "pending": 1, "approved": 12, "rejected": 3 },
"alert": null
}
}
```
**`duplicateHint`**:若该证件号与**其他任务的名册**匹配上,返回提示(帮管理员判断是否本校学生):
```json
"duplicateHint": { "type": "IN_OTHER_TASK", "taskName": "2026年3月认定", "realName": "李四" }
```
**`alert`**:单任务待审 > 50 条时返回 `{ "level": 2, "text": "待审申请已达 53 条,建议集中处理" }`(FR-509c)。
#### 8.7.4 `POST /api/v1/roster/join-requests/{id}/approve`
**权限**:管理 **幂等**:是
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `orgId` | int64 | ✅ | **指定班级**(默认取自述班级,无则必填) |
| `realName` | string | — | 修正姓名(管理员核对后可改) |
| `idCardNo` | string | — | 修正证件号 |
| `seqNo` | int | — | 序号(默认追加到末尾) |
**服务端动作(事务内)**
```
1. 校验申请状态 = 待审,否则 40040
2. 校验该 task 下证件号未被占用,否则 40041
3. 在 collect_roster 新建名册项:
real_name / id_card_no / id_card_hash / id_card_mask ← 申请值(或管理员修正值)
org_id ← 指定班级
bind_user_id ← 申请人 uid(FR-509g:自动绑定,免重输)
bind_type ← 3(来自申请)
ext_json.source ← 'JOIN_REQUEST'
4. 回写 collect_join_request:status=1, audit_by, audit_time, roster_id
5. 写操作日志 JOIN_APPROVE
6. 推送通知 bizType=7
```
**响应**
```json
{
"code": 0,
"data": {
"requestId": 7001, "status": 1,
"rosterId": 9029,
"rosterCreated": true,
"notifySent": true,
"canFillNow": true
}
}
```
> **`canFillNow: true`** 表示该 openid 已绑定,学生再次进入**直接放行**。
#### 8.7.5 `POST /api/v1/roster/join-requests/{id}/reject`
**权限**:管理 **幂等**:是
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `reason` | string | ✅ | **驳回原因(必填)**,会原样推送给学生 |
| `reasonCode` | string | — | 快捷原因码(`NOT_THIS_SCHOOL` / `INFO_MISMATCH` / `NOT_IN_SCOPE` / `OTHER`) |
**响应**:`{ "requestId": 7001, "status": 2, "notifySent": true }`
**约束**
| 条件 | 结果 |
|---|---|
| 已通过后再驳回 | `40042`(如需撤销请走名册删除) |
| 任务已归档 | `40034` |
| 批量审批 | **支持**,但**必须逐条确认**(不可一键全通过——涉及身份认定) |
> ⚠️ **批量审批的设计取舍**:批量**通过**允许(管理员已看过列表),但**驳回必须逐条填原因**;且批量通过时**不提供"全选"快捷键**。
> 理由与"批量通过只带绿色项"同源:**身份认定环节的误操作代价极高**。
---
> **现在的口径**:门禁失败页只给「**重新填写**」+ 一句"如确认已报名,请联系老师把你加入名单";
> 名单外的人由老师在任务里**手工加人**(§8.1 追加导入,`FR-508`)。**系统内不展示任何联系方式。**
>
> **治理提示**:B1 是客户确认函里**唯一的功能增量**,本次属**需求回退**,建议补一张变更确认单。
## §9 填报端 · 小程序
@@ -1827,7 +1741,7 @@ Excel 列:`层级` `组织名称` `组织编码` `上级编码` `负责人手
| `fileName` | string | ✅ | 原始文件名(**可能是乱的,正常**) |
| `fileSize` | int64 | ✅ | 字节 |
| `contentType` | string | ✅ | MIME |
| `source` | int | — | `1` 本人上传 / `2` 班主任代传 / `3` 管理员代传 / `4` 整理器导入(口径见 §3.6 注) |
| `source` | int | — | `1` 本人上传 / `2` 班主任代传 / `3` 管理员代传 / `4` 整理器导入(**模块已取消,仅历史数据**;口径见 §3.6 注) |
| `fileHash` | string | — | 客户端 SHA-256(**用于秒传与去重**) |
**响应**
@@ -1863,7 +1777,7 @@ Excel 列:`层级` `组织名称` `组织编码` `上级编码` `负责人手
> 记录问题(含 VAL-06 自动修复),默认**只存规范件**(`fill.storage.keep-raw=false`)。
> 所以本接口**只在「保留了原始件」时有用**:对原始件重新跑一遍规范化
> (典型场景:改了材料规则后重压)。默认配置下多数文件没有原始件,调用会明确说明这一点,
> 而不是假装处理了一遍。识别 / OCR 的四层递进是二期(M3/M4),见 §17.2 D-12。
> 而不是假装处理了一遍。识别 / OCR 的四层递进**缓做**(等 OCR 供应商选型,见 §17.2 D-12 与《01》D1)——**不再作为二期功能设计**。
> 下面的响应结构保留作为「规范化产物」的字段参考(上传接口的产物与之同构)。
**说明**:对**有原始件**的文件重新执行规范化(压缩 + 改名 + 校验)。
@@ -1970,7 +1884,7 @@ Excel 列:`层级` `组织名称` `组织编码` `上级编码` `负责人手
### 9.9 `POST /api/v1/mp/ocr/idcard`(一期缓做)
> ⚠️ **一期未实现(等 OCR 供应商选型,《01-客户与机构确认函》D1)**。
> 这是 121 个契约接口里**唯一**没有实现的一个;表单预填与校验位校验在学生端走人工填写。
> 这是 119 个契约接口里**唯一**没有实现的一个;表单预填与校验位校验在学生端走人工填写。
> 选型要同时评估**数据出境合规**。选型定了之后本节按原设计实现,以下为设计参考。
**说明**:身份证 OCR,用于表单预填与校验。**这是唯一返回明文姓名的 OCR 场景**。
@@ -2047,7 +1961,7 @@ Excel 列:`层级` `组织名称` `组织编码` `上级编码` `负责人手
}
```
**社会收集**(二期):`nextNode` 直接返回 `ADMIN_AUDIT`(跳过初审)。
**社会收集**(本次不做,传 `collectMode=2` 直接 `40044`):`nextNode` 返回 `ADMIN_AUDIT`(跳过初审)。
### 9.11 `POST /api/v1/mp/submissions/{id}/withdraw`
@@ -2518,110 +2432,22 @@ Excel 列:`层级` `组织名称` `组织编码` `上级编码` `负责人手
| I8 审核撤回 | **§11.4**(已定义) | ✅ |
| I9 任务暂停/恢复 | **§7.4**(已定义) | ✅ |
| I10 材料识别结果读取 | **并入 §11.2 详情响应**(不新增接口) | ✅ |
| I2 历史数据整理器 | **§13.2** | ✅ |
| I3 字典管理 | **§13.3** | ✅ |
| I4 / I5 日志 | **§13.4** | ✅ |
| I6 消息中心 | **§13.5** | ✅ |
| I7 订阅额度登记 | **§13.6** | ✅ |
### 13.2 历史数据整理器(5 个)· 差异化获客功能
### 13.2 历史数据整理器(已取消 · 0 个)
#### `POST /api/v1/cleanup/jobs`
**说明**:上传已有压缩包/文件夹,用**与正式收集完全相同的规则**做规范化。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `jobName` | string | ✅ | 如 `24中技数控1班-历史整理` |
| `sourceType` | int | ✅ | `1` 压缩包 / `2` 文件夹 / `3` 云盘链接 |
| `uploadToken` / `objectKey` | string | 条件 | `sourceType=1/2` 时必传(先走通用上传) |
| `sourceUrl` | string | 条件 | `sourceType=3` 时必传 |
| `namingTemplate` | string | — | 缺省用系统默认 |
| `compressConfig` | object | — | 缺省见 3.11 |
| `rosterTaskId` | int64 | — | **关联到已有任务的名册**(用于证件号纠错与串档检测) |
**响应**:`{ "jobId": 301, "status": 0, "estimatedSeconds": 120 }`
> `rosterTaskId` 是**关键参数**:没有名册就只能做文件名规范化,**有名单才能做证件号校验、疑似串档检测**——而"疑似串档 1 例"正是报告里最有价值的一条。
#### `GET /api/v1/cleanup/jobs`
**Query**:`status`、`keyword`、分页 **响应**:`{ list: CleanupJobVO[], total }`
#### `GET /api/v1/cleanup/jobs/{id}`
**响应**
```json
{
"code": 0,
"data": {
"id": 301, "jobName": "24中技数控1班-历史整理", "status": 3, "progress": 100,
"totalFiles": 112, "processedFiles": 112,
"parsedStudents": 28, "parsedMaterials": 4,
"rawSizeMb": 436, "finalSizeMb": 37, "compressRatio": "91.5%",
"resultPath": "/cleanup/301/result.zip",
"finishTime": "2026-09-28T16:00:00+08:00"
}
}
```
#### `GET /api/v1/cleanup/jobs/{id}/report` ⭐ 体检报告
**Query**:`format`(`json` / `xlsx`)
**响应(json)**
```json
{
"code": 0,
"data": {
"jobId": 301,
"summary": {
"totalFiles": 112,
"nameInvalid": 59, "sizeExceeded": 31, "formatConverted": 56,
"idcardMismatch": 5, "idcardAutoFixed": 3, "idcardNeedConfirm": 2,
"crossFileSuspected": 1, "lowResolution": 1, "orphanImages": 0
},
"autoHandled": 90,
"needManual": 1,
"highlights": [
{ "type": "CROSS_FILE", "level": 3, "rosterId": 9012, "realName": "张三",
"title": "疑似串档", "detail": "该生 4 项材料的证件号与名册均不同,且 4 项彼此一致",
"suggestion": "可能整个文件夹被换过,请核对学生本人" },
{ "type": "LOW_RESOLUTION", "level": 2, "rosterId": 9008, "realName": "李四",
"title": "分辨率偏低", "materialCode": "IDCARD",
"detail": "558×745,同批其他人为 1279×1706", "suggestion": "建议索取原图" }
],
"nameFixedSamples": [
{ "original": "身份证.jpg.jpg", "fixed": "曾伊豪-361029200907295110-身份证.jpg", "rule": "VAL-06" }
],
"idcardConflicts": [
{ "rosterId": 9012, "realName": "张三",
"rosterValue": "3610292009******10", "fileNameValue": "3610292009******99",
"ocrValue": "3610292009******10",
"verdict": "AUTO_FIXED", "verdictText": "OCR 与名册一致,按名册修正文件名" }
]
}
}
```
> **报告里最重要的一句话是 `autoHandled: 90 / needManual: 1`**——「112 个文件里 90 个系统全自动处理,只有 1 个必须你亲自判断」。这是最有说服力的销售话术,也是接口必须算出来的数字。
#### `POST /api/v1/cleanup/jobs/{id}/import`
**说明**:把整理结果**导入为正式任务**(名册 + 材料一次成型)。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `taskName` | string | ✅ | 新任务名 |
| `collectMode` | int | ✅ | `1` / `2` |
| `batchNo` | string | ✅ | — |
| `importRoster` | bool | — | 是否把解析出的学员建成名册(默认 true) |
| `importFiles` | bool | — | 是否把文件挂到填报单上(默认 true) |
| `markAsSubmitted` | bool | — | 是否直接置为"已提交待审"(默认 true) |
**响应**:`{ "taskId": 2026090002, "rosterCount": 28, "fileCount": 112 }`
> ⚠️ **本节已撤销(需求收敛 2026-10)**:历史整理器整块能力(`module/cleanup`)连同
> 5 个接口(`POST/GET /cleanup/jobs`、`GET /cleanup/jobs/{id}`、`.../report`、`.../import`)、
> `cleanup:run` 权限码、`collect_cleanup_job` 表、PC 端「历史整理器」菜单与「整理器」原型屏
> **一并删除**,不再设计。
>
> 保留这一节标题只是为了让旧的交叉引用(`§13.2`)仍能落到一个说清"为什么没有了"的地方 ——
> 节号不重排,`§13.3` 起的编号与代码注释里的引用都不受影响。
>
> ⚠️ **别误伤**:`POST /api/v1/exports/cleanup-run` 是**导出包的过期清理**,属导出中心,**仍在**。
### 13.3 字典管理(4 个)
@@ -2767,7 +2593,6 @@ Excel 列:`层级` `组织名称` `组织编码` `上级编码` `负责人手
| `AUDIT_RESULT` | 提交成功后 | 通过 / 驳回通知 |
| `DEADLINE_REMIND` | 进入填报页时 | 临期提醒 |
| `EXPORT_READY` | 管理端创建导出时 | 导出完成 |
| `JOIN_APPROVE` | 申请加入名单时 | 审批结果 |
**额度不足的降级链**
@@ -2855,7 +2680,7 @@ X-Sign: HMAC-SHA256(appSecret, appKey + timestamp + nonce + body)
## §15 错误码全集
> 在 PRD 附录 D(20 个码)基础上**新增 38 个**(标 ⭐),本节共 **58 个**(含 `0` 成功码)。所有码在 `code` 字段返回,HTTP 状态码见 §1.3。
> 在 PRD 附录 D(20 个码)基础上**新增 34 个**(标 ⭐),本节共 **54 个**(含 `0` 成功码)。所有码在 `code` 字段返回,HTTP 状态码见 §1.3。
### 15.1 客户端错误(4xxxx)
@@ -2899,14 +2724,10 @@ X-Sign: HMAC-SHA256(appSecret, appKey + timestamp + nonce + body)
| ⭐ `40034` | 已归档,不可撤回 | — |
| ⭐ `40035` | **存在不合格文件,禁止导出** | 禁用导出按钮 + 展示 blocking 清单 |
| ⭐ `40036` | 字典项有下级,不可停用 | 先处理下级 |
| ⭐ `40037` | **加入申请次数超限**(同 openid 每任务 >2) | 提示联系老师 |
| ⭐ `40038` | **该证件号已有待审申请** | "你已提交过申请,请耐心等待" |
| ⭐ `40039` | **该证件号已在名册中** | "你已在名单中,请返回重新核验"(引导回门禁,**不泄露新信息**) |
| ⭐ `40040` | 申请状态已变更(非待审) | 刷新列表 |
| ⭐ `40041` | **名册中该证件号已被占用** | 不覆盖,提示管理员核查 |
| ⭐ `40042` | 申请已通过,不可再驳回 | 如需撤销请走名册删除 |
| ⭐ `40043` | **文件归属存疑**(文件名指向名册中的另一个人,或归属无法确证) | **阻断提交**,提示"这份材料可能不是你的",引导重新上传(**无"仍然继续"选项**) |
| ⭐ `40044` | **一期不支持该收集模式 / 认定类型** | 请使用学校收集 + 自主认定(见 PRD「一期范围声明」) |
| ⭐ `40045` | **角色还有账号在用,不可删除** | 先给这些账号换一个角色(界面会说明还有几个账号) |
| `40101` | 未登录 / token 失效 | 静默重新登录 |
| `40301` | 无权限(含跨租户) | 跳转 403 |
| `40302` | 签名 URL 已过期 | 重新获取 |
@@ -2932,6 +2753,7 @@ X-Sign: HMAC-SHA256(appSecret, appKey + timestamp + nonce + body)
| 版本 | 日期 | 变更 |
|---|---|---|
| **V2.7** | **2026-10** | **需求收敛:删角色 / 删两模块 / 取消二期**。① 内置角色 **6 → 4**(删 `DEPUTY_TEACHER` 副班主任、`AUDITOR` 审核员;挂这两个角色的存量账号由 **V19 迁移**直接删除);② 删「**历史整理器**」(5 个接口 + `cleanup:run` 权限码 + `collect_cleanup_job` 表 + §13.2 整节)与「**加入申请**」(5 个接口 + `join:audit` 权限码 + `collect_join_request` 表 + §8.7 整节 + 错误码 `40037`/`40038`/`40039`/`40040`/`40042`);③ `collect_task_org.deputy_user_ids`(协办人)**整列删除**,审核范围由三级回退收窄为**两级**(`head_user_id` → `sys_org.leader_user_id`);④ **不再有任何二期功能设计** —— 社会收集 / 社会评价 / 专项职业能力 / 数据范围可配 / 角色变更即时失效 / 平台角色页一律**不做**,OCR 与材料识别改为「**缓做,等供应商选型**」;⑤ 内置材料模板 **3 → 1**(仅 `TPL_SCHOOL_ROLL`),`XJW_BA` / `OTHER_PROOF` **读得到、选不到**。**数字**:契约 **129 → 119**、错误码 **59 → 54**、权限码 **30 → 28**、PC 菜单 **12 → 10**、小程序页面 **10 → 9**、原型 **32 → 30 屏**。⚠️ **加入申请(确认函 B1)是客户唯一确认的功能增量,本次属需求回退,建议补变更确认单**。详见《交接摘要》§一·三十六 |
| **V2.6** | **2026-09-30** | **六处口径修正 + 新增 §17(全部经客户确认)**:① §2.7 `notifyBizType` 5/6 对调修正(5 催报 / 6 导出完成,F-30);② §2.7 `auditAction` 补全为 7 个动作、撤回=7(F-7);③ §3.6/§9.5 `source` 改为建表口径「谁传的」(F-38/39);④ §9.5 改为「上传前置检查」定位 + §9.6 `process` 语义改为「有原始件时的重新规范化」(F-3/71/72);⑤ §9.9 OCR 标注一期缓做;⑥ §11.4 撤回约束补「状态 2 可撤(撤初审)」、留痕 `action=7`;⑦ §12.1 `validation/run` 改同步并返回统计(F-51);⑧ **新增 §17 实现补充与偏差记录**(41 个 §4 未列接口按六类分组 + 12 条已确认偏离 D-1~D-12)。同步记录见《08-完成度盘点与问题清单》 |
| **V2.5** | **2026-09-30** | **两处"文档内部口径冲突"加注(不改口径,只把冲突摆到明处)**:① §2 枚举 `notifyBizType` 的 `5`/`6`(本文件写「5 导出完成 / 6 催报」)与 §13.5 消息中心示例、落库值 `collect_notify_record.biz_type`(「**5 催报 / 6 导出完成**」)不一致 —— **实现以 §13.5 为准**,需要客户确认以哪边为准(若以 §2 为准,要同时改两个常量与已落库数据);② §3.6 与 §9.5 的 `collect_file.source` 字段表写的是"入口来源"(1 聊天记录 / 2 相册 / 3 手机文件 / 4 代传),而建表注释与实现(`CollectFile.SOURCE_*`)是"**谁传的**"(1 本人上传 / 2 班主任代传 / 3 管理员代传 / 4 整理器导入)—— **实现以建表口径为准**(§10.6 要求代传可追溯,要回答的是"谁替他传的")。两处均已在原文加 ⚠️ 注。记录见《08-完成度盘点与问题清单》**F-30 / F-38 / F-39**。 |
| **V2.4** | **2026-09-28** | **修正 3 处加密列宽不足(DDL 缺陷)**:`sys_tenant.contact_phone`、`sys_user.phone`、`collect_roster.phone` 原为 `varchar(20)`,是按**明文手机号**的长度建的;但 PRD §10.3 要求手机号 AES-256 加密存储,AES-GCM 密文约 **52 字符**,插入会直接报 `value too long for type character varying(20)`。(`id_card_no` 当初给了 `varchar(128)` 就是考虑到了加密,手机号三列漏了——属设计不一致。)三列统一改为 **`varchar(128)`**,与 `id_card_no` 对齐。**发现方式**:M0 开通租户实测时数据库报错暴露。**审计结论**:其余 `*_mask varchar(32)` 列存的是脱敏值(如 `138****8888`)而非密文,宽度足够,无需改动。 |
@@ -2945,25 +2767,25 @@ X-Sign: HMAC-SHA256(appSecret, appKey + timestamp + nonce + body)
| 检查项 | 结果 |
|---|---|
| §4 各模块表行数合计 | **121** ✓ 与声称一致 |
| §4 各模块表行数合计 | **119** ✓ 与声称一致 |
| §4 内路径重复 | 无 ✓ |
| Markdown 表格列数一致性 | 0 行不符 ✓ |
| 文档引用的字段名 | 116 个唯一值,**全部存在于 DDL** ✓ |
| 错误码数量 | 58 个(新增 38 个标 ⭐)✓ |
| 文件规模 | 约 100 KB / 2570 行 |
| 错误码数量 | 54 个(新增 34 个标 ⭐)✓ |
| 文件规模 | 约 89 KB / 2870 行 |
---
## §17 实现补充与偏差记录
> **§4 的 121 个是设计口径**。本节回答两件事:① 代码里还有哪些 §4 没列的接口;
> **§4 的 119 个是设计口径**。本节回答两件事:① 代码里还有哪些 §4 没列的接口;
> ② 哪些地方**有意偏离**了本文档的原始写法、为什么、谁确认的。
>
> ⚠️ 17.1 的清单由工具生成:`python scripts/check-api-parity.py --extra`
> (当前 **41 个**,生成于 2026-09-30)。**手抄清单会过期,以命令输出为准**;
> (当前 **38 个**,2026-10 复核)。**手抄清单会过期,以命令输出为准**;
> 下表按"为什么不在 §4"分组,便于决定"补进 §4 还是留在本节"。
### 17.1 代码有、§4 清单没有的接口(41 个,按偏离原因分组)
### 17.1 代码有、§4 清单没有的接口(38 个,按偏离原因分组)
**A · 材料访问通道(7 个)** —— 审核端与学生端各一条通道(审核端要看防伪纹,学生端只看自己的);上传是一步 multipart
@@ -2992,7 +2814,7 @@ X-Sign: HMAC-SHA256(appSecret, appKey + timestamp + nonce + body)
|---|---|
| `GET /api/v1/exports/{id}/file` | 导出包的流式下载。§12.6 只定义了"取下载信息";真正下文件走这条,**URL 有有效期、不能入库/外发** |
**D · 界面与接口必需、清单漏列(17 个)**
**D · 界面与接口必需、清单漏列(16 个)**
| 接口 | 说明 |
|---|---|
@@ -3012,7 +2834,6 @@ X-Sign: HMAC-SHA256(appSecret, appKey + timestamp + nonce + body)
| `POST /api/v1/dicts/{code}/items/{itemCode}/toggle` | 内置字典项停用/恢复(§13.3) |
| `GET /api/v1/exports/precheck` | 导出前置检查(只检查不创建;界面靠它禁用按钮) |
| `GET /api/v1/exports/preview` | 导出目录树预览(与真正打包共用同一份构建代码) |
| `GET /api/v1/cleanup/jobs/{id}/result` | 整理器报告(§13.2 界面轮询用) |
**E · 契约链条原本断在第一步(8 个,已实现)**
@@ -3026,21 +2847,19 @@ X-Sign: HMAC-SHA256(appSecret, appKey + timestamp + nonce + body)
| `POST /api/v1/open-apps/{id}/toggle` | 启停接入方 |
| `GET /api/v1/open-apps/push-logs` | 推送记录(人工重推的依据) |
**F · 运维与调试动作(4 个)** —— 定时任务在测试里"等不起",所以都有手工入口(权限走运营侧 `log:view:all`)
**F · 运维与调试动作(2 个)** —— 定时任务在测试里"等不起",所以都有手工入口(权限走运营侧 `log:view:all`)
| 接口 | 说明 |
|---|---|
| `POST /api/v1/schedules/run` | 手工触发定时任务(`job` 取值 close/urge/timeout/raw/usage/retry/expire) |
| `POST /api/v1/exports/cleanup-run` | 手工触发导出包过期清理(可重复执行) |
| `POST /api/v1/cleanup/jobs/json` | 整理器:直接提交 JSON(不走上传) |
| `POST /api/v1/cleanup/jobs/parse-preview` | 整理器:解析预览(不落库) |
### 17.2 有意偏离契约的地方(均已确认,2026-09-30)
| # | 偏离点 | 契约原口径 | 实现口径 | 依据 |
|---|---|---|---|---|
| D-1 | `notifyBizType` 5/6 | §2.7 旧表「5 导出完成 / 6 催报」 | 「**5 催报 / 6 导出完成**」(与 §13.5、落库值一致) | F-30;按旧表实现催报去重会静默失效 |
| D-2 | `collect_file.source` | §3.6 旧表「入口来源」(聊天记录/相册/手机文件/代传) | 「**谁传的**」:1 本人 / 2 班主任代传 / 3 管理员代传 / 4 整理器(按建表注释) | F-38/F-39;§10.6 代传可追溯 |
| D-2 | `collect_file.source` | §3.6 旧表「入口来源」(聊天记录/相册/手机文件/代传) | 「**谁传的**」:1 本人 / 2 班主任代传 / 3 管理员代传 / 4 整理器导入(**模块已取消,仅历史数据**;按建表注释) | F-38/F-39;§10.6 代传可追溯 |
| D-3 | 上传链路 | OSS 直传 + `upload-token` 凭证 | **服务端一步上传**;`upload-token` 定位为**预检+秒传+压缩建议**(非安全边界) | F-3/F-71/F-72;直传需配回调且把凭证交前端 |
| D-4 | `process` 语义 | 「命名+压缩+识别+校验总入口」 | 上传当场完成这些;`process` = **有原始件时的重新规范化** | F-3;默认 `keep-raw=false` |
| D-5 | `POST /validation/run` | `async=true` + jobId | **同步跑完**并返回 `checkedSubmissions/issuesFound/unresolvedLeft/elapsedMs` | F-51;全契约没有查 jobId 的接口 |
@@ -3050,5 +2869,5 @@ X-Sign: HMAC-SHA256(appSecret, appKey + timestamp + nonce + body)
| D-9 | 任务删除 | 已发布任务不可删(`40001`) | **已归档(status=3)的任务可删**:管理员显式动作 + 操作留痕(`DELETE_TASK`,记行数/文件数);**子表级联物理删**(名册/材料/审核记录等 14 张表)、**任务行本身软删**(占住 `task_code` 不复用 —— 编码是文件目录名) | F-14;否则任务列表只增不减 |
| D-10 | 租户域名 | 未写 | 能力已实现(Host 定位 + token 授权),**默认关闭**;启用四步见《交接摘要》§一·二十六 | F-10;客户决定先不做 |
| D-11 | 通知降级链 | 「订阅消息→公众号模板→短信→站内信」四级 | 一期**两级**(订阅消息→站内信);公众号模板需已认证服务号、短信需采购通道(商务动作),不假装可发 | F-28 |
| D-12 | OCR(§9.9) | 已定义 | **一期缓做**(等供应商选型,见《01》D1,需评估数据出境合规) | F-3;121 个接口里唯一未实现 |
| D-12 | OCR(§9.9) | 已定义 | **一期缓做**(等供应商选型,见《01》D1,需评估数据出境合规) | F-3;119 个接口里唯一未实现 |