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 完成情况
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
清理 e2e-m0.py 产生的测试数据。
|
||||
|
||||
E2E 每跑一次就新建一个租户(编码 e2e######)并给它建角色与账号,
|
||||
反复跑会把开发库堆满。这个脚本把 e2e* 租户及其关联数据清掉。
|
||||
|
||||
用法
|
||||
----
|
||||
python scripts/dev-clean-e2e.py # 只看会删什么(默认 dry-run)
|
||||
python scripts/dev-clean-e2e.py --apply # 真删
|
||||
|
||||
⚠️ 只在开发库上跑,不要对生产执行。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
if hasattr(sys.stdout, "reconfigure"):
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
|
||||
# 子表在前、主表在后(无外键约束,顺序只为可读性与排查方便)
|
||||
STEPS = [
|
||||
("角色授权", "sys_role_permission"),
|
||||
("用户角色", "sys_user_role"),
|
||||
("登录日志", "sys_login_log"),
|
||||
("组织", "sys_org"),
|
||||
("角色", "sys_role"),
|
||||
("用户", "sys_user"),
|
||||
]
|
||||
|
||||
TENANT_FILTER = "tenant_code LIKE 'e2e%'"
|
||||
SUB_FILTER = f"tenant_id IN (SELECT id FROM sys_tenant WHERE {TENANT_FILTER})"
|
||||
|
||||
|
||||
class Db:
|
||||
def __init__(self, container: str, user: str, name: str):
|
||||
self.container = container
|
||||
self.user = user
|
||||
self.name = name
|
||||
|
||||
def scalar_rows(self, sql: str) -> list[str]:
|
||||
cmd = ["docker", "exec", self.container, "psql", "-U", self.user, "-d", self.name,
|
||||
"-v", "ON_ERROR_STOP=1", "-t", "-A", "-F", "|", "-c", sql]
|
||||
r = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8")
|
||||
if r.returncode != 0:
|
||||
print(r.stderr.strip(), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
return [x for x in r.stdout.split("\n") if x.strip()]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--apply", action="store_true", help="真正执行删除(默认只预览)")
|
||||
ap.add_argument("--container", default="xt-pg")
|
||||
ap.add_argument("--user", default="fill")
|
||||
ap.add_argument("--db", default="filldb")
|
||||
args = ap.parse_args()
|
||||
|
||||
db = Db(args.container, args.user, args.db)
|
||||
|
||||
tenants = db.scalar_rows(
|
||||
f"SELECT id || '|' || tenant_code || '|' || tenant_name "
|
||||
f"FROM sys_tenant WHERE {TENANT_FILTER} ORDER BY id")
|
||||
if not tenants:
|
||||
print("没有 e2e 测试租户,无需清理")
|
||||
return 0
|
||||
|
||||
print(f"找到 {len(tenants)} 个 e2e 测试租户:")
|
||||
for r in tenants[:15]:
|
||||
p = r.split("|")
|
||||
print(" id=%-5s code=%-14s %s" % (p[0], p[1], p[2] if len(p) > 2 else ""))
|
||||
if len(tenants) > 15:
|
||||
print(f" ... 另有 {len(tenants) - 15} 个")
|
||||
|
||||
if not args.apply:
|
||||
print("\n这是预览。加 --apply 才会真正删除。")
|
||||
return 0
|
||||
|
||||
print("\n开始清理:")
|
||||
total = 0
|
||||
for label, table in STEPS:
|
||||
rows = db.scalar_rows(f"DELETE FROM {table} WHERE {SUB_FILTER} RETURNING id")
|
||||
total += len(rows)
|
||||
print(" %-8s %-20s 删除 %d 行" % (label, table, len(rows)))
|
||||
|
||||
rows = db.scalar_rows(f"DELETE FROM sys_tenant WHERE {TENANT_FILTER} RETURNING id")
|
||||
total += len(rows)
|
||||
print(" %-8s %-20s 删除 %d 行" % ("租户", "sys_tenant", len(rows)))
|
||||
|
||||
print(f"\n完成,共删除 {total} 行。")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user