Files
workspace/code/fms/FMS新系统核心表结构设计.md
T
2026-09-02 17:31:35 +08:00

57 KiB
Raw Blame History

FMS 新系统模块核心表结构设计

本方案沿用旧系统以 s_module 为核心的元数据模型,删除独立的 s_entity;分类节点、数据模块和查询模块统一由 s_module 自关联树表达。 平台表以 s_ 开头,字段以 b_ 开头,复合词采用驼峰拼接(如 b_viewtable、b_inputdatetime),逻辑关联字段使用 b_xxx_id。

1. 设计原则

本方案坚持五项原则:

  1. 沿用模块主体:s_module 继续作为旧系统的数据配置主体,分类节点和数据节点在同一棵树中维护,不再增加 s_entity。
  2. 模块与字段分离:data/query 模块声明查询源、保存源和主键字段;字段挂模块保存物理列名、字段类型和数据库列约束,列表、表单、查询行为由字段配置子表承载。
  3. 菜单与模块分离:s_menu 是纯导航树,只引用数据模块或固定页面;本方案只恢复旧系统的模块数据主体,不把菜单、路由和角色授权重新塞回 s_module。
  4. 字段定义与视图配置分责:字段定义、列表、表单、查询是四类不同职责;本方案保留 s_field 作为字段定义表,并将列表、表单、查询行为分别放入 s_field_view、s_field_edit、s_field_query,避免一张宽表充满互不相关的空字段。
  5. 配置入口与存储职责分离:管理员可从模块树或菜单树定位数据模块,在一个配置工作台内完成数据源、字段、列表、表单、查询、编码和权限配置;底层仍按模块、菜单和权限分别存储。

其余约定:

  • 内部主键 b_id bigint 由应用层雪花算法生成(全局唯一、带时间序,配置跨系统导出/导入时引用原样保留、无需重映射);跨环境稳定标识使用 b_code(参考旧系统)。
  • 全系统不建立物理外键;关联字段保留索引,引用完整性、跨模块一致性、停用与删除保护统一由服务层维护。
  • 布尔语义使用 int(0/1),与旧系统 s_module 风格一致。
  • 时间统一使用 UTC:字段类型为 datetime2,默认 sysutcdatetime();前端按用户时区转换显示。
  • 审计字段:定义类表保留 b_inputuser_id / b_inputdatetime / b_updateuser_id / b_updatedatetime;关联类表只保留 b_inputdatetime。
  • s_module.b_parent_id 只表达模块树位置;模块间真实的引用、主从、外键和级联关系由 s_relation 表达。
  • b_module_type 使用 category/data/query:分类节点不配置数据源,数据节点可查可存,查询节点只读。
  • 一个数据模块默认只有一套字段和页面配置;菜单只是模块的导航入口,不额外引入菜单级字段覆盖层。

2. 表清单

# 表名 说明
1 s_module 模块树(分类/数据/查询节点,数据所有者)
2 s_field 字段定义(数据库列与通用控件元数据)
3 s_field_group 字段分组(模块级)
4 s_field_view 字段视图配置(列表显示、排序、列宽、分组)
5 s_field_edit 字段表单配置(显示、排序、分组、必填、只读、禁用)
6 s_field_query 字段查询配置(关联字段、操作符、默认值)
7 s_relation 模块关系(引用/主从/多对多,驱动外键校验与级联)
8 s_menu 菜单/页面导航(data 菜单直接绑定数据模块)
9 s_autocode 自动编码规则
10 s_role 角色
11 s_user_role 用户-角色
12 s_power 权限点(menu/module/action/field)
13 s_role_power 角色-权限(允许/拒绝)
14 s_role_scope 角色-模块数据范围
15 s_i18n 多语言资源
16 s_log_technical 技术日志(请求链路/SQL/异常/性能)
17 s_log_login 登录日志(登录/退出/失败/锁定)
18 s_log_audit 业务审计事件
19 s_log_audit_field 审计字段变更明细

3. 各表结构

3.1 s_module 模块

沿用旧系统核心语义:s_module 既是模块树节点,也是数据配置主体。category 节点只负责分类;data 节点可查询和保存;query 节点只查询不保存。菜单、角色和权限仍由独立表维护。

create table s_module (
    b_id bigint not null,                -- 雪花主键(应用层生成,全局唯一)
    b_code varchar(50) not null,                        -- 模块编码(全局唯一)
    b_parent_id bigint null,                            -- 父模块(配置树位置)
    b_name nvarchar(100) not null,                      -- 模块名称
    b_i18n varchar(150) null,                           -- 显示名多语言 key
    b_module_type varchar(20) not null,                 -- category/data/query
    b_viewtable varchar(128) null,                      -- 查询数据源(data/query 使用)
    b_savetable varchar(128) null,                      -- 保存目标(data 可用,query 必须为空)
    b_keyfield varchar(50) null,                        -- 主键字段(data/query 使用)
    b_orderfield varchar(500) null,                     -- 默认排序表达式
    b_delete_policy varchar(20) null,                   -- restrict/lifecycle/hard;级联由 s_relation 表达
    b_canuse int not null default 1,                    -- 是否启用
    b_xh int not null default 0,                        -- 排序号
    b_bz nvarchar(1000) null,                           -- 描述
    b_inputuser_id bigint null,
    b_inputdatetime datetime2 null default sysutcdatetime(),
    b_updateuser_id bigint null,
    b_updatedatetime datetime2 null,
    constraint pk_s_module primary key (b_id),
    constraint ux_s_module_code unique (b_code),
    constraint ck_s_module_type check (b_module_type in ('category', 'data', 'query')),
    constraint ck_s_module_delete_policy check (b_delete_policy is null or b_delete_policy in ('restrict', 'lifecycle', 'hard')),
    constraint ck_s_module_config check (
        (b_module_type = 'category' and b_viewtable is null and b_savetable is null and b_keyfield is null and b_orderfield is null and b_delete_policy is null)
        or (b_module_type = 'data' and b_viewtable is not null and b_savetable is not null and b_keyfield is not null and b_delete_policy is not null)
        or (b_module_type = 'query' and b_viewtable is not null and b_savetable is null and b_delete_policy is null)
    ),
    constraint ck_s_module_parent check (b_parent_id is null or b_parent_id <> b_id),
    -- constraint fk_s_module_parent foreign key (b_parent_id) references s_module (b_id)
);

create index ix_s_module_parent on s_module (b_parent_id, b_xh, b_canuse, b_id);
create index ix_s_module_savetable on s_module (b_savetable);

模块的查/存配置方式:

分类模块: b_module_type = category, 查/存/主键/排序/删除策略全部为空
数据模块: b_module_type = data, b_viewtable/b_savetable/b_keyfield/b_delete_policy 必填
查询模块: b_module_type = query, b_viewtable 必填, b_savetable/b_delete_policy 必须为空;汇总查询可不填 b_keyfield

