Commit 44083c96c390e6d627a9bd507ebe8b9662c06c7f
Committed by
Gitee
!1276 fix: 发起流程报错,返回的VO缺少字段,icon为null时,copyTo转换异常问题
Merge pull request !1276 from SamllNorth_Lee/feature/bpm
Showing
7 changed files
with
72 additions
and
63 deletions
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelMetaInfoVO.java
| ... | ... | @@ -44,6 +44,11 @@ public class BpmModelMetaInfoVO { |
| 44 | 44 | private Integer formType; |
| 45 | 45 | @Schema(description = "表单编号", example = "1024") |
| 46 | 46 | private Long formId; // formType 为 NORMAL 使用,必须非空 |
| 47 | + | |
| 48 | + @Schema(description = "表单的配置-JSON 字符串。在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空", requiredMode = Schema.RequiredMode.REQUIRED) | |
| 49 | + private String formConf; | |
| 50 | + @Schema(description = "表单项的数组-JSON 字符串的数组。在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空", requiredMode = Schema.RequiredMode.REQUIRED) | |
| 51 | + private List<String> formFields; | |
| 47 | 52 | @Schema(description = "自定义表单的提交路径,使用 Vue 的路由地址", example = "/bpm/oa/leave/create") |
| 48 | 53 | private String formCustomCreatePath; // 表单类型为 CUSTOM 时,必须非空 |
| 49 | 54 | @Schema(description = "自定义表单的查看路径,使用 Vue 的路由地址", example = "/bpm/oa/leave/view") | ... | ... |
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelSaveReqVO.java
| ... | ... | @@ -21,6 +21,9 @@ public class BpmModelSaveReqVO extends BpmModelMetaInfoVO { |
| 21 | 21 | @NotEmpty(message = "流程名称不能为空") |
| 22 | 22 | private String name; |
| 23 | 23 | |
| 24 | + @Schema(description = "表单名字", example = "请假表单") | |
| 25 | + private String formName; | |
| 26 | + | |
| 24 | 27 | @Schema(description = "流程分类", example = "1") |
| 25 | 28 | private String category; |
| 26 | 29 | ... | ... |
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/process/BpmProcessDefinitionRespVO.java
| ... | ... | @@ -5,6 +5,7 @@ import io.swagger.v3.oas.annotations.media.Schema; |
| 5 | 5 | import lombok.Data; |
| 6 | 6 | |
| 7 | 7 | import java.time.LocalDateTime; |
| 8 | +import java.util.List; | |
| 8 | 9 | |
| 9 | 10 | @Schema(description = "管理后台 - 流程定义 Response VO") |
| 10 | 11 | @Data |
| ... | ... | @@ -19,7 +20,10 @@ public class BpmProcessDefinitionRespVO extends BpmModelMetaInfoVO { |
| 19 | 20 | @Schema(description = "流程名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") |
| 20 | 21 | private String name; |
| 21 | 22 | |
| 22 | - @Schema(description = "流程标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "yudao") | |
| 23 | + @Schema(description = "表单名字", example = "请假表单") | |
| 24 | + private String formName; | |
| 25 | + | |
| 26 | + @Schema(description = "流程标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "youdao") | |
| 23 | 27 | private String key; |
| 24 | 28 | |
| 25 | 29 | @Schema(description = "流程分类", example = "1") |
| ... | ... | @@ -27,14 +31,11 @@ public class BpmProcessDefinitionRespVO extends BpmModelMetaInfoVO { |
| 27 | 31 | @Schema(description = "流程分类名字", example = "请假") |
| 28 | 32 | private String categoryName; |
| 29 | 33 | |
| 30 | - @Schema(description = "流程模型的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "ABC") | |
| 31 | - private String modelId; | |
| 32 | - | |
| 33 | 34 | @Schema(description = "流程模型的类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") |
| 34 | 35 | private Integer modelType; // 参见 BpmModelTypeEnum 枚举类 |
| 35 | 36 | |
| 36 | - @Schema(description = "表单名字", example = "请假表单") | |
| 37 | - private String formName; | |
| 37 | + @Schema(description = "流程模型的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "ABC") | |
| 38 | + private String modelId; | |
| 38 | 39 | |
| 39 | 40 | @Schema(description = "中断状态-参见 SuspensionState 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") |
| 40 | 41 | private Integer suspensionState; // 参见 SuspensionState 枚举 | ... | ... |
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java
| ... | ... | @@ -907,9 +907,16 @@ public class BpmnModelUtils { |
| 907 | 907 | * @return 符合条件的路径 |
| 908 | 908 | */ |
| 909 | 909 | private static SequenceFlow findMatchSequenceFlowByExclusiveGateway(Gateway gateway, Map<String, Object> variables) { |
| 910 | - SequenceFlow matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(), | |
| 911 | - flow -> ObjUtil.notEqual(gateway.getDefaultFlow(), flow.getId()) | |
| 912 | - && (evalConditionExpress(variables, flow.getConditionExpression()))); | |
| 910 | + // TODO 表单无可编辑字段时variables为空,流程走向会出现问题,比如流程审批过程中无需要修改的字段值, | |
| 911 | + SequenceFlow matchSequenceFlow; | |
| 912 | + if (CollUtil.isNotEmpty(variables)){ | |
| 913 | + matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(), | |
| 914 | + flow -> ObjUtil.notEqual(gateway.getDefaultFlow(), flow.getId()) | |
| 915 | + && (evalConditionExpress(variables, flow.getConditionExpression()))); | |
| 916 | + }else { | |
| 917 | + matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(), | |
| 918 | + flow -> ObjUtil.notEqual(gateway.getDefaultFlow(), flow.getId())); | |
| 919 | + } | |
| 913 | 920 | if (matchSequenceFlow == null) { |
| 914 | 921 | matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(), |
| 915 | 922 | flow -> ObjUtil.equal(gateway.getDefaultFlow(), flow.getId())); | ... | ... |
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java
| ... | ... | @@ -40,9 +40,7 @@ import org.springframework.stereotype.Service; |
| 40 | 40 | import org.springframework.transaction.annotation.Transactional; |
| 41 | 41 | import org.springframework.validation.annotation.Validated; |
| 42 | 42 | |
| 43 | -import java.util.List; | |
| 44 | -import java.util.Map; | |
| 45 | -import java.util.Objects; | |
| 43 | +import java.util.*; | |
| 46 | 44 | |
| 47 | 45 | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| 48 | 46 | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; |
| ... | ... | @@ -209,11 +207,11 @@ public class BpmModelServiceImpl implements BpmModelService { |
| 209 | 207 | public void deployModel(Long userId, String id) { |
| 210 | 208 | // 1.1 校验流程模型存在 |
| 211 | 209 | Model model = validateModelManager(id, userId); |
| 210 | + BpmModelMetaInfoVO metaInfo = BpmModelConvert.INSTANCE.parseMetaInfo(model); | |
| 212 | 211 | // 1.2 校验流程图 |
| 213 | 212 | byte[] bpmnBytes = getModelBpmnXML(model.getId()); |
| 214 | - validateBpmnXml(bpmnBytes); | |
| 213 | + validateBpmnXml(bpmnBytes, metaInfo.getType()); | |
| 215 | 214 | // 1.3 校验表单已配 |
| 216 | - BpmModelMetaInfoVO metaInfo = BpmModelConvert.INSTANCE.parseMetaInfo(model); | |
| 217 | 215 | BpmFormDO form = validateFormConfig(metaInfo); |
| 218 | 216 | // 1.4 校验任务分配规则已配置 |
| 219 | 217 | taskCandidateInvoker.validateBpmnConfig(bpmnBytes); |
| ... | ... | @@ -233,7 +231,7 @@ public class BpmModelServiceImpl implements BpmModelService { |
| 233 | 231 | repositoryService.saveModel(model); |
| 234 | 232 | } |
| 235 | 233 | |
| 236 | - private void validateBpmnXml(byte[] bpmnBytes) { | |
| 234 | + private void validateBpmnXml(byte[] bpmnBytes, int type) { | |
| 237 | 235 | BpmnModel bpmnModel = BpmnModelUtils.getBpmnModel(bpmnBytes); |
| 238 | 236 | if (bpmnModel == null) { |
| 239 | 237 | throw exception(MODEL_NOT_EXISTS); |
| ... | ... | @@ -243,24 +241,23 @@ public class BpmModelServiceImpl implements BpmModelService { |
| 243 | 241 | if (startEvent == null) { |
| 244 | 242 | throw exception(MODEL_DEPLOY_FAIL_BPMN_START_EVENT_NOT_EXISTS); |
| 245 | 243 | } |
| 246 | - // 2. 校验第一个用户任务的规则类型是否为“审批人自选”,如果是则抛出异常。原因是,流程发起后,直接进入第一个用户任务,会出现无审批人的情况 | |
| 247 | - List<SequenceFlow> outgoingFlows = startEvent.getOutgoingFlows(); | |
| 248 | - // TODO @小北:可能极端情况下,startEvent 后面接了个 serviceTask,接着才是 userTask。。。 | |
| 249 | - // TODO @小北:simple 因为第一个任务是发起人,可能要找第二个任务??? | |
| 250 | - if (CollUtil.isNotEmpty(outgoingFlows)) { | |
| 251 | - FlowElement targetFlowElement = outgoingFlows.get(0).getTargetFlowElement(); | |
| 252 | - Integer candidateStrategy = parseCandidateStrategy(targetFlowElement); | |
| 253 | - if (Objects.equals(candidateStrategy, BpmTaskCandidateStrategyEnum.APPROVE_USER_SELECT.getStrategy())) { | |
| 254 | - throw exception(MODEL_DEPLOY_FAIL_FIRST_USER_TASK_CANDIDATE_STRATEGY_ERROR, targetFlowElement.getName()); | |
| 255 | - } | |
| 256 | - } | |
| 257 | - // 3. 校验 UserTask 的 name 都配置了 | |
| 244 | + // 2. 校验 UserTask 的 name 都配置了 | |
| 258 | 245 | List<UserTask> userTasks = BpmnModelUtils.getBpmnModelElements(bpmnModel, UserTask.class); |
| 259 | 246 | userTasks.forEach(userTask -> { |
| 260 | 247 | if (StrUtil.isEmpty(userTask.getName())) { |
| 261 | 248 | throw exception(MODEL_DEPLOY_FAIL_BPMN_USER_TASK_NAME_NOT_EXISTS, userTask.getId()); |
| 262 | 249 | } |
| 263 | 250 | }); |
| 251 | + // 3. 校验第一个用户任务节点的规则类型是否为“审批人自选” | |
| 252 | + Map<Integer, UserTask> userTaskMap = new HashMap<>(); | |
| 253 | + // BPMN 设计器,校验第一个用户任务节点 | |
| 254 | + userTaskMap.put(BpmModelTypeEnum.BPMN.getType(), userTasks.get(0)); | |
| 255 | + // SIMPLE 设计器,第一个节点固定为发起人所以校验第二个用户任务节点 | |
| 256 | + userTaskMap.put(BpmModelTypeEnum.SIMPLE.getType(), userTasks.get(1)); | |
| 257 | + Integer candidateStrategy = parseCandidateStrategy(userTaskMap.get(type)); | |
| 258 | + if (Objects.equals(candidateStrategy, BpmTaskCandidateStrategyEnum.APPROVE_USER_SELECT.getStrategy())) { | |
| 259 | + throw exception(MODEL_DEPLOY_FAIL_FIRST_USER_TASK_CANDIDATE_STRATEGY_ERROR, userTaskMap.get(type).getName()); | |
| 260 | + } | |
| 264 | 261 | } |
| 265 | 262 | |
| 266 | 263 | @Override | ... | ... |
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java
| ... | ... | @@ -221,8 +221,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService |
| 221 | 221 | processDefinitionInfo, |
| 222 | 222 | processVariables, activities); |
| 223 | 223 | // 3.3 如果是发起动作,activityId 为开始节点,不校验审批人自选节点 |
| 224 | - // TODO @小北:ObjUtil.equals(reqVO.getActivityId(), BpmnModelConstants.START_USER_NODE_ID) 够啦,不用判空 | |
| 225 | - if (ObjUtil.isNotNull(reqVO.getActivityId()) && ObjUtil.equals(reqVO.getActivityId(), BpmnModelConstants.START_USER_NODE_ID)) { | |
| 224 | + if (ObjUtil.equals(reqVO.getActivityId(), BpmnModelConstants.START_USER_NODE_ID)) { | |
| 226 | 225 | simulateActivityNodes.removeIf(node -> |
| 227 | 226 | BpmTaskCandidateStrategyEnum.APPROVE_USER_SELECT.getStrategy().equals(node.getCandidateStrategy())); |
| 228 | 227 | } | ... | ... |
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java
| ... | ... | @@ -558,18 +558,12 @@ public class BpmTaskServiceImpl implements BpmTaskService { |
| 558 | 558 | taskService.addComment(task.getId(), task.getProcessInstanceId(), BpmCommentTypeEnum.APPROVE.getType(), |
| 559 | 559 | BpmCommentTypeEnum.APPROVE.formatComment(reqVO.getReason())); |
| 560 | 560 | // 2.3 调用 BPM complete 去完成任务 |
| 561 | - // 其中,variables 是存储动态表单到 local 任务级别。过滤一下,避免 ProcessInstance 系统级的变量被占用 | |
| 562 | - if (CollUtil.isNotEmpty(reqVO.getVariables())) { | |
| 563 | - // 校验并处理 APPROVE_USER_SELECT 当前审批人,选择下一节点审批人的逻辑 | |
| 564 | - Map<String, Object> variables = validateAndSetNextAssignees(task.getTaskDefinitionKey(), reqVO.getVariables(), | |
| 565 | - bpmnModel, reqVO.getNextAssignees(), instance); | |
| 566 | - // 完成任务 | |
| 567 | - runtimeService.setVariables(task.getProcessInstanceId(), variables); | |
| 568 | - taskService.complete(task.getId(), variables, true); | |
| 569 | - } else { | |
| 570 | - // 完成任务 | |
| 571 | - taskService.complete(task.getId()); | |
| 572 | - } | |
| 561 | + // 校验并处理 APPROVE_USER_SELECT 当前审批人,选择下一节点审批人的逻辑 | |
| 562 | + Map<String, Object> variables = validateAndSetNextAssignees(task.getTaskDefinitionKey(), reqVO.getVariables(), | |
| 563 | + bpmnModel, reqVO.getNextAssignees(), instance); | |
| 564 | + // 完成任务 | |
| 565 | + runtimeService.setVariables(task.getProcessInstanceId(), variables); | |
| 566 | + taskService.complete(task.getId(), variables, true); | |
| 573 | 567 | |
| 574 | 568 | // 【加签专属】处理加签任务 |
| 575 | 569 | handleParentTaskIfSign(task.getParentTaskId()); |
| ... | ... | @@ -590,38 +584,31 @@ public class BpmTaskServiceImpl implements BpmTaskService { |
| 590 | 584 | */ |
| 591 | 585 | private Map<String, Object> validateAndSetNextAssignees(String taskDefinitionKey, Map<String, Object> variables, BpmnModel bpmnModel, |
| 592 | 586 | Map<String, List<Long>> nextAssignees, ProcessInstance processInstance) { |
| 593 | - // 下一个节点参数为空,不做处理,表示流程正常流转,无需选择下一个节点的审判人 | |
| 594 | - // TODO @小北:会出现漏选,其实需要的情况哇? | |
| 595 | - if (CollUtil.isEmpty(nextAssignees)) { | |
| 596 | - return variables; | |
| 597 | - } | |
| 598 | 587 | // 1. 获取下一个将要执行的节点集合 |
| 599 | 588 | FlowElement flowElement = bpmnModel.getFlowElement(taskDefinitionKey); |
| 600 | 589 | List<FlowNode> nextFlowNodes = getNextFlowNodes(flowElement, bpmnModel, variables); |
| 601 | - | |
| 602 | 590 | // 2. 循环下一个将要执行的节点集合 |
| 603 | - Map<String, List<Long>> processVariables = new HashMap<>(); | |
| 591 | + Map<String, List<Long>> processVariables; | |
| 604 | 592 | for (FlowNode nextFlowNode : nextFlowNodes) { |
| 605 | - if (!nextAssignees.containsKey(nextFlowNode.getId())) { | |
| 606 | - throw exception(TASK_TARGET_NODE_NOT_EXISTS, nextFlowNode.getName()); | |
| 607 | - } | |
| 608 | - List<Long> assignees = nextAssignees.get(nextFlowNode.getId()); | |
| 609 | - // 2.1 情况一:如果节点中的审批人策略为 发起人自选 | |
| 593 | + // 获取任务节点中的审批人策略 | |
| 610 | 594 | Integer candidateStrategy = parseCandidateStrategy(nextFlowNode); |
| 595 | + // 2.1 情况一:如果节点中的审批人策略为 发起人自选 | |
| 611 | 596 | if (ObjUtil.equals(candidateStrategy, BpmTaskCandidateStrategyEnum.START_USER_SELECT.getStrategy())) { |
| 612 | - processVariables = FlowableUtils.getStartUserSelectAssignees(processInstance.getProcessVariables()); | |
| 613 | - if (processVariables == null) { | |
| 614 | - processVariables = new HashMap<>(); | |
| 615 | - } | |
| 616 | - List<Long> startUserSelectAssignee = processVariables.get(nextFlowNode.getId()); | |
| 617 | - // 特殊:如果当前节点已经存在审批人,则不允许覆盖 | |
| 618 | - if (CollUtil.isNotEmpty(startUserSelectAssignee)) { | |
| 619 | - continue; | |
| 620 | - } | |
| 621 | 597 | // 如果节点存在,但未配置审批人 |
| 598 | + List<Long> assignees = nextAssignees != null ? nextAssignees.get(nextFlowNode.getId()) : null; | |
| 622 | 599 | if (CollUtil.isEmpty(assignees)) { |
| 623 | 600 | throw exception(PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_CONFIG, nextFlowNode.getName()); |
| 624 | 601 | } |
| 602 | + processVariables = FlowableUtils.getStartUserSelectAssignees(processInstance.getProcessVariables()); | |
| 603 | + if (processVariables == null){ | |
| 604 | + processVariables = new HashMap<>(); | |
| 605 | + }else { | |
| 606 | + List<Long> startUserSelectAssignee = processVariables.get(nextFlowNode.getId()); | |
| 607 | + // 特殊:如果当前节点已经存在审批人,则不允许覆盖 | |
| 608 | + if (CollUtil.isNotEmpty(startUserSelectAssignee)) { | |
| 609 | + continue; | |
| 610 | + } | |
| 611 | + } | |
| 625 | 612 | // 校验通过的全部节点和审批人 |
| 626 | 613 | processVariables.put(nextFlowNode.getId(), assignees); |
| 627 | 614 | variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES, processVariables); |
| ... | ... | @@ -629,12 +616,22 @@ public class BpmTaskServiceImpl implements BpmTaskService { |
| 629 | 616 | // 2.2 情况二:如果节点中的审批人策略为 审批人,在审批时选择下一个节点的审批人,并且该节点的审批人为空 |
| 630 | 617 | if (ObjUtil.equals(candidateStrategy, BpmTaskCandidateStrategyEnum.APPROVE_USER_SELECT.getStrategy())) { |
| 631 | 618 | // 如果节点存在,但未配置审批人 |
| 619 | + List<Long> assignees = nextAssignees != null ? nextAssignees.get(nextFlowNode.getId()) : null; | |
| 632 | 620 | if (CollUtil.isEmpty(assignees)) { |
| 633 | 621 | throw exception(PROCESS_INSTANCE_APPROVE_USER_SELECT_ASSIGNEES_NOT_CONFIG, nextFlowNode.getName()); |
| 634 | 622 | } |
| 623 | + processVariables = FlowableUtils.getApproveUserSelectAssignees(processInstance.getProcessVariables()); | |
| 624 | + if (processVariables == null){ | |
| 625 | + processVariables = new HashMap<>(); | |
| 626 | + }else { | |
| 627 | + List<Long> approveUserSelectAssignee = processVariables.get(nextFlowNode.getId()); | |
| 628 | + // 特殊:如果当前节点已经存在审批人,则不允许覆盖 | |
| 629 | + if (CollUtil.isNotEmpty(approveUserSelectAssignee)) { | |
| 630 | + continue; | |
| 631 | + } | |
| 632 | + } | |
| 635 | 633 | // 校验通过的全部节点和审批人 |
| 636 | 634 | processVariables.put(nextFlowNode.getId(), assignees); |
| 637 | - // TODO @小北:是不是要类似情况一的做法,通过 PROCESS_INSTANCE_VARIABLE_APPROVE_USER_SELECT_ASSIGNEES 拿一下?因为如果 task1 是审批人自选,task2 是审批人自选,看着会覆盖? | |
| 638 | 635 | variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_APPROVE_USER_SELECT_ASSIGNEES, processVariables); |
| 639 | 636 | } |
| 640 | 637 | } | ... | ... |