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

37 KiB
Raw Blame History

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 模块

只保存模块身份、业务分组与启用状态,不承载菜单、路由或数据源。

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 模块依赖

模块依赖必须形成有向无环图,由服务层校验禁止循环。

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 表达(与菜单树无关)。

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);

实体的查/存配置方式:

简单实体:   b_viewtable = b_savetable = 同一张真实表
复杂列表:   b_viewtable = 带关联/聚合的 SQL 视图(只读), b_savetable = 真实表
纯查询实体: b_viewtable = 视图或表, b_savetable = 空(只读)

删除策略取值(b_delete_policy):

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

3.4 s_field 字段

合并旧系统 s_module_field + _edit + _list + _query 四张表,保存实体级默认定义。菜单页面的个性化显示、排序和交互覆盖放在 s_page_field,避免同一实体被多个菜单引用时互相影响。

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 引用,支持分组树。

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 实体关系

描述实体间的引用与主子关系,运行时据此执行外键校验、主子事务与级联删除。

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 引用实体;模块与菜单可互相独立演进(一个模块多个菜单入口、一个菜单也可属于无模块的公共区)。选中菜单后,配置工作台聚合展示其页面档、实体、字段、关联实体、自动编码和权限。

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 菜单通常对应一个默认配置档;同一实体被多个菜单复用时,每个菜单可以拥有独立配置档。配置档不复制实体字段定义,只保存页面级行为。

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 默认值的页面级覆盖;为空表示继承实体字段默认配置。这样既保留旧系统“进入菜单即可完整配置”的体验,又避免不同菜单修改同一实体时产生配置串扰。

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),避免自增竞争。

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 角色

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 用户-角色

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 权限点

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 角色-权限

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 多语言

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 等日志平台,本表用于集中日志库或短期数据库兜底,不建议长期写入租户业务库。

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 与技术日志关联。

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 控制内部/客户可见。

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 关联审计事件;客户门户查询按单号 + 客户可见过滤。

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

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

引用是否已启用
-> 目标资源是否存在且类型匹配
-> 跨模块引用是否合法(依赖图中可达)
-> 删除或停用前是否仍被其他资源引用

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 新增业务审计与字段变更(旧系统无)