Files
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

103 lines
3.6 KiB
Python
Raw Permalink 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 -*-
"""
DDL 同步:docs/db/schema-postgresql.sql → backend/.../db/migration/V1__business_tables.sql
为什么要有这个脚本
------------------
文档里的 DDL 是**交付物**(已实测、写进验收标准),Flyway 迁移是**工程产物**。
两者必须字字相同,否则"文档说一套、代码跑另一套"——这是最难查的一类事故。
所以:迁移文件由脚本生成,禁止手改;改 DDL 一律先改文档,再跑本脚本。
用法
----
python scripts/sync-ddl.py # 生成/更新迁移文件
python scripts/sync-ddl.py --check # 只校验是否同步(CI 用,不同步则退出码 1)
"""
from __future__ import annotations
import io
import os
import sys
# Windows 控制台默认不是 UTF-8
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8")
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SOURCE = os.path.join(ROOT, "docs", "db", "schema-postgresql.sql")
TARGET = os.path.join(
ROOT, "backend", "xt-server", "src", "main", "resources",
"db", "migration", "V1__business_tables.sql",
)
HEADER = """\
-- ═══════════════════════════════════════════════════════════════════
-- V1 · 业务表(20 张)
--
-- ⚠️ 本文件由脚本生成,请勿手改!
-- 来源 :docs/db/schema-postgresql.sql
-- 重新生成 :python scripts/sync-ddl.py
-- 校验同步 :python scripts/sync-ddl.py --check
--
-- 源文件已在 PostgreSQL 16.15 实测零错误:
-- 20 表 / 177 列注释 / 65 索引 / 20 表注释
-- ═══════════════════════════════════════════════════════════════════
"""
def read(path: str) -> str:
with io.open(path, encoding="utf-8") as f:
return f.read()
def normalize(text: str) -> str:
"""统一行尾并去掉首尾空白,用于比对。"""
return text.replace("\r\n", "\n").replace("\r", "\n").strip()
def render(source_text: str) -> str:
return HEADER + normalize(source_text) + "\n"
def main() -> int:
check_only = "--check" in sys.argv
if not os.path.exists(SOURCE):
print(f"[X] 找不到源文件:{SOURCE}")
return 1
expected = render(read(SOURCE))
if check_only:
if not os.path.exists(TARGET):
print(f"[X] 迁移文件不存在:{TARGET}")
return 1
actual = io.open(TARGET, encoding="utf-8").read()
if normalize(actual) != normalize(expected):
print("[X] DDL 已漂移:文档与迁移文件不一致")
print(f" 文档 :{SOURCE}")
print(f" 迁移 :{TARGET}")
print(" 修复 :python scripts/sync-ddl.py")
return 1
print("[OK] DDL 已同步")
return 0
os.makedirs(os.path.dirname(TARGET), exist_ok=True)
with io.open(TARGET, "w", encoding="utf-8", newline="\n") as f:
f.write(expected)
tables = expected.count("CREATE TABLE ")
indexes = expected.count("CREATE INDEX ") + expected.count("CREATE UNIQUE INDEX ")
comments = expected.count("COMMENT ON COLUMN ")
print(f"[OK] 已生成 {os.path.relpath(TARGET, ROOT)}")
print(f" {tables} 表 / {indexes} 索引 / {comments} 列注释 / "
f"{len(expected.encode('utf-8'))} 字节")
return 0
if __name__ == "__main__":
raise SystemExit(main())