From 0d37be9f2e8d3b285ead1bf5e228a20ee246b23d Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 25 Jan 2025 09:48:53 +0800 Subject: [PATCH] =?UTF-8?q?=E3=80=90=E5=90=8C=E6=AD=A5=E3=80=91BOOT=20?= =?UTF-8?q?=E5=92=8C=20CLOUD=20=E7=9A=84=E5=8A=9F=E8=83=BD=EF=BC=88BPM?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../YudaoWebSecurityConfigurerAdapter.java | 22 +- .../module/bpm/enums/ErrorCodeConstants.java | 3 + .../definition/BpmAutoApproveTypeEnum.java | 32 + ...ype.java => BpmBoundaryEventTypeEnum.java} | 7 +- .../definition/BpmDelayTimerTypeEnum.java | 31 + .../BpmHttpRequestParamTypeEnum.java | 31 + ...e.java => BpmProcessListenerTypeEnum.java} | 2 +- ...a => BpmProcessListenerValueTypeEnum.java} | 2 +- ...va => BpmSimpleModeConditionTypeEnum.java} | 6 +- ...e.java => BpmSimpleModelNodeTypeEnum.java} | 13 +- .../enums/definition/BpmTriggerTypeEnum.java | 42 + ... => BpmUserTaskRejectHandlerTypeEnum.java} | 6 +- .../admin/definition/BpmModelController.java | 9 + .../definition/vo/form/BpmFormFieldVO.java | 24 + .../vo/model/BpmModelMetaInfoVO.java | 70 ++ .../vo/model/simple/BpmSimpleModelNodeVO.java | 197 ++++- .../task/BpmProcessInstanceController.java | 8 +- .../admin/task/BpmTaskController.java | 16 +- .../vo/instance/BpmApprovalDetailRespVO.java | 3 + .../vo/instance/BpmProcessInstanceRespVO.java | 4 + .../task/vo/task/BpmTaskApproveReqVO.java | 6 +- .../task/vo/task/BpmTaskRejectReqVO.java | 1 - .../admin/task/vo/task/BpmTaskRespVO.java | 11 + .../task/BpmProcessInstanceConvert.java | 9 +- .../bpm/convert/task/BpmTaskConvert.java | 13 +- .../BpmProcessDefinitionInfoDO.java | 32 + .../definition/BpmProcessListenerDO.java | 3 +- .../task/BpmProcessInstanceCopyMapper.java | 7 +- .../bpm/dal/redis/BpmProcessIdRedisDAO.java | 55 ++ .../bpm/dal/redis/RedisKeyConstants.java | 15 + .../candidate/BpmTaskCandidateInvoker.java | 57 +- .../core/enums/BpmnModelConstants.java | 19 + .../core/enums/BpmnVariableConstants.java | 22 + .../BpmProcessInstanceEventListener.java | 8 + .../core/listener/BpmTaskEventListener.java | 22 +- .../core/listener/BpmTriggerTaskDelegate.java | 55 ++ .../flowable/core/util/BpmnModelUtils.java | 71 +- .../flowable/core/util/FlowableUtils.java | 94 ++- .../flowable/core/util/SimpleModelUtils.java | 349 ++++++-- .../service/definition/BpmModelService.java | 8 + .../definition/BpmModelServiceImpl.java | 46 ++ .../BpmProcessListenerServiceImpl.java | 14 +- .../task/BpmProcessInstanceCopyService.java | 7 + .../BpmProcessInstanceCopyServiceImpl.java | 5 + .../task/BpmProcessInstanceServiceImpl.java | 747 +++++++++++++++++- .../bpm/service/task/BpmTaskService.java | 11 +- .../bpm/service/task/BpmTaskServiceImpl.java | 111 ++- .../task/listener/BpmUserTaskListener.java | 96 +++ .../task/trigger/BpmHttpRequestTrigger.java | 75 ++ .../bpm/service/task/trigger/BpmTrigger.java | 30 + .../impl/weixin/AbstractWxPayClient.java | 2 +- 51 files changed, 2310 insertions(+), 219 deletions(-) create mode 100644 yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmAutoApproveTypeEnum.java rename yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/{BpmBoundaryEventType.java => BpmBoundaryEventTypeEnum.java} (68%) create mode 100644 yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmDelayTimerTypeEnum.java create mode 100644 yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmHttpRequestParamTypeEnum.java rename yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/{BpmProcessListenerType.java => BpmProcessListenerTypeEnum.java} (89%) rename yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/{BpmProcessListenerValueType.java => BpmProcessListenerValueTypeEnum.java} (90%) rename yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/{BpmSimpleModeConditionType.java => BpmSimpleModeConditionTypeEnum.java} (76%) rename yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/{BpmSimpleModelNodeType.java => BpmSimpleModelNodeTypeEnum.java} (79%) create mode 100644 yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmTriggerTypeEnum.java rename yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/{BpmUserTaskRejectHandlerType.java => BpmUserTaskRejectHandlerTypeEnum.java} (76%) create mode 100644 yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/form/BpmFormFieldVO.java create mode 100644 yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/redis/BpmProcessIdRedisDAO.java create mode 100644 yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/redis/RedisKeyConstants.java create mode 100644 yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmTriggerTaskDelegate.java create mode 100644 yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/listener/BpmUserTaskListener.java create mode 100644 yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/BpmHttpRequestTrigger.java create mode 100644 yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/BpmTrigger.java diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoWebSecurityConfigurerAdapter.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoWebSecurityConfigurerAdapter.java index e36faccf5..68ae1b30c 100644 --- a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoWebSecurityConfigurerAdapter.java +++ b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoWebSecurityConfigurerAdapter.java @@ -127,17 +127,17 @@ public class YudaoWebSecurityConfigurerAdapter { httpSecurity // ①:全局共享规则 .authorizeHttpRequests(c -> c - // 1.1 静态资源,可匿名访问 - .requestMatchers(HttpMethod.GET, "/*.html", "/*.html", "/*.css", "/*.js").permitAll() - // 1.2 设置 @PermitAll 无需认证 - .requestMatchers(HttpMethod.GET, permitAllUrls.get(HttpMethod.GET).toArray(new String[0])).permitAll() - .requestMatchers(HttpMethod.POST, permitAllUrls.get(HttpMethod.POST).toArray(new String[0])).permitAll() - .requestMatchers(HttpMethod.PUT, permitAllUrls.get(HttpMethod.PUT).toArray(new String[0])).permitAll() - .requestMatchers(HttpMethod.DELETE, permitAllUrls.get(HttpMethod.DELETE).toArray(new String[0])).permitAll() - .requestMatchers(HttpMethod.HEAD, permitAllUrls.get(HttpMethod.HEAD).toArray(new String[0])).permitAll() - .requestMatchers(HttpMethod.PATCH, permitAllUrls.get(HttpMethod.PATCH).toArray(new String[0])).permitAll() - // 1.3 基于 yudao.security.permit-all-urls 无需认证 - .requestMatchers(securityProperties.getPermitAllUrls().toArray(new String[0])).permitAll() + // 1.1 静态资源,可匿名访问 + .requestMatchers(HttpMethod.GET, "/*.html", "/*.css", "/*.js").permitAll() + // 1.2 设置 @PermitAll 无需认证 + .requestMatchers(HttpMethod.GET, permitAllUrls.get(HttpMethod.GET).toArray(new String[0])).permitAll() + .requestMatchers(HttpMethod.POST, permitAllUrls.get(HttpMethod.POST).toArray(new String[0])).permitAll() + .requestMatchers(HttpMethod.PUT, permitAllUrls.get(HttpMethod.PUT).toArray(new String[0])).permitAll() + .requestMatchers(HttpMethod.DELETE, permitAllUrls.get(HttpMethod.DELETE).toArray(new String[0])).permitAll() + .requestMatchers(HttpMethod.HEAD, permitAllUrls.get(HttpMethod.HEAD).toArray(new String[0])).permitAll() + .requestMatchers(HttpMethod.PATCH, permitAllUrls.get(HttpMethod.PATCH).toArray(new String[0])).permitAll() + // 1.3 基于 yudao.security.permit-all-urls 无需认证 + .requestMatchers(securityProperties.getPermitAllUrls().toArray(new String[0])).permitAll() ) // ②:每个项目的自定义规则 .authorizeHttpRequests(c -> authorizeRequestsCustomizers.forEach(customizer -> customizer.customize(c))) 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 6c7a7ce92..7fbc7ba22 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 @@ -38,6 +38,7 @@ public interface ErrorCodeConstants { 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, "发起流程失败,你没有权限发起该流程"); + ErrorCode PROCESS_INSTANCE_CANCEL_FAIL_NOT_ALLOW = new ErrorCode(1_009_004_005, "流程取消失败,该流程不允许取消"); // ========== 流程任务 1-009-005-000 ========== ErrorCode TASK_OPERATE_FAIL_ASSIGN_NOT_SELF = new ErrorCode(1_009_005_001, "操作失败,原因:该任务的审批人不是你"); @@ -54,6 +55,8 @@ public interface ErrorCodeConstants { ErrorCode TASK_TRANSFER_FAIL_USER_REPEAT = new ErrorCode(1_009_005_013, "任务转办失败,转办人和当前审批人为同一人"); ErrorCode TASK_TRANSFER_FAIL_USER_NOT_EXISTS = new ErrorCode(1_009_005_014, "任务转办失败,转办人不存在"); ErrorCode TASK_CREATE_FAIL_NO_CANDIDATE_USER = new ErrorCode(1_009_006_003, "操作失败,原因:找不到任务的审批人!"); + ErrorCode TASK_SIGNATURE_NOT_EXISTS = new ErrorCode(1_009_005_015, "签名不能为空!"); + ErrorCode TASK_REASON_REQUIRE = new ErrorCode(1_009_005_016, "审批意见不能为空!"); // ========== 动态表单模块 1-009-010-000 ========== ErrorCode FORM_NOT_EXISTS = new ErrorCode(1_009_010_000, "动态表单不存在"); diff --git a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmAutoApproveTypeEnum.java b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmAutoApproveTypeEnum.java new file mode 100644 index 000000000..f2f58c02e --- /dev/null +++ b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmAutoApproveTypeEnum.java @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.module.bpm.enums.definition; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * BPM 自动去重的类型的枚举 + * + * @author Lesan + */ +@Getter +@AllArgsConstructor +public enum BpmAutoApproveTypeEnum implements ArrayValuable { + + NONE(0, "不自动通过"), + APPROVE_ALL(1, "仅审批一次,后续重复的审批节点均自动通过"), + APPROVE_SEQUENT(2, "仅针对连续审批的节点自动通过"); + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(BpmAutoApproveTypeEnum::getType).toArray(Integer[]::new); + + private final Integer type; + private final String name; + + @Override + public Integer[] array() { + return ARRAYS; + } + +} \ 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/definition/BpmBoundaryEventType.java b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmBoundaryEventTypeEnum.java similarity index 68% rename from yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmBoundaryEventType.java rename to yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmBoundaryEventTypeEnum.java index 537e03e03..a63804de3 100644 --- a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmBoundaryEventType.java +++ b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmBoundaryEventTypeEnum.java @@ -11,14 +11,15 @@ import lombok.Getter; */ @Getter @AllArgsConstructor -public enum BpmBoundaryEventType { +public enum BpmBoundaryEventTypeEnum { - USER_TASK_TIMEOUT(1,"用户任务超时"); + USER_TASK_TIMEOUT(1, "用户任务超时"), + DELAY_TIMER_TIMEOUT(2, "延迟器超时"); private final Integer type; private final String name; - public static BpmBoundaryEventType typeOf(Integer type) { + public static BpmBoundaryEventTypeEnum typeOf(Integer type) { return ArrayUtil.firstMatch(eventType -> eventType.getType().equals(type), values()); } diff --git a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmDelayTimerTypeEnum.java b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmDelayTimerTypeEnum.java new file mode 100644 index 000000000..69f3c19bc --- /dev/null +++ b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmDelayTimerTypeEnum.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.bpm.enums.definition; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * BPM 延迟器类型枚举 + * + * @author Lesan + */ +@Getter +@AllArgsConstructor +public enum BpmDelayTimerTypeEnum implements ArrayValuable { + + FIXED_TIME_DURATION(1, "固定时长"), + FIXED_DATE_TIME(2, "固定日期"); + + private final Integer type; + private final String name; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(BpmDelayTimerTypeEnum::getType).toArray(Integer[]::new); + + @Override + public Integer[] array() { + return ARRAYS; + } + +} \ 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/definition/BpmHttpRequestParamTypeEnum.java b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmHttpRequestParamTypeEnum.java new file mode 100644 index 000000000..e072ba51d --- /dev/null +++ b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmHttpRequestParamTypeEnum.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.bpm.enums.definition; + +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * BPM HTTP 请求参数设置类型。用于 Simple 设计器任务监听器和触发器配置。 + * + * @author Lesan + */ +@Getter +@AllArgsConstructor +public enum BpmHttpRequestParamTypeEnum implements ArrayValuable { + + FIXED_VALUE(1, "固定值"), + FROM_FORM(2, "表单"); + + private final Integer type; + private final String name; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(BpmHttpRequestParamTypeEnum::getType).toArray(Integer[]::new); + + @Override + public Integer[] array() { + return ARRAYS; + } + +} \ 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/definition/BpmProcessListenerType.java b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmProcessListenerTypeEnum.java similarity index 89% rename from yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmProcessListenerType.java rename to yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmProcessListenerTypeEnum.java index 3dde5dfb5..e4fed4215 100644 --- a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmProcessListenerType.java +++ b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmProcessListenerTypeEnum.java @@ -10,7 +10,7 @@ import lombok.Getter; */ @Getter @AllArgsConstructor -public enum BpmProcessListenerType { +public enum BpmProcessListenerTypeEnum { EXECUTION("execution", "执行监听器"), TASK("task", "任务执行器"); diff --git a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmProcessListenerValueType.java b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmProcessListenerValueTypeEnum.java similarity index 90% rename from yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmProcessListenerValueType.java rename to yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmProcessListenerValueTypeEnum.java index 63e23af23..4e57f10bf 100644 --- a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmProcessListenerValueType.java +++ b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmProcessListenerValueTypeEnum.java @@ -10,7 +10,7 @@ import lombok.Getter; */ @Getter @AllArgsConstructor -public enum BpmProcessListenerValueType { +public enum BpmProcessListenerValueTypeEnum { CLASS("class", "Java 类"), DELEGATE_EXPRESSION("delegateExpression", "代理表达式"), diff --git a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmSimpleModeConditionType.java b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmSimpleModeConditionTypeEnum.java similarity index 76% rename from yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmSimpleModeConditionType.java rename to yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmSimpleModeConditionTypeEnum.java index 1a3fedce8..d886c37f7 100644 --- a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmSimpleModeConditionType.java +++ b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmSimpleModeConditionTypeEnum.java @@ -14,18 +14,18 @@ import java.util.Arrays; */ @Getter @AllArgsConstructor -public enum BpmSimpleModeConditionType implements ArrayValuable { +public enum BpmSimpleModeConditionTypeEnum implements ArrayValuable { EXPRESSION(1, "条件表达式"), RULE(2, "条件规则"); - public static final Integer[] ARRAYS = Arrays.stream(values()).map(BpmSimpleModeConditionType::getType).toArray(Integer[]::new); + public static final Integer[] ARRAYS = Arrays.stream(values()).map(BpmSimpleModeConditionTypeEnum::getType).toArray(Integer[]::new); private final Integer type; private final String name; - public static BpmSimpleModeConditionType valueOf(Integer type) { + public static BpmSimpleModeConditionTypeEnum 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/BpmSimpleModelNodeType.java b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmSimpleModelNodeTypeEnum.java similarity index 79% rename from yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmSimpleModelNodeType.java rename to yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmSimpleModelNodeTypeEnum.java index 5c0b5bf1f..238a9347c 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/BpmSimpleModelNodeTypeEnum.java @@ -15,7 +15,7 @@ import java.util.Objects; */ @Getter @AllArgsConstructor -public enum BpmSimpleModelNodeType implements ArrayValuable { +public enum BpmSimpleModelNodeTypeEnum implements ArrayValuable { // 0 ~ 1 开始和结束 START_NODE(0, "开始", "startEvent"), @@ -26,14 +26,18 @@ public enum BpmSimpleModelNodeType implements ArrayValuable { APPROVE_NODE(11, "审批人", "userTask"), COPY_NODE(12, "抄送人", "serviceTask"), + DELAY_TIMER_NODE(14, "延迟器", "receiveTask"), + TRIGGER_NODE(15, "触发器", "serviceTask"), + // 50 ~ 条件分支 CONDITION_NODE(50, "条件", "sequenceFlow"), // 用于构建流转条件的表达式 CONDITION_BRANCH_NODE(51, "条件分支", "exclusiveGateway"), PARALLEL_BRANCH_NODE(52, "并行分支", "parallelGateway"), INCLUSIVE_BRANCH_NODE(53, "包容分支", "inclusiveGateway"), + ROUTER_BRANCH_NODE(54, "路由分支", "exclusiveGateway") ; - public static final Integer[] ARRAYS = Arrays.stream(values()).map(BpmSimpleModelNodeType::getType).toArray(Integer[]::new); + public static final Integer[] ARRAYS = Arrays.stream(values()).map(BpmSimpleModelNodeTypeEnum::getType).toArray(Integer[]::new); private final Integer type; private final String name; @@ -47,10 +51,11 @@ public enum BpmSimpleModelNodeType implements ArrayValuable { public static boolean isBranchNode(Integer type) { return Objects.equals(CONDITION_BRANCH_NODE.getType(), type) || Objects.equals(PARALLEL_BRANCH_NODE.getType(), type) - || Objects.equals(INCLUSIVE_BRANCH_NODE.getType(), type); + || Objects.equals(INCLUSIVE_BRANCH_NODE.getType(), type) + || Objects.equals(ROUTER_BRANCH_NODE.getType(), type); } - public static BpmSimpleModelNodeType valueOf(Integer type) { + public static BpmSimpleModelNodeTypeEnum 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/BpmTriggerTypeEnum.java b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmTriggerTypeEnum.java new file mode 100644 index 000000000..66f34e655 --- /dev/null +++ b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmTriggerTypeEnum.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.bpm.enums.definition; + +import cn.hutool.core.util.ArrayUtil; +import cn.iocoder.yudao.framework.common.core.ArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * BPM Simple 触发器类型枚举 + * + * @author jason + */ +@Getter +@AllArgsConstructor +public enum BpmTriggerTypeEnum implements ArrayValuable { + + HTTP_REQUEST(1, "发起 HTTP 请求"); + + /** + * 触发器执行动作类型 + */ + private final Integer type; + + /** + * 触发器执行动作描述 + */ + private final String desc; + + public static final Integer[] ARRAYS = Arrays.stream(values()).map(BpmTriggerTypeEnum::getType).toArray(Integer[]::new); + + @Override + public Integer[] array() { + return ARRAYS; + } + + public static BpmTriggerTypeEnum typeOf(Integer type) { + return ArrayUtil.firstMatch(item -> item.getType().equals(type), values()); + } + +} diff --git a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmUserTaskRejectHandlerType.java b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmUserTaskRejectHandlerTypeEnum.java similarity index 76% rename from yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmUserTaskRejectHandlerType.java rename to yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmUserTaskRejectHandlerTypeEnum.java index adf2fd08d..6c9478916 100644 --- a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmUserTaskRejectHandlerType.java +++ b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmUserTaskRejectHandlerTypeEnum.java @@ -14,7 +14,7 @@ import java.util.Arrays; */ @Getter @AllArgsConstructor -public enum BpmUserTaskRejectHandlerType implements ArrayValuable { +public enum BpmUserTaskRejectHandlerTypeEnum implements ArrayValuable { FINISH_PROCESS_INSTANCE(1, "终止流程"), RETURN_USER_TASK(2, "驳回到指定任务节点"); @@ -22,9 +22,9 @@ public enum BpmUserTaskRejectHandlerType implements ArrayValuable { private final Integer type; private final String name; - public static final Integer[] ARRAYS = Arrays.stream(values()).map(BpmUserTaskRejectHandlerType::getType).toArray(Integer[]::new); + public static final Integer[] ARRAYS = Arrays.stream(values()).map(BpmUserTaskRejectHandlerTypeEnum::getType).toArray(Integer[]::new); - public static BpmUserTaskRejectHandlerType typeOf(Integer type) { + public static BpmUserTaskRejectHandlerTypeEnum typeOf(Integer type) { return ArrayUtil.firstMatch(item -> item.getType().equals(type), values()); } 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 c63b0f5a2..6483ee0e3 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 @@ -161,6 +161,15 @@ public class BpmModelController { return success(true); } + @DeleteMapping("/clean") + @Operation(summary = "清理模型") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('bpm:model:clean')") + public CommonResult cleanModel(@RequestParam("id") String id) { + modelService.cleanModel(getLoginUserId(), id); + return success(true); + } + // ========== 仿钉钉/飞书的精简模型 ========= @GetMapping("/simple/get") diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/form/BpmFormFieldVO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/form/BpmFormFieldVO.java new file mode 100644 index 000000000..65fb305d3 --- /dev/null +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/form/BpmFormFieldVO.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.form; + +import lombok.Data; + +/** + * 流程表单字段 VO + */ +@Data +public class BpmFormFieldVO { + + /** + * 字段类型 + */ + private String type; + /** + * 字段标识 + */ + private String field; + /** + * 字段标题 + */ + private String title; + +} 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 f0676b251..2ff18e1b5 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 @@ -1,9 +1,11 @@ package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model; import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmAutoApproveTypeEnum; import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum; import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import lombok.Data; @@ -62,4 +64,72 @@ public class BpmModelMetaInfoVO { @Schema(description = "排序", example = "1") private Long sort; // 创建时,后端自动生成 + @Schema(description = "允许撤销审批中的申请", example = "true") + private Boolean allowCancelRunningProcess; + + @Schema(description = "流程 ID 规则", example = "{}") + private ProcessIdRule processIdRule; + + @Schema(description = "自动去重类型", example = "1") + @InEnum(BpmAutoApproveTypeEnum.class) + private Integer autoApprovalType; + + @Schema(description = "标题设置", example = "{}") + private TitleSetting titleSetting; + + @Schema(description = "摘要设置", example = "{}") + private SummarySetting summarySetting; + + @Schema(description = "流程 ID 规则") + @Data + @Valid + public static class ProcessIdRule { + + @Schema(description = "是否启用", example = "false") + @NotNull(message = "是否启用不能为空") + private Boolean enable; + + @Schema(description = "前缀", example = "XX") + private String prefix; + + @Schema(description = "中缀", example = "20250120") + private String infix; // 精确到日、精确到时、精确到分、精确到秒 + + @Schema(description = "后缀", example = "YY") + private String postfix; + + @Schema(description = "序列长度", example = "5") + @NotNull(message = "序列长度不能为空") + private Integer length; + + } + + @Schema(description = "标题设置") + @Data + @Valid + public static class TitleSetting { + + @Schema(description = "是否自定义", example = "false") + @NotNull(message = "是否自定义不能为空") + private Boolean enable; + + @Schema(description = "标题", example = "流程标题") + private String title; + + } + + @Schema(description = "摘要设置") + @Data + @Valid + public static class SummarySetting { + + @Schema(description = "是否自定义", example = "false") + @NotNull(message = "是否自定义不能为空") + private Boolean enable; + + @Schema(description = "摘要字段数组", example = "[]") + private List summary; + + } + } 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 fca7cd6d6..d3890adf1 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 @@ -9,6 +9,7 @@ import jakarta.validation.Valid; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import lombok.Data; +import org.hibernate.validator.constraints.URL; import java.util.List; import java.util.Map; @@ -24,7 +25,7 @@ public class BpmSimpleModelNodeVO { @Schema(description = "模型节点类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "模型节点类型不能为空") - @InEnum(BpmSimpleModelNodeType.class) + @InEnum(BpmSimpleModelNodeTypeEnum.class) private Integer type; @Schema(description = "模型节点名称", example = "领导审批") @@ -36,23 +37,6 @@ public class BpmSimpleModelNodeVO { @Schema(description = "子节点") private BpmSimpleModelNodeVO childNode; // 补充说明:在该模型下,子节点有且仅有一个,不会有多个 - @Schema(description = "条件节点") - private List conditionNodes; // 补充说明:有且仅有条件、并行、包容等分支会使用 - - @Schema(description = "条件类型", example = "1") - @InEnum(BpmSimpleModeConditionType.class) - private Integer conditionType; // 仅用于条件节点 BpmSimpleModelNodeType.CONDITION_NODE - - @Schema(description = "条件表达式", example = "${day>3}") - private String conditionExpression; // 仅用于条件节点 BpmSimpleModelNodeType.CONDITION_NODE - - @Schema(description = "是否默认条件", example = "true") - private Boolean defaultFlow; // 仅用于条件节点 BpmSimpleModelNodeType.CONDITION_NODE - /** - * 条件组 - */ - private ConditionGroups conditionGroups; // 仅用于条件节点 BpmSimpleModelNodeType.CONDITION_NODE - @Schema(description = "候选人策略", example = "30") @InEnum(BpmTaskCandidateStrategyEnum.class) private Integer candidateStrategy; // 用于审批,抄送节点 @@ -77,6 +61,12 @@ public class BpmSimpleModelNodeVO { @Schema(description = "操作按钮设置", example = "[]") private List buttonsSetting; // 用于审批节点 + @Schema(description = "是否需要签名", example = "false") + private Boolean signEnable; + + @Schema(description = "是否填写审批意见", example = "false") + private Boolean reasonRequire; + /** * 审批节点拒绝处理 */ @@ -96,12 +86,85 @@ public class BpmSimpleModelNodeVO { */ private AssignEmptyHandler assignEmptyHandler; + /** + * 创建任务监听器 + */ + private ListenerHandler taskCreateListener; + /** + * 指派任务监听器 + */ + private ListenerHandler taskAssignListener; + /** + * 完成任务监听器 + */ + private ListenerHandler taskCompleteListener; + + @Schema(description = "延迟器设置", example = "{}") + private DelaySetting delaySetting; + + @Schema(description = "条件节点") + private List conditionNodes; // 补充说明:有且仅有条件、并行、包容分支会使用 + + /** + * 条件节点设置 + */ + private ConditionSetting conditionSetting; // 仅用于条件节点 BpmSimpleModelNodeType.CONDITION_NODE + + @Schema(description = "路由分支组", example = "[]") + private List routerGroups; + + @Schema(description = "路由分支默认分支 ID", example = "Flow_xxx", hidden = true) // 由后端生成,所以 hidden = true + private String routerDefaultFlowId; // 仅用于路由分支节点 BpmSimpleModelNodeType.ROUTER_BRANCH_NODE + + /** + * 触发器节点设置 + */ + private TriggerSetting triggerSetting; + + @Schema(description = "任务监听器") + @Valid + @Data + public static class ListenerHandler { + + @Schema(description = "是否开启任务监听器", example = "false") + @NotNull(message = "是否开启任务监听器不能为空") + private Boolean enable; + + @Schema(description = "请求路径", example = "http://xxxxx") + private String path; + + @Schema(description = "请求头", example = "[]") + private List header; + + @Schema(description = "请求体", example = "[]") + private List body; + + } + + @Schema(description = "HTTP 请求参数设置") + @Data + public static class HttpRequestParam { + + @Schema(description = "值类型", example = "1") + @InEnum(BpmHttpRequestParamTypeEnum.class) + @NotNull(message = "值类型不能为空") + private Integer type; + + @Schema(description = "键", example = "xxx") + @NotEmpty(message = "键不能为空") + private String key; + + @Schema(description = "值", example = "xxx") + @NotEmpty(message = "值不能为空") + private String value; + } + @Schema(description = "审批节点拒绝处理策略") @Data public static class RejectHandler { @Schema(description = "拒绝处理类型", example = "1") - @InEnum(BpmUserTaskRejectHandlerType.class) + @InEnum(BpmUserTaskRejectHandlerTypeEnum.class) private Integer type; @Schema(description = "任务拒绝后驳回的节点 Id", example = "Activity_1") @@ -128,7 +191,6 @@ public class BpmSimpleModelNodeVO { @Schema(description = "最大提醒次数", example = "1") private Integer maxRemindCount; - } @Schema(description = "空处理策略") @@ -143,7 +205,6 @@ public class BpmSimpleModelNodeVO { @Schema(description = "指定人员审批的用户编号数组", example = "1") private List userIds; - } @Schema(description = "操作按钮设置") @@ -162,6 +223,28 @@ public class BpmSimpleModelNodeVO { private Boolean enable; } + @Schema(description = "条件设置") + @Data + @Valid + // 仅用于条件节点 BpmSimpleModelNodeType.CONDITION_NODE + public static class ConditionSetting { + + @Schema(description = "条件类型", example = "1") + @InEnum(BpmSimpleModeConditionTypeEnum.class) + private Integer conditionType; + + @Schema(description = "条件表达式", example = "${day>3}") + private String conditionExpression; + + @Schema(description = "是否默认条件", example = "true") + private Boolean defaultFlow; + + /** + * 条件组 + */ + private ConditionGroups conditionGroups; + } + @Schema(description = "条件组") @Data @Valid @@ -208,5 +291,75 @@ public class BpmSimpleModelNodeVO { private String rightSide; } - // TODO @芋艿:条件;建议可以固化的一些选项;然后有个表达式兜底;要支持 + @Schema(description = "延迟器") + @Data + @Valid + public static class DelaySetting { + + @Schema(description = "延迟时间类型", example = "1") + @NotNull(message = "延迟时间类型不能为空") + @InEnum(BpmDelayTimerTypeEnum.class) + private Integer delayType; + + @Schema(description = "延迟时间表达式", example = "PT1H,2025-01-01T00:00:00") + @NotEmpty(message = "延迟时间表达式不能为空") + private String delayTime; + } + + @Schema(description = "路由分支") + @Data + @Valid + public static class RouterSetting { + + @Schema(description = "节点 Id", example = "Activity_xxx") // 跳转到该节点 + @NotEmpty(message = "节点 Id 不能为空") + private String nodeId; + + @Schema(description = "条件类型", example = "1") + @InEnum(BpmSimpleModeConditionTypeEnum.class) + @NotNull(message = "条件类型不能为空") + private Integer conditionType; + + @Schema(description = "条件表达式", example = "${day>3}") + private String conditionExpression; + + @Schema(description = "条件组", example = "{}") + private ConditionGroups conditionGroups; + } + + @Schema(description = "触发器节点配置") + @Data + @Valid + public static class TriggerSetting { + + @Schema(description = "触发器类型", example = "1") + @InEnum(BpmTriggerTypeEnum.class) + @NotNull(message = "触发器类型不能为空") + private Integer type; + + /** + * http 请求触发器设置 + */ + @Valid + private HttpRequestTriggerSetting httpRequestSetting; + + @Schema(description = "http 请求触发器设置", example = "{}") + @Data + public static class HttpRequestTriggerSetting { + + @Schema(description = "请求路径", example = "http://127.0.0.1") + @NotEmpty(message = "请求 URL 不能为空") + @URL(message = "请求 URL 格式不正确") + private String url; + + @Schema(description = "请求头参数设置", example = "[]") + @Valid + private List header; + + @Schema(description = "请求头参数设置", example = "[]") + @Valid + private List body; + } + + } } 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 401cbc90e..219cc9de4 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 @@ -74,8 +74,10 @@ public class BpmProcessInstanceController { convertSet(pageResult.getList(), HistoricProcessInstance::getProcessDefinitionId)); Map categoryMap = categoryService.getCategoryMap( convertSet(processDefinitionMap.values(), ProcessDefinition::getCategory)); + Map processDefinitionInfoMap = processDefinitionService.getProcessDefinitionInfoMap( + convertSet(pageResult.getList(), HistoricProcessInstance::getProcessDefinitionId)); return success(BpmProcessInstanceConvert.INSTANCE.buildProcessInstancePage(pageResult, - processDefinitionMap, categoryMap, taskMap, null, null)); + processDefinitionMap, categoryMap, taskMap, null, null, processDefinitionInfoMap)); } @GetMapping("/manager-page") @@ -101,8 +103,10 @@ public class BpmProcessInstanceController { convertSet(pageResult.getList(), processInstance -> NumberUtils.parseLong(processInstance.getStartUserId()))); Map deptMap = deptApi.getDeptMap( convertSet(userMap.values(), AdminUserRespDTO::getDeptId)); + Map processDefinitionInfoMap = processDefinitionService.getProcessDefinitionInfoMap( + convertSet(pageResult.getList(), HistoricProcessInstance::getProcessDefinitionId)); return success(BpmProcessInstanceConvert.INSTANCE.buildProcessInstancePage(pageResult, - processDefinitionMap, categoryMap, taskMap, userMap, deptMap)); + processDefinitionMap, categoryMap, taskMap, userMap, deptMap, processDefinitionInfoMap)); } @PostMapping("/create") 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 3a017a627..b796c5c17 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 @@ -7,7 +7,9 @@ import cn.iocoder.yudao.framework.common.util.number.NumberUtils; 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.dal.dataobject.definition.BpmProcessDefinitionInfoDO; 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; @@ -50,6 +52,8 @@ public class BpmTaskController { private BpmProcessInstanceService processInstanceService; @Resource private BpmFormService formService; + @Resource + private BpmProcessDefinitionService processDefinitionService; @Resource private AdminUserApi adminUserApi; @@ -70,7 +74,9 @@ public class BpmTaskController { convertSet(pageResult.getList(), Task::getProcessInstanceId)); Map userMap = adminUserApi.getUserMap( convertSet(processInstanceMap.values(), instance -> Long.valueOf(instance.getStartUserId()))); - return success(BpmTaskConvert.INSTANCE.buildTodoTaskPage(pageResult, processInstanceMap, userMap)); + Map processDefinitionInfoMap = processDefinitionService.getProcessDefinitionInfoMap( + convertSet(pageResult.getList(), Task::getProcessDefinitionId)); + return success(BpmTaskConvert.INSTANCE.buildTodoTaskPage(pageResult, processInstanceMap, userMap, processDefinitionInfoMap)); } @GetMapping("done-page") @@ -87,7 +93,9 @@ public class BpmTaskController { convertSet(pageResult.getList(), HistoricTaskInstance::getProcessInstanceId)); Map userMap = adminUserApi.getUserMap( convertSet(processInstanceMap.values(), instance -> Long.valueOf(instance.getStartUserId()))); - return success(BpmTaskConvert.INSTANCE.buildTaskPage(pageResult, processInstanceMap, userMap, null)); + Map processDefinitionInfoMap = processDefinitionService.getProcessDefinitionInfoMap( + convertSet(pageResult.getList(), HistoricTaskInstance::getProcessDefinitionId)); + return success(BpmTaskConvert.INSTANCE.buildTaskPage(pageResult, processInstanceMap, userMap, null, processDefinitionInfoMap)); } @GetMapping("manager-page") @@ -108,7 +116,9 @@ public class BpmTaskController { Map userMap = adminUserApi.getUserMap(userIds); Map deptMap = deptApi.getDeptMap( convertSet(userMap.values(), AdminUserRespDTO::getDeptId)); - return success(BpmTaskConvert.INSTANCE.buildTaskPage(pageResult, processInstanceMap, userMap, deptMap)); + Map processDefinitionInfoMap = processDefinitionService.getProcessDefinitionInfoMap( + convertSet(pageResult.getList(), HistoricTaskInstance::getProcessDefinitionId)); + return success(BpmTaskConvert.INSTANCE.buildTaskPage(pageResult, processInstanceMap, userMap, deptMap, processDefinitionInfoMap)); } @GetMapping("/list-by-process-instance-id") 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 148175d93..888f59ada 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 @@ -101,6 +101,9 @@ public class BpmApprovalDetailRespVO { @Schema(description = "审批意见", example = "同意") private String reason; + @Schema(description = "签名", example = "https://www.iocoder.cn/sign.png") + private String signPicUrl; + } } 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 2de0cbc95..76dca606a 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.framework.common.core.KeyValue; 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; @@ -19,6 +20,9 @@ public class BpmProcessInstanceRespVO { @Schema(description = "流程名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") private String name; + @Schema(description = "流程摘要") + private List> summary; // 只有流程表单,才有摘要! + @Schema(description = "流程分类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private String category; @Schema(description = "流程分类名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "请假") 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 5bec02660..40df86efc 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 @@ -14,10 +14,12 @@ public class BpmTaskApproveReqVO { @NotEmpty(message = "任务编号不能为空") private String id; - @Schema(description = "审批意见", requiredMode = Schema.RequiredMode.REQUIRED, example = "不错不错!") - @NotEmpty(message = "审批意见不能为空") + @Schema(description = "审批意见", example = "不错不错!") private String reason; + @Schema(description = "签名", example = "https://www.iocoder.cn/sign.png") + private String signPicUrl; + @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/BpmTaskRejectReqVO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskRejectReqVO.java index 3d5abc124..0b8d7d8fa 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskRejectReqVO.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskRejectReqVO.java @@ -13,7 +13,6 @@ public class BpmTaskRejectReqVO { private String id; @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/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 5b34d36db..f31c1bdcc 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,5 +1,6 @@ package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task; +import cn.iocoder.yudao.framework.common.core.KeyValue; 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; @@ -78,6 +79,16 @@ public class BpmTaskRespVO { @Schema(description = "操作按钮设置值") private Map buttonsSetting; + @Schema(description = "是否需要签名", example = "false") + private Boolean signEnable; + + @Schema(description = "是否填写审批意见", example = "false") + private Boolean reasonRequire; + + // TODO @lesan:要不放到 processInstance 里面?因为摘要是流程实例的,不是流程任务的 + @Schema(description = "流程摘要", example = "[]") + private List> summary; // 只有流程表单,才有摘要! + @Data @Schema(description = "流程实例") public static class ProcessInstance { 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 450699c2f..dfe23ce80 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 @@ -58,7 +58,8 @@ public interface BpmProcessInstanceConvert { Map categoryMap, Map> taskMap, Map userMap, - Map deptMap) { + Map deptMap, + Map processDefinitionInfoMap) { PageResult vpPageResult = BeanUtils.toBean(pageResult, BpmProcessInstanceRespVO.class); for (int i = 0; i < pageResult.getList().size(); i++) { BpmProcessInstanceRespVO respVO = vpPageResult.getList().get(i); @@ -76,6 +77,9 @@ public interface BpmProcessInstanceConvert { MapUtils.findAndThen(deptMap, startUser.getDeptId(), dept -> respVO.getStartUser().setDeptName(dept.getName())); } } + // 摘要 + respVO.setSummary(FlowableUtils.getSummary(processDefinitionInfoMap.get(respVO.getProcessDefinitionId()), + pageResult.getList().get(i).getProcessVariables())); } return vpPageResult; } @@ -186,7 +190,8 @@ public interface BpmProcessInstanceConvert { return null; } return BeanUtils.toBean(task, BpmApprovalDetailRespVO.ActivityNodeTask.class) - .setStatus(FlowableUtils.getTaskStatus(task)).setReason(FlowableUtils.getTaskReason(task)); + .setStatus(FlowableUtils.getTaskStatus(task)).setReason(FlowableUtils.getTaskReason(task)) + .setSignPicUrl(FlowableUtils.getTaskSignPicUrl(task)); } default Set parseUserIds(HistoricProcessInstance processInstance, 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 37c7218e0..2faa8841a 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 @@ -9,6 +9,7 @@ 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.task.BpmTaskRespVO; 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.enums.task.BpmTaskStatusEnum; import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils; import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenTaskCreatedReqDTO; @@ -41,7 +42,8 @@ public interface BpmTaskConvert { default PageResult buildTodoTaskPage(PageResult pageResult, Map processInstanceMap, - Map userMap) { + Map userMap, + Map processDefinitionInfoMap) { return BeanUtils.toBean(pageResult, BpmTaskRespVO.class, taskVO -> { ProcessInstance processInstance = processInstanceMap.get(taskVO.getProcessInstanceId()); if (processInstance == null) { @@ -50,13 +52,17 @@ 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, UserSimpleBaseVO.class)); + // 摘要 + taskVO.setSummary(FlowableUtils.getSummary(processDefinitionInfoMap.get(processInstance.getProcessDefinitionId()), + processInstance.getProcessVariables())); }); } default PageResult buildTaskPage(PageResult pageResult, Map processInstanceMap, Map userMap, - Map deptMap) { + Map deptMap, + Map processDefinitionInfoMap) { List taskVOList = CollectionUtils.convertList(pageResult.getList(), task -> { BpmTaskRespVO taskVO = BeanUtils.toBean(task, BpmTaskRespVO.class); taskVO.setStatus(FlowableUtils.getTaskStatus(task)).setReason(FlowableUtils.getTaskReason(task)); @@ -72,6 +78,9 @@ public interface BpmTaskConvert { AdminUserRespDTO startUser = userMap.get(NumberUtils.parseLong(processInstance.getStartUserId())); taskVO.setProcessInstance(BeanUtils.toBean(processInstance, BpmTaskRespVO.ProcessInstance.class)); taskVO.getProcessInstance().setStartUser(BeanUtils.toBean(startUser, UserSimpleBaseVO.class)); + // 摘要 + taskVO.setSummary(FlowableUtils.getSummary(processDefinitionInfoMap.get(processInstance.getProcessDefinitionId()), + processInstance.getProcessVariables())); } return taskVO; }); 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 7248b31cd..ccab7b043 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 @@ -3,6 +3,8 @@ 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.controller.admin.definition.vo.model.BpmModelMetaInfoVO; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmAutoApproveTypeEnum; import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum; import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum; import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; @@ -150,4 +152,34 @@ public class BpmProcessDefinitionInfoDO extends BaseDO { @TableField(typeHandler = StringListTypeHandler.class) // 为了可以使用 find_in_set 进行过滤 private List managerUserIds; + /** + * 是否允许撤销审批中的申请 + */ + private Boolean allowCancelRunningProcess; + + /** + * 流程 ID 规则 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private BpmModelMetaInfoVO.ProcessIdRule processIdRule; + + /** + * 自动去重类型 + * + * 枚举 {@link BpmAutoApproveTypeEnum} + */ + private Integer autoApprovalType; + + /** + * 标题设置 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private BpmModelMetaInfoVO.TitleSetting titleSetting; + + /** + * 摘要设置 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private BpmModelMetaInfoVO.SummarySetting summarySetting; + } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessListenerDO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessListenerDO.java index 08ecebe11..4a04c4666 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessListenerDO.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessListenerDO.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.module.bpm.enums.definition.BpmProcessListenerTypeEnum; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; @@ -42,7 +43,7 @@ public class BpmProcessListenerDO extends BaseDO { /** * 监听类型 * - * 枚举 {@link cn.iocoder.yudao.module.bpm.enums.definition.BpmProcessListenerType} + * 枚举 {@link BpmProcessListenerTypeEnum} * * 1. execution:ExecutionListener 执行监听器 * 2. task:TaskListener 任务监听器 diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/task/BpmProcessInstanceCopyMapper.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/task/BpmProcessInstanceCopyMapper.java index 8f23024e8..fbb6ddeab 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/task/BpmProcessInstanceCopyMapper.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/task/BpmProcessInstanceCopyMapper.java @@ -7,8 +7,6 @@ import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessI import cn.iocoder.yudao.module.bpm.dal.dataobject.task.BpmProcessInstanceCopyDO; import org.apache.ibatis.annotations.Mapper; -import java.util.List; - @Mapper public interface BpmProcessInstanceCopyMapper extends BaseMapperX { @@ -20,9 +18,8 @@ public interface BpmProcessInstanceCopyMapper extends BaseMapperX selectListByProcessInstanceIdAndActivityId(String processInstanceId, String activityId) { - return selectList(BpmProcessInstanceCopyDO::getProcessInstanceId, processInstanceId, - BpmProcessInstanceCopyDO::getActivityId, activityId); + default void deleteByProcessInstanceId(String processInstanceId) { + delete(BpmProcessInstanceCopyDO::getProcessInstanceId, processInstanceId); } } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/redis/BpmProcessIdRedisDAO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/redis/BpmProcessIdRedisDAO.java new file mode 100644 index 000000000..53a76322f --- /dev/null +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/redis/BpmProcessIdRedisDAO.java @@ -0,0 +1,55 @@ +package cn.iocoder.yudao.module.bpm.dal.redis; + +import cn.hutool.core.date.DateUtil; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO; +import jakarta.annotation.Resource; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Repository; + +import java.time.Duration; +import java.time.LocalDateTime; + +/** + * BPM 流程 Id 编码的 Redis DAO + * + * @author Lesan + */ +@Repository +public class BpmProcessIdRedisDAO { + + @Resource + private StringRedisTemplate stringRedisTemplate; + + /** + * 生成序号,使用定义的 processIdRule 规则生成 + * + * @param processIdRule 规则 + * @return 序号 + */ + public String generate(BpmModelMetaInfoVO.ProcessIdRule processIdRule) { + // 生成日期前缀 + String infix = ""; + switch (processIdRule.getInfix()) { + case "DAY": + infix = DateUtil.format(LocalDateTime.now(), "yyyyMMDD"); + break; + case "HOUR": + infix = DateUtil.format(LocalDateTime.now(), "yyyyMMDDHH"); + break; + case "MINUTE": + infix = DateUtil.format(LocalDateTime.now(), "yyyyMMDDHHmm"); + break; + case "SECOND": + infix = DateUtil.format(LocalDateTime.now(), "yyyyMMDDHHmmss"); + break; + } + + // 生成序号 + String noPrefix = processIdRule.getPrefix() + infix + processIdRule.getPostfix(); + String key = RedisKeyConstants.BPM_PROCESS_ID + noPrefix; + Long no = stringRedisTemplate.opsForValue().increment(key); + stringRedisTemplate.expire(key, Duration.ofDays(1L)); + return noPrefix + String.format("%0" + processIdRule.getLength() + "d", no); + } + +} diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/redis/RedisKeyConstants.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/redis/RedisKeyConstants.java new file mode 100644 index 000000000..304cda3d8 --- /dev/null +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/redis/RedisKeyConstants.java @@ -0,0 +1,15 @@ +package cn.iocoder.yudao.module.bpm.dal.redis; + +/** + * BPM Redis Key 枚举类 + * + * @author 芋道源码 + */ +public interface RedisKeyConstants { + + /** + * 流程 ID 的缓存 + */ + String BPM_PROCESS_ID = "bpm:process_id:"; + +} 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 30e675bf0..952f0f1be 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 @@ -12,6 +12,7 @@ import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskApproveTypeEnum; import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskAssignStartUserHandlerTypeEnum; 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 cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; @@ -91,35 +92,39 @@ public class BpmTaskCandidateInvoker { */ @DataPermission(enable = false) // 忽略数据权限,避免因为过滤,导致找不到候选人 public Set calculateUsersByTask(DelegateExecution execution) { - // 审批类型非人工审核时,不进行计算候选人。原因是:后续会自动通过、不通过 - FlowElement flowElement = execution.getCurrentFlowElement(); - Integer approveType = BpmnModelUtils.parseApproveType(flowElement); - if (ObjectUtils.equalsAny(approveType, - BpmUserTaskApproveTypeEnum.AUTO_APPROVE.getType(), - BpmUserTaskApproveTypeEnum.AUTO_REJECT.getType())) { - return new HashSet<>(); - } + // 注意:解决极端情况下,Flowable 异步调用,导致租户 id 丢失的情况 + // 例如说,SIMPLE 延迟器在 trigger 的时候!!! + return FlowableUtils.execute(execution.getTenantId(), () -> { + // 审批类型非人工审核时,不进行计算候选人。原因是:后续会自动通过、不通过 + FlowElement flowElement = execution.getCurrentFlowElement(); + 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).calculateUsersByTask(execution, param); - // 1.2 移除被禁用的用户 - removeDisableUsers(userIds); + // 1.1 计算任务的候选人 + 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()) - .calculateUsersByTask(execution, param); - // ASSIGN_EMPTY 策略,不需要移除被禁用的用户。原因是,再移除,可能会出现更没审批人了!!! - } + // 2. 候选人为空时,根据“审批人为空”的配置补充 + if (CollUtil.isEmpty(userIds)) { + userIds = getCandidateStrategy(BpmTaskCandidateStrategyEnum.ASSIGN_EMPTY.getStrategy()) + .calculateUsersByTask(execution, param); + // ASSIGN_EMPTY 策略,不需要移除被禁用的用户。原因是,再移除,可能会出现更没审批人了!!! + } - // 3. 移除发起人的用户 - ProcessInstance processInstance = SpringUtil.getBean(BpmProcessInstanceService.class) - .getProcessInstance(execution.getProcessInstanceId()); - Assert.notNull(processInstance, "流程实例({}) 不存在", execution.getProcessInstanceId()); - removeStartUserIfSkip(userIds, flowElement, Long.valueOf(processInstance.getStartUserId())); - return userIds; + // 3. 移除发起人的用户 + 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, 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 60a864848..9dce8a2ea 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 @@ -100,6 +100,15 @@ public interface BpmnModelConstants { */ String BUTTON_SETTING_ELEMENT_ENABLE_ATTRIBUTE = "enable"; + /** + * BPMN ExtensionElement 的扩展属性,用于标记触发器的类型 + */ + String TRIGGER_TYPE = "triggerType"; + /** + * BPMN ExtensionElement 的扩展属性,用于标记触发器参数 + */ + String TRIGGER_PARAM = "triggerParam"; + /** * BPMN Start Event Node Id */ @@ -110,4 +119,14 @@ public interface BpmnModelConstants { */ String START_USER_NODE_ID = "StartUserNode"; + /** + * 是否需要签名 + */ + String SIGN_ENABLE = "signEnable"; + + /** + * 审批意见是否必填 + */ + String REASON_REQUIRE = "reasonRequire"; + } 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 08fb5c48e..8172cf59a 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 @@ -43,6 +43,24 @@ public class BpmnVariableConstants { * @see ProcessInstance#getProcessVariables() */ public static final String PROCESS_INSTANCE_VARIABLE_RETURN_FLAG = "RETURN_FLAG_%s"; + /** + * 流程实例的变量 - 是否跳过表达式 + * + * @see ProcessInstance#getProcessVariables() + * @see Flowable/Activiti之SkipExpression 完成自动审批 + */ + public static final String PROCESS_INSTANCE_SKIP_EXPRESSION_ENABLED = "_FLOWABLE_SKIP_EXPRESSION_ENABLED"; + + /** + * 流程实例的变量 - 流程开始时间 + * + * 【非存储变量】用于部分需要 format 的场景,例如说:流程实例的自定义标题 + */ + public static final String PROCESS_START_TIME = "PROCESS_START_TIME"; + /** + * 流程实例的变量 - 流程定义名称 + */ + public static final String PROCESS_DEFINITION_NAME = "PROCESS_DEFINITION_NAME"; /** * 任务的变量 - 状态 @@ -58,5 +76,9 @@ public class BpmnVariableConstants { * @see org.flowable.task.api.Task#getTaskLocalVariables() */ public static final String TASK_VARIABLE_REASON = "TASK_REASON"; + /** + * 任务变量 - 签名图片 URL + */ + public static final String TASK_SIGN_PIC_URL = "TASK_SIGN_PIC_URL"; } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmProcessInstanceEventListener.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmProcessInstanceEventListener.java index a0ec3e40a..5251aea64 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmProcessInstanceEventListener.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmProcessInstanceEventListener.java @@ -6,6 +6,7 @@ import jakarta.annotation.Resource; import org.flowable.common.engine.api.delegate.event.FlowableEngineEntityEvent; import org.flowable.common.engine.api.delegate.event.FlowableEngineEventType; import org.flowable.engine.delegate.event.AbstractFlowableEngineEventListener; +import org.flowable.engine.delegate.event.FlowableCancelledEvent; import org.flowable.engine.runtime.ProcessInstance; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; @@ -22,6 +23,7 @@ public class BpmProcessInstanceEventListener extends AbstractFlowableEngineEvent public static final Set PROCESS_INSTANCE_EVENTS = ImmutableSet.builder() .add(FlowableEngineEventType.PROCESS_COMPLETED) + .add(FlowableEngineEventType.PROCESS_CANCELLED) .build(); @Resource @@ -37,4 +39,10 @@ public class BpmProcessInstanceEventListener extends AbstractFlowableEngineEvent processInstanceService.processProcessInstanceCompleted((ProcessInstance)event.getEntity()); } + @Override // 特殊情况:当跳转到 EndEvent 流程实例未结束, 会执行 deleteProcessInstance 方法 + protected void processCancelled(FlowableCancelledEvent event) { + ProcessInstance processInstance = processInstanceService.getProcessInstance(event.getProcessInstanceId()); + processInstanceService.processProcessInstanceCompleted(processInstance); + } + } 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 fc113936f..ecef8fdb4 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 @@ -4,7 +4,7 @@ import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.util.number.NumberUtils; -import cn.iocoder.yudao.module.bpm.enums.definition.BpmBoundaryEventType; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmBoundaryEventTypeEnum; 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; @@ -97,16 +97,20 @@ public class BpmTaskEventListener extends AbstractFlowableEngineEventListener { BoundaryEvent boundaryEvent = (BoundaryEvent) element; String boundaryEventType = BpmnModelUtils.parseBoundaryEventExtensionElement(boundaryEvent, BpmnModelConstants.BOUNDARY_EVENT_TYPE); - BpmBoundaryEventType bpmTimerBoundaryEventType = BpmBoundaryEventType.typeOf(NumberUtils.parseInt(boundaryEventType)); - if (ObjectUtil.notEqual(bpmTimerBoundaryEventType, BpmBoundaryEventType.USER_TASK_TIMEOUT)) { - return; - } + BpmBoundaryEventTypeEnum bpmTimerBoundaryEventType = BpmBoundaryEventTypeEnum.typeOf(NumberUtils.parseInt(boundaryEventType)); // 2. 处理超时 - String timeoutHandlerType = BpmnModelUtils.parseBoundaryEventExtensionElement(boundaryEvent, - BpmnModelConstants.USER_TASK_TIMEOUT_HANDLER_TYPE); - String taskKey = boundaryEvent.getAttachedToRefId(); - taskService.processTaskTimeout(event.getProcessInstanceId(), taskKey, NumberUtils.parseInt(timeoutHandlerType)); + if (ObjectUtil.equal(bpmTimerBoundaryEventType, BpmBoundaryEventTypeEnum.USER_TASK_TIMEOUT)) { + // 2.1 用户任务超时处理 + String timeoutHandlerType = BpmnModelUtils.parseBoundaryEventExtensionElement(boundaryEvent, + BpmnModelConstants.USER_TASK_TIMEOUT_HANDLER_TYPE); + String taskKey = boundaryEvent.getAttachedToRefId(); + taskService.processTaskTimeout(event.getProcessInstanceId(), taskKey, NumberUtils.parseInt(timeoutHandlerType)); + // 2.2 延迟器超时处理 + } else if (ObjectUtil.equal(bpmTimerBoundaryEventType, BpmBoundaryEventTypeEnum.DELAY_TIMER_TIMEOUT)) { + String taskKey = boundaryEvent.getAttachedToRefId(); + taskService.processDelayTimerTimeout(event.getProcessInstanceId(), taskKey); + } } } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmTriggerTaskDelegate.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmTriggerTaskDelegate.java new file mode 100644 index 000000000..1ee347dfa --- /dev/null +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmTriggerTaskDelegate.java @@ -0,0 +1,55 @@ +package cn.iocoder.yudao.module.bpm.framework.flowable.core.listener; + +import cn.iocoder.yudao.module.bpm.enums.definition.BpmTriggerTypeEnum; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils; +import cn.iocoder.yudao.module.bpm.service.task.trigger.BpmTrigger; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.flowable.bpmn.model.FlowElement; +import org.flowable.engine.delegate.DelegateExecution; +import org.flowable.engine.delegate.JavaDelegate; +import org.springframework.stereotype.Component; + +import java.util.EnumMap; +import java.util.List; + +import static cn.iocoder.yudao.module.bpm.framework.flowable.core.listener.BpmTriggerTaskDelegate.BEAN_NAME; + + +/** + * 处理触发器任务 {@link JavaDelegate} 的实现类 + *

+ * 目前只有 Simple 设计器【触发器节点】使用 + * + * @author jason + */ +@Component(BEAN_NAME) +@Slf4j +public class BpmTriggerTaskDelegate implements JavaDelegate { + + public static final String BEAN_NAME = "bpmTriggerTaskDelegate"; + + @Resource + private List triggers; + + private final EnumMap triggerMap = new EnumMap<>(BpmTriggerTypeEnum.class); + + @PostConstruct + private void init() { + triggers.forEach(trigger -> triggerMap.put(trigger.getType(), trigger)); + } + + @Override + public void execute(DelegateExecution execution) { + FlowElement flowElement = execution.getCurrentFlowElement(); + BpmTriggerTypeEnum bpmTriggerType = BpmnModelUtils.parserTriggerType(flowElement); + BpmTrigger bpmTrigger = triggerMap.get(bpmTriggerType); + if (bpmTrigger == null) { + log.error("[execute][FlowElement({}), {} 找不到匹配的触发器]", execution.getCurrentActivityId(), flowElement); + return; + } + bpmTrigger.execute(execution.getProcessInstanceId(), BpmnModelUtils.parserTriggerParam(flowElement)); + } + +} 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 01200298f..eae4eb988 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 @@ -1,19 +1,19 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.core.util; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.lang.Assert; 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.json.JsonUtils; 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.enums.definition.*; import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants; import com.google.common.collect.Maps; import lombok.extern.slf4j.Slf4j; @@ -22,6 +22,7 @@ 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 org.flowable.engine.impl.el.FixedValue; import java.util.*; @@ -170,9 +171,9 @@ public class BpmnModelUtils { * @param userTask 任务节点 * @return 任务拒绝处理类型 */ - public static BpmUserTaskRejectHandlerType parseRejectHandlerType(FlowElement userTask) { + public static BpmUserTaskRejectHandlerTypeEnum parseRejectHandlerType(FlowElement userTask) { Integer rejectHandlerType = NumberUtils.parseInt(parseExtensionElement(userTask, USER_TASK_REJECT_HANDLER_TYPE)); - return BpmUserTaskRejectHandlerType.typeOf(rejectHandlerType); + return BpmUserTaskRejectHandlerTypeEnum.typeOf(rejectHandlerType); } /** @@ -346,6 +347,62 @@ public class BpmnModelUtils { return Optional.ofNullable(extensionElement).map(ExtensionElement::getElementText).orElse(null); } + public static void addSignEnable(Boolean signEnable, FlowElement userTask) { + addExtensionElement(userTask, SIGN_ENABLE, + ObjUtil.isNotNull(signEnable) ? signEnable.toString() : Boolean.FALSE.toString()); + } + + public static Boolean parseSignEnable(BpmnModel bpmnModel, String flowElementId) { + FlowElement flowElement = getFlowElementById(bpmnModel, flowElementId); + if (flowElement == null) { + return false; + } + List extensionElements = flowElement.getExtensionElements().get(SIGN_ENABLE); + if (CollUtil.isEmpty(extensionElements)) { + return false; + } + return Convert.toBool(extensionElements.get(0).getElementText(), false); + } + + public static void addReasonRequire(Boolean reasonRequire, FlowElement userTask) { + addExtensionElement(userTask, REASON_REQUIRE, + ObjUtil.isNotNull(reasonRequire) ? reasonRequire.toString() : Boolean.FALSE.toString()); + } + + public static Boolean parseReasonRequire(BpmnModel bpmnModel, String flowElementId) { + FlowElement flowElement = getFlowElementById(bpmnModel, flowElementId); + if (flowElement == null) { + return false; + } + List extensionElements = flowElement.getExtensionElements().get(REASON_REQUIRE); + if (CollUtil.isEmpty(extensionElements)) { + return false; + } + return Convert.toBool(extensionElements.get(0).getElementText(), false); + } + + public static void addListenerConfig(FlowableListener flowableListener, BpmSimpleModelNodeVO.ListenerHandler handler) { + FieldExtension fieldExtension = new FieldExtension(); + fieldExtension.setFieldName("listenerConfig"); + fieldExtension.setStringValue(JsonUtils.toJsonString(handler)); + flowableListener.getFieldExtensions().add(fieldExtension); + } + + public static BpmSimpleModelNodeVO.ListenerHandler parseListenerConfig(FixedValue fixedValue) { + String expressionText = fixedValue.getExpressionText(); + Assert.notNull(expressionText, "监听器扩展字段({})不能为空", expressionText); + return JsonUtils.parseObject(expressionText, BpmSimpleModelNodeVO.ListenerHandler.class); + } + + public static BpmTriggerTypeEnum parserTriggerType(FlowElement flowElement) { + Integer triggerType = NumberUtils.parseInt(parseExtensionElement(flowElement, TRIGGER_TYPE)); + return BpmTriggerTypeEnum.typeOf(triggerType); + } + + public static String parserTriggerParam(FlowElement flowElement) { + return parseExtensionElement(flowElement, TRIGGER_PARAM); + } + // ========== BPM 简单查找相关的方法 ========== /** @@ -777,7 +834,7 @@ public class BpmnModelUtils { Object result = FlowableUtils.getExpressionValue(variables, express); return Boolean.TRUE.equals(result); } catch (FlowableException ex) { - log.error("[evalConditionExpress][条件表达式({}) 变量({}) 解析报错", express, variables, ex); + log.error("[evalConditionExpress][条件表达式({}) 变量({}) 解析报错]", express, variables, ex); return Boolean.FALSE; } } 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 3eead3329..e80bba502 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 @@ -2,9 +2,15 @@ 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.common.core.KeyValue; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.form.BpmFormFieldVO; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum; import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants; +import lombok.SneakyThrows; import org.flowable.common.engine.api.delegate.Expression; import org.flowable.common.engine.api.variable.VariableContainer; import org.flowable.common.engine.impl.el.ExpressionManager; @@ -18,10 +24,7 @@ import org.flowable.engine.impl.util.CommandContextUtil; import org.flowable.engine.runtime.ProcessInstance; import org.flowable.task.api.TaskInfo; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; +import java.util.*; import java.util.concurrent.Callable; /** @@ -67,6 +70,17 @@ public class FlowableUtils { } } + @SneakyThrows + public static V execute(String tenantIdStr, Callable callable) { + if (ObjectUtil.isEmpty(tenantIdStr) + || Objects.equals(tenantIdStr, ProcessEngineConfiguration.NO_TENANT_ID)) { + return callable.call(); + } else { + Long tenantId = Long.valueOf(tenantIdStr); + return TenantUtils.execute(tenantId, callable); + } + } + // ========== Execution 相关的工具方法 ========== /** @@ -179,6 +193,68 @@ public class FlowableUtils { BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES); } + // TODO @lesan:如果值是 null 的情况,可能要调研下飞书、钉钉,是不是不返回哈! + /** + * 获得流程实例的摘要 + * + * 仅有 {@link BpmModelFormTypeEnum#getType()} 表单,才有摘要。 + * 原因是,只有它才有表单项的配置,从而可以根据配置,展示摘要。 + * + * @param processDefinitionInfo 流程定义 + * @param processVariables 流程实例的 variables + * @return 摘要 + */ + public static List> getSummary(BpmProcessDefinitionInfoDO processDefinitionInfo, + Map processVariables) { + // TODO @lesan:建议 if return,减少 { 层级 + if (ObjectUtil.isNotNull(processDefinitionInfo) + && BpmModelFormTypeEnum.NORMAL.getType().equals(processDefinitionInfo.getFormType())) { + List> summaryList = new ArrayList<>(); + // TODO @lesan:可以使用 CollUtils.convertMap 简化工作量哈。 + Map formFieldsMap = new HashMap<>(); + processDefinitionInfo.getFormFields().forEach(formFieldStr -> { + BpmFormFieldVO formField = JsonUtils.parseObject(formFieldStr, BpmFormFieldVO.class); + if (formField != null) { + formFieldsMap.put(formField.getField(), formField); + } + }); + + // TODO @lesan:这里也可以 if return,还是为了减少括号哈。这样,就可以写注释,情况一:;情况二: + if (ObjectUtil.isNotNull(processDefinitionInfo.getSummarySetting()) + && Boolean.TRUE.equals(processDefinitionInfo.getSummarySetting().getEnable())) { + // TODO @lesan:这里,也可以通过 CollUtils.convertList 简化哈。 + for (String item : processDefinitionInfo.getSummarySetting().getSummary()) { + BpmFormFieldVO formField = formFieldsMap.get(item); + if (formField != null) { + summaryList.add(new KeyValue<>(formField.getTitle(), + processVariables.getOrDefault(item, "").toString())); + } + } + } else { + // 默认展示前三个 + /* TODO @lesan:stream 简化 + * summaryList.addAll(formFieldsMap.entrySet().stream() + * .limit(3) + * .map(entry -> new KeyValue<>(entry.getValue().getTitle(), + * processVariables.getOrDefault(entry.getValue().getField(), "").toString())) + * .collect(Collectors.toList())); + */ + int j = 0; + for (Map.Entry entry : formFieldsMap.entrySet()) { + BpmFormFieldVO formField = entry.getValue(); + if (j > 2) { + break; + } + summaryList.add(new KeyValue<>(formField.getTitle(), + processVariables.getOrDefault(formField.getField(), "").toString())); + j++; + } + } + return summaryList; + } + return null; + } + // ========== Task 相关的工具方法 ========== /** @@ -201,6 +277,16 @@ public class FlowableUtils { return (String) task.getTaskLocalVariables().get(BpmnVariableConstants.TASK_VARIABLE_REASON); } + /** + * 获得任务的签名图片 URL + * + * @param task 任务 + * @return 签名图片 URL + */ + public static String getTaskSignPicUrl(TaskInfo task) { + return (String) task.getTaskLocalVariables().get(BpmnVariableConstants.TASK_SIGN_PIC_URL); + } + /** * 获得任务的表单 * 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 ab6432375..97c5f8a70 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 @@ -5,21 +5,26 @@ import cn.hutool.core.lang.Assert; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.*; import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; 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.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 cn.iocoder.yudao.module.bpm.framework.flowable.core.listener.BpmTriggerTaskDelegate; import org.flowable.bpmn.BpmnAutoLayout; import org.flowable.bpmn.constants.BpmnXMLConstants; import org.flowable.bpmn.model.Process; import org.flowable.bpmn.model.*; +import org.flowable.engine.delegate.TaskListener; +import org.springframework.util.MultiValueMap; import java.util.*; import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.*; import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.*; +import static cn.iocoder.yudao.module.bpm.service.task.listener.BpmUserTaskListener.DELEGATE_EXPRESSION; import static java.util.Arrays.asList; /** @@ -32,12 +37,13 @@ import static java.util.Arrays.asList; */ public class SimpleModelUtils { - private static final Map NODE_CONVERTS = MapUtil.newHashMap(); + 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()); + new DelayTimerNodeConvert(), new TriggerNodeConvert(), + new ConditionBranchNodeConvert(), new ParallelBranchNodeConvert(), new InclusiveBranchNodeConvert(), new RouteBranchNodeConvert()); converts.forEach(convert -> NODE_CONVERTS.put(convert.getType(), convert)); } @@ -83,14 +89,14 @@ public class SimpleModelUtils { private static BpmSimpleModelNodeVO buildStartNode() { return new BpmSimpleModelNodeVO().setId(START_EVENT_NODE_ID) - .setName(BpmSimpleModelNodeType.START_NODE.getName()) - .setType(BpmSimpleModelNodeType.START_NODE.getType()); + .setName(BpmSimpleModelNodeTypeEnum.START_NODE.getName()) + .setType(BpmSimpleModelNodeTypeEnum.START_NODE.getType()); } /** * 遍历节点,构建 FlowNode 元素 * - * @param node SIMPLE 节点 + * @param node SIMPLE 节点 * @param process BPMN 流程 */ private static void traverseNodeToBuildFlowNode(BpmSimpleModelNodeVO node, Process process) { @@ -98,7 +104,7 @@ public class SimpleModelUtils { if (!isValidNode(node)) { return; } - BpmSimpleModelNodeType nodeType = BpmSimpleModelNodeType.valueOf(node.getType()); + BpmSimpleModelNodeTypeEnum nodeType = BpmSimpleModelNodeTypeEnum.valueOf(node.getType()); Assert.notNull(nodeType, "模型节点类型({})不支持", node.getType()); // 2. 处理当前节点 @@ -108,7 +114,7 @@ public class SimpleModelUtils { flowElements.forEach(process::addFlowElement); // 3.1 情况一:如果当前是分支节点,并且存在条件节点,则处理每个条件的子节点 - if (BpmSimpleModelNodeType.isBranchNode(node.getType()) + if (BpmSimpleModelNodeTypeEnum.isBranchNode(node.getType()) && CollUtil.isNotEmpty(node.getConditionNodes())) { // 注意:这里的 item.getChildNode() 处理的是每个条件的子节点,不是处理条件 node.getConditionNodes().forEach(item -> traverseNodeToBuildFlowNode(item.getChildNode(), process)); @@ -121,8 +127,8 @@ public class SimpleModelUtils { /** * 遍历节点,构建 SequenceFlow 元素 * - * @param process Bpmn 流程 - * @param node 当前节点 + * @param process Bpmn 流程 + * @param node 当前节点 * @param targetNodeId 目标节点 ID */ private static void traverseNodeToBuildSequenceFlow(Process process, BpmSimpleModelNodeVO node, String targetNodeId) { @@ -131,14 +137,14 @@ public class SimpleModelUtils { return; } // 1.2 END_NODE 直接返回 - BpmSimpleModelNodeType nodeType = BpmSimpleModelNodeType.valueOf(node.getType()); + BpmSimpleModelNodeTypeEnum nodeType = BpmSimpleModelNodeTypeEnum.valueOf(node.getType()); Assert.notNull(nodeType, "模型节点类型不支持"); - if (nodeType == BpmSimpleModelNodeType.END_NODE) { + if (nodeType == BpmSimpleModelNodeTypeEnum.END_NODE) { return; } // 2.1 情况一:普通节点 - if (!BpmSimpleModelNodeType.isBranchNode(node.getType())) { + if (!BpmSimpleModelNodeTypeEnum.isBranchNode(node.getType())) { traverseNormalNodeToBuildSequenceFlow(process, node, targetNodeId); } else { // 2.2 情况二:分支节点 @@ -149,8 +155,8 @@ public class SimpleModelUtils { /** * 遍历普通(非条件)节点,构建 SequenceFlow 元素 * - * @param process Bpmn 流程 - * @param node 当前节点 + * @param process Bpmn 流程 + * @param node 当前节点 * @param targetNodeId 目标节点 ID */ private static void traverseNormalNodeToBuildSequenceFlow(Process process, BpmSimpleModelNodeVO node, String targetNodeId) { @@ -158,7 +164,7 @@ public class SimpleModelUtils { boolean isChildNodeValid = isValidNode(childNode); // 情况一:有“子”节点,则建立连线 // 情况二:没有“子节点”,则直接跟 targetNodeId 建立连线。例如说,结束节点、条件分支(分支节点的孩子节点或聚合节点)的最后一个节点 - String finalTargetNodeId = isChildNodeValid? childNode.getId() : targetNodeId; + String finalTargetNodeId = isChildNodeValid ? childNode.getId() : targetNodeId; SequenceFlow sequenceFlow = buildBpmnSequenceFlow(node.getId(), finalTargetNodeId); process.addFlowElement(sequenceFlow); @@ -171,53 +177,68 @@ public class SimpleModelUtils { /** * 遍历条件节点,构建 SequenceFlow 元素 * - * @param process Bpmn 流程 - * @param node 当前节点 + * @param process Bpmn 流程 + * @param node 当前节点 * @param targetNodeId 目标节点 ID */ private static void traverseBranchNodeToBuildSequenceFlow(Process process, BpmSimpleModelNodeVO node, String targetNodeId) { - BpmSimpleModelNodeType nodeType = BpmSimpleModelNodeType.valueOf(node.getType()); + BpmSimpleModelNodeTypeEnum nodeType = BpmSimpleModelNodeTypeEnum.valueOf(node.getType()); BpmSimpleModelNodeVO childNode = node.getChildNode(); List conditionNodes = node.getConditionNodes(); - Assert.notEmpty(conditionNodes, "分支节点的条件节点不能为空"); + // TODO @芋艿 路由分支没有conditionNodes 这里注释会影响吗?@jason:一起帮忙瞅瞅! +// Assert.notEmpty(conditionNodes, "分支节点的条件节点不能为空"); // 分支终点节点 ID String branchEndNodeId = null; - if (nodeType == BpmSimpleModelNodeType.CONDITION_BRANCH_NODE) { // 条件分支 + if (nodeType == BpmSimpleModelNodeTypeEnum.CONDITION_BRANCH_NODE + || nodeType == BpmSimpleModelNodeTypeEnum.ROUTER_BRANCH_NODE) { // 条件分支或路由分支 // 分两种情况 1. 分支节点有孩子节点为孩子节点 Id 2. 分支节点孩子为无效节点时 (分支嵌套且为分支最后一个节点) 为分支终点节点 ID branchEndNodeId = isValidNode(childNode) ? childNode.getId() : targetNodeId; - } else if (nodeType == BpmSimpleModelNodeType.PARALLEL_BRANCH_NODE - || nodeType == BpmSimpleModelNodeType.INCLUSIVE_BRANCH_NODE) { // 并行分支或包容分支 + } else if (nodeType == BpmSimpleModelNodeTypeEnum.PARALLEL_BRANCH_NODE + || nodeType == BpmSimpleModelNodeTypeEnum.INCLUSIVE_BRANCH_NODE) { // 并行分支或包容分支 // 分支节点:分支终点节点 Id 为程序创建的网关集合节点。目前不会从前端传入。 branchEndNodeId = buildGatewayJoinId(node.getId()); } Assert.notEmpty(branchEndNodeId, "分支终点节点 Id 不能为空"); // 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); + if (nodeType == BpmSimpleModelNodeTypeEnum.ROUTER_BRANCH_NODE) { + // 路由分支遍历 + for (BpmSimpleModelNodeVO.RouterSetting router : node.getRouterGroups()) { + SequenceFlow sequenceFlow = RouteBranchNodeConvert.buildSequenceFlow(node.getId(), router); process.addFlowElement(sequenceFlow); } + } else { + // 下面的注释,以如下情况举例子。分支 1:A->B->C->D->E,分支 2:A->D->E。其中,A 为分支节点, D 为 A 孩子节点 + for (BpmSimpleModelNodeVO item : conditionNodes) { + Assert.isTrue(Objects.equals(item.getType(), BpmSimpleModelNodeTypeEnum.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); + } + } } - // 4. 如果是并行分支、包容分支,由于是程序创建的聚合网关,需要手工创建聚合网关和下一个节点的连线 - if (nodeType == BpmSimpleModelNodeType.PARALLEL_BRANCH_NODE - || nodeType == BpmSimpleModelNodeType.INCLUSIVE_BRANCH_NODE ) { + // 4.1 如果是并行分支、包容分支,由于是程序创建的聚合网关,需要手工创建聚合网关和下一个节点的连线 + if (nodeType == BpmSimpleModelNodeTypeEnum.PARALLEL_BRANCH_NODE + || nodeType == BpmSimpleModelNodeTypeEnum.INCLUSIVE_BRANCH_NODE) { String nextNodeId = isValidNode(childNode) ? childNode.getId() : targetNodeId; SequenceFlow sequenceFlow = buildBpmnSequenceFlow(branchEndNodeId, nextNodeId); process.addFlowElement(sequenceFlow); + // 4.2 如果是路由分支,需要连接后续节点为默认路由 + } else if (nodeType == BpmSimpleModelNodeTypeEnum.ROUTER_BRANCH_NODE) { + SequenceFlow sequenceFlow = buildBpmnSequenceFlow(node.getId(), branchEndNodeId, node.getRouterDefaultFlowId(), + null, null); + process.addFlowElement(sequenceFlow); } // 5. 递归调用后续节点 继续递归。例如说,建立 D->E 的连线 @@ -253,7 +274,7 @@ public class SimpleModelUtils { } public static boolean isSequentialApproveNode(BpmSimpleModelNodeVO node) { - return BpmSimpleModelNodeType.APPROVE_NODE.getType().equals(node.getType()) + return BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType().equals(node.getType()) && BpmUserTaskApproveMethodEnum.SEQUENTIAL.getMethod().equals(node.getApproveMethod()); } @@ -269,7 +290,7 @@ public class SimpleModelUtils { throw new UnsupportedOperationException("请实现该方法"); } - BpmSimpleModelNodeType getType(); + BpmSimpleModelNodeTypeEnum getType(); } @@ -284,8 +305,8 @@ public class SimpleModelUtils { } @Override - public BpmSimpleModelNodeType getType() { - return BpmSimpleModelNodeType.START_NODE; + public BpmSimpleModelNodeTypeEnum getType() { + return BpmSimpleModelNodeTypeEnum.START_NODE; } } @@ -302,8 +323,8 @@ public class SimpleModelUtils { } @Override - public BpmSimpleModelNodeType getType() { - return BpmSimpleModelNodeType.END_NODE; + public BpmSimpleModelNodeTypeEnum getType() { + return BpmSimpleModelNodeTypeEnum.END_NODE; } } @@ -331,8 +352,8 @@ public class SimpleModelUtils { } @Override - public BpmSimpleModelNodeType getType() { - return BpmSimpleModelNodeType.START_USER_NODE; + public BpmSimpleModelNodeTypeEnum getType() { + return BpmSimpleModelNodeTypeEnum.START_USER_NODE; } } @@ -355,8 +376,8 @@ public class SimpleModelUtils { } @Override - public BpmSimpleModelNodeType getType() { - return BpmSimpleModelNodeType.APPROVE_NODE; + public BpmSimpleModelNodeTypeEnum getType() { + return BpmSimpleModelNodeTypeEnum.APPROVE_NODE; } /** @@ -384,7 +405,7 @@ public class SimpleModelUtils { boundaryEvent.addEventDefinition(eventDefinition); // 2.1 添加定时器边界事件类型 - addExtensionElement(boundaryEvent, BOUNDARY_EVENT_TYPE, BpmBoundaryEventType.USER_TASK_TIMEOUT.getType()); + addExtensionElement(boundaryEvent, BOUNDARY_EVENT_TYPE, BpmBoundaryEventTypeEnum.USER_TASK_TIMEOUT.getType()); // 2.2 添加超时执行动作元素 addExtensionElement(boundaryEvent, USER_TASK_TIMEOUT_HANDLER_TYPE, timeoutHandler.getType()); return boundaryEvent; @@ -419,9 +440,49 @@ public class SimpleModelUtils { if (node.getTimeoutHandler() != null && node.getTimeoutHandler().getEnable()) { userTask.setDueDate(node.getTimeoutHandler().getTimeDuration()); } + // 设置监听器 + addUserTaskListener(node, userTask); + // 添加是否需要签名 + addSignEnable(node.getSignEnable(), userTask); + // 审批意见 + addReasonRequire(node.getReasonRequire(), userTask); return userTask; } + private void addUserTaskListener(BpmSimpleModelNodeVO node, UserTask userTask) { + List flowableListeners = new ArrayList<>(3); + if (node.getTaskCreateListener() != null + && Boolean.TRUE.equals(node.getTaskCreateListener().getEnable())) { + FlowableListener flowableListener = new FlowableListener(); + flowableListener.setEvent(TaskListener.EVENTNAME_CREATE); + flowableListener.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_DELEGATEEXPRESSION); + flowableListener.setImplementation(DELEGATE_EXPRESSION); + addListenerConfig(flowableListener, node.getTaskCreateListener()); + flowableListeners.add(flowableListener); + } + if (node.getTaskAssignListener() != null + && Boolean.TRUE.equals(node.getTaskAssignListener().getEnable())) { + FlowableListener flowableListener = new FlowableListener(); + flowableListener.setEvent(TaskListener.EVENTNAME_ASSIGNMENT); + flowableListener.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_DELEGATEEXPRESSION); + flowableListener.setImplementation(DELEGATE_EXPRESSION); + addListenerConfig(flowableListener, node.getTaskAssignListener()); + flowableListeners.add(flowableListener); + } + if (node.getTaskCompleteListener() != null + && Boolean.TRUE.equals(node.getTaskCompleteListener().getEnable())) { + FlowableListener flowableListener = new FlowableListener(); + flowableListener.setEvent(TaskListener.EVENTNAME_COMPLETE); + flowableListener.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_DELEGATEEXPRESSION); + flowableListener.setImplementation(DELEGATE_EXPRESSION); + addListenerConfig(flowableListener, node.getTaskCompleteListener()); + flowableListeners.add(flowableListener); + } + if (CollUtil.isNotEmpty(flowableListeners)) { + userTask.setTaskListeners(flowableListeners); + } + } + private void processMultiInstanceLoopCharacteristics(Integer approveMethod, Integer approveRatio, UserTask userTask) { BpmUserTaskApproveMethodEnum approveMethodEnum = BpmUserTaskApproveMethodEnum.valueOf(approveMethod); Assert.notNull(approveMethodEnum, "审批方式({})不能为空", approveMethodEnum); @@ -472,8 +533,8 @@ public class SimpleModelUtils { } @Override - public BpmSimpleModelNodeType getType() { - return BpmSimpleModelNodeType.COPY_NODE; + public BpmSimpleModelNodeTypeEnum getType() { + return BpmSimpleModelNodeTypeEnum.COPY_NODE; } } @@ -488,15 +549,15 @@ public class SimpleModelUtils { // 设置默认的序列流(条件) BpmSimpleModelNodeVO defaultSeqFlow = CollUtil.findOne(node.getConditionNodes(), - item -> BooleanUtil.isTrue(item.getDefaultFlow())); + item -> BooleanUtil.isTrue(item.getConditionSetting().getDefaultFlow())); Assert.notNull(defaultSeqFlow, "条件分支节点({})的默认序列流不能为空", node.getId()); exclusiveGateway.setDefaultFlow(defaultSeqFlow.getId()); return exclusiveGateway; } @Override - public BpmSimpleModelNodeType getType() { - return BpmSimpleModelNodeType.CONDITION_BRANCH_NODE; + public BpmSimpleModelNodeTypeEnum getType() { + return BpmSimpleModelNodeTypeEnum.CONDITION_BRANCH_NODE; } } @@ -517,8 +578,8 @@ public class SimpleModelUtils { } @Override - public BpmSimpleModelNodeType getType() { - return BpmSimpleModelNodeType.PARALLEL_BRANCH_NODE; + public BpmSimpleModelNodeTypeEnum getType() { + return BpmSimpleModelNodeTypeEnum.PARALLEL_BRANCH_NODE; } } @@ -531,7 +592,7 @@ public class SimpleModelUtils { inclusiveGateway.setId(node.getId()); // 设置默认的序列流(条件) BpmSimpleModelNodeVO defaultSeqFlow = CollUtil.findOne(node.getConditionNodes(), - item -> BooleanUtil.isTrue(item.getDefaultFlow())); + item -> BooleanUtil.isTrue(item.getConditionSetting().getDefaultFlow())); Assert.notNull(defaultSeqFlow, "包容分支节点({})的默认序列流不能为空", node.getId()); inclusiveGateway.setDefaultFlow(defaultSeqFlow.getId()); // TODO @jason:setName @@ -544,8 +605,8 @@ public class SimpleModelUtils { } @Override - public BpmSimpleModelNodeType getType() { - return BpmSimpleModelNodeType.INCLUSIVE_BRANCH_NODE; + public BpmSimpleModelNodeTypeEnum getType() { + return BpmSimpleModelNodeTypeEnum.INCLUSIVE_BRANCH_NODE; } } @@ -559,8 +620,8 @@ public class SimpleModelUtils { } @Override - public BpmSimpleModelNodeType getType() { - return BpmSimpleModelNodeType.CONDITION_NODE; + public BpmSimpleModelNodeTypeEnum getType() { + return BpmSimpleModelNodeTypeEnum.CONDITION_NODE; } public static SequenceFlow buildSequenceFlow(String sourceId, String targetId, @@ -575,12 +636,22 @@ public class SimpleModelUtils { * @param node 条件节点 */ public static String buildConditionExpression(BpmSimpleModelNodeVO node) { - BpmSimpleModeConditionType conditionTypeEnum = BpmSimpleModeConditionType.valueOf(node.getConditionType()); - if (conditionTypeEnum == BpmSimpleModeConditionType.EXPRESSION) { - return node.getConditionExpression(); + return buildConditionExpression(node.getConditionSetting().getConditionType(), node.getConditionSetting().getConditionExpression(), + node.getConditionSetting().getConditionGroups()); + } + + public static String buildConditionExpression(BpmSimpleModelNodeVO.RouterSetting router) { + return buildConditionExpression(router.getConditionType(), router.getConditionExpression(), + router.getConditionGroups()); + } + + public static String buildConditionExpression(Integer conditionType, String conditionExpression, + ConditionGroups conditionGroups) { + BpmSimpleModeConditionTypeEnum conditionTypeEnum = BpmSimpleModeConditionTypeEnum.valueOf(conditionType); + if (conditionTypeEnum == BpmSimpleModeConditionTypeEnum.EXPRESSION) { + return conditionExpression; } - if (conditionTypeEnum == BpmSimpleModeConditionType.RULE) { - ConditionGroups conditionGroups = node.getConditionGroups(); + if (conditionTypeEnum == BpmSimpleModeConditionTypeEnum.RULE) { if (conditionGroups == null || CollUtil.isEmpty(conditionGroups.getConditions())) { return null; } @@ -605,6 +676,96 @@ public class SimpleModelUtils { } + public static class DelayTimerNodeConvert implements NodeConvert { + + @Override + public List convertList(BpmSimpleModelNodeVO node) { + List flowElements = new ArrayList<>(2); + // 1. 构建接收任务,通过接收任务可卡住节点 + ReceiveTask receiveTask = new ReceiveTask(); + receiveTask.setId(node.getId()); + receiveTask.setName(node.getName()); + flowElements.add(receiveTask); + + // 2. 添加接收任务的 Timer Boundary Event + if (node.getDelaySetting() != null) { + // 2.1 定时器边界事件 + BoundaryEvent boundaryEvent = new BoundaryEvent(); + boundaryEvent.setId("Event-" + IdUtil.fastUUID()); + boundaryEvent.setCancelActivity(false); + boundaryEvent.setAttachedToRef(receiveTask); + // 2.2 定义超时时间 + TimerEventDefinition eventDefinition = new TimerEventDefinition(); + if (node.getDelaySetting().getDelayType().equals(BpmDelayTimerTypeEnum.FIXED_DATE_TIME.getType())) { + eventDefinition.setTimeDuration(node.getDelaySetting().getDelayTime()); + } else if (node.getDelaySetting().getDelayType().equals(BpmDelayTimerTypeEnum.FIXED_TIME_DURATION.getType())) { + eventDefinition.setTimeDate(node.getDelaySetting().getDelayTime()); + } + boundaryEvent.addEventDefinition(eventDefinition); + addExtensionElement(boundaryEvent, BOUNDARY_EVENT_TYPE, BpmBoundaryEventTypeEnum.DELAY_TIMER_TIMEOUT.getType()); + flowElements.add(boundaryEvent); + } + return flowElements; + } + + @Override + public BpmSimpleModelNodeTypeEnum getType() { + return BpmSimpleModelNodeTypeEnum.DELAY_TIMER_NODE; + } + } + + public static class TriggerNodeConvert implements NodeConvert { + + @Override + public ServiceTask convert(BpmSimpleModelNodeVO node) { + // 触发器使用 ServiceTask 来实现 + ServiceTask serviceTask = new ServiceTask(); + serviceTask.setId(node.getId()); + serviceTask.setName(node.getName()); + serviceTask.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_DELEGATEEXPRESSION); + serviceTask.setImplementation("${" + BpmTriggerTaskDelegate.BEAN_NAME + "}"); + if (node.getTriggerSetting() != null) { + addExtensionElement(serviceTask, TRIGGER_TYPE, node.getTriggerSetting().getType()); + if (node.getTriggerSetting().getHttpRequestSetting() != null) { + // TODO @jason:加个 addExtensionElementJson 方法,方便设置 JSON 类型的属性 + addExtensionElement(serviceTask, TRIGGER_PARAM, + JsonUtils.toJsonString(node.getTriggerSetting().getHttpRequestSetting())); + } + } + return serviceTask; + } + + @Override + public BpmSimpleModelNodeTypeEnum getType() { + return BpmSimpleModelNodeTypeEnum.TRIGGER_NODE; + } + } + + public static class RouteBranchNodeConvert implements NodeConvert { + + @Override + public ExclusiveGateway convert(BpmSimpleModelNodeVO node) { + ExclusiveGateway exclusiveGateway = new ExclusiveGateway(); + exclusiveGateway.setId(node.getId()); + + // 设置默认的序列流(条件) + node.setRouterDefaultFlowId("Flow_" + IdUtil.fastUUID()); + exclusiveGateway.setDefaultFlow(node.getRouterDefaultFlowId()); + return exclusiveGateway; + } + + @Override + public BpmSimpleModelNodeTypeEnum getType() { + return BpmSimpleModelNodeTypeEnum.ROUTER_BRANCH_NODE; + } + + public static SequenceFlow buildSequenceFlow(String nodeId, BpmSimpleModelNodeVO.RouterSetting router) { + String conditionExpression = ConditionNodeConvert.buildConditionExpression(router); + return buildBpmnSequenceFlow(nodeId, router.getNodeId(), null, null, conditionExpression); + } + + } + private static String buildGatewayJoinId(String id) { return id + "_join"; } @@ -620,33 +781,33 @@ public class SimpleModelUtils { } private static void simulateNextNode(BpmSimpleModelNodeVO currentNode, Map variables, - List resultNodes) { + List resultNodes) { // 如果不合法(包括为空),则直接结束 if (!isValidNode(currentNode)) { return; } - BpmSimpleModelNodeType nodeType = BpmSimpleModelNodeType.valueOf(currentNode.getType()); + BpmSimpleModelNodeTypeEnum nodeType = BpmSimpleModelNodeTypeEnum.valueOf(currentNode.getType()); Assert.notNull(nodeType, "模型节点类型不支持"); // 情况: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) { + if (nodeType == BpmSimpleModelNodeTypeEnum.START_NODE + || nodeType == BpmSimpleModelNodeTypeEnum.START_USER_NODE + || nodeType == BpmSimpleModelNodeTypeEnum.APPROVE_NODE + || nodeType == BpmSimpleModelNodeTypeEnum.COPY_NODE + || nodeType == BpmSimpleModelNodeTypeEnum.END_NODE) { // 添加元素 resultNodes.add(currentNode); } // 情况:CONDITION_BRANCH_NODE 排它,只有一个满足条件的。如果没有,就走默认的 - if (nodeType == BpmSimpleModelNodeType.CONDITION_BRANCH_NODE) { + if (nodeType == BpmSimpleModelNodeTypeEnum.CONDITION_BRANCH_NODE) { // 查找满足条件的 BpmSimpleModelNodeVO 节点 BpmSimpleModelNodeVO matchConditionNode = CollUtil.findOne(currentNode.getConditionNodes(), - conditionNode -> !BooleanUtil.isTrue(conditionNode.getDefaultFlow()) - && evalConditionExpress(variables, conditionNode)); + conditionNode -> !BooleanUtil.isTrue(conditionNode.getConditionSetting().getDefaultFlow()) + && evalConditionExpress(variables, conditionNode)); if (matchConditionNode == null) { matchConditionNode = CollUtil.findOne(currentNode.getConditionNodes(), - conditionNode -> BooleanUtil.isTrue(conditionNode.getDefaultFlow())); + conditionNode -> BooleanUtil.isTrue(conditionNode.getConditionSetting().getDefaultFlow())); } Assert.notNull(matchConditionNode, "找不到条件节点({})", currentNode); // 遍历满足条件的 BpmSimpleModelNodeVO 节点 @@ -654,14 +815,14 @@ public class SimpleModelUtils { } // 情况:INCLUSIVE_BRANCH_NODE 包容,多个满足条件的。如果没有,就走默认的 - if (nodeType == BpmSimpleModelNodeType.INCLUSIVE_BRANCH_NODE) { + if (nodeType == BpmSimpleModelNodeTypeEnum.INCLUSIVE_BRANCH_NODE) { // 查找满足条件的 BpmSimpleModelNodeVO 节点 Collection matchConditionNodes = CollUtil.filterNew(currentNode.getConditionNodes(), - conditionNode -> !BooleanUtil.isTrue(conditionNode.getDefaultFlow()) + conditionNode -> !BooleanUtil.isTrue(conditionNode.getConditionSetting().getDefaultFlow()) && evalConditionExpress(variables, conditionNode)); if (CollUtil.isEmpty(matchConditionNodes)) { matchConditionNodes = CollUtil.filterNew(currentNode.getConditionNodes(), - conditionNode -> BooleanUtil.isTrue(conditionNode.getDefaultFlow())); + conditionNode -> BooleanUtil.isTrue(conditionNode.getConditionSetting().getDefaultFlow())); } Assert.isTrue(!matchConditionNodes.isEmpty(), "找不到条件节点({})", currentNode); // 遍历满足条件的 BpmSimpleModelNodeVO 节点 @@ -670,7 +831,7 @@ public class SimpleModelUtils { } // 情况:PARALLEL_BRANCH_NODE 并行,都满足,都走 - if (nodeType == BpmSimpleModelNodeType.PARALLEL_BRANCH_NODE) { + if (nodeType == BpmSimpleModelNodeTypeEnum.PARALLEL_BRANCH_NODE) { // 遍历所有 BpmSimpleModelNodeVO 节点 currentNode.getConditionNodes().forEach(matchConditionNode -> simulateNextNode(matchConditionNode.getChildNode(), variables, resultNodes)); @@ -684,4 +845,26 @@ public class SimpleModelUtils { return BpmnModelUtils.evalConditionExpress(variables, ConditionNodeConvert.buildConditionExpression(conditionNode)); } + // TODO @芋艿:【高】要不要优化下,抽个 HttpUtils + /** + * 添加 HTTP 请求参数。请求头或者请求体 + * + * @param params HTTP 请求参数 + * @param paramSettings HTTP 请求参数设置 + * @param processVariables 流程变量 + */ + public static void addHttpRequestParam(MultiValueMap params, + List paramSettings, + Map processVariables) { + if (CollUtil.isEmpty(paramSettings)) { + return; + } + paramSettings.forEach(item -> { + if (item.getType().equals(BpmHttpRequestParamTypeEnum.FIXED_VALUE.getType())) { + params.add(item.getKey(), item.getValue()); + } else if (item.getType().equals(BpmHttpRequestParamTypeEnum.FROM_FORM.getType())) { + params.add(item.getKey(), processVariables.get(item.getValue()).toString()); + } + }); + } } 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 98a856bf4..5601bcda3 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 @@ -88,6 +88,14 @@ public interface BpmModelService { */ void deleteModel(Long userId, String id); + /** + * 清理模型,包括流程实例 + * + * @param userId 用户编号 + * @param id 编号 + */ + void cleanModel(Long userId, String id); + /** * 修改模型的状态,实际更新的部署的流程定义的状态 * 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 8ffbe688c..603ddd7ca 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 @@ -14,10 +14,12 @@ import cn.iocoder.yudao.module.bpm.convert.definition.BpmModelConvert; import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO; import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum; import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum; +import cn.iocoder.yudao.module.bpm.enums.task.BpmReasonEnum; import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker; 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.task.BpmProcessInstanceCopyService; import jakarta.annotation.Resource; import jakarta.validation.Valid; import lombok.extern.slf4j.Slf4j; @@ -25,10 +27,16 @@ import org.flowable.bpmn.model.BpmnModel; import org.flowable.bpmn.model.StartEvent; import org.flowable.bpmn.model.UserTask; import org.flowable.common.engine.impl.db.SuspensionState; +import org.flowable.engine.HistoryService; import org.flowable.engine.RepositoryService; +import org.flowable.engine.RuntimeService; +import org.flowable.engine.TaskService; +import org.flowable.engine.history.HistoricProcessInstance; import org.flowable.engine.repository.Model; import org.flowable.engine.repository.ModelQuery; import org.flowable.engine.repository.ProcessDefinition; +import org.flowable.engine.runtime.ProcessInstance; +import org.flowable.task.api.Task; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; @@ -63,6 +71,15 @@ public class BpmModelServiceImpl implements BpmModelService { @Resource private BpmTaskCandidateInvoker taskCandidateInvoker; + @Resource + private HistoryService historyService; + @Resource + private RuntimeService runtimeService; + @Resource + private TaskService taskService; + @Resource + private BpmProcessInstanceCopyService processInstanceCopyService; + @Override public List getModelList(String name) { ModelQuery modelQuery = repositoryService.createModelQuery(); @@ -246,6 +263,35 @@ public class BpmModelServiceImpl implements BpmModelService { updateProcessDefinitionSuspended(model.getDeploymentId()); } + @Override + public void cleanModel(Long userId, String id) { + // 1. 校验流程模型存在 + Model model = validateModelManager(id, userId); + + // 2. 清理所有流程数据 + // 2.1 先取消所有正在运行的流程 + List processInstances = runtimeService.createProcessInstanceQuery() + .processDefinitionKey(model.getKey()).list(); + processInstances.forEach(processInstance -> { + runtimeService.deleteProcessInstance(processInstance.getId(), + BpmReasonEnum.CANCEL_BY_SYSTEM.getReason()); + historyService.deleteHistoricProcessInstance(processInstance.getId()); + processInstanceCopyService.deleteProcessInstanceCopy(processInstance.getId()); + }); + // 2.2 再从历史中删除所有相关的流程数据 + List historicProcessInstances = historyService.createHistoricProcessInstanceQuery() + .processDefinitionKey(model.getKey()).list(); + historicProcessInstances.forEach(historicProcessInstance -> { + historyService.deleteHistoricProcessInstance(historicProcessInstance.getId()); + processInstanceCopyService.deleteProcessInstanceCopy(historicProcessInstance.getId()); + }); + // 2.3 清理所有 Task + List tasks = taskService.createTaskQuery() + .processDefinitionKey(model.getKey()).list(); + // TODO @lesan:貌似传递一个 reason 会好点! + tasks.forEach(task -> taskService.deleteTask(task.getId())); + } + @Override public void updateModelState(Long userId, String id, Integer state) { // 1.1 校验流程模型存在 diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessListenerServiceImpl.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessListenerServiceImpl.java index 078c92287..6a04bf27c 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessListenerServiceImpl.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessListenerServiceImpl.java @@ -7,8 +7,8 @@ import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.listener.BpmPr import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.listener.BpmProcessListenerSaveReqVO; import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessListenerDO; import cn.iocoder.yudao.module.bpm.dal.mysql.definition.BpmProcessListenerMapper; -import cn.iocoder.yudao.module.bpm.enums.definition.BpmProcessListenerType; -import cn.iocoder.yudao.module.bpm.enums.definition.BpmProcessListenerValueType; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmProcessListenerTypeEnum; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmProcessListenerValueTypeEnum; import jakarta.annotation.Resource; import org.flowable.engine.delegate.JavaDelegate; import org.flowable.engine.delegate.TaskListener; @@ -52,15 +52,15 @@ public class BpmProcessListenerServiceImpl implements BpmProcessListenerService private void validateCreateProcessListenerValue(BpmProcessListenerSaveReqVO createReqVO) { // class 类型 - if (createReqVO.getValueType().equals(BpmProcessListenerValueType.CLASS.getType())) { + if (createReqVO.getValueType().equals(BpmProcessListenerValueTypeEnum.CLASS.getType())) { try { Class clazz = Class.forName(createReqVO.getValue()); - if (createReqVO.getType().equals(BpmProcessListenerType.EXECUTION.getType()) - && !JavaDelegate.class.isAssignableFrom(clazz)) { + if (createReqVO.getType().equals(BpmProcessListenerTypeEnum.EXECUTION.getType()) + && !JavaDelegate.class.isAssignableFrom(clazz)) { throw exception(PROCESS_LISTENER_CLASS_IMPLEMENTS_ERROR, createReqVO.getValue(), JavaDelegate.class.getName()); - } else if (createReqVO.getType().equals(BpmProcessListenerType.TASK.getType()) - && !TaskListener.class.isAssignableFrom(clazz)) { + } else if (createReqVO.getType().equals(BpmProcessListenerTypeEnum.TASK.getType()) + && !TaskListener.class.isAssignableFrom(clazz)) { throw exception(PROCESS_LISTENER_CLASS_IMPLEMENTS_ERROR, createReqVO.getValue(), TaskListener.class.getName()); } 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 3cbac0616..6c784e49a 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 @@ -50,4 +50,11 @@ public interface BpmProcessInstanceCopyService { PageResult getProcessInstanceCopyPage(Long userId, BpmProcessInstanceCopyPageReqVO pageReqVO); + /** + * 删除抄送流程 + * + * @param processInstanceId 流程实例 ID + */ + void deleteProcessInstanceCopy(String processInstanceId); + } 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 c2d52de05..1f90888c4 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 @@ -87,4 +87,9 @@ public class BpmProcessInstanceCopyServiceImpl implements BpmProcessInstanceCopy return processInstanceCopyMapper.selectPage(userId, pageReqVO); } + @Override + public void deleteProcessInstanceCopy(String processInstanceId) { + processInstanceCopyMapper.deleteByProcessInstanceId(processInstanceId); + } + } 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 eda728e41..5873a208f 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,746 @@ -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. 表单权限 String taskId = reqVO.getTaskId() == null && todoTask != null ? todoTask.getId() : reqVO.getTaskId(); Map formFieldsPermission = getFormFieldsPermission(bpmnModel, reqVO.getActivityId(), taskId); // 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 移除掉 finishedTaskActivityIds.removeAll(unfinishedTaskActivityIds); // 特殊:如果流程实例被拒绝,则需要计算是哪个活动节点。 // 注意,只取最后一个。因为会存在多次拒绝的情况,拒绝驳回到指定节点 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) { return FlowableUtils.executeAuthenticatedUserId(userId, () -> { // 获得流程定义 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 +package cn.iocoder.yudao.module.bpm.service.task; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.date.DateUtil; +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.BpmModelMetaInfoVO; +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.dal.redis.BpmProcessIdRedisDAO; +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.BpmSimpleModelNodeTypeEnum; +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.engine.runtime.ProcessInstanceBuilder; +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; + + @Resource + private BpmProcessIdRedisDAO processIdRedisDAO; + + // ========== 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).includeProcessVariables().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).includeProcessVariables().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. 表单权限 + String taskId = reqVO.getTaskId() == null && todoTask != null ? todoTask.getId() : reqVO.getTaskId(); + Map formFieldsPermission = getFormFieldsPermission(bpmnModel, reqVO.getActivityId(), taskId); + + // 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()) ? + BpmSimpleModelNodeTypeEnum.START_USER_NODE.getType() : BpmSimpleModelNodeTypeEnum.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(BpmSimpleModelNodeTypeEnum.START_USER_NODE.getName()) + .setNodeType(BpmSimpleModelNodeTypeEnum.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(BpmSimpleModelNodeTypeEnum.END_NODE.getName()) + .setNodeType(BpmSimpleModelNodeTypeEnum.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(BpmSimpleModelNodeTypeEnum.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(), + BpmSimpleModelNodeTypeEnum.START_USER_NODE.getType(), + BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType())) { + List candidateUserIds = getTaskCandidateUserList(bpmnModel, node.getId(), + startUserId, processDefinitionInfo.getProcessDefinitionId(), processVariables); + activityNode.setCandidateUserIds(candidateUserIds); + return activityNode; + } + + // 2. 结束节点 + if (BpmSimpleModelNodeTypeEnum.END_NODE.getType().equals(node.getType())) { + return activityNode; + } + + // 3. 抄送节点 + if (CollUtil.isEmpty(runActivityIds) && // 流程发起时:需要展示抄送节点,用于选择抄送人 + BpmSimpleModelNodeTypeEnum.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(BpmSimpleModelNodeTypeEnum.START_USER_NODE.getName()) + .setNodeType(BpmSimpleModelNodeTypeEnum.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(BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType()) + .setCandidateStrategy(BpmnModelUtils.parseCandidateStrategy(node)) + .setCandidateUserIds(candidateUserIds); + } + + // 3. 结束节点 + if (node instanceof EndEvent) { + return activityNode.setName(BpmSimpleModelNodeTypeEnum.END_NODE.getName()) + .setNodeType(BpmSimpleModelNodeTypeEnum.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 移除掉 + finishedTaskActivityIds.removeAll(unfinishedTaskActivityIds); + // 特殊:如果流程实例被拒绝,则需要计算是哪个活动节点。 + // 注意,只取最后一个。因为会存在多次拒绝的情况,拒绝驳回到指定节点 + 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) { + return FlowableUtils.executeAuthenticatedUserId(userId, () -> { + // 获得流程定义 + 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()); + variables.put(BpmnVariableConstants.PROCESS_INSTANCE_SKIP_EXPRESSION_ENABLED, true); // 跳过表达式需要添加此变量为 true,不影响没配置 skipExpression 的节点 + if (CollUtil.isNotEmpty(startUserSelectAssignees)) { + variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES, startUserSelectAssignees); + } + + // 3. 创建流程 + ProcessInstanceBuilder processInstanceBuilder = runtimeService.createProcessInstanceBuilder() + .processDefinitionId(definition.getId()) + .businessKey(businessKey) + .variables(variables); + // 3.1 创建流程 ID + BpmModelMetaInfoVO.ProcessIdRule processIdRule = processDefinitionInfo.getProcessIdRule(); + if (processIdRule != null && Boolean.TRUE.equals(processIdRule.getEnable())) { + processInstanceBuilder.predefineProcessInstanceId(processIdRedisDAO.generate(processIdRule)); + } + // 3.2 流程名称 + BpmModelMetaInfoVO.TitleSetting titleSetting = processDefinitionInfo.getTitleSetting(); + if (titleSetting != null && Boolean.TRUE.equals(titleSetting.getEnable())) { + AdminUserRespDTO user = adminUserApi.getUser(userId).getCheckedData(); + Map cloneVariables = new HashMap<>(variables); + cloneVariables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_ID, user.getNickname()); + cloneVariables.put(BpmnVariableConstants.PROCESS_START_TIME, DateUtil.now()); + cloneVariables.put(BpmnVariableConstants.PROCESS_DEFINITION_NAME, definition.getName().trim()); + processInstanceBuilder.name(StrUtil.format(titleSetting.getTitle(), cloneVariables)); + } else { + processInstanceBuilder.name(definition.getName().trim()); + } + // 3.3 发起流程实例 + ProcessInstance instance = processInstanceBuilder.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); + } + // 1.3 校验允许撤销审批中的申请 + BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionService.getProcessDefinitionInfo(instance.getProcessDefinitionId()); + Assert.notNull(processDefinitionInfo, "流程定义({})不存在", processDefinitionInfo); + if (processDefinitionInfo.getAllowCancelRunningProcess() != null // 防止未配置 AllowCancelRunningProcess , 默认为可取消 + && Boolean.FALSE.equals(processDefinitionInfo.getAllowCancelRunningProcess())) { + throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_ALLOW); + } + + // 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, reason); + } + + @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)); + }); + } + +} 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 06b081953..bff859b06 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 @@ -195,8 +195,9 @@ public interface BpmTaskService { * 将指定流程实例的、进行中的流程任务,移动到结束节点 * * @param processInstanceId 流程编号 + * @param reason 原因 */ - void moveTaskToEnd(String processInstanceId); + void moveTaskToEnd(String processInstanceId, String reason); /** * 将任务退回到指定的 targetDefinitionKey 位置 @@ -275,4 +276,12 @@ public interface BpmTaskService { */ void processTaskTimeout(String processInstanceId, String taskDefineKey, Integer handlerType); + /** + * 处理 延迟器 超时事件 + * + * @param processInstanceId 流程示例编号 + * @param taskDefineKey 任务 Key + */ + void processDelayTimerTimeout(String processInstanceId, String taskDefineKey); + } 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 ca8100c36..65e63acea 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 @@ -13,6 +13,7 @@ import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; 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.dal.dataobject.definition.BpmProcessDefinitionInfoDO; import cn.iocoder.yudao.module.bpm.enums.definition.*; import cn.iocoder.yudao.module.bpm.enums.task.BpmCommentTypeEnum; import cn.iocoder.yudao.module.bpm.enums.task.BpmReasonEnum; @@ -33,15 +34,13 @@ 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.model.BpmnModel; -import org.flowable.bpmn.model.EndEvent; -import org.flowable.bpmn.model.FlowElement; -import org.flowable.bpmn.model.UserTask; +import org.flowable.bpmn.model.*; 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.Execution; import org.flowable.engine.runtime.ProcessInstance; import org.flowable.task.api.DelegationState; import org.flowable.task.api.Task; @@ -63,6 +62,8 @@ import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionU import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*; import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_RETURN_FLAG; +import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.parseReasonRequire; +import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.parseSignEnable; /** * 流程任务实例 Service 实现类 @@ -160,13 +161,18 @@ public class BpmTaskServiceImpl implements BpmTaskService { BpmnModel bpmnModel = bpmProcessDefinitionService.getProcessDefinitionBpmnModel(todoTask.getProcessDefinitionId()); Map buttonsSetting = BpmnModelUtils.parseButtonsSetting( bpmnModel, todoTask.getTaskDefinitionKey()); + Boolean signEnable = parseSignEnable(bpmnModel, todoTask.getTaskDefinitionKey()); + Boolean reasonRequire = parseReasonRequire(bpmnModel, todoTask.getTaskDefinitionKey()); // 4. 任务表单 BpmFormDO taskForm = null; - if (StrUtil.isNotBlank(todoTask.getFormKey())){ + if (StrUtil.isNotBlank(todoTask.getFormKey())) { taskForm = formService.getForm(NumberUtils.parseLong(todoTask.getFormKey())); } - return BpmTaskConvert.INSTANCE.buildTodoTask(todoTask, childrenTasks, buttonsSetting, taskForm); + + return BpmTaskConvert.INSTANCE.buildTodoTask(todoTask, childrenTasks, buttonsSetting, taskForm) + .setSignEnable(signEnable) + .setReasonRequire(reasonRequire); } @Override @@ -477,6 +483,17 @@ public class BpmTaskServiceImpl implements BpmTaskService { if (instance == null) { throw exception(PROCESS_INSTANCE_NOT_EXISTS); } + // 1.3 校验签名 + BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(task.getProcessDefinitionId()); + Boolean signEnable = parseSignEnable(bpmnModel, task.getTaskDefinitionKey()); + if (signEnable && StrUtil.isEmpty(reqVO.getSignPicUrl())) { + throw exception(TASK_SIGNATURE_NOT_EXISTS); + } + // 1.4 校验审批意见 + Boolean reasonRequire = parseReasonRequire(bpmnModel, task.getTaskDefinitionKey()); + if (reasonRequire && StrUtil.isEmpty(reqVO.getReason())) { + throw exception(TASK_REASON_REQUIRE); + } // 情况一:被委派的任务,不调用 complete 去完成任务 if (DelegationState.PENDING.equals(task.getDelegationState())) { @@ -491,8 +508,11 @@ public class BpmTaskServiceImpl implements BpmTaskService { } // 情况三:审批普通的任务。大多数情况下,都是这样 - // 2.1 更新 task 状态、原因 + // 2.1 更新 task 状态、原因、签字 updateTaskStatusAndReason(task.getId(), BpmTaskStatusEnum.APPROVE.getStatus(), reqVO.getReason()); + if (signEnable) { + taskService.setVariableLocal(task.getId(), BpmnVariableConstants.TASK_SIGN_PIC_URL, reqVO.getSignPicUrl()); + } // 2.2 添加评论 taskService.addComment(task.getId(), task.getProcessInstanceId(), BpmCommentTypeEnum.APPROVE.getType(), BpmCommentTypeEnum.APPROVE.formatComment(reqVO.getReason())); @@ -637,8 +657,8 @@ public class BpmTaskServiceImpl implements BpmTaskService { BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(task.getProcessDefinitionId()); FlowElement userTaskElement = BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey()); // 3.1 情况一:驳回到指定的任务节点 - BpmUserTaskRejectHandlerType userTaskRejectHandlerType = BpmnModelUtils.parseRejectHandlerType(userTaskElement); - if (userTaskRejectHandlerType == BpmUserTaskRejectHandlerType.RETURN_USER_TASK) { + BpmUserTaskRejectHandlerTypeEnum userTaskRejectHandlerType = BpmnModelUtils.parseRejectHandlerType(userTaskElement); + if (userTaskRejectHandlerType == BpmUserTaskRejectHandlerTypeEnum.RETURN_USER_TASK) { String returnTaskId = BpmnModelUtils.parseReturnTaskId(userTaskElement); Assert.notNull(returnTaskId, "退回的节点不能为空"); returnTask(userId, new BpmTaskReturnReqVO().setId(task.getId()) @@ -647,7 +667,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { } // 3.2 情况二:直接结束,审批不通过 processInstanceService.updateProcessInstanceReject(instance, reqVO.getReason()); // 标记不通过 - moveTaskToEnd(task.getProcessInstanceId()); // 结束流程 + moveTaskToEnd(task.getProcessInstanceId(), BpmCommentTypeEnum.REJECT.formatComment(reqVO.getReason())); // 结束流程 } /** @@ -818,7 +838,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { } @Override - public void moveTaskToEnd(String processInstanceId) { + public void moveTaskToEnd(String processInstanceId, String reason) { List taskList = getRunningTaskListByProcessInstanceId(processInstanceId, null, null); if (CollUtil.isEmpty(taskList)) { return; @@ -844,6 +864,14 @@ public class BpmTaskServiceImpl implements BpmTaskService { .processInstanceId(processInstanceId) .moveActivityIdsToSingleActivityId(activityIds, endEvent.getId()) .changeState(); + + // 3. 特殊:如果跳转到 EndEvent 流程还未结束, 执行 deleteProcessInstance 方法 + // TODO 芋艿:目前发现并行分支情况下,会存在这个情况,后续看看有没更好的方案; + List executions = runtimeService.createExecutionQuery().processInstanceId(processInstanceId).list(); + if (CollUtil.isNotEmpty(executions)) { + log.warn("[moveTaskToEnd][执行跳转到 EndEvent 后, 流程实例未结束,强制执行 deleteProcessInstance 方法]"); + runtimeService.deleteProcessInstance(processInstanceId, reason); + } } @Override @@ -1144,6 +1172,42 @@ public class BpmTaskServiceImpl implements BpmTaskService { log.error("[processTaskAssigned][taskId({}) 没有找到流程实例]", task.getId()); return; } + + // 自动去重,通过自动审批的方式 TODO @芋艿 驳回的情况得考虑一下;@lesan:驳回后,又自动审批么? + BpmProcessDefinitionInfoDO processDefinitionInfo = bpmProcessDefinitionService.getProcessDefinitionInfo(task.getProcessDefinitionId()); + if (processDefinitionInfo == null) { + log.error("[processTaskAssigned][taskId({}) 没有找到流程定义({})]", task.getId(), task.getProcessDefinitionId()); + return; + } + if (processDefinitionInfo.getAutoApprovalType() != null) { + HistoricTaskInstanceQuery sameAssigneeQuery = historyService.createHistoricTaskInstanceQuery() + .processInstanceId(task.getProcessInstanceId()) + .taskAssignee(task.getAssignee()) // 相同审批人 + .taskVariableValueEquals(BpmnVariableConstants.TASK_VARIABLE_STATUS, BpmTaskStatusEnum.APPROVE.getStatus()) + .finished(); + if (BpmAutoApproveTypeEnum.APPROVE_ALL.getType().equals(processDefinitionInfo.getAutoApprovalType()) + && sameAssigneeQuery.count() > 0) { + getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId()) + .setReason(BpmAutoApproveTypeEnum.APPROVE_ALL.getName())); + return; + } + if (BpmAutoApproveTypeEnum.APPROVE_SEQUENT.getType().equals(processDefinitionInfo.getAutoApprovalType())) { + BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(processInstance.getProcessDefinitionId()); + if (bpmnModel == null) { + log.error("[processTaskAssigned][taskId({}) 没有找到流程模型({})]", task.getId(), task.getProcessDefinitionId()); + return; + } + List sourceTaskIds = convertList(BpmnModelUtils.getElementIncomingFlows( // 获取所有上一个节点 + BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey())), + SequenceFlow::getSourceRef); + if (sameAssigneeQuery.taskDefinitionKeys(sourceTaskIds).count() > 0) { + getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId()) + .setReason(BpmAutoApproveTypeEnum.APPROVE_SEQUENT.getName())); + return; + } + } + } + // 审批人与提交人为同一人时,根据 BpmUserTaskAssignStartUserHandlerTypeEnum 策略进行处理 if (StrUtil.equals(task.getAssignee(), processInstance.getStartUserId())) { // 判断是否为退回或者驳回:如果是退回或者驳回不走这个策略 @@ -1191,9 +1255,11 @@ public class BpmTaskServiceImpl implements BpmTaskService { } } } - - AdminUserRespDTO startUser = adminUserApi.getUser(Long.valueOf(processInstance.getStartUserId())).getCheckedData(); - messageService.sendMessageWhenTaskAssigned(BpmTaskConvert.INSTANCE.convert(processInstance, startUser, task)); + // 注意:需要基于 instance 设置租户编号,避免 Flowable 内部异步时,丢失租户编号 + FlowableUtils.execute(processInstance.getTenantId(), () -> { + AdminUserRespDTO startUser = adminUserApi.getUser(Long.valueOf(processInstance.getStartUserId())).getCheckedData(); + messageService.sendMessageWhenTaskAssigned(BpmTaskConvert.INSTANCE.convert(processInstance, startUser, task)); + }); } }); @@ -1238,6 +1304,23 @@ public class BpmTaskServiceImpl implements BpmTaskService { })); } + @Override + public void processDelayTimerTimeout(String processInstanceId, String taskDefineKey) { + Execution execution = runtimeService.createExecutionQuery() + .processInstanceId(processInstanceId) + .activityId(taskDefineKey) + .singleResult(); + if (execution == null) { + log.error("[processDelayTimerTimeout][processInstanceId({}) activityId({}) 没有找到执行活动]", + processInstanceId, taskDefineKey); + return; + } + + // 若存在直接触发接收任务,执行后续节点 + FlowableUtils.execute(execution.getTenantId(), + () -> runtimeService.trigger(execution.getId())); + } + /** * 获得自身的代理对象,解决 AOP 生效问题 * diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/listener/BpmUserTaskListener.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/listener/BpmUserTaskListener.java new file mode 100644 index 000000000..97c103c14 --- /dev/null +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/listener/BpmUserTaskListener.java @@ -0,0 +1,96 @@ +package cn.iocoder.yudao.module.bpm.service.task.listener; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.SimpleModelUtils; +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; +import jakarta.annotation.Resource; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.flowable.engine.delegate.TaskListener; +import org.flowable.engine.history.HistoricProcessInstance; +import org.flowable.engine.impl.el.FixedValue; +import org.flowable.task.service.delegate.DelegateTask; +import org.springframework.context.annotation.Scope; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +import java.util.Map; + +import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; +import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.parseListenerConfig; + +// TODO @芋艿:可能会想换个包地址 +/** + * BPM 用户任务通用监听器 + * + * @author Lesan + */ +@Component +@Slf4j +@Scope("prototype") +public class BpmUserTaskListener implements TaskListener { + + public static final String DELEGATE_EXPRESSION = "${bpmUserTaskListener}"; + + @Resource + private BpmProcessInstanceService processInstanceService; + + @Resource + private RestTemplate restTemplate; + + @Setter + private FixedValue listenerConfig; + + @Override + public void notify(DelegateTask delegateTask) { + // 1. 获取所需基础信息 + HistoricProcessInstance processInstance = processInstanceService.getHistoricProcessInstance(delegateTask.getProcessInstanceId()); + BpmSimpleModelNodeVO.ListenerHandler listenerHandler = parseListenerConfig(listenerConfig); + + // 2. 获取请求头和请求体 + Map processVariables = processInstance.getProcessVariables(); + MultiValueMap headers = new LinkedMultiValueMap<>(); + MultiValueMap body = new LinkedMultiValueMap<>(); + SimpleModelUtils.addHttpRequestParam(headers, listenerHandler.getHeader(), processVariables); + SimpleModelUtils.addHttpRequestParam(body, listenerHandler.getBody(), processVariables); + // 2.1 请求头默认参数 + if (StrUtil.isNotEmpty(delegateTask.getTenantId())) { + headers.add(HEADER_TENANT_ID, delegateTask.getTenantId()); + } + // 2.2 请求体默认参数 + // TODO @芋艿:哪些默认参数,后续再调研下;感觉可以搞个 task 字段,把整个 delegateTask 放进去; + body.add("processInstanceId", delegateTask.getProcessInstanceId()); + body.add("assignee", delegateTask.getAssignee()); + body.add("taskDefinitionKey", delegateTask.getTaskDefinitionKey()); + body.add("taskId", delegateTask.getId()); + + // 3. 异步发起请求 + // TODO @芋艿:确认要同步,还是异步 + HttpEntity> requestEntity = new HttpEntity<>(body, headers); + try { + ResponseEntity responseEntity = restTemplate.exchange(listenerHandler.getPath(), HttpMethod.POST, + requestEntity, String.class); + log.info("[notify][监听器:{},事件类型:{},请求头:{},请求体:{},响应结果:{}]", + DELEGATE_EXPRESSION, + delegateTask.getEventName(), + headers, + body, + responseEntity); + } catch (RestClientException e) { + log.error("[error][监听器:{},事件类型:{},请求头:{},请求体:{},请求出错:{}]", + DELEGATE_EXPRESSION, + delegateTask.getEventName(), + headers, + body, + e.getMessage()); + } + // 4. 是否需要后续操作?TODO 芋艿:待定! + } +} \ 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/trigger/BpmHttpRequestTrigger.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/BpmHttpRequestTrigger.java new file mode 100644 index 000000000..9b42d0d4d --- /dev/null +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/BpmHttpRequestTrigger.java @@ -0,0 +1,75 @@ +package cn.iocoder.yudao.module.bpm.service.task.trigger; + +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO.TriggerSetting.HttpRequestTriggerSetting; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmTriggerTypeEnum; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.SimpleModelUtils; +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.flowable.engine.runtime.ProcessInstance; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +import java.util.Map; + +import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; + +/** + * BPM 发送 HTTP 请求触发器 + * + * @author jason + */ +@Component +@Slf4j +public class BpmHttpRequestTrigger implements BpmTrigger { + + @Resource + private BpmProcessInstanceService processInstanceService; + + @Resource + private RestTemplate restTemplate; + + @Override + public BpmTriggerTypeEnum getType() { + return BpmTriggerTypeEnum.HTTP_REQUEST; + } + + @Override + public void execute(String processInstanceId, String param) { + // 1. 解析 http 请求配置 + HttpRequestTriggerSetting setting = JsonUtils.parseObject(param, HttpRequestTriggerSetting.class); + if (setting == null) { + log.error("[execute][流程({}) HTTP 触发器请求配置为空]", processInstanceId); + return; + } + // 2.1 设置请求头 + ProcessInstance processInstance = processInstanceService.getProcessInstance(processInstanceId); + Map processVariables = processInstance.getProcessVariables(); + MultiValueMap headers = new LinkedMultiValueMap<>(); + headers.add(HEADER_TENANT_ID, processInstance.getTenantId()); + SimpleModelUtils.addHttpRequestParam(headers, setting.getHeader(), processVariables); + // 2.2 设置请求体 + MultiValueMap body = new LinkedMultiValueMap<>(); + SimpleModelUtils.addHttpRequestParam(body, setting.getBody(), processVariables); + body.add("processInstanceId", processInstanceId); + + // TODO @芋艿:要不要抽象一个 Http 请求的工具类,方便复用呢? + // 3. 发起请求 + HttpEntity> requestEntity = new HttpEntity<>(body, headers); + try { + ResponseEntity responseEntity = restTemplate.exchange(setting.getUrl(), HttpMethod.POST, + requestEntity, String.class); + log.info("[execute][HTTP 触发器,请求头:{},请求体:{},响应结果:{}]", headers, body, responseEntity); + } catch (RestClientException e) { + log.error("[execute][HTTP 触发器,请求头:{},请求体:{},请求出错:{}]", headers, body, e.getMessage()); + } + } + +} diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/BpmTrigger.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/BpmTrigger.java new file mode 100644 index 000000000..dfbaa63ec --- /dev/null +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/BpmTrigger.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.bpm.service.task.trigger; + +import cn.iocoder.yudao.module.bpm.enums.definition.BpmTriggerTypeEnum; + +// TODO @芋艿:可能会想换个包地址 +/** + * BPM 触发器接口 + *

+ * 处理不同的动作 + * + * @author jason + */ +public interface BpmTrigger { + + /** + * 对应触发器类型 + * + * @return 触发器类型 + */ + BpmTriggerTypeEnum getType(); + + /** + * 触发器执行 + * + * @param processInstanceId 流程实例编号 + * @param param 触发器参数 + */ + void execute(String processInstanceId, String param); + +} diff --git a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java index 7f4cc584a..374afccad 100644 --- a/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java +++ b/yudao-module-pay/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java @@ -48,7 +48,7 @@ import static cn.iocoder.yudao.framework.pay.core.client.impl.weixin.WxPayClient /** * 微信支付抽象类,实现微信统一的接口、以及部分实现(退款) * - * @author 遇到源码 + * @author 芋道源码 */ @Slf4j public abstract class AbstractWxPayClient extends AbstractPayClient {