数据库约束只能判断空值,服务层还必须拒绝空字符串,并校验表、视图、主键和排序字段真实存在。模块树、菜单树和字段分组树在修改父节点时必须检查完整祖先链,禁止 A -> B -> C -> A 形式的间接循环。

删除策略取值(b_delete_policy):

restrict  主数据:有引用时拒绝删除,只能停用或合并
lifecycle 业务单据:草稿且无下游时可删,正式后只能取消/作废
hard      临时记录:校验权限后直接物理删除

模块删除策略只控制“直接删除当前模块记录”是否允许;主记录删除时如何处理从记录由具体 s_relation.b_delete_policy 决定,因此模块层不再重复设置 cascade。

3.2 s_field 字段

只保存字段本身的规范定义,不混入列表、表单和查询布局。字段同步、数据库列校验和业务审计均以本表为准;三类界面行为分别放在后续配置表中。

create table s_field (
    b_id bigint not null,                -- 雪花主键(应用层生成,全局唯一)
    b_module_id bigint not null,                        -- 所属数据模块
    b_field varchar(50) not null,                       -- 物理列名(与真实表列一致)
    b_name nvarchar(100) not null,                      -- 字段名称
    b_i18n varchar(150) null,                           -- 显示名多语言 key
    b_type varchar(30) not null,                        -- 字段类型(input/textarea/number/money/date/datetime/checkbox/select/selectId...)
    b_dbtype varchar(30) not null,                      -- 数据库存储类型(varchar/nvarchar/int/bigint/decimal/date/datetime2/bit/text)
    b_length int null,                                  -- 长度
    b_precision int null,                               -- 数值精度(总位数)
    b_scale int null,                                   -- 数值小数位
    b_nullable int not null default 1,                  -- 数据库层是否可空
    b_default_value nvarchar(500) null,                 -- 固定业务/UI 默认值(文本或 JSON 表达)
    b_db_default nvarchar(500) null,                    -- 数据库列默认表达式(结构同步得到)
    b_writemode varchar(20) not null default 'direct',  -- direct/createOnly/readOnly/computed/system
    b_options nvarchar(max) null,                       -- 字段类型配置(JSON;静态选项或 selectId 选择器配置)
    b_canuse int not null default 1,                    -- 是否启用(停用隐藏并保留数据)
    b_inputuser_id bigint null,
    b_inputdatetime datetime2 null default sysutcdatetime(),
    b_updateuser_id bigint null,
    b_updatedatetime datetime2 null,
    constraint pk_s_field primary key (b_id),
    constraint ux_s_field_module_field unique (b_module_id, b_field),
    constraint ux_s_field_module_id unique (b_module_id, b_id),
    -- constraint fk_s_field_module foreign key (b_module_id) references s_module (b_id),
    constraint ck_s_field_type check (b_type in ('input', 'textarea', 'number', 'money', 'date', 'datetime', 'checkbox', 'select', 'selectId')),
    constraint ck_s_field_dbtype check (b_dbtype in ('varchar', 'nvarchar', 'char', 'nchar', 'int', 'bigint', 'smallint', 'tinyint', 'decimal', 'numeric', 'float', 'real', 'date', 'datetime', 'datetime2', 'bit', 'text', 'ntext', 'uniqueidentifier')),
    constraint ck_s_field_writemode check (b_writemode in ('direct', 'createOnly', 'readOnly', 'computed', 'system'))
);

create index ix_s_field_module on s_field (b_module_id, b_canuse, b_id);

b_type 沿用旧系统语义,直接指定字段的业务/控件类型,而不是数据库字段类型。数据库真实列类型记录在 b_dbtype,用于同步和结构校验;前端默认控件、值转换和展示规则由 b_type 决定。服务层必须执行类型兼容校验:checkbox 只能映射 bit/int;number 映射整数或数值类型;money 映射 decimal/numeric;date 映射 date/datetime/datetime2;datetime 映射 datetime/datetime2;selectId 映射可承载目标 ID 的整数、字符或 uniqueidentifier;input/textarea 映射字符类型;select 可映射字符或整数类型。b_default_value 只表示新增表单时可直接使用的固定默认值,b_db_default 是数据库列默认表达式,两者不能混用。

b_writemode 是通用保存服务的强制规则:direct 可新增可修改,createOnly 仅新增时写入,readOnly/computed 永不接收客户端写入,system 仅允许受信任的系统逻辑写入。查询视图中的显示列、计算列和 v_xxx 字段必须标记为 readOnly/computed,即使它们出现在 s_field_edit 中也不能被保存接口写入。

依赖其他业务数据的默认值不在 s_field 中配置通用规则,也不增加默认值类型字段。此类值由具体业务模块的服务逻辑处理:例如新增业务单据时,根据单据币种和业务日期查询汇率模块,将汇率写入单据作为当时的业务快照;用户修改币种或日期时,由该业务服务重新计算,是否覆盖人工修改由具体业务规则决定。

第一阶段沿用旧系统已有类型:

input       单行文本
textarea    多行文本
number      数字
money       金额
date        日期
datetime    日期时间
checkbox    布尔/复选框
select      静态枚举下拉选择,直接保存选项值
selectId    关联模块选择器,保存目标记录 ID

select 的静态选项直接放在 b_options。selectId 必须对应一条以当前字段作为外键端的启用 s_relation;b_options 只保存选择器展示配置,例如 relationCode、labelField、searchFields 和是否允许清空,不保存目标模块的动态数据。服务层保存配置时必须校验 relation、目标模块以及显示/搜索字段真实存在且已启用。后续增加人员选择、附件、级联选择等自定义类型时,继续扩展 b_type 的受控枚举,并在 b_options 保存该类型的专用配置。

示例:

{
  "relationCode": "booking.customer",
  "labelField": "b_name",
  "searchFields": ["b_code", "b_name"],
  "allowClear": true
}

3.3 s_field_group 字段分组

分组是模块级独立定义,支持分组树。列表和表单可以分别通过自己的配置表引用同一个分组,也可以选择不同分组。配置表冗余保存 b_module_id,通过复合外键保证字段、分组和配置始终属于同一个模块。

