Files
xiaotutongxue/docs/数据信息填报系统PRD.md
T
lovife 7523ebe789 feat(task): M1-02 覆盖班级与班主任配置
补上发布前置校验的最后一环(40019),同时确立任务的数据隔离边界 ——
班主任能看到哪些任务、能审哪些单子,全由 collect_task_org 决定。

后端 GET/PUT /tasks/{id}/orgs(整体替换语义 + 软移除 + 历史行复用);
前端「配置班级」抽屉:组织树只允许勾选班级,主班主任留空时显式提示
「将由该班负责人初审」而非留白。发布被拦时给出「去哪里补」的指引
(BLOCKING_HINTS),而不是只说"未设置覆盖班级"。

三条守住的规则:
1) 班级与初审人都必须在本租户内。跨租户的 id 一律当作"不存在或已删除",
   不透露"它存在但不属于你" —— 漏了的后果是 A 校能把 B 校的班挂到自己
   的任务上,B 校的班主任就出现在 A 校的审批流里。
2) 初审人必须持有班主任/副班主任角色。指定初审人本质上是一次授权,
   那个人将能看到该班学生的身份证号与学籍材料;不校验角色就等于把
   "谁能看学生隐私"变成任意授权。报错要给出下一步,不能只说"校验失败"。
3) 移除是软移除(status=0),且「移除再加回」必须复用原行。
   后者不是优化而是必须:uk_task_org UNIQUE (task_id, org_id) 不带 status
   条件,一个班在同一任务下只能有一行,直接 insert 会违反唯一约束。

本轮修掉的 3 个真问题:
1) 我此前误判 uk_task_org 不存在 —— grep 只匹配含表名的行,而
   CONSTRAINT uk_task_org UNIQUE (task_id, org_id) 那行没有表名。
   基于这个错误前提,我写了一个多余的部分唯一索引 uk_task_org_active
   和一段基于错误前提的代码,已全部撤掉并更正注释。
   教训:查约束要看 CREATE TABLE 整块,不能只 grep 表名。
2) collect_task_org.expected_count 是 NOT NULL DEFAULT 0,与接口契约矛盾:
   DDL 注释说它是"由名册汇总"(系统算的),但《03-接口设计》§7.6 把它放在
   PUT 请求体里、说"用于与名册实际人数比对" —— 只有"管理员填的期望值 vs
   名册实际值"才需要比对。而 0 是有意义的业务值("预期 0 人"),拿它兼职
   表示"没填"会让每个没填的班都触发"应填 0 人、实际 31 人"的假告警;告警
   变噪音就没人看了,真正"应填 28、名册却有 31"(名册导错)反而被漏掉,
   而这条告警的全部价值就在那一次。已由 V4 迁移改为可空 + 去掉默认值,
   并同步更正 docs/db/schema-postgresql.sql 与 PRD 里的内联 DDL。
3) 删掉迁移文件后必须 mvn clean,否则它会"复活":我只跑 mvn package,
   target/classes 里的旧副本被打进 jar,Flyway 发现历史里没有 v4 就又执行
   了一遍 —— 索引回来了、历史行填回去了。之后又建了同名版本号的新 V4,
   于是"两个 v4"→"checksum 冲突",排查绕了好几圈。

顺带修掉:
· 任务列表操作列从 3 个链接涨到 4 个,160px 会挤成竖排 —— 把「进度」并进
  「状态」列腾出 40px,操作列给到 200px,合计 820px(可用约 824px)。
· defineEmits 必须用函数调用签名(frontend-pc/.agents/skills/code-vue)。
  我此前写的 5 个组件都是被禁的 tuple 形式,一并改正:
  OrgFormModal / TaskFormModal / TaskOrgDrawer / UserFormModal / UserImportModal。
· e2e-m1-orgs.py 的清理抽成幂等的 cleanup_all(),并在 __main__ 的 finally
  里兜一次 —— 该脚本首次运行时崩在准备阶段、清理段没执行,把两个测试账号
  留在了库里(是在浏览器里看到下拉框里有个「E2E班主任」才发现的)。
  其余三条 E2E 脚本仍是老样子,下一轮一并改。

验收:e2e-m1-orgs.py 48/48(含跨租户隔离);e2e-m1-task.py 71/71、
e2e-m0-user.py 55/55、e2e-m0.py 36/36 回归通过,共 210 项断言;
前端 typecheck / eslint / lint:style 0 错、build 通过;
浏览器实测抽屉的开合、树的勾选规则与保存落库。
2026-09-29 09:32:48 +08:00

189 KiB
Raw Blame History

数据信息填报系统 PRD

产品代号:填报通 / CollectFlow(暂定) 版本:V2.3(2026-09-28 明确滑动交互规则) 日期:2026-09-28 文档类型:产品需求文档(PRD)+ 数据库设计 + 接口清单 适用场景:职业技能等级认定报名材料收集(可扩展至各类"班级/单位材料收集"场景) 📄 配套文档:《数据信息填报系统PRD-功能逻辑卷》—— 只写"系统怎么运转",不含库表/接口/代码

变更记录

版本 日期 变更内容 变更人
V2.4 2026-09-28 修正 3 处加密列宽不足(DDL 缺陷,M0 实测暴露):sys_tenant.contact_phone、sys_user.phone、collect_roster.phone 原为 varchar(20),是按明文手机号长度建的;但 §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.0 2026-09-24 初稿:背景/流程/功能/规则引擎/库表/接口 产品
V1.1 2026-09-24 复审修订(用 112 个真实文件重新实测验证):
① 勘误:命名违规率 98.2% → 52.7%(59/112);双扩展名 55 → 56;原始总量 436MB → 475MB(436MB 实为 .rar 包体积);需处理并集 62 → 59(原为重复计数)
② 推翻参数假设:工作证明是 1272dpi A4 扫描件(非手机拍照)→ §6.3 压缩策略由"按长边"重写为"按材料形态分五档"
③ 学籍表是 16:9 截图 → 新增 P4 截图档,禁止缩放与转 JPG
④ 新增 §6.1 证件号裁决矩阵,人工量由 5 例降至 3 例
⑤ 新增风险 R11(1.8 亿像素导致 OOM)、R12(微信订阅消息次数限制)
⑥ 补全 §9.4 班主任移动端页面清单(初稿遗漏)
⑦ 修正 §1.4 指标口径混淆(班主任操作时长 vs 端到端周期)
⑧ 补充小程序隐私保护指引、DPA、KMS 等合规项
⑨ 补 collect_task_org 表、名册 student_no 字段、附录 D 错误码、附录 E 定时任务
产品
V1.2 2026-09-24 压缩决策 D1 落定:① §6.3 P3 扫描件档由"建议放宽至 2M"改为 ≤1M 硬约束、无例外;② 压缩机制改为"体积优先、清晰度可牺牲,但必须标记 + 人工抽检";③ 风险 R1 由"要不要压"改为"压完机构认不认";④ 待确认项 Q1 / Q2-d1 关闭;⑤ 拆出《功能逻辑卷》,本文不再承载功能运转细节 产品
V1.3 2026-09-24 实测推翻了 D1 的估算,压缩问题彻底解决:用 28 张真实 A4 扫描件完整实测,300dpi/q85 下 28/28 全部 ≤1M(平均 806KB,最大 831KB)。① §1.2 量化结论重写;② §6.3 压缩由"逐级降质逼近"改为"一次编码到位";③ 风险 R1 由高降为低;④ 待验证项闭环(A/B 测试取消,M3 工期 −0.5 周);⑤ 新增 V1.2 中 D1"强制 1M"的落地参数 产品
V1.4 2026-09-24 修正材料来源通道:取消"直接拍照"(复核实测数据:学籍表是屏幕截图、工作证明是扫描仪扫描件、一寸照片是照相馆电子版、身份证是翻拍/扫描件,无一来自现场拍照)。① 新增 FR-601a 三通道(微信聊天记录 / 手机相册 / 聊天文件),拍照默认关闭;② 新增 FR-601b 保留原始文件名(聊天记录通道可拿到原名,是命名规范化的首要输入);③ §9.1/§9.2 交互重写为来源选择弹层 + 每个材料的"去哪里找"引导;④ §10.3 隐私声明补相册权限、§10.4 补小程序能力依赖;⑤ 驳回原因与提示文案由"重拍"改为"重新扫描/索取原图" 产品
V1.5 2026-09-24 新增「名单门禁」(新需求):学校收集与社会收集都必须先导入名单,填报人进入前必须填写「姓名 + 身份证号」与名单匹配,匹配不到只提示、不放行。① §4.1/§4.2 流程图与差异表重写,废除"社会收集可自由填报";② 新增 FR-503 ~ FR-508(门禁核验、统一失败提示、限流防枚举、名单不下发、姓名形近字纠错、已绑定保护、发布前校验、追加导入);③ §9.1 新增「门禁核验」「门禁失败」两页,§9.2 新增门禁交互;④ §10.3 新增门禁防枚举合规项;⑤ §2 术语表补"门禁";⑥ 附录 D 补门禁错误码;⑦ §14 新增 Q11–Q14;⑧ 修正 §5.14 残留的错误数字(110 → 59) 产品
V1.6 2026-09-24 补齐两处设计(回应评审提问):
① 材料识别(原为空缺):新增 FR-614 系列,方案 B「规则为主 + VLM 兜底」——四路信号(槽位/文件名/形态/内容)、置信度分级、VLM 仅三种触发条件、只提示不自动改位、识别失败一律放行;VAL-12 由"一句要求"展开为可实现规则;collect_file 表补 6 个识别字段(含 vlm_called 用于成本核算)。完整逻辑见功能逻辑卷 §3.5-B。
② 任务双层结构显性化:新增 FR-615 并在 §5.3 点明「填什么(表单字段)+ 传什么(材料清单)」两层独立配置、可互相条件联动、可存模板复用 —— 此前分散在「表单引擎」「材料采集」「表单定义表」「材料项定义表」四处,易被开发理解偏
产品
V1.7 2026-09-24 材料识别规则实测校正(用 112 个真实文件做形态分析 + 32 个文件跑真实 OCR):
① 证伪——初版形态规则写"身份证宽高比 ≈1.585",实测为 0.732–1.016,与一寸照片 0.714 重叠,形态层无法区分二者;② 架构改为四层递进(文件名 → 形态只分流派 → OCR 强词计分 → VLM);③ 证伪"OCR 命中即判定"——实测关键词大量交叉命中,改为只用强词 + 加权计分;④ 新增 FR-614e 一寸照片零文字判据(实测 8/8 无文字);⑤ VLM 触发率由 15% 重估为 2%~10%;⑥ FR-614b 明确通用词禁止入表
产品
V1.8 2026-09-28 按官方《考生线下报名信息导入模板》完整拆解校准(含 2 个工作表、21 字段、颜色语义、下拉全集、9 条填表规则):
① 新增「评价计划」核心概念(§5.3.1)——官方规则 04/05/06 明确"报考职业/工种/等级只能填当前评价计划内的",此前 PRD 无此实体,导致这三项红色必填字段根本无法校验;新增 FR-307311 与 3 张库表(sys_eval_plan / sys_occupation / sys_plan_occupation);
② 认定类型由 2 种扩为 3 种,新增专项职业能力(无工种、无等级、无补考);
③ 新增 §5.4.1 预置表单模板:21 字段全集 + 红/绿/黑三色语义 + 下拉选项全集;
④ 新增 **VAL-13
17** 校验规则(计划范围、工种级联、补考科目数量约束、在校生三项必填、工作年限数字);
⑤ 记录三个实现陷阱:"无"是合法业务值、专项要绕过三项校验、表头行与工作表名都不固定(实测 1 行 vs 6 行);
⑥ 向导第①步改为必选评价计划;术语表补 3 个概念
产品
V1.9 2026-09-28 确认计划来源为"认定机构下发文件",FR-311 提为 P0 并展开:① 拆出 FR-311a~e(字段映射向导、导入体检、差异对比、导入留痕、Word/PDF 提取);② 新增 sys_plan_import_log 表,sys_eval_plan 补 source_file_url / import_batch_no 等留痕字段;③ 实测发现官方 Excel 模板的「报考职业」「报考工种」两列根本没有下拉(21 列中 13 列自由填写) → 产品核心增量就是补上这个约束,已写入 §5.3.1 对比表;④ §14 新增 Q15/Q16(需索取真实计划清单样本);⑤ M2 里程碑 +1 周(新增计划导入模块),总工期 12.5–14.5 → 13.5–15.5 周 产品
V2.0 2026-09-28 实测校正:不存在"计划清单"文件,报考范围来自名单(用户澄清 + 真实名单验证)
① 实测发现名单里「报考职业/工种/等级」三列老师已全部填好(28/28,均为 车工/数控车工/四级中级工)→ V1.9 的整套"文件导入"设计作废;
② §5.3.1 重写为「报考范围与名单预填」:范围由名单聚合而来 + 一致性体检 + 追加名单告警,管理员可手工修正;
③ 删除 3 张库表(sys_eval_plan / sys_plan_occupation / sys_plan_import_log),改为 collect_task.plan_scope_json;sys_occupation_ref 降级为 P2 拼写校验;
④ 更重要的发现:名单不是"名单",是预填好的报名信息表——实测 21 个字段中 18 个已由老师填满(含学历/在校生/学校/专业/入学时间/联系电话)→ 新增 FR-502a/502b「字段三类规则」(🔒锁定 / ✏️预填可改 / 📄材料上传),学生端几乎不用打字,只需核对+上传;
⑤ §9.2 学生端交互重写;
⑥ 关闭 Q15/Q16/Q17,新增 Q18(预填字段能否由学生修改);
⑦ 工期回退:M2 3 周→2 周,总工期回到 12.5–14.5 周
产品
V2.1 2026-09-28 数据库改用 PostgreSQL / Vastbase + 数据模型纠正:
① 数据库定为 PG 系(生产可跑 Vastbase,与现有 schema-vastbase.sql 技术栈一致,私有化可满足信创);新增 §7.3 DDL 约定表(IDENTITY / 小写标识符 / deleted smallint / jsonb / COMMENT ON 独立语句 / PG 无 ON UPDATE 等 11 项);
② 全部 19 个 SQL 块由 MySQL 语法转为 PG 语法(17 张表 + 2 个 ALTER),并按用户确认改软删除字段 del_flag → deleted;
③ ⚠️ 数据模型纠正:撤销"19 个字段塞 prefilled_json"的设计——官方模板结构稳定(两份真实文件列完全一致)、机构明写"请勿调整模板",属强结构数据,改为 18 个正式列 + extras_json 逃生舱;collect_roster 与 collect_submission 对称建列(后者另加 changed_fields 用于 diff 留痕),并补 idx_roster_scope 支持按工种聚合统计;
④ §5.4 明确"动态表单"边界:内置字段(固定列,可统计)+ 扩展字段(jsonb,仅展示导出),而非任意字段;
⑤ FR-502a 同步更新
产品
V2.2 2026-09-28 DDL 在真实 PostgreSQL 16.15 上执行验证通过(零错误):
① 用 Docker 起 PG 16,将文档全部 DDL 抽出后真实执行,并用真实名单数据跑通聚合查询;
② ⚠️ 实测发现并修掉 9 处命名冲突——PG 的索引名与约束名必须在 schema 内全局唯一(MySQL 只需表内唯一,这是迁移最容易踩的坑):uk_tenant_code(2 处)、idx_name(2 处)、idx_tenant(3 处)、idx_submission(3 处),全部改为表缩写前缀形式;
③ §7.3 DDL 约定新增 ⭐ 命名唯一性行 + 实测踩坑记录表(含报错原文与修法);
④ 数据库定位由"PostgreSQL / Vastbase"明确为纯 PostgreSQL(不依赖 Vastbase 专有语法),并标注"已在 PG 16.15 实测通过";
⑤ 验证结果:19 张表 / 167 条列注释 / 19 条表注释 / 61 个索引;collect_roster 18 个业务列齐全;报考范围聚合查询走 Index Only Scan(索引设计生效);一致性体检能准确识别"少数派"报考项;jsonb 逃生舱读写正常
产品
V2.3 2026-09-28 明确班主任端滑动交互规则(原型审查后决策):§9.4 原写"支持滑动即通过(左滑驳回 / 右滑通过)",改为 只支持「右滑通过」、不支持左滑驳回,且必须二次确认。
理由:通过是正向动作且可撤回,适合低阻力滑动;驳回是负向动作、需选原因、代价高(学生重传 + 班主任重审),误触后果不对称,必须点按钮完成
产品

目录

  1. 项目背景与目标
  2. 术语表
  3. 用户角色与权限矩阵
  4. 业务流程
  5. 功能需求
  6. 核心规则详述(规则引擎)
  7. 数据模型与数据库设计
  8. 接口清单
  9. 移动端页面与交互
  10. 非功能需求
  11. 实施计划与里程碑
  12. 验收标准
  13. 风险与对策
  14. 待确认事项


一期范围声明(2026-09-28 客户确认)

一期只做「学校收集 + 自主认定」这一条流程。 社会收集、社会评价、专项职业能力 全部后置; 但 数据结构与枚举保留不启用——二期直接开,不需要 DB 迁移。

范围切分

维度 字段 一期 二期后置 为什么这么切
收集模式 collect_mode 1 学校收集 2 社会收集 一期只做一条审核流(学生→班主任→管理员),少一条分支、少一套测试
认定类型 cert_type 1 自主认定 2 社会评价
3 专项职业能力
后两者会引入两类零实测样本的材料,识别规则目前是推测

一期支持的材料(4 类,全部有 112 个真实文件实测)

材料编码 名称 规范文件名示例 压缩档位 实测
PHOTO1 一寸照片 张三-3601...-一寸照片.jpg P1 证件照 ✅
IDCARD 身份证 张三-3601...-身份证.jpg P2 证件翻拍 ✅
SCHOOL_ROLL 学籍表 张三-3601...-学籍表.jpg P4 截图 ✅
WORK_PROOF 工作证明 张三-3601...-工作证明.jpg P3 扫描件 ✅

二期才做的材料(零实测样本)

材料编码 名称 出现在 现状
XJW_BA 学信网备案表 社会评价 强特征词是从官方模板的文字要求里抄的,未见一张真实文件
OTHER_PROOF 其它佐证材料 社会评价 / 专项 含毕业证/社保单/职称/技能证书四类版式完全不同的东西,可能不该做自动识别

后置清单(二期启动前要准备的事)

# 事项 依赖
1 索取「社会评价」批次原始材料样本(≥10 份,须含备案表与各类佐证) 客户提供(D4)
2 标定 XJW_BA 的强特征词与形态阈值 ↑
3 决定 OTHER_PROOF 是否做自动识别,还是改为纯手动槽位 ↑
4 实现社会收集审核流(跳过班主任初审,直达管理员) 无
5 实现专项职业能力的字段隐藏(工种/等级/补考三项)与校验绕过 无
6 社会收集的名单来源流程(报名点/机构维护预报名名单) 客户确认(B2)

对现有设计的影响:几乎没有

因为这三项本来就是配置驱动的:

后置项 现有实现方式 代码改动量
社会收集跳过初审 audit_config.needClassAudit 是一个开关 0(只是一个配置值)
专项职业能力的字段隐藏 走 visibleWhen 条件显示规则 0(配置一条规则)
材料清单差异 材料项定义(collect_material_def) 0(配置差异)

⇒ 一期收窄省下的主要是「测试与联调成本」和「一个外部依赖(D4)」,而不是代码量。

工期影响:−5 人日(392 → 387)。关键路径不变,工期仍为 15 周。

一期不做的验证项(从测试范围移除)

移除项 原因
主流程 B(社会收集,3 步) 二期
XJW_BA / OTHER_PROOF 的识别断言 零样本,无法定基线
专项职业能力的字段隐藏与 VAL-14/15 绕过用例 二期
社会收集的跳过初审用例 二期

⚠️ 二期启用时必须要补的回归断言:XJW_BA / OTHER_PROOF 的识别准确率、社会收集审核流、专项职业能力字段隐藏。 这些都已在本文档与《06-验收测试清单》中保留描述,只是标注为二期。


1. 项目背景与目标

1.1 业务背景

职业技能等级认定机构(及合作院校)在每批次报名时,需要按认定机构下发的《申报材料上传要求》收集考生材料:线下报名信息表、一寸照片、身份证正反扫描件、学籍表 / 学信网备案表、工作证明 / 申报表、其它佐证材料等。

当前流程完全依赖"微信群发通知 → 学生各自命名 → 班主任手工核对 → 打包上传",存在三个结构性问题:

  • 命名靠人:每个学生理解不同,命名五花八门;
  • 校验靠眼:身份证号、姓名一致性只能人工比对,易漏;
  • 体积靠猜:认定机构要求单文件 ≤1M,学生直接传手机原图(十几 MB)。

1.2 真实数据实测(本项目立项的直接依据)

以真实样本批次 24中技数控1班(28 名学生 × 4 类材料 = 112 个文件)为样本,实测结果如下:

① 命名规范违反率

统计口径:文件名须严格等于 姓名-证件号-材料名.后缀,且姓名、证件号须与名册一致。

问题类型 实例 数量
双扩展名 .jpg.jpg 于豪-360103200805186619-工作证明.jpg.jpg 56(工作证明 28、身份证 28)
缺少分隔符 曾伊豪-361029200907295110身份证.jpg.jpg、徐程宇360981200901292334-身份证.jpg.jpg 2
证件号末位小写 x 朱彬宇-36012420080712697x-身份证.jpg.jpg 2
证件号与名册不符 于豪、杨晨、陈子文 3
证件号残缺(仅 17 位) 曹文锋、黎思明 2
姓名与名册不符 扬晨-360825200904303215-一寸照片.jpg(名册为"杨晨") 1
目录 / 模板命名错误 目录名"24中技数车1班"、模板名"24中技数车1班班",规范名为"24中技数控1班" —

命名合规率 53/112 = 47.3%,需改名文件 59 个(52.7%)。 ✅ 勘误:V1.0 初稿此处写"110 个需改名、违规率 98.2%",系把"存在任何问题"与"需要改名"混同所致,现已按严格口径复核修正。 📌 初稿遗漏的重要事实:学籍表 28 个文件全部合规,一寸照片 25 个合规 —— 并非"全员乱命名",而是特定材料(工作证明、身份证)批量出错,这恰恰是规则引擎价值最集中的地方。

② 身份证号一致性错误(致命,直接导致报名失败)

姓名 材料文件名中的证件号 名册(Excel 模板)中的证件号 错误性质 校验位裁决
陈子文 一寸照片 360122200902044232 36010520090817121X 串档,照片是别人的 两个号码均通过 → 确为两个不同的人
于豪 身份证 360103200805166169 360103200805186619 末 6 位倒置 文件失败/名册通过 → 文件名错
杨晨 身份证 360825200904303218 360825200904303215 末位错误 文件失败/名册通过 → 文件名错
曹文锋 一寸照片 36012120081211239(17 位) 360121200812111239 少 1 位 位数不足,无法校验
黎思明 身份证 36100200907283419(17 位) 361002200907283419 少 1 位 位数不足,无法校验

结论:28 人中 5 人(17.9%) 存在证件号错误,其中 1 例为材料串档。

🔑 关键发现:ISO 7064 校验位可以直接"裁决"争议,无需人工判断。

  • 名册 28 人的证件号校验位 全部通过 → 名册可信,可作为权威基准;
  • 于豪、杨晨的文件名证件号 校验位失败 → 确定性判定为文件名错误,且可自动改正(无需询问任何人);
  • 陈子文的两个号码 校验位均通过 → 说明是两个真实存在的人 → 串档实锤,而非打字错误,必须人工介入而非自动修正。

