Files
xiaotutongxue/scripts/verify-docs.py
T
lovife 9d7a8ca845 feat(m0): 后端骨架 + 账号体系(M0 验收 36/36 通过)
技术栈(已在 4.1.1 上实测跑通,非纸面选型)
  Java 21 + Spring Boot 4.1.1 + MyBatis-Plus 3.5.17(boot4) + PostgreSQL 16.15
  + Redis 7 + Flyway 12.4 + springdoc 3.1.1;前端定 Vue 3 + Element Plus + uni-app

交付内容
  M0-01 基础设施     deploy/docker-compose.yml(PG + Redis + MinIO,容器名与端口避让本机既有项目)
  M0-02 后端骨架     xt-common + xt-server;统一响应 / 全局异常 / 58 个错误码 /
                     多租户拦截 / 软删除 / 字段自动填充 / AES-GCM / HMAC 索引 / RSA / JWT
  M0-05 租户        开通租户(一次完成建租户 + 发 6 个内置角色 + 建首个管理员)、列表、详情
  M0-06 组织        CRUD + 组织树 + ancestor_path 自动维护 + 占用校验
  M0-07 PC 登录     RSA 解密 + 图形验证码 + 失败锁定 + 等时比较防账号枚举 + 审计日志
  M0-08 登出/当前用户  Token 黑名单(存 SHA-256,TTL 到自然过期)
  M0-01 数据库      Flyway 2 个迁移 → 26 张表(20 业务 + 5 标准 + 1 迁移记录)

安全姿态
  · 租户拦截器【默认拒绝】:匿名请求触及租户表直接抛 40101,不做「不加条件放行」
  · 权限码定义在代码里(PermissionDef / BuiltinRole),不建 sys_permission 表,杜绝前后端漂移
  · 匿名接口只靠 @PublicApi 放行,不做路径白名单,避免两套机制各说各话
  · 账号不存在时也跑一次 BCrypt(假哈希),拉平响应时间防枚举
  · 登录密码 RSA 传输,防止网关/APM 把明文密码记进日志

脚本
  scripts/sync-ddl.py          文档 DDL → Flyway 迁移,带 --check 防漂移
  scripts/e2e-m0.py            M0 验收 36 项,可进 CI
  scripts/verify-docs.py       文档一致性(接口数【逐节】核对、错误码、跨文档数字、表格列数)
  scripts/dev-login.py         RSA 加密登录助手
  scripts/dev-clean-e2e.py     清理 E2E 测试数据

文档修正(实测暴露,非人工审查)
  · DDL 缺陷:sys_tenant.contact_phone / sys_user.phone / collect_roster.phone
    原 varchar(20) 按明文长度建,装不下 AES-GCM 密文(约 52 字符),改 varchar(128)(PRD V2.4)
  · 错误码 40007b/40007c 带字母后缀与「code 为数字」的约定冲突,规范化为 40071/40072
  · 07-开发实施计划 新增 §9 版本兼容踩坑记录(10 条实录)与 §10 M0 完成情况
2026-09-28 17:28:21 +08:00