create table s_field_group (
    b_id bigint not null,                -- 雪花主键(应用层生成,全局唯一)
    b_module_id bigint not null,                        -- 所属数据模块
    b_parent_id bigint null,                            -- 父分组(分组树)
    b_title nvarchar(100) not null,                     -- 分组标题
    b_i18n varchar(150) null,                           -- 标题多语言 key
    b_layout_direction varchar(10) null,                -- 分组内字段排列方向(vertical/horizontal)
    b_colspan int not null default 48,                  -- 分组在外层 48 宫格中的占格数(默认 48 独占一行)
    b_xh int not null default 0,                        -- 分组排序
    b_canuse int not null default 1,                    -- 是否启用(停用整组隐藏)
    constraint pk_s_field_group primary key (b_id),
    constraint ux_s_field_group_module_id unique (b_module_id, b_id),
    constraint ck_s_field_group_parent check (b_parent_id is null or b_parent_id <> b_id),
    -- constraint fk_s_field_group_module foreign key (b_module_id) references s_module (b_id),
    -- constraint fk_s_field_group_parent foreign key (b_module_id, b_parent_id) references s_field_group (b_module_id, b_id)
);

create index ix_s_field_group_module on s_field_group (b_module_id, b_xh, b_canuse);

3.4 s_field_view 字段视图配置

保存字段在列表中的显示、顺序、宽度和分组。每个模块字段最多一条列表配置;未创建配置记录表示该字段不进入列表。

create table s_field_view (
    b_id bigint not null,
    b_module_id bigint not null,                        -- 所属数据模块(与字段、分组一致)
    b_field_id bigint not null,                         -- 字段
    b_group_id bigint null,                             -- 列表分组
    b_i18n varchar(150) null,                           -- 列表标题多语言 key(空则继承字段)
    b_visible int not null default 1,                   -- 是否显示
    b_xh int not null default 0,                        -- 列表排序
    b_width int null,                                   -- 列宽(px)
    constraint pk_s_field_view primary key (b_id),
    constraint ux_s_field_view_field unique (b_module_id, b_field_id),
    -- constraint fk_s_field_view_module foreign key (b_module_id) references s_module (b_id),
    -- constraint fk_s_field_view_field foreign key (b_module_id, b_field_id) references s_field (b_module_id, b_id),
    -- constraint fk_s_field_view_group foreign key (b_module_id, b_group_id) references s_field_group (b_module_id, b_id)
);

create index ix_s_field_view_order on s_field_view (b_xh, b_id);

3.5 s_field_edit 字段表单配置

保存字段在新增、编辑表单中的布局和交互行为。每个模块字段最多一条表单配置;字段的默认控件类型由 s_field.b_type 决定,本表只配置当前表单中的显示、必填、只读和禁用状态。

create table s_field_edit (
    b_id bigint not null,
    b_module_id bigint not null,                        -- 所属数据模块(与字段、分组一致)
    b_field_id bigint not null,                         -- 字段
    b_group_id bigint null,                             -- 表单分组
    b_visible int not null default 1,                   -- 是否显示
    b_required int not null default 0,                  -- 是否必填
    b_readonly int not null default 0,                  -- 是否只读
    b_disabled int not null default 0,                  -- 是否禁用
    b_xh int not null default 0,                        -- 表单排序
    constraint pk_s_field_edit primary key (b_id),
    constraint ux_s_field_edit_field unique (b_module_id, b_field_id),
    -- constraint fk_s_field_edit_module foreign key (b_module_id) references s_module (b_id),
    -- constraint fk_s_field_edit_field foreign key (b_module_id, b_field_id) references s_field (b_module_id, b_id),
    -- constraint fk_s_field_edit_group foreign key (b_module_id, b_group_id) references s_field_group (b_module_id, b_id)
);

create index ix_s_field_edit_order on s_field_edit (b_xh, b_id);

数据库可空性以 s_field.b_nullable 为准,数据库默认值以 s_field.b_db_default 为准,表单必填以 s_field_edit.b_required 为准;表单组件直接按 s_field.b_type 选择,不在 s_field_edit 重复配置。元数据校验器应保证:数据库不可空、无数据库默认值且允许客户端写入的字段,新增表单不能配置为非必填;readOnly/computed/system 字段不能配置为客户端必填。

3.6 s_field_query 字段查询配置

查询条件是可选集合,只有需要进入查询区的字段才创建记录。第一阶段所有启用条件按 b_xh 排序,并统一使用 AND 组合;暂不支持 OR、嵌套括号和条件组。同一字段可以配置多个不同操作符,也允许通过 b_condition_no 配置多个相同操作符的条件。b_module_id 表示查询所属模块,b_field_module_id 表示条件字段实际所属模块;查询本模块字段时二者相同,关联查询时必须通过 b_relation_id 指向一条从查询模块可达的启用关系。查询组件优先使用配置中的 b_component;为空时再根据 s_field.b_type 和操作符推导。

create table s_field_query (
    b_id bigint not null,
    b_module_id bigint not null,                        -- 查询所属模块
    b_field_module_id bigint not null,                  -- 条件字段所属模块
    b_field_id bigint not null,                         -- 条件字段
    b_relation_id bigint null,                          -- 关联字段使用的模块关系
    b_component varchar(30) null,                       -- 查询控件覆盖(input/select/date/datetime/number/checkbox...)
    b_condition_no int not null default 1,               -- 同模块同字段同操作符的条件序号
    b_operator varchar(20) not null,                    -- eq/like/gt/ge/lt/le/in/between
    b_default_value nvarchar(500) null,                 -- 默认查询值
    b_canuse int not null default 1,                    -- 是否启用
    b_xh int not null default 0,                        -- 查询条件排序
    constraint pk_s_field_query primary key (b_id),
    constraint ux_s_field_query_condition unique (b_module_id, b_field_module_id, b_field_id, b_relation_id, b_operator, b_condition_no),
    -- constraint fk_s_field_query_module foreign key (b_module_id) references s_module (b_id),
    -- constraint fk_s_field_query_field foreign key (b_field_module_id, b_field_id) references s_field (b_module_id, b_id)
);

create index ix_s_field_query_order on s_field_query (b_canuse, b_xh, b_id);

b_operator 表达查询条件的比较语义,默认控件由字段定义和操作符组合推导:b_options 非空时优先使用下拉/多选,datetime 使用日期或日期时间控件,数值和日期字段在 between 操作符下使用范围控件。服务层必须校验操作符与字段类型匹配,并将 in/between 的默认值按约定 JSON/数组格式解析。若 b_relation_id 为空,字段必须属于查询模块;若不为空,关系必须连接查询模块与字段模块,并验证字段确实位于关系指定的一侧。第一阶段运行时对所有启用条件追加 AND,未来如需 OR 或括号,再增加独立的条件组模型。

3.7 s_relation 模块关系

描述模块间的引用与主子关系,运行时据此执行外键校验、主子事务与级联删除。b_module_id 和 b_target_module_id 是关系两端,不预设哪一端保存外键;b_foreign_side 明确外键位于哪一端,两个端点字段都使用 s_field 主键引用,避免只写物理列名导致跨模块歧义。