这三种情形的处理策略完全不同(可自动修复 / 可自动修复 / 必须人工),而校验位算法仅凭数字本身就能区分它们。这是本产品最核心的校验资产,必须在 §6.2 完整实现。

③ 文件体积与图像属性(实测,非估算)

材料类型 文件数 超 1M 最大单文件 合计 实测分辨率区间 格式
工作证明 28 28(100%) 17.2 MB 444 MB 9984×14442 ~ 11050×16170 JPEG
身份证 28 3 5.4 MB 约 20 MB 558×745 ~ 2216×2796 JPEG 27、PNG 1
一寸照片 28 0 0.44 MB 约 10 MB 984×1378(27 张完全一致)/ 1553×2175 JPEG
学籍表 28 0 0.04 MB 约 1 MB 约 1025×590 PNG
班级打包 .rar 1 — 436 MB — — RAR
合计(原始) 112 31 — 约 475 MB — —

④ 三项"反直觉"实测发现(直接推翻初稿的参数假设,必须写入设计)

# 发现 实测证据 对设计的影响
D1 工作证明不是"手机拍照",是超高 DPI 的 A4 扫描件 10517×15445 ≈ 1.6 亿像素,换算约 1272 dpi(A4@300dpi 仅 2480×3508) ⚠️ 初稿"C 档:长边压到 1600px"= 把 1272dpi 降到约 195dpi,降质 6.5 倍。扫描件必须按纸张规格(A4)+ 目标 DPI 压缩,而非按长边。
✅ 后续实测(见下方量化结论):按 A4@300dpi/q85 处理,28/28 达标且清晰度无损,问题彻底解决
D2 学籍表是 16:9 屏幕截图,不是扫描件 28 张尺寸高度一致(1023–1030 × 583–607),比例 ≈1.74,PNG 格式 40KB ⚠️ 初稿"扫描件档最小长边 1400px"会把这 28 张全部误判为分辨率不足。截图类材料必须单独设档,且不宜转 JPG(文字边缘会发虚)
D3 体积小 ≠ 合格,存在"过度压缩"风险 杨晨身份证仅 558×745 / 0.08MB(同批其他人 1279×1706) ✅ 验证了 VAL-08(可读性校验)的必要性;且说明必须同时设"下限",不能只设 1M 上限
D4 一寸照片全部 984×1378(5:7 = 标准一寸比例) 27/28 张尺寸完全相同,显然来自同一照相馆批量产出 ✅ 规格化(FR-608)可行且收益明确:984×1378 → 295×413 @300dpi,体积可再降 70% 且更符合规范

D1 的量化结论(已用 28 张真实扫描件实测,推翻了本文原估算)

A4 等比缩放 + JPEG 编码,完整实测结果:

输出参数 平均体积 最大体积 达标情况
300dpi / q90 958 KB 964 KB ✅ 28/28(余量仅 6%)
300dpi / q85 806 KB 831 KB ✅ 28/28 ← 推荐默认参数
300dpi / q80 718 KB 740 KB ✅ 28/28
300dpi / q75 653 KB 673 KB ✅ 28/28
400dpi / q80 1070 KB 1078 KB ❌ 全部超标
500dpi / q80 1452 KB 1468 KB ❌ 全部超标

⇒ ✅ "≤1M"与"清晰度"根本不冲突。 300dpi 是文档扫描的标准清晰度,在该参数下 28/28 全部达标且留有余量,无需牺牲任何清晰度。 ⇒ ✅ 更正:本文曾推算"要压进 1M 只能取 150–170dpi",该估算错误——文档扫描件以白底为主,压缩率远高于照片。实测表明一张都不用降质。 ⇒ ✅ 风险 R1(清晰度不被认可)相应从"高"降为"低":输出是标准 300dpi 扫描件,认定机构没有任何理由拒收。 ⇒ ✅ 整班 444MB → 22MB,压缩率 95%,单张处理耗时约 2 秒(PIL 基准,换用 libvips 预期更快)。

⑤ 工作量口径(修正后)

口径 数量 占比
文件总数 112 100%
命名不合规(需改名) 59 52.7%
体积超标(需压缩) 31 27.7%
需处理文件并集 59 52.7%
↳ 其中可全自动完成 56 50.0%
↳ 其中需人工确认(串档 / 证件号残缺 / 分辨率不足) 3 2.7%
完全无需处理(已合规) 53 47.3%

⚠️ 勘误:本节曾写"需处理并集 62 个",属重复计数——体积超标的 31 个文件全部同时存在命名问题(工作证明与身份证的 28+28 份双扩展名),故并集就是命名的 59 个,而非 59+31−28=62。

即:112 个文件中 59 个必须处理,其中 56 个可全自动完成,仅 3 个需人工确认;其余 53 个已完全合规。 这就是"机器先跑一遍、人只看 3 个"的价值模型,也是本产品的验收基线。

⑥ 人工成本

单班 112 个文件,逐个检查命名、体积、证件号一致性,保守估计 2–4 小时;一所学校一学期通常 20–40 个班 ⇒ 40–160 人时/校/学期。

1.3 产品定位

不是网盘,是"带规则引擎的资料收集与合规加工平台"。

核心价值主张:让规范在收集的那一刻就自动发生,而不是事后返工。

老师建任务  →  学生手机填报(上传即自动改名/压缩/校验)
            →  班主任一键初审  →  管理员汇总终审  →  一键导出标准包上报

1.4 产品目标(可量化)

指标 现状(实测基线) 目标
交付时文件名合规率 47.3% 100%(系统自动生成)
单文件 ≤1M 达标率 工作证明 0%(全批 72.3%) 100%
证件号一致性错误拦截率 0%(无拦截,5 例全部漏过) 100%(校验位可确定性裁决 3 例,另 2 例位数异常可检出)
全量材料的人工复核量 112 个文件全看 ≤3 个(仅串档/低清/姓名存疑项)
系统自动处理覆盖率 0% ≥95%(59/62)
学校管理员汇总导出耗时 30–60 分钟 ≤2 分钟
端到端周期(任务发布→可上报) 数天(多轮返工) ≤24 小时

⚠️ 指标口径澄清:初稿把"班主任单班处理时长 ≤15 分钟"与端到端周期混为一谈。二者必须分开——

  • 班主任操作时长(只看 3 个异常 + 一键批量通过):目标 ≤15 分钟;
  • 学生填报是异步分散行为,不计入班主任时长,只统计"任务发布到收齐"的周期。

1.5 非目标(本期不做,架构预留)

  • 不做在线缴费 / 报名费支付;
  • 不做与认定机构业务系统的实时数据对接(预留开放 API + 导出包);
  • 不做视频材料采集(本期支持图片 / PDF / Office 文档);
  • 不做证书制作与发放。

2. 术语表

术语 定义
租户(Tenant) 一套独立数据空间的客户单位,通常是"一所学校"或"一个认定机构/报名点"
收集任务(Task) 一次完整的材料收集活动,如"2026年9月机电专业认定报名材料收集"
报考范围(Plan Scope) ⚠️ 本期允许报考的职业 × 工种 × 等级集合。由名单聚合而来(老师在名单里已填好),管理员可手工修正。存于 collect_task.plan_scope_json
专项职业能力 ⚠️ 第三种认定类型(另有自主认定、社会评价)。没有工种、没有等级、没有补考
职业 → 工种 → 等级 三级关系。"车工"是职业,"数控车工"是其下的工种,等级为五级/初级工 ~ 一级/高级技师。某职业可能没有工种(此时填"无")
收集模式(Mode) SCHOOL=学校收集(有班主任层);SOCIAL=社会收集(无班主任层,直达管理员)
名册(Roster) 任务下的准入名单,由 Excel 导入生成。「姓名」与「身份证号」为必填列,既是校验基准,也是填报的准入门槛(详见"门禁")
门禁(Gate) 填报人进入填报页前必须通过的身份核验:填写「姓名 + 身份证号」与名册匹配,命中才放行。学校收集与社会收集都启用
表单(Form Schema) 任务内需要填报的字段定义,JSON 存储,可版本化
材料项(Material) 需要上传的文件类型定义,如"一寸照片""身份证""学籍表"
填报单(Submission) 一个填报人针对一个任务产生的数据集合(表单数据 + 材料文件)
规范名(Standard Name) 由命名规则引擎根据模板生成的最终文件名
初审 / 终审 班主任审核 / 管理员审核
汇总包(Export Package) 按目录规范打包后可直接上报的文件集合
历史数据整理器 对已存在的"乱文件夹/压缩包"做批量规范化与体检的独立工具

3. 用户角色与权限矩阵

3.1 角色定义

角色 使用端 核心诉求
平台运营管理员 PC 租户开通、套餐计费、全局监控、内容风控
学校管理员 / 机构管理员 PC(主)+ 手机(次) 建任务、管名册、终审、汇总导出、上报
班主任 / 收集负责人 PC + 手机 催报、初审、批量通过、退回修改
填报人(学生) 微信小程序 3 分钟填完、少填字、从聊天记录/相册选图即传
社会填报人 微信小程序 / H5 自主填报、无班主任环节
上级接收方(只读) PC / 免登录链接 查看或下载汇总包(可选)

3.2 权限矩阵

功能 平台运营 学校管理员 班主任 填报人
租户 / 套餐管理 ✅ — — —
组织架构 / 账号管理 ✅ ✅ — —
创建 / 配置收集任务 — ✅ — —
任务克隆 / 复用 — ✅ — —
名册导入 / 修改 — ✅ 🔸 本班 —
催报 / 发送通知 ✅ ✅ 🔸 本班 —
查看填报进度看板 ✅ ✅ 🔸 本班 🔸 本人
初审(通过 / 驳回) — ✅ ✅ —
终审(通过 / 驳回) — ✅ — —
教师代传材料 — ✅ ✅ —
汇总导出 / 下载 — ✅ — —
数据脱敏导出 — 🔸 需二次确认 — —
全量操作日志 ✅ 🔸 本租户 — —
填报 / 修改 / 撤回 — — — ✅(本人)

4. 业务流程

4.1 学校收集主流程

┌────────┐   ┌────────┐   ┌──────────┐   ┌──────────┐   ┌──────────┐
│ 管理员 │   │ 系统   │   │ 填报人   │   │ 班主任   │   │ 管理员   │
│ 建任务 │──▶│ 下发   │──▶│ 门禁核验 │──▶│ 初审     │──▶│ 终审     │
│ 导名单 │   │ 通知   │   │ 填报上传 │   │ 通过/驳回│   │ 汇总导出 │
└────────┘   └────────┘   └──────────┘   └────┬─────┘   └──────────┘
                                                 │驳回
                                    ┌────────────▼────────────┐
                                    │ 通知填报人 → 修改重传 → │
                                    │ 回到班主任待办          │
                                    └─────────────────────────┘

关键设计点

  1. 【门禁】必须先过名单核验:填报人扫码 → 微信授权 → 填写「姓名 + 身份证号」与名单匹配 → 命中才进入填报页;未命中只提示、不放行(详见 §5.5、功能逻辑卷 §3.1)。

    🔒 学校收集与社会收集都必须先导入名单,不存在"自由填报"。

  2. 匹配成功后回填并锁定姓名、身份证号、班级、专业等信息为只读,避免用户改动后与名单不一致。
  3. 上传文件时,客户端只做方向校正与尺寸限幅,服务端做规范化与压缩。此举可把 475MB 的班级数据降到约 37MB,带宽成本下降 92%。

    ⚠️ 客户端压缩参数必须与 §6.3 的档位一致(不可对所有图片一刀切"长边 1600px"——实测工作证明是 A4 扫描件、学籍表是截图,二者按长边处理都会出错)。

  4. 每个材料项上传后立即返回校验结果(命名是否已规范、体积是否达标、OCR 证件号是否与名单一致),不达标就地提示,不让错误流到班主任。
  5. 班主任端提供批量初审:一屏查看全班 40 人的红黄绿状态,绿色一键批量通过,仅处理红色异常。

4.2 社会收集主流程

管理员建任务(Mode=SOCIAL)→ 导入名单(必填)→ 生成任务二维码/链接
→ 社会人员扫码 → 门禁核验(姓名 + 身份证号)→ 命中才可填报
→ 系统校验 → 管理员终审 → 汇总导出

与学校收集的差异

维度 学校收集 社会收集
名单 必填(含姓名 + 身份证号) 同样必填(含姓名 + 身份证号)
门禁核验 姓名 + 身份证号匹配名单 完全一致
归属 按班级/院系 按报名点 / 区域 / 批次
班主任层 有 无(提交后直达管理员终审)
材料清单 学籍表、工作证明(学校版) 学信网备案表、其它佐证材料(社会版,见 6.0)
校验强度 强(与名单比对) 强(同样与名单比对)
名单来源 学校教务导出 预报名名单 / 报名点收集 / 机构提供(⚠️ 需确保来源稳定)

⚠️ 本版变更:初稿写"社会收集名册可选、允许自由填报",现已废除。两种模式在"名单 + 门禁"上完全一致,唯一差异是审核流是否经过班主任层。

4.3 状态机

填报单状态(Submission)

DRAFT ──submit──▶ PENDING_CLASS ──班主任通过──▶ PENDING_ADMIN ──管理员通过──▶ APPROVED
  ▲                    │                              │                        │
  │                班主任驳回                     管理员驳回                 归档
  │                    ▼                              ▼                        ▼
  └────编辑──── REJECTED ◀───────────────────────────┘                    ARCHIVED
                  │
                  └── resubmit ──▶ PENDING_CLASS / PENDING_ADMIN
  • 学校收集:DRAFT → PENDING_CLASS → PENDING_ADMIN → APPROVED → ARCHIVED
  • 社会收集:DRAFT → PENDING_ADMIN → APPROVED → ARCHIVED(跳过 PENDING_CLASS)
  • 驳回后进入 REJECTED,填报人收到通知,可修改后重新提交(reject_count 累加,≥3 次自动升级通知管理员)
  • 终审通过后默认可"更正提交"1 次(窗口期内),过期需管理员解锁

任务状态(Task)

DRAFT(草稿)→ PUBLISHED(收集中)→ CLOSED(已截止)→ ARCHIVED(已归档) 支持 PAUSED(暂停)。

4.4 异常分支处理

场景 处理策略
逾期未提交 截止前 7/3/1 天自动催报;截止后管理员可"延长截止时间"或"点名催报"
学生不在名单 门禁匹配失败 → 统一提示"没有找到你的信息",不给填报;老师需在后台把此人加入名单后方可填报
一人多班 / 转班 名册以学号+证件号唯一;管理员可改归属
材料被驳回次数过多 ≥3 次自动提醒管理员介入,可开启"班主任代传"
学生不会操作 班主任/管理员可代传(代传记录标注操作人)
重复上传同一材料 保留版本历史,取最新有效版本,旧版本进入回收站(30 天)
上传中断网 支持断点续传与草稿自动保存(每 10 秒本地缓存)

5. 功能需求

5.1 租户与组织管理(FR-100)

编号 需求 优先级
FR-101 租户开通、停用、套餐绑定、到期提醒 P0
FR-102 租户级配置:Logo、名称、默认压缩参数、文件保留策略 P0
FR-103 组织树管理(学校 → 院系/专业 → 班级),支持批量导入 P0
FR-104 租户数据隔离(逻辑隔离 tenant_id,大客户支持独立库) P0
FR-105 微信小程序与租户绑定(统一小程序 + 扫码带参 scene 识别租户) P0

5.2 账号与角色(FR-200)

编号 需求 优先级
FR-201 PC 端账号密码登录(图形验证码 + 登录失败锁定) P0
FR-202 小程序端微信授权登录(code2session)+ 手机号快捷绑定 P0
FR-203 账号批量导入(Excel),自动生成初始密码并短信/微信通知 P0
FR-204 一个班主任可管理多个班级;一个班级可配多个班主任(主/副) P0
FR-205 角色自定义与数据权限(本人 / 本班 / 本院系 / 全租户) P0
FR-206 密码重置、强制改密、离职停用 P1

5.3 收集任务管理(FR-300)

5.3.1 报考范围与名单预填(⚠️ 按实际情况校正)

事实澄清(经真实名单实测)

上一版设计假设"认定机构会下发一份计划清单文件",该假设不成立。实测真实名单 24中技数控1班 后确认:报考职业/工种/等级这三列,是在导入名单时就已经由老师确定好的。

字段 已填 实际取值
报考职业 28/28 车工
报考工种 28/28 数控车工
报考等级 28/28 四级/中级工

⇒ 不存在独立的"计划清单"文件,报考范围就藏在名单里。

设计改为:报考范围从名单中聚合,而不是从外部文件导入

名单导入
   ↓ 自动聚合本期报考范围(职业 × 工种 × 等级的集合)
   ↓ 【一致性体检】
      · 是否存在同一职业下互相矛盾的工种
      · 是否等级不符合该工种的常规设置
      · 是否有人漏填了这三项(红色必填)
      · 是否与国标职业字典拼写不符(如"数控车床工" vs "数控车工")
   ↓ 生成任务级「本期报考范围」→ 管理员可确认/手工修正
   ↓ 供学生端只读展示 + 导出写回

本期报考范围的四个用途

# 用途 说明
1 名单内部一致性检查 40 人里若 38 人报"数控车工"、2 人报"车工",很可能是填错,需提醒
2 追加名单时报警 中途补录的新人若报了本期没有的职业/工种,立即提示"新增了范围外的报考项"
3 学生端只读展示 学生扫码核验后直接看到"你报考:车工 / 数控车工 / 四级中级工",不用自己选
4 导出写回 汇总导出时原样写回 Excel 模板的 G/H/I 列

保留手工修正:管理员可编辑范围(应对临时增补、机构特殊要求等异常情况)。

认定类型扩展为 3 种

类型 工种 等级 补考 面向谁 关键材料差异
1 自主认定 可有 有 有 本单位职工 / 本校学生 要学籍表
2 社会评价 可有 有 有 社会人员(非本校、非本单位) 没有学籍,改要学信网备案表 + 其它佐证(毕业证/社保权益单/职称证明)
3 专项职业能力 无 无 无 专项能力考核 规则 05/06/07 明确排除工种/等级/补考三项

⚠️ 「社会评价」≠「社会收集」,这两个词只差一个字,含义完全不同,务必区分

概念 字段 取值 决定什么
认定类型 cert_type 自主认定 / 社会评价 / 专项职业能力 这次认定是什么性质 → 决定填什么材料
收集模式 collect_mode 学校收集 / 社会收集 这次怎么收 → 决定谁审核(有无班主任初审层)

二者可以任意组合:

  • 学校收集 + 自主认定 = 最常见(院校统一组织本校学生)
  • 学校收集 + 社会评价 = 院校受委托组织社会人员报名(有班主任层,但材料是社会人员那套)
  • 社会收集 + 社会评价 = 报名点零散收集社会人员
  • 任意 + 专项职业能力 = 无工种/等级/补考

对材料识别的影响:社会评价 类型的任务,其材料集合里没有学籍表,而有学信网备案表 + 其它佐证材料。 因此材料识别引擎的强特征词表必须在该类批次的数据上另行标定——不能只用院校批次(学籍表为主)的样本推断。

编号 需求 优先级
FR-307 报考范围自动聚合:名单导入后自动提取"职业 × 工种 × 等级"集合 P0
FR-308 名单一致性体检:职业/工种矛盾、等级异常、漏填、拼写不符 → 输出体检报告 P0
FR-309 报考范围支持管理员手工修正,并留痕(谁改的、改前改后) P0
FR-310 专项职业能力类型支持:自动隐藏工种、等级、补考相关字段 P0 二期
⚠️ 一期只做自主认定(见「一期范围声明」)
FR-311 追加名单时,若出现范围外的新报考项,必须告警并由管理员确认后才接受 P0
FR-312 可选:接入国标职业字典做拼写校验与联想建议(仅辅助,不作强约束) P2

✅ 已作废:上一版的"计划文件导入(FR-311a~e)"、"字段映射向导"、"差异对比"、以及 sys_eval_plan / sys_plan_occupation / sys_plan_import_log 三张表——因不存在该文件,全部删除。


更重要的发现:名单不是"名单",而是一张预填好的报名信息表

实测发现,老师交上来的名单里,21 个字段几乎全部已填:

字段 已填 取值
学历 28/28 技工学校
是否补考 28/28 否
是否在校生 28/28 是
在读学校 28/28 江西省机电技师学院
所学专业 28/28 数控加工(数控车工)
入学时间 28/28 2024-09-01
工作单位 28/28 无 ← 印证"无"是合法值
联系电话 28/28 28 个不同号码
本职业工作年限 / 已获证书×3 0/28 全空

⇒ 学生端几乎不需要打字。老师已经把所有信息填好了,学生上来只需核对 + 上传材料。 这正好命中最初的产品目标:"3 分钟填完、少填字"。

字段分三类处理(核心规则)

类别 字段 学生端行为
🔒 锁定 姓名、身份证号、报考职业、报考工种、报考等级 只读不可改。名单为空则阻断任务发布(这三项必须由老师确定)。学生要改只能找老师
✏️ 预填可改 学历、是否在校生、在读学校、所学专业、入学时间、工作单位、联系电话、是否补考、补考科目、本职业工作年限、已获证书相关 名单有值 → 预填并标注"老师已填,请核对",学生可直接修改(修改留痕);名单为空 → 学生必填
📄 材料 一寸照片、身份证、学籍表、工作证明等 学生上传(唯一的"重活")

💡 为什么"报考职业/工种/等级"必须锁定:这三项是老师根据机构要求和报考策略定的,学生不懂也不该改。让它们出现在学生端可编辑,只会制造错误。 💡 为什么其余字段允许学生改:老师填的电话可能有误、学生可能转专业、入学时间可能记错——允许改正比强制回退给老师更高效,改动留痕即可。

这带来的设计简化

项 上一版(已作废) 本版
报考范围来源 外部计划文件导入 名单聚合
需要的新表 4 张(计划、字典、范围、导入日志) 0 张(存任务级 JSON)
学生端填写量 21 个字段都可能要填 几乎为 0(核对为主)
M2 工期 3 周 2 周(回到原估)

💡 这就是产品的核心价值:官方模板对最易错的「报考职业/工种」束手无策(只能靠说明文档和人的自觉),而我们的系统从计划生成级联下拉,从源头不可能填错。 而这三项恰恰是模板里的红色必填项——填错的后果是整批数据被打回重做。

任务创建方式:向导式 7 步(同时支持"复制已有任务"一键克隆,这是高频需求)

🧩 先理解任务的结构:一个收集任务 = 两层独立配置

层 回答的问题 存哪 配置入口 典型内容
① 填什么 学生要填哪些字 collect_form_schema 向导 ④ 表单 姓名、身份证号、学历、报考工种…
② 传什么 学生要交哪些文件 collect_material_def 向导 ⑤ 材料 一寸照片、身份证、学籍表、工作证明…

两层可互相条件联动(FR-402):例如"是否在校生=否" → 隐藏"学籍表"材料、显示"学信网备案表"。 每个任务的这两层都可以完全不同,且能存成规则模板(FR-306)跨学期复用。

