Parcourir la source

消息模块自测调整

1811872455@163.com il y a 2 semaines
Parent
commit
564e72b46f

+ 145 - 0
java/storlead-message/storlead-message-api/src/main/java/com/storlead/message/controller/MessageTemplateApiController.java

@@ -0,0 +1,145 @@
+package com.storlead.message.controller;
+
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.storlead.framework.common.result.Result;
+import com.storlead.framework.mybatis.entity.SysBaseField;
+import com.storlead.message.pojo.entity.MessageTemplateEventDetailEntity;
+import com.storlead.message.pojo.entity.MessageTemplateEventGroupEntity;
+import com.storlead.message.pojo.vo.MessageArgTemplateVO;
+import com.storlead.message.pojo.vo.MessageTemplateVO;
+import com.storlead.message.service.MessageTemplateEventDetailService;
+import com.storlead.message.service.MessageTemplateEventGroupService;
+import com.storlead.message.pojo.dto.MessageTemplateEventDTO;
+import com.storlead.message.util.MessageArgTemplateUtil;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.BeanUtils;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * @program: sp-sales-platform
+ * @description:
+ * @author: chenkq
+ * @create: 2024-04-26 15:15
+ */
+@RestController
+@RequestMapping("/message/template")
+@Api(tags = "消息: 消息模板机制配置")
+public class MessageTemplateApiController {
+
+    @Resource
+    private MessageTemplateEventGroupService templateEventGroupService;
+
+    @Resource
+    private MessageTemplateEventDetailService templateEventDetailService;
+
+    @ApiOperation("保存模板")
+    @PostMapping("save")
+    public Result save(@RequestBody MessageTemplateEventGroupEntity groupEntity) {
+
+//      groupEntity.dictFiled();
+        templateEventGroupService.saveOrUpdate(groupEntity);
+        return Result.ok();
+    }
+
+
+    @ApiOperation("分页查询")
+    @PostMapping("listPage")
+    public Result listPage(@RequestBody MessageTemplateEventDTO dto) {
+
+        Page<MessageTemplateEventGroupEntity> page = new Page<>(dto.getPageIndex(),dto.getPageSize());
+        LambdaQueryWrapper<MessageTemplateEventGroupEntity> queryWrapper = new LambdaQueryWrapper();
+        if (Objects.nonNull(dto.getTemplateServiceType())) {
+            queryWrapper.eq(MessageTemplateEventGroupEntity::getTemplateServiceType,dto.getTemplateServiceType());
+        }
+        if (Objects.nonNull(dto.getAppId())) {
+            queryWrapper.eq(MessageTemplateEventGroupEntity::getAppId, dto.getAppId());
+        }
+        if (StrUtil.isNotBlank(dto.getBlurry())) {
+            queryWrapper.like(MessageTemplateEventGroupEntity::getEventName,dto.getBlurry());
+        }
+        IPage<MessageTemplateVO>  pageList = templateEventGroupService.listPage(page, queryWrapper);
+        if (CollectionUtil.isNotEmpty(pageList.getRecords())) {
+            List<Long> dt = pageList.getRecords().stream().map(MessageTemplateVO::getId).collect(Collectors.toList());
+            LambdaQueryWrapper<MessageTemplateEventDetailEntity> wrapper = new LambdaQueryWrapper<>();
+            wrapper.in(MessageTemplateEventDetailEntity::getTemplateEventId,dt);
+            List<MessageTemplateEventDetailEntity>  detailEntities = templateEventDetailService.list(wrapper);
+            Map<Long, List<MessageTemplateEventDetailEntity>> groupMap = detailEntities.stream().collect(Collectors.groupingBy(MessageTemplateEventDetailEntity::getTemplateEventId));
+            pageList.getRecords().forEach(arg -> {
+                arg.setDetailEntities(groupMap.get(arg.getId()));
+            });
+            SysBaseField.dictFileds(pageList.getRecords());
+        }
+        return Result.ok(pageList);
+    }
+
+    @ApiOperation("保存模板详情")
+    @PostMapping("saveTempleteContent")
+    public Result saveTempleteContent(@RequestBody MessageTemplateEventDetailEntity entity) {
+        templateEventDetailService.saveOrUpdate(entity);
+        return Result.ok();
+    }
+
+    @ApiOperation("获取消息模板详情")
+    @PostMapping("getGroupDetail")
+    public Result getGroupDetail(Long id,String templateDetailType) {
+        MessageTemplateEventGroupEntity templateEventGroup =  templateEventGroupService.getById(id);
+        if (Objects.isNull(templateEventGroup)) {
+            return Result.ok();
+        }
+        MessageTemplateVO templateVO = new MessageTemplateVO();
+        BeanUtils.copyProperties(templateEventGroup,templateVO);
+        LambdaQueryWrapper<MessageTemplateEventDetailEntity> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(MessageTemplateEventDetailEntity::getTemplateEventId,templateEventGroup.getId());
+        if (Objects.nonNull(templateDetailType)) {
+            wrapper.eq(MessageTemplateEventDetailEntity::getTemplateDetailType,templateDetailType);
+        }
+        List<MessageTemplateEventDetailEntity>  detailEntities = templateEventDetailService.list(wrapper);
+        templateVO.setDetailEntities(detailEntities);
+        templateVO.dictFiled();
+
+        List<MessageArgTemplateVO> argTemplatels = MessageArgTemplateUtil.parseArgTemplateJson(templateEventGroup.getArgTemplateJson());
+        templateVO.setArgls(argTemplatels);
+        return Result.result(templateVO);
+    }
+    @ApiOperation("获取消息模板详情")
+    @PostMapping("getDetail")
+    public Result getDetail(Long id) {
+        MessageTemplateEventDetailEntity detailEntity =  templateEventDetailService.getById(id);
+        return Result.result(detailEntity);
+    }
+
+    @ApiOperation("禁用或启用模型")
+    @PostMapping("enabledDetail")
+    public Result enabledTemplateDetail(Long id,Boolean enable) {
+        LambdaUpdateWrapper<MessageTemplateEventDetailEntity> updateWrapper = new LambdaUpdateWrapper<>();
+        updateWrapper.set(MessageTemplateEventDetailEntity::getEnabled,enable);
+        updateWrapper.eq(MessageTemplateEventDetailEntity::getId,id);
+        templateEventDetailService.update(updateWrapper);
+        return Result.ok();
+    }
+
+    @ApiOperation("删除模板")
+    @PostMapping("delete")
+    public Result delete(Long id) {
+        if(Objects.isNull(id)) {
+            return Result.error("参数错误");
+        }
+        templateEventGroupService.lgDelete(id);
+        return Result.ok();
+    }
+}