create table s_relation (
    b_id bigint not null,                -- 雪花主键(应用层生成,全局唯一)
    b_code varchar(50) not null,                        -- 跨环境稳定关系编码
    b_module_id bigint not null,                        -- 关系端 A 模块
    b_target_module_id bigint not null,                 -- 关系端 B 模块
    b_relation_type varchar(20) not null,               -- one-to-one/one-to-many/many-to-one/many-to-many
    b_module_field_id bigint not null,                  -- 端 A 关联字段
    b_target_field_id bigint not null,                  -- 端 B 关联字段
    b_foreign_side varchar(10) null,                    -- 普通关系外键所在端:module/target
    b_junction_module_id bigint null,                   -- many-to-many 中间模块
    b_junction_module_field_id bigint null,             -- 中间模块指向端 A 字段
    b_junction_target_field_id bigint null,             -- 中间模块指向端 B 字段
    b_ownership varchar(20) not null default 'reference', -- reference 引用/owned 主从
    b_owner_side varchar(10) null,                      -- owned 时主/拥有者端:module/target
    b_delete_policy varchar(20) null,                   -- restrict/set-null/cascade/archive
    b_order_module_id bigint null,                      -- 从/被拥有模块
    b_order_field_id bigint null,                       -- 从模块排序字段
    b_canuse int not null default 1,                    -- 是否启用
    b_xh int not null default 0,                        -- 排序号
    constraint pk_s_relation primary key (b_id),
    constraint ux_s_relation_code unique (b_code),
    -- constraint fk_s_relation_module foreign key (b_module_id) references s_module (b_id),
    -- constraint fk_s_relation_target_module foreign key (b_target_module_id) references s_module (b_id),
    -- constraint fk_s_relation_module_field foreign key (b_module_id, b_module_field_id) references s_field (b_module_id, b_id),
    -- constraint fk_s_relation_target_field foreign key (b_target_module_id, b_target_field_id) references s_field (b_module_id, b_id),
    -- constraint fk_s_relation_junction_module foreign key (b_junction_module_id) references s_module (b_id),
    -- constraint fk_s_relation_junction_module_field foreign key (b_junction_module_id, b_junction_module_field_id) references s_field (b_module_id, b_id),
    -- constraint fk_s_relation_junction_target_field foreign key (b_junction_module_id, b_junction_target_field_id) references s_field (b_module_id, b_id),
    -- constraint fk_s_relation_order_module foreign key (b_order_module_id) references s_module (b_id),
    -- constraint fk_s_relation_order_field foreign key (b_order_module_id, b_order_field_id) references s_field (b_module_id, b_id),
    constraint ux_s_relation_key unique (b_module_id, b_target_module_id, b_module_field_id, b_target_field_id),
    constraint ck_s_relation_self_fields check (b_module_id <> b_target_module_id or b_module_field_id <> b_target_field_id),
    constraint ck_s_relation_type check (b_relation_type in ('one-to-one', 'one-to-many', 'many-to-one', 'many-to-many')),
    constraint ck_s_relation_foreign_side check (b_foreign_side is null or b_foreign_side in ('module', 'target')),
    constraint ck_s_relation_ownership check (b_ownership in ('reference', 'owned')),
    constraint ck_s_relation_owner_side check (b_owner_side is null or b_owner_side in ('module', 'target')),
    constraint ck_s_relation_shape check (
        (b_relation_type = 'many-to-many' and b_foreign_side is null and b_junction_module_id is not null and b_junction_module_field_id is not null and b_junction_target_field_id is not null)
        or (b_relation_type <> 'many-to-many' and b_foreign_side is not null and b_junction_module_id is null and b_junction_module_field_id is null and b_junction_target_field_id is null)
    ),
    constraint ck_s_relation_owned check (
        (b_ownership = 'reference' and b_owner_side is null)
        or (b_ownership = 'owned' and b_owner_side is not null and b_relation_type <> 'many-to-many')
    ),
    constraint ck_s_relation_order check (
        (b_order_module_id is null and b_order_field_id is null)
        or (b_order_module_id is not null and b_order_field_id is not null)
    ),
    constraint ck_s_relation_delete_policy check (b_delete_policy is null or b_delete_policy in ('restrict', 'set-null', 'cascade', 'archive'))
);

create index ix_s_relation_target on s_relation (b_target_module_id);

-- 不建物理外键:s_field_query.b_relation_id -> s_relation.b_id 为逻辑外键,由服务层校验

关系配置必须满足以下规则:

  • b_code 是跨环境稳定标识,selectId.b_options.relationCode 和配置导入导出都使用该编码;运行时内部关联仍可使用 b_id。
  • 允许同一模块自关联,用于组织树、父子记录等场景;自关联时两端字段不能相同,并仍需通过 b_foreign_side 明确父键和外键端。
  • one-to-one/one-to-many/many-to-one 不填写中间模块字段且必须明确 b_foreign_side;many-to-many 必须同时填写中间模块及两个中间外键字段,且不填写 b_foreign_side。
  • 普通关系由 b_foreign_side 指定哪一端保存外键;外键字段与另一端被引用字段类型必须兼容;one-to-one 还要求外键字段具有唯一约束。
  • one-to-many/many-to-one 必须与 b_foreign_side 的方向一致:外键在端 B 时端 A 为“一”,外键在端 A 时端 B 为“一”。set-null 仅允许实际外键字段可空时使用。
  • reference 不填写 b_owner_side,默认删除策略为 restrict/set-null;owned 必须明确 b_owner_side,另一端才是从/被拥有端,只有 owned 关系允许 cascade/archive、主从事务保存和从表排序。
  • b_order_module_id/b_order_field_id 必须同时为空或同时有值;有值时模块必须是 owned 关系的从端,字段必须属于该模块。
  • 模块自身的 b_delete_policy 决定该模块记录能否进入删除流程;关系的 b_delete_policy 决定删除发生后如何处理关联记录,两者都必须通过才可执行。
  • 同一对模块可以存在多条关系,但“两端模块 + 两侧字段”必须唯一;配置、导入导出使用关系 b_code,运行时和关联查询使用关系 b_id。

3.8 s_menu 菜单/页面

菜单是独立导航树,不保存字段、列表或表单配置。data 菜单通过 b_module_id 绑定一个 data/query 模块,并直接使用该模块的字段、列表、表单和查询配置。