步骤 内容 校验
① 基本信息 任务名称、收集模式(学校/社会)、所属评价计划、批次号、起止时间 批次号租户内唯一;结束时间 > 开始时间;必须选择评价计划
② 覆盖班级 勾选本次收集涉及哪些班级(社会模式可挂报名点/区域) 学校模式至少 1 个班
③ 名单 必填,两种模式都要;Excel 导入或手动添加;字段映射预览 必须含「姓名」「身份证号」列且人数 > 0,否则阻断发布
④ 表单 选择表单模板或自定义字段(见 5.4) 至少 1 个字段
⑤ 材料 配置材料清单、命名规则、体积与格式要求(见 5.6) 至少 1 项材料;命名模板必须含姓名/证件号/材料名
⑥ 审核流 是否启用班主任层、是否允许多人并审、驳回次数上限、超时小时数 学校模式默认启用;社会模式默认关闭
⑦ 预览发布 命名规则试跑(粘贴样本文件名看效果)、生成填报二维码/链接、预览填报端 未试跑确认命名规则不允许发布

⚠️ 步骤 ③(名单)是硬前置:没有名单就没有准入门槛,任务无法发布。详见 §5.5。

编号 需求 优先级
FR-301 任务 CRUD、发布、暂停、截止、归档 P0
FR-302 任务克隆(跨学期复用,保留名册与规则配置) P0
FR-303 任务统计看板(完成率、异常率、驳回率、平均处理时长) P0
FR-304 填报二维码 / 短链生成(带租户与任务标识) P0
FR-305 多任务并行与批次管理 P1
FR-306 任务级"规则模板"保存与复用(形成规则模板市场) P1

5.4 动态表单引擎(FR-400)

🧩 先明确"动态"的边界:本系统的表单是「内置字段 + 扩展字段」两段式,不是任意字段。

层 内容 存储 能力
内置字段 官方模板 21 项(姓名、身份证号、报考职业/工种/等级、学历、在校生相关…) 固定列(PG 强类型) ✅ 可查询 ✅ 可索引 ✅ 可统计 ✅ 可校验
扩展字段 租户/机构额外追加的字段 extras_json(jsonb) ✅ 可展示 ✅ 可导出 ❌ 不参与核心统计

为什么这样分:实测确认官方模板结构稳定(两份真实文件列完全一致),且机构明写"请勿调整模板"——这些是强结构数据。把它们塞进 JSON 会换来灵活性、但失去全部统计能力,得不偿失。 而"能动态设置表格"的需求由扩展字段满足。

字段类型全集

类型 说明 校验能力
单行文本 姓名、单位等 长度、正则、去首尾空格
身份证号 专用控件 格式、校验位、出生日期、性别自动推导、与名册比对、OCR 回填
手机号 专用控件 11 位格式、短信验证、去重
数字 工作年限、年龄 范围、整数/小数
单选 / 多选 学历、是否在校生、补考科目 选项字典
日期 / 日期区间 入学时间、有效期 范围限制
级联选择 专业 → 工种 → 等级 层级校验
下拉字典 报考职业、报考等级 字典维护
文件 见材料项(5.6) —
说明文本 提示语、示例图 只读

关键能力

编号 需求 优先级
FR-401 可视化拖拽配置字段(PC 端) P0
FR-402 条件显示:字段/材料可依赖其他字段值显示(例:是否在校生=是 → 显示"学籍表";=否 → 显示"学信网备案表") P0
FR-403 字段级校验规则配置(必填、正则、范围、自定义脚本) P0
FR-404 Excel 模板一键生成与下载(带示例行、红色必填标识) P0
FR-405 Excel 批量导入(名册即表单数据源),含字段映射向导 P0
FR-406 表单 schema 版本化:已发布任务改表单需生成新版本,避免数据错乱 P0
FR-407 表单模板库("职业技能等级认定报名表"等预置模板) P1
FR-408 移动端表单填写记忆(同一人跨任务复用已填信息) P1

5.4.1 预置表单模板:职业技能等级认定报名表(21 字段)

来源:真实《考生线下报名信息导入模板》完整拆解。颜色即填写要求——这是官方模板自带的语义,必须原样支持。

模板结构

工作表 用途 结构
考生报名信息 数据录入区 表头在第 1 行,A–U 共 21 列
填表注意事项 说明区 + 示例区 A1:L5、A6:K23 合并单元格;第 25 行"示例 ↓";第 27–29 行为表头 + 2 行示例

字段全集与填写要求

列 字段 颜色语义 类型 校验规则
A 序号 🔴 必填 数字 正整数
B 姓名 🔴 必填 文本 2–20 字;与名单一致
C 身份证号 🔴 必填 身份证 18 位 + 校验位 + 与名单一致
D 学历 🔴 必填 下拉(11) 见下方选项表
E 联系电话 🔴 必填 手机号 11 位数字
F 工作单位 🔴 必填 文本 没有则填"无"
G 报考职业 🔴 必填 字典 必须在当前评价计划的职业范围内
H 报考工种 🔴 必填 级联字典 只能填该职业包含的工种;无工种填"无";专项无需填写
I 报考等级 🔴 必填 下拉(5) 只能填当前计划中该职业/工种已有的等级;专项无需填写
J 是否补考 🔴 必填 下拉 是 / 否
K 补考理论 🟢 级联 下拉(4) 受 J 与等级约束(见下)
L 补考实操 🟢 级联 下拉(6) 同上
M 补考综合 🟢 级联 下拉(2) 同上
N 是否在校生 🔴 必填 下拉 是 / 否
O 在读学校 🟢 级联 文本 N=是 时必填
P 所学专业 🟢 级联 文本 N=是 时必填
Q 入学时间 🟢 级联 日期 N=是 时必填,格式必须 YYYY-MM-DD
R 本职业工作年限 ⚫ 选填 数字 只能填阿拉伯数字
S 已获证书职业工种 ⚫ 选填 文本 —
T 已获证书等级 ⚫ 选填 下拉(5) —
U 已获证书编号 ⚫ 选填 文本 —

颜色语义(官方模板约定,必须支持)

颜色 含义 涉及列
🔴 红色 必填项 A B C D E F G H I J N(11 项)
🟢 绿色 级联选项(与其它选项关联) K L M O P Q(6 项)
⚫ 黑色 选填项 R S T U(4 项)

官方下拉选项全集(照抄,不得擅改)

字段 选项
学历 博士研究生 / 硕士研究生 / 大学本科 / 大学专科 / 中等专科 / 职业高中 / 技工学校 / 普通中学 / 初级中学 / 小学 / 其他(11)
报考等级、已获证书等级 五级/初级工 / 四级/中级工 / 三级/高级工 / 二级/技师 / 一级/高级技师(5)
是否补考、是否在校生 是 / 否
补考理论 理论-纸笔作答 / 理论-机考 / 理论-直接认定 / 纸笔作答(4)
补考实操 实操-现场操作 / 实操-模拟操作 / 实操-纸笔作答 / 实操-机考 / 实操-直接认定 / 技能操作(6)
补考综合 综合评审 / 综合评审-直接认定(2)

官方说明:"下拉选项无需手填,选中表格列点击箭头选择即可;若手动输入则只能填写下拉选项中包含的值"。

级联与业务规则(源自《填表注意事项》原文)

# 规则 原文要点
1 是否补考 = 否 → 补考理论/实操/综合 全部不填 级联
2 是否补考 = 是 → 按等级限制补考科目数量 见下
3 是否在校生 = 是 → 在读学校、所学专业、入学时间 三项必填 级联
4 是否在校生 = 否 → 上述三项不填 级联
5 本职业工作年限 → 只能填阿拉伯数字 校验
6 工作单位 → 没有填"无" 值约定
7 报考职业 → 只能填当前评价计划中的职业 计划约束
8 报考工种 → 只能填该职业包含的工种;无工种填"无";专项无需填写 计划约束
9 报考等级 → 只能填当前计划中该职业/工种已有的等级;专项无需填写 计划约束

补考科目数量约束(规则 07 原文,极易做错)

认定类型 / 等级 可补考科目数 可选字段
专项职业能力 0(没有补考) —
职业技能等级 三、四、五级 只能选 1 个 补考理论 / 补考实操 中选一个
职业技能等级 一、二级 最多选 2 个 补考理论 / 补考实操 / 补考综合 中最多选两个

⚠️ 三个必须注意的实现陷阱

  1. "无"是合法业务值,不是空值 工作单位、报考工种都可能填"无"。系统在做必填校验、去重、单位统计时不能把"无"当作未填写处理。
  2. 专项职业能力是第三种认定类型(见 §5.3),它没有工种、没有等级、没有补考——校验规则要整体绕过这三项。
  3. 表头位置不固定,且工作表名不固定 实测两份真实文件:
    文件 工作表名 表头行
    空白官方模板 考生报名信息 第 1 行
    班级实际文件 填表注意事项 第 6 行(老师把数据填进了说明表)
    ⇒ 绝不能写死"第 1 行是表头"或"读某个固定工作表",必须按"前 10 行内查找含『姓名』『身份证号』的行"来识别(对应 FR-501)。

5.5 名册管理(FR-500)

编号 需求 优先级
FR-501 Excel 导入(自动识别表头行、跳过示例行 —— 实测两份真实文件表头分别在第 1 行和第 6 行,且工作表名不同,绝不能写死) P0
FR-502 导入前预校验:证件号格式、重复行、必填缺失,生成"导入体检报告" P0
FR-502a 名单即预填报名表:名单 21 列的值全部入库(18 个正式列 + 姓名/身份证号已有列),学生端按"锁定/预填可改/学生必填"三类规则呈现(见 §5.3.1) P0
FR-502b 名单字段三类规则:姓名+身份证号+报考职业/工种/等级 → 锁定(名单为空则阻断发布);其余 → 有值则预填可改、无值则学生必填 P0
FR-503 名单核验(门禁):填报人进入前必须填写「姓名 + 身份证号」匹配名单,命中才进入填报页;命中后绑定名单项 P0
FR-504 未命中统一提示(不区分"号码不在名单"与"姓名不符",防探测),提供 "重新填写" 与 "申请加入名单"(FR-509) 两条出路;取消原"自助认领" P0
FR-504a 匹配接口限流:同 openid 每小时 ≤10 次、同 IP 每分钟 ≤20 次,超出锁定并提示 P0
FR-504b 名单不下发前端,匹配全程在服务端完成;失败尝试不记录明文证件号 P0
FR-504c 姓名形近字/同音字纠错:身份证号命中但姓名不符时,给出"你是不是 XXX?"让用户确认 P1
FR-504d 名单项已被他人绑定时提示"该信息已被登记",不覆盖原绑定 P0
FR-505 名单导出、批量修改(例如统一修改专业) P1
FR-506 名单变更留痕(谁在何时把 A 的证件号从 X 改成 Y) P0
FR-507 发布前校验:名单人数 > 0 且必须含「姓名」「身份证号」列,否则阻断发布 P0
FR-508 名单追加导入(按证件号去重合并),追加后新人立即可填报 P0
FR-509 名单加入申请(客户已确认采纳的建议增量):门禁失败页提供「申请加入名单」入口 → 生成管理员待办 → 审批通过自动写入名册并放行。解决了"名单外的人只能找老师手工加人"的运营痛点 P0
FR-509a 通道边界:入口只在门禁失败页,不在填报页内;申请期间仍不能填报——通道只解决"进名单",不绕过门禁 P0
FR-509b 申请表单:姓名、身份证号、手机号(均必填)+ 自述班级 / 说明(选填);提交后展示"已提交,等待学校审核",并可查看审批进度 P0
FR-509c 限流与防灌:同 openid 每任务 ≤ 2 次;同证件号每任务仅保留 1 条待审或已通过申请(被驳回后可重新申请);单任务待审申请 > 50 条时告警管理员 P0
FR-509d 敏感信息处理与门禁一致:姓名明文存(业务必需);证件号与手机号仅存 HMAC + 脱敏值,不存明文 P0
FR-509e 管理员审批:通过 → 自动在目标任务名册中新建名册项(姓名/证件号取申请值、班级取自述或由管理员指定)并回填 roster_id;驳回 → 必填原因 P0
FR-509f 结果通知与可查:审批结果通过微信订阅消息推送(bizType=7 / 模板码 JOIN_APPROVE),申请人可随时在小程序查看申请状态与驳回原因 P0
FR-509g 审批通过后该 openid 自动绑定新名册项,学生再次进入直接放行(免重输);名册列表中该行标注「来自申请」,便于追溯 P1
FR-509h 全程留痕:申请与审批均写入操作日志(申请人、审批人、时间、结果、驳回原因);申请记录保留 2 年 P0
FR-509i 不能替代门禁:审批通过建的名册项与导入的名册项在后续流程中完全等价(可填报、可审核、可导出) P0

5.6 材料采集与规则引擎(FR-600)

编号 需求 优先级
FR-601 材料项配置:材料名、别名集合、是否必传、条件显示、示例图、说明文案、材料来源引导语 P0
FR-601a 材料来源通道(⚠️ 本版修正):仅提供 ① 从微信聊天记录选择 ② 从手机相册选择 ③ 从聊天/手机文件选择;「直接拍照」默认关闭,仅材料级可配开启。依据:实测 4 类材料(截图/扫描件/照相馆电子版/翻拍件)无一来自现场拍照 P0
FR-601b 保留原始文件名:从聊天记录选择文件时须取用微信返回的原始文件名作为命名规范化的首要输入(这是准确率最高的信息源);相册来源无文件名时降级为 OCR 识别 P0
FR-602 命名规则配置:模板变量拖拽({姓名}-{证件号}-{材料名}{后缀}),支持自定义前缀/后缀、大小写、连接符 P0
FR-603 命名规范化引擎:自动修复双扩展名、缺分隔符、大小写、材料同义词、姓名与名册纠错(详见 6.1) P0
FR-604 格式白名单:jpg / jpeg / png / pdf / heic(可配),非白名单自动转换 P0
FR-605 体积上限:默认 ≤1M,材料级可配(0.3–5M);同时具备体积下限保护(防止过度压缩,见 D3) P0
FR-606 自动压缩:按 §6.3 五档策略(P1 证件照 / P2 证件翻拍 / P3 A4 扫描件 / P4 截图 / P5 文档)智能压到目标体积且保证清晰度下限 P0
FR-607 图片自动纠偏:EXIF 方向校正、去黑边、去阴影(可选)、自动旋转 P1
FR-608 证件照规格化:一寸(295×413 @300dpi)、白底/蓝底替换、人脸居中 P1(实测 984×1378 → 295×413 可再降 70% 体积,收益已验证,提前至 P1)
FR-609 多页材料合并(身份证正反面自动合成 1 张 / 1 个 PDF);支持从聊天记录一次多选 P1
FR-610 上传即处理:客户端预处理 + 服务端规范化,返回处理前后对比(体积、文件名) P0
FR-611 处理失败的兜底:保留原图并提示原因,允许人工重试或跳过 P0
FR-612 原图保留策略:默认保留 30 天(可配),便于回溯与重处理 P0
FR-613 教师代传(记录代传人身份);教师端同样走三个来源通道 P0
FR-614 材料识别引擎(四层递进,实测校正后):① 文件名(能定就定,实测 100%)→ ② 形态只分流派、不定型 → ③ OCR 强词加权计分(实测 100%)→ ④ VLM 兜底(实测触发率仅 2%~10%)。详见功能逻辑卷 §3.5-B P0
FR-614a 识别结果只提示、不自动改位置(散图分拣除外);识别失败一律放行,不得阻断提交 P0
FR-614b 强特征词表可配置(平台预置 + 租户自维护);通用词(姓名/学校/专业/出生/单位)禁止入表(实测为噪声) P0
FR-614c 一键移动:用户确认后把文件从错误槽位移到正确槽位(保留版本) P0
FR-614d 审核辅助:材料卡片显示"系统判定:身份证 92%",供班主任快速判断 P1
FR-614e 一寸照片零文字判据:整图 OCR 文字量 < 5 字 → 判定为一寸照片 P0
FR-615 任务双层结构:每个任务由「填什么(表单字段,collect_form_schema)」+「传什么(材料清单,collect_material_def)」两层独立配置,二者可互相条件联动(FR-402) P0
FR-616 清晰度分级标记与抽检机制:按实际 DPI/质量给文件打「优 / 良 / 可 / 强制压缩」四级;🟡「可」与🔴「强制压缩」进入抽检清单,供班主任在审核时顺带确认。实测 28 张扫描件全部达标、不产生待抽检文件——默认不启用,仅作兜底 P1

材料清单默认模板(源自真实《申报材料上传要求》,含 §6.3 压缩档位)

场景 材料项 规范名示例 上限 压缩档位
通用 线下报名信息表 由系统生成 Excel — —
自主认定(学校) 职业技能等级个人申报表(手签+学校公章) 张三-360121200010101234-工作证明.jpg 1M P3 扫描件
自主认定(学校) 一寸电子照片 张三-360121200010101234-一寸照片.jpg 1M P1 证件照
自主认定(学校) 身份证正反面扫描件 张三-360121200010101234-身份证.jpg 1M P2 证件翻拍
自主认定(学校) 学籍表 张三-360121200010101234-学籍表.jpg 1M P4 截图
社会评价 个人申报表(手签+单位公章) 张三-360121200010101234-工作证明.jpg 1M P3 扫描件
社会评价 一寸电子照片 张三-360121200010101234-一寸照片.jpg 1M P1 证件照
社会评价 身份证正反面扫描件 张三-360121200010101234-身份证.jpg 1M P2 证件翻拍
社会评价 学信网备案表 张三-360121200010101234-学信网备案表.jpg 1M P2/P4(按形态自动判定)
社会评价 其它佐证材料(毕业证/社保权益单/职称/技能证书) 张三-360121200010101234-其它佐证材料.jpg 1M P2/P3/P4(自动判定)
专项职业能力 其它佐证材料(按机构要求) 张三-...-其它佐证材料.jpg 1M 自动判定

📌 档位由系统自动判定形态后套用(依据:像素尺寸、宽高比、色彩深度、来源),也允许管理员在材料级强制指定。

5.7 校验引擎(FR-700)

编号 校验规则 触发时机 严重级别 处理方式
VAL-01 身份证号格式与校验位(ISO 7064 MOD 11-2) 输入时 / 提交时 阻断 不允许提交
VAL-02 身份证号与名册一致 输入时 / OCR 后 阻断 提示正确值,需确认
VAL-03 姓名与名册一致(含近音字、形近字建议) 仅门禁输入时 警告 建议纠正,可强制通过。
⚠️ 修订(客户确认):本条仅适用于学生在门禁页手输姓名的场景(手输会打错字);不适用于文件名——文件名里的姓名不是"错误",而是归属线索
VAL-04 同一人跨材料证件号一致 每次上传后 阻断 标红,定位到冲突材料
VAL-05 材料完整性(必传项是否齐全) 提交时 阻断 列出缺失项
VAL-06 文件格式白名单 上传时 阻断 自动转换或提示重传
VAL-07 文件体积上限 上传时 阻断 自动压缩,压不动则提示
VAL-08 图片可读性(分辨率过低 / 全黑 / 全白 / 模糊) 上传时 警告 提示重新提供(重扫描 / 索取原图)
VAL-09 重复文件检测(文件哈希) 上传时 警告 提示"与 XX 的材料重复"
VAL-10 材料有效期(身份证是否过期) OCR 后 警告 提示
VAL-11 名册重复人员检测 导入时 阻断 导入报告指出行号
VAL-12 材料内容与类型匹配(防传错位置):按 §3.5-B 材料识别逻辑判定,仅提示、不阻断、不自动改位 上传后(异步) 警告 提示"这看起来是 XX,要移过去吗?" + 一键移动
VAL-13 报考职业 / 工种 / 等级必须在「评价计划」范围内(源自模板规则 04/05/06) 选择时 + 提交时 阻断 只展示计划内选项,从源头防错
VAL-14 报考工种必须属于所选职业;该职业无工种时只能填"无" 选择时 阻断 级联联动
VAL-15 补考科目数量与等级匹配:三/四/五级最多 1 科;一/二级最多 2 科;专项职业能力无补考 提交时 阻断 超出则禁止勾选并提示
VAL-16 是否在校生 = 是 时,在读学校 / 所学专业 / 入学时间三项必填;入学时间格式必须 YYYY-MM-DD 提交时 阻断 列出缺失项
VAL-17 本职业工作年限只能为阿拉伯数字 输入时 阻断 非数字直接拒绝
VAL-18 文件名规范化:双扩展名、非法字符、缺分隔符、材料名归一(别名→规范名) 上传时 信息 自动修复,展示前后对比
VAL-19 证件号疑似串档:校验位均通过,但与名册及其他材料均不符 上传后(异步) 阻断 转人工,不自动修正
VAL-20 文件体积下限保护:体积异常小,疑似非原图 上传时 警告 对照同批中位数提示"建议索取原图"
VAL-21 文件归属校验:文件名里的证件号能反查到名册中的另一个人,或归属无法确证 上传后(同步) 阻断 阻断提交,提示"这个文件名对应的是「李四」,不是你的材料",要求重新上传;绝不自动按名册改名

校验结果呈现三层:绿色(通过)/ 黄色(警告,可继续)/ 红色(阻断,必须处理),填报端即时反馈。

5.8 审核工作台(FR-800)

编号 需求 优先级
FR-801 待办列表(按状态、班级、异常类型筛选) P0
FR-802 学生卡片式浏览:头像、姓名、班级、5 项材料缩略图、红黄绿状态 P0
FR-803 材料在线预览(图片缩放、PDF 翻页、左右对比原图/压缩图) P0
FR-804 单个通过 / 驳回(填写驳回原因,支持常用原因快捷模板) P0
FR-805 批量通过(仅一键通过全部绿色项) P0
FR-806 批量驳回(同因驳回) P1
FR-807 审核意见历史与时间轴留痕 P0
FR-808 审核人变更 / 转交 P1
FR-809 审核统计(人均处理时长、驳回率、驳回原因 TOP10) P1
FR-810 管理员终审 + "一键全部通过"(仅在零异常时可用) P0

驳回原因快捷模板(示例) 材料不清晰,请重新提供 / 身份证号与名册不符 / 材料不是本人 / 文件超过 1M / 缺少 XX 材料 / 工作证明未盖公章

5.9 汇总与导出(FR-900)

编号 需求 优先级
FR-901 导出范围选择:全任务 / 指定班级 / 指定人员 / 仅通过终审 P0
FR-902 目录树自动生成(见下方规范) P0
FR-903 打包格式:ZIP / RAR(可选)/ 仅目录结构 P0
FR-904 分卷压缩:支持按固定体积分卷(如 ≤50MB/卷),便于上传与传输 P0
FR-905 汇总 Excel 自动生成(合并全部表单字段 + 已完成情况) P0
FR-906 导出包命名规范:{批次号}-{专业}-{班级}-{材料类型}-{日期}.zip P0
FR-907 异步导出 + 进度查询 + 完成后微信/短信通知 P0
FR-908 导出历史与重复下载(不重复打包,命中缓存) P1
FR-909 脱敏导出(证件号打码,用于非必要场景) P1
FR-910 导出审计日志(谁、何时、导出了多少人的数据) P0

导出目录规范(默认,可配置)

{批次号}-{任务名称}/
├── 24中技数控1班/
│   ├── 一寸照片/
│   │   └── 张三-360121200010101234-一寸照片.jpg
│   ├── 身份证/
│   │   └── 张三-360121200010101234-身份证.jpg
│   ├── 学籍表/
│   │   └── 张三-360121200010101234-学籍表.jpg
│   └── 工作证明/
│       └── 张三-360121200010101234-工作证明.jpg
├── 24中技数控2班/
│   └── ...
└── 考生线下报名信息导入模板(汇总).xlsx

5.10 通知与催报(FR-1000)