+ 112 - 0
java/storlead-message/storlead-message-api/src/main/java/com/storlead/message/controller/UserMessageConfigApiController.java

@@ -0,0 +1,112 @@
+package com.storlead.message.controller;
+
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.storlead.framework.common.result.Result;
+import com.storlead.framework.util.LoginUserUtil;
+import com.storlead.message.pojo.dto.MessageTemplateEventDTO;
+import com.storlead.message.pojo.entity.MessageTemplateEventGroupEntity;
+import com.storlead.message.pojo.entity.UserMessageNoticeConfigEntity;
+import com.storlead.message.pojo.vo.MessageTemplateVO;
+import com.storlead.message.service.MessageTemplateEventGroupService;
+import com.storlead.message.service.UserMessageNoticeConfigService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.util.CollectionUtils;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * @program: sp-sales-platform
+ * @description:
+ * @author: chenkq
+ * @create: 2025-04-25 11:26
+ */
+@RestController
+@RequestMapping("/message/config")
+@Api(tags = "消息: 用户消息设置")
+public class UserMessageConfigApiController {
+
+    @Resource
+    private UserMessageNoticeConfigService userMessageNoticeConfigService;
+
+    @Resource
+    private MessageTemplateEventGroupService templateEventGroupService;
+
+    @ApiOperation("保存用户配置详情")
+    @PostMapping("setMessageConfig")
+    public Result setMessageConfig(@RequestBody UserMessageNoticeConfigEntity entity) {
+
+        LambdaQueryWrapper<UserMessageNoticeConfigEntity> queryWrapper = new LambdaQueryWrapper();
+        queryWrapper.eq(UserMessageNoticeConfigEntity::getOwnerBy, LoginUserUtil.getCurrentUserId());
+        queryWrapper.eq(UserMessageNoticeConfigEntity::getTemplateEventId,entity.getTemplateEventId());
+        queryWrapper.eq(UserMessageNoticeConfigEntity::getTemplateDetailType,entity.getTemplateDetailType());
+        queryWrapper.last(" limit 1");
+        UserMessageNoticeConfigEntity o1 = userMessageNoticeConfigService.getOne(queryWrapper);
+        if (Objects.nonNull(o1)) {
+            entity.setId(o1.getId());
+        }
+        userMessageNoticeConfigService.saveOrUpdate(entity);
+        return Result.ok();
+    }
+
+    @ApiOperation("分页查询")
+    @PostMapping("listPage")
+    public Result listPage(@RequestBody MessageTemplateEventDTO dto) {
+        Page<MessageTemplateEventGroupEntity> page = new Page<>(dto.getPageIndex(),dto.getPageSize());
+        LambdaQueryWrapper<MessageTemplateEventGroupEntity> queryWrapper = new LambdaQueryWrapper();
+        if (Objects.nonNull(dto.getTemplateServiceType())) {
+            queryWrapper.eq(MessageTemplateEventGroupEntity::getTemplateServiceType,dto.getTemplateServiceType());
+        }
+        if (Objects.nonNull(dto.getAppId())) {
+            queryWrapper.eq(MessageTemplateEventGroupEntity::getAppId, dto.getAppId());
+        }
+        if (StrUtil.isNotBlank(dto.getBlurry())) {
+            queryWrapper.like(MessageTemplateEventGroupEntity::getEventName,dto.getBlurry());
+        }
+        IPage<MessageTemplateVO> pageList = templateEventGroupService.listPage(page, queryWrapper);
+        if (CollectionUtil.isNotEmpty(pageList.getRecords())) {
+            List<Long> dt = pageList.getRecords().stream().map(MessageTemplateVO::getId).collect(Collectors.toList());
+            LambdaQueryWrapper<UserMessageNoticeConfigEntity> wrapper = new LambdaQueryWrapper<>();
+            wrapper.in(UserMessageNoticeConfigEntity::getTemplateEventId,dt);
+            wrapper.eq(UserMessageNoticeConfigEntity::getOwnerBy, LoginUserUtil.getCurrentUserId());
+            List<UserMessageNoticeConfigEntity>  detailEntities = userMessageNoticeConfigService.list(wrapper);
+            Map<Long, List<UserMessageNoticeConfigEntity>> groupMap = detailEntities.stream().collect(Collectors.groupingBy(UserMessageNoticeConfigEntity::getTemplateEventId));
+            pageList.getRecords().forEach(arg -> {
+                List<UserMessageNoticeConfigEntity> rs =  groupMap.get(arg.getId());
+                arg.setUserDetails(Objects.isNull(rs) ? Collections.emptyList() : rs);
+            });
+        }
+        return Result.ok(pageList);
+    }
+
+    @ApiOperation("消息开启关闭")
+    @PostMapping("on_off_message_config")
+    public Result onOffMessageConfig(@RequestBody UserMessageNoticeConfigEntity entity) {
+
+        //    @ApiModelProperty(value = "site:站内,sms:短信,wecom:企业微信,mail:邮件")
+        List<String> templateDetailTypels = CollectionUtils.isEmpty(entity.getTemplateDetailTypels()) ? Arrays.asList("site","wecom","mail") : entity.getTemplateDetailTypels();
+        for (String templateDetailType : templateDetailTypels) {
+            LambdaQueryWrapper<UserMessageNoticeConfigEntity> queryWrapper = new LambdaQueryWrapper();
+            queryWrapper.eq(UserMessageNoticeConfigEntity::getOwnerBy,LoginUserUtil.getCurrentUserId());
+            queryWrapper.eq(UserMessageNoticeConfigEntity::getTemplateEventId,entity.getTemplateEventId());
+            queryWrapper.eq(UserMessageNoticeConfigEntity::getTemplateDetailType,templateDetailType);
+            queryWrapper.last(" limit 1");
+            UserMessageNoticeConfigEntity o1 = userMessageNoticeConfigService.getOne(queryWrapper);
+            if (Objects.nonNull(o1)) {
+                entity.setId(o1.getId());
+            }
+            userMessageNoticeConfigService.saveOrUpdate(entity);
+        }
+        return Result.ok();
+    }
+}

