Files
xiaotutongxue/docs/03-接口设计.md
T
lovife 0bbac7d228 feat(pc,platform): 验收走查修复(平台按学校代管 + 菜单/403/日志范围/分页)
客户在验收里点出来的问题,一次改完。两段改动:

一、平台运营与租户范围(后端为主)
- 新增请求头 X-Tenant-Id:平台运营是唯一跨租户身份,租户拦截器对它整体关掉了过滤,
  于是「组织架构 / 账号管理 / 工作台」拿到的是所有租户混在一起的数据 —— 而只有一所
  学校有数据时,它和学校管理员看到的完全一样,看不出错(这是最危险的一点)。
  现在这些页面必须先选定学校,服务端再把本次请求的租户上下文换成目标租户:
  只认平台身份(其它身份带头一律忽略)、目标租户必须存在且启用。
- OrgService.tree / UserService.list 补显式 tenant_id 条件(原来只靠拦截器,而拦截器对
  平台不生效);TaskService.seesWholeTenant 改用新的 LoginUser.isPlatformStaff() ——
  isPlatformAdmin 回答"当前租户是不是平台租户",代管时它为 false,会让平台退化成普通
  用户(任务列表掉进"班主任只看自己带的班"分支,得到空列表);PLATFORM_ADMIN 补
  TASK_VIEW(PRD §3.2「查看填报进度看板:平台运营 ✅」)。
- 操作日志对平台永远是空的:SQL 写死 tenant_id = 当前租户,而平台的 tenantId 是 0。
  按契约 §13.4 补 tenantId 参数与 log:viewAll:平台默认看全量、可指定某所学校;
  学校管理员恒为本租户,传别家返回 40301(不静默忽略,否则会让人以为"筛过了")。
- 开放接口"推送重试"从未生效(F-112):定时扫描缺 @InterceptorIgnore,异常被 log.warn
  吞掉;同一方法里"接入方已停用"那条 update 也没有租户上下文,会每 10 秒重试到永远。
  E2E 补了"真的重试了"的断言(原来只断言"安排了下次重试")。

二、PC 端走查体验
- 去掉 3 条无用菜单(名单管理 / 表单设计器 / 材料规则):功能都在「收集任务」行内入口,
  菜单里那一条点进去只有指路;后端菜单键同步删除,entryAt 那套机制一并删掉。
- 班主任点工作台必弹 403:工作台无条件调了要 org:view / progress:viewAll 的两个接口。
  403 走 HTTP 状态码,try/catch 拦不住它的弹窗 —— 改成按权限决定发不发请求。
- 操作日志:分页器被裁在可视区外(新增 .g-list-card 让表体自己滚、表头钉住、分页器留在
  卡片底部)+ 去掉页面上那段免责声明;收集任务的状态切换改成单独一行 tab;去掉顶栏假数据
  铃铛;配色按客户要求还原。

顺带修两处"检查器自己不严":check-wiring 新增第 ⑦ 项(PC 端引用的权限码必须存在于
PermissionDef —— 本轮我自己把 progress:viewAll 写成 progress:view:all,判断恒为 false,
而编译 / typecheck / lint / E2E 全都不红);verify-docs 的表格列数检查不再把 GFM 转义
竖线当成列分隔符。两个检查器都拿"故意写错的样例"试过,确认真的会红。

验证:typecheck / mvn package / check-wiring(未接线声明 0 条)/ verify-docs(22 项)全过;
e2e-m0 36、e2e-m1-orgs 48、e2e-m0-user 55、e2e-m5-logs 44、e2e-m5-notify-wiring 43、
e2e-m1-tenant-domain 6、e2e-m8-open 61 全绿;浏览器按角色逐个真点复验。
详见 docs/交接摘要.md §一·三十三 与 §一·三十四。
2026-10-09 17:29:20 +08:00

131 KiB
Raw Blame History

接口设计说明书

版本: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