create table s_menu (
    b_id bigint not null,                -- 雪花主键(应用层生成,全局唯一)
    b_parent_id bigint null,                            -- 父菜单(导航树自引用)
    b_code varchar(50) not null,                        -- 菜单编码
    b_name nvarchar(100) not null,                      -- 菜单名称
    b_i18n varchar(150) null,                           -- 显示名多语言 key
    b_menu_type varchar(20) not null,                   -- directory/page/data/external
    b_route varchar(200) null,                          -- 固定页面路由或外链
    b_module_id bigint null,                            -- data 页面绑定的数据/查询模块
    b_power_code varchar(50) null,                      -- 页面访问权限编码(对应 s_power.b_code)
    b_icon nvarchar(max) null,                          -- 图标:lucide:Name 引用 lucide 图标;其他字符串视为 SVG 标记
    b_xh int not null default 0,                        -- 排序号
    b_canuse int not null default 1,                    -- 是否启用(停用后不显示且不可访问)
    constraint pk_s_menu primary key (b_id),
    constraint ux_s_menu_code unique (b_code),
    constraint ck_s_menu_parent check (b_parent_id is null or b_parent_id <> b_id),
    -- constraint fk_s_menu_parent foreign key (b_parent_id) references s_menu (b_id),
    -- constraint fk_s_menu_module foreign key (b_module_id) references s_module (b_id)
);

create index ix_s_menu_parent on s_menu (b_parent_id, b_xh, b_canuse);
create index ix_s_menu_module on s_menu (b_module_id);

菜单类型约束由元数据校验器执行:directory 不填业务路由;page 必须填 b_route;data 必须填 b_module_id;external 必须填 b_route 和 b_power_code。目录仅在存在至少一个可访问子页面时显示。

配置工作台按以下顺序呈现:页面基本信息 → 绑定模块 → 字段与分组 → 列表 → 表单 → 查询 → 关联模块 → 自动编码 → 权限。保存时由服务层校验菜单类型、模块类型和模块关系。

没有独立菜单的子模块、费用明细、选择器和公共字典,通过主模块的 s_relation 挂在所属数据模块下;它们不需要重复创建菜单,也可以在同一个配置工作台中维护。

3.9 s_autocode 自动编码

挂数据模块 + 目标字段,规则可复用。并发控制:应用层使用原子更新取号(update ... set b_currentvalue = b_currentvalue + 1 output inserted.b_currentvalue),避免自增竞争。

create table s_autocode (
    b_id bigint not null,                -- 雪花主键(应用层生成,全局唯一)
    b_module_id bigint not null,                        -- 所属数据模块
    b_field_id bigint not null,                         -- 目标字段
    b_prefix nvarchar(100) not null default '',         -- 前缀
    b_dateformat varchar(30) not null default 'yyyyMM', -- 日期格式
    b_separator nvarchar(10) not null default '-',      -- 分隔符
    b_seqwidth int not null default 5,                  -- 流水位宽
    b_resettype varchar(10) not null default 'month',   -- 重置周期:none/day/month/year
    b_startvalue bigint not null default 1,             -- 起始值
    b_currentperiod varchar(8) null,                    -- 当前周期
    b_currentvalue bigint not null default 0,           -- 当前流水值
    b_canuse int not null default 1,                    -- 是否启用
    constraint pk_s_autocode primary key (b_id),
    constraint ux_s_autocode_module_field unique (b_module_id, b_field_id),
    -- constraint fk_s_autocode_module foreign key (b_module_id) references s_module (b_id),
    -- constraint fk_s_autocode_field foreign key (b_module_id, b_field_id) references s_field (b_module_id, b_id),
    constraint ck_s_autocode_resettype check (b_resettype in ('none', 'day', 'month', 'year')),
    constraint ck_s_autocode_values check (b_seqwidth > 0 and b_startvalue >= 0 and b_currentvalue >= 0)
);

自动编码目标字段必须是当前模块的可写字符字段;服务层还需校验目标字段未配置为 computed/system/readOnly,并保证业务字段上的唯一性约束。规则周期切换、取号和业务保存必须在同一事务内完成。

3.10 s_role 角色

create table s_role (
    b_id bigint not null,                -- 雪花主键(应用层生成,全局唯一)
    b_code varchar(50) not null,                        -- 角色编码
    b_name nvarchar(100) not null,                      -- 角色名称
    b_i18n varchar(150) null,                           -- 显示名多语言 key
    b_remark nvarchar(500) null,                        -- 描述
    b_is_system int not null default 0,                 -- 系统内置角色(禁止普通管理员删除)
    b_bypass_power int not null default 0,              -- 是否绕过业务权限校验(仅超级管理员)
    b_canuse int not null default 1,                    -- 是否启用
    b_inputuser_id bigint null,
    b_inputdatetime datetime2 null default sysutcdatetime(),
    b_updateuser_id bigint null,
    b_updatedatetime datetime2 null,
    constraint pk_s_role primary key (b_id),
    constraint ux_s_role_code unique (b_code),
    constraint ck_s_role_system_bypass check (b_bypass_power = 0 or b_is_system = 1)
);

3.11 s_user_role 用户-角色

create table s_user_role (
    b_id bigint not null,                -- 雪花主键(应用层生成,全局唯一)
    b_user_id bigint not null,                          -- 用户(逻辑外键,当前身份系统)
    b_role_id bigint not null,                          -- 角色
    b_canuse int not null default 1,                    -- 是否启用该关系
    b_inputdatetime datetime2 null default sysutcdatetime(),
    constraint pk_s_user_role primary key (b_id),
    constraint ux_s_user_role unique (b_user_id, b_role_id),
    -- constraint fk_s_user_role_role foreign key (b_role_id) references s_role (b_id)
);

create index ix_s_user_role_role on s_user_role (b_role_id, b_canuse);

3.12 s_power 权限点

create table s_power (
    b_id bigint not null,                -- 雪花主键(应用层生成,全局唯一)
    b_code varchar(50) not null,                        -- 权限编码(如 booking.confirm)
    b_name nvarchar(100) not null,                      -- 权限名称
    b_i18n varchar(150) null,                           -- 显示名多语言 key
    b_power_type varchar(20) not null,                  -- menu/module/action/field
    b_resource varchar(128) null,                       -- 菜单 code/固定页面或扩展资源 code
    b_module_id bigint null,                            -- module/action/field 所属模块
    b_field_id bigint null,                             -- field 权限对应字段
    b_operation varchar(30) not null,                   -- access/read/create/update/delete/export/execute/view/edit
    b_canuse int not null default 1,                    -- 是否启用
    b_xh int not null default 0,                        -- 排序号
    constraint pk_s_power primary key (b_id),
    constraint ux_s_power_code unique (b_code),
    -- constraint fk_s_power_module foreign key (b_module_id) references s_module (b_id),
    -- constraint fk_s_power_field foreign key (b_module_id, b_field_id) references s_field (b_module_id, b_id),
    constraint ck_s_power_type check (b_power_type in ('menu', 'module', 'action', 'field')),
    constraint ck_s_power_shape check (
        (b_power_type = 'menu' and b_resource is not null and b_module_id is null and b_field_id is null and b_operation = 'access')
        or (b_power_type = 'module' and b_resource is null and b_module_id is not null and b_field_id is null and b_operation in ('read', 'create', 'update', 'delete', 'export'))
        or (b_power_type = 'action' and b_resource is not null and b_module_id is not null and b_field_id is null and b_operation = 'execute')
        or (b_power_type = 'field' and b_resource is null and b_module_id is not null and b_field_id is not null and b_operation in ('view', 'edit'))
    )
);