编号 需求 优先级
FR-1001 通知渠道:微信订阅消息(主)、短信(备,按条计费)、公众号模板消息 P0
FR-1002 通知模板可配置(任务发布、临期提醒、驳回通知、审核通过、导出完成) P0
FR-1003 自动催报策略:截止前 7/3/1 天 + 截止当日,仅催未提交者 P0
FR-1004 一键催报名单(可指定人员、班级) P0
FR-1005 通知记录与到达率统计 P1
FR-1006 班主任侧"班级完成率"日报(可开关) P2

5.11 数据看板(FR-1100)

编号 需求 优先级
FR-1101 任务总览:应填/已填/待审/已通过/驳回数、完成率环形图 P0
FR-1102 班级排行榜(完成率、异常率),便于管理员督促 P1
FR-1103 材料维度统计(哪类材料最易出错、平均处理时长) P1
FR-1104 异常清单导出(全部红黄项一览,供班主任逐条处理) P0
FR-1105 租户级运营看板(供平台运营) P1

5.12 系统管理与审计(FR-1200)

编号 需求 优先级
FR-1201 字典管理(专业、工种、等级、学历、地区) P0
FR-1202 操作日志(登录、增删改、审核、导出、下载) P0
FR-1203 文件下载/预览日志(含 IP、UA) P0
FR-1204 敏感信息脱敏展示(证件号 3601**********1234) P0
FR-1205 数据备份与恢复策略 P0
FR-1206 数据导出与账号注销(合规要求) P1

5.13 SaaS 计费(FR-1300)

编号 需求 优先级
FR-1301 套餐定义:免费试用 / 标准版 / 旗舰版 / 私有化 P0
FR-1302 计量项:任务数、人数、存储量(GB·天)、OCR 次数、短信条数 P0
FR-1303 用量看板与超额提醒、续费提醒 P1
FR-1304 在线开通与试用(14 天自动到期降级) P1

建议套餐(待定价)

版本 适用 额度
免费试用 单个班主任体验 1 个任务 / 60 人 / 14 天
标准版 单个学校 不限任务,按在册人数计费(如 ¥3–5/人/学期)
旗舰版 多校区/大型机构 不限人数,含定制规则、专属客服、API 对接
私有化 政企/大型认定机构 一次性 + 年维护

5.14 历史数据整理器(FR-1400)★ 差异化获客功能

背景:客户手头已有大量"历史乱数据"(如本项目的 436MB .rar)。让他们先看到价值,再谈系统上线。

编号 需求 优先级
FR-1401 上传 ZIP/RAR 或指定目录结构,系统自动解析文件树 P1
FR-1402 自动识别"姓名 / 证件号 / 材料名"三要素并规范化命名 P1
FR-1403 批量压缩到目标体积、格式统一 P1
FR-1404 输出**《材料体检报告》**:命名违规清单、证件号异常清单、体积超标清单、缺失材料清单、重复文件清单 P1
FR-1405 输出规范化后的 ZIP 包 + 可直接上报的目录结构 P1
FR-1406 报告与结果一键分享给班主任(生成链接) P2

用本项目 24中技数控1班 做基准测试集,预期报告输出:命名违规 59 项、证件号异常 5 项、体积超标 31 项、疑似串档 1 项。


6. 核心规则详述(规则引擎)

本章是本文档技术含量最高的部分,也是产品壁垒所在。

6.0 场景化材料清单(源自真实《申报材料上传要求》原文)

场景 材料 命名要求
通用 EXCEL:考生线下报名信息导入模板 系统生成
自主认定 WORD:职业技能等级个人申报表(手签后盖学校公章的扫描件) 张三-360121200010101234-工作证明.jpg
自主认定 一寸电子照片(jpg) 张三-360121200010101234-一寸照片.jpg
自主认定 身份证正反扫描件 张三-360121200010101234-身份证.jpg
自主认定 学籍表 张三-360121200010101234-学籍表.jpg
社会评价 WORD:个人申报表(手签后盖单位公章) 张三-360121200010101234-工作证明.jpg
社会评价 一寸电子照片(jpg) 张三-360121200010101234-一寸照片.jpg
社会评价 身份证正反扫描件 张三-360121200010101234-身份证.jpg
社会评价 学信网备案表 张三-360121200010101234-学信网备案表.jpg
社会评价 其它佐证材料(毕业证、社保权益单、职称证明、技能等级证书等) 张三-360121200010101234-其它佐证材料.jpg
全局约束 所有图片格式 jpg / png 单文件 ≤ 1M

⚠️ 注意:"个人申报表"的规范文件名却是"工作证明"(认定机构的既有习惯),必须在材料项配置中通过"别名映射"支持这种反直觉规则。

6.1 文件名规范化引擎

规则模板(可配置)

{姓名}-{证件号}-{材料名}.{后缀}          ← 默认

可用变量:{姓名} {证件号} {证件类型} {材料名} {班级} {专业} {工种} {等级} {报名点} {序号} {日期} {自定义字段.xxx}

解析与规范化算法(7 步)

输入文件名 → ① 剥离扩展名(循环剥离 .jpg/.jpeg/.png/.pdf/... 直至无匹配)
          → ② 证件号提取(正则 \d{15,18}[xX]?,取最长匹配)
          → ③ 证件号归一化(补零/大小写,17/19 位则触发名册纠错)
          → ④ 材料名识别(与材料别名表做最长匹配;未命中则用"其他")
          → ⑤ 姓名候选(剩余中文串,2–4 字;**仅作辅助证据**)
          → ⑥ **归属校验**(证件号反查名册 → OCR 佐证 → 姓名仅辅助,见裁决矩阵)
          → ⑦ 组装规范名(非法字符替换、重名加序号)

文件归属裁决矩阵(核心算法,优先级从高到低)

⚠️ 先明确一件事:名册的姓名与身份证号是唯一权威,且先于填报存在(门禁核验通过才能填)。 因此填报单里的姓名永远不会错。文件名的任何不符,都不该被理解为"名称错误",而应被理解为 「这份文件可能不属于这个人」。

由此推出两条铁律:

  1. 输出名一律取名册值,文件名的值只用于判断归属,不用于命名;
  2. 凡归属存疑,一律阻断提交,让填报人自己改——绝不自动按名册改名。 自动改名会把"A 传了 B 的材料"静默洗白成 A 的材料,审核时完全看不出来,事后无从追溯。

三条证据(优先级从高到低)

级别 证据 可信度 说明
E1 文件名证件号能反查名册(定位到某一行) ★★★ 名册证件号有唯一索引,能查到谁就是谁——这是最强判据,一次查询即得
E2 OCR 读出的证件号 ★★ 读的是文件里印的字,比文件名可靠
E3 文件名里的姓名(相似度) ★ 弱证据,仅辅助。姓名单独不构成规则

裁决矩阵

# 文件名情况 名册 结论 处理
1 无证件号(如 微信图片_20260928.jpg) 有值 正常情况 按名册补全,不产生任何提示
2 证件号 = 名册值 有值 归属确证 按名册输出。姓名若有差异,视为错别字,仅内部留痕不打扰学生
3 证件号能反查到名册中的另一个人 有值 🔴 铁证:这是那个人的材料 阻断提交——"这个文件名对应的是「李四」,不是你的材料"
4 证件号合法,但名册中查不到 有值 归属存疑 有 E2 且 E2=名册 → 按名册处理(信息级);否则 阻断提交,要求重新上传
5 证件号校验位失败 / 位数不足 有值 文件名打错(实测 2 例) 自动按名册(信息级提示)——校验位失败说明这不是一个真实的号码,不构成归属证据
6 名册为空(尚未导入) 无值 无名册可依 保留原值 + 标黄;不猜

💡 第 3 条与第 5 条的区别是整张表的精髓: 校验位失败 = 不是一个真实号码 → 只是写错了,可以自动改; 校验位通过 = 一个真实存在的自然人 → 改了就是改了身份,必须阻断。 这正是校验位算法带来的直接收益:它把"这到底是谁的材料"从人判断变成机器判断。

📌 已废除:原矩阵中"不在名册 → 转待认领队列"——"自助认领"机制已在 V1.5 废除(会绕过门禁)。 证件号不在名册时的正确出路是:引导走「申请加入名单」通道(FR-509,客户已确认采纳), 由管理员逐条审批后入册,而不是让系统自行认领。

容错与纠正对照表(基于真实数据)

原始文件名 识别到的问题 规范名 自动 / 需确认
于豪-360103200805186619-工作证明.jpg.jpg 双扩展名 于豪-360103200805186619-工作证明.jpg 自动
曾伊豪-361029200907295110身份证.jpg.jpg 缺分隔符 + 双后缀 曾伊豪-361029200907295110-身份证.jpg 自动
徐程宇360981200901292334-身份证.jpg.jpg 缺分隔符 徐程宇-360981200901292334-身份证.jpg 自动
朱彬宇-36012420080712697x-身份证.jpg.jpg 小写 x 朱彬宇-36012420080712697X-身份证.jpg 自动
于豪-360103200805166169-身份证.jpg.jpg 与名册不符,校验位失败 于豪-360103200805186619-身份证.jpg ✅ 自动(校验位裁决为文件名错误)
杨晨-360825200904303218-身份证.jpg.jpg 与名册不符,校验位失败 杨晨-360825200904303215-身份证.jpg ✅ 自动(同上)
陈子文-360122200902044232-一寸照片.jpg 与名册不符,校验位均通过 — 🔴 阻断提交(两个真实的人 → 串档;绝不自动改正)
曹文锋-36012120081211239-一寸照片.jpg 证件号 17 位 曹文锋-360121200812111239-一寸照片.jpg 名册兜底补齐 + 标黄确认
黎思明-36100200907283419-身份证.jpg.jpg 证件号 17 位 黎思明-361002200907283419-身份证.jpg 同上
扬晨-360825200904303215-一寸照片.jpg 姓名形近字(证件号正确) 杨晨-360825200904303215-一寸照片.jpg ✅ 自动(归属已由证件号确证;姓名仅留痕,不打扰学生)
...-工作证明扫描件.jpg 材料名同义词 -工作证明.jpg 自动
1寸照片 / 照片 / 证件照 材料名同义词 -一寸照片.jpg 自动

材料名同义词表(可维护)

规范材料名 同义词
一寸照片 1寸照片、一寸电子照片、证件照、照片、免冠照
身份证 身份证正反面、身份证扫描件、身份证正面、身份证反面
学籍表 学籍证明、在读证明、学籍卡
工作证明 在职证明、工作证明扫描件、个人申报表、申报表、单位证明
学信网备案表 学信网学历备案表、学历证书电子注册备案表、备案表
其它佐证材料 毕业证书、社保个人权益单、职称证明、技能等级证书、佐证

姓名的定位:辅助证据,不是独立规则

⚠️ 修订(客户确认):姓名单独不构成规则。 理由:名册先于填报存在且门禁已核验,填报单里的姓名永远是对的; 文件名里的姓名既不用于命名,也不是可靠的归属证据(弱证据 E3)。

情形 处理
证件号 = 名册值,姓名与名册有差异 归属已由证件号确证 → 按名册取值(如"扬晨"→"杨晨")。
不打扰学生,仅写入 collect_file_process_log 供审计
证件号 = 名册值,姓名完全一致 无事发生
证件号 = 名册值,姓名完全不同(如"李四"vs"张三") 归属仍由证件号确证 → 按名册取值,但记一条信息级提示供审核参考
证件号存疑(矩阵第 3/4 条) 不看姓名——直接走阻断流程。姓名相似不能成为放行理由

实测支撑:112 个真实文件里"姓名与名册不符"仅 1 例(扬晨 vs 名册杨晨), 且该例的证件号是正确的——因此它本来就该按"矩阵第 2 条"处理,既不需要姓名规则,也不该触发任何确认。 一条真实数据里只出现 1 次、且有更强判据可替代的规则,不值得单独做一套逻辑。

姓名的技术实现(仅在需要时调用)

FUNCTION name_differs_soft(fn, rn):        # 仅用于矩阵第 2 条的"留痕"
    RETURN edit_distance(fn, rn) <= 1 AND same_pinyin(fn, rn)
    # 注意:返回值只影响"要不要写审计日志",不影响任何放行/阻断决策

冲突处理:同一人同一材料重复上传 → 保留版本历史(version 递增),导出取最新有效版本。

6.2 身份证号校验算法(ISO 7064:1983 MOD 11-2)

1. 长度必须为 18(15 位旧号自动升级支持)
2. 前 17 位必须为数字,第 18 位为数字或 X/x
3. 地址码前 2 位必须是有效省级行政区代码
4. 出生日期段(第 7–14 位)必须是合法日期,且年龄在合理区间(如 10–80 岁)
5. 校验位计算:
   加权因子 W = [7,9,10,5,8,4,2,1,6,3,7,9,10,5,8,4,2]
   S = Σ(a[i] × W[i]),i = 0..16
   校验码表 C = "10X98765432"
   合法 ⟺ C[S mod 11] == a[17](X 需大写比较)

用途:填报端实时校验、名册导入体检、历史数据整理器、OCR 结果二次验证。

6.3 压缩规则(按材料形态分档 + 强制 1M)

核心决策(D1,已定):所有材料一律强制输出 ≤1M,扫描件无例外、不设体积放宽选项。 当"≤1M"与"清晰度"冲突时——体积优先,牺牲清晰度;但被牺牲清晰度的文件必须标记出来交人工抽检。

⚠️ 本节已按真实数据重写。 初稿按"长边"单一口径设计,实测发现四类材料物理形态完全不同(A4 扫描件 / 手机翻拍 / 系统截图 / 证件照),用同一把尺子必然出错,详见 §1.2 ④。 📄 本文只保留设计结论;完整的压缩运转逻辑(执行顺序、标记规则、抽检机制、二次有损防护、超大图保护)见配套文档《数据信息填报系统PRD-功能逻辑卷》§3.7。

五档策略(材料级可配)

档位 适用材料 形态判定 目标体积 尺寸规则 质量区间 依据
P1 证件照档 一寸照片 标准证件照 ≤0.3M 固定 295×413 px(25×35mm @300dpi) 85 真实数据 984×1378 为 5:7 标准比例,规格化收益明确
P2 证件翻拍档 身份证、备案表 手机翻拍(透视畸变) ≤1M 长边 clamp 至 1400–2400 px 72–85 自适应 真实数据 558×745 ~ 2216×2796,跨度 4 倍
P3 扫描件档 工作证明、申报表 A4 纸张扫描/翻拍 ≤1M(硬约束,无例外) 按 A4 等比缩放至 300dpi(2480×3508) 85 实测 28/28 达标,平均 806KB,无需降质
P4 截图档 学籍表、系统导出件 屏幕截图(16:9、像素规整) ≤1M 保持原始像素,不缩放 无损 / PNG 真实数据 1025×590 PNG,缩放与转 JPG 均会损伤文字
P5 文档档 PDF / Word 文档 ≤5M 页面重采样 — —

📌 A档与证件照规格统一(初稿矛盾已修正):P1 档的"最小长边 600px"与 FR-608 的"295×413"互相冲突,现统一为固定标准尺寸,长边 413px。

安全闸门(前置,必做)

⚠️ 真实数据中存在 11050×16170 = 1.8 亿像素的扫描件。
   全量解码需约 1.8亿 × 3B ≈ 540MB 内存,30 并发即 16GB,必然 OOM。

强制要求:
1. 图片处理引擎使用 libvips(流式、分块、金字塔降采样),**禁止使用 Java ImageIO / PIL 全量解码**
2. 任何处理前先读取头部获取宽高;像素数 > 5000 万 → 直接走 vips_thumbnail 降采样到安全区再处理
3. 单文件处理超时 20s 熔断;处理队列设置单机并发上限(建议 ≤4 线程 × 2 实例,按内存反推)
4. 解压炸弹防护:单文件像素上限 2 亿、单文件体积上限 50MB

压缩算法(一次到位 · 按形态定参数)

输入:图片 img,档位 profile,目标体积 targetSize = 1M(硬约束)

1. 【安全闸门】见上,超限先降采样
2. EXIF 方向校正 → sRGB 色彩一致化 → 剥离 GPS/设备隐私信息
3. 【尺寸归一化 + 一次编码】按档位取"已经实测达标"的参数直接输出:
     P1 → 裁切并缩放至 295×413,写入 DPI=300,质量 85
     P2 → 长边压至 min(原长边, 2400),且不得低于 1400,质量 85 起下探
     P3 → 按 A4(210:297) 等比缩放至 300dpi(2480×3508),质量 85   ← 实测 28/28 达标
     P4 → 保持原始像素,仅无损优化
4. 【体积检查】编码后测量体积:
     若 ≤ 1M → 直接输出,结束(预计 100% 走这条路径)
     若 > 1M → 进入降质路径(兜底,预计极少触发)
5. 【兜底降质路径】顺序按形态区分:
     · 文字类(P3/P4):先降采样(×0.85 步长)→ 触底后再降质量(85→80→75→70→65→60)
       理由:JPEG 量化伪影集中出现在高对比边缘(正是笔画处),
             而等比重采样是受控的平滑,对文字可读性更友好
     · 照片类(P1/P2):先降质量(85→80→76→72)→ 触底后再降采样
       理由:人脸细节对重采样敏感,对轻度质量下降不敏感
6. 【地板】DPI 不低于 120、质量不低于 60;触碰地板仍超 → 继续降采样直至达标,
     并标记"强制压缩",必须人工确认
7. 【过度压缩保护】若输出体积 < 100KB 且分辨率低于建议值 → 标记"疑似过度压缩"
8. 记录处理前后体积、质量、尺寸 → collect_file_process_log

✅ 待验证项已闭环(2026-09-24 实测完成):初稿担心的"降质顺序"问题在本批数据上根本不会触发——300dpi/q85 一次编码即全部达标(平均 806KB / 最大 831KB / 28 张 0 超标)。 ⇒ 降质顺序仍需实现(作为兜底),但不再是关键路径,也不必再为此做 A/B 测试与机构确认。 ⇒ 真正需要保留的验证是:认定机构确认 300dpi 扫描件可接受(这已不是技术问题)。

输出格式决策

原格式 场景 输出
JPG/JPEG 全部 JPG(保留方向与色彩)
PNG 照片 / 扫描件(无透明通道) JPG(白底填充)
PNG 截图 / 系统导出(P4 档) PNG(无损优化) ⚠️ 初稿会在此处转 JPG 并损伤文字,已修正
HEIC/HEIF iPhone 拍摄 JPG
PDF 多页扫描件 PDF(图片重采样)或转 JPG(单页)
BMP/TIFF 少见 JPG

性能预期(基于真实样本实测)

输入(真实文件) 输出 耗时目标
谭宇峰-工作证明 11114×16320 / 17.2MB(1.8 亿像素) A4@300dpi 2480×3508 / 约 810KB(实测) ≤3s(libvips)
某-工作证明(本批 28 张全部) 平均 806KB / 最大 831KB / 0 张超标 28 张合计 ≤60s
于豪-身份证 1148×1520 / 0.95MB 约 0.4MB ≤1s
杨晨-身份证 558×745 / 0.08MB ⚠️ 不压缩,标记"分辨率不足" ≤0.3s
某-学籍表 1025×590 / 0.04MB PNG 原样(无损优化后 ≈0.035MB) ≤0.3s
某-一寸照片 984×1378 / 0.39MB 295×413 / 0.06–0.12MB ≤0.5s

体积估算(以本项目为基准,实测)

材料 原始合计 处理目标 说明
工作证明 × 28 444 MB ≈ 22 MB 0.8MB/份
身份证 × 28 ≈ 20 MB ≈ 11 MB 0.4MB/份
一寸照片 × 28 ≈ 10 MB ≈ 3 MB 0.1MB/份
学籍表 × 28 ≈ 1 MB ≈ 1 MB 不变
合计 ≈ 475 MB(.rar 打包后 436MB) ≈ 37 MB 压缩率 ≈ 92%

✅ 修正:初稿写"原始 436MB",实为 .rar 压缩包体积;原始文件合计 475MB。

6.4 OCR 识别与自动回填

材料 OCR 引擎能力 提取字段 用途
身份证正面 身份证识别(专用) 姓名、性别、民族、出生日期、住址、身份证号 自动回填表单 + 与名册/文件名三方比对
身份证反面 身份证识别 签发机关、有效期限 有效期是否过期校验
学籍表 / 备案表 通用文字识别 姓名、身份证号、学校 内容一致性校验
工作证明 通用文字识别 + 关键词检测 是否含"公章"字样区域、姓名、单位 盖章完整性提示

置信度策略

置信度 处理方式
≥ 0.90 自动采纳并回填,标注"OCR 自动识别"
0.70 – 0.90 提示填报人确认识别结果
< 0.70 不采纳,要求人工填写;记录到"OCR 失败"统计供算法优化

三方一致性比对(核心价值点)

文件名中的证件号  ⟷  名册中的证件号  ⟷  OCR 识别出的证件号
        │                    │                    │
        └────────── 三者不一致 → 红色阻断告警 ──────┘

该机制可 100% 命中本项目实测的 5 例证件号错误与 1 例材料串档。

6.5 审核流可配置规则

{
  "flowType": "SCHOOL",
  "nodes": [
    {"node": "CLASS_AUDIT", "name": "班主任初审", "enabled": true,
     "assignee": "ROSTER_CLASS_HEAD", "mode": "ANY", "timeoutHours": 48},
    {"node": "ADMIN_AUDIT", "name": "管理员终审", "enabled": true,
     "assignee": "TENANT_ADMIN", "mode": "ANY", "timeoutHours": 72}
  ],
  "reject": {
    "maxRejectCount": 3,
    "escalateToAdmin": true,
    "allowResubmitAfterApproved": false,
    "resubmitWindowDays": 7
  },
  "autoApprove": {
    "enabled": false,
    "conditions": ["noRedIssue", "noYellowIssue", "ocrConfidenceAbove:0.95"]
  }
}

7. 数据模型与数据库设计

7.1 ER 关系概览

