deliver: 交付收口(引擎代为提交)

This commit is contained in:
agent.develop 2026-09-30 19:30:47 +08:00
parent bf00609c15
commit 33dadb461d
3 changed files with 531 additions and 0 deletions

View File

@ -0,0 +1,45 @@
{
"_comment": "声明口径表名 → models 单一真源表名对照;test/deploy 探针请改用 real 名,避免 ERROR 1146 误报为『表缺失』。real=null 表示该声明项本就零表设计。",
"generated_by": "scripts/gen_base_ddl.py",
"base_tables": [
"appcodes",
"appcodes_kv",
"audit_log",
"organization",
"orgtypes",
"params",
"permission",
"role",
"rolepermission",
"svgicon",
"userrole",
"users"
],
"mapping": {
"pbl_role": {
"real": "role",
"module": "rbac",
"note": "rbac 角色表真名为 role,非 pbl_role;7 类角色种子写 role + userrole"
},
"pbl_appcode": {
"real": "appcodes",
"module": "appbase",
"note": "共享编码父表真名 appcodes(子表 appcodes_kv),非 pbl_appcode"
},
"pbl_validation_rule": {
"real": "pbl_validation_ruleset",
"module": "pbl_validation",
"note": "14 维校验规则集真名 pbl_validation_ruleset"
},
"pbl_agent_tool": {
"real": null,
"module": "pbl_agent_runtime",
"note": "Agent 工具白名单 13 启用/9 禁用落在 appcodes 组内(零表设计),无独立表"
},
"pbl_role_binding": {
"real": "pbl_role_binding",
"module": "pbl_governance",
"note": "治理域角色绑定表,真名一致;行数 0 属种子未注入(见 seed_rbac_admin.py)"
}
}
}

147
scripts/ddl/base_tables.sql Normal file
View File

