Commit c24a3a02c28161e91672668037031c44bbf4f597

Authored by YunaiV
2 parents c48f52ec 97096216

Merge branch 'feature/im-dev' of https://gitee.com/zhijiantianya/ruoyi-vue-pro into master-jdk17

# Conflicts:
#	sql/mysql/ruoyi-vue-pro.sql
Showing 155 changed files with 2800 additions and 2541 deletions
sql/mysql/ruoyi-vue-pro.sql
... ... @@ -11,7 +11,7 @@
11 11 Target Server Version : 80200 (8.2.0)
12 12 File Encoding : 65001
13 13  
14   - Date: 19/06/2026 18:41:15
  14 + Date: 31/05/2026 22:29:18
15 15 */
16 16  
17 17 SET NAMES utf8mb4;
... ... @@ -49,7 +49,7 @@ CREATE TABLE `infra_api_access_log` (
49 49 `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
50 50 PRIMARY KEY (`id`) USING BTREE,
51 51 INDEX `idx_create_time`(`create_time` ASC) USING BTREE
52   -) ENGINE = InnoDB AUTO_INCREMENT = 36333 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'API 访问日志表';
  52 +) ENGINE = InnoDB AUTO_INCREMENT = 36301 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'API 访问日志表';
53 53  
54 54 -- ----------------------------
55 55 -- Records of infra_api_access_log
... ... @@ -92,7 +92,7 @@ CREATE TABLE `infra_api_error_log` (
92 92 `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
93 93 PRIMARY KEY (`id`) USING BTREE,
94 94 INDEX `idx_create_time`(`create_time` ASC) USING BTREE
95   -) ENGINE = InnoDB AUTO_INCREMENT = 24233 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统异常日志';
  95 +) ENGINE = InnoDB AUTO_INCREMENT = 23958 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统异常日志';
96 96  
97 97 -- ----------------------------
98 98 -- Records of infra_api_error_log
... ... @@ -253,7 +253,7 @@ CREATE TABLE `infra_file` (
253 253 `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
254 254 `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
255 255 PRIMARY KEY (`id`) USING BTREE
256   -) ENGINE = InnoDB AUTO_INCREMENT = 2260 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '文件表';
  256 +) ENGINE = InnoDB AUTO_INCREMENT = 2233 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '文件表';
257 257  
258 258 -- ----------------------------
259 259 -- Records of infra_file
... ... @@ -462,7 +462,7 @@ CREATE TABLE `system_dict_data` (
462 462 `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
463 463 `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
464 464 PRIMARY KEY (`id`) USING BTREE
465   -) ENGINE = InnoDB AUTO_INCREMENT = 1061137 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典数据表';
  465 +) ENGINE = InnoDB AUTO_INCREMENT = 1061122 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典数据表';
466 466  
467 467 -- ----------------------------
468 468 -- Records of system_dict_data
... ... @@ -1374,14 +1374,14 @@ INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `st
1374 1374 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3443, 1, '草稿', '0', 'mes_wm_product_produce_status', 0, 'info', '', '草稿状态', '1', '2026-04-05 15:53:46', '1', '2026-04-05 15:53:46', b'0');
1375 1375 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3444, 2, '已完成', '4', 'mes_wm_product_produce_status', 0, 'success', '', '已完成状态', '1', '2026-04-05 15:53:46', '1', '2026-04-05 15:53:46', b'0');
1376 1376 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3445, 3, '已取消', '5', 'mes_wm_product_produce_status', 0, 'danger', '', '已取消状态', '1', '2026-04-05 15:53:46', '1', '2026-04-05 15:53:46', b'0');
1377   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3446, 0, '草稿', '0', 'mes_pro_task_status', 0, 'default', '', 'PREPARE', '1', '2026-04-16 09:47:00', '1', '2026-06-13 10:32:58', b'0');
1378   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3447, 1, '已完成', '4', 'mes_pro_task_status', 0, 'success', '', 'FINISHED', '1', '2026-04-16 09:47:00', '1', '2026-06-13 10:32:58', b'0');
1379   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3448, 2, '已取消', '5', 'mes_pro_task_status', 0, 'danger', '', 'CANCELED', '1', '2026-04-16 09:47:00', '1', '2026-06-13 10:32:58', b'0');
1380   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3525, 1, '正常', '0', 'im_message_status', 0, 'success', '', '正常(初始状态)', 'admin', '2026-04-30 15:14:36', 'admin', '2026-06-16 03:41:55', b'0');
1381   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3526, 2, '已撤回', '2', 'im_message_status', 0, 'danger', '', '已撤回', 'admin', '2026-04-30 15:14:36', 'admin', '2026-06-16 03:41:55', b'0');
1382   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3530, 1, '不需要回执', '0', 'im_message_receipt_status', 0, 'info', '', 'NO_RECEIPT', 'admin', '2026-04-30 11:35:07', 'admin', '2026-06-14 00:32:47', b'0');
1383   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3531, 2, '待完成', '1', 'im_message_receipt_status', 0, 'warning', '', 'PENDING', 'admin', '2026-04-30 11:35:07', 'admin', '2026-06-14 00:32:47', b'0');
1384   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3532, 3, '已完成', '2', 'im_message_receipt_status', 0, 'success', '', 'DONE', 'admin', '2026-04-30 11:35:07', 'admin', '2026-06-14 00:32:47', b'0');
  1377 +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3446, 0, '草稿', '0', 'mes_pro_task_status', 0, '', '', NULL, '1', '2026-04-16 09:47:00', '1', '2026-04-16 09:47:00', b'0');
  1378 +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3447, 1, '已完成', '4', 'mes_pro_task_status', 0, '', '', NULL, '1', '2026-04-16 09:47:00', '1', '2026-04-16 09:47:00', b'0');
  1379 +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3448, 2, '已取消', '5', 'mes_pro_task_status', 0, '', '', NULL, '1', '2026-04-16 09:47:00', '1', '2026-04-16 09:47:00', b'0');
  1380 +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3525, 1, '正常', '0', 'im_message_status', 0, 'success', '', '正常(初始状态)', 'admin', '2026-04-30 15:14:36', 'admin', '2026-04-30 15:14:36', b'0');
  1381 +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3526, 2, '已撤回', '2', 'im_message_status', 0, 'danger', '', '已撤回', 'admin', '2026-04-30 15:14:36', 'admin', '2026-04-30 15:14:36', b'0');
  1382 +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3530, 1, '不需要回执', '0', 'im_message_receipt_status', 0, 'info', '', 'NO_RECEIPT', 'admin', '2026-04-30 11:35:07', 'admin', '2026-04-30 11:35:07', b'0');
  1383 +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3531, 2, '待完成', '1', 'im_message_receipt_status', 0, 'warning', '', 'PENDING', 'admin', '2026-04-30 11:35:07', 'admin', '2026-04-30 11:35:07', b'0');
  1384 +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3532, 3, '已完成', '2', 'im_message_receipt_status', 0, 'success', '', 'DONE', 'admin', '2026-04-30 11:35:07', 'admin', '2026-04-30 11:35:07', b'0');
1385 1385 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3540, 1, '正常', '0', 'im_friend_status', 0, 'success', '', '正常好友关系', 'admin', '2026-04-30 11:35:07', 'admin', '2026-04-30 11:35:07', b'0');
1386 1386 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3541, 2, '已删除', '1', 'im_friend_status', 0, 'danger', '', '已删除好友关系', 'admin', '2026-04-30 11:35:07', 'admin', '2026-04-30 11:35:07', b'0');
1387 1387 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3550, 1, '正常', '0', 'im_group_status', 0, 'success', '', '群正常', 'admin', '2026-04-30 11:35:07', 'admin', '2026-04-30 11:35:07', b'0');
... ... @@ -1396,39 +1396,39 @@ INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `st
1396 1396 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3559, 1, '等待验证', '0', 'im_friend_request_handle_result', 0, 'warning', '', NULL, '1', '2026-05-04 02:43:41', '1', '2026-05-04 02:43:41', b'0');
1397 1397 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3560, 2, '已添加', '1', 'im_friend_request_handle_result', 0, 'success', '', NULL, '1', '2026-05-04 02:43:41', '1', '2026-05-04 02:43:41', b'0');
1398 1398 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3561, 3, '已拒绝', '2', 'im_friend_request_handle_result', 0, 'info', '', NULL, '1', '2026-05-04 02:43:41', '1', '2026-05-04 02:43:41', b'0');
1399   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3562, 101, '文本', '101', 'im_content_type', 0, '', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-06-16 03:41:55', b'0');
1400   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3563, 102, '图片', '102', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-06-16 03:41:55', b'0');
1401   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3564, 103, '语音', '103', 'im_content_type', 0, 'warning', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-06-16 03:41:55', b'0');
1402   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3565, 104, '视频', '104', 'im_content_type', 0, 'warning', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-06-16 03:41:55', b'0');
1403   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3566, 105, '文件', '105', 'im_content_type', 0, 'info', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-06-16 03:41:55', b'0');
1404   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3568, 2101, '撤回', '2101', 'im_content_type', 0, 'danger', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-06-16 03:41:55', b'0');
1405   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3569, 2200, '回执', '2200', 'im_content_type', 0, 'warning', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-06-16 03:41:55', b'0');
1406   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3570, 2201, '已读', '2201', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-06-16 03:41:55', b'0');
1407   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3571, 1501, '群创建', '1501', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-06-16 03:41:55', b'0');
1408   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3572, 1502, '群信息变更', '1502', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-06-16 03:41:55', b'0');
1409   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3573, 1503, '收到新的入群申请', '1503', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-06-16 03:47:00', b'0');
1410   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3574, 1504, '成员退群', '1504', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-06-16 03:41:55', b'0');
1411   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3575, 1505, '入群申请被同意', '1505', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-06-16 03:47:00', b'0');
1412   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3576, 1506, '入群申请被拒绝', '1506', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-06-16 03:47:00', b'0');
1413   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3577, 1507, '群主转让', '1507', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-06-16 03:41:55', b'0');
1414   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3578, 1508, '成员被移出', '1508', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-06-16 03:41:55', b'0');
1415   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3579, 1509, '成员加入', '1509', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-06-16 03:41:55', b'0');
1416   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3580, 1510, '自由进群', '1510', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-06-16 03:41:55', b'0');
1417   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3581, 1511, '群解散', '1511', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-06-16 03:41:55', b'0');
1418   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3582, 1512, '成员禁言', '1512', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-06-16 03:41:55', b'0');
1419   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3583, 1513, '成员取消禁言', '1513', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-06-16 03:41:55', b'0');
1420   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3584, 1514, '全群禁言', '1514', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-06-16 03:41:55', b'0');
1421   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3585, 1515, '全群取消禁言', '1515', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-06-16 03:41:55', b'0');
1422   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3586, 1516, '成员昵称变更', '1516', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-06-16 03:41:55', b'0');
1423   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3587, 1517, '添加管理员', '1517', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-06-16 03:41:55', b'0');
1424   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3588, 1518, '撤销管理员', '1518', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-06-16 03:41:55', b'0');
1425   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3589, 1519, '群公告变更', '1519', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-06-16 03:41:55', b'0');
1426   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3590, 1520, '群名变更', '1520', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-06-16 03:41:55', b'0');
1427   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3591, 1531, '群消息置顶', '1531', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-06-16 03:41:55', b'0');
1428   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3592, 1532, '群消息取消置顶', '1532', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-06-16 03:41:55', b'0');
1429   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3593, 1533, '群封禁变更', '1533', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-06-16 03:41:55', b'0');
1430   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3594, 1204, '新增好友', '1204', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-05-05 13:26:53', 'admin', '2026-06-16 03:41:55', b'0');
1431   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3595, 1205, '好友被删除', '1205', 'im_content_type', 0, 'warning', '', NULL, 'admin', '2026-05-05 13:26:53', 'admin', '2026-06-16 03:41:55', b'0');
  1399 +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3562, 101, '文本', '101', 'im_content_type', 0, '', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-05-05 11:52:30', b'0');
  1400 +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3563, 102, '图片', '102', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-05-05 11:52:30', b'0');
  1401 +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3564, 103, '语音', '103', 'im_content_type', 0, 'warning', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-05-05 11:52:30', b'0');
  1402 +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3565, 104, '视频', '104', 'im_content_type', 0, 'warning', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-05-05 11:52:30', b'0');
  1403 +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3566, 105, '文件', '105', 'im_content_type', 0, 'info', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-05-05 11:52:30', b'0');
  1404 +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3568, 2101, '撤回', '2101', 'im_content_type', 0, 'danger', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-05-05 11:52:30', b'0');
  1405 +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3569, 2200, '回执', '2200', 'im_content_type', 0, 'warning', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-05-05 11:52:30', b'0');
  1406 +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3570, 2201, '已读', '2201', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-05-05 11:52:30', b'0');
  1407 +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3571, 1501, '群创建', '1501', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-05-05 11:52:30', b'0');
  1408 +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3572, 1502, '群信息变更', '1502', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-05-05 11:52:30', b'0');
  1409 +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3573, 1503, '入群申请', '1503', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-05-05 11:52:30', b'0');
  1410 +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3574, 1504, '成员退群', '1504', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-05-05 11:52:30', b'0');
  1411 +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3575, 1505, '入群申请通过', '1505', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-05-05 11:52:30', b'0');
  1412 +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3576, 1506, '入群申请拒绝', '1506', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-05-05 11:52:30', b'0');
  1413 +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3577, 1507, '群主转让', '1507', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-05-05 11:52:30', b'0');
  1414 +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3578, 1508, '成员被移出', '1508', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-05-05 11:52:30', b'0');
  1415 +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3579, 1509, '成员加入', '1509', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-05-05 11:52:30', b'0');
  1416 +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3580, 1510, '自由进群', '1510', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-05-05 11:52:30', b'0');
  1417 +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3581, 1511, '群解散', '1511', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-05-05 11:52:30', b'0');
  1418 +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3582, 1512, '成员禁言', '1512', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-05-05 11:52:30', b'0');
  1419 +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3583, 1513, '成员取消禁言', '1513', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-05-05 11:52:30', b'0');
  1420 +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3584, 1514, '全群禁言', '1514', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-05-05 11:52:30', b'0');
  1421 +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3585, 1515, '全群取消禁言', '1515', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-05-05 11:52:30', b'0');
  1422 +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3586, 1516, '成员昵称变更', '1516', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-05-05 11:52:30', b'0');
  1423 +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3587, 1517, '添加管理员', '1517', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-05-05 11:52:30', b'0');
  1424 +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3588, 1518, '撤销管理员', '1518', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-05-05 11:52:30', b'0');
  1425 +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3589, 1519, '群公告变更', '1519', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-05-05 11:52:30', b'0');
  1426 +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3590, 1520, '群名变更', '1520', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-05-05 11:52:30', b'0');
  1427 +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3591, 1531, '群消息置顶', '1531', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-05-05 11:52:30', b'0');
  1428 +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3592, 1532, '群消息取消置顶', '1532', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-05-05 11:52:30', b'0');
  1429 +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3593, 1533, '群封禁变更', '1533', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-05-05 11:52:30', 'admin', '2026-05-05 11:52:30', b'0');
  1430 +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3594, 1204, '新增好友', '1204', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-05-05 13:26:53', 'admin', '2026-05-05 13:26:53', b'0');
  1431 +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3595, 1205, '好友被删除', '1205', 'im_content_type', 0, 'warning', '', NULL, 'admin', '2026-05-05 13:26:53', 'admin', '2026-05-05 13:26:53', b'0');
1432 1432 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3596, 1, '搜索', '1', 'im_group_add_source', 0, '', '', NULL, '', '2026-05-06 09:26:36', '', '2026-05-06 09:26:36', b'0');
1433 1433 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3597, 2, '邀请', '2', 'im_group_add_source', 0, '', '', NULL, '', '2026-05-06 09:26:36', '', '2026-05-06 09:26:36', b'0');
1434 1434 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3598, 3, '扫码', '3', 'im_group_add_source', 0, '', '', NULL, '', '2026-05-06 09:26:36', '', '2026-05-06 09:26:36', b'0');
... ... @@ -1474,26 +1474,11 @@ INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `st
1474 1474 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061114, 3, '已拒绝', '30', 'im_rtc_participant_status', 0, 'warning', '', '接通前点拒接', 'admin', '2026-05-18 03:36:12', 'admin', '2026-05-18 03:36:12', b'0');
1475 1475 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061115, 4, '未应答', '40', 'im_rtc_participant_status', 0, 'info', '', '通话已结束仍未应答', 'admin', '2026-05-18 03:36:12', 'admin', '2026-05-18 03:36:12', b'0');
1476 1476 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061116, 5, '已离开', '50', 'im_rtc_participant_status', 0, 'success', '', '接通后挂断 / 离开', 'admin', '2026-05-18 03:36:12', 'admin', '2026-05-18 03:36:12', b'0');
1477   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061117, 1610, '通话开始', '1610', 'im_content_type', 0, 'info', '', '入消息流;私聊定向通知,群聊全员广播', 'admin', '2026-05-18 03:36:12', 'admin', '2026-06-16 03:41:55', b'0');
1478   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061118, 1611, '通话结束', '1611', 'im_content_type', 0, 'info', '', '入消息流;私聊准气泡,群聊系统 tip', 'admin', '2026-05-18 03:36:12', 'admin', '2026-06-16 03:41:55', b'0');
1479   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061119, 125, '素材', '125', 'im_content_type', 0, 'success', '', '频道运营推送的图文卡片消息', '1', '2026-05-18 13:14:34', 'admin', '2026-06-16 03:41:55', b'0');
  1477 +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061117, 1610, '通话开始', '1610', 'im_content_type', 0, 'info', '', '入消息流;私聊定向通知,群聊全员广播', 'admin', '2026-05-18 03:36:12', 'admin', '2026-05-18 03:36:12', b'0');
  1478 +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061118, 1611, '通话结束', '1611', 'im_content_type', 0, 'info', '', '入消息流;私聊准气泡,群聊系统 tip', 'admin', '2026-05-18 03:36:12', 'admin', '2026-05-18 03:36:12', b'0');
  1479 +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061119, 125, '素材', '125', 'im_content_type', 0, 'success', '', '频道运营推送的图文卡片消息', '1', '2026-05-18 13:14:34', '1', '2026-05-18 13:14:34', b'0');
1480 1480 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061120, 1, '富文本', '1', 'im_channel_material_type', 0, 'primary', '', '', '1', '2026-05-19 14:09:25', '1', '2026-05-19 14:09:25', b'0');
1481 1481 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061121, 2, '外链', '2', 'im_channel_material_type', 0, 'info', '', '', '1', '2026-05-19 14:09:25', '1', '2026-05-19 14:09:25', b'0');
1482   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061122, 7, 'SN 码', '109', 'mes_wm_barcode_biz_type', 0, 'default', '', 'SN', '1', '2026-06-13 10:30:30', '1', '2026-06-13 10:30:30', b'0');
1483   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061123, 107, '合并转发', '107', 'im_content_type', 0, '', '', NULL, 'admin', '2026-06-16 03:47:00', 'admin', '2026-06-16 03:47:00', b'0');
1484   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061124, 108, '名片', '108', 'im_content_type', 0, '', '', NULL, 'admin', '2026-06-16 03:47:00', 'admin', '2026-06-16 03:47:00', b'0');
1485   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061125, 115, '表情', '115', 'im_content_type', 0, '', '', NULL, 'admin', '2026-06-16 03:47:00', 'admin', '2026-06-16 03:47:00', b'0');
1486   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061126, 1201, '好友申请被同意', '1201', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-06-16 03:47:00', 'admin', '2026-06-16 03:47:00', b'0');
1487   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061127, 1202, '好友申请被拒绝', '1202', 'im_content_type', 0, 'warning', '', NULL, 'admin', '2026-06-16 03:47:00', 'admin', '2026-06-16 03:47:00', b'0');
1488   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061128, 1203, '收到新的好友申请', '1203', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-06-16 03:47:00', 'admin', '2026-06-16 03:47:00', b'0');
1489   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061129, 1207, '加入黑名单', '1207', 'im_content_type', 0, 'danger', '', NULL, 'admin', '2026-06-16 03:47:00', 'admin', '2026-06-16 03:47:00', b'0');
1490   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061130, 1208, '移出黑名单', '1208', 'im_content_type', 0, 'success', '', NULL, 'admin', '2026-06-16 03:47:00', 'admin', '2026-06-16 03:47:00', b'0');
1491   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061131, 1209, '好友资料变更', '1209', 'im_content_type', 0, 'info', '', NULL, 'admin', '2026-06-16 03:47:00', 'admin', '2026-06-16 03:47:00', b'0');
1492   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061132, 1210, '好友信息批量更新', '1210', 'im_content_type', 0, 'info', '', NULL, 'admin', '2026-06-16 03:47:00', 'admin', '2026-06-16 03:47:00', b'0');
1493   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061133, 1530, '群成员个人设置变更', '1530', 'im_content_type', 0, 'info', '', NULL, 'admin', '2026-06-16 03:47:00', 'admin', '2026-06-16 03:47:00', b'0');
1494   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061134, 1601, '通话信令', '1601', 'im_content_type', 0, 'info', '', NULL, 'admin', '2026-06-16 03:47:00', 'admin', '2026-06-16 03:47:00', b'0');
1495   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061135, 1602, '通话参与者加入', '1602', 'im_content_type', 0, 'info', '', NULL, 'admin', '2026-06-16 03:47:00', 'admin', '2026-06-16 03:47:00', b'0');
1496   -INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1061136, 1603, '通话参与者离开', '1603', 'im_content_type', 0, 'info', '', NULL, 'admin', '2026-06-16 03:47:00', 'admin', '2026-06-16 03:47:00', b'0');
1497 1482 COMMIT;
1498 1483  
1499 1484 -- ----------------------------
... ... @@ -1651,7 +1636,7 @@ INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creat
1651 1636 INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (2029, 'MES 工单类型', 'mes_pro_work_order_type', 0, 'MES 生产管理 - 工单类型(1=自行生产,2=代工,3=采购)', '1', '2026-02-17 11:43:47', '1', '2026-02-17 11:43:47', b'0', NULL);
1652 1637 INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (2036, 'MES 工序关系类型', 'mes_pro_link_type', 0, '工艺路线中工序之间的关系类型', '1', '2026-02-19 04:24:53', '1', '2026-04-05 15:05:07', b'0', NULL);
1653 1638 INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (2037, 'MES 时间单位', 'mes_time_unit_type', 0, '生产时间的计量单位', '1', '2026-02-19 04:24:53', '1', '2026-04-05 15:04:57', b'0', NULL);
1654   -INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (2038, 'MES 生产任务状态', 'mes_pro_task_status', 0, 'MES 生产管理 - 任务状态(0=草稿,4=已完成,5=已取消)', '1', '2026-02-19 15:25:27', '1', '2026-06-13 10:32:58', b'0', NULL);
  1639 +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (2038, 'MES 生产任务状态', 'mes_pro_task_status', 0, 'MES 生产管理 - 任务状态(0=草稿,1=进行中,2=暂停,3=已完成,4=已取消)', '1', '2026-02-19 15:25:27', '1', '2026-02-19 15:25:27', b'0', NULL);
1655 1640 INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (2039, 'MES 点检保养项目类型', 'mes_dv_subject_type', 0, 'MES 设备管理 - 点检保养项目类型(1=设备点检,2=设备保养)', '1', '2026-02-20 01:42:58', '1', '2026-02-20 01:42:58', b'0', NULL);
1656 1641 INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (2040, 'MES 保养记录状态', 'mes_mainten_record_status', 0, NULL, 'admin', '2026-02-20 02:59:55', 'admin', '2026-02-20 02:59:55', b'0', NULL);
1657 1642 INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (2041, 'MES 保养结果', 'mes_mainten_status', 0, NULL, 'admin', '2026-02-20 02:59:55', 'admin', '2026-02-20 02:59:55', b'0', NULL);
... ... @@ -1704,11 +1689,11 @@ INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creat
1704 1689 INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (2133, 'MES 销售退货单状态', 'mes_wm_return_sales_status', 0, 'MES 销售退货单状态枚举', '1', '2026-04-03 17:20:25', '1', '2026-04-05 15:05:39', b'0', NULL);
1705 1690 INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (2135, 'MES 上下工状态类型', 'mes_pro_work_record_type', 0, 'MES 上下工状态类型', '1', '2026-04-05 14:07:27', '1', '2026-04-05 14:07:27', b'0', NULL);
1706 1691 INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (2138, 'MES 生产入库单状态', 'mes_wm_product_produce_status', 0, 'MES 生产入库单状态', '1', '2026-04-05 15:53:46', '1', '2026-04-05 15:53:46', b'0', '1970-01-01 00:00:00');
1707   -INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (2200, 'IM 内容类型', 'im_content_type', 0, '对应 ImContentTypeEnum', 'admin', '2026-04-30 11:35:07', 'admin', '2026-06-16 03:41:55', b'0', NULL);
1708   -INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (2202, 'IM 消息回执状态', 'im_message_receipt_status', 0, '对应 ImMessageReceiptStatusEnum', 'admin', '2026-04-30 11:35:07', 'admin', '2026-06-16 03:41:55', b'0', NULL);
  1692 +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (2200, 'IM 内容类型', 'im_content_type', 0, '对应 ImContentTypeEnum', 'admin', '2026-04-30 11:35:07', 'admin', '2026-04-30 11:35:07', b'0', NULL);
  1693 +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (2202, 'IM 消息回执状态', 'im_message_receipt_status', 0, '对应 ImMessageReceiptStatusEnum', 'admin', '2026-04-30 11:35:07', 'admin', '2026-04-30 11:35:07', b'0', NULL);
1709 1694 INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (2203, 'IM 好友状态', 'im_friend_status', 0, '0=正常 / 1=已删除', 'admin', '2026-04-30 11:35:07', 'admin', '2026-04-30 11:35:07', b'0', NULL);
1710 1695 INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (2204, 'IM 群状态', 'im_group_status', 0, '0=正常 / 1=已解散', 'admin', '2026-04-30 11:35:07', 'admin', '2026-04-30 11:35:07', b'0', NULL);
1711   -INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (2205, 'IM 消息状态', 'im_message_status', 0, '对应 ImMessageStatusEnum;0=正常 / 2=已撤回(私聊 / 群聊共用)', 'admin', '2026-04-30 15:14:36', 'admin', '2026-06-16 03:41:55', b'0', NULL);
  1696 +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (2205, 'IM 消息状态', 'im_message_status', 0, '对应 ImMessageStatusEnum;0=正常 / 2=已撤回(私聊 / 群聊共用)', 'admin', '2026-04-30 15:14:36', 'admin', '2026-04-30 15:14:36', b'0', NULL);
1712 1697 INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (2206, 'IM 群成员角色', 'im_group_member_role', 0, NULL, '1', '2026-05-02 02:14:12', '1', '2026-05-02 02:14:12', b'0', NULL);
1713 1698 INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (2207, 'IM 好友添加来源', 'im_friend_add_source', 0, NULL, '1', '2026-05-04 02:43:41', '1', '2026-05-04 02:43:41', b'0', NULL);
1714 1699 INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (2208, 'IM 好友申请处理结果', 'im_friend_request_handle_result', 0, NULL, '1', '2026-05-04 02:43:41', '1', '2026-05-04 02:43:41', b'0', NULL);
... ... @@ -1751,7 +1736,7 @@ CREATE TABLE `system_login_log` (
1751 1736 PRIMARY KEY (`id`) USING BTREE,
1752 1737 INDEX `idx_username`(`username` ASC) USING BTREE,
1753 1738 INDEX `idx_create_time`(`create_time` ASC) USING BTREE
1754   -) ENGINE = InnoDB AUTO_INCREMENT = 5226 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统访问记录';
  1739 +) ENGINE = InnoDB AUTO_INCREMENT = 4697 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统访问记录';
1755 1740  
1756 1741 -- ----------------------------
1757 1742 -- Records of system_login_log
... ... @@ -2229,7 +2214,7 @@ INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_i
2229 2214 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2156, '查询项目', 'report:go-view-project:query', 3, 0, 2153, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2023-02-07 19:25:53', '1', '2023-02-07 19:25:53', b'0');
2230 2215 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2157, '使用 SQL 查询数据', 'report:go-view-data:get-by-sql', 3, 3, 2153, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2023-02-07 19:26:15', '1', '2023-02-07 19:26:15', b'0');
2231 2216 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2158, '使用 HTTP 查询数据', 'report:go-view-data:get-by-http', 3, 4, 2153, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2023-02-07 19:26:35', '1', '2023-02-07 19:26:35', b'0');
2232   -INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2159, 'Boot 开发文档', '', 1, 1, 0, 'https://doc.iocoder.cn/', 'ep:document', NULL, NULL, 0, b'1', b'1', b'1', '1', '2023-02-10 22:46:28', '1', '2026-06-12 00:05:19', b'0');
  2217 +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2159, 'Boot 开发文档', '', 1, 1, 0, 'https://doc.iocoder.cn/', 'ep:document', NULL, NULL, 0, b'1', b'1', b'1', '1', '2023-02-10 22:46:28', '1', '2026-01-05 19:31:07', b'0');
2233 2218 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2160, 'Cloud 开发文档', '', 1, 2, 0, 'https://cloud.iocoder.cn', 'ep:document-copy', NULL, NULL, 0, b'1', b'1', b'1', '1', '2023-02-10 22:47:07', '1', '2023-12-02 21:32:29', b'0');
2234 2219 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2161, '接入示例', '', 1, 99, 1117, 'demo', 'fa-solid:dragon', 'pay/demo/index', NULL, 0, b'1', b'1', b'1', '', '2023-02-11 14:21:42', '1', '2024-01-18 23:50:00', b'0');
2235 2220 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2162, '商品导出', 'product:spu:export', 3, 5, 2014, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-07-30 14:22:58', '', '2022-07-30 14:22:58', b'0');
... ... @@ -3453,7 +3438,7 @@ CREATE TABLE `system_oauth2_access_token` (
3453 3438 PRIMARY KEY (`id`) USING BTREE,
3454 3439 INDEX `idx_access_token`(`access_token` ASC) USING BTREE,
3455 3440 INDEX `idx_refresh_token`(`refresh_token` ASC) USING BTREE
3456   -) ENGINE = InnoDB AUTO_INCREMENT = 107056 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 访问令牌';
  3441 +) ENGINE = InnoDB AUTO_INCREMENT = 57395 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 访问令牌';
3457 3442  
3458 3443 -- ----------------------------
3459 3444 -- Records of system_oauth2_access_token
... ... @@ -3526,7 +3511,7 @@ BEGIN;
3526 3511 INSERT INTO `system_oauth2_client` (`id`, `client_id`, `secret`, `name`, `logo`, `description`, `status`, `access_token_validity_seconds`, `refresh_token_validity_seconds`, `redirect_uris`, `authorized_grant_types`, `scopes`, `auto_approve_scopes`, `authorities`, `resource_ids`, `additional_information`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1, 'default', 'admin123', '芋道源码', 'http://test.yudao.iocoder.cn/20250502/sort2_1746189740718.png', '我是描述', 0, 1800, 2592000, '[\"https://www.iocoder.cn\",\"https://doc.iocoder.cn\"]', '[\"password\",\"authorization_code\",\"implicit\",\"refresh_token\",\"client_credentials\"]', '[\"user.read\",\"user.write\"]', '[]', '[\"user.read\",\"user.write\"]', '[]', '{}', '1', '2022-05-11 21:47:12', '1', '2025-12-07 20:07:09', b'0');
3527 3512 INSERT INTO `system_oauth2_client` (`id`, `client_id`, `secret`, `name`, `logo`, `description`, `status`, `access_token_validity_seconds`, `refresh_token_validity_seconds`, `redirect_uris`, `authorized_grant_types`, `scopes`, `auto_approve_scopes`, `authorities`, `resource_ids`, `additional_information`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (40, 'test', 'test2', 'biubiu', 'http://test.yudao.iocoder.cn/20251227/javayuanma_1766829882970.jpg', '啦啦啦啦', 0, 1800, 43200, '[\"https://www.iocoder.cn\"]', '[\"password\",\"authorization_code\",\"implicit\"]', '[\"user_info\",\"projects\"]', '[\"user_info\"]', '[]', '[]', '{}', '1', '2022-05-12 00:28:20', '1', '2025-12-27 18:04:44', b'0');
3528 3513 INSERT INTO `system_oauth2_client` (`id`, `client_id`, `secret`, `name`, `logo`, `description`, `status`, `access_token_validity_seconds`, `refresh_token_validity_seconds`, `redirect_uris`, `authorized_grant_types`, `scopes`, `auto_approve_scopes`, `authorities`, `resource_ids`, `additional_information`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (41, 'yudao-sso-demo-by-code', 'test', '基于授权码模式,如何实现 SSO 单点登录?', 'http://test.yudao.iocoder.cn/it/20250502/sign_1746181948685.png', NULL, 0, 1800, 43200, '[\"http://127.0.0.1:18080\"]', '[\"authorization_code\",\"refresh_token\"]', '[\"user.read\",\"user.write\"]', '[]', '[]', '[]', NULL, '1', '2022-09-29 13:28:31', '1', '2025-05-02 18:32:30', b'0');
3529   -INSERT INTO `system_oauth2_client` (`id`, `client_id`, `secret`, `name`, `logo`, `description`, `status`, `access_token_validity_seconds`, `refresh_token_validity_seconds`, `redirect_uris`, `authorized_grant_types`, `scopes`, `auto_approve_scopes`, `authorities`, `resource_ids`, `additional_information`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (42, 'yudao-sso-demo-by-password', 'test', '基于密码模式,如何实现 SSO 单点登录?', 'http://test.yudao.iocoder.cn/oauth2/20260615/iShot_2026-05-31_23.18.10.png', NULL, 0, 1800, 43200, '[\"http://127.0.0.1:18080\"]', '[\"password\",\"refresh_token\"]', '[\"user.read\",\"user.write\"]', '[]', '[]', '[]', NULL, '1', '2022-10-04 17:40:16', '1', '2026-06-15 18:58:30', b'0');
  3514 +INSERT INTO `system_oauth2_client` (`id`, `client_id`, `secret`, `name`, `logo`, `description`, `status`, `access_token_validity_seconds`, `refresh_token_validity_seconds`, `redirect_uris`, `authorized_grant_types`, `scopes`, `auto_approve_scopes`, `authorities`, `resource_ids`, `additional_information`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (42, 'yudao-sso-demo-by-password', 'test', '基于密码模式,如何实现 SSO 单点登录?', 'http://test.yudao.iocoder.cn/20251025/images (3)_1761360515810.jpeg', NULL, 0, 1800, 43200, '[\"http://127.0.0.1:18080\"]', '[\"password\",\"refresh_token\"]', '[\"user.read\",\"user.write\"]', '[]', '[]', '[]', NULL, '1', '2022-10-04 17:40:16', '1', '2025-10-25 10:49:40', b'0');
3530 3515 COMMIT;
3531 3516  
3532 3517 -- ----------------------------
... ... @@ -3579,7 +3564,7 @@ CREATE TABLE `system_oauth2_refresh_token` (
3579 3564 `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
3580 3565 PRIMARY KEY (`id`) USING BTREE,
3581 3566 INDEX `idx_refresh_token`(`refresh_token` ASC) USING BTREE
3582   -) ENGINE = InnoDB AUTO_INCREMENT = 3244 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 刷新令牌';
  3567 +) ENGINE = InnoDB AUTO_INCREMENT = 2728 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 刷新令牌';
3583 3568  
3584 3569 -- ----------------------------
3585 3570 -- Records of system_oauth2_refresh_token
... ... @@ -3615,7 +3600,7 @@ CREATE TABLE `system_operate_log` (
3615 3600 PRIMARY KEY (`id`) USING BTREE,
3616 3601 INDEX `idx_user_id`(`user_id` ASC) USING BTREE,
3617 3602 INDEX `idx_create_time`(`create_time` ASC) USING BTREE
3618   -) ENGINE = InnoDB AUTO_INCREMENT = 9207 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '操作日志记录 V2 版本';
  3603 +) ENGINE = InnoDB AUTO_INCREMENT = 9195 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '操作日志记录 V2 版本';
3619 3604  
3620 3605 -- ----------------------------
3621 3606 -- Records of system_operate_log
... ... @@ -3641,7 +3626,7 @@ CREATE TABLE `system_post` (
3641 3626 `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
3642 3627 `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
3643 3628 PRIMARY KEY (`id`) USING BTREE
3644   -) ENGINE = InnoDB AUTO_INCREMENT = 9 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '岗位信息表';
  3629 +) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '岗位信息表';
3645 3630  
3646 3631 -- ----------------------------
3647 3632 -- Records of system_post
... ... @@ -3685,7 +3670,7 @@ INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_sco
3685 3670 INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3, 'CRM 管理员', 'crm_admin', 2, 1, '', 0, 1, 'CRM 专属角色', '1', '2024-02-24 10:51:13', '1', '2024-02-24 02:51:32', b'0', 1);
3686 3671 INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (109, '租户管理员', 'tenant_admin', 0, 1, '', 0, 1, '系统自动生成', '1', '2022-02-22 00:56:14', '1', '2022-02-22 00:56:14', b'0', 121);
3687 3672 INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (111, '租户管理员', 'tenant_admin', 0, 1, '', 0, 1, '系统自动生成', '1', '2022-03-07 21:37:58', '1', '2022-03-07 21:37:58', b'0', 122);
3688   -INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (155, '测试数据权限12', 'test-dp', 4, 2, '[112,100,102,103,104,105,107,108]', 0, 2, '1111', '1', '2025-03-31 14:58:06', '1', '2026-06-07 01:37:53', b'0', 1);
  3673 +INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (155, '测试数据权限1', 'test-dp', 4, 2, '[112,100,102,103,104,105,107,108]', 0, 2, '1111', '1', '2025-03-31 14:58:06', '1', '2025-12-04 23:29:40', b'0', 1);
3689 3674 COMMIT;
3690 3675  
3691 3676 -- ----------------------------
... ... @@ -3704,7 +3689,7 @@ CREATE TABLE `system_role_menu` (
3704 3689 `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
3705 3690 PRIMARY KEY (`id`) USING BTREE,
3706 3691 INDEX `idx_role_id`(`role_id` ASC) USING BTREE
3707   -) ENGINE = InnoDB AUTO_INCREMENT = 6692 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '角色和菜单关联表';
  3692 +) ENGINE = InnoDB AUTO_INCREMENT = 6381 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '角色和菜单关联表';
3708 3693  
3709 3694 -- ----------------------------
3710 3695 -- Records of system_role_menu
... ... @@ -3744,6 +3729,7 @@ INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_t
3744 3729 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (706, 2, 112, '1', '2022-02-22 13:16:57', '1', '2022-02-22 13:16:57', b'0', 1);
3745 3730 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (707, 2, 113, '1', '2022-02-22 13:16:57', '1', '2022-02-22 13:16:57', b'0', 1);
3746 3731 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1296, 110, 1, '110', '2022-02-23 00:23:55', '110', '2022-02-23 00:23:55', b'0', 121);
  3732 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1578, 111, 1, '1', '2022-03-07 21:37:58', '1', '2022-03-07 21:37:58', b'0', 122);
3747 3733 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1729, 109, 100, '1', '2022-09-21 22:08:51', '1', '2022-09-21 22:08:51', b'0', 121);
3748 3734 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1730, 109, 101, '1', '2022-09-21 22:08:51', '1', '2022-09-21 22:08:51', b'0', 121);
3749 3735 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1731, 109, 1063, '1', '2022-09-21 22:08:51', '1', '2022-09-21 22:08:51', b'0', 121);
... ... @@ -3761,6 +3747,23 @@ INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_t
3761 3747 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1743, 109, 1010, '1', '2022-09-21 22:08:51', '1', '2022-09-21 22:08:51', b'0', 121);
3762 3748 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1744, 109, 1011, '1', '2022-09-21 22:08:51', '1', '2022-09-21 22:08:51', b'0', 121);
3763 3749 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1745, 109, 1012, '1', '2022-09-21 22:08:51', '1', '2022-09-21 22:08:51', b'0', 121);
  3750 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1746, 111, 100, '1', '2022-09-21 22:08:52', '1', '2022-09-21 22:08:52', b'0', 122);
  3751 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1747, 111, 101, '1', '2022-09-21 22:08:52', '1', '2022-09-21 22:08:52', b'0', 122);
  3752 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1748, 111, 1063, '1', '2022-09-21 22:08:52', '1', '2022-09-21 22:08:52', b'0', 122);
  3753 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1749, 111, 1064, '1', '2022-09-21 22:08:52', '1', '2022-09-21 22:08:52', b'0', 122);
  3754 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1750, 111, 1001, '1', '2022-09-21 22:08:52', '1', '2022-09-21 22:08:52', b'0', 122);
  3755 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1751, 111, 1065, '1', '2022-09-21 22:08:52', '1', '2022-09-21 22:08:52', b'0', 122);
  3756 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1752, 111, 1002, '1', '2022-09-21 22:08:52', '1', '2022-09-21 22:08:52', b'0', 122);
  3757 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1753, 111, 1003, '1', '2022-09-21 22:08:52', '1', '2022-09-21 22:08:52', b'0', 122);
  3758 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1754, 111, 1004, '1', '2022-09-21 22:08:52', '1', '2022-09-21 22:08:52', b'0', 122);
  3759 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1755, 111, 1005, '1', '2022-09-21 22:08:52', '1', '2022-09-21 22:08:52', b'0', 122);
  3760 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1756, 111, 1006, '1', '2022-09-21 22:08:52', '1', '2022-09-21 22:08:52', b'0', 122);
  3761 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1757, 111, 1007, '1', '2022-09-21 22:08:52', '1', '2022-09-21 22:08:52', b'0', 122);
  3762 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1758, 111, 1008, '1', '2022-09-21 22:08:52', '1', '2022-09-21 22:08:52', b'0', 122);
  3763 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1759, 111, 1009, '1', '2022-09-21 22:08:52', '1', '2022-09-21 22:08:52', b'0', 122);
  3764 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1760, 111, 1010, '1', '2022-09-21 22:08:52', '1', '2022-09-21 22:08:52', b'0', 122);
  3765 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1761, 111, 1011, '1', '2022-09-21 22:08:52', '1', '2022-09-21 22:08:52', b'0', 122);
  3766 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1762, 111, 1012, '1', '2022-09-21 22:08:52', '1', '2022-09-21 22:08:52', b'0', 122);
3764 3767 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1763, 109, 100, '1', '2022-09-21 22:08:53', '1', '2022-09-21 22:08:53', b'0', 121);
3765 3768 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1764, 109, 101, '1', '2022-09-21 22:08:53', '1', '2022-09-21 22:08:53', b'0', 121);
3766 3769 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1765, 109, 1063, '1', '2022-09-21 22:08:53', '1', '2022-09-21 22:08:53', b'0', 121);
... ... @@ -3778,6 +3781,23 @@ INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_t
3778 3781 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1777, 109, 1010, '1', '2022-09-21 22:08:53', '1', '2022-09-21 22:08:53', b'0', 121);
3779 3782 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1778, 109, 1011, '1', '2022-09-21 22:08:53', '1', '2022-09-21 22:08:53', b'0', 121);
3780 3783 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1779, 109, 1012, '1', '2022-09-21 22:08:53', '1', '2022-09-21 22:08:53', b'0', 121);
  3784 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1780, 111, 100, '1', '2022-09-21 22:08:54', '1', '2022-09-21 22:08:54', b'0', 122);
  3785 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1781, 111, 101, '1', '2022-09-21 22:08:54', '1', '2022-09-21 22:08:54', b'0', 122);
  3786 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1782, 111, 1063, '1', '2022-09-21 22:08:54', '1', '2022-09-21 22:08:54', b'0', 122);
  3787 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1783, 111, 1064, '1', '2022-09-21 22:08:54', '1', '2022-09-21 22:08:54', b'0', 122);
  3788 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1784, 111, 1001, '1', '2022-09-21 22:08:54', '1', '2022-09-21 22:08:54', b'0', 122);
  3789 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1785, 111, 1065, '1', '2022-09-21 22:08:54', '1', '2022-09-21 22:08:54', b'0', 122);
  3790 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1786, 111, 1002, '1', '2022-09-21 22:08:54', '1', '2022-09-21 22:08:54', b'0', 122);
  3791 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1787, 111, 1003, '1', '2022-09-21 22:08:54', '1', '2022-09-21 22:08:54', b'0', 122);
  3792 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1788, 111, 1004, '1', '2022-09-21 22:08:54', '1', '2022-09-21 22:08:54', b'0', 122);
  3793 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1789, 111, 1005, '1', '2022-09-21 22:08:54', '1', '2022-09-21 22:08:54', b'0', 122);
  3794 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1790, 111, 1006, '1', '2022-09-21 22:08:54', '1', '2022-09-21 22:08:54', b'0', 122);
  3795 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1791, 111, 1007, '1', '2022-09-21 22:08:54', '1', '2022-09-21 22:08:54', b'0', 122);
  3796 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1792, 111, 1008, '1', '2022-09-21 22:08:54', '1', '2022-09-21 22:08:54', b'0', 122);
  3797 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1793, 111, 1009, '1', '2022-09-21 22:08:54', '1', '2022-09-21 22:08:54', b'0', 122);
  3798 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1794, 111, 1010, '1', '2022-09-21 22:08:54', '1', '2022-09-21 22:08:54', b'0', 122);
  3799 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1795, 111, 1011, '1', '2022-09-21 22:08:54', '1', '2022-09-21 22:08:54', b'0', 122);
  3800 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1796, 111, 1012, '1', '2022-09-21 22:08:54', '1', '2022-09-21 22:08:54', b'0', 122);
3781 3801 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1797, 109, 100, '1', '2022-09-21 22:08:55', '1', '2022-09-21 22:08:55', b'0', 121);
3782 3802 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1798, 109, 101, '1', '2022-09-21 22:08:55', '1', '2022-09-21 22:08:55', b'0', 121);
3783 3803 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1799, 109, 1063, '1', '2022-09-21 22:08:55', '1', '2022-09-21 22:08:55', b'0', 121);
... ... @@ -3795,16 +3815,43 @@ INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_t
3795 3815 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1811, 109, 1010, '1', '2022-09-21 22:08:55', '1', '2022-09-21 22:08:55', b'0', 121);
3796 3816 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1812, 109, 1011, '1', '2022-09-21 22:08:55', '1', '2022-09-21 22:08:55', b'0', 121);
3797 3817 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1813, 109, 1012, '1', '2022-09-21 22:08:55', '1', '2022-09-21 22:08:55', b'0', 121);
  3818 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1814, 111, 100, '1', '2022-09-21 22:08:56', '1', '2022-09-21 22:08:56', b'0', 122);
  3819 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1815, 111, 101, '1', '2022-09-21 22:08:56', '1', '2022-09-21 22:08:56', b'0', 122);
  3820 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1816, 111, 1063, '1', '2022-09-21 22:08:56', '1', '2022-09-21 22:08:56', b'0', 122);
  3821 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1817, 111, 1064, '1', '2022-09-21 22:08:56', '1', '2022-09-21 22:08:56', b'0', 122);
  3822 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1818, 111, 1001, '1', '2022-09-21 22:08:56', '1', '2022-09-21 22:08:56', b'0', 122);
  3823 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1819, 111, 1065, '1', '2022-09-21 22:08:56', '1', '2022-09-21 22:08:56', b'0', 122);
  3824 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1820, 111, 1002, '1', '2022-09-21 22:08:56', '1', '2022-09-21 22:08:56', b'0', 122);
  3825 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1821, 111, 1003, '1', '2022-09-21 22:08:56', '1', '2022-09-21 22:08:56', b'0', 122);
  3826 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1822, 111, 1004, '1', '2022-09-21 22:08:56', '1', '2022-09-21 22:08:56', b'0', 122);
  3827 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1823, 111, 1005, '1', '2022-09-21 22:08:56', '1', '2022-09-21 22:08:56', b'0', 122);
  3828 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1824, 111, 1006, '1', '2022-09-21 22:08:56', '1', '2022-09-21 22:08:56', b'0', 122);
  3829 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1825, 111, 1007, '1', '2022-09-21 22:08:56', '1', '2022-09-21 22:08:56', b'0', 122);
  3830 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1826, 111, 1008, '1', '2022-09-21 22:08:56', '1', '2022-09-21 22:08:56', b'0', 122);
  3831 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1827, 111, 1009, '1', '2022-09-21 22:08:56', '1', '2022-09-21 22:08:56', b'0', 122);
  3832 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1828, 111, 1010, '1', '2022-09-21 22:08:56', '1', '2022-09-21 22:08:56', b'0', 122);
  3833 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1829, 111, 1011, '1', '2022-09-21 22:08:56', '1', '2022-09-21 22:08:56', b'0', 122);
  3834 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1830, 111, 1012, '1', '2022-09-21 22:08:56', '1', '2022-09-21 22:08:56', b'0', 122);
3798 3835 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1831, 109, 103, '1', '2022-09-21 22:43:23', '1', '2022-09-21 22:43:23', b'0', 121);
3799 3836 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1832, 109, 1017, '1', '2022-09-21 22:43:23', '1', '2022-09-21 22:43:23', b'0', 121);
3800 3837 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1833, 109, 1018, '1', '2022-09-21 22:43:23', '1', '2022-09-21 22:43:23', b'0', 121);
3801 3838 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1834, 109, 1019, '1', '2022-09-21 22:43:23', '1', '2022-09-21 22:43:23', b'0', 121);
3802 3839 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1835, 109, 1020, '1', '2022-09-21 22:43:23', '1', '2022-09-21 22:43:23', b'0', 121);
  3840 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1836, 111, 103, '1', '2022-09-21 22:43:24', '1', '2022-09-21 22:43:24', b'0', 122);
  3841 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1837, 111, 1017, '1', '2022-09-21 22:43:24', '1', '2022-09-21 22:43:24', b'0', 122);
  3842 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1838, 111, 1018, '1', '2022-09-21 22:43:24', '1', '2022-09-21 22:43:24', b'0', 122);
  3843 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1839, 111, 1019, '1', '2022-09-21 22:43:24', '1', '2022-09-21 22:43:24', b'0', 122);
  3844 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1840, 111, 1020, '1', '2022-09-21 22:43:24', '1', '2022-09-21 22:43:24', b'0', 122);
3803 3845 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1841, 109, 1036, '1', '2022-09-21 22:48:13', '1', '2022-09-21 22:48:13', b'0', 121);
3804 3846 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1842, 109, 1037, '1', '2022-09-21 22:48:13', '1', '2022-09-21 22:48:13', b'0', 121);
3805 3847 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1843, 109, 1038, '1', '2022-09-21 22:48:13', '1', '2022-09-21 22:48:13', b'0', 121);
3806 3848 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1844, 109, 1039, '1', '2022-09-21 22:48:13', '1', '2022-09-21 22:48:13', b'0', 121);
3807 3849 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1845, 109, 107, '1', '2022-09-21 22:48:13', '1', '2022-09-21 22:48:13', b'0', 121);
  3850 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1846, 111, 1036, '1', '2022-09-21 22:48:13', '1', '2022-09-21 22:48:13', b'0', 122);
  3851 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1847, 111, 1037, '1', '2022-09-21 22:48:13', '1', '2022-09-21 22:48:13', b'0', 122);
  3852 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1848, 111, 1038, '1', '2022-09-21 22:48:13', '1', '2022-09-21 22:48:13', b'0', 122);
  3853 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1849, 111, 1039, '1', '2022-09-21 22:48:13', '1', '2022-09-21 22:48:13', b'0', 122);
  3854 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1850, 111, 107, '1', '2022-09-21 22:48:13', '1', '2022-09-21 22:48:13', b'0', 122);
3808 3855 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1991, 2, 1024, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1);
3809 3856 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1992, 2, 1025, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1);
3810 3857 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1993, 2, 1026, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1);
... ... @@ -3974,6 +4021,18 @@ INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_t
3974 4021 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2938, 109, 1141, '1', '2023-12-02 23:19:40', '1', '2023-12-02 23:19:40', b'0', 121);
3975 4022 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2939, 109, 1142, '1', '2023-12-02 23:19:40', '1', '2023-12-02 23:19:40', b'0', 121);
3976 4023 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2940, 109, 1143, '1', '2023-12-02 23:19:40', '1', '2023-12-02 23:19:40', b'0', 121);
  4024 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2941, 111, 1224, '1', '2023-12-02 23:19:40', '1', '2023-12-02 23:19:40', b'0', 122);
  4025 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2942, 111, 1225, '1', '2023-12-02 23:19:40', '1', '2023-12-02 23:19:40', b'0', 122);
  4026 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2943, 111, 1226, '1', '2023-12-02 23:19:40', '1', '2023-12-02 23:19:40', b'0', 122);
  4027 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2944, 111, 1227, '1', '2023-12-02 23:19:40', '1', '2023-12-02 23:19:40', b'0', 122);
  4028 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2945, 111, 1228, '1', '2023-12-02 23:19:40', '1', '2023-12-02 23:19:40', b'0', 122);
  4029 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2946, 111, 1229, '1', '2023-12-02 23:19:40', '1', '2023-12-02 23:19:40', b'0', 122);
  4030 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2947, 111, 1138, '1', '2023-12-02 23:19:40', '1', '2023-12-02 23:19:40', b'0', 122);
  4031 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2948, 111, 1139, '1', '2023-12-02 23:19:40', '1', '2023-12-02 23:19:40', b'0', 122);
  4032 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2949, 111, 1140, '1', '2023-12-02 23:19:40', '1', '2023-12-02 23:19:40', b'0', 122);
  4033 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2950, 111, 1141, '1', '2023-12-02 23:19:40', '1', '2023-12-02 23:19:40', b'0', 122);
  4034 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2951, 111, 1142, '1', '2023-12-02 23:19:40', '1', '2023-12-02 23:19:40', b'0', 122);
  4035 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2952, 111, 1143, '1', '2023-12-02 23:19:40', '1', '2023-12-02 23:19:40', b'0', 122);
3977 4036 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2993, 109, 2, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 121);
3978 4037 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2994, 109, 1031, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 121);
3979 4038 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2995, 109, 1032, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 121);
... ... @@ -4049,11 +4108,91 @@ INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_t
4049 4108 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3066, 109, 1258, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 121);
4050 4109 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3067, 109, 1259, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 121);
4051 4110 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3068, 109, 1260, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 121);
  4111 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3069, 111, 2, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4112 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3070, 111, 1031, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4113 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3071, 111, 1032, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4114 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3072, 111, 1033, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4115 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3073, 111, 1034, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4116 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3074, 111, 1035, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4117 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3075, 111, 1050, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4118 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3076, 111, 1051, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4119 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3077, 111, 1052, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4120 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3078, 111, 1053, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4121 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3079, 111, 1054, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4122 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3080, 111, 1056, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4123 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3081, 111, 1057, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4124 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3082, 111, 1058, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4125 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3083, 111, 1059, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4126 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3084, 111, 1060, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4127 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3085, 111, 1066, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4128 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3086, 111, 1067, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4129 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3087, 111, 1070, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4130 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3088, 111, 1075, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4131 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3090, 111, 1077, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4132 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3091, 111, 1078, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4133 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3092, 111, 1082, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4134 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3093, 111, 1083, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4135 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3094, 111, 1084, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4136 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3095, 111, 1085, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4137 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3096, 111, 1086, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4138 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3097, 111, 1087, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4139 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3098, 111, 1088, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4140 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3099, 111, 1089, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4141 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3100, 111, 1090, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4142 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3101, 111, 1091, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4143 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3102, 111, 1092, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4144 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3103, 111, 106, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4145 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3104, 111, 110, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4146 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3105, 111, 111, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4147 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3106, 111, 112, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4148 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3107, 111, 113, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4149 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3108, 111, 114, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4150 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3109, 111, 115, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4151 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3110, 111, 116, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4152 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3111, 111, 2472, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4153 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3112, 111, 2478, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4154 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3113, 111, 2479, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4155 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3114, 111, 2480, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4156 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3115, 111, 2481, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4157 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3116, 111, 2482, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4158 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3117, 111, 2483, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4159 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3118, 111, 2484, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4160 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3119, 111, 2485, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4161 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3120, 111, 2486, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4162 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3121, 111, 2487, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4163 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3122, 111, 2488, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4164 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3123, 111, 2489, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4165 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3124, 111, 2490, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4166 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3125, 111, 2491, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4167 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3126, 111, 2492, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4168 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3127, 111, 2493, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4169 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3128, 111, 2494, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4170 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3129, 111, 2495, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4171 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3130, 111, 2497, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4172 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3131, 111, 1237, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4173 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3132, 111, 1238, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4174 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3133, 111, 1239, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4175 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3134, 111, 1240, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4176 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3135, 111, 1241, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4177 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3136, 111, 1242, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4178 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3137, 111, 1243, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4179 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3138, 111, 2525, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4180 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3139, 111, 1255, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4181 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3140, 111, 1256, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4182 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3141, 111, 1257, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4183 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3142, 111, 1258, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4184 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3143, 111, 1259, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
  4185 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3144, 111, 1260, '1', '2023-12-02 23:41:02', '1', '2023-12-02 23:41:02', b'0', 122);
4052 4186 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3221, 109, 102, '1', '2023-12-30 11:42:36', '1', '2023-12-30 11:42:36', b'0', 121);
4053 4187 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3222, 109, 1013, '1', '2023-12-30 11:42:36', '1', '2023-12-30 11:42:36', b'0', 121);
4054 4188 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3223, 109, 1014, '1', '2023-12-30 11:42:36', '1', '2023-12-30 11:42:36', b'0', 121);
4055 4189 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3224, 109, 1015, '1', '2023-12-30 11:42:36', '1', '2023-12-30 11:42:36', b'0', 121);
4056 4190 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3225, 109, 1016, '1', '2023-12-30 11:42:36', '1', '2023-12-30 11:42:36', b'0', 121);
  4191 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3226, 111, 102, '1', '2023-12-30 11:42:36', '1', '2023-12-30 11:42:36', b'0', 122);
  4192 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3227, 111, 1013, '1', '2023-12-30 11:42:36', '1', '2023-12-30 11:42:36', b'0', 122);
  4193 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3228, 111, 1014, '1', '2023-12-30 11:42:36', '1', '2023-12-30 11:42:36', b'0', 122);
  4194 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3229, 111, 1015, '1', '2023-12-30 11:42:36', '1', '2023-12-30 11:42:36', b'0', 122);
  4195 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3230, 111, 1016, '1', '2023-12-30 11:42:36', '1', '2023-12-30 11:42:36', b'0', 122);
4057 4196 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4163, 109, 5, '1', '2024-03-30 17:53:17', '1', '2024-03-30 17:53:17', b'0', 121);
4058 4197 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4164, 109, 1118, '1', '2024-03-30 17:53:17', '1', '2024-03-30 17:53:17', b'0', 121);
4059 4198 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4165, 109, 1119, '1', '2024-03-30 17:53:17', '1', '2024-03-30 17:53:17', b'0', 121);
... ... @@ -4112,6 +4251,64 @@ INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_t
4112 4251 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4219, 109, 1220, '1', '2024-03-30 17:53:17', '1', '2024-03-30 17:53:17', b'0', 121);
4113 4252 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4220, 109, 1221, '1', '2024-03-30 17:53:17', '1', '2024-03-30 17:53:17', b'0', 121);
4114 4253 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4221, 109, 1222, '1', '2024-03-30 17:53:17', '1', '2024-03-30 17:53:17', b'0', 121);
  4254 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4222, 111, 5, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4255 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4223, 111, 1118, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4256 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4224, 111, 1119, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4257 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4225, 111, 1120, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4258 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4226, 111, 2713, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4259 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4227, 111, 2714, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4260 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4228, 111, 2715, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4261 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4229, 111, 2716, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4262 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4230, 111, 2717, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4263 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4231, 111, 2718, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4264 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4232, 111, 2720, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4265 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4233, 111, 1185, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4266 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4234, 111, 2721, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4267 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4235, 111, 1186, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4268 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4236, 111, 2722, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4269 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4237, 111, 1187, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4270 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4238, 111, 2723, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4271 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4239, 111, 1188, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4272 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4240, 111, 2724, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4273 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4241, 111, 1189, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4274 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4242, 111, 2725, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4275 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4243, 111, 1190, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4276 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4244, 111, 2726, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4277 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4245, 111, 1191, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4278 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4246, 111, 2727, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4279 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4247, 111, 1192, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4280 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4248, 111, 2728, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4281 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4249, 111, 1193, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4282 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4250, 111, 2729, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4283 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4251, 111, 1194, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4284 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4252, 111, 2730, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4285 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4253, 111, 1195, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4286 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4254, 111, 2731, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4287 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4256, 111, 2732, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4288 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4257, 111, 1197, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4289 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4258, 111, 2733, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4290 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4259, 111, 1198, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4291 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4260, 111, 2734, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4292 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4261, 111, 1199, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4293 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4262, 111, 2735, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4294 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4263, 111, 1200, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4295 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4264, 111, 1201, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4296 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4265, 111, 1202, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4297 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4266, 111, 1207, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4298 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4267, 111, 1208, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4299 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4268, 111, 1209, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4300 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4269, 111, 1210, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4301 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4270, 111, 1211, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4302 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4271, 111, 1212, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4303 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4272, 111, 1213, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4304 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4273, 111, 1215, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4305 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4274, 111, 1216, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4306 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4275, 111, 1217, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4307 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4276, 111, 1218, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4308 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4277, 111, 1219, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4309 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4278, 111, 1220, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4310 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4279, 111, 1221, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
  4311 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4280, 111, 1222, '1', '2024-03-30 17:53:18', '1', '2024-03-30 17:53:18', b'0', 122);
4115 4312 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (5779, 2, 2739, '1', '2024-07-07 20:39:38', '1', '2024-07-07 20:39:38', b'0', 1);
4116 4313 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (5780, 2, 2740, '1', '2024-07-07 20:39:38', '1', '2024-07-07 20:39:38', b'0', 1);
4117 4314 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (5781, 2, 2758, '1', '2024-07-07 20:39:38', '1', '2024-07-07 20:39:38', b'0', 1);
... ... @@ -4121,6 +4318,8 @@ INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_t
4121 4318 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (5785, 2, 2030, '1', '2024-07-07 20:39:38', '1', '2024-07-07 20:39:38', b'0', 1);
4122 4319 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (5789, 109, 2739, '1', '2024-07-13 22:37:24', '1', '2024-07-13 22:37:24', b'0', 121);
4123 4320 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (5790, 109, 2740, '1', '2024-07-13 22:37:24', '1', '2024-07-13 22:37:24', b'0', 121);
  4321 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (5791, 111, 2739, '1', '2024-07-13 22:37:24', '1', '2024-07-13 22:37:24', b'0', 122);
  4322 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (5792, 111, 2740, '1', '2024-07-13 22:37:24', '1', '2024-07-13 22:37:24', b'0', 122);
4124 4323 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6053, 155, 4000, '1', '2025-04-01 13:48:26', '1', '2025-04-01 13:48:26', b'0', 1);
4125 4324 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6097, 155, 4050, '1', '2025-04-01 13:48:26', '1', '2025-04-01 13:48:26', b'0', 1);
4126 4325 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6104, 155, 4032, '1', '2025-04-01 13:49:30', '1', '2025-04-01 13:49:30', b'0', 1);
... ... @@ -4195,6 +4394,45 @@ INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_t
4195 4394 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6175, 109, 2557, '1', '2025-09-06 20:52:12', '1', '2025-09-06 20:52:12', b'0', 121);
4196 4395 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6176, 109, 2558, '1', '2025-09-06 20:52:12', '1', '2025-09-06 20:52:12', b'0', 121);
4197 4396 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6177, 109, 2559, '1', '2025-09-06 20:52:12', '1', '2025-09-06 20:52:12', b'0', 121);
  4397 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6178, 111, 1117, '1', '2025-09-06 20:52:12', '1', '2025-09-06 20:52:12', b'0', 122);
  4398 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6179, 111, 1126, '1', '2025-09-06 20:52:12', '1', '2025-09-06 20:52:12', b'0', 122);
  4399 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6180, 111, 1127, '1', '2025-09-06 20:52:12', '1', '2025-09-06 20:52:12', b'0', 122);
  4400 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6181, 111, 1128, '1', '2025-09-06 20:52:12', '1', '2025-09-06 20:52:12', b'0', 122);
  4401 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6182, 111, 1129, '1', '2025-09-06 20:52:12', '1', '2025-09-06 20:52:12', b'0', 122);
  4402 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6183, 111, 1130, '1', '2025-09-06 20:52:12', '1', '2025-09-06 20:52:12', b'0', 122);
  4403 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6184, 111, 1132, '1', '2025-09-06 20:52:12', '1', '2025-09-06 20:52:12', b'0', 122);
  4404 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6185, 111, 1133, '1', '2025-09-06 20:52:12', '1', '2025-09-06 20:52:12', b'0', 122);
  4405 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6186, 111, 1134, '1', '2025-09-06 20:52:12', '1', '2025-09-06 20:52:12', b'0', 122);
  4406 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6187, 111, 1135, '1', '2025-09-06 20:52:12', '1', '2025-09-06 20:52:12', b'0', 122);
  4407 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6188, 111, 1136, '1', '2025-09-06 20:52:12', '1', '2025-09-06 20:52:12', b'0', 122);
  4408 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6189, 111, 1137, '1', '2025-09-06 20:52:12', '1', '2025-09-06 20:52:12', b'0', 122);
  4409 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6190, 111, 2161, '1', '2025-09-06 20:52:12', '1', '2025-09-06 20:52:12', b'0', 122);
  4410 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6191, 111, 1150, '1', '2025-09-06 20:52:12', '1', '2025-09-06 20:52:12', b'0', 122);
  4411 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6192, 111, 1161, '1', '2025-09-06 20:52:12', '1', '2025-09-06 20:52:12', b'0', 122);
  4412 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6193, 111, 1162, '1', '2025-09-06 20:52:12', '1', '2025-09-06 20:52:12', b'0', 122);
  4413 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6194, 111, 1166, '1', '2025-09-06 20:52:12', '1', '2025-09-06 20:52:12', b'0', 122);
  4414 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6195, 111, 1173, '1', '2025-09-06 20:52:12', '1', '2025-09-06 20:52:12', b'0', 122);
  4415 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6196, 111, 1174, '1', '2025-09-06 20:52:12', '1', '2025-09-06 20:52:12', b'0', 122);
  4416 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6197, 111, 1178, '1', '2025-09-06 20:52:12', '1', '2025-09-06 20:52:12', b'0', 122);
  4417 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6198, 111, 2745, '1', '2025-09-06 20:52:12', '1', '2025-09-06 20:52:12', b'0', 122);
  4418 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6199, 111, 2746, '1', '2025-09-06 20:52:12', '1', '2025-09-06 20:52:12', b'0', 122);
  4419 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6200, 111, 2747, '1', '2025-09-06 20:52:12', '1', '2025-09-06 20:52:12', b'0', 122);
  4420 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6201, 111, 2748, '1', '2025-09-06 20:52:12', '1', '2025-09-06 20:52:12', b'0', 122);
  4421 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6202, 111, 2301, '1', '2025-09-06 20:52:12', '1', '2025-09-06 20:52:12', b'0', 122);
  4422 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6203, 111, 2302, '1', '2025-09-06 20:52:12', '1', '2025-09-06 20:52:12', b'0', 122);
  4423 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6204, 111, 5011, '1', '2025-09-06 20:52:12', '1', '2025-09-06 20:52:12', b'0', 122);
  4424 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6205, 111, 5012, '1', '2025-09-06 20:52:12', '1', '2025-09-06 20:52:12', b'0', 122);
  4425 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6206, 111, 2549, '1', '2025-09-06 20:52:12', '1', '2025-09-06 20:52:12', b'0', 122);
  4426 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6207, 111, 2550, '1', '2025-09-06 20:52:12', '1', '2025-09-06 20:52:12', b'0', 122);
  4427 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6208, 111, 2551, '1', '2025-09-06 20:52:12', '1', '2025-09-06 20:52:12', b'0', 122);
  4428 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6209, 111, 2552, '1', '2025-09-06 20:52:12', '1', '2025-09-06 20:52:12', b'0', 122);
  4429 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6210, 111, 2553, '1', '2025-09-06 20:52:12', '1', '2025-09-06 20:52:12', b'0', 122);
  4430 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6211, 111, 2554, '1', '2025-09-06 20:52:12', '1', '2025-09-06 20:52:12', b'0', 122);
  4431 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6212, 111, 2555, '1', '2025-09-06 20:52:12', '1', '2025-09-06 20:52:12', b'0', 122);
  4432 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6213, 111, 2556, '1', '2025-09-06 20:52:12', '1', '2025-09-06 20:52:12', b'0', 122);
  4433 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6214, 111, 2557, '1', '2025-09-06 20:52:12', '1', '2025-09-06 20:52:12', b'0', 122);
  4434 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6215, 111, 2558, '1', '2025-09-06 20:52:12', '1', '2025-09-06 20:52:12', b'0', 122);
  4435 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6216, 111, 2559, '1', '2025-09-06 20:52:12', '1', '2025-09-06 20:52:12', b'0', 122);
4198 4436 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6217, 109, 2756, '1', '2025-09-06 20:52:25', '1', '2025-09-06 20:52:25', b'0', 121);
4199 4437 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6218, 109, 2757, '1', '2025-09-06 20:52:25', '1', '2025-09-06 20:52:25', b'0', 121);
4200 4438 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6219, 109, 2262, '1', '2025-09-06 20:52:25', '1', '2025-09-06 20:52:25', b'0', 121);
... ... @@ -4233,6 +4471,44 @@ INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_t
4233 4471 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6252, 109, 2335, '1', '2025-09-06 20:52:25', '1', '2025-09-06 20:52:25', b'0', 121);
4234 4472 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6253, 109, 2363, '1', '2025-09-06 20:52:25', '1', '2025-09-06 20:52:25', b'0', 121);
4235 4473 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6254, 109, 2364, '1', '2025-09-06 20:52:25', '1', '2025-09-06 20:52:25', b'0', 121);
  4474 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6255, 111, 2756, '1', '2025-09-06 20:52:25', '1', '2025-09-06 20:52:25', b'0', 122);
  4475 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6256, 111, 2757, '1', '2025-09-06 20:52:25', '1', '2025-09-06 20:52:25', b'0', 122);
  4476 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6257, 111, 2262, '1', '2025-09-06 20:52:25', '1', '2025-09-06 20:52:25', b'0', 122);
  4477 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6258, 111, 2275, '1', '2025-09-06 20:52:25', '1', '2025-09-06 20:52:25', b'0', 122);
  4478 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6259, 111, 2276, '1', '2025-09-06 20:52:25', '1', '2025-09-06 20:52:25', b'0', 122);
  4479 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6260, 111, 2277, '1', '2025-09-06 20:52:25', '1', '2025-09-06 20:52:25', b'0', 122);
  4480 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6261, 111, 2281, '1', '2025-09-06 20:52:25', '1', '2025-09-06 20:52:25', b'0', 122);
  4481 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6262, 111, 2282, '1', '2025-09-06 20:52:25', '1', '2025-09-06 20:52:25', b'0', 122);
  4482 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6263, 111, 2283, '1', '2025-09-06 20:52:25', '1', '2025-09-06 20:52:25', b'0', 122);
  4483 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6264, 111, 2284, '1', '2025-09-06 20:52:25', '1', '2025-09-06 20:52:25', b'0', 122);
  4484 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6265, 111, 2285, '1', '2025-09-06 20:52:25', '1', '2025-09-06 20:52:25', b'0', 122);
  4485 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6266, 111, 2287, '1', '2025-09-06 20:52:25', '1', '2025-09-06 20:52:25', b'0', 122);
  4486 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6267, 111, 2288, '1', '2025-09-06 20:52:25', '1', '2025-09-06 20:52:25', b'0', 122);
  4487 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6268, 111, 2293, '1', '2025-09-06 20:52:25', '1', '2025-09-06 20:52:25', b'0', 122);
  4488 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6269, 111, 2294, '1', '2025-09-06 20:52:25', '1', '2025-09-06 20:52:25', b'0', 122);
  4489 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6270, 111, 2297, '1', '2025-09-06 20:52:25', '1', '2025-09-06 20:52:25', b'0', 122);
  4490 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6271, 111, 2300, '1', '2025-09-06 20:52:25', '1', '2025-09-06 20:52:25', b'0', 122);
  4491 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6272, 111, 2317, '1', '2025-09-06 20:52:25', '1', '2025-09-06 20:52:25', b'0', 122);
  4492 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6273, 111, 2318, '1', '2025-09-06 20:52:25', '1', '2025-09-06 20:52:25', b'0', 122);
  4493 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6274, 111, 2319, '1', '2025-09-06 20:52:25', '1', '2025-09-06 20:52:25', b'0', 122);
  4494 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6275, 111, 2320, '1', '2025-09-06 20:52:25', '1', '2025-09-06 20:52:25', b'0', 122);
  4495 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6276, 111, 2321, '1', '2025-09-06 20:52:25', '1', '2025-09-06 20:52:25', b'0', 122);
  4496 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6277, 111, 2322, '1', '2025-09-06 20:52:25', '1', '2025-09-06 20:52:25', b'0', 122);
  4497 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6278, 111, 2323, '1', '2025-09-06 20:52:25', '1', '2025-09-06 20:52:25', b'0', 122);
  4498 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6279, 111, 2324, '1', '2025-09-06 20:52:25', '1', '2025-09-06 20:52:25', b'0', 122);
  4499 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6280, 111, 2325, '1', '2025-09-06 20:52:25', '1', '2025-09-06 20:52:25', b'0', 122);
  4500 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6281, 111, 2326, '1', '2025-09-06 20:52:25', '1', '2025-09-06 20:52:25', b'0', 122);
  4501 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6282, 111, 2327, '1', '2025-09-06 20:52:25', '1', '2025-09-06 20:52:25', b'0', 122);
  4502 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6283, 111, 2328, '1', '2025-09-06 20:52:25', '1', '2025-09-06 20:52:25', b'0', 122);
  4503 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6284, 111, 2329, '1', '2025-09-06 20:52:25', '1', '2025-09-06 20:52:25', b'0', 122);
  4504 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6285, 111, 2330, '1', '2025-09-06 20:52:25', '1', '2025-09-06 20:52:25', b'0', 122);
  4505 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6286, 111, 2331, '1', '2025-09-06 20:52:25', '1', '2025-09-06 20:52:25', b'0', 122);
  4506 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6287, 111, 2332, '1', '2025-09-06 20:52:25', '1', '2025-09-06 20:52:25', b'0', 122);
  4507 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6288, 111, 2333, '1', '2025-09-06 20:52:25', '1', '2025-09-06 20:52:25', b'0', 122);
  4508 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6289, 111, 2334, '1', '2025-09-06 20:52:25', '1', '2025-09-06 20:52:25', b'0', 122);
  4509 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6290, 111, 2335, '1', '2025-09-06 20:52:25', '1', '2025-09-06 20:52:25', b'0', 122);
  4510 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6291, 111, 2363, '1', '2025-09-06 20:52:25', '1', '2025-09-06 20:52:25', b'0', 122);
  4511 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6292, 111, 2364, '1', '2025-09-06 20:52:25', '1', '2025-09-06 20:52:25', b'0', 122);
4236 4512 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6293, 2, 5, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
4237 4513 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6294, 2, 1118, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
4238 4514 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6295, 2, 1119, '1', '2026-01-04 18:09:41', '1', '2026-01-04 18:09:41', b'0', 1);
... ... @@ -4303,6 +4579,7 @@ INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_t
4303 4579 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6360, 2, 5811, '', '2026-03-30 03:13:05', '', '2026-03-30 03:13:05', b'0', 1);
4304 4580 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6361, 3, 5811, '', '2026-03-30 03:13:05', '', '2026-03-30 03:13:05', b'0', 1);
4305 4581 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6362, 109, 5811, '', '2026-03-30 03:13:05', '', '2026-03-30 03:13:05', b'0', 121);
  4582 +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6363, 111, 5811, '', '2026-03-30 03:13:05', '', '2026-03-30 03:13:05', b'0', 122);
4306 4583 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6364, 155, 5811, '', '2026-03-30 03:13:05', '', '2026-03-30 03:13:05', b'0', 1);
4307 4584 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6365, 2, 6400, '1', '2026-05-09 16:11:01', '1', '2026-05-09 16:11:01', b'0', 1);
4308 4585 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6366, 2, 6401, '1', '2026-05-09 16:11:01', '1', '2026-05-09 16:11:01', b'0', 1);
... ... @@ -4312,317 +4589,6 @@ INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_t
4312 4589 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6370, 2, 6405, '1', '2026-05-09 16:11:01', '1', '2026-05-09 16:11:01', b'0', 1);
4313 4590 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6371, 2, 6406, '1', '2026-05-09 16:11:01', '1', '2026-05-09 16:11:01', b'0', 1);
4314 4591 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6380, 2, 6490, '1', '2026-05-14 09:36:12', '1', '2026-05-14 09:36:12', b'0', 1);
4315   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6381, 111, 1, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4316   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6382, 111, 2, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4317   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6383, 111, 5, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4318   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6384, 111, 1031, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4319   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6385, 111, 1032, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4320   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6386, 111, 1033, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4321   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6387, 111, 1034, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4322   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6388, 111, 1035, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4323   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6389, 111, 1036, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4324   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6390, 111, 1037, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4325   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6391, 111, 1038, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4326   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6392, 111, 1039, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4327   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6393, 111, 1050, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4328   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6394, 111, 1051, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4329   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6395, 111, 1052, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4330   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6396, 111, 1053, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4331   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6397, 111, 1054, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4332   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6398, 111, 1056, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4333   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6399, 111, 1057, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4334   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6400, 111, 1058, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4335   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6401, 111, 1059, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4336   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6402, 111, 1060, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4337   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6403, 111, 1063, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4338   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6404, 111, 1064, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4339   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6405, 111, 1065, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4340   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6406, 111, 1066, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4341   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6407, 111, 1067, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4342   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6408, 111, 1070, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4343   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6409, 111, 1075, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4344   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6410, 111, 1077, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4345   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6411, 111, 1078, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4346   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6412, 111, 1082, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4347   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6413, 111, 1083, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4348   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6414, 111, 1084, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4349   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6415, 111, 1085, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4350   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6416, 111, 1086, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4351   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6417, 111, 1087, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4352   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6418, 111, 1088, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4353   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6419, 111, 1089, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4354   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6420, 111, 1090, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4355   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6421, 111, 1091, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4356   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6422, 111, 1092, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4357   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6423, 111, 1117, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4358   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6424, 111, 1118, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4359   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6425, 111, 1119, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4360   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6426, 111, 1120, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4361   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6427, 111, 100, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4362   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6428, 111, 101, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4363   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6429, 111, 102, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4364   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6430, 111, 1126, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4365   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6431, 111, 103, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4366   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6432, 111, 1127, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4367   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6433, 111, 1128, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4368   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6434, 111, 1129, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4369   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6435, 111, 106, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4370   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6436, 111, 1130, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4371   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6437, 111, 107, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4372   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6438, 111, 1132, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4373   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6439, 111, 1133, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4374   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6440, 111, 110, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4375   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6441, 111, 1134, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4376   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6442, 111, 111, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4377   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6443, 111, 1135, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4378   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6444, 111, 112, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4379   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6445, 111, 1136, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4380   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6446, 111, 113, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4381   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6447, 111, 1137, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4382   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6448, 111, 2161, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4383   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6449, 111, 114, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4384   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6450, 111, 1138, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4385   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6451, 111, 1139, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4386   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6452, 111, 115, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4387   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6453, 111, 1140, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4388   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6454, 111, 116, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4389   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6455, 111, 1141, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4390   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6456, 111, 1142, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4391   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6457, 111, 1143, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4392   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6458, 111, 1150, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4393   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6459, 111, 1161, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4394   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6460, 111, 1162, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4395   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6461, 111, 1166, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4396   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6462, 111, 1173, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4397   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6463, 111, 1174, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4398   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6464, 111, 2713, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4399   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6465, 111, 2714, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4400   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6466, 111, 1178, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4401   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6467, 111, 2715, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4402   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6468, 111, 2716, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4403   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6469, 111, 2717, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4404   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6470, 111, 2718, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4405   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6471, 111, 2720, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4406   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6472, 111, 2721, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4407   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6473, 111, 1185, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4408   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6474, 111, 2722, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4409   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6475, 111, 1186, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4410   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6476, 111, 1187, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4411   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6477, 111, 2723, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4412   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6478, 111, 1188, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4413   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6479, 111, 2724, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4414   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6480, 111, 1189, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4415   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6481, 111, 2725, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4416   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6482, 111, 1190, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4417   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6483, 111, 2726, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4418   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6484, 111, 1191, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4419   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6485, 111, 2727, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4420   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6486, 111, 1192, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4421   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6487, 111, 2728, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4422   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6488, 111, 2729, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4423   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6489, 111, 1193, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4424   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6490, 111, 1194, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4425   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6491, 111, 2730, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4426   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6492, 111, 1195, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4427   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6493, 111, 2731, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4428   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6494, 111, 2732, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4429   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6495, 111, 1197, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4430   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6496, 111, 2733, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4431   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6497, 111, 1198, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4432   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6498, 111, 2734, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4433   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6499, 111, 1199, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4434   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6500, 111, 2735, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4435   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6501, 111, 1200, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4436   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6502, 111, 1201, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4437   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6503, 111, 1202, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4438   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6504, 111, 2739, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4439   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6505, 111, 2740, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4440   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6506, 111, 1207, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4441   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6507, 111, 1208, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4442   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6508, 111, 1209, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4443   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6509, 111, 2745, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4444   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6510, 111, 1210, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4445   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6511, 111, 2746, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4446   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6512, 111, 1211, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4447   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6513, 111, 2747, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4448   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6514, 111, 1212, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4449   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6515, 111, 2748, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4450   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6516, 111, 1213, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4451   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6517, 111, 1215, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4452   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6518, 111, 1216, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4453   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6519, 111, 1217, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4454   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6520, 111, 1218, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4455   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6521, 111, 1219, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4456   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6522, 111, 1220, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4457   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6523, 111, 2756, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4458   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6524, 111, 1221, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4459   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6525, 111, 2757, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4460   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6526, 111, 1222, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4461   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6527, 111, 1224, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4462   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6528, 111, 1225, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4463   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6529, 111, 1226, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4464   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6530, 111, 1227, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4465   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6531, 111, 1228, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4466   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6532, 111, 1229, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4467   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6533, 111, 1237, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4468   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6534, 111, 1238, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4469   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6535, 111, 2262, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4470   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6536, 111, 1239, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4471   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6537, 111, 1240, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4472   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6538, 111, 1241, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4473   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6539, 111, 1242, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4474   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6540, 111, 1243, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4475   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6541, 111, 2275, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4476   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6542, 111, 2276, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4477   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6543, 111, 2277, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4478   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6544, 111, 1255, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4479   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6545, 111, 1256, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4480   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6546, 111, 1257, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4481   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6547, 111, 2281, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4482   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6548, 111, 1258, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4483   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6549, 111, 2282, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4484   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6550, 111, 1259, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4485   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6551, 111, 2283, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4486   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6552, 111, 1260, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4487   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6553, 111, 2284, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4488   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6554, 111, 2285, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4489   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6555, 111, 2287, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4490   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6556, 111, 2288, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4491   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6557, 111, 2293, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4492   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6558, 111, 2294, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4493   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6559, 111, 2297, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4494   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6560, 111, 2300, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4495   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6561, 111, 2301, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4496   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6562, 111, 2302, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4497   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6563, 111, 2317, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4498   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6564, 111, 2318, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4499   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6565, 111, 2319, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4500   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6566, 111, 2320, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4501   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6567, 111, 2321, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4502   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6568, 111, 2322, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4503   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6569, 111, 2323, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4504   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6570, 111, 2324, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4505   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6571, 111, 2325, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4506   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6572, 111, 2326, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4507   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6573, 111, 2327, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4508   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6574, 111, 2328, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4509   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6575, 111, 2329, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4510   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6576, 111, 2330, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4511   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6577, 111, 2331, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4512   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6578, 111, 2332, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4513   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6579, 111, 2333, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4514   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6580, 111, 2334, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4515   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6581, 111, 2335, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4516   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6582, 111, 2363, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4517   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6583, 111, 2364, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4518   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6584, 111, 5011, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4519   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6585, 111, 5012, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4520   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6586, 111, 2472, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4521   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6587, 111, 2478, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4522   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6588, 111, 2479, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4523   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6589, 111, 2480, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4524   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6590, 111, 2481, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4525   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6591, 111, 2482, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4526   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6592, 111, 2483, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4527   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6593, 111, 2484, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4528   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6594, 111, 2485, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4529   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6595, 111, 2486, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4530   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6596, 111, 2487, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4531   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6597, 111, 2488, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4532   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6598, 111, 2489, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4533   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6599, 111, 2490, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4534   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6600, 111, 2491, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4535   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6601, 111, 2492, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4536   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6602, 111, 2493, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4537   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6603, 111, 2494, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4538   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6604, 111, 2495, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4539   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6605, 111, 2497, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4540   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6606, 111, 2525, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4541   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6607, 111, 1001, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4542   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6608, 111, 1002, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4543   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6609, 111, 1003, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4544   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6610, 111, 1004, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4545   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6611, 111, 1005, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4546   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6612, 111, 1006, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4547   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6613, 111, 1007, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4548   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6614, 111, 1008, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4549   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6615, 111, 1009, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4550   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6616, 111, 1010, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4551   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6617, 111, 1011, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4552   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6618, 111, 1012, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4553   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6619, 111, 1013, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4554   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6620, 111, 2549, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4555   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6621, 111, 1014, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4556   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6622, 111, 2550, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4557   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6623, 111, 1015, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4558   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6624, 111, 2551, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4559   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6625, 111, 1016, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4560   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6626, 111, 2552, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4561   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6627, 111, 1017, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4562   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6628, 111, 2553, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4563   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6629, 111, 1018, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4564   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6630, 111, 2554, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4565   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6631, 111, 1019, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4566   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6632, 111, 2555, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4567   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6633, 111, 1020, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4568   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6634, 111, 2556, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4569   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6635, 111, 2557, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4570   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6636, 111, 2558, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4571   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6637, 111, 2559, '1', '2026-06-01 23:29:35', '1', '2026-06-01 23:29:35', b'0', 122);
4572   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6638, 155, 5780, '1', '2026-06-10 09:04:18', '1', '2026-06-10 09:04:18', b'0', 1);
4573   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6639, 155, 2084, '1', '2026-06-10 09:04:18', '1', '2026-06-10 09:04:18', b'0', 1);
4574   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6640, 155, 2085, '1', '2026-06-10 09:04:18', '1', '2026-06-10 09:04:18', b'0', 1);
4575   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6641, 155, 2086, '1', '2026-06-10 09:04:18', '1', '2026-06-10 09:04:18', b'0', 1);
4576   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6642, 155, 2087, '1', '2026-06-10 09:04:18', '1', '2026-06-10 09:04:18', b'0', 1);
4577   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6643, 155, 2088, '1', '2026-06-10 09:04:18', '1', '2026-06-10 09:04:18', b'0', 1);
4578   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6644, 155, 2089, '1', '2026-06-10 09:04:18', '1', '2026-06-10 09:04:18', b'0', 1);
4579   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6645, 155, 2090, '1', '2026-06-10 09:04:18', '1', '2026-06-10 09:04:18', b'0', 1);
4580   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6646, 155, 2091, '1', '2026-06-10 09:04:18', '1', '2026-06-10 09:04:18', b'0', 1);
4581   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6647, 155, 2092, '1', '2026-06-10 09:04:18', '1', '2026-06-10 09:04:18', b'0', 1);
4582   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6648, 155, 2093, '1', '2026-06-10 09:04:18', '1', '2026-06-10 09:04:18', b'0', 1);
4583   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6649, 155, 5805, '1', '2026-06-10 09:04:18', '1', '2026-06-10 09:04:18', b'0', 1);
4584   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6650, 155, 2094, '1', '2026-06-10 09:04:18', '1', '2026-06-10 09:04:18', b'0', 1);
4585   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6651, 155, 2095, '1', '2026-06-10 09:04:18', '1', '2026-06-10 09:04:18', b'0', 1);
4586   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6652, 155, 2096, '1', '2026-06-10 09:04:18', '1', '2026-06-10 09:04:18', b'0', 1);
4587   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6653, 155, 2097, '1', '2026-06-10 09:04:18', '1', '2026-06-10 09:04:18', b'0', 1);
4588   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6654, 155, 2098, '1', '2026-06-10 09:04:18', '1', '2026-06-10 09:04:18', b'0', 1);
4589   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6655, 155, 5042, '1', '2026-06-10 09:04:18', '1', '2026-06-10 09:04:18', b'0', 1);
4590   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6656, 155, 2099, '1', '2026-06-10 09:04:18', '1', '2026-06-10 09:04:18', b'0', 1);
4591   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6657, 155, 5043, '1', '2026-06-10 09:04:18', '1', '2026-06-10 09:04:18', b'0', 1);
4592   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6658, 155, 2100, '1', '2026-06-10 09:04:18', '1', '2026-06-10 09:04:18', b'0', 1);
4593   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6659, 155, 5044, '1', '2026-06-10 09:04:18', '1', '2026-06-10 09:04:18', b'0', 1);
4594   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6660, 155, 2101, '1', '2026-06-10 09:04:18', '1', '2026-06-10 09:04:18', b'0', 1);
4595   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6661, 155, 5045, '1', '2026-06-10 09:04:18', '1', '2026-06-10 09:04:18', b'0', 1);
4596   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6662, 155, 2102, '1', '2026-06-10 09:04:18', '1', '2026-06-10 09:04:18', b'0', 1);
4597   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6663, 155, 5046, '1', '2026-06-10 09:04:18', '1', '2026-06-10 09:04:18', b'0', 1);
4598   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6664, 155, 2103, '1', '2026-06-10 09:04:18', '1', '2026-06-10 09:04:18', b'0', 1);
4599   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6665, 155, 2104, '1', '2026-06-10 09:04:18', '1', '2026-06-10 09:04:18', b'0', 1);
4600   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6666, 155, 2105, '1', '2026-06-10 09:04:18', '1', '2026-06-10 09:04:18', b'0', 1);
4601   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6667, 155, 2106, '1', '2026-06-10 09:04:18', '1', '2026-06-10 09:04:18', b'0', 1);
4602   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6668, 155, 2107, '1', '2026-06-10 09:04:18', '1', '2026-06-10 09:04:18', b'0', 1);
4603   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6669, 155, 2108, '1', '2026-06-10 09:04:18', '1', '2026-06-10 09:04:18', b'0', 1);
4604   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6670, 155, 2109, '1', '2026-06-10 09:04:18', '1', '2026-06-10 09:04:18', b'0', 1);
4605   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6671, 155, 2110, '1', '2026-06-10 09:04:18', '1', '2026-06-10 09:04:18', b'0', 1);
4606   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6672, 155, 2111, '1', '2026-06-10 09:04:18', '1', '2026-06-10 09:04:18', b'0', 1);
4607   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6673, 155, 2112, '1', '2026-06-10 09:04:18', '1', '2026-06-10 09:04:18', b'0', 1);
4608   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6674, 155, 2113, '1', '2026-06-10 09:04:18', '1', '2026-06-10 09:04:18', b'0', 1);
4609   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6675, 155, 2114, '1', '2026-06-10 09:04:18', '1', '2026-06-10 09:04:18', b'0', 1);
4610   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6676, 155, 2115, '1', '2026-06-10 09:04:18', '1', '2026-06-10 09:04:18', b'0', 1);
4611   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6677, 155, 2116, '1', '2026-06-10 09:04:18', '1', '2026-06-10 09:04:18', b'0', 1);
4612   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6678, 155, 2117, '1', '2026-06-10 09:04:18', '1', '2026-06-10 09:04:18', b'0', 1);
4613   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6679, 155, 2118, '1', '2026-06-10 09:04:18', '1', '2026-06-10 09:04:18', b'0', 1);
4614   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6680, 155, 2119, '1', '2026-06-10 09:04:18', '1', '2026-06-10 09:04:18', b'0', 1);
4615   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6681, 155, 2120, '1', '2026-06-10 09:04:18', '1', '2026-06-10 09:04:18', b'0', 1);
4616   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6682, 155, 2121, '1', '2026-06-10 09:04:18', '1', '2026-06-10 09:04:18', b'0', 1);
4617   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6683, 155, 2122, '1', '2026-06-10 09:04:18', '1', '2026-06-10 09:04:18', b'0', 1);
4618   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6684, 155, 2123, '1', '2026-06-10 09:04:18', '1', '2026-06-10 09:04:18', b'0', 1);
4619   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6685, 155, 2124, '1', '2026-06-10 09:04:18', '1', '2026-06-10 09:04:18', b'0', 1);
4620   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6686, 155, 2125, '1', '2026-06-10 09:04:18', '1', '2026-06-10 09:04:18', b'0', 1);
4621   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6687, 155, 2126, '1', '2026-06-10 09:04:18', '1', '2026-06-10 09:04:18', b'0', 1);
4622   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6688, 155, 2127, '1', '2026-06-10 09:04:18', '1', '2026-06-10 09:04:18', b'0', 1);
4623   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6689, 155, 2128, '1', '2026-06-10 09:04:18', '1', '2026-06-10 09:04:18', b'0', 1);
4624   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6690, 155, 2129, '1', '2026-06-10 09:04:18', '1', '2026-06-10 09:04:18', b'0', 1);
4625   -INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6691, 155, 5100, '1', '2026-06-10 09:04:18', '1', '2026-06-10 09:04:18', b'0', 1);
4626 4592 COMMIT;
4627 4593  
4628 4594 -- ----------------------------
... ... @@ -4677,7 +4643,7 @@ CREATE TABLE `system_sms_code` (
4677 4643 `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
4678 4644 PRIMARY KEY (`id`) USING BTREE,
4679 4645 INDEX `idx_mobile`(`mobile` ASC) USING BTREE COMMENT '手机号'
4680   -) ENGINE = InnoDB AUTO_INCREMENT = 694 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '手机验证码';
  4646 +) ENGINE = InnoDB AUTO_INCREMENT = 690 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '手机验证码';
4681 4647  
4682 4648 -- ----------------------------
4683 4649 -- Records of system_sms_code
... ... @@ -4718,7 +4684,7 @@ CREATE TABLE `system_sms_log` (
4718 4684 `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
4719 4685 `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
4720 4686 PRIMARY KEY (`id`) USING BTREE
4721   -) ENGINE = InnoDB AUTO_INCREMENT = 1568 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '短信日志';
  4687 +) ENGINE = InnoDB AUTO_INCREMENT = 1553 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '短信日志';
4722 4688  
4723 4689 -- ----------------------------
4724 4690 -- Records of system_sms_log
... ... @@ -4895,7 +4861,7 @@ CREATE TABLE `system_tenant` (
4895 4861 BEGIN;
4896 4862 INSERT INTO `system_tenant` (`id`, `name`, `contact_user_id`, `contact_name`, `contact_mobile`, `status`, `websites`, `package_id`, `expire_time`, `account_count`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1, '芋道源码', NULL, '芋艿', '17321315478', 0, 'www.iocoder.cn,127.0.0.1:3000,wxc4598c446f8a9cb3', 0, '2099-02-19 17:14:16', 9999, '1', '2021-01-05 17:03:47', '1', '2025-08-19 05:18:41', b'0');
4897 4863 INSERT INTO `system_tenant` (`id`, `name`, `contact_user_id`, `contact_name`, `contact_mobile`, `status`, `websites`, `package_id`, `expire_time`, `account_count`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (121, '小租户', 110, '小王2', '15601691300', 0, 'zsxq.iocoder.cn,123321', 111, '2026-07-10 00:00:00', 30, '1', '2022-02-22 00:56:14', '1', '2025-08-19 21:19:29', b'0');
4898   -INSERT INTO `system_tenant` (`id`, `name`, `contact_user_id`, `contact_name`, `contact_mobile`, `status`, `websites`, `package_id`, `expire_time`, `account_count`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (122, '测试租户', 113, '芋道', '15601691300', 0, 'test.iocoder.cn,222,333', 111, '2023-04-29 00:00:00', 50, '1', '2022-03-07 21:37:58', '1', '2026-06-01 23:29:35', b'0');
  4864 +INSERT INTO `system_tenant` (`id`, `name`, `contact_user_id`, `contact_name`, `contact_mobile`, `status`, `websites`, `package_id`, `expire_time`, `account_count`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (122, '测试租户', 113, '芋道', '15601691300', 0, 'test.iocoder.cn,222,333', 111, '2023-04-29 00:00:00', 50, '1', '2022-03-07 21:37:58', '1', '2025-12-21 09:50:00', b'0');
4899 4865 COMMIT;
4900 4866  
4901 4867 -- ----------------------------
... ... @@ -4914,14 +4880,13 @@ CREATE TABLE `system_tenant_package` (
4914 4880 `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
4915 4881 `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
4916 4882 PRIMARY KEY (`id`) USING BTREE
4917   -) ENGINE = InnoDB AUTO_INCREMENT = 114 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '租户套餐表';
  4883 +) ENGINE = InnoDB AUTO_INCREMENT = 113 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '租户套餐表';
4918 4884  
4919 4885 -- ----------------------------
4920 4886 -- Records of system_tenant_package
4921 4887 -- ----------------------------
4922 4888 BEGIN;
4923 4889 INSERT INTO `system_tenant_package` (`id`, `name`, `status`, `remark`, `menu_ids`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (111, '普通套餐', 0, '小功能', '[1,2,5,1031,1032,1033,1034,1035,1036,1037,1038,1039,1050,1051,1052,1053,1054,1056,1057,1058,1059,1060,1063,1064,1065,1066,1067,1070,1075,1077,1078,1082,1083,1084,1085,1086,1087,1088,1089,1090,1091,1092,1117,1118,1119,1120,100,101,102,1126,103,1127,1128,1129,106,1130,107,1132,1133,110,1134,111,1135,112,1136,113,1137,2161,114,1138,1139,115,1140,116,1141,1142,1143,1150,1161,1162,1166,1173,1174,2713,2714,1178,2715,2716,2717,2718,2720,2721,1185,2722,1186,1187,2723,1188,2724,1189,2725,1190,2726,1191,2727,1192,2728,2729,1193,1194,2730,1195,2731,2732,1197,2733,1198,2734,1199,2735,1200,1201,1202,2739,2740,1207,1208,1209,2745,1210,2746,1211,2747,1212,2748,1213,1215,1216,1217,1218,1219,1220,2756,1221,2757,1222,1224,1225,1226,1227,1228,1229,1237,1238,2262,1239,1240,1241,1242,1243,2275,2276,2277,1255,1256,1257,2281,1258,2282,1259,2283,1260,2284,2285,2287,2288,2293,2294,2297,2300,2301,2302,2317,2318,2319,2320,2321,2322,2323,2324,2325,2326,2327,2328,2329,2330,2331,2332,2333,2334,2335,2363,2364,5011,5012,2472,2478,2479,2480,2481,2482,2483,2484,2485,2486,2487,2488,2489,2490,2491,2492,2493,2494,2495,2497,2525,1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1011,1012,1013,2549,1014,2550,1015,2551,1016,2552,1017,2553,1018,2554,1019,2555,1020,2556,2557,2558,2559]', '1', '2022-02-22 00:54:00', '1', '2025-09-06 20:52:25', b'0');
4924   -INSERT INTO `system_tenant_package` (`id`, `name`, `status`, `remark`, `menu_ids`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (113, '测试套餐(啥都没有)', 0, '', '[2160,1254,2159]', '1', '2026-06-01 23:22:39', '1', '2026-06-09 14:44:00', b'0');
4925 4890 COMMIT;
4926 4891  
4927 4892 -- ----------------------------
... ... @@ -5039,26 +5004,26 @@ CREATE TABLE `system_users` (
5039 5004 -- Records of system_users
5040 5005 -- ----------------------------
5041 5006 BEGIN;
5042   -INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1, 'admin', '$2a$04$.vd8nPeLwxt6hnSzmAoAyul8BOLX7Cib6QhcxRe30rfvrIPQHH1OG', '芋道源码', '管理员', 103, '[1,2]', '13aoteman@126.com', '18818260272', 1, 'http://test.yudao.iocoder.cn/20260517/blob_1778998103688.png', 0, '0:0:0:0:0:0:0:1', '2026-06-19 18:38:00', 'admin', '2021-01-05 17:03:47', NULL, '2026-06-19 18:38:00', b'0', 1);
5043   -INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (100, 'yudao', '$2a$04$.vd8nPeLwxt6hnSzmAoAyul8BOLX7Cib6QhcxRe30rfvrIPQHH1OG', '芋道', '不要吓我', 104, '[1]', 'yudao@iocoder.cn', '15601691300', 1, NULL, 0, '127.0.0.1', '2026-06-17 09:37:17', '', '2021-01-07 09:07:17', NULL, '2026-06-17 09:37:17', b'0', 1);
  5007 +INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1, 'admin', '$2a$04$.vd8nPeLwxt6hnSzmAoAyul8BOLX7Cib6QhcxRe30rfvrIPQHH1OG', '芋道源码', '管理员', 103, '[1,2]', '13aoteman@126.com', '18818260272', 1, 'http://test.yudao.iocoder.cn/20260517/blob_1778998103688.png', 0, '0:0:0:0:0:0:0:1', '2026-05-31 21:54:49', 'admin', '2021-01-05 17:03:47', NULL, '2026-05-31 21:54:49', b'0', 1);
  5008 +INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (100, 'yudao', '$2a$04$.vd8nPeLwxt6hnSzmAoAyul8BOLX7Cib6QhcxRe30rfvrIPQHH1OG', '芋道', '不要吓我', 104, '[1]', 'yudao@iocoder.cn', '15601691300', 1, NULL, 0, '0:0:0:0:0:0:0:1', '2026-04-19 17:40:55', '', '2021-01-07 09:07:17', NULL, '2026-04-19 17:40:55', b'0', 1);
5044 5009 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (103, 'yuanma', '$2a$04$k/d6mc0nySN0i2udwcI8Ee8V5aM5OHixBRbQfXmPuFTUl3Zf/DBs.', '源码', NULL, 106, NULL, 'yuanma@iocoder.cn', '15601701300', 0, NULL, 0, '0:0:0:0:0:0:0:1', '2026-04-27 13:19:27', '', '2021-01-13 23:50:35', NULL, '2026-04-27 13:19:27', b'0', 1);
5045   -INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (104, 'test', '$2a$04$BrwaYn303hjA/6TnXqdGoOLhyHOAA0bVrAFu6.1dJKycqKUnIoRz2', '测试号', NULL, 107, '[1,2]', '111@qq.com', '15601691200', 1, NULL, 0, '0:0:0:0:0:0:0:1', '2026-06-16 11:37:56', '', '2021-01-21 02:13:53', NULL, '2026-06-16 11:37:56', b'0', 1);
  5010 +INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (104, 'test', '$2a$04$BrwaYn303hjA/6TnXqdGoOLhyHOAA0bVrAFu6.1dJKycqKUnIoRz2', '测试号', NULL, 107, '[1,2]', '111@qq.com', '15601691200', 1, NULL, 0, '0:0:0:0:0:0:0:1', '2026-05-20 23:37:11', '', '2021-01-21 02:13:53', NULL, '2026-05-20 23:37:11', b'0', 1);
5046 5011 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (107, 'admin107', '$2a$10$dYOOBKMO93v/.ReCqzyFg.o67Tqk.bbc2bhrpyBGkIw9aypCtr2pm', '芋艿', NULL, NULL, NULL, '', '15601691300', 0, NULL, 0, '', NULL, '1', '2022-02-20 22:59:33', '1', '2025-04-21 14:23:08', b'0', 118);
5047 5012 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (108, 'admin108', '$2a$10$y6mfvKoNYL1GXWak8nYwVOH.kCWqjactkzdoIDgiKl93WN3Ejg.Lu', '芋艿', NULL, NULL, NULL, '', '15601691300', 0, NULL, 0, '', NULL, '1', '2022-02-20 23:00:50', '1', '2025-04-21 14:23:08', b'0', 119);
5048 5013 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (109, 'admin109', '$2a$10$JAqvH0tEc0I7dfDVBI7zyuB4E3j.uH6daIjV53.vUS6PknFkDJkuK', '芋艿', NULL, NULL, NULL, '', '15601691300', 0, NULL, 0, '', NULL, '1', '2022-02-20 23:11:50', '1', '2025-04-21 14:23:08', b'0', 120);
5049 5014 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (110, 'admin110', '$2a$10$mRMIYLDtRHlf6.9ipiqH1.Z.bh/R9dO9d5iHiGYPigi6r5KOoR2Wm', '小王', NULL, NULL, NULL, '', '15601691300', 0, NULL, 0, '0:0:0:0:0:0:0:1', '2024-07-20 22:23:17', '1', '2022-02-22 00:56:14', NULL, '2025-04-21 14:23:08', b'0', 121);
5050 5015 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (111, 'test', '$2a$10$mRMIYLDtRHlf6.9ipiqH1.Z.bh/R9dO9d5iHiGYPigi6r5KOoR2Wm', '测试用户', NULL, NULL, '[]', '', '', 0, NULL, 0, '0:0:0:0:0:0:0:1', '2023-12-30 11:42:17', '110', '2022-02-23 13:14:33', NULL, '2025-04-21 14:23:08', b'0', 121);
5051   -INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (112, 'newobject', '$2a$04$dB0z8Q819fJWz0hbaLe6B.VfHCjYgWx6LFfET5lyz3JwcqlyCkQ4C', '新对象', NULL, 100, '[]', '', '15601691235', 1, NULL, 0, '127.0.0.1', '2026-06-17 09:07:20', '1', '2022-02-23 19:08:03', NULL, '2026-06-17 09:07:20', b'0', 1);
  5016 +INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (112, 'newobject', '$2a$04$dB0z8Q819fJWz0hbaLe6B.VfHCjYgWx6LFfET5lyz3JwcqlyCkQ4C', '新对象', NULL, 100, '[]', '', '15601691235', 1, NULL, 0, '0:0:0:0:0:0:0:1', '2024-03-16 23:11:38', '1', '2022-02-23 19:08:03', NULL, '2025-04-21 14:23:08', b'0', 1);
5052 5017 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (113, 'aoteman', '$2a$10$0acJOIk2D25/oC87nyclE..0lzeu9DtQ/n3geP4fkun/zIVRhHJIO', '芋道1', NULL, NULL, NULL, '', '15601691300', 0, NULL, 0, '127.0.0.1', '2022-03-19 18:38:51', '1', '2022-03-07 21:37:58', '1', '2025-05-05 15:30:53', b'0', 122);
5053 5018 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (114, 'hrmgr', '$2a$10$TR4eybBioGRhBmDBWkqWLO6NIh3mzYa8KBKDDB5woiGYFVlRAi.fu', 'hr 小姐姐', NULL, NULL, '[5]', '', '15601691236', 1, NULL, 0, '0:0:0:0:0:0:0:1', '2026-01-04 18:16:01', '1', '2022-03-19 21:50:58', NULL, '2026-01-04 18:16:01', b'0', 1);
5054 5019 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (115, 'aotemane', '$2a$04$GcyP0Vyzb2F2Yni5PuIK9ueGxM0tkZGMtDwVRwrNbtMvorzbpNsV2', '阿呆', '11222', 102, '[1,2]', '7648@qq.com', '15601691229', 2, NULL, 0, '', NULL, '1', '2022-04-30 02:55:43', '1', '2025-04-21 14:23:08', b'0', 1);
5055 5020 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (117, 'admin123', '$2a$04$sEtimsHu9YCkYY4/oqElHem2Ijc9ld20eYO6lN.g/21NfLUTDLB9W', '测试号02', '1111', 100, '[2]', '', '15601691234', 1, NULL, 0, '0:0:0:0:0:0:0:1', '2024-10-02 10:16:20', '1', '2022-07-09 17:40:26', '1', '2025-05-14 09:56:04', b'0', 1);
5056 5021 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (118, 'goudan', '$2a$04$3suGZjnA6rM5bErf38u1felbgqbsPHGdRG3l9NkxPCEt2ah9Y6aJi', '狗蛋', NULL, 103, '[1]', '', '15601691239', 1, NULL, 0, '0:0:0:0:0:0:0:1', '2025-11-23 15:28:25', '1', '2022-07-09 17:44:43', NULL, '2025-11-23 15:28:25', b'0', 1);
5057 5022 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (139, 'wwbwwb', '$2a$04$FJLIyg8lbPytP29pbZaiU.LesJvCsYfEaHqQfB0pGQhK3e9BeZmLy', '小秃头', '123', 108, '[2,4]', '', '', 1, NULL, 0, '0:0:0:0:0:0:0:1', '2024-09-10 21:03:58', NULL, '2024-09-10 21:03:58', '1', '2025-12-15 22:38:15', b'0', 1);
5058   -INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (141, 'admin1', '$2a$04$oj6F6d7HrZ70kYVD3TNzEu.m3TPUzajOVuC66zdKna8KRerK1FmVa', '新用户', NULL, NULL, NULL, '', '', 0, '', 0, '127.0.0.1', '2026-06-17 09:37:18', '1', '2025-04-08 13:09:07', NULL, '2026-06-17 09:37:18', b'0', 1);
5059   -INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (142, 'test01', '$2a$04$4bCYWZkjxxOC4QE0LY2M9uEEKWeJbLfs489NFtQoyidL5I0FndRaO', 'test01', '', NULL, '[]', '', '19021719925', 1, '', 0, '0:0:0:0:0:0:0:1', '2026-06-17 10:44:58', '1', '2025-07-09 21:07:10', NULL, '2026-06-17 10:44:58', b'0', 1);
  5023 +INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (141, 'admin1', '$2a$04$oj6F6d7HrZ70kYVD3TNzEu.m3TPUzajOVuC66zdKna8KRerK1FmVa', '新用户', NULL, NULL, NULL, '', '', 0, '', 0, '0:0:0:0:0:0:0:1', '2025-04-08 13:09:07', '1', '2025-04-08 13:09:07', '1', '2025-05-14 19:11:48', b'0', 1);
  5024 +INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (142, 'test01', '$2a$04$4bCYWZkjxxOC4QE0LY2M9uEEKWeJbLfs489NFtQoyidL5I0FndRaO', 'test01', '', NULL, '[]', '', '19021719925', 1, '', 0, '0:0:0:0:0:0:0:1', '2025-07-29 19:47:17', '1', '2025-07-09 21:07:10', NULL, '2025-12-02 13:23:11', b'0', 1);
5060 5025 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (143, 'a00001', '$2a$04$GhVHFviOw/SsTmiQtifHJesDYFlHMeGK7OWh7aGCCjGGVCmbHVAwa', 'a00001', NULL, 104, NULL, '', '', 0, '', 0, '0:0:0:0:0:0:0:1', '2025-12-01 16:10:13', NULL, '2025-12-01 16:10:13', '1', '2025-12-05 21:34:05', b'0', 1);
5061   -INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (144, 'aoteman001', '$2a$04$omQOmhz8OyUFBKw77nr8KOtMp6xdvoQ1gWStjk9r8.OYT3Bv6oEYe', 'aoteman00112', NULL, 104, NULL, '', '', 0, '', 1, '0:0:0:0:0:0:0:1', '2025-12-01 17:05:27', '1', '2025-12-01 17:05:27', '1', '2026-06-15 18:58:56', b'0', 1);
  5026 +INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (144, 'aoteman001', '$2a$04$omQOmhz8OyUFBKw77nr8KOtMp6xdvoQ1gWStjk9r8.OYT3Bv6oEYe', 'aoteman001', NULL, 104, NULL, '', '', 0, '', 1, '0:0:0:0:0:0:0:1', '2025-12-01 17:05:27', '1', '2025-12-01 17:05:27', '1', '2026-05-31 21:52:48', b'0', 1);
5062 5027 COMMIT;
5063 5028  
5064 5029 -- ----------------------------
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/conversation/ImConversationReadController.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.controller.admin.conversation;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  4 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  5 +import cn.iocoder.yudao.module.im.controller.admin.conversation.vo.ImConversationReadRespVO;
  6 +import cn.iocoder.yudao.module.im.dal.dataobject.conversation.ImConversationReadDO;
  7 +import cn.iocoder.yudao.module.im.service.conversation.ImConversationReadService;
  8 +import io.swagger.v3.oas.annotations.Operation;
  9 +import io.swagger.v3.oas.annotations.Parameter;
  10 +import io.swagger.v3.oas.annotations.Parameters;
  11 +import io.swagger.v3.oas.annotations.tags.Tag;
  12 +import jakarta.annotation.Resource;
  13 +import jakarta.validation.constraints.Max;
  14 +import jakarta.validation.constraints.Min;
  15 +import org.springframework.validation.annotation.Validated;
  16 +import org.springframework.web.bind.annotation.GetMapping;
  17 +import org.springframework.web.bind.annotation.RequestMapping;
  18 +import org.springframework.web.bind.annotation.RequestParam;
  19 +import org.springframework.web.bind.annotation.RestController;
  20 +
  21 +import java.util.List;
  22 +
  23 +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  24 +import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getLoginUserId;
  25 +
  26 +@Tag(name = "管理后台 - IM 会话读位置")
  27 +@RestController
  28 +@RequestMapping("/im/conversation-read")
  29 +@Validated
  30 +public class ImConversationReadController {
  31 +
  32 + @Resource
  33 + private ImConversationReadService conversationReadService;
  34 +
  35 + @GetMapping("/pull")
  36 + @Operation(summary = "增量拉取当前用户的会话读位置(重连 / 离线补偿)")
  37 + @Parameters({
  38 + @Parameter(name = "lastUpdateTime", description = "上次拉取到的最新更新时间(毫秒时间戳);首次拉取不传"),
  39 + @Parameter(name = "lastId", description = "上次拉取到的最后一条记录 id;首次拉取不传"),
  40 + @Parameter(name = "limit", description = "单次拉取条数", required = true)
  41 + })
  42 + public CommonResult<List<ImConversationReadRespVO>> pullMyConversationRead(
  43 + @RequestParam(value = "lastUpdateTime", required = false) Long lastUpdateTime,
  44 + @RequestParam(value = "lastId", required = false) Long lastId,
  45 + @RequestParam("limit") @Min(1) @Max(200) Integer limit) {
  46 + List<ImConversationReadDO> list = conversationReadService.pullConversationReadList(
  47 + getLoginUserId(), lastUpdateTime, lastId, limit);
  48 + return success(BeanUtils.toBean(list, ImConversationReadRespVO.class));
  49 + }
  50 +
  51 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/conversation/vo/ImConversationReadRespVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.controller.admin.conversation.vo;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import lombok.Data;
  5 +
  6 +import java.time.LocalDateTime;
  7 +
  8 +/**
  9 + * IM 会话读位置 Response VO
  10 + *
  11 + * @author 芋道源码
  12 + */
  13 +@Schema(description = "管理后台 - IM 会话读位置 Response VO")
  14 +@Data
  15 +public class ImConversationReadRespVO {
  16 +
  17 + @Schema(description = "读位置编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
  18 + private Long id;
  19 +
  20 + @Schema(description = "会话类型", example = "1")
  21 + private Integer conversationType; // 参见 ImConversationTypeEnum 枚举
  22 +
  23 + @Schema(description = "目标编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
  24 + private Long targetId;
  25 +
  26 + @Schema(description = "最大已读消息编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "4096")
  27 + private Long messageId;
  28 +
  29 + @Schema(description = "最近更新时间(增量拉取游标用)")
  30 + private LocalDateTime updateTime;
  31 +
  32 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/ImFriendController.java
... ... @@ -17,6 +17,8 @@ import io.swagger.v3.oas.annotations.Parameters;
17 17 import io.swagger.v3.oas.annotations.tags.Tag;
18 18 import jakarta.annotation.Resource;
19 19 import jakarta.validation.Valid;
  20 +import jakarta.validation.constraints.Max;
  21 +import jakarta.validation.constraints.Min;
20 22 import jakarta.validation.constraints.NotNull;
21 23 import org.springframework.validation.annotation.Validated;
22 24 import org.springframework.web.bind.annotation.*;
... ... @@ -46,11 +48,25 @@ public class ImFriendController {
46 48 @GetMapping("/list")
47 49 @Operation(summary = "获得当前登录用户的好友列表")
48 50 public CommonResult<List<ImFriendRespVO>> getMyFriendList() {
49   - // 含 DISABLE 历史好友:保留给前端展示「已删除好友」的历史对话信息;前端按 status 决定会话级联清理
50 51 List<ImFriendDO> friends = friendService.getFriendList(getLoginUserId());
51 52 return success(buildFriendRespVOList(friends));
52 53 }
53 54  
  55 + @GetMapping("/pull")
  56 + @Operation(summary = "增量拉取当前用户的好友关系(重连 / 离线补偿)")
  57 + @Parameters({
  58 + @Parameter(name = "lastUpdateTime", description = "上次拉取到的最新更新时间(毫秒时间戳);首次拉取不传"),
  59 + @Parameter(name = "lastId", description = "上次拉取到的最后一条记录 id;首次拉取不传"),
  60 + @Parameter(name = "limit", description = "单次拉取条数", required = true)
  61 + })
  62 + public CommonResult<List<ImFriendRespVO>> pullMyFriendList(
  63 + @RequestParam(value = "lastUpdateTime", required = false) Long lastUpdateTime,
  64 + @RequestParam(value = "lastId", required = false) Long lastId,
  65 + @RequestParam("limit") @Min(1) @Max(200) Integer limit) {
  66 + List<ImFriendDO> list = friendService.pullFriendList(getLoginUserId(), lastUpdateTime, lastId, limit);
  67 + return success(buildFriendRespVOList(list));
  68 + }
  69 +
54 70 @GetMapping("/get")
55 71 @Operation(summary = "获得好友详情")
56 72 @Parameter(name = "friendUserId", description = "好友的用户编号", required = true, example = "2048")
... ... @@ -62,8 +78,8 @@ public class ImFriendController {
62 78 @DeleteMapping("/delete")
63 79 @Operation(summary = "删除好友(单向软删除)")
64 80 @Parameters({
65   - @Parameter(description = "好友的用户编号", required = true, example = "2048"),
66   - @Parameter(description = "是否级联清理本端相关数据(如私聊会话)")
  81 + @Parameter(name = "friendUserId", description = "好友的用户编号", required = true, example = "2048"),
  82 + @Parameter(name = "clear", description = "是否级联清理本端相关数据(如私聊会话)")
67 83 })
68 84 public CommonResult<Boolean> deleteFriend(
69 85 @RequestParam("friendUserId") @NotNull(message = "好友用户编号不能为空") Long friendUserId,
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/ImFriendRequestController.java
... ... @@ -13,6 +13,7 @@ import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
13 13 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
14 14 import io.swagger.v3.oas.annotations.Operation;
15 15 import io.swagger.v3.oas.annotations.Parameter;
  16 +import io.swagger.v3.oas.annotations.Parameters;
16 17 import io.swagger.v3.oas.annotations.tags.Tag;
17 18 import jakarta.annotation.Resource;
18 19 import jakarta.validation.Valid;
... ... @@ -79,15 +80,32 @@ public class ImFriendRequestController {
79 80  
80 81 @GetMapping("/list")
81 82 @Operation(summary = "查询「我相关」的好友申请列表(游标分页:传 maxId 加载更多)")
  83 + @Parameters({
  84 + @Parameter(name = "maxId", description = "当前列表最旧记录的 id;首页不传"),
  85 + @Parameter(name = "limit", description = "单次拉取条数", required = true)
  86 + })
82 87 public CommonResult<List<ImFriendRequestRespVO>> getMyFriendRequestList(
83   - @Parameter(description = "当前列表最旧记录的 id;首页不传")
84 88 @RequestParam(value = "maxId", required = false) Long maxId,
85   - @Parameter(description = "单次拉取条数", required = true)
86 89 @RequestParam("limit") @Min(1) @Max(200) Integer limit) {
87 90 List<ImFriendRequestDO> list = friendRequestService.getMyFriendRequestList(getLoginUserId(), maxId, limit);
88 91 return success(buildList(list));
89 92 }
90 93  
  94 + @GetMapping("/pull")
  95 + @Operation(summary = "增量拉取「我相关」的好友申请(重连 / 离线补偿)")
  96 + @Parameters({
  97 + @Parameter(name = "lastUpdateTime", description = "上次拉取到的最新更新时间(毫秒时间戳);首次拉取不传"),
  98 + @Parameter(name = "lastId", description = "上次拉取到的最后一条记录 id;首次拉取不传"),
  99 + @Parameter(name = "limit", description = "单次拉取条数", required = true)
  100 + })
  101 + public CommonResult<List<ImFriendRequestRespVO>> pullMyFriendRequestList(
  102 + @RequestParam(value = "lastUpdateTime", required = false) Long lastUpdateTime,
  103 + @RequestParam(value = "lastId", required = false) Long lastId,
  104 + @RequestParam("limit") @Min(1) @Max(200) Integer limit) {
  105 + List<ImFriendRequestDO> list = friendRequestService.pullFriendRequestList(getLoginUserId(), lastUpdateTime, lastId, limit);
  106 + return success(buildList(list));
  107 + }
  108 +
91 109 @GetMapping("/get")
92 110 @Operation(summary = "按 id 单查「我相关」的申请记录(带越权过滤;WebSocket 通知到达后用)")
93 111 @Parameter(name = "id", description = "申请记录编号", required = true)
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/vo/ImFriendRespVO.java
... ... @@ -47,6 +47,9 @@ public class ImFriendRespVO {
47 47 @Schema(description = "删除好友时间")
48 48 private LocalDateTime deleteTime;
49 49  
  50 + @Schema(description = "最近更新时间(增量拉取游标用)")
  51 + private LocalDateTime updateTime;
  52 +
50 53 // ========== 下面是聚合字段,方便前端显示 ==========
51 54  
52 55 @Schema(description = "好友昵称(实时聚合自 AdminUser)", example = "芋道")
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/vo/request/ImFriendRequestRespVO.java
... ... @@ -41,6 +41,9 @@ public class ImFriendRequestRespVO {
41 41 @Schema(description = "申请创建时间")
42 42 private LocalDateTime createTime;
43 43  
  44 + @Schema(description = "最近更新时间(增量拉取游标用)")
  45 + private LocalDateTime updateTime;
  46 +
44 47 // ========== 下面是聚合字段,方便前端显示 ==========
45 48  
46 49 @Schema(description = "发起方昵称(实时聚合自 AdminUser)", example = "芋道")
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupController.java
... ... @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.im.controller.admin.group;
3 3 import cn.hutool.core.collection.CollUtil;
4 4 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
5 5 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  6 +import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
6 7 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
7 8 import cn.iocoder.yudao.module.im.controller.admin.group.vo.*;
8 9 import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberInviteReqVO;
... ... @@ -187,8 +188,10 @@ public class ImGroupController {
187 188 return Collections.emptyList();
188 189 }
189 190 // 仅当前用户是有效成员的群才允许回填置顶消息
190   - Set<Long> activeGroupIds = convertSet(
191   - groupMemberService.getActiveGroupMemberListByUserId(loginUserId), ImGroupMemberDO::getGroupId);
  191 + Map<Long, ImGroupMemberDO> memberMap = convertMap(
  192 + groupMemberService.getGroupMemberListByUserId(loginUserId), ImGroupMemberDO::getGroupId);
  193 + Set<Long> activeGroupIds = convertSet(memberMap.values(), ImGroupMemberDO::getGroupId,
  194 + member -> CommonStatusEnum.ENABLE.getStatus().equals(member.getStatus()));
192 195 Set<Long> allMessageIds = convertSetByFlatMap(groups, group -> activeGroupIds.contains(group.getId())
193 196 ? CollUtil.emptyIfNull(group.getPinnedMessageIds()).stream() : Stream.empty());
194 197 Map<Long, ImGroupMessageDO> messageMap = groupMessageService.getGroupMessageMap(allMessageIds);
... ... @@ -198,6 +201,8 @@ public class ImGroupController {
198 201 // 标记登录用户在该群的成员状态,供前端区分当前群与历史退群
199 202 boolean joined = activeGroupIds.contains(group.getId());
200 203 groupVO.setJoinStatus(joined ? CommonStatusEnum.ENABLE.getStatus() : CommonStatusEnum.DISABLE.getStatus());
  204 + MapUtils.findAndThen(memberMap, group.getId(), member ->
  205 + groupVO.setGroupRemark(member.getGroupRemark()).setSilent(Boolean.TRUE.equals(member.getSilent())));
201 206 if (!joined || CollUtil.isEmpty(group.getPinnedMessageIds())) {
202 207 return groupVO;
203 208 }
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupMemberController.java
... ... @@ -21,6 +21,7 @@ import jakarta.validation.Valid;
21 21 import org.springframework.validation.annotation.Validated;
22 22 import org.springframework.web.bind.annotation.*;
23 23  
  24 +import java.util.Collections;
24 25 import java.util.List;
25 26 import java.util.Map;
26 27  
... ... @@ -88,7 +89,7 @@ public class ImGroupMemberController {
88 89 }
89 90  
90 91 @GetMapping("/list")
91   - @Operation(summary = "获得指定群的成员列表")
  92 + @Operation(summary = "获得指定群的成员列表(按群全量拉取,作为前端本地成员 cache 的完整基线)")
92 93 @Parameter(name = "groupId", description = "群编号", required = true, example = "1024")
93 94 public CommonResult<List<ImGroupMemberRespVO>> getGroupMemberList(@RequestParam("groupId") Long groupId) {
94 95 // 1.1 查询群成员列表(包含 DISABLE 已退群的成员,不按时间过滤)
... ... @@ -101,16 +102,30 @@ public class ImGroupMemberController {
101 102 throw exception(GROUP_MEMBER_NOT_IN_GROUP);
102 103 }
103 104  
104   - // 2.批量聚合 AdminUser 信息(昵称 / 头像)
  105 + // 2. 批量聚合昵称 / 头像,并对非本人置空私人字段
  106 + return success(buildGroupMemberRespVOList(members));
  107 + }
  108 +
  109 + // ========== 私有方法:VO 组装 ==========
  110 +
  111 + /**
  112 + * 群成员列表批量转 VO + 关联回填昵称 / 头像,并对非本人置空私人字段
  113 + */
  114 + private List<ImGroupMemberRespVO> buildGroupMemberRespVOList(List<ImGroupMemberDO> members) {
  115 + if (CollUtil.isEmpty(members)) {
  116 + return Collections.emptyList();
  117 + }
  118 + Long loginUserId = getLoginUserId();
  119 + // 批量聚合 AdminUser 信息(昵称 / 头像),避免 N+1
105 120 Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
106 121 convertList(members, ImGroupMemberDO::getUserId));
107   - return success(convertList(members, m -> {
108   - ImGroupMemberRespVO vo = BeanUtils.toBean(m, ImGroupMemberRespVO.class);
109   - MapUtils.findAndThen(userMap, m.getUserId(), user ->
  122 + return convertList(members, member -> {
  123 + ImGroupMemberRespVO vo = BeanUtils.toBean(member, ImGroupMemberRespVO.class);
  124 + MapUtils.findAndThen(userMap, member.getUserId(), user ->
110 125 vo.setNickname(user.getNickname()).setAvatar(user.getAvatar()));
111   - hidePrivateFieldsIfNotSelf(vo, m.getUserId(), loginUserId);
  126 + hidePrivateFieldsIfNotSelf(vo, member.getUserId(), loginUserId);
112 127 return vo;
113   - }));
  128 + });
114 129 }
115 130  
116 131 /**
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupRequestController.java
... ... @@ -19,9 +19,12 @@ import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
19 19 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
20 20 import io.swagger.v3.oas.annotations.Operation;
21 21 import io.swagger.v3.oas.annotations.Parameter;
  22 +import io.swagger.v3.oas.annotations.Parameters;
22 23 import io.swagger.v3.oas.annotations.tags.Tag;
23 24 import jakarta.annotation.Resource;
24 25 import jakarta.validation.Valid;
  26 +import jakarta.validation.constraints.Max;
  27 +import jakarta.validation.constraints.Min;
25 28 import jakarta.validation.constraints.NotNull;
26 29 import jakarta.validation.constraints.Size;
27 30 import org.springframework.validation.annotation.Validated;
... ... @@ -88,6 +91,22 @@ public class ImGroupRequestController {
88 91 return success(buildVOList(list));
89 92 }
90 93  
  94 + @GetMapping("/pull")
  95 + @Operation(summary = "增量拉取「我管理的群」下的加群申请(重连 / 离线补偿)")
  96 + @Parameters({
  97 + @Parameter(name = "lastUpdateTime", description = "上次拉取到的最新更新时间(毫秒时间戳);首次拉取不传"),
  98 + @Parameter(name = "lastId", description = "上次拉取到的最后一条记录 id;首次拉取不传"),
  99 + @Parameter(name = "limit", description = "单次拉取条数", required = true)
  100 + })
  101 + public CommonResult<List<ImGroupRequestRespVO>> pullMyGroupRequestList(
  102 + @RequestParam(value = "lastUpdateTime", required = false) Long lastUpdateTime,
  103 + @RequestParam(value = "lastId", required = false) Long lastId,
  104 + @RequestParam("limit") @Min(1) @Max(200) Integer limit) {
  105 + List<ImGroupRequestDO> list = groupRequestService.pullGroupRequestList(
  106 + getLoginUserId(), lastUpdateTime, lastId, limit);
  107 + return success(buildVOList(list));
  108 + }
  109 +
91 110 @GetMapping("/list-by-group")
92 111 @Operation(summary = "查询指定群下的全部加群申请(含已处理);仅群主 / 管理员可查")
93 112 @Parameter(name = "groupId", description = "群编号", required = true, example = "1024")
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupRespVO.java
... ... @@ -50,6 +50,12 @@ public class ImGroupRespVO {
50 50 @Schema(description = "当前登录用户在该群的成员状态:在群 / 已退群(历史退群群仍返回,供前端展示离线消息的群名 / 头像)")
51 51 private Integer joinStatus; // 参见 CommonStatusEnum 枚举
52 52  
  53 + @Schema(description = "当前登录用户对该群的备注")
  54 + private String groupRemark;
  55 +
  56 + @Schema(description = "当前登录用户是否免打扰")
  57 + private Boolean silent;
  58 +
53 59 @Schema(description = "群置顶消息列表,按 pin 顺序(最先置顶的在前);非该群有效成员时为空")
54 60 private List<ImGroupMessageRespVO> pinnedMessages;
55 61  
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/request/ImGroupRequestRespVO.java
... ... @@ -42,6 +42,9 @@ public class ImGroupRequestRespVO {
42 42 @Schema(description = "申请创建时间")
43 43 private LocalDateTime createTime;
44 44  
  45 + @Schema(description = "申请更新时间;增量拉取游标")
  46 + private LocalDateTime updateTime;
  47 +
45 48 // ========== 下面是聚合字段,方便前端显示 ==========
46 49  
47 50 @Schema(description = "申请人 / 被邀请人昵称(实时聚合自 AdminUser)", example = "芋道")
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/channel/ImChannelManagerController.java
... ... @@ -75,7 +75,6 @@ public class ImChannelManagerController {
75 75 @GetMapping("/simple-list")
76 76 @Operation(summary = "获得启用的频道精简列表;前端表单选择频道时调用")
77 77 public CommonResult<List<ImChannelRespVO>> getSimpleChannelList() {
78   - // TODO DONE @AI:getChannelListByStatus 统一命名
79 78 List<ImChannelDO> list = channelService.getChannelListByStatus(CommonStatusEnum.ENABLE.getStatus());
80 79 return success(BeanUtils.toBean(list, ImChannelRespVO.class));
81 80 }
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/message/vo/channel/ImChannelMessageRespVO.java
... ... @@ -29,7 +29,7 @@ public class ImChannelMessageRespVO {
29 29 private String materialCoverUrl;
30 30  
31 31 @Schema(description = "消息类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "125")
32   - private Integer type; // 参见 ImMessageTypeEnum 枚举类
  32 + private Integer type; // 参见 ImContentTypeEnum 枚举类
33 33  
34 34 @Schema(description = "消息内容;payload JSON 快照")
35 35 private String content;
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/message/vo/group/ImGroupMessageManagerPageReqVO.java
... ... @@ -22,7 +22,7 @@ public class ImGroupMessageManagerPageReqVO extends PageParam {
22 22 private Long senderId;
23 23  
24 24 @Schema(description = "消息类型", example = "1")
25   - private Integer type; // 参见 ImMessageTypeEnum 枚举类
  25 + private Integer type; // 参见 ImContentTypeEnum 枚举类
26 26  
27 27 @Schema(description = "消息内容", example = "你好")
28 28 private String content;
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/message/vo/group/ImGroupMessageManagerRespVO.java
... ... @@ -29,7 +29,7 @@ public class ImGroupMessageManagerRespVO {
29 29 private String senderNickname;
30 30  
31 31 @Schema(description = "消息类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
32   - private Integer type; // 参见 ImMessageTypeEnum 枚举类
  32 + private Integer type; // 参见 ImContentTypeEnum 枚举类
33 33  
34 34 @Schema(description = "消息内容(JSON 格式)", requiredMode = Schema.RequiredMode.REQUIRED)
35 35 private String content;
... ... @@ -43,7 +43,7 @@ public class ImGroupMessageManagerRespVO {
43 43 private List<String> atUserNicknames;
44 44  
45 45 @Schema(description = "回执状态", example = "0")
46   - private Integer receiptStatus; // 参见 ImGroupMessageReceiptStatusEnum 枚举类
  46 + private Integer receiptStatus; // 参见 ImMessageReceiptStatusEnum 枚举类
47 47  
48 48 @Schema(description = "发送时间", requiredMode = Schema.RequiredMode.REQUIRED)
49 49 private LocalDateTime sendTime;
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/message/vo/privates/ImPrivateMessageManagerPageReqVO.java
... ... @@ -22,7 +22,7 @@ public class ImPrivateMessageManagerPageReqVO extends PageParam {
22 22 private Long receiverId;
23 23  
24 24 @Schema(description = "消息类型", example = "1")
25   - private Integer type; // 参见 ImMessageTypeEnum 枚举类
  25 + private Integer type; // 参见 ImContentTypeEnum 枚举类
26 26  
27 27 @Schema(description = "消息内容", example = "你好")
28 28 private String content;
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/message/vo/privates/ImPrivateMessageManagerRespVO.java
... ... @@ -28,7 +28,7 @@ public class ImPrivateMessageManagerRespVO {
28 28 private String receiverNickname;
29 29  
30 30 @Schema(description = "消息类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
31   - private Integer type; // 参见 ImMessageTypeEnum 枚举类
  31 + private Integer type; // 参见 ImContentTypeEnum 枚举类
32 32  
33 33 @Schema(description = "消息内容(JSON 格式)", requiredMode = Schema.RequiredMode.REQUIRED)
34 34 private String content;
... ... @@ -36,6 +36,9 @@ public class ImPrivateMessageManagerRespVO {
36 36 @Schema(description = "消息状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
37 37 private Integer status; // 参见 ImMessageStatusEnum 枚举类
38 38  
  39 + @Schema(description = "回执状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
  40 + private Integer receiptStatus; // 参见 ImMessageReceiptStatusEnum 枚举类
  41 +
39 42 @Schema(description = "发送时间", requiredMode = Schema.RequiredMode.REQUIRED)
40 43 private LocalDateTime sendTime;
41 44  
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/statistics/ImStatisticsManagerController.java
... ... @@ -112,7 +112,7 @@ public class ImStatisticsManagerController {
112 112 }
113 113  
114 114 @GetMapping("/message-type-distribution")
115   - @Operation(summary = "获得消息类型分布(最近 30 天)")
  115 + @Operation(summary = "获得内容类型分布(最近 30 天)")
116 116 @PreAuthorize("@ss.hasPermission('im:manager:statistics:query')")
117 117 public CommonResult<List<ImStatisticsManagerMessageTypeRespVO>> getMessageTypeDistribution() {
118 118 LocalDateTime endTime = LocalDate.now().plusDays(1).atStartOfDay();
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/statistics/vo/ImStatisticsManagerMessageTypeRespVO.java
... ... @@ -3,12 +3,12 @@ package cn.iocoder.yudao.module.im.controller.admin.manager.statistics.vo;
3 3 import io.swagger.v3.oas.annotations.media.Schema;
4 4 import lombok.Data;
5 5  
6   -@Schema(description = "管理后台 - IM 数据看板消息类型分布项 Response VO")
  6 +@Schema(description = "管理后台 - IM 数据看板内容类型分布项 Response VO")
7 7 @Data
8 8 public class ImStatisticsManagerMessageTypeRespVO {
9 9  
10 10 @Schema(description = "消息类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
11   - private Integer type; // 参见 ImMessageTypeEnum 枚举类
  11 + private Integer type; // 参见 ImContentTypeEnum 枚举类
12 12  
13 13 @Schema(description = "消息数", requiredMode = Schema.RequiredMode.REQUIRED, example = "8000")
14 14 private Long value;
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/ImChannelMessageController.java
... ... @@ -5,7 +5,7 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult;
5 5 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
6 6 import cn.iocoder.yudao.module.im.controller.admin.message.vo.channel.ImChannelMessagePullRespVO;
7 7 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImChannelMessageDO;
8   -import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum;
  8 +import cn.iocoder.yudao.module.im.enums.message.ImMessageReceiptStatusEnum;
9 9 import cn.iocoder.yudao.module.im.service.message.ImChannelMessageService;
10 10 import io.swagger.v3.oas.annotations.Operation;
11 11 import io.swagger.v3.oas.annotations.Parameter;
... ... @@ -40,25 +40,25 @@ public class ImChannelMessageController {
40 40  
41 41 @GetMapping("/pull")
42 42 @Operation(summary = "拉取频道消息(离线增量);按 minId 游标分页")
43   - public CommonResult<List<ImChannelMessagePullRespVO>> pull(
  43 + public CommonResult<List<ImChannelMessagePullRespVO>> pullChannelMessageList(
44 44 @RequestParam(value = "minId", defaultValue = "0") @PositiveOrZero(message = "minId 不能小于 0") Long minId,
45 45 @RequestParam(value = "size", defaultValue = "100")
46 46 @Min(value = 1, message = "size 必须大于 0")
47 47 @Max(value = 200, message = "size 一次最多 200 条") Integer size) {
48 48 // 1. 拉取消息列表
49 49 Long userId = getLoginUserId();
50   - List<ImChannelMessageDO> list = channelMessageService.getMessageListForPull(userId, minId, size);
  50 + List<ImChannelMessageDO> list = channelMessageService.pullChannelMessageList(userId, minId, size);
51 51 if (CollUtil.isEmpty(list)) {
52 52 return success(Collections.emptyList());
53 53 }
54   - // 2. 按 Redis 已读游标补 status;device A 已读后 device B 拉到这条不再算入未读
  54 + // 2. 按已读游标补 receiptStatus(已读 DONE / 未读 PENDING)
55 55 Map<Long, Long> readMaxByChannel = channelMessageService.getChannelReadMaxMessageIdMap(
56 56 userId, convertSet(list, ImChannelMessageDO::getChannelId));
57 57 return success(BeanUtils.toBean(list, ImChannelMessagePullRespVO.class, vo -> {
58 58 Long readMax = readMaxByChannel.get(vo.getChannelId());
59   - vo.setStatus(readMax != null && readMax >= vo.getId()
60   - ? ImMessageStatusEnum.READ.getStatus()
61   - : ImMessageStatusEnum.UNREAD.getStatus());
  59 + vo.setReceiptStatus(readMax != null && readMax >= vo.getId()
  60 + ? ImMessageReceiptStatusEnum.DONE.getStatus()
  61 + : ImMessageReceiptStatusEnum.PENDING.getStatus());
62 62 }));
63 63 }
64 64  
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/ImPrivateMessageController.java
... ... @@ -61,7 +61,7 @@ public class ImPrivateMessageController {
61 61  
62 62 @GetMapping("/max-read-message-id")
63 63 @Operation(summary = "查询对方已读到我发的最大消息 id",
64   - description = "用于多端 / 离线场景下的已读位置补齐:进入会话或断线重连后调用,结果用于翻转本地自发消息状态")
  64 + description = "用于多端 / 离线场景下的已读位置补齐:进入会话或断线重连后调用,据此把本地自发消息更新为已读")
65 65 @Parameter(name = "peerId", description = "对方用户编号", required = true, example = "2")
66 66 public CommonResult<Long> getMaxReadMessageId(@RequestParam("peerId") Long peerId) {
67 67 return success(privateMessageService.getMaxReadMessageId(getLoginUserId(), peerId));
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/vo/channel/ImChannelMessagePullRespVO.java
... ... @@ -24,8 +24,8 @@ public class ImChannelMessagePullRespVO {
24 24 @Schema(description = "消息内容;payload JSON 快照", requiredMode = Schema.RequiredMode.REQUIRED)
25 25 private String content;
26 26  
27   - @Schema(description = "当前用户的已读态;按 Redis 游标计算填充", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
28   - private Integer status; // 参见 ImMessageStatusEnum 枚举类
  27 + @Schema(description = "当前用户回执 / 已读态;按 Redis 读位置计算(已读 DONE,未读 PENDING)", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
  28 + private Integer receiptStatus; // 参见 ImMessageReceiptStatusEnum 枚举类
29 29  
30 30 @Schema(description = "发送时间", requiredMode = Schema.RequiredMode.REQUIRED)
31 31 private LocalDateTime sendTime;
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/vo/group/ImGroupMessageSendReqVO.java
1 1 package cn.iocoder.yudao.module.im.controller.admin.message.vo.group;
2 2  
3 3 import cn.iocoder.yudao.framework.common.validation.InEnum;
4   -import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
  4 +import cn.iocoder.yudao.module.im.enums.ImContentTypeEnum;
5 5 import com.fasterxml.jackson.annotation.JsonIgnore;
6 6 import io.swagger.v3.oas.annotations.media.Schema;
7 7 import jakarta.validation.constraints.AssertTrue;
... ... @@ -28,7 +28,7 @@ public class ImGroupMessageSendReqVO {
28 28  
29 29 @Schema(description = "消息类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
30 30 @NotNull(message = "消息类型不能为空")
31   - @InEnum(ImMessageTypeEnum.class)
  31 + @InEnum(ImContentTypeEnum.class)
32 32 private Integer type;
33 33  
34 34 @Schema(description = "消息内容,JSON 格式", requiredMode = Schema.RequiredMode.REQUIRED, example = "{\"content\":\"你好\"}")
... ... @@ -47,7 +47,7 @@ public class ImGroupMessageSendReqVO {
47 47 @AssertTrue(message = "消息类型不允许")
48 48 @JsonIgnore
49 49 public boolean isTypeNormal() {
50   - return type == null || ImMessageTypeEnum.validate(type).isNormal();
  50 + return type == null || ImContentTypeEnum.validate(type).isNormal();
51 51 }
52 52  
53 53 }
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/vo/privates/ImPrivateMessageRespVO.java
... ... @@ -31,7 +31,10 @@ public class ImPrivateMessageRespVO {
31 31 private String content;
32 32  
33 33 @Schema(description = "消息状态", example = "0")
34   - private Integer status;
  34 + private Integer status; // 参见 ImMessageStatusEnum 枚举类
  35 +
  36 + @Schema(description = "回执状态", example = "0")
  37 + private Integer receiptStatus; // 参见 ImMessageReceiptStatusEnum 枚举类
35 38  
36 39 @Schema(description = "发送时间")
37 40 private LocalDateTime sendTime;
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/vo/privates/ImPrivateMessageSendReqVO.java
1 1 package cn.iocoder.yudao.module.im.controller.admin.message.vo.privates;
2 2  
3 3 import cn.iocoder.yudao.framework.common.validation.InEnum;
4   -import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
  4 +import cn.iocoder.yudao.module.im.enums.ImContentTypeEnum;
5 5 import com.fasterxml.jackson.annotation.JsonIgnore;
6 6 import io.swagger.v3.oas.annotations.media.Schema;
7 7 import jakarta.validation.constraints.AssertTrue;
... ... @@ -26,20 +26,23 @@ public class ImPrivateMessageSendReqVO {
26 26  
27 27 @Schema(description = "消息类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
28 28 @NotNull(message = "消息类型不能为空")
29   - @InEnum(ImMessageTypeEnum.class)
  29 + @InEnum(ImContentTypeEnum.class)
30 30 private Integer type;
31 31  
32 32 @Schema(description = "消息内容,JSON 格式", requiredMode = Schema.RequiredMode.REQUIRED, example = "{\"content\":\"你好\"}")
33 33 @NotEmpty(message = "消息内容不能为空")
34 34 private String content;
35 35  
  36 + @Schema(description = "是否需要回执", example = "false")
  37 + private Boolean receipt;
  38 +
36 39 /**
37 40 * 仅允许用户消息(normal)类型
38 41 */
39 42 @AssertTrue(message = "消息类型不允许")
40 43 @JsonIgnore
41 44 public boolean isTypeNormal() {
42   - return type == null || ImMessageTypeEnum.validate(type).isNormal();
  45 + return type == null || ImContentTypeEnum.validate(type).isNormal();
43 46 }
44 47  
45 48 }
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/conversation/ImConversationReadDO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.dal.dataobject.conversation;
  2 +
  3 +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
  4 +import cn.iocoder.yudao.module.im.dal.dataobject.channel.ImChannelDO;
  5 +import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO;
  6 +import cn.iocoder.yudao.module.im.dal.dataobject.message.ImChannelMessageDO;
  7 +import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO;
  8 +import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO;
  9 +import cn.iocoder.yudao.module.im.enums.ImConversationTypeEnum;
  10 +import com.baomidou.mybatisplus.annotation.KeySequence;
  11 +import com.baomidou.mybatisplus.annotation.TableId;
  12 +import com.baomidou.mybatisplus.annotation.TableName;
  13 +import lombok.*;
  14 +
  15 +import java.time.LocalDateTime;
  16 +
  17 +/**
  18 + * IM 会话读位置 DO
  19 + * <p>
  20 + * 只表达「用户在某个会话的最大已读位置」,私聊 / 群聊 / 频道统一落这张表,是读位置的唯一权威。
  21 + *
  22 + * @author 芋道源码
  23 + */
  24 +@TableName("im_conversation_read")
  25 +@KeySequence("im_conversation_read_seq")
  26 +@Data
  27 +@EqualsAndHashCode(callSuper = true)
  28 +@ToString(callSuper = true)
  29 +@Builder
  30 +@NoArgsConstructor
  31 +@AllArgsConstructor
  32 +public class ImConversationReadDO extends BaseDO {
  33 +
  34 + /**
  35 + * 编号
  36 + */
  37 + @TableId
  38 + private Long id;
  39 + /**
  40 + * 用户编号
  41 + *
  42 + * 关联 AdminUserDO 的 id 字段
  43 + */
  44 + private Long userId;
  45 + /**
  46 + * 会话类型
  47 + * <p>
  48 + * 枚举 {@link ImConversationTypeEnum}
  49 + */
  50 + private Integer conversationType;
  51 + /**
  52 + * 目标编号
  53 + * <p>
  54 + * 私聊关联 AdminUserDO 的 id;群聊关联 {@link ImGroupDO#getId()};频道关联 {@link ImChannelDO#getId()}
  55 + */
  56 + private Long targetId;
  57 + /**
  58 + * 最大已读消息编号
  59 + * <p>
  60 + * 关联 {@link ImPrivateMessageDO#getId()}、{@link ImGroupMessageDO#getId()}、{@link ImChannelMessageDO#getId()}
  61 + */
  62 + private Long messageId;
  63 + /**
  64 + * 最近已读时间
  65 + */
  66 + private LocalDateTime readTime;
  67 +
  68 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/ImChannelMessageDO.java
... ... @@ -4,7 +4,7 @@ import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
4 4 import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler;
5 5 import cn.iocoder.yudao.module.im.dal.dataobject.channel.ImChannelDO;
6 6 import cn.iocoder.yudao.module.im.dal.dataobject.channel.ImChannelMaterialDO;
7   -import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
  7 +import cn.iocoder.yudao.module.im.enums.ImContentTypeEnum;
8 8 import com.baomidou.mybatisplus.annotation.KeySequence;
9 9 import com.baomidou.mybatisplus.annotation.TableField;
10 10 import com.baomidou.mybatisplus.annotation.TableId;
... ... @@ -54,7 +54,7 @@ public class ImChannelMessageDO extends BaseDO {
54 54 /**
55 55 * 消息类型
56 56 * <p>
57   - * 枚举 {@link ImMessageTypeEnum}
  57 + * 枚举 {@link ImContentTypeEnum}
58 58 */
59 59 private Integer type;
60 60 /**
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/ImGroupMessageDO.java
... ... @@ -3,9 +3,9 @@ package cn.iocoder.yudao.module.im.dal.dataobject.message;
3 3 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
4 4 import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler;
5 5 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO;
6   -import cn.iocoder.yudao.module.im.enums.message.ImGroupMessageReceiptStatusEnum;
  6 +import cn.iocoder.yudao.module.im.enums.message.ImMessageReceiptStatusEnum;
7 7 import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum;
8   -import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
  8 +import cn.iocoder.yudao.module.im.enums.ImContentTypeEnum;
9 9 import com.baomidou.mybatisplus.annotation.KeySequence;
10 10 import com.baomidou.mybatisplus.annotation.TableField;
11 11 import com.baomidou.mybatisplus.annotation.TableId;
... ... @@ -54,7 +54,7 @@ public class ImGroupMessageDO extends BaseDO {
54 54 /**
55 55 * 消息类型
56 56 * <p>
57   - * 枚举 {@link ImMessageTypeEnum}
  57 + * 枚举 {@link ImContentTypeEnum}
58 58 */
59 59 private Integer type;
60 60 /**
... ... @@ -67,8 +67,6 @@ public class ImGroupMessageDO extends BaseDO {
67 67 * 消息状态
68 68 * <p>
69 69 * 枚举 {@link ImMessageStatusEnum}
70   - *
71   - * 为什么没有 READ 状态?与单聊的差异:单聊用 UNREAD/READ 跟踪每条消息状态,群聊用 Redis 存储每个成员的已读位置(游标模型)
72 70 */
73 71 private Integer status;
74 72 /**
... ... @@ -94,7 +92,7 @@ public class ImGroupMessageDO extends BaseDO {
94 92 /**
95 93 * 回执状态
96 94 * <p>
97   - * 枚举 {@link ImGroupMessageReceiptStatusEnum}
  95 + * 枚举 {@link ImMessageReceiptStatusEnum}
98 96 */
99 97 private Integer receiptStatus;
100 98  
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/ImPrivateMessageDO.java
1 1 package cn.iocoder.yudao.module.im.dal.dataobject.message;
2 2  
3 3 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
  4 +import cn.iocoder.yudao.module.im.enums.message.ImMessageReceiptStatusEnum;
4 5 import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum;
5   -import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
  6 +import cn.iocoder.yudao.module.im.enums.ImContentTypeEnum;
6 7 import com.baomidou.mybatisplus.annotation.KeySequence;
7 8 import com.baomidou.mybatisplus.annotation.TableId;
8 9 import com.baomidou.mybatisplus.annotation.TableName;
... ... @@ -49,7 +50,7 @@ public class ImPrivateMessageDO extends BaseDO {
49 50 /**
50 51 * 消息类型
51 52 * <p>
52   - * 枚举 {@link ImMessageTypeEnum}
  53 + * 枚举 {@link ImContentTypeEnum}
53 54 */
54 55 private Integer type;
55 56 /**
... ... @@ -65,6 +66,12 @@ public class ImPrivateMessageDO extends BaseDO {
65 66 */
66 67 private Integer status;
67 68 /**
  69 + * 回执状态
  70 + * <p>
  71 + * 枚举 {@link ImMessageReceiptStatusEnum}
  72 + */
  73 + private Integer receiptStatus;
  74 + /**
68 75 * 发送时间
69 76 */
70 77 private LocalDateTime sendTime;
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/AudioMessage.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/AudioMessage.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.message;
  1 +package cn.iocoder.yudao.module.im.dal.dataobject.message.content;
2 2  
3 3 import lombok.Data;
4 4  
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/CardMessage.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/CardMessage.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.message;
  1 +package cn.iocoder.yudao.module.im.dal.dataobject.message.content;
2 2  
3 3 import cn.iocoder.yudao.module.im.enums.ImConversationTypeEnum;
4 4 import lombok.Data;
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/FaceMessage.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/FaceMessage.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.message;
  1 +package cn.iocoder.yudao.module.im.dal.dataobject.message.content;
2 2  
3 3 import lombok.Data;
4 4  
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/FileMessage.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/FileMessage.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.message;
  1 +package cn.iocoder.yudao.module.im.dal.dataobject.message.content;
2 2  
3 3 import lombok.Data;
4 4  
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/ImageMessage.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/ImageMessage.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.message;
  1 +package cn.iocoder.yudao.module.im.dal.dataobject.message.content;
2 2  
3 3 import lombok.Data;
4 4  
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/LocationMessage.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/LocationMessage.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.message;
  1 +package cn.iocoder.yudao.module.im.dal.dataobject.message.content;
2 2  
3 3 import lombok.Data;
4 4  
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/MaterialMessage.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/MaterialMessage.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.message;
  1 +package cn.iocoder.yudao.module.im.dal.dataobject.message.content;
2 2  
3 3 import lombok.Data;
4 4 import lombok.experimental.Accessors;
... ... @@ -6,7 +6,7 @@ import lombok.experimental.Accessors;
6 6 /**
7 7 * 频道素材消息 payload
8 8 * <p>
9   - * 对应 {@link cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum#MATERIAL}(type=125)。
  9 + * 对应 {@link cn.iocoder.yudao.module.im.enums.ImContentTypeEnum#MATERIAL}(type=125)。
10 10 * 客户端按本字段集渲染图文卡片;
11 11 * 富文本正文不在本 payload 中传递,点击详情时另调 /im/channel/material/get-content?id= 按需拉取。
12 12 */
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/MergeMessage.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/MergeMessage.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.message;
  1 +package cn.iocoder.yudao.module.im.dal.dataobject.message.content;
2 2  
3   -import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
  3 +import cn.iocoder.yudao.module.im.enums.ImContentTypeEnum;
4 4 import lombok.Data;
5 5  
6 6 import java.util.List;
... ... @@ -47,7 +47,7 @@ public class MergeMessage {
47 47 /**
48 48 * 消息类型
49 49 * <p>
50   - * 枚举 {@link ImMessageTypeEnum}
  50 + * 枚举 {@link ImContentTypeEnum}
51 51 */
52 52 private Integer type;
53 53 /**
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/QuoteMessage.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/QuoteMessage.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.message;
  1 +package cn.iocoder.yudao.module.im.dal.dataobject.message.content;
2 2  
3   -import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
  3 +import cn.iocoder.yudao.module.im.enums.ImContentTypeEnum;
4 4 import lombok.Data;
5 5 import lombok.experimental.Accessors;
6 6  
... ... @@ -39,7 +39,7 @@ public class QuoteMessage {
39 39 /**
40 40 * 被引用消息类型
41 41 * <p>
42   - * 枚举 {@link ImMessageTypeEnum}
  42 + * 枚举 {@link ImContentTypeEnum}
43 43 */
44 44 private Integer type;
45 45 /**
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/RecallMessage.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/RecallMessage.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.message;
  1 +package cn.iocoder.yudao.module.im.dal.dataobject.message.content;
2 2  
3 3 import lombok.Data;
4 4 import lombok.experimental.Accessors;
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/TextMessage.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/TextMessage.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.message;
  1 +package cn.iocoder.yudao.module.im.dal.dataobject.message.content;
2 2  
3 3 import lombok.Data;
4 4  
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/VideoMessage.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content/VideoMessage.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.message;
  1 +package cn.iocoder.yudao.module.im.dal.dataobject.message.content;
2 2  
3 3 import lombok.Data;
4 4  
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/conversation/ImConversationReadMapper.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.dal.mysql.conversation;
  2 +
  3 +import cn.hutool.core.date.LocalDateTimeUtil;
  4 +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
  5 +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
  6 +import cn.iocoder.yudao.module.im.dal.dataobject.conversation.ImConversationReadDO;
  7 +import com.baomidou.mybatisplus.core.toolkit.Wrappers;
  8 +import org.apache.ibatis.annotations.Mapper;
  9 +
  10 +import java.time.LocalDateTime;
  11 +import java.util.Collection;
  12 +import java.util.List;
  13 +
  14 +/**
  15 + * IM 会话读位置 Mapper
  16 + *
  17 + * @author 芋道源码
  18 + */
  19 +@Mapper
  20 +public interface ImConversationReadMapper extends BaseMapperX<ImConversationReadDO> {
  21 +
  22 + default ImConversationReadDO selectByUserIdAndConversation(Long userId, Integer conversationType, Long conversationId) {
  23 + return selectOne(new LambdaQueryWrapperX<ImConversationReadDO>()
  24 + .eq(ImConversationReadDO::getUserId, userId)
  25 + .eq(ImConversationReadDO::getConversationType, conversationType)
  26 + .eq(ImConversationReadDO::getTargetId, conversationId));
  27 + }
  28 +
  29 + default List<ImConversationReadDO> selectListByConversation(Integer conversationType, Long conversationId) {
  30 + return selectList(new LambdaQueryWrapperX<ImConversationReadDO>()
  31 + .eq(ImConversationReadDO::getConversationType, conversationType)
  32 + .eq(ImConversationReadDO::getTargetId, conversationId));
  33 + }
  34 +
  35 + default List<ImConversationReadDO> selectListByUserIdAndConversations(Long userId, Integer conversationType,
  36 + Collection<Long> conversationIds) {
  37 + return selectList(new LambdaQueryWrapperX<ImConversationReadDO>()
  38 + .eq(ImConversationReadDO::getUserId, userId)
  39 + .eq(ImConversationReadDO::getConversationType, conversationType)
  40 + .in(ImConversationReadDO::getTargetId, conversationIds));
  41 + }
  42 +
  43 + /**
  44 + * 增量拉取当前用户的会话读位置(按 update_time + id 正向游标)
  45 + *
  46 + * @param userId 当前用户编号
  47 + * @param lastUpdateTime 上次拉取到的更新时间;首次拉取传 null
  48 + * @param lastId 上次拉取到的记录编号;首次拉取传 null
  49 + * @param limit 拉取数量
  50 + * @return 会话读位置列表
  51 + */
  52 + default List<ImConversationReadDO> selectPullListByUserId(Long userId, Long lastUpdateTime, Long lastId, Integer limit) {
  53 + LambdaQueryWrapperX<ImConversationReadDO> query = new LambdaQueryWrapperX<ImConversationReadDO>()
  54 + .eq(ImConversationReadDO::getUserId, userId);
  55 + if (lastUpdateTime != null && lastId != null) {
  56 + LocalDateTime lastTime = LocalDateTimeUtil.of(lastUpdateTime);
  57 + query.and(w -> w.gt(ImConversationReadDO::getUpdateTime, lastTime)
  58 + .or(n -> n.eq(ImConversationReadDO::getUpdateTime, lastTime).gt(ImConversationReadDO::getId, lastId)));
  59 + }
  60 + return selectList(query.orderByAsc(ImConversationReadDO::getUpdateTime).orderByAsc(ImConversationReadDO::getId)
  61 + .last("LIMIT " + limit));
  62 + }
  63 +
  64 + /**
  65 + * 单调递增更新读位置:仅当新位置更大时才更新
  66 + * <p>
  67 + * 通过 {@code read_message_id < messageId} 的 CAS 条件,保证乱序 / 并发上报时读位置不会回退。
  68 + *
  69 + * @param id 记录编号
  70 + * @param messageId 新的最大已读消息编号
  71 + * @param readTime 已读时间
  72 + * @return 影响行数;0 表示新位置不大于已有位置,未更新
  73 + */
  74 + default int updateReadMessageIdToLarger(Long id, Long messageId, LocalDateTime readTime) {
  75 + return update(null, Wrappers.<ImConversationReadDO>lambdaUpdate()
  76 + .set(ImConversationReadDO::getMessageId, messageId)
  77 + .set(ImConversationReadDO::getReadTime, readTime)
  78 + .set(ImConversationReadDO::getUpdateTime, readTime)
  79 + .eq(ImConversationReadDO::getId, id)
  80 + .lt(ImConversationReadDO::getMessageId, messageId));
  81 + }
  82 +
  83 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/friend/ImFriendMapper.java
1 1 package cn.iocoder.yudao.module.im.dal.mysql.friend;
2 2  
  3 +import cn.hutool.core.date.LocalDateTimeUtil;
3 4 import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 5 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
5 6 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
... ... @@ -31,6 +32,28 @@ public interface ImFriendMapper extends BaseMapperX&lt;ImFriendDO&gt; {
31 32 .eq(ImFriendDO::getUserId, userId)
32 33 .orderByDesc(ImFriendDO::getId));
33 34 }
  35 +
  36 + /**
  37 + * 增量拉取当前用户的好友关系(含已删除,按 update_time + id 正向游标)
  38 + *
  39 + * @param userId 当前用户编号
  40 + * @param lastUpdateTime 上次拉取到的更新时间;首次拉取传 null
  41 + * @param lastId 上次拉取到的记录编号;首次拉取传 null
  42 + * @param limit 拉取数量
  43 + * @return 好友关系列表
  44 + */
  45 + default List<ImFriendDO> selectPullListByUserId(Long userId, Long lastUpdateTime, Long lastId, Integer limit) {
  46 + LambdaQueryWrapperX<ImFriendDO> query = new LambdaQueryWrapperX<ImFriendDO>()
  47 + .eq(ImFriendDO::getUserId, userId);
  48 + if (lastUpdateTime != null && lastId != null) {
  49 + LocalDateTime lastTime = LocalDateTimeUtil.of(lastUpdateTime);
  50 + query.and(w -> w.gt(ImFriendDO::getUpdateTime, lastTime)
  51 + .or(n -> n.eq(ImFriendDO::getUpdateTime, lastTime).gt(ImFriendDO::getId, lastId)));
  52 + }
  53 + return selectList(query.orderByAsc(ImFriendDO::getUpdateTime).orderByAsc(ImFriendDO::getId)
  54 + .last("LIMIT " + limit));
  55 + }
  56 +
34 57 default List<ImFriendDO> selectListByUserIdAndStatus(Long userId, Integer status) {
35 58 return selectList(new LambdaQueryWrapperX<ImFriendDO>()
36 59 .eq(ImFriendDO::getUserId, userId)
... ... @@ -68,12 +91,14 @@ public interface ImFriendMapper extends BaseMapperX&lt;ImFriendDO&gt; {
68 91  
69 92 @SuppressWarnings("UnusedReturnValue")
70 93 default int updateReAddFields(Long id, Integer status, LocalDateTime addTime,
  94 + LocalDateTime updateTime,
71 95 Boolean silent, Boolean pinned, Boolean blocked,
72 96 String displayName, Integer addSource) {
73 97 return update(null, Wrappers.<ImFriendDO>lambdaUpdate()
74 98 .eq(ImFriendDO::getId, id)
75 99 .set(ImFriendDO::getStatus, status)
76 100 .set(ImFriendDO::getAddTime, addTime)
  101 + .set(ImFriendDO::getUpdateTime, updateTime)
77 102 .set(ImFriendDO::getSilent, silent)
78 103 .set(ImFriendDO::getPinned, pinned)
79 104 .set(ImFriendDO::getBlocked, blocked)
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/friend/ImFriendRequestMapper.java
1 1 package cn.iocoder.yudao.module.im.dal.mysql.friend;
2 2  
  3 +import cn.hutool.core.date.LocalDateTimeUtil;
3 4 import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 5 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
5 6 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
... ... @@ -28,6 +29,12 @@ public interface ImFriendRequestMapper extends BaseMapperX&lt;ImFriendRequestDO&gt; {
28 29  
29 30 /**
30 31 * 拉取「我相关」的好友申请列表
  32 + *
  33 + * @param userId 当前用户编号
  34 + * @param maxRequestUpdateTime 起始更新时间;首次拉取传 null
  35 + * @param maxId 起始记录编号;首次拉取传 null
  36 + * @param limit 拉取数量
  37 + * @return 好友申请列表
31 38 */
32 39 default List<ImFriendRequestDO> selectMyList(Long userId, LocalDateTime maxRequestUpdateTime,
33 40 Long maxId, int limit) {
... ... @@ -45,6 +52,28 @@ public interface ImFriendRequestMapper extends BaseMapperX&lt;ImFriendRequestDO&gt; {
45 52 return selectList(wrapper);
46 53 }
47 54  
  55 + /**
  56 + * 增量拉取「我相关」的好友申请(双向 OR,按 update_time + id 正向游标)
  57 + *
  58 + * @param userId 当前用户编号
  59 + * @param lastUpdateTime 上次拉取到的更新时间;首次拉取传 null
  60 + * @param lastId 上次拉取到的记录编号;首次拉取传 null
  61 + * @param limit 拉取数量
  62 + * @return 好友申请列表
  63 + */
  64 + default List<ImFriendRequestDO> selectPullListByUserId(Long userId, Long lastUpdateTime, Long lastId, Integer limit) {
  65 + LambdaQueryWrapperX<ImFriendRequestDO> query = new LambdaQueryWrapperX<>();
  66 + query.and(w -> w.eq(ImFriendRequestDO::getFromUserId, userId)
  67 + .or().eq(ImFriendRequestDO::getToUserId, userId));
  68 + if (lastUpdateTime != null && lastId != null) {
  69 + LocalDateTime lastTime = LocalDateTimeUtil.of(lastUpdateTime);
  70 + query.and(w -> w.gt(ImFriendRequestDO::getUpdateTime, lastTime)
  71 + .or(n -> n.eq(ImFriendRequestDO::getUpdateTime, lastTime).gt(ImFriendRequestDO::getId, lastId)));
  72 + }
  73 + return selectList(query.orderByAsc(ImFriendRequestDO::getUpdateTime).orderByAsc(ImFriendRequestDO::getId)
  74 + .last("LIMIT " + limit));
  75 + }
  76 +
48 77 default int updateByIdAndHandleResult(Long id, Integer handleResult, ImFriendRequestDO updateObj) {
49 78 return update(updateObj, new LambdaUpdateWrapper<ImFriendRequestDO>()
50 79 .eq(ImFriendRequestDO::getId, id).eq(ImFriendRequestDO::getHandleResult, handleResult));
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/group/ImGroupMemberMapper.java
1 1 package cn.iocoder.yudao.module.im.dal.mysql.group;
2 2  
3 3 import cn.hutool.core.collection.CollUtil;
4   -import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
5 4 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
6 5 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
7 6 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO;
... ... @@ -59,20 +58,8 @@ public interface ImGroupMemberMapper extends BaseMapperX&lt;ImGroupMemberDO&gt; {
59 58 .eq(ImGroupMemberDO::getStatus, status));
60 59 }
61 60  
62   - /**
63   - * 查询用户已退群的成员记录
64   - * <p>
65   - * 当 {@code minQuitTime} 非空时额外按 {@code quitTime ≥ minQuitTime} 过滤。
66   - *
67   - * @param userId 用户编号
68   - * @param minQuitTime 最早退群时间(含),可空
69   - * @return 已退群成员记录列表
70   - */
71   - default List<ImGroupMemberDO> selectQuitListByUserId(Long userId, LocalDateTime minQuitTime) {
72   - return selectList(new LambdaQueryWrapperX<ImGroupMemberDO>()
73   - .eq(ImGroupMemberDO::getUserId, userId)
74   - .eq(ImGroupMemberDO::getStatus, CommonStatusEnum.DISABLE.getStatus())
75   - .geIfPresent(ImGroupMemberDO::getQuitTime, minQuitTime));
  61 + default List<ImGroupMemberDO> selectListByUserId(Long userId) {
  62 + return selectList(ImGroupMemberDO::getUserId, userId);
76 63 }
77 64  
78 65 @SuppressWarnings("UnusedReturnValue")
... ... @@ -143,7 +130,8 @@ public interface ImGroupMemberMapper extends BaseMapperX&lt;ImGroupMemberDO&gt; {
143 130 .set(ImGroupMemberDO::getAddSource, addSource)
144 131 .set(ImGroupMemberDO::getInviterUserId, inviterUserId)
145 132 .set(ImGroupMemberDO::getQuitTime, null)
146   - .set(ImGroupMemberDO::getMuteEndTime, null));
  133 + .set(ImGroupMemberDO::getMuteEndTime, null)
  134 + .set(ImGroupMemberDO::getUpdateTime, joinTime));
147 135 }
148 136  
149 137 }
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/group/ImGroupRequestMapper.java
1 1 package cn.iocoder.yudao.module.im.dal.mysql.group;
2 2  
  3 +import cn.hutool.core.date.LocalDateTimeUtil;
3 4 import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 5 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
5 6 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
... ... @@ -43,6 +44,28 @@ public interface ImGroupRequestMapper extends BaseMapperX&lt;ImGroupRequestDO&gt; {
43 44 .orderByDesc(ImGroupRequestDO::getId));
44 45 }
45 46  
  47 + /**
  48 + * 增量拉取「我管理的群」下的加群申请(含已处理,按 update_time + id 正向游标)
  49 + *
  50 + * @param groupIds 群编号集合
  51 + * @param lastUpdateTime 上次拉取到的更新时间;首次拉取传 null
  52 + * @param lastId 上次拉取到的记录编号;首次拉取传 null
  53 + * @param limit 拉取数量
  54 + * @return 加群申请列表
  55 + */
  56 + default List<ImGroupRequestDO> selectPullListByGroupIds(Collection<Long> groupIds, Long lastUpdateTime,
  57 + Long lastId, Integer limit) {
  58 + LambdaQueryWrapperX<ImGroupRequestDO> query = new LambdaQueryWrapperX<ImGroupRequestDO>()
  59 + .in(ImGroupRequestDO::getGroupId, groupIds);
  60 + if (lastUpdateTime != null && lastId != null) {
  61 + LocalDateTime lastTime = LocalDateTimeUtil.of(lastUpdateTime);
  62 + query.and(w -> w.gt(ImGroupRequestDO::getUpdateTime, lastTime)
  63 + .or(n -> n.eq(ImGroupRequestDO::getUpdateTime, lastTime).gt(ImGroupRequestDO::getId, lastId)));
  64 + }
  65 + return selectList(query.orderByAsc(ImGroupRequestDO::getUpdateTime).orderByAsc(ImGroupRequestDO::getId)
  66 + .last("LIMIT " + limit));
  67 + }
  68 +
46 69 default int updateByIdAndHandleResult(Long id, Integer expectedHandleResult, ImGroupRequestDO updateObj) {
47 70 return update(updateObj, new LambdaUpdateWrapper<ImGroupRequestDO>()
48 71 .eq(ImGroupRequestDO::getId, id).eq(ImGroupRequestDO::getHandleResult, expectedHandleResult));
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/message/ImGroupMessageMapper.java
... ... @@ -4,13 +4,15 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 4 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
5 5 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
6 6 import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX;
  7 +import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
7 8 import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.group.ImGroupMessageManagerPageReqVO;
8 9 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO;
9   -import cn.iocoder.yudao.module.im.enums.message.ImGroupMessageReceiptStatusEnum;
  10 +import cn.iocoder.yudao.module.im.enums.message.ImMessageReceiptStatusEnum;
10 11 import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum;
11 12 import org.apache.ibatis.annotations.Mapper;
12 13  
13 14 import java.time.LocalDateTime;
  15 +import java.util.Collection;
14 16 import java.util.List;
15 17  
16 18 /**
... ... @@ -22,48 +24,22 @@ import java.util.List;
22 24 public interface ImGroupMessageMapper extends BaseMapperX<ImGroupMessageDO> {
23 25  
24 26 /**
25   - * 根据 minId + 时间窗口增量拉取群聊消息(在群成员使用)
  27 + * 根据 minId + 时间窗口增量拉取群聊消息
26 28 *
27   - * @param groupIds 用户当前仍在群内的群编号列表
  29 + * @param userId 当前用户编号
  30 + * @param groupIds 候选群编号集合(当前在群 ∪ 窗口内退群),不能为空
28 31 * @param minId 最小消息 id(不含)
29 32 * @param minSendTime 最早发送时间(不含),限制离线消息时间窗口
30 33 * @param size 拉取数量
31 34 * @return 消息列表(按 id 升序)
32 35 */
33   - default List<ImGroupMessageDO> selectListByMinId(List<Long> groupIds, Long minId,
  36 + default List<ImGroupMessageDO> selectListByMinId(Long userId, Collection<Long> groupIds, Long minId,
34 37 LocalDateTime minSendTime, Integer size) {
35 38 QueryWrapperX<ImGroupMessageDO> wrapper = new QueryWrapperX<>();
36 39 wrapper.in("group_id", groupIds)
37 40 .gt("id", minId)
38 41 .gt("send_time", minSendTime)
39   - .orderByAsc("id");
40   - wrapper.limitN(size);
41   - return selectList(wrapper);
42   - }
43   -
44   - /**
45   - * 查询"退群前"的离线消息(退群成员使用)
46   - * <p>
47   - * 语义:用户已退出某群,但仍需把 {@code minId} 之后、{@code minSendTime} 之后、不晚于退群时间的消息补齐到本地,便于前端看到完整上下文。
48   - * <p>
49   - * 撤回消息(status=RECALL)保留返回,与在群成员的 {@link #selectListByMinId} 行为一致,由前端按撤回信号渲染「消息已撤回」气泡。
50   - *
51   - * @param groupId 群编号
52   - * @param minId 最小消息 id(不含)
53   - * @param minSendTime 最早发送时间(不含)
54   - * @param quitTime 退群时间(含),仅返回退群当时已存在的消息
55   - * @param size 拉取数量
56   - * @return 消息列表(按 id 升序)
57   - */
58   - default List<ImGroupMessageDO> selectListByGroupIdAndMinIdAndQuitTimeBefore(Long groupId, Long minId,
59   - LocalDateTime minSendTime,
60   - LocalDateTime quitTime,
61   - Integer size) {
62   - QueryWrapperX<ImGroupMessageDO> wrapper = new QueryWrapperX<>();
63   - wrapper.eq("group_id", groupId)
64   - .gt("id", minId)
65   - .gt("send_time", minSendTime)
66   - .le("send_time", quitTime)
  42 + .apply(MyBatisUtils.findInSet("receiver_user_ids"), userId)
67 43 .orderByAsc("id");
68 44 wrapper.limitN(size);
69 45 return selectList(wrapper);
... ... @@ -72,17 +48,17 @@ public interface ImGroupMessageMapper extends BaseMapperX&lt;ImGroupMessageDO&gt; {
72 48 /**
73 49 * 查询群聊历史消息(游标拉取)
74 50 *
75   - * @param groupId 群编号
76   - * @param maxId 起始消息 id(不含),为空则从最新开始
77   - * @param limit 拉取数量
78   - * @param joinTime 入群时间,仅返回入群之后的消息
  51 + * @param userId 当前用户编号
  52 + * @param groupId 群编号
  53 + * @param maxId 起始消息 id(不含),为空则从最新开始
  54 + * @param limit 拉取数量
79 55 * @return 消息列表(按 id 倒序)
80 56 */
81   - default List<ImGroupMessageDO> selectHistoryList(Long groupId, Long maxId, Integer limit, LocalDateTime joinTime) {
  57 + default List<ImGroupMessageDO> selectHistoryListByUser(Long userId, Long groupId, Long maxId, Integer limit) {
82 58 QueryWrapperX<ImGroupMessageDO> wrapper = new QueryWrapperX<>();
83 59 wrapper.eq("group_id", groupId)
84 60 .lt(maxId != null, "id", maxId)
85   - .ge(joinTime != null, "send_time", joinTime)
  61 + .apply(MyBatisUtils.findInSet("receiver_user_ids"), userId)
86 62 .orderByDesc("id");
87 63 wrapper.limitN(limit);
88 64 return selectList(wrapper);
... ... @@ -108,7 +84,7 @@ public interface ImGroupMessageMapper extends BaseMapperX&lt;ImGroupMessageDO&gt; {
108 84 default List<ImGroupMessageDO> selectListByGroupIdAndPendingReceipt(Long groupId, Long minId, Long maxId) {
109 85 return selectList(new LambdaQueryWrapperX<ImGroupMessageDO>()
110 86 .eq(ImGroupMessageDO::getGroupId, groupId)
111   - .eq(ImGroupMessageDO::getReceiptStatus, ImGroupMessageReceiptStatusEnum.PENDING.getStatus())
  87 + .eq(ImGroupMessageDO::getReceiptStatus, ImMessageReceiptStatusEnum.PENDING.getStatus())
112 88 .gt(minId != null, ImGroupMessageDO::getId, minId)
113 89 .le(ImGroupMessageDO::getId, maxId)
114 90 .ne(ImGroupMessageDO::getStatus, ImMessageStatusEnum.RECALL.getStatus()));
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/message/ImPrivateMessageMapper.java
... ... @@ -67,23 +67,13 @@ public interface ImPrivateMessageMapper extends BaseMapperX&lt;ImPrivateMessageDO&gt;
67 67 .eq(ImPrivateMessageDO::getClientMessageId, clientMessageId));
68 68 }
69 69  
70   - default Long selectMaxIdBySenderIdAndReceiverIdAndStatus(Long senderId, Long receiverId, Integer status) {
71   - ImPrivateMessageDO message = selectOne(new LambdaQueryWrapperX<ImPrivateMessageDO>()
72   - .eq(ImPrivateMessageDO::getSenderId, senderId)
73   - .eq(ImPrivateMessageDO::getReceiverId, receiverId)
74   - .eq(ImPrivateMessageDO::getStatus, status)
75   - .orderByDesc(ImPrivateMessageDO::getId)
76   - .last("LIMIT 1"));
77   - return message != null ? message.getId() : null;
78   - }
79   -
80   - default int updateBySenderIdAndReceiverIdAndIdLeAndStatus(Long senderId, Long receiverId, Long maxMessageId,
81   - Integer whereStatus, ImPrivateMessageDO updateObj) {
  70 + default int updateBySenderIdAndReceiverIdAndIdLeAndReceiptStatus(Long senderId, Long receiverId, Long maxMessageId,
  71 + Integer whereReceiptStatus, ImPrivateMessageDO updateObj) {
82 72 return update(updateObj, new LambdaQueryWrapperX<ImPrivateMessageDO>()
83 73 .eq(ImPrivateMessageDO::getSenderId, senderId)
84 74 .eq(ImPrivateMessageDO::getReceiverId, receiverId)
85 75 .le(ImPrivateMessageDO::getId, maxMessageId)
86   - .eq(ImPrivateMessageDO::getStatus, whereStatus));
  76 + .eq(ImPrivateMessageDO::getReceiptStatus, whereReceiptStatus));
87 77 }
88 78  
89 79 default PageResult<ImPrivateMessageDO> selectPage(ImPrivateMessageManagerPageReqVO reqVO) {
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/statistics/ImStatisticsManagerMapper.java
... ... @@ -154,7 +154,7 @@ public interface ImStatisticsManagerMapper {
154 154 @Param("endTime") LocalDateTime endTime);
155 155  
156 156 /**
157   - * 区间内消息类型分布(私聊+群聊合并)
  157 + * 区间内内容类型分布(私聊+群聊合并)
158 158 *
159 159 * @return [{type: 0, count: 123}, ...]
160 160 */
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/redis/RedisKeyConstants.java
... ... @@ -9,20 +9,6 @@ package cn.iocoder.yudao.module.im.dal.redis;
9 9 public interface RedisKeyConstants {
10 10  
11 11 /**
12   - * 群消息已读位置
13   - * KEY 格式: im:group:message:read:{groupId}
14   - * VALUE 数据类型: Hash (field: userId, value: maxReadMessageId)
15   - */
16   - String GROUP_MESSAGE_READ = "im:group:message:read:%s";
17   -
18   - /**
19   - * 频道消息已读位置
20   - * KEY 格式: im:channel:message:read:{channelId}
21   - * VALUE 数据类型: Hash (field: userId, value: maxReadMessageId)
22   - */
23   - String CHANNEL_MESSAGE_READ = "im:channel:message:read:%s";
24   -
25   - /**
26 12 * 好友关系状态缓存(合并「是否好友」+「是否拉黑」两态)
27 13 * <p>
28 14 * KEY 格式:friend_state:{userId}_{friendUserId}
... ... @@ -44,7 +30,7 @@ public interface RedisKeyConstants {
44 30 * KEY 格式:group_member_ids:{groupId}
45 31 * VALUE 数据类型:List<Long>
46 32 * <p>
47   - * 说明:只缓存轻量的 userId 列表,适合"群消息推送目标"这类只关心 userId 的场景。
  33 + * 说明:只缓存轻量的 userId 列表,适合「群消息推送目标」这类只关心 userId 的场景
48 34 */
49 35 String GROUP_MEMBER_IDS = "group_member_ids";
50 36  
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/redis/message/ImChannelMessageReadRedisDAO.java deleted
1   -package cn.iocoder.yudao.module.im.dal.redis.message;
2   -
3   -import cn.hutool.core.convert.Convert;
4   -import jakarta.annotation.Resource;
5   -import org.springframework.data.redis.core.StringRedisTemplate;
6   -import org.springframework.stereotype.Repository;
7   -
8   -import static cn.iocoder.yudao.module.im.dal.redis.RedisKeyConstants.CHANNEL_MESSAGE_READ;
9   -
10   -/**
11   - * IM 频道消息已读位置 Redis DAO
12   - *
13   - * @author 芋道源码
14   - */
15   -@Repository
16   -public class ImChannelMessageReadRedisDAO {
17   -
18   - @Resource
19   - private StringRedisTemplate stringRedisTemplate;
20   -
21   - /**
22   - * 更新用户在某频道的最大已读消息编号
23   - *
24   - * @param channelId 频道编号
25   - * @param userId 用户编号
26   - * @param maxMessageId 最大已读消息编号
27   - */
28   - public void updateReadMaxMessageId(Long channelId, Long userId, Long maxMessageId) {
29   - String key = formatKey(channelId);
30   - stringRedisTemplate.opsForHash().put(key, userId.toString(), maxMessageId.toString());
31   - }
32   -
33   - /**
34   - * 获取用户在某频道的最大已读消息编号
35   - *
36   - * @param channelId 频道编号
37   - * @param userId 用户编号
38   - * @return 最大已读消息编号;不存在则返回 null
39   - */
40   - public Long getReadMaxMessageId(Long channelId, Long userId) {
41   - String key = formatKey(channelId);
42   - Object val = stringRedisTemplate.opsForHash().get(key, userId.toString());
43   - return Convert.toLong(val);
44   - }
45   -
46   - private static String formatKey(Long channelId) {
47   - return String.format(CHANNEL_MESSAGE_READ, channelId);
48   - }
49   -
50   -}
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/redis/message/ImGroupMessageReadRedisDAO.java deleted
1   -package cn.iocoder.yudao.module.im.dal.redis.message;
2   -
3   -import cn.hutool.core.convert.Convert;
4   -import jakarta.annotation.Resource;
5   -import org.springframework.data.redis.core.StringRedisTemplate;
6   -import org.springframework.stereotype.Repository;
7   -
8   -import java.util.Collection;
9   -import java.util.HashMap;
10   -import java.util.Map;
11   -
12   -import static cn.iocoder.yudao.module.im.dal.redis.RedisKeyConstants.GROUP_MESSAGE_READ;
13   -
14   -/**
15   - * IM 群消息已读位置 Redis DAO
16   - *
17   - * @author 芋道源码
18   - */
19   -@Repository
20   -public class ImGroupMessageReadRedisDAO {
21   -
22   - @Resource
23   - private StringRedisTemplate stringRedisTemplate;
24   -
25   - /**
26   - * 更新用户在某群的最大已读消息编号
27   - *
28   - * @param groupId 群编号
29   - * @param userId 用户编号
30   - * @param maxMessageId 最大已读消息编号
31   - */
32   - public void updateReadMaxMessageId(Long groupId, Long userId, Long maxMessageId) {
33   - String key = formatKey(groupId);
34   - stringRedisTemplate.opsForHash().put(key, userId.toString(), maxMessageId.toString());
35   - }
36   -
37   - /**
38   - * 获取用户在某群的最大已读消息编号
39   - *
40   - * @param groupId 群编号
41   - * @param userId 用户编号
42   - * @return 最大已读消息编号,不存在则返回 null
43   - */
44   - public Long getReadMaxMessageId(Long groupId, Long userId) {
45   - String key = formatKey(groupId);
46   - Object val = stringRedisTemplate.opsForHash().get(key, userId.toString());
47   - return Convert.toLong(val);
48   - }
49   -
50   - /**
51   - * 获取某群所有用户的已读位置
52   - *
53   - * @param groupId 群编号
54   - * @return userId → maxReadMessageId 映射
55   - */
56   - public Map<Long, Long> getReadMaxMessageIdMap(Long groupId) {
57   - String key = formatKey(groupId);
58   - Map<Object, Object> entries = stringRedisTemplate.opsForHash().entries(key);
59   - // 转换为 Long → Long 的 Map
60   - Map<Long, Long> result = new HashMap<>(entries.size());
61   - entries.forEach((k, v) -> result.put(Long.parseLong(k.toString()), Long.parseLong(v.toString())));
62   - return result;
63   - }
64   -
65   - /**
66   - * 删除用户在某群的已读位置
67   - *
68   - * @param groupId 群编号
69   - * @param userId 用户编号
70   - */
71   - public void deleteReadMaxMessageId(Long groupId, Long userId) {
72   - String key = formatKey(groupId);
73   - stringRedisTemplate.opsForHash().delete(key, userId.toString());
74   - }
75   -
76   - /**
77   - * 批量删除用户在某群的已读位置
78   - *
79   - * @param groupId 群编号
80   - * @param userIds 用户编号集合
81   - */
82   - public void deleteReadMaxMessageIds(Long groupId, Collection<Long> userIds) {
83   - String key = formatKey(groupId);
84   - Object[] hashKeys = userIds.stream().map(String::valueOf).toArray();
85   - stringRedisTemplate.opsForHash().delete(key, hashKeys);
86   - }
87   -
88   - /**
89   - * 删除某群所有用户的已读位置(整个 Hash Key)
90   - *
91   - * @param groupId 群编号
92   - */
93   - public void deleteReadMaxMessageIdMap(Long groupId) {
94   - String key = formatKey(groupId);
95   - stringRedisTemplate.delete(key);
96   - }
97   -
98   - private static String formatKey(Long groupId) {
99   - return String.format(GROUP_MESSAGE_READ, groupId);
100   - }
101   -
102   -}
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/message/ImMessageTypeEnum.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/ImContentTypeEnum.java
1   -package cn.iocoder.yudao.module.im.enums.message;
  1 +package cn.iocoder.yudao.module.im.enums;
2 2  
3 3 import cn.hutool.core.collection.CollUtil;
4 4 import cn.hutool.core.lang.Assert;
... ... @@ -11,13 +11,13 @@ import java.util.Arrays;
11 11 import java.util.Set;
12 12  
13 13 /**
14   - * IM 消息类型枚举
  14 + * IM 内容类型枚举
15 15 *
16 16 * @author 芋道源码
17 17 */
18 18 @Getter
19 19 @RequiredArgsConstructor
20   -public enum ImMessageTypeEnum implements ArrayValuable<Integer> {
  20 +public enum ImContentTypeEnum implements ArrayValuable<Integer> {
21 21  
22 22 // ========== 用户聊天消息(101-105 直接复用 OpenIM 段位编号) ==========
23 23 /**
... ... @@ -128,10 +128,6 @@ public enum ImMessageTypeEnum implements ArrayValuable&lt;Integer&gt; {
128 128 RTC_CALL_END(1611, "通话结束", true, false),
129 129  
130 130 // ========== 好友通知(1201-1210 直接复用 OpenIM 段位编号) ==========
131   - // TODO @芋艿:FRIEND_REQUEST_* 与 GROUP_REQUEST_* 都是 persistent=false 的 SysMsg,离线 pull 拉不到,
132   - // 目前可能丢失「实时 toast 提醒」体验(业务状态不丢,前端上线 fetch{Friend,Group}RequestList 能补回来);
133   - // 未来思考下怎么优化:候选方案 1)改 persistent=true 入私聊消息流 + 让客户端按 type 自渲染;
134   - // 2)服务端补一个「未读通知拉取」接口给前端冷启动调用。
135 131 /**
136 132 * 对应 OpenIM:FriendApplicationApprovedNotification 1201
137 133 * 对应自己的类:FriendRequestApprovedNotification
... ... @@ -285,9 +281,9 @@ public enum ImMessageTypeEnum implements ArrayValuable&lt;Integer&gt; {
285 281 /**
286 282 * 对应 OpenIM:sdkws.GroupMemberInfoSetTips(GroupMemberInfoSetNotification 1516,窄化到 displayUserName)
287 283 * 对应自己的类:GroupMemberNicknameUpdateNotification
288   - * 场景:成员修改自己在群里的昵称,全员广播;前端按 displayUserName 局部更新对应 member
  284 + * 场景:成员修改自己在群里的昵称,在线成员同步对应 member
289 285 */
290   - GROUP_MEMBER_NICKNAME_UPDATE(1516, "成员昵称变更", true, false),
  286 + GROUP_MEMBER_NICKNAME_UPDATE(1516, "成员昵称变更", false, false),
291 287 /**
292 288 * 对应 OpenIM:GroupMemberSetToAdminNotification 1517
293 289 * 对应自己的类:GroupAdminAddNotification
... ... @@ -339,7 +335,7 @@ public enum ImMessageTypeEnum implements ArrayValuable&lt;Integer&gt; {
339 335 */
340 336 GROUP_BANNED(1533, "群封禁变更", true, false);
341 337  
342   - public static final Integer[] ARRAYS = Arrays.stream(values()).map(ImMessageTypeEnum::getType).toArray(Integer[]::new);
  338 + public static final Integer[] ARRAYS = Arrays.stream(values()).map(ImContentTypeEnum::getType).toArray(Integer[]::new);
343 339  
344 340 private static final Set<Integer> FRIEND_NOTIFICATION_TYPES = CollUtil.newHashSet(
345 341 FRIEND_REQUEST_APPROVED.type,
... ... @@ -396,8 +392,8 @@ public enum ImMessageTypeEnum implements ArrayValuable&lt;Integer&gt; {
396 392 * @param type 消息类型
397 393 * @return 枚举实例
398 394 */
399   - public static ImMessageTypeEnum validate(Integer type) {
400   - ImMessageTypeEnum result = ArrayUtil.firstMatch(item -> item.type.equals(type), values());
  395 + public static ImContentTypeEnum validate(Integer type) {
  396 + ImContentTypeEnum result = ArrayUtil.firstMatch(item -> item.type.equals(type), values());
401 397 Assert.notNull(result, "未注册的消息类型 type={}", type);
402 398 return result;
403 399 }
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/ImConversationTypeEnum.java
... ... @@ -16,6 +16,7 @@ import java.util.Objects;
16 16 @Getter
17 17 public enum ImConversationTypeEnum implements ArrayValuable<Integer> {
18 18  
  19 + NONE(0, "无会话"), // 无会话
19 20 PRIVATE(1, "私聊"), // 私聊
20 21 GROUP(2, "群聊"), // 群聊
21 22 CHANNEL(3, "频道"); // 频道 / 公众号
... ... @@ -36,6 +37,10 @@ public enum ImConversationTypeEnum implements ArrayValuable&lt;Integer&gt; {
36 37 return ARRAYS;
37 38 }
38 39  
  40 + public static boolean isNone(Integer type) {
  41 + return Objects.equals(NONE.type, type);
  42 + }
  43 +
39 44 public static boolean isPrivate(Integer type) {
40 45 return Objects.equals(PRIVATE.type, type);
41 46 }
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/message/ImGroupMessageReceiptStatusEnum.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/message/ImMessageReceiptStatusEnum.java
... ... @@ -7,19 +7,19 @@ import lombok.Getter;
7 7 import java.util.Arrays;
8 8  
9 9 /**
10   - * IM 群消息回执状态枚举
  10 + * IM 消息回执状态枚举
11 11 *
12 12 * @author 芋道源码
13 13 */
14 14 @Getter
15 15 @AllArgsConstructor
16   -public enum ImGroupMessageReceiptStatusEnum implements ArrayValuable<Integer> {
  16 +public enum ImMessageReceiptStatusEnum implements ArrayValuable<Integer> {
17 17  
18 18 NO_RECEIPT(0, "不需要回执"),
19 19 PENDING(1, "待完成"),
20 20 DONE(2, "已完成");
21 21  
22   - public static final Integer[] ARRAYS = Arrays.stream(values()).map(ImGroupMessageReceiptStatusEnum::getStatus).toArray(Integer[]::new);
  22 + public static final Integer[] ARRAYS = Arrays.stream(values()).map(ImMessageReceiptStatusEnum::getStatus).toArray(Integer[]::new);
23 23  
24 24 /**
25 25 * 状态
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/message/ImMessageStatusEnum.java
... ... @@ -8,9 +8,6 @@ import java.util.Arrays;
8 8  
9 9 /**
10 10 * IM 消息状态枚举
11   - * <p>
12   - * 私聊:SENDING(-1, 仅客户端) / UNREAD(0) / RECALL(2) / READ(3)
13   - * 群聊:SENDING(-1, 仅客户端) / UNREAD(0, 作为正常状态) / RECALL(2)
14 11 *
15 12 * @author 芋道源码
16 13 */
... ... @@ -19,9 +16,8 @@ import java.util.Arrays;
19 16 public enum ImMessageStatusEnum implements ArrayValuable<Integer> {
20 17  
21 18 SENDING(-1, "发送中"), // 仅客户端使用
22   - UNREAD(0, "未读"), // 私聊=未读,群聊=正常(初始状态)
23   - RECALL(2, "已撤回"),
24   - READ(3, "已读"); // 仅私聊使用;群聊已读通过 Redis 已读位置实现
  19 + NORMAL(0, "正常"), // 私聊 / 群聊的正常(初始)状态
  20 + RECALL(2, "已撤回");
25 21  
26 22 public static final Integer[] ARRAYS = Arrays.stream(values()).map(ImMessageStatusEnum::getStatus).toArray(Integer[]::new);
27 23  
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/rtc/ImRtcParticipantStatusEnum.java
... ... @@ -24,7 +24,6 @@ import java.util.Objects;
24 24 @Getter
25 25 public enum ImRtcParticipantStatusEnum implements ArrayValuable<Integer> {
26 26  
27   - // TODO @芋艿:hand up 要不要也搞下。
28 27 INVITING(10, "邀请中"), // 已发出 invite,等被叫响应
29 28 JOINED(20, "已加入"), // 已 connect 进 LiveKit 房间
30 29 REJECTED(30, "已拒绝"), // 接通前点拒接
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/mq/consumer/friend/AdminUserProfileUpdateConsumer.java
... ... @@ -2,11 +2,11 @@ package cn.iocoder.yudao.module.im.mq.consumer.friend;
2 2  
3 3 import cn.hutool.core.collection.CollUtil;
4 4 import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO;
5   -import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
  5 +import cn.iocoder.yudao.module.im.enums.ImContentTypeEnum;
  6 +import cn.iocoder.yudao.module.im.enums.ImConversationTypeEnum;
6 7 import cn.iocoder.yudao.module.im.service.friend.ImFriendService;
7 8 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService;
8   -import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO;
9   -import cn.iocoder.yudao.module.im.service.websocket.dto.notification.friend.FriendInfoUpdatedNotification;
  9 +import cn.iocoder.yudao.module.im.service.websocket.notification.friend.FriendInfoUpdatedNotification;
10 10 import cn.iocoder.yudao.module.system.api.message.user.AdminUserProfileUpdateMessage;
11 11 import jakarta.annotation.Resource;
12 12 import lombok.extern.slf4j.Slf4j;
... ... @@ -52,8 +52,9 @@ public class AdminUserProfileUpdateConsumer {
52 52 try {
53 53 FriendInfoUpdatedNotification payload = (FriendInfoUpdatedNotification) new FriendInfoUpdatedNotification()
54 54 .setOperatorUserId(userId).setFriendUserId(userId);
55   - websocketService.sendPrivateMessageAsync(friend.getFriendUserId(), ImPrivateMessageDTO.ofFriendNotification(
56   - ImMessageTypeEnum.FRIEND_INFO_UPDATED.getType(), userId, friend.getFriendUserId(), payload));
  55 + websocketService.sendNotificationAsync(friend.getFriendUserId(),
  56 + ImConversationTypeEnum.NONE.getType(),
  57 + ImContentTypeEnum.FRIEND_INFO_UPDATED.getType(), payload);
57 58 successCount++;
58 59 } catch (Exception e) {
59 60 log.warn("[onMessage][userId({}) friendUserId({}) 推送失败]",
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/conversation/ImConversationReadService.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.service.conversation;
  2 +
  3 +import cn.iocoder.yudao.module.im.dal.dataobject.conversation.ImConversationReadDO;
  4 +
  5 +import java.util.Collection;
  6 +import java.util.List;
  7 +import java.util.Map;
  8 +
  9 +/**
  10 + * IM 会话读位置 Service 接口
  11 + *
  12 + * @author 芋道源码
  13 + */
  14 +public interface ImConversationReadService {
  15 +
  16 + /**
  17 + * 更新用户在某会话的最大已读位置(单调递增,乱序 / 并发上报不会回退)
  18 + *
  19 + * @param userId 用户编号
  20 + * @param conversationType 会话类型
  21 + * @param conversationId 会话编号
  22 + * @param readMessageId 已读到的最大消息编号
  23 + * @return 读位置是否前进;true 时调用方才需要下发已读 / 回执事件
  24 + */
  25 + boolean updateConversationReadPosition(Long userId, Integer conversationType, Long conversationId, Long readMessageId);
  26 +
  27 + /**
  28 + * 获取用户在某会话的最大已读位置
  29 + *
  30 + * @param userId 用户编号
  31 + * @param conversationType 会话类型
  32 + * @param conversationId 会话编号
  33 + * @return 最大已读消息编号;不存在则返回 null
  34 + */
  35 + Long getConversationReadMessageId(Long userId, Integer conversationType, Long conversationId);
  36 +
  37 + /**
  38 + * 获取某会话内所有用户的读位置(用于群回执人数聚合)
  39 + *
  40 + * @param conversationType 会话类型
  41 + * @param conversationId 会话编号
  42 + * @return userId → 最大已读消息编号
  43 + */
  44 + Map<Long, Long> getUserReadMessageIdMap(Integer conversationType, Long conversationId);
  45 +
  46 + /**
  47 + * 批量获取某用户在多个会话的读位置(用于频道批量读位置、重连后按活跃会话补偿)
  48 + *
  49 + * @param userId 用户编号
  50 + * @param conversationType 会话类型
  51 + * @param conversationIds 会话编号集合
  52 + * @return conversationId → 最大已读消息编号
  53 + */
  54 + Map<Long, Long> getConversationReadMessageIdMap(Long userId, Integer conversationType, Collection<Long> conversationIds);
  55 +
  56 + /**
  57 + * 增量拉取当前用户的会话读位置(重连 / 离线补偿:按 update_time + id 游标)
  58 + *
  59 + * @param userId 用户编号
  60 + * @param lastUpdateTime 上次拉取到的最新更新时间(毫秒时间戳);首次拉取传 null
  61 + * @param lastId 上次拉取到的最后一条记录 id;首次拉取传 null
  62 + * @param limit 单次拉取条数
  63 + * @return 会话读位置列表,按更新时间、id 正序
  64 + */
  65 + List<ImConversationReadDO> pullConversationReadList(Long userId, Long lastUpdateTime, Long lastId, Integer limit);
  66 +
  67 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/conversation/ImConversationReadServiceImpl.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.service.conversation;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.iocoder.yudao.module.im.dal.dataobject.conversation.ImConversationReadDO;
  5 +import cn.iocoder.yudao.module.im.dal.mysql.conversation.ImConversationReadMapper;
  6 +import jakarta.annotation.Resource;
  7 +import lombok.extern.slf4j.Slf4j;
  8 +import org.springframework.dao.DuplicateKeyException;
  9 +import org.springframework.stereotype.Service;
  10 +
  11 +import java.time.LocalDateTime;
  12 +import java.util.Collection;
  13 +import java.util.Collections;
  14 +import java.util.List;
  15 +import java.util.Map;
  16 +
  17 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
  18 +
  19 +/**
  20 + * IM 会话读位置 Service 实现类
  21 + *
  22 + * @author 芋道源码
  23 + */
  24 +@Service
  25 +@Slf4j
  26 +public class ImConversationReadServiceImpl implements ImConversationReadService {
  27 +
  28 + @Resource
  29 + private ImConversationReadMapper conversationReadMapper;
  30 +
  31 + @Override
  32 + public boolean updateConversationReadPosition(Long userId, Integer conversationType, Long conversationId, Long readMessageId) {
  33 + LocalDateTime now = LocalDateTime.now();
  34 + // 1. 不存在则插入;并发下唯一键冲突,降级为回查 + CAS 更新
  35 + ImConversationReadDO existing = conversationReadMapper.selectByUserIdAndConversation(
  36 + userId, conversationType, conversationId);
  37 + if (existing == null) {
  38 + try {
  39 + conversationReadMapper.insert(new ImConversationReadDO().setUserId(userId)
  40 + .setConversationType(conversationType).setTargetId(conversationId)
  41 + .setMessageId(readMessageId).setReadTime(now));
  42 + return true;
  43 + } catch (DuplicateKeyException e) {
  44 + log.warn("[updateConversationReadPosition][userId({}) type({}) conversationId({}) 并发插入冲突,回查更新]",
  45 + userId, conversationType, conversationId);
  46 + existing = conversationReadMapper.selectByUserIdAndConversation(userId, conversationType, conversationId);
  47 + }
  48 + }
  49 + if (existing == null) {
  50 + return false;
  51 + }
  52 +
  53 + // 2. CAS 单调更新:mapper 内 WHERE message_id < ? 保证乱序 / 并发不回退;影响行数 > 0 即读位置前进
  54 + return conversationReadMapper.updateReadMessageIdToLarger(existing.getId(), readMessageId, now) > 0;
  55 + }
  56 +
  57 + @Override
  58 + public Long getConversationReadMessageId(Long userId, Integer conversationType, Long conversationId) {
  59 + ImConversationReadDO read = conversationReadMapper.selectByUserIdAndConversation(
  60 + userId, conversationType, conversationId);
  61 + return read != null ? read.getMessageId() : null;
  62 + }
  63 +
  64 + @Override
  65 + public Map<Long, Long> getUserReadMessageIdMap(Integer conversationType, Long conversationId) {
  66 + return convertMap(conversationReadMapper.selectListByConversation(conversationType, conversationId),
  67 + ImConversationReadDO::getUserId, ImConversationReadDO::getMessageId);
  68 + }
  69 +
  70 + @Override
  71 + public Map<Long, Long> getConversationReadMessageIdMap(Long userId, Integer conversationType,
  72 + Collection<Long> conversationIds) {
  73 + if (CollUtil.isEmpty(conversationIds)) {
  74 + return Collections.emptyMap();
  75 + }
  76 + List<ImConversationReadDO> list = conversationReadMapper.selectListByUserIdAndConversations(
  77 + userId, conversationType, conversationIds);
  78 + return convertMap(list, ImConversationReadDO::getTargetId, ImConversationReadDO::getMessageId);
  79 + }
  80 +
  81 + @Override
  82 + public List<ImConversationReadDO> pullConversationReadList(Long userId, Long lastUpdateTime, Long lastId, Integer limit) {
  83 + return conversationReadMapper.selectPullListByUserId(userId, lastUpdateTime, lastId, limit);
  84 + }
  85 +
  86 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendRequestService.java
... ... @@ -51,6 +51,17 @@ public interface ImFriendRequestService {
51 51 List<ImFriendRequestDO> getMyFriendRequestList(Long userId, Long maxId, Integer limit);
52 52  
53 53 /**
  54 + * 增量拉取「我相关」的好友申请(重连 / 离线补偿:双向 OR,按 update_time + id 游标)
  55 + *
  56 + * @param userId 用户编号
  57 + * @param lastUpdateTime 上次拉取到的最新更新时间(毫秒时间戳);首次拉取传 null
  58 + * @param lastId 上次拉取到的最后一条记录 id;首次拉取传 null
  59 + * @param limit 单次拉取条数
  60 + * @return 申请记录列表,按更新时间、id 正序
  61 + */
  62 + List<ImFriendRequestDO> pullFriendRequestList(Long userId, Long lastUpdateTime, Long lastId, Integer limit);
  63 +
  64 + /**
54 65 * 按 id 单查申请记录;通用读接口,调用方自行做越权过滤
55 66 */
56 67 ImFriendRequestDO getFriendRequest(Long id);
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendRequestServiceImpl.java
... ... @@ -14,13 +14,13 @@ import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendRequestDO;
14 14 import cn.iocoder.yudao.module.im.dal.mysql.friend.ImFriendRequestMapper;
15 15 import cn.iocoder.yudao.module.im.enums.friend.ImFriendRequestHandleResultEnum;
16 16 import cn.iocoder.yudao.module.im.enums.friend.ImFriendStateEnum;
17   -import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
  17 +import cn.iocoder.yudao.module.im.enums.ImContentTypeEnum;
  18 +import cn.iocoder.yudao.module.im.enums.ImConversationTypeEnum;
18 19 import cn.iocoder.yudao.module.im.framework.config.ImProperties;
19 20 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService;
20   -import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO;
21   -import cn.iocoder.yudao.module.im.service.websocket.dto.notification.friend.FriendRequestApprovedNotification;
22   -import cn.iocoder.yudao.module.im.service.websocket.dto.notification.friend.FriendRequestNotification;
23   -import cn.iocoder.yudao.module.im.service.websocket.dto.notification.friend.FriendRequestRejectedNotification;
  21 +import cn.iocoder.yudao.module.im.service.websocket.notification.friend.FriendRequestApprovedNotification;
  22 +import cn.iocoder.yudao.module.im.service.websocket.notification.friend.FriendRequestNotification;
  23 +import cn.iocoder.yudao.module.im.service.websocket.notification.friend.FriendRequestRejectedNotification;
24 24 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
25 25 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
26 26 import jakarta.annotation.Resource;
... ... @@ -105,8 +105,8 @@ public class ImFriendRequestServiceImpl implements ImFriendRequestService {
105 105 if (fromUser != null) {
106 106 payload.setFromNickname(fromUser.getNickname()).setFromAvatar(fromUser.getAvatar());
107 107 }
108   - websocketService.sendPrivateMessageAsync(toUserId, ImPrivateMessageDTO.ofFriendNotification(
109   - ImMessageTypeEnum.FRIEND_REQUEST_RECEIVED.getType(), fromUserId, toUserId, payload));
  108 + websocketService.sendNotificationAsync(toUserId, ImConversationTypeEnum.NONE.getType(),
  109 + ImContentTypeEnum.FRIEND_REQUEST_RECEIVED.getType(), payload);
110 110  
111 111 // 4. 全局自动通过开关:注册 afterCommit 回调,事务提交后再走同意流程
112 112 // 回调内 try/catch 兜底 —— afterCommit 异常会被 Spring 静默吞掉,否则同意失败时申请方永远等不到 APPROVED
... ... @@ -172,8 +172,6 @@ public class ImFriendRequestServiceImpl implements ImFriendRequestService {
172 172 public void agreeFriendRequest(Long userId, Long requestId) {
173 173 // 1.1 校验申请存在、未处理、操作人是接收方
174 174 ImFriendRequestDO request = validateRequestForHandle(userId, requestId);
175   - // 1.2 复验双方用户有效
176   - adminUserApi.validateUserList(ListUtil.of(request.getFromUserId(), request.getToUserId()));
177 175  
178 176 // 2. 乐观锁更新申请处理结果
179 177 ImFriendRequestDO updateObj = new ImFriendRequestDO()
... ... @@ -192,8 +190,8 @@ public class ImFriendRequestServiceImpl implements ImFriendRequestService {
192 190 FriendRequestApprovedNotification payload = (FriendRequestApprovedNotification)
193 191 new FriendRequestApprovedNotification().setRequestId(request.getId())
194 192 .setOperatorUserId(userId).setFriendUserId(userId);
195   - websocketService.sendPrivateMessageAsync(request.getFromUserId(), ImPrivateMessageDTO.ofFriendNotification(
196   - ImMessageTypeEnum.FRIEND_REQUEST_APPROVED.getType(), userId, request.getFromUserId(), payload));
  193 + websocketService.sendNotificationAsync(request.getFromUserId(), ImConversationTypeEnum.NONE.getType(),
  194 + ImContentTypeEnum.FRIEND_REQUEST_APPROVED.getType(), payload);
197 195 }
198 196  
199 197 @Override
... ... @@ -217,8 +215,8 @@ public class ImFriendRequestServiceImpl implements ImFriendRequestService {
217 215 new FriendRequestRejectedNotification().setRequestId(request.getId())
218 216 .setHandleContent(handleContent)
219 217 .setOperatorUserId(userId).setFriendUserId(userId);
220   - websocketService.sendPrivateMessageAsync(request.getFromUserId(), ImPrivateMessageDTO.ofFriendNotification(
221   - ImMessageTypeEnum.FRIEND_REQUEST_REJECTED.getType(), userId, request.getFromUserId(), payload));
  218 + websocketService.sendNotificationAsync(request.getFromUserId(), ImConversationTypeEnum.NONE.getType(),
  219 + ImContentTypeEnum.FRIEND_REQUEST_REJECTED.getType(), payload);
222 220 }
223 221  
224 222 @Override
... ... @@ -234,6 +232,11 @@ public class ImFriendRequestServiceImpl implements ImFriendRequestService {
234 232 }
235 233  
236 234 @Override
  235 + public List<ImFriendRequestDO> pullFriendRequestList(Long userId, Long lastUpdateTime, Long lastId, Integer limit) {
  236 + return friendRequestMapper.selectPullListByUserId(userId, lastUpdateTime, lastId, limit);
  237 + }
  238 +
  239 + @Override
237 240 public ImFriendRequestDO getFriendRequest(Long id) {
238 241 return friendRequestMapper.selectById(id);
239 242 }
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendService.java
... ... @@ -43,6 +43,11 @@ public interface ImFriendService {
43 43 List<ImFriendDO> getFriendList(Long userId);
44 44  
45 45 /**
  46 + * 增量拉取当前用户的好友关系(重连 / 离线补偿:含已删除,按 update_time + id 游标)
  47 + */
  48 + List<ImFriendDO> pullFriendList(Long userId, Long lastUpdateTime, Long lastId, Integer limit);
  49 +
  50 + /**
46 51 * 获得当前用户的有效好友列表(仅 ENABLE 状态)
47 52 */
48 53 List<ImFriendDO> getEnableFriendList(Long userId);
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImpl.java
... ... @@ -11,12 +11,12 @@ import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO;
11 11 import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendRequestDO;
12 12 import cn.iocoder.yudao.module.im.dal.mysql.friend.ImFriendMapper;
13 13 import cn.iocoder.yudao.module.im.enums.friend.ImFriendStateEnum;
14   -import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
  14 +import cn.iocoder.yudao.module.im.enums.ImContentTypeEnum;
  15 +import cn.iocoder.yudao.module.im.enums.ImConversationTypeEnum;
15 16 import cn.iocoder.yudao.module.im.service.message.ImPrivateMessageService;
16 17 import cn.iocoder.yudao.module.im.service.message.dto.ImPrivateMessageSendDTO;
17 18 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService;
18   -import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO;
19   -import cn.iocoder.yudao.module.im.service.websocket.dto.notification.friend.*;
  19 +import cn.iocoder.yudao.module.im.service.websocket.notification.friend.*;
20 20 import jakarta.annotation.Resource;
21 21 import lombok.extern.slf4j.Slf4j;
22 22 import org.springframework.cache.annotation.CacheEvict;
... ... @@ -99,6 +99,11 @@ public class ImFriendServiceImpl implements ImFriendService {
99 99 }
100 100  
101 101 @Override
  102 + public List<ImFriendDO> pullFriendList(Long userId, Long lastUpdateTime, Long lastId, Integer limit) {
  103 + return friendMapper.selectPullListByUserId(userId, lastUpdateTime, lastId, limit);
  104 + }
  105 +
  106 + @Override
102 107 public List<ImFriendDO> getEnableFriendList(Long userId) {
103 108 return friendMapper.selectListByUserIdAndStatus(userId, CommonStatusEnum.ENABLE.getStatus());
104 109 }
... ... @@ -155,8 +160,8 @@ public class ImFriendServiceImpl implements ImFriendService {
155 160 FriendUpdateNotification payload = (FriendUpdateNotification) new FriendUpdateNotification()
156 161 .setDisplayName(reqVO.getDisplayName()).setSilent(reqVO.getSilent()).setPinned(reqVO.getPinned())
157 162 .setOperatorUserId(userId).setFriendUserId(reqVO.getFriendUserId());
158   - websocketService.sendPrivateMessageAsync(userId, ImPrivateMessageDTO.ofFriendNotification(
159   - ImMessageTypeEnum.FRIEND_UPDATE.getType(), userId, userId, payload));
  163 + websocketService.sendNotificationAsync(userId, ImConversationTypeEnum.NONE.getType(),
  164 + ImContentTypeEnum.FRIEND_UPDATE.getType(), payload);
160 165 }
161 166  
162 167 @Override
... ... @@ -178,7 +183,7 @@ public class ImFriendServiceImpl implements ImFriendService {
178 183 FriendAddNotification payload = (FriendAddNotification) new FriendAddNotification()
179 184 .setOperatorUserId(fromUserId).setFriendUserId(toUserId);
180 185 privateMessageService.sendPrivateMessage(fromUserId, new ImPrivateMessageSendDTO()
181   - .setReceiverId(toUserId).setType(ImMessageTypeEnum.FRIEND_ADD.getType()).setContent(payload));
  186 + .setReceiverId(toUserId).setType(ImContentTypeEnum.FRIEND_ADD.getType()).setContent(payload));
182 187 }
183 188  
184 189 @Override
... ... @@ -197,7 +202,7 @@ public class ImFriendServiceImpl implements ImFriendService {
197 202 FriendAddNotification payload = (FriendAddNotification) new FriendAddNotification()
198 203 .setOperatorUserId(friendUserId).setFriendUserId(friendUserId);
199 204 privateMessageService.sendPrivateMessage(userId, new ImPrivateMessageSendDTO()
200   - .setReceiverId(friendUserId).setType(ImMessageTypeEnum.FRIEND_ADD.getType())
  205 + .setReceiverId(friendUserId).setType(ImContentTypeEnum.FRIEND_ADD.getType())
201 206 .setContent(payload).setPersistent(false));
202 207 }
203 208  
... ... @@ -219,7 +224,7 @@ public class ImFriendServiceImpl implements ImFriendService {
219 224 FriendDeleteNotification payload = ((FriendDeleteNotification) new FriendDeleteNotification()
220 225 .setOperatorUserId(userId).setFriendUserId(friendUserId)).setClear(clear);
221 226 privateMessageService.sendPrivateMessage(userId, new ImPrivateMessageSendDTO()
222   - .setReceiverId(friendUserId).setType(ImMessageTypeEnum.FRIEND_DELETE.getType())
  227 + .setReceiverId(friendUserId).setType(ImContentTypeEnum.FRIEND_DELETE.getType())
223 228 .setContent(payload).setPersistent(false));
224 229 }
225 230  
... ... @@ -245,8 +250,8 @@ public class ImFriendServiceImpl implements ImFriendService {
245 250 // 3. 推 FRIEND_BLOCK 给 A 多端
246 251 FriendBlockNotification payload = (FriendBlockNotification) new FriendBlockNotification()
247 252 .setOperatorUserId(userId).setFriendUserId(friendUserId);
248   - websocketService.sendPrivateMessageAsync(userId, ImPrivateMessageDTO.ofFriendNotification(
249   - ImMessageTypeEnum.FRIEND_BLOCK.getType(), userId, userId, payload));
  253 + websocketService.sendNotificationAsync(userId, ImConversationTypeEnum.NONE.getType(),
  254 + ImContentTypeEnum.FRIEND_BLOCK.getType(), payload);
250 255 }
251 256  
252 257 @Override
... ... @@ -271,8 +276,8 @@ public class ImFriendServiceImpl implements ImFriendService {
271 276 // 3. 推 FRIEND_UNBLOCK 给 A 多端
272 277 FriendUnblockNotification payload = (FriendUnblockNotification) new FriendUnblockNotification()
273 278 .setOperatorUserId(userId).setFriendUserId(friendUserId);
274   - websocketService.sendPrivateMessageAsync(userId, ImPrivateMessageDTO.ofFriendNotification(
275   - ImMessageTypeEnum.FRIEND_UNBLOCK.getType(), userId, userId, payload));
  279 + websocketService.sendNotificationAsync(userId, ImConversationTypeEnum.NONE.getType(),
  280 + ImContentTypeEnum.FRIEND_UNBLOCK.getType(), payload);
276 281 }
277 282  
278 283 /**
... ... @@ -294,7 +299,8 @@ public class ImFriendServiceImpl implements ImFriendService {
294 299 }
295 300 // 情况二:复用 DISABLE 旧记录 → 恢复 ENABLE + 重置 silent / pinned / blocked,对齐"重新加好友"语义
296 301 if (exists != null) {
297   - friendMapper.updateReAddFields(exists.getId(), CommonStatusEnum.ENABLE.getStatus(), LocalDateTime.now(),
  302 + LocalDateTime now = LocalDateTime.now();
  303 + friendMapper.updateReAddFields(exists.getId(), CommonStatusEnum.ENABLE.getStatus(), now, now,
298 304 false, false, false, displayName, addSource);
299 305 return;
300 306 }
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberService.java
... ... @@ -87,13 +87,12 @@ public interface ImGroupMemberService {
87 87 List<ImGroupMemberDO> getActiveGroupMemberListByUserId(Long userId);
88 88  
89 89 /**
90   - * 查询用户已退群的群成员记录(DISABLE 状态)
  90 + * 查询用户曾经加入的所有群成员记录(含已退群)
91 91 *
92   - * @param userId 用户编号,必传
93   - * @param minQuitTime 最早退群时间(含),可空
94   - * @return 已退群成员记录列表
  92 + * @param userId 用户编号
  93 + * @return 群成员记录列表
95 94 */
96   - List<ImGroupMemberDO> getQuitGroupMemberListByUserId(Long userId, LocalDateTime minQuitTime);
  95 + List<ImGroupMemberDO> getGroupMemberListByUserId(Long userId);
97 96  
98 97 /**
99 98 * 添加群成员(入群),角色默认 MEMBER
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberServiceImpl.java
... ... @@ -77,7 +77,6 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService {
77 77  
78 78 @Override
79 79 public List<ImGroupMemberDO> getGroupMemberListByOwnerAndAdmin(Long groupId) {
80   - // TODO DONE @AI:把条件往下传;这样减少加载数据量!
81 80 return groupMemberMapper.selectListByGroupIdAndStatusAndRoles(groupId, CommonStatusEnum.ENABLE.getStatus(),
82 81 ListUtil.of(ImGroupMemberRoleEnum.OWNER.getRole(), ImGroupMemberRoleEnum.ADMIN.getRole()));
83 82 }
... ... @@ -106,8 +105,8 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService {
106 105 }
107 106  
108 107 @Override
109   - public List<ImGroupMemberDO> getQuitGroupMemberListByUserId(Long userId, LocalDateTime minQuitTime) {
110   - return groupMemberMapper.selectQuitListByUserId(userId, minQuitTime);
  108 + public List<ImGroupMemberDO> getGroupMemberListByUserId(Long userId) {
  109 + return groupMemberMapper.selectListByUserId(userId);
111 110 }
112 111  
113 112 @Override
... ... @@ -259,7 +258,7 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService {
259 258 .setId(member.getId());
260 259 groupMemberMapper.updateById(updateObj);
261 260  
262   - // 3.1 displayUserName 是公开字段,单独走 GROUP_MEMBER_NICKNAME_UPDATE 广播给全员;空串视为「清空昵称」也要广播;与旧值相同跳过
  261 + // 3.1 displayUserName 是公开字段,单独走 GROUP_MEMBER_NICKNAME_UPDATE 在线同步给全员;空串视为「清空昵称」也要同步;与旧值相同跳过
263 262 if (updateReqVO.getDisplayUserName() != null
264 263 && ObjUtil.notEqual(updateReqVO.getDisplayUserName(), member.getDisplayUserName())) {
265 264 groupMessageService.sendGroupMessage(userId, ImGroupMessageSendDTO.ofGroupMemberNicknameUpdate(
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupRequestService.java
... ... @@ -64,6 +64,19 @@ public interface ImGroupRequestService {
64 64 List<ImGroupRequestDO> getUnhandledRequestListByOwnerOrAdmin(Long userId);
65 65  
66 66 /**
  67 + * 增量拉取「我管理的群」下的加群申请(重连 / 离线补偿:含已处理,按 update_time + id 游标)
  68 + * <p>
  69 + * 作用域与 {@link #getUnhandledRequestListByOwnerOrAdmin} 一致:按 ImGroupMember.role 取我作为 OWNER / ADMIN 的群
  70 + *
  71 + * @param userId 当前用户编号
  72 + * @param lastUpdateTime 游标:上次拉取的最后一条更新时间戳(毫秒)
  73 + * @param lastId 游标:上次拉取的最后一条申请编号
  74 + * @param limit 单次拉取条数
  75 + * @return 申请记录列表
  76 + */
  77 + List<ImGroupRequestDO> pullGroupRequestList(Long userId, Long lastUpdateTime, Long lastId, Integer limit);
  78 +
  79 + /**
67 80 * 拉取指定群下的全部加群申请(含已处理);仅群主 / 管理员可查
68 81 * <p>
69 82 * 用于群「进群申请」子页:最新一条卡片化突出 + 历史申请按 id 倒序
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupRequestServiceImpl.java
... ... @@ -13,15 +13,15 @@ import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupRequestMapper;
13 13 import cn.iocoder.yudao.module.im.enums.group.ImGroupAddSourceEnum;
14 14 import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum;
15 15 import cn.iocoder.yudao.module.im.enums.group.ImGroupRequestHandleResultEnum;
16   -import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
  16 +import cn.iocoder.yudao.module.im.enums.ImContentTypeEnum;
  17 +import cn.iocoder.yudao.module.im.enums.ImConversationTypeEnum;
17 18 import cn.iocoder.yudao.module.im.service.message.ImGroupMessageService;
18 19 import cn.iocoder.yudao.module.im.service.message.dto.ImGroupMessageSendDTO;
19 20 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService;
20   -import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO;
21   -import cn.iocoder.yudao.module.im.service.websocket.dto.notification.group.BaseGroupNotification;
22   -import cn.iocoder.yudao.module.im.service.websocket.dto.notification.group.GroupRequestApprovedNotification;
23   -import cn.iocoder.yudao.module.im.service.websocket.dto.notification.group.GroupRequestReceivedNotification;
24   -import cn.iocoder.yudao.module.im.service.websocket.dto.notification.group.GroupRequestRejectedNotification;
  21 +import cn.iocoder.yudao.module.im.service.websocket.notification.group.BaseGroupNotification;
  22 +import cn.iocoder.yudao.module.im.service.websocket.notification.group.GroupRequestApprovedNotification;
  23 +import cn.iocoder.yudao.module.im.service.websocket.notification.group.GroupRequestReceivedNotification;
  24 +import cn.iocoder.yudao.module.im.service.websocket.notification.group.GroupRequestRejectedNotification;
25 25 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
26 26 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
27 27 import jakarta.annotation.Resource;
... ... @@ -97,12 +97,12 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService {
97 97 // 3. 情况二:群开启了审批,创建或复用一条主动申请记录
98 98 ImGroupRequestDO request = createOrResetApplyRequest(groupId, userId, reqVO);
99 99  
100   - // 4. 1503 私聊定向推群主 + 全部管理员(多端同步);payload 携带申请方昵称 / 头像
  100 + // 4. 1503 定向推群主 + 全部管理员(多端同步);payload 携带申请方昵称 / 头像
101 101 AdminUserRespDTO applyUser = adminUserApi.getUser(userId);
102 102 GroupRequestReceivedNotification payload = buildRequestNotification(group, request, applyUser);
103 103 for (Long receiverUserId : getGroupMemberListByOwnerAndAdminUserIds(group)) {
104   - websocketService.sendPrivateMessageAsync(receiverUserId, ImPrivateMessageDTO.ofGroupNotification(
105   - ImMessageTypeEnum.GROUP_REQUEST_RECEIVED.getType(), userId, receiverUserId, payload));
  104 + websocketService.sendNotificationAsync(receiverUserId, ImConversationTypeEnum.NONE.getType(),
  105 + ImContentTypeEnum.GROUP_REQUEST_RECEIVED.getType(), payload);
106 106 }
107 107 return request;
108 108 }
... ... @@ -139,12 +139,12 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService {
139 139 groupMemberService.addGroupMember(request.getGroupId(), request.getUserId(),
140 140 ImGroupMemberRoleEnum.NORMAL.getRole(), request.getAddSource(), request.getInviterUserId());
141 141  
142   - // 5.1 1505 私聊推送给申请人 + 群主 + 全部管理员(每端单推)
  142 + // 5.1 1505 定向推送给申请人 + 群主 + 全部管理员(每端单推)
143 143 GroupRequestApprovedNotification payload = (GroupRequestApprovedNotification) new GroupRequestApprovedNotification()
144 144 .setRequestId(request.getId()).setGroupId(request.getGroupId()).setUserId(request.getUserId())
145 145 .setOperatorUserId(userId);
146 146 broadcastToOwnerAdminsAndApplicant(request.getGroupId(), request.getUserId(), payload,
147   - ImMessageTypeEnum.GROUP_REQUEST_APPROVED.getType(), userId);
  147 + ImContentTypeEnum.GROUP_REQUEST_APPROVED.getType(), userId);
148 148 // 5.2 群事件:主动申请 → 1510 自由进群;被邀请 → 1509 成员加入
149 149 if (request.getInviterUserId() == null) {
150 150 groupMessageService.sendGroupMessage(userId,
... ... @@ -174,12 +174,12 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService {
174 174 throw exception(GROUP_REQUEST_HANDLED);
175 175 }
176 176  
177   - // 3. 1506 私聊推送给申请人 + 群主 + 全部管理员
  177 + // 3. 1506 定向推送给申请人 + 群主 + 全部管理员
178 178 GroupRequestRejectedNotification payload = (GroupRequestRejectedNotification) new GroupRequestRejectedNotification()
179 179 .setRequestId(request.getId()).setGroupId(request.getGroupId()).setUserId(request.getUserId())
180 180 .setHandleContent(handleContent).setOperatorUserId(userId);
181 181 broadcastToOwnerAdminsAndApplicant(request.getGroupId(), request.getUserId(), payload,
182   - ImMessageTypeEnum.GROUP_REQUEST_REJECTED.getType(), userId);
  182 + ImContentTypeEnum.GROUP_REQUEST_REJECTED.getType(), userId);
183 183 }
184 184  
185 185 @Override
... ... @@ -201,8 +201,8 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService {
201 201 AdminUserRespDTO applyUser = userMap.get(request.getUserId());
202 202 GroupRequestReceivedNotification payload = buildRequestNotification(group, request, applyUser);
203 203 for (Long receiverUserId : ownerAndAdmins) {
204   - websocketService.sendPrivateMessageAsync(receiverUserId, ImPrivateMessageDTO.ofGroupNotification(
205   - ImMessageTypeEnum.GROUP_REQUEST_RECEIVED.getType(), inviterUserId, receiverUserId, payload));
  204 + websocketService.sendNotificationAsync(receiverUserId, ImConversationTypeEnum.NONE.getType(),
  205 + ImContentTypeEnum.GROUP_REQUEST_RECEIVED.getType(), payload);
206 206 }
207 207 }
208 208 }
... ... @@ -212,8 +212,7 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService {
212 212 // 1. 找出当前用户作为 OWNER / ADMIN 的所有群
213 213 List<ImGroupMemberDO> myMembers = groupMemberService.getActiveGroupMemberListByUserId(userId);
214 214 Set<Long> ownerOrAdminGroupIds = convertSet(myMembers,
215   - ImGroupMemberDO::getGroupId,
216   - m -> ImGroupMemberRoleEnum.isOwnerOrAdmin(m.getRole()));
  215 + ImGroupMemberDO::getGroupId, member -> ImGroupMemberRoleEnum.isOwnerOrAdmin(member.getRole()));
217 216 if (CollUtil.isEmpty(ownerOrAdminGroupIds)) {
218 217 return Collections.emptyList();
219 218 }
... ... @@ -223,6 +222,19 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService {
223 222 }
224 223  
225 224 @Override
  225 + public List<ImGroupRequestDO> pullGroupRequestList(Long userId, Long lastUpdateTime, Long lastId, Integer limit) {
  226 + // 1. 找出当前用户作为 OWNER / ADMIN 的所有群
  227 + List<ImGroupMemberDO> myMembers = groupMemberService.getActiveGroupMemberListByUserId(userId);
  228 + Set<Long> ownerOrAdminGroupIds = convertSet(myMembers,
  229 + ImGroupMemberDO::getGroupId, member -> ImGroupMemberRoleEnum.isOwnerOrAdmin(member.getRole()));
  230 + if (CollUtil.isEmpty(ownerOrAdminGroupIds)) {
  231 + return Collections.emptyList();
  232 + }
  233 + // 2. 按游标增量拉取这些群下的申请
  234 + return groupRequestMapper.selectPullListByGroupIds(ownerOrAdminGroupIds, lastUpdateTime, lastId, limit);
  235 + }
  236 +
  237 + @Override
226 238 public List<ImGroupRequestDO> getGroupRequestListByGroupId(Long userId, Long groupId) {
227 239 // 1. 校验群存在 + 当前用户是群主 / 管理员
228 240 groupService.validateGroupExists(groupId);
... ... @@ -383,7 +395,7 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService {
383 395 }
384 396  
385 397 /**
386   - * 1505 / 1506 受众:申请人 + 群主 + 全部管理员;每端单独推一帧,前端按 receiver 是否申请人区分文案
  398 + * 1505 / 1506 受众:申请人 + 群主 + 全部管理员
387 399 */
388 400 private void broadcastToOwnerAdminsAndApplicant(Long groupId, Long applicantUserId, BaseGroupNotification payload,
389 401 Integer messageType, Long operatorUserId) {
... ... @@ -394,8 +406,8 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService {
394 406 Set<Long> receivers = new LinkedHashSet<>(getGroupMemberListByOwnerAndAdminUserIds(group));
395 407 receivers.add(applicantUserId);
396 408 for (Long receiverUserId : receivers) {
397   - websocketService.sendPrivateMessageAsync(receiverUserId, ImPrivateMessageDTO.ofGroupNotification(
398   - messageType, operatorUserId, receiverUserId, payload));
  409 + websocketService.sendNotificationAsync(receiverUserId, ImConversationTypeEnum.NONE.getType(),
  410 + messageType, payload);
399 411 }
400 412 }
401 413  
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImpl.java
... ... @@ -30,7 +30,7 @@ import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupMapper;
30 30 import cn.iocoder.yudao.module.im.enums.group.ImGroupAddSourceEnum;
31 31 import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum;
32 32 import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum;
33   -import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
  33 +import cn.iocoder.yudao.module.im.enums.ImContentTypeEnum;
34 34 import cn.iocoder.yudao.module.im.framework.config.ImProperties;
35 35 import cn.iocoder.yudao.module.im.service.friend.ImFriendService;
36 36 import cn.iocoder.yudao.module.im.service.message.ImGroupMessageService;
... ... @@ -149,7 +149,9 @@ public class ImGroupServiceImpl implements ImGroupService {
149 149 boolean nameChanged = StrUtil.isNotEmpty(updateReqVO.getName());
150 150 boolean noticeChanged = updateReqVO.getNotice() != null;
151 151 boolean avatarChanged = StrUtil.isNotEmpty(updateReqVO.getAvatar());
152   - if (nameChanged || noticeChanged || avatarChanged) {
  152 + boolean joinApprovalChanged = updateReqVO.getJoinApproval() != null
  153 + && ObjUtil.notEqual(group.getJoinApproval(), updateReqVO.getJoinApproval());
  154 + if (nameChanged || noticeChanged || avatarChanged || joinApprovalChanged) {
153 155 List<Long> memberUserIds = groupMemberService.getActiveGroupMemberUserIdsByGroupId(groupId);
154 156 if (nameChanged) {
155 157 groupMessageService.sendGroupMessage(userId, memberUserIds, ImGroupMessageSendDTO.ofGroupNameUpdate(
... ... @@ -161,7 +163,11 @@ public class ImGroupServiceImpl implements ImGroupService {
161 163 }
162 164 if (avatarChanged) {
163 165 groupMessageService.sendGroupMessage(userId, memberUserIds, ImGroupMessageSendDTO.ofGroupInfoUpdate(
164   - groupId, userId, group.getAvatar(), updateReqVO.getAvatar()));
  166 + groupId, userId, group.getAvatar(), updateReqVO.getAvatar(), null, null));
  167 + }
  168 + if (joinApprovalChanged) {
  169 + groupMessageService.sendGroupMessage(userId, memberUserIds, ImGroupMessageSendDTO.ofGroupInfoUpdate(
  170 + groupId, userId, null, null, group.getJoinApproval(), updateReqVO.getJoinApproval()));
165 171 }
166 172 }
167 173  
... ... @@ -193,8 +199,6 @@ public class ImGroupServiceImpl implements ImGroupService {
193 199 .setStatus(CommonStatusEnum.DISABLE.getStatus()).setDissolvedTime(LocalDateTime.now()));
194 200 // 2.2 移除全部群成员
195 201 groupMemberService.removeGroupMembersByGroupId(id);
196   - // 2.3 清理已读缓存
197   - groupMessageService.deleteReadMaxMessageIdMap(id);
198 202 }
199 203  
200 204 // ==================== 群成员的写操作 ====================
... ... @@ -267,10 +271,8 @@ public class ImGroupServiceImpl implements ImGroupService {
267 271 // 2. 先发广播,后移成员(见类 javadoc)
268 272 groupMessageService.sendGroupMessage(userId, ImGroupMessageSendDTO.ofGroupMemberQuit(groupId, userId));
269 273  
270   - // 3.1 移除群成员
  274 + // 3. 移除群成员
271 275 groupMemberService.removeGroupMember(groupId, userId);
272   - // 3.2 清理已读缓存
273   - groupMessageService.deleteReadMaxMessageId(groupId, userId);
274 276 }
275 277  
276 278 @Override
... ... @@ -307,10 +309,8 @@ public class ImGroupServiceImpl implements ImGroupService {
307 309 groupMessageService.sendGroupMessage(userId,
308 310 ImGroupMessageSendDTO.ofGroupMemberKick(groupId, userId, validTargetUserIds));
309 311  
310   - // 3.1 批量移除群成员
  312 + // 3. 批量移除群成员;不清理读位置(保留退群前历史已读,供离线补偿)
311 313 groupMemberService.removeGroupMembers(groupId, validTargetUserIds);
312   - // 3.2 批量清理已读缓存
313   - groupMessageService.deleteReadMaxMessageIds(groupId, validTargetUserIds);
314 314 }
315 315  
316 316 @Override
... ... @@ -442,7 +442,7 @@ public class ImGroupServiceImpl implements ImGroupService {
442 442 if (message == null || ObjUtil.notEqual(message.getGroupId(), groupId)) {
443 443 throw exception(MESSAGE_NOT_IN_GROUP);
444 444 }
445   - if (!ImMessageTypeEnum.validate(message.getType()).isNormal()
  445 + if (!ImContentTypeEnum.validate(message.getType()).isNormal()
446 446 || ImMessageStatusEnum.RECALL.getStatus().equals(message.getStatus())) {
447 447 throw exception(MESSAGE_NOT_IN_GROUP);
448 448 }
... ... @@ -601,11 +601,8 @@ public class ImGroupServiceImpl implements ImGroupService {
601 601  
602 602 @Override
603 603 public List<ImGroupDO> getMyGroupList(Long userId) {
604   - // 1.1 查用户所在的、仍有效的群成员记录(仅 ENABLE 状态)
605   - List<ImGroupMemberDO> members = groupMemberService.getActiveGroupMemberListByUserId(userId);
606   - // 1.2 再查最近 N 天(与群消息离线拉取窗口一致)内退群的成员记录(退群前可能有离线消息需要展示,一并返回作为前端缓存)
607   - LocalDateTime minQuitTime = LocalDateTime.now().minusDays(imProperties.getMessage().getGroupPullMaxDays());
608   - members.addAll(groupMemberService.getQuitGroupMemberListByUserId(userId, minQuitTime));
  604 + // 1. 查用户曾经加入的所有群(含退群,前端按需过滤);退群前的离线消息也要能展示
  605 + List<ImGroupMemberDO> members = groupMemberService.getGroupMemberListByUserId(userId);
609 606 if (CollUtil.isEmpty(members)) {
610 607 return Collections.emptyList();
611 608 }
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImChannelMessageService.java
... ... @@ -27,7 +27,7 @@ public interface ImChannelMessageService {
27 27 * @param size 返回条数
28 28 * @return 频道消息列表;按 id 升序
29 29 */
30   - List<ImChannelMessageDO> getMessageListForPull(Long userId, Long minId, Integer size);
  30 + List<ImChannelMessageDO> pullChannelMessageList(Long userId, Long minId, Integer size);
31 31  
32 32 /**
33 33 * 上报频道消息已读位置;同步推 READ 事件给自己多端
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImChannelMessageServiceImpl.java
... ... @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.im.service.message;
2 2  
3 3 import cn.hutool.core.collection.CollUtil;
4 4 import cn.hutool.core.lang.Assert;
  5 +import cn.hutool.core.util.ObjUtil;
5 6 import cn.hutool.extra.spring.SpringUtil;
6 7 import cn.iocoder.yudao.framework.common.pojo.PageResult;
7 8 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
... ... @@ -11,12 +12,14 @@ import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.channel.Im
11 12 import cn.iocoder.yudao.module.im.dal.dataobject.channel.ImChannelMaterialDO;
12 13 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImChannelMessageDO;
13 14 import cn.iocoder.yudao.module.im.dal.mysql.message.ImChannelMessageMapper;
14   -import cn.iocoder.yudao.module.im.dal.redis.message.ImChannelMessageReadRedisDAO;
15   -import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
  15 +import cn.iocoder.yudao.module.im.enums.ImConversationTypeEnum;
  16 +import cn.iocoder.yudao.module.im.enums.ImContentTypeEnum;
16 17 import cn.iocoder.yudao.module.im.service.channel.ImChannelMaterialService;
  18 +import cn.iocoder.yudao.module.im.service.conversation.ImConversationReadService;
17 19 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService;
18   -import cn.iocoder.yudao.module.im.service.websocket.dto.ImChannelMessageDTO;
19   -import cn.iocoder.yudao.module.im.service.websocket.dto.message.MaterialMessage;
  20 +import cn.iocoder.yudao.module.im.service.websocket.notification.message.ImChannelMessageNotification;
  21 +import cn.iocoder.yudao.module.im.service.websocket.notification.message.ImMessageReadNotification;
  22 +import cn.iocoder.yudao.module.im.dal.dataobject.message.content.MaterialMessage;
20 23 import jakarta.annotation.Resource;
21 24 import org.springframework.scheduling.annotation.Async;
22 25 import org.springframework.stereotype.Service;
... ... @@ -25,7 +28,6 @@ import org.springframework.validation.annotation.Validated;
25 28  
26 29 import java.time.LocalDateTime;
27 30 import java.util.Collection;
28   -import java.util.HashMap;
29 31 import java.util.List;
30 32 import java.util.Map;
31 33  
... ... @@ -50,39 +52,41 @@ public class ImChannelMessageServiceImpl implements ImChannelMessageService {
50 52 private ImWebSocketService webSocketService;
51 53  
52 54 @Resource
53   - private ImChannelMessageReadRedisDAO channelMessageReadRedisDAO;
  55 + private ImConversationReadService conversationReadService;
54 56  
55 57 // ==================== 用户端 ====================
56 58  
57 59 @Override
58   - public List<ImChannelMessageDO> getMessageListForPull(Long userId, Long minId, Integer size) {
  60 + public List<ImChannelMessageDO> pullChannelMessageList(Long userId, Long minId, Integer size) {
59 61 return channelMessageMapper.selectListByUserAndMinId(userId, minId, size);
60 62 }
61 63  
62 64 @Override
63 65 public Map<Long, Long> getChannelReadMaxMessageIdMap(Long userId, Collection<Long> channelIds) {
64   - Map<Long, Long> result = new HashMap<>(channelIds.size());
65   - for (Long channelId : channelIds) {
66   - Long max = channelMessageReadRedisDAO.getReadMaxMessageId(channelId, userId);
67   - if (max != null) {
68   - result.put(channelId, max);
69   - }
70   - }
71   - return result;
  66 + return conversationReadService.getConversationReadMessageIdMap(
  67 + userId, ImConversationTypeEnum.CHANNEL.getType(), channelIds);
72 68 }
73 69  
74 70 @Override
75 71 public void readChannelMessages(Long userId, Long channelId, Long messageId) {
76 72 Assert.notNull(channelId, "频道编号不能为空");
77 73 Assert.notNull(messageId, "已读消息编号不能为空");
78   - // 1. 已读位置未前进,直接返回
79   - Long prevMaxMessageId = channelMessageReadRedisDAO.getReadMaxMessageId(channelId, userId);
80   - if (prevMaxMessageId != null && prevMaxMessageId >= messageId) {
  74 + // 1.1 校验消息真实存在且属于该频道,避免未来 / 伪造 messageId 污染读位置
  75 + ImChannelMessageDO message = channelMessageMapper.selectById(messageId);
  76 + if (message == null || ObjUtil.notEqual(message.getChannelId(), channelId)) {
  77 + return;
  78 + }
  79 + // 1.2 定向消息校验对当前用户可见(receiver_user_ids 为空表示全员可见)
  80 + if (CollUtil.isNotEmpty(message.getReceiverUserIds()) && !message.getReceiverUserIds().contains(userId)) {
81 81 return;
82 82 }
83 83  
84   - // 2. 更新 Redis 频道已读位置
85   - channelMessageReadRedisDAO.updateReadMaxMessageId(channelId, userId, messageId);
  84 + // 2. 更新频道已读位置;读位置未前进则不推
  85 + boolean advanced = conversationReadService.updateConversationReadPosition(
  86 + userId, ImConversationTypeEnum.CHANNEL.getType(), channelId, messageId);
  87 + if (!advanced) {
  88 + return;
  89 + }
86 90  
87 91 // 3. 异步推 READ 事件给自己多端同步
88 92 getSelf().readChannelMessageEvent(userId, channelId, messageId);
... ... @@ -93,7 +97,8 @@ public class ImChannelMessageServiceImpl implements ImChannelMessageService {
93 97 */
94 98 @Async
95 99 public void readChannelMessageEvent(Long userId, Long channelId, Long readId) {
96   - webSocketService.sendChannelMessageAsync(userId, ImChannelMessageDTO.ofRead(channelId, readId));
  100 + webSocketService.sendNotificationAsync(userId, ImConversationTypeEnum.CHANNEL.getType(),
  101 + ImContentTypeEnum.READ.getType(), ImMessageReadNotification.ofChannel(channelId, readId));
97 102 }
98 103  
99 104 private ImChannelMessageServiceImpl getSelf() {
... ... @@ -113,15 +118,16 @@ public class ImChannelMessageServiceImpl implements ImChannelMessageService {
113 118 String payloadJson = JsonUtils.toJsonString(payload);
114 119 // 2.2 落库 1 行 message;reqVO 同名字段(materialId / receiverUserIds)自动拷贝,剩余字段补 set
115 120 ImChannelMessageDO message = BeanUtils.toBean(reqVO, ImChannelMessageDO.class).setChannelId(material.getChannelId())
116   - .setType(ImMessageTypeEnum.MATERIAL.getType()).setContent(payloadJson).setSendTime(LocalDateTime.now());
  121 + .setType(ImContentTypeEnum.MATERIAL.getType()).setContent(payloadJson).setSendTime(LocalDateTime.now());
117 122 channelMessageMapper.insert(message);
118 123  
119 124 // 3. 异步推 WebSocket:指定用户走点对点;全员(receiverUserIds 为空)走广播
120   - ImChannelMessageDTO dto = ImChannelMessageDTO.ofSend(message);
  125 + ImChannelMessageNotification dto = ImChannelMessageNotification.ofSend(message);
121 126 if (CollUtil.isNotEmpty(reqVO.getReceiverUserIds())) {
122   - webSocketService.sendChannelMessageAsync(reqVO.getReceiverUserIds(), dto);
  127 + webSocketService.sendNotificationAsync(reqVO.getReceiverUserIds(), ImConversationTypeEnum.CHANNEL.getType(),
  128 + dto.getType(), dto);
123 129 } else {
124   - webSocketService.broadcastChannelMessageAsync(dto);
  130 + webSocketService.broadcastNotificationAsync(ImConversationTypeEnum.CHANNEL.getType(), dto.getType(), dto);
125 131 }
126 132 return message.getId();
127 133 }
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageService.java
... ... @@ -98,35 +98,6 @@ public interface ImGroupMessageService {
98 98 */
99 99 List<ImGroupMessageDO> getGroupMessageList(Long userId, ImGroupMessageListReqVO reqVO);
100 100  
101   - /**
102   - * 清理用户在某群的已读位置缓存
103   - * <p>
104   - * 用于成员退群场景
105   - *
106   - * @param groupId 群编号
107   - * @param userId 用户编号
108   - */
109   - void deleteReadMaxMessageId(Long groupId, Long userId);
110   -
111   - /**
112   - * 批量清理用户在某群的已读位置缓存
113   - * <p>
114   - * 用于批量踢出场景
115   - *
116   - * @param groupId 群编号
117   - * @param userIds 用户编号集合
118   - */
119   - void deleteReadMaxMessageIds(Long groupId, Collection<Long> userIds);
120   -
121   - /**
122   - * 清理某群所有用户的已读位置缓存
123   - * <p>
124   - * 用于群解散场景
125   - *
126   - * @param groupId 群编号
127   - */
128   - void deleteReadMaxMessageIdMap(Long groupId);
129   -
130 101 // ==================== 管理后台 ====================
131 102  
132 103 /**
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImpl.java
... ... @@ -6,7 +6,6 @@ import cn.hutool.core.util.BooleanUtil;
6 6 import cn.hutool.core.util.IdUtil;
7 7 import cn.hutool.core.util.ObjUtil;
8 8 import cn.hutool.extra.spring.SpringUtil;
9   -import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
10 9 import cn.iocoder.yudao.framework.common.pojo.PageResult;
11 10 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
12 11 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
... ... @@ -17,23 +16,27 @@ import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO;
17 16 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO;
18 17 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO;
19 18 import cn.iocoder.yudao.module.im.dal.mysql.message.ImGroupMessageMapper;
20   -import cn.iocoder.yudao.module.im.dal.redis.message.ImGroupMessageReadRedisDAO;
  19 +import cn.iocoder.yudao.module.im.enums.ImConversationTypeEnum;
21 20 import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum;
22   -import cn.iocoder.yudao.module.im.enums.message.ImGroupMessageReceiptStatusEnum;
  21 +import cn.iocoder.yudao.module.im.enums.message.ImMessageReceiptStatusEnum;
23 22 import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum;
24   -import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
  23 +import cn.iocoder.yudao.module.im.enums.ImContentTypeEnum;
25 24 import cn.iocoder.yudao.module.im.framework.config.ImProperties;
  25 +import cn.iocoder.yudao.module.im.service.conversation.ImConversationReadService;
26 26 import cn.iocoder.yudao.module.im.service.group.ImGroupMemberService;
27 27 import cn.iocoder.yudao.module.im.service.group.ImGroupService;
28 28 import cn.iocoder.yudao.module.im.service.message.dto.ImGroupMessageSendDTO;
29 29 import cn.iocoder.yudao.module.im.service.sensitiveword.ImSensitiveWordService;
30 30 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService;
31   -import cn.iocoder.yudao.module.im.service.websocket.dto.ImGroupMessageDTO;
32   -import cn.iocoder.yudao.module.im.service.websocket.dto.message.QuoteMessage;
33   -import cn.iocoder.yudao.module.im.service.websocket.dto.message.RecallMessage;
  31 +import cn.iocoder.yudao.module.im.service.websocket.notification.message.ImGroupMessageNotification;
  32 +import cn.iocoder.yudao.module.im.service.websocket.notification.message.ImMessageReadNotification;
  33 +import cn.iocoder.yudao.module.im.service.websocket.notification.message.ImMessageReceiptNotification;
  34 +import cn.iocoder.yudao.module.im.dal.dataobject.message.content.QuoteMessage;
  35 +import cn.iocoder.yudao.module.im.dal.dataobject.message.content.RecallMessage;
34 36 import cn.iocoder.yudao.module.im.util.ImMessageUtils;
35 37 import jakarta.annotation.Resource;
36 38 import lombok.extern.slf4j.Slf4j;
  39 +import org.springframework.dao.DuplicateKeyException;
37 40 import org.springframework.scheduling.annotation.Async;
38 41 import org.springframework.stereotype.Service;
39 42 import org.springframework.transaction.annotation.Transactional;
... ... @@ -41,7 +44,6 @@ import org.springframework.validation.annotation.Validated;
41 44  
42 45 import java.time.LocalDateTime;
43 46 import java.util.*;
44   -import java.util.stream.Collectors;
45 47  
46 48 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
47 49 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
... ... @@ -57,17 +59,8 @@ import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*;
57 59 @Slf4j
58 60 public class ImGroupMessageServiceImpl implements ImGroupMessageService {
59 61  
60   - /**
61   - * 仅用于规避群消息 pull 的"假空页":
62   - * 首批消息可能因入群时间或定向接收过滤后变成空列表,但后续更大的 id 仍然存在可见消息。
63   - * 因此仅在过滤结果为空时,按本轮消息最大 id 向后再试几次。
64   - */
65   - private static final int PULL_GROUP_MESSAGE_EMPTY_RETRY_TIMES = 3;
66   -
67 62 @Resource
68 63 private ImGroupMessageMapper groupMessageMapper;
69   - @Resource
70   - private ImGroupMessageReadRedisDAO groupMessageReadRedisDAO;
71 64  
72 65 @Resource
73 66 private ImGroupService groupService;
... ... @@ -75,6 +68,8 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService {
75 68 private ImGroupMemberService groupMemberService;
76 69 @Resource
77 70 private ImSensitiveWordService sensitiveWordService;
  71 + @Resource
  72 + private ImConversationReadService conversationReadService;
78 73  
79 74 @Resource
80 75 private ImWebSocketService imWebSocketService;
... ... @@ -100,34 +95,44 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService {
100 95 // 1.4 禁言校验
101 96 validateMuteStatus(group, senderMember);
102 97 // 1.5 文本消息敏感词过滤
103   - if (ImMessageTypeEnum.TEXT.getType().equals(reqVO.getType())) {
  98 + if (ImContentTypeEnum.TEXT.getType().equals(reqVO.getType())) {
104 99 sensitiveWordService.validateText(reqVO.getContent());
105 100 }
106 101  
107   - // 2.1 引用 quote 消息规范化
108   - reqVO.setContent(normalizeQuoteContent(reqVO, senderMember));
109   - // 2.2 构建并保存消息
  102 + // 2.1 固化发送当时可见成员快照:用户发送均为全员广播;getReceiverUserIds 兜底纳入发送者,钉死发送者必可见
  103 + List<Long> memberUserIds = groupMemberService.getActiveGroupMemberUserIdsByGroupId(reqVO.getGroupId());
  104 + Set<Long> receiverUserIds = getReceiverUserIds(null, senderId, memberUserIds);
  105 + // 2.2 引用 quote 消息规范化(含可见性子集校验,防止定向消息内容被广播引用泄漏)
  106 + reqVO.setContent(normalizeQuoteContent(reqVO, senderMember, receiverUserIds));
  107 + // 2.3 构建并保存消息;唯一键冲突时回查已存在消息返回
110 108 ImGroupMessageDO message = BeanUtils.toBean(reqVO, ImGroupMessageDO.class, m -> m
111   - .setSenderId(senderId).setStatus(ImMessageStatusEnum.UNREAD.getStatus()).setSendTime(LocalDateTime.now())
  109 + .setSenderId(senderId).setStatus(ImMessageStatusEnum.NORMAL.getStatus()).setSendTime(LocalDateTime.now())
  110 + .setReceiverUserIds(new ArrayList<>(receiverUserIds))
112 111 .setReceiptStatus(resolveReceiptStatus(reqVO.getReceipt())));
113   - groupMessageMapper.insert(message);
  112 + try {
  113 + groupMessageMapper.insert(message);
  114 + } catch (DuplicateKeyException e) {
  115 + log.warn("[sendGroupMessage][senderId({}) clientMessageId({}) 并发插入冲突,回查返回]",
  116 + senderId, reqVO.getClientMessageId());
  117 + return groupMessageMapper.selectBySenderIdAndClientMessageId(senderId, reqVO.getClientMessageId());
  118 + }
114 119  
115   - // 3. WebSocket 异步推送(群内可见成员 + 发送方多端同步)
116   - List<Long> memberUserIds = groupMemberService.getActiveGroupMemberUserIdsByGroupId(message.getGroupId());
117   - Set<Long> targetUserIds = getVisibleUserIds(message.getReceiverUserIds(), senderId, memberUserIds);
118   - imWebSocketService.sendGroupMessageAsync(targetUserIds, ImGroupMessageDTO.ofSend(message));
  120 + // 3. WebSocket 异步推送(快照内可见成员 + 发送方多端同步)
  121 + ImGroupMessageNotification notification = ImGroupMessageNotification.ofSend(message);
  122 + imWebSocketService.sendNotificationAsync(receiverUserIds, ImConversationTypeEnum.GROUP.getType(),
  123 + notification.getType(), notification);
119 124 return message;
120 125 }
121 126  
122 127 @Override
123 128 public ImGroupMessageDO sendGroupMessage(Long senderId, ImGroupMessageSendDTO dto) {
124 129 List<Long> memberUserIds = groupMemberService.getActiveGroupMemberUserIdsByGroupId(dto.getGroupId());
125   - Set<Long> targetUserIds = getVisibleUserIds(dto.getReceiverUserIds(), senderId, memberUserIds);
126   - return sendGroupMessage(senderId, targetUserIds, dto);
  130 + Set<Long> receiverUserIds = getReceiverUserIds(dto.getReceiverUserIds(), senderId, memberUserIds);
  131 + return sendGroupMessage(senderId, receiverUserIds, dto);
127 132 }
128 133  
129 134 @Override
130   - public ImGroupMessageDO sendGroupMessage(Long senderId, Collection<Long> targetUserIds, ImGroupMessageSendDTO dto) {
  135 + public ImGroupMessageDO sendGroupMessage(Long senderId, Collection<Long> receiverUserIds, ImGroupMessageSendDTO dto) {
131 136 // 1.1 content 序列化:null / String 透传,POJO 走 JSON
132 137 Object payload = dto.getContent();
133 138 String contentString = payload == null || payload instanceof String
... ... @@ -137,16 +142,18 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService {
137 142 ImGroupMessageDO message = new ImGroupMessageDO().setClientMessageId(IdUtil.fastSimpleUUID())
138 143 .setSenderId(senderId).setGroupId(dto.getGroupId())
139 144 .setType(dto.getType()).setContent(contentString)
140   - .setStatus(ImMessageStatusEnum.UNREAD.getStatus()).setSendTime(LocalDateTime.now())
141   - .setAtUserIds(dto.getAtUserIds()).setReceiverUserIds(dto.getReceiverUserIds())
  145 + .setStatus(ImMessageStatusEnum.NORMAL.getStatus()).setSendTime(LocalDateTime.now())
  146 + .setAtUserIds(dto.getAtUserIds()).setReceiverUserIds(new ArrayList<>(receiverUserIds))
142 147 .setReceiptStatus(resolveReceiptStatus(dto.getReceipt()));
143 148 // 1.3 按 type.persistent 决定是否入库
144   - if (ImMessageTypeEnum.validate(dto.getType()).isPersistent()) {
  149 + if (ImContentTypeEnum.validate(dto.getType()).isPersistent()) {
145 150 groupMessageMapper.insert(message);
146 151 }
147 152  
148 153 // 2. WebSocket 异步推送
149   - imWebSocketService.sendGroupMessageAsync(targetUserIds, ImGroupMessageDTO.ofSend(message));
  154 + ImGroupMessageNotification notification = ImGroupMessageNotification.ofSend(message);
  155 + imWebSocketService.sendNotificationAsync(receiverUserIds, ImConversationTypeEnum.GROUP.getType(),
  156 + notification.getType(), notification);
150 157 return message;
151 158 }
152 159  
... ... @@ -180,7 +187,7 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService {
180 187  
181 188 // 3. 发送撤回事件
182 189 return sendGroupMessage(userId, new ImGroupMessageSendDTO().setGroupId(message.getGroupId())
183   - .setType(ImMessageTypeEnum.RECALL.getType())
  190 + .setType(ImContentTypeEnum.RECALL.getType())
184 191 .setReceiverUserIds(message.getReceiverUserIds())
185 192 .setContent(new RecallMessage().setMessageId(messageId)));
186 193 }
... ... @@ -195,155 +202,53 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService {
195 202 // 0. 拉取时间窗;超过窗口的老消息不再通过离线通道推送
196 203 LocalDateTime minSendTime = LocalDateTime.now().minusDays(imProperties.getMessage().getGroupPullMaxDays());
197 204  
198   - // 1.1 主查询:仅用"当前仍在群"的成员记录驱动;若首批消息过滤后为空,则允许内部重试
199   - List<ImGroupMemberDO> activeMembers = groupMemberService.getActiveGroupMemberListByUserId(userId);
200   - Map<Long, ImGroupMemberDO> memberMap = convertMap(activeMembers, ImGroupMemberDO::getGroupId);
201   - List<ImGroupMessageDO> messages = new ArrayList<>();
202   - if (CollUtil.isNotEmpty(activeMembers)) {
203   - List<Long> groupIds = convertList(activeMembers, ImGroupMemberDO::getGroupId);
204   - messages.addAll(pullActiveGroupMessageList(userId, groupIds, minId, size, minSendTime, memberMap));
205   - }
206   - // 1.2 补齐"退群前"的消息:
207   - // - 继续基于用户原始 minId 单次查询,不能被主查询内部探测游标带着前进;
208   - // - 若 minId > 0 且能查到对应消息,则进一步把下限抬到该消息的 sendTime,避免把客户端已拥有的老消息再次推送。
209   - messages.addAll(pullQuitGroupMessageList(userId, minId, size, minSendTime, memberMap));
210   -
211   - // 2. 合并后统一过滤,得到当前用户可见的结果
212   - List<ImGroupMessageDO> result = filterGroupMessageList(messages, memberMap, userId, size);
213   -
214   - // 3. 按当前用户补齐:消息已读态(相对 Redis 已读游标)、本人发送的回执消息的已读人数
215   - appendMessageStatusAndReceipt(userId, result);
216   - log.info("[pullGroupMessageList][userId({}) minId({}) size({}) result({})]", userId, minId, size, result.size());
217   - return result;
218   - }
219   -
220   - /**
221   - * 拉取当前仍在群的主路径消息。
222   - *
223   - * 仅当首批消息过滤后仍无可见消息时,才按消息最大 id 继续向后探测,
224   - * 直到命中可见消息或确认该来源已耗尽。
225   - */
226   - private List<ImGroupMessageDO> pullActiveGroupMessageList(Long userId, List<Long> groupIds, Long minId,
227   - Integer size, LocalDateTime minSendTime,
228   - Map<Long, ImGroupMemberDO> memberMap) {
229   - // 1. 主查询内部探测游标:仅用于向后探测,不代表客户端真实已送达边界
230   - Long activeMinId = minId;
231   - for (int retryCount = 0; retryCount <= PULL_GROUP_MESSAGE_EMPTY_RETRY_TIMES; retryCount++) {
232   - // 2. 查询本轮消息;若已无更多消息,则当前主路径直接结束
233   - List<ImGroupMessageDO> messages = groupMessageMapper.selectListByMinId(groupIds, activeMinId,
234   - minSendTime, size);
235   - if (CollUtil.isEmpty(messages)) {
236   - return Collections.emptyList();
237   - }
238   - boolean hasVisibleMessage = CollUtil.anyMatch(messages,
239   - message -> isMessageVisible(message, memberMap.get(message.getGroupId()), userId));
240   - boolean sourceExhausted = messages.size() < size;
241   - // 3. 本轮已命中可见消息,或主查询来源已耗尽,直接返回这一轮消息
242   - if (hasVisibleMessage || sourceExhausted) {
243   - return messages;
244   - }
245   -
246   - // 4. 按本轮消息最大 id 推进内部游标,跳过这段不可见区间;若游标未前进则直接停止
247   - Long maxMessageId = getMaxValue(messages, ImGroupMessageDO::getId);
248   - if (maxMessageId == null || maxMessageId <= activeMinId) {
249   - return Collections.emptyList();
250   - }
251   - activeMinId = maxMessageId;
  205 + // 1. 候选群 = 用户曾经加入的所有群(含退群);可见性与时间窗都交给 SQL,退群群最多贡献 0 条
  206 + Set<Long> groupIds = convertSet(groupMemberService.getGroupMemberListByUserId(userId),
  207 + ImGroupMemberDO::getGroupId);
  208 + if (CollUtil.isEmpty(groupIds)) {
  209 + return Collections.emptyList();
252 210 }
253   - return Collections.emptyList();
254   - }
255 211  
256   - /**
257   - * 拉取已离开群路径的消息(退群前消息)
258   - */
259   - private List<ImGroupMessageDO> pullQuitGroupMessageList(Long userId, Long minId, Integer size,
260   - LocalDateTime minSendTime,
261   - Map<Long, ImGroupMemberDO> memberMap) {
262   - // 1. 退群补齐始终基于用户原始 minId 计算时间边界,避免被主查询内部重试游标误伤
263   - LocalDateTime minQuitTime = minSendTime;
264   - if (minId != null && minId > 0) {
265   - ImGroupMessageDO minMessage = groupMessageMapper.selectById(minId);
266   - if (minMessage != null && minMessage.getSendTime() != null
267   - && minMessage.getSendTime().isAfter(minSendTime)) {
268   - minQuitTime = minMessage.getSendTime();
269   - }
270   - }
271   - // 2. 查询用户离开的群记录;若原始 minId 对应消息仍在窗口内,则用它的发送时间抬升退群筛选下限
272   - List<ImGroupMessageDO> messages = new ArrayList<>();
273   - List<ImGroupMemberDO> quitMembers = groupMemberService.getQuitGroupMemberListByUserId(userId, minQuitTime);
274   - for (ImGroupMemberDO quitMember : quitMembers) {
275   - // 3. 按原始 minId + 退群时间补齐该群退群前消息,并把成员记录写回 memberMap 供统一可见性过滤使用
276   - List<ImGroupMessageDO> quitGroupMessages = groupMessageMapper.selectListByGroupIdAndMinIdAndQuitTimeBefore(
277   - quitMember.getGroupId(), minId, minSendTime, quitMember.getQuitTime(), size);
278   - if (CollUtil.isEmpty(quitGroupMessages)) {
279   - continue;
280   - }
281   - messages.addAll(quitGroupMessages);
282   - memberMap.put(quitMember.getGroupId(), quitMember);
283   - }
284   - return messages;
285   - }
  212 + // 2. 按 receiver_user_ids 快照过滤可见性,结果按 id 升序
  213 + List<ImGroupMessageDO> messages = groupMessageMapper.selectListByMinId(userId, groupIds, minId, minSendTime, size);
286 214  
287   - /**
288   - * 过滤一批原始群消息,得到当前用户可见的返回结果
289   - */
290   - private List<ImGroupMessageDO> filterGroupMessageList(List<ImGroupMessageDO> messages,
291   - Map<Long, ImGroupMemberDO> memberMap,
292   - Long userId, Integer size) {
293   - // 按可见性过滤(入群前不可见、定向消息排除),按 id 升序后仅取本页 size 条,
294   - // 避免「在群 + 退群前」多路合并时一次响应跨度过大、游标直接跳到全局最大 id 而漏拉中间消息
295   - return messages.stream()
296   - .filter(msg -> isMessageVisible(msg, memberMap.get(msg.getGroupId()), userId))
297   - .sorted(Comparator.comparing(ImGroupMessageDO::getId))
298   - .limit(size)
299   - .collect(Collectors.toList());
  215 + // 3. 补齐本人发送的回执消息的已读人数
  216 + appendMessageReceipt(userId, messages);
  217 + log.info("[pullGroupMessageList][userId({}) minId({}) size({}) result({})]", userId, minId, size, messages.size());
  218 + return messages;
300 219 }
301 220  
302 221 /**
303   - * 补全消息已读态和回执已读人数
304   - *
305   - * 1. 消息已读态(status):根据 Redis 已读游标判断 READ / UNREAD
306   - * 2. 回执已读人数(readCount):仅对本人发送的回执消息,计算可见成员中的已读人数
  222 + * 补全本人发送消息的回执已读人数(readCount)
  223 + * <p>
  224 + * 仅对本人发送、且需要回执(非 NO_RECEIPT)的消息,按 receiver_user_ids 快照 ∩ 当前有效成员 - 发送者 算已读人数
307 225 */
308   - @SuppressWarnings({"StatementWithEmptyBody", "DataFlowIssue"})
309   - private void appendMessageStatusAndReceipt(Long userId, List<ImGroupMessageDO> messages) {
  226 + @SuppressWarnings("DataFlowIssue")
  227 + private void appendMessageReceipt(Long userId, List<ImGroupMessageDO> messages) {
310 228 if (CollUtil.isEmpty(messages)) {
311 229 return;
312 230 }
313   - // 群已读关闭:不查 Redis 已读游标,status 保持 DB 原值(含撤回),readCount 不补齐
  231 + // 群已读关闭:不补 readCount
314 232 if (BooleanUtil.isFalse(imProperties.getMessage().isGroupReadEnabled())) {
315 233 return;
316 234 }
317   - Map<Long, Long> readMaxMessageIdsByGroup = new HashMap<>(); // 群 → 已读位置
318 235 Map<Long, Map<Long, Long>> readPositionsByGroup = new HashMap<>(); // 群 → (用户 → 已读位置)
319 236 Map<Long, List<ImGroupMemberDO>> membersByGroup = new HashMap<>(); // 群 → 全部成员列表
320 237 for (ImGroupMessageDO message : messages) {
321   - // 消息已读态(status):撤回 > 已读 > 未读
322   - Long groupId = message.getGroupId();
323   - long readMaxMessageId = readMaxMessageIdsByGroup.computeIfAbsent(groupId, gid -> {
324   - Long readMaxMsgId = groupMessageReadRedisDAO.getReadMaxMessageId(gid, userId);
325   - return readMaxMsgId != null ? readMaxMsgId : -1L;
326   - });
327   - if (ImMessageStatusEnum.RECALL.getStatus().equals(message.getStatus())) {
328   - // 保持撤回态
329   - } else if (readMaxMessageId >= message.getId()) {
330   - message.setStatus(ImMessageStatusEnum.READ.getStatus());
331   - } else {
332   - message.setStatus(ImMessageStatusEnum.UNREAD.getStatus());
333   - }
334   -
335   - // 回执消息的已读人数(readCount):仅补齐本人发送的,其他消息不处理(回执消息才关心已读人数,且只对发送者可见)
  238 + // 仅补本人发送、且需要回执(非 NO_RECEIPT)的消息
336 239 if (ObjUtil.notEqual(message.getSenderId(), userId)
337   - || ImGroupMessageReceiptStatusEnum.NO_RECEIPT.getStatus().equals(message.getReceiptStatus())) {
  240 + || ImMessageReceiptStatusEnum.NO_RECEIPT.getStatus().equals(message.getReceiptStatus())) {
338 241 continue;
339 242 }
  243 + Long groupId = message.getGroupId();
340 244 Map<Long, Long> positions = readPositionsByGroup.computeIfAbsent(groupId,
341   - gid -> groupMessageReadRedisDAO.getReadMaxMessageIdMap(gid));
342   - List<ImGroupMemberDO> allMembers = membersByGroup.computeIfAbsent(groupId,
343   - groupMemberService::getGroupMemberListByGroupId);
344   - Set<Long> visibleUserIds = getVisibleUserIds(message, allMembers);
345   - visibleUserIds.remove(message.getSenderId());
346   - int readCount = getSumValue(visibleUserIds,
  245 + gid -> conversationReadService.getUserReadMessageIdMap(ImConversationTypeEnum.GROUP.getType(), gid));
  246 + // 应读分母取当前有效成员(剔除已退群),与异步回执刷新、前端已读弹层口径一致
  247 + List<ImGroupMemberDO> activeMembers = membersByGroup.computeIfAbsent(groupId,
  248 + groupMemberService::getActiveGroupMemberListByGroupId);
  249 + Set<Long> receiverUserIds = getReceiverUserIds(message, activeMembers);
  250 + receiverUserIds.remove(message.getSenderId());
  251 + int readCount = getSumValue(receiverUserIds,
347 252 uid -> positions.getOrDefault(uid, -1L) >= message.getId() ? 1 : null,
348 253 Integer::sum, 0);
349 254 message.setReadCount(readCount);
... ... @@ -357,25 +262,24 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService {
357 262 throw exception(MESSAGE_GROUP_READ_DISABLED);
358 263 }
359 264 Assert.notNull(messageId, "已读消息编号不能为空");
360   - // 1.1 获取群成员记录
361   - ImGroupMemberDO member = groupMemberService.getGroupMember(groupId, userId);
362   - // 1.2 校验消息属于当前群,且对当前用户可见
  265 + // 1. 校验消息属于当前群,且对当前用户可见(按快照)
363 266 ImGroupMessageDO message = groupMessageMapper.selectById(messageId);
364 267 if (message == null
365 268 || ObjUtil.notEqual(message.getGroupId(), groupId)
366   - || !isMessageVisible(message, member, userId)) {
  269 + || !isMessageReceived(message, userId)) {
367 270 throw exception(MESSAGE_NOT_IN_GROUP);
368 271 }
369 272  
370   - // 2. 已读位置未前进,直接返回
371   - Long prevMaxMessageId = groupMessageReadRedisDAO.getReadMaxMessageId(groupId, userId);
372   - if (prevMaxMessageId != null && prevMaxMessageId >= messageId) {
  273 + // 2. 取旧读位置(用于回执刷新区间),再更新群已读位置;读位置未前进则不推
  274 + Long prevMaxMessageId = conversationReadService.getConversationReadMessageId(
  275 + userId, ImConversationTypeEnum.GROUP.getType(), groupId);
  276 + boolean advanced = conversationReadService.updateConversationReadPosition(
  277 + userId, ImConversationTypeEnum.GROUP.getType(), groupId, messageId);
  278 + if (!advanced) {
373 279 return;
374 280 }
375   - // 3. 更新 Redis 群已读位置
376   - groupMessageReadRedisDAO.updateReadMaxMessageId(groupId, userId, messageId);
377 281  
378   - // 4. 异步发送 READ 事件 + 刷新范围内的群回执
  282 + // 3. 异步发送 READ 事件 + 刷新范围内的群回执
379 283 getSelf().readGroupMessageEvent(userId, groupId, prevMaxMessageId, messageId);
380 284 }
381 285  
... ... @@ -386,13 +290,13 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService {
386 290 throw exception(MESSAGE_GROUP_READ_DISABLED);
387 291 }
388 292 // 1.1 校验用户在群中(权限校验)
389   - ImGroupMemberDO operator = groupMemberService.validateMemberInGroup(groupId, userId);
  293 + groupMemberService.validateMemberInGroup(groupId, userId);
390 294 // 1.2 获取消息;并校验消息归属于该群、对调用者可见、调用者是发送方
391 295 ImGroupMessageDO message = groupMessageMapper.selectById(messageId);
392 296 if (message == null || ObjUtil.notEqual(message.getGroupId(), groupId)) {
393 297 return Collections.emptyList();
394 298 }
395   - if (!isMessageVisible(message, operator, userId)) {
  299 + if (!isMessageReceived(message, userId)) {
396 300 return Collections.emptyList();
397 301 }
398 302 // 1.3 仅消息发送方关心已读人数;非发送方查询直接返回空
... ... @@ -400,18 +304,19 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService {
400 304 return Collections.emptyList();
401 305 }
402 306  
403   - // 2. 获取所有成员和已读位置
404   - List<ImGroupMemberDO> allMembers = groupMemberService.getGroupMemberListByGroupId(groupId);
405   - Map<Long, Long> allPositions = groupMessageReadRedisDAO.getReadMaxMessageIdMap(groupId);
  307 + // 2. 获取当前有效成员和已读位置(剔除已退群,与回执分母口径一致)
  308 + List<ImGroupMemberDO> activeMembers = groupMemberService.getActiveGroupMemberListByGroupId(groupId);
  309 + Map<Long, Long> allPositions = conversationReadService.getUserReadMessageIdMap(
  310 + ImConversationTypeEnum.GROUP.getType(), groupId);
406 311  
407 312 // 3. 计算该消息的可见成员集合(排除发送者自己)
408   - Set<Long> visibleUserIds = getVisibleUserIds(message, allMembers);
409   - visibleUserIds.remove(message.getSenderId());
  313 + Set<Long> receiverUserIds = getReceiverUserIds(message, activeMembers);
  314 + receiverUserIds.remove(message.getSenderId());
410 315  
411 316 // 4. 只返回在可见范围内且已读位置 >= messageId 的用户
412 317 List<Long> readUserIds = new ArrayList<>();
413 318 allPositions.forEach((uid, readMaxMessageId) -> {
414   - if (visibleUserIds.contains(uid) && readMaxMessageId >= messageId) {
  319 + if (receiverUserIds.contains(uid) && readMaxMessageId >= messageId) {
415 320 readUserIds.add(uid);
416 321 }
417 322 });
... ... @@ -420,15 +325,14 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService {
420 325  
421 326 @Override
422 327 public List<ImGroupMessageDO> getGroupMessageList(Long userId, ImGroupMessageListReqVO reqVO) {
423   - // 1. 校验用户在群中
424   - ImGroupMemberDO member = groupMemberService.validateMemberInGroup(reqVO.getGroupId(), userId);
425   -
426   - // 2. 查询历史消息(仅入群之后)
427   - List<ImGroupMessageDO> messages = groupMessageMapper.selectHistoryList(
428   - reqVO.getGroupId(), reqVO.getMaxId(), reqVO.getLimit(), member.getJoinTime());
  328 + // 1. 校验用户曾经在群(当前在群或已退群),与 pull 退群窗口口径一致;内容仍由 receiver_user_ids 快照过滤
  329 + if (groupMemberService.getGroupMember(reqVO.getGroupId(), userId) == null) {
  330 + throw exception(GROUP_MEMBER_NOT_IN_GROUP);
  331 + }
429 332  
430   - // 3. 过滤定向消息:仅保留当前用户可见的(receiverUserIds 为空 / 含当前用户 / 本人发送)
431   - return filterList(messages, message -> isMessageVisible(message, member, userId));
  333 + // 2. 查询历史消息:SQL 已按 receiver_user_ids 快照过滤当前用户可见
  334 + return groupMessageMapper.selectHistoryListByUser(
  335 + userId, reqVO.getGroupId(), reqVO.getMaxId(), reqVO.getLimit());
432 336 }
433 337  
434 338 // ========== 异步 WebSocket 推送 ==========
... ... @@ -445,8 +349,9 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService {
445 349 @SuppressWarnings("DataFlowIssue")
446 350 public void readGroupMessageEvent(Long userId, Long groupId, Long prevMaxMessageId, Long newMaxMessageId) {
447 351 // 1. 发送 READ 事件给自己的其他终端(多端同步)
448   - imWebSocketService.sendGroupMessageAsync(userId,
449   - ImGroupMessageDTO.ofRead(userId, groupId, newMaxMessageId));
  352 + imWebSocketService.sendNotificationAsync(userId, ImConversationTypeEnum.GROUP.getType(),
  353 + ImContentTypeEnum.READ.getType(),
  354 + ImMessageReadNotification.ofGroup(userId, groupId, newMaxMessageId));
450 355  
451 356 // 2. 刷新 (prevMaxMessageId, newMaxMessageId] 区间内的待回执消息
452 357 List<ImGroupMessageDO> pendingMessages = groupMessageMapper.selectListByGroupIdAndPendingReceipt(
... ... @@ -455,28 +360,30 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService {
455 360 return;
456 361 }
457 362 List<ImGroupMemberDO> activeMembers = groupMemberService.getActiveGroupMemberListByGroupId(groupId);
458   - Map<Long, Long> allPositions = groupMessageReadRedisDAO.getReadMaxMessageIdMap(groupId);
  363 + Map<Long, Long> allPositions = conversationReadService.getUserReadMessageIdMap(
  364 + ImConversationTypeEnum.GROUP.getType(), groupId);
459 365 for (ImGroupMessageDO message : pendingMessages) {
460   - // 2.1.1 统计可见成员中的已读人数
461   - Set<Long> visibleUserIds = getVisibleUserIds(message, activeMembers);
462   - visibleUserIds.remove(message.getSenderId()); // 发送者自己不算已读
463   - if (CollUtil.isEmpty(visibleUserIds)) {
  366 + // 2.1.1 统计可见成员中的已读人数(应读分母 = 快照 ∩ 当前有效成员 - sender)
  367 + Set<Long> receiverUserIds = getReceiverUserIds(message, activeMembers);
  368 + receiverUserIds.remove(message.getSenderId()); // 发送者自己不算已读
  369 + if (CollUtil.isEmpty(receiverUserIds)) {
464 370 continue;
465 371 }
466   - int readCount = getSumValue(visibleUserIds,
  372 + int readCount = getSumValue(receiverUserIds,
467 373 uid -> allPositions.getOrDefault(uid, -1L) >= message.getId() ? 1 : null,
468 374 Integer::sum, 0);
469 375 // 2.1.2 全部已读 → 标记回执完成
470   - Integer newReceiptStatus = ImGroupMessageReceiptStatusEnum.PENDING.getStatus();
471   - if (readCount >= visibleUserIds.size()) {
472   - newReceiptStatus = ImGroupMessageReceiptStatusEnum.DONE.getStatus();
  376 + Integer newReceiptStatus = ImMessageReceiptStatusEnum.PENDING.getStatus();
  377 + if (readCount >= receiverUserIds.size()) {
  378 + newReceiptStatus = ImMessageReceiptStatusEnum.DONE.getStatus();
473 379 groupMessageMapper.updateById(new ImGroupMessageDO().setId(message.getId())
474 380 .setReceiptStatus(newReceiptStatus));
475 381 }
476 382  
477 383 // 2.2 发送 RECEIPT 事件给消息发送方(只有 ta 关心已读进度)
478   - imWebSocketService.sendGroupMessageAsync(message.getSenderId(),
479   - ImGroupMessageDTO.ofReceipt(message.getId(), groupId, readCount, newReceiptStatus));
  384 + imWebSocketService.sendNotificationAsync(message.getSenderId(), ImConversationTypeEnum.GROUP.getType(),
  385 + ImContentTypeEnum.RECEIPT.getType(),
  386 + ImMessageReceiptNotification.ofGroup(message.getId(), groupId, readCount, newReceiptStatus));
480 387 }
481 388 }
482 389  
... ... @@ -487,9 +394,11 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService {
487 394 *
488 395 * @param reqVO 发送请求
489 396 * @param senderMember 发送人成员
  397 + * @param newAudience 新消息可见成员集合(发送当时快照)
490 398 * @return 规范化后的 content
491 399 */
492   - private String normalizeQuoteContent(ImGroupMessageSendReqVO reqVO, ImGroupMemberDO senderMember) {
  400 + private String normalizeQuoteContent(ImGroupMessageSendReqVO reqVO, ImGroupMemberDO senderMember,
  401 + Set<Long> newAudience) {
493 402 // 解析客户端 content 里的 quote.messageId
494 403 Long quoteMessageId = ImMessageUtils.parseQuoteMessageId(reqVO.getContent());
495 404  
... ... @@ -508,12 +417,13 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService {
508 417 if (ObjUtil.notEqual(original.getGroupId(), reqVO.getGroupId())) {
509 418 throw exception(MESSAGE_QUOTE_INVALID);
510 419 }
511   - // 拒绝定向消息(仅发送人可见的内容若被全员广播 quote.content,会泄漏给原本看不到的成员)
512   - if (CollUtil.isNotEmpty(original.getReceiverUserIds())) {
  420 + // 校验对发送人可见(按快照)
  421 + if (!isMessageReceived(original, senderMember.getUserId())) {
513 422 throw exception(MESSAGE_QUOTE_INVALID);
514 423 }
515   - // 校验对发送人可见(入群时间 / 退群时间)
516   - if (!isMessageVisible(original, senderMember, senderMember.getUserId())) {
  424 + // 防泄漏:新消息可见集合必须是被引用消息可见集合的子集,否则禁止引用;
  425 + // 否则只对部分成员可见的定向消息内容,会随广播引用泄漏给原本看不到的成员
  426 + if (!new HashSet<>(original.getReceiverUserIds()).containsAll(newAudience)) {
517 427 throw exception(MESSAGE_QUOTE_INVALID);
518 428 }
519 429 // 构建 quote 对象并注入 content
... ... @@ -537,87 +447,48 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService {
537 447 }
538 448  
539 449 /**
540   - * 判断一条群消息对某个群成员是否可见
  450 + * 判断一条群消息是否被某个用户接收到
  451 + * <p>
  452 + * 接收范围以发送当时固化的 receiver_user_ids 快照为准;发送者也在快照内。
541 453 *
542   - * @param msg 消息
543   - * @param member 群成员
544   - * @param userId 当前用户编号(用于定向消息过滤)
545   - * @return 是否可见
  454 + * @param message 消息
  455 + * @param userId 当前用户编号
  456 + * @return 是否接收到
546 457 */
547   - private boolean isMessageVisible(ImGroupMessageDO msg, ImGroupMemberDO member, Long userId) {
548   - if (member == null) {
549   - return false;
550   - }
551   - // 1. 入群时间晚于消息发送时间 → 不可见
552   - if (member.getJoinTime() != null && msg.getSendTime().isBefore(member.getJoinTime())) {
553   - return false;
554   - }
555   - // 2. 已退群且退群时间早于消息发送时间 → 不可见
556   - if (CommonStatusEnum.DISABLE.getStatus().equals(member.getStatus())
557   - && member.getQuitTime() != null && msg.getSendTime().isAfter(member.getQuitTime())) {
558   - return false;
559   - }
560   - // 3.1 无定向接收列表 → 全员可见
561   - if (CollUtil.isEmpty(msg.getReceiverUserIds())) {
562   - return true;
563   - }
564   - // 3.2 当前用户在定向列表中,或本人即发送者 → 可见
565   - return msg.getReceiverUserIds().contains(userId)
566   - || ObjUtil.equal(msg.getSenderId(), userId);
  458 + @SuppressWarnings("BooleanMethodIsAlwaysInverted")
  459 + private boolean isMessageReceived(ImGroupMessageDO message, Long userId) {
  460 + return CollUtil.contains(message.getReceiverUserIds(), userId);
567 461 }
568 462  
569 463 /**
570   - * 计算一条群消息的可见成员集合(含发送者)
  464 + * 计算一条群消息的可见成员集合(快照与传入成员的交集,含发送者)
571 465 */
572   - private Set<Long> getVisibleUserIds(ImGroupMessageDO message, List<ImGroupMemberDO> members) {
573   - return convertSet(members, ImGroupMemberDO::getUserId,
574   - member -> isMessageVisible(message, member, member.getUserId()));
  466 + private Set<Long> getReceiverUserIds(ImGroupMessageDO message, List<ImGroupMemberDO> members) {
  467 + if (CollUtil.isEmpty(message.getReceiverUserIds())) {
  468 + return new HashSet<>();
  469 + }
  470 + Set<Long> snapshot = new HashSet<>(message.getReceiverUserIds());
  471 + return convertSet(members, ImGroupMemberDO::getUserId, member -> snapshot.contains(member.getUserId()));
575 472 }
576 473  
577 474 /**
578   - * 基于群成员 userId 列表,过滤出一条新消息的可见成员集合(含发送者)。
  475 + * 基于群成员 userId 列表,计算一条新消息的可见成员快照(含发送者)。
579 476 * <p>
580   - * 仅适用于「新消息」推送场景({@code sendTime = now}),不涉及 joinTime / quitTime 判定,
581   - * 只应用 {@code receiverUserIds} 定向过滤;语义与
582   - * {@link #isMessageVisible(ImGroupMessageDO, ImGroupMemberDO, Long)} 的第 3 步保持一致。
  477 + * 仅适用于「新消息」发送场景:无定向接收列表则全员可见,否则取定向用户与当前成员的交集;
  478 + * 发送者始终纳入快照,保证 receiver_user_ids 非空且发送方可见。
583 479 */
584   - private Set<Long> getVisibleUserIds(List<Long> receiverUserIds, Long senderId, Collection<Long> memberUserIds) {
585   - if (CollUtil.isEmpty(memberUserIds)) {
586   - return new HashSet<>();
587   - }
588   - // 无定向接收列表 → 全员可见
589   - if (CollUtil.isEmpty(receiverUserIds)) {
590   - return new HashSet<>(memberUserIds);
591   - }
592   - // 有定向接收列表 → 仅定向用户可见;发送者自己也能看到自己的消息(多端同步)
593   - Set<Long> allowed = new HashSet<>(receiverUserIds);
  480 + private Set<Long> getReceiverUserIds(List<Long> directedUserIds, Long senderId, Collection<Long> memberUserIds) {
  481 + // 无定向接收列表 → 全员可见;否则取定向用户与当前成员的交集
  482 + Set<Long> result = CollUtil.isEmpty(directedUserIds)
  483 + ? new HashSet<>(memberUserIds)
  484 + : new HashSet<>(CollUtil.intersection(memberUserIds, directedUserIds));
  485 + // 发送者始终可见(多端同步),即便成员缓存暂未包含
594 486 if (senderId != null) {
595   - allowed.add(senderId);
596   - }
597   - Set<Long> result = new HashSet<>();
598   - for (Long userId : memberUserIds) {
599   - if (allowed.contains(userId)) {
600   - result.add(userId);
601   - }
  487 + result.add(senderId);
602 488 }
603 489 return result;
604 490 }
605 491  
606   - @Override
607   - public void deleteReadMaxMessageId(Long groupId, Long userId) {
608   - groupMessageReadRedisDAO.deleteReadMaxMessageId(groupId, userId);
609   - }
610   -
611   - @Override
612   - public void deleteReadMaxMessageIds(Long groupId, Collection<Long> userIds) {
613   - groupMessageReadRedisDAO.deleteReadMaxMessageIds(groupId, userIds);
614   - }
615   -
616   - @Override
617   - public void deleteReadMaxMessageIdMap(Long groupId) {
618   - groupMessageReadRedisDAO.deleteReadMaxMessageIdMap(groupId);
619   - }
620   -
621 492 // ==================== 管理后台 ====================
622 493  
623 494 @Override
... ... @@ -652,11 +523,11 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService {
652 523 */
653 524 private Integer resolveReceiptStatus(Boolean receipt) {
654 525 if (BooleanUtil.isFalse(imProperties.getMessage().isGroupReadEnabled())) {
655   - return ImGroupMessageReceiptStatusEnum.NO_RECEIPT.getStatus();
  526 + return ImMessageReceiptStatusEnum.NO_RECEIPT.getStatus();
656 527 }
657 528 return BooleanUtil.isTrue(receipt)
658   - ? ImGroupMessageReceiptStatusEnum.PENDING.getStatus()
659   - : ImGroupMessageReceiptStatusEnum.NO_RECEIPT.getStatus();
  529 + ? ImMessageReceiptStatusEnum.PENDING.getStatus()
  530 + : ImMessageReceiptStatusEnum.NO_RECEIPT.getStatus();
660 531 }
661 532  
662 533 /**
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageService.java
... ... @@ -59,8 +59,8 @@ public interface ImPrivateMessageService {
59 59 /**
60 60 * 标记私聊消息已读
61 61 * <p>
62   - * 语义:将「对方发给当前用户、id <= messageId 的未读消息」一次性翻转为已读,
63   - * 与群聊 readGroupMessages 对称,避免"select-then-update"两步式带来的竞态。
  62 + * 语义:把「对方发给当前用户、id <= messageId 的待回执消息」一次性更新为已完成(DONE)并前进读位置,
  63 + * 与群聊 readGroupMessages 对称,避免「select-then-update」两步式带来的竞态。
64 64 *
65 65 * @param userId 当前用户编号
66 66 * @param receiverId 接收方用户编号(对方)
... ... @@ -72,7 +72,7 @@ public interface ImPrivateMessageService {
72 72 * 查询对方已读到我发的最大消息 id
73 73 * <p>
74 74 * 用于多端 / 离线场景下的已读位置补齐:客户端进入会话或断线重连后,
75   - * 调用此接口拿到对方的 maxReadId,再按 id <= maxReadId 翻转本地自发消息为已读,弥补离线期间错过的 RECEIPT 推送事件。
  75 + * 调用此接口拿到对方的 maxReadId,再按 id <= maxReadId 更新本地自发消息的回执状态,弥补离线期间错过的 RECEIPT 推送事件。
76 76 *
77 77 * @param userId 当前用户编号
78 78 * @param peerId 对方用户编号
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageServiceImpl.java
... ... @@ -12,19 +12,25 @@ import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivate
12 12 import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageSendReqVO;
13 13 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO;
14 14 import cn.iocoder.yudao.module.im.dal.mysql.message.ImPrivateMessageMapper;
  15 +import cn.iocoder.yudao.module.im.enums.ImConversationTypeEnum;
  16 +import cn.iocoder.yudao.module.im.enums.message.ImMessageReceiptStatusEnum;
15 17 import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum;
16   -import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
  18 +import cn.iocoder.yudao.module.im.enums.ImContentTypeEnum;
17 19 import cn.iocoder.yudao.module.im.framework.config.ImProperties;
  20 +import cn.iocoder.yudao.module.im.service.conversation.ImConversationReadService;
18 21 import cn.iocoder.yudao.module.im.service.friend.ImFriendService;
19 22 import cn.iocoder.yudao.module.im.service.message.dto.ImPrivateMessageSendDTO;
20 23 import cn.iocoder.yudao.module.im.service.sensitiveword.ImSensitiveWordService;
21 24 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService;
22   -import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO;
23   -import cn.iocoder.yudao.module.im.service.websocket.dto.message.QuoteMessage;
24   -import cn.iocoder.yudao.module.im.service.websocket.dto.message.RecallMessage;
  25 +import cn.iocoder.yudao.module.im.service.websocket.notification.message.ImMessageReadNotification;
  26 +import cn.iocoder.yudao.module.im.service.websocket.notification.message.ImMessageReceiptNotification;
  27 +import cn.iocoder.yudao.module.im.service.websocket.notification.message.ImPrivateMessageNotification;
  28 +import cn.iocoder.yudao.module.im.dal.dataobject.message.content.QuoteMessage;
  29 +import cn.iocoder.yudao.module.im.dal.dataobject.message.content.RecallMessage;
25 30 import cn.iocoder.yudao.module.im.util.ImMessageUtils;
26 31 import jakarta.annotation.Resource;
27 32 import lombok.extern.slf4j.Slf4j;
  33 +import org.springframework.dao.DuplicateKeyException;
28 34 import org.springframework.stereotype.Service;
29 35 import org.springframework.transaction.annotation.Transactional;
30 36 import org.springframework.validation.annotation.Validated;
... ... @@ -52,6 +58,8 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService {
52 58 private ImFriendService friendService;
53 59 @Resource
54 60 private ImSensitiveWordService sensitiveWordService;
  61 + @Resource
  62 + private ImConversationReadService conversationReadService;
55 63  
56 64 @Resource
57 65 private ImWebSocketService imWebSocketService;
... ... @@ -74,21 +82,32 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService {
74 82 // 1.3 好友校验
75 83 friendService.validateFriend(senderId, reqVO.getReceiverId());
76 84 // 1.4 文本消息敏感词过滤
77   - if (ImMessageTypeEnum.TEXT.getType().equals(reqVO.getType())) {
  85 + if (ImContentTypeEnum.TEXT.getType().equals(reqVO.getType())) {
78 86 sensitiveWordService.validateText(reqVO.getContent());
79 87 }
80 88  
81 89 // 2.1 引用 quote 消息规范化
82 90 reqVO.setContent(normalizeQuoteContent(reqVO, senderId));
83   - // 2.2 构建并保存消息
  91 + // 2.2 构建并保存消息;唯一键冲突时回查已存在消息返回
  92 + // 用户私聊消息默认需要回执(receipt 不传按 true);系统通知走 DTO 通道,默认不回执
  93 + Boolean receipt = reqVO.getReceipt() != null ? reqVO.getReceipt() : Boolean.TRUE;
84 94 ImPrivateMessageDO message = BeanUtils.toBean(reqVO, ImPrivateMessageDO.class, m -> m
85   - .setSenderId(senderId).setStatus(ImMessageStatusEnum.UNREAD.getStatus()).setSendTime(LocalDateTime.now()));
86   - privateMessageMapper.insert(message);
  95 + .setSenderId(senderId).setStatus(ImMessageStatusEnum.NORMAL.getStatus())
  96 + .setReceiptStatus(resolveReceiptStatus(receipt)).setSendTime(LocalDateTime.now()));
  97 + try {
  98 + privateMessageMapper.insert(message);
  99 + } catch (DuplicateKeyException e) {
  100 + log.warn("[sendPrivateMessage][senderId({}) clientMessageId({}) 并发插入冲突,回查返回]",
  101 + senderId, reqVO.getClientMessageId());
  102 + return privateMessageMapper.selectBySenderIdAndClientMessageId(senderId, reqVO.getClientMessageId());
  103 + }
87 104  
88 105 // 3. WebSocket 异步推送:接收方 + 发送方多端同步
89   - ImPrivateMessageDTO websocketMessage = ImPrivateMessageDTO.ofSend(message);
90   - imWebSocketService.sendPrivateMessageAsync(message.getReceiverId(), websocketMessage);
91   - imWebSocketService.sendPrivateMessageAsync(senderId, websocketMessage);
  106 + ImPrivateMessageNotification notification = ImPrivateMessageNotification.ofSend(message);
  107 + imWebSocketService.sendNotificationAsync(message.getReceiverId(), ImConversationTypeEnum.PRIVATE.getType(),
  108 + notification.getType(), notification);
  109 + imWebSocketService.sendNotificationAsync(senderId, ImConversationTypeEnum.PRIVATE.getType(),
  110 + notification.getType(), notification);
92 111 return message;
93 112 }
94 113  
... ... @@ -103,24 +122,39 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService {
103 122 ImPrivateMessageDO message = new ImPrivateMessageDO().setClientMessageId(IdUtil.fastSimpleUUID())
104 123 .setSenderId(senderId).setReceiverId(dto.getReceiverId())
105 124 .setType(dto.getType()).setContent(contentString)
106   - .setStatus(ImMessageStatusEnum.UNREAD.getStatus()).setSendTime(LocalDateTime.now());
  125 + .setStatus(ImMessageStatusEnum.NORMAL.getStatus())
  126 + .setReceiptStatus(resolveReceiptStatus(dto.getReceipt())).setSendTime(LocalDateTime.now());
107 127 // 1.3 决定是否持久化:dto.persistent 优先;为 null 时按 type 默认
108 128 boolean persistent = dto.getPersistent() != null
109 129 ? dto.getPersistent()
110   - : ImMessageTypeEnum.validate(dto.getType()).isPersistent();
  130 + : ImContentTypeEnum.validate(dto.getType()).isPersistent();
111 131 if (persistent) {
112 132 privateMessageMapper.insert(message);
113 133 }
114 134  
115 135 // 2. WebSocket 异步推送:双向(默认);单边语义(persistent=false)下仅推 sender 多端,对方不感知
116   - ImPrivateMessageDTO websocketMessage = ImPrivateMessageDTO.ofSend(message);
  136 + ImPrivateMessageNotification notification = ImPrivateMessageNotification.ofSend(message);
117 137 if (persistent) {
118   - imWebSocketService.sendPrivateMessageAsync(dto.getReceiverId(), websocketMessage);
  138 + imWebSocketService.sendNotificationAsync(dto.getReceiverId(), ImConversationTypeEnum.PRIVATE.getType(),
  139 + notification.getType(), notification);
119 140 }
120   - imWebSocketService.sendPrivateMessageAsync(senderId, websocketMessage);
  141 + imWebSocketService.sendNotificationAsync(senderId, ImConversationTypeEnum.PRIVATE.getType(),
  142 + notification.getType(), notification);
121 143 return message;
122 144 }
123 145  
  146 + /**
  147 + * 计算私聊消息回执 status:私聊已读关闭时强制 NO_RECEIPT,忽略发送方传入的 receipt(receipt 为 null 等价 false)
  148 + */
  149 + private Integer resolveReceiptStatus(Boolean receipt) {
  150 + if (BooleanUtil.isFalse(imProperties.getMessage().isPrivateReadEnabled())) {
  151 + return ImMessageReceiptStatusEnum.NO_RECEIPT.getStatus();
  152 + }
  153 + return BooleanUtil.isTrue(receipt)
  154 + ? ImMessageReceiptStatusEnum.PENDING.getStatus()
  155 + : ImMessageReceiptStatusEnum.NO_RECEIPT.getStatus();
  156 + }
  157 +
124 158 @Override
125 159 @Transactional(rollbackFor = Exception.class)
126 160 public ImPrivateMessageDO recallPrivateMessage(Long userId, Long messageId) {
... ... @@ -149,7 +183,7 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService {
149 183  
150 184 // 3. 发送撤回事件
151 185 return sendPrivateMessage(userId, new ImPrivateMessageSendDTO().setReceiverId(message.getReceiverId())
152   - .setType(ImMessageTypeEnum.RECALL.getType()).setContent(new RecallMessage().setMessageId(messageId)));
  186 + .setType(ImContentTypeEnum.RECALL.getType()).setContent(new RecallMessage().setMessageId(messageId)));
153 187 }
154 188  
155 189 /**
... ... @@ -211,20 +245,26 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService {
211 245 throw exception(MESSAGE_PRIVATE_READ_DISABLED);
212 246 }
213 247 Assert.notNull(messageId, "已读消息编号不能为空");
214   - // 2. 把 (receiverId → userId) 这条会话上、id <= messageId 的未读消息一步更新为已读
215   - // 仅 UNREAD 行被命中,避免覆盖已撤回/已读的状态;select-then-update 合成单条 SQL 后也消除了竞态窗口
216   - int updated = privateMessageMapper.updateBySenderIdAndReceiverIdAndIdLeAndStatus(
217   - receiverId, userId, messageId, ImMessageStatusEnum.UNREAD.getStatus(),
218   - new ImPrivateMessageDO().setStatus(ImMessageStatusEnum.READ.getStatus()));
219   - if (updated == 0) {
  248 +
  249 + // 2. 回执置 DONE:把 (receiverId → userId) 上 id <= messageId、待回执(PENDING) 的消息标记为已完成
  250 + // status 不再表达已读(保持 NORMAL);只翻 PENDING 行,避免覆盖 NO_RECEIPT / 已 DONE
  251 + privateMessageMapper.updateBySenderIdAndReceiverIdAndIdLeAndReceiptStatus(
  252 + receiverId, userId, messageId, ImMessageReceiptStatusEnum.PENDING.getStatus(),
  253 + new ImPrivateMessageDO().setReceiptStatus(ImMessageReceiptStatusEnum.DONE.getStatus()));
  254 +
  255 + // 3. 同步写 im_conversation_read(读位置唯一权威,单调递增);读位置前进才下发事件
  256 + boolean advanced = conversationReadService.updateConversationReadPosition(
  257 + userId, ImConversationTypeEnum.PRIVATE.getType(), receiverId, messageId);
  258 + if (!advanced) {
220 259 return;
221 260 }
222 261  
223   - // 3. 异步发送 READ + RECEIPT 事件(已读位置以前端上报为准,与多端 / 对方 UI 显示一致)
224   - imWebSocketService.sendPrivateMessageAsync(userId,
225   - ImPrivateMessageDTO.ofRead(userId, receiverId, messageId));
226   - imWebSocketService.sendPrivateMessageAsync(receiverId,
227   - ImPrivateMessageDTO.ofReceipt(userId, receiverId, messageId));
  262 + // 4. 异步发送 READ + RECEIPT 事件(已读位置以前端上报为准,与多端 / 对方 UI 显示一致)
  263 + imWebSocketService.sendNotificationAsync(userId, ImConversationTypeEnum.PRIVATE.getType(),
  264 + ImContentTypeEnum.READ.getType(), ImMessageReadNotification.ofPrivate(userId, receiverId, messageId));
  265 + imWebSocketService.sendNotificationAsync(receiverId, ImConversationTypeEnum.PRIVATE.getType(),
  266 + ImContentTypeEnum.RECEIPT.getType(),
  267 + ImMessageReceiptNotification.ofPrivate(userId, receiverId, messageId));
228 268 }
229 269  
230 270 @Override
... ... @@ -232,8 +272,9 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService {
232 272 if (BooleanUtil.isFalse(imProperties.getMessage().isPrivateReadEnabled())) {
233 273 throw exception(MESSAGE_PRIVATE_READ_DISABLED);
234 274 }
235   - return privateMessageMapper.selectMaxIdBySenderIdAndReceiverIdAndStatus(
236   - userId, peerId, ImMessageStatusEnum.READ.getStatus());
  275 + // 对端 peer 在「与 userId 的会话」里的读位置 = peer 把 userId 发的消息读到哪
  276 + return conversationReadService.getConversationReadMessageId(
  277 + peerId, ImConversationTypeEnum.PRIVATE.getType(), userId);
237 278 }
238 279  
239 280 @Override
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/dto/ImGroupMessageSendDTO.java
1 1 package cn.iocoder.yudao.module.im.service.message.dto;
2 2  
3 3 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO;
4   -import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
5   -import cn.iocoder.yudao.module.im.service.websocket.dto.notification.group.*;
  4 +import cn.iocoder.yudao.module.im.enums.ImContentTypeEnum;
  5 +import cn.iocoder.yudao.module.im.service.websocket.notification.group.*;
6 6 import jakarta.validation.constraints.NotNull;
7 7 import lombok.Data;
8 8  
... ... @@ -26,7 +26,7 @@ public class ImGroupMessageSendDTO {
26 26 /**
27 27 * 消息类型
28 28 * <p>
29   - * 枚举 {@link ImMessageTypeEnum}
  29 + * 枚举 {@link ImContentTypeEnum}
30 30 */
31 31 @NotNull(message = "消息类型不能为空")
32 32 private Integer type;
... ... @@ -53,30 +53,34 @@ public class ImGroupMessageSendDTO {
53 53 */
54 54 private Boolean receipt;
55 55  
56   - // ========== 群广播事件静态工厂(对应 ImMessageTypeEnum 群事件) ==========
  56 + // ========== 群广播事件静态工厂(对应 ImContentTypeEnum 群事件) ==========
57 57  
58 58 public static ImGroupMessageSendDTO ofGroupCreate(Long groupId, Long operatorUserId, Collection<Long> memberUserIds) {
59 59 GroupCreateNotification notification = new GroupCreateNotification();
60 60 notification.setOperatorUserId(operatorUserId);
61 61 notification.setMemberUserIds(new ArrayList<>(memberUserIds));
62 62 return new ImGroupMessageSendDTO().setGroupId(groupId)
63   - .setType(ImMessageTypeEnum.GROUP_CREATE.getType()).setContent(notification);
  63 + .setType(ImContentTypeEnum.GROUP_CREATE.getType()).setContent(notification);
64 64 }
65 65  
66   - public static ImGroupMessageSendDTO ofGroupInfoUpdate(Long groupId, Long operatorUserId, String oldAvatar, String newAvatar) {
  66 + public static ImGroupMessageSendDTO ofGroupInfoUpdate(Long groupId, Long operatorUserId,
  67 + String oldAvatar, String newAvatar,
  68 + Boolean oldJoinApproval, Boolean newJoinApproval) {
67 69 GroupInfoUpdateNotification notification = new GroupInfoUpdateNotification();
68 70 notification.setOperatorUserId(operatorUserId);
69 71 notification.setOldAvatar(oldAvatar);
70 72 notification.setNewAvatar(newAvatar);
  73 + notification.setOldJoinApproval(oldJoinApproval);
  74 + notification.setNewJoinApproval(newJoinApproval);
71 75 return new ImGroupMessageSendDTO().setGroupId(groupId)
72   - .setType(ImMessageTypeEnum.GROUP_INFO_UPDATE.getType()).setContent(notification);
  76 + .setType(ImContentTypeEnum.GROUP_INFO_UPDATE.getType()).setContent(notification);
73 77 }
74 78  
75 79 public static ImGroupMessageSendDTO ofGroupMemberQuit(Long groupId, Long operatorUserId) {
76 80 GroupMemberQuitNotification notification = new GroupMemberQuitNotification();
77 81 notification.setOperatorUserId(operatorUserId);
78 82 return new ImGroupMessageSendDTO().setGroupId(groupId)
79   - .setType(ImMessageTypeEnum.GROUP_MEMBER_QUIT.getType()).setContent(notification);
  83 + .setType(ImContentTypeEnum.GROUP_MEMBER_QUIT.getType()).setContent(notification);
80 84 }
81 85  
82 86 public static ImGroupMessageSendDTO ofGroupOwnerTransfer(Long groupId, Long operatorUserId, Long newOwnerUserId) {
... ... @@ -84,7 +88,7 @@ public class ImGroupMessageSendDTO {
84 88 notification.setOperatorUserId(operatorUserId);
85 89 notification.setNewOwnerUserId(newOwnerUserId);
86 90 return new ImGroupMessageSendDTO().setGroupId(groupId)
87   - .setType(ImMessageTypeEnum.GROUP_OWNER_TRANSFER.getType()).setContent(notification);
  91 + .setType(ImContentTypeEnum.GROUP_OWNER_TRANSFER.getType()).setContent(notification);
88 92 }
89 93  
90 94 public static ImGroupMessageSendDTO ofGroupMemberKick(Long groupId, Long operatorUserId, Collection<Long> memberUserIds) {
... ... @@ -92,7 +96,7 @@ public class ImGroupMessageSendDTO {
92 96 notification.setOperatorUserId(operatorUserId);
93 97 notification.setMemberUserIds(new ArrayList<>(memberUserIds));
94 98 return new ImGroupMessageSendDTO().setGroupId(groupId)
95   - .setType(ImMessageTypeEnum.GROUP_MEMBER_KICK.getType()).setContent(notification);
  99 + .setType(ImContentTypeEnum.GROUP_MEMBER_KICK.getType()).setContent(notification);
96 100 }
97 101  
98 102 public static ImGroupMessageSendDTO ofGroupMemberInvite(Long groupId, Long operatorUserId, Collection<Long> memberUserIds) {
... ... @@ -100,7 +104,7 @@ public class ImGroupMessageSendDTO {
100 104 notification.setOperatorUserId(operatorUserId);
101 105 notification.setMemberUserIds(new ArrayList<>(memberUserIds));
102 106 return new ImGroupMessageSendDTO().setGroupId(groupId)
103   - .setType(ImMessageTypeEnum.GROUP_MEMBER_INVITE.getType()).setContent(notification);
  107 + .setType(ImContentTypeEnum.GROUP_MEMBER_INVITE.getType()).setContent(notification);
104 108 }
105 109  
106 110 public static ImGroupMessageSendDTO ofGroupMemberEnter(Long groupId, Long entrantUserId, Integer addSource) {
... ... @@ -109,14 +113,14 @@ public class ImGroupMessageSendDTO {
109 113 notification.setEntrantUserId(entrantUserId);
110 114 notification.setAddSource(addSource);
111 115 return new ImGroupMessageSendDTO().setGroupId(groupId)
112   - .setType(ImMessageTypeEnum.GROUP_MEMBER_ENTER.getType()).setContent(notification);
  116 + .setType(ImContentTypeEnum.GROUP_MEMBER_ENTER.getType()).setContent(notification);
113 117 }
114 118  
115 119 public static ImGroupMessageSendDTO ofGroupDissolve(Long groupId, Long operatorUserId) {
116 120 GroupDissolveNotification notification = new GroupDissolveNotification();
117 121 notification.setOperatorUserId(operatorUserId);
118 122 return new ImGroupMessageSendDTO().setGroupId(groupId)
119   - .setType(ImMessageTypeEnum.GROUP_DISSOLVE.getType()).setContent(notification);
  123 + .setType(ImContentTypeEnum.GROUP_DISSOLVE.getType()).setContent(notification);
120 124 }
121 125  
122 126 public static ImGroupMessageSendDTO ofGroupMemberNicknameUpdate(Long groupId, Long operatorUserId, String displayUserName) {
... ... @@ -124,7 +128,7 @@ public class ImGroupMessageSendDTO {
124 128 notification.setOperatorUserId(operatorUserId);
125 129 notification.setDisplayUserName(displayUserName);
126 130 return new ImGroupMessageSendDTO().setGroupId(groupId)
127   - .setType(ImMessageTypeEnum.GROUP_MEMBER_NICKNAME_UPDATE.getType()).setContent(notification);
  131 + .setType(ImContentTypeEnum.GROUP_MEMBER_NICKNAME_UPDATE.getType()).setContent(notification);
128 132 }
129 133  
130 134 public static ImGroupMessageSendDTO ofGroupAdminAdd(Long groupId, Long operatorUserId, Collection<Long> memberUserIds) {
... ... @@ -132,7 +136,7 @@ public class ImGroupMessageSendDTO {
132 136 notification.setOperatorUserId(operatorUserId);
133 137 notification.setMemberUserIds(new ArrayList<>(memberUserIds));
134 138 return new ImGroupMessageSendDTO().setGroupId(groupId)
135   - .setType(ImMessageTypeEnum.GROUP_ADMIN_ADD.getType()).setContent(notification);
  139 + .setType(ImContentTypeEnum.GROUP_ADMIN_ADD.getType()).setContent(notification);
136 140 }
137 141  
138 142 public static ImGroupMessageSendDTO ofGroupAdminRemove(Long groupId, Long operatorUserId, Collection<Long> memberUserIds) {
... ... @@ -140,7 +144,7 @@ public class ImGroupMessageSendDTO {
140 144 notification.setOperatorUserId(operatorUserId);
141 145 notification.setMemberUserIds(new ArrayList<>(memberUserIds));
142 146 return new ImGroupMessageSendDTO().setGroupId(groupId)
143   - .setType(ImMessageTypeEnum.GROUP_ADMIN_REMOVE.getType()).setContent(notification);
  147 + .setType(ImContentTypeEnum.GROUP_ADMIN_REMOVE.getType()).setContent(notification);
144 148 }
145 149  
146 150 public static ImGroupMessageSendDTO ofGroupNoticeUpdate(Long groupId, Long operatorUserId, String oldNotice, String newNotice) {
... ... @@ -148,7 +152,7 @@ public class ImGroupMessageSendDTO {
148 152 notification.setOperatorUserId(operatorUserId);
149 153 notification.setOldNotice(oldNotice).setNewNotice(newNotice);
150 154 return new ImGroupMessageSendDTO().setGroupId(groupId)
151   - .setType(ImMessageTypeEnum.GROUP_NOTICE_UPDATE.getType()).setContent(notification);
  155 + .setType(ImContentTypeEnum.GROUP_NOTICE_UPDATE.getType()).setContent(notification);
152 156 }
153 157  
154 158 public static ImGroupMessageSendDTO ofGroupNameUpdate(Long groupId, Long operatorUserId, String oldName, String newName) {
... ... @@ -156,7 +160,7 @@ public class ImGroupMessageSendDTO {
156 160 notification.setOperatorUserId(operatorUserId);
157 161 notification.setOldName(oldName).setNewName(newName);
158 162 return new ImGroupMessageSendDTO().setGroupId(groupId)
159   - .setType(ImMessageTypeEnum.GROUP_NAME_UPDATE.getType()).setContent(notification);
  163 + .setType(ImContentTypeEnum.GROUP_NAME_UPDATE.getType()).setContent(notification);
160 164 }
161 165  
162 166 public static ImGroupMessageSendDTO ofGroupMemberSettingUpdate(Long groupId, Long operatorUserId, Boolean silent, String groupRemark) {
... ... @@ -164,7 +168,7 @@ public class ImGroupMessageSendDTO {
164 168 notification.setOperatorUserId(operatorUserId);
165 169 notification.setSilent(silent).setGroupRemark(groupRemark);
166 170 return new ImGroupMessageSendDTO().setGroupId(groupId)
167   - .setType(ImMessageTypeEnum.GROUP_MEMBER_SETTING_UPDATE.getType()).setContent(notification);
  171 + .setType(ImContentTypeEnum.GROUP_MEMBER_SETTING_UPDATE.getType()).setContent(notification);
168 172 }
169 173  
170 174 public static ImGroupMessageSendDTO ofGroupMessagePin(Long groupId, Long operatorUserId, ImGroupMessageDO message) {
... ... @@ -176,7 +180,7 @@ public class ImGroupMessageSendDTO {
176 180 notification.setOperatorUserId(operatorUserId);
177 181 notification.setMessageId(message.getId()).setMessage(pinnedMessage);
178 182 return new ImGroupMessageSendDTO().setGroupId(groupId)
179   - .setType(ImMessageTypeEnum.GROUP_MESSAGE_PIN.getType()).setContent(notification);
  183 + .setType(ImContentTypeEnum.GROUP_MESSAGE_PIN.getType()).setContent(notification);
180 184 }
181 185  
182 186 public static ImGroupMessageSendDTO ofGroupMessageUnpin(Long groupId, Long operatorUserId, Long messageId) {
... ... @@ -184,7 +188,7 @@ public class ImGroupMessageSendDTO {
184 188 notification.setOperatorUserId(operatorUserId);
185 189 notification.setMessageId(messageId);
186 190 return new ImGroupMessageSendDTO().setGroupId(groupId)
187   - .setType(ImMessageTypeEnum.GROUP_MESSAGE_UNPIN.getType()).setContent(notification);
  191 + .setType(ImContentTypeEnum.GROUP_MESSAGE_UNPIN.getType()).setContent(notification);
188 192 }
189 193  
190 194 public static ImGroupMessageSendDTO ofGroupMemberMuted(Long groupId, Long operatorUserId,
... ... @@ -195,7 +199,7 @@ public class ImGroupMessageSendDTO {
195 199 notification.setMutedUserId(mutedUserId);
196 200 notification.setMuteEndTime(muteEndTime);
197 201 return new ImGroupMessageSendDTO().setGroupId(groupId)
198   - .setType(ImMessageTypeEnum.GROUP_MEMBER_MUTED.getType()).setContent(notification);
  202 + .setType(ImContentTypeEnum.GROUP_MEMBER_MUTED.getType()).setContent(notification);
199 203 }
200 204  
201 205 public static ImGroupMessageSendDTO ofGroupMemberCancelMuted(Long groupId, Long operatorUserId, Long mutedUserId) {
... ... @@ -203,21 +207,21 @@ public class ImGroupMessageSendDTO {
203 207 notification.setOperatorUserId(operatorUserId);
204 208 notification.setMutedUserId(mutedUserId);
205 209 return new ImGroupMessageSendDTO().setGroupId(groupId)
206   - .setType(ImMessageTypeEnum.GROUP_MEMBER_CANCEL_MUTED.getType()).setContent(notification);
  210 + .setType(ImContentTypeEnum.GROUP_MEMBER_CANCEL_MUTED.getType()).setContent(notification);
207 211 }
208 212  
209 213 public static ImGroupMessageSendDTO ofGroupMuted(Long groupId, Long operatorUserId) {
210 214 GroupMutedNotification notification = new GroupMutedNotification();
211 215 notification.setOperatorUserId(operatorUserId);
212 216 return new ImGroupMessageSendDTO().setGroupId(groupId)
213   - .setType(ImMessageTypeEnum.GROUP_MUTED.getType()).setContent(notification);
  217 + .setType(ImContentTypeEnum.GROUP_MUTED.getType()).setContent(notification);
214 218 }
215 219  
216 220 public static ImGroupMessageSendDTO ofGroupCancelMuted(Long groupId, Long operatorUserId) {
217 221 GroupCancelMutedNotification notification = new GroupCancelMutedNotification();
218 222 notification.setOperatorUserId(operatorUserId);
219 223 return new ImGroupMessageSendDTO().setGroupId(groupId)
220   - .setType(ImMessageTypeEnum.GROUP_CANCEL_MUTED.getType()).setContent(notification);
  224 + .setType(ImContentTypeEnum.GROUP_CANCEL_MUTED.getType()).setContent(notification);
221 225 }
222 226  
223 227 public static ImGroupMessageSendDTO ofGroupBanned(Long groupId, Long operatorUserId, boolean banned) {
... ... @@ -225,7 +229,7 @@ public class ImGroupMessageSendDTO {
225 229 notification.setOperatorUserId(operatorUserId);
226 230 notification.setBanned(banned);
227 231 return new ImGroupMessageSendDTO().setGroupId(groupId)
228   - .setType(ImMessageTypeEnum.GROUP_BANNED.getType()).setContent(notification);
  232 + .setType(ImContentTypeEnum.GROUP_BANNED.getType()).setContent(notification);
229 233 }
230 234  
231 235 }
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/dto/ImPrivateMessageSendDTO.java
1 1 package cn.iocoder.yudao.module.im.service.message.dto;
2 2  
3   -import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
  3 +import cn.iocoder.yudao.module.im.enums.ImContentTypeEnum;
4 4 import lombok.Data;
5 5  
6 6 /**
... ... @@ -18,7 +18,7 @@ public class ImPrivateMessageSendDTO {
18 18 /**
19 19 * 消息类型
20 20 * <p>
21   - * 枚举 {@link ImMessageTypeEnum}
  21 + * 枚举 {@link ImContentTypeEnum}
22 22 */
23 23 private Integer type;
24 24 /**
... ... @@ -30,10 +30,14 @@ public class ImPrivateMessageSendDTO {
30 30 /**
31 31 * 是否持久化 + 推送给接收方(单边语义开关)
32 32 * <p>
33   - * null:默认按 {@link ImMessageTypeEnum#isPersistent()} 决定是否入库 + 双向 WS 推送(保持原行为)<br>
  33 + * null:默认按 {@link ImContentTypeEnum#isPersistent()} 决定是否入库 + 双向 WS 推送(保持原行为)<br>
34 34 * false:覆盖为单边——不入库、仅推 sender 多端,对方不感知(如「你已删除 XXX」这类仅自己可见的 TIP)<br>
35 35 * true:覆盖为双向 + 入库
36 36 */
37 37 private Boolean persistent;
  38 + /**
  39 + * 是否需要回执;null 等价 false(系统消息默认不需要回执)
  40 + */
  41 + private Boolean receipt;
38 42  
39 43 }
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/rtc/ImRtcCallServiceImpl.java
... ... @@ -15,7 +15,7 @@ import cn.iocoder.yudao.module.im.dal.mysql.rtc.ImRtcCallMapper;
15 15 import cn.iocoder.yudao.module.im.dal.mysql.rtc.ImRtcParticipantMapper;
16 16 import cn.iocoder.yudao.module.im.dal.redis.rtc.ImRtcCallLockRedisDAO;
17 17 import cn.iocoder.yudao.module.im.enums.ImConversationTypeEnum;
18   -import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
  18 +import cn.iocoder.yudao.module.im.enums.ImContentTypeEnum;
19 19 import cn.iocoder.yudao.module.im.enums.rtc.ImRtcCallEndReasonEnum;
20 20 import cn.iocoder.yudao.module.im.enums.rtc.ImRtcCallStatusEnum;
21 21 import cn.iocoder.yudao.module.im.enums.rtc.ImRtcParticipantRoleEnum;
... ... @@ -30,8 +30,7 @@ import cn.iocoder.yudao.module.im.service.message.ImPrivateMessageService;
30 30 import cn.iocoder.yudao.module.im.service.message.dto.ImGroupMessageSendDTO;
31 31 import cn.iocoder.yudao.module.im.service.message.dto.ImPrivateMessageSendDTO;
32 32 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService;
33   -import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO;
34   -import cn.iocoder.yudao.module.im.service.websocket.dto.notification.rtc.*;
  33 +import cn.iocoder.yudao.module.im.service.websocket.notification.rtc.*;
35 34 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
36 35 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
37 36 import jakarta.annotation.Resource;
... ... @@ -57,7 +56,7 @@ import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*;
57 56 * 并发幂等:同好友对 / 同群活跃唯一性走 {@link ImRtcCallLockRedisDAO} 分布式锁 + 锁内 SELECT 兜底;webhook 兜底走条件 UPDATE;
58 57 * <p>
59 58 * 推送通道分流:
60   - * 1601 RTC_CALL(INVITING / JOINED / REJECTED / NO_ANSWER / LEFT 子类型)→ {@link ImWebSocketService#sendPrivateMessageAsync} 仅推参与方;
  59 + * 1601 RTC_CALL(INVITING / JOINED / REJECTED / NO_ANSWER / LEFT 子类型)→ {@link ImWebSocketService#sendNotificationAsync} 仅推参与方;
61 60 * 1602 / 1603 PARTICIPANT_CONNECTED / DISCONNECTED → {@link ImWebSocketService} 推参与方 + 群通话场景广播全群;
62 61 * 1610 RTC_CALL_START + 1611 RTC_CALL_END → {@link ImPrivateMessageService} / {@link ImGroupMessageService} 入消息流当聊天 tip
63 62 * (START 仅群通话;两者分别在 invite / cancel(leave) 事务里 INSERT,自增 id 自然保证顺序)
... ... @@ -707,7 +706,6 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
707 706 }
708 707  
709 708 // 3.1 群通话:推 RTC_CALL(NO_ANSWER) 让前端 banner 移除该人 + 级联关房判定
710   - // TODO DONE @AI:拆分独立 NO_ANSWER 信令,不再复用 REJECT
711 709 if (ImConversationTypeEnum.isGroup(call.getConversationType())) {
712 710 pushCallNoAnswerNotification(call, userId, userMap.get(userId));
713 711 endSessionIfTerminal(call, userId);
... ... @@ -1003,8 +1001,8 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
1003 1001 String token = signToken(inviteeId, resolveDisplayName(invitee, inviteeId), call.getRoom());
1004 1002 ImRtcCallNotification payload = ImRtcCallNotification.ofInvite(
1005 1003 call, inviter, imProperties.getRtc().getLivekitUrl(), token, inviteeIds);
1006   - webSocketService.sendPrivateMessageAsync(inviteeId, ImPrivateMessageDTO.ofRtcNotification(
1007   - ImMessageTypeEnum.RTC_CALL.getType(), call.getInviterUserId(), inviteeId, payload));
  1004 + webSocketService.sendNotificationAsync(inviteeId, ImConversationTypeEnum.NONE.getType(),
  1005 + ImContentTypeEnum.RTC_CALL.getType(), payload);
1008 1006 }
1009 1007  
1010 1008 /**
... ... @@ -1019,8 +1017,8 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
1019 1017 AdminUserRespDTO operator = operatorUserId != null ? adminUserApi.getUser(operatorUserId) : null;
1020 1018 ImRtcCallNotification payload = ImRtcCallNotification.ofReject(call, operatorUserId, operator);
1021 1019 for (Long receiverUserId : getCallAudienceUserIdList(call)) {
1022   - webSocketService.sendPrivateMessageAsync(receiverUserId, ImPrivateMessageDTO.ofRtcNotification(
1023   - ImMessageTypeEnum.RTC_CALL.getType(), operatorUserId, receiverUserId, payload));
  1020 + webSocketService.sendNotificationAsync(receiverUserId, ImConversationTypeEnum.NONE.getType(),
  1021 + ImContentTypeEnum.RTC_CALL.getType(), payload);
1024 1022 }
1025 1023 }
1026 1024  
... ... @@ -1036,8 +1034,8 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
1036 1034 private void pushCallNoAnswerNotification(ImRtcCallDO call, Long operatorUserId, AdminUserRespDTO operator) {
1037 1035 ImRtcCallNotification payload = ImRtcCallNotification.ofNoAnswer(call, operatorUserId, operator);
1038 1036 for (Long receiverUserId : getCallAudienceUserIdList(call)) {
1039   - webSocketService.sendPrivateMessageAsync(receiverUserId, ImPrivateMessageDTO.ofRtcNotification(
1040   - ImMessageTypeEnum.RTC_CALL.getType(), operatorUserId, receiverUserId, payload));
  1037 + webSocketService.sendNotificationAsync(receiverUserId, ImConversationTypeEnum.NONE.getType(),
  1038 + ImContentTypeEnum.RTC_CALL.getType(), payload);
1041 1039 }
1042 1040 }
1043 1041  
... ... @@ -1048,7 +1046,7 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
1048 1046 * @param userId 加入的参与者用户编号
1049 1047 */
1050 1048 private void pushParticipantConnectedNotification(ImRtcCallDO call, Long userId) {
1051   - pushParticipantNotification(call, ImMessageTypeEnum.RTC_PARTICIPANT_CONNECTED.getType(), userId,
  1049 + pushParticipantNotification(call, ImContentTypeEnum.RTC_PARTICIPANT_CONNECTED.getType(), userId,
1052 1050 ImRtcParticipantConnectedNotification.of(call, userId));
1053 1051 }
1054 1052  
... ... @@ -1059,7 +1057,7 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
1059 1057 * @param userId 离开的参与者用户编号
1060 1058 */
1061 1059 private void pushParticipantDisconnectedNotification(ImRtcCallDO call, Long userId) {
1062   - pushParticipantNotification(call, ImMessageTypeEnum.RTC_PARTICIPANT_DISCONNECTED.getType(), userId,
  1060 + pushParticipantNotification(call, ImContentTypeEnum.RTC_PARTICIPANT_DISCONNECTED.getType(), userId,
1063 1061 ImRtcParticipantDisconnectedNotification.of(call, userId));
1064 1062 }
1065 1063  
... ... @@ -1076,8 +1074,7 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
1076 1074 if (CollUtil.isEmpty(receivers)) {
1077 1075 return;
1078 1076 }
1079   - ImPrivateMessageDTO dto = ImPrivateMessageDTO.ofRtcNotification(type, actorUserId, null, payload);
1080   - webSocketService.sendPrivateMessageAsync(receivers, dto);
  1077 + webSocketService.sendNotificationAsync(receivers, ImConversationTypeEnum.NONE.getType(), type, payload);
1081 1078 }
1082 1079  
1083 1080 /**
... ... @@ -1092,7 +1089,7 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
1092 1089 private void pushCallStartNotification(ImRtcCallDO call, AdminUserRespDTO inviter, Set<Long> invitees) {
1093 1090 ImRtcCallStartNotification payload = ImRtcCallStartNotification.of(call, inviter);
1094 1091 Long peerUserId = ImConversationTypeEnum.isGroup(call.getConversationType()) ? null : CollUtil.getFirst(invitees);
1095   - pushCallChatMessage(call, ImMessageTypeEnum.RTC_CALL_START, payload, peerUserId);
  1092 + pushCallChatMessage(call, ImContentTypeEnum.RTC_CALL_START, payload, peerUserId);
1096 1093 }
1097 1094  
1098 1095 /**
... ... @@ -1116,7 +1113,7 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
1116 1113 p -> ObjUtil.notEqual(p.getUserId(), call.getInviterUserId()));
1117 1114 peerUserId = peer != null ? peer.getUserId() : null;
1118 1115 }
1119   - pushCallChatMessage(call, ImMessageTypeEnum.RTC_CALL_END, payload, peerUserId);
  1116 + pushCallChatMessage(call, ImContentTypeEnum.RTC_CALL_END, payload, peerUserId);
1120 1117 }
1121 1118  
1122 1119 /**
... ... @@ -1129,7 +1126,7 @@ public class ImRtcCallServiceImpl implements ImRtcCallService {
1129 1126 * @param payload 推送 payload
1130 1127 * @param peerUserId 私聊对端用户编号;群聊忽略,私聊缺失时回退为 senderId
1131 1128 */
1132   - private void pushCallChatMessage(ImRtcCallDO call, ImMessageTypeEnum type, Object payload, Long peerUserId) {
  1129 + private void pushCallChatMessage(ImRtcCallDO call, ImContentTypeEnum type, Object payload, Long peerUserId) {
1133 1130 Long senderId = call.getInviterUserId();
1134 1131 if (ImConversationTypeEnum.isGroup(call.getConversationType())) {
1135 1132 ImGroupMessageSendDTO dto = new ImGroupMessageSendDTO().setGroupId(call.getGroupId())
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/statistics/ImStatisticsManagerService.java
... ... @@ -80,7 +80,7 @@ public interface ImStatisticsManagerService {
80 80 Map<LocalDateTime, Long> getGroupMessageDailyCountMap(LocalDateTime beginTime, LocalDateTime endTime);
81 81  
82 82 /**
83   - * 获取区间内消息类型分布 Map(key 为消息类型)
  83 + * 获取区间内内容类型分布 Map(key 为消息类型)
84 84 */
85 85 Map<Integer, Long> getMessageTypeCountMap(LocalDateTime beginTime, LocalDateTime endTime);
86 86  
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/ImWebSocketService.java
1 1 package cn.iocoder.yudao.module.im.service.websocket;
2 2  
3   -import cn.iocoder.yudao.module.im.service.websocket.dto.ImChannelMessageDTO;
4   -import cn.iocoder.yudao.module.im.service.websocket.dto.ImGroupMessageDTO;
5   -import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO;
6   -
7 3 import java.util.Collection;
8 4 import java.util.Collections;
9 5  
10 6 /**
11 7 * IM WebSocket 推送 Service 接口
12 8 * <p>
13   - * 统一封装 WebSocket 消息推送,事务内调用时提交后异步执行。
  9 + * 统一封装 WebSocket 通知推送,事务内调用时提交后异步执行。
14 10 *
15 11 * @author 芋道源码
16 12 */
17 13 public interface ImWebSocketService {
18 14  
19 15 /**
20   - * 异步推送私聊消息给指定用户
21   - *
22   - * @param userId 目标用户编号
23   - * @param dto 私聊消息 DTO
24   - */
25   - default void sendPrivateMessageAsync(Long userId, ImPrivateMessageDTO dto) {
26   - sendPrivateMessageAsync(Collections.singleton(userId), dto);
27   - }
28   -
29   - /**
30   - * 异步批量推送私聊消息给多个用户;用于同一份 DTO 扇出到多个收件人
31   - * <p>
32   - * 相比逐个发送,仅注册一次 afterCommit 回调和异步任务。
33   - *
34   - * @param userIds 目标用户编号列表
35   - * @param dto 私聊消息 DTO
36   - */
37   - void sendPrivateMessageAsync(Collection<Long> userIds, ImPrivateMessageDTO dto);
38   -
39   - /**
40   - * 异步推送群聊消息给指定用户
41   - *
42   - * @param userId 目标用户编号
43   - * @param dto 群聊消息 DTO
44   - */
45   - default void sendGroupMessageAsync(Long userId, ImGroupMessageDTO dto) {
46   - sendGroupMessageAsync(Collections.singleton(userId), dto);
47   - }
48   -
49   - /**
50   - * 异步批量推送群聊消息给多个用户
51   - *
52   - * @param userIds 目标用户编号列表
53   - * @param dto 群聊消息 DTO
54   - */
55   - void sendGroupMessageAsync(Collection<Long> userIds, ImGroupMessageDTO dto);
56   -
57   - /**
58   - * 异步推送频道消息给指定用户
  16 + * 异步推送 WebSocket 通知给指定用户
59 17 *
60   - * @param userId 目标用户编号
61   - * @param dto 频道消息 DTO
  18 + * @param userId 目标用户编号
  19 + * @param conversationType 会话类型,参见 ImConversationTypeEnum 枚举类
  20 + * @param contentType 内容类型,参见 ImContentTypeEnum 枚举类
  21 + * @param payload 通知 payload
62 22 */
63   - default void sendChannelMessageAsync(Long userId, ImChannelMessageDTO dto) {
64   - sendChannelMessageAsync(Collections.singleton(userId), dto);
  23 + default void sendNotificationAsync(Long userId, Integer conversationType, Integer contentType, Object payload) {
  24 + sendNotificationAsync(Collections.singleton(userId), conversationType, contentType, payload);
65 25 }
66 26  
67 27 /**
68   - * 异步批量推送频道消息给多个用户
  28 + * 异步批量推送 WebSocket 通知给多个用户
69 29 *
70   - * @param userIds 目标用户编号列表
71   - * @param dto 频道消息 DTO
  30 + * @param userIds 目标用户编号列表
  31 + * @param conversationType 会话类型,参见 ImConversationTypeEnum 枚举类
  32 + * @param contentType 内容类型,参见 ImContentTypeEnum 枚举类
  33 + * @param payload 通知 payload
72 34 */
73   - void sendChannelMessageAsync(Collection<Long> userIds, ImChannelMessageDTO dto);
  35 + void sendNotificationAsync(Collection<Long> userIds, Integer conversationType, Integer contentType, Object payload);
74 36  
75 37 /**
76   - * 异步广播频道消息给当前所有在线管理端用户;用于全员推送
  38 + * 异步广播 WebSocket 通知给当前所有在线管理端用户
77 39 *
78   - * @param dto 频道消息 DTO
  40 + * @param conversationType 会话类型,参见 ImConversationTypeEnum 枚举类
  41 + * @param contentType 内容类型,参见 ImContentTypeEnum 枚举类
  42 + * @param payload 通知 payload
79 43 */
80   - void broadcastChannelMessageAsync(ImChannelMessageDTO dto);
  44 + void broadcastNotificationAsync(Integer conversationType, Integer contentType, Object payload);
81 45  
82 46 }
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/ImWebSocketServiceImpl.java
... ... @@ -2,9 +2,7 @@ package cn.iocoder.yudao.module.im.service.websocket;
2 2  
3 3 import cn.hutool.extra.spring.SpringUtil;
4 4 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
5   -import cn.iocoder.yudao.module.im.service.websocket.dto.ImChannelMessageDTO;
6   -import cn.iocoder.yudao.module.im.service.websocket.dto.ImGroupMessageDTO;
7   -import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO;
  5 +import cn.iocoder.yudao.module.im.service.websocket.notification.ImNotificationWebSocketDTO;
8 6 import cn.iocoder.yudao.module.infra.api.websocket.WebSocketSenderApi;
9 7 import jakarta.annotation.Resource;
10 8 import lombok.extern.slf4j.Slf4j;
... ... @@ -36,83 +34,51 @@ public class ImWebSocketServiceImpl implements ImWebSocketService {
36 34 private WebSocketSenderApi webSocketSenderApi;
37 35  
38 36 @Override
39   - public void sendPrivateMessageAsync(Collection<Long> userIds, ImPrivateMessageDTO dto) {
40   - // 说明:通过 executeAfterTransaction 保证事务提交后再推送,避免客户端收到消息后查询数据库时事务尚未提交
41   - // 通过 getSelf() 获取 Spring 代理对象调用 @Async 方法,确保异步 AOP 生效(直接 this 调用会绕过代理)
42   - executeAfterTransaction(() -> getSelf().doSendPrivateMessage(userIds, dto));
  37 + public void sendNotificationAsync(Collection<Long> userIds, Integer conversationType, Integer contentType,
  38 + Object payload) {
  39 + ImNotificationWebSocketDTO notification = buildNotification(conversationType, contentType, payload);
  40 + executeAfterTransaction(() -> getSelf().doSendNotification(userIds, notification));
43 41 }
44 42  
45 43 @Override
46   - public void sendGroupMessageAsync(Collection<Long> userIds, ImGroupMessageDTO dto) {
47   - executeAfterTransaction(() -> getSelf().doSendGroupMessage(userIds, dto));
  44 + public void broadcastNotificationAsync(Integer conversationType, Integer contentType, Object payload) {
  45 + ImNotificationWebSocketDTO notification = buildNotification(conversationType, contentType, payload);
  46 + executeAfterTransaction(() -> getSelf().doBroadcastNotification(notification));
48 47 }
49 48  
50   - @Override
51   - public void sendChannelMessageAsync(Collection<Long> userIds, ImChannelMessageDTO dto) {
52   - executeAfterTransaction(() -> getSelf().doSendChannelMessage(userIds, dto));
53   - }
54   -
55   - @Override
56   - public void broadcastChannelMessageAsync(ImChannelMessageDTO dto) {
57   - executeAfterTransaction(() -> getSelf().doBroadcastChannelMessage(dto));
58   - }
59   -
60   - /**
61   - * 异步发送私聊 WebSocket 消息;多收件人共享同一 dto,避免按收件人重复注册 afterCommit 回调
62   - */
63   - @Async
64   - public void doSendPrivateMessage(Collection<Long> userIds, ImPrivateMessageDTO dto) {
65   - for (Long userId : getDistinctUserIds(userIds)) {
66   - try {
67   - webSocketSenderApi.sendObject(UserTypeEnum.ADMIN.getValue(), userId,
68   - ImPrivateMessageDTO.TYPE, dto);
69   - } catch (Exception e) {
70   - log.error("[doSendPrivateMessage][userId({}) dto({}) 发送失败]", userId, dto, e);
71   - }
72   - }
73   - }
74   -
75   - /**
76   - * 异步发送群聊 WebSocket 消息
77   - */
78   - @Async
79   - public void doSendGroupMessage(Collection<Long> userIds, ImGroupMessageDTO dto) {
80   - for (Long userId : getDistinctUserIds(userIds)) {
81   - try {
82   - webSocketSenderApi.sendObject(UserTypeEnum.ADMIN.getValue(), userId,
83   - ImGroupMessageDTO.TYPE, dto);
84   - } catch (Exception e) {
85   - log.error("[doSendGroupMessage][userId({}) dto({}) 发送失败]", userId, dto, e);
86   - }
87   - }
  49 + private static ImNotificationWebSocketDTO buildNotification(Integer conversationType, Integer contentType,
  50 + Object payload) {
  51 + return new ImNotificationWebSocketDTO()
  52 + .setConversationType(conversationType)
  53 + .setContentType(contentType)
  54 + .setPayload(payload);
88 55 }
89 56  
90 57 /**
91   - * 异步发送频道 WebSocket 消息
  58 + * 异步发送 WebSocket 通知
92 59 */
93 60 @Async
94   - public void doSendChannelMessage(Collection<Long> userIds, ImChannelMessageDTO dto) {
  61 + public void doSendNotification(Collection<Long> userIds, ImNotificationWebSocketDTO notification) {
95 62 for (Long userId : getDistinctUserIds(userIds)) {
96 63 try {
97 64 webSocketSenderApi.sendObject(UserTypeEnum.ADMIN.getValue(), userId,
98   - ImChannelMessageDTO.TYPE, dto);
  65 + ImNotificationWebSocketDTO.TYPE, notification);
99 66 } catch (Exception e) {
100   - log.error("[doSendChannelMessage][userId({}) dto({}) 发送失败]", userId, dto, e);
  67 + log.error("[doSendNotification][userId({}) notification({}) 发送失败]", userId, notification, e);
101 68 }
102 69 }
103 70 }
104 71  
105 72 /**
106   - * 异步广播频道 WebSocket 消息给当前所有在线管理端用户;
107   - * 依赖 infra WebSocketSenderApi 按 UserType 广播能力,离线用户由客户端上线 pull 兜底
  73 + * 异步广播 WebSocket 通知
108 74 */
109 75 @Async
110   - public void doBroadcastChannelMessage(ImChannelMessageDTO dto) {
  76 + public void doBroadcastNotification(ImNotificationWebSocketDTO notification) {
111 77 try {
112 78 webSocketSenderApi.sendObject(UserTypeEnum.ADMIN.getValue(),
113   - ImChannelMessageDTO.TYPE, dto);
  79 + ImNotificationWebSocketDTO.TYPE, notification);
114 80 } catch (Exception e) {
115   - log.error("[doBroadcastChannelMessage][dto({}) 广播失败]", dto, e);
  81 + log.error("[doBroadcastNotification][notification({}) 广播失败]", notification, e);
116 82 }
117 83 }
118 84  
... ... @@ -121,19 +87,15 @@ public class ImWebSocketServiceImpl implements ImWebSocketService {
121 87 }
122 88  
123 89 /**
124   - * 事务感知的任务调度:
125   - * - 有事务:注册 afterCommit 回调,事务提交后再执行,防止客户端拿到消息去查库时数据还没落盘
126   - * - 无事务:直接执行(如非 @Transactional 方法中的调用)
  90 + * 事务感知的任务调度
127 91 *
128   - * @param task 待执行的推送任务(内部通过 getSelf() 走 @Async 异步执行)
  92 + * @param task 待执行的推送任务
129 93 */
130 94 private void executeAfterTransaction(Runnable task) {
131   - // 情况一:没有事务,直接执行
132 95 if (!TransactionSynchronizationManager.isSynchronizationActive()) {
133 96 task.run();
134 97 return;
135 98 }
136   - // 情况二:有事务,注册 afterCommit 事件,在事务提交后执行
137 99 TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
138 100  
139 101 @Override
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/ImPrivateMessageDTO.java deleted
1   -package cn.iocoder.yudao.module.im.service.websocket.dto;
2   -
3   -import cn.hutool.core.lang.Assert;
4   -import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
5   -import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
6   -import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO;
7   -import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
8   -import cn.iocoder.yudao.module.im.service.websocket.dto.notification.friend.BaseFriendNotification;
9   -import cn.iocoder.yudao.module.im.service.websocket.dto.notification.group.BaseGroupNotification;
10   -import lombok.Data;
11   -import lombok.experimental.Accessors;
12   -
13   -import java.time.LocalDateTime;
14   -
15   -/**
16   - * IM 私聊消息 WebSocket 统一推送 DTO
17   - * <p>
18   - * 发送、已读、回执、撤回都通过 {@link #type} 字段区分,
19   - * 并统一复用 {@link #id} 字段表达目标消息或已读位置。
20   - *
21   - * @author 芋道源码
22   - */
23   -@Data
24   -@Accessors(chain = true)
25   -public class ImPrivateMessageDTO {
26   -
27   - public static final String TYPE = "im-private-message";
28   -
29   - /**
30   - * 消息编号
31   - * <p>
32   - * 普通消息:当前消息 id
33   - * READ:已读位置(我已读到这条消息)
34   - * RECEIPT:已读位置(对方已读到这条消息)
35   - * RECALL:被撤回的原消息 id
36   - */
37   - private Long id;
38   - /**
39   - * 客户端消息编号
40   - */
41   - private String clientMessageId;
42   - /**
43   - * 发送人编号
44   - */
45   - private Long senderId;
46   - /**
47   - * 接收人编号
48   - */
49   - private Long receiverId;
50   - /**
51   - * 消息类型
52   - */
53   - private Integer type;
54   - /**
55   - * 消息内容
56   - */
57   - private String content;
58   - /**
59   - * 消息状态
60   - */
61   - private Integer status;
62   - /**
63   - * 发送时间
64   - */
65   - private LocalDateTime sendTime;
66   -
67   - // ========== 静态工厂方法 ==========
68   -
69   - /**
70   - * 构建发送消息 DTO
71   - *
72   - * @param message 私聊消息 DO
73   - * @return 私聊 DTO
74   - */
75   - public static ImPrivateMessageDTO ofSend(ImPrivateMessageDO message) {
76   - return BeanUtils.toBean(message, ImPrivateMessageDTO.class);
77   - }
78   -
79   - /**
80   - * 构建已读同步 DTO(多端同步:通知自己的其他终端"我已经读了某个会话")
81   - *
82   - * @param senderId 当前用户编号
83   - * @param receiverId 对方用户编号
84   - * @param readId 已读位置(最大已读消息编号)
85   - * @return 私聊 DTO
86   - */
87   - public static ImPrivateMessageDTO ofRead(Long senderId, Long receiverId, Long readId) {
88   - return new ImPrivateMessageDTO()
89   - .setId(readId).setType(ImMessageTypeEnum.READ.getType())
90   - .setSenderId(senderId).setReceiverId(receiverId);
91   - }
92   -
93   - /**
94   - * 构建已读回执 DTO(通知对方"我已读了你的消息")
95   - *
96   - * @param senderId 已读方的用户编号
97   - * @param receiverId 对方用户编号
98   - * @param readId 已读位置(最大已读消息编号)
99   - * @return 私聊 DTO
100   - */
101   - public static ImPrivateMessageDTO ofReceipt(Long senderId, Long receiverId, Long readId) {
102   - return new ImPrivateMessageDTO()
103   - .setId(readId).setType(ImMessageTypeEnum.RECEIPT.getType())
104   - .setSenderId(senderId).setReceiverId(receiverId);
105   - }
106   -
107   - // ==================== 好友变更相关 ====================
108   -
109   - /**
110   - * 构建好友通知推送 DTO(统一入口)
111   - *
112   - * @param type 消息类型;取自 {@link ImMessageTypeEnum} 中的 FRIEND_* 段
113   - * @param operatorUserId 操作人用户编号;同时作为帧的 senderId 用于定位接收端 friendUserId
114   - * @param receiverUserId 推送目标用户编号
115   - * @param payload 好友通知 payload(继承 {@link BaseFriendNotification})
116   - * @return 私聊 DTO
117   - */
118   - public static ImPrivateMessageDTO ofFriendNotification(Integer type, Long operatorUserId,
119   - Long receiverUserId, BaseFriendNotification payload) {
120   - validateNotification(type, payload, ImMessageTypeEnum.isFriendNotification(type));
121   - return new ImPrivateMessageDTO().setType(type)
122   - .setSenderId(operatorUserId).setReceiverId(receiverUserId)
123   - .setContent(JsonUtils.toJsonString(payload)).setSendTime(LocalDateTime.now());
124   - }
125   -
126   - // ==================== 群定向私聊通知 ====================
127   -
128   - // TODO DONE @AI:群申请定向通知继续走私聊通道
129   - /**
130   - * 构建群通知推送 DTO(走私聊通道定向推送,不入群消息流)
131   - * <p>
132   - * 用于 GROUP_REQUEST_RECEIVED / GROUP_REQUEST_APPROVED / GROUP_REQUEST_REJECTED 段位;
133   - * 与 {@link cn.iocoder.yudao.module.im.service.message.dto.ImGroupMessageSendDTO} 走 sendGroupMessage 群广播路径不同
134   - *
135   - * @param type 消息类型;取自 {@link ImMessageTypeEnum} 中的 GROUP_REQUEST_* 段
136   - * @param operatorUserId 操作人用户编号;同时作为帧的 senderId
137   - * @param receiverUserId 推送目标用户编号
138   - * @param payload 群事件 payload(继承 {@link BaseGroupNotification})
139   - * @return 私聊 DTO
140   - */
141   - public static ImPrivateMessageDTO ofGroupNotification(Integer type, Long operatorUserId,
142   - Long receiverUserId, BaseGroupNotification payload) {
143   - validateNotification(type, payload, ImMessageTypeEnum.isGroupRequestNotification(type));
144   - return new ImPrivateMessageDTO().setType(type)
145   - .setSenderId(operatorUserId).setReceiverId(receiverUserId)
146   - .setContent(JsonUtils.toJsonString(payload)).setSendTime(LocalDateTime.now());
147   - }
148   -
149   - // ==================== 实时通话信令 ====================
150   -
151   - /**
152   - * 构建通话信令推送 DTO(走私聊通道仅推参与方,不入消息流)
153   - * <p>
154   - * 用于 RTC_CALL(INVITE / REJECT 等)/ RTC_PARTICIPANT_CONNECTED / RTC_PARTICIPANT_DISCONNECTED
155   - *
156   - * @param type 消息类型;取自 {@link ImMessageTypeEnum} 中的 RTC_* 段
157   - * @param senderId 发送人编号;INVITE 时为发起人,REJECT 时为拒绝者,参与者事件时为加入 / 离开者
158   - * @param receiverUserId 推送目标用户编号
159   - * @param payload 通话事件 payload(任意 RTC 通知 DTO)
160   - * @return 私聊 DTO
161   - */
162   - public static ImPrivateMessageDTO ofRtcNotification(Integer type, Long senderId,
163   - Long receiverUserId, Object payload) {
164   - validateNotification(type, payload, ImMessageTypeEnum.isRtcNotification(type));
165   - return new ImPrivateMessageDTO().setType(type)
166   - .setSenderId(senderId).setReceiverId(receiverUserId)
167   - .setContent(JsonUtils.toJsonString(payload)).setSendTime(LocalDateTime.now());
168   - }
169   -
170   - private static void validateNotification(Integer type, Object payload, boolean supported) {
171   - Assert.notNull(type, "消息类型不能为空");
172   - Assert.notNull(payload, "消息内容不能为空");
173   - Assert.isTrue(supported, "消息类型不匹配 type={}", type);
174   - }
175   -
176   -}
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/notification/ImNotificationWebSocketDTO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.service.websocket.notification;
  2 +
  3 +import lombok.Data;
  4 +import lombok.experimental.Accessors;
  5 +
  6 +/**
  7 + * IM WebSocket 在线通知外壳
  8 + * <p>
  9 + * conversationType 定位会话维度;contentType 定位业务内容;payload 承载对应通知对象。
  10 + * 会进入聊天流的私聊、群聊、频道事件走 message 子包;不进入聊天流的好友、加群申请、通话信令走 NONE 会话。
  11 + *
  12 + * @author 芋道源码
  13 + */
  14 +@Data
  15 +@Accessors(chain = true)
  16 +public class ImNotificationWebSocketDTO {
  17 +
  18 + public static final String TYPE = "im-notification";
  19 +
  20 + /**
  21 + * 会话类型,参见 ImConversationTypeEnum 枚举类
  22 + */
  23 + private Integer conversationType;
  24 + /**
  25 + * 内容类型,参见 ImContentTypeEnum 枚举类
  26 + */
  27 + private Integer contentType;
  28 + /**
  29 + * 负载数据
  30 + */
  31 + private Object payload;
  32 +
  33 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/friend/BaseFriendNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/notification/friend/BaseFriendNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification.friend;
  1 +package cn.iocoder.yudao.module.im.service.websocket.notification.friend;
2 2  
3 3 import lombok.Data;
4 4  
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/friend/FriendAddNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/notification/friend/FriendAddNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification.friend;
  1 +package cn.iocoder.yudao.module.im.service.websocket.notification.friend;
2 2  
3 3 import lombok.Data;
4 4  
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/friend/FriendBlockNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/notification/friend/FriendBlockNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification.friend;
  1 +package cn.iocoder.yudao.module.im.service.websocket.notification.friend;
2 2  
3 3 import lombok.Data;
4 4  
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/friend/FriendDeleteNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/notification/friend/FriendDeleteNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification.friend;
  1 +package cn.iocoder.yudao.module.im.service.websocket.notification.friend;
2 2  
3 3 import lombok.Data;
4 4  
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/friend/FriendInfoUpdatedNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/notification/friend/FriendInfoUpdatedNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification.friend;
  1 +package cn.iocoder.yudao.module.im.service.websocket.notification.friend;
2 2  
3 3 import lombok.Data;
4 4  
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/friend/FriendRequestApprovedNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/notification/friend/FriendRequestApprovedNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification.friend;
  1 +package cn.iocoder.yudao.module.im.service.websocket.notification.friend;
2 2  
3 3 import lombok.Data;
4 4  
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/friend/FriendRequestNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/notification/friend/FriendRequestNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification.friend;
  1 +package cn.iocoder.yudao.module.im.service.websocket.notification.friend;
2 2  
3 3 import lombok.Data;
4 4  
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/friend/FriendRequestRejectedNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/notification/friend/FriendRequestRejectedNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification.friend;
  1 +package cn.iocoder.yudao.module.im.service.websocket.notification.friend;
2 2  
3 3 import lombok.Data;
4 4  
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/friend/FriendUnblockNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/notification/friend/FriendUnblockNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification.friend;
  1 +package cn.iocoder.yudao.module.im.service.websocket.notification.friend;
2 2  
3 3 import lombok.Data;
4 4  
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/friend/FriendUpdateNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/notification/friend/FriendUpdateNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification.friend;
  1 +package cn.iocoder.yudao.module.im.service.websocket.notification.friend;
2 2  
3 3 import lombok.Data;
4 4  
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/BaseGroupNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/notification/group/BaseGroupNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group;
  1 +package cn.iocoder.yudao.module.im.service.websocket.notification.group;
2 2  
3 3 import lombok.Data;
4 4  
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupAdminAddNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/notification/group/GroupAdminAddNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group;
  1 +package cn.iocoder.yudao.module.im.service.websocket.notification.group;
2 2  
3 3 /**
4 4 * 添加管理员事件通知(memberUserIds 为被设管理员的成员)
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupAdminRemoveNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/notification/group/GroupAdminRemoveNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group;
  1 +package cn.iocoder.yudao.module.im.service.websocket.notification.group;
2 2  
3 3 /**
4 4 * 撤销管理员事件通知(memberUserIds 为被撤销管理员的成员)
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupBannedNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/notification/group/GroupBannedNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group;
  1 +package cn.iocoder.yudao.module.im.service.websocket.notification.group;
2 2  
3 3 import lombok.Data;
4 4  
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupCancelMutedNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/notification/group/GroupCancelMutedNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group;
  1 +package cn.iocoder.yudao.module.im.service.websocket.notification.group;
2 2  
3 3 import lombok.Data;
4 4 import lombok.EqualsAndHashCode;
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupCreateNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/notification/group/GroupCreateNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group;
  1 +package cn.iocoder.yudao.module.im.service.websocket.notification.group;
2 2  
3 3 /**
4 4 * 群创建事件通知(memberUserIds 含创建者 + 初始邀请成员)
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupDissolveNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/notification/group/GroupDissolveNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group;
  1 +package cn.iocoder.yudao.module.im.service.websocket.notification.group;
2 2  
3 3 /**
4 4 * 群解散事件通知
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupInfoUpdateNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/notification/group/GroupInfoUpdateNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group;
  1 +package cn.iocoder.yudao.module.im.service.websocket.notification.group;
2 2  
3 3 import lombok.Data;
4 4  
5 5 /**
6   - * 群信息变更事件通知(当前承载头像变更,NAME / NOTICE 走独立事件)
  6 + * 群信息变更事件通知(NAME / NOTICE 走独立事件)
7 7 */
8 8 @Data
9 9 public class GroupInfoUpdateNotification extends BaseGroupNotification {
... ... @@ -16,5 +16,13 @@ public class GroupInfoUpdateNotification extends BaseGroupNotification {
16 16 * 新群头像
17 17 */
18 18 private String newAvatar;
  19 + /**
  20 + * 旧进群审批开关
  21 + */
  22 + private Boolean oldJoinApproval;
  23 + /**
  24 + * 新进群审批开关
  25 + */
  26 + private Boolean newJoinApproval;
19 27  
20 28 }
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMemberCancelMutedNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/notification/group/GroupMemberCancelMutedNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group;
  1 +package cn.iocoder.yudao.module.im.service.websocket.notification.group;
2 2  
3 3 import lombok.Data;
4 4 import lombok.EqualsAndHashCode;
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMemberEnterNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/notification/group/GroupMemberEnterNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group;
  1 +package cn.iocoder.yudao.module.im.service.websocket.notification.group;
2 2  
3 3 import lombok.Data;
4 4 import lombok.EqualsAndHashCode;
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMemberInviteNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/notification/group/GroupMemberInviteNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group;
  1 +package cn.iocoder.yudao.module.im.service.websocket.notification.group;
2 2  
3 3 /**
4 4 * 成员加入事件通知(memberUserIds 为被邀请人)
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMemberKickNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/notification/group/GroupMemberKickNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group;
  1 +package cn.iocoder.yudao.module.im.service.websocket.notification.group;
2 2  
3 3 /**
4 4 * 成员被移出事件通知(memberUserIds 为被移出成员)
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMemberListNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/notification/group/GroupMemberListNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group;
  1 +package cn.iocoder.yudao.module.im.service.websocket.notification.group;
2 2  
3 3 import lombok.Data;
4 4  
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMemberMutedNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/notification/group/GroupMemberMutedNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group;
  1 +package cn.iocoder.yudao.module.im.service.websocket.notification.group;
2 2  
3 3 import lombok.Data;
4 4 import lombok.EqualsAndHashCode;
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMemberNicknameUpdateNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/notification/group/GroupMemberNicknameUpdateNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group;
  1 +package cn.iocoder.yudao.module.im.service.websocket.notification.group;
2 2  
3 3 import lombok.Data;
4 4  
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMemberQuitNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/notification/group/GroupMemberQuitNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group;
  1 +package cn.iocoder.yudao.module.im.service.websocket.notification.group;
2 2  
3 3 /**
4 4 * 成员退群事件通知
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMemberSettingUpdateNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/notification/group/GroupMemberSettingUpdateNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group;
  1 +package cn.iocoder.yudao.module.im.service.websocket.notification.group;
2 2  
3 3 import lombok.Data;
4 4  
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMessagePinNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/notification/group/GroupMessagePinNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group;
  1 +package cn.iocoder.yudao.module.im.service.websocket.notification.group;
2 2  
3 3 import lombok.Data;
4 4  
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMessageUnpinNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/notification/group/GroupMessageUnpinNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group;
  1 +package cn.iocoder.yudao.module.im.service.websocket.notification.group;
2 2  
3 3 import lombok.Data;
4 4  
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMutedNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/notification/group/GroupMutedNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group;
  1 +package cn.iocoder.yudao.module.im.service.websocket.notification.group;
2 2  
3 3 import lombok.Data;
4 4  
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupNameUpdateNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/notification/group/GroupNameUpdateNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group;
  1 +package cn.iocoder.yudao.module.im.service.websocket.notification.group;
2 2  
3 3 import lombok.Data;
4 4  
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupNoticeUpdateNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/notification/group/GroupNoticeUpdateNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group;
  1 +package cn.iocoder.yudao.module.im.service.websocket.notification.group;
2 2  
3 3 import lombok.Data;
4 4  
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupOwnerTransferNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/notification/group/GroupOwnerTransferNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group;
  1 +package cn.iocoder.yudao.module.im.service.websocket.notification.group;
2 2  
3 3 import lombok.Data;
4 4  
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupRequestApprovedNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/notification/group/GroupRequestApprovedNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group;
  1 +package cn.iocoder.yudao.module.im.service.websocket.notification.group;
2 2  
3 3 import lombok.Data;
4 4 import lombok.EqualsAndHashCode;
... ... @@ -6,7 +6,7 @@ import lombok.EqualsAndHashCode;
6 6 /**
7 7 * 入群申请被同意通知
8 8 * <p>
9   - * 走私聊通道定向推送给申请人 + 群主 + 全部管理员;admin 侧据此把 unhandledCount 减 1 并从未处理列表移除
  9 + * 定向推送给申请人 + 群主 + 全部管理员;admin 侧据此把 unhandledCount 减 1 并从未处理列表移除
10 10 */
11 11 @EqualsAndHashCode(callSuper = true)
12 12 @Data
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupRequestReceivedNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/notification/group/GroupRequestReceivedNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group;
  1 +package cn.iocoder.yudao.module.im.service.websocket.notification.group;
2 2  
3 3 import lombok.Data;
4 4 import lombok.EqualsAndHashCode;
... ... @@ -6,7 +6,7 @@ import lombok.EqualsAndHashCode;
6 6 /**
7 7 * 收到新的入群申请通知
8 8 * <p>
9   - * 走私聊通道定向推送给群主 + 全部管理员(多端同步);payload 已携带申请方昵称 / 头像,前端按 requestId 直接 push 进列表
  9 + * 定向推送给群主 + 全部管理员(多端同步);payload 已携带申请方昵称 / 头像,前端按 requestId 直接 push 进列表
10 10 */
11 11 @EqualsAndHashCode(callSuper = true)
12 12 @Data
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupRequestRejectedNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/notification/group/GroupRequestRejectedNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group;
  1 +package cn.iocoder.yudao.module.im.service.websocket.notification.group;
2 2  
3 3 import lombok.Data;
4 4 import lombok.EqualsAndHashCode;
... ... @@ -6,7 +6,7 @@ import lombok.EqualsAndHashCode;
6 6 /**
7 7 * 入群申请被拒绝通知
8 8 * <p>
9   - * 走私聊通道定向推送给申请人 + 群主 + 全部管理员;admin 侧据此把 unhandledCount 减 1 并从未处理列表移除
  9 + * 定向推送给申请人 + 群主 + 全部管理员;admin 侧据此把 unhandledCount 减 1 并从未处理列表移除
10 10 */
11 11 @EqualsAndHashCode(callSuper = true)
12 12 @Data
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/ImChannelMessageDTO.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/notification/message/ImChannelMessageNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto;
  1 +package cn.iocoder.yudao.module.im.service.websocket.notification.message;
2 2  
3 3 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
4 4 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImChannelMessageDO;
5   -import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
6 5 import lombok.Data;
7 6 import lombok.experimental.Accessors;
8 7  
9 8 import java.time.LocalDateTime;
10 9  
11 10 /**
12   - * IM 频道消息 WebSocket 推送 DTO
  11 + * IM 频道消息 WebSocket 推送通知
13 12 * <p>
14 13 * 单向:服务端运营推送 → C 端用户;C 端不能向频道发消息。
15 14 * 字段分层:顶层是消息元数据 + 检索维度,content 是 MaterialMessage payload 的 JSON 串。
... ... @@ -18,9 +17,7 @@ import java.time.LocalDateTime;
18 17 */
19 18 @Data
20 19 @Accessors(chain = true)
21   -public class ImChannelMessageDTO {
22   -
23   - public static final String TYPE = "im-channel-message";
  20 +public class ImChannelMessageNotification {
24 21  
25 22 /**
26 23 * 消息编号
... ... @@ -48,23 +45,10 @@ public class ImChannelMessageDTO {
48 45 private LocalDateTime sendTime;
49 46  
50 47 /**
51   - * 由频道消息 DO 构建推送 DTO
52   - */
53   - public static ImChannelMessageDTO ofSend(ImChannelMessageDO message) {
54   - return BeanUtils.toBean(message, ImChannelMessageDTO.class);
55   - }
56   -
57   - /**
58   - * 构建已读同步 DTO;多端同步:通知自己的其他终端「我已经读了某频道」
59   - *
60   - * @param channelId 频道编号
61   - * @param readId 已读位置(最大已读消息编号)
62   - * @return 频道 DTO
  48 + * 由频道消息 DO 构建推送通知
63 49 */
64   - public static ImChannelMessageDTO ofRead(Long channelId, Long readId) {
65   - return new ImChannelMessageDTO()
66   - .setId(readId).setType(ImMessageTypeEnum.READ.getType())
67   - .setChannelId(channelId);
  50 + public static ImChannelMessageNotification ofSend(ImChannelMessageDO message) {
  51 + return BeanUtils.toBean(message, ImChannelMessageNotification.class);
68 52 }
69 53  
70 54 }
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/ImGroupMessageDTO.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/notification/message/ImGroupMessageNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto;
  1 +package cn.iocoder.yudao.module.im.service.websocket.notification.message;
2 2  
3 3 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
4 4 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO;
5   -import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
6 5 import lombok.Data;
7 6  
8 7 import java.time.LocalDateTime;
9 8 import java.util.List;
10 9  
11 10 /**
12   - * IM 群聊消息 WebSocket 统一推送 DTO
13   - * <p>
14   - * 发送、已读、回执都通过 {@link #type} 字段区分,
15   - * 并统一复用 {@link #id} 字段表达目标消息或已读位置。
  11 + * IM 群聊消息 WebSocket 统一推送通知
16 12 *
17 13 * @author 芋道源码
18 14 */
19 15 @Data
20   -public class ImGroupMessageDTO {
21   -
22   - public static final String TYPE = "im-group-message";
  16 +public class ImGroupMessageNotification {
23 17  
24 18 /**
25 19 * 消息编号
26   - * <p>
27   - * 普通消息:当前消息 id
28   - * READ:无(群已读不需要)
29   - * RECEIPT:需要回执的目标消息 id
30 20 */
31 21 private Long id;
32 22 /**
... ... @@ -54,6 +44,14 @@ public class ImGroupMessageDTO {
54 44 */
55 45 private Integer status;
56 46 /**
  47 + * 回执状态
  48 + */
  49 + private Integer receiptStatus;
  50 + /**
  51 + * 已读人数
  52 + */
  53 + private Integer readCount;
  54 + /**
57 55 * 发送时间
58 56 */
59 57 private LocalDateTime sendTime;
... ... @@ -66,58 +64,14 @@ public class ImGroupMessageDTO {
66 64 */
67 65 private List<Long> receiverUserIds;
68 66 /**
69   - * 群回执状态
70   - */
71   - private Integer receiptStatus;
72   - /**
73   - * 群回执已读人数
74   - */
75   - private Integer readCount;
76   - /**
77   - * 已读位置
78   - */
79   - private Long readId;
80   -
81   - // ========== 静态工厂方法 ==========
82   -
83   - /**
84   - * 构建发送消息 DTO
  67 + * 构建发送消息通知
85 68 *
86 69 * @param message 群聊消息 DO
87   - * @return 群聊 DTO
88   - */
89   - public static ImGroupMessageDTO ofSend(ImGroupMessageDO message) {
90   - return BeanUtils.toBean(message, ImGroupMessageDTO.class).setReceiverUserIds(null);
91   - }
92   -
93   - /**
94   - * 构建已读同步 DTO(多端同步:通知自己的其他终端"我已经读了某个群")
95   - *
96   - * @param senderId 当前用户编号
97   - * @param groupId 群编号
98   - * @param readId 已读位置(最大已读消息编号)
99   - * @return 群聊 DTO
100   - */
101   - public static ImGroupMessageDTO ofRead(Long senderId, Long groupId, Long readId) {
102   - return new ImGroupMessageDTO()
103   - .setId(readId).setReadId(readId).setType(ImMessageTypeEnum.READ.getType())
104   - .setSenderId(senderId).setGroupId(groupId);
105   - }
106   -
107   - /**
108   - * 构建群回执 DTO(广播回执状态和已读人数)
109   - *
110   - * @param messageId 消息编号
111   - * @param groupId 群编号
112   - * @param readCount 已读人数
113   - * @param receiptStatus 回执状态
114   - * @return 群聊 DTO
  70 + * @return 群聊通知
115 71 */
116   - public static ImGroupMessageDTO ofReceipt(Long messageId, Long groupId,
117   - Integer readCount, Integer receiptStatus) {
118   - return new ImGroupMessageDTO()
119   - .setId(messageId).setType(ImMessageTypeEnum.RECEIPT.getType())
120   - .setGroupId(groupId).setReadCount(readCount).setReceiptStatus(receiptStatus);
  72 + public static ImGroupMessageNotification ofSend(ImGroupMessageDO message) {
  73 + return BeanUtils.toBean(message, ImGroupMessageNotification.class)
  74 + .setReadCount(message.getReadCount() != null ? message.getReadCount() : 0);
121 75 }
122 76  
123 77 }
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/notification/message/ImMessageReadNotification.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.service.websocket.notification.message;
  2 +
  3 +import lombok.Data;
  4 +import lombok.experimental.Accessors;
  5 +
  6 +/**
  7 + * IM 消息已读同步通知
  8 + *
  9 + * @author 芋道源码
  10 + */
  11 +@Data
  12 +@Accessors(chain = true)
  13 +public class ImMessageReadNotification {
  14 +
  15 + /**
  16 + * 已读位置(最大已读消息编号)
  17 + */
  18 + private Long id;
  19 + /**
  20 + * 发送人编号
  21 + */
  22 + private Long senderId;
  23 + /**
  24 + * 私聊接收人编号
  25 + */
  26 + private Long receiverId;
  27 + /**
  28 + * 群编号
  29 + */
  30 + private Long groupId;
  31 + /**
  32 + * 频道编号
  33 + */
  34 + private Long channelId;
  35 + /**
  36 + * 已读位置(最大已读消息编号)
  37 + */
  38 + private Long readId;
  39 +
  40 + public static ImMessageReadNotification ofPrivate(Long senderId, Long receiverId, Long readId) {
  41 + return new ImMessageReadNotification()
  42 + .setId(readId).setReadId(readId)
  43 + .setSenderId(senderId).setReceiverId(receiverId);
  44 + }
  45 +
  46 + public static ImMessageReadNotification ofGroup(Long senderId, Long groupId, Long readId) {
  47 + return new ImMessageReadNotification()
  48 + .setId(readId).setReadId(readId)
  49 + .setSenderId(senderId).setGroupId(groupId);
  50 + }
  51 +
  52 + public static ImMessageReadNotification ofChannel(Long channelId, Long readId) {
  53 + return new ImMessageReadNotification()
  54 + .setId(readId).setReadId(readId)
  55 + .setChannelId(channelId);
  56 + }
  57 +
  58 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/notification/message/ImMessageReceiptNotification.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.service.websocket.notification.message;
  2 +
  3 +import lombok.Data;
  4 +import lombok.experimental.Accessors;
  5 +
  6 +/**
  7 + * IM 消息回执通知
  8 + *
  9 + * @author 芋道源码
  10 + */
  11 +@Data
  12 +@Accessors(chain = true)
  13 +public class ImMessageReceiptNotification {
  14 +
  15 + /**
  16 + * 消息编号
  17 + */
  18 + private Long id;
  19 + /**
  20 + * 已读方的用户编号
  21 + */
  22 + private Long senderId;
  23 + /**
  24 + * 私聊接收人编号
  25 + */
  26 + private Long receiverId;
  27 + /**
  28 + * 群编号
  29 + */
  30 + private Long groupId;
  31 + /**
  32 + * 已读人数
  33 + */
  34 + private Integer readCount;
  35 + /**
  36 + * 回执状态
  37 + */
  38 + private Integer receiptStatus;
  39 +
  40 + public static ImMessageReceiptNotification ofPrivate(Long senderId, Long receiverId, Long readId) {
  41 + return new ImMessageReceiptNotification()
  42 + .setId(readId).setSenderId(senderId).setReceiverId(receiverId);
  43 + }
  44 +
  45 + public static ImMessageReceiptNotification ofGroup(Long messageId, Long groupId,
  46 + Integer readCount, Integer receiptStatus) {
  47 + return new ImMessageReceiptNotification()
  48 + .setId(messageId).setGroupId(groupId)
  49 + .setReadCount(readCount).setReceiptStatus(receiptStatus);
  50 + }
  51 +
  52 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/notification/message/ImPrivateMessageNotification.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.service.websocket.notification.message;
  2 +
  3 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
  4 +import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO;
  5 +import lombok.Data;
  6 +import lombok.experimental.Accessors;
  7 +
  8 +import java.time.LocalDateTime;
  9 +
  10 +/**
  11 + * IM 私聊消息 WebSocket 统一推送通知
  12 + *
  13 + * @author 芋道源码
  14 + */
  15 +@Data
  16 +@Accessors(chain = true)
  17 +public class ImPrivateMessageNotification {
  18 +
  19 + /**
  20 + * 消息编号
  21 + */
  22 + private Long id;
  23 + /**
  24 + * 客户端消息编号
  25 + */
  26 + private String clientMessageId;
  27 + /**
  28 + * 发送人编号
  29 + */
  30 + private Long senderId;
  31 + /**
  32 + * 接收人编号
  33 + */
  34 + private Long receiverId;
  35 + /**
  36 + * 消息类型
  37 + */
  38 + private Integer type;
  39 + /**
  40 + * 消息内容
  41 + */
  42 + private String content;
  43 + /**
  44 + * 消息状态
  45 + */
  46 + private Integer status;
  47 + /**
  48 + * 回执状态
  49 + */
  50 + private Integer receiptStatus;
  51 + /**
  52 + * 发送时间
  53 + */
  54 + private LocalDateTime sendTime;
  55 +
  56 + // ========== 静态工厂方法 ==========
  57 +
  58 + /**
  59 + * 构建发送消息通知
  60 + *
  61 + * @param message 私聊消息 DO
  62 + * @return 私聊通知
  63 + */
  64 + public static ImPrivateMessageNotification ofSend(ImPrivateMessageDO message) {
  65 + return BeanUtils.toBean(message, ImPrivateMessageNotification.class);
  66 + }
  67 +
  68 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/notification/package-info.java 0 → 100644
  1 +/**
  2 + * IM WebSocket 在线通知对象。
  3 + *
  4 + * <p>
  5 + * 本包只放 WebSocket 外层通知对象,固定使用 im-notification 作为推送 type。
  6 + * 具体业务 payload 放在子包中:
  7 + * <ul>
  8 + * <li>{@code message}:私聊、群聊、频道消息通知,以及消息内容结构;</li>
  9 + * <li>{@code friend}:好友关系和好友申请通知;</li>
  10 + * <li>{@code group}:群资料、群成员、加群申请通知;</li>
  11 + * <li>{@code rtc}:实时通话通知。</li>
  12 + * </ul>
  13 + */
  14 +package cn.iocoder.yudao.module.im.service.websocket.notification;
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/rtc/ImRtcCallEndNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/notification/rtc/ImRtcCallEndNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification.rtc;
  1 +package cn.iocoder.yudao.module.im.service.websocket.notification.rtc;
2 2  
3 3 import cn.iocoder.yudao.module.im.dal.dataobject.rtc.ImRtcCallDO;
4 4 import cn.iocoder.yudao.module.im.enums.rtc.ImRtcCallEndReasonEnum;
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/rtc/ImRtcCallNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/notification/rtc/ImRtcCallNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification.rtc;
  1 +package cn.iocoder.yudao.module.im.service.websocket.notification.rtc;
2 2  
3 3 import cn.iocoder.yudao.module.im.dal.dataobject.rtc.ImRtcCallDO;
4 4 import cn.iocoder.yudao.module.im.enums.rtc.ImRtcParticipantStatusEnum;
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/rtc/ImRtcCallStartNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/notification/rtc/ImRtcCallStartNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification.rtc;
  1 +package cn.iocoder.yudao.module.im.service.websocket.notification.rtc;
2 2  
3 3 import cn.iocoder.yudao.module.im.dal.dataobject.rtc.ImRtcCallDO;
4 4 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/rtc/ImRtcParticipantConnectedNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/notification/rtc/ImRtcParticipantConnectedNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification.rtc;
  1 +package cn.iocoder.yudao.module.im.service.websocket.notification.rtc;
2 2  
3 3 import cn.iocoder.yudao.module.im.dal.dataobject.rtc.ImRtcCallDO;
4 4 import lombok.Data;
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/rtc/ImRtcParticipantDisconnectedNotification.java renamed to yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/notification/rtc/ImRtcParticipantDisconnectedNotification.java
1   -package cn.iocoder.yudao.module.im.service.websocket.dto.notification.rtc;
  1 +package cn.iocoder.yudao.module.im.service.websocket.notification.rtc;
2 2  
3 3 import cn.iocoder.yudao.module.im.dal.dataobject.rtc.ImRtcCallDO;
4 4 import lombok.Data;
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/util/ImMessageUtils.java
... ... @@ -4,8 +4,8 @@ import cn.hutool.core.collection.CollUtil;
4 4 import cn.hutool.core.convert.Convert;
5 5 import cn.hutool.core.util.StrUtil;
6 6 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
7   -import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
8   -import cn.iocoder.yudao.module.im.service.websocket.dto.message.QuoteMessage;
  7 +import cn.iocoder.yudao.module.im.enums.ImContentTypeEnum;
  8 +import cn.iocoder.yudao.module.im.dal.dataobject.message.content.QuoteMessage;
9 9  
10 10 import java.util.Collection;
11 11 import java.util.LinkedHashMap;
... ... @@ -39,9 +39,9 @@ public class ImMessageUtils {
39 39 * @param content 消息内容
40 40 */
41 41 public static void validateUserMessageContent(Integer type, String content) {
42   - ImMessageTypeEnum messageType;
  42 + ImContentTypeEnum messageType;
43 43 try {
44   - messageType = ImMessageTypeEnum.validate(type);
  44 + messageType = ImContentTypeEnum.validate(type);
45 45 } catch (IllegalArgumentException e) {
46 46 throw exception(MESSAGE_CONTENT_INVALID);
47 47 }
... ... @@ -52,23 +52,23 @@ public class ImMessageUtils {
52 52 if (map == null) {
53 53 throw exception(MESSAGE_CONTENT_INVALID);
54 54 }
55   - if (ImMessageTypeEnum.TEXT == messageType) {
  55 + if (ImContentTypeEnum.TEXT == messageType) {
56 56 validateTextContent(map);
57   - } else if (ImMessageTypeEnum.IMAGE == messageType || ImMessageTypeEnum.FACE == messageType) {
  57 + } else if (ImContentTypeEnum.IMAGE == messageType || ImContentTypeEnum.FACE == messageType) {
58 58 validateUrl(getString(map, "url"));
59   - } else if (ImMessageTypeEnum.VOICE == messageType) {
  59 + } else if (ImContentTypeEnum.VOICE == messageType) {
60 60 validateUrl(getString(map, "url"));
61 61 validatePositiveNumber(map.get("duration"));
62   - } else if (ImMessageTypeEnum.VIDEO == messageType) {
  62 + } else if (ImContentTypeEnum.VIDEO == messageType) {
63 63 validateUrl(getString(map, "url"));
64   - } else if (ImMessageTypeEnum.FILE == messageType) {
  64 + } else if (ImContentTypeEnum.FILE == messageType) {
65 65 validateUrl(getString(map, "url"));
66 66 validateNotBlank(getString(map, "name"));
67   - } else if (ImMessageTypeEnum.CARD == messageType) {
  67 + } else if (ImContentTypeEnum.CARD == messageType) {
68 68 validateCardContent(map);
69   - } else if (ImMessageTypeEnum.MERGE == messageType) {
  69 + } else if (ImContentTypeEnum.MERGE == messageType) {
70 70 validateMergeContent(map);
71   - } else if (ImMessageTypeEnum.MATERIAL == messageType) {
  71 + } else if (ImContentTypeEnum.MATERIAL == messageType) {
72 72 validateMaterialContent(map);
73 73 }
74 74 }
... ...
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/dal/mysql/conversation/ImConversationReadMapperTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.dal.mysql.conversation;
  2 +
  3 +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
  4 +import cn.iocoder.yudao.module.im.dal.dataobject.conversation.ImConversationReadDO;
  5 +import cn.iocoder.yudao.module.im.enums.ImConversationTypeEnum;
  6 +import jakarta.annotation.Resource;
  7 +import org.junit.jupiter.api.Test;
  8 +
  9 +import java.time.LocalDateTime;
  10 +import java.util.List;
  11 +import java.util.Map;
  12 +
  13 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
  14 +import static org.junit.jupiter.api.Assertions.*;
  15 +
  16 +/**
  17 + * {@link ImConversationReadMapper} 的单元测试
  18 + *
  19 + * @author 芋道源码
  20 + */
  21 +public class ImConversationReadMapperTest extends BaseDbUnitTest {
  22 +
  23 + @Resource
  24 + private ImConversationReadMapper mapper;
  25 +
  26 + @Test
  27 + public void testUpdateReadMessageIdToLarger_onlyAdvancesWhenLarger() {
  28 + // 准备:用户 1 在群 10 的读位置 = 50
  29 + ImConversationReadDO read = buildRead(1L, ImConversationTypeEnum.GROUP.getType(), 10L, 50L);
  30 + mapper.insert(read);
  31 +
  32 + // 1. 新位置更大 → 前进
  33 + int advanced = mapper.updateReadMessageIdToLarger(read.getId(), 100L, LocalDateTime.now());
  34 + assertEquals(1, advanced);
  35 + assertEquals(100L, mapper.selectById(read.getId()).getMessageId());
  36 +
  37 + // 2. 新位置相等 → 不更新
  38 + int equalUpdate = mapper.updateReadMessageIdToLarger(read.getId(), 100L, LocalDateTime.now());
  39 + assertEquals(0, equalUpdate);
  40 +
  41 + // 3. 新位置更小(乱序上报)→ 不回退
  42 + int smaller = mapper.updateReadMessageIdToLarger(read.getId(), 80L, LocalDateTime.now());
  43 + assertEquals(0, smaller);
  44 + assertEquals(100L, mapper.selectById(read.getId()).getMessageId());
  45 + }
  46 +
  47 + @Test
  48 + public void testSelectByUserIdAndConversation() {
  49 + ImConversationReadDO read = buildRead(1L, ImConversationTypeEnum.GROUP.getType(), 10L, 50L);
  50 + mapper.insert(read);
  51 + // 同用户不同会话类型,不应命中
  52 + mapper.insert(buildRead(1L, ImConversationTypeEnum.PRIVATE.getType(), 10L, 99L));
  53 +
  54 + ImConversationReadDO result = mapper.selectByUserIdAndConversation(
  55 + 1L, ImConversationTypeEnum.GROUP.getType(), 10L);
  56 +
  57 + assertNotNull(result);
  58 + assertEquals(50L, result.getMessageId());
  59 + }
  60 +
  61 + @Test
  62 + public void testSelectListByConversation_aggregatesAllUsers() {
  63 + // 群 10 内三个用户的读位置,用于群回执人数聚合
  64 + mapper.insert(buildRead(1L, ImConversationTypeEnum.GROUP.getType(), 10L, 100L));
  65 + mapper.insert(buildRead(2L, ImConversationTypeEnum.GROUP.getType(), 10L, 80L));
  66 + mapper.insert(buildRead(3L, ImConversationTypeEnum.GROUP.getType(), 10L, 50L));
  67 + // 别的群,不应混入
  68 + mapper.insert(buildRead(1L, ImConversationTypeEnum.GROUP.getType(), 20L, 999L));
  69 +
  70 + List<ImConversationReadDO> list = mapper.selectListByConversation(
  71 + ImConversationTypeEnum.GROUP.getType(), 10L);
  72 + Map<Long, Long> positions = convertMap(list,
  73 + ImConversationReadDO::getUserId, ImConversationReadDO::getMessageId);
  74 +
  75 + assertEquals(3, positions.size());
  76 + assertEquals(100L, positions.get(1L));
  77 + assertEquals(80L, positions.get(2L));
  78 + assertEquals(50L, positions.get(3L));
  79 + }
  80 +
  81 + @Test
  82 + public void testSelectListByUserIdAndConversations_batch() {
  83 + // 用户 1 在频道 10 / 20 / 30 的读位置
  84 + mapper.insert(buildRead(1L, ImConversationTypeEnum.CHANNEL.getType(), 10L, 5L));
  85 + mapper.insert(buildRead(1L, ImConversationTypeEnum.CHANNEL.getType(), 20L, 8L));
  86 + mapper.insert(buildRead(1L, ImConversationTypeEnum.CHANNEL.getType(), 30L, 3L));
  87 +
  88 + List<ImConversationReadDO> list = mapper.selectListByUserIdAndConversations(
  89 + 1L, ImConversationTypeEnum.CHANNEL.getType(), List.of(10L, 20L));
  90 +
  91 + assertEquals(2, list.size());
  92 + }
  93 +
  94 + private ImConversationReadDO buildRead(Long userId, Integer conversationType, Long conversationId,
  95 + Long readMessageId) {
  96 + return ImConversationReadDO.builder()
  97 + .userId(userId)
  98 + .conversationType(conversationType)
  99 + .targetId(conversationId)
  100 + .messageId(readMessageId)
  101 + .readTime(LocalDateTime.now())
  102 + .build();
  103 + }
  104 +
  105 +}
... ...
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/dal/mysql/friend/ImFriendRequestMapperTest.java
... ... @@ -11,7 +11,7 @@ import java.util.List;
11 11 import static org.junit.jupiter.api.Assertions.*;
12 12  
13 13 /**
14   - * IM 好友申请记录 Mapper 单元测试
  14 + * {@link ImFriendRequestMapper} 的单元测试
15 15 *
16 16 * @author 芋道源码
17 17 */
... ...
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/dal/mysql/message/ImGroupMessageMapperTest.java
1 1 package cn.iocoder.yudao.module.im.dal.mysql.message;
2 2  
3   -import cn.hutool.core.collection.ListUtil;
4 3 import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
5 4 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO;
6   -import cn.iocoder.yudao.module.im.enums.message.ImGroupMessageReceiptStatusEnum;
  5 +import cn.iocoder.yudao.module.im.enums.message.ImMessageReceiptStatusEnum;
7 6 import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum;
8 7 import jakarta.annotation.Resource;
9 8 import org.junit.jupiter.api.Test;
... ... @@ -14,7 +13,7 @@ import java.util.List;
14 13 import static org.junit.jupiter.api.Assertions.*;
15 14  
16 15 /**
17   - * IM 群聊消息 Mapper 单元测试
  16 + * {@link ImGroupMessageMapper} 的单元测试
18 17 *
19 18 * @author 芋道源码
20 19 */
... ... @@ -28,46 +27,76 @@ public class ImGroupMessageMapperTest extends BaseDbUnitTest {
28 27 // ========== selectListByMinId ==========
29 28  
30 29 @Test
  30 + public void testSelectListByMinId_filterByReceiverSnapshot() {
  31 + // 准备:群 10 三条消息,快照分别命中 / 不命中用户 1
  32 + ImGroupMessageDO visible = buildMessage(10L, 2L, ImMessageStatusEnum.NORMAL, List.of(1L, 2L));
  33 + mapper.insert(visible);
  34 + ImGroupMessageDO invisible = buildMessage(10L, 2L, ImMessageStatusEnum.NORMAL, List.of(2L, 3L));
  35 + mapper.insert(invisible);
  36 + ImGroupMessageDO selfSend = buildMessage(10L, 1L, ImMessageStatusEnum.NORMAL, List.of(1L, 2L));
  37 + mapper.insert(selfSend);
  38 +
  39 + // 调用:用户 1 拉取群 10
  40 + List<ImGroupMessageDO> result = mapper.selectListByMinId(1L, List.of(10L), 0L, FAR_PAST, 100);
  41 +
  42 + // 断言:仅返回快照包含用户 1 的消息
  43 + assertEquals(2, result.size());
  44 + assertEquals(visible.getId(), result.get(0).getId());
  45 + assertEquals(selfSend.getId(), result.get(1).getId());
  46 + }
  47 +
  48 + @Test
  49 + public void testSelectListByMinId_excludeNullAndEmptySnapshot() {
  50 + // 准备:快照为 null / 空字符串的老数据,不应被任何用户拉到
  51 + ImGroupMessageDO nullSnapshot = buildMessage(10L, 2L, ImMessageStatusEnum.NORMAL, null);
  52 + mapper.insert(nullSnapshot);
  53 + ImGroupMessageDO emptySnapshot = buildMessage(10L, 2L, ImMessageStatusEnum.NORMAL, List.of());
  54 + mapper.insert(emptySnapshot);
  55 +
  56 + List<ImGroupMessageDO> result = mapper.selectListByMinId(1L, List.of(10L), 0L, FAR_PAST, 100);
  57 +
  58 + assertTrue(result.isEmpty());
  59 + }
  60 +
  61 + @Test
31 62 public void testSelectListByMinId_onlyReturnsGroupsInIdList() {
32   - // 准备:群 10 的消息 + 群 20 的消息
33   - ImGroupMessageDO msg10 = buildMessage(10L, 1L, ImMessageStatusEnum.UNREAD);
  63 + // 准备:群 10 + 群 20 各一条,用户 1 都在快照内
  64 + ImGroupMessageDO msg10 = buildMessage(10L, 2L, ImMessageStatusEnum.NORMAL, List.of(1L, 2L));
34 65 mapper.insert(msg10);
35   - ImGroupMessageDO msg20 = buildMessage(20L, 1L, ImMessageStatusEnum.UNREAD);
  66 + ImGroupMessageDO msg20 = buildMessage(20L, 2L, ImMessageStatusEnum.NORMAL, List.of(1L, 2L));
36 67 mapper.insert(msg20);
37 68  
38   - // 调用:只查群 10
39   - List<ImGroupMessageDO> result = mapper.selectListByMinId(ListUtil.of(10L), 0L, FAR_PAST, 100);
  69 + // 调用:候选群只含群 10
  70 + List<ImGroupMessageDO> result = mapper.selectListByMinId(1L, List.of(10L), 0L, FAR_PAST, 100);
40 71  
41   - // 断言:只有 msg10
42 72 assertEquals(1, result.size());
43 73 assertEquals(msg10.getId(), result.get(0).getId());
44 74 }
45 75  
46 76 @Test
47 77 public void testSelectListByMinId_includesRecall() {
48   - // 准备:一条正常 + 一条撤回;客户端拉离线消息需要拿到撤回状态以渲染「此消息已撤回」占位
49   - ImGroupMessageDO normal = buildMessage(10L, 1L, ImMessageStatusEnum.UNREAD);
  78 + // 准备:撤回消息也要返回,由客户端按 status 渲染「此消息已撤回」占位
  79 + ImGroupMessageDO normal = buildMessage(10L, 2L, ImMessageStatusEnum.NORMAL, List.of(1L));
50 80 mapper.insert(normal);
51   - ImGroupMessageDO recalled = buildMessage(10L, 1L, ImMessageStatusEnum.RECALL);
  81 + ImGroupMessageDO recalled = buildMessage(10L, 2L, ImMessageStatusEnum.RECALL, List.of(1L));
52 82 mapper.insert(recalled);
53 83  
54   - List<ImGroupMessageDO> result = mapper.selectListByMinId(ListUtil.of(10L), 0L, FAR_PAST, 100);
  84 + List<ImGroupMessageDO> result = mapper.selectListByMinId(1L, List.of(10L), 0L, FAR_PAST, 100);
55 85  
56   - // 断言:撤回消息一并返回,由客户端按 status 切换渲染
57 86 assertEquals(2, result.size());
58 87 }
59 88  
60 89 @Test
61 90 public void testSelectListByMinId_sendTimeWindow() {
62 91 // 准备:一条在窗口内,一条在窗口外
63   - ImGroupMessageDO inWindow = buildMessage(10L, 1L, ImMessageStatusEnum.UNREAD);
  92 + ImGroupMessageDO inWindow = buildMessage(10L, 2L, ImMessageStatusEnum.NORMAL, List.of(1L));
64 93 inWindow.setSendTime(LocalDateTime.now().minusDays(1));
65 94 mapper.insert(inWindow);
66   - ImGroupMessageDO outWindow = buildMessage(10L, 1L, ImMessageStatusEnum.UNREAD);
  95 + ImGroupMessageDO outWindow = buildMessage(10L, 2L, ImMessageStatusEnum.NORMAL, List.of(1L));
67 96 outWindow.setSendTime(LocalDateTime.now().minusDays(40));
68 97 mapper.insert(outWindow);
69 98  
70   - List<ImGroupMessageDO> result = mapper.selectListByMinId(ListUtil.of(10L), 0L,
  99 + List<ImGroupMessageDO> result = mapper.selectListByMinId(1L, List.of(10L), 0L,
71 100 LocalDateTime.now().minusDays(30), 100);
72 101  
73 102 assertEquals(1, result.size());
... ... @@ -76,16 +105,16 @@ public class ImGroupMessageMapperTest extends BaseDbUnitTest {
76 105  
77 106 @Test
78 107 public void testSelectListByMinId_sortAscLimit() {
79   - // 准备:插入 3 条
80   - ImGroupMessageDO m1 = buildMessage(10L, 1L, ImMessageStatusEnum.UNREAD);
  108 + // 准备:插入 3 条可见消息
  109 + ImGroupMessageDO m1 = buildMessage(10L, 2L, ImMessageStatusEnum.NORMAL, List.of(1L));
81 110 mapper.insert(m1);
82   - ImGroupMessageDO m2 = buildMessage(10L, 1L, ImMessageStatusEnum.UNREAD);
  111 + ImGroupMessageDO m2 = buildMessage(10L, 2L, ImMessageStatusEnum.NORMAL, List.of(1L));
83 112 mapper.insert(m2);
84   - ImGroupMessageDO m3 = buildMessage(10L, 1L, ImMessageStatusEnum.UNREAD);
  113 + ImGroupMessageDO m3 = buildMessage(10L, 2L, ImMessageStatusEnum.NORMAL, List.of(1L));
85 114 mapper.insert(m3);
86 115  
87 116 // 调用:size=2,返回 id 最小的 2 条
88   - List<ImGroupMessageDO> result = mapper.selectListByMinId(ListUtil.of(10L), 0L, FAR_PAST, 2);
  117 + List<ImGroupMessageDO> result = mapper.selectListByMinId(1L, List.of(10L), 0L, FAR_PAST, 2);
89 118  
90 119 assertEquals(2, result.size());
91 120 assertTrue(result.get(0).getId() < result.get(1).getId());
... ... @@ -93,129 +122,63 @@ public class ImGroupMessageMapperTest extends BaseDbUnitTest {
93 122 assertEquals(m2.getId(), result.get(1).getId());
94 123 }
95 124  
96   - // ========== selectListByGroupIdAndMinIdAndQuitTimeBefore ==========
97   -
98   - @Test
99   - public void testSelectListByGroupIdAndMinIdAndQuitTimeBefore_onlyReturnsBeforeQuit() {
100   - // 准备:退群时间 = now-1 day,插入一条"退群前",一条"退群后"
101   - LocalDateTime quitTime = LocalDateTime.now().minusDays(1);
102   - ImGroupMessageDO before = buildMessage(10L, 1L, ImMessageStatusEnum.UNREAD);
103   - before.setSendTime(LocalDateTime.now().minusDays(2));
104   - mapper.insert(before);
105   - ImGroupMessageDO after = buildMessage(10L, 1L, ImMessageStatusEnum.UNREAD);
106   - after.setSendTime(LocalDateTime.now().minusHours(1));
107   - mapper.insert(after);
108   -
109   - List<ImGroupMessageDO> result = mapper.selectListByGroupIdAndMinIdAndQuitTimeBefore(
110   - 10L, 0L, FAR_PAST, quitTime, 100);
111   -
112   - // 断言:只返回退群前消息
113   - assertEquals(1, result.size());
114   - assertEquals(before.getId(), result.get(0).getId());
115   - }
116   -
117 125 @Test
118   - public void testSelectListByGroupIdAndMinIdAndQuitTimeBefore_boundaryEqualsQuitTime() {
119   - // 准备:消息发送时间恰好等于退群时间(le 语义应包含该条)
120   - LocalDateTime quitTime = LocalDateTime.now().minusDays(1).withNano(0);
121   - ImGroupMessageDO boundary = buildMessage(10L, 1L, ImMessageStatusEnum.UNREAD);
122   - boundary.setSendTime(quitTime);
123   - mapper.insert(boundary);
124   -
125   - List<ImGroupMessageDO> result = mapper.selectListByGroupIdAndMinIdAndQuitTimeBefore(
126   - 10L, 0L, FAR_PAST, quitTime, 100);
127   -
128   - assertEquals(1, result.size());
129   - }
  126 + public void testSelectListByMinId_minIdExclusive() {
  127 + // 准备:游标之前的消息不返回
  128 + ImGroupMessageDO m1 = buildMessage(10L, 2L, ImMessageStatusEnum.NORMAL, List.of(1L));
  129 + mapper.insert(m1);
  130 + ImGroupMessageDO m2 = buildMessage(10L, 2L, ImMessageStatusEnum.NORMAL, List.of(1L));
  131 + mapper.insert(m2);
130 132  
131   - @Test
132   - public void testSelectListByGroupIdAndMinIdAndQuitTimeBefore_otherGroupExcluded() {
133   - // 准备:群 10 和群 20 各一条消息
134   - LocalDateTime quitTime = LocalDateTime.now();
135   - ImGroupMessageDO in10 = buildMessage(10L, 1L, ImMessageStatusEnum.UNREAD);
136   - in10.setSendTime(LocalDateTime.now().minusHours(2));
137   - mapper.insert(in10);
138   - ImGroupMessageDO in20 = buildMessage(20L, 1L, ImMessageStatusEnum.UNREAD);
139   - in20.setSendTime(LocalDateTime.now().minusHours(2));
140   - mapper.insert(in20);
141   -
142   - List<ImGroupMessageDO> result = mapper.selectListByGroupIdAndMinIdAndQuitTimeBefore(
143   - 10L, 0L, FAR_PAST, quitTime, 100);
  133 + List<ImGroupMessageDO> result = mapper.selectListByMinId(1L, List.of(10L), m1.getId(), FAR_PAST, 100);
144 134  
145 135 assertEquals(1, result.size());
146   - assertEquals(in10.getId(), result.get(0).getId());
  136 + assertEquals(m2.getId(), result.get(0).getId());
147 137 }
148 138  
149   - // ========== selectHistoryList ==========
  139 + // ========== selectHistoryListByUser ==========
150 140  
151 141 @Test
152   - public void testSelectHistoryList_basic() {
153   - ImGroupMessageDO m1 = buildMessage(10L, 1L, ImMessageStatusEnum.UNREAD);
  142 + public void testSelectHistoryListByUser_filterByReceiverSnapshot() {
  143 + // 准备:群 10 三条,用户 1 在前两条快照内、第三条定向给别人
  144 + ImGroupMessageDO m1 = buildMessage(10L, 2L, ImMessageStatusEnum.NORMAL, List.of(1L, 2L));
154 145 mapper.insert(m1);
155   - ImGroupMessageDO m2 = buildMessage(10L, 2L, ImMessageStatusEnum.UNREAD);
  146 + ImGroupMessageDO m2 = buildMessage(10L, 2L, ImMessageStatusEnum.NORMAL, List.of(1L, 2L));
156 147 mapper.insert(m2);
  148 + ImGroupMessageDO directedOther = buildMessage(10L, 2L, ImMessageStatusEnum.NORMAL, List.of(2L, 3L));
  149 + mapper.insert(directedOther);
157 150 // 别的群
158   - ImGroupMessageDO other = buildMessage(20L, 1L, ImMessageStatusEnum.UNREAD);
  151 + ImGroupMessageDO other = buildMessage(20L, 2L, ImMessageStatusEnum.NORMAL, List.of(1L));
159 152 mapper.insert(other);
160 153  
161   - List<ImGroupMessageDO> result = mapper.selectHistoryList(10L, null, 100, null);
162   -
163   - // 断言:只返回 10 群的,按 id 倒序
164   - assertEquals(2, result.size());
165   - assertTrue(result.get(0).getId() > result.get(1).getId());
166   - }
167   -
168   - @Test
169   - public void testSelectHistoryList_includeRecall() {
170   - ImGroupMessageDO normal = buildMessage(10L, 1L, ImMessageStatusEnum.UNREAD);
171   - mapper.insert(normal);
172   - ImGroupMessageDO recalled = buildMessage(10L, 1L, ImMessageStatusEnum.RECALL);
173   - mapper.insert(recalled);
174   -
175   - List<ImGroupMessageDO> result = mapper.selectHistoryList(10L, null, 100, null);
  154 + List<ImGroupMessageDO> result = mapper.selectHistoryListByUser(1L, 10L, null, 100);
176 155  
  156 + // 断言:只返回群 10 中用户 1 可见的,按 id 倒序
177 157 assertEquals(2, result.size());
178   - assertEquals(recalled.getId(), result.get(0).getId());
179   - assertEquals(normal.getId(), result.get(1).getId());
  158 + assertEquals(m2.getId(), result.get(0).getId());
  159 + assertEquals(m1.getId(), result.get(1).getId());
180 160 }
181 161  
182 162 @Test
183   - public void testSelectHistoryList_maxIdCursor() {
184   - ImGroupMessageDO m1 = buildMessage(10L, 1L, ImMessageStatusEnum.UNREAD);
  163 + public void testSelectHistoryListByUser_maxIdCursor() {
  164 + ImGroupMessageDO m1 = buildMessage(10L, 2L, ImMessageStatusEnum.NORMAL, List.of(1L));
185 165 mapper.insert(m1);
186   - ImGroupMessageDO m2 = buildMessage(10L, 1L, ImMessageStatusEnum.UNREAD);
  166 + ImGroupMessageDO m2 = buildMessage(10L, 2L, ImMessageStatusEnum.NORMAL, List.of(1L));
187 167 mapper.insert(m2);
188   - ImGroupMessageDO m3 = buildMessage(10L, 1L, ImMessageStatusEnum.UNREAD);
  168 + ImGroupMessageDO m3 = buildMessage(10L, 2L, ImMessageStatusEnum.NORMAL, List.of(1L));
189 169 mapper.insert(m3);
190 170  
191   - List<ImGroupMessageDO> result = mapper.selectHistoryList(10L, m3.getId(), 100, null);
  171 + List<ImGroupMessageDO> result = mapper.selectHistoryListByUser(1L, 10L, m3.getId(), 100);
192 172  
193   - // 断言:只返回 id < m3.id 的,即 m1、m2
  173 + // 断言:只返回 id < m3.id 的 m1、m2
194 174 assertEquals(2, result.size());
195 175 assertTrue(result.stream().allMatch(m -> m.getId() < m3.getId()));
196 176 }
197 177  
198   - @Test
199   - public void testSelectHistoryList_filterByJoinTime() {
200   - // 准备:一条在入群前,一条在入群后
201   - LocalDateTime joinTime = LocalDateTime.now().minusHours(1);
202   - ImGroupMessageDO before = buildMessage(10L, 1L, ImMessageStatusEnum.UNREAD);
203   - before.setSendTime(LocalDateTime.now().minusHours(2));
204   - mapper.insert(before);
205   - ImGroupMessageDO after = buildMessage(10L, 1L, ImMessageStatusEnum.UNREAD);
206   - after.setSendTime(LocalDateTime.now().minusMinutes(30));
207   - mapper.insert(after);
208   -
209   - List<ImGroupMessageDO> result = mapper.selectHistoryList(10L, null, 100, joinTime);
210   -
211   - // 断言:入群前消息不可见
212   - assertEquals(1, result.size());
213   - assertEquals(after.getId(), result.get(0).getId());
214   - }
215   -
216 178 // ========== 工具方法 ==========
217 179  
218   - private ImGroupMessageDO buildMessage(Long groupId, Long senderId, ImMessageStatusEnum status) {
  180 + private ImGroupMessageDO buildMessage(Long groupId, Long senderId, ImMessageStatusEnum status,
  181 + List<Long> receiverUserIds) {
219 182 return ImGroupMessageDO.builder()
220 183 .clientMessageId("uuid-" + System.nanoTime())
221 184 .senderId(senderId)
... ... @@ -224,7 +187,8 @@ public class ImGroupMessageMapperTest extends BaseDbUnitTest {
224 187 .content("{\"content\":\"test\"}")
225 188 .status(status.getStatus())
226 189 .sendTime(LocalDateTime.now())
227   - .receiptStatus(ImGroupMessageReceiptStatusEnum.NO_RECEIPT.getStatus())
  190 + .receiverUserIds(receiverUserIds)
  191 + .receiptStatus(ImMessageReceiptStatusEnum.NO_RECEIPT.getStatus())
228 192 .build();
229 193 }
230 194  
... ...
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/dal/mysql/message/ImPrivateMessageMapperTest.java
... ... @@ -12,7 +12,7 @@ import java.util.List;
12 12 import static org.junit.jupiter.api.Assertions.*;
13 13  
14 14 /**
15   - * IM 私聊消息 Mapper 单元测试
  15 + * {@link ImPrivateMessageMapper} 的单元测试
16 16 *
17 17 * @author 芋道源码
18 18 */
... ... @@ -21,9 +21,6 @@ public class ImPrivateMessageMapperTest extends BaseDbUnitTest {
21 21 @Resource
22 22 private ImPrivateMessageMapper mapper;
23 23  
24   - /**
25   - * 一个足够早的时间作为 minSendTime,避免把测试数据过滤掉
26   - */
27 24 private static final LocalDateTime FAR_PAST = LocalDateTime.of(2000, 1, 1, 0, 0, 0);
28 25  
29 26 // ========== selectListByMinId ==========
... ... @@ -31,13 +28,13 @@ public class ImPrivateMessageMapperTest extends BaseDbUnitTest {
31 28 @Test
32 29 public void testSelectListByMinId() {
33 30 // 准备:用户 1 发给用户 2
34   - ImPrivateMessageDO msg1 = buildMessage(1L, 2L, ImMessageStatusEnum.UNREAD);
  31 + ImPrivateMessageDO msg1 = buildMessage(1L, 2L, ImMessageStatusEnum.NORMAL);
35 32 mapper.insert(msg1);
36 33 // 用户 2 发给用户 1
37   - ImPrivateMessageDO msg2 = buildMessage(2L, 1L, ImMessageStatusEnum.UNREAD);
  34 + ImPrivateMessageDO msg2 = buildMessage(2L, 1L, ImMessageStatusEnum.NORMAL);
38 35 mapper.insert(msg2);
39 36 // 用户 3 发给用户 4(不相关)
40   - ImPrivateMessageDO msg3 = buildMessage(3L, 4L, ImMessageStatusEnum.UNREAD);
  37 + ImPrivateMessageDO msg3 = buildMessage(3L, 4L, ImMessageStatusEnum.NORMAL);
41 38 mapper.insert(msg3);
42 39  
43 40 // 调用:用户 1 从 id=0 拉取
... ... @@ -52,9 +49,9 @@ public class ImPrivateMessageMapperTest extends BaseDbUnitTest {
52 49 @Test
53 50 public void testSelectListByMinId_withMinId() {
54 51 // 准备
55   - ImPrivateMessageDO msg1 = buildMessage(1L, 2L, ImMessageStatusEnum.UNREAD);
  52 + ImPrivateMessageDO msg1 = buildMessage(1L, 2L, ImMessageStatusEnum.NORMAL);
56 53 mapper.insert(msg1);
57   - ImPrivateMessageDO msg2 = buildMessage(2L, 1L, ImMessageStatusEnum.UNREAD);
  54 + ImPrivateMessageDO msg2 = buildMessage(2L, 1L, ImMessageStatusEnum.NORMAL);
58 55 mapper.insert(msg2);
59 56  
60 57 // 调用:从 msg1.id 之后拉取
... ... @@ -68,9 +65,9 @@ public class ImPrivateMessageMapperTest extends BaseDbUnitTest {
68 65 @Test
69 66 public void testSelectListByMinId_limitSize() {
70 67 // 准备:插入 3 条
71   - mapper.insert(buildMessage(1L, 2L, ImMessageStatusEnum.UNREAD));
72   - mapper.insert(buildMessage(2L, 1L, ImMessageStatusEnum.UNREAD));
73   - mapper.insert(buildMessage(1L, 2L, ImMessageStatusEnum.UNREAD));
  68 + mapper.insert(buildMessage(1L, 2L, ImMessageStatusEnum.NORMAL));
  69 + mapper.insert(buildMessage(2L, 1L, ImMessageStatusEnum.NORMAL));
  70 + mapper.insert(buildMessage(1L, 2L, ImMessageStatusEnum.NORMAL));
74 71  
75 72 // 调用:limit 2
76 73 List<ImPrivateMessageDO> result = mapper.selectListByMinId(1L, 0L, FAR_PAST, 2);
... ... @@ -82,10 +79,10 @@ public class ImPrivateMessageMapperTest extends BaseDbUnitTest {
82 79 @Test
83 80 public void testSelectListByMinId_sendTimeFilter() {
84 81 // 准备:一条落在窗口内,一条落在窗口外
85   - ImPrivateMessageDO newMsg = buildMessage(1L, 2L, ImMessageStatusEnum.UNREAD);
  82 + ImPrivateMessageDO newMsg = buildMessage(1L, 2L, ImMessageStatusEnum.NORMAL);
86 83 newMsg.setSendTime(LocalDateTime.now().minusDays(1));
87 84 mapper.insert(newMsg);
88   - ImPrivateMessageDO oldMsg = buildMessage(1L, 2L, ImMessageStatusEnum.UNREAD);
  85 + ImPrivateMessageDO oldMsg = buildMessage(1L, 2L, ImMessageStatusEnum.NORMAL);
89 86 oldMsg.setSendTime(LocalDateTime.now().minusDays(40));
90 87 mapper.insert(oldMsg);
91 88  
... ... @@ -103,12 +100,12 @@ public class ImPrivateMessageMapperTest extends BaseDbUnitTest {
103 100 @Test
104 101 public void testSelectHistoryList_basic() {
105 102 // 准备:用户 1 <-> 用户 2 的消息
106   - ImPrivateMessageDO msg1 = buildMessage(1L, 2L, ImMessageStatusEnum.UNREAD);
  103 + ImPrivateMessageDO msg1 = buildMessage(1L, 2L, ImMessageStatusEnum.NORMAL);
107 104 mapper.insert(msg1);
108   - ImPrivateMessageDO msg2 = buildMessage(2L, 1L, ImMessageStatusEnum.UNREAD);
  105 + ImPrivateMessageDO msg2 = buildMessage(2L, 1L, ImMessageStatusEnum.NORMAL);
109 106 mapper.insert(msg2);
110 107 // 用户 1 <-> 用户 3 的消息(不相关)
111   - ImPrivateMessageDO msg3 = buildMessage(1L, 3L, ImMessageStatusEnum.UNREAD);
  108 + ImPrivateMessageDO msg3 = buildMessage(1L, 3L, ImMessageStatusEnum.NORMAL);
112 109 mapper.insert(msg3);
113 110  
114 111 // 调用
... ... @@ -122,7 +119,7 @@ public class ImPrivateMessageMapperTest extends BaseDbUnitTest {
122 119 @Test
123 120 public void testSelectHistoryList_includeRecall() {
124 121 // 准备
125   - ImPrivateMessageDO msg1 = buildMessage(1L, 2L, ImMessageStatusEnum.UNREAD);
  122 + ImPrivateMessageDO msg1 = buildMessage(1L, 2L, ImMessageStatusEnum.NORMAL);
126 123 mapper.insert(msg1);
127 124 ImPrivateMessageDO msg2 = buildMessage(1L, 2L, ImMessageStatusEnum.RECALL);
128 125 mapper.insert(msg2);
... ... @@ -139,11 +136,11 @@ public class ImPrivateMessageMapperTest extends BaseDbUnitTest {
139 136 @Test
140 137 public void testSelectHistoryList_withMaxId() {
141 138 // 准备
142   - ImPrivateMessageDO msg1 = buildMessage(1L, 2L, ImMessageStatusEnum.UNREAD);
  139 + ImPrivateMessageDO msg1 = buildMessage(1L, 2L, ImMessageStatusEnum.NORMAL);
143 140 mapper.insert(msg1);
144   - ImPrivateMessageDO msg2 = buildMessage(1L, 2L, ImMessageStatusEnum.UNREAD);
  141 + ImPrivateMessageDO msg2 = buildMessage(1L, 2L, ImMessageStatusEnum.NORMAL);
145 142 mapper.insert(msg2);
146   - ImPrivateMessageDO msg3 = buildMessage(1L, 2L, ImMessageStatusEnum.UNREAD);
  143 + ImPrivateMessageDO msg3 = buildMessage(1L, 2L, ImMessageStatusEnum.NORMAL);
147 144 mapper.insert(msg3);
148 145  
149 146 // 调用:从 msg3 往前拉
... ... @@ -157,9 +154,9 @@ public class ImPrivateMessageMapperTest extends BaseDbUnitTest {
157 154 @Test
158 155 public void testSelectHistoryList_limit() {
159 156 // 准备
160   - mapper.insert(buildMessage(1L, 2L, ImMessageStatusEnum.UNREAD));
161   - mapper.insert(buildMessage(2L, 1L, ImMessageStatusEnum.UNREAD));
162   - mapper.insert(buildMessage(1L, 2L, ImMessageStatusEnum.UNREAD));
  157 + mapper.insert(buildMessage(1L, 2L, ImMessageStatusEnum.NORMAL));
  158 + mapper.insert(buildMessage(2L, 1L, ImMessageStatusEnum.NORMAL));
  159 + mapper.insert(buildMessage(1L, 2L, ImMessageStatusEnum.NORMAL));
163 160  
164 161 // 调用:limit 2
165 162 List<ImPrivateMessageDO> result = mapper.selectHistoryList(1L, 2L, null, 2);
... ... @@ -171,7 +168,7 @@ public class ImPrivateMessageMapperTest extends BaseDbUnitTest {
171 168 @Test
172 169 public void testSelectHistoryList_bidirectional() {
173 170 // 准备:验证双向查询
174   - ImPrivateMessageDO msg1 = buildMessage(2L, 1L, ImMessageStatusEnum.UNREAD); // 对方发的
  171 + ImPrivateMessageDO msg1 = buildMessage(2L, 1L, ImMessageStatusEnum.NORMAL); // 对方发的
175 172 mapper.insert(msg1);
176 173  
177 174 // 调用:以用户 1 的视角查
... ...
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/dal/mysql/statistics/ImStatisticsManagerMapperTest.java
... ... @@ -11,7 +11,7 @@ import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupMemberMapper;
11 11 import cn.iocoder.yudao.module.im.dal.mysql.message.ImGroupMessageMapper;
12 12 import cn.iocoder.yudao.module.im.dal.mysql.message.ImPrivateMessageMapper;
13 13 import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum;
14   -import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
  14 +import cn.iocoder.yudao.module.im.enums.ImContentTypeEnum;
15 15 import jakarta.annotation.Resource;
16 16 import org.junit.jupiter.api.BeforeEach;
17 17 import org.junit.jupiter.api.Test;
... ... @@ -25,7 +25,7 @@ import java.util.Map;
25 25 import static org.junit.jupiter.api.Assertions.*;
26 26  
27 27 /**
28   - * IM 数据看板 Mapper 单元测试
  28 + * {@link ImStatisticsManagerMapper} 的单元测试
29 29 *
30 30 * @author 芋道源码
31 31 */
... ... @@ -194,10 +194,10 @@ public class ImStatisticsManagerMapperTest extends BaseDbUnitTest {
194 194 @Test
195 195 public void testSelectMessageTypeDistribution_mergePrivateAndGroup() {
196 196 // 准备:私聊 type=0 ×1,群聊 type=0 ×2、type=1 ×1
197   - privateMessageMapper.insert(buildPrivate(1L, 2L, WINDOW_BEGIN.plusDays(1)).setType(ImMessageTypeEnum.TEXT.getType()));
198   - groupMessageMapper.insert(buildGroupMessage(1L, 100L, WINDOW_BEGIN.plusDays(1)).setType(ImMessageTypeEnum.TEXT.getType()));
199   - groupMessageMapper.insert(buildGroupMessage(1L, 100L, WINDOW_BEGIN.plusDays(1)).setType(ImMessageTypeEnum.TEXT.getType()));
200   - groupMessageMapper.insert(buildGroupMessage(1L, 100L, WINDOW_BEGIN.plusDays(1)).setType(ImMessageTypeEnum.IMAGE.getType()));
  197 + privateMessageMapper.insert(buildPrivate(1L, 2L, WINDOW_BEGIN.plusDays(1)).setType(ImContentTypeEnum.TEXT.getType()));
  198 + groupMessageMapper.insert(buildGroupMessage(1L, 100L, WINDOW_BEGIN.plusDays(1)).setType(ImContentTypeEnum.TEXT.getType()));
  199 + groupMessageMapper.insert(buildGroupMessage(1L, 100L, WINDOW_BEGIN.plusDays(1)).setType(ImContentTypeEnum.TEXT.getType()));
  200 + groupMessageMapper.insert(buildGroupMessage(1L, 100L, WINDOW_BEGIN.plusDays(1)).setType(ImContentTypeEnum.IMAGE.getType()));
201 201  
202 202 // 调用
203 203 List<Map<String, Object>> dist = mapper.selectMessageTypeDistribution(WINDOW_BEGIN, WINDOW_END);
... ... @@ -205,8 +205,8 @@ public class ImStatisticsManagerMapperTest extends BaseDbUnitTest {
205 205 // 断言:type=TEXT(0) → 3;type=IMAGE(1) → 1
206 206 Map<Object, Long> byType = dist.stream().collect(java.util.stream.Collectors.toMap(
207 207 m -> ((Number) m.get("type")).intValue(), m -> ((Number) m.get("count")).longValue()));
208   - assertEquals(3L, byType.get(ImMessageTypeEnum.TEXT.getType()));
209   - assertEquals(1L, byType.get(ImMessageTypeEnum.IMAGE.getType()));
  208 + assertEquals(3L, byType.get(ImContentTypeEnum.TEXT.getType()));
  209 + assertEquals(1L, byType.get(ImContentTypeEnum.IMAGE.getType()));
210 210 }
211 211  
212 212 @Test
... ... @@ -243,9 +243,9 @@ public class ImStatisticsManagerMapperTest extends BaseDbUnitTest {
243 243 return ImPrivateMessageDO.builder()
244 244 .clientMessageId("uuid-" + System.nanoTime())
245 245 .senderId(senderId).receiverId(receiverId)
246   - .type(ImMessageTypeEnum.TEXT.getType())
  246 + .type(ImContentTypeEnum.TEXT.getType())
247 247 .content("{}")
248   - .status(ImMessageStatusEnum.UNREAD.getStatus())
  248 + .status(ImMessageStatusEnum.NORMAL.getStatus())
249 249 .sendTime(sendTime).build();
250 250 }
251 251  
... ... @@ -253,9 +253,9 @@ public class ImStatisticsManagerMapperTest extends BaseDbUnitTest {
253 253 return new ImGroupMessageDO()
254 254 .setClientMessageId("uuid-" + System.nanoTime())
255 255 .setSenderId(senderId).setGroupId(groupId)
256   - .setType(ImMessageTypeEnum.TEXT.getType())
  256 + .setType(ImContentTypeEnum.TEXT.getType())
257 257 .setContent("{}")
258   - .setStatus(ImMessageStatusEnum.UNREAD.getStatus())
  258 + .setStatus(ImMessageStatusEnum.NORMAL.getStatus())
259 259 .setSendTime(sendTime);
260 260 }
261 261  
... ...
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/face/ImFacePackItemServiceImplTest.java
... ... @@ -26,7 +26,7 @@ import static org.mockito.Mockito.verify;
26 26 import static org.mockito.Mockito.when;
27 27  
28 28 /**
29   - * IM 表情包项 Service 单元测试
  29 + * {@link ImFacePackItemServiceImpl} 的单元测试
30 30 *
31 31 * @author 芋道源码
32 32 */
... ...
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/face/ImFacePackServiceImplTest.java
... ... @@ -25,7 +25,7 @@ import static org.mockito.Mockito.verify;
25 25 import static org.mockito.Mockito.when;
26 26  
27 27 /**
28   - * IM 表情包 Service 单元测试
  28 + * {@link ImFacePackServiceImpl} 的单元测试
29 29 *
30 30 * @author 芋道源码
31 31 */
... ...
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/face/ImFaceUserItemServiceImplTest.java
... ... @@ -26,7 +26,7 @@ import static org.mockito.Mockito.verify;
26 26 import static org.mockito.Mockito.when;
27 27  
28 28 /**
29   - * IM 用户私有表情 Service 单元测试
  29 + * {@link ImFaceUserItemServiceImpl} 的单元测试
30 30 *
31 31 * @author 芋道源码
32 32 */
... ... @@ -141,7 +141,7 @@ public class ImFaceUserItemServiceImplTest extends BaseMockitoUnitTest {
141 141 verify(faceUserItemMapper, never()).deleteById(anyLong());
142 142 }
143 143  
144   - // ========== deleteFaceUserItem(管理后台 by id) ==========
  144 + // ========== deleteFaceUserItem(管理后台) ==========
145 145  
146 146 @Test
147 147 public void testDeleteFaceUserItemAdmin_success() {
... ...
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/friend/ImFriendRequestServiceImplTest.java
1 1 package cn.iocoder.yudao.module.im.service.friend;
2 2  
3   -import cn.hutool.core.collection.ListUtil;
4 3 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
5 4 import cn.iocoder.yudao.framework.common.exception.ServiceException;
6 5 import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
... ... @@ -12,7 +11,6 @@ import cn.iocoder.yudao.module.im.enums.friend.ImFriendRequestHandleResultEnum;
12 11 import cn.iocoder.yudao.module.im.enums.friend.ImFriendStateEnum;
13 12 import cn.iocoder.yudao.module.im.framework.config.ImProperties;
14 13 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService;
15   -import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO;
16 14 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
17 15 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
18 16 import org.junit.jupiter.api.Test;
... ... @@ -36,7 +34,7 @@ import static org.mockito.Mockito.verify;
36 34 import static org.mockito.Mockito.when;
37 35  
38 36 /**
39   - * IM 好友申请 Service 单元测试
  37 + * {@link ImFriendRequestServiceImpl} 的单元测试
40 38 *
41 39 * @author 芋道源码
42 40 */
... ... @@ -113,7 +111,7 @@ public class ImFriendRequestServiceImplTest extends BaseMockitoUnitTest {
113 111 assertNull(result);
114 112 verify(friendService).silentReAddFriend(eq(1L), eq(2L), eq("老张"), eq(1));
115 113 verify(friendRequestMapper, never()).insert(any(ImFriendRequestDO.class));
116   - verify(websocketService, never()).sendPrivateMessageAsync(anyLong(), any(ImPrivateMessageDTO.class));
  114 + verify(websocketService, never()).sendNotificationAsync(anyLong(), anyInt(), anyInt(), any());
117 115 }
118 116  
119 117 @Test
... ... @@ -158,7 +156,7 @@ public class ImFriendRequestServiceImplTest extends BaseMockitoUnitTest {
158 156 assertEquals(ImFriendRequestHandleResultEnum.UNHANDLED.getResult(), saved.getHandleResult());
159 157 assertSame(saved, result);
160 158 // 断言:推送给接收方
161   - verify(websocketService).sendPrivateMessageAsync(eq(2L), any(ImPrivateMessageDTO.class));
  159 + verify(websocketService).sendNotificationAsync(eq(2L), anyInt(), anyInt(), any());
162 160 }
163 161  
164 162 @Test
... ... @@ -211,7 +209,7 @@ public class ImFriendRequestServiceImplTest extends BaseMockitoUnitTest {
211 209 any(LocalDateTime.class));
212 210 assertEquals(100L, result.getId());
213 211 assertEquals(ImFriendRequestHandleResultEnum.UNHANDLED.getResult(), result.getHandleResult());
214   - verify(websocketService).sendPrivateMessageAsync(eq(2L), any(ImPrivateMessageDTO.class));
  212 + verify(websocketService).sendNotificationAsync(eq(2L), anyInt(), anyInt(), any());
215 213 }
216 214  
217 215 // ========== agreeFriendRequest ==========
... ... @@ -229,9 +227,9 @@ public class ImFriendRequestServiceImplTest extends BaseMockitoUnitTest {
229 227 friendRequestService.agreeFriendRequest(2L, 100L);
230 228  
231 229 // 断言:双向建立好友 + 推 APPROVED 给发起方
232   - verify(adminUserApi).validateUserList(ListUtil.of(1L, 2L));
  230 + verify(adminUserApi, never()).validateUserList(any());
233 231 verify(friendService).becomeFriends(request);
234   - verify(websocketService).sendPrivateMessageAsync(eq(1L), any(ImPrivateMessageDTO.class));
  232 + verify(websocketService).sendNotificationAsync(eq(1L), anyInt(), anyInt(), any());
235 233 }
236 234  
237 235 @Test
... ... @@ -306,7 +304,7 @@ public class ImFriendRequestServiceImplTest extends BaseMockitoUnitTest {
306 304 eq(ImFriendRequestHandleResultEnum.UNHANDLED.getResult()), captor.capture());
307 305 assertEquals(ImFriendRequestHandleResultEnum.REFUSED.getResult(), captor.getValue().getHandleResult());
308 306 assertEquals("不认识", captor.getValue().getHandleContent());
309   - verify(websocketService).sendPrivateMessageAsync(eq(1L), any(ImPrivateMessageDTO.class));
  307 + verify(websocketService).sendNotificationAsync(eq(1L), anyInt(), anyInt(), any());
310 308 }
311 309  
312 310 @Test
... ... @@ -321,7 +319,7 @@ public class ImFriendRequestServiceImplTest extends BaseMockitoUnitTest {
321 319 ServiceException exception = assertThrows(ServiceException.class,
322 320 () -> friendRequestService.refuseFriendRequest(2L, 100L, "x"));
323 321 assertEquals(FRIEND_REQUEST_HANDLED.getCode(), exception.getCode());
324   - verify(websocketService, never()).sendPrivateMessageAsync(anyLong(), any(ImPrivateMessageDTO.class));
  322 + verify(websocketService, never()).sendNotificationAsync(anyLong(), anyInt(), anyInt(), any());
325 323 }
326 324  
327 325 // ========== getMyFriendRequestList ==========
... ...
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImplTest.java
... ... @@ -10,7 +10,6 @@ import cn.iocoder.yudao.module.im.dal.mysql.friend.ImFriendMapper;
10 10 import cn.iocoder.yudao.module.im.service.message.ImPrivateMessageService;
11 11 import cn.iocoder.yudao.module.im.service.message.dto.ImPrivateMessageSendDTO;
12 12 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService;
13   -import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO;
14 13 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
15 14 import org.junit.jupiter.api.Test;
16 15 import org.mockito.ArgumentCaptor;
... ... @@ -30,7 +29,7 @@ import static org.mockito.Mockito.verify;
30 29 import static org.mockito.Mockito.when;
31 30  
32 31 /**
33   - * IM 好友关系 Service 单元测试
  32 + * {@link ImFriendServiceImpl} 的单元测试
34 33 *
35 34 * @author 芋道源码
36 35 */
... ... @@ -69,7 +68,7 @@ public class ImFriendServiceImplTest extends BaseMockitoUnitTest {
69 68 assertEquals(100L, captor.getValue().getId());
70 69 assertTrue(captor.getValue().getSilent());
71 70 // 断言:推送了好友更新通知
72   - verify(imWebSocketService).sendPrivateMessageAsync(eq(1L), any(ImPrivateMessageDTO.class));
  71 + verify(imWebSocketService).sendNotificationAsync(eq(1L), anyInt(), anyInt(), any());
73 72 }
74 73  
75 74 @Test
... ... @@ -101,7 +100,7 @@ public class ImFriendServiceImplTest extends BaseMockitoUnitTest {
101 100 () -> friendService.updateFriend(1L, reqVO));
102 101 assertEquals(FRIEND_NOT_FRIEND.getCode(), exception.getCode());
103 102 verify(imFriendMapper, never()).updateById(any(ImFriendDO.class));
104   - verify(imWebSocketService, never()).sendPrivateMessageAsync(any(Long.class), any(ImPrivateMessageDTO.class));
  103 + verify(imWebSocketService, never()).sendNotificationAsync(any(Long.class), anyInt(), anyInt(), any());
105 104 }
106 105  
107 106 @Test
... ... @@ -124,7 +123,7 @@ public class ImFriendServiceImplTest extends BaseMockitoUnitTest {
124 123 assertEquals("老张", captor.getValue().getDisplayName());
125 124 assertNull(captor.getValue().getSilent());
126 125 // 断言:推送好友更新通知
127   - verify(imWebSocketService).sendPrivateMessageAsync(eq(1L), any(ImPrivateMessageDTO.class));
  126 + verify(imWebSocketService).sendNotificationAsync(eq(1L), anyInt(), anyInt(), any());
128 127 }
129 128  
130 129 @Test
... ... @@ -139,10 +138,10 @@ public class ImFriendServiceImplTest extends BaseMockitoUnitTest {
139 138 // 断言:没查记录、没触发 SQL 更新 / 没发 WebSocket 推送
140 139 verify(imFriendMapper, never()).selectByUserIdAndFriendUserId(anyLong(), anyLong());
141 140 verify(imFriendMapper, never()).updateById(any(ImFriendDO.class));
142   - verify(imWebSocketService, never()).sendPrivateMessageAsync(any(Long.class), any(ImPrivateMessageDTO.class));
  141 + verify(imWebSocketService, never()).sendNotificationAsync(any(Long.class), anyInt(), anyInt(), any());
143 142 }
144 143  
145   - // ========== addFriend0(内部方法,被 becomeFriends / silentReAddFriend 调用) ==========
  144 + // ========== 建立好友 ==========
146 145  
147 146 @Test
148 147 public void testAddFriend0_existingEnabledSkip() {
... ... @@ -158,7 +157,7 @@ public class ImFriendServiceImplTest extends BaseMockitoUnitTest {
158 157 verify(imFriendMapper, never()).insert(any(ImFriendDO.class));
159 158 verify(imFriendMapper, never()).updateById(any(ImFriendDO.class));
160 159 verify(imFriendMapper, never()).updateReAddFields(anyLong(), anyInt(), any(LocalDateTime.class),
161   - anyBoolean(), anyBoolean(), anyBoolean(), any(), any());
  160 + any(LocalDateTime.class), anyBoolean(), anyBoolean(), anyBoolean(), any(), any());
162 161 }
163 162  
164 163 @Test
... ... @@ -173,7 +172,7 @@ public class ImFriendServiceImplTest extends BaseMockitoUnitTest {
173 172  
174 173 // 断言:恢复 ENABLE,并清空 deleteTime
175 174 verify(imFriendMapper).updateReAddFields(eq(10L), eq(CommonStatusEnum.ENABLE.getStatus()),
176   - any(LocalDateTime.class), eq(false), eq(false), eq(false), isNull(), isNull());
  175 + any(LocalDateTime.class), any(LocalDateTime.class), eq(false), eq(false), eq(false), isNull(), isNull());
177 176 verify(imFriendMapper, never()).insert(any(ImFriendDO.class));
178 177 }
179 178  
... ...
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberServiceImplTest.java
... ... @@ -9,7 +9,7 @@ import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO;
9 9 import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupMemberMapper;
10 10 import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum;
11 11 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService;
12   -import cn.iocoder.yudao.module.im.service.websocket.dto.ImGroupMessageDTO;
  12 +import cn.iocoder.yudao.module.im.service.websocket.notification.message.ImGroupMessageNotification;
13 13 import org.junit.jupiter.api.Test;
14 14 import org.mockito.ArgumentCaptor;
15 15 import org.mockito.InjectMocks;
... ... @@ -24,7 +24,7 @@ import static org.mockito.ArgumentMatchers.*;
24 24 import static org.mockito.Mockito.*;
25 25  
26 26 /**
27   - * IM 群成员 Service 单元测试
  27 + * {@link ImGroupMemberServiceImpl} 的单元测试
28 28 *
29 29 * @author 芋道源码
30 30 */
... ... @@ -242,7 +242,7 @@ public class ImGroupMemberServiceImplTest extends BaseMockitoUnitTest {
242 242 verify(groupMemberMapper).updateById(captor.capture());
243 243 assertEquals(50L, captor.getValue().getId());
244 244 assertTrue(captor.getValue().getSilent());
245   - // 公开字段昵称变化 → 全员广播 GROUP_MEMBER_NICKNAME_UPDATE
  245 + // 公开字段昵称变化 → 全员在线同步 GROUP_MEMBER_NICKNAME_UPDATE
246 246 verify(groupMessageService).sendGroupMessage(eq(1L), any(cn.iocoder.yudao.module.im.service.message.dto.ImGroupMessageSendDTO.class));
247 247 // 个人字段 silent 变化 → 仅自己多端同步 GROUP_MEMBER_SETTING_UPDATE
248 248 verify(groupMessageService).sendGroupMessage(eq(1L), eq(ListUtil.of(1L)),
... ...
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/group/ImGroupRequestServiceImplTest.java
... ... @@ -13,11 +13,10 @@ import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupRequestMapper;
13 13 import cn.iocoder.yudao.module.im.enums.group.ImGroupAddSourceEnum;
14 14 import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum;
15 15 import cn.iocoder.yudao.module.im.enums.group.ImGroupRequestHandleResultEnum;
16   -import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
  16 +import cn.iocoder.yudao.module.im.enums.ImContentTypeEnum;
17 17 import cn.iocoder.yudao.module.im.service.message.ImGroupMessageService;
18 18 import cn.iocoder.yudao.module.im.service.message.dto.ImGroupMessageSendDTO;
19 19 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService;
20   -import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO;
21 20 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
22 21 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
23 22 import org.junit.jupiter.api.Test;
... ... @@ -35,7 +34,7 @@ import static org.mockito.ArgumentMatchers.*;
35 34 import static org.mockito.Mockito.*;
36 35  
37 36 /**
38   - * IM 加群申请 Service 单元测试
  37 + * {@link ImGroupRequestServiceImpl} 的单元测试
39 38 *
40 39 * @author 芋道源码
41 40 */
... ... @@ -84,7 +83,7 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest {
84 83 // 推 1510 自由进群
85 84 ArgumentCaptor<ImGroupMessageSendDTO> dtoCaptor = ArgumentCaptor.forClass(ImGroupMessageSendDTO.class);
86 85 verify(groupMessageService).sendGroupMessage(eq(1L), dtoCaptor.capture());
87   - assertEquals(ImMessageTypeEnum.GROUP_MEMBER_ENTER.getType(), dtoCaptor.getValue().getType());
  86 + assertEquals(ImContentTypeEnum.GROUP_MEMBER_ENTER.getType(), dtoCaptor.getValue().getType());
88 87 }
89 88  
90 89 @Test
... ... @@ -117,7 +116,7 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest {
117 116 verify(groupMemberService, never()).addGroupMember(anyLong(), anyLong(), anyInt(), anyInt(), anyLong());
118 117 verify(groupRequestMapper).insert(any(ImGroupRequestDO.class));
119 118 // 1503 推送给 owner(99) + admin(98),去重后两条
120   - verify(websocketService, times(2)).sendPrivateMessageAsync(anyLong(), any(ImPrivateMessageDTO.class));
  119 + verify(websocketService, times(2)).sendNotificationAsync(anyLong(), anyInt(), anyInt(), any());
121 120 }
122 121  
123 122 @Test
... ... @@ -150,7 +149,7 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest {
150 149 eq(ImGroupAddSourceEnum.SEARCH.getSource()), any());
151 150 assertEquals(50L, result.getId());
152 151 assertEquals(ImGroupRequestHandleResultEnum.UNHANDLED.getResult(), result.getHandleResult());
153   - verify(websocketService).sendPrivateMessageAsync(eq(99L), any(ImPrivateMessageDTO.class));
  152 + verify(websocketService).sendNotificationAsync(eq(99L), anyInt(), anyInt(), any());
154 153 }
155 154  
156 155 @Test
... ... @@ -207,9 +206,9 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest {
207 206 eq(ImGroupAddSourceEnum.SEARCH.getSource()), isNull());
208 207 ArgumentCaptor<ImGroupMessageSendDTO> dtoCaptor = ArgumentCaptor.forClass(ImGroupMessageSendDTO.class);
209 208 verify(groupMessageService).sendGroupMessage(eq(1L), dtoCaptor.capture());
210   - assertEquals(ImMessageTypeEnum.GROUP_MEMBER_ENTER.getType(), dtoCaptor.getValue().getType());
  209 + assertEquals(ImContentTypeEnum.GROUP_MEMBER_ENTER.getType(), dtoCaptor.getValue().getType());
211 210 // 1505 推送给申请人 + owner,去重后两条
212   - verify(websocketService, times(2)).sendPrivateMessageAsync(anyLong(), any(ImPrivateMessageDTO.class));
  211 + verify(websocketService, times(2)).sendNotificationAsync(anyLong(), anyInt(), anyInt(), any());
213 212 }
214 213  
215 214 @Test
... ... @@ -279,7 +278,7 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest {
279 278  
280 279 // 不写群成员;推 1506 给申请人 + 群主(同一人 1L 时去重为 1 + 申请人 2L = 2 条)
281 280 verify(groupMemberService, never()).addGroupMember(anyLong(), anyLong(), anyInt(), any(), any());
282   - verify(websocketService, times(2)).sendPrivateMessageAsync(anyLong(), any(ImPrivateMessageDTO.class));
  281 + verify(websocketService, times(2)).sendNotificationAsync(anyLong(), anyInt(), anyInt(), any());
283 282 }
284 283  
285 284 // ==================== createInviteRequestList ====================
... ... @@ -306,7 +305,7 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest {
306 305 // 断言:插入 2 条 + 推 1503 给 owner(每条 1 帧)共 2 帧
307 306 ArgumentCaptor<ImGroupRequestDO> captor = ArgumentCaptor.forClass(ImGroupRequestDO.class);
308 307 verify(groupRequestMapper, times(2)).insert(captor.capture());
309   - verify(websocketService, times(2)).sendPrivateMessageAsync(anyLong(), any(ImPrivateMessageDTO.class));
  308 + verify(websocketService, times(2)).sendNotificationAsync(anyLong(), anyInt(), anyInt(), any());
310 309 // 断言:每条记录 inviterUserId=1 + addSource=INVITE,避免审批通过后回写群成员留痕的来源为空 / 脏带旧值
311 310 Collection<ImGroupRequestDO> inserted = captor.getAllValues();
312 311 assertEquals(2, inserted.size());
... ... @@ -338,7 +337,7 @@ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest {
338 337 // 断言:复用并重置旧邀请申请
339 338 verify(groupRequestMapper).updateInviteByIdReset(eq(50L), eq(1L),
340 339 eq(ImGroupAddSourceEnum.INVITE.getSource()), any());
341   - verify(websocketService).sendPrivateMessageAsync(eq(99L), any(ImPrivateMessageDTO.class));
  340 + verify(websocketService).sendNotificationAsync(eq(99L), anyInt(), anyInt(), any());
342 341 }
343 342  
344 343 private AdminUserRespDTO buildUser(Long id, String nickname) {
... ...
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImplTest.java
1 1 package cn.iocoder.yudao.module.im.service.group;
2 2  
3   -import cn.hutool.core.collection.CollUtil;
4   -import cn.hutool.core.collection.ListUtil;
5   -import cn.hutool.core.map.MapUtil;
6 3 import cn.hutool.extra.spring.SpringUtil;
7 4 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
8 5 import cn.iocoder.yudao.framework.common.exception.ServiceException;
... ... @@ -22,13 +19,13 @@ import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO;
22 19 import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupMapper;
23 20 import cn.iocoder.yudao.module.im.enums.group.ImGroupAddSourceEnum;
24 21 import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum;
25   -import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
  22 +import cn.iocoder.yudao.module.im.enums.ImContentTypeEnum;
26 23 import cn.iocoder.yudao.module.im.framework.config.ImProperties;
27 24 import cn.iocoder.yudao.module.im.service.friend.ImFriendService;
28 25 import cn.iocoder.yudao.module.im.service.message.ImGroupMessageService;
29 26 import cn.iocoder.yudao.module.im.service.message.dto.ImGroupMessageSendDTO;
30 27 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService;
31   -import cn.iocoder.yudao.module.im.service.websocket.dto.ImGroupMessageDTO;
  28 +import cn.iocoder.yudao.module.im.service.websocket.notification.message.ImGroupMessageNotification;
32 29 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
33 30 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
34 31 import org.junit.jupiter.api.Test;
... ... @@ -51,7 +48,7 @@ import static org.mockito.ArgumentMatchers.*;
51 48 import static org.mockito.Mockito.*;
52 49  
53 50 /**
54   - * IM 群 Service 单元测试
  51 + * {@link ImGroupServiceImpl} 的单元测试
55 52 *
56 53 * @author 芋道源码
57 54 */
... ... @@ -103,7 +100,7 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
103 100 // 验证:推送 GROUP_CREATE 通知(payload memberUserIds 含创建者自己)
104 101 ArgumentCaptor<ImGroupMessageSendDTO> dtoCaptor = ArgumentCaptor.forClass(ImGroupMessageSendDTO.class);
105 102 verify(groupMessageService).sendGroupMessage(eq(1L), anyCollection(), dtoCaptor.capture());
106   - assertEquals(ImMessageTypeEnum.GROUP_CREATE.getType(), dtoCaptor.getValue().getType());
  103 + assertEquals(ImContentTypeEnum.GROUP_CREATE.getType(), dtoCaptor.getValue().getType());
107 104 }
108 105  
109 106 @Test
... ... @@ -111,13 +108,13 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
111 108 // 准备:创建者 + 2 个初始成员,都是好友
112 109 ImGroupCreateReqVO reqVO = new ImGroupCreateReqVO();
113 110 reqVO.setName("测试群");
114   - reqVO.setMemberUserIds(new ArrayList<>(ListUtil.of(2L, 3L)));
  111 + reqVO.setMemberUserIds(new ArrayList<>(List.of(2L, 3L)));
115 112 when(groupMapper.insert(any(ImGroupDO.class))).thenAnswer(invocation -> {
116 113 ImGroupDO group = invocation.getArgument(0);
117 114 group.setId(100L);
118 115 return 1;
119 116 });
120   - when(friendService.getActiveFriendList(eq(1L), anyCollection())).thenReturn(ListUtil.of(
  117 + when(friendService.getActiveFriendList(eq(1L), anyCollection())).thenReturn(List.of(
121 118 ImFriendDO.builder().userId(1L).friendUserId(2L)
122 119 .status(CommonStatusEnum.ENABLE.getStatus()).build(),
123 120 ImFriendDO.builder().userId(1L).friendUserId(3L)
... ... @@ -135,7 +132,7 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
135 132 // 验证:推送 GROUP_CREATE 通知,payload memberUserIds 含全员(创建者 + 邀请)
136 133 ArgumentCaptor<ImGroupMessageSendDTO> dtoCaptor = ArgumentCaptor.forClass(ImGroupMessageSendDTO.class);
137 134 verify(groupMessageService).sendGroupMessage(eq(1L), anyCollection(), dtoCaptor.capture());
138   - assertEquals(ImMessageTypeEnum.GROUP_CREATE.getType(), dtoCaptor.getValue().getType());
  135 + assertEquals(ImContentTypeEnum.GROUP_CREATE.getType(), dtoCaptor.getValue().getType());
139 136 }
140 137  
141 138 @Test
... ... @@ -143,15 +140,15 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
143 140 // 准备:初始成员里有非好友
144 141 ImGroupCreateReqVO reqVO = new ImGroupCreateReqVO();
145 142 reqVO.setName("测试群");
146   - reqVO.setMemberUserIds(new ArrayList<>(ListUtil.of(2L, 3L)));
  143 + reqVO.setMemberUserIds(new ArrayList<>(List.of(2L, 3L)));
147 144 // 只有 2 是好友,3 不是
148   - when(friendService.getActiveFriendList(eq(1L), anyCollection())).thenReturn(ListUtil.of(
  145 + when(friendService.getActiveFriendList(eq(1L), anyCollection())).thenReturn(List.of(
149 146 ImFriendDO.builder().userId(1L).friendUserId(2L)
150 147 .status(CommonStatusEnum.ENABLE.getStatus()).build()));
151 148 AdminUserRespDTO u3 = new AdminUserRespDTO();
152 149 u3.setId(3L);
153 150 u3.setNickname("李四");
154   - when(adminUserApi.getUserMap(anyCollection())).thenReturn(MapUtil.of(3L, u3));
  151 + when(adminUserApi.getUserMap(anyCollection())).thenReturn(Map.of(3L, u3));
155 152  
156 153 // 调用并断言
157 154 ServiceException exception = assertThrows(ServiceException.class,
... ... @@ -208,7 +205,59 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
208 205 // 推送 GROUP_NAME_UPDATE 通知给全员
209 206 ArgumentCaptor<ImGroupMessageSendDTO> dtoCaptor = ArgumentCaptor.forClass(ImGroupMessageSendDTO.class);
210 207 verify(groupMessageService).sendGroupMessage(eq(1L), anyCollection(), dtoCaptor.capture());
211   - assertEquals(ImMessageTypeEnum.GROUP_NAME_UPDATE.getType(), dtoCaptor.getValue().getType());
  208 + assertEquals(ImContentTypeEnum.GROUP_NAME_UPDATE.getType(), dtoCaptor.getValue().getType());
  209 + }
  210 + }
  211 +
  212 + @Test
  213 + public void testUpdateGroup_joinApprovalChanged() {
  214 + try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
  215 + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class)))
  216 + .thenReturn(groupService);
  217 +
  218 + // 准备
  219 + ImGroupDO group = ImGroupDO.builder().id(10L).name("群").ownerUserId(1L)
  220 + .joinApproval(false).status(CommonStatusEnum.ENABLE.getStatus()).build();
  221 + when(groupMapper.selectById(10L)).thenReturn(group);
  222 +
  223 + ImGroupUpdateReqVO reqVO = new ImGroupUpdateReqVO();
  224 + reqVO.setId(10L);
  225 + reqVO.setJoinApproval(true);
  226 +
  227 + // 调用
  228 + ImGroupDO result = groupService.updateGroup(reqVO, 1L);
  229 +
  230 + // 断言
  231 + verify(groupMapper).updateById(any(ImGroupDO.class));
  232 + assertTrue(result.getJoinApproval());
  233 + ArgumentCaptor<ImGroupMessageSendDTO> dtoCaptor = ArgumentCaptor.forClass(ImGroupMessageSendDTO.class);
  234 + verify(groupMessageService).sendGroupMessage(eq(1L), anyCollection(), dtoCaptor.capture());
  235 + assertEquals(ImContentTypeEnum.GROUP_INFO_UPDATE.getType(), dtoCaptor.getValue().getType());
  236 + }
  237 + }
  238 +
  239 + @Test
  240 + public void testUpdateGroup_joinApprovalSame() {
  241 + try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
  242 + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class)))
  243 + .thenReturn(groupService);
  244 +
  245 + // 准备
  246 + ImGroupDO group = ImGroupDO.builder().id(10L).name("群").ownerUserId(1L)
  247 + .joinApproval(true).status(CommonStatusEnum.ENABLE.getStatus()).build();
  248 + when(groupMapper.selectById(10L)).thenReturn(group);
  249 +
  250 + ImGroupUpdateReqVO reqVO = new ImGroupUpdateReqVO();
  251 + reqVO.setId(10L);
  252 + reqVO.setJoinApproval(true);
  253 +
  254 + // 调用
  255 + ImGroupDO result = groupService.updateGroup(reqVO, 1L);
  256 +
  257 + // 断言
  258 + verify(groupMapper).updateById(any(ImGroupDO.class));
  259 + assertTrue(result.getJoinApproval());
  260 + verify(groupMessageService, never()).sendGroupMessage(anyLong(), anyCollection(), any());
212 261 }
213 262 }
214 263  
... ... @@ -234,11 +283,10 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
234 283 assertEquals(CommonStatusEnum.DISABLE.getStatus(), captor.getValue().getStatus());
235 284 assertNotNull(captor.getValue().getDissolvedTime());
236 285 verify(groupMemberService).removeGroupMembersByGroupId(10L);
237   - verify(groupMessageService).deleteReadMaxMessageIdMap(10L);
238 286 // 推送 GROUP_DISSOLVE 通知(send-before-remove,sendGroupMessage 内部查 active 自动覆盖全员,含群主多端同步)
239 287 ArgumentCaptor<ImGroupMessageSendDTO> dtoCaptor = ArgumentCaptor.forClass(ImGroupMessageSendDTO.class);
240 288 verify(groupMessageService).sendGroupMessage(eq(1L), dtoCaptor.capture());
241   - assertEquals(ImMessageTypeEnum.GROUP_DISSOLVE.getType(), dtoCaptor.getValue().getType());
  289 + assertEquals(ImContentTypeEnum.GROUP_DISSOLVE.getType(), dtoCaptor.getValue().getType());
242 290 }
243 291 }
244 292  
... ... @@ -280,9 +328,8 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
280 328 // 断言:使用管理员编号发通知并完成清理
281 329 ArgumentCaptor<ImGroupMessageSendDTO> dtoCaptor = ArgumentCaptor.forClass(ImGroupMessageSendDTO.class);
282 330 verify(groupMessageService).sendGroupMessage(eq(99L), dtoCaptor.capture());
283   - assertEquals(ImMessageTypeEnum.GROUP_DISSOLVE.getType(), dtoCaptor.getValue().getType());
  331 + assertEquals(ImContentTypeEnum.GROUP_DISSOLVE.getType(), dtoCaptor.getValue().getType());
284 332 verify(groupMemberService).removeGroupMembersByGroupId(10L);
285   - verify(groupMessageService).deleteReadMaxMessageIdMap(10L);
286 333 }
287 334 }
288 335  
... ... @@ -330,8 +377,8 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
330 377 // 被邀请人 2 和 3 都是好友
331 378 ImGroupMemberInviteReqVO reqVO = new ImGroupMemberInviteReqVO();
332 379 reqVO.setGroupId(10L);
333   - reqVO.setMemberUserIds(new ArrayList<>(ListUtil.of(2L, 3L)));
334   - List<ImFriendDO> friends = ListUtil.of(
  380 + reqVO.setMemberUserIds(new ArrayList<>(List.of(2L, 3L)));
  381 + List<ImFriendDO> friends = List.of(
335 382 ImFriendDO.builder().userId(1L).friendUserId(2L)
336 383 .status(CommonStatusEnum.ENABLE.getStatus()).build(),
337 384 ImFriendDO.builder().userId(1L).friendUserId(3L)
... ... @@ -347,10 +394,10 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
347 394 verify(groupMemberService).addGroupMembers(eq(10L), anyCollection(),
348 395 eq(ImGroupAddSourceEnum.INVITE.getSource()), eq(1L));
349 396 verify(groupRequestService, never()).createInviteRequestList(anyLong(), anyLong(), anyCollection());
350   - verify(webSocketService, never()).sendGroupMessageAsync(anyCollection(), any(ImGroupMessageDTO.class));
  397 + verify(webSocketService, never()).sendNotificationAsync(anyCollection(), anyInt(), anyInt(), any());
351 398 ArgumentCaptor<ImGroupMessageSendDTO> dtoCaptor = ArgumentCaptor.forClass(ImGroupMessageSendDTO.class);
352 399 verify(groupMessageService).sendGroupMessage(eq(1L), anyCollection(), dtoCaptor.capture());
353   - assertEquals(ImMessageTypeEnum.GROUP_MEMBER_INVITE.getType(), dtoCaptor.getValue().getType());
  400 + assertEquals(ImContentTypeEnum.GROUP_MEMBER_INVITE.getType(), dtoCaptor.getValue().getType());
354 401 }
355 402 }
356 403  
... ... @@ -369,7 +416,7 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
369 416 ImGroupMemberDO.builder().groupId(10L).userId(1L)
370 417 .role(ImGroupMemberRoleEnum.NORMAL.getRole())
371 418 .status(CommonStatusEnum.ENABLE.getStatus()).build());
372   - when(groupMemberService.getActiveGroupMemberListByGroupId(10L)).thenReturn(ListUtil.of(
  419 + when(groupMemberService.getActiveGroupMemberListByGroupId(10L)).thenReturn(List.of(
373 420 ImGroupMemberDO.builder().groupId(10L).userId(99L)
374 421 .role(ImGroupMemberRoleEnum.OWNER.getRole())
375 422 .status(CommonStatusEnum.ENABLE.getStatus()).build(),
... ... @@ -379,8 +426,8 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
379 426  
380 427 ImGroupMemberInviteReqVO reqVO = new ImGroupMemberInviteReqVO();
381 428 reqVO.setGroupId(10L);
382   - reqVO.setMemberUserIds(new ArrayList<>(ListUtil.of(2L, 3L)));
383   - when(friendService.getActiveFriendList(eq(1L), anyCollection())).thenReturn(ListUtil.of(
  429 + reqVO.setMemberUserIds(new ArrayList<>(List.of(2L, 3L)));
  430 + when(friendService.getActiveFriendList(eq(1L), anyCollection())).thenReturn(List.of(
384 431 ImFriendDO.builder().userId(1L).friendUserId(2L)
385 432 .status(CommonStatusEnum.ENABLE.getStatus()).build(),
386 433 ImFriendDO.builder().userId(1L).friendUserId(3L)
... ... @@ -411,15 +458,15 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
411 458 ImGroupMemberDO.builder().groupId(10L).userId(1L)
412 459 .role(ImGroupMemberRoleEnum.OWNER.getRole())
413 460 .status(CommonStatusEnum.ENABLE.getStatus()).build());
414   - when(groupMemberService.getActiveGroupMemberListByGroupId(10L)).thenReturn(ListUtil.of(
  461 + when(groupMemberService.getActiveGroupMemberListByGroupId(10L)).thenReturn(List.of(
415 462 ImGroupMemberDO.builder().groupId(10L).userId(1L)
416 463 .role(ImGroupMemberRoleEnum.OWNER.getRole())
417 464 .status(CommonStatusEnum.ENABLE.getStatus()).build()));
418 465  
419 466 ImGroupMemberInviteReqVO reqVO = new ImGroupMemberInviteReqVO();
420 467 reqVO.setGroupId(10L);
421   - reqVO.setMemberUserIds(new ArrayList<>(ListUtil.of(2L, 3L)));
422   - when(friendService.getActiveFriendList(eq(1L), anyCollection())).thenReturn(ListUtil.of(
  468 + reqVO.setMemberUserIds(new ArrayList<>(List.of(2L, 3L)));
  469 + when(friendService.getActiveFriendList(eq(1L), anyCollection())).thenReturn(List.of(
423 470 ImFriendDO.builder().userId(1L).friendUserId(2L)
424 471 .status(CommonStatusEnum.ENABLE.getStatus()).build(),
425 472 ImFriendDO.builder().userId(1L).friendUserId(3L)
... ... @@ -434,7 +481,7 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
434 481 eq(ImGroupAddSourceEnum.INVITE.getSource()), eq(1L));
435 482 ArgumentCaptor<ImGroupMessageSendDTO> dtoCaptor = ArgumentCaptor.forClass(ImGroupMessageSendDTO.class);
436 483 verify(groupMessageService).sendGroupMessage(eq(1L), anyCollection(), dtoCaptor.capture());
437   - assertEquals(ImMessageTypeEnum.GROUP_MEMBER_INVITE.getType(), dtoCaptor.getValue().getType());
  484 + assertEquals(ImContentTypeEnum.GROUP_MEMBER_INVITE.getType(), dtoCaptor.getValue().getType());
438 485 }
439 486 }
440 487  
... ... @@ -448,18 +495,18 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
448 495 .status(CommonStatusEnum.ENABLE.getStatus()).build();
449 496 when(groupMapper.selectById(10L)).thenReturn(group);
450 497 when(groupMemberService.getActiveGroupMemberListByGroupId(10L))
451   - .thenReturn(ListUtil.of(ImGroupMemberDO.builder().groupId(10L).userId(1L)
  498 + .thenReturn(List.of(ImGroupMemberDO.builder().groupId(10L).userId(1L)
452 499 .status(CommonStatusEnum.ENABLE.getStatus()).build()));
453 500  
454 501 ImGroupMemberInviteReqVO reqVO = new ImGroupMemberInviteReqVO();
455 502 reqVO.setGroupId(10L);
456   - reqVO.setMemberUserIds(new ArrayList<>(ListUtil.of(2L, 3L)));
  503 + reqVO.setMemberUserIds(new ArrayList<>(List.of(2L, 3L)));
457 504 // 只有 2 是好友,3 不是
458   - when(friendService.getActiveFriendList(eq(1L), anyCollection())).thenReturn(ListUtil.of(
  505 + when(friendService.getActiveFriendList(eq(1L), anyCollection())).thenReturn(List.of(
459 506 ImFriendDO.builder().userId(1L).friendUserId(2L)
460 507 .status(CommonStatusEnum.ENABLE.getStatus()).build()));
461 508 AdminUserRespDTO u3 = new AdminUserRespDTO(); u3.setId(3L); u3.setNickname("李四");
462   - when(adminUserApi.getUserMap(anyCollection())).thenReturn(MapUtil.of(3L, u3));
  509 + when(adminUserApi.getUserMap(anyCollection())).thenReturn(Map.of(3L, u3));
463 510  
464 511 ServiceException exception = assertThrows(ServiceException.class,
465 512 () -> groupService.inviteGroupMember(1L, reqVO));
... ... @@ -487,9 +534,9 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
487 534  
488 535 ImGroupMemberInviteReqVO reqVO = new ImGroupMemberInviteReqVO();
489 536 reqVO.setGroupId(10L);
490   - reqVO.setMemberUserIds(new ArrayList<>(ListUtil.of(600L, 601L)));
  537 + reqVO.setMemberUserIds(new ArrayList<>(List.of(600L, 601L)));
491 538 // 被邀请人都是好友
492   - when(friendService.getActiveFriendList(eq(1L), anyCollection())).thenReturn(ListUtil.of(
  539 + when(friendService.getActiveFriendList(eq(1L), anyCollection())).thenReturn(List.of(
493 540 ImFriendDO.builder().userId(1L).friendUserId(600L)
494 541 .status(CommonStatusEnum.ENABLE.getStatus()).build(),
495 542 ImFriendDO.builder().userId(1L).friendUserId(601L)
... ... @@ -515,7 +562,7 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
515 562 .status(CommonStatusEnum.ENABLE.getStatus()).build();
516 563 when(groupMapper.selectById(10L)).thenReturn(group);
517 564 // 用户 2 已在群中
518   - when(groupMemberService.getActiveGroupMemberListByGroupId(10L)).thenReturn(ListUtil.of(
  565 + when(groupMemberService.getActiveGroupMemberListByGroupId(10L)).thenReturn(List.of(
519 566 ImGroupMemberDO.builder().groupId(10L).userId(1L)
520 567 .status(CommonStatusEnum.ENABLE.getStatus()).build(),
521 568 ImGroupMemberDO.builder().groupId(10L).userId(2L)
... ... @@ -524,14 +571,14 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
524 571  
525 572 ImGroupMemberInviteReqVO reqVO = new ImGroupMemberInviteReqVO();
526 573 reqVO.setGroupId(10L);
527   - reqVO.setMemberUserIds(new ArrayList<>(ListUtil.of(2L))); // 只邀请 2,他已在群中
  574 + reqVO.setMemberUserIds(new ArrayList<>(List.of(2L))); // 只邀请 2,他已在群中
528 575  
529 576 // 调用
530 577 groupService.inviteGroupMember(1L, reqVO);
531 578  
532 579 // 断言:不会触发添加、不会推送
533 580 verify(groupMemberService, never()).addGroupMembers(anyLong(), anyCollection());
534   - verify(webSocketService, never()).sendGroupMessageAsync(anyCollection(), any(ImGroupMessageDTO.class));
  581 + verify(webSocketService, never()).sendNotificationAsync(anyCollection(), anyInt(), anyInt(), any());
535 582 }
536 583 }
537 584  
... ... @@ -566,11 +613,10 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
566 613 groupService.quitGroup(10L, 1L);
567 614  
568 615 verify(groupMemberService).removeGroupMember(10L, 1L);
569   - verify(groupMessageService).deleteReadMaxMessageId(10L, 1L);
570 616 // 推送 GROUP_MEMBER_QUIT 通知给全员(含 quitter,前端自判清群)
571 617 ArgumentCaptor<ImGroupMessageSendDTO> dtoCaptor = ArgumentCaptor.forClass(ImGroupMessageSendDTO.class);
572 618 verify(groupMessageService).sendGroupMessage(eq(1L), dtoCaptor.capture());
573   - assertEquals(ImMessageTypeEnum.GROUP_MEMBER_QUIT.getType(), dtoCaptor.getValue().getType());
  619 + assertEquals(ImContentTypeEnum.GROUP_MEMBER_QUIT.getType(), dtoCaptor.getValue().getType());
574 620 }
575 621 }
576 622  
... ... @@ -587,7 +633,6 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
587 633 groupService.quitGroup(10L, 1L);
588 634  
589 635 verify(groupMemberService).removeGroupMember(10L, 1L);
590   - verify(groupMessageService).deleteReadMaxMessageId(10L, 1L);
591 636 verify(groupMessageService).sendGroupMessage(eq(1L), any(ImGroupMessageSendDTO.class));
592 637 }
593 638 }
... ... @@ -609,7 +654,7 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
609 654  
610 655 ImGroupMemberRemoveReqVO reqVO = new ImGroupMemberRemoveReqVO();
611 656 reqVO.setGroupId(10L);
612   - reqVO.setMemberUserIds(ListUtil.of(1L, 2L));
  657 + reqVO.setMemberUserIds(List.of(1L, 2L));
613 658  
614 659 ServiceException exception = assertThrows(ServiceException.class,
615 660 () -> groupService.removeGroupMember(1L, reqVO));
... ... @@ -631,7 +676,7 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
631 676 ImGroupMemberDO.builder().groupId(10L).userId(1L)
632 677 .role(ImGroupMemberRoleEnum.OWNER.getRole()).build());
633 678 // 目标:两个普通成员
634   - when(groupMemberService.getGroupMembers(eq(10L), anyCollection())).thenReturn(ListUtil.of(
  679 + when(groupMemberService.getGroupMembers(eq(10L), anyCollection())).thenReturn(List.of(
635 680 ImGroupMemberDO.builder().groupId(10L).userId(2L)
636 681 .role(ImGroupMemberRoleEnum.NORMAL.getRole())
637 682 .status(CommonStatusEnum.ENABLE.getStatus()).build(),
... ... @@ -641,16 +686,15 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
641 686  
642 687 ImGroupMemberRemoveReqVO reqVO = new ImGroupMemberRemoveReqVO();
643 688 reqVO.setGroupId(10L);
644   - reqVO.setMemberUserIds(ListUtil.of(2L, 3L));
  689 + reqVO.setMemberUserIds(List.of(2L, 3L));
645 690  
646 691 groupService.removeGroupMember(1L, reqVO);
647 692  
648 693 verify(groupMemberService).removeGroupMembers(eq(10L), anyCollection());
649   - verify(groupMessageService).deleteReadMaxMessageIds(eq(10L), anyCollection());
650 694 // 推送 GROUP_MEMBER_KICK 通知给全员(含被踢者,前端自判清群)
651 695 ArgumentCaptor<ImGroupMessageSendDTO> dtoCaptor = ArgumentCaptor.forClass(ImGroupMessageSendDTO.class);
652 696 verify(groupMessageService).sendGroupMessage(eq(1L), dtoCaptor.capture());
653   - assertEquals(ImMessageTypeEnum.GROUP_MEMBER_KICK.getType(), dtoCaptor.getValue().getType());
  697 + assertEquals(ImContentTypeEnum.GROUP_MEMBER_KICK.getType(), dtoCaptor.getValue().getType());
654 698 }
655 699 }
656 700  
... ... @@ -668,14 +712,14 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
668 712 ImGroupMemberDO.builder().groupId(10L).userId(1L)
669 713 .role(ImGroupMemberRoleEnum.ADMIN.getRole()).build());
670 714 // 目标:另一个管理员
671   - when(groupMemberService.getGroupMembers(eq(10L), anyCollection())).thenReturn(ListUtil.of(
  715 + when(groupMemberService.getGroupMembers(eq(10L), anyCollection())).thenReturn(List.of(
672 716 ImGroupMemberDO.builder().groupId(10L).userId(2L)
673 717 .role(ImGroupMemberRoleEnum.ADMIN.getRole())
674 718 .status(CommonStatusEnum.ENABLE.getStatus()).build()));
675 719  
676 720 ImGroupMemberRemoveReqVO reqVO = new ImGroupMemberRemoveReqVO();
677 721 reqVO.setGroupId(10L);
678   - reqVO.setMemberUserIds(ListUtil.of(2L));
  722 + reqVO.setMemberUserIds(List.of(2L));
679 723  
680 724 ServiceException exception = assertThrows(ServiceException.class,
681 725 () -> groupService.removeGroupMember(1L, reqVO));
... ... @@ -697,14 +741,14 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
697 741 when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn(
698 742 ImGroupMemberDO.builder().groupId(10L).userId(1L)
699 743 .role(ImGroupMemberRoleEnum.OWNER.getRole()).build());
700   - when(groupMemberService.getGroupMembers(eq(10L), anyCollection())).thenReturn(ListUtil.of(
  744 + when(groupMemberService.getGroupMembers(eq(10L), anyCollection())).thenReturn(List.of(
701 745 ImGroupMemberDO.builder().groupId(10L).userId(99L)
702 746 .role(ImGroupMemberRoleEnum.OWNER.getRole())
703 747 .status(CommonStatusEnum.ENABLE.getStatus()).build()));
704 748  
705 749 ImGroupMemberRemoveReqVO reqVO = new ImGroupMemberRemoveReqVO();
706 750 reqVO.setGroupId(10L);
707   - reqVO.setMemberUserIds(ListUtil.of(99L));
  751 + reqVO.setMemberUserIds(List.of(99L));
708 752  
709 753 ServiceException exception = assertThrows(ServiceException.class,
710 754 () -> groupService.removeGroupMember(1L, reqVO));
... ... @@ -726,7 +770,7 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
726 770 ImGroupMemberDO.builder().groupId(10L).userId(1L)
727 771 .role(ImGroupMemberRoleEnum.OWNER.getRole()).build());
728 772 // 目标:2L 有效普通成员;3L 已退群(DISABLE)的历史管理员,应被跳过而非拦截整批
729   - when(groupMemberService.getGroupMembers(eq(10L), anyCollection())).thenReturn(ListUtil.of(
  773 + when(groupMemberService.getGroupMembers(eq(10L), anyCollection())).thenReturn(List.of(
730 774 ImGroupMemberDO.builder().groupId(10L).userId(2L)
731 775 .role(ImGroupMemberRoleEnum.NORMAL.getRole())
732 776 .status(CommonStatusEnum.ENABLE.getStatus()).build(),
... ... @@ -736,17 +780,14 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
736 780  
737 781 ImGroupMemberRemoveReqVO reqVO = new ImGroupMemberRemoveReqVO();
738 782 reqVO.setGroupId(10L);
739   - reqVO.setMemberUserIds(ListUtil.of(2L, 3L));
  783 + reqVO.setMemberUserIds(List.of(2L, 3L));
740 784  
741 785 groupService.removeGroupMember(1L, reqVO);
742 786  
743   - // 仅有效成员 2L 进入移除 / 已读清理,已退群的 3L 被跳过
  787 + // 仅有效成员 2L 进入移除,已退群的 3L 被跳过
744 788 ArgumentCaptor<Collection> removeCaptor = ArgumentCaptor.forClass(Collection.class);
745 789 verify(groupMemberService).removeGroupMembers(eq(10L), removeCaptor.capture());
746   - assertEquals(CollUtil.newHashSet(2L), CollUtil.newHashSet(removeCaptor.getValue()));
747   - ArgumentCaptor<Collection> readCaptor = ArgumentCaptor.forClass(Collection.class);
748   - verify(groupMessageService).deleteReadMaxMessageIds(eq(10L), readCaptor.capture());
749   - assertEquals(CollUtil.newHashSet(2L), CollUtil.newHashSet(readCaptor.getValue()));
  790 + assertEquals(Set.of(2L), Set.copyOf(removeCaptor.getValue()));
750 791 }
751 792 }
752 793  
... ... @@ -762,7 +803,7 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
762 803 .status(CommonStatusEnum.ENABLE.getStatus()).build();
763 804 when(groupMapper.selectByIdForUpdate(10L)).thenReturn(group);
764 805 // 目标 3 是普通成员
765   - when(groupMemberService.getGroupMembers(eq(10L), anyCollection())).thenReturn(ListUtil.of(
  806 + when(groupMemberService.getGroupMembers(eq(10L), anyCollection())).thenReturn(List.of(
766 807 ImGroupMemberDO.builder().userId(3L).status(CommonStatusEnum.ENABLE.getStatus())
767 808 .role(ImGroupMemberRoleEnum.NORMAL.getRole()).build()));
768 809 // 群里已有 1 个 ADMIN,1 + 1 ≤ 3 不超上限
... ... @@ -773,7 +814,7 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
773 814  
774 815 ImGroupAdminAddReqVO reqVO = new ImGroupAdminAddReqVO();
775 816 reqVO.setId(10L);
776   - reqVO.setUserIds(ListUtil.of(3L));
  817 + reqVO.setUserIds(List.of(3L));
777 818  
778 819 groupService.addGroupAdmin(1L, reqVO);
779 820  
... ... @@ -792,7 +833,7 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
792 833 ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(1L)
793 834 .status(CommonStatusEnum.ENABLE.getStatus()).build();
794 835 when(groupMapper.selectByIdForUpdate(10L)).thenReturn(group);
795   - when(groupMemberService.getGroupMembers(eq(10L), anyCollection())).thenReturn(ListUtil.of(
  836 + when(groupMemberService.getGroupMembers(eq(10L), anyCollection())).thenReturn(List.of(
796 837 ImGroupMemberDO.builder().userId(5L).status(CommonStatusEnum.ENABLE.getStatus())
797 838 .role(ImGroupMemberRoleEnum.NORMAL.getRole()).build()));
798 839 // 群里已有 3 个 ADMIN(达到上限),再加 1 会超
... ... @@ -801,7 +842,7 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
801 842  
802 843 ImGroupAdminAddReqVO reqVO = new ImGroupAdminAddReqVO();
803 844 reqVO.setId(10L);
804   - reqVO.setUserIds(ListUtil.of(5L));
  845 + reqVO.setUserIds(List.of(5L));
805 846  
806 847 ServiceException exception = assertThrows(ServiceException.class,
807 848 () -> groupService.addGroupAdmin(1L, reqVO));
... ... @@ -819,13 +860,13 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
819 860 ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(1L)
820 861 .status(CommonStatusEnum.ENABLE.getStatus()).build();
821 862 when(groupMapper.selectByIdForUpdate(10L)).thenReturn(group);
822   - when(groupMemberService.getGroupMembers(eq(10L), anyCollection())).thenReturn(ListUtil.of(
  863 + when(groupMemberService.getGroupMembers(eq(10L), anyCollection())).thenReturn(List.of(
823 864 ImGroupMemberDO.builder().userId(1L).status(CommonStatusEnum.ENABLE.getStatus())
824 865 .role(ImGroupMemberRoleEnum.OWNER.getRole()).build()));
825 866  
826 867 ImGroupAdminAddReqVO reqVO = new ImGroupAdminAddReqVO();
827 868 reqVO.setId(10L);
828   - reqVO.setUserIds(ListUtil.of(1L));
  869 + reqVO.setUserIds(List.of(1L));
829 870  
830 871 ServiceException exception = assertThrows(ServiceException.class,
831 872 () -> groupService.addGroupAdmin(1L, reqVO));
... ... @@ -843,13 +884,13 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
843 884 .status(CommonStatusEnum.ENABLE.getStatus()).build();
844 885 when(groupMapper.selectByIdForUpdate(10L)).thenReturn(group);
845 886 // 目标已是 ADMIN:再加无需操作
846   - when(groupMemberService.getGroupMembers(eq(10L), anyCollection())).thenReturn(ListUtil.of(
  887 + when(groupMemberService.getGroupMembers(eq(10L), anyCollection())).thenReturn(List.of(
847 888 ImGroupMemberDO.builder().userId(2L).status(CommonStatusEnum.ENABLE.getStatus())
848 889 .role(ImGroupMemberRoleEnum.ADMIN.getRole()).build()));
849 890  
850 891 ImGroupAdminAddReqVO reqVO = new ImGroupAdminAddReqVO();
851 892 reqVO.setId(10L);
852   - reqVO.setUserIds(ListUtil.of(2L));
  893 + reqVO.setUserIds(List.of(2L));
853 894  
854 895 groupService.addGroupAdmin(1L, reqVO);
855 896  
... ... @@ -869,7 +910,7 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
869 910 ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(1L)
870 911 .status(CommonStatusEnum.ENABLE.getStatus()).build();
871 912 when(groupMapper.selectByIdForUpdate(10L)).thenReturn(group);
872   - when(groupMemberService.getGroupMembers(eq(10L), anyCollection())).thenReturn(ListUtil.of(
  913 + when(groupMemberService.getGroupMembers(eq(10L), anyCollection())).thenReturn(List.of(
873 914 ImGroupMemberDO.builder().userId(2L).status(CommonStatusEnum.ENABLE.getStatus())
874 915 .role(ImGroupMemberRoleEnum.ADMIN.getRole()).build()));
875 916 when(groupMemberService.updateGroupMemberRole(eq(10L), anyCollection(),
... ... @@ -877,7 +918,7 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
877 918  
878 919 ImGroupAdminRemoveReqVO reqVO = new ImGroupAdminRemoveReqVO();
879 920 reqVO.setId(10L);
880   - reqVO.setUserIds(ListUtil.of(2L));
  921 + reqVO.setUserIds(List.of(2L));
881 922  
882 923 groupService.removeGroupAdmin(1L, reqVO);
883 924  
... ... @@ -897,13 +938,13 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
897 938 .status(CommonStatusEnum.ENABLE.getStatus()).build();
898 939 when(groupMapper.selectByIdForUpdate(10L)).thenReturn(group);
899 940 // 目标已是 MEMBER:撤销无需操作
900   - when(groupMemberService.getGroupMembers(eq(10L), anyCollection())).thenReturn(ListUtil.of(
  941 + when(groupMemberService.getGroupMembers(eq(10L), anyCollection())).thenReturn(List.of(
901 942 ImGroupMemberDO.builder().userId(2L).status(CommonStatusEnum.ENABLE.getStatus())
902 943 .role(ImGroupMemberRoleEnum.NORMAL.getRole()).build()));
903 944  
904 945 ImGroupAdminRemoveReqVO reqVO = new ImGroupAdminRemoveReqVO();
905 946 reqVO.setId(10L);
906   - reqVO.setUserIds(ListUtil.of(2L));
  947 + reqVO.setUserIds(List.of(2L));
907 948  
908 949 groupService.removeGroupAdmin(1L, reqVO);
909 950  
... ... @@ -925,9 +966,9 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
925 966 when(groupMemberService.validateMemberInGroup(10L, 2L)).thenReturn(
926 967 ImGroupMemberDO.builder().groupId(10L).userId(2L)
927 968 .role(ImGroupMemberRoleEnum.NORMAL.getRole()).build());
928   - when(groupMemberService.updateGroupMemberRole(eq(10L), eq(CollUtil.newHashSet(2L)),
  969 + when(groupMemberService.updateGroupMemberRole(eq(10L), eq(Set.of(2L)),
929 970 eq(ImGroupMemberRoleEnum.OWNER.getRole()))).thenReturn(1);
930   - when(groupMemberService.updateGroupMemberRole(eq(10L), eq(CollUtil.newHashSet(1L)),
  971 + when(groupMemberService.updateGroupMemberRole(eq(10L), eq(Set.of(1L)),
931 972 eq(ImGroupMemberRoleEnum.NORMAL.getRole()))).thenReturn(1);
932 973  
933 974 ImGroupTransferOwnerReqVO reqVO = new ImGroupTransferOwnerReqVO();
... ... @@ -941,9 +982,9 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
941 982 verify(groupMapper).updateById(groupCaptor.capture());
942 983 assertEquals(2L, groupCaptor.getValue().getOwnerUserId());
943 984 // 旧群主 → MEMBER;新群主 → OWNER
944   - verify(groupMemberService).updateGroupMemberRole(eq(10L), eq(CollUtil.newHashSet(1L)),
  985 + verify(groupMemberService).updateGroupMemberRole(eq(10L), eq(Set.of(1L)),
945 986 eq(ImGroupMemberRoleEnum.NORMAL.getRole()));
946   - verify(groupMemberService).updateGroupMemberRole(eq(10L), eq(CollUtil.newHashSet(2L)),
  987 + verify(groupMemberService).updateGroupMemberRole(eq(10L), eq(Set.of(2L)),
947 988 eq(ImGroupMemberRoleEnum.OWNER.getRole()));
948 989 }
949 990 }
... ... @@ -1111,9 +1152,7 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
1111 1152  
1112 1153 @Test
1113 1154 public void testGetMyGroupList_noMembers() {
1114   - when(groupMemberService.getActiveGroupMemberListByUserId(1L)).thenReturn(new ArrayList<>());
1115   - when(groupMemberService.getQuitGroupMemberListByUserId(eq(1L), any(LocalDateTime.class)))
1116   - .thenReturn(new ArrayList<>());
  1155 + when(groupMemberService.getGroupMemberListByUserId(1L)).thenReturn(new ArrayList<>());
1117 1156  
1118 1157 List<ImGroupDO> result = groupService.getMyGroupList(1L);
1119 1158 assertTrue(result.isEmpty());
... ... @@ -1122,20 +1161,16 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
1122 1161  
1123 1162 @Test
1124 1163 public void testGetMyGroupList_success() {
1125   - // 活跃群成员
1126   - when(groupMemberService.getActiveGroupMemberListByUserId(1L)).thenReturn(new ArrayList<>(ListUtil.of(
  1164 + // 曾经加入的所有群(含退群)
  1165 + when(groupMemberService.getGroupMemberListByUserId(1L)).thenReturn(new ArrayList<>(List.of(
1127 1166 ImGroupMemberDO.builder().groupId(10L).userId(1L)
1128 1167 .status(CommonStatusEnum.ENABLE.getStatus()).build(),
1129 1168 ImGroupMemberDO.builder().groupId(20L).userId(1L)
1130   - .status(CommonStatusEnum.ENABLE.getStatus()).build()
  1169 + .status(CommonStatusEnum.ENABLE.getStatus()).build(),
  1170 + ImGroupMemberDO.builder().groupId(30L).userId(1L)
  1171 + .status(CommonStatusEnum.DISABLE.getStatus()).build()
1131 1172 )));
1132   - // 最近退群成员(最近 30 天内)
1133   - when(groupMemberService.getQuitGroupMemberListByUserId(eq(1L), any(LocalDateTime.class)))
1134   - .thenReturn(new ArrayList<>(ListUtil.of(
1135   - ImGroupMemberDO.builder().groupId(30L).userId(1L)
1136   - .status(CommonStatusEnum.DISABLE.getStatus()).build()
1137   - )));
1138   - List<ImGroupDO> groups = ListUtil.of(
  1173 + List<ImGroupDO> groups = List.of(
1139 1174 ImGroupDO.builder().id(10L).status(CommonStatusEnum.ENABLE.getStatus()).build(),
1140 1175 ImGroupDO.builder().id(20L).status(CommonStatusEnum.ENABLE.getStatus()).build(),
1141 1176 ImGroupDO.builder().id(30L).status(CommonStatusEnum.ENABLE.getStatus()).build()
... ...
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/message/ImChannelMessageServiceImplTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.service.message;
  2 +
  3 +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
  4 +import cn.iocoder.yudao.module.im.dal.dataobject.message.ImChannelMessageDO;
  5 +import cn.iocoder.yudao.module.im.dal.mysql.message.ImChannelMessageMapper;
  6 +import cn.iocoder.yudao.module.im.service.channel.ImChannelMaterialService;
  7 +import cn.iocoder.yudao.module.im.service.conversation.ImConversationReadService;
  8 +import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService;
  9 +import org.junit.jupiter.api.Test;
  10 +import org.mockito.InjectMocks;
  11 +import org.mockito.Mock;
  12 +
  13 +import java.time.LocalDateTime;
  14 +import java.util.List;
  15 +
  16 +import static org.mockito.ArgumentMatchers.*;
  17 +import static org.mockito.Mockito.*;
  18 +
  19 +/**
  20 + * {@link ImChannelMessageServiceImpl} 的单元测试
  21 + *
  22 + * @author 芋道源码
  23 + */
  24 +public class ImChannelMessageServiceImplTest extends BaseMockitoUnitTest {
  25 +
  26 + @InjectMocks
  27 + private ImChannelMessageServiceImpl channelMessageService;
  28 +
  29 + @Mock
  30 + private ImChannelMessageMapper channelMessageMapper;
  31 + @Mock
  32 + private ImChannelMaterialService channelMaterialService;
  33 + @Mock
  34 + private ImWebSocketService webSocketService;
  35 + @Mock
  36 + private ImConversationReadService conversationReadService;
  37 +
  38 + @Test
  39 + public void testReadChannelMessages_messageNotExists() {
  40 + // 准备:messageId 不存在(伪造 / 未来 id)
  41 + when(channelMessageMapper.selectById(999L)).thenReturn(null);
  42 +
  43 + // 调用
  44 + channelMessageService.readChannelMessages(1L, 10L, 999L);
  45 +
  46 + // 断言:不推进读位置
  47 + verify(conversationReadService, never()).updateConversationReadPosition(anyLong(), anyInt(), anyLong(), anyLong());
  48 + }
  49 +
  50 + @Test
  51 + public void testReadChannelMessages_wrongChannel() {
  52 + // 准备:消息属于别的频道
  53 + ImChannelMessageDO message = ImChannelMessageDO.builder()
  54 + .id(100L).channelId(20L).sendTime(LocalDateTime.now()).build();
  55 + when(channelMessageMapper.selectById(100L)).thenReturn(message);
  56 +
  57 + // 调用:声称读的是频道 10
  58 + channelMessageService.readChannelMessages(1L, 10L, 100L);
  59 +
  60 + // 断言:不推进读位置
  61 + verify(conversationReadService, never()).updateConversationReadPosition(anyLong(), anyInt(), anyLong(), anyLong());
  62 + }
  63 +
  64 + @Test
  65 + public void testReadChannelMessages_notVisible() {
  66 + // 准备:定向消息,接收人不含当前用户
  67 + ImChannelMessageDO message = ImChannelMessageDO.builder()
  68 + .id(100L).channelId(10L).receiverUserIds(List.of(2L, 3L)).sendTime(LocalDateTime.now()).build();
  69 + when(channelMessageMapper.selectById(100L)).thenReturn(message);
  70 +
  71 + // 调用
  72 + channelMessageService.readChannelMessages(1L, 10L, 100L);
  73 +
  74 + // 断言:不推进读位置
  75 + verify(conversationReadService, never()).updateConversationReadPosition(anyLong(), anyInt(), anyLong(), anyLong());
  76 + }
  77 +
  78 + @Test
  79 + public void testReadChannelMessages_notAdvanced() {
  80 + // 准备:消息真实可见,但读位置未前进(updateConversationReadPosition 返回 false)
  81 + ImChannelMessageDO message = ImChannelMessageDO.builder()
  82 + .id(100L).channelId(10L).sendTime(LocalDateTime.now()).build();
  83 + when(channelMessageMapper.selectById(100L)).thenReturn(message);
  84 + when(conversationReadService.updateConversationReadPosition(anyLong(), anyInt(), anyLong(), anyLong()))
  85 + .thenReturn(false);
  86 +
  87 + // 调用
  88 + channelMessageService.readChannelMessages(1L, 10L, 100L);
  89 +
  90 + // 断言:读位置未前进 → 不推 READ 事件
  91 + verify(webSocketService, never()).sendNotificationAsync(anyLong(), anyInt(), anyInt(), any());
  92 + }
  93 +
  94 +}
... ...
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImplTest.java
1 1 package cn.iocoder.yudao.module.im.service.message;
2 2  
3   -import cn.hutool.core.collection.CollUtil;
4   -import cn.hutool.core.collection.ListUtil;
5 3 import cn.hutool.extra.spring.SpringUtil;
6 4 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
7 5 import cn.iocoder.yudao.framework.common.exception.ServiceException;
... ... @@ -12,19 +10,20 @@ import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO;
12 10 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO;
13 11 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO;
14 12 import cn.iocoder.yudao.module.im.dal.mysql.message.ImGroupMessageMapper;
15   -import cn.iocoder.yudao.module.im.dal.redis.message.ImGroupMessageReadRedisDAO;
  13 +import cn.iocoder.yudao.module.im.enums.ImConversationTypeEnum;
16 14 import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum;
17   -import cn.iocoder.yudao.module.im.enums.message.ImGroupMessageReceiptStatusEnum;
  15 +import cn.iocoder.yudao.module.im.enums.message.ImMessageReceiptStatusEnum;
18 16 import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum;
19   -import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
  17 +import cn.iocoder.yudao.module.im.enums.ImContentTypeEnum;
20 18 import cn.iocoder.yudao.module.im.framework.config.ImProperties;
  19 +import cn.iocoder.yudao.module.im.service.conversation.ImConversationReadService;
21 20 import cn.iocoder.yudao.module.im.service.group.ImGroupMemberService;
22 21 import cn.iocoder.yudao.module.im.service.group.ImGroupService;
23 22 import cn.iocoder.yudao.module.im.service.message.dto.ImGroupMessageSendDTO;
24 23 import cn.iocoder.yudao.module.im.service.sensitiveword.ImSensitiveWordService;
25 24 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService;
26   -import cn.iocoder.yudao.module.im.service.websocket.dto.ImGroupMessageDTO;
27   -import cn.iocoder.yudao.module.im.service.websocket.dto.message.RecallMessage;
  25 +import cn.iocoder.yudao.module.im.service.websocket.notification.message.ImGroupMessageNotification;
  26 +import cn.iocoder.yudao.module.im.dal.dataobject.message.content.RecallMessage;
28 27 import org.junit.jupiter.api.Test;
29 28 import org.mockito.ArgumentCaptor;
30 29 import org.mockito.InjectMocks;
... ... @@ -41,7 +40,7 @@ import static org.mockito.ArgumentMatchers.*;
41 40 import static org.mockito.Mockito.*;
42 41  
43 42 /**
44   - * IM 群聊消息 Service 单元测试
  43 + * {@link ImGroupMessageServiceImpl} 的单元测试
45 44 *
46 45 * @author 芋道源码
47 46 */
... ... @@ -59,11 +58,10 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
59 58 @Mock
60 59 private ImSensitiveWordService sensitiveWordService;
61 60 @Mock
62   - private ImGroupMessageReadRedisDAO groupMessageReadRedisDAO;
  61 + private ImConversationReadService conversationReadService;
63 62 @Mock
64 63 private ImWebSocketService imWebSocketService;
65 64  
66   - /** 用真实实例避免 NPE;默认值与生产保持一致(recallTimeoutMinutes=5、private/group read enabled=true、maxPullSize=1000);个别用例可改字段测分支 */
67 65 @Spy
68 66 private ImProperties imProperties = new ImProperties();
69 67  
... ... @@ -71,7 +69,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
71 69 ImGroupMessageSendReqVO reqVO = new ImGroupMessageSendReqVO();
72 70 reqVO.setClientMessageId("test-uuid-group-001");
73 71 reqVO.setGroupId(10L);
74   - reqVO.setType(ImMessageTypeEnum.TEXT.getType());
  72 + reqVO.setType(ImContentTypeEnum.TEXT.getType());
75 73 reqVO.setContent("{\"content\":\"群聊你好\"}");
76 74 return reqVO;
77 75 }
... ... @@ -86,6 +84,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
86 84  
87 85 // 准备
88 86 ImGroupMessageSendReqVO reqVO = buildSendReqVO();
  87 + reqVO.setReceipt(true);
89 88 when(groupMessageMapper.selectBySenderIdAndClientMessageId(1L, "test-uuid-group-001"))
90 89 .thenReturn(null);
91 90 ImGroupDO group = new ImGroupDO();
... ... @@ -98,7 +97,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
98 97 when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn(member);
99 98  
100 99 when(groupMemberService.getActiveGroupMemberUserIdsByGroupId(10L))
101   - .thenReturn(ListUtil.of(1L, 2L, 3L));
  100 + .thenReturn(List.of(1L, 2L, 3L));
102 101 when(groupMessageMapper.insert(any(ImGroupMessageDO.class))).thenAnswer(invocation -> {
103 102 ImGroupMessageDO msg = invocation.getArgument(0);
104 103 msg.setId(99L);
... ... @@ -112,13 +111,56 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
112 111 assertNotNull(result);
113 112 assertEquals(1L, result.getSenderId());
114 113 assertEquals(10L, result.getGroupId());
115   - assertEquals(ImMessageStatusEnum.UNREAD.getStatus(), result.getStatus());
116   - assertEquals(ImGroupMessageReceiptStatusEnum.NO_RECEIPT.getStatus(), result.getReceiptStatus());
  114 + assertEquals(ImMessageStatusEnum.NORMAL.getStatus(), result.getStatus());
  115 + assertEquals(ImMessageReceiptStatusEnum.PENDING.getStatus(), result.getReceiptStatus());
117 116  
118 117 // 验证推送给 3 个群成员(含发送者自己,用于多端同步)
119   - verify(imWebSocketService).sendGroupMessageAsync(argThat((Collection<Long> ids) ->
  118 + ArgumentCaptor<ImGroupMessageNotification> payloadCaptor =
  119 + ArgumentCaptor.forClass(ImGroupMessageNotification.class);
  120 + verify(imWebSocketService).sendNotificationAsync(argThat((Collection<Long> ids) ->
120 121 ids.size() == 3 && ids.contains(1L) && ids.contains(2L) && ids.contains(3L)),
121   - any(ImGroupMessageDTO.class));
  122 + eq(ImConversationTypeEnum.GROUP.getType()), eq(ImContentTypeEnum.TEXT.getType()),
  123 + payloadCaptor.capture());
  124 + ImGroupMessageNotification payload = payloadCaptor.getValue();
  125 + assertEquals(ImMessageReceiptStatusEnum.PENDING.getStatus(), payload.getReceiptStatus());
  126 + assertEquals(0, payload.getReadCount());
  127 + assertEquals(List.of(1L, 2L, 3L), payload.getReceiverUserIds());
  128 + }
  129 + }
  130 +
  131 + @Test
  132 + public void testSendMessage_senderAlwaysVisible() {
  133 + // 边界:成员缓存漏掉发送者(理论上 validateMemberInGroup 已挡,这里纯防御);发送者仍必须固化进 receiver_user_ids
  134 + try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
  135 + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupMessageServiceImpl.class)))
  136 + .thenReturn(groupMessageService);
  137 +
  138 + ImGroupMessageSendReqVO reqVO = buildSendReqVO();
  139 + when(groupMessageMapper.selectBySenderIdAndClientMessageId(1L, "test-uuid-group-001"))
  140 + .thenReturn(null);
  141 + ImGroupDO group = new ImGroupDO();
  142 + group.setId(10L);
  143 + group.setName("测试群");
  144 + when(groupService.validateGroupExists(10L)).thenReturn(group);
  145 + ImGroupMemberDO member = ImGroupMemberDO.builder()
  146 + .groupId(10L).userId(1L).status(CommonStatusEnum.ENABLE.getStatus()).build();
  147 + when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn(member);
  148 + // 成员缓存只返回 {2,3},漏掉发送者 1
  149 + when(groupMemberService.getActiveGroupMemberUserIdsByGroupId(10L))
  150 + .thenReturn(List.of(2L, 3L));
  151 + when(groupMessageMapper.insert(any(ImGroupMessageDO.class))).thenAnswer(invocation -> {
  152 + ImGroupMessageDO msg = invocation.getArgument(0);
  153 + msg.setId(99L);
  154 + return 1;
  155 + });
  156 +
  157 + ImGroupMessageDO result = groupMessageService.sendGroupMessage(1L, reqVO);
  158 +
  159 + // 断言:固化快照必含发送者,推送目标补回发送者 = {1,2,3}
  160 + assertTrue(result.getReceiverUserIds().contains(1L), "发送者必须在 receiver_user_ids 快照内");
  161 + verify(imWebSocketService).sendNotificationAsync(argThat((Collection<Long> ids) ->
  162 + ids.size() == 3 && ids.contains(1L) && ids.contains(2L) && ids.contains(3L)),
  163 + anyInt(), anyInt(), any());
122 164 }
123 165 }
124 166  
... ... @@ -159,375 +201,122 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
159 201 assertEquals(GROUP_MEMBER_NOT_IN_GROUP.getCode(), exception.getCode());
160 202 }
161 203  
162   - // ========== pull 测试 ==========
163   -
164 204 @Test
165   - public void testPullMessages_joinTimeFilter() {
166   - // 准备:消息时间用"最近 1 天内"避免被 30 天窗口过滤
167   - LocalDateTime now = LocalDateTime.now();
168   - LocalDateTime joinTime = now.minusHours(2);
169   - ImGroupMemberDO member = ImGroupMemberDO.builder()
170   - .groupId(10L).userId(1L)
171   - .status(CommonStatusEnum.ENABLE.getStatus())
172   - .joinTime(joinTime).build();
173   - when(groupMemberService.getActiveGroupMemberListByUserId(1L)).thenReturn(ListUtil.of(member));
174   -
175   - // 入群前 1 条 + 入群后 1 条
176   - ImGroupMessageDO beforeJoin = ImGroupMessageDO.builder()
177   - .id(1L).groupId(10L).senderId(2L)
178   - .sendTime(now.minusHours(3)).build();
179   - ImGroupMessageDO afterJoin = ImGroupMessageDO.builder()
180   - .id(2L).groupId(10L).senderId(2L)
181   - .sendTime(now.minusHours(1)).build();
182   - when(groupMessageMapper.selectListByMinId(eq(ListUtil.of(10L)), eq(0L),
183   - any(LocalDateTime.class), eq(100)))
184   - .thenReturn(ListUtil.of(beforeJoin, afterJoin));
185   - when(groupMemberService.getQuitGroupMemberListByUserId(eq(1L), any(LocalDateTime.class)))
186   - .thenReturn(ListUtil.of());
187   -
188   - // 调用
189   - List<ImGroupMessageDO> result = groupMessageService.pullGroupMessageList(1L, 0L, 100);
190   -
191   - // 断言:入群前消息不可见
192   - assertEquals(1, result.size());
193   - assertEquals(2L, result.get(0).getId());
194   - }
195   -
196   - @Test
197   - public void testPullMessages_activeQueryRetriesWhenFirstBatchInvisible() {
198   - // 准备:首批 2 条(入群前 + 定向给别人)全部不可见,第二批命中可见消息
199   - LocalDateTime now = LocalDateTime.now();
200   - LocalDateTime joinTime = now.minusHours(1);
201   - ImGroupMemberDO member = ImGroupMemberDO.builder()
202   - .groupId(10L).userId(1L)
203   - .status(CommonStatusEnum.ENABLE.getStatus())
204   - .joinTime(joinTime).build();
205   - when(groupMemberService.getActiveGroupMemberListByUserId(1L)).thenReturn(ListUtil.of(member));
206   -
207   - ImGroupMessageDO beforeJoin = ImGroupMessageDO.builder()
208   - .id(1L).groupId(10L).senderId(2L)
209   - .sendTime(now.minusHours(3)).build();
210   - ImGroupMessageDO directedMsg = ImGroupMessageDO.builder()
211   - .id(2L).groupId(10L).senderId(2L)
212   - .receiverUserIds(ListUtil.of(2L, 3L))
213   - .sendTime(now.minusHours(2)).build();
214   - ImGroupMessageDO visibleMsg = ImGroupMessageDO.builder()
215   - .id(3L).groupId(10L).senderId(2L)
216   - .sendTime(now.minusMinutes(30)).build();
217   - when(groupMessageMapper.selectListByMinId(eq(ListUtil.of(10L)), eq(0L),
218   - any(LocalDateTime.class), eq(2)))
219   - .thenReturn(ListUtil.of(beforeJoin, directedMsg));
220   - when(groupMessageMapper.selectListByMinId(eq(ListUtil.of(10L)), eq(2L),
221   - any(LocalDateTime.class), eq(2)))
222   - .thenReturn(ListUtil.of(visibleMsg));
223   - when(groupMemberService.getQuitGroupMemberListByUserId(eq(1L), any(LocalDateTime.class)))
224   - .thenReturn(ListUtil.of());
225   -
226   - // 调用
227   - List<ImGroupMessageDO> result = groupMessageService.pullGroupMessageList(1L, 0L, 2);
228   -
229   - // 断言:仅返回第二批的可见消息;两批 selectListByMinId 各被调用一次
230   - assertEquals(1, result.size());
231   - assertEquals(3L, result.get(0).getId());
232   - verify(groupMessageMapper).selectListByMinId(eq(ListUtil.of(10L)), eq(0L),
233   - any(LocalDateTime.class), eq(2));
234   - verify(groupMessageMapper).selectListByMinId(eq(ListUtil.of(10L)), eq(2L),
235   - any(LocalDateTime.class), eq(2));
236   - }
237   -
238   - @Test
239   - public void testPullMessages_quitMemberLoadsPreQuitMessages() {
240   - // 准备:当前群(在群)+ 一个最近退群
241   - LocalDateTime now = LocalDateTime.now();
242   - ImGroupMemberDO activeMember = ImGroupMemberDO.builder()
243   - .groupId(10L).userId(1L)
244   - .status(CommonStatusEnum.ENABLE.getStatus())
245   - .joinTime(now.minusDays(10)).build();
246   - when(groupMemberService.getActiveGroupMemberListByUserId(1L)).thenReturn(ListUtil.of(activeMember));
247   -
248   - ImGroupMessageDO activeMsg = ImGroupMessageDO.builder()
249   - .id(5L).groupId(10L).senderId(2L)
250   - .sendTime(now.minusHours(2)).build();
251   - when(groupMessageMapper.selectListByMinId(eq(ListUtil.of(10L)), eq(0L),
252   - any(LocalDateTime.class), eq(100)))
253   - .thenReturn(ListUtil.of(activeMsg));
254   -
255   - // 已退群:退群时间在窗口内(minId=0,minQuitTime 直接用 minSendTime)
256   - LocalDateTime quitTime = now.minusDays(3);
257   - ImGroupMemberDO quitMember = ImGroupMemberDO.builder()
258   - .groupId(20L).userId(1L)
259   - .status(CommonStatusEnum.DISABLE.getStatus())
260   - .joinTime(now.minusDays(20))
261   - .quitTime(quitTime).build();
262   - when(groupMemberService.getQuitGroupMemberListByUserId(eq(1L), any(LocalDateTime.class)))
263   - .thenReturn(ListUtil.of(quitMember));
264   -
265   - ImGroupMessageDO quitGroupMsg = ImGroupMessageDO.builder()
266   - .id(3L).groupId(20L).senderId(99L)
267   - .sendTime(now.minusDays(5)).build();
268   - when(groupMessageMapper.selectListByGroupIdAndMinIdAndQuitTimeBefore(eq(20L), eq(0L),
269   - any(LocalDateTime.class), eq(quitTime), eq(100)))
270   - .thenReturn(ListUtil.of(quitGroupMsg));
271   -
272   - // 调用
273   - List<ImGroupMessageDO> result = groupMessageService.pullGroupMessageList(1L, 0L, 100);
274   -
275   - // 断言:两条消息均返回,并按 id 升序
276   - assertEquals(2, result.size());
277   - assertEquals(3L, result.get(0).getId());
278   - assertEquals(5L, result.get(1).getId());
279   - }
280   -
281   - @Test
282   - public void testPullMessages_quitMemberFilteredByBoundarySendTime() {
283   - // 准备:minId > 0,边界消息 sendTime 晚于窗口起点,应当被用作 minQuitTime
284   - LocalDateTime now = LocalDateTime.now();
285   - when(groupMemberService.getActiveGroupMemberListByUserId(1L)).thenReturn(ListUtil.of());
286   -
287   - LocalDateTime boundarySendTime = now.minusDays(2);
288   - ImGroupMessageDO boundary = ImGroupMessageDO.builder()
289   - .id(8L).groupId(10L).senderId(2L).sendTime(boundarySendTime).build();
290   - when(groupMessageMapper.selectById(8L)).thenReturn(boundary);
291   - when(groupMemberService.getQuitGroupMemberListByUserId(1L, boundarySendTime))
292   - .thenReturn(ListUtil.of());
293   -
294   - // 调用
295   - List<ImGroupMessageDO> result = groupMessageService.pullGroupMessageList(1L, 8L, 100);
296   -
297   - // 断言:使用边界消息 sendTime 抬升 minQuitTime
298   - assertEquals(0, result.size());
299   - verify(groupMemberService).getQuitGroupMemberListByUserId(1L, boundarySendTime);
300   - }
301   -
302   - @Test
303   - public void testPullMessages_quitQueryKeepsOriginalMinIdWhenActiveQueryRetries() {
304   - // 准备:主查询首批不可见触发重试(游标从 8→101),退群补齐仍使用原始 minId=8
305   - LocalDateTime now = LocalDateTime.now();
306   - LocalDateTime joinTime = now.minusHours(1);
307   - ImGroupMemberDO activeMember = ImGroupMemberDO.builder()
308   - .groupId(10L).userId(1L)
309   - .status(CommonStatusEnum.ENABLE.getStatus())
310   - .joinTime(joinTime).build();
311   - when(groupMemberService.getActiveGroupMemberListByUserId(1L)).thenReturn(ListUtil.of(activeMember));
312   -
313   - ImGroupMessageDO beforeJoin = ImGroupMessageDO.builder()
314   - .id(100L).groupId(10L).senderId(2L)
315   - .sendTime(now.minusHours(3)).build();
316   - ImGroupMessageDO directedMsg = ImGroupMessageDO.builder()
317   - .id(101L).groupId(10L).senderId(2L)
318   - .receiverUserIds(ListUtil.of(2L, 3L))
319   - .sendTime(now.minusMinutes(50)).build();
320   - ImGroupMessageDO visibleActiveMsg = ImGroupMessageDO.builder()
321   - .id(102L).groupId(10L).senderId(2L)
322   - .sendTime(now.minusMinutes(30)).build();
323   - when(groupMessageMapper.selectListByMinId(eq(ListUtil.of(10L)), eq(8L),
324   - any(LocalDateTime.class), eq(2)))
325   - .thenReturn(ListUtil.of(beforeJoin, directedMsg));
326   - when(groupMessageMapper.selectListByMinId(eq(ListUtil.of(10L)), eq(101L),
327   - any(LocalDateTime.class), eq(2)))
328   - .thenReturn(ListUtil.of(visibleActiveMsg));
329   -
330   - LocalDateTime boundarySendTime = now.minusDays(2);
331   - ImGroupMessageDO boundary = ImGroupMessageDO.builder()
332   - .id(8L).groupId(99L).senderId(2L).sendTime(boundarySendTime).build();
333   - when(groupMessageMapper.selectById(8L)).thenReturn(boundary);
334   -
335   - LocalDateTime quitTime = now.minusHours(12);
336   - ImGroupMemberDO quitMember = ImGroupMemberDO.builder()
337   - .groupId(20L).userId(1L)
338   - .status(CommonStatusEnum.DISABLE.getStatus())
339   - .joinTime(now.minusDays(10))
340   - .quitTime(quitTime).build();
341   - when(groupMemberService.getQuitGroupMemberListByUserId(1L, boundarySendTime))
342   - .thenReturn(ListUtil.of(quitMember));
343   -
344   - ImGroupMessageDO quitGroupMsg = ImGroupMessageDO.builder()
345   - .id(50L).groupId(20L).senderId(99L)
346   - .sendTime(now.minusDays(1)).build();
347   - when(groupMessageMapper.selectListByGroupIdAndMinIdAndQuitTimeBefore(eq(20L), eq(8L),
348   - any(LocalDateTime.class), eq(quitTime), eq(2)))
349   - .thenReturn(ListUtil.of(quitGroupMsg));
350   -
351   - // 调用
352   - List<ImGroupMessageDO> result = groupMessageService.pullGroupMessageList(1L, 8L, 2);
353   -
354   - // 断言:退群补齐使用原始 minId=8 而非主查询重试后的 101
355   - assertEquals(2, result.size());
356   - assertEquals(50L, result.get(0).getId());
357   - assertEquals(102L, result.get(1).getId());
358   - verify(groupMessageMapper).selectListByMinId(eq(ListUtil.of(10L)), eq(8L),
359   - any(LocalDateTime.class), eq(2));
360   - verify(groupMessageMapper).selectListByMinId(eq(ListUtil.of(10L)), eq(101L),
361   - any(LocalDateTime.class), eq(2));
362   - verify(groupMemberService).getQuitGroupMemberListByUserId(1L, boundarySendTime);
363   - verify(groupMessageMapper).selectListByGroupIdAndMinIdAndQuitTimeBefore(eq(20L), eq(8L),
364   - any(LocalDateTime.class), eq(quitTime), eq(2));
365   - verify(groupMessageMapper, never()).selectListByGroupIdAndMinIdAndQuitTimeBefore(eq(20L), eq(101L),
366   - any(LocalDateTime.class), eq(quitTime), eq(2));
367   - }
368   -
369   - @Test
370   - public void testPullMessages_receiverUserIdsFilter() {
371   - // 准备
372   - LocalDateTime now = LocalDateTime.now();
373   - ImGroupMemberDO member = ImGroupMemberDO.builder()
374   - .groupId(10L).userId(1L)
375   - .status(CommonStatusEnum.ENABLE.getStatus())
376   - .joinTime(now.minusDays(10)).build();
377   - when(groupMemberService.getActiveGroupMemberListByUserId(1L)).thenReturn(ListUtil.of(member));
378   -
379   - // 定向接收消息:只给用户 2 和 3
380   - ImGroupMessageDO directedMsg = ImGroupMessageDO.builder()
381   - .id(1L).groupId(10L).senderId(5L)
382   - .receiverUserIds(ListUtil.of(2L, 3L))
383   - .sendTime(now.minusHours(2)).build();
384   - // 全员消息
385   - ImGroupMessageDO allMsg = ImGroupMessageDO.builder()
386   - .id(2L).groupId(10L).senderId(5L)
387   - .sendTime(now.minusHours(1)).build();
388   - when(groupMessageMapper.selectListByMinId(eq(ListUtil.of(10L)), eq(0L),
389   - any(LocalDateTime.class), eq(100)))
390   - .thenReturn(ListUtil.of(directedMsg, allMsg));
391   - when(groupMemberService.getQuitGroupMemberListByUserId(eq(1L), any(LocalDateTime.class)))
392   - .thenReturn(ListUtil.of());
393   -
394   - // 调用
395   - List<ImGroupMessageDO> result = groupMessageService.pullGroupMessageList(1L, 0L, 100);
  205 + public void testSendMessage_quoteDirectedMessageRejected() {
  206 + // 准备:发送人 1 在群 10,当前有效成员(广播受众)= {1,2,3}
  207 + ImGroupMessageSendReqVO reqVO = buildSendReqVO();
  208 + reqVO.setContent("{\"content\":\"引用\",\"quote\":{\"messageId\":500}}");
  209 + when(groupMessageMapper.selectBySenderIdAndClientMessageId(1L, "test-uuid-group-001")).thenReturn(null);
  210 + ImGroupDO group = new ImGroupDO();
  211 + group.setId(10L);
  212 + when(groupService.validateGroupExists(10L)).thenReturn(group);
  213 + ImGroupMemberDO member = ImGroupMemberDO.builder().groupId(10L).userId(1L)
  214 + .status(CommonStatusEnum.ENABLE.getStatus()).build();
  215 + when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn(member);
  216 + when(groupMemberService.getActiveGroupMemberUserIdsByGroupId(10L)).thenReturn(List.of(1L, 2L, 3L));
  217 + // 被引用原消息:定向给 {1,2}(对发送人 1 可见,但不覆盖广播受众 {1,2,3})
  218 + ImGroupMessageDO original = ImGroupMessageDO.builder().id(500L).groupId(10L).senderId(2L)
  219 + .status(ImMessageStatusEnum.NORMAL.getStatus()).receiverUserIds(List.of(1L, 2L))
  220 + .type(ImContentTypeEnum.TEXT.getType()).content("{\"content\":\"密\"}")
  221 + .sendTime(LocalDateTime.now()).build();
  222 + when(groupMessageMapper.selectById(500L)).thenReturn(original);
396 223  
397   - // 断言:定向接收的消息用户 1 看不到,只能看到全员消息
398   - assertEquals(1, result.size());
399   - assertEquals(2L, result.get(0).getId());
  224 + // 调用并断言:引用定向消息被拒绝,避免原文随广播泄漏给全群
  225 + ServiceException exception = assertThrows(ServiceException.class,
  226 + () -> groupMessageService.sendGroupMessage(1L, reqVO));
  227 + assertEquals(MESSAGE_QUOTE_INVALID.getCode(), exception.getCode());
  228 + verify(groupMessageMapper, never()).insert(any(ImGroupMessageDO.class));
400 229 }
401 230  
402 231 @Test
403   - public void testPullMessages_atFieldCorrectReturn() {
404   - // 准备
405   - LocalDateTime now = LocalDateTime.now();
406   - ImGroupMemberDO member = ImGroupMemberDO.builder()
407   - .groupId(10L).userId(1L)
408   - .status(CommonStatusEnum.ENABLE.getStatus())
409   - .joinTime(now.minusDays(10)).build();
410   - when(groupMemberService.getActiveGroupMemberListByUserId(1L)).thenReturn(ListUtil.of(member));
411   -
412   - ImGroupMessageDO atMsg = ImGroupMessageDO.builder()
413   - .id(1L).groupId(10L).senderId(2L)
414   - .atUserIds(ListUtil.of(1L, 3L))
415   - .sendTime(now.minusHours(1)).build();
416   - when(groupMessageMapper.selectListByMinId(eq(ListUtil.of(10L)), eq(0L),
417   - any(LocalDateTime.class), eq(100)))
418   - .thenReturn(ListUtil.of(atMsg));
419   - when(groupMemberService.getQuitGroupMemberListByUserId(eq(1L), any(LocalDateTime.class)))
420   - .thenReturn(ListUtil.of());
421   -
422   - // 调用
423   - List<ImGroupMessageDO> result = groupMessageService.pullGroupMessageList(1L, 0L, 100);
  232 + public void testSendMessage_quoteBroadcastAccepted() {
  233 + // 准备:广播受众 {1,2,3},被引用原消息快照覆盖全部受众 → 允许引用
  234 + ImGroupMessageSendReqVO reqVO = buildSendReqVO();
  235 + reqVO.setContent("{\"content\":\"引用\",\"quote\":{\"messageId\":500}}");
  236 + when(groupMessageMapper.selectBySenderIdAndClientMessageId(1L, "test-uuid-group-001")).thenReturn(null);
  237 + ImGroupDO group = new ImGroupDO();
  238 + group.setId(10L);
  239 + when(groupService.validateGroupExists(10L)).thenReturn(group);
  240 + ImGroupMemberDO member = ImGroupMemberDO.builder().groupId(10L).userId(1L)
  241 + .status(CommonStatusEnum.ENABLE.getStatus()).build();
  242 + when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn(member);
  243 + when(groupMemberService.getActiveGroupMemberUserIdsByGroupId(10L)).thenReturn(List.of(1L, 2L, 3L));
  244 + ImGroupMessageDO original = ImGroupMessageDO.builder().id(500L).groupId(10L).senderId(2L)
  245 + .status(ImMessageStatusEnum.NORMAL.getStatus()).receiverUserIds(List.of(1L, 2L, 3L))
  246 + .type(ImContentTypeEnum.TEXT.getType()).content("{\"content\":\"公开\"}")
  247 + .sendTime(LocalDateTime.now()).build();
  248 + when(groupMessageMapper.selectById(500L)).thenReturn(original);
  249 + when(groupMessageMapper.insert(any(ImGroupMessageDO.class))).thenAnswer(invocation -> {
  250 + invocation.<ImGroupMessageDO>getArgument(0).setId(99L);
  251 + return 1;
  252 + });
424 253  
425   - // 断言:@ 字段正确返回
426   - assertEquals(1, result.size());
427   - assertEquals(ListUtil.of(1L, 3L), result.get(0).getAtUserIds());
  254 + // 调用并断言:正常入库
  255 + ImGroupMessageDO result = groupMessageService.sendGroupMessage(1L, reqVO);
  256 + assertEquals(99L, result.getId());
  257 + verify(groupMessageMapper).insert(any(ImGroupMessageDO.class));
428 258 }
429 259  
430   - @Test
431   - public void testPullMessages_pageTrimmedToSize() {
432   - LocalDateTime now = LocalDateTime.now();
433   - ImGroupMemberDO activeMember = ImGroupMemberDO.builder()
434   - .groupId(10L).userId(1L)
435   - .status(CommonStatusEnum.ENABLE.getStatus())
436   - .joinTime(now.minusDays(10)).build();
437   - when(groupMemberService.getActiveGroupMemberListByUserId(1L)).thenReturn(ListUtil.of(activeMember));
438   -
439   - ImGroupMessageDO activeMsg1 = ImGroupMessageDO.builder()
440   - .id(1000L).groupId(10L).senderId(2L).sendTime(now.minusHours(2)).build();
441   - ImGroupMessageDO activeMsg2 = ImGroupMessageDO.builder()
442   - .id(2000L).groupId(10L).senderId(2L).sendTime(now.minusHours(1)).build();
443   - when(groupMessageMapper.selectListByMinId(eq(ListUtil.of(10L)), eq(0L),
444   - any(LocalDateTime.class), eq(2)))
445   - .thenReturn(ListUtil.of(activeMsg1, activeMsg2));
446   -
447   - LocalDateTime quitTime = now.minusDays(1);
448   - ImGroupMemberDO quitMember = ImGroupMemberDO.builder()
449   - .groupId(20L).userId(1L)
450   - .status(CommonStatusEnum.DISABLE.getStatus())
451   - .joinTime(now.minusDays(20))
452   - .quitTime(quitTime).build();
453   - when(groupMemberService.getQuitGroupMemberListByUserId(eq(1L), any(LocalDateTime.class)))
454   - .thenReturn(ListUtil.of(quitMember));
455   -
456   - ImGroupMessageDO quit1 = ImGroupMessageDO.builder()
457   - .id(150L).groupId(20L).senderId(99L).sendTime(now.minusDays(5)).build();
458   - ImGroupMessageDO quit2 = ImGroupMessageDO.builder()
459   - .id(160L).groupId(20L).senderId(99L).sendTime(now.minusDays(4)).build();
460   - when(groupMessageMapper.selectListByGroupIdAndMinIdAndQuitTimeBefore(eq(20L), eq(0L),
461   - any(LocalDateTime.class), eq(quitTime), eq(2)))
462   - .thenReturn(ListUtil.of(quit1, quit2));
463   -
464   - when(groupMessageReadRedisDAO.getReadMaxMessageId(anyLong(), eq(1L))).thenReturn(null);
465   -
466   - List<ImGroupMessageDO> result = groupMessageService.pullGroupMessageList(1L, 0L, 2);
467   -
468   - assertEquals(2, result.size());
469   - assertEquals(150L, result.get(0).getId());
470   - assertEquals(160L, result.get(1).getId());
471   - }
  260 + // ========== pull 测试 ==========
472 261  
473 262 @Test
474   - public void testPullMessages_pullSetsReadFromRedisCursor() {
  263 + public void testPullMessages_doesNotOverwriteStatus() {
  264 + // 准备:拉到两条消息;Phase 2 起 pull 不再按读位置覆盖 status,已读由前端按读位置判断
475 265 LocalDateTime now = LocalDateTime.now();
476 266 ImGroupMemberDO member = ImGroupMemberDO.builder()
477 267 .groupId(10L).userId(1L)
478 268 .status(CommonStatusEnum.ENABLE.getStatus())
479 269 .joinTime(now.minusDays(10)).build();
480   - when(groupMemberService.getActiveGroupMemberListByUserId(1L)).thenReturn(ListUtil.of(member));
  270 + when(groupMemberService.getGroupMemberListByUserId(1L)).thenReturn(List.of(member));
481 271  
482 272 ImGroupMessageDO low = ImGroupMessageDO.builder()
483   - .id(5L).groupId(10L).senderId(2L).status(ImMessageStatusEnum.UNREAD.getStatus())
  273 + .id(5L).groupId(10L).senderId(2L).receiverUserIds(List.of(1L))
  274 + .status(ImMessageStatusEnum.NORMAL.getStatus())
484 275 .sendTime(now.minusHours(2)).build();
485 276 ImGroupMessageDO high = ImGroupMessageDO.builder()
486   - .id(10L).groupId(10L).senderId(2L).status(ImMessageStatusEnum.UNREAD.getStatus())
  277 + .id(10L).groupId(10L).senderId(2L).receiverUserIds(List.of(1L))
  278 + .status(ImMessageStatusEnum.NORMAL.getStatus())
487 279 .sendTime(now.minusHours(1)).build();
488   - when(groupMessageMapper.selectListByMinId(eq(ListUtil.of(10L)), eq(0L),
  280 + when(groupMessageMapper.selectListByMinId(eq(1L), anyCollection(), eq(0L),
489 281 any(LocalDateTime.class), eq(100)))
490   - .thenReturn(ListUtil.of(low, high));
491   - when(groupMemberService.getQuitGroupMemberListByUserId(eq(1L), any(LocalDateTime.class)))
492   - .thenReturn(ListUtil.of());
493   -
494   - when(groupMessageReadRedisDAO.getReadMaxMessageId(10L, 1L)).thenReturn(7L);
  282 + .thenReturn(List.of(low, high));
495 283  
496 284 List<ImGroupMessageDO> result = groupMessageService.pullGroupMessageList(1L, 0L, 100);
497 285  
  286 + // 断言:status 保持 DB 原值(都 NORMAL),pull 不覆盖
498 287 assertEquals(2, result.size());
499   - assertEquals(ImMessageStatusEnum.READ.getStatus(), result.get(0).getStatus());
500   - assertEquals(ImMessageStatusEnum.UNREAD.getStatus(), result.get(1).getStatus());
  288 + assertEquals(ImMessageStatusEnum.NORMAL.getStatus(), result.get(0).getStatus());
  289 + assertEquals(ImMessageStatusEnum.NORMAL.getStatus(), result.get(1).getStatus());
501 290 }
502 291  
503 292 @Test
504 293 public void testPullMessages_receiptReadCountForSender() {
  294 + // 准备:本人发送的回执消息,可见成员 {1,2,3},分母排除发送者后为 {2,3}
505 295 LocalDateTime now = LocalDateTime.now();
506 296 ImGroupMemberDO member = ImGroupMemberDO.builder()
507 297 .groupId(10L).userId(1L)
508 298 .status(CommonStatusEnum.ENABLE.getStatus())
509 299 .joinTime(now.minusDays(10)).build();
510   - when(groupMemberService.getActiveGroupMemberListByUserId(1L)).thenReturn(ListUtil.of(member));
  300 + when(groupMemberService.getGroupMemberListByUserId(1L)).thenReturn(List.of(member));
511 301  
512 302 ImGroupMessageDO receiptMsg = ImGroupMessageDO.builder()
513 303 .id(100L).groupId(10L).senderId(1L)
514   - .status(ImMessageStatusEnum.UNREAD.getStatus())
515   - .receiptStatus(ImGroupMessageReceiptStatusEnum.PENDING.getStatus())
  304 + .receiverUserIds(List.of(1L, 2L, 3L))
  305 + .status(ImMessageStatusEnum.NORMAL.getStatus())
  306 + .receiptStatus(ImMessageReceiptStatusEnum.PENDING.getStatus())
516 307 .sendTime(now.minusHours(1)).build();
517   - when(groupMessageMapper.selectListByMinId(eq(ListUtil.of(10L)), eq(0L),
  308 + when(groupMessageMapper.selectListByMinId(eq(1L), anyCollection(), eq(0L),
518 309 any(LocalDateTime.class), eq(100)))
519   - .thenReturn(ListUtil.of(receiptMsg));
520   - when(groupMemberService.getQuitGroupMemberListByUserId(eq(1L), any(LocalDateTime.class)))
521   - .thenReturn(ListUtil.of());
  310 + .thenReturn(List.of(receiptMsg));
522 311  
523   - when(groupMessageReadRedisDAO.getReadMaxMessageId(10L, 1L)).thenReturn(100L);
524 312 Map<Long, Long> positions = new HashMap<>();
525 313 positions.put(1L, 100L);
526 314 positions.put(2L, 100L);
527 315 positions.put(3L, 50L);
528   - when(groupMessageReadRedisDAO.getReadMaxMessageIdMap(10L)).thenReturn(positions);
  316 + when(conversationReadService.getUserReadMessageIdMap(eq(ImConversationTypeEnum.GROUP.getType()), eq(10L)))
  317 + .thenReturn(positions);
529 318  
530   - List<ImGroupMemberDO> allMembers = ListUtil.of(
  319 + List<ImGroupMemberDO> allMembers = List.of(
531 320 member,
532 321 ImGroupMemberDO.builder().groupId(10L).userId(2L)
533 322 .status(CommonStatusEnum.ENABLE.getStatus())
... ... @@ -536,14 +325,44 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
536 325 .status(CommonStatusEnum.ENABLE.getStatus())
537 326 .joinTime(now.minusDays(20)).build()
538 327 );
539   - when(groupMemberService.getGroupMemberListByGroupId(10L)).thenReturn(allMembers);
  328 + when(groupMemberService.getActiveGroupMemberListByGroupId(10L)).thenReturn(allMembers);
540 329  
541 330 List<ImGroupMessageDO> result = groupMessageService.pullGroupMessageList(1L, 0L, 100);
542 331  
  332 + // 断言:分母 {2,3},仅用户 2 读到 >=100 → readCount==1
543 333 assertEquals(1, result.size());
544 334 assertEquals(1, result.get(0).getReadCount());
545 335 }
546 336  
  337 + @Test
  338 + public void testPullMessages_noReceiptMessageSkipsReadCount() {
  339 + // 准备:本人发送但不需要回执(NO_RECEIPT)的消息——不补 readCount,status 也不被覆盖
  340 + LocalDateTime now = LocalDateTime.now();
  341 + ImGroupMemberDO member = ImGroupMemberDO.builder()
  342 + .groupId(10L).userId(1L)
  343 + .status(CommonStatusEnum.ENABLE.getStatus())
  344 + .joinTime(now.minusDays(10)).build();
  345 + when(groupMemberService.getGroupMemberListByUserId(1L)).thenReturn(List.of(member));
  346 +
  347 + ImGroupMessageDO noReceiptMsg = ImGroupMessageDO.builder()
  348 + .id(100L).groupId(10L).senderId(1L)
  349 + .receiverUserIds(List.of(1L, 2L, 3L))
  350 + .status(ImMessageStatusEnum.NORMAL.getStatus())
  351 + .receiptStatus(ImMessageReceiptStatusEnum.NO_RECEIPT.getStatus())
  352 + .sendTime(now.minusHours(1)).build();
  353 + when(groupMessageMapper.selectListByMinId(eq(1L), anyCollection(), eq(0L),
  354 + any(LocalDateTime.class), eq(100)))
  355 + .thenReturn(List.of(noReceiptMsg));
  356 +
  357 + List<ImGroupMessageDO> result = groupMessageService.pullGroupMessageList(1L, 0L, 100);
  358 +
  359 + // 断言:status 保持 NORMAL(pull 不覆盖),NO_RECEIPT 不补 readCount,也不查 readCount 相关的读位置映射
  360 + assertEquals(1, result.size());
  361 + assertEquals(ImMessageStatusEnum.NORMAL.getStatus(), result.get(0).getStatus());
  362 + assertNull(result.get(0).getReadCount());
  363 + verify(conversationReadService, never()).getUserReadMessageIdMap(anyInt(), anyLong());
  364 + }
  365 +
547 366 // ========== 撤回测试 ==========
548 367  
549 368 @Test
... ... @@ -555,7 +374,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
555 374 // 准备:消息由用户 1 发送,刚发送 5 分钟内
556 375 ImGroupMessageDO message = ImGroupMessageDO.builder()
557 376 .id(50L).senderId(1L).groupId(10L)
558   - .status(ImMessageStatusEnum.UNREAD.getStatus())
  377 + .status(ImMessageStatusEnum.NORMAL.getStatus())
559 378 .sendTime(LocalDateTime.now()).build();
560 379 when(groupMessageMapper.selectById(50L)).thenReturn(message);
561 380 // 撤回前需要校验用户仍在群中
... ... @@ -566,7 +385,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
566 385 when(groupMessageMapper.insert(any(ImGroupMessageDO.class))).thenReturn(1);
567 386  
568 387 when(groupMemberService.getActiveGroupMemberUserIdsByGroupId(10L))
569   - .thenReturn(ListUtil.of(1L, 2L));
  388 + .thenReturn(List.of(1L, 2L));
570 389  
571 390 // 调用
572 391 ImGroupMessageDO result = groupMessageService.recallGroupMessage(1L, 50L);
... ... @@ -577,9 +396,9 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
577 396 verify(groupMessageMapper).updateById(any(ImGroupMessageDO.class));
578 397 verify(groupMessageMapper).insert(any(ImGroupMessageDO.class));
579 398 // 验证:给 2 个活跃成员推送撤回提示
580   - verify(imWebSocketService).sendGroupMessageAsync(argThat((Collection<Long> ids) ->
  399 + verify(imWebSocketService).sendNotificationAsync(argThat((Collection<Long> ids) ->
581 400 ids.size() == 2 && ids.contains(1L) && ids.contains(2L)),
582   - any(ImGroupMessageDTO.class));
  401 + anyInt(), anyInt(), any());
583 402 }
584 403 }
585 404  
... ... @@ -588,7 +407,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
588 407 // 准备
589 408 ImGroupMessageDO message = ImGroupMessageDO.builder()
590 409 .id(50L).senderId(2L).groupId(10L)
591   - .status(ImMessageStatusEnum.UNREAD.getStatus())
  410 + .status(ImMessageStatusEnum.NORMAL.getStatus())
592 411 .sendTime(LocalDateTime.now()).build();
593 412 when(groupMessageMapper.selectById(50L)).thenReturn(message);
594 413 when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn(
... ... @@ -624,7 +443,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
624 443 // 准备:消息发送于 10 分钟前(超过 5 分钟窗口)
625 444 ImGroupMessageDO message = ImGroupMessageDO.builder()
626 445 .id(50L).senderId(1L).groupId(10L)
627   - .status(ImMessageStatusEnum.UNREAD.getStatus())
  446 + .status(ImMessageStatusEnum.NORMAL.getStatus())
628 447 .sendTime(LocalDateTime.now().minusMinutes(10)).build();
629 448 when(groupMessageMapper.selectById(50L)).thenReturn(message);
630 449 when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn(
... ... @@ -641,16 +460,16 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
641 460  
642 461 @Test
643 462 public void testReadMessages_messageInvisible() {
644   - // 准备:用户不是群成员
645   - when(groupMemberService.getGroupMember(10L, 1L)).thenReturn(null);
  463 + // 准备:消息不在用户接收快照内
646 464 when(groupMessageMapper.selectById(100L)).thenReturn(ImGroupMessageDO.builder()
647   - .id(100L).groupId(10L).senderId(2L).sendTime(LocalDateTime.now()).build());
  465 + .id(100L).groupId(10L).senderId(2L).receiverUserIds(List.of(2L))
  466 + .sendTime(LocalDateTime.now()).build());
648 467  
649 468 // 调用并断言
650 469 ServiceException exception = assertThrows(ServiceException.class,
651 470 () -> groupMessageService.readGroupMessages(1L, 10L, 100L));
652 471 assertEquals(MESSAGE_NOT_IN_GROUP.getCode(), exception.getCode());
653   - verify(groupMessageReadRedisDAO, never()).updateReadMaxMessageId(anyLong(), anyLong(), anyLong());
  472 + verify(conversationReadService, never()).updateConversationReadPosition(anyLong(), anyInt(), anyLong(), anyLong());
654 473 }
655 474  
656 475 @Test
... ... @@ -662,18 +481,19 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
662 481 .joinTime(LocalDateTime.of(2026, 1, 1, 0, 0, 0)).build();
663 482 when(groupMemberService.validateMemberInGroup(10L, 5L)).thenReturn(currentMember);
664 483  
665   - // 准备:消息由用户 5 发,发送时间在 2026-04-12 10:00
  484 + // 准备:消息由用户 5 发,快照可见范围为 {1,2,5}(用户 3 不在快照内 → 不可见)
666 485 ImGroupMessageDO message = ImGroupMessageDO.builder()
667 486 .id(80L).groupId(10L).senderId(5L)
  487 + .receiverUserIds(List.of(1L, 2L, 5L))
668 488 .sendTime(LocalDateTime.of(2026, 4, 12, 10, 0, 0)).build();
669 489 when(groupMessageMapper.selectById(80L)).thenReturn(message);
670 490  
671 491 // 准备:群成员一览
672   - // 用户 1: 正常,入群在消息之前
673   - // 用户 2: 正常,入群在消息之前
674   - // 用户 3: 正常,但入群在消息之后 → 不可见
  492 + // 用户 1: 在快照内
  493 + // 用户 2: 在快照内
  494 + // 用户 3: 不在快照内 → 不可见
675 495 // 用户 5: 发送者,不计入回执
676   - List<ImGroupMemberDO> allMembers = ListUtil.of(
  496 + List<ImGroupMemberDO> allMembers = List.of(
677 497 currentMember,
678 498 ImGroupMemberDO.builder().groupId(10L).userId(1L)
679 499 .status(CommonStatusEnum.ENABLE.getStatus())
... ... @@ -683,19 +503,20 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
683 503 .joinTime(LocalDateTime.of(2026, 1, 1, 0, 0, 0)).build(),
684 504 ImGroupMemberDO.builder().groupId(10L).userId(3L)
685 505 .status(CommonStatusEnum.ENABLE.getStatus())
686   - .joinTime(LocalDateTime.of(2026, 4, 13, 0, 0, 0)).build(), // 消息之后才入群
  506 + .joinTime(LocalDateTime.of(2026, 1, 1, 0, 0, 0)).build(),
687 507 ImGroupMemberDO.builder().groupId(10L).userId(5L)
688 508 .status(CommonStatusEnum.ENABLE.getStatus())
689 509 .joinTime(LocalDateTime.of(2026, 1, 1, 0, 0, 0)).build() // 发送者
690 510 );
691   - when(groupMemberService.getGroupMemberListByGroupId(10L)).thenReturn(allMembers);
  511 + when(groupMemberService.getActiveGroupMemberListByGroupId(10L)).thenReturn(allMembers);
692 512  
693   - // 准备:Redis 已读位置 — 用户 1 读到 100, 用户 2 读到 50, 用户 3 读到 200
  513 + // 准备:已读位置 — 用户 1 读到 100, 用户 2 读到 50, 用户 3 读到 200
694 514 Map<Long, Long> positions = new HashMap<>();
695 515 positions.put(1L, 100L);
696 516 positions.put(2L, 50L);
697 517 positions.put(3L, 200L);
698   - when(groupMessageReadRedisDAO.getReadMaxMessageIdMap(10L)).thenReturn(positions);
  518 + when(conversationReadService.getUserReadMessageIdMap(eq(ImConversationTypeEnum.GROUP.getType()), eq(10L)))
  519 + .thenReturn(positions);
699 520  
700 521 // 调用:查询 messageId=80 的已读用户
701 522 List<Long> readUsers = groupMessageService.getGroupReadUserIds(5L, 10L, 80L);
... ... @@ -703,7 +524,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
703 524 // 断言:
704 525 // 用户 1: 可见 + readMaxId=100>=80 → 已读 ✓
705 526 // 用户 2: 可见 + readMaxId=50<80 → 未读 ✗
706   - // 用户 3: 入群在消息之后 → 不可见 → 不算
  527 + // 用户 3: 不在快照内 → 不可见 → 不算
707 528 // 用户 5: 发送者 → 排除
708 529 assertEquals(1, readUsers.size());
709 530 assertTrue(readUsers.contains(1L));
... ... @@ -724,7 +545,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
724 545 assertEquals(GROUP_MEMBER_NOT_IN_GROUP.getCode(), exception.getCode());
725 546 }
726 547  
727   - // ========== 发送:补充边界 ==========
  548 + // ========== 发送边界 ==========
728 549  
729 550 @Test
730 551 public void testSendMessage_receiptPending() {
... ... @@ -743,14 +564,14 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
743 564 ImGroupMemberDO member = ImGroupMemberDO.builder()
744 565 .groupId(10L).userId(1L).status(CommonStatusEnum.ENABLE.getStatus()).build();
745 566 when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn(member);
746   - when(groupMemberService.getActiveGroupMemberUserIdsByGroupId(10L)).thenReturn(ListUtil.of(1L));
  567 + when(groupMemberService.getActiveGroupMemberUserIdsByGroupId(10L)).thenReturn(List.of(1L));
747 568 when(groupMessageMapper.insert(any(ImGroupMessageDO.class))).thenReturn(1);
748 569  
749 570 // 调用
750 571 ImGroupMessageDO result = groupMessageService.sendGroupMessage(1L, reqVO);
751 572  
752 573 // 断言:receipt=true → 回执状态为 PENDING
753   - assertEquals(ImGroupMessageReceiptStatusEnum.PENDING.getStatus(), result.getReceiptStatus());
  574 + assertEquals(ImMessageReceiptStatusEnum.PENDING.getStatus(), result.getReceiptStatus());
754 575 }
755 576 }
756 577  
... ... @@ -775,10 +596,10 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
775 596 assertEquals(MESSAGE_SENSITIVE_WORD_BLOCKED.getCode(), exception.getCode());
776 597 // 断言:不入库、不推送
777 598 verify(groupMessageMapper, never()).insert(any(ImGroupMessageDO.class));
778   - verify(imWebSocketService, never()).sendGroupMessageAsync(anyCollection(), any(ImGroupMessageDTO.class));
  599 + verify(imWebSocketService, never()).sendNotificationAsync(anyCollection(), anyInt(), anyInt(), any());
779 600 }
780 601  
781   - // ========== 撤回:补充边界 ==========
  602 + // ========== 撤回边界 ==========
782 603  
783 604 @Test
784 605 public void testRecallMessage_notExists() {
... ... @@ -794,7 +615,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
794 615 // 准备:消息合法、可撤回时间内,但发送人已退群
795 616 ImGroupMessageDO message = ImGroupMessageDO.builder()
796 617 .id(50L).senderId(1L).groupId(10L)
797   - .status(ImMessageStatusEnum.UNREAD.getStatus())
  618 + .status(ImMessageStatusEnum.NORMAL.getStatus())
798 619 .sendTime(LocalDateTime.now()).build();
799 620 when(groupMessageMapper.selectById(50L)).thenReturn(message);
800 621 when(groupMemberService.validateMemberInGroup(10L, 1L))
... ... @@ -808,7 +629,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
808 629 verify(groupMessageMapper, never()).insert(any(ImGroupMessageDO.class));
809 630 }
810 631  
811   - // ========== 已读:happy path + 跳过 ==========
  632 + // ========== 已读事件 ==========
812 633  
813 634 @Test
814 635 public void testReadGroupMessages_success() {
... ... @@ -816,24 +637,24 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
816 637 springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupMessageServiceImpl.class)))
817 638 .thenReturn(groupMessageService);
818 639  
819   - // 准备:用户在群;Redis 游标从 5 前进到 100
820   - ImGroupMemberDO member = ImGroupMemberDO.builder()
821   - .groupId(10L).userId(1L)
822   - .status(CommonStatusEnum.ENABLE.getStatus())
823   - .joinTime(LocalDateTime.now().minusDays(1)).build();
824   - when(groupMemberService.getGroupMember(10L, 1L)).thenReturn(member);
  640 + // 准备:消息对用户可见;已读位置从 5 前进到 100
825 641 when(groupMessageMapper.selectById(100L)).thenReturn(ImGroupMessageDO.builder()
826   - .id(100L).groupId(10L).senderId(2L).sendTime(LocalDateTime.now()).build());
827   - when(groupMessageReadRedisDAO.getReadMaxMessageId(10L, 1L)).thenReturn(5L);
  642 + .id(100L).groupId(10L).senderId(2L).receiverUserIds(List.of(1L))
  643 + .sendTime(LocalDateTime.now()).build());
  644 + when(conversationReadService.getConversationReadMessageId(1L, ImConversationTypeEnum.GROUP.getType(), 10L))
  645 + .thenReturn(5L);
  646 + when(conversationReadService.updateConversationReadPosition(1L, ImConversationTypeEnum.GROUP.getType(), 10L, 100L))
  647 + .thenReturn(true);
828 648 // readGroupMessageEvent 内部会调 selectListByGroupIdAndPendingReceipt → 返回空简化流程
829   - when(groupMessageMapper.selectListByGroupIdAndPendingReceipt(10L, 5L, 100L)).thenReturn(ListUtil.of());
  649 + when(groupMessageMapper.selectListByGroupIdAndPendingReceipt(10L, 5L, 100L)).thenReturn(List.of());
830 650  
831 651 // 调用
832 652 groupMessageService.readGroupMessages(1L, 10L, 100L);
833 653  
834   - // 断言:Redis 游标更新 + READ 事件
835   - verify(groupMessageReadRedisDAO).updateReadMaxMessageId(10L, 1L, 100L);
836   - verify(imWebSocketService).sendGroupMessageAsync(eq(1L), any(ImGroupMessageDTO.class));
  654 + // 断言:已读位置更新 + READ 事件
  655 + verify(conversationReadService).updateConversationReadPosition(1L, ImConversationTypeEnum.GROUP.getType(), 10L, 100L);
  656 + verify(imWebSocketService).sendNotificationAsync(eq(1L), anyInt(), anyInt(), any());
  657 + verify(groupMemberService, never()).validateMemberInGroup(10L, 1L);
837 658 }
838 659 }
839 660  
... ... @@ -846,9 +667,9 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
846 667 ServiceException exception = assertThrows(ServiceException.class,
847 668 () -> groupMessageService.readGroupMessages(1L, 10L, 100L));
848 669 assertEquals(MESSAGE_GROUP_READ_DISABLED.getCode(), exception.getCode());
849   - // 断言:Redis 不写、不推送
850   - verify(groupMessageReadRedisDAO, never()).updateReadMaxMessageId(anyLong(), anyLong(), anyLong());
851   - verify(imWebSocketService, never()).sendGroupMessageAsync(anyLong(), any(ImGroupMessageDTO.class));
  670 + // 断言:已读位置不写、不推送
  671 + verify(conversationReadService, never()).updateConversationReadPosition(anyLong(), anyInt(), anyLong(), anyLong());
  672 + verify(imWebSocketService, never()).sendNotificationAsync(anyLong(), anyInt(), anyInt(), any());
852 673 }
853 674  
854 675 @Test
... ... @@ -880,11 +701,11 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
880 701 ImGroupMemberDO member = ImGroupMemberDO.builder()
881 702 .groupId(10L).userId(1L).status(CommonStatusEnum.ENABLE.getStatus()).build();
882 703 when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn(member);
883   - when(groupMemberService.getActiveGroupMemberUserIdsByGroupId(10L)).thenReturn(ListUtil.of(1L, 2L));
  704 + when(groupMemberService.getActiveGroupMemberUserIdsByGroupId(10L)).thenReturn(List.of(1L, 2L));
884 705  
885 706 ImGroupMessageDO result = groupMessageService.sendGroupMessage(1L, reqVO);
886 707  
887   - assertEquals(ImGroupMessageReceiptStatusEnum.NO_RECEIPT.getStatus(), result.getReceiptStatus(),
  708 + assertEquals(ImMessageReceiptStatusEnum.NO_RECEIPT.getStatus(), result.getReceiptStatus(),
888 709 "群已读关闭时即使发送方传 receipt=true 也强制落 NO_RECEIPT");
889 710 }
890 711 }
... ... @@ -892,31 +713,26 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
892 713 @Test
893 714 public void testReadGroupMessages_cursorAlreadyAhead() {
894 715 // 准备:已读游标已 >= 目标,直接返回
895   - ImGroupMemberDO member = ImGroupMemberDO.builder()
896   - .groupId(10L).userId(1L)
897   - .status(CommonStatusEnum.ENABLE.getStatus())
898   - .joinTime(LocalDateTime.now().minusDays(1)).build();
899   - when(groupMemberService.getGroupMember(10L, 1L)).thenReturn(member);
900 716 when(groupMessageMapper.selectById(100L)).thenReturn(ImGroupMessageDO.builder()
901   - .id(100L).groupId(10L).senderId(2L).sendTime(LocalDateTime.now()).build());
902   - when(groupMessageReadRedisDAO.getReadMaxMessageId(10L, 1L)).thenReturn(200L);
  717 + .id(100L).groupId(10L).senderId(2L).receiverUserIds(List.of(1L))
  718 + .sendTime(LocalDateTime.now()).build());
  719 + when(conversationReadService.getConversationReadMessageId(1L, ImConversationTypeEnum.GROUP.getType(), 10L))
  720 + .thenReturn(200L);
  721 + // CAS 命中旧位置 200 > 100,单调更新不前进,返回 false
  722 + when(conversationReadService.updateConversationReadPosition(1L, ImConversationTypeEnum.GROUP.getType(), 10L, 100L))
  723 + .thenReturn(false);
903 724  
904 725 // 调用
905 726 groupMessageService.readGroupMessages(1L, 10L, 100L);
906 727  
907   - // 断言:不更新、不推送
908   - verify(groupMessageReadRedisDAO, never()).updateReadMaxMessageId(anyLong(), anyLong(), anyLong());
909   - verify(imWebSocketService, never()).sendGroupMessageAsync(anyLong(), any(ImGroupMessageDTO.class));
  728 + // 断言:调用了 CAS 更新但读位置未前进 → 不推送
  729 + verify(conversationReadService).updateConversationReadPosition(1L, ImConversationTypeEnum.GROUP.getType(), 10L, 100L);
  730 + verify(imWebSocketService, never()).sendNotificationAsync(anyLong(), anyInt(), anyInt(), any());
910 731 }
911 732  
912 733 @Test
913 734 public void testReadGroupMessages_messageNotInGroup() {
914   - // 准备:用户在群,但 messageId 属于其它群
915   - ImGroupMemberDO member = ImGroupMemberDO.builder()
916   - .groupId(10L).userId(1L)
917   - .status(CommonStatusEnum.ENABLE.getStatus())
918   - .joinTime(LocalDateTime.now().minusDays(1)).build();
919   - when(groupMemberService.getGroupMember(10L, 1L)).thenReturn(member);
  735 + // 准备:messageId 属于其它群
920 736 when(groupMessageMapper.selectById(100L)).thenReturn(ImGroupMessageDO.builder()
921 737 .id(100L).groupId(20L).senderId(2L).sendTime(LocalDateTime.now()).build());
922 738  
... ... @@ -924,7 +740,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
924 740 ServiceException exception = assertThrows(ServiceException.class,
925 741 () -> groupMessageService.readGroupMessages(1L, 10L, 100L));
926 742 assertEquals(MESSAGE_NOT_IN_GROUP.getCode(), exception.getCode());
927   - verify(groupMessageReadRedisDAO, never()).updateReadMaxMessageId(anyLong(), anyLong(), anyLong());
  743 + verify(conversationReadService, never()).updateConversationReadPosition(anyLong(), anyInt(), anyLong(), anyLong());
928 744 }
929 745  
930 746 @Test
... ... @@ -933,62 +749,54 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
933 749 springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupMessageServiceImpl.class)))
934 750 .thenReturn(groupMessageService);
935 751  
936   - LocalDateTime now = LocalDateTime.now();
937   - ImGroupMemberDO member = ImGroupMemberDO.builder()
938   - .groupId(10L).userId(1L)
939   - .status(CommonStatusEnum.DISABLE.getStatus())
940   - .joinTime(now.minusDays(1))
941   - .quitTime(now.plusMinutes(1)).build();
942   - when(groupMemberService.getGroupMember(10L, 1L)).thenReturn(member);
  752 + // 准备:历史消息的接收快照包含用户
943 753 when(groupMessageMapper.selectById(100L)).thenReturn(ImGroupMessageDO.builder()
944   - .id(100L).groupId(10L).senderId(2L).sendTime(now)
945   - .receiverUserIds(ListUtil.of(1L, 2L)).build());
946   - when(groupMessageReadRedisDAO.getReadMaxMessageId(10L, 1L)).thenReturn(0L);
947   - when(groupMessageMapper.selectListByGroupIdAndPendingReceipt(10L, 0L, 100L)).thenReturn(ListUtil.of());
  754 + .id(100L).groupId(10L).senderId(2L).receiverUserIds(List.of(1L, 2L))
  755 + .sendTime(LocalDateTime.now().minusMinutes(1)).build());
  756 + when(conversationReadService.getConversationReadMessageId(1L, ImConversationTypeEnum.GROUP.getType(), 10L))
  757 + .thenReturn(0L);
  758 + when(conversationReadService.updateConversationReadPosition(1L, ImConversationTypeEnum.GROUP.getType(), 10L, 100L))
  759 + .thenReturn(true);
  760 + when(groupMessageMapper.selectListByGroupIdAndPendingReceipt(10L, 0L, 100L)).thenReturn(List.of());
948 761  
949 762 // 调用
950 763 groupMessageService.readGroupMessages(1L, 10L, 100L);
951 764  
952 765 // 断言:不校验当前在群,直接推进已读位置
953 766 verify(groupMemberService, never()).validateMemberInGroup(10L, 1L);
954   - verify(groupMessageReadRedisDAO).updateReadMaxMessageId(10L, 1L, 100L);
  767 + verify(conversationReadService).updateConversationReadPosition(1L, ImConversationTypeEnum.GROUP.getType(), 10L, 100L);
955 768 }
956 769 }
957 770  
958 771 @Test
959 772 public void testReadGroupMessages_quitMemberInvisibleMessage() {
960   - LocalDateTime now = LocalDateTime.now();
961   - ImGroupMemberDO member = ImGroupMemberDO.builder()
962   - .groupId(10L).userId(1L)
963   - .status(CommonStatusEnum.DISABLE.getStatus())
964   - .joinTime(now.minusDays(2))
965   - .quitTime(now.minusDays(1)).build();
966   - when(groupMemberService.getGroupMember(10L, 1L)).thenReturn(member);
  773 + // 准备:消息不在历史接收快照内
967 774 when(groupMessageMapper.selectById(100L)).thenReturn(ImGroupMessageDO.builder()
968   - .id(100L).groupId(10L).senderId(2L).sendTime(now)
969   - .receiverUserIds(ListUtil.of(1L, 2L)).build());
  775 + .id(100L).groupId(10L).senderId(2L).receiverUserIds(List.of(2L))
  776 + .sendTime(LocalDateTime.now()).build());
970 777  
971 778 // 调用并断言
972 779 ServiceException exception = assertThrows(ServiceException.class,
973 780 () -> groupMessageService.readGroupMessages(1L, 10L, 100L));
974 781 assertEquals(MESSAGE_NOT_IN_GROUP.getCode(), exception.getCode());
975   - verify(groupMessageReadRedisDAO, never()).updateReadMaxMessageId(anyLong(), anyLong(), anyLong());
  782 + verify(conversationReadService, never()).updateConversationReadPosition(anyLong(), anyInt(), anyLong(), anyLong());
976 783 }
977 784  
978   - // ========== 回执:DONE 状态迁移 ==========
  785 + // ========== 回执状态迁移 ==========
979 786  
980 787 @Test
981 788 public void testReadGroupMessageEvent_receiptDoneTransition() {
982 789 // 准备:消息 100 发送者 5,群 10 活跃成员 {5,1,2},可见用户中除发送者外 {1,2}
983   - // Redis 位置 1→100、2→100 → 全部已读 → 迁移为 DONE
  790 + // 已读位置 1→100、2→100 → 全部已读 → 迁移为 DONE
984 791 ImGroupMessageDO pending = ImGroupMessageDO.builder()
985 792 .id(100L).groupId(10L).senderId(5L)
  793 + .receiverUserIds(List.of(5L, 1L, 2L))
986 794 .sendTime(LocalDateTime.now().minusMinutes(1))
987   - .receiptStatus(ImGroupMessageReceiptStatusEnum.PENDING.getStatus())
988   - .status(ImMessageStatusEnum.UNREAD.getStatus()).build();
  795 + .receiptStatus(ImMessageReceiptStatusEnum.PENDING.getStatus())
  796 + .status(ImMessageStatusEnum.NORMAL.getStatus()).build();
989 797 when(groupMessageMapper.selectListByGroupIdAndPendingReceipt(10L, 0L, 100L))
990   - .thenReturn(ListUtil.of(pending));
991   - List<ImGroupMemberDO> activeMembers = ListUtil.of(
  798 + .thenReturn(List.of(pending));
  799 + List<ImGroupMemberDO> activeMembers = List.of(
992 800 ImGroupMemberDO.builder().groupId(10L).userId(5L)
993 801 .status(CommonStatusEnum.ENABLE.getStatus())
994 802 .joinTime(LocalDateTime.now().minusDays(10)).build(),
... ... @@ -1003,7 +811,8 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
1003 811 Map<Long, Long> positions = new HashMap<>();
1004 812 positions.put(1L, 100L);
1005 813 positions.put(2L, 100L);
1006   - when(groupMessageReadRedisDAO.getReadMaxMessageIdMap(10L)).thenReturn(positions);
  814 + when(conversationReadService.getUserReadMessageIdMap(ImConversationTypeEnum.GROUP.getType(), 10L))
  815 + .thenReturn(positions);
1007 816  
1008 817 // 调用
1009 818 groupMessageService.readGroupMessageEvent(1L, 10L, 0L, 100L);
... ... @@ -1012,9 +821,9 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
1012 821 ArgumentCaptor<ImGroupMessageDO> captor = ArgumentCaptor.forClass(ImGroupMessageDO.class);
1013 822 verify(groupMessageMapper).updateById(captor.capture());
1014 823 assertEquals(100L, captor.getValue().getId());
1015   - assertEquals(ImGroupMessageReceiptStatusEnum.DONE.getStatus(), captor.getValue().getReceiptStatus());
  824 + assertEquals(ImMessageReceiptStatusEnum.DONE.getStatus(), captor.getValue().getReceiptStatus());
1016 825 // 断言:给消息发送方(5)推送 RECEIPT 事件
1017   - verify(imWebSocketService).sendGroupMessageAsync(eq(5L), any(ImGroupMessageDTO.class));
  826 + verify(imWebSocketService).sendNotificationAsync(eq(5L), anyInt(), anyInt(), any());
1018 827 }
1019 828  
1020 829 @Test
... ... @@ -1022,12 +831,13 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
1022 831 // 准备:用户 2 还没读到 100 → 仍为 PENDING,不更新 DB
1023 832 ImGroupMessageDO pending = ImGroupMessageDO.builder()
1024 833 .id(100L).groupId(10L).senderId(5L)
  834 + .receiverUserIds(List.of(5L, 1L, 2L))
1025 835 .sendTime(LocalDateTime.now().minusMinutes(1))
1026   - .receiptStatus(ImGroupMessageReceiptStatusEnum.PENDING.getStatus())
1027   - .status(ImMessageStatusEnum.UNREAD.getStatus()).build();
  836 + .receiptStatus(ImMessageReceiptStatusEnum.PENDING.getStatus())
  837 + .status(ImMessageStatusEnum.NORMAL.getStatus()).build();
1028 838 when(groupMessageMapper.selectListByGroupIdAndPendingReceipt(10L, 0L, 100L))
1029   - .thenReturn(ListUtil.of(pending));
1030   - List<ImGroupMemberDO> activeMembers = ListUtil.of(
  839 + .thenReturn(List.of(pending));
  840 + List<ImGroupMemberDO> activeMembers = List.of(
1031 841 ImGroupMemberDO.builder().groupId(10L).userId(5L)
1032 842 .status(CommonStatusEnum.ENABLE.getStatus())
1033 843 .joinTime(LocalDateTime.now().minusDays(10)).build(),
... ... @@ -1042,33 +852,34 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
1042 852 Map<Long, Long> positions = new HashMap<>();
1043 853 positions.put(1L, 100L);
1044 854 positions.put(2L, 50L); // 还没读到 100
1045   - when(groupMessageReadRedisDAO.getReadMaxMessageIdMap(10L)).thenReturn(positions);
  855 + when(conversationReadService.getUserReadMessageIdMap(ImConversationTypeEnum.GROUP.getType(), 10L))
  856 + .thenReturn(positions);
1046 857  
1047 858 groupMessageService.readGroupMessageEvent(1L, 10L, 0L, 100L);
1048 859  
1049 860 // 断言:回执状态保持 PENDING,不更新 DB
1050 861 verify(groupMessageMapper, never()).updateById(any(ImGroupMessageDO.class));
1051 862 // 仍然推送 RECEIPT 事件给发送方(携带已读人数)
1052   - verify(imWebSocketService).sendGroupMessageAsync(eq(5L), any(ImGroupMessageDTO.class));
  863 + verify(imWebSocketService).sendNotificationAsync(eq(5L), anyInt(), anyInt(), any());
1053 864 }
1054 865  
1055   - // ========== sendGroupMessage(senderId, dto):receiverUserIds 定向过滤 ==========
  866 + // ========== 定向群消息 ==========
1056 867  
1057 868 @Test
1058 869 public void testSendGroupMessage_noReceiverUserIds_broadcastsToAllMembers() {
1059 870 // 准备:无定向 → 应发给所有 ENABLE 成员(含发送者自己,多端同步)
1060 871 ImGroupMessageSendDTO dto = new ImGroupMessageSendDTO()
1061   - .setGroupId(10L).setType(ImMessageTypeEnum.RECALL.getType())
  872 + .setGroupId(10L).setType(ImContentTypeEnum.RECALL.getType())
1062 873 .setContent("{\"messageId\":1}");
1063 874 when(groupMemberService.getActiveGroupMemberUserIdsByGroupId(10L))
1064   - .thenReturn(ListUtil.of(1L, 2L, 3L, 4L));
  875 + .thenReturn(List.of(1L, 2L, 3L, 4L));
1065 876  
1066 877 groupMessageService.sendGroupMessage(1L, dto);
1067 878  
1068   - verify(imWebSocketService).sendGroupMessageAsync(argThat((Collection<Long> ids) ->
  879 + verify(imWebSocketService).sendNotificationAsync(argThat((Collection<Long> ids) ->
1069 880 ids.size() == 4 && ids.contains(1L) && ids.contains(2L)
1070 881 && ids.contains(3L) && ids.contains(4L)),
1071   - any(ImGroupMessageDTO.class));
  882 + anyInt(), anyInt(), any());
1072 883 }
1073 884  
1074 885 @Test
... ... @@ -1076,47 +887,48 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
1076 887 // 准备:定向给 {2,3},发送者 1;群成员 {1,2,3,4}
1077 888 // 预期推送目标:{1,2,3}(发送者自己多端同步 + 定向列表);4 被过滤
1078 889 ImGroupMessageSendDTO dto = new ImGroupMessageSendDTO()
1079   - .setGroupId(10L).setType(ImMessageTypeEnum.RECALL.getType())
  890 + .setGroupId(10L).setType(ImContentTypeEnum.RECALL.getType())
1080 891 .setContent("{\"messageId\":1}")
1081   - .setReceiverUserIds(ListUtil.of(2L, 3L));
  892 + .setReceiverUserIds(List.of(2L, 3L));
1082 893 when(groupMemberService.getActiveGroupMemberUserIdsByGroupId(10L))
1083   - .thenReturn(ListUtil.of(1L, 2L, 3L, 4L));
  894 + .thenReturn(List.of(1L, 2L, 3L, 4L));
1084 895  
1085 896 groupMessageService.sendGroupMessage(1L, dto);
1086 897  
1087   - verify(imWebSocketService).sendGroupMessageAsync(argThat((Collection<Long> ids) ->
  898 + verify(imWebSocketService).sendNotificationAsync(argThat((Collection<Long> ids) ->
1088 899 ids.size() == 3 && ids.contains(1L) && ids.contains(2L)
1089 900 && ids.contains(3L) && !ids.contains(4L)),
1090   - any(ImGroupMessageDTO.class));
  901 + anyInt(), anyInt(), any());
1091 902 }
1092 903  
1093 904 @Test
1094   - public void testSendGroupMessage_withReceiverUserIds_senderNotInGroup_senderStillExcluded() {
1095   - // 边界:发送者不在群的 userId 列表里(理论上应先被 validateMemberInGroup 挡住,这里纯防御)
1096   - // 预期:仅定向用户可见
  905 + public void testSendGroupMessage_withReceiverUserIds_senderAlwaysIncluded() {
  906 + // 边界:发送者不在群成员缓存里(理论上应先被 validateMemberInGroup 挡住,这里纯防御)
  907 + // 预期:定向用户 + 发送者自己(始终纳入快照保证多端同步);非定向成员 4 被过滤
1097 908 ImGroupMessageSendDTO dto = new ImGroupMessageSendDTO()
1098   - .setGroupId(10L).setType(ImMessageTypeEnum.RECALL.getType())
  909 + .setGroupId(10L).setType(ImContentTypeEnum.RECALL.getType())
1099 910 .setContent("{\"messageId\":1}")
1100   - .setReceiverUserIds(ListUtil.of(2L, 3L));
  911 + .setReceiverUserIds(List.of(2L, 3L));
1101 912 when(groupMemberService.getActiveGroupMemberUserIdsByGroupId(10L))
1102   - .thenReturn(ListUtil.of(1L, 2L, 3L, 4L));
  913 + .thenReturn(List.of(1L, 2L, 3L, 4L));
1103 914  
1104 915 groupMessageService.sendGroupMessage(99L, dto);
1105 916  
1106   - verify(imWebSocketService).sendGroupMessageAsync(argThat((Collection<Long> ids) ->
1107   - ids.size() == 2 && ids.contains(2L) && ids.contains(3L)),
1108   - any(ImGroupMessageDTO.class));
  917 + verify(imWebSocketService).sendNotificationAsync(argThat((Collection<Long> ids) ->
  918 + ids.size() == 3 && ids.contains(2L) && ids.contains(3L)
  919 + && ids.contains(99L) && !ids.contains(4L)),
  920 + anyInt(), anyInt(), any());
1109 921 }
1110 922  
1111   - // ========== sendGroupMessage(senderId, dto):helper 行为 ==========
  923 + // ========== DTO 群消息 ==========
1112 924  
1113 925 @Test
1114 926 public void testSendGroupMessage_dto_persistsAndSerializesPojoContent() {
1115 927 // 准备:persistent=true 类型 + POJO content
1116 928 ImGroupMessageSendDTO dto = new ImGroupMessageSendDTO()
1117   - .setGroupId(10L).setType(ImMessageTypeEnum.RECALL.getType())
  929 + .setGroupId(10L).setType(ImContentTypeEnum.RECALL.getType())
1118 930 .setContent(new RecallMessage().setMessageId(50L));
1119   - when(groupMemberService.getActiveGroupMemberUserIdsByGroupId(10L)).thenReturn(ListUtil.of(1L));
  931 + when(groupMemberService.getActiveGroupMemberUserIdsByGroupId(10L)).thenReturn(List.of(1L));
1120 932  
1121 933 groupMessageService.sendGroupMessage(1L, dto);
1122 934  
... ... @@ -1126,10 +938,10 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
1126 938 ImGroupMessageDO message = captor.getValue();
1127 939 assertEquals(1L, message.getSenderId());
1128 940 assertEquals(10L, message.getGroupId());
1129   - assertEquals(ImMessageTypeEnum.RECALL.getType(), message.getType());
  941 + assertEquals(ImContentTypeEnum.RECALL.getType(), message.getType());
1130 942 assertEquals("{\"messageId\":50}", message.getContent());
1131   - assertEquals(ImMessageStatusEnum.UNREAD.getStatus(), message.getStatus());
1132   - assertEquals(ImGroupMessageReceiptStatusEnum.NO_RECEIPT.getStatus(), message.getReceiptStatus());
  943 + assertEquals(ImMessageStatusEnum.NORMAL.getStatus(), message.getStatus());
  944 + assertEquals(ImMessageReceiptStatusEnum.NO_RECEIPT.getStatus(), message.getReceiptStatus());
1133 945 assertNotNull(message.getClientMessageId());
1134 946 assertNotNull(message.getSendTime());
1135 947 }
... ... @@ -1138,114 +950,103 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
1138 950 public void testSendGroupMessage_dto_receiptPending() {
1139 951 // 准备:dto.receipt=true → receiptStatus=PENDING
1140 952 ImGroupMessageSendDTO dto = new ImGroupMessageSendDTO()
1141   - .setGroupId(10L).setType(ImMessageTypeEnum.RECALL.getType())
  953 + .setGroupId(10L).setType(ImContentTypeEnum.RECALL.getType())
1142 954 .setContent("{\"messageId\":50}").setReceipt(true);
1143   - when(groupMemberService.getActiveGroupMemberUserIdsByGroupId(10L)).thenReturn(ListUtil.of(1L));
  955 + when(groupMemberService.getActiveGroupMemberUserIdsByGroupId(10L)).thenReturn(List.of(1L));
1144 956  
1145 957 groupMessageService.sendGroupMessage(1L, dto);
1146 958  
1147 959 ArgumentCaptor<ImGroupMessageDO> captor = ArgumentCaptor.forClass(ImGroupMessageDO.class);
1148 960 verify(groupMessageMapper).insert(captor.capture());
1149   - assertEquals(ImGroupMessageReceiptStatusEnum.PENDING.getStatus(), captor.getValue().getReceiptStatus());
  961 + assertEquals(ImMessageReceiptStatusEnum.PENDING.getStatus(), captor.getValue().getReceiptStatus());
1150 962 }
1151 963  
1152 964 @Test
1153 965 public void testSendGroupMessage_dto_nonPersistentTypeNotInserted() {
1154 966 // 准备:persistent=false 类型(RECEIPT 回执)→ 不入库,仅推送
1155 967 ImGroupMessageSendDTO dto = new ImGroupMessageSendDTO()
1156   - .setGroupId(10L).setType(ImMessageTypeEnum.RECEIPT.getType());
1157   - when(groupMemberService.getActiveGroupMemberUserIdsByGroupId(10L)).thenReturn(ListUtil.of(1L, 2L));
  968 + .setGroupId(10L).setType(ImContentTypeEnum.RECEIPT.getType());
  969 + when(groupMemberService.getActiveGroupMemberUserIdsByGroupId(10L)).thenReturn(List.of(1L, 2L));
1158 970  
1159 971 groupMessageService.sendGroupMessage(1L, dto);
1160 972  
1161 973 verify(groupMessageMapper, never()).insert(any(ImGroupMessageDO.class));
1162   - verify(imWebSocketService).sendGroupMessageAsync(anyCollection(), any(ImGroupMessageDTO.class));
  974 + verify(imWebSocketService).sendNotificationAsync(anyCollection(), anyInt(), anyInt(), any());
  975 + }
  976 +
  977 + @Test
  978 + public void testSendGroupMessage_dto_groupMemberNicknameUpdateNotInserted() {
  979 + // 准备:成员昵称变更只做在线同步,不入库
  980 + ImGroupMessageSendDTO dto = ImGroupMessageSendDTO.ofGroupMemberNicknameUpdate(10L, 1L, "群昵称");
  981 + when(groupMemberService.getActiveGroupMemberUserIdsByGroupId(10L)).thenReturn(List.of(1L, 2L));
  982 +
  983 + groupMessageService.sendGroupMessage(1L, dto);
  984 +
  985 + verify(groupMessageMapper, never()).insert(any(ImGroupMessageDO.class));
  986 + verify(imWebSocketService).sendNotificationAsync(argThat((Collection<Long> ids) ->
  987 + ids.size() == 2 && ids.contains(1L) && ids.contains(2L)),
  988 + anyInt(), eq(ImContentTypeEnum.GROUP_MEMBER_NICKNAME_UPDATE.getType()), any());
1163 989 }
1164 990  
1165 991 @Test
1166 992 public void testSendGroupMessage_threeArg_explicitTargetsBypassActiveMembers() {
1167 993 // 准备:调用方传入显式 targets(解散场景成员已被批量 DISABLE,必须按移除前快照推送)
1168   - Set<Long> targets = CollUtil.newHashSet(1L, 2L, 3L);
  994 + Set<Long> targets = Set.of(1L, 2L, 3L);
1169 995 ImGroupMessageSendDTO dto = new ImGroupMessageSendDTO()
1170   - .setGroupId(10L).setType(ImMessageTypeEnum.GROUP_DISSOLVE.getType()).setContent("{}");
  996 + .setGroupId(10L).setType(ImContentTypeEnum.GROUP_DISSOLVE.getType()).setContent("{}");
1171 997  
1172 998 groupMessageService.sendGroupMessage(1L, targets, dto);
1173 999  
1174 1000 // 断言:不读取 active members,按调用方 targets 推送
1175 1001 verify(groupMemberService, never()).getActiveGroupMemberUserIdsByGroupId(anyLong());
1176   - verify(imWebSocketService).sendGroupMessageAsync(eq(targets), any(ImGroupMessageDTO.class));
  1002 + verify(imWebSocketService).sendNotificationAsync(eq(targets), anyInt(), anyInt(), any());
1177 1003 }
1178 1004  
1179 1005 // ========== getGroupMessageList ==========
1180 1006  
1181 1007 @Test
1182   - public void testGetGroupMessageList_delegatesToMapperWithJoinTime() {
1183   - // 准备
1184   - LocalDateTime joinTime = LocalDateTime.now().minusDays(5);
  1008 + public void testGetGroupMessageList_delegatesToMapper() {
  1009 + // 准备:可见性由 SQL 按 receiver_user_ids 快照过滤,Service 直接透传 mapper 结果
1185 1010 ImGroupMemberDO member = ImGroupMemberDO.builder()
1186 1011 .id(50L).groupId(10L).userId(1L)
1187 1012 .status(CommonStatusEnum.ENABLE.getStatus())
1188   - .joinTime(joinTime).build();
1189   - when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn(member);
  1013 + .joinTime(LocalDateTime.now().minusDays(5)).build();
  1014 + when(groupMemberService.getGroupMember(10L, 1L)).thenReturn(member);
1190 1015  
1191 1016 ImGroupMessageListReqVO reqVO = new ImGroupMessageListReqVO();
1192 1017 reqVO.setGroupId(10L);
1193 1018 reqVO.setMaxId(100L);
1194 1019 reqVO.setLimit(20);
1195 1020  
1196   - List<ImGroupMessageDO> mockList = ListUtil.of(
  1021 + List<ImGroupMessageDO> mockList = List.of(
1197 1022 ImGroupMessageDO.builder().id(99L).groupId(10L).senderId(2L)
1198   - .sendTime(joinTime.plusMinutes(1)).build()
  1023 + .receiverUserIds(List.of(1L)).sendTime(LocalDateTime.now()).build()
1199 1024 );
1200   - when(groupMessageMapper.selectHistoryList(10L, 100L, 20, joinTime)).thenReturn(mockList);
  1025 + when(groupMessageMapper.selectHistoryListByUser(1L, 10L, 100L, 20)).thenReturn(mockList);
1201 1026  
1202 1027 // 调用
1203 1028 List<ImGroupMessageDO> result = groupMessageService.getGroupMessageList(1L, reqVO);
1204 1029  
1205   - // 断言:使用成员的 joinTime 作为历史消息的下限
  1030 + // 断言:透传到 mapper,参数一致
1206 1031 assertEquals(1, result.size());
1207   - verify(groupMessageMapper).selectHistoryList(10L, 100L, 20, joinTime);
  1032 + assertEquals(99L, result.get(0).getId());
  1033 + verify(groupMessageMapper).selectHistoryListByUser(1L, 10L, 100L, 20);
1208 1034 }
1209 1035  
1210 1036 @Test
1211 1037 public void testGetGroupMessageList_notInGroup() {
1212   - // 准备:用户不在群中
  1038 + // 准备:用户从未在群中(getGroupMember 返回 null)
1213 1039 ImGroupMessageListReqVO reqVO = new ImGroupMessageListReqVO();
1214 1040 reqVO.setGroupId(10L);
1215 1041 reqVO.setLimit(20);
1216   - when(groupMemberService.validateMemberInGroup(10L, 1L))
1217   - .thenThrow(new ServiceException(GROUP_MEMBER_NOT_IN_GROUP.getCode(),
1218   - GROUP_MEMBER_NOT_IN_GROUP.getMsg()));
  1042 + when(groupMemberService.getGroupMember(10L, 1L)).thenReturn(null);
1219 1043  
1220 1044 // 调用并断言:鉴权失败
1221 1045 ServiceException exception = assertThrows(ServiceException.class,
1222 1046 () -> groupMessageService.getGroupMessageList(1L, reqVO));
1223 1047 assertEquals(GROUP_MEMBER_NOT_IN_GROUP.getCode(), exception.getCode());
1224 1048 // 断言:不查询 DB
1225   - verify(groupMessageMapper, never()).selectHistoryList(anyLong(), anyLong(), anyInt(), any());
1226   - }
1227   -
1228   - // ========== delete read cursor 委托 ==========
1229   -
1230   - @Test
1231   - public void testDeleteReadMaxMessageId_delegatesToRedis() {
1232   - groupMessageService.deleteReadMaxMessageId(10L, 1L);
1233   -
1234   - verify(groupMessageReadRedisDAO).deleteReadMaxMessageId(10L, 1L);
1235   - }
1236   -
1237   - @Test
1238   - public void testDeleteReadMaxMessageIds_delegatesToRedis() {
1239   - groupMessageService.deleteReadMaxMessageIds(10L, ListUtil.of(1L, 2L));
1240   -
1241   - verify(groupMessageReadRedisDAO).deleteReadMaxMessageIds(10L, ListUtil.of(1L, 2L));
1242   - }
1243   -
1244   - @Test
1245   - public void testDeleteReadMaxMessageIdMap_delegatesToRedis() {
1246   - groupMessageService.deleteReadMaxMessageIdMap(10L);
1247   -
1248   - verify(groupMessageReadRedisDAO).deleteReadMaxMessageIdMap(10L);
  1049 + verify(groupMessageMapper, never()).selectHistoryListByUser(anyLong(), anyLong(), anyLong(), anyInt());
1249 1050 }
1250 1051  
1251 1052 }
... ...
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageServiceImplTest.java
... ... @@ -7,15 +7,20 @@ import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivate
7 7 import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageSendReqVO;
8 8 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO;
9 9 import cn.iocoder.yudao.module.im.dal.mysql.message.ImPrivateMessageMapper;
  10 +import cn.iocoder.yudao.module.im.enums.ImConversationTypeEnum;
  11 +import cn.iocoder.yudao.module.im.enums.message.ImMessageReceiptStatusEnum;
10 12 import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum;
11   -import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
  13 +import cn.iocoder.yudao.module.im.enums.ImContentTypeEnum;
12 14 import cn.iocoder.yudao.module.im.framework.config.ImProperties;
  15 +import cn.iocoder.yudao.module.im.service.conversation.ImConversationReadService;
13 16 import cn.iocoder.yudao.module.im.service.friend.ImFriendService;
14 17 import cn.iocoder.yudao.module.im.service.sensitiveword.ImSensitiveWordService;
15 18 import cn.iocoder.yudao.module.im.service.message.dto.ImPrivateMessageSendDTO;
16 19 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService;
17   -import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO;
18   -import cn.iocoder.yudao.module.im.service.websocket.dto.message.RecallMessage;
  20 +import cn.iocoder.yudao.module.im.service.websocket.notification.message.ImMessageReadNotification;
  21 +import cn.iocoder.yudao.module.im.service.websocket.notification.message.ImMessageReceiptNotification;
  22 +import cn.iocoder.yudao.module.im.service.websocket.notification.message.ImPrivateMessageNotification;
  23 +import cn.iocoder.yudao.module.im.dal.dataobject.message.content.RecallMessage;
19 24 import org.junit.jupiter.api.Test;
20 25 import org.mockito.ArgumentCaptor;
21 26 import org.mockito.InjectMocks;
... ... @@ -31,7 +36,7 @@ import static org.mockito.ArgumentMatchers.*;
31 36 import static org.mockito.Mockito.*;
32 37  
33 38 /**
34   - * IM 私聊消息 Service 单元测试
  39 + * {@link ImPrivateMessageServiceImpl} 的单元测试
35 40 *
36 41 * @author 芋道源码
37 42 */
... ... @@ -47,9 +52,10 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest {
47 52 @Mock
48 53 private ImSensitiveWordService sensitiveWordService;
49 54 @Mock
  55 + private ImConversationReadService conversationReadService;
  56 + @Mock
50 57 private ImWebSocketService imWebSocketService;
51 58  
52   - /** 用真实实例避免 NPE;默认值与生产保持一致(recallTimeoutMinutes=5、private/group read enabled=true);个别用例可改字段测分支 */
53 59 @Spy
54 60 private ImProperties imProperties = new ImProperties();
55 61  
... ... @@ -57,7 +63,7 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest {
57 63 ImPrivateMessageSendReqVO reqVO = new ImPrivateMessageSendReqVO();
58 64 reqVO.setClientMessageId("test-uuid-001");
59 65 reqVO.setReceiverId(2L);
60   - reqVO.setType(ImMessageTypeEnum.TEXT.getType());
  66 + reqVO.setType(ImContentTypeEnum.TEXT.getType());
61 67 reqVO.setContent("{\"content\":\"你好\"}");
62 68 return reqVO;
63 69 }
... ... @@ -83,8 +89,10 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest {
83 89 assertNotNull(result);
84 90 assertEquals(1L, result.getSenderId());
85 91 assertEquals(2L, result.getReceiverId());
86   - assertEquals(ImMessageTypeEnum.TEXT.getType(), result.getType());
87   - assertEquals(ImMessageStatusEnum.UNREAD.getStatus(), result.getStatus());
  92 + assertEquals(ImContentTypeEnum.TEXT.getType(), result.getType());
  93 + assertEquals(ImMessageStatusEnum.NORMAL.getStatus(), result.getStatus());
  94 + assertEquals(ImMessageReceiptStatusEnum.PENDING.getStatus(), result.getReceiptStatus(),
  95 + "用户私聊消息默认需要回执(PENDING)");
88 96 assertNotNull(result.getSendTime());
89 97  
90 98 // 验证调用
... ... @@ -92,8 +100,8 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest {
92 100 verify(sensitiveWordService).validateText(reqVO.getContent());
93 101 verify(privateMessageMapper).insert(any(ImPrivateMessageDO.class));
94 102 // 验证推送给接收方和发送方
95   - verify(imWebSocketService).sendPrivateMessageAsync(eq(2L), any(ImPrivateMessageDTO.class));
96   - verify(imWebSocketService).sendPrivateMessageAsync(eq(1L), any(ImPrivateMessageDTO.class));
  103 + verify(imWebSocketService).sendNotificationAsync(eq(2L), anyInt(), anyInt(), any());
  104 + verify(imWebSocketService).sendNotificationAsync(eq(1L), anyInt(), anyInt(), any());
97 105 }
98 106  
99 107 @Test
... ... @@ -162,38 +170,44 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest {
162 170  
163 171 @Test
164 172 public void testReadMessages_success() {
165   - // 准备:前端上报已读到 messageId=5;mapper 返回更新行数 2 表示有未读被翻转
166   - when(privateMessageMapper.updateBySenderIdAndReceiverIdAndIdLeAndStatus(
  173 + // 准备:前端上报已读到 messageId=5;mapper 返回更新行数 2 表示有待回执消息被标记完成
  174 + when(privateMessageMapper.updateBySenderIdAndReceiverIdAndIdLeAndReceiptStatus(
167 175 eq(2L), eq(1L), eq(5L),
168   - eq(ImMessageStatusEnum.UNREAD.getStatus()), any(ImPrivateMessageDO.class)))
  176 + eq(ImMessageReceiptStatusEnum.PENDING.getStatus()), any(ImPrivateMessageDO.class)))
169 177 .thenReturn(2);
  178 + // 读位置前进 → 才下发事件
  179 + when(conversationReadService.updateConversationReadPosition(anyLong(), anyInt(), anyLong(), anyLong()))
  180 + .thenReturn(true);
170 181  
171 182 // 调用
172 183 privateMessageService.readPrivateMessages(1L, 2L, 5L);
173 184  
174   - // 断言:更新了消息状态
175   - verify(privateMessageMapper).updateBySenderIdAndReceiverIdAndIdLeAndStatus(
  185 + // 断言:把待回执(PENDING)消息标记为已完成(DONE);status 不再写 READ
  186 + verify(privateMessageMapper).updateBySenderIdAndReceiverIdAndIdLeAndReceiptStatus(
176 187 eq(2L), eq(1L), eq(5L),
177   - eq(ImMessageStatusEnum.UNREAD.getStatus()), any(ImPrivateMessageDO.class));
  188 + eq(ImMessageReceiptStatusEnum.PENDING.getStatus()), any(ImPrivateMessageDO.class));
178 189  
179 190 // 断言:发送了 READ + RECEIPT 事件,payload 字段正确
180 191 ArgumentCaptor<Long> userCaptor = ArgumentCaptor.forClass(Long.class);
181   - ArgumentCaptor<ImPrivateMessageDTO> contentCaptor = ArgumentCaptor.forClass(ImPrivateMessageDTO.class);
182   - verify(imWebSocketService, times(2)).sendPrivateMessageAsync(
183   - userCaptor.capture(), contentCaptor.capture());
  192 + ArgumentCaptor<Integer> contentTypeCaptor = ArgumentCaptor.forClass(Integer.class);
  193 + ArgumentCaptor<Object> payloadCaptor = ArgumentCaptor.forClass(Object.class);
  194 + verify(imWebSocketService, times(2)).sendNotificationAsync(
  195 + userCaptor.capture(), eq(ImConversationTypeEnum.PRIVATE.getType()),
  196 + contentTypeCaptor.capture(), payloadCaptor.capture());
184 197  
185 198 // 第一次:发给自己的 READ 事件
186 199 assertEquals(1L, userCaptor.getAllValues().get(0));
187   - ImPrivateMessageDTO readPayload = contentCaptor.getAllValues().get(0);
188   - assertEquals(ImMessageTypeEnum.READ.getType(), readPayload.getType());
  200 + assertEquals(ImContentTypeEnum.READ.getType(), contentTypeCaptor.getAllValues().get(0));
  201 + ImMessageReadNotification readPayload = (ImMessageReadNotification) payloadCaptor.getAllValues().get(0);
189 202 assertEquals(1L, readPayload.getSenderId());
190 203 assertEquals(2L, readPayload.getReceiverId());
191 204 assertEquals(5L, readPayload.getId(), "READ id 应为前端上报的 messageId");
192 205  
193 206 // 第二次:发给对方的 RECEIPT 事件
194 207 assertEquals(2L, userCaptor.getAllValues().get(1));
195   - ImPrivateMessageDTO receiptPayload = contentCaptor.getAllValues().get(1);
196   - assertEquals(ImMessageTypeEnum.RECEIPT.getType(), receiptPayload.getType());
  208 + assertEquals(ImContentTypeEnum.RECEIPT.getType(), contentTypeCaptor.getAllValues().get(1));
  209 + ImMessageReceiptNotification receiptPayload =
  210 + (ImMessageReceiptNotification) payloadCaptor.getAllValues().get(1);
197 211 assertEquals(5L, receiptPayload.getId(), "RECEIPT id 应为前端上报的 messageId");
198 212 }
199 213  
... ... @@ -204,7 +218,7 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest {
204 218 // 准备
205 219 ImPrivateMessageDO message = ImPrivateMessageDO.builder()
206 220 .id(10L).senderId(1L).receiverId(2L)
207   - .status(ImMessageStatusEnum.UNREAD.getStatus())
  221 + .status(ImMessageStatusEnum.NORMAL.getStatus())
208 222 .sendTime(LocalDateTime.now()).build(); // 刚发送,5 分钟内
209 223 when(privateMessageMapper.selectById(10L)).thenReturn(message);
210 224 when(privateMessageMapper.updateById(any(ImPrivateMessageDO.class))).thenReturn(1);
... ... @@ -219,7 +233,7 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest {
219 233 verify(privateMessageMapper).updateById(any(ImPrivateMessageDO.class));
220 234 verify(privateMessageMapper).insert(any(ImPrivateMessageDO.class));
221 235 // 验证推送了消息(给接收方和发送方)
222   - verify(imWebSocketService, times(2)).sendPrivateMessageAsync(anyLong(), any(ImPrivateMessageDTO.class));
  236 + verify(imWebSocketService, times(2)).sendNotificationAsync(anyLong(), anyInt(), anyInt(), any());
223 237 }
224 238  
225 239 @Test
... ... @@ -227,7 +241,7 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest {
227 241 // 准备
228 242 ImPrivateMessageDO message = ImPrivateMessageDO.builder()
229 243 .id(10L).senderId(2L).receiverId(1L)
230   - .status(ImMessageStatusEnum.UNREAD.getStatus())
  244 + .status(ImMessageStatusEnum.NORMAL.getStatus())
231 245 .sendTime(LocalDateTime.now()).build();
232 246 when(privateMessageMapper.selectById(10L)).thenReturn(message);
233 247  
... ... @@ -268,7 +282,7 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest {
268 282 // 准备:消息发送于 10 分钟前(超过 5 分钟窗口)
269 283 ImPrivateMessageDO message = ImPrivateMessageDO.builder()
270 284 .id(10L).senderId(1L).receiverId(2L)
271   - .status(ImMessageStatusEnum.UNREAD.getStatus())
  285 + .status(ImMessageStatusEnum.NORMAL.getStatus())
272 286 .sendTime(LocalDateTime.now().minusMinutes(10)).build();
273 287 when(privateMessageMapper.selectById(10L)).thenReturn(message);
274 288  
... ... @@ -295,7 +309,7 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest {
295 309 assertEquals(MESSAGE_SENSITIVE_WORD_BLOCKED.getCode(), exception.getCode());
296 310 // 断言:不入库、不推送
297 311 verify(privateMessageMapper, never()).insert(any(ImPrivateMessageDO.class));
298   - verify(imWebSocketService, never()).sendPrivateMessageAsync(anyLong(), any(ImPrivateMessageDTO.class));
  312 + verify(imWebSocketService, never()).sendNotificationAsync(anyLong(), anyInt(), anyInt(), any());
299 313 }
300 314  
301 315 @Test
... ... @@ -308,33 +322,31 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest {
308 322 () -> privateMessageService.readPrivateMessages(1L, 2L, 5L));
309 323 assertEquals(MESSAGE_PRIVATE_READ_DISABLED.getCode(), exception.getCode());
310 324 // 断言:不更新消息状态、不推送
311   - verify(privateMessageMapper, never()).updateBySenderIdAndReceiverIdAndIdLeAndStatus(
  325 + verify(privateMessageMapper, never()).updateBySenderIdAndReceiverIdAndIdLeAndReceiptStatus(
312 326 anyLong(), anyLong(), anyLong(), anyInt(), any(ImPrivateMessageDO.class));
313   - verify(imWebSocketService, never()).sendPrivateMessageAsync(anyLong(), any(ImPrivateMessageDTO.class));
  327 + verify(imWebSocketService, never()).sendNotificationAsync(anyLong(), anyInt(), anyInt(), any());
314 328 }
315 329  
316 330 @Test
317   - public void testReadMessages_noUnread() {
318   - // 准备:mapper 返回 0 表示 id <= messageId 范围内没有未读消息
319   - when(privateMessageMapper.updateBySenderIdAndReceiverIdAndIdLeAndStatus(
320   - eq(2L), eq(1L), eq(5L),
321   - eq(ImMessageStatusEnum.UNREAD.getStatus()), any(ImPrivateMessageDO.class)))
322   - .thenReturn(0);
  331 + public void testReadMessages_notAdvanced() {
  332 + // 准备:读位置未前进(已读过 / CAS 失败),不下发事件
  333 + when(conversationReadService.updateConversationReadPosition(anyLong(), anyInt(), anyLong(), anyLong()))
  334 + .thenReturn(false);
323 335  
324 336 // 调用
325 337 privateMessageService.readPrivateMessages(1L, 2L, 5L);
326 338  
327   - // 断言:不推送 READ / RECEIPT
328   - verify(imWebSocketService, never()).sendPrivateMessageAsync(anyLong(), any(ImPrivateMessageDTO.class));
  339 + // 断言:读位置没前进,不推送 READ / RECEIPT
  340 + verify(imWebSocketService, never()).sendNotificationAsync(anyLong(), anyInt(), anyInt(), any());
329 341 }
330 342  
331 343 // ========== getMaxReadMessageId 测试 ==========
332 344  
333 345 @Test
334 346 public void testGetMaxReadMessageId_hit() {
335   - // 准备:对方读到我发的最大 id=10
336   - when(privateMessageMapper.selectMaxIdBySenderIdAndReceiverIdAndStatus(
337   - 1L, 2L, ImMessageStatusEnum.READ.getStatus())).thenReturn(10L);
  347 + // 准备:对方(2) 在与我(1) 的会话里读位置=10
  348 + when(conversationReadService.getConversationReadMessageId(eq(2L), anyInt(), eq(1L)))
  349 + .thenReturn(10L);
338 350  
339 351 // 调用
340 352 Long result = privateMessageService.getMaxReadMessageId(1L, 2L);
... ... @@ -345,9 +357,9 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest {
345 357  
346 358 @Test
347 359 public void testGetMaxReadMessageId_miss() {
348   - // 准备:对方一条都没读过
349   - when(privateMessageMapper.selectMaxIdBySenderIdAndReceiverIdAndStatus(
350   - 1L, 2L, ImMessageStatusEnum.READ.getStatus())).thenReturn(null);
  360 + // 准备:对方一条都没读过(读位置为 null)
  361 + when(conversationReadService.getConversationReadMessageId(eq(2L), anyInt(), eq(1L)))
  362 + .thenReturn(null);
351 363  
352 364 // 调用
353 365 Long result = privateMessageService.getMaxReadMessageId(1L, 2L);
... ... @@ -367,13 +379,13 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest {
367 379 assertEquals(MESSAGE_PRIVATE_READ_DISABLED.getCode(), exception.getCode());
368 380 }
369 381  
370   - // ========== sendPrivateMessage(senderId, dto):helper 行为 ==========
  382 + // ========== DTO 私聊消息 ==========
371 383  
372 384 @Test
373 385 public void testSendPrivateMessage_dto_persistsAndSerializesPojoContent() {
374 386 // 准备:persistent=true 类型 + POJO content
375 387 ImPrivateMessageSendDTO dto = new ImPrivateMessageSendDTO()
376   - .setReceiverId(2L).setType(ImMessageTypeEnum.RECALL.getType())
  388 + .setReceiverId(2L).setType(ImContentTypeEnum.RECALL.getType())
377 389 .setContent(new RecallMessage().setMessageId(50L));
378 390  
379 391 privateMessageService.sendPrivateMessage(1L, dto);
... ... @@ -384,27 +396,27 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest {
384 396 ImPrivateMessageDO message = captor.getValue();
385 397 assertEquals(1L, message.getSenderId());
386 398 assertEquals(2L, message.getReceiverId());
387   - assertEquals(ImMessageTypeEnum.RECALL.getType(), message.getType());
  399 + assertEquals(ImContentTypeEnum.RECALL.getType(), message.getType());
388 400 assertEquals("{\"messageId\":50}", message.getContent());
389   - assertEquals(ImMessageStatusEnum.UNREAD.getStatus(), message.getStatus());
  401 + assertEquals(ImMessageStatusEnum.NORMAL.getStatus(), message.getStatus());
390 402 assertNotNull(message.getClientMessageId());
391 403 assertNotNull(message.getSendTime());
392 404 // 断言:sender + receiver 双端推送
393   - verify(imWebSocketService).sendPrivateMessageAsync(eq(1L), any(ImPrivateMessageDTO.class));
394   - verify(imWebSocketService).sendPrivateMessageAsync(eq(2L), any(ImPrivateMessageDTO.class));
  405 + verify(imWebSocketService).sendNotificationAsync(eq(1L), anyInt(), anyInt(), any());
  406 + verify(imWebSocketService).sendNotificationAsync(eq(2L), anyInt(), anyInt(), any());
395 407 }
396 408  
397 409 @Test
398 410 public void testSendPrivateMessage_dto_nonPersistentTypeNotInserted() {
399 411 // 准备:persistent=false 类型(FRIEND_DELETE 通知)→ 不入库;仅推 sender 多端,receiver 不感知
400 412 ImPrivateMessageSendDTO dto = new ImPrivateMessageSendDTO()
401   - .setReceiverId(2L).setType(ImMessageTypeEnum.FRIEND_DELETE.getType());
  413 + .setReceiverId(2L).setType(ImContentTypeEnum.FRIEND_DELETE.getType());
402 414  
403 415 privateMessageService.sendPrivateMessage(1L, dto);
404 416  
405 417 verify(privateMessageMapper, never()).insert(any(ImPrivateMessageDO.class));
406   - verify(imWebSocketService).sendPrivateMessageAsync(eq(1L), any(ImPrivateMessageDTO.class));
407   - verify(imWebSocketService, never()).sendPrivateMessageAsync(eq(2L), any(ImPrivateMessageDTO.class));
  418 + verify(imWebSocketService).sendNotificationAsync(eq(1L), anyInt(), anyInt(), any());
  419 + verify(imWebSocketService, never()).sendNotificationAsync(eq(2L), anyInt(), anyInt(), any());
408 420 }
409 421  
410 422 // ========== getPrivateMessageList ==========
... ...
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/rtc/ImRtcCallServiceImplTest.java
... ... @@ -20,7 +20,6 @@ import cn.iocoder.yudao.module.im.enums.rtc.ImRtcParticipantStatusEnum;
20 20 import cn.iocoder.yudao.module.im.framework.config.ImProperties;
21 21 import cn.iocoder.yudao.module.im.service.group.ImGroupMemberService;
22 22 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService;
23   -import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO;
24 23 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
25 24 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
26 25 import org.junit.jupiter.api.Test;
... ... @@ -44,6 +43,11 @@ import static org.junit.jupiter.api.Assertions.*;
44 43 import static org.mockito.ArgumentMatchers.*;
45 44 import static org.mockito.Mockito.*;
46 45  
  46 +/**
  47 + * {@link ImRtcCallServiceImpl} 的单元测试
  48 + *
  49 + * @author 芋道源码
  50 + */
47 51 public class ImRtcCallServiceImplTest extends BaseMockitoUnitTest {
48 52  
49 53 @InjectMocks
... ... @@ -64,7 +68,7 @@ public class ImRtcCallServiceImplTest extends BaseMockitoUnitTest {
64 68 @Mock
65 69 private ImGroupMemberService groupMemberService;
66 70  
67   - // ========== timeoutInvitingParticipants(Job 入口)==========
  71 + // ========== timeoutInvitingParticipants ==========
68 72  
69 73 @Test
70 74 public void testTimeoutInvitingParticipants_emptyCandidates_returnsZeroAndNoDownstream() {
... ... @@ -144,7 +148,7 @@ public class ImRtcCallServiceImplTest extends BaseMockitoUnitTest {
144 148  
145 149 // 断言:成功 1 个;NO_ANSWER 信令推到主叫;不触发 endSession
146 150 assertEquals(1, result);
147   - verify(webSocketService).sendPrivateMessageAsync(eq(200L), any(ImPrivateMessageDTO.class));
  151 + verify(webSocketService).sendNotificationAsync(eq(200L), anyInt(), anyInt(), any());
148 152 verify(rtcCallMapper, never()).updateByIdAndStatusIn(any(), anyCollection(), any());
149 153 }
150 154  
... ... @@ -167,7 +171,7 @@ public class ImRtcCallServiceImplTest extends BaseMockitoUnitTest {
167 171 verifyNoInteractions(webSocketService);
168 172 }
169 173  
170   - // ========== noAnswerCallCheck(前端 timer 入口)==========
  174 + // ========== noAnswerCallCheck ==========
171 175  
172 176 @Test
173 177 public void testNoAnswerCallCheck_authFails_silentNoOp() {
... ... @@ -239,7 +243,7 @@ public class ImRtcCallServiceImplTest extends BaseMockitoUnitTest {
239 243 rtcCallService.noAnswerCallCheck(100L, "r1");
240 244  
241 245 // 断言:NO_ANSWER 信令推到主叫 200L;不触发 endSession
242   - verify(webSocketService).sendPrivateMessageAsync(eq(200L), any(ImPrivateMessageDTO.class));
  246 + verify(webSocketService).sendNotificationAsync(eq(200L), anyInt(), anyInt(), any());
243 247 verify(rtcCallMapper, never()).updateByIdAndStatusIn(any(), anyCollection(), any());
244 248 }
245 249  
... ...
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/sensitiveword/ImSensitiveWordServiceImplTest.java
... ... @@ -24,7 +24,7 @@ import static org.junit.jupiter.api.Assertions.*;
24 24 import static org.mockito.Mockito.*;
25 25  
26 26 /**
27   - * IM 敏感词 Service 单元测试
  27 + * {@link ImSensitiveWordServiceImpl} 的单元测试
28 28 *
29 29 * @author 芋道源码
30 30 */
... ...
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/statistics/ImStatisticsManagerServiceImplTest.java
... ... @@ -19,7 +19,7 @@ import static org.mockito.ArgumentMatchers.eq;
19 19 import static org.mockito.Mockito.when;
20 20  
21 21 /**
22   - * IM 数据看板 Service 单元测试
  22 + * {@link ImStatisticsManagerServiceImpl} 的单元测试
23 23 *
24 24 * @author 芋道源码
25 25 */
... ... @@ -72,7 +72,7 @@ public class ImStatisticsManagerServiceImplTest extends BaseMockitoUnitTest {
72 72 assertEquals(456L, service.getGroupMessageCount(BEGIN, END));
73 73 }
74 74  
75   - // ========== 每日序列 toDailyCountMap ==========
  75 + // ========== 每日序列 ==========
76 76  
77 77 @Test
78 78 public void testGetNewUserDailyCountMap_dateConvert() {
... ...
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/websocket/ImWebSocketServiceImplTest.java
... ... @@ -4,8 +4,11 @@ import cn.hutool.core.collection.ListUtil;
4 4 import cn.hutool.extra.spring.SpringUtil;
5 5 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
6 6 import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
7   -import cn.iocoder.yudao.module.im.service.websocket.dto.ImGroupMessageDTO;
8   -import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO;
  7 +import cn.iocoder.yudao.module.im.enums.ImContentTypeEnum;
  8 +import cn.iocoder.yudao.module.im.enums.ImConversationTypeEnum;
  9 +import cn.iocoder.yudao.module.im.service.websocket.notification.ImNotificationWebSocketDTO;
  10 +import cn.iocoder.yudao.module.im.service.websocket.notification.message.ImGroupMessageNotification;
  11 +import cn.iocoder.yudao.module.im.service.websocket.notification.message.ImPrivateMessageNotification;
9 12 import cn.iocoder.yudao.module.infra.api.websocket.WebSocketSenderApi;
10 13 import org.junit.jupiter.api.AfterEach;
11 14 import org.junit.jupiter.api.Test;
... ... @@ -21,10 +24,11 @@ import java.util.List;
21 24  
22 25 import static org.junit.jupiter.api.Assertions.assertEquals;
23 26 import static org.mockito.ArgumentMatchers.eq;
  27 +import static org.mockito.ArgumentMatchers.argThat;
24 28 import static org.mockito.Mockito.*;
25 29  
26 30 /**
27   - * IM WebSocket 推送 Service 单元测试
  31 + * {@link ImWebSocketServiceImpl} 的单元测试
28 32 *
29 33 * @author 芋道源码
30 34 */
... ... @@ -44,28 +48,32 @@ public class ImWebSocketServiceImplTest extends BaseMockitoUnitTest {
44 48 }
45 49 }
46 50  
47   - // ========== sendPrivateMessageAsync ==========
  51 + // ========== 私聊推送 ==========
48 52  
49 53 @Test
50   - public void testSendPrivateMessageAsync_noTransactionSendsImmediately() {
  54 + public void testSendNotificationAsync_private_noTransactionSendsImmediately() {
51 55 try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
52 56 springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImWebSocketServiceImpl.class)))
53 57 .thenReturn(imWebSocketService);
54 58  
55 59 // 准备
56   - ImPrivateMessageDTO dto = new ImPrivateMessageDTO().setSenderId(1L).setReceiverId(2L);
  60 + ImPrivateMessageNotification dto = new ImPrivateMessageNotification().setSenderId(1L).setReceiverId(2L)
  61 + .setType(ImContentTypeEnum.TEXT.getType());
57 62  
58 63 // 调用:无事务,应立即发送
59   - imWebSocketService.sendPrivateMessageAsync(2L, dto);
  64 + imWebSocketService.sendNotificationAsync(2L, ImConversationTypeEnum.PRIVATE.getType(),
  65 + ImContentTypeEnum.TEXT.getType(), dto);
60 66  
61 67 // 断言
62 68 verify(webSocketSenderApi).sendObject(
63   - eq(UserTypeEnum.ADMIN.getValue()), eq(2L), eq(ImPrivateMessageDTO.TYPE), eq(dto));
  69 + eq(UserTypeEnum.ADMIN.getValue()), eq(2L), eq(ImNotificationWebSocketDTO.TYPE),
  70 + argThat(actual -> isNotification(actual, ImConversationTypeEnum.PRIVATE,
  71 + ImContentTypeEnum.TEXT, dto)));
64 72 }
65 73 }
66 74  
67 75 @Test
68   - public void testSendPrivateMessageAsync_inTransactionDeferredUntilCommit() {
  76 + public void testSendNotificationAsync_private_inTransactionDeferredUntilCommit() {
69 77 try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
70 78 springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImWebSocketServiceImpl.class)))
71 79 .thenReturn(imWebSocketService);
... ... @@ -73,10 +81,12 @@ public class ImWebSocketServiceImplTest extends BaseMockitoUnitTest {
73 81 // 准备:开启事务同步
74 82 TransactionSynchronizationManager.initSynchronization();
75 83 try {
76   - ImPrivateMessageDTO dto = new ImPrivateMessageDTO().setSenderId(1L).setReceiverId(2L);
  84 + ImPrivateMessageNotification dto = new ImPrivateMessageNotification().setSenderId(1L).setReceiverId(2L)
  85 + .setType(ImContentTypeEnum.TEXT.getType());
77 86  
78 87 // 调用
79   - imWebSocketService.sendPrivateMessageAsync(2L, dto);
  88 + imWebSocketService.sendNotificationAsync(2L, ImConversationTypeEnum.PRIVATE.getType(),
  89 + ImContentTypeEnum.TEXT.getType(), dto);
80 90  
81 91 // 断言:事务未提交,未推送
82 92 verify(webSocketSenderApi, never()).sendObject(anyInt(), anyLong(), anyString(), any());
... ... @@ -89,49 +99,60 @@ public class ImWebSocketServiceImplTest extends BaseMockitoUnitTest {
89 99  
90 100 // 断言:提交后推送
91 101 verify(webSocketSenderApi).sendObject(
92   - eq(UserTypeEnum.ADMIN.getValue()), eq(2L), eq(ImPrivateMessageDTO.TYPE), eq(dto));
  102 + eq(UserTypeEnum.ADMIN.getValue()), eq(2L), eq(ImNotificationWebSocketDTO.TYPE),
  103 + argThat(actual -> isNotification(actual, ImConversationTypeEnum.PRIVATE,
  104 + ImContentTypeEnum.TEXT, dto)));
93 105 } finally {
94 106 TransactionSynchronizationManager.clear();
95 107 }
96 108 }
97 109 }
98 110  
99   - // ========== sendGroupMessageAsync ==========
  111 + // ========== 群聊推送 ==========
100 112  
101 113 @Test
102   - public void testSendGroupMessageAsync_fanOutToAllUsers() {
  114 + public void testSendNotificationAsync_group_fanOutToAllUsers() {
103 115 try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
104 116 springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImWebSocketServiceImpl.class)))
105 117 .thenReturn(imWebSocketService);
106 118  
107   - ImGroupMessageDTO dto = new ImGroupMessageDTO();
  119 + ImGroupMessageNotification dto = new ImGroupMessageNotification();
108 120 dto.setGroupId(10L);
109 121 dto.setSenderId(1L);
  122 + dto.setType(ImContentTypeEnum.TEXT.getType());
110 123  
111   - imWebSocketService.sendGroupMessageAsync(ListUtil.of(1L, 2L, 3L), dto);
  124 + imWebSocketService.sendNotificationAsync(ListUtil.of(1L, 2L, 3L),
  125 + ImConversationTypeEnum.GROUP.getType(), ImContentTypeEnum.TEXT.getType(), dto);
112 126  
113 127 verify(webSocketSenderApi).sendObject(
114   - eq(UserTypeEnum.ADMIN.getValue()), eq(1L), eq(ImGroupMessageDTO.TYPE), eq(dto));
  128 + eq(UserTypeEnum.ADMIN.getValue()), eq(1L), eq(ImNotificationWebSocketDTO.TYPE),
  129 + argThat(actual -> isNotification(actual, ImConversationTypeEnum.GROUP,
  130 + ImContentTypeEnum.TEXT, dto)));
115 131 verify(webSocketSenderApi).sendObject(
116   - eq(UserTypeEnum.ADMIN.getValue()), eq(2L), eq(ImGroupMessageDTO.TYPE), eq(dto));
  132 + eq(UserTypeEnum.ADMIN.getValue()), eq(2L), eq(ImNotificationWebSocketDTO.TYPE),
  133 + argThat(actual -> isNotification(actual, ImConversationTypeEnum.GROUP,
  134 + ImContentTypeEnum.TEXT, dto)));
117 135 verify(webSocketSenderApi).sendObject(
118   - eq(UserTypeEnum.ADMIN.getValue()), eq(3L), eq(ImGroupMessageDTO.TYPE), eq(dto));
  136 + eq(UserTypeEnum.ADMIN.getValue()), eq(3L), eq(ImNotificationWebSocketDTO.TYPE),
  137 + argThat(actual -> isNotification(actual, ImConversationTypeEnum.GROUP,
  138 + ImContentTypeEnum.TEXT, dto)));
119 139 }
120 140 }
121 141  
122 142 @Test
123   - public void testSendGroupMessageAsync_senderExceptionDoesNotBreakOthers() {
  143 + public void testSendNotificationAsync_group_senderExceptionDoesNotBreakOthers() {
124 144 try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
125 145 springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImWebSocketServiceImpl.class)))
126 146 .thenReturn(imWebSocketService);
127 147  
128   - ImGroupMessageDTO dto = new ImGroupMessageDTO();
  148 + ImGroupMessageNotification dto = new ImGroupMessageNotification();
129 149 dto.setGroupId(10L);
130 150 // 给 1 号用户推送时抛异常,不能影响 2/3 号
131 151 doThrow(new RuntimeException("user offline"))
132 152 .when(webSocketSenderApi).sendObject(anyInt(), eq(1L), anyString(), any());
133 153  
134   - imWebSocketService.sendGroupMessageAsync(ListUtil.of(1L, 2L, 3L), dto);
  154 + imWebSocketService.sendNotificationAsync(ListUtil.of(1L, 2L, 3L),
  155 + ImConversationTypeEnum.GROUP.getType(), ImContentTypeEnum.TEXT.getType(), dto);
135 156  
136 157 // 2L 和 3L 也都被推送
137 158 verify(webSocketSenderApi).sendObject(anyInt(), eq(2L), anyString(), any());
... ... @@ -140,60 +161,96 @@ public class ImWebSocketServiceImplTest extends BaseMockitoUnitTest {
140 161 }
141 162  
142 163 @Test
143   - public void testDoSendGroupMessage_emptyUserIds_noSend() {
144   - ImGroupMessageDTO dto = new ImGroupMessageDTO().setGroupId(10L);
  164 + public void testDoSendNotification_emptyUserIds_noSend() {
  165 + ImGroupMessageNotification dto = new ImGroupMessageNotification();
  166 + dto.setGroupId(10L);
  167 + dto.setType(ImContentTypeEnum.TEXT.getType());
  168 + ImNotificationWebSocketDTO notification = buildNotification(ImConversationTypeEnum.GROUP,
  169 + ImContentTypeEnum.TEXT, dto);
145 170  
146   - imWebSocketService.doSendGroupMessage(Collections.emptyList(), dto);
147   - imWebSocketService.doSendGroupMessage(null, dto);
  171 + imWebSocketService.doSendNotification(Collections.emptyList(), notification);
  172 + imWebSocketService.doSendNotification(null, notification);
148 173  
149 174 verifyNoInteractions(webSocketSenderApi);
150 175 }
151 176  
152 177 @Test
153   - public void testDoSendGroupMessage_distinctUserIds() {
154   - ImGroupMessageDTO dto = new ImGroupMessageDTO().setGroupId(10L);
  178 + public void testDoSendNotification_distinctUserIds() {
  179 + ImGroupMessageNotification dto = new ImGroupMessageNotification();
  180 + dto.setGroupId(10L);
  181 + dto.setType(ImContentTypeEnum.TEXT.getType());
  182 + ImNotificationWebSocketDTO notification = buildNotification(ImConversationTypeEnum.GROUP,
  183 + ImContentTypeEnum.TEXT, dto);
155 184  
156   - imWebSocketService.doSendGroupMessage(Arrays.asList(1L, 2L, 1L, null), dto);
  185 + imWebSocketService.doSendNotification(Arrays.asList(1L, 2L, 1L, null), notification);
157 186  
158 187 verify(webSocketSenderApi).sendObject(
159   - eq(UserTypeEnum.ADMIN.getValue()), eq(1L), eq(ImGroupMessageDTO.TYPE), eq(dto));
  188 + eq(UserTypeEnum.ADMIN.getValue()), eq(1L), eq(ImNotificationWebSocketDTO.TYPE),
  189 + argThat(actual -> isNotification(actual, ImConversationTypeEnum.GROUP,
  190 + ImContentTypeEnum.TEXT, dto)));
160 191 verify(webSocketSenderApi).sendObject(
161   - eq(UserTypeEnum.ADMIN.getValue()), eq(2L), eq(ImGroupMessageDTO.TYPE), eq(dto));
  192 + eq(UserTypeEnum.ADMIN.getValue()), eq(2L), eq(ImNotificationWebSocketDTO.TYPE),
  193 + argThat(actual -> isNotification(actual, ImConversationTypeEnum.GROUP,
  194 + ImContentTypeEnum.TEXT, dto)));
162 195 verifyNoMoreInteractions(webSocketSenderApi);
163 196 }
164 197  
165 198 @Test
166   - public void testSendPrivateMessageAsync_exceptionSwallowed() {
  199 + public void testSendNotificationAsync_private_exceptionSwallowed() {
167 200 try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
168 201 springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImWebSocketServiceImpl.class)))
169 202 .thenReturn(imWebSocketService);
170 203  
171 204 // 准备:sender 抛异常
172   - ImPrivateMessageDTO dto = new ImPrivateMessageDTO().setSenderId(1L).setReceiverId(2L);
  205 + ImPrivateMessageNotification dto = new ImPrivateMessageNotification().setSenderId(1L).setReceiverId(2L)
  206 + .setType(ImContentTypeEnum.TEXT.getType());
173 207 doThrow(new RuntimeException("user offline"))
174 208 .when(webSocketSenderApi).sendObject(anyInt(), anyLong(), anyString(), any());
175 209  
176 210 // 调用:异常应被吞掉,不向上抛
177   - imWebSocketService.sendPrivateMessageAsync(2L, dto);
  211 + imWebSocketService.sendNotificationAsync(2L, ImConversationTypeEnum.PRIVATE.getType(),
  212 + ImContentTypeEnum.TEXT.getType(), dto);
178 213  
179 214 verify(webSocketSenderApi).sendObject(anyInt(), eq(2L), anyString(), any());
180 215 }
181 216 }
182 217  
183 218 @Test
184   - public void testSendGroupMessageAsync_singleUserDefaultOverload() {
  219 + public void testSendNotificationAsync_group_singleUserDefaultOverload() {
185 220 try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
186 221 springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImWebSocketServiceImpl.class)))
187 222 .thenReturn(imWebSocketService);
188 223  
189   - ImGroupMessageDTO dto = new ImGroupMessageDTO();
  224 + ImGroupMessageNotification dto = new ImGroupMessageNotification();
190 225 dto.setGroupId(10L);
  226 + dto.setType(ImContentTypeEnum.TEXT.getType());
191 227  
192   - imWebSocketService.sendGroupMessageAsync(42L, dto);
  228 + imWebSocketService.sendNotificationAsync(42L, ImConversationTypeEnum.GROUP.getType(),
  229 + ImContentTypeEnum.TEXT.getType(), dto);
193 230  
194 231 verify(webSocketSenderApi).sendObject(
195   - eq(UserTypeEnum.ADMIN.getValue()), eq(42L), eq(ImGroupMessageDTO.TYPE), eq(dto));
  232 + eq(UserTypeEnum.ADMIN.getValue()), eq(42L), eq(ImNotificationWebSocketDTO.TYPE),
  233 + argThat(actual -> isNotification(actual, ImConversationTypeEnum.GROUP,
  234 + ImContentTypeEnum.TEXT, dto)));
196 235 }
197 236 }
198 237  
  238 + private static boolean isNotification(Object object, ImConversationTypeEnum conversationType,
  239 + ImContentTypeEnum contentType, Object payload) {
  240 + if (!(object instanceof ImNotificationWebSocketDTO notification)) {
  241 + return false;
  242 + }
  243 + return conversationType.getType().equals(notification.getConversationType())
  244 + && contentType.getType().equals(notification.getContentType())
  245 + && payload == notification.getPayload();
  246 + }
  247 +
  248 + private static ImNotificationWebSocketDTO buildNotification(ImConversationTypeEnum conversationType,
  249 + ImContentTypeEnum contentType, Object payload) {
  250 + return new ImNotificationWebSocketDTO()
  251 + .setConversationType(conversationType.getType())
  252 + .setContentType(contentType.getType())
  253 + .setPayload(payload);
  254 + }
  255 +
199 256 }
... ...
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/util/ImMessageUtilsTest.java
1 1 package cn.iocoder.yudao.module.im.util;
2 2  
3 3 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
4   -import cn.iocoder.yudao.module.im.service.websocket.dto.message.QuoteMessage;
  4 +import cn.iocoder.yudao.module.im.dal.dataobject.message.content.QuoteMessage;
5 5 import org.junit.jupiter.api.Test;
6 6  
7 7 import java.util.Map;
... ... @@ -12,7 +12,7 @@ import static org.junit.jupiter.api.Assertions.assertNull;
12 12 import static org.junit.jupiter.api.Assertions.assertTrue;
13 13  
14 14 /**
15   - * IM 消息内容工具类单元测试
  15 + * {@link ImMessageUtils} 的单元测试
16 16 *
17 17 * @author 芋道源码
18 18 */
... ...
yudao-module-im/src/test/resources/sql/clean.sql
1 1 DELETE FROM "im_private_message";
2 2 DELETE FROM "im_group_message";
  3 +DELETE FROM "im_conversation_read";
3 4 DELETE FROM "im_group";
4 5 DELETE FROM "im_group_member";
5 6 DELETE FROM "im_friend";
... ...
yudao-module-im/src/test/resources/sql/create_tables.sql
... ... @@ -6,6 +6,7 @@ CREATE TABLE IF NOT EXISTS &quot;im_private_message&quot; (
6 6 "type" smallint NOT NULL COMMENT '消息类型',
7 7 "content" varchar(8192) DEFAULT NULL COMMENT '消息内容',
8 8 "status" tinyint NOT NULL COMMENT '消息状态',
  9 + "receipt_status" tinyint NOT NULL DEFAULT 0 COMMENT '回执状态',
9 10 "send_time" timestamp NOT NULL COMMENT '发送时间',
10 11 "creator" varchar(64) DEFAULT '',
11 12 "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
... ... @@ -26,7 +27,7 @@ CREATE TABLE IF NOT EXISTS &quot;im_group_message&quot; (
26 27 "content" varchar(8192) DEFAULT NULL COMMENT '消息内容',
27 28 "status" tinyint NOT NULL COMMENT '消息状态',
28 29 "send_time" timestamp NOT NULL COMMENT '发送时间',
29   - "receiver_user_ids" varchar(1024) DEFAULT NULL COMMENT '定向接收用户编号列表',
  30 + "receiver_user_ids" text DEFAULT NULL COMMENT '定向接收用户编号列表',
30 31 "at_user_ids" varchar(1024) DEFAULT NULL COMMENT '@ 目标用户编号列表',
31 32 "receipt_status" tinyint NOT NULL DEFAULT 0 COMMENT '回执状态',
32 33 "creator" varchar(64) DEFAULT '',
... ... @@ -281,7 +282,7 @@ CREATE TABLE IF NOT EXISTS &quot;im_channel_message&quot; (
281 282 "material_id" bigint NOT NULL COMMENT '关联素材编号',
282 283 "type" smallint NOT NULL COMMENT '消息类型',
283 284 "content" varchar(8192) DEFAULT NULL COMMENT '消息内容;推送时 payload JSON 快照;不含富文本正文',
284   - "receiver_user_ids" varchar(1024) DEFAULT NULL COMMENT '接收人编号列表;逗号分隔;为空表示全员',
  285 + "receiver_user_ids" text DEFAULT NULL COMMENT '接收人编号列表;逗号分隔;为空表示全员',
285 286 "send_time" timestamp NOT NULL COMMENT '发送时间',
286 287 "creator" varchar(64) DEFAULT '',
287 288 "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
... ... @@ -292,6 +293,23 @@ CREATE TABLE IF NOT EXISTS &quot;im_channel_message&quot; (
292 293 PRIMARY KEY ("id")
293 294 ) COMMENT 'IM 频道消息表';
294 295  
  296 +CREATE TABLE IF NOT EXISTS "im_conversation_read" (
  297 + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY COMMENT '编号',
  298 + "user_id" bigint NOT NULL COMMENT '用户编号',
  299 + "conversation_type" tinyint NOT NULL COMMENT '会话类型',
  300 + "target_id" bigint NOT NULL COMMENT '目标编号',
  301 + "message_id" bigint NOT NULL COMMENT '最大已读消息编号',
  302 + "read_time" timestamp NOT NULL COMMENT '最近已读时间',
  303 + "creator" varchar(64) DEFAULT '',
  304 + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  305 + "updater" varchar(64) DEFAULT '',
  306 + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  307 + "deleted" bit NOT NULL DEFAULT FALSE,
  308 + "tenant_id" bigint NOT NULL DEFAULT 0,
  309 + PRIMARY KEY ("id"),
  310 + CONSTRAINT "uk_im_conversation_read_user_target" UNIQUE ("user_id", "conversation_type", "target_id", "tenant_id")
  311 +) COMMENT 'IM 会话读位置表';
  312 +
295 313 CREATE TABLE IF NOT EXISTS "im_sensitive_word" (
296 314 "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY COMMENT '编号',
297 315 "word" varchar(128) NOT NULL COMMENT '敏感词',
... ...