{
  "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 的名称陷阱:按机构模板,「职业技能等级个人申报表」的规范文件名是「工作证明」。这是机构既有习惯,不要"修正"。

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 已暂停 (与后端 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 导出完成 / 7 加入名单审批
exportStatus 0 排队中 / 1 打包中 / 2 已完成 / 3 失败 / 4 已过期
cleanupSourceType 1 压缩包 / 2 文件夹 / 3 云盘链接
cleanupStatus 0 排队 / 1 解析中 / 2 处理中 / 3 已完成 / 4 失败

✅ notifyBizType 的 5/6 曾经在本文件内部对调过(已修正,2026-09-30 客户确认): 本表旧版写「5 导出完成 / 6 催报」,与 §13.5 消息中心示例、落库值 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
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 条件 验证码

响应

{
  "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 【已移除】

❌ 本接口已按客户要求移除(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)

响应(导入体检报告,与名册导入同构)

{
  "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

响应

{
  "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 一寸照片 + 身份证 + 学籍表 + 工作证明
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 次/分钟/任务。


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 — 申请说明,如"我是转班过来的"

响应

{
  "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

响应

{
  "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、分页

响应

{
  "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:若该证件号与其他任务的名册匹配上,返回提示(帮管理员判断是否本校学生):

"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

响应

{
  "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

权限:学生 说明:只返回该学生被绑定/在名册中的任务。

响应

{
  "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 的四层递进是二期(M3/M4),见 §17.2 D-12。 下面的响应结构保留作为「规范化产物」的字段参考(上传接口的产物与之同构)。

说明:对有原始件的文件重新执行规范化(压缩 + 改名 + 校验)。

请求

字段 类型 默认 说明
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)。 这是 121 个契约接口里唯一没有实现的一个;表单预填与校验位校验在学生端走人工填写。 选型要同时评估数据出境合规。选型定了之后本节按原设计实现,以下为设计参考。

说明:身份证 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
  }
}

社会收集(二期):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 详情响应(不新增接口) ✅
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}

响应

{
  "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)

{
  "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、分页

范围(权限二选一,@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 管理端创建导出时 导出完成
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)

响应

{
  "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 个码)基础上新增 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.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 各模块表行数合计 121 ✓ 与声称一致
§4 内路径重复 无 ✓
Markdown 表格列数一致性 0 行不符 ✓
文档引用的字段名 116 个唯一值,全部存在于 DDL ✓
错误码数量 58 个(新增 38 个标 ⭐)✓
文件规模 约 100 KB / 2570 行

§17 实现补充与偏差记录

§4 的 121 个是设计口径。本节回答两件事:① 代码里还有哪些 §4 没列的接口; ② 哪些地方有意偏离了本文档的原始写法、为什么、谁确认的。

⚠️ 17.1 的清单由工具生成:python scripts/check-api-parity.py --extra (当前 41 个,生成于 2026-09-30)。手抄清单会过期,以命令输出为准; 下表按"为什么不在 §4"分组,便于决定"补进 §4 还是留在本节"。

17.1 代码有、§4 清单没有的接口(41 个,按偏离原因分组)

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 · 界面与接口必需、清单漏列(17 个)

接口 说明
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 导出目录树预览(与真正打包共用同一份构建代码)
GET /api/v1/cleanup/jobs/{id}/result 整理器报告(§13.2 界面轮询用)

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 · 运维与调试动作(4 个) —— 定时任务在测试里"等不起",所以都有手工入口(权限走运营侧 log:view:all)

接口 说明
POST /api/v1/schedules/run 手工触发定时任务(job 取值 close/urge/timeout/raw/usage/retry/expire)
POST /api/v1/exports/cleanup-run 手工触发导出包过期清理(可重复执行)
POST /api/v1/cleanup/jobs/json 整理器:直接提交 JSON(不走上传)
POST /api/v1/cleanup/jobs/parse-preview 整理器:解析预览(不落库)

17.2 有意偏离契约的地方(均已确认,2026-09-30)

# 偏离点 契约原口径 实现口径 依据
D-1 notifyBizType 5/6 §2.7 旧表「5 导出完成 / 6 催报」 「5 催报 / 6 导出完成」(与 §13.5、落库值一致) F-30;按旧表实现催报去重会静默失效
D-2 collect_file.source §3.6 旧表「入口来源」(聊天记录/相册/手机文件/代传) 「谁传的」:1 本人 / 2 班主任代传 / 3 管理员代传 / 4 整理器(按建表注释) F-38/F-39;§10.6 代传可追溯
D-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;121 个接口里唯一未实现