diff --git a/code/fms/CODEBUDDY.md b/code/fms/CODEBUDDY.md
index 94f078ed..b1855734 100644
--- a/code/fms/CODEBUDDY.md
+++ b/code/fms/CODEBUDDY.md
@@ -182,4 +182,4 @@ i18n:`module/` 下的管理界面文案固定中文、不接入业务多语言
后端测试在 `fms-api/src/test/java/...`,用 `@SpringBootTest` / 纯 JUnit 类,`mvnw test` 运行。
-改动后的验收(`开发规范.md`「变更验收」):先全局搜索确认无旧路径/旧导出/旧方法名残留;至少跑受影响模块的测试,涉及公共 store / 布局缓存 / 跨页面工具时补相关页面与状态测试;若被改动前已存在的缺失文件、旧测试或环境问题阻断,在结果中明确记录,不要为通过检查去改无关代码。
+改动后的验收(`开发规范.md`「变更验收」):开发阶段以功能可用为完成标准,**不运行测试、不跑构建检查**,测试由用户统一安排(等用户明确要求再一次性执行);但先全局搜索确认无旧路径/旧导出/旧方法名残留,被改动的类名、导出名、DOM 结构所波及的既有测试文件要同步更新到与新实现一致;若被改动前已存在的缺失文件、旧测试或环境问题阻断,在结果中明确记录,不要为通过检查去改无关代码。
diff --git a/code/fms/FMS删除策略重构设计.md b/code/fms/FMS删除策略重构设计.md
index 85c152ad..fe731a40 100644
--- a/code/fms/FMS删除策略重构设计.md
+++ b/code/fms/FMS删除策略重构设计.md
@@ -273,6 +273,23 @@ where b_id in (:ids)
这样列表删除和表单删行不会形成两套规则。
+依赖行删除由调用方在请求里声明,服务端不按表名猜:
+
+```json
+{
+ "table": "s_power",
+ "key_field": "b_object_type,b_object_id",
+ "deletes": [{ "b_object_type": "module", "b_object_id": "sea" }],
+ "dependent_deletes": [
+ { "table": "s_user_power", "key_field": "b_power_id", "source_field": "b_id" }
+ ]
+}
+```
+
+`dependent_deletes` 的每一项是 `{table, key_field, source_field}`:对本表本次要删的每一行,用 `source_field` 的取值去删 `table` 中 `key_field` 等于该值的行;取值由后端按本行的列值条件查出(与物理删除的 `WHERE` 同口径),调用方不需要先把键值查出来再拼删除行。依赖行先于父行删除,与 `cascade` 同一执行顺序。
+
+它解决“非模块表之间的引用清理”(例如权限点 `s_power` → 用户授权行 `s_user_power`,设计文档 §14.6):这类关系不登记在 `s_relation`(那是有模块归属的业务关系),也不能由服务端按表名反查——表名只出现在请求里,服务端不认识具体业务表。
+
### 6.3 文件和其他无模块删除
文件删除继续由 `/file/delete` 负责:按文件主键定位记录,先删除本地文件或 OSS 对象,再以 `target=direct`、`table=bf_files`、`key_field=subid` 删除数据库行。即使 `bf_files.mx_moduleid` 保存了业务模块编码,它也只是文件元数据,不是本次删除策略的模块目标;文件删除不需要 `moduleCode`,也不触发 `s_relation` 或 `s_delete_rule`。
diff --git a/code/fms/FMS工作流与审批设计.md b/code/fms/FMS工作流与审批设计.md
new file mode 100644
index 00000000..f14e7c1c
--- /dev/null
+++ b/code/fms/FMS工作流与审批设计.md
@@ -0,0 +1,1178 @@
+# FMS 工作流与审批设计 V1
+
+## 1. 文档定位
+
+本文是《FMS 新系统核心表结构设计》的工作流补充设计,重点解决业务单据的送审、审批、退回、驳回、撤回、转交、委托和审批追溯问题。
+
+本文不把审批当成某个页面的附属配置,也不把审批记录直接塞进业务表。工作流被设计成独立的系统能力:
+
+```text
+业务模块
+ └── 流程绑定
+ └── 已发布流程版本
+ ├── 节点与连线
+ ├── 审批人解析规则
+ └── 条件与表单策略
+
+业务单据提交
+ └── 流程实例
+ └── 节点运行
+ └── 待办任务
+ └── 审批动作 / 审计 / 通知
+```
+
+本文遵循以下已有设计:
+
+- 表名使用小写 `snake_case`,系统表使用 `s_` 前缀;
+- 关系型表保存稳定、需要查询和关联的对象;变化频繁的配置使用 `nvarchar(max)` JSON;
+- 不依赖数据库外键、触发器和 `CHECK` 约束表达业务规则;
+- 权限由 `s_power`、`s_user_power`、`s_user_field_power`、`s_user_data_power` 和业务层权限引擎共同完成;
+- 通用审计使用 `s_log_audit` 和 `s_log_audit_field`,工作流动作历史单独使用 `s_wf_action`;
+- 业务表主键可以是 bigint 雪花 ID,工作流运行对象使用字符串保存业务主键,避免依赖业务表物理主键类型。
+
+相关文档:
+
+- [FMS 新系统核心表结构设计](./FMS新系统核心表结构设计.md)
+- [FMS 删除策略重构设计](./FMS删除策略重构设计.md)
+
+## 2. 设计目标与边界
+
+### 2.1 设计目标
+
+工作流系统需要满足:
+
+1. 一个模块可以绑定多个流程,按照业务事件、组织和业务条件选择流程;
+2. 流程定义可编辑、校验、模拟、发布和停用;
+3. 已发布版本不可修改,运行中的流程固定使用提交时的版本;
+4. 支持串行审批、并行审批、会签、任一人通过、全部通过和 N 人通过;
+5. 支持指定用户、发起人直属领导、部门负责人、岗位、业务角色和业务字段用户;
+6. 支持条件分支,条件由结构化规则表达,不允许前端直接拼接 SQL;
+7. 审批权限同时受动作权限、当前待办任务和数据范围限制;
+8. 所有审批动作可追溯,能够还原当时的审批版本、审批人规则和实际审批人;
+9. 流程状态、业务状态、通知状态相互解耦,但关键状态变更必须在同一事务内完成;
+10. 支持高并发下的幂等、乐观锁、任务抢占和重复点击保护。
+
+### 2.2 设计边界
+
+工作流负责:
+
+- 流程路由;
+- 待办任务;
+- 审批人解析;
+- 审批动作;
+- 流程状态和业务状态投影;
+- 流程审计和通知事件。
+
+工作流不负责:
+
+- 替代业务表保存业务数据;
+- 替代通用权限系统;
+- 替代 HR 系统保存完整组织主数据;
+- 通过任意 SQL 直接修改业务表;
+- 用通用流程配置代替业务领域中的复杂计算和结算逻辑。
+
+## 3. 现有简单审批方案的问题
+
+旧方案以 `sh_set` 保存“模块、级别、用户、条件”,以 `sh_record` 保存某张单据的审核过程。当前配置页面是一个基于表格的 CRUD 页面,核心字段为 `b_level`、`b_user_id`、`b_condition` 和 `b_bz`。
+
+这种模型存在以下问题:
+
+| 现状 | 问题 |
+| --- | --- |
+| `b_level` 表示审批级别 | 只能表达固定的线性流程,不能表达分支、并行、回退和循环处理 |
+| `b_user_id` 保存审批人 | 不能表达部门负责人、岗位、角色、发起人领导等动态审批人 |
+| 一个级别对应一行用户 | 无法可靠表达会签、或签、N 人通过和候选人抢占 |
+| `b_condition` 是字符串条件 | 规则不可校验、不可视化、难以版本化,并存在 SQL 拼接风险 |
+| 多个用户使用逗号分隔 | 无法建立索引、统计待办、实现转交和委托 |
+| 通过最大 `b_id` 获取最新记录 | 依赖 ID 排序,面对并发、重试和回退时语义不稳定 |
+| 直接更新业务表 `b_state` | 流程实例、业务状态和审批历史互相耦合 |
+| 配置直接保存到当前表 | 没有草稿、发布、版本、回滚和生效时间 |
+| 前端直接调用通用保存接口 | 业务状态、权限、任务状态和审计无法由一个后端事务统一控制 |
+| 模块使用旧的 `b_SH`、`b_cate_id` 等字段 | 与新版 `s_module` 模型不一致,不能作为新系统的基础配置页 |
+
+因此不建议在 `sh_set` 上继续增加“审批人 2、审批人 3、条件 2、审核方式”等字段。应将 `sh_set` 和 `sh_record` 作为迁移来源,建立新的工作流配置和运行模型。
+
+## 4. 核心概念
+
+### 4.1 流程定义(Definition)
+
+流程的稳定身份,例如:
+
+```text
+expense_approval
+contract_approval
+customer_activation
+release_order_approval
+```
+
+流程定义本身不直接表示某个版本,也不直接表示某张业务单据。
+
+### 4.2 流程版本(Version)
+
+流程定义的一个可执行版本。版本状态建议为:
+
+```text
+draft 草稿,可编辑
+published 已发布,可被新实例使用
+retired 已停用,不再接受新实例
+```
+
+已发布版本不能原地编辑。修改流程时复制为新版本,重新校验并发布。已经运行的实例继续绑定原版本。
+
+### 4.3 流程绑定(Binding)
+
+把模块、业务事件、组织范围和流程版本关联起来。例如:
+
+```text
+模块:cw_expense
+事件:submit
+条件:金额大于 10000 且公司为 company_001
+流程:expense_approval v3
+```
+
+一个模块可以有多个绑定,但同一业务上下文只能选择一个最终流程。绑定的优先级、有效期和条件由后端校验,不能出现多个无法确定优先级的生效流程。
+
+### 4.4 节点与连线(Node / Edge)
+
+节点表达处理步骤,连线表达节点之间的路由和条件。
+
+建议第一阶段支持:
+
+```text
+start 开始
+user_task 人工审批
+exclusive_gateway 条件分支
+parallel_gateway 并行分支
+service_task 后端服务动作
+end 结束
+```
+
+`b_level` 不再作为流程结构。流程结构由节点编码和连线表达。
+
+### 4.5 流程实例(Instance)
+
+某张业务单据某次提交产生的一次运行流程。一个业务对象可以有多次历史实例,但同一个业务事件通常只能有一个活动实例。
+
+### 4.6 节点运行(Node Run)
+
+某个节点在某次实例中的一次进入和处理记录。节点可能因退回、循环或重试被多次运行,因此不能只在实例上保存一个当前级别字段。
+
+### 4.7 待办任务(Task)
+
+某次节点运行分配给某个实际用户的工作项。每一个实际审批人使用独立记录,不使用逗号分隔的用户编码。
+
+### 4.8 审批动作(Action)
+
+用户或系统对流程执行的不可变事件,例如:
+
+```text
+submit 送审
+approve 审批通过
+reject 驳回并结束
+return 退回修改
+withdraw 发起人撤回
+cancel 管理员或系统取消
+claim 抢占待办
+transfer 转交
+delegate 委托处理
+reassign 管理员转派
+```
+
+数据库保存稳定编码,页面文案通过多语言资源显示。
+
+## 5. 总体架构
+
+```text
+┌────────────────────────────────────────────────────────┐
+│ 流程设计与发布 │
+│ definition → version → node / edge → actor rule │
+└───────────────────────┬────────────────────────────────┘
+ │ 发布版本
+ ▼
+┌────────────────────────────────────────────────────────┐
+│ 流程绑定选择 │
+│ module + event + organization + condition → version │
+└───────────────────────┬────────────────────────────────┘
+ │ 业务提交
+ ▼
+┌────────────────────────────────────────────────────────┐
+│ 流程运行引擎 │
+│ instance → node_run → task → action │
+│ │ │ │
+│ ├── business status ├── audit log │
+│ └── notification outbox └── timeline │
+└────────────────────────────────────────────────────────┘
+```
+
+运行引擎必须在后端执行,前端只能提交意图:
+
+```text
+POST /workflow/instances/{id}/actions
+{ "action": "approve", "comment": "同意", "idempotencyKey": "..." }
+```
+
+前端不能直接修改 `s_wf_instance`、`s_wf_task` 或业务表状态。
+
+## 6. 配置模型
+
+### 6.1 流程定义
+
+```sql
+create table dbo.s_wf_definition (
+ b_id varchar(50) not null primary key, -- 稳定流程编码
+ b_name nvarchar(200) not null, -- 流程名称
+ b_i18n varchar(150) null, -- 多语言资源键
+ b_canuse tinyint not null default 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_s_wf_definition_canuse
+ on dbo.s_wf_definition (b_canuse, b_id);
+```
+
+`b_id` 创建后原则上不可修改。流程定义是逻辑身份,具体执行内容放在 `s_wf_version`。
+
+### 6.2 流程版本
+
+```sql
+create table dbo.s_wf_version (
+ b_id varchar(50) not null primary key, -- 版本 ID
+ b_definition_id varchar(50) not null, -- 流程定义编码
+ 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, -- 配置结构版本
+ 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_s_wf_version_no
+ on dbo.s_wf_version (b_definition_id, b_version_no);
+
+create index ix_s_wf_version_status
+ on dbo.s_wf_version (b_definition_id, b_status, b_version_no);
+```
+
+发布时必须完成:
+
+1. 校验开始节点、结束节点和连线完整性;
+2. 校验所有人工节点至少有一个有效审批人规则;
+3. 校验条件字段存在于对应模块的 `s_field`;
+4. 校验不存在不可达节点、死循环和无出口节点;
+5. 计算 `b_checksum`,发布后禁止修改节点和连线;
+6. 写入发布审计和版本变更日志。
+
+### 6.3 流程绑定
+
+```sql
+create table dbo.s_wf_binding (
+ b_id varchar(50) not null primary key, -- 绑定 ID
+ b_module_id varchar(50) not null, -- data / virtual 模块
+ b_event varchar(30) not null, -- submit / resubmit / change
+ b_version_id varchar(50) not null, -- 已发布流程版本
+ 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,
+ 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_s_wf_binding_select
+ on dbo.s_wf_binding (
+ b_module_id,
+ b_event,
+ b_canuse,
+ b_priority,
+ b_effective_from,
+ b_effective_to
+ );
+```
+
+`b_status_field` 用于兼容不同业务表已有的 `b_state`、`b_status` 等字段,但字段必须在模块元数据中登记。新建可审批业务表时,优先统一使用 `b_status`。
+
+### 6.4 流程节点
+
+```sql
+create table dbo.s_wf_node (
+ b_id varchar(50) not null primary key, -- 节点 ID
+ b_version_id varchar(50) not null,
+ 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/...
+ b_approval_mode varchar(30) null, -- single/any/all/n_of_m/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, -- 节点扩展配置
+ b_xh int not null default 0,
+ b_canuse tinyint not null default 1
+);
+
+create unique index ux_s_wf_node_key
+ on dbo.s_wf_node (b_version_id, b_node_key);
+
+create index ix_s_wf_node_type
+ on dbo.s_wf_node (b_version_id, b_node_type, b_xh, b_node_key);
+```
+
+同一版本内 `b_node_key` 必须稳定。设计器可以显示名称,但节点关联和历史记录使用 `b_node_key`。
+
+节点 `b_config_json` 可以保存以下低频配置:
+
+```json
+{
+ "schemaVersion": 1,
+ "allowInitiator": false,
+ "excludePreviousActors": true,
+ "allowClaim": false,
+ "commentRequired": true,
+ "requiredCount": 2,
+ "formPolicy": {
+ "readonly": ["b_amount"],
+ "required": ["b_reason"]
+ }
+}
+```
+
+`requiredCount` 只对 `n_of_m` 生效,不能用界面约定替代后端校验。
+
+### 6.5 节点连线
+
+```sql
+create table dbo.s_wf_edge (
+ b_id varchar(50) not null primary key,
+ 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,
+ b_bz nvarchar(1000) null
+);
+
+create index ix_s_wf_edge_from
+ on dbo.s_wf_edge (b_version_id, b_from_node_key, b_canuse, b_priority, b_to_node_key);
+
+create index ix_s_wf_edge_to
+ on dbo.s_wf_edge (b_version_id, b_to_node_key, b_canuse, b_priority, b_from_node_key);
+```
+
+同一个来源节点的条件连线按 `b_priority` 从小到大判断。发布校验要求条件分支具有明确的默认出口,避免所有条件不满足时流程卡死。
+
+### 6.6 审批人规则
+
+```sql
+create table dbo.s_wf_actor_rule (
+ b_id varchar(50) not null primary key,
+ b_version_id varchar(50) not null,
+ b_node_key varchar(80) not null,
+ b_actor_type varchar(30) not null, -- user/initiator/manager/position/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,
+ b_bz nvarchar(1000) null
+);
+
+create index ix_s_wf_actor_rule_node
+ on dbo.s_wf_actor_rule (b_version_id, b_node_key, b_canuse, b_priority, b_actor_type);
+```
+
+建议支持以下 `b_actor_type`:
+
+| 类型 | 含义 |
+| --- | --- |
+| `user` | 指定用户 |
+| `initiator` | 流程发起人 |
+| `initiator_manager` | 发起人直属领导 |
+| `dept_manager` | 发起人或业务归属部门负责人 |
+| `position` | 岗位或职位 |
+| `org_role` | 组织业务角色,不等同于功能权限角色 |
+| `field_user` | 业务单据某字段对应的用户,例如销售员、负责人 |
+| `custom` | 后端注册的审批人解析器 |
+
+解析规则执行后,必须把规则快照和实际用户快照写入运行表。组织架构后续变化不能修改已经产生的历史审批责任。
+
+## 7. 条件表达式
+
+### 7.1 结构化 AST
+
+业务条件、绑定条件和连线条件统一使用结构化 JSON。示例:
+
+```json
+{
+ "all": [
+ { "field": "b_amount", "operator": "gt", "value": 10000 },
+ { "field": "b_company_id", "operator": "eq", "value": "company_001" }
+ ]
+}
+```
+
+支持的基础逻辑:
+
+```text
+all AND
+any OR
+not NOT
+```
+
+支持的基础操作符:
+
+```text
+eq / ne / gt / ge / lt / le
+in / not_in
+like / is_null / is_not_null
+between
+```
+
+### 7.2 条件安全边界
+
+- `field` 必须来自模块 `s_field`,不能任意填写数据库列名;
+- 字段类型、操作符和值类型由后端校验;
+- 值可以引用流程上下文,例如 `{ "context": "initiator.deptId" }`;
+- SQL 由后端编译为参数化查询;
+- 普通管理员不能输入任意 SQL;
+- 复杂业务条件使用后端注册的 `custom resolver`;
+- 解析结果、命中的条件和选中的连线需要写入流程上下文或动作载荷,方便审计。
+
+系统的 SQL 扩展能力仍然保留,但工作流条件不能直接采用前端字符串拼接 SQL。
+
+## 8. 运行表结构
+
+### 8.1 流程实例
+
+```sql
+create table dbo.s_wf_instance (
+ b_id uniqueidentifier not null primary key, -- 应用层生成 UUIDv7
+ b_definition_id varchar(50) not null,
+ b_version_id varchar(50) not null,
+ b_binding_id varchar(50) not null,
+ b_module_id varchar(50) not null,
+ b_event varchar(30) not null,
+ 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/...
+ 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_s_wf_instance_business
+ on dbo.s_wf_instance (b_module_id, b_business_id, b_event, b_status, b_id);
+
+create index ix_s_wf_instance_initiator
+ on dbo.s_wf_instance (b_initiator_id, b_status, b_started_at, b_id);
+
+create index ix_s_wf_instance_version
+ on dbo.s_wf_instance (b_version_id, b_started_at, b_id);
+```
+
+流程实例状态建议使用稳定编码:
+
+```text
+running 运行中
+approved 已通过
+rejected 已驳回并结束
+returned 已退回修改
+withdrawn 已撤回
+cancelled 已取消
+expired 已过期
+error 异常待处理
+```
+
+“驳回”和“退回”必须区分:驳回通常结束本次流程,退回通常允许修改后重新提交。
+
+### 8.2 节点运行
+
+```sql
+create table dbo.s_wf_node_run (
+ b_id uniqueidentifier not null primary key,
+ b_instance_id uniqueidentifier not null,
+ 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_s_wf_node_run_instance
+ on dbo.s_wf_node_run (b_instance_id, b_status, b_node_key, b_run_no, b_id);
+```
+
+`b_run_no` 用于支持退回后重新进入同一节点、循环节点和重新提交。
+
+### 8.3 待办任务
+
+一名实际审批人一行任务。会签和或签通过同一个 `b_node_run_id` 进行聚合判断。
+
+```sql
+create table dbo.s_wf_task (
+ b_id uniqueidentifier not null primary key,
+ 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/代理等
+ b_assignee_id varchar(50) not null, -- 实际用户编码
+ b_status varchar(30) not null, -- pending/claimed/approved/...
+ 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_s_wf_task_assignee
+ on dbo.s_wf_task (b_assignee_id, b_status, b_due_at, b_id);
+
+create index ix_s_wf_task_instance
+ on dbo.s_wf_task (b_instance_id, b_status, b_node_run_id, b_id);
+
+create index ix_s_wf_task_due
+ on dbo.s_wf_task (b_status, b_due_at, b_id);
+```
+
+如果组织角色解析出大量候选人,仍然要在节点激活时形成可追溯的候选任务,不能只在页面实时计算“当前谁可以审批”。
+
+### 8.4 审批动作
+
+```sql
+create table dbo.s_wf_action (
+ b_id uniqueidentifier not null primary key,
+ b_instance_id uniqueidentifier not null,
+ b_node_run_id uniqueidentifier null,
+ b_task_id uniqueidentifier null,
+ b_action varchar(30) not null,
+ 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,
+ b_trace_id varchar(64) null,
+ b_occurdatetime datetime2 not null
+);
+
+create index ix_s_wf_action_instance
+ on dbo.s_wf_action (b_instance_id, b_occurdatetime, b_id);
+
+create index ix_s_wf_action_operator
+ on dbo.s_wf_action (b_operator_id, b_occurdatetime, b_id);
+```
+
+`s_wf_action` 只追加,不更新历史动作。撤回、转交和管理员转派也必须形成动作记录。
+
+### 8.5 委托和通知消息
+
+```sql
+create table dbo.s_wf_delegation (
+ b_id varchar(50) not null primary key,
+ 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,
+ b_created_by varchar(50) null,
+ b_created_at datetime2 null,
+ b_updated_by varchar(50) null,
+ b_updated_at datetime2 null
+);
+
+create table dbo.s_wf_outbox (
+ b_id uniqueidentifier not null primary key,
+ b_event_code varchar(80) not null,
+ b_aggregate_type varchar(30) not null, -- instance/task
+ b_aggregate_id varchar(50) not null,
+ 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_s_wf_outbox_dispatch
+ on dbo.s_wf_outbox (b_status, b_next_retry_at, b_occurdatetime, b_id);
+```
+
+通知、站内信、邮件和企业微信等外部动作通过 outbox 异步发送,不阻塞审批事务。关键的实例、任务和业务状态仍然在同一数据库事务中提交。
+
+## 9. 审批方式与路由语义
+
+### 9.1 单人审批
+
+节点解析出一个或多个候选人,但只有一个有效任务。管理员可以指定是否允许候选人抢占。
+
+### 9.2 任一人通过
+
+为所有候选人创建任务。任意一个人通过后,节点完成,其余待办自动变为 `cancelled`,并写入系统动作。
+
+### 9.3 全部通过
+
+所有有效任务都通过后节点完成。任一人驳回时,根据节点 `b_reject_mode` 结束或退回。
+
+### 9.4 N 人通过或按比例通过
+
+节点运行记录保存 `required_count`、`completed_count` 和 `rejected_count`。计数必须由后端在锁定节点运行记录后完成,不能由前端计算。
+
+### 9.5 条件分支
+
+条件分支通过 `s_wf_edge.b_condition_json` 表达。路由时记录:
+
+```text
+命中的边 ID
+条件上下文
+条件计算结果
+计算时间
+```
+
+这样在业务条件后来变化时,历史流程仍然可以解释为什么走了某条路径。
+
+## 10. 权限和安全模型
+
+### 10.1 三层校验
+
+一次审批动作必须同时满足:
+
+```text
+1. 动作权限:用户拥有 action.{module}.{action}
+2. 任务权限:当前存在分配给该用户的有效任务
+3. 数据权限:用户对该业务记录具有相应操作范围
+```
+
+仅拥有 `action.xxx.approve` 不代表可以审批所有单据;仅出现在待办中也不代表可以绕过模块动作权限。
+
+### 10.2 建议动作权限
+
+```text
+action.{module}.submit
+action.{module}.approve
+action.{module}.reject
+action.{module}.return
+action.{module}.withdraw
+action.{module}.transfer
+action.{module}.delegate
+action.{module}.reassign
+action.{module}.workflow_admin
+```
+
+已有系统使用 `audit` 的模块可以暂时做兼容映射,但新模块建议统一使用 `approve`。
+
+### 10.3 职责分离
+
+节点策略应支持:
+
+```text
+allowInitiator = false
+excludePreviousActors = true
+excludeCreator = true
+```
+
+这些规则必须由后端执行。前端只负责显示不可审批原因。
+
+### 10.4 数据范围
+
+继续复用 `s_user_data_power`:
+
+- `read` 控制用户能否查看业务记录;
+- `update` 控制业务修改;
+- `action.{module}.approve` 等精确动作控制业务动作范围;
+- 当前任务校验负责判断“是不是这一票的审批人”。
+
+数据范围条件、流程条件和审批人规则不能互相替代。
+
+## 11. 业务状态投影
+
+工作流实例是流程事实来源,业务表的状态字段是查询和业务模块使用的投影。
+
+推荐状态映射:
+
+```json
+{
+ "running": "pending_approval",
+ "approved": "approved",
+ "rejected": "rejected",
+ "returned": "draft",
+ "withdrawn": "draft",
+ "cancelled": "cancelled"
+}
+```
+
+业务表中的状态字段不能由普通 CRUD 保存接口随意修改。工作流相关状态变更必须通过流程服务完成,并且在同一事务中:
+
+```text
+锁定业务记录
+→ 校验当前状态和版本
+→ 校验用户权限与任务
+→ 写 s_wf_action
+→ 更新 s_wf_task / s_wf_node_run
+→ 创建下一节点任务或结束实例
+→ 更新业务表状态投影
+→ 写 s_log_audit / s_log_audit_field
+→ 写 s_wf_outbox
+→ 提交
+```
+
+如果业务表本身还有“结算、放单、作废”等业务状态,工作流状态不能覆盖这些业务生命周期状态。必要时使用独立的 `b_workflow_status` 或在模块配置中声明状态映射。
+
+## 12. 并发、幂等和异常处理
+
+### 12.1 幂等
+
+提交和审批接口都必须支持幂等键:
+
+```text
+request_id + operator_id + action
+```
+
+同一请求重复到达时返回第一次处理结果,不重复创建实例、任务和动作。
+
+### 12.2 乐观锁
+
+`s_wf_instance.b_row_version` 用于防止多个管理员或审批人同时推进同一个实例。更新时必须带上读取时的版本号。
+
+### 12.3 任务抢占
+
+对于允许抢占的候选任务:
+
+1. 后端锁定待办记录;
+2. 校验任务仍为 `pending`;
+3. 写入 `claimed_by` 和 `claimed_at`;
+4. 修改为 `claimed`;
+5. 其他用户再次操作时返回任务已被处理或已被占用。
+
+### 12.4 审批人为空
+
+审批人解析为空时不能自动跳过节点。应进入:
+
+```text
+error / waiting_admin_resolve
+```
+
+管理员可以补充审批人或转派,但必须形成管理员动作和审计记录。
+
+### 12.5 外部通知失败
+
+通知失败不回滚已经完成的审批事务。outbox 记录重试次数、下次重试时间和最后错误,超过阈值进入人工处理队列。
+
+## 13. 后端服务接口建议
+
+接口名称可根据现有 API 规范调整,但不能使用通用表保存接口直接承载流程动作。
+
+```text
+POST /api/workflows/definitions/{definitionId}/validate
+POST /api/workflows/versions/{versionId}/publish
+POST /api/workflows/instances/submit
+GET /api/workflows/tasks
+GET /api/workflows/instances/{instanceId}
+GET /api/workflows/instances/{instanceId}/timeline
+POST /api/workflows/tasks/{taskId}/actions
+POST /api/workflows/instances/{instanceId}/withdraw
+POST /api/workflows/tasks/{taskId}/transfer
+POST /api/workflows/tasks/{taskId}/delegate
+```
+
+审批动作请求示例:
+
+```json
+{
+ "action": "approve",
+ "comment": "费用和附件已核对",
+ "payload": {},
+ "idempotencyKey": "req-20260922-000001"
+}
+```
+
+后端返回中应包含:
+
+```text
+实例状态
+当前节点摘要
+下一步待办摘要
+业务状态投影
+动作结果和审计事件 ID
+```
+
+## 14. 前端配置和使用页面
+
+### 14.1 流程管理页
+
+替代原来的“模块树 + sh_set 表格”,展示:
+
+```text
+流程名称
+所属业务模块
+触发事件
+当前发布版本
+草稿版本
+状态
+适用组织
+生效时间
+最后发布人
+```
+
+操作包括:新建、复制版本、编辑草稿、校验、模拟、发布、停用、查看版本历史。
+
+### 14.2 流程设计器
+
+使用节点画布,而不是只编辑级别表格。节点属性面板至少包括:
+
+- 节点名称和节点类型;
+- 审批方式;
+- 审批人来源;
+- 条件和默认分支;
+- 驳回、退回和撤回策略;
+- 审批时限;
+- 表单字段只读、必填和可见策略;
+- 是否允许转交、委托、加签和抢占。
+
+### 14.3 发布前模拟
+
+管理员可以输入一组业务样例数据,查看:
+
+```text
+选择了哪个流程绑定
+命中了哪些条件
+每个节点解析出哪些审批人
+最终生成哪些待办
+是否触发申请人自审限制
+```
+
+### 14.4 流程待办和历史
+
+业务用户使用独立的:
+
+```text
+我的待办
+我发起的
+我已处理
+流程监控
+审批历史
+```
+
+业务详情页只显示当前流程状态、当前待办和时间线,不直接读取流程配置表判断按钮是否可用。
+
+## 15. 旧数据迁移
+
+### 15.1 配置迁移
+
+| 旧字段 | 新模型 |
+| --- | --- |
+| `sh_set.b_module_id` | `s_wf_binding.b_module_id` |
+| `sh_set.b_level` | 线性节点的顺序和节点编码 |
+| `sh_set.b_user_id` | `s_wf_actor_rule` 的 `user` 规则 |
+| `sh_set.b_condition` | 条件 AST;不能自动转换的进入人工复核 |
+| `sh_set.b_logic`、`b_leap` | 节点类型、连线条件或退回策略,不能机械照搬 |
+| `sh_set.b_bz` | 节点或审批人规则备注 |
+
+### 15.2 历史迁移
+
+| 旧字段 | 新模型 |
+| --- | --- |
+| `sh_record.b_dj_id` | `s_wf_instance.b_business_id` |
+| `sh_record.b_level` | `s_wf_node_run.b_node_key` / `b_run_no` |
+| `b_tosh_useid` | 多条 `s_wf_task` |
+| `b_link_useid` | 已处理用户快照或动作载荷 |
+| `b_dosh_useid` | `s_wf_action.b_operator_id` 历史动作 |
+| `b_sh_date` | `s_wf_action.b_occurdatetime` |
+| `b_sh_memo` | `s_wf_action.b_comment` |
+| 业务表 `b_state` | 保留为业务状态投影 |
+
+历史迁移不应篡改原始业务状态和原始日志。无法完整映射的旧数据可以以“历史兼容实例”导入,并在 `b_payload_json` 中保存原始字段。
+
+### 15.3 切换策略
+
+建议按模块逐步切换:
+
+1. 旧审批设置只读,停止新增配置;
+2. 将旧线性配置导入新流程草稿;
+3. 通过模拟和业务负责人确认审批路径;
+4. 发布新版本;
+5. 新提交使用新流程,历史实例继续由旧逻辑或兼容服务处理;
+6. 稳定后再迁移历史查询和审批中心;
+7. 最后下线旧 `sh_set`、`sh_record` 的写入入口。
+
+## 16. 分阶段落地建议
+
+### 第一阶段:企业级基础内核
+
+建议先实现:
+
+- 流程定义、版本、发布和停用;
+- 模块和业务事件绑定;
+- 开始、人工审批、条件分支、结束节点;
+- 串行、任一人、全部人;
+- 指定用户、发起人领导、部门负责人;
+- 提交、通过、驳回、退回、撤回;
+- 流程实例、节点运行、待办和动作历史;
+- 动作权限、任务权限和数据范围三层校验;
+- 业务状态投影、审计日志和 outbox 通知。
+
+### 第二阶段:组织和协同能力
+
+- 岗位和组织角色审批人;
+- 委托、转交、管理员转派;
+- N 人会签、百分比会签;
+- 审批期限、提醒和升级;
+- 流程模拟和审批人解析预览;
+- 流程监控和 SLA 报表。
+
+### 第三阶段:复杂业务编排
+
+- 服务节点和外部系统回调;
+- 自动补数、自动校验和自动归档;
+- 多公司、多租户和跨组织审批;
+- 日历工作时间和节假日 SLA;
+- 流程归档、分区和历史数据治理;
+- 失败补偿和人工恢复机制。
+
+## 17. 发布前检查清单
+
+### 流程配置
+
+- 是否存在且只有一个开始节点;
+- 是否至少存在一个结束节点;
+- 所有节点是否可达;
+- 所有人工节点是否存在有效审批人规则;
+- 所有条件字段是否存在于 `s_field`;
+- 是否存在没有默认出口的条件分支;
+- 是否存在重复生效的流程绑定;
+- 是否配置了驳回和退回策略;
+- 是否存在申请人自审风险;
+- 是否配置审批人为空时的处理方式。
+
+### 运行安全
+
+- 业务记录和流程实例是否在同一事务内更新;
+- 是否有幂等键和乐观锁;
+- 是否校验动作权限、任务权限和数据范围;
+- 是否禁止前端直接修改流程状态;
+- 是否记录审批人规则和实际审批人的快照;
+- 是否写入审计日志和 outbox;
+- 是否能处理重复点击、超时、转交和通知失败。
+
+## 18. 最终建议
+
+FMS 的审批设计应采用以下边界:
+
+```text
+权限:用户能不能做这种动作
+流程:当前轮到谁做什么
+业务:单据现在处于什么业务状态
+审计:过去发生过什么以及当时依据是什么
+```
+
+不要把四个问题压缩到一张 `sh_set` 表或一个业务状态字段中。新系统应以“版本化流程定义 + 运行实例 + 独立待办 + 不可变动作历史”为核心,业务模块只负责绑定流程并展示状态投影。这样才能从简单的多级审核扩展到大公司需要的组织审批、条件路由、会签、委托、审计和流程治理。
+
+## 19. 流程图编辑器选型
+
+### 19.1 选型结论
+
+FMS 流程设计器第一阶段选择 **LogicFlow**。
+
+LogicFlow 用于实现流程定义的可视化编辑,包括节点拖拽、连线、条件分支、缩放、框选、撤销重做和流程图导入导出。它只负责前端编辑体验,不负责审批执行、权限判断、流程状态推进和数据库事务。
+
+```text
+LogicFlow
+ └── 负责流程图编辑器
+
+FMS Workflow Service
+ └── 负责流程校验、发布、审批人解析、实例推进和事务
+
+s_wf_definition / s_wf_version / s_wf_node / s_wf_edge
+ └── 负责保存流程领域模型
+```
+
+### 19.2 备选方案比较
+
+| 方案 | 定位 | FMS 适配评价 |
+| --- | --- | --- |
+| Vue Flow | Vue 3 通用节点画布 | 上手简单,适合原型和简单流程,但流程扩展需要自行建设 |
+| LogicFlow | 业务逻辑流程图编辑器 | 与审批流、条件分支、业务流程图最匹配,作为当前首选 |
+| AntV X6 | 通用图形和图编辑引擎 | 扩展性和通用性最强,但需要自行实现较多流程语义和编辑器能力 |
+
+如果未来扩展为统一企业图形平台,涉及组织架构图、数据血缘、ER 图、网络拓扑或复杂端口连接,可以再评估 AntV X6。当前业务目标是审批流和业务流程图,不提前引入 X6 的底层复杂度。
+
+### 19.3 依赖和集成边界
+
+前端依赖建议使用:
+
+```text
+@logicflow/core
+@logicflow/extension
+```
+
+具体版本以项目锁定的 Vue 3 和 Vite 版本兼容性验证结果为准。LogicFlow 的扩展包用于节点、控制栏、小地图、菜单、选择框和流程相关扩展;流程业务规则仍由 FMS 自己实现。
+
+流程设计器建议拆成以下组件:
+
+```text
+WorkflowDesigner
+├── WorkflowToolbar
+├── WorkflowNodePanel
+├── WorkflowCanvas(LogicFlow)
+├── WorkflowPropertyPanel
+├── WorkflowValidationPanel
+└── WorkflowSimulationPanel
+```
+
+### 19.4 领域模型和画布模型隔离
+
+不能直接把 LogicFlow 的原始 JSON 当作流程定义存储。数据库和接口使用 FMS 自己的稳定模型:
+
+```json
+{
+ "schemaVersion": 1,
+ "nodes": [
+ {
+ "nodeKey": "department_manager",
+ "nodeType": "user_task",
+ "name": "部门负责人审批",
+ "config": {
+ "approvalMode": "single"
+ }
+ }
+ ],
+ "edges": [
+ {
+ "from": "start",
+ "to": "department_manager",
+ "condition": null
+ }
+ ]
+}
+```
+
+映射关系如下:
+
+| FMS 领域模型 | LogicFlow 画布模型 |
+| --- | --- |
+| `b_node_key` | `id` |
+| `b_node_type` | 自定义节点类型 |
+| `b_name` | 节点显示文本 |
+| `b_config_json` | 节点属性面板数据 |
+| `b_from_node_key` | `sourceNodeId` |
+| `b_to_node_key` | `targetNodeId` |
+| `b_condition_json` | 连线属性或条件编辑器数据 |
+| 节点位置和缩放 | `b_canvas_json` |
+
+建议建立独立的画布适配层:
+
+```text
+workflowDomainToLogicFlow(version)
+logicFlowToWorkflowDraft(graph)
+validateWorkflowDomain(version)
+```
+
+`s_wf_node` 和 `s_wf_edge` 是可执行流程的来源,`b_canvas_json` 只保存节点坐标、画布缩放、分组位置等界面布局信息。即使以后更换为 X6,也不能改变流程节点编码、审批规则和运行实例语义。
+
+### 19.5 LogicFlow 节点约定
+
+画布节点类型与工作流节点类型保持一一映射:
+
+```text
+fms-start
+fms-user-task
+fms-exclusive-gateway
+fms-parallel-gateway
+fms-service-task
+fms-end
+```
+
+LogicFlow 的节点类型只是前端渲染类型,后端落库仍使用:
+
+```text
+start
+user_task
+exclusive_gateway
+parallel_gateway
+service_task
+end
+```
+
+人工审批节点的审批人、审批方式、时限和表单策略在右侧属性面板编辑,保存到 `s_wf_node` 和 `s_wf_actor_rule`,不保存为节点显示文本。
+
+### 19.6 编辑器校验边界
+
+LogicFlow 可以做即时交互校验,例如:
+
+- 禁止连接到自身;
+- 禁止从结束节点继续连线;
+- 限制不同节点类型的连接方向;
+- 删除节点时提示是否同时删除相关连线;
+- 连线重复时提示用户。
+
+但以下校验必须由后端在保存和发布时再次执行:
+
+- 是否存在唯一开始节点;
+- 是否存在可达的结束节点;
+- 所有人工节点是否存在审批人规则;
+- 条件字段和操作符是否合法;
+- 是否存在死循环、不可达节点和无默认出口;
+- 流程绑定是否存在条件冲突;
+- 发布版本是否可以被当前组织范围使用。
+
+前端校验用于提升体验,后端校验才是最终规则。
+
+### 19.7 迁移和长期替换策略
+
+当前选择 LogicFlow 不意味着将流程数据绑定到 LogicFlow。以后如果需要改用 AntV X6,只替换:
+
+```text
+LogicFlowCanvasAdapter → X6CanvasAdapter
+```
+
+以下内容不应改变:
+
+- `s_wf_definition`;
+- `s_wf_version`;
+- `s_wf_node`;
+- `s_wf_edge`;
+- `s_wf_actor_rule`;
+- `s_wf_instance`、`s_wf_task`、`s_wf_action`;
+- 流程发布和运行接口;
+- 审批历史和审计记录。
+
+因此,LogicFlow 是当前阶段的流程设计器选型,而不是 FMS 工作流领域模型的依赖。
diff --git a/code/fms/FMS新系统核心表结构设计.md b/code/fms/FMS新系统核心表结构设计.md
index 5bc82a1e..a54bf445 100644
--- a/code/fms/FMS新系统核心表结构设计.md
+++ b/code/fms/FMS新系统核心表结构设计.md
@@ -40,6 +40,7 @@
- 启用和停用统一使用 `b_canuse`,取值为 `0/1`。
- 排序字段统一使用 `b_xh`,默认值为 `0`;需要稳定排序时增加业务主键作为次排序字段。
+- 最后修改时间统一使用 `b_updated_at`、修改人使用 `b_updated_by`(均为 `datetime2` / `varchar`,可空);不建触发器,由写入方维护。
- 名称、备注和多语言文本使用 `nvarchar`;编码、状态、类型和 SQL 标识类字段使用 `varchar`。
- 日期时间优先使用 `datetime2`;金额、数量等数值字段根据业务精度明确设置 `precision` 和 `scale`。
- 是否允许为空及默认值应根据业务语义明确设置,不能无理由全部允许为空或全部设置默认值。
@@ -191,12 +192,14 @@ create table dbo.s_module (
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_bz nvarchar(2000) null, -- 备注
+ b_updated_at datetime2 null -- 最后保存时间(本模块任意配置保存时刷新,含字段 / 界面配置等子表)
);
create index ix_s_module_parent
@@ -253,6 +256,7 @@ create index ix_s_module_save_table
- 数据之间的主子表、引用和多对多关系统一由 `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 查询来源优先级
@@ -991,7 +995,8 @@ b_condition_sql = 'b_salesman_id = {userId}'
### 14.6 与模块、菜单和关系的关系
- `s_power` 中的 `menu.*` 对应 `s_menu`,`module.*` 对应 `s_module`,`action.*` 对应业务动作;权限点编码与对象编码的对应关系由业务层维护,不建立外键;
-- `s_user_field_power.b_module_id`、`s_user_data_power.b_module_id` 指向 `s_module.b_id`,字段还需在 `s_field` 中存在;
+- 授权行只能引用存在的权限点:删 `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 生成时统一追加范围条件。
diff --git a/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/service/DataDeleteService.java b/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/service/DataDeleteService.java
index 50a9452e..b10a9d11 100644
--- a/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/service/DataDeleteService.java
+++ b/code/fms/fms-api/src/main/java/cn/g3soft/fmsapi/service/DataDeleteService.java
@@ -30,6 +30,9 @@ import java.util.Set;
* 不读模块关系、不跑模块 SQL 规则。普通模块列表不允许传表名。
*
*
+ *
另外,{@code saveobjt} 的删除行可以带 {@code dependent_deletes} 声明,把引用这些行的
+ * 依赖行一并删除(见 {@link #deleteDependents}):依赖关系由调用方声明,服务端不按表名猜。
+ *
*
计划、检查、删除在同一事务中完成(设计 §5.3):任一步骤失败整批回滚。
*/
@Service
@@ -178,6 +181,110 @@ public class DataDeleteService {
return deleted;
}
+ /**
+ * 依赖行删除:本表被删的行被别的表引用时,连引用行一起删(《FMS删除策略重构设计》§6.2)。
+ *
+ *
例如删除权限点({@code s_power})要连引用它的用户授权行({@code s_user_power})
+ * 一起删:授权行只记录“拥有”,没有外键约束,权限点没了、授权行留着就是幽灵授权 ——
+ * 权限判定会读到它,授权界面也无从解释。这类关系不登记在 {@code s_relation} 里
+ * (那是有模块归属的业务关系),也不由服务端按表名反查:调用方在请求里声明依赖表、
+ * 键列和取值列,这里按声明执行。表名只出现在请求里,服务端不认识具体业务表。
+ *
+ *
声明格式 {@code {table, key_field, source_field}}:对本表本次要删的每一行,
+ * 用取值列的值得出要删的键值,再删依赖表里键列等于该值的行。取值用本表这一行的列值
+ * 条件查出(与物理删除的 WHERE 同口径),所以调用方不需要先查一遍再拼删除行。
+ *
+ * @param parentTable 本表(正在删除的表)的元数据
+ * @param parentRows 本次要删除的行,列值即删除条件
+ * @param dependents 依赖声明列表
+ * @return 实际删除的依赖行数
+ */
+ public int deleteDependents(
+ Connection connection,
+ DbUtils.TableMetadata parentTable,
+ List