Commit 3d1b66d40b57b636da7fb1fcd06be5d07aa58488

Authored by 芋道源码
Committed by Gitee
2 parents 61d312d2 9cd964a9

!1556 feat: 审批,拒绝任务时允许添加附件

Merge pull request !1556 from Jason/master-jdk17-bpm-bug-fix
yudao-dependencies/pom.xml
@@ -46,7 +46,7 @@ @@ -46,7 +46,7 @@
46 <jedis-mock.version>1.1.12</jedis-mock.version> 46 <jedis-mock.version>1.1.12</jedis-mock.version>
47 <mockito-inline.version>5.2.0</mockito-inline.version> 47 <mockito-inline.version>5.2.0</mockito-inline.version>
48 <!-- Bpm 工作流相关 --> 48 <!-- Bpm 工作流相关 -->
49 - <flowable.version>7.2.0</flowable.version> 49 + <flowable.version>8.0.0</flowable.version>
50 <!-- 工具类相关 --> 50 <!-- 工具类相关 -->
51 <anji-plus-captcha.version>1.4.0</anji-plus-captcha.version> 51 <anji-plus-captcha.version>1.4.0</anji-plus-captcha.version>
52 <jsoup.version>1.22.2</jsoup.version> 52 <jsoup.version>1.22.2</jsoup.version>
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmApprovalDetailRespVO.java
@@ -107,6 +107,9 @@ public class BpmApprovalDetailRespVO { @@ -107,6 +107,9 @@ public class BpmApprovalDetailRespVO {
107 @Schema(description = "签名", example = "https://www.iocoder.cn/sign.png") 107 @Schema(description = "签名", example = "https://www.iocoder.cn/sign.png")
108 private String signPicUrl; 108 private String signPicUrl;
109 109
  110 + @Schema(description = "附件", example = "[https://test.yudao.iocoder.cn/20260609/test.txt]")
  111 + private List<String> attachments;
  112 +
110 } 113 }
111 114
112 } 115 }
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskApproveReqVO.java
@@ -21,6 +21,9 @@ public class BpmTaskApproveReqVO { @@ -21,6 +21,9 @@ public class BpmTaskApproveReqVO {
21 @Schema(description = "签名", example = "https://www.iocoder.cn/sign.png") 21 @Schema(description = "签名", example = "https://www.iocoder.cn/sign.png")
22 private String signPicUrl; 22 private String signPicUrl;
23 23
  24 + @Schema(description = "附件", example = "[https://test.yudao.iocoder.cn/20260609/test.txt]")
  25 + private List<String> attachments;
  26 +
24 @Schema(description = "变量实例(动态表单)", requiredMode = Schema.RequiredMode.REQUIRED) 27 @Schema(description = "变量实例(动态表单)", requiredMode = Schema.RequiredMode.REQUIRED)
25 private Map<String, Object> variables; 28 private Map<String, Object> variables;
26 29
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskRejectReqVO.java
@@ -5,6 +5,8 @@ import lombok.Data; @@ -5,6 +5,8 @@ import lombok.Data;
5 5
6 import jakarta.validation.constraints.NotEmpty; 6 import jakarta.validation.constraints.NotEmpty;
7 7
  8 +import java.util.List;
  9 +
8 @Schema(description = "管理后台 - 不通过流程任务的 Request VO") 10 @Schema(description = "管理后台 - 不通过流程任务的 Request VO")
9 @Data 11 @Data
10 public class BpmTaskRejectReqVO { 12 public class BpmTaskRejectReqVO {
@@ -16,4 +18,7 @@ public class BpmTaskRejectReqVO { @@ -16,4 +18,7 @@ public class BpmTaskRejectReqVO {
16 @Schema(description = "审批意见", requiredMode = Schema.RequiredMode.REQUIRED, example = "不错不错!") 18 @Schema(description = "审批意见", requiredMode = Schema.RequiredMode.REQUIRED, example = "不错不错!")
17 private String reason; 19 private String reason;
18 20
  21 + @Schema(description = "附件", example = "[https://test.yudao.iocoder.cn/20260609/test.txt]")
  22 + private List<String> attachments;
  23 +
19 } 24 }
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmProcessInstanceConvert.java
@@ -9,6 +9,7 @@ import cn.iocoder.yudao.framework.common.util.collection.SetUtils; @@ -9,6 +9,7 @@ import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
9 import cn.iocoder.yudao.framework.common.util.date.DateUtils; 9 import cn.iocoder.yudao.framework.common.util.date.DateUtils;
10 import cn.iocoder.yudao.framework.common.util.number.NumberUtils; 10 import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
11 import cn.iocoder.yudao.framework.common.util.object.BeanUtils; 11 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  12 +import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEvent;
12 import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO; 13 import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO;
13 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO; 14 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO;
14 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO; 15 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO;
@@ -21,7 +22,6 @@ import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.BpmTaskRespVO; @@ -21,7 +22,6 @@ import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.BpmTaskRespVO;
21 import cn.iocoder.yudao.module.bpm.convert.definition.BpmProcessDefinitionConvert; 22 import cn.iocoder.yudao.module.bpm.convert.definition.BpmProcessDefinitionConvert;
22 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO; 23 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO;
23 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO; 24 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
24 -import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEvent;  
25 import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum; 25 import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
26 import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants; 26 import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants;
27 import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils; 27 import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
@@ -34,6 +34,7 @@ import org.flowable.bpmn.model.BpmnModel; @@ -34,6 +34,7 @@ import org.flowable.bpmn.model.BpmnModel;
34 import org.flowable.engine.history.HistoricProcessInstance; 34 import org.flowable.engine.history.HistoricProcessInstance;
35 import org.flowable.engine.repository.ProcessDefinition; 35 import org.flowable.engine.repository.ProcessDefinition;
36 import org.flowable.engine.runtime.ProcessInstance; 36 import org.flowable.engine.runtime.ProcessInstance;
  37 +import org.flowable.engine.task.Attachment;
37 import org.flowable.task.api.Task; 38 import org.flowable.task.api.Task;
38 import org.flowable.task.api.history.HistoricTaskInstance; 39 import org.flowable.task.api.history.HistoricTaskInstance;
39 import org.mapstruct.Mapper; 40 import org.mapstruct.Mapper;
@@ -82,7 +83,7 @@ public interface BpmProcessInstanceConvert { @@ -82,7 +83,7 @@ public interface BpmProcessInstanceConvert {
82 if (CollUtil.isNotEmpty(respVO.getTasks())) { 83 if (CollUtil.isNotEmpty(respVO.getTasks())) {
83 respVO.getTasks().forEach(task -> { 84 respVO.getTasks().forEach(task -> {
84 AdminUserRespDTO assigneeUser = userMap.get(task.getAssignee()); 85 AdminUserRespDTO assigneeUser = userMap.get(task.getAssignee());
85 - if (assigneeUser!= null) { 86 + if (assigneeUser != null) {
86 task.setAssigneeUser(BeanUtils.toBean(assigneeUser, UserSimpleBaseVO.class)); 87 task.setAssigneeUser(BeanUtils.toBean(assigneeUser, UserSimpleBaseVO.class));
87 MapUtils.findAndThen(deptMap, assigneeUser.getDeptId(), dept -> task.getAssigneeUser().setDeptName(dept.getName())); 88 MapUtils.findAndThen(deptMap, assigneeUser.getDeptId(), dept -> task.getAssigneeUser().setDeptName(dept.getName()));
88 } 89 }
@@ -137,10 +138,10 @@ public interface BpmProcessInstanceConvert { @@ -137,10 +138,10 @@ public interface BpmProcessInstanceConvert {
137 138
138 default BpmMessageSendWhenProcessInstanceRejectReqDTO buildProcessInstanceRejectMessage(ProcessInstance instance, String reason) { 139 default BpmMessageSendWhenProcessInstanceRejectReqDTO buildProcessInstanceRejectMessage(ProcessInstance instance, String reason) {
139 return new BpmMessageSendWhenProcessInstanceRejectReqDTO() 140 return new BpmMessageSendWhenProcessInstanceRejectReqDTO()
140 - .setProcessInstanceName(instance.getName())  
141 - .setProcessInstanceId(instance.getId())  
142 - .setReason(reason)  
143 - .setStartUserId(NumberUtils.parseLong(instance.getStartUserId())); 141 + .setProcessInstanceName(instance.getName())
  142 + .setProcessInstanceId(instance.getId())
  143 + .setReason(reason)
  144 + .setStartUserId(NumberUtils.parseLong(instance.getStartUserId()));
144 } 145 }
145 146
146 default BpmProcessInstanceBpmnModelViewRespVO buildProcessInstanceBpmnModelView(HistoricProcessInstance processInstance, 147 default BpmProcessInstanceBpmnModelViewRespVO buildProcessInstanceBpmnModelView(HistoricProcessInstance processInstance,
@@ -183,8 +184,8 @@ public interface BpmProcessInstanceConvert { @@ -183,8 +184,8 @@ public interface BpmProcessInstanceConvert {
183 } 184 }
184 185
185 default UserSimpleBaseVO buildUser(Long userId, 186 default UserSimpleBaseVO buildUser(Long userId,
186 - Map<Long, AdminUserRespDTO> userMap,  
187 - Map<Long, DeptRespDTO> deptMap) { 187 + Map<Long, AdminUserRespDTO> userMap,
  188 + Map<Long, DeptRespDTO> deptMap) {
188 if (userId == null) { 189 if (userId == null) {
189 return null; 190 return null;
190 } 191 }
@@ -200,13 +201,14 @@ public interface BpmProcessInstanceConvert { @@ -200,13 +201,14 @@ public interface BpmProcessInstanceConvert {
200 return userVO; 201 return userVO;
201 } 202 }
202 203
203 - default BpmApprovalDetailRespVO.ActivityNodeTask buildApprovalTaskInfo(HistoricTaskInstance task) { 204 + default BpmApprovalDetailRespVO.ActivityNodeTask buildApprovalTaskInfo(HistoricTaskInstance task, List<Attachment> attachments) {
204 if (task == null) { 205 if (task == null) {
205 return null; 206 return null;
206 } 207 }
207 return BeanUtils.toBean(task, BpmApprovalDetailRespVO.ActivityNodeTask.class) 208 return BeanUtils.toBean(task, BpmApprovalDetailRespVO.ActivityNodeTask.class)
208 .setStatus(FlowableUtils.getTaskStatus(task)).setReason(FlowableUtils.getTaskReason(task)) 209 .setStatus(FlowableUtils.getTaskStatus(task)).setReason(FlowableUtils.getTaskReason(task))
209 - .setSignPicUrl(FlowableUtils.getTaskSignPicUrl(task)); 210 + .setSignPicUrl(FlowableUtils.getTaskSignPicUrl(task))
  211 + .setAttachments(convertList(attachments, Attachment::getUrl));
210 } 212 }
211 213
212 default Set<Long> parseUserIds(HistoricProcessInstance processInstance, 214 default Set<Long> parseUserIds(HistoricProcessInstance processInstance,
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmAttachmentTypeEnum.java 0 → 100644
  1 +package cn.iocoder.yudao.module.bpm.enums.task;
  2 +
  3 +import lombok.AllArgsConstructor;
  4 +import lombok.Getter;
  5 +
  6 +/**
  7 + * 附件类型枚举
  8 + *
  9 + * @author jason
  10 + */
  11 +@Getter
  12 +@AllArgsConstructor
  13 +public enum BpmAttachmentTypeEnum {
  14 +
  15 + TASK_ATTACHMENT("1", "用户任务附件");
  16 +
  17 + /**
  18 + * 操作类型
  19 + * <p>
  20 + * 由于 BPM attachment 类型为 String,所以这里就不使用 Integer
  21 + */
  22 + private final String type;
  23 + /**
  24 + * 操作名字
  25 + */
  26 + private final String name;
  27 +}
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java
@@ -25,6 +25,7 @@ import cn.iocoder.yudao.module.bpm.dal.redis.BpmProcessIdRedisDAO; @@ -25,6 +25,7 @@ import cn.iocoder.yudao.module.bpm.dal.redis.BpmProcessIdRedisDAO;
25 import cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants; 25 import cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants;
26 import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum; 26 import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum;
27 import cn.iocoder.yudao.module.bpm.enums.definition.BpmSimpleModelNodeTypeEnum; 27 import cn.iocoder.yudao.module.bpm.enums.definition.BpmSimpleModelNodeTypeEnum;
  28 +import cn.iocoder.yudao.module.bpm.enums.task.BpmAttachmentTypeEnum;
28 import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum; 29 import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum;
29 import cn.iocoder.yudao.module.bpm.enums.task.BpmReasonEnum; 30 import cn.iocoder.yudao.module.bpm.enums.task.BpmReasonEnum;
30 import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum; 31 import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
@@ -57,6 +58,7 @@ import org.flowable.engine.repository.ProcessDefinition; @@ -57,6 +58,7 @@ import org.flowable.engine.repository.ProcessDefinition;
57 import org.flowable.engine.runtime.Execution; 58 import org.flowable.engine.runtime.Execution;
58 import org.flowable.engine.runtime.ProcessInstance; 59 import org.flowable.engine.runtime.ProcessInstance;
59 import org.flowable.engine.runtime.ProcessInstanceBuilder; 60 import org.flowable.engine.runtime.ProcessInstanceBuilder;
  61 +import org.flowable.engine.task.Attachment;
60 import org.flowable.task.api.Task; 62 import org.flowable.task.api.Task;
61 import org.flowable.task.api.history.HistoricTaskInstance; 63 import org.flowable.task.api.history.HistoricTaskInstance;
62 import org.springframework.context.annotation.Lazy; 64 import org.springframework.context.annotation.Lazy;
@@ -404,8 +406,13 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService @@ -404,8 +406,13 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
404 // 遍历 tasks 列表,只处理已结束的 UserTask 406 // 遍历 tasks 列表,只处理已结束的 UserTask
405 // 为什么不通过 activities 呢?因为,加签场景下,它只存在于 tasks,没有 activities,导致如果遍历 activities 的话,它无法成为一个节点 407 // 为什么不通过 activities 呢?因为,加签场景下,它只存在于 tasks,没有 activities,导致如果遍历 activities 的话,它无法成为一个节点
406 List<HistoricTaskInstance> endTasks = filterList(tasks, task -> task.getEndTime() != null); 408 List<HistoricTaskInstance> endTasks = filterList(tasks, task -> task.getEndTime() != null);
  409 + // 获取已完成节点的附件
  410 + Set<String> endTaskIds = convertSet(endTasks, HistoricTaskInstance::getId);
  411 + List<Attachment> attachments = taskService.getAttachments(historicProcessInstance.getId(), endTaskIds, BpmAttachmentTypeEnum.TASK_ATTACHMENT);
  412 + Map<String, List<Attachment>> taskAttachmentMap = convertMultiMap(attachments, Attachment::getTaskId);
407 List<ActivityNode> approvalNodes = convertList(endTasks, task -> { 413 List<ActivityNode> approvalNodes = convertList(endTasks, task -> {
408 FlowElement flowNode = BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey()); 414 FlowElement flowNode = BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey());
  415 + List<Attachment> taskAttachments = taskAttachmentMap.get(task.getId());
409 ActivityNode activityNode = new ActivityNode().setId(task.getTaskDefinitionKey()).setName(task.getName()) 416 ActivityNode activityNode = new ActivityNode().setId(task.getTaskDefinitionKey()).setName(task.getName())
410 .setNodeType(START_USER_NODE_ID.equals(task.getTaskDefinitionKey()) 417 .setNodeType(START_USER_NODE_ID.equals(task.getTaskDefinitionKey())
411 ? BpmSimpleModelNodeTypeEnum.START_USER_NODE.getType() 418 ? BpmSimpleModelNodeTypeEnum.START_USER_NODE.getType()
@@ -414,7 +421,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService @@ -414,7 +421,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
414 .setStatus(getEndActivityNodeStatus(task)) 421 .setStatus(getEndActivityNodeStatus(task))
415 .setCandidateStrategy(BpmnModelUtils.parseCandidateStrategy(flowNode)) 422 .setCandidateStrategy(BpmnModelUtils.parseCandidateStrategy(flowNode))
416 .setStartTime(DateUtils.of(task.getCreateTime())).setEndTime(DateUtils.of(task.getEndTime())) 423 .setStartTime(DateUtils.of(task.getCreateTime())).setEndTime(DateUtils.of(task.getEndTime()))
417 - .setTasks(singletonList(BpmProcessInstanceConvert.INSTANCE.buildApprovalTaskInfo(task))); 424 + .setTasks(singletonList(BpmProcessInstanceConvert.INSTANCE.buildApprovalTaskInfo(task, taskAttachments)));
418 // 如果是取消状态,则跳过 425 // 如果是取消状态,则跳过
419 if (BpmTaskStatusEnum.isCancelStatus(activityNode.getStatus())) { 426 if (BpmTaskStatusEnum.isCancelStatus(activityNode.getStatus())) {
420 return null; 427 return null;
@@ -528,14 +535,14 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService @@ -528,14 +535,14 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
528 if (task == null) { 535 if (task == null) {
529 continue; 536 continue;
530 } 537 }
531 - activityNode.getTasks().add(BpmProcessInstanceConvert.INSTANCE.buildApprovalTaskInfo(task)); 538 + activityNode.getTasks().add(BpmProcessInstanceConvert.INSTANCE.buildApprovalTaskInfo(task, null));
532 // 加签子任务,需要过滤掉已经完成的加签子任务 539 // 加签子任务,需要过滤掉已经完成的加签子任务
533 List<HistoricTaskInstance> childrenTasks = filterList( 540 List<HistoricTaskInstance> childrenTasks = filterList(
534 taskService.getAllChildrenTaskListByParentTaskId(activity.getTaskId(), tasks), 541 taskService.getAllChildrenTaskListByParentTaskId(activity.getTaskId(), tasks),
535 childTask -> childTask.getEndTime() == null); 542 childTask -> childTask.getEndTime() == null);
536 if (CollUtil.isNotEmpty(childrenTasks)) { 543 if (CollUtil.isNotEmpty(childrenTasks)) {
537 activityNode.getTasks().addAll( 544 activityNode.getTasks().addAll(
538 - convertList(childrenTasks, BpmProcessInstanceConvert.INSTANCE::buildApprovalTaskInfo)); 545 + convertList(childrenTasks, item->BpmProcessInstanceConvert.INSTANCE.buildApprovalTaskInfo(item, null)));
539 } 546 }
540 } 547 }
541 // 处理每个任务的 candidateUsers 属性:如果是依次审批,需要预测它的后续审批人。因为 Task 是审批完一个,创建一个新的 Task 548 // 处理每个任务的 candidateUsers 属性:如果是依次审批,需要预测它的后续审批人。因为 Task 是审批完一个,创建一个新的 Task
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskService.java
@@ -4,9 +4,11 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult; @@ -4,9 +4,11 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; 4 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
5 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.*; 5 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.*;
6 import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskTimeoutHandlerTypeEnum; 6 import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskTimeoutHandlerTypeEnum;
  7 +import cn.iocoder.yudao.module.bpm.enums.task.BpmAttachmentTypeEnum;
7 import jakarta.validation.Valid; 8 import jakarta.validation.Valid;
8 import org.flowable.bpmn.model.UserTask; 9 import org.flowable.bpmn.model.UserTask;
9 import org.flowable.engine.history.HistoricActivityInstance; 10 import org.flowable.engine.history.HistoricActivityInstance;
  11 +import org.flowable.engine.task.Attachment;
10 import org.flowable.task.api.Task; 12 import org.flowable.task.api.Task;
11 import org.flowable.task.api.TaskInfo; 13 import org.flowable.task.api.TaskInfo;
12 import org.flowable.task.api.history.HistoricTaskInstance; 14 import org.flowable.task.api.history.HistoricTaskInstance;
@@ -14,6 +16,7 @@ import org.flowable.task.api.history.HistoricTaskInstance; @@ -14,6 +16,7 @@ import org.flowable.task.api.history.HistoricTaskInstance;
14 import java.util.Collection; 16 import java.util.Collection;
15 import java.util.List; 17 import java.util.List;
16 import java.util.Map; 18 import java.util.Map;
  19 +import java.util.Set;
17 20
18 /** 21 /**
19 * 流程任务实例 Service 接口 22 * 流程任务实例 Service 接口
@@ -184,6 +187,17 @@ public interface BpmTaskService { @@ -184,6 +187,17 @@ public interface BpmTaskService {
184 */ 187 */
185 List<HistoricTaskInstance> getFinishedTaskListByProcessInstanceIdWithoutCancel(String processInstanceId); 188 List<HistoricTaskInstance> getFinishedTaskListByProcessInstanceIdWithoutCancel(String processInstanceId);
186 189
  190 +
  191 + /**
  192 + * 根据条件获取附件
  193 + *
  194 + * @param processInstanceId 流程 id
  195 + * @param taskIds 任务 id 集合
  196 + * @param attachmentType 附件类型
  197 + * @return 附件集合
  198 + */
  199 + List<Attachment> getAttachments(String processInstanceId, Set<String> taskIds, BpmAttachmentTypeEnum attachmentType);
  200 +
187 // ========== Update 写入相关方法 ========== 201 // ========== Update 写入相关方法 ==========
188 202
189 /** 203 /**
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java
@@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.bpm.service.task; @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.bpm.service.task;
2 2
3 import cn.hutool.core.collection.CollUtil; 3 import cn.hutool.core.collection.CollUtil;
4 import cn.hutool.core.convert.Convert; 4 import cn.hutool.core.convert.Convert;
  5 +import cn.hutool.core.io.FileUtil;
5 import cn.hutool.core.lang.Assert; 6 import cn.hutool.core.lang.Assert;
6 import cn.hutool.core.util.*; 7 import cn.hutool.core.util.*;
7 import cn.hutool.extra.spring.SpringUtil; 8 import cn.hutool.extra.spring.SpringUtil;
@@ -19,10 +20,7 @@ import cn.iocoder.yudao.module.bpm.convert.task.BpmTaskConvert; @@ -19,10 +20,7 @@ import cn.iocoder.yudao.module.bpm.convert.task.BpmTaskConvert;
19 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO; 20 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO;
20 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO; 21 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
21 import cn.iocoder.yudao.module.bpm.enums.definition.*; 22 import cn.iocoder.yudao.module.bpm.enums.definition.*;
22 -import cn.iocoder.yudao.module.bpm.enums.task.BpmCommentTypeEnum;  
23 -import cn.iocoder.yudao.module.bpm.enums.task.BpmReasonEnum;  
24 -import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskSignTypeEnum;  
25 -import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum; 23 +import cn.iocoder.yudao.module.bpm.enums.task.*;
26 import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; 24 import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
27 import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants; 25 import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants;
28 import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmHttpRequestUtils; 26 import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmHttpRequestUtils;
@@ -49,6 +47,7 @@ import org.flowable.engine.history.HistoricActivityInstance; @@ -49,6 +47,7 @@ import org.flowable.engine.history.HistoricActivityInstance;
49 import org.flowable.engine.runtime.ActivityInstance; 47 import org.flowable.engine.runtime.ActivityInstance;
50 import org.flowable.engine.runtime.Execution; 48 import org.flowable.engine.runtime.Execution;
51 import org.flowable.engine.runtime.ProcessInstance; 49 import org.flowable.engine.runtime.ProcessInstance;
  50 +import org.flowable.engine.task.Attachment;
52 import org.flowable.task.api.DelegationState; 51 import org.flowable.task.api.DelegationState;
53 import org.flowable.task.api.Task; 52 import org.flowable.task.api.Task;
54 import org.flowable.task.api.TaskInfo; 53 import org.flowable.task.api.TaskInfo;
@@ -510,6 +509,18 @@ public class BpmTaskServiceImpl implements BpmTaskService { @@ -510,6 +509,18 @@ public class BpmTaskServiceImpl implements BpmTaskService {
510 .orderByHistoricTaskInstanceStartTime().asc().list(); 509 .orderByHistoricTaskInstanceStartTime().asc().list();
511 } 510 }
512 511
  512 + @Override
  513 + public List<Attachment> getAttachments(String processInstanceId, Set<String> taskIds, BpmAttachmentTypeEnum attachmentType) {
  514 + List<Attachment> result = taskService.getProcessInstanceAttachments(processInstanceId);
  515 + if (CollUtil.isNotEmpty(taskIds)) {
  516 + result = filterList(result, attachment -> taskIds.contains(attachment.getTaskId()));
  517 + }
  518 + if (attachmentType != null) {
  519 + result = filterList(result, attachment -> attachmentType.getType().equals(attachment.getType()));
  520 + }
  521 + return result;
  522 + }
  523 +
513 /** 524 /**
514 * 判断指定用户,是否是当前任务的审批人 525 * 判断指定用户,是否是当前任务的审批人
515 * 526 *
@@ -592,6 +603,11 @@ public class BpmTaskServiceImpl implements BpmTaskService { @@ -592,6 +603,11 @@ public class BpmTaskServiceImpl implements BpmTaskService {
592 // 2.2 添加评论 603 // 2.2 添加评论
593 taskService.addComment(task.getId(), task.getProcessInstanceId(), BpmCommentTypeEnum.APPROVE.getType(), 604 taskService.addComment(task.getId(), task.getProcessInstanceId(), BpmCommentTypeEnum.APPROVE.getType(),
594 BpmCommentTypeEnum.APPROVE.formatComment(reqVO.getReason())); 605 BpmCommentTypeEnum.APPROVE.formatComment(reqVO.getReason()));
  606 + // 2.3 添加附件
  607 + if (CollUtil.isNotEmpty(reqVO.getAttachments())) {
  608 + reqVO.getAttachments().forEach(attachment -> taskService.createAttachment(BpmAttachmentTypeEnum.TASK_ATTACHMENT.getType(),
  609 + task.getId(), task.getProcessInstanceId(), FileUtil.getName(URLUtil.getPath(attachment)), null, attachment));
  610 + }
595 611
596 // 3. 设置流程变量。如果流程变量前端传空,需要从历史实例中获取,原因:前端表单如果在当前节点无可编辑的字段时 variables 一定会为空 612 // 3. 设置流程变量。如果流程变量前端传空,需要从历史实例中获取,原因:前端表单如果在当前节点无可编辑的字段时 variables 一定会为空
597 // 场景一:A 节点发起,B 节点表单无可编辑字段,审批通过时,C 节点需要流程变量获取下一个执行节点,但因为 B 节点无可编辑的字段,variables 为空,流程可能出现问题。 613 // 场景一:A 节点发起,B 节点表单无可编辑字段,审批通过时,C 节点需要流程变量获取下一个执行节点,但因为 B 节点无可编辑的字段,variables 为空,流程可能出现问题。
@@ -817,7 +833,13 @@ public class BpmTaskServiceImpl implements BpmTaskService { @@ -817,7 +833,13 @@ public class BpmTaskServiceImpl implements BpmTaskService {
817 // 2.2 添加流程评论 833 // 2.2 添加流程评论
818 taskService.addComment(task.getId(), task.getProcessInstanceId(), BpmCommentTypeEnum.REJECT.getType(), 834 taskService.addComment(task.getId(), task.getProcessInstanceId(), BpmCommentTypeEnum.REJECT.getType(),
819 BpmCommentTypeEnum.REJECT.formatComment(reqVO.getReason())); 835 BpmCommentTypeEnum.REJECT.formatComment(reqVO.getReason()));
820 - // 2.3 如果当前任务时被加签的,则加它的根任务也标记成未通过 836 + // 2.3 添加附件
  837 + if (CollUtil.isNotEmpty(reqVO.getAttachments())) {
  838 + reqVO.getAttachments().forEach(attachment -> taskService.createAttachment(BpmAttachmentTypeEnum.TASK_ATTACHMENT.getType(),
  839 + task.getId(), task.getProcessInstanceId(), FileUtil.getName(URLUtil.getPath(attachment)), null, attachment));
  840 + }
  841 +
  842 + // 2.4 如果当前任务时被加签的,则加它的根任务也标记成未通过
821 // 疑问:为什么要标记未通过呢? 843 // 疑问:为什么要标记未通过呢?
822 // 回答:例如说 A 任务被向前加签除 B 任务时,B 任务被审批不通过,此时 A 会被取消。而 yudao-ui-admin-vue3 不展示“已取消”的任务,导致展示不出审批不通过的细节。 844 // 回答:例如说 A 任务被向前加签除 B 任务时,B 任务被审批不通过,此时 A 会被取消。而 yudao-ui-admin-vue3 不展示“已取消”的任务,导致展示不出审批不通过的细节。
823 if (task.getParentTaskId() != null) { 845 if (task.getParentTaskId() != null) {
@@ -921,7 +943,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { @@ -921,7 +943,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
921 * @param reqVO 前端参数封装 943 * @param reqVO 前端参数封装
922 */ 944 */
923 public void returnTask(Long userId, BpmnModel bpmnModel, Task currentTask, FlowElement targetElement, BpmTaskReturnReqVO reqVO) { 945 public void returnTask(Long userId, BpmnModel bpmnModel, Task currentTask, FlowElement targetElement, BpmTaskReturnReqVO reqVO) {
924 - // 1. 获得所有需要回撤的任务 taskDefinitionKey,用于稍后的 moveExecutionsToSingleActivityId 回撤 946 + // 1. 获得所有需要回撤的任务 taskDefinitionKey,用于稍后的 moveActivityIdsToSingleActivityId 回撤
925 // 1.1 获取所有正常进行的任务节点 Key 947 // 1.1 获取所有正常进行的任务节点 Key
926 List<Task> taskList = taskService.createTaskQuery().processInstanceId(currentTask.getProcessInstanceId()).list(); 948 List<Task> taskList = taskService.createTaskQuery().processInstanceId(currentTask.getProcessInstanceId()).list();
927 List<String> runTaskKeyList = convertList(taskList, Task::getTaskDefinitionKey); 949 List<String> runTaskKeyList = convertList(taskList, Task::getTaskDefinitionKey);
@@ -929,16 +951,12 @@ public class BpmTaskServiceImpl implements BpmTaskService { @@ -929,16 +951,12 @@ public class BpmTaskServiceImpl implements BpmTaskService {
929 // 为什么不直接使用 runTaskKeyList 呢?因为可能存在多个审批分支,例如说:A -> B -> C 和 D -> F,而只要 C 撤回到 A,需要排除掉 F 951 // 为什么不直接使用 runTaskKeyList 呢?因为可能存在多个审批分支,例如说:A -> B -> C 和 D -> F,而只要 C 撤回到 A,需要排除掉 F
930 List<UserTask> returnUserTaskList = BpmnModelUtils.iteratorFindChildUserTasks(targetElement, runTaskKeyList, null, null); 952 List<UserTask> returnUserTaskList = BpmnModelUtils.iteratorFindChildUserTasks(targetElement, runTaskKeyList, null, null);
931 List<String> returnTaskKeyList = convertList(returnUserTaskList, UserTask::getId); 953 List<String> returnTaskKeyList = convertList(returnUserTaskList, UserTask::getId);
932 - List<String> runExecutionIds = new ArrayList<>();  
933 // 2. 给当前要被退回的 task 数组,设置退回意见 954 // 2. 给当前要被退回的 task 数组,设置退回意见
934 taskList.forEach(task -> { 955 taskList.forEach(task -> {
935 // 需要排除掉,不需要设置退回意见的任务 956 // 需要排除掉,不需要设置退回意见的任务
936 if (!returnTaskKeyList.contains(task.getTaskDefinitionKey())) { 957 if (!returnTaskKeyList.contains(task.getTaskDefinitionKey())) {
937 return; 958 return;
938 } 959 }
939 - if (task.getExecutionId() != null) {  
940 - runExecutionIds.add(task.getExecutionId());  
941 - }  
942 // 判断是否分配给自己任务,因为会签任务,一个节点会有多个任务 960 // 判断是否分配给自己任务,因为会签任务,一个节点会有多个任务
943 if (isAssignUserTask(userId, task)) { // 情况一:自己的任务,进行 RETURN 标记 961 if (isAssignUserTask(userId, task)) { // 情况一:自己的任务,进行 RETURN 标记
944 // 2.1.1 添加评论 962 // 2.1.1 添加评论
@@ -955,21 +973,17 @@ public class BpmTaskServiceImpl implements BpmTaskService { @@ -955,21 +973,17 @@ public class BpmTaskServiceImpl implements BpmTaskService {
955 Set<String> needSimulateTaskDefinitionKeys = getNeedSimulateTaskDefinitionKeys(bpmnModel, currentTask, targetElement); 973 Set<String> needSimulateTaskDefinitionKeys = getNeedSimulateTaskDefinitionKeys(bpmnModel, currentTask, targetElement);
956 974
957 // 4. 执行驳回 975 // 4. 执行驳回
958 - // 4.1 校验是否有可回撤的 execution,避免 moveExecutionsToSingleActivityId 传入空集合时 Flowable 内部报错  
959 - if (CollUtil.isEmpty(runExecutionIds)) {  
960 - throw exception(TASK_RETURN_FAIL_SOURCE_TARGET_ERROR);  
961 - }  
962 - // 4.2 执行驳回  
963 // ① 使用 moveExecutionsToSingleActivityId 替换 moveActivityIdsToSingleActivityId。原因:当多实例任务回退的时候有问题。 976 // ① 使用 moveExecutionsToSingleActivityId 替换 moveActivityIdsToSingleActivityId。原因:当多实例任务回退的时候有问题。
964 // 相关 issue: https://github.com/flowable/flowable-engine/issues/3944 977 // 相关 issue: https://github.com/flowable/flowable-engine/issues/3944
965 // ② flowable 7.2.0 版本后,继续使用 moveActivityIdsToSingleActivityId 方法。原因:flowable 7.2.0 版本修复了该问题。 978 // ② flowable 7.2.0 版本后,继续使用 moveActivityIdsToSingleActivityId 方法。原因:flowable 7.2.0 版本修复了该问题。
966 // 相关 issue:https://github.com/YunaiV/ruoyi-vue-pro/issues/1018 979 // 相关 issue:https://github.com/YunaiV/ruoyi-vue-pro/issues/1018
967 // ③ moveActivityIdsToSingleActivityId 使用遇到问题, 相关 issue https://gitee.com/zhijiantianya/yudao-cloud/issues/IJM8MS 980 // ③ moveActivityIdsToSingleActivityId 使用遇到问题, 相关 issue https://gitee.com/zhijiantianya/yudao-cloud/issues/IJM8MS
968 // 改成 moveExecutionsToSingleActivityId 好像并没有遇到 ② 提到的超时提醒失效的问题。暂时先改回 moveExecutionsToSingleActivityId 981 // 改成 moveExecutionsToSingleActivityId 好像并没有遇到 ② 提到的超时提醒失效的问题。暂时先改回 moveExecutionsToSingleActivityId
969 - // 目前还有的相关问题 https://t.zsxq.com/z4d9i。 估计需要升级 flowable 8 版本试试 982 + // ④ moveExecutionsToSingleActivityId 回退多实例的时候不会去删除多实例根, 应改成 moveActivityIdsToSingleActivityId
  983 + // flowable 8.0.0 修复上面相关问题, 还修复了并行分支回退的问题 https://t.zsxq.com/z4d9i。
970 runtimeService.createChangeActivityStateBuilder() 984 runtimeService.createChangeActivityStateBuilder()
971 .processInstanceId(currentTask.getProcessInstanceId()) 985 .processInstanceId(currentTask.getProcessInstanceId())
972 - .moveExecutionsToSingleActivityId(runExecutionIds, reqVO.getTargetTaskDefinitionKey()) 986 + .moveActivityIdsToSingleActivityId(returnTaskKeyList, reqVO.getTargetTaskDefinitionKey())
973 // 设置需要预测的任务 ids 的流程变量,用于辅助预测 987 // 设置需要预测的任务 ids 的流程变量,用于辅助预测
974 .processVariable(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_NEED_SIMULATE_TASK_IDS, needSimulateTaskDefinitionKeys) 988 .processVariable(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_NEED_SIMULATE_TASK_IDS, needSimulateTaskDefinitionKeys)
975 // 设置流程变量节点退回标记, 用于退回到节点,不执行 BpmUserTaskAssignStartUserHandlerTypeEnum 策略,导致自动通过 989 // 设置流程变量节点退回标记, 用于退回到节点,不执行 BpmUserTaskAssignStartUserHandlerTypeEnum 策略,导致自动通过