tuqin
2 天以前 601eca3d487e9833ffc6253579bb85496f767f23
fix(bpm): 修复流程模型修改后保存报「你不是该流程的管理员」

现象:流程模型 → 修改某条 → 第三步流程设计 → 保存,报
「操作流程失败,原因:你不是该流程(合同审批)的管理员」;
管理员问题修掉后又会变成「系统异常」
(FlowableOptimisticLockingException: was updated by another transaction concurrently)。

根因有两个,都在 validateModelManager / saveModel 链路上:

1) updateModel 是「先校验管理员、后写入」,校验读的是库里 META_INFO_ 的值。
而这批模型从旧备份恢复过来,managerUserIds 是空数组或整个字段缺失,
于是永久校验失败——前端即使传了新的 managerUserIds 也没用(校验不看请求体),
属于无法自愈的死锁。

2) act_re_model.REV_(Flowable 乐观锁版本号)有 14 条为 NULL,
saveModel 的 UPDATE 条件带 REV_ = ?,为 NULL 时匹配不到行,
Flowable 判定为并发修改 → 抛 OptimisticLockingException → 前端「系统异常」。

修复:
- validateModelManager 改为三层判定:
① 未配置管理员(空/缺失)→ 放行,并 warn 记录;
② 当前用户在管理员列表里 → 放行;
③ 当前用户是超级管理员(super_admin)→ 放行。
这样模型一旦被保存成只含某个具体账号,超管和换账号的人也还能继续维护。
- 新增 SQL 脚本 bpm_fix_model_manager_and_rev.sql:
补齐 managerUserIds(空数组/缺失 → [1])与 REV_(NULL → 1)。
已在 mom-zyjc 执行:20 个模型全部补齐。

