Files
xiaotutongxue/docs/03-接口设计.md
T
lovife 63aec55329 docs: 产品文档与交互原型(P1-P7 交付物)
一期范围:学校收集 + 自主认定(4 类材料全部有实测样本)
- 主 PRD 190KB:125 条 FR / 21 条 VAL / 20 张表 DDL / 121 个接口
- 功能逻辑卷 88KB:不含代码模块
- 接口设计 116KB:58 个错误码 / 12 个 VO
- 核心模块设计:M1 命名 / M2 压缩 / M3 识别 / M4 门禁 / M5 校验 / M6 导出
- 开发任务拆解:100 任务 / 387 人日 / 15 周
- 验收测试清单:36 条 Golden 断言 / 111 条功能验收
- PostgreSQL DDL:已在 PG 16.15 实测零错误
- 交互原型 31 屏:学生端 11 / 班主任端 9 / 管理端 11
2026-09-28 16:19:19 +08:00

114 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 天(静默续期)

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 次/分钟 40007b
上传凭证 200 次/分钟/用户 42901
OCR 30 次/分钟/用户 42901
催报 10 次/分钟/任务 42901
导出创建 5 次/分钟/租户 42901

门禁限流是安全设计的一部分(防身份证号枚举探测),阈值调低是刻意的。上线后按真实人数标定。

1.7 脱敏规则

字段 明文传入 响应返回
身份证号 ✅ 允许(HTTPS,仅门禁/导入场景) ❌ 一律返回脱敏值 idCardMask,形如 3610292009******10
手机号 ✅ 允许 ❌ 返回脱敏值 138****8888
姓名 ✅ ✅ 明文(业务必需)

例外:GET /audit/submissions/{id} 等审核场景,管理员/班主任可见完整身份证号(审核必需),但仍不返回明文——返回 idCardFull 需通过 GET /audit/submissions/{id}/idcard 单独获取,该接口单独记录审计日志。

这条设计的意义:审核人员看材料图片时不需要抄身份证号,真需要核验时走单独接口、留痕可追溯。

1.8 其他约定

项 约定
ID 类型 int64,JSON 中以 number 传输(生成规则保证 < 2^53)
时间 ISO 8601 带时区,2026-09-28T14:30:00+08:00;日期型 2026-09-28
体积 统一 KB(finalSizeKb),原始字节用 rawSizeBytes
比例 字符串百分比 "97.1%"(避免前端再乘 100)
空值 统一 null,不用空字符串 ""
枚举 数字码(与 DDL smallint 对齐),文案由前端映射;唯一例外:materialCode 用字符串码(可读性优先,见 §2.1)
排序 数组顺序即业务顺序,不额外传 sortNo 以外的字段
批量上限 单次批量操作 ≤ 200 条;超限返回 40011

布尔字段命名:统一 isXxx / hasXxx / needXxx,不用 0/1(JSON 原生 true/false)。


§2 枚举字典全集

与数据库 smallint 列严格对齐。「无」是合法业务值,见 §2.6。

2.1 材料类型 materialCode(字符串码)

编码 名称 别名(识别用) 默认压缩档
PHOTO1 一寸照片 一寸电子照片 / 证件照 / 免冠照 / 1寸照片 P1
IDCARD 身份证 身份证正面 / 身份证反面 / 身份证人像面 P2
SCHOOL_ROLL 学籍表 学籍证明 / 在读证明 / 在校证明 P4
XJW_BA 学信网备案表 学信网 / 学历备案表 / 电子注册备案表 P4
WORK_PROOF 工作证明 个人申报表 / 职业技能等级个人申报表 P3
OTHER_PROOF 其它佐证材料 其他 / 补充材料 P3

⚠️ WORK_PROOF 的名称陷阱:按机构模板,「职业技能等级个人申报表」的规范文件名是「工作证明」。这是机构既有习惯,不要"修正"。

2.2 收集模式 collectMode

值 含义 审核流
1 学校收集 学生 → 班主任初审 → 管理员终审
2 社会收集 提交后直接进管理员终审队列(跳过初审)。⚠️ 一期不启用

2.3 认证类型 certType

值 含义 面向谁 特有约束
1 自主认定 本单位职工 / 本校学生 要学籍表
2 社会评价 社会人员 材料为学信网备案表 + 其它佐证(无学籍表)。⚠️ 一期不启用(零样本材料)
3 专项职业能力 专项能力考核 ⚠️ 无工种 / 无等级 / 无补考科目(0 科)。⚠️ 一期不启用

⚠️ 社会评价(认定类型)≠ 社会收集(collectMode): 前者决定填什么材料,后者决定谁审核(有无班主任初审层)。二者可任意组合。

2.4 填报单状态 submissionStatus

值 含义 可流转到
0 草稿 1
1 待初审 2 / 4
2 待终审 3 / 4
3 已通过 5
4 已驳回 1(学生重提交)
5 已归档 —

