From 61ca559f97b82d04e181c403dccbb8c4eb105822 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Tue, 6 Oct 2026 19:27:30 +0800 Subject: [PATCH] fix(bpm): 修复流程详情表单权限错配 --- yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java | 91 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------- yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java | 13 +++++++++---- yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/BpmProcessInstanceQueryFlowableTest.java | 131 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-bpm/src/test/resources/bpmn/form-permission.bpmn20.xml | 29 +++++++++++++++++++++++++++++ 4 files changed, 247 insertions(+), 17 deletions(-) create mode 100644 yudao-module-bpm/src/test/resources/bpmn/form-permission.bpmn20.xml diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java index 9fed30f..3582157 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java @@ -22,6 +22,7 @@ import cn.iocoder.yudao.module.bpm.convert.task.BpmProcessInstanceConvert; import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO; import cn.iocoder.yudao.module.bpm.dal.redis.BpmProcessIdRedisDAO; import cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmFieldPermissionEnum; import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum; import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum; import cn.iocoder.yudao.module.bpm.enums.definition.BpmSimpleModelNodeTypeEnum; @@ -152,19 +153,82 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService .list(); } - private Map getFormFieldsPermission(BpmnModel bpmnModel, - String activityId, String taskId) { - // 1. 获取流程活动编号。流程活动 Id 为空事,从流程任务中获取流程活动 Id - if (StrUtil.isEmpty(activityId) && StrUtil.isNotEmpty(taskId)) { - activityId = Optional.ofNullable(taskService.getHistoricTask(taskId)) - .map(HistoricTaskInstance::getTaskDefinitionKey).orElse(null); + /** + * 获得当前登录用户的表单字段权限 + * + * @param loginUserId 当前登录用户编号 + * @param bpmnModel 流程模型 + * @param reqVO 审批详情请求 + * @param processInstance 流程实例;为 null 时,表示流程未发起 + * @param todoTask 当前登录用户的待办任务 + * @return 表单字段权限 + */ + private Map getFormFieldsPermission(Long loginUserId, BpmnModel bpmnModel, + BpmApprovalDetailReqVO reqVO, + HistoricProcessInstance processInstance, + BpmTaskRespVO todoTask) { + // 情况一:有待办任务,以待办任务所在节点的表单权限为准,可编辑 + if (todoTask != null) { + return BpmnModelUtils.parseFormFieldsPermission(bpmnModel, todoTask.getTaskDefinitionKey()); + } + // 情况二:流程未发起,使用传递的流程活动(例如说,发起人节点)的表单权限,可编辑 + if (processInstance == null) { + return BpmnModelUtils.parseFormFieldsPermission(bpmnModel, reqVO.getActivityId()); } + + // 情况三:流程已发起,但没有待办任务,只能查看 + // 3.1 优先,使用自己办理过的任务(已办)所在节点 + String activityId = getMyHistoricTaskDefinitionKey(loginUserId, reqVO.getTaskId(), processInstance.getId()); + // 3.2 其次,使用传递的流程活动(例如说,抄送节点) if (StrUtil.isEmpty(activityId)) { + activityId = reqVO.getActivityId(); + } + // 3.3 最后,如果是流程发起人(例如说,从“我的流程”进入),使用发起人节点 + if (StrUtil.isEmpty(activityId) && ObjUtil.equal(String.valueOf(loginUserId), processInstance.getStartUserId())) { + activityId = START_USER_NODE_ID; + } + // 没有待办任务,无法提交表单,所以将“可编辑”降级为“只读”,避免误导 + return downgradeWritePermission(BpmnModelUtils.parseFormFieldsPermission(bpmnModel, activityId)); + } + + /** + * 获得用户在指定流程实例中,办理过的 taskId 任务所在的节点编号 + * + * @param userId 用户编号 + * @param taskId 任务编号 + * @param processInstanceId 流程实例编号 + * @return 节点编号;任务不存在、不属于该流程实例、或者不是该用户办理的,返回 null + */ + private String getMyHistoricTaskDefinitionKey(Long userId, String taskId, String processInstanceId) { + if (StrUtil.isEmpty(taskId)) { return null; } + HistoricTaskInstance task = taskService.getHistoricTask(taskId); + if (task == null || ObjUtil.notEqual(task.getProcessInstanceId(), processInstanceId)) { + return null; + } + // 校验是该用户办理(审批人或拥有人)的任务 + String userIdStr = String.valueOf(userId); + if (ObjUtil.notEqual(userIdStr, task.getAssignee()) && ObjUtil.notEqual(userIdStr, task.getOwner())) { + return null; + } + return task.getTaskDefinitionKey(); + } - // 2. 从 BpmnModel 中解析表单字段权限 - return BpmnModelUtils.parseFormFieldsPermission(bpmnModel, activityId); + /** + * 将表单字段权限中的“可编辑”降级为“只读” + * + * @param fieldsPermission 表单字段权限 + * @return 表单字段权限 + */ + private static Map downgradeWritePermission(Map fieldsPermission) { + if (CollUtil.isEmpty(fieldsPermission)) { + return fieldsPermission; + } + String write = String.valueOf(BpmFieldPermissionEnum.WRITE.getPermission()); + String read = String.valueOf(BpmFieldPermissionEnum.READ.getPermission()); + fieldsPermission.replaceAll((field, permission) -> write.equals(permission) ? read : permission); + return fieldsPermission; } @Override @@ -221,7 +285,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService // 2.2 流程已经结束,直接 return,无需预测 if (BpmProcessInstanceStatusEnum.isProcessEndStatus(processInstanceStatus)) { - return buildApprovalDetail(reqVO, bpmnModel, processDefinition, processDefinitionInfo, + return buildApprovalDetail(loginUserId, reqVO, bpmnModel, processDefinition, processDefinitionInfo, historicProcessInstance, processInstanceStatus, endActivityNodes, runActivityNodes, null, null); } @@ -246,7 +310,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService processVariables, activities, needSimulateTaskDefKeysByReturn); // 4. 拼接最终数据 - return buildApprovalDetail(reqVO, bpmnModel, processDefinition, processDefinitionInfo, historicProcessInstance, + return buildApprovalDetail(loginUserId, reqVO, bpmnModel, processDefinition, processDefinitionInfo, historicProcessInstance, processInstanceStatus, endActivityNodes, runActivityNodes, simulateActivityNodes, todoTask); } @@ -371,7 +435,8 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService *

* 主要是,拼接审批人的用户信息、部门信息 */ - private BpmApprovalDetailRespVO buildApprovalDetail(BpmApprovalDetailReqVO reqVO, + private BpmApprovalDetailRespVO buildApprovalDetail(Long loginUserId, + BpmApprovalDetailReqVO reqVO, BpmnModel bpmnModel, ProcessDefinition processDefinition, BpmProcessDefinitionInfoDO processDefinitionInfo, @@ -389,8 +454,8 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService Map deptMap = deptApi.getDeptMap(convertSet(userMap.values(), AdminUserRespDTO::getDeptId)); // 2. 表单权限 - String taskId = reqVO.getTaskId() == null && todoTask != null ? todoTask.getId() : reqVO.getTaskId(); - Map formFieldsPermission = getFormFieldsPermission(bpmnModel, reqVO.getActivityId(), taskId); + Map formFieldsPermission = getFormFieldsPermission(loginUserId, bpmnModel, reqVO, + processInstance, todoTask); // 3. 拼接数据 return BpmProcessInstanceConvert.INSTANCE.buildApprovalDetail(bpmnModel, processDefinition, diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java index b1d045e..b103a0b 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java @@ -148,7 +148,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { @Override public BpmTaskRespVO getTodoTask(Long userId, String taskId, String processInstanceId) { // 1.1 获取指定的用户待办任务 - Task todoTask = getMyTodoTask(userId, taskId); + Task todoTask = getMyTodoTask(userId, taskId, processInstanceId); // 1.2 获取不到,则获取该流程实例下,第一个用户的待办任务 if (todoTask == null) { todoTask = getMyFirstTodoTask(userId, processInstanceId); @@ -181,11 +181,12 @@ public class BpmTaskServiceImpl implements BpmTaskService { /** * 获得用户指定 taskId 任务编号的“待办”(未审批、且可审核)的任务 * - * @param userId 用户编号 - * @param taskId 任务编号 + * @param userId 用户编号 + * @param taskId 任务编号 + * @param processInstanceId 流程编号 * @return 任务 */ - private Task getMyTodoTask(Long userId, String taskId) { + private Task getMyTodoTask(Long userId, String taskId, String processInstanceId) { if (StrUtil.isEmpty(taskId)) { return null; } @@ -193,6 +194,10 @@ public class BpmTaskServiceImpl implements BpmTaskService { if (task == null) { return null; } + // 校验任务属于该流程实例,避免使用其它流程实例的任务 + if (ObjectUtil.notEqual(task.getProcessInstanceId(), processInstanceId)) { + return null; + } if (!isAssignUserTask(userId, task) && !isAddSignUserTask(userId, task)) { return null; } diff --git a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/BpmProcessInstanceQueryFlowableTest.java b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/BpmProcessInstanceQueryFlowableTest.java index 3e628ea..b7a9b05 100644 --- a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/BpmProcessInstanceQueryFlowableTest.java +++ b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/BpmProcessInstanceQueryFlowableTest.java @@ -18,6 +18,7 @@ import jakarta.annotation.Resource; import org.flowable.engine.RepositoryService; import org.flowable.engine.RuntimeService; import org.flowable.engine.TaskService; +import org.flowable.engine.delegate.DelegateExecution; import org.flowable.engine.history.HistoricProcessInstance; import org.flowable.engine.repository.Deployment; import org.flowable.engine.runtime.ProcessInstance; @@ -36,6 +37,7 @@ import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.PROCESS_INSTA import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.START_USER_NODE_ID; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.when; /** @@ -186,6 +188,120 @@ public class BpmProcessInstanceQueryFlowableTest extends BaseFlowableUnitTest { new BpmApprovalDetailReqVO().setProcessDefinitionId("not-exists")), PROCESS_DEFINITION_NOT_EXISTS); } + // ========== getApprovalDetail 表单字段权限 ========== + + @Test // 发起人从“我的流程”进入,待办属于其他审批人:使用发起人节点的权限,可编辑降级为只读 + public void testGetApprovalDetail_formFieldsPermission_startUserWithoutTodo() { + // 准备参数:发起人 1,task1 待用户 2 审批 + String processInstanceId = startFormPermissionProcessInstance(); + + // 调用 + BpmApprovalDetailRespVO respVO = processInstanceService.getApprovalDetail(1L, + new BpmApprovalDetailReqVO().setProcessInstanceId(processInstanceId)); + // 断言 + assertNull(respVO.getTodoTask()); + assertEquals(Map.of("a", "1", "b", "3", "c", "1"), respVO.getFormFieldsPermission()); + } + + @Test // 审批人有本流程的待办:使用待办所在节点的权限,保留可编辑 + public void testGetApprovalDetail_formFieldsPermission_todoTask() { + // 准备参数:task1 待用户 2 审批 + String processInstanceId = startFormPermissionProcessInstance(); + Task task1 = getRunningTask(processInstanceId); + + // 调用 + BpmApprovalDetailRespVO respVO = processInstanceService.getApprovalDetail(2L, + new BpmApprovalDetailReqVO().setProcessInstanceId(processInstanceId)); + // 断言 + assertEquals(task1.getId(), respVO.getTodoTask().getId()); + assertEquals(Map.of("a", "1", "b", "2", "c", "3"), respVO.getFormFieldsPermission()); + } + + @Test // 传递其它流程实例的待办 taskId:忽略该任务,使用本流程实例的待办 + public void testGetApprovalDetail_formFieldsPermission_todoTaskOfOtherProcessInstance() { + // 准备参数:两个流程实例,task1 均待用户 2 审批 + String processInstanceId = startFormPermissionProcessInstance(); + Task task1 = getRunningTask(processInstanceId); + Task otherTask1 = getRunningTask(startFormPermissionProcessInstance()); + + // 调用 + BpmApprovalDetailRespVO respVO = processInstanceService.getApprovalDetail(2L, + new BpmApprovalDetailReqVO().setProcessInstanceId(processInstanceId).setTaskId(otherTask1.getId())); + // 断言 + assertEquals(task1.getId(), respVO.getTodoTask().getId()); + assertEquals(processInstanceId, respVO.getTodoTask().getProcessInstanceId()); + assertEquals(Map.of("a", "1", "b", "2", "c", "3"), respVO.getFormFieldsPermission()); + } + + @Test // 发起人传递其他审批人的 taskId:忽略该任务,回落发起人节点的权限 + public void testGetApprovalDetail_formFieldsPermission_taskIdOfOtherUser() { + // 准备参数:task1 待用户 2 审批 + String processInstanceId = startFormPermissionProcessInstance(); + Task task1 = getRunningTask(processInstanceId); + + // 调用 + BpmApprovalDetailRespVO respVO = processInstanceService.getApprovalDetail(1L, + new BpmApprovalDetailReqVO().setProcessInstanceId(processInstanceId).setTaskId(task1.getId())); + // 断言 + assertNull(respVO.getTodoTask()); + assertEquals(Map.of("a", "1", "b", "3", "c", "1"), respVO.getFormFieldsPermission()); + } + + @Test // 流程已结束:已办使用办理节点的权限,发起人使用发起人节点的权限,均降级为只读;其他人不返回权限 + public void testGetApprovalDetail_formFieldsPermission_processEnd() { + // 准备参数:用户 2 审批通过 task1,流程结束 + String processInstanceId = startFormPermissionProcessInstance(); + Task task1 = getRunningTask(processInstanceId); + taskService.approveTask(2L, new BpmTaskApproveReqVO().setId(task1.getId()).setReason("同意")); + + // 调用 + 断言:用户 2 从“已办”进入 + BpmApprovalDetailRespVO doneRespVO = processInstanceService.getApprovalDetail(2L, + new BpmApprovalDetailReqVO().setProcessInstanceId(processInstanceId).setTaskId(task1.getId())); + assertEquals(BpmProcessInstanceStatusEnum.APPROVE.getStatus(), doneRespVO.getStatus()); + assertNull(doneRespVO.getTodoTask()); + assertEquals(Map.of("a", "1", "b", "1", "c", "3"), doneRespVO.getFormFieldsPermission()); + // 调用 + 断言:发起人 1 从“我的流程”进入 + BpmApprovalDetailRespVO startUserRespVO = processInstanceService.getApprovalDetail(1L, + new BpmApprovalDetailReqVO().setProcessInstanceId(processInstanceId)); + assertEquals(Map.of("a", "1", "b", "3", "c", "1"), startUserRespVO.getFormFieldsPermission()); + // 调用 + 断言:用户 2 不传 taskId,既不是发起人也没有待办 + BpmApprovalDetailRespVO otherRespVO = processInstanceService.getApprovalDetail(2L, + new BpmApprovalDetailReqVO().setProcessInstanceId(processInstanceId)); + assertNull(otherRespVO.getFormFieldsPermission()); + } + + @Test // 传递其它流程实例的已办 taskId:忽略该任务 + public void testGetApprovalDetail_formFieldsPermission_doneTaskOfOtherProcessInstance() { + // 准备参数:两个流程实例,task1 均由用户 2 审批通过 + String processInstanceId = startFormPermissionProcessInstance(); + taskService.approveTask(2L, new BpmTaskApproveReqVO().setId(getRunningTask(processInstanceId).getId()) + .setReason("同意")); + String otherProcessInstanceId = startFormPermissionProcessInstance(); + Task otherTask1 = getRunningTask(otherProcessInstanceId); + taskService.approveTask(2L, new BpmTaskApproveReqVO().setId(otherTask1.getId()).setReason("同意")); + + // 调用 + BpmApprovalDetailRespVO respVO = processInstanceService.getApprovalDetail(2L, + new BpmApprovalDetailReqVO().setProcessInstanceId(processInstanceId).setTaskId(otherTask1.getId())); + // 断言 + assertNull(respVO.getFormFieldsPermission()); + } + + @Test // 流程未发起:使用传递的发起人节点的权限,保留可编辑 + public void testGetApprovalDetail_formFieldsPermission_notStart() { + // 准备参数 + deploy("bpmn/form-permission.bpmn20.xml"); + String processDefinitionId = repositoryService.createProcessDefinitionQuery() + .processDefinitionKey("formPermission").latestVersion().singleResult().getId(); + + // 调用 + BpmApprovalDetailRespVO respVO = processInstanceService.getApprovalDetail(1L, new BpmApprovalDetailReqVO() + .setProcessDefinitionId(processDefinitionId).setActivityId(START_USER_NODE_ID)); + // 断言 + assertNull(respVO.getTodoTask()); + assertEquals(Map.of("a", "2", "b", "3", "c", "1"), respVO.getFormFieldsPermission()); + } + // ========== getProcessInstanceBpmnModelView ========== @Test @@ -313,6 +429,21 @@ public class BpmProcessInstanceQueryFlowableTest extends BaseFlowableUnitTest { .setProcessDefinitionKey("twoStepApprove").setVariables(variables)); } + /** + * 发起表单字段权限流程:发起人 1 的发起人节点自动通过,task1 待用户 2 审批 + */ + private String startFormPermissionProcessInstance() { + if (repositoryService.createProcessDefinitionQuery().processDefinitionKey("formPermission").count() == 0) { + deploy("bpmn/form-permission.bpmn20.xml"); + } + doAnswer(invocation -> { + DelegateExecution execution = invocation.getArgument(0); + return START_USER_NODE_ID.equals(execution.getCurrentActivityId()) ? Set.of(1L) : Set.of(2L); + }).when(taskCandidateInvoker).calculateUsersByTask(any()); + return processInstanceService.createProcessInstance(1L, new BpmProcessInstanceCreateReqDTO() + .setProcessDefinitionKey("formPermission")); + } + private Task getRunningTask(String processInstanceId) { List tasks = flowableTaskService.createTaskQuery().processInstanceId(processInstanceId).list(); assertEquals(1, tasks.size()); diff --git a/yudao-module-bpm/src/test/resources/bpmn/form-permission.bpmn20.xml b/yudao-module-bpm/src/test/resources/bpmn/form-permission.bpmn20.xml new file mode 100644 index 0000000..3262057 --- /dev/null +++ b/yudao-module-bpm/src/test/resources/bpmn/form-permission.bpmn20.xml @@ -0,0 +1,29 @@ + + + + + + + + + 36 + + + + + + + + + 30 + + + + + + + + + -- libgit2 0.21.4