【同步】BOOT 和 CLOUD 的功能

This commit is contained in:
YunaiV
2025-10-02 17:51:49 +08:00
parent ec3a391981
commit f02c004736
90 changed files with 3143 additions and 1365 deletions

View File

@@ -100,6 +100,10 @@ public class BpmModelMetaInfoVO {
@Schema(description = "任务后置通知设置", example = "{}")
private HttpRequestSetting taskAfterTriggerSetting;
@Schema(description = "自定义打印模板设置", example = "{}")
@Valid
private PrintTemplateSetting printTemplateSetting;
@Schema(description = "流程 ID 规则")
@Data
@Valid
@@ -180,4 +184,17 @@ public class BpmModelMetaInfoVO {
}
@Schema(description = "自定义打印模板设置")
@Data
public static class PrintTemplateSetting {
@Schema(description = "是否自定义打印模板", example = "false")
@NotNull(message = "是否自定义打印模板不能为空")
private Boolean enable;
@Schema(description = "打印模板", example = "<p></p>")
private String template;
}
}

View File

@@ -6,6 +6,7 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO;
import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.*;
import cn.iocoder.yudao.module.bpm.convert.task.BpmProcessInstanceConvert;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO;
@@ -26,6 +27,7 @@ import jakarta.validation.Valid;
import org.flowable.engine.history.HistoricProcessInstance;
import org.flowable.engine.repository.ProcessDefinition;
import org.flowable.task.api.Task;
import org.flowable.task.api.history.HistoricTaskInstance;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@@ -34,9 +36,11 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.PROCESS_INSTANCE_NOT_EXISTS;
@Tag(name = "管理后台 - 流程实例") // 流程实例,通过流程定义创建的一次“申请”
@RestController
@@ -192,8 +196,30 @@ public class BpmProcessInstanceController {
@GetMapping("/get-bpmn-model-view")
@Operation(summary = "获取流程实例的 BPMN 模型视图", description = "在【流程详细】界面中,进行调用")
@Parameter(name = "id", description = "流程实例的编号", required = true)
public CommonResult<BpmProcessInstanceBpmnModelViewRespVO> getProcessInstanceBpmnModelView(@RequestParam(value = "id") String id) {
public CommonResult<BpmProcessInstanceBpmnModelViewRespVO> getProcessInstanceBpmnModelView(
@RequestParam(value = "id") String id) {
return success(processInstanceService.getProcessInstanceBpmnModelView(id));
}
@GetMapping("/get-print-data")
@Operation(summary = "获得流程实例的打印数据")
@Parameter(name = "id", description = "流程实例的编号", required = true)
@PreAuthorize("@ss.hasPermission('bpm:process-instance:query')")
public CommonResult<BpmProcessPrintDataRespVO> getProcessInstancePrintData(
@RequestParam("processInstanceId") String processInstanceId) {
HistoricProcessInstance historicProcessInstance = processInstanceService.getHistoricProcessInstance(processInstanceId);
if (historicProcessInstance == null) {
throw exception(PROCESS_INSTANCE_NOT_EXISTS);
}
AdminUserRespDTO startUser = adminUserApi.getUser(Long.valueOf(historicProcessInstance.getStartUserId())).getCheckedData();
DeptRespDTO dept = deptApi.getDept(startUser.getDeptId()).getCheckedData();
List<HistoricTaskInstance> tasks = taskService.getFinishedTaskListByProcessInstanceIdWithoutCancel(processInstanceId);
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
convertSet(tasks, item -> Long.valueOf(item.getAssignee())));
return success(BpmProcessInstanceConvert.INSTANCE.buildProcessInstancePrintData(historicProcessInstance,
processDefinitionService.getProcessDefinitionInfo(historicProcessInstance.getProcessDefinitionId()),
tasks, userMap,
new UserSimpleBaseVO().setNickname(startUser.getNickname()).setDeptName(dept.getName())));
}
}

View File