2.5 问题级别 severity

值 含义 UI 色 阻断提交
1 信息 蓝 否
2 警告 黄 否(可 confirmWarning 强提)
3 错误 红 是

2.6 「无」的语义(三条铁律)

这是最容易实现错的地方,单列说明。

场景 错误做法 正确做法
学历为"无" 存 null 存字符串 "无"——它是合法业务值
专项职业能力无工种 存 null 存 null + certType=3,由 certType 决定字段是否参与校验
补考科目为"无" 存 null 存 [] 空数组(0 科是合法值)

判定原则:「用户选择了无」与「用户没填」必须能区分。前者是有效数据,后者是缺失。

2.7 其他枚举

枚举 取值
userType 1 平台运营 / 2 学校管理员 / 3 班主任 / 4 学生
roleCode PLATFORM_ADMIN / SCHOOL_ADMIN / HEAD_TEACHER / DEPUTY_TEACHER / STUDENT / AUDITOR
taskStatus 0 草稿 / 1 已发布 / 2 已暂停 / 3 已截止 / 4 已归档
rosterSubmitStatus 0 未提交 / 1 已提交 / 2 已通过 / 3 已驳回
fileValidateStatus 0 待校验 / 1 通过 / 2 警告 / 3 不通过 / 4 待人工
auditNode 1 初审 / 2 终审
auditAction 1 通过 / 2 驳回 / 3 撤回 / 4 代传
detectSource 1 文件名 / 2 形态 / 3 OCR 内容 / 4 VLM / 5 用户指定
compressProfile P1 证件照 / P2 证件翻拍 / P3 A4 扫描件 / P4 截图 / P5 兜底
notifyChannel 1 微信订阅消息 / 2 短信 / 3 公众号模板
notifyBizType 1 任务发布 / 2 临期提醒 / 3 驳回通知 / 4 审核通过 / 5 导出完成 / 6 催报 / 7 加入名单审批
exportStatus 0 排队中 / 1 打包中 / 2 已完成 / 3 失败 / 4 已过期
cleanupSourceType 1 压缩包 / 2 文件夹 / 3 云盘链接
cleanupStatus 0 排队 / 1 解析中 / 2 处理中 / 3 已完成 / 4 失败

§3 通用数据结构

下列对象在多个接口中复用。字段名与数据库列名一一对应(下划线转驼峰),便于后端直接映射,避免二次改名。 表格中「↔ 列」列标注该字段对应的 DDL 列,— 表示计算字段。

3.1 TaskVO 任务

字段 类型 ↔ 列 说明
id int64 id 任务 ID
taskCode string task_code 任务编码,如 2026090001
taskName string task_name 任务名称
collectMode int collect_mode 1 学校 / 2 社会
batchNo string batch_no 批次号,如 202609
majorName string major_name 专业名称
jobName string job_name 职业(工种)名称
jobLevel string job_level 等级,如 四级
certType int cert_type 见 §2.3
startTime / endTime string start_time / end_time 填报起止
auditConfig object audit_config 见 3.2
namingTemplate string naming_template 命名模板
compressConfig object compress_config 见 3.11
rosterRequired bool roster_required 是否强制门禁(当前恒为 true)
status int status 见 §2.7 taskStatus
publishedTime string published_time 发布时间
stat object — 统计快照,见 3.3
createTime string create_time —

3.2 AuditConfigVO 审核配置

字段 类型 默认 说明
needClassAudit bool true 是否需班主任初审(社会收集恒为 false)。一期不启用社会收集
needAdminAudit bool true 是否需管理员终审
classAuditTimeoutHours int 48 初审超时提醒
adminAuditTimeoutHours int 72 终审超时提醒
maxRejectCount int 3 驳回次数上限,超限升级管理员
allowProxyUpload bool true 允许班主任代传
batchApproveOnlyGreen bool true 批量通过只带无问题项(不可关闭)

3.3 TaskStatVO 任务统计

字段 类型 说明
rosterTotal int 应填人数(名册行数)
submittedCount int 已提交数
approvedCount int 已通过数
pendingCount int 待审数
rejectedCount int 已驳回数
unsubmittedCount int 未提交数
submitRate string 已提交率(submittedCount / rosterTotal)
passRate string 通过率(approvedCount / rosterTotal)
abnormalCount int 有红/黄问题的单数
avgAuditHours number 平均处理时长(小时)

⚠️ submitRate 与 passRate 必须分开报。原型定稿的口径是「完成率 82% 是已提交率 23/28」,不要合并成一个数字——已提交≠已通过。

3.4 MaterialDefVO 材料项定义

