# FMS 新系统核心表结构设计 V2 > 本文用于记录新版核心表结构方案,权限设计见第 14 节,工作流与审批见第 16 节。旧版文档保留作为历史参考。 **本系统以 SQL 为核心驱动。设计时应优先保证查询和数据处理的灵活性,并在各模块预留 SQL 扩展能力,支持传入 SQL、动态拼接查询条件、组合查询及自定义数据处理逻辑,以便通过 SQL 快速适配和解决复杂业务场景。** ## 1. 数据表规范 本规范用于指导 AI 设计、新增和调整数据库表结构。除非已有表结构或业务要求明确指定,否则应优先遵循以下约定。 ### 1.1 表名规范 - 统一使用小写 `snake_case` 命名,单词之间使用下划线分隔。 - 系统配置、元数据和权限相关表使用 `s_` 前缀,例如 `s_module`、`s_field`。 - 业务数据表使用 `b_` 前缀或明确的业务域前缀,例如 `cw_fee`;组织与用户相关的基础主数据沿用现有 `b_` 前缀,例如 `b_user`、`b_dept`。 - 表名应表达稳定的业务含义,避免使用页面名称、路由名称或临时功能名称作为表名。 ### 1.2 表职责规范 - 一张表应围绕一个明确的业务对象、配置对象或关系对象设计。 - 系统表用于保存系统配置、元数据、权限和运行记录;业务表用于保存具体业务数据;关系表用于表达对象之间的关联关系。 - 不要为了适配单个页面重复创建业务表;页面差异优先通过模块配置、JSON 布局和 SQL 组合实现。 - 核心、稳定且需要被 SQL 查询的数据使用关系表;层级化、变化频繁的界面配置和用户偏好使用 JSON。 ### 1.3 主键和关联字段规范 - 每张表必须定义稳定、明确的主键。业务对象优先使用 `b_id` 作为业务编码主键;关系表可根据业务关系使用联合主键。 - 表之间的关联字段使用 `<对象>_id` 或已有的 `b_module_id` 等明确命名,不使用无法判断来源的通用字段名。 - 主键和关联字段的数据类型、长度必须保持一致。 - 本设计不依赖数据库外键约束保证关联完整性,关联存在性和删除规则由业务层处理。 ### 1.4 字段命名规范 - 字段统一使用小写 `snake_case` 命名。 - 主表或主业务对象字段使用 `b_` 前缀;明细、子表或扩展业务字段使用 `mx_` 前缀。 - 系统公共字段沿用现有命名,例如 `b_id`、`b_name`、`b_bz`、`b_canuse`、`b_xh`。 - 字段名应表达业务含义,避免使用 `value`、`data`、`type1` 等无法说明用途的名称;同一含义在不同表中应使用统一命名。 ### 1.5 通用字段规范 - 启用和停用统一使用 `b_canuse`,取值为 `0/1`。 - 排序字段统一使用 `b_xh`,默认值为 `0`;需要稳定排序时增加业务主键作为次排序字段。 - 最后修改时间统一使用 `b_updated_at`、修改人使用 `b_updated_by`(均为 `datetime2` / `varchar`,可空);不建触发器,由写入方维护。 - 名称、备注和多语言文本使用 `nvarchar`;编码、状态、类型和 SQL 标识类字段使用 `varchar`。 - 日期时间优先使用 `datetime2`;金额、数量等数值字段根据业务精度明确设置 `precision` 和 `scale`。 - 是否允许为空及默认值应根据业务语义明确设置,不能无理由全部允许为空或全部设置默认值。 ### 1.6 通用系统审计字段 - 通用创建人使用 `b_created_by`,保存用户编码,不保存用户名称。 - 通用创建时间使用 `b_created_at`,类型使用 `datetime2`。 - 通用最后修改人使用 `b_updated_by`,保存最后修改用户编码,不保存用户名称。 - 通用最后修改时间使用 `b_updated_at`,类型使用 `datetime2`。 - 以上字段按表的实际审计需求选用;创建人、修改人和对应时间应保持成对出现。 - 审计字段不是所有表的必选项,由客户维护、需要追溯变更的业务表才需要;系统内部使用的关联表、权限表等可以不加。 - 例外:授权类表(`s_user_power`、`s_user_field_power`、`s_user_data_power`)由用户在授权界面操作,需要追溯授权变更人,因此保留审计字段;`s_power` 属于开发期维护的系统元数据,不设审计字段。 - 这些字段只表示系统层面的创建和修改信息,不替代业务字段中的录入人、审核人、负责人或当前操作人。 ### 1.7 树结构规范 - 需要维护层级、支持新增和移动的树,统一采用“邻接表为主 + `b_depth`、`b_path` 冗余”的方案。 - `b_parent_id` 是树结构的权威关系,表示当前节点的直接父节点;`b_depth` 和 `b_path` 是由业务层维护的派生字段。 - 根节点的 `b_parent_id` 为 `NULL`,`b_depth` 从 `0` 开始计算。 - `b_path` 使用稳定节点编码组成,并在每个节点编码前后保留 `/` 分隔符,例如 `/sea/container/`;节点编码创建后原则上不可修改。 - `b_path` 用于前缀查询子树,`b_parent_id` 配合递归 CTE 用于查询父级、祖先和复杂层级关系。 - 新增和移动节点时,必须在同一业务事务中同步更新当前节点及其全部后代的 `b_depth` 和 `b_path`。 - 业务层必须校验节点不能指向自身,不能形成循环;这些规则不使用数据库 `CHECK` 或触发器约束。 - 当冗余字段可能不一致时,以 `b_parent_id` 为准,并提供按父子关系重建 `b_depth`、`b_path` 的维护能力。 - 模块树、菜单树和部门树使用该规范;JSON 内的布局树只在 JSON 内维护,不额外建立树表;主子表和引用关系使用 `s_relation`,多对多通过中间表模块加两条 `one_to_many` 表达,都不使用树字段表达。 ### 1.8 JSON 配置规范 - SQL Server 使用 `nvarchar(max)` 保存 JSON,由业务层负责序列化、反序列化和结构校验。 - JSON 配置如果存在结构升级需求,可以在顶层包含 `schemaVersion`,用于业务层迁移;不单独在数据库表中保存重复的版本字段。 - JSON 中通过字段编码引用 `s_field`,不得重复定义数据库类型、长度、精度等核心字段元数据。 - 布局 JSON 保存完整的模块默认配置;用户偏好 JSON 只保存相对默认配置的偏离量。 - 表单显示、只读、必填、禁用、条件显示、默认值、选项、格式化和字段计算等界面行为配置放在对应的布局 JSON 中,不放入 `s_field`。 - 读取用户配置时,先加载模块默认配置,再应用用户偏好覆盖;恢复默认时删除对应偏好或清空偏离配置。 - JSON 可以扩展新属性,但不得随意改变已有属性语义;不再使用的属性由业务层兼容或迁移。 ### 1.9 SQL 驱动规范 - 查询和数据处理优先采用可组合的 SQL 方案,避免把业务逻辑固化在单个页面或固定查询中。 - 各模块应预留 SQL 扩展入口,支持传入 SQL、动态拼接 `where` 条件、选择字段、排序、分页、关联查询和组合查询。 - 通用配置无法覆盖业务场景时,优先通过 SQL 视图、查询 SQL、条件片段或数据处理 SQL 扩展,不要立即新增重复表或重复模块。 - SQL 参数、查询字段、排序字段和关联关系应由明确的模块元数据描述,使 AI 能根据模块定义生成和组合 SQL。 - SQL 配置只描述可复用的数据处理能力,具体运行参数由调用方传入。 ### 1.10 索引和约束规范 - 主键、关联字段、常用查询条件、排序字段和唯一业务键应根据实际查询场景建立索引。 - 索引应服务于明确的查询或关联场景,避免无依据地为每个字段单独建立索引。 - 数据库层只保留主键、联合主键、索引和必要的唯一约束;唯一约束可用于保证业务键或组合业务键不重复。 - 不增加 `CHECK`、触发器等业务规则约束。字段取值、字段组合、状态流转、关联完整性和其他业务校验统一交给业务层处理。 - 非空和默认值属于字段定义的一部分,应根据业务语义设置,不使用数据库约束代替业务逻辑。 ## 2. 总体设计 新版结构强化“模块”概念,同时避免为字段布局和表单结构建立过多关系表。 ```text s_module ├── module 业务模块,可包含下级 module、data 或 virtual ├── data 数据模块,对应表、视图或查询 SQL,拥有字段定义 └── virtual 虚拟模块,不要求对应标准数据表,可由 SQL 或业务实现驱动 ``` “子模块”不是模块类型,由 `b_parent_id` 表达任意层级的父子关系。 核心存储原则: | 内容 | 存储方式 | 说明 | | --- | --- | --- | | 模块定义 | 关系表 | 系统核心业务结构 | | 字段定义 | 关系表 | 参与 SQL 生成、保存和数据处理 | | 列表布局 | JSON | 支持列分组、冻结、格式化和扩展属性 | | 表单布局 | JSON | 支持分组、栅格、选项卡和条件显示 | | 查询配置 | JSON | 支持同字段多条件、条件组和嵌套逻辑 | | 字段分组 | 布局 JSON 节点 | 不再建立独立字段分组表 | | 用户偏好 | JSON 偏离量 | 只保存用户相对系统默认的修改 | | 多语言 | 关系表 | 支持按语言查询、导入、导出和回退 | | 用户与部门 | 关系表 | 用户归属部门,部门支持层级树 | | 权限 | 关系表 | 见第 14 节,不与界面布局混合 | ## 3. 用户与部门 ### 3.1 用户 ```sql -- 用户表 create table dbo.b_user ( b_id varchar(50) not null primary key, -- 用户编码(业务键主键) b_name nvarchar(100) null, -- 用户名 b_dept_id varchar(50) null, -- 所属部门编码 b_bz nvarchar(400) null, -- 备注 b_password nvarchar(255) not null, -- 密码(明文保存) b_canuse tinyint not null default 1, -- 是否启用(0/1) b_logincount int not null default 0, -- 登录次数 b_created_by varchar(50) null, -- 创建人 b_created_at datetime2 null, -- 创建时间 b_updated_by varchar(50) null, -- 最后修改人 b_updated_at datetime2 null -- 最后修改时间 ); create index ix_b_user_dept on dbo.b_user (b_dept_id, b_canuse, b_id); ``` ### 3.2 部门 ```sql -- 部门表 create table dbo.b_dept ( b_id varchar(50) not null primary key, -- 部门编码(业务键主键) b_parent_id varchar(50) null, -- 父部门编码,NULL 表示根节点 b_depth int not null default 0, -- 树层级,根节点为 0 b_path varchar(1000) not null, -- 根到当前部门的路径,例如 /cn/east/ b_name nvarchar(200) not null, -- 部门名称 b_i18n varchar(150) null, -- 多语言资源键 b_canuse tinyint not null default 1, -- 是否启用(0/1) b_xh int not null default 0, -- 同级显示顺序 b_bz nvarchar(2000) null, -- 备注 b_created_by varchar(50) null, -- 创建人 b_created_at datetime2 null, -- 创建时间 b_updated_by varchar(50) null, -- 最后修改人 b_updated_at datetime2 null -- 最后修改时间 ); create index ix_b_dept_parent on dbo.b_dept (b_parent_id, b_canuse, b_depth, b_xh, b_id); create index ix_b_dept_path on dbo.b_dept (b_path, b_canuse, b_id); ``` 部门树使用 1.7 的树结构规范,`b_parent_id` 是权威关系,`b_depth`、`b_path` 是派生字段;`b_path` 的节点编码前缀用于查询下级部门。部门编码创建后原则上不可修改。`b_user.b_dept_id` 指向 `b_dept.b_id`,数据范围中的 `dept`、`dept_tree` 依赖该归属关系。 ## 4. 模块 ### 4.1 模块定义 ```sql -- 模块定义表 create table dbo.s_module ( b_id varchar(50) not null primary key, -- 模块编码(业务键主键) b_parent_id varchar(50) null, -- 父模块编码,NULL 表示根节点 b_depth int not null default 0, -- 树层级,根节点为 0 b_path varchar(1000) not null, -- 根到当前节点的路径,例如 /sea/container/ b_name nvarchar(200) not null, -- 模块名称 b_i18n varchar(150) null, -- 多语言资源键 b_module_type varchar(20) not null, -- module / data / virtual b_view_table varchar(128) null, -- 默认查询表或视图 b_save_table varchar(128) null, -- 默认保存目标表 b_key_field varchar(50) null, -- 主键字段 b_scope_field varchar(50) null, -- 数据范围字段(记录归属人字段,如 b_inputuser_id) b_order_sql varchar(500) null, -- 默认排序 SQL b_query_sql nvarchar(max) null, -- 可选查询 SQL 或 SQL 模板 b_config_json nvarchar(max) null, -- 低频模块扩展配置 b_canuse tinyint not null default 1, -- 是否启用(0/1) b_xh int not null default 0, -- 同级显示顺序 b_bz nvarchar(2000) null, -- 备注 b_updated_at datetime2 null -- 最后保存时间(本模块任意配置保存时刷新,含字段 / 界面配置等子表) ); create index ix_s_module_parent on dbo.s_module (b_parent_id, b_canuse, b_depth, b_xh, b_id); create index ix_s_module_path on dbo.s_module (b_path, b_canuse, b_id); create index ix_s_module_type on dbo.s_module (b_module_type, b_canuse, b_xh, b_id); create index ix_s_module_view_table on dbo.s_module (b_view_table, b_canuse, b_id); create index ix_s_module_save_table on dbo.s_module (b_save_table, b_canuse, b_id); ``` ### 4.2 模块类型约定 #### `module` - 表示业务模块或业务域,例如海运、空运、财务、客户管理。 - 可以包含下级 `module`、`data` 和 `virtual`。 - 默认不配置字段、查询表、保存表和主键字段。 - 可作为模块级配置、统计、导航上下文和权限继承的边界。 #### `data` - 表示具体数据对象,例如海运主单、集装箱、费用明细。 - 必须配置 `b_view_table` 或 `b_query_sql` 作为查询来源。 - 可以配置 `b_save_table` 作为默认保存目标。 - `b_key_field` 表示该数据对象的主键字段;没有稳定主键的查询数据可以为空。 - 字段定义保存在 `s_field`,界面配置保存在 `s_module_schema`。 - 主子表和引用关系通过 `s_relation` 表达,不使用模块树表达。 #### `virtual` - 表示不适合使用标准数据表和通用 CRUD 表达的业务能力。 - 可以由查询 SQL、组合 SQL、专用接口或业务处理器实现。 - 可以拥有字段定义和界面配置,但不强制配置 `b_save_table`。 - 没有标准保存表时,保存和业务动作由 SQL 或业务处理器实现。 模块类型、父子组合、字段适用规则和 SQL 配置的完整性由业务层校验,不建立 `CHECK` 约束。 ### 4.3 模块树与数据关系边界 - `b_parent_id`、`b_depth`、`b_path` 共同构成模块树,其中 `b_parent_id` 是权威关系,后两者是冗余查询字段。 - `b_path` 使用 `/` 包裹节点编码,例如 `/sea/`、`/sea/container/`;查询模块子树时使用路径前缀匹配。 - `module` 可以包含下级 `module`、`data` 和 `virtual`;`data` 和 `virtual` 默认作为叶子节点。 - 新增或移动模块时,业务层必须在同一事务中更新当前模块及全部后代的 `b_depth` 和 `b_path`。 - 不允许模块指向自身或形成循环;`b_id` 创建后原则上不可修改;这些规则由业务层校验。 - `b_depth`、`b_path` 与 `b_parent_id` 不一致时,以 `b_parent_id` 为准并重建冗余字段。 - 数据之间的主子表、引用和多对多关系统一由 `s_relation` 表达。 - 多对多的中间表模块(`data` 模块)在模块树中统一挂在所属业务域 `module` 节点下,与参与多对多的主表模块平级;不挂在任一参与多对多的主表模块下——模块树只表达业务组织,不表达数据关系,混入会污染 `b_path` 子树查询、菜单挂载与权限继承。 - 模块级扩展配置使用 `b_config_json`,只保存低频、非核心的扩展属性,不重复保存字段定义、权限或界面布局。 - `b_scope_field` 是本模块的数据范围字段(记录归属人字段,如 `b_inputuser_id`),只对 `data` / `virtual` 有意义,取值必须来自 `s_field`;它是数据范围判定的唯一字段来源,配在模块上而不是配在用户授权行上——同一个模块的所有用户必须用同一字段判范围。`self` 直接比该字段,`dept` / `dept_tree` 由当前用户反查 `b_user.b_dept_id` 得到部门再来比(见 §14.4);为空表示该模块不按字段判范围。 ### 4.4 查询来源优先级 数据加载时按以下规则确定默认查询来源: 1. 配置 `b_query_sql` 时,使用查询 SQL 或 SQL 模板; 2. 未配置 `b_query_sql` 时,使用 `b_view_table`; 3. 两者均未配置时,由虚拟模块的业务实现负责提供数据; 4. 调用方传入的筛选、字段、排序和分页配置在默认查询来源上继续组合。 保存时按以下规则确定默认保存方式: 1. `data` 模块配置 `b_save_table` 时,默认写入该表; 2. 未配置 `b_save_table` 或需要特殊处理时,由业务层使用模块对应的 SQL 保存逻辑或业务处理器负责保存; 3. `virtual` 模块不要求具备通用保存能力。 ## 5. 字段定义 字段定义保持关系表结构。数据库表或视图是物理字段结构的来源,`s_field` 只保存模块使用字段时所需的业务元数据。 当前阶段不在 `s_field` 中保存数据库类型、长度、精度、小数位等物理结构信息。保存器或 SQL 生成器需要这些信息时,直接读取数据库表或视图的实际结构;后续确定元数据同步方案后再补充持久化字段。 ```sql -- 字段定义表 create table dbo.s_field ( b_module_id varchar(50) not null, -- 所属 data / virtual 模块编码 b_field varchar(50) not null, -- 模块字段编码或查询结果别名 b_name nvarchar(200) not null, -- 字段名称 b_i18n varchar(150) null, -- 多语言资源键 b_type varchar(30) not null, -- 业务类型 b_canuse tinyint not null default 1, -- 是否启用(0/1) b_xh int not null default 0, -- 默认顺序 primary key (b_module_id, b_field) ); create index ix_s_field_module_order on dbo.s_field (b_module_id, b_canuse, b_xh, b_field); ``` 字段同步约定: - `data` 模块可以从 `b_view_table` 或 `b_query_sql` 的结果结构增量同步字段。 - 同步负责按来源表或视图的列结构对齐字段定义,不覆盖人工维护的字段名称和业务类型。 - 数据库中已删除的字段不直接物理删除,可由业务层标记为停用并提示人工确认。 - 计算字段、关联显示字段和虚拟字段可以人工登记,并通过模块查询 SQL 或界面配置 JSON 产生。 - 字段是否可以直接写入、是否只读以及是否需要自定义保存逻辑,不在 `s_field` 中增加固定模式字段;由表单 JSON、模块保存配置或业务处理器根据具体场景处理。 - `b_type` 的取值集合由业务层维护,本版不做固定枚举。 ## 6. 模块界面配置 列表布局、表单布局、查询配置和字段分组统一保存为 JSON,不再建立以下独立表: ```text s_field_group s_field_view s_field_edit s_field_query ``` ```sql -- 模块界面配置表 create table dbo.s_module_schema ( b_module_id varchar(50) not null, -- 所属 data / virtual 模块编码 b_schema_type varchar(20) not null, -- view / edit / query b_schema_json nvarchar(max) not null, -- 完整 JSON 配置 b_canuse tinyint not null default 1, -- 是否启用(0/1) b_updated_by varchar(50) null, -- 最后修改人 b_updated_at datetime2 null, -- 最后修改时间 primary key (b_module_id, b_schema_type) ); create index ix_s_module_schema_type on dbo.s_module_schema ( b_module_id, b_schema_type, b_canuse ); ``` 每个模块默认维护三行配置,分别对应列表、表单和查询: ```text view edit query ``` 当前阶段不维护同一类型的多套命名配置。若未来确实需要多套列表、表单或查询方案,再增加配置编码字段,或在 JSON 内增加方案节点。 ### 6.0 配置两层继承(系统默认 → 个人) 模块 UI 配置采用两层继承,由两张字符结构一致的表(`s_module_schema` 系统默认 + `s_user_module_pref` 个人覆盖)表达。配置层级由**表本身**体现,不在 JSON 内部用 `default`、`system`、`user` 等节点表示层级。 ```text 系统默认(s_module_schema) 产品/开发者提供的标准 Schema ↓ 覆盖 个人配置(s_user_module_pref) 用户在系统默认之上的个人调整 ``` - `view`、`edit`、`query` 是三种一致的 `b_schema_type`,两层共用同一命名,不拆成独立表,也不引入部门/角色/租户配置层。 - 优先级:个人 > 系统默认。个人层无记录时即系统默认。 - **保存为通用**:把当前查询布局回写为新的系统默认——直接更新 `s_module_schema.query` 的 quick 顺序并标记隐藏字段,同时清空该用户的个人覆盖,所有用户默认即在改后配置。 - 覆盖方式:`s_module_schema` 保存完整默认 Schema;`s_user_module_pref` 只保存相对系统默认的**增量覆盖**,不全量复制,因此系统默认修改后能自然继承。 ### 6.1 列表配置示例 ```json { "schemaVersion": 1, "columns": [ { "field": "b_no", "visible": true, "width": 140, "fixed": "left" }, { "type": "group", "id": "group1", "title": "费用信息", "i18n": "group.cw_fee.view.group1", "children": [ { "field": "mx_amount", "width": 120, "format": "money" }, { "field": "mx_currency", "width": 90 } ] } ] } ``` ### 6.2 表单配置示例 ```json { "schemaVersion": 1, "children": [ { "type": "group", "id": "group1", "title": "基础信息", "i18n": "group.sea_main.edit.group1", "children": [ { "type": "row", "children": [ { "field": "b_no", "span": 12, "readonly": true }, { "field": "b_name", "span": 12, "required": true } ] } ] } ] } ``` 表单 JSON 可以描述字段的展示和交互行为,例如: ```json { "field": "mx_amount", "readonly": true, "visibleWhen": { "field": "mx_type", "operator": "eq", "value": "tax" }, "formula": "mx_qty * mx_price" } ``` 其中 `readonly`、`required`、`disabled`、`visibleWhen`、`defaultValue`、`options`、`format` 和 `formula` 都属于界面配置,不放入 `s_field`。如果计算字段需要作为查询结果参与 SQL、排序或筛选,应在模块的 `b_query_sql` 中定义,而不是依赖表单 JSON。 ### 6.3 查询配置示例 ```json { "schemaVersion": 1, "quick": ["b_no", "b_name", "b_status"], "groups": [ { "logic": "and", "conditions": [ { "field": "b_status", "operator": "eq" }, { "logic": "or", "conditions": [ { "field": "b_no", "operator": "like" }, { "field": "b_name", "operator": "like" } ] } ] } ] } ``` ### 6.4 分组节点 id 与资源键 `view`、`edit` 的 JSON 里,分组节点(`type: "group"`)用 `id` 表达稳定身份;层级由 `children` 嵌套表达,顺序由数组顺序表达,分组不建独立表(见本节开头的 `s_field_group`)。 ```text "id": "group1" 按 schema 类型独立编号(view 与 edit 各自从 group1 开始) ``` - **唯一范围**:分组 id 只在单个 `b_schema_json` 内唯一。列表的 `group1` 与表单的 `group1` 是两处不同分组,互不引用,因此也不做模块内跨 schema 全局唯一。 - **稳定优先**:编号一旦分配不再重排,删除分组留下的空洞允许存在(重排会改身份,进而改资源键)。 - **取号规则**:新建分组取该配置内已用编号里最小的未用正整数;同时保留 `s_i18n` 中该模块同 schema 已存在的分组键编号,避免新分组捡到已删除分组的编号、连带继承其残留译文。 - **老数据兼容**:`id` 缺失或重复的节点由归一化补齐(先序、取最小可用编号),非 `group{n}` 的自定义 id 视为已占用并原样保留。归一化必须是确定性纯函数——加载后要与基线比较,若补号依赖外部上下文,同一份数据两次归一化结果会不同而被误判为未保存改动。 - **资源键**:分组标题的资源键为 `group.{模块编码}.{schema 类型}.{分组编号}`(如 `group.cw_fee.view.group1`),由 id 派生而非人工填写;节点上的 `i18n` 手填优先,未填时按此规则派生,使分组标题无需手工登记即进入多语言受管清单。schema 类型必须入键,否则列表与表单的同号分组会互相覆盖译文。 ## 7. 用户个性化(个人层) 个人配置是两层继承的最上层,只保存相对系统默认(`s_module_schema`)的偏离量,不复制完整模块配置。与 `s_module_schema` 统一使用 `b_schema_type` + `b_schema_json`,按 schema 类型分别存一条。 ```sql -- 用户模块偏好表(个人层) create table dbo.s_user_module_pref ( b_user_id varchar(50) not null, -- 用户编码(关联 b_user.b_id) b_module_id varchar(50) not null, -- 模块编码 b_schema_type varchar(20) not null, -- view / edit / query b_schema_json nvarchar(max) not null, -- 个人覆盖 JSON(相对系统默认) b_updated_by varchar(50) null, -- 最后修改人 b_updated_at datetime2 null, -- 最后修改时间 primary key (b_user_id, b_module_id, b_schema_type) ); create index ix_s_user_module_pref_module on dbo.s_user_module_pref (b_module_id, b_user_id); ``` 查询布局偏好示例(平铺、增量,不使用 `view.default` / `query.default` 节点): ```json { "schemaVersion": 1, "area": { "b_no": "quick", "b_bz": "hidden" }, "order": { "b_no": 10, "b_name": 20 } } ``` 合并规则(两层依次应用): 1. 读取 `s_module_schema` 系统默认(query 布局由 `quick` 列表 + 条件推导); 2. 读取当前用户的 `s_user_module_pref.b_schema_json`,按 schema 类型叠加个人偏离量,得到最终配置; 3. 已停用、已删除或无权限的字段(`s_user_field_power`)不得因配置重新出现;权限过滤在配置合并之后执行,永远优先于配置; 4. 恢复默认(恢复通用)= 删除 `s_user_module_pref` 对应行,回落到系统默认;「恢复默认」是删除覆盖,不是新增配置层。 ## 8. 自动编码 ```sql -- 自动编码表 create table dbo.s_autocode ( b_module_id varchar(50) not null, -- data 模块编码 b_field varchar(50) not null, -- 编码字段 b_prefix nvarchar(200) not null default N'', -- 编码前缀 b_dateformat varchar(30) not null default 'yyyyMM', -- 日期格式 b_separator nvarchar(20) not null default N'-', -- 分隔符 b_seqwidth int not null default 5, -- 序号宽度 b_resettype varchar(10) not null default 'month', -- 重置类型 b_startvalue bigint not null default 1, -- 起始值 b_currentperiod varchar(20) null, -- 当前周期 b_currentvalue bigint not null default 0, -- 当前值 b_canuse tinyint not null default 1, -- 是否启用(0/1) primary key (b_module_id, b_field) ); ``` 编码格式、重置类型和字段适用性由业务层校验。 ## 9. 模块关系 模块树只表达业务模块层级,主子表、引用和多对多关系统一由 `s_relation` 表达。 ```sql -- 模块关系表 create table dbo.s_relation ( b_source_module_id varchar(50) not null, -- 源模块 b_source_field varchar(50) not null, -- 源字段 b_target_module_id varchar(50) not null, -- 目标模块 b_target_field varchar(50) not null, -- 目标字段 b_relation_type varchar(20) not null, -- one_to_one / one_to_many / many_to_one b_canuse tinyint not null default 1, -- 是否启用(0/1) b_xh int not null default 0, -- 显示顺序 primary key ( b_source_module_id, b_source_field, b_target_module_id, b_target_field ) ); create index ix_s_relation_source on dbo.s_relation (b_source_module_id, b_canuse, b_xh, b_source_field, b_target_module_id); create index ix_s_relation_target on dbo.s_relation (b_target_module_id, b_canuse, b_source_module_id); ``` 关系类型取值由业务层校验;主子表和引用关系都通过"源字段 → 目标字段"这一组映射表达。暂时不提供 `many_to_many`,多对多通过中间表模块加两条 `one_to_many` 关系表达。 ## 10. 菜单 菜单负责导航和页面入口,模块负责业务结构和数据能力。一个菜单可以使用多个 `data` 或 `virtual` 模块,一个模块也可以被多个菜单复用。 ```sql -- 菜单表 create table dbo.s_menu ( b_id varchar(50) not null primary key, -- 菜单编码(业务键主键) b_parent_id varchar(50) null, -- 父菜单编码,NULL 表示根节点 b_depth int not null default 0, -- 树层级,根节点为 0 b_path varchar(1000) not null, -- 根到当前菜单的路径,例如 /system/user/ b_name nvarchar(200) not null, -- 菜单名称 b_i18n varchar(150) null, -- 多语言资源键 b_menu_type varchar(20) not null, -- directory / page / external b_route varchar(500) null, -- 页面路由或外链地址 b_icon varchar(50) null, -- 图标 b_xh int not null default 0, -- 显示顺序 b_canuse tinyint not null default 1 -- 是否启用(0/1) ); create index ix_s_menu_parent on dbo.s_menu (b_parent_id, b_canuse, b_depth, b_xh, b_id); create index ix_s_menu_path on dbo.s_menu (b_path, b_canuse, b_id); create index ix_s_menu_route on dbo.s_menu (b_route, b_canuse, b_id); -- 菜单使用模块关系表 create table dbo.s_menu_module ( b_menu_id varchar(50) not null, -- 菜单编码 b_module_id varchar(50) not null, -- 页面实际使用的 data / virtual 模块编码 b_xh int not null default 0, -- 页面内模块顺序 b_canuse tinyint not null default 1, -- 是否启用(0/1) primary key (b_menu_id, b_module_id) ); create index ix_s_menu_module_module on dbo.s_menu_module (b_module_id, b_canuse, b_menu_id); ``` 菜单树同样使用 `b_parent_id` 作为权威关系,并冗余维护 `b_depth`、`b_path`。菜单节点新增、移动和删除时,由业务层同步处理后代节点路径和层级;菜单编码创建后原则上不可修改。 绑定规则: - `directory`(目录)和 `external`(外链)不绑定模块;`page` 可绑定一个或多个模块; - 关系表只登记 `data` / `virtual` 模块:权限按 `data` 模块判定,`module` 类型只是模块树上的分类节点,不参与权限,也不进入关系表; - 菜单管理界面按模块树勾选,勾中分类节点时自动展开为其下全部 `data` 模块逐条登记(界面级快捷操作,落库的始终是明确的模块清单,之后可单独增删); - 菜单编码创建后原则上不可修改,绑定关系随菜单保存一并提交。 业务模块上下文可以根据所绑定数据模块的父级模块获得;暂不增加独立的菜单上下文字段。 菜单权限(`menu.*`)只控制页面入口;页面内各 `data`、`virtual` 模块的操作权限按模块编码独立判定,一个菜单绑定多个模块时不做权限合并。 ## 11. 多语言 多语言继续使用关系表。`s_i18n_type` 保存系统支持的语言区域(例如 `zh-CN`、`en`),模块、菜单、字段和 JSON 布局节点只保存资源键,具体语言文本统一保存到 `s_i18n`。资源来源由资源键命名空间表达,例如 `module.sea.name`、`menu.main.title`、`field.sea.b_no`;不在翻译表中重复保存多态来源字段。 ```sql -- 多语言类型表 create table dbo.s_i18n_type ( b_id varchar(20) not null primary key, -- 语言区域编码,例如 zh-CN / en b_name nvarchar(200) not null, -- 语言名称 b_canuse tinyint not null default 1, -- 是否启用(0/1) b_default tinyint not null default 0, -- 是否默认(0/1) b_xh int not null default 0 -- 显示顺序 ); -- 多语言资源表 create table dbo.s_i18n ( b_key varchar(150) not null, -- 资源键 b_locale varchar(20) not null, -- 语言区域 b_value nvarchar(1000) not null, -- 翻译值 b_canuse tinyint not null default 1, -- 是否启用(0/1) primary key (b_key, b_locale) ); create index ix_s_i18n_locale on dbo.s_i18n (b_locale, b_canuse, b_key); ``` `s_i18n.b_locale` 的取值应存在于 `s_i18n_type.b_id`,由业务层校验,不建立外键。`b_default = 1` 表示默认语言,默认语言只能有一个有效项,由业务层维护。资源清理按资源键或资源键前缀执行,不依赖 `b_source_id`。 资源键命名应保持稳定并体现来源边界,推荐使用以下前缀: | 前缀 | 用途 | 示例 | | --- | --- | --- | | `module.` | 模块名称或模块业务文案 | `module.sea.name` | | `menu.` | 菜单名称 | `menu.main.title` | | `field.` | 字段名称 | `field.sea.b_no` | | `group.` | 列表/表单布局分组标题 | `group.sea.view.group1` | | `action.` | 动作动词(按钮 + 权限共用) | `action.insert` | 资源键一旦投入使用原则上不可修改;模块、菜单或字段删除时,由业务层根据已知资源键清理对应翻译行。 `module.*`、`menu.*`、`field.*`、`action.*` 由业务编码派生,不需要人工填写;`group.*` 的编号段取自界面配置里分组节点的 `id`,未手填节点 `i18n` 时同样由 id 派生,规则见 6.4 节。模块管理页登记的分组译文在表单渲染时按同一规则解析(节点缺失 id 的老数据由归一化补齐后同样能取到译文)。 `action.*` 保存通用的动作动词资源键(例如 `action.insert`、`action.update`、`action.audit`),按钮文案和权限点名称共用同一翻译,同一动词只维护一份;动作动词是通用词,不按模块或对象分段。 ## 12. 日志 ```sql -- 技术日志表 create table dbo.s_log_technical ( b_id uniqueidentifier not null primary key, -- 日志 ID(UUIDv7) b_user_id varchar(50) null, -- 用户编码 b_sourcetype varchar(20) null, -- 来源类型 b_loglevel varchar(16) not null, -- 日志级别 b_logcategory varchar(64) null, -- 日志分类 b_operation varchar(100) null, -- 操作 b_module_id 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(4000) null, -- 错误信息 b_exception_text nvarchar(max) null, -- 异常文本 b_context_text nvarchar(max) null, -- 上下文文本 b_occurdatetime datetime2 default sysutcdatetime() -- 发生时间 ); -- 登录日志表 create table dbo.s_log_login ( b_id uniqueidentifier not null primary key, -- 日志 ID(UUIDv7) b_user_id varchar(50) null, -- 用户编码 b_login_type varchar(20) not null, -- 登录类型 b_login_result tinyint not null default 0, -- 登录结果(0/1) b_fail_reason nvarchar(400) null, -- 失败原因 b_ip varchar(50) null, -- IP b_ip_location nvarchar(400) 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, -- 设备类型 b_user_agent nvarchar(max) null, -- UserAgent b_session_id varchar(50) null, -- 会话 ID b_request_id varchar(64) null, -- 请求 ID b_trace_id varchar(64) null, -- 链路 ID b_occurdatetime datetime2 default sysutcdatetime() -- 发生时间 ); -- 审计日志表 create table dbo.s_log_audit ( b_id uniqueidentifier not null primary key, -- 日志 ID(UUIDv7) b_event_code varchar(50) not null, -- 事件编码 b_module_id varchar(50) null, -- 模块编码 b_data_id varchar(50) null, -- 数据 ID b_business_no varchar(50) null, -- 业务单号 b_operation varchar(100) null, -- 操作 b_operator_id varchar(50) null, -- 操作人 ID b_operator_name nvarchar(200) null, -- 操作人名称 b_source_type varchar(20) not null default 'user', -- 来源类型 b_visibility varchar(20) not null default 'internal', -- 可见性 b_request_id varchar(64) null, -- 请求 ID b_trace_id varchar(64) null, -- 链路 ID b_payload_text nvarchar(max) null, -- 载荷文本 b_occurdatetime datetime2 default sysutcdatetime() -- 发生时间 ); -- 审计字段明细表 create table dbo.s_log_audit_field ( b_id uniqueidentifier not null primary key, -- 日志 ID(UUIDv7) b_event_id uniqueidentifier not null, -- 审计事件 ID b_module_id varchar(50) null, -- 模块编码 b_data_id varchar(50) null, -- 数据 ID b_field varchar(50) not null, -- 字段编码 b_field_label nvarchar(200) null, -- 字段名称 b_before_value nvarchar(max) null, -- 变更前值 b_after_value nvarchar(max) null, -- 变更后值 b_before_display nvarchar(1000) null, -- 变更前显示值 b_after_display nvarchar(1000) null, -- 变更后显示值 b_visibility varchar(20) not null default 'internal' -- 可见性 ); create index ix_s_log_technical_occur on dbo.s_log_technical (b_occurdatetime, b_id); create index ix_s_log_technical_trace on dbo.s_log_technical (b_trace_id, b_occurdatetime); create index ix_s_log_login_occur on dbo.s_log_login (b_occurdatetime, b_id); create index ix_s_log_login_user on dbo.s_log_login (b_user_id, b_occurdatetime); create index ix_s_log_audit_occur on dbo.s_log_audit (b_occurdatetime, b_id); create index ix_s_log_audit_module on dbo.s_log_audit (b_module_id, b_data_id, b_occurdatetime); create index ix_s_log_audit_field_event on dbo.s_log_audit_field (b_event_id, b_id); ``` 审计日志中的 `b_data_id` 统一使用 `varchar(50)` 保存业务主键:业务表使用 `bigint` 雪花 ID 时按字符串写入,不额外记录主键原始类型;用户、模块等 `varchar(50)` 主键直接原样保存。 ## 13. 业务表主键约定 业务表使用应用层生成的雪花 ID。需要跨系统公开、离线生成或按时间有序的标识时,可以单独评估 UUIDv7,但同一业务域内应保持一致。 ```sql -- 业务表示例 create table dbo.b_example ( b_id bigint not null primary key, -- 主键(应用层雪花 ID) b_inputuser_id varchar(50) null, -- 录入用户 ID b_inputdatetime datetime2 default sysutcdatetime() -- 录入时间 ); ``` ## 14. 权限设计 权限部分只保留四张表,分别回答四个问题:"系统有哪些权限点""用户拥有哪些权限点""用户对字段能做什么""用户能看到哪些数据"。 | 表 | 职责 | | --- | --- | | `s_power` | 定义系统有哪些权限点(菜单、模块、动作) | | `s_user_power` | 定义用户拥有哪几个权限点 | | `s_user_field_power` | 定义用户对字段能做什么 | | `s_user_data_power` | 定义用户能看到/操作哪些数据 | 边界约定: - 菜单、模块和动作权限统一由 `s_power` + `s_user_power` 表达,不按权限类型拆分独立表; - 字段权限是"用户 + 模块 + 字段"的能力矩阵,不展开成 `field.xxx.read`、`field.xxx.edit` 这类权限点; - 数据范围由 `s_user_data_power` 单独表达,不写入 `s_power`; - 当前阶段采用**用户直接授权**,不引入角色、权限模板和用户组;授权记录只表达"拥有",不做拒绝授权;后续如需角色继承或权限模板,可在不改变本表语义的前提下扩展来源字段或另建角色授权表; - 权限的加载、合并、继承、条件生成和 SQL 拼接由业务层权限引擎处理,不建立外键、触发器或数据库约束。 权限判定的整体顺序: ```text 用户 ├── 菜单权限 s_user_power (menu.*) → 能否进入页面 ├── 模块权限 s_user_power (module.*) → 能否使用该模块(访问模块数据) ├── 动作权限 s_user_power (action.*) → 能否审核 / 反审 / 核销 / 结算 ├── 字段权限 s_user_field_power → 哪些字段能看 / 编辑 / 查询 / 导出 └── 数据范围 s_user_data_power → 哪些数据行可操作 ``` ### 14.1 权限点定义 ```sql create table dbo.s_power ( b_id varchar(150) not null primary key, -- 权限编码,如 module.sea、menu.sea_list b_name nvarchar(200) not null, b_i18n varchar(150) null, -- 多语言资源键 b_type varchar(20) not null, -- menu / module / action(权限点性质) b_object_type varchar(20) not null, -- menu / module(挂在谁身上) b_object_id varchar(50) not null, -- s_menu.b_id 或 s_module.b_id b_action varchar(30) not null, -- access(menu / module 使用权限)或具体业务动作(action) b_canuse tinyint not null default 1, b_xh int not null default 0, b_bz nvarchar(500) null ); create index ix_s_power_type on dbo.s_power (b_type, b_canuse, b_xh, b_id); create index ix_s_power_object on dbo.s_power (b_object_type, b_object_id, b_canuse, b_action, b_id); create unique index ux_s_power_object_action on dbo.s_power (b_object_type, b_object_id, b_action); ``` `b_type` 只保留三类。权限编码分段命名:`menu`、`module` 为 `<类型>.<对象编码>` 两段,表达"能否使用";`action` 为 `<类型>.<对象编码>.<动作>` 三段: | b_type | 含义 | 示例 | | --- | --- | --- | | `menu` | 菜单/页面访问权限 | `menu.sea_list` | | `module` | 模块使用权限 | `module.sea` | | `action` | 业务动作权限 | `action.sea.audit` | ```text menu.sea_list menu.sea_edit module.sea action.sea.audit action.sea.unaudit ``` 权限编码创建后原则上不可修改;编码分段规则和 `b_type` 取值由业务层校验。字段权限和数据范围不进入 `s_power`。 `s_power.b_i18n` 保存权限点名称的多语言资源键,全部复用既有资源键命名空间,不设独立的 `power.*` 前缀。`action` 类型权限点使用通用动作动词资源键,例如 `action.insert`、`action.update`、`action.audit`,与按钮文案共用同一翻译;`menu`、`module` 类型权限点的名称与对应菜单、模块名称一致,直接复用 `menu.*`、`module.*` 名称资源键,不再单独保存权限点名称翻译。资源键的稳定性、清理和回退规则见第 11 节。 ### 14.2 用户功能权限 ```sql -- 用户权限表 create table dbo.s_user_power ( b_user_id varchar(50) not null, -- 用户编码 b_power_id varchar(150) not null, -- 权限编码 b_canuse tinyint not null default 1, -- 是否启用(0/1) b_created_by varchar(50) null, -- 创建人 b_created_at datetime2 null, -- 创建时间 b_updated_by varchar(50) null, -- 最后修改人 b_updated_at datetime2 null, -- 最后修改时间 primary key (b_user_id, b_power_id) ); create index ix_s_user_power_power on dbo.s_user_power (b_power_id, b_user_id); ``` 示例: | b_user_id | b_power_id | b_canuse | | --- | --- | --- | | u001 | `menu.sea_list` | 1 | | u001 | `module.sea` | 1 | | u001 | `action.sea.audit` | 1 | 判定规则: 1. **无记录即无权限**(白名单):`s_user_power` 中不存在该用户该权限点的记录时视为无权限; 2. `b_canuse = 0` 或对应 `s_power.b_canuse = 0` 时,该授权不生效。 ### 14.3 用户字段权限 字段权限是能力矩阵,直接以"用户 + 模块 + 字段"记录,不拆分为多个权限点。 ```sql -- 用户字段权限表 create table dbo.s_user_field_power ( b_user_id varchar(50) not null, -- 用户编码 b_module_id varchar(50) not null, -- 模块编码 b_field_id varchar(50) not null, -- 字段编码,对应 s_field.b_field b_view tinyint not null default 1, -- 查看(0/1) b_edit tinyint not null default 0, -- 编辑(0/1) b_query tinyint not null default 1, -- 查询(0/1) b_export tinyint not null default 1, -- 导出(0/1) b_canuse tinyint not null default 1, -- 是否启用(0/1) b_created_by varchar(50) null, -- 创建人 b_created_at datetime2 null, -- 创建时间 b_updated_by varchar(50) null, -- 最后修改人 b_updated_at datetime2 null, -- 最后修改时间 primary key (b_user_id, b_module_id, b_field_id) ); create index ix_s_user_field_power_module on dbo.s_user_field_power (b_module_id, b_user_id, b_field_id); ``` 示例: | 用户 | 模块 | 字段 | view | edit | query | export | | --- | --- | --- | --- | --- | --- | --- | | u001 | sea | `b_amount` | 1 | 1 | 1 | 1 | | u001 | sea | `b_cost` | 1 | 0 | 0 | 0 | | u001 | sea | `b_profit` | 0 | 0 | 0 | 0 | 判定规则: 1. **无记录时取模块默认行为**:可查看、可查询、可导出,不可编辑; 2. 有记录时按 `b_view`、`b_edit`、`b_query`、`b_export` 逐项取值,四项相互独立; 3. `b_view = 0` 的字段在列表、表单、查询和导出中统一移除,且**不得因用户偏好重新出现**(与第 7 节偏好合并规则一致); 4. `b_query = 0` 的字段不出现在查询配置和快捷搜索中,也不得作为调用方传入的筛选条件。 ### 14.4 用户数据范围 ```sql -- 用户数据范围表 create table dbo.s_user_data_power ( b_user_id varchar(50) not null, -- 用户编码 b_module_id varchar(50) not null, -- 模块编码 b_operation varchar(30) not null default '*', -- 操作,* 表示全部操作 b_scope_type varchar(20) not null, -- all / self / dept / dept_tree / custom b_scope_field varchar(50) null, -- 模块中用于范围判断的字段 b_condition_sql nvarchar(max) null, -- 自定义条件 SQL(custom 时使用) b_xh int not null default 0, -- 同操作内规则顺序 b_canuse tinyint not null default 1, -- 是否启用(0/1) b_created_by varchar(50) null, -- 创建人 b_created_at datetime2 null, -- 创建时间 b_updated_by varchar(50) null, -- 最后修改人 b_updated_at datetime2 null, -- 最后修改时间 primary key (b_user_id, b_module_id, b_operation, b_xh) ); create index ix_s_user_data_power_module on dbo.s_user_data_power (b_module_id, b_user_id, b_operation, b_xh); ``` `b_scope_type` 先固定五类: | 类型 | 含义 | 说明 | | --- | --- | --- | | `all` | 全部数据 | 不追加范围条件 | | `self` | 仅本人 | `b_scope_field = 当前用户编码` | | `dept` | 本部门 | `b_scope_field = b_user.b_dept_id` | | `dept_tree` | 本部门及下级部门 | 用当前用户部门的 `b_dept.b_path` 前缀匹配出部门集合,再过滤 `b_scope_field` | | `custom` | 自定义规则 | 由 `b_condition_sql` 提供条件 | `b_operation` 取值:`read`、`create`、`update`、`delete`、`export` 为通用模块操作;业务动作使用对应的 `action` 权限编码(例如 `action.sea.audit`);`*` 表示对该模块的所有操作生效,精确值优先于 `*`。 示例: ```text b_user_id = u001 b_module_id = sea b_operation = read b_scope_type = self b_scope_field = b_inputuser_id ``` 表示 u001 查看海运单时只能看 `b_inputuser_id = 当前用户` 的数据。 `b_scope_field` 表示**当前模块中用于执行数据范围判断的字段**(例如 `sea.b_inputuser_id`、`sea.b_department_id`),不是用户表的字段。`self`、`dept`、`dept_tree` 必须配置该字段;`b_scope_field` 应是模块 `s_field` 中已登记且可直接参与 SQL 过滤的字段。`dept`、`dept_tree` 依赖 `b_user.b_dept_id` 与 `b_dept` 部门树,用户未归属部门时该规则不生效。 判定规则: 1. 按 `b_operation` **精确匹配优先**,未命中时使用 `b_operation = '*'` 的规则; 2. 同一用户、同一模块、同一操作的**多条生效规则之间按 OR 组合**,最终整体再与其他来源条件按 AND 组合; 3. `b_canuse = 0` 的规则不参与组合;无任何生效规则时,视为该模块无数据范围限制。 ### 14.5 `b_condition_sql` 的使用边界 - 该字段**保留用于复杂场景的出口**,只由管理员配置,不开放给普通用户直接编写 SQL; - 运行时必须由后端权限引擎处理参数替换(例如 `{userId}`、`{deptId}`),并以预编译参数或受控片段方式拼装,**不得直接拼接前端传入内容**; - 仅当 `b_scope_type = 'custom'` 时生效; - 后续如规则体系成熟,优先将 `b_condition_sql` 升级为结构化条件(字段 / 操作符 / 值的 AST),避免让 SQL 成为权限系统的核心表达方式。 示例: ```sql b_scope_type = 'custom' b_condition_sql = 'b_status = ''正常''' b_scope_type = 'custom' b_condition_sql = 'b_salesman_id = {userId}' ``` ### 14.6 与模块、菜单和关系的关系 - `s_power` 中的 `menu.*` 对应 `s_menu`,`module.*` 对应 `s_module`,`action.*` 对应业务动作;权限点编码与对象编码的对应关系由业务层维护,不建立外键; - 授权行只能引用存在的权限点:删 `s_power` 行时用 `dependent_deletes` 声明(`{table:'s_user_power', key_field:'b_power_id', source_field:'b_id'}`),保存层在同一事务里按同一组条件查出权限编码并连带删除授权行,调用方不需要先查编码(《FMS删除策略重构设计》§6.2); - `s_user_field_power.b_module_id`、`s_user_data_power.b_module_id` 指向 `s_module.b_id`,字段还需在 `s_field` 中存在;删除模块时随模块一起删除 `s_user_field_power`、`s_user_data_power` 行,删除字段定义时删除该字段的 `s_user_field_power` 行; - 数据范围只作用于本表的 `b_user_id + b_module_id + b_operation`;是否向关联模块延伸(例如子表、明细模块)由权限引擎根据业务语义决定,不依赖 `s_relation` 自动传播; - 权限缓存、批量校验和 SQL 条件注入点由权限引擎实现,业务 SQL 生成时统一追加范围条件。 ## 15. 文件 文件统一由文件服务托管:业务表不保存文件二进制,只通过 `bf_files.father` 关联宿主业务主键, 按 `father + mx_moduleid + mx_cate_id` 聚合出某个业务对象、某模块、某分类下的附件列表。 存储方式由配置决定(本地磁盘 / 阿里云 OSS),两种模式共用同一张表:本地模式由后端写盘后落库, OSS 模式由前端取号直传后回调落库。 ```sql -- 文件表 create table dbo.bf_files ( subid bigint not null primary key, -- 文件主键(雪花,应用层取号:本地由后端、OSS 直传由前端先取号) father bigint null, -- 宿主业务主键(业务表 b_id),列表按它聚合 mx_filename nvarchar(500) null, -- 原始文件名(含扩展名,展示与下载用) mx_filesize varchar(50) null, -- 文件大小(字节,字符串存储) mx_fileext varchar(50) null, -- 扩展名(小写、不含点) mx_mapfilename nvarchar(500) null, -- 访问地址:本地为 {url-prefix}/{org}/{yyyy}/{MM}/{dd}/{uuid}.{ext},OSS 为完整 URL mx_moduleid varchar(50) null, -- 业务模块编码(s_module.b_id) mx_cate_id bigint null, -- 文件分类(bf_files_category.b_id,雪花主键) mx_xh int not null default 0, -- 排序号(保留;当前实现固定 0,列表按 subid 倒序) mx_sh tinyint not null default 0, -- 审核标识(保留;当前实现固定 0) mx_shuser_id varchar(50) null, -- 审核人(保留) mx_shdatetime datetime2 null, -- 审核时间(保留) mx_cutfilename nvarchar(500) null, -- 缩略图地址(保留) mx_cutfilesize varchar(50) null, -- 缩略图大小(保留) mx_orderid varchar(50) null, -- 排序组(保留) mx_bz nvarchar(500) null, -- 备注(保留) mx_inputuser_id varchar(50) null, -- 上传人(用户账号) mx_inputdatetime datetime2 null, -- 上传时间 mx_updateuser_id varchar(50) null, -- 最后修改人 mx_updatedatetime datetime2 null -- 最后修改时间 ); create index ix_bf_files_host on dbo.bf_files (father, mx_moduleid, mx_cate_id, mx_xh, subid); -- 文件分类表(按模块划分;b_module_id 为空表示跨模块通用分类) create table dbo.bf_files_category ( b_id bigint not null primary key, -- 主键(雪花,新增行由前端 nextIdApi 生成) b_module_id varchar(50) null, -- 归属模块(s_module.b_id);NULL = 通用分类 b_name nvarchar(50) not null, -- 分类名称 b_i18n varchar(150) null, -- 多语言资源键(s_i18n.b_key) b_files_type varchar(50) null, -- 允许的扩展名(逗号分隔,空 = 不限;预留) b_xh int not null default 0, -- 显示顺序 b_canuse tinyint not null default 1, -- 是否启用(0/1) b_bz nvarchar(200) null, -- 备注 b_created_by varchar(50) null, -- 创建人(用户账号) b_created_at datetime2 null, -- 创建时间 b_updated_by varchar(50) null, -- 最后修改人 b_updated_at datetime2 null -- 最后修改时间 ); create index ix_bf_files_category_module on dbo.bf_files_category (b_module_id, b_canuse, b_xh, b_id); ``` 要点: - **主键**:`subid` 为应用层雪花 ID,不使用 `identity`。本地模式由后端在写盘前取号;OSS 模式由前端在上传前取号(`nextIdApi`),直传完成后回调 `/file/save-record` 落库 —— 两条链路都不能依赖数据库自增。 - **宿主关联**:`father` 存宿主业务主键(业务表 `b_id`,bigint 雪花),不建外键;删除业务数据时由业务层级联清理附件(先删存储、后删行)。 - **存储地址**:`mx_mapfilename` 即访问地址,前端预览 / 下载直接使用。本地模式写入 `{fms.file.local.url-prefix}/{orgId}/{yyyy}/{MM}/{dd}/{uuid}.{ext}`,物理文件落在 `{fms.file.local.storage-dir}` 下的同路径;OSS 模式写入 g3oss 返回的完整 URL。 - **保留字段**:`mx_sh*`(审核)、`mx_cut*`(缩略图)、`mx_orderid`、`mx_bz`、`mx_xh` 沿用旧库结构,当前实现未使用(写 0 / 空值),保留以兼容历史数据迁移。 - **分类归属模块**:`bf_files_category.b_module_id` 指定分类所属模块,不填(NULL)为跨模块通用分类;分类树按当前模块过滤(`b_module_id IS NULL OR b_module_id = 当前模块`)。分类主键与 `bf_files.mx_cate_id` 类型保持一致(bigint 雪花)。 - **风格**:分类表按新版字典表风格(应用层雪花主键、`b_created_*` 审计列、`b_canuse tinyint`);`bf_files` 的 `subid` / `father` / `mx_*` 列名已被前后端实现引用,保持现状。表名 `bf_*` 属于"明确的业务域前缀",符合第 1 节表名规范。 - **旧库遗留**:`bf_files_type`(模块文件类型 / 数量配置)在旧库存在,但新旧系统均未使用,不迁移。 存储配置(`application.yaml`): ```yaml fms: file: storage-type: local # local / aliyun-oss local: storage-dir: ./data/files # 物理根目录 url-prefix: /api/file/static # 访问前缀 oss: g3oss-url: http://g3oss.g3soft.cn:8082 ``` 业务接入(前端 `FmsFile` 组件): ```text ├── 列表:bf_files 按 father + mx_moduleid + mx_cate_id 过滤,mx_xh ASC, subid DESC 排序 ├── 上传:local → POST /file/upload(后端写盘 + 落库) │ aliyun-oss → 取号 → 凭证直传 → POST /file/save-record 落库 └── 删除:POST /file/delete(删存储 → 删 DB 行) ``` ## 16. 工作流与审批 工作流是独立子系统,不属于系统内核,表统一使用 `wf_` 前缀(第 1.1 节的业务域前缀约定)。配置表保存流程定义、版本、节点、连线、审批人规则和模块绑定;运行表保存实例、节点运行、待办、抄送和不可变动作历史。 完整语义、审批方式(或签、会签、串行会签、N 人通过、按比例通过)、驳回策略、加签、催办和抄送规则见《FMS工作流与审批设计》,本文只记录表结构。 | 表 | 职责 | | --- | --- | | `wf_definition` / `wf_version` | 流程定义与可执行版本 | | `wf_node` / `wf_edge` / `wf_actor_rule` | 版本内的节点、连线和审批人规则 | | `wf_binding` | 模块和业务事件到流程版本的绑定 | | `wf_instance` / `wf_node_run` / `wf_task` | 流程实例、节点运行和待办任务 | | `wf_action` | 不可变的审批动作历史 | | `wf_cc` / `wf_delegation` / `wf_outbox` | 抄送记录、委托和通知消息 | 表结构约定: - 配置表使用 `varchar` 业务编码主键;运行表使用应用层生成的 UUIDv7(`uniqueidentifier`),与 `s_log_*` 一致(第 13 节); - 业务主键统一按字符串保存在 `b_business_id`,不依赖业务表的物理主键类型; - 状态、类型和动作编码的取值只写在列注释里,不使用 `CHECK` 约束、外键和触发器,合法性由业务层校验(第 1.10 节); - 已发布版本不可修改,运行中的实例固定引用提交时的 `b_version_id`; - 变化频繁的配置使用 `nvarchar(max)` JSON,顶层包含 `schemaVersion`(第 1.8 节)。 ### 16.1 流程定义 ```sql -- 流程定义表(稳定逻辑身份,执行内容在版本表) create table dbo.wf_definition ( b_id varchar(50) not null primary key, -- 流程定义编码(创建后不可修改) b_name nvarchar(200) not null, -- 流程名称 b_i18n varchar(150) null, -- 多语言资源键(s_i18n.b_key) b_canuse tinyint not null default 1, -- 是否启用(0/1) b_bz nvarchar(2000) null, -- 备注 b_created_by varchar(50) null, -- 创建人(用户编码) b_created_at datetime2 null, -- 创建时间 b_updated_by varchar(50) null, -- 最后修改人 b_updated_at datetime2 null -- 最后修改时间 ); create index ix_wf_definition_canuse on dbo.wf_definition (b_canuse, b_id); ``` ### 16.2 流程版本 ```sql -- 流程版本表(draft/published/retired,已发布版本不可原地修改) create table dbo.wf_version ( b_id varchar(50) not null primary key, -- 版本 ID b_definition_id varchar(50) not null, -- 流程定义编码(wf_definition.b_id) b_version_no int not null, -- 版本序号(同一流程内递增) b_status varchar(20) not null default 'draft', -- 版本状态:draft(草稿)/published(已发布)/retired(已停用) b_schema_version int not null default 1, -- 配置结构版本(JSON 结构升级用) b_canvas_json nvarchar(max) null, -- 设计器布局(节点坐标、缩放),不参与执行 b_checksum varchar(128) null, -- 发布时计算的配置摘要 b_published_by varchar(50) null, -- 发布人(用户编码) b_published_at datetime2 null, -- 发布时间 b_bz nvarchar(2000) null, -- 备注 b_created_by varchar(50) null, -- 创建人 b_created_at datetime2 null, -- 创建时间 b_updated_by varchar(50) null, -- 最后修改人 b_updated_at datetime2 null -- 最后修改时间 ); create unique index ux_wf_version_no on dbo.wf_version (b_definition_id, b_version_no); create index ix_wf_version_status on dbo.wf_version (b_definition_id, b_status, b_version_no); ``` 发布校验由业务层完成:开始/结束节点完整、人工节点有审批人规则、条件字段来自 `s_field`、不存在不可达节点和死循环;发布后禁止修改节点和连线。 ### 16.3 流程绑定 ```sql -- 流程绑定表(模块 + 业务事件 + 条件 → 流程版本) create table dbo.wf_binding ( b_id varchar(50) not null primary key, -- 绑定 ID b_module_id varchar(50) not null, -- 模块编码(s_module.b_id,data/virtual 模块) b_event varchar(30) not null, -- 业务事件:submit(送审)/resubmit(重新提交)/change(变更) b_version_id varchar(50) not null, -- 已发布流程版本(wf_version.b_id) b_match_json nvarchar(max) null, -- 业务匹配条件 AST b_org_scope_json nvarchar(max) null, -- 公司、组织范围 b_status_field varchar(50) null, -- 业务状态字段(必须来自 s_field) b_status_map_json nvarchar(max) null, -- 流程状态到业务状态的映射 b_priority int not null default 0, -- 匹配优先级(小的先匹配) b_effective_from datetime2 null, -- 生效开始时间 b_effective_to datetime2 null, -- 生效结束时间 b_canuse tinyint not null default 1, -- 是否启用(0/1) b_bz nvarchar(2000) null, -- 备注 b_created_by varchar(50) null, -- 创建人 b_created_at datetime2 null, -- 创建时间 b_updated_by varchar(50) null, -- 最后修改人 b_updated_at datetime2 null -- 最后修改时间 ); create index ix_wf_binding_select on dbo.wf_binding (b_module_id, b_event, b_canuse, b_priority, b_effective_from, b_effective_to); ``` 同一业务上下文只能匹配一个最终流程,优先级冲突由业务层校验。`b_status_field` 用于兼容业务表已有的 `b_state`、`b_status` 字段名。 ### 16.4 流程节点 ```sql -- 流程节点表(版本内节点定义) create table dbo.wf_node ( b_id varchar(50) not null primary key, -- 节点 ID b_version_id varchar(50) not null, -- 流程版本(wf_version.b_id) b_node_key varchar(80) not null, -- 版本内稳定编码(历史记录引用它) b_name nvarchar(200) not null, -- 节点名称 b_i18n varchar(150) null, -- 多语言资源键 b_node_type varchar(30) not null, -- 节点类型:start(开始)/user_task(人工审批)/cc_task(抄送)/exclusive_gateway(条件分支)/parallel_gateway(并行分支)/service_task(服务节点)/end(结束) b_approval_mode varchar(30) null, -- 审批方式:single(单人)/any(或签)/all(会签)/serial(串行会签)/n_of_m(N 人通过)/percentage(按比例通过) b_reject_mode varchar(30) null, -- 驳回策略:terminate(驳回结束)/return_initiator(退回发起人)/return_previous(退回上一节点) b_due_hours decimal(10,2) null, -- 审批时限(小时) b_config_json nvarchar(max) null, -- 节点扩展配置(requiredCount、requiredRatio、formPolicy 等) b_xh int not null default 0, -- 显示顺序 b_canuse tinyint not null default 1 -- 是否启用(0/1) ); create unique index ux_wf_node_key on dbo.wf_node (b_version_id, b_node_key); create index ix_wf_node_type on dbo.wf_node (b_version_id, b_node_type, b_xh, b_node_key); ``` `b_approval_mode` 取值:`single` 单人、`any` 或签、`all` 会签、`serial` 串行会签、`n_of_m` N 人通过、`percentage` 按比例通过。`requiredCount` 只对 `n_of_m` 生效,`requiredRatio` 只对 `percentage` 生效。 ### 16.5 节点连线 ```sql -- 节点连线表(含条件分支) create table dbo.wf_edge ( b_id varchar(50) not null primary key, -- 连线 ID b_version_id varchar(50) not null, -- 流程版本 b_from_node_key varchar(80) not null, -- 来源节点编码 b_to_node_key varchar(80) not null, -- 目标节点编码 b_condition_json nvarchar(max) null, -- 连线条件 AST b_priority int not null default 0, -- 判断优先级(小的先判断) b_canuse tinyint not null default 1, -- 是否启用(0/1) b_bz nvarchar(1000) null -- 备注 ); create index ix_wf_edge_from on dbo.wf_edge (b_version_id, b_from_node_key, b_canuse, b_priority, b_to_node_key); create index ix_wf_edge_to on dbo.wf_edge (b_version_id, b_to_node_key, b_canuse, b_priority, b_from_node_key); ``` 同一来源节点的条件连线按 `b_priority` 从小到大判断,条件分支必须有默认出口,避免条件都不满足时流程卡死。 ### 16.6 审批人规则 ```sql -- 审批人规则表(节点内审批人、抄送人的解析规则) create table dbo.wf_actor_rule ( b_id varchar(50) not null primary key, -- 规则 ID b_version_id varchar(50) not null, -- 流程版本 b_node_key varchar(80) not null, -- 节点编码 b_actor_type varchar(30) not null, -- 解析类型:user(指定用户)/initiator(发起人)/initiator_manager(发起人直属领导)/dept_manager(部门负责人)/position(岗位)/org_role(组织角色)/field_user(业务字段用户)/custom(自定义解析器) b_actor_ref varchar(150) null, -- 用户、岗位、角色或字段编码 b_resolve_json nvarchar(max) null, -- 解析参数 b_condition_json nvarchar(max) null, -- 规则适用条件 AST b_fallback_type varchar(30) null, -- 解析为空时的兜底:none(不兜底)/admin(管理员)/initiator(发起人)/custom(自定义) b_fallback_ref varchar(150) null, -- 兜底引用 b_priority int not null default 0, -- 优先级(串行会签的顺序来源) b_canuse tinyint not null default 1, -- 是否启用(0/1) b_bz nvarchar(1000) null -- 备注 ); create index ix_wf_actor_rule_node on dbo.wf_actor_rule (b_version_id, b_node_key, b_canuse, b_priority, b_actor_type); ``` 解析结果必须把规则快照和实际用户快照写入运行表,组织架构后续变化不影响历史审批责任。 ### 16.7 流程实例 ```sql -- 流程实例表(某张业务单据某次提交的运行流程) create table dbo.wf_instance ( b_id uniqueidentifier not null primary key, -- 实例 ID(应用层生成 UUIDv7) b_definition_id varchar(50) not null, -- 流程定义编码 b_version_id varchar(50) not null, -- 流程版本(提交时固定) b_binding_id varchar(50) not null, -- 命中的绑定(wf_binding.b_id) b_module_id varchar(50) not null, -- 业务模块编码 b_event varchar(30) not null, -- 业务事件:submit(送审)/resubmit(重新提交)/change(变更) b_business_id varchar(50) not null, -- 业务主键(统一按字符串保存) b_business_no nvarchar(200) null, -- 业务单号(列表展示) b_org_id varchar(50) null, -- 公司/组织编码 b_initiator_id varchar(50) not null, -- 发起人(用户编码) b_initiator_dept_id varchar(50) null, -- 发起人部门编码 b_status varchar(30) not null, -- 实例状态:running(运行中)/approved(已通过)/rejected(已驳回)/returned(已退回)/withdrawn(已撤回)/cancelled(已取消)/expired(已过期)/error(异常待处理) b_context_json nvarchar(max) null, -- 运行变量 b_snapshot_json nvarchar(max) null, -- 提交时业务快照 b_current_summary_json nvarchar(max) null, -- 列表展示用当前节点摘要 b_row_version bigint not null default 0, -- 乐观锁版本号 b_idempotency_key varchar(100) null, -- 幂等键 b_started_at datetime2 not null, -- 开始时间 b_completed_at datetime2 null, -- 结束时间 b_created_by varchar(50) null, -- 创建人 b_created_at datetime2 null, -- 创建时间 b_updated_by varchar(50) null, -- 最后修改人 b_updated_at datetime2 null -- 最后修改时间 ); create index ix_wf_instance_business on dbo.wf_instance (b_module_id, b_business_id, b_event, b_status, b_id); create index ix_wf_instance_initiator on dbo.wf_instance (b_initiator_id, b_status, b_started_at, b_id); create index ix_wf_instance_version on dbo.wf_instance (b_version_id, b_started_at, b_id); ``` “驳回”和“退回”必须区分:驳回结束本次流程,退回允许修改后重新提交。 ### 16.8 节点运行 ```sql -- 节点运行表(节点在实例中的一次进入和处理记录) create table dbo.wf_node_run ( b_id uniqueidentifier not null primary key, -- 节点运行 ID(UUIDv7) b_instance_id uniqueidentifier not null, -- 流程实例(wf_instance.b_id) b_node_key varchar(80) not null, -- 节点编码 b_run_no int not null default 1, -- 同一节点的第几次运行(退回、循环用) b_status varchar(30) not null, -- 节点状态:pending(待激活)/running(运行中)/completed(已完成)/skipped(已跳过) b_required_count int null, -- 应签人数(按比例时在节点激活时换算) b_completed_count int not null default 0, -- 已通过人数 b_rejected_count int not null default 0, -- 已驳回人数 b_entered_at datetime2 null, -- 进入时间 b_exited_at datetime2 null, -- 离开时间 b_actor_snapshot_json nvarchar(max) null, -- 审批人规则快照 b_created_at datetime2 null -- 创建时间 ); create index ix_wf_node_run_instance on dbo.wf_node_run (b_instance_id, b_status, b_node_key, b_run_no, b_id); ``` `b_required_count`、`b_completed_count` 和 `b_rejected_count` 必须由后端在锁定节点运行记录后更新,不能由前端计算。 ### 16.9 待办任务 ```sql -- 待办任务表(一名实际审批人一行) create table dbo.wf_task ( b_id uniqueidentifier not null primary key, -- 任务 ID(UUIDv7) b_instance_id uniqueidentifier not null, -- 流程实例 b_node_run_id uniqueidentifier not null, -- 节点运行(会签、或签聚合判断用) b_node_key varchar(80) not null, -- 节点编码 b_actor_rule_id varchar(50) null, -- 命中的审批人规则 b_assignee_type varchar(30) not null, -- 受理人类型:user(指定用户)/position(岗位)/role(角色)/delegate(委托) b_assignee_id varchar(50) not null, -- 实际用户编码 b_seq int not null default 0, -- 同节点内处理顺序(串行会签生效) b_status varchar(30) not null, -- 任务状态:waiting(未激活)/pending(待处理)/claimed(已抢占)/approved(已通过)/rejected(已驳回)/cancelled(已取消)/expired(已过期) b_due_at datetime2 null, -- 应处理时间 b_claimed_by varchar(50) null, -- 抢占人 b_claimed_at datetime2 null, -- 抢占时间 b_completed_by varchar(50) null, -- 处理人 b_completed_at datetime2 null, -- 处理时间 b_rule_snapshot_json nvarchar(max) null, -- 审批人解析快照 b_created_at datetime2 null, -- 创建时间 b_updated_at datetime2 null -- 最后修改时间 ); create index ix_wf_task_assignee on dbo.wf_task (b_assignee_id, b_status, b_due_at, b_id); create index ix_wf_task_instance on dbo.wf_task (b_instance_id, b_status, b_node_run_id, b_id); create index ix_wf_task_due on dbo.wf_task (b_status, b_due_at, b_id); ``` 会签、或签和串行会签通过同一个 `b_node_run_id` 聚合判断;串行会签的未激活任务使用 `waiting` 状态,只有 `pending` 任务出现在待办列表。 ### 16.10 审批动作 ```sql -- 审批动作表(只追加,不更新历史动作) create table dbo.wf_action ( b_id uniqueidentifier not null primary key, -- 动作 ID(UUIDv7) b_instance_id uniqueidentifier not null, -- 流程实例 b_node_run_id uniqueidentifier null, -- 节点运行 b_task_id uniqueidentifier null, -- 待办任务 b_action varchar(30) not null, -- 动作编码:submit(送审)/approve(通过)/reject(驳回)/return(退回)/withdraw(撤回)/cancel(取消)/claim(抢占)/transfer(转交)/delegate(委托)/reassign(转派)/add_approver(加签)/urge(催办)/cc(抄送) b_operator_id varchar(50) null, -- 操作人(用户编码,系统动作为空) b_from_status varchar(30) null, -- 变更前状态 b_to_status varchar(30) null, -- 变更后状态 b_comment nvarchar(2000) null, -- 审批意见 b_payload_json nvarchar(max) null, -- 动作载荷(命中的条件、边、加签位置等) b_request_id varchar(64) null, -- 请求 ID b_trace_id varchar(64) null, -- 链路 ID b_occurdatetime datetime2 not null -- 发生时间 ); create index ix_wf_action_instance on dbo.wf_action (b_instance_id, b_occurdatetime, b_id); create index ix_wf_action_operator on dbo.wf_action (b_operator_id, b_occurdatetime, b_id); ``` 撤回、转交、加签和管理员转派也必须形成动作记录,历史动作只追加不修改。 ### 16.11 抄送记录 ```sql -- 抄送记录表(抄送人不产生待办,不参与节点判定) create table dbo.wf_cc ( b_id uniqueidentifier not null primary key, -- 抄送 ID(UUIDv7) b_instance_id uniqueidentifier not null, -- 流程实例 b_node_run_id uniqueidentifier null, -- 来源节点运行 b_node_key varchar(80) null, -- 来源节点编码 b_recipient_id varchar(50) not null, -- 抄送接收人(用户编码) b_actor_rule_id varchar(50) null, -- 命中的抄送规则 b_status varchar(20) not null default 'unread', -- 阅读状态:unread(未读)/read(已读) b_read_at datetime2 null, -- 阅读时间 b_rule_snapshot_json nvarchar(max) null, -- 抄送人解析快照 b_created_at datetime2 null -- 创建时间 ); create index ix_wf_cc_recipient on dbo.wf_cc (b_recipient_id, b_status, b_created_at, b_id); create index ix_wf_cc_instance on dbo.wf_cc (b_instance_id, b_created_at, b_id); ``` ### 16.12 委托 ```sql -- 委托表(审批人把待办委托给他人处理) create table dbo.wf_delegation ( b_id varchar(50) not null primary key, -- 委托 ID b_from_user_id varchar(50) not null, -- 委托人(用户编码) b_to_user_id varchar(50) not null, -- 受托人(用户编码) b_scope_json nvarchar(max) null, -- 委托范围(模块、动作、组织) b_start_at datetime2 not null, -- 生效时间 b_end_at datetime2 not null, -- 失效时间 b_reason nvarchar(500) null, -- 委托原因 b_canuse tinyint not null default 1, -- 是否启用(0/1) b_created_by varchar(50) null, -- 创建人 b_created_at datetime2 null, -- 创建时间 b_updated_by varchar(50) null, -- 最后修改人 b_updated_at datetime2 null -- 最后修改时间 ); create index ix_wf_delegation_user on dbo.wf_delegation (b_from_user_id, b_canuse, b_start_at, b_end_at, b_to_user_id); ``` ### 16.13 通知消息 ```sql -- 通知消息表(outbox,异步发送,不阻塞审批事务) create table dbo.wf_outbox ( b_id uniqueidentifier not null primary key, -- 消息 ID(UUIDv7) b_event_code varchar(80) not null, -- 事件编码 b_aggregate_type varchar(30) not null, -- 聚合类型:instance(流程实例)/task(待办任务) b_aggregate_id varchar(50) not null, -- 聚合 ID b_payload_json nvarchar(max) not null, -- 消息载荷 b_status varchar(20) not null default 'pending', -- 发送状态:pending(待发送)/sent(已发送)/failed(发送失败) b_retry_count int not null default 0, -- 重试次数 b_next_retry_at datetime2 null, -- 下次重试时间 b_last_error nvarchar(2000) null, -- 最后错误 b_occurdatetime datetime2 not null -- 发生时间 ); create index ix_wf_outbox_dispatch on dbo.wf_outbox (b_status, b_next_retry_at, b_occurdatetime, b_id); ``` 实例、任务、抄送记录和业务状态在同一数据库事务中提交;通知发送失败不回滚审批事务,超过阈值进入人工处理队列。