fix(bpm): 修复发起流程报 500 - 流程定义编号 URL 编码归一化 + 缺失定义容错

1. 新增 BpmProcessDefinitionIdUtils,对 processDefinitionId 做幂等归一化(最多解 5 层 URL 编码),前端多编码几次也不会 500

2. BpmProcessDefinitionServiceImpl 的 getProcessDefinition / getProcessDefinitionBpmnModel / getProcessDefinitionInfo / 批量查询统一归一化;流程定义不存在时返回 null 而非抛 FlowableObjectNotFoundException

3. BpmProcessInstanceServiceImpl 的 getApprovalDetail、createProcessInstance 增加空值校验,转为明确的 PROCESS_DEFINITION_NOT_EXISTS 业务错误

4. BpmProcessDefinitionInfoMapper 查询改为取最新一条,兼容同一 processDefinitionId 多条脏数据导致的 TooManyResultsException

5. BpmProcessInstanceConvert.buildProcessInstance 容错流程定义已删除的场景
已添加1个文件
已修改5个文件
133 ■■■■■ 文件已修改
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmProcessDefinitionController.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmProcessInstanceConvert.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/definition/BpmProcessDefinitionInfoMapper.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmProcessDefinitionIdUtils.java 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionServiceImpl.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmProcessDefinitionController.java
@@ -122,8 +122,10 @@
        ProcessDefinition processDefinition = id != null ? processDefinitionService.getProcessDefinition(id)
                : processDefinitionService.getActiveProcessDefinition(key);
        if (processDefinition == null) {
            // ç¼–号不存在时返回 null,与「key æŸ¥ä¸åˆ°ã€ä¿æŒä¸€è‡´ï¼Œé¿å… Flowable æŠ›å¼‚常变成 500
            return success(null);
        }
        // ç¼–号不存在时上面的查询会返回 null,这里直接返回 null,避免 NPE å˜æˆ 500
        BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionService.getProcessDefinitionInfo(processDefinition.getId());
        BpmnModel bpmnModel = processDefinitionService.getProcessDefinitionBpmnModel(processDefinition.getId());
        return success(BpmProcessDefinitionConvert.INSTANCE.buildProcessDefinition(
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmProcessInstanceConvert.java
@@ -107,9 +107,13 @@
        BpmProcessInstanceRespVO respVO = BeanUtils.toBean(processInstance, BpmProcessInstanceRespVO.class);
        respVO.setStatus(FlowableUtils.getProcessInstanceStatus(processInstance))
                .setFormVariables(FlowableUtils.getProcessInstanceFormVariable(processInstance));
        // definition
        respVO.setProcessDefinition(BeanUtils.toBean(processDefinition, BpmProcessDefinitionRespVO.class));
        copyTo(processDefinitionInfo, respVO.getProcessDefinition());
        // definition(流程定义可能已被删除,需要容错,否则前端拿到 500)
        if (processDefinition != null) {
            respVO.setProcessDefinition(BeanUtils.toBean(processDefinition, BpmProcessDefinitionRespVO.class));
            if (respVO.getProcessDefinition() != null && processDefinitionInfo != null) {
                copyTo(processDefinitionInfo, respVO.getProcessDefinition());
            }
        }
        // user
        if (startUser != null) {
            respVO.setStartUser(BeanUtils.toBean(startUser, UserSimpleBaseVO.class));
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/definition/BpmProcessDefinitionInfoMapper.java
@@ -15,8 +15,18 @@
        return selectList(BpmProcessDefinitionInfoDO::getProcessDefinitionId, processDefinitionIds);
    }
    /**
     * æŸ¥è¯¢æµç¨‹å®šä¹‰çš„æ‰©å±•信息
     *
     * åŽ†å²åŽŸå› åŒä¸€ processDefinitionId å¯èƒ½å­˜åœ¨å¤šæ¡è®°å½•(例如重复部署/重复保存模型),
     * ç›´æŽ¥ç”¨ selectOne ä¼šæŠ› TooManyResultsException å¯¼è‡´æŽ¥å£ 500,
     * è¿™é‡Œæ”¹ä¸ºå–最新的一条,保证接口可用。
     */
    default BpmProcessDefinitionInfoDO selectByProcessDefinitionId(String processDefinitionId) {
        return selectOne(BpmProcessDefinitionInfoDO::getProcessDefinitionId, processDefinitionId);
        List<BpmProcessDefinitionInfoDO> list = selectList(new LambdaQueryWrapperX<BpmProcessDefinitionInfoDO>()
                .eq(BpmProcessDefinitionInfoDO::getProcessDefinitionId, processDefinitionId)
                .orderByDesc(BpmProcessDefinitionInfoDO::getId));
        return list.isEmpty() ? null : list.get(0);
    }
    default void updateByModelId(String modelId, BpmProcessDefinitionInfoDO updateObj) {
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmProcessDefinitionIdUtils.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,57 @@
package cn.iocoder.yudao.module.bpm.framework.flowable.core.util;
import cn.hutool.core.util.StrUtil;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
/**
 * Flowable æµç¨‹å®šä¹‰ç¼–号工具
 *
 * <p>流程定义编号形如 {@code qingjia:2:553a7ac9-...},包含冒号。
 * å‰ç«¯å¦‚果对该值做了 URL ç¼–码后再作为查询参数传递(双重编码),
 * Spring åªè§£ç ä¸€å±‚,最终拿到的仍是 {@code qingjia%3A2%3A...},
 * Flowable æŒ‰è¯¥ç¼–号查不到流程定义,直接抛 FlowableObjectNotFoundException å¯¼è‡´ 500。
 *
 * <p>这里在入口处做一次幂等的归一化:只在确实残留 % è½¬ä¹‰æ—¶æ‰è§£ç ï¼Œ
 * åˆæ³•的流程定义编号不可能包含 %,因此不会误伤。
 *
 * @author è¶…级管理员
 */
public class BpmProcessDefinitionIdUtils {
    /** æœ€å¤šè§£ç å±‚数,防止异常输入无限循环 */
    private static final int MAX_DECODE_TIMES = 5;
    private BpmProcessDefinitionIdUtils() {
    }
    /**
     * å½’一化流程定义编号:最多解码一层残留的 URL è½¬ä¹‰
     *
     * @param id æµç¨‹å®šä¹‰ç¼–号
     * @return å½’一化后的编号;入参为空时原样返回
     */
    public static String normalize(String id) {
        if (StrUtil.isBlank(id) || id.indexOf('%') < 0) {
            return id;
        }
        // å‰ç«¯å¯èƒ½ç¼–码多次(1~3 å±‚都见过),逐层解码直到不再含 % è½¬ä¹‰
        String current = id;
        for (int i = 0; i < MAX_DECODE_TIMES && current.indexOf('%') >= 0; i++) {
            String decoded;
            try {
                decoded = URLDecoder.decode(current, StandardCharsets.UTF_8);
            } catch (Exception ignored) {
                // è§£ç å¤±è´¥è¯´æ˜Žä¸æ˜¯ URL è½¬ä¹‰ï¼Œä¿æŒåŽŸå€¼
                break;
            }
            if (decoded.equals(current)) {
                break;
            }
            current = decoded;
        }
        return current;
    }
}
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionServiceImpl.java
@@ -11,12 +11,14 @@
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
import cn.iocoder.yudao.module.bpm.dal.mysql.definition.BpmProcessDefinitionInfoMapper;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmProcessDefinitionIdUtils;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.common.engine.api.FlowableObjectNotFoundException;
import org.flowable.common.engine.impl.db.SuspensionState;
import org.flowable.engine.RepositoryService;
import org.flowable.engine.repository.Deployment;
@@ -57,7 +59,18 @@
    @Override
    public ProcessDefinition getProcessDefinition(String id) {
        return repositoryService.getProcessDefinition(id);
        String definitionId = BpmProcessDefinitionIdUtils.normalize(id);
        if (StrUtil.isBlank(definitionId)) {
            return null;
        }
        // ç¼–号不存在时 Flowable ä¼šæŠ› FlowableObjectNotFoundException,这里统一转成 null ç”±è°ƒç”¨æ–¹å¤„理,
        // é¿å…å‰ç«¯æ‹¿åˆ° 500「系统异常」
        try {
            return repositoryService.getProcessDefinition(definitionId);
        } catch (FlowableObjectNotFoundException ex) {
            log.warn("[getProcessDefinition][流程定义({}) ä¸å­˜åœ¨]", definitionId);
            return null;
        }
    }
    @Override
@@ -65,7 +78,8 @@
        if (CollUtil.isEmpty(ids)) {
            return emptyList();
        }
        return repositoryService.createProcessDefinitionQuery().processDefinitionIds(ids).list();
        return repositoryService.createProcessDefinitionQuery().processDefinitionIds(
                ids.stream().map(BpmProcessDefinitionIdUtils::normalize).collect(java.util.stream.Collectors.toSet())).list();
    }
    @Override
@@ -88,7 +102,7 @@
    public ProcessDefinition getActiveProcessDefinition(String key) {
        return repositoryService.createProcessDefinitionQuery()
                .processDefinitionTenantId(FlowableUtils.getTenantId())
                .processDefinitionKey(key).active().singleResult();
                .processDefinitionKey(BpmProcessDefinitionIdUtils.normalize(key)).active().singleResult();
    }
    @Override
@@ -203,17 +217,30 @@
    @Override
    public BpmnModel getProcessDefinitionBpmnModel(String id) {
        return repositoryService.getBpmnModel(id);
        String definitionId = BpmProcessDefinitionIdUtils.normalize(id);
        if (StrUtil.isBlank(definitionId)) {
            return null;
        }
        try {
            return repositoryService.getBpmnModel(definitionId);
        } catch (FlowableObjectNotFoundException ex) {
            log.warn("[getProcessDefinitionBpmnModel][流程定义({}) ä¸å­˜åœ¨]", definitionId);
            return null;
        }
    }
    @Override
    public BpmProcessDefinitionInfoDO getProcessDefinitionInfo(String id) {
        return processDefinitionMapper.selectByProcessDefinitionId(id);
        return processDefinitionMapper.selectByProcessDefinitionId(BpmProcessDefinitionIdUtils.normalize(id));
    }
    @Override
    public List<BpmProcessDefinitionInfoDO> getProcessDefinitionInfoList(Collection<String> ids) {
        return processDefinitionMapper.selectListByProcessDefinitionIds(ids);
        if (CollUtil.isEmpty(ids)) {
            return emptyList();
        }
        return processDefinitionMapper.selectListByProcessDefinitionIds(
                ids.stream().map(BpmProcessDefinitionIdUtils::normalize).collect(java.util.stream.Collectors.toSet()));
    }
    @Override
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java
@@ -198,9 +198,13 @@
            processVariables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_ID, loginUserId);
        }
        // 1.3 è¯»å–其它相关数据
        ProcessDefinition processDefinition = processDefinitionService.getProcessDefinition(
                historicProcessInstance != null ? historicProcessInstance.getProcessDefinitionId()
                        : reqVO.getProcessDefinitionId());
        String processDefinitionId = historicProcessInstance != null ? historicProcessInstance.getProcessDefinitionId()
                : reqVO.getProcessDefinitionId();
        ProcessDefinition processDefinition = processDefinitionService.getProcessDefinition(processDefinitionId);
        if (processDefinition == null) {
            // æµç¨‹å®šä¹‰ä¸å­˜åœ¨æ—¶ï¼Œè¿”回明确的业务错误,避免抛出 500「系统异常」
            throw exception(ErrorCodeConstants.PROCESS_DEFINITION_NOT_EXISTS);
        }
        BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionService
                .getProcessDefinitionInfo(processDefinition.getId());
        BpmnModel bpmnModel = processDefinitionService.getProcessDefinitionBpmnModel(processDefinition.getId());
@@ -772,6 +776,9 @@
        // èŽ·å¾—æµç¨‹å®šä¹‰
        ProcessDefinition definition = processDefinitionService
                .getProcessDefinition(createReqVO.getProcessDefinitionId());
        if (definition == null) {
            throw exception(ErrorCodeConstants.PROCESS_DEFINITION_NOT_EXISTS);
        }
        // å‘起流程
        return createProcessInstance0(userId, definition, createReqVO.getVariables(), null,
                createReqVO.getStartUserSelectAssignees());