Files
xiaotutongxue/scripts/verify-all.py
T
lovife 1f3c485ceb feat(role,scope)!: 角色与授权可配 + 需求收敛(4 角色 / 10 菜单 / 删两模块)
角色与授权(交接摘要 §一·三十五):
- 权限判定合并两个来源:内置角色按代码(BuiltinRole)、自定义角色按库
  (sys_role_permission)。实现放 module 层的 DbRoleGrantSource,security 只依赖
  接口 RoleGrantSource —— 否则角色服务要调 permissionChecker,两个包迟早成环
- 权限判定必须加缓存:此前是零 IO,而压测基线是"SQL 条数恒定"。缓存放 Redis,
  空结果也缓存(否则已删角色的 token 会把库打穿)、Redis 挂了退化成直读库、TTL 只兜底
- V18 建 sys_role.user_type(账号的 user_type 一直从角色反推,自定义角色不在代码映射里)
  与 sys_role_menu;8 个接口 + 错误码 40045;契约 119 -> 129
- 内置角色只读(权限码是前后端契约,做成可改 = 一次误操作就能把管理员锁在系统外);
  平台级能力两道防线(界面禁用 + 服务端 40301);防自锁;改完立即生效(写授权清缓存)
- RoleProvisioner 从"已存在就 continue"改成幂等回填 + 每日 03:20 定时任务 ——
  修的是"代码改了权限、已开通租户永远停在发放那一刻",这种漂移不报错也没测试会红
- scripts/e2e-m10-role.py 77/77

需求收敛(交接摘要 §一·三十六,本节的数字为准):
- 删副班主任 / 审核员两个内置角色(6 -> 4)、历史整理器(module/cleanup)、
  加入申请(module/join + 学生端 pages/join)、collect_task_org.deputy_user_ids 整列;
  审核范围三级回退 -> 两级,连带 6 个 mapper 的 LIKE '%,id,%' 分支
- V19 迁移处理存量:先解 sys_user_role 绑定再软删角色;账号只在"除这两个角色外没有
  别的角色"时才软删。必须迁而不能只删代码 —— 角色码删掉后 mergePermissions 返回空集,
  存量账号会"能登录、处处 40301",不抛异常不打日志
- 契约 129 -> 119、错误码 59 -> 54、权限码 30 -> 28、PC 菜单 12 -> 10、
  小程序页 10 -> 9、原型 32 -> 29 屏、E2E 39 -> 37 个(断言 1695 条)
- UserService.list 对不认识的角色码由"静默丢掉过滤条件"改成失败关闭
  (之前传个不存在的 roleCode 会返回全校账号,且看起来完全正常)
- 品牌元信息同步改名「深学在线」:pom 名称、/doc.html 标题、配置与启动类注释、
  存储自检探针内容;学生端 app 名与全局导航栏标题
2026-10-10 16:48:54 +08:00