字段 类型 ↔ 列 说明
id int64 id —
materialCode string material_code 见 §2.1
materialName string material_name 展示名
aliases string[] aliases 别名集合(识别用)
namePattern string name_pattern 命名模板,如 {姓名}-{证件号}-{材料名}{后缀}
required bool required 是否必传
visibleWhen object visible_when 条件显示,如 { "field": "is_student", "op": "eq", "value": true }
fileTypes string[] file_types 格式白名单 ["jpg","jpeg","png","pdf","heic"]
maxSizeKb int max_size_kb 体积上限(默认 1024)
minSizeKb int — 体积下限保护,低于此值提示"可能不是原图"
compressProfile string compress_profile P1~P5
minLongSide int min_long_side 分辨率下限(长边 px)
minQuality int min_quality 压缩质量下限(默认 70)
targetDpi int target_dpi 目标 DPI(默认 300)
fixedSize object fixed_size 固定尺寸,如 { "width": 295, "height": 413 }
outputFormat string output_format 输出格式;null = 保持原格式
maxCount int max_count 最多可传份数
mergeEnabled bool merge_enabled 是否允许多图合并为一份(如身份证正反面)
ocrType string ocr_type IDCARD / NONE
sampleUrl string sample_url 示例图
tipText string tip_text 来源引导语(显示在学生端)
sortNo int sort_no 排序

3.5 FormSchemaVO 表单定义

字段 类型 说明
version int 版本号,已发布任务改表单必须递增
isCurrent bool 是否当前生效版本
fields array 字段数组,见下
fields[].key string 字段键,内置字段与 collect_submission 列名一致
fields[].label string 展示标签
fields[].type string text / number / select / cascade / date / idcard / phone / bool
fields[].builtin bool 是否内置字段(内置不可删除)
fields[].locked bool 是否锁定(学生不可改)
fields[].required bool 必填
fields[].dictCode string 选项来源字典码
fields[].options array 手填选项
fields[].validate object { "regex": "...", "min": 0, "max": 0, "message": "..." }
fields[].visibleWhen object 条件显示规则
fields[].tip string 字段提示
fields[].sortNo int 排序

内置字段清单(21 个,builtin: true)

分类 字段 key 学生端权限
身份 real_name、id_card_no 🔒 锁定
报考 occupation、worktype、job_level 🔒 锁定
学历 education、is_student、school、major、enroll_date ✏️ 可核对修改
工作 work_unit、work_years ✏️ 可核对修改
证书 cert_occupation、cert_level、cert_no ✏️ 可核对修改
补考 is_retake、retake_theory、retake_practice、retake_overall ✏️ 可核对修改
联系 phone ✏️ 可核对修改

锁定三项的理由:姓名、身份证号、报考三项一旦改动,会与名册、报考范围、导出模板全部失配,必须由老师改。 改动留痕:学生修改任一预填字段,写入 collect_submission.changed_fields(JSON 数组),审核时高亮显示。

3.6 RosterVO 名册项

字段 类型 ↔ 列 说明
id int64 id 名册项 ID
seqNo int seq_no 序号
studentNo string student_no 学号
realName string real_name 姓名
namePinyin string name_pinyin 姓名拼音(形近字纠错用)
idCardNo string — ⚠️ 仅导入时可传,响应一律返回 idCardMask
idCardMask string id_card_mask 脱敏证件号
idCardHash string id_card_hash 哈希(核验用,不可逆)
phone string — 手机号(响应脱敏)
phoneMask string — 脱敏手机号
orgId / orgName int64 / string org_id 所属班级
seqNo int seq_no —
occupation / worktype / jobLevel string ext_json 报考三项(来自导入,非独立字段)
extJson object ext_json 其它列
bindStatus int bind_user_id 0 未绑定 / 1 已绑定
bindTime string bind_time 绑定时间
bindType int bind_type 1 本人 / 2 代填
submitStatus int submit_status 见 §2.7

3.7 SubmissionVO 填报单

字段 类型 ↔ 列 说明
id int64 id 填报单 ID
taskId int64 task_id —
rosterId int64 roster_id 名册项
formVersion int form_version 填报时的表单版本
formData object form_data 表单值(内置字段在此冗余一份快照)
extrasJson object extras_json 扩展字段值
changedFields string[] changed_fields 学生改过的预填字段列表(审核高亮用)
status int status 见 §2.4
rejectCount int reject_count 累计驳回次数
lastRejectReason object last_reject_reason { reasonCode, reasonText }
redCount / yellowCount int red_count / yellow_count 红/黄问题数
materials array — MaterialFileVO[](见 3.8),仅详情接口返回
issues array — ValidationIssueVO[](见 3.9),仅详情接口返回
submitTime string submit_time 提交时间
classAudit object — 初审信息 { auditorName, auditTime, action }
adminAudit object — 终审信息
createTime string create_time —

展开字段(formData 内的扁平字段,与 DDL 列同名)

real_name · id_card_no · occupation · worktype · job_level · education · is_student · school · major · enroll_date · work_unit · is_retake · retake_theory · retake_practice · retake_overall · work_years · cert_occupation · cert_level · cert_no