+ 33 - 0
java/storlead-message/storlead-message-core/src/main/java/com/storlead/message/util/MessageArgTemplateUtil.java

@@ -0,0 +1,33 @@
+package com.storlead.message.util;
+
+import cn.hutool.core.util.StrUtil;
+import com.alibaba.fastjson.JSON;
+import com.storlead.message.pojo.vo.MessageArgTemplateVO;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * 消息模板变量配置 JSON 解析。
+ */
+public final class MessageArgTemplateUtil {
+
+    private MessageArgTemplateUtil() {
+    }
+
+    /**
+     * 解析 {@code argTemplateJson} 为模板变量列表。
+     * JSON 示例:[{"propertyName":"客户名称","propertyKey":"${customerName}$"}]
+     */
+    public static List<MessageArgTemplateVO> parseArgTemplateJson(String argTemplateJson) {
+        if (StrUtil.isBlank(argTemplateJson)) {
+            return Collections.emptyList();
+        }
+        try {
+            List<MessageArgTemplateVO> list = JSON.parseArray(argTemplateJson, MessageArgTemplateVO.class);
+            return list == null ? Collections.emptyList() : list;
+        } catch (Exception e) {
+            return Collections.emptyList();
+        }
+    }
+}

+ 190 - 0
java/storlead-system/sql/app_id_increment.sql

