|
|
@@ -0,0 +1,364 @@
|
|
|
+package com.qqflow.engine.domain.flow.service.impl;
|
|
|
+
|
|
|
+import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
+import com.qqflow.engine.common.exception.BusinessException;
|
|
|
+import com.qqflow.engine.domain.flow.assembler.ApprovalTaskAssembler;
|
|
|
+import com.qqflow.engine.domain.flow.enums.ApprovalAction;
|
|
|
+import com.qqflow.engine.domain.flow.enums.ApprovalResult;
|
|
|
+import com.qqflow.engine.domain.flow.enums.NodeType;
|
|
|
+import com.qqflow.engine.domain.flow.enums.ProcessStatus;
|
|
|
+import com.qqflow.engine.domain.flow.enums.TaskStatus;
|
|
|
+import com.qqflow.engine.domain.flow.mapper.ApprovalTaskMapper;
|
|
|
+import com.qqflow.engine.domain.flow.mapper.ProcessDefinitionMapper;
|
|
|
+import com.qqflow.engine.domain.flow.mapper.ProcessInstanceMapper;
|
|
|
+import com.qqflow.engine.domain.flow.model.FlowEdge;
|
|
|
+import com.qqflow.engine.domain.flow.model.FlowModel;
|
|
|
+import com.qqflow.engine.domain.flow.model.FlowNode;
|
|
|
+import com.qqflow.engine.domain.flow.po.ApprovalTask;
|
|
|
+import com.qqflow.engine.domain.flow.po.ProcessDefinition;
|
|
|
+import com.qqflow.engine.domain.flow.po.ProcessInstance;
|
|
|
+import com.qqflow.engine.domain.flow.event.ProcessCompletedEvent;
|
|
|
+import com.qqflow.engine.domain.flow.event.TaskAssignedEvent;
|
|
|
+import com.qqflow.engine.domain.flow.service.FlowEngineService;
|
|
|
+import com.qqflow.engine.domain.system.entity.SysDept;
|
|
|
+import com.qqflow.engine.domain.system.entity.SysRole;
|
|
|
+import com.qqflow.engine.domain.system.entity.SysUser;
|
|
|
+import com.qqflow.engine.domain.system.entity.SysUserRole;
|
|
|
+import com.qqflow.engine.domain.system.mapper.SysDeptMapper;
|
|
|
+import com.qqflow.engine.domain.system.mapper.SysRoleMapper;
|
|
|
+import com.qqflow.engine.domain.system.mapper.SysUserMapper;
|
|
|
+import com.qqflow.engine.domain.system.mapper.SysUserRoleMapper;
|
|
|
+import lombok.RequiredArgsConstructor;
|
|
|
+import lombok.SneakyThrows;
|
|
|
+import org.springframework.context.ApplicationEventPublisher;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+
|
|
|
+import java.time.LocalDateTime;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.Collections;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.Objects;
|
|
|
+import java.util.stream.Collectors;
|
|
|
+
|
|
|
+@Service
|
|
|
+@RequiredArgsConstructor
|
|
|
+public class FlowEngineServiceImpl implements FlowEngineService {
|
|
|
+
|
|
|
+ private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
|
|
+
|
|
|
+ private final ProcessDefinitionMapper processDefinitionMapper;
|
|
|
+ private final ProcessInstanceMapper processInstanceMapper;
|
|
|
+ private final ApprovalTaskMapper approvalTaskMapper;
|
|
|
+ private final ApprovalTaskAssembler approvalTaskAssembler;
|
|
|
+ private final SysRoleMapper sysRoleMapper;
|
|
|
+ private final SysUserRoleMapper sysUserRoleMapper;
|
|
|
+ private final SysUserMapper sysUserMapper;
|
|
|
+ private final SysDeptMapper sysDeptMapper;
|
|
|
+ private final ApplicationEventPublisher eventPublisher;
|
|
|
+
|
|
|
+ @Override
|
|
|
+ @SneakyThrows
|
|
|
+ public FlowModel parseModel(String modelJson) {
|
|
|
+ return OBJECT_MAPPER.readValue(modelJson, FlowModel.class);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public List<FlowNode> getNextNodes(FlowModel model, String currentNodeId) {
|
|
|
+ List<String> targetIds = model.getEdges().stream()
|
|
|
+ .filter(e -> Objects.equals(e.getSourceNodeId(), currentNodeId))
|
|
|
+ .map(FlowEdge::getTargetNodeId)
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ return model.getNodes().stream()
|
|
|
+ .filter(n -> targetIds.contains(n.getId()))
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public List<Long> calculateAssignees(FlowNode node, ProcessInstance instance) {
|
|
|
+ Map<String, Object> props = node.getProperties();
|
|
|
+ if (props == null) {
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+ // 新格式
|
|
|
+ Object assigneeTypeObj = props.get("assigneeType");
|
|
|
+ Object assigneeValueObj = props.get("assigneeValue");
|
|
|
+ if (assigneeTypeObj != null) {
|
|
|
+ String assigneeType = assigneeTypeObj.toString();
|
|
|
+ String assigneeValue = assigneeValueObj != null ? assigneeValueObj.toString() : null;
|
|
|
+ return this.doCalculateAssignees(assigneeType, assigneeValue, instance);
|
|
|
+ }
|
|
|
+ // 兼容旧格式:approver + approveType
|
|
|
+ Object approverObj = props.get("approver");
|
|
|
+ if (approverObj != null) {
|
|
|
+ String approver = approverObj.toString();
|
|
|
+ return this.doCalculateAssignees("ROLE", approver, instance);
|
|
|
+ }
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public String getApproveMode(FlowNode node) {
|
|
|
+ Map<String, Object> props = node.getProperties();
|
|
|
+ if (props == null) {
|
|
|
+ return "or";
|
|
|
+ }
|
|
|
+ Object modeObj = props.get("approveMode");
|
|
|
+ if (modeObj != null) {
|
|
|
+ return modeObj.toString();
|
|
|
+ }
|
|
|
+ // 兼容旧格式
|
|
|
+ Object typeObj = props.get("approveType");
|
|
|
+ if (typeObj != null) {
|
|
|
+ return typeObj.toString();
|
|
|
+ }
|
|
|
+ return "or";
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void executeTransition(ProcessInstance instance, ApprovalTask currentTask, ApprovalAction action) {
|
|
|
+ this.executeTransition(instance, currentTask, action, null);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void executeTransition(ProcessInstance instance, ApprovalTask currentTask, ApprovalAction action, String comment) {
|
|
|
+ switch (action) {
|
|
|
+ case APPROVE -> this.handleApprove(instance, currentTask, comment);
|
|
|
+ case REJECT -> this.handleReject(instance, currentTask, comment);
|
|
|
+ case RETURN -> this.handleReturn(instance, currentTask, comment);
|
|
|
+ default -> throw new BusinessException("不支持的操作类型");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void startInstance(ProcessInstance instance, ProcessDefinition definition) {
|
|
|
+ FlowModel model = this.parseModel(definition.getModelJson());
|
|
|
+ FlowNode startNode = this.findStartNode(model);
|
|
|
+ List<FlowNode> nextNodes = this.getNextNodes(model, startNode.getId());
|
|
|
+ this.createTasksForNodes(instance, nextNodes, model);
|
|
|
+ this.updateInstanceNode(instance, nextNodes);
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<Long> doCalculateAssignees(String assigneeType, String assigneeValue, ProcessInstance instance) {
|
|
|
+ if ("USER".equals(assigneeType) && assigneeValue != null) {
|
|
|
+ List<Long> list = new ArrayList<>();
|
|
|
+ for (String s : assigneeValue.split(",")) {
|
|
|
+ list.add(Long.valueOf(s.trim()));
|
|
|
+ }
|
|
|
+ return list;
|
|
|
+ }
|
|
|
+ if ("SELF".equals(assigneeType)) {
|
|
|
+ return Collections.singletonList(instance.getApplicantId());
|
|
|
+ }
|
|
|
+ if ("ROLE".equals(assigneeType) && assigneeValue != null) {
|
|
|
+ SysRole role = this.sysRoleMapper.selectByRoleCode(assigneeValue);
|
|
|
+ if (role == null) {
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+ List<SysUserRole> userRoles = this.sysUserRoleMapper.selectList(
|
|
|
+ new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<SysUserRole>()
|
|
|
+ .eq(SysUserRole::getRoleId, role.getId()));
|
|
|
+ return userRoles.stream().map(SysUserRole::getUserId).distinct().collect(Collectors.toList());
|
|
|
+ }
|
|
|
+ if ("LEADER".equals(assigneeType)) {
|
|
|
+ SysUser applicant = this.sysUserMapper.selectById(instance.getApplicantId());
|
|
|
+ if (applicant == null || applicant.getDeptId() == null) {
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+ SysDept dept = this.sysDeptMapper.selectById(applicant.getDeptId());
|
|
|
+ if (dept == null || dept.getLeaderId() == null) {
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+ return Collections.singletonList(dept.getLeaderId());
|
|
|
+ }
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+
|
|
|
+ private void handleApprove(ProcessInstance instance, ApprovalTask currentTask, String comment) {
|
|
|
+ this.completeCurrentTask(currentTask, ApprovalResult.PASS.getCode(), comment);
|
|
|
+
|
|
|
+ FlowModel model = this.getModelByInstance(instance);
|
|
|
+ FlowNode currentNode = model.getNodes().stream()
|
|
|
+ .filter(n -> n.getId().equals(currentTask.getNodeId()))
|
|
|
+ .findFirst()
|
|
|
+ .orElse(null);
|
|
|
+ String approveMode = currentNode != null ? this.getApproveMode(currentNode) : "or";
|
|
|
+
|
|
|
+ // 会签模式:检查同节点是否还有未处理的任务
|
|
|
+ if ("and".equals(approveMode)) {
|
|
|
+ long pendingCount = this.approvalTaskMapper.selectCount(
|
|
|
+ new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<ApprovalTask>()
|
|
|
+ .eq(ApprovalTask::getInstanceId, instance.getId())
|
|
|
+ .eq(ApprovalTask::getNodeId, currentTask.getNodeId())
|
|
|
+ .eq(ApprovalTask::getTaskStatus, TaskStatus.PENDING.getCode()));
|
|
|
+ if (pendingCount > 0) {
|
|
|
+ // 还有未处理的任务,不推进流程
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 或签模式:将同节点其他 PENDING 任务标记为 SKIPPED
|
|
|
+ this.approvalTaskMapper.update(null,
|
|
|
+ new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<ApprovalTask>()
|
|
|
+ .eq(ApprovalTask::getInstanceId, instance.getId())
|
|
|
+ .eq(ApprovalTask::getNodeId, currentTask.getNodeId())
|
|
|
+ .eq(ApprovalTask::getTaskStatus, TaskStatus.PENDING.getCode())
|
|
|
+ .ne(ApprovalTask::getId, currentTask.getId())
|
|
|
+ .set(ApprovalTask::getTaskStatus, TaskStatus.SKIPPED.getCode()));
|
|
|
+ }
|
|
|
+
|
|
|
+ List<FlowNode> nextNodes = this.getNextNodes(model, currentTask.getNodeId());
|
|
|
+ if (this.isEndNode(nextNodes)) {
|
|
|
+ this.completeInstance(instance);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.createTasksForNodes(instance, nextNodes, model);
|
|
|
+ this.updateInstanceNode(instance, nextNodes);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void handleReject(ProcessInstance instance, ApprovalTask currentTask, String comment) {
|
|
|
+ this.completeCurrentTask(currentTask, ApprovalResult.REJECT.getCode(), comment);
|
|
|
+ this.rejectInstance(instance);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void handleReturn(ProcessInstance instance, ApprovalTask currentTask, String comment) {
|
|
|
+ this.updateTaskStatus(currentTask, TaskStatus.RETURNED.getCode(), comment);
|
|
|
+ FlowModel model = this.getModelByInstance(instance);
|
|
|
+ FlowNode prevNode = this.findPreviousNode(model, currentTask.getNodeId());
|
|
|
+ if (prevNode == null) {
|
|
|
+ throw new BusinessException("无法找到回退目标节点");
|
|
|
+ }
|
|
|
+ List<FlowNode> returnTargets = Collections.singletonList(prevNode);
|
|
|
+ this.createTasksForNodes(instance, returnTargets, model);
|
|
|
+ this.updateInstanceNode(instance, returnTargets);
|
|
|
+ }
|
|
|
+
|
|
|
+ private FlowModel getModelByInstance(ProcessInstance instance) {
|
|
|
+ ProcessDefinition definition = this.processDefinitionMapper.selectById(instance.getProcessDefinitionId());
|
|
|
+ if (definition == null) {
|
|
|
+ throw new BusinessException("流程定义不存在");
|
|
|
+ }
|
|
|
+ return this.parseModel(definition.getModelJson());
|
|
|
+ }
|
|
|
+
|
|
|
+ private FlowNode findStartNode(FlowModel model) {
|
|
|
+ return model.getNodes().stream()
|
|
|
+ .filter(n -> NodeType.START.getCode().equals(n.getType()))
|
|
|
+ .findFirst()
|
|
|
+ .orElseThrow(() -> new BusinessException("未找到开始节点"));
|
|
|
+ }
|
|
|
+
|
|
|
+ private FlowNode findPreviousNode(FlowModel model, String currentNodeId) {
|
|
|
+ List<String> sourceIds = model.getEdges().stream()
|
|
|
+ .filter(e -> Objects.equals(e.getTargetNodeId(), currentNodeId))
|
|
|
+ .map(FlowEdge::getSourceNodeId)
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ return model.getNodes().stream()
|
|
|
+ .filter(n -> sourceIds.contains(n.getId()) && !NodeType.START.getCode().equals(n.getType()))
|
|
|
+ .findFirst()
|
|
|
+ .orElse(null);
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean isEndNode(List<FlowNode> nodes) {
|
|
|
+ return nodes.stream().anyMatch(n -> NodeType.END.getCode().equals(n.getType()));
|
|
|
+ }
|
|
|
+
|
|
|
+ private void createTasksForNodes(ProcessInstance instance, List<FlowNode> nodes, FlowModel model) {
|
|
|
+ ProcessDefinition definition = this.processDefinitionMapper.selectById(instance.getProcessDefinitionId());
|
|
|
+ String processName = definition != null ? definition.getProcessName() : "";
|
|
|
+ for (FlowNode node : nodes) {
|
|
|
+ if (NodeType.START.getCode().equals(node.getType()) || NodeType.END.getCode().equals(node.getType())) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ // CC节点:不创建审批任务,仅记录(后续可扩展抄送通知)
|
|
|
+ if (NodeType.CC.getCode().equals(node.getType())) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ List<Long> assignees = this.calculateAssignees(node, instance);
|
|
|
+ if (assignees.isEmpty()) {
|
|
|
+ assignees = Collections.singletonList(instance.getApplicantId());
|
|
|
+ }
|
|
|
+ String approveMode = this.getApproveMode(node);
|
|
|
+ List<Long> finalAssignees = assignees;
|
|
|
+ if ("or".equals(approveMode) && assignees.size() > 1) {
|
|
|
+ finalAssignees = assignees;
|
|
|
+ }
|
|
|
+ for (Long assigneeId : finalAssignees) {
|
|
|
+ ApprovalTask task = this.approvalTaskAssembler.buildNew(
|
|
|
+ instance.getId(), node.getId(), node.getName(),
|
|
|
+ node.getType(), assigneeId, "USER", TaskStatus.PENDING.getCode()
|
|
|
+ );
|
|
|
+ this.approvalTaskMapper.insert(task);
|
|
|
+ }
|
|
|
+ // 发布任务分配通知事件
|
|
|
+ this.eventPublisher.publishEvent(new TaskAssignedEvent(
|
|
|
+ this, instance.getId(), instance.getTitle(),
|
|
|
+ processName, node.getName(), finalAssignees
|
|
|
+ ));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void updateInstanceNode(ProcessInstance instance, List<FlowNode> nodes) {
|
|
|
+ FlowNode firstNode = nodes.stream()
|
|
|
+ .filter(n -> !NodeType.START.getCode().equals(n.getType()) && !NodeType.END.getCode().equals(n.getType()))
|
|
|
+ .findFirst()
|
|
|
+ .orElse(null);
|
|
|
+ if (firstNode == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ Integer status = ProcessStatus.PENDING.getCode();
|
|
|
+ this.processInstanceMapper.update(null,
|
|
|
+ new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<ProcessInstance>()
|
|
|
+ .eq(ProcessInstance::getId, instance.getId())
|
|
|
+ .set(ProcessInstance::getCurrentNodeId, firstNode.getId())
|
|
|
+ .set(ProcessInstance::getStatus, status));
|
|
|
+ }
|
|
|
+
|
|
|
+ private void completeInstance(ProcessInstance instance) {
|
|
|
+ this.processInstanceMapper.update(null,
|
|
|
+ new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<ProcessInstance>()
|
|
|
+ .eq(ProcessInstance::getId, instance.getId())
|
|
|
+ .set(ProcessInstance::getStatus, ProcessStatus.COMPLETED.getCode())
|
|
|
+ .set(ProcessInstance::getEndTime, LocalDateTime.now())
|
|
|
+ .set(ProcessInstance::getResult, ApprovalResult.PASS.getCode()));
|
|
|
+ ProcessDefinition definition = this.processDefinitionMapper.selectById(instance.getProcessDefinitionId());
|
|
|
+ String processName = definition != null ? definition.getProcessName() : "";
|
|
|
+ this.eventPublisher.publishEvent(new ProcessCompletedEvent(
|
|
|
+ this, instance.getId(), instance.getTitle(),
|
|
|
+ processName, instance.getApplicantId(), ApprovalResult.PASS.getCode()
|
|
|
+ ));
|
|
|
+ }
|
|
|
+
|
|
|
+ private void rejectInstance(ProcessInstance instance) {
|
|
|
+ this.processInstanceMapper.update(null,
|
|
|
+ new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<ProcessInstance>()
|
|
|
+ .eq(ProcessInstance::getId, instance.getId())
|
|
|
+ .set(ProcessInstance::getStatus, ProcessStatus.REJECTED.getCode())
|
|
|
+ .set(ProcessInstance::getResult, ApprovalResult.REJECT.getCode())
|
|
|
+ .set(ProcessInstance::getEndTime, LocalDateTime.now()));
|
|
|
+ ProcessDefinition definition = this.processDefinitionMapper.selectById(instance.getProcessDefinitionId());
|
|
|
+ String processName = definition != null ? definition.getProcessName() : "";
|
|
|
+ this.eventPublisher.publishEvent(new ProcessCompletedEvent(
|
|
|
+ this, instance.getId(), instance.getTitle(),
|
|
|
+ processName, instance.getApplicantId(), ApprovalResult.REJECT.getCode()
|
|
|
+ ));
|
|
|
+ }
|
|
|
+
|
|
|
+ private void completeCurrentTask(ApprovalTask task, String result, String comment) {
|
|
|
+ this.approvalTaskMapper.update(null,
|
|
|
+ new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<ApprovalTask>()
|
|
|
+ .eq(ApprovalTask::getId, task.getId())
|
|
|
+ .set(ApprovalTask::getTaskStatus, TaskStatus.HANDLED.getCode())
|
|
|
+ .set(ApprovalTask::getApprovalResult, result)
|
|
|
+ .set(ApprovalTask::getApprovalComment, comment)
|
|
|
+ .set(ApprovalTask::getHandleTime, LocalDateTime.now()));
|
|
|
+ }
|
|
|
+
|
|
|
+ private void updateTaskStatus(ApprovalTask task, Integer status, String comment) {
|
|
|
+ this.approvalTaskMapper.update(null,
|
|
|
+ new com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper<ApprovalTask>()
|
|
|
+ .eq(ApprovalTask::getId, task.getId())
|
|
|
+ .set(ApprovalTask::getTaskStatus, status)
|
|
|
+ .set(ApprovalTask::getApprovalComment, comment)
|
|
|
+ .set(ApprovalTask::getHandleTime, LocalDateTime.now()));
|
|
|
+ }
|
|
|
+}
|