这些字段在 collect_submission 里是正式列(不是 JSON),所以可以直接 WHERE education = '无' 做统计。extras_json 只存扩展字段。

3.8 MaterialFileVO 材料文件

字段 类型 ↔ 列 说明
id int64 id 文件 ID
materialCode string material_code 所属槽位
version int version 版本号
isCurrent bool is_current 是否当前版本
originalName string original_name 学生原始文件名
standardName string standard_name 规范文件名
fileExt string file_ext 扩展名
mimeType string mime_type —
rawSizeKb number raw_size 原始体积(KB)
finalSizeKb number final_size 处理后体积(KB)
compressRatio string — 压缩率,如 "97.1%"
previewUrl string — 预览签名 URL(有效期 30 分钟)
width / height int width 像素尺寸
dpi int dpi 实际 DPI
ocrStatus int ocr_status 0 未跑 / 1 成功 / 2 失败 / 3 跳过
ocrConfidence number ocr_confidence 置信度 0–1
detectedMaterialCode string detected_material_code 系统识别结果
detectConfidence number detect_confidence 识别置信度
detectSource int detect_source 见 §2.7 detectSource
detectConflict bool detect_conflict 识别结果与所在槽位冲突
needManualPlace bool need_manual_place 需人工分拣
vlmCalled bool vlm_called 是否调用过 VLM(成本监控用)
validateStatus int validate_status 见 §2.7 fileValidateStatus
issues array — 该文件的 ValidationIssueVO[]
source int source 1 聊天记录 / 2 相册 / 3 手机文件 / 4 代传
rawExpireTime string raw_expire_time 原图过期时间(默认 30 天)

ownership.verdict 取值(对应 PRD §6.1 归属裁决矩阵)

verdict 含义 blockSubmit 学生端表现
NO_IDCARD_IN_NAME 文件名无证件号(正常) false 无提示
CONFIRMED_SAME 证件号 = 名册值 false 无提示
BELONGS_TO_OTHER 证件号指向名册中的另一个人 true 阻断,提示文件名对应的是谁
UNKNOWN_IDCARD 证件号合法但名册查不到 视 OCR 而定 无 OCR 佐证时阻断
CHECKSUM_INVALID 校验位失败(只是写错了) false 信息级"已按名册修正"
NO_ROSTER 本次无名单可依 false 标黄

⚠️ canForce: false 是硬约定:归属问题不提供"仍然继续"。 与 VAL-08(分辨率偏低)这类警告不同——那种可以"仍然使用",这个不行。

detectSource 的展示约定:审核辅助显示「系统判定:身份证 92%」,其中档位由 detectSource 决定可信度标注:

detectSource 展示文案
1 文件名 文件名匹配
2 形态 形态识别
3 OCR 内容 内容识别
4 VLM 智能识别(置信度 < 0.8 时不显示,避免误导)

3.9 ValidationIssueVO 校验问题

字段 类型 ↔ 列 说明
id int64 id —
materialCode string — 关联材料(可为空)
fileId int64 file_id 关联文件
ruleCode string rule_code 如 VAL-05
severity int severity 见 §2.5
fieldKey string field_key 关联字段(表单类问题)
title string title 标题
detail string detail 详情
expectedVal string expected_val 期望值(脱敏)
actualVal string actual_val 实际值(脱敏)
autoFixed bool auto_fixed 是否已自动修复
resolved bool resolved 是否已解决
resolvedBy string resolved_by 处理人

3.10 AuditRecordVO 审核记录

字段 类型 说明
id int64 —
auditNode int 1 初审 / 2 终审
auditorId / auditorName int64 / string 审核人
action int 见 §2.7 auditAction(含 3 撤回)
reasonCode string 驳回原因码(见 PRD 附录 C)
reasonText string 手填原因
materialCodes string[] 关联材料
durationSec int 从提交到本次操作的耗时
createTime string —

3.11 CompressConfigVO 压缩配置

字段 类型 默认 说明
defaultMaxSizeKb int 1024 全局体积上限(D1 决策:无例外)
defaultTargetDpi int 300 A4 扫描件目标 DPI
defaultQuality int 85 默认质量
minQuality int 70 质量下限(低于此值标记"强制压缩")
hardFloorDpi int 200 DPI 硬下限
maxPixels int 50000000 超大图熔断阈值(5000 万像素)
keepPngForScreenshot bool true 截图类保持 PNG 不转 JPG
markWhenForced bool true 强制压缩时打标记进抽检清单

3.12 其余对象(简表)