@@ -0,0 +1,190 @@
+-- =============================================================================
+-- 应用域 app_id 增量脚本(MySQL 5.7+)
+-- 说明:
+--   1. 新建 sys_app 应用主表,并与历史 magic number(10/20/30…)对齐
+--   2. 为业务表增量添加 app_id 字段(可重复执行)
+--   3. 可选:回填历史数据默认归属「销售」应用 app_id=20
+-- 执行前请备份;生产环境建议先在测试库验证
+-- =============================================================================
+
+SET NAMES utf8mb4;
+
+-- -----------------------------------------------------------------------------
+-- 1. 应用主表 sys_app
+-- -----------------------------------------------------------------------------
+CREATE TABLE IF NOT EXISTS `sys_app` (
+    `id`           BIGINT(20)   NOT NULL AUTO_INCREMENT COMMENT '主键,与 menu.app_id 等 FK 对齐',
+    `app_code`     VARCHAR(64)  NOT NULL COMMENT '应用编码,全局唯一',
+    `app_name`     VARCHAR(128) NOT NULL COMMENT '应用名称',
+    `app_title`    VARCHAR(128)          DEFAULT NULL COMMENT '前台标题',
+    `icon`         VARCHAR(255)          DEFAULT NULL COMMENT '图标',
+    `home_path`    VARCHAR(255)          DEFAULT NULL COMMENT '默认首页路由',
+    `owner_by`     BIGINT(20)            DEFAULT NULL,
+    `create_by`    BIGINT(20)            DEFAULT NULL,
+    `update_by`    BIGINT(20)            DEFAULT NULL,
+    `create_time`  DATETIME              DEFAULT NULL,
+    `update_time`  DATETIME              DEFAULT NULL,
+    `is_delete`    TINYINT(1)            DEFAULT 0 COMMENT '0未删除 1已删除',
+    `enabled`      TINYINT(1)            DEFAULT 1 COMMENT '1启用 0禁用',
+    `sort`         INT(11)               DEFAULT 0,
+    PRIMARY KEY (`id`),
+    UNIQUE KEY `uk_sys_app_app_code` (`app_code`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统应用主数据';
+
+-- -----------------------------------------------------------------------------
+-- 2. 安全加列存储过程(列已存在则跳过)
+-- -----------------------------------------------------------------------------
+DROP PROCEDURE IF EXISTS `proc_add_column_if_not_exists`;
+DELIMITER $$
+CREATE PROCEDURE `proc_add_column_if_not_exists`(
+    IN p_table  VARCHAR(64),
+    IN p_column VARCHAR(64),
+    IN p_ddl    TEXT
+)
+BEGIN
+    IF NOT EXISTS (
+        SELECT 1
+        FROM information_schema.COLUMNS
+        WHERE TABLE_SCHEMA = DATABASE()
+          AND TABLE_NAME = p_table
+          AND COLUMN_NAME = p_column
+    ) THEN
+        SET @sql = CONCAT('ALTER TABLE `', p_table, '` ADD COLUMN ', p_ddl);
+        PREPARE stmt FROM @sql;
+        EXECUTE stmt;
+        DEALLOCATE PREPARE stmt;
+    END IF;
+END$$
+DELIMITER ;
+
+-- menu(多数环境已有 app_id,此处兜底)
+CALL proc_add_column_if_not_exists(
+    'menu',
+    'app_id',
+    '`app_id` BIGINT(20) NULL COMMENT ''所属应用 ID,对应 sys_app.id'' AFTER `pid`'
+);
+
+-- 角色
+CALL proc_add_column_if_not_exists(
+    'role',
+    'app_id',
+    '`app_id` BIGINT(20) NULL COMMENT ''所属应用 ID,对应 sys_app.id'' AFTER `remark`'
+);
+
+-- AI 提示词模板
+CALL proc_add_column_if_not_exists(
+    'agent_prompt_template',
+    'app_id',
+    '`app_id` BIGINT(20) NULL COMMENT ''所属应用 ID,对应 sys_app.id'' AFTER `template_code`'
+);
+
+-- 站内消息记录
+CALL proc_add_column_if_not_exists(
+    'inside_message_record',
+    'app_id',
+    '`app_id` BIGINT(20) NULL COMMENT ''所属应用 ID,对应 sys_app.id'' AFTER `title`'
+);
+
+-- 消息模板事件组
+CALL proc_add_column_if_not_exists(
+    'message_template_event_group',
+    'app_id',
+    '`app_id` BIGINT(20) NULL COMMENT ''所属应用 ID,对应 sys_app.id'' AFTER `template_service_name`'
+);
+
+-- 消息模板明细
+CALL proc_add_column_if_not_exists(
+    'message_template_event_detail',
+    'app_id',
+    '`app_id` BIGINT(20) NULL COMMENT ''所属应用 ID,对应 sys_app.id'' AFTER `template_event_id`'
+);
+
+DROP PROCEDURE IF EXISTS `proc_add_column_if_not_exists`;
+
+-- -----------------------------------------------------------------------------
+-- 3. 索引(不存在则创建)
+-- -----------------------------------------------------------------------------
+-- role
+SET @idx_exists := (
+    SELECT COUNT(1) FROM information_schema.STATISTICS
+    WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'role' AND INDEX_NAME = 'idx_role_app_id'
+);
+SET @sql := IF(@idx_exists = 0, 'ALTER TABLE `role` ADD INDEX `idx_role_app_id` (`app_id`)', 'SELECT 1');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+-- agent_prompt_template
+SET @idx_exists := (
+    SELECT COUNT(1) FROM information_schema.STATISTICS
+    WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'agent_prompt_template' AND INDEX_NAME = 'idx_agent_prompt_template_app_id'
+);
+SET @sql := IF(@idx_exists = 0, 'ALTER TABLE `agent_prompt_template` ADD INDEX `idx_agent_prompt_template_app_id` (`app_id`)', 'SELECT 1');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+-- inside_message_record
+SET @idx_exists := (
+    SELECT COUNT(1) FROM information_schema.STATISTICS
+    WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'inside_message_record' AND INDEX_NAME = 'idx_inside_message_record_app_id'
+);
+SET @sql := IF(@idx_exists = 0, 'ALTER TABLE `inside_message_record` ADD INDEX `idx_inside_message_record_app_id` (`app_id`)', 'SELECT 1');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+-- message_template_event_group
+SET @idx_exists := (
+    SELECT COUNT(1) FROM information_schema.STATISTICS
+    WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'message_template_event_group' AND INDEX_NAME = 'idx_msg_tpl_group_app_id'
+);
+SET @sql := IF(@idx_exists = 0, 'ALTER TABLE `message_template_event_group` ADD INDEX `idx_msg_tpl_group_app_id` (`app_id`)', 'SELECT 1');
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
+
+-- -----------------------------------------------------------------------------
+-- 4. 初始化 sys_app(与历史 magic number 对齐,id 固定便于 menu.app_id 引用)
+-- -----------------------------------------------------------------------------
+SET @OLD_SQL_MODE = @@SQL_MODE;
+SET SESSION sql_mode = CONCAT(IFNULL(@@SQL_MODE, ''), ',NO_AUTO_VALUE_ON_ZERO');
+
+INSERT INTO `sys_app` (`id`, `app_code`, `app_name`, `app_title`, `home_path`, `enabled`, `sort`, `is_delete`, `create_time`)
+VALUES
+    (0,  'main',      '主应用',     '主应用',     NULL, 1, 0,  0, NOW()),
+    (10, 'trade',     '智能贸易',   '智能贸易',   NULL, 1, 10, 0, NOW()),
+    (20, 'sales',     '销售',       '销售',       NULL, 1, 20, 0, NOW()),
+    (30, 'otr',       'OTR',        'OTR',        NULL, 1, 30, 0, NOW()),
+    (40, 'payroll',   '工资',       '工资',       NULL, 1, 40, 0, NOW()),
+    (50, 'project',   '项目',       '项目',       NULL, 1, 50, 0, NOW()),
+    (60, 'warehouse', '领存平台',   '领存平台',   NULL, 1, 60, 0, NOW())
+ON DUPLICATE KEY UPDATE
+    `app_name`   = VALUES(`app_name`),
+    `app_title`  = VALUES(`app_title`),
+    `enabled`    = VALUES(`enabled`),
+    `sort`       = VALUES(`sort`),
+    `update_time`= NOW();
+
+SET SESSION sql_mode = @OLD_SQL_MODE;
+
+-- -----------------------------------------------------------------------------
+-- 5. 历史数据回填(按需执行,默认归到销售 app_id=20)
+--    若 menu 已有 app_id 可不执行第一条
+-- -----------------------------------------------------------------------------
+-- UPDATE `menu` SET `app_id` = 20 WHERE `app_id` IS NULL;
+
+UPDATE `inside_message_record`
+SET `app_id` = 20
+WHERE `app_id` IS NULL;
+
+UPDATE `message_template_event_group`
+SET `app_id` = 20
+WHERE `app_id` IS NULL;
+
+UPDATE `message_template_event_detail` d
+INNER JOIN `message_template_event_group` g ON g.id = d.template_event_id
+SET d.`app_id` = g.`app_id`
+WHERE d.`app_id` IS NULL AND g.`app_id` IS NOT NULL;
+
+-- 角色、提示词模板:无历史 app 归属时可默认 20,或按业务自行 UPDATE
+-- UPDATE `role` SET `app_id` = 20 WHERE `app_id` IS NULL;
+-- UPDATE `agent_prompt_template` SET `app_id` = 20 WHERE `app_id` IS NULL;
+
+-- =============================================================================
+-- 验证
+-- SELECT id, app_code, app_name FROM sys_app ORDER BY sort;
+-- SHOW COLUMNS FROM inside_message_record LIKE 'app_id';
+-- =============================================================================