Files
xiaotutongxue/scripts/verify-docs.py
T
lovife 0bbac7d228 feat(pc,platform): 验收走查修复(平台按学校代管 + 菜单/403/日志范围/分页)
客户在验收里点出来的问题,一次改完。两段改动:

一、平台运营与租户范围(后端为主)
- 新增请求头 X-Tenant-Id:平台运营是唯一跨租户身份,租户拦截器对它整体关掉了过滤,
  于是「组织架构 / 账号管理 / 工作台」拿到的是所有租户混在一起的数据 —— 而只有一所
  学校有数据时,它和学校管理员看到的完全一样,看不出错(这是最危险的一点)。
  现在这些页面必须先选定学校,服务端再把本次请求的租户上下文换成目标租户:
  只认平台身份(其它身份带头一律忽略)、目标租户必须存在且启用。
- OrgService.tree / UserService.list 补显式 tenant_id 条件(原来只靠拦截器,而拦截器对
  平台不生效);TaskService.seesWholeTenant 改用新的 LoginUser.isPlatformStaff() ——
  isPlatformAdmin 回答"当前租户是不是平台租户",代管时它为 false,会让平台退化成普通
  用户(任务列表掉进"班主任只看自己带的班"分支,得到空列表);PLATFORM_ADMIN 补
  TASK_VIEW(PRD §3.2「查看填报进度看板:平台运营 ✅」)。
- 操作日志对平台永远是空的:SQL 写死 tenant_id = 当前租户,而平台的 tenantId 是 0。
  按契约 §13.4 补 tenantId 参数与 log:viewAll:平台默认看全量、可指定某所学校;
  学校管理员恒为本租户,传别家返回 40301(不静默忽略,否则会让人以为"筛过了")。
- 开放接口"推送重试"从未生效(F-112):定时扫描缺 @InterceptorIgnore,异常被 log.warn
  吞掉;同一方法里"接入方已停用"那条 update 也没有租户上下文,会每 10 秒重试到永远。
  E2E 补了"真的重试了"的断言(原来只断言"安排了下次重试")。

二、PC 端走查体验
- 去掉 3 条无用菜单(名单管理 / 表单设计器 / 材料规则):功能都在「收集任务」行内入口,
  菜单里那一条点进去只有指路;后端菜单键同步删除,entryAt 那套机制一并删掉。
- 班主任点工作台必弹 403:工作台无条件调了要 org:view / progress:viewAll 的两个接口。
  403 走 HTTP 状态码,try/catch 拦不住它的弹窗 —— 改成按权限决定发不发请求。
- 操作日志:分页器被裁在可视区外(新增 .g-list-card 让表体自己滚、表头钉住、分页器留在
  卡片底部)+ 去掉页面上那段免责声明;收集任务的状态切换改成单独一行 tab;去掉顶栏假数据
  铃铛;配色按客户要求还原。

顺带修两处"检查器自己不严":check-wiring 新增第 ⑦ 项(PC 端引用的权限码必须存在于
PermissionDef —— 本轮我自己把 progress:viewAll 写成 progress:view:all,判断恒为 false,
而编译 / typecheck / lint / E2E 全都不红);verify-docs 的表格列数检查不再把 GFM 转义
竖线当成列分隔符。两个检查器都拿"故意写错的样例"试过,确认真的会红。

验证:typecheck / mvn package / check-wiring(未接线声明 0 条)/ verify-docs(22 项)全过;
e2e-m0 36、e2e-m1-orgs 48、e2e-m0-user 55、e2e-m5-logs 44、e2e-m5-notify-wiring 43、
e2e-m1-tenant-domain 6、e2e-m8-open 61 全绿;浏览器按角色逐个真点复验。
详见 docs/交接摘要.md §一·三十三 与 §一·三十四。
2026-10-09 17:29:20 +08:00

212 lines
6.9 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 table_pipes(line: str) -> int:
"""数表格竖线,**跳过转义过的 `\\|`**。
GFM 里 `\\|` 表示"内容里的竖线",不是列分隔符(本仓库用手写表格,
正文里写 `a \\|\\| b` 是很自然的事)。第一版直接 `line.count("|")`,
于是把这种写法报成"列数不一致" —— **检查器自己要先认得它检查的语法**。
"""
return len(re.findall(r"(?<!\\)\|", line))
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 = table_pipes(lines[i])
j = i + 2
while j < len(lines) and lines[j].strip().startswith("|"):
if table_pipes(lines[j]) != 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())