diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwChartExtServiceImpl.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwChartExtServiceImpl.java index 7c9cd1747..2803388e3 100644 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwChartExtServiceImpl.java +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwChartExtServiceImpl.java @@ -20,10 +20,15 @@ import org.dromara.warm.flow.core.dto.PromptContent; import org.dromara.warm.flow.core.enums.NodeType; import org.dromara.warm.flow.core.utils.MapUtil; import org.dromara.warm.flow.orm.entity.FlowHisTask; +import org.dromara.warm.flow.orm.entity.FlowTask; +import org.dromara.warm.flow.orm.entity.FlowUser; import org.dromara.warm.flow.orm.mapper.FlowHisTaskMapper; +import org.dromara.warm.flow.orm.mapper.FlowTaskMapper; +import org.dromara.warm.flow.orm.mapper.FlowUserMapper; import org.dromara.warm.flow.ui.service.ChartExtService; import org.dromara.workflow.common.ConditionalOnEnable; import org.dromara.workflow.common.constant.FlowConstant; +import org.dromara.workflow.common.enums.TaskAssigneeType; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @@ -45,6 +50,8 @@ public class FlwChartExtServiceImpl implements ChartExtService { private final UserService userService; private final DeptService deptService; private final FlowHisTaskMapper flowHisTaskMapper; + private final FlowTaskMapper flowTaskMapper; + private final FlowUserMapper flowUserMapper; private final DictService dictService; @Value("${warm-flow.node-tooltip:true}") private boolean nodeTooltip; @@ -61,45 +68,80 @@ public class FlwChartExtServiceImpl implements ChartExtService { return; } - // 根据流程实例ID查询所有相关的历史任务列表 - List flowHisTasks = this.getHisTaskGroupedByNode(defJson.getInstance().getId()); - if (CollUtil.isEmpty(flowHisTasks)) { + Long instanceId = defJson.getInstance().getId(); + List flowHisTasks = this.getHisTaskGroupedByNode(instanceId); + List activeTasks = flowTaskMapper.selectList(Wrappers.lambdaQuery() + .eq(FlowTask::getInstanceId, instanceId) + .eq(FlowTask::getNodeType, NodeType.BETWEEN.getKey())); + if (CollUtil.isEmpty(flowHisTasks) && CollUtil.isEmpty(activeTasks)) { return; } - // 按节点编号(nodeCode)对历史任务进行分组 - Map> groupedByNode = StreamUtils.groupByKey(flowHisTasks, FlowHisTask::getNodeCode); + Map> groupedByNode = CollUtil.isEmpty(flowHisTasks) + ? Collections.emptyMap() : StreamUtils.groupByKey(flowHisTasks, FlowHisTask::getNodeCode); + Map taskNodeCodes = activeTasks.stream() + .collect(Collectors.toMap(FlowTask::getId, FlowTask::getNodeCode)); + List activeUsers = taskNodeCodes.isEmpty() ? Collections.emptyList() + : flowUserMapper.selectList(Wrappers.lambdaQuery() + .in(FlowUser::getAssociated, taskNodeCodes.keySet()) + .in(FlowUser::getType, List.of(TaskAssigneeType.APPROVER.getCode(), + TaskAssigneeType.TRANSFER.getCode(), TaskAssigneeType.DELEGATE.getCode()))); + Map> pendingUsersByNode = new HashMap<>(); + for (FlowUser activeUser : activeUsers) { + Long userId = Convert.toLong(activeUser.getProcessedBy(), null); + String nodeCode = taskNodeCodes.get(activeUser.getAssociated()); + if (userId != null && nodeCode != null) { + pendingUsersByNode.computeIfAbsent(nodeCode, key -> new LinkedHashSet<>()).add(userId); + } + } - // 批量查询所有审批人的用户信息 - List userDTOList = userService.selectListByIds(StreamUtils.toSet(flowHisTasks, e -> Convert.toLong(e.getApprover()))); + Set userIds = new HashSet<>(); + flowHisTasks.forEach(task -> { + Long approverId = Convert.toLong(task.getApprover(), null); + if (approverId != null) { + userIds.add(approverId); + } + }); + pendingUsersByNode.values().forEach(userIds::addAll); + if (userIds.isEmpty()) { + return; + } + List userDTOList = userService.selectListByIds(userIds); // 将查询到的用户列表转换为以用户ID为key的映射 Map userMap = StreamUtils.toIdentityMap(userDTOList, UserDTO::getUserId); Set deptIds = StreamUtils.toSet(userDTOList, UserDTO::getDeptId); - Map deptNameMap = deptService.selectDeptNamesByIds(deptIds); + deptIds.remove(null); + Map deptNameMap = deptIds.isEmpty() ? Collections.emptyMap() + : deptService.selectDeptNamesByIds(deptIds); - Map dictType = dictService.getAllDictByDictType(FlowConstant.WF_TASK_STATUS); + Map dictType = flowHisTasks.isEmpty() ? Collections.emptyMap() + : dictService.getAllDictByDictType(FlowConstant.WF_TASK_STATUS); for (NodeJson nodeJson : defJson.getNodeList()) { List taskList = groupedByNode.get(nodeJson.getNodeCode()); - if (CollUtil.isEmpty(taskList)) { - continue; + if (CollUtil.isNotEmpty(taskList)) { + List latestPerApprover = taskList.stream() + .filter(task -> StringUtils.isNotBlank(task.getApprover())) + .collect(Collectors.collectingAndThen( + Collectors.toMap( + FlowHisTask::getApprover, + Function.identity(), + (oldTask, newTask) -> newTask.getUpdateTime().after(oldTask.getUpdateTime()) ? newTask : oldTask, + LinkedHashMap::new + ), + map -> new ArrayList<>(map.values()) + )); + this.processNodeExtInfo(nodeJson, latestPerApprover, userMap, deptNameMap, dictType); + } + for (Long userId : pendingUsersByNode.getOrDefault(nodeJson.getNodeCode(), Collections.emptySet())) { + UserDTO user = userMap.get(userId); + if (user != null) { + addUserInfo(nodeJson.getPromptContent().getInfo(), user, + deptNameMap.getOrDefault(user.getDeptId(), StringUtils.EMPTY)); + nodeJson.getPromptContent().getInfo().add(buildInfoItem("审批状态", "待审批")); + } } - - // 按审批人分组去重,保留最新处理记录,最终转换成 List - List latestPerApprover = taskList.stream() - .collect(Collectors.collectingAndThen( - Collectors.toMap( - FlowHisTask::getApprover, - Function.identity(), - (oldTask, newTask) -> newTask.getUpdateTime().after(oldTask.getUpdateTime()) ? newTask : oldTask, - LinkedHashMap::new - ), - map -> new ArrayList<>(map.values()) - )); - - // 处理当前节点的扩展信息 - this.processNodeExtInfo(nodeJson, latestPerApprover, userMap, deptNameMap, dictType); } } @@ -183,10 +225,16 @@ public class FlwChartExtServiceImpl implements ChartExtService { continue; } - String deptName = deptNameMap.getOrDefault(userDTO.getDeptId(), StringUtils.EMPTY); + addUserInfo(info, userDTO, deptNameMap.getOrDefault(userDTO.getDeptId(), StringUtils.EMPTY)); - // 添加标题项,如:👤 张三(市场部) - info.add(new PromptContent.InfoItem() + info.add(buildInfoItem("审批状态", dictType.get(task.getFlowStatus()))); + info.add(buildInfoItem("审批耗时", DateUtils.formatBetweenBySecond(task.getUpdateTime(), task.getCreateTime()))); + info.add(buildInfoItem("办理时间", DateUtils.formatDateTime(task.getUpdateTime()))); + } + } + + private void addUserInfo(List info, UserDTO userDTO, String deptName) { + info.add(new PromptContent.InfoItem() .setPrefix(StringUtils.format("👥 {}({})", userDTO.getNickName(), deptName)) .setPrefixStyle(Map.of( "fontWeight", "bold", @@ -199,12 +247,7 @@ public class FlwChartExtServiceImpl implements ChartExtService { )) ); - // 添加具体信息项:账号、耗时、时间 - info.add(buildInfoItem("用户账号", userDTO.getUserName())); - info.add(buildInfoItem("审批状态", dictType.get(task.getFlowStatus()))); - info.add(buildInfoItem("审批耗时", DateUtils.formatBetweenBySecond(task.getUpdateTime(), task.getCreateTime()))); - info.add(buildInfoItem("办理时间", DateUtils.formatDateTime(task.getUpdateTime()))); - } + info.add(buildInfoItem("用户账号", userDTO.getUserName())); } /** diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwDefinitionServiceImpl.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwDefinitionServiceImpl.java index 3f9d167cd..b24d4fe37 100644 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwDefinitionServiceImpl.java +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwDefinitionServiceImpl.java @@ -126,7 +126,7 @@ public class FlwDefinitionServiceImpl implements IFlwDefinitionService { if (CollUtil.isNotEmpty(flowNodes)) { String applyNodeCode = flwCommonService.applyNodeCode(id); for (FlowNode flowNode : flowNodes) { - if (StringUtils.isBlank(flowNode.getPermissionFlag()) && !applyNodeCode.equals(flowNode.getNodeCode()) && NodeType.BETWEEN.getKey().equals(flowNode.getNodeType())) { + if (requiresAssignee(flowNode, applyNodeCode) && StringUtils.isBlank(flowNode.getPermissionFlag())) { errorMsg.add(flowNode.getNodeName()); } } @@ -137,6 +137,12 @@ public class FlwDefinitionServiceImpl implements IFlwDefinitionService { return defService.publish(id); } + static boolean requiresAssignee(FlowNode node, String applyNodeCode) { + return NodeType.BETWEEN.getKey().equals(node.getNodeType()) + && !applyNodeCode.equals(node.getNodeCode()) + && StringUtils.isBlank(node.getEventType()); + } + /** * 导入流程定义 * diff --git a/ruoyi-modules/ruoyi-workflow/src/test/java/org/dromara/workflow/service/impl/FlwChartExtServiceImplTest.java b/ruoyi-modules/ruoyi-workflow/src/test/java/org/dromara/workflow/service/impl/FlwChartExtServiceImplTest.java new file mode 100644 index 000000000..edd16e697 --- /dev/null +++ b/ruoyi-modules/ruoyi-workflow/src/test/java/org/dromara/workflow/service/impl/FlwChartExtServiceImplTest.java @@ -0,0 +1,81 @@ +package org.dromara.workflow.service.impl; + +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import org.apache.ibatis.builder.MapperBuilderAssistant; +import org.apache.ibatis.session.Configuration; +import org.dromara.common.core.service.DictService; +import org.dromara.system.api.DeptService; +import org.dromara.system.api.UserService; +import org.dromara.system.api.domain.UserDTO; +import org.dromara.warm.flow.core.dto.DefJson; +import org.dromara.warm.flow.core.dto.NodeJson; +import org.dromara.warm.flow.orm.entity.FlowInstance; +import org.dromara.warm.flow.orm.entity.FlowTask; +import org.dromara.warm.flow.orm.entity.FlowUser; +import org.dromara.warm.flow.orm.mapper.FlowHisTaskMapper; +import org.dromara.warm.flow.orm.mapper.FlowTaskMapper; +import org.dromara.warm.flow.orm.mapper.FlowUserMapper; +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class FlwChartExtServiceImplTest { + + @Test + void showsCurrentAssigneeWithoutApprovalHistory() { + TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new Configuration(), "test"), FlowUser.class); + UserService userService = mock(UserService.class); + DeptService deptService = mock(DeptService.class); + FlowHisTaskMapper historyMapper = mock(FlowHisTaskMapper.class); + FlowTaskMapper taskMapper = mock(FlowTaskMapper.class); + FlowUserMapper flowUserMapper = mock(FlowUserMapper.class); + DictService dictService = mock(DictService.class); + FlwChartExtServiceImpl service = new FlwChartExtServiceImpl( + userService, deptService, historyMapper, taskMapper, flowUserMapper, dictService); + ReflectionTestUtils.setField(service, "nodeTooltip", true); + + NodeJson node = new NodeJson().setNodeCode("approve").setNodeName("部门审批"); + DefJson definition = new DefJson() + .setFlowName("请假申请") + .setInstance(new FlowInstance().setId(1L)) + .setNodeList(List.of(node)); + service.initPromptContent(definition); + + when(historyMapper.selectList(any())).thenReturn(List.of()); + when(taskMapper.selectList(any())).thenReturn(List.of( + new FlowTask().setId(10L).setNodeCode("approve"))); + when(flowUserMapper.selectList(any())).thenReturn(List.of( + new FlowUser().setAssociated(10L).setType("1").setProcessedBy("7"))); + UserDTO user = new UserDTO(); + user.setUserId(7L); + user.setDeptId(3L); + user.setNickName("张三"); + user.setUserName("zhangsan"); + when(userService.selectListByIds(any())).thenReturn(List.of(user)); + when(deptService.selectDeptNamesByIds(any())).thenReturn(Map.of(3L, "研发部")); + when(dictService.getAllDictByDictType(any())).thenReturn(Map.of()); + + service.execute(definition); + + var info = node.getPromptContent().getInfo(); + assertTrue(info.stream().anyMatch(item -> item.getPrefix().contains("张三") + && item.getPrefix().contains("研发部"))); + assertTrue(info.stream().anyMatch(item -> "用户账号: ".equals(item.getPrefix()) + && "zhangsan".equals(item.getContent()))); + assertTrue(info.stream().anyMatch(item -> "审批状态: ".equals(item.getPrefix()) + && "待审批".equals(item.getContent()))); + verify(userService).selectListByIds(argThat(ids -> ids.size() == 1 && ids.contains(7L))); + verify(deptService).selectDeptNamesByIds(argThat(ids -> ids.size() == 1 && ids.contains(3L))); + assertEquals(4, info.size()); + } +} diff --git a/ruoyi-modules/ruoyi-workflow/src/test/java/org/dromara/workflow/service/impl/FlwDefinitionServiceImplTest.java b/ruoyi-modules/ruoyi-workflow/src/test/java/org/dromara/workflow/service/impl/FlwDefinitionServiceImplTest.java new file mode 100644 index 000000000..f2b73a448 --- /dev/null +++ b/ruoyi-modules/ruoyi-workflow/src/test/java/org/dromara/workflow/service/impl/FlwDefinitionServiceImplTest.java @@ -0,0 +1,39 @@ +package org.dromara.workflow.service.impl; + +import org.dromara.warm.flow.core.enums.NodeType; +import org.dromara.warm.flow.orm.entity.FlowNode; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class FlwDefinitionServiceImplTest { + + @Test + void eventNodesDoNotRequireAssignee() { + for (String eventType : new String[]{"MESSAGE_START", "TIMER_CATCH", "ERROR_BOUNDARY", "TERMINATE_END"}) { + FlowNode node = new FlowNode() + .setNodeType(NodeType.BETWEEN.getKey()) + .setNodeCode("event") + .setEventType(eventType); + assertFalse(FlwDefinitionServiceImpl.requiresAssignee(node, "apply"), eventType); + } + } + + @Test + void manualApprovalStillRequiresAssignee() { + FlowNode approval = new FlowNode() + .setNodeType(NodeType.BETWEEN.getKey()) + .setNodeCode("approve"); + assertTrue(FlwDefinitionServiceImpl.requiresAssignee(approval, "apply")); + assertFalse(FlwDefinitionServiceImpl.requiresAssignee(approval, "approve")); + } + + @Test + void serviceTaskDoesNotRequireAssignee() { + FlowNode service = new FlowNode() + .setNodeType(NodeType.SERVICE.getKey()) + .setNodeCode("service"); + assertFalse(FlwDefinitionServiceImpl.requiresAssignee(service, "apply")); + } +}