# 验收测试清单
> 版本: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` 的判断。