Commit 61ca559f97b82d04e181c403dccbb8c4eb105822

Authored by YunaiV
1 parent ed24181e

fix(bpm): 修复流程详情表单权限错配

校验任务归属流程与当前用户,并在发起人无待办时回落为只读权限。补充跨流程任务、他人待办及流程结束场景的 Flowable 集成测试。
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;
22 22 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
23 23 import cn.iocoder.yudao.module.bpm.dal.redis.BpmProcessIdRedisDAO;
24 24 import cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants;
  25 +import cn.iocoder.yudao.module.bpm.enums.definition.BpmFieldPermissionEnum;
25 26 import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum;
26 27 import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum;
27 28 import cn.iocoder.yudao.module.bpm.enums.definition.BpmSimpleModelNodeTypeEnum;
... ... @@ -152,19 +153,82 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
152 153 .list();
153 154 }
154 155  
155   - private Map<String, String> getFormFieldsPermission(BpmnModel bpmnModel,
156   - String activityId, String taskId) {
157   - // 1. 获取流程活动编号。流程活动 Id 为空事,从流程任务中获取流程活动 Id
158   - if (StrUtil.isEmpty(activityId) && StrUtil.isNotEmpty(taskId)) {
159   - activityId = Optional.ofNullable(taskService.getHistoricTask(taskId))
160   - .map(HistoricTaskInstance::getTaskDefinitionKey).orElse(null);
  156 + /**
  157 + * 获得当前登录用户的表单字段权限
  158 + *
  159 + * @param loginUserId 当前登录用户编号
  160 + * @param bpmnModel 流程模型
  161 + * @param reqVO 审批详情请求
  162 + * @param processInstance 流程实例;为 null 时,表示流程未发起
  163 + * @param todoTask 当前登录用户的待办任务
  164 + * @return 表单字段权限
  165 + */
  166 + private Map<String, String> getFormFieldsPermission(Long loginUserId, BpmnModel bpmnModel,
  167 + BpmApprovalDetailReqVO reqVO,
  168 + HistoricProcessInstance processInstance,
  169 + BpmTaskRespVO todoTask) {
  170 + // 情况一:有待办任务,以待办任务所在节点的表单权限为准,可编辑
  171 + if (todoTask != null) {
  172 + return BpmnModelUtils.parseFormFieldsPermission(bpmnModel, todoTask.getTaskDefinitionKey());
  173 + }
  174 + // 情况二:流程未发起,使用传递的流程活动(例如说,发起人节点)的表单权限,可编辑
  175 + if (processInstance == null) {
  176 + return BpmnModelUtils.parseFormFieldsPermission(bpmnModel, reqVO.getActivityId());
161 177 }
  178 +
  179 + // 情况三:流程已发起,但没有待办任务,只能查看
  180 + // 3.1 优先,使用自己办理过的任务(已办)所在节点
  181 + String activityId = getMyHistoricTaskDefinitionKey(loginUserId, reqVO.getTaskId(), processInstance.getId());
  182 + // 3.2 其次,使用传递的流程活动(例如说,抄送节点)
162 183 if (StrUtil.isEmpty(activityId)) {
  184 + activityId = reqVO.getActivityId();
  185 + }
  186 + // 3.3 最后,如果是流程发起人(例如说,从“我的流程”进入),使用发起人节点
  187 + if (StrUtil.isEmpty(activityId) && ObjUtil.equal(String.valueOf(loginUserId), processInstance.getStartUserId())) {
  188 + activityId = START_USER_NODE_ID;
  189 + }
  190 + // 没有待办任务,无法提交表单,所以将“可编辑”降级为“只读”,避免误导
  191 + return downgradeWritePermission(BpmnModelUtils.parseFormFieldsPermission(bpmnModel, activityId));
  192 + }
  193 +
  194 + /**
  195 + * 获得用户在指定流程实例中,办理过的 taskId 任务所在的节点编号
  196 + *
  197 + * @param userId 用户编号
  198 + * @param taskId 任务编号
  199 + * @param processInstanceId 流程实例编号
  200 + * @return 节点编号;任务不存在、不属于该流程实例、或者不是该用户办理的,返回 null
  201 + */
  202 + private String getMyHistoricTaskDefinitionKey(Long userId, String taskId, String processInstanceId) {
  203 + if (StrUtil.isEmpty(taskId)) {
163 204 return null;
164 205 }
  206 + HistoricTaskInstance task = taskService.getHistoricTask(taskId);
  207 + if (task == null || ObjUtil.notEqual(task.getProcessInstanceId(), processInstanceId)) {
  208 + return null;
  209 + }
  210 + // 校验是该用户办理(审批人或拥有人)的任务
  211 + String userIdStr = String.valueOf(userId);
  212 + if (ObjUtil.notEqual(userIdStr, task.getAssignee()) && ObjUtil.notEqual(userIdStr, task.getOwner())) {
  213 + return null;
  214 + }
  215 + return task.getTaskDefinitionKey();
  216 + }
165 217  
166   - // 2. 从 BpmnModel 中解析表单字段权限
167   - return BpmnModelUtils.parseFormFieldsPermission(bpmnModel, activityId);
  218 + /**
  219 + * 将表单字段权限中的“可编辑”降级为“只读”
  220 + *
  221 + * @param fieldsPermission 表单字段权限
  222 + * @return 表单字段权限
  223 + */
  224 + private static Map<String, String> downgradeWritePermission(Map<String, String> fieldsPermission) {
  225 + if (CollUtil.isEmpty(fieldsPermission)) {
  226 + return fieldsPermission;
  227 + }
  228 + String write = String.valueOf(BpmFieldPermissionEnum.WRITE.getPermission());
  229 + String read = String.valueOf(BpmFieldPermissionEnum.READ.getPermission());
  230 + fieldsPermission.replaceAll((field, permission) -> write.equals(permission) ? read : permission);
  231 + return fieldsPermission;
168 232 }
169 233  
170 234 @Override
... ... @@ -221,7 +285,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
221 285  
222 286 // 2.2 流程已经结束,直接 return,无需预测
223 287 if (BpmProcessInstanceStatusEnum.isProcessEndStatus(processInstanceStatus)) {
224   - return buildApprovalDetail(reqVO, bpmnModel, processDefinition, processDefinitionInfo,
  288 + return buildApprovalDetail(loginUserId, reqVO, bpmnModel, processDefinition, processDefinitionInfo,
225 289 historicProcessInstance,
226 290 processInstanceStatus, endActivityNodes, runActivityNodes, null, null);
227 291 }
... ... @@ -246,7 +310,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
246 310 processVariables, activities, needSimulateTaskDefKeysByReturn);
247 311  
248 312 // 4. 拼接最终数据
249   - return buildApprovalDetail(reqVO, bpmnModel, processDefinition, processDefinitionInfo, historicProcessInstance,
  313 + return buildApprovalDetail(loginUserId, reqVO, bpmnModel, processDefinition, processDefinitionInfo, historicProcessInstance,
250 314 processInstanceStatus, endActivityNodes, runActivityNodes, simulateActivityNodes, todoTask);
251 315 }
252 316  
... ... @@ -371,7 +435,8 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
371 435 * <p>
372 436 * 主要是,拼接审批人的用户信息、部门信息
373 437 */
374   - private BpmApprovalDetailRespVO buildApprovalDetail(BpmApprovalDetailReqVO reqVO,
  438 + private BpmApprovalDetailRespVO buildApprovalDetail(Long loginUserId,
  439 + BpmApprovalDetailReqVO reqVO,
375 440 BpmnModel bpmnModel,
376 441 ProcessDefinition processDefinition,
377 442 BpmProcessDefinitionInfoDO processDefinitionInfo,
... ... @@ -389,8 +454,8 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
389 454 Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(convertSet(userMap.values(), AdminUserRespDTO::getDeptId));
390 455  
391 456 // 2. 表单权限
392   - String taskId = reqVO.getTaskId() == null && todoTask != null ? todoTask.getId() : reqVO.getTaskId();
393   - Map<String, String> formFieldsPermission = getFormFieldsPermission(bpmnModel, reqVO.getActivityId(), taskId);
  457 + Map<String, String> formFieldsPermission = getFormFieldsPermission(loginUserId, bpmnModel, reqVO,
  458 + processInstance, todoTask);
394 459  
395 460 // 3. 拼接数据
396 461 return BpmProcessInstanceConvert.INSTANCE.buildApprovalDetail(bpmnModel, processDefinition,
... ...
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java
... ... @@ -148,7 +148,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
148 148 @Override
149 149 public BpmTaskRespVO getTodoTask(Long userId, String taskId, String processInstanceId) {
150 150 // 1.1 获取指定的用户待办任务
151   - Task todoTask = getMyTodoTask(userId, taskId);
  151 + Task todoTask = getMyTodoTask(userId, taskId, processInstanceId);
152 152 // 1.2 获取不到,则获取该流程实例下,第一个用户的待办任务
153 153 if (todoTask == null) {
154 154 todoTask = getMyFirstTodoTask(userId, processInstanceId);
... ... @@ -181,11 +181,12 @@ public class BpmTaskServiceImpl implements BpmTaskService {
181 181 /**
182 182 * 获得用户指定 taskId 任务编号的“待办”(未审批、且可审核)的任务
183 183 *
184   - * @param userId 用户编号
185   - * @param taskId 任务编号
  184 + * @param userId 用户编号
  185 + * @param taskId 任务编号
  186 + * @param processInstanceId 流程编号
186 187 * @return 任务
187 188 */
188   - private Task getMyTodoTask(Long userId, String taskId) {
  189 + private Task getMyTodoTask(Long userId, String taskId, String processInstanceId) {
189 190 if (StrUtil.isEmpty(taskId)) {
190 191 return null;
191 192 }
... ... @@ -193,6 +194,10 @@ public class BpmTaskServiceImpl implements BpmTaskService {
193 194 if (task == null) {
194 195 return null;
195 196 }
  197 + // 校验任务属于该流程实例,避免使用其它流程实例的任务
  198 + if (ObjectUtil.notEqual(task.getProcessInstanceId(), processInstanceId)) {
  199 + return null;
  200 + }
196 201 if (!isAssignUserTask(userId, task) && !isAddSignUserTask(userId, task)) {
197 202 return null;
198 203 }
... ...
yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/BpmProcessInstanceQueryFlowableTest.java
... ... @@ -18,6 +18,7 @@ import jakarta.annotation.Resource;
18 18 import org.flowable.engine.RepositoryService;
19 19 import org.flowable.engine.RuntimeService;
20 20 import org.flowable.engine.TaskService;
  21 +import org.flowable.engine.delegate.DelegateExecution;
21 22 import org.flowable.engine.history.HistoricProcessInstance;
22 23 import org.flowable.engine.repository.Deployment;
23 24 import org.flowable.engine.runtime.ProcessInstance;
... ... @@ -36,6 +37,7 @@ import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.PROCESS_INSTA
36 37 import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.START_USER_NODE_ID;
37 38 import static org.junit.jupiter.api.Assertions.*;
38 39 import static org.mockito.ArgumentMatchers.any;
  40 +import static org.mockito.Mockito.doAnswer;
39 41 import static org.mockito.Mockito.when;
40 42  
41 43 /**
... ... @@ -186,6 +188,120 @@ public class BpmProcessInstanceQueryFlowableTest extends BaseFlowableUnitTest {
186 188 new BpmApprovalDetailReqVO().setProcessDefinitionId("not-exists")), PROCESS_DEFINITION_NOT_EXISTS);
187 189 }
188 190  
  191 + // ========== getApprovalDetail 表单字段权限 ==========
  192 +
  193 + @Test // 发起人从“我的流程”进入,待办属于其他审批人:使用发起人节点的权限,可编辑降级为只读
  194 + public void testGetApprovalDetail_formFieldsPermission_startUserWithoutTodo() {
  195 + // 准备参数:发起人 1,task1 待用户 2 审批
  196 + String processInstanceId = startFormPermissionProcessInstance();
  197 +
  198 + // 调用
  199 + BpmApprovalDetailRespVO respVO = processInstanceService.getApprovalDetail(1L,
  200 + new BpmApprovalDetailReqVO().setProcessInstanceId(processInstanceId));
  201 + // 断言
  202 + assertNull(respVO.getTodoTask());
  203 + assertEquals(Map.of("a", "1", "b", "3", "c", "1"), respVO.getFormFieldsPermission());
  204 + }
  205 +
  206 + @Test // 审批人有本流程的待办:使用待办所在节点的权限,保留可编辑
  207 + public void testGetApprovalDetail_formFieldsPermission_todoTask() {
  208 + // 准备参数:task1 待用户 2 审批
  209 + String processInstanceId = startFormPermissionProcessInstance();
  210 + Task task1 = getRunningTask(processInstanceId);
  211 +
  212 + // 调用
  213 + BpmApprovalDetailRespVO respVO = processInstanceService.getApprovalDetail(2L,
  214 + new BpmApprovalDetailReqVO().setProcessInstanceId(processInstanceId));
  215 + // 断言
  216 + assertEquals(task1.getId(), respVO.getTodoTask().getId());
  217 + assertEquals(Map.of("a", "1", "b", "2", "c", "3"), respVO.getFormFieldsPermission());
  218 + }
  219 +
  220 + @Test // 传递其它流程实例的待办 taskId:忽略该任务,使用本流程实例的待办
  221 + public void testGetApprovalDetail_formFieldsPermission_todoTaskOfOtherProcessInstance() {
  222 + // 准备参数:两个流程实例,task1 均待用户 2 审批
  223 + String processInstanceId = startFormPermissionProcessInstance();
  224 + Task task1 = getRunningTask(processInstanceId);
  225 + Task otherTask1 = getRunningTask(startFormPermissionProcessInstance());
  226 +
  227 + // 调用
  228 + BpmApprovalDetailRespVO respVO = processInstanceService.getApprovalDetail(2L,
  229 + new BpmApprovalDetailReqVO().setProcessInstanceId(processInstanceId).setTaskId(otherTask1.getId()));
  230 + // 断言
  231 + assertEquals(task1.getId(), respVO.getTodoTask().getId());
  232 + assertEquals(processInstanceId, respVO.getTodoTask().getProcessInstanceId());
  233 + assertEquals(Map.of("a", "1", "b", "2", "c", "3"), respVO.getFormFieldsPermission());
  234 + }
  235 +
  236 + @Test // 发起人传递其他审批人的 taskId:忽略该任务,回落发起人节点的权限
  237 + public void testGetApprovalDetail_formFieldsPermission_taskIdOfOtherUser() {
  238 + // 准备参数:task1 待用户 2 审批
  239 + String processInstanceId = startFormPermissionProcessInstance();
  240 + Task task1 = getRunningTask(processInstanceId);
  241 +
  242 + // 调用
  243 + BpmApprovalDetailRespVO respVO = processInstanceService.getApprovalDetail(1L,
  244 + new BpmApprovalDetailReqVO().setProcessInstanceId(processInstanceId).setTaskId(task1.getId()));
  245 + // 断言
  246 + assertNull(respVO.getTodoTask());
  247 + assertEquals(Map.of("a", "1", "b", "3", "c", "1"), respVO.getFormFieldsPermission());
  248 + }
  249 +
  250 + @Test // 流程已结束:已办使用办理节点的权限,发起人使用发起人节点的权限,均降级为只读;其他人不返回权限
  251 + public void testGetApprovalDetail_formFieldsPermission_processEnd() {
  252 + // 准备参数:用户 2 审批通过 task1,流程结束
  253 + String processInstanceId = startFormPermissionProcessInstance();
  254 + Task task1 = getRunningTask(processInstanceId);
  255 + taskService.approveTask(2L, new BpmTaskApproveReqVO().setId(task1.getId()).setReason("同意"));
  256 +
  257 + // 调用 + 断言:用户 2 从“已办”进入
  258 + BpmApprovalDetailRespVO doneRespVO = processInstanceService.getApprovalDetail(2L,
  259 + new BpmApprovalDetailReqVO().setProcessInstanceId(processInstanceId).setTaskId(task1.getId()));
  260 + assertEquals(BpmProcessInstanceStatusEnum.APPROVE.getStatus(), doneRespVO.getStatus());
  261 + assertNull(doneRespVO.getTodoTask());
  262 + assertEquals(Map.of("a", "1", "b", "1", "c", "3"), doneRespVO.getFormFieldsPermission());
  263 + // 调用 + 断言:发起人 1 从“我的流程”进入
  264 + BpmApprovalDetailRespVO startUserRespVO = processInstanceService.getApprovalDetail(1L,
  265 + new BpmApprovalDetailReqVO().setProcessInstanceId(processInstanceId));
  266 + assertEquals(Map.of("a", "1", "b", "3", "c", "1"), startUserRespVO.getFormFieldsPermission());
  267 + // 调用 + 断言:用户 2 不传 taskId,既不是发起人也没有待办
  268 + BpmApprovalDetailRespVO otherRespVO = processInstanceService.getApprovalDetail(2L,
  269 + new BpmApprovalDetailReqVO().setProcessInstanceId(processInstanceId));
  270 + assertNull(otherRespVO.getFormFieldsPermission());
  271 + }
  272 +
  273 + @Test // 传递其它流程实例的已办 taskId:忽略该任务
  274 + public void testGetApprovalDetail_formFieldsPermission_doneTaskOfOtherProcessInstance() {
  275 + // 准备参数:两个流程实例,task1 均由用户 2 审批通过
  276 + String processInstanceId = startFormPermissionProcessInstance();
  277 + taskService.approveTask(2L, new BpmTaskApproveReqVO().setId(getRunningTask(processInstanceId).getId())
  278 + .setReason("同意"));
  279 + String otherProcessInstanceId = startFormPermissionProcessInstance();
  280 + Task otherTask1 = getRunningTask(otherProcessInstanceId);
  281 + taskService.approveTask(2L, new BpmTaskApproveReqVO().setId(otherTask1.getId()).setReason("同意"));
  282 +
  283 + // 调用
  284 + BpmApprovalDetailRespVO respVO = processInstanceService.getApprovalDetail(2L,
  285 + new BpmApprovalDetailReqVO().setProcessInstanceId(processInstanceId).setTaskId(otherTask1.getId()));
  286 + // 断言
  287 + assertNull(respVO.getFormFieldsPermission());
  288 + }
  289 +
  290 + @Test // 流程未发起:使用传递的发起人节点的权限,保留可编辑
  291 + public void testGetApprovalDetail_formFieldsPermission_notStart() {
  292 + // 准备参数
  293 + deploy("bpmn/form-permission.bpmn20.xml");
  294 + String processDefinitionId = repositoryService.createProcessDefinitionQuery()
  295 + .processDefinitionKey("formPermission").latestVersion().singleResult().getId();
  296 +
  297 + // 调用
  298 + BpmApprovalDetailRespVO respVO = processInstanceService.getApprovalDetail(1L, new BpmApprovalDetailReqVO()
  299 + .setProcessDefinitionId(processDefinitionId).setActivityId(START_USER_NODE_ID));
  300 + // 断言
  301 + assertNull(respVO.getTodoTask());
  302 + assertEquals(Map.of("a", "2", "b", "3", "c", "1"), respVO.getFormFieldsPermission());
  303 + }
  304 +
189 305 // ========== getProcessInstanceBpmnModelView ==========
190 306  
191 307 @Test
... ... @@ -313,6 +429,21 @@ public class BpmProcessInstanceQueryFlowableTest extends BaseFlowableUnitTest {
313 429 .setProcessDefinitionKey("twoStepApprove").setVariables(variables));
314 430 }
315 431  
  432 + /**
  433 + * 发起表单字段权限流程:发起人 1 的发起人节点自动通过,task1 待用户 2 审批
  434 + */
  435 + private String startFormPermissionProcessInstance() {
  436 + if (repositoryService.createProcessDefinitionQuery().processDefinitionKey("formPermission").count() == 0) {
  437 + deploy("bpmn/form-permission.bpmn20.xml");
  438 + }
  439 + doAnswer(invocation -> {
  440 + DelegateExecution execution = invocation.getArgument(0);
  441 + return START_USER_NODE_ID.equals(execution.getCurrentActivityId()) ? Set.of(1L) : Set.of(2L);
  442 + }).when(taskCandidateInvoker).calculateUsersByTask(any());
  443 + return processInstanceService.createProcessInstance(1L, new BpmProcessInstanceCreateReqDTO()
  444 + .setProcessDefinitionKey("formPermission"));
  445 + }
  446 +
316 447 private Task getRunningTask(String processInstanceId) {
317 448 List<Task> tasks = flowableTaskService.createTaskQuery().processInstanceId(processInstanceId).list();
318 449 assertEquals(1, tasks.size());
... ...
yudao-module-bpm/src/test/resources/bpmn/form-permission.bpmn20.xml 0 → 100644
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
  3 + xmlns:flowable="http://flowable.org/bpmn"
  4 + targetNamespace="http://www.yudao.iocoder.cn/bpm">
  5 + <!-- 表单字段权限:1 只读、2 可编辑、3 隐藏;发起人节点、审批节点配置不同的权限,用于区分权限来源 -->
  6 + <process id="formPermission" name="表单字段权限" isExecutable="true">
  7 + <startEvent id="start"/>
  8 + <sequenceFlow id="flow-start" sourceRef="start" targetRef="StartUserNode"/>
  9 + <userTask id="StartUserNode" name="发起人">
  10 + <extensionElements>
  11 + <flowable:candidateStrategy>36</flowable:candidateStrategy>
  12 + <flowable:fieldsPermission field="a" permission="2"/>
  13 + <flowable:fieldsPermission field="b" permission="3"/>
  14 + <flowable:fieldsPermission field="c" permission="1"/>
  15 + </extensionElements>
  16 + </userTask>
  17 + <sequenceFlow id="flow-start-user" sourceRef="StartUserNode" targetRef="task1"/>
  18 + <userTask id="task1" name="审批">
  19 + <extensionElements>
  20 + <flowable:candidateStrategy>30</flowable:candidateStrategy>
  21 + <flowable:fieldsPermission field="a" permission="1"/>
  22 + <flowable:fieldsPermission field="b" permission="2"/>
  23 + <flowable:fieldsPermission field="c" permission="3"/>
  24 + </extensionElements>
  25 + </userTask>
  26 + <sequenceFlow id="flow-task1" sourceRef="task1" targetRef="end"/>
  27 + <endEvent id="end"/>
  28 + </process>
  29 +</definitions>
... ...