9d7a8ca845
技术栈(已在 4.1.1 上实测跑通,非纸面选型)
Java 21 + Spring Boot 4.1.1 + MyBatis-Plus 3.5.17(boot4) + PostgreSQL 16.15
+ Redis 7 + Flyway 12.4 + springdoc 3.1.1;前端定 Vue 3 + Element Plus + uni-app
交付内容
M0-01 基础设施 deploy/docker-compose.yml(PG + Redis + MinIO,容器名与端口避让本机既有项目)
M0-02 后端骨架 xt-common + xt-server;统一响应 / 全局异常 / 58 个错误码 /
多租户拦截 / 软删除 / 字段自动填充 / AES-GCM / HMAC 索引 / RSA / JWT
M0-05 租户 开通租户(一次完成建租户 + 发 6 个内置角色 + 建首个管理员)、列表、详情
M0-06 组织 CRUD + 组织树 + ancestor_path 自动维护 + 占用校验
M0-07 PC 登录 RSA 解密 + 图形验证码 + 失败锁定 + 等时比较防账号枚举 + 审计日志
M0-08 登出/当前用户 Token 黑名单(存 SHA-256,TTL 到自然过期)
M0-01 数据库 Flyway 2 个迁移 → 26 张表(20 业务 + 5 标准 + 1 迁移记录)
安全姿态
· 租户拦截器【默认拒绝】:匿名请求触及租户表直接抛 40101,不做「不加条件放行」
· 权限码定义在代码里(PermissionDef / BuiltinRole),不建 sys_permission 表,杜绝前后端漂移
· 匿名接口只靠 @PublicApi 放行,不做路径白名单,避免两套机制各说各话
· 账号不存在时也跑一次 BCrypt(假哈希),拉平响应时间防枚举
· 登录密码 RSA 传输,防止网关/APM 把明文密码记进日志
脚本
scripts/sync-ddl.py 文档 DDL → Flyway 迁移,带 --check 防漂移
scripts/e2e-m0.py M0 验收 36 项,可进 CI
scripts/verify-docs.py 文档一致性(接口数【逐节】核对、错误码、跨文档数字、表格列数)
scripts/dev-login.py RSA 加密登录助手
scripts/dev-clean-e2e.py 清理 E2E 测试数据
文档修正(实测暴露,非人工审查)
· DDL 缺陷:sys_tenant.contact_phone / sys_user.phone / collect_roster.phone
原 varchar(20) 按明文长度建,装不下 AES-GCM 密文(约 52 字符),改 varchar(128)(PRD V2.4)
· 错误码 40007b/40007c 带字母后缀与「code 为数字」的约定冲突,规范化为 40071/40072
· 07-开发实施计划 新增 §9 版本兼容踩坑记录(10 条实录)与 §10 M0 完成情况
2883 lines
115 KiB
Markdown
2883 lines
115 KiB
Markdown
# 接口设计说明书
|
||
|
||
> 版本:V1.0 | 日期:2026-09-28
|
||
> 对应主文档:《数据信息填报系统PRD.md》§8 接口清单(V2.3)
|
||
> 读者:前后端开发、测试
|
||
> **本文档是 PRD §8 的展开实现层**——§8 定义了「有哪些接口」,本文档定义「每个接口收发什么字段」。
|
||
|
||
---
|
||
|
||
## 目录
|
||
|
||
| 节 | 内容 |
|
||
|---|---|
|
||
| §1 | 通用约定(域名、鉴权、响应、分页、幂等、限流、脱敏) |
|
||
| §2 | 枚举字典全集 |
|
||
| §3 | 通用数据结构(Schema 定义) |
|
||
| §4 | 接口总览(**121 个**) |
|
||
| §5 | 认证与账号(13) |
|
||
| §6 | 组织与租户(15) |
|
||
| §7 | 任务与配置(30) |
|
||
| §8 | 名册(12) |
|
||
| §9 | 填报端 · 小程序(16) |
|
||
| §10 | 班主任移动端(10) |
|
||
| §11 | 审核(9) |
|
||
| §12 | 校验 / 导出 / 通知(12) |
|
||
| §13 | **本次补齐的缺口接口(21)** |
|
||
| §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 天(静默续期)
|
||
|
||
### 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` 的名称陷阱**:按机构模板,**「职业技能等级个人申报表」的规范文件名是「工作证明」**。这是机构既有习惯,不要"修正"。
|
||
|
||
### 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` / `DEPUTY_TEACHER` / `STUDENT` / `AUDITOR` |
|
||
| `taskStatus` | `0` 草稿 / `1` 已发布 / `2` **已暂停** / `3` 已截止 / `4` 已归档 |
|
||
| `rosterSubmitStatus` | `0` 未提交 / `1` 已提交 / `2` 已通过 / `3` 已驳回 |
|
||
| `fileValidateStatus` | `0` 待校验 / `1` 通过 / `2` 警告 / `3` 不通过 / `4` 待人工 |
|
||
| `auditNode` | `1` 初审 / `2` 终审 |
|
||
| `auditAction` | `1` 通过 / `2` 驳回 / `3` **撤回** / `4` 代传 |
|
||
| `detectSource` | `1` 文件名 / `2` 形态 / `3` OCR 内容 / `4` VLM / `5` 用户指定 |
|
||
| `compressProfile` | `P1` 证件照 / `P2` 证件翻拍 / `P3` A4 扫描件 / `P4` 截图 / `P5` 兜底 |
|
||
| `notifyChannel` | `1` 微信订阅消息 / `2` 短信 / `3` 公众号模板 |
|
||
| `notifyBizType` | `1` 任务发布 / `2` 临期提醒 / `3` 驳回通知 / `4` 审核通过 / `5` 导出完成 / `6` 催报 / `7` 加入名单审批 |
|
||
| `exportStatus` | `0` 排队中 / `1` 打包中 / `2` 已完成 / `3` 失败 / `4` 已过期 |
|
||
| `cleanupSourceType` | `1` 压缩包 / `2` 文件夹 / `3` 云盘链接 |
|
||
| `cleanupStatus` | `0` 排队 / `1` 解析中 / `2` 处理中 / `3` 已完成 / `4` 失败 |
|
||
|
||
---
|
||
|
||
## §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 天)** |
|
||
|
||
**`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`(含 `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` |
|
||
| `CleanupJobVO` | `id` `jobName` `sourceType` `sourcePath` `targetTaskId` `status` `progress` `totalFiles` `resultPath` `reportJson` `finishTime` |
|
||
| `DictItemVO` | `id` `dictCode` `dictName` `itemCode` `itemName` `sortNo` `parentItemCode` `status` `builtin` |
|
||
| `MessageVO` | `id` `bizType` `title` `content` `read` `linkType` `linkId` `createTime` |
|
||
|
||
---
|
||
|
||
## §4 接口总览
|
||
|
||
> 合计 **121 个**:PRD §8 已定义 96 个 + 追溯矩阵补齐 **21 个** + 客户确认新增 **5 个**,减去客户要求移除的联系方式接口 1 个(标 ⭐)。补齐项见 §4.9 索引,客户确认新增见 §8.7。
|
||
> 「权限」列:`公开` / `学生` / `教师` / `管理` / `运营`(平台方)。
|
||
> 「修补」列指该接口是补齐追溯矩阵审计发现的缺口。
|
||
|
||
| 分类 | 接口数 |
|
||
|---|:--:|
|
||
| 认证与账号 | 13 |
|
||
| 组织与租户 | 15 |
|
||
| 任务与配置 | 30 |
|
||
| 名册 | 12 |
|
||
| 填报端 · 小程序 | 16 |
|
||
| 班主任移动端 | 10 |
|
||
| 审核 | 9 |
|
||
| 校验 / 导出 / 通知 | 12 |
|
||
| 开放接口 | 4 |
|
||
| **合计** | **121** |
|
||
|
||
### 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 任务与配置(30)
|
||
|
||
| | 路径 | 说明 | 权限 |
|
||
|---|---|---|---|
|
||
| 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` | 应用模板 | 管理 |
|
||
| ⭐ POST | `/cleanup/jobs` | **创建整理任务** | 管理 |
|
||
| ⭐ GET | `/cleanup/jobs` | **整理任务列表** | 管理 |
|
||
| ⭐ GET | `/cleanup/jobs/{id}` | **整理进度** | 管理 |
|
||
| ⭐ GET | `/cleanup/jobs/{id}/report` | **体检报告** | 管理 |
|
||
| ⭐ POST | `/cleanup/jobs/{id}/import` | **结果导入为正式任务** | 管理 |
|
||
|
||
### 4.4 名册(12)
|
||
|
||
| | 路径 | 说明 | 权限 |
|
||
|---|---|---|---|
|
||
| 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` | 一键催报未提交者 | 管理/教师 |
|
||
| ⭐ POST | `/mp/roster/join-request` | **提交加入申请** | 学生 |
|
||
| ⭐ GET | `/mp/roster/join-request` | **我的申请状态** | 学生 |
|
||
| ⭐ GET | `/roster/join-requests` | **申请待办列表** | 管理 |
|
||
| ⭐ POST | `/roster/join-requests/{id}/approve` | **审批通过** | 管理 |
|
||
| ⭐ POST | `/roster/join-requests/{id}/reject` | **审批驳回** | 管理 |
|
||
|
||
### 4.5 填报端 · 小程序(16)
|
||
|
||
| | 路径 | 说明 | 权限 |
|
||
|---|---|---|---|
|
||
| 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 ⭐ 本次新增接口索引(21)
|
||
|
||
> 本表是**索引**,不是额外接口——其中 5 个已在上方 §4.3 / §4.7 列出,**不计入总数**。
|
||
> 表内 18 行覆盖 **21 个接口**(含 GET/PUT、GET/POST 合并行)。
|
||
|
||
| | 路径 | 说明 | 详情节 |
|
||
|---|---|---|---|
|
||
| ⭐ | `/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 |
|
||
| ⭐ | `/cleanup/jobs` | 创建整理任务 | §13.2 |
|
||
| ⭐ | `/cleanup/jobs` | 整理任务列表 | §13.2 |
|
||
| ⭐ | `/cleanup/jobs/{id}` | 整理进度 | §13.2 |
|
||
| ⭐ | `/cleanup/jobs/{id}/report` | 体检报告 | §13.2 |
|
||
| ⭐ | `/cleanup/jobs/{id}/import` | 结果导入为正式任务 | §13.2 |
|
||
| ⭐ | `/dicts` | 字典列表 | §13.3 |
|
||
| ⭐ | `/dicts/{code}/items` | 字典项(GET / POST) | §13.3 |
|
||
| ⭐ | `/dicts/items/{id}` | 修改字典项 | §13.3 |
|
||
| ⭐ | `/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 |
|
||
|
||
**新增 21 个的分布**
|
||
|
||
| 归属 | 数量 | 明细 |
|
||
|---|:--:|---|
|
||
| 报考范围 | 2 | plan-scope GET / PUT |
|
||
| 任务状态 | 2 | pause / resume |
|
||
| 审核 | 1 | revoke |
|
||
| 文件处理 | 1 | process(登记遗漏) |
|
||
| 历史整理器 | 5 | cleanup 全套 |
|
||
| 字典 | 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 ⭐ 客户确认后新增(5)
|
||
|
||
> 来源:确认函 **B1**(客户确认采用"申请加入名单 → 管理员审批"通道)。这是本次唯一的功能增量。
|
||
> 详述见 **§8.7**。
|
||
|
||
| | 路径 | 说明 | 权限 |
|
||
|---|---|---|---|
|
||
| ⭐ | `/mp/roster/join-request` | 提交加入申请 / 查看我的申请(GET / POST) | 学生 |
|
||
| ⭐ | `/roster/join-requests` | 申请待办列表 | 管理 |
|
||
| ⭐ | `/roster/join-requests/{id}/approve` | 审批通过(自动建名册项 + 自动绑定) | 管理 |
|
||
| ⭐ | `/roster/join-requests/{id}/reject` | 审批驳回(原因必填) | 管理 |
|
||
|
||
**设计边界(三条)**
|
||
|
||
| # | 边界 | 理由 |
|
||
|:--:|---|---|
|
||
| 1 | **只解决"进名单",不绕过门禁** | 申请期间仍不能填报;审批通过后学生**仍需重新核验**(或凭已绑定 openid 直接放行) |
|
||
| 2 | **证件号仅存 HMAC,不落明文** | 与门禁口径一致;申请记录保留 2 年 |
|
||
| 3 | **驳回必须逐条填原因;批量通过不提供"全选"** | 身份认定环节误操作代价极高,与"批量通过只带绿色项"同源 |
|
||
|
||
**与现有机制的关系**
|
||
|
||
| 机制 | 区别 |
|
||
|---|---|
|
||
| `FR-508` 名册追加导入 | 老师批量加人;申请通道是**学生自助 + 逐条审批** |
|
||
| `FR-504` 统一失败提示 | 申请通道**不破坏**统一提示——`40039`(已在名单中)不泄露新信息 |
|
||
| `FR-504d` 已被他人绑定 | 申请通过后若该证件号已被占用 → `40041`,不覆盖 |
|
||
|
||
---
|
||
|
||
## §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)。
|
||
>
|
||
> **原设计**:门禁失败页展示班主任/管理员的脱敏手机号,学生可一键拨打。
|
||
>
|
||
> **移除理由**:学生在班级群里**本来就有班主任的联系方式**,系统内再放一份:
|
||
> ① 不必要——用户已经知道怎么找人;
|
||
> ② 有维护成本——班主任换人、号码变更都要同步;
|
||
> ③ 多一处隐私暴露面——即使是脱敏号,也意味着系统里要多存一份联系方式数据。
|
||
>
|
||
> **替代**:门禁失败页只保留两条**系统内的动作**——「重新填写」与「申请加入名单」(FR-509)。
|
||
> 纯文案提示(如"如有疑问请联系老师")保留,因为**不涉及任何联系方式数据**。
|
||
|
||
### 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`,用于门禁二次校验 |
|
||
|
||
---
|
||
|
||
## §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`、分页
|
||
|
||
**响应**:`{ list: TenantVO[], total }`
|
||
|
||
### 6.6 `POST /api/v1/tenants`
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|---|---|---|---|
|
||
| `tenantName` | string | ✅ | — |
|
||
| `tenantCode` | string | ✅ | 全局唯一,如 `jxjd` |
|
||
| `tenantType` | int | ✅ | `1` 学校 / `2` 机构 / `3` 企业 |
|
||
| `contactName` / `contactPhone` | string | ✅ | 联系人 |
|
||
| `packageId` | int64 | ✅ | 套餐 |
|
||
| `expireTime` | string | ✅ | 到期时间 |
|
||
| `adminUsername` / `adminRealName` / `adminPhone` | string | ✅ | **同时创建首个管理员** |
|
||
|
||
**响应**:`{ "id": 1002, "adminInitialPassword": "Kp9#vR4t", "miniProgramScene": "t1002" }`
|
||
|
||
### 6.7 `GET /api/v1/tenants/{id}/usage`
|
||
|
||
**Query**:`period`(`2026-09` 或 `2026Q3`)
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"data": {
|
||
"studentCount": 1240, "storageMb": 8420, "ocrCount": 3105, "smsCount": 420,
|
||
"quota": { "studentLimit": 2000, "storageLimitMb": 20480, "ocrLimit": 10000, "smsLimit": 2000 },
|
||
"alerts": [ { "type": "STORAGE", "level": 1, "ratio": "41.1%" } ]
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## §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` | 一寸照片 + 身份证 + 学籍表 + 工作证明 |
|
||
| `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 次/分钟/任务。
|
||
|
||
---
|
||
|
||
### 8.7 名单加入申请(FR-509 · 客户确认后新增)
|
||
|
||
> **背景**:门禁不通过的人原先只能"联系老师",老师再手工加名单。几百人的社会收集场景下,老师负担极重。
|
||
> 本组接口提供一条**受控通道**:学生自助申请 → 管理员审批 → 自动入册。
|
||
>
|
||
> ⚠️ **关键边界**:这条通道**只解决"进名单",不绕过门禁**——申请期间学生仍不能填报。
|
||
|
||
#### 8.7.1 `POST /api/v1/mp/roster/join-request`
|
||
|
||
**权限**:学生(**须已过门禁失败流程**) **限流**:同 openid 每任务 ≤2 次
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|---|---|---|---|
|
||
| `taskId` | int64 | ✅ | — |
|
||
| `realName` | string | ✅ | 姓名(明文存储,业务必需) |
|
||
| `idCardNo` | string | ✅ | 身份证号(**服务端转 HMAC,不落明文**) |
|
||
| `phone` | string | ✅ | 手机号(**仅存 HMAC + 脱敏值**) |
|
||
| `orgHint` | string | — | 自述班级 |
|
||
| `reason` | string | — | 申请说明,如"我是转班过来的" |
|
||
|
||
**响应**
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"data": {
|
||
"requestId": 7001,
|
||
"status": 0,
|
||
"statusText": "已提交,等待学校审核",
|
||
"estimatedHours": 24,
|
||
"remainAttempts": 1
|
||
}
|
||
}
|
||
```
|
||
|
||
**主要错误**
|
||
|
||
| 码 | 场景 | 提示 |
|
||
|---|---|---|
|
||
| ⭐ `40037` | 同任务申请次数超限(>2) | "该任务申请次数已达上限,请联系老师" |
|
||
| ⭐ `40038` | 该证件号已有待审申请 | "你已提交过申请,请耐心等待" |
|
||
| ⭐ `40039` | 该证件号已在名册中 | "你已在名单中,请直接返回重新核验" |
|
||
| `40008` | 任务已截止/暂停 | — |
|
||
|
||
> **`40039` 是必要的**:若证件号已在名册,说明学生**只是输错了姓名**,该引导他回去重填门禁,而不是提交申请。
|
||
> 这一条不违反"统一失败提示"原则——因为学生已经主动提供了正确证件号,此时告知"你已在名单中"**不泄露任何新信息**。
|
||
|
||
#### 8.7.2 `GET /api/v1/mp/roster/join-request`
|
||
|
||
**Query**:`taskId`
|
||
|
||
**响应**
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"data": {
|
||
"list": [
|
||
{ "requestId": 7001, "taskId": 2026090001, "taskName": "2026年9月职业技能认定",
|
||
"status": 0, "statusText": "待审核",
|
||
"realName": "李四", "idCardMask": "3610**********10",
|
||
"orgHint": "24中技数控2班",
|
||
"createTime": "2026-09-28T18:00:00+08:00",
|
||
"auditTime": null, "rejectReason": null,
|
||
"rosterId": null }
|
||
],
|
||
"total": 1
|
||
}
|
||
}
|
||
```
|
||
|
||
**驳回态**:`status: 2` + `rejectReason` 明文返回(学生需要知道原因)。
|
||
|
||
**通过态**:`status: 1` + `rosterId`,并提示"已加入名单,请返回重新核验进入填报"。
|
||
|
||
#### 8.7.3 `GET /api/v1/roster/join-requests`
|
||
|
||
**权限**:管理 **Query**:`taskId`、`status`(默认 `0` 待审)、`orgId`、`keyword`、分页
|
||
|
||
**响应**
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"data": {
|
||
"list": [
|
||
{ "requestId": 7001, "taskId": 2026090001,
|
||
"realName": "李四", "idCardMask": "3610**********10",
|
||
"phoneMask": "138****8888",
|
||
"orgHint": "24中技数控2班",
|
||
"reason": "我是转班过来的",
|
||
"status": 0,
|
||
"createTime": "2026-09-28T18:00:00+08:00",
|
||
"waitHours": 2.5,
|
||
"duplicateHint": null }
|
||
],
|
||
"total": 1,
|
||
"counts": { "pending": 1, "approved": 12, "rejected": 3 },
|
||
"alert": null
|
||
}
|
||
}
|
||
```
|
||
|
||
**`duplicateHint`**:若该证件号与**其他任务的名册**匹配上,返回提示(帮管理员判断是否本校学生):
|
||
|
||
```json
|
||
"duplicateHint": { "type": "IN_OTHER_TASK", "taskName": "2026年3月认定", "realName": "李四" }
|
||
```
|
||
|
||
**`alert`**:单任务待审 > 50 条时返回 `{ "level": 2, "text": "待审申请已达 53 条,建议集中处理" }`(FR-509c)。
|
||
|
||
#### 8.7.4 `POST /api/v1/roster/join-requests/{id}/approve`
|
||
|
||
**权限**:管理 **幂等**:是
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|---|---|---|---|
|
||
| `orgId` | int64 | ✅ | **指定班级**(默认取自述班级,无则必填) |
|
||
| `realName` | string | — | 修正姓名(管理员核对后可改) |
|
||
| `idCardNo` | string | — | 修正证件号 |
|
||
| `seqNo` | int | — | 序号(默认追加到末尾) |
|
||
|
||
**服务端动作(事务内)**
|
||
|
||
```
|
||
1. 校验申请状态 = 待审,否则 40040
|
||
2. 校验该 task 下证件号未被占用,否则 40041
|
||
3. 在 collect_roster 新建名册项:
|
||
real_name / id_card_no / id_card_hash / id_card_mask ← 申请值(或管理员修正值)
|
||
org_id ← 指定班级
|
||
bind_user_id ← 申请人 uid(FR-509g:自动绑定,免重输)
|
||
bind_type ← 3(来自申请)
|
||
ext_json.source ← 'JOIN_REQUEST'
|
||
4. 回写 collect_join_request:status=1, audit_by, audit_time, roster_id
|
||
5. 写操作日志 JOIN_APPROVE
|
||
6. 推送通知 bizType=7
|
||
```
|
||
|
||
**响应**
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"data": {
|
||
"requestId": 7001, "status": 1,
|
||
"rosterId": 9029,
|
||
"rosterCreated": true,
|
||
"notifySent": true,
|
||
"canFillNow": true
|
||
}
|
||
}
|
||
```
|
||
|
||
> **`canFillNow: true`** 表示该 openid 已绑定,学生再次进入**直接放行**。
|
||
|
||
#### 8.7.5 `POST /api/v1/roster/join-requests/{id}/reject`
|
||
|
||
**权限**:管理 **幂等**:是
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|---|---|---|---|
|
||
| `reason` | string | ✅ | **驳回原因(必填)**,会原样推送给学生 |
|
||
| `reasonCode` | string | — | 快捷原因码(`NOT_THIS_SCHOOL` / `INFO_MISMATCH` / `NOT_IN_SCOPE` / `OTHER`) |
|
||
|
||
**响应**:`{ "requestId": 7001, "status": 2, "notifySent": true }`
|
||
|
||
**约束**
|
||
|
||
| 条件 | 结果 |
|
||
|---|---|
|
||
| 已通过后再驳回 | `40042`(如需撤销请走名册删除) |
|
||
| 任务已归档 | `40034` |
|
||
| 批量审批 | **支持**,但**必须逐条确认**(不可一键全通过——涉及身份认定) |
|
||
|
||
> ⚠️ **批量审批的设计取舍**:批量**通过**允许(管理员已看过列表),但**驳回必须逐条填原因**;且批量通过时**不提供"全选"快捷键**。
|
||
> 理由与"批量通过只带绿色项"同源:**身份认定环节的误操作代价极高**。
|
||
|
||
---
|
||
|
||
## §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` 获取直传凭证
|
||
|
||
**幂等**:是
|
||
|
||
**请求**
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|---|---|---|---|
|
||
| `taskId` | int64 | ✅ | — |
|
||
| `materialCode` | string | ✅ | 目标槽位 |
|
||
| `fileName` | string | ✅ | 原始文件名(**可能是乱的,正常**) |
|
||
| `fileSize` | int64 | ✅ | 字节 |
|
||
| `contentType` | string | ✅ | MIME |
|
||
| `source` | int | — | `1` 聊天记录 / `2` 相册 / `3` 手机文件 |
|
||
| `fileHash` | string | — | 客户端 SHA-256(**用于秒传与去重**) |
|
||
|
||
**响应**
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"data": {
|
||
"fileId": 987654321,
|
||
"uploadUrl": "https://oss.xxx.com/raw/1001/2026090001/202609/9f3a....jpg?Signature=...",
|
||
"objectKey": "raw/1001/2026090001/202609/9f3a....jpg",
|
||
"expireSeconds": 900,
|
||
"headers": { "Content-Type": "image/jpeg" },
|
||
"existed": false,
|
||
"clientCompressHint": {
|
||
"enabled": true, "maxLongSide": 1600, "quality": 80, "targetSizeKb": 1024
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
| 字段 | 说明 |
|
||
|---|---|
|
||
| `existed` | `true` = **秒传命中**(同 hash 已存在),前端跳过上传直接进入处理 |
|
||
| `clientCompressHint` | **端侧预压缩建议**。这是第一道防线,能大幅降低上传流量与处理时长;**服务端仍会按规则重压**,端侧压缩只是加速 |
|
||
|
||
> **端侧压缩与「一次编码到位」的关系**:实测 300dpi/q85 即可达标,**端侧压缩只是减少上传量的优化,不是质量决定环节**。服务端始终以原图(或端侧压缩后的图)为输入重新规范,保证输出一致。
|
||
|
||
### 9.6 ⭐ `POST /api/v1/mp/files/{fileId}/process` 文件规范化(核心)
|
||
|
||
**说明**:这是**命名 + 压缩 + 识别 + 校验**的总入口。
|
||
|
||
**请求**
|
||
|
||
| 字段 | 类型 | 默认 | 说明 |
|
||
|---|---|---|---|
|
||
| `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,用于表单预填与校验。**这是唯一返回明文姓名的 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
|
||
}
|
||
}
|
||
```
|
||
|
||
**社会收集**(二期):`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` |
|
||
| **已被导出打包** | ❌ `40033`——**已交付的数据不可追溯修改** |
|
||
| 已归档 | ❌ `40034` |
|
||
| 撤回者非原审核人且非管理员 | ❌ `40301` |
|
||
|
||
**留痕**:写入一条 `action = 3`(撤回)的 `collect_audit_record`,时间轴显示"李老师撤回了通过结论"。
|
||
|
||
### 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`
|
||
|
||
**说明**:任务级全量校验,产出问题清单。
|
||
|
||
| 字段 | 类型 | 默认 | 说明 |
|
||
|---|---|---|---|
|
||
| `taskId` | int64 | ✅ | — |
|
||
| `scope` | string | `ALL` | `ALL` / `ISSUES_ONLY` / `APPOINTED` |
|
||
| `submissionIds` | int64[] | — | `scope=APPOINTED` 时必填 |
|
||
| `rules` | string[] | 全部 | 只跑指定规则(如 `["VAL-02","VAL-04"]`) |
|
||
| `async` | bool | `true` | — |
|
||
|
||
**响应**:`{ "jobId": "v_3f2a", "estimatedSeconds": 30 }`
|
||
|
||
### 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 详情响应**(不新增接口) | ✅ |
|
||
| I2 历史数据整理器 | **§13.2** | ✅ |
|
||
| I3 字典管理 | **§13.3** | ✅ |
|
||
| I4 / I5 日志 | **§13.4** | ✅ |
|
||
| I6 消息中心 | **§13.5** | ✅ |
|
||
| I7 订阅额度登记 | **§13.6** | ✅ |
|
||
|
||
### 13.2 历史数据整理器(5 个)· 差异化获客功能
|
||
|
||
#### `POST /api/v1/cleanup/jobs`
|
||
|
||
**说明**:上传已有压缩包/文件夹,用**与正式收集完全相同的规则**做规范化。
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|---|---|---|---|
|
||
| `jobName` | string | ✅ | 如 `24中技数控1班-历史整理` |
|
||
| `sourceType` | int | ✅ | `1` 压缩包 / `2` 文件夹 / `3` 云盘链接 |
|
||
| `uploadToken` / `objectKey` | string | 条件 | `sourceType=1/2` 时必传(先走通用上传) |
|
||
| `sourceUrl` | string | 条件 | `sourceType=3` 时必传 |
|
||
| `namingTemplate` | string | — | 缺省用系统默认 |
|
||
| `compressConfig` | object | — | 缺省见 3.11 |
|
||
| `rosterTaskId` | int64 | — | **关联到已有任务的名册**(用于证件号纠错与串档检测) |
|
||
|
||
**响应**:`{ "jobId": 301, "status": 0, "estimatedSeconds": 120 }`
|
||
|
||
> `rosterTaskId` 是**关键参数**:没有名册就只能做文件名规范化,**有名单才能做证件号校验、疑似串档检测**——而"疑似串档 1 例"正是报告里最有价值的一条。
|
||
|
||
#### `GET /api/v1/cleanup/jobs`
|
||
|
||
**Query**:`status`、`keyword`、分页 **响应**:`{ list: CleanupJobVO[], total }`
|
||
|
||
#### `GET /api/v1/cleanup/jobs/{id}`
|
||
|
||
**响应**
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"data": {
|
||
"id": 301, "jobName": "24中技数控1班-历史整理", "status": 3, "progress": 100,
|
||
"totalFiles": 112, "processedFiles": 112,
|
||
"parsedStudents": 28, "parsedMaterials": 4,
|
||
"rawSizeMb": 436, "finalSizeMb": 37, "compressRatio": "91.5%",
|
||
"resultPath": "/cleanup/301/result.zip",
|
||
"finishTime": "2026-09-28T16:00:00+08:00"
|
||
}
|
||
}
|
||
```
|
||
|
||
#### `GET /api/v1/cleanup/jobs/{id}/report` ⭐ 体检报告
|
||
|
||
**Query**:`format`(`json` / `xlsx`)
|
||
|
||
**响应(json)**
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"data": {
|
||
"jobId": 301,
|
||
"summary": {
|
||
"totalFiles": 112,
|
||
"nameInvalid": 59, "sizeExceeded": 31, "formatConverted": 56,
|
||
"idcardMismatch": 5, "idcardAutoFixed": 3, "idcardNeedConfirm": 2,
|
||
"crossFileSuspected": 1, "lowResolution": 1, "orphanImages": 0
|
||
},
|
||
"autoHandled": 90,
|
||
"needManual": 1,
|
||
"highlights": [
|
||
{ "type": "CROSS_FILE", "level": 3, "rosterId": 9012, "realName": "张三",
|
||
"title": "疑似串档", "detail": "该生 4 项材料的证件号与名册均不同,且 4 项彼此一致",
|
||
"suggestion": "可能整个文件夹被换过,请核对学生本人" },
|
||
{ "type": "LOW_RESOLUTION", "level": 2, "rosterId": 9008, "realName": "李四",
|
||
"title": "分辨率偏低", "materialCode": "IDCARD",
|
||
"detail": "558×745,同批其他人为 1279×1706", "suggestion": "建议索取原图" }
|
||
],
|
||
"nameFixedSamples": [
|
||
{ "original": "身份证.jpg.jpg", "fixed": "曾伊豪-361029200907295110-身份证.jpg", "rule": "VAL-06" }
|
||
],
|
||
"idcardConflicts": [
|
||
{ "rosterId": 9012, "realName": "张三",
|
||
"rosterValue": "3610292009******10", "fileNameValue": "3610292009******99",
|
||
"ocrValue": "3610292009******10",
|
||
"verdict": "AUTO_FIXED", "verdictText": "OCR 与名册一致,按名册修正文件名" }
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
> **报告里最重要的一句话是 `autoHandled: 90 / needManual: 1`**——「112 个文件里 90 个系统全自动处理,只有 1 个必须你亲自判断」。这是最有说服力的销售话术,也是接口必须算出来的数字。
|
||
|
||
#### `POST /api/v1/cleanup/jobs/{id}/import`
|
||
|
||
**说明**:把整理结果**导入为正式任务**(名册 + 材料一次成型)。
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|---|---|---|---|
|
||
| `taskName` | string | ✅ | 新任务名 |
|
||
| `collectMode` | int | ✅ | `1` / `2` |
|
||
| `batchNo` | string | ✅ | — |
|
||
| `importRoster` | bool | — | 是否把解析出的学员建成名册(默认 true) |
|
||
| `importFiles` | bool | — | 是否把文件挂到填报单上(默认 true) |
|
||
| `markAsSubmitted` | bool | — | 是否直接置为"已提交待审"(默认 true) |
|
||
|
||
**响应**:`{ "taskId": 2026090002, "rosterCount": 28, "fileCount": 112 }`
|
||
|
||
### 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`、`targetId`、`from` / `to`、分页
|
||
|
||
**响应**
|
||
|
||
```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", "createTime": "2026-09-28T16:20:00+08:00" }
|
||
],
|
||
"total": 1
|
||
}
|
||
}
|
||
```
|
||
|
||
**必记的敏感操作**(PRD FR-506 / FR-1202)
|
||
|
||
| `action` | 场景 |
|
||
|---|---|
|
||
| `UPDATE_IDCARD` | **改名册证件号**(留痕强制) |
|
||
| `UPDATE_ROSTER_NAME` | 改名 |
|
||
| `DELETE_ROSTER` | 删名册项 |
|
||
| `PUBLISH_TASK` / `PAUSE_TASK` / `ARCHIVE_TASK` | 任务状态流转 |
|
||
| `REVOKE_AUDIT` | **撤回审核** |
|
||
| `EXPORT_CREATE` | 创建导出 |
|
||
| `PROXY_UPLOAD` | 代传材料 |
|
||
| `VIEW_IDCARD_FULL` | **查看完整身份证号**(单独留痕) |
|
||
| `CONFIG_COMPRESS` | 改压缩配置 |
|
||
|
||
**脱敏**:`beforeVal` / `afterVal` 中的证件号**一律脱敏存储**,即使日志泄露也不暴露明文。
|
||
|
||
#### `GET /api/v1/logs/logins`
|
||
|
||
**Query**:`userId`、`userType`、`result`、`from` / `to`、分页
|
||
|
||
**响应**:`{ list: [{ id, userId, userName, userType, loginType, ip, location, device, result, failReason, loginTime }], total }`
|
||
|
||
| `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` | 管理端创建导出时 | 导出完成 |
|
||
| `JOIN_APPROVE` | 申请加入名单时 | 审批结果 |
|
||
|
||
**额度不足的降级链**
|
||
|
||
```
|
||
微信订阅消息(免费,额度受限)
|
||
↓ 额度耗尽
|
||
公众号模板消息(需关注公众号)
|
||
↓ 未关注
|
||
短信(按条计费,走 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 个码)基础上**新增 38 个**(标 ⭐),本节共 **58 个**(含 `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` | 字典项有下级,不可停用 | 先处理下级 |
|
||
| ⭐ `40037` | **加入申请次数超限**(同 openid 每任务 >2) | 提示联系老师 |
|
||
| ⭐ `40038` | **该证件号已有待审申请** | "你已提交过申请,请耐心等待" |
|
||
| ⭐ `40039` | **该证件号已在名册中** | "你已在名单中,请返回重新核验"(引导回门禁,**不泄露新信息**) |
|
||
| ⭐ `40040` | 申请状态已变更(非待审) | 刷新列表 |
|
||
| ⭐ `40041` | **名册中该证件号已被占用** | 不覆盖,提示管理员核查 |
|
||
| ⭐ `40042` | 申请已通过,不可再驳回 | 如需撤销请走名册删除 |
|
||
| ⭐ `40043` | **文件归属存疑**(文件名指向名册中的另一个人,或归属无法确证) | **阻断提交**,提示"这份材料可能不是你的",引导重新上传(**无"仍然继续"选项**) |
|
||
| ⭐ `40044` | **一期不支持该收集模式 / 认定类型** | 请使用学校收集 + 自主认定(见 PRD「一期范围声明」) |
|
||
| `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.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 各模块表行数合计 | **121** ✓ 与声称一致 |
|
||
| §4 内路径重复 | 无 ✓ |
|
||
| Markdown 表格列数一致性 | 0 行不符 ✓ |
|
||
| 文档引用的字段名 | 116 个唯一值,**全部存在于 DDL** ✓ |
|
||
| 错误码数量 | 58 个(新增 38 个标 ⭐)✓ |
|
||
| 文件规模 | 约 100 KB / 2570 行 |
|
||
|