erDiagram
    sys_tenant      ||--o{ sys_org          : "拥有"
    sys_tenant      ||--o{ sys_user         : "拥有"
    sys_tenant      ||--o{ collect_task     : "创建"
    sys_org         ||--o{ sys_user         : "归属"
    sys_org         ||--o{ collect_roster   : "包含"
    collect_task    ||--o{ collect_form_schema : "定义表单"
    collect_task    ||--o{ collect_material_def : "定义材料"
    collect_task    ||--o{ collect_roster   : "含名单"
    collect_task    ||--o{ collect_task_org : "覆盖班级"
    collect_task    ||--o{ collect_submission : "产生填报单"
    collect_roster  ||--o| collect_submission : "对应"
    collect_submission ||--o{ collect_file  : "挂载文件"
    collect_file    ||--o{ collect_validation_issue : "触发"
    collect_submission ||--o{ collect_audit_record : "审核记录"
    collect_task    ||--o{ collect_export_task : "导出"
    collect_task    ||--o{ collect_notify_record : "通知"

7.2 表清单总览

分类 表名 说明 状态
平台 sys_tenant 租户(学校/机构) ✅
平台 sys_org 组织树(院系/班级) ✅
平台 sys_occupation_ref 职业工种参考字典(仅拼写校验与联想,非约束,P2) ✅
平台 sys_user 用户 ✅
平台 sys_tenant_package 租户套餐与用量 从略
平台 sys_role / sys_user_role 角色与用户角色 从略
平台 sys_dict_type / sys_dict_data 字典(学历、等级、补考科目等下拉项) 从略
平台 sys_operation_log 操作日志 从略
业务 collect_task 收集任务 ✅
业务 collect_task_org 任务覆盖组织与班主任绑定 ✅
业务 collect_form_schema 表单定义(含 21 内置字段 + 扩展字段定义,版本化) ✅
业务 collect_material_def 材料项定义(含命名与压缩规则) ✅
业务 collect_roster 名单(即预填报名表,含 18 个业务列) ✅
业务 collect_submission 填报单(与名单对称建列 + changed_fields) ✅
业务 collect_file 文件实体(含材料识别结果) ✅
业务 collect_file_process_log 文件处理日志(压缩/改名/OCR) ✅
业务 collect_validation_issue 校验问题 ✅
业务 collect_audit_record 审核记录 ✅
业务 collect_export_task 导出任务 ✅
业务 collect_notify_record 通知记录与订阅额度 ✅
业务 collect_subscribe_quota 微信订阅消息额度余额(应对 R12 次数限制) ✅
业务 collect_rule_template 规则模板(可复用) ✅
业务 collect_cleanup_job 历史数据整理任务 ✅
业务 collect_join_request 名单加入申请(⚠️ 客户确认后新增,FR-509) ✅

✅ = 本文提供完整 DDL;从略 = 标准表(角色/字典/日志),沿用现有框架实现。

⚠️ 已合并:collect_submission_item(填报单明细项)取消——collect_file 已直接挂在 collect_submission 上并带 material_code,无需中间表。ER 图已同步。 ⚠️ 已合并:collect_task_auditor(审核人绑定)已并入 collect_task_org。

通用字段约定(所有业务表):id tenant_id create_by create_time update_by update_time deleted

本项目数据库为 PostgreSQL,DDL 全部按 PG 语法书写并已在 PG 16 实测执行通过。

7.3 DDL 约定(PostgreSQL)

✅ 本节全部 DDL 已在 PostgreSQL 16.15 真实执行验证通过(零错误),并用真实名单数据跑通了聚合查询与索引验证。

项 约定 说明
数据库 PostgreSQL 12+(实测于 16.15) 纯 PG,不依赖 Vastbase 专有语法;如需国产化可整体迁移到 PG 兼容库
主键 id bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL 替代 MySQL 的 AUTO_INCREMENT
标识符 全小写 + snake_case(不加引号) PG 会把未加引号的标识符折叠为小写,避免大小写敏感问题
软删除 deleted smallint DEFAULT 0 NOT NULL 对齐现有 schema 习惯(不是 del_flag)
布尔语义 smallint(0/1) 与现有 schema 一致(不用 PG 的 boolean)
时间 timestamp ⚠️ PG 没有 ON UPDATE CURRENT_TIMESTAMP,更新时间由应用层统一写入,或建触发器
JSON jsonb(不是 json) 二进制存储、可建 GIN 索引、支持 @> / ? / ->>;仅用于扩展字段
注释 独立语句 COMMENT ON COLUMN t.c IS '...' PG 不支持列定义里的行内 COMMENT
索引 独立语句 CREATE INDEX ... ON t (...) PG 不支持建表语句里的 KEY idx_x (...)
唯一键 表内 CONSTRAINT uk_x UNIQUE (...) —
⭐ 命名唯一性 索引名与约束名必须在 schema 内全局唯一 ⚠️ 这是 MySQL 迁移最容易踩的坑:MySQL 的索引名只需表内唯一,PG 需全库唯一。所有索引与约束名必须加表名缩写前缀(如 idx_roster_scope 而不是 idx_scope)
无符号 PG 不支持 unsigned 已在转换中去除
存储子句 本 DDL 不含 WITH (...) 存储子句,保持可移植 需要时可自行追加

实测踩到的两个真实错误(已修正)

# 报错 原因 修法
1 relation "uk_tenant_code" already exists sys_tenant 与 collect_task 都用了这个约束名 collect_task 改为 uk_task_tenant_code
2 relation "idx_name" / "idx_tenant" / "idx_submission" already exists 索引名在多个表间重复(MySQL 允许,PG 不允许) 全部改为 idx_<表缩写>_<列> 形式(共 9 处)

💡 教训:DDL 的语法转换(类型、注释、索引)可以自动化,但命名唯一性这类"语义级差异"必须靠真跑一遍才能发现。仅靠人工审阅或正则替换一定会漏。

7.3.1 核心表 DDL(PostgreSQL)

7.3.1 租户

CREATE TABLE sys_tenant (
  id               bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
  tenant_code      varchar(32)  NOT NULL,
  tenant_name      varchar(128) NOT NULL,
  tenant_type      smallint      NOT NULL DEFAULT 1,
  contact_name     varchar(64),
  contact_phone    varchar(20),
  package_id       bigint,
  expire_time      timestamp,
  status           smallint      NOT NULL DEFAULT 1,
  logo_url         varchar(512),
  config_json      jsonb,
  create_by      bigint,
  create_time    timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  update_by      bigint,
  update_time    timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  deleted          smallint DEFAULT 0 NOT NULL,
  PRIMARY KEY (id),
  CONSTRAINT uk_tenant_code UNIQUE (tenant_code)
);
COMMENT ON TABLE sys_tenant IS '租户';
COMMENT ON COLUMN sys_tenant.id IS '租户ID';
COMMENT ON COLUMN sys_tenant.tenant_code IS '租户编码(短码,用于小程序扫码识别)';
COMMENT ON COLUMN sys_tenant.tenant_name IS '租户名称(学校/机构)';
COMMENT ON COLUMN sys_tenant.tenant_type IS '1学校 2认定机构 3报名点 4企业';
COMMENT ON COLUMN sys_tenant.contact_name IS '联系人';
COMMENT ON COLUMN sys_tenant.contact_phone IS '联系电话';
COMMENT ON COLUMN sys_tenant.package_id IS '套餐ID';
COMMENT ON COLUMN sys_tenant.expire_time IS '到期时间';
COMMENT ON COLUMN sys_tenant.status IS '0停用 1正常 2试用';
COMMENT ON COLUMN sys_tenant.logo_url IS 'Logo';
COMMENT ON COLUMN sys_tenant.config_json IS '租户配置(默认压缩参数/文件保留天数等)';

7.3.2 组织

CREATE TABLE sys_org (
  id               bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
  tenant_id        bigint NOT NULL,
  parent_id        bigint NOT NULL DEFAULT 0,
  org_name         varchar(128) NOT NULL,
  org_type         smallint NOT NULL DEFAULT 3,
  org_code         varchar(64),
  ancestor_path    varchar(512),
  leader_user_id   bigint,
  sort_no          integer NOT NULL DEFAULT 0,
  status           smallint NOT NULL DEFAULT 1,
  create_by      bigint,
  create_time    timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  update_by      bigint,
  update_time    timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  deleted          smallint DEFAULT 0 NOT NULL,
  PRIMARY KEY (id)
);
CREATE INDEX idx_tenant_parent ON sys_org (tenant_id, parent_id);
CREATE INDEX idx_tenant_name ON sys_org (tenant_id, org_name);
COMMENT ON TABLE sys_org IS '组织架构';
COMMENT ON COLUMN sys_org.parent_id IS '父级ID,0为根';
COMMENT ON COLUMN sys_org.org_name IS '组织名称,如 24中技数控1班';
COMMENT ON COLUMN sys_org.org_type IS '1学校 2院系/专业 3班级 4报名点';
COMMENT ON COLUMN sys_org.org_code IS '组织编码/班号';
COMMENT ON COLUMN sys_org.ancestor_path IS '祖先路径,如 0,1,5';
COMMENT ON COLUMN sys_org.leader_user_id IS '负责人(班主任)';

7.3.3 报考范围(⚠️ 按实际情况校正)

变更说明:上一版设计了 sys_eval_plan / sys_plan_occupation / sys_plan_import_log 三张表,前提是"认定机构会下发计划清单文件"。 该前提不成立——实测确认真实名单里「报考职业/工种/等级」三列已由老师填好(28/28)。 ⇒ 报考范围从名单聚合而来,只需在任务上存一份快照,三张表全部删除。

ALTER TABLE collect_task ADD COLUMN plan_scope_json jsonb;
ALTER TABLE collect_task ADD COLUMN plan_scope_confirmed smallint NOT NULL DEFAULT 0;
COMMENT ON COLUMN collect_task.plan_scope_json IS '本期报考范围快照(从名单聚合): {occupations:[{name,worktypes:[{name,levels:[]}]}], confirmed:true}';
COMMENT ON COLUMN collect_task.plan_scope_confirmed IS '管理员是否已确认范围(未确认则追加名单不告警)';

可选参考字典(P2,非强约束)

仅用于拼写校验与联想建议(例如"数控车床工"→提示"是否要填数控车工")。不作为报考范围的约束来源——约束来源是名单本身。

CREATE TABLE sys_occupation_ref (
  id               bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
  parent_id        bigint NOT NULL DEFAULT 0,
  name             varchar(128) NOT NULL,
  aliases          varchar(512),
  has_child        smallint NOT NULL DEFAULT 0,
  status           smallint NOT NULL DEFAULT 1,
  create_time      timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (id)
);
CREATE INDEX idx_parent ON sys_occupation_ref (parent_id);
CREATE INDEX idx_occref_name ON sys_occupation_ref (name);
COMMENT ON TABLE sys_occupation_ref IS '职业工种参考字典(仅拼写校验,非约束)';
COMMENT ON COLUMN sys_occupation_ref.parent_id IS '0=职业; 非0=该职业下的工种';
COMMENT ON COLUMN sys_occupation_ref.aliases IS '常见错写/别名,用于纠错提示';
COMMENT ON COLUMN sys_occupation_ref.has_child IS '该职业是否有工种';

名单行上需要存的预填值 —— 18 个正式列 + 1 个逃生舱

⚠️ 设计更正:上一版把 19 个字段统一塞进一个 prefilled_json,理由是"每个机构模板可能不同"。 实测两份真实文件后确认:官方模板结构稳定,两份文件的列完全一致(都是 A–U 共 21 列),且机构明写"请勿调整模板"。 ⇒ 这些是强结构、要查询、要统计的数据,必须建正式列。JSON 只留给"机构额外追加的字段"。

-- 同学号的补齐:官方模板 21 字段中的非身份字段全部建为正式列
-- 姓名、身份证号已有独立列(real_name / id_card_no),此处补齐其余 18 个
ALTER TABLE collect_roster
  ADD COLUMN occupation      varchar(64),    -- 报考职业   G
  ADD COLUMN worktype        varchar(64),    -- 报考工种   H
  ADD COLUMN job_level       varchar(32),    -- 报考等级   I
  ADD COLUMN education       varchar(32),    -- 学历       D
  ADD COLUMN is_student      smallint,       -- 是否在校生  N   0否 1是
  ADD COLUMN school          varchar(128),   -- 在读学校   O
  ADD COLUMN major           varchar(128),   -- 所学专业   P
  ADD COLUMN enroll_date     date,           -- 入学时间   Q   YYYY-MM-DD
  ADD COLUMN work_unit       varchar(128),   -- 工作单位   F   无则填"无"
  ADD COLUMN is_retake       smallint,       -- 是否补考   J   0否 1是
  ADD COLUMN retake_theory   varchar(64),    -- 补考理论   K
  ADD COLUMN retake_practice varchar(64),    -- 补考实操   L
  ADD COLUMN retake_overall  varchar(64),    -- 补考综合   M
  ADD COLUMN work_years      smallint,       -- 本职业工作年限 R
  ADD COLUMN cert_occupation varchar(128),   -- 已获证书职业工种 S
  ADD COLUMN cert_level      varchar(32),    -- 已获证书等级     T
  ADD COLUMN cert_no         varchar(64),    -- 已获证书编号     U
  ADD COLUMN extras_json     jsonb DEFAULT '{}'::jsonb;  -- 逃生舱:仅存租户/机构额外追加的字段

-- 报考范围聚合与按工种统计必需的索引
CREATE INDEX idx_roster_scope ON collect_roster (tenant_id, task_id, occupation, worktype, job_level);
CREATE INDEX idx_roster_org   ON collect_roster (task_id, org_id, deleted);

COMMENT ON COLUMN collect_roster.occupation IS '报考职业(锁定,由老师确定)';
COMMENT ON COLUMN collect_roster.worktype IS '报考工种(锁定;该职业无工种时填"无";专项职业能力为空)';
COMMENT ON COLUMN collect_roster.job_level IS '报考等级(锁定;专项职业能力为空)';
COMMENT ON COLUMN collect_roster.education IS '学历(下拉11项)';
COMMENT ON COLUMN collect_roster.is_student IS '是否在校生 0否 1是';
COMMENT ON COLUMN collect_roster.school IS '在读学校(在校生=是时必填)';
COMMENT ON COLUMN collect_roster.major IS '所学专业(在校生=是时必填)';
COMMENT ON COLUMN collect_roster.enroll_date IS '入学时间(在校生=是时必填,格式YYYY-MM-DD)';
COMMENT ON COLUMN collect_roster.work_unit IS '工作单位(没有填"无")';
COMMENT ON COLUMN collect_roster.is_retake IS '是否补考 0否 1是';
COMMENT ON COLUMN collect_roster.retake_theory IS '补考理论(级联)';
COMMENT ON COLUMN collect_roster.retake_practice IS '补考实操(级联)';
COMMENT ON COLUMN collect_roster.retake_overall IS '补考综合(级联)';
COMMENT ON COLUMN collect_roster.work_years IS '本职业工作年限(仅阿拉伯数字)';
COMMENT ON COLUMN collect_roster.cert_occupation IS '已获证书职业工种(选填)';
COMMENT ON COLUMN collect_roster.cert_level IS '已获证书等级(选填)';
COMMENT ON COLUMN collect_roster.cert_no IS '已获证书编号(选填)';
COMMENT ON COLUMN collect_roster.extras_json IS '租户/机构额外追加的字段(逃生舱,不参与核心统计)';

规则:名单有值的字段 → 学生端预填(🔒锁定项只读 / ✏️其余可改并留痕);名单无值的字段 → 学生必填。详见 §5.3.1。 collect_submission 同样需要这 18 列(存学生确认/修改后的最终值),以便与名单值做 diff 留痕。

7.3.4 用户

CREATE TABLE sys_user (
  id               bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
  tenant_id        bigint NOT NULL DEFAULT 0,
  org_id           bigint,
  username         varchar(64)  NOT NULL,
  password         varchar(128),
  real_name        varchar(64)  NOT NULL,
  phone            varchar(20),
  phone_hash       char(64),
  open_id          varchar(64),
  union_id         varchar(64),
  id_card_no       varchar(128),
  id_card_hash     char(64),
  avatar_url       varchar(512),
  user_type        smallint NOT NULL DEFAULT 1,
  status           smallint NOT NULL DEFAULT 1,
  last_login_time  timestamp,
  create_by      bigint,
  create_time    timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  update_by      bigint,
  update_time    timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  deleted          smallint DEFAULT 0 NOT NULL,
  PRIMARY KEY (id),
  CONSTRAINT uk_tenant_username UNIQUE (tenant_id, username)
);
CREATE INDEX idx_phone_hash ON sys_user (phone_hash);
CREATE INDEX idx_idcard_hash ON sys_user (id_card_hash);
CREATE INDEX idx_openid ON sys_user (open_id);
COMMENT ON TABLE sys_user IS '用户';
COMMENT ON COLUMN sys_user.tenant_id IS '0=平台运营';
COMMENT ON COLUMN sys_user.org_id IS '所属组织';
COMMENT ON COLUMN sys_user.username IS '登录名(教师)';
COMMENT ON COLUMN sys_user.password IS 'BCrypt';
COMMENT ON COLUMN sys_user.phone IS '手机号(AES加密存储)';
COMMENT ON COLUMN sys_user.phone_hash IS '手机号HMAC,用于唯一检索';
COMMENT ON COLUMN sys_user.open_id IS '微信openid';
COMMENT ON COLUMN sys_user.id_card_no IS '证件号(AES加密存储)';
COMMENT ON COLUMN sys_user.id_card_hash IS '证件号HMAC,用于比对与去重';
COMMENT ON COLUMN sys_user.user_type IS '1平台运营 2学校管理员 3班主任 4学生(口径见 03-接口设计 §2.7)';
COMMENT ON COLUMN sys_user.status IS '0停用 1正常 2待激活';

7.3.5 收集任务

CREATE TABLE collect_task (
  id               bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
  tenant_id        bigint NOT NULL,
  task_code        varchar(64)  NOT NULL,
  task_name        varchar(200) NOT NULL,
  collect_mode     smallint NOT NULL DEFAULT 1,
  batch_no         varchar(64),
  major_name       varchar(64),
  job_name         varchar(64),
  job_level        varchar(32),
  cert_type        smallint NOT NULL DEFAULT 1,
  start_time       timestamp,
  end_time         timestamp,
  audit_config     jsonb,
  naming_template  varchar(256) NOT NULL DEFAULT '{姓名}-{证件号}-{材料名}',
  compress_config  jsonb,
  roster_required  smallint NOT NULL DEFAULT 1,
  status           smallint NOT NULL DEFAULT 0,
  published_time   timestamp,
  create_by      bigint,
  create_time    timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  update_by      bigint,
  update_time    timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  deleted          smallint DEFAULT 0 NOT NULL,
  PRIMARY KEY (id),
  CONSTRAINT uk_task_tenant_code UNIQUE (tenant_id, task_code)
);
CREATE INDEX idx_tenant_status ON collect_task (tenant_id, status);
CREATE INDEX idx_end_time ON collect_task (end_time);
COMMENT ON TABLE collect_task IS '收集任务';
COMMENT ON COLUMN collect_task.task_code IS '任务编码,用于文件路径';
COMMENT ON COLUMN collect_task.task_name IS '如 2026年9月机电专业认定材料收集';
COMMENT ON COLUMN collect_task.collect_mode IS '1学校收集 2社会收集';
COMMENT ON COLUMN collect_task.batch_no IS '批次号';
COMMENT ON COLUMN collect_task.major_name IS '专业';
COMMENT ON COLUMN collect_task.job_name IS '报考工种';
COMMENT ON COLUMN collect_task.job_level IS '报考等级';
COMMENT ON COLUMN collect_task.cert_type IS '1自主认定 2社会评价 3专项职业能力';
COMMENT ON COLUMN collect_task.end_time IS '截止时间';
COMMENT ON COLUMN collect_task.audit_config IS '审核流配置,见6.5';
COMMENT ON COLUMN collect_task.compress_config IS '任务级压缩默认参数';
COMMENT ON COLUMN collect_task.roster_required IS '是否需要名册: 固定为1,两种收集模式都必填(名单门禁)';
COMMENT ON COLUMN collect_task.status IS '0草稿 1收集中 2已截止 3已归档 4暂停';

7.3.6 任务-组织关联(⚠️ 初稿遗漏)

初稿只让 collect_roster.org_id 承载班级归属,但任务创建时必须先选定"本次收集覆盖哪些班级",才能算出应填人数、下发通知、统计完成率。且社会收集模式下任务可挂"报名点/区域"而非班级。故必须独立成表。

CREATE TABLE collect_task_org (
  id               bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
  tenant_id        bigint NOT NULL,
  task_id          bigint NOT NULL,
  org_id           bigint NOT NULL,
  head_user_id     bigint,
  deputy_user_ids  varchar(256),
  expected_count   integer,
  status           smallint NOT NULL DEFAULT 1,
  create_time      timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (id),
  CONSTRAINT uk_task_org UNIQUE (task_id, org_id)
);
CREATE INDEX idx_tenant_head ON collect_task_org (tenant_id, head_user_id);
COMMENT ON TABLE collect_task_org IS '任务覆盖组织与审核人绑定';
COMMENT ON COLUMN collect_task_org.head_user_id IS '该班初审人(班主任),为空则取 sys_org.leader_user_id';
COMMENT ON COLUMN collect_task_org.deputy_user_ids IS '副班主任/协办人,逗号分隔';
COMMENT ON COLUMN collect_task_org.expected_count IS '应填人数(期望值,管理员填写,可空);仅在与名册实际人数不一致时告警。NULL 表示未设置,与 0(预期 0 人)含义不同';
COMMENT ON COLUMN collect_task_org.status IS '0已移除 1生效';

7.3.7 表单定义

CREATE TABLE collect_form_schema (
  id               bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
  tenant_id        bigint NOT NULL,
  task_id          bigint NOT NULL,
  version          integer NOT NULL DEFAULT 1,
  schema_json      jsonb NOT NULL,
  is_current       smallint NOT NULL DEFAULT 1,
  create_by        bigint, create_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (id)
);
CREATE INDEX idx_task_version ON collect_form_schema (task_id, version);
COMMENT ON TABLE collect_form_schema IS '表单定义(版本化)';
COMMENT ON COLUMN collect_form_schema.version IS '版本号,已发布任务修改则+1';
COMMENT ON COLUMN collect_form_schema.schema_json IS '字段定义数组';

schema_json 示例

[
  {"key":"name","label":"姓名","type":"text","required":true,"maxLen":20,
   "source":"ROSTER","readonlyOnMatched":true},
  {"key":"idCard","label":"身份证号","type":"idcard","required":true,
   "validate":["format","checksum","rosterMatch"],"ocrFill":true},
  {"key":"phone","label":"联系电话","type":"phone","required":true,"smsVerify":false},
  {"key":"education","label":"学历","type":"select","required":true,"dictCode":"education"},
  {"key":"isStudent","label":"是否在校生","type":"radio","required":true,
   "options":[{"label":"是","value":"Y"},{"label":"否","value":"N"}]},
  {"key":"school","label":"在读学校","type":"text","required":true,
   "visibleWhen":"isStudent == 'Y'"},
  {"key":"xxwba","label":"学信网备案表","type":"file","materialCode":"XXWBA",
   "visibleWhen":"isStudent == 'N'"}
]

7.3.8 材料项定义

CREATE TABLE collect_material_def (
  id               bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
  tenant_id        bigint NOT NULL,
  task_id          bigint NOT NULL,
  material_code    varchar(32) NOT NULL,
  material_name    varchar(64) NOT NULL,
  aliases          varchar(512),
  name_pattern     varchar(256),
  required         smallint NOT NULL DEFAULT 1,
  visible_when     varchar(256),
  file_types       varchar(128) NOT NULL DEFAULT 'jpg,jpeg,png',
  max_size_kb      integer NOT NULL DEFAULT 1024,
  compress_profile varchar(8) NOT NULL DEFAULT 'AUTO',
  min_long_side    integer NOT NULL DEFAULT 1400,
  min_quality      smallint NOT NULL DEFAULT 70,
  target_dpi       integer,
  fixed_size       varchar(32),
  output_format    varchar(16) NOT NULL DEFAULT 'jpg',
  max_count        smallint NOT NULL DEFAULT 1,
  merge_enabled    smallint NOT NULL DEFAULT 0,
  ocr_type         varchar(32),
  sample_url       varchar(512),
  tip_text         varchar(512),
  sort_no          integer NOT NULL DEFAULT 0,
  create_by      bigint,
  create_time    timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  update_by      bigint,
  update_time    timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  deleted          smallint DEFAULT 0 NOT NULL,
  PRIMARY KEY (id),
  CONSTRAINT uk_task_code UNIQUE (task_id, material_code)
);
CREATE INDEX idx_matdef_tenant ON collect_material_def (tenant_id);
COMMENT ON TABLE collect_material_def IS '材料项定义';
COMMENT ON COLUMN collect_material_def.material_code IS '如 IDCARD/PHOTO1/SCHOOL_ROLL/WORK_PROOF';
COMMENT ON COLUMN collect_material_def.material_name IS '规范材料名,如 身份证';
COMMENT ON COLUMN collect_material_def.aliases IS '别名,逗号分隔,用于历史文件名识别';
COMMENT ON COLUMN collect_material_def.name_pattern IS '命名模板,空则继承任务级';
COMMENT ON COLUMN collect_material_def.visible_when IS '条件显示表达式';
COMMENT ON COLUMN collect_material_def.file_types IS '格式白名单';
COMMENT ON COLUMN collect_material_def.max_size_kb IS '体积上限KB';
COMMENT ON COLUMN collect_material_def.compress_profile IS '压缩档位: AUTO自动判定/P1证件照/P2证件翻拍/P3扫描件/P4截图/P5文档, 见6.3';
COMMENT ON COLUMN collect_material_def.min_long_side IS '分辨率下限(仅P2证件翻拍档生效)';
COMMENT ON COLUMN collect_material_def.min_quality IS '质量下限,低于65视为不可接受';
COMMENT ON COLUMN collect_material_def.target_dpi IS '目标DPI(仅P3扫描件档生效,建议150-300)';
COMMENT ON COLUMN collect_material_def.fixed_size IS '固定像素尺寸(仅P1证件照档,如295x413)';
COMMENT ON COLUMN collect_material_def.output_format IS 'P4截图档固定输出png';
COMMENT ON COLUMN collect_material_def.max_count IS '允许上传份数,身份证正反面可2';
COMMENT ON COLUMN collect_material_def.merge_enabled IS '多份是否合并为一张';
COMMENT ON COLUMN collect_material_def.ocr_type IS 'IDCARD/GENERAL/NONE';
COMMENT ON COLUMN collect_material_def.sample_url IS '示例图';
COMMENT ON COLUMN collect_material_def.tip_text IS '填报提示语';

7.3.9 名册

CREATE TABLE collect_roster (
  id               bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
  tenant_id        bigint NOT NULL,
  task_id          bigint NOT NULL,
  org_id           bigint,
  seq_no           integer,
  student_no       varchar(64),
  real_name        varchar(64) NOT NULL,
  name_pinyin      varchar(128),
  id_card_no       varchar(128) NOT NULL,
  id_card_hash     char(64) NOT NULL,
  id_card_mask     varchar(32),
  phone            varchar(20),
  phone_hash       char(64),
  ext_json         jsonb,
  bind_user_id     bigint,
  bind_time        timestamp,
  bind_type        smallint,
  submit_status    smallint NOT NULL DEFAULT 0,
  create_by      bigint,
  create_time    timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  update_by      bigint,
  update_time    timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  deleted          smallint DEFAULT 0 NOT NULL,
  PRIMARY KEY (id),
  CONSTRAINT uk_task_idcard UNIQUE (task_id, id_card_hash)
);
CREATE INDEX idx_task_org ON collect_roster (task_id, org_id);
CREATE INDEX idx_bind_user ON collect_roster (bind_user_id);
CREATE INDEX idx_roster_name ON collect_roster (task_id, real_name);
COMMENT ON TABLE collect_roster IS '名册';
COMMENT ON COLUMN collect_roster.org_id IS '班级';
COMMENT ON COLUMN collect_roster.seq_no IS '序号(来自Excel)';
COMMENT ON COLUMN collect_roster.student_no IS '学号(⚠️初稿遗漏,名册唯一性依据之一)';
COMMENT ON COLUMN collect_roster.name_pinyin IS '拼音,用于近音纠错';
COMMENT ON COLUMN collect_roster.id_card_no IS 'AES加密';
COMMENT ON COLUMN collect_roster.id_card_hash IS 'HMAC,唯一性判断';
COMMENT ON COLUMN collect_roster.id_card_mask IS '脱敏展示 3601**********1234';
COMMENT ON COLUMN collect_roster.ext_json IS '名册其余字段(学历/工作单位/报考职业等)';
COMMENT ON COLUMN collect_roster.bind_user_id IS '绑定的填报人';
COMMENT ON COLUMN collect_roster.bind_type IS '1门禁核验匹配 2管理员指定';
COMMENT ON COLUMN collect_roster.submit_status IS '0未提交 1草稿 2待审 3已通过 4已驳回';

7.3.10 填报单

CREATE TABLE collect_submission (
  id               bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
  tenant_id        bigint NOT NULL,
  task_id          bigint NOT NULL,
  roster_id        bigint,
  org_id           bigint,
  submitter_id     bigint NOT NULL,
  form_version     integer NOT NULL DEFAULT 1,
  form_data        jsonb,
  -- ⚠️ 与 collect_roster 对称的 18 个业务列:存学生「确认/修改后」的最终值
  -- 需要与名单值做 diff(留痕"学生改了哪些字段"),也是统计"实际报考各工种人数"的依据
  occupation       varchar(64),
  worktype         varchar(64),
  job_level        varchar(32),
  education        varchar(32),
  is_student       smallint,
  school           varchar(128),
  major            varchar(128),
  enroll_date      date,
  work_unit        varchar(128),
  is_retake        smallint,
  retake_theory    varchar(64),
  retake_practice  varchar(64),
  retake_overall   varchar(64),
  work_years       smallint,
  cert_occupation  varchar(128),
  cert_level       varchar(32),
  cert_no          varchar(64),
  extras_json      jsonb DEFAULT '{}'::jsonb,  -- 逃生舱
  changed_fields   jsonb,                -- 学生相对名单修改过的字段(留痕)
  status           smallint NOT NULL DEFAULT 0,
  reject_count     integer NOT NULL DEFAULT 0,
  last_reject_reason varchar(512),
  red_count        integer NOT NULL DEFAULT 0,
  yellow_count     integer NOT NULL DEFAULT 0,
  submit_time      timestamp,
  class_audit_by   bigint, class_audit_time timestamp,
  admin_audit_by   bigint, admin_audit_time timestamp,
  create_by      bigint,
  create_time    timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  update_by      bigint,
  update_time    timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  deleted          smallint DEFAULT 0 NOT NULL,
  PRIMARY KEY (id),
  CONSTRAINT uk_task_submitter UNIQUE (task_id, submitter_id)
);
CREATE INDEX idx_task_status ON collect_submission (task_id, status, org_id);
CREATE INDEX idx_submitter ON collect_submission (submitter_id);
COMMENT ON TABLE collect_submission IS '填报单';
COMMENT ON COLUMN collect_submission.roster_id IS '社会收集可为空';
COMMENT ON COLUMN collect_submission.submitter_id IS '填报人user_id';
COMMENT ON COLUMN collect_submission.form_data IS '表单数据快照(扩展字段;核心18列见上方独立列)';
COMMENT ON COLUMN collect_submission.occupation IS '最终报考职业(学生确认后,应与名单一致)';
COMMENT ON COLUMN collect_submission.worktype IS '最终报考工种';
COMMENT ON COLUMN collect_submission.job_level IS '最终报考等级';
COMMENT ON COLUMN collect_submission.is_student IS '是否在校生 0否 1是';
COMMENT ON COLUMN collect_submission.work_unit IS '工作单位(没有填"无")';
COMMENT ON COLUMN collect_submission.is_retake IS '是否补考 0否 1是';
COMMENT ON COLUMN collect_submission.extras_json IS '租户/机构额外追加字段(逃生舱)';
COMMENT ON COLUMN collect_submission.changed_fields IS '学生相对名单修改过的字段(留痕: {字段:{old,new,time}})';
COMMENT ON COLUMN collect_submission.status IS '0草稿 1待初审 2待终审 3已通过 4已驳回 5已归档';
COMMENT ON COLUMN collect_submission.red_count IS '红色问题数';
COMMENT ON COLUMN collect_submission.yellow_count IS '黄色问题数';

7.3.11 文件实体

CREATE TABLE collect_file (
  id               bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
  tenant_id        bigint NOT NULL,
  task_id          bigint NOT NULL,
  submission_id    bigint NOT NULL,
  roster_id        bigint,
  material_code    varchar(32) NOT NULL,
  version          integer NOT NULL DEFAULT 1,
  is_current       smallint NOT NULL DEFAULT 1,
  original_name    varchar(512) NOT NULL,
  standard_name    varchar(512),
  file_ext         varchar(16)  NOT NULL,
  mime_type        varchar(64),
  raw_size         bigint NOT NULL,
  final_size       bigint NOT NULL,
  file_hash        char(64) NOT NULL,
  raw_path         varchar(512),
  std_path         varchar(512),
  raw_expire_time  timestamp,
  width            integer, height integer, quality smallint,
  dpi              integer,
  ocr_status       smallint NOT NULL DEFAULT 0,
  ocr_result       jsonb,
  ocr_confidence   numeric(4,3),
  detected_material_code varchar(32),
  detect_confidence numeric(4,3),
  detect_source    varchar(32),
  detect_conflict  smallint NOT NULL DEFAULT 0,
  need_manual_place smallint NOT NULL DEFAULT 0,
  vlm_called       smallint NOT NULL DEFAULT 0,
  validate_status  smallint NOT NULL DEFAULT 0,
  source           smallint NOT NULL DEFAULT 1,
  upload_ip        varchar(64),
  create_by      bigint,
  create_time    timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  update_by      bigint,
  update_time    timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  deleted          smallint DEFAULT 0 NOT NULL,
  PRIMARY KEY (id)
);
CREATE INDEX idx_file_submission ON collect_file (submission_id, material_code, is_current);
CREATE INDEX idx_task_material ON collect_file (task_id, material_code);
CREATE INDEX idx_hash ON collect_file (file_hash);
CREATE INDEX idx_raw_expire ON collect_file (raw_expire_time);
COMMENT ON TABLE collect_file IS '文件实体';
COMMENT ON COLUMN collect_file.is_current IS '是否最新有效版本';
COMMENT ON COLUMN collect_file.original_name IS '原始文件名';
COMMENT ON COLUMN collect_file.standard_name IS '规范文件名';
COMMENT ON COLUMN collect_file.raw_size IS '原始字节';
COMMENT ON COLUMN collect_file.final_size IS '处理后字节';
COMMENT ON COLUMN collect_file.file_hash IS 'SHA-256,用于查重';
COMMENT ON COLUMN collect_file.raw_path IS '对象存储key-原始';
COMMENT ON COLUMN collect_file.std_path IS '对象存储key-规范';
COMMENT ON COLUMN collect_file.raw_expire_time IS '原始文件清理时间';
COMMENT ON COLUMN collect_file.ocr_status IS '0未识别 1成功 2失败';
COMMENT ON COLUMN collect_file.ocr_result IS 'OCR结构化结果(不存明文证件号,存hash)';
COMMENT ON COLUMN collect_file.detected_material_code IS '材料识别结果(材料类型编码),见功能逻辑卷3.5-B';
COMMENT ON COLUMN collect_file.detect_confidence IS '材料识别置信度';
COMMENT ON COLUMN collect_file.detect_source IS '识别来源: FILENAME/形态KEYWORD/POSITION/VLM/MIXED';
COMMENT ON COLUMN collect_file.detect_conflict IS '是否与槽位冲突(1=冲突)';
COMMENT ON COLUMN collect_file.need_manual_place IS '是否需要人工归属(散图分拣用)';
COMMENT ON COLUMN collect_file.vlm_called IS '是否触发了VLM兜底(用于成本核算)';
COMMENT ON COLUMN collect_file.validate_status IS '0未校验 1通过 2警告 3错误';
COMMENT ON COLUMN collect_file.source IS '1本人上传 2班主任代传 3管理员代传 4整理器导入';

7.3.12 校验问题

CREATE TABLE collect_validation_issue (
  id               bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
  tenant_id        bigint NOT NULL,
  task_id          bigint NOT NULL,
  submission_id    bigint NOT NULL,
  file_id          bigint,
  rule_code        varchar(32) NOT NULL,
  severity         smallint NOT NULL,
  field_key        varchar(64),
  title            varchar(128) NOT NULL,
  detail           varchar(512),
  expected_val     varchar(256),
  actual_val       varchar(256),
  auto_fixed       smallint NOT NULL DEFAULT 0,
  resolved         smallint NOT NULL DEFAULT 0,
  resolved_by      bigint, resolved_time timestamp,
  create_time      timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (id)
);
CREATE INDEX idx_issue_submission ON collect_validation_issue (submission_id, resolved);
CREATE INDEX idx_task_severity ON collect_validation_issue (task_id, severity, resolved);
COMMENT ON TABLE collect_validation_issue IS '校验问题';
COMMENT ON COLUMN collect_validation_issue.file_id IS '可为空(表单级问题)';
COMMENT ON COLUMN collect_validation_issue.rule_code IS 'VAL-01..VAL-12';
COMMENT ON COLUMN collect_validation_issue.severity IS '1信息 2警告 3错误';
COMMENT ON COLUMN collect_validation_issue.expected_val IS '期望值(如名册中的证件号)';
COMMENT ON COLUMN collect_validation_issue.actual_val IS '实际值';
COMMENT ON COLUMN collect_validation_issue.auto_fixed IS '是否已自动修复';

7.3.13 审核记录

CREATE TABLE collect_audit_record (
  id               bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
  tenant_id        bigint NOT NULL,
  task_id          bigint NOT NULL,
  submission_id    bigint NOT NULL,
  audit_node       varchar(32) NOT NULL,
  auditor_id       bigint NOT NULL,
  action           smallint NOT NULL,
  reason_code      varchar(32),
  reason_text      varchar(512),
  material_codes   varchar(256),
  duration_sec     integer,
  create_time      timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (id)
);
CREATE INDEX idx_audit_submission ON collect_audit_record (submission_id, create_time);
CREATE INDEX idx_task_node ON collect_audit_record (task_id, audit_node, create_time);
COMMENT ON TABLE collect_audit_record IS '审核记录';
COMMENT ON COLUMN collect_audit_record.audit_node IS 'CLASS_AUDIT/ADMIN_AUDIT';
COMMENT ON COLUMN collect_audit_record.action IS '1通过 2驳回 3批量通过 4批量驳回 5转交 6代传';
COMMENT ON COLUMN collect_audit_record.reason_code IS '驳回原因编码';
COMMENT ON COLUMN collect_audit_record.material_codes IS '被驳回的材料项';
COMMENT ON COLUMN collect_audit_record.duration_sec IS '处理耗时';

7.3.14 导出任务

CREATE TABLE collect_export_task (
  id               bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
  tenant_id        bigint NOT NULL,
  task_id          bigint NOT NULL,
  export_name      varchar(256) NOT NULL,
  scope_json       jsonb,
  split_size_mb    integer NOT NULL DEFAULT 0,
  desensitize      smallint NOT NULL DEFAULT 0,
  status           smallint NOT NULL DEFAULT 0,
  progress         smallint NOT NULL DEFAULT 0,
  file_count       integer, total_size bigint,
  result_files     jsonb,
  error_msg        varchar(512),
  expire_time      timestamp,
  create_by        bigint, create_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  finish_time      timestamp,
  PRIMARY KEY (id)
);
CREATE INDEX idx_task ON collect_export_task (task_id, create_time);
CREATE INDEX idx_status ON collect_export_task (status);
COMMENT ON TABLE collect_export_task IS '导出任务';
COMMENT ON COLUMN collect_export_task.scope_json IS '范围:orgIds/userIds/仅通过终审/是否含原图';
COMMENT ON COLUMN collect_export_task.split_size_mb IS '0不分卷,>0按MB分卷';
COMMENT ON COLUMN collect_export_task.desensitize IS '是否脱敏';
COMMENT ON COLUMN collect_export_task.status IS '0排队 1处理中 2完成 3失败';
COMMENT ON COLUMN collect_export_task.progress IS '0-100';
COMMENT ON COLUMN collect_export_task.result_files IS '生成的包列表[{name,path,size,part}]';
COMMENT ON COLUMN collect_export_task.expire_time IS '下载有效期';

7.3.15 文件处理日志

CREATE TABLE collect_file_process_log (
  id               bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
  tenant_id        bigint NOT NULL,
  file_id          bigint NOT NULL,
  step             varchar(32) NOT NULL,
  before_val       varchar(512),
  after_val        varchar(512),
  extra_json       jsonb,
  cost_ms          integer,
  success          smallint NOT NULL DEFAULT 1,
  error_msg        varchar(512),
  create_time      timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (id)
);
CREATE INDEX idx_file ON collect_file_process_log (file_id, step);
COMMENT ON TABLE collect_file_process_log IS '文件处理日志';
COMMENT ON COLUMN collect_file_process_log.step IS 'NORMALIZE_NAME/COMPRESS/CONVERT/OCR/EXIF_FIX';
COMMENT ON COLUMN collect_file_process_log.extra_json IS '如 {beforeSize,afterSize,attempts,quality,finalLongSide}';

7.3.16 通知记录与订阅额度

CREATE TABLE collect_notify_record (
  id            bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
  tenant_id     bigint NOT NULL,
  task_id       bigint,
  channel       varchar(16) NOT NULL,          -- WECHAT_SUBSCRIBE / WECHAT_MP / SMS / INBOX
  biz_type      varchar(32) NOT NULL,          -- TASK_PUBLISH / DEADLINE_REMIND / REJECT / APPROVE / EXPORT_DONE / AUDIT_TIMEOUT
  receiver_id   bigint NOT NULL,               -- 接收人 sys_user.id
  receiver_role varchar(16),                   -- 接收人角色(便于按角色统计)
  template_code varchar(64),
  content       varchar(1024),
  status        smallint NOT NULL DEFAULT 0,   -- 0待发 1成功 2失败 3额度不足跳过
  fail_reason   varchar(512),
  send_time     timestamp,
  create_time   timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (id)
);
CREATE INDEX idx_notify_task ON collect_notify_record (task_id, biz_type, create_time);
CREATE INDEX idx_notify_recv ON collect_notify_record (receiver_id, create_time);
COMMENT ON TABLE collect_notify_record IS '通知记录';
COMMENT ON COLUMN collect_notify_record.channel IS '渠道: 微信订阅消息/服务号/短信/站内';
COMMENT ON COLUMN collect_notify_record.biz_type IS '业务类型';
COMMENT ON COLUMN collect_notify_record.status IS '0待发 1成功 2失败 3额度不足跳过';
COMMENT ON COLUMN collect_notify_record.fail_reason IS '失败原因;额度不足时记录(应对R12订阅消息次数限制)';

-- 订阅额度余额(微信一次性订阅消息,发一条消耗一条)
CREATE TABLE collect_subscribe_quota (
  id           bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
  tenant_id    bigint NOT NULL,
  user_id      bigint NOT NULL,
  template_code varchar(64) NOT NULL,
  total        integer NOT NULL DEFAULT 0,      -- 累计授权次数
  used         integer NOT NULL DEFAULT 0,      -- 已消耗次数
  update_time  timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (id),
  CONSTRAINT uk_quota_user_tpl UNIQUE (user_id, template_code)
);
COMMENT ON TABLE collect_subscribe_quota IS '微信订阅消息额度余额';
COMMENT ON COLUMN collect_subscribe_quota.total IS '累计授权次数';
COMMENT ON COLUMN collect_subscribe_quota.used IS '已消耗次数;余额=total-used,不足时自动降级到服务号或短信';

7.3.17 规则模板

CREATE TABLE collect_rule_template (
  id               bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
  tenant_id        bigint NOT NULL,
  template_name    varchar(128) NOT NULL,
  cert_type        smallint,
  form_schema      jsonb,
  materials        jsonb,
  naming_template  varchar(256),
  is_public        smallint NOT NULL DEFAULT 0,
  use_count        integer NOT NULL DEFAULT 0,
  create_by      bigint,
  create_time    timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  update_by      bigint,
  update_time    timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  deleted          smallint DEFAULT 0 NOT NULL,
  PRIMARY KEY (id)
);
CREATE INDEX idx_ruletpl_tenant ON collect_rule_template (tenant_id, is_public);
COMMENT ON TABLE collect_rule_template IS '规则模板';
COMMENT ON COLUMN collect_rule_template.tenant_id IS '0=平台公共模板';
COMMENT ON COLUMN collect_rule_template.template_name IS '如 自主认定标准模板';
COMMENT ON COLUMN collect_rule_template.cert_type IS '1自主认定 2社会评价 3专项职业能力';
COMMENT ON COLUMN collect_rule_template.form_schema IS '表单字段模板';
COMMENT ON COLUMN collect_rule_template.materials IS '材料清单模板';

7.3.18 历史数据整理任务

CREATE TABLE collect_cleanup_job (
  id               bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
  tenant_id        bigint NOT NULL,
  job_name         varchar(128) NOT NULL,
  source_type      smallint NOT NULL DEFAULT 1,
  source_path      varchar(512) NOT NULL,
  target_task_id   bigint,
  naming_template  varchar(256) NOT NULL DEFAULT '{姓名}-{证件号}-{材料名}',
  compress_config  jsonb,
  status           smallint NOT NULL DEFAULT 0,
  progress         smallint NOT NULL DEFAULT 0,
  report_json      jsonb,
  total_files      integer, fixed_files integer, failed_files integer,
  result_path      varchar(512),
  create_by        bigint, create_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  finish_time      timestamp,
  PRIMARY KEY (id)
);
CREATE INDEX idx_cleanup_tenant ON collect_cleanup_job (tenant_id, create_time);
COMMENT ON TABLE collect_cleanup_job IS '历史数据整理任务';
COMMENT ON COLUMN collect_cleanup_job.source_type IS '1上传压缩包 2对象存储目录';
COMMENT ON COLUMN collect_cleanup_job.target_task_id IS '结果可导入某任务';
COMMENT ON COLUMN collect_cleanup_job.status IS '0排队 1解析中 2处理中 3完成 4失败';
COMMENT ON COLUMN collect_cleanup_job.report_json IS '体检报告:命名违规/证件号异常/体积超标/缺失/重复';

7.3.19 名单加入申请(⚠️ 客户确认后新增)

对应 FR-509 系列。解决"名单外的人只能找老师手工加人"的运营痛点。

CREATE TABLE collect_join_request (
  id                bigint GENERATED BY DEFAULT AS IDENTITY NOT NULL,
  tenant_id         bigint NOT NULL,
  task_id           bigint NOT NULL,
  -- 申请人自述(证件号与手机号不存明文,与门禁一致)
  real_name         varchar(64) NOT NULL,
  id_card_hash      char(64) NOT NULL,
  id_card_mask      varchar(32) NOT NULL,
  phone_hash        char(64),
  phone_mask        varchar(32),
  org_hint          varchar(128),
  reason            varchar(500),
  -- 审批
  status            smallint NOT NULL DEFAULT 0,
  reject_reason     varchar(500),
  roster_id         bigint,
  audit_by          bigint,
  audit_time        timestamp,
  -- 申请人标识(用于限流与自动绑定)
  applicant_openid  varchar(64) NOT NULL,
  applicant_uid     bigint,
  source            smallint NOT NULL DEFAULT 1,
  create_by         bigint,
  create_time       timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  update_by         bigint,
  update_time       timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  deleted           smallint DEFAULT 0 NOT NULL,
  PRIMARY KEY (id)
);
-- 同证件号每任务仅保留 1 条"待审或已通过"申请;被驳回后可重新申请
CREATE UNIQUE INDEX uk_join_task_idcard ON collect_join_request (task_id, id_card_hash)
  WHERE deleted = 0 AND status IN (0, 1);
CREATE INDEX idx_join_task_status ON collect_join_request (task_id, status, create_time);
CREATE INDEX idx_join_openid ON collect_join_request (applicant_openid);
COMMENT ON TABLE collect_join_request IS '名单加入申请';
COMMENT ON COLUMN collect_join_request.org_hint IS '申请人自述班级(选填,可由管理员修正)';
COMMENT ON COLUMN collect_join_request.status IS '0待审 1已通过 2已驳回 3已失效(任务截止)';
COMMENT ON COLUMN collect_join_request.roster_id IS '审批通过后生成的名册项';
COMMENT ON COLUMN collect_join_request.applicant_openid IS '用于限流(每任务<=2次)与审批通过后自动绑定';
COMMENT ON COLUMN collect_join_request.source IS '1门禁失败页申请 2管理员代录';

7.4 对象存储路径规范

# 原始文件(保留 30 天,可配)
raw/{tenantId}/{taskId}/{yyyyMM}/{uuid}.{ext}

# 规范文件(长期保留)
std/{tenantId}/{taskId}/{orgId}/{materialCode}/{规范文件名}

# 示例
std/1001/2026090001/2001/IDCARD/张三-360121200010101234-身份证.jpg

# 导出包(保留 7 天)
export/{tenantId}/{taskId}/{exportId}/{批次号}-{专业}-{班级}.zip

说明

  • 对象存储私有读,访问走签名 URL(有效期 5 分钟);
  • 上传走 前端直传 OSS(服务端签发 STS 临时凭证),避免应用服务器带宽瓶颈;
  • 原始文件到期由定时任务清理(collect_file.raw_expire_time),仅保留规范文件;
  • 规范文件名作为 OSS Key 的一部分需 URL 编码,同时单独存于 collect_file.standard_name。

7.5 索引与性能要点

场景 索引
待办列表(班主任) collect_submission(task_id, status, org_id)
名册匹配 collect_roster(task_id, id_card_hash) 唯一索引
文件查重 collect_file(file_hash)
异常清单 collect_validation_issue(task_id, severity, resolved)
原始文件清理 collect_file(raw_expire_time)
证件号检索 sys_user.id_card_hash(HMAC 而非明文,避免全表扫描)

8. 接口清单

8.1 通用约定

项 约定
风格 RESTful,/api/v1/**
鉴权 JWT(PC 端 Header Authorization: Bearer;小程序端同)
租户透传 JWT 中携带 tenantId,服务端强制校验,防越租户
统一响应 { "code": 0, "msg": "success", "data": {...} };业务错误码:4xxxx 客户端 / 5xxxx 服务端
分页 ?pageNum=1&pageSize=20,响应 {list,total,pageNum,pageSize}
幂等 提交、上传、导出接口需 Idempotency-Key
限流 登录 5 次/分钟;上传 200 次/分钟/用户;OCR 30 次/分钟/用户
加签 开放接口 appKey + timestamp + nonce + HMAC-SHA256
时间 ISO 8601,服务端统一 UTC+8

8.2 认证与账号

方法 路径 说明 权限
POST /api/v1/auth/login PC 账号密码登录 公开
POST /api/v1/auth/logout 退出 登录用户
GET /api/v1/auth/me 当前用户信息 + 权限菜单 登录用户
PUT /api/v1/auth/password 修改密码 登录用户
POST /api/v1/mp/login 小程序 code 换 session 公开
POST /api/v1/mp/bind-phone 微信手机号快捷绑定 登录用户
POST /api/v1/mp/roster/match 门禁核验:提交姓名 + 身份证号匹配名单(限流、统一失败提示) 小程序登录
GET /api/v1/users 用户列表 管理员
POST /api/v1/users 新增用户 管理员
PUT /api/v1/users/{id} 修改用户 管理员
DELETE /api/v1/users/{id} 停用用户 管理员
POST /api/v1/users/import 批量导入(Excel) 管理员
POST /api/v1/users/{id}/reset-password 重置密码 管理员

8.3 组织与租户

方法 路径 说明 权限
GET /api/v1/orgs/tree 组织树 管理员
POST /api/v1/orgs 新增组织 管理员
PUT /api/v1/orgs/{id} 修改 管理员
DELETE /api/v1/orgs/{id} 删除 管理员
POST /api/v1/orgs/import 批量导入组织 管理员
GET /api/v1/tenants 租户列表 平台运营
POST /api/v1/tenants 开通租户 平台运营
PUT /api/v1/tenants/{id} 修改租户/套餐 平台运营
GET /api/v1/tenants/{id}/usage 用量统计 平台运营

8.4 任务与配置

方法 路径 说明 权限
GET /api/v1/tasks 任务列表(按角色过滤) 登录用户
POST /api/v1/tasks 创建任务 管理员
GET /api/v1/tasks/{id} 任务详情 管理员/班主任
PUT /api/v1/tasks/{id} 修改任务 管理员
DELETE /api/v1/tasks/{id} 删除(草稿) 管理员
POST /api/v1/tasks/{id}/publish 发布任务 管理员
POST /api/v1/tasks/{id}/close 截止任务 管理员
POST /api/v1/tasks/{id}/clone 克隆任务 管理员
POST /api/v1/tasks/{id}/archive 归档任务(⚠️ 复审补充) 管理员
GET /api/v1/tasks/{id}/orgs 查询任务覆盖班级及班主任(⚠️ 复审补充) 管理员
PUT /api/v1/tasks/{id}/orgs 设置任务覆盖班级、指定/调整班主任(⚠️ 复审补充) 管理员
GET /api/v1/tasks/{id}/form-schema 获取表单定义 管理员
PUT /api/v1/tasks/{id}/form-schema 保存表单定义(版本化) 管理员
GET /api/v1/tasks/{id}/materials 材料项列表 管理员/班主任
POST /api/v1/tasks/{id}/materials 新增材料项 管理员
PUT /api/v1/materials/{id} 修改材料项 管理员
DELETE /api/v1/materials/{id} 删除材料项 管理员
POST /api/v1/tasks/{id}/naming-rules/preview 命名规则预览:传样本文件名,返回规范化结果与问题 管理员
GET /api/v1/tasks/{id}/statistics 任务统计 管理员/班主任
GET /api/v1/tasks/{id}/qrcode 获取填报二维码 管理员
GET /api/v1/rule-templates 规则模板列表 管理员
POST /api/v1/rule-templates/{id}/apply 应用模板到任务 管理员

8.5 名册

方法 路径 说明 权限
POST /api/v1/tasks/{id}/roster/import 导入名册(返回体检报告) 管理员/班主任
GET /api/v1/tasks/{id}/roster 名册列表/分页 管理员/班主任
PUT /api/v1/roster/{rid} 修改名册项 管理员
DELETE /api/v1/roster/{rid} 删除名册项 管理员
GET /api/v1/tasks/{id}/roster/template 下载导入模板 管理员
POST /api/v1/roster/{rid}/notify 定向催报 管理员/班主任
POST /api/v1/tasks/{id}/roster/notify-unsubmitted 一键催报未提交者 管理员/班主任

8.6 填报(小程序端)

方法 路径 说明 权限
GET /api/v1/mp/tasks 我的任务列表(含进度) 填报人
GET /api/v1/mp/tasks/{id}/form 获取表单结构 + 已填数据 填报人
POST /api/v1/mp/submissions 创建/获取草稿 填报人
PUT /api/v1/mp/submissions/{id} 保存草稿(自动保存) 填报人
POST /api/v1/mp/submissions/{id}/submit 提交 填报人
POST /api/v1/mp/submissions/{id}/withdraw 撤回(未审核前) 填报人
GET /api/v1/mp/submissions/{id}/progress 填报进度与问题清单 填报人
POST /api/v1/mp/files/upload-token 获取 OSS 直传凭证(含路径规则) 填报人
POST /api/v1/mp/files/{fileId}/process 触发规范化:改名+压缩+格式转换+校验(详见 8.9) 填报人
GET /api/v1/mp/files/{fileId} 获取预览签名 URL 填报人
DELETE /api/v1/mp/files/{fileId} 删除材料(软删,保留版本) 填报人
POST /api/v1/mp/ocr/idcard 身份证 OCR 识别 填报人
POST /api/v1/mp/files/batch-process 批量处理(多材料一次提交) 填报人

8.6b 班主任移动端(⚠️ 复审补充)

方法 路径 说明 权限
GET /api/v1/mp/teacher/dashboard 班级看板(完成率、未提交、异常数) 班主任
GET /api/v1/mp/teacher/audit/todos 待审列表(学生卡片流) 班主任
GET /api/v1/mp/teacher/audit/{submissionId} 学生详情(材料预览 + 问题清单) 班主任
POST /api/v1/mp/teacher/audit/{submissionId}/approve 通过 班主任
POST /api/v1/mp/teacher/audit/{submissionId}/reject 驳回(原因快选 + 可选语音转文字) 班主任
POST /api/v1/mp/teacher/audit/batch-approve 一键通过全部绿色项 班主任
POST /api/v1/mp/teacher/proxy-upload 代传材料(记录代传人) 班主任
POST /api/v1/mp/teacher/notify/unsubmitted 一键催报未提交者 班主任
GET /api/v1/mp/teacher/notify/records 催报历史 班主任
POST /api/v1/mp/subscribe 请求并登记微信订阅消息授权次数(⚠️ 应对 R12) 全部用户

8.7 审核

方法 路径 说明 权限
GET /api/v1/audit/todos 待办列表(含红黄绿状态) 班主任/管理员
GET /api/v1/audit/submissions/{id} 填报单详情(含材料预览、问题清单) 班主任/管理员
POST /api/v1/audit/submissions/{id}/approve 通过 班主任/管理员
POST /api/v1/audit/submissions/{id}/reject 驳回(原因) 班主任/管理员
POST /api/v1/audit/batch 批量通过/驳回(详见 8.10) 班主任/管理员
GET /api/v1/audit/submissions/{id}/timeline 审核时间轴 班主任/管理员
GET /api/v1/audit/statistics 审核统计 管理员
GET /api/v1/audit/reason-codes 驳回原因字典 班主任/管理员
POST /api/v1/audit/submissions/{id}/proxy-upload 代传材料 班主任/管理员

8.8 校验、导出与通知

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

8.9 关键接口详述

① 上传凭证 POST /api/v1/mp/files/upload-token

请求

{
  "taskId": 2026090001,
  "materialCode": "IDCARD",
  "fileName": "曾伊豪-361029200907295110身份证.jpg.jpg",
  "fileSize": 17962304,
  "contentType": "image/jpeg"
}

响应

{
  "code": 0,
  "data": {
    "fileId": 987654321,
    "uploadUrl": "https://oss.xxx.com/raw/1001/...&Signature=...",
    "objectKey": "raw/1001/2026090001/202609/9f3a....jpg",
    "expireSeconds": 900,
    "clientCompressHint": {
      "enabled": true,
      "maxLongSide": 1600,
      "quality": 80,
      "targetSizeKb": 1024
    }
  }
}

② 文件规范化 POST /api/v1/mp/files/{fileId}/process

请求

{ "async": true, "runOcr": true, "dryRun": false }

响应(同步模式)

{
  "code": 0,
  "data": {
    "fileId": 987654321,
    "originalName": "曾伊豪-361029200907295110身份证.jpg.jpg",
    "standardName": "曾伊豪-361029200907295110-身份证.jpg",
    "rawSize": 17962304,
    "finalSize": 524288,
    "compressRatio": "97.1%",
    "width": 1600, "height": 1200, "quality": 74,
    "issues": [
      { "ruleCode": "VAL-06", "severity": 1, "title": "格式已转换", "detail": "jpg.jpg → jpg", "autoFixed": true },
      { "ruleCode": "VAL-02", "severity": 3, "title": "身份证号与名册不一致",
        "expectedVal": "3610292009072**110", "actualVal": "3610292009072**510",
        "detail": "OCR 识别值与名册一致,文件名号码有误,已按名册修正" }
    ],
    "ocr": { "status": 1, "confidence": 0.973, "fields": { "name": "曾伊豪", "idCardHash": "a1b2..." } }
  }
}

异步模式:返回 taskToken,通过 GET /api/v1/mp/files/{fileId}/process-status 轮询或 SSE 推送进度。

③ 提交 POST /api/v1/mp/submissions/{id}/submit

请求:{ "confirmWarning": true }

校验失败响应

{
  "code": 40001,
  "msg": "存在 2 项必须处理的材料问题",
  "data": {
    "canSubmit": false,
    "blocking": [
      { "materialCode": "SCHOOL_ROLL", "ruleCode": "VAL-05", "title": "缺少材料:学籍表" },
      { "materialCode": "IDCARD", "ruleCode": "VAL-04", "title": "本材料证件号与其他材料不一致",
        "detail": "一寸照片为 360122200902044232,身份证为 36010520090817121X" }
    ],
    "warnings": [
      { "materialCode": "PHOTO1", "ruleCode": "VAL-08", "title": "照片分辨率偏低,建议让对方发一张原图" }
    ]
  }
}

④ 批量审核 POST /api/v1/audit/batch

请求

{
  "taskId": 2026090001,
  "submissionIds": [1001, 1002, 1003],
  "action": "APPROVE",
  "reasonCode": null,
  "reasonText": null
}

响应

{
  "code": 0,
  "data": { "success": 2, "skipped": 1, "failed": 0,
            "details": [{ "submissionId": 1003, "skipped": true, "reason": "存在红色问题,需单独审核" }] }
}

⑤ 创建导出 POST /api/v1/exports

请求

{
  "taskId": 2026090001,
  "exportName": "202609机电认定-24中技数控1班",
  "scope": { "orgIds": [2001], "onlyApproved": true, "includeRaw": false },
  "splitSizeMb": 50,
  "desensitize": false,
  "format": "ZIP",
  "generateSummaryExcel": true,
  "notifyOnFinish": true
}

响应

{ "code": 0, "data": { "exportId": 555, "status": 0, "estimatedSeconds": 45 } }

8.10 开放接口(预留,供认定机构对接)

方法 路径 说明
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 主动推送审核结果到机构系统

9. 移动端页面与交互

9.1 小程序页面清单

页面 路径 关键交互
首页(任务列表) pages/index 卡片式展示任务;显示进度环"已完成 3/5 项";倒计时提醒
门禁核验 pages/gate/verify ⚠️ 新增(入口第一屏):填写「姓名 + 身份证号」;身份证输入用数字键盘、自动分段、末位 X 大写、支持粘贴;本地先校验格式与校验位;提交后匹配名单
门禁失败 pages/gate/fail 统一提示"没有找到你的信息";两个系统内动作:重新填写 / 申请加入名单(FR-509)。不展示任何联系方式
任务详情 pages/task/detail 材料清单、状态、示例图、操作指引
信息填写 pages/form/index 分步表单;已匹配名册自动回填;身份证 OCR 一键识别
材料上传 pages/upload/index 来源选择:微信聊天记录 / 手机相册 / 手机文件(无"拍照");每个材料项附"去哪里找"引导;上传即显示"压缩中 92%…";实时校验结果
材料来源选择 pages/upload/source 半屏弹层,三个来源卡片 + 每个材料的引导文案;推荐标"从聊天记录选"(可保留原始文件名)
文件预览 pages/preview/index 原图/压缩图对比滑动条;显式展示规范文件名
提交确认 pages/submit/confirm 红黄绿三项汇总;阻挡项明确提示"去处理"
我的状态 pages/status/index 审核时间轴;驳回原因与修改入口
消息中心 pages/message/index 系统通知、催报、审核结果

9.2 关键交互设计

⓪ 门禁核验(第一道关,决定后续一切)

扫码进入
   ↓ 微信授权(静默)
   ↓ ┌────────────────────────────────────┐
     │  请填写你的信息,与老师提供的名单核对  │
     │                                     │
     │  姓名    [ 张 三            ]        │
     │  身份证号 [ 3601 21 2000 1010 1234 ] │  ← 数字键盘、自动分段、末位X大写
     │                                     │
     │           [   下一步   ]             │
     └────────────────────────────────────┘
   ↓ 本地先校验(位数、校验位)→ 不合格当场提示,不发请求
   ↓ 服务端匹配名单
   ├─ 命中 ─▶ 回填【全部预填字段】→ 进入填报页
   │           🔒 姓名 / 身份证号 / 报考职业 / 报考工种 / 报考等级 → 只读
   │           ✏️ 其余字段 → 预填并标注"老师已填,请核对",可改
   │           📄 学生真正要做的:上传材料
   ├─ 号码命中但姓名形近 ─▶ "你是不是 杨晨?" ─▶ 确认后通过
   └─ 未命中 ─▶ ┌──────────────────────────────────┐
                │  没有找到你的信息                  │
                │  请核对姓名和身份证号是否与老师      │
                │  提供的名单一致。                  │
                │ 如果确实已报名,可申请加入名单。    │
                │                                  │
                │[ 重新填写 ] [ 申请加入名单 ]   │
                └──────────────────────────────────┘

🔒 两条不能妥协的规则:

  1. 失败提示不区分原因——无论"身份证号不在名单"还是"姓名不匹配",文案完全一致。否则可被用来逐个探测某个身份证号是否在名单里。
  2. 必须限流——同 openid 每小时 ≤10 次,超出锁定。

💡 学生端实际工作量极小:实测老师交上来的名单里 21 个字段已填满 18 个(含学历、在校生、学校、专业、入学时间、报考三项、联系电话)。学生匹配成功后只需核对 + 上传材料——这才是"3 分钟填完"的真正实现方式。

① 上传即规范化(核心体验)

点击"选择材料"
   ↓ 弹出来源(无"拍照"选项):
       ┌──────────────────────────────────────┐
       │  📋 从微信聊天记录选择   ← 推荐       │
       │     照相馆/打印店发的文件在这里        │
       │  🖼 从手机相册选择                    │
       │     系统截图、已有的照片在这里          │
       │  📄 从手机文件选择                    │
       │     PDF 扫描件、Word 申报表在这里      │
       └──────────────────────────────────────┘
   ↓ 客户端预处理(方向校正 + 长边限幅 ≤2400px,不做有损压缩)
   ↓ 上传至 OSS(显示进度条)
   ↓ 服务端规范化(改名 / 精准压缩 / 校验 / OCR)
   ↓ 卡片翻转动画展示:
     ┌────────────────────────────────────┐
     │ 曾伊豪-361029200907295110身份证.jpg.jpg │  ← 原文件名(红色删除线)
     │              ↓ 已自动规范              │
     │ 曾伊豪-361029200907295110-身份证.jpg   │  ← 规范文件名(绿色)
     │ 17.1MB → 810KB(压缩 95%,清晰度无损) │
     └────────────────────────────────────┘

①-a 来源提示(决定成败的细节)

材料 界面提示
一寸照片 "照相馆给过电子版吗?在微信聊天记录里搜一下"
工作证明 "打印店扫描后会发给你,从聊天记录里选"
学籍表 "教务系统截图后,从手机相册里选"
身份证 "扫描件或拍过的照片都行,从聊天记录或相册里选"

💡 优先引导用户从聊天记录选:该通道能拿到原始文件名,命名规范化引擎可直接读出姓名、证件号、材料类型,一次通过率最高;相册来源只能拿到 IMG_0001.jpg 这类名字,必须靠 OCR 兜底。

② 错误提示文案(面向家长,必须"零术语")

场景 文案
证件号与名册不符 「身份证号第 X 位和老师名单上的不一样,请核对身份证原件(老师名单上是 3601...1234)」
文件过大且压缩受限 「这张图太复杂,压到最小还是 1.2M,建议重新拍一张更清晰的」
照片可能是别人 「这张"一寸照片"上的身份证号是 3601...4232,和你其他材料不一致,请检查是否传错了」
缺少材料 「还差 1 项:学籍表。点击去上传 →」
照片模糊 「照片有点模糊,公章可能看不清,建议找打印店重新扫描一份,或让对方发一张原图给你」

③ 无障碍与低门槛

  • 全流程最多 3 步、单页最多 6 个字段;
  • 支持"上一步保存的草稿"自动恢复;
  • 支持语音输入(可选);
  • 大字号模式(适配家长群体)。

9.3 管理端(PC)页面清单

模块 页面
工作台 待办、任务总览、异常概览、快捷入口
收集任务 任务列表 / 任务向导(7 步)/ 任务详情与统计
表单设计器 拖拽式字段编排、条件显示配置、实时预览
材料规则 材料项配置、命名规则可视化编辑器(支持上传样本文件名试跑)
名册 导入向导、体检报告、名册列表、认领管理
审核中心 待办列表、学生卡片视图、批量通过、时间轴
数据中心 异常清单、完成率看板、班级排行、材料统计
导出中心 创建导出、进度、历史、下载
通知中心 模板管理、发送记录、催报设置
系统设置 组织、用户、角色、字典、日志、套餐用量
整理器(差异化) 上传压缩包 → 体检报告 → 一键规范化 → 下载结果

命名规则编辑器(重点)

左侧:模板变量面板(拖拽)        右侧:实时试跑
 ┌──────────────────────┐       ┌────────────────────────────────┐
 │ {姓名} {证件号}        │       │ 原始:曾伊豪-...110身份证.jpg.jpg │
 │ {材料名} {班级}        │  →    │ 结果:曾伊豪-...110-身份证.jpg    │
 │ {后缀} {序号} {日期}   │       │ 状态:✅ 已自动修复               │
 └──────────────────────┘       └────────────────────────────────┘
 模板:[{姓名}-{证件号}-{材料名}{后缀}]      [批量试跑 20 条]  [下载报告]

9.4 班主任移动端(小程序)页面清单 ⚠️ 初稿缺失,已补

用户明确要求"班主任在电脑或手机操作",但初稿只给了 PC 管理端和填报端,遗漏了班主任移动端。这是使用频次最高的一环——班主任通常在微信里被学生追问、在通勤路上随手处理。

页面 路径 关键交互
班级看板 pages/teacher/dashboard 完成率进度条、未提交名单、异常项数;顶部大按钮"一键催报未提交"
待审列表 pages/teacher/audit/list 学生卡片流:头像、姓名、5 项材料缩略图、🟢🟡🔴 状态点;支持按状态筛选(只看红/只看未审)
学生详情 pages/teacher/audit/detail 材料大图横滑预览、缩放、原图/压缩图对比;问题清单;底部"通过 / 驳回"
批量通过 pages/teacher/audit/batch 勾选全部绿项 → 一键通过;红黄项自动留在列表
驳回反馈 pages/teacher/audit/reject 常用原因快选(§附录 C)+ 语音输入补充
代传材料 pages/teacher/proxy-upload 班主任帮学生上传/替换材料,记录代传人
催报中心 pages/teacher/notify 未提交/未通过名单、一键催报、催报历史
消息中心 pages/teacher/message 学生提交提醒、超时提醒、导出完成通知

设计约束

  • 一屏能看完整班状态(40 人无需滚动超过 2 屏);
  • 审批动作 ≤3 次点击(进入 → 选人 → 通过);
  • 图片预览使用渐进式加载 + 缩略图优先,弱网下先出低清再补高清;
  • 只支持「右滑通过」,不支持左滑驳回,且必须带二次确认。

    ⚠️ 设计决策(已确认):通过是正向动作、可撤回,适合用低阻力的滑动触发;驳回是负向动作、需要选原因、会打断学生的流程,必须点按钮完成。若两者都用滑动,班主任在通勤路上一次误触就可能误驳学生——而驳回的代价(学生重传、班主任重审)远高于通过。


10. 非功能需求

10.1 性能指标

项 指标
页面首屏(小程序) ≤2s(4G 网络)
表单保存(草稿自动保存) ≤500ms
客户端压缩(4000×3000) ≤1.5s
服务端规范化(改名+压缩+校验) P95 ≤3s
OCR 识别 P95 ≤2.5s
导出打包(1 个班 112 文件) ≤60s
导出打包(50 个班 5600 文件) ≤10min(异步)
并发上传 ≥500 并发
单文件上传上限 50MB(客户端预压后实际远小于此)
图片解码内存峰值 ≤1GB/进程(实测存在 1.8 亿像素图片,必须用 libvips 流式处理,详见 §6.3 安全闸门)
超大图处理耗时 1.8 亿像素扫描件 ≤6s;超 20s 熔断并转异步
图片处理并发上限 按内存反推设定(不得按 CPU 核数设定),超出进队列

10.2 容量与成本估算

以 100 所学校 / 校均 30 个班 / 班均 40 人 / 人均 4 类材料 测算(单文件体积取自 §1.2 实测均值):

项 计算 结果
文件数 100×30×40×4 48 万个/学期
规范文件存储 48 万 × 0.33MB(实测均值) 约 158 GB/学期
原始文件存储 48 万 × 4.24MB(实测均值) 约 2.0 TB/学期 ⚠️
OCR 调用次数 48 万 × 1.5 约 72 万次/学期

✅ 修正:初稿的"原始平均 8MB / 3.8TB"是按 1000 万像素手机照片估的;实测该批数据是扫描件,均值 4.24MB,故修正为 2.0TB。 ⚠️ 关键结论不变且更紧迫:原始文件绝不能长期保留。 2.0TB vs 158GB 仍相差 13 倍。 💡 客户端预压缩可将单班上传量从 475MB 降至约 37MB(降幅 92%),是成本控制的第一道闸门。

参考成本(公有云,仅供参考)

  • 对象存储:288GB ≈ ¥35/月(标准存储)
  • 外网下行流量:按需,建议导出走内网 + 签名 URL
  • OCR:72 万次/学期 × ¥0.015 ≈ ¥10,800/学期 ⇒ 建议对"高置信度"结果缓存,同一张图不重复识别
  • 短信:¥0.045/条,建议仅用于未绑定微信的用户

10.3 安全与合规

依据《个人信息保护法》《数据安全法》及教育行业数据管理要求:

项 措施
传输安全 全站 HTTPS/TLS 1.2+;OSS 私有读 + 5 分钟签名 URL
存储加密 身份证号、手机号 AES-256 加密存储;检索用 HMAC 索引(id_card_hash)
展示脱敏 默认 3601**********1234;查看明文需二次授权并记录日志
访问控制 租户隔离 + 数据权限(本班/本院系)+ 最小权限原则;名单不下发前端,匹配在服务端完成
门禁防枚举 ⚠️ 新增。① 匹配失败不区分原因(防探测某身份证号是否在名单中);② 匹配接口限流(同 openid 每小时 ≤10 次、同 IP 每分钟 ≤20 次);③ 失败尝试不记录明文姓名与身份证号;④ 名单接口禁止任何形式的导出枚举
审计留痕 登录、查看明文、下载、导出、代传、删除全部记录(含 IP、UA)
授权同意 填报前需勾选《个人信息处理告知同意书》,未成年人需监护人同意
水印 预览与导出可加隐形水印(含操作人 ID),防外泄溯源
反爬防盗链 签名 URL 一次性、限时、限 IP 段(可选)
内容安全 小程序 UGC 内容调用微信 msgSecCheck(图片 + 文本)
数据留存 原始文件 30 天;规范文件随任务保留;账号注销后 30 天内清除
等保 目标等保二级(SaaS 公有云)
微信小程序隐私保护指引 ⚠️ 初稿遗漏。小程序需在微信后台声明收集的敏感信息(身份证号、手机号、相册(仅上传)),并配置《用户隐私保护指引》。未配置将无法调用相关接口、审核不通过。需在 M1 阶段完成
受托处理者协议 与学校/机构签署《数据处理协议》(DPA),明确 SaaS 方为受托处理者、校方为个人信息处理者
密钥管理 AES 密钥托管于 KMS,禁止硬编码;密钥轮换周期 ≤12 个月

10.4 兼容性

端 要求
微信小程序 基础库 ≥2.30;覆盖微信 8.0+;iOS 14+ / Android 8+。
依赖能力:wx.chooseMessageFile(从聊天记录选文件,需基础库 ≥2.5.0)、wx.chooseMedia(相册,需 ≥2.10.0)、wx.uploadFile、订阅消息。不使用相机能力(默认关闭拍照入口)
PC 管理端 Chrome / Edge 100+(推荐 Chromium 内核)
图片格式 输入:JPG/JPEG/PNG/HEIC/BMP/TIFF/PDF(含从聊天记录选中的 PDF/Word 扫描件);输出:JPG/PNG/PDF

10.5 可维护性

  • 规则引擎配置化(材料、别名、校验规则、压缩档位均不入代码);
  • 全链路追踪(traceId 贯穿上传 → 处理 → 校验 → 审核 → 导出);
  • 文件处理失败可重放(保留原图 30 天即为重放保障);
  • 关键算法(命名解析、压缩、校验)需覆盖单元测试,以本项目 28 人真实数据为回归测试集。

11. 实施计划与里程碑

里程碑 内容 工期 交付物
M0 需求与原型 需求确认、原型设计、§14 待确认项闭环(尤其 Q1/Q2) 1 周 原型图、需求确认书、压缩参数书面确认函
M1 基础平台 租户、组织、用户、角色、登录、字典、微信小程序隐私保护指引配置 2 周 可登录的骨架系统 + 隐私合规就绪
M2 任务与表单 任务向导、动态表单引擎、名单导入(含报考范围聚合与字段三类规则)、任务-班级绑定 2 周 可建任务、可导名单
M3 规则引擎(核心) 命名规范化、五档压缩(libvips)、格式转换、校验位裁决、OCR、超大图 OOM 防护 3.5 周(A/B 验证已因实测达标而取消,较 V1.2 减 0.5 周) 可上传即规范化
M4 审核与导出 审核工作台、批量审核、汇总导出、分卷打包 2 周 全流程闭环(PC 端)
M5 移动端 小程序填报端 + 班主任端、通知与催报(含订阅消息授权策略) 2.5 周 手机端可用
M6 整理器 ���史数据批量规范化 + 体检报告 1.5 周 获客工具
M7 联调与试运行 压测(必须用 28 张 1.6 亿像素真实扫描件)、真实数据回归、试点 1–2 个班 2 周 试运行报告
合计 部分任务可并行 12.5–14.5 周 V1.0 上线

MVP 建议范围(若需 6 周内出可用版本) 保留:M1 + M2(简化版:表单字段固定为模板)+ M3 + M4 + M5 砍掉:整理器(M6)、证件照规格化、自动催报、部分统计看板

⚠️ 不建议再压缩 M3:超大图 OOM 防护与压缩参数验证是项目成败的关键路径,压缩这部分的工期会直接把风险推到试运行阶段(即"在真实客户面前爆炸")。


12. 验收标准

12.1 功能验收(以真实数据为基准测试集)

使用 D:\项目\202609机电填报系统\24中技数控1班 全部 112 个文件作为 Golden Test Set:

验收项 期望结果
命名合规率(处理后) 112 个文件输出规范名,合规率 100%
需改名文件 精确识别 59 个(误差 0),不多改、不漏改
双扩展名修复 56 个 .jpg.jpg 全部修复(工作证明 28 + 身份证 28)
缺分隔符修复 2 个(曾伊豪、徐程宇)全部修复
大小写统一 2 个末位 x → X
证件号异常检出 必须检出全部 5 例,且按 §6.1 裁决矩阵正确分类
↳ 可自动改名 2 例:于豪、杨晨(校验位失败 → 裁决为文件名错误)
↳ 名册兜底补齐(标黄确认) 2 例:曹文锋、黎思明(17 位,无法校验)
↳ 必须人工 1 例:陈子文(校验位均通过 → 串档,不得自动修正)
姓名纠错 扬晨 → 杨晨(编辑距离 1 + 拼音一致,可自动);其余 27 人姓名不得误改
体积达标率 31 个超标文件全部压至 ≤1M;另 81 个文件不得被无谓重压
过度压缩保护 杨晨-身份证(558×745) 必须被标记"分辨率不足"而非被进一步压缩
截图类保护 学籍表 28 个 PNG 保持 PNG 且不缩放(像素尺寸不变)
图片处理稳定性 处理 28 个 1.6 亿像素扫描件不得 OOM,内存峰值 ≤1GB/进程
章迹清晰度 压缩后公章文字在 100% 缩放下肉眼可辨(人工抽检 10 份);输出为 300dpi 标准扫描件,实测 28/28 达标
班级包总量 从 475MB 降至 ≤40MB(实测预期约 37MB)
导出目录结构 与本文档 5.9 规范的目录树一致
端到端耗时 单班从上传完成到导出可上报 ≤15 分钟(不含学生填报时间)

📊 测试基线已固化:本节的 59 / 56 / 31 / 3 等数字均来自 2026-09-24 的实测脚本,可直接作为自动化回归断言的期望值。

12.2 性能验收

  • 500 并发上传无失败;
  • 单文件规范化 P95 ≤3s(1.8 亿像素超大图豁免,单独 ≤6s);
  • 处理 28 个 1.6 亿像素扫描件时,JVM/进程内存峰值 ≤1GB,无 OOM;
  • 1 个班导出打包 ≤60s。

12.3 安全验收

  • 越权测试:A 租户无法访问 B 租户任意资源(含文件签名 URL);
  • 明文查看、导出、下载均有审计日志;
  • 数据库中的证件号、手机号为加密存储(肉眼不可读);
  • 签名 URL 过期后无法访问;
  • 微信小程序隐私保护指引已配置并通过审核。

13. 风险与对策

# 风险 影响 对策
R1 认定机构不接受强制压缩后的清晰度 已基本解除,降为低风险 低 ✅ 实测证明 300dpi/q85 下 28/28 达标(平均 806KB),输出的是标准 300dpi 扫描件,清晰度无损失,机构无正当理由拒收。
剩余动作:① 取得机构对"300dpi 扫描件"的书面认可(走个形式);② 保留原图 30 天可随时重出;③ 清晰度标记 + 抽检机制作为兜底保留
R2 OCR 对低质量来源图片识别率低(聊天中被压缩过的图、翻拍照有反光/遮挡) 中 ① 置信度分级 + 人工兜底;② 优先引导从聊天记录选文件(可用原始文件名,不完全依赖 OCR);③ 检测到分辨率偏低时提示"索要原图";④ 双引擎兜底(腾讯云 + 百度)
R3 微信小程序审核受阻(涉及教育/UGC 类目、需资质) 高 ① 提前确认类目与资质(可能需 ICP + 办学许可或合作方资质);② 备选 H5 方案(扫码即填)保底
R4 个人信息合规(身份证号属敏感个人信息) 高 见 10.3;签约时明确数据处理者角色与责任划分(SaaS 服务商 = 受托处理者)
R5 存储与带宽成本超预期(原始文件 3.8TB/学期) 中 强制原始文件 30 天清理 + 客户端预压缩 + 导出走签名 URL
R6 老师抵触新系统("又要多学一个东西") 高 ① "历史数据整理器"作为钩子,先让老师看到价值;② 名册 Excel 直接导入,无需重复录入;③ 一屏批量审核,比现在更快
R7 学生不会操作 / 使用老旧手机 中 三步极简流程 + 班主任代传兜底 + 大字号模式
R8 重复提交 / 版本混乱 低 版本化文件管理 + 导出去重取最新
R9 与现有系统(如需对接认定机构平台)不兼容 中 预留开放 API(8.10);导出包格式遵循机构要求
R10 试用期后不续费 中 免费版限制在"1 任务 / 60 人",让老师在真实任务中形成依赖
R11 超大扫描件导致图片处理服务 OOM ⚠️ 初稿未识别 高 实测单图 1.8 亿像素(全解码约 540MB 内存)。对策:① 强制 libvips 流式处理;② 5000 万像素以上先金字塔降采样;③ 按内存反推并发上限(非 CPU);④ 20s 熔断;⑤ 压测必须用这 28 张真实扫描件做样本
R12 微信订阅消息无法反复催报 ⚠️ 初稿未识别 中 微信"一次性订阅消息"需用户每次授权才能发 1 条,催报 3–4 次很快耗尽额度。对策:① 填报时一次性请求多次订阅(用户可勾选"总是保持以上选择");② 引导关注服务号用模板消息(无次数限制);③ 短信兜底(按条计费);④ 产品层减少催报次数、提高一次填完率
R13 家长/学生手机号本身可能来自他人(代填场景) 低 允许一人代填多份(如哥哥帮弟弟),需在名册认领时留痕

14. 待确认事项

# 事项 影响 需要谁确认
Q1 认定机构是否书面接受"压缩后 ≤1M" 已决策:D1 强制 1M,无例外。待办转为:取得机构对「300dpi 扫描件」的书面认可(实测 28/28 达标,无技术争议) 中 业务/客户
Q2 压缩参数底线:A4 扫描件的目标 DPI 实测标定值、质量下限 高 认定机构
Q2-d1 是否接受体积放宽到 2M 已决策:D1 不接受放宽,强制 1M — 已关闭
Q2b 截图类材料(学籍表)是否必须转 JPG?保留 PNG 是否会被拒收 中 认定机构
Q2c 分辨率下限:身份证翻拍长边低于多少判定为不可接受(建议 1000px) 中 认定机构
Q3 微信小程序主体资质是否齐备(教育类目可能需办学许可) 高 商务/法务
Q4 "工作证明"材料的规范名与材料实际内容不符(个人申报表),是否按机构习惯保留 中 客户
Q5 社会收集的人员是否需公安实名核验(涉及额外成本) 中 客户
Q6 OCR 引擎选型(腾讯云 / 百度 / 阿里云 / 本地模型),成本与数据出境要求 中 技术 + 合规
Q7 定价策略:按人数 / 按任务 / 按年订阅 中 商务
Q8 是否需要向学生收取费用(如报名服务费),涉及支付资质 中 商务
Q9 一期是否需要与"认定机构业务系统"对接 中 客户
Q10 私有化部署版本是否同步开发(当前决策为 SaaS 优先、私有化后置) 低 产品
Q11 名单外的人怎么处理? 当前按需求实现为"不给填、只提示",老师需去后台加人。建议增加"申请加入名单 → 管理员审批后自动放行"通道(既不破门禁、又免手工加人)。请决策 高 客户
Q12 社会收集的名单从哪来? 社会收集同样"先导名单",但若名单来源不稳定(预报名名单 / 报名点收集 / 机构提供),落地难度较大。需确认名单来源与责任方 高 客户
Q13 门禁是否再加一层手机号验证码(防止有人拿到他人姓名+身份证号即可代填) 中 客户
Q14 门禁限流阈值(同 openid 每小时 10 次)是否合适,需按实际人数标定 中 研发
Q15 认定机构下发的"计划清单"样本 已澄清:不存在该文件,报考范围来自名单,问题关闭 — ✅ 已闭环
Q16 计划下发是全量还是增量 同上,问题关闭 — ✅ 已闭环
Q17 国标职业字典初始数据来源 降级:字典改为可选的拼写校验(P2),非强约束,不阻塞开发 低 产品
Q18 是否允许学生修改"老师预填"的字段? 本版设计为"可改并留痕"。若机构要求严格一致,需改为"只读+申请修改" 中 客户

附录 A:数据字典(部分)

材料类型 material_code

编码 名称
PHOTO1 一寸照片
IDCARD 身份证
SCHOOL_ROLL 学籍表
XJW_BA 学信网备案表
WORK_PROOF 工作证明 / 个人申报表
OTHER_PROOF 其它佐证材料

收集模式 collect_mode:1 学校收集 / 2 社会收集 认定类型 cert_type:1 自主认定 / 2 社会评价 / 3 专项职业能力

⚠️ 社会评价(认定类型)与 社会收集(收集模式)是两回事: 前者决定填什么材料(社会人员要学信网备案表,不要学籍表),后者决定谁审核(有无班主任层)。详见 §5.3。 填报单状态:0 草稿 / 1 待初审 / 2 待终审 / 3 已通过 / 4 已驳回 / 5 已归档 问题级别 severity:1 信息 / 2 警告 / 3 错误

附录 B:命名规则正则参考

# 身份证号(18位,末位可X)
[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]

# 身份证号(15位旧号)
[1-9]\d{5}\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}

# 完整规范文件名(校验用)
^[\u4e00-\u9fa5]{2,6}-[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dX]-(一寸照片|身份证|学籍表|学信网备案表|工作证明|其它佐证材料)\.(jpg|jpeg|png)$

# 需清理的非法字符
[\\/:*?"<>|\s]

# 重复扩展名检测
\.(jpg|jpeg|png|pdf|bmp|tiff|gif|heic){2,}$

附录 C:驳回原因字典

编码 文案
R001 材料不清晰,请重新提供(建议找打印店重新扫描或索取原图)
R002 身份证号与老师名单不一致
R003 材料不是本人
R004 文件超过 1M
R005 缺少材料
R006 工作证明未盖公章
R007 材料不完整(缺正反面 / 缺页)
R008 信息填写有误
R009 照片背景不符合要求
R999 其他(手填)

附录 D:错误码表(⚠️ 复审补充)

码 含义 端处理
0 成功 —
40001 存在阻断性校验问题,无法提交 展示 blocking 清单,引导逐项处理
40002 文件格式不在白名单 提示支持的格式
40003 文件体积超上限且压缩受限 提示重新提供(重扫描 / 索取原图 / 换来源通道)
40004 身份证号校验位不通过 高亮错误位,提示核对原件
40005 证件号与名册不一致 展示名册值,要求确认
40006 疑似串档(校验位均通过但与他人不符) 转人工,不自动修正
40007 门禁未匹配(姓名或身份证号与名单不符) 统一提示"没有找到你的信息",提供重新填写 / 申请加入名单(不透露是哪一项错)
40071 门禁尝试过于频繁(触发限流) 提示"操作过于频繁,请稍后再试"
40072 名单项已被他人绑定 提示"该信息已被登记,如有疑问请联系老师"
40008 任务已截止 / 已归档 禁止提交,提示联系管理员
40009 重复提交(幂等键冲突) 返回既有结果
40010 驳回次数超上限 提示联系班主任
40101 未登录 / token 失效 静默重新登录
40301 无权限(含跨租户) 跳转 403
40302 签名 URL 已过期 重新获取
42901 触发限流 退避重试
50001 文件处理失败 保留原图,允许重试
50002 OCR 服务异常 降级为人工填写
50003 图片过大被熔断 转异步处理

附录 E:定时任务清单(⚠️ 复审补充)

任务 频率 说明
原始文件清理 每日 02:00 删除 raw_expire_time 到期的原图(默认 30 天)
导出包清理 每日 02:30 删除过期导出包(默认 7 天)
自动催报 每日 09:00 按截止日 7/3/1 天规则,仅催未提交者
审核超时提醒 每小时 初/终审超 timeoutHours 未处理 → 提醒并升级
任务状态流转 每小时 到期任务自动 PUBLISHED → CLOSED
待认领名册清理 每日 超时未认领的人员移出应填名单并通知管理员
用量统计 每日 01:00 租户存储量/OCR/短信用量快照,供计费与超额告警
压缩失败重试 每 10 分钟 扫描"压缩受限/处理失败"文件,重试 ≤3 次
租户到期提醒 每日 08:00 到期前 30/7/1 天提醒

附录 F:复审方法论说明

本次复审(V1.1)的验证方式,供后续版本沿用:

  1. 用真实数据复核每一个数字:文档中所有"数量/占比/体积"均通过脚本对 24中技数控1班 112 个文件重新实测,不依赖估算;
  2. 用真实数据复核每一条参数假设:读取每张图片的实际像素尺寸与格式,而非假设"学生上传的是手机照片"——本次正是靠这一步才发现工作证明是 1272dpi 扫描件、学籍表是截图;
  3. 用算法裁决业务争议:ISO 7064 校验位把"证件号到底谁对"从人判断变为机器判断;
  4. 把实测结果固化为回归断言:§12.1 的 59/56/31/3 可直接写成自动化测试的期望值。

附录 G:PostgreSQL DDL 实测验证记录(2026-09-28)

验证环境:postgres:16-alpine(PostgreSQL 16.15),Docker 本地实例,全新空库。

验证方式:从本文档 §7.3 抽取全部 DDL → 重排为「先建表、后 ALTER」→ 建空库执行 → 再用真实名单数据跑功能查询。

G.1 DDL 执行结果

项 结果
SQL 块 20 个(18 个 CREATE TABLE + 2 个 ALTER)
执行错误 0
建成表 19 张
列注释 167 条
表注释 19 条
索引 61 个

G.2 实际踩到并修掉的错误(原始报错)

ERROR: relation "uk_tenant_code" already exists
ERROR: relation "idx_name" already exists
ERROR: relation "idx_submission" already exists
ERROR: relation "idx_tenant" already exists
ERROR: relation "collect_task" does not exist     ← 上面第 1 个错误的连锁反应
冲突名 涉及表 修法
uk_tenant_code sys_tenant、collect_task → uk_task_tenant_code
idx_name sys_occupation_ref、collect_roster → idx_occref_name / idx_roster_name
idx_tenant collect_material_def、collect_rule_template、collect_cleanup_job → idx_matdef_tenant / idx_ruletpl_tenant / idx_cleanup_tenant
idx_submission collect_file、collect_validation_issue、collect_audit_record → idx_file_submission / idx_issue_submission / idx_audit_submission

⚠️ 根因:MySQL 的索引名只需表内唯一,PG 要求 schema 内全局唯一。 这是 MySQL→PG 迁移最容易踩的坑,且语法检查器查不出来(SQL 本身完全合法),只有真跑才会报错。 ⇒ 本项目的所有索引/约束命名必须带表前缀,这是硬规范。

G.3 功能查询验证(用真实名单数据)

写入 3 条真实名单(含 1 条故意填错工种的"少数派"):

验证项 结果
collect_roster 业务列 18 个齐全 ✅
报考范围聚合 车工/数控车工/四级 → 2 人;车工/普通车工/三级 → 1 人 ✅
一致性体检(识别少数派) ✅ 准确筛出"普通车工 1 人(占比 <50%)"
按学历/在校生筛选 ✅ 正常
jsonb 逃生舱读写(含中文键) ✅ 正常
聚合查询执行计划 Index Only Scan using idx_roster_scope ✅

💡 Index Only Scan 的意义:说明"按报考范围聚合""按工种统计人数"这类高频查询完全不回表,直接从索引取数。这是把 18 个字段建成正式列(而非塞 JSON)换来的直接收益——如果当初走 prefilled_json,这类查询只能全表扫描并逐行解析 JSON。

G.4 一处刻意不做的约束

"报考三项为空 → 阻断任务发布"没有做成 CHECK 约束,是应用层校验。

原因:名单导入过程中允许暂时为空(老师分批补录),若加 CHECK (occupation IS NOT NULL) 会导致导入直接失败。 正确做法是导入时体检 + 发布时阻断(见 §5.3.1 与功能逻辑卷 §3.2-A)——该拦的地方拦,不该拦的地方别拦。


文档结束


实现说明(2026-09-28):本文件中的 40007b / 40007c 已规范化为 40071 / 40072。 原因:code 按 §1.3 约定是数字,带字母后缀会在 JSON 里变成字符串,破坏前端 code === 0 的判断。