31e7483464
1) docs/03-接口设计.md:
- §6.5 补 expireBefore **含当天**的语义与"格式不对返回 40001,不静默忽略筛选";补
"列表 contactPhone 恒为 null"与"packageName 与内嵌 usage{} 不实现(200 行内嵌用量会变成
N+1 统计)"。
- §6.6 的 tenantType 由 3 值改回真实的 **4 值**(1 学校 / 2 认定机构 / 3 报名点 / 4 企业)、
packageId 由 ✅ 必填改为可选(DTO 里本来就是可选),并补副作用(发放 6 个内置角色 + 建管理员
+ 写 CREATE_TENANT 日志)。
- §6.7 的响应示例原来写着 ocrCount=3105 / smsCount=420,**与实现恒 0 矛盾** —— 改成 0 并补齐
契约没写的 notes 与 alerts.used/limit,再加三条口径:实时值 vs 周期、恒 0 的原因(编数字会让
运营拿假数据去谈续费)、**只算这一个租户**(含"库里只有一所学校时看不出"这一点)。
- **新增 §6.8 / §6.9**(PUT /tenants/{id} 与 GET /tenants/{id}):PUT 此前只在 §4.2 的总览表里
出现过一次、全文**没有详情节**;GET 更是只登记在 §17.1 的"清单漏列"组里。§6.8 含字段表、
"不传的字段不修改"、"tenantCode 不可修改"的理由,以及留痕对照表(status→0 是 DISABLE_TENANT,
不是 UPDATE_TENANT)+ "这些日志行的 tenant_id 是 0,要用 target_type/target_id 查"的警告。
- §13.4 的 Query 补 targetType(并说明为什么 targetType+targetId 才是最重要的查询方式:
关键字全文匹配会把"改了它"和"顺带提到它"混在一起,而合规结论不能建立在这种匹配上);
响应补 request_method / request_uri / extraJson;动作表由 9 行扩到**全集 27 个**并按来源分组
(任务 / 名册 / 导出 / 角色 / 租户);写明 request_param / duration_ms 刻意不采集;
并记下"一期只做查询、不做日志导出"这条**此前只写在 LogController 类注释里**的口径
(导出等于把"谁在什么时候看了谁的身份证"这份名单落到本地磁盘,而导出行为本身也要留痕)。
- 登录日志响应补 tenantId,并写明 userType 的权威枚举与 location 恒为 null。
2) docs/08-完成度盘点与问题清单.md:新增 **F-112~F-124** —— 用量越界(含"为什么本地完全看不出来")、
PUT 明文写入加密列、两个枚举能建不能改、租户零留痕、expireBefore 差一天、AUTO_CLOSE 字面量、
模块下拉里有死项、LogController 描述与事实相反、request_param/duration_ms 有列无写入点、
schema-postgresql.sql 缺三张表、**"平台专属权限码 + 查带 tenant_id 的表"这个风险面**、
登录日志缺租户列、userType 注释错位。F-112 标注为本轮最严重的一条。
3) docs/10-用户手册与客服话术.md:「三种角色,三个入口」→ **四种角色**(补平台运营,并写明它是
唯一跨单位的角色、用"组织与账号/收集管理"这类单位内页面之前要先选"当前学校");
§4 速查表的菜单名与路径同步(平台运维 → 日志,含两个页签;新增租户管理一行);
附录的排查话术补 traceId("日志详情里能拿到,那是后端排查时最有用的一个线索")。
4) docs/交接摘要.md:新增 **§一·三十九**,九节 —— 一、口径变更(旧注释原文 + 推翻理由);
二、用量越界那个 bug 的完整来龙去脉(权限码 → ignoreTable → 实测数据分布 → 修法与夹具);
三、另外四条后端缺陷表;四、日志详情为什么不新增接口(轻字段 vs 全库 0 行有值的重字段);
五、AUTO_CLOSE 与检查 ⑩ 的关系(**"这一族检查的价值取决于被检查的那一侧有没有被规范化"**);
六、顺带修掉的五处;七、前端刻意用分段按钮而不是 NTabs 的两条原因;八、验证表 +
"改了后端必须重新打包重启、判据是看返回体里有没有新字段"的提醒;九、约定同步。
另把 E2E 断言总数 1595 → **1617**(四处)。
5) 根 AGENTS.md:接线检查说明补第 ⑩ 项,并写明做这一项时才发现 AUTO_CLOSE 是字符串字面量写的
—— "提成常量"是让这个检查真正生效的前提。
验证:python scripts/verify-docs.py **22/22**;python scripts/check-wiring.py 未接线 **0 条**
(⑦ 11 个权限码 / ⑧ 0 处漏 import / ⑨ 3 个 SVG 全合法 / ⑩ 27 个动作码两端逐字一致);
python scripts/check-api-parity.py 契约 **119** / 已实现 118 / 缺口 1 —— **没变**,符合本轮
"不新增后端接口"的决定。
2989 lines
138 KiB
Markdown
2989 lines
138 KiB
Markdown
# 接口设计说明书
|
||
|
||
> 版本:V1.0 | 日期:2026-09-28
|
||
> 对应主文档:《数据信息填报系统PRD.md》§8 接口清单(V2.3)
|
||
> 读者:前后端开发、测试
|
||
> **本文档是 PRD §8 的展开实现层**——§8 定义了「有哪些接口」,本文档定义「每个接口收发什么字段」。
|
||
|
||
---
|
||
|
||
## 目录
|
||
|
||
| 节 | 内容 |
|
||
|---|---|
|
||
| §1 | 通用约定(域名、鉴权、响应、分页、幂等、限流、脱敏) |
|
||
| §2 | 枚举字典全集 |
|
||
| §3 | 通用数据结构(Schema 定义) |
|
||
| §4 | 接口总览(**119 个**) |
|
||
| §5 | 认证与账号(13) |
|
||
| §6 | 组织与租户(15) |
|
||
| §7 | 任务与配置(25) |
|
||
| §8 | 名册(7) |
|
||
| §9 | 填报端 · 小程序(16) |
|
||
| §10 | 班主任移动端(10) |
|
||
| §11 | 审核(9) |
|
||
| §12 | 校验 / 导出 / 通知(12) |
|
||
| §13 | **本次补齐的缺口接口(16)** |
|
||
| §14 | 开放接口 · 供认定机构对接(4) |
|
||
| §15 | 错误码全集 |
|
||
|
||
---
|
||
|
||
## §1 通用约定
|
||
|
||
### 1.1 域名与环境
|
||
|
||
| 环境 | 域名 | 用途 |
|
||
|---|---|---|
|
||
| 生产 | `https://api.tianbaotong.cn/api/v1/**` | 正式 |
|
||
| 开放接口 | `https://api.tianbaotong.cn/open/v1/**` | 认定机构对接 |
|
||
| 测试 | `https://api-test.tianbaotong.cn/api/v1/**` | 联调 |
|
||
|
||
### 1.2 鉴权
|
||
|
||
| 端 | 方式 | Header |
|
||
|---|---|---|
|
||
| PC 管理端 / 班主任 PC | 账号密码 → JWT | `Authorization: Bearer <token>` |
|
||
| 微信小程序(学生 / 班主任) | `code2session` → JWT | `Authorization: Bearer <token>` |
|
||
| 开放接口(机构) | appKey + HMAC-SHA256 | `X-App-Key` / `X-Timestamp` / `X-Nonce` / `X-Sign` |
|
||
|
||
**JWT Payload**
|
||
|
||
```json
|
||
{
|
||
"uid": 10086,
|
||
"tenantId": 1001,
|
||
"orgIds": [2001, 2002],
|
||
"roles": ["SCHOOL_ADMIN"],
|
||
"userType": 2,
|
||
"exp": 1790000000
|
||
}
|
||
```
|
||
|
||
| 字段 | 说明 |
|
||
|---|---|
|
||
| `uid` | 用户 ID |
|
||
| `tenantId` | **租户 ID,服务端强制校验,防越租户** |
|
||
| `orgIds` | 数据权限范围(本人 / 本班 / 本院系 / 全租户) |
|
||
| `roles` | 角色码数组 |
|
||
| `userType` | `1` 平台运营 / `2` 学校管理员 / `3` 班主任 / `4` 学生 |
|
||
|
||
**Token 有效期**:PC 端 8 小时(可续期)/ 小程序端 7 天(静默续期)
|
||
|
||
#### `X-Tenant-Id`:平台运营的「当前学校」(可选,仅平台运营)
|
||
|
||
平台运营(`tenantId = 0`)是唯一跨租户的身份,服务端的租户过滤对它整体关闭。于是
|
||
「组织架构」「账号管理」这类**本身就是租户内**的接口,平台不指明学校时会拿到
|
||
**所有租户混在一起**的数据 —— 而这看不出错(只有一所学校有数据时,它与学校管理员看到的完全一样)。
|
||
|
||
| 项 | 约定 |
|
||
|---|---|
|
||
| Header | `X-Tenant-Id: <tenantId>` |
|
||
| 谁能用 | **只认平台运营**。其它身份带这个头一律忽略(改了也不生效,不是"报错") |
|
||
| 作用 | 本次请求的租户上下文换成该租户:`/orgs/**`、`/users/**`、`/tasks/**` 等按它过滤与写入 |
|
||
| 不带 / 传 `0` | 不切换(`0` 视为"平台自己")。平台的租户列表、全量日志等跨租户能力照旧 |
|
||
| 非法值 | 非数字、或租户不存在/已停用 → `40001` |
|
||
| 与 token 的关系 | 只对平台生效,**不能**用来访问别的租户(学校管理员带它不会越权) |
|
||
| 权限怎么判 | 切换之后**不再享受"平台超管"捷径**,一律按 `PLATFORM_ADMIN` 声明的权限码判 —— 所以那些码必须真的够用(例如「查看填报进度看板」要 `task:view` + `progress:viewAll`) |
|
||
|
||
> 例:`GET /api/v1/orgs/tree` + `X-Tenant-Id: 1` → 返回租户 1 的组织树;
|
||
> 不带该头 → 返回平台自己租户的组织(空树),**不再**把各租户的组织混在一起。
|
||
|
||
### 1.3 统一响应
|
||
|
||
```json
|
||
{ "code": 0, "msg": "success", "data": { } }
|
||
```
|
||
|
||
| 情况 | code | HTTP |
|
||
|---|---|---|
|
||
| 成功 | `0` | 200 |
|
||
| 业务错误(客户端) | `4xxxx` | 200(**业务错误不占用 HTTP 状态码**) |
|
||
| 服务端错误 | `5xxxx` | 200 |
|
||
| 未登录 | `40101` | 401 |
|
||
| 无权限 | `40301` | 403 |
|
||
| 限流 | `42901` | 429 |
|
||
|
||
> **实现约定**:HTTP 状态码只表达传输层语义(401/403/429/500),**业务判定一律看 `code`**。这样前端拦截器只需处理两类分支,减少漏判。
|
||
|
||
**校验失败的标准结构**(`40001`)
|
||
|
||
```json
|
||
{
|
||
"code": 40001,
|
||
"msg": "存在 2 项必须处理的材料问题",
|
||
"data": {
|
||
"canSubmit": false,
|
||
"blocking": [ { "materialCode": "SCHOOL_ROLL", "ruleCode": "VAL-05", "title": "缺少材料:学籍表" } ],
|
||
"warnings": [ { "materialCode": "PHOTO1", "ruleCode": "VAL-08", "title": "照片分辨率偏低" } ]
|
||
}
|
||
}
|
||
```
|
||
|
||
### 1.4 分页
|
||
|
||
**请求**:`?pageNum=1&pageSize=20&sortBy=create_time&sortOrder=desc`
|
||
|
||
| 参数 | 类型 | 默认 | 约束 |
|
||
|---|---|---|---|
|
||
| `pageNum` | int | 1 | ≥1 |
|
||
| `pageSize` | int | 20 | 1–200,超出按 200 |
|
||
| `sortBy` | string | 见各接口 | 白名单字段,非法值忽略 |
|
||
| `sortOrder` | string | `desc` | `asc` / `desc` |
|
||
|
||
**响应**
|
||
|
||
```json
|
||
{ "list": [ ], "total": 128, "pageNum": 1, "pageSize": 20 }
|
||
```
|
||
|
||
> **注意**:分页响应直接放在 `data` 里(不是 `data.page`),与 §8.1 约定一致。
|
||
|
||
### 1.5 幂等
|
||
|
||
需带 `Idempotency-Key: <uuid>` 请求头的接口:
|
||
|
||
| 接口 | 说明 |
|
||
|---|---|
|
||
| `POST /mp/submissions/{id}/submit` | 防重复提交 |
|
||
| `POST /mp/files/upload-token` | 防重复建文件记录 |
|
||
| `POST /exports` | 防重复打包(**避免重复计费**) |
|
||
| `POST /mp/roster/match` | 防重放 |
|
||
| 所有 `POST` 审核动作 | 防重复审批 |
|
||
|
||
**幂等窗口**:24 小时。重复请求返回 **首次结果 + `code: 40009`**,`data.idempotentReplay: true`。
|
||
|
||
### 1.6 限流
|
||
|
||
| 场景 | 阈值 | 超限响应 |
|
||
|---|---|---|
|
||
| 登录 | 5 次/分钟/IP + 账号锁定 5 次 | `42901` |
|
||
| **门禁核验** | **同 openid 10 次/小时;同 IP 20 次/分钟** | `40071` |
|
||
| 上传凭证 | 200 次/分钟/用户 | `42901` |
|
||
| OCR | 30 次/分钟/用户 | `42901` |
|
||
| 催报 | 10 次/分钟/任务 | `42901` |
|
||
| 导出创建 | 5 次/分钟/租户 | `42901` |
|
||
|
||
> **门禁限流是安全设计的一部分**(防身份证号枚举探测),阈值调低是刻意的。上线后按真实人数标定。
|
||
|
||
### 1.7 脱敏规则
|
||
|
||
| 字段 | 明文传入 | 响应返回 |
|
||
|---|---|---|
|
||
| 身份证号 | ✅ 允许(HTTPS,仅门禁/导入场景) | ❌ **一律返回脱敏值** `idCardMask`,形如 `3610292009******10` |
|
||
| 手机号 | ✅ 允许 | ❌ 返回脱敏值 `138****8888` |
|
||
| 姓名 | ✅ | ✅ 明文(业务必需) |
|
||
|
||
**例外**:`GET /audit/submissions/{id}` 等审核场景,管理员/班主任可见**完整身份证号**(审核必需),但仍**不返回明文**——返回 `idCardFull` 需通过 `GET /audit/submissions/{id}/idcard` 单独获取,该接口**单独记录审计日志**。
|
||
|
||
> 这条设计的意义:审核人员看材料图片时不需要抄身份证号,真需要核验时走单独接口、留痕可追溯。
|
||
|
||
### 1.8 其他约定
|
||
|
||
| 项 | 约定 |
|
||
|---|---|
|
||
| ID 类型 | `int64`,JSON 中以 number 传输(生成规则保证 < 2^53) |
|
||
| 时间 | ISO 8601 带时区,`2026-09-28T14:30:00+08:00`;日期型 `2026-09-28` |
|
||
| 体积 | 统一 **KB**(`finalSizeKb`),原始字节用 `rawSizeBytes` |
|
||
| 比例 | 字符串百分比 `"97.1%"`(避免前端再乘 100) |
|
||
| 空值 | 统一 `null`,不用空字符串 `""` |
|
||
| 枚举 | **数字码**(与 DDL `smallint` 对齐),文案由前端映射;**唯一例外:`materialCode` 用字符串码**(可读性优先,见 §2.1) |
|
||
| 排序 | 数组顺序即业务顺序,不额外传 `sortNo` 以外的字段 |
|
||
| 批量上限 | 单次批量操作 ≤ 200 条;超限返回 `40011` |
|
||
|
||
**布尔字段命名**:统一 `isXxx` / `hasXxx` / `needXxx`,**不用 0/1**(JSON 原生 true/false)。
|
||
|
||
---
|
||
|
||
## §2 枚举字典全集
|
||
|
||
> 与数据库 `smallint` 列严格对齐。**「无」是合法业务值**,见 §2.6。
|
||
|
||
### 2.1 材料类型 `materialCode`(字符串码)
|
||
|
||
| 编码 | 名称 | 别名(识别用) | 默认压缩档 |
|
||
|---|---|---|---|
|
||
| `PHOTO1` | 一寸照片 | 一寸电子照片 / 证件照 / 免冠照 / 1寸照片 | P1 |
|
||
| `IDCARD` | 身份证 | 身份证正面 / 身份证反面 / 身份证人像面 | P2 |
|
||
| `SCHOOL_ROLL` | 学籍表 | 学籍证明 / 在读证明 / 在校证明 | P4 |
|
||
| `XJW_BA` | 学信网备案表 | 学信网 / 学历备案表 / 电子注册备案表 | P4 |
|
||
| `WORK_PROOF` | **工作证明** | 个人申报表 / 职业技能等级个人申报表 | P3 |
|
||
| `OTHER_PROOF` | 其它佐证材料 | 其他 / 补充材料 | P3 |
|
||
|
||
> ⚠️ **`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`
|
||
|
||
| 值 | 含义 | 审核流 |
|
||
|:--:|---|---|
|
||
| `1` | 学校收集 | 学生 → 班主任初审 → 管理员终审 |
|
||
| `2` | 社会收集 | 提交后**直接进管理员终审队列**(跳过初审)。⚠️ **一期不启用** |
|
||
|
||
### 2.3 认证类型 `certType`
|
||
|
||
| 值 | 含义 | 面向谁 | 特有约束 |
|
||
|:--:|---|---|---|
|
||
| `1` | 自主认定 | 本单位职工 / 本校学生 | 要**学籍表** |
|
||
| `2` | **社会评价** | **社会人员** | 材料为**学信网备案表** + 其它佐证(**无学籍表**)。⚠️ **一期不启用**(零样本材料) |
|
||
| `3` | **专项职业能力** | 专项能力考核 | ⚠️ **无工种 / 无等级 / 无补考科目(0 科)**。⚠️ **一期不启用** |
|
||
|
||
> ⚠️ **`社会评价`(认定类型)≠ `社会收集`(`collectMode`)**:
|
||
> 前者决定**填什么材料**,后者决定**谁审核**(有无班主任初审层)。二者可任意组合。
|
||
|
||
### 2.4 填报单状态 `submissionStatus`
|
||
|
||
| 值 | 含义 | 可流转到 |
|
||
|:--:|---|---|
|
||
| `0` | 草稿 | 1 |
|
||
| `1` | 待初审 | 2 / 4 |
|
||
| `2` | 待终审 | 3 / 4 |
|
||
| `3` | 已通过 | 5 |
|
||
| `4` | 已驳回 | 1(学生重提交) |
|
||
| `5` | 已归档 | — |
|
||
|
||
### 2.5 问题级别 `severity`
|
||
|
||
| 值 | 含义 | UI 色 | 阻断提交 |
|
||
|:--:|:--:|:--:|:--:|
|
||
| `1` | 信息 | 蓝 | 否 |
|
||
| `2` | 警告 | 黄 | 否(可 `confirmWarning` 强提) |
|
||
| `3` | **错误** | 红 | **是** |
|
||
|
||
### 2.6 「无」的语义(三条铁律)
|
||
|
||
> 这是最容易实现错的地方,单列说明。
|
||
|
||
| 场景 | 错误做法 | 正确做法 |
|
||
|---|---|---|
|
||
| 学历为"无" | 存 `null` | 存字符串 `"无"`——**它是合法业务值** |
|
||
| 专项职业能力无工种 | 存 `null` | 存 `null` + `certType=3`,**由 `certType` 决定字段是否参与校验** |
|
||
| 补考科目为"无" | 存 `null` | 存 `[]` 空数组(**0 科是合法值**) |
|
||
|
||
**判定原则**:**「用户选择了无」与「用户没填」必须能区分**。前者是有效数据,后者是缺失。
|
||
|
||
### 2.7 其他枚举
|
||
|
||
| 枚举 | 取值 |
|
||
|---|---|
|
||
| `userType` | `1` 平台运营 / `2` 学校管理员 / `3` 班主任 / `4` 学生 |
|
||
| `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` 待人工 |
|
||
| `auditNode` | `1` 初审 / `2` 终审 |
|
||
| `auditAction` | `1` 通过 / `2` 驳回 / `3` 批量通过 / `4` 批量驳回 / `5` 转交 / `6` 代传 / `7` **撤回** |
|
||
| `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` **导出完成** |
|
||
| `exportStatus` | `0` 排队中 / `1` 打包中 / `2` 已完成 / `3` 失败 / `4` 已过期 |
|
||
|
||
> ✅ **`notifyBizType` 的 `5`/`6` 曾经在本文件内部对调过(已修正,2026-09-30 客户确认)**:
|
||
> 本表旧版写「`5` 导出完成 / `6` 催报」,与 §13.5 消息中心示例、落库值
|
||
> `collect_notify_record.biz_type`(`5` 催报 / `6` 导出完成)不一致 ——
|
||
> 若按旧表实现,催报会被写成"导出完成",**催报去重会静默失效**(老师点两下学生收到两条),
|
||
> 而排查时从数据上完全看不出是码对不上。现全文档统一为「`5` 催报 / `6` 导出完成」。
|
||
> 记录见《08-完成度盘点与问题清单》**F-30**。
|
||
|
||
---
|
||
|
||
## §3 通用数据结构
|
||
|
||
> 下列对象在多个接口中复用。**字段名与数据库列名一一对应**(下划线转驼峰),便于后端直接映射,避免二次改名。
|
||
> 表格中「↔ 列」列标注该字段对应的 DDL 列,`—` 表示计算字段。
|
||
|
||
### 3.1 `TaskVO` 任务
|
||
|
||
| 字段 | 类型 | ↔ 列 | 说明 |
|
||
|---|---|---|---|
|
||
| `id` | int64 | `id` | 任务 ID |
|
||
| `taskCode` | string | `task_code` | 任务编码,如 `2026090001` |
|
||
| `taskName` | string | `task_name` | 任务名称 |
|
||
| `collectMode` | int | `collect_mode` | `1` 学校 / `2` 社会 |
|
||
| `batchNo` | string | `batch_no` | 批次号,如 `202609` |
|
||
| `majorName` | string | `major_name` | 专业名称 |
|
||
| `jobName` | string | `job_name` | 职业(工种)名称 |
|
||
| `jobLevel` | string | `job_level` | 等级,如 `四级` |
|
||
| `certType` | int | `cert_type` | 见 §2.3 |
|
||
| `startTime` / `endTime` | string | `start_time` / `end_time` | 填报起止 |
|
||
| `auditConfig` | object | `audit_config` | 见 3.2 |
|
||
| `namingTemplate` | string | `naming_template` | 命名模板 |
|
||
| `compressConfig` | object | `compress_config` | 见 3.11 |
|
||
| `rosterRequired` | bool | `roster_required` | 是否强制门禁(**当前恒为 true**) |
|
||
| `status` | int | `status` | 见 §2.7 `taskStatus` |
|
||
| `publishedTime` | string | `published_time` | 发布时间 |
|
||
| `stat` | object | — | 统计快照,见 3.3 |
|
||
| `createTime` | string | `create_time` | — |
|
||
|
||
### 3.2 `AuditConfigVO` 审核配置
|
||
|
||
| 字段 | 类型 | 默认 | 说明 |
|
||
|---|---|---|---|
|
||
| `needClassAudit` | bool | `true` | 是否需班主任初审(**社会收集恒为 false**)。一期不启用社会收集 |
|
||
| `needAdminAudit` | bool | `true` | 是否需管理员终审 |
|
||
| `classAuditTimeoutHours` | int | `48` | 初审超时提醒 |
|
||
| `adminAuditTimeoutHours` | int | `72` | 终审超时提醒 |
|
||
| `maxRejectCount` | int | `3` | 驳回次数上限,超限升级管理员 |
|
||
| `allowProxyUpload` | bool | `true` | 允许班主任代传 |
|
||
| `batchApproveOnlyGreen` | bool | `true` | **批量通过只带无问题项(不可关闭)** |
|
||
|
||
### 3.3 `TaskStatVO` 任务统计
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|---|---|---|
|
||
| `rosterTotal` | int | 应填人数(名册行数) |
|
||
| `submittedCount` | int | 已提交数 |
|
||
| `approvedCount` | int | 已通过数 |
|
||
| `pendingCount` | int | 待审数 |
|
||
| `rejectedCount` | int | 已驳回数 |
|
||
| `unsubmittedCount` | int | 未提交数 |
|
||
| **`submitRate`** | string | **已提交率**(`submittedCount / rosterTotal`) |
|
||
| `passRate` | string | 通过率(`approvedCount / rosterTotal`) |
|
||
| `abnormalCount` | int | 有红/黄问题的单数 |
|
||
| `avgAuditHours` | number | 平均处理时长(小时) |
|
||
|
||
> ⚠️ **`submitRate` 与 `passRate` 必须分开报**。原型定稿的口径是「**完成率 82% 是已提交率 23/28**」,不要合并成一个数字——已提交≠已通过。
|
||
|
||
### 3.4 `MaterialDefVO` 材料项定义
|
||
|
||
| 字段 | 类型 | ↔ 列 | 说明 |
|
||
|---|---|---|---|
|
||
| `id` | int64 | `id` | — |
|
||
| `materialCode` | string | `material_code` | 见 §2.1 |
|
||
| `materialName` | string | `material_name` | 展示名 |
|
||
| `aliases` | string[] | `aliases` | 别名集合(识别用) |
|
||
| `namePattern` | string | `name_pattern` | 命名模板,如 `{姓名}-{证件号}-{材料名}{后缀}` |
|
||
| `required` | bool | `required` | 是否必传 |
|
||
| `visibleWhen` | object | `visible_when` | 条件显示,如 `{ "field": "is_student", "op": "eq", "value": true }` |
|
||
| `fileTypes` | string[] | `file_types` | 格式白名单 `["jpg","jpeg","png","pdf","heic"]` |
|
||
| `maxSizeKb` | int | `max_size_kb` | 体积上限(**默认 1024**) |
|
||
| `minSizeKb` | int | — | **体积下限保护**,低于此值提示"可能不是原图" |
|
||
| `compressProfile` | string | `compress_profile` | `P1`~`P5` |
|
||
| `minLongSide` | int | `min_long_side` | 分辨率下限(长边 px) |
|
||
| `minQuality` | int | `min_quality` | 压缩质量下限(默认 70) |
|
||
| `targetDpi` | int | `target_dpi` | 目标 DPI(默认 300) |
|
||
| `fixedSize` | object | `fixed_size` | 固定尺寸,如 `{ "width": 295, "height": 413 }` |
|
||
| `outputFormat` | string | `output_format` | 输出格式;`null` = 保持原格式 |
|
||
| `maxCount` | int | `max_count` | 最多可传份数 |
|
||
| `mergeEnabled` | bool | `merge_enabled` | 是否允许多图合并为一份(如身份证正反面) |
|
||
| `ocrType` | string | `ocr_type` | `IDCARD` / `NONE` |
|
||
| `sampleUrl` | string | `sample_url` | 示例图 |
|
||
| `tipText` | string | `tip_text` | **来源引导语**(显示在学生端) |
|
||
| `sortNo` | int | `sort_no` | 排序 |
|
||
|
||
### 3.5 `FormSchemaVO` 表单定义
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|---|---|---|
|
||
| `version` | int | 版本号,**已发布任务改表单必须递增** |
|
||
| `isCurrent` | bool | 是否当前生效版本 |
|
||
| `fields` | array | 字段数组,见下 |
|
||
| `fields[].key` | string | 字段键,内置字段与 `collect_submission` 列名一致 |
|
||
| `fields[].label` | string | 展示标签 |
|
||
| `fields[].type` | string | `text` / `number` / `select` / `cascade` / `date` / `idcard` / `phone` / `bool` |
|
||
| `fields[].builtin` | bool | **是否内置字段(内置不可删除)** |
|
||
| `fields[].locked` | bool | **是否锁定(学生不可改)** |
|
||
| `fields[].required` | bool | 必填 |
|
||
| `fields[].dictCode` | string | 选项来源字典码 |
|
||
| `fields[].options` | array | 手填选项 |
|
||
| `fields[].validate` | object | `{ "regex": "...", "min": 0, "max": 0, "message": "..." }` |
|
||
| `fields[].visibleWhen` | object | 条件显示规则 |
|
||
| `fields[].tip` | string | 字段提示 |
|
||
| `fields[].sortNo` | int | 排序 |
|
||
|
||
**内置字段清单(21 个,`builtin: true`)**
|
||
|
||
| 分类 | 字段 key | 学生端权限 |
|
||
|---|---|---|
|
||
| 身份 | `real_name`、`id_card_no` | 🔒 锁定 |
|
||
| 报考 | `occupation`、`worktype`、`job_level` | 🔒 锁定 |
|
||
| 学历 | `education`、`is_student`、`school`、`major`、`enroll_date` | ✏️ 可核对修改 |
|
||
| 工作 | `work_unit`、`work_years` | ✏️ 可核对修改 |
|
||
| 证书 | `cert_occupation`、`cert_level`、`cert_no` | ✏️ 可核对修改 |
|
||
| 补考 | `is_retake`、`retake_theory`、`retake_practice`、`retake_overall` | ✏️ 可核对修改 |
|
||
| 联系 | `phone` | ✏️ 可核对修改 |
|
||
|
||
> **锁定三项的理由**:姓名、身份证号、报考三项一旦改动,会与名册、报考范围、导出模板全部失配,**必须由老师改**。
|
||
> **改动留痕**:学生修改任一预填字段,写入 `collect_submission.changed_fields`(JSON 数组),审核时高亮显示。
|
||
|
||
### 3.6 `RosterVO` 名册项
|
||
|
||
| 字段 | 类型 | ↔ 列 | 说明 |
|
||
|---|---|---|---|
|
||
| `id` | int64 | `id` | 名册项 ID |
|
||
| `seqNo` | int | `seq_no` | 序号 |
|
||
| `studentNo` | string | `student_no` | 学号 |
|
||
| `realName` | string | `real_name` | 姓名 |
|
||
| `namePinyin` | string | `name_pinyin` | 姓名拼音(形近字纠错用) |
|
||
| `idCardNo` | string | — | ⚠️ **仅导入时可传,响应一律返回 `idCardMask`** |
|
||
| `idCardMask` | string | `id_card_mask` | 脱敏证件号 |
|
||
| `idCardHash` | string | `id_card_hash` | 哈希(核验用,不可逆) |
|
||
| `phone` | string | — | 手机号(响应脱敏) |
|
||
| `phoneMask` | string | — | 脱敏手机号 |
|
||
| `orgId` / `orgName` | int64 / string | `org_id` | 所属班级 |
|
||
| `seqNo` | int | `seq_no` | — |
|
||
| `occupation` / `worktype` / `jobLevel` | string | `ext_json` | 报考三项(**来自导入,非独立字段**) |
|
||
| `extJson` | object | `ext_json` | 其它列 |
|
||
| `bindStatus` | int | `bind_user_id` | `0` 未绑定 / `1` 已绑定 |
|
||
| `bindTime` | string | `bind_time` | 绑定时间 |
|
||
| `bindType` | int | `bind_type` | `1` 本人 / `2` 代填 |
|
||
| `submitStatus` | int | `submit_status` | 见 §2.7 |
|
||
|
||
### 3.7 `SubmissionVO` 填报单
|
||
|
||
| 字段 | 类型 | ↔ 列 | 说明 |
|
||
|---|---|---|---|
|
||
| `id` | int64 | `id` | 填报单 ID |
|
||
| `taskId` | int64 | `task_id` | — |
|
||
| `rosterId` | int64 | `roster_id` | 名册项 |
|
||
| `formVersion` | int | `form_version` | 填报时的表单版本 |
|
||
| `formData` | object | `form_data` | 表单值(内置字段在此冗余一份快照) |
|
||
| `extrasJson` | object | `extras_json` | 扩展字段值 |
|
||
| `changedFields` | string[] | `changed_fields` | **学生改过的预填字段列表**(审核高亮用) |
|
||
| `status` | int | `status` | 见 §2.4 |
|
||
| `rejectCount` | int | `reject_count` | 累计驳回次数 |
|
||
| `lastRejectReason` | object | `last_reject_reason` | `{ reasonCode, reasonText }` |
|
||
| `redCount` / `yellowCount` | int | `red_count` / `yellow_count` | 红/黄问题数 |
|
||
| `materials` | array | — | `MaterialFileVO[]`(见 3.8),**仅详情接口返回** |
|
||
| `issues` | array | — | `ValidationIssueVO[]`(见 3.9),**仅详情接口返回** |
|
||
| `submitTime` | string | `submit_time` | 提交时间 |
|
||
| `classAudit` | object | — | 初审信息 `{ auditorName, auditTime, action }` |
|
||
| `adminAudit` | object | — | 终审信息 |
|
||
| `createTime` | string | `create_time` | — |
|
||
|
||
**展开字段(`formData` 内的扁平字段,与 DDL 列同名)**
|
||
|
||
`real_name` · `id_card_no` · `occupation` · `worktype` · `job_level` · `education` · `is_student` · `school` · `major` · `enroll_date` · `work_unit` · `is_retake` · `retake_theory` · `retake_practice` · `retake_overall` · `work_years` · `cert_occupation` · `cert_level` · `cert_no`
|
||
|
||
> 这些字段**在 `collect_submission` 里是正式列**(不是 JSON),所以可以直接 `WHERE education = '无'` 做统计。`extras_json` 只存扩展字段。
|
||
|
||
### 3.8 `MaterialFileVO` 材料文件
|
||
|
||
| 字段 | 类型 | ↔ 列 | 说明 |
|
||
|---|---|---|---|
|
||
| `id` | int64 | `id` | 文件 ID |
|
||
| `materialCode` | string | `material_code` | 所属槽位 |
|
||
| `version` | int | `version` | 版本号 |
|
||
| `isCurrent` | bool | `is_current` | 是否当前版本 |
|
||
| `originalName` | string | `original_name` | 学生原始文件名 |
|
||
| `standardName` | string | `standard_name` | **规范文件名** |
|
||
| `fileExt` | string | `file_ext` | 扩展名 |
|
||
| `mimeType` | string | `mime_type` | — |
|
||
| `rawSizeKb` | number | `raw_size` | 原始体积(KB) |
|
||
| `finalSizeKb` | number | `final_size` | 处理后体积(KB) |
|
||
| `compressRatio` | string | — | 压缩率,如 `"97.1%"` |
|
||
| `previewUrl` | string | — | 预览签名 URL(有效期 30 分钟) |
|
||
| `width` / `height` | int | `width` | 像素尺寸 |
|
||
| `dpi` | int | `dpi` | 实际 DPI |
|
||
| `ocrStatus` | int | `ocr_status` | `0` 未跑 / `1` 成功 / `2` 失败 / `3` 跳过 |
|
||
| `ocrConfidence` | number | `ocr_confidence` | 置信度 0–1 |
|
||
| `detectedMaterialCode` | string | `detected_material_code` | **系统识别结果** |
|
||
| `detectConfidence` | number | `detect_confidence` | 识别置信度 |
|
||
| `detectSource` | int | `detect_source` | 见 §2.7 `detectSource` |
|
||
| `detectConflict` | bool | `detect_conflict` | **识别结果与所在槽位冲突** |
|
||
| `needManualPlace` | bool | `need_manual_place` | 需人工分拣 |
|
||
| `vlmCalled` | bool | `vlm_called` | 是否调用过 VLM(**成本监控用**) |
|
||
| `validateStatus` | int | `validate_status` | 见 §2.7 `fileValidateStatus` |
|
||
| `issues` | array | — | 该文件的 `ValidationIssueVO[]` |
|
||
| `source` | int | `source` | `1` 本人上传 / `2` 班主任代传 / `3` 管理员代传 / `4` 整理器导入(**该模块已取消,仅历史数据**) |
|
||
| `rawExpireTime` | string | `raw_expire_time` | **原图过期时间(默认 30 天)** |
|
||
|
||
> ✅ **`collect_file.source` 的口径曾经有两套,已确认(2026-09-30 客户确认)**:本表旧版写
|
||
> "从哪个入口选的"(`1` 聊天记录 / `2` 相册 / `3` 手机文件 / `4` 代传),与建表注释
|
||
> (`V1__business_tables.sql`)冲突。**以建表注释为准("谁传的")**:§10.6 要求代传可追溯,
|
||
> 要回答的是"**谁**替他传的、**为什么**",而"从哪个入口选的"会随同一个学生的操作路径变化,
|
||
> 统计上没有意义。实现见 `CollectFile.SOURCE_*` 常量。记录见《08》**F-38 / F-39**。
|
||
|
||
**`ownership.verdict` 取值**(对应 PRD §6.1 归属裁决矩阵)
|
||
|
||
| verdict | 含义 | blockSubmit | 学生端表现 |
|
||
|---|---|:--:|---|
|
||
| `NO_IDCARD_IN_NAME` | 文件名无证件号(正常) | false | 无提示 |
|
||
| `CONFIRMED_SAME` | 证件号 = 名册值 | false | 无提示 |
|
||
| **`BELONGS_TO_OTHER`** | **证件号指向名册中的另一个人** | **true** | **阻断**,提示文件名对应的是谁 |
|
||
| `UNKNOWN_IDCARD` | 证件号合法但名册查不到 | 视 OCR 而定 | 无 OCR 佐证时阻断 |
|
||
| `CHECKSUM_INVALID` | 校验位失败(只是写错了) | false | 信息级"已按名册修正" |
|
||
| `NO_ROSTER` | 本次无名单可依 | false | 标黄 |
|
||
|
||
> ⚠️ **`canForce: false` 是硬约定**:归属问题**不提供"仍然继续"**。
|
||
> 与 `VAL-08`(分辨率偏低)这类警告不同——那种可以"仍然使用",这个不行。
|
||
|
||
**`detectSource` 的展示约定**:审核辅助显示「系统判定:身份证 92%」,其中档位由 `detectSource` 决定可信度标注:
|
||
|
||
| `detectSource` | 展示文案 |
|
||
|---|---|
|
||
| `1` 文件名 | 文件名匹配 |
|
||
| `2` 形态 | 形态识别 |
|
||
| `3` OCR 内容 | 内容识别 |
|
||
| `4` VLM | **智能识别**(置信度 < 0.8 时不显示,避免误导) |
|
||
|
||
### 3.9 `ValidationIssueVO` 校验问题
|
||
|
||
| 字段 | 类型 | ↔ 列 | 说明 |
|
||
|---|---|---|---|
|
||
| `id` | int64 | `id` | — |
|
||
| `materialCode` | string | — | 关联材料(可为空) |
|
||
| `fileId` | int64 | `file_id` | 关联文件 |
|
||
| `ruleCode` | string | `rule_code` | 如 `VAL-05` |
|
||
| `severity` | int | `severity` | 见 §2.5 |
|
||
| `fieldKey` | string | `field_key` | 关联字段(表单类问题) |
|
||
| `title` | string | `title` | 标题 |
|
||
| `detail` | string | `detail` | 详情 |
|
||
| `expectedVal` | string | `expected_val` | 期望值(**脱敏**) |
|
||
| `actualVal` | string | `actual_val` | 实际值(**脱敏**) |
|
||
| `autoFixed` | bool | `auto_fixed` | 是否已自动修复 |
|
||
| `resolved` | bool | `resolved` | 是否已解决 |
|
||
| `resolvedBy` | string | `resolved_by` | 处理人 |
|
||
|
||
### 3.10 `AuditRecordVO` 审核记录
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|---|---|---|
|
||
| `id` | int64 | — |
|
||
| `auditNode` | int | `1` 初审 / `2` 终审 |
|
||
| `auditorId` / `auditorName` | int64 / string | 审核人 |
|
||
| `action` | int | 见 §2.7 `auditAction`(**撤回是 `7`**;`3` 是批量通过 —— 旧版此表把两者写混过) |
|
||
| `reasonCode` | string | 驳回原因码(见 PRD 附录 C) |
|
||
| `reasonText` | string | 手填原因 |
|
||
| `materialCodes` | string[] | 关联材料 |
|
||
| `durationSec` | int | 从提交到本次操作的耗时 |
|
||
| `createTime` | string | — |
|
||
|
||
### 3.11 `CompressConfigVO` 压缩配置
|
||
|
||
| 字段 | 类型 | 默认 | 说明 |
|
||
|---|---|---|---|
|
||
| `defaultMaxSizeKb` | int | `1024` | 全局体积上限(**D1 决策:无例外**) |
|
||
| `defaultTargetDpi` | int | `300` | A4 扫描件目标 DPI |
|
||
| `defaultQuality` | int | `85` | 默认质量 |
|
||
| `minQuality` | int | `70` | 质量下限(低于此值标记"强制压缩") |
|
||
| `hardFloorDpi` | int | `200` | DPI 硬下限 |
|
||
| `maxPixels` | int | `50000000` | 超大图熔断阈值(5000 万像素) |
|
||
| `keepPngForScreenshot` | bool | `true` | **截图类保持 PNG 不转 JPG** |
|
||
| `markWhenForced` | bool | `true` | 强制压缩时打标记进抽检清单 |
|
||
|
||
### 3.12 其余对象(简表)
|
||
|
||
| 对象 | 字段 |
|
||
|---|---|
|
||
| `OrgVO` | `id` `parentId` `orgName` `orgType` `orgCode` `ancestorPath` `leaderUserId` `leaderName` `sortNo` `status` `children[]` `studentCount` |
|
||
| `TenantVO` | `id` `tenantCode` `tenantName` `tenantType` `packageId` `packageName` `expireTime` `status` `logoUrl` `configJson` `usage{ studentCount, storageMb, ocrCount, smsCount }` |
|
||
| `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` |
|
||
| `DictItemVO` | `id` `dictCode` `dictName` `itemCode` `itemName` `sortNo` `parentItemCode` `status` `builtin` |
|
||
| `MessageVO` | `id` `bizType` `title` `content` `read` `linkType` `linkId` `createTime` |
|
||
|
||
---
|
||
|
||
## §4 接口总览
|
||
|
||
> 合计 **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**。
|
||
> 「权限」列:`公开` / `学生` / `教师` / `管理` / `运营`(平台方)。
|
||
> 「修补」列指该接口是补齐追溯矩阵审计发现的缺口。
|
||
|
||
| 分类 | 接口数 |
|
||
|---|:--:|
|
||
| 认证与账号 | 13 |
|
||
| 组织与租户 | 15 |
|
||
| 任务与配置 | 25 |
|
||
| 名册 | 7 |
|
||
| 填报端 · 小程序 | 16 |
|
||
| 班主任移动端 | 10 |
|
||
| 审核 | 9 |
|
||
| 校验 / 导出 / 通知 | 12 |
|
||
| 开放接口 | 4 |
|
||
| ⭐ 角色与授权 | 8 |
|
||
| **合计** | **119** |
|
||
|
||
### 4.1 认证与账号(13)
|
||
|
||
| | 路径 | 说明 | 权限 |
|
||
|---|---|---|---|
|
||
| POST | `/auth/login` | PC 账号密码登录 | 公开 |
|
||
| POST | `/auth/logout` | 退出 | 全部 |
|
||
| GET | `/auth/me` | 当前用户 + 权限菜单 | 全部 |
|
||
| PUT | `/auth/password` | 修改密码 | 全部 |
|
||
| POST | `/mp/login` | 小程序 code 换 session | 公开 |
|
||
| POST | `/mp/bind-phone` | 微信手机号快捷绑定 | 学生/教师 |
|
||
| POST | `/mp/roster/match` | **门禁核验** | 学生 |
|
||
| GET | `/users` | 用户列表 | 管理 |
|
||
| POST | `/users` | 新增用户 | 管理 |
|
||
| PUT | `/users/{id}` | 修改用户 | 管理 |
|
||
| DELETE | `/users/{id}` | 停用用户 | 管理 |
|
||
| POST | `/users/import` | 批量导入 | 管理 |
|
||
| POST | `/users/{id}/reset-password` | 重置密码 | 管理 |
|
||
|
||
### 4.2 组织与租户(15)
|
||
|
||
| | 路径 | 说明 | 权限 |
|
||
|---|---|---|---|
|
||
| GET | `/orgs/tree` | 组织树 | 管理 |
|
||
| POST | `/orgs` | 新增组织 | 管理 |
|
||
| PUT | `/orgs/{id}` | 修改 | 管理 |
|
||
| DELETE | `/orgs/{id}` | 删除 | 管理 |
|
||
| POST | `/orgs/import` | 批量导入 | 管理 |
|
||
| GET | `/tenants` | 租户列表 | 运营 |
|
||
| POST | `/tenants` | 开通租户 | 运营 |
|
||
| PUT | `/tenants/{id}` | 修改租户/套餐 | 运营 |
|
||
| GET | `/tenants/{id}/usage` | 用量统计 | 运营 |
|
||
| ⭐ GET | `/dicts` | **字典列表** | 管理 |
|
||
| ⭐ GET | `/dicts/{code}/items` | **字典项** | 管理 |
|
||
| ⭐ POST | `/dicts/{code}/items` | **新增字典项** | 管理 |
|
||
| ⭐ PUT | `/dicts/items/{id}` | **修改字典项** | 管理 |
|
||
| ⭐ GET | `/logs/operations` | **操作日志** | 管理 |
|
||
| ⭐ GET | `/logs/logins` | **登录日志** | 管理 |
|
||
|
||
### 4.3 任务与配置(25)
|
||
|
||
| | 路径 | 说明 | 权限 |
|
||
|---|---|---|---|
|
||
| GET | `/tasks` | 任务列表 | 管理 |
|
||
| POST | `/tasks` | 创建任务 | 管理 |
|
||
| GET | `/tasks/{id}` | 任务详情 | 管理/教师 |
|
||
| PUT | `/tasks/{id}` | 修改任务 | 管理 |
|
||
| DELETE | `/tasks/{id}` | 删除(仅草稿) | 管理 |
|
||
| POST | `/tasks/{id}/publish` | 发布 | 管理 |
|
||
| POST | `/tasks/{id}/close` | 截止 | 管理 |
|
||
| POST | `/tasks/{id}/archive` | 归档 | 管理 |
|
||
| ⭐ POST | `/tasks/{id}/pause` | **暂停** | 管理 |
|
||
| ⭐ POST | `/tasks/{id}/resume` | **恢复** | 管理 |
|
||
| POST | `/tasks/{id}/clone` | 克隆 | 管理 |
|
||
| GET | `/tasks/{id}/orgs` | 覆盖班级与班主任 | 管理 |
|
||
| PUT | `/tasks/{id}/orgs` | 设置覆盖班级/班主任 | 管理 |
|
||
| ⭐ GET | `/tasks/{id}/plan-scope` | **报考范围读取** | 管理 |
|
||
| ⭐ PUT | `/tasks/{id}/plan-scope` | **报考范围确认** | 管理 |
|
||
| GET | `/tasks/{id}/form-schema` | 表单定义 | 管理 |
|
||
| PUT | `/tasks/{id}/form-schema` | 保存表单(版本化) | 管理 |
|
||
| GET | `/tasks/{id}/materials` | 材料项列表 | 管理 |
|
||
| POST | `/tasks/{id}/materials` | 新增材料项 | 管理 |
|
||
| PUT | `/materials/{id}` | 修改材料项 | 管理 |
|
||
| DELETE | `/materials/{id}` | 删除材料项 | 管理 |
|
||
| GET | `/tasks/{id}/statistics` | 任务统计 | 管理/教师 |
|
||
| GET | `/tasks/{id}/qrcode` | 填报二维码 | 管理 |
|
||
| GET | `/rule-templates` | 规则模板列表 | 管理 |
|
||
| POST | `/rule-templates/{id}/apply` | 应用模板 | 管理 |
|
||
|
||
### 4.4 名册(7)
|
||
|
||
| | 路径 | 说明 | 权限 |
|
||
|---|---|---|---|
|
||
| POST | `/tasks/{id}/roster/import` | 导入(返回体检报告) | 管理 |
|
||
| GET | `/tasks/{id}/roster` | 名册分页 | 管理/教师 |
|
||
| PUT | `/roster/{rid}` | 修改名册项 | 管理 |
|
||
| DELETE | `/roster/{rid}` | 删除名册项 | 管理 |
|
||
| GET | `/tasks/{id}/roster/template` | 下载导入模板 | 管理 |
|
||
| POST | `/roster/{rid}/notify` | 定向催报 | 管理/教师 |
|
||
| POST | `/tasks/{id}/roster/notify-unsubmitted` | 一键催报未提交者 | 管理/教师 |
|
||
|
||
### 4.5 填报端 · 小程序(16)
|
||
|
||
| | 路径 | 说明 | 权限 |
|
||
|---|---|---|---|
|
||
| GET | `/mp/tasks` | 我的任务列表 | 学生 |
|
||
| GET | `/mp/tasks/{id}/form` | 表单结构 + 已填数据 | 学生 |
|
||
| POST | `/mp/submissions` | 创建/获取草稿 | 学生 |
|
||
| PUT | `/mp/submissions/{id}` | 保存草稿 | 学生 |
|
||
| POST | `/mp/submissions/{id}/submit` | 提交 | 学生 |
|
||
| POST | `/mp/submissions/{id}/withdraw` | 撤回(未审核前) | 学生 |
|
||
| GET | `/mp/submissions/{id}/progress` | 进度与问题清单 | 学生 |
|
||
| POST | `/mp/files/upload-token` | OSS 直传凭证 | 学生 |
|
||
| ⭐ POST | `/mp/files/{fileId}/process` | **文件规范化(命名+压缩+识别)** | 学生 |
|
||
| POST | `/mp/files/batch-process` | 批量处理(多材料) | 学生 |
|
||
| GET | `/mp/files/{fileId}` | 预览签名 URL | 学生/教师 |
|
||
| DELETE | `/mp/files/{fileId}` | 删除材料(软删) | 学生 |
|
||
| POST | `/mp/ocr/idcard` | 身份证 OCR | 学生 |
|
||
| ⭐ GET | `/mp/messages` | **消息中心** | 学生 |
|
||
| ⭐ POST | `/mp/messages/read` | **标记已读** | 学生 |
|
||
| ⭐ POST | `/mp/subscribe` | **订阅消息额度登记** | 学生 |
|
||
|
||
> 注:本表 16 行,其中 `/mp/files/{fileId}/process` 与 `/mp/files/batch-process` 在 PRD §8.6 中分别登记于 §8.6 与 §8.9,此处合并列出。
|
||
|
||
### 4.6 班主任移动端(10)
|
||
|
||
| | 路径 | 说明 | 权限 |
|
||
|---|---|---|---|
|
||
| GET | `/mp/teacher/dashboard` | 班级看板 | 教师 |
|
||
| GET | `/mp/teacher/audit/todos` | 待审列表(卡片流) | 教师 |
|
||
| GET | `/mp/teacher/audit/{submissionId}` | 学生详情 | 教师 |
|
||
| POST | `/mp/teacher/audit/{submissionId}/approve` | 通过 | 教师 |
|
||
| POST | `/mp/teacher/audit/{submissionId}/reject` | 驳回 | 教师 |
|
||
| POST | `/mp/teacher/audit/batch-approve` | **一键通过全部绿色项** | 教师 |
|
||
| POST | `/mp/teacher/proxy-upload` | 代传材料 | 教师 |
|
||
| POST | `/mp/teacher/notify/unsubmitted` | 一键催报 | 教师 |
|
||
| GET | `/mp/teacher/notify/records` | 催报历史 | 教师 |
|
||
| ⭐ GET | `/mp/teacher/messages` | **教师消息中心** | 教师 |
|
||
|
||
### 4.7 审核(9)
|
||
|
||
| | 路径 | 说明 | 权限 |
|
||
|---|---|---|---|
|
||
| GET | `/audit/todos` | 待办列表(红黄绿) | 教师/管理 |
|
||
| GET | `/audit/submissions/{id}` | 填报单详情 | 教师/管理 |
|
||
| POST | `/audit/submissions/{id}/approve` | 通过 | 教师/管理 |
|
||
| POST | `/audit/submissions/{id}/reject` | 驳回 | 教师/管理 |
|
||
| ⭐ POST | `/audit/submissions/{id}/revoke` | **撤回已审** | 教师/管理 |
|
||
| GET | `/audit/submissions/{id}/timeline` | 审核时间轴 | 教师/管理 |
|
||
| GET | `/audit/statistics` | 审核统计 | 教师/管理 |
|
||
| GET | `/audit/reason-codes` | 驳回原因字典 | 教师/管理 |
|
||
| POST | `/audit/submissions/{id}/proxy-upload` | 代传材料 | 教师/管理 |
|
||
|
||
### 4.8 校验 / 导出 / 通知(12)
|
||
|
||
| | 路径 | 说明 | 权限 |
|
||
|---|---|---|---|
|
||
| POST | `/validation/run` | 触发任务级全量校验 | 管理 |
|
||
| GET | `/validation/issues` | 问题清单 | 教师/管理 |
|
||
| GET | `/validation/issues/export` | 导出异常清单 Excel | 管理 |
|
||
| POST | `/exports` | 创建导出任务 | 管理 |
|
||
| GET | `/exports` | 导出历史 | 管理 |
|
||
| GET | `/exports/{id}` | 导出进度 | 管理 |
|
||
| GET | `/exports/{id}/download` | 下载签名 URL | 管理 |
|
||
| GET | `/notify/templates` | 通知模板 | 管理 |
|
||
| PUT | `/notify/templates/{id}` | 修改模板 | 管理 |
|
||
| POST | `/notify/send` | 手动发送通知 | 管理/教师 |
|
||
| GET | `/notify/records` | 通知记录 | 管理 |
|
||
| POST | `/callback/wechat/msg` | 微信订阅消息回调 | 公开 |
|
||
|
||
### 4.9 ⭐ 本次新增接口索引(16)
|
||
|
||
> 本表是**索引**,不是额外接口——其中 5 个已在上方 §4.3 / §4.7 列出,**不计入总数**。
|
||
> 表内 13 行覆盖 **16 个接口**(含 GET/PUT、GET/POST 合并行)。
|
||
> ⚠️ 需求收敛(2026-10)后,原表里的**历史整理器 5 个**随模块删除,故 21 − 5 = **16**。
|
||
|
||
| | 路径 | 说明 | 详情节 |
|
||
|---|---|---|---|
|
||
| ⭐ | `/tasks/{id}/plan-scope` | 报考范围读取 / 确认(GET / PUT) | §7.7 / §7.8 |
|
||
| ⭐ | `/tasks/{id}/pause` `/resume` | 暂停 / 恢复任务 | §7.4 |
|
||
| ⭐ | `/audit/submissions/{id}/revoke` | 撤回已审 | §11.4 |
|
||
| ⭐ | `/mp/files/{fileId}/process` | **文件规范化总入口**(原仅在 §8.9 详述,未登记进清单) | §9.6 |
|
||
| ⭐ | `/dicts` | 字典列表 | §13.3 |
|
||
| ⭐ | `/dicts/{code}/items` | 字典项(GET / POST) | §13.3 |
|
||
| ⭐ | `/dicts/items/{id}` | 修改字典项 | §13.3 |
|
||
| ⭐ | `/logs/operations` | 操作日志 | §13.4 |
|
||
| ⭐ | `/logs/logins` | 登录日志 | §13.4 |
|
||
| ⭐ | `/mp/messages` | 学生消息中心 | §13.5 |
|
||
| ⭐ | `/mp/messages/read` | 标记已读 | §13.5 |
|
||
| ⭐ | `/mp/teacher/messages` | 教师消息中心 | §13.5 |
|
||
| ⭐ | `/mp/subscribe` | 订阅消息额度登记 | §13.6 |
|
||
|
||
**新增 16 个的分布**
|
||
|
||
| 归属 | 数量 | 明细 |
|
||
|---|:--:|---|
|
||
| 报考范围 | 2 | plan-scope GET / PUT |
|
||
| 任务状态 | 2 | pause / resume |
|
||
| 审核 | 1 | revoke |
|
||
| 文件处理 | 1 | process(登记遗漏) |
|
||
| 字典 | 4 | dicts |
|
||
| 日志 | 2 | operations / logins |
|
||
| 消息中心 | 3 | 学生 2 + 教师 1 |
|
||
| 订阅额度 | 1 | subscribe |
|
||
|
||
### 4.10 开放接口(4)
|
||
|
||
| | 路径 | 说明 |
|
||
|---|---|---|
|
||
| POST | `/open/v1/auth/token` | appKey/secret 换 token |
|
||
| GET | `/open/v1/tasks/{id}/submissions` | 拉取已终审通过的数据 |
|
||
| GET | `/open/v1/tasks/{id}/files` | 批量获取文件签名 URL |
|
||
| POST | `/open/v1/callback/push` | 推送审核结果到机构系统 |
|
||
|
||
### 4.11 ⭐ 加入申请(已撤销 · 0 个)
|
||
|
||
> ⚠️ **本节原为「客户确认后新增 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**。
|
||
|
||
| | 路径 | 说明 | 权限 |
|
||
|---|---|---|---|
|
||
| ⭐ 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`) |
|
||
|
||
---
|
||
|
||
## §5 认证与账号
|
||
|
||
### 5.1 `POST /api/v1/auth/login`
|
||
|
||
**权限**:公开 **限流**:5 次/分钟/IP
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|---|---|---|---|
|
||
| `username` | string | ✅ | 账号 |
|
||
| `password` | string | ✅ | 密码(前端 RSA 加密后传输) |
|
||
| `captchaKey` | string | 条件 | 图形验证码 key(连续失败 2 次后必填) |
|
||
| `captchaCode` | string | 条件 | 验证码 |
|
||
|
||
**响应**
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"data": {
|
||
"token": "eyJhbGciOi...",
|
||
"expireSeconds": 28800,
|
||
"user": { "id": 10086, "realName": "王老师", "userType": 2, "avatarUrl": null },
|
||
"tenant": { "id": 1001, "tenantName": "江西机电职业技术学院", "logoUrl": "..." },
|
||
"roleCodes": ["SCHOOL_ADMIN"],
|
||
"menus": ["dashboard", "task", "roster", "audit", "export"]
|
||
}
|
||
}
|
||
```
|
||
|
||
**主要错误**:`40012` 账号或密码错误 / `40013` 账号锁定 / `42901` 限流
|
||
|
||
### 5.2 `GET /api/v1/auth/me`
|
||
|
||
**响应**:同 5.1 的 `user` + `tenant` + `roleCodes` + `menus` + `permissions[]`(细粒度权限码)。
|
||
|
||
### 5.3 `POST /api/v1/mp/login`
|
||
|
||
**权限**:公开
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|---|---|---|---|
|
||
| `code` | string | ✅ | `wx.login` 返回的 code |
|
||
| `scene` | string | — | 扫码进入时的小程序码参数(**含租户与任务标识**) |
|
||
|
||
**响应**
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"data": {
|
||
"token": "eyJ...",
|
||
"expireSeconds": 604800,
|
||
"needBindPhone": false,
|
||
"user": { "id": 20001, "realName": "曾伊豪", "userType": 4 },
|
||
"boundRoster": { "taskId": 202609, "taskName": "2026年9月机电认定", "rosterId": 9001 }
|
||
}
|
||
}
|
||
```
|
||
|
||
> `boundRoster` 用于**已绑定学生的免门禁直入**:门禁只需过一次,绑定关系长期有效。
|
||
|
||
### 5.4 `POST /api/v1/mp/roster/match` ⭐ 门禁核验(核心接口)
|
||
|
||
**权限**:学生 **限流**:同 openid 10 次/小时;同 IP 20 次/分钟 **幂等**:是
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|---|---|---|---|
|
||
| `realName` | string | ✅ | 姓名 |
|
||
| `idCardNo` | string | ✅ | 身份证号 |
|
||
| `taskId` | int64 | ✅ | 任务 ID |
|
||
|
||
**匹配逻辑**(服务端)
|
||
|
||
```
|
||
1. 限流检查 → 超限返回 40071
|
||
2. 规范化:姓名去空格、全角转半角;证件号转大写
|
||
3. 按 (task_id, id_card_hash) 定位名册项
|
||
├─ 未命中 → 记录失败尝试(不记明文) → 40007 统一提示
|
||
└─ 命中 → 比对姓名
|
||
├─ 完全一致 → 通过
|
||
├─ 形近字/同音字 → 返回候选让用户确认(FR-504c)
|
||
└─ 不一致 → 40007 统一提示
|
||
4. 检查绑定状态
|
||
├─ 已被他人绑定 → 40072
|
||
└─ 未绑定 → 写入 bind_user_id / bind_time / bind_type
|
||
```
|
||
|
||
**成功响应**
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"data": {
|
||
"matched": true,
|
||
"rosterId": 9001,
|
||
"realName": "曾伊豪",
|
||
"idCardMask": "3610292009******10",
|
||
"orgName": "24中技数控1班",
|
||
"prefill": {
|
||
"education": "中等职业技术学校",
|
||
"isStudent": true,
|
||
"school": "江西机电职业技术学院",
|
||
"major": "数控加工",
|
||
"enrollDate": "2024-09-01",
|
||
"phone": "138****8888",
|
||
"occupation": "车工",
|
||
"worktype": "数控车工",
|
||
"jobLevel": "四级"
|
||
},
|
||
"needConfirmName": false
|
||
}
|
||
}
|
||
```
|
||
|
||
**姓名形近字确认响应**(FR-504c)
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"data": {
|
||
"matched": false,
|
||
"needConfirmName": true,
|
||
"candidates": [ { "rosterId": 9001, "realName": "曾伊豪", "similarity": 0.92 } ],
|
||
"confirmToken": "tmp_8f3a...",
|
||
"tip": "你是不是「曾伊豪」?"
|
||
}
|
||
}
|
||
```
|
||
|
||
**失败统一响应**(`40007`)——**不区分是姓名错还是号码错**
|
||
|
||
```json
|
||
{
|
||
"code": 40007,
|
||
"msg": "没有找到你的信息,请核对后重试",
|
||
"data": { "matched": false, "remainAttempts": 7, "contactTeacher": true }
|
||
}
|
||
```
|
||
|
||
> **安全要求**:`remainAttempts` 只报剩余次数,**不透露任何关于名单的信息**;失败尝试**不落明文证件号**(只落 hash 与时间)。
|
||
|
||
### 5.5 ~~`GET /api/v1/mp/roster/contact`~~ 【已移除】
|
||
|
||
> ❌ **本接口已按客户要求移除**(2026-09-28)。
|
||
>
|
||
> **原设计**:门禁失败页展示班主任/管理员的脱敏手机号,学生可一键拨打。
|
||
>
|
||
> **移除理由**:学生在班级群里**本来就有班主任的联系方式**,系统内再放一份:
|
||
> ① 不必要——用户已经知道怎么找人;
|
||
> ② 有维护成本——班主任换人、号码变更都要同步;
|
||
> ③ 多一处隐私暴露面——即使是脱敏号,也意味着系统里要多存一份联系方式数据。
|
||
>
|
||
> **替代**:门禁失败页只保留「**重新填写**」一个**系统内动作**(原「申请加入名单」通道已于 2026-10 需求收敛时取消)。
|
||
> 纯文案提示(如"如确认已报名,请联系老师把你加入名单")保留,因为**不涉及任何联系方式数据**;
|
||
> 名单外的人由老师在任务里手工加人(`FR-508` 追加导入)。
|
||
|
||
### 5.6 `GET /api/v1/users`
|
||
|
||
**Query**:`keyword`(姓名/账号/手机号模糊)、`orgId`、`roleCode`、`userType`、`status`、分页参数
|
||
|
||
**响应**:`{ list: UserVO[], total }`
|
||
|
||
### 5.7 `POST /api/v1/users`
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|---|---|---|---|
|
||
| `username` | string | ✅ | 登录账号,租户内唯一 |
|
||
| `realName` | string | ✅ | 姓名 |
|
||
| `phone` | string | ✅ | 手机号 |
|
||
| `orgId` | int64 | — | 所属组织 |
|
||
| `roleCodes` | string[] | ✅ | 至少 1 个 |
|
||
| `userType` | int | ✅ | 见 §2.7 |
|
||
|
||
**响应**:`{ "id": 10087, "initialPassword": "Ab3#k9mP" }`(初始密码仅此一次返回)
|
||
|
||
### 5.8 `POST /api/v1/users/import`
|
||
|
||
**Content-Type**:`multipart/form-data` **字段**:`file`(Excel,≤10MB)
|
||
|
||
**响应**(导入体检报告,与名册导入同构)
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"data": {
|
||
"total": 42, "success": 40, "failed": 2,
|
||
"headerRow": 1,
|
||
"errors": [
|
||
{ "row": 17, "field": "phone", "value": "1380013", "reason": "手机号格式不正确" },
|
||
{ "row": 33, "field": "username", "value": "lisi", "reason": "账号已存在" }
|
||
],
|
||
"initialPasswords": [
|
||
{ "username": "zhangsan", "password": "Xy7$mQ2p" }
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
> ⚠️ **导入类接口一律返回「体检报告」结构**(`total/success/failed/headerRow/errors[]`)。`headerRow` 必须回传——实测两份真实文件的表头分别在**第 1 行和第 6 行**,界面要把它可视化出来让用户确认。
|
||
|
||
### 5.9 其余账号接口
|
||
|
||
| 接口 | 关键字段 | 备注 |
|
||
|---|---|---|
|
||
| `PUT /users/{id}` | 同 5.7,`username` 不可改 | — |
|
||
| `DELETE /users/{id}` | — | **软删**(`deleted=1`);有在办任务时返回 `40014` 并提示先移交 |
|
||
| `POST /users/{id}/reset-password` | `{ "mode": "GENERATE" }` 或 `{ "newPassword": "..." }` | 返回 `{ newPassword }` |
|
||
| `PUT /auth/password` | `{ oldPassword, newPassword }` | 强制改密场景 `oldPassword` 可空 |
|
||
| `POST /auth/logout` | — | 服务端把 token 加入黑名单,TTL = 剩余有效期 |
|
||
| `POST /mp/bind-phone` | `{ code }`(`getPhoneNumber` 返回) | 写入 `phone_hash`,用于门禁二次校验 |
|
||
|
||
---
|
||
|
||
### 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`
|
||
|
||
**Query**:`withCount`(bool,是否带学生数)、`rootId`
|
||
|
||
**响应**
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"data": [
|
||
{
|
||
"id": 2000, "orgName": "江西机电职业技术学院", "orgType": 1, "orgCode": "JXJD",
|
||
"leaderUserId": null, "sortNo": 1, "status": 1, "studentCount": 1240,
|
||
"children": [
|
||
{ "id": 2001, "orgName": "24中技数控1班", "orgType": 3, "orgCode": "24ZK1",
|
||
"leaderUserId": 10087, "leaderName": "李老师", "sortNo": 1, "status": 1,
|
||
"studentCount": 28, "children": [] }
|
||
]
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
| `orgType` | 含义 |
|
||
|:--:|---|
|
||
| `1` | 学校 |
|
||
| `2` | 院系 / 专业 |
|
||
| `3` | 班级 |
|
||
|
||
### 6.2 `POST /api/v1/orgs` / `PUT /api/v1/orgs/{id}`
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|---|---|---|---|
|
||
| `parentId` | int64 | — | 顶级为空 |
|
||
| `orgName` | string | ✅ | — |
|
||
| `orgType` | int | ✅ | `1`/`2`/`3` |
|
||
| `orgCode` | string | ✅ | 租户内唯一 |
|
||
| `leaderUserId` | int64 | — | 负责人 |
|
||
| `sortNo` | int | — | 默认 0 |
|
||
|
||
> `ancestor_path` 由服务端维护(如 `/2000/2010/2001/`),用于**按院系统计**,客户端不要传。
|
||
|
||
### 6.3 `DELETE /api/v1/orgs/{id}`
|
||
|
||
**约束**:有下级组织、有名册项、有进行中任务时**拒绝**,返回 `40015` 并给出占用清单。
|
||
|
||
### 6.4 `POST /api/v1/orgs/import`
|
||
|
||
**Content-Type**:`multipart/form-data` **字段**:`file`
|
||
|
||
Excel 列:`层级` `组织名称` `组织编码` `上级编码` `负责人手机号`。返回同 5.8 的体检报告结构。
|
||
|
||
### 6.5 `GET /api/v1/tenants`
|
||
|
||
**权限**:运营 **Query**:`keyword`、`status`、`expireBefore`、分页
|
||
|
||
`expireBefore`(`yyyy-MM-dd`)**含当天** —— `expire_time` 存的是当天 `23:59:59`,
|
||
所以比较要补到当天末尾,否则"今天到期"的租户查不出来。格式不对返回 `40001`
|
||
(**不静默忽略筛选**:这个值只由界面按当前日期算出来,非法值只可能来自手搓请求)。
|
||
|
||
**响应**:`{ list: TenantVO[], total }`
|
||
|
||
⚠️ 列表里的 `contactPhone` **恒为 `null`** —— 联系电话以密文入库,列表接口不能成为
|
||
批量拉取联系方式的通道。脱敏值只有 6.9 给。
|
||
`packageName` 与内嵌 `usage{}`(§3.12 的简表里画了)**不实现**:200 行内嵌用量会变成 N+1 统计。
|
||
|
||
### 6.6 `POST /api/v1/tenants`
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|---|---|---|---|
|
||
| `tenantName` | string | ✅ | — |
|
||
| `tenantCode` | string | ✅ | 全局唯一,如 `jxjd`;**入库时小写化** |
|
||
| `tenantType` | int | ✅ | `1` 学校 / `2` 认定机构 / `3` 报名点 / `4` 企业 |
|
||
| `contactName` / `contactPhone` | string | ✅ | 联系人;电话**加密入库** |
|
||
| `packageId` | int64 | | 套餐(可选,无外键) |
|
||
| `expireTime` | string | ✅ | 到期时间;只给日期时补成当天 `23:59:59` |
|
||
| `adminUsername` / `adminRealName` / `adminPhone` | string | ✅ | **同时创建首个管理员** |
|
||
|
||
**响应**:`{ "id": 1002, "adminInitialPassword": "Kp9#vR4t", "miniProgramScene": "t1002" }`
|
||
|
||
**副作用**:切到新租户的作用域发放 6 个内置角色、建管理员并绑定 `SCHOOL_ADMIN`;
|
||
写一条操作日志(动作 `CREATE_TENANT`,`target_type=TENANT`)。
|
||
|
||
### 6.7 `GET /api/v1/tenants/{id}/usage`
|
||
|
||
**Query**:`period`(`2026-09` 或 `2026Q3`;不传 = 当月)
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"data": {
|
||
"tenantId": 1, "tenantName": "示范职业学校", "period": "2026-09",
|
||
"studentCount": 1240, "storageMb": 8420, "ocrCount": 0, "smsCount": 0,
|
||
"quota": { "studentLimit": 2000, "storageLimitMb": 20480, "ocrLimit": 10000, "smsLimit": 2000 },
|
||
"alerts": [ { "type": "STORAGE", "level": 1, "ratio": "41.1%", "used": 8420, "limit": 20480 } ],
|
||
"notes": [
|
||
"ocrCount 恒为 0:M4 的识别通道还没接(等 OCR 选型),不是「没用量」",
|
||
"smsCount 恒为 0:短信通道还没有商务结论(降级链只实现到站内信)"
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
⚠️ 三条口径(都不是"实现细节",是**看数的人必须知道的**):
|
||
|
||
- `studentCount` / `storageMb` 是**实时**值(它们决定"还能不能再收人"),
|
||
`period` **只影响计费项**(`ocrCount` / `smsCount`)—— 界面必须写明,
|
||
否则会以为"2026Q3 的学生数"是历史值;
|
||
- `ocrCount` / `smsCount` 一期**恒为 0 并带 `notes` 说明**:
|
||
两条通道都还没接,编一个数字会让运营拿着假数据去谈续费;
|
||
- 统计**只算这一个租户**。这条不是废话:调用方一定是平台运营(`tenant:view` 是平台专属码),
|
||
而平台身份的 `tenantId` 是 `0`,租户拦截器对它**整张表关掉过滤** ——
|
||
实现里必须显式切到目标租户的上下文再统计,否则返回的是**全平台**的数
|
||
(库里只有一所学校有数据时,它与正确值完全相等,看不出来)。
|
||
|
||
### 6.8 `PUT /api/v1/tenants/{id}`
|
||
|
||
**权限**:运营
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|---|---|---|---|
|
||
| `tenantName` | string | | 不能是空白串 |
|
||
| `tenantType` | int | | `1` 学校 / `2` 认定机构 / `3` 报名点 / `4` 企业 |
|
||
| `contactName` | string | | — |
|
||
| `contactPhone` | string | | **留空 = 不修改**;非空才写,且写入的是**密文** |
|
||
| `packageId` | int64 | | — |
|
||
| `expireTime` | string | | `2027-12-31` 或 `2027-12-31T23:59:59` |
|
||
| `status` | int | | `0` 停用 / `1` 正常 / `2` 试用 |
|
||
|
||
**不传的字段不修改**(不是"清空")。**`tenantCode` 不可修改**:它出现在小程序扫码
|
||
落地页、域名解析与对接文档里,改了会让已经发出去的东西失效,而我们通知不到客户那边。
|
||
|
||
**留痕**(本接口必须留痕,因为它是这套系统里影响面最大的一类动作):
|
||
|
||
| 变更 | 动作码 |
|
||
|---|---|
|
||
| `status` → `0` | `DISABLE_TENANT`(该校所有学生与老师**立刻登不进来**,单独一个码) |
|
||
| `status` → `1` | `ENABLE_TENANT` |
|
||
| `status` → `2` | `UPDATE_TENANT` |
|
||
| 其它字段有变化 | `UPDATE_TENANT`(`before` / `after` 是 `k=v; k=v` 摘要) |
|
||
|
||
⚠️ 这些日志行的 `tenant_id` 是 **`0`**(写日志用的是**操作者**的上下文,平台运营的租户号是 0),
|
||
不是被操作的那所学校。要查"这所学校被谁停过",用 `target_type=TENANT` + `target_id=<租户 id>`,
|
||
**不能用 `tenantId` 筛**。
|
||
|
||
**响应**:`TenantVO`(同 6.9)
|
||
|
||
### 6.9 `GET /api/v1/tenants/{id}`
|
||
|
||
**权限**:运营 **响应**:`TenantVO`,其中 `contactPhone` 是**脱敏值**(形如 `139****1111`)
|
||
|
||
这是**唯一**能拿到联系电话的接口。为什么不给明文:这是全系统唯一一处"加密存储的联系方式",
|
||
若在详情里可读且不留痕,就与「看证件号明文必须留痕」(`VIEW_IDCARD_FULL`)这条既有规矩
|
||
自相矛盾;而脱敏值足够确认"档里有号",要改直接填新号(6.8 留空 = 不修改)。
|
||
|
||
⚠️ 库里存在**历史明文行**(早期版本的写入路径没有加密)。服务端对解密失败
|
||
按明文处理并记一条 WARN —— 否则一所学校的详情页会**永远打不开**,
|
||
而那种故障看起来像"系统坏了",不像"数据里有一行是旧的"。
|
||
|
||
---
|
||
|
||
## §7 任务与配置
|
||
|
||
### 7.1 `GET /api/v1/tasks`
|
||
|
||
**Query**:`keyword`、`status`、`collectMode`、`batchNo`、`orgId`、分页
|
||
|
||
**权限过滤**:管理员看全租户;班主任**只看自己带的班级的任务**(由 `collect_task_org.head_user_id` 决定)。
|
||
|
||
**响应**:`{ list: TaskVO[], total }`(含 `stat` 快照)
|
||
|
||
### 7.2 `POST /api/v1/tasks`
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|---|---|---|---|
|
||
| `taskName` | string | ✅ | — |
|
||
| `collectMode` | int | ✅ | `1` 学校 / `2` 社会(**决定审核流,发布后不可改**) |
|
||
| `batchNo` | string | ✅ | 如 `202609` |
|
||
| `majorName` | string | ✅ | — |
|
||
| `certType` | int | ✅ | 见 §2.3 |
|
||
| `startTime` / `endTime` | string | ✅ | — |
|
||
| `auditConfig` | object | — | 缺省按 `collectMode` 取默认值 |
|
||
| `namingTemplate` | string | — | 缺省 `{姓名}-{证件号}-{材料名}{后缀}` |
|
||
| `compressConfig` | object | — | 缺省见 3.11 |
|
||
| `formTemplateId` | int64 | — | 从表单模板库初始化 |
|
||
| `materialTemplateId` | int64 | — | 从材料模板初始化 |
|
||
|
||
**响应**:`{ "id": 2026090001, "taskCode": "2026090001", "status": 0 }`
|
||
|
||
> **社会收集的审核配置默认值**(该模式**本次不做**,仅保留数据结构):`needClassAudit: false`(跳过初审),由服务端强制设置,客户端传 true 会被忽略并返回 `warnings`。
|
||
> ⚠️ **一期只支持 `collectMode=1`(学校收集)**。传 `2` 会被拒绝并返回 `40044`。
|
||
|
||
### 7.3 `POST /api/v1/tasks/{id}/publish`
|
||
|
||
**前置校验**(FR-507)——**任一不通过则阻断发布**
|
||
|
||
| 检查 | 不通过时的响应 |
|
||
|---|---|
|
||
| 名册人数 > 0 | `40016` 名册为空 |
|
||
| 名册含「姓名」「身份证号」列 | `40017` |
|
||
| 至少 1 项必传材料 | `40018` |
|
||
| 已设置覆盖班级与班主任 | `40019` |
|
||
| **命名规则试跑通过** | `40020` 附试跑失败样例 |
|
||
|
||
**请求**:`{ "force": false, "confirmWarnings": [] }`
|
||
|
||
**响应**:`{ "status": 1, "publishedTime": "2026-09-28T10:00:00+08:00", "qrcodeUrl": "..." }`
|
||
|
||
> **`force` 只对 P1 级警告有效**(如"某班的班主任未指定");上述 5 项**全部为阻断级,`force` 无法绕过**。
|
||
|
||
### 7.4 ⭐ `POST /api/v1/tasks/{id}/pause` · `/resume`
|
||
|
||
**权限**:管理 **幂等**:是
|
||
|
||
**暂停语义**(与"截止"的区别):
|
||
|
||
| 操作 | 学生可提交 | 已提交的审核 | 恢复 | 典型场景 |
|
||
|---|---|---|---|---|
|
||
| **暂停** | ❌(提示"任务已暂停") | ✅ **继续可审** | ✅ 可恢复 | 名单要改、规则要调,先冻住入口 |
|
||
| **截止** | ❌ | ✅ 可审 | ❌ 不可逆 | 时间到了 |
|
||
| **归档** | ❌ | ❌ 只读 | ❌ 不可逆 | 结束,进历史 |
|
||
|
||
**请求**:`{ "reason": "名册需补充 3 人" }`(可选)
|
||
|
||
**响应**:`{ "status": 2, "pausedTime": "..." }`
|
||
|
||
**错误**:`40021` 仅已发布任务可暂停 / `40022` 仅已暂停任务可恢复
|
||
|
||
### 7.5 `POST /api/v1/tasks/{id}/close` · `/archive` · `/clone`
|
||
|
||
| 接口 | 请求 | 响应 | 约束 |
|
||
|---|---|---|---|
|
||
| `/close` | `{ "force": false }` | `{ status: 3 }` | 未提交者仍可被催报;`force` 可跳过"有未审单"警告 |
|
||
| `/archive` | `{ "archiveRemark": "..." }` | `{ status: 4 }` | **要求无待审单**,否则 `40023` |
|
||
| `/clone` | `{ "newTaskName": "...", "newBatchNo": "202703", "copyRoster": true, "copyFormSchema": true, "copyMaterials": true, "copyOrgs": true }` | `{ newTaskId, newTaskCode }` | 名册复制时**清空绑定与提交状态** |
|
||
|
||
### 7.6 `GET/PUT /api/v1/tasks/{id}/orgs`
|
||
|
||
**GET 响应**
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"data": [
|
||
{ "orgId": 2001, "orgName": "24中技数控1班", "headUserId": 10087, "headUserName": "李老师",
|
||
"deputyUserIds": [10088], "deputyUserNames": ["张老师"], "expectedCount": 28, "status": 1 }
|
||
]
|
||
}
|
||
```
|
||
|
||
**PUT 请求**:`{ "orgs": [ { "orgId": 2001, "headUserId": 10087, "deputyUserIds": [10088], "expectedCount": 28 } ] }`
|
||
|
||
> 一个班主任可带多个班;一个班可配**主/副**班主任(FR-204)。`expectedCount` 用于与名册实际人数比对,不一致时在任务详情告警。
|
||
|
||
### 7.7 ⭐ `GET /api/v1/tasks/{id}/plan-scope` 报考范围
|
||
|
||
**背景**:**不存在独立的"计划清单"文件**。报考范围由**名册聚合**得出。
|
||
|
||
**响应**
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"data": {
|
||
"source": "ROSTER_AGGREGATED",
|
||
"updatedAt": "2026-09-28T10:00:00+08:00",
|
||
"rosterCount": 28,
|
||
"groups": [
|
||
{
|
||
"occupation": "车工", "worktype": "数控车工", "jobLevel": "四级",
|
||
"certType": 1, "count": 26,
|
||
"rosterIds": [9001, 9002, "..."],
|
||
"status": "CONFIRMED"
|
||
},
|
||
{
|
||
"occupation": "铣工", "worktype": "数控铣工", "jobLevel": "四级",
|
||
"certType": 1, "count": 1, "rosterIds": [9027], "status": "CONFIRMED"
|
||
},
|
||
{
|
||
"occupation": null, "worktype": null, "jobLevel": null,
|
||
"certType": 3, "count": 1, "rosterIds": [9028],
|
||
"status": "NEED_REVIEW",
|
||
"issue": "专项职业能力无工种/无等级,已自动识别"
|
||
}
|
||
],
|
||
"healthCheck": {
|
||
"passed": false,
|
||
"issues": [
|
||
{ "level": 2, "code": "SCOPE_LEVEL_MIXED",
|
||
"title": "同一职业出现多个等级",
|
||
"detail": "「车工」下同时存在 四级(26人) 与 三级(1人),请确认是否应为两个任务",
|
||
"rosterIds": [9012] },
|
||
{ "level": 1, "code": "SCOPE_UNFILLED",
|
||
"title": "1 人未填报考项",
|
||
"detail": "序号 28 曾某某 的报考职业为空,将无法导出",
|
||
"rosterIds": [9028] }
|
||
]
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**`groups[].status`**
|
||
|
||
| 值 | 含义 |
|
||
|---|---|
|
||
| `CONFIRMED` | 已确认 |
|
||
| `NEED_REVIEW` | 需人工确认(有异常) |
|
||
| `EXCLUDED` | 已排除(不参与本次认定) |
|
||
|
||
**`healthCheck.issues[].code` 清单**
|
||
|
||
| code | 级别 | 含义 |
|
||
|---|:--:|---|
|
||
| `SCOPE_LEVEL_MIXED` | 2 | 同职业多等级 |
|
||
| `SCOPE_UNFILLED` | 1 | 报考项为空 |
|
||
| `SCOPE_FREE_TEXT` | 1 | 报考职业/工种为自由填写,与字典不完全一致(**Excel 模板本来就没有下拉**) |
|
||
| `SCOPE_TYPEO_MISMATCH` | 2 | `certType` 与是否有工种/等级矛盾 |
|
||
| `SCOPE_OUT_OF_TASK` | 3 | 出现任务专业范围外的职业 |
|
||
|
||
### 7.8 ⭐ `PUT /api/v1/tasks/{id}/plan-scope` 报考范围确认
|
||
|
||
**请求**
|
||
|
||
```json
|
||
{
|
||
"groups": [
|
||
{ "occupation": "车工", "worktype": "数控车工", "jobLevel": "四级",
|
||
"certType": 1, "rosterIds": [9001, 9002], "status": "CONFIRMED" },
|
||
{ "occupation": null, "worktype": null, "jobLevel": null,
|
||
"certType": 3, "rosterIds": [9028], "status": "EXCLUDED" }
|
||
],
|
||
"ignoredIssueCodes": ["SCOPE_FREE_TEXT"],
|
||
"remark": "9028 待补报考信息,本轮先排除"
|
||
}
|
||
```
|
||
|
||
**响应**:`{ "updatedAt": "...", "healthCheck": { "passed": true, "issues": [] } }`
|
||
|
||
**校验**:`rosterIds` 的并集必须**恰好覆盖**名册全员,否则 `40024` 并返回差集。
|
||
|
||
> **为什么强制全覆盖**:遗漏任何一人,导出包就会缺人;而漏人在人工核对时极难发现。**用接口约束代替人眼核对**。
|
||
|
||
### 7.9 `GET/PUT /api/v1/tasks/{id}/form-schema`
|
||
|
||
**GET 响应**:`FormSchemaVO`(见 3.5)
|
||
|
||
**PUT 请求**
|
||
|
||
```json
|
||
{
|
||
"version": 3,
|
||
"baseVersion": 2,
|
||
"fields": [
|
||
{ "key": "real_name", "label": "姓名", "type": "text", "builtin": true, "locked": true, "required": true, "sortNo": 1 },
|
||
{ "key": "education", "label": "学历", "type": "select", "builtin": true, "locked": false,
|
||
"required": true, "dictCode": "EDU_LEVEL", "sortNo": 4,
|
||
"tip": "老师已填,请核对" },
|
||
{ "key": "is_retake", "label": "是否补考", "type": "bool", "builtin": true, "sortNo": 7 },
|
||
{ "key": "retake_theory", "label": "补考理论", "type": "text", "builtin": true, "sortNo": 8,
|
||
"visibleWhen": { "field": "is_retake", "op": "eq", "value": true } }
|
||
],
|
||
"extras": [
|
||
{ "key": "extra_skill_cert", "label": "其它技能证书编号", "type": "text", "required": false, "sortNo": 100 }
|
||
]
|
||
}
|
||
```
|
||
|
||
**版本冲突**:`baseVersion` 与当前版本不符 → `40025` 并返回最新 schema,前端做差异合并。
|
||
|
||
**内置字段不可删**:删除任一 `builtin: true` 的字段 → `40026`。
|
||
|
||
> **`extras` 存入 `extras_json`,不参与统计**。凡需要统计的字段必须落在内置列上——这是架构约束,在设计器里以「扩展字段不参与核心统计」明示。
|
||
|
||
### 7.10 `GET/POST /api/v1/tasks/{id}/materials` · `PUT/DELETE /api/v1/materials/{id}`
|
||
|
||
**POST/PUT 请求**:`MaterialDefVO`(见 3.4)
|
||
|
||
**删除约束**:已有文件上传的材料项**不允许删**(`40027`),只能设 `visibleWhen` 隐藏。
|
||
|
||
**材料项模板**(`materialTemplateId` 可引用)
|
||
|
||
| 模板 | 材料项 |
|
||
|---|---|
|
||
| `TPL_SCHOOL_ROLL` | 一寸照片 + 身份证 + 学籍表 + 工作证明 |
|
||
|
||
> ⚠️ **需求收敛(2026-10)后模板只剩这一个**:原 `TPL_SOCIAL`(社会收集)与
|
||
> `TPL_ZHUANXIANG`(专项职业能力)随"二期取消"一并删除;传这两个码会得到**不认识的模板**,
|
||
> 不再有对应的初始化行为。
|
||
|
||
### 7.11 `GET /api/v1/tasks/{id}/statistics`
|
||
|
||
**响应**:`TaskStatVO`(见 3.3)+ 分班明细
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"data": {
|
||
"rosterTotal": 28, "submittedCount": 23, "approvedCount": 15,
|
||
"pendingCount": 4, "rejectedCount": 1, "unsubmittedCount": 5,
|
||
"submitRate": "82.1%", "passRate": "53.6%",
|
||
"abnormalCount": 4, "avgAuditHours": 6.4,
|
||
"byOrg": [ { "orgId": 2001, "orgName": "24中技数控1班", "rosterTotal": 28,
|
||
"submittedCount": 23, "approvedCount": 15, "submitRate": "82.1%" } ],
|
||
"byOccupation": [ { "occupation": "车工", "worktype": "数控车工", "jobLevel": "四级",
|
||
"count": 26, "approvedCount": 14 } ]
|
||
}
|
||
}
|
||
```
|
||
|
||
> **`byOccupation` 走的是正式列**(`collect_submission.occupation`),实时且走索引——这正是当初坚持"18 个正式列 + extras_json"的收益。
|
||
|
||
### 7.12 `GET /api/v1/tasks/{id}/qrcode`
|
||
|
||
**Query**:`type`(`MINI` 小程序码 / `URL` 短链)、`orgId`(可选,带班级参数)
|
||
|
||
**响应**:`{ "qrcodeUrl": "https://oss.../qr.png", "shortUrl": "https://t.xx/ab3K9", "scene": "t1001-t202609-o2001", "expireTime": null }`
|
||
|
||
> 短链永久有效,**任务截止后扫码进入提示"已截止"**,不做失效。
|
||
|
||
### 7.13 `GET /api/v1/rule-templates` · `POST /api/v1/rule-templates/{id}/apply`
|
||
|
||
| 接口 | 关键字段 | 说明 |
|
||
|---|---|---|
|
||
| GET | 返回 `{ id, templateName, certType, isPublic, useCount }` 列表 | 含公共模板与租户自建 |
|
||
| apply | 请求 `{ "targetTaskId": 2026090001, "parts": ["FORM","MATERIALS","NAMING"] }` | **可选择只应用其中一部分** |
|
||
|
||
---
|
||
|
||
## §8 名册
|
||
|
||
### 8.1 `POST /api/v1/tasks/{id}/roster/import` ⭐ 核心
|
||
|
||
**Content-Type**:`multipart/form-data`
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|---|---|---|---|
|
||
| `file` | file | ✅ | Excel(≤10MB,≤5000 行) |
|
||
| `headerRow` | int | — | 手动指定表头行;**不传则自动探测** |
|
||
| `sheetName` | string | — | 手动指定工作表;不传则取第一个**有数据的**表 |
|
||
| `mode` | string | — | `APPEND`(默认,按证件号去重合并)/ `REPLACE` |
|
||
| `dryRun` | bool | — | 只体检不落库 |
|
||
|
||
**关键设计**:**表头行与工作表名都不固定**(实测一份在第 1 行、一份在第 6 行),因此:
|
||
|
||
```
|
||
表头探测算法:
|
||
1. 若传 headerRow → 用它
|
||
2. 否则扫前 10 行,选「命中已知列名 ≥ 3 个」的行作为表头
|
||
已知列名 = 官方模板 21 列 + 常见别名(姓名/名字/学生姓名…)
|
||
3. 若有多个候选 → 选命中最多者;并列 → 取最靠上者,并在响应中回传 candidates 让用户确认
|
||
4. 全部不命中 → 40028,返回前 10 行预览,请用户手动指定
|
||
```
|
||
|
||
**响应(导入体检报告)**
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"data": {
|
||
"total": 28, "success": 28, "failed": 0,
|
||
"headerRow": 6,
|
||
"sheetName": "24中技数控1班",
|
||
"headerCandidates": [ { "row": 6, "score": 18 }, { "row": 1, "score": 2 } ],
|
||
"headerConfirmed": true,
|
||
"columns": [
|
||
{ "col": "A", "title": "序号", "mappedTo": null, "matched": false },
|
||
{ "col": "B", "title": "姓名", "mappedTo": "real_name", "matched": true },
|
||
{ "col": "C", "title": "身份证号", "mappedTo": "id_card_no", "matched": true },
|
||
{ "col": "D", "title": "报考职业", "mappedTo": "occupation", "matched": true },
|
||
{ "col": "U", "title": "备注", "mappedTo": "ext_json.remark", "matched": false }
|
||
],
|
||
"errors": [
|
||
{ "row": 12, "col": "C", "field": "id_card_no", "value": "3601052009081712**",
|
||
"reason": "身份证号校验位不通过", "code": "IDCARD_CHECKSUM" },
|
||
{ "row": 19, "col": "C", "field": "id_card_no", "value": "36102920090729**10",
|
||
"reason": "证件号与第 8 行重复", "code": "DUP_IDCARD" }
|
||
],
|
||
"warnings": [
|
||
{ "row": 28, "col": "D", "field": "occupation",
|
||
"reason": "报考职业为空(专项职业能力可留空)", "code": "EMPTY_OCCUPATION" }
|
||
],
|
||
"summary": { "uniquePersons": 28, "dupRows": 2, "idcardInvalid": 1, "nameSuspect": 0, "phoneInvalid": 3 }
|
||
}
|
||
}
|
||
```
|
||
|
||
**体检项清单**
|
||
|
||
| code | 说明 | 处理 |
|
||
|---|---|---|
|
||
| `IDCARD_CHECKSUM` | 校验位不通过(ISO 7064) | **阻断该行** |
|
||
| `DUP_IDCARD` | 证件号重复 | **阻断该行** |
|
||
| `IDCARD_FORMAT` | 格式非法(长度/字符) | 阻断该行 |
|
||
| `EMPTY_NAME` | 姓名为空 | 阻断该行 |
|
||
| `EMPTY_IDCARD` | 证件号为空 | 阻断该行 |
|
||
| `EMPTY_OCCUPATION` | 报考职业为空 | 警告 |
|
||
| `NAME_SUSPECT` | 姓名含罕见字/形近字 | 警告 |
|
||
| `PHONE_INVALID` | 手机号格式异常 | 警告(不阻断,可后续补) |
|
||
|
||
> **`dryRun` 模式**:先体检、给用户确认表头与错行,确认无误后再正式导入。**推荐前端默认走 dryRun**,避免"导进去才发现表头选错"。
|
||
|
||
### 8.2 `GET /api/v1/tasks/{id}/roster`
|
||
|
||
**Query**:`keyword`、`orgId`、`submitStatus`、`bindStatus`、`hasIssue`、分页
|
||
|
||
**响应**:`{ list: RosterVO[], total }`
|
||
|
||
### 8.3 `PUT /api/v1/roster/{rid}`
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|---|---|---|
|
||
| `realName` | string | 改名会**重置绑定**(需二次确认 `confirmRebind: true`) |
|
||
| `idCardNo` | string | **改证件号必留痕**(FR-506) |
|
||
| `phone` | string | — |
|
||
| `extJson` | object | 报考三项等 |
|
||
|
||
**留痕**:写入 `collect_roster` 变更前值快照 + 操作日志,`GET /logs/operations` 可查。
|
||
|
||
### 8.4 `DELETE /api/v1/roster/{rid}`
|
||
|
||
**约束**:已绑定或已提交的名册项**不允许删除**(`40029`),只能标记为"不参与本次认定"。
|
||
|
||
### 8.5 `GET /api/v1/tasks/{id}/roster/template`
|
||
|
||
**Query**:`format`(`xlsx`)、`withSample`(bool,是否带示例行)
|
||
|
||
**响应**:文件流。表头**固定在第 1 行**,必填列标红,附 9 条填表规则说明页。
|
||
|
||
### 8.6 `POST /api/v1/roster/{rid}/notify` · `POST /api/v1/tasks/{id}/roster/notify-unsubmitted`
|
||
|
||
| 接口 | 请求 | 说明 |
|
||
|---|---|---|
|
||
| 定向催报 | `{ "channel": 1, "templateCode": "NOTIFY_UNSUBMITTED", "extraText": "..." }` | 单人或多人(`rosterIds[]`) |
|
||
| 一键催报 | `{ "orgIds": [2001], "channel": 1, "excludeRecentHours": 24 }` | **仅催未提交者**;`excludeRecentHours` 防重复骚扰 |
|
||
|
||
**响应**:`{ "sent": 5, "failed": 0, "skipped": 1, "details": [ { "rosterId": 9012, "skipped": true, "reason": "24 小时内已催过" } ] }`
|
||
|
||
**限流**:10 次/分钟/任务。
|
||
|
||
---
|
||
|
||
> ⚠️ **本节已撤销(需求收敛 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.1 追加导入,`FR-508`)。**系统内不展示任何联系方式。**
|
||
>
|
||
> **治理提示**:B1 是客户确认函里**唯一的功能增量**,本次属**需求回退**,建议补一张变更确认单。
|
||
|
||
## §9 填报端 · 小程序
|
||
|
||
### 9.1 `GET /api/v1/mp/tasks`
|
||
|
||
**权限**:学生 **说明**:只返回**该学生被绑定/在名册中**的任务。
|
||
|
||
**响应**
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"data": [
|
||
{
|
||
"taskId": 2026090001, "taskName": "2026年9月职业技能等级认定",
|
||
"orgName": "24中技数控1班", "endTime": "2026-10-10T23:59:59+08:00",
|
||
"status": 1, "submissionStatus": 0,
|
||
"progress": { "total": 4, "done": 2, "percent": "50%" },
|
||
"issueCount": { "red": 1, "yellow": 2 },
|
||
"hasDraft": true,
|
||
"materials": [
|
||
{ "materialCode": "PHOTO1", "materialName": "一寸照片", "required": true,
|
||
"done": true, "visible": true },
|
||
{ "materialCode": "SCHOOL_ROLL", "materialName": "学籍表", "required": true,
|
||
"done": false, "visible": true }
|
||
]
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
> **`materials[].visible`**:条件显示的材料(如"是否在校生=否"时学籍表隐藏、学信网备案表显示)由服务端算好,**前端不做条件判断**——避免两端逻辑不一致。
|
||
|
||
### 9.2 `GET /api/v1/mp/tasks/{id}/form`
|
||
|
||
**响应**
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"data": {
|
||
"taskId": 2026090001, "formVersion": 3,
|
||
"sections": [
|
||
{
|
||
"title": "基本信息", "fields": [
|
||
{ "key": "real_name", "label": "姓名", "type": "text", "required": true,
|
||
"locked": true, "value": "曾伊豪", "prefilled": true },
|
||
{ "key": "education", "label": "学历", "type": "select", "required": true,
|
||
"locked": false, "value": "中等职业技术学校", "prefilled": true,
|
||
"tip": "老师已填,请核对",
|
||
"options": [ { "value": "中等职业技术学校", "label": "中等职业技术学校" } ] }
|
||
]
|
||
}
|
||
],
|
||
"materials": [ { "materialCode": "PHOTO1", "...": "见 3.4 MaterialDefVO" } ],
|
||
"submitPolicy": {
|
||
"blockOnRed": true,
|
||
"allowForceOnYellow": true,
|
||
"deadlineTip": "请在 10 月 10 日前提交"
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**字段三态**
|
||
|
||
| `locked` | `prefilled` | 学生端表现 |
|
||
|:--:|:--:|---|
|
||
| `true` | — | 只读,样式灰底,右侧提示"如有误请联系老师" |
|
||
| `false` | `true` | 可编辑,标注"**老师已填,请核对**",修改后留痕 |
|
||
| `false` | `false` | 可编辑,正常样式 |
|
||
|
||
### 9.3 `POST /api/v1/mp/submissions`
|
||
|
||
**幂等**:是 **说明**:取草稿(存在则返回既有,不存在则创建)
|
||
|
||
| 字段 | 类型 | 必填 |
|
||
|---|---|---|
|
||
| `taskId` | int64 | ✅ |
|
||
|
||
**响应**:`{ "id": 5001, "formVersion": 3, "created": true }`
|
||
|
||
### 9.4 `PUT /api/v1/mp/submissions/{id}` 保存草稿
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|---|---|---|
|
||
| `formData` | object | 表单值(**只传变化的字段**,服务端做合并) |
|
||
| `extrasJson` | object | 扩展字段 |
|
||
| `clientTime` | string | 客户端时间,用于冲突提示 |
|
||
|
||
**响应**
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"data": {
|
||
"savedAt": "2026-09-28T14:30:00+08:00",
|
||
"changedFields": ["phone"],
|
||
"conflict": null
|
||
}
|
||
}
|
||
```
|
||
|
||
**并发冲突**:同一份草稿在多端同时编辑,后写者返回 `conflict`(含对方已改字段),由前端提示。
|
||
|
||
**自动保存**:前端每 15 秒或失焦时调一次;服务端**不限流**(草稿合并是轻量操作)。
|
||
|
||
### 9.5 ⭐ `POST /api/v1/mp/files/upload-token` 上传前置检查(预检 / 秒传 / 压缩建议)
|
||
|
||
> ✅ **定位已确认(2026-09-30 客户确认,原「OSS 直传凭证」改为此口径)**:
|
||
> 一期实现是**服务端一步上传**(`POST /api/v1/mp/files/upload`,multipart,
|
||
> 收上来即压缩 + 按模板改名,见 §17.2 D-3),**不走对象存储直传** ——
|
||
> 直传要把上传凭证交给前端、且七牛直传必须配回调才能让服务端知道"传完了"。
|
||
> 本接口因此**不是安全边界**(客户端可以跳过它直接上传,服务端上传本身做全部校验),
|
||
> 它的价值是**在上传一个字节之前**完成三件事:
|
||
> ① 预检(格式 / 体积 / 份数,传 8MB 才发现格式不对就晚了);
|
||
> ② **秒传**(同 `fileHash` 已存在 → `existed: true`,网络断开重试时不用重传);
|
||
> ③ 端侧压缩建议(能大幅降低上传流量)。
|
||
> 大文件 / 弱网场景要恢复直传时再单独立项(含回调闭环),见《08》**F-3 / F-71 / F-72**。
|
||
|
||
**幂等**:是
|
||
|
||
**请求**
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|---|---|---|---|
|
||
| `taskId` | int64 | ✅ | — |
|
||
| `materialCode` | string | ✅ | 目标槽位 |
|
||
| `fileName` | string | ✅ | 原始文件名(**可能是乱的,正常**) |
|
||
| `fileSize` | int64 | ✅ | 字节 |
|
||
| `contentType` | string | ✅ | MIME |
|
||
| `source` | int | — | `1` 本人上传 / `2` 班主任代传 / `3` 管理员代传 / `4` 整理器导入(**模块已取消,仅历史数据**;口径见 §3.6 注) |
|
||
| `fileHash` | string | — | 客户端 SHA-256(**用于秒传与去重**) |
|
||
|
||
**响应**
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"data": {
|
||
"fileId": 987654321,
|
||
"direct": false,
|
||
"uploadUrl": "/api/v1/mp/files/upload",
|
||
"expireSeconds": 900,
|
||
"existed": false,
|
||
"clientCompressHint": {
|
||
"enabled": true, "maxLongSide": 1600, "quality": 80, "targetSizeKb": 1024
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
| 字段 | 说明 |
|
||
|---|---|
|
||
| `direct` | 恒为 `false`(一期不做对象存储直传)。`uploadUrl` 就是本项目自己的接收地址,学生端 `wx.uploadFile` 直接 POST 它,与 H5 路径完全一样 |
|
||
| `existed` | `true` = **秒传命中**(同 hash 已存在),前端跳过上传直接进入处理 |
|
||
| `clientCompressHint` | **端侧预压缩建议**。这是第一道防线,能大幅降低上传流量与处理时长;**服务端仍会按规则重压**,端侧压缩只是加速 |
|
||
|
||
> **端侧压缩与「一次编码到位」的关系**:实测 300dpi/q85 即可达标,**端侧压缩只是减少上传量的优化,不是质量决定环节**。服务端始终以原图(或端侧压缩后的图)为输入重新规范,保证输出一致。
|
||
|
||
### 9.6 ⭐ `POST /api/v1/mp/files/{fileId}/process` 文件规范化(有原始件时的重新处理)
|
||
|
||
> ✅ **语义已确认(2026-09-30,原「命名+压缩+识别+校验总入口」改为此口径)**:
|
||
> 一期上传是**一步**的 —— `POST /mp/files/upload` 收上来**当场**完成压缩、按模板改名、校验、
|
||
> 记录问题(含 VAL-06 自动修复),默认**只存规范件**(`fill.storage.keep-raw=false`)。
|
||
> 所以本接口**只在「保留了原始件」时有用**:对原始件重新跑一遍规范化
|
||
> (典型场景:改了材料规则后重压)。默认配置下多数文件没有原始件,调用会明确说明这一点,
|
||
> 而不是假装处理了一遍。识别 / OCR 的四层递进**缓做**(等 OCR 供应商选型,见 §17.2 D-12 与《01》D1)——**不再作为二期功能设计**。
|
||
> 下面的响应结构保留作为「规范化产物」的字段参考(上传接口的产物与之同构)。
|
||
|
||
**说明**:对**有原始件**的文件重新执行规范化(压缩 + 改名 + 校验)。
|
||
|
||
**请求**
|
||
|
||
| 字段 | 类型 | 默认 | 说明 |
|
||
|---|---|---|---|
|
||
| `async` | bool | `false` | 大文件建议异步 |
|
||
| `runOcr` | bool | `true` | 是否跑 OCR |
|
||
| `dryRun` | bool | `false` | 只试跑不落库 |
|
||
| `confirmMove` | bool | `false` | 识别到槽位不符时是否执行移动 |
|
||
|
||
**响应(同步模式)**
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"data": {
|
||
"fileId": 987654321,
|
||
"originalName": "曾伊豪-361029200907295110身份证.jpg.jpg",
|
||
"standardName": "曾伊豪-361029200907295110-身份证.jpg",
|
||
"rawSizeKb": 17541, "finalSizeKb": 512,
|
||
"compressRatio": "97.1%",
|
||
"width": 1600, "height": 1200, "dpi": 300, "quality": 74,
|
||
"compressProfile": "P2",
|
||
"forced": false,
|
||
"steps": [
|
||
{ "step": "NAME_NORMALIZE", "before": "曾伊豪-361029200907295110身份证.jpg.jpg",
|
||
"after": "曾伊豪-361029200907295110-身份证.jpg", "success": true, "costMs": 12 },
|
||
{ "step": "FORMAT_CONVERT", "before": "jpg.jpg", "after": "jpg", "success": true, "costMs": 3 },
|
||
{ "step": "RESIZE", "before": "1600x1200", "after": "1600x1200", "success": true, "costMs": 210 },
|
||
{ "step": "ENCODE", "before": "17541KB", "after": "512KB", "success": true, "costMs": 840 }
|
||
],
|
||
"issues": [
|
||
{ "ruleCode": "VAL-06", "severity": 1, "title": "格式已转换",
|
||
"detail": "jpg.jpg → jpg", "autoFixed": true },
|
||
{ "ruleCode": "VAL-02", "severity": 3, "title": "身份证号与名册不一致",
|
||
"expectedVal": "3610292009******10", "actualVal": "3610292009******10",
|
||
"detail": "OCR 识别值与名册一致,文件名号码有误,已按名册修正", "autoFixed": true }
|
||
],
|
||
"ocr": { "status": 1, "confidence": 0.973, "fieldsRedacted": { "name": "曾*豪" } },
|
||
"detect": {
|
||
"materialCode": "IDCARD", "confidence": 0.98, "source": 1,
|
||
"conflict": false, "needManualPlace": false, "vlmCalled": false
|
||
},
|
||
"ownership": {
|
||
"verdict": "BELONGS_TO_OTHER",
|
||
"blockSubmit": true,
|
||
"otherRosterName": "李四",
|
||
"message": "这份材料可能不是你的(文件名对应的是「李四」)",
|
||
"action": "REUPLOAD",
|
||
"canForce": false
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**异步模式**:返回 `{ "taskToken": "..." }`,通过 `GET /api/v1/mp/files/{fileId}/process-status` 轮询(间隔 500ms,最多 30 次)或 SSE 订阅。
|
||
|
||
**`process-status` 响应**:`{ "status": 1, "progress": 60, "currentStep": "ENCODE", "result": { ...同同步响应... } }`
|
||
|
||
> ⚠️ **`issues[].severity = 1`(信息级)不阻断提交**。学生看到的应该是"已自动修正",不是"你有问题"——这个区分直接影响填报完成率。
|
||
|
||
### 9.7 `POST /api/v1/mp/files/batch-process`
|
||
|
||
**说明**:多材料一次提交(学生端"一次传 4 个文件"场景)。
|
||
|
||
**请求**:`{ "submissionId": 5001, "fileIds": [987654321, 987654322, 987654323], "async": true }`
|
||
|
||
**响应**
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"data": {
|
||
"batchToken": "b_7f2a...",
|
||
"total": 3,
|
||
"results": [
|
||
{ "fileId": 987654321, "status": 2, "materialCode": "PHOTO1", "detectMaterialCode": "PHOTO1", "conflict": false },
|
||
{ "fileId": 987654322, "status": 2, "materialCode": "IDCARD", "detectMaterialCode": "IDCARD", "conflict": false },
|
||
{ "fileId": 987654323, "status": 2, "materialCode": "SCHOOL_ROLL", "detectMaterialCode": "WORK_PROOF",
|
||
"conflict": true, "detectConfidence": 0.71, "needManualPlace": true }
|
||
],
|
||
"summary": { "ok": 2, "conflict": 1, "failed": 0 }
|
||
}
|
||
}
|
||
```
|
||
|
||
**冲突处理**(`conflict: true`):**只提示、不自动改位置**(FR-614a)。前端展示:
|
||
|
||
> 这张图看起来是「工作证明」,你放在了「学籍表」。要移过去吗?
|
||
> [移到工作证明] [就放这里]
|
||
|
||
### 9.8 `GET /api/v1/mp/files/{fileId}` · `DELETE /api/v1/mp/files/{fileId}`
|
||
|
||
| 接口 | Query / 说明 | 响应 |
|
||
|---|---|---|
|
||
| GET | `prefer=preview` / `prefer=original`;返回**签名 URL**(30 分钟) | `{ previewUrl, thumbUrl, standardName, finalSizeKb, width, height }` |
|
||
| DELETE | **软删**,保留版本历史 | `{ deleted: true, remainVersions: 1 }` |
|
||
|
||
**删除约束**:已提交并进入审核的文件删除需先**撤回**(`40030`)。
|
||
|
||
### 9.9 `POST /api/v1/mp/ocr/idcard`(一期缓做)
|
||
|
||
> ⚠️ **一期未实现(等 OCR 供应商选型,《01-客户与机构确认函》D1)**。
|
||
> 这是 119 个契约接口里**唯一**没有实现的一个;表单预填与校验位校验在学生端走人工填写。
|
||
> 选型要同时评估**数据出境合规**。选型定了之后本节按原设计实现,以下为设计参考。
|
||
|
||
**说明**:身份证 OCR,用于表单预填与校验。**这是唯一返回明文姓名的 OCR 场景**。
|
||
|
||
**请求**:`{ "fileId": 987654321, "side": "FRONT" }`
|
||
|
||
**响应**
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"data": {
|
||
"name": "曾伊豪",
|
||
"idCardNo": "361029200907295110",
|
||
"idCardMask": "3610292009******10",
|
||
"gender": "男", "birthDate": "2009-07-29", "address": "江西省...",
|
||
"confidence": 0.973,
|
||
"checksumValid": true,
|
||
"crossCheck": { "rosterMatch": true, "consistentWithOtherFiles": true }
|
||
}
|
||
}
|
||
```
|
||
|
||
> **错误 `40004`**:校验位不通过时**高亮错误位**提示核对原件,而不是简单说"格式错误"。
|
||
> **错误 `50002`**:OCR 服务异常 → **降级为人工填写**,绝不阻断。
|
||
|
||
### 9.10 `POST /api/v1/mp/submissions/{id}/submit`
|
||
|
||
**幂等**:是 **错误码**:`40001` 阻断 / `40008` 已截止或已暂停
|
||
|
||
**请求**
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|---|---|---|
|
||
| `confirmWarning` | bool | 知悉黄色警告后继续 |
|
||
| `ackIssueIds` | int64[] | 已确认知悉的警告 ID |
|
||
|
||
**校验失败响应**(`40001`)
|
||
|
||
```json
|
||
{
|
||
"code": 40001,
|
||
"msg": "存在 2 项必须处理的材料问题",
|
||
"data": {
|
||
"canSubmit": false,
|
||
"blocking": [
|
||
{ "materialCode": "SCHOOL_ROLL", "ruleCode": "VAL-05", "title": "缺少材料:学籍表",
|
||
"action": "UPLOAD", "actionText": "去上传" },
|
||
{ "materialCode": "IDCARD", "ruleCode": "VAL-04", "title": "本材料证件号与其他材料不一致",
|
||
"detail": "一寸照片为 360122200902044232,身份证为 36010520090817121X",
|
||
"action": "REPLACE", "actionText": "重新上传" }
|
||
],
|
||
"warnings": [
|
||
{ "materialCode": "PHOTO1", "ruleCode": "VAL-08", "title": "照片分辨率偏低,建议让对方发一张原图",
|
||
"action": "KEEP", "actionText": "仍然使用" }
|
||
],
|
||
"canForceSubmit": true
|
||
}
|
||
}
|
||
```
|
||
|
||
**成功响应**
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"data": {
|
||
"submissionId": 5001, "status": 1,
|
||
"submitTime": "2026-09-28T15:00:00+08:00",
|
||
"nextNode": "CLASS_AUDIT",
|
||
"nextNodeText": "等待李老师初审",
|
||
"estimatedAuditHours": 48
|
||
}
|
||
}
|
||
```
|
||
|
||
**社会收集**(本次不做,传 `collectMode=2` 直接 `40044`):`nextNode` 返回 `ADMIN_AUDIT`(跳过初审)。
|
||
|
||
### 9.11 `POST /api/v1/mp/submissions/{id}/withdraw`
|
||
|
||
**约束**:`status ∈ {1, 2}`(未终审)可撤回;`status = 3` 需管理员操作(`40031`)。
|
||
|
||
**响应**:`{ "status": 0, "withdrawnAt": "..." }`
|
||
|
||
### 9.12 `GET /api/v1/mp/submissions/{id}/progress`
|
||
|
||
**响应**
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"data": {
|
||
"status": 1, "progressPercent": "75%",
|
||
"materials": [
|
||
{ "materialCode": "PHOTO1", "materialName": "一寸照片", "required": true, "done": true,
|
||
"fileCount": 1, "issueLevel": 0 },
|
||
{ "materialCode": "SCHOOL_ROLL", "materialName": "学籍表", "required": true, "done": false,
|
||
"fileCount": 0, "issueLevel": 3, "issueText": "缺少材料" }
|
||
],
|
||
"timeline": [
|
||
{ "time": "2026-09-28T14:00:00+08:00", "title": "创建草稿" },
|
||
{ "time": "2026-09-28T15:00:00+08:00", "title": "已提交", "desc": "等待初审" }
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## §10 班主任移动端
|
||
|
||
### 10.1 `GET /api/v1/mp/teacher/dashboard`
|
||
|
||
**响应**
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"data": {
|
||
"summary": {
|
||
"rosterTotal": 28, "submittedCount": 23, "approvedCount": 15,
|
||
"pendingCount": 4, "unsubmittedCount": 5, "abnormalCount": 4,
|
||
"submitRate": "82.1%"
|
||
},
|
||
"myOrgs": [
|
||
{ "orgId": 2001, "orgName": "24中技数控1班", "rosterTotal": 28,
|
||
"submittedCount": 23, "approvedCount": 15, "submitRate": "82.1%",
|
||
"isHead": true }
|
||
],
|
||
"todos": { "auditCount": 4, "abnormalCount": 4, "rejectedAwaitingCount": 1 },
|
||
"deadline": { "taskId": 2026090001, "endTime": "2026-10-10T23:59:59+08:00", "remainDays": 12 }
|
||
}
|
||
}
|
||
```
|
||
|
||
> **口径铁律**:`submitRate` 是**已提交率 23/28 = 82.1%**,与 `approvedCount 15` 分开呈现。原型定稿如此,接口不许合并。
|
||
|
||
### 10.2 `GET /api/v1/mp/teacher/audit/todos`
|
||
|
||
**Query**:`orgId`、`level`(`0` 全部 / `1` 仅无问题 / `2` 有问题)、`keyword`、分页(**卡片流默认 pageSize=10**)
|
||
|
||
**响应**
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"data": {
|
||
"list": [
|
||
{
|
||
"submissionId": 5001, "rosterId": 9001,
|
||
"realName": "曾伊豪", "avatarUrl": null,
|
||
"orgName": "24中技数控1班",
|
||
"submitTime": "2026-09-28T15:00:00+08:00",
|
||
"waitHours": 3.2,
|
||
"level": 0,
|
||
"redCount": 0, "yellowCount": 0,
|
||
"materialThumbs": [ "url1", "url2", "url3", "url4" ],
|
||
"materialCount": 4,
|
||
"changedFields": ["phone"],
|
||
"rejectCount": 0,
|
||
"batchApprovable": true
|
||
},
|
||
{
|
||
"submissionId": 5002, "realName": "张三", "level": 3,
|
||
"redCount": 1, "yellowCount": 1,
|
||
"redSummary": "身份证号与名册不一致",
|
||
"batchApprovable": false
|
||
}
|
||
],
|
||
"total": 4,
|
||
"counts": { "all": 4, "clean": 3, "abnormal": 1 }
|
||
}
|
||
}
|
||
```
|
||
|
||
| `level` | 含义 | 卡片表现 |
|
||
|:--:|---|---|
|
||
| `0` | 无问题 | 绿色,**可右滑通过** |
|
||
| `2` | 仅黄色 | 黄色 |
|
||
| `3` | 有红色 | 红色,**不可批量、不可滑动** |
|
||
|
||
> **`batchApprovable`**:服务端算好,前端据此决定是否让该卡片参与"一键通过"。**规则不落地到前端**。
|
||
|
||
### 10.3 `POST /api/v1/mp/teacher/audit/{submissionId}/approve`
|
||
|
||
**幂等**:是
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|---|---|---|
|
||
| `remark` | string | 可选备注 |
|
||
| `actionSource` | string | `SWIPE`(右滑)/ `BUTTON`(按钮)——**用于埋点分析误触率** |
|
||
|
||
**响应**:`{ "status": 2, "nextNode": "ADMIN_AUDIT", "nextNodeText": "等待管理员终审" }`
|
||
|
||
**校验**:`redCount > 0` 时返回 `40032` —— **有红色问题的单不允许通过**,必须先驳回让学生改。
|
||
|
||
### 10.4 `POST /api/v1/mp/teacher/audit/{submissionId}/reject`
|
||
|
||
> ⚠️ **班主任端不做左滑驳回**。驳回必须点按钮 + 二次确认(误触后果不对称)。
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|---|---|---|---|
|
||
| `reasonCode` | string | ✅ | 见 PRD 附录 C |
|
||
| `reasonText` | string | 条件 | `R999` 时必填 |
|
||
| `materialCodes` | string[] | — | 指出是哪几项材料的问题 |
|
||
| `voiceText` | string | — | 语音转文字结果(可选输入方式) |
|
||
| `suggestReupload` | bool | — | 是否建议重新扫描 |
|
||
|
||
**响应**:`{ "status": 4, "rejectCount": 1, "notifySent": true, "escalated": false }`
|
||
|
||
**升级规则**:`rejectCount >= maxRejectCount(3)` 时 `escalated: true`,同时通知管理员。
|
||
|
||
### 10.5 `POST /api/v1/mp/teacher/audit/batch-approve`
|
||
|
||
**请求**
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|---|---|---|
|
||
| `submissionIds` | int64[] | ≤200 |
|
||
| `onlyClean` | bool | **默认 true 且服务端强制 true** |
|
||
|
||
**响应**
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"data": {
|
||
"approved": 3, "skipped": 1,
|
||
"details": [
|
||
{ "submissionId": 5002, "skipped": true,
|
||
"reason": "存在红色问题,需单独审核", "redCount": 1 }
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
> **铁律**:**批量通过只带无问题的单,红黄一律排除**。即使前端传了红单,服务端也跳过并回报原因。这是防止"手快全选"放行异常的最后一道闸。
|
||
|
||
### 10.6 `POST /api/v1/mp/teacher/proxy-upload`
|
||
|
||
**说明**:班主任代传材料(学生不会弄 / 人不在)。
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|---|---|---|---|
|
||
| `submissionId` | int64 | ✅ | — |
|
||
| `materialCode` | string | ✅ | — |
|
||
| `fileId` | int64 | ✅ | 已上传的文件 |
|
||
| `proxyReason` | string | — | 代传原因 |
|
||
|
||
**响应**:`{ "fileId": 987654322, "proxyBy": 10087, "proxyByName": "李老师" }`
|
||
|
||
> **代传必须留痕**:文件记录 `source = 4`(代传),审核时间轴显示"由李老师代传"。学生端可见。
|
||
|
||
### 10.7 `POST /api/v1/mp/teacher/notify/unsubmitted` · `GET /mp/teacher/notify/records`
|
||
|
||
| 接口 | 说明 |
|
||
|---|---|
|
||
| `notify/unsubmitted` | 请求 `{ orgIds[], channel, excludeRecentHours }`,**仅催未提交者** |
|
||
| `notify/records` | 崔报历史:`{ list: NotifyRecordVO[], total }`,可按 `bizType` / `status` 筛 |
|
||
|
||
---
|
||
|
||
## §11 审核(PC 端为主)
|
||
|
||
### 11.1 `GET /api/v1/audit/todos`
|
||
|
||
**Query**:`taskId`、`orgId`、`level`、`node`(`1` 初审 / `2` 终审)、`waitingOverHours`、分页
|
||
|
||
**响应**:`{ list: SubmissionVO[](精简), total, counts: { all, clean, abnormal, overTime } }`
|
||
|
||
### 11.2 `GET /api/v1/audit/submissions/{id}` ⭐ 审核详情
|
||
|
||
**响应**:完整 `SubmissionVO` + 材料 + 问题 + **识别结果**
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"data": {
|
||
"id": 5001, "status": 1,
|
||
"realName": "曾伊豪", "idCardMask": "3610292009******10",
|
||
"orgName": "24中技数控1班", "submitTime": "2026-09-28T15:00:00+08:00",
|
||
"changedFields": ["phone"],
|
||
"changedDetail": [
|
||
{ "field": "phone", "label": "联系电话",
|
||
"rosterValue": "138****8888", "studentValue": "139****9999",
|
||
"changedAt": "2026-09-28T14:50:00+08:00" }
|
||
],
|
||
"materials": [
|
||
{
|
||
"materialCode": "IDCARD", "materialName": "身份证", "done": true,
|
||
"files": [
|
||
{
|
||
"id": 987654321,
|
||
"originalName": "曾伊豪-361029200907295110身份证.jpg.jpg",
|
||
"standardName": "曾伊豪-361029200907295110-身份证.jpg",
|
||
"previewUrl": "https://...", "thumbUrl": "https://...",
|
||
"finalSizeKb": 512, "maxSizeKb": 1024,
|
||
"width": 1600, "height": 1200, "dpi": 300,
|
||
"validateStatus": 2,
|
||
"detect": { "materialCode": "IDCARD", "confidence": 0.98, "source": 1,
|
||
"sourceText": "文件名匹配", "conflict": false },
|
||
"issues": [
|
||
{ "ruleCode": "VAL-02", "severity": 1, "title": "证件号已按名册修正",
|
||
"expectedVal": "3610292009******10", "actualVal": "3610292009******10",
|
||
"autoFixed": true }
|
||
]
|
||
}
|
||
]
|
||
}
|
||
],
|
||
"redCount": 0, "yellowCount": 1,
|
||
"approvePolicy": { "canApprove": true, "canBatch": true, "reason": null },
|
||
"timeline": [ { "node": 0, "title": "学生提交", "time": "..." } ]
|
||
}
|
||
}
|
||
```
|
||
|
||
> **识别结果 (`detect`) 在此一并返回**——这就是追溯矩阵里 I10 缺口的落点,不需要独立接口。
|
||
|
||
### 11.3 `POST /api/v1/audit/submissions/{id}/approve` · `/reject`
|
||
|
||
**与班主任端同构**,差异:
|
||
|
||
| 项 | 初审(班主任) | 终审(管理员) |
|
||
|---|---|---|
|
||
| 通过后状态 | `1 → 2`(待终审) | `2 → 3`(已通过) |
|
||
| 驳回后状态 | `→ 4` | `→ 4` |
|
||
| 驳回是否可越过初审 | — | **可以**(终审驳回直接生效) |
|
||
| `redCount > 0` | 禁止通过 | 禁止通过 |
|
||
|
||
**终审请求额外字段**:`{ "finalOpinion": "同意认定", "needArchive": false }`
|
||
|
||
### 11.4 ⭐ `POST /api/v1/audit/submissions/{id}/revoke`
|
||
|
||
**说明**:撤回已作出的审核结论(**这是原设计缺失的接口**)。
|
||
|
||
**请求**
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|---|---|---|---|
|
||
| `auditRecordId` | int64 | ✅ | 要撤回的审核记录 |
|
||
| `reason` | string | ✅ | 撤回原因 |
|
||
|
||
**响应**:`{ "status": 1, "revokedRecordId": 5501 }`
|
||
|
||
**约束**
|
||
|
||
| 条件 | 结果 |
|
||
|---|---|
|
||
| 当前状态 `3`(已通过)且未被导出 | ✅ 允许,回退到 `2` 或 `1` |
|
||
| 当前状态 `2`(待终审) | ✅ 允许,回退到 `1`(撤回的是**初审**结论 —— 初审通过后发现填错,正是最需要撤的时刻) |
|
||
| **已被导出打包** | ❌ `40033`——**已交付的数据不可追溯修改** |
|
||
| 已归档 | ❌ `40034` |
|
||
| 撤回者非原审核人且非管理员 | ❌ `40301` |
|
||
|
||
**留痕**:写入一条 `action = 7`(撤回)的 `collect_audit_record`,时间轴显示"李老师撤回了通过结论"。
|
||
(旧版写 `action = 3` —— `3` 实际是**批量通过**,撤回恒为 `7`,与代码常量 `ACTION_REVOKE` 一致。)
|
||
|
||
### 11.5 `GET /api/v1/audit/submissions/{id}/timeline`
|
||
|
||
**响应**:`AuditRecordVO[]`,倒序。
|
||
|
||
### 11.6 `GET /api/v1/audit/statistics`
|
||
|
||
**Query**:`taskId`、`orgId`、`from` / `to`
|
||
|
||
**响应**:`{ pendingCount, approvedCount, rejectedCount, avgAuditHours, timeoutCount, byAuditor: [{ auditorName, handled, avgHours }], byReason: [{ reasonCode, reasonText, count }] }`
|
||
|
||
> **`byReason`** 是给管理员看"哪种问题最多"的,用于反过来优化材料引导语。
|
||
|
||
### 11.7 `GET /api/v1/audit/reason-codes` · `POST /api/v1/audit/submissions/{id}/proxy-upload`
|
||
|
||
| 接口 | 说明 |
|
||
|---|---|
|
||
| `reason-codes` | 返回 PRD 附录 C 的 10 条,仅返回启用的;可按 `taskId` 定制 |
|
||
| `proxy-upload` | 同 10.6,PC 端入口 |
|
||
|
||
---
|
||
|
||
## §12 校验 / 导出 / 通知
|
||
|
||
### 12.1 `POST /api/v1/validation/run`
|
||
|
||
**说明**:任务级全量校验,产出问题清单。
|
||
|
||
> ✅ **改为同步(2026-09-30 客户确认)**:旧版声明 `async` 默认 `true` 并返回 `jobId`,
|
||
> 但全契约**没有"查 jobId"的接口** —— 那个 jobId 查不到任何东西。
|
||
> 实测一份填报单的校验 <100ms、一个任务的量级秒级,**同步跑完直接回结果**
|
||
> 比返回一个查不到状态的作业号有用。保留 `jobId` 仅作本次运行的标识(幂等对账用)。
|
||
|
||
| 字段 | 类型 | 默认 | 说明 |
|
||
|---|---|---|---|
|
||
| `taskId` | int64 | ✅ | — |
|
||
| `scope` | string | `ALL` | `ALL` / `ISSUES_ONLY` / `APPOINTED` |
|
||
| `submissionIds` | int64[] | — | `scope=APPOINTED` 时必填 |
|
||
| `rules` | string[] | 全部 | 只跑指定规则(如 `["VAL-02","VAL-04"]`) |
|
||
|
||
**响应**:`{ "jobId": "v_3f2a", "checkedSubmissions": 320, "issuesFound": 47, "unresolvedLeft": 31, "elapsedMs": 812 }`
|
||
|
||
### 12.2 `GET /api/v1/validation/issues`
|
||
|
||
**Query**:`taskId`、`severity`、`ruleCode`、`orgId`、`resolved`、分页
|
||
|
||
**响应**
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"data": {
|
||
"list": [ { "...": "ValidationIssueVO" } ],
|
||
"total": 47,
|
||
"counts": { "severity1": 12, "severity2": 30, "severity3": 5 },
|
||
"byRule": [ { "ruleCode": "VAL-06", "title": "双扩展名", "count": 56, "autoFixed": 56 } ]
|
||
}
|
||
}
|
||
```
|
||
|
||
### 12.3 `GET /api/v1/validation/issues/export`
|
||
|
||
**响应**:Excel 文件流。列:`姓名` `班级` `材料` `规则码` `级别` `问题` `期望值` `实际值` `是否自动修复` `处理人`。
|
||
|
||
### 12.4 `POST /api/v1/exports` ⭐ 导出(前置检查是铁律)
|
||
|
||
**幂等**:是 **限流**:5 次/分钟/租户
|
||
|
||
**请求**
|
||
|
||
| 字段 | 类型 | 默认 | 说明 |
|
||
|---|---|---|---|
|
||
| `taskId` | int64 | ✅ | — |
|
||
| `exportName` | string | ✅ | 如 `202609机电认定-24中技数控1班` |
|
||
| `scope` | object | — | `{ orgIds, onlyApproved, includeRaw }` |
|
||
| `splitSizeMb` | int | `50` | 分卷大小 |
|
||
| `desensitize` | bool | `false` | 是否脱敏导出 |
|
||
| `format` | string | `ZIP` | `ZIP` / `FOLDER` |
|
||
| `generateSummaryExcel` | bool | `true` | 生成汇总 Excel |
|
||
| `notifyOnFinish` | bool | `true` | — |
|
||
|
||
**⚠️ 前置检查失败响应(`40035`)——导出按钮必须被禁用**
|
||
|
||
```json
|
||
{
|
||
"code": 40035,
|
||
"msg": "存在 3 项不合格文件,无法导出",
|
||
"data": {
|
||
"canExport": false,
|
||
"blocking": [
|
||
{ "type": "FILE_SIZE", "rosterId": 9012, "realName": "张三",
|
||
"materialCode": "WORK_PROOF", "fileId": 987654400,
|
||
"title": "工作证明 1.8M 超过 1M 上限", "action": "重新压缩或重传" },
|
||
{ "type": "NAME_INVALID", "rosterId": 9014, "realName": "李四",
|
||
"materialCode": "IDCARD", "fileId": 987654410,
|
||
"title": "文件名未规范化", "action": "触发重新处理" },
|
||
{ "type": "MISSING_MATERIAL", "rosterId": 9020, "realName": "王五",
|
||
"materialCode": "SCHOOL_ROLL",
|
||
"title": "缺少必传材料:学籍表", "action": "催报" }
|
||
],
|
||
"summary": { "sizeExceeded": 1, "nameInvalid": 1, "missing": 1, "unapproved": 0 },
|
||
"quickActions": ["RETRY_COMPRESS", "REPROCESS_NAMES", "NOTIFY_UNSUBMITTED"]
|
||
}
|
||
}
|
||
```
|
||
|
||
> **铁律 2**:`canExport = false` 时,前端**直接禁用「开始导出」按钮**,并指出是哪一个人的哪一项卡住了。**不允许"警告后放行"**。
|
||
|
||
**成功响应**:`{ "exportId": 555, "status": 0, "estimatedSeconds": 45 }`
|
||
|
||
### 12.5 `GET /api/v1/exports` · `GET /api/v1/exports/{id}`
|
||
|
||
**列表响应**:`{ list: ExportTaskVO[], total }`
|
||
|
||
**进度响应**
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"data": {
|
||
"id": 555, "status": 1, "progress": 62,
|
||
"currentStep": "COMPRESS",
|
||
"stepText": "正在压缩 24中技数控1班/工作证明",
|
||
"stats": { "fileCount": 112, "processedCount": 70, "rawSizeMb": 475, "finalSizeMb": 22.4,
|
||
"compressRatio": "95.3%" },
|
||
"resultFiles": null, "errorMsg": null
|
||
}
|
||
}
|
||
```
|
||
|
||
**完成响应**:`resultFiles: [{ "name": "202609机电认定-24中技数控1班.zip", "sizeMb": 22.4, "url": "..." }]`
|
||
|
||
### 12.6 `GET /api/v1/exports/{id}/download`
|
||
|
||
**Query**:`fileIndex`(分卷序号,默认 0)
|
||
|
||
**响应**
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"data": {
|
||
"url": "https://oss.../export/555/part1.zip?Signature=...",
|
||
"expireSeconds": 1800,
|
||
"fileName": "202609机电认定-24中技数控1班.zip",
|
||
"sizeMb": 22.4,
|
||
"checksum": "sha256:abc123...",
|
||
"remainsCachedDays": 7
|
||
}
|
||
}
|
||
```
|
||
|
||
**错误 `40302`**:签名过期 → 重新获取(不重复计费)。
|
||
|
||
### 12.7 通知
|
||
|
||
| 接口 | 请求要点 | 响应 |
|
||
|---|---|---|
|
||
| `GET /notify/templates` | `taskId`(可选,租户级/任务级) | `[{ id, templateCode, bizType, channel, title, content, enabled }]` |
|
||
| `PUT /notify/templates/{id}` | `{ title, content, enabled }`;`content` 支持变量 `{姓名}` `{任务名}` `{截止日}` `{驳回原因}` | `{ updated: true }` |
|
||
| `POST /notify/send` | `{ bizType, receiverIds[], channel, templateCode, extraText }` | `{ sent, failed, skipped }` |
|
||
| `GET /notify/records` | `taskId`、`bizType`、`status`、分页 | `{ list: NotifyRecordVO[], total, deliveryRate: "97.2%" }` |
|
||
| `POST /callback/wechat/msg` | 微信回调原始报文 | `{ errcode: 0 }` |
|
||
|
||
**模板变量白名单**(越界变量渲染为空并告警):
|
||
|
||
| 变量 | 适用 |
|
||
|---|---|
|
||
| `{姓名}` `{班级}` | 全部 |
|
||
| `{任务名}` `{截止日}` `{剩余天数}` | 全部 |
|
||
| `{驳回原因}` `{材料名}` | 驳回/催报 |
|
||
| `{提交进度}` | 催报 |
|
||
| `{导出包名}` `{下载链接}` | 导出完成 |
|
||
|
||
**订阅消息限制**:微信订阅消息为**一次性订阅**,每次发送消耗 1 次额度(`collect_subscribe_quota`)。额度不足时**降级为公众号模板消息**,再不足降级短信(按条计费,见 §13.6)。
|
||
|
||
---
|
||
|
||
## §13 本次补齐的缺口接口
|
||
|
||
### 13.1 说明
|
||
|
||
> 来源:《需求追溯矩阵》§3.1 审计发现的 10 个缺口。**这些接口在原 PRD §8 中被需求提及但未定义**,若不补齐,对应页面将无数据来源。
|
||
|
||
| 缺口 | 补在哪 | 状态 |
|
||
|---|---|---|
|
||
| I1 报考范围读取/确认 | **§7.7 / §7.8**(已定义) | ✅ |
|
||
| I8 审核撤回 | **§11.4**(已定义) | ✅ |
|
||
| I9 任务暂停/恢复 | **§7.4**(已定义) | ✅ |
|
||
| I10 材料识别结果读取 | **并入 §11.2 详情响应**(不新增接口) | ✅ |
|
||
| I3 字典管理 | **§13.3** | ✅ |
|
||
| I4 / I5 日志 | **§13.4** | ✅ |
|
||
| I6 消息中心 | **§13.5** | ✅ |
|
||
| I7 订阅额度登记 | **§13.6** | ✅ |
|
||
|
||
### 13.2 历史数据整理器(已取消 · 0 个)
|
||
|
||
> ⚠️ **本节已撤销(需求收敛 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 个)
|
||
|
||
| 接口 | 说明 |
|
||
|---|---|
|
||
| `GET /api/v1/dicts` | 字典列表 `[{ dictCode, dictName, itemCount, builtin }]` |
|
||
| `GET /api/v1/dicts/{code}/items` | 字典项 `[{ id, itemCode, itemName, parentItemCode, sortNo, status }]`,**含层级**(如职业-工种) |
|
||
| `POST /api/v1/dicts/{code}/items` | 新增项:`{ itemCode, itemName, parentItemCode, sortNo }` |
|
||
| `PUT /api/v1/dicts/items/{id}` | 修改:`{ itemName, sortNo, status }` |
|
||
|
||
**内置字典(`builtin: true`,不可删,只可停用单项)**
|
||
|
||
| `dictCode` | 名称 | 项数 |
|
||
|---|---|---|
|
||
| `EDU_LEVEL` | 学历 | 11 |
|
||
| `JOB_LEVEL` | 职业技能等级 | 5(一~五级) |
|
||
| `CERT_TYPE` | 认定类型 | 3 |
|
||
| `RETARGET_SUBJECT` | 补考科目 | 3 |
|
||
| `OCCUPATION` | 职业(国标,可选) | 由 `sys_occupation_ref` 提供 |
|
||
| `REJECT_REASON` | 驳回原因 | 10 |
|
||
|
||
**约束**:有下级项的字典项不可停用(`40036`)。修改 `itemCode` 不允许(会破坏历史数据)。
|
||
|
||
> **`OCCUPATION` 字典的特殊性**:官方 Excel 模板里「报考职业」「报考工种」**是自由填写、没有下拉**。所以字典在这里**只做拼写校验与联想建议(P2 级),不做强约束**。凡发现自由填写值不在字典中,记 `SCOPE_FREE_TEXT` 信息级提示,**不阻断**。
|
||
|
||
### 13.4 日志(2 个)
|
||
|
||
#### `GET /api/v1/logs/operations`
|
||
|
||
**Query**:`tenantId`、`operatorId`、`module`、`action`、`targetType`、`targetId`、`from` / `to`、分页
|
||
|
||
> `targetType` + `targetId` 是**最重要的查询方式**:靠关键字全文匹配会把
|
||
> 「改了它」和「顺带提到它」混在一起,而合规结论不能建立在这种匹配上。
|
||
>
|
||
> `from` / `to` 只给 10 位日期时,`to` 会补成当天 `23:59:59` —— 不补的话
|
||
> 「查 9 月 30 日」查不到当天的记录。
|
||
|
||
**范围**(权限二选一,`@RequirePerm({log:viewTenant, log:viewAll})`):
|
||
|
||
| 调用者 | 权限 | 看到什么 |
|
||
|---|---|---|
|
||
| 平台运营 | `log:viewAll` | **默认全量**(跨租户);传 `tenantId` 则只看该单位 |
|
||
| 学校管理员 / 教务处 | `log:viewTenant` | **恒为本租户**;传了别的 `tenantId` → `40301`(**不静默忽略**:静默会让人以为"筛过了") |
|
||
|
||
**响应**
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"data": {
|
||
"list": [
|
||
{ "id": 70001, "operatorId": 10086, "operatorName": "王老师", "module": "ROSTER",
|
||
"action": "UPDATE_IDCARD", "targetType": "ROSTER", "targetId": 9012,
|
||
"beforeVal": "3610292009******10", "afterVal": "3610292009******99",
|
||
"ip": "10.1.2.3", "userAgent": "Chrome/120",
|
||
"requestMethod": "PUT", "requestUri": "/api/v1/roster/9012",
|
||
"extraJson": "{\"orgId\":43,\"taskId\":487}",
|
||
"bizCode": 0, "traceId": "3f9c…", "createTime": "2026-09-28T16:20:00+08:00" }
|
||
],
|
||
"total": 1
|
||
}
|
||
}
|
||
```
|
||
|
||
**动作码全集**(`action`,权威来源是后端 `OperationLogService` 的 `ACTION_*` 常量,
|
||
PC 端 `apis/xt/log.ts` 的 `OPERATION_ACTIONS` 是它的文案表 —— 两端由
|
||
`scripts/check-wiring.py` 第 ⑩ 项**逐字核对**,缺一个界面上就会原样回显英文码)
|
||
|
||
| `action` | 场景 |
|
||
|---|---|
|
||
| `UPDATE_IDCARD` | **改名册证件号**(留痕强制) |
|
||
| `UPDATE_ROSTER_NAME` | 改名 |
|
||
| `DELETE_ROSTER` | 删名册项 |
|
||
| `PUBLISH_TASK` | 发布任务 |
|
||
| `PAUSE_TASK` / `RESUME_TASK` | 暂停 / 恢复("恢复"意味着曾经暂停过,必须与"发布"分开记) |
|
||
| `CLOSE_TASK` / `AUTO_CLOSE` | 手动截止 / **定时任务自动截止**(出争议时第一个要看的就是这个区别) |
|
||
| `STATUS_CHANGE` | 任务状态变更(兜底名:调用方说不出是哪个动作时才用) |
|
||
| `ARCHIVE_TASK` / `DELETE_TASK` | 归档 / 删除已归档任务 |
|
||
| `REVOKE_AUDIT` | **撤回审核** |
|
||
| `EXPORT_CREATE` / `EXPORT_DOWNLOAD` | 创建导出 / 下载导出文件 |
|
||
| `PROXY_UPLOAD` | 代传材料 |
|
||
| `VIEW_IDCARD_FULL` | **查看完整身份证号**(单独留痕) |
|
||
| `CONFIG_COMPRESS` | 改压缩配置 |
|
||
| `CREATE_ROLE` / `UPDATE_ROLE` / `DELETE_ROLE` | 角色的增删改 |
|
||
| `GRANT_ROLE` / `UPDATE_ROLE_MENU` / `TOGGLE_ROLE` | **改授权** / 改可见菜单 / 停用启用角色 |
|
||
| `CREATE_TENANT` / `UPDATE_TENANT` | 开通租户 / 改租户信息·套餐 |
|
||
| `DISABLE_TENANT` / `ENABLE_TENANT` | **停用租户**(该校所有人立刻登不进来)/ 恢复启用 |
|
||
|
||
**模块 `module`**:`TASK` / `ROSTER` / `FILE` / `ROLE` / `TENANT`。
|
||
(`AUDIT` / `EXPORT` / `USER` / `JOIN` 四个常量**当前没有写入点** —— 界面上的模块下拉
|
||
刻意不列它们:选中只会得到一张空表,让人以为"这段时间没有审核动作"。)
|
||
|
||
**脱敏**:`beforeVal` / `afterVal` 中的证件号**一律脱敏存储**,即使日志泄露也不暴露明文。
|
||
服务层还会再兜底脱敏一次并截断到 512 字符(两层都做,是因为"忘记脱敏"的后果不可逆)。
|
||
|
||
**两列刻意不采集**:`request_param`(请求参数)与 `duration_ms`(耗时)**没有任何写入点**
|
||
(全库 0 行有值)。要采集得引入 Servlet 过滤器或 AOP 抓请求体并脱敏,
|
||
而合规留痕本来就不需要它们 —— 界面上不展示、也不做筛选。
|
||
|
||
> **一期只做查询,不做日志导出**(`backend/.../module/log/controller/LogController.java` 的类注释):
|
||
> 导出等于把「谁在什么时候看了谁的身份证」这份名单落到本地磁盘上,
|
||
> 而"有人导出过这份日志"本身也要留痕 —— 一个追着自己尾巴的循环。
|
||
> 这条口径**只写在代码注释里**过一段时间,所以在这里也记一笔。
|
||
|
||
#### `GET /api/v1/logs/logins`
|
||
|
||
**Query**:`tenantId`、`userId`、`userType`、`result`、`from` / `to`、分页
|
||
|
||
**范围**:与操作日志同一口径(`log:viewAll` 可跨租户,学校管理员恒为本租户)。
|
||
|
||
**响应**:`{ list: [{ id, tenantId, userId, userName, userType, loginType, ip, location, device, result, failReason, loginTime }], total }`
|
||
|
||
⚠️ 三条口径:
|
||
|
||
- `tenantId`(`0` = 平台)**必须有**:平台运营看到的是跨单位的登录日志,
|
||
没有这一列就分不清一行是哪所学校的;
|
||
- `userType` 是 **`1` 平台运营 / `2` 学校管理员 / `3` 班主任 / `4` 学生**
|
||
(权威来源是 `BuiltinRole.UserType` 与 `UserService.USER_TYPE_NAMES`,库列注释也一致)
|
||
—— **不要**按某些模板里那套「1 教师 / 2 学生」去映射;
|
||
- `location`(IP 归属地)**一期恒为 `null`**:没接 IP 库,留 null 而不是编一个。
|
||
界面**不要显示这一列**(否则整列空白,看起来像功能坏了)。
|
||
|
||
| `loginType` | 含义 |
|
||
|---|---|
|
||
| `PASSWORD` | PC 账号密码 |
|
||
| `WECHAT` | 小程序微信授权 |
|
||
| `PHONE` | 手机号快捷登录 |
|
||
|
||
> **保留期**:操作日志 3 年,登录日志 1 年(可在租户配置中调整)。
|
||
|
||
### 13.5 消息中心(3 个)
|
||
|
||
#### `GET /api/v1/mp/messages`(学生端)
|
||
|
||
**Query**:`bizType`、`read`、分页
|
||
|
||
**响应**
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"data": {
|
||
"list": [
|
||
{ "id": 80001, "bizType": 3, "title": "你的材料被退回",
|
||
"content": "「工作证明」未盖公章,请重新提供",
|
||
"read": false, "linkType": "SUBMISSION", "linkId": 5001,
|
||
"createTime": "2026-09-28T16:30:00+08:00" },
|
||
{ "id": 80002, "bizType": 2, "title": "距截止还有 3 天",
|
||
"content": "请在 10 月 10 日前完成填报", "read": true,
|
||
"linkType": "TASK", "linkId": 2026090001, "createTime": "2026-10-07T09:00:00+08:00" }
|
||
],
|
||
"total": 2,
|
||
"unreadCount": 1
|
||
}
|
||
}
|
||
```
|
||
|
||
#### `POST /api/v1/mp/messages/read`
|
||
|
||
`{ "messageIds": [80001], "all": false }` → `{ "unreadCount": 0 }`
|
||
|
||
#### `GET /api/v1/mp/teacher/messages`(教师端)
|
||
|
||
同构,`bizType` 增加 `7`(加入名单审批)等教师专属类型。
|
||
|
||
> **消息中心 vs 通知记录的区别**:`collect_notify_record` 记的是**发送通道结果**(发出去没有、到没到),消息中心记的是**站内信内容**(用户看到什么)。二者通过 `bizType` 关联。
|
||
|
||
### 13.6 订阅消息额度登记(1 个)
|
||
|
||
#### `POST /api/v1/mp/subscribe`
|
||
|
||
**说明**:微信订阅消息为**一次性授权**,用户每点一次"允许"只能发一条。小程序端在关键节点**主动索取授权**并登记额度。
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|---|---|---|---|
|
||
| `templateCode` | string | ✅ | 模板码,见下表 |
|
||
| `subscribeCount` | int | — | 本次授权次数(微信返回,默认 1) |
|
||
|
||
**响应**:`{ "templateCode": "AUDIT_RESULT", "total": 5, "used": 2, "remain": 3 }`
|
||
|
||
**模板码与索取时机**
|
||
|
||
| `templateCode` | 索取时机 | 用途 |
|
||
|---|---|---|
|
||
| `SUBMIT_SUCCESS` | 提交成功后 | 通知审核进展 |
|
||
| `AUDIT_RESULT` | 提交成功后 | 通过 / 驳回通知 |
|
||
| `DEADLINE_REMIND` | 进入填报页时 | 临期提醒 |
|
||
| `EXPORT_READY` | 管理端创建导出时 | 导出完成 |
|
||
|
||
**额度不足的降级链**
|
||
|
||
```
|
||
微信订阅消息(免费,额度受限)
|
||
↓ 额度耗尽
|
||
公众号模板消息(需关注公众号)
|
||
↓ 未关注
|
||
短信(按条计费,走 collect_subscribe_quota 计费口径)
|
||
↓ 超出套餐
|
||
仅站内信(不推送,用户主动进小程序可见)
|
||
```
|
||
|
||
> **降级必须静默**:学生端不显示"你的额度不够了"。运营侧在 `GET /tenants/{id}/usage` 里看到 `smsCount` 上涨与告警。
|
||
|
||
---
|
||
|
||
## §14 开放接口(供认定机构对接)
|
||
|
||
**鉴权**:`appKey + timestamp + nonce + HMAC-SHA256`
|
||
|
||
```
|
||
X-App-Key: jxjd_2026
|
||
X-Timestamp: 1790000000
|
||
X-Nonce: 8f3a9c2e
|
||
X-Sign: HMAC-SHA256(appSecret, appKey + timestamp + nonce + body)
|
||
```
|
||
|
||
**签名规则**:`body` 为**原始 JSON 字符串**(未格式化、未排序);`timestamp` 与服务器时间偏差 > 5 分钟拒绝;`nonce` 5 分钟内不可重复。
|
||
|
||
### 14.1 `POST /open/v1/auth/token`
|
||
|
||
**请求**:`{ "appKey": "jxjd_2026", "appSecret": "..." }`
|
||
|
||
**响应**:`{ "accessToken": "...", "expireSeconds": 7200, "scope": ["submission:read","file:read"] }`
|
||
|
||
### 14.2 `GET /open/v1/tasks/{id}/submissions`
|
||
|
||
**Query**:`since`(增量拉取时间)、`status`(默认 `3` 已通过)、分页
|
||
|
||
**响应**:`SubmissionVO` 数组 + `idCardMask`(**开放接口一律不返回明文证件号**)
|
||
|
||
### 14.3 `GET /open/v1/tasks/{id}/files`
|
||
|
||
**Query**:`submissionIds`(逗号分隔,≤200)
|
||
|
||
**响应**
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"data": {
|
||
"files": [
|
||
{ "fileId": 987654321, "submissionId": 5001, "realName": "曾伊豪",
|
||
"materialCode": "IDCARD", "standardName": "曾伊豪-3610292009******10-身份证.jpg",
|
||
"url": "https://oss.../sign?expire=1800", "sizeKb": 512, "expireSeconds": 1800 }
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
> 签名 URL **有效期 30 分钟**,过期需重新获取。
|
||
|
||
### 14.4 `POST /open/v1/callback/push`
|
||
|
||
**说明**:认定机构提供回调地址,系统在终审通过后主动推送。
|
||
|
||
**请求(我方 → 机构)**
|
||
|
||
```json
|
||
{
|
||
"eventType": "SUBMISSION_APPROVED",
|
||
"occurTime": "2026-09-28T17:00:00+08:00",
|
||
"taskId": 2026090001, "taskCode": "2026090001",
|
||
"submissionId": 5001,
|
||
"data": { "realName": "曾伊豪", "idCardMask": "3610292009******10",
|
||
"occupation": "车工", "worktype": "数控车工", "jobLevel": "四级" },
|
||
"files": [ { "materialCode": "IDCARD", "url": "https://...", "expireSeconds": 1800 } ]
|
||
}
|
||
```
|
||
|
||
**重试策略**:失败重试 5 次,间隔 1/5/30/120/600 秒;全部失败后标记并告警,支持人工重推。
|
||
|
||
---
|
||
|
||
## §15 错误码全集
|
||
|
||
> 在 PRD 附录 D(20 个码)基础上**新增 34 个**(标 ⭐),本节共 **54 个**(含 `0` 成功码)。所有码在 `code` 字段返回,HTTP 状态码见 §1.3。
|
||
|
||
### 15.1 客户端错误(4xxxx)
|
||
|
||
| 码 | 含义 | 端处理 |
|
||
|---|---|---|
|
||
| `40001` | 存在阻断性校验问题,无法提交 | 展示 blocking 清单,引导逐项处理 |
|
||
| `40002` | 文件格式不在白名单 | 提示支持的格式 |
|
||
| `40003` | 文件体积超上限且压缩受限 | 提示重新提供 |
|
||
| `40004` | 身份证号校验位不通过 | **高亮错误位**,提示核对原件 |
|
||
| `40005` | 证件号与名册不一致 | 展示名册值,要求确认 |
|
||
| `40006` | 疑似串档 | 转人工,**不自动修正** |
|
||
| `40007` | 门禁未匹配 | **统一提示**"没有找到你的信息",不透露哪项错 |
|
||
| `40071` | 门禁尝试过于频繁 | 提示"操作过于频繁,请稍后再试" |
|
||
| `40072` | 名单项已被他人绑定 | 提示"该信息已被登记" |
|
||
| `40008` | 任务已截止 / 已归档 / **已暂停** | 禁止提交 |
|
||
| `40009` | 重复提交(幂等键冲突) | 返回既有结果 |
|
||
| `40010` | 驳回次数超上限 | 提示联系班主任 |
|
||
| ⭐ `40011` | 批量操作超过 200 条上限 | 分批提交 |
|
||
| ⭐ `40012` | 账号或密码错误 | 重新输入(不透露账号是否存在) |
|
||
| ⭐ `40013` | 账号已锁定 | 提示剩余锁定时间 |
|
||
| ⭐ `40014` | 用户有在办任务,不可停用 | 提示先移交任务 |
|
||
| ⭐ `40015` | 组织被占用,不可删除 | 展示占用清单 |
|
||
| ⭐ `40016` | 名册为空,不可发布 | 引导导入名册 |
|
||
| ⭐ `40017` | 名册缺少「姓名」或「身份证号」列 | 引导重新导入 |
|
||
| ⭐ `40018` | 未配置必传材料项 | 引导配置材料 |
|
||
| ⭐ `40019` | 未设置覆盖班级或班主任 | 引导配置 |
|
||
| ⭐ `40020` | 命名规则试跑未通过 | 展示失败样例,引导修规则 |
|
||
| ⭐ `40021` | 仅已发布任务可暂停 | 刷新状态 |
|
||
| ⭐ `40022` | 仅已暂停任务可恢复 | 刷新状态 |
|
||
| ⭐ `40023` | 存在待审单,不可归档 | 展示待审单 |
|
||
| ⭐ `40024` | 报考范围未覆盖全部名册 | 返回差集(**漏人查验**) |
|
||
| ⭐ `40025` | 表单版本冲突 | 返回最新 schema,前端差异合并 |
|
||
| ⭐ `40026` | 内置字段不可删除 | 提示可改标签/顺序 |
|
||
| ⭐ `40027` | 材料项已有文件,不可删除 | 提示改为隐藏 |
|
||
| ⭐ `40028` | 无法自动定位表头行 | 返回前 10 行预览,要求手动指定 |
|
||
| ⭐ `40029` | 名册项已绑定/已提交,不可删除 | 提示改为"不参与本次认定" |
|
||
| ⭐ `40030` | 已进入审核,需先撤回才能删材料 | 引导撤回 |
|
||
| ⭐ `40031` | 已通过终审,学生不可撤回 | 提示联系管理员 |
|
||
| ⭐ `40032` | **存在红色问题,不允许通过** | 必须先驳回让学生修正 |
|
||
| ⭐ `40033` | **已导出打包,不可撤回审核** | 已交付数据不可追溯修改 |
|
||
| ⭐ `40034` | 已归档,不可撤回 | — |
|
||
| ⭐ `40035` | **存在不合格文件,禁止导出** | 禁用导出按钮 + 展示 blocking 清单 |
|
||
| ⭐ `40036` | 字典项有下级,不可停用 | 先处理下级 |
|
||
| ⭐ `40041` | **名册中该证件号已被占用** | 不覆盖,提示管理员核查 |
|
||
| ⭐ `40043` | **文件归属存疑**(文件名指向名册中的另一个人,或归属无法确证) | **阻断提交**,提示"这份材料可能不是你的",引导重新上传(**无"仍然继续"选项**) |
|
||
| ⭐ `40044` | **一期不支持该收集模式 / 认定类型** | 请使用学校收集 + 自主认定(见 PRD「一期范围声明」) |
|
||
| ⭐ `40045` | **角色还有账号在用,不可删除** | 先给这些账号换一个角色(界面会说明还有几个账号) |
|
||
| `40101` | 未登录 / token 失效 | 静默重新登录 |
|
||
| `40301` | 无权限(含跨租户) | 跳转 403 |
|
||
| `40302` | 签名 URL 已过期 | 重新获取 |
|
||
| `42901` | 触发限流 | 退避重试 |
|
||
|
||
### 15.2 服务端错误(5xxxx)
|
||
|
||
| 码 | 含义 | 端处理 |
|
||
|---|---|---|
|
||
| `50001` | 文件处理失败 | 保留原图,允许重试 |
|
||
| `50002` | OCR 服务异常 | **降级为人工填写** |
|
||
| `50003` | 图片过大被熔断 | 转异步处理 |
|
||
| ⭐ `50004` | VLM 服务异常 | **降级为规则结果或不判**(识别失败一律放行) |
|
||
| ⭐ `50005` | 导出打包失败 | 保留中间态,允许重试(不重复计费) |
|
||
| ⭐ `50006` | 通知发送失败 | 记录 `fail_reason`,走降级链 |
|
||
| ⭐ `50007` | 存储写入失败 | 重试 3 次后提示用户重传 |
|
||
|
||
> **降级原则**(贯穿全部 5xxxx):**任何服务端能力失效都不得阻断学生提交**。OCR 挂了就手填,VLM 挂了就不判,识别失败就放行。这是填报完成率的底线保障。
|
||
|
||
---
|
||
|
||
## §16 变更记录
|
||
|
||
| 版本 | 日期 | 变更 |
|
||
|---|---|---|
|
||
| **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`)而非密文,宽度足够,无需改动。 |
|
||
| **V1.2** | **2026-09-28** | **错误码规范化**:`40007b` / `40007c` 带字母后缀,与 §1.3「`code` 为数字」的约定冲突(JSON 里会变成字符串,前端 `code === 0` 判断失效)。改为纯数字:**`40007b` → `40071`**(门禁限流)、**`40007c` → `40072`**(名单项已被绑定)。总码数不变,仍 **58 个**。 |
|
||
| V1.0 | 2026-09-28 | 首版。PRD §8 的 96 个接口全部展开字段级定义;**补齐追溯矩阵审计发现的 10 个缺口(新增 21 个接口,含 1 个登记遗漏)**;错误码由 20 个扩至 **50 个**;新增 §2 枚举全集、§3 通用数据结构、§15 错误码全集 |
|
||
|
||
| **V1.1** | **2026-09-28** | **客户要求移除「联系方式展示」**:删除 `GET /api/v1/mp/roster/contact`(原用于门禁失败页展示班主任脱敏手机号)。<br>**理由**:学生在班级群里本来就有班主任联系方式,系统内再放一份既不必要(用户已知怎么找人)、有维护成本(换人换号要同步)、又多一处隐私暴露面。<br>门禁失败页改为只保留两条**系统内动作**:重新填写 / 申请加入名单(FR-509)。纯文案提示("如有疑问请联系老师")保留,因**不涉及任何联系方式数据**。<br>**接口总数 122 → 121** |
|
||
| V1.0 | 2026-09-28 | 首版。PRD §8 的 96 个接口全部展开字段级定义
|
||
|
||
### 自校验记录
|
||
|
||
| 检查项 | 结果 |
|
||
|---|---|
|
||
| §4 各模块表行数合计 | **119** ✓ 与声称一致 |
|
||
| §4 内路径重复 | 无 ✓ |
|
||
| Markdown 表格列数一致性 | 0 行不符 ✓ |
|
||
| 文档引用的字段名 | 116 个唯一值,**全部存在于 DDL** ✓ |
|
||
| 错误码数量 | 54 个(新增 34 个标 ⭐)✓ |
|
||
| 文件规模 | 约 89 KB / 2870 行 |
|
||
|
||
---
|
||
|
||
## §17 实现补充与偏差记录
|
||
|
||
> **§4 的 119 个是设计口径**。本节回答两件事:① 代码里还有哪些 §4 没列的接口;
|
||
> ② 哪些地方**有意偏离**了本文档的原始写法、为什么、谁确认的。
|
||
>
|
||
> ⚠️ 17.1 的清单由工具生成:`python scripts/check-api-parity.py --extra`
|
||
> (当前 **38 个**,2026-10 复核)。**手抄清单会过期,以命令输出为准**;
|
||
> 下表按"为什么不在 §4"分组,便于决定"补进 §4 还是留在本节"。
|
||
|
||
### 17.1 代码有、§4 清单没有的接口(38 个,按偏离原因分组)
|
||
|
||
**A · 材料访问通道(7 个)** —— 审核端与学生端各一条通道(审核端要看防伪纹,学生端只看自己的);上传是一步 multipart
|
||
|
||
| 接口 | 说明 |
|
||
|---|---|
|
||
| `POST /api/v1/mp/files/upload` | 实际的上传入口(一步 multipart,收上来即压缩+改名) |
|
||
| `GET /api/v1/mp/files/{id}/url` | 学生端取签名预览链接 |
|
||
| `GET /api/v1/mp/files/{id}/content` | 学生端直出内容 |
|
||
| `GET /api/v1/mp/files/submissions/{submissionId}` | 按提交单列出材料 |
|
||
| `GET /api/v1/audit/files/{id}/url` | 审核端签名链接(独立通道;支持 `?thumb=true` 取缩略图) |
|
||
| `GET /api/v1/audit/files/{id}/content` | 审核端直出内容 |
|
||
| `GET /api/v1/mp/teacher/files/{fileId}/preview` | 教师端(代传场景)预览学生材料 |
|
||
|
||
**B · 微信授权链路代替 `POST /mp/login`(4 个)**
|
||
|
||
| 接口 | 说明 |
|
||
|---|---|
|
||
| `GET /api/v1/mp/h5/authorize` | H5 微信网页授权跳转 |
|
||
| `GET /api/v1/mp/h5/callback` | 授权回调(token 放 URL fragment) |
|
||
| `GET /api/v1/mp/h5/stub-login` | **仅非生产**打桩登录(`stub-enabled` + profile 双重保护) |
|
||
| `GET /api/v1/mp/scene-context` | 匿名扫码落地页查询(scene 三段式,只返回最少信息 + 限流) |
|
||
|
||
**C · 签名 URL 下载(1 个)**
|
||
|
||
| 接口 | 说明 |
|
||
|---|---|
|
||
| `GET /api/v1/exports/{id}/file` | 导出包的流式下载。§12.6 只定义了"取下载信息";真正下文件走这条,**URL 有有效期、不能入库/外发** |
|
||
|
||
**D · 界面与接口必需、清单漏列(16 个)**
|
||
|
||
| 接口 | 说明 |
|
||
|---|---|
|
||
| `GET /api/v1/auth/captcha` | 图形验证码(连续失败 2 次后必填) |
|
||
| `GET /api/v1/auth/public-key` | 密码 RSA 加密传输用的公钥 |
|
||
| `GET /api/v1/material-catalog` | 材料目录(码/档位/格式/模板/体积范围) |
|
||
| `POST /api/v1/tasks/{id}/materials/apply-template` | 应用内置材料模板(幂等) |
|
||
| `POST /api/v1/tasks/{id}/save-as-template` | 保存为模板(§7.13 的缺口,F-62) |
|
||
| `POST /api/v1/audit/submissions/batch-approve` | 批量通过(§11.3 标题带过、清单未单列) |
|
||
| `GET /api/v1/mp/teacher/audit/reason-codes` | 教师端驳回原因字典(§11.7 有 PC 版) |
|
||
| `GET /api/v1/mp/teacher/submissions/{id}/proxy-context` | 代传前取上下文(哪个班、哪份草稿) |
|
||
| `GET /api/v1/qrcodes/{scene}` | 匿名出图 |
|
||
| `POST /api/v1/roster/{rid}/deactivate` | 名册项"标记不参与"(删除的可逆替代,§8.3/8.4) |
|
||
| `GET /api/v1/tenants/{id}` | 租户详情 |
|
||
| `PUT /api/v1/users/{id}/status` | 启用/停用(清单只有 DELETE 停用) |
|
||
| `DELETE /api/v1/dicts/items/{id}` | 删除自定义字典项(§13.3) |
|
||
| `POST /api/v1/dicts/{code}/items/{itemCode}/toggle` | 内置字典项停用/恢复(§13.3) |
|
||
| `GET /api/v1/exports/precheck` | 导出前置检查(只检查不创建;界面靠它禁用按钮) |
|
||
| `GET /api/v1/exports/preview` | 导出目录树预览(与真正打包共用同一份构建代码) |
|
||
|
||
**E · 契约链条原本断在第一步(8 个,已实现)**
|
||
|
||
| 接口 | 说明 |
|
||
|---|---|
|
||
| `POST /api/v1/notify/templates/{code}/customize` | 把内置模板复制成可编辑的库行(F-52:内置模板没有 id,`PUT /{id}` 拿不出 id) |
|
||
| `DELETE /api/v1/notify/templates/{code}` | 恢复默认模板 |
|
||
| `GET /api/v1/notify/templates/variables` | 变量白名单(前端抄一份迟早不一致) |
|
||
| `POST /api/v1/open-apps` · `GET /api/v1/open-apps` | §14 的接入方管理(F-67:没有它,appSecret 只能改库产生) |
|
||
| `POST /api/v1/open-apps/{id}/reset-secret` | 重置密钥(显式动作,会让对方现有签名立即失效) |
|
||
| `POST /api/v1/open-apps/{id}/toggle` | 启停接入方 |
|
||
| `GET /api/v1/open-apps/push-logs` | 推送记录(人工重推的依据) |
|
||
|
||
**F · 运维与调试动作(2 个)** —— 定时任务在测试里"等不起",所以都有手工入口(权限走运营侧 `log:view:all`)
|
||
|
||
| 接口 | 说明 |
|
||
|---|---|
|
||
| `POST /api/v1/schedules/run` | 手工触发定时任务(`job` 取值 close/urge/timeout/raw/usage/retry/expire) |
|
||
| `POST /api/v1/exports/cleanup-run` | 手工触发导出包过期清理(可重复执行) |
|
||
|
||
### 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-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 的接口 |
|
||
| D-6 | 撤回动作码 | §11.4 旧文 `action=3` | **`action=7`(撤回)**;`3` 是批量通过;状态 2/3 均可撤(2 撤的是初审) | F-7;时间轴不能混淆 |
|
||
| D-7 | §14 路径前缀 | §14 详情写 `POST /open/v1/...`(无 `/api/v1`) | 统一 `/api/v1/open/v1/...`(与 §1 及其余接口一致,网关一条规则) | F-65 |
|
||
| D-8 | `biz_type` 落库类型 | §6 字段表按数字码定义 | `collect_notify_record.biz_type` 是 `varchar(32)`,存**数字码的字符串形式**("3");接口层仍是数字 | F-26;DDL 不可改 |
|
||
| 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;119 个接口里唯一未实现 |
|
||
|