@@ -1,9 +1,10 @@
package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import jakarta.validation.constraints.NotEmpty;
@Schema(description = "管理后台 - 流程实例的取消 Request VO")
@Data
public class BpmProcessInstanceCancelReqVO {

View File

@@ -1,9 +1,9 @@
package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import jakarta.validation.constraints.NotEmpty;
import java.util.List;
import java.util.Map;

View File

@@ -0,0 +1,42 @@
package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Schema(description = "管理后台 - 流程实例的打印数据 Response VO")
@Data
public class BpmProcessPrintDataRespVO {
@Schema(description = "流程实例数据")
private BpmProcessInstanceRespVO processInstance;
@Schema(description = "是否开启自定义打印模板", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
private Boolean printTemplateEnable;
@Schema(description = "自定义打印模板 HTML")
private String printTemplateHtml;
@Schema(description = "审批任务列表")
private List<Task> tasks;
@Schema(description = "流程任务")
@Data
public static class Task {
@Schema(description = "流程任务的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private String id;
@Schema(description = "任务名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道")
private String name;
@Schema(description = "签名 URL", example = "https://www.iocoder.cn/sign.png")
private String signPicUrl;
@Schema(description = "任务描述", requiredMode = Schema.RequiredMode.REQUIRED)
private String description; // 该字段由后端拼接
}
}

View File

@@ -2,6 +2,8 @@ package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.util.date.DateUtils;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
@@ -21,6 +23,10 @@ public class BpmTaskPageReqVO extends PageParam {
@Schema(description = "流程定义的标识", example = "2048")
private String processDefinitionKey; // 精准匹配
@Schema(description = "审批状态", example = "1")
@InEnum(BpmTaskStatusEnum.class)
private Integer status; // 仅【已办】使用
@Schema(description = "创建时间")
@DateTimeFormat(pattern = DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] createTime;

View File

@@ -1,23 +1,29 @@
package cn.iocoder.yudao.module.bpm.convert.task;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
import cn.iocoder.yudao.framework.common.util.date.DateUtils;
import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionRespVO;
import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmApprovalDetailRespVO;
import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceBpmnModelViewRespVO;
import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceRespVO;
import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessPrintDataRespVO;
import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.BpmTaskRespVO;
import cn.iocoder.yudao.module.bpm.convert.definition.BpmProcessDefinitionConvert;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEvent;
import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenProcessInstanceApproveReqDTO;
@@ -35,10 +41,7 @@ import org.mapstruct.Mapping;
import org.mapstruct.MappingTarget;
import org.mapstruct.factory.Mappers;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.*;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
@@ -293,4 +296,47 @@ public interface BpmProcessInstanceConvert {
.setActivityNodes(activityNodes);
}
default BpmProcessPrintDataRespVO buildProcessInstancePrintData(HistoricProcessInstance historicProcessInstance,
BpmProcessDefinitionInfoDO processDefinitionInfo,
List<HistoricTaskInstance> tasks,
Map<Long, AdminUserRespDTO> userMap,
UserSimpleBaseVO startUser) {
BpmModelMetaInfoVO.PrintTemplateSetting printTemplateSetting = processDefinitionInfo.getPrintTemplateSetting();
BpmProcessPrintDataRespVO printData = new BpmProcessPrintDataRespVO();
// 打印模板是否开启
printData.setPrintTemplateEnable(printTemplateSetting != null && Boolean.TRUE.equals(printTemplateSetting.getEnable()));
// 流程相关数据
BpmProcessInstanceRespVO processInstance = new BpmProcessInstanceRespVO()
.setId(historicProcessInstance.getId()).setName(historicProcessInstance.getName())
.setBusinessKey(historicProcessInstance.getBusinessKey())
.setStartTime(DateUtils.of(historicProcessInstance.getStartTime()))
.setEndTime(DateUtils.of(historicProcessInstance.getEndTime()))
.setStartUser(startUser).setStatus(FlowableUtils.getProcessInstanceStatus(historicProcessInstance))
.setFormVariables(historicProcessInstance.getProcessVariables())
.setProcessDefinition(BeanUtils.toBean(processDefinitionInfo, BpmProcessDefinitionRespVO.class));
printData.setProcessInstance(processInstance);
// 审批历史
List<BpmProcessPrintDataRespVO.Task> approveTasks = new ArrayList<>(tasks.size());
tasks.forEach(item -> {
Map<String, Object> taskLocalVariables = item.getTaskLocalVariables();
BpmProcessPrintDataRespVO.Task approveTask = new BpmProcessPrintDataRespVO.Task();
approveTask.setName(item.getName());
approveTask.setId(item.getId());
approveTask.setSignPicUrl((String) taskLocalVariables.get(BpmnVariableConstants.TASK_SIGN_PIC_URL));
approveTask.setDescription(StrUtil.format("{} / {} / {} / {} / {}",
userMap.get(Long.valueOf(item.getAssignee())).getNickname(),
item.getName(),
DateUtil.formatDateTime(item.getEndTime()),
BpmTaskStatusEnum.valueOf((Integer) taskLocalVariables.get(BpmnVariableConstants.TASK_VARIABLE_STATUS)).getName(),
taskLocalVariables.get(BpmnVariableConstants.TASK_VARIABLE_REASON)));
approveTasks.add(approveTask);
});
printData.setTasks(approveTasks);
// 自定义模板
if (printData.getPrintTemplateEnable() && printTemplateSetting != null) {
printData.setPrintTemplateHtml(printTemplateSetting.getTemplate());
}
return printData;
}
}

View File

@@ -43,14 +43,23 @@ public class BpmnVariableConstants {
* @see ProcessInstance#getProcessVariables()
*/
public static final String PROCESS_INSTANCE_VARIABLE_START_USER_ID = "PROCESS_START_USER_ID";
/**
* 流程实例的变量 - 用于判断流程实例变量节点是否驳回. 格式 RETURN_FLAG_{节点 id}
* 流程实例的变量 - 用于判断流程实例变量节点是否驳回格式 RETURN_FLAG_{节点 id}
*
* 目的是:回到发起节点时,因为审批人与发起人相同,所以被自动通过。但是,此时还是希望不要自动通过
* 目的是:退回到发起节点时,因为审批人与发起人相同,所以被自动通过。但是,此时还是希望不要自动通过
*
* @see ProcessInstance#getProcessVariables()
*/
public static final String PROCESS_INSTANCE_VARIABLE_RETURN_FLAG = "RETURN_FLAG_%s";
/**
* 流程实例的变量前缀 - 用于退回操作,记录需要预测的节点:格式 NEED_SIMULATE_TASK_{节点定义 id}
*
* 目的是:退回操作,预测节点会不准,在流程变量中记录需要预测的节点,来辅助预测
*/
public static final String PROCESS_INSTANCE_VARIABLE_NEED_SIMULATE_PREFIX = "NEED_SIMULATE_TASK_";
/**
* 流程实例的变量 - 是否跳过表达式
*

View File

@@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.*;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
@@ -71,6 +72,7 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.
import static cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmApprovalDetailRespVO.ActivityNode;
import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.START_USER_NODE_ID;
import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_NEED_SIMULATE_PREFIX;
import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.parseNodeType;
import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
@@ -186,6 +188,10 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
if (CollUtil.isNotEmpty(reqVO.getProcessVariables())) {
processVariables.putAll(reqVO.getProcessVariables());
}
// 特殊如果是未发起的场景则设置发起用户解决“发起流程”时需要使用到该变量的问题。例如说https://t.zsxq.com/fMw5g
if (historicProcessInstance == null) {
processVariables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_ID, loginUserId);
}
// 1.3 读取其它相关数据
ProcessDefinition processDefinition = processDefinitionService.getProcessDefinition(
historicProcessInstance != null ? historicProcessInstance.getProcessDefinitionId()
@@ -217,10 +223,24 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
// 3.1 计算当前登录用户的待办任务
BpmTaskRespVO todoTask = taskService.getTodoTask(loginUserId, reqVO.getTaskId(), reqVO.getProcessInstanceId());
// 3.2 预测未运行节点的审批信息
// 3.2 获取由于退回操作,需要预测的节点。从流程变量中获取,回退操作会设置这些变量
Set<String> needSimulateTaskDefKeysByReturn = new HashSet<>();
if (StrUtil.isNotEmpty(reqVO.getProcessInstanceId())) {
Map<String, Object> variables = runtimeService.getVariables(reqVO.getProcessInstanceId());
Map<String, Object> simulateTaskVariables = MapUtil.filter(variables,
item -> item.getKey().startsWith(PROCESS_INSTANCE_VARIABLE_NEED_SIMULATE_PREFIX));
simulateTaskVariables.forEach((key, value) ->
needSimulateTaskDefKeysByReturn.add(StrUtil.removePrefix(key, PROCESS_INSTANCE_VARIABLE_NEED_SIMULATE_PREFIX)));
}
// 移除运行中的节点,运行中的节点无需预测
// TODO @jason是不是 foreach runActivityNodes然后移除 needSimulateTaskDefKeysByReturn 更好?(理解成本低一点)
CollectionUtils.convertList(runActivityNodes, ActivityNode::getId).forEach(needSimulateTaskDefKeysByReturn::remove);
// 3.3 预测未运行节点的审批信息
List<ActivityNode> simulateActivityNodes = getSimulateApproveNodeList(startUserId, bpmnModel,
processDefinitionInfo,
processVariables, activities);
processVariables, activities, needSimulateTaskDefKeysByReturn);
// 4. 拼接最终数据
return buildApprovalDetail(reqVO, bpmnModel, processDefinition, processDefinitionInfo, historicProcessInstance,
@@ -460,7 +480,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
}
/**
* 获取结束节点的状态
* 获取结束节点的状态
*/
private Integer getEndActivityNodeStatus(HistoricTaskInstance task) {
Integer status = FlowableUtils.getTaskStatus(task);
@@ -545,7 +565,8 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
private List<ActivityNode> getSimulateApproveNodeList(Long startUserId, BpmnModel bpmnModel,
BpmProcessDefinitionInfoDO processDefinitionInfo,
Map<String, Object> processVariables,
List<HistoricActivityInstance> activities) {
List<HistoricActivityInstance> activities,
Set<String> needSimulateTaskDefKeysByReturn) {
// TODO @芋艿【可优化】在驳回场景下未来的预测准确性不高。原因是驳回后HistoricActivityInstance
// 包括了历史的操作,不是只有 startEvent 到当前节点的记录
Set<String> runActivityIds = convertSet(activities, HistoricActivityInstance::getActivityId);
@@ -554,7 +575,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
List<FlowElement> flowElements = BpmnModelUtils.simulateProcess(bpmnModel, processVariables);
return convertList(flowElements, flowElement -> buildNotRunApproveNodeForBpmn(
startUserId, bpmnModel, flowElements,
processDefinitionInfo, processVariables, flowElement, runActivityIds));
processDefinitionInfo, processVariables, flowElement, runActivityIds, needSimulateTaskDefKeysByReturn));
}
// 情况二SIMPLE 设计器
if (Objects.equals(BpmModelTypeEnum.SIMPLE.getType(), processDefinitionInfo.getModelType())) {
@@ -563,17 +584,19 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
List<BpmSimpleModelNodeVO> simpleNodes = SimpleModelUtils.simulateProcess(simpleModel, processVariables);
return convertList(simpleNodes, simpleNode -> buildNotRunApproveNodeForSimple(
startUserId, bpmnModel,
processDefinitionInfo, processVariables, simpleNode, runActivityIds));
processDefinitionInfo, processVariables, simpleNode, runActivityIds, needSimulateTaskDefKeysByReturn));
}
throw new IllegalArgumentException("未知设计器类型:" + processDefinitionInfo.getModelType());
}
private ActivityNode buildNotRunApproveNodeForSimple(Long startUserId, BpmnModel bpmnModel,
BpmProcessDefinitionInfoDO processDefinitionInfo, Map<String, Object> processVariables,
BpmSimpleModelNodeVO node, Set<String> runActivityIds) {
BpmSimpleModelNodeVO node, Set<String> runActivityIds,
Set<String> needSimulateTaskDefKeysByReturn) {
// TODO @芋艿【可优化】在驳回场景下未来的预测准确性不高。原因是驳回后HistoricActivityInstance
// 包括了历史的操作,不是只有 startEvent 到当前节点的记录
if (runActivityIds.contains(node.getId())) {
if (runActivityIds.contains(node.getId())
&& !needSimulateTaskDefKeysByReturn.contains(node.getId())) { // 特殊:回退操作时候,会记录需要预测的节点到流程变量中。即使在历史操作中,也需要预测
return null;
}
Integer status = BpmTaskStatusEnum.NOT_START.getStatus();
@@ -620,13 +643,16 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
private ActivityNode buildNotRunApproveNodeForBpmn(Long startUserId, BpmnModel bpmnModel, List<FlowElement> flowElements,
BpmProcessDefinitionInfoDO processDefinitionInfo,
Map<String, Object> processVariables,
FlowElement node, Set<String> runActivityIds) {
if (runActivityIds.contains(node.getId())) {
FlowElement node, Set<String> runActivityIds,
Set<String> needSimulateTaskDefKeysByReturn) {
// 回退操作时候,会记录需要预测的节点到流程变量中。即使节点在历史操作中,也需要预测。
if (!needSimulateTaskDefKeysByReturn.contains(node.getId()) && runActivityIds.contains(node.getId())) {
return null;
}
Integer status = BpmTaskStatusEnum.NOT_START.getStatus();
// 如果节点被跳过,状态设置为跳过
if(BpmnModelUtils.isSkipNode(node, processVariables)){
if (BpmnModelUtils.isSkipNode(node, processVariables)) {
status = BpmTaskStatusEnum.SKIP.getStatus();
}
ActivityNode activityNode = new ActivityNode().setId(node.getId())

View File

@@ -176,6 +176,14 @@ public interface BpmTaskService {
*/
List<HistoricActivityInstance> getHistoricActivityListByExecutionId(String executionId);
/**
* 获得指定流程实例的已完成的流程任务列表,不包含取消状态
*
* @param processInstanceId 流程实例的编号
* @return 流程任务列表
*/
List<HistoricTaskInstance> getFinishedTaskListByProcessInstanceIdWithoutCancel(String processInstanceId);
// ========== Update 写入相关方法 ==========
/**

View File

@@ -2,10 +2,12 @@ package cn.iocoder.yudao.module.bpm.service.task;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.*;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.common.util.date.DateUtils;
import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
@@ -68,8 +70,7 @@ import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionU
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.START_USER_NODE_ID;
import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_RETURN_FLAG;
import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_SKIP_START_USER_NODE;
//import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants.*;
import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.*;
/**
@@ -230,6 +231,9 @@ public class BpmTaskServiceImpl implements BpmTaskService {
if (StrUtil.isNotBlank(pageVO.getName())) {
taskQuery.taskNameLike("%" + pageVO.getName() + "%");
}
if (pageVO.getStatus() != null) {
taskQuery.taskVariableValueEquals(BpmnVariableConstants.TASK_VARIABLE_STATUS, pageVO.getStatus());
}
// if (ArrayUtil.isNotEmpty(pageVO.getCreateTime())) {
// taskQuery.taskCreatedAfter(DateUtils.of(pageVO.getCreateTime()[0]));
// taskQuery.taskCreatedBefore(DateUtils.of(pageVO.getCreateTime()[1]));
@@ -491,6 +495,17 @@ public class BpmTaskServiceImpl implements BpmTaskService {
return historyService.createHistoricActivityInstanceQuery().executionId(executionId).list();
}
@Override
public List<HistoricTaskInstance> getFinishedTaskListByProcessInstanceIdWithoutCancel(String processInstanceId) {
return historyService.createHistoricTaskInstanceQuery()
.finished()
.includeTaskLocalVariables()
.processInstanceId(processInstanceId)
.taskVariableValueNotEquals(BpmnVariableConstants.TASK_VARIABLE_STATUS,
BpmTaskStatusEnum.CANCEL.getStatus())
.orderByHistoricTaskInstanceStartTime().asc().list();
}
/**
* 判断指定用户,是否是当前任务的审批人
*
@@ -590,7 +605,13 @@ public class BpmTaskServiceImpl implements BpmTaskService {
bpmnModel, reqVO.getNextAssignees(), instance);
runtimeService.setVariables(task.getProcessInstanceId(), variables);
// 5. 调用 BPM complete 去完成任务
// 5. 移除辅助预测的流程变量,这些变量在回退操作中设置
// todo @jason可以直接 + 拼接哈
String simulateVariableName = StrUtil.concat(false,
BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_NEED_SIMULATE_PREFIX, task.getTaskDefinitionKey());
runtimeService.removeVariable(task.getProcessInstanceId(), simulateVariableName);
// 6. 调用 BPM complete 去完成任务
taskService.complete(task.getId(), variables, true);
// 【加签专属】处理加签任务
@@ -840,34 +861,34 @@ public class BpmTaskServiceImpl implements BpmTaskService {
if (task.isSuspended()) {
throw exception(TASK_IS_PENDING);
}
// 1.2 校验源头和目标节点的关系,并返回目标元素
FlowElement targetElement = validateTargetTaskCanReturn(task.getTaskDefinitionKey(),
reqVO.getTargetTaskDefinitionKey(), task.getProcessDefinitionId());
// 1.2 获取流程模型信息
BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(task.getProcessDefinitionId());
// 1.3 校验源头和目标节点的关系,并返回目标元素
FlowElement targetElement = validateTargetTaskCanReturn(bpmnModel, task.getTaskDefinitionKey(),
reqVO.getTargetTaskDefinitionKey());
// 2. 调用 Flowable 框架的退回逻辑
returnTask(userId, task, targetElement, reqVO);
returnTask(userId, bpmnModel, task, targetElement, reqVO);
}
/**
* 退回流程节点时,校验目标任务节点是否可退回
*
* @param sourceKey 当前任务节点 Key
* @param targetKey 目标任务节点 key
* @param processDefinitionId 当前流程定义 ID
* @param bpmnModel 流程模型
* @param sourceKey 当前任务节点 Key
* @param targetKey 目标任务节点 key
* @return 目标任务节点元素
*/
private FlowElement validateTargetTaskCanReturn(String sourceKey, String targetKey, String processDefinitionId) {
// 1.1 获取流程模型信息
BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(processDefinitionId);
// 1.3 获取当前任务节点元素
private FlowElement validateTargetTaskCanReturn(BpmnModel bpmnModel, String sourceKey, String targetKey) {
// 1.1 获取当前任务节点元素
FlowElement source = BpmnModelUtils.getFlowElementById(bpmnModel, sourceKey);
// 1.3 获取跳转的节点元素
// 1.2 获取跳转的节点元素
FlowElement target = BpmnModelUtils.getFlowElementById(bpmnModel, targetKey);
if (target == null) {
throw exception(TASK_TARGET_NODE_NOT_EXISTS);
}
// 2.2 只有串行可到达的节点,才可以退回。类似非串行、子流程无法退回
// 2. 只有串行可到达的节点,才可以退回。类似非串行、子流程无法退回
if (!BpmnModelUtils.isSequentialReachable(source, target, null)) {
throw exception(TASK_RETURN_FAIL_SOURCE_TARGET_ERROR);
}
@@ -878,11 +899,12 @@ public class BpmTaskServiceImpl implements BpmTaskService {
* 执行退回逻辑
*
* @param userId 用户编号
* @param bpmnModel 流程模型
* @param currentTask 当前退回的任务
* @param targetElement 需要退回到的目标任务
* @param reqVO 前端参数封装
*/
public void returnTask(Long userId, Task currentTask, FlowElement targetElement, BpmTaskReturnReqVO reqVO) {
public void returnTask(Long userId, BpmnModel bpmnModel, Task currentTask, FlowElement targetElement, BpmTaskReturnReqVO reqVO) {
// 1. 获得所有需要回撤的任务 taskDefinitionKey用于稍后的 moveActivityIdsToSingleActivityId 回撤
// 1.1 获取所有正常进行的任务节点 Key
List<Task> taskList = taskService.createTaskQuery().processInstanceId(currentTask.getProcessInstanceId()).list();
@@ -915,18 +937,54 @@ public class BpmTaskServiceImpl implements BpmTaskService {
}
});
// 3. 设置流程变量节点驳回标记:用于驳回到节点,不执行 BpmUserTaskAssignStartUserHandlerTypeEnum 策略。导致自动通过
runtimeService.setVariable(currentTask.getProcessInstanceId(),
String.format(PROCESS_INSTANCE_VARIABLE_RETURN_FLAG, reqVO.getTargetTaskDefinitionKey()), Boolean.TRUE);
// 3. 构建需要预测的任务流程变量
// TODO @jason【驳回预测相关】是不是搞成一个变量里面是 set 更简洁一点呀?
Set<String> taskDefinitionKeyList = getNeedSimulateTaskDefinitionKeys(bpmnModel, currentTask, targetElement);
Map<String, Object> needSimulateVariables = convertMap(taskDefinitionKeyList,
taskId -> StrUtil.concat(false, BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_NEED_SIMULATE_PREFIX, taskId), item -> Boolean.TRUE);
// 4. 执行驳回
// 使用 moveExecutionsToSingleActivityId 替换 moveActivityIdsToSingleActivityId 原因:
// 当多实例任务回退的时候有问题。相关 issue: https://github.com/flowable/flowable-engine/issues/3944
runtimeService.createChangeActivityStateBuilder()
.processInstanceId(currentTask.getProcessInstanceId())
.moveExecutionsToSingleActivityId(runExecutionIds, reqVO.getTargetTaskDefinitionKey())
// 设置需要预测的任务流程变量,用于辅助预测
.processVariables(needSimulateVariables)
// 设置流程变量local节点退回标记, 用于退回到节点,不执行 BpmUserTaskAssignStartUserHandlerTypeEnum 策略,导致自动通过
.localVariable(reqVO.getTargetTaskDefinitionKey(),
String.format(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_RETURN_FLAG, reqVO.getTargetTaskDefinitionKey()),
Boolean.TRUE)
.changeState();
}
private Set<String> getNeedSimulateTaskDefinitionKeys(BpmnModel bpmnModel, Task currentTask, FlowElement targetElement) {
// 1. 获取需要预测的任务的 definition key。因为当前任务还没完成也需要预测
Set<String> taskDefinitionKeys = CollUtil.newHashSet(currentTask.getTaskDefinitionKey());
// 2.1 从已结束任务中找到要回退的目标任务,按时间倒序最近的一个目标任务
List<HistoricTaskInstance> endTaskList = CollectionUtils.filterList(
getTaskListByProcessInstanceId(currentTask.getProcessInstanceId(), Boolean.FALSE),
item -> item.getEndTime() != null);
// 2.2 遍历已结束的任务,找到在 targetTask 之后生成的任务,且串行可达的任务
HistoricTaskInstance targetTask = findFirst(endTaskList,
item -> item.getTaskDefinitionKey().equals(targetElement.getId()));
// TODO @jason【驳回预测相关】是不是 if targetTask 先判空?
endTaskList.forEach(item -> {
FlowElement element = getFlowElementById(bpmnModel, item.getTaskDefinitionKey());
// 如果已结束的任务在回退目标节点之后生成,且串行可达,则标记为需要预算节点
// TODO 串行可达的方法需要和判断可回退节点 validateTargetTaskCanReturn 分开吗? 并行网关可能会有问题。
// TODO @jason【驳回预测相关】这里是不是判断 element 哈?
if (targetTask != null
// TODO @jason【驳回预测相关】这里直接 createTime 的 compare 更简单?因为不太会出现空哈。
&& DateUtil.compare(item.getCreateTime(), targetTask.getCreateTime()) > 0
&& BpmnModelUtils.isSequentialReachable(element, targetElement, null)) {
taskDefinitionKeys.add(item.getTaskDefinitionKey());
}
});
return taskDefinitionKeys;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void delegateTask(Long userId, BpmTaskDelegateReqVO reqVO) {
@@ -1438,12 +1496,11 @@ public class BpmTaskServiceImpl implements BpmTaskService {
return;
}
FlowElement userTaskElement = BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey());
// 判断是否为退回或者驳回:如果是退回或者驳回不走这个策略
// TODO 芋艿:【优化】未来有没更好的判断方式?!另外,还要考虑清理机制。就是说,下次处理了之后,就移除这个标识
Boolean returnTaskFlag = runtimeService.getVariable(processInstance.getProcessInstanceId(),
String.format(PROCESS_INSTANCE_VARIABLE_RETURN_FLAG, task.getTaskDefinitionKey()), Boolean.class);
// 判断是否为退回或者驳回:如果是退回或者驳回不走这个策略(使用 local variable
Boolean returnTaskFlag = runtimeService.getVariableLocal(task.getExecutionId(),
String.format(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_RETURN_FLAG, task.getTaskDefinitionKey()), Boolean.class);
Boolean skipStartUserNodeFlag = Convert.toBool(runtimeService.getVariable(processInstance.getProcessInstanceId(),
PROCESS_INSTANCE_VARIABLE_SKIP_START_USER_NODE, String.class));
BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_SKIP_START_USER_NODE, String.class));
if (userTaskElement.getId().equals(START_USER_NODE_ID)
&& (skipStartUserNodeFlag == null // 目的:一般是“主流程”,发起人节点,自动通过审核
|| BooleanUtil.isTrue(skipStartUserNodeFlag)) // 目的:一般是“子流程”,发起人节点,按配置自动通过审核