diff --git a/db/0pigxx_boot.sql b/db/0pigxx_boot.sql index 48e8bc173..f97e2bcca 100644 --- a/db/0pigxx_boot.sql +++ b/db/0pigxx_boot.sql @@ -1701,11 +1701,13 @@ CREATE TABLE `process_node_data` ( `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '0' COMMENT '删除标志', `create_time` datetime COMMENT '创建时间', `update_time` datetime COMMENT '更新时间', + `process_definition_id` varchar(64) DEFAULT NULL COMMENT '引擎流程定义ID,发布版本快照', `flow_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '流程id', `data` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '表单数据', `node_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, `tenant_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '所属租户id', PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `uk_node_definition` (`tenant_id`,`process_definition_id`,`node_id`), KEY `data_idx_id` (`id`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=1195 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='流程节点数据'; diff --git a/db/3pigxx_flow.sql b/db/3pigxx_flow.sql index 8c5648ee8..bc6fecafe 100644 --- a/db/3pigxx_flow.sql +++ b/db/3pigxx_flow.sql @@ -138,11 +138,13 @@ CREATE TABLE `process_node_data` ( `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '0' COMMENT '删除标志', `create_time` datetime COMMENT '创建时间', `update_time` datetime COMMENT '更新时间', + `process_definition_id` varchar(64) DEFAULT NULL COMMENT '引擎流程定义ID,发布版本快照', `flow_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '流程id', `data` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '表单数据', `node_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, `tenant_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '所属租户id', PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `uk_node_definition` (`tenant_id`,`process_definition_id`,`node_id`), KEY `data_idx_id` (`id`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=1195 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='流程节点数据'; diff --git a/db/migration/20261013_flow_node_version.md b/db/migration/20261013_flow_node_version.md new file mode 100644 index 000000000..d4c5f2670 --- /dev/null +++ b/db/migration/20261013_flow_node_version.md @@ -0,0 +1,69 @@ +# 节点配置版本隔离升级 + +本次只隔离节点配置。业务 flowId 不变;流程图、表单定义的完整历史快照不在本次范围。任务、抄送和实例详情使用对应版本的字段权限,当前表单中未被该快照声明的字段默认隐藏;这不替代服务端的提交字段鉴权。已结束实例的详情依赖 Flowable 历史数据来确定版本,历史已清理时会明确报错,不能回退最新版。 + +## 上线顺序 + +1. 暂停流程发布、发起、任务处理和异步执行器,备份流程业务表及 Flowable 数据。不得新旧版本混跑:旧代码会按 flowId + nodeId 删除节点记录。 +2. 对已有库执行同目录 20261013_flow_node_version.sql;新库直接使用初始化脚本。升级 SQL 只执行一次,不删除旧配置。 +3. 核对存量定义与节点配置,建立明确的版本映射。至少覆盖所有运行中定义;如需要查询历史任务、抄送或使用旧定义,也必须覆盖对应历史定义。 +4. 将已核验配置写入对应 process_definition_id,包含 root 和所有业务节点。每个租户、定义、节点只能有一条快照。按该版本的已核验表单字段集合补齐各节点 formPerms:原来未配置/空值的字段按原有前端默认语义补 E,已有 H/R/E 保留;包含 children 中的嵌套字段及带 field 的容器。不能用当前表单的新增字段补旧版权限。无法恢复原字段集合时,此版本迁移尚未完成。 +5. 复查覆盖情况,部署全部新代码后恢复业务。无法确认映射时不要恢复受影响流程;运行期会明确报“缺少节点版本快照”,不会回退读取最新版。 + +## 只读盘点 + +在工作流所在数据库执行以下查询;若自定义了 Flowable 表名前缀,请先调整。 + + SELECT DISTINCT d.TENANT_ID_, d.KEY_, d.ID_, d.VERSION_, d.DEPLOYMENT_ID_ + FROM ACT_RU_EXECUTION e + JOIN ACT_RE_PROCDEF d ON d.ID_ = e.PROC_DEF_ID_ + ORDER BY d.TENANT_ID_, d.KEY_, d.VERSION_; + + SELECT p.tenant_id, p.flow_id, p.unique_id, d.ID_, d.VERSION_, p.process + FROM process p + JOIN ACT_RE_PROCDEF d + ON d.DEPLOYMENT_ID_ = p.unique_id + AND d.KEY_ = p.flow_id + AND d.TENANT_ID_ = CAST(p.tenant_id AS CHAR) + WHERE p.del_flag = '0'; + + SELECT id, tenant_id, flow_id, node_id, del_flag, process_definition_id, data + FROM process_node_data + ORDER BY tenant_id, flow_id, node_id, id; + +process.unique_id 存储部署 ID,可以定位当前模板的引擎定义。它只能帮助定位当前发布版本,不代表所有旧节点记录都属于该版本。旧节点表保留了逻辑删除数据,但没有发布版本标记;不能按行 ID 或时间顺序自动配对。 + +核对节点 JSON 与相应发布资料/备份,包括审批人、多实例模式、拒绝策略、字段权限、触发器和通知地址。配置中的敏感业务内容不要写入审查日志。 + +## 存量写入模板 + +下面只是单行映射示例,所有占位值都必须替换为已核验结果。先 SELECT 确认目标行和定义的租户、业务 key、版本一致,再在受控事务中执行。不要按 flowId 批量绑定最新版。 + + UPDATE process_node_data n + JOIN ACT_RE_PROCDEF d + ON d.ID_ = '<已核验的流程定义ID>' + AND d.KEY_ = n.flow_id + AND d.TENANT_ID_ = CAST(n.tenant_id AS CHAR) + SET n.process_definition_id = d.ID_, n.del_flag = '0' + WHERE n.id = <已核验的节点记录ID> + AND n.tenant_id = <目标租户ID> + AND n.process_definition_id IS NULL; + +一条旧记录如果需属于多个已验证版本,应分别插入不同 ID 的快照副本;不要反复改写同一行的版本 ID。若需从原始设计 JSON 重建节点,注意前端的 nodeName/childNode 与数据库 JSON 的 name/children 映射,并保存所有节点。无法恢复的历史版本应明确列为阻塞,不能用最新版替代。 + +迁移后检查所有运行实例是否有 root 快照: + + SELECT DISTINCT d.ID_, d.KEY_, d.TENANT_ID_ + FROM ACT_RU_EXECUTION e + JOIN ACT_RE_PROCDEF d ON d.ID_ = e.PROC_DEF_ID_ + LEFT JOIN process_node_data n + ON n.process_definition_id = d.ID_ + AND CAST(n.tenant_id AS CHAR) = d.TENANT_ID_ + AND n.node_id = 'root' AND n.del_flag = '0' + WHERE n.id IS NULL; + +该查询必须无遗漏;还需对照设计检查全部业务节点,不能仅用 root 存在代替完整性核验。新发布时程序先按当时的表单字段集合补齐默认 E 权限,再在部署事务中写快照,后续不得人工修改已发布数据。 + +## 回退 + +不能在产生新快照后直接恢复旧代码继续写流程。需要先冻结流程写入,再根据备份和期间新增实例单独制定回退方案。不要直接删除快照或批量清空版本列。 diff --git a/db/migration/20261013_flow_node_version.sql b/db/migration/20261013_flow_node_version.sql new file mode 100644 index 000000000..4951bf3ef --- /dev/null +++ b/db/migration/20261013_flow_node_version.sql @@ -0,0 +1,6 @@ +-- 升级已有数据库;全新数据库使用 0pigxx_boot.sql 或 3pigxx_flow.sql。 +-- 执行前停止流程写入并备份;本脚本不猜测存量节点与流程版本的对应关系。 +-- 必须完成同目录说明中的存量核对后再上线应用。 +ALTER TABLE process_node_data + ADD COLUMN process_definition_id varchar(64) DEFAULT NULL COMMENT '引擎流程定义ID,发布版本快照', + ADD UNIQUE KEY uk_node_definition (tenant_id, process_definition_id, node_id); diff --git a/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/dto/ProcessNodeRecordAssignUserParamDto.java b/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/dto/ProcessNodeRecordAssignUserParamDto.java index bab6406e5..6b4073dbf 100644 --- a/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/dto/ProcessNodeRecordAssignUserParamDto.java +++ b/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/dto/ProcessNodeRecordAssignUserParamDto.java @@ -5,11 +5,9 @@ import lombok.Data; /** * 流程节点记录执行人参数数据传输对象 *

- * 该DTO用于记录流程节点的执行人信息。当任务分配给用户或用户处理任务时, - * 创建相应的执行人记录,用于追踪任务的处理过程和审批历史。 - * 包含任务执行的完整上下文信息。 + * 该DTO用于记录流程节点的执行人信息。当任务分配给用户或用户处理任务时, 创建相应的执行人记录,用于追踪任务的处理过程和审批历史。 包含任务执行的完整上下文信息。 *

- * + * * @author pigx */ @Data @@ -18,17 +16,18 @@ public class ProcessNodeRecordAssignUserParamDto { /** * 流程定义ID *

- * 流程模板的唯一标识。 - * 标识该记录所属的流程定义。 + * 流程模板的唯一标识。 标识该记录所属的流程定义。 *

*/ private String flowId; + /** 事件产生时绑定的引擎流程定义 ID。 */ + private String processDefinitionId; + /** * 流程实例ID *

- * 运行中的流程实例唯一标识。 - * 用于关联到具体的流程实例。 + * 运行中的流程实例唯一标识。 用于关联到具体的流程实例。 *

*/ private String processInstanceId; @@ -36,8 +35,7 @@ public class ProcessNodeRecordAssignUserParamDto { /** * 表单数据 *

- * JSON格式的表单数据,包含流程实例的业务数据。 - * 记录任务执行时的表单内容,便于历史查看。 + * JSON格式的表单数据,包含流程实例的业务数据。 记录任务执行时的表单内容,便于历史查看。 *

*/ private String data; @@ -45,8 +43,7 @@ public class ProcessNodeRecordAssignUserParamDto { /** * 节点本地数据 *

- * 节点级别的局部数据,仅在当前节点范围内有效。 - * 可能包含节点特定的配置或临时数据。 + * 节点级别的局部数据,仅在当前节点范围内有效。 可能包含节点特定的配置或临时数据。 *

*/ private String localData; @@ -54,8 +51,7 @@ public class ProcessNodeRecordAssignUserParamDto { /** * 节点ID *

- * 流程节点的唯一标识。 - * 标识任务所在的节点。 + * 流程节点的唯一标识。 标识任务所在的节点。 *

*/ private String nodeId; @@ -63,8 +59,7 @@ public class ProcessNodeRecordAssignUserParamDto { /** * 执行人用户ID *

- * 被分配执行该任务的用户ID。 - * 记录谁负责处理这个任务。 + * 被分配执行该任务的用户ID。 记录谁负责处理这个任务。 *

*/ private Long userId; @@ -72,8 +67,7 @@ public class ProcessNodeRecordAssignUserParamDto { /** * 执行ID *

- * Flowable引擎的执行ID。 - * 用于关联流程引擎中的执行实例。 + * Flowable引擎的执行ID。 用于关联流程引擎中的执行实例。 *

*/ private String executionId; @@ -81,8 +75,7 @@ public class ProcessNodeRecordAssignUserParamDto { /** * 任务ID *

- * Flowable引擎生成的任务ID。 - * 用于唯一标识一个用户任务实例。 + * Flowable引擎生成的任务ID。 用于唯一标识一个用户任务实例。 *

*/ private String taskId; @@ -90,8 +83,7 @@ public class ProcessNodeRecordAssignUserParamDto { /** * 审批意见 *

- * 用户处理任务时填写的审批意见或备注。 - * 记录审批理由、建议等信息。 + * 用户处理任务时填写的审批意见或备注。 记录审批理由、建议等信息。 *

*/ private String approveDesc; @@ -99,8 +91,7 @@ public class ProcessNodeRecordAssignUserParamDto { /** * 节点名称 *

- * 流程节点的显示名称,如"部门经理审批"。 - * 用于在界面上展示,提高可读性。 + * 流程节点的显示名称,如"部门经理审批"。 用于在界面上展示,提高可读性。 *

*/ private String nodeName; @@ -108,11 +99,7 @@ public class ProcessNodeRecordAssignUserParamDto { /** * 任务类型 *

- * 标识任务的类型: - * - APPROVAL: 审批任务 - * - COPY: 抄送任务 - * - NOTIFY: 通知任务 - * - CUSTOM: 自定义任务 + * 标识任务的类型: - APPROVAL: 审批任务 - COPY: 抄送任务 - NOTIFY: 通知任务 - CUSTOM: 自定义任务 *

*/ private String taskType; diff --git a/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/dto/TaskResultDto.java b/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/dto/TaskResultDto.java index f8a916f7d..173322976 100644 --- a/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/dto/TaskResultDto.java +++ b/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/dto/TaskResultDto.java @@ -7,9 +7,7 @@ import java.util.Map; /** * 任务结果数据传输对象 *

- * 该DTO用于返回任务的详细信息和执行结果。 - * 包含任务的当前状态、节点配置、流程变量、委派状态等信息。 - * 主要用于任务详情展示、任务处理页面的数据渲染。 + * 该DTO用于返回任务的详细信息和执行结果。 包含任务的当前状态、节点配置、流程变量、委派状态等信息。 主要用于任务详情展示、任务处理页面的数据渲染。 *

* * @author pigx @@ -20,9 +18,7 @@ public class TaskResultDto { /** * 是否为当前任务 *

- * 标识该任务是否为流程的当前活动任务。 - * true: 任务正在等待处理 - * false: 任务已完成或不是当前节点 + * 标识该任务是否为流程的当前活动任务。 true: 任务正在等待处理 false: 任务已完成或不是当前节点 *

*/ private Boolean currentTask; @@ -30,18 +26,18 @@ public class TaskResultDto { /** * 流程定义ID *

- * 任务所属的流程定义唯一标识。 - * 用于获取流程模板信息。 + * 任务所属的流程定义唯一标识。 用于获取流程模板信息。 *

*/ private String flowId; + /** 任务实际绑定的引擎流程定义 ID。 */ + private String processDefinitionId; + /** * 任务节点配置 *

- * 包含任务节点的完整配置信息。 - * 如节点类型、审批人设置、表单权限、条件配置等。 - * 用于渲染任务处理界面。 + * 包含任务节点的完整配置信息。 如节点类型、审批人设置、表单权限、条件配置等。 用于渲染任务处理界面。 *

*/ private Node taskNode; @@ -49,8 +45,7 @@ public class TaskResultDto { /** * 节点ID *

- * 任务对应的流程节点ID。 - * 用于定位任务在流程中的位置。 + * 任务对应的流程节点ID。 用于定位任务在流程中的位置。 *

*/ private String nodeId; @@ -58,8 +53,7 @@ public class TaskResultDto { /** * 流程实例ID *

- * 任务所属的流程实例唯一标识。 - * 用于关联到具体的流程实例。 + * 任务所属的流程实例唯一标识。 用于关联到具体的流程实例。 *

*/ private String processInstanceId; @@ -67,10 +61,7 @@ public class TaskResultDto { /** * 委派状态 *

- * 标识任务的委派状态: - * - PENDING: 待处理,委派任务等待被委派人处理 - * - RESOLVED: 已解决,委派任务已完成并返回给委派人 - * - null: 非委派任务 + * 标识任务的委派状态: - PENDING: 待处理,委派任务等待被委派人处理 - RESOLVED: 已解决,委派任务已完成并返回给委派人 - null: 非委派任务 *

*/ private String delegationState; @@ -78,9 +69,7 @@ public class TaskResultDto { /** * 是否允许继续委派 *

- * 控制任务是否可以被继续委派给其他人。 - * true: 允许当前处理人将任务委派给其他人 - * false: 不允许继续委派 + * 控制任务是否可以被继续委派给其他人。 true: 允许当前处理人将任务委派给其他人 false: 不允许继续委派 *

*/ private Boolean delegate; @@ -88,10 +77,7 @@ public class TaskResultDto { /** * 所有流程变量 *

- * 包含流程实例的所有变量,包括: - * - 表单数据:用户填写的业务数据 - * - 系统变量:流程引擎设置的变量 - * - 控制变量:用于流程控制的变量 + * 包含流程实例的所有变量,包括: - 表单数据:用户填写的业务数据 - 系统变量:流程引擎设置的变量 - 控制变量:用于流程控制的变量 * 这些变量可用于任务处理和页面展示。 *

*/ diff --git a/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/entity/ProcessNodeData.java b/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/entity/ProcessNodeData.java index ef8a4bcd7..0927a2dee 100644 --- a/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/entity/ProcessNodeData.java +++ b/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/entity/ProcessNodeData.java @@ -25,11 +25,16 @@ import java.time.LocalDateTime; @TableName("process_node_data") public class ProcessNodeData { + /** 已发布的引擎流程定义 ID;旧记录为空,不作为运行期快照。 */ + private String processDefinitionId; + + /** 所属租户。 */ + private Long tenantId; + /** * 流程实例ID *

- * 关联到具体的流程实例,标识这些节点数据属于哪个流程实例。 - * 对应ProcessInstanceRecord表的flowId字段。 + * 关联到具体的流程实例,标识这些节点数据属于哪个流程实例。 对应ProcessInstanceRecord表的flowId字段。 *

*/ @TableField("flow_id") @@ -38,8 +43,7 @@ public class ProcessNodeData { /** * 节点表单数据 *

- * 以JSON格式存储的节点表单数据,包含用户在该节点填写的所有表单字段值。 - * 数据结构根据节点配置的表单项动态变化,支持文本、数字、日期、选择等多种表单类型。 + * 以JSON格式存储的节点表单数据,包含用户在该节点填写的所有表单字段值。 数据结构根据节点配置的表单项动态变化,支持文本、数字、日期、选择等多种表单类型。 * 例如:{"amount": 5000, "reason": "采购办公用品", "approver": "user123"} *

*/ @@ -49,9 +53,7 @@ public class ProcessNodeData { /** * 节点ID *

- * 流程定义中的节点标识符,用于关联到具体的流程节点。 - * 通过此ID可以确定数据属于流程的哪个环节,如发起节点、审批节点、抄送节点等。 - * 与Node对象中的id字段对应。 + * 流程定义中的节点标识符,用于关联到具体的流程节点。 通过此ID可以确定数据属于流程的哪个环节,如发起节点、审批节点、抄送节点等。 与Node对象中的id字段对应。 *

*/ @TableField("node_id") @@ -69,8 +71,7 @@ public class ProcessNodeData { /** * 逻辑删除标记 *

- * 用于软删除功能,删除时不会真正从数据库中移除记录,而是标记为已删除。 - * 0-正常,1-已删除。保留历史数据便于审计和数据恢复。 + * 用于软删除功能,删除时不会真正从数据库中移除记录,而是标记为已删除。 0-正常,1-已删除。保留历史数据便于审计和数据恢复。 *

*/ @TableLogic @@ -80,8 +81,7 @@ public class ProcessNodeData { /** * 创建时间 *

- * 记录节点数据的创建时间,通常是用户提交表单的时间。 - * 自动填充,用于数据审计和流程追踪。 + * 记录节点数据的创建时间,通常是用户提交表单的时间。 自动填充,用于数据审计和流程追踪。 *

*/ @TableField(fill = FieldFill.INSERT) @@ -90,8 +90,7 @@ public class ProcessNodeData { /** * 更新时间 *

- * 记录节点数据的最后更新时间,在创建和更新时自动填充。 - * 用于跟踪数据的修改历史,如表单数据的补充或修正。 + * 记录节点数据的最后更新时间,在创建和更新时自动填充。 用于跟踪数据的修改历史,如表单数据的补充或修正。 *

*/ @TableField(fill = FieldFill.INSERT_UPDATE) diff --git a/pigx-flow/pigx-flow-biz/pom.xml b/pigx-flow/pigx-flow-biz/pom.xml index ba2e6a6e1..5fbd9da88 100644 --- a/pigx-flow/pigx-flow-biz/pom.xml +++ b/pigx-flow/pigx-flow-biz/pom.xml @@ -117,6 +117,11 @@ com.pig4cloud pigx-common-swagger + + com.h2database + h2 + test + org.springframework.boot diff --git a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/IProcessNodeDataService.java b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/IProcessNodeDataService.java index 231247cbc..1d599f415 100644 --- a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/IProcessNodeDataService.java +++ b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/IProcessNodeDataService.java @@ -9,9 +9,8 @@ import com.pig4cloud.pigx.flow.entity.ProcessNodeData; /** * 流程节点数据管理服务接口 *

- * 该服务负责管理流程定义中每个节点的配置数据,包括节点的属性设置、审批人配置、 - * 表单权限、条件表达式等。这些数据是流程引擎执行时的重要依据,决定了流程节点 - * 的行为方式。服务提供了节点数据的保存和查询功能,支持流程的动态配置。 + * 该服务负责管理流程定义中每个节点的配置数据,包括节点的属性设置、审批人配置、 表单权限、条件表达式等。这些数据是流程引擎执行时的重要依据,决定了流程节点 + * 的行为方式。节点配置随发布保存为不可变快照;设计态可查询最新版,运行期必须指定引擎定义 ID。 *

* * @author Vincent @@ -20,43 +19,41 @@ import com.pig4cloud.pigx.flow.entity.ProcessNodeData; public interface IProcessNodeDataService extends IService { /** - * 保存或更新流程节点数据 - *

- * 保存流程节点的完整配置信息,包括节点基本属性、审批人设置、表单字段权限、 - * 条件配置等。如果节点数据已存在则更新,不存在则新增。保存时会进行数据 - * 验证,确保配置的合法性和完整性。该接口通常在流程设计时调用。 - *

- * - * @param processNodeDataDto 节点数据传输对象,包含: - * - flowId: 流程定义ID - * - nodeId: 节点ID - * - nodeName: 节点名称 - * - nodeType: 节点类型(审批/抄送/条件等) - * - nodeUser: 节点处理人配置 - * - formPermission: 表单字段权限配置 - * - conditions: 条件表达式(条件节点使用) - * - properties: 其他扩展属性 - * @return R 响应结果,成功返回保存后的节点数据ID,失败返回错误信息 + * 保留旧接口契约但拒绝直接修改节点配置;修改须随流程重新发布。 + * @param processNodeDataDto 旧接口的节点数据,允许为 null;不读取也不持久化 + * @return 说明需重新发布的失败结果,不为 null */ R saveNodeData(ProcessNodeDataDto processNodeDataDto); /** - * 获取指定节点的配置数据 - *

- * 根据流程ID和节点ID查询节点的完整配置信息。返回的数据包括节点的所有 - * 配置项,可直接用于流程引擎的执行或流程设计器的回显。查询时会进行 - * 权限校验,确保调用者有权限访问该流程的节点数据。 - *

- * - * @param flowId 流程定义ID,标识节点所属的流程 - * @param nodeId 节点ID,流程中节点的唯一标识 - * @return R 响应结果,包含节点的完整配置信息: - * - 基本信息(ID、名称、类型等) - * - 处理人配置(固定人员、角色、部门负责人等) - * - 表单权限(各字段的可见、可编辑权限) - * - 条件配置(分支条件、跳转条件等) - * - 其他属性(超时设置、多实例配置等) + * 按当前租户查询业务流程的最新发布配置,仅供设计态使用。 运行期必须使用 getPublishedNode,避免读取其他发布版本。 + * @param flowId 业务流程 key,不允许为 null + * @param nodeId 节点 ID,不允许为空 + * @return 最新发布节点;没有发布定义或请求的是生成节点时,结果 data 为 null + * @throws IllegalArgumentException 已找到定义但 nodeId 为空时抛出 + * @throws IllegalStateException 已发布定义缺少版本快照或业务节点快照时抛出 */ R getNodeData(String flowId, String nodeId); + /** + * 读取指定引擎发布版本的节点快照,不回退到最新版本。 + * @param processDefinitionId 引擎发布定义 ID,包含版本信息,非业务流程 key,不允许为空 + * @param nodeId 节点 ID,不允许为空 + * @return 节点配置;根快照存在且其中不包含该节点(如编译器生成节点)时,data 为 null + * @throws IllegalArgumentException 任一参数为空时抛出 + * @throws IllegalStateException 根快照缺失,或根树中存在该业务节点但独立快照缺失时抛出 + * @throws cn.hutool.json.JSONException 已保存的节点 JSON 无法解析时抛出 + */ + R getPublishedNode(String processDefinitionId, String nodeId); + + /** + * 递归插入发布节点快照,使用引擎定义的 key、ID 和租户,不覆盖既有数据。 调用方必须在同一事务中部署引擎定义;此方法参与该事务,失败时整体回滚。 + * @param definition 刚部署的引擎定义,不允许为 null,租户 ID 须为 Long 格式 + * @param root 发起节点树,不允许为 null,ID 必须为 root;空节点和空 ID 子树跳过 + * @throws IllegalArgumentException 定义或根节点无效、节点 ID 重复或租户 ID 非数字时抛出 + * @throws IllegalStateException 任一节点插入数量不为一时抛出 + * @throws org.springframework.dao.DataAccessException 插入失败或违反版本唯一约束时抛出 + */ + void savePublishedNodes(org.flowable.engine.repository.ProcessDefinition definition, Node root); + } diff --git a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/impl/ProcessCopyServiceImpl.java b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/impl/ProcessCopyServiceImpl.java index 2cc4b3b4a..d061af43d 100644 --- a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/impl/ProcessCopyServiceImpl.java +++ b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/impl/ProcessCopyServiceImpl.java @@ -14,17 +14,18 @@ import com.pig4cloud.pigx.flow.service.IProcessService; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import org.springframework.stereotype.Service; +import org.flowable.engine.HistoryService; +import org.flowable.engine.RuntimeService; +import com.pig4cloud.pigx.flow.support.utils.NodeUtil; -import java.util.Map; +import java.util.Objects; /** * 流程抄送数据服务实现类 *

- * 该类负责处理流程抄送相关的业务逻辑,主要功能包括: - * 1. 管理流程抄送记录的增删改查 - * 2. 查询抄送详情,包括流程表单、表单数据和权限信息 - * 3. 为被抄送人提供查看流程执行情况的能力 - * + * 该类负责处理流程抄送相关的业务逻辑,主要功能包括: 1. 管理流程抄送记录的增删改查 2. 查询抄送详情,包括流程表单、表单数据和权限信息 3. + * 为被抄送人提供查看流程执行情况的能力 + * * 抄送功能允许流程在执行过程中通知相关人员,被抄送人可以查看但不参与流程审批 *

* @@ -35,56 +36,71 @@ import java.util.Map; @RequiredArgsConstructor public class ProcessCopyServiceImpl extends ServiceImpl implements IProcessCopyService { - private final ObjectMapper objectMapper; + private final ObjectMapper objectMapper; - private final IProcessService processService; + private final IProcessService processService; - private final IProcessNodeDataService nodeDataService; + private final IProcessNodeDataService nodeDataService; - /** - * 查询单个抄送详细信息 - *

- * 根据抄送记录ID查询完整的抄送详情,包括: - * 1. 流程定义信息(process) - * 2. 表单项定义(formItems) - * 3. 表单数据(formData)- 流程实例的实际填写数据 - * 4. 表单权限(formPerms)- 基于当前节点的表单字段权限设置 - * - * 该方法主要用于抄送人查看被抄送的流程详情 - *

- * - * @param id 抄送记录ID - * @return R 包含流程详情的响应对象,包括process、formItems、formData、formPerms - * @throws Exception 当解析JSON数据失败时抛出异常 - */ - @SneakyThrows - @Override - public R querySingleDetail(long id) { - // 获取抄送记录 - ProcessCopy processCopy = this.getById(id); - String flowId = processCopy.getFlowId(); - - // 查询流程定义 - Process oaForms = processService.getByFlowId(flowId); - if (oaForms == null) { - return R.failed("流程不存在"); - } - - // 获取表单数据和节点信息 - String formData = processCopy.getFormData(); - String nodeId = processCopy.getNodeId(); + private final HistoryService historyService; - // 获取节点的表单权限配置 - Node node = nodeDataService.getNodeData(flowId, nodeId).getData(); - Map formPerms = node.getFormPerms(); + private final RuntimeService runtimeService; - // 构建返回数据 - Dict set = Dict.create() - .set("process", oaForms.getProcess()) // 流程定义 - .set("formItems", oaForms.getFormItems()) // 表单项定义 - .set("formData", formData) // 表单数据 - .set("formPerms", formPerms); // 表单权限 - return R.ok(set); - } + /** + * 查询单个抄送详细信息 + *

+ * 根据抄送记录ID查询完整的抄送详情,包括: 1. 流程定义信息(process) 2. 表单项定义(formItems) 3. 表单数据(formData)- + * 流程实例的实际填写数据 4. 表单权限(formPerms)- 基于当前节点的表单字段权限设置 + * + * 该方法主要用于抄送人查看被抄送的流程详情 + *

+ * @param id 抄送记录ID + * @return R 包含流程详情的响应对象,包括process、formItems、formData、formPerms + * @throws Exception 当解析JSON数据失败时抛出异常 + */ + @SneakyThrows + @Override + public R querySingleDetail(long id) { + // 获取抄送记录 + ProcessCopy processCopy = this.getById(id); + String flowId = processCopy.getFlowId(); + + // 查询流程定义 + Process oaForms = processService.getByFlowId(flowId); + if (oaForms == null) { + return R.failed("流程不存在"); + } + + // 获取表单数据和节点信息 + String formData = processCopy.getFormData(); + String nodeId = processCopy.getNodeId(); + + // 获取节点的表单权限配置 + var running = runtimeService.createProcessInstanceQuery() + .processInstanceId(processCopy.getProcessInstanceId()) + .singleResult(); + String definitionId; + if (Objects.nonNull(running)) { + definitionId = running.getProcessDefinitionId(); + } + else { + var historic = historyService.createHistoricProcessInstanceQuery() + .processInstanceId(processCopy.getProcessInstanceId()) + .singleResult(); + if (Objects.isNull(historic)) { + return R.failed("流程实例历史不存在,无法确定节点配置版本"); + } + definitionId = historic.getProcessDefinitionId(); + } + Node node = nodeDataService.getPublishedNode(definitionId, nodeId).getData(); + + // 构建返回数据 + Dict set = Dict.create() + .set("process", oaForms.getProcess()) // 流程定义 + .set("formItems", oaForms.getFormItems()) // 表单项定义 + .set("formData", formData) // 表单数据 + .set("formPerms", NodeUtil.publishedFormPermissions(node, oaForms.getFormItems())); // 表单权限 + return R.ok(set); + } } diff --git a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/impl/ProcessInstanceServiceImpl.java b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/impl/ProcessInstanceServiceImpl.java index f95a124a9..3e837c632 100644 --- a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/impl/ProcessInstanceServiceImpl.java +++ b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/impl/ProcessInstanceServiceImpl.java @@ -66,638 +66,719 @@ import java.util.stream.Collectors; @RequiredArgsConstructor public class ProcessInstanceServiceImpl implements IProcessInstanceService { - private final RemoteUserService userService; - - private final TaskService taskService; - - private final RuntimeService runtimeService; - - private final HistoryService historyService; - - private final IProcessInstanceRecordService processInstanceRecordService; - - private final IProcessCopyService processCopyService; - - private final IProcessService processService; - - private final IProcessNodeRecordService processNodeRecordService; - - private final IProcessNodeRecordAssignUserService processNodeRecordAssignUserService; - - private final ObjectMapper objectMapper; - - /** - * 启动流程 - *

- * 根据流程定义启动一个新的流程实例。该方法会: 1. 获取当前登录用户作为流程发起人 2. 设置流程发起人信息到流程变量中 3. 调用Flowable引擎启动流程 - *

- * - * @param processInstanceParamDto 流程启动参数,包含: - flowId: 流程定义ID - paramMap: 流程变量 - * @return R 启动结果,包含流程实例ID等信息 - */ - @Override - public R startProcessInstance(ProcessInstanceParamDto processInstanceParamDto) { - - // 获取当前登录用户 - PigxUser user = SecurityUtils.getUser(); - - // 设置流程发起人ID - processInstanceParamDto.setStartUserId(String.valueOf(user.getId())); - Map paramMap = processInstanceParamDto.getParamMap(); - - // 将发起人信息作为root变量存入流程,用于流程节点的权限判断 - Dict rootUser = Dict.create().set("id", user.getId()).set("name", user.getUsername()).set("type", NodeUserTypeEnum.USER.getKey()); - paramMap.put("root", CollUtil.newArrayList(rootUser)); - - // 调用引擎启动流程 - Authentication.setAuthenticatedUserId(processInstanceParamDto.getStartUserId()); - ProcessInstance processInstance = runtimeService.startProcessInstanceByKeyAndTenantId(processInstanceParamDto.getFlowId(), processInstanceParamDto.getParamMap(), TenantContextHolder.getTenantId().toString()); - - String processInstanceId = processInstance.getProcessInstanceId(); - return R.ok(processInstanceId); - } - - /** - * 查询当前登录用户的待办任务 - *

- * 查询分配给当前用户的待办任务列表,并补充以下信息: 1. 流程实例的详细信息(流程名称、分组等) 2. 流程发起人信息(姓名、头像等) 3. 流程启动时间 - *

- * - * @param taskQueryParamDto 查询参数,支持分页和条件过滤 - * @return R 包含分页后的待办任务列表 - */ - @Override - public R queryMineTask(TaskQueryParamDto taskQueryParamDto) { - taskQueryParamDto.setAssign(SecurityUtils.getUser().getId().toString()); - - String assign = taskQueryParamDto.getAssign(); - - List taskDtoList = new ArrayList<>(); - - int pageIndex = taskQueryParamDto.getCurrent() - 1; - int pageSize = taskQueryParamDto.getSize(); - - Page pageResultDto = new Page<>(); - TaskQuery taskQuery = taskService.createTaskQuery().taskAssignee(assign).orderByTaskCreateTime().desc(); - if (StrUtil.isNotBlank(taskQueryParamDto.getProcessName())) { - List processInstanceList = runtimeService.createProcessInstanceQuery().processDefinitionName(taskQueryParamDto.getProcessName()).processInstanceTenantId(TenantContextHolder.getTenantId().toString()).list(); - if (CollUtil.isNotEmpty(processInstanceList)) { - taskQuery.processInstanceIdIn(processInstanceList.stream().map(ProcessInstance::getProcessInstanceId).toList()); - } - } - - if (ArrayUtil.isNotEmpty(taskQueryParamDto.getTaskTime())) { - ZoneId zoneId = ZoneId.systemDefault(); - ZonedDateTime zonedBeforeDateTime = taskQueryParamDto.getTaskTime()[0].atZone(zoneId); - ZonedDateTime zonedAfterDateTime = taskQueryParamDto.getTaskTime()[1].atZone(zoneId); - Date beforeDate = Date.from(zonedBeforeDateTime.toInstant()); - Date afterDate = Date.from(zonedAfterDateTime.toInstant()); - taskQuery.taskCreatedBefore(afterDate).taskCreatedAfter(beforeDate); - } - - taskQuery.listPage(pageIndex * pageSize, pageSize).forEach(task -> { - String taskId = task.getId(); - String processInstanceId = task.getProcessInstanceId(); - log.debug("(taskId) {} processInstanceId={} executrionId={}", task.getName(), processInstanceId, task.getExecutionId()); - - String taskDefinitionKey = task.getTaskDefinitionKey(); - String processDefinitionId = task.getProcessDefinitionId(); - String flowId = NodeUtil.getFlowId(processDefinitionId); - - TaskDto taskDto = new TaskDto(); - taskDto.setFlowId(flowId); - taskDto.setTaskCreateTime(task.getCreateTime()); - taskDto.setNodeId(taskDefinitionKey); - taskDto.setProcessInstanceId(processInstanceId); - taskDto.setTaskId(taskId); - taskDto.setAssign(task.getAssignee()); - taskDto.setTaskName(task.getName()); - taskDtoList.add(taskDto); - }); - - long count = taskQuery.count(); - - log.debug("当前有 {} 个任务:", count); - - pageResultDto.setTotal(count); - pageResultDto.setRecords(taskDtoList); - - if (CollUtil.isEmpty(taskDtoList)) { - return R.ok(); - } - - Set processInstanceIdSet = taskDtoList.stream().map(TaskDto::getProcessInstanceId).collect(Collectors.toSet()); - - // 流程实例记录 - List processInstanceRecordList = processInstanceRecordService.lambdaQuery().in(ProcessInstanceRecord::getProcessInstanceId, processInstanceIdSet).list(); - - // 发起人 - List collect = processInstanceRecordList.stream().map(ProcessInstanceRecord::getUserId).toList(); - List startUserList = userService.getUserById(collect).getData(); - - List taskNewDtoList = new ArrayList<>(); - for (TaskDto record : taskDtoList) { - ProcessInstanceRecord processInstanceRecord = processInstanceRecordList.stream().filter(w -> StrUtil.equals(w.getProcessInstanceId(), record.getProcessInstanceId())).findAny().orElse(null); - - if (processInstanceRecord != null) { - record.setProcessName(processInstanceRecord.getName()); - SysUser startUser = startUserList.stream().filter(w -> w.getUserId().longValue() == processInstanceRecord.getUserId()).findAny().orElse(null); - - record.setRootUserId(processInstanceRecord.getUserId()); - record.setGroupName(processInstanceRecord.getGroupName()); - record.setRootUserName(startUser.getUsername()); - record.setRootUserAvatarUrl(startUser.getAvatar()); - record.setStartTime(processInstanceRecord.getCreateTime()); - - taskNewDtoList.add(record); - } - } - - pageResultDto.setRecords(taskNewDtoList); - - return R.ok(pageResultDto); - } - - /** - * 查询已办任务 - *

- * 查询当前用户已经完成的任务列表,包括: 1. 用户已审批的任务 2. 补充流程实例信息、发起人信息等 - *

- * - * @param taskQueryParamDto 查询参数,支持分页和时间范围过滤 - * @return R 包含分页后的已办任务列表 - */ - @Override - public R queryMineEndTask(TaskQueryParamDto taskQueryParamDto) { - taskQueryParamDto.setAssign(SecurityUtils.getUser().getId().toString()); - - HistoricActivityInstanceQuery historicActivityInstanceQuery = historyService.createHistoricActivityInstanceQuery(); - HistoricActivityInstanceQuery activityInstanceQuery = historicActivityInstanceQuery.taskAssignee(taskQueryParamDto.getAssign()).finished().orderByHistoricActivityInstanceEndTime().desc(); - - if (ArrayUtil.isNotEmpty(taskQueryParamDto.getTaskTime())) { - ZoneId zoneId = ZoneId.systemDefault(); - ZonedDateTime zonedBeforeDateTime = taskQueryParamDto.getTaskTime()[0].atZone(zoneId); - ZonedDateTime zonedAfterDateTime = taskQueryParamDto.getTaskTime()[1].atZone(zoneId); - Date beforeDate = Date.from(zonedBeforeDateTime.toInstant()); - Date afterDate = Date.from(zonedAfterDateTime.toInstant()); - activityInstanceQuery.finishedBefore(afterDate).finishedAfter(beforeDate); - } - - List list = activityInstanceQuery.listPage((taskQueryParamDto.getCurrent() - 1) * taskQueryParamDto.getSize(), taskQueryParamDto.getSize()); - - long count = activityInstanceQuery.count(); - List taskDtoList = new ArrayList<>(); - - for (HistoricActivityInstance historicActivityInstance : list) { - String activityId = historicActivityInstance.getActivityId(); - String activityName = historicActivityInstance.getActivityName(); - String executionId = historicActivityInstance.getExecutionId(); - String taskId = historicActivityInstance.getTaskId(); - Date startTime = historicActivityInstance.getStartTime(); - Date endTime = historicActivityInstance.getEndTime(); - Long durationInMillis = historicActivityInstance.getDurationInMillis(); - String processInstanceId = historicActivityInstance.getProcessInstanceId(); - - String processDefinitionId = historicActivityInstance.getProcessDefinitionId(); - // 流程id - String flowId = NodeUtil.getFlowId(processDefinitionId); - - TaskDto taskDto = new TaskDto(); - taskDto.setFlowId(flowId); - taskDto.setTaskCreateTime(startTime); - taskDto.setTaskEndTime(endTime); - taskDto.setNodeId(activityId); - taskDto.setExecutionId(executionId); - taskDto.setProcessInstanceId(processInstanceId); - taskDto.setDurationInMillis(durationInMillis); - taskDto.setTaskId(taskId); - taskDto.setAssign(historicActivityInstance.getAssignee()); - taskDto.setTaskName(activityName); - - taskDtoList.add(taskDto); - } - - Page pageResultDto = new Page<>(); - pageResultDto.setTotal(count); - pageResultDto.setRecords(taskDtoList); - - List records = pageResultDto.getRecords(); - if (CollUtil.isEmpty(records)) { - return R.ok(pageResultDto); - - } - - Set processInstanceIdSet = records.stream().map(TaskDto::getProcessInstanceId).collect(Collectors.toSet()); - - // 流程实例记录 - List processInstanceRecordList = processInstanceRecordService.lambdaQuery().in(ProcessInstanceRecord::getProcessInstanceId, processInstanceIdSet).list(); - - // 发起人 - List collect = processInstanceRecordList.stream().map(ProcessInstanceRecord::getUserId).toList(); - List startUserList = userService.getUserById(collect).getData(); - - List taskNewDtoList = new ArrayList<>(); - for (TaskDto record : records) { - - ProcessInstanceRecord processInstanceRecord = processInstanceRecordList.stream().filter(w -> StrUtil.equals(w.getProcessInstanceId(), record.getProcessInstanceId())).findAny().orElse(null); - - if (processInstanceRecord != null) { - - record.setProcessName(processInstanceRecord.getName()); - - SysUser startUser = startUserList.stream().filter(w -> w.getUserId().longValue() == processInstanceRecord.getUserId()).findAny().orElse(null); - - record.setRootUserId(processInstanceRecord.getUserId()); - record.setGroupName(processInstanceRecord.getGroupName()); - record.setRootUserName(startUser.getName()); - record.setRootUserAvatarUrl(startUser.getAvatar()); - record.setStartTime(processInstanceRecord.getCreateTime()); - taskNewDtoList.add(record); - } - } - - pageResultDto.setRecords(taskNewDtoList); - - return R.ok(pageResultDto); - } - - /** - * 结束流程实例 - *

- * 手动结束一个流程实例,通常用于: 1. 流程审批完成,正常结束 2. 流程被终止或取消 - *

- * 该方法会更新流程实例记录的结束时间、状态和结束原因 - *

- * - * @param processInstanceParamDto 流程实例参数,包含: - processInstanceId: 流程实例ID - paramMap: - * 可能包含approve_condition等结束原因 - * @return R 操作结果 - */ - @Override - public R end(ProcessInstanceParamDto processInstanceParamDto) { - - // 拒绝结束时会写入明确的结束原因;正常流转到结束节点时默认为通过 - Map paramMap = Optional.ofNullable(processInstanceParamDto.getParamMap()) - .orElseGet(Collections::emptyMap); - String finishReason = MapUtil.getStr(paramMap, ProcessInstanceConstant.FINISH_REASON_VAR); - if (StrUtil.isBlank(finishReason)) { - finishReason = ProcessInstanceConstant.FINISH_REASON_PASS; - } - - // 构建更新条件 - LambdaUpdateChainWrapper lambdaUpdateChainWrapper = processInstanceRecordService.lambdaUpdate().set(ProcessInstanceRecord::getEndTime, new Date()).set(ProcessInstanceRecord::getStatus, NodeStatusEnum.YJS.getCode()).set(ProcessInstanceRecord::getFinishReason, finishReason); - - // 执行更新 - lambdaUpdateChainWrapper.eq(ProcessInstanceRecord::getProcessInstanceId, processInstanceParamDto.getProcessInstanceId()).update(new ProcessInstanceRecord()); - return R.ok(); - } - - /** - * 查询我发起的流程 - *

- * 查询当前用户发起的所有流程实例,支持: 1. 按状态过滤(进行中、已完成等) 2. 按时间范围过滤 3. 分页查询 - *

- * - * @param taskQueryParamDto 查询条件,包含状态、时间范围、分页参数 - * @return R 包含分页后的流程实例列表 - */ - @Override - public R queryMineStarted(TaskQueryParamDto taskQueryParamDto) { - - long userId = SecurityUtils.getUser().getId(); - - Page instanceRecordPage = processInstanceRecordService.lambdaQuery().eq(ProcessInstanceRecord::getUserId, userId).eq(ProcessInstanceRecord::getStatus, taskQueryParamDto.getStatus()).between(ArrayUtil.isNotEmpty(taskQueryParamDto.getTaskTime()), ProcessInstanceRecord::getCreateTime, ArrayUtil.isNotEmpty(taskQueryParamDto.getTaskTime()) ? taskQueryParamDto.getTaskTime()[0] : null, ArrayUtil.isNotEmpty(taskQueryParamDto.getTaskTime()) ? taskQueryParamDto.getTaskTime()[1] : null).orderByDesc(ProcessInstanceRecord::getCreateTime).page(new Page<>(taskQueryParamDto.getCurrent(), taskQueryParamDto.getSize())); - - // 转换为 VO 并添加驳回状态信息 - List voList = BeanUtil.copyToList(instanceRecordPage.getRecords(), ProcessInstanceRecordVo.class); - - // 批量查询流程定义,用于检查打印模板 - Set flowIds = voList.stream() - .map(ProcessInstanceRecordVo::getFlowId) - .filter(StrUtil::isNotBlank) - .collect(Collectors.toSet()); - List processList = CollUtil.isEmpty(flowIds) ? Collections.emptyList() - : processService.lambdaQuery().in(Process::getFlowId, flowIds).list(); - Set processInstanceIds = voList.stream() - .map(ProcessInstanceRecordVo::getProcessInstanceId) - .filter(StrUtil::isNotBlank) - .collect(Collectors.toSet()); - Map> assignUserMap = queryAssignUserMap(processInstanceIds); - - // 批量查询进行中流程的 rejectToStarter 变量,避免 N+1 - Set runningInstanceIds = voList.stream() - .filter(vo -> Objects.equals(vo.getStatus(), NodeStatusEnum.JXZ.getCode())) - .map(ProcessInstanceRecordVo::getProcessInstanceId) - .filter(StrUtil::isNotBlank) - .collect(Collectors.toSet()); - - Set rejectToStarterIds = batchQueryRejectToStarter(runningInstanceIds); - - // 批量查询驳回待提交的 root 任务,避免 N+1 - Map resubmitTaskMap = Collections.emptyMap(); - if (!rejectToStarterIds.isEmpty()) { - resubmitTaskMap = taskService.createTaskQuery() - .processInstanceIdIn(rejectToStarterIds) - .taskDefinitionKey("root") - .list() - .stream() - .collect(Collectors.toMap(Task::getProcessInstanceId, Task::getId, (a, b) -> a)); - } - - // 构建打印模板 Map,避免循环内 stream 遍历 - Map processMap = processList.stream() - .collect(Collectors.toMap(p -> p.getFlowId().toLowerCase(), p -> p, (a, b) -> a)); - - for (ProcessInstanceRecordVo vo : voList) { - boolean rejectToStarter = false; - if (Objects.equals(vo.getStatus(), NodeStatusEnum.JXZ.getCode())) { - rejectToStarter = rejectToStarterIds.contains(vo.getProcessInstanceId()); - vo.setRejectToStarter(rejectToStarter); - - if (rejectToStarter) { - vo.setResubmitTaskId(resubmitTaskMap.get(vo.getProcessInstanceId())); - } - } else { - vo.setRejectToStarter(false); - } - - WithdrawState withdrawState = buildWithdrawState(vo, vo.getRejectToStarter(), - assignUserMap.getOrDefault(vo.getProcessInstanceId(), Collections.emptyList())); - vo.setCanWithdraw(withdrawState.canWithdraw); - vo.setWithdrawDisabledReason(withdrawState.disabledReason); - - // 检查是否配置了打印模板 - Process matchedProcess = processMap.get(vo.getFlowId().toLowerCase()); - if (matchedProcess != null) { - vo.setHasPrintTemplate(StrUtil.isNotBlank(matchedProcess.getPrintTemplate())); - } - } - - // 构建返回的分页对象 - Page resultPage = new Page<>(instanceRecordPage.getCurrent(), instanceRecordPage.getSize(), instanceRecordPage.getTotal()); - resultPage.setRecords(voList); - - return R.ok(resultPage); - } - - /** - * 查询抄送给我的流程 - *

- * 查询抄送给当前用户的流程列表。抄送功能允许流程参与者将流程信息 通知给其他相关人员,被抄送人可以查看流程但不参与审批 - *

- * - * @param taskQueryParamDto 查询参数,支持时间范围过滤和分页 - * @return R 包含分页后的抄送记录,包括发起人信息 - */ - @Override - public R queryMineCC(TaskQueryParamDto taskQueryParamDto) { - - long userId = SecurityUtils.getUser().getId(); - - Page page = processCopyService.lambdaQuery().eq(ProcessCopy::getUserId, userId).between(ArrayUtil.isNotEmpty(taskQueryParamDto.getTaskTime()), ProcessCopy::getCreateTime, ArrayUtil.isNotEmpty(taskQueryParamDto.getTaskTime()) ? taskQueryParamDto.getTaskTime()[0] : null, ArrayUtil.isNotEmpty(taskQueryParamDto.getTaskTime()) ? taskQueryParamDto.getTaskTime()[1] : null).orderByDesc(ProcessCopy::getNodeTime).page(new Page<>(taskQueryParamDto.getCurrent(), taskQueryParamDto.getSize())); - - List records = page.getRecords(); - - List processCopyVoList = BeanUtil.copyToList(records, ProcessCopyVo.class); - - if (CollUtil.isNotEmpty(records)) { - // 发起人 - List userIdList = records.stream().map(ProcessCopy::getStartUserId).toList(); - List startUserList = userService.getUserById(userIdList).getData(); - - for (ProcessCopyVo record : processCopyVoList) { - SysUser startUser = startUserList.stream().filter(w -> w.getUserId().longValue() == record.getStartUserId()).findAny().orElse(null); - record.setStartUserName(startUser.getUsername()); - } - } - - Page p = BeanUtil.copyProperties(page, Page.class); - p.setRecords(processCopyVoList); - return R.ok(p); - } - - /** - * 格式化流程显示 - *

- * 将流程定义数据格式化为前端可展示的节点结构,主要功能: 1. 解析流程定义的JSON数据 2. 标记已完成的节点 3. 根据流程变量计算条件节点的显示状态 4. - * 处理节点权限和表单权限 - *

- * 该方法用于流程图的可视化展示 - *

- * - * @param nodeFormatParamVo 格式化参数,包含: - flowId: 流程定义ID - processInstanceId: 流程实例ID - - * taskId: 任务ID(可选) - paramMap: 流程变量 - * @return R 包含格式化后的节点列表 - */ - @SneakyThrows - @Override - public R formatStartNodeShow(NodeFormatParamVo nodeFormatParamVo) { - String flowId = nodeFormatParamVo.getFlowId(); - String processInstanceId = nodeFormatParamVo.getProcessInstanceId(); - if (StrUtil.isAllBlank(flowId, processInstanceId)) { - return R.ok(new ArrayList<>()); - } - - if (StrUtil.isBlankIfStr(flowId) && StrUtil.isNotBlank(processInstanceId)) { - ProcessInstanceRecord processInstanceRecord = processInstanceRecordService.lambdaQuery().eq(ProcessInstanceRecord::getProcessInstanceId, processInstanceId).one(); - flowId = processInstanceRecord.getFlowId(); - - } - Map paramMap = nodeFormatParamVo.getParamMap(); - if (StrUtil.isNotBlank(nodeFormatParamVo.getTaskId())) { - VariableQueryParamDto variableQueryParamDto = new VariableQueryParamDto(); - variableQueryParamDto.setTaskId(nodeFormatParamVo.getTaskId()); - Map variables; - List keyList = variableQueryParamDto.getKeyList(); - if (CollUtil.isEmpty(keyList)) { - TaskQuery taskQuery = taskService.createTaskQuery(); - - Task task = taskQuery.taskId(variableQueryParamDto.getTaskId()).singleResult(); - if (task == null) { - return R.failed("任务不存在"); - } - - variables = runtimeService.getVariables(task.getExecutionId()); - } else { - variables = taskService.getVariables(variableQueryParamDto.getTaskId(), keyList); - } - paramMap.putAll(variables); - } - - Set completeNodeSet = new HashSet<>(); - Set continueNodeSet = new HashSet<>(); - if (StrUtil.isNotBlank(processInstanceId)) { - List processNodeRecordList = processNodeRecordService.lambdaQuery().eq(ProcessNodeRecord::getProcessInstanceId, processInstanceId).eq(ProcessNodeRecord::getStatus, NodeStatusEnum.YJS.getCode()).list(); - Set collect = processNodeRecordList.stream().map(ProcessNodeRecord::getNodeId).collect(Collectors.toSet()); - - - List jxzNodeRecordList = processNodeRecordService.lambdaQuery().eq(ProcessNodeRecord::getProcessInstanceId, processInstanceId).eq(ProcessNodeRecord::getStatus, NodeStatusEnum.JXZ.getCode()).list(); - - Set jxzCollect = jxzNodeRecordList.stream().map(ProcessNodeRecord::getNodeId).collect(Collectors.toSet()); - continueNodeSet.addAll(jxzCollect); - completeNodeSet.addAll(collect); - } - - Process oaForms = processService.getByFlowId(flowId); - String process = oaForms.getProcess(); - Node nodeDto = objectMapper.readValue(process, new TypeReference<>() { - }); - - List processNodeShowDtos = NodeFormatUtil.formatProcessNodeShow(nodeDto, completeNodeSet, continueNodeSet, processInstanceId, paramMap); - - return R.ok(processNodeShowDtos); - } - - /** - * 流程详情 - *

- * 查询流程实例的详细信息,包括: 1. 流程定义信息(流程结构) 2. 表单项定义 3. 表单数据(用户填写的数据) 4. 表单权限配置 5. 流程状态信息 - *

- * 该方法主要用于流程详情页面的数据展示 - *

- * - * @param processInstanceId 流程实例ID - * @return R 包含流程详情的Dict对象 - * @throws Exception 当解析JSON数据失败时抛出异常 - */ - @SneakyThrows - @Override - public R detail(String processInstanceId) { - ProcessInstanceRecord processInstanceRecord = processInstanceRecordService.lambdaQuery().eq(ProcessInstanceRecord::getProcessInstanceId, processInstanceId).one(); - - Process oaForms = processService.getByFlowId(processInstanceRecord.getFlowId()); - if (oaForms == null) { - return R.failed("流程不存在"); - } - - // 发起人变量数据 - String formData = processInstanceRecord.getFormData(); - String process = oaForms.getProcess(); - - Node nodeDto = objectMapper.readValue(process, Node.class); - Map formPerms1 = nodeDto.getFormPerms(); - boolean rejectToStarter = Objects.equals(processInstanceRecord.getStatus(), NodeStatusEnum.JXZ.getCode()) - && isRejectToStarter(processInstanceId); - WithdrawState withdrawState = buildWithdrawState(processInstanceRecord, rejectToStarter, - queryAssignUserMap(Collections.singleton(processInstanceId)) - .getOrDefault(processInstanceId, Collections.emptyList())); - - Dict set = Dict.create() - .set(ProcessInstanceRecord.Fields.processInstanceId, processInstanceId) - .set(ProcessInstanceRecord.Fields.status, processInstanceRecord.getStatus()) - .set("finishReason", processInstanceRecord.getFinishReason()) - .set("rejectToStarter", rejectToStarter) - .set("canWithdraw", withdrawState.canWithdraw) - .set("withdrawDisabledReason", withdrawState.disabledReason) - .set("process", oaForms.getProcess()) - .set("formItems", oaForms.getFormItems()) - .set("formData", formData) - .set("formConfig", oaForms.getFormConfig()) - .set("formPerms", formPerms1); - - return R.ok(set); - } - - private Map> queryAssignUserMap(Set processInstanceIds) { - if (CollUtil.isEmpty(processInstanceIds)) { - return Collections.emptyMap(); - } - - return processNodeRecordAssignUserService.lambdaQuery() - .in(ProcessNodeRecordAssignUser::getProcessInstanceId, processInstanceIds) - .ne(ProcessNodeRecordAssignUser::getNodeId, "root") - .list() - .stream() - .collect(Collectors.groupingBy(ProcessNodeRecordAssignUser::getProcessInstanceId)); - } - - private boolean isRejectToStarter(String processInstanceId) { - try { - Object rejectVar = runtimeService.getVariable(processInstanceId, - ProcessInstanceConstant.REJECT_TO_STARTER_VAR); - return Boolean.TRUE.equals(rejectVar); - } catch (Exception e) { - log.debug("查询驳回待提交状态失败: processInstanceId={}, error={}", processInstanceId, e.getMessage()); - return false; - } - } - - /** - * 批量查询流程实例的驳回到发起人状态 - * - * @param processInstanceIds 流程实例ID集合 - * @return 处于驳回待提交状态的流程实例ID集合 - */ - private Set batchQueryRejectToStarter(Set processInstanceIds) { - if (CollUtil.isEmpty(processInstanceIds)) { - return Collections.emptySet(); - } - try { - return historyService.createHistoricVariableInstanceQuery() - .processInstanceIds(processInstanceIds) - .variableName(ProcessInstanceConstant.REJECT_TO_STARTER_VAR) - .list() - .stream() - .filter(v -> Boolean.TRUE.equals(v.getValue())) - .map(HistoricVariableInstance::getProcessInstanceId) - .collect(Collectors.toSet()); - } catch (Exception e) { - log.warn("批量查询驳回状态失败,降级为逐条查询: {}", e.getMessage()); - return processInstanceIds.stream() - .filter(this::isRejectToStarter) - .collect(Collectors.toSet()); - } - } - - private WithdrawState buildWithdrawState(ProcessInstanceRecord processInstanceRecord, Boolean rejectToStarter, - List assignUsers) { - if (!Objects.equals(processInstanceRecord.getStatus(), NodeStatusEnum.JXZ.getCode())) { - return new WithdrawState(false, getFinishedProcessReason(processInstanceRecord.getFinishReason())); - } - - if (Boolean.TRUE.equals(rejectToStarter)) { - return new WithdrawState(false, "驳回待提交状态不可撤回"); - } - - boolean hasProcessedApproval = assignUsers.stream().anyMatch(this::isProcessedAssignUser); - if (hasProcessedApproval) { - return new WithdrawState(false, "首个审批节点已处理,无法撤回"); - } - - return new WithdrawState(true, null); - } - - private boolean isProcessedAssignUser(ProcessNodeRecordAssignUser assignUser) { - return assignUser.getEndTime() != null - || !Objects.equals(assignUser.getStatus(), NodeStatusEnum.JXZ.getCode()); - } - - private String getFinishedProcessReason(String finishReason) { - if (StrUtil.equals(finishReason, ProcessInstanceConstant.FINISH_REASON_WITHDRAW)) { - return "流程已撤回"; - } - if (StrUtil.equals(finishReason, ProcessInstanceConstant.FINISH_REASON_TERMINATE)) { - return "流程已终止"; - } - if (StrUtil.equals(finishReason, ProcessInstanceConstant.FINISH_REASON_PASS)) { - return "流程已通过"; - } - if (StrUtil.equals(finishReason, ProcessInstanceConstant.FINISH_REASON_REFUSE)) { - return "流程已拒绝"; - } - return "流程已结束"; - } - - private static final class WithdrawState { - - private final boolean canWithdraw; - - private final String disabledReason; - - private WithdrawState(boolean canWithdraw, String disabledReason) { - this.canWithdraw = canWithdraw; - this.disabledReason = disabledReason; - } - } + private final RemoteUserService userService; + + private final TaskService taskService; + + private final RuntimeService runtimeService; + + private final HistoryService historyService; + + private final IProcessInstanceRecordService processInstanceRecordService; + + private final IProcessCopyService processCopyService; + + private final IProcessService processService; + + private final IProcessNodeRecordService processNodeRecordService; + + private final IProcessNodeRecordAssignUserService processNodeRecordAssignUserService; + + private final ObjectMapper objectMapper; + + private final IProcessNodeDataService nodeDataService; + + /** + * 启动流程 + *

+ * 根据流程定义启动一个新的流程实例。该方法会: 1. 获取当前登录用户作为流程发起人 2. 设置流程发起人信息到流程变量中 3. 调用Flowable引擎启动流程 + *

+ * @param processInstanceParamDto 流程启动参数,包含: - flowId: 流程定义ID - paramMap: 流程变量 + * @return R 启动结果,包含流程实例ID等信息 + */ + @Override + public R startProcessInstance(ProcessInstanceParamDto processInstanceParamDto) { + + // 获取当前登录用户 + PigxUser user = SecurityUtils.getUser(); + + // 设置流程发起人ID + processInstanceParamDto.setStartUserId(String.valueOf(user.getId())); + Map paramMap = processInstanceParamDto.getParamMap(); + + // 将发起人信息作为root变量存入流程,用于流程节点的权限判断 + Dict rootUser = Dict.create() + .set("id", user.getId()) + .set("name", user.getUsername()) + .set("type", NodeUserTypeEnum.USER.getKey()); + paramMap.put("root", CollUtil.newArrayList(rootUser)); + + // 调用引擎启动流程 + Authentication.setAuthenticatedUserId(processInstanceParamDto.getStartUserId()); + ProcessInstance processInstance = runtimeService.startProcessInstanceByKeyAndTenantId( + processInstanceParamDto.getFlowId(), processInstanceParamDto.getParamMap(), + TenantContextHolder.getTenantId().toString()); + + String processInstanceId = processInstance.getProcessInstanceId(); + return R.ok(processInstanceId); + } + + /** + * 查询当前登录用户的待办任务 + *

+ * 查询分配给当前用户的待办任务列表,并补充以下信息: 1. 流程实例的详细信息(流程名称、分组等) 2. 流程发起人信息(姓名、头像等) 3. 流程启动时间 + *

+ * @param taskQueryParamDto 查询参数,支持分页和条件过滤 + * @return R 包含分页后的待办任务列表 + */ + @Override + public R queryMineTask(TaskQueryParamDto taskQueryParamDto) { + taskQueryParamDto.setAssign(SecurityUtils.getUser().getId().toString()); + + String assign = taskQueryParamDto.getAssign(); + + List taskDtoList = new ArrayList<>(); + + int pageIndex = taskQueryParamDto.getCurrent() - 1; + int pageSize = taskQueryParamDto.getSize(); + + Page pageResultDto = new Page<>(); + TaskQuery taskQuery = taskService.createTaskQuery().taskAssignee(assign).orderByTaskCreateTime().desc(); + if (StrUtil.isNotBlank(taskQueryParamDto.getProcessName())) { + List processInstanceList = runtimeService.createProcessInstanceQuery() + .processDefinitionName(taskQueryParamDto.getProcessName()) + .processInstanceTenantId(TenantContextHolder.getTenantId().toString()) + .list(); + if (CollUtil.isNotEmpty(processInstanceList)) { + taskQuery.processInstanceIdIn( + processInstanceList.stream().map(ProcessInstance::getProcessInstanceId).toList()); + } + } + + if (ArrayUtil.isNotEmpty(taskQueryParamDto.getTaskTime())) { + ZoneId zoneId = ZoneId.systemDefault(); + ZonedDateTime zonedBeforeDateTime = taskQueryParamDto.getTaskTime()[0].atZone(zoneId); + ZonedDateTime zonedAfterDateTime = taskQueryParamDto.getTaskTime()[1].atZone(zoneId); + Date beforeDate = Date.from(zonedBeforeDateTime.toInstant()); + Date afterDate = Date.from(zonedAfterDateTime.toInstant()); + taskQuery.taskCreatedBefore(afterDate).taskCreatedAfter(beforeDate); + } + + taskQuery.listPage(pageIndex * pageSize, pageSize).forEach(task -> { + String taskId = task.getId(); + String processInstanceId = task.getProcessInstanceId(); + log.debug("(taskId) {} processInstanceId={} executrionId={}", task.getName(), processInstanceId, + task.getExecutionId()); + + String taskDefinitionKey = task.getTaskDefinitionKey(); + String processDefinitionId = task.getProcessDefinitionId(); + String flowId = NodeUtil.getFlowId(processDefinitionId); + + TaskDto taskDto = new TaskDto(); + taskDto.setFlowId(flowId); + taskDto.setTaskCreateTime(task.getCreateTime()); + taskDto.setNodeId(taskDefinitionKey); + taskDto.setProcessInstanceId(processInstanceId); + taskDto.setTaskId(taskId); + taskDto.setAssign(task.getAssignee()); + taskDto.setTaskName(task.getName()); + taskDtoList.add(taskDto); + }); + + long count = taskQuery.count(); + + log.debug("当前有 {} 个任务:", count); + + pageResultDto.setTotal(count); + pageResultDto.setRecords(taskDtoList); + + if (CollUtil.isEmpty(taskDtoList)) { + return R.ok(); + } + + Set processInstanceIdSet = taskDtoList.stream() + .map(TaskDto::getProcessInstanceId) + .collect(Collectors.toSet()); + + // 流程实例记录 + List processInstanceRecordList = processInstanceRecordService.lambdaQuery() + .in(ProcessInstanceRecord::getProcessInstanceId, processInstanceIdSet) + .list(); + + // 发起人 + List collect = processInstanceRecordList.stream().map(ProcessInstanceRecord::getUserId).toList(); + List startUserList = userService.getUserById(collect).getData(); + + List taskNewDtoList = new ArrayList<>(); + for (TaskDto record : taskDtoList) { + ProcessInstanceRecord processInstanceRecord = processInstanceRecordList.stream() + .filter(w -> StrUtil.equals(w.getProcessInstanceId(), record.getProcessInstanceId())) + .findAny() + .orElse(null); + + if (processInstanceRecord != null) { + record.setProcessName(processInstanceRecord.getName()); + SysUser startUser = startUserList.stream() + .filter(w -> w.getUserId().longValue() == processInstanceRecord.getUserId()) + .findAny() + .orElse(null); + + record.setRootUserId(processInstanceRecord.getUserId()); + record.setGroupName(processInstanceRecord.getGroupName()); + record.setRootUserName(startUser.getUsername()); + record.setRootUserAvatarUrl(startUser.getAvatar()); + record.setStartTime(processInstanceRecord.getCreateTime()); + + taskNewDtoList.add(record); + } + } + + pageResultDto.setRecords(taskNewDtoList); + + return R.ok(pageResultDto); + } + + /** + * 查询已办任务 + *

+ * 查询当前用户已经完成的任务列表,包括: 1. 用户已审批的任务 2. 补充流程实例信息、发起人信息等 + *

+ * @param taskQueryParamDto 查询参数,支持分页和时间范围过滤 + * @return R 包含分页后的已办任务列表 + */ + @Override + public R queryMineEndTask(TaskQueryParamDto taskQueryParamDto) { + taskQueryParamDto.setAssign(SecurityUtils.getUser().getId().toString()); + + HistoricActivityInstanceQuery historicActivityInstanceQuery = historyService + .createHistoricActivityInstanceQuery(); + HistoricActivityInstanceQuery activityInstanceQuery = historicActivityInstanceQuery + .taskAssignee(taskQueryParamDto.getAssign()) + .finished() + .orderByHistoricActivityInstanceEndTime() + .desc(); + + if (ArrayUtil.isNotEmpty(taskQueryParamDto.getTaskTime())) { + ZoneId zoneId = ZoneId.systemDefault(); + ZonedDateTime zonedBeforeDateTime = taskQueryParamDto.getTaskTime()[0].atZone(zoneId); + ZonedDateTime zonedAfterDateTime = taskQueryParamDto.getTaskTime()[1].atZone(zoneId); + Date beforeDate = Date.from(zonedBeforeDateTime.toInstant()); + Date afterDate = Date.from(zonedAfterDateTime.toInstant()); + activityInstanceQuery.finishedBefore(afterDate).finishedAfter(beforeDate); + } + + List list = activityInstanceQuery + .listPage((taskQueryParamDto.getCurrent() - 1) * taskQueryParamDto.getSize(), taskQueryParamDto.getSize()); + + long count = activityInstanceQuery.count(); + List taskDtoList = new ArrayList<>(); + + for (HistoricActivityInstance historicActivityInstance : list) { + String activityId = historicActivityInstance.getActivityId(); + String activityName = historicActivityInstance.getActivityName(); + String executionId = historicActivityInstance.getExecutionId(); + String taskId = historicActivityInstance.getTaskId(); + Date startTime = historicActivityInstance.getStartTime(); + Date endTime = historicActivityInstance.getEndTime(); + Long durationInMillis = historicActivityInstance.getDurationInMillis(); + String processInstanceId = historicActivityInstance.getProcessInstanceId(); + + String processDefinitionId = historicActivityInstance.getProcessDefinitionId(); + // 流程id + String flowId = NodeUtil.getFlowId(processDefinitionId); + + TaskDto taskDto = new TaskDto(); + taskDto.setFlowId(flowId); + taskDto.setTaskCreateTime(startTime); + taskDto.setTaskEndTime(endTime); + taskDto.setNodeId(activityId); + taskDto.setExecutionId(executionId); + taskDto.setProcessInstanceId(processInstanceId); + taskDto.setDurationInMillis(durationInMillis); + taskDto.setTaskId(taskId); + taskDto.setAssign(historicActivityInstance.getAssignee()); + taskDto.setTaskName(activityName); + + taskDtoList.add(taskDto); + } + + Page pageResultDto = new Page<>(); + pageResultDto.setTotal(count); + pageResultDto.setRecords(taskDtoList); + + List records = pageResultDto.getRecords(); + if (CollUtil.isEmpty(records)) { + return R.ok(pageResultDto); + + } + + Set processInstanceIdSet = records.stream() + .map(TaskDto::getProcessInstanceId) + .collect(Collectors.toSet()); + + // 流程实例记录 + List processInstanceRecordList = processInstanceRecordService.lambdaQuery() + .in(ProcessInstanceRecord::getProcessInstanceId, processInstanceIdSet) + .list(); + + // 发起人 + List collect = processInstanceRecordList.stream().map(ProcessInstanceRecord::getUserId).toList(); + List startUserList = userService.getUserById(collect).getData(); + + List taskNewDtoList = new ArrayList<>(); + for (TaskDto record : records) { + + ProcessInstanceRecord processInstanceRecord = processInstanceRecordList.stream() + .filter(w -> StrUtil.equals(w.getProcessInstanceId(), record.getProcessInstanceId())) + .findAny() + .orElse(null); + + if (processInstanceRecord != null) { + + record.setProcessName(processInstanceRecord.getName()); + + SysUser startUser = startUserList.stream() + .filter(w -> w.getUserId().longValue() == processInstanceRecord.getUserId()) + .findAny() + .orElse(null); + + record.setRootUserId(processInstanceRecord.getUserId()); + record.setGroupName(processInstanceRecord.getGroupName()); + record.setRootUserName(startUser.getName()); + record.setRootUserAvatarUrl(startUser.getAvatar()); + record.setStartTime(processInstanceRecord.getCreateTime()); + taskNewDtoList.add(record); + } + } + + pageResultDto.setRecords(taskNewDtoList); + + return R.ok(pageResultDto); + } + + /** + * 结束流程实例 + *

+ * 手动结束一个流程实例,通常用于: 1. 流程审批完成,正常结束 2. 流程被终止或取消 + *

+ * 该方法会更新流程实例记录的结束时间、状态和结束原因 + *

+ * @param processInstanceParamDto 流程实例参数,包含: - processInstanceId: 流程实例ID - paramMap: + * 可能包含approve_condition等结束原因 + * @return R 操作结果 + */ + @Override + public R end(ProcessInstanceParamDto processInstanceParamDto) { + + // 拒绝结束时会写入明确的结束原因;正常流转到结束节点时默认为通过 + Map paramMap = Optional.ofNullable(processInstanceParamDto.getParamMap()) + .orElseGet(Collections::emptyMap); + String finishReason = MapUtil.getStr(paramMap, ProcessInstanceConstant.FINISH_REASON_VAR); + if (StrUtil.isBlank(finishReason)) { + finishReason = ProcessInstanceConstant.FINISH_REASON_PASS; + } + + // 构建更新条件 + LambdaUpdateChainWrapper lambdaUpdateChainWrapper = processInstanceRecordService + .lambdaUpdate() + .set(ProcessInstanceRecord::getEndTime, new Date()) + .set(ProcessInstanceRecord::getStatus, NodeStatusEnum.YJS.getCode()) + .set(ProcessInstanceRecord::getFinishReason, finishReason); + + // 执行更新 + lambdaUpdateChainWrapper + .eq(ProcessInstanceRecord::getProcessInstanceId, processInstanceParamDto.getProcessInstanceId()) + .update(new ProcessInstanceRecord()); + return R.ok(); + } + + /** + * 查询我发起的流程 + *

+ * 查询当前用户发起的所有流程实例,支持: 1. 按状态过滤(进行中、已完成等) 2. 按时间范围过滤 3. 分页查询 + *

+ * @param taskQueryParamDto 查询条件,包含状态、时间范围、分页参数 + * @return R 包含分页后的流程实例列表 + */ + @Override + public R queryMineStarted(TaskQueryParamDto taskQueryParamDto) { + + long userId = SecurityUtils.getUser().getId(); + + Page instanceRecordPage = processInstanceRecordService.lambdaQuery() + .eq(ProcessInstanceRecord::getUserId, userId) + .eq(ProcessInstanceRecord::getStatus, taskQueryParamDto.getStatus()) + .between(ArrayUtil.isNotEmpty(taskQueryParamDto.getTaskTime()), ProcessInstanceRecord::getCreateTime, + ArrayUtil.isNotEmpty(taskQueryParamDto.getTaskTime()) ? taskQueryParamDto.getTaskTime()[0] : null, + ArrayUtil.isNotEmpty(taskQueryParamDto.getTaskTime()) ? taskQueryParamDto.getTaskTime()[1] : null) + .orderByDesc(ProcessInstanceRecord::getCreateTime) + .page(new Page<>(taskQueryParamDto.getCurrent(), taskQueryParamDto.getSize())); + + // 转换为 VO 并添加驳回状态信息 + List voList = BeanUtil.copyToList(instanceRecordPage.getRecords(), + ProcessInstanceRecordVo.class); + + // 批量查询流程定义,用于检查打印模板 + Set flowIds = voList.stream() + .map(ProcessInstanceRecordVo::getFlowId) + .filter(StrUtil::isNotBlank) + .collect(Collectors.toSet()); + List processList = CollUtil.isEmpty(flowIds) ? Collections.emptyList() + : processService.lambdaQuery().in(Process::getFlowId, flowIds).list(); + Set processInstanceIds = voList.stream() + .map(ProcessInstanceRecordVo::getProcessInstanceId) + .filter(StrUtil::isNotBlank) + .collect(Collectors.toSet()); + Map> assignUserMap = queryAssignUserMap(processInstanceIds); + + // 批量查询进行中流程的 rejectToStarter 变量,避免 N+1 + Set runningInstanceIds = voList.stream() + .filter(vo -> Objects.equals(vo.getStatus(), NodeStatusEnum.JXZ.getCode())) + .map(ProcessInstanceRecordVo::getProcessInstanceId) + .filter(StrUtil::isNotBlank) + .collect(Collectors.toSet()); + + Set rejectToStarterIds = batchQueryRejectToStarter(runningInstanceIds); + + // 批量查询驳回待提交的 root 任务,避免 N+1 + Map resubmitTaskMap = Collections.emptyMap(); + if (!rejectToStarterIds.isEmpty()) { + resubmitTaskMap = taskService.createTaskQuery() + .processInstanceIdIn(rejectToStarterIds) + .taskDefinitionKey("root") + .list() + .stream() + .collect(Collectors.toMap(Task::getProcessInstanceId, Task::getId, (a, b) -> a)); + } + + // 构建打印模板 Map,避免循环内 stream 遍历 + Map processMap = processList.stream() + .collect(Collectors.toMap(p -> p.getFlowId().toLowerCase(), p -> p, (a, b) -> a)); + + for (ProcessInstanceRecordVo vo : voList) { + boolean rejectToStarter = false; + if (Objects.equals(vo.getStatus(), NodeStatusEnum.JXZ.getCode())) { + rejectToStarter = rejectToStarterIds.contains(vo.getProcessInstanceId()); + vo.setRejectToStarter(rejectToStarter); + + if (rejectToStarter) { + vo.setResubmitTaskId(resubmitTaskMap.get(vo.getProcessInstanceId())); + } + } + else { + vo.setRejectToStarter(false); + } + + WithdrawState withdrawState = buildWithdrawState(vo, vo.getRejectToStarter(), + assignUserMap.getOrDefault(vo.getProcessInstanceId(), Collections.emptyList())); + vo.setCanWithdraw(withdrawState.canWithdraw); + vo.setWithdrawDisabledReason(withdrawState.disabledReason); + + // 检查是否配置了打印模板 + Process matchedProcess = processMap.get(vo.getFlowId().toLowerCase()); + if (matchedProcess != null) { + vo.setHasPrintTemplate(StrUtil.isNotBlank(matchedProcess.getPrintTemplate())); + } + } + + // 构建返回的分页对象 + Page resultPage = new Page<>(instanceRecordPage.getCurrent(), + instanceRecordPage.getSize(), instanceRecordPage.getTotal()); + resultPage.setRecords(voList); + + return R.ok(resultPage); + } + + /** + * 查询抄送给我的流程 + *

+ * 查询抄送给当前用户的流程列表。抄送功能允许流程参与者将流程信息 通知给其他相关人员,被抄送人可以查看流程但不参与审批 + *

+ * @param taskQueryParamDto 查询参数,支持时间范围过滤和分页 + * @return R 包含分页后的抄送记录,包括发起人信息 + */ + @Override + public R queryMineCC(TaskQueryParamDto taskQueryParamDto) { + + long userId = SecurityUtils.getUser().getId(); + + Page page = processCopyService.lambdaQuery() + .eq(ProcessCopy::getUserId, userId) + .between(ArrayUtil.isNotEmpty(taskQueryParamDto.getTaskTime()), ProcessCopy::getCreateTime, + ArrayUtil.isNotEmpty(taskQueryParamDto.getTaskTime()) ? taskQueryParamDto.getTaskTime()[0] : null, + ArrayUtil.isNotEmpty(taskQueryParamDto.getTaskTime()) ? taskQueryParamDto.getTaskTime()[1] : null) + .orderByDesc(ProcessCopy::getNodeTime) + .page(new Page<>(taskQueryParamDto.getCurrent(), taskQueryParamDto.getSize())); + + List records = page.getRecords(); + + List processCopyVoList = BeanUtil.copyToList(records, ProcessCopyVo.class); + + if (CollUtil.isNotEmpty(records)) { + // 发起人 + List userIdList = records.stream().map(ProcessCopy::getStartUserId).toList(); + List startUserList = userService.getUserById(userIdList).getData(); + + for (ProcessCopyVo record : processCopyVoList) { + SysUser startUser = startUserList.stream() + .filter(w -> w.getUserId().longValue() == record.getStartUserId()) + .findAny() + .orElse(null); + record.setStartUserName(startUser.getUsername()); + } + } + + Page p = BeanUtil.copyProperties(page, Page.class); + p.setRecords(processCopyVoList); + return R.ok(p); + } + + /** + * 格式化流程显示 + *

+ * 将流程定义数据格式化为前端可展示的节点结构,主要功能: 1. 解析流程定义的JSON数据 2. 标记已完成的节点 3. 根据流程变量计算条件节点的显示状态 4. + * 处理节点权限和表单权限 + *

+ * 该方法用于流程图的可视化展示 + *

+ * @param nodeFormatParamVo 格式化参数,包含: - flowId: 流程定义ID - processInstanceId: 流程实例ID - + * taskId: 任务ID(可选) - paramMap: 流程变量 + * @return R 包含格式化后的节点列表 + */ + @SneakyThrows + @Override + public R formatStartNodeShow(NodeFormatParamVo nodeFormatParamVo) { + String flowId = nodeFormatParamVo.getFlowId(); + String processInstanceId = nodeFormatParamVo.getProcessInstanceId(); + if (StrUtil.isAllBlank(flowId, processInstanceId)) { + return R.ok(new ArrayList<>()); + } + + if (StrUtil.isBlankIfStr(flowId) && StrUtil.isNotBlank(processInstanceId)) { + ProcessInstanceRecord processInstanceRecord = processInstanceRecordService.lambdaQuery() + .eq(ProcessInstanceRecord::getProcessInstanceId, processInstanceId) + .one(); + flowId = processInstanceRecord.getFlowId(); + + } + Map paramMap = nodeFormatParamVo.getParamMap(); + if (StrUtil.isNotBlank(nodeFormatParamVo.getTaskId())) { + VariableQueryParamDto variableQueryParamDto = new VariableQueryParamDto(); + variableQueryParamDto.setTaskId(nodeFormatParamVo.getTaskId()); + Map variables; + List keyList = variableQueryParamDto.getKeyList(); + if (CollUtil.isEmpty(keyList)) { + TaskQuery taskQuery = taskService.createTaskQuery(); + + Task task = taskQuery.taskId(variableQueryParamDto.getTaskId()).singleResult(); + if (task == null) { + return R.failed("任务不存在"); + } + + variables = runtimeService.getVariables(task.getExecutionId()); + } + else { + variables = taskService.getVariables(variableQueryParamDto.getTaskId(), keyList); + } + paramMap.putAll(variables); + } + + Set completeNodeSet = new HashSet<>(); + Set continueNodeSet = new HashSet<>(); + if (StrUtil.isNotBlank(processInstanceId)) { + List processNodeRecordList = processNodeRecordService.lambdaQuery() + .eq(ProcessNodeRecord::getProcessInstanceId, processInstanceId) + .eq(ProcessNodeRecord::getStatus, NodeStatusEnum.YJS.getCode()) + .list(); + Set collect = processNodeRecordList.stream() + .map(ProcessNodeRecord::getNodeId) + .collect(Collectors.toSet()); + + List jxzNodeRecordList = processNodeRecordService.lambdaQuery() + .eq(ProcessNodeRecord::getProcessInstanceId, processInstanceId) + .eq(ProcessNodeRecord::getStatus, NodeStatusEnum.JXZ.getCode()) + .list(); + + Set jxzCollect = jxzNodeRecordList.stream() + .map(ProcessNodeRecord::getNodeId) + .collect(Collectors.toSet()); + continueNodeSet.addAll(jxzCollect); + completeNodeSet.addAll(collect); + } + + Process oaForms = processService.getByFlowId(flowId); + String process = oaForms.getProcess(); + Node nodeDto = objectMapper.readValue(process, new TypeReference<>() { + }); + + List processNodeShowDtos = NodeFormatUtil.formatProcessNodeShow(nodeDto, completeNodeSet, + continueNodeSet, processInstanceId, paramMap); + + return R.ok(processNodeShowDtos); + } + + /** + * 流程详情 + *

+ * 查询流程实例的详细信息,包括: 1. 流程定义信息(流程结构) 2. 表单项定义 3. 表单数据(用户填写的数据) 4. 表单权限配置 5. 流程状态信息 + *

+ * 该方法主要用于流程详情页面的数据展示 + *

+ * @param processInstanceId 流程实例ID + * @return R 包含流程详情的Dict对象 + * @throws Exception 当解析JSON数据失败时抛出异常 + */ + @SneakyThrows + @Override + public R detail(String processInstanceId) { + ProcessInstanceRecord processInstanceRecord = processInstanceRecordService.lambdaQuery() + .eq(ProcessInstanceRecord::getProcessInstanceId, processInstanceId) + .one(); + + Process oaForms = processService.getByFlowId(processInstanceRecord.getFlowId()); + if (oaForms == null) { + return R.failed("流程不存在"); + } + + // 发起人变量数据 + String formData = processInstanceRecord.getFormData(); + var running = runtimeService.createProcessInstanceQuery().processInstanceId(processInstanceId).singleResult(); + String definitionId; + if (Objects.nonNull(running)) { + definitionId = running.getProcessDefinitionId(); + } + else { + var historic = historyService.createHistoricProcessInstanceQuery() + .processInstanceId(processInstanceId) + .singleResult(); + if (Objects.isNull(historic)) { + return R.failed("流程实例历史不存在,无法确定节点配置版本"); + } + definitionId = historic.getProcessDefinitionId(); + } + Node nodeDto = nodeDataService.getPublishedNode(definitionId, "root").getData(); + Map formPerms1 = NodeUtil.publishedFormPermissions(nodeDto, oaForms.getFormItems()); + boolean rejectToStarter = Objects.equals(processInstanceRecord.getStatus(), NodeStatusEnum.JXZ.getCode()) + && isRejectToStarter(processInstanceId); + WithdrawState withdrawState = buildWithdrawState(processInstanceRecord, rejectToStarter, + queryAssignUserMap(Collections.singleton(processInstanceId)).getOrDefault(processInstanceId, + Collections.emptyList())); + + Dict set = Dict.create() + .set(ProcessInstanceRecord.Fields.processInstanceId, processInstanceId) + .set(ProcessInstanceRecord.Fields.status, processInstanceRecord.getStatus()) + .set("finishReason", processInstanceRecord.getFinishReason()) + .set("rejectToStarter", rejectToStarter) + .set("canWithdraw", withdrawState.canWithdraw) + .set("withdrawDisabledReason", withdrawState.disabledReason) + .set("process", oaForms.getProcess()) + .set("formItems", oaForms.getFormItems()) + .set("formData", formData) + .set("formConfig", oaForms.getFormConfig()) + .set("formPerms", formPerms1); + + return R.ok(set); + } + + private Map> queryAssignUserMap(Set processInstanceIds) { + if (CollUtil.isEmpty(processInstanceIds)) { + return Collections.emptyMap(); + } + + return processNodeRecordAssignUserService.lambdaQuery() + .in(ProcessNodeRecordAssignUser::getProcessInstanceId, processInstanceIds) + .ne(ProcessNodeRecordAssignUser::getNodeId, "root") + .list() + .stream() + .collect(Collectors.groupingBy(ProcessNodeRecordAssignUser::getProcessInstanceId)); + } + + private boolean isRejectToStarter(String processInstanceId) { + try { + Object rejectVar = runtimeService.getVariable(processInstanceId, + ProcessInstanceConstant.REJECT_TO_STARTER_VAR); + return Boolean.TRUE.equals(rejectVar); + } + catch (Exception e) { + log.debug("查询驳回待提交状态失败: processInstanceId={}, error={}", processInstanceId, e.getMessage()); + return false; + } + } + + /** + * 批量查询流程实例的驳回到发起人状态 + * @param processInstanceIds 流程实例ID集合 + * @return 处于驳回待提交状态的流程实例ID集合 + */ + private Set batchQueryRejectToStarter(Set processInstanceIds) { + if (CollUtil.isEmpty(processInstanceIds)) { + return Collections.emptySet(); + } + try { + return historyService.createHistoricVariableInstanceQuery() + .processInstanceIds(processInstanceIds) + .variableName(ProcessInstanceConstant.REJECT_TO_STARTER_VAR) + .list() + .stream() + .filter(v -> Boolean.TRUE.equals(v.getValue())) + .map(HistoricVariableInstance::getProcessInstanceId) + .collect(Collectors.toSet()); + } + catch (Exception e) { + log.warn("批量查询驳回状态失败,降级为逐条查询: {}", e.getMessage()); + return processInstanceIds.stream().filter(this::isRejectToStarter).collect(Collectors.toSet()); + } + } + + private WithdrawState buildWithdrawState(ProcessInstanceRecord processInstanceRecord, Boolean rejectToStarter, + List assignUsers) { + if (!Objects.equals(processInstanceRecord.getStatus(), NodeStatusEnum.JXZ.getCode())) { + return new WithdrawState(false, getFinishedProcessReason(processInstanceRecord.getFinishReason())); + } + + if (Boolean.TRUE.equals(rejectToStarter)) { + return new WithdrawState(false, "驳回待提交状态不可撤回"); + } + + boolean hasProcessedApproval = assignUsers.stream().anyMatch(this::isProcessedAssignUser); + if (hasProcessedApproval) { + return new WithdrawState(false, "首个审批节点已处理,无法撤回"); + } + + return new WithdrawState(true, null); + } + + private boolean isProcessedAssignUser(ProcessNodeRecordAssignUser assignUser) { + return assignUser.getEndTime() != null || !Objects.equals(assignUser.getStatus(), NodeStatusEnum.JXZ.getCode()); + } + + private String getFinishedProcessReason(String finishReason) { + if (StrUtil.equals(finishReason, ProcessInstanceConstant.FINISH_REASON_WITHDRAW)) { + return "流程已撤回"; + } + if (StrUtil.equals(finishReason, ProcessInstanceConstant.FINISH_REASON_TERMINATE)) { + return "流程已终止"; + } + if (StrUtil.equals(finishReason, ProcessInstanceConstant.FINISH_REASON_PASS)) { + return "流程已通过"; + } + if (StrUtil.equals(finishReason, ProcessInstanceConstant.FINISH_REASON_REFUSE)) { + return "流程已拒绝"; + } + return "流程已结束"; + } + + private static final class WithdrawState { + + private final boolean canWithdraw; + + private final String disabledReason; + + private WithdrawState(boolean canWithdraw, String disabledReason) { + this.canWithdraw = canWithdraw; + this.disabledReason = disabledReason; + } + + } } diff --git a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/impl/ProcessNodeDataServiceImpl.java b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/impl/ProcessNodeDataServiceImpl.java index 44a0d0413..15ebb51e4 100644 --- a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/impl/ProcessNodeDataServiceImpl.java +++ b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/impl/ProcessNodeDataServiceImpl.java @@ -1,74 +1,144 @@ package com.pig4cloud.pigx.flow.service.impl; -import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONUtil; -import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.pig4cloud.pigx.common.core.util.R; +import com.pig4cloud.pigx.common.data.tenant.TenantContextHolder; import com.pig4cloud.pigx.flow.dto.Node; import com.pig4cloud.pigx.flow.dto.ProcessNodeDataDto; import com.pig4cloud.pigx.flow.entity.ProcessNodeData; import com.pig4cloud.pigx.flow.mapper.ProcessNodeDataMapper; import com.pig4cloud.pigx.flow.service.IProcessNodeDataService; +import lombok.RequiredArgsConstructor; +import org.flowable.engine.RepositoryService; +import org.flowable.engine.repository.ProcessDefinition; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; -import java.util.Optional; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; -/** - * 流程节点数据服务实现类 - *

- * 该类负责管理流程节点的详细数据,主要功能包括: 1. 保存流程节点的配置数据(审批人、表单权限、条件等) 2. 查询指定节点的详细配置信息 3. - * 为流程执行提供节点配置数据支持 - * - * 节点数据以JSON格式存储,包含节点类型、审批人配置、表单权限等信息 - *

- * - * @author Vincent - * @since 2023-05-07 - */ +/** 已发布节点配置按引擎定义 ID 隔离,不允许原地修改。 */ @Service +@RequiredArgsConstructor public class ProcessNodeDataServiceImpl extends ServiceImpl implements IProcessNodeDataService { + private final RepositoryService repositoryService; + /** - * 保存流程节点数据 - *

- * 将流程节点的配置信息保存到数据库。节点数据包括: - 节点类型(审批、抄送、条件等) - 审批人配置 - 表单权限设置 - 条件表达式等 - *

- * @param processNodeDataDto 流程节点数据DTO,包含flowId、nodeId和data(JSON格式的节点配置) - * @return R 保存结果 + * {@inheritDoc} */ @Override public R saveNodeData(ProcessNodeDataDto processNodeDataDto) { - ProcessNodeData processNodeData = BeanUtil.copyProperties(processNodeDataDto, ProcessNodeData.class); - baseMapper.delete(Wrappers.lambdaQuery() - .eq(ProcessNodeData::getNodeId, processNodeData.getNodeId()) - .eq(ProcessNodeData::getFlowId, processNodeData.getFlowId())); - baseMapper.insert(processNodeData); - return R.ok(); + return R.failed("不允许直接修改已发布节点配置,请编辑流程后重新发布"); } /** - * 获取节点数据 - *

- * 根据流程ID和节点ID查询节点的详细配置信息。 节点数据存储为JSON格式,查询后会自动转换为Node对象 - *

- * @param flowId 流程ID - * @param nodeId 节点ID - * @return R 包含节点配置的Node对象,如果节点不存在则返回null + * {@inheritDoc} */ @Override public R getNodeData(String flowId, String nodeId) { - Optional processNodeDataOptional = this.lambdaQuery() - .eq(ProcessNodeData::getFlowId, flowId) + ProcessDefinition definition = repositoryService.createProcessDefinitionQuery() + .processDefinitionKey(flowId) + .processDefinitionTenantId(String.valueOf(TenantContextHolder.getTenantId())) + .latestVersion() + .singleResult(); + if (Objects.isNull(definition)) { + return R.ok(null); + } + return getPublishedNode(definition.getId(), nodeId); + } + + /** + * {@inheritDoc} + */ + @Override + public R getPublishedNode(String processDefinitionId, String nodeId) { + if (StrUtil.hasBlank(processDefinitionId, nodeId)) { + throw new IllegalArgumentException("查询已发布节点必须指定流程定义 ID 和节点 ID"); + } + ProcessNodeData data = this.lambdaQuery() + .eq(ProcessNodeData::getProcessDefinitionId, processDefinitionId) .eq(ProcessNodeData::getNodeId, nodeId) - .oneOpt(); + .one(); + if (Objects.nonNull(data)) { + return R.ok(JSONUtil.toBean(data.getData(), Node.class)); + } + // start、网关汇聚及审批后的服务任务由编译器生成,没有业务节点配置。 + ProcessNodeData root = this.lambdaQuery() + .eq(ProcessNodeData::getProcessDefinitionId, processDefinitionId) + .eq(ProcessNodeData::getNodeId, "root") + .one(); + if (Objects.isNull(root)) { + throw new IllegalStateException("流程定义 " + processDefinitionId + " 缺少节点版本快照,请先核对并迁移存量配置"); + } + if (containsNode(JSONUtil.toBean(root.getData(), Node.class), nodeId)) { + throw new IllegalStateException("流程定义 " + processDefinitionId + " 缺少节点版本快照:" + nodeId); + } + return R.ok(null); + } - Node node = processNodeDataOptional - .map(processNodeData -> JSONUtil.toBean(processNodeData.getData(), Node.class)) - .orElse(null); + private boolean containsNode(Node node, String nodeId) { + if (Objects.isNull(node)) { + return false; + } + if (nodeId.equals(node.getId())) { + return true; + } + if (Objects.nonNull(node.getConditionNodes())) { + for (Node branch : node.getConditionNodes()) { + if (containsNode(branch, nodeId)) { + return true; + } + } + } + return containsNode(node.getChildren(), nodeId); + } - return R.ok(node); + /** + * {@inheritDoc} + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void savePublishedNodes(ProcessDefinition definition, Node root) { + if (Objects.isNull(definition) || Objects.isNull(root) || !"root".equals(root.getId())) { + throw new IllegalArgumentException("发布流程缺少定义或发起节点"); + } + List nodes = new ArrayList<>(); + collectNodes(definition, root, new HashSet<>(), nodes); + // 数据库唯一约束防止同一版本被重复写入,禁止删除或覆盖已有快照。 + for (ProcessNodeData node : nodes) { + if (baseMapper.insert(node) != 1) { + throw new IllegalStateException("保存节点版本快照失败"); + } + } + } + + private void collectNodes(ProcessDefinition definition, Node node, Set ids, List nodes) { + if (Objects.isNull(node) || StrUtil.isBlank(node.getId())) { + return; + } + if (!ids.add(node.getId())) { + throw new IllegalArgumentException("流程节点 ID 重复:" + node.getId()); + } + ProcessNodeData data = new ProcessNodeData(); + data.setProcessDefinitionId(definition.getId()); + data.setFlowId(definition.getKey()); + data.setTenantId(Long.valueOf(definition.getTenantId())); + data.setNodeId(node.getId()); + data.setData(JSONUtil.toJsonStr(node)); + nodes.add(data); + if (Objects.nonNull(node.getConditionNodes())) { + for (Node branch : node.getConditionNodes()) { + collectNodes(definition, branch, ids, nodes); + } + } + collectNodes(definition, node.getChildren(), ids, nodes); } } diff --git a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/impl/ProcessServiceImpl.java b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/impl/ProcessServiceImpl.java index 228742460..a3d47dcf8 100644 --- a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/impl/ProcessServiceImpl.java +++ b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/impl/ProcessServiceImpl.java @@ -26,6 +26,7 @@ import com.pig4cloud.pigx.flow.entity.ProcessStarter; import com.pig4cloud.pigx.flow.event.FlowStatusEventPublisher; import com.pig4cloud.pigx.flow.mapper.ProcessMapper; import com.pig4cloud.pigx.flow.service.IProcessService; +import com.pig4cloud.pigx.flow.service.IProcessNodeDataService; import com.pig4cloud.pigx.flow.service.IProcessStarterService; import com.pig4cloud.pigx.flow.support.utils.ModelUtil; import com.pig4cloud.pigx.flow.support.utils.NodeUtil; @@ -39,6 +40,7 @@ import org.flowable.bpmn.model.BpmnModel; import org.flowable.engine.RepositoryService; import org.flowable.engine.repository.Deployment; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.util.Date; import java.util.List; @@ -60,270 +62,272 @@ import java.util.Objects; @RequiredArgsConstructor public class ProcessServiceImpl extends ServiceImpl implements IProcessService { - private final IProcessStarterService processStarterService; + private final IProcessStarterService processStarterService; - private final RepositoryService repositoryService; + private final RepositoryService repositoryService; - private final ObjectMapper objectMapper; + private final IProcessNodeDataService processNodeDataService; - private final Sequence flowSequence; + private final ObjectMapper objectMapper; - private final FlowStatusEventPublisher flowStatusEventPublisher; + private final Sequence flowSequence; - /** - * 获取流程详细信息 - *

- * 根据流程ID获取完整的流程配置信息,包括: - 流程基本信息 - 表单配置(自动生成序列号字段的值) - 需要发起人选择执行人的节点列表 - 表单权限配置 - *

- * - * @param flowId 流程定义ID - * @return 包含流程详细信息的响应对象 - */ - @Override - public R getDetail(String flowId) { - // 获取流程详情 - ProcessVO processVO = this.getProcessVO(flowId); - return R.ok(processVO); - } + private final FlowStatusEventPublisher flowStatusEventPublisher; - /** - * 获取流程视图对象 - *

- * 内部方法,用于构建流程的视图对象。主要处理: - 解析流程节点结构 - 查找需要发起人选择的节点 - 处理序列号类型的表单字段,自动生成下一个序号 - - * 提取起始节点的表单权限配置 - *

- * - * @param flowId 流程定义ID - * @return 流程视图对象,包含所有展示所需的信息 - */ - @SneakyThrows - private ProcessVO getProcessVO(String flowId) { + /** + * 获取流程详细信息 + *

+ * 根据流程ID获取完整的流程配置信息,包括: - 流程基本信息 - 表单配置(自动生成序列号字段的值) - 需要发起人选择执行人的节点列表 - 表单权限配置 + *

+ * @param flowId 流程定义ID + * @return 包含流程详细信息的响应对象 + */ + @Override + public R getDetail(String flowId) { + // 获取流程详情 + ProcessVO processVO = this.getProcessVO(flowId); + return R.ok(processVO); + } - Process oaForms = getByFlowId(flowId); - String process = oaForms.getProcess(); - String formItems = oaForms.getFormItems(); - Node startNode = objectMapper.readValue(process, new TypeReference<>() { - }); + /** + * 获取流程视图对象 + *

+ * 内部方法,用于构建流程的视图对象。主要处理: - 解析流程节点结构 - 查找需要发起人选择的节点 - 处理序列号类型的表单字段,自动生成下一个序号 - + * 提取起始节点的表单权限配置 + *

+ * @param flowId 流程定义ID + * @return 流程视图对象,包含所有展示所需的信息 + */ + @SneakyThrows + private ProcessVO getProcessVO(String flowId) { - List selectUserNodeId = NodeUtil.selectUserNodeId(startNode); + Process oaForms = getByFlowId(flowId); + String process = oaForms.getProcess(); + String formItems = oaForms.getFormItems(); + Node startNode = objectMapper.readValue(process, new TypeReference<>() { + }); - ProcessVO processVO = BeanUtil.copyProperties(oaForms, ProcessVO.class); - processVO.setSelectUserNodeId(selectUserNodeId); + List selectUserNodeId = NodeUtil.selectUserNodeId(startNode); - JSONArray formItemJsonArray = JSONUtil.parseArray(formItems); - // 特殊字段复制 - for (Object object : formItemJsonArray) { - if (object instanceof JSONObject) { - JSONObject formVO = (JSONObject) object; - JSONObject props = formVO.getJSONObject(FormItemVO.Fields.props); - if (Objects.nonNull(props) - && FormTypeEnum.SEQUENCE.getType().equals(props.getStr(FormItemVO.Fields.type))) { - String nextNo = flowSequence.nextNo(); - props.set(FormItemVO.Props.Fields.value, nextNo); - formVO.set(FormItemVO.Props.Fields.value, nextNo); - } - } - } + ProcessVO processVO = BeanUtil.copyProperties(oaForms, ProcessVO.class); + processVO.setSelectUserNodeId(selectUserNodeId); - processVO.setFormItems(formItemJsonArray.toStringPretty()); - processVO.setFormPerms(startNode.getFormPerms()); - return processVO; - } + JSONArray formItemJsonArray = JSONUtil.parseArray(formItems); + // 特殊字段复制 + for (Object object : formItemJsonArray) { + if (object instanceof JSONObject) { + JSONObject formVO = (JSONObject) object; + JSONObject props = formVO.getJSONObject(FormItemVO.Fields.props); + if (Objects.nonNull(props) + && FormTypeEnum.SEQUENCE.getType().equals(props.getStr(FormItemVO.Fields.type))) { + String nextNo = flowSequence.nextNo(); + props.set(FormItemVO.Props.Fields.value, nextNo); + formVO.set(FormItemVO.Props.Fields.value, nextNo); + } + } + } - /** - * 根据流程ID查询流程定义 - *

- * 通过流程的唯一标识符查询流程定义实体。 注意:一个流程可能有多个版本,此方法返回当前有效版本。 - *

- * - * @param flowId 流程定义ID - * @return 流程定义实体,如果不存在返回null - */ - @Override - public Process getByFlowId(String flowId) { - return this.lambdaQuery().eq(Process::getFlowId, flowId).one(); - } + processVO.setFormItems(formItemJsonArray.toStringPretty()); + processVO.setFormPerms(startNode.getFormPerms()); + return processVO; + } - /** - * 根据流程ID更新流程定义 - *

- * 更新流程的配置信息,包括表单、节点、权限等。 更新不会影响已经运行的流程实例。 - *

- * - * @param process 包含更新信息的流程实体 - */ - @Override - public void updateByFlowId(Process process) { - this.lambdaUpdate().eq(Process::getFlowId, process.getFlowId()).update(process); - } + /** + * 根据流程ID查询流程定义 + *

+ * 通过流程的唯一标识符查询流程定义实体。 注意:一个流程可能有多个版本,此方法返回当前有效版本。 + *

+ * @param flowId 流程定义ID + * @return 流程定义实体,如果不存在返回null + */ + @Override + public Process getByFlowId(String flowId) { + return this.lambdaQuery().eq(Process::getFlowId, flowId).one(); + } - /** - * 隐藏流程定义 - *

- * 将流程标记为隐藏状态,隐藏的流程: - 不在流程列表中显示 - 不能创建新的流程实例 - 已有的流程实例可以继续执行 - *

- * - * @param flowId 要隐藏的流程ID - */ - @Override - public void hide(String flowId) { - this.lambdaUpdate() - .set(Process::getHidden, YesNoEnum.YES.getCode()) - .eq(Process::getFlowId, flowId) - .update(new Process()); - } + /** + * 根据流程ID更新流程定义 + *

+ * 更新流程的配置信息,包括表单、节点、权限等。 更新不会影响已经运行的流程实例。 + *

+ * @param process 包含更新信息的流程实体 + */ + @Override + public void updateByFlowId(Process process) { + this.lambdaUpdate().eq(Process::getFlowId, process.getFlowId()).update(process); + } - /** - * 创建新的流程定义 - *

- * 创建流程的完整流程: 1. 调用流程引擎创建BPMN模型 2. 如果是更新操作,隐藏旧版本 3. 保存流程定义到数据库 4. 设置流程发起人权限 5. - * 生成流程使用范围描述 - *

- * - * @param process 流程定义实体,包含流程的所有配置信息 - * @return 创建结果,成功返回流程ID,失败返回错误信息 - */ - @SneakyThrows - @Override - public R create(Process process) { - String flowId; - if (StrUtil.isNotBlank(process.getFlowId())) { - flowId = process.getFlowId(); - } else { - flowId = "P" + DateUtil.format(new Date(), DatePattern.PURE_DATE_PATTERN) - + RandomUtil.randomString(5).toUpperCase(); - } + /** + * 隐藏流程定义 + *

+ * 将流程标记为隐藏状态,隐藏的流程: - 不在流程列表中显示 - 不能创建新的流程实例 - 已有的流程实例可以继续执行 + *

+ * @param flowId 要隐藏的流程ID + */ + @Override + public void hide(String flowId) { + this.lambdaUpdate() + .set(Process::getHidden, YesNoEnum.YES.getCode()) + .eq(Process::getFlowId, flowId) + .update(new Process()); + } - BpmnModel bpmnModel = ModelUtil.buildBpmnModel(objectMapper.readValue(process.getProcess(), Node.class), - process.getName(), flowId); - { - byte[] bpmnBytess = new BpmnXMLConverter().convertToXML(bpmnModel); - String filename = "/tmp/flowable-deployment/" + flowId + ".bpmn20.xml"; - log.debug("部署时的模型文件:{}", filename); - FileUtil.writeBytes(bpmnBytess, filename); - } + /** + * 创建新的流程定义 + *

+ * 创建流程的完整流程: 1. 调用流程引擎创建BPMN模型 2. 如果是更新操作,隐藏旧版本 3. 保存流程定义到数据库 4. 设置流程发起人权限 5. + * 生成流程使用范围描述 + *

+ * @param process 流程定义实体,包含流程的所有配置信息 + * @return 创建结果,成功返回流程ID,失败返回错误信息 + */ + @SneakyThrows + @Override + @Transactional(rollbackFor = Exception.class) + public R create(Process process) { + String flowId; + if (StrUtil.isNotBlank(process.getFlowId())) { + flowId = process.getFlowId(); + } + else { + flowId = "P" + DateUtil.format(new Date(), DatePattern.PURE_DATE_PATTERN) + + RandomUtil.randomString(5).toUpperCase(); + } - Process oldProcess = this.getByFlowId(flowId); + Node publishedRoot = objectMapper.readValue(process.getProcess(), Node.class); + NodeUtil.snapshotFormPermissions(publishedRoot, process.getFormItems()); + BpmnModel bpmnModel = ModelUtil.buildBpmnModel(publishedRoot, process.getName(), flowId); + { + byte[] bpmnBytess = new BpmnXMLConverter().convertToXML(bpmnModel); + String filename = "/tmp/flowable-deployment/" + flowId + ".bpmn20.xml"; + log.debug("部署时的模型文件:{}", filename); + FileUtil.writeBytes(bpmnBytess, filename); + } - Deployment deployment = repositoryService.createDeployment() - .addBpmnModel(StrUtil.format("{}.bpmn20.xml", "pig"), bpmnModel) - .tenantId(TenantContextHolder.getTenantId().toString()) - .deploy(); + Process oldProcess = this.getByFlowId(flowId); - NodeUser nodeUser = objectMapper.readValue(process.getAdminList(), new TypeReference>() { - }).get(0); + Deployment deployment = repositoryService.createDeployment() + .addBpmnModel(StrUtil.format("{}.bpmn20.xml", "pig"), bpmnModel) + .tenantId(TenantContextHolder.getTenantId().toString()) + .deploy(); - // 更新流程 - if (StrUtil.isNotBlank(process.getFlowId()) && Objects.nonNull(oldProcess)) { - this.hide(process.getFlowId()); - // 修改所有的管理员 - this.lambdaUpdate() - .set(Process::getAdminId, nodeUser.getId()) - .eq(Process::getUniqueId, oldProcess.getUniqueId()) - .update(new Process()); + var definition = repositoryService.createProcessDefinitionQuery() + .deploymentId(deployment.getId()) + .singleResult(); + processNodeDataService.savePublishedNodes(definition, publishedRoot); - } + NodeUser nodeUser = objectMapper.readValue(process.getAdminList(), new TypeReference>() { + }).get(0); - Node startNode = objectMapper.readValue(process.getProcess(), Node.class); + // 更新流程 + if (StrUtil.isNotBlank(process.getFlowId()) && Objects.nonNull(oldProcess)) { + this.hide(process.getFlowId()); + // 修改所有的管理员 + this.lambdaUpdate() + .set(Process::getAdminId, nodeUser.getId()) + .eq(Process::getUniqueId, oldProcess.getUniqueId()) + .update(new Process()); - List nodeUserList = startNode.getNodeUserList(); + } - StringBuilder stringBuilder = new StringBuilder(); - if (CollUtil.isNotEmpty(nodeUserList)) { - int index = 0; + Node startNode = objectMapper.readValue(process.getProcess(), Node.class); - for (NodeUser user : nodeUserList) { - if (index > 0) { - stringBuilder.append(","); - } - stringBuilder.append(user.getName()); - index++; - if (index > 5) { - break; - } + List nodeUserList = startNode.getNodeUserList(); - } - } - Process p = new Process(); + StringBuilder stringBuilder = new StringBuilder(); + if (CollUtil.isNotEmpty(nodeUserList)) { + int index = 0; - if (Objects.nonNull(oldProcess)) { - p = oldProcess; - } + for (NodeUser user : nodeUserList) { + if (index > 0) { + stringBuilder.append(","); + } + stringBuilder.append(user.getName()); + index++; + if (index > 5) { + break; + } - p.setFlowId(flowId); - p.setName(process.getName()); - p.setLogo(process.getLogo()); - p.setSettings(process.getSettings()); - p.setGroupId(process.getGroupId()); - p.setFormConfig(process.getFormConfig()); - p.setFormItems(process.getFormItems()); - p.setProcess(process.getProcess()); - p.setRemark(process.getRemark()); - p.setSort(0); - p.setHidden("0"); - p.setStop("0"); - p.setAdminId(nodeUser.getId()); - p.setUniqueId(deployment.getId()); - p.setAdminList(process.getAdminList()); - p.setRangeShow(stringBuilder.toString()); - this.saveOrUpdate(p); + } + } + Process p = new Process(); - // 删除原有发起范围 - processStarterService.remove(Wrappers.lambdaQuery().eq(ProcessStarter::getProcessId, p.getId())); + if (Objects.nonNull(oldProcess)) { + p = oldProcess; + } - if (Objects.isNull(nodeUserList)) { - return R.ok(); - } - for (NodeUser nodeUserDto : nodeUserList) { - ProcessStarter processStarter = new ProcessStarter(); - processStarter.setProcessId(p.getId()); - processStarter.setTypeId(nodeUserDto.getId()); - processStarter.setType(nodeUserDto.getType()); - processStarterService.save(processStarter); - } + p.setFlowId(flowId); + p.setName(process.getName()); + p.setLogo(process.getLogo()); + p.setSettings(process.getSettings()); + p.setGroupId(process.getGroupId()); + p.setFormConfig(process.getFormConfig()); + p.setFormItems(process.getFormItems()); + p.setProcess(process.getProcess()); + p.setRemark(process.getRemark()); + p.setSort(0); + p.setHidden("0"); + p.setStop("0"); + p.setAdminId(nodeUser.getId()); + p.setUniqueId(deployment.getId()); + p.setAdminList(process.getAdminList()); + p.setRangeShow(stringBuilder.toString()); + this.saveOrUpdate(p); - return R.ok(); - } + // 删除原有发起范围 + processStarterService + .remove(Wrappers.lambdaQuery().eq(ProcessStarter::getProcessId, p.getId())); - /** - * 更新流程状态或分组 - *

- * 支持三种操作类型: - stop:停用流程(设置stop=1) - delete:删除流程(逻辑删除,设置hidden=1) - 其他:启用流程(设置stop=0) - * 同时支持修改流程所属分组 - *

- * - * @param flowId 流程定义ID - * @param type 操作类型:stop(停用)、delete(删除)、其他(启用) - * @param groupId 流程分组ID,可选参数 - * @return 操作结果 - */ - @Override - public R update(String flowId, String type, Long groupId) { - Process process = new Process(); - process.setFlowId(flowId); - process.setStop("stop".equals(type) ? "1" : "0"); - process.setHidden("delete".equals(type) ? "1" : "0"); - process.setGroupId(groupId); - this.updateByFlowId(process); - return R.ok(); - } + if (Objects.isNull(nodeUserList)) { + return R.ok(); + } + for (NodeUser nodeUserDto : nodeUserList) { + ProcessStarter processStarter = new ProcessStarter(); + processStarter.setProcessId(p.getId()); + processStarter.setTypeId(nodeUserDto.getId()); + processStarter.setType(nodeUserDto.getType()); + processStarterService.save(processStarter); + } - /** - * 验证流程ID是否已注册流程状态事件处理 - *

- * 通过 Redis Set 查询 flowId 是否已被任意微服务模块注册, - * 支持跨服务校验。 - *

- * - * @param flowId 流程定义ID - * @return 验证结果:true表示存在,false表示不存在 - */ - @Override - public R validateFlowId(String flowId) { - if (StrUtil.isBlank(flowId)) { - return R.ok(false); - } - return R.ok(flowStatusEventPublisher.isFlowIdRegistered(flowId)); - } + return R.ok(); + } + + /** + * 更新流程状态或分组 + *

+ * 支持三种操作类型: - stop:停用流程(设置stop=1) - delete:删除流程(逻辑删除,设置hidden=1) - 其他:启用流程(设置stop=0) + * 同时支持修改流程所属分组 + *

+ * @param flowId 流程定义ID + * @param type 操作类型:stop(停用)、delete(删除)、其他(启用) + * @param groupId 流程分组ID,可选参数 + * @return 操作结果 + */ + @Override + public R update(String flowId, String type, Long groupId) { + Process process = new Process(); + process.setFlowId(flowId); + process.setStop("stop".equals(type) ? "1" : "0"); + process.setHidden("delete".equals(type) ? "1" : "0"); + process.setGroupId(groupId); + this.updateByFlowId(process); + return R.ok(); + } + + /** + * 验证流程ID是否已注册流程状态事件处理 + *

+ * 通过 Redis Set 查询 flowId 是否已被任意微服务模块注册, 支持跨服务校验。 + *

+ * @param flowId 流程定义ID + * @return 验证结果:true表示存在,false表示不存在 + */ + @Override + public R validateFlowId(String flowId) { + if (StrUtil.isBlank(flowId)) { + return R.ok(false); + } + return R.ok(flowStatusEventPublisher.isFlowIdRegistered(flowId)); + } } diff --git a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/impl/TaskServiceImpl.java b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/impl/TaskServiceImpl.java index a6dbbda27..6f6098b90 100644 --- a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/impl/TaskServiceImpl.java +++ b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/impl/TaskServiceImpl.java @@ -59,229 +59,233 @@ import java.util.*; @RequiredArgsConstructor public class TaskServiceImpl implements ITaskService { - private final IProcessService processService; + private final IProcessService processService; - private final IProcessCopyService processCopyService; + private final IProcessCopyService processCopyService; - private final IProcessNodeDataService nodeDataService; + private final IProcessNodeDataService nodeDataService; - private final IProcessNodeRecordAssignUserService processNodeRecordAssignUserService; + private final IProcessNodeRecordAssignUserService processNodeRecordAssignUserService; - private final IProcessInstanceRecordService processInstanceRecordService; + private final IProcessInstanceRecordService processInstanceRecordService; - private final IProcessNodeRecordService processNodeRecordService; + private final IProcessNodeRecordService processNodeRecordService; - private final IFlowableEngineService flowableEngineService; + private final IFlowableEngineService flowableEngineService; - private final TaskService taskService; + private final TaskService taskService; - private final HistoryService historyService; + private final HistoryService historyService; - private final RuntimeService runtimeService; + private final RuntimeService runtimeService; - private final ObjectMapper objectMapper; + private final ObjectMapper objectMapper; - private final RemoteUserService remoteUserService; + private final RemoteUserService remoteUserService; - private final ProcessNodeRecordMapper processNodeRecordMapper; + private final ProcessNodeRecordMapper processNodeRecordMapper; - private final ProcessTaskAddSignRecordMapper processTaskAddSignRecordMapper; + private final ProcessTaskAddSignRecordMapper processTaskAddSignRecordMapper; - /** - * 查询首页数据看板 - *

- * 获取当前用户的任务统计数据,包括: 1. 待办任务数量 2. 已办任务数量 3. 我发起的流程数量 4. 抄送给我的数量 - *

- * - * @return R 包含IndexPageStatistics对象,含有各项统计数据 - */ - @Override - public R queryTaskData() { + /** + * 查询首页数据看板 + *

+ * 获取当前用户的任务统计数据,包括: 1. 待办任务数量 2. 已办任务数量 3. 我发起的流程数量 4. 抄送给我的数量 + *

+ * @return R 包含IndexPageStatistics对象,含有各项统计数据 + */ + @Override + public R queryTaskData() { - TaskQuery taskQuery = taskService.createTaskQuery(); + TaskQuery taskQuery = taskService.createTaskQuery(); - // 待办数量 - long pendingNum = taskQuery.taskAssignee(String.valueOf(SecurityUtils.getUser().getId())) - .taskTenantId(TenantContextHolder.getTenantId().toString()) - .count(); - // 已完成任务 - HistoricActivityInstanceQuery historicActivityInstanceQuery = historyService - .createHistoricActivityInstanceQuery(); + // 待办数量 + long pendingNum = taskQuery.taskAssignee(String.valueOf(SecurityUtils.getUser().getId())) + .taskTenantId(TenantContextHolder.getTenantId().toString()) + .count(); + // 已完成任务 + HistoricActivityInstanceQuery historicActivityInstanceQuery = historyService + .createHistoricActivityInstanceQuery(); - long completedNum = historicActivityInstanceQuery.taskAssignee(String.valueOf(SecurityUtils.getUser().getId())) - .tenantIdIn(List.of(TenantContextHolder.getTenantId().toString())) - .finished() - .count(); + long completedNum = historicActivityInstanceQuery.taskAssignee(String.valueOf(SecurityUtils.getUser().getId())) + .tenantIdIn(List.of(TenantContextHolder.getTenantId().toString())) + .finished() + .count(); - IndexPageStatistics indexPageStatistics = IndexPageStatistics.builder() - .pendingNum(pendingNum) - .completedNum(completedNum) - .build(); + IndexPageStatistics indexPageStatistics = IndexPageStatistics.builder() + .pendingNum(pendingNum) + .completedNum(completedNum) + .build(); - // 获取抄送任务 - Long copyCount = processCopyService.lambdaQuery() - .eq(ProcessCopy::getUserId, SecurityUtils.getUser().getId()) - .count(); - indexPageStatistics.setCopyNum(copyCount); - return R.ok(indexPageStatistics); - } + // 获取抄送任务 + Long copyCount = processCopyService.lambdaQuery() + .eq(ProcessCopy::getUserId, SecurityUtils.getUser().getId()) + .count(); + indexPageStatistics.setCopyNum(copyCount); + return R.ok(indexPageStatistics); + } - /** - * 查询任务 - *

- * 查询指定任务的详细信息,包括: 1. 任务的基本信息(节点名称、执行人等) 2. 流程表单定义和表单数据 3. 当前节点的表单权限配置 4. - * 任务是否可委托、是否是委托任务等状态 - *

- * 对于已完成的任务,会从历史记录中获取任务数据 - *

- * - * @param taskId 任务ID - * @param view 是否是查看模式(true:只查看,false:可操作) - * @return R 包含任务详情的Dict对象 - * @throws Exception 当解析JSON数据失败时抛出异常 - */ - @SneakyThrows - @Override - public R queryTask(String taskId, boolean view) { - // 默认使用主表单数据源 - return queryTask(taskId, view, true); - } + /** + * 查询任务 + *

+ * 查询指定任务的详细信息,包括: 1. 任务的基本信息(节点名称、执行人等) 2. 流程表单定义和表单数据 3. 当前节点的表单权限配置 4. + * 任务是否可委托、是否是委托任务等状态 + *

+ * 对于已完成的任务,会从历史记录中获取任务数据 + *

+ * @param taskId 任务ID + * @param view 是否是查看模式(true:只查看,false:可操作) + * @return R 包含任务详情的Dict对象 + * @throws Exception 当解析JSON数据失败时抛出异常 + */ + @SneakyThrows + @Override + public R queryTask(String taskId, boolean view) { + // 默认使用主表单数据源 + return queryTask(taskId, view, true); + } - /** - * 查询任务(支持选择表单数据源) - *

- * 查询指定任务的详细信息,包括: 1. 任务的基本信息(节点名称、执行人等) 2. 流程表单定义和表单数据 3. 当前节点的表单权限配置 4. - * 任务是否可委托、是否是委托任务等状态 - *

- * 对于已完成的任务,可选择从主表单数据或节点快照读取表单数据 - *

- * - * @param taskId 任务ID - * @param view 是否是查看模式(true:只查看,false:可操作) - * @param useMainFormData 是否使用主表单数据(true=从process_instance_record.form_data读取最新数据,false=从节点快照读取) - * @return R 包含任务详情的Dict对象 - * @throws Exception 当解析JSON数据失败时抛出异常 - */ - @SneakyThrows - @Override - public R queryTask(String taskId, boolean view, boolean useMainFormData) { + /** + * 查询任务(支持选择表单数据源) + *

+ * 查询指定任务的详细信息,包括: 1. 任务的基本信息(节点名称、执行人等) 2. 流程表单定义和表单数据 3. 当前节点的表单权限配置 4. + * 任务是否可委托、是否是委托任务等状态 + *

+ * 对于已完成的任务,可选择从主表单数据或节点快照读取表单数据 + *

+ * @param taskId 任务ID + * @param view 是否是查看模式(true:只查看,false:可操作) + * @param useMainFormData + * 是否使用主表单数据(true=从process_instance_record.form_data读取最新数据,false=从节点快照读取) + * @return R 包含任务详情的Dict对象 + * @throws Exception 当解析JSON数据失败时抛出异常 + */ + @SneakyThrows + @Override + public R queryTask(String taskId, boolean view, boolean useMainFormData) { - long userId = SecurityUtils.getUser().getId(); + long userId = SecurityUtils.getUser().getId(); - Optional task = Optional.ofNullable(taskService.createTaskQuery() - .taskId(taskId) - .taskTenantId(TenantContextHolder.getTenantId().toString()) - .taskAssignee(String.valueOf(userId)) - .singleResult()); - TaskResultDto taskResultDto = new TaskResultDto(); + Optional task = Optional.ofNullable(taskService.createTaskQuery() + .taskId(taskId) + .taskTenantId(TenantContextHolder.getTenantId().toString()) + .taskAssignee(String.valueOf(userId)) + .singleResult()); + TaskResultDto taskResultDto = new TaskResultDto(); - if (task.isPresent()) { - String processDefinitionId = task.get().getProcessDefinitionId(); - String taskDefinitionKey = task.get().getTaskDefinitionKey(); - DelegationState delegationState = task.get().getDelegationState(); - String processInstanceId = task.get().getProcessInstanceId(); - Object delegateVariable = taskService.getVariableLocal(taskId, "delegate"); + if (task.isPresent()) { + String processDefinitionId = task.get().getProcessDefinitionId(); + String taskDefinitionKey = task.get().getTaskDefinitionKey(); + DelegationState delegationState = task.get().getDelegationState(); + String processInstanceId = task.get().getProcessInstanceId(); + Object delegateVariable = taskService.getVariableLocal(taskId, "delegate"); - String flowId = NodeUtil.getFlowId(processDefinitionId); - Map variables = taskService.getVariables(taskId); - Map variableAll = new HashMap<>(variables); + String flowId = NodeUtil.getFlowId(processDefinitionId); + Map variables = taskService.getVariables(taskId); + Map variableAll = new HashMap<>(variables); - taskResultDto.setFlowId(flowId); - taskResultDto.setNodeId(taskDefinitionKey); - taskResultDto.setCurrentTask(true); - taskResultDto.setDelegate(Convert.toBool(delegateVariable, false)); - taskResultDto.setVariableAll(variableAll); - taskResultDto.setProcessInstanceId(processInstanceId); - taskResultDto.setDelegationState(delegationState == null ? null : delegationState.toString()); - } else { - Optional historicTaskInstance = Optional - .ofNullable(historyService.createHistoricTaskInstanceQuery() - .taskId(taskId) - .taskAssignee(String.valueOf(userId)) - .singleResult()); + taskResultDto.setFlowId(flowId); + taskResultDto.setProcessDefinitionId(processDefinitionId); + taskResultDto.setNodeId(taskDefinitionKey); + taskResultDto.setCurrentTask(true); + taskResultDto.setDelegate(Convert.toBool(delegateVariable, false)); + taskResultDto.setVariableAll(variableAll); + taskResultDto.setProcessInstanceId(processInstanceId); + taskResultDto.setDelegationState(delegationState == null ? null : delegationState.toString()); + } + else { + Optional historicTaskInstance = Optional + .ofNullable(historyService.createHistoricTaskInstanceQuery() + .taskId(taskId) + .taskAssignee(String.valueOf(userId)) + .singleResult()); - String processDefinitionId = historicTaskInstance.get().getProcessDefinitionId(); - String taskDefinitionKey = historicTaskInstance.get().getTaskDefinitionKey(); - String processInstanceId = historicTaskInstance.get().getProcessInstanceId(); + String processDefinitionId = historicTaskInstance.get().getProcessDefinitionId(); + String taskDefinitionKey = historicTaskInstance.get().getTaskDefinitionKey(); + String processInstanceId = historicTaskInstance.get().getProcessInstanceId(); - String flowId = NodeUtil.getFlowId(processDefinitionId); + String flowId = NodeUtil.getFlowId(processDefinitionId); - List variableInstanceList = historyService.createHistoricVariableInstanceQuery() - .processInstanceId(processInstanceId) - .list(); - Map variableAll = new HashMap<>(); + List variableInstanceList = historyService.createHistoricVariableInstanceQuery() + .processInstanceId(processInstanceId) + .list(); + Map variableAll = new HashMap<>(); - if (CollUtil.isNotEmpty(variableInstanceList)) { - variableInstanceList.forEach(historicVariableInstance -> variableAll - .put(historicVariableInstance.getVariableName(), historicVariableInstance.getValue())); - } + if (CollUtil.isNotEmpty(variableInstanceList)) { + variableInstanceList.forEach(historicVariableInstance -> variableAll + .put(historicVariableInstance.getVariableName(), historicVariableInstance.getValue())); + } - taskResultDto.setFlowId(flowId); - taskResultDto.setNodeId(taskDefinitionKey); - taskResultDto.setCurrentTask(false); - taskResultDto.setVariableAll(variableAll); - taskResultDto.setProcessInstanceId(processInstanceId); + taskResultDto.setFlowId(flowId); + taskResultDto.setProcessDefinitionId(processDefinitionId); + taskResultDto.setNodeId(taskDefinitionKey); + taskResultDto.setCurrentTask(false); + taskResultDto.setVariableAll(variableAll); + taskResultDto.setProcessInstanceId(processInstanceId); - } + } - // 变量 - Map paramMap = taskResultDto.getVariableAll(); - // 是否是当前活动任务 - Boolean currentTask = taskResultDto.getCurrentTask(); - if (!currentTask) { - // 判断是否使用主表单数据源 - boolean shouldUseMainFormData = view && useMainFormData; + // 变量 + Map paramMap = taskResultDto.getVariableAll(); + // 是否是当前活动任务 + Boolean currentTask = taskResultDto.getCurrentTask(); + if (!currentTask) { + // 判断是否使用主表单数据源 + boolean shouldUseMainFormData = view && useMainFormData; - if (shouldUseMainFormData) { - // 新增: 从主流程实例记录读取最新表单数据 - String processInstanceId = taskResultDto.getProcessInstanceId(); - ProcessInstanceRecord processInstanceRecord = processInstanceRecordService.lambdaQuery() - .eq(ProcessInstanceRecord::getProcessInstanceId, processInstanceId) - .one(); + if (shouldUseMainFormData) { + // 新增: 从主流程实例记录读取最新表单数据 + String processInstanceId = taskResultDto.getProcessInstanceId(); + ProcessInstanceRecord processInstanceRecord = processInstanceRecordService.lambdaQuery() + .eq(ProcessInstanceRecord::getProcessInstanceId, processInstanceId) + .one(); - if (processInstanceRecord != null && StrUtil.isNotBlank(processInstanceRecord.getFormData())) { - try { - Map mainFormData = objectMapper.readValue(processInstanceRecord.getFormData(), - new TypeReference>() { - }); - paramMap.putAll(mainFormData); - log.debug("Using main form data for completed task {}", taskId); - } catch (Exception e) { - log.warn("Failed to parse main form data, falling back to node snapshot: {}", e.getMessage()); - shouldUseMainFormData = false; - } - } else { - log.debug("Main form data empty, falling back to node snapshot for task {}", taskId); - shouldUseMainFormData = false; - } - } + if (processInstanceRecord != null && StrUtil.isNotBlank(processInstanceRecord.getFormData())) { + try { + Map mainFormData = objectMapper.readValue(processInstanceRecord.getFormData(), + new TypeReference>() { + }); + paramMap.putAll(mainFormData); + log.debug("Using main form data for completed task {}", taskId); + } + catch (Exception e) { + log.warn("Failed to parse main form data, falling back to node snapshot: {}", e.getMessage()); + shouldUseMainFormData = false; + } + } + else { + log.debug("Main form data empty, falling back to node snapshot for task {}", taskId); + shouldUseMainFormData = false; + } + } - // 回退: 使用节点级别快照(原有逻辑) - if (!shouldUseMainFormData) { - ProcessNodeRecordAssignUser processNodeRecordAssignUser = processNodeRecordAssignUserService.lambdaQuery() - .eq(ProcessNodeRecordAssignUser::getTaskId, taskId) - .eq(ProcessNodeRecordAssignUser::getUserId, userId) - .eq(ProcessNodeRecordAssignUser::getStatus, NodeStatusEnum.YJS.getCode()) - .last("limit 1") - .orderByDesc(ProcessNodeRecordAssignUser::getEndTime) - .one(); + // 回退: 使用节点级别快照(原有逻辑) + if (!shouldUseMainFormData) { + ProcessNodeRecordAssignUser processNodeRecordAssignUser = processNodeRecordAssignUserService + .lambdaQuery() + .eq(ProcessNodeRecordAssignUser::getTaskId, taskId) + .eq(ProcessNodeRecordAssignUser::getUserId, userId) + .eq(ProcessNodeRecordAssignUser::getStatus, NodeStatusEnum.YJS.getCode()) + .last("limit 1") + .orderByDesc(ProcessNodeRecordAssignUser::getEndTime) + .one(); - if (processNodeRecordAssignUser != null) { - String data = processNodeRecordAssignUser.getData(); - if (StrUtil.isNotBlank(data)) { - Map collect = objectMapper.readValue(data, new TypeReference<>() { - }); - paramMap.putAll(collect); - log.debug("Using node-level snapshot for task {}", taskId); - } - } - } + if (processNodeRecordAssignUser != null) { + String data = processNodeRecordAssignUser.getData(); + if (StrUtil.isNotBlank(data)) { + Map collect = objectMapper.readValue(data, new TypeReference<>() { + }); + paramMap.putAll(collect); + log.debug("Using node-level snapshot for task {}", taskId); + } + } + } - } + } - // 当前节点数据 - Node node = nodeDataService.getNodeData(taskResultDto.getFlowId(), taskResultDto.getNodeId()).getData(); - Map formPerms = node.getFormPerms(); + // 当前节点数据 + Node node = nodeDataService.getPublishedNode(taskResultDto.getProcessDefinitionId(), taskResultDto.getNodeId()) + .getData(); ProcessNodeRecord activeNodeRecord = null; List participantUserIds = Collections.emptyList(); @@ -295,49 +299,48 @@ public class TaskServiceImpl implements ITaskService { } } - Process oaForms = processService.getByFlowId(taskResultDto.getFlowId()); - if (oaForms == null) { - return R.failed("流程不存在"); - } + Process oaForms = processService.getByFlowId(taskResultDto.getFlowId()); + if (oaForms == null) { + return R.failed("流程不存在"); + } - Dict set = Dict.create() - .set("taskId", taskId) - .set("nodeId", taskResultDto.getNodeId()) - .set("processInstanceId", taskResultDto.getProcessInstanceId()) - .set("node", node) - .set("process", oaForms.getProcess()) - .set("delegateAgain", taskResultDto.getDelegate()) - .set("delegationTask", StrUtil.equals(taskResultDto.getDelegationState(), "PENDING")) - .set("canAddSign", canAddSign) - .set("participantUserIds", participantUserIds) - .set("formItems", oaForms.getFormItems()) - .set("formData", paramMap) - .set("formConfig", oaForms.getFormConfig()) - .set("formPerms", formPerms); + Dict set = Dict.create() + .set("taskId", taskId) + .set("nodeId", taskResultDto.getNodeId()) + .set("processInstanceId", taskResultDto.getProcessInstanceId()) + .set("node", node) + .set("process", oaForms.getProcess()) + .set("delegateAgain", taskResultDto.getDelegate()) + .set("delegationTask", StrUtil.equals(taskResultDto.getDelegationState(), "PENDING")) + .set("canAddSign", canAddSign) + .set("participantUserIds", participantUserIds) + .set("formItems", oaForms.getFormItems()) + .set("formData", paramMap) + .set("formConfig", oaForms.getFormConfig()) + .set("formPerms", NodeUtil.publishedFormPermissions(node, oaForms.getFormItems())); - return R.ok(set); - } + return R.ok(set); + } - /** - * 完成任务 - *

- * 执行任务的完成操作,将任务标记为已完成并流转到下一个节点。 该方法会将当前用户ID设置到任务参数中,确保任务由正确的用户完成 - *

- * - * @param taskParamDto 任务参数,包含任务ID、审批意见、流程变量等 - * @return R 操作结果 - */ - @Override + /** + * 完成任务 + *

+ * 执行任务的完成操作,将任务标记为已完成并流转到下一个节点。 该方法会将当前用户ID设置到任务参数中,确保任务由正确的用户完成 + *

+ * @param taskParamDto 任务参数,包含任务ID、审批意见、流程变量等 + * @return R 操作结果 + */ + @Override @Transactional(rollbackFor = Exception.class) - public R completeTask(TaskParamDto taskParamDto) { - long userId = SecurityUtils.getUser().getId(); - taskParamDto.setUserId(String.valueOf(userId)); + public R completeTask(TaskParamDto taskParamDto) { + long userId = SecurityUtils.getUser().getId(); + taskParamDto.setUserId(String.valueOf(userId)); Task task = taskService.createTaskQuery() - .taskId(taskParamDto.getTaskId()) - .taskTenantId(TenantContextHolder.getTenantId().toString()) - .taskAssignee(String.valueOf(userId)) - .singleResult(); + .taskId(taskParamDto.getTaskId()) + .taskTenantId(TenantContextHolder.getTenantId().toString()) + .taskAssignee(String.valueOf(userId)) + .singleResult(); if (task == null) { return R.failed("当前任务已完成或无权限操作"); } @@ -347,95 +350,96 @@ public class TaskServiceImpl implements ITaskService { return R.failed("当前审批节点已结束,无法操作"); } - Map taskLocalParamMap = taskParamDto.getTaskLocalParamMap(); - if (CollUtil.isNotEmpty(taskLocalParamMap)) { - taskService.setVariablesLocal(taskParamDto.getTaskId(), taskLocalParamMap); - } + Map taskLocalParamMap = taskParamDto.getTaskLocalParamMap(); + if (CollUtil.isNotEmpty(taskLocalParamMap)) { + taskService.setVariablesLocal(taskParamDto.getTaskId(), taskLocalParamMap); + } - // 始终获取 processInstanceId,确保触发器终止时可用 - taskParamDto.setProcessInstanceId(task.getProcessInstanceId()); + // 始终获取 processInstanceId,确保触发器终止时可用 + taskParamDto.setProcessInstanceId(task.getProcessInstanceId()); - if (CollUtil.isNotEmpty(taskParamDto.getFormData())) { - runtimeService.setVariables(task.getExecutionId(), taskParamDto.getFormData()); + if (CollUtil.isNotEmpty(taskParamDto.getFormData())) { + runtimeService.setVariables(task.getExecutionId(), taskParamDto.getFormData()); - // 同步更新流程实例记录的主表单数据 - String processInstanceId = task.getProcessInstanceId(); - ProcessInstanceRecord processInstanceRecord = processInstanceRecordService.lambdaQuery() - .eq(ProcessInstanceRecord::getProcessInstanceId, processInstanceId) - .one(); + // 同步更新流程实例记录的主表单数据 + String processInstanceId = task.getProcessInstanceId(); + ProcessInstanceRecord processInstanceRecord = processInstanceRecordService.lambdaQuery() + .eq(ProcessInstanceRecord::getProcessInstanceId, processInstanceId) + .one(); - if (processInstanceRecord != null) { - try { - // 合并现有表单数据和新提交的数据 - Map existingFormData = new HashMap<>(); - if (StrUtil.isNotBlank(processInstanceRecord.getFormData())) { - existingFormData = objectMapper.readValue(processInstanceRecord.getFormData(), - new TypeReference>() { - }); - } + if (processInstanceRecord != null) { + try { + // 合并现有表单数据和新提交的数据 + Map existingFormData = new HashMap<>(); + if (StrUtil.isNotBlank(processInstanceRecord.getFormData())) { + existingFormData = objectMapper.readValue(processInstanceRecord.getFormData(), + new TypeReference>() { + }); + } - // 用新数据覆盖现有数据 - existingFormData.putAll(taskParamDto.getFormData()); + // 用新数据覆盖现有数据 + existingFormData.putAll(taskParamDto.getFormData()); - // 更新到数据库 - String updatedFormData = objectMapper.writeValueAsString(existingFormData); - processInstanceRecordService.lambdaUpdate() - .set(ProcessInstanceRecord::getFormData, updatedFormData) - .eq(ProcessInstanceRecord::getProcessInstanceId, processInstanceId) - .update(); + // 更新到数据库 + String updatedFormData = objectMapper.writeValueAsString(existingFormData); + processInstanceRecordService.lambdaUpdate() + .set(ProcessInstanceRecord::getFormData, updatedFormData) + .eq(ProcessInstanceRecord::getProcessInstanceId, processInstanceId) + .update(); - log.debug("流程实例 {} 的表单数据已同步更新", processInstanceId); - } catch (Exception e) { - log.error("同步更新流程实例表单数据失败: {}", e.getMessage(), e); - // 不中断流程执行,仅记录错误 - } - } - } + log.debug("流程实例 {} 的表单数据已同步更新", processInstanceId); + } + catch (Exception e) { + log.error("同步更新流程实例表单数据失败: {}", e.getMessage(), e); + // 不中断流程执行,仅记录错误 + } + } + } - try { - taskService.complete(taskParamDto.getTaskId(), taskParamDto.getParamMap()); - } catch (FlowableException e) { - String message = e.getMessage(); - if (StrUtil.isNotBlank(message) && message.startsWith("TRIGGER_TERMINATE:")) { - return handleTriggerTerminate(message, taskParamDto.getProcessInstanceId()); - } - throw e; - } + try { + taskService.complete(taskParamDto.getTaskId(), taskParamDto.getParamMap()); + } + catch (FlowableException e) { + String message = e.getMessage(); + if (StrUtil.isNotBlank(message) && message.startsWith("TRIGGER_TERMINATE:")) { + return handleTriggerTerminate(message, taskParamDto.getProcessInstanceId()); + } + throw e; + } - return R.ok(); - } + return R.ok(); + } - /** - * 处理触发器异常终止 - *

- * 在 Flowable 事务回滚后执行,此时处于新的事务上下文中, - * 可安全地删除流程实例和更新 ProcessInstanceRecord 状态。 - *

- * - * @param exceptionMessage 格式: TRIGGER_TERMINATE:{errorMessage} - * @param processInstanceId 流程实例ID - * @return R 失败结果 - */ - private R handleTriggerTerminate(String exceptionMessage, String processInstanceId) { - String errorMsg = StrUtil.removePrefix(exceptionMessage, "TRIGGER_TERMINATE:"); + /** + * 处理触发器异常终止 + *

+ * 在 Flowable 事务回滚后执行,此时处于新的事务上下文中, 可安全地删除流程实例和更新 ProcessInstanceRecord 状态。 + *

+ * @param exceptionMessage 格式: TRIGGER_TERMINATE:{errorMessage} + * @param processInstanceId 流程实例ID + * @return R 失败结果 + */ + private R handleTriggerTerminate(String exceptionMessage, String processInstanceId) { + String errorMsg = StrUtil.removePrefix(exceptionMessage, "TRIGGER_TERMINATE:"); - // 在 Flowable 事务外删除流程实例(新的命令上下文 = 新事务) - try { - runtimeService.deleteProcessInstance(processInstanceId, "触发器异常终止: " + errorMsg); - } catch (Exception ex) { - log.error("触发器终止-删除流程实例失败: processInstanceId={}, error={}", processInstanceId, ex.getMessage()); - } + // 在 Flowable 事务外删除流程实例(新的命令上下文 = 新事务) + try { + runtimeService.deleteProcessInstance(processInstanceId, "触发器异常终止: " + errorMsg); + } + catch (Exception ex) { + log.error("触发器终止-删除流程实例失败: processInstanceId={}, error={}", processInstanceId, ex.getMessage()); + } - // 更新 ProcessInstanceRecord 状态为已结束(与 stopProcessInstance 保持一致) - processInstanceRecordService.lambdaUpdate() - .set(ProcessInstanceRecord::getStatus, NodeStatusEnum.YJS.getCode()) - .set(ProcessInstanceRecord::getEndTime, new Date()) - .set(ProcessInstanceRecord::getFinishReason, String.valueOf(NodeStatusEnum.YZZ.getCode())) - .eq(ProcessInstanceRecord::getProcessInstanceId, processInstanceId) - .update(); + // 更新 ProcessInstanceRecord 状态为已结束(与 stopProcessInstance 保持一致) + processInstanceRecordService.lambdaUpdate() + .set(ProcessInstanceRecord::getStatus, NodeStatusEnum.YJS.getCode()) + .set(ProcessInstanceRecord::getEndTime, new Date()) + .set(ProcessInstanceRecord::getFinishReason, String.valueOf(NodeStatusEnum.YZZ.getCode())) + .eq(ProcessInstanceRecord::getProcessInstanceId, processInstanceId) + .update(); - return R.failed("触发器调用失败: " + errorMsg); - } + return R.failed("触发器调用失败: " + errorMsg); + } /** * 向运行中的并行会签节点动态追加审批人。 @@ -466,10 +470,10 @@ public class TaskServiceImpl implements ITaskService { long userId = SecurityUtils.getUser().getId(); Long tenantId = TenantContextHolder.getTenantId(); Task sourceTask = taskService.createTaskQuery() - .taskId(addSignTaskDto.getTaskId()) - .taskTenantId(tenantId.toString()) - .taskAssignee(String.valueOf(userId)) - .singleResult(); + .taskId(addSignTaskDto.getTaskId()) + .taskTenantId(tenantId.toString()) + .taskAssignee(String.valueOf(userId)) + .singleResult(); if (sourceTask == null) { return R.failed("当前任务已完成或已转办"); } @@ -481,7 +485,9 @@ public class TaskServiceImpl implements ITaskService { } String flowId = NodeUtil.getFlowId(sourceTask.getProcessDefinitionId()); - Node node = nodeDataService.getNodeData(flowId, sourceTask.getTaskDefinitionKey()).getData(); + Node node = nodeDataService + .getPublishedNode(sourceTask.getProcessDefinitionId(), sourceTask.getTaskDefinitionKey()) + .getData(); if (!isParallelCountersign(node)) { return R.failed("仅并行会签节点支持加签"); } @@ -514,16 +520,15 @@ public class TaskServiceImpl implements ITaskService { } } - ProcessTaskAddSignRecord addSignRecord = new ProcessTaskAddSignRecord() - .setFlowId(flowId) - .setProcessInstanceId(sourceTask.getProcessInstanceId()) - .setNodeRecordId(nodeRecord.getId()) - .setNodeId(sourceTask.getTaskDefinitionKey()) - .setSourceTaskId(sourceTask.getId()) - .setAddedByUserId(userId) - .setReason(reason) - .setTargetUserIds(objectMapper.writeValueAsString(targetUserIds)) - .setParticipantUserIdsBefore(objectMapper.writeValueAsString(participantUserIds)); + ProcessTaskAddSignRecord addSignRecord = new ProcessTaskAddSignRecord().setFlowId(flowId) + .setProcessInstanceId(sourceTask.getProcessInstanceId()) + .setNodeRecordId(nodeRecord.getId()) + .setNodeId(sourceTask.getTaskDefinitionKey()) + .setSourceTaskId(sourceTask.getId()) + .setAddedByUserId(userId) + .setReason(reason) + .setTargetUserIds(objectMapper.writeValueAsString(targetUserIds)) + .setParticipantUserIdsBefore(objectMapper.writeValueAsString(participantUserIds)); processTaskAddSignRecordMapper.insert(addSignRecord); List taskSummaries = new ArrayList<>(); @@ -545,21 +550,21 @@ public class TaskServiceImpl implements ITaskService { sourceTask.getProcessInstanceId(), executionVariables); Task newTask = taskService.createTaskQuery() - .processInstanceId(sourceTask.getProcessInstanceId()) - .taskDefinitionKey(sourceTask.getTaskDefinitionKey()) - .taskAssignee(targetUserId.toString()) - .active() - .singleResult(); + .processInstanceId(sourceTask.getProcessInstanceId()) + .taskDefinitionKey(sourceTask.getTaskDefinitionKey()) + .taskAssignee(targetUserId.toString()) + .active() + .singleResult(); if (newTask == null) { return rollbackFailed("新增会签任务失败,请刷新后重试"); } boolean assignmentRecorded = processNodeRecordAssignUserService.lambdaQuery() - .eq(ProcessNodeRecordAssignUser::getNodeRecordId, nodeRecord.getId()) - .eq(ProcessNodeRecordAssignUser::getTaskId, newTask.getId()) - .eq(ProcessNodeRecordAssignUser::getUserId, targetUserId) - .eq(ProcessNodeRecordAssignUser::getAssignmentOrigin, - ProcessInstanceConstant.ASSIGNMENT_ORIGIN_ADD_SIGN) - .exists(); + .eq(ProcessNodeRecordAssignUser::getNodeRecordId, nodeRecord.getId()) + .eq(ProcessNodeRecordAssignUser::getTaskId, newTask.getId()) + .eq(ProcessNodeRecordAssignUser::getUserId, targetUserId) + .eq(ProcessNodeRecordAssignUser::getAssignmentOrigin, + ProcessInstanceConstant.ASSIGNMENT_ORIGIN_ADD_SIGN) + .exists(); if (!assignmentRecorded) { return rollbackFailed("新增会签任务记录失败,请刷新后重试"); } @@ -583,18 +588,17 @@ public class TaskServiceImpl implements ITaskService { return R.ok(new AddSignResultDto(addSignRecord.getId(), taskSummaries)); } - /** - * 前加签 - *

- * 将任务委托给其他用户处理,委托后任务会分配给被委托人, 被委托人完成后任务会返回给原委托人 - *

- * - * @param taskParamDto 任务参数,包含任务ID、被委托人ID列表等 - * @return R 操作结果 - */ - @Transactional - @Override - public R delegateTask(TaskParamDto taskParamDto) { + /** + * 前加签 + *

+ * 将任务委托给其他用户处理,委托后任务会分配给被委托人, 被委托人完成后任务会返回给原委托人 + *

+ * @param taskParamDto 任务参数,包含任务ID、被委托人ID列表等 + * @return R 操作结果 + */ + @Transactional + @Override + public R delegateTask(TaskParamDto taskParamDto) { if (taskParamDto == null || StrUtil.isBlank(taskParamDto.getTaskId())) { return R.failed("任务ID不能为空"); } @@ -605,10 +609,10 @@ public class TaskServiceImpl implements ITaskService { long userId = SecurityUtils.getUser().getId(); Long tenantId = TenantContextHolder.getTenantId(); Task task = taskService.createTaskQuery() - .taskId(taskParamDto.getTaskId()) - .taskTenantId(tenantId.toString()) - .taskAssignee(String.valueOf(userId)) - .singleResult(); + .taskId(taskParamDto.getTaskId()) + .taskTenantId(tenantId.toString()) + .taskAssignee(String.valueOf(userId)) + .singleResult(); if (task == null) { return R.failed("当前任务已完成或无权限委托"); } @@ -632,7 +636,7 @@ public class TaskServiceImpl implements ITaskService { List.of(ProcessInstanceConstant.VAR_ASSIGNMENT_ORIGIN, ProcessInstanceConstant.VAR_INITIAL_USER_ID, ProcessInstanceConstant.VAR_NODE_RECORD_ID, ProcessInstanceConstant.VAR_ADD_SIGN_RECORD_ID, ProcessInstanceConstant.VAR_ADDED_BY_USER_ID, ProcessInstanceConstant.VAR_ASSIGNMENT_OPERATION) - .forEach(safeLocalVariables::remove); + .forEach(safeLocalVariables::remove); taskParamDto.setTaskLocalParamMap(safeLocalVariables); taskParamDto.setUserId(String.valueOf(userId)); R result = flowableEngineService.delegateTask(taskParamDto); @@ -647,34 +651,32 @@ public class TaskServiceImpl implements ITaskService { return rollbackFailed("委托任务记录失败,请刷新后重试"); } return result; - } + } - /** - * 加签完成任务 - *

- * 被委托人完成委托任务后,通过此方法将任务返回给原委托人。 这是委托流程的第二步,用于结束委托状态 - *

- * - * @param taskParamDto 任务参数,包含任务ID、审批意见等 - * @return R 操作结果 - */ - @Override - public R resolveTask(TaskParamDto taskParamDto) { - return flowableEngineService.resolveTask(taskParamDto); - } + /** + * 加签完成任务 + *

+ * 被委托人完成委托任务后,通过此方法将任务返回给原委托人。 这是委托流程的第二步,用于结束委托状态 + *

+ * @param taskParamDto 任务参数,包含任务ID、审批意见等 + * @return R 操作结果 + */ + @Override + public R resolveTask(TaskParamDto taskParamDto) { + return flowableEngineService.resolveTask(taskParamDto); + } - /** - * 设置执行人 - *

- * 转办任务给其他用户,与委托不同,转办后任务直接由新执行人处理, 不会返回给原执行人 - *

- * - * @param taskParamDto 任务参数,包含任务ID、新执行人ID等 - * @return R 操作结果 - */ - @Override + /** + * 设置执行人 + *

+ * 转办任务给其他用户,与委托不同,转办后任务直接由新执行人处理, 不会返回给原执行人 + *

+ * @param taskParamDto 任务参数,包含任务ID、新执行人ID等 + * @return R 操作结果 + */ + @Override @Transactional(rollbackFor = Exception.class) - public R setAssignee(TaskParamDto taskParamDto) { + public R setAssignee(TaskParamDto taskParamDto) { if (taskParamDto == null || StrUtil.isBlank(taskParamDto.getTaskId())) { return R.failed("任务ID不能为空"); } @@ -687,11 +689,11 @@ public class TaskServiceImpl implements ITaskService { Long tenantId = TenantContextHolder.getTenantId(); Long targetUserId = taskParamDto.getTargetUserIdList().get(0); Task task = taskService.createTaskQuery() - .taskId(taskParamDto.getTaskId()) - .taskTenantId(tenantId.toString()) - .taskAssignee(String.valueOf(userId)) - .includeTaskLocalVariables() - .singleResult(); + .taskId(taskParamDto.getTaskId()) + .taskTenantId(tenantId.toString()) + .taskAssignee(String.valueOf(userId)) + .includeTaskLocalVariables() + .singleResult(); if (task == null) { return R.failed("当前任务已完成或已转办"); } @@ -712,10 +714,10 @@ public class TaskServiceImpl implements ITaskService { return R.failed("未找到当前任务的执行人记录"); } - Map taskLocalParamMap = taskParamDto.getTaskLocalParamMap(); - if (CollUtil.isNotEmpty(taskLocalParamMap)) { - taskService.setVariablesLocal(taskParamDto.getTaskId(), taskLocalParamMap); - } + Map taskLocalParamMap = taskParamDto.getTaskLocalParamMap(); + if (CollUtil.isNotEmpty(taskLocalParamMap)) { + taskService.setVariablesLocal(taskParamDto.getTaskId(), taskLocalParamMap); + } taskService.setVariableLocal(task.getId(), ProcessInstanceConstant.VAR_NODE_RECORD_ID, nodeRecord.getId()); taskService.setVariableLocal(task.getId(), ProcessInstanceConstant.VAR_ASSIGNMENT_ORIGIN, sourceAssignment.getAssignmentOrigin()); @@ -727,186 +729,177 @@ public class TaskServiceImpl implements ITaskService { } taskService.setVariableLocal(task.getId(), ProcessInstanceConstant.VAR_ASSIGNMENT_OPERATION, "TRANSFER"); - // 设置任务的 owner 为当前任务的 assignee - taskService.setOwner(taskParamDto.getTaskId(), task.getAssignee()); + // 设置任务的 owner 为当前任务的 assignee + taskService.setOwner(taskParamDto.getTaskId(), task.getAssignee()); - // 设置任务的 assignee 为目标用户 (转办用户) + // 设置任务的 assignee 为目标用户 (转办用户) taskService.setAssignee(taskParamDto.getTaskId(), targetUserId.toString()); boolean assignmentRecorded = processNodeRecordAssignUserService.lambdaQuery() - .eq(ProcessNodeRecordAssignUser::getNodeRecordId, nodeRecord.getId()) - .eq(ProcessNodeRecordAssignUser::getTaskId, task.getId()) - .eq(ProcessNodeRecordAssignUser::getUserId, targetUserId) - .eq(ProcessNodeRecordAssignUser::getStatus, NodeStatusEnum.JXZ.getCode()) - .exists(); + .eq(ProcessNodeRecordAssignUser::getNodeRecordId, nodeRecord.getId()) + .eq(ProcessNodeRecordAssignUser::getTaskId, task.getId()) + .eq(ProcessNodeRecordAssignUser::getUserId, targetUserId) + .eq(ProcessNodeRecordAssignUser::getStatus, NodeStatusEnum.JXZ.getCode()) + .exists(); if (!assignmentRecorded) { return rollbackFailed("转办任务记录失败,请刷新后重试"); } - // 更新表单 - if (CollUtil.isNotEmpty(taskParamDto.getFormData())) { - runtimeService.setVariables(task.getExecutionId(), taskParamDto.getFormData()); - } - return R.ok(); - } + // 更新表单 + if (CollUtil.isNotEmpty(taskParamDto.getFormData())) { + runtimeService.setVariables(task.getExecutionId(), taskParamDto.getFormData()); + } + return R.ok(); + } - /** - * 结束流程 - *

- * 强制终止流程实例的执行。该方法会: 1. 递归查找并终止所有子流程实例 2. 更新流程实例记录状态为已结束 3. - * 设置结束原因为强制终止(finishReason=9) - *

- * 通常用于流程异常或需要强制结束的场景 - *

- * - * @param taskParamDto 任务参数,包含流程实例ID - * @return R 操作结果 - */ - @Override - public R stopProcessInstance(TaskParamDto taskParamDto) { - return terminateProcessInstance(taskParamDto.getProcessInstanceId(), - ProcessInstanceConstant.FINISH_REASON_TERMINATE); - } + /** + * 结束流程 + *

+ * 强制终止流程实例的执行。该方法会: 1. 递归查找并终止所有子流程实例 2. 更新流程实例记录状态为已结束 3. + * 设置结束原因为强制终止(finishReason=9) + *

+ * 通常用于流程异常或需要强制结束的场景 + *

+ * @param taskParamDto 任务参数,包含流程实例ID + * @return R 操作结果 + */ + @Override + public R stopProcessInstance(TaskParamDto taskParamDto) { + return terminateProcessInstance(taskParamDto.getProcessInstanceId(), + ProcessInstanceConstant.FINISH_REASON_TERMINATE); + } - /** - * 撤回流程 - * - * @param taskParamDto 任务参数,包含流程实例ID - * @return R 操作结果 - */ - @Override - @Transactional(rollbackFor = Exception.class) - public R withdrawProcessInstance(TaskParamDto taskParamDto) { - String processInstanceId = taskParamDto.getProcessInstanceId(); - if (StrUtil.isBlank(processInstanceId)) { - return R.failed("流程实例ID不能为空"); - } + /** + * 撤回流程 + * @param taskParamDto 任务参数,包含流程实例ID + * @return R 操作结果 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public R withdrawProcessInstance(TaskParamDto taskParamDto) { + String processInstanceId = taskParamDto.getProcessInstanceId(); + if (StrUtil.isBlank(processInstanceId)) { + return R.failed("流程实例ID不能为空"); + } - long userId = SecurityUtils.getUser().getId(); - ProcessInstanceRecord processInstanceRecord = processInstanceRecordService.lambdaQuery() - .eq(ProcessInstanceRecord::getProcessInstanceId, processInstanceId) - .one(); - if (processInstanceRecord == null - || !Objects.equals(processInstanceRecord.getStatus(), NodeStatusEnum.JXZ.getCode())) { - return R.failed("流程不存在或已结束"); - } + long userId = SecurityUtils.getUser().getId(); + ProcessInstanceRecord processInstanceRecord = processInstanceRecordService.lambdaQuery() + .eq(ProcessInstanceRecord::getProcessInstanceId, processInstanceId) + .one(); + if (processInstanceRecord == null + || !Objects.equals(processInstanceRecord.getStatus(), NodeStatusEnum.JXZ.getCode())) { + return R.failed("流程不存在或已结束"); + } - if (!Objects.equals(processInstanceRecord.getUserId(), userId)) { - return R.failed("仅发起人可撤回流程"); - } + if (!Objects.equals(processInstanceRecord.getUserId(), userId)) { + return R.failed("仅发起人可撤回流程"); + } - try { - Object rejectToStarter = runtimeService.getVariable(processInstanceId, - ProcessInstanceConstant.REJECT_TO_STARTER_VAR); - if (Boolean.TRUE.equals(rejectToStarter)) { - return R.failed("驳回待提交状态不可撤回"); - } - } catch (Exception e) { - log.warn("查询撤回状态变量失败: processInstanceId={}, error={}", processInstanceId, e.getMessage()); - } + try { + Object rejectToStarter = runtimeService.getVariable(processInstanceId, + ProcessInstanceConstant.REJECT_TO_STARTER_VAR); + if (Boolean.TRUE.equals(rejectToStarter)) { + return R.failed("驳回待提交状态不可撤回"); + } + } + catch (Exception e) { + log.warn("查询撤回状态变量失败: processInstanceId={}, error={}", processInstanceId, e.getMessage()); + } - boolean hasProcessedApproval = processNodeRecordAssignUserService.lambdaQuery() - .eq(ProcessNodeRecordAssignUser::getProcessInstanceId, processInstanceId) - .ne(ProcessNodeRecordAssignUser::getNodeId, "root") - .and(wrapper -> wrapper - .isNotNull(ProcessNodeRecordAssignUser::getEndTime) - .or() - .ne(ProcessNodeRecordAssignUser::getStatus, NodeStatusEnum.JXZ.getCode())) - .exists(); - if (hasProcessedApproval) { - return R.failed("首个审批节点已处理,无法撤回"); - } + boolean hasProcessedApproval = processNodeRecordAssignUserService.lambdaQuery() + .eq(ProcessNodeRecordAssignUser::getProcessInstanceId, processInstanceId) + .ne(ProcessNodeRecordAssignUser::getNodeId, "root") + .and(wrapper -> wrapper.isNotNull(ProcessNodeRecordAssignUser::getEndTime) + .or() + .ne(ProcessNodeRecordAssignUser::getStatus, NodeStatusEnum.JXZ.getCode())) + .exists(); + if (hasProcessedApproval) { + return R.failed("首个审批节点已处理,无法撤回"); + } - return terminateProcessInstance(processInstanceId, ProcessInstanceConstant.FINISH_REASON_WITHDRAW); - } + return terminateProcessInstance(processInstanceId, ProcessInstanceConstant.FINISH_REASON_WITHDRAW); + } - /** - * 退回 - *

- * 将任务退回到指定的历史节点重新处理。 常用于审批不通过需要重新修改的场景 - *

- * - * @param taskParamDto 任务参数,包含任务ID、目标节点ID、退回原因等 - * @return R 操作结果 - */ - @Override - public R back(TaskParamDto taskParamDto) { - taskParamDto.setUserId(SecurityUtils.getUser().getId().toString()); - return flowableEngineService.back(taskParamDto); - } + /** + * 退回 + *

+ * 将任务退回到指定的历史节点重新处理。 常用于审批不通过需要重新修改的场景 + *

+ * @param taskParamDto 任务参数,包含任务ID、目标节点ID、退回原因等 + * @return R 操作结果 + */ + @Override + public R back(TaskParamDto taskParamDto) { + taskParamDto.setUserId(SecurityUtils.getUser().getId().toString()); + return flowableEngineService.back(taskParamDto); + } - /** - * 重新提交任务(驳回到发起人后) - *

- * 当流程被驳回到发起人时,发起人使用此方法重新编辑表单并提交。 - * 该方法会: - * 1. 验证任务存在且属于当前用户 - * 2. 验证任务是 root 节点任务 - * 3. 更新表单数据 - * 4. 清理 rejectToStarter 流程变量 - * 5. 完成任务,流程继续到下一个审批节点 - *

- * - * @param taskParamDto 任务参数,包含taskId和更新后的formData - * @return R 操作结果 - */ - @Override - @SneakyThrows - @Transactional(rollbackFor = Exception.class) - public R resubmitTask(TaskParamDto taskParamDto) { - long userId = SecurityUtils.getUser().getId(); + /** + * 重新提交任务(驳回到发起人后) + *

+ * 当流程被驳回到发起人时,发起人使用此方法重新编辑表单并提交。 该方法会: 1. 验证任务存在且属于当前用户 2. 验证任务是 root 节点任务 3. 更新表单数据 + * 4. 清理 rejectToStarter 流程变量 5. 完成任务,流程继续到下一个审批节点 + *

+ * @param taskParamDto 任务参数,包含taskId和更新后的formData + * @return R 操作结果 + */ + @Override + @SneakyThrows + @Transactional(rollbackFor = Exception.class) + public R resubmitTask(TaskParamDto taskParamDto) { + long userId = SecurityUtils.getUser().getId(); - // 1. 获取任务并验证 - Task task = taskService.createTaskQuery() - .taskId(taskParamDto.getTaskId()) - .taskTenantId(TenantContextHolder.getTenantId().toString()) - .taskAssignee(String.valueOf(userId)) - .singleResult(); + // 1. 获取任务并验证 + Task task = taskService.createTaskQuery() + .taskId(taskParamDto.getTaskId()) + .taskTenantId(TenantContextHolder.getTenantId().toString()) + .taskAssignee(String.valueOf(userId)) + .singleResult(); - if (task == null) { - return R.failed("任务不存在或无权限操作"); - } + if (task == null) { + return R.failed("任务不存在或无权限操作"); + } - // 2. 验证是 root 任务(发起人节点) - if (!StrUtil.equals(task.getTaskDefinitionKey(), "root")) { - return R.failed("非重新提交任务,无法操作"); - } + // 2. 验证是 root 任务(发起人节点) + if (!StrUtil.equals(task.getTaskDefinitionKey(), "root")) { + return R.failed("非重新提交任务,无法操作"); + } - String processInstanceId = task.getProcessInstanceId(); + String processInstanceId = task.getProcessInstanceId(); - // 3. 更新表单数据 - if (CollUtil.isNotEmpty(taskParamDto.getFormData())) { - runtimeService.setVariables(task.getExecutionId(), taskParamDto.getFormData()); + // 3. 更新表单数据 + if (CollUtil.isNotEmpty(taskParamDto.getFormData())) { + runtimeService.setVariables(task.getExecutionId(), taskParamDto.getFormData()); - // 同步更新到 ProcessInstanceRecord - ProcessInstanceRecord processInstanceRecord = processInstanceRecordService.lambdaQuery() - .eq(ProcessInstanceRecord::getProcessInstanceId, processInstanceId) - .one(); + // 同步更新到 ProcessInstanceRecord + ProcessInstanceRecord processInstanceRecord = processInstanceRecordService.lambdaQuery() + .eq(ProcessInstanceRecord::getProcessInstanceId, processInstanceId) + .one(); - if (processInstanceRecord != null) { - Map existingFormData = new HashMap<>(); - if (StrUtil.isNotBlank(processInstanceRecord.getFormData())) { - existingFormData = objectMapper.readValue(processInstanceRecord.getFormData(), - new TypeReference>() { - }); - } - existingFormData.putAll(taskParamDto.getFormData()); - String updatedFormData = objectMapper.writeValueAsString(existingFormData); + if (processInstanceRecord != null) { + Map existingFormData = new HashMap<>(); + if (StrUtil.isNotBlank(processInstanceRecord.getFormData())) { + existingFormData = objectMapper.readValue(processInstanceRecord.getFormData(), + new TypeReference>() { + }); + } + existingFormData.putAll(taskParamDto.getFormData()); + String updatedFormData = objectMapper.writeValueAsString(existingFormData); - processInstanceRecordService.lambdaUpdate() - .set(ProcessInstanceRecord::getFormData, updatedFormData) - .eq(ProcessInstanceRecord::getProcessInstanceId, processInstanceId) - .update(); - } - } + processInstanceRecordService.lambdaUpdate() + .set(ProcessInstanceRecord::getFormData, updatedFormData) + .eq(ProcessInstanceRecord::getProcessInstanceId, processInstanceId) + .update(); + } + } - // 4. 清理 rejectToStarter 变量 - runtimeService.removeVariable(processInstanceId, "rejectToStarter"); + // 4. 清理 rejectToStarter 变量 + runtimeService.removeVariable(processInstanceId, "rejectToStarter"); - // 5. 完成任务,流程继续 - taskService.complete(taskParamDto.getTaskId()); + // 5. 完成任务,流程继续 + taskService.complete(taskParamDto.getTaskId()); - return R.ok("重新提交成功"); - } + return R.ok("重新提交成功"); + } private ProcessNodeRecord findActiveNodeRecord(Task task, ProcessNodeRecordAssignUser assignment) { if (task == null || assignment == null || assignment.getNodeRecordId() == null) { @@ -934,24 +927,24 @@ public class TaskServiceImpl implements ITaskService { return Collections.emptyList(); } return processNodeRecordAssignUserService.lambdaQuery() - .eq(ProcessNodeRecordAssignUser::getNodeRecordId, nodeRecordId) - .orderByAsc(ProcessNodeRecordAssignUser::getId) - .list() - .stream() - .map(ProcessNodeRecordAssignUser::getUserId) - .filter(Objects::nonNull) - .distinct() - .toList(); + .eq(ProcessNodeRecordAssignUser::getNodeRecordId, nodeRecordId) + .orderByAsc(ProcessNodeRecordAssignUser::getId) + .list() + .stream() + .map(ProcessNodeRecordAssignUser::getUserId) + .filter(Objects::nonNull) + .distinct() + .toList(); } private ProcessNodeRecordAssignUser findActiveAssignment(String taskId, long userId) { return processNodeRecordAssignUserService.lambdaQuery() - .eq(ProcessNodeRecordAssignUser::getTaskId, taskId) - .eq(ProcessNodeRecordAssignUser::getUserId, userId) - .eq(ProcessNodeRecordAssignUser::getStatus, NodeStatusEnum.JXZ.getCode()) - .orderByDesc(ProcessNodeRecordAssignUser::getId) - .last("limit 1") - .one(); + .eq(ProcessNodeRecordAssignUser::getTaskId, taskId) + .eq(ProcessNodeRecordAssignUser::getUserId, userId) + .eq(ProcessNodeRecordAssignUser::getStatus, NodeStatusEnum.JXZ.getCode()) + .orderByDesc(ProcessNodeRecordAssignUser::getId) + .last("limit 1") + .one(); } private boolean canAddSign(Task task, Node node, ProcessNodeRecord nodeRecord, @@ -974,9 +967,11 @@ public class TaskServiceImpl implements ITaskService { } return switch (assignedType) { case ProcessInstanceConstant.AssignedTypeClass.USER -> CollUtil.size(node.getNodeUserList()) > 1; - case ProcessInstanceConstant.AssignedTypeClass.LEADER, ProcessInstanceConstant.AssignedTypeClass.ROLE -> true; + case ProcessInstanceConstant.AssignedTypeClass.LEADER, ProcessInstanceConstant.AssignedTypeClass.ROLE -> + true; case ProcessInstanceConstant.AssignedTypeClass.SELF_SELECT, - ProcessInstanceConstant.AssignedTypeClass.FORM_USER -> Boolean.TRUE.equals(node.getMultiple()); + ProcessInstanceConstant.AssignedTypeClass.FORM_USER -> + Boolean.TRUE.equals(node.getMultiple()); case 7 -> node.getDeptLeaderLevel() != null && node.getDeptLeaderLevel() > 1; default -> false; }; @@ -984,9 +979,9 @@ public class TaskServiceImpl implements ITaskService { private boolean isProcessStarter(String processInstanceId, long userId) { return processInstanceRecordService.lambdaQuery() - .eq(ProcessInstanceRecord::getProcessInstanceId, processInstanceId) - .eq(ProcessInstanceRecord::getUserId, userId) - .exists(); + .eq(ProcessInstanceRecord::getProcessInstanceId, processInstanceId) + .eq(ProcessInstanceRecord::getUserId, userId) + .exists(); } private boolean isOriginalApprover(Task task, ProcessNodeRecordAssignUser assignment, long userId) { @@ -1005,13 +1000,13 @@ public class TaskServiceImpl implements ITaskService { } Set requestedIds = new HashSet<>(userIds); return response.getData() - .stream() - .filter(Objects::nonNull) - .filter(user -> requestedIds.contains(user.getUserId())) - .filter(user -> Objects.equals(user.getTenantId(), tenantId)) - .filter(user -> !StrUtil.equals("1", user.getDelFlag())) - .filter(user -> StrUtil.equals("0", user.getLockFlag())) - .toList(); + .stream() + .filter(Objects::nonNull) + .filter(user -> requestedIds.contains(user.getUserId())) + .filter(user -> Objects.equals(user.getTenantId(), tenantId)) + .filter(user -> !StrUtil.equals("1", user.getDelFlag())) + .filter(user -> StrUtil.equals("0", user.getLockFlag())) + .toList(); } catch (Exception exception) { log.warn("校验工作流目标用户失败: userIds={}", userIds, exception); @@ -1024,99 +1019,97 @@ public class TaskServiceImpl implements ITaskService { return R.failed(message); } - /** - * 递归获取所有需要停止的子流程实例ID列表 - *

- * 当一个流程包含子流程时,终止主流程需要同时终止所有子流程。 该方法递归查找指定流程的所有子流程实例ID - *

- * - * @param processInstanceId 父流程实例ID - * @return 所有子流程实例ID列表 - */ - private List getAllStopProcessInstanceIdList(String processInstanceId) { - List list = processInstanceRecordService.lambdaQuery() - .eq(ProcessInstanceRecord::getParentProcessInstanceId, processInstanceId) - .list(); + /** + * 递归获取所有需要停止的子流程实例ID列表 + *

+ * 当一个流程包含子流程时,终止主流程需要同时终止所有子流程。 该方法递归查找指定流程的所有子流程实例ID + *

+ * @param processInstanceId 父流程实例ID + * @return 所有子流程实例ID列表 + */ + private List getAllStopProcessInstanceIdList(String processInstanceId) { + List list = processInstanceRecordService.lambdaQuery() + .eq(ProcessInstanceRecord::getParentProcessInstanceId, processInstanceId) + .list(); - List collect = new ArrayList<>(list.stream().map(ProcessInstanceRecord::getProcessInstanceId).toList()); + List collect = new ArrayList<>(list.stream().map(ProcessInstanceRecord::getProcessInstanceId).toList()); - for (ProcessInstanceRecord processInstanceRecord : list) { - List allStopProcessInstanceIdList = getAllStopProcessInstanceIdList( - processInstanceRecord.getProcessInstanceId()); + for (ProcessInstanceRecord processInstanceRecord : list) { + List allStopProcessInstanceIdList = getAllStopProcessInstanceIdList( + processInstanceRecord.getProcessInstanceId()); - collect.addAll(allStopProcessInstanceIdList); + collect.addAll(allStopProcessInstanceIdList); - } - return collect; - } + } + return collect; + } - private R terminateProcessInstance(String reqProcessInstanceId, String finishReason) { - if (StrUtil.isBlank(reqProcessInstanceId)) { - return R.failed("流程实例ID不能为空"); - } + private R terminateProcessInstance(String reqProcessInstanceId, String finishReason) { + if (StrUtil.isBlank(reqProcessInstanceId)) { + return R.failed("流程实例ID不能为空"); + } - List allStopProcessInstanceIdList = getAllStopProcessInstanceIdList(reqProcessInstanceId); - CollUtil.reverse(allStopProcessInstanceIdList); - allStopProcessInstanceIdList.add(reqProcessInstanceId); + List allStopProcessInstanceIdList = getAllStopProcessInstanceIdList(reqProcessInstanceId); + CollUtil.reverse(allStopProcessInstanceIdList); + allStopProcessInstanceIdList.add(reqProcessInstanceId); - Date now = new Date(); - int assignUserStatus = Objects.equals(finishReason, ProcessInstanceConstant.FINISH_REASON_WITHDRAW) - ? ProcessInstanceConstant.ASSIGN_USER_STATUS_WITHDRAW - : ProcessInstanceConstant.ASSIGN_USER_STATUS_TERMINATE; + Date now = new Date(); + int assignUserStatus = Objects.equals(finishReason, ProcessInstanceConstant.FINISH_REASON_WITHDRAW) + ? ProcessInstanceConstant.ASSIGN_USER_STATUS_WITHDRAW + : ProcessInstanceConstant.ASSIGN_USER_STATUS_TERMINATE; - // 先更新本地记录,确保流程结束通知能拿到准确的 finishReason - processInstanceRecordService.lambdaUpdate() - .set(ProcessInstanceRecord::getStatus, NodeStatusEnum.YJS.getCode()) - .set(ProcessInstanceRecord::getFinishReason, finishReason) - .set(ProcessInstanceRecord::getEndTime, now) - .in(ProcessInstanceRecord::getProcessInstanceId, allStopProcessInstanceIdList) - .update(new ProcessInstanceRecord()); + // 先更新本地记录,确保流程结束通知能拿到准确的 finishReason + processInstanceRecordService.lambdaUpdate() + .set(ProcessInstanceRecord::getStatus, NodeStatusEnum.YJS.getCode()) + .set(ProcessInstanceRecord::getFinishReason, finishReason) + .set(ProcessInstanceRecord::getEndTime, now) + .in(ProcessInstanceRecord::getProcessInstanceId, allStopProcessInstanceIdList) + .update(new ProcessInstanceRecord()); - processNodeRecordService.lambdaUpdate() - .set(ProcessNodeRecord::getStatus, NodeStatusEnum.YJS.getCode()) - .set(ProcessNodeRecord::getEndTime, now) - .in(ProcessNodeRecord::getProcessInstanceId, allStopProcessInstanceIdList) - .eq(ProcessNodeRecord::getStatus, NodeStatusEnum.JXZ.getCode()) - .update(new ProcessNodeRecord()); + processNodeRecordService.lambdaUpdate() + .set(ProcessNodeRecord::getStatus, NodeStatusEnum.YJS.getCode()) + .set(ProcessNodeRecord::getEndTime, now) + .in(ProcessNodeRecord::getProcessInstanceId, allStopProcessInstanceIdList) + .eq(ProcessNodeRecord::getStatus, NodeStatusEnum.JXZ.getCode()) + .update(new ProcessNodeRecord()); - processNodeRecordAssignUserService.lambdaUpdate() - .set(ProcessNodeRecordAssignUser::getStatus, assignUserStatus) - .set(ProcessNodeRecordAssignUser::getEndTime, Convert.toLocalDateTime(now)) - .set(ProcessNodeRecordAssignUser::getTaskType, - Objects.equals(finishReason, ProcessInstanceConstant.FINISH_REASON_WITHDRAW) - ? "WITHDRAW" - : "TERMINATE") - .in(ProcessNodeRecordAssignUser::getProcessInstanceId, allStopProcessInstanceIdList) - .eq(ProcessNodeRecordAssignUser::getStatus, NodeStatusEnum.JXZ.getCode()) - .update(new ProcessNodeRecordAssignUser()); + processNodeRecordAssignUserService.lambdaUpdate() + .set(ProcessNodeRecordAssignUser::getStatus, assignUserStatus) + .set(ProcessNodeRecordAssignUser::getEndTime, Convert.toLocalDateTime(now)) + .set(ProcessNodeRecordAssignUser::getTaskType, + Objects.equals(finishReason, ProcessInstanceConstant.FINISH_REASON_WITHDRAW) ? "WITHDRAW" + : "TERMINATE") + .in(ProcessNodeRecordAssignUser::getProcessInstanceId, allStopProcessInstanceIdList) + .eq(ProcessNodeRecordAssignUser::getStatus, NodeStatusEnum.JXZ.getCode()) + .update(new ProcessNodeRecordAssignUser()); - allStopProcessInstanceIdList.forEach(processInstanceId -> { - ProcessInstance processInstance = runtimeService.createProcessInstanceQuery() - .processInstanceId(processInstanceId) - .processInstanceTenantId(TenantContextHolder.getTenantId().toString()) - .singleResult(); - if (processInstance == null) { - return; - } + allStopProcessInstanceIdList.forEach(processInstanceId -> { + ProcessInstance processInstance = runtimeService.createProcessInstanceQuery() + .processInstanceId(processInstanceId) + .processInstanceTenantId(TenantContextHolder.getTenantId().toString()) + .singleResult(); + if (processInstance == null) { + return; + } - List executionIds = runtimeService.createExecutionQuery() - .parentId(processInstanceId) - .list() - .stream() - .map(Execution::getId) - .toList(); - if (CollUtil.isEmpty(executionIds)) { - log.warn("流程实例缺少可迁移执行流,跳过状态迁移: processInstanceId={}", processInstanceId); - return; - } + List executionIds = runtimeService.createExecutionQuery() + .parentId(processInstanceId) + .list() + .stream() + .map(Execution::getId) + .toList(); + if (CollUtil.isEmpty(executionIds)) { + log.warn("流程实例缺少可迁移执行流,跳过状态迁移: processInstanceId={}", processInstanceId); + return; + } - runtimeService.setVariable(processInstanceId, ProcessInstanceConstant.FINISH_REASON_VAR, finishReason); - runtimeService.createChangeActivityStateBuilder() - .moveExecutionsToSingleActivityId(executionIds, "end") - .changeState(); - }); + runtimeService.setVariable(processInstanceId, ProcessInstanceConstant.FINISH_REASON_VAR, finishReason); + runtimeService.createChangeActivityStateBuilder() + .moveExecutionsToSingleActivityId(executionIds, "end") + .changeState(); + }); - return R.ok(); - } + return R.ok(); + } } diff --git a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/listeners/ApprovalCreateListener.java b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/listeners/ApprovalCreateListener.java index 31d27c102..b4051c7b8 100644 --- a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/listeners/ApprovalCreateListener.java +++ b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/listeners/ApprovalCreateListener.java @@ -20,14 +20,10 @@ import org.flowable.task.service.impl.persistence.entity.TaskEntityImpl; /** * 审批任务创建监听器 *

- * Flowable任务监听器实现,用于处理审批任务创建时的特殊逻辑。 - * 主要功能是处理没有指定执行人的任务,根据节点配置的"无人处理策略"自动执行相应操作。 + * Flowable任务监听器实现,用于处理审批任务创建时的特殊逻辑。 主要功能是处理没有指定执行人的任务,根据节点配置的"无人处理策略"自动执行相应操作。 *

- * 支持的无人处理策略: - * 1. 自动通过:任务自动完成并通过 - * 2. 指派给管理员:将任务分配给流程管理员 - * 3. 指派给特定用户:将任务分配给配置中指定的用户 - * 4. 自动拒绝:任务自动完成并拒绝 + * 支持的无人处理策略: 1. 自动通过:任务自动完成并通过 2. 指派给管理员:将任务分配给流程管理员 3. 指派给特定用户:将任务分配给配置中指定的用户 4. + * 自动拒绝:任务自动完成并拒绝 *

* 该监听器需要在流程定义的用户任务节点中配置,通过taskListener扩展属性指定。 * @@ -39,9 +35,7 @@ public class ApprovalCreateListener implements TaskListener { /** * 任务创建事件处理方法 *

- * 当任务被创建时触发,检查任务是否有执行人,如果没有则根据配置的策略自动处理。 - * 这个机制解决了流程执行过程中可能出现的无人处理任务的问题。 - * + * 当任务被创建时触发,检查任务是否有执行人,如果没有则根据配置的策略自动处理。 这个机制解决了流程执行过程中可能出现的无人处理任务的问题。 * @param delegateTask 委派的任务对象,包含任务的所有信息和上下文 */ @Override @@ -72,7 +66,7 @@ public class ApprovalCreateListener implements TaskListener { IRemoteService remoteService = SpringUtil.getBean(IRemoteService.class); // 查询节点原始配置数据,获取无人处理策略 - Node node = processNodeDataService.getNodeData(flowId, nodeId).getData(); + Node node = processNodeDataService.getPublishedNode(processDefinitionId, nodeId).getData(); Nobody nobody = node.getNobody(); // 获取无人处理配置 String handler = nobody.getHandler(); // 获取无人处理的处理方式 diff --git a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/listeners/FlowProcessEventListener.java b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/listeners/FlowProcessEventListener.java index 0f1663b83..1525b2c2a 100644 --- a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/listeners/FlowProcessEventListener.java +++ b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/listeners/FlowProcessEventListener.java @@ -75,7 +75,7 @@ public class FlowProcessEventListener implements FlowableEventListener { String flowId = NodeUtil.getFlowId(processDefinitionId); // 查询节点原始数据 - Node node = processNodeDataService.getNodeData(flowId, activityId).getData(); + Node node = processNodeDataService.getPublishedNode(processDefinitionId, activityId).getData(); // 构建节点开始记录参数并发送事件 ProcessNodeRecordParamDto processNodeRecordParamDto = new ProcessNodeRecordParamDto(); @@ -217,6 +217,7 @@ public class FlowProcessEventListener implements FlowableEventListener { // 构建任务完成记录参数并发送事件 ProcessNodeRecordAssignUserParamDto processNodeRecordAssignUserParamDto = new ProcessNodeRecordAssignUserParamDto(); processNodeRecordAssignUserParamDto.setFlowId(flowId); + processNodeRecordAssignUserParamDto.setProcessDefinitionId(processDefinitionId); processNodeRecordAssignUserParamDto.setProcessInstanceId(processInstanceId); processNodeRecordAssignUserParamDto .setData(objectMapper.writeValueAsString(taskService.getVariables(task.getId()))); @@ -258,28 +259,29 @@ public class FlowProcessEventListener implements FlowableEventListener { // 构建任务分配记录参数并发送事件 ProcessNodeRecordAssignUserParamDto processNodeRecordAssignUserParamDto = new ProcessNodeRecordAssignUserParamDto(); processNodeRecordAssignUserParamDto.setFlowId(flowId); + processNodeRecordAssignUserParamDto.setProcessDefinitionId(processDefinitionId); processNodeRecordAssignUserParamDto.setProcessInstanceId(processInstanceId); processNodeRecordAssignUserParamDto.setNodeId(taskDefinitionKey); processNodeRecordAssignUserParamDto.setUserId(Long.parseLong(task.getAssignee())); processNodeRecordAssignUserParamDto.setTaskId(task.getId()); processNodeRecordAssignUserParamDto.setNodeName(task.getName()); - processNodeRecordAssignUserParamDto.setNodeRecordId( - Convert.toLong(task.getVariable(ProcessInstanceConstant.VAR_NODE_RECORD_ID), null)); - processNodeRecordAssignUserParamDto.setAssignmentOrigin( - Convert.toStr(task.getVariable(ProcessInstanceConstant.VAR_ASSIGNMENT_ORIGIN))); - processNodeRecordAssignUserParamDto.setInitialUserId( - Convert.toLong(task.getVariable(ProcessInstanceConstant.VAR_INITIAL_USER_ID), null)); + processNodeRecordAssignUserParamDto + .setNodeRecordId(Convert.toLong(task.getVariable(ProcessInstanceConstant.VAR_NODE_RECORD_ID), null)); + processNodeRecordAssignUserParamDto + .setAssignmentOrigin(Convert.toStr(task.getVariable(ProcessInstanceConstant.VAR_ASSIGNMENT_ORIGIN))); + processNodeRecordAssignUserParamDto + .setInitialUserId(Convert.toLong(task.getVariable(ProcessInstanceConstant.VAR_INITIAL_USER_ID), null)); processNodeRecordAssignUserParamDto.setAddSignRecordId( Convert.toLong(task.getVariable(ProcessInstanceConstant.VAR_ADD_SIGN_RECORD_ID), null)); // 根据委派状态设置任务类型 String assignmentOperation = Convert - .toStr(task.getVariableLocal(ProcessInstanceConstant.VAR_ASSIGNMENT_OPERATION)); + .toStr(task.getVariableLocal(ProcessInstanceConstant.VAR_ASSIGNMENT_OPERATION)); processNodeRecordAssignUserParamDto .setTaskType(StrUtil.isNotBlank(assignmentOperation) ? assignmentOperation : (StrUtil.equals(DelegationState.PENDING.toString(), delegationStateString) ? "DELEGATION" - : (StrUtil.equals(DelegationState.RESOLVED.toString(), delegationStateString) ? "RESOLVED" - : StrUtil.EMPTY))); + : (StrUtil.equals(DelegationState.RESOLVED.toString(), delegationStateString) + ? "RESOLVED" : StrUtil.EMPTY))); processNodeRecordAssignUserParamDto.setApproveDesc(Convert.toStr(task.getVariableLocal("approveDesc"))); processNodeRecordAssignUserParamDto.setExecutionId(task.getExecutionId()); diff --git a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/listeners/NodeTaskCompleteNotify.java b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/listeners/NodeTaskCompleteNotify.java index f40a04d5e..7c80cbeed 100644 --- a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/listeners/NodeTaskCompleteNotify.java +++ b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/listeners/NodeTaskCompleteNotify.java @@ -57,7 +57,8 @@ public class NodeTaskCompleteNotify { // 获取节点配置并发送通知 RetOps - .of(processNodeDataService.getNodeData(taskCompleteParamDto.getFlowId(), taskCompleteParamDto.getNodeId())) + .of(processNodeDataService.getPublishedNode(taskCompleteParamDto.getProcessDefinitionId(), + taskCompleteParamDto.getNodeId())) .getData() .ifPresent(node -> { // 验证事件配置是否为有效的URL @@ -75,8 +76,7 @@ public class NodeTaskCompleteNotify { /** * 异步发送流程实例状态变更通知 *

- * 当流程实例状态发生变更时,查询最新的流程实例记录状态, - * 并通过Redis发布流程状态变更事件,供订阅方消费处理。 + * 当流程实例状态发生变更时,查询最新的流程实例记录状态, 并通过Redis发布流程状态变更事件,供订阅方消费处理。 *

* @param token 安全令牌,用于在异步线程中传递认证信息 * @param instanceRecordParamDto 流程实例参数,包含流程实例ID等信息 diff --git a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/node/MultiInstanceHandler.java b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/node/MultiInstanceHandler.java index 08bdf5d06..d5f9df833 100644 --- a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/node/MultiInstanceHandler.java +++ b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/node/MultiInstanceHandler.java @@ -71,7 +71,7 @@ public class MultiInstanceHandler { }).get(0); // 节点数据 - Node node = processNodeDataService.getNodeData(flowId, nodeId).getData(); + Node node = processNodeDataService.getPublishedNode(execution.getProcessDefinitionId(), nodeId).getData(); if (node != null) { Map variables = execution.getVariables(); variables.put("execution", execution); @@ -116,7 +116,7 @@ public class MultiInstanceHandler { String processDefinitionKey = entity.getProcessDefinitionKey(); String nodeId = execution.getCurrentActivityId(); - Node node = processNodeDataService.getNodeData(processDefinitionKey, nodeId).getData(); + Node node = processNodeDataService.getPublishedNode(execution.getProcessDefinitionId(), nodeId).getData(); Integer multipleMode = node.getMultipleMode(); BigDecimal modePercentage = BigDecimal.valueOf(100); diff --git a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/tasks/ApproveServiceTask.java b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/tasks/ApproveServiceTask.java index 2eada9c80..719d64c11 100644 --- a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/tasks/ApproveServiceTask.java +++ b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/tasks/ApproveServiceTask.java @@ -14,17 +14,11 @@ import org.flowable.engine.impl.persistence.entity.ExecutionEntityImpl; /** * 审批服务任务 *

- * Flowable服务任务(Service Task)实现,用于处理审批节点的自动化逻辑。 - * 该任务在审批节点执行后自动触发,根据审批结果(通过/拒绝)决定流程的走向。 + * Flowable服务任务(Service Task)实现,用于处理审批节点的自动化逻辑。 该任务在审批节点执行后自动触发,根据审批结果(通过/拒绝)决定流程的走向。 *

- * 主要功能: - * 1. 读取审批节点的审批结果 - * 2. 如果审批通过,流程继续正常执行 - * 3. 如果审批拒绝,根据配置执行拒绝策略(跳转到指定节点或结束流程) + * 主要功能: 1. 读取审批节点的审批结果 2. 如果审批通过,流程继续正常执行 3. 如果审批拒绝,根据配置执行拒绝策略(跳转到指定节点或结束流程) *

- * 使用方式: - * 在流程定义中,将此类配置为审批节点后的服务任务, - * 任务ID需要遵循命名规则:approve_service_task_{nodeId} + * 使用方式: 在流程定义中,将此类配置为审批节点后的服务任务, 任务ID需要遵循命名规则:approve_service_task_{nodeId} * * @author pigx */ @@ -33,9 +27,7 @@ public class ApproveServiceTask implements JavaDelegate { /** * 执行审批服务任务 *

- * 根据审批节点的审批结果决定流程的下一步走向。 - * 如果审批通过,流程正常继续;如果审批拒绝,执行拒绝策略。 - * + * 根据审批节点的审批结果决定流程的下一步走向。 如果审批通过,流程正常继续;如果审批拒绝,执行拒绝策略。 * @param execution 流程执行上下文,包含流程实例信息和变量 */ @Override @@ -45,7 +37,7 @@ public class ApproveServiceTask implements JavaDelegate { // 从执行上下文中提取关键标识符 String currentActivityId = entity.getActivityId(); // 当前活动ID - String flowDefinitionKey = entity.getProcessDefinitionKey(); // 流程定义键 + String processDefinitionId = entity.getProcessDefinitionId(); // 实例绑定的发布版本 String processInstanceId = entity.getProcessInstanceId(); // 流程实例ID // 从活动ID中提取节点标识符 @@ -64,7 +56,7 @@ public class ApproveServiceTask implements JavaDelegate { // 如果未批准,确定工作流重定向的位置 if (!isApproved) { - handleRejection(flowDefinitionKey, nodeId, currentActivityId, processInstanceId, runtimeService); + handleRejection(processDefinitionId, nodeId, currentActivityId, processInstanceId, runtimeService); } // 如果已批准,工作流将沿其正常路径继续(无需操作) } @@ -73,24 +65,20 @@ public class ApproveServiceTask implements JavaDelegate { /** * 处理任务被拒绝时的流程重定向逻辑 *

- * 根据节点配置的拒绝策略,将流程重定向到指定节点或结束节点。 - * 支持的拒绝策略: - * 1. TO_NODE:跳转到指定节点 - * 2. 其他:直接结束流程 - * - * @param flowDefinitionKey 流程定义键 - * @param nodeId 当前节点标识符 + * 根据节点配置的拒绝策略,将流程重定向到指定节点或结束节点。 支持的拒绝策略: 1. TO_NODE:跳转到指定节点 2. 其他:直接结束流程 + * @param processDefinitionId 流程定义 ID + * @param nodeId 当前节点标识符 * @param currentActivityId 当前活动标识符 * @param processInstanceId 流程实例标识符 - * @param runtimeService 用于流程状态操作的运行时服务 + * @param runtimeService 用于流程状态操作的运行时服务 */ - private void handleRejection(String flowDefinitionKey, String nodeId, String currentActivityId, + private void handleRejection(String processDefinitionId, String nodeId, String currentActivityId, String processInstanceId, RuntimeService runtimeService) { // 获取流程节点数据服务以获取节点配置数据 IProcessNodeDataService processNodeDataService = SpringUtil.getBean(IProcessNodeDataService.class); // 查询节点的原始配置数据 - Node node = processNodeDataService.getNodeData(flowDefinitionKey, nodeId).getData(); + Node node = processNodeDataService.getPublishedNode(processDefinitionId, nodeId).getData(); // 获取拒绝处理配置 Refuse refuseConfig = node.getRefuse(); diff --git a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/tasks/CopyServiceTask.java b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/tasks/CopyServiceTask.java index 9450bd681..ad79ac72f 100644 --- a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/tasks/CopyServiceTask.java +++ b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/tasks/CopyServiceTask.java @@ -26,19 +26,12 @@ import java.util.stream.Collectors; /** * 抄送服务任务 *

- * Flowable服务任务(Service Task)实现,用于处理流程中的抄送功能。 - * 抄送任务会将流程信息发送给指定的用户或部门成员,供其查看但不需要审批。 + * Flowable服务任务(Service Task)实现,用于处理流程中的抄送功能。 抄送任务会将流程信息发送给指定的用户或部门成员,供其查看但不需要审批。 *

- * 主要功能: - * 1. 获取节点配置的抄送人员(支持用户、部门、角色、岗位) - * 2. 将部门、角色、岗位展开为具体的用户列表 - * 3. 为每个抄送人创建抄送记录 - * 4. 记录流程快照信息供抄送人查看 + * 主要功能: 1. 获取节点配置的抄送人员(支持用户、部门、角色、岗位) 2. 将部门、角色、岗位展开为具体的用户列表 3. 为每个抄送人创建抄送记录 4. + * 记录流程快照信息供抄送人查看 *

- * 使用场景: - * - 需要通知相关人员但不需要审批的节点 - * - 流程信息备案和知会 - * - 跨部门协作中的信息同步 + * 使用场景: - 需要通知相关人员但不需要审批的节点 - 流程信息备案和知会 - 跨部门协作中的信息同步 * * @author pigx */ @@ -47,9 +40,7 @@ public class CopyServiceTask implements JavaDelegate { /** * 执行抄送任务 *

- * 从节点配置中获取抄送人员列表,创建抄送记录并通过远程服务保存。 - * 支持直接指定用户或指定部门(会展开为部门下的所有用户)。 - * + * 从节点配置中获取抄送人员列表,创建抄送记录并通过远程服务保存。 支持直接指定用户或指定部门(会展开为部门下的所有用户)。 * @param execution 流程执行上下文,包含流程实例信息和变量 * @throws Exception 当JSON解析或远程服务调用失败时抛出异常 */ @@ -63,7 +54,7 @@ public class CopyServiceTask implements JavaDelegate { IRemoteService remoteService = SpringUtil.getBean(IRemoteService.class); IProcessNodeDataService processNodeDataService = SpringUtil.getBean(IProcessNodeDataService.class); - Node node = processNodeDataService.getNodeData(flowId, nodeId).getData(); + Node node = processNodeDataService.getPublishedNode(execution.getProcessDefinitionId(), nodeId).getData(); // 获取指定人员 List userDtoList = node.getNodeUserList(); @@ -80,29 +71,29 @@ public class CopyServiceTask implements JavaDelegate { if (CollUtil.isNotEmpty(deptIdList)) { R> r = remoteService.queryUserIdListByDepIdList(deptIdList); - addUserIds(userIdList, r.getData()); - } + addUserIds(userIdList, r.getData()); + } - // 角色ID列表 - List roleIdList = userDtoList.stream() - .filter(w -> StrUtil.equals(w.getType(), NodeUserTypeEnum.ROLE.getKey())) - .map(NodeUser::getId) - .toList(); + // 角色ID列表 + List roleIdList = userDtoList.stream() + .filter(w -> StrUtil.equals(w.getType(), NodeUserTypeEnum.ROLE.getKey())) + .map(NodeUser::getId) + .toList(); - if (CollUtil.isNotEmpty(roleIdList)) { - R> r = remoteService.queryUserIdListByRoleIdList(roleIdList); - addUserIds(userIdList, r.getData()); - } + if (CollUtil.isNotEmpty(roleIdList)) { + R> r = remoteService.queryUserIdListByRoleIdList(roleIdList); + addUserIds(userIdList, r.getData()); + } - // 岗位ID列表 - List postIdList = userDtoList.stream() - .filter(w -> StrUtil.equals(w.getType(), NodeUserTypeEnum.POST.getKey())) - .map(NodeUser::getId) - .toList(); + // 岗位ID列表 + List postIdList = userDtoList.stream() + .filter(w -> StrUtil.equals(w.getType(), NodeUserTypeEnum.POST.getKey())) + .map(NodeUser::getId) + .toList(); - if (CollUtil.isNotEmpty(postIdList)) { - R> r = remoteService.queryUserIdListByPostIdList(postIdList); - addUserIds(userIdList, r.getData()); + if (CollUtil.isNotEmpty(postIdList)) { + R> r = remoteService.queryUserIdListByPostIdList(postIdList); + addUserIds(userIdList, r.getData()); } ObjectMapper objectMapper = SpringUtil.getBean(ObjectMapper.class); @@ -114,7 +105,7 @@ public class CopyServiceTask implements JavaDelegate { .get(0); Map variables = execution.getVariables(); - String formData = objectMapper.writeValueAsString(variables); + String formData = objectMapper.writeValueAsString(variables); for (String userIds : userIdList) { // 发送抄送任务 @@ -125,28 +116,27 @@ public class CopyServiceTask implements JavaDelegate { processCopyDto.setProcessInstanceId(execution.getProcessInstanceId()); processCopyDto.setNodeId(nodeId); processCopyDto.setNodeName(node.getName()); - processCopyDto.setFormData(formData); + processCopyDto.setFormData(formData); processCopyDto.setUserId(Long.parseLong(userIds)); remoteService.saveCC(processCopyDto); } } - /** - * 将用户ID列表合并到目标列表中(去重) - * - * @param targetList 目标用户ID列表 - * @param sourceList 待合并的用户ID列表 - */ - private void addUserIds(List targetList, List sourceList) { - if (CollUtil.isNotEmpty(sourceList)) { - for (Long userId : sourceList) { - String userIdStr = Convert.toStr(userId); - if (!targetList.contains(userIdStr)) { - targetList.add(userIdStr); - } - } - } - } + /** + * 将用户ID列表合并到目标列表中(去重) + * @param targetList 目标用户ID列表 + * @param sourceList 待合并的用户ID列表 + */ + private void addUserIds(List targetList, List sourceList) { + if (CollUtil.isNotEmpty(sourceList)) { + for (Long userId : sourceList) { + String userIdStr = Convert.toStr(userId); + if (!targetList.contains(userIdStr)) { + targetList.add(userIdStr); + } + } + } + } } diff --git a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/tasks/TriggerServiceTask.java b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/tasks/TriggerServiceTask.java index 53712f27c..3cb34d009 100644 --- a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/tasks/TriggerServiceTask.java +++ b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/tasks/TriggerServiceTask.java @@ -29,11 +29,9 @@ import java.util.Map; /** * 触发器服务任务 *

- * 在流程执行过程中调用外部 HTTP POST 接口, - * 支持传递表单数据作为请求参数,并将返回值映射回流程变量。 + * 在流程执行过程中调用外部 HTTP POST 接口, 支持传递表单数据作为请求参数,并将返回值映射回流程变量。 *

- * HTTP 状态码 200 即视为调用成功,响应体按 JSON 对象解析后进行返回值映射。 - * 异常处理支持两种策略:THROW(抛出异常)和 TERMINATE(终止流程)。 + * HTTP 状态码 200 即视为调用成功,响应体按 JSON 对象解析后进行返回值映射。 异常处理支持两种策略:THROW(抛出异常)和 TERMINATE(终止流程)。 * * @author pigx * @date 2026-03-03 @@ -41,162 +39,173 @@ import java.util.Map; @Slf4j public class TriggerServiceTask implements JavaDelegate { - private static final int READ_TIMEOUT = 30000; + private static final int READ_TIMEOUT = 30000; - @SneakyThrows - @Override - public void execute(DelegateExecution execution) { - ExecutionEntityImpl entity = (ExecutionEntityImpl) execution; - String nodeId = entity.getActivityId(); - String flowId = entity.getProcessDefinitionKey(); + /** + * 按当前实例的发布定义读取触发器配置,调用外部 POST 接口并将响应映射回流程变量。 请求或响应处理失败时遵循配置的 THROW / TERMINATE + * 策略;节点快照缺失时直接失败。 + * @param execution 当前触发器节点的 Flowable 执行上下文,不允许为 null,须为 ExecutionEntityImpl + * @throws IllegalStateException 当前发布定义缺少业务节点快照时抛出 + * @throws FlowableException 配置无效或调用失败时抛出;TERMINATE 策略使用特定异常前缀交由上层终止实例 + */ + @SneakyThrows + @Override + public void execute(DelegateExecution execution) { + ExecutionEntityImpl entity = (ExecutionEntityImpl) execution; + String nodeId = entity.getActivityId(); + String flowId = entity.getProcessDefinitionKey(); - IProcessNodeDataService processNodeDataService = SpringUtil.getBean(IProcessNodeDataService.class); - Node node = processNodeDataService.getNodeData(flowId, nodeId).getData(); + IProcessNodeDataService processNodeDataService = SpringUtil.getBean(IProcessNodeDataService.class); + Node node = processNodeDataService.getPublishedNode(execution.getProcessDefinitionId(), nodeId).getData(); - TriggerConfig config = node.getTriggerConfig(); - if (config == null || StrUtil.isBlank(config.getUrl())) { - throw new FlowableException("触发器配置无效: URL为空"); - } + TriggerConfig config = node.getTriggerConfig(); + if (config == null || StrUtil.isBlank(config.getUrl())) { + throw new FlowableException("触发器配置无效: URL为空"); + } - // 构建请求头 - Map headerMap = new HashMap<>(); - headerMap.put("Content-Type", "application/json"); - if (CollUtil.isNotEmpty(config.getHeaders())) { - for (TriggerParam param : config.getHeaders()) { - String value = resolveParamValue(param, execution); - if (StrUtil.isNotBlank(param.getKey()) && value != null) { - headerMap.put(param.getKey(), value); - } - } - } + // 构建请求头 + Map headerMap = new HashMap<>(); + headerMap.put("Content-Type", "application/json"); + if (CollUtil.isNotEmpty(config.getHeaders())) { + for (TriggerParam param : config.getHeaders()) { + String value = resolveParamValue(param, execution); + if (StrUtil.isNotBlank(param.getKey()) && value != null) { + headerMap.put(param.getKey(), value); + } + } + } - // 构建请求体 - Map body = new HashMap<>(); - if (CollUtil.isNotEmpty(config.getBody())) { - for (TriggerParam param : config.getBody()) { - Object value = resolveParamObjectValue(param, execution); - if (StrUtil.isNotBlank(param.getKey())) { - body.put(param.getKey(), value); - } - } - } + // 构建请求体 + Map body = new HashMap<>(); + if (CollUtil.isNotEmpty(config.getBody())) { + for (TriggerParam param : config.getBody()) { + Object value = resolveParamObjectValue(param, execution); + if (StrUtil.isNotBlank(param.getKey())) { + body.put(param.getKey(), value); + } + } + } - // 发起 POST 请求 - try { - HttpResponse response = HttpRequest.post(config.getUrl()) - .headerMap(headerMap, true) - .body(JSONUtil.toJsonStr(body)) - .timeout(READ_TIMEOUT) - .execute(); + // 发起 POST 请求 + try { + HttpResponse response = HttpRequest.post(config.getUrl()) + .headerMap(headerMap, true) + .body(JSONUtil.toJsonStr(body)) + .timeout(READ_TIMEOUT) + .execute(); - // HTTP 状态码非 2xx 视为调用失败 - if (!response.isOk()) { - String msg = StrUtil.format("HTTP请求失败: status={}, body={}", response.getStatus(), response.body()); - log.warn("触发器返回异常: url={}, {}", config.getUrl(), msg); - handleError(config.getErrorHandler(), execution, msg); - return; - } + // HTTP 状态码非 2xx 视为调用失败 + if (!response.isOk()) { + String msg = StrUtil.format("HTTP请求失败: status={}, body={}", response.getStatus(), response.body()); + log.warn("触发器返回异常: url={}, {}", config.getUrl(), msg); + handleError(config.getErrorHandler(), execution, msg); + return; + } - // 响应体按 JSON 对象解析,进行返回值映射 - String responseBody = response.body(); - if (StrUtil.isNotBlank(responseBody)) { - ObjectMapper objectMapper = SpringUtil.getBean(ObjectMapper.class); - Object data = objectMapper.readValue(responseBody, Object.class); - mapReturnValues(config.getReturnMapping(), data, execution); - } - } catch (Exception ex) { - log.error("触发器调用异常: url={}, error={}", config.getUrl(), ex.getMessage()); - handleError(config.getErrorHandler(), execution, ex.getMessage()); - } - } + // 响应体按 JSON 对象解析,进行返回值映射 + String responseBody = response.body(); + if (StrUtil.isNotBlank(responseBody)) { + ObjectMapper objectMapper = SpringUtil.getBean(ObjectMapper.class); + Object data = objectMapper.readValue(responseBody, Object.class); + mapReturnValues(config.getReturnMapping(), data, execution); + } + } + catch (Exception ex) { + log.error("触发器调用异常: url={}, error={}", config.getUrl(), ex.getMessage()); + handleError(config.getErrorHandler(), execution, ex.getMessage()); + } + } - private String resolveParamValue(TriggerParam param, DelegateExecution execution) { - Object value = resolveParamObjectValue(param, execution); - return value != null ? value.toString() : null; - } + private String resolveParamValue(TriggerParam param, DelegateExecution execution) { + Object value = resolveParamObjectValue(param, execution); + return value != null ? value.toString() : null; + } - private Object resolveParamObjectValue(TriggerParam param, DelegateExecution execution) { - if (Boolean.TRUE.equals(param.getUseForm()) && StrUtil.isNotBlank(param.getFormFieldId())) { - return execution.getVariable(param.getFormFieldId()); - } - return param.getValue(); - } + private Object resolveParamObjectValue(TriggerParam param, DelegateExecution execution) { + if (Boolean.TRUE.equals(param.getUseForm()) && StrUtil.isNotBlank(param.getFormFieldId())) { + return execution.getVariable(param.getFormFieldId()); + } + return param.getValue(); + } - @SneakyThrows - @SuppressWarnings("unchecked") - private void mapReturnValues(List mappings, Object data, DelegateExecution execution) { - if (CollUtil.isEmpty(mappings) || data == null) { - return; - } + @SneakyThrows + @SuppressWarnings("unchecked") + private void mapReturnValues(List mappings, Object data, DelegateExecution execution) { + if (CollUtil.isEmpty(mappings) || data == null) { + return; + } - Map dataMap; - if (data instanceof Map) { - dataMap = (Map) data; - } else { - ObjectMapper objectMapper = SpringUtil.getBean(ObjectMapper.class); - dataMap = objectMapper.convertValue(data, Map.class); - } + Map dataMap; + if (data instanceof Map) { + dataMap = (Map) data; + } + else { + ObjectMapper objectMapper = SpringUtil.getBean(ObjectMapper.class); + dataMap = objectMapper.convertValue(data, Map.class); + } - // 收集需要更新的表单字段 - Map formUpdates = new HashMap<>(); - for (TriggerReturnMapping mapping : mappings) { - Object value = dataMap.get(mapping.getResponseKey()); - if (value != null && StrUtil.isNotBlank(mapping.getFormFieldId())) { - execution.setVariable(mapping.getFormFieldId(), value); - formUpdates.put(mapping.getFormFieldId(), value); - } - } + // 收集需要更新的表单字段 + Map formUpdates = new HashMap<>(); + for (TriggerReturnMapping mapping : mappings) { + Object value = dataMap.get(mapping.getResponseKey()); + if (value != null && StrUtil.isNotBlank(mapping.getFormFieldId())) { + execution.setVariable(mapping.getFormFieldId(), value); + formUpdates.put(mapping.getFormFieldId(), value); + } + } - // 同步更新 ProcessInstanceRecord.formData - if (CollUtil.isNotEmpty(formUpdates)) { - syncFormData(execution.getProcessInstanceId(), formUpdates); - } - } + // 同步更新 ProcessInstanceRecord.formData + if (CollUtil.isNotEmpty(formUpdates)) { + syncFormData(execution.getProcessInstanceId(), formUpdates); + } + } - /** - * 同步更新流程实例记录的表单数据 - */ - @SneakyThrows - private void syncFormData(String processInstanceId, Map formUpdates) { - try { - ObjectMapper objectMapper = SpringUtil.getBean(ObjectMapper.class); - IProcessInstanceRecordService recordService = SpringUtil.getBean(IProcessInstanceRecordService.class); + /** + * 同步更新流程实例记录的表单数据 + */ + @SneakyThrows + private void syncFormData(String processInstanceId, Map formUpdates) { + try { + ObjectMapper objectMapper = SpringUtil.getBean(ObjectMapper.class); + IProcessInstanceRecordService recordService = SpringUtil.getBean(IProcessInstanceRecordService.class); - ProcessInstanceRecord record = recordService.lambdaQuery() - .eq(ProcessInstanceRecord::getProcessInstanceId, processInstanceId) - .one(); + ProcessInstanceRecord record = recordService.lambdaQuery() + .eq(ProcessInstanceRecord::getProcessInstanceId, processInstanceId) + .one(); - if (record == null) { - return; - } + if (record == null) { + return; + } - Map existingFormData = new HashMap<>(); - if (StrUtil.isNotBlank(record.getFormData())) { - existingFormData = objectMapper.readValue(record.getFormData(), new TypeReference>() { - }); - } + Map existingFormData = new HashMap<>(); + if (StrUtil.isNotBlank(record.getFormData())) { + existingFormData = objectMapper.readValue(record.getFormData(), + new TypeReference>() { + }); + } - existingFormData.putAll(formUpdates); + existingFormData.putAll(formUpdates); - String updatedFormData = objectMapper.writeValueAsString(existingFormData); - recordService.lambdaUpdate() - .set(ProcessInstanceRecord::getFormData, updatedFormData) - .eq(ProcessInstanceRecord::getProcessInstanceId, processInstanceId) - .update(); + String updatedFormData = objectMapper.writeValueAsString(existingFormData); + recordService.lambdaUpdate() + .set(ProcessInstanceRecord::getFormData, updatedFormData) + .eq(ProcessInstanceRecord::getProcessInstanceId, processInstanceId) + .update(); - log.debug("触发器同步更新流程实例 {} 的表单数据", processInstanceId); - } catch (Exception e) { - log.error("触发器同步更新表单数据失败: {}", e.getMessage(), e); - } - } + log.debug("触发器同步更新流程实例 {} 的表单数据", processInstanceId); + } + catch (Exception e) { + log.error("触发器同步更新表单数据失败: {}", e.getMessage(), e); + } + } - private void handleError(String handler, DelegateExecution execution, String msg) { - if ("TERMINATE".equals(handler)) { - // 不在 JavaDelegate 内调用 deleteProcessInstance,因为后续异常会导致 Flowable 事务回滚 - // 抛出带特定前缀的异常,由上层 completeTask 捕获后在事务外执行终止操作 - throw new FlowableException("TRIGGER_TERMINATE:" + msg); - } - throw new FlowableException("触发器调用失败: " + msg); - } + private void handleError(String handler, DelegateExecution execution, String msg) { + if ("TERMINATE".equals(handler)) { + // 不在 JavaDelegate 内调用 deleteProcessInstance,因为后续异常会导致 Flowable 事务回滚 + // 抛出带特定前缀的异常,由上层 completeTask 捕获后在事务外执行终止操作 + throw new FlowableException("TRIGGER_TERMINATE:" + msg); + } + throw new FlowableException("触发器调用失败: " + msg); + } } diff --git a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/utils/ModelUtil.java b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/utils/ModelUtil.java index f5637f167..ff0163b4c 100644 --- a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/utils/ModelUtil.java +++ b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/utils/ModelUtil.java @@ -3,15 +3,11 @@ package com.pig4cloud.pigx.flow.support.utils; import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.RandomUtil; import cn.hutool.core.util.StrUtil; -import cn.hutool.extra.spring.SpringUtil; -import cn.hutool.json.JSONUtil; import com.pig4cloud.pigx.flow.constant.NodeTypeEnum; import com.pig4cloud.pigx.flow.constant.ProcessInstanceConstant; import com.pig4cloud.pigx.flow.dto.Node; -import com.pig4cloud.pigx.flow.dto.ProcessNodeDataDto; import com.pig4cloud.pigx.flow.dto.TimerConfig; import com.pig4cloud.pigx.flow.dto.TimerDuration; -import com.pig4cloud.pigx.flow.service.IProcessNodeDataService; import com.pig4cloud.pigx.flow.support.expression.condition.NodeExpressionStrategyFactory; import com.pig4cloud.pigx.flow.support.listeners.ApprovalCreateListener; import com.pig4cloud.pigx.flow.support.listeners.FlowProcessEventListener; @@ -30,22 +26,12 @@ import java.util.List; /** * BPMN模型构建工具类 *

- * 负责将前端设计的流程节点结构转换为Flowable引擎可执行的BPMN模型。 - * 该工具类是流程定义创建的核心,支持各种节点类型和流程结构的转换。 + * 负责将前端设计的流程节点结构转换为Flowable引擎可执行的BPMN模型。 该工具类是流程定义创建的核心,支持各种节点类型和流程结构的转换。 *

- * 主要功能: - * 1. 将前端节点树结构转换为BPMN模型 - * 2. 创建各种类型的流程节点(开始、结束、审批、抄送、网关等) - * 3. 创建节点间的连接线(顺序流) - * 4. 配置节点的执行策略和监听器 - * 5. 处理多实例任务(会签、或签) + * 主要功能: 1. 将前端节点树结构转换为BPMN模型 2. 创建各种类型的流程节点(开始、结束、审批、抄送、网关等) 3. 创建节点间的连接线(顺序流) 4. + * 配置节点的执行策略和监听器 5. 处理多实例任务(会签、或签) *

- * 支持的节点类型: - * - 开始节点、结束节点 - * - 审批节点(支持单人、会签、或签) - * - 抄送节点 - * - 排他网关(条件分支) - * - 并行网关 + * 支持的节点类型: - 开始节点、结束节点 - 审批节点(支持单人、会签、或签) - 抄送节点 - 排他网关(条件分支) - 并行网关 * * @author pigx * @date 2026-03-04 @@ -53,774 +39,740 @@ import java.util.List; @Slf4j public class ModelUtil { - /** - * 构建BPMN模型 - *

- * 将前端传输的节点树结构转换为Flowable引擎可执行的BPMN模型。 - * 该方法是整个转换过程的入口,负责协调各个步骤。 - * - * @param nodeDto 前端传输的根节点,包含整个流程的节点树结构 - * @param processName 流程名称 - * @param flowId 流程定义ID - * @return 构建完成的BPMN模型对象 - */ - public static BpmnModel buildBpmnModel(Node nodeDto, String processName, String flowId) { - BpmnModel bpmnModel = new BpmnModel(); - bpmnModel.setTargetNamespace("pig"); - - Process process = new Process(); - process.setId(flowId); - process.setName(processName); - - // 流程监听器 - ArrayList eventListeners = new ArrayList<>(); - - { - // 流程实例监听器 - EventListener eventListener = new EventListener(); - - eventListener.setImplementationType("class"); - eventListener.setImplementation(FlowProcessEventListener.class.getCanonicalName()); - - eventListeners.add(eventListener); - - } - process.setEventListeners(eventListeners); - - NodeUtil.addEndNode(nodeDto); - - // 创建所有的节点 - buildAllNode(process, nodeDto, flowId); - // 创建所有的内部节点连接线 - buildAllNodeInnerSequence(process, nodeDto, flowId); - // 创建节点间连线 - buildAllNodeOuterSequence(process, nodeDto, null); - // 处理分支和下级连线 - - bpmnModel.addProcess(process); - return bpmnModel; - } - - /** - * 递归创建所有节点 - *

- * 遍历节点树结构,创建所有的流程节点元素。 - * 该方法只负责创建节点,不处理节点间的连接关系。 - * - * @param process 流程对象,用于添加流程元素 - * @param nodeDto 当前处理的节点 - * @param flowId 流程定义ID - */ - public static void buildAllNode(Process process, Node nodeDto, String flowId) { - if (!NodeUtil.isNode(nodeDto)) { - return; - } - - List flowElementList = buildNode(nodeDto, flowId); - for (FlowElement flowElement : flowElementList) { - if (process.getFlowElement(flowElement.getId()) == null) { - process.addFlowElement(flowElement); - } - } - - // 子节点 - Node children = nodeDto.getChildren(); - - if (NodeTypeEnum.getByValue(nodeDto.getType()).getBranch()) { - - // 条件分支 - List branchs = nodeDto.getConditionNodes(); - for (Node branch : branchs) { - buildAllNode(process, branch.getChildren(), flowId); - - } - if (NodeUtil.isNode(children)) { - buildAllNode(process, children, flowId); - } - - } else { - - if (NodeUtil.isNode(children)) { - buildAllNode(process, children, flowId); - } - } - - } - - /** - * 创建所有节点的内部连接线 - *

- * 处理节点内部的连接线,例如审批节点与其后续服务任务的连接。 - * 这些连接线是节点内部的逻辑关系,不属于节点间的连接。 - * - * @param process 流程对象 - * @param nodeDto 当前处理的节点 - * @param flowId 流程定义ID - */ - public static void buildAllNodeInnerSequence(Process process, Node nodeDto, String flowId) { - if (!NodeUtil.isNode(nodeDto)) { - return; - } - - // 画内部线 - List flowList = buildInnerSequenceFlow(nodeDto, flowId); - for (SequenceFlow sequenceFlow : flowList) { - process.addFlowElement(sequenceFlow); - } - - // 子节点 - Node children = nodeDto.getChildren(); - if (NodeTypeEnum.getByValue(nodeDto.getType()).getBranch()) { - // 条件分支 - List branchs = nodeDto.getConditionNodes(); - for (Node branch : branchs) { - buildAllNodeInnerSequence(process, branch.getChildren(), flowId); - - } - if (NodeUtil.isNode(children)) { - buildAllNodeInnerSequence(process, children, flowId); - } - - } else { - - if (NodeUtil.isNode(children)) { - buildAllNodeInnerSequence(process, children, flowId); - } - } - - } - - /** - * 递归创建节点间连接线 - *

- * 处理节点与节点之间的连接关系,包括: - * 1. 普通节点间的顺序连接 - * 2. 网关节点的分支连接 - * 3. 分支合并的连接 - * - * @param process 流程对象 - * @param nodeDto 当前处理的节点 - * @param nextId 下一个节点的ID,用于处理最后一个节点的连接 - */ - public static void buildAllNodeOuterSequence(Process process, Node nodeDto, String nextId) { - - if (!NodeUtil.isNode(nodeDto)) { - return; - } - - // 子节点 - Node children = nodeDto.getChildren(); - if (NodeTypeEnum.getByValue(nodeDto.getType()).getBranch()) { - // children = children.getChildren(); - // 条件分支 - List branchs = nodeDto.getConditionNodes(); - int ord = 1; - int size = branchs.size(); - for (Node branch : branchs) { - - buildAllNodeOuterSequence(process, branch.getChildren(), nodeDto.getTailId()); - - String expression = null; - - if (nodeDto.getType() == NodeTypeEnum.EXCLUSIVE_GATEWAY.getValue().intValue()) { - if (ord == size) { - expression = NodeExpressionStrategyFactory.handleDefaultBranch(branchs, ord - 1); - } else { - expression = NodeExpressionStrategyFactory.handle(branch); - } - - } - - // 添加连线 - if (!NodeUtil.isNode(branch.getChildren())) { - // 当前分支 没有其他节点了 所有就是网关和网关后面节点直接连线 - - SequenceFlow sequenceFlow = buildSingleSequenceFlow(nodeDto.getId(), nodeDto.getTailId(), - expression, StrUtil.format("{}->{}", nodeDto.getName(), nodeDto.getName())); - process.addFlowElement(sequenceFlow); - } else { - - SequenceFlow sequenceFlow = buildSingleSequenceFlow(nodeDto.getId(), - branch.getChildren().getHeadId(), expression, - StrUtil.format("{}->{}", nodeDto.getName(), branch.getChildren().getName())); - process.addFlowElement(sequenceFlow); - } - ord++; - - } - // 分支结尾的合并分支节点-》下一个节点 - if (children != null && StrUtil.isNotBlank(children.getHeadId()) - && StrUtil.isNotBlank(nodeDto.getTailId())) { - - SequenceFlow sequenceFlow = buildSingleSequenceFlow(nodeDto.getTailId(), children.getHeadId(), "", - StrUtil.format("{}->{}", nodeDto.getName(), children.getName())); - process.addFlowElement(sequenceFlow); - - } else if (StrUtil.isAllNotBlank(nodeDto.getTailId(), nextId)) { - SequenceFlow sequenceFlow = buildSingleSequenceFlow(nodeDto.getTailId(), nextId, "", - StrUtil.format("{}->{}", nodeDto.getName(), nextId)); - process.addFlowElement(sequenceFlow); - } - - buildAllNodeOuterSequence(process, children, nextId); - - } else { - // 添加连线 - if (NodeUtil.isNode(children)) { - List sequenceFlowList = buildSequenceFlow(children, nodeDto, ""); - for (SequenceFlow sequenceFlow : sequenceFlowList) { - process.addFlowElement(sequenceFlow); - } - buildAllNodeOuterSequence(process, children, nextId); - } else if (nodeDto.getType() != NodeTypeEnum.END.getValue().intValue()) { - SequenceFlow seq = buildSingleSequenceFlow(nodeDto.getTailId(), nextId, "", - StrUtil.format("{}->{}", nodeDto.getName(), nextId)); - - process.addFlowElement(seq); - - } - } - - } - - /** - * 构建节点 - *

- * 根据节点类型创建对应的BPMN元素。 - * 同时会将节点数据存储到数据库中,供运行时查询使用。 - * - * @param node 前端传输的节点数据 - * @param flowId 流程定义ID - * @return 创建的流程元素列表 - */ - private static List buildNode(Node node, String flowId) { - List flowElementList = new ArrayList<>(); - if (!NodeUtil.isNode(node)) { - return flowElementList; - } - - // 设置节点的连线头节点 - node.setHeadId(node.getId()); - // 设置节点的连线尾节点 - node.setTailId(node.getId()); - node.setName(StrUtil.format("{}[{}]", node.getName(), RandomUtil.randomNumbers(5))); - - // 存储节点数据 - IProcessNodeDataService processNodeDataService = SpringUtil.getBean(IProcessNodeDataService.class); - ProcessNodeDataDto processNodeDataDto = new ProcessNodeDataDto(); - processNodeDataDto.setFlowId(flowId); - processNodeDataDto.setNodeId(node.getId()); - processNodeDataDto.setData(JSONUtil.toJsonStr(node)); - processNodeDataService.saveNodeData(processNodeDataDto); - - // 开始 - if (node.getType() == NodeTypeEnum.ROOT.getValue().intValue()) { - flowElementList.addAll(buildStartNode(node)); - } - - // 结束 - if (node.getType() == NodeTypeEnum.END.getValue().intValue()) { - flowElementList.add(buildEndNode(node, false)); - } - - // 审批 - if (node.getType() == NodeTypeEnum.APPROVAL.getValue().intValue()) { - - flowElementList.addAll(buildApproveNode(node)); - } - - // 抄送 - if (node.getType() == NodeTypeEnum.CC.getValue().intValue()) { - flowElementList.add(buildCCNode(node)); - } - // 条件分支 - if (node.getType() == NodeTypeEnum.EXCLUSIVE_GATEWAY.getValue().intValue()) { - flowElementList.addAll(buildExclusiveGatewayNode(node)); - } - // 并行分支 - if (node.getType() == NodeTypeEnum.PARALLEL_GATEWAY.getValue().intValue()) { - flowElementList.addAll(buildParallelGatewayNode(node)); - } - // 延时器 - if (node.getType() == NodeTypeEnum.TIMER.getValue().intValue()) { - flowElementList.add(buildTimerNode(node)); - } - - // 触发器 - if (node.getType() == NodeTypeEnum.TRIGGER.getValue().intValue()) { - flowElementList.add(buildTriggerNode(node)); - } - - - return flowElementList; - } - - /** - * 构建开始节点 - *

- * 创建BPMN流程的开始事件节点和发起人任务节点。 - * 流程结构:StartEvent(start) → UserTask(root) → 第一个审批节点 - *

- *

- * 发起人任务节点的作用: - *

    - *
  • 正常发起流程时,通过RootTaskCreateListener自动完成,流程继续到下一个节点
  • - *
  • 驳回到发起人时,任务保留给发起人,发起人可以在待办任务中编辑并重新提交
  • - *
- *

- * - * @param node 前端传输的开始节点数据 - * @return 包含开始事件和发起人任务的流程元素列表 - */ - private static List buildStartNode(Node node) { - - List flowElementList = new ArrayList<>(); - - // 1. 创建真正的开始事件(ID 为 start) - StartEvent startEvent = new StartEvent(); - startEvent.setId("start"); - startEvent.setName("开始"); - flowElementList.add(startEvent); - - // 2. 创建发起人任务节点(ID 保持为 root) - // 配置任务监听器,用于处理驳回场景 - FlowableListener createListener = new FlowableListener(); - createListener.setImplementation(RootTaskCreateListener.class.getCanonicalName()); - createListener.setImplementationType("class"); - createListener.setEvent("create"); - - UserTask rootTask = new UserTask(); - rootTask.setId(node.getId()); // 保持 "root" 作为 ID - rootTask.setName(node.getName()); - // 动态获取发起人作为执行人 - rootTask.setAssignee("${rootTaskHandler.resolveAssignee(execution)}"); - - List taskListeners = new ArrayList<>(); - taskListeners.add(createListener); - rootTask.setTaskListeners(taskListeners); - - flowElementList.add(rootTask); - - // 3. 更新节点的 head/tail 信息 - node.setHeadId("start"); // 头部是 StartEvent - node.setTailId(node.getId()); // 尾部是 root UserTask - - return flowElementList; - } - - /** - * 构建审批节点 - *

- * 创建审批节点,包括用户任务和后续的服务任务。 - * 支持多种审批模式: - * 1. 单人审批 - * 2. 会签(并行/串行) - * 3. 或签 - * - * @param node 审批节点数据 - * @return 包含用户任务和服务任务的流程元素列表 - */ - private static List buildApproveNode(Node node) { - List flowElementList = new ArrayList<>(); - node.setTailId(StrUtil.format("approve_service_task_{}", node.getId())); - - // 创建了任务执行监听器 - // 先执行指派人 后创建 - // https://tkjohn.github.io/flowable-userguide/#eventDispatcher - FlowableListener createListener = new FlowableListener(); - createListener.setImplementation(ApprovalCreateListener.class.getCanonicalName()); - createListener.setImplementationType("class"); - createListener.setEvent("create"); - - UserTask userTask = buildUserTask(node, createListener); - flowElementList.add(userTask); - - ServiceTask serviceTask = new ServiceTask(); - serviceTask.setId(StrUtil.format("approve_service_task_{}", node.getId())); - serviceTask.setName(StrUtil.format("{}_服务任务", node.getName())); - serviceTask.setImplementationType("class"); - serviceTask.setImplementation(ApproveServiceTask.class.getCanonicalName()); - serviceTask.setAsynchronous(false); - - flowElementList.add(serviceTask); - - { - - // 执行人处理 - - String inputDataItem = "${multiInstanceHandler.resolveAssignee(execution)}"; - - // 串行 - - boolean isSequential = true; - - Integer multipleMode = node.getMultipleMode(); - // 多人 - if ((multipleMode == ProcessInstanceConstant.MULTIPLE_MODE_AL_SAME)) { - // 并行会签 - isSequential = false; - } - if ((multipleMode == ProcessInstanceConstant.MULTIPLE_MODE_ALL_SORT)) { - - // 串行会签 - } - if ((multipleMode == ProcessInstanceConstant.MULTIPLE_MODE_ONE)) { - - // 或签 - isSequential = false; - } - - MultiInstanceLoopCharacteristics loopCharacteristics = new MultiInstanceLoopCharacteristics(); - loopCharacteristics.setSequential(isSequential); - loopCharacteristics.setInputDataItem(inputDataItem); - loopCharacteristics.setElementVariable(StrUtil.format("{}_assignee_temp", node.getId())); - - loopCharacteristics.setCompletionCondition("${multiInstanceHandler.completionCondition(execution)}"); - - userTask.setLoopCharacteristics(loopCharacteristics); - String format = StrUtil.format("${{}_assignee_temp}", node.getId()); - userTask.setAssignee(format); - - } - return flowElementList; - } - - /** - * 创建用户任务 - *

- * 创建基本的用户任务节点,并可以添加任务监听器。 - * - * @param node 节点数据 - * @param flowableListeners 可选的任务监听器数组 - * @return 用户任务对象 - */ - private static UserTask buildUserTask(Node node, FlowableListener... flowableListeners) { - UserTask userTask = new UserTask(); - userTask.setId(node.getId()); - userTask.setName(node.getName()); - - if (flowableListeners != null) { - List taskListeners = new ArrayList<>(); - - for (FlowableListener flowableListener : flowableListeners) { - taskListeners.add(flowableListener); - - } - userTask.setTaskListeners(taskListeners); - } - - return userTask; - } - - /** - * 构建简单的排他网关 - *

- * 创建排他网关节点,用于条件分支。 - * - * @param node 网关节点数据 - * @return 排他网关元素 - */ - private static FlowElement buildSimpleExclusiveGatewayNode(Node node) { - - ExclusiveGateway exclusiveGateway = new ExclusiveGateway(); - exclusiveGateway.setId(node.getId()); - exclusiveGateway.setName(node.getName()); - - return exclusiveGateway; - - } - - /** - * 构建并行网关 - *

- * 创建并行网关节点和对应的合并网关。 - * 并行网关用于创建多个并行执行的分支。 - * - * @param node 并行网关节点数据 - * @return 包含分叉网关和合并网关的元素列表 - */ - private static List buildParallelGatewayNode(Node node) { - node.setTailId(StrUtil.format("{}_merge_gateway", node.getId())); - List flowElementList = new ArrayList<>(); - - ParallelGateway inclusiveGateway = new ParallelGateway(); - inclusiveGateway.setId(node.getId()); - inclusiveGateway.setName(node.getName()); - flowElementList.add(inclusiveGateway); - - // 合并网关 - ParallelGateway parallelGateway = new ParallelGateway(); - parallelGateway.setId(StrUtil.format("{}_merge_gateway", node.getId())); - parallelGateway.setName(StrUtil.format("{}_合并网关", node.getName())); - flowElementList.add(parallelGateway); - - return flowElementList; - } - - /** - * 构建排他网关 - *

- * 创建排他网关(条件分支)节点和对应的合并网关。 - * 排他网关用于根据条件只选择一条执行路径,避免多个条件同时为真时并行进入多个分支。 - * - * @param node 排他网关节点数据 - * @return 包含分叉网关和合并网关的元素列表 - */ - private static List buildExclusiveGatewayNode(Node node) { - - node.setTailId(StrUtil.format("{}_merge_gateway", node.getId())); - - List flowElementList = new ArrayList<>(); - - ExclusiveGateway exclusiveGateway = new ExclusiveGateway(); - exclusiveGateway.setId(node.getId()); - exclusiveGateway.setName(node.getName()); - flowElementList.add(exclusiveGateway); - - // 合并网关 - ExclusiveGateway gateway = new ExclusiveGateway(); - gateway.setId(StrUtil.format("{}_merge_gateway", node.getId())); - gateway.setName(StrUtil.format("{}_合并网关", node.getName())); - flowElementList.add(gateway); - - return flowElementList; - } - - /** - * 构建结束节点 - *

- * 创建BPMN流程的结束事件节点。 - * 使用终止事件定义,确保流程实例正确结束。 - * - * @param node 结束节点数据 - * @param terminateAll 是否终止所有活动实例 - * @return 结束事件对象 - */ - private static EndEvent buildEndNode(Node node, boolean terminateAll) { - EndEvent endEvent = new EndEvent(); - endEvent.setId(node.getId()); - endEvent.setName(node.getName()); - - List definitionList = new ArrayList<>(); - TerminateEventDefinition definition = new TerminateEventDefinition(); - definition.setTerminateAll(terminateAll); - definitionList.add(definition); - endEvent.setEventDefinitions(definitionList); - - return endEvent; - } - - /** - * 创建节点间连接线 - *

- * 创建父节点到子节点的连接线。 - * 使用节点的tailId和headId确定连接点。 - * - * @param node 子级节点 - * @param parentNode 父级节点 - * @param expression 连接线条件表达式 - * @return 连接线列表 - */ - private static List buildSequenceFlow(Node node, Node parentNode, String expression) { - List sequenceFlowList = new ArrayList<>(); - // 没有子级了 - if (!NodeUtil.isNode(node)) { - return sequenceFlowList; - } - - String pid = parentNode.getId(); - - if (StrUtil.hasBlank(pid, node.getId())) { - return sequenceFlowList; - } - - SequenceFlow sequenceFlow = buildSingleSequenceFlow(parentNode.getTailId(), node.getHeadId(), expression, - StrUtil.format("{}->{}", parentNode.getName(), node.getName())); - sequenceFlowList.add(sequenceFlow); - - return sequenceFlowList; - } - - /** - * 生成扩展属性 - *

- * 创建自定义的扩展属性,用于在BPMN元素中存储额外信息。 - * - * @param key 属性键 - * @param val 属性值 - * @return 扩展属性对象 - */ - public static ExtensionAttribute generateExtensionAttribute(String key, String val) { - ExtensionAttribute ea = new ExtensionAttribute(); - - ea.setName(key); - ea.setValue(val); - return ea; - } - - /** - * 创建简单服务任务节点(通用方法) - *

- * 创建一个通用的服务任务节点,配置了失败重试机制(重试5次,每次间隔1分钟)。 - * - * @param node 节点数据 - * @param delegateClass JavaDelegate 实现类 - * @return 服务任务元素 - */ - private static FlowElement buildSimpleServiceTaskNode(Node node, Class delegateClass) { - ServiceTask serviceTask = new ServiceTask(); - serviceTask.setId(node.getId()); - serviceTask.setName(node.getName()); - serviceTask.setAsynchronous(false); - serviceTask.setImplementationType("class"); - serviceTask.setImplementation(delegateClass.getCanonicalName()); - - ExtensionElement e = new ExtensionElement(); - e.setName("flowable:failedJobRetryTimeCycle"); - e.setElementText("R5/PT1M"); - - serviceTask.addExtensionElement(e); - return serviceTask; - } - - /** - * 构建延时器节点 - *

- * 创建Flowable中间捕获事件(Timer Intermediate Catch Event), - * 用于在流程中暂停执行,等待指定时间后自动继续。 - *

- * 支持三种延时模式: - *

    - *
  • DURATION - 固定时长(ISO 8601格式,如PT30M、PT2H、P1D、P1W)
  • - *
  • DATETIME - 指定日期时间(ISO 8601格式)
  • - *
  • FORM_FIELD - 表单字段日期(使用流程变量表达式)
  • - *
- * - * @param node 延时器节点数据 - * @return 中间捕获事件元素 - */ - private static FlowElement buildTimerNode(Node node) { - IntermediateCatchEvent catchEvent = new IntermediateCatchEvent(); - catchEvent.setId(node.getId()); - catchEvent.setName(node.getName()); - - TimerEventDefinition timerDef = new TimerEventDefinition(); - - TimerConfig config = node.getTimerConfig(); - if (config != null && StrUtil.isNotBlank(config.getTimerType())) { - String timerType = config.getTimerType(); - - if ("DURATION".equals(timerType)) { - TimerDuration duration = config.getDuration(); - if (duration != null && duration.getValue() != null) { - int value = duration.getValue(); - String unit = duration.getUnit() != null ? duration.getUnit() : "MINUTE"; - String iso = switch (unit) { - case "HOUR" -> StrUtil.format("PT{}H", value); - case "DAY" -> StrUtil.format("P{}D", value); - case "WEEK" -> StrUtil.format("P{}W", value); - default -> StrUtil.format("PT{}M", value); - }; - timerDef.setTimeDuration(iso); - } - } else if ("DATETIME".equals(timerType)) { - String dateTime = config.getDateTime(); - if (StrUtil.isNotBlank(dateTime)) { - timerDef.setTimeDate(dateTime); - } - } else if ("FORM_FIELD".equals(timerType)) { - String formFieldId = config.getFormFieldId(); - if (StrUtil.isNotBlank(formFieldId)) { - timerDef.setTimeDate("${timerHandler.resolveDate(execution, '" + formFieldId + "')}"); - } - } else { - log.warn("未知的延时器类型: {},使用默认30分钟", timerType); - timerDef.setTimeDuration("PT30M"); - } - } else { - timerDef.setTimeDuration("PT30M"); - } - - catchEvent.setEventDefinitions(List.of(timerDef)); - - return catchEvent; - } - - private static FlowElement buildCCNode(Node node) { - return buildSimpleServiceTaskNode(node, CopyServiceTask.class); - } - - private static FlowElement buildTriggerNode(Node node) { - return buildSimpleServiceTaskNode(node, TriggerServiceTask.class); - } - - /** - * 创建节点内部连接线 - *

- * 创建节点内部的连接线,例如: - *

    - *
  • 开始节点:start → root(StartEvent到发起人UserTask)
  • - *
  • 审批节点:审批任务 → 服务任务
  • - *
- * - * @param node 节点数据 - * @param flowId 流程定义ID - * @return 内部连接线列表 - */ - private static List buildInnerSequenceFlow(Node node, String flowId) { - - List sequenceFlowList = new ArrayList<>(); - if (!NodeUtil.isNode(node)) { - return sequenceFlowList; - } - - String nodeId = node.getId(); - if (StrUtil.hasBlank(nodeId)) { - return sequenceFlowList; - } - - // 开始节点:创建 start → root 的连线 - if (node.getType() == NodeTypeEnum.ROOT.getValue().intValue()) { - SequenceFlow sequenceFlow = buildSingleSequenceFlow("start", nodeId, null, "start->root"); - sequenceFlowList.add(sequenceFlow); - } - - // 审批节点:创建审批任务到服务任务的连线 - if (node.getType() == NodeTypeEnum.APPROVAL.getValue().intValue()) { - - String gatewayId = StrUtil.format("approve_service_task_{}", nodeId); - - { - SequenceFlow sequenceFlow = buildSingleSequenceFlow(nodeId, gatewayId, "${12==12}", null); - sequenceFlowList.add(sequenceFlow); - } - - } - - return sequenceFlowList; - } - - /** - * 创建单个连接线 - *

- * 创建两个节点之间的顺序流连接线。 - * 自动生成唯一的连接线ID和名称。 - * - * @param pId 父级节点ID(起始节点) - * @param childId 子级节点ID(目标节点) - * @param expression 条件表达式,可以为空 - * @param name 连接线名称,可以为null - * @return 顺序流对象,如果参数不合法则返回null - */ - private static SequenceFlow buildSingleSequenceFlow(String pId, String childId, String expression, String name) { - if (StrUtil.hasBlank(pId, childId)) { - return null; - } - SequenceFlow sequenceFlow = new SequenceFlow(pId, childId); - sequenceFlow.setConditionExpression(expression); - sequenceFlow.setName(StrUtil.isNotBlank(name) ? name : StrUtil.format("连线[{}]", RandomUtil.randomString(5))); - sequenceFlow.setId(StrUtil.format("sq-id-{}-{}", IdUtil.fastSimpleUUID(), RandomUtil.randomInt(1, 10000000))); - return sequenceFlow; - } + /** + * 构建BPMN模型 + *

+ * 将前端传输的节点树结构转换为Flowable引擎可执行的BPMN模型。 该方法是整个转换过程的入口,负责协调各个步骤。 + * @param nodeDto 前端传输的根节点,包含整个流程的节点树结构 + * @param processName 流程名称 + * @param flowId 流程定义ID + * @return 构建完成的BPMN模型对象 + */ + public static BpmnModel buildBpmnModel(Node nodeDto, String processName, String flowId) { + BpmnModel bpmnModel = new BpmnModel(); + bpmnModel.setTargetNamespace("pig"); + + Process process = new Process(); + process.setId(flowId); + process.setName(processName); + + // 流程监听器 + ArrayList eventListeners = new ArrayList<>(); + + { + // 流程实例监听器 + EventListener eventListener = new EventListener(); + + eventListener.setImplementationType("class"); + eventListener.setImplementation(FlowProcessEventListener.class.getCanonicalName()); + + eventListeners.add(eventListener); + + } + process.setEventListeners(eventListeners); + + NodeUtil.addEndNode(nodeDto); + + // 创建所有的节点 + buildAllNode(process, nodeDto, flowId); + // 创建所有的内部节点连接线 + buildAllNodeInnerSequence(process, nodeDto, flowId); + // 创建节点间连线 + buildAllNodeOuterSequence(process, nodeDto, null); + // 处理分支和下级连线 + + bpmnModel.addProcess(process); + return bpmnModel; + } + + /** + * 递归创建所有节点 + *

+ * 遍历节点树结构,创建所有的流程节点元素。 该方法只负责创建节点,不处理节点间的连接关系。 + * @param process 流程对象,用于添加流程元素 + * @param nodeDto 当前处理的节点 + * @param flowId 流程定义ID + */ + public static void buildAllNode(Process process, Node nodeDto, String flowId) { + if (!NodeUtil.isNode(nodeDto)) { + return; + } + + List flowElementList = buildNode(nodeDto, flowId); + for (FlowElement flowElement : flowElementList) { + if (process.getFlowElement(flowElement.getId()) == null) { + process.addFlowElement(flowElement); + } + } + + // 子节点 + Node children = nodeDto.getChildren(); + + if (NodeTypeEnum.getByValue(nodeDto.getType()).getBranch()) { + + // 条件分支 + List branchs = nodeDto.getConditionNodes(); + for (Node branch : branchs) { + buildAllNode(process, branch.getChildren(), flowId); + + } + if (NodeUtil.isNode(children)) { + buildAllNode(process, children, flowId); + } + + } + else { + + if (NodeUtil.isNode(children)) { + buildAllNode(process, children, flowId); + } + } + + } + + /** + * 创建所有节点的内部连接线 + *

+ * 处理节点内部的连接线,例如审批节点与其后续服务任务的连接。 这些连接线是节点内部的逻辑关系,不属于节点间的连接。 + * @param process 流程对象 + * @param nodeDto 当前处理的节点 + * @param flowId 流程定义ID + */ + public static void buildAllNodeInnerSequence(Process process, Node nodeDto, String flowId) { + if (!NodeUtil.isNode(nodeDto)) { + return; + } + + // 画内部线 + List flowList = buildInnerSequenceFlow(nodeDto, flowId); + for (SequenceFlow sequenceFlow : flowList) { + process.addFlowElement(sequenceFlow); + } + + // 子节点 + Node children = nodeDto.getChildren(); + if (NodeTypeEnum.getByValue(nodeDto.getType()).getBranch()) { + // 条件分支 + List branchs = nodeDto.getConditionNodes(); + for (Node branch : branchs) { + buildAllNodeInnerSequence(process, branch.getChildren(), flowId); + + } + if (NodeUtil.isNode(children)) { + buildAllNodeInnerSequence(process, children, flowId); + } + + } + else { + + if (NodeUtil.isNode(children)) { + buildAllNodeInnerSequence(process, children, flowId); + } + } + + } + + /** + * 递归创建节点间连接线 + *

+ * 处理节点与节点之间的连接关系,包括: 1. 普通节点间的顺序连接 2. 网关节点的分支连接 3. 分支合并的连接 + * @param process 流程对象 + * @param nodeDto 当前处理的节点 + * @param nextId 下一个节点的ID,用于处理最后一个节点的连接 + */ + public static void buildAllNodeOuterSequence(Process process, Node nodeDto, String nextId) { + + if (!NodeUtil.isNode(nodeDto)) { + return; + } + + // 子节点 + Node children = nodeDto.getChildren(); + if (NodeTypeEnum.getByValue(nodeDto.getType()).getBranch()) { + // children = children.getChildren(); + // 条件分支 + List branchs = nodeDto.getConditionNodes(); + int ord = 1; + int size = branchs.size(); + for (Node branch : branchs) { + + buildAllNodeOuterSequence(process, branch.getChildren(), nodeDto.getTailId()); + + String expression = null; + + if (nodeDto.getType() == NodeTypeEnum.EXCLUSIVE_GATEWAY.getValue().intValue()) { + if (ord == size) { + expression = NodeExpressionStrategyFactory.handleDefaultBranch(branchs, ord - 1); + } + else { + expression = NodeExpressionStrategyFactory.handle(branch); + } + + } + + // 添加连线 + if (!NodeUtil.isNode(branch.getChildren())) { + // 当前分支 没有其他节点了 所有就是网关和网关后面节点直接连线 + + SequenceFlow sequenceFlow = buildSingleSequenceFlow(nodeDto.getId(), nodeDto.getTailId(), + expression, StrUtil.format("{}->{}", nodeDto.getName(), nodeDto.getName())); + process.addFlowElement(sequenceFlow); + } + else { + + SequenceFlow sequenceFlow = buildSingleSequenceFlow(nodeDto.getId(), + branch.getChildren().getHeadId(), expression, + StrUtil.format("{}->{}", nodeDto.getName(), branch.getChildren().getName())); + process.addFlowElement(sequenceFlow); + } + ord++; + + } + // 分支结尾的合并分支节点-》下一个节点 + if (children != null && StrUtil.isNotBlank(children.getHeadId()) + && StrUtil.isNotBlank(nodeDto.getTailId())) { + + SequenceFlow sequenceFlow = buildSingleSequenceFlow(nodeDto.getTailId(), children.getHeadId(), "", + StrUtil.format("{}->{}", nodeDto.getName(), children.getName())); + process.addFlowElement(sequenceFlow); + + } + else if (StrUtil.isAllNotBlank(nodeDto.getTailId(), nextId)) { + SequenceFlow sequenceFlow = buildSingleSequenceFlow(nodeDto.getTailId(), nextId, "", + StrUtil.format("{}->{}", nodeDto.getName(), nextId)); + process.addFlowElement(sequenceFlow); + } + + buildAllNodeOuterSequence(process, children, nextId); + + } + else { + // 添加连线 + if (NodeUtil.isNode(children)) { + List sequenceFlowList = buildSequenceFlow(children, nodeDto, ""); + for (SequenceFlow sequenceFlow : sequenceFlowList) { + process.addFlowElement(sequenceFlow); + } + buildAllNodeOuterSequence(process, children, nextId); + } + else if (nodeDto.getType() != NodeTypeEnum.END.getValue().intValue()) { + SequenceFlow seq = buildSingleSequenceFlow(nodeDto.getTailId(), nextId, "", + StrUtil.format("{}->{}", nodeDto.getName(), nextId)); + + process.addFlowElement(seq); + + } + } + + } + + /** + * 构建节点 + *

+ * 根据节点类型创建对应的BPMN元素。 同时会将节点数据存储到数据库中,供运行时查询使用。 + * @param node 前端传输的节点数据 + * @param flowId 流程定义ID + * @return 创建的流程元素列表 + */ + private static List buildNode(Node node, String flowId) { + List flowElementList = new ArrayList<>(); + if (!NodeUtil.isNode(node)) { + return flowElementList; + } + + // 设置节点的连线头节点 + node.setHeadId(node.getId()); + // 设置节点的连线尾节点 + node.setTailId(node.getId()); + node.setName(StrUtil.format("{}[{}]", node.getName(), RandomUtil.randomNumbers(5))); + + // 开始 + if (node.getType() == NodeTypeEnum.ROOT.getValue().intValue()) { + flowElementList.addAll(buildStartNode(node)); + } + + // 结束 + if (node.getType() == NodeTypeEnum.END.getValue().intValue()) { + flowElementList.add(buildEndNode(node, false)); + } + + // 审批 + if (node.getType() == NodeTypeEnum.APPROVAL.getValue().intValue()) { + + flowElementList.addAll(buildApproveNode(node)); + } + + // 抄送 + if (node.getType() == NodeTypeEnum.CC.getValue().intValue()) { + flowElementList.add(buildCCNode(node)); + } + // 条件分支 + if (node.getType() == NodeTypeEnum.EXCLUSIVE_GATEWAY.getValue().intValue()) { + flowElementList.addAll(buildExclusiveGatewayNode(node)); + } + // 并行分支 + if (node.getType() == NodeTypeEnum.PARALLEL_GATEWAY.getValue().intValue()) { + flowElementList.addAll(buildParallelGatewayNode(node)); + } + // 延时器 + if (node.getType() == NodeTypeEnum.TIMER.getValue().intValue()) { + flowElementList.add(buildTimerNode(node)); + } + + // 触发器 + if (node.getType() == NodeTypeEnum.TRIGGER.getValue().intValue()) { + flowElementList.add(buildTriggerNode(node)); + } + + return flowElementList; + } + + /** + * 构建开始节点 + *

+ * 创建BPMN流程的开始事件节点和发起人任务节点。 流程结构:StartEvent(start) → UserTask(root) → 第一个审批节点 + *

+ *

+ * 发起人任务节点的作用: + *

    + *
  • 正常发起流程时,通过RootTaskCreateListener自动完成,流程继续到下一个节点
  • + *
  • 驳回到发起人时,任务保留给发起人,发起人可以在待办任务中编辑并重新提交
  • + *
+ *

+ * @param node 前端传输的开始节点数据 + * @return 包含开始事件和发起人任务的流程元素列表 + */ + private static List buildStartNode(Node node) { + + List flowElementList = new ArrayList<>(); + + // 1. 创建真正的开始事件(ID 为 start) + StartEvent startEvent = new StartEvent(); + startEvent.setId("start"); + startEvent.setName("开始"); + flowElementList.add(startEvent); + + // 2. 创建发起人任务节点(ID 保持为 root) + // 配置任务监听器,用于处理驳回场景 + FlowableListener createListener = new FlowableListener(); + createListener.setImplementation(RootTaskCreateListener.class.getCanonicalName()); + createListener.setImplementationType("class"); + createListener.setEvent("create"); + + UserTask rootTask = new UserTask(); + rootTask.setId(node.getId()); // 保持 "root" 作为 ID + rootTask.setName(node.getName()); + // 动态获取发起人作为执行人 + rootTask.setAssignee("${rootTaskHandler.resolveAssignee(execution)}"); + + List taskListeners = new ArrayList<>(); + taskListeners.add(createListener); + rootTask.setTaskListeners(taskListeners); + + flowElementList.add(rootTask); + + // 3. 更新节点的 head/tail 信息 + node.setHeadId("start"); // 头部是 StartEvent + node.setTailId(node.getId()); // 尾部是 root UserTask + + return flowElementList; + } + + /** + * 构建审批节点 + *

+ * 创建审批节点,包括用户任务和后续的服务任务。 支持多种审批模式: 1. 单人审批 2. 会签(并行/串行) 3. 或签 + * @param node 审批节点数据 + * @return 包含用户任务和服务任务的流程元素列表 + */ + private static List buildApproveNode(Node node) { + List flowElementList = new ArrayList<>(); + node.setTailId(StrUtil.format("approve_service_task_{}", node.getId())); + + // 创建了任务执行监听器 + // 先执行指派人 后创建 + // https://tkjohn.github.io/flowable-userguide/#eventDispatcher + FlowableListener createListener = new FlowableListener(); + createListener.setImplementation(ApprovalCreateListener.class.getCanonicalName()); + createListener.setImplementationType("class"); + createListener.setEvent("create"); + + UserTask userTask = buildUserTask(node, createListener); + flowElementList.add(userTask); + + ServiceTask serviceTask = new ServiceTask(); + serviceTask.setId(StrUtil.format("approve_service_task_{}", node.getId())); + serviceTask.setName(StrUtil.format("{}_服务任务", node.getName())); + serviceTask.setImplementationType("class"); + serviceTask.setImplementation(ApproveServiceTask.class.getCanonicalName()); + serviceTask.setAsynchronous(false); + + flowElementList.add(serviceTask); + + { + + // 执行人处理 + + String inputDataItem = "${multiInstanceHandler.resolveAssignee(execution)}"; + + // 串行 + + boolean isSequential = true; + + Integer multipleMode = node.getMultipleMode(); + // 多人 + if ((multipleMode == ProcessInstanceConstant.MULTIPLE_MODE_AL_SAME)) { + // 并行会签 + isSequential = false; + } + if ((multipleMode == ProcessInstanceConstant.MULTIPLE_MODE_ALL_SORT)) { + + // 串行会签 + } + if ((multipleMode == ProcessInstanceConstant.MULTIPLE_MODE_ONE)) { + + // 或签 + isSequential = false; + } + + MultiInstanceLoopCharacteristics loopCharacteristics = new MultiInstanceLoopCharacteristics(); + loopCharacteristics.setSequential(isSequential); + loopCharacteristics.setInputDataItem(inputDataItem); + loopCharacteristics.setElementVariable(StrUtil.format("{}_assignee_temp", node.getId())); + + loopCharacteristics.setCompletionCondition("${multiInstanceHandler.completionCondition(execution)}"); + + userTask.setLoopCharacteristics(loopCharacteristics); + String format = StrUtil.format("${{}_assignee_temp}", node.getId()); + userTask.setAssignee(format); + + } + return flowElementList; + } + + /** + * 创建用户任务 + *

+ * 创建基本的用户任务节点,并可以添加任务监听器。 + * @param node 节点数据 + * @param flowableListeners 可选的任务监听器数组 + * @return 用户任务对象 + */ + private static UserTask buildUserTask(Node node, FlowableListener... flowableListeners) { + UserTask userTask = new UserTask(); + userTask.setId(node.getId()); + userTask.setName(node.getName()); + + if (flowableListeners != null) { + List taskListeners = new ArrayList<>(); + + for (FlowableListener flowableListener : flowableListeners) { + taskListeners.add(flowableListener); + + } + userTask.setTaskListeners(taskListeners); + } + + return userTask; + } + + /** + * 构建简单的排他网关 + *

+ * 创建排他网关节点,用于条件分支。 + * @param node 网关节点数据 + * @return 排他网关元素 + */ + private static FlowElement buildSimpleExclusiveGatewayNode(Node node) { + + ExclusiveGateway exclusiveGateway = new ExclusiveGateway(); + exclusiveGateway.setId(node.getId()); + exclusiveGateway.setName(node.getName()); + + return exclusiveGateway; + + } + + /** + * 构建并行网关 + *

+ * 创建并行网关节点和对应的合并网关。 并行网关用于创建多个并行执行的分支。 + * @param node 并行网关节点数据 + * @return 包含分叉网关和合并网关的元素列表 + */ + private static List buildParallelGatewayNode(Node node) { + node.setTailId(StrUtil.format("{}_merge_gateway", node.getId())); + List flowElementList = new ArrayList<>(); + + ParallelGateway inclusiveGateway = new ParallelGateway(); + inclusiveGateway.setId(node.getId()); + inclusiveGateway.setName(node.getName()); + flowElementList.add(inclusiveGateway); + + // 合并网关 + ParallelGateway parallelGateway = new ParallelGateway(); + parallelGateway.setId(StrUtil.format("{}_merge_gateway", node.getId())); + parallelGateway.setName(StrUtil.format("{}_合并网关", node.getName())); + flowElementList.add(parallelGateway); + + return flowElementList; + } + + /** + * 构建排他网关 + *

+ * 创建排他网关(条件分支)节点和对应的合并网关。 排他网关用于根据条件只选择一条执行路径,避免多个条件同时为真时并行进入多个分支。 + * @param node 排他网关节点数据 + * @return 包含分叉网关和合并网关的元素列表 + */ + private static List buildExclusiveGatewayNode(Node node) { + + node.setTailId(StrUtil.format("{}_merge_gateway", node.getId())); + + List flowElementList = new ArrayList<>(); + + ExclusiveGateway exclusiveGateway = new ExclusiveGateway(); + exclusiveGateway.setId(node.getId()); + exclusiveGateway.setName(node.getName()); + flowElementList.add(exclusiveGateway); + + // 合并网关 + ExclusiveGateway gateway = new ExclusiveGateway(); + gateway.setId(StrUtil.format("{}_merge_gateway", node.getId())); + gateway.setName(StrUtil.format("{}_合并网关", node.getName())); + flowElementList.add(gateway); + + return flowElementList; + } + + /** + * 构建结束节点 + *

+ * 创建BPMN流程的结束事件节点。 使用终止事件定义,确保流程实例正确结束。 + * @param node 结束节点数据 + * @param terminateAll 是否终止所有活动实例 + * @return 结束事件对象 + */ + private static EndEvent buildEndNode(Node node, boolean terminateAll) { + EndEvent endEvent = new EndEvent(); + endEvent.setId(node.getId()); + endEvent.setName(node.getName()); + + List definitionList = new ArrayList<>(); + TerminateEventDefinition definition = new TerminateEventDefinition(); + definition.setTerminateAll(terminateAll); + definitionList.add(definition); + endEvent.setEventDefinitions(definitionList); + + return endEvent; + } + + /** + * 创建节点间连接线 + *

+ * 创建父节点到子节点的连接线。 使用节点的tailId和headId确定连接点。 + * @param node 子级节点 + * @param parentNode 父级节点 + * @param expression 连接线条件表达式 + * @return 连接线列表 + */ + private static List buildSequenceFlow(Node node, Node parentNode, String expression) { + List sequenceFlowList = new ArrayList<>(); + // 没有子级了 + if (!NodeUtil.isNode(node)) { + return sequenceFlowList; + } + + String pid = parentNode.getId(); + + if (StrUtil.hasBlank(pid, node.getId())) { + return sequenceFlowList; + } + + SequenceFlow sequenceFlow = buildSingleSequenceFlow(parentNode.getTailId(), node.getHeadId(), expression, + StrUtil.format("{}->{}", parentNode.getName(), node.getName())); + sequenceFlowList.add(sequenceFlow); + + return sequenceFlowList; + } + + /** + * 生成扩展属性 + *

+ * 创建自定义的扩展属性,用于在BPMN元素中存储额外信息。 + * @param key 属性键 + * @param val 属性值 + * @return 扩展属性对象 + */ + public static ExtensionAttribute generateExtensionAttribute(String key, String val) { + ExtensionAttribute ea = new ExtensionAttribute(); + + ea.setName(key); + ea.setValue(val); + return ea; + } + + /** + * 创建简单服务任务节点(通用方法) + *

+ * 创建一个通用的服务任务节点,配置了失败重试机制(重试5次,每次间隔1分钟)。 + * @param node 节点数据 + * @param delegateClass JavaDelegate 实现类 + * @return 服务任务元素 + */ + private static FlowElement buildSimpleServiceTaskNode(Node node, Class delegateClass) { + ServiceTask serviceTask = new ServiceTask(); + serviceTask.setId(node.getId()); + serviceTask.setName(node.getName()); + serviceTask.setAsynchronous(false); + serviceTask.setImplementationType("class"); + serviceTask.setImplementation(delegateClass.getCanonicalName()); + + ExtensionElement e = new ExtensionElement(); + e.setName("flowable:failedJobRetryTimeCycle"); + e.setElementText("R5/PT1M"); + + serviceTask.addExtensionElement(e); + return serviceTask; + } + + /** + * 构建延时器节点 + *

+ * 创建Flowable中间捕获事件(Timer Intermediate Catch Event), 用于在流程中暂停执行,等待指定时间后自动继续。 + *

+ * 支持三种延时模式: + *

    + *
  • DURATION - 固定时长(ISO 8601格式,如PT30M、PT2H、P1D、P1W)
  • + *
  • DATETIME - 指定日期时间(ISO 8601格式)
  • + *
  • FORM_FIELD - 表单字段日期(使用流程变量表达式)
  • + *
+ * @param node 延时器节点数据 + * @return 中间捕获事件元素 + */ + private static FlowElement buildTimerNode(Node node) { + IntermediateCatchEvent catchEvent = new IntermediateCatchEvent(); + catchEvent.setId(node.getId()); + catchEvent.setName(node.getName()); + + TimerEventDefinition timerDef = new TimerEventDefinition(); + + TimerConfig config = node.getTimerConfig(); + if (config != null && StrUtil.isNotBlank(config.getTimerType())) { + String timerType = config.getTimerType(); + + if ("DURATION".equals(timerType)) { + TimerDuration duration = config.getDuration(); + if (duration != null && duration.getValue() != null) { + int value = duration.getValue(); + String unit = duration.getUnit() != null ? duration.getUnit() : "MINUTE"; + String iso = switch (unit) { + case "HOUR" -> StrUtil.format("PT{}H", value); + case "DAY" -> StrUtil.format("P{}D", value); + case "WEEK" -> StrUtil.format("P{}W", value); + default -> StrUtil.format("PT{}M", value); + }; + timerDef.setTimeDuration(iso); + } + } + else if ("DATETIME".equals(timerType)) { + String dateTime = config.getDateTime(); + if (StrUtil.isNotBlank(dateTime)) { + timerDef.setTimeDate(dateTime); + } + } + else if ("FORM_FIELD".equals(timerType)) { + String formFieldId = config.getFormFieldId(); + if (StrUtil.isNotBlank(formFieldId)) { + timerDef.setTimeDate("${timerHandler.resolveDate(execution, '" + formFieldId + "')}"); + } + } + else { + log.warn("未知的延时器类型: {},使用默认30分钟", timerType); + timerDef.setTimeDuration("PT30M"); + } + } + else { + timerDef.setTimeDuration("PT30M"); + } + + catchEvent.setEventDefinitions(List.of(timerDef)); + + return catchEvent; + } + + private static FlowElement buildCCNode(Node node) { + return buildSimpleServiceTaskNode(node, CopyServiceTask.class); + } + + private static FlowElement buildTriggerNode(Node node) { + return buildSimpleServiceTaskNode(node, TriggerServiceTask.class); + } + + /** + * 创建节点内部连接线 + *

+ * 创建节点内部的连接线,例如: + *

    + *
  • 开始节点:start → root(StartEvent到发起人UserTask)
  • + *
  • 审批节点:审批任务 → 服务任务
  • + *
+ * @param node 节点数据 + * @param flowId 流程定义ID + * @return 内部连接线列表 + */ + private static List buildInnerSequenceFlow(Node node, String flowId) { + + List sequenceFlowList = new ArrayList<>(); + if (!NodeUtil.isNode(node)) { + return sequenceFlowList; + } + + String nodeId = node.getId(); + if (StrUtil.hasBlank(nodeId)) { + return sequenceFlowList; + } + + // 开始节点:创建 start → root 的连线 + if (node.getType() == NodeTypeEnum.ROOT.getValue().intValue()) { + SequenceFlow sequenceFlow = buildSingleSequenceFlow("start", nodeId, null, "start->root"); + sequenceFlowList.add(sequenceFlow); + } + + // 审批节点:创建审批任务到服务任务的连线 + if (node.getType() == NodeTypeEnum.APPROVAL.getValue().intValue()) { + + String gatewayId = StrUtil.format("approve_service_task_{}", nodeId); + + { + SequenceFlow sequenceFlow = buildSingleSequenceFlow(nodeId, gatewayId, "${12==12}", null); + sequenceFlowList.add(sequenceFlow); + } + + } + + return sequenceFlowList; + } + + /** + * 创建单个连接线 + *

+ * 创建两个节点之间的顺序流连接线。 自动生成唯一的连接线ID和名称。 + * @param pId 父级节点ID(起始节点) + * @param childId 子级节点ID(目标节点) + * @param expression 条件表达式,可以为空 + * @param name 连接线名称,可以为null + * @return 顺序流对象,如果参数不合法则返回null + */ + private static SequenceFlow buildSingleSequenceFlow(String pId, String childId, String expression, String name) { + if (StrUtil.hasBlank(pId, childId)) { + return null; + } + SequenceFlow sequenceFlow = new SequenceFlow(pId, childId); + sequenceFlow.setConditionExpression(expression); + sequenceFlow.setName(StrUtil.isNotBlank(name) ? name : StrUtil.format("连线[{}]", RandomUtil.randomString(5))); + sequenceFlow.setId(StrUtil.format("sq-id-{}-{}", IdUtil.fastSimpleUUID(), RandomUtil.randomInt(1, 10000000))); + return sequenceFlow; + } } diff --git a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/utils/NodeUtil.java b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/utils/NodeUtil.java index 7aef91f31..750707fc9 100644 --- a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/utils/NodeUtil.java +++ b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/utils/NodeUtil.java @@ -1,23 +1,86 @@ package com.pig4cloud.pigx.flow.support.utils; import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; import com.pig4cloud.pigx.flow.constant.NodeTypeEnum; import com.pig4cloud.pigx.flow.constant.ProcessInstanceConstant; import com.pig4cloud.pigx.flow.dto.Node; import java.util.ArrayList; import java.util.List; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; /** * 流程节点工具类 *

* 提供流程节点的常用操作方法,包括节点遍历、节点查找、节点构建等功能。 主要用于处理流程定义的树形结构,支持条件分支和并行分支的递归处理。 *

- * + * * @author pigx */ public class NodeUtil { + /** + * 发布时递归修改节点及条件分支的权限映射,为当前表单中未配置权限的字段补齐 EDIT。 已配置的非空权限保持不变;支持表单 children 中的嵌套字段。 + * @param node 待固化的节点树;为 null 时不执行任何操作 + * @param formItems 表单字段 JSON 数组;为 null、空串或空白时只复制已有权限 + * @throws cn.hutool.json.JSONException 表单内容不能解析为 JSON 数组时抛出;此前已处理的节点不会恢复 + */ + public static void snapshotFormPermissions(Node node, String formItems) { + if (Objects.isNull(node)) { + return; + } + node.setFormPerms(formPermissions(node, formItems, ProcessInstanceConstant.FormPermClass.EDIT)); + if (Objects.nonNull(node.getConditionNodes())) { + for (Node branch : node.getConditionNodes()) { + snapshotFormPermissions(branch, formItems); + } + } + snapshotFormPermissions(node.getChildren(), formItems); + } + + /** + * 复制发布时的字段权限,为当前表单中快照未声明的字段补齐 HIDE,不修改节点原有映射。 同时处理表单 children 中的嵌套字段,并保留已有非空权限。 + * @param node 已发布节点,不允许为 null;其权限映射允许为 null + * @param formItems 当前表单字段 JSON 数组;为 null、空串或空白时仅返回已有权限的副本 + * @return 独立、可修改的字段权限映射,不为 null + * @throws NullPointerException node 为 null 时抛出 + * @throws cn.hutool.json.JSONException 表单内容不能解析为 JSON 数组时抛出 + */ + public static Map publishedFormPermissions(Node node, String formItems) { + return formPermissions(node, formItems, ProcessInstanceConstant.FormPermClass.HIDE); + } + + private static Map formPermissions(Node node, String formItems, String defaultPermission) { + Map permissions = new HashMap<>(); + if (Objects.nonNull(node.getFormPerms())) { + permissions.putAll(node.getFormPerms()); + } + if (StrUtil.isNotBlank(formItems)) { + fillMissingPermissions(JSONUtil.parseArray(formItems), permissions, defaultPermission); + } + return permissions; + } + + private static void fillMissingPermissions(JSONArray items, Map permissions, + String defaultPermission) { + for (int i = 0; i < items.size(); i++) { + JSONObject item = items.getJSONObject(i); + String field = item.getStr("field"); + if (StrUtil.isNotBlank(field) && StrUtil.isBlank(permissions.get(field))) { + permissions.put(field, defaultPermission); + } + JSONArray children = item.getJSONArray("children"); + if (Objects.nonNull(children)) { + fillMissingPermissions(children, permissions, defaultPermission); + } + } + } + /** * 从流程定义ID中提取流程ID *

diff --git a/pigx-flow/pigx-flow-biz/src/test/java/com/pig4cloud/pigx/flow/service/impl/ProcessInstanceServiceImplTests.java b/pigx-flow/pigx-flow-biz/src/test/java/com/pig4cloud/pigx/flow/service/impl/ProcessInstanceServiceImplTests.java index 1ce2ff71a..909d7e57c 100644 --- a/pigx-flow/pigx-flow-biz/src/test/java/com/pig4cloud/pigx/flow/service/impl/ProcessInstanceServiceImplTests.java +++ b/pigx-flow/pigx-flow-biz/src/test/java/com/pig4cloud/pigx/flow/service/impl/ProcessInstanceServiceImplTests.java @@ -64,7 +64,8 @@ class ProcessInstanceServiceImplTests { ProcessInstanceServiceImpl service = new ProcessInstanceServiceImpl(mock(RemoteUserService.class), mock(TaskService.class), mock(RuntimeService.class), mock(HistoryService.class), recordService, mock(IProcessCopyService.class), mock(IProcessService.class), mock(IProcessNodeRecordService.class), - mock(IProcessNodeRecordAssignUserService.class), new ObjectMapper()); + mock(IProcessNodeRecordAssignUserService.class), new ObjectMapper(), + mock(com.pig4cloud.pigx.flow.service.IProcessNodeDataService.class)); ProcessInstanceParamDto request = new ProcessInstanceParamDto(); request.setProcessInstanceId("process-7598"); request.setParamMap(variables); diff --git a/pigx-flow/pigx-flow-biz/src/test/java/com/pig4cloud/pigx/flow/service/impl/ProcessNodeVersionTests.java b/pigx-flow/pigx-flow-biz/src/test/java/com/pig4cloud/pigx/flow/service/impl/ProcessNodeVersionTests.java new file mode 100644 index 000000000..27dbb8ae2 --- /dev/null +++ b/pigx-flow/pigx-flow-biz/src/test/java/com/pig4cloud/pigx/flow/service/impl/ProcessNodeVersionTests.java @@ -0,0 +1,290 @@ +package com.pig4cloud.pigx.flow.service.impl; + +import cn.hutool.extra.spring.SpringUtil; +import com.pig4cloud.pigx.common.data.tenant.TenantContextHolder; +import com.baomidou.mybatisplus.core.MybatisConfiguration; +import com.baomidou.mybatisplus.core.MybatisSqlSessionFactoryBuilder; +import com.baomidou.mybatisplus.core.config.GlobalConfig; +import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; +import com.baomidou.mybatisplus.core.toolkit.GlobalConfigUtils; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.pig4cloud.pigx.flow.dto.Node; +import com.pig4cloud.pigx.flow.dto.NodeUser; +import com.pig4cloud.pigx.flow.dto.ProcessNodeDataDto; +import com.pig4cloud.pigx.flow.entity.Process; +import com.pig4cloud.pigx.flow.mapper.ProcessNodeDataMapper; +import com.pig4cloud.pigx.flow.service.IProcessService; +import com.pig4cloud.pigx.flow.service.IProcessStarterService; +import com.pig4cloud.pigx.flow.support.node.MultiInstanceHandler; +import com.pig4cloud.pigx.flow.support.node.impl.AssignUserFixedStrategyImpl; +import com.pig4cloud.pigx.flow.support.utils.ModelUtil; +import com.pig4cloud.pigx.flow.support.utils.NodeUtil; +import org.apache.ibatis.mapping.Environment; +import org.apache.ibatis.reflection.MetaObject; +import org.h2.jdbcx.JdbcDataSource; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mybatis.spring.SqlSessionTemplate; +import org.mybatis.spring.transaction.SpringManagedTransactionFactory; +import org.flowable.bpmn.model.BpmnModel; +import org.flowable.bpmn.model.UserTask; +import org.flowable.engine.ProcessEngine; +import org.flowable.engine.ProcessEngineConfiguration; +import org.flowable.engine.repository.ProcessDefinition; +import org.flowable.engine.runtime.ProcessInstance; +import org.flowable.spring.SpringProcessEngineConfiguration; +import org.flowable.task.api.Task; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource; +import org.springframework.transaction.interceptor.TransactionInterceptor; +import org.springframework.transaction.support.TransactionTemplate; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; + +class ProcessNodeVersionTests { + + private ProcessEngine engine; + + private ProcessNodeDataServiceImpl nodes; + + private GenericApplicationContext context; + + private DataSourceTransactionManager transactions; + + private JdbcTemplate jdbc; + + @BeforeEach + void setUp() throws Exception { + TenantContextHolder.setTenantId(1L); + JdbcDataSource dataSource = new JdbcDataSource(); + dataSource.setURL("jdbc:h2:mem:" + UUID.randomUUID() + ";DB_CLOSE_DELAY=-1"); + dataSource.setUser("sa"); + jdbc = new JdbcTemplate(dataSource); + jdbc.execute("create table process_node_data(id bigint primary key, flow_id varchar(255), " + + "process_definition_id varchar(64), tenant_id bigint, node_id varchar(50), data clob, " + + "del_flag varchar(1) default '0', create_time timestamp, update_time timestamp, " + + "unique(tenant_id, process_definition_id, node_id))"); + transactions = new DataSourceTransactionManager(dataSource); + SpringProcessEngineConfiguration configuration = new SpringProcessEngineConfiguration(); + configuration.setDataSource(dataSource); + configuration.setTransactionManager(transactions); + configuration.setDatabaseSchemaUpdate(ProcessEngineConfiguration.DB_SCHEMA_UPDATE_TRUE); + configuration.setDisableIdmEngine(true); + configuration.setDisableEventRegistry(true); + configuration.setAsyncExecutorActivate(false); + Map beans = new HashMap<>(); + configuration.setBeans(beans); + engine = configuration.buildProcessEngine(); + + MybatisConfiguration mybatis = new MybatisConfiguration(); + mybatis.setMapUnderscoreToCamelCase(true); + mybatis.setEnvironment(new Environment("test", new SpringManagedTransactionFactory(), dataSource)); + GlobalConfig global = new GlobalConfig(); + global.setDbConfig(new GlobalConfig.DbConfig()); + global.setMetaObjectHandler(new MetaObjectHandler() { + @Override + public void insertFill(MetaObject metaObject) { + setFieldValByName("delFlag", "0", metaObject); + } + + @Override + public void updateFill(MetaObject metaObject) { + } + }); + GlobalConfigUtils.setGlobalConfig(mybatis, global); + mybatis.addMapper(ProcessNodeDataMapper.class); + var factory = new MybatisSqlSessionFactoryBuilder().build(mybatis); + nodes = new ProcessNodeDataServiceImpl(engine.getRepositoryService()); + ReflectionTestUtils.setField(nodes, "baseMapper", + new SqlSessionTemplate(factory).getMapper(ProcessNodeDataMapper.class)); + beans.put("multiInstanceHandler", new MultiInstanceHandler(nodes, + Map.of("1AssignUserStrategy", new AssignUserFixedStrategyImpl(null)), new ObjectMapper())); + context = new GenericApplicationContext(); + context.getBeanFactory().registerSingleton("processNodeDataService", nodes); + context.refresh(); + SpringUtil spring = new SpringUtil(); + spring.setApplicationContext(context); + spring.postProcessBeanFactory(context.getBeanFactory()); + } + + @AfterEach + void tearDown() { + TenantContextHolder.clear(); + if (engine != null) { + engine.close(); + } + if (context != null) { + context.close(); + } + if (jdbc != null) { + jdbc.execute("shutdown"); + } + } + + @Test + void oldInstanceKeepsPublishedApproverAndPermissionsAfterRepublish() { + ProcessDefinition a = publish("leave", 1001L, "R"); + ProcessInstance old = start("leave"); + ProcessDefinition b = publish("leave", 2002L, "E"); + + assertThat(engine.getRuntimeService() + .createProcessInstanceQuery() + .processInstanceId(old.getId()) + .singleResult() + .getProcessDefinitionId()).isEqualTo(a.getId()); + assertThat(advance(old).getAssignee()).isEqualTo("1001"); + assertThat(advance(start("leave")).getAssignee()).isEqualTo("2002"); + assertThat(nodes.getPublishedNode(a.getId(), "approve").getData().getFormPerms()).containsEntry("amount", "R"); + assertThat(nodes.getPublishedNode(b.getId(), "approve").getData().getFormPerms()).containsEntry("amount", "E"); + assertThat(NodeUtil.publishedFormPermissions(nodes.getPublishedNode(a.getId(), "approve").getData(), + "[{\"field\":\"amount\"},{\"children\":[{\"field\":\"extra\"}]}]")) + .containsEntry("amount", "R") + .containsEntry("extra", "H"); + assertThat(NodeUtil.publishedFormPermissions(nodes.getPublishedNode(a.getId(), "root").getData(), + "[{\"field\":\"amount\"},{\"field\":\"layout\",\"children\":[{\"field\":\"nested\"},{\"field\":\"extra\"}]}]")) + .containsEntry("amount", "E") + .containsEntry("layout", "E") + .containsEntry("nested", "E") + .containsEntry("extra", "H"); + assertThat(nodes.getPublishedNode(a.getId(), "start").getData()).isNull(); + jdbc.update("delete from process_node_data where process_definition_id = ? and node_id = ?", a.getId(), + "approve"); + assertThatThrownBy(() -> nodes.getPublishedNode(a.getId(), "approve")).isInstanceOf(IllegalStateException.class) + .hasMessageContaining("缺少节点版本快照:approve"); + assertThat(nodes.saveNodeData(new ProcessNodeDataDto()).getCode()).isEqualTo(1); + assertThatThrownBy(() -> nodes.getPublishedNode("leave:0:missing", "approve")) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("缺少节点版本快照"); + } + + @Test + void failureAfterSnapshotRollsBackDeploymentAndNodesWithoutChangingOldVersion() throws Exception { + ProcessDefinition a = publish("leave", 1001L, "R"); + long before = jdbc.queryForObject("select count(*) from process_node_data", Long.class); + ProcessServiceImpl target = new ProcessServiceImpl(mock(IProcessStarterService.class), + engine.getRepositoryService(), nodes, new ObjectMapper(), null, null) { + @Override + public Process getByFlowId(String flowId) { + return null; + } + + @Override + public boolean saveOrUpdate(Process process) { + assertThat(jdbc.queryForObject("select count(*) from process_node_data", Long.class)) + .isGreaterThan(before); + ProcessDefinition published = engine.getRepositoryService() + .createProcessDefinitionQuery() + .processDefinitionKey("leave") + .latestVersion() + .singleResult(); + assertThat(published.getId()).isNotEqualTo(a.getId()); + assertThat(nodes.getPublishedNode(published.getId(), "root").getData().getFormPerms()) + .containsEntry("amount", "E") + .containsEntry("layout", "E") + .containsEntry("nested", "E"); + assertThat(nodes.getPublishedNode(published.getId(), "approve").getData().getFormPerms()) + .containsEntry("amount", "R") + .containsEntry("secret", "H") + .containsEntry("layout", "E") + .containsEntry("nested", "E"); + throw new IllegalStateException("simulated metadata write failure"); + } + }; + TransactionInterceptor interceptor = new TransactionInterceptor(); + interceptor.setTransactionManager(transactions); + interceptor.setTransactionAttributeSource(new AnnotationTransactionAttributeSource()); + ProxyFactory proxy = new ProxyFactory(target); + proxy.addAdvice(interceptor); + IProcessService service = (IProcessService) proxy.getProxy(); + Process request = new Process(); + request.setFlowId("leave"); + request.setName("Leave B"); + Node requestedRoot = tree(2002L, "R"); + requestedRoot.getChildren().setFormPerms(Map.of("amount", "R", "secret", "H")); + request.setProcess(new ObjectMapper().writeValueAsString(requestedRoot)); + request.setFormItems( + "[{\"field\":\"amount\"},{\"field\":\"secret\"},{\"field\":\"layout\",\"children\":[{\"field\":\"nested\"}]}]"); + request.setAdminList("[{\"id\":9000,\"type\":\"user\"}]"); + + assertThatThrownBy(() -> service.create(request)).isInstanceOf(IllegalStateException.class) + .hasMessage("simulated metadata write failure"); + assertThat(engine.getRepositoryService().createProcessDefinitionQuery().processDefinitionKey("leave").count()) + .isEqualTo(1); + assertThat(jdbc.queryForObject("select count(*) from process_node_data", Long.class)).isEqualTo(before); + assertThat(advance(start("leave")).getAssignee()).isEqualTo("1001"); + assertThat(nodes.getPublishedNode(a.getId(), "approve").getData().getFormPerms()).containsEntry("amount", "R"); + } + + private ProcessDefinition publish(String key, long approver, String permission) { + return new TransactionTemplate(transactions).execute(status -> { + Node root = tree(approver, permission); + NodeUtil.snapshotFormPermissions(root, + "[{\"field\":\"amount\"},{\"field\":\"layout\",\"children\":[{\"field\":\"nested\"}]}]"); + BpmnModel model = ModelUtil.buildBpmnModel(root, key, key); + // 隔离通知/业务记录依赖;保留真实多实例表达式和节点配置读写。 + model.getMainProcess().setEventListeners(new ArrayList<>()); + for (var element : model.getMainProcess().getFlowElements()) { + if (element instanceof UserTask task) { + task.setTaskListeners(new ArrayList<>()); + if ("root".equals(task.getId())) { + task.setAssignee("9000"); + } + } + } + var deployment = engine.getRepositoryService() + .createDeployment() + .addBpmnModel(key + ".bpmn20.xml", model) + .tenantId("1") + .deploy(); + ProcessDefinition definition = engine.getRepositoryService() + .createProcessDefinitionQuery() + .deploymentId(deployment.getId()) + .singleResult(); + nodes.savePublishedNodes(definition, root); + return definition; + }); + } + + private Node tree(long approver, String permission) { + Node root = new Node(); + root.setId("root"); + root.setType(0); + root.setName("Start"); + Node approval = new Node(); + approval.setId("approve"); + approval.setName("Approve"); + approval.setType(1); + approval.setAssignedType(1); + approval.setMultipleMode(1); + approval.setNodeUserList(List.of(NodeUser.builder().id(approver).type("user").build())); + approval.setFormPerms(Map.of("amount", permission)); + root.setChildren(approval); + return root; + } + + private ProcessInstance start(String key) { + return engine.getRuntimeService() + .startProcessInstanceByKeyAndTenantId(key, + Map.of("root", List.of(Map.of("id", 9000L, "name", "Starter", "type", "user"))), "1"); + } + + private Task advance(ProcessInstance instance) { + Task waiting = engine.getTaskService().createTaskQuery().processInstanceId(instance.getId()).singleResult(); + engine.getTaskService().complete(waiting.getId()); + return engine.getTaskService().createTaskQuery().processInstanceId(instance.getId()).singleResult(); + } + +} diff --git a/pigx-flow/pigx-flow-biz/src/test/java/com/pig4cloud/pigx/flow/service/impl/TaskServiceImplTests.java b/pigx-flow/pigx-flow-biz/src/test/java/com/pig4cloud/pigx/flow/service/impl/TaskServiceImplTests.java index 52b0785d8..d147636f5 100644 --- a/pigx-flow/pigx-flow-biz/src/test/java/com/pig4cloud/pigx/flow/service/impl/TaskServiceImplTests.java +++ b/pigx-flow/pigx-flow-biz/src/test/java/com/pig4cloud/pigx/flow/service/impl/TaskServiceImplTests.java @@ -105,42 +105,61 @@ class TaskServiceImplTests { @Mock private IProcessService processService; + @Mock private IProcessCopyService processCopyService; + @Mock private IProcessNodeDataService nodeDataService; + @Mock private IProcessNodeRecordAssignUserService processNodeRecordAssignUserService; + @Mock private IProcessInstanceRecordService processInstanceRecordService; + @Mock private IProcessNodeRecordService processNodeRecordService; + @Mock private IFlowableEngineService flowableEngineService; + @Mock private TaskService taskService; + @Mock private HistoryService historyService; + @Mock private RuntimeService runtimeService; + @Spy private ObjectMapper objectMapper = new ObjectMapper(); + @Mock private RemoteUserService remoteUserService; + @Mock private ProcessNodeRecordMapper processNodeRecordMapper; + @Mock private ProcessTaskAddSignRecordMapper processTaskAddSignRecordMapper; + @Mock(answer = Answers.RETURNS_SELF) private TaskQuery taskQuery; + @Mock private BaseMapper processInstanceBaseMapper; + @Mock private BaseMapper assignmentBaseMapper; + @Mock private Task sourceTask; + @Mock private Task addedTask; + @Mock private Execution addedExecution; @@ -157,11 +176,10 @@ class TaskServiceImplTests { @BeforeEach void setCurrentUserAndTenant() { - PigxUser user = new PigxUser(USER_ID, "approver", List.of(), null, List.of(), List.of(), null, null, - "审批人", "审批人", null, TENANT_ID, "password", true, true, "sys", true, - LocalDateTime.now(), true, List.of()); + PigxUser user = new PigxUser(USER_ID, "approver", List.of(), null, List.of(), List.of(), null, null, "审批人", + "审批人", null, TENANT_ID, "password", true, true, "sys", true, LocalDateTime.now(), true, List.of()); SecurityContextHolder.getContext() - .setAuthentication(UsernamePasswordAuthenticationToken.authenticated(user, "password", List.of())); + .setAuthentication(UsernamePasswordAuthenticationToken.authenticated(user, "password", List.of())); TenantContextHolder.setTenantId(TENANT_ID); } @@ -185,10 +203,9 @@ class TaskServiceImplTests { @Test void withdrawProcessInstancePropagatesWithdrawReasonBeforeMovingToEnd() { - ProcessInstanceRecord record = new ProcessInstanceRecord() - .setProcessInstanceId(PROCESS_INSTANCE_ID) - .setUserId(USER_ID) - .setStatus(NodeStatusEnum.JXZ.getCode()); + ProcessInstanceRecord record = new ProcessInstanceRecord().setProcessInstanceId(PROCESS_INSTANCE_ID) + .setUserId(USER_ID) + .setStatus(NodeStatusEnum.JXZ.getCode()); ChangeActivityStateBuilder stateChangeBuilder = prepareTerminationFlow(record); TaskParamDto request = new TaskParamDto(); request.setProcessInstanceId(PROCESS_INSTANCE_ID); @@ -236,7 +253,7 @@ class TaskServiceImplTests { prepareTargetValidation(List.of(originalAssignment()), validUser(TARGET_USER_ID, TENANT_ID)); prepareAuditInsert(); when(runtimeService.addMultiInstanceExecution(eq(NODE_ID), eq(PROCESS_INSTANCE_ID), anyMap())) - .thenReturn(addedExecution); + .thenReturn(addedExecution); when(assignmentBaseMapper.selectCount(any())).thenReturn(1L); R result = taskServiceImpl.addSign(request(List.of(TARGET_USER_ID), "需要共同确认")); @@ -249,8 +266,7 @@ class TaskServiceImplTests { assertThat(addSignResult.getTasks().get(0).getTaskId()).isEqualTo(ADDED_TASK_ID); assertThat(addSignResult.getTasks().get(0).getUserId()).isEqualTo(TARGET_USER_ID); - ArgumentCaptor recordCaptor = ArgumentCaptor - .forClass(ProcessTaskAddSignRecord.class); + ArgumentCaptor recordCaptor = ArgumentCaptor.forClass(ProcessTaskAddSignRecord.class); verify(processTaskAddSignRecordMapper).insert(recordCaptor.capture()); ProcessTaskAddSignRecord record = recordCaptor.getValue(); assertThat(record.getSourceTaskId()).isEqualTo(TASK_ID); @@ -264,7 +280,7 @@ class TaskServiceImplTests { verify(runtimeService).addMultiInstanceExecution(eq(NODE_ID), eq(PROCESS_INSTANCE_ID), argThat(variables -> Long.valueOf(TARGET_USER_ID).equals(variables.get(NODE_ID + "_assignee_temp")) && Long.valueOf(ADD_SIGN_RECORD_ID) - .equals(variables.get(ProcessInstanceConstant.VAR_ADD_SIGN_RECORD_ID)))); + .equals(variables.get(ProcessInstanceConstant.VAR_ADD_SIGN_RECORD_ID)))); } @Test @@ -272,7 +288,7 @@ class TaskServiceImplTests { prepareTaskAndNode(null); prepareActiveAssignment(); when(processInstanceRecordService.lambdaQuery()) - .thenAnswer(invocation -> new LambdaQueryChainWrapper<>(processInstanceBaseMapper)); + .thenAnswer(invocation -> new LambdaQueryChainWrapper<>(processInstanceBaseMapper)); when(processInstanceBaseMapper.selectCount(any())).thenReturn(1L); R result = taskServiceImpl.addSign(request(List.of(TARGET_USER_ID), "需要共同确认")); @@ -300,8 +316,8 @@ class TaskServiceImplTests { prepareTaskAndNode(null); prepareOriginalApprover(); ProcessNodeRecordAssignUser existingParticipant = new ProcessNodeRecordAssignUser() - .setNodeRecordId(NODE_RECORD_ID) - .setUserId(TARGET_USER_ID); + .setNodeRecordId(NODE_RECORD_ID) + .setUserId(TARGET_USER_ID); prepareTargetValidation(List.of(originalAssignment(), existingParticipant), validUser(TARGET_USER_ID, TENANT_ID)); @@ -319,11 +335,10 @@ class TaskServiceImplTests { prepareTargetValidation(List.of(originalAssignment()), validUser(TARGET_USER_ID, TENANT_ID)); prepareAuditInsert(); when(runtimeService.addMultiInstanceExecution(anyString(), anyString(), anyMap())) - .thenThrow(new FlowableOptimisticLockingException("conflict")); + .thenThrow(new FlowableOptimisticLockingException("conflict")); RecordingTransactionManager transactionManager = new RecordingTransactionManager(); - R result = transactionalService(transactionManager) - .addSign(request(List.of(TARGET_USER_ID), "需要共同确认")); + R result = transactionalService(transactionManager).addSign(request(List.of(TARGET_USER_ID), "需要共同确认")); assertThat(result.getCode()).isEqualTo(1); assertThat(result.getMsg()).isEqualTo("节点状态已变化,请刷新后重试"); @@ -349,27 +364,26 @@ class TaskServiceImplTests { } ProcessNodeRecord nodeRecord = new ProcessNodeRecord().setId(NODE_RECORD_ID); - when(processNodeRecordMapper.selectActiveByIdForUpdate(NODE_RECORD_ID, PROCESS_INSTANCE_ID, NODE_ID, - TENANT_ID)) - .thenReturn(nodeRecord); + when(processNodeRecordMapper.selectActiveByIdForUpdate(NODE_RECORD_ID, PROCESS_INSTANCE_ID, NODE_ID, TENANT_ID)) + .thenReturn(nodeRecord); Node node = new Node(); node.setAssignedType(ProcessInstanceConstant.AssignedTypeClass.ROLE); node.setMultipleMode(ProcessInstanceConstant.MULTIPLE_MODE_AL_SAME); node.setAllowAddSign(true); - when(nodeDataService.getNodeData(FLOW_ID, NODE_ID)).thenReturn(R.ok(node)); + when(nodeDataService.getPublishedNode(PROCESS_DEFINITION_ID, NODE_ID)).thenReturn(R.ok(node)); } private void prepareOriginalApprover() { prepareActiveAssignment(); when(sourceTask.getAssignee()).thenReturn(String.valueOf(USER_ID)); when(processInstanceRecordService.lambdaQuery()) - .thenAnswer(invocation -> new LambdaQueryChainWrapper<>(processInstanceBaseMapper)); + .thenAnswer(invocation -> new LambdaQueryChainWrapper<>(processInstanceBaseMapper)); } private void prepareActiveAssignment() { when(sourceTask.getId()).thenReturn(TASK_ID); when(processNodeRecordAssignUserService.lambdaQuery()) - .thenAnswer(invocation -> new LambdaQueryChainWrapper<>(assignmentBaseMapper)); + .thenAnswer(invocation -> new LambdaQueryChainWrapper<>(assignmentBaseMapper)); when(assignmentBaseMapper.selectOne(any())).thenReturn(originalAssignment()); } @@ -388,20 +402,20 @@ class TaskServiceImplTests { private ChangeActivityStateBuilder prepareTerminationFlow(ProcessInstanceRecord activeRecord) { when(processInstanceRecordService.lambdaQuery()) - .thenAnswer(invocation -> new LambdaQueryChainWrapper<>(processInstanceBaseMapper)); + .thenAnswer(invocation -> new LambdaQueryChainWrapper<>(processInstanceBaseMapper)); when(processInstanceBaseMapper.selectList(any())).thenReturn(List.of()); if (activeRecord != null) { when(processInstanceBaseMapper.selectOne(any())).thenReturn(activeRecord); when(processNodeRecordAssignUserService.lambdaQuery()) - .thenAnswer(invocation -> new LambdaQueryChainWrapper<>(assignmentBaseMapper)); + .thenAnswer(invocation -> new LambdaQueryChainWrapper<>(assignmentBaseMapper)); when(assignmentBaseMapper.selectCount(any())).thenReturn(0L); } when(processInstanceRecordService.lambdaUpdate()) - .thenAnswer(invocation -> new LambdaUpdateChainWrapper<>(processInstanceBaseMapper)); + .thenAnswer(invocation -> new LambdaUpdateChainWrapper<>(processInstanceBaseMapper)); when(processNodeRecordService.lambdaUpdate()) - .thenAnswer(invocation -> new LambdaUpdateChainWrapper<>(processNodeRecordMapper)); + .thenAnswer(invocation -> new LambdaUpdateChainWrapper<>(processNodeRecordMapper)); when(processNodeRecordAssignUserService.lambdaUpdate()) - .thenAnswer(invocation -> new LambdaUpdateChainWrapper<>(assignmentBaseMapper)); + .thenAnswer(invocation -> new LambdaUpdateChainWrapper<>(assignmentBaseMapper)); ProcessInstanceQuery processInstanceQuery = mock(ProcessInstanceQuery.class, Answers.RETURNS_SELF); when(runtimeService.createProcessInstanceQuery()).thenReturn(processInstanceQuery); @@ -422,20 +436,19 @@ class TaskServiceImplTests { String finishReason) { var inOrder = inOrder(runtimeService, stateChangeBuilder); inOrder.verify(runtimeService) - .setVariable(PROCESS_INSTANCE_ID, ProcessInstanceConstant.FINISH_REASON_VAR, finishReason); + .setVariable(PROCESS_INSTANCE_ID, ProcessInstanceConstant.FINISH_REASON_VAR, finishReason); inOrder.verify(runtimeService).createChangeActivityStateBuilder(); inOrder.verify(stateChangeBuilder).moveExecutionsToSingleActivityId(List.of("execution-1"), "end"); inOrder.verify(stateChangeBuilder).changeState(); } private ProcessNodeRecordAssignUser originalAssignment() { - return new ProcessNodeRecordAssignUser() - .setNodeRecordId(NODE_RECORD_ID) - .setTaskId(TASK_ID) - .setUserId(USER_ID) - .setInitialUserId(USER_ID) - .setAssignmentOrigin(ProcessInstanceConstant.ASSIGNMENT_ORIGIN_ORIGINAL) - .setStatus(NodeStatusEnum.JXZ.getCode()); + return new ProcessNodeRecordAssignUser().setNodeRecordId(NODE_RECORD_ID) + .setTaskId(TASK_ID) + .setUserId(USER_ID) + .setInitialUserId(USER_ID) + .setAssignmentOrigin(ProcessInstanceConstant.ASSIGNMENT_ORIGIN_ORIGINAL) + .setStatus(NodeStatusEnum.JXZ.getCode()); } private SysUser validUser(long userId, long tenantId) { @@ -452,8 +465,8 @@ class TaskServiceImplTests { private ITaskService transactionalService(RecordingTransactionManager transactionManager) { ProxyFactory proxyFactory = new ProxyFactory(taskServiceImpl); proxyFactory.addInterface(ITaskService.class); - proxyFactory.addAdvice(new TransactionInterceptor(transactionManager, - new AnnotationTransactionAttributeSource())); + proxyFactory + .addAdvice(new TransactionInterceptor(transactionManager, new AnnotationTransactionAttributeSource())); return (ITaskService) proxyFactory.getProxy(); } diff --git a/pigx-flow/pigx-flow-biz/src/test/java/com/pig4cloud/pigx/flow/support/node/MultiInstanceHandlerTests.java b/pigx-flow/pigx-flow-biz/src/test/java/com/pig4cloud/pigx/flow/support/node/MultiInstanceHandlerTests.java new file mode 100644 index 000000000..5d73797ce --- /dev/null +++ b/pigx-flow/pigx-flow-biz/src/test/java/com/pig4cloud/pigx/flow/support/node/MultiInstanceHandlerTests.java @@ -0,0 +1,40 @@ +package com.pig4cloud.pigx.flow.support.node; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.pig4cloud.pigx.common.core.util.R; +import com.pig4cloud.pigx.flow.constant.ProcessInstanceConstant; +import com.pig4cloud.pigx.flow.dto.Node; +import com.pig4cloud.pigx.flow.service.IProcessNodeDataService; +import org.flowable.engine.impl.persistence.entity.ExecutionEntityImpl; +import org.junit.jupiter.api.Test; + +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class MultiInstanceHandlerTests { + + @Test + void countersignWaitsForDynamicallyExpandedInstanceCount() { + IProcessNodeDataService nodeDataService = mock(IProcessNodeDataService.class); + Node node = new Node(); + node.setMultipleMode(ProcessInstanceConstant.MULTIPLE_MODE_AL_SAME); + when(nodeDataService.getPublishedNode("flow-1:1:deployment", "approve-1")).thenReturn(R.ok(node)); + MultiInstanceHandler handler = new MultiInstanceHandler(nodeDataService, Collections.emptyMap(), + new ObjectMapper()); + ExecutionEntityImpl execution = mock(ExecutionEntityImpl.class); + when(execution.getProcessDefinitionId()).thenReturn("flow-1:1:deployment"); + when(execution.getCurrentActivityId()).thenReturn("approve-1"); + when(execution.getVariable("approve-1_approve_condition")).thenReturn(true); + when(execution.getVariable("nrOfInstances")).thenReturn(3); + when(execution.getVariable("nrOfCompletedInstances")).thenReturn(2); + + assertThat(handler.completionCondition(execution)).isFalse(); + + when(execution.getVariable("nrOfCompletedInstances")).thenReturn(3); + assertThat(handler.completionCondition(execution)).isTrue(); + } + +} diff --git a/pigx-flow/pigx-flow-biz/src/test/java/com/pig4cloud/pigx/flow/support/tasks/ApproveServiceTaskTests.java b/pigx-flow/pigx-flow-biz/src/test/java/com/pig4cloud/pigx/flow/support/tasks/ApproveServiceTaskTests.java index ea79746b0..bdae7039f 100644 --- a/pigx-flow/pigx-flow-biz/src/test/java/com/pig4cloud/pigx/flow/support/tasks/ApproveServiceTaskTests.java +++ b/pigx-flow/pigx-flow-biz/src/test/java/com/pig4cloud/pigx/flow/support/tasks/ApproveServiceTaskTests.java @@ -26,6 +26,7 @@ class ApproveServiceTaskTests { ExecutionEntityImpl execution = mock(ExecutionEntityImpl.class); when(execution.getActivityId()).thenReturn("approve_service_task_approve-1"); when(execution.getProcessDefinitionKey()).thenReturn("leave"); + when(execution.getProcessDefinitionId()).thenReturn("leave:1:deployment"); when(execution.getProcessInstanceId()).thenReturn("process-7598"); when(execution.getVariable("approve-1_approve_condition", Boolean.class)).thenReturn(false); @@ -34,14 +35,14 @@ class ApproveServiceTaskTests { when(runtimeService.createChangeActivityStateBuilder()).thenReturn(stateChangeBuilder); when(stateChangeBuilder.processInstanceId("process-7598")).thenReturn(stateChangeBuilder); when(stateChangeBuilder.moveActivityIdTo("approve_service_task_approve-1", "end")) - .thenReturn(stateChangeBuilder); + .thenReturn(stateChangeBuilder); Refuse refuse = new Refuse(); refuse.setHandler("TO_END"); Node node = new Node(); node.setRefuse(refuse); IProcessNodeDataService nodeDataService = mock(IProcessNodeDataService.class); - when(nodeDataService.getNodeData("leave", "approve-1")).thenReturn(R.ok(node)); + when(nodeDataService.getPublishedNode("leave:1:deployment", "approve-1")).thenReturn(R.ok(node)); ApplicationContext previousContext = SpringUtil.getApplicationContext(); GenericApplicationContext context = new GenericApplicationContext(); @@ -58,8 +59,9 @@ class ApproveServiceTaskTests { } InOrder inOrder = inOrder(runtimeService, stateChangeBuilder); - inOrder.verify(runtimeService).setVariable("process-7598", ProcessInstanceConstant.FINISH_REASON_VAR, - ProcessInstanceConstant.FINISH_REASON_REFUSE); + inOrder.verify(runtimeService) + .setVariable("process-7598", ProcessInstanceConstant.FINISH_REASON_VAR, + ProcessInstanceConstant.FINISH_REASON_REFUSE); inOrder.verify(stateChangeBuilder).moveActivityIdTo("approve_service_task_approve-1", "end"); inOrder.verify(stateChangeBuilder).changeState(); verify(runtimeService).createChangeActivityStateBuilder();