parseUserIds(HistoricProcessInstance processInstance,
diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmAttachmentTypeEnum.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmAttachmentTypeEnum.java
new file mode 100644
index 0000000..fa1bf26
--- /dev/null
+++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmAttachmentTypeEnum.java
@@ -0,0 +1,27 @@
+package cn.iocoder.yudao.module.bpm.enums.task;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 附件类型枚举
+ *
+ * @author jason
+ */
+@Getter
+@AllArgsConstructor
+public enum BpmAttachmentTypeEnum {
+
+ TASK_ATTACHMENT("1", "用户任务附件");
+
+ /**
+ * 操作类型
+ *
+ * 由于 BPM attachment 类型为 String,所以这里就不使用 Integer
+ */
+ private final String type;
+ /**
+ * 操作名字
+ */
+ private final String name;
+}
diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java
index 5344dd7..1205bb1 100644
--- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java
+++ b/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;
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.BpmAttachmentTypeEnum;
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;
@@ -57,6 +58,7 @@ import org.flowable.engine.repository.ProcessDefinition;
import org.flowable.engine.runtime.Execution;
import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.engine.runtime.ProcessInstanceBuilder;
+import org.flowable.engine.task.Attachment;
import org.flowable.task.api.Task;
import org.flowable.task.api.history.HistoricTaskInstance;
import org.springframework.context.annotation.Lazy;
@@ -404,8 +406,13 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
// 遍历 tasks 列表,只处理已结束的 UserTask
// 为什么不通过 activities 呢?因为,加签场景下,它只存在于 tasks,没有 activities,导致如果遍历 activities 的话,它无法成为一个节点
List endTasks = filterList(tasks, task -> task.getEndTime() != null);
+ // 获取已完成节点的附件
+ Set endTaskIds = convertSet(endTasks, HistoricTaskInstance::getId);
+ List attachments = taskService.getAttachments(historicProcessInstance.getId(), endTaskIds, BpmAttachmentTypeEnum.TASK_ATTACHMENT);
+ Map> taskAttachmentMap = convertMultiMap(attachments, Attachment::getTaskId);
List approvalNodes = convertList(endTasks, task -> {
FlowElement flowNode = BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey());
+ List taskAttachments = taskAttachmentMap.get(task.getId());
ActivityNode activityNode = new ActivityNode().setId(task.getTaskDefinitionKey()).setName(task.getName())
.setNodeType(START_USER_NODE_ID.equals(task.getTaskDefinitionKey())
? BpmSimpleModelNodeTypeEnum.START_USER_NODE.getType()
@@ -414,7 +421,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
.setStatus(getEndActivityNodeStatus(task))
.setCandidateStrategy(BpmnModelUtils.parseCandidateStrategy(flowNode))
.setStartTime(DateUtils.of(task.getCreateTime())).setEndTime(DateUtils.of(task.getEndTime()))
- .setTasks(singletonList(BpmProcessInstanceConvert.INSTANCE.buildApprovalTaskInfo(task)));
+ .setTasks(singletonList(BpmProcessInstanceConvert.INSTANCE.buildApprovalTaskInfo(task, taskAttachments)));
// 如果是取消状态,则跳过
if (BpmTaskStatusEnum.isCancelStatus(activityNode.getStatus())) {
return null;
@@ -528,14 +535,14 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
if (task == null) {
continue;
}
- activityNode.getTasks().add(BpmProcessInstanceConvert.INSTANCE.buildApprovalTaskInfo(task));
+ activityNode.getTasks().add(BpmProcessInstanceConvert.INSTANCE.buildApprovalTaskInfo(task, null));
// 加签子任务,需要过滤掉已经完成的加签子任务
List childrenTasks = filterList(
taskService.getAllChildrenTaskListByParentTaskId(activity.getTaskId(), tasks),
childTask -> childTask.getEndTime() == null);
if (CollUtil.isNotEmpty(childrenTasks)) {
activityNode.getTasks().addAll(
- convertList(childrenTasks, BpmProcessInstanceConvert.INSTANCE::buildApprovalTaskInfo));
+ convertList(childrenTasks, item->BpmProcessInstanceConvert.INSTANCE.buildApprovalTaskInfo(item, null)));
}
}
// 处理每个任务的 candidateUsers 属性:如果是依次审批,需要预测它的后续审批人。因为 Task 是审批完一个,创建一个新的 Task
diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskService.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskService.java
index f7d260e..36a35b3 100644
--- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskService.java
+++ b/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;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.*;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskTimeoutHandlerTypeEnum;
+import cn.iocoder.yudao.module.bpm.enums.task.BpmAttachmentTypeEnum;
import jakarta.validation.Valid;
import org.flowable.bpmn.model.UserTask;
import org.flowable.engine.history.HistoricActivityInstance;
+import org.flowable.engine.task.Attachment;
import org.flowable.task.api.Task;
import org.flowable.task.api.TaskInfo;
import org.flowable.task.api.history.HistoricTaskInstance;
@@ -14,6 +16,7 @@ import org.flowable.task.api.history.HistoricTaskInstance;
import java.util.Collection;
import java.util.List;
import java.util.Map;
+import java.util.Set;
/**
* 流程任务实例 Service 接口
@@ -184,6 +187,17 @@ public interface BpmTaskService {
*/
List getFinishedTaskListByProcessInstanceIdWithoutCancel(String processInstanceId);
+
+ /**
+ * 根据条件获取附件
+ *
+ * @param processInstanceId 流程 id
+ * @param taskIds 任务 id 集合
+ * @param attachmentType 附件类型
+ * @return 附件集合
+ */
+ List getAttachments(String processInstanceId, Set taskIds, BpmAttachmentTypeEnum attachmentType);
+
// ========== Update 写入相关方法 ==========
/**
diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java
index c178ea2..b31b8a0 100644
--- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java
+++ b/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;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
+import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.*;
import cn.hutool.extra.spring.SpringUtil;
@@ -19,10 +20,7 @@ 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;
-import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskSignTypeEnum;
-import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
+import cn.iocoder.yudao.module.bpm.enums.task.*;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmHttpRequestUtils;
@@ -49,6 +47,7 @@ import org.flowable.engine.history.HistoricActivityInstance;
import org.flowable.engine.runtime.ActivityInstance;
import org.flowable.engine.runtime.Execution;
import org.flowable.engine.runtime.ProcessInstance;
+import org.flowable.engine.task.Attachment;
import org.flowable.task.api.DelegationState;
import org.flowable.task.api.Task;
import org.flowable.task.api.TaskInfo;
@@ -510,6 +509,18 @@ public class BpmTaskServiceImpl implements BpmTaskService {
.orderByHistoricTaskInstanceStartTime().asc().list();
}
+ @Override
+ public List getAttachments(String processInstanceId, Set taskIds, BpmAttachmentTypeEnum attachmentType) {
+ List result = taskService.getProcessInstanceAttachments(processInstanceId);
+ if (CollUtil.isNotEmpty(taskIds)) {
+ result = filterList(result, attachment -> taskIds.contains(attachment.getTaskId()));
+ }
+ if (attachmentType != null) {
+ result = filterList(result, attachment -> attachmentType.getType().equals(attachment.getType()));
+ }
+ return result;
+ }
+
/**
* 判断指定用户,是否是当前任务的审批人
*
@@ -592,6 +603,11 @@ public class BpmTaskServiceImpl implements BpmTaskService {
// 2.2 添加评论
taskService.addComment(task.getId(), task.getProcessInstanceId(), BpmCommentTypeEnum.APPROVE.getType(),
BpmCommentTypeEnum.APPROVE.formatComment(reqVO.getReason()));
+ // 2.3 添加附件
+ if (CollUtil.isNotEmpty(reqVO.getAttachments())) {
+ reqVO.getAttachments().forEach(attachment -> taskService.createAttachment(BpmAttachmentTypeEnum.TASK_ATTACHMENT.getType(),
+ task.getId(), task.getProcessInstanceId(), FileUtil.getName(URLUtil.getPath(attachment)), null, attachment));
+ }
// 3. 设置流程变量。如果流程变量前端传空,需要从历史实例中获取,原因:前端表单如果在当前节点无可编辑的字段时 variables 一定会为空
// 场景一:A 节点发起,B 节点表单无可编辑字段,审批通过时,C 节点需要流程变量获取下一个执行节点,但因为 B 节点无可编辑的字段,variables 为空,流程可能出现问题。
@@ -817,7 +833,13 @@ public class BpmTaskServiceImpl implements BpmTaskService {
// 2.2 添加流程评论
taskService.addComment(task.getId(), task.getProcessInstanceId(), BpmCommentTypeEnum.REJECT.getType(),
BpmCommentTypeEnum.REJECT.formatComment(reqVO.getReason()));
- // 2.3 如果当前任务时被加签的,则加它的根任务也标记成未通过
+ // 2.3 添加附件
+ if (CollUtil.isNotEmpty(reqVO.getAttachments())) {
+ reqVO.getAttachments().forEach(attachment -> taskService.createAttachment(BpmAttachmentTypeEnum.TASK_ATTACHMENT.getType(),
+ task.getId(), task.getProcessInstanceId(), FileUtil.getName(URLUtil.getPath(attachment)), null, attachment));
+ }
+
+ // 2.4 如果当前任务时被加签的,则加它的根任务也标记成未通过
// 疑问:为什么要标记未通过呢?
// 回答:例如说 A 任务被向前加签除 B 任务时,B 任务被审批不通过,此时 A 会被取消。而 yudao-ui-admin-vue3 不展示“已取消”的任务,导致展示不出审批不通过的细节。
if (task.getParentTaskId() != null) {
@@ -921,7 +943,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
* @param reqVO 前端参数封装
*/
public void returnTask(Long userId, BpmnModel bpmnModel, Task currentTask, FlowElement targetElement, BpmTaskReturnReqVO reqVO) {
- // 1. 获得所有需要回撤的任务 taskDefinitionKey,用于稍后的 moveExecutionsToSingleActivityId 回撤
+ // 1. 获得所有需要回撤的任务 taskDefinitionKey,用于稍后的 moveActivityIdsToSingleActivityId 回撤
// 1.1 获取所有正常进行的任务节点 Key
List taskList = taskService.createTaskQuery().processInstanceId(currentTask.getProcessInstanceId()).list();
List runTaskKeyList = convertList(taskList, Task::getTaskDefinitionKey);
@@ -929,16 +951,12 @@ public class BpmTaskServiceImpl implements BpmTaskService {
// 为什么不直接使用 runTaskKeyList 呢?因为可能存在多个审批分支,例如说:A -> B -> C 和 D -> F,而只要 C 撤回到 A,需要排除掉 F
List returnUserTaskList = BpmnModelUtils.iteratorFindChildUserTasks(targetElement, runTaskKeyList, null, null);
List returnTaskKeyList = convertList(returnUserTaskList, UserTask::getId);
- List runExecutionIds = new ArrayList<>();
// 2. 给当前要被退回的 task 数组,设置退回意见
taskList.forEach(task -> {
// 需要排除掉,不需要设置退回意见的任务
if (!returnTaskKeyList.contains(task.getTaskDefinitionKey())) {
return;
}
- if (task.getExecutionId() != null) {
- runExecutionIds.add(task.getExecutionId());
- }
// 判断是否分配给自己任务,因为会签任务,一个节点会有多个任务
if (isAssignUserTask(userId, task)) { // 情况一:自己的任务,进行 RETURN 标记
// 2.1.1 添加评论
@@ -955,21 +973,17 @@ public class BpmTaskServiceImpl implements BpmTaskService {
Set needSimulateTaskDefinitionKeys = getNeedSimulateTaskDefinitionKeys(bpmnModel, currentTask, targetElement);
// 4. 执行驳回
- // 4.1 校验是否有可回撤的 execution,避免 moveExecutionsToSingleActivityId 传入空集合时 Flowable 内部报错
- if (CollUtil.isEmpty(runExecutionIds)) {
- throw exception(TASK_RETURN_FAIL_SOURCE_TARGET_ERROR);
- }
- // 4.2 执行驳回
// ① 使用 moveExecutionsToSingleActivityId 替换 moveActivityIdsToSingleActivityId。原因:当多实例任务回退的时候有问题。
// 相关 issue: https://github.com/flowable/flowable-engine/issues/3944
// ② flowable 7.2.0 版本后,继续使用 moveActivityIdsToSingleActivityId 方法。原因:flowable 7.2.0 版本修复了该问题。
// 相关 issue:https://github.com/YunaiV/ruoyi-vue-pro/issues/1018
// ③ moveActivityIdsToSingleActivityId 使用遇到问题, 相关 issue https://gitee.com/zhijiantianya/yudao-cloud/issues/IJM8MS
// 改成 moveExecutionsToSingleActivityId 好像并没有遇到 ② 提到的超时提醒失效的问题。暂时先改回 moveExecutionsToSingleActivityId
- // 目前还有的相关问题 https://t.zsxq.com/z4d9i。 估计需要升级 flowable 8 版本试试
+ // ④ moveExecutionsToSingleActivityId 回退多实例的时候不会去删除多实例根, 应改成 moveActivityIdsToSingleActivityId
+ // flowable 8.0.0 修复上面相关问题, 还修复了并行分支回退的问题 https://t.zsxq.com/z4d9i。
runtimeService.createChangeActivityStateBuilder()
.processInstanceId(currentTask.getProcessInstanceId())
- .moveExecutionsToSingleActivityId(runExecutionIds, reqVO.getTargetTaskDefinitionKey())
+ .moveActivityIdsToSingleActivityId(returnTaskKeyList, reqVO.getTargetTaskDefinitionKey())
// 设置需要预测的任务 ids 的流程变量,用于辅助预测
.processVariable(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_NEED_SIMULATE_TASK_IDS, needSimulateTaskDefinitionKeys)
// 设置流程变量节点退回标记, 用于退回到节点,不执行 BpmUserTaskAssignStartUserHandlerTypeEnum 策略,导致自动通过