Commit fe3ca8deba506690d60ac771101807c16dc562b9
1 parent
ae7dbd3c
【代码优化】BPM:审批超时提醒的实现
Showing
17 changed files
with
179 additions
and
218 deletions
yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmBoundaryEventType.java
yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmUserTaskTimeoutHandlerType.java
yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/message/BpmMessageEnum.java
| ... | ... | @@ -14,7 +14,8 @@ public enum BpmMessageEnum { |
| 14 | 14 | |
| 15 | 15 | PROCESS_INSTANCE_APPROVE("bpm_process_instance_approve"), // 流程任务被审批通过时,发送给申请人 |
| 16 | 16 | PROCESS_INSTANCE_REJECT("bpm_process_instance_reject"), // 流程任务被审批不通过时,发送给申请人 |
| 17 | - TASK_ASSIGNED("bpm_task_assigned"); // 任务被分配时,发送给审批人 | |
| 17 | + TASK_ASSIGNED("bpm_task_assigned"), // 任务被分配时,发送给审批人 | |
| 18 | + TASK_TIMEOUT("bpm_task_timeout"); // 任务审批超时时,发送给审批人 | |
| 18 | 19 | |
| 19 | 20 | /** |
| 20 | 21 | * 短信模板的标识 | ... | ... |
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/simple/BpmSimpleModelNodeVO.java
| ... | ... | @@ -96,6 +96,7 @@ public class BpmSimpleModelNodeVO { |
| 96 | 96 | private String returnNodeId; |
| 97 | 97 | } |
| 98 | 98 | |
| 99 | + // TODO @芋艿:参数校验 | |
| 99 | 100 | @Data |
| 100 | 101 | @Schema(description = "审批节点超时处理策略") |
| 101 | 102 | public static class TimeoutHandler { |
| ... | ... | @@ -103,6 +104,7 @@ public class BpmSimpleModelNodeVO { |
| 103 | 104 | @Schema(description = "是否开启超时处理", example = "false") |
| 104 | 105 | private Boolean enable; |
| 105 | 106 | |
| 107 | + // TODO @jason:type 是不是更合适哈; | |
| 106 | 108 | @Schema(description = "任务超时未处理的行为", example = "1") |
| 107 | 109 | @InEnum(BpmUserTaskTimeoutHandlerType.class) |
| 108 | 110 | private Integer action; |
| ... | ... | @@ -112,6 +114,7 @@ public class BpmSimpleModelNodeVO { |
| 112 | 114 | |
| 113 | 115 | @Schema(description = "最大提醒次数", example = "1") |
| 114 | 116 | private Integer maxRemindCount; |
| 117 | + | |
| 115 | 118 | } |
| 116 | 119 | |
| 117 | 120 | @Data | ... | ... |
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmTaskEventListener.java
| 1 | 1 | package cn.iocoder.yudao.module.bpm.framework.flowable.core.listener; |
| 2 | 2 | |
| 3 | 3 | import cn.hutool.core.collection.CollUtil; |
| 4 | +import cn.hutool.core.util.ObjectUtil; | |
| 4 | 5 | import cn.hutool.core.util.StrUtil; |
| 6 | +import cn.iocoder.yudao.framework.common.util.number.NumberUtils; | |
| 7 | +import cn.iocoder.yudao.module.bpm.enums.definition.BpmBoundaryEventType; | |
| 8 | +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants; | |
| 9 | +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils; | |
| 10 | +import cn.iocoder.yudao.module.bpm.service.definition.BpmModelService; | |
| 5 | 11 | import cn.iocoder.yudao.module.bpm.service.task.BpmActivityService; |
| 6 | 12 | import cn.iocoder.yudao.module.bpm.service.task.BpmTaskService; |
| 7 | 13 | import com.google.common.collect.ImmutableSet; |
| 8 | 14 | import jakarta.annotation.Resource; |
| 9 | 15 | import lombok.extern.slf4j.Slf4j; |
| 16 | +import org.flowable.bpmn.model.BoundaryEvent; | |
| 17 | +import org.flowable.bpmn.model.BpmnModel; | |
| 18 | +import org.flowable.bpmn.model.FlowElement; | |
| 10 | 19 | import org.flowable.common.engine.api.delegate.event.FlowableEngineEntityEvent; |
| 11 | 20 | import org.flowable.common.engine.api.delegate.event.FlowableEngineEventType; |
| 12 | 21 | import org.flowable.engine.delegate.event.AbstractFlowableEngineEventListener; |
| 13 | 22 | import org.flowable.engine.delegate.event.FlowableActivityCancelledEvent; |
| 14 | 23 | import org.flowable.engine.history.HistoricActivityInstance; |
| 24 | +import org.flowable.job.api.Job; | |
| 15 | 25 | import org.flowable.task.api.Task; |
| 16 | 26 | import org.springframework.context.annotation.Lazy; |
| 17 | 27 | import org.springframework.stereotype.Component; |
| ... | ... | @@ -29,6 +39,9 @@ import java.util.Set; |
| 29 | 39 | public class BpmTaskEventListener extends AbstractFlowableEngineEventListener { |
| 30 | 40 | |
| 31 | 41 | @Resource |
| 42 | + @Lazy // 延迟加载,避免循环依赖 | |
| 43 | + private BpmModelService modelService; | |
| 44 | + @Resource | |
| 32 | 45 | @Lazy // 解决循环依赖 |
| 33 | 46 | private BpmTaskService taskService; |
| 34 | 47 | @Resource |
| ... | ... | @@ -40,6 +53,7 @@ public class BpmTaskEventListener extends AbstractFlowableEngineEventListener { |
| 40 | 53 | .add(FlowableEngineEventType.TASK_ASSIGNED) |
| 41 | 54 | // .add(FlowableEngineEventType.TASK_COMPLETED) // 由于审批通过时,已经记录了 task 的 status 为通过,所以不需要监听了。 |
| 42 | 55 | .add(FlowableEngineEventType.ACTIVITY_CANCELLED) |
| 56 | + .add(FlowableEngineEventType.TIMER_FIRED) // 监听审批超时 | |
| 43 | 57 | .build(); |
| 44 | 58 | |
| 45 | 59 | public BpmTaskEventListener() { |
| ... | ... | @@ -72,4 +86,30 @@ public class BpmTaskEventListener extends AbstractFlowableEngineEventListener { |
| 72 | 86 | }); |
| 73 | 87 | } |
| 74 | 88 | |
| 89 | + @Override | |
| 90 | + protected void timerFired(FlowableEngineEntityEvent event) { | |
| 91 | + // 1.1 只处理 BoundaryEvent 边界计时时间 | |
| 92 | + String processDefinitionId = event.getProcessDefinitionId(); | |
| 93 | + BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(processDefinitionId); | |
| 94 | + Job entity = (Job) event.getEntity(); | |
| 95 | + FlowElement element = BpmnModelUtils.getFlowElementById(bpmnModel, entity.getElementId()); | |
| 96 | + if (!(element instanceof BoundaryEvent)) { | |
| 97 | + return; | |
| 98 | + } | |
| 99 | + // 1.2 判断是否为超时处理 | |
| 100 | + BoundaryEvent boundaryEvent = (BoundaryEvent) element; | |
| 101 | + String boundaryEventType = BpmnModelUtils.parseBoundaryEventExtensionElement(boundaryEvent, | |
| 102 | + BpmnModelConstants.BOUNDARY_EVENT_TYPE); | |
| 103 | + BpmBoundaryEventType bpmTimerBoundaryEventType = BpmBoundaryEventType.typeOf(NumberUtils.parseInt(boundaryEventType)); | |
| 104 | + if (ObjectUtil.notEqual(bpmTimerBoundaryEventType, BpmBoundaryEventType.USER_TASK_TIMEOUT)) { | |
| 105 | + return; | |
| 106 | + } | |
| 107 | + | |
| 108 | + // 2. 处理超时 | |
| 109 | + String timeoutAction = BpmnModelUtils.parseBoundaryEventExtensionElement(boundaryEvent, | |
| 110 | + BpmnModelConstants.USER_TASK_TIMEOUT_HANDLER_ACTION); | |
| 111 | + String taskKey = boundaryEvent.getAttachedToRefId(); | |
| 112 | + taskService.processTaskTimeout(event.getProcessInstanceId(), taskKey, NumberUtils.parseInt(timeoutAction)); | |
| 113 | + } | |
| 114 | + | |
| 75 | 115 | } | ... | ... |
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmTimerFiredEventListener.java deleted
| 1 | -package cn.iocoder.yudao.module.bpm.framework.flowable.core.listener; | |
| 2 | - | |
| 3 | -import cn.iocoder.yudao.framework.common.util.number.NumberUtils; | |
| 4 | -import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; | |
| 5 | -import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.BpmTaskApproveReqVO; | |
| 6 | -import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.BpmTaskRejectReqVO; | |
| 7 | -import cn.iocoder.yudao.module.bpm.enums.definition.BpmBoundaryEventType; | |
| 8 | -import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskTimeoutHandlerType; | |
| 9 | -import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants; | |
| 10 | -import cn.iocoder.yudao.module.bpm.framework.flowable.core.mq.message.task.TodoTaskReminderMessage; | |
| 11 | -import cn.iocoder.yudao.module.bpm.framework.flowable.core.mq.producer.task.TodoTaskReminderProducer; | |
| 12 | -import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils; | |
| 13 | -import cn.iocoder.yudao.module.bpm.service.definition.BpmModelService; | |
| 14 | -import cn.iocoder.yudao.module.bpm.service.task.BpmTaskService; | |
| 15 | -import com.google.common.collect.ImmutableSet; | |
| 16 | -import jakarta.annotation.Resource; | |
| 17 | -import lombok.extern.slf4j.Slf4j; | |
| 18 | -import org.flowable.bpmn.model.BoundaryEvent; | |
| 19 | -import org.flowable.bpmn.model.BpmnModel; | |
| 20 | -import org.flowable.bpmn.model.FlowElement; | |
| 21 | -import org.flowable.common.engine.api.delegate.event.FlowableEngineEntityEvent; | |
| 22 | -import org.flowable.common.engine.api.delegate.event.FlowableEngineEventType; | |
| 23 | -import org.flowable.engine.delegate.event.AbstractFlowableEngineEventListener; | |
| 24 | -import org.flowable.job.api.Job; | |
| 25 | -import org.flowable.task.api.Task; | |
| 26 | -import org.springframework.context.annotation.Lazy; | |
| 27 | -import org.springframework.stereotype.Component; | |
| 28 | - | |
| 29 | -import java.util.List; | |
| 30 | -import java.util.Set; | |
| 31 | - | |
| 32 | -// TODO @芋艿:这块需要仔细再瞅瞅 | |
| 33 | -/** | |
| 34 | - * 监听定时器触发事件 | |
| 35 | - * | |
| 36 | - * @author jason | |
| 37 | - */ | |
| 38 | -@Component | |
| 39 | -@Slf4j | |
| 40 | -public class BpmTimerFiredEventListener extends AbstractFlowableEngineEventListener { | |
| 41 | - | |
| 42 | - @Resource | |
| 43 | - @Lazy // 延迟加载,避免循环依赖 | |
| 44 | - private BpmModelService bpmModelService; | |
| 45 | - @Resource | |
| 46 | - @Lazy // 延迟加载,避免循环依赖 | |
| 47 | - private BpmTaskService bpmTaskService; | |
| 48 | - | |
| 49 | - @Resource | |
| 50 | - private TodoTaskReminderProducer todoTaskReminderProducer; | |
| 51 | - | |
| 52 | - public static final Set<FlowableEngineEventType> TIME_EVENTS = ImmutableSet.<FlowableEngineEventType>builder() | |
| 53 | - .add(FlowableEngineEventType.TIMER_FIRED) | |
| 54 | - .build(); | |
| 55 | - | |
| 56 | - public BpmTimerFiredEventListener() { | |
| 57 | - super(TIME_EVENTS); | |
| 58 | - } | |
| 59 | - | |
| 60 | - @Override | |
| 61 | - protected void timerFired(FlowableEngineEntityEvent event) { | |
| 62 | - String processDefinitionId = event.getProcessDefinitionId(); | |
| 63 | - BpmnModel bpmnModel = bpmModelService.getBpmnModelByDefinitionId(processDefinitionId); | |
| 64 | - Job entity = (Job) event.getEntity(); | |
| 65 | - FlowElement element = BpmnModelUtils.getFlowElementById(bpmnModel, entity.getElementId()); | |
| 66 | - // 如果是定时器边界事件 | |
| 67 | - if (element instanceof BoundaryEvent) { | |
| 68 | - BoundaryEvent boundaryEvent = (BoundaryEvent) element; | |
| 69 | - String boundaryEventType = BpmnModelUtils.parseBoundaryEventExtensionElement(boundaryEvent, BpmnModelConstants.BOUNDARY_EVENT_TYPE); | |
| 70 | - BpmBoundaryEventType bpmTimerBoundaryEventType = BpmBoundaryEventType.typeOf(NumberUtils.parseInt(boundaryEventType)); | |
| 71 | - // 类型为用户任务超时未处理的情况 | |
| 72 | - if (bpmTimerBoundaryEventType == BpmBoundaryEventType.USER_TASK_TIMEOUT) { | |
| 73 | - String timeoutAction = BpmnModelUtils.parseBoundaryEventExtensionElement(boundaryEvent, BpmnModelConstants.USER_TASK_TIMEOUT_HANDLER_ACTION); | |
| 74 | - userTaskTimeoutHandler(event.getProcessInstanceId(), boundaryEvent.getAttachedToRefId(), NumberUtils.parseInt(timeoutAction)); | |
| 75 | - } | |
| 76 | - } | |
| 77 | - } | |
| 78 | - | |
| 79 | - private void userTaskTimeoutHandler(String processInstanceId, String taskDefKey, Integer timeoutAction) { | |
| 80 | - BpmUserTaskTimeoutHandlerType userTaskTimeoutAction = BpmUserTaskTimeoutHandlerType.typeOf(timeoutAction); | |
| 81 | - if (userTaskTimeoutAction != null) { | |
| 82 | - // 查询超时未处理的任务 TODO 加签的情况会不会有问题 ??? | |
| 83 | - List<Task> taskList = bpmTaskService.getRunningTaskListByProcessInstanceId(processInstanceId, true, taskDefKey); | |
| 84 | - taskList.forEach(task -> { | |
| 85 | - // 自动提醒 | |
| 86 | - if (userTaskTimeoutAction == BpmUserTaskTimeoutHandlerType.REMINDER) { | |
| 87 | - TodoTaskReminderMessage message = new TodoTaskReminderMessage().setTenantId(Long.parseLong(task.getTenantId())) | |
| 88 | - .setUserId(Long.parseLong(task.getAssignee())).setTaskName(task.getName()); | |
| 89 | - todoTaskReminderProducer.sendReminderMessage(message); | |
| 90 | - } | |
| 91 | - // 自动同意 | |
| 92 | - if (userTaskTimeoutAction == BpmUserTaskTimeoutHandlerType.APPROVE) { | |
| 93 | - // TODO @芋艿 这个上下文如何清除呢? 任务通过后, BpmProcessInstanceEventListener 会有回调 | |
| 94 | - TenantContextHolder.setTenantId(Long.parseLong(task.getTenantId())); | |
| 95 | - TenantContextHolder.setIgnore(false); | |
| 96 | - BpmTaskApproveReqVO req = new BpmTaskApproveReqVO().setId(task.getId()) | |
| 97 | - .setReason("超时系统自动同意"); | |
| 98 | - bpmTaskService.approveTask(Long.parseLong(task.getAssignee()), req); | |
| 99 | - } | |
| 100 | - // 自动拒绝 | |
| 101 | - if (userTaskTimeoutAction == BpmUserTaskTimeoutHandlerType.REJECT) { | |
| 102 | - // TODO @芋艿 这个上下文如何清除呢? 任务拒绝后, BpmProcessInstanceEventListener 会有回调 | |
| 103 | - TenantContextHolder.setTenantId(Long.parseLong(task.getTenantId())); | |
| 104 | - TenantContextHolder.setIgnore(false); | |
| 105 | - BpmTaskRejectReqVO req = new BpmTaskRejectReqVO().setId(task.getId()).setReason("超时系统自动拒绝"); | |
| 106 | - bpmTaskService.rejectTask(Long.parseLong(task.getAssignee()), req); | |
| 107 | - } | |
| 108 | - }); | |
| 109 | - } | |
| 110 | - } | |
| 111 | -} |
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/mq/consumer/task/SysNotifyTodoTaskReminderConsumer.java deleted
| 1 | -package cn.iocoder.yudao.module.bpm.framework.flowable.core.mq.consumer.task; | |
| 2 | - | |
| 3 | -import cn.hutool.core.map.MapUtil; | |
| 4 | -import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; | |
| 5 | -import cn.iocoder.yudao.module.bpm.framework.flowable.core.mq.message.task.TodoTaskReminderMessage; | |
| 6 | -import cn.iocoder.yudao.module.system.api.notify.NotifyMessageSendApi; | |
| 7 | -import cn.iocoder.yudao.module.system.api.notify.dto.NotifySendSingleToUserReqDTO; | |
| 8 | -import jakarta.annotation.Resource; | |
| 9 | -import lombok.extern.slf4j.Slf4j; | |
| 10 | -import org.springframework.context.event.EventListener; | |
| 11 | -import org.springframework.scheduling.annotation.Async; | |
| 12 | -import org.springframework.stereotype.Component; | |
| 13 | - | |
| 14 | -import java.util.Map; | |
| 15 | - | |
| 16 | -/** | |
| 17 | - * 待办任务提醒 - 站内信的消费者 | |
| 18 | - * | |
| 19 | - * @author jason | |
| 20 | - */ | |
| 21 | -@Component | |
| 22 | -@Slf4j | |
| 23 | -public class SysNotifyTodoTaskReminderConsumer { | |
| 24 | - | |
| 25 | - private static final String TASK_REMIND_TEMPLATE_CODE = "user_task_remind"; | |
| 26 | - | |
| 27 | - @Resource | |
| 28 | - private NotifyMessageSendApi notifyMessageSendApi; | |
| 29 | - | |
| 30 | - @EventListener | |
| 31 | - @Async | |
| 32 | - public void onMessage(TodoTaskReminderMessage message) { | |
| 33 | - log.info("站内信消费者接收到消息 [消息内容({})] ", message); | |
| 34 | - TenantUtils.execute(message.getTenantId(), ()-> { | |
| 35 | - Map<String,Object> templateParams = MapUtil.newHashMap(); | |
| 36 | - templateParams.put("name", message.getTaskName()); | |
| 37 | - NotifySendSingleToUserReqDTO req = new NotifySendSingleToUserReqDTO().setUserId(message.getUserId()) | |
| 38 | - .setTemplateCode(TASK_REMIND_TEMPLATE_CODE).setTemplateParams(templateParams); | |
| 39 | - notifyMessageSendApi.sendSingleMessageToAdmin(req); | |
| 40 | - }); | |
| 41 | - } | |
| 42 | -} |
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/mq/message/task/TodoTaskReminderMessage.java deleted
| 1 | -package cn.iocoder.yudao.module.bpm.framework.flowable.core.mq.message.task; | |
| 2 | - | |
| 3 | -import jakarta.validation.constraints.NotEmpty; | |
| 4 | -import jakarta.validation.constraints.NotNull; | |
| 5 | -import lombok.Data; | |
| 6 | - | |
| 7 | -/** | |
| 8 | - * 待办任务提醒消息 | |
| 9 | - * | |
| 10 | - * @author jason | |
| 11 | - */ | |
| 12 | -@Data | |
| 13 | -public class TodoTaskReminderMessage { | |
| 14 | - | |
| 15 | - /** | |
| 16 | - * 租户 Id | |
| 17 | - */ | |
| 18 | - @NotNull(message = "租户 Id 不能未空") | |
| 19 | - private Long tenantId; | |
| 20 | - | |
| 21 | - /** | |
| 22 | - * 用户Id | |
| 23 | - */ | |
| 24 | - @NotNull(message = "用户 Id 不能未空") | |
| 25 | - private Long userId; | |
| 26 | - | |
| 27 | - /** | |
| 28 | - * 任务名称 | |
| 29 | - */ | |
| 30 | - @NotEmpty(message = "任务名称不能未空") | |
| 31 | - private String taskName; | |
| 32 | - | |
| 33 | - // TODO 暂时只有站内信通知. 后面可以增加 | |
| 34 | -} |
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/mq/producer/task/TodoTaskReminderProducer.java deleted
| 1 | -package cn.iocoder.yudao.module.bpm.framework.flowable.core.mq.producer.task; | |
| 2 | - | |
| 3 | -import cn.iocoder.yudao.module.bpm.framework.flowable.core.mq.message.task.TodoTaskReminderMessage; | |
| 4 | -import jakarta.annotation.Resource; | |
| 5 | -import jakarta.validation.Valid; | |
| 6 | -import org.springframework.context.ApplicationContext; | |
| 7 | -import org.springframework.stereotype.Component; | |
| 8 | -import org.springframework.validation.annotation.Validated; | |
| 9 | - | |
| 10 | -// TODO @jason:建议直接调用 BpmMessageService 哈;更简化一点~ | |
| 11 | -/** | |
| 12 | - * 待办任务提醒 Producer | |
| 13 | - * | |
| 14 | - * @author jason | |
| 15 | - */ | |
| 16 | -@Component | |
| 17 | -@Validated | |
| 18 | -public class TodoTaskReminderProducer { | |
| 19 | - | |
| 20 | - @Resource | |
| 21 | - private ApplicationContext applicationContext; | |
| 22 | - | |
| 23 | - public void sendReminderMessage(@Valid TodoTaskReminderMessage message) { | |
| 24 | - applicationContext.publishEvent(message); | |
| 25 | - } | |
| 26 | - | |
| 27 | -} |
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/FlowableUtils.java
| 1 | 1 | package cn.iocoder.yudao.module.bpm.framework.flowable.core.util; |
| 2 | 2 | |
| 3 | +import cn.hutool.core.util.ObjectUtil; | |
| 3 | 4 | import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; |
| 5 | +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; | |
| 4 | 6 | import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmConstants; |
| 5 | 7 | import org.flowable.common.engine.api.delegate.Expression; |
| 6 | 8 | import org.flowable.common.engine.api.variable.VariableContainer; |
| ... | ... | @@ -16,6 +18,7 @@ import org.flowable.task.api.TaskInfo; |
| 16 | 18 | import java.util.HashMap; |
| 17 | 19 | import java.util.List; |
| 18 | 20 | import java.util.Map; |
| 21 | +import java.util.Objects; | |
| 19 | 22 | |
| 20 | 23 | /** |
| 21 | 24 | * Flowable 相关的工具方法 |
| ... | ... | @@ -39,6 +42,16 @@ public class FlowableUtils { |
| 39 | 42 | return tenantId != null ? String.valueOf(tenantId) : ProcessEngineConfiguration.NO_TENANT_ID; |
| 40 | 43 | } |
| 41 | 44 | |
| 45 | + public static void execute(String tenantIdStr, Runnable runnable) { | |
| 46 | + if (ObjectUtil.isEmpty(tenantIdStr) | |
| 47 | + || Objects.equals(tenantIdStr, ProcessEngineConfiguration.NO_TENANT_ID)) { | |
| 48 | + runnable.run(); | |
| 49 | + } else { | |
| 50 | + Long tenantId = Long.valueOf(tenantIdStr); | |
| 51 | + TenantUtils.execute(tenantId, runnable); | |
| 52 | + } | |
| 53 | + } | |
| 54 | + | |
| 42 | 55 | // ========== Execution 相关的工具方法 ========== |
| 43 | 56 | |
| 44 | 57 | /** | ... | ... |
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java
| ... | ... | @@ -59,7 +59,6 @@ public class SimpleModelUtils { |
| 59 | 59 | */ |
| 60 | 60 | public static final String APPROVE_BY_RATIO_COMPLETE_EXPRESSION = "${ nrOfCompletedInstances/nrOfInstances >= %s}"; |
| 61 | 61 | |
| 62 | - // TODO-DONE @jason:建议方法名,改成 buildBpmnModel | |
| 63 | 62 | // TODO @yunai:注释需要完善下; |
| 64 | 63 | |
| 65 | 64 | /** |
| ... | ... | @@ -347,6 +346,13 @@ public class SimpleModelUtils { |
| 347 | 346 | return flowElements; |
| 348 | 347 | } |
| 349 | 348 | |
| 349 | + /** | |
| 350 | + * 添加 UserTask 用户审批的 BoundaryEvent 超时事件 | |
| 351 | + * | |
| 352 | + * @param userTask 审批任务 | |
| 353 | + * @param timeoutHandler 超时处理器 | |
| 354 | + * @return | |
| 355 | + */ | |
| 350 | 356 | private static BoundaryEvent buildUserTaskTimerBoundaryEvent(UserTask userTask, TimeoutHandler timeoutHandler) { |
| 351 | 357 | // 定时器边界事件 |
| 352 | 358 | BoundaryEvent boundaryEvent = new BoundaryEvent(); |
| ... | ... | @@ -362,6 +368,7 @@ public class SimpleModelUtils { |
| 362 | 368 | eventDefinition.setTimeCycle(String.format("R%d/%s", timeoutHandler.getMaxRemindCount(), timeoutHandler.getTimeDuration())); |
| 363 | 369 | } |
| 364 | 370 | boundaryEvent.addEventDefinition(eventDefinition); |
| 371 | + | |
| 365 | 372 | // 添加定时器边界事件类型 |
| 366 | 373 | addExtensionElement(boundaryEvent, BOUNDARY_EVENT_TYPE, USER_TASK_TIMEOUT.getType().toString()); |
| 367 | 374 | // 添加超时执行动作元素 | ... | ... |
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/message/BpmMessageService.java
| ... | ... | @@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.bpm.service.message; |
| 3 | 3 | import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenProcessInstanceApproveReqDTO; |
| 4 | 4 | import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenProcessInstanceRejectReqDTO; |
| 5 | 5 | import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenTaskCreatedReqDTO; |
| 6 | - | |
| 6 | +import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenTaskTimeoutReqDTO; | |
| 7 | 7 | import jakarta.validation.Valid; |
| 8 | 8 | |
| 9 | 9 | /** |
| ... | ... | @@ -36,4 +36,11 @@ public interface BpmMessageService { |
| 36 | 36 | */ |
| 37 | 37 | void sendMessageWhenTaskAssigned(@Valid BpmMessageSendWhenTaskCreatedReqDTO reqDTO); |
| 38 | 38 | |
| 39 | + /** | |
| 40 | + * 发送任务审批超时的消息 | |
| 41 | + * | |
| 42 | + * @param reqDTO 发送信息 | |
| 43 | + */ | |
| 44 | + void sendMessageWhenTaskTimeout(@Valid BpmMessageSendWhenTaskTimeoutReqDTO reqDTO); | |
| 45 | + | |
| 39 | 46 | } | ... | ... |
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/message/BpmMessageServiceImpl.java
| ... | ... | @@ -6,6 +6,7 @@ import cn.iocoder.yudao.module.bpm.enums.message.BpmMessageEnum; |
| 6 | 6 | import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenProcessInstanceApproveReqDTO; |
| 7 | 7 | import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenProcessInstanceRejectReqDTO; |
| 8 | 8 | import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenTaskCreatedReqDTO; |
| 9 | +import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenTaskTimeoutReqDTO; | |
| 9 | 10 | import cn.iocoder.yudao.module.system.api.sms.SmsSendApi; |
| 10 | 11 | import lombok.extern.slf4j.Slf4j; |
| 11 | 12 | import org.springframework.stereotype.Service; |
| ... | ... | @@ -61,6 +62,16 @@ public class BpmMessageServiceImpl implements BpmMessageService { |
| 61 | 62 | BpmMessageEnum.TASK_ASSIGNED.getSmsTemplateCode(), templateParams)); |
| 62 | 63 | } |
| 63 | 64 | |
| 65 | + @Override | |
| 66 | + public void sendMessageWhenTaskTimeout(BpmMessageSendWhenTaskTimeoutReqDTO reqDTO) { | |
| 67 | + Map<String, Object> templateParams = new HashMap<>(); | |
| 68 | + templateParams.put("processInstanceName", reqDTO.getProcessInstanceName()); | |
| 69 | + templateParams.put("taskName", reqDTO.getTaskName()); | |
| 70 | + templateParams.put("detailUrl", getProcessInstanceDetailUrl(reqDTO.getProcessInstanceId())); | |
| 71 | + smsSendApi.sendSingleSmsToAdmin(BpmMessageConvert.INSTANCE.convert(reqDTO.getAssigneeUserId(), | |
| 72 | + BpmMessageEnum.TASK_TIMEOUT.getSmsTemplateCode(), templateParams)); | |
| 73 | + } | |
| 74 | + | |
| 64 | 75 | private String getProcessInstanceDetailUrl(String taskId) { |
| 65 | 76 | return webProperties.getAdminUi().getUrl() + "/bpm/process-instance/detail?id=" + taskId; |
| 66 | 77 | } | ... | ... |
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/message/dto/BpmMessageSendWhenTaskTimeoutReqDTO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.bpm.service.message.dto; | |
| 2 | + | |
| 3 | +import jakarta.validation.constraints.NotEmpty; | |
| 4 | +import jakarta.validation.constraints.NotNull; | |
| 5 | +import lombok.Data; | |
| 6 | + | |
| 7 | +/** | |
| 8 | + * BPM 发送任务审批超时 Request DTO | |
| 9 | + */ | |
| 10 | +@Data | |
| 11 | +public class BpmMessageSendWhenTaskTimeoutReqDTO { | |
| 12 | + | |
| 13 | + /** | |
| 14 | + * 流程实例的编号 | |
| 15 | + */ | |
| 16 | + @NotEmpty(message = "流程实例的编号不能为空") | |
| 17 | + private String processInstanceId; | |
| 18 | + /** | |
| 19 | + * 流程实例的名字 | |
| 20 | + */ | |
| 21 | + @NotEmpty(message = "流程实例的名字不能为空") | |
| 22 | + private String processInstanceName; | |
| 23 | + | |
| 24 | + /** | |
| 25 | + * 流程任务的编号 | |
| 26 | + */ | |
| 27 | + @NotEmpty(message = "流程任务的编号不能为空") | |
| 28 | + private String taskId; | |
| 29 | + /** | |
| 30 | + * 流程任务的名字 | |
| 31 | + */ | |
| 32 | + @NotEmpty(message = "流程任务的名字不能为空") | |
| 33 | + private String taskName; | |
| 34 | + | |
| 35 | + /** | |
| 36 | + * 审批人的用户编号 | |
| 37 | + */ | |
| 38 | + @NotNull(message = "审批人的用户编号不能为空") | |
| 39 | + private Long assigneeUserId; | |
| 40 | + | |
| 41 | +} | ... | ... |
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java
| 1 | -package cn.iocoder.yudao.module.bpm.service.task; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.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.object.PageUtils; import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO; import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceCancelReqVO; import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceCreateReqVO; import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstancePageReqVO; import cn.iocoder.yudao.module.bpm.convert.task.BpmProcessInstanceConvert; import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum; import cn.iocoder.yudao.module.bpm.enums.task.BpmReasonEnum; import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.BpmTaskCandidateStartUserSelectStrategy; import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmConstants; import cn.iocoder.yudao.module.bpm.framework.flowable.core.event.BpmProcessInstanceEventPublisher; import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils; 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.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.model.BpmnModel; import org.flowable.bpmn.model.UserTask; import org.flowable.engine.HistoryService; import org.flowable.engine.RuntimeService; 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.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.module.bpm.enums.ErrorCodeConstants.*; /** * 流程实例 Service 实现类 * * ProcessDefinition & ProcessInstance & Execution & Task 的关系: * 1. <a href="https://blog.csdn.net/bobozai86/article/details/105210414" /> * * HistoricProcessInstance & ProcessInstance 的关系: * 1. <a href=" https://my.oschina.net/843294669/blog/71902" /> * * 简单来说,前者 = 历史 + 运行中的流程实例,后者仅是运行中的流程实例 * * @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 BpmProcessInstanceEventPublisher processInstanceEventPublisher; // ========== Query 查询相关方法 ========== @Override public ProcessInstance getProcessInstance(String id) { return runtimeService.createProcessInstanceQuery() .includeProcessVariables() .processInstanceId(id) .singleResult(); } @Override public List<ProcessInstance> getProcessInstances(Set<String> ids) { return runtimeService.createProcessInstanceQuery().processInstanceIds(ids).list(); } @Override public HistoricProcessInstance getHistoricProcessInstance(String id) { return historyService.createHistoricProcessInstanceQuery().processInstanceId(id).includeProcessVariables().singleResult(); } @Override public List<HistoricProcessInstance> getHistoricProcessInstances(Set<String> ids) { return historyService.createHistoricProcessInstanceQuery().processInstanceIds(ids).list(); } @Override public PageResult<HistoricProcessInstance> 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.getProcessDefinitionId())) { processInstanceQuery.processDefinitionId("%" + pageReqVO.getProcessDefinitionId() + "%"); } if (StrUtil.isNotEmpty(pageReqVO.getCategory())) { processInstanceQuery.processDefinitionCategory(pageReqVO.getCategory()); } if (pageReqVO.getStatus() != null) { processInstanceQuery.variableValueEquals(BpmConstants.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<HistoricProcessInstance> processInstanceList = processInstanceQuery.listPage(PageUtils.getStart(pageReqVO), pageReqVO.getPageSize()); return new PageResult<>(processInstanceList, processInstanceCount); } // ========== Update 写入相关方法 ========== @Override @Transactional(rollbackFor = Exception.class) public String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqVO createReqVO) { // 获得流程定义 ProcessDefinition definition = processDefinitionService.getProcessDefinition(createReqVO.getProcessDefinitionId()); // 发起流程 return createProcessInstance0(userId, definition, createReqVO.getVariables(), null, createReqVO.getStartUserSelectAssignees()); } @Override public String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqDTO createReqDTO) { // 获得流程定义 ProcessDefinition definition = processDefinitionService.getActiveProcessDefinition(createReqDTO.getProcessDefinitionKey()); // 发起流程 return createProcessInstance0(userId, definition, createReqDTO.getVariables(), createReqDTO.getBusinessKey(), createReqDTO.getStartUserSelectAssignees()); } private String createProcessInstance0(Long userId, ProcessDefinition definition, Map<String, Object> variables, String businessKey, Map<String, List<Long>> startUserSelectAssignees) { // 1.1 校验流程定义 if (definition == null) { throw exception(PROCESS_DEFINITION_NOT_EXISTS); } if (definition.isSuspended()) { throw exception(PROCESS_DEFINITION_IS_SUSPENDED); } // 1.2 校验发起人自选审批人 validateStartUserSelectAssignees(definition, startUserSelectAssignees); // 2. 创建流程实例 if (variables == null) { variables = new HashMap<>(); } FlowableUtils.filterProcessInstanceFormVariable(variables); // 过滤一下,避免 ProcessInstance 系统级的变量被占用 variables.put(BpmConstants.PROCESS_INSTANCE_VARIABLE_STATUS, // 流程实例状态:审批中 BpmProcessInstanceStatusEnum.RUNNING.getStatus()); if (CollUtil.isNotEmpty(startUserSelectAssignees)) { variables.put(BpmConstants.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<String, List<Long>> startUserSelectAssignees) { // 1. 获得发起人自选审批人的 UserTask 列表 BpmnModel bpmnModel = processDefinitionService.getProcessDefinitionBpmnModel(definition.getId()); List<UserTask> userTaskList = BpmTaskCandidateStartUserSelectStrategy.getStartUserSelectUserTaskList(bpmnModel); if (CollUtil.isEmpty(userTaskList)) { return; } // 2. 校验发起人自选审批人的 UserTask 是否都配置了 userTaskList.forEach(userTask -> { List<Long> assignees = startUserSelectAssignees != null ? startUserSelectAssignees.get(userTask.getId()) : null; if (CollUtil.isEmpty(assignees)) { throw exception(PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_CONFIG, userTask.getName()); } Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(assignees); assignees.forEach(assignee -> { if (userMap.get(assignee) == null) { throw exception(PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_EXISTS, userTask.getName(), assignee); } }); }); } @Override public void cancelProcessInstanceByStartUser(Long userId, @Valid BpmProcessInstanceCancelReqVO cancelReqVO) { // 1.1 校验流程实例存在 ProcessInstance instance = getProcessInstance(cancelReqVO.getId()); if (instance == null) { throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_EXISTS); } // 1.2 只能取消自己的 if (!Objects.equals(instance.getStartUserId(), String.valueOf(userId))) { throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_SELF); } // 2. 取消流程 updateProcessInstanceCancel(cancelReqVO.getId(), BpmReasonEnum.CANCEL_PROCESS_INSTANCE_BY_START_USER.format(cancelReqVO.getReason())); } @Override public void cancelProcessInstanceByAdmin(Long userId, BpmProcessInstanceCancelReqVO cancelReqVO) { // 1.1 校验流程实例存在 ProcessInstance instance = getProcessInstance(cancelReqVO.getId()); if (instance == null) { throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_EXISTS); } // 2. 取消流程 AdminUserRespDTO user = adminUserApi.getUser(userId); 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, BpmConstants.PROCESS_INSTANCE_VARIABLE_STATUS, BpmProcessInstanceStatusEnum.CANCEL.getStatus()); runtimeService.setVariable(id, BpmConstants.PROCESS_INSTANCE_VARIABLE_REASON, reason); // 2. 结束流程 taskService.moveTaskToEnd(id); } @Override public void updateProcessInstanceReject(ProcessInstance processInstance, String reason) { runtimeService.setVariable(processInstance.getProcessInstanceId(), BpmConstants.PROCESS_INSTANCE_VARIABLE_STATUS, BpmProcessInstanceStatusEnum.REJECT.getStatus()); runtimeService.setVariable(processInstance.getProcessInstanceId(), BpmConstants.PROCESS_INSTANCE_VARIABLE_REASON, BpmReasonEnum.REJECT_TASK.format(reason)); } // ========== Event 事件相关方法 ========== @Override public void processProcessInstanceCompleted(ProcessInstance instance) { // 1.1 获取当前状态 Integer status = (Integer) instance.getProcessVariables().get(BpmConstants.PROCESS_INSTANCE_VARIABLE_STATUS); String reason = (String) instance.getProcessVariables().get(BpmConstants.PROCESS_INSTANCE_VARIABLE_REASON); // 1.2 当流程状态还是审批状态中,说明审批通过了,则变更下它的状态 // 为什么这么处理?因为流程完成,并且完成了,说明审批通过了 if (Objects.equals(status, BpmProcessInstanceStatusEnum.RUNNING.getStatus())) { status = BpmProcessInstanceStatusEnum.APPROVE.getStatus(); runtimeService.setVariable(instance.getId(), BpmConstants.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)); } } | |
| 2 | 1 | \ No newline at end of file |
| 2 | +package cn.iocoder.yudao.module.bpm.service.task; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.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.object.PageUtils; import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO; import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceCancelReqVO; import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceCreateReqVO; import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstancePageReqVO; import cn.iocoder.yudao.module.bpm.convert.task.BpmProcessInstanceConvert; import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum; import cn.iocoder.yudao.module.bpm.enums.task.BpmReasonEnum; import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.BpmTaskCandidateStartUserSelectStrategy; import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmConstants; import cn.iocoder.yudao.module.bpm.framework.flowable.core.event.BpmProcessInstanceEventPublisher; import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils; 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.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.model.BpmnModel; import org.flowable.bpmn.model.UserTask; import org.flowable.engine.HistoryService; import org.flowable.engine.RuntimeService; 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.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.module.bpm.enums.ErrorCodeConstants.*; /** * 流程实例 Service 实现类 * * ProcessDefinition & ProcessInstance & Execution & Task 的关系: * 1. <a href="https://blog.csdn.net/bobozai86/article/details/105210414" /> * * HistoricProcessInstance & ProcessInstance 的关系: * 1. <a href=" https://my.oschina.net/843294669/blog/71902" /> * * 简单来说,前者 = 历史 + 运行中的流程实例,后者仅是运行中的流程实例 * * @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 BpmProcessInstanceEventPublisher processInstanceEventPublisher; // ========== Query 查询相关方法 ========== @Override public ProcessInstance getProcessInstance(String id) { return runtimeService.createProcessInstanceQuery() .includeProcessVariables() .processInstanceId(id) .singleResult(); } @Override public List<ProcessInstance> getProcessInstances(Set<String> ids) { return runtimeService.createProcessInstanceQuery().processInstanceIds(ids).list(); } @Override public HistoricProcessInstance getHistoricProcessInstance(String id) { return historyService.createHistoricProcessInstanceQuery().processInstanceId(id).includeProcessVariables().singleResult(); } @Override public List<HistoricProcessInstance> getHistoricProcessInstances(Set<String> ids) { return historyService.createHistoricProcessInstanceQuery().processInstanceIds(ids).list(); } @Override public PageResult<HistoricProcessInstance> 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.getProcessDefinitionId())) { processInstanceQuery.processDefinitionId("%" + pageReqVO.getProcessDefinitionId() + "%"); } if (StrUtil.isNotEmpty(pageReqVO.getCategory())) { processInstanceQuery.processDefinitionCategory(pageReqVO.getCategory()); } if (pageReqVO.getStatus() != null) { processInstanceQuery.variableValueEquals(BpmConstants.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<HistoricProcessInstance> processInstanceList = processInstanceQuery.listPage(PageUtils.getStart(pageReqVO), pageReqVO.getPageSize()); return new PageResult<>(processInstanceList, processInstanceCount); } // ========== Update 写入相关方法 ========== @Override @Transactional(rollbackFor = Exception.class) public String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqVO createReqVO) { // 获得流程定义 ProcessDefinition definition = processDefinitionService.getProcessDefinition(createReqVO.getProcessDefinitionId()); // 发起流程 return createProcessInstance0(userId, definition, createReqVO.getVariables(), null, createReqVO.getStartUserSelectAssignees()); } @Override public String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqDTO createReqDTO) { // 获得流程定义 ProcessDefinition definition = processDefinitionService.getActiveProcessDefinition(createReqDTO.getProcessDefinitionKey()); // 发起流程 return createProcessInstance0(userId, definition, createReqDTO.getVariables(), createReqDTO.getBusinessKey(), createReqDTO.getStartUserSelectAssignees()); } private String createProcessInstance0(Long userId, ProcessDefinition definition, Map<String, Object> variables, String businessKey, Map<String, List<Long>> startUserSelectAssignees) { // 1.1 校验流程定义 if (definition == null) { throw exception(PROCESS_DEFINITION_NOT_EXISTS); } if (definition.isSuspended()) { throw exception(PROCESS_DEFINITION_IS_SUSPENDED); } // 1.2 校验发起人自选审批人 validateStartUserSelectAssignees(definition, startUserSelectAssignees); // 2. 创建流程实例 if (variables == null) { variables = new HashMap<>(); } FlowableUtils.filterProcessInstanceFormVariable(variables); // 过滤一下,避免 ProcessInstance 系统级的变量被占用 variables.put(BpmConstants.PROCESS_INSTANCE_VARIABLE_STATUS, // 流程实例状态:审批中 BpmProcessInstanceStatusEnum.RUNNING.getStatus()); if (CollUtil.isNotEmpty(startUserSelectAssignees)) { variables.put(BpmConstants.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<String, List<Long>> startUserSelectAssignees) { // 1. 获得发起人自选审批人的 UserTask 列表 BpmnModel bpmnModel = processDefinitionService.getProcessDefinitionBpmnModel(definition.getId()); List<UserTask> userTaskList = BpmTaskCandidateStartUserSelectStrategy.getStartUserSelectUserTaskList(bpmnModel); if (CollUtil.isEmpty(userTaskList)) { return; } // 2. 校验发起人自选审批人的 UserTask 是否都配置了 userTaskList.forEach(userTask -> { List<Long> assignees = startUserSelectAssignees != null ? startUserSelectAssignees.get(userTask.getId()) : null; if (CollUtil.isEmpty(assignees)) { throw exception(PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_CONFIG, userTask.getName()); } Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(assignees); assignees.forEach(assignee -> { if (userMap.get(assignee) == null) { throw exception(PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_EXISTS, userTask.getName(), assignee); } }); }); } @Override public void cancelProcessInstanceByStartUser(Long userId, @Valid BpmProcessInstanceCancelReqVO cancelReqVO) { // 1.1 校验流程实例存在 ProcessInstance instance = getProcessInstance(cancelReqVO.getId()); if (instance == null) { throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_EXISTS); } // 1.2 只能取消自己的 if (!Objects.equals(instance.getStartUserId(), String.valueOf(userId))) { throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_SELF); } // 2. 取消流程 updateProcessInstanceCancel(cancelReqVO.getId(), BpmReasonEnum.CANCEL_PROCESS_INSTANCE_BY_START_USER.format(cancelReqVO.getReason())); } @Override public void cancelProcessInstanceByAdmin(Long userId, BpmProcessInstanceCancelReqVO cancelReqVO) { // 1.1 校验流程实例存在 ProcessInstance instance = getProcessInstance(cancelReqVO.getId()); if (instance == null) { throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_EXISTS); } // 2. 取消流程 AdminUserRespDTO user = adminUserApi.getUser(userId); 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, BpmConstants.PROCESS_INSTANCE_VARIABLE_STATUS, BpmProcessInstanceStatusEnum.CANCEL.getStatus()); runtimeService.setVariable(id, BpmConstants.PROCESS_INSTANCE_VARIABLE_REASON, reason); // 2. 结束流程 taskService.moveTaskToEnd(id); } @Override public void updateProcessInstanceReject(ProcessInstance processInstance, String reason) { runtimeService.setVariable(processInstance.getProcessInstanceId(), BpmConstants.PROCESS_INSTANCE_VARIABLE_STATUS, BpmProcessInstanceStatusEnum.REJECT.getStatus()); runtimeService.setVariable(processInstance.getProcessInstanceId(), BpmConstants.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(BpmConstants.PROCESS_INSTANCE_VARIABLE_STATUS); String reason = (String) instance.getProcessVariables().get(BpmConstants.PROCESS_INSTANCE_VARIABLE_REASON); // 1.2 当流程状态还是审批状态中,说明审批通过了,则变更下它的状态 // 为什么这么处理?因为流程完成,并且完成了,说明审批通过了 if (Objects.equals(status, BpmProcessInstanceStatusEnum.RUNNING.getStatus())) { status = BpmProcessInstanceStatusEnum.APPROVE.getStatus(); runtimeService.setVariable(instance.getId(), BpmConstants.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)); }); } } | |
| 3 | 3 | \ No newline at end of file | ... | ... |
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskService.java
| ... | ... | @@ -207,4 +207,13 @@ public interface BpmTaskService { |
| 207 | 207 | */ |
| 208 | 208 | void processTaskAssigned(Task task); |
| 209 | 209 | |
| 210 | + /** | |
| 211 | + * 处理 Task 审批超时事件,可能会处理多个当前审批中的任务 | |
| 212 | + * | |
| 213 | + * @param processInstanceId 流程示例编号 | |
| 214 | + * @param taskDefineKey 任务 Key | |
| 215 | + * @param taskAction 处理类型 | |
| 216 | + */ | |
| 217 | + void processTaskTimeout(String processInstanceId, String taskDefineKey, Integer taskAction); | |
| 218 | + | |
| 210 | 219 | } | ... | ... |
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java
| ... | ... | @@ -10,6 +10,7 @@ import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; |
| 10 | 10 | import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.*; |
| 11 | 11 | import cn.iocoder.yudao.module.bpm.convert.task.BpmTaskConvert; |
| 12 | 12 | import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskRejectHandlerType; |
| 13 | +import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskTimeoutHandlerType; | |
| 13 | 14 | import cn.iocoder.yudao.module.bpm.enums.task.BpmCommentTypeEnum; |
| 14 | 15 | import cn.iocoder.yudao.module.bpm.enums.task.BpmReasonEnum; |
| 15 | 16 | import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskSignTypeEnum; |
| ... | ... | @@ -19,6 +20,7 @@ import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils; |
| 19 | 20 | import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils; |
| 20 | 21 | import cn.iocoder.yudao.module.bpm.service.definition.BpmModelService; |
| 21 | 22 | import cn.iocoder.yudao.module.bpm.service.message.BpmMessageService; |
| 23 | +import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenTaskTimeoutReqDTO; | |
| 22 | 24 | import cn.iocoder.yudao.module.system.api.user.AdminUserApi; |
| 23 | 25 | import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; |
| 24 | 26 | import jakarta.annotation.Resource; |
| ... | ... | @@ -925,4 +927,43 @@ public class BpmTaskServiceImpl implements BpmTaskService { |
| 925 | 927 | }); |
| 926 | 928 | } |
| 927 | 929 | |
| 930 | + @Override | |
| 931 | + @Transactional(rollbackFor = Exception.class) | |
| 932 | + public void processTaskTimeout(String processInstanceId, String taskDefineKey, Integer taskAction) { | |
| 933 | + ProcessInstance processInstance = processInstanceService.getProcessInstance(processInstanceId); | |
| 934 | + if (processInstance == null) { | |
| 935 | + log.error("[processTaskTimeout][processInstanceId({}) 没有找到流程实例]", processInstanceId); | |
| 936 | + return; | |
| 937 | + } | |
| 938 | + List<Task> taskList = getRunningTaskListByProcessInstanceId(processInstanceId, true, taskDefineKey); | |
| 939 | + // TODO 优化:未来需要考虑加签的情况 | |
| 940 | + if (CollUtil.isEmpty(taskList)) { | |
| 941 | + log.error("[processTaskTimeout][processInstanceId({}) 定义Key({}) 没有找到任务]", processInstanceId, taskDefineKey); | |
| 942 | + return; | |
| 943 | + } | |
| 944 | + | |
| 945 | + taskList.forEach(task -> FlowableUtils.execute(task.getTenantId(), () -> { | |
| 946 | + // 情况一:自动提醒 | |
| 947 | + if (Objects.equals(taskAction, BpmUserTaskTimeoutHandlerType.REMINDER.getAction())) { | |
| 948 | + messageService.sendMessageWhenTaskTimeout(new BpmMessageSendWhenTaskTimeoutReqDTO() | |
| 949 | + .setProcessInstanceId(processInstanceId).setProcessInstanceName(processInstance.getName()) | |
| 950 | + .setTaskId(task.getId()).setTaskName(task.getName()).setAssigneeUserId(Long.parseLong(task.getAssignee()))); | |
| 951 | + return; | |
| 952 | + } | |
| 953 | + | |
| 954 | + // 情况二:自动同意 | |
| 955 | + if (Objects.equals(taskAction, BpmUserTaskTimeoutHandlerType.APPROVE.getAction())) { | |
| 956 | + approveTask(Long.parseLong(task.getAssignee()), | |
| 957 | + new BpmTaskApproveReqVO().setId(task.getId()).setReason("超时系统自动同意")); | |
| 958 | + return; | |
| 959 | + } | |
| 960 | + | |
| 961 | + // 情况三:自动拒绝 | |
| 962 | + if (Objects.equals(taskAction, BpmUserTaskTimeoutHandlerType.REJECT.getAction())) { | |
| 963 | + rejectTask(Long.parseLong(task.getAssignee()), | |
| 964 | + new BpmTaskRejectReqVO().setId(task.getId()).setReason("超时系统自动拒绝")); | |
| 965 | + } | |
| 966 | + })); | |
| 967 | + } | |
| 968 | + | |
| 928 | 969 | } | ... | ... |