0bbac7d228
客户在验收里点出来的问题,一次改完。两段改动:
一、平台运营与租户范围(后端为主)
- 新增请求头 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 §一·三十三 与 §一·三十四。
212 lines
6.9 KiB
Python
212 lines
6.9 KiB
Python
#!/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())
|