create index ix_s_power_resource on s_power (b_power_type, b_resource, b_canuse);

权限点的资源约束由服务层补充:menu 使用菜单编码且操作只能为 access;module 必须填写 b_module_id 且操作为 read/create/update/delete/export;action 必须填写模块和动作编码且操作为 execute;field 必须同时填写模块、字段且操作为 view/edit。权限编码 b_code 在全库唯一,资源停用后对应权限自动失效。

3.13 s_role_power 角色-权限

create table s_role_power (
    b_id bigint not null,                -- 雪花主键(应用层生成,全局唯一)
    b_role_id bigint not null,                          -- 角色
    b_power_id bigint not null,                         -- 权限点
    b_effect varchar(10) not null default 'allow',      -- allow/deny
    b_canuse int not null default 1,                    -- 是否启用
    b_inputdatetime datetime2 null default sysutcdatetime(),
    constraint pk_s_role_power primary key (b_id),
    constraint ux_s_role_power unique (b_role_id, b_power_id),
    -- constraint fk_s_role_power_role foreign key (b_role_id) references s_role (b_id),
    -- constraint fk_s_role_power_power foreign key (b_power_id) references s_power (b_id),
    constraint ck_s_role_power_effect check (b_effect in ('allow', 'deny'))
);

create index ix_s_role_power_power on s_role_power (b_power_id, b_canuse);

3.14 s_role_scope 角色-模块数据范围

数据范围控制用户通过某个模块操作时可访问哪些业务记录。范围按角色、模块和操作配置,不承担租户隔离;本系统采用一个租户一个数据库,数据范围只解决租户内部的本人、部门、负责人等业务授权。

create table s_role_scope (
    b_id bigint not null,
    b_role_id bigint not null,                          -- 角色
    b_module_id bigint not null,                        -- 数据模块
    b_operation varchar(20) not null,                   -- read/update/delete/export
    b_scope_type varchar(30) not null,                  -- all/creator/owner/department/department-tree/custom
    b_scope_no int not null default 1,                   -- 同角色同模块同操作的范围序号
    b_scope_field_id bigint null,                       -- owner/department/custom 用于匹配的业务字段
    b_scope_value nvarchar(max) null,                   -- 固定值或自定义条件(JSON)
    b_canuse int not null default 1,
    b_inputdatetime datetime2 null default sysutcdatetime(),
    constraint pk_s_role_scope primary key (b_id),
    constraint ux_s_role_scope unique (b_role_id, b_module_id, b_operation, b_scope_type, b_scope_field_id, b_scope_no),
    -- constraint fk_s_role_scope_role foreign key (b_role_id) references s_role (b_id),
    -- constraint fk_s_role_scope_module foreign key (b_module_id) references s_module (b_id),
    -- constraint fk_s_role_scope_field foreign key (b_module_id, b_scope_field_id) references s_field (b_module_id, b_id),
    constraint ck_s_role_scope_operation check (b_operation in ('read', 'update', 'delete', 'export')),
    constraint ck_s_role_scope_type check (b_scope_type in ('all', 'creator', 'owner', 'department', 'department-tree', 'custom')),
    constraint ck_s_role_scope_no check (b_scope_no > 0),
    constraint ck_s_role_scope_shape check (
        (b_scope_type in ('all', 'creator') and b_scope_field_id is null and b_scope_value is null)
        or (b_scope_type in ('owner', 'department', 'department-tree') and b_scope_field_id is not null)
        or (b_scope_type = 'custom' and b_scope_value is not null)
    )
);

create index ix_s_role_scope_role on s_role_scope (b_role_id, b_module_id, b_operation, b_canuse);

权限运行规则:

  • 默认拒绝:用户必须通过至少一个启用角色取得权限;b_bypass_power = 1 的受保护系统角色除外。
  • 多角色权限先合并,任一有效 deny 优先于所有 allow;没有 allow 视为无权。超级管理员角色只能由受保护的系统管理接口分配,不能通过普通角色维护接口创建或提权。
  • 访问数据页需要同时具备菜单 access、模块 read 和对应操作权限;前端只负责隐藏,后端接口必须按同一权限编码再次校验。
  • 字段权限区分 view 与 edit。未配置字段权限时继承模块权限;存在字段级 deny 时,查询结果必须移除或脱敏该字段,保存接口必须忽略并拒绝提交无编辑权限的字段。
  • 数据范围在操作权限通过后追加。creator 匹配记录创建人,owner/department/department-tree 通过 b_scope_field_id 与当前登录上下文匹配;custom 使用结构化 JSON 条件并只允许引用当前模块字段。
  • 用户拥有多个角色时,同一操作的数据范围取并集;同一角色内多个范围记录也取并集,任一角色/范围为 all 即不再追加范围条件。没有有效范围记录时,即使拥有操作权限也默认无数据,避免误放开全表。

菜单显示继承页面访问权限:用户拥有 s_menu.b_power_code 对应权限时菜单才显示;前端只控制显示,后端运行时必须再次校验同一个权限编码。

3.15 s_i18n 多语言

create table s_i18n (
    b_id bigint not null,                -- 雪花主键(应用层生成,全局唯一)
    b_key varchar(150) not null,                        -- 资源 key(如 booking.name)
    b_locale varchar(20) not null,                      -- 语言区域(如 zh-CN/en-US)
    b_value nvarchar(500) not null,                     -- 该语言下的显示文本
    b_i18ntype varchar(20) null,                        -- 资源类型:module/field/menu/action
    b_canuse int not null default 1,                    -- 是否启用
    constraint pk_s_i18n primary key (b_id),
    constraint ux_s_i18n_key_locale unique (b_key, b_locale)
);

create index ix_s_i18n_locale on s_i18n (b_locale, b_canuse);

3.16 s_log_technical 技术日志

用于请求链路、SQL、异常、性能与外部接口排查。高频日志推荐写入 Loki/OpenSearch/Elasticsearch 等日志平台,本表用于集中日志库或短期数据库兜底,不建议长期写入租户业务库。