对象 字段
OrgVO id parentId orgName orgType orgCode ancestorPath leaderUserId leaderName sortNo status children[] studentCount
TenantVO id tenantCode tenantName tenantType packageId packageName expireTime status logoUrl configJson usage{ studentCount, storageMb, ocrCount, smsCount }
UserVO id username realName phone orgId orgName roleCodes[] userType status lastLoginTime
ExportTaskVO id taskId exportName scopeJson splitSizeMb desensitize status progress fileCount fileSizeMb resultFiles[]{ name, sizeMb, url } errorMsg expireTime finishTime
NotifyRecordVO id taskId channel bizType receiverId receiverName receiverRole templateCode content status failReason sendTime
CleanupJobVO id jobName sourceType sourcePath targetTaskId status progress totalFiles resultPath reportJson finishTime
DictItemVO id dictCode dictName itemCode itemName sortNo parentItemCode status builtin
MessageVO id bizType title content read linkType linkId createTime

§4 接口总览

合计 121 个:PRD §8 已定义 96 个 + 追溯矩阵补齐 21 个 + 客户确认新增 5 个,减去客户要求移除的联系方式接口 1 个(标 ⭐)。补齐项见 §4.9 索引,客户确认新增见 §8.7。 「权限」列:公开 / 学生 / 教师 / 管理 / 运营(平台方)。 「修补」列指该接口是补齐追溯矩阵审计发现的缺口。

分类 接口数
认证与账号 13
组织与租户 15
任务与配置 30
名册 12
填报端 · 小程序 16
班主任移动端 10
审核 9
校验 / 导出 / 通知 12
开放接口 4
合计 121

4.1 认证与账号(13)

路径 说明 权限
POST /auth/login PC 账号密码登录 公开
POST /auth/logout 退出 全部
GET /auth/me 当前用户 + 权限菜单 全部
PUT /auth/password 修改密码 全部
POST /mp/login 小程序 code 换 session 公开
POST /mp/bind-phone 微信手机号快捷绑定 学生/教师
POST /mp/roster/match 门禁核验 学生
GET /users 用户列表 管理
POST /users 新增用户 管理
PUT /users/{id} 修改用户 管理
DELETE /users/{id} 停用用户 管理
POST /users/import 批量导入 管理
POST /users/{id}/reset-password 重置密码 管理

4.2 组织与租户(15)

路径 说明 权限
GET /orgs/tree 组织树 管理
POST /orgs 新增组织 管理
PUT /orgs/{id} 修改 管理
DELETE /orgs/{id} 删除 管理
POST /orgs/import 批量导入 管理
GET /tenants 租户列表 运营
POST /tenants 开通租户 运营
PUT /tenants/{id} 修改租户/套餐 运营
GET /tenants/{id}/usage 用量统计 运营
⭐ GET /dicts 字典列表 管理
⭐ GET /dicts/{code}/items 字典项 管理
⭐ POST /dicts/{code}/items 新增字典项 管理
⭐ PUT /dicts/items/{id} 修改字典项 管理
⭐ GET /logs/operations 操作日志 管理
⭐ GET /logs/logins 登录日志 管理

4.3 任务与配置(30)

路径 说明 权限
GET /tasks 任务列表 管理
POST /tasks 创建任务 管理
GET /tasks/{id} 任务详情 管理/教师
PUT /tasks/{id} 修改任务 管理
DELETE /tasks/{id} 删除(仅草稿) 管理
POST /tasks/{id}/publish 发布 管理
POST /tasks/{id}/close 截止 管理
POST /tasks/{id}/archive 归档 管理
⭐ POST /tasks/{id}/pause 暂停 管理
⭐ POST /tasks/{id}/resume 恢复 管理
POST /tasks/{id}/clone 克隆 管理
GET /tasks/{id}/orgs 覆盖班级与班主任 管理
PUT /tasks/{id}/orgs 设置覆盖班级/班主任 管理
⭐ GET /tasks/{id}/plan-scope 报考范围读取 管理
⭐ PUT /tasks/{id}/plan-scope 报考范围确认 管理
GET /tasks/{id}/form-schema 表单定义 管理
PUT /tasks/{id}/form-schema 保存表单(版本化) 管理
GET /tasks/{id}/materials 材料项列表 管理
POST /tasks/{id}/materials 新增材料项 管理
PUT /materials/{id} 修改材料项 管理
DELETE /materials/{id} 删除材料项 管理
GET /tasks/{id}/statistics 任务统计 管理/教师
GET /tasks/{id}/qrcode 填报二维码 管理
GET /rule-templates 规则模板列表 管理
POST /rule-templates/{id}/apply 应用模板 管理
⭐ POST /cleanup/jobs 创建整理任务 管理
⭐ GET /cleanup/jobs 整理任务列表 管理
⭐ GET /cleanup/jobs/{id} 整理进度 管理
⭐ GET /cleanup/jobs/{id}/report 体检报告 管理
⭐ POST /cleanup/jobs/{id}/import 结果导入为正式任务 管理

4.4 名册(12)

