From 10b2d10fcc1f055f7d28fbe8870d3cf33ca57488 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 23 Nov 2024 11:03:12 +0800 Subject: [PATCH] =?UTF-8?q?=E3=80=90=E5=8A=9F=E8=83=BD=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E3=80=91bpm=EF=BC=9A=E4=BB=BF=E9=92=89=E9=92=89=E3=80=81?= =?UTF-8?q?=E9=A3=9E=E4=B9=A6=E7=9A=84=E8=AE=BE=E8=AE=A1=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .image/工作流设计器-bpmn.jpg | Bin 0 -> 181405 bytes .image/工作流设计器-simple.jpg | Bin 0 -> 129583 bytes README.md | 22 +- sql/mysql/ruoyi-vue-pro.sql | 19 +- .../util/collection/CollectionUtils.java | 10 +- .../module/bpm/enums/ErrorCodeConstants.java | 8 +- .../definition/BpmSimpleModelNodeType.java | 34 +- .../BpmUserTaskApproveMethodEnum.java | 13 +- .../task/BpmProcessInstanceStatusEnum.java | 4 + .../bpm/enums/task/BpmTaskStatusEnum.java | 10 +- .../admin/base/user/UserSimpleBaseVO.java | 7 +- .../definition/BpmCategoryController.java | 9 + .../admin/definition/BpmModelController.java | 41 +- .../BpmProcessDefinitionController.java | 5 +- .../vo/model/BpmModelMetaInfoVO.java | 19 +- .../vo/model/BpmModelPageReqVO.java | 21 - .../vo/model/simple/BpmSimpleModelNodeVO.java | 8 - .../process/BpmProcessDefinitionRespVO.java | 10 +- .../admin/task/BpmActivityController.java | 40 - .../task/BpmProcessInstanceController.http | 16 + .../task/BpmProcessInstanceController.java | 20 +- .../BpmProcessInstanceCopyController.java | 14 +- .../admin/task/BpmTaskController.java | 27 +- .../vo/cc/BpmProcessInstanceCopyRespVO.java | 31 +- .../vo/instance/BpmApprovalDetailReqVO.java | 17 +- .../vo/instance/BpmApprovalDetailRespVO.java | 73 +- .../BpmFormFieldsPermissionReqVO.java | 37 - ...BpmProcessInstanceBpmnModelViewRespVO.java | 43 + .../vo/instance/BpmProcessInstanceRespVO.java | 19 +- .../task/vo/task/BpmTaskApproveReqVO.java | 4 - .../admin/task/vo/task/BpmTaskCopyReqVO.java | 23 + .../admin/task/vo/task/BpmTaskRespVO.java | 21 +- .../task/vo/task/BpmTaskReturnReqVO.java | 10 +- .../convert/definition/BpmModelConvert.java | 29 +- .../BpmProcessDefinitionConvert.java | 13 +- .../bpm/convert/task/BpmActivityConvert.java | 30 - .../task/BpmProcessInstanceConvert.java | 185 ++- .../bpm/convert/task/BpmTaskConvert.java | 108 +- .../BpmProcessDefinitionInfoDO.java | 7 +- .../task/BpmProcessInstanceCopyDO.java | 31 +- .../BpmProcessDefinitionInfoMapper.java | 6 + .../BpmParallelMultiInstanceBehavior.java | 2 +- .../BpmSequentialMultiInstanceBehavior.java | 2 +- .../behavior/BpmUserTaskActivityBehavior.java | 2 +- .../candidate/BpmTaskCandidateInvoker.java | 67 +- .../candidate/BpmTaskCandidateStrategy.java | 43 +- .../BpmTaskCandidateAbstractStrategy.java | 37 - ...ctBpmTaskCandidateDeptLeaderStrategy.java} | 27 +- ...mTaskCandidateDeptLeaderMultiStrategy.java | 22 +- .../BpmTaskCandidateDeptLeaderStrategy.java | 16 +- .../BpmTaskCandidateDeptMemberStrategy.java | 17 +- ...idateStartUserDeptLeaderMultiStrategy.java | 50 +- ...kCandidateStartUserDeptLeaderStrategy.java | 44 +- ...mTaskCandidateStartUserSelectStrategy.java | 67 +- ...mTaskCandidateFormSDeptLeaderStrategy.java | 56 + .../BpmTaskCandidateFormUserStrategy.java | 47 + .../BpmTaskCandidateAssignEmptyStrategy.java | 35 +- .../BpmTaskCandidateExpressionStrategy.java | 24 +- .../BpmTaskCandidateGroupStrategy.java | 16 +- .../BpmTaskCandidatePostStrategy.java | 15 +- .../BpmTaskCandidateRoleStrategy.java | 9 +- .../BpmTaskCandidateStartUserStrategy.java | 25 +- .../BpmTaskCandidateUserStrategy.java | 13 +- ...riableConvertByTypeExpressionFunction.java | 8 +- .../enums/BpmTaskCandidateStrategyEnum.java | 2 + .../core/enums/BpmnModelConstants.java | 6 +- .../core/enums/BpmnVariableConstants.java | 7 +- .../core/enums/SimpleModelConstants.java | 33 - .../core/listener/BpmCopyTaskDelegate.java | 10 +- .../core/listener/BpmTaskEventListener.java | 7 +- .../flowable/core/util/BpmnModelUtils.java | 398 ++++++- .../flowable/core/util/FlowableUtils.java | 71 +- .../flowable/core/util/SimpleModelUtils.java | 1044 +++++++++-------- .../definition/BpmCategoryService.java | 7 + .../definition/BpmCategoryServiceImpl.java | 23 +- .../service/definition/BpmModelService.java | 22 +- .../definition/BpmModelServiceImpl.java | 88 +- .../BpmProcessDefinitionService.java | 14 +- .../BpmProcessDefinitionServiceImpl.java | 12 +- .../bpm/service/task/BpmActivityService.java | 30 - .../service/task/BpmActivityServiceImpl.java | 37 - .../task/BpmProcessInstanceCopyService.java | 33 +- .../BpmProcessInstanceCopyServiceImpl.java | 24 +- .../task/BpmProcessInstanceService.java | 17 +- .../task/BpmProcessInstanceServiceImpl.java | 2 +- .../bpm/service/task/BpmTaskService.java | 67 +- .../bpm/service/task/BpmTaskServiceImpl.java | 185 ++- .../task/bo/AlreadyRunApproveNodeRespBO.java | 36 - .../BpmTaskCandidateInvokerTest.java | 243 +++- ...kCandidateDeptLeaderMultiStrategyTest.java | 45 + ...pmTaskCandidateDeptLeaderStrategyTest.java | 21 +- ...pmTaskCandidateDeptMemberStrategyTest.java | 28 +- ...eStartUserDeptLeaderMultiStrategyTest.java | 84 ++ ...didateStartUserDeptLeaderStrategyTest.java | 84 ++ ...kCandidateStartUserSelectStrategyTest.java | 68 ++ ...mTaskCandidateAssignEmptyStrategyTest.java | 88 ++ ...pmTaskCandidateExpressionStrategyTest.java | 26 +- .../BpmTaskCandidateGroupStrategyTest.java | 6 +- .../BpmTaskCandidatePostStrategyTest.java | 6 +- .../BpmTaskCandidateRoleStrategyTest.java | 6 +- ...BpmTaskCandidateStartUserStrategyTest.java | 56 + .../BpmTaskCandidateUserStrategyTest.java | 9 +- 102 files changed, 3026 insertions(+), 1607 deletions(-) create mode 100644 .image/工作流设计器-bpmn.jpg create mode 100644 .image/工作流设计器-simple.jpg delete mode 100644 yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelPageReqVO.java delete mode 100644 yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmActivityController.java create mode 100644 yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceController.http delete mode 100644 yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmFormFieldsPermissionReqVO.java create mode 100644 yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceBpmnModelViewRespVO.java create mode 100644 yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskCopyReqVO.java delete mode 100644 yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmActivityConvert.java delete mode 100644 yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateAbstractStrategy.java rename yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/{BpmTaskCandidateAbstractDeptLeaderStrategy.java => dept/AbstractBpmTaskCandidateDeptLeaderStrategy.java} (78%) rename yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/{ => dept}/BpmTaskCandidateDeptLeaderMultiStrategy.java (62%) rename yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/{ => dept}/BpmTaskCandidateDeptLeaderStrategy.java (75%) rename yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/{ => dept}/BpmTaskCandidateDeptMemberStrategy.java (79%) rename yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/{ => dept}/BpmTaskCandidateStartUserDeptLeaderMultiStrategy.java (51%) rename yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/{ => dept}/BpmTaskCandidateStartUserDeptLeaderStrategy.java (59%) rename yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/{ => dept}/BpmTaskCandidateStartUserSelectStrategy.java (58%) create mode 100644 yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/form/BpmTaskCandidateFormSDeptLeaderStrategy.java create mode 100644 yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/form/BpmTaskCandidateFormUserStrategy.java rename yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/{ => other}/BpmTaskCandidateAssignEmptyStrategy.java (63%) rename yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/{ => other}/BpmTaskCandidateExpressionStrategy.java (58%) rename yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/{ => user}/BpmTaskCandidateGroupStrategy.java (74%) rename yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/{ => user}/BpmTaskCandidatePostStrategy.java (82%) rename yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/{ => user}/BpmTaskCandidateRoleStrategy.java (82%) rename yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/{ => user}/BpmTaskCandidateStartUserStrategy.java (63%) rename yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/{ => user}/BpmTaskCandidateUserStrategy.java (77%) delete mode 100644 yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/SimpleModelConstants.java delete mode 100644 yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmActivityService.java delete mode 100644 yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmActivityServiceImpl.java delete mode 100644 yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/bo/AlreadyRunApproveNodeRespBO.java create mode 100644 yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateDeptLeaderMultiStrategyTest.java rename yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/{ => dept}/BpmTaskCandidateDeptLeaderStrategyTest.java (62%) rename yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/{ => dept}/BpmTaskCandidateDeptMemberStrategyTest.java (54%) create mode 100644 yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserDeptLeaderMultiStrategyTest.java create mode 100644 yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserDeptLeaderStrategyTest.java create mode 100644 yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserSelectStrategyTest.java create mode 100644 yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/other/BpmTaskCandidateAssignEmptyStrategyTest.java rename yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/{ => other}/BpmTaskCandidateExpressionStrategyTest.java (60%) rename yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/{ => user}/BpmTaskCandidateGroupStrategyTest.java (90%) rename yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/{ => user}/BpmTaskCandidatePostStrategyTest.java (91%) rename yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/{ => user}/BpmTaskCandidateRoleStrategyTest.java (90%) create mode 100644 yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateStartUserStrategyTest.java rename yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/{ => user}/BpmTaskCandidateUserStrategyTest.java (80%) diff --git a/.image/工作流设计器-bpmn.jpg b/.image/工作流设计器-bpmn.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2a61f60f869b28dce151f0964a1aaad8c0b333cd GIT binary patch literal 181405 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

3SP)i)UQi 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 CczQ?x literal 0 HcmV?d00001 diff --git a/.image/工作流设计器-simple.jpg b/.image/工作流设计器-simple.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9ef2c9e293b2eb91f0e0b19c63bdbda263f3254b GIT binary patch literal 129583 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 PageResult convertPage(PageResult from, Function func) { + if (ArrayUtil.isEmpty(from)) { + return new PageResult<>(from.getTotal()); + } + return new PageResult<>(convertList(from.getList(), func), from.getTotal()); + } + public static List convertListByFlatMap(Collection from, Function> func) { if (CollUtil.isEmpty(from)) { @@ -324,7 +332,7 @@ public class CollectionUtils { } public static List newArrayList(List> list) { - return list.stream().flatMap(Collection::stream).collect(Collectors.toList()); + return list.stream().filter(Objects::nonNull).flatMap(Collection::stream).collect(Collectors.toList()); } } \ No newline at end of file diff --git a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java index 6e32decc0..6c7a7ce92 100644 --- a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java +++ b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java @@ -23,7 +23,7 @@ public interface ErrorCodeConstants { "原因:用户任务({})未配置审批人,请点击【流程设计】按钮,选择该它的【任务(审批人)】进行配置"); ErrorCode MODEL_DEPLOY_FAIL_BPMN_START_EVENT_NOT_EXISTS = new ErrorCode(1_009_002_005, "部署流程失败,原因:BPMN 流程图中,没有开始事件"); ErrorCode MODEL_DEPLOY_FAIL_BPMN_USER_TASK_NAME_NOT_EXISTS = new ErrorCode(1_009_002_006, "部署流程失败,原因:BPMN 流程图中,用户任务({})的名字不存在"); - ErrorCode MODEL_UPDATE_FAIL_NOT_MANAGER = new ErrorCode(1_009_002_007, "操作流程失败,原因:你不是该流程的管理员"); + ErrorCode MODEL_UPDATE_FAIL_NOT_MANAGER = new ErrorCode(1_009_002_007, "操作流程失败,原因:你不是该流程({})的管理员"); // ========== 流程定义 1-009-003-000 ========== ErrorCode PROCESS_DEFINITION_KEY_NOT_MATCH = new ErrorCode(1_009_003_000, "流程定义的标识期望是({}),当前是({}),请修改 BPMN 流程图"); @@ -35,8 +35,8 @@ public interface ErrorCodeConstants { ErrorCode PROCESS_INSTANCE_NOT_EXISTS = new ErrorCode(1_009_004_000, "流程实例不存在"); ErrorCode PROCESS_INSTANCE_CANCEL_FAIL_NOT_EXISTS = new ErrorCode(1_009_004_001, "流程取消失败,流程不处于运行中"); ErrorCode PROCESS_INSTANCE_CANCEL_FAIL_NOT_SELF = new ErrorCode(1_009_004_002, "流程取消失败,该流程不是你发起的"); - ErrorCode PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_CONFIG = new ErrorCode(1_009_004_003, "审批任务({})的审批人未配置"); - ErrorCode PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_EXISTS = new ErrorCode(1_009_004_004, "审批任务({})的审批人({})不存在"); + ErrorCode PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_CONFIG = new ErrorCode(1_009_004_003, "任务({})的候选人未配置"); + ErrorCode PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_EXISTS = new ErrorCode(1_009_004_004, "任务({})的候选人({})不存在"); ErrorCode PROCESS_INSTANCE_START_USER_CAN_START = new ErrorCode(1_009_004_005, "发起流程失败,你没有权限发起该流程"); // ========== 流程任务 1-009-005-000 ========== @@ -44,7 +44,7 @@ public interface ErrorCodeConstants { ErrorCode TASK_NOT_EXISTS = new ErrorCode(1_009_005_002, "流程任务不存在"); ErrorCode TASK_IS_PENDING = new ErrorCode(1_009_005_003, "当前任务处于挂起状态,不能操作"); ErrorCode TASK_TARGET_NODE_NOT_EXISTS = new ErrorCode(1_009_005_004, " 目标节点不存在"); - ErrorCode TASK_RETURN_FAIL_SOURCE_TARGET_ERROR = new ErrorCode(1_009_005_006, "回退任务失败,目标节点是在并行网关上或非同一路线上,不可跳转"); + ErrorCode TASK_RETURN_FAIL_SOURCE_TARGET_ERROR = new ErrorCode(1_009_005_006, "退回任务失败,目标节点是在并行网关上或非同一路线上,不可跳转"); ErrorCode TASK_DELEGATE_FAIL_USER_REPEAT = new ErrorCode(1_009_005_007, "任务委派失败,委派人和当前审批人为同一人"); ErrorCode TASK_DELEGATE_FAIL_USER_NOT_EXISTS = new ErrorCode(1_009_005_008, "任务委派失败,被委派人不存在"); ErrorCode TASK_SIGN_CREATE_USER_NOT_EXIST = new ErrorCode(1_009_005_009, "任务加签:选择的用户不存在"); diff --git a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmSimpleModelNodeType.java b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmSimpleModelNodeType.java index 36ad0e5ee..4a2e1d50f 100644 --- a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmSimpleModelNodeType.java +++ b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmSimpleModelNodeType.java @@ -18,30 +18,26 @@ import java.util.Objects; public enum BpmSimpleModelNodeType implements IntArrayValuable { // 0 ~ 1 开始和结束 - START_NODE(0, "startEvent", "开始节点"), - END_NODE(1, "endEvent", "结束节点"), + START_NODE(0, "开始", "startEvent"), + END_NODE(1, "结束", "endEvent"), // 10 ~ 49 各种节点 - START_USER_NODE(10, "userTask", "发起人节点"), // 发起人节点。前端的开始节点,Id 固定 - APPROVE_NODE(11, "userTask", "审批人节点"), - COPY_NODE(12, "serviceTask", "抄送人节点"), + START_USER_NODE(10, "发起人", "userTask"), // 发起人节点。前端的开始节点,Id 固定 + APPROVE_NODE(11, "审批人", "userTask"), + COPY_NODE(12, "抄送人", "serviceTask"), // 50 ~ 条件分支 - CONDITION_NODE(50, "sequenceFlow", "条件节点"), // 用于构建流转条件的表达式 - CONDITION_BRANCH_NODE(51, " “parallelGateway”", "条件分支节点"), // TODO @jason:是不是改成叫 条件分支? - PARALLEL_BRANCH_NODE(52, "exclusiveGateway", "并行分支节点"), // TODO @jason:是不是一个 并行分支 ?就可以啦? 后面是否去掉并行网关。只用包容网关 - INCLUSIVE_BRANCH_NODE(53, "inclusiveGateway", "包容分支节点"), - // TODO @jason:建议整合 join,最终只有 条件分支、并行分支、包容分支,三种~ - // TODO @芋艿。 感觉还是分开好理解一点,也好处理一点。前端结构中把聚合节点显示并传过来。 + CONDITION_NODE(50, "条件", "sequenceFlow"), // 用于构建流转条件的表达式 + CONDITION_BRANCH_NODE(51, "条件分支", "exclusiveGateway"), + PARALLEL_BRANCH_NODE(52, "并行分支", "parallelGateway"), + INCLUSIVE_BRANCH_NODE(53, "包容分支", "inclusiveGateway"), ; public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(BpmSimpleModelNodeType::getType).toArray(); - public static final String BPMN_USER_TASK_TYPE = "userTask"; - private final Integer type; - private final String bpmnType; private final String name; + private final String bpmnType; /** * 判断是否为分支节点 @@ -54,16 +50,6 @@ public enum BpmSimpleModelNodeType implements IntArrayValuable { || Objects.equals(INCLUSIVE_BRANCH_NODE.getType(), type); } - /** - * 判断是否需要记录的节点 - * - * @param bpmnType bpmn节点类型 - */ - public static boolean isRecordNode(String bpmnType) { - return Objects.equals(APPROVE_NODE.getBpmnType(), bpmnType) - || Objects.equals(END_NODE.getBpmnType(), bpmnType); - } - public static BpmSimpleModelNodeType valueOf(Integer type) { return ArrayUtil.firstMatch(nodeType -> nodeType.getType().equals(type), values()); } diff --git a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmUserTaskApproveMethodEnum.java b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmUserTaskApproveMethodEnum.java index 9d4dd63af..1089f181d 100644 --- a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmUserTaskApproveMethodEnum.java +++ b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmUserTaskApproveMethodEnum.java @@ -16,20 +16,23 @@ import java.util.Arrays; @AllArgsConstructor public enum BpmUserTaskApproveMethodEnum implements IntArrayValuable { - RANDOM(1, "随机挑选一人审批"), - RATIO(2, "多人会签(按通过比例)"), // 会签(按通过比例) - ANY(3, "多人或签(一人通过或拒绝)"), // 或签(通过只需一人,拒绝只需一人) - SEQUENTIAL(4, "依次审批"); // 依次审批 + RANDOM(1, "随机挑选一人审批", null), + RATIO(2, "多人会签(按通过比例)", "${ nrOfCompletedInstances/nrOfInstances >= %s}"), // 会签(按通过比例) + ANY(3, "多人或签(一人通过或拒绝)", "${ nrOfCompletedInstances > 0 }"), // 或签(通过只需一人,拒绝只需一人) + SEQUENTIAL(4, "依次审批", "${ nrOfCompletedInstances >= nrOfInstances }"); // 依次审批 /** * 审批方式 */ private final Integer method; - /** * 名字 */ private final String name; + /** + * 完成表达式 + */ + private final String completionCondition; public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(BpmUserTaskApproveMethodEnum::getMethod).toArray(); diff --git a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmProcessInstanceStatusEnum.java b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmProcessInstanceStatusEnum.java index c635e92ba..29cc1280e 100644 --- a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmProcessInstanceStatusEnum.java +++ b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmProcessInstanceStatusEnum.java @@ -38,6 +38,10 @@ public enum BpmProcessInstanceStatusEnum implements IntArrayValuable { return ARRAYS; } + public static boolean isRejectStatus(Integer status) { + return REJECT.getStatus().equals(status); + } + public static boolean isProcessEndStatus(Integer status) { return ObjectUtils.equalsAny(status, APPROVE.getStatus(), REJECT.getStatus(), CANCEL.getStatus()); diff --git a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmTaskStatusEnum.java b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmTaskStatusEnum.java index f577fc020..a19f122bd 100644 --- a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmTaskStatusEnum.java +++ b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmTaskStatusEnum.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.bpm.enums.task; +import cn.hutool.core.util.ObjUtil; import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; import lombok.AllArgsConstructor; import lombok.Getter; @@ -20,7 +21,6 @@ public enum BpmTaskStatusEnum { CANCEL(4, "已取消"), RETURN(5, "已退回"), - DELEGATE(6, "委派中"), /** * 使用场景: @@ -45,6 +45,10 @@ public enum BpmTaskStatusEnum { */ private final String name; + public static boolean isRejectStatus(Integer status) { + return REJECT.getStatus().equals(status); + } + /** * 判断该状态是否已经处于 End 最终状态 *

@@ -59,4 +63,8 @@ public enum BpmTaskStatusEnum { RETURN.getStatus(), APPROVING.getStatus()); } + public static boolean isCancelStatus(Integer status) { + return ObjUtil.equal(status, CANCEL.getStatus()); + } + } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/base/user/UserSimpleBaseVO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/base/user/UserSimpleBaseVO.java index e245b3026..b2fb01623 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/base/user/UserSimpleBaseVO.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/base/user/UserSimpleBaseVO.java @@ -9,11 +9,14 @@ public class UserSimpleBaseVO { @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Long id; - @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿") private String nickname; - @Schema(description = "用户头像", example = "https://www.iocoder.cn/1.png") private String avatar; + @Schema(description = "部门编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long deptId; + @Schema(description = "部门名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "研发部") + private String deptName; + } \ No newline at end of file diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmCategoryController.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmCategoryController.java index 322666615..05e571f71 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmCategoryController.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmCategoryController.java @@ -48,6 +48,15 @@ public class BpmCategoryController { return success(true); } + @PutMapping("/update-sort-batch") + @Operation(summary = "批量更新流程分类的排序") + @Parameter(name = "ids", description = "分类编号列表", required = true, example = "1,2,3") + @PreAuthorize("@ss.hasPermission('bpm:category:update')") + public CommonResult updateCategorySortBatch(@RequestParam("ids") List ids) { + categoryService.updateCategorySortBatch(ids); + return success(true); + } + @DeleteMapping("/delete") @Operation(summary = "删除流程分类") @Parameter(name = "id", description = "编号", required = true) diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmModelController.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmModelController.java index 28398a702..a84bf46ef 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmModelController.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmModelController.java @@ -2,8 +2,6 @@ package cn.iocoder.yudao.module.bpm.controller.admin.definition; import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.*; import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO; import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelUpdateReqVO; @@ -28,7 +26,7 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; -import java.util.HashSet; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; @@ -56,38 +54,38 @@ public class BpmModelController { @Resource private AdminUserApi adminUserApi; - @GetMapping("/page") + @GetMapping("/list") @Operation(summary = "获得模型分页") - public CommonResult> getModelPage(BpmModelPageReqVO pageVO) { - PageResult pageResult = modelService.getModelPage(pageVO); - if (CollUtil.isEmpty(pageResult.getList())) { - return success(PageResult.empty(pageResult.getTotal())); + @Parameter(name = "name", description = "模型名称", example = "芋艿") + public CommonResult> getModelPage(@RequestParam(value = "name", required = false) String name) { + List list = modelService.getModelList(name); + if (CollUtil.isEmpty(list)) { + return success(Collections.emptyList()); } - // 拼接数据 // 获得 Form 表单 - Set formIds = convertSet(pageResult.getList(), model -> { + Set formIds = convertSet(list, model -> { BpmModelMetaInfoVO metaInfo = BpmModelConvert.INSTANCE.parseMetaInfo(model); return metaInfo != null ? metaInfo.getFormId() : null; }); Map formMap = formService.getFormMap(formIds); // 获得 Category Map Map categoryMap = categoryService.getCategoryMap( - convertSet(pageResult.getList(), Model::getCategory)); + convertSet(list, Model::getCategory)); // 获得 Deployment Map - Set deploymentIds = new HashSet<>(); - pageResult.getList().forEach(model -> CollectionUtils.addIfNotNull(deploymentIds, model.getDeploymentId())); - Map deploymentMap = processDefinitionService.getDeploymentMap(deploymentIds); + Map deploymentMap = processDefinitionService.getDeploymentMap( + convertSet(list, Model::getDeploymentId)); // 获得 ProcessDefinition Map - List processDefinitions = processDefinitionService.getProcessDefinitionListByDeploymentIds(deploymentIds); + List processDefinitions = processDefinitionService.getProcessDefinitionListByDeploymentIds( + deploymentMap.keySet()); Map processDefinitionMap = convertMap(processDefinitions, ProcessDefinition::getDeploymentId); // 获得 User Map - Set userIds = convertSetByFlatMap(pageResult.getList(), model -> { + Set userIds = convertSetByFlatMap(list, model -> { BpmModelMetaInfoVO metaInfo = BpmModelConvert.INSTANCE.parseMetaInfo(model); return metaInfo != null ? metaInfo.getStartUserIds().stream() : Stream.empty(); }); Map userMap = adminUserApi.getUserMap(userIds); - return success(BpmModelConvert.INSTANCE.buildModelPage(pageResult, + return success(BpmModelConvert.INSTANCE.buildModelList(list, formMap, categoryMap, deploymentMap, processDefinitionMap, userMap)); } @@ -111,6 +109,7 @@ public class BpmModelController { return success(modelService.createModel(createRetVO)); } + @PutMapping("/update") @Operation(summary = "修改模型") @PreAuthorize("@ss.hasPermission('bpm:model:update')") @@ -119,6 +118,14 @@ public class BpmModelController { return success(true); } + @PutMapping("/update-sort-batch") + @Operation(summary = "批量修改模型排序") + @Parameter(name = "ids", description = "编号数组", required = true, example = "1,2,3") + public CommonResult updateModelSortBatch(@RequestParam("ids") List ids) { + modelService.updateModelSortBatch(getLoginUserId(), ids); + return success(true); + } + @PostMapping("/deploy") @Operation(summary = "部署模型") @Parameter(name = "id", description = "编号", required = true, example = "1024") diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmProcessDefinitionController.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmProcessDefinitionController.java index 542803c6a..28cbd0ab9 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmProcessDefinitionController.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmProcessDefinitionController.java @@ -9,7 +9,6 @@ import cn.iocoder.yudao.module.bpm.convert.definition.BpmProcessDefinitionConver import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO; import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO; import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO; -import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.BpmTaskCandidateStartUserSelectStrategy; import cn.iocoder.yudao.module.bpm.service.definition.BpmCategoryService; import cn.iocoder.yudao.module.bpm.service.definition.BpmFormService; import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService; @@ -18,7 +17,6 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import org.flowable.bpmn.model.BpmnModel; -import org.flowable.bpmn.model.UserTask; import org.flowable.engine.repository.Deployment; import org.flowable.engine.repository.ProcessDefinition; import org.springframework.security.access.prepost.PreAuthorize; @@ -115,9 +113,8 @@ public class BpmProcessDefinitionController { } BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionService.getProcessDefinitionInfo(processDefinition.getId()); BpmnModel bpmnModel = processDefinitionService.getProcessDefinitionBpmnModel(processDefinition.getId()); - List userTaskList = BpmTaskCandidateStartUserSelectStrategy.getStartUserSelectUserTaskList(bpmnModel); return success(BpmProcessDefinitionConvert.INSTANCE.buildProcessDefinition( - processDefinition, null, processDefinitionInfo, null, null, bpmnModel, userTaskList)); + processDefinition, null, processDefinitionInfo, null, null, bpmnModel)); } } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelMetaInfoVO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelMetaInfoVO.java index fa82ab1e6..f0676b251 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelMetaInfoVO.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelMetaInfoVO.java @@ -15,7 +15,9 @@ import java.util.List; * BPM 流程 MetaInfo Response DTO * 主要用于 { Model#setMetaInfo(String)} 的存储 * - * 最终,它的字段和 {@link cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO} 是一致的 + * 最终,它的字段和 + * {@link cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO} + * 是一致的 * * @author 芋道源码 */ @@ -40,13 +42,11 @@ public class BpmModelMetaInfoVO { @NotNull(message = "表单类型不能为空") private Integer formType; @Schema(description = "表单编号", example = "1024") - private Long formId; // formType 为 NORMAL 使用,必须非空 - @Schema(description = "自定义表单的提交路径,使用 Vue 的路由地址", - example = "/bpm/oa/leave/create") - private String formCustomCreatePath; // 表单类型为 CUSTOM 时,必须非空 - @Schema(description = "自定义表单的查看路径,使用 Vue 的路由地址", - example = "/bpm/oa/leave/view") - private String formCustomViewPath; // 表单类型为 CUSTOM 时,必须非空 + private Long formId; // formType 为 NORMAL 使用,必须非空 + @Schema(description = "自定义表单的提交路径,使用 Vue 的路由地址", example = "/bpm/oa/leave/create") + private String formCustomCreatePath; // 表单类型为 CUSTOM 时,必须非空 + @Schema(description = "自定义表单的查看路径,使用 Vue 的路由地址", example = "/bpm/oa/leave/view") + private String formCustomViewPath; // 表单类型为 CUSTOM 时,必须非空 @Schema(description = "是否可见", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") @NotNull(message = "是否可见不能为空") @@ -59,4 +59,7 @@ public class BpmModelMetaInfoVO { @NotEmpty(message = "可管理用户编号数组不能为空") private List managerUserIds; + @Schema(description = "排序", example = "1") + private Long sort; // 创建时,后端自动生成 + } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelPageReqVO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelPageReqVO.java deleted file mode 100644 index d2767c17f..000000000 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelPageReqVO.java +++ /dev/null @@ -1,21 +0,0 @@ -package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model; - -import cn.iocoder.yudao.framework.common.pojo.PageParam; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - - -@Schema(description = "管理后台 - 流程模型分页 Request VO") -@Data -public class BpmModelPageReqVO extends PageParam { - - @Schema(description = "标识,精准匹配", example = "process1641042089407") - private String key; - - @Schema(description = "名字,模糊匹配", example = "芋道") - private String name; - - @Schema(description = "流程分类", example = "1") - private String category; - -} diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/simple/BpmSimpleModelNodeVO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/simple/BpmSimpleModelNodeVO.java index 502511753..fca7cd6d6 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/simple/BpmSimpleModelNodeVO.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/simple/BpmSimpleModelNodeVO.java @@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple; import cn.iocoder.yudao.framework.common.validation.InEnum; import cn.iocoder.yudao.module.bpm.enums.definition.*; import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; -import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; @@ -31,7 +30,6 @@ public class BpmSimpleModelNodeVO { @Schema(description = "模型节点名称", example = "领导审批") private String name; - // TODO @jason:和 gpt 大模型对了下这个字段的命名,貌似叫 displayText 合适点。可以等最后我们全局替换下。(优先级:低) @Schema(description = "节点展示内容", example = "指定成员: 芋道源码") private String showText; @@ -79,12 +77,6 @@ public class BpmSimpleModelNodeVO { @Schema(description = "操作按钮设置", example = "[]") private List buttonsSetting; // 用于审批节点 - // TODO @jason:看看是不是可以简化;@芋艿: 暂时先放着。不知道后面是否会用到 - /** - * 附加节点 Id, 该节点不从前端传入。 由程序生成. 由于当个节点无法完成功能。 需要附加节点来完成。 - */ - @JsonIgnore - private String attachNodeId; /** * 审批节点拒绝处理 */ diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/process/BpmProcessDefinitionRespVO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/process/BpmProcessDefinitionRespVO.java index 2fb8dd4dc..1e9dfc820 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/process/BpmProcessDefinitionRespVO.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/process/BpmProcessDefinitionRespVO.java @@ -33,6 +33,9 @@ public class BpmProcessDefinitionRespVO { @Schema(description = "流程分类名字", example = "请假") private String categoryName; + @Schema(description = "流程模型的类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Integer modelType; // 参见 BpmModelTypeEnum 枚举类 + @Schema(description = "表单类型-参见 bpm_model_form_type 数据字典", example = "1") private Integer formType; @Schema(description = "表单编号-在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空", example = "1024") @@ -59,9 +62,12 @@ public class BpmProcessDefinitionRespVO { @Schema(description = "BPMN XML") private String bpmnXml; // 需要从对应的 BpmnModel 读取,非必须返回 - @Schema(description = "发起用户需要选择审批人的任务数组") - private List startUserSelectTasks; // 需要从对应的 BpmnModel 读取,非必须返回 + @Schema(description = "SIMPLE 设计器模型数据 json 格式") + private String simpleModel; // 非必须返回 + @Schema(description = "流程定义排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long sort; + @Schema(description = "BPMN UserTask 用户任务") @Data public static class UserTask { diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmActivityController.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmActivityController.java deleted file mode 100644 index 89bdb6d53..000000000 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmActivityController.java +++ /dev/null @@ -1,40 +0,0 @@ -package cn.iocoder.yudao.module.bpm.controller.admin.task; - -import cn.iocoder.yudao.framework.common.pojo.CommonResult; -import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.activity.BpmActivityRespVO; -import cn.iocoder.yudao.module.bpm.convert.task.BpmActivityConvert; -import cn.iocoder.yudao.module.bpm.service.task.BpmActivityService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.annotation.Resource; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -import java.util.List; - -import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; - -@Tag(name = "管理后台 - 流程活动实例") -@RestController -@RequestMapping("/bpm/activity") -@Validated -public class BpmActivityController { - - @Resource - private BpmActivityService activityService; - - @GetMapping("/list") - @Operation(summary = "生成指定流程实例的高亮流程图", - description = "只高亮进行中的任务。不过要注意,该接口暂时没用,通过前端的 ProcessViewer.vue 界面的 highlightDiagram 方法生成") - @Parameter(name = "processInstanceId", description = "流程实例的编号", required = true) - @PreAuthorize("@ss.hasPermission('bpm:task:query')") - public CommonResult> getActivityList( - @RequestParam("processInstanceId") String processInstanceId) { - return success(BpmActivityConvert.INSTANCE.convertList(activityService.getActivityListByProcessInstanceId(processInstanceId))); - } -} diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceController.http b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceController.http new file mode 100644 index 000000000..c69082725 --- /dev/null +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceController.http @@ -0,0 +1,16 @@ +### 请求 /bpm/process-instance/get-bpmn 接口 => 成功 +GET {{baseUrl}}/bpm/process-instance/get-bpmn-model-view?id=1d5fb5a6-85f8-11ef-b717-7e93075f94e3 +Content-Type: application/json +tenant-id: 1 +Authorization: Bearer {{token}} + +### 请求 /bpm/process-instance/get-bpmn 接口 => 失败 +#GET {{baseUrl}}/bpm/process-instance/get-approval-detail?processInstanceId=1d5fb5a6-85f8-11ef-b717-7e93075f94e3 +#GET {{baseUrl}}/bpm/process-instance/get-approval-detail?processInstanceId=3ee5c5ba-904a-11ef-a76e-b2ed5d6ef911 +#GET {{baseUrl}}/bpm/process-instance/get-approval-detail?processInstanceId=f630dfa2-8f92-11ef-947c-ba5e239a6eb4 +#GET {{baseUrl}}/bpm/process-instance/get-approval-detail?processInstanceId=9de8bdbf-9133-11ef-ae97-eaf49df1f932 +#GET {{baseUrl}}/bpm/process-instance/get-approval-detail?processInstanceId=dd2188eb-9394-11ef-a039-7a9ac3d9eb6b +GET {{baseUrl}}/bpm/process-instance/get-approval-detail?processDefinitionId=test-auto:1:c70a799a-9394-11ef-a039-7a9ac3d9eb6b +Content-Type: application/json +tenant-id: 1 +Authorization: Bearer {{token}} \ No newline at end of file diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceController.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceController.java index 5e1ea40d7..401cbc90e 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceController.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceController.java @@ -8,7 +8,6 @@ import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.*; import cn.iocoder.yudao.module.bpm.convert.task.BpmProcessInstanceConvert; import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO; import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO; -import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils; import cn.iocoder.yudao.module.bpm.service.definition.BpmCategoryService; import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService; import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; @@ -128,15 +127,13 @@ public class BpmProcessInstanceController { processInstance.getProcessDefinitionId()); BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionService.getProcessDefinitionInfo( processInstance.getProcessDefinitionId()); - String bpmnXml = BpmnModelUtils.getBpmnXml( - processDefinitionService.getProcessDefinitionBpmnModel(processInstance.getProcessDefinitionId())); AdminUserRespDTO startUser = adminUserApi.getUser(NumberUtils.parseLong(processInstance.getStartUserId())).getCheckedData(); DeptRespDTO dept = null; if (startUser != null && startUser.getDeptId() != null) { dept = deptApi.getDept(startUser.getDeptId()).getCheckedData(); } return success(BpmProcessInstanceConvert.INSTANCE.buildProcessInstance(processInstance, - processDefinition, processDefinitionInfo, bpmnXml, startUser, dept)); + processDefinition, processDefinitionInfo, startUser, dept)); } @DeleteMapping("/cancel-by-start-user") @@ -157,14 +154,6 @@ public class BpmProcessInstanceController { return success(true); } - @GetMapping("/get-form-fields-permission") - @Operation(summary = "获得表单字段权限") - @PreAuthorize("@ss.hasPermission('bpm:process-instance:query')") - public CommonResult> getFormFieldsPermission( - @Valid BpmFormFieldsPermissionReqVO reqVO) { - return success(processInstanceService.getFormFieldsPermission(reqVO)); - } - @GetMapping("/get-approval-detail") @Operation(summary = "获得审批详情") @Parameter(name = "id", description = "流程实例的编号", required = true) @@ -173,4 +162,11 @@ public class BpmProcessInstanceController { return success(processInstanceService.getApprovalDetail(getLoginUserId(), reqVO)); } + @GetMapping("/get-bpmn-model-view") + @Operation(summary = "获取流程实例的 BPMN 模型视图", description = "在【流程详细】界面中,进行调用") + @Parameter(name = "id", description = "流程实例的编号", required = true) + public CommonResult getProcessInstanceBpmnModelView(@RequestParam(value = "id") String id) { + return success(processInstanceService.getProcessInstanceBpmnModelView(id)); + } + } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceCopyController.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceCopyController.java index 8b97d6ae5..e8e2f9c71 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceCopyController.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceCopyController.java @@ -6,6 +6,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.collection.MapUtils; import cn.iocoder.yudao.framework.common.util.date.DateUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO; import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.cc.BpmProcessInstanceCopyRespVO; import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceCopyPageReqVO; import cn.iocoder.yudao.module.bpm.dal.dataobject.task.BpmProcessInstanceCopyDO; @@ -28,8 +29,7 @@ import java.util.Map; import java.util.stream.Stream; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertListByFlatMap; -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; @Tag(name = "管理后台 - 流程实例抄送") @@ -62,11 +62,15 @@ public class BpmProcessInstanceCopyController { convertSet(pageResult.getList(), BpmProcessInstanceCopyDO::getProcessInstanceId)); Map userMap = adminUserApi.getUserMap(convertListByFlatMap(pageResult.getList(), copy -> Stream.of(copy.getStartUserId(), Long.parseLong(copy.getCreator())))); - return success(BeanUtils.toBean(pageResult, BpmProcessInstanceCopyRespVO.class, copyVO -> { - MapUtils.findAndThen(userMap, Long.valueOf(copyVO.getCreator()), user -> copyVO.setCreatorName(user.getNickname())); - MapUtils.findAndThen(userMap, copyVO.getStartUserId(), user -> copyVO.setStartUserName(user.getNickname())); + return success(convertPage(pageResult, copy -> { + BpmProcessInstanceCopyRespVO copyVO = BeanUtils.toBean(copy, BpmProcessInstanceCopyRespVO.class); + MapUtils.findAndThen(userMap, Long.valueOf(copy.getCreator()), + user -> copyVO.setStartUser(BeanUtils.toBean(user, UserSimpleBaseVO.class))); + MapUtils.findAndThen(userMap, copy.getStartUserId(), + user -> copyVO.setCreateUser(BeanUtils.toBean(user, UserSimpleBaseVO.class))); MapUtils.findAndThen(processInstanceMap, copyVO.getProcessInstanceId(), processInstance -> copyVO.setProcessInstanceStartTime(DateUtils.of(processInstance.getStartTime()))); + return copyVO; })); } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmTaskController.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmTaskController.java index 8409d2e2f..3a017a627 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmTaskController.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmTaskController.java @@ -8,7 +8,6 @@ import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.*; import cn.iocoder.yudao.module.bpm.convert.task.BpmTaskConvert; import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO; import cn.iocoder.yudao.module.bpm.service.definition.BpmFormService; -import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService; import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; import cn.iocoder.yudao.module.bpm.service.task.BpmTaskService; import cn.iocoder.yudao.module.system.api.dept.DeptApi; @@ -20,7 +19,6 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.validation.Valid; -import org.flowable.bpmn.model.BpmnModel; import org.flowable.bpmn.model.UserTask; import org.flowable.engine.history.HistoricProcessInstance; import org.flowable.engine.runtime.ProcessInstance; @@ -52,8 +50,6 @@ public class BpmTaskController { private BpmProcessInstanceService processInstanceService; @Resource private BpmFormService formService; - @Resource - private BpmProcessDefinitionService bpmProcessDefinitionService; @Resource private AdminUserApi adminUserApi; @@ -121,27 +117,22 @@ public class BpmTaskController { @PreAuthorize("@ss.hasPermission('bpm:task:query')") public CommonResult> getTaskListByProcessInstanceId( @RequestParam("processInstanceId") String processInstanceId) { - List taskList = taskService.getTaskListByProcessInstanceId(processInstanceId); + List taskList = taskService.getTaskListByProcessInstanceId(processInstanceId, true); if (CollUtil.isEmpty(taskList)) { return success(Collections.emptyList()); } // 拼接数据 - HistoricProcessInstance processInstance = processInstanceService.getHistoricProcessInstance(processInstanceId); - // 获得 User 和 Dept Map Set userIds = convertSetByFlatMap(taskList, task -> Stream.of(NumberUtils.parseLong(task.getAssignee()), NumberUtils.parseLong(task.getOwner()))); - userIds.add(NumberUtils.parseLong(processInstance.getStartUserId())); Map userMap = adminUserApi.getUserMap(userIds); Map deptMap = deptApi.getDeptMap( convertSet(userMap.values(), AdminUserRespDTO::getDeptId)); // 获得 Form Map Map formMap = formService.getFormMap( convertSet(taskList, task -> NumberUtils.parseLong(task.getFormKey()))); - // 获得 BpmnModel - BpmnModel bpmnModel = bpmProcessDefinitionService.getProcessDefinitionBpmnModel(processInstance.getProcessDefinitionId()); - return success(BpmTaskConvert.INSTANCE.buildTaskListByProcessInstanceId(taskList, processInstance, - formMap, userMap, deptMap, bpmnModel)); + return success(BpmTaskConvert.INSTANCE.buildTaskListByProcessInstanceId(taskList, + formMap, userMap, deptMap)); } @PutMapping("/approve") @@ -161,7 +152,7 @@ public class BpmTaskController { } @GetMapping("/list-by-return") - @Operation(summary = "获取所有可回退的节点", description = "用于【流程详情】的【回退】按钮") + @Operation(summary = "获取所有可退回的节点", description = "用于【流程详情】的【退回】按钮") @Parameter(name = "taskId", description = "当前任务ID", required = true) @PreAuthorize("@ss.hasPermission('bpm:task:update')") public CommonResult> getTaskListByReturn(@RequestParam("id") String id) { @@ -171,7 +162,7 @@ public class BpmTaskController { } @PutMapping("/return") - @Operation(summary = "回退任务", description = "用于【流程详情】的【回退】按钮") + @Operation(summary = "退回任务", description = "用于【流程详情】的【退回】按钮") @PreAuthorize("@ss.hasPermission('bpm:task:update')") public CommonResult returnTask(@Valid @RequestBody BpmTaskReturnReqVO reqVO) { taskService.returnTask(getLoginUserId(), reqVO); @@ -210,6 +201,14 @@ public class BpmTaskController { return success(true); } + @PutMapping("/copy") + @Operation(summary = "抄送任务") + @PreAuthorize("@ss.hasPermission('bpm:task:update')") + public CommonResult copyTask(@Valid @RequestBody BpmTaskCopyReqVO reqVO) { + taskService.copyTask(getLoginUserId(), reqVO); + return success(true); + } + @GetMapping("/list-by-parent-task-id") @Operation(summary = "获得指定父级任务的子任务列表") // 目前用于,减签的时候,获得子任务列表 @Parameter(name = "parentTaskId", description = "父级任务编号", required = true) diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/cc/BpmProcessInstanceCopyRespVO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/cc/BpmProcessInstanceCopyRespVO.java index f5163faa3..b087c3a44 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/cc/BpmProcessInstanceCopyRespVO.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/cc/BpmProcessInstanceCopyRespVO.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.cc; +import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -12,31 +13,31 @@ public class BpmProcessInstanceCopyRespVO { @Schema(description = "抄送主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Long id; - @Schema(description = "发起人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "888") - private Long startUserId; - @Schema(description = "发起人昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") - private String startUserName; + @Schema(description = "发起人", requiredMode = Schema.RequiredMode.REQUIRED) + private UserSimpleBaseVO startUser; @Schema(description = "流程实例编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "A233") private String processInstanceId; - @Schema(description = "流程实例的名称") + @Schema(description = "流程实例的名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "测试") private String processInstanceName; - @Schema(description = "流程实例的发起时间") + @Schema(description = "流程实例的发起时间", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime processInstanceStartTime; - @Schema(description = "抄送的节点的活动编号") + @Schema(description = "流程活动的编号", requiredMode = Schema.RequiredMode.REQUIRED) private String activityId; - @Schema(description = "发起抄送的任务编号") + @Schema(description = "流程活动的名字", requiredMode = Schema.RequiredMode.REQUIRED) + private String activityName; + + @Schema(description = "流程活动的编号") private String taskId; - @Schema(description = "发起抄送的任务名称") - private String taskName; - @Schema(description = "抄送人") - private String creator; - @Schema(description = "抄送人昵称") - private String creatorName; + @Schema(description = "抄送人意见") + private String reason; - @Schema(description = "抄送时间") + @Schema(description = "创建人", requiredMode = Schema.RequiredMode.REQUIRED) + private UserSimpleBaseVO createUser; + + @Schema(description = "抄送时间", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime createTime; } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmApprovalDetailReqVO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmApprovalDetailReqVO.java index ffe0b0139..9121f1036 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmApprovalDetailReqVO.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmApprovalDetailReqVO.java @@ -6,16 +6,27 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.AssertTrue; import lombok.Data; -// TODO @jason:这个可以简化下,使用 @RequestParam。嘿嘿,主要 VO 项不要太多 +import java.util.Map; + @Schema(description = "管理后台 - 审批详情 Request VO") @Data public class BpmApprovalDetailReqVO { @Schema(description = "流程定义的编号", example = "1024") - private String processDefinitionId; + private String processDefinitionId; // 使用场景:发起流程时,传流程定义 ID + + @Schema(description = "流程变量") + private Map processVariables; // 使用场景:同 processDefinitionId,用于流程预测 @Schema(description = "流程实例的编号", example = "1024") - private String processInstanceId; + private String processInstanceId; // 使用场景:流程已发起时候传流程实例 ID + + // TODO @芋艿:如果未来 BPMN 增加流程图,它没有发起人节点,会有问题。 + @Schema(description = "流程活动编号", example = "StartUserNode") + private String activityId; // 用于获取表单权限。1)发起流程时,传“发起人节点” activityId 可获取发起人的表单权限;2)从抄送列表界面进来时,传抄送的 activityId 可获取抄送人的表单权限; + + @Schema(description = "流程任务编号", example = "95f2f08b-621b-11ef-bf39-00ff4722db8b") + private String taskId; // 用于获取表单权限。1)从待审批/已审批界面进来时,传递 taskId 任务编号,可获取任务节点的变得权限 @AssertTrue(message = "流程定义的编号和流程实例的编号不能同时为空") @JsonIgnore diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmApprovalDetailRespVO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmApprovalDetailRespVO.java index 283373893..148175d93 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmApprovalDetailRespVO.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmApprovalDetailRespVO.java @@ -1,10 +1,15 @@ package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance; +import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionRespVO; +import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.BpmTaskRespVO; +import com.fasterxml.jackson.annotation.JsonIgnore; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; import java.util.List; +import java.util.Map; @Schema(description = "管理后台 - 审批详情 Response VO") @@ -14,12 +19,28 @@ public class BpmApprovalDetailRespVO { @Schema(description = "流程实例的状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Integer status; // 参见 BpmProcessInstanceStatusEnum 枚举 - @Schema(description = "审批信息列表", requiredMode = Schema.RequiredMode.REQUIRED) - private List approveNodes; + @Schema(description = "活动节点列表", requiredMode = Schema.RequiredMode.REQUIRED) + private List activityNodes; - @Schema(description = "审批节点信息") + @Schema(description = "表单字段权限") + private Map formFieldsPermission; + + @Schema(description = "待办任务") + private BpmTaskRespVO todoTask; + + /** + * 所属流程定义信息 + */ + private BpmProcessDefinitionRespVO processDefinition; + + /** + * 所属流程实例信息 + */ + private BpmProcessInstanceRespVO processInstance; + + @Schema(description = "活动节点信息") @Data - public static class ApprovalNodeInfo { + public static class ActivityNode { @Schema(description = "节点编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "StartUserNode") private String id; @@ -39,42 +60,40 @@ public class BpmApprovalDetailRespVO { private LocalDateTime endTime; @Schema(description = "审批节点的任务信息") - private List tasks; + private List tasks; + + @Schema(description = "候选人策略", example = "35") + private Integer candidateStrategy; // 参见 BpmTaskCandidateStrategyEnum 枚举。主要用于发起时,审批节点、抄送节点自选 + + @Schema(description = "候选人用户 ID 列表", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "1818") + @JsonIgnore // 不返回,只是方便后续读取,赋值给 candidateUsers + private List candidateUserIds; @Schema(description = "候选人用户列表") - // TODO @jason:candidateUserList => candidateUsers,保持和 tasks 的命名风格一致哈 - private List candidateUserList; // 用于未运行任务节点 + private List candidateUsers; // 只包含未生成 ApprovalTaskInfo 的用户列表 } - // TODO @jason:可以替换成 UserSimpleBaseVO。简化下 - @Schema(description = "用户信息") + @Schema(description = "活动节点的任务信息") @Data - public static class User { - - @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") - private Long id; - - @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿") - private String nickname; - - @Schema(description = "用户头像", example = "https://www.iocoder.cn/1.png") - private String avatar; - - } - - @Schema(description = "审批任务信息") - @Data - public static class ApprovalTaskInfo { + public static class ActivityNodeTask { @Schema(description = "任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private String id; + @Schema(description = "任务所属人编号", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "1818") + @JsonIgnore // 不返回,只是方便后续读取,赋值给 ownerUser + private Long owner; + @Schema(description = "任务所属人", example = "1024") - private User ownerUser; + private UserSimpleBaseVO ownerUser; + + @Schema(description = "任务分配人编号", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "2048") + @JsonIgnore // 不返回,只是方便后续读取,赋值给 assigneeUser + private Long assignee; @Schema(description = "任务分配人", example = "2048") - private User assigneeUser; + private UserSimpleBaseVO assigneeUser; @Schema(description = "任务状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Integer status; // 参见 BpmTaskStatusEnum 枚举 diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmFormFieldsPermissionReqVO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmFormFieldsPermissionReqVO.java deleted file mode 100644 index c5dc824de..000000000 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmFormFieldsPermissionReqVO.java +++ /dev/null @@ -1,37 +0,0 @@ -package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance; - -import cn.hutool.core.util.StrUtil; -import com.fasterxml.jackson.annotation.JsonIgnore; -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.AssertTrue; -import lombok.Data; - -@Schema(description = "管理后台 - 表单字段权限 Request VO") -@Data -public class BpmFormFieldsPermissionReqVO { - - @Schema(description = "流程定义的编号", example = "1024") - private String processDefinitionId; - - @Schema(description = "流程实例的编号", example = "1024") - private String processInstanceId; - - @Schema(description = "流程活动编号", example = "StartUserNode") - private String activityId; // 对应 BPMN XML 节点 Id - - @Schema(description = "流程任务编号", example = "95f2f08b-621b-11ef-bf39-00ff4722db8b") - private String taskId; // UserTask 对应的Id - - @AssertTrue(message = "流程定义的编号和流程实例的编号不能同时为空") - @JsonIgnore - public boolean isValidProcessParam() { - return StrUtil.isNotEmpty(processDefinitionId) || StrUtil.isNotEmpty(processInstanceId); - } - - @AssertTrue(message = "流程活动编号和流程任务编号编号不能同时为空") - @JsonIgnore - public boolean isValidActivityParam() { - return StrUtil.isNotEmpty(activityId) || StrUtil.isNotEmpty(taskId); - } - -} diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceBpmnModelViewRespVO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceBpmnModelViewRespVO.java new file mode 100644 index 000000000..5f9c0f37d --- /dev/null +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceBpmnModelViewRespVO.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance; + +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO; +import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.BpmTaskRespVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; +import java.util.Set; + +@Schema(description = "管理后台 - 流程示例的 BPMN 视图 Response VO") +@Data +public class BpmProcessInstanceBpmnModelViewRespVO { + + // ========== 基本信息 ========== + + @Schema(description = "流程实例信息", requiredMode = Schema.RequiredMode.REQUIRED) + private BpmProcessInstanceRespVO processInstance; + + @Schema(description = "任务列表", requiredMode = Schema.RequiredMode.REQUIRED) + private List tasks; + + @Schema(description = "BPMN XML", requiredMode = Schema.RequiredMode.REQUIRED) + private String bpmnXml; + + @Schema(description = "SIMPLE 模型") + private BpmSimpleModelNodeVO simpleModel; + + // ========== 进度信息 ========== + + @Schema(description = "进行中的活动节点编号集合", requiredMode = Schema.RequiredMode.REQUIRED) + private Set unfinishedTaskActivityIds; // 只包括 UserTask + + @Schema(description = "已经完成的活动节点编号集合", requiredMode = Schema.RequiredMode.REQUIRED) + private Set finishedTaskActivityIds; // 包括 UserTask、Gateway 等,不包括 SequenceFlow + + @Schema(description = "已经完成的连线节点编号集合", requiredMode = Schema.RequiredMode.REQUIRED) + private Set finishedSequenceFlowActivityIds; // 只包括 SequenceFlow + + @Schema(description = "已经拒绝的活动节点编号集合", requiredMode = Schema.RequiredMode.REQUIRED) + private Set rejectedTaskActivityIds; // 只包括 UserTask + +} diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceRespVO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceRespVO.java index ac6b90c7e..2de0cbc95 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceRespVO.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceRespVO.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance; +import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO; import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionRespVO; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -44,7 +45,7 @@ public class BpmProcessInstanceRespVO { /** * 发起流程的用户 */ - private User startUser; + private UserSimpleBaseVO startUser; @Schema(description = "流程定义的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") private String processDefinitionId; @@ -58,22 +59,6 @@ public class BpmProcessInstanceRespVO { */ private List tasks; // 仅在流程实例分页才返回 - @Schema(description = "用户信息") - @Data - public static class User { - - @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") - private Long id; - @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿") - private String nickname; - - @Schema(description = "部门编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") - private Long deptId; - @Schema(description = "部门名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "研发部") - private String deptName; - - } - @Schema(description = "流程任务") @Data public static class Task { diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskApproveReqVO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskApproveReqVO.java index 0be06a6c8..5bec02660 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskApproveReqVO.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskApproveReqVO.java @@ -4,7 +4,6 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import lombok.Data; -import java.util.Collection; import java.util.Map; @Schema(description = "管理后台 - 通过流程任务的 Request VO") @@ -19,9 +18,6 @@ public class BpmTaskApproveReqVO { @NotEmpty(message = "审批意见不能为空") private String reason; - @Schema(description = "抄送的用户编号数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "1,2") - private Collection copyUserIds; - @Schema(description = "变量实例(动态表单)", requiredMode = Schema.RequiredMode.REQUIRED) private Map variables; diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskCopyReqVO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskCopyReqVO.java new file mode 100644 index 000000000..0889875f0 --- /dev/null +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskCopyReqVO.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +import java.util.Collection; + +@Schema(description = "管理后台 - 抄送流程任务的 Request VO") +@Data +public class BpmTaskCopyReqVO { + + @Schema(description = "任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotEmpty(message = "任务编号不能为空") + private String id; + + @Schema(description = "抄送的用户编号数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1,2]") + @NotEmpty(message = "抄送用户不能为空") + private Collection copyUserIds; + + @Schema(description = "抄送意见", example = "帮忙看看!") + private String reason; +} diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskRespVO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskRespVO.java index ac64fcccd..5b34d36db 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskRespVO.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskRespVO.java @@ -1,6 +1,7 @@ package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task; -import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceRespVO; +import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO; +import com.fasterxml.jackson.annotation.JsonIgnore; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -33,14 +34,21 @@ public class BpmTaskRespVO { @Schema(description = "审批理由", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") private String reason; + @Schema(description = "任务负责人编号", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "2048") + @JsonIgnore // 不返回,只是方便后续读取,赋值给 ownerUser + private Long owner; /** * 负责人的用户信息 */ - private BpmProcessInstanceRespVO.User ownerUser; + private UserSimpleBaseVO ownerUser; + + @Schema(description = "任务分配人编号", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "2048") + @JsonIgnore // 不返回,只是方便后续读取,赋值给 assigneeUser + private Long assignee; /** * 审核的用户信息 */ - private BpmProcessInstanceRespVO.User assigneeUser; + private UserSimpleBaseVO assigneeUser; @Schema(description = "任务定义的标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "Activity_one") private String taskDefinitionKey; @@ -55,7 +63,7 @@ public class BpmTaskRespVO { @Schema(description = "父任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private String parentTaskId; @Schema(description = "子任务列表(由加签生成)", requiredMode = Schema.RequiredMode.REQUIRED, example = "childrenTask") - private List children; + private List children; // 由加签生成,包含多层子任务 @Schema(description = "表单编号", example = "1024") private Long formId; @@ -67,9 +75,6 @@ public class BpmTaskRespVO { private List formFields; @Schema(description = "提交的表单值", requiredMode = Schema.RequiredMode.REQUIRED) private Map formVariables; - // @芋艿 都改成了 fieldsPermission。 buttonsSetting。和 BpmSimpleModelNodeVO 统一 - @Schema(description = "表单字段权限值") - private Map fieldsPermission; @Schema(description = "操作按钮设置值") private Map buttonsSetting; @@ -92,7 +97,7 @@ public class BpmTaskRespVO { /** * 发起人的用户信息 */ - private BpmProcessInstanceRespVO.User startUser; + private UserSimpleBaseVO startUser; } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskReturnReqVO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskReturnReqVO.java index c974a7169..ceea3e427 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskReturnReqVO.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskReturnReqVO.java @@ -4,7 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import lombok.Data; -@Schema(description = "管理后台 - 回退流程任务的 Request VO") +@Schema(description = "管理后台 - 退回流程任务的 Request VO") @Data public class BpmTaskReturnReqVO { @@ -12,12 +12,12 @@ public class BpmTaskReturnReqVO { @NotEmpty(message = "任务编号不能为空") private String id; - @Schema(description = "回退到的任务 Key", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") - @NotEmpty(message = "回退到的任务 Key 不能为空") + @Schema(description = "退回到的任务 Key", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotEmpty(message = "退回到的任务 Key 不能为空") private String targetTaskDefinitionKey; - @Schema(description = "回退意见", requiredMode = Schema.RequiredMode.REQUIRED, example = "我就是想驳回") - @NotEmpty(message = "回退意见不能为空") + @Schema(description = "退回意见", requiredMode = Schema.RequiredMode.REQUIRED, example = "我就是想驳回") + @NotEmpty(message = "退回意见不能为空") private String reason; } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/definition/BpmModelConvert.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/definition/BpmModelConvert.java index 5d5ced5d3..64701ef1f 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/definition/BpmModelConvert.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/definition/BpmModelConvert.java @@ -1,7 +1,6 @@ package cn.iocoder.yudao.module.bpm.convert.definition; import cn.hutool.core.util.ArrayUtil; -import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.date.DateUtils; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; @@ -22,6 +21,7 @@ import org.mapstruct.Mapper; import org.mapstruct.factory.Mappers; import java.util.Collections; +import java.util.Comparator; import java.util.List; import java.util.Map; @@ -37,25 +37,28 @@ public interface BpmModelConvert { BpmModelConvert INSTANCE = Mappers.getMapper(BpmModelConvert.class); - default PageResult buildModelPage(PageResult pageResult, - Map formMap, - Map categoryMap, Map deploymentMap, - Map processDefinitionMap, - Map userMap) { - List list = convertList(pageResult.getList(), model -> { + default List buildModelList(List list, + Map formMap, + Map categoryMap, + Map deploymentMap, + Map processDefinitionMap, + Map userMap) { + List result = convertList(list, model -> { BpmModelMetaInfoVO metaInfo = parseMetaInfo(model); BpmFormDO form = metaInfo != null ? formMap.get(metaInfo.getFormId()) : null; BpmCategoryDO category = categoryMap.get(model.getCategory()); Deployment deployment = model.getDeploymentId() != null ? deploymentMap.get(model.getDeploymentId()) : null; - ProcessDefinition processDefinition = model.getDeploymentId() != null ? processDefinitionMap.get(model.getDeploymentId()) : null; + ProcessDefinition processDefinition = model.getDeploymentId() != null ? + processDefinitionMap.get(model.getDeploymentId()) : null; List startUsers = metaInfo != null ? convertList(metaInfo.getStartUserIds(), userMap::get) : null; return buildModel0(model, metaInfo, form, category, deployment, processDefinition, startUsers); }); - return new PageResult<>(list, pageResult.getTotal()); + // 排序 + result.sort(Comparator.comparing(BpmModelMetaInfoVO::getSort)); + return result; } - default BpmModelRespVO buildModel(Model model, - byte[] bpmnBytes) { + default BpmModelRespVO buildModel(Model model, byte[] bpmnBytes) { BpmModelMetaInfoVO metaInfo = parseMetaInfo(model); BpmModelRespVO modelVO = buildModel0(model, metaInfo, null, null, null, null, null); if (ArrayUtil.isNotEmpty(bpmnBytes)) { @@ -112,6 +115,10 @@ public interface BpmModelConvert { if (vo.getStartUserIds() == null) { vo.setStartUserIds(Collections.emptyList()); } + // 如果为空,兜底处理,使用 createTime 创建时间 + if (vo.getSort() == null) { + vo.setSort(model.getCreateTime().getTime()); + } return vo; } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/definition/BpmProcessDefinitionConvert.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/definition/BpmProcessDefinitionConvert.java index 0e767d787..1ef8b6f05 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/definition/BpmProcessDefinitionConvert.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/definition/BpmProcessDefinitionConvert.java @@ -11,7 +11,6 @@ import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO; import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO; import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils; import org.flowable.bpmn.model.BpmnModel; -import org.flowable.bpmn.model.UserTask; import org.flowable.common.engine.impl.db.SuspensionState; import org.flowable.engine.repository.Deployment; import org.flowable.engine.repository.ProcessDefinition; @@ -20,6 +19,7 @@ import org.mapstruct.Mapping; import org.mapstruct.MappingTarget; import org.mapstruct.factory.Mappers; +import java.util.Comparator; import java.util.List; import java.util.Map; @@ -47,7 +47,7 @@ public interface BpmProcessDefinitionConvert { Map processDefinitionInfoMap, Map formMap, Map categoryMap) { - return CollectionUtils.convertList(list, definition -> { + List result = CollectionUtils.convertList(list, definition -> { Deployment deployment = MapUtil.get(deploymentMap, definition.getDeploymentId(), Deployment.class); BpmProcessDefinitionInfoDO processDefinitionInfo = MapUtil.get(processDefinitionInfoMap, definition.getId(), BpmProcessDefinitionInfoDO.class); BpmFormDO form = null; @@ -55,8 +55,11 @@ public interface BpmProcessDefinitionConvert { form = MapUtil.get(formMap, processDefinitionInfo.getFormId(), BpmFormDO.class); } BpmCategoryDO category = MapUtil.get(categoryMap, definition.getCategory(), BpmCategoryDO.class); - return buildProcessDefinition(definition, deployment, processDefinitionInfo, form, category, null, null); + return buildProcessDefinition(definition, deployment, processDefinitionInfo, form, category, null); }); + // 排序 + result.sort(Comparator.comparing(BpmProcessDefinitionRespVO::getSort)); + return result; } default BpmProcessDefinitionRespVO buildProcessDefinition(ProcessDefinition definition, @@ -64,8 +67,7 @@ public interface BpmProcessDefinitionConvert { BpmProcessDefinitionInfoDO processDefinitionInfo, BpmFormDO form, BpmCategoryDO category, - BpmnModel bpmnModel, - List startUserSelectUserTaskList) { + BpmnModel bpmnModel) { BpmProcessDefinitionRespVO respVO = BeanUtils.toBean(definition, BpmProcessDefinitionRespVO.class); respVO.setSuspensionState(definition.isSuspended() ? SuspensionState.SUSPENDED.getStateCode() : SuspensionState.ACTIVE.getStateCode()); // Deployment @@ -87,7 +89,6 @@ public interface BpmProcessDefinitionConvert { // BpmnModel if (bpmnModel != null) { respVO.setBpmnXml(BpmnModelUtils.getBpmnXml(bpmnModel)); - respVO.setStartUserSelectTasks(BeanUtils.toBean(startUserSelectUserTaskList, BpmProcessDefinitionRespVO.UserTask.class)); } return respVO; } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmActivityConvert.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmActivityConvert.java deleted file mode 100644 index 3cb674c1c..000000000 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmActivityConvert.java +++ /dev/null @@ -1,30 +0,0 @@ -package cn.iocoder.yudao.module.bpm.convert.task; - -import cn.iocoder.yudao.framework.common.util.date.DateUtils; -import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.activity.BpmActivityRespVO; -import org.flowable.engine.history.HistoricActivityInstance; -import org.mapstruct.Mapper; -import org.mapstruct.Mapping; -import org.mapstruct.Mappings; -import org.mapstruct.factory.Mappers; - -import java.util.List; - -/** - * BPM 活动 Convert - * - * @author 芋道源码 - */ -@Mapper(uses = DateUtils.class) -public interface BpmActivityConvert { - - BpmActivityConvert INSTANCE = Mappers.getMapper(BpmActivityConvert.class); - - List convertList(List list); - - @Mappings({ - @Mapping(source = "activityId", target = "key"), - @Mapping(source = "activityType", target = "type") - }) - BpmActivityRespVO convert(HistoricActivityInstance bean); -} diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmProcessInstanceConvert.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmProcessInstanceConvert.java index b797c612b..450699c2f 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmProcessInstanceConvert.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmProcessInstanceConvert.java @@ -1,30 +1,47 @@ package cn.iocoder.yudao.module.bpm.convert.task; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.framework.common.util.collection.SetUtils; import cn.iocoder.yudao.framework.common.util.number.NumberUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO; import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionRespVO; +import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmApprovalDetailRespVO; +import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceBpmnModelViewRespVO; import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceRespVO; +import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.BpmTaskRespVO; +import cn.iocoder.yudao.module.bpm.convert.definition.BpmProcessDefinitionConvert; import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO; import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO; import cn.iocoder.yudao.module.bpm.event.BpmProcessInstanceStatusEvent; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils; import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils; import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenProcessInstanceApproveReqDTO; import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenProcessInstanceRejectReqDTO; import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import org.flowable.bpmn.model.BpmnModel; import org.flowable.engine.history.HistoricProcessInstance; import org.flowable.engine.repository.ProcessDefinition; import org.flowable.engine.runtime.ProcessInstance; import org.flowable.task.api.Task; +import org.flowable.task.api.history.HistoricTaskInstance; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.MappingTarget; import org.mapstruct.factory.Mappers; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; /** * 流程实例 Convert @@ -55,7 +72,7 @@ public interface BpmProcessInstanceConvert { if (userMap != null) { AdminUserRespDTO startUser = userMap.get(NumberUtils.parseLong(pageResult.getList().get(i).getStartUserId())); if (startUser != null) { - respVO.setStartUser(BeanUtils.toBean(startUser, BpmProcessInstanceRespVO.User.class)); + respVO.setStartUser(BeanUtils.toBean(startUser, UserSimpleBaseVO.class)); MapUtils.findAndThen(deptMap, startUser.getDeptId(), dept -> respVO.getStartUser().setDeptName(dept.getName())); } } @@ -65,20 +82,18 @@ public interface BpmProcessInstanceConvert { default BpmProcessInstanceRespVO buildProcessInstance(HistoricProcessInstance processInstance, ProcessDefinition processDefinition, - BpmProcessDefinitionInfoDO processDefinitionExt, - String bpmnXml, + BpmProcessDefinitionInfoDO processDefinitionInfo, AdminUserRespDTO startUser, DeptRespDTO dept) { BpmProcessInstanceRespVO respVO = BeanUtils.toBean(processInstance, BpmProcessInstanceRespVO.class); - respVO.setStatus(FlowableUtils.getProcessInstanceStatus(processInstance)); - respVO.setFormVariables(FlowableUtils.getProcessInstanceFormVariable(processInstance)); + respVO.setStatus(FlowableUtils.getProcessInstanceStatus(processInstance)) + .setFormVariables(FlowableUtils.getProcessInstanceFormVariable(processInstance)); // definition respVO.setProcessDefinition(BeanUtils.toBean(processDefinition, BpmProcessDefinitionRespVO.class)); - copyTo(processDefinitionExt, respVO.getProcessDefinition()); - respVO.getProcessDefinition().setBpmnXml(bpmnXml); + copyTo(processDefinitionInfo, respVO.getProcessDefinition()); // user if (startUser != null) { - respVO.setStartUser(BeanUtils.toBean(startUser, BpmProcessInstanceRespVO.User.class)); + respVO.setStartUser(BeanUtils.toBean(startUser, UserSimpleBaseVO.class)); if (dept != null) { respVO.getStartUser().setDeptName(dept.getName()); } @@ -89,7 +104,7 @@ public interface BpmProcessInstanceConvert { @Mapping(source = "from.id", target = "to.id", ignore = true) void copyTo(BpmProcessDefinitionInfoDO from, @MappingTarget BpmProcessDefinitionRespVO to); - default BpmProcessInstanceStatusEvent buildProcessInstanceStatusEvent(Object source, ProcessInstance instance, Integer status) {; + default BpmProcessInstanceStatusEvent buildProcessInstanceStatusEvent(Object source, ProcessInstance instance, Integer status) { return new BpmProcessInstanceStatusEvent(source).setId(instance.getId()).setStatus(status) .setProcessDefinitionKey(instance.getProcessDefinitionKey()).setBusinessKey(instance.getBusinessKey()); } @@ -109,4 +124,156 @@ public interface BpmProcessInstanceConvert { .setStartUserId(NumberUtils.parseLong(instance.getStartUserId())); } + default BpmProcessInstanceBpmnModelViewRespVO buildProcessInstanceBpmnModelView(HistoricProcessInstance processInstance, + List taskInstances, + BpmnModel bpmnModel, + BpmSimpleModelNodeVO simpleModel, + Set unfinishedTaskActivityIds, + Set finishedTaskActivityIds, + Set finishedSequenceFlowActivityIds, + Set rejectTaskActivityIds, + Map userMap, + Map deptMap) { + BpmProcessInstanceBpmnModelViewRespVO respVO = new BpmProcessInstanceBpmnModelViewRespVO(); + // 基本信息 + respVO.setProcessInstance(BeanUtils.toBean(processInstance, BpmProcessInstanceRespVO.class, o -> o + .setStatus(FlowableUtils.getProcessInstanceStatus(processInstance))) + .setStartUser(buildUser(processInstance.getStartUserId(), userMap, deptMap))); + respVO.setTasks(convertList(taskInstances, task -> BeanUtils.toBean(task, BpmTaskRespVO.class) + .setStatus(FlowableUtils.getTaskStatus(task)).setReason(FlowableUtils.getTaskReason(task)) + .setAssigneeUser(buildUser(task.getAssignee(), userMap, deptMap)) + .setOwnerUser(buildUser(task.getOwner(), userMap, deptMap)))); + respVO.setBpmnXml(BpmnModelUtils.getBpmnXml(bpmnModel)); + respVO.setSimpleModel(simpleModel); + // 进度信息 + respVO.setUnfinishedTaskActivityIds(unfinishedTaskActivityIds) + .setFinishedTaskActivityIds(finishedTaskActivityIds) + .setFinishedSequenceFlowActivityIds(finishedSequenceFlowActivityIds) + .setRejectedTaskActivityIds(rejectTaskActivityIds); + return respVO; + } + + default UserSimpleBaseVO buildUser(String userIdStr, + Map userMap, + Map deptMap) { + if (StrUtil.isEmpty(userIdStr)) { + return null; + } + Long userId = NumberUtils.parseLong(userIdStr); + return buildUser(userId, userMap, deptMap); + } + + default UserSimpleBaseVO buildUser(Long userId, + Map userMap, + Map deptMap) { + if (userId == null) { + return null; + } + AdminUserRespDTO user = userMap.get(userId); + if (user == null) { + return null; + } + UserSimpleBaseVO userVO = BeanUtils.toBean(user, UserSimpleBaseVO.class); + DeptRespDTO dept = user.getDeptId() != null ? deptMap.get(user.getDeptId()) : null; + if (dept != null) { + userVO.setDeptName(dept.getName()); + } + return userVO; + } + + default BpmApprovalDetailRespVO.ActivityNodeTask buildApprovalTaskInfo(HistoricTaskInstance task) { + if (task == null) { + return null; + } + return BeanUtils.toBean(task, BpmApprovalDetailRespVO.ActivityNodeTask.class) + .setStatus(FlowableUtils.getTaskStatus(task)).setReason(FlowableUtils.getTaskReason(task)); + } + + default Set parseUserIds(HistoricProcessInstance processInstance, + List activityNodes, + BpmTaskRespVO todoTask) { + Set userIds = new HashSet<>(); + if (processInstance != null) { + userIds.add(NumberUtils.parseLong(processInstance.getStartUserId())); + } + for (BpmApprovalDetailRespVO.ActivityNode activityNode : activityNodes) { + CollUtil.addAll(userIds, convertSet(activityNode.getTasks(), BpmApprovalDetailRespVO.ActivityNodeTask::getAssignee)); + CollUtil.addAll(userIds, convertSet(activityNode.getTasks(), BpmApprovalDetailRespVO.ActivityNodeTask::getOwner)); + CollUtil.addAll(userIds, activityNode.getCandidateUserIds()); + } + if (todoTask != null) { + CollUtil.addIfAbsent(userIds, todoTask.getAssignee()); + CollUtil.addIfAbsent(userIds, todoTask.getOwner()); + if (CollUtil.isNotEmpty(todoTask.getChildren())) { + CollUtil.addAll(userIds, convertSet(todoTask.getChildren(), BpmTaskRespVO::getAssignee)); + CollUtil.addAll(userIds, convertSet(todoTask.getChildren(), BpmTaskRespVO::getOwner)); + } + } + return userIds; + } + + default Set parseUserIds02(HistoricProcessInstance processInstance, + List tasks) { + Set userIds = SetUtils.asSet(Long.valueOf(processInstance.getStartUserId())); + tasks.forEach(task -> { + CollUtil.addIfAbsent(userIds, NumberUtils.parseLong((task.getAssignee()))); + CollUtil.addIfAbsent(userIds, NumberUtils.parseLong((task.getOwner()))); + }); + return userIds; + } + + default BpmApprovalDetailRespVO buildApprovalDetail(BpmnModel bpmnModel, + ProcessDefinition processDefinition, + BpmProcessDefinitionInfoDO processDefinitionInfo, + HistoricProcessInstance processInstance, + Integer processInstanceStatus, + List activityNodes, + BpmTaskRespVO todoTask, + Map formFieldsPermission, + Map userMap, + Map deptMap) { + // 1.1 流程实例 + BpmProcessInstanceRespVO processInstanceResp = null; + if (processInstance != null) { + AdminUserRespDTO startUser = userMap.get(NumberUtils.parseLong(processInstance.getStartUserId())); + DeptRespDTO dept = startUser != null ? deptMap.get(startUser.getDeptId()) : null; + processInstanceResp = buildProcessInstance(processInstance, null, null, startUser, dept); + } + + // 1.2 流程定义 + BpmProcessDefinitionRespVO definitionResp = BpmProcessDefinitionConvert.INSTANCE.buildProcessDefinition( + processDefinition, null, processDefinitionInfo, null, null, bpmnModel); + + // 1.3 流程节点 + activityNodes.forEach(approveNode -> { + if (approveNode.getTasks() != null) { + approveNode.getTasks().forEach(task -> { + task.setAssigneeUser(buildUser(task.getAssignee(), userMap, deptMap)); + task.setOwnerUser(buildUser(task.getOwner(), userMap, deptMap)); + }); + } + approveNode.setCandidateUsers(convertList(approveNode.getCandidateUserIds(), userId -> buildUser(userId, userMap, deptMap))); + }); + + // 1.4 待办任务 + if (todoTask != null) { + todoTask.setAssigneeUser(buildUser(todoTask.getAssignee(), userMap, deptMap)); + todoTask.setOwnerUser(buildUser(todoTask.getOwner(), userMap, deptMap)); + if (CollUtil.isNotEmpty(todoTask.getChildren())) { + todoTask.getChildren().forEach(childTask -> { + childTask.setAssigneeUser(buildUser(childTask.getAssignee(), userMap, deptMap)); + childTask.setOwnerUser(buildUser(childTask.getOwner(), userMap, deptMap)); + }); + } + } + + // 2. 拼接起来 + return new BpmApprovalDetailRespVO().setStatus(processInstanceStatus) + .setProcessDefinition(definitionResp) + .setProcessInstance(processInstanceResp) + .setFormFieldsPermission(formFieldsPermission) + .setTodoTask(todoTask) + .setActivityNodes(activityNodes); + } + } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmTaskConvert.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmTaskConvert.java index 4131dc502..b44c91951 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmTaskConvert.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmTaskConvert.java @@ -1,21 +1,19 @@ package cn.iocoder.yudao.module.bpm.convert.task; +import cn.hutool.core.collection.CollUtil; import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; import cn.iocoder.yudao.framework.common.util.number.NumberUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceRespVO; +import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO; import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.BpmTaskRespVO; import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO; import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum; -import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils; import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils; import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenTaskCreatedReqDTO; import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; -import org.flowable.bpmn.model.BpmnModel; import org.flowable.engine.history.HistoricProcessInstance; import org.flowable.engine.runtime.ProcessInstance; import org.flowable.task.api.Task; @@ -28,7 +26,7 @@ import java.util.Date; import java.util.List; import java.util.Map; -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen; /** @@ -51,7 +49,7 @@ public interface BpmTaskConvert { } taskVO.setProcessInstance(BeanUtils.toBean(processInstance, BpmTaskRespVO.ProcessInstance.class)); AdminUserRespDTO startUser = userMap.get(NumberUtils.parseLong(processInstance.getStartUserId())); - taskVO.getProcessInstance().setStartUser(BeanUtils.toBean(startUser, BpmProcessInstanceRespVO.User.class)); + taskVO.getProcessInstance().setStartUser(BeanUtils.toBean(startUser, UserSimpleBaseVO.class)); }); } @@ -65,7 +63,7 @@ public interface BpmTaskConvert { // 用户信息 AdminUserRespDTO assignUser = userMap.get(NumberUtils.parseLong(task.getAssignee())); if (assignUser != null) { - taskVO.setAssigneeUser(BeanUtils.toBean(assignUser, BpmProcessInstanceRespVO.User.class)); + taskVO.setAssigneeUser(BeanUtils.toBean(assignUser, UserSimpleBaseVO.class)); findAndThen(deptMap, assignUser.getDeptId(), dept -> taskVO.getAssigneeUser().setDeptName(dept.getName())); } // 流程实例 @@ -73,7 +71,7 @@ public interface BpmTaskConvert { if (processInstance != null) { AdminUserRespDTO startUser = userMap.get(NumberUtils.parseLong(processInstance.getStartUserId())); taskVO.setProcessInstance(BeanUtils.toBean(processInstance, BpmTaskRespVO.ProcessInstance.class)); - taskVO.getProcessInstance().setStartUser(BeanUtils.toBean(startUser, BpmProcessInstanceRespVO.User.class)); + taskVO.getProcessInstance().setStartUser(BeanUtils.toBean(startUser, UserSimpleBaseVO.class)); } return taskVO; }); @@ -81,19 +79,17 @@ public interface BpmTaskConvert { } default List buildTaskListByProcessInstanceId(List taskList, - HistoricProcessInstance processInstance, Map formMap, Map userMap, - Map deptMap, - BpmnModel bpmnModel) { - List taskVOList = CollectionUtils.convertList(taskList, task -> { + Map deptMap) { + return CollectionUtils.convertList(taskList, task -> { + // 特殊:已取消的任务,不返回 BpmTaskRespVO taskVO = BeanUtils.toBean(task, BpmTaskRespVO.class); Integer taskStatus = FlowableUtils.getTaskStatus(task); + if (BpmTaskStatusEnum.isCancelStatus(taskStatus)) { + return null; + } taskVO.setStatus(taskStatus).setReason(FlowableUtils.getTaskReason(task)); - // 流程实例 - AdminUserRespDTO startUser = userMap.get(NumberUtils.parseLong(processInstance.getStartUserId())); - taskVO.setProcessInstance(BeanUtils.toBean(processInstance, BpmTaskRespVO.ProcessInstance.class)); - taskVO.getProcessInstance().setStartUser(BeanUtils.toBean(startUser, BpmProcessInstanceRespVO.User.class)); // 表单信息 BpmFormDO form = MapUtil.get(formMap, NumberUtils.parseLong(task.getFormKey()), BpmFormDO.class); if (form != null) { @@ -101,35 +97,10 @@ public interface BpmTaskConvert { .setFormFields(form.getFields()).setFormVariables(FlowableUtils.getTaskFormVariable(task)); } // 用户信息 - AdminUserRespDTO assignUser = userMap.get(NumberUtils.parseLong(task.getAssignee())); - if (assignUser != null) { - taskVO.setAssigneeUser(BeanUtils.toBean(assignUser, BpmProcessInstanceRespVO.User.class)); - findAndThen(deptMap, assignUser.getDeptId(), dept -> taskVO.getAssigneeUser().setDeptName(dept.getName())); - } - AdminUserRespDTO ownerUser = userMap.get(NumberUtils.parseLong(task.getOwner())); - if (ownerUser != null) { - taskVO.setOwnerUser(BeanUtils.toBean(ownerUser, BpmProcessInstanceRespVO.User.class)); - findAndThen(deptMap, ownerUser.getDeptId(), dept -> taskVO.getOwnerUser().setDeptName(dept.getName())); - } - if (BpmTaskStatusEnum.RUNNING.getStatus().equals(taskStatus)){ - // 设置表单权限 TODO @芋艿 是不是还要加一个全局的权限 基于 processInstance 的权限;回复:可能不需要,但是发起人,需要有个权限配置 - // TODO @jason:貌似这么返回,主要解决当前审批 task 的表单权限,但是不同抄送人的表单权限,可能不太对。例如说,对 A 抄送人是隐藏某个字段。 - // @芋艿 表单权限需要分离开。单独的接口来获取了 BpmProcessInstanceService.getProcessInstanceFormFieldsPermission - taskVO.setFieldsPermission(BpmnModelUtils.parseFormFieldsPermission(bpmnModel, task.getTaskDefinitionKey())); - // 操作按钮设置 - taskVO.setButtonsSetting(BpmnModelUtils.parseButtonsSetting(bpmnModel, task.getTaskDefinitionKey())); - } - return taskVO; + buildTaskAssignee(taskVO, task.getAssignee(), userMap, deptMap); + buildTaskOwner(taskVO, task.getOwner(), userMap, deptMap); + return taskVO; }); - - // 拼接父子关系 - Map> childrenTaskMap = convertMultiMap( - filterList(taskVOList, r -> StrUtil.isNotEmpty(r.getParentTaskId())), - BpmTaskRespVO::getParentTaskId); - for (BpmTaskRespVO taskVO : taskVOList) { - taskVO.setChildren(childrenTaskMap.get(taskVO.getId())); - } - return filterList(taskVOList, r -> StrUtil.isEmpty(r.getParentTaskId())); } default List buildTaskListByParentTaskId(List taskList, @@ -138,7 +109,7 @@ public interface BpmTaskConvert { return convertList(taskList, task -> BeanUtils.toBean(task, BpmTaskRespVO.class, taskVO -> { AdminUserRespDTO assignUser = userMap.get(NumberUtils.parseLong(task.getAssignee())); if (assignUser != null) { - taskVO.setAssigneeUser(BeanUtils.toBean(assignUser, BpmProcessInstanceRespVO.User.class)); + taskVO.setAssigneeUser(BeanUtils.toBean(assignUser, UserSimpleBaseVO.class)); DeptRespDTO dept = deptMap.get(assignUser.getDeptId()); if (dept != null) { taskVO.getAssigneeUser().setDeptName(dept.getName()); @@ -146,12 +117,21 @@ public interface BpmTaskConvert { } AdminUserRespDTO ownerUser = userMap.get(NumberUtils.parseLong(task.getOwner())); if (ownerUser != null) { - taskVO.setOwnerUser(BeanUtils.toBean(ownerUser, BpmProcessInstanceRespVO.User.class)); + taskVO.setOwnerUser(BeanUtils.toBean(ownerUser, UserSimpleBaseVO.class)); findAndThen(deptMap, ownerUser.getDeptId(), dept -> taskVO.getOwnerUser().setDeptName(dept.getName())); } })); } + default BpmTaskRespVO buildTodoTask(Task todoTask, List childrenTasks, + Map buttonsSetting) { + return BeanUtils.toBean(todoTask, BpmTaskRespVO.class) + .setStatus(FlowableUtils.getTaskStatus(todoTask)).setReason(FlowableUtils.getTaskReason(todoTask)) + .setButtonsSetting(buttonsSetting) + .setChildren(convertList(childrenTasks, childTask -> BeanUtils.toBean(childTask, BpmTaskRespVO.class) + .setStatus(FlowableUtils.getTaskStatus(childTask)))); + } + default BpmMessageSendWhenTaskCreatedReqDTO convert(ProcessInstance processInstance, AdminUserRespDTO startUser, Task task) { BpmMessageSendWhenTaskCreatedReqDTO reqDTO = new BpmMessageSendWhenTaskCreatedReqDTO(); @@ -162,6 +142,42 @@ public interface BpmTaskConvert { return reqDTO; } + default void buildTaskOwner(BpmTaskRespVO task, String taskOwner, + Map userMap, + Map deptMap) { + AdminUserRespDTO ownerUser = userMap.get(NumberUtils.parseLong(taskOwner)); + if (ownerUser != null) { + task.setOwnerUser(BeanUtils.toBean(ownerUser, UserSimpleBaseVO.class)); + findAndThen(deptMap, ownerUser.getDeptId(), dept -> task.getOwnerUser().setDeptName(dept.getName())); + } + } + + default void buildTaskChildren(BpmTaskRespVO task, Map> childrenTaskMap, + Map userMap, Map deptMap) { + List childTasks = childrenTaskMap.get(task.getId()); + if (CollUtil.isNotEmpty(childTasks)) { + task.setChildren( + convertList(childTasks, childTask -> { + BpmTaskRespVO childTaskVO = BeanUtils.toBean(childTask, BpmTaskRespVO.class); + childTaskVO.setStatus(FlowableUtils.getTaskStatus(childTask)); + buildTaskOwner(childTaskVO, childTask.getOwner(), userMap, deptMap); + buildTaskAssignee(childTaskVO, childTask.getAssignee(), userMap, deptMap); + return childTaskVO; + }) + ); + } + } + + default void buildTaskAssignee(BpmTaskRespVO task, String taskAssignee, + Map userMap, + Map deptMap) { + AdminUserRespDTO assignUser = userMap.get(NumberUtils.parseLong(taskAssignee)); + if (assignUser != null) { + task.setAssigneeUser(BeanUtils.toBean(assignUser, UserSimpleBaseVO.class)); + findAndThen(deptMap, assignUser.getDeptId(), dept -> task.getAssigneeUser().setDeptName(dept.getName())); + } + } + /** * 将父任务的属性,拷贝到子任务(加签任务) *

diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessDefinitionInfoDO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessDefinitionInfoDO.java index 35864f4a6..7248b31cd 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessDefinitionInfoDO.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessDefinitionInfoDO.java @@ -1,6 +1,7 @@ package cn.iocoder.yudao.module.bpm.dal.dataobject.definition; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler; import cn.iocoder.yudao.framework.mybatis.core.type.StringListTypeHandler; import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum; import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum; @@ -122,6 +123,10 @@ public class BpmProcessDefinitionInfoDO extends BaseDO { * 目的:如果 false 不可见,则不展示在“发起流程”的列表里 */ private Boolean visible; + /** + * 排序值 + */ + private Long sort; /** * 可发起用户编号数组 @@ -134,7 +139,7 @@ public class BpmProcessDefinitionInfoDO extends BaseDO { * 1. {@link #visible} 只是决定是否可见。即使不可见,还是可以发起 * 2. startUserIds 决定某个用户是否可以发起。如果该用户不可发起,则他也是不可见的 */ - @TableField(typeHandler = StringListTypeHandler.class) // 为了可以使用 find_in_set 进行过滤 + @TableField(typeHandler = LongListTypeHandler.class) // 为了可以使用 find_in_set 进行过滤 private List startUserIds; /** diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/task/BpmProcessInstanceCopyDO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/task/BpmProcessInstanceCopyDO.java index 4150612ef..50a7cbb9f 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/task/BpmProcessInstanceCopyDO.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/task/BpmProcessInstanceCopyDO.java @@ -8,6 +8,8 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import org.flowable.bpmn.model.FlowNode; +import org.flowable.task.api.history.HistoricTaskInstance; /** * 流程抄送 DO @@ -54,23 +56,25 @@ public class BpmProcessInstanceCopyDO extends BaseDO { */ private String category; /** - * 流程活动编号 + * 流程活动的编号 *

- * 对应 BPMN XML 节点编号,用于查询抄送节点的表单字段权限 - * 这里冗余的原因:如果是钉钉易搭的抄送节点 (ServiceTask),使用 taskId 可能查不到对应的 activityId + * + * 冗余 {@link FlowNode#getId()},对应 BPMN XML 节点编号 + * 原因:用于查询抄送节点的表单字段权限。因为仿钉钉/飞书的抄送节点 (ServiceTask),没有 taskId,只有 activityId */ private String activityId; /** - * 任务主键 - * 关联 Task 的 id 属性 + * 流程活动的名字 + * + * 冗余 {@link FlowNode#getName()} + */ + private String activityName; + /** + * 流程活动的编号 + * + * 关联 {@link HistoricTaskInstance#getId()} */ private String taskId; - /** - * 任务名称 - * - * 冗余 Task 的 name 属性 - */ - private String taskName; /** * 用户编号(被抄送的用户编号) @@ -79,4 +83,9 @@ public class BpmProcessInstanceCopyDO extends BaseDO { */ private Long userId; + /** + * 抄送意见 + */ + private String reason; + } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/definition/BpmProcessDefinitionInfoMapper.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/definition/BpmProcessDefinitionInfoMapper.java index b868fdce6..d34081e25 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/definition/BpmProcessDefinitionInfoMapper.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/definition/BpmProcessDefinitionInfoMapper.java @@ -1,6 +1,7 @@ package cn.iocoder.yudao.module.bpm.dal.mysql.definition; import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO; import org.apache.ibatis.annotations.Mapper; @@ -18,4 +19,9 @@ public interface BpmProcessDefinitionInfoMapper extends BaseMapperX().eq(BpmProcessDefinitionInfoDO::getModelId, modelId)); + } + } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmParallelMultiInstanceBehavior.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmParallelMultiInstanceBehavior.java index e389c27f3..f4e4ccfe1 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmParallelMultiInstanceBehavior.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmParallelMultiInstanceBehavior.java @@ -53,7 +53,7 @@ public class BpmParallelMultiInstanceBehavior extends ParallelMultiInstanceBehav @SuppressWarnings("unchecked") Set assigneeUserIds = (Set) execution.getVariable(super.collectionVariable, Set.class); if (assigneeUserIds == null) { - assigneeUserIds = taskCandidateInvoker.calculateUsers(execution); + assigneeUserIds = taskCandidateInvoker.calculateUsersByTask(execution); execution.setVariable(super.collectionVariable, assigneeUserIds); if (CollUtil.isEmpty(assigneeUserIds)) { // 特殊:如果没有处理人的情况下,至少有一个 null 空元素,避免自动通过! diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmSequentialMultiInstanceBehavior.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmSequentialMultiInstanceBehavior.java index 8e9acdd15..c433f5911 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmSequentialMultiInstanceBehavior.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmSequentialMultiInstanceBehavior.java @@ -46,7 +46,7 @@ public class BpmSequentialMultiInstanceBehavior extends SequentialMultiInstanceB @SuppressWarnings("unchecked") Set assigneeUserIds = (Set) execution.getVariable(super.collectionVariable, Set.class); if (assigneeUserIds == null) { - assigneeUserIds = taskCandidateInvoker.calculateUsers(execution); + assigneeUserIds = taskCandidateInvoker.calculateUsersByTask(execution); execution.setVariable(super.collectionVariable, assigneeUserIds); if (CollUtil.isEmpty(assigneeUserIds)) { // 特殊:如果没有处理人的情况下,至少有一个 null 空元素,避免自动通过! diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmUserTaskActivityBehavior.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmUserTaskActivityBehavior.java index 592e02bfb..cba5187b3 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmUserTaskActivityBehavior.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmUserTaskActivityBehavior.java @@ -57,7 +57,7 @@ public class BpmUserTaskActivityBehavior extends UserTaskActivityBehavior { // 情况二,如果非多实例的任务,则计算任务处理人 // 第一步,先计算可处理该任务的处理人们 - Set candidateUserIds = taskCandidateInvoker.calculateUsers(execution); + Set candidateUserIds = taskCandidateInvoker.calculateUsersByTask(execution); if (CollUtil.isEmpty(candidateUserIds)) { return null; } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateInvoker.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateInvoker.java index 5ac0a00e6..30e675bf0 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateInvoker.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateInvoker.java @@ -18,6 +18,7 @@ import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; import com.google.common.annotations.VisibleForTesting; import lombok.extern.slf4j.Slf4j; import org.flowable.bpmn.model.BpmnModel; +import org.flowable.bpmn.model.FlowElement; import org.flowable.bpmn.model.UserTask; import org.flowable.engine.delegate.DelegateExecution; import org.flowable.engine.runtime.ProcessInstance; @@ -89,32 +90,66 @@ public class BpmTaskCandidateInvoker { * @return 用户编号集合 */ @DataPermission(enable = false) // 忽略数据权限,避免因为过滤,导致找不到候选人 - public Set calculateUsers(DelegateExecution execution) { + public Set calculateUsersByTask(DelegateExecution execution) { // 审批类型非人工审核时,不进行计算候选人。原因是:后续会自动通过、不通过 - Integer approveType = BpmnModelUtils.parseApproveType(execution.getCurrentFlowElement()); + FlowElement flowElement = execution.getCurrentFlowElement(); + Integer approveType = BpmnModelUtils.parseApproveType(flowElement); if (ObjectUtils.equalsAny(approveType, BpmUserTaskApproveTypeEnum.AUTO_APPROVE.getType(), BpmUserTaskApproveTypeEnum.AUTO_REJECT.getType())) { return new HashSet<>(); } - Integer strategy = BpmnModelUtils.parseCandidateStrategy(execution.getCurrentFlowElement()); - String param = BpmnModelUtils.parseCandidateParam(execution.getCurrentFlowElement()); // 1.1 计算任务的候选人 - Set userIds = getCandidateStrategy(strategy).calculateUsers(execution, param); - removeDisableUsers(userIds); + Integer strategy = BpmnModelUtils.parseCandidateStrategy(flowElement); + String param = BpmnModelUtils.parseCandidateParam(flowElement); + Set userIds = getCandidateStrategy(strategy).calculateUsersByTask(execution, param); // 1.2 移除被禁用的用户 removeDisableUsers(userIds); // 2. 候选人为空时,根据“审批人为空”的配置补充 if (CollUtil.isEmpty(userIds)) { userIds = getCandidateStrategy(BpmTaskCandidateStrategyEnum.ASSIGN_EMPTY.getStrategy()) - .calculateUsers(execution, param); + .calculateUsersByTask(execution, param); // ASSIGN_EMPTY 策略,不需要移除被禁用的用户。原因是,再移除,可能会出现更没审批人了!!! } // 3. 移除发起人的用户 - removeStartUserIfSkip(execution, userIds); + ProcessInstance processInstance = SpringUtil.getBean(BpmProcessInstanceService.class) + .getProcessInstance(execution.getProcessInstanceId()); + Assert.notNull(processInstance, "流程实例({}) 不存在", execution.getProcessInstanceId()); + removeStartUserIfSkip(userIds, flowElement, Long.valueOf(processInstance.getStartUserId())); + return userIds; + } + + public Set calculateUsersByActivity(BpmnModel bpmnModel, String activityId, + Long startUserId, String processDefinitionId, Map processVariables) { + // 审批类型非人工审核时,不进行计算候选人。原因是:后续会自动通过、不通过 + FlowElement flowElement = BpmnModelUtils.getFlowElementById(bpmnModel, activityId); + Integer approveType = BpmnModelUtils.parseApproveType(flowElement); + if (ObjectUtils.equalsAny(approveType, + BpmUserTaskApproveTypeEnum.AUTO_APPROVE.getType(), + BpmUserTaskApproveTypeEnum.AUTO_REJECT.getType())) { + return new HashSet<>(); + } + + // 1.1 计算任务的候选人 + Integer strategy = BpmnModelUtils.parseCandidateStrategy(flowElement); + String param = BpmnModelUtils.parseCandidateParam(flowElement); + Set userIds = getCandidateStrategy(strategy).calculateUsersByActivity(bpmnModel, activityId, param, + startUserId, processDefinitionId, processVariables); + // 1.2 移除被禁用的用户 + removeDisableUsers(userIds); + + // 2. 候选人为空时,根据“审批人为空”的配置补充 + if (CollUtil.isEmpty(userIds)) { + userIds = getCandidateStrategy(BpmTaskCandidateStrategyEnum.ASSIGN_EMPTY.getStrategy()) + .calculateUsersByActivity(bpmnModel, activityId, param, startUserId, processDefinitionId, processVariables); + // ASSIGN_EMPTY 策略,不需要移除被禁用的用户。原因是,再移除,可能会出现更没审批人了!!! + } + + // 3. 移除发起人的用户 + removeStartUserIfSkip(userIds, flowElement, startUserId); return userIds; } @@ -126,7 +161,7 @@ public class BpmTaskCandidateInvoker { Map userMap = adminUserApi.getUserMap(assigneeUserIds); assigneeUserIds.removeIf(id -> { AdminUserRespDTO user = userMap.get(id); - return user == null || !CommonStatusEnum.ENABLE.getStatus().equals(user.getStatus()); + return user == null || CommonStatusEnum.isDisable(user.getStatus()); }); } @@ -135,25 +170,23 @@ public class BpmTaskCandidateInvoker { * * 注意:如果只有一个候选人,则不处理,避免无法审批 * - * @param execution 执行中的任务 * @param assigneeUserIds 当前分配的候选人 + * @param flowElement 当前节点 + * @param startUserId 发起人 */ @VisibleForTesting - void removeStartUserIfSkip(DelegateExecution execution, Set assigneeUserIds) { + void removeStartUserIfSkip(Set assigneeUserIds, FlowElement flowElement, Long startUserId) { if (CollUtil.size(assigneeUserIds) <= 1) { return; } - Integer assignStartUserHandlerType = BpmnModelUtils.parseAssignStartUserHandlerType(execution.getCurrentFlowElement()); + Integer assignStartUserHandlerType = BpmnModelUtils.parseAssignStartUserHandlerType(flowElement); if (ObjectUtil.notEqual(assignStartUserHandlerType, BpmUserTaskAssignStartUserHandlerTypeEnum.SKIP.getType())) { return; } - ProcessInstance processInstance = SpringUtil.getBean(BpmProcessInstanceService.class) - .getProcessInstance(execution.getProcessInstanceId()); - Assert.notNull(processInstance, "流程实例({}) 不存在", execution.getProcessInstanceId()); - assigneeUserIds.remove(Long.valueOf(processInstance.getStartUserId())); + assigneeUserIds.remove(startUserId); } - public BpmTaskCandidateStrategy getCandidateStrategy(Integer strategy) { + private BpmTaskCandidateStrategy getCandidateStrategy(Integer strategy) { BpmTaskCandidateStrategyEnum strategyEnum = BpmTaskCandidateStrategyEnum.valueOf(strategy); Assert.notNull(strategyEnum, "策略(%s) 不存在", strategy); BpmTaskCandidateStrategy strategyObj = strategyMap.get(strategyEnum); diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateStrategy.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateStrategy.java index 9057a0ca1..90eb37c96 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateStrategy.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateStrategy.java @@ -1,10 +1,10 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate; import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; +import org.flowable.bpmn.model.BpmnModel; import org.flowable.engine.delegate.DelegateExecution; -import org.flowable.engine.runtime.ProcessInstance; -import java.util.Collections; +import java.util.Map; import java.util.Set; /** @@ -42,47 +42,44 @@ public interface BpmTaskCandidateStrategy { /** * 基于候选人参数,获得任务的候选用户们 * + * 注意:实现 calculateUsers 系列方法时,有两种选择: + * 1. 只重写 calculateUsers 默认方法 + * 2. 都重写 calculateUsersByTask 和 calculateUsersByActivity 两个方法 + * * @param param 执行任务 * @return 用户编号集合 */ default Set calculateUsers(String param) { - return Collections.emptySet(); + throw new UnsupportedOperationException("该分配方法未实现,请检查!"); } /** - * 基于执行任务,获得任务的候选用户们 + * 基于【执行任务】,获得任务的候选用户们 * * @param execution 执行任务 * @return 用户编号集合 */ - default Set calculateUsers(DelegateExecution execution, String param) { - Set users = calculateUsers(param); - removeDisableUsers(users); - return users; + default Set calculateUsersByTask(DelegateExecution execution, String param) { + return calculateUsers(param); } /** - * 基于流程实例,获得任务的候选用户们 + * 基于【流程活动】,获得任务的候选用户们 *

* 目的:用于获取未执行节点的候选用户们 * - * @param startUserId 流程发起人编号 - * @param processInstance 流程实例编号 - * @param activityId 活动 Id (对应 Bpmn XML id) + * @param bpmnModel 流程图 + * @param activityId 活动 ID (对应 Bpmn XML id) * @param param 节点的参数 + * @param startUserId 流程发起人编号 + * @param processDefinitionId 流程定义编号 + * @param processVariables 流程变量 * @return 用户编号集合 */ - default Set calculateUsers(Long startUserId, ProcessInstance processInstance, String activityId, String param) { - Set users = calculateUsers(param); - removeDisableUsers(users); - return users; + @SuppressWarnings("unused") + default Set calculateUsersByActivity(BpmnModel bpmnModel, String activityId, String param, + Long startUserId, String processDefinitionId, Map processVariables) { + return calculateUsers(param); } - /** - * 移除被禁用的用户 - * - * @param users 用户 Ids - */ - void removeDisableUsers(Set users); - } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateAbstractStrategy.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateAbstractStrategy.java deleted file mode 100644 index 8ff2bdaab..000000000 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateAbstractStrategy.java +++ /dev/null @@ -1,37 +0,0 @@ -package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy; - -import cn.hutool.core.collection.CollUtil; -import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; -import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy; -import cn.iocoder.yudao.module.system.api.user.AdminUserApi; -import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; - -import java.util.Map; -import java.util.Set; - -/** - * {@link BpmTaskCandidateStrategy} 抽象类 - * - * @author jason - */ -public abstract class BpmTaskCandidateAbstractStrategy implements BpmTaskCandidateStrategy { - - protected AdminUserApi adminUserApi; - - public BpmTaskCandidateAbstractStrategy(AdminUserApi adminUserApi) { - this.adminUserApi = adminUserApi; - } - - @Override - public void removeDisableUsers(Set users) { - if (CollUtil.isEmpty(users)) { - return; - } - Map userMap = adminUserApi.getUserMap(users); - users.removeIf(id -> { - AdminUserRespDTO user = userMap.get(id); - return user == null || !CommonStatusEnum.ENABLE.getStatus().equals(user.getStatus()); - }); - } - -} diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateAbstractDeptLeaderStrategy.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/AbstractBpmTaskCandidateDeptLeaderStrategy.java similarity index 78% rename from yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateAbstractDeptLeaderStrategy.java rename to yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/AbstractBpmTaskCandidateDeptLeaderStrategy.java index d5ac6706f..5cd249252 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateAbstractDeptLeaderStrategy.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/AbstractBpmTaskCandidateDeptLeaderStrategy.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy; +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.lang.Assert; @@ -6,6 +6,8 @@ import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCand import cn.iocoder.yudao.module.system.api.dept.DeptApi; import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import jakarta.annotation.Resource; import java.util.HashSet; import java.util.LinkedHashSet; @@ -17,14 +19,12 @@ import java.util.Set; * * @author jason */ -public abstract class BpmTaskCandidateAbstractDeptLeaderStrategy extends BpmTaskCandidateAbstractStrategy { +public abstract class AbstractBpmTaskCandidateDeptLeaderStrategy implements BpmTaskCandidateStrategy { + @Resource protected DeptApi deptApi; - - public BpmTaskCandidateAbstractDeptLeaderStrategy(AdminUserApi adminUserApi, DeptApi deptApi) { - super(adminUserApi); - this.deptApi = deptApi; - } + @Resource + protected AdminUserApi adminUserApi; /** * 获得指定层级的部门负责人,只有第 level 的负责人 @@ -78,4 +78,17 @@ public abstract class BpmTaskCandidateAbstractDeptLeaderStrategy extends BpmTas return deptLeaderIds; } + /** + * 获取发起人的部门 + * + * @param startUserId 发起人 Id + */ + protected DeptRespDTO getStartUserDept(Long startUserId) { + AdminUserRespDTO startUser = adminUserApi.getUser(startUserId).getCheckedData(); + if (startUser.getDeptId() == null) { // 找不到部门 + return null; + } + return deptApi.getDept(startUser.getDeptId()).getCheckedData(); + } + } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateDeptLeaderMultiStrategy.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateDeptLeaderMultiStrategy.java similarity index 62% rename from yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateDeptLeaderMultiStrategy.java rename to yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateDeptLeaderMultiStrategy.java index de0906923..61767ff89 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateDeptLeaderMultiStrategy.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateDeptLeaderMultiStrategy.java @@ -1,13 +1,12 @@ -package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy; +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept; import cn.hutool.core.lang.Assert; import cn.iocoder.yudao.framework.common.util.string.StrUtils; import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy; import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; -import cn.iocoder.yudao.module.system.api.dept.DeptApi; -import cn.iocoder.yudao.module.system.api.user.AdminUserApi; import org.springframework.stereotype.Component; +import java.util.List; import java.util.Set; /** @@ -16,11 +15,7 @@ import java.util.Set; * @author jason */ @Component -public class BpmTaskCandidateDeptLeaderMultiStrategy extends BpmTaskCandidateAbstractDeptLeaderStrategy { - - public BpmTaskCandidateDeptLeaderMultiStrategy(AdminUserApi adminUserApi, DeptApi deptApi) { - super(adminUserApi, deptApi); - } +public class BpmTaskCandidateDeptLeaderMultiStrategy extends AbstractBpmTaskCandidateDeptLeaderStrategy { @Override public BpmTaskCandidateStrategyEnum getStrategy() { @@ -32,14 +27,19 @@ public class BpmTaskCandidateDeptLeaderMultiStrategy extends BpmTaskCandidateAbs // 参数格式: | 分隔:1)左边为部门(多个部门用 , 分隔)。2)右边为部门层级 String[] params = param.split("\\|"); Assert.isTrue(params.length == 2, "参数格式不匹配"); - deptApi.validateDeptList(StrUtils.splitToLong(params[0], ",")).checkError(); - Assert.isTrue(Integer.parseInt(params[1]) > 0, "部门层级必须大于 0"); + List deptIds = StrUtils.splitToLong(params[0], ","); + int level = Integer.parseInt(params[1]); + // 校验部门存在 + deptApi.validateDeptList(deptIds).checkError(); + Assert.isTrue(level > 0, "部门层级必须大于 0"); } @Override public Set calculateUsers(String param) { String[] params = param.split("\\|"); - return getMultiLevelDeptLeaderIds(StrUtils.splitToLong(params[0], ","), Integer.valueOf(params[1])); + List deptIds = StrUtils.splitToLong(params[0], ","); + int level = Integer.parseInt(params[1]); + return super.getMultiLevelDeptLeaderIds(deptIds, level); } } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateDeptLeaderStrategy.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateDeptLeaderStrategy.java similarity index 75% rename from yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateDeptLeaderStrategy.java rename to yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateDeptLeaderStrategy.java index b257aafbb..f5d4a6a26 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateDeptLeaderStrategy.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateDeptLeaderStrategy.java @@ -1,11 +1,11 @@ -package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy; +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept; import cn.iocoder.yudao.framework.common.util.string.StrUtils; import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy; import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; import cn.iocoder.yudao.module.system.api.dept.DeptApi; import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; -import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import jakarta.annotation.Resource; import org.springframework.stereotype.Component; import java.util.List; @@ -19,14 +19,10 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils. * @author kyle */ @Component -public class BpmTaskCandidateDeptLeaderStrategy extends BpmTaskCandidateAbstractStrategy { +public class BpmTaskCandidateDeptLeaderStrategy implements BpmTaskCandidateStrategy { - private final DeptApi deptApi; - - public BpmTaskCandidateDeptLeaderStrategy(AdminUserApi adminUserApi, DeptApi deptApi) { - super(adminUserApi); - this.deptApi = deptApi; - } + @Resource + private DeptApi deptApi; @Override public BpmTaskCandidateStrategyEnum getStrategy() { @@ -36,7 +32,7 @@ public class BpmTaskCandidateDeptLeaderStrategy extends BpmTaskCandidateAbstract @Override public void validateParam(String param) { Set deptIds = StrUtils.splitToLongSet(param); - deptApi.validateDeptList(deptIds); + deptApi.validateDeptList(deptIds).checkError(); } @Override diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateDeptMemberStrategy.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateDeptMemberStrategy.java similarity index 79% rename from yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateDeptMemberStrategy.java rename to yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateDeptMemberStrategy.java index fa2031a0c..aa1059933 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateDeptMemberStrategy.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateDeptMemberStrategy.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy; +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept; import cn.iocoder.yudao.framework.common.util.string.StrUtils; import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy; @@ -6,6 +6,7 @@ import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidat import cn.iocoder.yudao.module.system.api.dept.DeptApi; import cn.iocoder.yudao.module.system.api.user.AdminUserApi; import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import jakarta.annotation.Resource; import org.springframework.stereotype.Component; import java.util.List; @@ -19,14 +20,12 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils. * @author kyle */ @Component -public class BpmTaskCandidateDeptMemberStrategy extends BpmTaskCandidateAbstractStrategy { +public class BpmTaskCandidateDeptMemberStrategy implements BpmTaskCandidateStrategy { - private final DeptApi deptApi; - - public BpmTaskCandidateDeptMemberStrategy(AdminUserApi adminUserApi, DeptApi deptApi) { - super(adminUserApi); - this.deptApi = deptApi; - } + @Resource + private DeptApi deptApi; + @Resource + private AdminUserApi adminUserApi; @Override public BpmTaskCandidateStrategyEnum getStrategy() { @@ -36,7 +35,7 @@ public class BpmTaskCandidateDeptMemberStrategy extends BpmTaskCandidateAbstract @Override public void validateParam(String param) { Set deptIds = StrUtils.splitToLongSet(param); - deptApi.validateDeptList(deptIds); + deptApi.validateDeptList(deptIds).checkError(); } @Override diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateStartUserDeptLeaderMultiStrategy.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserDeptLeaderMultiStrategy.java similarity index 51% rename from yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateStartUserDeptLeaderMultiStrategy.java rename to yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserDeptLeaderMultiStrategy.java index 1ba520e4a..00da98bd3 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateStartUserDeptLeaderMultiStrategy.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserDeptLeaderMultiStrategy.java @@ -1,21 +1,20 @@ -package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy; +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept; import cn.hutool.core.lang.Assert; import cn.iocoder.yudao.framework.common.util.number.NumberUtils; import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy; import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; -import cn.iocoder.yudao.module.system.api.dept.DeptApi; import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; -import cn.iocoder.yudao.module.system.api.user.AdminUserApi; -import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; import jakarta.annotation.Resource; +import org.flowable.bpmn.model.BpmnModel; import org.flowable.engine.delegate.DelegateExecution; import org.flowable.engine.runtime.ProcessInstance; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; import java.util.HashSet; +import java.util.Map; import java.util.Set; import static cn.hutool.core.collection.ListUtil.toList; @@ -26,16 +25,12 @@ import static cn.hutool.core.collection.ListUtil.toList; * @author jason */ @Component -public class BpmTaskCandidateStartUserDeptLeaderMultiStrategy extends BpmTaskCandidateAbstractDeptLeaderStrategy { +public class BpmTaskCandidateStartUserDeptLeaderMultiStrategy extends AbstractBpmTaskCandidateDeptLeaderStrategy { @Resource @Lazy private BpmProcessInstanceService processInstanceService; - public BpmTaskCandidateStartUserDeptLeaderMultiStrategy(AdminUserApi adminUserApi, DeptApi deptApi) { - super(adminUserApi, deptApi); - } - @Override public BpmTaskCandidateStrategyEnum getStrategy() { return BpmTaskCandidateStrategyEnum.START_USER_DEPT_LEADER_MULTI; @@ -43,48 +38,33 @@ public class BpmTaskCandidateStartUserDeptLeaderMultiStrategy extends BpmTaskCan @Override public void validateParam(String param) { - // 参数是部门的层级 - Assert.isTrue(Integer.parseInt(param) > 0, "部门的层级必须大于 0"); + int level = Integer.parseInt(param); // 参数是部门的层级 + Assert.isTrue(level > 0, "部门的层级必须大于 0"); } @Override - public Set calculateUsers(DelegateExecution execution, String param) { + public Set calculateUsersByTask(DelegateExecution execution, String param) { + int level = Integer.parseInt(param); // 参数是部门的层级 // 获得流程发起人 ProcessInstance processInstance = processInstanceService.getProcessInstance(execution.getProcessInstanceId()); Long startUserId = NumberUtils.parseLong(processInstance.getStartUserId()); // 获取发起人的 multi 部门负责人 - DeptRespDTO dept = getStartUserDept(startUserId); + DeptRespDTO dept = super.getStartUserDept(startUserId); if (dept == null) { return new HashSet<>(); } - Set users = getMultiLevelDeptLeaderIds(toList(dept.getId()), Integer.valueOf(param)); // 参数是部门的层级 - // TODO @jason:这里 removeDisableUsers 的原因是啥呀? - removeDisableUsers(users); - return users; + return super.getMultiLevelDeptLeaderIds(toList(dept.getId()), level); } @Override - public Set calculateUsers(Long startUserId, ProcessInstance processInstance, String activityId, String param) { - DeptRespDTO dept = getStartUserDept(startUserId); + public Set calculateUsersByActivity(BpmnModel bpmnModel, String activityId, String param, + Long startUserId, String processDefinitionId, Map processVariables) { + int level = Integer.parseInt(param); // 参数是部门的层级 + DeptRespDTO dept = super.getStartUserDept(startUserId); if (dept == null) { return new HashSet<>(); } - Set users = getMultiLevelDeptLeaderIds(toList(dept.getId()), Integer.valueOf(param)); // 参数是部门的层级 - removeDisableUsers(users); - return users; - } - - /** - * 获取发起人的部门 - * - * @param startUserId 发起人 Id - */ - protected DeptRespDTO getStartUserDept(Long startUserId) { - AdminUserRespDTO startUser = adminUserApi.getUser(startUserId).getCheckedData(); - if (startUser.getDeptId() == null) { // 找不到部门 - return null; - } - return deptApi.getDept(startUser.getDeptId()).getCheckedData(); + return super.getMultiLevelDeptLeaderIds(toList(dept.getId()), level); } } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateStartUserDeptLeaderStrategy.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserDeptLeaderStrategy.java similarity index 59% rename from yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateStartUserDeptLeaderStrategy.java rename to yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserDeptLeaderStrategy.java index 7bad6b8c2..115bb3c39 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateStartUserDeptLeaderStrategy.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserDeptLeaderStrategy.java @@ -1,21 +1,20 @@ -package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy; +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept; import cn.hutool.core.lang.Assert; import cn.iocoder.yudao.framework.common.util.number.NumberUtils; import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy; import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; -import cn.iocoder.yudao.module.system.api.dept.DeptApi; import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; -import cn.iocoder.yudao.module.system.api.user.AdminUserApi; -import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; import jakarta.annotation.Resource; +import org.flowable.bpmn.model.BpmnModel; import org.flowable.engine.delegate.DelegateExecution; import org.flowable.engine.runtime.ProcessInstance; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; import java.util.HashSet; +import java.util.Map; import java.util.Set; import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet; @@ -26,7 +25,7 @@ import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet; * @author jason */ @Component -public class BpmTaskCandidateStartUserDeptLeaderStrategy extends BpmTaskCandidateAbstractDeptLeaderStrategy { +public class BpmTaskCandidateStartUserDeptLeaderStrategy extends AbstractBpmTaskCandidateDeptLeaderStrategy { @Resource @Lazy // 避免循环依赖 @@ -37,10 +36,6 @@ public class BpmTaskCandidateStartUserDeptLeaderStrategy extends BpmTaskCandidat return BpmTaskCandidateStrategyEnum.START_USER_DEPT_LEADER; } - public BpmTaskCandidateStartUserDeptLeaderStrategy(AdminUserApi adminUserApi, DeptApi deptApi) { - super(adminUserApi, deptApi); - } - @Override public void validateParam(String param) { // 参数是部门的层级 @@ -48,44 +43,29 @@ public class BpmTaskCandidateStartUserDeptLeaderStrategy extends BpmTaskCandidat } @Override - public Set calculateUsers(DelegateExecution execution, String param) { + public Set calculateUsersByTask(DelegateExecution execution, String param) { // 获得流程发起人 ProcessInstance processInstance = processInstanceService.getProcessInstance(execution.getProcessInstanceId()); Long startUserId = NumberUtils.parseLong(processInstance.getStartUserId()); // 获取发起人的部门负责人 - Set users = getStartUserDeptLeader(startUserId, param); - removeDisableUsers(users); - return users; + return getStartUserDeptLeader(startUserId, param); } @Override - public Set calculateUsers(Long startUserId, ProcessInstance processInstance, String activityId, String param) { + public Set calculateUsersByActivity(BpmnModel bpmnModel, String activityId, String param, + Long startUserId, String processDefinitionId, Map processVariables) { // 获取发起人的部门负责人 - Set users = getStartUserDeptLeader(startUserId, param); - removeDisableUsers(users); - return users; + return getStartUserDeptLeader(startUserId, param); } private Set getStartUserDeptLeader(Long startUserId, String param) { - DeptRespDTO dept = getStartUserDept(startUserId); + int level = Integer.parseInt(param); // 参数是部门的层级 + DeptRespDTO dept = super.getStartUserDept(startUserId); if (dept == null) { return new HashSet<>(); } - Long deptLeaderId = getAssignLevelDeptLeaderId(dept, Integer.valueOf(param)); // 参数是部门的层级 + Long deptLeaderId = super.getAssignLevelDeptLeaderId(dept, level); return deptLeaderId != null ? asSet(deptLeaderId) : new HashSet<>(); } - /** - * 获取发起人的部门 - * - * @param startUserId 发起人 Id - */ - protected DeptRespDTO getStartUserDept(Long startUserId) { - AdminUserRespDTO startUser = adminUserApi.getUser(startUserId).getCheckedData(); - if (startUser.getDeptId() == null) { // 找不到部门 - return null; - } - return deptApi.getDept(startUser.getDeptId()).getCheckedData(); - } - } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateStartUserSelectStrategy.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserSelectStrategy.java similarity index 58% rename from yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateStartUserSelectStrategy.java rename to yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserSelectStrategy.java index af9438c56..9fd14d6de 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateStartUserSelectStrategy.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserSelectStrategy.java @@ -1,14 +1,18 @@ -package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy; +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.user.BpmTaskCandidateUserStrategy; import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils; import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils; import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; -import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import com.google.common.collect.Sets; import jakarta.annotation.Resource; import org.flowable.bpmn.model.BpmnModel; +import org.flowable.bpmn.model.ServiceTask; +import org.flowable.bpmn.model.Task; import org.flowable.bpmn.model.UserTask; import org.flowable.engine.delegate.DelegateExecution; import org.flowable.engine.runtime.ProcessInstance; @@ -23,16 +27,12 @@ import java.util.*; * @author 芋道源码 */ @Component -public class BpmTaskCandidateStartUserSelectStrategy extends BpmTaskCandidateAbstractStrategy { +public class BpmTaskCandidateStartUserSelectStrategy extends AbstractBpmTaskCandidateDeptLeaderStrategy { @Resource @Lazy // 延迟加载,避免循环依赖 private BpmProcessInstanceService processInstanceService; - public BpmTaskCandidateStartUserSelectStrategy(AdminUserApi adminUserApi) { - super(adminUserApi); - } - @Override public BpmTaskCandidateStrategyEnum getStrategy() { return BpmTaskCandidateStrategyEnum.START_USER_SELECT; @@ -42,7 +42,12 @@ public class BpmTaskCandidateStartUserSelectStrategy extends BpmTaskCandidateAbs public void validateParam(String param) {} @Override - public Set calculateUsers(DelegateExecution execution, String param) { + public boolean isParamRequired() { + return false; + } + + @Override + public LinkedHashSet calculateUsersByTask(DelegateExecution execution, String param) { ProcessInstance processInstance = processInstanceService.getProcessInstance(execution.getProcessInstanceId()); Assert.notNull(processInstance, "流程实例({})不能为空", execution.getProcessInstanceId()); Map> startUserSelectAssignees = FlowableUtils.getStartUserSelectAssignees(processInstance); @@ -50,47 +55,43 @@ public class BpmTaskCandidateStartUserSelectStrategy extends BpmTaskCandidateAbs execution.getProcessInstanceId()); // 获得审批人 List assignees = startUserSelectAssignees.get(execution.getCurrentActivityId()); - Set users = new LinkedHashSet<>(assignees); - removeDisableUsers(users); - return users; + return new LinkedHashSet<>(assignees); } @Override - public Set calculateUsers(Long startUserId, ProcessInstance processInstance, String activityId, String param) { - if (processInstance == null) { - return Collections.emptySet(); + public LinkedHashSet calculateUsersByActivity(BpmnModel bpmnModel, String activityId, String param, + Long startUserId, String processDefinitionId, Map processVariables) { + if (processVariables == null) { + return Sets.newLinkedHashSet(); + } + Map> startUserSelectAssignees = FlowableUtils.getStartUserSelectAssignees(processVariables); + if (startUserSelectAssignees == null) { + return Sets.newLinkedHashSet(); } - Map> startUserSelectAssignees = FlowableUtils.getStartUserSelectAssignees(processInstance); - Assert.notNull(startUserSelectAssignees, "流程实例({}) 的发起人自选审批人不能为空", processInstance.getId()); // 获得审批人 List assignees = startUserSelectAssignees.get(activityId); - Set users = new LinkedHashSet<>(assignees); - removeDisableUsers(users); - return users; - } - - @Override - public boolean isParamRequired() { - return false; + return new LinkedHashSet<>(assignees); } /** - * 获得发起人自选审批人的 UserTask 列表 + * 获得发起人自选审批人或抄送人的 Task 列表 * * @param bpmnModel BPMN 模型 - * @return UserTask 列表 + * @return Task 列表 */ - public static List getStartUserSelectUserTaskList(BpmnModel bpmnModel) { + public static List getStartUserSelectTaskList(BpmnModel bpmnModel) { if (bpmnModel == null) { - return null; + return Collections.emptyList(); } - List userTaskList = BpmnModelUtils.getBpmnModelElements(bpmnModel, UserTask.class); - if (CollUtil.isEmpty(userTaskList)) { - return null; + List tasks = new ArrayList<>(); + tasks.addAll(BpmnModelUtils.getBpmnModelElements(bpmnModel, UserTask.class)); + tasks.addAll(BpmnModelUtils.getBpmnModelElements(bpmnModel, ServiceTask.class)); + if (CollUtil.isEmpty(tasks)) { + return Collections.emptyList(); } - userTaskList.removeIf(userTask -> !Objects.equals(BpmnModelUtils.parseCandidateStrategy(userTask), + tasks.removeIf(task -> ObjectUtil.notEqual(BpmnModelUtils.parseCandidateStrategy(task), BpmTaskCandidateStrategyEnum.START_USER_SELECT.getStrategy())); - return userTaskList; + return tasks; } } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/form/BpmTaskCandidateFormSDeptLeaderStrategy.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/form/BpmTaskCandidateFormSDeptLeaderStrategy.java new file mode 100644 index 000000000..f13934364 --- /dev/null +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/form/BpmTaskCandidateFormSDeptLeaderStrategy.java @@ -0,0 +1,56 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.form; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept.AbstractBpmTaskCandidateDeptLeaderStrategy; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; +import org.flowable.bpmn.model.BpmnModel; +import org.flowable.engine.delegate.DelegateExecution; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.Set; + +/** + * 表单内部门负责人 {@link BpmTaskCandidateStrategy} 实现类 + * + * @author jason + */ +@Component +public class BpmTaskCandidateFormSDeptLeaderStrategy extends AbstractBpmTaskCandidateDeptLeaderStrategy { + + @Override + public BpmTaskCandidateStrategyEnum getStrategy() { + return BpmTaskCandidateStrategyEnum.FORM_DEPT_LEADER; + } + + @Override + public void validateParam(String param) { + // 参数格式: | 分隔:1)左边为表单内部门字段。2)右边为部门层级 + String[] params = param.split("\\|"); + Assert.isTrue(params.length == 2, "参数格式不匹配"); + Assert.notEmpty(param, "表单内部门字段不能为空"); + int level = Integer.parseInt(params[1]); + Assert.isTrue(level > 0, "部门层级必须大于 0"); + } + + @Override + public Set calculateUsersByTask(DelegateExecution execution, String param) { + String[] params = param.split("\\|"); + Object result = execution.getVariable(params[0]); + int level = Integer.parseInt(params[1]); + return super.getMultiLevelDeptLeaderIds(Convert.toList(Long.class, result), level); + } + + @Override + public Set calculateUsersByActivity(BpmnModel bpmnModel, String activityId, + String param, Long startUserId, String processDefinitionId, + Map processVariables) { + String[] params = param.split("\\|"); + Object result = processVariables == null ? null : processVariables.get(params[0]); + int level = Integer.parseInt(params[1]); + return super.getMultiLevelDeptLeaderIds(Convert.toList(Long.class, result), level); + } + +} diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/form/BpmTaskCandidateFormUserStrategy.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/form/BpmTaskCandidateFormUserStrategy.java new file mode 100644 index 000000000..2d315979a --- /dev/null +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/form/BpmTaskCandidateFormUserStrategy.java @@ -0,0 +1,47 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.form; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.user.BpmTaskCandidateUserStrategy; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; +import org.flowable.bpmn.model.BpmnModel; +import org.flowable.engine.delegate.DelegateExecution; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.Set; + +/** + * 表单内用户字段 {@link BpmTaskCandidateUserStrategy} 实现类 + * + * @author jason + */ +@Component +public class BpmTaskCandidateFormUserStrategy implements BpmTaskCandidateStrategy { + + @Override + public BpmTaskCandidateStrategyEnum getStrategy() { + return BpmTaskCandidateStrategyEnum.FORM_USER; + } + + @Override + public void validateParam(String param) { + Assert.notEmpty(param, "表单内用户字段不能为空"); + } + + @Override + public Set calculateUsersByTask(DelegateExecution execution, String param) { + Object result = execution.getVariable(param); + return Convert.toSet(Long.class, result); + } + + @Override + public Set calculateUsersByActivity(BpmnModel bpmnModel, String activityId, + String param, Long startUserId, String processDefinitionId, + Map processVariables) { + Object result = processVariables == null ? null : processVariables.get(param); + return Convert.toSet(Long.class, result); + } + +} diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateAssignEmptyStrategy.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/other/BpmTaskCandidateAssignEmptyStrategy.java similarity index 63% rename from yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateAssignEmptyStrategy.java rename to yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/other/BpmTaskCandidateAssignEmptyStrategy.java index d6bd19caf..de1999791 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateAssignEmptyStrategy.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/other/BpmTaskCandidateAssignEmptyStrategy.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy; +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.other; import cn.hutool.core.lang.Assert; import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO; @@ -7,13 +7,15 @@ import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCand import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils; import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService; -import cn.iocoder.yudao.module.system.api.user.AdminUserApi; import jakarta.annotation.Resource; +import org.flowable.bpmn.model.BpmnModel; +import org.flowable.bpmn.model.FlowElement; import org.flowable.engine.delegate.DelegateExecution; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; import java.util.HashSet; +import java.util.Map; import java.util.Objects; import java.util.Set; @@ -23,16 +25,12 @@ import java.util.Set; * @author kyle */ @Component -public class BpmTaskCandidateAssignEmptyStrategy extends BpmTaskCandidateAbstractStrategy { +public class BpmTaskCandidateAssignEmptyStrategy implements BpmTaskCandidateStrategy { @Resource @Lazy // 延迟加载,避免循环依赖 private BpmProcessDefinitionService processDefinitionService; - public BpmTaskCandidateAssignEmptyStrategy(AdminUserApi adminUserApi) { - super(adminUserApi); - } - @Override public BpmTaskCandidateStrategyEnum getStrategy() { return BpmTaskCandidateStrategyEnum.ASSIGN_EMPTY; @@ -43,19 +41,28 @@ public class BpmTaskCandidateAssignEmptyStrategy extends BpmTaskCandidateAbstrac } @Override - public Set calculateUsers(DelegateExecution execution, String param) { + public Set calculateUsersByTask(DelegateExecution execution, String param) { + return getCandidateUsers(execution.getProcessDefinitionId(), execution.getCurrentFlowElement()); + } + + @Override + public Set calculateUsersByActivity(BpmnModel bpmnModel, String activityId, String param, + Long startUserId, String processDefinitionId, Map processVariables) { + FlowElement flowElement = BpmnModelUtils.getFlowElementById(bpmnModel, activityId); + return getCandidateUsers(processDefinitionId, flowElement); + } + + private Set getCandidateUsers(String processDefinitionId, FlowElement flowElement) { // 情况一:指定人员审批 - Integer assignEmptyHandlerType = BpmnModelUtils.parseAssignEmptyHandlerType(execution.getCurrentFlowElement()); + Integer assignEmptyHandlerType = BpmnModelUtils.parseAssignEmptyHandlerType(flowElement); if (Objects.equals(assignEmptyHandlerType, BpmUserTaskAssignEmptyHandlerTypeEnum.ASSIGN_USER.getType())) { - Set users = new HashSet<>(BpmnModelUtils.parseAssignEmptyHandlerUserIds(execution.getCurrentFlowElement())); - removeDisableUsers(users); - return users; + return new HashSet<>(BpmnModelUtils.parseAssignEmptyHandlerUserIds(flowElement)); } // 情况二:流程管理员 if (Objects.equals(assignEmptyHandlerType, BpmUserTaskAssignEmptyHandlerTypeEnum.ASSIGN_ADMIN.getType())) { - BpmProcessDefinitionInfoDO processDefinition = processDefinitionService.getProcessDefinitionInfo(execution.getProcessDefinitionId()); - Assert.notNull(processDefinition, "流程定义({})不存在", execution.getProcessDefinitionId()); + BpmProcessDefinitionInfoDO processDefinition = processDefinitionService.getProcessDefinitionInfo(processDefinitionId); + Assert.notNull(processDefinition, "流程定义({})不存在", processDefinitionId); return new HashSet<>(processDefinition.getManagerUserIds()); } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateExpressionStrategy.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/other/BpmTaskCandidateExpressionStrategy.java similarity index 58% rename from yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateExpressionStrategy.java rename to yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/other/BpmTaskCandidateExpressionStrategy.java index 1e48cdf94..5683edeef 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateExpressionStrategy.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/other/BpmTaskCandidateExpressionStrategy.java @@ -1,13 +1,14 @@ -package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy; +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.other; import cn.hutool.core.convert.Convert; import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy; import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils; -import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import org.flowable.bpmn.model.BpmnModel; import org.flowable.engine.delegate.DelegateExecution; import org.springframework.stereotype.Component; +import java.util.Map; import java.util.Set; /** @@ -16,11 +17,7 @@ import java.util.Set; * @author 芋道源码 */ @Component -public class BpmTaskCandidateExpressionStrategy extends BpmTaskCandidateAbstractStrategy { - - public BpmTaskCandidateExpressionStrategy(AdminUserApi adminUserApi) { - super(adminUserApi); - } +public class BpmTaskCandidateExpressionStrategy implements BpmTaskCandidateStrategy { @Override public BpmTaskCandidateStrategyEnum getStrategy() { @@ -33,11 +30,16 @@ public class BpmTaskCandidateExpressionStrategy extends BpmTaskCandidateAbstract } @Override - public Set calculateUsers(DelegateExecution execution, String param) { + public Set calculateUsersByTask(DelegateExecution execution, String param) { Object result = FlowableUtils.getExpressionValue(execution, param); - Set users = Convert.toSet(Long.class, result); - removeDisableUsers(users); - return users; + return Convert.toSet(Long.class, result); + } + + @Override + public Set calculateUsersByActivity(BpmnModel bpmnModel, String activityId, String param, + Long startUserId, String processDefinitionId, Map processVariables) { + Object result = FlowableUtils.getExpressionValue(processVariables, param); + return Convert.toSet(Long.class, result); } } \ No newline at end of file diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateGroupStrategy.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateGroupStrategy.java similarity index 74% rename from yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateGroupStrategy.java rename to yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateGroupStrategy.java index 9a239f7bb..1d63e15f1 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateGroupStrategy.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateGroupStrategy.java @@ -1,11 +1,11 @@ -package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy; +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.user; import cn.iocoder.yudao.framework.common.util.string.StrUtils; import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmUserGroupDO; import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy; import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; import cn.iocoder.yudao.module.bpm.service.definition.BpmUserGroupService; -import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import jakarta.annotation.Resource; import org.springframework.stereotype.Component; import java.util.Collection; @@ -20,14 +20,10 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils. * @author kyle */ @Component -public class BpmTaskCandidateGroupStrategy extends BpmTaskCandidateAbstractStrategy { +public class BpmTaskCandidateGroupStrategy implements BpmTaskCandidateStrategy { - private final BpmUserGroupService userGroupService; - - public BpmTaskCandidateGroupStrategy(AdminUserApi adminUserApi, BpmUserGroupService userGroupService) { - super(adminUserApi); - this.userGroupService = userGroupService; - } + @Resource + private BpmUserGroupService userGroupService; @Override public BpmTaskCandidateStrategyEnum getStrategy() { @@ -37,7 +33,7 @@ public class BpmTaskCandidateGroupStrategy extends BpmTaskCandidateAbstractStrat @Override public void validateParam(String param) { Set groupIds = StrUtils.splitToLongSet(param); - userGroupService.getUserGroupList(groupIds); + userGroupService.validUserGroups(groupIds); } @Override diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidatePostStrategy.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidatePostStrategy.java similarity index 82% rename from yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidatePostStrategy.java rename to yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidatePostStrategy.java index 14793fcc9..573b2ff08 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidatePostStrategy.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidatePostStrategy.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy; +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.user; import cn.iocoder.yudao.framework.common.util.string.StrUtils; import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy; @@ -6,6 +6,7 @@ import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidat import cn.iocoder.yudao.module.system.api.dept.PostApi; import cn.iocoder.yudao.module.system.api.user.AdminUserApi; import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import jakarta.annotation.Resource; import org.springframework.stereotype.Component; import java.util.List; @@ -19,14 +20,12 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils. * @author kyle */ @Component -public class BpmTaskCandidatePostStrategy extends BpmTaskCandidateAbstractStrategy { +public class BpmTaskCandidatePostStrategy implements BpmTaskCandidateStrategy { - private final PostApi postApi; - - public BpmTaskCandidatePostStrategy(AdminUserApi adminUserApi, PostApi postApi) { - super(adminUserApi); - this.postApi = postApi; - } + @Resource + private PostApi postApi; + @Resource + private AdminUserApi adminUserApi; @Override public BpmTaskCandidateStrategyEnum getStrategy() { diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateRoleStrategy.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateRoleStrategy.java similarity index 82% rename from yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateRoleStrategy.java rename to yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateRoleStrategy.java index 8a0f40f22..a42add9ea 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateRoleStrategy.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateRoleStrategy.java @@ -1,11 +1,10 @@ -package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy; +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.user; import cn.iocoder.yudao.framework.common.util.string.StrUtils; import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy; import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; import cn.iocoder.yudao.module.system.api.permission.PermissionApi; import cn.iocoder.yudao.module.system.api.permission.RoleApi; -import cn.iocoder.yudao.module.system.api.user.AdminUserApi; import jakarta.annotation.Resource; import org.springframework.stereotype.Component; @@ -17,17 +16,13 @@ import java.util.Set; * @author kyle */ @Component -public class BpmTaskCandidateRoleStrategy extends BpmTaskCandidateAbstractStrategy { +public class BpmTaskCandidateRoleStrategy implements BpmTaskCandidateStrategy { @Resource private RoleApi roleApi; @Resource private PermissionApi permissionApi; - public BpmTaskCandidateRoleStrategy(AdminUserApi adminUserApi) { - super(adminUserApi); - } - @Override public BpmTaskCandidateStrategyEnum getStrategy() { return BpmTaskCandidateStrategyEnum.ROLE; diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateStartUserStrategy.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateStartUserStrategy.java similarity index 63% rename from yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateStartUserStrategy.java rename to yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateStartUserStrategy.java index ddc990c61..227316718 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateStartUserStrategy.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateStartUserStrategy.java @@ -1,15 +1,17 @@ -package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy; +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.user; import cn.iocoder.yudao.framework.common.util.collection.SetUtils; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy; import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; -import cn.iocoder.yudao.module.system.api.user.AdminUserApi; import jakarta.annotation.Resource; +import org.flowable.bpmn.model.BpmnModel; import org.flowable.engine.delegate.DelegateExecution; import org.flowable.engine.runtime.ProcessInstance; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; +import java.util.Map; import java.util.Set; /** @@ -20,16 +22,12 @@ import java.util.Set; * @author jason */ @Component -public class BpmTaskCandidateStartUserStrategy extends BpmTaskCandidateAbstractStrategy { +public class BpmTaskCandidateStartUserStrategy implements BpmTaskCandidateStrategy { @Resource @Lazy // 延迟加载,避免循环依赖 private BpmProcessInstanceService processInstanceService; - public BpmTaskCandidateStartUserStrategy(AdminUserApi adminUserApi) { - super(adminUserApi); - } - @Override public BpmTaskCandidateStrategyEnum getStrategy() { return BpmTaskCandidateStrategyEnum.START_USER; @@ -45,18 +43,15 @@ public class BpmTaskCandidateStartUserStrategy extends BpmTaskCandidateAbstractS } @Override - public Set calculateUsers(DelegateExecution execution, String param) { + public Set calculateUsersByTask(DelegateExecution execution, String param) { ProcessInstance processInstance = processInstanceService.getProcessInstance(execution.getProcessInstanceId()); - Set users = SetUtils.asSet(Long.valueOf(processInstance.getStartUserId())); - removeDisableUsers(users); - return users; + return SetUtils.asSet(Long.valueOf(processInstance.getStartUserId())); } @Override - public Set calculateUsers(Long startUserId, ProcessInstance processInstance, String activityId, String param) { - Set users = SetUtils.asSet(startUserId); - removeDisableUsers(users); - return users; + public Set calculateUsersByActivity(BpmnModel bpmnModel, String activityId, String param, + Long startUserId, String processDefinitionId, Map processVariables) { + return SetUtils.asSet(startUserId); } } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateUserStrategy.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateUserStrategy.java similarity index 77% rename from yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateUserStrategy.java rename to yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateUserStrategy.java index 47f2f541f..44fb1df4c 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateUserStrategy.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateUserStrategy.java @@ -1,14 +1,14 @@ -package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy; +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.user; import cn.hutool.core.text.StrPool; import cn.iocoder.yudao.framework.common.util.string.StrUtils; import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy; import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import jakarta.annotation.Resource; import org.springframework.stereotype.Component; import java.util.LinkedHashSet; -import java.util.Set; /** * 用户 {@link BpmTaskCandidateStrategy} 实现类 @@ -16,11 +16,10 @@ import java.util.Set; * @author kyle */ @Component -public class BpmTaskCandidateUserStrategy extends BpmTaskCandidateAbstractStrategy { +public class BpmTaskCandidateUserStrategy implements BpmTaskCandidateStrategy { - public BpmTaskCandidateUserStrategy(AdminUserApi adminUserApi) { - super(adminUserApi); - } + @Resource + private AdminUserApi adminUserApi; @Override public BpmTaskCandidateStrategyEnum getStrategy() { @@ -33,7 +32,7 @@ public class BpmTaskCandidateUserStrategy extends BpmTaskCandidateAbstractStrate } @Override - public Set calculateUsers(String param) { + public LinkedHashSet calculateUsers(String param) { return new LinkedHashSet<>(StrUtils.splitToLong(param, StrPool.COMMA)); } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/el/VariableConvertByTypeExpressionFunction.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/el/VariableConvertByTypeExpressionFunction.java index e2a7252b7..0e181769b 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/el/VariableConvertByTypeExpressionFunction.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/el/VariableConvertByTypeExpressionFunction.java @@ -4,9 +4,10 @@ import org.flowable.common.engine.api.variable.VariableContainer; import org.flowable.common.engine.impl.el.function.AbstractFlowableVariableExpressionFunction; import org.springframework.stereotype.Component; -// TODO @jason:这个自定义转换的原因是啥呀? /** - * 根据流程变量 variable 的类型, 转换参数的值 + * 根据流程变量 variable 的类型,转换参数的值 + * + * 目前用于 ConditionNodeConvert 的 buildConditionExpression 方法中 * * @author jason */ @@ -20,11 +21,12 @@ public class VariableConvertByTypeExpressionFunction extends AbstractFlowableVar public static Object convertByType(VariableContainer variableContainer, String variableName, Object parmaValue) { Object variable = variableContainer.getVariable(variableName); if (variable != null && parmaValue != null) { - // 如果值不是字符串类型, 流程变量的类型是字符串。 把值转成字符串 + // 如果值不是字符串类型,流程变量的类型是字符串,把值转成字符串 if (!(parmaValue instanceof String) && variable instanceof String ) { return parmaValue.toString(); } } return parmaValue; } + } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmTaskCandidateStrategyEnum.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmTaskCandidateStrategyEnum.java index 240aa18dc..687614f40 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmTaskCandidateStrategyEnum.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmTaskCandidateStrategyEnum.java @@ -29,6 +29,8 @@ public enum BpmTaskCandidateStrategyEnum implements IntArrayValuable { START_USER_DEPT_LEADER(37, "发起人部门负责人"), START_USER_DEPT_LEADER_MULTI(38, "发起人连续多级部门的负责人"), USER_GROUP(40, "用户组"), + FORM_USER(50, "表单内用户字段"), + FORM_DEPT_LEADER(51, "表单内部门负责人"), EXPRESSION(60, "流程表达式"), // 表达式 ExpressionManager ASSIGN_EMPTY(1, "审批人为空"), ; diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnModelConstants.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnModelConstants.java index 25772d5f3..60a864848 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnModelConstants.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnModelConstants.java @@ -52,7 +52,7 @@ public interface BpmnModelConstants { */ String USER_TASK_REJECT_HANDLER_TYPE = "rejectHandlerType"; /** - * BPMN ExtensionElement 的扩展属性,用于标记用户任务拒绝后的回退的任务 Id + * BPMN ExtensionElement 的扩展属性,用于标记用户任务拒绝后的退回的任务 Id */ String USER_TASK_REJECT_RETURN_TASK_ID = "rejectReturnTaskId"; @@ -104,10 +104,6 @@ public interface BpmnModelConstants { * BPMN Start Event Node Id */ String START_EVENT_NODE_ID = "StartEvent"; - /** - * BPMN Start Event Node Name - */ - String START_EVENT_NODE_NAME = "开始"; /** * 发起人节点 ID diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnVariableConstants.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnVariableConstants.java index 5ccaea306..08fb5c48e 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnVariableConstants.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnVariableConstants.java @@ -29,7 +29,12 @@ public class BpmnVariableConstants { * @see ProcessInstance#getProcessVariables() */ public static final String PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES = "PROCESS_START_USER_SELECT_ASSIGNEES"; - + /** + * 流程实例的变量 - 发起用户 ID + * + * @see ProcessInstance#getProcessVariables() + */ + public static final String PROCESS_INSTANCE_VARIABLE_START_USER_ID = "PROCESS_START_USER_ID"; /** * 流程实例的变量 - 用于判断流程实例变量节点是否驳回. 格式 RETURN_FLAG_{节点 id} * diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/SimpleModelConstants.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/SimpleModelConstants.java deleted file mode 100644 index 2275f0c12..000000000 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/SimpleModelConstants.java +++ /dev/null @@ -1,33 +0,0 @@ -package cn.iocoder.yudao.module.bpm.framework.flowable.core.enums; - -// TODO @jason:要不合并到 BpmnModelConstants 那 -/** - * 仿钉钉快搭 JSON 常量信息 - * - * @author jason - */ -public interface SimpleModelConstants { - - // TODO @芋艿:条件表达式的字段名 - - /** - * 网关节点默认序列流属性 - */ - String DEFAULT_FLOW_ATTRIBUTE = "defaultFlow"; - - /** - * 条件节点的条件类型属性 - */ - String CONDITION_TYPE_ATTRIBUTE = "conditionType"; - - /** - * 条件节点条件表达式属性 - */ - String CONDITION_EXPRESSION_ATTRIBUTE = "conditionExpression"; - - /** - * 条件规则的条件组属性 - */ - String CONDITION_GROUPS_ATTRIBUTE = "conditionGroups"; - -} diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmCopyTaskDelegate.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmCopyTaskDelegate.java index 3b9b34e59..e58d345e9 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmCopyTaskDelegate.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmCopyTaskDelegate.java @@ -15,8 +15,8 @@ import static cn.iocoder.yudao.module.bpm.framework.flowable.core.listener.BpmCo /** * 处理抄送用户的 {@link JavaDelegate} 的实现类 - * - * 目前只有快搭模式的【抄送节点】使用 + *

+ * 目前只有仿钉钉/飞书模式的【抄送节点】使用 * * @author jason */ @@ -34,14 +34,14 @@ public class BpmCopyTaskDelegate implements JavaDelegate { @Override public void execute(DelegateExecution execution) { // 1. 获得抄送人 - Set userIds = taskCandidateInvoker.calculateUsers(execution); + Set userIds = taskCandidateInvoker.calculateUsersByTask(execution); if (CollUtil.isEmpty(userIds)) { return; } // 2. 执行抄送 FlowElement currentFlowElement = execution.getCurrentFlowElement(); - processInstanceCopyService.createProcessInstanceCopy(userIds, execution.getProcessInstanceId(), - currentFlowElement.getId(), null, currentFlowElement.getName()); + processInstanceCopyService.createProcessInstanceCopy(userIds, null, execution.getProcessInstanceId(), + currentFlowElement.getId(), currentFlowElement.getName(), null); } } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmTaskEventListener.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmTaskEventListener.java index bece6740e..fc113936f 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmTaskEventListener.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmTaskEventListener.java @@ -8,7 +8,6 @@ import cn.iocoder.yudao.module.bpm.enums.definition.BpmBoundaryEventType; import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants; import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils; import cn.iocoder.yudao.module.bpm.service.definition.BpmModelService; -import cn.iocoder.yudao.module.bpm.service.task.BpmActivityService; import cn.iocoder.yudao.module.bpm.service.task.BpmTaskService; import com.google.common.collect.ImmutableSet; import jakarta.annotation.Resource; @@ -44,9 +43,6 @@ public class BpmTaskEventListener extends AbstractFlowableEngineEventListener { @Resource @Lazy // 解决循环依赖 private BpmTaskService taskService; - @Resource - @Lazy // 解决循环依赖 - private BpmActivityService activityService; public static final Set TASK_EVENTS = ImmutableSet.builder() .add(FlowableEngineEventType.TASK_CREATED) @@ -72,7 +68,7 @@ public class BpmTaskEventListener extends AbstractFlowableEngineEventListener { @Override protected void activityCancelled(FlowableActivityCancelledEvent event) { - List activityList = activityService.getHistoricActivityListByExecutionId(event.getExecutionId()); + List activityList = taskService.getHistoricActivityListByExecutionId(event.getExecutionId()); if (CollUtil.isEmpty(activityList)) { log.error("[activityCancelled][使用 executionId({}) 查找不到对应的活动实例]", event.getExecutionId()); return; @@ -87,6 +83,7 @@ public class BpmTaskEventListener extends AbstractFlowableEngineEventListener { } @Override + @SuppressWarnings("PatternVariableCanBeUsed") protected void timerFired(FlowableEngineEntityEvent event) { // 1.1 只处理 BoundaryEvent 边界计时时间 String processDefinitionId = event.getProcessDefinitionId(); diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java index 5ac1933aa..0d93fdfac 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java @@ -3,27 +3,116 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.core.util; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; import cn.iocoder.yudao.framework.common.util.number.NumberUtils; import cn.iocoder.yudao.framework.common.util.string.StrUtils; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO; import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.BpmTaskRespVO; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskApproveTypeEnum; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskAssignEmptyHandlerTypeEnum; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskAssignStartUserHandlerTypeEnum; import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskRejectHandlerType; import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants; +import com.google.common.collect.Maps; +import lombok.extern.slf4j.Slf4j; import org.flowable.bpmn.converter.BpmnXMLConverter; import org.flowable.bpmn.model.Process; import org.flowable.bpmn.model.*; +import org.flowable.common.engine.api.FlowableException; import org.flowable.common.engine.impl.util.io.BytesStreamSource; import java.util.*; import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.*; import static org.flowable.bpmn.constants.BpmnXMLConstants.FLOWABLE_EXTENSIONS_NAMESPACE; +import static org.flowable.bpmn.constants.BpmnXMLConstants.FLOWABLE_EXTENSIONS_PREFIX; /** - * 流程模型转操作工具类 + * BPMN Model 操作工具类。目前分成三部分: + * + * 1. BPMN 修改 + 解析元素相关的方法 + * 2. BPMN 简单查找相关的方法 + * 3. BPMN 复杂遍历相关的方法 + * 4. BPMN 流程预测相关的方法 + * + * @author 芋道源码 */ +@Slf4j public class BpmnModelUtils { + // ========== BPMN 修改 + 解析元素相关的方法 ========== + + public static void addExtensionElement(FlowElement element, String name, String value) { + if (value == null) { + return; + } + ExtensionElement extensionElement = new ExtensionElement(); + extensionElement.setNamespace(FLOWABLE_EXTENSIONS_NAMESPACE); + extensionElement.setNamespacePrefix(FLOWABLE_EXTENSIONS_PREFIX); + extensionElement.setElementText(value); + extensionElement.setName(name); + element.addExtensionElement(extensionElement); + } + + public static void addExtensionElement(FlowElement element, String name, Integer value) { + if (value == null) { + return; + } + addExtensionElement(element, name, String.valueOf(value)); + } + + public static void addExtensionElement(FlowElement element, String name, Map attributes) { + if (attributes == null) { + return; + } + ExtensionElement extensionElement = new ExtensionElement(); + extensionElement.setNamespace(FLOWABLE_EXTENSIONS_NAMESPACE); + extensionElement.setNamespacePrefix(FLOWABLE_EXTENSIONS_PREFIX); + extensionElement.setName(name); + attributes.forEach((key, value) -> { + ExtensionAttribute extensionAttribute = new ExtensionAttribute(key, value); + extensionAttribute.setNamespace(FLOWABLE_EXTENSIONS_NAMESPACE); + extensionElement.addAttribute(extensionAttribute); + }); + element.addExtensionElement(extensionElement); + } + + /** + * 解析扩展元素 + * + * @param flowElement 节点 + * @param elementName 元素名称 + * @return 扩展元素 + */ + public static String parseExtensionElement(FlowElement flowElement, String elementName) { + if (flowElement == null) { + return null; + } + ExtensionElement element = CollUtil.getFirst(flowElement.getExtensionElements().get(elementName)); + return element != null ? element.getElementText() : null; + } + + /** + * 给节点添加候选人元素 + * + * @param candidateStrategy 候选人策略 + * @param candidateParam 候选人参数,允许空 + * @param flowElement 节点 + */ + public static void addCandidateElements(Integer candidateStrategy, String candidateParam, FlowElement flowElement) { + addExtensionElement(flowElement, BpmnModelConstants.USER_TASK_CANDIDATE_STRATEGY, + candidateStrategy == null ? null : candidateStrategy.toString()); + addExtensionElement(flowElement, BpmnModelConstants.USER_TASK_CANDIDATE_PARAM, candidateParam); + } + + /** + * 解析候选人策略 + * + * @param userTask 任务节点 + * @return 候选人策略 + */ public static Integer parseCandidateStrategy(FlowElement userTask) { Integer candidateStrategy = NumberUtils.parseInt(userTask.getAttributeValue( BpmnModelConstants.NAMESPACE, BpmnModelConstants.USER_TASK_CANDIDATE_STRATEGY)); @@ -35,6 +124,12 @@ public class BpmnModelUtils { return candidateStrategy; } + /** + * 解析候选人参数 + * + * @param userTask 任务节点 + * @return 候选人参数 + */ public static String parseCandidateParam(FlowElement userTask) { String candidateParam = userTask.getAttributeValue( BpmnModelConstants.NAMESPACE, BpmnModelConstants.USER_TASK_CANDIDATE_PARAM); @@ -45,39 +140,130 @@ public class BpmnModelUtils { return candidateParam; } + /** + * 解析审批类型 + * + * @see BpmUserTaskApproveTypeEnum + * @param userTask 任务节点 + * @return 审批类型 + */ public static Integer parseApproveType(FlowElement userTask) { return NumberUtils.parseInt(parseExtensionElement(userTask, BpmnModelConstants.USER_TASK_APPROVE_TYPE)); } + /** + * 添加任务拒绝处理元素 + * + * @param rejectHandler 任务拒绝处理 + * @param userTask 任务节点 + */ + public static void addTaskRejectElements(BpmSimpleModelNodeVO.RejectHandler rejectHandler, UserTask userTask) { + if (rejectHandler == null) { + return; + } + addExtensionElement(userTask, USER_TASK_REJECT_HANDLER_TYPE, StrUtil.toStringOrNull(rejectHandler.getType())); + addExtensionElement(userTask, USER_TASK_REJECT_RETURN_TASK_ID, rejectHandler.getReturnNodeId()); + } + + /** + * 解析任务拒绝处理类型 + * + * @param userTask 任务节点 + * @return 任务拒绝处理类型 + */ public static BpmUserTaskRejectHandlerType parseRejectHandlerType(FlowElement userTask) { Integer rejectHandlerType = NumberUtils.parseInt(parseExtensionElement(userTask, USER_TASK_REJECT_HANDLER_TYPE)); return BpmUserTaskRejectHandlerType.typeOf(rejectHandlerType); } + /** + * 解析任务拒绝返回任务节点 ID + * + * @param flowElement 任务节点 + * @return 任务拒绝返回任务节点 ID + */ public static String parseReturnTaskId(FlowElement flowElement) { return parseExtensionElement(flowElement, USER_TASK_REJECT_RETURN_TASK_ID); } + /** + * 给节点添加用户任务的审批人与发起人相同时,处理类型枚举 + * + * @see BpmUserTaskAssignStartUserHandlerTypeEnum + * @param assignStartUserHandlerType 发起人处理类型 + * @param userTask 任务节点 + */ + public static void addAssignStartUserHandlerType(Integer assignStartUserHandlerType, UserTask userTask) { + if (assignStartUserHandlerType == null) { + return; + } + addExtensionElement(userTask, USER_TASK_ASSIGN_START_USER_HANDLER_TYPE, assignStartUserHandlerType.toString()); + } + + /** + * 给节点添加用户任务的审批人为空时,处理类型枚举 + * + * @see BpmUserTaskAssignEmptyHandlerTypeEnum + * @param emptyHandler 空处理 + * @param userTask 任务节点 + */ + public static void addAssignEmptyHandlerType(BpmSimpleModelNodeVO.AssignEmptyHandler emptyHandler, UserTask userTask) { + if (emptyHandler == null) { + return; + } + addExtensionElement(userTask, USER_TASK_ASSIGN_EMPTY_HANDLER_TYPE, StrUtil.toStringOrNull(emptyHandler.getType())); + addExtensionElement(userTask, USER_TASK_ASSIGN_USER_IDS, StrUtil.join(",", emptyHandler.getUserIds())); + } + + /** + * 解析用户任务的审批人与发起人相同时,处理类型枚举 + * + * @param userTask 任务节点 + * @return 处理类型枚举 + */ public static Integer parseAssignStartUserHandlerType(FlowElement userTask) { return NumberUtils.parseInt(parseExtensionElement(userTask, USER_TASK_ASSIGN_START_USER_HANDLER_TYPE)); } + /** + * 解析用户任务的审批人为空时,处理类型枚举 + * + * @param userTask 任务节点 + * @return 处理类型枚举 + */ public static Integer parseAssignEmptyHandlerType(FlowElement userTask) { return NumberUtils.parseInt(parseExtensionElement(userTask, USER_TASK_ASSIGN_EMPTY_HANDLER_TYPE)); } + /** + * 解析用户任务的审批人为空时,处理用户 ID 数组 + * + * @param userTask 任务节点 + * @return 处理用户 ID 数组 + */ public static List parseAssignEmptyHandlerUserIds(FlowElement userTask) { return StrUtils.splitToLong(parseExtensionElement(userTask, USER_TASK_ASSIGN_USER_IDS), ","); } - public static String parseExtensionElement(FlowElement flowElement, String elementName) { - if (flowElement == null) { - return null; + /** + * 给节点添加表单字段权限元素 + * + * @param fieldsPermissions 表单字段权限 + * @param flowElement 节点 + */ + public static void addFormFieldsPermission(List> fieldsPermissions, FlowElement flowElement) { + if (CollUtil.isNotEmpty(fieldsPermissions)) { + fieldsPermissions.forEach(item -> addExtensionElement(flowElement, FORM_FIELD_PERMISSION_ELEMENT, item)); } - ExtensionElement element = CollUtil.getFirst(flowElement.getExtensionElements().get(elementName)); - return element != null ? element.getElementText() : null; } + /** + * 解析表单字段权限 + * + * @param bpmnModel bpmnModel 对象 + * @param flowElementId 元素 ID + * @return 表单字段权限 + */ public static Map parseFormFieldsPermission(BpmnModel bpmnModel, String flowElementId) { if (bpmnModel == null || StrUtil.isEmpty(flowElementId)) { return null; @@ -101,6 +287,29 @@ public class BpmnModelUtils { return fieldsPermission; } + /** + * 给节点添加操作按钮设置元素 + */ + public static void addButtonsSetting(List buttonsSetting, UserTask userTask) { + if (CollUtil.isNotEmpty(buttonsSetting)) { + List> list = CollectionUtils.convertList(buttonsSetting, item -> { + Map settingMap = Maps.newHashMapWithExpectedSize(3); + settingMap.put(BUTTON_SETTING_ELEMENT_ID_ATTRIBUTE, String.valueOf(item.getId())); + settingMap.put(BUTTON_SETTING_ELEMENT_DISPLAY_NAME_ATTRIBUTE, item.getDisplayName()); + settingMap.put(BUTTON_SETTING_ELEMENT_ENABLE_ATTRIBUTE, String.valueOf(item.getEnable())); + return settingMap; + }); + list.forEach(item -> addExtensionElement(userTask, BUTTON_SETTING_ELEMENT, item)); + } + } + + /** + * 解析操作按钮设置 + * + * @param bpmnModel bpmnModel 对象 + * @param flowElementId 元素 ID + * @return 操作按钮设置 + */ public static Map parseButtonsSetting(BpmnModel bpmnModel, String flowElementId) { FlowElement flowElement = getFlowElementById(bpmnModel, flowElementId); if (flowElement == null) { @@ -110,7 +319,7 @@ public class BpmnModelUtils { if (CollUtil.isEmpty(extensionElements)) { return null; } - Map buttonSettings = MapUtil.newHashMap(16); + Map buttonSettings = Maps.newHashMapWithExpectedSize(extensionElements.size()); extensionElements.forEach(element -> { String id = element.getAttributeValue(FLOWABLE_EXTENSIONS_NAMESPACE, BUTTON_SETTING_ELEMENT_ID_ATTRIBUTE); String displayName = element.getAttributeValue(FLOWABLE_EXTENSIONS_NAMESPACE, BUTTON_SETTING_ELEMENT_DISPLAY_NAME_ATTRIBUTE); @@ -123,6 +332,23 @@ public class BpmnModelUtils { return buttonSettings; } + /** + * 解析边界事件扩展元素 + * + * @param boundaryEvent 边界事件 + * @param customElement 元素 + * @return 扩展元素 + */ + public static String parseBoundaryEventExtensionElement(BoundaryEvent boundaryEvent, String customElement) { + if (boundaryEvent == null) { + return null; + } + ExtensionElement extensionElement = CollUtil.getFirst(boundaryEvent.getExtensionElements().get(customElement)); + return Optional.ofNullable(extensionElement).map(ExtensionElement::getElementText).orElse(null); + } + + // ========== BPM 简单查找相关的方法 ========== + /** * 根据节点,获取入口连线 * @@ -168,15 +394,14 @@ public class BpmnModelUtils { * @param clazz 指定元素。例如说,{@link UserTask}、{@link Gateway} 等等 * @return 元素们 */ + @SuppressWarnings("unchecked") public static List getBpmnModelElements(BpmnModel model, Class clazz) { List result = new ArrayList<>(); - model.getProcesses().forEach(process -> { - process.getFlowElements().forEach(flowElement -> { - if (flowElement.getClass().isAssignableFrom(clazz)) { - result.add((T) flowElement); - } - }); - }); + model.getProcesses().forEach(process -> process.getFlowElements().forEach(flowElement -> { + if (flowElement.getClass().isAssignableFrom(clazz)) { + result.add((T) flowElement); + } + })); return result; } @@ -193,7 +418,7 @@ public class BpmnModelUtils { public static EndEvent getEndEvent(BpmnModel model) { Process process = model.getMainProcess(); - // 从 flowElementList 找 endEvent. TODO 多个 EndEvent 会有问题 + // 从 flowElementList 找 endEvent return (EndEvent) CollUtil.findOne(process.getFlowElements(), flowElement -> flowElement instanceof EndEvent); } @@ -221,7 +446,7 @@ public class BpmnModelUtils { return StrUtil.utf8Str(bpmnBytes); } - // ========== 遍历相关的方法 ========== + // ========== BPMN 复杂遍历相关的方法 ========== /** * 找到 source 节点之前的所有用户任务节点 @@ -316,16 +541,16 @@ public class BpmnModelUtils { return userTaskList; } - /** * 迭代从后向前扫描,判断目标节点相对于当前节点是否是串行 - * 不存在直接回退到子流程中的情况,但存在从子流程出去到父流程情况 + * 不存在直接退回到子流程中的情况,但存在从子流程出去到父流程情况 * * @param source 起始节点 * @param target 目标节点 * @param visitedElements 已经经过的连线的 ID,用于判断线路是否重复 * @return 结果 */ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") public static boolean isSequentialReachable(FlowElement source, FlowElement target, Set visitedElements) { visitedElements = visitedElements == null ? new HashSet<>() : visitedElements; // 不能是开始事件和子流程 @@ -436,11 +661,136 @@ public class BpmnModelUtils { return userTaskList; } - public static String parseBoundaryEventExtensionElement(BoundaryEvent boundaryEvent, String customElement) { - if (boundaryEvent == null) { - return null; - } - ExtensionElement extensionElement = CollUtil.getFirst(boundaryEvent.getExtensionElements().get(customElement)); - return Optional.ofNullable(extensionElement).map(ExtensionElement::getElementText).orElse(null); + // ========== BPMN 流程预测相关的方法 ========== + + /** + * 流程预测,返回 StartEvent、UserTask、ServiceTask、EndEvent 节点元素,最终是 List 串行结果 + * + * @param bpmnModel BPMN 图 + * @param variables 变量 + * @return 节点元素数组 + */ + public static List simulateProcess(BpmnModel bpmnModel, Map variables) { + List resultElements = new ArrayList<>(); + Set visitElements = new HashSet<>(); + + // 从 StartEvent 开始遍历 + StartEvent startEvent = getStartEvent(bpmnModel); + simulateNextFlowElements(startEvent, variables, resultElements, visitElements); + + // 将 EndEvent 放在末尾。原因是,DFS 遍历,可能 EndEvent 在 resultElements 中 + List endEvents = CollUtil.removeWithAddIf(resultElements, + flowElement -> flowElement instanceof EndEvent); + resultElements.addAll(endEvents); + return resultElements; } + + @SuppressWarnings("PatternVariableCanBeUsed") + private static void simulateNextFlowElements(FlowElement currentElement, Map variables, + List resultElements, Set visitElements) { + // 如果为空,或者已经遍历过,则直接结束 + if (currentElement == null) { + return; + } + if (visitElements.contains(currentElement)) { + return; + } + visitElements.add(currentElement); + + // 情况:StartEvent/EndEvent/UserTask/ServiceTask + if (currentElement instanceof StartEvent + || currentElement instanceof EndEvent + || currentElement instanceof UserTask + || currentElement instanceof ServiceTask) { + // 添加元素 + FlowNode flowNode = (FlowNode) currentElement; + resultElements.add(flowNode); + // 遍历子节点 + flowNode.getOutgoingFlows().forEach( + nextElement -> simulateNextFlowElements(nextElement.getTargetFlowElement(), variables, resultElements, visitElements)); + return; + } + + // 情况:ExclusiveGateway 排它,只有一个满足条件的。如果没有,就走默认的 + if (currentElement instanceof ExclusiveGateway) { + // 查找满足条件的 SequenceFlow 路径 + Gateway gateway = (Gateway) currentElement; + SequenceFlow matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(), + flow -> ObjUtil.notEqual(gateway.getDefaultFlow(), flow.getId()) + && evalConditionExpress(variables, flow.getConditionExpression())); + if (matchSequenceFlow == null) { + matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(), + flow -> ObjUtil.notEqual(gateway.getDefaultFlow(), flow.getId())); + // 特殊:没有默认的情况下,并且只有 1 个条件,则认为它是默认的 + if (matchSequenceFlow == null && gateway.getOutgoingFlows().size() == 1) { + matchSequenceFlow = gateway.getOutgoingFlows().get(0); + } + } + // 遍历满足条件的 SequenceFlow 路径 + if (matchSequenceFlow != null) { + simulateNextFlowElements(matchSequenceFlow.getTargetFlowElement(), variables, resultElements, visitElements); + } + return; + } + + // 情况:InclusiveGateway 包容,多个满足条件的。如果没有,就走默认的 + if (currentElement instanceof InclusiveGateway) { + // 查找满足条件的 SequenceFlow 路径 + Gateway gateway = (Gateway) currentElement; + Collection matchSequenceFlows = CollUtil.filterNew(gateway.getOutgoingFlows(), + flow -> ObjUtil.notEqual(gateway.getDefaultFlow(), flow.getId()) + && evalConditionExpress(variables, flow.getConditionExpression())); + if (CollUtil.isEmpty(matchSequenceFlows)) { + matchSequenceFlows = CollUtil.filterNew(gateway.getOutgoingFlows(), + flow -> ObjUtil.notEqual(gateway.getDefaultFlow(), flow.getId())); + // 特殊:没有默认的情况下,并且只有 1 个条件,则认为它是默认的 + if (CollUtil.isEmpty(matchSequenceFlows) && gateway.getOutgoingFlows().size() == 1) { + matchSequenceFlows = gateway.getOutgoingFlows(); + } + } + // 遍历满足条件的 SequenceFlow 路径 + matchSequenceFlows.forEach( + flow -> simulateNextFlowElements(flow.getTargetFlowElement(), variables, resultElements, visitElements)); + } + + // 情况:ParallelGateway 并行,都满足,都走 + if (currentElement instanceof ParallelGateway) { + Gateway gateway = (Gateway) currentElement; + // 遍历子节点 + gateway.getOutgoingFlows().forEach( + nextElement -> simulateNextFlowElements(nextElement.getTargetFlowElement(), variables, resultElements, visitElements)); + return; + } + } + + /** + * 计算条件表达式是否为 true 满足条件 + * + * @param variables 流程实例 + * @param express 条件表达式 + * @return 是否满足条件 + */ + public static boolean evalConditionExpress(Map variables, String express) { + if (express == null) { + return Boolean.FALSE; + } + try { + Object result = FlowableUtils.getExpressionValue(variables, express); + return Boolean.TRUE.equals(result); + } catch (FlowableException ex) { + log.error("[evalConditionExpress][条件表达式({}) 变量({}) 解析报错", express, variables, ex); + return Boolean.FALSE; + } + } + + @SuppressWarnings("PatternVariableCanBeUsed") + public static boolean isSequentialUserTask(FlowElement flowElement) { + if (!(flowElement instanceof UserTask)) { + return false; + } + UserTask userTask = (UserTask) flowElement; + MultiInstanceLoopCharacteristics loopCharacteristics = userTask.getLoopCharacteristics(); + return loopCharacteristics != null && loopCharacteristics.isSequential(); + } + } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/FlowableUtils.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/FlowableUtils.java index 6456c943a..6b7a99bbd 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/FlowableUtils.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/FlowableUtils.java @@ -1,6 +1,7 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.core.util; import cn.hutool.core.util.ObjectUtil; +import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants; @@ -8,6 +9,8 @@ import org.flowable.common.engine.api.delegate.Expression; import org.flowable.common.engine.api.variable.VariableContainer; import org.flowable.common.engine.impl.el.ExpressionManager; import org.flowable.common.engine.impl.identity.Authentication; +import org.flowable.common.engine.impl.variable.MapDelegateVariableContainer; +import org.flowable.engine.ManagementService; import org.flowable.engine.ProcessEngineConfiguration; import org.flowable.engine.history.HistoricProcessInstance; import org.flowable.engine.impl.cfg.ProcessEngineConfigurationImpl; @@ -94,6 +97,27 @@ public class FlowableUtils { return (Integer) processVariables.get(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS); } + /** + * 获得流程实例的审批原因 + * + * @param processInstance 流程实例 + * @return 审批原因 + */ + public static String getProcessInstanceReason(HistoricProcessInstance processInstance) { + return (String) processInstance.getProcessVariables().get(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_REASON); + } + + /** + * 获得流程实例的表单 + * + * @param processInstance 流程实例 + * @return 表单 + */ + public static Map getProcessInstanceFormVariable(ProcessInstance processInstance) { + Map processVariables = new HashMap<>(processInstance.getProcessVariables()); + return filterProcessInstanceFormVariable(processVariables); + } + /** * 获得流程实例的表单 * @@ -101,9 +125,8 @@ public class FlowableUtils { * @return 表单 */ public static Map getProcessInstanceFormVariable(HistoricProcessInstance processInstance) { - Map formVariables = new HashMap<>(processInstance.getProcessVariables()); - filterProcessInstanceFormVariable(formVariables); - return formVariables; + Map processVariables = new HashMap<>(processInstance.getProcessVariables()); + return filterProcessInstanceFormVariable(processVariables); } /** @@ -125,9 +148,22 @@ public class FlowableUtils { * @param processInstance 流程实例 * @return 发起用户选择的审批人 Map */ - @SuppressWarnings("unchecked") public static Map> getStartUserSelectAssignees(ProcessInstance processInstance) { - return (Map>) processInstance.getProcessVariables().get( + return processInstance != null ? getStartUserSelectAssignees(processInstance.getProcessVariables()) : null; + } + + /** + * 获得流程实例的发起用户选择的审批人 Map + * + * @param processVariables 流程变量 + * @return 发起用户选择的审批人 Map + */ + @SuppressWarnings("unchecked") + public static Map> getStartUserSelectAssignees(Map processVariables) { + if (processVariables == null) { + return null; + } + return (Map>) processVariables.get( BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES); } @@ -181,13 +217,30 @@ public class FlowableUtils { // ========== Expression 相关的工具方法 ========== - public static Object getExpressionValue(VariableContainer variableContainer, String expressionString) { - ProcessEngineConfigurationImpl processEngineConfiguration = CommandContextUtil.getProcessEngineConfiguration(); - assert processEngineConfiguration != null; + private static Object getExpressionValue(VariableContainer variableContainer, String expressionString, + ProcessEngineConfigurationImpl processEngineConfiguration) { + assert processEngineConfiguration!= null; ExpressionManager expressionManager = processEngineConfiguration.getExpressionManager(); - assert expressionManager != null; + assert expressionManager!= null; Expression expression = expressionManager.createExpression(expressionString); return expression.getValue(variableContainer); } + public static Object getExpressionValue(VariableContainer variableContainer, String expressionString) { + ProcessEngineConfigurationImpl processEngineConfiguration = CommandContextUtil.getProcessEngineConfiguration(); + if (processEngineConfiguration != null) { + return getExpressionValue(variableContainer, expressionString, processEngineConfiguration); + } + // 如果 ProcessEngineConfigurationImpl 获取不到,则需要通过 ManagementService 来获取 + ManagementService managementService = SpringUtil.getBean(ManagementService.class); + assert managementService != null; + return managementService.executeCommand(context -> + getExpressionValue(variableContainer, expressionString, CommandContextUtil.getProcessEngineConfiguration())); + } + + public static Object getExpressionValue(Map variable, String expressionString) { + VariableContainer variableContainer = new MapDelegateVariableContainer(variable, VariableContainer.empty()); + return getExpressionValue(variableContainer, expressionString); + } + } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java index f9e74e981..28a1ef124 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java @@ -7,64 +7,47 @@ import cn.hutool.core.util.*; import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO; import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO.ConditionGroups; -import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO.RejectHandler; -import cn.iocoder.yudao.module.bpm.enums.definition.BpmBoundaryEventType; -import cn.iocoder.yudao.module.bpm.enums.definition.BpmSimpleModeConditionType; -import cn.iocoder.yudao.module.bpm.enums.definition.BpmSimpleModelNodeType; -import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskApproveMethodEnum; +import cn.iocoder.yudao.module.bpm.enums.definition.*; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants; import cn.iocoder.yudao.module.bpm.framework.flowable.core.listener.BpmCopyTaskDelegate; import org.flowable.bpmn.BpmnAutoLayout; +import org.flowable.bpmn.constants.BpmnXMLConstants; import org.flowable.bpmn.model.Process; import org.flowable.bpmn.model.*; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Objects; +import java.util.*; -import static cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO.OperationButtonSetting; -import static cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO.TimeoutHandler; -import static cn.iocoder.yudao.module.bpm.enums.definition.BpmSimpleModelNodeType.*; -import static cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskApproveMethodEnum.*; -import static cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskApproveTypeEnum.USER; -import static cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskAssignStartUserHandlerTypeEnum.SKIP; -import static cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskTimeoutHandlerTypeEnum.REMINDER; -import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum.START_USER; import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.*; -import static org.flowable.bpmn.constants.BpmnXMLConstants.*; +import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.*; +import static java.util.Arrays.asList; /** - * 仿钉钉快搭模型相关的工具方法 + * 仿钉钉/飞书的模型相关的工具方法 + *

+ * 1. 核心的逻辑实现,可见 {@link #buildBpmnModel(String, String, BpmSimpleModelNodeVO)} 方法 + * 2. 所有的 BpmSimpleModelNodeVO 转换成 BPMN FlowNode 元素,可见 {@link NodeConvert} 实现类 * * @author jason */ public class SimpleModelUtils { - /** - * 聚合网关节点 Id 后缀 - */ - public static final String JOIN_GATE_WAY_NODE_ID_SUFFIX = "_join"; + private static final Map NODE_CONVERTS = MapUtil.newHashMap(); + + static { + List converts = asList(new StartNodeConvert(), new EndNodeConvert(), + new StartUserNodeConvert(), new ApproveNodeConvert(), new CopyNodeConvert(), + new ConditionBranchNodeConvert(), new ParallelBranchNodeConvert(), new InclusiveBranchNodeConvert()); + converts.forEach(convert -> NODE_CONVERTS.put(convert.getType(), convert)); + } /** - * 所有审批人同意的表达式 - */ - public static final String ALL_APPROVE_COMPLETE_EXPRESSION = "${ nrOfCompletedInstances >= nrOfInstances }"; - - /** - * 任一一名审批人同意的表达式 - */ - public static final String ANY_OF_APPROVE_COMPLETE_EXPRESSION = "${ nrOfCompletedInstances > 0 }"; - - /** - * 按通过比例完成表达式 - */ - public static final String APPROVE_BY_RATIO_COMPLETE_EXPRESSION = "${ nrOfCompletedInstances/nrOfInstances >= %s}"; - - // TODO @yunai:注释需要完善下; - - /** - * 仿钉钉流程设计模型数据结构(json) 转换成 Bpmn Model (待完善) + * 仿钉钉流程设计模型数据结构(json)转换成 Bpmn Model + *

+ * 整体逻辑如下: + * 1. 创建:BpmnModel、Process 对象 + * 2. 转换:将 BpmSimpleModelNodeVO 转换成 BPMN FlowNode 元素 + * 3. 连接:构建并添加节点之间的连线 Sequence Flow * * @param processId 流程标识 * @param processName 流程名称 @@ -72,42 +55,76 @@ public class SimpleModelUtils { * @return Bpmn Model */ public static BpmnModel buildBpmnModel(String processId, String processName, BpmSimpleModelNodeVO simpleModelNode) { + // 1. 创建 BpmnModel BpmnModel bpmnModel = new BpmnModel(); - // 不加这个 解析 Message 会报 NPE 异常 . - bpmnModel.setTargetNamespace(BPMN2_NAMESPACE); // TODO @jason:待定:是不是搞个自定义的 namespace; - // TODO 芋艿:后续在 review - + bpmnModel.setTargetNamespace(BpmnXMLConstants.BPMN2_NAMESPACE); // 设置命名空间。不加这个,解析 Message 会报 NPE 异常 + // 创建 Process 对象 Process process = new Process(); process.setId(processId); process.setName(processName); - process.setExecutable(Boolean.TRUE); // TODO @jason:这个是必须设置的么? + process.setExecutable(Boolean.TRUE); bpmnModel.addProcess(process); - // TODO 芋艿:这里可能纠结下,到底前端传递,还是后端创建出来。 - // 目前前端的第一个节点是“发起人节点”。这里构建一个 StartNode,用于创建 Bpmn 的 StartEvent 节点 - BpmSimpleModelNodeVO startNode = buildStartSimpleModelNode(); + // 2.1 创建 StartNode 节点 + // 原因是:目前前端的第一个节点是“发起人节点”,所以这里构建一个 StartNode,用于创建 Bpmn 的 StartEvent 节点 + BpmSimpleModelNodeVO startNode = buildStartNode(); startNode.setChildNode(simpleModelNode); - // 从 前端模型数据结构 SimpleModel 构建 FlowNode 并添加到 Main Process + // 2.2 将前端传递的 simpleModelNode 数据结构(json),转换成从 BPMN FlowNode 元素,并添加到 Main Process 中 traverseNodeToBuildFlowNode(startNode, process); - // 找到 end event - EndEvent endEvent = (EndEvent) CollUtil.findOne(process.getFlowElements(), item -> item instanceof EndEvent); - // 构建并添加节点之间的连线 Sequence Flow + // 3. 构建并添加节点之间的连线 Sequence Flow + EndEvent endEvent = BpmnModelUtils.getEndEvent(bpmnModel); traverseNodeToBuildSequenceFlow(process, startNode, endEvent.getId()); - // 自动布局 + + // 4. 自动布局 new BpmnAutoLayout(bpmnModel).execute(); return bpmnModel; } - private static BpmSimpleModelNodeVO buildStartSimpleModelNode() { - BpmSimpleModelNodeVO startNode = new BpmSimpleModelNodeVO(); - startNode.setId(START_EVENT_NODE_ID); - startNode.setName(START_EVENT_NODE_NAME); - startNode.setType(START_NODE.getType()); - return startNode; + private static BpmSimpleModelNodeVO buildStartNode() { + return new BpmSimpleModelNodeVO().setId(START_EVENT_NODE_ID) + .setName(BpmSimpleModelNodeType.START_USER_NODE.getName()) + .setType(BpmSimpleModelNodeType.START_NODE.getType()); } - // TODO @芋艿:在优化下这个注释 + /** + * 遍历节点,构建 FlowNode 元素 + * + * @param node SIMPLE 节点 + * @param process BPMN 流程 + */ + private static void traverseNodeToBuildFlowNode(BpmSimpleModelNodeVO node, Process process) { + // 1. 判断是否有效节点 + if (!isValidNode(node)) { + return; + } + BpmSimpleModelNodeType nodeType = BpmSimpleModelNodeType.valueOf(node.getType()); + Assert.notNull(nodeType, "模型节点类型({})不支持", node.getType()); + + // 2. 处理当前节点 + NodeConvert nodeConvert = NODE_CONVERTS.get(nodeType); + Assert.notNull(nodeConvert, "模型节点类型的转换器({})不存在", node.getType()); + List flowElements = nodeConvert.convertList(node); + flowElements.forEach(process::addFlowElement); + + // 3.1 情况一:如果当前是分支节点,并且存在条件节点,则处理每个条件的子节点 + if (BpmSimpleModelNodeType.isBranchNode(node.getType()) + && CollUtil.isNotEmpty(node.getConditionNodes())) { + // 注意:这里的 item.getChildNode() 处理的是每个条件的子节点,不是处理条件 + node.getConditionNodes().forEach(item -> traverseNodeToBuildFlowNode(item.getChildNode(), process)); + } + + // 3.2 情况二:如果有“子”节点,则递归处理子节点 + traverseNodeToBuildFlowNode(node.getChildNode(), process); + } + + /** + * 遍历节点,构建 SequenceFlow 元素 + * + * @param process Bpmn 流程 + * @param node 当前节点 + * @param targetNodeId 目标节点 ID + */ private static void traverseNodeToBuildSequenceFlow(Process process, BpmSimpleModelNodeVO node, String targetNodeId) { // 1.1 无效节点返回 if (!isValidNode(node)) { @@ -116,516 +133,555 @@ public class SimpleModelUtils { // 1.2 END_NODE 直接返回 BpmSimpleModelNodeType nodeType = BpmSimpleModelNodeType.valueOf(node.getType()); Assert.notNull(nodeType, "模型节点类型不支持"); - if (nodeType == END_NODE) { + if (nodeType == BpmSimpleModelNodeType.END_NODE) { return; } + // 2.1 情况一:普通节点 - BpmSimpleModelNodeVO childNode = node.getChildNode(); if (!BpmSimpleModelNodeType.isBranchNode(node.getType())) { - if (!isValidNode(childNode)) { - // 2.1.1 普通节点且无孩子节点。分两种情况 - // a.结束节点 b. 条件分支的最后一个节点.与分支节点的孩子节点或聚合节点建立连线。 - if (StrUtil.isNotEmpty(node.getAttachNodeId())) { - // 2.1.1.1 如果有附加节点. 需要先建立和附加节点的连线。再建立附加节点和目标节点的连线 - List sequenceFlows = buildAttachNodeSequenceFlow(node.getId(), node.getAttachNodeId(), targetNodeId); - sequenceFlows.forEach(process::addFlowElement); - } else { - SequenceFlow sequenceFlow = buildBpmnSequenceFlow(node.getId(), targetNodeId, null, null, null); - process.addFlowElement(sequenceFlow); - } - } else { - // 2.1.2 普通节点且有孩子节点。建立连线 - if (StrUtil.isNotEmpty(node.getAttachNodeId())) { - // 2.1.1.2 如果有附加节点. 需要先建立和附加节点的连线。再建立附加节点和目标节点的连线 - List sequenceFlows = buildAttachNodeSequenceFlow(node.getId(), node.getAttachNodeId(), childNode.getId()); - sequenceFlows.forEach(process::addFlowElement); - } else { - SequenceFlow sequenceFlow = buildBpmnSequenceFlow(node.getId(), childNode.getId(), null, null, null); - process.addFlowElement(sequenceFlow); - } - // 递归调用后续节点 - traverseNodeToBuildSequenceFlow(process, childNode, targetNodeId); - } + traverseNormalNodeToBuildSequenceFlow(process, node, targetNodeId); } else { // 2.2 情况二:分支节点 - List conditionNodes = node.getConditionNodes(); - Assert.notEmpty(conditionNodes, "分支节点的条件节点不能为空"); - // 分支终点节点 Id - String branchEndNodeId = null; - if (nodeType == CONDITION_BRANCH_NODE) { // 条件分支 - // 分两种情况 1. 分支节点有孩子节点为孩子节点 Id 2. 分支节点孩子为无效节点时 (分支嵌套且为分支最后一个节点) 为分支终点节点Id - branchEndNodeId = isValidNode(childNode) ? childNode.getId() : targetNodeId; - } else if (nodeType == PARALLEL_BRANCH_NODE) { // 并行分支 - // 分支节点:分支终点节点 Id 为程序创建的网关集合节点。目前不会从前端传入。 - branchEndNodeId = node.getId() + JOIN_GATE_WAY_NODE_ID_SUFFIX; - } - // TODO 包容网关待实现 - Assert.notEmpty(branchEndNodeId, "分支终点节点 Id 不能为空"); - // 3.1 遍历分支节点. 如下情况: - // 分支1、A->B->C->D->E 和 分支2、A->D->E。 A为分支节点, D为A孩子节点 - for (BpmSimpleModelNodeVO item : conditionNodes) { - // TODO @jason:条件分支的情况下,需要分 item 搞的条件,和 conditionNodes 搞的条件 - // @芋艿 这个是啥意思。 这里的 item 的节点类型为 BpmSimpleModelNodeType.CONDITION_NODE 类型,没有对应的 bpmn 的节点。 仅仅用于构建条件表达式。 - Assert.isTrue(Objects.equals(item.getType(), CONDITION_NODE.getType()), "条件节点类型不符合"); - // 构建表达式,可以为空. 并行分支为空 - String conditionExpression = buildConditionExpression(item); - BpmSimpleModelNodeVO nextNodeOnCondition = item.getChildNode(); - // 3.2 分支有后续节点, 分支1: A->B->C->D - if (isValidNode(nextNodeOnCondition)) { - // 3.2.1 建立 A->B - SequenceFlow sequenceFlow = buildBpmnSequenceFlow(node.getId(), nextNodeOnCondition.getId(), - item.getId(), item.getName(), conditionExpression); - process.addFlowElement(sequenceFlow); - // 3.2.2 递归调用后续节点连线。 建立 B->C->D 的连线 - traverseNodeToBuildSequenceFlow(process, nextNodeOnCondition, branchEndNodeId); - } else { - // 3.3 分支无后续节点 建立 A->D - SequenceFlow sequenceFlow = buildBpmnSequenceFlow(node.getId(), branchEndNodeId, - item.getId(), item.getName(), conditionExpression); - process.addFlowElement(sequenceFlow); - } - } - // 如果是并行分支。由于是程序创建的聚合网关。需要手工创建聚合网关和下一个节点的连线 - if (nodeType == PARALLEL_BRANCH_NODE) { - String nextNodeId = isValidNode(childNode) ? childNode.getId() : targetNodeId; - SequenceFlow sequenceFlow = buildBpmnSequenceFlow(branchEndNodeId, nextNodeId, null, null, null); - process.addFlowElement(sequenceFlow); - } - // 4.递归调用后续节点 继续递归建立 D->E 的连线 + traverseBranchNodeToBuildSequenceFlow(process, node, targetNodeId); + } + } + + /** + * 遍历普通(非条件)节点,构建 SequenceFlow 元素 + * + * @param process Bpmn 流程 + * @param node 当前节点 + * @param targetNodeId 目标节点 ID + */ + private static void traverseNormalNodeToBuildSequenceFlow(Process process, BpmSimpleModelNodeVO node, String targetNodeId) { + BpmSimpleModelNodeVO childNode = node.getChildNode(); + boolean isChildNodeValid = isValidNode(childNode); + // 情况一:有“子”节点,则建立连线 + // 情况二:没有“子节点”,则直接跟 targetNodeId 建立连线。例如说,结束节点、条件分支(分支节点的孩子节点或聚合节点)的最后一个节点 + String finalTargetNodeId = isChildNodeValid? childNode.getId() : targetNodeId; + SequenceFlow sequenceFlow = buildBpmnSequenceFlow(node.getId(), finalTargetNodeId); + process.addFlowElement(sequenceFlow); + + // 因为有子节点,递归调用后续子节点 + if (isChildNodeValid) { traverseNodeToBuildSequenceFlow(process, childNode, targetNodeId); } } /** - * 构建有附加节点的连线 + * 遍历条件节点,构建 SequenceFlow 元素 * - * @param nodeId 当前节点 Id - * @param attachNodeId 附属节点 Id - * @param targetNodeId 目标节点 Id + * @param process Bpmn 流程 + * @param node 当前节点 + * @param targetNodeId 目标节点 ID */ - private static List buildAttachNodeSequenceFlow(String nodeId, String attachNodeId, String targetNodeId) { - SequenceFlow sequenceFlow = buildBpmnSequenceFlow(nodeId, attachNodeId, null, null, null); - SequenceFlow attachSequenceFlow = buildBpmnSequenceFlow(attachNodeId, targetNodeId, null, null, null); - return CollUtil.newArrayList(sequenceFlow, attachSequenceFlow); - } + private static void traverseBranchNodeToBuildSequenceFlow(Process process, BpmSimpleModelNodeVO node, String targetNodeId) { + BpmSimpleModelNodeType nodeType = BpmSimpleModelNodeType.valueOf(node.getType()); + BpmSimpleModelNodeVO childNode = node.getChildNode(); + List conditionNodes = node.getConditionNodes(); + Assert.notEmpty(conditionNodes, "分支节点的条件节点不能为空"); + // 分支终点节点 ID + String branchEndNodeId = null; + if (nodeType == BpmSimpleModelNodeType.CONDITION_BRANCH_NODE) { // 条件分支 + // 分两种情况 1. 分支节点有孩子节点为孩子节点 Id 2. 分支节点孩子为无效节点时 (分支嵌套且为分支最后一个节点) 为分支终点节点 ID + branchEndNodeId = isValidNode(childNode) ? childNode.getId() : targetNodeId; + } else if (nodeType == BpmSimpleModelNodeType.PARALLEL_BRANCH_NODE + || nodeType == BpmSimpleModelNodeType.INCLUSIVE_BRANCH_NODE) { // 并行分支或包容分支 + // 分支节点:分支终点节点 Id 为程序创建的网关集合节点。目前不会从前端传入。 + branchEndNodeId = buildGatewayJoinId(node.getId()); + } + Assert.notEmpty(branchEndNodeId, "分支终点节点 Id 不能为空"); - /** - * 构造条件表达式 - * - * @param conditionNode 条件节点 - */ - public static String buildConditionExpression(BpmSimpleModelNodeVO conditionNode) { - BpmSimpleModeConditionType conditionTypeEnum = BpmSimpleModeConditionType.valueOf(conditionNode.getConditionType()); - String conditionExpression = null; - if (conditionTypeEnum == BpmSimpleModeConditionType.EXPRESSION) { - conditionExpression = conditionNode.getConditionExpression(); - } else if (conditionTypeEnum == BpmSimpleModeConditionType.RULE) { - ConditionGroups conditionGroups = conditionNode.getConditionGroups(); - if (conditionGroups != null && CollUtil.isNotEmpty(conditionGroups.getConditions())) { - List strConditionGroups = conditionGroups.getConditions().stream().map(item -> { - if (CollUtil.isNotEmpty(item.getRules())) { - Boolean and = item.getAnd(); - List list = CollectionUtils.convertList(item.getRules(), (rule) -> { - // 如果非数值类型加引号 - String rightSide = NumberUtil.isNumber(rule.getRightSide()) ? rule.getRightSide() : "\"" + rule.getRightSide() + "\""; - return String.format(" %s %s var:convertByType(%s,%s)", rule.getLeftSide(), rule.getOpCode(), rule.getLeftSide(), rightSide); - }); - return "(" + CollUtil.join(list, and ? " && " : " || ") + ")"; - } else { - return ""; - } - }).toList(); - conditionExpression = String.format("${%s}", CollUtil.join(strConditionGroups, conditionGroups.getAnd() ? " && " : " || ")); + // 3. 遍历分支节点 + // 下面的注释,以如下情况举例子。分支 1:A->B->C->D->E,分支 2:A->D->E。其中,A 为分支节点, D 为 A 孩子节点 + for (BpmSimpleModelNodeVO item : conditionNodes) { + Assert.isTrue(Objects.equals(item.getType(), BpmSimpleModelNodeType.CONDITION_NODE.getType()), + "条件节点类型({})不符合", item.getType()); + BpmSimpleModelNodeVO conditionChildNode = item.getChildNode(); + // 3.1 分支有后续节点。即分支 1: A->B->C->D 的情况 + if (isValidNode(conditionChildNode)) { + // 3.1.1 建立与后续的节点的连线。例如说,建立 A->B 的连线 + SequenceFlow sequenceFlow = ConditionNodeConvert.buildSequenceFlow(node.getId(), conditionChildNode.getId(), item); + process.addFlowElement(sequenceFlow); + // 3.1.2 递归调用后续节点连线。例如说,建立 B->C->D 的连线 + traverseNodeToBuildSequenceFlow(process, conditionChildNode, branchEndNodeId); + } else { + // 3.2 分支没有后续节点。例如说,建立 A->D 的连线 + SequenceFlow sequenceFlow = ConditionNodeConvert.buildSequenceFlow(node.getId(), branchEndNodeId, item); + process.addFlowElement(sequenceFlow); } } - // TODO 待增加其它类型 - return conditionExpression; + + // 4. 如果是并行分支、包容分支,由于是程序创建的聚合网关,需要手工创建聚合网关和下一个节点的连线 + if (nodeType == BpmSimpleModelNodeType.PARALLEL_BRANCH_NODE + || nodeType == BpmSimpleModelNodeType.INCLUSIVE_BRANCH_NODE ) { + String nextNodeId = isValidNode(childNode) ? childNode.getId() : targetNodeId; + SequenceFlow sequenceFlow = buildBpmnSequenceFlow(branchEndNodeId, nextNodeId); + process.addFlowElement(sequenceFlow); + } + + // 5. 递归调用后续节点 继续递归。例如说,建立 D->E 的连线 + traverseNodeToBuildSequenceFlow(process, childNode, targetNodeId); } - private static SequenceFlow buildBpmnSequenceFlow(String sourceId, String targetId, String seqFlowId, String seqName, String conditionExpression) { + private static SequenceFlow buildBpmnSequenceFlow(String sourceId, String targetId) { + return buildBpmnSequenceFlow(sourceId, targetId, null, null, null); + } + + private static SequenceFlow buildBpmnSequenceFlow(String sourceId, String targetId, + String sequenceFlowId, String sequenceFlowName, + String conditionExpression) { Assert.notEmpty(sourceId, "sourceId 不能为空"); Assert.notEmpty(targetId, "targetId 不能为空"); - // TODO @jason:如果 seqFlowId 不存在的时候,是不是要生成一个默认的 seqFlowId? @芋艿: 貌似不需要,Flowable 会默认生成 - // TODO @jason:如果 name 不存在的时候,是不是要生成一个默认的 name? @芋艿: 不需要生成默认的吧? 这个会在流程图展示的, 一般用户填写的。不好生成默认的吧 + // TODO @jason:如果 sequenceFlowId 不存在的时候,是不是要生成一个默认的 sequenceFlowId? @芋艿: 貌似不需要,Flowable 会默认生成;TODO @jason:建议还是搞一个,主要是后续好排查问题。 + // TODO @jason:如果 name 不存在的时候,是不是要生成一个默认的 name? @芋艿: 不需要生成默认的吧? 这个会在流程图展示的, 一般用户填写的。不好生成默认的吧;TODO @jason:建议还是搞一个,主要是后续好排查问题。 SequenceFlow sequenceFlow = new SequenceFlow(sourceId, targetId); + if (StrUtil.isNotEmpty(sequenceFlowId)) { + sequenceFlow.setId(sequenceFlowId); + } + if (StrUtil.isNotEmpty(sequenceFlowName)) { + sequenceFlow.setName(sequenceFlowName); + } if (StrUtil.isNotEmpty(conditionExpression)) { sequenceFlow.setConditionExpression(conditionExpression); } - if (StrUtil.isNotEmpty(seqFlowId)) { - sequenceFlow.setId(seqFlowId); - } - if (StrUtil.isNotEmpty(seqName)) { - sequenceFlow.setName(seqName); - } return sequenceFlow; } - // TODO @芋艿 改成了 traverseNodeToBuildFlowNode, 连线的叫 traverseNodeToBuildSequenceFlow - private static void traverseNodeToBuildFlowNode(BpmSimpleModelNodeVO node, Process process) { - // 判断是否有效节点 - if (!isValidNode(node)) { - return; - } - BpmSimpleModelNodeType nodeType = BpmSimpleModelNodeType.valueOf(node.getType()); - Assert.notNull(nodeType, "模型节点类型不支持"); - - List flowElements = buildFlowNode(node, nodeType); - flowElements.forEach(process::addFlowElement); - - // 如果不是网关类型的接口, 并且chileNode为空退出 - // 如果是“分支”节点,则递归处理条件 - if (BpmSimpleModelNodeType.isBranchNode(node.getType()) - && ArrayUtil.isNotEmpty(node.getConditionNodes())) { - node.getConditionNodes().forEach(item -> traverseNodeToBuildFlowNode(item.getChildNode(), process)); - } - - // 如果有“子”节点,则递归处理子节点 - traverseNodeToBuildFlowNode(node.getChildNode(), process); - } - public static boolean isValidNode(BpmSimpleModelNodeVO node) { return node != null && node.getId() != null; } public static boolean isSequentialApproveNode(BpmSimpleModelNodeVO node) { - return APPROVE_NODE.getType().equals(node.getType()) && SEQUENTIAL.getMethod().equals(node.getApproveMethod()); + return BpmSimpleModelNodeType.APPROVE_NODE.getType().equals(node.getType()) + && BpmUserTaskApproveMethodEnum.SEQUENTIAL.getMethod().equals(node.getApproveMethod()); } - private static List buildFlowNode(BpmSimpleModelNodeVO node, BpmSimpleModelNodeType nodeType) { - List list = new ArrayList<>(); - switch (nodeType) { - case START_NODE: { // 开始节点 - StartEvent startEvent = convertStartNode(node); - list.add(startEvent); - break; - } - case END_NODE: { // 结束节点 - EndEvent endEvent = convertEndNode(node); - list.add(endEvent); - break; - } - case START_USER_NODE: { // 发起人节点 - UserTask userTask = convertStartUserNode(node); - list.add(userTask); - break; - } - case APPROVE_NODE: { // 审批节点 - List flowElements = convertApproveNode(node); - list.addAll(flowElements); - break; - } - case COPY_NODE: { // 抄送节点 - ServiceTask serviceTask = convertCopyNode(node); - list.add(serviceTask); - break; - } - case CONDITION_BRANCH_NODE: { - ExclusiveGateway exclusiveGateway = convertConditionBranchNode(node); - list.add(exclusiveGateway); - break; - } - case PARALLEL_BRANCH_NODE: { - List parallelGateways = convertParallelBranchNode(node); - list.addAll(parallelGateways); - break; - } + // ========== 各种 convert 节点的方法: BpmSimpleModelNodeVO => BPMN FlowElement ========== - case INCLUSIVE_BRANCH_NODE: { - // TODO jason 待实现 - break; - } - default: { - // TODO 其它节点类型的实现 - } + private interface NodeConvert { + + default List convertList(BpmSimpleModelNodeVO node) { + return Collections.singletonList(convert(node)); } - return list; - } - private static UserTask convertStartUserNode(BpmSimpleModelNodeVO node) { - return buildBpmnStartUserTask(node); - } - - private static List convertApproveNode(BpmSimpleModelNodeVO node) { - List flowElements = new ArrayList<>(); - UserTask userTask = buildBpmnUserTask(node); - flowElements.add(userTask); - - // 添加用户任务的 Timer Boundary Event, 用于任务的审批超时处理 - if (node.getTimeoutHandler() != null && node.getTimeoutHandler().getEnable()) { - BoundaryEvent boundaryEvent = buildUserTaskTimeoutBoundaryEvent(userTask, node.getTimeoutHandler()); - flowElements.add(boundaryEvent); + default FlowElement convert(BpmSimpleModelNodeVO node) { + throw new UnsupportedOperationException("请实现该方法"); } - return flowElements; + + BpmSimpleModelNodeType getType(); + } - /** - * 添加 UserTask 用户的审批超时 BoundaryEvent 事件 - * - * @param userTask 审批任务 - * @param timeoutHandler 超时处理器 - * @return BoundaryEvent 超时事件 - */ - private static BoundaryEvent buildUserTaskTimeoutBoundaryEvent(UserTask userTask, TimeoutHandler timeoutHandler) { - // 1.1 定时器边界事件 - BoundaryEvent boundaryEvent = new BoundaryEvent(); - boundaryEvent.setId("Event-" + IdUtil.fastUUID()); - boundaryEvent.setCancelActivity(false); // 设置关联的任务为不会被中断 - boundaryEvent.setAttachedToRef(userTask); - // 1.2 定义超时时间、最大提醒次数 - TimerEventDefinition eventDefinition = new TimerEventDefinition(); - eventDefinition.setTimeDuration(timeoutHandler.getTimeDuration()); - if (Objects.equals(REMINDER.getType(), timeoutHandler.getType()) && - timeoutHandler.getMaxRemindCount() != null && timeoutHandler.getMaxRemindCount() > 1) { - eventDefinition.setTimeCycle(String.format("R%d/%s", - timeoutHandler.getMaxRemindCount(), timeoutHandler.getTimeDuration())); + private static class StartNodeConvert implements NodeConvert { + + @Override + public StartEvent convert(BpmSimpleModelNodeVO node) { + StartEvent startEvent = new StartEvent(); + startEvent.setId(node.getId()); + startEvent.setName(node.getName()); + return startEvent; } - boundaryEvent.addEventDefinition(eventDefinition); - // 2.1 添加定时器边界事件类型 - addExtensionElement(boundaryEvent, BOUNDARY_EVENT_TYPE, BpmBoundaryEventType.USER_TASK_TIMEOUT.getType().toString()); - // 2.2 添加超时执行动作元素 - addExtensionElement(boundaryEvent, USER_TASK_TIMEOUT_HANDLER_TYPE, StrUtil.toStringOrNull(timeoutHandler.getType())); - return boundaryEvent; - } - - private static List convertParallelBranchNode(BpmSimpleModelNodeVO node) { - ParallelGateway parallelGateway = new ParallelGateway(); - parallelGateway.setId(node.getId()); - // TODO @jason:setName - - // TODO @芋艿 + jason:合并网关;是不是要有条件啥的。微信讨论 - // 并行聚合网关有程序创建。前端不需要传入 - ParallelGateway joinParallelGateway = new ParallelGateway(); - joinParallelGateway.setId(node.getId() + JOIN_GATE_WAY_NODE_ID_SUFFIX); - return CollUtil.newArrayList(parallelGateway, joinParallelGateway); - } - - private static ServiceTask convertCopyNode(BpmSimpleModelNodeVO node) { - ServiceTask serviceTask = new ServiceTask(); - serviceTask.setId(node.getId()); - serviceTask.setName(node.getName()); - serviceTask.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_DELEGATEEXPRESSION); - serviceTask.setImplementation("${" + BpmCopyTaskDelegate.BEAN_NAME + "}"); - - // 添加抄送候选人元素 - addCandidateElements(node.getCandidateStrategy(), node.getCandidateParam(), serviceTask); - // 添加表单字段权限属性元素 - addFormFieldsPermission(node.getFieldsPermission(), serviceTask); - return serviceTask; - } - - /** - * 给节点添加候选人元素 - */ - private static void addCandidateElements(Integer candidateStrategy, String candidateParam, FlowElement flowElement) { - addExtensionElement(flowElement, BpmnModelConstants.USER_TASK_CANDIDATE_STRATEGY, - candidateStrategy == null ? null : candidateStrategy.toString()); - addExtensionElement(flowElement, BpmnModelConstants.USER_TASK_CANDIDATE_PARAM, candidateParam); - } - - private static ExclusiveGateway convertConditionBranchNode(BpmSimpleModelNodeVO node) { - Assert.notEmpty(node.getConditionNodes(), "条件分支节点不能为空"); - ExclusiveGateway exclusiveGateway = new ExclusiveGateway(); - exclusiveGateway.setId(node.getId()); - // 寻找默认的序列流 - BpmSimpleModelNodeVO defaultSeqFlow = CollUtil.findOne(node.getConditionNodes(), - item -> BooleanUtil.isTrue(item.getDefaultFlow())); - if (defaultSeqFlow != null) { - exclusiveGateway.setDefaultFlow(defaultSeqFlow.getId()); + @Override + public BpmSimpleModelNodeType getType() { + return BpmSimpleModelNodeType.START_NODE; } - return exclusiveGateway; + } - private static InclusiveGateway convertInclusiveBranchNode(BpmSimpleModelNodeVO node, Boolean isFork) { - InclusiveGateway inclusiveGateway = new InclusiveGateway(); - inclusiveGateway.setId(node.getId()); - // TODO @jason:这里是不是 setName 哈; + private static class EndNodeConvert implements NodeConvert { - // TODO @芋艿 + jason:是不是搞个合并网关;这里微信讨论下,有点奇怪; - // @芋艿 isFork 为 false 就是合并网关。由前端传入。这个前端暂时还未实现 - if (isFork) { - Assert.notEmpty(node.getConditionNodes(), "条件节点不能为空"); - // 寻找默认的序列流 - BpmSimpleModelNodeVO defaultSeqFlow = CollUtil.findOne( - node.getConditionNodes(), item -> BooleanUtil.isTrue(item.getDefaultFlow())); - if (defaultSeqFlow != null) { - inclusiveGateway.setDefaultFlow(defaultSeqFlow.getId()); - } + @Override + public EndEvent convert(BpmSimpleModelNodeVO node) { + EndEvent endEvent = new EndEvent(); + endEvent.setId(node.getId()); + endEvent.setName(node.getName()); + // TODO @芋艿 + jason:要不要加一个终止定义? + return endEvent; } - return inclusiveGateway; + + @Override + public BpmSimpleModelNodeType getType() { + return BpmSimpleModelNodeType.END_NODE; + } + } - private static UserTask buildBpmnUserTask(BpmSimpleModelNodeVO node) { - UserTask userTask = new UserTask(); - userTask.setId(node.getId()); - userTask.setName(node.getName()); + private static class StartUserNodeConvert implements NodeConvert { - // 如果不是审批人节点,则直接返回 - addExtensionElement(userTask, USER_TASK_APPROVE_TYPE, StrUtil.toStringOrNull(node.getApproveType())); - if (ObjectUtil.notEqual(node.getApproveType(), USER.getType())) { + @Override + public UserTask convert(BpmSimpleModelNodeVO node) { + UserTask userTask = new UserTask(); + userTask.setId(node.getId()); + userTask.setName(node.getName()); + + // 人工审批 + addExtensionElement(userTask, BpmnModelConstants.USER_TASK_APPROVE_TYPE, BpmUserTaskApproveTypeEnum.USER.getType()); + // 候选人策略为发起人自己 + addCandidateElements(BpmTaskCandidateStrategyEnum.START_USER.getStrategy(), null, userTask); + // 添加表单字段权限属性元素 + addFormFieldsPermission(node.getFieldsPermission(), userTask); + // 添加操作按钮配置属性元素 + addButtonsSetting(node.getButtonsSetting(), userTask); + // 使用自动通过策略 + // TODO @芋艿 复用了SKIP, 是否需要新加一个策略;TODO @芋艿:【回复】是不是应该类似飞书,搞个草稿状态。待定;还有一种策略,不标记自动通过,而是首次发起后,第一个节点,自动通过; + addAssignStartUserHandlerType(BpmUserTaskAssignStartUserHandlerTypeEnum.SKIP.getType(), userTask); return userTask; } - // 添加候选人元素 - addCandidateElements(node.getCandidateStrategy(), node.getCandidateParam(), userTask); - // 添加表单字段权限属性元素 - addFormFieldsPermission(node.getFieldsPermission(), userTask); - // 添加操作按钮配置属性元素 - addButtonsSetting(node.getButtonsSetting(), userTask); - // 处理多实例(审批方式) - processMultiInstanceLoopCharacteristics(node.getApproveMethod(), node.getApproveRatio(), userTask); - // 添加任务被拒绝的处理元素 - addTaskRejectElements(node.getRejectHandler(), userTask); - // 添加用户任务的审批人与发起人相同时的处理元素 - addAssignStartUserHandlerType(node.getAssignStartUserHandlerType(), userTask); - // 添加用户任务的空处理元素 - addAssignEmptyHandlerType(node.getAssignEmptyHandler(), userTask); - // 设置审批任务的截止时间 - if (node.getTimeoutHandler() != null && node.getTimeoutHandler().getEnable()) { - userTask.setDueDate(node.getTimeoutHandler().getTimeDuration()); + @Override + public BpmSimpleModelNodeType getType() { + return BpmSimpleModelNodeType.START_USER_NODE; } - return userTask; + } - private static void addTaskRejectElements(RejectHandler rejectHandler, UserTask userTask) { - if (rejectHandler == null) { - return; - } - addExtensionElement(userTask, USER_TASK_REJECT_HANDLER_TYPE, StrUtil.toStringOrNull(rejectHandler.getType())); - addExtensionElement(userTask, USER_TASK_REJECT_RETURN_TASK_ID, rejectHandler.getReturnNodeId()); - } + private static class ApproveNodeConvert implements NodeConvert { - private static void addAssignStartUserHandlerType(Integer assignStartUserHandlerType, UserTask userTask) { - if (assignStartUserHandlerType == null) { - return; - } - addExtensionElement(userTask, USER_TASK_ASSIGN_START_USER_HANDLER_TYPE, assignStartUserHandlerType.toString()); - } + @Override + public List convertList(BpmSimpleModelNodeVO node) { + List flowElements = new ArrayList<>(2); + // 1. 构建用户任务 + UserTask userTask = buildBpmnUserTask(node); + flowElements.add(userTask); - private static void addAssignEmptyHandlerType(BpmSimpleModelNodeVO.AssignEmptyHandler emptyHandler, UserTask userTask) { - if (emptyHandler == null) { - return; + // 2. 添加用户任务的 Timer Boundary Event, 用于任务的审批超时处理 + if (node.getTimeoutHandler() != null && node.getTimeoutHandler().getEnable()) { + BoundaryEvent boundaryEvent = buildUserTaskTimeoutBoundaryEvent(userTask, node.getTimeoutHandler()); + flowElements.add(boundaryEvent); + } + return flowElements; } - addExtensionElement(userTask, USER_TASK_ASSIGN_EMPTY_HANDLER_TYPE, StrUtil.toStringOrNull(emptyHandler.getType())); - addExtensionElement(userTask, USER_TASK_ASSIGN_USER_IDS, StrUtil.join(",", emptyHandler.getUserIds())); - } - private static void processMultiInstanceLoopCharacteristics(Integer approveMethod, Integer approveRatio, UserTask userTask) { - BpmUserTaskApproveMethodEnum approveMethodEnum = BpmUserTaskApproveMethodEnum.valueOf(approveMethod); - // TODO @jason:这种枚举,最终不要去掉哈 BpmUserTaskApproveMethodEnum。因为容易不经意重叠 - if (approveMethodEnum == null || approveMethodEnum == RANDOM) { - return; + @Override + public BpmSimpleModelNodeType getType() { + return BpmSimpleModelNodeType.APPROVE_NODE; } - // 添加审批方式的扩展属性 - addExtensionElement(userTask, BpmnModelConstants.USER_TASK_APPROVE_METHOD, - approveMethod == null ? null : approveMethod.toString()); - MultiInstanceLoopCharacteristics multiInstanceCharacteristics = new MultiInstanceLoopCharacteristics(); - // 设置 collectionVariable。本系统用不到。仅仅为了 Flowable 校验不报错。 - multiInstanceCharacteristics.setInputDataItem("${coll_userList}"); - if (approveMethodEnum == BpmUserTaskApproveMethodEnum.ANY) { - multiInstanceCharacteristics.setCompletionCondition(ANY_OF_APPROVE_COMPLETE_EXPRESSION); - multiInstanceCharacteristics.setSequential(false); + + /** + * 添加 UserTask 用户的审批超时 BoundaryEvent 事件 + * + * @param userTask 审批任务 + * @param timeoutHandler 超时处理器 + * @return BoundaryEvent 超时事件 + */ + private BoundaryEvent buildUserTaskTimeoutBoundaryEvent(UserTask userTask, + BpmSimpleModelNodeVO.TimeoutHandler timeoutHandler) { + // 1.1 定时器边界事件 + BoundaryEvent boundaryEvent = new BoundaryEvent(); + boundaryEvent.setId("Event-" + IdUtil.fastUUID()); + boundaryEvent.setCancelActivity(false); // 设置关联的任务为不会被中断 + boundaryEvent.setAttachedToRef(userTask); + // 1.2 定义超时时间、最大提醒次数 + TimerEventDefinition eventDefinition = new TimerEventDefinition(); + eventDefinition.setTimeDuration(timeoutHandler.getTimeDuration()); + if (Objects.equals(BpmUserTaskTimeoutHandlerTypeEnum.REMINDER.getType(), timeoutHandler.getType()) && + timeoutHandler.getMaxRemindCount() != null && timeoutHandler.getMaxRemindCount() > 1) { + eventDefinition.setTimeCycle(String.format("R%d/%s", + timeoutHandler.getMaxRemindCount(), timeoutHandler.getTimeDuration())); + } + boundaryEvent.addEventDefinition(eventDefinition); + + // 2.1 添加定时器边界事件类型 + addExtensionElement(boundaryEvent, BOUNDARY_EVENT_TYPE, BpmBoundaryEventType.USER_TASK_TIMEOUT.getType()); + // 2.2 添加超时执行动作元素 + addExtensionElement(boundaryEvent, USER_TASK_TIMEOUT_HANDLER_TYPE, timeoutHandler.getType()); + return boundaryEvent; + } + + private UserTask buildBpmnUserTask(BpmSimpleModelNodeVO node) { + UserTask userTask = new UserTask(); + userTask.setId(node.getId()); + userTask.setName(node.getName()); + + // 如果不是审批人节点,则直接返回 + addExtensionElement(userTask, USER_TASK_APPROVE_TYPE, node.getApproveType()); + if (ObjectUtil.notEqual(node.getApproveType(), BpmUserTaskApproveTypeEnum.USER.getType())) { + return userTask; + } + + // 添加候选人元素 + addCandidateElements(node.getCandidateStrategy(), node.getCandidateParam(), userTask); + // 添加表单字段权限属性元素 + addFormFieldsPermission(node.getFieldsPermission(), userTask); + // 添加操作按钮配置属性元素 + addButtonsSetting(node.getButtonsSetting(), userTask); + // 处理多实例(审批方式) + processMultiInstanceLoopCharacteristics(node.getApproveMethod(), node.getApproveRatio(), userTask); + // 添加任务被拒绝的处理元素 + addTaskRejectElements(node.getRejectHandler(), userTask); + // 添加用户任务的审批人与发起人相同时的处理元素 + addAssignStartUserHandlerType(node.getAssignStartUserHandlerType(), userTask); + // 添加用户任务的空处理元素 + addAssignEmptyHandlerType(node.getAssignEmptyHandler(), userTask); + // 设置审批任务的截止时间 + if (node.getTimeoutHandler() != null && node.getTimeoutHandler().getEnable()) { + userTask.setDueDate(node.getTimeoutHandler().getTimeDuration()); + } + return userTask; + } + + private void processMultiInstanceLoopCharacteristics(Integer approveMethod, Integer approveRatio, UserTask userTask) { + BpmUserTaskApproveMethodEnum approveMethodEnum = BpmUserTaskApproveMethodEnum.valueOf(approveMethod); + Assert.notNull(approveMethodEnum, "审批方式({})不能为空", approveMethodEnum); + // 添加审批方式的扩展属性 + addExtensionElement(userTask, BpmnModelConstants.USER_TASK_APPROVE_METHOD, approveMethod); + if (approveMethodEnum == BpmUserTaskApproveMethodEnum.RANDOM) { + // 随机审批,不需要设置多实例属性 + return; + } + + // 处理多实例审批方式 + MultiInstanceLoopCharacteristics multiInstanceCharacteristics = new MultiInstanceLoopCharacteristics(); + // 设置 collectionVariable。本系统用不到,仅仅为了 Flowable 校验不报错 + multiInstanceCharacteristics.setInputDataItem("${coll_userList}"); + if (approveMethodEnum == BpmUserTaskApproveMethodEnum.ANY) { + multiInstanceCharacteristics.setCompletionCondition(approveMethodEnum.getCompletionCondition()); + multiInstanceCharacteristics.setSequential(false); + } else if (approveMethodEnum == BpmUserTaskApproveMethodEnum.SEQUENTIAL) { + multiInstanceCharacteristics.setCompletionCondition(approveMethodEnum.getCompletionCondition()); + multiInstanceCharacteristics.setSequential(true); + multiInstanceCharacteristics.setLoopCardinality("1"); + } else if (approveMethodEnum == BpmUserTaskApproveMethodEnum.RATIO) { + Assert.notNull(approveRatio, "通过比例不能为空"); + multiInstanceCharacteristics.setCompletionCondition( + String.format(approveMethodEnum.getCompletionCondition(), String.format("%.2f", approveRatio / 100D))); + multiInstanceCharacteristics.setSequential(false); + } userTask.setLoopCharacteristics(multiInstanceCharacteristics); - } else if (approveMethodEnum == SEQUENTIAL) { - multiInstanceCharacteristics.setCompletionCondition(ALL_APPROVE_COMPLETE_EXPRESSION); - multiInstanceCharacteristics.setSequential(true); - multiInstanceCharacteristics.setLoopCardinality("1"); - userTask.setLoopCharacteristics(multiInstanceCharacteristics); - } else if (approveMethodEnum == RATIO) { - Assert.notNull(approveRatio, "通过比例不能为空"); - multiInstanceCharacteristics.setCompletionCondition( - String.format(APPROVE_BY_RATIO_COMPLETE_EXPRESSION, String.format("%.2f", approveRatio / (double) 100))); - multiInstanceCharacteristics.setSequential(false); } - userTask.setLoopCharacteristics(multiInstanceCharacteristics); + } - /** - * 给节点添加操作按钮设置元素 - */ - private static void addButtonsSetting(List buttonsSetting, UserTask userTask) { - if (CollUtil.isNotEmpty(buttonsSetting)) { - List> list = CollectionUtils.convertList(buttonsSetting, item -> { - Map settingMap = MapUtil.newHashMap(16); - settingMap.put(BUTTON_SETTING_ELEMENT_ID_ATTRIBUTE, String.valueOf(item.getId())); - settingMap.put(BUTTON_SETTING_ELEMENT_DISPLAY_NAME_ATTRIBUTE, item.getDisplayName()); - settingMap.put(BUTTON_SETTING_ELEMENT_ENABLE_ATTRIBUTE, String.valueOf(item.getEnable())); - return settingMap; - }); - list.forEach(item -> addExtensionElement(userTask, BUTTON_SETTING_ELEMENT, item)); + private static class CopyNodeConvert implements NodeConvert { + + @Override + public ServiceTask convert(BpmSimpleModelNodeVO node) { + ServiceTask serviceTask = new ServiceTask(); + serviceTask.setId(node.getId()); + serviceTask.setName(node.getName()); + serviceTask.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_DELEGATEEXPRESSION); + serviceTask.setImplementation("${" + BpmCopyTaskDelegate.BEAN_NAME + "}"); + + // 添加抄送候选人元素 + addCandidateElements(node.getCandidateStrategy(), node.getCandidateParam(), serviceTask); + // 添加表单字段权限属性元素 + addFormFieldsPermission(node.getFieldsPermission(), serviceTask); + return serviceTask; } + + @Override + public BpmSimpleModelNodeType getType() { + return BpmSimpleModelNodeType.COPY_NODE; + } + } - /** - * 给节点添加表单字段权限元素 - */ - private static void addFormFieldsPermission(List> fieldsPermissions, FlowElement flowElement) { - if (CollUtil.isNotEmpty(fieldsPermissions)) { - fieldsPermissions.forEach(item -> addExtensionElement(flowElement, FORM_FIELD_PERMISSION_ELEMENT, item)); + private static class ConditionBranchNodeConvert implements NodeConvert { + + @Override + public ExclusiveGateway convert(BpmSimpleModelNodeVO node) { + ExclusiveGateway exclusiveGateway = new ExclusiveGateway(); + exclusiveGateway.setId(node.getId()); + // TODO @jason:setName + + // 设置默认的序列流(条件) + BpmSimpleModelNodeVO defaultSeqFlow = CollUtil.findOne(node.getConditionNodes(), + item -> BooleanUtil.isTrue(item.getDefaultFlow())); + Assert.notNull(defaultSeqFlow, "条件分支节点({})的默认序列流不能为空", node.getId()); + exclusiveGateway.setDefaultFlow(defaultSeqFlow.getId()); + return exclusiveGateway; } + + @Override + public BpmSimpleModelNodeType getType() { + return BpmSimpleModelNodeType.CONDITION_BRANCH_NODE; + } + } - private static void addExtensionElement(FlowElement element, String name, Map attributes) { - if (attributes == null) { + private static class ParallelBranchNodeConvert implements NodeConvert { + + @Override + public List convertList(BpmSimpleModelNodeVO node) { + ParallelGateway parallelGateway = new ParallelGateway(); + parallelGateway.setId(node.getId()); + // TODO @jason:setName + + // 并行聚合网关由程序创建,前端不需要传入 + ParallelGateway joinParallelGateway = new ParallelGateway(); + joinParallelGateway.setId(buildGatewayJoinId(node.getId())); + // TODO @jason:setName + return CollUtil.newArrayList(parallelGateway, joinParallelGateway); + } + + @Override + public BpmSimpleModelNodeType getType() { + return BpmSimpleModelNodeType.PARALLEL_BRANCH_NODE; + } + + } + + private static class InclusiveBranchNodeConvert implements NodeConvert { + + @Override + public List convertList(BpmSimpleModelNodeVO node) { + InclusiveGateway inclusiveGateway = new InclusiveGateway(); + inclusiveGateway.setId(node.getId()); + // 设置默认的序列流(条件) + BpmSimpleModelNodeVO defaultSeqFlow = CollUtil.findOne(node.getConditionNodes(), + item -> BooleanUtil.isTrue(item.getDefaultFlow())); + Assert.notNull(defaultSeqFlow, "包容分支节点({})的默认序列流不能为空", node.getId()); + inclusiveGateway.setDefaultFlow(defaultSeqFlow.getId()); + // TODO @jason:setName + + // 并行聚合网关由程序创建,前端不需要传入 + InclusiveGateway joinInclusiveGateway = new InclusiveGateway(); + joinInclusiveGateway.setId(buildGatewayJoinId(node.getId())); + // TODO @jason:setName + return CollUtil.newArrayList(inclusiveGateway, joinInclusiveGateway); + } + + @Override + public BpmSimpleModelNodeType getType() { + return BpmSimpleModelNodeType.INCLUSIVE_BRANCH_NODE; + } + + } + + public static class ConditionNodeConvert implements NodeConvert { + + @Override + public List convertList(BpmSimpleModelNodeVO node) { + // 原因是:正常情况下,它不会被调用到 + throw new UnsupportedOperationException("条件节点不支持转换"); + } + + @Override + public BpmSimpleModelNodeType getType() { + return BpmSimpleModelNodeType.CONDITION_NODE; + } + + public static SequenceFlow buildSequenceFlow(String sourceId, String targetId, + BpmSimpleModelNodeVO node) { + String conditionExpression = buildConditionExpression(node); + return buildBpmnSequenceFlow(sourceId, targetId, node.getId(), node.getName(), conditionExpression); + } + + /** + * 构造条件表达式 + * + * @param node 条件节点 + */ + public static String buildConditionExpression(BpmSimpleModelNodeVO node) { + BpmSimpleModeConditionType conditionTypeEnum = BpmSimpleModeConditionType.valueOf(node.getConditionType()); + if (conditionTypeEnum == BpmSimpleModeConditionType.EXPRESSION) { + return node.getConditionExpression(); + } + if (conditionTypeEnum == BpmSimpleModeConditionType.RULE) { + ConditionGroups conditionGroups = node.getConditionGroups(); + if (conditionGroups == null || CollUtil.isEmpty(conditionGroups.getConditions())) { + return null; + } + List strConditionGroups = CollectionUtils.convertList(conditionGroups.getConditions(), item -> { + if (CollUtil.isEmpty(item.getRules())) { + return ""; + } + // 构造规则表达式 + List list = CollectionUtils.convertList(item.getRules(), (rule) -> { + String rightSide = NumberUtil.isNumber(rule.getRightSide()) ? rule.getRightSide() + : "\"" + rule.getRightSide() + "\""; // 如果非数值类型加引号 + return String.format(" %s %s var:convertByType(%s,%s)", rule.getLeftSide(), rule.getOpCode(), rule.getLeftSide(), rightSide); + }); + // 构造条件组的表达式 + Boolean and = item.getAnd(); + return "(" + CollUtil.join(list, and ? " && " : " || ") + ")"; + }); + return String.format("${%s}", CollUtil.join(strConditionGroups, conditionGroups.getAnd() ? " && " : " || ")); + } + return null; + } + + } + + private static String buildGatewayJoinId(String id) { + return id + "_join"; + } + + // ========== SIMPLE 流程预测相关的方法 ========== + + public static List simulateProcess(BpmSimpleModelNodeVO rootNode, Map variables) { + List resultNodes = new ArrayList<>(); + + // 从头开始遍历 + simulateNextNode(rootNode, variables, resultNodes); + return resultNodes; + } + + private static void simulateNextNode(BpmSimpleModelNodeVO currentNode, Map variables, + List resultNodes) { + // 如果不合法(包括为空),则直接结束 + if (!isValidNode(currentNode)) { return; } - ExtensionElement extensionElement = new ExtensionElement(); - extensionElement.setNamespace(FLOWABLE_EXTENSIONS_NAMESPACE); - extensionElement.setNamespacePrefix(FLOWABLE_EXTENSIONS_PREFIX); - extensionElement.setName(name); - attributes.forEach((key, value) -> { - ExtensionAttribute extensionAttribute = new ExtensionAttribute(key, value); - extensionAttribute.setNamespace(FLOWABLE_EXTENSIONS_NAMESPACE); - extensionElement.addAttribute(extensionAttribute); - }); - element.addExtensionElement(extensionElement); - } + BpmSimpleModelNodeType nodeType = BpmSimpleModelNodeType.valueOf(currentNode.getType()); + Assert.notNull(nodeType, "模型节点类型不支持"); - private static void addExtensionElement(FlowElement element, String name, String value) { - if (value == null) { - return; + // 情况:START_NODE/START_USER_NODE/APPROVE_NODE/COPY_NODE/END_NODE + if (nodeType == BpmSimpleModelNodeType.START_NODE + || nodeType == BpmSimpleModelNodeType.START_USER_NODE + || nodeType == BpmSimpleModelNodeType.APPROVE_NODE + || nodeType == BpmSimpleModelNodeType.COPY_NODE + || nodeType == BpmSimpleModelNodeType.END_NODE) { + // 添加元素 + resultNodes.add(currentNode); } - ExtensionElement extensionElement = new ExtensionElement(); - extensionElement.setNamespace(FLOWABLE_EXTENSIONS_NAMESPACE); - extensionElement.setNamespacePrefix(FLOWABLE_EXTENSIONS_PREFIX); - extensionElement.setElementText(value); - extensionElement.setName(name); - element.addExtensionElement(extensionElement); + + // 情况:CONDITION_BRANCH_NODE 排它,只有一个满足条件的。如果没有,就走默认的 + if (nodeType == BpmSimpleModelNodeType.CONDITION_BRANCH_NODE) { + // 查找满足条件的 BpmSimpleModelNodeVO 节点 + BpmSimpleModelNodeVO matchConditionNode = CollUtil.findOne(currentNode.getConditionNodes(), + conditionNode -> !BooleanUtil.isTrue(conditionNode.getDefaultFlow()) + && evalConditionExpress(variables, conditionNode)); + if (matchConditionNode == null) { + matchConditionNode = CollUtil.findOne(currentNode.getConditionNodes(), + conditionNode -> BooleanUtil.isTrue(conditionNode.getDefaultFlow())); + } + Assert.notNull(matchConditionNode, "找不到条件节点({})", currentNode); + // 遍历满足条件的 BpmSimpleModelNodeVO 节点 + simulateNextNode(matchConditionNode.getChildNode(), variables, resultNodes); + } + + // 情况:INCLUSIVE_BRANCH_NODE 包容,多个满足条件的。如果没有,就走默认的 + if (nodeType == BpmSimpleModelNodeType.INCLUSIVE_BRANCH_NODE) { + // 查找满足条件的 BpmSimpleModelNodeVO 节点 + Collection matchConditionNodes = CollUtil.filterNew(currentNode.getConditionNodes(), + conditionNode -> !BooleanUtil.isTrue(conditionNode.getDefaultFlow()) + && evalConditionExpress(variables, conditionNode)); + if (CollUtil.isEmpty(matchConditionNodes)) { + matchConditionNodes = CollUtil.filterNew(currentNode.getConditionNodes(), + conditionNode -> BooleanUtil.isTrue(conditionNode.getDefaultFlow())); + } + Assert.isTrue(!matchConditionNodes.isEmpty(), "找不到条件节点({})", currentNode); + // 遍历满足条件的 BpmSimpleModelNodeVO 节点 + matchConditionNodes.forEach(matchConditionNode -> + simulateNextNode(matchConditionNode.getChildNode(), variables, resultNodes)); + } + + // 情况:PARALLEL_BRANCH_NODE 并行,都满足,都走 + if (nodeType == BpmSimpleModelNodeType.PARALLEL_BRANCH_NODE) { + // 遍历所有 BpmSimpleModelNodeVO 节点 + currentNode.getConditionNodes().forEach(matchConditionNode -> + simulateNextNode(matchConditionNode.getChildNode(), variables, resultNodes)); + } + + // 遍历子节点 + simulateNextNode(currentNode.getChildNode(), variables, resultNodes); } - // ========== 各种 build 节点的方法 ========== - - private static StartEvent convertStartNode(BpmSimpleModelNodeVO node) { - StartEvent startEvent = new StartEvent(); - startEvent.setId(node.getId()); - startEvent.setName(node.getName()); - return startEvent; - } - - private static UserTask buildBpmnStartUserTask(BpmSimpleModelNodeVO node) { - UserTask userTask = new UserTask(); - userTask.setId(node.getId()); - userTask.setName(node.getName()); - // 人工审批 - addExtensionElement(userTask, USER_TASK_APPROVE_TYPE, USER.getType().toString()); - // 候选人策略为发起人自己 - addCandidateElements(START_USER.getStrategy(), null, userTask); - // 添加表单字段权限属性元素 - addFormFieldsPermission(node.getFieldsPermission(), userTask); - // 添加操作按钮配置属性元素 - addButtonsSetting(node.getButtonsSetting(), userTask); - // 使用自动通过策略 TODO @芋艿 复用了SKIP, 是否需要新加一个策略;TODO @芋艿:【回复】是不是应该类似飞书,搞个草稿状态。待定;还有一种策略,不标记自动通过,而是首次发起后,第一个节点,自动通过; - addAssignStartUserHandlerType(SKIP.getType(), userTask); - return userTask; - } - - private static EndEvent convertEndNode(BpmSimpleModelNodeVO node) { - EndEvent endEvent = new EndEvent(); - endEvent.setId(node.getId()); - endEvent.setName(node.getName()); - - // TODO @芋艿 + jason:要不要加一个终止定义? - return endEvent; + public static boolean evalConditionExpress(Map variables, BpmSimpleModelNodeVO conditionNode) { + return BpmnModelUtils.evalConditionExpress(variables, ConditionNodeConvert.buildConditionExpression(conditionNode)); } } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmCategoryService.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmCategoryService.java index a61b132b5..104f26938 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmCategoryService.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmCategoryService.java @@ -82,4 +82,11 @@ public interface BpmCategoryService { */ List getCategoryListByStatus(Integer status); + /** + * 批量更新流程分类的排序:每个分类的 sort 值,从 0 开始递增 + * + * @param ids 分类编号列表 + */ + void updateCategorySortBatch(List ids); + } \ No newline at end of file diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmCategoryServiceImpl.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmCategoryServiceImpl.java index 2a9ee9270..8a48da15a 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmCategoryServiceImpl.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmCategoryServiceImpl.java @@ -10,11 +10,14 @@ import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO; import cn.iocoder.yudao.module.bpm.dal.mysql.category.BpmCategoryMapper; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*; @@ -56,7 +59,7 @@ public class BpmCategoryServiceImpl implements BpmCategoryService { private void validateCategoryNameUnique(BpmCategorySaveReqVO updateReqVO) { BpmCategoryDO category = bpmCategoryMapper.selectByName(updateReqVO.getName()); if (category == null - || ObjUtil.equal(category.getId(), updateReqVO.getId())) { + || ObjUtil.equal(category.getId(), updateReqVO.getId())) { return; } throw exception(CATEGORY_NAME_DUPLICATE, updateReqVO.getName()); @@ -65,7 +68,7 @@ public class BpmCategoryServiceImpl implements BpmCategoryService { private void validateCategoryCodeUnique(BpmCategorySaveReqVO updateReqVO) { BpmCategoryDO category = bpmCategoryMapper.selectByCode(updateReqVO.getCode()); if (category == null - || ObjUtil.equal(category.getId(), updateReqVO.getId())) { + || ObjUtil.equal(category.getId(), updateReqVO.getId())) { return; } throw exception(CATEGORY_CODE_DUPLICATE, updateReqVO.getCode()); @@ -108,4 +111,20 @@ public class BpmCategoryServiceImpl implements BpmCategoryService { return bpmCategoryMapper.selectListByStatus(status); } + @Override + @Transactional(rollbackFor = Exception.class) + public void updateCategorySortBatch(List ids) { + // 校验分类都存在 + List categories = bpmCategoryMapper.selectByIds(ids); + if (categories.size() != ids.size()) { + throw exception(CATEGORY_NOT_EXISTS); + } + + // 批量更新排序 + List updateList = IntStream.range(0, ids.size()) + .mapToObj(index -> new BpmCategoryDO().setId(ids.get(index)).setSort(index)) + .collect(Collectors.toList()); + bpmCategoryMapper.updateBatch(updateList); + } + } \ No newline at end of file diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelService.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelService.java index a2dcba480..98a856bf4 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelService.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelService.java @@ -1,7 +1,5 @@ package cn.iocoder.yudao.module.bpm.service.definition; -import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelPageReqVO; import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelSaveReqVO; import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO; import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelUpdateReqVO; @@ -9,20 +7,22 @@ import jakarta.validation.Valid; import org.flowable.bpmn.model.BpmnModel; import org.flowable.engine.repository.Model; +import java.util.List; + /** - * Flowable流程模型接口 + * 流程模型接口 * * @author yunlongn */ public interface BpmModelService { /** - * 获得流程模型分页 + * 获得流程模型列表 * - * @param pageVO 分页查询 - * @return 流程模型分页 + * @param name 模型名称 + * @return 流程模型列表 */ - PageResult getModelPage(BpmModelPageReqVO pageVO); + List getModelList(String name); /** * 创建流程模型 @@ -64,6 +64,14 @@ public interface BpmModelService { */ void updateModel(Long userId, @Valid BpmModelSaveReqVO updateReqVO); + /** + * 批量更新模型排序 + * + * @param userId 用户编号 + * @param ids 编号列表 + */ + void updateModelSortBatch(Long userId, List ids); + /** * 将流程模型,部署成一个流程定义 * diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java index 64ba6ef16..ed845d155 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java @@ -3,12 +3,9 @@ package cn.iocoder.yudao.module.bpm.service.definition; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import cn.iocoder.yudao.framework.common.util.object.PageUtils; import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils; import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO; -import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelPageReqVO; import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelSaveReqVO; import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO; import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelUpdateReqVO; @@ -35,14 +32,15 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import java.util.List; +import java.util.Map; import java.util.Objects; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*; /** - * Flowable流程模型实现 - * 主要进行 Flowable {@link Model} 的维护 + * 流程模型实现:主要进行 Flowable {@link Model} 的维护 * * @author yunlongn * @author 芋道源码 @@ -64,27 +62,12 @@ public class BpmModelServiceImpl implements BpmModelService { private BpmTaskCandidateInvoker taskCandidateInvoker; @Override - public PageResult getModelPage(BpmModelPageReqVO pageVO) { + public List getModelList(String name) { ModelQuery modelQuery = repositoryService.createModelQuery(); - modelQuery.modelTenantId(FlowableUtils.getTenantId()); - if (StrUtil.isNotBlank(pageVO.getKey())) { - modelQuery.modelKey(pageVO.getKey()); + if (StrUtil.isNotEmpty(name)) { + modelQuery.modelNameLike(name); } - if (StrUtil.isNotBlank(pageVO.getName())) { - modelQuery.modelNameLike("%" + pageVO.getName() + "%"); // 模糊匹配 - } - if (StrUtil.isNotBlank(pageVO.getCategory())) { - modelQuery.modelCategory(pageVO.getCategory()); - } - // 执行查询 - long count = modelQuery.count(); - if (count == 0) { - return PageResult.empty(count); - } - List models = modelQuery - .orderByCreateTime().desc() - .listPage(PageUtils.getStart(pageVO), pageVO.getPageSize()); - return new PageResult<>(models, count); + return modelQuery.list(); } @Override @@ -100,6 +83,7 @@ public class BpmModelServiceImpl implements BpmModelService { } // 2.1 创建流程定义 + createReqVO.setSort(System.currentTimeMillis()); // 使用当前时间,作为排序 Model model = repositoryService.newModel(); BpmModelConvert.INSTANCE.copyToModel(model, createReqVO); model.setTenantId(FlowableUtils.getTenantId()); @@ -120,6 +104,34 @@ public class BpmModelServiceImpl implements BpmModelService { repositoryService.saveModel(model); } + @Override + @Transactional(rollbackFor = Exception.class) + public void updateModelSortBatch(Long userId, List ids) { + // 1.1 校验流程模型存在 + List models = repositoryService.createModelQuery() + .modelTenantId(FlowableUtils.getTenantId()).list(); + models.removeIf(model ->!ids.contains(model.getId())); + if (ids.size() != models.size()) { + throw exception(MODEL_NOT_EXISTS); + } + Map modelMap = convertMap(models, Model::getId); + // 1.2 校验是否为管理员 + ids.forEach(id -> validateModelManager(id, userId)); + + // 保存排序 + long sort = System.currentTimeMillis(); // 使用时间戳 - i 作为排序 + for (int i = ids.size() - 1; i > 0; i--) { + Model model = modelMap.get(ids.get(i)); + // 更新模型 + BpmModelMetaInfoVO metaInfo = BpmModelConvert.INSTANCE.parseMetaInfo(model).setSort(sort); + model.setMetaInfo(JsonUtils.toJsonString(metaInfo)); + repositoryService.saveModel(model); + // 更新排序 + processDefinitionService.updateProcessDefinitionSortByModelId(model.getId(), sort); + sort--; + } + } + private Model validateModelExists(String id) { Model model = repositoryService.getModel(id); if (model == null) { @@ -139,7 +151,7 @@ public class BpmModelServiceImpl implements BpmModelService { Model model = validateModelExists(id); BpmModelMetaInfoVO metaInfo = BpmModelConvert.INSTANCE.parseMetaInfo(model); if (metaInfo == null || !CollUtil.contains(metaInfo.getManagerUserIds(), userId)) { - throw exception(MODEL_UPDATE_FAIL_NOT_MANAGER); + throw exception(MODEL_UPDATE_FAIL_NOT_MANAGER, model.getName()); } return model; } @@ -158,10 +170,10 @@ public class BpmModelServiceImpl implements BpmModelService { // 1.4 校验任务分配规则已配置 taskCandidateInvoker.validateBpmnConfig(bpmnBytes); // 1.5 获取仿钉钉流程设计器模型数据 - byte[] simpleBytes = getModelSimpleJson(model.getId()); + String simpleJson = getModelSimpleJson(model.getId()); // 2.1 创建流程定义 - String definitionId = processDefinitionService.createProcessDefinition(model, metaInfo, bpmnBytes, simpleBytes, form); + String definitionId = processDefinitionService.createProcessDefinition(model, metaInfo, bpmnBytes, simpleJson, form); // 2.2 将老的流程定义进行挂起。也就是说,只有最新部署的流程定义,才可以发起任务。 updateProcessDefinitionSuspended(model.getDeploymentId()); @@ -226,8 +238,8 @@ public class BpmModelServiceImpl implements BpmModelService { public BpmSimpleModelNodeVO getSimpleModel(String modelId) { Model model = validateModelExists(modelId); // 通过 ACT_RE_MODEL 表 EDITOR_SOURCE_EXTRA_VALUE_ID_ ,获取仿钉钉快搭模型的 JSON 数据 - byte[] jsonBytes = getModelSimpleJson(model.getId()); - return JsonUtils.parseObject(jsonBytes, BpmSimpleModelNodeVO.class); + String json = getModelSimpleJson(model.getId()); + return JsonUtils.parseObject(json, BpmSimpleModelNodeVO.class); } @Override @@ -240,7 +252,7 @@ public class BpmModelServiceImpl implements BpmModelService { // 2.2 保存 Bpmn XML updateModelBpmnXml(model.getId(), BpmnModelUtils.getBpmnXml(bpmnModel)); // 2.3 保存 JSON 数据 - saveModelSimpleJson(model.getId(), JsonUtils.toJsonByte(reqVO.getSimpleModel())); + updateModelSimpleJson(model.getId(), reqVO.getSimpleModel()); } /** @@ -279,15 +291,21 @@ public class BpmModelServiceImpl implements BpmModelService { repositoryService.addModelEditorSource(id, StrUtil.utf8Bytes(bpmnXml)); } - private byte[] getModelSimpleJson(String id) { - return repositoryService.getModelEditorSourceExtra(id); + @SuppressWarnings("JavaExistingMethodCanBeUsed") + private String getModelSimpleJson(String id) { + byte[] bytes = repositoryService.getModelEditorSourceExtra(id); + if (ArrayUtil.isEmpty(bytes)) { + return null; + } + return StrUtil.utf8Str(bytes); } - private void saveModelSimpleJson(String id, byte[] jsonBytes) { - if (ArrayUtil.isEmpty(jsonBytes)) { + private void updateModelSimpleJson(String id, BpmSimpleModelNodeVO node) { + if (node == null) { return; } - repositoryService.addModelEditorSourceExtra(id, jsonBytes); + byte[] bytes = JsonUtils.toJsonByte(node); + repositoryService.addModelEditorSourceExtra(id, bytes); } /** diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionService.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionService.java index ff3c1cb5b..71922b250 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionService.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionService.java @@ -18,7 +18,7 @@ import java.util.Set; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; /** - * Flowable流程定义接口 + * 流程定义接口 * * @author yunlong.li * @author ZJQ @@ -48,12 +48,12 @@ public interface BpmProcessDefinitionService { * @param model 流程模型 * @param modelMetaInfo 流程模型元信息 * @param bpmnBytes BPMN XML 字节数组 - * @param simpleBytes SIMPLE Model JSON 字节数组 + * @param simpleJson SIMPLE Model JSON * @param form 表单 * @return 流程编号 */ String createProcessDefinition(Model model, BpmModelMetaInfoVO modelMetaInfo, - byte[] bpmnBytes, byte[] simpleBytes, BpmFormDO form); + byte[] bpmnBytes, String simpleJson, BpmFormDO form); /** * 更新流程定义状态 @@ -63,6 +63,14 @@ public interface BpmProcessDefinitionService { */ void updateProcessDefinitionState(String id, Integer state); + /** + * 更新模型编号 + * + * @param modelId 流程定义编号 + * @param sort 排序 + */ + void updateProcessDefinitionSortByModelId(String modelId, Long sort); + /** * 获得流程定义对应的 BPMN * diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionServiceImpl.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionServiceImpl.java index 01abad2f8..c6a178c6c 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionServiceImpl.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionServiceImpl.java @@ -24,7 +24,6 @@ import org.flowable.engine.repository.ProcessDefinitionQuery; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; -import java.nio.charset.StandardCharsets; import java.util.*; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; @@ -120,7 +119,7 @@ public class BpmProcessDefinitionServiceImpl implements BpmProcessDefinitionServ @Override public String createProcessDefinition(Model model, BpmModelMetaInfoVO modelMetaInfo, - byte[] bpmnBytes, byte[] simpleBytes, BpmFormDO form) { + byte[] bpmnBytes, String simpleJson, BpmFormDO form) { // 创建 Deployment 部署 Deployment deploy = repositoryService.createDeployment() .key(model.getKey()).name(model.getName()).category(model.getCategory()) @@ -145,8 +144,8 @@ public class BpmProcessDefinitionServiceImpl implements BpmProcessDefinitionServ // 插入拓展表 BpmProcessDefinitionInfoDO definitionDO = BeanUtils.toBean(modelMetaInfo, BpmProcessDefinitionInfoDO.class) - .setModelId(model.getId()).setProcessDefinitionId(definition.getId()).setModelType(modelMetaInfo.getType()) - .setSimpleModel(StrUtil.str(simpleBytes, StandardCharsets.UTF_8)); + .setModelId(model.getId()).setProcessDefinitionId(definition.getId()) + .setModelType(modelMetaInfo.getType()).setSimpleModel(simpleJson); if (form != null) { definitionDO.setFormFields(form.getFields()).setFormConf(form.getConf()); @@ -172,6 +171,11 @@ public class BpmProcessDefinitionServiceImpl implements BpmProcessDefinitionServ log.error("[updateProcessDefinitionState][流程定义({}) 修改未知状态({})]", id, state); } + @Override + public void updateProcessDefinitionSortByModelId(String modelId, Long sort) { + processDefinitionMapper.updateByModelId(modelId, new BpmProcessDefinitionInfoDO().setSort(sort)); + } + @Override public BpmnModel getProcessDefinitionBpmnModel(String id) { return repositoryService.getBpmnModel(id); diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmActivityService.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmActivityService.java deleted file mode 100644 index be76c7013..000000000 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmActivityService.java +++ /dev/null @@ -1,30 +0,0 @@ -package cn.iocoder.yudao.module.bpm.service.task; - -import org.flowable.engine.history.HistoricActivityInstance; - -import java.util.List; - -/** - * BPM 活动实例 Service 接口 - * - * @author 芋道源码 - */ -public interface BpmActivityService { - - /** - * 获得指定流程实例的活动实例列表 - * - * @param processInstanceId 流程实例的编号 - * @return 活动实例列表 - */ - List getActivityListByProcessInstanceId(String processInstanceId); - - /** - * 获得执行编号对应的活动实例 - * - * @param executionId 执行编号 - * @return 活动实例 - */ - List getHistoricActivityListByExecutionId(String executionId); - -} diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmActivityServiceImpl.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmActivityServiceImpl.java deleted file mode 100644 index 26da5ad0e..000000000 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmActivityServiceImpl.java +++ /dev/null @@ -1,37 +0,0 @@ -package cn.iocoder.yudao.module.bpm.service.task; - -import jakarta.annotation.Resource; -import lombok.extern.slf4j.Slf4j; -import org.flowable.engine.HistoryService; -import org.flowable.engine.history.HistoricActivityInstance; -import org.springframework.stereotype.Service; -import org.springframework.validation.annotation.Validated; - -import java.util.List; - - -/** - * BPM 活动实例 Service 实现类 - * - * @author 芋道源码 - */ -@Service -@Slf4j -@Validated -public class BpmActivityServiceImpl implements BpmActivityService { - - @Resource - private HistoryService historyService; - - @Override - public List getActivityListByProcessInstanceId(String processInstanceId) { - return historyService.createHistoricActivityInstanceQuery().processInstanceId(processInstanceId) - .orderByHistoricActivityInstanceStartTime().asc().list(); - } - - @Override - public List getHistoricActivityListByExecutionId(String executionId) { - return historyService.createHistoricActivityInstanceQuery().executionId(executionId).list(); - } - -} diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceCopyService.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceCopyService.java index 7fd5ff361..3cbac0616 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceCopyService.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceCopyService.java @@ -3,9 +3,10 @@ package cn.iocoder.yudao.module.bpm.service.task; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceCopyPageReqVO; import cn.iocoder.yudao.module.bpm.dal.dataobject.task.BpmProcessInstanceCopyDO; +import jakarta.validation.constraints.NotEmpty; +import org.flowable.bpmn.model.FlowNode; import java.util.Collection; -import java.util.Set; /** * 流程抄送 Service 接口 @@ -15,24 +16,29 @@ import java.util.Set; public interface BpmProcessInstanceCopyService { /** - * 流程实例的抄送 + * 【管理员】流程实例的抄送 * * @param userIds 抄送的用户编号 + * @param reason 抄送意见 * @param taskId 流程任务编号 */ - void createProcessInstanceCopy(Collection userIds, String taskId); + void createProcessInstanceCopy(Collection userIds, String reason, String taskId); /** - * 流程实例的抄送 + * 【自动抄送】流程实例的抄送 * * @param userIds 抄送的用户编号 + * @param reason 抄送意见 * @param processInstanceId 流程编号 - * @param activityId 流程活动编号 id (对应 BPMN XML 节点 Id) - * // TODO 芋艿这个 taskId 是不是可以不要了 - * @param taskId 任务编号 - * @param taskName 任务名称 + * @param activityId 流程活动编号(对应 {@link FlowNode#getId()}) + * @param activityName 任务编号(对应 {@link FlowNode#getName()}) + * @param taskId 任务编号,允许空 */ - void createProcessInstanceCopy(Collection userIds, String processInstanceId, String activityId, String taskId, String taskName); + void createProcessInstanceCopy(Collection userIds, String reason, + @NotEmpty(message = "流程实例编号不能为空") String processInstanceId, + @NotEmpty(message = "流程活动编号不能为空") String activityId, + @NotEmpty(message = "流程活动名字不能为空") String activityName, + String taskId); /** * 获得抄送的流程的分页 @@ -43,14 +49,5 @@ public interface BpmProcessInstanceCopyService { */ PageResult getProcessInstanceCopyPage(Long userId, BpmProcessInstanceCopyPageReqVO pageReqVO); - // TODO @芋艿:重点在 review 下 - /** - * 通过流程实例和流程活动编号获取抄送人的 Id - * - * @param processInstanceId 流程实例 Id - * @param activityId 流程活动编号 Id - * @return 抄送人 Ids - */ - Set getCopyUserIds(String processInstanceId, String activityId); } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceCopyServiceImpl.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceCopyServiceImpl.java index ad677eb28..c2d52de05 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceCopyServiceImpl.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceCopyServiceImpl.java @@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.bpm.service.task; import cn.hutool.core.util.ObjectUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceCopyPageReqVO; import cn.iocoder.yudao.module.bpm.dal.dataobject.task.BpmProcessInstanceCopyDO; import cn.iocoder.yudao.module.bpm.dal.mysql.task.BpmProcessInstanceCopyMapper; @@ -19,7 +18,6 @@ import org.springframework.validation.annotation.Validated; import java.util.Collection; import java.util.List; -import java.util.Set; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; @@ -49,17 +47,19 @@ public class BpmProcessInstanceCopyServiceImpl implements BpmProcessInstanceCopy private BpmProcessDefinitionService processDefinitionService; @Override - public void createProcessInstanceCopy(Collection userIds, String taskId) { + public void createProcessInstanceCopy(Collection userIds, String reason, String taskId) { Task task = taskService.getTask(taskId); if (ObjectUtil.isNull(task)) { throw exception(ErrorCodeConstants.TASK_NOT_EXISTS); } - String processInstanceId = task.getProcessInstanceId(); - createProcessInstanceCopy(userIds, processInstanceId, task.getTaskDefinitionKey(), task.getId(), task.getName()); + // 执行抄送 + createProcessInstanceCopy(userIds, reason, + task.getProcessInstanceId(), task.getTaskDefinitionKey(), task.getId(), task.getName()); } @Override - public void createProcessInstanceCopy(Collection userIds, String processInstanceId, String activityId, String taskId, String taskName) { + public void createProcessInstanceCopy(Collection userIds, String reason, String processInstanceId, + String activityId, String activityName, String taskId) { // 1.1 校验流程实例存在 ProcessInstance processInstance = processInstanceService.getProcessInstance(processInstanceId); if (processInstance == null) { @@ -74,10 +74,10 @@ public class BpmProcessInstanceCopyServiceImpl implements BpmProcessInstanceCopy // 2. 创建抄送流程 List copyList = convertList(userIds, userId -> new BpmProcessInstanceCopyDO() - .setUserId(userId).setStartUserId(Long.valueOf(processInstance.getStartUserId())) + .setUserId(userId).setReason(reason).setStartUserId(Long.valueOf(processInstance.getStartUserId())) .setProcessInstanceId(processInstanceId).setProcessInstanceName(processInstance.getName()) - .setCategory(processDefinition.getCategory()).setActivityId(activityId) - .setTaskId(taskId).setTaskName(taskName)); + .setCategory(processDefinition.getCategory()).setTaskId(taskId) + .setActivityId(activityId).setActivityName(activityName)); processInstanceCopyMapper.insertBatch(copyList); } @@ -87,10 +87,4 @@ public class BpmProcessInstanceCopyServiceImpl implements BpmProcessInstanceCopy return processInstanceCopyMapper.selectPage(userId, pageReqVO); } - @Override - public Set getCopyUserIds(String processInstanceId, String activityId) { - return CollectionUtils.convertSet(processInstanceCopyMapper.selectListByProcessInstanceIdAndActivityId(processInstanceId, activityId), - BpmProcessInstanceCopyDO::getUserId); - } - } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceService.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceService.java index a14624d93..d37886aa7 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceService.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceService.java @@ -84,14 +84,6 @@ public interface BpmProcessInstanceService { PageResult getProcessInstancePage(Long userId, @Valid BpmProcessInstancePageReqVO pageReqVO); - /** - * 获得表单字段权限 - * - * @param reqVO 请求消息 - * @return 表单字段权限 - */ - Map getFormFieldsPermission(@Valid BpmFormFieldsPermissionReqVO reqVO); - // TODO @芋艿:重点在 review 下 /** * 获取审批详情。 @@ -104,6 +96,14 @@ public interface BpmProcessInstanceService { */ BpmApprovalDetailRespVO getApprovalDetail(Long loginUserId, @Valid BpmApprovalDetailReqVO reqVO); + /** + * 获取流程实例的 BPMN 模型视图 + * + * @param id 流程实例的编号 + * @return BPMN 模型视图 + */ + BpmProcessInstanceBpmnModelViewRespVO getProcessInstanceBpmnModelView(String id); + // ========== Update 写入相关方法 ========== /** @@ -157,5 +157,4 @@ public interface BpmProcessInstanceService { */ void processProcessInstanceCompleted(ProcessInstance instance); - } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java index 4f4747e3b..b99d418c5 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java @@ -1 +1 @@ -package cn.iocoder.yudao.module.bpm.service.task; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.BooleanUtil; import cn.hutool.core.util.NumberUtil; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; import cn.iocoder.yudao.framework.common.util.date.DateUtils; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.framework.common.util.object.PageUtils; import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO; import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO; import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.*; import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmApprovalDetailRespVO.ApprovalTaskInfo; import cn.iocoder.yudao.module.bpm.convert.task.BpmProcessInstanceConvert; import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO; import cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants; import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum; import cn.iocoder.yudao.module.bpm.enums.definition.BpmSimpleModelNodeType; import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum; import cn.iocoder.yudao.module.bpm.enums.task.BpmReasonEnum; import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum; import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker; import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy; import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.BpmTaskCandidateStartUserSelectStrategy; import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants; import cn.iocoder.yudao.module.bpm.framework.flowable.core.event.BpmProcessInstanceEventPublisher; import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils; import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils; import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.SimpleModelUtils; import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService; import cn.iocoder.yudao.module.bpm.service.message.BpmMessageService; import cn.iocoder.yudao.module.bpm.service.task.bo.AlreadyRunApproveNodeRespBO; import cn.iocoder.yudao.module.system.api.user.AdminUserApi; import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Multimap; import jakarta.annotation.Resource; import jakarta.validation.Valid; import lombok.extern.slf4j.Slf4j; import org.flowable.bpmn.model.BpmnModel; import org.flowable.bpmn.model.UserTask; import org.flowable.common.engine.api.FlowableException; import org.flowable.common.engine.api.variable.VariableContainer; import org.flowable.engine.HistoryService; import org.flowable.engine.ManagementService; import org.flowable.engine.RuntimeService; import org.flowable.engine.history.HistoricActivityInstance; import org.flowable.engine.history.HistoricProcessInstance; import org.flowable.engine.history.HistoricProcessInstanceQuery; import org.flowable.engine.repository.ProcessDefinition; import org.flowable.engine.runtime.ProcessInstance; import org.flowable.task.api.history.HistoricTaskInstance; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import java.util.*; import java.util.stream.Stream; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; import static cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmApprovalDetailRespVO.ApprovalNodeInfo; import static cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmApprovalDetailRespVO.User; import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*; import static cn.iocoder.yudao.module.bpm.enums.definition.BpmSimpleModelNodeType.*; import static cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskApproveTypeEnum.USER; import static cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum.*; import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum.START_USER; import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.START_USER_NODE_ID; /** * 流程实例 Service 实现类 *

* ProcessDefinition & ProcessInstance & Execution & Task 的关系: * 1. *

* HistoricProcessInstance & ProcessInstance 的关系: * 1. *

* 简单来说,前者 = 历史 + 运行中的流程实例,后者仅是运行中的流程实例 * * @author 芋道源码 */ @Service @Validated @Slf4j public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService { @Resource private RuntimeService runtimeService; @Resource private HistoryService historyService; @Resource private ManagementService managementService; @Resource private BpmActivityService activityService; @Resource private BpmProcessDefinitionService processDefinitionService; @Resource @Lazy // 避免循环依赖 private BpmTaskService taskService; @Resource private BpmMessageService messageService; @Resource private BpmTaskCandidateInvoker bpmTaskCandidateInvoker; @Resource private AdminUserApi adminUserApi; @Resource private BpmProcessInstanceEventPublisher processInstanceEventPublisher; // ========== Query 查询相关方法 ========== @Override public ProcessInstance getProcessInstance(String id) { return runtimeService.createProcessInstanceQuery() .includeProcessVariables() .processInstanceId(id) .singleResult(); } @Override public List getProcessInstances(Set ids) { return runtimeService.createProcessInstanceQuery().processInstanceIds(ids).list(); } @Override public HistoricProcessInstance getHistoricProcessInstance(String id) { return historyService.createHistoricProcessInstanceQuery().processInstanceId(id).includeProcessVariables().singleResult(); } @Override public List getHistoricProcessInstances(Set ids) { return historyService.createHistoricProcessInstanceQuery().processInstanceIds(ids).list(); } @Override public PageResult getProcessInstancePage(Long userId, BpmProcessInstancePageReqVO pageReqVO) { // 通过 BpmProcessInstanceExtDO 表,先查询到对应的分页 HistoricProcessInstanceQuery processInstanceQuery = historyService.createHistoricProcessInstanceQuery() .includeProcessVariables() .processInstanceTenantId(FlowableUtils.getTenantId()) .orderByProcessInstanceStartTime().desc(); if (userId != null) { // 【我的流程】菜单时,需要传递该字段 processInstanceQuery.startedBy(String.valueOf(userId)); } else if (pageReqVO.getStartUserId() != null) { // 【管理流程】菜单时,才会传递该字段 processInstanceQuery.startedBy(String.valueOf(pageReqVO.getStartUserId())); } if (StrUtil.isNotEmpty(pageReqVO.getName())) { processInstanceQuery.processInstanceNameLike("%" + pageReqVO.getName() + "%"); } if (StrUtil.isNotEmpty(pageReqVO.getProcessDefinitionKey())) { processInstanceQuery.processDefinitionKey(pageReqVO.getProcessDefinitionKey()); } if (StrUtil.isNotEmpty(pageReqVO.getCategory())) { processInstanceQuery.processDefinitionCategory(pageReqVO.getCategory()); } if (pageReqVO.getStatus() != null) { processInstanceQuery.variableValueEquals(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS, pageReqVO.getStatus()); } if (ArrayUtil.isNotEmpty(pageReqVO.getCreateTime())) { processInstanceQuery.startedAfter(DateUtils.of(pageReqVO.getCreateTime()[0])); processInstanceQuery.startedBefore(DateUtils.of(pageReqVO.getCreateTime()[1])); } // 查询数量 long processInstanceCount = processInstanceQuery.count(); if (processInstanceCount == 0) { return PageResult.empty(processInstanceCount); } // 查询列表 List processInstanceList = processInstanceQuery.listPage(PageUtils.getStart(pageReqVO), pageReqVO.getPageSize()); return new PageResult<>(processInstanceList, processInstanceCount); } @Override public Map getFormFieldsPermission(BpmFormFieldsPermissionReqVO reqVO) { // 1.1 获取流程定义 Id String processDefinitionId = reqVO.getProcessDefinitionId(); if (StrUtil.isEmpty(processDefinitionId) && StrUtil.isNotEmpty(reqVO.getProcessInstanceId())) { HistoricProcessInstance processInstance = getHistoricProcessInstance(reqVO.getProcessInstanceId()); if (processInstance == null) { throw exception(ErrorCodeConstants.PROCESS_INSTANCE_NOT_EXISTS); } processDefinitionId = processInstance.getProcessDefinitionId(); } // 1.2 获取流程活动编号 String activityId = reqVO.getActivityId(); if (StrUtil.isEmpty(activityId) && StrUtil.isNotEmpty(reqVO.getTaskId())) { // 流程活动 Id 为空。从流程任务中获取流程活动 Id activityId = Optional.ofNullable(taskService.getHistoricTask(reqVO.getTaskId())) .map(HistoricTaskInstance::getTaskDefinitionKey).orElse(null); } if (StrUtil.isEmpty(activityId)) { return null; } // 2. 从 BpmnModel 中解析表单字段权限 return BpmnModelUtils.parseFormFieldsPermission( processDefinitionService.getProcessDefinitionBpmnModel(processDefinitionId), activityId); } @Override public BpmApprovalDetailRespVO getApprovalDetail(Long startUserId, BpmApprovalDetailReqVO reqVO) { // 1. 审批详情 BpmApprovalDetailRespVO respVO = new BpmApprovalDetailRespVO(); String processDefinitionId = reqVO.getProcessDefinitionId(); ProcessInstance runProcessInstance = null; // 正在运行的流程实例 Set runNodeIds = new HashSet<>(); // 已经运行的节点 Ids (BPMN XML 节点 Id) Map runningApprovalNodes = new HashMap<>(); // 正在运行的节点的审批信息 List approvalNodes = new ArrayList<>(); // 1.1 情况一:流程未发起 if (reqVO.getProcessInstanceId() == null) { respVO.setStatus(BpmProcessInstanceStatusEnum.NOT_START.getStatus()); // 1.2 情况二:流程已发起 } else { // 1.2.1 获取流程实例状态 HistoricProcessInstance processInstance = getHistoricProcessInstance(reqVO.getProcessInstanceId()); if (processInstance == null) { throw exception(ErrorCodeConstants.PROCESS_INSTANCE_NOT_EXISTS); } Integer processInstanceStatus = FlowableUtils.getProcessInstanceStatus(processInstance); respVO.setStatus(processInstanceStatus); // 1.2.2 构建已运行节点的审批信息 List historicActivityList = activityService.getActivityListByProcessInstanceId(processInstance.getId()); AlreadyRunApproveNodeRespBO respBO = buildAlreadyRunApproveNodes(processInstance.getId(), processInstanceStatus, historicActivityList); approvalNodes = respBO.getApproveNodes(); runNodeIds = respBO.getRunNodeIds(); // 1.2.3 特殊:流程已经结束,直接 return,无需预测 if (BpmProcessInstanceStatusEnum.isProcessEndStatus(processInstanceStatus)) { respVO.setApproveNodes(approvalNodes); return respVO; } runningApprovalNodes = respBO.getRunningApprovalNodes(); processDefinitionId = processInstance.getProcessDefinitionId(); runProcessInstance = getProcessInstance(processInstance.getId()); startUserId = Long.valueOf(runProcessInstance.getStartUserId()); } // 2. 流程未结束,预测未运行节点的审批信息。需要区分 BPMN 设计器 和 SIMPLE 设计器 BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionService.getProcessDefinitionInfo(processDefinitionId); // 2.1 情况一:仿钉钉流程设计器 if (Objects.equals(BpmModelTypeEnum.SIMPLE.getType(), processDefinitionInfo.getModelType())) { BpmSimpleModelNodeVO simpleModel = JsonUtils.parseObject(processDefinitionInfo.getSimpleModel(), BpmSimpleModelNodeVO.class); List notRunApproveNodes = new ArrayList<>(); traverseSimpleModelNodeToBuildNotRunApproveNodes( startUserId, runProcessInstance, simpleModel, runNodeIds, runningApprovalNodes, notRunApproveNodes); approvalNodes.addAll(notRunApproveNodes); respVO.setApproveNodes(approvalNodes); // 会不会有极端的情况:对于依次审批来说,它是已经 running,但是当前节点也要计算其它审批人?(已修改) // 2.2 情况二:BPMN 流程设计器 } else if (Objects.equals(BpmModelTypeEnum.BPMN.getType(), processDefinitionInfo.getModelType())) { // TODO 芋艿:需要把 start 节点加出来 // TODO Bpmn 设计器,构建未运行节点的审批信息;未完全实现 respVO.setApproveNodes(approvalNodes); } return respVO; } /** * 遍历 SIMPLE 设计器模型 构建未运行节点的审批信息 * * @param startUserId 流程发起人编号 * @param processInstance 流程实例 * @param simpleModelNode SIMPLE 设计器模型 * @param runNodeIds 已经运行节点的 Ids * @param runningApprovalNodes 正在运行的节点的审批信息 * @param approveNodeList 未运行节点的审批信息列表 */ private void traverseSimpleModelNodeToBuildNotRunApproveNodes(Long startUserId, ProcessInstance processInstance, BpmSimpleModelNodeVO simpleModelNode, Set runNodeIds, Map runningApprovalNodes, List approveNodeList) { if (!SimpleModelUtils.isValidNode(simpleModelNode)) { return; } buildNotRunApproveNodes(startUserId, processInstance, simpleModelNode, runNodeIds, runningApprovalNodes, approveNodeList); // 如果有子节点递归遍历子节点 traverseSimpleModelNodeToBuildNotRunApproveNodes( startUserId, processInstance, simpleModelNode.getChildNode(), runNodeIds, runningApprovalNodes, approveNodeList); } private void buildNotRunApproveNodes(Long startUserId, ProcessInstance processInstance, BpmSimpleModelNodeVO node, Set runNodeIds, Map runningApprovalNodes, List approveNodeList) { // 情况一:节点未运行:需要进行预测 if (!runNodeIds.contains(node.getId())) { // 1. 对需要人工审批的审批节点,进行预测 if (APPROVE_NODE.getType().equals(node.getType()) && USER.getType().equals(node.getApproveType()) || START_USER_NODE.getType().equals(node.getType())) { ApprovalNodeInfo approvalNodeInfo = new ApprovalNodeInfo().setNodeType(node.getType()) .setName(node.getName()).setStatus(NOT_START.getStatus()); Integer candidateStrategy = START_USER_NODE.getType().equals(node.getType()) ? START_USER.getStrategy() : node.getCandidateStrategy(); approvalNodeInfo.setCandidateUserList( getNotRunTaskCandidateUserList(startUserId, processInstance, node.getId(), candidateStrategy, node.getCandidateParam())); approveNodeList.add(approvalNodeInfo); // 2. 对分支节点,进行预测 } else if (BpmSimpleModelNodeType.isBranchNode(node.getType())) { // 并行分支,不用预测条件。所有分支都需要遍历 if (PARALLEL_BRANCH_NODE.getType().equals(node.getType())) { node.getConditionNodes().forEach(conditionNode -> traverseSimpleModelNodeToBuildNotRunApproveNodes(startUserId, processInstance, conditionNode.getChildNode() , runNodeIds, runningApprovalNodes, approveNodeList)); } else if (CONDITION_BRANCH_NODE.getType().equals(node.getType())) { for (BpmSimpleModelNodeVO conditionNode : node.getConditionNodes()) { // 满足一个条件, 遍历该分支并 if ((processInstance != null && evalConditionExpress(processInstance, SimpleModelUtils.buildConditionExpression(conditionNode))) // 预测条件表达式的值 || BooleanUtil.isTrue(conditionNode.getDefaultFlow())) { // 是否默认的序列 traverseSimpleModelNodeToBuildNotRunApproveNodes(startUserId, processInstance, conditionNode.getChildNode(), runNodeIds, runningApprovalNodes, approveNodeList); break; } } } // TODO 包容分支待实现 // 3. 结束节点 } else if (END_NODE.getType().equals(node.getType())) { ApprovalNodeInfo nodeProgress = new ApprovalNodeInfo(); nodeProgress.setNodeType(node.getType()); nodeProgress.setName(node.getName()); nodeProgress.setStatus(NOT_START.getStatus()); approveNodeList.add(nodeProgress); } } else { // 情况二:节点已经运行 // 如果是分支节点,需要检查分支节点的运行情况 if (BpmSimpleModelNodeType.isBranchNode(node.getType()) && ArrayUtil.isNotEmpty(node.getConditionNodes())) { node.getConditionNodes().forEach(conditionNode -> { // 只有运行的条件,才需要遍历 if (runNodeIds.contains(conditionNode.getId())) { traverseSimpleModelNodeToBuildNotRunApproveNodes(startUserId, processInstance, conditionNode.getChildNode() , runNodeIds, runningApprovalNodes, approveNodeList); } }); // 如果是依次审批, 需要加其它未审批候选人 } else if (SimpleModelUtils.isSequentialApproveNode(node) && runningApprovalNodes.containsKey(node.getId())) { ApprovalNodeInfo approvalNodeInfo = runningApprovalNodes.get(node.getId()); List candidateUserList = getNotRunTaskCandidateUserList( startUserId, processInstance, node.getId(), node.getCandidateStrategy(), node.getCandidateParam()); // TODO @jason:这里的逻辑,可能可以简化成,直接拿已经审批过的人的 userId 集合,从 candidateUserList remove 下。一方面简单一点,方面 calculateUsers 返回的是 set,目前不是很有序。 ApprovalTaskInfo approvalTaskInfo = CollUtil.getFirst(approvalNodeInfo.getTasks()); Long currentAssignedUserId = null; if (approvalTaskInfo != null && approvalTaskInfo.getAssigneeUser() != null) { currentAssignedUserId = approvalTaskInfo.getAssigneeUser().getId(); } // 找到当前审批人在候选人列表的位置 int index = 0; for (User user : candidateUserList) { if (user.getId().equals(currentAssignedUserId)) { break; } index++; } // 截取当前审批人位置后面的候选人, 不包含当前审批人 approvalNodeInfo.setCandidateUserList(CollUtil.sub(candidateUserList, ++index, candidateUserList.size())); } } } /** * 从已经运行活动节点构建的审批信息列表 * * @param processInstanceId 流程实例 Id * @param processInstanceStatus 流程实例状态 * @param historicActivityList 已经运行活动 */ private AlreadyRunApproveNodeRespBO buildAlreadyRunApproveNodes(String processInstanceId, Integer processInstanceStatus, List historicActivityList) { // 1.1 获取待处理活动:只有 "userTask" 和 "endEvent" 需要处理 List pendingActivityNodes = filterList(historicActivityList, item -> BpmSimpleModelNodeType.isRecordNode(item.getActivityType())); // 1.2 已运行节点的 activityId Set runNodeIds = convertSet(historicActivityList, HistoricActivityInstance::getActivityId); // 2.1 获取已运行的任务(包括运行中的任务) List taskList = taskService.getTaskListByProcessInstanceId(processInstanceId); Map taskMap = convertMap(taskList, HistoricTaskInstance::getId); // 2.2 获取加签的任务 Map> addSignTaskMap = convertMultiMap( filterList(taskList, task -> StrUtil.isNotEmpty(task.getParentTaskId())), HistoricTaskInstance::getParentTaskId); // 3.1 获取节点的用户信息 Set userIds = CollectionUtils.convertSetByFlatMap(pendingActivityNodes, activity -> { if (BPMN_USER_TASK_TYPE.equals(activity.getActivityType())) { HistoricTaskInstance task = taskMap.get(activity.getTaskId()); Set taskUsers = CollUtil.newHashSet(); CollUtil.addIfAbsent(taskUsers, NumberUtil.parseLong(task.getAssignee(), null)); CollUtil.addIfAbsent(taskUsers, NumberUtil.parseLong(task.getOwner(), null)); List addSignTasks = addSignTaskMap.get(activity.getTaskId()); if (CollUtil.isNotEmpty(addSignTasks)) { addSignTasks.forEach(item -> { CollUtil.addIfAbsent(taskUsers, NumberUtil.parseLong(item.getAssignee(), null)); CollUtil.addIfAbsent(taskUsers, NumberUtil.parseLong(item.getOwner(), null)); }); } return taskUsers.stream(); } else { return Stream.empty(); } }); Map userMap = convertMap(adminUserApi.getUserList(userIds).getCheckedData(), AdminUserRespDTO::getId); // 3.2 已经结束的任务转换为审批信息 final Multimap runningTask = ArrayListMultimap.create(); // 运行中的任务 List approvalNodeList = convertList(pendingActivityNodes, activity -> { ApprovalNodeInfo approvalNodeInfo = new ApprovalNodeInfo().setId(activity.getId()).setName(activity.getActivityName()) .setStartTime(DateUtils.of(activity.getStartTime())).setEndTime(DateUtils.of(activity.getEndTime())); if (BPMN_USER_TASK_TYPE.equals(activity.getActivityType())) { // 用户任务 // nodeType approvalNodeInfo.setNodeType(START_USER_NODE_ID.equals(activity.getActivityId()) ? START_USER_NODE.getType() : APPROVE_NODE.getType()); // status HistoricTaskInstance task = taskMap.get(activity.getTaskId()); Integer taskStatus = FlowableUtils.getTaskStatus(task); // 运行中的任务, 会签,或签任务聚合在一起。 if (!BpmTaskStatusEnum.isEndStatus(taskStatus)) { runningTask.put(activity.getActivityId(), activity); return null; } // tasks ApprovalTaskInfo approveTask = convertApproveTaskInfo(task, userMap); List approveTasks = CollUtil.newArrayList(approveTask); List addSignTasks = addSignTaskMap.get(activity.getTaskId()); if (CollUtil.isNotEmpty(addSignTasks)) { // 处理加签任务 approveTasks.addAll(convertList(addSignTasks, item -> convertApproveTaskInfo(item, userMap))); } approvalNodeInfo.setStatus(taskStatus); approvalNodeInfo.setTasks(approveTasks); } else if (END_NODE.getBpmnType().equals(activity.getActivityType())) { approvalNodeInfo.setNodeType(END_NODE.getType()); approvalNodeInfo.setStatus(processInstanceStatus); } return approvalNodeInfo; }); // 3.3 运行中的任务转换为审批信息。 final Map runningApprovalNodes = new HashMap<>(); // 正在运行节点的审批信息 runningTask.asMap().forEach((activityId, activities) -> { if (CollUtil.isNotEmpty(activities)) { ApprovalNodeInfo approvalNodeInfo = new ApprovalNodeInfo(); approvalNodeInfo.setNodeType(APPROVE_NODE.getType()); approvalNodeInfo.setStatus(RUNNING.getStatus()); List approveTasks = CollUtil.newArrayList(); int i = 0; for (HistoricActivityInstance activity : activities) { HistoricTaskInstance task = taskMap.get(activity.getTaskId()); // 取第一个任务, 会签/或签的任务。开始时间相同的 if (i == 0) { approvalNodeInfo.setId(activity.getId()).setName(activity.getActivityName()). setStartTime(DateUtils.of(activity.getStartTime())); } // tasks ApprovalTaskInfo approveTask = convertApproveTaskInfo(task, userMap); approveTasks.add(approveTask); List addSignTasks = addSignTaskMap.get(activity.getTaskId()); if (CollUtil.isNotEmpty(addSignTasks)) { // 处理加签任务 approveTasks.addAll(convertList(addSignTasks, item -> convertApproveTaskInfo(item, userMap))); } i++; } approvalNodeInfo.setTasks(approveTasks); approvalNodeList.add(approvalNodeInfo); runningApprovalNodes.put(activityId, approvalNodeInfo); } }); return new AlreadyRunApproveNodeRespBO().setApproveNodes(approvalNodeList).setRunNodeIds(runNodeIds) .setRunningApprovalNodes(runningApprovalNodes); } private ApprovalTaskInfo convertApproveTaskInfo(HistoricTaskInstance task, Map userMap) { if (task == null) { return null; } ApprovalTaskInfo approveTask = BeanUtils.toBean(task, ApprovalTaskInfo.class); approveTask.setStatus(FlowableUtils.getTaskStatus(task)).setReason(FlowableUtils.getTaskReason(task)); Long taskAssignee = NumberUtil.parseLong(task.getAssignee(), null); if (taskAssignee != null) { approveTask.setAssigneeUser(BeanUtils.toBean(userMap.get(taskAssignee), User.class)); } Long taskOwner = NumberUtil.parseLong(task.getOwner(), null); if (taskOwner != null) { approveTask.setOwnerUser(BeanUtils.toBean(userMap.get(taskOwner), User.class)); } return approveTask; } private List getNotRunTaskCandidateUserList(Long startUserId, ProcessInstance processInstance, String activityId, Integer candidateStrategy, String candidateParam) { BpmTaskCandidateStrategy taskCandidateStrategy = bpmTaskCandidateInvoker.getCandidateStrategy(candidateStrategy); Set userIds = taskCandidateStrategy.calculateUsers(startUserId, processInstance, activityId, candidateParam); Map adminUserMap = adminUserApi.getUserMap(userIds); // 需要按照候选人的顺序返回。原因是,依次审批需要按顺序展示用户 return convertList(userIds, userId -> BeanUtils.toBean(adminUserMap.get(userId), User.class)); } /** * 计算条件表达式的值 * * @param processInstance 流程实例 * @param express 条件表达式 */ private Boolean evalConditionExpress(ProcessInstance processInstance, String express) { if (express == null) { return Boolean.FALSE; } Object result = managementService.executeCommand(context -> { try { return FlowableUtils.getExpressionValue((VariableContainer) processInstance, express); } catch (FlowableException ex) { log.error("[evalConditionExpress][条件表达式({}) 解析报错", express, ex); return Boolean.FALSE; } }); return Boolean.TRUE.equals(result); } // ========== Update 写入相关方法 ========== @Override @Transactional(rollbackFor = Exception.class) public String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqVO createReqVO) { // 获得流程定义 ProcessDefinition definition = processDefinitionService.getProcessDefinition(createReqVO.getProcessDefinitionId()); // 发起流程 return createProcessInstance0(userId, definition, createReqVO.getVariables(), null, createReqVO.getStartUserSelectAssignees()); } @Override public String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqDTO createReqDTO) { // 获得流程定义 ProcessDefinition definition = processDefinitionService.getActiveProcessDefinition(createReqDTO.getProcessDefinitionKey()); // 发起流程 return createProcessInstance0(userId, definition, createReqDTO.getVariables(), createReqDTO.getBusinessKey(), createReqDTO.getStartUserSelectAssignees()); } private String createProcessInstance0(Long userId, ProcessDefinition definition, Map variables, String businessKey, Map> startUserSelectAssignees) { // 1.1 校验流程定义 if (definition == null) { throw exception(PROCESS_DEFINITION_NOT_EXISTS); } if (definition.isSuspended()) { throw exception(PROCESS_DEFINITION_IS_SUSPENDED); } BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionService.getProcessDefinitionInfo(definition.getId()); if (processDefinitionInfo == null) { throw exception(PROCESS_DEFINITION_NOT_EXISTS); } // 1.2 校验是否能够发起 if (!processDefinitionService.canUserStartProcessDefinition(processDefinitionInfo, userId)) { throw exception(PROCESS_INSTANCE_START_USER_CAN_START); } // 1.3 校验发起人自选审批人 validateStartUserSelectAssignees(definition, startUserSelectAssignees); // 2. 创建流程实例 if (variables == null) { variables = new HashMap<>(); } FlowableUtils.filterProcessInstanceFormVariable(variables); // 过滤一下,避免 ProcessInstance 系统级的变量被占用 variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS, // 流程实例状态:审批中 BpmProcessInstanceStatusEnum.RUNNING.getStatus()); if (CollUtil.isNotEmpty(startUserSelectAssignees)) { variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES, startUserSelectAssignees); } ProcessInstance instance = runtimeService.createProcessInstanceBuilder() .processDefinitionId(definition.getId()) .businessKey(businessKey) .name(definition.getName().trim()) .variables(variables) .start(); return instance.getId(); } private void validateStartUserSelectAssignees(ProcessDefinition definition, Map> startUserSelectAssignees) { // 1. 获得发起人自选审批人的 UserTask 列表 BpmnModel bpmnModel = processDefinitionService.getProcessDefinitionBpmnModel(definition.getId()); List userTaskList = BpmTaskCandidateStartUserSelectStrategy.getStartUserSelectUserTaskList(bpmnModel); if (CollUtil.isEmpty(userTaskList)) { return; } // 2. 校验发起人自选审批人的 UserTask 是否都配置了 userTaskList.forEach(userTask -> { List assignees = startUserSelectAssignees != null ? startUserSelectAssignees.get(userTask.getId()) : null; if (CollUtil.isEmpty(assignees)) { throw exception(PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_CONFIG, userTask.getName()); } Map userMap = adminUserApi.getUserMap(assignees); assignees.forEach(assignee -> { if (userMap.get(assignee) == null) { throw exception(PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_EXISTS, userTask.getName(), assignee); } }); }); } @Override public void cancelProcessInstanceByStartUser(Long userId, @Valid BpmProcessInstanceCancelReqVO cancelReqVO) { // 1.1 校验流程实例存在 ProcessInstance instance = getProcessInstance(cancelReqVO.getId()); if (instance == null) { throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_EXISTS); } // 1.2 只能取消自己的 if (!Objects.equals(instance.getStartUserId(), String.valueOf(userId))) { throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_SELF); } // 2. 取消流程 updateProcessInstanceCancel(cancelReqVO.getId(), BpmReasonEnum.CANCEL_PROCESS_INSTANCE_BY_START_USER.format(cancelReqVO.getReason())); } @Override public void cancelProcessInstanceByAdmin(Long userId, BpmProcessInstanceCancelReqVO cancelReqVO) { // 1.1 校验流程实例存在 ProcessInstance instance = getProcessInstance(cancelReqVO.getId()); if (instance == null) { throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_EXISTS); } // 2. 取消流程 AdminUserRespDTO user = adminUserApi.getUser(userId).getCheckedData(); updateProcessInstanceCancel(cancelReqVO.getId(), BpmReasonEnum.CANCEL_PROCESS_INSTANCE_BY_ADMIN.format(user.getNickname(), cancelReqVO.getReason())); } private void updateProcessInstanceCancel(String id, String reason) { // 1. 更新流程实例 status runtimeService.setVariable(id, BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS, BpmProcessInstanceStatusEnum.CANCEL.getStatus()); runtimeService.setVariable(id, BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_REASON, reason); // 2. 结束流程 taskService.moveTaskToEnd(id); } @Override public void updateProcessInstanceReject(ProcessInstance processInstance, String reason) { runtimeService.setVariable(processInstance.getProcessInstanceId(), BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS, BpmProcessInstanceStatusEnum.REJECT.getStatus()); runtimeService.setVariable(processInstance.getProcessInstanceId(), BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_REASON, BpmReasonEnum.REJECT_TASK.format(reason)); } // ========== Event 事件相关方法 ========== @Override public void processProcessInstanceCompleted(ProcessInstance instance) { // 注意:需要基于 instance 设置租户编号,避免 Flowable 内部异步时,丢失租户编号 FlowableUtils.execute(instance.getTenantId(), () -> { // 1.1 获取当前状态 Integer status = (Integer) instance.getProcessVariables().get(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS); String reason = (String) instance.getProcessVariables().get(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_REASON); // 1.2 当流程状态还是审批状态中,说明审批通过了,则变更下它的状态 // 为什么这么处理?因为流程完成,并且完成了,说明审批通过了 if (Objects.equals(status, BpmProcessInstanceStatusEnum.RUNNING.getStatus())) { status = BpmProcessInstanceStatusEnum.APPROVE.getStatus(); runtimeService.setVariable(instance.getId(), BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS, status); } // 2. 发送对应的消息通知 if (Objects.equals(status, BpmProcessInstanceStatusEnum.APPROVE.getStatus())) { messageService.sendMessageWhenProcessInstanceApprove(BpmProcessInstanceConvert.INSTANCE.buildProcessInstanceApproveMessage(instance)); } else if (Objects.equals(status, BpmProcessInstanceStatusEnum.REJECT.getStatus())) { messageService.sendMessageWhenProcessInstanceReject( BpmProcessInstanceConvert.INSTANCE.buildProcessInstanceRejectMessage(instance, reason)); } // 3. 发送流程实例的状态事件 processInstanceEventPublisher.sendProcessInstanceResultEvent( BpmProcessInstanceConvert.INSTANCE.buildProcessInstanceStatusEvent(this, instance, status)); }); } } \ No newline at end of file +package cn.iocoder.yudao.module.bpm.service.task; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.ListUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.date.DateUtils; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; import cn.iocoder.yudao.framework.common.util.object.PageUtils; import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO; import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO; import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.*; import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmApprovalDetailRespVO.ActivityNodeTask; import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.BpmTaskRespVO; import cn.iocoder.yudao.module.bpm.convert.task.BpmProcessInstanceConvert; import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO; import cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants; import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum; import cn.iocoder.yudao.module.bpm.enums.definition.BpmSimpleModelNodeType; import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum; import cn.iocoder.yudao.module.bpm.enums.task.BpmReasonEnum; import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum; import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker; import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept.BpmTaskCandidateStartUserSelectStrategy; import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants; import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants; import cn.iocoder.yudao.module.bpm.framework.flowable.core.event.BpmProcessInstanceEventPublisher; import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils; import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils; import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.SimpleModelUtils; import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService; import cn.iocoder.yudao.module.bpm.service.message.BpmMessageService; import cn.iocoder.yudao.module.system.api.dept.DeptApi; import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; import cn.iocoder.yudao.module.system.api.user.AdminUserApi; import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; import jakarta.annotation.Resource; import jakarta.validation.Valid; import lombok.extern.slf4j.Slf4j; import org.flowable.bpmn.constants.BpmnXMLConstants; import org.flowable.bpmn.model.*; import org.flowable.engine.HistoryService; import org.flowable.engine.RuntimeService; import org.flowable.engine.history.HistoricActivityInstance; import org.flowable.engine.history.HistoricProcessInstance; import org.flowable.engine.history.HistoricProcessInstanceQuery; import org.flowable.engine.repository.ProcessDefinition; import org.flowable.engine.runtime.ProcessInstance; import org.flowable.task.api.history.HistoricTaskInstance; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import java.util.*; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; import static cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmApprovalDetailRespVO.ActivityNode; import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*; import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.START_USER_NODE_ID; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static org.flowable.bpmn.constants.BpmnXMLConstants.*; /** * 流程实例 Service 实现类 *

* ProcessDefinition & ProcessInstance & Execution & Task 的关系: * 1. *

* HistoricProcessInstance & ProcessInstance 的关系: * 1. *

* 简单来说,前者 = 历史 + 运行中的流程实例,后者仅是运行中的流程实例 * * @author 芋道源码 */ @Service @Validated @Slf4j public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService { @Resource private RuntimeService runtimeService; @Resource private HistoryService historyService; @Resource private BpmProcessDefinitionService processDefinitionService; @Resource @Lazy // 避免循环依赖 private BpmTaskService taskService; @Resource private BpmMessageService messageService; @Resource private AdminUserApi adminUserApi; @Resource private DeptApi deptApi; @Resource private BpmProcessInstanceEventPublisher processInstanceEventPublisher; @Resource private BpmTaskCandidateInvoker taskCandidateInvoker; // ========== Query 查询相关方法 ========== @Override public ProcessInstance getProcessInstance(String id) { return runtimeService.createProcessInstanceQuery() .includeProcessVariables() .processInstanceId(id) .singleResult(); } @Override public List getProcessInstances(Set ids) { return runtimeService.createProcessInstanceQuery().processInstanceIds(ids).list(); } @Override public HistoricProcessInstance getHistoricProcessInstance(String id) { return historyService.createHistoricProcessInstanceQuery().processInstanceId(id).includeProcessVariables().singleResult(); } @Override public List getHistoricProcessInstances(Set ids) { return historyService.createHistoricProcessInstanceQuery().processInstanceIds(ids).list(); } @Override public PageResult getProcessInstancePage(Long userId, BpmProcessInstancePageReqVO pageReqVO) { // 通过 BpmProcessInstanceExtDO 表,先查询到对应的分页 HistoricProcessInstanceQuery processInstanceQuery = historyService.createHistoricProcessInstanceQuery() .includeProcessVariables() .processInstanceTenantId(FlowableUtils.getTenantId()) .orderByProcessInstanceStartTime().desc(); if (userId != null) { // 【我的流程】菜单时,需要传递该字段 processInstanceQuery.startedBy(String.valueOf(userId)); } else if (pageReqVO.getStartUserId() != null) { // 【管理流程】菜单时,才会传递该字段 processInstanceQuery.startedBy(String.valueOf(pageReqVO.getStartUserId())); } if (StrUtil.isNotEmpty(pageReqVO.getName())) { processInstanceQuery.processInstanceNameLike("%" + pageReqVO.getName() + "%"); } if (StrUtil.isNotEmpty(pageReqVO.getProcessDefinitionKey())) { processInstanceQuery.processDefinitionKey(pageReqVO.getProcessDefinitionKey()); } if (StrUtil.isNotEmpty(pageReqVO.getCategory())) { processInstanceQuery.processDefinitionCategory(pageReqVO.getCategory()); } if (pageReqVO.getStatus() != null) { processInstanceQuery.variableValueEquals(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS, pageReqVO.getStatus()); } if (ArrayUtil.isNotEmpty(pageReqVO.getCreateTime())) { processInstanceQuery.startedAfter(DateUtils.of(pageReqVO.getCreateTime()[0])); processInstanceQuery.startedBefore(DateUtils.of(pageReqVO.getCreateTime()[1])); } // 查询数量 long processInstanceCount = processInstanceQuery.count(); if (processInstanceCount == 0) { return PageResult.empty(processInstanceCount); } // 查询列表 List processInstanceList = processInstanceQuery.listPage(PageUtils.getStart(pageReqVO), pageReqVO.getPageSize()); return new PageResult<>(processInstanceList, processInstanceCount); } private Map getFormFieldsPermission(BpmnModel bpmnModel, String activityId, String taskId) { // 1. 获取流程活动编号。流程活动 Id 为空事,从流程任务中获取流程活动 Id if (StrUtil.isEmpty(activityId) && StrUtil.isNotEmpty(taskId)) { activityId = Optional.ofNullable(taskService.getHistoricTask(taskId)) .map(HistoricTaskInstance::getTaskDefinitionKey).orElse(null); } if (StrUtil.isEmpty(activityId)) { return null; } // 2. 从 BpmnModel 中解析表单字段权限 return BpmnModelUtils.parseFormFieldsPermission(bpmnModel, activityId); } @Override public BpmApprovalDetailRespVO getApprovalDetail(Long loginUserId, BpmApprovalDetailReqVO reqVO) { // 1.1 从 reqVO 中,读取公共变量 Long startUserId = loginUserId; // 流程发起人 HistoricProcessInstance historicProcessInstance = null; // 流程实例 Integer processInstanceStatus = BpmProcessInstanceStatusEnum.NOT_START.getStatus(); // 流程状态 Map processVariables = reqVO.getProcessVariables(); // 流程变量 // 1.2 如果是流程已发起的场景,则使用流程实例的数据 if (reqVO.getProcessInstanceId() != null) { historicProcessInstance = getHistoricProcessInstance(reqVO.getProcessInstanceId()); if (historicProcessInstance == null) { throw exception(ErrorCodeConstants.PROCESS_INSTANCE_NOT_EXISTS); } startUserId = Long.valueOf(historicProcessInstance.getStartUserId()); processInstanceStatus = FlowableUtils.getProcessInstanceStatus(historicProcessInstance); processVariables = historicProcessInstance.getProcessVariables(); } // 1.3 读取其它相关数据 ProcessDefinition processDefinition = processDefinitionService.getProcessDefinition( historicProcessInstance != null ? historicProcessInstance.getProcessDefinitionId() : reqVO.getProcessDefinitionId()); BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionService.getProcessDefinitionInfo(processDefinition.getId()); BpmnModel bpmnModel = processDefinitionService.getProcessDefinitionBpmnModel(processDefinition.getId()); // 2.1 已结束 + 进行中的活动节点 List endActivityNodes = null; // 已结束的审批信息 List runActivityNodes = null; // 进行中的审批信息 List activities = null; // 流程实例列表 if (reqVO.getProcessInstanceId() != null) { activities = taskService.getActivityListByProcessInstanceId(reqVO.getProcessInstanceId()); List tasks = taskService.getTaskListByProcessInstanceId(reqVO.getProcessInstanceId(), true); endActivityNodes = getEndActivityNodeList(startUserId, bpmnModel, processDefinitionInfo, historicProcessInstance, processInstanceStatus, activities, tasks); runActivityNodes = getRunApproveNodeList(startUserId, bpmnModel, processDefinition, processVariables, activities, tasks); } // 2.2 流程已经结束,直接 return,无需预测 if (BpmProcessInstanceStatusEnum.isProcessEndStatus(processInstanceStatus)) { return buildApprovalDetail(reqVO, bpmnModel, processDefinition, processDefinitionInfo, historicProcessInstance, processInstanceStatus, endActivityNodes, runActivityNodes, null, null); } // 3.1 计算当前登录用户的待办任务 // TODO @jason:有一个极端情况,如果一个用户有 2 个 task A 和 B,A 已经通过,B 需要审核。这个时,通过 A 进来,todo 拿到 B,会不会表单权限不一致哈。 BpmTaskRespVO todoTask = taskService.getFirstTodoTask(loginUserId, reqVO.getProcessInstanceId()); // 3.2 预测未运行节点的审批信息 List simulateActivityNodes = getSimulateApproveNodeList(startUserId, bpmnModel, processDefinitionInfo, processVariables, activities); // 4. 拼接最终数据 return buildApprovalDetail(reqVO, bpmnModel, processDefinition, processDefinitionInfo, historicProcessInstance, processInstanceStatus, endActivityNodes, runActivityNodes, simulateActivityNodes, todoTask); } /** * 拼接审批详情的最终数据 *

* 主要是,拼接审批人的用户信息、部门信息 */ private BpmApprovalDetailRespVO buildApprovalDetail(BpmApprovalDetailReqVO reqVO, BpmnModel bpmnModel, ProcessDefinition processDefinition, BpmProcessDefinitionInfoDO processDefinitionInfo, HistoricProcessInstance processInstance, Integer processInstanceStatus, List endApprovalNodeInfos, List runningApprovalNodeInfos, List simulateApprovalNodeInfos, BpmTaskRespVO todoTask) { // 1. 获取所有需要读取用户信息的 userIds List approveNodes = newArrayList(asList(endApprovalNodeInfos, runningApprovalNodeInfos, simulateApprovalNodeInfos)); Set userIds = BpmProcessInstanceConvert.INSTANCE.parseUserIds(processInstance, approveNodes, todoTask); Map userMap = adminUserApi.getUserMap(userIds); Map deptMap = deptApi.getDeptMap(convertSet(userMap.values(), AdminUserRespDTO::getDeptId)); // 2. 表单权限 Map formFieldsPermission = getFormFieldsPermission(bpmnModel, reqVO.getActivityId(), reqVO.getTaskId()); // 3. 拼接数据 return BpmProcessInstanceConvert.INSTANCE.buildApprovalDetail(bpmnModel, processDefinition, processDefinitionInfo, processInstance, processInstanceStatus, approveNodes, todoTask, formFieldsPermission, userMap, deptMap); } /** * 获得【已结束】的活动节点们 */ private List getEndActivityNodeList(Long startUserId, BpmnModel bpmnModel, BpmProcessDefinitionInfoDO processDefinitionInfo, HistoricProcessInstance historicProcessInstance, Integer processInstanceStatus, List activities, List tasks) { // 遍历 tasks 列表,只处理已结束的 UserTask // 为什么不通过 activities 呢?因为,加签场景下,它只存在于 tasks,没有 activities,导致如果遍历 activities 的话,它无法成为一个节点 List endTasks = filterList(tasks, task -> task.getEndTime() != null); List approvalNodes = convertList(endTasks, task -> { FlowElement flowNode = BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey()); ActivityNode activityNode = new ActivityNode().setId(task.getTaskDefinitionKey()).setName(task.getName()) .setNodeType(START_USER_NODE_ID.equals(task.getTaskDefinitionKey()) ? BpmSimpleModelNodeType.START_USER_NODE.getType() : BpmSimpleModelNodeType.APPROVE_NODE.getType()) .setStatus(FlowableUtils.getTaskStatus(task)) .setCandidateStrategy(BpmnModelUtils.parseCandidateStrategy(flowNode)) .setStartTime(DateUtils.of(task.getCreateTime())).setEndTime(DateUtils.of(task.getEndTime())) .setTasks(singletonList(BpmProcessInstanceConvert.INSTANCE.buildApprovalTaskInfo(task))); // 如果是取消状态,则跳过 if (BpmTaskStatusEnum.isCancelStatus(activityNode.getStatus())) { return null; } return activityNode; }); // 遍历 activities,只处理已结束的 StartEvent、EndEvent List endActivities = filterList(activities, activity -> activity.getEndTime() != null && (StrUtil.equalsAny(activity.getActivityType(), ELEMENT_EVENT_START, ELEMENT_EVENT_END))); endActivities.forEach(activity -> { // StartEvent:只处理 BPMN 的场景。因为,SIMPLE 情况下,已经有 START_USER_NODE 节点 if (ELEMENT_EVENT_START.equals(activity.getActivityType()) && BpmModelTypeEnum.BPMN.getType().equals(processDefinitionInfo.getModelType())) { ActivityNodeTask startTask = new ActivityNodeTask().setId(BpmnModelConstants.START_USER_NODE_ID) .setAssignee(startUserId).setStatus(BpmTaskStatusEnum.APPROVE.getStatus()); ActivityNode startNode = new ActivityNode().setId(startTask.getId()) .setName(BpmSimpleModelNodeType.START_USER_NODE.getName()) .setNodeType(BpmSimpleModelNodeType.START_USER_NODE.getType()) .setStatus(startTask.getStatus()).setTasks(ListUtil.of(startTask)) .setStartTime(DateUtils.of(activity.getStartTime())).setEndTime(DateUtils.of(activity.getEndTime())); approvalNodes.add(0, startNode); return; } // EndEvent if (ELEMENT_EVENT_END.equals(activity.getActivityType())) { if (BpmProcessInstanceStatusEnum.isRejectStatus(processInstanceStatus)) { // 拒绝情况下,不需要展示 EndEvent 结束节点。原因是:前端已经展示 x 效果,无需重复展示 return; } ActivityNode endNode = new ActivityNode().setId(activity.getId()) .setName(BpmSimpleModelNodeType.END_NODE.getName()) .setNodeType(BpmSimpleModelNodeType.END_NODE.getType()).setStatus(processInstanceStatus) .setStartTime(DateUtils.of(activity.getStartTime())).setEndTime(DateUtils.of(activity.getEndTime())); String reason = FlowableUtils.getProcessInstanceReason(historicProcessInstance); if (StrUtil.isNotEmpty(reason)) { endNode.setTasks(singletonList(new ActivityNodeTask().setId(endNode.getId()) .setStatus(endNode.getStatus()).setReason(reason))); } approvalNodes.add(endNode); } }); return approvalNodes; } /** * 获得【进行中】的活动节点们 */ private List getRunApproveNodeList(Long startUserId, BpmnModel bpmnModel, ProcessDefinition processDefinition, Map processVariables, List activities, List tasks) { // 构建运行中的任务,基于 activityId 分组 List runActivities = filterList(activities, activity -> activity.getEndTime() == null && (StrUtil.equalsAny(activity.getActivityType(), ELEMENT_TASK_USER))); Map> runningTaskMap = convertMultiMap(runActivities, HistoricActivityInstance::getActivityId); // 按照 activityId 分组,构建 ApprovalNodeInfo 节点 Map taskMap = convertMap(tasks, HistoricTaskInstance::getId); return convertList(runningTaskMap.entrySet(), entry -> { String activityId = entry.getKey(); List taskActivities = entry.getValue(); // 构建活动节点 FlowElement flowNode = BpmnModelUtils.getFlowElementById(bpmnModel, activityId); HistoricActivityInstance firstActivity = CollUtil.getFirst(taskActivities); // 取第一个任务,会签/或签的任务,开始时间相同 ActivityNode activityNode = new ActivityNode().setId(firstActivity.getActivityId()).setName(firstActivity.getActivityName()) .setNodeType(BpmSimpleModelNodeType.APPROVE_NODE.getType()).setStatus(BpmTaskStatusEnum.RUNNING.getStatus()) .setCandidateStrategy(BpmnModelUtils.parseCandidateStrategy(flowNode)) .setStartTime(DateUtils.of(CollUtil.getFirst(taskActivities).getStartTime())) .setTasks(new ArrayList<>()); // 处理每个任务的 tasks 属性 for (HistoricActivityInstance activity : taskActivities) { HistoricTaskInstance task = taskMap.get(activity.getTaskId()); activityNode.getTasks().add(BpmProcessInstanceConvert.INSTANCE.buildApprovalTaskInfo(task)); // 加签子任务,需要过滤掉已经完成的加签子任务 List childrenTasks = filterList( taskService.getAllChildrenTaskListByParentTaskId(activity.getTaskId(), tasks), childTask -> childTask.getEndTime() == null); if (CollUtil.isNotEmpty(childrenTasks)) { activityNode.getTasks().addAll(convertList(childrenTasks, BpmProcessInstanceConvert.INSTANCE::buildApprovalTaskInfo)); } } // 处理每个任务的 candidateUsers 属性:如果是依次审批,需要预测它的后续审批人。因为 Task 是审批完一个,创建一个新的 Task if (BpmnModelUtils.isSequentialUserTask(flowNode)) { List candidateUserIds = getTaskCandidateUserList(bpmnModel, flowNode.getId(), startUserId, processDefinition.getId(), processVariables); // 截取当前审批人位置后面的候选人,不包含当前审批人 ActivityNodeTask approvalTaskInfo = CollUtil.getFirst(activityNode.getTasks()); Assert.notNull(approvalTaskInfo, "任务不能为空"); int index = CollUtil.indexOf(candidateUserIds, userId -> ObjectUtils.equalsAny(userId, approvalTaskInfo.getOwner(), approvalTaskInfo.getAssignee())); // 委派或者向前加签情况,需要先比较 owner activityNode.setCandidateUserIds(CollUtil.sub(candidateUserIds, index + 1, candidateUserIds.size())); } return activityNode; }); } /** * 获得【预测(未来)】的活动节点们 */ private List getSimulateApproveNodeList(Long startUserId, BpmnModel bpmnModel, BpmProcessDefinitionInfoDO processDefinitionInfo, Map processVariables, List activities) { // TODO @芋艿:【可优化】在驳回场景下,未来的预测准确性不高。原因是,驳回后,HistoricActivityInstance 包括了历史的操作,不是只有 startEvent 到当前节点的记录 Set runActivityIds = convertSet(activities, HistoricActivityInstance::getActivityId); // 情况一:BPMN 设计器 if (Objects.equals(BpmModelTypeEnum.BPMN.getType(), processDefinitionInfo.getModelType())) { List flowElements = BpmnModelUtils.simulateProcess(bpmnModel, processVariables); return convertList(flowElements, flowElement -> buildNotRunApproveNodeForBpmn(startUserId, bpmnModel, processDefinitionInfo, processVariables, flowElement, runActivityIds)); } // 情况二:SIMPLE 设计器 if (Objects.equals(BpmModelTypeEnum.SIMPLE.getType(), processDefinitionInfo.getModelType())) { BpmSimpleModelNodeVO simpleModel = JsonUtils.parseObject(processDefinitionInfo.getSimpleModel(), BpmSimpleModelNodeVO.class); List simpleNodes = SimpleModelUtils.simulateProcess(simpleModel, processVariables); return convertList(simpleNodes, simpleNode -> buildNotRunApproveNodeForSimple(startUserId, bpmnModel, processDefinitionInfo, processVariables, simpleNode, runActivityIds)); } throw new IllegalArgumentException("未知设计器类型:" + processDefinitionInfo.getModelType()); } private ActivityNode buildNotRunApproveNodeForSimple(Long startUserId, BpmnModel bpmnModel, BpmProcessDefinitionInfoDO processDefinitionInfo, Map processVariables, BpmSimpleModelNodeVO node, Set runActivityIds) { // TODO @芋艿:【可优化】在驳回场景下,未来的预测准确性不高。原因是,驳回后,HistoricActivityInstance 包括了历史的操作,不是只有 startEvent 到当前节点的记录 if (runActivityIds.contains(node.getId())) { return null; } ActivityNode activityNode = new ActivityNode().setId(node.getId()).setName(node.getName()) .setNodeType(node.getType()).setCandidateStrategy(node.getCandidateStrategy()) .setStatus(BpmTaskStatusEnum.NOT_START.getStatus()); // 1. 开始节点/审批节点 if (ObjectUtils.equalsAny(node.getType(), BpmSimpleModelNodeType.START_USER_NODE.getType(), BpmSimpleModelNodeType.APPROVE_NODE.getType())) { List candidateUserIds = getTaskCandidateUserList(bpmnModel, node.getId(), startUserId, processDefinitionInfo.getProcessDefinitionId(), processVariables); activityNode.setCandidateUserIds(candidateUserIds); return activityNode; } // 2. 结束节点 if (BpmSimpleModelNodeType.END_NODE.getType().equals(node.getType())) { return activityNode; } // 3. 抄送节点 if (CollUtil.isEmpty(runActivityIds) && // 流程发起时:需要展示抄送节点,用于选择抄送人 BpmSimpleModelNodeType.COPY_NODE.getType().equals(node.getType())) { return activityNode; } return null; } private ActivityNode buildNotRunApproveNodeForBpmn(Long startUserId, BpmnModel bpmnModel, BpmProcessDefinitionInfoDO processDefinitionInfo, Map processVariables, FlowElement node, Set runActivityIds) { if (runActivityIds.contains(node.getId())) { return null; } ActivityNode activityNode = new ActivityNode().setId(node.getId()).setStatus(BpmTaskStatusEnum.NOT_START.getStatus()); // 1. 开始节点 if (node instanceof StartEvent) { return activityNode.setName(BpmSimpleModelNodeType.START_USER_NODE.getName()) .setNodeType(BpmSimpleModelNodeType.START_USER_NODE.getType()); } // 2. 审批节点 if (node instanceof UserTask) { List candidateUserIds = getTaskCandidateUserList(bpmnModel, node.getId(), startUserId, processDefinitionInfo.getProcessDefinitionId(), processVariables); return activityNode.setName(node.getName()).setNodeType(BpmSimpleModelNodeType.APPROVE_NODE.getType()) .setCandidateStrategy(BpmnModelUtils.parseCandidateStrategy(node)) .setCandidateUserIds(candidateUserIds); } // 3. 结束节点 if (node instanceof EndEvent) { return activityNode.setName(BpmSimpleModelNodeType.END_NODE.getName()) .setNodeType(BpmSimpleModelNodeType.END_NODE.getType()); } return null; } private List getTaskCandidateUserList(BpmnModel bpmnModel, String activityId, Long startUserId, String processDefinitionId, Map processVariables) { Set userIds = taskCandidateInvoker.calculateUsersByActivity(bpmnModel, activityId, startUserId, processDefinitionId, processVariables); return new ArrayList<>(userIds); } @Override public BpmProcessInstanceBpmnModelViewRespVO getProcessInstanceBpmnModelView(String id) { // 1.1 获得流程实例 HistoricProcessInstance processInstance = getHistoricProcessInstance(id); if (processInstance == null) { return null; } // 1.2 获得流程定义 BpmnModel bpmnModel = processDefinitionService.getProcessDefinitionBpmnModel(processInstance.getProcessDefinitionId()); if (bpmnModel == null) { return null; } BpmSimpleModelNodeVO simpleModel = null; BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionService.getProcessDefinitionInfo( processInstance.getProcessDefinitionId()); if (processDefinitionInfo != null && BpmModelTypeEnum.SIMPLE.getType().equals(processDefinitionInfo.getModelType())) { simpleModel = JsonUtils.parseObject(processDefinitionInfo.getSimpleModel(), BpmSimpleModelNodeVO.class); } // 1.3 获得流程实例对应的活动实例列表 + 任务列表 List activities = taskService.getActivityListByProcessInstanceId(id); List tasks = taskService.getTaskListByProcessInstanceId(id, true); // 2.1 拼接进度信息 Set unfinishedTaskActivityIds = convertSet(activities, HistoricActivityInstance::getActivityId, activityInstance -> activityInstance.getEndTime() == null); Set finishedTaskActivityIds = convertSet(activities, HistoricActivityInstance::getActivityId, activityInstance -> activityInstance.getEndTime() != null && ObjectUtil.notEqual(activityInstance.getActivityType(), BpmnXMLConstants.ELEMENT_SEQUENCE_FLOW)); Set finishedSequenceFlowActivityIds = convertSet(activities, HistoricActivityInstance::getActivityId, activityInstance -> activityInstance.getEndTime() != null && ObjectUtil.equals(activityInstance.getActivityType(), BpmnXMLConstants.ELEMENT_SEQUENCE_FLOW)); // 特殊:会签情况下,会有部分已完成(审批)、部分未完成(待审批),此时需要 finishedTaskActivityIds 移除掉 unfinishedTaskActivityIds.removeAll(finishedTaskActivityIds); // 特殊:如果流程实例被拒绝,则需要计算是哪个活动节点。 // 注意,只取最后一个。因为会存在多次拒绝的情况,拒绝驳回到指定节点 Set rejectTaskActivityIds = CollUtil.newHashSet(); if (BpmProcessInstanceStatusEnum.isRejectStatus(FlowableUtils.getProcessInstanceStatus(processInstance))) { tasks.stream() .filter(task -> BpmTaskStatusEnum.isRejectStatus(FlowableUtils.getTaskStatus(task))) .max(Comparator.comparing(HistoricTaskInstance::getEndTime)) .ifPresent(reject -> rejectTaskActivityIds.add(reject.getTaskDefinitionKey())); finishedTaskActivityIds.removeAll(rejectTaskActivityIds); } // 2.2 拼接基础信息 Set userIds = BpmProcessInstanceConvert.INSTANCE.parseUserIds02(processInstance, tasks); Map userMap = adminUserApi.getUserMap(userIds); Map deptMap = deptApi.getDeptMap(convertSet(userMap.values(), AdminUserRespDTO::getDeptId)); return BpmProcessInstanceConvert.INSTANCE.buildProcessInstanceBpmnModelView(processInstance, tasks, bpmnModel, simpleModel, unfinishedTaskActivityIds, finishedTaskActivityIds, finishedSequenceFlowActivityIds, rejectTaskActivityIds, userMap, deptMap); } // ========== Update 写入相关方法 ========== @Override @Transactional(rollbackFor = Exception.class) public String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqVO createReqVO) { // 获得流程定义 ProcessDefinition definition = processDefinitionService.getProcessDefinition(createReqVO.getProcessDefinitionId()); // 发起流程 return createProcessInstance0(userId, definition, createReqVO.getVariables(), null, createReqVO.getStartUserSelectAssignees()); } @Override public String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqDTO createReqDTO) { // 获得流程定义 ProcessDefinition definition = processDefinitionService.getActiveProcessDefinition(createReqDTO.getProcessDefinitionKey()); // 发起流程 return createProcessInstance0(userId, definition, createReqDTO.getVariables(), createReqDTO.getBusinessKey(), createReqDTO.getStartUserSelectAssignees()); } private String createProcessInstance0(Long userId, ProcessDefinition definition, Map variables, String businessKey, Map> startUserSelectAssignees) { // 1.1 校验流程定义 if (definition == null) { throw exception(PROCESS_DEFINITION_NOT_EXISTS); } if (definition.isSuspended()) { throw exception(PROCESS_DEFINITION_IS_SUSPENDED); } BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionService.getProcessDefinitionInfo(definition.getId()); if (processDefinitionInfo == null) { throw exception(PROCESS_DEFINITION_NOT_EXISTS); } // 1.2 校验是否能够发起 if (!processDefinitionService.canUserStartProcessDefinition(processDefinitionInfo, userId)) { throw exception(PROCESS_INSTANCE_START_USER_CAN_START); } // 1.3 校验发起人自选审批人 validateStartUserSelectAssignees(definition, startUserSelectAssignees); // 2. 创建流程实例 if (variables == null) { variables = new HashMap<>(); } FlowableUtils.filterProcessInstanceFormVariable(variables); // 过滤一下,避免 ProcessInstance 系统级的变量被占用 variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_ID, userId); // 设置流程变量,发起人 ID variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS, // 流程实例状态:审批中 BpmProcessInstanceStatusEnum.RUNNING.getStatus()); if (CollUtil.isNotEmpty(startUserSelectAssignees)) { variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES, startUserSelectAssignees); } ProcessInstance instance = runtimeService.createProcessInstanceBuilder() .processDefinitionId(definition.getId()) .businessKey(businessKey) .name(definition.getName().trim()) .variables(variables) .start(); return instance.getId(); } private void validateStartUserSelectAssignees(ProcessDefinition definition, Map> startUserSelectAssignees) { // 1. 获得发起人自选审批人的 UserTask/ServiceTask 列表 BpmnModel bpmnModel = processDefinitionService.getProcessDefinitionBpmnModel(definition.getId()); List tasks = BpmTaskCandidateStartUserSelectStrategy.getStartUserSelectTaskList(bpmnModel); if (CollUtil.isEmpty(tasks)) { return; } // 2. 校验发起人自选审批人的审批人和抄送人是否都配置了 tasks.forEach(task -> { List assignees = startUserSelectAssignees != null ? startUserSelectAssignees.get(task.getId()) : null; if (CollUtil.isEmpty(assignees)) { throw exception(PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_CONFIG, task.getName()); } Map userMap = adminUserApi.getUserMap(assignees); assignees.forEach(assignee -> { if (userMap.get(assignee) == null) { throw exception(PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_EXISTS, task.getName(), assignee); } }); }); } @Override public void cancelProcessInstanceByStartUser(Long userId, @Valid BpmProcessInstanceCancelReqVO cancelReqVO) { // 1.1 校验流程实例存在 ProcessInstance instance = getProcessInstance(cancelReqVO.getId()); if (instance == null) { throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_EXISTS); } // 1.2 只能取消自己的 if (!Objects.equals(instance.getStartUserId(), String.valueOf(userId))) { throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_SELF); } // 2. 取消流程 updateProcessInstanceCancel(cancelReqVO.getId(), BpmReasonEnum.CANCEL_PROCESS_INSTANCE_BY_START_USER.format(cancelReqVO.getReason())); } @Override public void cancelProcessInstanceByAdmin(Long userId, BpmProcessInstanceCancelReqVO cancelReqVO) { // 1.1 校验流程实例存在 ProcessInstance instance = getProcessInstance(cancelReqVO.getId()); if (instance == null) { throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_EXISTS); } // 2. 取消流程 AdminUserRespDTO user = adminUserApi.getUser(userId).getCheckedData(); updateProcessInstanceCancel(cancelReqVO.getId(), BpmReasonEnum.CANCEL_PROCESS_INSTANCE_BY_ADMIN.format(user.getNickname(), cancelReqVO.getReason())); } private void updateProcessInstanceCancel(String id, String reason) { // 1. 更新流程实例 status runtimeService.setVariable(id, BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS, BpmProcessInstanceStatusEnum.CANCEL.getStatus()); runtimeService.setVariable(id, BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_REASON, reason); // 2. 结束流程 taskService.moveTaskToEnd(id); } @Override public void updateProcessInstanceReject(ProcessInstance processInstance, String reason) { runtimeService.setVariable(processInstance.getProcessInstanceId(), BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS, BpmProcessInstanceStatusEnum.REJECT.getStatus()); runtimeService.setVariable(processInstance.getProcessInstanceId(), BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_REASON, BpmReasonEnum.REJECT_TASK.format(reason)); } // ========== Event 事件相关方法 ========== @Override public void processProcessInstanceCompleted(ProcessInstance instance) { // 注意:需要基于 instance 设置租户编号,避免 Flowable 内部异步时,丢失租户编号 FlowableUtils.execute(instance.getTenantId(), () -> { // 1.1 获取当前状态 Integer status = (Integer) instance.getProcessVariables().get(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS); String reason = (String) instance.getProcessVariables().get(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_REASON); // 1.2 当流程状态还是审批状态中,说明审批通过了,则变更下它的状态 // 为什么这么处理?因为流程完成,并且完成了,说明审批通过了 if (Objects.equals(status, BpmProcessInstanceStatusEnum.RUNNING.getStatus())) { status = BpmProcessInstanceStatusEnum.APPROVE.getStatus(); runtimeService.setVariable(instance.getId(), BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS, status); } // 2. 发送对应的消息通知 if (Objects.equals(status, BpmProcessInstanceStatusEnum.APPROVE.getStatus())) { messageService.sendMessageWhenProcessInstanceApprove(BpmProcessInstanceConvert.INSTANCE.buildProcessInstanceApproveMessage(instance)); } else if (Objects.equals(status, BpmProcessInstanceStatusEnum.REJECT.getStatus())) { messageService.sendMessageWhenProcessInstanceReject( BpmProcessInstanceConvert.INSTANCE.buildProcessInstanceRejectMessage(instance, reason)); } // 3. 发送流程实例的状态事件 processInstanceEventPublisher.sendProcessInstanceResultEvent( BpmProcessInstanceConvert.INSTANCE.buildProcessInstanceStatusEvent(this, instance, status)); }); } } \ No newline at end of file diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskService.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskService.java index 4a71b63fa..06b081953 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskService.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskService.java @@ -6,7 +6,9 @@ import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.*; import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskTimeoutHandlerTypeEnum; import jakarta.validation.Valid; import org.flowable.bpmn.model.UserTask; +import org.flowable.engine.history.HistoricActivityInstance; import org.flowable.task.api.Task; +import org.flowable.task.api.TaskInfo; import org.flowable.task.api.history.HistoricTaskInstance; import java.util.Collection; @@ -32,6 +34,15 @@ public interface BpmTaskService { */ PageResult getTaskTodoPage(Long userId, BpmTaskPageReqVO pageReqVO); + /** + * 获得用户在指定流程下,首个需要处理(待办)的任务 + * + * @param userId 用户编号 + * @param processInstanceId 流程实例编号 + * @return 待办任务 + */ + BpmTaskRespVO getFirstTodoTask(Long userId, String processInstanceId); + /** * 获得已办的流程任务分页 * @@ -73,9 +84,10 @@ public interface BpmTaskService { * 获得指定流程实例的流程任务列表,包括所有状态的 * * @param processInstanceId 流程实例的编号 + * @param asc 是否升序 * @return 流程任务列表 */ - List getTaskListByProcessInstanceId(String processInstanceId); + List getTaskListByProcessInstanceId(String processInstanceId, Boolean asc); /** * 获取任务 @@ -105,21 +117,30 @@ public interface BpmTaskService { * 根据条件查询正在进行中的任务 * * @param processInstanceId 流程实例编号,不允许为空 - * @param assigned 是否分配了审批人,允许空 - * @param taskDefineKey 任务定义 Key,允许空 + * @param assigned 是否分配了审批人,允许空 + * @param taskDefineKey 任务定义 Key,允许空 */ List getRunningTaskListByProcessInstanceId(String processInstanceId, Boolean assigned, String taskDefineKey); /** - * 获取当前任务的可回退的 UserTask 集合 + * 获取当前任务的可退回的 UserTask 集合 * * @param id 当前的任务 ID - * @return 可以回退的节点列表 + * @return 可以退回的节点列表 */ List getUserTaskListByReturn(String id); + /** + * 获取指定任务的子任务列表(多层) + * + * @param parentTaskId 父任务 ID + * @param tasks 任务列表 + * @return 子任务列表 + */ + List getAllChildrenTaskListByParentTaskId(String parentTaskId, List tasks); + /** * 获取指定任务的子任务列表 * @@ -129,12 +150,20 @@ public interface BpmTaskService { List getTaskListByParentTaskId(String parentTaskId); /** - * 通过任务 ID,查询任务名 Map + * 获得指定流程实例的活动实例列表 * - * @param taskIds 任务 ID - * @return 任务 ID 与名字的 Map + * @param processInstanceId 流程实例的编号 + * @return 活动实例列表 */ - Map getTaskNameByTaskIds(Collection taskIds); + List getActivityListByProcessInstanceId(String processInstanceId); + + /** + * 获得执行编号对应的活动实例 + * + * @param executionId 执行编号 + * @return 活动实例 + */ + List getHistoricActivityListByExecutionId(String executionId); // ========== Update 写入相关方法 ========== @@ -170,10 +199,10 @@ public interface BpmTaskService { void moveTaskToEnd(String processInstanceId); /** - * 将任务回退到指定的 targetDefinitionKey 位置 + * 将任务退回到指定的 targetDefinitionKey 位置 * * @param userId 用户编号 - * @param reqVO 回退的任务key和当前所在的任务ID + * @param reqVO 退回的任务key和当前所在的任务ID */ void returnTask(Long userId, BpmTaskReturnReqVO reqVO); @@ -201,14 +230,22 @@ public interface BpmTaskService { */ void deleteSignTask(Long userId, BpmTaskSignDeleteReqVO reqVO); + /** + * 抄送任务 + * + * @param userId 用户编号 + * @param reqVO 通过请求 + */ + void copyTask(Long userId, @Valid BpmTaskCopyReqVO reqVO); + // ========== Event 事件相关方法 ========== /** * 处理 Task 创建事件,目前是 - * + *

* 1. 更新它的状态为审批中 * 2. 处理自动通过的情况,例如说:1)无审批人时,是否自动通过、不通过;2)非【人工审核】时,是否自动通过、不通过 - * + *

* 注意:它的触发时机,晚于 {@link #processTaskAssigned(Task)} 之后 * * @param task 任务实体 @@ -233,8 +270,8 @@ public interface BpmTaskService { * 处理 Task 审批超时事件,可能会处理多个当前审批中的任务 * * @param processInstanceId 流程示例编号 - * @param taskDefineKey 任务 Key - * @param handlerType 处理类型,参见 {@link BpmUserTaskTimeoutHandlerTypeEnum} + * @param taskDefineKey 任务 Key + * @param handlerType 处理类型,参见 {@link BpmUserTaskTimeoutHandlerTypeEnum} */ void processTaskTimeout(String processInstanceId, String taskDefineKey, Integer handlerType); diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java index 0d13b71d7..a933e55ce 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java @@ -21,6 +21,7 @@ import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableCon import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils; import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils; import cn.iocoder.yudao.module.bpm.service.definition.BpmModelService; +import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService; import cn.iocoder.yudao.module.bpm.service.message.BpmMessageService; import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenTaskTimeoutReqDTO; import cn.iocoder.yudao.module.system.api.dept.DeptApi; @@ -38,9 +39,11 @@ import org.flowable.engine.HistoryService; import org.flowable.engine.ManagementService; import org.flowable.engine.RuntimeService; import org.flowable.engine.TaskService; +import org.flowable.engine.history.HistoricActivityInstance; import org.flowable.engine.runtime.ProcessInstance; import org.flowable.task.api.DelegationState; import org.flowable.task.api.Task; +import org.flowable.task.api.TaskInfo; import org.flowable.task.api.TaskQuery; import org.flowable.task.api.history.HistoricTaskInstance; import org.flowable.task.api.history.HistoricTaskInstanceQuery; @@ -81,6 +84,8 @@ public class BpmTaskServiceImpl implements BpmTaskService { @Resource private BpmProcessInstanceService processInstanceService; @Resource + private BpmProcessDefinitionService bpmProcessDefinitionService; + @Resource private BpmProcessInstanceCopyService processInstanceCopyService; @Resource private BpmModelService modelService; @@ -116,6 +121,41 @@ public class BpmTaskServiceImpl implements BpmTaskService { return new PageResult<>(tasks, count); } + @Override + public BpmTaskRespVO getFirstTodoTask(Long userId, String processInstanceId) { + if (processInstanceId == null) { + return null; + } + // 1. 查询所有任务 + List tasks = taskService.createTaskQuery() + .active() + .processInstanceId(processInstanceId) + .includeTaskLocalVariables() + .includeProcessVariables() + .orderByTaskCreateTime().asc() // 按创建时间升序 + .list(); + if (CollUtil.isEmpty(tasks)) { + return null; + } + + // 2.1 查询我的首个任务 + Task todoTask = CollUtil.findOne(tasks, task -> { + return isAssignUserTask(userId, task) // 当前用户为审批人 + || isAddSignUserTask(userId, task); // 当前用户为加签人(为了减签) + }); + if (todoTask == null) { + return null; + } + // 2.2 查询该任务的子任务 + List childrenTasks = getAllChildrenTaskListByParentTaskId(todoTask.getId(), tasks); + + // 3. 转换返回 + BpmnModel bpmnModel = bpmProcessDefinitionService.getProcessDefinitionBpmnModel(todoTask.getProcessDefinitionId()); + Map buttonsSetting = BpmnModelUtils.parseButtonsSetting( + bpmnModel, todoTask.getTaskDefinitionKey()); + return BpmTaskConvert.INSTANCE.buildTodoTask(todoTask, childrenTasks, buttonsSetting); + } + @Override public PageResult getTaskDonePage(Long userId, BpmTaskPageReqVO pageVO) { HistoricTaskInstanceQuery taskQuery = historyService.createHistoricTaskInstanceQuery() @@ -170,16 +210,16 @@ public class BpmTaskServiceImpl implements BpmTaskService { } @Override - public List getTaskListByProcessInstanceId(String processInstanceId) { - List tasks = historyService.createHistoricTaskInstanceQuery() + public List getTaskListByProcessInstanceId(String processInstanceId, Boolean asc) { + HistoricTaskInstanceQuery query = historyService.createHistoricTaskInstanceQuery() .includeTaskLocalVariables() - .processInstanceId(processInstanceId) - .orderByHistoricTaskInstanceStartTime().desc() // 创建时间倒序 - .list(); - if (CollUtil.isEmpty(tasks)) { - return Collections.emptyList(); + .processInstanceId(processInstanceId); + if (Boolean.TRUE.equals(asc)) { + query.orderByHistoricTaskInstanceStartTime().asc(); + } else { + query.orderByHistoricTaskInstanceStartTime().desc(); } - return tasks; + return query.list(); } /** @@ -254,11 +294,43 @@ public class BpmTaskServiceImpl implements BpmTaskService { if (CollUtil.isEmpty(previousUserList)) { return Collections.emptyList(); } - // 2.2 过滤:只有串行可到达的节点,才可以回退。类似非串行、子流程无法退回 + // 2.2 过滤:只有串行可到达的节点,才可以退回。类似非串行、子流程无法退回 previousUserList.removeIf(userTask -> !BpmnModelUtils.isSequentialReachable(source, userTask, null)); return previousUserList; } + @Override + public List getAllChildrenTaskListByParentTaskId(String parentTaskId, List tasks) { + if (CollUtil.isEmpty(tasks)) { + return Collections.emptyList(); + } + Map> parentTaskMap = convertMultiMap( + filterList(tasks, task -> StrUtil.isNotEmpty(task.getParentTaskId())), TaskInfo::getParentTaskId); + if (CollUtil.isEmpty(parentTaskMap)) { + return Collections.emptyList(); + } + + List result = new ArrayList<>(); + // 1. 递归获取子级 + Stack stack = new Stack<>(); + stack.push(parentTaskId); + // 2. 递归遍历 + for (int i = 0; i < Short.MAX_VALUE; i++) { + if (stack.isEmpty()) { + break; + } + // 2.1 获取子任务们 + String taskId = stack.pop(); + List childTaskList = filterList(tasks, task -> StrUtil.equals(task.getParentTaskId(), taskId)); + // 2.2 如果非空,则添加到 stack 进一步递归 + if (CollUtil.isNotEmpty(childTaskList)) { + stack.addAll(convertList(childTaskList, TaskInfo::getId)); + result.addAll(childTaskList); + } + } + return result; + } + /** * 获得所有子任务列表 * @@ -331,12 +403,50 @@ public class BpmTaskServiceImpl implements BpmTaskService { } @Override - public Map getTaskNameByTaskIds(Collection taskIds) { - if (CollUtil.isEmpty(taskIds)) { - return Collections.emptyMap(); - } - List tasks = taskService.createTaskQuery().taskIds(taskIds).list(); - return convertMap(tasks, Task::getId, Task::getName); + public List getActivityListByProcessInstanceId(String processInstanceId) { + return historyService.createHistoricActivityInstanceQuery().processInstanceId(processInstanceId) + .orderByHistoricActivityInstanceStartTime().asc().list(); + } + + @Override + public List getHistoricActivityListByExecutionId(String executionId) { + return historyService.createHistoricActivityInstanceQuery().executionId(executionId).list(); + } + + /** + * 判断指定用户,是否是当前任务的审批人 + * + * @param userId 用户编号 + * @param task 任务 + * @return 是否 + */ + private boolean isAssignUserTask(Long userId, Task task) { + Long assignee = NumberUtil.parseLong(task.getAssignee(), null); + return ObjectUtil.equals(userId, assignee); + } + + /** + * 判断指定用户,是否是当前任务的拥有人 + * + * @param userId 用户编号 + * @param task 任务 + * @return 是否 + */ + private boolean isOwnerUserTask(Long userId, Task task) { + Long assignee = NumberUtil.parseLong(task.getOwner(), null); + return ObjectUtil.equal(userId, assignee); + } + + /** + * 判断指定用户,是否是当前任务的加签人 + * + * @param userId 用户 Id + * @param task 任务 + * @return 是否 + */ + private boolean isAddSignUserTask(Long userId, Task task) { + return (isAssignUserTask(userId, task) || isOwnerUserTask(userId, task)) + && BpmTaskSignTypeEnum.of(task.getScopeType()) != null; } // ========== Update 写入相关方法 ========== @@ -352,11 +462,6 @@ public class BpmTaskServiceImpl implements BpmTaskService { throw exception(PROCESS_INSTANCE_NOT_EXISTS); } - // 2. 抄送用户 - if (CollUtil.isNotEmpty(reqVO.getCopyUserIds())) { - processInstanceCopyService.createProcessInstanceCopy(reqVO.getCopyUserIds(), reqVO.getId()); - } - // 情况一:被委派的任务,不调用 complete 去完成任务 if (DelegationState.PENDING.equals(task.getDelegationState())) { approveDelegateTask(reqVO, task); @@ -370,12 +475,12 @@ public class BpmTaskServiceImpl implements BpmTaskService { } // 情况三:审批普通的任务。大多数情况下,都是这样 - // 3.1 更新 task 状态、原因 + // 2.1 更新 task 状态、原因 updateTaskStatusAndReason(task.getId(), BpmTaskStatusEnum.APPROVE.getStatus(), reqVO.getReason()); - // 3.2 添加评论 + // 2.2 添加评论 taskService.addComment(task.getId(), task.getProcessInstanceId(), BpmCommentTypeEnum.APPROVE.getType(), BpmCommentTypeEnum.APPROVE.formatComment(reqVO.getReason())); - // 3.3 调用 BPM complete 去完成任务 + // 2.3 调用 BPM complete 去完成任务 // 其中,variables 是存储动态表单到 local 任务级别。过滤一下,避免 ProcessInstance 系统级的变量被占用 if (CollUtil.isNotEmpty(reqVO.getVariables())) { Map variables = FlowableUtils.filterTaskFormVariable(reqVO.getVariables()); @@ -519,7 +624,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { BpmUserTaskRejectHandlerType userTaskRejectHandlerType = BpmnModelUtils.parseRejectHandlerType(userTaskElement); if (userTaskRejectHandlerType == BpmUserTaskRejectHandlerType.RETURN_USER_TASK) { String returnTaskId = BpmnModelUtils.parseReturnTaskId(userTaskElement); - Assert.notNull(returnTaskId, "回退的节点不能为空"); + Assert.notNull(returnTaskId, "退回的节点不能为空"); returnTask(userId, new BpmTaskReturnReqVO().setId(task.getId()) .setTargetTaskDefinitionKey(returnTaskId).setReason(reqVO.getReason())); return; @@ -563,12 +668,12 @@ public class BpmTaskServiceImpl implements BpmTaskService { FlowElement targetElement = validateTargetTaskCanReturn(task.getTaskDefinitionKey(), reqVO.getTargetTaskDefinitionKey(), task.getProcessDefinitionId()); - // 2. 调用 Flowable 框架的回退逻辑 + // 2. 调用 Flowable 框架的退回逻辑 returnTask(task, targetElement, reqVO); } /** - * 回退流程节点时,校验目标任务节点是否可回退 + * 退回流程节点时,校验目标任务节点是否可退回 * * @param sourceKey 当前任务节点 Key * @param targetKey 目标任务节点 key @@ -586,7 +691,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { throw exception(TASK_TARGET_NODE_NOT_EXISTS); } - // 2.2 只有串行可到达的节点,才可以回退。类似非串行、子流程无法退回 + // 2.2 只有串行可到达的节点,才可以退回。类似非串行、子流程无法退回 if (!BpmnModelUtils.isSequentialReachable(source, target, null)) { throw exception(TASK_RETURN_FAIL_SOURCE_TARGET_ERROR); } @@ -594,10 +699,10 @@ public class BpmTaskServiceImpl implements BpmTaskService { } /** - * 执行回退逻辑 + * 执行退回逻辑 * - * @param currentTask 当前回退的任务 - * @param targetElement 需要回退到的目标任务 + * @param currentTask 当前退回的任务 + * @param targetElement 需要退回到的目标任务 * @param reqVO 前端参数封装 */ public void returnTask(Task currentTask, FlowElement targetElement, BpmTaskReturnReqVO reqVO) { @@ -610,9 +715,9 @@ public class BpmTaskServiceImpl implements BpmTaskService { List returnUserTaskList = BpmnModelUtils.iteratorFindChildUserTasks(targetElement, runTaskKeyList, null, null); List returnTaskKeyList = convertList(returnUserTaskList, UserTask::getId); - // 2. 给当前要被回退的 task 数组,设置回退意见 + // 2. 给当前要被退回的 task 数组,设置退回意见 taskList.forEach(task -> { - // 需要排除掉,不需要设置回退意见的任务 + // 需要排除掉,不需要设置退回意见的任务 if (!returnTaskKeyList.contains(task.getTaskDefinitionKey())) { return; } @@ -659,9 +764,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { taskService.setOwner(taskId, task.getAssignee()); // 3.2 执行委派,将任务委派给 delegateUser taskService.delegateTask(taskId, reqVO.getDelegateUserId().toString()); - // 3.3 更新 task 状态。 - // 为什么不更新原因?因为原因目前主要给审批通过、不通过时使用 - updateTaskStatus(taskId, BpmTaskStatusEnum.DELEGATE.getStatus()); + // 补充说明:委托不单独设置状态。如果需要,可通过 Task 的 DelegationState 字段,判断是否为 DelegationState.PENDING 委托中 } @Override @@ -868,6 +971,11 @@ public class BpmTaskServiceImpl implements BpmTaskService { handleParentTaskIfSign(task.getParentTaskId()); } + @Override + public void copyTask(Long userId, BpmTaskCopyReqVO reqVO) { + processInstanceCopyService.createProcessInstanceCopy(reqVO.getCopyUserIds(), reqVO.getReason(), reqVO.getId()); + } + /** * 校验任务是否能被减签 * @@ -920,8 +1028,13 @@ public class BpmTaskServiceImpl implements BpmTaskService { if (ObjectUtil.notEqual(transactionStatus, TransactionSynchronization.STATUS_COMMITTED)) { return; } + // TODO 芋艿:可以后续优化成 getSelf(); // 特殊情况一:【人工审核】审批人为空,根据配置是否要自动通过、自动拒绝 if (ObjectUtil.equal(approveType, BpmUserTaskApproveTypeEnum.USER.getType())) { + // 如果有审批人、或者拥有人,则说明不满足情况一,不自动通过、不自动拒绝 + if (!ObjectUtil.isAllEmpty(task.getAssignee(), task.getOwner())) { + return; + } if (ObjectUtil.equal(assignEmptyHandlerType, BpmUserTaskAssignEmptyHandlerTypeEnum.APPROVE.getType())) { SpringUtil.getBean(BpmTaskService.class).approveTask(null, new BpmTaskApproveReqVO() .setId(task.getId()).setReason(BpmReasonEnum.ASSIGN_EMPTY_APPROVE.getReason())); @@ -987,7 +1100,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { } // 审批人与提交人为同一人时,根据 BpmUserTaskAssignStartUserHandlerTypeEnum 策略进行处理 if (StrUtil.equals(task.getAssignee(), processInstance.getStartUserId())) { - // 判断是否为回退或者驳回:如果是回退或者驳回不走这个策略 + // 判断是否为退回或者驳回:如果是退回或者驳回不走这个策略 // TODO 芋艿:【优化】未来有没更好的判断方式?!另外,还要考虑清理机制。就是说,下次处理了之后,就移除这个标识 Boolean returnTaskFlag = runtimeService.getVariable(processInstance.getProcessInstanceId(), String.format(PROCESS_INSTANCE_VARIABLE_RETURN_FLAG, task.getTaskDefinitionKey()), Boolean.class); diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/bo/AlreadyRunApproveNodeRespBO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/bo/AlreadyRunApproveNodeRespBO.java deleted file mode 100644 index 4d92d2e77..000000000 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/bo/AlreadyRunApproveNodeRespBO.java +++ /dev/null @@ -1,36 +0,0 @@ -package cn.iocoder.yudao.module.bpm.service.task.bo; - -import lombok.Data; - -import java.util.List; -import java.util.Map; -import java.util.Set; - -import static cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmApprovalDetailRespVO.ApprovalNodeInfo; - -/** - * 已经进行中的审批节点 Response BO - * - * @author jason - */ -@Data -public class AlreadyRunApproveNodeRespBO { - - /** - * 审批节点信息数组 - */ - private List approveNodes; - - /** - * 已运行的节点 ID 数组 (对应 Bpmn XML 节点 id) - */ - private Set runNodeIds; - - /** - * 正在运行的节点的审批信息(key: activityId, value: 审批信息) - *

- * 用于依次审批,需要加上候选人信息 - */ - private Map runningApprovalNodes; - -} diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateInvokerTest.java b/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateInvokerTest.java index cf08bb11b..d1db10d6f 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateInvokerTest.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateInvokerTest.java @@ -1,31 +1,41 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate; import cn.hutool.core.map.MapUtil; +import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; -import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.BpmTaskCandidateUserStrategy; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskAssignStartUserHandlerTypeEnum; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.other.BpmTaskCandidateAssignEmptyStrategy; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.user.BpmTaskCandidateUserStrategy; import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils; +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; import cn.iocoder.yudao.module.system.api.user.AdminUserApi; import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import org.flowable.bpmn.model.BpmnModel; +import org.flowable.bpmn.model.ExtensionElement; +import org.flowable.bpmn.model.FlowElement; import org.flowable.bpmn.model.UserTask; import org.flowable.engine.delegate.DelegateExecution; +import org.flowable.engine.runtime.ProcessInstance; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock; +import org.mockito.MockedStatic; import org.mockito.Spy; +import org.mockito.internal.util.collections.Sets; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet; import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; +import static org.flowable.bpmn.constants.BpmnXMLConstants.FLOWABLE_EXTENSIONS_NAMESPACE; +import static org.flowable.bpmn.constants.BpmnXMLConstants.FLOWABLE_EXTENSIONS_PREFIX; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; /** * {@link BpmTaskCandidateInvoker} 的单元测试 @@ -39,44 +49,207 @@ public class BpmTaskCandidateInvokerTest extends BaseMockitoUnitTest { @Mock private AdminUserApi adminUserApi; - @Spy - private BpmTaskCandidateStrategy strategy ; + @Mock + private BpmProcessInstanceService processInstanceService; @Spy - private List strategyList ; + private BpmTaskCandidateStrategy userStrategy; + @Mock + private BpmTaskCandidateAssignEmptyStrategy emptyStrategy; + + @Spy + private List strategyList; @BeforeEach public void setUp() { - strategy = new BpmTaskCandidateUserStrategy(adminUserApi); // 创建strategy实例 - strategyList = Collections.singletonList(strategy); // 创建strategyList + userStrategy = new BpmTaskCandidateUserStrategy(); // 创建 strategy 实例 + when(emptyStrategy.getStrategy()).thenReturn(BpmTaskCandidateStrategyEnum.ASSIGN_EMPTY); + strategyList = List.of(userStrategy, emptyStrategy); // 创建 strategyList taskCandidateInvoker = new BpmTaskCandidateInvoker(strategyList, adminUserApi); } + /** + * 场景:成功计算到候选人,但是移除了发起人的用户 + */ @Test - public void testCalculateUsers() { - // 准备参数 - String param = "1,2"; - DelegateExecution execution = mock(DelegateExecution.class); - // mock 方法(DelegateExecution) - UserTask userTask = mock(UserTask.class); - when(execution.getCurrentFlowElement()).thenReturn(userTask); - when(userTask.getAttributeValue(eq(BpmnModelConstants.NAMESPACE), eq(BpmnModelConstants.USER_TASK_CANDIDATE_STRATEGY))) - .thenReturn(BpmTaskCandidateStrategyEnum.USER.getStrategy().toString()); - when(userTask.getAttributeValue(eq(BpmnModelConstants.NAMESPACE), eq(BpmnModelConstants.USER_TASK_CANDIDATE_PARAM))) - .thenReturn(param); - // mock 方法(adminUserApi) - AdminUserRespDTO user1 = randomPojo(AdminUserRespDTO.class, o -> o.setId(1L) - .setStatus(CommonStatusEnum.ENABLE.getStatus())); - AdminUserRespDTO user2 = randomPojo(AdminUserRespDTO.class, o -> o.setId(2L) - .setStatus(CommonStatusEnum.ENABLE.getStatus())); - Map userMap = MapUtil.builder(user1.getId(), user1) - .put(user2.getId(), user2).build(); - when(adminUserApi.getUserMap(eq(asSet(1L, 2L)))).thenReturn(userMap); + public void testCalculateUsersByTask_some() { + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + // 准备参数 + String param = "1,2"; + DelegateExecution execution = mock(DelegateExecution.class); + // mock 方法(DelegateExecution) + UserTask userTask = mock(UserTask.class); + String processInstanceId = randomString(); + when(execution.getProcessInstanceId()).thenReturn(processInstanceId); + when(execution.getCurrentFlowElement()).thenReturn(userTask); + when(userTask.getAttributeValue(eq(BpmnModelConstants.NAMESPACE), eq(BpmnModelConstants.USER_TASK_CANDIDATE_STRATEGY))) + .thenReturn(BpmTaskCandidateStrategyEnum.USER.getStrategy().toString()); + when(userTask.getAttributeValue(eq(BpmnModelConstants.NAMESPACE), eq(BpmnModelConstants.USER_TASK_CANDIDATE_PARAM))) + .thenReturn(param); + // mock 方法(adminUserApi) + AdminUserRespDTO user1 = randomPojo(AdminUserRespDTO.class, o -> o.setId(1L) + .setStatus(CommonStatusEnum.ENABLE.getStatus())); + AdminUserRespDTO user2 = randomPojo(AdminUserRespDTO.class, o -> o.setId(2L) + .setStatus(CommonStatusEnum.ENABLE.getStatus())); + Map userMap = MapUtil.builder(user1.getId(), user1) + .put(user2.getId(), user2).build(); + when(adminUserApi.getUserMap(eq(asSet(1L, 2L)))).thenReturn(userMap); + // mock 移除发起人的用户 + springUtilMockedStatic.when(() -> SpringUtil.getBean(BpmProcessInstanceService.class)) + .thenReturn(processInstanceService); + ProcessInstance processInstance = mock(ProcessInstance.class); + when(processInstanceService.getProcessInstance(eq(processInstanceId))).thenReturn(processInstance); + when(processInstance.getStartUserId()).thenReturn("1"); + mockFlowElementExtensionElement(userTask, BpmnModelConstants.USER_TASK_ASSIGN_START_USER_HANDLER_TYPE, + String.valueOf(BpmUserTaskAssignStartUserHandlerTypeEnum.SKIP.getType())); - // 调用 - Set results = taskCandidateInvoker.calculateUsers(execution); - // 断言 - assertEquals(asSet(1L, 2L), results); + // 调用 + Set results = taskCandidateInvoker.calculateUsersByTask(execution); + // 断言 + assertEquals(asSet(2L), results); + } + } + + /** + * 场景:没有计算到候选人,但是被禁用移除,最终通过 empty 进行分配 + */ + @Test + public void testCalculateUsersByTask_none() { + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + // 准备参数 + String param = "1,2"; + DelegateExecution execution = mock(DelegateExecution.class); + // mock 方法(DelegateExecution) + UserTask userTask = mock(UserTask.class); + String processInstanceId = randomString(); + when(execution.getProcessInstanceId()).thenReturn(processInstanceId); + when(execution.getCurrentFlowElement()).thenReturn(userTask); + when(userTask.getAttributeValue(eq(BpmnModelConstants.NAMESPACE), eq(BpmnModelConstants.USER_TASK_CANDIDATE_STRATEGY))) + .thenReturn(BpmTaskCandidateStrategyEnum.USER.getStrategy().toString()); + when(userTask.getAttributeValue(eq(BpmnModelConstants.NAMESPACE), eq(BpmnModelConstants.USER_TASK_CANDIDATE_PARAM))) + .thenReturn(param); + // mock 方法(adminUserApi) + AdminUserRespDTO user1 = randomPojo(AdminUserRespDTO.class, o -> o.setId(1L) + .setStatus(CommonStatusEnum.DISABLE.getStatus())); + AdminUserRespDTO user2 = randomPojo(AdminUserRespDTO.class, o -> o.setId(2L) + .setStatus(CommonStatusEnum.DISABLE.getStatus())); + Map userMap = MapUtil.builder(user1.getId(), user1) + .put(user2.getId(), user2).build(); + when(adminUserApi.getUserMap(eq(asSet(1L, 2L)))).thenReturn(userMap); + // mock 方法(empty) + when(emptyStrategy.calculateUsersByTask(same(execution), same(param))) + .thenReturn(Sets.newSet(2L)); + // mock 移除发起人的用户 + springUtilMockedStatic.when(() -> SpringUtil.getBean(BpmProcessInstanceService.class)) + .thenReturn(processInstanceService); + ProcessInstance processInstance = mock(ProcessInstance.class); + when(processInstanceService.getProcessInstance(eq(processInstanceId))).thenReturn(processInstance); + when(processInstance.getStartUserId()).thenReturn("1"); + + // 调用 + Set results = taskCandidateInvoker.calculateUsersByTask(execution); + // 断言 + assertEquals(asSet(2L), results); + } + } + + /** + * 场景:没有计算到候选人,但是被禁用移除,最终通过 empty 进行分配 + */ + @Test + public void testCalculateUsersByActivity_some() { + try (MockedStatic bpmnModelUtilsMockedStatic = mockStatic(BpmnModelUtils.class)) { + // 准备参数 + String param = "1,2"; + BpmnModel bpmnModel = mock(BpmnModel.class); + String activityId = randomString(); + Long startUserId = 1L; + String processDefinitionId = randomString(); + Map processVariables = new HashMap<>(); + // mock 方法(DelegateExecution) + UserTask userTask = mock(UserTask.class); + bpmnModelUtilsMockedStatic.when(() -> BpmnModelUtils.parseCandidateStrategy(same(userTask))) + .thenReturn(BpmTaskCandidateStrategyEnum.USER.getStrategy()); + bpmnModelUtilsMockedStatic.when(() -> BpmnModelUtils.parseCandidateParam(same(userTask))) + .thenReturn(param); + bpmnModelUtilsMockedStatic.when(() -> BpmnModelUtils.getFlowElementById(same(bpmnModel), eq(activityId))).thenReturn(userTask); + // mock 方法(adminUserApi) + AdminUserRespDTO user1 = randomPojo(AdminUserRespDTO.class, o -> o.setId(1L) + .setStatus(CommonStatusEnum.ENABLE.getStatus())); + AdminUserRespDTO user2 = randomPojo(AdminUserRespDTO.class, o -> o.setId(2L) + .setStatus(CommonStatusEnum.ENABLE.getStatus())); + Map userMap = MapUtil.builder(user1.getId(), user1) + .put(user2.getId(), user2).build(); + when(adminUserApi.getUserMap(eq(asSet(1L, 2L)))).thenReturn(userMap); + // mock 移除发起人的用户 + bpmnModelUtilsMockedStatic.when(() -> BpmnModelUtils.parseAssignStartUserHandlerType(same(userTask))) + .thenReturn(BpmUserTaskAssignStartUserHandlerTypeEnum.SKIP.getType()); + + // 调用 + Set results = taskCandidateInvoker.calculateUsersByActivity(bpmnModel, activityId, + startUserId, processDefinitionId, processVariables); + // 断言 + assertEquals(asSet(2L), results); + } + } + + /** + * 场景:成功计算到候选人,但是移除了发起人的用户 + */ + @Test + public void testCalculateUsersByActivity_none() { + try (MockedStatic bpmnModelUtilsMockedStatic = mockStatic(BpmnModelUtils.class)) { + // 准备参数 + String param = "1,2"; + BpmnModel bpmnModel = mock(BpmnModel.class); + String activityId = randomString(); + Long startUserId = 1L; + String processDefinitionId = randomString(); + Map processVariables = new HashMap<>(); + // mock 方法(DelegateExecution) + UserTask userTask = mock(UserTask.class); + bpmnModelUtilsMockedStatic.when(() -> BpmnModelUtils.parseCandidateStrategy(same(userTask))) + .thenReturn(BpmTaskCandidateStrategyEnum.USER.getStrategy()); + bpmnModelUtilsMockedStatic.when(() -> BpmnModelUtils.parseCandidateParam(same(userTask))) + .thenReturn(param); + bpmnModelUtilsMockedStatic.when(() -> BpmnModelUtils.getFlowElementById(same(bpmnModel), eq(activityId))).thenReturn(userTask); + // mock 方法(adminUserApi) + AdminUserRespDTO user1 = randomPojo(AdminUserRespDTO.class, o -> o.setId(1L) + .setStatus(CommonStatusEnum.DISABLE.getStatus())); + AdminUserRespDTO user2 = randomPojo(AdminUserRespDTO.class, o -> o.setId(2L) + .setStatus(CommonStatusEnum.DISABLE.getStatus())); + Map userMap = MapUtil.builder(user1.getId(), user1) + .put(user2.getId(), user2).build(); + when(adminUserApi.getUserMap(eq(asSet(1L, 2L)))).thenReturn(userMap); + // mock 方法(empty) + when(emptyStrategy.calculateUsersByActivity(same(bpmnModel), eq(activityId), + eq(param), same(startUserId), same(processDefinitionId), same(processVariables))) + .thenReturn(Sets.newSet(2L)); + + // 调用 + Set results = taskCandidateInvoker.calculateUsersByActivity(bpmnModel, activityId, + startUserId, processDefinitionId, processVariables); + // 断言 + assertEquals(asSet(2L), results); + } + } + + private static void mockFlowElementExtensionElement(FlowElement element, String name, String value) { + if (value == null) { + return; + } + ExtensionElement extensionElement = new ExtensionElement(); + extensionElement.setNamespace(FLOWABLE_EXTENSIONS_NAMESPACE); + extensionElement.setNamespacePrefix(FLOWABLE_EXTENSIONS_PREFIX); + extensionElement.setElementText(value); + extensionElement.setName(name); + // mock + Map> extensionElements = element.getExtensionElements(); + if (extensionElements == null) { + extensionElements = new LinkedHashMap<>(); + } + extensionElements.put(name, Collections.singletonList(extensionElement)); + when(element.getExtensionElements()).thenReturn(extensionElements); } @Test diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateDeptLeaderMultiStrategyTest.java b/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateDeptLeaderMultiStrategyTest.java new file mode 100644 index 000000000..4bac14e95 --- /dev/null +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateDeptLeaderMultiStrategyTest.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.system.api.dept.DeptApi; +import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; +import org.assertj.core.util.Sets; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.stubbing.Answer; + +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +public class BpmTaskCandidateDeptLeaderMultiStrategyTest extends BaseMockitoUnitTest { + + @InjectMocks + private BpmTaskCandidateDeptLeaderMultiStrategy strategy; + + @Mock + private DeptApi deptApi; + + @Test + public void testCalculateUsers() { + // 准备参数 + String param = "10,20|2"; + // mock 方法 + when(deptApi.getDept(any())).thenAnswer((Answer< CommonResult>) invocationOnMock -> { + Long deptId = invocationOnMock.getArgument(0); + return success(randomPojo(DeptRespDTO.class, o -> o.setId(deptId).setParentId(deptId * 100).setLeaderUserId(deptId + 1))); + }); + + // 调用 + Set userIds = strategy.calculateUsers(param); + // 断言结果 + assertEquals(Sets.newLinkedHashSet(11L, 1001L, 21L, 2001L), userIds); + } + +} diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateDeptLeaderStrategyTest.java b/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateDeptLeaderStrategyTest.java similarity index 62% rename from yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateDeptLeaderStrategyTest.java rename to yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateDeptLeaderStrategyTest.java index 3df20d64b..8d515e6fb 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateDeptLeaderStrategyTest.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateDeptLeaderStrategyTest.java @@ -1,9 +1,10 @@ -package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy; +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept; +import cn.iocoder.yudao.framework.common.util.collection.SetUtils; import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; import cn.iocoder.yudao.module.system.api.dept.DeptApi; import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; -import org.junit.jupiter.api.Disabled; +import org.assertj.core.util.Sets; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; @@ -11,14 +12,12 @@ import org.mockito.Mock; import java.util.Set; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; -import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet; import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; import static java.util.Arrays.asList; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; -@Disabled // TODO 芋艿:临时注释 public class BpmTaskCandidateDeptLeaderStrategyTest extends BaseMockitoUnitTest { @InjectMocks @@ -30,16 +29,16 @@ public class BpmTaskCandidateDeptLeaderStrategyTest extends BaseMockitoUnitTest @Test public void testCalculateUsers() { // 准备参数 - String param = "1,2"; + String param = "10,20"; // mock 方法 - DeptRespDTO dept1 = randomPojo(DeptRespDTO.class, o -> o.setLeaderUserId(11L)); - DeptRespDTO dept2 = randomPojo(DeptRespDTO.class, o -> o.setLeaderUserId(22L)); - when(deptApi.getDeptList(eq(asSet(1L, 2L)))).thenReturn(success(asList(dept1, dept2))); + when(deptApi.getDeptList(eq(SetUtils.asSet(10L, 20L)))).thenReturn(success(asList( + randomPojo(DeptRespDTO.class, o -> o.setId(10L).setParentId(10L).setLeaderUserId(11L)), + randomPojo(DeptRespDTO.class, o -> o.setId(20L).setParentId(20L).setLeaderUserId(21L))))); // 调用 - Set results = strategy.calculateUsers(null, param); - // 断言 - assertEquals(asSet(11L, 22L), results); + Set userIds = strategy.calculateUsers(param); + // 断言结果 + assertEquals(Sets.newLinkedHashSet(11L, 21L), userIds); } } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateDeptMemberStrategyTest.java b/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateDeptMemberStrategyTest.java similarity index 54% rename from yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateDeptMemberStrategyTest.java rename to yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateDeptMemberStrategyTest.java index 198c3fea7..809d714e2 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateDeptMemberStrategyTest.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateDeptMemberStrategyTest.java @@ -1,45 +1,47 @@ -package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy; +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept; +import cn.iocoder.yudao.framework.common.util.collection.SetUtils; import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.system.api.dept.DeptApi; import cn.iocoder.yudao.module.system.api.user.AdminUserApi; import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; -import org.junit.jupiter.api.Disabled; +import org.assertj.core.util.Sets; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; -import java.util.List; import java.util.Set; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; -import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static java.util.Arrays.asList; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; -@Disabled // TODO 芋艿:临时注释 public class BpmTaskCandidateDeptMemberStrategyTest extends BaseMockitoUnitTest { @InjectMocks private BpmTaskCandidateDeptMemberStrategy strategy; + @Mock + private DeptApi deptApi; @Mock private AdminUserApi adminUserApi; @Test public void testCalculateUsers() { // 准备参数 - String param = "11,22"; + String param = "10,20"; // mock 方法 - List users = convertList(asSet(11L, 22L), - id -> new AdminUserRespDTO().setId(id)); - when(adminUserApi.getUserListByDeptIds(eq(asSet(11L, 22L)))).thenReturn(success(users)); + when(adminUserApi.getUserListByDeptIds(eq(SetUtils.asSet(10L, 20L)))).thenReturn(success(asList( + randomPojo(AdminUserRespDTO.class, o -> o.setId(11L)), + randomPojo(AdminUserRespDTO.class, o -> o.setId(21L))))); // 调用 - Set results = strategy.calculateUsers(null, param); - // 断言 - assertEquals(asSet(11L, 22L), results); + Set userIds = strategy.calculateUsers(param); + // 断言结果 + assertEquals(Sets.newLinkedHashSet(11L, 21L), userIds); } } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserDeptLeaderMultiStrategyTest.java b/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserDeptLeaderMultiStrategyTest.java new file mode 100644 index 000000000..76a1cf376 --- /dev/null +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserDeptLeaderMultiStrategyTest.java @@ -0,0 +1,84 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; +import cn.iocoder.yudao.module.system.api.dept.DeptApi; +import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import org.assertj.core.util.Sets; +import org.flowable.engine.delegate.DelegateExecution; +import org.flowable.engine.runtime.ProcessInstance; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.stubbing.Answer; + +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class BpmTaskCandidateStartUserDeptLeaderMultiStrategyTest extends BaseMockitoUnitTest { + + @InjectMocks + private BpmTaskCandidateStartUserDeptLeaderMultiStrategy strategy; + + @Mock + private BpmProcessInstanceService processInstanceService; + + @Mock + private AdminUserApi adminUserApi; + @Mock + private DeptApi deptApi; + + @Test + public void testCalculateUsersByTask() { + // 准备参数 + String param = "2"; + // mock 方法(获得流程发起人) + Long startUserId = 1L; + ProcessInstance processInstance = mock(ProcessInstance.class); + DelegateExecution execution = mock(DelegateExecution.class); + when(processInstanceService.getProcessInstance(eq(execution.getProcessInstanceId()))).thenReturn(processInstance); + when(processInstance.getStartUserId()).thenReturn(startUserId.toString()); + // mock 方法(获取发起人的 multi 部门负责人) + mockGetStartUserDept(startUserId); + + // 调用 + Set userIds = strategy.calculateUsersByTask(execution, param); + // 断言 + assertEquals(Sets.newLinkedHashSet(11L, 1001L), userIds); + } + + @Test + public void testCalculateUsersByActivity() { + // 准备参数 + String param = "2"; + // mock 方法 + Long startUserId = 1L; + mockGetStartUserDept(startUserId); + + // 调用 + Set userIds = strategy.calculateUsersByActivity(null, null, param, + startUserId, null, null); + // 断言 + assertEquals(Sets.newLinkedHashSet(11L, 1001L), userIds); + } + + private void mockGetStartUserDept(Long startUserId) { + when(adminUserApi.getUser(eq(startUserId))).thenReturn( + success(randomPojo(AdminUserRespDTO.class, o -> o.setId(startUserId).setDeptId(10L)))); + when(deptApi.getDept(any())).thenAnswer((Answer< CommonResult>) invocationOnMock -> { + Long deptId = invocationOnMock.getArgument(0); + return success(randomPojo(DeptRespDTO.class, o -> o.setId(deptId).setParentId(deptId * 100).setLeaderUserId(deptId + 1))); + }); + } + +} diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserDeptLeaderStrategyTest.java b/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserDeptLeaderStrategyTest.java new file mode 100644 index 000000000..559b017e1 --- /dev/null +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserDeptLeaderStrategyTest.java @@ -0,0 +1,84 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; +import cn.iocoder.yudao.module.system.api.dept.DeptApi; +import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import org.assertj.core.util.Sets; +import org.flowable.engine.delegate.DelegateExecution; +import org.flowable.engine.runtime.ProcessInstance; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.stubbing.Answer; + +import java.util.Set; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class BpmTaskCandidateStartUserDeptLeaderStrategyTest extends BaseMockitoUnitTest { + + @InjectMocks + private BpmTaskCandidateStartUserDeptLeaderStrategy strategy; + + @Mock + private BpmProcessInstanceService processInstanceService; + + @Mock + private AdminUserApi adminUserApi; + @Mock + private DeptApi deptApi; + + @Test + public void testCalculateUsersByTask() { + // 准备参数 + String param = "2"; + // mock 方法(获得流程发起人) + Long startUserId = 1L; + ProcessInstance processInstance = mock(ProcessInstance.class); + DelegateExecution execution = mock(DelegateExecution.class); + when(processInstanceService.getProcessInstance(eq(execution.getProcessInstanceId()))).thenReturn(processInstance); + when(processInstance.getStartUserId()).thenReturn(startUserId.toString()); + // mock 方法(获取发起人的部门负责人) + mockGetStartUserDeptLeader(startUserId); + + // 调用 + Set userIds = strategy.calculateUsersByTask(execution, param); + // 断言 + assertEquals(Sets.newLinkedHashSet(1001L), userIds); + } + + @Test + public void testGetStartUserDeptLeader() { + // 准备参数 + String param = "2"; + // mock 方法 + Long startUserId = 1L; + mockGetStartUserDeptLeader(startUserId); + + // 调用 + Set userIds = strategy.calculateUsersByActivity(null, null, param, + startUserId, null, null); + // 断言 + assertEquals(Sets.newLinkedHashSet(1001L), userIds); + } + + private void mockGetStartUserDeptLeader(Long startUserId) { + when(adminUserApi.getUser(eq(startUserId))).thenReturn( + success(randomPojo(AdminUserRespDTO.class, o -> o.setId(startUserId).setDeptId(10L)))); + when(deptApi.getDept(any())).thenAnswer((Answer< CommonResult>) invocationOnMock -> { + Long deptId = invocationOnMock.getArgument(0); + return success(randomPojo(DeptRespDTO.class, o -> o.setId(deptId).setParentId(deptId * 100).setLeaderUserId(deptId + 1))); + }); + } + +} diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserSelectStrategyTest.java b/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserSelectStrategyTest.java new file mode 100644 index 000000000..f63ccc332 --- /dev/null +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserSelectStrategyTest.java @@ -0,0 +1,68 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept; + +import cn.hutool.core.map.MapUtil; +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants; +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; +import org.assertj.core.util.Sets; +import org.flowable.engine.delegate.DelegateExecution; +import org.flowable.engine.runtime.ProcessInstance; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class BpmTaskCandidateStartUserSelectStrategyTest extends BaseMockitoUnitTest { + + @InjectMocks + private BpmTaskCandidateStartUserSelectStrategy strategy; + + @Mock + private BpmProcessInstanceService processInstanceService; + + @Test + public void testCalculateUsersByTask() { + // 准备参数 + String param = "2"; + // mock 方法(获得流程发起人) + ProcessInstance processInstance = mock(ProcessInstance.class); + DelegateExecution execution = mock(DelegateExecution.class); + when(processInstanceService.getProcessInstance(eq(execution.getProcessInstanceId()))).thenReturn(processInstance); + when(execution.getCurrentActivityId()).thenReturn("activity_001"); + // mock 方法(FlowableUtils) + Map processVariables = new HashMap<>(); + processVariables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES, + MapUtil.of("activity_001", List.of(1L, 2L))); + when(processInstance.getProcessVariables()).thenReturn(processVariables); + + // 调用 + Set userIds = strategy.calculateUsersByTask(execution, param); + // 断言 + assertEquals(Sets.newLinkedHashSet(1L, 2L), userIds); + } + + @Test + public void testCalculateUsersByActivity() { + // 准备参数 + String activityId = "activity_001"; + Map processVariables = new HashMap<>(); + processVariables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES, + MapUtil.of("activity_001", List.of(1L, 2L))); + + // 调用 + Set userIds = strategy.calculateUsersByActivity(null, activityId, null, + null, null, processVariables); + // 断言 + assertEquals(Sets.newLinkedHashSet(1L, 2L), userIds); + } + +} diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/other/BpmTaskCandidateAssignEmptyStrategyTest.java b/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/other/BpmTaskCandidateAssignEmptyStrategyTest.java new file mode 100644 index 000000000..d0add2481 --- /dev/null +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/other/BpmTaskCandidateAssignEmptyStrategyTest.java @@ -0,0 +1,88 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.other; + +import cn.hutool.core.collection.ListUtil; +import cn.iocoder.yudao.framework.common.util.collection.SetUtils; +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskAssignEmptyHandlerTypeEnum; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils; +import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService; +import org.flowable.bpmn.model.BpmnModel; +import org.flowable.bpmn.model.FlowElement; +import org.flowable.engine.delegate.DelegateExecution; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; + +import java.util.Set; + +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.*; + +public class BpmTaskCandidateAssignEmptyStrategyTest extends BaseMockitoUnitTest { + + @InjectMocks + private BpmTaskCandidateAssignEmptyStrategy strategy; + + @Mock + private BpmProcessDefinitionService processDefinitionService; + + @Test + public void testCalculateUsersByTask() { + try (MockedStatic flowableUtilMockedStatic = mockStatic(FlowableUtils.class); + MockedStatic bpmnModelUtilsMockedStatic = mockStatic(BpmnModelUtils.class)) { + // 准备参数 + DelegateExecution execution = mock(DelegateExecution.class); + String param = randomString(); + // mock 方法(execution) + String processDefinitionId = randomString(); + when(execution.getProcessDefinitionId()).thenReturn(processDefinitionId); + FlowElement flowElement = mock(FlowElement.class); + when(execution.getCurrentFlowElement()).thenReturn(flowElement); + // mock 方法(parseAssignEmptyHandlerType) + bpmnModelUtilsMockedStatic.when(() -> BpmnModelUtils.parseAssignEmptyHandlerType(same(flowElement))) + .thenReturn(BpmUserTaskAssignEmptyHandlerTypeEnum.ASSIGN_USER.getType()); + bpmnModelUtilsMockedStatic.when(() -> BpmnModelUtils.parseAssignEmptyHandlerUserIds(same(flowElement))) + .thenReturn(ListUtil.of(1L, 2L)); + + // 调用 + Set userIds = strategy.calculateUsersByTask(execution, param); + // 断言 + assertEquals(SetUtils.asSet(1L, 2L), userIds); + } + + } + + @Test + public void testCalculateUsersByActivity() { + try (MockedStatic bpmnModelUtilsMockedStatic = mockStatic(BpmnModelUtils.class)) { + // 准备参数 + String processDefinitionId = randomString(); + String activityId = randomString(); + String param = randomString(); + // mock 方法(getFlowElementById) + FlowElement flowElement = mock(FlowElement.class); + BpmnModel bpmnModel = mock(BpmnModel.class); + bpmnModelUtilsMockedStatic.when(() -> BpmnModelUtils.getFlowElementById(same(bpmnModel), eq(activityId))) + .thenReturn(flowElement); + // mock 方法(parseAssignEmptyHandlerType) + bpmnModelUtilsMockedStatic.when(() -> BpmnModelUtils.parseAssignEmptyHandlerType(same(flowElement))) + .thenReturn(BpmUserTaskAssignEmptyHandlerTypeEnum.ASSIGN_ADMIN.getType()); + // mock 方法(getProcessDefinitionInfo) + BpmProcessDefinitionInfoDO processDefinition = randomPojo(BpmProcessDefinitionInfoDO.class, + o -> o.setManagerUserIds(ListUtil.of(1L, 2L))); + when(processDefinitionService.getProcessDefinitionInfo(eq(processDefinitionId))).thenReturn(processDefinition); + + // 调用 + Set userIds = strategy.calculateUsersByActivity(bpmnModel, activityId, param, + null, processDefinitionId, null); + // 断言 + assertEquals(SetUtils.asSet(1L, 2L), userIds); + } + } + +} diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateExpressionStrategyTest.java b/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/other/BpmTaskCandidateExpressionStrategyTest.java similarity index 60% rename from yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateExpressionStrategyTest.java rename to yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/other/BpmTaskCandidateExpressionStrategyTest.java index 5182ab03b..1da8a2f2e 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateExpressionStrategyTest.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/other/BpmTaskCandidateExpressionStrategyTest.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy; +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.other; import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils; @@ -8,6 +8,8 @@ import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.MockedStatic; +import java.util.HashMap; +import java.util.Map; import java.util.Set; import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet; @@ -22,7 +24,7 @@ public class BpmTaskCandidateExpressionStrategyTest extends BaseMockitoUnitTest private BpmTaskCandidateExpressionStrategy strategy; @Test - public void testCalculateUsers() { + public void testCalculateUsersByTask() { try (MockedStatic flowableUtilMockedStatic = mockStatic(FlowableUtils.class)) { // 准备参数 String param = "1,2"; @@ -32,7 +34,25 @@ public class BpmTaskCandidateExpressionStrategyTest extends BaseMockitoUnitTest .thenReturn(asSet(1L, 2L)); // 调用 - Set results = strategy.calculateUsers(execution, param); + Set results = strategy.calculateUsersByTask(execution, param); + // 断言 + assertEquals(asSet(1L, 2L), results); + } + } + + @Test + public void testCalculateUsersByActivity() { + try (MockedStatic flowableUtilMockedStatic = mockStatic(FlowableUtils.class)) { + // 准备参数 + String param = "1,2"; + Map processVariables = new HashMap<>(); + // mock 方法 + flowableUtilMockedStatic.when(() -> FlowableUtils.getExpressionValue(same(processVariables), eq(param))) + .thenReturn(asSet(1L, 2L)); + + // 调用 + Set results = strategy.calculateUsersByActivity(null, null, param, + null, null, processVariables); // 断言 assertEquals(asSet(1L, 2L), results); } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateGroupStrategyTest.java b/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateGroupStrategyTest.java similarity index 90% rename from yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateGroupStrategyTest.java rename to yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateGroupStrategyTest.java index 3977879ce..44327ec7a 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateGroupStrategyTest.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateGroupStrategyTest.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy; +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.user; import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmUserGroupDO; @@ -36,9 +36,9 @@ public class BpmTaskCandidateGroupStrategyTest extends BaseMockitoUnitTest { when(userGroupService.getUserGroupList(eq(asSet(1L, 2L)))).thenReturn(Arrays.asList(userGroup1, userGroup2)); // 调用 - Set results = strategy.calculateUsers(null, param); + Set userIds = strategy.calculateUsersByTask(null, param); // 断言 - assertEquals(asSet(11L, 12L, 21L, 22L), results); + assertEquals(asSet(11L, 12L, 21L, 22L), userIds); } } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidatePostStrategyTest.java b/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidatePostStrategyTest.java similarity index 91% rename from yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidatePostStrategyTest.java rename to yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidatePostStrategyTest.java index 280480bdb..8d2191eeb 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidatePostStrategyTest.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidatePostStrategyTest.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy; +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.user; import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; import cn.iocoder.yudao.module.system.api.dept.PostApi; @@ -40,9 +40,9 @@ public class BpmTaskCandidatePostStrategyTest extends BaseMockitoUnitTest { when(adminUserApi.getUserListByPostIds(eq(asSet(1L, 2L)))).thenReturn(success(users)); // 调用 - Set results = strategy.calculateUsers(null, param); + Set userIds = strategy.calculateUsersByTask(null, param); // 断言 - assertEquals(asSet(11L, 22L), results); + assertEquals(asSet(11L, 22L), userIds); } } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateRoleStrategyTest.java b/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateRoleStrategyTest.java similarity index 90% rename from yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateRoleStrategyTest.java rename to yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateRoleStrategyTest.java index cecc2a9fb..0e4aa917a 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateRoleStrategyTest.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateRoleStrategyTest.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy; +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.user; import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; import cn.iocoder.yudao.module.system.api.permission.PermissionApi; @@ -36,9 +36,9 @@ public class BpmTaskCandidateRoleStrategyTest extends BaseMockitoUnitTest { .thenReturn(success(asSet(11L, 22L))); // 调用 - Set results = strategy.calculateUsers(null, param); + Set userIds = strategy.calculateUsersByTask(null, param); // 断言 - assertEquals(asSet(11L, 22L), results); + assertEquals(asSet(11L, 22L), userIds); } } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateStartUserStrategyTest.java b/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateStartUserStrategyTest.java new file mode 100644 index 000000000..1b8eba195 --- /dev/null +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateStartUserStrategyTest.java @@ -0,0 +1,56 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.user; + +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; +import org.assertj.core.util.Sets; +import org.flowable.engine.delegate.DelegateExecution; +import org.flowable.engine.runtime.ProcessInstance; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class BpmTaskCandidateStartUserStrategyTest extends BaseMockitoUnitTest { + + @InjectMocks + private BpmTaskCandidateStartUserStrategy strategy; + + @Mock + private BpmProcessInstanceService processInstanceService; + + @Test + public void testCalculateUsersByTask() { + // 准备参数 + String param = "2"; + // mock 方法(获得流程发起人) + Long startUserId = 1L; + ProcessInstance processInstance = mock(ProcessInstance.class); + DelegateExecution execution = mock(DelegateExecution.class); + when(processInstanceService.getProcessInstance(eq(execution.getProcessInstanceId()))).thenReturn(processInstance); + when(processInstance.getStartUserId()).thenReturn(startUserId.toString()); + + // 调用 + Set userIds = strategy.calculateUsersByTask(execution, param); + // 断言 + assertEquals(Sets.newLinkedHashSet(startUserId), userIds); + } + + @Test + public void testCalculateUsersByActivity() { + // 准备参数 + Long startUserId = 1L; + + // 调用 + Set userIds = strategy.calculateUsersByActivity(null, null, null, + startUserId, null, null); + // 断言 + assertEquals(Sets.newLinkedHashSet(startUserId), userIds); + } + +} diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateUserStrategyTest.java b/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateUserStrategyTest.java similarity index 80% rename from yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateUserStrategyTest.java rename to yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateUserStrategyTest.java index ca1b71e98..fca4aa192 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateUserStrategyTest.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateUserStrategyTest.java @@ -1,4 +1,4 @@ -package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy; +package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.user; import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; import org.junit.jupiter.api.Disabled; @@ -17,14 +17,15 @@ public class BpmTaskCandidateUserStrategyTest extends BaseMockitoUnitTest { private BpmTaskCandidateUserStrategy strategy; @Test - public void testCalculateUsers() { + public void test() { // 准备参数 String param = "1,2"; // 调用 - Set results = strategy.calculateUsers(null, param); + Set userIds = strategy.calculateUsersByTask(null, param); // 断言 - assertEquals(asSet(1L, 2L), results); + assertEquals(asSet(1L, 2L), userIds); } + }