实测(mom-zyjc,超管角色账号):
- 数据库管理员=[1]、当前用户非 1 时保存 → code:0(日志:超级管理员,放行);
- 保存后 REV_ 1 → 3,流程图写入 1884 字节;
- 普通角色账号(非超管、非管理员)保存 → 仍正确报「你不是该流程的管理员」,权限校验未被削弱。
已添加1个文件
已修改1个文件
54 ■■■■■ 文件已修改
sql/mysql/bpm_fix_model_manager_and_rev.sql 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/bpm_fix_model_manager_and_rev.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,31 @@
-- ============================================================
-- ä¿®å¤ã€Œæµç¨‹æ¨¡åž‹ â†’ ä¿®æ”¹ â†’ æµç¨‹è®¾è®¡ â†’ ä¿å­˜ã€å¤±è´¥
--
-- çŽ°è±¡ï¼šæŠ¥ã€Œæ“ä½œæµç¨‹å¤±è´¥ï¼ŒåŽŸå› ï¼šä½ ä¸æ˜¯è¯¥æµç¨‹(xxx)的管理员」,
--       æˆ–修掉管理员问题后变成「系统异常」
--       ï¼ˆFlowableOptimisticLockingException: was updated by another transaction concurrently)
--
-- æ ¹å› æœ‰ä¸¤ä¸ªï¼Œéƒ½å‡ºåœ¨ä»Žå¤‡ä»½æ¢å¤/迁移过来的 act_re_model æ•°æ®ä¸Šï¼š
--
-- 1) META_INFO_ é‡Œ managerUserIds ä¸ºç©ºæ•°ç»„或缺失
--    åŽç«¯ updateModel æ˜¯ã€Œå…ˆæ ¡éªŒç®¡ç†å‘˜ã€åŽå†™å…¥ã€ï¼Œæ ¡éªŒè¯»çš„æ˜¯åº“里的值,
--    æ‰€ä»¥ä¸€æ—¦ä¸ºç©ºå°±æ°¸ä¹…校验失败,无法自愈(前端怎么传新值都没用)。
--
-- 2) REV_(Flowable ä¹è§‚锁版本号)为 NULL
--    saveModel æ—¶çš„ UPDATE æ¡ä»¶å¸¦ REV_ = ?,为 NULL æ—¶åŒ¹é…ä¸åˆ°ä»»ä½•行,
--    Flowable è®¤ä¸ºè¢«å¹¶å‘修改,抛 OptimisticLockingException â†’ å‰ç«¯æ˜¾ç¤ºã€Œç³»ç»Ÿå¼‚常」。
--
-- æ‰§è¡ŒåŽéœ€é‡å¯åŽç«¯æœåŠ¡ï¼ˆFlowable æœ‰æ¨¡åž‹ç¼“存)。
-- ============================================================
-- 1) è¡¥é½æµç¨‹ç®¡ç†å‘˜ï¼šç©ºæ•°ç»„ â†’ è¶…管(1);缺失该字段的,补到 visible åŽé¢
UPDATE act_re_model
SET META_INFO_ = REPLACE(META_INFO_, '"managerUserIds":[]', '"managerUserIds":[1]')
WHERE META_INFO_ LIKE '%"managerUserIds":[]%';
UPDATE act_re_model
SET META_INFO_ = REPLACE(META_INFO_, '"visible":true', '"visible":true,"managerUserIds":[1]')
WHERE META_INFO_ NOT LIKE '%managerUserIds%';
-- 2) è¡¥é½ä¹è§‚锁版本号
UPDATE act_re_model SET REV_ = 1 WHERE REV_ IS NULL;
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java
@@ -4,6 +4,7 @@
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO;
@@ -204,10 +205,26 @@
    private Model validateModelManager(String id, Long userId) {
        Model model = validateModelExists(id);
        BpmModelMetaInfoVO metaInfo = BpmModelConvert.INSTANCE.parseMetaInfo(model);
        if (metaInfo == null || !CollUtil.contains(metaInfo.getManagerUserIds(), userId)) {
            throw exception(MODEL_UPDATE_FAIL_NOT_MANAGER, model.getName());
        // 1. æœªé…ç½®æµç¨‹ç®¡ç†å‘˜ â†’ æ”¾è¡Œã€‚
        //    åŽ†å²æ¨¡åž‹ï¼ˆå¤‡ä»½æ¢å¤/迁移)META_INFO é‡Œ managerUserIds ä¸ºç©ºæ•°ç»„或缺失,
        //    è€Œ updateModel æ˜¯ã€Œå…ˆæ ¡éªŒåŽå†™å…¥ã€ï¼Œä¸€æ—¦ä¸ºç©ºå°±ä¼šæ°¸ä¹…校验失败,无法自愈。
        if (metaInfo == null || CollUtil.isEmpty(metaInfo.getManagerUserIds())) {
            log.warn("[validateModelManager][模型({}) æœªé…ç½®æµç¨‹ç®¡ç†å‘˜ï¼Œè·³è¿‡ç®¡ç†å‘˜æ ¡éªŒ]", model.getName());
            return model;
        }
        return model;
        // 2. å½“前操作人在管理员列表里 â†’ æ”¾è¡Œ
        if (CollUtil.contains(metaInfo.getManagerUserIds(), userId)) {
            return model;
        }
        // 3. è¶…级管理员始终放行。
        //    å¦åˆ™æ¨¡åž‹ä¸€æ—¦è¢«ä¿å­˜æˆåªåŒ…含某个具体账号,换账号(含超管)就再也改不了该模型,
        //    æŠ¥ã€Œæ“ä½œæµç¨‹å¤±è´¥ï¼ŒåŽŸå› ï¼šä½ ä¸æ˜¯è¯¥æµç¨‹(xxx)的管理员」,形成死锁。
        if (SecurityFrameworkUtils.isSuperAdmin()) {
            log.warn("[validateModelManager][模型({}) ç®¡ç†å‘˜({}) ä¸å«å½“前用户({}),但为超级管理员,放行]",
                    model.getName(), metaInfo.getManagerUserIds(), userId);
            return model;
        }
        throw exception(MODEL_UPDATE_FAIL_NOT_MANAGER, model.getName());
    }
    @Override