Files
workspace/code/fms/FMS工作流与审批设计.md
2026-09-23 16:54:54 +08:00

1299 lines
49 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# FMS 工作流与审批设计 V1
## 1. 文档定位
本文是《FMS 新系统核心表结构设计》的工作流补充设计,重点解决业务单据的送审、审批、退回、驳回、撤回、转交、委托和审批追溯问题。
本文不把审批当成某个页面的附属配置,也不把审批记录直接塞进业务表。工作流被设计成独立的系统能力:
```text
业务模块
└── 流程绑定
└── 已发布流程版本
├── 节点与连线
├── 审批人解析规则
└── 条件与表单策略
业务单据提交
└── 流程实例
└── 节点运行
└── 待办任务
└── 审批动作 / 审计 / 通知
```
本文遵循以下已有设计:
- 表名使用小写 `snake_case`;工作流是独立子系统,不属于系统内核,表统一使用 `wf_` 前缀,不使用 `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`,工作流动作历史单独使用 `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 人工审批
cc_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 管理员转派
add_approver 加签
urge 催办
cc 抄送(系统动作)
```
数据库保存稳定编码,页面文案通过多语言资源显示。
### 4.9 抄送(Cc)
抄送把流程的进展或结果告知不参与审批的人。抄送不产生待办,不参与节点判定,只产生可查询的抄送记录和 outbox 通知。抄送记录使用独立的 `wf_cc`,不写入 `wf_task`。
## 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": "..." }
```
前端不能直接修改 `wf_instance`、`wf_task`、`wf_cc` 或业务表状态。
## 6. 配置模型
### 6.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, -- 多语言资源键
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_wf_definition_canuse
on dbo.wf_definition (b_canuse, b_id);
```
`b_id` 创建后原则上不可修改。流程定义是逻辑身份,具体执行内容放在 `wf_version`。
### 6.2 流程版本
```sql
create table dbo.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_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);
```
发布时必须完成:
1. 校验开始节点、结束节点和连线完整性;
2. 校验所有人工节点至少有一个有效审批人规则;
3. 校验条件字段存在于对应模块的 `s_field`;
4. 校验不存在不可达节点、死循环和无出口节点;
5. 计算 `b_checksum`,发布后禁止修改节点和连线;
6. 写入发布审计和版本变更日志。
### 6.3 流程绑定
```sql
create table dbo.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_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` 等字段,但字段必须在模块元数据中登记。新建可审批业务表时,优先统一使用 `b_status`。
### 6.4 流程节点
```sql
create table dbo.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(人工审批)/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, -- 节点扩展配置
b_xh int not null default 0,
b_canuse tinyint not null default 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_node_key` 必须稳定。设计器可以显示名称,但节点关联和历史记录使用 `b_node_key`。
节点 `b_config_json` 可以保存以下低频配置:
```json
{
"schemaVersion": 1,
"allowInitiator": false,
"excludePreviousActors": true,
"allowClaim": false,
"commentRequired": true,
"requiredCount": 2,
"requiredRatio": 0.6,
"formPolicy": {
"readonly": ["b_amount"],
"required": ["b_reason"]
}
}
```
`requiredCount` 只对 `n_of_m` 生效,`requiredRatio` 只对 `percentage` 生效,都不能用界面约定替代后端校验。
### 6.5 节点连线
```sql
create table dbo.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_name nvarchar(200) 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_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` 从小到大判断。发布校验要求条件分支具有明确的默认出口,避免所有条件不满足时流程卡死。
### 6.6 审批人规则
```sql
create table dbo.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(发起人)/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,
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);
```
建议支持以下 `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.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, -- 业务事件: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);
```
流程实例状态建议使用稳定编码:
```text
running 运行中
approved 已通过
rejected 已驳回并结束
returned 已退回修改
withdrawn 已撤回
cancelled 已取消
expired 已过期
error 异常待处理
```
“驳回”和“退回”必须区分:驳回通常结束本次流程,退回通常允许修改后重新提交。
### 8.2 节点运行
```sql
create table dbo.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_wf_node_run_instance
on dbo.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` 进行聚合判断;串行会签的未激活任务使用 `waiting` 状态,按 `b_seq` 顺序逐个激活。
```sql
create table dbo.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(角色)/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);
```
如果组织角色解析出大量候选人,仍然要在节点激活时形成可追溯的候选任务,不能只在页面实时计算“当前谁可以审批”。
### 8.4 审批动作
```sql
create table dbo.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, -- 动作编码: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,
b_trace_id varchar(64) null,
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);
```
`wf_action` 只追加,不更新历史动作。撤回、转交和管理员转派也必须形成动作记录。
### 8.5 委托、抄送和通知消息
```sql
create table dbo.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 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);
create table dbo.wf_cc (
b_id uniqueidentifier not null primary key,
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);
create table dbo.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_wf_outbox_dispatch
on dbo.wf_outbox (b_status, b_next_retry_at, b_occurdatetime, b_id);
```
通知、站内信、邮件和企业微信等外部动作通过 outbox 异步发送,不阻塞审批事务。关键的实例、任务、抄送记录和业务状态仍然在同一数据库事务中提交。
## 9. 审批方式与路由语义
### 9.1 单人审批
节点解析出一个或多个候选人,但只有一个有效任务。管理员可以指定是否允许候选人抢占。
### 9.2 任一人通过
为所有候选人创建任务。任意一个人通过后,节点完成,其余待办自动变为 `cancelled`,并写入系统动作。或签的驳回同样即时生效:第一位处理人驳回时立即按 `b_reject_mode` 处理(见 9.7),其余待办自动取消。
### 9.3 全部通过
所有有效任务都通过后节点完成。任一人驳回时,根据节点 `b_reject_mode` 结束或退回。
### 9.4 N 人通过或按比例通过
节点运行记录保存 `required_count`、`completed_count` 和 `rejected_count`。计数必须由后端在锁定节点运行记录后完成,不能由前端计算。按比例通过时,节点激活必须把 `requiredRatio` 换算为 `required_count` 并写入快照,避免审批人数变化后票数漂移。
### 9.5 条件分支
条件分支通过 `wf_edge.b_condition_json` 表达。路由时记录:
```text
命中的边 ID
条件上下文
条件计算结果
计算时间
```
这样在业务条件后来变化时,历史流程仍然可以解释为什么走了某条路径。
### 9.6 串行会签
`serial` 模式按 `wf_task.b_seq` 顺序逐个激活审批人:
- 审批顺序由 `wf_actor_rule.b_priority` 和规则解析顺序决定,生成任务时写入 `b_seq`;
- 同一时刻只有一名审批人的任务为 `pending`,其余为 `waiting`;
- 当前审批人通过后激活下一名,全部通过后节点完成;
- 任一环节驳回按 `b_reject_mode` 处理(见 9.7),未激活任务置为 `cancelled`;
- 只有 `pending` 任务出现在待办列表,`waiting` 任务靠任务快照解释“即将轮到我”。
未激活任务的顺序允许管理员调整,但必须写入动作记录。
### 9.7 驳回和否决语义
驳回是即时生效的:任何审批方式下,节点内出现第一个驳回时立即按 `b_reject_mode` 处理,其余未完成任务自动置为 `cancelled` 并写入系统动作。
```text
terminate 实例进入 rejected,本次流程结束,业务状态投影为已驳回(即“一票否决”)
return_initiator 实例进入 returned,退回发起人修改后重新提交
return_previous 回到上一节点重新审批
```
- 会签、N 人通过和按比例通过节点默认使用 `terminate`,即任一驳回即一票否决;
- 驳回必须记录操作人、命中任务和当时的节点计数;
- “驳回不影响其他人”不是驳回语义;需要停止本次审批时应由发起人撤回。
### 9.8 加签
加签在当前节点运行中追加审批人:
- 加签人必须拥有 `action.{module}.add_approver`,且为当前节点待办人或流程管理员;
- 加签任务挂在同一个 `b_node_run_id` 下,按节点 `b_approval_mode` 参与判定;
- `n_of_m` 和 `percentage` 的 `required_count`、`requiredRatio` 不因加签自动改变,需要调整时必须在动作载荷中显式声明;
- 串行会签的加签任务按 `b_seq` 插入到指定位置,其余任务顺序顺延;
- 加签必须写入 `wf_action`,载荷保存加签理由、加签人和插入位置。
### 9.9 抄送
抄送节点激活后立即解析抄送人、写入 `wf_cc`、投递 outbox 通知并自动完成,不产生待办,也不参与节点计数。
- 抄送规则复用 `wf_actor_rule`,支持与审批人相同的解析类型;
- 抄送人不写入 `wf_task`,不进入审批人的待办列表;
- 抄送记录独立于审批动作历史,支持“抄送我的”查询和已读标记。
## 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}.add_approver
action.{module}.urge
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
锁定业务记录
→ 校验当前状态和版本
→ 校验用户权限与任务
→ 写 wf_action
→ 更新 wf_task / wf_node_run
→ 创建下一节点任务或结束实例
→ 更新业务表状态投影
→ 写 s_log_audit / s_log_audit_field
→ 写 wf_outbox
→ 提交
```
如果业务表本身还有“结算、放单、作废”等业务状态,工作流状态不能覆盖这些业务生命周期状态。必要时使用独立的 `b_workflow_status` 或在模块配置中声明状态映射。
## 12. 并发、幂等和异常处理
### 12.1 幂等
提交和审批接口都必须支持幂等键:
```text
request_id + operator_id + action
```
同一请求重复到达时返回第一次处理结果,不重复创建实例、任务和动作。
### 12.2 乐观锁
`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
POST /api/workflows/tasks/{taskId}/add-approver
POST /api/workflows/instances/{instanceId}/urge
GET /api/workflows/cc
```
审批动作请求示例:
```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
我的待办 审批中心菜单,读 wf_task(当前用户的 pending/claimed 任务)
我发起的 审批中心菜单,读 wf_instance(b_initiator_id = 当前用户)
我已处理 审批中心菜单,读 wf_task(当前用户已完成的任务)
抄送我的 审批中心菜单,读 wf_cc
流程监控 不开独立菜单,作为「流程管理」页内的页签,供管理员查看
审批历史 不开独立菜单,作为业务单据详情页的时间线
```
菜单归属:
- 「我的待办 / 我发起的 / 我已处理 / 抄送我的」是**审批中心**(一级菜单)下的四个页面,面向全员,用 `menu.approval*` 授权;
- 「流程监控」并入「系统管理 → 流程管理」页,与流程配置、委派管理同页,共用 `menu.workflow` 授权。配置和监控同属管理员场景,不再单独开菜单;仅当监控受众与配置受众分离(例如业务主管要看本部门积压)时,才拆到审批中心下并单独授权;
- 「审批历史」是单据详情页里的时间线(读 `wf_action`),脱离单据没有使用场景,不做独立入口。
业务详情页只显示当前流程状态、当前待办和时间线,不直接读取流程配置表判断按钮是否可用。
## 15. 旧数据迁移
### 15.1 配置迁移
| 旧字段 | 新模型 |
| --- | --- |
| `sh_set.b_module_id` | `wf_binding.b_module_id` |
| `sh_set.b_level` | 线性节点的顺序和节点编码 |
| `sh_set.b_user_id` | `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` | `wf_instance.b_business_id` |
| `sh_record.b_level` | `wf_node_run.b_node_key` / `b_run_no` |
| `b_tosh_useid` | 多条 `wf_task` |
| `b_link_useid` | 已处理用户快照或动作载荷 |
| `b_dosh_useid` | `wf_action.b_operator_id` 历史动作 |
| `b_sh_date` | `wf_action.b_occurdatetime` |
| `b_sh_memo` | `wf_action.b_comment` |
| 业务表 `b_state` | 保留为业务状态投影 |
历史迁移不应篡改原始业务状态和原始日志。无法完整映射的旧数据可以以“历史兼容实例”导入,并在 `b_payload_json` 中保存原始字段。
### 15.3 切换策略
建议按模块逐步切换:
1. 旧审批设置只读,停止新增配置;
2. 将旧线性配置导入新流程草稿;
3. 通过模拟和业务负责人确认审批路径;
4. 发布新版本;
5. 新提交使用新流程,历史实例继续由旧逻辑或兼容服务处理;
6. 稳定后再迁移历史查询和审批中心;
7. 最后下线旧 `sh_set`、`sh_record` 的写入入口。
## 16. 分阶段落地建议
涉及枚举的能力——审批方式(`b_approval_mode`)、节点类型(`b_node_type`)和动作编码——在第一阶段一次做完。枚举值在运行后再扩充,会导致历史数据、界面文案和后端分支各自维护,代价高于一次实现。
### 第一阶段:企业级基础内核
建议先实现:
- 流程定义、版本、发布和停用;
- 模块和业务事件绑定;
- 开始、人工审批、抄送、条件分支、结束节点;
- 单人、或签、会签、串行会签、N 人通过、按比例通过;
- 指定用户、发起人领导、部门负责人;
- 提交、通过、驳回、退回、撤回、加签、催办;
- 流程实例、节点运行、待办、抄送和动作历史;
- 动作权限、任务权限和数据范围三层校验;
- 业务状态投影、审计日志和 outbox 通知。
### 第二阶段:组织和协同能力
- 岗位和组织角色审批人;
- 委托、转交、管理员转派;
- 审批期限、提醒和升级;
- 流程模拟和审批人解析预览;
- 流程监控和 SLA 报表。
### 第三阶段:复杂业务编排
- 服务节点和外部系统回调;
- 自动补数、自动校验和自动归档;
- 多公司、多租户和跨组织审批;
- 日历工作时间和节假日 SLA;
- 流程归档、分区和历史数据治理;
- 失败补偿和人工恢复机制。
## 17. 发布前检查清单
### 流程配置
- 是否存在且只有一个开始节点;
- 是否至少存在一个结束节点;
- 所有节点是否可达;
- 所有人工节点是否存在有效审批人规则;
- 所有抄送节点是否存在有效抄送人规则;
- 串行会签节点的审批人顺序来源是否明确;
- 所有条件字段是否存在于 `s_field`;
- 是否存在没有默认出口的条件分支;
- 是否存在重复生效的流程绑定;
- 是否配置了驳回和退回策略;
- 是否存在申请人自审风险;
- 是否配置审批人为空时的处理方式。
### 运行安全
- 业务记录和流程实例是否在同一事务内更新;
- 是否有幂等键和乐观锁;
- 是否校验动作权限、任务权限和数据范围;
- 是否禁止前端直接修改流程状态;
- 是否记录审批人规则和实际审批人的快照;
- 是否写入审计日志和 outbox;
- 是否能处理重复点击、超时、转交和通知失败。
## 18. 最终建议
FMS 的审批设计应采用以下边界:
```text
权限:用户能不能做这种动作
流程:当前轮到谁做什么
业务:单据现在处于什么业务状态
审计:过去发生过什么以及当时依据是什么
```
不要把四个问题压缩到一张 `sh_set` 表或一个业务状态字段中。新系统应以“版本化流程定义 + 运行实例 + 独立待办 + 不可变动作历史”为核心,业务模块只负责绑定流程并展示状态投影。这样才能从简单的多级审核扩展到大公司需要的组织审批、条件路由、会签、委托、审计和流程治理。
## 19. 流程图编辑器选型
### 19.1 选型结论
FMS 流程设计器第一阶段选择 **LogicFlow**。
LogicFlow 用于实现流程定义的可视化编辑,包括节点拖拽、连线、条件分支、缩放、框选、撤销重做和流程图导入导出。它只负责前端编辑体验,不负责审批执行、权限判断、流程状态推进和数据库事务。
```text
LogicFlow
└── 负责流程图编辑器
FMS Workflow Service
└── 负责流程校验、发布、审批人解析、实例推进和事务
wf_definition / wf_version / wf_node / 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)
```
`wf_node` 和 `wf_edge` 是可执行流程的来源,`b_canvas_json` 只保存节点坐标、画布缩放、分组位置等界面布局信息。即使以后更换为 X6,也不能改变流程节点编码、审批规则和运行实例语义。
### 19.5 LogicFlow 节点约定
画布节点类型与工作流节点类型保持一一映射:
```text
fms-start
fms-user-task
fms-cc-task
fms-exclusive-gateway
fms-parallel-gateway
fms-service-task
fms-end
```
LogicFlow 的节点类型只是前端渲染类型,后端落库仍使用:
```text
start
user_task
cc_task
exclusive_gateway
parallel_gateway
service_task
end
```
人工审批节点的审批人、审批方式、时限和表单策略在右侧属性面板编辑,保存到 `wf_node` 和 `wf_actor_rule`,不保存为节点显示文本。
### 19.6 编辑器校验边界
LogicFlow 可以做即时交互校验,例如:
- 禁止连接到自身;
- 禁止从结束节点继续连线;
- 限制不同节点类型的连接方向;
- 删除节点时提示是否同时删除相关连线;
- 连线重复时提示用户。
但以下校验必须由后端在保存和发布时再次执行:
- 是否存在唯一开始节点;
- 是否存在可达的结束节点;
- 所有人工节点是否存在审批人规则;
- 条件字段和操作符是否合法;
- 是否存在死循环、不可达节点和无默认出口;
- 流程绑定是否存在条件冲突;
- 发布版本是否可以被当前组织范围使用。
前端校验用于提升体验,后端校验才是最终规则。
### 19.7 迁移和长期替换策略
当前选择 LogicFlow 不意味着将流程数据绑定到 LogicFlow。以后如果需要改用 AntV X6,只替换:
```text
LogicFlowCanvasAdapter → X6CanvasAdapter
```
以下内容不应改变:
- `wf_definition`;
- `wf_version`;
- `wf_node`;
- `wf_edge`;
- `wf_actor_rule`;
- `wf_instance`、`wf_task`、`wf_action`;
- 流程发布和运行接口;
- 审批历史和审计记录。
因此,LogicFlow 是当前阶段的流程设计器选型,而不是 FMS 工作流领域模型的依赖。
## 20. 签核能力清单
本节把常见签核能力映射到本设计的配置、动作和表,作为能力边界声明。
| 能力 | 支持情况 | 实现方式 |
| --- | --- | --- |
| 会签(全部通过) | 支持 | `b_approval_mode = 'all'`,同节点多任务聚合,见 9.3 |
| 或签(任一人通过) | 支持 | `'any'`,第一票决定,其余任务自动取消,见 9.2 |
| 穿行会签(串行会签) | 支持 | `'serial'`,按 `wf_task.b_seq` 逐个激活,见 9.6 |
| N 人通过 | 支持 | `'n_of_m'`,`requiredCount` 由后端锁定计算,见 9.4 |
| 比例签 | 支持 | `'percentage'`,`requiredRatio` 在节点激活时换算为 `required_count`,见 9.4 |
| 一票否决 | 支持 | 驳回即时生效,`b_reject_mode = 'terminate'`,见 9.7 |
| 转办/转交 | 支持 | 动作 `transfer`,任务级转交并强制留痕,见 8.4 |
| 委托 | 支持 | `wf_delegation` + 动作 `delegate`,支持时间范围和范围过滤,见 8.5 |
| 加签 | 支持 | 动作 `add_approver`,向当前节点运行追加任务,见 9.8 |
| 催办 | 支持 | 动作 `urge`,通过 outbox 发送提醒,不改变任务状态 |
| 抄送 | 支持 | `cc_task` 节点 + `wf_cc` 记录 + outbox 通知,见 9.9 |
| 跳转(指定任意节点) | 不支持 | 见下方说明 |
跳转不是遗漏,而是有意排除:流程推进只允许沿已发布版本的连线进行。允许运行中跳到任意节点,会绕过节点校验、审批人解析和动作权限,并让历史流程无法解释为什么走了这条路径。需要类似效果时使用:
- 发起人撤回后重新提交,产生新实例和新快照;
- 管理员使用任务级 `reassign` 转派,并写入管理员动作;
- 业务上确需提前结束时使用 `cancel`,由系统记录取消原因。