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;
+ }
+}