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

567 lines
32 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_` 开头,字段以 `b_` 开头,复合词采用驼峰拼接(如 `b_viewtable`、`b_inputdatetime`),逻辑关联字段使用 `b_xxx_id`。
## 1. 设计原则
针对旧系统 `s_module` 职责混杂的问题,本方案坚持四个分离:
1. **模块与实体分离**:`s_module` 只表达业务能力边界与依赖关系,不承载表、路由、菜单;数据归属由 `s_entity` 承担。
2. **实体与字段分离**:实体是数据所有者,声明查询源/保存源/主键字段;字段挂实体,同时携带数据库列定义与 UI 行为。
3. **菜单与模块分离**:`s_menu` 是纯导航树,只引用实体或固定页面,不掺入数据源配置。
4. **字段定义与视图配置合并**:旧系统把字段拆成 `s_module_field` + `_edit` + `_list` + `_query` 四张表,维护极易失配;本方案合并为单表 `s_field`,用标志列表达列表/表单/查询行为。
其余约定:
- 内部主键 `b_id bigint identity(1, 1)`,跨环境稳定标识使用 `b_code`(参考旧系统)。
- 元数据表保留物理外键(低频、高一致性,防脏数据);若担心部署顺序,可退化为逻辑外键并保留索引。
- 布尔语义使用 `int`(0/1),与旧系统 `s_module` 风格一致。
- 时间使用 `datetime2`,默认 `sysdatetime()`。
- 审计字段:定义类表保留 `b_inputuser_id / b_inputdatetime / b_updateuser_id / b_updatedatetime`;关联类表只保留 `b_inputdatetime`。
- 模块归属保留树形(`b_parent_id`,仅用于业务分组),模块间依赖使用独立的 DAG 表,两者语义互不混淆。
## 2. 表清单
| # | 表名 | 说明 |
| --- | --- | --- |
| 1 | `s_module` | 模块(业务能力边界 + 业务分组树) |
| 2 | `s_module_dependency` | 模块依赖(DAG,禁止循环) |
| 3 | `s_entity` | 实体(数据所有者,声明查/存数据源) |
| 4 | `s_field` | 字段(列定义 + 列表/表单/查询行为合并单表) |
| 5 | `s_field_group` | 字段分组(实体级) |
| 6 | `s_relation` | 实体关系(引用/主子,驱动外键校验与级联) |
| 7 | `s_menu` | 菜单/页面(纯导航,挂模块做组织维度) |
| 8 | `s_autocode` | 自动编码规则 |
| 9 | `s_role` | 角色 |
| 10 | `s_user_role` | 用户-角色 |
| 11 | `s_power` | 权限点(menu/entity/action/field) |
| 12 | `s_role_power` | 角色-权限 |
| 13 | `s_i18n` | 多语言资源 |
| 14 | `s_log_technical` | 技术日志(请求链路/SQL/异常/性能) |
| 15 | `s_log_login` | 登录日志(登录/退出/失败/锁定) |
| 16 | `s_log_audit` | 业务审计事件 |
| 17 | `s_log_audit_field` | 审计字段变更明细 |
## 3. 各表结构
### 3.1 s_module 模块
只保存模块身份、业务分组与启用状态,不承载菜单、路由或数据源。
```sql
create table s_module (
b_id bigint identity(1, 1) not null, -- 内部主键
b_code varchar(50) not null, -- 模块编码(如 ocean/finance/master-data)
b_name nvarchar(100) not null, -- 模块名称
b_i18n varchar(150) null, -- 显示名多语言 key
b_parent_id bigint null, -- 父模块(仅业务分组,不承担导航)
b_module_type varchar(20) not null default 'business', -- 类型:platform 平台/tenant 租户/business 业务
b_canuse int not null default 1, -- 是否启用
b_xh int not null default 0, -- 排序号
b_icon varchar(50) null, -- 图标
b_bz nvarchar(1000) null, -- 描述
b_inputuser_id bigint null,
b_inputdatetime datetime2 null default sysdatetime(),
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 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);
```
### 3.2 s_module_dependency 模块依赖
模块依赖必须形成有向无环图,由服务层校验禁止循环。
```sql
create table s_module_dependency (
b_id bigint identity(1, 1) not null, -- 内部主键
b_module_id bigint not null, -- 依赖方模块
b_depend_module_id bigint not null, -- 被依赖模块
b_inputdatetime datetime2 null default sysdatetime(),
constraint pk_s_module_dependency primary key (b_id),
constraint ux_s_module_dependency unique (b_module_id, b_depend_module_id),
constraint fk_s_module_dependency_module foreign key (b_module_id) references s_module (b_id),
constraint fk_s_module_dependency_depend foreign key (b_depend_module_id) references s_module (b_id)
);
create index ix_s_module_dependency_depend on s_module_dependency (b_depend_module_id);
```
### 3.3 s_entity 实体
实体是数据所有者,同时声明查询数据源与保存数据源;主子表通过 `b_parent_id` 表达(与菜单树无关)。
```sql
create table s_entity (
b_id bigint identity(1, 1) not null, -- 内部主键
b_module_id bigint not null, -- 所属模块
b_parent_id bigint null, -- 父实体(主子表关系)
b_code varchar(50) not null, -- 实体编码(如 ocean_booking)
b_name nvarchar(100) not null, -- 实体名称
b_i18n varchar(150) null, -- 显示名多语言 key
b_viewtable varchar(128) null, -- 查询数据源:真实表或只读视图
b_savetable varchar(128) null, -- 保存目标:真实表(纯查询实体可空)
b_keyfield varchar(50) null, -- 保存对象主键字段(如 b_id)
b_entity_type varchar(20) not null default 'table', -- table 简单实体/view 复杂列表/query 纯查询
b_delete_policy varchar(20) not null default 'lifecycle', -- restrict/lifecycle/cascade/hard
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 sysdatetime(),
b_updateuser_id bigint null,
b_updatedatetime datetime2 null,
constraint pk_s_entity primary key (b_id),
constraint ux_s_entity_module_code unique (b_module_id, b_code),
constraint fk_s_entity_module foreign key (b_module_id) references s_module (b_id),
constraint fk_s_entity_parent foreign key (b_parent_id) references s_entity (b_id)
);
create index ix_s_entity_parent on s_entity (b_parent_id);
create index ix_s_entity_savetable on s_entity (b_savetable);
```
实体的查/存配置方式:
```text
简单实体: b_viewtable = b_savetable = 同一张真实表
复杂列表: b_viewtable = 带关联/聚合的 SQL 视图(只读), b_savetable = 真实表
纯查询实体: b_viewtable = 视图或表, b_savetable = 空(只读)
```
删除策略取值(`b_delete_policy`):
```text
restrict 主数据:有引用时拒绝删除,只能停用或合并
lifecycle 业务单据:草稿且无下游时可删,正式后只能取消/作废
cascade 主表拥有的从数据:随主表同一事务级联删除
hard 临时记录:校验权限后直接物理删除
```
### 3.4 s_field 字段
合并旧系统 `s_module_field` + `_edit` + `_list` + `_query` 四张表,字段增删改只需维护一处。
```sql
create table s_field (
b_id bigint identity(1, 1) not null, -- 内部主键
b_entity_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, -- 存储类型:varchar/decimal/datetime/int/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, -- 默认值(文本表达)
b_options nvarchar(max) null, -- 下拉/单选选项(JSON 文本)
b_edittype varchar(30) null, -- 编辑组件:input/select/date/datetime/textarea/number/checkbox/radio
b_required int not null default 0, -- 编辑必填
b_readonly int not null default 0, -- 编辑只读
b_disabled int not null default 0, -- 编辑禁用
b_listvisible int not null default 1, -- 是否显示在列表
b_listwidth int null, -- 列表列宽(px)
b_queryable int not null default 0, -- 是否出现在查询条件
b_querytype varchar(30) null, -- 查询组件:input/select/date
b_queryop varchar(20) null, -- 查询操作符:eq/like/gt/ge/lt/le/in/between
b_group_id bigint null, -- 所属分组(逻辑外键 s_field_group.b_id)
b_xh int not null default 0, -- 列表/表单排序
b_canuse int not null default 1, -- 是否启用(停用隐藏并保留数据)
b_inputuser_id bigint null,
b_inputdatetime datetime2 null default sysdatetime(),
b_updateuser_id bigint null,
b_updatedatetime datetime2 null,
constraint pk_s_field primary key (b_id),
constraint ux_s_field_entity_field unique (b_entity_id, b_field),
constraint fk_s_field_entity foreign key (b_entity_id) references s_entity (b_id)
);
create index ix_s_field_group on s_field (b_group_id, b_xh, b_canuse);
```
写入模式由 `b_readonly` 与 `b_canuse` 组合表达;如需更细的写入语义(computed/actionOnly/system),可增加 `b_writemode varchar(20)` 列(direct/createOnly/readOnly/computed/actionOnly/system)。
### 3.5 s_field_group 字段分组
分组是实体级独立定义,字段通过 `b_group_id` 引用,支持分组树。
```sql
create table s_field_group (
b_id bigint identity(1, 1) not null, -- 内部主键
b_entity_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 fk_s_field_group_entity foreign key (b_entity_id) references s_entity (b_id),
constraint fk_s_field_group_parent foreign key (b_parent_id) references s_field_group (b_id)
);
create index ix_s_field_group_entity on s_field_group (b_entity_id, b_xh, b_canuse);
```
### 3.6 s_relation 实体关系
描述实体间的引用与主子关系,运行时据此执行外键校验、主子事务与级联删除。
```sql
create table s_relation (
b_id bigint identity(1, 1) not null, -- 内部主键
b_entity_id bigint not null, -- 主实体
b_target_entity_id bigint not null, -- 目标实体
b_relation_type varchar(20) not null, -- one-to-one/one-to-many/many-to-one/many-to-many
b_foreign_field varchar(50) null, -- 子表侧外键字段
b_link_field varchar(50) null, -- 主实体侧关联字段
b_ownership varchar(20) not null default 'reference', -- reference 引用/owned 主从
b_delete_policy varchar(20) null, -- restrict/set-null/cascade/archive
b_orderfield varchar(500) null, -- 子表排序字段表达式
b_canuse int not null default 1, -- 是否启用
b_xh int not null default 0, -- 排序号
constraint pk_s_relation primary key (b_id),
constraint fk_s_relation_entity foreign key (b_entity_id) references s_entity (b_id),
constraint fk_s_relation_target foreign key (b_target_entity_id) references s_entity (b_id)
);
create index ix_s_relation_target on s_relation (b_target_entity_id);
```
### 3.7 s_menu 菜单/页面
纯导航树,通过 `b_module_id` 挂到模块做组织维度,通过 `b_entity_id` 引用实体;模块与菜单可互相独立演进(一个模块多个菜单入口、一个菜单也可属于无模块的公共区)。
```sql
create table s_menu (
b_id bigint identity(1, 1) not null, -- 内部主键
b_parent_id bigint null, -- 父菜单(导航树自引用)
b_module_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_entity_id bigint null, -- data 页面绑定的实体
b_power_code varchar(50) null, -- 页面访问权限编码(对应 s_power.b_code)
b_icon varchar(50) null, -- 图标
b_canmenu int not null default 1, -- 是否显示在菜单
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 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),
constraint fk_s_menu_entity foreign key (b_entity_id) references s_entity (b_id)
);
create index ix_s_menu_parent on s_menu (b_parent_id, b_xh, b_canuse);
create index ix_s_menu_entity on s_menu (b_entity_id);
```
菜单类型约束由元数据校验器执行:`directory` 不填业务路由;`page` 必须填 `b_route`;`data` 必须填 `b_entity_id`;`external` 必须填 `b_route` 和 `b_power_code`。目录仅在存在至少一个可访问子页面时显示。
### 3.8 s_autocode 自动编码
独立于模块,挂实体 + 目标字段,规则可复用。并发控制:应用层使用原子更新取号(`update ... set b_currentvalue = b_currentvalue + 1 output inserted.b_currentvalue`),避免自增竞争。
```sql
create table s_autocode (
b_id bigint identity(1, 1) not null, -- 内部主键
b_entity_id bigint not null, -- 所属实体
b_field varchar(50) 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_entity_field unique (b_entity_id, b_field),
constraint fk_s_autocode_entity foreign key (b_entity_id) references s_entity (b_id)
);
```
### 3.9 s_role 角色
```sql
create table s_role (
b_id bigint identity(1, 1) 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_canuse int not null default 1, -- 是否启用
b_inputuser_id bigint null,
b_inputdatetime datetime2 null default sysdatetime(),
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)
);
```
### 3.10 s_user_role 用户-角色
```sql
create table s_user_role (
b_id bigint identity(1, 1) 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 sysdatetime(),
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.11 s_power 权限点
```sql
create table s_power (
b_id bigint identity(1, 1) 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/entity/action/field
b_resource varchar(128) null, -- 资源标识(菜单 code/实体 code/动作 code/字段)
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)
);
create index ix_s_power_resource on s_power (b_power_type, b_resource, b_canuse);
```
### 3.12 s_role_power 角色-权限
```sql
create table s_role_power (
b_id bigint identity(1, 1) not null, -- 内部主键
b_role_id bigint not null, -- 角色
b_power_id bigint not null, -- 权限点
b_canuse int not null default 1, -- 是否启用
b_inputdatetime datetime2 null default sysdatetime(),
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)
);
create index ix_s_role_power_power on s_role_power (b_power_id, b_canuse);
```
菜单显示继承页面访问权限:用户拥有 `s_menu.b_power_code` 对应权限时菜单才显示;前端只控制显示,后端运行时必须再次校验同一个权限编码。
### 3.13 s_i18n 多语言
```sql
create table s_i18n (
b_id bigint identity(1, 1) 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/entity/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.14 s_log_technical 技术日志
用于请求链路、SQL、异常、性能与外部接口排查。高频日志推荐写入 Loki/OpenSearch/Elasticsearch 等日志平台,本表用于集中日志库或短期数据库兜底,不建议长期写入租户业务库。
```sql
create table s_log_technical (
b_id bigint identity(1, 1) not null, -- 内部主键
b_org_id varchar(50) 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 sysdatetime(), -- 发生时间(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_org on s_log_technical (b_org_id, b_occurdatetime desc);
create index ix_s_log_technical_level on s_log_technical (b_loglevel, b_occurdatetime desc);
```
### 3.15 s_log_login 登录日志
记录登录成功、失败、退出、刷新令牌与账号锁定等安全事件,可通过 `b_request_id` / `b_trace_id` 与技术日志关联。
```sql
create table s_log_login (
b_id bigint identity(1, 1) not null, -- 内部主键
b_org_id varchar(50) 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 sysdatetime(), -- 登录时间(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.16 业务审计事件与字段变更
审计事件与字段变更分表存储:事件一条、字段变更多条,通过 `b_event_id` 关联。字段级明细是**面向客户的硬需求**(客户可在门户查看某个单号发生过哪些字段变更),**字段一旦发生修改即写入明细**,不按字段配置开关;`b_visibility` 控制内部/客户可见。
```sql
create table s_log_audit (
b_id bigint identity(1, 1) not null, -- 内部主键
b_event_code varchar(50) not null, -- 业务事件编码(如 booking.submit)
b_module_code varchar(50) null, -- 模块编码
b_entity_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 sysdatetime(), -- 发生时间(UTC)
constraint pk_s_log_audit primary key (b_id)
);
create index ix_s_log_audit_entity on s_log_audit (b_entity_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 identity(1, 1) not null, -- 内部主键
b_event_id bigint not null, -- 所属审计事件
b_entity_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_entity_code, b_data_id, b_visibility, b_event_id);
```
## 4. 关键设计决策说明
### 4.1 为什么模块与实体分离
旧系统把表、视图、路由、菜单、权限全部压在 `s_module` 一条记录上,导致"一个模块只能配一张主表"、菜单与数据源互相绑架。本方案中模块只回答"这是什么业务能力、依赖谁",实体回答"数据在哪、怎么存",两者可独立演进:一个模块可拥有多个实体(订舱主表 + 集装箱/费用/运输子表),一个实体也可被多个菜单入口引用。
### 4.2 为什么保留模块树 + 独立依赖 DAG
树形(`b_parent_id`)表达"业务分组归属"(基础资料 > 海运 > 订舱),是货代业务的真实组织诉求,且操作直观;但树无法表达"finance 依赖 master-data"这类跨分支依赖,所以依赖单独用 `s_module_dependency` 表达并强制 DAG。树归树、依赖归依赖,语义不再混淆。
### 4.3 为什么字段合并单表
旧系统字段配置拆 4 张表,字段新增要同步维护 4 处、分组标题另存一份,配置漂移是常态。合并后字段的"定义 + 列表 + 表单 + 查询"行为集中在一行,增删改一处完成;代价是行较宽,但对元数据表(数量级远小于业务表)完全可接受。
### 4.4 为什么菜单独立且引用实体
导航是展示层概念,数据是业务层概念。`s_menu` 引用 `b_entity_id` 而非模块,同一实体可在多个菜单入口出现(如"订舱列表"与"订舱审核"共用 `ocean_booking` 实体但绑定不同权限)。
### 4.5 为什么权限引入角色层
旧系统 `b_user_power` 是用户直挂权限,用户一多授权矩阵爆炸。本方案 `s_role` + `s_user_role` + `s_role_power` 三层,权限点 `s_power` 支持到 menu/entity/action/field 四级粒度,为后续数据范围、字段脱敏策略留出扩展位。
### 4.6 自动编码的并发处理
`b_currentvalue` 若采用"读-改-写"必然在并发下产生重号。方案要求应用层用原子自增语句取号(`update ... set b_currentvalue = b_currentvalue + 1 output inserted.b_currentvalue`),并把取号与业务保存放同一事务,确保单号不重不漏。
### 4.7 日志的写入去向
三类日志职责不同、量级不同,不建议全部落租户业务库:
- 技术日志高频(每条请求、SQL、异常),推荐写 Loki/ES 等日志平台,`s_log_technical` 仅作集中日志库或短期兜底。
- 登录日志与业务审计属于安全与合规数据,低频但必须长期留存,落库保存;审计字段明细在字段发生修改时全量写入,不做按字段开关,数据量按变更频率自然收敛。
- 三类日志统一以 `b_request_id` / `b_trace_id` 串联,便于从业务审计反查技术日志。
## 5. 逻辑外键清单
元数据表已建物理外键;以下为业务侧或跨系统引用,由服务层校验:
| 表 | 字段 | 逻辑目标 |
| --- | --- | --- |
| `s_user_role` | `b_user_id` | 当前身份系统的用户 `b_id` |
| `s_menu` | `b_module_id` | `s_module.b_id` |
| `s_field` | `b_group_id` | `s_field_group.b_id` |
| `s_menu` | `b_power_code` | `s_power.b_code` |
| `s_entity` | `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
引用是否已启用
-> 目标资源是否存在且类型匹配
-> 跨模块引用是否合法(依赖图中可达)
-> 删除或停用前是否仍被其他资源引用
```
## 6. 与旧系统的映射对照
| 旧系统 | 本方案 | 变化 |
| --- | --- | --- |
| `s_module`(含表/路由/菜单/权限) | `s_module` + `s_entity` + `s_menu` | 职责拆分 |
| `s_module_field` | `s_field` | 字段挂实体 |
| `s_module_field_edit` / `_list` / `_query` | 并入 `s_field` 标志列 | 四表合一 |
| `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` | 引入角色层 |
| `b_i18n` | `s_i18n` | 增加资源类型维度 |
| `s_login_log` | `s_log_login` | 字段扩充(设备/UA/请求链路) |
| (无) | `s_module_dependency` | 新增依赖 DAG |
| (无) | `s_relation` | 新增实体关系 |
| (无) | `s_log_technical` | 新增技术日志(旧系统无) |
| (无) | `s_log_audit` + `s_log_audit_field` | 新增业务审计与字段变更(旧系统无) |