路径 说明 权限
POST /tasks/{id}/roster/import 导入(返回体检报告) 管理
GET /tasks/{id}/roster 名册分页 管理/教师
PUT /roster/{rid} 修改名册项 管理
DELETE /roster/{rid} 删除名册项 管理
GET /tasks/{id}/roster/template 下载导入模板 管理
POST /roster/{rid}/notify 定向催报 管理/教师
POST /tasks/{id}/roster/notify-unsubmitted 一键催报未提交者 管理/教师
⭐ POST /mp/roster/join-request 提交加入申请 学生
⭐ GET /mp/roster/join-request 我的申请状态 学生
⭐ GET /roster/join-requests 申请待办列表 管理
⭐ POST /roster/join-requests/{id}/approve 审批通过 管理
⭐ POST /roster/join-requests/{id}/reject 审批驳回 管理

4.5 填报端 · 小程序(16)

路径 说明 权限
GET /mp/tasks 我的任务列表 学生
GET /mp/tasks/{id}/form 表单结构 + 已填数据 学生
POST /mp/submissions 创建/获取草稿 学生
PUT /mp/submissions/{id} 保存草稿 学生
POST /mp/submissions/{id}/submit 提交 学生
POST /mp/submissions/{id}/withdraw 撤回(未审核前) 学生
GET /mp/submissions/{id}/progress 进度与问题清单 学生
POST /mp/files/upload-token OSS 直传凭证 学生
⭐ POST /mp/files/{fileId}/process 文件规范化(命名+压缩+识别) 学生
POST /mp/files/batch-process 批量处理(多材料) 学生
GET /mp/files/{fileId} 预览签名 URL 学生/教师
DELETE /mp/files/{fileId} 删除材料(软删) 学生
POST /mp/ocr/idcard 身份证 OCR 学生
⭐ GET /mp/messages 消息中心 学生
⭐ POST /mp/messages/read 标记已读 学生
⭐ POST /mp/subscribe 订阅消息额度登记 学生

注:本表 16 行,其中 /mp/files/{fileId}/process 与 /mp/files/batch-process 在 PRD §8.6 中分别登记于 §8.6 与 §8.9,此处合并列出。

4.6 班主任移动端(10)

路径 说明 权限
GET /mp/teacher/dashboard 班级看板 教师
GET /mp/teacher/audit/todos 待审列表(卡片流) 教师
GET /mp/teacher/audit/{submissionId} 学生详情 教师
POST /mp/teacher/audit/{submissionId}/approve 通过 教师
POST /mp/teacher/audit/{submissionId}/reject 驳回 教师
POST /mp/teacher/audit/batch-approve 一键通过全部绿色项 教师
POST /mp/teacher/proxy-upload 代传材料 教师
POST /mp/teacher/notify/unsubmitted 一键催报 教师
GET /mp/teacher/notify/records 催报历史 教师
⭐ GET /mp/teacher/messages 教师消息中心 教师

4.7 审核(9)

路径 说明 权限
GET /audit/todos 待办列表(红黄绿) 教师/管理
GET /audit/submissions/{id} 填报单详情 教师/管理
POST /audit/submissions/{id}/approve 通过 教师/管理
POST /audit/submissions/{id}/reject 驳回 教师/管理
⭐ POST /audit/submissions/{id}/revoke 撤回已审 教师/管理
GET /audit/submissions/{id}/timeline 审核时间轴 教师/管理
GET /audit/statistics 审核统计 教师/管理
GET /audit/reason-codes 驳回原因字典 教师/管理
POST /audit/submissions/{id}/proxy-upload 代传材料 教师/管理

4.8 校验 / 导出 / 通知(12)

路径 说明 权限
POST /validation/run 触发任务级全量校验 管理
GET /validation/issues 问题清单 教师/管理
GET /validation/issues/export 导出异常清单 Excel 管理
POST /exports 创建导出任务 管理
GET /exports 导出历史 管理
GET /exports/{id} 导出进度 管理
GET /exports/{id}/download 下载签名 URL 管理
GET /notify/templates 通知模板 管理
PUT /notify/templates/{id} 修改模板 管理
POST /notify/send 手动发送通知 管理/教师
GET /notify/records 通知记录 管理
POST /callback/wechat/msg 微信订阅消息回调 公开

4.9 ⭐ 本次新增接口索引(21)

本表是索引,不是额外接口——其中 5 个已在上方 §4.3 / §4.7 列出,不计入总数。 表内 18 行覆盖 21 个接口(含 GET/PUT、GET/POST 合并行)。

路径 说明 详情节
⭐ /tasks/{id}/plan-scope 报考范围读取 / 确认(GET / PUT) §7.7 / §7.8
⭐ /tasks/{id}/pause /resume 暂停 / 恢复任务 §7.4
⭐ /audit/submissions/{id}/revoke 撤回已审 §11.4
⭐ /mp/files/{fileId}/process 文件规范化总入口(原仅在 §8.9 详述,未登记进清单) §9.6
⭐ /cleanup/jobs 创建整理任务 §13.2
⭐ /cleanup/jobs 整理任务列表 §13.2
⭐ /cleanup/jobs/{id} 整理进度 §13.2
⭐ /cleanup/jobs/{id}/report 体检报告 §13.2
⭐ /cleanup/jobs/{id}/import 结果导入为正式任务 §13.2
⭐ /dicts 字典列表 §13.3
⭐ /dicts/{code}/items 字典项(GET / POST) §13.3
⭐ /dicts/items/{id} 修改字典项 §13.3
⭐ /logs/operations 操作日志 §13.4
⭐ /logs/logins 登录日志 §13.4
⭐ /mp/messages 学生消息中心 §13.5
⭐ /mp/messages/read 标记已读 §13.5
⭐ /mp/teacher/messages 教师消息中心 §13.5
⭐ /mp/subscribe 订阅消息额度登记 §13.6

