# 验收测试清单 > 版本:V1.0 | 日期:2026-09-28 > 读者:测试工程师、开发(自测)、项目经理(验收) > 配套:《04-核心模块设计说明.md》附录 B(基线)、《05-开发任务拆解.md》(里程碑验收) > 核心原则:**能用真实数据断言的不靠人眼;能自动化的一次写、长期跑** --- ## 一、测试策略 ### 1.1 四层结构 ``` ┌─────────────────────────────────────────────┐ │ L4 验收测试(人工) ← §6 里程碑验收签署表 │ │ 客户在场、用真实数据、不可自动化 │ ├─────────────────────────────────────────────┤ │ L3 端到端(半自动) ← §3 主流程用例 │ │ 真机 + 真实数据,脚本辅助准备与核对 │ ├─────────────────────────────────────────────┤ │ L2 集成测试(自动) ← §4 功能验收清单 │ │ 接口级,含边界与异常路径 │ ├─────────────────────────────────────────────┤ │ L1 单元测试(自动) ← §2 Golden Assertions │ │ 引擎级纯函数,**用 112 个真实文件做断言** │ └─────────────────────────────────────────────┘ ``` ### 1.2 各层覆盖目标 | 层 | 覆盖对象 | 目标 | 执行时机 | |---|---|:--:|---| | L1 | M1 命名引擎、M2 压缩引擎、M5 校验规则 | 行覆盖 ≥ 80%,**Golden 断言 100% 通过** | 每次提交(CI 门禁) | | L2 | 全部 117 个接口 | 接口覆盖 100%,异常路径 ≥ 60% | 每日构建 | | L3 | 学生填报、班主任审核、管理员导出 | 3 条主流程 × 平台差异 | 每周 / 里程碑 | | L4 | 里程碑交付物 | 按 §6 签署表 | 里程碑节点 | ### 1.3 Golden Test Set(唯一权威测试集) | 项 | 内容 | |---|---| | 路径 | `D:\项目\202609机电填报系统\24中技数控1班\` | | 规模 | **28 人 × 4 类材料 = 112 个文件**,原始 **475 MB** | | 配套名单 | `考生线下报名信息导入模板(24中技数车1班班).xlsx`(表头**第 6 行**) | | 参照模板 | 官方空白模板(表头**第 1 行**),21 字段 | | 特点 | **不是构造数据**——含真实的乱命名、双扩展名、证件号错误、串档、低分辨率 | > ⚠️ **这个数据集不可替代**。所有"实测"结论都来自它;换成构造数据,双扩展名 56/112 这种数字就复现不出来。 > **必须纳入版本管理**(或至少保持只读副本),否则半年后无法回归。 --- ## 二、Golden Assertions(L1 · 可自动化) > 以下断言**直接从 P4 附录 B 的基线转写**。任何改动后必须全绿。 ### 2.1 命名规范化(M1) ```python # G-01 命名违规总数 assert count_files(failing_naming_rule) == 59 # 59 / 112 = 52.7% # G-02 双扩展名 assert count_files(lambda f: re.search(r'\.(jpg|jpeg|png){2,}$', f.name, re.I)) == 56 # G-03 规范化幂等性(关键!) for f in all_files: once = normalize(f.name, roster, material_def) twice = normalize(once.standard_name, roster, material_def) assert twice.standard_name == once.standard_name assert twice.issues == [] # 第二次不应产生任何 issue # G-04 双扩展名修复正确性 sample = "曾伊豪-361029200907295110身份证.jpg.jpg" assert normalize(sample).standard_name == "曾伊豪-361029200907295110-身份证.jpg" # G-05 材料别名归一 assert normalize("...个人申报表.jpg", slot="WORK_PROOF").material_name == "工作证明" # G-06 证件号抽取:不许暴力凑合法号 assert normalize("张三-111111111111111111.jpg").idcard_found == False # 非法号不得被"修好" # G-07 无名册时不猜姓名 assert normalize("乱名.jpg", roster=None).standard_name.startswith("未知姓名") is False # 用占位而非臆测 ``` ### 2.2 压缩引擎(M2) ```python # G-08 体积上限:全部达标(D1 决策,无例外) for f in a4_scans: # 28 张工作证明 out = compress(f, profile="P3") assert out.final_size_kb <= 1024, f"{f.name} 超标 {out.final_size_kb}KB" # G-09 质量未受损:均值与最大值 sizes = [compress(f, profile="P3").final_size_kb for f in a4_scans] assert 700 <= mean(sizes) <= 900 # 实测均值 806KB assert max(sizes) <= 900 # 实测最大 831KB # G-10 零强制压缩 assert count(out.forced == True for out in results) == 0 # G-11 证件照规格化 for f in photo_files: # 实测 984×1378 的 27 张 out = compress(f, profile="P1") assert (out.width, out.height) == (295, 413) assert out.dpi == 300 # G-12 截图不缩放、保持 PNG out = compress(school_roll_png, profile="P4") # 实测 1025×590 PNG assert (out.width, out.height) == (1025, 590) assert out.format == "PNG" # G-13 不放大(关键!) out = compress(small_idcard, profile="P2") # 实测 558×745 assert out.width <= 558 and out.height <= 745 assert out.issues_contains("VAL-20") # 体积下限保护 # G-14 EXIF 方向正确(不拉扁) out = compress(rotated_photo, profile="P2") assert out.width > out.height # 或按原图判断,绝不允许宽高互换 ``` ### 2.2b 文件归属(M1 · 归属校验) ```python # G-14b 归属:证件号 = 名册值 + 姓名错别字 → 不打扰学生 f = "扬晨-360825200904303215-一寸照片.jpg" # 名册为「杨晨」 r = normalize(f, roster=yangchen, material_def=photo1) assert r.standard_name == "杨晨-360825200904303215-一寸照片.jpg" assert r.student_facing_issues == [] # ★ 不产生面向学生的 issue assert r.logonly(NAME_DIFFERS) # 仅留痕 # G-14c 归属:乱名无号码 → 零提示(正常路径) r = normalize("微信图片_20260928103015.jpg", roster=yangchen, material_def=photo1) assert r.standard_name == "杨晨-360825200904303215-一寸照片.jpg" assert r.issues == [] # ★ 零提示 assert r.ownership.verdict == "NO_IDCARD_IN_NAME" # G-14d 归属:指向名册中的另一个人 → 阻断(最关键) f = "李四-360122200902044232-工作证明.jpg" # 该号码在名册里能查到「李四」 r = normalize(f, roster=zhangsan, material_def=work_proof) assert r.ownership.verdict == "BELONGS_TO_OTHER" assert r.ownership.block_submit == True # ★ 阻断 assert r.ownership.other_roster_name == "李四" assert r.ownership.can_force == False # ★ 没有"仍然继续" # G-14e 归属:陌生号 + OCR 佐证 → 不阻断 r = normalize("张三-111111111111111111-身份证.jpg", roster=zhangsan, ocr_idcard=zhangsan.id_card_no) assert r.ownership.verdict == "UNKNOWN_IDCARD" assert r.ownership.block_submit == False # OCR 佐证后放行 assert r.issues_contains("VAL-02", severity=1) # G-14f 归属:陌生号 + 无 OCR → 阻断 r = normalize("张三-111111111111111111-身份证.jpg", roster=zhangsan, ocr_idcard=None) assert r.ownership.block_submit == True # G-14g 校验位失败 → 自动改,不阻断(实测 2 例:于豪、杨晨) r = normalize("于豪-360103200805166169-身份证.jpg.jpg", roster=yuhuo, material_def=idcard) assert r.ownership.verdict == "CHECKSUM_INVALID" assert r.ownership.block_submit == False assert r.standard_name == "于豪-360103200805186619-身份证.jpg" # G-14h ★ 回归:绝不把"别人的材料"静默洗白 # 这是本设计最重要的一条断言——它防止的是数据污染,不是体验问题 for f in all_files: r = normalize(f, roster=current_student, material_def=md) if r.ownership.verdict == "BELONGS_TO_OTHER": assert r.standard_name != "{当前学生名}-{当前学生证件号}-..." or r.block_submit # 即:凡判为他人材料,必须阻断,不得生成一份"名字正确"的输出 ``` ### 2.3 材料识别(M3) ```python # G-15 文件名层命中率 assert detect_by_name_accuracy(all_files) == 1.0 # 112/112 # G-16 形态层分流派准确率 assert detect_by_shape_accuracy(all_files) >= 0.99 # 实测 99.1% # G-17 零文字判据 assert detect(photo1).material_code == "PHOTO1" assert detect(photo1).reason == "整图文字量 < 5 字" # G-18 通用词禁入表(关键!) assert "身份证号" not in STRONG_FEATURES for f in all_files: # 不得因为文件里出现"身份证号"四字就判成 IDCARD assert not (contains(f, "身份证号") and detect(f).material_code == "IDCARD" and detect(f).source == "NAME" and detect(f).confidence >= 0.9) # G-19 识别失败必须放行 assert detect(black_image).material_code is None assert can_submit(submission_with(black_image)) == True # 不阻断! # G-20 VLM 触发率在预算内 assert 0.02 <= vlm_trigger_rate(all_files) <= 0.10 ``` ### 2.4 校验与串档(M5) ```python # G-21 证件号异常总数 issues = run_validation(all_submissions) assert count(issues, rule="VAL-02") + count(issues, rule="VAL-01") == 5 # G-22 自动/人工分流 assert count(issues, rule="VAL-02", auto_fixed=True) == 3 assert count(issues, rule="VAL-02", auto_fixed=False) == 2 # G-23 疑似串档(最关键的一条) assert count(issues, rule="VAL-19") == 1 s = the_cross_file_issue assert s.need_human == True and s.auto_fixed == False # 绝不自动改 # G-24 跨文件一致性不误报 assert count(issues, rule="VAL-04") == 0 # 除串档外无其他不一致 # G-25 低分辨率检出 assert count(issues, rule="VAL-08") == 1 assert issues_for_rule("VAL-08")[0].dimension == "558×745" ``` ### 2.5 导出(M6) ```python # G-26 导出总量 result = export(task_id, scope={only_approved: False}) assert 30 <= result.size_mb <= 45 # 实测 ≈37MB # G-27 目录树结构 assert result.tree["24中技数控1班"]["工作证明"][0].name \ == "曾伊豪-361029200907295110-工作证明.jpg" # G-28 汇总 Excel 列对齐 xlsx = result.summary_excel assert xlsx.header == OFFICIAL_21_COLUMNS # A–U 严格对齐 # G-29 中文文件名编码 with unzip(result.zip) as z: for name in z.namelist(): assert name.encode('utf-8').decode('utf-8') == name # 无乱码 ``` ### 2.6 断言的"红灯"处理规则 | 情况 | 处理 | |---|---| | G-01~G-10 任一失败 | **阻断发布**。这是产品核心承诺 | | G-11~G-14 失败 | **阻断发布**。直接影响认定机构接收 | | G-15~G-20 失败 | 视偏差幅度:>5% 阻断;≤5% 记录并评估 | | G-21~G-25 失败 | **阻断发布**。漏掉串档会流出错误数据 | | G-26~G-29 失败 | **阻断发布**。导出包是最终交付物 | | 基线数字本身需调整 | **必须走变更评审**,在 P4 附录 B 留版本记录 | --- ## 三、端到端主流程用例(L3) ### 3.1 主流程 A · 学校收集(学生 → 班主任 → 管理员) | 步 | 操作 | 期望 | 对应原型 | |:--:|---|---|---| | A1 | 学生扫码进小程序 | 进入门禁页 | ① | | A2 | 输入名单上的姓名 + 证件号 | 通过,显示姓名与班级 | ② | | A3 | 刻意输错一位证件号 | **统一提示**"没有找到你的信息",不透露哪项错 | ③ | | ~~A4~~ | ~~点"申请加入名单"~~ | **已取消**(2026-10 需求收敛:学生端不再有自助申请入口;门禁失败只提示核对 + "请联系老师",且不展示任何联系方式) | — | | A5 | 正确登录后进填报页 | 18 个字段已预填,标注"老师已填,请核对" | ④ | | A6 | 修改"联系电话" | 可改,`changedFields` 记录 phone | ④ | | A7 | 尝试改"身份证号" | **只读**,点击提示"如有误请联系老师" | ④ | | A8 | 选"微信聊天记录"传工作证明(双扩展名乱名) | 上传成功,结果卡显示**命名前后对比** | ⑤⑦ | | A9 | 查看结果卡 | 显示标准名、体积 17.5MB→512KB、`VAL-06` 信息级"格式已转换" | ⑦ | | A10 | 传"学籍表"时误传工作证明 | 提示"这张图看起来是工作证明,要移过去吗?",**默认不移** | ⑧ | | A11 | 点"移到工作证明" | 文件移动,学籍表槽位变空 | ⑧ | | A12 | 故意不传必传材料直接提交 | 被拦,**列出缺哪项**,给出"去上传" | — | | A13 | 补齐后提交 | 成功,状态"等待李老师初审" | ⑨ | | A14 | 班主任打开待审列表 | 该生卡片为**绿色**(无问题) | ② | | A15 | **右滑该卡片** | 通过,卡片移出列表,显示"已通过" | ② | | A16 | 另一个学生有红色问题 | 卡片**灰色/不可滑**,点击进详情 | ②③ | | A17 | 尝试对该生执行"一键通过" | **被跳过并回报原因**"存在红色问题" | ④ | | A18 | 驳回该生(选"材料不清晰") | 需**二次确认**;提交后学生收到通知 | ⑤ | | A19 | 学生查看消息 | 消息中心有"你的材料被退回" | ⑩ | | A20 | 学生重新上传后提交 | 状态回到"待初审" | — | | A21 | 管理员终审 | 通过后状态"已通过" | ⑥ | | A22 | 管理员导出 | 前置检查通过 → 打包 → 下载 zip | ⑦ | | A23 | **Windows 资源管理器解压** | **中文名不乱码**,目录树正确 | — | ### 3.2 主流程 B · 社会收集(**不做**) > ⚠️ **本节作废**(2026-10 取消二期,见 PRD「一期范围声明」):`collectMode=2` 传入直接返回 `40044`。保留描述仅供追溯,**不要再按它验收**。 | 步 | 操作 | 期望 | |:--:|---|---| | B1 | 用社会收集任务重跑 A1~A13 | 提交后状态直接是"待终审" | | B2 | 班主任打开待审列表 | **看不到该任务**(社会收集不经班主任) | | B3 | 管理员终审 → 导出 | 正常 | ### 3.3 主流程 C · 历史数据整理器 —— **已取消**(2026-10) | 步 | 操作 | 期望 | |:--:|---|---| | C1 | 上传 `24中技数控1班.rar`(436MB) | 解析出 28 人 4 类 | | C2 | 查看体检报告 | 命名违规 **59**、体积超标 **31**、证件号异常 **5**、疑似串档 **1** | | C3 | 读报告结论 | "**90 个系统全自动处理,只有 1 个必须你亲自判断**" | | C4 | 下载规范化包 | 475MB→**37MB**,目录树正确 | | C5 | 点"导入为正式任务" | 生成任务 + 名册 + 112 个文件全挂上 | | C6 | 进该任务看数据 | 真实、无占位、可直接导出 | ### 3.4 异常与边界用例(23 条) | # | 场景 | 期望 | |:--:|---|---| | E1 | 门禁连错 11 次 | 第 11 次返回 `40071`,提示稍后再试 | | E2 | 同一 IP 20 人同时门禁 | **全部通过**(NAT 不误伤) | | E3 | 并发 100 请求抢同一名册项 | **恰好 1 个成功**,99 个 `40072` | | E4 | 门禁失败后查库 | **无明文证件号**,只有 hash | | E5 | 学籍表 4 项材料证件号均与名册不符 | 标 `VAL-19` 疑似串档,**不自动改** | | E6 | 学生提交后撤回,再改再提交 | 正常;驳回次数不因撤回增加 | | E7 | 任务暂停中 | 学生看到"已暂停",**已提交的仍可继续审核** | | E8 | 任务截止后 | 学生不能提交,已提交的可审 | | E9 | 归档有未审单 | 拒绝,返回 `40023` | | E10 | 导出前有 1 个超标文件 | **导出按钮禁用**,指出"张三的工作证明 1.8M" | | E11 | 已导出后撤回审核 | 拒绝,返回 `40033` | | E12 | 表单版本冲突(两人同时改) | `40025` + 返回最新 schema | | E13 | 删除内置字段 | `40026`,提示可改标签/顺序 | | E14 | 报考范围未覆盖全员 | `40024` + 返回差集 | | E15 | 名字含罕见字(生僻字) | 不误判、不阻断 | | E16 | 上传 8000×10000 超大图 | 触发熔断转异步,**不超时** | | E17 | OCR 服务不可用 | **降级为人工填写**,不阻断 | | E18 | VLM 服务不可用 | 取前三层结果或放行 | | E19 | 微信订阅消息额度耗尽 | **静默降级**,学生端无异常提示 | | E20 | 网络中断后重进 | 草稿保留,续填 | | E21 | 申请提交后管理员未处理,任务截止 | 申请自动置为已失效,学生收到提示 | | ~~E22~~ | ~~两人同时申请同一证件号~~ | **已取消**(2026-10:加入申请模块删除) | | ~~E23~~ | ~~管理员在审批页停留很久后点通过~~ | **已取消**(2026-10) | | E24 | 学生传了同学的材料后点提交 | **被阻断**,提示文件名对应的是谁,**无"仍然继续"** | | E25 | 学生把文件名改成自己的名字后重传,内容仍是同学的 | 证件号/OCR 仍会暴露 → 阻断或转人工(**改名绕不过去**) | | E26 | 归属问题在异步 OCR 后才判定可放行 | 自动解除阻断 + 推送通知;**阶段二只解除、不新增阻断** | | E27 | 文件名形如 `微信图片_2026....jpg`(无姓名无号码) | 按名册补全,**零提示**(这是最常见的情况) | --- ## 四、功能验收清单(L2 · 按模块) > 编号规则:`F-{模块}-{序号}`。每条必须有明确的**可观测结果**。 ### F-100 租户与组织(FR-100) | # | 验收项 | 期望 | |:--:|---|---| | F-100-1 | 开通租户 | 生成 `tenantCode`,同时创建首个管理员,返回初始密码 | | F-100-2 | 组织树三层 | 学校/院系/班级,`ancestorPath` 自动维护 | | F-100-3 | 删除被占用组织 | 拒绝,返回占用清单 | | F-100-4 | **租户隔离** | A 租户 token 访问 B 租户数据 → `40301` | | F-100-5 | 用量统计 | 学生数/存储/OCR/短信四项,超限告警 | ### F-200 账号与角色(FR-200) | # | 验收项 | 期望 | |:--:|---|---| | F-200-1 | PC 登录 | 连续失败 2 次后**出现图形验证码** | | F-200-2 | 账号锁定 | 5 次失败锁定,提示剩余时间 | | F-200-3 | 小程序登录 | `code2session` 成功,绑定手机号 | | F-200-4 | 一班主任多班 | 看板聚合展示多个班 | | F-200-5 | 批量导入用户 | 返回体检报告 + 初始密码清单 | | F-200-6 | 停用有在办任务的用户 | 拒绝 `40014` | | F-200-7 | **角色列表**(一期功能) | 内置 + 自定义都列出;内置 `editable=false`、权限与菜单**取代码**;带 `userCount` | | F-200-8 | **自定义角色的权限真的生效** | 只授 `task:view` 的账号能读任务列表、建任务得 `40301`;**改授权后同一个 token 立刻生效**(不必重新登录);停用角色同理 | | F-200-9 | **内置角色只读** | 改 / 删 / 授权 / 停用内置角色一律 `40001` | | F-200-10 | **越权与自锁防线** | 学校租户授予平台级权限码 / 配置平台专用菜单 / 建平台运营类角色 → `40301`;发 `PLATFORM_ADMIN` → `40301`;去掉自己最后的 `role:manage` → `40001` | | F-200-11 | **角色删除守卫** | 还有账号在用 → `40045`;解绑后可删;删后该账号重新登录不再有那些权限 | | F-200-12 | **菜单随角色走** | 改角色的可见菜单后,`/auth/me`(**以及登录响应**)的 `menus` 同步变化 | ### F-300 收集任务(FR-300) | # | 验收项 | 期望 | |:--:|---|---| | F-300-1 | 任务 CRUD 与状态机 | 草稿→发布→暂停→恢复→截止→归档 | | F-300-2 | **暂停 ≠ 截止** | 暂停可恢复、审核继续;截止不可逆 | | F-300-3 | 发布前置校验 5 项 | 任一不通过阻断,`force` 无法绕过 | | F-300-4 | 任务克隆 | 名册复制但**清空绑定与提交状态** | | F-300-5 | 二维码/短链 | 带租户+任务+班级参数,截止后扫码提示 | | ~~F-300-6~~ | ~~社会收集审核流~~ | **不做**(2026-10 取消二期;传 `collectMode=2` 返回 `40044`) | ### F-400 动态表单(FR-400) | # | 验收项 | 期望 | |:--:|---|---| | F-400-1 | 21 个内置字段 | 可改标签、可调顺序、**不可删除** | | F-400-2 | 扩展字段 | 存 `extras_json`,**不参与统计** | | F-400-3 | 条件显示 | 三条级联全部生效 | | F-400-4 | 表单版本化 | 改后旧单不受影响,`formVersion` 各自保存 | | F-400-5 | 版本冲突 | `40025` + 返回最新 schema | | F-400-6 | 学生三态字段 | 锁定/可改留痕/正常 三种表现正确 | ### F-500 名册(FR-500) | # | 验收项 | 期望 | |:--:|---|---| | F-500-1 | **表头自动探测** | 第 1 行与第 6 行两份真实文件都识别正确 | | F-500-2 | 表头候选回传 | 多候选时返回并让用户确认 | | F-500-3 | 无法定位表头 | `40028` + 返回前 10 行预览 | | F-500-4 | 体检报告 | 证件号校验位/重复/必填缺失三项检出 | | F-500-5 | `dryRun` 模式 | 只体检不落库 | | F-500-6 | 追加导入去重 | 按证件号合并,新人立即可填 | | F-500-7 | 改名册证件号留痕 | 操作日志可查改前改后(**脱敏**) | | F-500-8 | 已绑定名册项不可删 | `40029` | | F-500-9 | **门禁匹配** | 正确通过 / 错误统一提示 / 形近字确认 | | F-500-10 | 门禁限流 | openid 10 次/小时;IP 20 次/分钟 | | F-500-11 | **并发绑定** | 100 并发抢同一项,恰好 1 个成功 | | F-500-12 | 失败不留明文 | 查库确认只有 hash | | F-500-13 | 已绑定提示 | `40072`,**这一条明确提示** | ### ~~F-509 名单加入申请(客户确认新增)~~ —— **已撤销**(2026-10 需求收敛) > ⚠️ 本节 8 条验收全部作废:加入申请通道(含 `40037`/`40038`/`40039`/`40040`/`40042`) > 连同模块、表、接口、学生端页面一并删除。 > **替代口径**:门禁失败页只有「重新填写」+ 一句"请联系老师把你加入名单", > 名单外的人由老师在任务的「名单管理」里手工加人;系统内不展示任何联系方式。 ### F-600 材料与规则(FR-600) | # | 验收项 | 期望 | |:--:|---|---| | F-600-1 | 命名模板配置 | 变量可拖拽,支持自定义连接符与大小写 | | F-600-2 | 命名引擎 | 双扩展名/非法字符/缺分隔符/别名归一 全部修复 | | F-600-3 | **幂等性** | 跑两次结果相同、第二次无 issue | | F-600-4 | 证件号三源裁决 | 5 种组合按裁决表执行 | | F-600-5 | **NEED_HUMAN 不自动改** | 两票对一票场景保持原值 | | F-600-6 | 格式白名单 | 非白名单自动转换 | | F-600-7 | 体积上下限 | 超限压缩;过小提示"可能不是原图" | | F-600-8 | 五档压缩 | P1 固定尺寸 / P4 不缩放 各自正确 | | F-600-9 | 材料识别四层 | 各层命中率达标 | | F-600-10 | 冲突提示 | 只提示不自动移位置 | | F-600-11 | 散图分拣 | 唯一允许建议落位的场景 | | F-600-12 | 来源引导语 | 学生端可见自定义文案 | | F-600-13 | **无拍照入口** | 只有聊天记录/相册/手机文件三种来源 | | F-600-21 | **材料范围** | 只支持 `PHOTO1`/`IDCARD`/`SCHOOL_ROLL`/`WORK_PROOF` 四类;`XJW_BA`/`OTHER_PROOF` **读得到、选不到**(2026-10 取消,不再"二期启用") | | F-600-22 | **一期收集模式** | 创建 `collectMode=2` 的任务被拒绝(`40044`) | | F-600-14 | **归属校验:他人材料** | 文件名证件号指向名册中另一人 → **阻断提交**,提示"文件名对应的是「李四」" | | F-600-15 | **归属校验:无"仍然继续"** | 归属问题的提示里**不提供**"仍然使用/强制提交"按钮(`canForce=false`) | | F-600-16 | 归属校验:乱名无号码 | `微信图片_xxx.jpg` → 按名册补全,**零提示** | | F-600-17 | 归属校验:姓名错别字 | 证件号正确 + 姓名形近 → 按名册输出,**学生端无任何提示**,仅服务端留痕 | | F-600-18 | 归属校验:陌生号 + OCR 佐证 | OCR = 名册 → 自动放行,信息级 | | F-600-19 | 归属校验:校验位失败 | 校验位失败 → 自动按名册,信息级,**不阻断** | | F-600-20 | **姓名不做独立规则** | 构造"证件号正确但姓名完全不同"的文件 → 仍按名册输出,**不阻断**(归属由证件号定) | ### F-800 审核(FR-800) | # | 验收项 | 期望 | |:--:|---|---| | F-800-1 | 待办分级 | 红/黄/绿三色,`batchApprovable` 服务端判定 | | F-800-2 | **只右滑通过** | 左滑**不绑定驳回** | | F-800-3 | 驳回需二次确认 | 不能误触 | | F-800-4 | 驳回原因快选 | 10 条字典 + R999 手填 | | F-800-5 | **批量通过只带绿项** | 红黄被跳过并回报原因 | | F-800-6 | 红色问题不可通过 | `40032` | | F-800-7 | 审核时间轴 | 含提交/初审/终审/撤回/代传 | | F-800-8 | **撤回** | 未导出可撤;已导出 `40033` | | F-800-9 | 代传材料留痕 | `source=4`,学生端可见 | | F-800-10 | 驳回超上限升级 | 达 3 次通知管理员 | | F-800-11 | 变更字段高亮 | 学生改过的预填字段在详情高亮 | ### F-900 导出(FR-900) | # | 验收项 | 期望 | |:--:|---|---| | F-900-1 | **前置检查** | 有不合格文件 → `canExport=false` → 按钮禁用 | | F-900-2 | blocking 定位 | 精确到"谁的哪一项" | | F-900-3 | 目录树预览 | 导出前可见最终形态 | | F-900-4 | 打包总量 | ≈37MB,压缩率 ≈92% | | F-900-5 | **中文文件名** | Windows 解压不乱码 | | F-900-6 | 汇总 Excel | 严格对齐官方 21 列 | | F-900-7 | 分卷 | ≤50MB,不切分单个文件 | | F-900-8 | 快照一致性 | 打包中数据变化不影响产物 | | F-900-9 | 断点续传 | 失败后从断点继续 | | F-900-10 | 过期清理 | 7 天自动清理,重导不重复计费 | ### F-1000 通知(FR-1000) | # | 验收项 | 期望 | |:--:|---|---| | F-1000-1 | 订阅消息额度登记 | 各节点索取授权并记录 | | F-1000-2 | **降级链** | 订阅→公众号→短信→站内信,**静默** | | F-1000-3 | 自动催报 | 截止前 7/3/1 天 + 当日,**仅催未提交者** | | F-1000-4 | 催报去重 | `excludeRecentHours=24` 防骚扰 | | F-1000-5 | 模板变量 | 白名单外的变量渲染为空并告警 | | F-1000-6 | 消息中心 | 学生端 + 教师端可读可标记 | ### F-1200 / F-1300 / F-1400 | # | 验收项 | 期望 | |:--:|---|---| | F-1200-1 | 字典管理 | 6 个内置字典,有下级项的不可停用 | | F-1200-2 | 职业字典 | **只做拼写校验,不强约束**(Excel 模板本就无下拉) | | F-1200-3 | 操作日志 | 9 类敏感操作全留痕,证件号脱敏存储 | | F-1200-4 | 查看完整身份证号 | 单独接口、单独留痕 `VIEW_IDCARD_FULL` | | F-1300-1 | 用量与套餐 | 四项用量 + 超限告警 | | ~~F-1400-1~~ | ~~整理器上传解析~~ | **已取消**(2026-10:模块删除) | | F-1400-2 | **体检报告数字** | 59/31/5/1 全部命中 | | F-1400-3 | 报告结论 | autoHandled 90 / needManual 1 | | F-1400-4 | 结果导入为任务 | 名册 + 112 文件一次成型 | --- ## 五、交互与视觉验收(对照原型) > 原型是视觉与交互的**唯一基准**。实现与原型不一致时,以原型为准(除非原型本身有错,走变更)。 | 原型 | 屏数 | 必须一致的要素 | |---|:--:|---| | `student-app.html` | 10 | 门禁失败页文案(不透露哪项错)、三态字段样式、结果卡命名前后对比、无拍照入口 | | `teacher-app.html` | 9 | 卡片红黄绿三色、**只能右滑**、驳回二次确认、按钮 44px 触达区 | | `admin-web.html` | 10 | 表格信息密度、目录树预览、设计器内置字段锁标记、角色与授权矩阵 | | `index.html` | — | 三端入口 | ### 视觉验收要点 | # | 项 | 标准 | |:--:|---|---| | V1 | 移动端触达区 | 主要按钮高度 ≥ 44px | | V2 | 色彩语义 | 红=阻断 / 黄=警告 / 绿=通过,全局一致 | | V3 | 无 emoji | 全部用内联 SVG 图标,1.7px 描边 | | V4 | 零外部依赖 | 断网可打开(原型约束,生产可放宽) | | V5 | 数字口径一致 | 教师端 **28 = 15+4+3+1+5**,完成率 **82% 是已提交率** | | V6 | 体积表述 | 475MB → **37MB**,压缩率 **92%**(不得出现 22.4MB) | --- ## 六、性能与安全测试 ### 6.1 性能 | # | 场景 | 指标 | 说明 | |:--:|---|---|---| | P1 | 门禁匹配(5000 人名册) | **< 50ms** | 走 `id_card_hash` 索引 | | P2 | 单文件处理(A4 扫描件) | **< 3s** | 实测 PIL 约 2s,libvips 更快 | | P3 | 批量处理 4 个文件 | **< 8s** | 建议走异步 | | P4 | 导出 112 文件 | **< 60s** | — | | P5 | 导出 5000 文件 | **< 15min** | 分卷 + 流式 | | P6 | 门禁并发 100 | 全部响应 < 1s,恰好 1 个绑定成功 | **必测** | | P7 | 上传并发 200 | 无失败,p99 < 2s | — | | P8 | 任务统计查询(10 万行) | < 500ms | 走正式列索引 | | P9 | 页面首屏(小程序) | < 1.5s | — | ### 6.2 安全 | # | 项 | 验证方法 | |:--:|---|---| | S1 | 跨租户越权 | 用 A 租户 token 请求 B 租户资源,全部 `40301` | | S2 | 门禁防枚举 | 20 个不同证件号试同一 IP → 触发异常告警并拉黑 | | S3 | **失败不泄露** | 抓包对比"姓名错"与"号码错"的响应,**字节级一致** | | S4 | 失败不留明文 | 查库 + 查日志,无明文证件号 | | S5 | 脱敏覆盖 | 遍历所有响应,确认 `idCardNo` 从不出现,只有 `idCardMask` | | S6 | 签名 URL 过期 | 过期后返回 `40302` | | S7 | 幂等键重放 | 同 key 重复提交返回首次结果,不产生脏数据 | | S8 | 上传文件类型伪造 | 改扩展名的可执行文件被拒 | | S9 | 路径穿越 | 文件名含 `../` 被清理 | | S10 | 开放接口加签 | 篡改 body / 过期 timestamp / 重放 nonce 全部拒绝 | | S11 | SQL 注入 | 全参数化,渗透测试 | | S12 | 越权访问他人填报单 | 学生 A 访问学生 B 的 submissionId → 403 | --- ## 七、里程碑验收签署表(L4) > 每个里程碑结束时由**项目经理 + 技术负责人 + 客户代表**三方签署。 | 里程碑 | 签署项 | 交付物 | 签署 | |:--:|---|---|:--:| | M0 | 登录/组织/用户可用,跨租户拦截 | 内网演示 | ☐ | | M1 | 能配出完整任务;内置字段不可删 | 演示 + 截图 | ☐ | | M2 | 真实名单导入正确;门禁并发测试通过 | **并发压测报告** | ☐ | | M3 | **Golden Assertions G-01~G-14 全绿** | 回归报告 | ☐ | | M4 | G-15~G-25 全绿;识别报告 | 112 文件识别报告 | ☐ | | M5 | 三端审核闭环;一键通过跳过红项 | 真机录屏 | ☐ | | M6 | G-26~G-29 全绿;**Windows 解压验证** | 导出包样本 | ☐ | | M7 | 性能 P1~P9 达标;安全 S1~S12 通过 | 压测 + 渗透报告 | ☐ | ### 上线前必须全部满足(Go/No-Go) | # | 条件 | 不满足的后果 | |:--:|---|---| | 1 | **全部 29 条 Golden Assertions 通过** | No-Go | | 2 | 性能 P1、P2、P6 达标 | No-Go(P6 是安全底线) | | 3 | 安全 S3、S4、S5、S11 通过 | No-Go | | 4 | 小程序主体资质获批 | No-Go(无法上线) | | 5 | 认定机构书面接受 300dpi | No-Go(材料可能被拒收) | | 6 | 真实数据跑通一次完整导出并被客户确认 | No-Go | | 7 | 回滚预案就绪(能退回上一版本) | 可延期但需记录 | --- ## 八、测试环境要求 | 项 | 要求 | |---|---| | 数据库 | PostgreSQL 16(DDL 已实测) | | 图像库 | 支持 300dpi DPI 写入、EXIF 方向读取 | | 测试数据 | **Golden Test Set 只读副本**,纳入版本管理 | | 真机 | iOS + Android 各 ≥ 1 台;Windows 用于验证解压不乱码 | | 并发工具 | 支持 100 并发的压测工具 | | 字符集 | 文件名测试必须覆盖中文 + 双扩展名 + 全角字符 | ### 易被忽略的三个测试盲区 | # | 盲区 | 为什么容易漏 | |:--:|---|---| | 1 | **Windows 解压中文名** | 开发用 Mac/Linux 测试一切正常,交付到客户才炸 | | 2 | **并发绑定的临界窗口** | 单机测试永远不复现,只有压测才暴露 | | 3 | **「无」是合法值** | 测试数据里没人填"无",上线后真实数据一填就错 | --- ## 变更记录 | 版本 | 日期 | 变更 | |---|---|---| | **V1.1** | **2026-09-28** | **客户确认后修订:修正「文件归属」设计缺陷**。原设计把"文件名姓名/证件号与名册不符"处理为"自动按名册修正"(信息级),**会把传错的文件静默洗白**。修正为:
① 新增 **7 条归属断言 G-14b~G-14h**(Golden 29 → **36**);② 判定优先级改为 **证件号反查名册 > OCR 内容 > 姓名(仅辅助)**;③ 新增 **VAL-21 文件归属校验(阻断,且无"仍然继续")**;④ **姓名不再作为独立规则**(客户确认);⑤ 新增 **7 条功能验收 F-600-14~20**(104 → **111**)与 **4 条异常边界 E24~E27**(23 → **27**)
**修订后总计:36 条断言 / 111 条功能验收 / 27 条异常边界 / 6 条视觉 / 9 项性能 / 12 项安全** | | V1.0 | 2026-09-28 | 首版。**29 条 Golden Assertions**(可直接写成自动化)、3 条端到端主流程(32 步)、23 条异常边界、**104 条功能验收项**(含 F-509 共 19 条)、6 条视觉验收、9 项性能、12 项安全;含里程碑签署表与 7 条 Go/No-Go 条件 | --- > **实现说明(2026-09-28)**:本文件中的 `40007b` / `40007c` 已规范化为 **`40071`** / **`40072`**。 > 原因:`code` 按 §1.3 约定是数字,带字母后缀会在 JSON 里变成字符串,破坏前端 `code === 0` 的判断。