213 lines
8.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.
# -*- coding: utf-8 -*-
"""交付自检:一条命令把"这个仓库现在到底对不对"跑一遍。
## 为什么需要它
这个项目的验证手段有六类,散在六个地方:
| 类别 | 命令 | 需要什么 |
|---|---|---|
| 契约 ↔ 路由 | `check-api-parity.py` | 无 |
| 实体 ↔ 建表 | `check-entity-columns.py` | 无 |
| 文档自身一致 | `verify-docs.py` | 无 |
| 手册 ↔ 代码文案 | `check-manual.py` | 无 |
| 接线(定义 ↔ 使用) | `check-wiring.py` | 无 |
| 接口行为(断言总数见下方"用法"里的实测值) | `scripts/e2e-*.py` | **后端在跑 + PG/Redis** |
| 生产配置拦截 | `e2e-prodguard.py` | **能起 jar** |
| CI 那个 profile 能不能起 | `check-test-profile.py` | 后端 jar + PG/Redis |
**要记住"该跑哪几个、哪些需要前置"本身就是一种负担**,而漏跑一个的后果是
"以为过了"。所以这里合成一个入口,并且**如实说明哪些因为前置不足而跳过**
(不是静默跳过 —— 那与 F-85 是同一类问题)。
## 用法
```bash
python scripts/verify-all.py # 全部(跳过需要外部前置的,并说明)
python scripts/verify-all.py --with-e2e # 连 37 个脚本的接口断言一起跑(需要后端在跑)
python scripts/verify-all.py --quick # 只跑不需要任何前置的五项
```
> 脚本数与断言总数会随需求变化(2026-10 需求收敛后是 **37 个脚本**,断言总数以本命令
> 实际输出为准 —— 别再往文档里抄一个会过期的固定数字)。
退出码:0 = 全过;1 = 有失败(**跳过不算失败**,但会明确列出来)。
"""
import argparse
import glob
import os
import re
import shutil
import subprocess
import sys
import time
import urllib.request
sys.stdout.reconfigure(encoding="utf-8")
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
os.chdir(ROOT)
GREEN, RED, YELLOW, DIM, RESET = "\033[32m", "\033[31m", "\033[33m", "\033[2m", "\033[0m"
if not sys.stdout.isatty():
GREEN = RED = YELLOW = DIM = RESET = ""
def run(cmd, timeout=3600):
"""跑一条命令,返回 (退出码, 输出)。"""
try:
p = subprocess.run(cmd, shell=True, capture_output=True, text=True,
encoding="utf-8", errors="replace", timeout=timeout)
return p.returncode, (p.stdout or "") + (p.stderr or "")
except subprocess.TimeoutExpired:
return -9, "超时"
def backend_up():
try:
with urllib.request.urlopen("http://localhost:8080/api/v1/auth/captcha", timeout=5):
return True
except Exception:
return False
def main():
ap = argparse.ArgumentParser(description="交付自检")
ap.add_argument("--with-e2e", action="store_true", help="连 e2e 一起跑(需要后端在跑)")
ap.add_argument("--quick", action="store_true", help="只跑不需要前置的五项")
args = ap.parse_args()
results = [] # (名称, 结果, 摘要)
print("=" * 92)
print(" 交付自检 仓库:%s" % ROOT)
print("=" * 92)
# ── 一、不需要任何前置的机械校验 ──
print("\n【一】机械校验(不需要后端/数据库)")
checks = [
("契约 ↔ 路由", "python scripts/check-api-parity.py --quiet",
r"合计:契约 (\d+) / 已实现 (\d+) / 缺口 (\d+)"),
("实体 ↔ 建表", "python scripts/check-entity-columns.py --quiet",
r"(\d+) 个实体全部与迁移一致((\d+) 张表)"),
("文档自身一致", "python scripts/verify-docs.py", r"结果: (\d+) 项全部通过"),
("手册 ↔ 代码文案", "python scripts/check-manual.py", r"结果:.*"),
("接线(定义↔使用)", "python scripts/check-wiring.py --quiet",
r"未接线的声明 (\d+) 条"),
]
for name, cmd, pat in checks:
code, out = run(cmd, timeout=300)
line = ""
m = re.search(pat, out)
if m:
line = m.group(0).strip()
else:
tail = [x for x in out.strip().split("\n") if x.strip()]
line = tail[-1][:80] if tail else "(无输出)"
ok = code == 0
results.append((name, ok, line))
print(" %s%-14s%s %s" % (GREEN if ok else RED, name, RESET, line))
# 脚本语法(改脚本时最容易踩:中文串里嵌半角引号)
bad = []
for fn in sorted(glob.glob("scripts/*.py")):
try:
compile(open(fn, encoding="utf-8").read(), fn, "exec")
except SyntaxError as e:
bad.append("%s:%s" % (os.path.basename(fn), e.lineno))
ok = not bad
results.append(("脚本语法", ok, "全部可编译" if ok else "有语法错:%s" % ", ".join(bad)))
print(" %s%-14s%s %s" % (GREEN if ok else RED, "脚本语法", RESET,
"全部可编译" if ok else "有语法错:%s" % ", ".join(bad)))
if args.quick:
return summary(results)
# ── 二、需要后端在跑 / 能起 jar 的 ──
print("\n【二】带前置的检查")
up = backend_up()
if not up:
msg = "**跳过**:后端 8080 没在跑(先 `cd backend && java -jar ... --spring.profiles.active=dev`)"
results.append(("接口断言(E2E)", None, msg))
print(" %s%-14s%s %s" % (YELLOW, "接口断言", RESET, msg))
elif not args.with_e2e:
msg = "**跳过**:要跑就加 `--with-e2e`(37 个脚本、约 10 分钟)"
results.append(("接口断言(E2E)", None, msg))
print(" %s%-14s%s %s" % (YELLOW, "接口断言", RESET, msg))
else:
t0 = time.time()
files = sorted(glob.glob("scripts/e2e-*.py"))
total = fails = 0
failed_names = []
for f in files:
code, out = run("python %s" % f, timeout=900)
m = re.search(r"通过 (\d+) 失败 (\d+) 共 (\d+)", out)
if m:
total += int(m.group(1))
if int(m.group(2)) > 0:
fails += int(m.group(2))
failed_names.append(os.path.basename(f))
else:
m2 = re.search(r"结果: (\d+)/(\d+)", out)
if m2:
total += int(m2.group(2))
if m2.group(1) != m2.group(2):
fails += 1
failed_names.append(os.path.basename(f))
else:
fails += 1
failed_names.append(os.path.basename(f) + "(无汇总)")
ok = fails == 0
msg = "%d 个脚本 / %d 条断言 / 失败 %d%s(%.0fs)" % (
len(files), total, fails,
"" if ok else ":" + ", ".join(failed_names[:5]), time.time() - t0)
results.append(("接口断言(E2E)", ok, msg))
print(" %s%-14s%s %s" % (GREEN if ok else RED, "接口断言", RESET, msg))
# 生产自检:需要 jar
jar = glob.glob("backend/xt-server/target/xt-server-*.jar")
if not jar:
msg = "**跳过**:没有 jar(先 `cd backend && mvn -DskipTests package`)"
results.append(("生产配置拦截", None, msg))
print(" %s%-14s%s %s" % (YELLOW, "生产配置拦截", RESET, msg))
else:
# 起 jar 的检查会撞端口,所以只在没别的 java 在跑时做 —— 如实说明
code, out = run("python scripts/e2e-prodguard.py", timeout=900)
m = re.search(r"通过 (\d+) 失败 (\d+) 共 (\d+)", out)
ok = code == 0
msg = m.group(0) if m else ("通过" if ok else "失败")
results.append(("生产配置拦截", ok, msg))
print(" %s%-14s%s %s" % (GREEN if ok else RED, "生产配置拦截", RESET, msg))
if up:
code, out = run("python scripts/check-test-profile.py", timeout=600)
m = re.search(r"结果:(.*)", out)
ok = code == 0
msg = (m.group(1).strip() if m else ("通过" if ok else "失败"))[:70]
results.append(("test profile 能起", ok, msg))
print(" %s%-14s%s %s" % (GREEN if ok else RED, "test profile 能起", RESET, msg))
else:
results.append(("test profile 能起", None, "**跳过**:后端没在跑"))
print(" %s%-14s%s **跳过**:后端没在跑" % (YELLOW, "test profile 能起", RESET))
return summary(results)
def summary(results):
print("\n" + "=" * 92)
passed = [r for r in results if r[1] is True]
failed = [r for r in results if r[1] is False]
skipped = [r for r in results if r[1] is None]
print(" 通过 %d 失败 %d 跳过 %d" % (len(passed), len(failed), len(skipped)))
for name, ok, msg in failed:
print(" %s✗ %s%s:%s" % (RED, name, RESET, msg[:110]))
for name, ok, msg in skipped:
# ⚠️ 跳过要**明确列出来**,不能静默 —— 否则"以为过了"(与 F-85 同类)
print(" %s— %s 跳过:%s%s" % (YELLOW, name, msg[:100], RESET))
print("=" * 92)
sys.stdout.flush()
return 1 if failed else 0
if __name__ == "__main__":
raise SystemExit(main())