deliver: 交付收口(引擎代为提交)
This commit is contained in:
parent
bf00609c15
commit
33dadb461d
45
scripts/ddl/base_table_alias_map.json
Normal file
45
scripts/ddl/base_table_alias_map.json
Normal 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
147
scripts/ddl/base_tables.sql
Normal 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
339
scripts/gen_base_ddl.py
Normal 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())
|
||||
Loading…
x
Reference in New Issue
Block a user