新增 21 个的分布

归属 数量 明细
报考范围 2 plan-scope GET / PUT
任务状态 2 pause / resume
审核 1 revoke
文件处理 1 process(登记遗漏)
历史整理器 5 cleanup 全套
字典 4 dicts
日志 2 operations / logins
消息中心 3 学生 2 + 教师 1
订阅额度 1 subscribe

4.10 开放接口(4)

路径 说明
POST /open/v1/auth/token appKey/secret 换 token
GET /open/v1/tasks/{id}/submissions 拉取已终审通过的数据
GET /open/v1/tasks/{id}/files 批量获取文件签名 URL
POST /open/v1/callback/push 推送审核结果到机构系统

4.11 ⭐ 客户确认后新增(5)

来源:确认函 B1(客户确认采用"申请加入名单 → 管理员审批"通道)。这是本次唯一的功能增量。 详述见 §8.7。

路径 说明 权限
⭐ /mp/roster/join-request 提交加入申请 / 查看我的申请(GET / POST) 学生
⭐ /roster/join-requests 申请待办列表 管理
⭐ /roster/join-requests/{id}/approve 审批通过(自动建名册项 + 自动绑定) 管理
⭐ /roster/join-requests/{id}/reject 审批驳回(原因必填) 管理

设计边界(三条)

# 边界 理由
1 只解决"进名单",不绕过门禁 申请期间仍不能填报;审批通过后学生仍需重新核验(或凭已绑定 openid 直接放行)
2 证件号仅存 HMAC,不落明文 与门禁口径一致;申请记录保留 2 年
3 驳回必须逐条填原因;批量通过不提供"全选" 身份认定环节误操作代价极高,与"批量通过只带绿色项"同源

与现有机制的关系

机制 区别
FR-508 名册追加导入 老师批量加人;申请通道是学生自助 + 逐条审批
FR-504 统一失败提示 申请通道不破坏统一提示——40039(已在名单中)不泄露新信息
FR-504d 已被他人绑定 申请通过后若该证件号已被占用 → 40041,不覆盖

§5 认证与账号

5.1 POST /api/v1/auth/login

权限:公开 限流:5 次/分钟/IP

字段 类型 必填 说明
username string ✅ 账号
password string ✅ 密码(前端 RSA 加密后传输)
captchaKey string 条件 图形验证码 key(连续失败 2 次后必填)
captchaCode string 条件 验证码

响应

