Files
xiaotutongxue/docs/06-验收测试清单.md
lovife 7df078e0ea docs: 同步交接摘要 / PRD / 原型,并记录品牌改名「深学在线」
- 交接摘要补 §一·三十五~§一·三十八:角色与授权、需求收敛(4 角色 / 10 菜单 / 取消二期)、
  PC 端重搭后的 UI 修复与卡片式基线、品牌改名与新标识
- PRD 与功能逻辑卷只改标题行与产品代号线;**文件名不动** —— verify-docs.py 按路径读,
  全库还有 20+ 处按文件名引用,改名会连带炸脚本
- 原型 index / admin-web / teacher-app / student-app 的品牌区改「深学在线」,
  admin-web 的 .mark(10 处)换成内嵌的新标 data URI,保持原型"单文件自包含"的约定
- 根 AGENTS.md 与 frontend-pc 的规范同步:接线检查补第 ⑨ 项(SVG 资产是不是合法 XML ——
  SVG 注释里出现连续两个减号就 not well-formed,图标裂开而控制台一句话都不报)、
  「最容易写错的约定」6 -> 7 条(新增品牌标)
- deploy/docker-compose.yml 只改头注释里的项目名(compose 的 name 与七牛对象前缀都没动)
2026-10-10 16:49:04 +08:00

605 lines
33 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 验收测试清单
> 版本: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** | **客户确认后修订:修正「文件归属」设计缺陷**。原设计把"文件名姓名/证件号与名册不符"处理为"自动按名册修正"(信息级),**会把传错的文件静默洗白**。修正为:<br>① 新增 **7 条归属断言 G-14b~G-14h**(Golden 29 → **36**);② 判定优先级改为 **证件号反查名册 > OCR 内容 > 姓名(仅辅助)**;③ 新增 **VAL-21 文件归属校验(阻断,且无"仍然继续")**;④ **姓名不再作为独立规则**(客户确认);⑤ 新增 **7 条功能验收 F-600-14~20**(104 → **111**)与 **4 条异常边界 E24~E27**(23 → **27**)<br>**修订后总计: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` 的判断。