create table s_log_technical (
    b_id bigint not null,                -- 雪花主键(应用层生成,全局唯一)
    b_user_id bigint null,                              -- 操作用户(逻辑外键)
    b_account nvarchar(50) null,                        -- 登录名快照
    b_sourcetype varchar(20) null,                      -- 来源类型:web/api
    b_loglevel varchar(16) not null,                    -- 级别:INFO/WARN/ERROR
    b_logcategory varchar(64) null,                     -- 分类:sql/exception/performance
    b_operation varchar(100) null,                      -- 操作名(如 loadData/saveObject)
    b_module_code varchar(50) null,                     -- 相关模块编码
    b_request_id varchar(64) null,                      -- 请求 ID
    b_trace_id varchar(64) null,                        -- 追踪 ID(串联链路)
    b_server_node varchar(128) null,                    -- 服务节点标识
    b_duration_ms bigint null,                          -- 耗时(毫秒)
    b_result_code varchar(50) null,                     -- 结果码
    b_error_code varchar(50) null,                      -- 错误码
    b_error_message nvarchar(2000) null,                -- 错误信息
    b_exception_text nvarchar(max) null,                -- 异常堆栈(文本)
    b_context_text nvarchar(max) null,                  -- 上下文信息(文本)
    b_occurdatetime datetime2 null default sysutcdatetime(), -- 发生时间(UTC)
    constraint pk_s_log_technical primary key (b_id)
);

create index ix_s_log_technical_trace on s_log_technical (b_trace_id, b_occurdatetime desc);
create index ix_s_log_technical_level on s_log_technical (b_loglevel, b_occurdatetime desc);

3.17 s_log_login 登录日志

记录登录成功、失败、退出、刷新令牌与账号锁定等安全事件,可通过 b_request_id / b_trace_id 与技术日志关联。

create table s_log_login (
    b_id bigint not null,                -- 雪花主键(应用层生成,全局唯一)
    b_user_id bigint null,                              -- 用户(逻辑外键)
    b_account nvarchar(50) null,                        -- 登录名
    b_login_type varchar(20) not null,                  -- 登录类型:password/token/oidc/ldap
    b_login_result int not null default 0,              -- 登录结果:0失败/1成功
    b_fail_reason nvarchar(200) null,                   -- 失败原因
    b_ip varchar(50) null,                              -- 登录 IP
    b_ip_location nvarchar(200) null,                   -- IP 归属地
    b_browser varchar(100) null,                        -- 浏览器
    b_browser_version varchar(50) null,                 -- 浏览器版本
    b_os varchar(100) null,                             -- 操作系统
    b_os_version varchar(50) null,                      -- 系统版本
    b_device_type varchar(20) null,                     -- 设备类型:pc/mobile/tablet
    b_user_agent nvarchar(max) null,                    -- 完整 UA
    b_session_id varchar(50) null,                      -- 会话 ID
    b_request_id varchar(64) null,                      -- 请求 ID
    b_trace_id varchar(64) null,                        -- 追踪 ID
    b_occurdatetime datetime2 null default sysutcdatetime(), -- 登录时间(UTC)
    constraint pk_s_log_login primary key (b_id)
);

create index ix_s_log_login_user on s_log_login (b_user_id, b_occurdatetime desc);
create index ix_s_log_login_account on s_log_login (b_account, b_occurdatetime desc);
create index ix_s_log_login_result on s_log_login (b_login_result, b_occurdatetime desc);

3.18 业务审计事件与字段变更

审计事件与字段变更分表存储:事件一条、字段变更多条,通过 b_event_id 关联。字段级明细是面向客户的硬需求(客户可在门户查看某个单号发生过哪些字段变更),字段一旦发生修改即写入明细,不按字段配置开关;b_visibility 控制内部/客户可见。

create table s_log_audit (
    b_id bigint not null,                -- 雪花主键(应用层生成,全局唯一)
    b_event_code varchar(50) not null,                  -- 业务事件编码(如 booking.submit)
    b_module_code varchar(50) null,                     -- 模块编码
    b_data_id varchar(50) null,                         -- 被审计的业务数据 ID
    b_business_no varchar(50) null,                     -- 业务单号(如订舱号)
    b_operation varchar(100) null,                      -- 操作 key(如 saveObject.submit)
    b_operator_id bigint null,                          -- 操作人(逻辑外键)
    b_operator_name nvarchar(100) null,                 -- 操作人名称(快照)
    b_source_type varchar(20) not null default 'user',  -- 来源类型:user/ai/api
    b_visibility varchar(20) not null default 'internal', -- 可见性:internal 内部/customer 客户可见
    b_request_id varchar(64) null,                      -- 请求 ID
    b_trace_id varchar(64) null,                        -- 追踪 ID(关联技术日志)
    b_payload_text nvarchar(max) null,                  -- 事件载荷(文本,如前后快照)
    b_occurdatetime datetime2 null default sysutcdatetime(), -- 发生时间(UTC)
    constraint pk_s_log_audit primary key (b_id)
);

create index ix_s_log_audit_module on s_log_audit (b_module_code, b_data_id, b_occurdatetime desc);
create index ix_s_log_audit_trace on s_log_audit (b_trace_id);

字段级变更明细表 s_log_audit_field:b_event_id 关联审计事件;客户门户查询按单号 + 客户可见过滤。

create table s_log_audit_field (
    b_id bigint not null,                -- 雪花主键(应用层生成,全局唯一)
    b_event_id bigint not null,                         -- 所属审计事件
    b_module_code varchar(50) null,                     -- 模块编码
    b_data_id varchar(50) null,                         -- 业务数据 ID
    b_field varchar(50) not null,                       -- 变更字段(物理列名)
    b_field_label nvarchar(100) null,                   -- 字段标签快照
    b_before_value nvarchar(max) null,                  -- 变更前原始值
    b_after_value nvarchar(max) null,                   -- 变更后原始值
    b_before_display nvarchar(500) null,                -- 变更前显示值(如引用字段 label)
    b_after_display nvarchar(500) null,                 -- 变更后显示值
    b_visibility varchar(20) not null default 'internal', -- 可见性:internal 内部/customer 客户可见
    constraint pk_s_log_audit_field primary key (b_id),
    -- constraint fk_s_log_audit_field_event foreign key (b_event_id) references s_log_audit (b_id)
);

create index ix_s_log_audit_field_event on s_log_audit_field (b_event_id);
create index ix_s_log_audit_field_customer on s_log_audit_field (b_module_code, b_data_id, b_visibility, b_event_id);

4. 关键设计决策说明

4.1 为什么保留 s_module 并删除 s_entity

旧系统已经以 s_module 作为配置、查询和保存的核心主体,现有接口、字段表和迁移工具也普遍使用 b_module_id。继续拆出 s_entity 会增加概念、外键和迁移成本,却没有形成足够独立的业务职责。因此新方案保留 s_module 自关联树,由 category/data/query 区分分类节点和数据节点;菜单和权限仍保持独立,避免恢复旧系统全部职责混杂的问题。

4.2 为什么不设置模块依赖表

当前 s_module 不是可独立安装、发布的软件包,因此不额外维护模块依赖 DAG。数据模块之间的引用和主从关系由 s_relation 表达,数据库对象依赖由实际表、视图和 Schema Compiler 校验,部署依赖由构建或插件系统管理。

