Files
workspace/code/fms/FMS新系统核心表结构设计.md
T
2026-08-21 23:06:56 +08:00

782 lines
56 KiB
Markdown
Raw 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.
# 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` 节点只查询不保存。菜单、角色和权限仍由独立表维护。
```sql
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);
```
模块的查/存配置方式:
```text
分类模块: 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`):
```text
restrict 主数据:有引用时拒绝删除,只能停用或合并
lifecycle 业务单据:草稿且无下游时可删,正式后只能取消/作废
hard 临时记录:校验权限后直接物理删除
```
模块删除策略只控制“直接删除当前模块记录”是否允许;主记录删除时如何处理从记录由具体 `s_relation.b_delete_policy` 决定,因此模块层不再重复设置 `cascade`。
### 3.2 s_field 字段
只保存字段本身的规范定义,不混入列表、表单和查询布局。字段同步、数据库列校验和业务审计均以本表为准;三类界面行为分别放在后续配置表中。
```sql
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` 中配置通用规则,也不增加默认值类型字段。此类值由具体业务模块的服务逻辑处理:例如新增业务单据时,根据单据币种和业务日期查询汇率模块,将汇率写入单据作为当时的业务快照;用户修改币种或日期时,由该业务服务重新计算,是否覆盖人工修改由具体业务规则决定。
第一阶段沿用旧系统已有类型:
```text
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` 保存该类型的专用配置。
示例:
```json
{
"relationCode": "booking.customer",
"labelField": "b_name",
"searchFields": ["b_code", "b_name"],
"allowClear": true
}
```
### 3.3 s_field_group 字段分组
分组是模块级独立定义,支持分组树。列表和表单可以分别通过自己的配置表引用同一个分组,也可以选择不同分组。配置表冗余保存 `b_module_id`,通过复合外键保证字段、分组和配置始终属于同一个模块。
```sql
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_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 字段视图配置
保存字段在列表中的显示、顺序、宽度和分组。每个模块字段最多一条列表配置;未创建配置记录表示该字段不进入列表。
```sql
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` 决定,本表只配置当前表单中的显示、必填、只读和禁用状态。
```sql
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` 和操作符推导。
```sql
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` 主键引用,避免只写物理列名导致跨模块歧义。
```sql
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` 模块,并直接使用该模块的字段、列表、表单和查询配置。
```sql
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 varchar(50) null, -- 图标
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`),避免自增竞争。
```sql
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 角色
```sql
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 用户-角色
```sql
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 权限点
```sql
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 角色-权限
```sql
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 角色-模块数据范围
数据范围控制用户通过某个模块操作时可访问哪些业务记录。范围按角色、模块和操作配置,不承担租户隔离;本系统采用一个租户一个数据库,数据范围只解决租户内部的本人、部门、负责人等业务授权。
```sql
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 多语言
```sql
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 等日志平台,本表用于集中日志库或短期数据库兜底,不建议长期写入租户业务库。
```sql
create table s_log_technical (
b_id bigint not null, -- 雪花主键(应用层生成,全局唯一)
b_user_id bigint null, -- 操作用户(逻辑外键)
b_account varchar(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` 与技术日志关联。
```sql
create table s_log_login (
b_id bigint not null, -- 雪花主键(应用层生成,全局唯一)
b_user_id bigint null, -- 用户(逻辑外键)
b_account varchar(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` 控制内部/客户可见。
```sql
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` 关联审计事件;客户门户查询按单号 + 客户可见过滤。
```sql
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` |
逻辑外键写入时应执行以下校验:
```text
引用是否已启用
-> 目标资源是否存在且类型匹配
-> 字段、分组和关系是否属于正确的数据模块
-> 权限类型、操作、字段和数据范围规则是否匹配
-> 删除或停用前是否仍被其他资源引用
```
### 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` | 新增业务审计与字段变更(旧系统无) |