!1276 fix: 发起流程报错,返回的VO缺少字段,icon为null时,copyTo转换异常问题

Merge pull request !1276 from SamllNorth_Lee/feature/bpm
This commit is contained in:
芋道源码 2025-03-12 01:56:06 +00:00 committed by Gitee
commit 44083c96c3
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
7 changed files with 72 additions and 63 deletions

View File

@ -44,6 +44,11 @@ public class BpmModelMetaInfoVO {
private Integer formType; private Integer formType;
@Schema(description = "表单编号", example = "1024") @Schema(description = "表单编号", example = "1024")
private Long formId; // formType NORMAL 使用必须非空 private Long formId; // formType NORMAL 使用必须非空
@Schema(description = "表单的配置-JSON 字符串。在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空", requiredMode = Schema.RequiredMode.REQUIRED)
private String formConf;
@Schema(description = "表单项的数组-JSON 字符串的数组。在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空", requiredMode = Schema.RequiredMode.REQUIRED)
private List<String> formFields;
@Schema(description = "自定义表单的提交路径,使用 Vue 的路由地址", example = "/bpm/oa/leave/create") @Schema(description = "自定义表单的提交路径,使用 Vue 的路由地址", example = "/bpm/oa/leave/create")
private String formCustomCreatePath; // 表单类型为 CUSTOM 必须非空 private String formCustomCreatePath; // 表单类型为 CUSTOM 必须非空
@Schema(description = "自定义表单的查看路径,使用 Vue 的路由地址", example = "/bpm/oa/leave/view") @Schema(description = "自定义表单的查看路径,使用 Vue 的路由地址", example = "/bpm/oa/leave/view")

View File

@ -21,6 +21,9 @@ public class BpmModelSaveReqVO extends BpmModelMetaInfoVO {
@NotEmpty(message = "流程名称不能为空") @NotEmpty(message = "流程名称不能为空")
private String name; private String name;
@Schema(description = "表单名字", example = "请假表单")
private String formName;
@Schema(description = "流程分类", example = "1") @Schema(description = "流程分类", example = "1")
private String category; private String category;

View File

@ -5,6 +5,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "管理后台 - 流程定义 Response VO") @Schema(description = "管理后台 - 流程定义 Response VO")
@Data @Data
@ -19,7 +20,10 @@ public class BpmProcessDefinitionRespVO extends BpmModelMetaInfoVO {
@Schema(description = "流程名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") @Schema(description = "流程名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道")
private String name; private String name;
@Schema(description = "流程标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "yudao") @Schema(description = "表单名字", example = "请假表单")
private String formName;
@Schema(description = "流程标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "youdao")
private String key; private String key;
@Schema(description = "流程分类", example = "1") @Schema(description = "流程分类", example = "1")
@ -27,14 +31,11 @@ public class BpmProcessDefinitionRespVO extends BpmModelMetaInfoVO {
@Schema(description = "流程分类名字", example = "请假") @Schema(description = "流程分类名字", example = "请假")
private String categoryName; private String categoryName;
@Schema(description = "流程模型的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "ABC")
private String modelId;
@Schema(description = "流程模型的类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") @Schema(description = "流程模型的类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
private Integer modelType; // 参见 BpmModelTypeEnum 枚举类 private Integer modelType; // 参见 BpmModelTypeEnum 枚举类
@Schema(description = "表单名字", example = "请假表单") @Schema(description = "流程模型的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "ABC")
private String formName; private String modelId;
@Schema(description = "中断状态-参见 SuspensionState 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @Schema(description = "中断状态-参见 SuspensionState 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer suspensionState; // 参见 SuspensionState 枚举 private Integer suspensionState; // 参见 SuspensionState 枚举

View File

@ -907,9 +907,16 @@ public class BpmnModelUtils {
* @return 符合条件的路径 * @return 符合条件的路径
*/ */
private static SequenceFlow findMatchSequenceFlowByExclusiveGateway(Gateway gateway, Map<String, Object> variables) { private static SequenceFlow findMatchSequenceFlowByExclusiveGateway(Gateway gateway, Map<String, Object> variables) {
SequenceFlow matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(), // TODO 表单无可编辑字段时variables为空流程走向会出现问题比如流程审批过程中无需要修改的字段值
SequenceFlow matchSequenceFlow;
if (CollUtil.isNotEmpty(variables)){
matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(),
flow -> ObjUtil.notEqual(gateway.getDefaultFlow(), flow.getId()) flow -> ObjUtil.notEqual(gateway.getDefaultFlow(), flow.getId())
&& (evalConditionExpress(variables, flow.getConditionExpression()))); && (evalConditionExpress(variables, flow.getConditionExpression())));
}else {
matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(),
flow -> ObjUtil.notEqual(gateway.getDefaultFlow(), flow.getId()));
}
if (matchSequenceFlow == null) { if (matchSequenceFlow == null) {
matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(), matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(),
flow -> ObjUtil.equal(gateway.getDefaultFlow(), flow.getId())); flow -> ObjUtil.equal(gateway.getDefaultFlow(), flow.getId()));

View File

@ -40,9 +40,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import java.util.List; import java.util.*;
import java.util.Map;
import java.util.Objects;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
@ -209,11 +207,11 @@ public class BpmModelServiceImpl implements BpmModelService {
public void deployModel(Long userId, String id) { public void deployModel(Long userId, String id) {
// 1.1 校验流程模型存在 // 1.1 校验流程模型存在
Model model = validateModelManager(id, userId); Model model = validateModelManager(id, userId);
BpmModelMetaInfoVO metaInfo = BpmModelConvert.INSTANCE.parseMetaInfo(model);
// 1.2 校验流程图 // 1.2 校验流程图
byte[] bpmnBytes = getModelBpmnXML(model.getId()); byte[] bpmnBytes = getModelBpmnXML(model.getId());
validateBpmnXml(bpmnBytes); validateBpmnXml(bpmnBytes, metaInfo.getType());
// 1.3 校验表单已配 // 1.3 校验表单已配
BpmModelMetaInfoVO metaInfo = BpmModelConvert.INSTANCE.parseMetaInfo(model);
BpmFormDO form = validateFormConfig(metaInfo); BpmFormDO form = validateFormConfig(metaInfo);
// 1.4 校验任务分配规则已配置 // 1.4 校验任务分配规则已配置
taskCandidateInvoker.validateBpmnConfig(bpmnBytes); taskCandidateInvoker.validateBpmnConfig(bpmnBytes);
@ -233,7 +231,7 @@ public class BpmModelServiceImpl implements BpmModelService {
repositoryService.saveModel(model); repositoryService.saveModel(model);
} }
private void validateBpmnXml(byte[] bpmnBytes) { private void validateBpmnXml(byte[] bpmnBytes, int type) {
BpmnModel bpmnModel = BpmnModelUtils.getBpmnModel(bpmnBytes); BpmnModel bpmnModel = BpmnModelUtils.getBpmnModel(bpmnBytes);
if (bpmnModel == null) { if (bpmnModel == null) {
throw exception(MODEL_NOT_EXISTS); throw exception(MODEL_NOT_EXISTS);
@ -243,24 +241,23 @@ public class BpmModelServiceImpl implements BpmModelService {
if (startEvent == null) { if (startEvent == null) {
throw exception(MODEL_DEPLOY_FAIL_BPMN_START_EVENT_NOT_EXISTS); throw exception(MODEL_DEPLOY_FAIL_BPMN_START_EVENT_NOT_EXISTS);
} }
// 2. 校验第一个用户任务的规则类型是否为审批人自选如果是则抛出异常原因是流程发起后直接进入第一个用户任务会出现无审批人的情况 // 2. 校验 UserTask name 都配置了
List<SequenceFlow> outgoingFlows = startEvent.getOutgoingFlows();
// TODO @小北可能极端情况下startEvent 后面接了个 serviceTask接着才是 userTask
// TODO @小北simple 因为第一个任务是发起人可能要找第二个任务
if (CollUtil.isNotEmpty(outgoingFlows)) {
FlowElement targetFlowElement = outgoingFlows.get(0).getTargetFlowElement();
Integer candidateStrategy = parseCandidateStrategy(targetFlowElement);
if (Objects.equals(candidateStrategy, BpmTaskCandidateStrategyEnum.APPROVE_USER_SELECT.getStrategy())) {
throw exception(MODEL_DEPLOY_FAIL_FIRST_USER_TASK_CANDIDATE_STRATEGY_ERROR, targetFlowElement.getName());
}
}
// 3. 校验 UserTask name 都配置了
List<UserTask> userTasks = BpmnModelUtils.getBpmnModelElements(bpmnModel, UserTask.class); List<UserTask> userTasks = BpmnModelUtils.getBpmnModelElements(bpmnModel, UserTask.class);
userTasks.forEach(userTask -> { userTasks.forEach(userTask -> {
if (StrUtil.isEmpty(userTask.getName())) { if (StrUtil.isEmpty(userTask.getName())) {
throw exception(MODEL_DEPLOY_FAIL_BPMN_USER_TASK_NAME_NOT_EXISTS, userTask.getId()); throw exception(MODEL_DEPLOY_FAIL_BPMN_USER_TASK_NAME_NOT_EXISTS, userTask.getId());
} }
}); });
// 3. 校验第一个用户任务节点的规则类型是否为审批人自选
Map<Integer, UserTask> userTaskMap = new HashMap<>();
// BPMN 设计器校验第一个用户任务节点
userTaskMap.put(BpmModelTypeEnum.BPMN.getType(), userTasks.get(0));
// SIMPLE 设计器第一个节点固定为发起人所以校验第二个用户任务节点
userTaskMap.put(BpmModelTypeEnum.SIMPLE.getType(), userTasks.get(1));
Integer candidateStrategy = parseCandidateStrategy(userTaskMap.get(type));
if (Objects.equals(candidateStrategy, BpmTaskCandidateStrategyEnum.APPROVE_USER_SELECT.getStrategy())) {
throw exception(MODEL_DEPLOY_FAIL_FIRST_USER_TASK_CANDIDATE_STRATEGY_ERROR, userTaskMap.get(type).getName());
}
} }
@Override @Override

View File

@ -221,8 +221,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
processDefinitionInfo, processDefinitionInfo,
processVariables, activities); processVariables, activities);
// 3.3 如果是发起动作activityId 为开始节点不校验审批人自选节点 // 3.3 如果是发起动作activityId 为开始节点不校验审批人自选节点
// TODO @小北ObjUtil.equals(reqVO.getActivityId(), BpmnModelConstants.START_USER_NODE_ID) 够啦不用判空 if (ObjUtil.equals(reqVO.getActivityId(), BpmnModelConstants.START_USER_NODE_ID)) {
if (ObjUtil.isNotNull(reqVO.getActivityId()) && ObjUtil.equals(reqVO.getActivityId(), BpmnModelConstants.START_USER_NODE_ID)) {
simulateActivityNodes.removeIf(node -> simulateActivityNodes.removeIf(node ->
BpmTaskCandidateStrategyEnum.APPROVE_USER_SELECT.getStrategy().equals(node.getCandidateStrategy())); BpmTaskCandidateStrategyEnum.APPROVE_USER_SELECT.getStrategy().equals(node.getCandidateStrategy()));
} }

View File

@ -558,18 +558,12 @@ public class BpmTaskServiceImpl implements BpmTaskService {
taskService.addComment(task.getId(), task.getProcessInstanceId(), BpmCommentTypeEnum.APPROVE.getType(), taskService.addComment(task.getId(), task.getProcessInstanceId(), BpmCommentTypeEnum.APPROVE.getType(),
BpmCommentTypeEnum.APPROVE.formatComment(reqVO.getReason())); BpmCommentTypeEnum.APPROVE.formatComment(reqVO.getReason()));
// 2.3 调用 BPM complete 去完成任务 // 2.3 调用 BPM complete 去完成任务
// 其中variables 是存储动态表单到 local 任务级别过滤一下避免 ProcessInstance 系统级的变量被占用
if (CollUtil.isNotEmpty(reqVO.getVariables())) {
// 校验并处理 APPROVE_USER_SELECT 当前审批人选择下一节点审批人的逻辑 // 校验并处理 APPROVE_USER_SELECT 当前审批人选择下一节点审批人的逻辑
Map<String, Object> variables = validateAndSetNextAssignees(task.getTaskDefinitionKey(), reqVO.getVariables(), Map<String, Object> variables = validateAndSetNextAssignees(task.getTaskDefinitionKey(), reqVO.getVariables(),
bpmnModel, reqVO.getNextAssignees(), instance); bpmnModel, reqVO.getNextAssignees(), instance);
// 完成任务 // 完成任务
runtimeService.setVariables(task.getProcessInstanceId(), variables); runtimeService.setVariables(task.getProcessInstanceId(), variables);
taskService.complete(task.getId(), variables, true); taskService.complete(task.getId(), variables, true);
} else {
// 完成任务
taskService.complete(task.getId());
}
// 加签专属处理加签任务 // 加签专属处理加签任务
handleParentTaskIfSign(task.getParentTaskId()); handleParentTaskIfSign(task.getParentTaskId());
@ -590,37 +584,30 @@ public class BpmTaskServiceImpl implements BpmTaskService {
*/ */
private Map<String, Object> validateAndSetNextAssignees(String taskDefinitionKey, Map<String, Object> variables, BpmnModel bpmnModel, private Map<String, Object> validateAndSetNextAssignees(String taskDefinitionKey, Map<String, Object> variables, BpmnModel bpmnModel,
Map<String, List<Long>> nextAssignees, ProcessInstance processInstance) { Map<String, List<Long>> nextAssignees, ProcessInstance processInstance) {
// 下一个节点参数为空不做处理表示流程正常流转无需选择下一个节点的审判人
// TODO @小北会出现漏选其实需要的情况哇
if (CollUtil.isEmpty(nextAssignees)) {
return variables;
}
// 1. 获取下一个将要执行的节点集合 // 1. 获取下一个将要执行的节点集合
FlowElement flowElement = bpmnModel.getFlowElement(taskDefinitionKey); FlowElement flowElement = bpmnModel.getFlowElement(taskDefinitionKey);
List<FlowNode> nextFlowNodes = getNextFlowNodes(flowElement, bpmnModel, variables); List<FlowNode> nextFlowNodes = getNextFlowNodes(flowElement, bpmnModel, variables);
// 2. 循环下一个将要执行的节点集合 // 2. 循环下一个将要执行的节点集合
Map<String, List<Long>> processVariables = new HashMap<>(); Map<String, List<Long>> processVariables;
for (FlowNode nextFlowNode : nextFlowNodes) { for (FlowNode nextFlowNode : nextFlowNodes) {
if (!nextAssignees.containsKey(nextFlowNode.getId())) { // 获取任务节点中的审批人策略
throw exception(TASK_TARGET_NODE_NOT_EXISTS, nextFlowNode.getName());
}
List<Long> assignees = nextAssignees.get(nextFlowNode.getId());
// 2.1 情况一如果节点中的审批人策略为 发起人自选
Integer candidateStrategy = parseCandidateStrategy(nextFlowNode); Integer candidateStrategy = parseCandidateStrategy(nextFlowNode);
// 2.1 情况一如果节点中的审批人策略为 发起人自选
if (ObjUtil.equals(candidateStrategy, BpmTaskCandidateStrategyEnum.START_USER_SELECT.getStrategy())) { if (ObjUtil.equals(candidateStrategy, BpmTaskCandidateStrategyEnum.START_USER_SELECT.getStrategy())) {
// 如果节点存在但未配置审批人
List<Long> assignees = nextAssignees != null ? nextAssignees.get(nextFlowNode.getId()) : null;
if (CollUtil.isEmpty(assignees)) {
throw exception(PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_CONFIG, nextFlowNode.getName());
}
processVariables = FlowableUtils.getStartUserSelectAssignees(processInstance.getProcessVariables()); processVariables = FlowableUtils.getStartUserSelectAssignees(processInstance.getProcessVariables());
if (processVariables == null){ if (processVariables == null){
processVariables = new HashMap<>(); processVariables = new HashMap<>();
} }else {
List<Long> startUserSelectAssignee = processVariables.get(nextFlowNode.getId()); List<Long> startUserSelectAssignee = processVariables.get(nextFlowNode.getId());
// 特殊如果当前节点已经存在审批人则不允许覆盖 // 特殊如果当前节点已经存在审批人则不允许覆盖
if (CollUtil.isNotEmpty(startUserSelectAssignee)) { if (CollUtil.isNotEmpty(startUserSelectAssignee)) {
continue; continue;
} }
// 如果节点存在但未配置审批人
if (CollUtil.isEmpty(assignees)) {
throw exception(PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_CONFIG, nextFlowNode.getName());
} }
// 校验通过的全部节点和审批人 // 校验通过的全部节点和审批人
processVariables.put(nextFlowNode.getId(), assignees); processVariables.put(nextFlowNode.getId(), assignees);
@ -629,12 +616,22 @@ public class BpmTaskServiceImpl implements BpmTaskService {
// 2.2 情况二如果节点中的审批人策略为 审批人在审批时选择下一个节点的审批人并且该节点的审批人为空 // 2.2 情况二如果节点中的审批人策略为 审批人在审批时选择下一个节点的审批人并且该节点的审批人为空
if (ObjUtil.equals(candidateStrategy, BpmTaskCandidateStrategyEnum.APPROVE_USER_SELECT.getStrategy())) { if (ObjUtil.equals(candidateStrategy, BpmTaskCandidateStrategyEnum.APPROVE_USER_SELECT.getStrategy())) {
// 如果节点存在但未配置审批人 // 如果节点存在但未配置审批人
List<Long> assignees = nextAssignees != null ? nextAssignees.get(nextFlowNode.getId()) : null;
if (CollUtil.isEmpty(assignees)) { if (CollUtil.isEmpty(assignees)) {
throw exception(PROCESS_INSTANCE_APPROVE_USER_SELECT_ASSIGNEES_NOT_CONFIG, nextFlowNode.getName()); throw exception(PROCESS_INSTANCE_APPROVE_USER_SELECT_ASSIGNEES_NOT_CONFIG, nextFlowNode.getName());
} }
processVariables = FlowableUtils.getApproveUserSelectAssignees(processInstance.getProcessVariables());
if (processVariables == null){
processVariables = new HashMap<>();
}else {
List<Long> approveUserSelectAssignee = processVariables.get(nextFlowNode.getId());
// 特殊如果当前节点已经存在审批人则不允许覆盖
if (CollUtil.isNotEmpty(approveUserSelectAssignee)) {
continue;
}
}
// 校验通过的全部节点和审批人 // 校验通过的全部节点和审批人
processVariables.put(nextFlowNode.getId(), assignees); processVariables.put(nextFlowNode.getId(), assignees);
// TODO @小北是不是要类似情况一的做法通过 PROCESS_INSTANCE_VARIABLE_APPROVE_USER_SELECT_ASSIGNEES 拿一下因为如果 task1 是审批人自选task2 是审批人自选看着会覆盖
variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_APPROVE_USER_SELECT_ASSIGNEES, processVariables); variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_APPROVE_USER_SELECT_ASSIGNEES, processVariables);
} }
} }