4.3 为什么字段定义与页面行为分表

旧系统的字段、列表、编辑、查询虽然都挂在 b_module_id 下,但运行时和管理端都把它们当作四类独立配置。列表与表单的显示、排序、分组并不相同,查询条件也只覆盖部分字段。强行并入 s_field 会形成宽表,并混淆数据库列定义与页面行为。因此本方案保留字段定义、列表、表单和查询条件等职责明确的表;列表、表单通过模块 + 字段/分组复合外键保证同模块,查询条件允许通过明确的 s_relation 引用关联模块字段。第一阶段查询条件统一使用 AND,暂不增加条件组。

4.4 为什么菜单独立且引用模块

导航是展示层概念,数据模块是业务数据概念。s_menu 引用 b_module_id,不重复保存数据源;同一数据模块可以被多个菜单入口引用,但默认共享模块上的同一套字段、列表、表单和查询配置。

4.5 为什么暂不增加页面配置档

旧系统实际以一个 data 模块承载一套字段、列表、编辑和查询配置,没有菜单级字段覆盖机制。当前若再增加 s_page_profile、s_page_field,会与字段配置表重复,并引入菜单、配置档、模块三者的一致性维护成本。因此第一阶段规定一个数据模块只有一套页面配置。

4.6 为什么权限引入角色层

旧系统 b_user_power 是用户直挂权限,用户一多授权矩阵爆炸。本方案使用 s_role + s_user_role + s_role_power 管理角色权限,并通过 s_role_scope 补充模块级数据范围。权限点 s_power 支持 menu/module/action/field 四级粒度,角色权限支持 allow/deny,字段权限支持 view/edit;数据范围按角色取并集,显式 deny 优先,后端在每次请求中重新计算有效权限。

4.7 自动编码的并发处理

b_currentvalue 若采用"读-改-写"必然在并发下产生重号。方案要求应用层用原子自增语句取号(update ... set b_currentvalue = b_currentvalue + 1 output inserted.b_currentvalue),并把取号与业务保存放同一事务,确保单号不重不漏。

4.8 日志的写入去向

三类日志职责不同、量级不同,不建议全部落租户业务库:

  • 技术日志高频(每条请求、SQL、异常),推荐写 Loki/ES 等日志平台,s_log_technical 仅作集中日志库或短期兜底。
  • 登录日志与业务审计属于安全与合规数据,低频但必须长期留存,落库保存;审计字段明细在字段发生修改时全量写入,不做按字段开关,数据量按变更频率自然收敛。
  • 三类日志统一以 b_request_id / b_trace_id 串联,便于从业务审计反查技术日志。

4.9 为什么元数据表主键采用雪花 ID

配置数据(模块、字段、关系、菜单、权限等)需要跨系统导入导出与搬迁。若使用数据库自增主键,两个系统各自从 1 计数,搬入目标系统后必然 ID 冲突,模块子树下所有引用字段(b_module_id、b_field_id、b_relation_id 等 30+ 处)都必须重映射,而重映射正是搬迁工具中 bug 的高发区。雪花 ID 全局唯一,导出/导入时子树引用原样保留,无需重映射。元数据表行数少,雪花随机写入对聚簇索引的影响可忽略;b_code 仍作为跨环境稳定业务键,负责冲突检测与 dev/prod 对齐。业务数据表(模块生成的物理表)和用户表不在此列,用户表见 5.1。

5. 逻辑外键清单

全系统(含元数据表)不建立物理外键;DDL 中的 fk_* 约束以注释形式保留,仅作逻辑关系参考,建表时不创建。以下引用统一由服务层校验:

表 字段 逻辑目标
s_user_role b_user_id 当前身份系统的用户 b_id
s_menu b_module_id 绑定的数据/查询模块 s_module.b_id
s_field_group b_parent_id 父分组必须与当前分组属于同一数据模块
s_field_query b_relation_id 关联关系必须从查询模块可达条件字段所属模块
s_menu b_power_code s_power.b_code
s_power b_resource / b_operation 权限类型、资源和操作必须匹配;field 权限必须引用对应模块字段
s_role_scope b_scope_field_id / b_scope_value 数据范围字段和范围值必须与模块及范围类型匹配
s_relation 关系类型与字段组合 多对多必须有中间模块;两侧字段必须属于声明的模块
s_module b_viewtable / b_savetable Schema Compiler 生成的物理表/视图
s_log_technical b_user_id 当前身份系统的用户 b_id
s_log_login b_user_id 当前身份系统的用户 b_id
s_log_audit b_operator_id 当前身份系统的用户 b_id

逻辑外键写入时应执行以下校验:

引用是否已启用
-> 目标资源是否存在且类型匹配
-> 字段、分组和关系是否属于正确的数据模块
-> 权限类型、操作、字段和数据范围规则是否匹配
-> 删除或停用前是否仍被其他资源引用

5.1 用户与身份系统(b_user)

b_user 不属于 FMS 业务库,由独立的身份系统维护;FMS 侧只保存用户引用 b_user_id(bigint,不透明 ID),不解析账号,也不关心身份系统内部的 ID 生成方案。

身份系统用户表约定:

  • b_id bigint 主键:单库自增 identity(1,1) 即可;若未来用户需要跨系统迁移或合并,再改为全局唯一 ID(如雪花),FMS 侧无需任何改动。
  • b_account nvarchar(50) 唯一键:账号是自然键,允许改名;绝不用账号做主键。
  • 账号改名只更新用户表自身,引用 b_user_id 的业务数据(s_user_role、s_log_*)不受影响。
  • 日志表冗余保存 b_account 快照(s_log_login.b_account、s_log_technical.b_account),保证改名后历史日志仍可读、可追溯。

6. 与旧系统的映射对照

旧系统 本方案 变化
s_module(含表/路由/菜单/权限) s_module + s_menu 保留模块数据主体,拆出导航与角色权限
s_module_field s_field 字段挂模块
s_module_field_list s_field_view 改为通过字段关联模块
s_module_field_edit s_field_edit 改为通过字段关联模块
s_module_field_query s_field_query 保留平铺查询配置,增加关联字段与同字段多条件
s_module_field_group s_field_group 分组挂模块
s_module_auto_code s_autocode 挂模块+字段,原子取号
s_module_power + b_user_power s_power + s_role + s_user_role + s_role_power + s_role_scope 引入角色、允许/拒绝和数据范围
b_i18n s_i18n 增加资源类型维度
s_login_log s_log_login 字段扩充(设备/UA/请求链路)
(无) s_relation 新增模块关系
(无) s_log_technical 新增技术日志(旧系统无)
(无) s_log_audit + s_log_audit_field 新增业务审计与字段变更(旧系统无)