# 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`,用标志列表达列表/表单/查询行为。 5. **配置入口与存储职责分离**:管理员始终从菜单树定位页面,在一个配置工作台内完成数据源、字段、列表、表单、查询、编码和权限配置;底层数据仍按模块、实体、菜单和权限分别存储。 其余约定: - 内部主键 `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_page_profile` | 菜单页面配置档(一个菜单入口的一套页面行为) | | 9 | `s_page_field` | 页面字段覆盖(菜单级显示、排序、表单/查询覆盖) | | 10 | `s_autocode` | 自动编码规则 | | 11 | `s_role` | 角色 | | 12 | `s_user_role` | 用户-角色 | | 13 | `s_power` | 权限点(menu/entity/action/field) | | 14 | `s_role_power` | 角色-权限 | | 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 模块 只保存模块身份、业务分组与启用状态,不承载菜单、路由或数据源。 ```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` 四张表,保存实体级默认定义。菜单页面的个性化显示、排序和交互覆盖放在 `s_page_field`,避免同一实体被多个菜单引用时互相影响。 ```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_page_profile 页面配置档 页面配置档是菜单入口与实体默认元数据之间的配置上下文。一个 `data` 菜单通常对应一个默认配置档;同一实体被多个菜单复用时,每个菜单可以拥有独立配置档。配置档不复制实体字段定义,只保存页面级行为。 ```sql create table s_page_profile ( b_id bigint identity(1, 1) not null, b_menu_id bigint not null, -- 配置入口 b_entity_id bigint not null, -- 页面主实体 b_code varchar(50) not null, b_name nvarchar(100) not null, b_list_enabled int not null default 1, b_edit_enabled int not null default 1, b_query_enabled int not null default 1, 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_page_profile primary key (b_id), constraint ux_s_page_profile_menu unique (b_menu_id), constraint ux_s_page_profile_code unique (b_code), constraint fk_s_page_profile_menu foreign key (b_menu_id) references s_menu (b_id), constraint fk_s_page_profile_entity foreign key (b_entity_id) references s_entity (b_id) ); ``` 配置工作台按以下顺序呈现:页面基本信息 → 数据源/实体 → 字段与分组 → 列表 → 表单 → 查询 → 关联实体 → 自动编码 → 权限。保存时由服务层校验菜单类型、实体类型和模块依赖关系。 没有独立菜单的子实体、费用明细、选择器和公共字典,通过主实体的 `s_relation` 或页面关联配置挂在所属页面下;它们不需要重复创建菜单,也可以在同一个配置工作台中维护。 ### 3.9 s_page_field 页面字段覆盖 只记录相对于 `s_field` 默认值的页面级覆盖;为空表示继承实体字段默认配置。这样既保留旧系统“进入菜单即可完整配置”的体验,又避免不同菜单修改同一实体时产生配置串扰。 ```sql create table s_page_field ( b_id bigint identity(1, 1) not null, b_page_profile_id bigint not null, b_field_id bigint not null, b_group_id bigint null, b_visible int null, b_xh int null, b_width int null, b_required int null, b_readonly int null, b_disabled int null, b_queryable int null, b_querytype varchar(30) null, b_queryop varchar(20) null, constraint pk_s_page_field primary key (b_id), constraint ux_s_page_field unique (b_page_profile_id, b_field_id), constraint fk_s_page_field_profile foreign key (b_page_profile_id) references s_page_profile (b_id), constraint fk_s_page_field_field foreign key (b_field_id) references s_field (b_id), constraint fk_s_page_field_group foreign key (b_group_id) references s_field_group (b_id) ); create index ix_s_page_field_profile on s_page_field (b_page_profile_id, b_xh, b_id); ``` ### 3.10 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.11 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.12 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.13 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.14 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.15 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.16 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.17 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.18 业务审计事件与字段变更 审计事件与字段变更分表存储:事件一条、字段变更多条,通过 `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 处、分组标题另存一份,配置漂移是常态。合并后字段的定义与默认列表/表单/查询行为集中在一行,实体字段增删改只需维护一处;菜单级差异由 `s_page_field` 覆盖,既避免重复定义,又保留不同页面的独立布局。代价是行较宽,但对元数据表(数量级远小于业务表)完全可接受。 ### 4.4 为什么菜单独立且引用实体 导航是展示层概念,数据是业务层概念。`s_menu` 引用 `b_entity_id` 而非模块,同一实体可在多个菜单入口出现(如"订舱列表"与"订舱审核"共用 `ocean_booking` 实体但绑定不同权限)。菜单树同时是管理员的统一配置入口,配置工作台通过菜单聚合其页面档、字段覆盖、关联实体和权限;因此数据库职责分离不会牺牲旧系统的定位效率。 ### 4.5 为什么增加页面配置档 实体是可复用的数据定义,菜单是具体的使用入口。同一实体可能需要不同的列表列、表单可编辑性或查询条件,不能把这些差异继续写回实体字段默认值。`s_page_profile` 表示一个菜单入口的页面上下文,`s_page_field` 只记录覆盖项;管理员仍然只需进入对应菜单即可完成完整配置。 ### 4.6 为什么权限引入角色层 旧系统 `b_user_power` 是用户直挂权限,用户一多授权矩阵爆炸。本方案 `s_role` + `s_user_role` + `s_role_power` 三层,权限点 `s_power` 支持到 menu/entity/action/field 四级粒度,为后续数据范围、字段脱敏策略留出扩展位。 ### 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` 串联,便于从业务审计反查技术日志。 ## 5. 逻辑外键清单 元数据表已建物理外键;以下为业务侧或跨系统引用,由服务层校验: | 表 | 字段 | 逻辑目标 | | --- | --- | --- | | `s_user_role` | `b_user_id` | 当前身份系统的用户 `b_id` | | `s_menu` | `b_module_id` | `s_module.b_id` | | `s_page_profile` | `b_entity_id` | 必须与所属 `s_menu.b_entity_id` 一致,且实体已启用 | | `s_page_field` | `b_field_id` | 必须属于所属 `s_page_profile.b_entity_id` | | `s_page_field` | `b_group_id` | 必须属于所属实体的 `s_field_group.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` | 引入角色层 | | (旧系统由 module 直接承载) | `s_page_profile` + `s_page_field` | 菜单级页面配置与字段覆盖,保留集中配置体验 | | `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` | 新增业务审计与字段变更(旧系统无) |