202 lines
6.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
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.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
文档一致性校验。
被校的东西都是历史上真实出错过的:
1. 表格列数一致(错位会让 Markdown 渲染成一堆竖线)
2. 接口数:逐节「标题声称数」vs「实际表行数」——只看总数会漏掉
"这节多算 3 个、那节少算 3 个、总数恰好相等"这种情况
3. 错误码数量与文档声称一致
4. 关键数字跨文档一致(FR / VAL / 任务 / 人日 / 表数 / 接口)
5. 原型屏数与 figcaption 数一致
用法
----
python scripts/verify-docs.py # 校验,失败退出码 1
"""
from __future__ import annotations
import io
import os
import re
import sys
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8")
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
D = os.path.join(ROOT, "docs")
BT = chr(96)
ok_count = 0
bad: list[str] = []
def ok(msg: str) -> None:
global ok_count
ok_count += 1
print(f" [OK] {msg}")
def fail(msg: str) -> None:
bad.append(msg)
print(f" [FAIL] {msg}")
def read(*parts: str) -> str:
p = os.path.join(ROOT, *parts)
return io.open(p, encoding="utf-8").read() if os.path.exists(p) else ""
# ── 1. 表格列数 ────────────────────────────────────────
def check_tables(name: str, text: str) -> None:
lines = text.split("\n")
bad_rows = 0
i = 0
while i < len(lines):
if lines[i].strip().startswith("|") and i + 1 < len(lines) \
and re.match(r"^\|[\s:|-]+\|$", lines[i + 1].strip()):
n = lines[i].count("|")
j = i + 2
while j < len(lines) and lines[j].strip().startswith("|"):
if lines[j].count("|") != n:
bad_rows += 1
j += 1
i = j
else:
i += 1
if bad_rows:
fail(f"{name}:{bad_rows} 行表格列数不一致")
else:
ok(f"{name}:表格列数一致")
# ── 2. 接口数逐节核对 ──────────────────────────────────
def check_api_count() -> int:
s = read("docs", "03-接口设计.md")
if not s:
fail("找不到 03-接口设计.md")
return 0
sec = s[s.index("## §4"):s.index("## §5")]
parts = re.split(r"^(### 4\.\d+ .+)$", sec, flags=re.M)
claim_sum = actual_sum = 0
mismatches = []
i = 1
while i < len(parts) - 1:
title, body = parts[i].strip(), parts[i + 1]
i += 2
m = re.search(r"[((](\d+)[))]\s*$", title)
claim = int(m.group(1)) if m else None
# ⚠️ 允许 ⭐ 出现在方法之前(| ⭐ GET | ... |)
actual = len(re.findall(r"^\|\s*(?:⭐\s*)?(GET|POST|PUT|DELETE|PATCH)\s*\|", body, re.M))
# 索引节(交叉引用别处已计数的接口),不重复计入
if "索引" in title or "新增" in title:
continue
if claim is None:
continue
claim_sum += claim
actual_sum += actual
if claim != actual:
mismatches.append(f"{title[:30]} 声称{claim} 实际{actual}")
if mismatches:
fail(f"03 接口数逐节不一致:{'; '.join(mismatches)}")
elif claim_sum == actual_sum:
ok(f"03 接口数逐节一致 = {actual_sum}")
else:
fail(f"03 接口数:声称 {claim_sum} vs 实际 {actual_sum}")
return actual_sum
# ── 3. 错误码 ──────────────────────────────────────────
def check_error_codes() -> int:
s = read("docs", "03-接口设计.md")
sec = s[s.index("## §15"):]
codes = set(re.findall(
r"^\|\s*(?:⭐\s*)?" + re.escape(BT) + r"(\d{4,5})" + re.escape(BT) + r"\s*\|", sec, re.M))
total = len(codes) + 1 # + 成功码 0
claimed = re.search(r"本节共 \*\*(\d+) 个\*\*", s)
if claimed and int(claimed.group(1)) != total:
fail(f"03 错误码:实际 {total} vs 文档声称 {claimed.group(1)}")
else:
ok(f"03 错误码 = {total}")
return total
# ── 4. 跨文档关键数字 ──────────────────────────────────
def check_cross_doc(api: int, err: int) -> None:
docs = {
"PRD": read("docs", "数据信息填报系统PRD.md"),
"03": read("docs", "03-接口设计.md"),
"05": read("docs", "05-开发任务拆解.md"),
"README": read("README.md"),
"交接摘要": read("docs", "交接摘要.md"),
}
checks = [
("接口", str(api)),
("错误码", str(err)),
("任务", "100"),
("人日", "387"),
]
for label, value in checks:
hit = [n for n, t in docs.items() if value in t]
if len(hit) >= 3:
ok(f"{label} {value} 出现于 {len(hit)} 份文档")
else:
fail(f"{label} {value} 仅出现于 {hit}")
# ── 5. 原型屏数 ────────────────────────────────────────
def check_screens() -> None:
total = 0
for f in ["student-app.html", "teacher-app.html", "admin-web.html"]:
s = read("docs", "prototype", f)
total += len(re.findall(r"<figcaption", s, re.I))
claimed = re.search(r"合计屏数\s*:\s*(\d+)", read("docs", "交接摘要.md"))
if total:
ok(f"原型屏数(figcaption 计数)= {total}")
if claimed and int(claimed.group(1)) != total:
fail(f"原型屏数:实际 {total} vs 交接摘要声称 {claimed.group(1)}")
def main() -> int:
print("=" * 66)
print(" 文档一致性校验")
print("=" * 66)
print("\n[1] 表格列数")
for f in ["README.md"]:
check_tables(f, read(f))
for f in os.listdir(D):
if f.endswith(".md"):
check_tables(f, read("docs", f))
print("\n[2] 接口数(逐节核对)")
api = check_api_count()
print("\n[3] 错误码")
err = check_error_codes()
print("\n[4] 跨文档关键数字")
check_cross_doc(api, err)
print("\n[5] 原型屏数")
check_screens()
print("\n" + "=" * 66)
if bad:
print(f" 结果: {ok_count} 项通过,{len(bad)} 项失败")
for b in bad:
print(f" - {b}")
else:
print(f" 结果: {ok_count} 项全部通过")
print("=" * 66)
return 1 if bad else 0
if __name__ == "__main__":
raise SystemExit(main())