test(scripts): 36 个 E2E 与 6 个机械校验工具(1548 条断言)+ 一键交付自检

新增:check-api-parity(契约 121 接口 ↔ 控制器,自带「必须恰好 121」自检)、
check-entity-columns(基类要求的列 vs 迁移建表列,F-35 那个坑踩了四次之后上的工具)、
check-manual(手册引用的错误码与提示文案对得上代码)、check-test-profile(test profile 真起一次)、
e2e-prodguard、load-test(四场景、按端点分别统计)、verify-all(一条命令跑完交付自检)、
dev-seed-audit(界面验收用的演示数据),以及 M1~M9 各轮 E2E。

压测抓到一个真缺陷并修掉(F-92):门禁 IP 限流原来计「所有请求」,于是一个班连同一个
WiFi 时 60 个不同学生同时核验只过了 20 个 → 改为只计失败 + 单独的成功上限,20/60 → 60/60。

度量工具的偏差会伪装成产品缺陷(F-60/F-77/F-97),所以每个脚本都断言「动作自己的结论字段」
(submitted / created / canExport),而不是 HTTP 200 + code 0。

验证:verify-all.py --quick 5/5;--with-e2e 见 docs/09 §8。
This commit is contained in:
2026-09-30 09:02:57 +08:00
parent 6945f194b5
commit 30c3714d35
26 changed files with 9190 additions and 1 deletions
+88
View File
@@ -0,0 +1,88 @@
# -*- coding: utf-8 -*-
"""手册与代码的一致性检查(客服话术的可信度靠它)。
## 为什么要查这个
手册(`docs/10-用户手册与客服话术.md`)的价值**完全建立在
"手册里那句话 == 用户屏幕上那句话"**上 —— 客服照着它回答,
对不上就会开始自己编,那比没有手册更糟。
而**手册是文档、文案在代码里**:改一次代码文案,手册就静默失准,
且没有任何东西会红。这与"首页在说一件不真的事"(F-82)是同一类问题。
所以这里把它变成机械检查,与 `check-api-parity.py`(契约↔路由)、
`check-entity-columns.py`(实体↔DDL)并列 —— 都是"文档与代码是否还对得上"。
## 查两件事
1. 手册里引用的每个错误码(反引号包着的 5 位数)在 `ErrorCode.java` 里**确实存在**;
2. 手册里"客服会照着念"的那些提示文案在代码里**逐字存在**。
⚠️ 它**不检查**手册有没有漏掉新错误码 —— 那要靠人。
它拦的是"手册写了代码里没有的东西"(这会让客服查到一半卡住)。
跑法:python scripts/check-manual.py
"""
import io
import re
import sys
sys.stdout.reconfigure(encoding="utf-8")
manual = io.open('docs/10-用户手册与客服话术.md', encoding='utf-8').read()
codes_src = io.open('backend/xt-common/src/main/java/cn/tianbaotong/fill/common/api/'
'ErrorCode.java', encoding='utf-8').read()
# ① 码:三个捕获组 → 自己建索引
rows = re.findall(r'([A-Z_]+)\((\d{4,5}),\s*"([^"]*)"\)', codes_src)
declared_by_code = {code: (name, msg) for name, code, msg in rows}
print("ErrorCode 里声明了 %d 个码" % len(declared_by_code))
used = sorted(set(re.findall(r'`(\d{5})`', manual)))
print("手册引用了 %d 个码" % len(used))
bad = []
for c in used:
if c in declared_by_code:
name, msg = declared_by_code[c]
print(" ✓ %s %-24s %s" % (c, name, msg[:44]))
else:
bad.append(c)
print(" ✗ %s **ErrorCode 里没有这个码**" % c)
# ② 关键文案
key_phrases = [
"没有找到你的信息,请核对后重试",
"操作过于频繁,请稍后再试",
"该信息已被登记",
"你已在名单中,请返回重新核验",
"你已提交过申请,请耐心等待",
"这份材料可能不是你的,请重新上传",
"已进入审核,需先撤回才能删材料",
"已通过终审,不可撤回",
"任务已截止、已归档或已暂停",
"驳回次数已达上限",
"存在不合格文件,禁止导出",
"你的材料被退回了",
"导出包已过期",
"这个学生还没扫码核对身份",
]
src_all = ""
import glob
for p in glob.glob('backend/**/*.java', recursive=True):
try:
src_all += io.open(p, encoding='utf-8').read()
except Exception:
pass
print("\n核对手册里引用的提示文案(%d 句):" % len(key_phrases))
missing = []
for ph in key_phrases:
if ph in src_all:
print(" ✓ %s" % ph)
else:
missing.append(ph)
print(" ✗ %s —— **代码里找不到这句**" % ph)
print("\n结果:错误的码 %d 个,找不到的文案 %d 句" % (len(bad), len(missing)))
sys.exit(1 if (bad or missing) else 0)