{
  "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. 限流检查 → 超限返回 40007b
2. 规范化:姓名去空格、全角转半角;证件号转大写
3. 按 (task_id, id_card_hash) 定位名册项
   ├─ 未命中 → 记录失败尝试(不记明文) → 40007 统一提示
   └─ 命中 → 比对姓名
        ├─ 完全一致 → 通过
        ├─ 形近字/同音字 → 返回候选让用户确认(FR-504c)
        └─ 不一致 → 40007 统一提示
4. 检查绑定状态
   ├─ 已被他人绑定 → 40007c
   └─ 未绑定 → 写入 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 获取直传凭证

幂等:是

请求

字段 类型 必填 说明
taskId int64 ✅ —
materialCode string ✅ 目标槽位
fileName string ✅ 原始文件名(可能是乱的,正常)
fileSize int64 ✅ 字节
contentType string ✅ MIME
source int — 1 聊天记录 / 2 相册 / 3 手机文件
fileHash string — 客户端 SHA-256(用于秒传与去重)

响应

{
  "code": 0,
  "data": {
    "fileId": 987654321,
    "uploadUrl": "https://oss.xxx.com/raw/1001/2026090001/202609/9f3a....jpg?Signature=...",
    "objectKey": "raw/1001/2026090001/202609/9f3a....jpg",
    "expireSeconds": 900,
    "headers": { "Content-Type": "image/jpeg" },
    "existed": false,
    "clientCompressHint": {
      "enabled": true, "maxLongSide": 1600, "quality": 80, "targetSizeKb": 1024
    }
  }
}
字段 说明
existed true = 秒传命中(同 hash 已存在),前端跳过上传直接进入处理
clientCompressHint 端侧预压缩建议。这是第一道防线,能大幅降低上传流量与处理时长;服务端仍会按规则重压,端侧压缩只是加速

端侧压缩与「一次编码到位」的关系:实测 300dpi/q85 即可达标,端侧压缩只是减少上传量的优化,不是质量决定环节。服务端始终以原图(或端侧压缩后的图)为输入重新规范,保证输出一致。

9.6 ⭐ POST /api/v1/mp/files/{fileId}/process 文件规范化(核心)

说明:这是命名 + 压缩 + 识别 + 校验的总入口。

请求

字段 类型 默认 说明
async bool false 大文件建议异步
runOcr bool true 是否跑 OCR
dryRun bool false 只试跑不落库
confirmMove bool false 识别到槽位不符时是否执行移动

响应(同步模式)

{
  "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,用于表单预填与校验。这是唯一返回明文姓名的 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
已被导出打包 ❌ 40033——已交付的数据不可追溯修改
已归档 ❌ 40034
撤回者非原审核人且非管理员 ❌ 40301

留痕:写入一条 action = 3(撤回)的 collect_audit_record,时间轴显示"李老师撤回了通过结论"。

11.5 GET /api/v1/audit/submissions/{id}/timeline

响应:AuditRecordVO[],倒序。

11.6 GET /api/v1/audit/statistics

Query:taskId、orgId、from / to

响应:{ pendingCount, approvedCount, rejectedCount, avgAuditHours, timeoutCount, byAuditor: [{ auditorName, handled, avgHours }], byReason: [{ reasonCode, reasonText, count }] }

byReason 是给管理员看"哪种问题最多"的,用于反过来优化材料引导语。

11.7 GET /api/v1/audit/reason-codes · POST /api/v1/audit/submissions/{id}/proxy-upload

接口 说明
reason-codes 返回 PRD 附录 C 的 10 条,仅返回启用的;可按 taskId 定制
proxy-upload 同 10.6,PC 端入口

§12 校验 / 导出 / 通知

12.1 POST /api/v1/validation/run

说明:任务级全量校验,产出问题清单。

字段 类型 默认 说明
taskId int64 ✅ —
scope string ALL ALL / ISSUES_ONLY / APPOINTED
submissionIds int64[] — scope=APPOINTED 时必填
rules string[] 全部 只跑指定规则(如 ["VAL-02","VAL-04"])
async bool true —

响应:{ "jobId": "v_3f2a", "estimatedSeconds": 30 }

12.2 GET /api/v1/validation/issues

Query:taskId、severity、ruleCode、orgId、resolved、分页

响应

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

响应

{
  "code": 0,
  "data": {
    "list": [
      { "id": 70001, "operatorId": 10086, "operatorName": "王老师", "module": "ROSTER",
        "action": "UPDATE_IDCARD", "targetType": "ROSTER", "targetId": 9012,
        "beforeVal": "3610292009******10", "afterVal": "3610292009******99",
        "ip": "10.1.2.3", "userAgent": "Chrome/120", "createTime": "2026-09-28T16:20:00+08:00" }
    ],
    "total": 1
  }
}

必记的敏感操作(PRD FR-506 / FR-1202)

action 场景
UPDATE_IDCARD 改名册证件号(留痕强制)
UPDATE_ROSTER_NAME 改名
DELETE_ROSTER 删名册项
PUBLISH_TASK / PAUSE_TASK / ARCHIVE_TASK 任务状态流转
REVOKE_AUDIT 撤回审核
EXPORT_CREATE 创建导出
PROXY_UPLOAD 代传材料
VIEW_IDCARD_FULL 查看完整身份证号(单独留痕)
CONFIG_COMPRESS 改压缩配置

脱敏:beforeVal / afterVal 中的证件号一律脱敏存储,即使日志泄露也不暴露明文。

GET /api/v1/logs/logins

Query:userId、userType、result、from / to、分页

响应:{ list: [{ id, userId, userName, userType, loginType, ip, location, device, result, failReason, loginTime }], total }

loginType 含义
PASSWORD PC 账号密码
WECHAT 小程序微信授权
PHONE 手机号快捷登录

保留期:操作日志 3 年,登录日志 1 年(可在租户配置中调整)。

13.5 消息中心(3 个)

GET /api/v1/mp/messages(学生端)

Query:bizType、read、分页

响应

{
  "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 门禁未匹配 统一提示"没有找到你的信息",不透露哪项错
40007b 门禁尝试过于频繁 提示"操作过于频繁,请稍后再试"
40007c 名单项已被他人绑定 提示"该信息已被登记"
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 申请已通过,不可再驳回 如需撤销请走名册删除
⭐ 40044 一期不支持该收集模式 / 认定类型 请使用学校收集 + 自主认定(见 PRD「一期范围声明」)
⭐ 40043 文件归属存疑(文件名指向名册中的另一个人,或归属无法确证) 阻断提交,提示"这份材料可能不是你的",引导重新上传(无"仍然继续"选项)
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 变更记录

版本 日期 变更
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 行