From 779bad9314f74ac92736206c4b63e0dba5ccc686 Mon Sep 17 00:00:00 2001 From: dzc <2388224167@qq.com> Date: Tue, 1 Sep 2026 17:40:02 +0800 Subject: [PATCH] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E5=85=85=E5=80=BC/?= =?UTF-8?q?=E6=89=A3=E6=AC=BE=E7=94=B3=E8=AF=B7=E4=B8=AD=E5=BC=80=E5=90=AF?= =?UTF-8?q?=E7=94=B3=E8=AF=B7=E6=B2=A1=E6=9C=89=E6=9F=A5=E8=AF=A2=E5=88=B0?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/EngineFlowController.java | 31 +++++-- .../engine/node/MultiInstanceHandler.java | 10 ++- .../controller/EngineFlowControllerTest.java | 90 +++++++++++++++++++ .../engine/node/MultiInstanceHandlerTest.java | 51 +++++++++++ .../service/impl/SysUserServiceImpl.java | 12 ++- .../impl/SysUserRoleResolutionTest.java | 52 +++++++++++ 6 files changed, 234 insertions(+), 12 deletions(-) create mode 100644 pjcloud-flow/pjcloud-flow-engine/pjcloud-flow-engine-biz/src/test/java/com/bsd/pjcloud/flow/engine/controller/EngineFlowControllerTest.java create mode 100644 pjcloud-flow/pjcloud-flow-engine/pjcloud-flow-engine-biz/src/test/java/com/bsd/pjcloud/flow/engine/node/MultiInstanceHandlerTest.java create mode 100644 pjcloud-upms/pjcloud-upms-biz/src/test/java/com/bsd/pjcloud/admin/service/impl/SysUserRoleResolutionTest.java diff --git a/pjcloud-flow/pjcloud-flow-engine/pjcloud-flow-engine-biz/src/main/java/com/bsd/pjcloud/flow/engine/controller/EngineFlowController.java b/pjcloud-flow/pjcloud-flow-engine/pjcloud-flow-engine-biz/src/main/java/com/bsd/pjcloud/flow/engine/controller/EngineFlowController.java index 573785ecd..0be08bc8d 100644 --- a/pjcloud-flow/pjcloud-flow-engine/pjcloud-flow-engine-biz/src/main/java/com/bsd/pjcloud/flow/engine/controller/EngineFlowController.java +++ b/pjcloud-flow/pjcloud-flow-engine/pjcloud-flow-engine-biz/src/main/java/com/bsd/pjcloud/flow/engine/controller/EngineFlowController.java @@ -297,18 +297,35 @@ public class EngineFlowController { return R.ok(pageResultDto); } + /** + * 根据流程实例查询当前用户最新的一条活动待办任务。 + * + *

正常情况下同一用户在一个流程实例中只有一条活动待办。历史流程可能因用户同时拥有多个审批角色而 + * 产生重复任务,此处兼容返回最新任务,避免业务页面无法继续处理,并通过告警保留异常排查信息。

+ * + * @param processInstanceId 流程实例 ID + * @return 当前用户最新的活动待办;不存在时返回明确失败信息 + */ @GetMapping("/queryAssignTaskOne") public R queryAssignTaskOne(@RequestParam String processInstanceId) { String assign = SecurityUtils.getUser().getId().toString(); - TaskQuery taskQuery = taskService.createTaskQuery().taskAssignee(assign).orderByTaskCreateTime().processInstanceId(processInstanceId).active().desc(); - long count = taskQuery.count(); - if (count != 1) { - log.debug("queryAssignTaskOne查询错误,count!=1,processInstanceId:{}", processInstanceId); - return R.failed(); + TaskQuery taskQuery = taskService.createTaskQuery() + .taskAssignee(assign) + .processInstanceId(processInstanceId) + .active() + .orderByTaskCreateTime() + .desc(); + List taskList = taskQuery.listPage(0, 2); + if (CollUtil.isEmpty(taskList)) { + log.debug("当前用户没有活动待办,userId={},processInstanceId={}", assign, processInstanceId); + return R.failed("当前用户没有可处理的待办任务"); } - Task task = taskQuery.singleResult(); + if (taskList.size() > 1) { + log.warn("同一用户在同一流程实例中存在多条活动待办,将返回最新任务,userId={},processInstanceId={}", assign, processInstanceId); + } + Task task = taskList.get(0); String taskId = task.getId(); - log.debug("queryAssignTaskOne查询错误(taskId) {} processInstanceId={} executrionId={}", task.getName(), processInstanceId, task.getExecutionId()); + log.debug("查询到活动待办(taskId) {} processInstanceId={} executionId={}", taskId, processInstanceId, task.getExecutionId()); String taskDefinitionKey = task.getTaskDefinitionKey(); String processDefinitionId = task.getProcessDefinitionId(); String flowId = NodeUtil.getFlowId(processDefinitionId); diff --git a/pjcloud-flow/pjcloud-flow-engine/pjcloud-flow-engine-biz/src/main/java/com/bsd/pjcloud/flow/engine/node/MultiInstanceHandler.java b/pjcloud-flow/pjcloud-flow-engine/pjcloud-flow-engine-biz/src/main/java/com/bsd/pjcloud/flow/engine/node/MultiInstanceHandler.java index 04fe11084..83670aac5 100644 --- a/pjcloud-flow/pjcloud-flow-engine/pjcloud-flow-engine-biz/src/main/java/com/bsd/pjcloud/flow/engine/node/MultiInstanceHandler.java +++ b/pjcloud-flow/pjcloud-flow-engine/pjcloud-flow-engine-biz/src/main/java/com/bsd/pjcloud/flow/engine/node/MultiInstanceHandler.java @@ -18,6 +18,7 @@ import org.springframework.stereotype.Component; import java.math.BigDecimal; import java.util.ArrayList; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -78,11 +79,12 @@ public class MultiInstanceHandler { } } - // 标记为空 - if (CollUtil.isEmpty(assignList)) { - assignList.add(ProcessInstanceConstant.DEFAULT_EMPTY_ASSIGN); + // 不同角色、部门或自选来源可能解析到同一用户,多实例任务必须按用户唯一创建。 + List distinctAssignList = new ArrayList<>(new LinkedHashSet<>(assignList)); + if (CollUtil.isEmpty(distinctAssignList)) { + distinctAssignList.add(ProcessInstanceConstant.DEFAULT_EMPTY_ASSIGN); } - return assignList; + return distinctAssignList; } /** diff --git a/pjcloud-flow/pjcloud-flow-engine/pjcloud-flow-engine-biz/src/test/java/com/bsd/pjcloud/flow/engine/controller/EngineFlowControllerTest.java b/pjcloud-flow/pjcloud-flow-engine/pjcloud-flow-engine-biz/src/test/java/com/bsd/pjcloud/flow/engine/controller/EngineFlowControllerTest.java new file mode 100644 index 000000000..7be4c8df8 --- /dev/null +++ b/pjcloud-flow/pjcloud-flow-engine/pjcloud-flow-engine-biz/src/test/java/com/bsd/pjcloud/flow/engine/controller/EngineFlowControllerTest.java @@ -0,0 +1,90 @@ +package com.bsd.pjcloud.flow.engine.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.bsd.pjcloud.common.core.util.R; +import com.bsd.pjcloud.common.security.service.PjcloudUser; +import com.bsd.pjcloud.flow.task.dto.TaskDto; +import org.flowable.engine.HistoryService; +import org.flowable.engine.RepositoryService; +import org.flowable.engine.RuntimeService; +import org.flowable.engine.TaskService; +import org.flowable.task.api.Task; +import org.flowable.task.api.TaskQuery; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * 工作流查询控制器测试,覆盖历史重复待办数据的兼容读取行为。 + */ +class EngineFlowControllerTest { + + /** + * 每条测试结束后清理认证上下文,避免登录用户泄漏到其他测试。 + */ + @AfterEach + void clearSecurityContext() { + SecurityContextHolder.clearContext(); + } + + /** + * 同一用户存在两条历史重复活动待办时,应返回按创建时间倒序查询到的第一条任务。 + */ + @Test + void shouldReturnLatestTaskWhenHistoricalDuplicateTasksExist() { + PjcloudUser user = mock(PjcloudUser.class); + when(user.getId()).thenReturn(1L); + SecurityContextHolder.getContext().setAuthentication( + new UsernamePasswordAuthenticationToken(user, null, List.of())); + + TaskService taskService = mock(TaskService.class); + TaskQuery taskQuery = mock(TaskQuery.class); + when(taskService.createTaskQuery()).thenReturn(taskQuery); + when(taskQuery.taskAssignee("1")).thenReturn(taskQuery); + when(taskQuery.processInstanceId("process-instance")).thenReturn(taskQuery); + when(taskQuery.active()).thenReturn(taskQuery); + when(taskQuery.orderByTaskCreateTime()).thenReturn(taskQuery); + when(taskQuery.desc()).thenReturn(taskQuery); + Task latestTask = task("latest-task"); + Task earlierTask = task("earlier-task"); + when(taskQuery.listPage(0, 2)).thenReturn(List.of(latestTask, earlierTask)); + EngineFlowController controller = new EngineFlowController( + taskService, + mock(HistoryService.class), + mock(RepositoryService.class), + mock(RuntimeService.class), + new ObjectMapper()); + + R result = controller.queryAssignTaskOne("process-instance"); + + assertEquals(0, result.getCode()); + assertEquals("latest-task", result.getData().getTaskId()); + verify(taskQuery, never()).singleResult(); + } + + /** + * 构造当前用户的活动审批任务。 + * + * @param taskId 任务 ID + * @return Flowable 任务 + */ + private static Task task(String taskId) { + Task task = mock(Task.class); + when(task.getId()).thenReturn(taskId); + when(task.getName()).thenReturn("审批人"); + when(task.getExecutionId()).thenReturn(taskId + "-execution"); + when(task.getTaskDefinitionKey()).thenReturn("approve-node"); + when(task.getProcessDefinitionId()).thenReturn("test-flow:1:definition"); + when(task.getAssignee()).thenReturn("1"); + return task; + } +} diff --git a/pjcloud-flow/pjcloud-flow-engine/pjcloud-flow-engine-biz/src/test/java/com/bsd/pjcloud/flow/engine/node/MultiInstanceHandlerTest.java b/pjcloud-flow/pjcloud-flow-engine/pjcloud-flow-engine-biz/src/test/java/com/bsd/pjcloud/flow/engine/node/MultiInstanceHandlerTest.java new file mode 100644 index 000000000..1cd5fa714 --- /dev/null +++ b/pjcloud-flow/pjcloud-flow-engine/pjcloud-flow-engine-biz/src/test/java/com/bsd/pjcloud/flow/engine/node/MultiInstanceHandlerTest.java @@ -0,0 +1,51 @@ +package com.bsd.pjcloud.flow.engine.node; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.bsd.pjcloud.common.core.util.R; +import com.bsd.pjcloud.flow.task.api.feign.RemoteFlowTaskService; +import com.bsd.pjcloud.flow.task.constant.ProcessInstanceConstant; +import com.bsd.pjcloud.flow.task.dto.Node; +import com.bsd.pjcloud.flow.task.dto.NodeUser; +import org.flowable.engine.impl.persistence.entity.ExecutionEntityImpl; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * 多实例审批人解析测试,验证进入 Flowable 前的用户唯一性约束。 + */ +class MultiInstanceHandlerTest { + + /** + * 上游策略返回重复用户时,流程引擎只能为每个用户创建一个多实例任务。 + */ + @Test + void shouldDeduplicateAssigneesBeforeCreatingMultiInstanceTasks() throws Exception { + RemoteFlowTaskService remoteFlowTaskService = mock(RemoteFlowTaskService.class); + AssignUserStrategy assignUserStrategy = mock(AssignUserStrategy.class); + String strategyName = ProcessInstanceConstant.AssignedTypeClass.ROLE + "AssignUserStrategy"; + MultiInstanceHandler handler = new MultiInstanceHandler( + remoteFlowTaskService, Map.of(strategyName, assignUserStrategy), new ObjectMapper()); + + ExecutionEntityImpl execution = mock(ExecutionEntityImpl.class); + NodeUser rootUser = NodeUser.builder().id(9L).name("发起人").build(); + Node node = new Node(); + node.setAssignedType(ProcessInstanceConstant.AssignedTypeClass.ROLE); + when(execution.getProcessDefinitionKey()).thenReturn("test-flow"); + when(execution.getActivityId()).thenReturn("approve-node"); + when(execution.getActivityName()).thenReturn("审批人"); + when(execution.getVariable("root")).thenReturn(List.of(rootUser)); + when(execution.getVariables()).thenReturn(Map.of()); + when(remoteFlowTaskService.queryNodeOriData("test-flow", "approve-node")).thenReturn(R.ok(node)); + when(assignUserStrategy.handle(node, rootUser, Map.of())).thenReturn(List.of(1L, 2L, 1L)); + + List assignees = handler.resolveAssignee(execution); + + assertEquals(List.of(1L, 2L), assignees); + } +} diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysUserServiceImpl.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysUserServiceImpl.java index d81352d05..2a5557d11 100644 --- a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysUserServiceImpl.java +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysUserServiceImpl.java @@ -1084,9 +1084,19 @@ public class SysUserServiceImpl extends ServiceImpl impl return null; } + /** + * 根据角色 ID 列表查询唯一用户 ID,避免同一用户拥有多个目标角色时被重复返回。 + * + * @param roleIdList 角色 ID 列表 + * @return 按角色关系查询顺序去重后的用户 ID 列表 + */ @Override public List listUserIdByRoleIds(List roleIdList) { - return sysUserRoleMapper.selectList(Wrappers.lambdaQuery().in(SysUserRole::getRoleId, roleIdList)).stream().map(SysUserRole::getUserId).collect(Collectors.toList()); + return sysUserRoleMapper.selectList(Wrappers.lambdaQuery().in(SysUserRole::getRoleId, roleIdList)) + .stream() + .map(SysUserRole::getUserId) + .distinct() + .collect(Collectors.toList()); } /** diff --git a/pjcloud-upms/pjcloud-upms-biz/src/test/java/com/bsd/pjcloud/admin/service/impl/SysUserRoleResolutionTest.java b/pjcloud-upms/pjcloud-upms-biz/src/test/java/com/bsd/pjcloud/admin/service/impl/SysUserRoleResolutionTest.java new file mode 100644 index 000000000..85636f10a --- /dev/null +++ b/pjcloud-upms/pjcloud-upms-biz/src/test/java/com/bsd/pjcloud/admin/service/impl/SysUserRoleResolutionTest.java @@ -0,0 +1,52 @@ +package com.bsd.pjcloud.admin.service.impl; + +import com.bsd.pjcloud.admin.api.entity.SysUserRole; +import com.bsd.pjcloud.admin.mapper.SysUserRoleMapper; +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Answers.CALLS_REAL_METHODS; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * 用户角色解析测试,验证多个审批角色命中同一用户时只返回一个用户 ID。 + */ +class SysUserRoleResolutionTest { + + /** + * 同一用户同时拥有两个目标角色时,查询结果必须保持首次出现顺序并去重。 + */ + @Test + void shouldReturnDistinctUserIdsAcrossRoles() { + SysUserRoleMapper userRoleMapper = mock(SysUserRoleMapper.class); + SysUserServiceImpl userService = mock(SysUserServiceImpl.class, CALLS_REAL_METHODS); + ReflectionTestUtils.setField(userService, "sysUserRoleMapper", userRoleMapper); + when(userRoleMapper.selectList(any())).thenReturn(List.of( + userRole(1L, 101L), + userRole(2L, 101L), + userRole(1L, 102L))); + + List userIds = userService.listUserIdByRoleIds(List.of(101L, 102L)); + + assertEquals(List.of(1L, 2L), userIds); + } + + /** + * 构造测试使用的用户角色关系。 + * + * @param userId 用户 ID + * @param roleId 角色 ID + * @return 用户角色关系 + */ + private static SysUserRole userRole(Long userId, Long roleId) { + SysUserRole userRole = new SysUserRole(); + userRole.setUserId(userId); + userRole.setRoleId(roleId); + return userRole; + } +}