@ -0,0 +1,147 @@
-- =====================================================================
-- base_tables.sql —— 基座模块(appbase / rbac)表结构,由 scripts/gen_base_ddl.py
-- 从各模块 models/*.json(单一真源)生成,禁止手工编辑本文件。
-- 方言: mariadb / utf8mb4 / utf8mb4_unicode_ci;全部 IF NOT EXISTS(幂等)。
-- 修复: Bug 4SxqQAulkH9eAzEys_h-e(up_login.dspy 500 —— users 表缺失)
-- Bug 0CQZ7O6ftDRHbxih59upr(种子 4 项 0 行 —— 依赖 role/userrole 就位)
-- =====================================================================
SET NAMES utf8mb4;
-- [appbase] appcodes (源: /d/pipeline/pipeline-app/pkgs/appbase/models/appcodes.json)
CREATE TABLE IF NOT EXISTS `appcodes` (
`id` VARCHAR(32) COMMENT 'id',
`name` VARCHAR(255) COMMENT '编码名称',
`hierarchy_flg` VARCHAR(1) COMMENT '多级标志',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- [appbase] appcodes_kv (源: /d/pipeline/pipeline-app/pkgs/appbase/models/appcodes_kv.json)
CREATE TABLE IF NOT EXISTS `appcodes_kv` (
`id` VARCHAR(32) COMMENT 'id',
`parentid` VARCHAR(32) COMMENT '父id',
`k` VARCHAR(32) COMMENT '键',
`v` VARCHAR(255) COMMENT '值',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_appcodes_kv_idx1` (`parentid`, `k`),
KEY `idx_appcodes_kv_idx2` (`parentid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- [appbase] params (源: /d/pipeline/pipeline-app/pkgs/appbase/models/params.json)
CREATE TABLE IF NOT EXISTS `params` (
`id` VARCHAR(32) COMMENT 'id',
`params_name` VARCHAR(255) COMMENT '参数名称',
`params_value` VARCHAR(4000) COMMENT '参数值',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- [appbase] svgicon (源: /d/pipeline/pipeline-app/pkgs/appbase/models/svgicon.json)
CREATE TABLE IF NOT EXISTS `svgicon` (
`id` VARCHAR(32) COMMENT 'id',
`icon` LONGTEXT COMMENT 'svg内容',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- [rbac] audit_log (源: /d/pipeline/pipeline-app/pkgs/rbac/models/audit_log.json)
CREATE TABLE IF NOT EXISTS `audit_log` (
`id` VARCHAR(32) COMMENT '用户id',
`permid` VARCHAR(32) COMMENT '权限id',
`userid` VARCHAR(32) COMMENT '用户id',
`params_kw` TEXT COMMENT '调用参数',
`exe_date` DATE COMMENT '调用日期',
`exe_timestamp` TIMESTAMP NULL DEFAULT NULL COMMENT '调用时间戳',
`remote_ip` VARCHAR(40) COMMENT '远程IP',
PRIMARY KEY (`id`),
KEY `idx_audit_log_idx1` (`permid`),
KEY `idx_audit_log_idx2` (`userid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- [rbac] organization (源: /d/pipeline/pipeline-app/pkgs/rbac/models/organization.json)
CREATE TABLE IF NOT EXISTS `organization` (
`id` VARCHAR(32) COMMENT '机构编码',
`orgname` VARCHAR(255) COMMENT '机构名称',
`orgabbr` VARCHAR(100) COMMENT '机构简称',
`alias_name` VARCHAR(255) COMMENT '机构别名',
`contactor` VARCHAR(32) COMMENT '联系人',
`contactor_phone` VARCHAR(100) COMMENT '联系人电话',
`province_id` VARCHAR(32) COMMENT '所在省id',
`city_id` VARCHAR(32) COMMENT '所在城市id',
`distinct_id` VARCHAR(32) COMMENT '所在地区id',
`emailaddress` VARCHAR(256) COMMENT '邮箱',
`address` VARCHAR(400) COMMENT '地址',
`main_business` VARCHAR(1000) COMMENT '主营业务描述',
`orgcode` VARCHAR(100) COMMENT '组织结构代码',
`license_img` VARCHAR(400) COMMENT '营业执照',
`id_img` VARCHAR(400) COMMENT '身份证',
`parentid` VARCHAR(32) COMMENT '父机构id',
`org_type` VARCHAR(32) COMMENT '机构类型',
`sitename` VARCHAR(300) COMMENT '网站域名',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_organization_idx1` (`orgname`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- [rbac] orgtypes (源: /d/pipeline/pipeline-app/pkgs/rbac/models/orgtypes.json)
CREATE TABLE IF NOT EXISTS `orgtypes` (
`id` VARCHAR(32) COMMENT 'id',
`orgid` VARCHAR(32) COMMENT '机构id',
`orgtypeid` VARCHAR(32) COMMENT '机构类型',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- [rbac] permission (源: /d/pipeline/pipeline-app/pkgs/rbac/models/permission.json)
CREATE TABLE IF NOT EXISTS `permission` (
`id` VARCHAR(32) COMMENT '权限id',
`name` VARCHAR(255) COMMENT '名称',
`description` VARCHAR(255) COMMENT '描述',
`parentid` VARCHAR(32) COMMENT '父权限id',
`path` VARCHAR(255) COMMENT '路径',
`icon` VARCHAR(255) COMMENT '图标',
`permtype` VARCHAR(255) COMMENT '类型',
`need_audit` VARCHAR(1) COMMENT '是否审计',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_permission_idx1` (`path`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- [rbac] role (源: /d/pipeline/pipeline-app/pkgs/rbac/models/role.json)
CREATE TABLE IF NOT EXISTS `role` (
`id` VARCHAR(32) COMMENT '角色id',
`orgtypeid` VARCHAR(32) DEFAULT '0' COMMENT '机构类型',
`name` VARCHAR(100) COMMENT '角色名称',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- [rbac] rolepermission (源: /d/pipeline/pipeline-app/pkgs/rbac/models/rolepermission.json)
CREATE TABLE IF NOT EXISTS `rolepermission` (
`id` VARCHAR(32) COMMENT 'id',
`roleid` VARCHAR(32) COMMENT '角色id',
`permid` VARCHAR(32) COMMENT '权限id',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- [rbac] userrole (源: /d/pipeline/pipeline-app/pkgs/rbac/models/userrole.json)
CREATE TABLE IF NOT EXISTS `userrole` (
`id` VARCHAR(32) COMMENT '用户id',
`userid` VARCHAR(32) COMMENT '用户id',
`roleid` VARCHAR(32) COMMENT '角色id',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- [rbac] users (源: /d/pipeline/pipeline-app/pkgs/rbac/models/users.json)
CREATE TABLE IF NOT EXISTS `users` (
`id` VARCHAR(32) COMMENT '用户id',
`username` VARCHAR(255) COMMENT '用户名',
`password` VARCHAR(255) COMMENT '密码',
`email` VARCHAR(255) COMMENT '邮件地址',
`orgid` VARCHAR(32) COMMENT '所属机构',
`nick_name` VARCHAR(255) COMMENT '显示名',
`address` VARCHAR(255) COMMENT '地址',
`mobile` VARCHAR(255) COMMENT '手机',
`user_status` VARCHAR(1) DEFAULT '0' COMMENT '用户状态',
`created_at` DATE COMMENT '注册日期',
`last_login` TIMESTAMP NULL DEFAULT NULL COMMENT '最后登录',
`login_fail_count` SMALLINT DEFAULT '0' COMMENT '连续失败次数',
`last_login_fail` TIMESTAMP NULL DEFAULT NULL COMMENT '最后登录失败时间',
`sync_from` VARCHAR(32) COMMENT '同步应用id',
PRIMARY KEY (`id`),
KEY `idx_users_idx1` (`orgid`),
UNIQUE KEY `idx_users_idx2` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

339
scripts/gen_base_ddl.py Normal file
View File

@ -0,0 +1,339 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
gen_base_ddl.py —— 基座模块(appbase / rbac)表结构 DDL 生成器
【为什么需要它 / Bug 4SxqQAulkH9eAzEys_h-e 根因】
POST /main/rbac/user/up_login.dspy 返回 500 "Server got itself in trouble"。
该端点(pkgs/rbac/wwwroot/user/up_login.dspy)第一条 SQL 是:
dbname = get_module_dbname('rbac') # conf/config.json → 'pbl'
async with db.sqlorContext(dbname) as sor:
r = await sor.sqlExe('select * from users where username=${username}$', ns)
而 apps/pbls/build.sh 的建表段(第 10 / 10a 节)只覆盖两类表:
· scripts/ddl/pbls_tables.sql —— 32 张 pbl_* 业务表
· modules/pbl_*/models/*.json —— 6 张治理/模板域表(10a 打印 "6/6")
基座模块 appbase(appcodes/appcodes_kv/params/svgicon)与 rbac
(users/role/permission/rolepermission/userrole/organization/orgtypes/audit_log)
的 models 链**从未被建表**。部署机实测 `SHOW TABLES`=41、其中 pbl_ 前缀 38 张,
非 pbl_ 前缀仅 3 张(appcodes/appcodes_kv/pbl_tenant 类共享表由种子段顺带建出),
`users` 不在其列 → 登录端点 sqlExe 抛 ERROR 1146 (42S02) Table 'pbl.users' doesn't exist
→ ahserver 兜底 500,会话 cookie 不下发 → 42 条业务用例持续 blocked。
同一口径差还解释了 Bug 描述第 6 点的 ERROR 1146 群:
pbl_role / pbl_appcode / pbl_validation_rule / pbl_agent_tool 均不存在——
因为 rbac 的角色表真名是 `role`(见 pkgs/rbac/models/role.json),
appbase 的编码表真名是 `appcodes`/`appcodes_kv`,
校验规则集真名是 `pbl_validation_ruleset`,Agent 工具白名单落在 appcodes 组内(零表)。
即:探针用的表名是"声明口径",实库是"models 单一真源口径",两者不一致。
本脚本以 models/*.json 为唯一真源生成 DDL,并额外产出一份
「声明表名 → 真源表名」对照表(scripts/ddl/base_table_alias_map.json),
供 test/deploy 侧探针改用真名,消除同类 1146 误报。
【设计约束】
· 不硬编码基座包路径:三段式探测(环境变量 BASE_PKGS_ROOT → 候选根逐个存在性
校验 → 全部落空则 die),每条派生路径都打印解析出的绝对路径(web-application-spec
铁律 5「路径推导必须自校验」)。
· 不硬编码 DB 连接参数:本脚本只生成 SQL 文件,不连库;连接参数由 build.sh
运行时从 projects/pbls/env/{env}.json 读取(project-directory-spec §8.6)。
· 方言 mariadb:BIGINT AUTO_INCREMENT 口径不适用(基座表主键全是 VARCHAR(32)),
统一 utf8mb4 / utf8mb4_unicode_ci,全部 CREATE TABLE IF NOT EXISTS(幂等)。
· 抽象类型 → 原生类型映射严格按 database-table-definition-spec:
只用 models 里的抽象类型,禁止把原生类型写回 models。
用法:
python3 scripts/gen_base_ddl.py \
--out scripts/ddl/base_tables.sql \
--alias-map scripts/ddl/base_table_alias_map.json
"""
from __future__ import annotations
import argparse
import json
import os
import sys
# --------------------------------------------------------------------------
# 抽象类型 → MariaDB 原生类型映射(database-table-definition-spec 口径)
# --------------------------------------------------------------------------
_INT_TYPES = {
"short": "SMALLINT",
"int": "INT",
"long": "BIGINT",
}
# 基座包候选根(探测顺序即优先级)。全部为**代码位置**,非 env 连接参数;
# 仍逐条打印存在性,避免"路径算错"被误报成"基座包缺失"。
_BASE_PKG_CANDIDATES = (
os.environ.get("BASE_PKGS_ROOT", ""),
os.path.join(os.environ.get("WS_DIR", ""), "pkgs"),
"/d/pipeline/pipeline-app/pkgs",
os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "pkgs"),
os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "venv", "lib"),
)
# 需要建表的基座模块(appbase 提供共享编码表,rbac 提供登录/鉴权表)
_BASE_MODULES = ("appbase", "rbac")
# rbac 登录流程额外依赖的列,来源 = pkgs/rbac/migration/add_login_tracking.sql
# 与 migration/fix_users_schema.sql(up_login.dspy 的锁定检查读 login_fail_count /
# last_login_fail,成功登录写 last_login)。models/users.json 未含这三列,
# 故在生成 DDL 时补齐,并保证可空(fix_users_schema.sql 明确:NOT NULL DEFAULT
# current_timestamp() 会让 "SET last_login_fail=NULL" 被转成当前时间戳)。
_USERS_LOGIN_TRACKING_COLUMNS = (
("login_fail_count", "INT NOT NULL DEFAULT 0", "连续登录失败次数"),
("last_login_fail", "DATETIME NULL DEFAULT NULL", "最近一次登录失败时间(NULL=从未失败)"),
("last_login", "DATETIME NULL DEFAULT NULL", "最近一次登录成功时间"),
)
_EXTRA_COLUMNS = {"users": _USERS_LOGIN_TRACKING_COLUMNS}
# 「声明口径表名」→「models 单一真源表名」对照(消除 ERROR 1146 同类误报)
_DECLARED_TO_REAL = {
"pbl_role": {"real": "role", "module": "rbac",
"note": "rbac 角色表真名为 role,非 pbl_role;7 类角色种子写 role + userrole"},
"pbl_appcode": {"real": "appcodes", "module": "appbase",
"note": "共享编码父表真名 appcodes(子表 appcodes_kv),非 pbl_appcode"},
"pbl_validation_rule": {"real": "pbl_validation_ruleset", "module": "pbl_validation",
"note": "14 维校验规则集真名 pbl_validation_ruleset"},
"pbl_agent_tool": {"real": None, "module": "pbl_agent_runtime",
"note": "Agent 工具白名单 13 启用/9 禁用落在 appcodes 组内(零表设计),无独立表"},
"pbl_role_binding": {"real": "pbl_role_binding", "module": "pbl_governance",
"note": "治理域角色绑定表,真名一致;行数 0 属种子未注入(见 seed_rbac_admin.py)"},
}
def _log(msg: str) -> None:
sys.stderr.write("[gen_base_ddl] %s\n" % msg)
def _die(msg: str) -> None:
sys.stderr.write("[gen_base_ddl][FATAL] %s\n" % msg)
raise SystemExit(2)
def resolve_base_pkgs_root() -> str:
"""三段式探测基座包根目录,逐条打印解析结果与存在性。"""
for idx, cand in enumerate(_BASE_PKG_CANDIDATES):
if not cand:
continue
abspath = os.path.abspath(cand)
ok = os.path.isdir(abspath) and all(
os.path.isdir(os.path.join(abspath, m, "models")) for m in _BASE_MODULES
)
_log("candidate[%d] BASE_PKGS_ROOT=%s exists=%s models_ok=%s"
% (idx, abspath, os.path.isdir(abspath), ok))
if ok:
_log("resolved BASE_PKGS_ROOT=%s" % abspath)
return abspath
_die("未找到含 appbase/models 与 rbac/models 的基座包根;"
"请导出 BASE_PKGS_ROOT=<绝对路径> 或 WS_DIR=<机构工作空间> 后重试")
return "" # unreachable
def _sql_type(field: dict) -> str:
"""抽象类型 + length/dec → MariaDB 原生类型。"""
t = (field.get("type") or "").strip().lower()
length = field.get("length")
dec = field.get("dec")
if t in ("str", "varchar"):
n = int(length) if length else 255
return "VARCHAR(%d)" % n
if t == "char":
n = int(length) if length else 1
return "CHAR(%d)" % n
if t in _INT_TYPES:
return _INT_TYPES[t]
if t in ("float", "decimal"):
p = int(length) if length else 15
s = int(dec) if dec else 2
return "DECIMAL(%d,%d)" % (p, s)
if t in ("double", "ddouble"):
return "DOUBLE"
if t == "date":
return "DATE"
if t == "time":
return "TIME"
if t == "datetime":
return "DATETIME"
if t == "timestamp":
# 显式 NULL DEFAULT NULL:MariaDB 对表内首个 TIMESTAMP 列会自动加
# DEFAULT CURRENT_TIMESTAMP ON UPDATE,会让幂等种子每次重写时间戳。
return "TIMESTAMP NULL DEFAULT NULL"
if t == "text":
n = int(length) if length else 0
if n and n <= 65535:
return "TEXT"
return "LONGTEXT"
if t == "bin":
return "LONGBLOB"
_log("WARN 未知抽象类型 %r(字段 %s),按 VARCHAR(255) 兜底" % (t, field.get("name")))
return "VARCHAR(255)"
def _col_ddl(field: dict) -> str:
name = field["name"]
parts = [" `%s` %s" % (name, _sql_type(field))]
nullable = field.get("nullable")
# 规范:只有字符串 "no" 渲染 NOT NULL;null:false / 布尔 False 均不识别,
# 这里同时兼容遗留写法,避免静默按可空列生成。
if nullable == "no" or nullable is False or field.get("null") is False:
parts.append("NOT NULL")
default = field.get("default")
if default is not None and default != "":
dv = str(default)
if dv.upper() in ("CURRENT_TIMESTAMP", "NULL"):
parts.append("DEFAULT %s" % dv)
else:
parts.append("DEFAULT '%s'" % dv.replace("'", "''"))
title = (field.get("title") or "").replace("'", "''")
if title:
parts.append("COMMENT '%s'" % title)
return " ".join(parts)
def load_table_defs(pkgs_root: str):
"""读取 appbase/rbac 的 models/*.json,返回 [(module, table_name, def_dict)]。"""
out = []
for mod in _BASE_MODULES:
mdir = os.path.join(pkgs_root, mod, "models")
for fn in sorted(os.listdir(mdir)):
if not fn.endswith(".json"):
continue
path = os.path.join(mdir, fn)
try:
with open(path, "r", encoding="utf-8") as fh:
d = json.load(fh)
except Exception as exc: # noqa: BLE001
_die("models JSON 解析失败 %s: %s" % (path, exc))
summary = d.get("summary") or []
if not summary:
_die("%s 缺 summary 段(四段式硬约束)" % path)
s0 = summary[0]
tname = s0.get("name") or fn[:-5]
primary = s0.get("primary") or ["id"]
if isinstance(primary, str):
# 规范铁律:primary 必须是数组;字符串会让 DDL 拼出 "i,d"
_die("%s summary.primary 是字符串 %r,应为数组" % (path, primary))
out.append((mod, tname, d, primary, path))
_log("loaded %s/models/%s → table=%s primary=%s fields=%d"
% (mod, fn, tname, ",".join(primary), len(d.get("fields") or [])))
if not out:
_die("基座 models 一张表都没读到,拒绝生成空 DDL")
return out
def render_ddl(defs) -> str:
lines = [
"-- =====================================================================",
"-- base_tables.sql —— 基座模块(appbase / rbac)表结构,由 scripts/gen_base_ddl.py",
"-- 从各模块 models/*.json(单一真源)生成,禁止手工编辑本文件。",
"-- 方言: mariadb / utf8mb4 / utf8mb4_unicode_ci;全部 IF NOT EXISTS(幂等)。",
"-- 修复: Bug 4SxqQAulkH9eAzEys_h-e(up_login.dspy 500 —— users 表缺失)",
"-- Bug 0CQZ7O6ftDRHbxih59upr(种子 4 项 0 行 —— 依赖 role/userrole 就位)",
"-- =====================================================================",
"SET NAMES utf8mb4;",
"",
]
for mod, tname, d, primary, path in defs:
fields = d.get("fields") or []
if not fields:
_die("%s 无 fields 段" % path)
names = [f["name"] for f in fields]
if len(set(names)) != len(names):
_die("%s 字段名重复: %s" % (path, names))
for p in primary:
if p not in names:
_die("%s 主键 %r 不在 fields 中" % (path, p))
lines.append("-- [%s] %s (源: %s)" % (mod, tname, path))
lines.append("CREATE TABLE IF NOT EXISTS `%s` (" % tname)
body = [_col_ddl(f) for f in fields]
# 补齐登录跟踪列(仅 users)
for extra_name, extra_type, extra_comment in _EXTRA_COLUMNS.get(tname, ()):
if extra_name in names:
continue
body.append(" `%s` %s COMMENT '%s'" % (extra_name, extra_type, extra_comment))
names.append(extra_name)
lines.append("-- + 补列 %s(源: rbac/migration/add_login_tracking.sql)" % extra_name)
body.append(" PRIMARY KEY (%s)" % ", ".join("`%s`" % p for p in primary))
seen_idx = set()
for idx in d.get("indexes") or []:
iname = idx.get("name")
ifields = idx.get("idxfields")
if not iname or not ifields:
continue
if isinstance(ifields, str):
ifields = [ifields]
if not all(c in names for c in ifields):
_log("WARN %s 索引 %s 含未知列 %s,跳过" % (tname, iname, ifields))
continue
# 索引名表内唯一:基座 models 里普遍叫 idx1/idx2,跨表重名在 MySQL 合法,
# 但为可读性统一加表名前缀。
uniq = "idx_%s_%s" % (tname, iname)
if uniq in seen_idx:
continue
seen_idx.add(uniq)
kind = "UNIQUE KEY" if (idx.get("idxtype") or "index") == "unique" else "KEY"
body.append(" %s `%s` (%s)"
% (kind, uniq, ", ".join("`%s`" % c for c in ifields)))
lines.append(",\n".join(body))
lines.append(") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;")
lines.append("")
return "\n".join(lines)
def render_alias_map(defs) -> dict:
real_tables = sorted({t for _, t, _, _, _ in defs})
return {
"_comment": "声明口径表名 → models 单一真源表名对照;test/deploy 探针请改用 real 名,"
"避免 ERROR 1146 误报为『表缺失』。real=null 表示该声明项本就零表设计。",
"generated_by": "scripts/gen_base_ddl.py",
"base_tables": real_tables,
"mapping": _DECLARED_TO_REAL,
}
def main() -> int:
ap = argparse.ArgumentParser(description="生成基座模块(appbase/rbac)DDL")
here = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
ap.add_argument("--out", default=os.path.join(here, "scripts", "ddl", "base_tables.sql"))
ap.add_argument("--alias-map",
default=os.path.join(here, "scripts", "ddl", "base_table_alias_map.json"))
args = ap.parse_args()
pkgs_root = resolve_base_pkgs_root()
defs = load_table_defs(pkgs_root)
ddl = render_ddl(defs)
for target in (args.out, args.alias_map):
d = os.path.dirname(os.path.abspath(target))
if d and not os.path.isdir(d):
os.makedirs(d, exist_ok=True)
with open(args.out, "w", encoding="utf-8") as fh:
fh.write(ddl)
with open(args.alias_map, "w", encoding="utf-8") as fh:
json.dump(render_alias_map(defs), fh, ensure_ascii=False, indent=2)
fh.write("\n")
n_tables = ddl.count("CREATE TABLE IF NOT EXISTS")
_log("OK 写出 %s(%d 张表,%d 字节)" % (args.out, n_tables, len(ddl.encode("utf-8"))))
_log("OK 写出 %s" % args.alias_map)
if n_tables < 8:
_die("生成表数 %d < 8,基座 models 读取不完整,拒绝交付" % n_tables)
# 硬门禁:users 必须在内,否则登录端点仍会 1146
if "CREATE TABLE IF NOT EXISTS `users`" not in ddl:
_die("生成的 DDL 不含 users 表 —— up_login.dspy 500 根因未消除")
_log("SELF_CHECK base_ddl: PASS tables=%d users=present" % n_tables)
return 0
if __name__ == "__main__":
raise SystemExit(main())