- 交接摘要补 §一·三十五~§一·三十八:角色与授权、需求收敛(4 角色 / 10 菜单 / 取消二期)、 PC 端重搭后的 UI 修复与卡片式基线、品牌改名与新标识 - PRD 与功能逻辑卷只改标题行与产品代号线;**文件名不动** —— verify-docs.py 按路径读, 全库还有 20+ 处按文件名引用,改名会连带炸脚本 - 原型 index / admin-web / teacher-app / student-app 的品牌区改「深学在线」, admin-web 的 .mark(10 处)换成内嵌的新标 data URI,保持原型"单文件自包含"的约定 - 根 AGENTS.md 与 frontend-pc 的规范同步:接线检查补第 ⑨ 项(SVG 资产是不是合法 XML —— SVG 注释里出现连续两个减号就 not well-formed,图标裂开而控制台一句话都不报)、 「最容易写错的约定」6 -> 7 条(新增品牌标) - deploy/docker-compose.yml 只改头注释里的项目名(compose 的 name 与七牛对象前缀都没动)
130 KiB
接口设计说明书
版本:V1.0 | 日期:2026-09-28 对应主文档:《数据信息填报系统PRD.md》§8 接口清单(V2.3) 读者:前后端开发、测试 本文档是 PRD §8 的展开实现层——§8 定义了「有哪些接口」,本文档定义「每个接口收发什么字段」。
目录
| 节 | 内容 |
|---|---|
| §1 | 通用约定(域名、鉴权、响应、分页、幂等、限流、脱敏) |
| §2 | 枚举字典全集 |
| §3 | 通用数据结构(Schema 定义) |
| §4 | 接口总览(119 个) |
| §5 | 认证与账号(13) |
| §6 | 组织与租户(15) |
| §7 | 任务与配置(25) |
| §8 | 名册(7) |
| §9 | 填报端 · 小程序(16) |
| §10 | 班主任移动端(10) |
| §11 | 审核(9) |
| §12 | 校验 / 导出 / 通知(12) |
| §13 | 本次补齐的缺口接口(16) |
| §14 | 开放接口 · 供认定机构对接(4) |
| §15 | 错误码全集 |
§1 通用约定
1.1 域名与环境
| 环境 | 域名 | 用途 |
|---|---|---|
| 生产 | https://api.tianbaotong.cn/api/v1/** |
正式 |
| 开放接口 | https://api.tianbaotong.cn/open/v1/** |
认定机构对接 |
| 测试 | https://api-test.tianbaotong.cn/api/v1/** |
联调 |
1.2 鉴权
| 端 | 方式 | Header |
|---|---|---|
| PC 管理端 / 班主任 PC | 账号密码 → JWT | Authorization: Bearer <token> |
| 微信小程序(学生 / 班主任) | code2session → JWT |
Authorization: Bearer <token> |
| 开放接口(机构) | appKey + HMAC-SHA256 | X-App-Key / X-Timestamp / X-Nonce / X-Sign |
JWT Payload
{
"uid": 10086,
"tenantId": 1001,
"orgIds": [2001, 2002],
"roles": ["SCHOOL_ADMIN"],
"userType": 2,
"exp": 1790000000
}
| 字段 | 说明 |
|---|---|
uid |
用户 ID |
tenantId |
租户 ID,服务端强制校验,防越租户 |
orgIds |
数据权限范围(本人 / 本班 / 本院系 / 全租户) |
roles |
角色码数组 |
userType |
1 平台运营 / 2 学校管理员 / 3 班主任 / 4 学生 |
Token 有效期:PC 端 8 小时(可续期)/ 小程序端 7 天(静默续期)
X-Tenant-Id:平台运营的「当前学校」(可选,仅平台运营)
平台运营(tenantId = 0)是唯一跨租户的身份,服务端的租户过滤对它整体关闭。于是
「组织架构」「账号管理」这类本身就是租户内的接口,平台不指明学校时会拿到
所有租户混在一起的数据 —— 而这看不出错(只有一所学校有数据时,它与学校管理员看到的完全一样)。
| 项 | 约定 |
|---|---|
| Header | X-Tenant-Id: <tenantId> |
| 谁能用 | 只认平台运营。其它身份带这个头一律忽略(改了也不生效,不是"报错") |
| 作用 | 本次请求的租户上下文换成该租户:/orgs/**、/users/**、/tasks/** 等按它过滤与写入 |
不带 / 传 0 |
不切换(0 视为"平台自己")。平台的租户列表、全量日志等跨租户能力照旧 |
| 非法值 | 非数字、或租户不存在/已停用 → 40001 |
| 与 token 的关系 | 只对平台生效,不能用来访问别的租户(学校管理员带它不会越权) |
| 权限怎么判 | 切换之后不再享受"平台超管"捷径,一律按 PLATFORM_ADMIN 声明的权限码判 —— 所以那些码必须真的够用(例如「查看填报进度看板」要 task:view + progress:viewAll) |
例:
GET /api/v1/orgs/tree+X-Tenant-Id: 1→ 返回租户 1 的组织树; 不带该头 → 返回平台自己租户的组织(空树),不再把各租户的组织混在一起。
1.3 统一响应
{ "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)
{
"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 |
响应
{ "list": [ ], "total": 128, "pageNum": 1, "pageSize": 20 }
注意:分页响应直接放在
data里(不是data.page),与 §8.1 约定一致。
1.5 幂等
需带 Idempotency-Key: <uuid> 请求头的接口:
| 接口 | 说明 |
|---|---|
POST /mp/submissions/{id}/submit |
防重复提交 |
POST /mp/files/upload-token |
防重复建文件记录 |
POST /exports |
防重复打包(避免重复计费) |
POST /mp/roster/match |
防重放 |
所有 POST 审核动作 |
防重复审批 |
幂等窗口:24 小时。重复请求返回 首次结果 + code: 40009,data.idempotentReplay: true。
1.6 限流
| 场景 | 阈值 | 超限响应 |
|---|---|---|
| 登录 | 5 次/分钟/IP + 账号锁定 5 次 | 42901 |
| 门禁核验 | 同 openid 10 次/小时;同 IP 20 次/分钟 | 40071 |
| 上传凭证 | 200 次/分钟/用户 | 42901 |
| OCR | 30 次/分钟/用户 | 42901 |
| 催报 | 10 次/分钟/任务 | 42901 |
| 导出创建 | 5 次/分钟/租户 | 42901 |
门禁限流是安全设计的一部分(防身份证号枚举探测),阈值调低是刻意的。上线后按真实人数标定。
1.7 脱敏规则
| 字段 | 明文传入 | 响应返回 |
|---|---|---|
| 身份证号 | ✅ 允许(HTTPS,仅门禁/导入场景) | ❌ 一律返回脱敏值 idCardMask,形如 3610292009******10 |
| 手机号 | ✅ 允许 | ❌ 返回脱敏值 138****8888 |
| 姓名 | ✅ | ✅ 明文(业务必需) |
例外:GET /audit/submissions/{id} 等审核场景,管理员/班主任可见完整身份证号(审核必需),但仍不返回明文——返回 idCardFull 需通过 GET /audit/submissions/{id}/idcard 单独获取,该接口单独记录审计日志。
这条设计的意义:审核人员看材料图片时不需要抄身份证号,真需要核验时走单独接口、留痕可追溯。
1.8 其他约定
| 项 | 约定 |
|---|---|
| ID 类型 | int64,JSON 中以 number 传输(生成规则保证 < 2^53) |
| 时间 | ISO 8601 带时区,2026-09-28T14:30:00+08:00;日期型 2026-09-28 |
| 体积 | 统一 KB(finalSizeKb),原始字节用 rawSizeBytes |
| 比例 | 字符串百分比 "97.1%"(避免前端再乘 100) |
| 空值 | 统一 null,不用空字符串 "" |
| 枚举 | 数字码(与 DDL smallint 对齐),文案由前端映射;唯一例外:materialCode 用字符串码(可读性优先,见 §2.1) |
| 排序 | 数组顺序即业务顺序,不额外传 sortNo 以外的字段 |
| 批量上限 | 单次批量操作 ≤ 200 条;超限返回 40011 |
布尔字段命名:统一 isXxx / hasXxx / needXxx,不用 0/1(JSON 原生 true/false)。
§2 枚举字典全集
与数据库
smallint列严格对齐。「无」是合法业务值,见 §2.6。
2.1 材料类型 materialCode(字符串码)
| 编码 | 名称 | 别名(识别用) | 默认压缩档 |
|---|---|---|---|
PHOTO1 |
一寸照片 | 一寸电子照片 / 证件照 / 免冠照 / 1寸照片 | P1 |
IDCARD |
身份证 | 身份证正面 / 身份证反面 / 身份证人像面 | P2 |
SCHOOL_ROLL |
学籍表 | 学籍证明 / 在读证明 / 在校证明 | P4 |
XJW_BA |
学信网备案表 | 学信网 / 学历备案表 / 电子注册备案表 | P4 |
WORK_PROOF |
工作证明 | 个人申报表 / 职业技能等级个人申报表 | P3 |
OTHER_PROOF |
其它佐证材料 | 其他 / 补充材料 | P3 |
⚠️
WORK_PROOF的名称陷阱:按机构模板,「职业技能等级个人申报表」的规范文件名是「工作证明」。这是机构既有习惯,不要"修正"。
⚠️ 一期实际只有 4 类材料:
PHOTO1/IDCARD/SCHOOL_ROLL/WORK_PROOF。XJW_BA与OTHER_PROOF是需求收敛(2026-10)之前留下的编码 —— 历史数据读得到、但"新增材料"选不到(GET /api/v1/material-catalog的可选项不含它们), 内置材料模板也只剩TPL_SCHOOL_ROLL一个(原TPL_SOCIAL/TPL_ZHUANXIANG已删)。 这样处理是为了不让历史任务里的材料项悬空:删编码会让已配的任务读不出材料名。
2.2 收集模式 collectMode
| 值 | 含义 | 审核流 |
|---|---|---|
1 |
学校收集 | 学生 → 班主任初审 → 管理员终审 |
2 |
社会收集 | 提交后直接进管理员终审队列(跳过初审)。⚠️ 一期不启用 |
2.3 认证类型 certType
| 值 | 含义 | 面向谁 | 特有约束 |
|---|---|---|---|
1 |
自主认定 | 本单位职工 / 本校学生 | 要学籍表 |
2 |
社会评价 | 社会人员 | 材料为学信网备案表 + 其它佐证(无学籍表)。⚠️ 一期不启用(零样本材料) |
3 |
专项职业能力 | 专项能力考核 | ⚠️ 无工种 / 无等级 / 无补考科目(0 科)。⚠️ 一期不启用 |
⚠️
社会评价(认定类型)≠社会收集(collectMode): 前者决定填什么材料,后者决定谁审核(有无班主任初审层)。二者可任意组合。
2.4 填报单状态 submissionStatus
| 值 | 含义 | 可流转到 |
|---|---|---|
0 |
草稿 | 1 |
1 |
待初审 | 2 / 4 |
2 |
待终审 | 3 / 4 |
3 |
已通过 | 5 |
4 |
已驳回 | 1(学生重提交) |
5 |
已归档 | — |
2.5 问题级别 severity
| 值 | 含义 | UI 色 | 阻断提交 |
|---|---|---|---|
1 |
信息 | 蓝 | 否 |
2 |
警告 | 黄 | 否(可 confirmWarning 强提) |
3 |
错误 | 红 | 是 |
2.6 「无」的语义(三条铁律)
这是最容易实现错的地方,单列说明。
| 场景 | 错误做法 | 正确做法 |
|---|---|---|
| 学历为"无" | 存 null |
存字符串 "无"——它是合法业务值 |
| 专项职业能力无工种 | 存 null |
存 null + certType=3,由 certType 决定字段是否参与校验 |
| 补考科目为"无" | 存 null |
存 [] 空数组(0 科是合法值) |
判定原则:「用户选择了无」与「用户没填」必须能区分。前者是有效数据,后者是缺失。
2.7 其他枚举
| 枚举 | 取值 |
|---|---|
userType |
1 平台运营 / 2 学校管理员 / 3 班主任 / 4 学生 |
roleCode |
PLATFORM_ADMIN / SCHOOL_ADMIN / HEAD_TEACHER / STUDENT |
taskStatus |
0 草稿 / 1 收集中 / 2 已截止 / 3 已归档 / 4 已暂停 (与后端 CollectTask.STATUS_* 及 DDL 列注释一致) |
rosterSubmitStatus |
0 未提交 / 1 已提交 / 2 已通过 / 3 已驳回 |
fileValidateStatus |
0 待校验 / 1 通过 / 2 警告 / 3 不通过 / 4 待人工 |
auditNode |
1 初审 / 2 终审 |
auditAction |
1 通过 / 2 驳回 / 3 批量通过 / 4 批量驳回 / 5 转交 / 6 代传 / 7 撤回 |
detectSource |
1 文件名 / 2 形态 / 3 OCR 内容 / 4 VLM / 5 用户指定 |
compressProfile |
P1 证件照 / P2 证件翻拍 / P3 A4 扫描件 / P4 截图 / P5 兜底 |
notifyChannel |
1 微信订阅消息 / 2 短信 / 3 公众号模板 |
notifyBizType |
1 任务发布 / 2 临期提醒 / 3 驳回通知 / 4 审核通过 / 5 催报 / 6 导出完成 |
exportStatus |
0 排队中 / 1 打包中 / 2 已完成 / 3 失败 / 4 已过期 |
✅
notifyBizType的5/6曾经在本文件内部对调过(已修正,2026-09-30 客户确认): 本表旧版写「5导出完成 /6催报」,与 §13.5 消息中心示例、落库值collect_notify_record.biz_type(5催报 /6导出完成)不一致 —— 若按旧表实现,催报会被写成"导出完成",催报去重会静默失效(老师点两下学生收到两条), 而排查时从数据上完全看不出是码对不上。现全文档统一为「5催报 /6导出完成」。 记录见《08-完成度盘点与问题清单》F-30。
§3 通用数据结构
下列对象在多个接口中复用。字段名与数据库列名一一对应(下划线转驼峰),便于后端直接映射,避免二次改名。 表格中「↔ 列」列标注该字段对应的 DDL 列,
—表示计算字段。
3.1 TaskVO 任务
| 字段 | 类型 | ↔ 列 | 说明 |
|---|---|---|---|
id |
int64 | id |
任务 ID |
taskCode |
string | task_code |
任务编码,如 2026090001 |
taskName |
string | task_name |
任务名称 |
collectMode |
int | collect_mode |
1 学校 / 2 社会 |
batchNo |
string | batch_no |
批次号,如 202609 |
majorName |
string | major_name |
专业名称 |
jobName |
string | job_name |
职业(工种)名称 |
jobLevel |
string | job_level |
等级,如 四级 |
certType |
int | cert_type |
见 §2.3 |
startTime / endTime |
string | start_time / end_time |
填报起止 |
auditConfig |
object | audit_config |
见 3.2 |
namingTemplate |
string | naming_template |
命名模板 |
compressConfig |
object | compress_config |
见 3.11 |
rosterRequired |
bool | roster_required |
是否强制门禁(当前恒为 true) |
status |
int | status |
见 §2.7 taskStatus |
publishedTime |
string | published_time |
发布时间 |
stat |
object | — | 统计快照,见 3.3 |
createTime |
string | create_time |
— |
3.2 AuditConfigVO 审核配置
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
needClassAudit |
bool | true |
是否需班主任初审(社会收集恒为 false)。一期不启用社会收集 |
needAdminAudit |
bool | true |
是否需管理员终审 |
classAuditTimeoutHours |
int | 48 |
初审超时提醒 |
adminAuditTimeoutHours |
int | 72 |
终审超时提醒 |
maxRejectCount |
int | 3 |
驳回次数上限,超限升级管理员 |
allowProxyUpload |
bool | true |
允许班主任代传 |
batchApproveOnlyGreen |
bool | true |
批量通过只带无问题项(不可关闭) |
3.3 TaskStatVO 任务统计
| 字段 | 类型 | 说明 |
|---|---|---|
rosterTotal |
int | 应填人数(名册行数) |
submittedCount |
int | 已提交数 |
approvedCount |
int | 已通过数 |
pendingCount |
int | 待审数 |
rejectedCount |
int | 已驳回数 |
unsubmittedCount |
int | 未提交数 |
submitRate |
string | 已提交率(submittedCount / rosterTotal) |
passRate |
string | 通过率(approvedCount / rosterTotal) |
abnormalCount |
int | 有红/黄问题的单数 |
avgAuditHours |
number | 平均处理时长(小时) |
⚠️
submitRate与passRate必须分开报。原型定稿的口径是「完成率 82% 是已提交率 23/28」,不要合并成一个数字——已提交≠已通过。
3.4 MaterialDefVO 材料项定义
| 字段 | 类型 | ↔ 列 | 说明 |
|---|---|---|---|
id |
int64 | id |
— |
materialCode |
string | material_code |
见 §2.1 |
materialName |
string | material_name |
展示名 |
aliases |
string[] | aliases |
别名集合(识别用) |
namePattern |
string | name_pattern |
命名模板,如 {姓名}-{证件号}-{材料名}{后缀} |
required |
bool | required |
是否必传 |
visibleWhen |
object | visible_when |
条件显示,如 { "field": "is_student", "op": "eq", "value": true } |
fileTypes |
string[] | file_types |
格式白名单 ["jpg","jpeg","png","pdf","heic"] |
maxSizeKb |
int | max_size_kb |
体积上限(默认 1024) |
minSizeKb |
int | — | 体积下限保护,低于此值提示"可能不是原图" |
compressProfile |
string | compress_profile |
P1~P5 |
minLongSide |
int | min_long_side |
分辨率下限(长边 px) |
minQuality |
int | min_quality |
压缩质量下限(默认 70) |
targetDpi |
int | target_dpi |
目标 DPI(默认 300) |
fixedSize |
object | fixed_size |
固定尺寸,如 { "width": 295, "height": 413 } |
outputFormat |
string | output_format |
输出格式;null = 保持原格式 |
maxCount |
int | max_count |
最多可传份数 |
mergeEnabled |
bool | merge_enabled |
是否允许多图合并为一份(如身份证正反面) |
ocrType |
string | ocr_type |
IDCARD / NONE |
sampleUrl |
string | sample_url |
示例图 |
tipText |
string | tip_text |
来源引导语(显示在学生端) |
sortNo |
int | sort_no |
排序 |
3.5 FormSchemaVO 表单定义
| 字段 | 类型 | 说明 |
|---|---|---|
version |
int | 版本号,已发布任务改表单必须递增 |
isCurrent |
bool | 是否当前生效版本 |
fields |
array | 字段数组,见下 |
fields[].key |
string | 字段键,内置字段与 collect_submission 列名一致 |
fields[].label |
string | 展示标签 |
fields[].type |
string | text / number / select / cascade / date / idcard / phone / bool |
fields[].builtin |
bool | 是否内置字段(内置不可删除) |
fields[].locked |
bool | 是否锁定(学生不可改) |
fields[].required |
bool | 必填 |
fields[].dictCode |
string | 选项来源字典码 |
fields[].options |
array | 手填选项 |
fields[].validate |
object | { "regex": "...", "min": 0, "max": 0, "message": "..." } |
fields[].visibleWhen |
object | 条件显示规则 |
fields[].tip |
string | 字段提示 |
fields[].sortNo |
int | 排序 |
内置字段清单(21 个,builtin: true)
| 分类 | 字段 key | 学生端权限 |
|---|---|---|
| 身份 | real_name、id_card_no |
🔒 锁定 |
| 报考 | occupation、worktype、job_level |
🔒 锁定 |
| 学历 | education、is_student、school、major、enroll_date |
✏️ 可核对修改 |
| 工作 | work_unit、work_years |
✏️ 可核对修改 |
| 证书 | cert_occupation、cert_level、cert_no |
✏️ 可核对修改 |
| 补考 | is_retake、retake_theory、retake_practice、retake_overall |
✏️ 可核对修改 |
| 联系 | phone |
✏️ 可核对修改 |
锁定三项的理由:姓名、身份证号、报考三项一旦改动,会与名册、报考范围、导出模板全部失配,必须由老师改。 改动留痕:学生修改任一预填字段,写入
collect_submission.changed_fields(JSON 数组),审核时高亮显示。
3.6 RosterVO 名册项
| 字段 | 类型 | ↔ 列 | 说明 |
|---|---|---|---|
id |
int64 | id |
名册项 ID |
seqNo |
int | seq_no |
序号 |
studentNo |
string | student_no |
学号 |
realName |
string | real_name |
姓名 |
namePinyin |
string | name_pinyin |
姓名拼音(形近字纠错用) |
idCardNo |
string | — | ⚠️ 仅导入时可传,响应一律返回 idCardMask |
idCardMask |
string | id_card_mask |
脱敏证件号 |
idCardHash |
string | id_card_hash |
哈希(核验用,不可逆) |
phone |
string | — | 手机号(响应脱敏) |
phoneMask |
string | — | 脱敏手机号 |
orgId / orgName |
int64 / string | org_id |
所属班级 |
seqNo |
int | seq_no |
— |
occupation / worktype / jobLevel |
string | ext_json |
报考三项(来自导入,非独立字段) |
extJson |
object | ext_json |
其它列 |
bindStatus |
int | bind_user_id |
0 未绑定 / 1 已绑定 |
bindTime |
string | bind_time |
绑定时间 |
bindType |
int | bind_type |
1 本人 / 2 代填 |
submitStatus |
int | submit_status |
见 §2.7 |
3.7 SubmissionVO 填报单
| 字段 | 类型 | ↔ 列 | 说明 |
|---|---|---|---|
id |
int64 | id |
填报单 ID |
taskId |
int64 | task_id |
— |
rosterId |
int64 | roster_id |
名册项 |
formVersion |
int | form_version |
填报时的表单版本 |
formData |
object | form_data |
表单值(内置字段在此冗余一份快照) |
extrasJson |
object | extras_json |
扩展字段值 |
changedFields |
string[] | changed_fields |
学生改过的预填字段列表(审核高亮用) |
status |
int | status |
见 §2.4 |
rejectCount |
int | reject_count |
累计驳回次数 |
lastRejectReason |
object | last_reject_reason |
{ reasonCode, reasonText } |
redCount / yellowCount |
int | red_count / yellow_count |
红/黄问题数 |
materials |
array | — | MaterialFileVO[](见 3.8),仅详情接口返回 |
issues |
array | — | ValidationIssueVO[](见 3.9),仅详情接口返回 |
submitTime |
string | submit_time |
提交时间 |
classAudit |
object | — | 初审信息 { auditorName, auditTime, action } |
adminAudit |
object | — | 终审信息 |
createTime |
string | create_time |
— |
展开字段(formData 内的扁平字段,与 DDL 列同名)
real_name · id_card_no · occupation · worktype · job_level · education · is_student · school · major · enroll_date · work_unit · is_retake · retake_theory · retake_practice · retake_overall · work_years · cert_occupation · cert_level · cert_no
这些字段在
collect_submission里是正式列(不是 JSON),所以可以直接WHERE education = '无'做统计。extras_json只存扩展字段。
3.8 MaterialFileVO 材料文件
| 字段 | 类型 | ↔ 列 | 说明 |
|---|---|---|---|
id |
int64 | id |
文件 ID |
materialCode |
string | material_code |
所属槽位 |
version |
int | version |
版本号 |
isCurrent |
bool | is_current |
是否当前版本 |
originalName |
string | original_name |
学生原始文件名 |
standardName |
string | standard_name |
规范文件名 |
fileExt |
string | file_ext |
扩展名 |
mimeType |
string | mime_type |
— |
rawSizeKb |
number | raw_size |
原始体积(KB) |
finalSizeKb |
number | final_size |
处理后体积(KB) |
compressRatio |
string | — | 压缩率,如 "97.1%" |
previewUrl |
string | — | 预览签名 URL(有效期 30 分钟) |
width / height |
int | width |
像素尺寸 |
dpi |
int | dpi |
实际 DPI |
ocrStatus |
int | ocr_status |
0 未跑 / 1 成功 / 2 失败 / 3 跳过 |
ocrConfidence |
number | ocr_confidence |
置信度 0–1 |
detectedMaterialCode |
string | detected_material_code |
系统识别结果 |
detectConfidence |
number | detect_confidence |
识别置信度 |
detectSource |
int | detect_source |
见 §2.7 detectSource |
detectConflict |
bool | detect_conflict |
识别结果与所在槽位冲突 |
needManualPlace |
bool | need_manual_place |
需人工分拣 |
vlmCalled |
bool | vlm_called |
是否调用过 VLM(成本监控用) |
validateStatus |
int | validate_status |
见 §2.7 fileValidateStatus |
issues |
array | — | 该文件的 ValidationIssueVO[] |
source |
int | source |
1 本人上传 / 2 班主任代传 / 3 管理员代传 / 4 整理器导入(该模块已取消,仅历史数据) |
rawExpireTime |
string | raw_expire_time |
原图过期时间(默认 30 天) |
✅
collect_file.source的口径曾经有两套,已确认(2026-09-30 客户确认):本表旧版写 "从哪个入口选的"(1聊天记录 /2相册 /3手机文件 /4代传),与建表注释 (V1__business_tables.sql)冲突。以建表注释为准("谁传的"):§10.6 要求代传可追溯, 要回答的是"谁替他传的、为什么",而"从哪个入口选的"会随同一个学生的操作路径变化, 统计上没有意义。实现见CollectFile.SOURCE_*常量。记录见《08》F-38 / F-39。
ownership.verdict 取值(对应 PRD §6.1 归属裁决矩阵)
| verdict | 含义 | blockSubmit | 学生端表现 |
|---|---|---|---|
NO_IDCARD_IN_NAME |
文件名无证件号(正常) | false | 无提示 |
CONFIRMED_SAME |
证件号 = 名册值 | false | 无提示 |
BELONGS_TO_OTHER |
证件号指向名册中的另一个人 | true | 阻断,提示文件名对应的是谁 |
UNKNOWN_IDCARD |
证件号合法但名册查不到 | 视 OCR 而定 | 无 OCR 佐证时阻断 |
CHECKSUM_INVALID |
校验位失败(只是写错了) | false | 信息级"已按名册修正" |
NO_ROSTER |
本次无名单可依 | false | 标黄 |
⚠️
canForce: false是硬约定:归属问题不提供"仍然继续"。 与VAL-08(分辨率偏低)这类警告不同——那种可以"仍然使用",这个不行。
detectSource 的展示约定:审核辅助显示「系统判定:身份证 92%」,其中档位由 detectSource 决定可信度标注:
detectSource |
展示文案 |
|---|---|
1 文件名 |
文件名匹配 |
2 形态 |
形态识别 |
3 OCR 内容 |
内容识别 |
4 VLM |
智能识别(置信度 < 0.8 时不显示,避免误导) |
3.9 ValidationIssueVO 校验问题
| 字段 | 类型 | ↔ 列 | 说明 |
|---|---|---|---|
id |
int64 | id |
— |
materialCode |
string | — | 关联材料(可为空) |
fileId |
int64 | file_id |
关联文件 |
ruleCode |
string | rule_code |
如 VAL-05 |
severity |
int | severity |
见 §2.5 |
fieldKey |
string | field_key |
关联字段(表单类问题) |
title |
string | title |
标题 |
detail |
string | detail |
详情 |
expectedVal |
string | expected_val |
期望值(脱敏) |
actualVal |
string | actual_val |
实际值(脱敏) |
autoFixed |
bool | auto_fixed |
是否已自动修复 |
resolved |
bool | resolved |
是否已解决 |
resolvedBy |
string | resolved_by |
处理人 |
3.10 AuditRecordVO 审核记录
| 字段 | 类型 | 说明 |
|---|---|---|
id |
int64 | — |
auditNode |
int | 1 初审 / 2 终审 |
auditorId / auditorName |
int64 / string | 审核人 |
action |
int | 见 §2.7 auditAction(撤回是 7;3 是批量通过 —— 旧版此表把两者写混过) |
reasonCode |
string | 驳回原因码(见 PRD 附录 C) |
reasonText |
string | 手填原因 |
materialCodes |
string[] | 关联材料 |
durationSec |
int | 从提交到本次操作的耗时 |
createTime |
string | — |
3.11 CompressConfigVO 压缩配置
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
defaultMaxSizeKb |
int | 1024 |
全局体积上限(D1 决策:无例外) |
defaultTargetDpi |
int | 300 |
A4 扫描件目标 DPI |
defaultQuality |
int | 85 |
默认质量 |
minQuality |
int | 70 |
质量下限(低于此值标记"强制压缩") |
hardFloorDpi |
int | 200 |
DPI 硬下限 |
maxPixels |
int | 50000000 |
超大图熔断阈值(5000 万像素) |
keepPngForScreenshot |
bool | true |
截图类保持 PNG 不转 JPG |
markWhenForced |
bool | true |
强制压缩时打标记进抽检清单 |
3.12 其余对象(简表)
| 对象 | 字段 |
|---|---|
OrgVO |
id parentId orgName orgType orgCode ancestorPath leaderUserId leaderName sortNo status children[] studentCount |
TenantVO |
id tenantCode tenantName tenantType packageId packageName expireTime status logoUrl configJson usage{ studentCount, storageMb, ocrCount, smsCount } |
UserVO |
id username realName phone orgId orgName roleCodes[] userType status lastLoginTime |
ExportTaskVO |
id taskId exportName scopeJson splitSizeMb desensitize status progress fileCount fileSizeMb resultFiles[]{ name, sizeMb, url } errorMsg expireTime finishTime |
NotifyRecordVO |
id taskId channel bizType receiverId receiverName receiverRole templateCode content status failReason sendTime |
DictItemVO |
id dictCode dictName itemCode itemName sortNo parentItemCode status builtin |
MessageVO |
id bizType title content read linkType linkId createTime |
§4 接口总览
合计 119 个:PRD §8 已定义 96 个 + 追溯矩阵补齐 16 个 + 「角色与授权」8 个,减去客户要求移除的联系方式接口 1 个(标 ⭐)。 补齐项见 §4.9 索引,角色与授权见 §4.12 与 §5.10。 ⚠️ 需求收敛(2026-10)的减法:原「客户确认新增 5 个」(加入申请,确认函 B1)整条通道取消, 原「补齐 21 个」中的「历史整理器 5 个」随模块删除 → 21 − 5 = 16,故 96 + 16 + 8 − 1 = 119。 「权限」列:
公开/学生/教师/管理/运营(平台方)。 「修补」列指该接口是补齐追溯矩阵审计发现的缺口。
| 分类 | 接口数 |
|---|---|
| 认证与账号 | 13 |
| 组织与租户 | 15 |
| 任务与配置 | 25 |
| 名册 | 7 |
| 填报端 · 小程序 | 16 |
| 班主任移动端 | 10 |
| 审核 | 9 |
| 校验 / 导出 / 通知 | 12 |
| 开放接口 | 4 |
| ⭐ 角色与授权 | 8 |
| 合计 | 119 |
4.1 认证与账号(13)
| 路径 | 说明 | 权限 | |
|---|---|---|---|
| POST | /auth/login |
PC 账号密码登录 | 公开 |
| POST | /auth/logout |
退出 | 全部 |
| GET | /auth/me |
当前用户 + 权限菜单 | 全部 |
| PUT | /auth/password |
修改密码 | 全部 |
| POST | /mp/login |
小程序 code 换 session | 公开 |
| POST | /mp/bind-phone |
微信手机号快捷绑定 | 学生/教师 |
| POST | /mp/roster/match |
门禁核验 | 学生 |
| GET | /users |
用户列表 | 管理 |
| POST | /users |
新增用户 | 管理 |
| PUT | /users/{id} |
修改用户 | 管理 |
| DELETE | /users/{id} |
停用用户 | 管理 |
| POST | /users/import |
批量导入 | 管理 |
| POST | /users/{id}/reset-password |
重置密码 | 管理 |
4.2 组织与租户(15)
| 路径 | 说明 | 权限 | |
|---|---|---|---|
| GET | /orgs/tree |
组织树 | 管理 |
| POST | /orgs |
新增组织 | 管理 |
| PUT | /orgs/{id} |
修改 | 管理 |
| DELETE | /orgs/{id} |
删除 | 管理 |
| POST | /orgs/import |
批量导入 | 管理 |
| GET | /tenants |
租户列表 | 运营 |
| POST | /tenants |
开通租户 | 运营 |
| PUT | /tenants/{id} |
修改租户/套餐 | 运营 |
| GET | /tenants/{id}/usage |
用量统计 | 运营 |
| ⭐ GET | /dicts |
字典列表 | 管理 |
| ⭐ GET | /dicts/{code}/items |
字典项 | 管理 |
| ⭐ POST | /dicts/{code}/items |
新增字典项 | 管理 |
| ⭐ PUT | /dicts/items/{id} |
修改字典项 | 管理 |
| ⭐ GET | /logs/operations |
操作日志 | 管理 |
| ⭐ GET | /logs/logins |
登录日志 | 管理 |
4.3 任务与配置(25)
| 路径 | 说明 | 权限 | |
|---|---|---|---|
| GET | /tasks |
任务列表 | 管理 |
| POST | /tasks |
创建任务 | 管理 |
| GET | /tasks/{id} |
任务详情 | 管理/教师 |
| PUT | /tasks/{id} |
修改任务 | 管理 |
| DELETE | /tasks/{id} |
删除(仅草稿) | 管理 |
| POST | /tasks/{id}/publish |
发布 | 管理 |
| POST | /tasks/{id}/close |
截止 | 管理 |
| POST | /tasks/{id}/archive |
归档 | 管理 |
| ⭐ POST | /tasks/{id}/pause |
暂停 | 管理 |
| ⭐ POST | /tasks/{id}/resume |
恢复 | 管理 |
| POST | /tasks/{id}/clone |
克隆 | 管理 |
| GET | /tasks/{id}/orgs |
覆盖班级与班主任 | 管理 |
| PUT | /tasks/{id}/orgs |
设置覆盖班级/班主任 | 管理 |
| ⭐ GET | /tasks/{id}/plan-scope |
报考范围读取 | 管理 |
| ⭐ PUT | /tasks/{id}/plan-scope |
报考范围确认 | 管理 |
| GET | /tasks/{id}/form-schema |
表单定义 | 管理 |
| PUT | /tasks/{id}/form-schema |
保存表单(版本化) | 管理 |
| GET | /tasks/{id}/materials |
材料项列表 | 管理 |
| POST | /tasks/{id}/materials |
新增材料项 | 管理 |
| PUT | /materials/{id} |
修改材料项 | 管理 |
| DELETE | /materials/{id} |
删除材料项 | 管理 |
| GET | /tasks/{id}/statistics |
任务统计 | 管理/教师 |
| GET | /tasks/{id}/qrcode |
填报二维码 | 管理 |
| GET | /rule-templates |
规则模板列表 | 管理 |
| POST | /rule-templates/{id}/apply |
应用模板 | 管理 |
4.4 名册(7)
| 路径 | 说明 | 权限 | |
|---|---|---|---|
| POST | /tasks/{id}/roster/import |
导入(返回体检报告) | 管理 |
| GET | /tasks/{id}/roster |
名册分页 | 管理/教师 |
| PUT | /roster/{rid} |
修改名册项 | 管理 |
| DELETE | /roster/{rid} |
删除名册项 | 管理 |
| GET | /tasks/{id}/roster/template |
下载导入模板 | 管理 |
| POST | /roster/{rid}/notify |
定向催报 | 管理/教师 |
| POST | /tasks/{id}/roster/notify-unsubmitted |
一键催报未提交者 | 管理/教师 |
4.5 填报端 · 小程序(16)
| 路径 | 说明 | 权限 | |
|---|---|---|---|
| GET | /mp/tasks |
我的任务列表 | 学生 |
| GET | /mp/tasks/{id}/form |
表单结构 + 已填数据 | 学生 |
| POST | /mp/submissions |
创建/获取草稿 | 学生 |
| PUT | /mp/submissions/{id} |
保存草稿 | 学生 |
| POST | /mp/submissions/{id}/submit |
提交 | 学生 |
| POST | /mp/submissions/{id}/withdraw |
撤回(未审核前) | 学生 |
| GET | /mp/submissions/{id}/progress |
进度与问题清单 | 学生 |
| POST | /mp/files/upload-token |
OSS 直传凭证 | 学生 |
| ⭐ POST | /mp/files/{fileId}/process |
文件规范化(命名+压缩+识别) | 学生 |
| POST | /mp/files/batch-process |
批量处理(多材料) | 学生 |
| GET | /mp/files/{fileId} |
预览签名 URL | 学生/教师 |
| DELETE | /mp/files/{fileId} |
删除材料(软删) | 学生 |
| POST | /mp/ocr/idcard |
身份证 OCR | 学生 |
| ⭐ GET | /mp/messages |
消息中心 | 学生 |
| ⭐ POST | /mp/messages/read |
标记已读 | 学生 |
| ⭐ POST | /mp/subscribe |
订阅消息额度登记 | 学生 |
注:本表 16 行,其中
/mp/files/{fileId}/process与/mp/files/batch-process在 PRD §8.6 中分别登记于 §8.6 与 §8.9,此处合并列出。
4.6 班主任移动端(10)
| 路径 | 说明 | 权限 | |
|---|---|---|---|
| GET | /mp/teacher/dashboard |
班级看板 | 教师 |
| GET | /mp/teacher/audit/todos |
待审列表(卡片流) | 教师 |
| GET | /mp/teacher/audit/{submissionId} |
学生详情 | 教师 |
| POST | /mp/teacher/audit/{submissionId}/approve |
通过 | 教师 |
| POST | /mp/teacher/audit/{submissionId}/reject |
驳回 | 教师 |
| POST | /mp/teacher/audit/batch-approve |
一键通过全部绿色项 | 教师 |
| POST | /mp/teacher/proxy-upload |
代传材料 | 教师 |
| POST | /mp/teacher/notify/unsubmitted |
一键催报 | 教师 |
| GET | /mp/teacher/notify/records |
催报历史 | 教师 |
| ⭐ GET | /mp/teacher/messages |
教师消息中心 | 教师 |
4.7 审核(9)
| 路径 | 说明 | 权限 | |
|---|---|---|---|
| GET | /audit/todos |
待办列表(红黄绿) | 教师/管理 |
| GET | /audit/submissions/{id} |
填报单详情 | 教师/管理 |
| POST | /audit/submissions/{id}/approve |
通过 | 教师/管理 |
| POST | /audit/submissions/{id}/reject |
驳回 | 教师/管理 |
| ⭐ POST | /audit/submissions/{id}/revoke |
撤回已审 | 教师/管理 |
| GET | /audit/submissions/{id}/timeline |
审核时间轴 | 教师/管理 |
| GET | /audit/statistics |
审核统计 | 教师/管理 |
| GET | /audit/reason-codes |
驳回原因字典 | 教师/管理 |
| POST | /audit/submissions/{id}/proxy-upload |
代传材料 | 教师/管理 |
4.8 校验 / 导出 / 通知(12)
| 路径 | 说明 | 权限 | |
|---|---|---|---|
| POST | /validation/run |
触发任务级全量校验 | 管理 |
| GET | /validation/issues |
问题清单 | 教师/管理 |
| GET | /validation/issues/export |
导出异常清单 Excel | 管理 |
| POST | /exports |
创建导出任务 | 管理 |
| GET | /exports |
导出历史 | 管理 |
| GET | /exports/{id} |
导出进度 | 管理 |
| GET | /exports/{id}/download |
下载签名 URL | 管理 |
| GET | /notify/templates |
通知模板 | 管理 |
| PUT | /notify/templates/{id} |
修改模板 | 管理 |
| POST | /notify/send |
手动发送通知 | 管理/教师 |
| GET | /notify/records |
通知记录 | 管理 |
| POST | /callback/wechat/msg |
微信订阅消息回调 | 公开 |
4.9 ⭐ 本次新增接口索引(16)
本表是索引,不是额外接口——其中 5 个已在上方 §4.3 / §4.7 列出,不计入总数。 表内 13 行覆盖 16 个接口(含 GET/PUT、GET/POST 合并行)。 ⚠️ 需求收敛(2026-10)后,原表里的历史整理器 5 个随模块删除,故 21 − 5 = 16。
| 路径 | 说明 | 详情节 | |
|---|---|---|---|
| ⭐ | /tasks/{id}/plan-scope |
报考范围读取 / 确认(GET / PUT) | §7.7 / §7.8 |
| ⭐ | /tasks/{id}/pause /resume |
暂停 / 恢复任务 | §7.4 |
| ⭐ | /audit/submissions/{id}/revoke |
撤回已审 | §11.4 |
| ⭐ | /mp/files/{fileId}/process |
文件规范化总入口(原仅在 §8.9 详述,未登记进清单) | §9.6 |
| ⭐ | /dicts |
字典列表 | §13.3 |
| ⭐ | /dicts/{code}/items |
字典项(GET / POST) | §13.3 |
| ⭐ | /dicts/items/{id} |
修改字典项 | §13.3 |
| ⭐ | /logs/operations |
操作日志 | §13.4 |
| ⭐ | /logs/logins |
登录日志 | §13.4 |
| ⭐ | /mp/messages |
学生消息中心 | §13.5 |
| ⭐ | /mp/messages/read |
标记已读 | §13.5 |
| ⭐ | /mp/teacher/messages |
教师消息中心 | §13.5 |
| ⭐ | /mp/subscribe |
订阅消息额度登记 | §13.6 |
新增 16 个的分布
| 归属 | 数量 | 明细 |
|---|---|---|
| 报考范围 | 2 | plan-scope GET / PUT |
| 任务状态 | 2 | pause / resume |
| 审核 | 1 | revoke |
| 文件处理 | 1 | process(登记遗漏) |
| 字典 | 4 | dicts |
| 日志 | 2 | operations / logins |
| 消息中心 | 3 | 学生 2 + 教师 1 |
| 订阅额度 | 1 | subscribe |
4.10 开放接口(4)
| 路径 | 说明 | |
|---|---|---|
| POST | /open/v1/auth/token |
appKey/secret 换 token |
| GET | /open/v1/tasks/{id}/submissions |
拉取已终审通过的数据 |
| GET | /open/v1/tasks/{id}/files |
批量获取文件签名 URL |
| POST | /open/v1/callback/push |
推送审核结果到机构系统 |
4.11 ⭐ 加入申请(已撤销 · 0 个)
⚠️ 本节原为「客户确认后新增 5 个」(确认函 B1:申请加入名单 → 管理员审批)。 需求收敛(2026-10)后整条通道取消:模块
module/join、5 个接口、join:audit权限码、collect_join_request表、学生端「申请加入名单」页与 5 个专用错误码 (40037/40038/40039/40040/40042)已全部删除。门禁失败后的口径:只保留「重新填写」一个系统内动作 + 一句纯文案提示 "如确认已报名,请联系老师把你加入名单"。名单外的人由老师在任务里手工加人(
FR-508追加导入), 不展示任何联系方式(铁律:联系方式不进系统)。⚠️ 治理提示:B1 是确认函里唯一的功能增量,本次属需求回退,建议补一张变更确认单。
4.12 ⭐ 角色与授权(8)
来源:FR-205「角色自定义与数据权限」。内置角色 4 个(平台运营 / 学校管理员 / 班主任 / 学生, 权限由代码
BuiltinRole决定、界面只读),租户可另建自定义角色并配置权限码与可见菜单。 详述见 §5.10。
| 路径 | 说明 | 权限 | |
|---|---|---|---|
| ⭐ GET | /roles |
角色列表(内置 + 自定义) | 管理 |
| ⭐ GET | /permissions |
权限码目录(按分组) | 管理(role:manage) |
| ⭐ POST | /roles |
新增自定义角色 | 管理(role:manage) |
| ⭐ PUT | /roles/{id} |
修改自定义角色(名称 / 用户类型 / 备注) | 管理(role:manage) |
| ⭐ DELETE | /roles/{id} |
删除自定义角色(软删) | 管理(role:manage) |
| ⭐ PUT | /roles/{id}/permissions |
设置权限码(覆盖式) | 管理(role:manage) |
| ⭐ PUT | /roles/{id}/menus |
设置可见菜单(覆盖式) | 管理(role:manage) |
| ⭐ PUT | /roles/{id}/status |
启用 / 停用角色 | 管理(role:manage) |
§5 认证与账号
5.1 POST /api/v1/auth/login
权限:公开 限流:5 次/分钟/IP
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
username |
string | ✅ | 账号 |
password |
string | ✅ | 密码(前端 RSA 加密后传输) |
captchaKey |
string | 条件 | 图形验证码 key(连续失败 2 次后必填) |
captchaCode |
string | 条件 | 验证码 |
响应
{
"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 | — | 扫码进入时的小程序码参数(含租户与任务标识) |
响应
{
"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
成功响应
{
"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)
{
"code": 0,
"data": {
"matched": false,
"needConfirmName": true,
"candidates": [ { "rosterId": 9001, "realName": "曾伊豪", "similarity": 0.92 } ],
"confirmToken": "tmp_8f3a...",
"tip": "你是不是「曾伊豪」?"
}
}
失败统一响应(40007)——不区分是姓名错还是号码错
{
"code": 40007,
"msg": "没有找到你的信息,请核对后重试",
"data": { "matched": false, "remainAttempts": 7, "contactTeacher": true }
}
安全要求:
remainAttempts只报剩余次数,不透露任何关于名单的信息;失败尝试不落明文证件号(只落 hash 与时间)。
5.5 GET /api/v1/mp/roster/contact 【已移除】
GET /api/v1/mp/roster/contact❌ 本接口已按客户要求移除(2026-09-28)。
原设计:门禁失败页展示班主任/管理员的脱敏手机号,学生可一键拨打。
移除理由:学生在班级群里本来就有班主任的联系方式,系统内再放一份: ① 不必要——用户已经知道怎么找人; ② 有维护成本——班主任换人、号码变更都要同步; ③ 多一处隐私暴露面——即使是脱敏号,也意味着系统里要多存一份联系方式数据。
替代:门禁失败页只保留「重新填写」一个系统内动作(原「申请加入名单」通道已于 2026-10 需求收敛时取消)。 纯文案提示(如"如确认已报名,请联系老师把你加入名单")保留,因为不涉及任何联系方式数据; 名单外的人由老师在任务里手工加人(
FR-508追加导入)。
5.6 GET /api/v1/users
Query:keyword(姓名/账号/手机号模糊)、orgId、roleCode、userType、status、分页参数
响应:{ list: UserVO[], total }
5.7 POST /api/v1/users
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
username |
string | ✅ | 登录账号,租户内唯一 |
realName |
string | ✅ | 姓名 |
phone |
string | ✅ | 手机号 |
orgId |
int64 | — | 所属组织 |
roleCodes |
string[] | ✅ | 至少 1 个 |
userType |
int | ✅ | 见 §2.7 |
响应:{ "id": 10087, "initialPassword": "Ab3#k9mP" }(初始密码仅此一次返回)
5.8 POST /api/v1/users/import
Content-Type:multipart/form-data 字段:file(Excel,≤10MB)
响应(导入体检报告,与名册导入同构)
{
"code": 0,
"data": {
"total": 42, "success": 40, "failed": 2,
"headerRow": 1,
"errors": [
{ "row": 17, "field": "phone", "value": "1380013", "reason": "手机号格式不正确" },
{ "row": 33, "field": "username", "value": "lisi", "reason": "账号已存在" }
],
"initialPasswords": [
{ "username": "zhangsan", "password": "Xy7$mQ2p" }
]
}
}
⚠️ 导入类接口一律返回「体检报告」结构(
total/success/failed/headerRow/errors[])。headerRow必须回传——实测两份真实文件的表头分别在第 1 行和第 6 行,界面要把它可视化出来让用户确认。
5.9 其余账号接口
| 接口 | 关键字段 | 备注 |
|---|---|---|
PUT /users/{id} |
同 5.7,username 不可改 |
— |
DELETE /users/{id} |
— | 软删(deleted=1);有在办任务时返回 40014 并提示先移交 |
POST /users/{id}/reset-password |
{ "mode": "GENERATE" } 或 { "newPassword": "..." } |
返回 { newPassword } |
PUT /auth/password |
{ oldPassword, newPassword } |
强制改密场景 oldPassword 可空 |
POST /auth/logout |
— | 服务端把 token 加入黑名单,TTL = 剩余有效期 |
POST /mp/bind-phone |
{ code }(getPhoneNumber 返回) |
写入 phone_hash,用于门禁二次校验 |
5.10 角色与授权(§4.12 的 8 个接口)
对应 FR-205。内置角色 4 个(
PLATFORM_ADMIN/SCHOOL_ADMIN/HEAD_TEACHER/STUDENT), 权限由代码BuiltinRole决定、界面只读(能看、能当模板,改不了); 租户可另建自定义角色,于是"角色 → 权限码"与"角色 → 可见菜单"两件事可以由租户自己配。 改授权立即生效(服务端写授权后清缓存),不需要重新登录。权限码本身仍然定义在代码里(
PermissionDef,28 个),没有sys_permission定义表 —— 它是前后端契约:后端用它校验、前端用它控制按钮显隐,存 DB 就会漂移。⚠️ 需求收敛(2026-10):本条不再是"二期功能",它就是一期能力;同时 数据范围(本人/本班/本院系/全租户)只展示、不提供修改入口 —— 它目前由"拿到了哪些权限码 + 任务里被指派了哪些班"共同决定,做成可配要先把判据收口(本次不做)。
五条口径(实现与文档同源,改一处必须改另一处)
| # | 口径 | 为什么 |
|---|---|---|
| 1 | 内置角色只读 | roleType=1 的角色可以看权限矩阵,但改 / 删 / 授权 / 停用一律 40001。一次误操作就能把管理员锁在系统外,而这类错误不可逆 |
| 2 | 内置角色的权限以代码为准 | 列表里内置角色的 permCodes / menus 由代码返回,不读库;库里的行只是"看得见、可审计"(每日定时任务把两者对齐,见 §17.2 D-13) |
| 3 | 平台级能力与入口不给学校 | 学校租户授予 tenant:manage / tenant:view / log:viewAll → 40301;配置平台专用菜单 tenant → 40301;建"平台运营"类角色(userType=1)→ 40301;发 PLATFORM_ADMIN 给账号 → 40301 |
| 4 | 防自锁 | 不允许去掉"自己进入本页所依赖的最后一条 role:manage 权限 / role 菜单",也不允许停用或删除那个角色;改账号角色时同理保护 user:manage |
| 5 | 改完立即生效 | 每次写授权都清该角色的缓存,所以不需要重新登录;停用角色立刻不再给权限(不是等 token 过期)。⚠️ 改账号的角色归属仍是"下次登录生效"(角色码在 token 里) |
GET /api/v1/roles 权限:role:manage 或 user:view / user:manage(账号页的角色下拉要用它)
| 字段 | 类型 | 说明 |
|---|---|---|
id |
long | 角色 ID |
roleCode |
string | 角色码(建后不可改) |
roleName |
string | 角色名 |
roleType |
int | 1 内置(只读)/ 2 自定义 |
userType |
int | 1 平台运营 / 2 学校管理员 / 3 班主任 / 4 学生(账号的 userType 由它推导) |
dataScope |
int | 1 全平台 / 2 本租户 / 3 本组织及下级 / 4 仅本人(本期只展示,见下) |
status |
int | 0 停用 / 1 正常 |
userCount |
int | 有多少账号在用(> 0 时不可删除) |
editable |
bool | 能否编辑 / 删除 / 停用(内置恒 false) |
assignable |
bool | 能否发给账号(内置的「平台运营」在学校租户里 false:看得见、发不出去) |
permCodes |
string[] | 权限码(内置取代码,自定义取库) |
menus |
string[] | 可见菜单键(与前端 XtMenu.key 一致) |
⚠️
dataScope本期只展示、不提供修改入口:数据范围(本人 / 本班 / 本院系 / 全租户)目前由 "拿到了哪些权限码 + 任务里被指派了哪些班"共同决定,散在审核 / 催报 / 代传 / 看板各处; 做成可配要先把那些判据收口(单独一批)。让界面出现一个"改了不生效"的开关,比没有它更糟。
GET /api/v1/permissions 权限:role:manage
返回按分组排列的 28 个权限码:[{ group, items: [{ code, label, platform, platformOnly }] }]。
platform 为 PC / MP / BOTH;platformOnly=true 的码学校租户不可授予(界面禁用勾选,服务端 40301)。
POST /api/v1/roles 权限:role:manage
| 字段 | 必填 | 说明 |
|---|---|---|
roleCode |
✅ | 大写字母 / 数字 / 下划线,以字母开头,租户内唯一,不得占用内置角色码 |
roleName |
✅ | 显示名(可改) |
userType |
✅ | 该角色对应的用户类型(学校租户不能选 1) |
remark |
— | 备注 |
permCodes |
✅ | 至少一项(服务端校验存在性与平台级白名单) |
menus |
✅ | 至少一个(服务端校验是否为现存菜单键、是否平台专用) |
返回新角色 id。建后不能改 roleCode(它在 JWT 与操作日志里,改了历史留痕就对不上)。
PUT /api/v1/roles/{id} 权限:role:manage 只改 roleName / userType / remark。
⚠️ 角色还有账号在用(userCount > 0)时改 userType → 40001:那会让账号的 user_type 与角色不一致,
而 user_type 决定"他算学生还是老师"。
DELETE /api/v1/roles/{id} 权限:role:manage 软删(含授权与菜单行)。
还有账号在用 → 40045;命中防自锁 → 40001。
PUT /api/v1/roles/{id}/permissions / .../menus 权限:role:manage
请求体 { permCodes: [] } / { menus: [] },覆盖式;返回 void。写后立即失效该角色的授权缓存。
PUT /api/v1/roles/{id}/status?status=0|1 权限:role:manage
停用 = 该角色立刻不再给权限(不必等 token 过期);内置角色不可停用。
错误码
| 码 | 场景 |
|---|---|
40001 |
角色码不合法 / 重复 / 占用内置码;权限码或菜单键不存在;改内置角色;userType 与在用的账号冲突;命中防自锁 |
40301 |
无 role:manage;授予平台级权限码;配置平台专用菜单;建平台运营类角色;跨租户(平台未选定学校) |
40045 |
角色还有账号在用,不可删除 |
§6 组织与租户
6.1 GET /api/v1/orgs/tree
Query:withCount(bool,是否带学生数)、rootId
响应
{
"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)
{
"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 响应
{
"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 报考范围
背景:不存在独立的"计划清单"文件。报考范围由名册聚合得出。
响应
{
"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 报考范围确认
请求
{
"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 请求
{
"version": 3,
"baseVersion": 2,
"fields": [
{ "key": "real_name", "label": "姓名", "type": "text", "builtin": true, "locked": true, "required": true, "sortNo": 1 },
{ "key": "education", "label": "学历", "type": "select", "builtin": true, "locked": false,
"required": true, "dictCode": "EDU_LEVEL", "sortNo": 4,
"tip": "老师已填,请核对" },
{ "key": "is_retake", "label": "是否补考", "type": "bool", "builtin": true, "sortNo": 7 },
{ "key": "retake_theory", "label": "补考理论", "type": "text", "builtin": true, "sortNo": 8,
"visibleWhen": { "field": "is_retake", "op": "eq", "value": true } }
],
"extras": [
{ "key": "extra_skill_cert", "label": "其它技能证书编号", "type": "text", "required": false, "sortNo": 100 }
]
}
版本冲突:baseVersion 与当前版本不符 → 40025 并返回最新 schema,前端做差异合并。
内置字段不可删:删除任一 builtin: true 的字段 → 40026。
extras存入extras_json,不参与统计。凡需要统计的字段必须落在内置列上——这是架构约束,在设计器里以「扩展字段不参与核心统计」明示。
7.10 GET/POST /api/v1/tasks/{id}/materials · PUT/DELETE /api/v1/materials/{id}
POST/PUT 请求:MaterialDefVO(见 3.4)
删除约束:已有文件上传的材料项不允许删(40027),只能设 visibleWhen 隐藏。
材料项模板(materialTemplateId 可引用)
| 模板 | 材料项 |
|---|---|
TPL_SCHOOL_ROLL |
一寸照片 + 身份证 + 学籍表 + 工作证明 |
⚠️ 需求收敛(2026-10)后模板只剩这一个:原
TPL_SOCIAL(社会收集)与TPL_ZHUANXIANG(专项职业能力)随"二期取消"一并删除;传这两个码会得到不认识的模板, 不再有对应的初始化行为。
7.11 GET /api/v1/tasks/{id}/statistics
响应:TaskStatVO(见 3.3)+ 分班明细
{
"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 行预览,请用户手动指定
响应(导入体检报告)
{
"code": 0,
"data": {
"total": 28, "success": 28, "failed": 0,
"headerRow": 6,
"sheetName": "24中技数控1班",
"headerCandidates": [ { "row": 6, "score": 18 }, { "row": 1, "score": 2 } ],
"headerConfirmed": true,
"columns": [
{ "col": "A", "title": "序号", "mappedTo": null, "matched": false },
{ "col": "B", "title": "姓名", "mappedTo": "real_name", "matched": true },
{ "col": "C", "title": "身份证号", "mappedTo": "id_card_no", "matched": true },
{ "col": "D", "title": "报考职业", "mappedTo": "occupation", "matched": true },
{ "col": "U", "title": "备注", "mappedTo": "ext_json.remark", "matched": false }
],
"errors": [
{ "row": 12, "col": "C", "field": "id_card_no", "value": "3601052009081712**",
"reason": "身份证号校验位不通过", "code": "IDCARD_CHECKSUM" },
{ "row": 19, "col": "C", "field": "id_card_no", "value": "36102920090729**10",
"reason": "证件号与第 8 行重复", "code": "DUP_IDCARD" }
],
"warnings": [
{ "row": 28, "col": "D", "field": "occupation",
"reason": "报考职业为空(专项职业能力可留空)", "code": "EMPTY_OCCUPATION" }
],
"summary": { "uniquePersons": 28, "dupRows": 2, "idcardInvalid": 1, "nameSuspect": 0, "phoneInvalid": 3 }
}
}
体检项清单
| code | 说明 | 处理 |
|---|---|---|
IDCARD_CHECKSUM |
校验位不通过(ISO 7064) | 阻断该行 |
DUP_IDCARD |
证件号重复 | 阻断该行 |
IDCARD_FORMAT |
格式非法(长度/字符) | 阻断该行 |
EMPTY_NAME |
姓名为空 | 阻断该行 |
EMPTY_IDCARD |
证件号为空 | 阻断该行 |
EMPTY_OCCUPATION |
报考职业为空 | 警告 |
NAME_SUSPECT |
姓名含罕见字/形近字 | 警告 |
PHONE_INVALID |
手机号格式异常 | 警告(不阻断,可后续补) |
dryRun模式:先体检、给用户确认表头与错行,确认无误后再正式导入。推荐前端默认走 dryRun,避免"导进去才发现表头选错"。
8.2 GET /api/v1/tasks/{id}/roster
Query:keyword、orgId、submitStatus、bindStatus、hasIssue、分页
响应:{ list: RosterVO[], total }
8.3 PUT /api/v1/roster/{rid}
| 字段 | 类型 | 说明 |
|---|---|---|
realName |
string | 改名会重置绑定(需二次确认 confirmRebind: true) |
idCardNo |
string | 改证件号必留痕(FR-506) |
phone |
string | — |
extJson |
object | 报考三项等 |
留痕:写入 collect_roster 变更前值快照 + 操作日志,GET /logs/operations 可查。
8.4 DELETE /api/v1/roster/{rid}
约束:已绑定或已提交的名册项不允许删除(40029),只能标记为"不参与本次认定"。
8.5 GET /api/v1/tasks/{id}/roster/template
Query:format(xlsx)、withSample(bool,是否带示例行)
响应:文件流。表头固定在第 1 行,必填列标红,附 9 条填表规则说明页。
8.6 POST /api/v1/roster/{rid}/notify · POST /api/v1/tasks/{id}/roster/notify-unsubmitted
| 接口 | 请求 | 说明 |
|---|---|---|
| 定向催报 | { "channel": 1, "templateCode": "NOTIFY_UNSUBMITTED", "extraText": "..." } |
单人或多人(rosterIds[]) |
| 一键催报 | { "orgIds": [2001], "channel": 1, "excludeRecentHours": 24 } |
仅催未提交者;excludeRecentHours 防重复骚扰 |
响应:{ "sent": 5, "failed": 0, "skipped": 1, "details": [ { "rosterId": 9012, "skipped": true, "reason": "24 小时内已催过" } ] }
限流:10 次/分钟/任务。
⚠️ 本节已撤销(需求收敛 2026-10):原「名单加入申请」通道(
FR-509,确认函 B1) 连同 5 个接口(POST/GET /mp/roster/join-request、GET /roster/join-requests、POST /roster/join-requests/{id}/approve|reject)、join:audit权限码、collect_join_request表 与错误码40037/40038/40039/40040/40042一并删除。现在的口径:门禁失败页只给「重新填写」+ 一句"如确认已报名,请联系老师把你加入名单"; 名单外的人由老师在任务里手工加人(§8.1 追加导入,
FR-508)。系统内不展示任何联系方式。治理提示:B1 是客户确认函里唯一的功能增量,本次属需求回退,建议补一张变更确认单。
§9 填报端 · 小程序
9.1 GET /api/v1/mp/tasks
权限:学生 说明:只返回该学生被绑定/在名册中的任务。
响应
{
"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
响应
{
"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 | 客户端时间,用于冲突提示 |
响应
{
"code": 0,
"data": {
"savedAt": "2026-09-28T14:30:00+08:00",
"changedFields": ["phone"],
"conflict": null
}
}
并发冲突:同一份草稿在多端同时编辑,后写者返回 conflict(含对方已改字段),由前端提示。
自动保存:前端每 15 秒或失焦时调一次;服务端不限流(草稿合并是轻量操作)。
9.5 ⭐ POST /api/v1/mp/files/upload-token 上传前置检查(预检 / 秒传 / 压缩建议)
✅ 定位已确认(2026-09-30 客户确认,原「OSS 直传凭证」改为此口径): 一期实现是服务端一步上传(
POST /api/v1/mp/files/upload,multipart, 收上来即压缩 + 按模板改名,见 §17.2 D-3),不走对象存储直传 —— 直传要把上传凭证交给前端、且七牛直传必须配回调才能让服务端知道"传完了"。 本接口因此不是安全边界(客户端可以跳过它直接上传,服务端上传本身做全部校验), 它的价值是在上传一个字节之前完成三件事: ① 预检(格式 / 体积 / 份数,传 8MB 才发现格式不对就晚了); ② 秒传(同fileHash已存在 →existed: true,网络断开重试时不用重传); ③ 端侧压缩建议(能大幅降低上传流量)。 大文件 / 弱网场景要恢复直传时再单独立项(含回调闭环),见《08》F-3 / F-71 / F-72。
幂等:是
请求
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
taskId |
int64 | ✅ | — |
materialCode |
string | ✅ | 目标槽位 |
fileName |
string | ✅ | 原始文件名(可能是乱的,正常) |
fileSize |
int64 | ✅ | 字节 |
contentType |
string | ✅ | MIME |
source |
int | — | 1 本人上传 / 2 班主任代传 / 3 管理员代传 / 4 整理器导入(模块已取消,仅历史数据;口径见 §3.6 注) |
fileHash |
string | — | 客户端 SHA-256(用于秒传与去重) |
响应
{
"code": 0,
"data": {
"fileId": 987654321,
"direct": false,
"uploadUrl": "/api/v1/mp/files/upload",
"expireSeconds": 900,
"existed": false,
"clientCompressHint": {
"enabled": true, "maxLongSide": 1600, "quality": 80, "targetSizeKb": 1024
}
}
}
| 字段 | 说明 |
|---|---|
direct |
恒为 false(一期不做对象存储直传)。uploadUrl 就是本项目自己的接收地址,学生端 wx.uploadFile 直接 POST 它,与 H5 路径完全一样 |
existed |
true = 秒传命中(同 hash 已存在),前端跳过上传直接进入处理 |
clientCompressHint |
端侧预压缩建议。这是第一道防线,能大幅降低上传流量与处理时长;服务端仍会按规则重压,端侧压缩只是加速 |
端侧压缩与「一次编码到位」的关系:实测 300dpi/q85 即可达标,端侧压缩只是减少上传量的优化,不是质量决定环节。服务端始终以原图(或端侧压缩后的图)为输入重新规范,保证输出一致。
9.6 ⭐ POST /api/v1/mp/files/{fileId}/process 文件规范化(有原始件时的重新处理)
✅ 语义已确认(2026-09-30,原「命名+压缩+识别+校验总入口」改为此口径): 一期上传是一步的 ——
POST /mp/files/upload收上来当场完成压缩、按模板改名、校验、 记录问题(含 VAL-06 自动修复),默认只存规范件(fill.storage.keep-raw=false)。 所以本接口只在「保留了原始件」时有用:对原始件重新跑一遍规范化 (典型场景:改了材料规则后重压)。默认配置下多数文件没有原始件,调用会明确说明这一点, 而不是假装处理了一遍。识别 / OCR 的四层递进缓做(等 OCR 供应商选型,见 §17.2 D-12 与《01》D1)——不再作为二期功能设计。 下面的响应结构保留作为「规范化产物」的字段参考(上传接口的产物与之同构)。
说明:对有原始件的文件重新执行规范化(压缩 + 改名 + 校验)。
请求
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
async |
bool | false |
大文件建议异步 |
runOcr |
bool | true |
是否跑 OCR |
dryRun |
bool | false |
只试跑不落库 |
confirmMove |
bool | false |
识别到槽位不符时是否执行移动 |
响应(同步模式)
{
"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 }
响应
{
"code": 0,
"data": {
"batchToken": "b_7f2a...",
"total": 3,
"results": [
{ "fileId": 987654321, "status": 2, "materialCode": "PHOTO1", "detectMaterialCode": "PHOTO1", "conflict": false },
{ "fileId": 987654322, "status": 2, "materialCode": "IDCARD", "detectMaterialCode": "IDCARD", "conflict": false },
{ "fileId": 987654323, "status": 2, "materialCode": "SCHOOL_ROLL", "detectMaterialCode": "WORK_PROOF",
"conflict": true, "detectConfidence": 0.71, "needManualPlace": true }
],
"summary": { "ok": 2, "conflict": 1, "failed": 0 }
}
}
冲突处理(conflict: true):只提示、不自动改位置(FR-614a)。前端展示:
这张图看起来是「工作证明」,你放在了「学籍表」。要移过去吗? [移到工作证明] [就放这里]
9.8 GET /api/v1/mp/files/{fileId} · DELETE /api/v1/mp/files/{fileId}
| 接口 | Query / 说明 | 响应 |
|---|---|---|
| GET | prefer=preview / prefer=original;返回签名 URL(30 分钟) |
{ previewUrl, thumbUrl, standardName, finalSizeKb, width, height } |
| DELETE | 软删,保留版本历史 | { deleted: true, remainVersions: 1 } |
删除约束:已提交并进入审核的文件删除需先撤回(40030)。
9.9 POST /api/v1/mp/ocr/idcard(一期缓做)
⚠️ 一期未实现(等 OCR 供应商选型,《01-客户与机构确认函》D1)。 这是 119 个契约接口里唯一没有实现的一个;表单预填与校验位校验在学生端走人工填写。 选型要同时评估数据出境合规。选型定了之后本节按原设计实现,以下为设计参考。
说明:身份证 OCR,用于表单预填与校验。这是唯一返回明文姓名的 OCR 场景。
请求:{ "fileId": 987654321, "side": "FRONT" }
响应
{
"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)
{
"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
}
}
成功响应
{
"code": 0,
"data": {
"submissionId": 5001, "status": 1,
"submitTime": "2026-09-28T15:00:00+08:00",
"nextNode": "CLASS_AUDIT",
"nextNodeText": "等待李老师初审",
"estimatedAuditHours": 48
}
}
社会收集(本次不做,传 collectMode=2 直接 40044):nextNode 返回 ADMIN_AUDIT(跳过初审)。
9.11 POST /api/v1/mp/submissions/{id}/withdraw
约束:status ∈ {1, 2}(未终审)可撤回;status = 3 需管理员操作(40031)。
响应:{ "status": 0, "withdrawnAt": "..." }
9.12 GET /api/v1/mp/submissions/{id}/progress
响应
{
"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
响应
{
"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)
响应
{
"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 |
响应
{
"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 + 材料 + 问题 + 识别结果
{
"code": 0,
"data": {
"id": 5001, "status": 1,
"realName": "曾伊豪", "idCardMask": "3610292009******10",
"orgName": "24中技数控1班", "submitTime": "2026-09-28T15:00:00+08:00",
"changedFields": ["phone"],
"changedDetail": [
{ "field": "phone", "label": "联系电话",
"rosterValue": "138****8888", "studentValue": "139****9999",
"changedAt": "2026-09-28T14:50:00+08:00" }
],
"materials": [
{
"materialCode": "IDCARD", "materialName": "身份证", "done": true,
"files": [
{
"id": 987654321,
"originalName": "曾伊豪-361029200907295110身份证.jpg.jpg",
"standardName": "曾伊豪-361029200907295110-身份证.jpg",
"previewUrl": "https://...", "thumbUrl": "https://...",
"finalSizeKb": 512, "maxSizeKb": 1024,
"width": 1600, "height": 1200, "dpi": 300,
"validateStatus": 2,
"detect": { "materialCode": "IDCARD", "confidence": 0.98, "source": 1,
"sourceText": "文件名匹配", "conflict": false },
"issues": [
{ "ruleCode": "VAL-02", "severity": 1, "title": "证件号已按名册修正",
"expectedVal": "3610292009******10", "actualVal": "3610292009******10",
"autoFixed": true }
]
}
]
}
],
"redCount": 0, "yellowCount": 1,
"approvePolicy": { "canApprove": true, "canBatch": true, "reason": null },
"timeline": [ { "node": 0, "title": "学生提交", "time": "..." } ]
}
}
识别结果 (
detect) 在此一并返回——这就是追溯矩阵里 I10 缺口的落点,不需要独立接口。
11.3 POST /api/v1/audit/submissions/{id}/approve · /reject
与班主任端同构,差异:
| 项 | 初审(班主任) | 终审(管理员) |
|---|---|---|
| 通过后状态 | 1 → 2(待终审) |
2 → 3(已通过) |
| 驳回后状态 | → 4 |
→ 4 |
| 驳回是否可越过初审 | — | 可以(终审驳回直接生效) |
redCount > 0 |
禁止通过 | 禁止通过 |
终审请求额外字段:{ "finalOpinion": "同意认定", "needArchive": false }
11.4 ⭐ POST /api/v1/audit/submissions/{id}/revoke
说明:撤回已作出的审核结论(这是原设计缺失的接口)。
请求
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
auditRecordId |
int64 | ✅ | 要撤回的审核记录 |
reason |
string | ✅ | 撤回原因 |
响应:{ "status": 1, "revokedRecordId": 5501 }
约束
| 条件 | 结果 |
|---|---|
当前状态 3(已通过)且未被导出 |
✅ 允许,回退到 2 或 1 |
当前状态 2(待终审) |
✅ 允许,回退到 1(撤回的是初审结论 —— 初审通过后发现填错,正是最需要撤的时刻) |
| 已被导出打包 | ❌ 40033——已交付的数据不可追溯修改 |
| 已归档 | ❌ 40034 |
| 撤回者非原审核人且非管理员 | ❌ 40301 |
留痕:写入一条 action = 7(撤回)的 collect_audit_record,时间轴显示"李老师撤回了通过结论"。
(旧版写 action = 3 —— 3 实际是批量通过,撤回恒为 7,与代码常量 ACTION_REVOKE 一致。)
11.5 GET /api/v1/audit/submissions/{id}/timeline
响应:AuditRecordVO[],倒序。
11.6 GET /api/v1/audit/statistics
Query:taskId、orgId、from / to
响应:{ pendingCount, approvedCount, rejectedCount, avgAuditHours, timeoutCount, byAuditor: [{ auditorName, handled, avgHours }], byReason: [{ reasonCode, reasonText, count }] }
byReason是给管理员看"哪种问题最多"的,用于反过来优化材料引导语。
11.7 GET /api/v1/audit/reason-codes · POST /api/v1/audit/submissions/{id}/proxy-upload
| 接口 | 说明 |
|---|---|
reason-codes |
返回 PRD 附录 C 的 10 条,仅返回启用的;可按 taskId 定制 |
proxy-upload |
同 10.6,PC 端入口 |
§12 校验 / 导出 / 通知
12.1 POST /api/v1/validation/run
说明:任务级全量校验,产出问题清单。
✅ 改为同步(2026-09-30 客户确认):旧版声明
async默认true并返回jobId, 但全契约没有"查 jobId"的接口 —— 那个 jobId 查不到任何东西。 实测一份填报单的校验 <100ms、一个任务的量级秒级,同步跑完直接回结果 比返回一个查不到状态的作业号有用。保留jobId仅作本次运行的标识(幂等对账用)。
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
taskId |
int64 | ✅ | — |
scope |
string | ALL |
ALL / ISSUES_ONLY / APPOINTED |
submissionIds |
int64[] | — | scope=APPOINTED 时必填 |
rules |
string[] | 全部 | 只跑指定规则(如 ["VAL-02","VAL-04"]) |
响应:{ "jobId": "v_3f2a", "checkedSubmissions": 320, "issuesFound": 47, "unresolvedLeft": 31, "elapsedMs": 812 }
12.2 GET /api/v1/validation/issues
Query:taskId、severity、ruleCode、orgId、resolved、分页
响应
{
"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)——导出按钮必须被禁用
{
"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 }
进度响应
{
"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)
响应
{
"code": 0,
"data": {
"url": "https://oss.../export/555/part1.zip?Signature=...",
"expireSeconds": 1800,
"fileName": "202609机电认定-24中技数控1班.zip",
"sizeMb": 22.4,
"checksum": "sha256:abc123...",
"remainsCachedDays": 7
}
}
错误 40302:签名过期 → 重新获取(不重复计费)。
12.7 通知
| 接口 | 请求要点 | 响应 |
|---|---|---|
GET /notify/templates |
taskId(可选,租户级/任务级) |
[{ id, templateCode, bizType, channel, title, content, enabled }] |
PUT /notify/templates/{id} |
{ title, content, enabled };content 支持变量 {姓名} {任务名} {截止日} {驳回原因} |
{ updated: true } |
POST /notify/send |
{ bizType, receiverIds[], channel, templateCode, extraText } |
{ sent, failed, skipped } |
GET /notify/records |
taskId、bizType、status、分页 |
{ list: NotifyRecordVO[], total, deliveryRate: "97.2%" } |
POST /callback/wechat/msg |
微信回调原始报文 | { errcode: 0 } |
模板变量白名单(越界变量渲染为空并告警):
| 变量 | 适用 |
|---|---|
{姓名} {班级} |
全部 |
{任务名} {截止日} {剩余天数} |
全部 |
{驳回原因} {材料名} |
驳回/催报 |
{提交进度} |
催报 |
{导出包名} {下载链接} |
导出完成 |
订阅消息限制:微信订阅消息为一次性订阅,每次发送消耗 1 次额度(collect_subscribe_quota)。额度不足时降级为公众号模板消息,再不足降级短信(按条计费,见 §13.6)。
§13 本次补齐的缺口接口
13.1 说明
来源:《需求追溯矩阵》§3.1 审计发现的 10 个缺口。这些接口在原 PRD §8 中被需求提及但未定义,若不补齐,对应页面将无数据来源。
| 缺口 | 补在哪 | 状态 |
|---|---|---|
| I1 报考范围读取/确认 | §7.7 / §7.8(已定义) | ✅ |
| I8 审核撤回 | §11.4(已定义) | ✅ |
| I9 任务暂停/恢复 | §7.4(已定义) | ✅ |
| I10 材料识别结果读取 | 并入 §11.2 详情响应(不新增接口) | ✅ |
| I3 字典管理 | §13.3 | ✅ |
| I4 / I5 日志 | §13.4 | ✅ |
| I6 消息中心 | §13.5 | ✅ |
| I7 订阅额度登记 | §13.6 | ✅ |
13.2 历史数据整理器(已取消 · 0 个)
⚠️ 本节已撤销(需求收敛 2026-10):历史整理器整块能力(
module/cleanup)连同 5 个接口(POST/GET /cleanup/jobs、GET /cleanup/jobs/{id}、.../report、.../import)、cleanup:run权限码、collect_cleanup_job表、PC 端「历史整理器」菜单与「整理器」原型屏 一并删除,不再设计。保留这一节标题只是为了让旧的交叉引用(
§13.2)仍能落到一个说清"为什么没有了"的地方 —— 节号不重排,§13.3起的编号与代码注释里的引用都不受影响。⚠️ 别误伤:
POST /api/v1/exports/cleanup-run是导出包的过期清理,属导出中心,仍在。
13.3 字典管理(4 个)
| 接口 | 说明 |
|---|---|
GET /api/v1/dicts |
字典列表 [{ dictCode, dictName, itemCount, builtin }] |
GET /api/v1/dicts/{code}/items |
字典项 [{ id, itemCode, itemName, parentItemCode, sortNo, status }],含层级(如职业-工种) |
POST /api/v1/dicts/{code}/items |
新增项:{ itemCode, itemName, parentItemCode, sortNo } |
PUT /api/v1/dicts/items/{id} |
修改:{ itemName, sortNo, status } |
内置字典(builtin: true,不可删,只可停用单项)
dictCode |
名称 | 项数 |
|---|---|---|
EDU_LEVEL |
学历 | 11 |
JOB_LEVEL |
职业技能等级 | 5(一~五级) |
CERT_TYPE |
认定类型 | 3 |
RETARGET_SUBJECT |
补考科目 | 3 |
OCCUPATION |
职业(国标,可选) | 由 sys_occupation_ref 提供 |
REJECT_REASON |
驳回原因 | 10 |
约束:有下级项的字典项不可停用(40036)。修改 itemCode 不允许(会破坏历史数据)。
OCCUPATION字典的特殊性:官方 Excel 模板里「报考职业」「报考工种」是自由填写、没有下拉。所以字典在这里只做拼写校验与联想建议(P2 级),不做强约束。凡发现自由填写值不在字典中,记SCOPE_FREE_TEXT信息级提示,不阻断。
13.4 日志(2 个)
GET /api/v1/logs/operations
Query:tenantId、operatorId、module、action、targetId、from / to、分页
范围(权限二选一,@RequirePerm({log:viewTenant, log:viewAll})):
| 调用者 | 权限 | 看到什么 |
|---|---|---|
| 平台运营 | log:viewAll |
默认全量(跨租户);传 tenantId 则只看该单位 |
| 学校管理员 / 教务处 | log:viewTenant |
恒为本租户;传了别的 tenantId → 40301(不静默忽略:静默会让人以为"筛过了") |
响应
{
"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:tenantId、userId、userType、result、from / to、分页
范围:与操作日志同一口径(log:viewAll 可跨租户,学校管理员恒为本租户)。
响应:{ 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、分页
响应
{
"code": 0,
"data": {
"list": [
{ "id": 80001, "bizType": 3, "title": "你的材料被退回",
"content": "「工作证明」未盖公章,请重新提供",
"read": false, "linkType": "SUBMISSION", "linkId": 5001,
"createTime": "2026-09-28T16:30:00+08:00" },
{ "id": 80002, "bizType": 2, "title": "距截止还有 3 天",
"content": "请在 10 月 10 日前完成填报", "read": true,
"linkType": "TASK", "linkId": 2026090001, "createTime": "2026-10-07T09:00:00+08:00" }
],
"total": 2,
"unreadCount": 1
}
}
POST /api/v1/mp/messages/read
{ "messageIds": [80001], "all": false } → { "unreadCount": 0 }
GET /api/v1/mp/teacher/messages(教师端)
同构,bizType 增加 7(加入名单审批)等教师专属类型。
消息中心 vs 通知记录的区别:
collect_notify_record记的是发送通道结果(发出去没有、到没到),消息中心记的是站内信内容(用户看到什么)。二者通过bizType关联。
13.6 订阅消息额度登记(1 个)
POST /api/v1/mp/subscribe
说明:微信订阅消息为一次性授权,用户每点一次"允许"只能发一条。小程序端在关键节点主动索取授权并登记额度。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
templateCode |
string | ✅ | 模板码,见下表 |
subscribeCount |
int | — | 本次授权次数(微信返回,默认 1) |
响应:{ "templateCode": "AUDIT_RESULT", "total": 5, "used": 2, "remain": 3 }
模板码与索取时机
templateCode |
索取时机 | 用途 |
|---|---|---|
SUBMIT_SUCCESS |
提交成功后 | 通知审核进展 |
AUDIT_RESULT |
提交成功后 | 通过 / 驳回通知 |
DEADLINE_REMIND |
进入填报页时 | 临期提醒 |
EXPORT_READY |
管理端创建导出时 | 导出完成 |
额度不足的降级链
微信订阅消息(免费,额度受限)
↓ 额度耗尽
公众号模板消息(需关注公众号)
↓ 未关注
短信(按条计费,走 collect_subscribe_quota 计费口径)
↓ 超出套餐
仅站内信(不推送,用户主动进小程序可见)
降级必须静默:学生端不显示"你的额度不够了"。运营侧在
GET /tenants/{id}/usage里看到smsCount上涨与告警。
§14 开放接口(供认定机构对接)
鉴权:appKey + timestamp + nonce + HMAC-SHA256
X-App-Key: jxjd_2026
X-Timestamp: 1790000000
X-Nonce: 8f3a9c2e
X-Sign: HMAC-SHA256(appSecret, appKey + timestamp + nonce + body)
签名规则:body 为原始 JSON 字符串(未格式化、未排序);timestamp 与服务器时间偏差 > 5 分钟拒绝;nonce 5 分钟内不可重复。
14.1 POST /open/v1/auth/token
请求:{ "appKey": "jxjd_2026", "appSecret": "..." }
响应:{ "accessToken": "...", "expireSeconds": 7200, "scope": ["submission:read","file:read"] }
14.2 GET /open/v1/tasks/{id}/submissions
Query:since(增量拉取时间)、status(默认 3 已通过)、分页
响应:SubmissionVO 数组 + idCardMask(开放接口一律不返回明文证件号)
14.3 GET /open/v1/tasks/{id}/files
Query:submissionIds(逗号分隔,≤200)
响应
{
"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
说明:认定机构提供回调地址,系统在终审通过后主动推送。
请求(我方 → 机构)
{
"eventType": "SUBMISSION_APPROVED",
"occurTime": "2026-09-28T17:00:00+08:00",
"taskId": 2026090001, "taskCode": "2026090001",
"submissionId": 5001,
"data": { "realName": "曾伊豪", "idCardMask": "3610292009******10",
"occupation": "车工", "worktype": "数控车工", "jobLevel": "四级" },
"files": [ { "materialCode": "IDCARD", "url": "https://...", "expireSeconds": 1800 } ]
}
重试策略:失败重试 5 次,间隔 1/5/30/120/600 秒;全部失败后标记并告警,支持人工重推。
§15 错误码全集
在 PRD 附录 D(20 个码)基础上新增 34 个(标 ⭐),本节共 54 个(含
0成功码)。所有码在code字段返回,HTTP 状态码见 §1.3。
15.1 客户端错误(4xxxx)
| 码 | 含义 | 端处理 |
|---|---|---|
40001 |
存在阻断性校验问题,无法提交 | 展示 blocking 清单,引导逐项处理 |
40002 |
文件格式不在白名单 | 提示支持的格式 |
40003 |
文件体积超上限且压缩受限 | 提示重新提供 |
40004 |
身份证号校验位不通过 | 高亮错误位,提示核对原件 |
40005 |
证件号与名册不一致 | 展示名册值,要求确认 |
40006 |
疑似串档 | 转人工,不自动修正 |
40007 |
门禁未匹配 | 统一提示"没有找到你的信息",不透露哪项错 |
40071 |
门禁尝试过于频繁 | 提示"操作过于频繁,请稍后再试" |
40072 |
名单项已被他人绑定 | 提示"该信息已被登记" |
40008 |
任务已截止 / 已归档 / 已暂停 | 禁止提交 |
40009 |
重复提交(幂等键冲突) | 返回既有结果 |
40010 |
驳回次数超上限 | 提示联系班主任 |
⭐ 40011 |
批量操作超过 200 条上限 | 分批提交 |
⭐ 40012 |
账号或密码错误 | 重新输入(不透露账号是否存在) |
⭐ 40013 |
账号已锁定 | 提示剩余锁定时间 |
⭐ 40014 |
用户有在办任务,不可停用 | 提示先移交任务 |
⭐ 40015 |
组织被占用,不可删除 | 展示占用清单 |
⭐ 40016 |
名册为空,不可发布 | 引导导入名册 |
⭐ 40017 |
名册缺少「姓名」或「身份证号」列 | 引导重新导入 |
⭐ 40018 |
未配置必传材料项 | 引导配置材料 |
⭐ 40019 |
未设置覆盖班级或班主任 | 引导配置 |
⭐ 40020 |
命名规则试跑未通过 | 展示失败样例,引导修规则 |
⭐ 40021 |
仅已发布任务可暂停 | 刷新状态 |
⭐ 40022 |
仅已暂停任务可恢复 | 刷新状态 |
⭐ 40023 |
存在待审单,不可归档 | 展示待审单 |
⭐ 40024 |
报考范围未覆盖全部名册 | 返回差集(漏人查验) |
⭐ 40025 |
表单版本冲突 | 返回最新 schema,前端差异合并 |
⭐ 40026 |
内置字段不可删除 | 提示可改标签/顺序 |
⭐ 40027 |
材料项已有文件,不可删除 | 提示改为隐藏 |
⭐ 40028 |
无法自动定位表头行 | 返回前 10 行预览,要求手动指定 |
⭐ 40029 |
名册项已绑定/已提交,不可删除 | 提示改为"不参与本次认定" |
⭐ 40030 |
已进入审核,需先撤回才能删材料 | 引导撤回 |
⭐ 40031 |
已通过终审,学生不可撤回 | 提示联系管理员 |
⭐ 40032 |
存在红色问题,不允许通过 | 必须先驳回让学生修正 |
⭐ 40033 |
已导出打包,不可撤回审核 | 已交付数据不可追溯修改 |
⭐ 40034 |
已归档,不可撤回 | — |
⭐ 40035 |
存在不合格文件,禁止导出 | 禁用导出按钮 + 展示 blocking 清单 |
⭐ 40036 |
字典项有下级,不可停用 | 先处理下级 |
⭐ 40041 |
名册中该证件号已被占用 | 不覆盖,提示管理员核查 |
⭐ 40043 |
文件归属存疑(文件名指向名册中的另一个人,或归属无法确证) | 阻断提交,提示"这份材料可能不是你的",引导重新上传(无"仍然继续"选项) |
⭐ 40044 |
一期不支持该收集模式 / 认定类型 | 请使用学校收集 + 自主认定(见 PRD「一期范围声明」) |
⭐ 40045 |
角色还有账号在用,不可删除 | 先给这些账号换一个角色(界面会说明还有几个账号) |
40101 |
未登录 / token 失效 | 静默重新登录 |
40301 |
无权限(含跨租户) | 跳转 403 |
40302 |
签名 URL 已过期 | 重新获取 |
42901 |
触发限流 | 退避重试 |
15.2 服务端错误(5xxxx)
| 码 | 含义 | 端处理 |
|---|---|---|
50001 |
文件处理失败 | 保留原图,允许重试 |
50002 |
OCR 服务异常 | 降级为人工填写 |
50003 |
图片过大被熔断 | 转异步处理 |
⭐ 50004 |
VLM 服务异常 | 降级为规则结果或不判(识别失败一律放行) |
⭐ 50005 |
导出打包失败 | 保留中间态,允许重试(不重复计费) |
⭐ 50006 |
通知发送失败 | 记录 fail_reason,走降级链 |
⭐ 50007 |
存储写入失败 | 重试 3 次后提示用户重传 |
降级原则(贯穿全部 5xxxx):任何服务端能力失效都不得阻断学生提交。OCR 挂了就手填,VLM 挂了就不判,识别失败就放行。这是填报完成率的底线保障。
§16 变更记录
| 版本 | 日期 | 变更 |
|---|---|---|
| V2.7 | 2026-10 | 需求收敛:删角色 / 删两模块 / 取消二期。① 内置角色 6 → 4(删 DEPUTY_TEACHER 副班主任、AUDITOR 审核员;挂这两个角色的存量账号由 V19 迁移直接删除);② 删「历史整理器」(5 个接口 + cleanup:run 权限码 + collect_cleanup_job 表 + §13.2 整节)与「加入申请」(5 个接口 + join:audit 权限码 + collect_join_request 表 + §8.7 整节 + 错误码 40037/40038/40039/40040/40042);③ collect_task_org.deputy_user_ids(协办人)整列删除,审核范围由三级回退收窄为两级(head_user_id → sys_org.leader_user_id);④ 不再有任何二期功能设计 —— 社会收集 / 社会评价 / 专项职业能力 / 数据范围可配 / 角色变更即时失效 / 平台角色页一律不做,OCR 与材料识别改为「缓做,等供应商选型」;⑤ 内置材料模板 3 → 1(仅 TPL_SCHOOL_ROLL),XJW_BA / OTHER_PROOF 读得到、选不到。数字:契约 129 → 119、错误码 59 → 54、权限码 30 → 28、PC 菜单 12 → 10、小程序页面 10 → 9、原型 32 → 30 屏。⚠️ 加入申请(确认函 B1)是客户唯一确认的功能增量,本次属需求回退,建议补变更确认单。详见《交接摘要》§一·三十六 |
| V2.6 | 2026-09-30 | 六处口径修正 + 新增 §17(全部经客户确认):① §2.7 notifyBizType 5/6 对调修正(5 催报 / 6 导出完成,F-30);② §2.7 auditAction 补全为 7 个动作、撤回=7(F-7);③ §3.6/§9.5 source 改为建表口径「谁传的」(F-38/39);④ §9.5 改为「上传前置检查」定位 + §9.6 process 语义改为「有原始件时的重新规范化」(F-3/71/72);⑤ §9.9 OCR 标注一期缓做;⑥ §11.4 撤回约束补「状态 2 可撤(撤初审)」、留痕 action=7;⑦ §12.1 validation/run 改同步并返回统计(F-51);⑧ 新增 §17 实现补充与偏差记录(41 个 §4 未列接口按六类分组 + 12 条已确认偏离 D-1~D-12)。同步记录见《08-完成度盘点与问题清单》 |
| V2.5 | 2026-09-30 | 两处"文档内部口径冲突"加注(不改口径,只把冲突摆到明处):① §2 枚举 notifyBizType 的 5/6(本文件写「5 导出完成 / 6 催报」)与 §13.5 消息中心示例、落库值 collect_notify_record.biz_type(「5 催报 / 6 导出完成」)不一致 —— 实现以 §13.5 为准,需要客户确认以哪边为准(若以 §2 为准,要同时改两个常量与已落库数据);② §3.6 与 §9.5 的 collect_file.source 字段表写的是"入口来源"(1 聊天记录 / 2 相册 / 3 手机文件 / 4 代传),而建表注释与实现(CollectFile.SOURCE_*)是"谁传的"(1 本人上传 / 2 班主任代传 / 3 管理员代传 / 4 整理器导入)—— 实现以建表口径为准(§10.6 要求代传可追溯,要回答的是"谁替他传的")。两处均已在原文加 ⚠️ 注。记录见《08-完成度盘点与问题清单》F-30 / F-38 / F-39。 |
| V2.4 | 2026-09-28 | 修正 3 处加密列宽不足(DDL 缺陷):sys_tenant.contact_phone、sys_user.phone、collect_roster.phone 原为 varchar(20),是按明文手机号的长度建的;但 PRD §10.3 要求手机号 AES-256 加密存储,AES-GCM 密文约 52 字符,插入会直接报 value too long for type character varying(20)。(id_card_no 当初给了 varchar(128) 就是考虑到了加密,手机号三列漏了——属设计不一致。)三列统一改为 varchar(128),与 id_card_no 对齐。发现方式:M0 开通租户实测时数据库报错暴露。审计结论:其余 *_mask varchar(32) 列存的是脱敏值(如 138****8888)而非密文,宽度足够,无需改动。 |
| V1.2 | 2026-09-28 | 错误码规范化:40007b / 40007c 带字母后缀,与 §1.3「code 为数字」的约定冲突(JSON 里会变成字符串,前端 code === 0 判断失效)。改为纯数字:40007b → 40071(门禁限流)、40007c → 40072(名单项已被绑定)。总码数不变,仍 58 个。 |
| V1.0 | 2026-09-28 | 首版。PRD §8 的 96 个接口全部展开字段级定义;补齐追溯矩阵审计发现的 10 个缺口(新增 21 个接口,含 1 个登记遗漏);错误码由 20 个扩至 50 个;新增 §2 枚举全集、§3 通用数据结构、§15 错误码全集 |
| V1.1 | 2026-09-28 | 客户要求移除「联系方式展示」:删除 GET /api/v1/mp/roster/contact(原用于门禁失败页展示班主任脱敏手机号)。
理由:学生在班级群里本来就有班主任联系方式,系统内再放一份既不必要(用户已知怎么找人)、有维护成本(换人换号要同步)、又多一处隐私暴露面。
门禁失败页改为只保留两条系统内动作:重新填写 / 申请加入名单(FR-509)。纯文案提示("如有疑问请联系老师")保留,因不涉及任何联系方式数据。
接口总数 122 → 121 |
| V1.0 | 2026-09-28 | 首版。PRD §8 的 96 个接口全部展开字段级定义
自校验记录
| 检查项 | 结果 |
|---|---|
| §4 各模块表行数合计 | 119 ✓ 与声称一致 |
| §4 内路径重复 | 无 ✓ |
| Markdown 表格列数一致性 | 0 行不符 ✓ |
| 文档引用的字段名 | 116 个唯一值,全部存在于 DDL ✓ |
| 错误码数量 | 54 个(新增 34 个标 ⭐)✓ |
| 文件规模 | 约 89 KB / 2870 行 |
§17 实现补充与偏差记录
§4 的 119 个是设计口径。本节回答两件事:① 代码里还有哪些 §4 没列的接口; ② 哪些地方有意偏离了本文档的原始写法、为什么、谁确认的。
⚠️ 17.1 的清单由工具生成:
python scripts/check-api-parity.py --extra(当前 38 个,2026-10 复核)。手抄清单会过期,以命令输出为准; 下表按"为什么不在 §4"分组,便于决定"补进 §4 还是留在本节"。
17.1 代码有、§4 清单没有的接口(38 个,按偏离原因分组)
A · 材料访问通道(7 个) —— 审核端与学生端各一条通道(审核端要看防伪纹,学生端只看自己的);上传是一步 multipart
| 接口 | 说明 |
|---|---|
POST /api/v1/mp/files/upload |
实际的上传入口(一步 multipart,收上来即压缩+改名) |
GET /api/v1/mp/files/{id}/url |
学生端取签名预览链接 |
GET /api/v1/mp/files/{id}/content |
学生端直出内容 |
GET /api/v1/mp/files/submissions/{submissionId} |
按提交单列出材料 |
GET /api/v1/audit/files/{id}/url |
审核端签名链接(独立通道;支持 ?thumb=true 取缩略图) |
GET /api/v1/audit/files/{id}/content |
审核端直出内容 |
GET /api/v1/mp/teacher/files/{fileId}/preview |
教师端(代传场景)预览学生材料 |
B · 微信授权链路代替 POST /mp/login(4 个)
| 接口 | 说明 |
|---|---|
GET /api/v1/mp/h5/authorize |
H5 微信网页授权跳转 |
GET /api/v1/mp/h5/callback |
授权回调(token 放 URL fragment) |
GET /api/v1/mp/h5/stub-login |
仅非生产打桩登录(stub-enabled + profile 双重保护) |
GET /api/v1/mp/scene-context |
匿名扫码落地页查询(scene 三段式,只返回最少信息 + 限流) |
C · 签名 URL 下载(1 个)
| 接口 | 说明 |
|---|---|
GET /api/v1/exports/{id}/file |
导出包的流式下载。§12.6 只定义了"取下载信息";真正下文件走这条,URL 有有效期、不能入库/外发 |
D · 界面与接口必需、清单漏列(16 个)
| 接口 | 说明 |
|---|---|
GET /api/v1/auth/captcha |
图形验证码(连续失败 2 次后必填) |
GET /api/v1/auth/public-key |
密码 RSA 加密传输用的公钥 |
GET /api/v1/material-catalog |
材料目录(码/档位/格式/模板/体积范围) |
POST /api/v1/tasks/{id}/materials/apply-template |
应用内置材料模板(幂等) |
POST /api/v1/tasks/{id}/save-as-template |
保存为模板(§7.13 的缺口,F-62) |
POST /api/v1/audit/submissions/batch-approve |
批量通过(§11.3 标题带过、清单未单列) |
GET /api/v1/mp/teacher/audit/reason-codes |
教师端驳回原因字典(§11.7 有 PC 版) |
GET /api/v1/mp/teacher/submissions/{id}/proxy-context |
代传前取上下文(哪个班、哪份草稿) |
GET /api/v1/qrcodes/{scene} |
匿名出图 |
POST /api/v1/roster/{rid}/deactivate |
名册项"标记不参与"(删除的可逆替代,§8.3/8.4) |
GET /api/v1/tenants/{id} |
租户详情 |
PUT /api/v1/users/{id}/status |
启用/停用(清单只有 DELETE 停用) |
DELETE /api/v1/dicts/items/{id} |
删除自定义字典项(§13.3) |
POST /api/v1/dicts/{code}/items/{itemCode}/toggle |
内置字典项停用/恢复(§13.3) |
GET /api/v1/exports/precheck |
导出前置检查(只检查不创建;界面靠它禁用按钮) |
GET /api/v1/exports/preview |
导出目录树预览(与真正打包共用同一份构建代码) |
E · 契约链条原本断在第一步(8 个,已实现)
| 接口 | 说明 |
|---|---|
POST /api/v1/notify/templates/{code}/customize |
把内置模板复制成可编辑的库行(F-52:内置模板没有 id,PUT /{id} 拿不出 id) |
DELETE /api/v1/notify/templates/{code} |
恢复默认模板 |
GET /api/v1/notify/templates/variables |
变量白名单(前端抄一份迟早不一致) |
POST /api/v1/open-apps · GET /api/v1/open-apps |
§14 的接入方管理(F-67:没有它,appSecret 只能改库产生) |
POST /api/v1/open-apps/{id}/reset-secret |
重置密钥(显式动作,会让对方现有签名立即失效) |
POST /api/v1/open-apps/{id}/toggle |
启停接入方 |
GET /api/v1/open-apps/push-logs |
推送记录(人工重推的依据) |
F · 运维与调试动作(2 个) —— 定时任务在测试里"等不起",所以都有手工入口(权限走运营侧 log:view:all)
| 接口 | 说明 |
|---|---|
POST /api/v1/schedules/run |
手工触发定时任务(job 取值 close/urge/timeout/raw/usage/retry/expire) |
POST /api/v1/exports/cleanup-run |
手工触发导出包过期清理(可重复执行) |
17.2 有意偏离契约的地方(均已确认,2026-09-30)
| # | 偏离点 | 契约原口径 | 实现口径 | 依据 |
|---|---|---|---|---|
| D-1 | notifyBizType 5/6 |
§2.7 旧表「5 导出完成 / 6 催报」 | 「5 催报 / 6 导出完成」(与 §13.5、落库值一致) | F-30;按旧表实现催报去重会静默失效 |
| D-2 | collect_file.source |
§3.6 旧表「入口来源」(聊天记录/相册/手机文件/代传) | 「谁传的」:1 本人 / 2 班主任代传 / 3 管理员代传 / 4 整理器导入(模块已取消,仅历史数据;按建表注释) | F-38/F-39;§10.6 代传可追溯 |
| D-3 | 上传链路 | OSS 直传 + upload-token 凭证 |
服务端一步上传;upload-token 定位为预检+秒传+压缩建议(非安全边界) |
F-3/F-71/F-72;直传需配回调且把凭证交前端 |
| D-4 | process 语义 |
「命名+压缩+识别+校验总入口」 | 上传当场完成这些;process = 有原始件时的重新规范化 |
F-3;默认 keep-raw=false |
| D-5 | POST /validation/run |
async=true + jobId |
同步跑完并返回 checkedSubmissions/issuesFound/unresolvedLeft/elapsedMs |
F-51;全契约没有查 jobId 的接口 |
| D-6 | 撤回动作码 | §11.4 旧文 action=3 |
action=7(撤回);3 是批量通过;状态 2/3 均可撤(2 撤的是初审) |
F-7;时间轴不能混淆 |
| D-7 | §14 路径前缀 | §14 详情写 POST /open/v1/...(无 /api/v1) |
统一 /api/v1/open/v1/...(与 §1 及其余接口一致,网关一条规则) |
F-65 |
| D-8 | biz_type 落库类型 |
§6 字段表按数字码定义 | collect_notify_record.biz_type 是 varchar(32),存数字码的字符串形式("3");接口层仍是数字 |
F-26;DDL 不可改 |
| D-9 | 任务删除 | 已发布任务不可删(40001) |
已归档(status=3)的任务可删:管理员显式动作 + 操作留痕(DELETE_TASK,记行数/文件数);子表级联物理删(名册/材料/审核记录等 14 张表)、任务行本身软删(占住 task_code 不复用 —— 编码是文件目录名) |
F-14;否则任务列表只增不减 |
| D-10 | 租户域名 | 未写 | 能力已实现(Host 定位 + token 授权),默认关闭;启用四步见《交接摘要》§一·二十六 | F-10;客户决定先不做 |
| D-11 | 通知降级链 | 「订阅消息→公众号模板→短信→站内信」四级 | 一期两级(订阅消息→站内信);公众号模板需已认证服务号、短信需采购通道(商务动作),不假装可发 | F-28 |
| D-12 | OCR(§9.9) | 已定义 | 一期缓做(等供应商选型,见《01》D1,需评估数据出境合规) | F-3;119 个接口里唯一未实现 |