Commit 927b64fc4e7dbc582c31ee80a15d610b832daff9

Authored by YunaiV
1 parent 4b27620e

feat(im): 统一消息读位置和回执状态模型

- 新增 im_conversation_read 会话读位置表,并补充消息存储推拉相关索引
- 群消息固化 receiver_user_ids 快照,按可见成员快照拉取和统计回执
- 统一消息 status 为 NORMAL/RECALL,新增私聊 receipt_status 并复用统一回执状态
- 前端改用 receiptStatus 展示私聊已读、群回执和频道已读态
- 补齐私聊、群聊、频道 WebSocket 已读同步和离线补偿逻辑
- 更新 IM 消息状态、回执状态字典和管理后台展示
- 调整相关单测和测试建表脚本
Showing 25 changed files with 203 additions and 201 deletions
sql/mysql/ruoyi-vue-pro.sql
@@ -1377,14 +1377,11 @@ INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `st @@ -1377,14 +1377,11 @@ INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `st
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'); 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'); 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'); 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 (3520, 1, '未读', '0', 'im_private_message_status', 0, 'warning', '', '私聊=未读,群聊=正常', 'admin', '2026-04-30 11:35:07', '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 (3521, 2, '已撤回', '2', 'im_private_message_status', 0, 'danger', '', 'RECALL', 'admin', '2026-04-30 11:35:07', '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 (3522, 3, '已读', '3', 'im_private_message_status', 0, 'success', '', 'READ(仅私聊)', 'admin', '2026-04-30 11:35:07', 'admin', '2026-04-30 15:14:36', 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 (3525, 1, '正常', '0', 'im_group_message_status', 0, 'success', '', '群聊正常(初始状态)', 'admin', '2026-04-30 15:14:36', 'admin', '2026-04-30 15:14:36', 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 (3526, 2, '已撤回', '2', 'im_group_message_status', 0, 'danger', '', '群聊已撤回', 'admin', '2026-04-30 15:14:36', 'admin', '2026-04-30 15:14:36', b'0');  
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 (3530, 1, '不需要回执', '0', 'im_group_message_receipt_status', 0, 'info', '', 'NO_RECEIPT', 'admin', '2026-04-30 11:35:07', 'admin', '2026-04-30 11:35:07', b'0');  
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 (3531, 2, '待完成', '1', 'im_group_message_receipt_status', 0, 'warning', '', 'PENDING', 'admin', '2026-04-30 11:35:07', 'admin', '2026-04-30 11:35:07', b'0');  
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 (3532, 3, '已完成', '2', 'im_group_message_receipt_status', 0, 'success', '', 'DONE', 'admin', '2026-04-30 11:35:07', 'admin', '2026-04-30 11:35:07', 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');
1388 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'); 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');
1389 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'); 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');
1390 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'); 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');
@@ -1693,11 +1690,10 @@ INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creat @@ -1693,11 +1690,10 @@ INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creat
1693 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); 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);
1694 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'); 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');
1695 INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (2200, 'IM 消息类型', 'im_message_type', 0, '对应 ImMessageTypeEnum', 'admin', '2026-04-30 11:35:07', 'admin', '2026-04-30 11:35:07', 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_message_type', 0, '对应 ImMessageTypeEnum', 'admin', '2026-04-30 11:35:07', 'admin', '2026-04-30 11:35:07', b'0', NULL);
1696 -INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (2201, 'IM 私聊消息状态', 'im_private_message_status', 0, '对应 ImMessageStatusEnum;私聊 0=未读 / 2=已撤回 / 3=已读', 'admin', '2026-04-30 11:35:07', 'admin', '2026-04-30 15:14:36', b'0', NULL);  
1697 -INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (2202, 'IM 群消息回执状态', 'im_group_message_receipt_status', 0, '对应 ImGroupMessageReceiptStatusEnum', '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);
1698 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); 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);
1699 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); 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);
1700 -INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (2205, 'IM 群聊消息状态', 'im_group_message_status', 0, '对应 ImMessageStatusEnum;群聊 0=正常 / 2=已撤回(无未读概念)', 'admin', '2026-04-30 15:14:36', 'admin', '2026-04-30 15:14:36', 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);
1701 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); 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);
1702 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); 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);
1703 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); 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);
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/message/vo/group/ImGroupMessageManagerRespVO.java
@@ -43,7 +43,7 @@ public class ImGroupMessageManagerRespVO { @@ -43,7 +43,7 @@ public class ImGroupMessageManagerRespVO {
43 private List<String> atUserNicknames; 43 private List<String> atUserNicknames;
44 44
45 @Schema(description = "回执状态", example = "0") 45 @Schema(description = "回执状态", example = "0")
46 - private Integer receiptStatus; // 参见 ImGroupMessageReceiptStatusEnum 枚举类 46 + private Integer receiptStatus; // 参见 ImMessageReceiptStatusEnum 枚举类
47 47
48 @Schema(description = "发送时间", requiredMode = Schema.RequiredMode.REQUIRED) 48 @Schema(description = "发送时间", requiredMode = Schema.RequiredMode.REQUIRED)
49 private LocalDateTime sendTime; 49 private LocalDateTime sendTime;
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/manager/message/vo/privates/ImPrivateMessageManagerRespVO.java
@@ -36,6 +36,9 @@ public class ImPrivateMessageManagerRespVO { @@ -36,6 +36,9 @@ public class ImPrivateMessageManagerRespVO {
36 @Schema(description = "消息状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") 36 @Schema(description = "消息状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
37 private Integer status; // 参见 ImMessageStatusEnum 枚举类 37 private Integer status; // 参见 ImMessageStatusEnum 枚举类
38 38
  39 + @Schema(description = "回执状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
  40 + private Integer receiptStatus; // 参见 ImMessageReceiptStatusEnum 枚举类
  41 +
39 @Schema(description = "发送时间", requiredMode = Schema.RequiredMode.REQUIRED) 42 @Schema(description = "发送时间", requiredMode = Schema.RequiredMode.REQUIRED)
40 private LocalDateTime sendTime; 43 private LocalDateTime sendTime;
41 44
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,7 +5,7 @@ import cn.iocoder.yudao.framework.common.pojo.CommonResult;
5 import cn.iocoder.yudao.framework.common.util.object.BeanUtils; 5 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
6 import cn.iocoder.yudao.module.im.controller.admin.message.vo.channel.ImChannelMessagePullRespVO; 6 import cn.iocoder.yudao.module.im.controller.admin.message.vo.channel.ImChannelMessagePullRespVO;
7 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImChannelMessageDO; 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 import cn.iocoder.yudao.module.im.service.message.ImChannelMessageService; 9 import cn.iocoder.yudao.module.im.service.message.ImChannelMessageService;
10 import io.swagger.v3.oas.annotations.Operation; 10 import io.swagger.v3.oas.annotations.Operation;
11 import io.swagger.v3.oas.annotations.Parameter; 11 import io.swagger.v3.oas.annotations.Parameter;
@@ -51,14 +51,14 @@ public class ImChannelMessageController { @@ -51,14 +51,14 @@ public class ImChannelMessageController {
51 if (CollUtil.isEmpty(list)) { 51 if (CollUtil.isEmpty(list)) {
52 return success(Collections.emptyList()); 52 return success(Collections.emptyList());
53 } 53 }
54 - // 2. 按 Redis 已读游标补 status;device A 已读后 device B 拉到这条不再算入未读 54 + // 2. 按已读游标补 receiptStatus(已读 DONE / 未读 PENDING)
55 Map<Long, Long> readMaxByChannel = channelMessageService.getChannelReadMaxMessageIdMap( 55 Map<Long, Long> readMaxByChannel = channelMessageService.getChannelReadMaxMessageIdMap(
56 userId, convertSet(list, ImChannelMessageDO::getChannelId)); 56 userId, convertSet(list, ImChannelMessageDO::getChannelId));
57 return success(BeanUtils.toBean(list, ImChannelMessagePullRespVO.class, vo -> { 57 return success(BeanUtils.toBean(list, ImChannelMessagePullRespVO.class, vo -> {
58 Long readMax = readMaxByChannel.get(vo.getChannelId()); 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,7 +61,7 @@ public class ImPrivateMessageController {
61 61
62 @GetMapping("/max-read-message-id") 62 @GetMapping("/max-read-message-id")
63 @Operation(summary = "查询对方已读到我发的最大消息 id", 63 @Operation(summary = "查询对方已读到我发的最大消息 id",
64 - description = "用于多端 / 离线场景下的已读位置补齐:进入会话或断线重连后调用,结果用于翻转本地自发消息状态") 64 + description = "用于多端 / 离线场景下的已读位置补齐:进入会话或断线重连后调用,据此把本地自发消息更新为已读")
65 @Parameter(name = "peerId", description = "对方用户编号", required = true, example = "2") 65 @Parameter(name = "peerId", description = "对方用户编号", required = true, example = "2")
66 public CommonResult<Long> getMaxReadMessageId(@RequestParam("peerId") Long peerId) { 66 public CommonResult<Long> getMaxReadMessageId(@RequestParam("peerId") Long peerId) {
67 return success(privateMessageService.getMaxReadMessageId(getLoginUserId(), peerId)); 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,8 +24,8 @@ public class ImChannelMessagePullRespVO {
24 @Schema(description = "消息内容;payload JSON 快照", requiredMode = Schema.RequiredMode.REQUIRED) 24 @Schema(description = "消息内容;payload JSON 快照", requiredMode = Schema.RequiredMode.REQUIRED)
25 private String content; 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 @Schema(description = "发送时间", requiredMode = Schema.RequiredMode.REQUIRED) 30 @Schema(description = "发送时间", requiredMode = Schema.RequiredMode.REQUIRED)
31 private LocalDateTime sendTime; 31 private LocalDateTime sendTime;
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,7 +31,10 @@ public class ImPrivateMessageRespVO {
31 private String content; 31 private String content;
32 32
33 @Schema(description = "消息状态", example = "0") 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 @Schema(description = "发送时间") 39 @Schema(description = "发送时间")
37 private LocalDateTime sendTime; 40 private LocalDateTime sendTime;
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/vo/privates/ImPrivateMessageSendReqVO.java
@@ -33,6 +33,9 @@ public class ImPrivateMessageSendReqVO { @@ -33,6 +33,9 @@ public class ImPrivateMessageSendReqVO {
33 @NotEmpty(message = "消息内容不能为空") 33 @NotEmpty(message = "消息内容不能为空")
34 private String content; 34 private String content;
35 35
  36 + @Schema(description = "是否需要回执", example = "false")
  37 + private Boolean receipt;
  38 +
36 /** 39 /**
37 * 仅允许用户消息(normal)类型 40 * 仅允许用户消息(normal)类型
38 */ 41 */
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/ImGroupMessageDO.java
@@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.im.dal.dataobject.message; @@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.im.dal.dataobject.message;
3 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; 3 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
4 import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler; 4 import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler;
5 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO; 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 import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum; 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.message.ImMessageTypeEnum;
9 import com.baomidou.mybatisplus.annotation.KeySequence; 9 import com.baomidou.mybatisplus.annotation.KeySequence;
@@ -67,8 +67,6 @@ public class ImGroupMessageDO extends BaseDO { @@ -67,8 +67,6 @@ public class ImGroupMessageDO extends BaseDO {
67 * 消息状态 67 * 消息状态
68 * <p> 68 * <p>
69 * 枚举 {@link ImMessageStatusEnum} 69 * 枚举 {@link ImMessageStatusEnum}
70 - *  
71 - * 为什么没有 READ 状态?与单聊的差异:单聊用 UNREAD/READ 跟踪每条消息状态,群聊用 Redis 存储每个成员的已读位置(游标模型)  
72 */ 70 */
73 private Integer status; 71 private Integer status;
74 /** 72 /**
@@ -94,7 +92,7 @@ public class ImGroupMessageDO extends BaseDO { @@ -94,7 +92,7 @@ public class ImGroupMessageDO extends BaseDO {
94 /** 92 /**
95 * 回执状态 93 * 回执状态
96 * <p> 94 * <p>
97 - * 枚举 {@link ImGroupMessageReceiptStatusEnum} 95 + * 枚举 {@link ImMessageReceiptStatusEnum}
98 */ 96 */
99 private Integer receiptStatus; 97 private Integer receiptStatus;
100 98
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/ImPrivateMessageDO.java
1 package cn.iocoder.yudao.module.im.dal.dataobject.message; 1 package cn.iocoder.yudao.module.im.dal.dataobject.message;
2 2
3 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; 3 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
  4 +import cn.iocoder.yudao.module.im.enums.message.ImMessageReceiptStatusEnum;
4 import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum; 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.message.ImMessageTypeEnum;
6 import com.baomidou.mybatisplus.annotation.KeySequence; 7 import com.baomidou.mybatisplus.annotation.KeySequence;
@@ -65,6 +66,12 @@ public class ImPrivateMessageDO extends BaseDO { @@ -65,6 +66,12 @@ public class ImPrivateMessageDO extends BaseDO {
65 */ 66 */
66 private Integer status; 67 private Integer status;
67 /** 68 /**
  69 + * 回执状态
  70 + * <p>
  71 + * 枚举 {@link ImMessageReceiptStatusEnum}
  72 + */
  73 + private Integer receiptStatus;
  74 + /**
68 * 发送时间 75 * 发送时间
69 */ 76 */
70 private LocalDateTime sendTime; 77 private LocalDateTime sendTime;
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/message/ImGroupMessageMapper.java
@@ -7,7 +7,7 @@ import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX; @@ -7,7 +7,7 @@ import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX;
7 import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; 7 import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
8 import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.group.ImGroupMessageManagerPageReqVO; 8 import cn.iocoder.yudao.module.im.controller.admin.manager.message.vo.group.ImGroupMessageManagerPageReqVO;
9 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO; 9 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO;
10 -import cn.iocoder.yudao.module.im.enums.message.ImGroupMessageReceiptStatusEnum; 10 +import cn.iocoder.yudao.module.im.enums.message.ImMessageReceiptStatusEnum;
11 import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum; 11 import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum;
12 import org.apache.ibatis.annotations.Mapper; 12 import org.apache.ibatis.annotations.Mapper;
13 13
@@ -84,7 +84,7 @@ public interface ImGroupMessageMapper extends BaseMapperX&lt;ImGroupMessageDO&gt; { @@ -84,7 +84,7 @@ public interface ImGroupMessageMapper extends BaseMapperX&lt;ImGroupMessageDO&gt; {
84 default List<ImGroupMessageDO> selectListByGroupIdAndPendingReceipt(Long groupId, Long minId, Long maxId) { 84 default List<ImGroupMessageDO> selectListByGroupIdAndPendingReceipt(Long groupId, Long minId, Long maxId) {
85 return selectList(new LambdaQueryWrapperX<ImGroupMessageDO>() 85 return selectList(new LambdaQueryWrapperX<ImGroupMessageDO>()
86 .eq(ImGroupMessageDO::getGroupId, groupId) 86 .eq(ImGroupMessageDO::getGroupId, groupId)
87 - .eq(ImGroupMessageDO::getReceiptStatus, ImGroupMessageReceiptStatusEnum.PENDING.getStatus()) 87 + .eq(ImGroupMessageDO::getReceiptStatus, ImMessageReceiptStatusEnum.PENDING.getStatus())
88 .gt(minId != null, ImGroupMessageDO::getId, minId) 88 .gt(minId != null, ImGroupMessageDO::getId, minId)
89 .le(ImGroupMessageDO::getId, maxId) 89 .le(ImGroupMessageDO::getId, maxId)
90 .ne(ImGroupMessageDO::getStatus, ImMessageStatusEnum.RECALL.getStatus())); 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,23 +67,13 @@ public interface ImPrivateMessageMapper extends BaseMapperX&lt;ImPrivateMessageDO&gt;
67 .eq(ImPrivateMessageDO::getClientMessageId, clientMessageId)); 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 return update(updateObj, new LambdaQueryWrapperX<ImPrivateMessageDO>() 72 return update(updateObj, new LambdaQueryWrapperX<ImPrivateMessageDO>()
83 .eq(ImPrivateMessageDO::getSenderId, senderId) 73 .eq(ImPrivateMessageDO::getSenderId, senderId)
84 .eq(ImPrivateMessageDO::getReceiverId, receiverId) 74 .eq(ImPrivateMessageDO::getReceiverId, receiverId)
85 .le(ImPrivateMessageDO::getId, maxMessageId) 75 .le(ImPrivateMessageDO::getId, maxMessageId)
86 - .eq(ImPrivateMessageDO::getStatus, whereStatus)); 76 + .eq(ImPrivateMessageDO::getReceiptStatus, whereReceiptStatus));
87 } 77 }
88 78
89 default PageResult<ImPrivateMessageDO> selectPage(ImPrivateMessageManagerPageReqVO reqVO) { 79 default PageResult<ImPrivateMessageDO> selectPage(ImPrivateMessageManagerPageReqVO reqVO) {
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,19 +7,19 @@ import lombok.Getter;
7 import java.util.Arrays; 7 import java.util.Arrays;
8 8
9 /** 9 /**
10 - * IM 群消息回执状态枚举 10 + * IM 消息回执状态枚举
11 * 11 *
12 * @author 芋道源码 12 * @author 芋道源码
13 */ 13 */
14 @Getter 14 @Getter
15 @AllArgsConstructor 15 @AllArgsConstructor
16 -public enum ImGroupMessageReceiptStatusEnum implements ArrayValuable<Integer> { 16 +public enum ImMessageReceiptStatusEnum implements ArrayValuable<Integer> {
17 17
18 NO_RECEIPT(0, "不需要回执"), 18 NO_RECEIPT(0, "不需要回执"),
19 PENDING(1, "待完成"), 19 PENDING(1, "待完成"),
20 DONE(2, "已完成"); 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,9 +8,6 @@ import java.util.Arrays;
8 8
9 /** 9 /**
10 * IM 消息状态枚举 10 * IM 消息状态枚举
11 - * <p>  
12 - * 私聊:SENDING(-1, 仅客户端) / UNREAD(0) / RECALL(2) / READ(3)  
13 - * 群聊:SENDING(-1, 仅客户端) / UNREAD(0, 作为正常状态) / RECALL(2)  
14 * 11 *
15 * @author 芋道源码 12 * @author 芋道源码
16 */ 13 */
@@ -19,9 +16,8 @@ import java.util.Arrays; @@ -19,9 +16,8 @@ import java.util.Arrays;
19 public enum ImMessageStatusEnum implements ArrayValuable<Integer> { 16 public enum ImMessageStatusEnum implements ArrayValuable<Integer> {
20 17
21 SENDING(-1, "发送中"), // 仅客户端使用 18 SENDING(-1, "发送中"), // 仅客户端使用
22 - UNREAD(0, "未读"), // 私聊=未读,群聊=正常(初始状态)  
23 - RECALL(2, "已撤回"),  
24 - READ(3, "已读"); // 仅私聊使用;群聊已读通过 Redis 已读位置实现 19 + NORMAL(0, "正常"), // 私聊 / 群聊的正常(初始)状态
  20 + RECALL(2, "已撤回");
25 21
26 public static final Integer[] ARRAYS = Arrays.stream(values()).map(ImMessageStatusEnum::getStatus).toArray(Integer[]::new); 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/service/message/ImGroupMessageServiceImpl.java
@@ -18,7 +18,7 @@ import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO; @@ -18,7 +18,7 @@ import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO;
18 import cn.iocoder.yudao.module.im.dal.mysql.message.ImGroupMessageMapper; 18 import cn.iocoder.yudao.module.im.dal.mysql.message.ImGroupMessageMapper;
19 import cn.iocoder.yudao.module.im.enums.ImConversationTypeEnum; 19 import cn.iocoder.yudao.module.im.enums.ImConversationTypeEnum;
20 import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum; 20 import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum;
21 -import cn.iocoder.yudao.module.im.enums.message.ImGroupMessageReceiptStatusEnum; 21 +import cn.iocoder.yudao.module.im.enums.message.ImMessageReceiptStatusEnum;
22 import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum; 22 import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum;
23 import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; 23 import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
24 import cn.iocoder.yudao.module.im.framework.config.ImProperties; 24 import cn.iocoder.yudao.module.im.framework.config.ImProperties;
@@ -104,7 +104,7 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { @@ -104,7 +104,7 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService {
104 reqVO.setContent(normalizeQuoteContent(reqVO, senderMember, receiverUserIds)); 104 reqVO.setContent(normalizeQuoteContent(reqVO, senderMember, receiverUserIds));
105 // 2.3 构建并保存消息;唯一键冲突时回查已存在消息返回 105 // 2.3 构建并保存消息;唯一键冲突时回查已存在消息返回
106 ImGroupMessageDO message = BeanUtils.toBean(reqVO, ImGroupMessageDO.class, m -> m 106 ImGroupMessageDO message = BeanUtils.toBean(reqVO, ImGroupMessageDO.class, m -> m
107 - .setSenderId(senderId).setStatus(ImMessageStatusEnum.UNREAD.getStatus()).setSendTime(LocalDateTime.now()) 107 + .setSenderId(senderId).setStatus(ImMessageStatusEnum.NORMAL.getStatus()).setSendTime(LocalDateTime.now())
108 .setReceiverUserIds(new ArrayList<>(receiverUserIds)) 108 .setReceiverUserIds(new ArrayList<>(receiverUserIds))
109 .setReceiptStatus(resolveReceiptStatus(reqVO.getReceipt()))); 109 .setReceiptStatus(resolveReceiptStatus(reqVO.getReceipt())));
110 try { 110 try {
@@ -138,7 +138,7 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { @@ -138,7 +138,7 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService {
138 ImGroupMessageDO message = new ImGroupMessageDO().setClientMessageId(IdUtil.fastSimpleUUID()) 138 ImGroupMessageDO message = new ImGroupMessageDO().setClientMessageId(IdUtil.fastSimpleUUID())
139 .setSenderId(senderId).setGroupId(dto.getGroupId()) 139 .setSenderId(senderId).setGroupId(dto.getGroupId())
140 .setType(dto.getType()).setContent(contentString) 140 .setType(dto.getType()).setContent(contentString)
141 - .setStatus(ImMessageStatusEnum.UNREAD.getStatus()).setSendTime(LocalDateTime.now()) 141 + .setStatus(ImMessageStatusEnum.NORMAL.getStatus()).setSendTime(LocalDateTime.now())
142 .setAtUserIds(dto.getAtUserIds()).setReceiverUserIds(new ArrayList<>(receiverUserIds)) 142 .setAtUserIds(dto.getAtUserIds()).setReceiverUserIds(new ArrayList<>(receiverUserIds))
143 .setReceiptStatus(resolveReceiptStatus(dto.getReceipt())); 143 .setReceiptStatus(resolveReceiptStatus(dto.getReceipt()));
144 // 1.3 按 type.persistent 决定是否入库 144 // 1.3 按 type.persistent 决定是否入库
@@ -206,52 +206,35 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { @@ -206,52 +206,35 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService {
206 // 2. 按 receiver_user_ids 快照过滤可见性,结果按 id 升序 206 // 2. 按 receiver_user_ids 快照过滤可见性,结果按 id 升序
207 List<ImGroupMessageDO> messages = groupMessageMapper.selectListByMinId(userId, groupIds, minId, minSendTime, size); 207 List<ImGroupMessageDO> messages = groupMessageMapper.selectListByMinId(userId, groupIds, minId, minSendTime, size);
208 208
209 - // 3. 按当前用户补齐:消息已读态、本人发送的回执消息的已读人数  
210 - appendMessageStatusAndReceipt(userId, messages); 209 + // 3. 补齐本人发送的回执消息的已读人数
  210 + appendMessageReceipt(userId, messages);
211 log.info("[pullGroupMessageList][userId({}) minId({}) size({}) result({})]", userId, minId, size, messages.size()); 211 log.info("[pullGroupMessageList][userId({}) minId({}) size({}) result({})]", userId, minId, size, messages.size());
212 return messages; 212 return messages;
213 } 213 }
214 214
215 /** 215 /**
216 - * 补全消息已读态和回执已读人数 216 + * 补全本人发送消息的回执已读人数(readCount)
217 * <p> 217 * <p>
218 - * 1. 消息已读态(status):根据 im_conversation_read 读位置判断 READ / UNREAD,所有消息都补  
219 - * 2. 回执已读人数(readCount):仅对本人发送、且需要回执(非 NO_RECEIPT)的消息,计算可见成员中的已读人数 218 + * 仅对本人发送、且需要回执(非 NO_RECEIPT)的消息,按 receiver_user_ids 快照 ∩ 当前有效成员 - 发送者 算已读人数
220 */ 219 */
221 - @SuppressWarnings({"StatementWithEmptyBody", "DataFlowIssue"})  
222 - private void appendMessageStatusAndReceipt(Long userId, List<ImGroupMessageDO> messages) { 220 + @SuppressWarnings("DataFlowIssue")
  221 + private void appendMessageReceipt(Long userId, List<ImGroupMessageDO> messages) {
223 if (CollUtil.isEmpty(messages)) { 222 if (CollUtil.isEmpty(messages)) {
224 return; 223 return;
225 } 224 }
226 - // 群已读关闭:不查读位置,status 保持 DB 原值(含撤回),readCount 不补齐 225 + // 群已读关闭:不补 readCount
227 if (BooleanUtil.isFalse(imProperties.getMessage().isGroupReadEnabled())) { 226 if (BooleanUtil.isFalse(imProperties.getMessage().isGroupReadEnabled())) {
228 return; 227 return;
229 } 228 }
230 - Map<Long, Long> readMaxMessageIdsByGroup = new HashMap<>(); // 群 → 当前用户已读位置  
231 Map<Long, Map<Long, Long>> readPositionsByGroup = new HashMap<>(); // 群 → (用户 → 已读位置) 229 Map<Long, Map<Long, Long>> readPositionsByGroup = new HashMap<>(); // 群 → (用户 → 已读位置)
232 Map<Long, List<ImGroupMemberDO>> membersByGroup = new HashMap<>(); // 群 → 全部成员列表 230 Map<Long, List<ImGroupMemberDO>> membersByGroup = new HashMap<>(); // 群 → 全部成员列表
233 for (ImGroupMessageDO message : messages) { 231 for (ImGroupMessageDO message : messages) {
234 - // 消息已读态(status):撤回 > 已读 > 未读  
235 - Long groupId = message.getGroupId();  
236 - long readMaxMessageId = readMaxMessageIdsByGroup.computeIfAbsent(groupId, gid -> {  
237 - Long readMaxMsgId = conversationReadService.getConversationReadMessageId(  
238 - userId, ImConversationTypeEnum.GROUP.getType(), gid);  
239 - return readMaxMsgId != null ? readMaxMsgId : -1L;  
240 - });  
241 - if (ImMessageStatusEnum.RECALL.getStatus().equals(message.getStatus())) {  
242 - // 保持撤回态  
243 - } else if (readMaxMessageId >= message.getId()) {  
244 - message.setStatus(ImMessageStatusEnum.READ.getStatus());  
245 - } else {  
246 - message.setStatus(ImMessageStatusEnum.UNREAD.getStatus());  
247 - }  
248 -  
249 - // 回执已读人数(readCount):仅补本人发送、且需要回执(非 NO_RECEIPT)的消息  
250 - // 读位置(status)所有消息都补;readCount 只服务「需要回执」的消息,不把已读位置和消息回执混为一谈 232 + // 仅补本人发送、且需要回执(非 NO_RECEIPT)的消息
251 if (ObjUtil.notEqual(message.getSenderId(), userId) 233 if (ObjUtil.notEqual(message.getSenderId(), userId)
252 - || ImGroupMessageReceiptStatusEnum.NO_RECEIPT.getStatus().equals(message.getReceiptStatus())) { 234 + || ImMessageReceiptStatusEnum.NO_RECEIPT.getStatus().equals(message.getReceiptStatus())) {
253 continue; 235 continue;
254 } 236 }
  237 + Long groupId = message.getGroupId();
255 Map<Long, Long> positions = readPositionsByGroup.computeIfAbsent(groupId, 238 Map<Long, Long> positions = readPositionsByGroup.computeIfAbsent(groupId,
256 gid -> conversationReadService.getUserReadMessageIdMap(ImConversationTypeEnum.GROUP.getType(), gid)); 239 gid -> conversationReadService.getUserReadMessageIdMap(ImConversationTypeEnum.GROUP.getType(), gid));
257 // 应读分母取当前有效成员(剔除已退群),与异步回执刷新、前端已读弹层口径一致 240 // 应读分母取当前有效成员(剔除已退群),与异步回执刷新、前端已读弹层口径一致
@@ -385,9 +368,9 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { @@ -385,9 +368,9 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService {
385 uid -> allPositions.getOrDefault(uid, -1L) >= message.getId() ? 1 : null, 368 uid -> allPositions.getOrDefault(uid, -1L) >= message.getId() ? 1 : null,
386 Integer::sum, 0); 369 Integer::sum, 0);
387 // 2.1.2 全部已读 → 标记回执完成 370 // 2.1.2 全部已读 → 标记回执完成
388 - Integer newReceiptStatus = ImGroupMessageReceiptStatusEnum.PENDING.getStatus(); 371 + Integer newReceiptStatus = ImMessageReceiptStatusEnum.PENDING.getStatus();
389 if (readCount >= receiverUserIds.size()) { 372 if (readCount >= receiverUserIds.size()) {
390 - newReceiptStatus = ImGroupMessageReceiptStatusEnum.DONE.getStatus(); 373 + newReceiptStatus = ImMessageReceiptStatusEnum.DONE.getStatus();
391 groupMessageMapper.updateById(new ImGroupMessageDO().setId(message.getId()) 374 groupMessageMapper.updateById(new ImGroupMessageDO().setId(message.getId())
392 .setReceiptStatus(newReceiptStatus)); 375 .setReceiptStatus(newReceiptStatus));
393 } 376 }
@@ -534,11 +517,11 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { @@ -534,11 +517,11 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService {
534 */ 517 */
535 private Integer resolveReceiptStatus(Boolean receipt) { 518 private Integer resolveReceiptStatus(Boolean receipt) {
536 if (BooleanUtil.isFalse(imProperties.getMessage().isGroupReadEnabled())) { 519 if (BooleanUtil.isFalse(imProperties.getMessage().isGroupReadEnabled())) {
537 - return ImGroupMessageReceiptStatusEnum.NO_RECEIPT.getStatus(); 520 + return ImMessageReceiptStatusEnum.NO_RECEIPT.getStatus();
538 } 521 }
539 return BooleanUtil.isTrue(receipt) 522 return BooleanUtil.isTrue(receipt)
540 - ? ImGroupMessageReceiptStatusEnum.PENDING.getStatus()  
541 - : ImGroupMessageReceiptStatusEnum.NO_RECEIPT.getStatus(); 523 + ? ImMessageReceiptStatusEnum.PENDING.getStatus()
  524 + : ImMessageReceiptStatusEnum.NO_RECEIPT.getStatus();
542 } 525 }
543 526
544 /** 527 /**
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageService.java
@@ -59,8 +59,8 @@ public interface ImPrivateMessageService { @@ -59,8 +59,8 @@ public interface ImPrivateMessageService {
59 /** 59 /**
60 * 标记私聊消息已读 60 * 标记私聊消息已读
61 * <p> 61 * <p>
62 - * 语义:将「对方发给当前用户、id <= messageId 的未读消息」一次性翻转为已读,  
63 - * 与群聊 readGroupMessages 对称,避免"select-then-update"两步式带来的竞态。 62 + * 语义:把「对方发给当前用户、id <= messageId 的待回执消息」一次性更新为已完成(DONE)并前进读位置,
  63 + * 与群聊 readGroupMessages 对称,避免「select-then-update」两步式带来的竞态。
64 * 64 *
65 * @param userId 当前用户编号 65 * @param userId 当前用户编号
66 * @param receiverId 接收方用户编号(对方) 66 * @param receiverId 接收方用户编号(对方)
@@ -72,7 +72,7 @@ public interface ImPrivateMessageService { @@ -72,7 +72,7 @@ public interface ImPrivateMessageService {
72 * 查询对方已读到我发的最大消息 id 72 * 查询对方已读到我发的最大消息 id
73 * <p> 73 * <p>
74 * 用于多端 / 离线场景下的已读位置补齐:客户端进入会话或断线重连后, 74 * 用于多端 / 离线场景下的已读位置补齐:客户端进入会话或断线重连后,
75 - * 调用此接口拿到对方的 maxReadId,再按 id <= maxReadId 翻转本地自发消息为已读,弥补离线期间错过的 RECEIPT 推送事件。 75 + * 调用此接口拿到对方的 maxReadId,再按 id <= maxReadId 更新本地自发消息的回执状态,弥补离线期间错过的 RECEIPT 推送事件。
76 * 76 *
77 * @param userId 当前用户编号 77 * @param userId 当前用户编号
78 * @param peerId 对方用户编号 78 * @param peerId 对方用户编号
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageServiceImpl.java
@@ -13,6 +13,7 @@ import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivate @@ -13,6 +13,7 @@ import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivate
13 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO; 13 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO;
14 import cn.iocoder.yudao.module.im.dal.mysql.message.ImPrivateMessageMapper; 14 import cn.iocoder.yudao.module.im.dal.mysql.message.ImPrivateMessageMapper;
15 import cn.iocoder.yudao.module.im.enums.ImConversationTypeEnum; 15 import cn.iocoder.yudao.module.im.enums.ImConversationTypeEnum;
  16 +import cn.iocoder.yudao.module.im.enums.message.ImMessageReceiptStatusEnum;
16 import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum; 17 import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum;
17 import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; 18 import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
18 import cn.iocoder.yudao.module.im.framework.config.ImProperties; 19 import cn.iocoder.yudao.module.im.framework.config.ImProperties;
@@ -86,8 +87,11 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService { @@ -86,8 +87,11 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService {
86 // 2.1 引用 quote 消息规范化 87 // 2.1 引用 quote 消息规范化
87 reqVO.setContent(normalizeQuoteContent(reqVO, senderId)); 88 reqVO.setContent(normalizeQuoteContent(reqVO, senderId));
88 // 2.2 构建并保存消息;唯一键冲突时回查已存在消息返回 89 // 2.2 构建并保存消息;唯一键冲突时回查已存在消息返回
  90 + // 用户私聊消息默认需要回执(receipt 不传按 true);系统通知走 DTO 通道,默认不回执
  91 + Boolean receipt = reqVO.getReceipt() != null ? reqVO.getReceipt() : Boolean.TRUE;
89 ImPrivateMessageDO message = BeanUtils.toBean(reqVO, ImPrivateMessageDO.class, m -> m 92 ImPrivateMessageDO message = BeanUtils.toBean(reqVO, ImPrivateMessageDO.class, m -> m
90 - .setSenderId(senderId).setStatus(ImMessageStatusEnum.UNREAD.getStatus()).setSendTime(LocalDateTime.now())); 93 + .setSenderId(senderId).setStatus(ImMessageStatusEnum.NORMAL.getStatus())
  94 + .setReceiptStatus(resolveReceiptStatus(receipt)).setSendTime(LocalDateTime.now()));
91 try { 95 try {
92 privateMessageMapper.insert(message); 96 privateMessageMapper.insert(message);
93 } catch (DuplicateKeyException e) { 97 } catch (DuplicateKeyException e) {
@@ -114,7 +118,8 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService { @@ -114,7 +118,8 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService {
114 ImPrivateMessageDO message = new ImPrivateMessageDO().setClientMessageId(IdUtil.fastSimpleUUID()) 118 ImPrivateMessageDO message = new ImPrivateMessageDO().setClientMessageId(IdUtil.fastSimpleUUID())
115 .setSenderId(senderId).setReceiverId(dto.getReceiverId()) 119 .setSenderId(senderId).setReceiverId(dto.getReceiverId())
116 .setType(dto.getType()).setContent(contentString) 120 .setType(dto.getType()).setContent(contentString)
117 - .setStatus(ImMessageStatusEnum.UNREAD.getStatus()).setSendTime(LocalDateTime.now()); 121 + .setStatus(ImMessageStatusEnum.NORMAL.getStatus())
  122 + .setReceiptStatus(resolveReceiptStatus(dto.getReceipt())).setSendTime(LocalDateTime.now());
118 // 1.3 决定是否持久化:dto.persistent 优先;为 null 时按 type 默认 123 // 1.3 决定是否持久化:dto.persistent 优先;为 null 时按 type 默认
119 boolean persistent = dto.getPersistent() != null 124 boolean persistent = dto.getPersistent() != null
120 ? dto.getPersistent() 125 ? dto.getPersistent()
@@ -132,6 +137,18 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService { @@ -132,6 +137,18 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService {
132 return message; 137 return message;
133 } 138 }
134 139
  140 + /**
  141 + * 计算私聊消息回执 status:私聊已读关闭时强制 NO_RECEIPT,忽略发送方传入的 receipt(receipt 为 null 等价 false)
  142 + */
  143 + private Integer resolveReceiptStatus(Boolean receipt) {
  144 + if (BooleanUtil.isFalse(imProperties.getMessage().isPrivateReadEnabled())) {
  145 + return ImMessageReceiptStatusEnum.NO_RECEIPT.getStatus();
  146 + }
  147 + return BooleanUtil.isTrue(receipt)
  148 + ? ImMessageReceiptStatusEnum.PENDING.getStatus()
  149 + : ImMessageReceiptStatusEnum.NO_RECEIPT.getStatus();
  150 + }
  151 +
135 @Override 152 @Override
136 @Transactional(rollbackFor = Exception.class) 153 @Transactional(rollbackFor = Exception.class)
137 public ImPrivateMessageDO recallPrivateMessage(Long userId, Long messageId) { 154 public ImPrivateMessageDO recallPrivateMessage(Long userId, Long messageId) {
@@ -223,11 +240,11 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService { @@ -223,11 +240,11 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService {
223 } 240 }
224 Assert.notNull(messageId, "已读消息编号不能为空"); 241 Assert.notNull(messageId, "已读消息编号不能为空");
225 242
226 - // 2. status 双写:把 (receiverId → userId) 上 id <= messageId 的未读消息置为已读  
227 - // 仅 UNREAD 行被命中,避免覆盖已撤回/已读的状态;select-then-update 合成单条 SQL 后也消除了竞态窗口  
228 - privateMessageMapper.updateBySenderIdAndReceiverIdAndIdLeAndStatus(  
229 - receiverId, userId, messageId, ImMessageStatusEnum.UNREAD.getStatus(),  
230 - new ImPrivateMessageDO().setStatus(ImMessageStatusEnum.READ.getStatus())); 243 + // 2. 回执置 DONE:把 (receiverId → userId) 上 id <= messageId、待回执(PENDING) 的消息标记为已完成
  244 + // status 不再表达已读(保持 NORMAL);只翻 PENDING 行,避免覆盖 NO_RECEIPT / 已 DONE
  245 + privateMessageMapper.updateBySenderIdAndReceiverIdAndIdLeAndReceiptStatus(
  246 + receiverId, userId, messageId, ImMessageReceiptStatusEnum.PENDING.getStatus(),
  247 + new ImPrivateMessageDO().setReceiptStatus(ImMessageReceiptStatusEnum.DONE.getStatus()));
231 248
232 // 3. 同步写 im_conversation_read(读位置唯一权威,单调递增);读位置前进才下发事件 249 // 3. 同步写 im_conversation_read(读位置唯一权威,单调递增);读位置前进才下发事件
233 boolean advanced = conversationReadService.updateConversationReadPosition( 250 boolean advanced = conversationReadService.updateConversationReadPosition(
@@ -248,8 +265,9 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService { @@ -248,8 +265,9 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService {
248 if (BooleanUtil.isFalse(imProperties.getMessage().isPrivateReadEnabled())) { 265 if (BooleanUtil.isFalse(imProperties.getMessage().isPrivateReadEnabled())) {
249 throw exception(MESSAGE_PRIVATE_READ_DISABLED); 266 throw exception(MESSAGE_PRIVATE_READ_DISABLED);
250 } 267 }
251 - return privateMessageMapper.selectMaxIdBySenderIdAndReceiverIdAndStatus(  
252 - userId, peerId, ImMessageStatusEnum.READ.getStatus()); 268 + // 对端 peer 在「与 userId 的会话」里的读位置 = peer 把 userId 发的消息读到哪
  269 + return conversationReadService.getConversationReadMessageId(
  270 + peerId, ImConversationTypeEnum.PRIVATE.getType(), userId);
253 } 271 }
254 272
255 @Override 273 @Override
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/dto/ImPrivateMessageSendDTO.java
@@ -35,5 +35,9 @@ public class ImPrivateMessageSendDTO { @@ -35,5 +35,9 @@ public class ImPrivateMessageSendDTO {
35 * true:覆盖为双向 + 入库 35 * true:覆盖为双向 + 入库
36 */ 36 */
37 private Boolean persistent; 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/websocket/dto/ImPrivateMessageDTO.java
@@ -60,6 +60,10 @@ public class ImPrivateMessageDTO { @@ -60,6 +60,10 @@ public class ImPrivateMessageDTO {
60 */ 60 */
61 private Integer status; 61 private Integer status;
62 /** 62 /**
  63 + * 回执状态
  64 + */
  65 + private Integer receiptStatus;
  66 + /**
63 * 发送时间 67 * 发送时间
64 */ 68 */
65 private LocalDateTime sendTime; 69 private LocalDateTime sendTime;
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/dal/mysql/message/ImGroupMessageMapperTest.java
@@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.im.dal.mysql.message; @@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.im.dal.mysql.message;
2 2
3 import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; 3 import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
4 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO; 4 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO;
5 -import cn.iocoder.yudao.module.im.enums.message.ImGroupMessageReceiptStatusEnum; 5 +import cn.iocoder.yudao.module.im.enums.message.ImMessageReceiptStatusEnum;
6 import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum; 6 import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum;
7 import jakarta.annotation.Resource; 7 import jakarta.annotation.Resource;
8 import org.junit.jupiter.api.Test; 8 import org.junit.jupiter.api.Test;
@@ -29,11 +29,11 @@ public class ImGroupMessageMapperTest extends BaseDbUnitTest { @@ -29,11 +29,11 @@ public class ImGroupMessageMapperTest extends BaseDbUnitTest {
29 @Test 29 @Test
30 public void testSelectListByMinId_filterByReceiverSnapshot() { 30 public void testSelectListByMinId_filterByReceiverSnapshot() {
31 // 准备:群 10 三条消息,快照分别命中 / 不命中用户 1 31 // 准备:群 10 三条消息,快照分别命中 / 不命中用户 1
32 - ImGroupMessageDO visible = buildMessage(10L, 2L, ImMessageStatusEnum.UNREAD, List.of(1L, 2L)); 32 + ImGroupMessageDO visible = buildMessage(10L, 2L, ImMessageStatusEnum.NORMAL, List.of(1L, 2L));
33 mapper.insert(visible); 33 mapper.insert(visible);
34 - ImGroupMessageDO invisible = buildMessage(10L, 2L, ImMessageStatusEnum.UNREAD, List.of(2L, 3L)); 34 + ImGroupMessageDO invisible = buildMessage(10L, 2L, ImMessageStatusEnum.NORMAL, List.of(2L, 3L));
35 mapper.insert(invisible); 35 mapper.insert(invisible);
36 - ImGroupMessageDO selfSend = buildMessage(10L, 1L, ImMessageStatusEnum.UNREAD, List.of(1L, 2L)); 36 + ImGroupMessageDO selfSend = buildMessage(10L, 1L, ImMessageStatusEnum.NORMAL, List.of(1L, 2L));
37 mapper.insert(selfSend); 37 mapper.insert(selfSend);
38 38
39 // 调用:用户 1 拉取群 10 39 // 调用:用户 1 拉取群 10
@@ -48,9 +48,9 @@ public class ImGroupMessageMapperTest extends BaseDbUnitTest { @@ -48,9 +48,9 @@ public class ImGroupMessageMapperTest extends BaseDbUnitTest {
48 @Test 48 @Test
49 public void testSelectListByMinId_excludeNullAndEmptySnapshot() { 49 public void testSelectListByMinId_excludeNullAndEmptySnapshot() {
50 // 准备:快照为 null / 空字符串的老数据,不应被任何用户拉到 50 // 准备:快照为 null / 空字符串的老数据,不应被任何用户拉到
51 - ImGroupMessageDO nullSnapshot = buildMessage(10L, 2L, ImMessageStatusEnum.UNREAD, null); 51 + ImGroupMessageDO nullSnapshot = buildMessage(10L, 2L, ImMessageStatusEnum.NORMAL, null);
52 mapper.insert(nullSnapshot); 52 mapper.insert(nullSnapshot);
53 - ImGroupMessageDO emptySnapshot = buildMessage(10L, 2L, ImMessageStatusEnum.UNREAD, List.of()); 53 + ImGroupMessageDO emptySnapshot = buildMessage(10L, 2L, ImMessageStatusEnum.NORMAL, List.of());
54 mapper.insert(emptySnapshot); 54 mapper.insert(emptySnapshot);
55 55
56 List<ImGroupMessageDO> result = mapper.selectListByMinId(1L, List.of(10L), 0L, FAR_PAST, 100); 56 List<ImGroupMessageDO> result = mapper.selectListByMinId(1L, List.of(10L), 0L, FAR_PAST, 100);
@@ -61,9 +61,9 @@ public class ImGroupMessageMapperTest extends BaseDbUnitTest { @@ -61,9 +61,9 @@ public class ImGroupMessageMapperTest extends BaseDbUnitTest {
61 @Test 61 @Test
62 public void testSelectListByMinId_onlyReturnsGroupsInIdList() { 62 public void testSelectListByMinId_onlyReturnsGroupsInIdList() {
63 // 准备:群 10 + 群 20 各一条,用户 1 都在快照内 63 // 准备:群 10 + 群 20 各一条,用户 1 都在快照内
64 - ImGroupMessageDO msg10 = buildMessage(10L, 2L, ImMessageStatusEnum.UNREAD, List.of(1L, 2L)); 64 + ImGroupMessageDO msg10 = buildMessage(10L, 2L, ImMessageStatusEnum.NORMAL, List.of(1L, 2L));
65 mapper.insert(msg10); 65 mapper.insert(msg10);
66 - ImGroupMessageDO msg20 = buildMessage(20L, 2L, ImMessageStatusEnum.UNREAD, List.of(1L, 2L)); 66 + ImGroupMessageDO msg20 = buildMessage(20L, 2L, ImMessageStatusEnum.NORMAL, List.of(1L, 2L));
67 mapper.insert(msg20); 67 mapper.insert(msg20);
68 68
69 // 调用:候选群只含群 10 69 // 调用:候选群只含群 10
@@ -76,7 +76,7 @@ public class ImGroupMessageMapperTest extends BaseDbUnitTest { @@ -76,7 +76,7 @@ public class ImGroupMessageMapperTest extends BaseDbUnitTest {
76 @Test 76 @Test
77 public void testSelectListByMinId_includesRecall() { 77 public void testSelectListByMinId_includesRecall() {
78 // 准备:撤回消息也要返回,由客户端按 status 渲染「此消息已撤回」占位 78 // 准备:撤回消息也要返回,由客户端按 status 渲染「此消息已撤回」占位
79 - ImGroupMessageDO normal = buildMessage(10L, 2L, ImMessageStatusEnum.UNREAD, List.of(1L)); 79 + ImGroupMessageDO normal = buildMessage(10L, 2L, ImMessageStatusEnum.NORMAL, List.of(1L));
80 mapper.insert(normal); 80 mapper.insert(normal);
81 ImGroupMessageDO recalled = buildMessage(10L, 2L, ImMessageStatusEnum.RECALL, List.of(1L)); 81 ImGroupMessageDO recalled = buildMessage(10L, 2L, ImMessageStatusEnum.RECALL, List.of(1L));
82 mapper.insert(recalled); 82 mapper.insert(recalled);
@@ -89,10 +89,10 @@ public class ImGroupMessageMapperTest extends BaseDbUnitTest { @@ -89,10 +89,10 @@ public class ImGroupMessageMapperTest extends BaseDbUnitTest {
89 @Test 89 @Test
90 public void testSelectListByMinId_sendTimeWindow() { 90 public void testSelectListByMinId_sendTimeWindow() {
91 // 准备:一条在窗口内,一条在窗口外 91 // 准备:一条在窗口内,一条在窗口外
92 - ImGroupMessageDO inWindow = buildMessage(10L, 2L, ImMessageStatusEnum.UNREAD, List.of(1L)); 92 + ImGroupMessageDO inWindow = buildMessage(10L, 2L, ImMessageStatusEnum.NORMAL, List.of(1L));
93 inWindow.setSendTime(LocalDateTime.now().minusDays(1)); 93 inWindow.setSendTime(LocalDateTime.now().minusDays(1));
94 mapper.insert(inWindow); 94 mapper.insert(inWindow);
95 - ImGroupMessageDO outWindow = buildMessage(10L, 2L, ImMessageStatusEnum.UNREAD, List.of(1L)); 95 + ImGroupMessageDO outWindow = buildMessage(10L, 2L, ImMessageStatusEnum.NORMAL, List.of(1L));
96 outWindow.setSendTime(LocalDateTime.now().minusDays(40)); 96 outWindow.setSendTime(LocalDateTime.now().minusDays(40));
97 mapper.insert(outWindow); 97 mapper.insert(outWindow);
98 98
@@ -106,11 +106,11 @@ public class ImGroupMessageMapperTest extends BaseDbUnitTest { @@ -106,11 +106,11 @@ public class ImGroupMessageMapperTest extends BaseDbUnitTest {
106 @Test 106 @Test
107 public void testSelectListByMinId_sortAscLimit() { 107 public void testSelectListByMinId_sortAscLimit() {
108 // 准备:插入 3 条可见消息 108 // 准备:插入 3 条可见消息
109 - ImGroupMessageDO m1 = buildMessage(10L, 2L, ImMessageStatusEnum.UNREAD, List.of(1L)); 109 + ImGroupMessageDO m1 = buildMessage(10L, 2L, ImMessageStatusEnum.NORMAL, List.of(1L));
110 mapper.insert(m1); 110 mapper.insert(m1);
111 - ImGroupMessageDO m2 = buildMessage(10L, 2L, ImMessageStatusEnum.UNREAD, List.of(1L)); 111 + ImGroupMessageDO m2 = buildMessage(10L, 2L, ImMessageStatusEnum.NORMAL, List.of(1L));
112 mapper.insert(m2); 112 mapper.insert(m2);
113 - ImGroupMessageDO m3 = buildMessage(10L, 2L, ImMessageStatusEnum.UNREAD, List.of(1L)); 113 + ImGroupMessageDO m3 = buildMessage(10L, 2L, ImMessageStatusEnum.NORMAL, List.of(1L));
114 mapper.insert(m3); 114 mapper.insert(m3);
115 115
116 // 调用:size=2,返回 id 最小的 2 条 116 // 调用:size=2,返回 id 最小的 2 条
@@ -125,9 +125,9 @@ public class ImGroupMessageMapperTest extends BaseDbUnitTest { @@ -125,9 +125,9 @@ public class ImGroupMessageMapperTest extends BaseDbUnitTest {
125 @Test 125 @Test
126 public void testSelectListByMinId_minIdExclusive() { 126 public void testSelectListByMinId_minIdExclusive() {
127 // 准备:游标之前的消息不返回 127 // 准备:游标之前的消息不返回
128 - ImGroupMessageDO m1 = buildMessage(10L, 2L, ImMessageStatusEnum.UNREAD, List.of(1L)); 128 + ImGroupMessageDO m1 = buildMessage(10L, 2L, ImMessageStatusEnum.NORMAL, List.of(1L));
129 mapper.insert(m1); 129 mapper.insert(m1);
130 - ImGroupMessageDO m2 = buildMessage(10L, 2L, ImMessageStatusEnum.UNREAD, List.of(1L)); 130 + ImGroupMessageDO m2 = buildMessage(10L, 2L, ImMessageStatusEnum.NORMAL, List.of(1L));
131 mapper.insert(m2); 131 mapper.insert(m2);
132 132
133 List<ImGroupMessageDO> result = mapper.selectListByMinId(1L, List.of(10L), m1.getId(), FAR_PAST, 100); 133 List<ImGroupMessageDO> result = mapper.selectListByMinId(1L, List.of(10L), m1.getId(), FAR_PAST, 100);
@@ -141,14 +141,14 @@ public class ImGroupMessageMapperTest extends BaseDbUnitTest { @@ -141,14 +141,14 @@ public class ImGroupMessageMapperTest extends BaseDbUnitTest {
141 @Test 141 @Test
142 public void testSelectHistoryListByUser_filterByReceiverSnapshot() { 142 public void testSelectHistoryListByUser_filterByReceiverSnapshot() {
143 // 准备:群 10 三条,用户 1 在前两条快照内、第三条定向给别人 143 // 准备:群 10 三条,用户 1 在前两条快照内、第三条定向给别人
144 - ImGroupMessageDO m1 = buildMessage(10L, 2L, ImMessageStatusEnum.UNREAD, List.of(1L, 2L)); 144 + ImGroupMessageDO m1 = buildMessage(10L, 2L, ImMessageStatusEnum.NORMAL, List.of(1L, 2L));
145 mapper.insert(m1); 145 mapper.insert(m1);
146 - ImGroupMessageDO m2 = buildMessage(10L, 2L, ImMessageStatusEnum.UNREAD, List.of(1L, 2L)); 146 + ImGroupMessageDO m2 = buildMessage(10L, 2L, ImMessageStatusEnum.NORMAL, List.of(1L, 2L));
147 mapper.insert(m2); 147 mapper.insert(m2);
148 - ImGroupMessageDO directedOther = buildMessage(10L, 2L, ImMessageStatusEnum.UNREAD, List.of(2L, 3L)); 148 + ImGroupMessageDO directedOther = buildMessage(10L, 2L, ImMessageStatusEnum.NORMAL, List.of(2L, 3L));
149 mapper.insert(directedOther); 149 mapper.insert(directedOther);
150 // 别的群 150 // 别的群
151 - ImGroupMessageDO other = buildMessage(20L, 2L, ImMessageStatusEnum.UNREAD, List.of(1L)); 151 + ImGroupMessageDO other = buildMessage(20L, 2L, ImMessageStatusEnum.NORMAL, List.of(1L));
152 mapper.insert(other); 152 mapper.insert(other);
153 153
154 List<ImGroupMessageDO> result = mapper.selectHistoryListByUser(1L, 10L, null, 100); 154 List<ImGroupMessageDO> result = mapper.selectHistoryListByUser(1L, 10L, null, 100);
@@ -161,11 +161,11 @@ public class ImGroupMessageMapperTest extends BaseDbUnitTest { @@ -161,11 +161,11 @@ public class ImGroupMessageMapperTest extends BaseDbUnitTest {
161 161
162 @Test 162 @Test
163 public void testSelectHistoryListByUser_maxIdCursor() { 163 public void testSelectHistoryListByUser_maxIdCursor() {
164 - ImGroupMessageDO m1 = buildMessage(10L, 2L, ImMessageStatusEnum.UNREAD, List.of(1L)); 164 + ImGroupMessageDO m1 = buildMessage(10L, 2L, ImMessageStatusEnum.NORMAL, List.of(1L));
165 mapper.insert(m1); 165 mapper.insert(m1);
166 - ImGroupMessageDO m2 = buildMessage(10L, 2L, ImMessageStatusEnum.UNREAD, List.of(1L)); 166 + ImGroupMessageDO m2 = buildMessage(10L, 2L, ImMessageStatusEnum.NORMAL, List.of(1L));
167 mapper.insert(m2); 167 mapper.insert(m2);
168 - ImGroupMessageDO m3 = buildMessage(10L, 2L, ImMessageStatusEnum.UNREAD, List.of(1L)); 168 + ImGroupMessageDO m3 = buildMessage(10L, 2L, ImMessageStatusEnum.NORMAL, List.of(1L));
169 mapper.insert(m3); 169 mapper.insert(m3);
170 170
171 List<ImGroupMessageDO> result = mapper.selectHistoryListByUser(1L, 10L, m3.getId(), 100); 171 List<ImGroupMessageDO> result = mapper.selectHistoryListByUser(1L, 10L, m3.getId(), 100);
@@ -188,7 +188,7 @@ public class ImGroupMessageMapperTest extends BaseDbUnitTest { @@ -188,7 +188,7 @@ public class ImGroupMessageMapperTest extends BaseDbUnitTest {
188 .status(status.getStatus()) 188 .status(status.getStatus())
189 .sendTime(LocalDateTime.now()) 189 .sendTime(LocalDateTime.now())
190 .receiverUserIds(receiverUserIds) 190 .receiverUserIds(receiverUserIds)
191 - .receiptStatus(ImGroupMessageReceiptStatusEnum.NO_RECEIPT.getStatus()) 191 + .receiptStatus(ImMessageReceiptStatusEnum.NO_RECEIPT.getStatus())
192 .build(); 192 .build();
193 } 193 }
194 194
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/dal/mysql/message/ImPrivateMessageMapperTest.java
@@ -31,13 +31,13 @@ public class ImPrivateMessageMapperTest extends BaseDbUnitTest { @@ -31,13 +31,13 @@ public class ImPrivateMessageMapperTest extends BaseDbUnitTest {
31 @Test 31 @Test
32 public void testSelectListByMinId() { 32 public void testSelectListByMinId() {
33 // 准备:用户 1 发给用户 2 33 // 准备:用户 1 发给用户 2
34 - ImPrivateMessageDO msg1 = buildMessage(1L, 2L, ImMessageStatusEnum.UNREAD); 34 + ImPrivateMessageDO msg1 = buildMessage(1L, 2L, ImMessageStatusEnum.NORMAL);
35 mapper.insert(msg1); 35 mapper.insert(msg1);
36 // 用户 2 发给用户 1 36 // 用户 2 发给用户 1
37 - ImPrivateMessageDO msg2 = buildMessage(2L, 1L, ImMessageStatusEnum.UNREAD); 37 + ImPrivateMessageDO msg2 = buildMessage(2L, 1L, ImMessageStatusEnum.NORMAL);
38 mapper.insert(msg2); 38 mapper.insert(msg2);
39 // 用户 3 发给用户 4(不相关) 39 // 用户 3 发给用户 4(不相关)
40 - ImPrivateMessageDO msg3 = buildMessage(3L, 4L, ImMessageStatusEnum.UNREAD); 40 + ImPrivateMessageDO msg3 = buildMessage(3L, 4L, ImMessageStatusEnum.NORMAL);
41 mapper.insert(msg3); 41 mapper.insert(msg3);
42 42
43 // 调用:用户 1 从 id=0 拉取 43 // 调用:用户 1 从 id=0 拉取
@@ -52,9 +52,9 @@ public class ImPrivateMessageMapperTest extends BaseDbUnitTest { @@ -52,9 +52,9 @@ public class ImPrivateMessageMapperTest extends BaseDbUnitTest {
52 @Test 52 @Test
53 public void testSelectListByMinId_withMinId() { 53 public void testSelectListByMinId_withMinId() {
54 // 准备 54 // 准备
55 - ImPrivateMessageDO msg1 = buildMessage(1L, 2L, ImMessageStatusEnum.UNREAD); 55 + ImPrivateMessageDO msg1 = buildMessage(1L, 2L, ImMessageStatusEnum.NORMAL);
56 mapper.insert(msg1); 56 mapper.insert(msg1);
57 - ImPrivateMessageDO msg2 = buildMessage(2L, 1L, ImMessageStatusEnum.UNREAD); 57 + ImPrivateMessageDO msg2 = buildMessage(2L, 1L, ImMessageStatusEnum.NORMAL);
58 mapper.insert(msg2); 58 mapper.insert(msg2);
59 59
60 // 调用:从 msg1.id 之后拉取 60 // 调用:从 msg1.id 之后拉取
@@ -68,9 +68,9 @@ public class ImPrivateMessageMapperTest extends BaseDbUnitTest { @@ -68,9 +68,9 @@ public class ImPrivateMessageMapperTest extends BaseDbUnitTest {
68 @Test 68 @Test
69 public void testSelectListByMinId_limitSize() { 69 public void testSelectListByMinId_limitSize() {
70 // 准备:插入 3 条 70 // 准备:插入 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)); 71 + mapper.insert(buildMessage(1L, 2L, ImMessageStatusEnum.NORMAL));
  72 + mapper.insert(buildMessage(2L, 1L, ImMessageStatusEnum.NORMAL));
  73 + mapper.insert(buildMessage(1L, 2L, ImMessageStatusEnum.NORMAL));
74 74
75 // 调用:limit 2 75 // 调用:limit 2
76 List<ImPrivateMessageDO> result = mapper.selectListByMinId(1L, 0L, FAR_PAST, 2); 76 List<ImPrivateMessageDO> result = mapper.selectListByMinId(1L, 0L, FAR_PAST, 2);
@@ -82,10 +82,10 @@ public class ImPrivateMessageMapperTest extends BaseDbUnitTest { @@ -82,10 +82,10 @@ public class ImPrivateMessageMapperTest extends BaseDbUnitTest {
82 @Test 82 @Test
83 public void testSelectListByMinId_sendTimeFilter() { 83 public void testSelectListByMinId_sendTimeFilter() {
84 // 准备:一条落在窗口内,一条落在窗口外 84 // 准备:一条落在窗口内,一条落在窗口外
85 - ImPrivateMessageDO newMsg = buildMessage(1L, 2L, ImMessageStatusEnum.UNREAD); 85 + ImPrivateMessageDO newMsg = buildMessage(1L, 2L, ImMessageStatusEnum.NORMAL);
86 newMsg.setSendTime(LocalDateTime.now().minusDays(1)); 86 newMsg.setSendTime(LocalDateTime.now().minusDays(1));
87 mapper.insert(newMsg); 87 mapper.insert(newMsg);
88 - ImPrivateMessageDO oldMsg = buildMessage(1L, 2L, ImMessageStatusEnum.UNREAD); 88 + ImPrivateMessageDO oldMsg = buildMessage(1L, 2L, ImMessageStatusEnum.NORMAL);
89 oldMsg.setSendTime(LocalDateTime.now().minusDays(40)); 89 oldMsg.setSendTime(LocalDateTime.now().minusDays(40));
90 mapper.insert(oldMsg); 90 mapper.insert(oldMsg);
91 91
@@ -103,12 +103,12 @@ public class ImPrivateMessageMapperTest extends BaseDbUnitTest { @@ -103,12 +103,12 @@ public class ImPrivateMessageMapperTest extends BaseDbUnitTest {
103 @Test 103 @Test
104 public void testSelectHistoryList_basic() { 104 public void testSelectHistoryList_basic() {
105 // 准备:用户 1 <-> 用户 2 的消息 105 // 准备:用户 1 <-> 用户 2 的消息
106 - ImPrivateMessageDO msg1 = buildMessage(1L, 2L, ImMessageStatusEnum.UNREAD); 106 + ImPrivateMessageDO msg1 = buildMessage(1L, 2L, ImMessageStatusEnum.NORMAL);
107 mapper.insert(msg1); 107 mapper.insert(msg1);
108 - ImPrivateMessageDO msg2 = buildMessage(2L, 1L, ImMessageStatusEnum.UNREAD); 108 + ImPrivateMessageDO msg2 = buildMessage(2L, 1L, ImMessageStatusEnum.NORMAL);
109 mapper.insert(msg2); 109 mapper.insert(msg2);
110 // 用户 1 <-> 用户 3 的消息(不相关) 110 // 用户 1 <-> 用户 3 的消息(不相关)
111 - ImPrivateMessageDO msg3 = buildMessage(1L, 3L, ImMessageStatusEnum.UNREAD); 111 + ImPrivateMessageDO msg3 = buildMessage(1L, 3L, ImMessageStatusEnum.NORMAL);
112 mapper.insert(msg3); 112 mapper.insert(msg3);
113 113
114 // 调用 114 // 调用
@@ -122,7 +122,7 @@ public class ImPrivateMessageMapperTest extends BaseDbUnitTest { @@ -122,7 +122,7 @@ public class ImPrivateMessageMapperTest extends BaseDbUnitTest {
122 @Test 122 @Test
123 public void testSelectHistoryList_includeRecall() { 123 public void testSelectHistoryList_includeRecall() {
124 // 准备 124 // 准备
125 - ImPrivateMessageDO msg1 = buildMessage(1L, 2L, ImMessageStatusEnum.UNREAD); 125 + ImPrivateMessageDO msg1 = buildMessage(1L, 2L, ImMessageStatusEnum.NORMAL);
126 mapper.insert(msg1); 126 mapper.insert(msg1);
127 ImPrivateMessageDO msg2 = buildMessage(1L, 2L, ImMessageStatusEnum.RECALL); 127 ImPrivateMessageDO msg2 = buildMessage(1L, 2L, ImMessageStatusEnum.RECALL);
128 mapper.insert(msg2); 128 mapper.insert(msg2);
@@ -139,11 +139,11 @@ public class ImPrivateMessageMapperTest extends BaseDbUnitTest { @@ -139,11 +139,11 @@ public class ImPrivateMessageMapperTest extends BaseDbUnitTest {
139 @Test 139 @Test
140 public void testSelectHistoryList_withMaxId() { 140 public void testSelectHistoryList_withMaxId() {
141 // 准备 141 // 准备
142 - ImPrivateMessageDO msg1 = buildMessage(1L, 2L, ImMessageStatusEnum.UNREAD); 142 + ImPrivateMessageDO msg1 = buildMessage(1L, 2L, ImMessageStatusEnum.NORMAL);
143 mapper.insert(msg1); 143 mapper.insert(msg1);
144 - ImPrivateMessageDO msg2 = buildMessage(1L, 2L, ImMessageStatusEnum.UNREAD); 144 + ImPrivateMessageDO msg2 = buildMessage(1L, 2L, ImMessageStatusEnum.NORMAL);
145 mapper.insert(msg2); 145 mapper.insert(msg2);
146 - ImPrivateMessageDO msg3 = buildMessage(1L, 2L, ImMessageStatusEnum.UNREAD); 146 + ImPrivateMessageDO msg3 = buildMessage(1L, 2L, ImMessageStatusEnum.NORMAL);
147 mapper.insert(msg3); 147 mapper.insert(msg3);
148 148
149 // 调用:从 msg3 往前拉 149 // 调用:从 msg3 往前拉
@@ -157,9 +157,9 @@ public class ImPrivateMessageMapperTest extends BaseDbUnitTest { @@ -157,9 +157,9 @@ public class ImPrivateMessageMapperTest extends BaseDbUnitTest {
157 @Test 157 @Test
158 public void testSelectHistoryList_limit() { 158 public void testSelectHistoryList_limit() {
159 // 准备 159 // 准备
160 - mapper.insert(buildMessage(1L, 2L, ImMessageStatusEnum.UNREAD));  
161 - mapper.insert(buildMessage(2L, 1L, ImMessageStatusEnum.UNREAD));  
162 - mapper.insert(buildMessage(1L, 2L, ImMessageStatusEnum.UNREAD)); 160 + mapper.insert(buildMessage(1L, 2L, ImMessageStatusEnum.NORMAL));
  161 + mapper.insert(buildMessage(2L, 1L, ImMessageStatusEnum.NORMAL));
  162 + mapper.insert(buildMessage(1L, 2L, ImMessageStatusEnum.NORMAL));
163 163
164 // 调用:limit 2 164 // 调用:limit 2
165 List<ImPrivateMessageDO> result = mapper.selectHistoryList(1L, 2L, null, 2); 165 List<ImPrivateMessageDO> result = mapper.selectHistoryList(1L, 2L, null, 2);
@@ -171,7 +171,7 @@ public class ImPrivateMessageMapperTest extends BaseDbUnitTest { @@ -171,7 +171,7 @@ public class ImPrivateMessageMapperTest extends BaseDbUnitTest {
171 @Test 171 @Test
172 public void testSelectHistoryList_bidirectional() { 172 public void testSelectHistoryList_bidirectional() {
173 // 准备:验证双向查询 173 // 准备:验证双向查询
174 - ImPrivateMessageDO msg1 = buildMessage(2L, 1L, ImMessageStatusEnum.UNREAD); // 对方发的 174 + ImPrivateMessageDO msg1 = buildMessage(2L, 1L, ImMessageStatusEnum.NORMAL); // 对方发的
175 mapper.insert(msg1); 175 mapper.insert(msg1);
176 176
177 // 调用:以用户 1 的视角查 177 // 调用:以用户 1 的视角查
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/dal/mysql/statistics/ImStatisticsManagerMapperTest.java
@@ -245,7 +245,7 @@ public class ImStatisticsManagerMapperTest extends BaseDbUnitTest { @@ -245,7 +245,7 @@ public class ImStatisticsManagerMapperTest extends BaseDbUnitTest {
245 .senderId(senderId).receiverId(receiverId) 245 .senderId(senderId).receiverId(receiverId)
246 .type(ImMessageTypeEnum.TEXT.getType()) 246 .type(ImMessageTypeEnum.TEXT.getType())
247 .content("{}") 247 .content("{}")
248 - .status(ImMessageStatusEnum.UNREAD.getStatus()) 248 + .status(ImMessageStatusEnum.NORMAL.getStatus())
249 .sendTime(sendTime).build(); 249 .sendTime(sendTime).build();
250 } 250 }
251 251
@@ -255,7 +255,7 @@ public class ImStatisticsManagerMapperTest extends BaseDbUnitTest { @@ -255,7 +255,7 @@ public class ImStatisticsManagerMapperTest extends BaseDbUnitTest {
255 .setSenderId(senderId).setGroupId(groupId) 255 .setSenderId(senderId).setGroupId(groupId)
256 .setType(ImMessageTypeEnum.TEXT.getType()) 256 .setType(ImMessageTypeEnum.TEXT.getType())
257 .setContent("{}") 257 .setContent("{}")
258 - .setStatus(ImMessageStatusEnum.UNREAD.getStatus()) 258 + .setStatus(ImMessageStatusEnum.NORMAL.getStatus())
259 .setSendTime(sendTime); 259 .setSendTime(sendTime);
260 } 260 }
261 261
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImplTest.java
@@ -12,7 +12,7 @@ import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO; @@ -12,7 +12,7 @@ import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO;
12 import cn.iocoder.yudao.module.im.dal.mysql.message.ImGroupMessageMapper; 12 import cn.iocoder.yudao.module.im.dal.mysql.message.ImGroupMessageMapper;
13 import cn.iocoder.yudao.module.im.enums.ImConversationTypeEnum; 13 import cn.iocoder.yudao.module.im.enums.ImConversationTypeEnum;
14 import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum; 14 import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum;
15 -import cn.iocoder.yudao.module.im.enums.message.ImGroupMessageReceiptStatusEnum; 15 +import cn.iocoder.yudao.module.im.enums.message.ImMessageReceiptStatusEnum;
16 import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum; 16 import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum;
17 import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; 17 import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
18 import cn.iocoder.yudao.module.im.framework.config.ImProperties; 18 import cn.iocoder.yudao.module.im.framework.config.ImProperties;
@@ -111,8 +111,8 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { @@ -111,8 +111,8 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
111 assertNotNull(result); 111 assertNotNull(result);
112 assertEquals(1L, result.getSenderId()); 112 assertEquals(1L, result.getSenderId());
113 assertEquals(10L, result.getGroupId()); 113 assertEquals(10L, result.getGroupId());
114 - assertEquals(ImMessageStatusEnum.UNREAD.getStatus(), result.getStatus());  
115 - assertEquals(ImGroupMessageReceiptStatusEnum.NO_RECEIPT.getStatus(), result.getReceiptStatus()); 114 + assertEquals(ImMessageStatusEnum.NORMAL.getStatus(), result.getStatus());
  115 + assertEquals(ImMessageReceiptStatusEnum.NO_RECEIPT.getStatus(), result.getReceiptStatus());
116 116
117 // 验证推送给 3 个群成员(含发送者自己,用于多端同步) 117 // 验证推送给 3 个群成员(含发送者自己,用于多端同步)
118 verify(imWebSocketService).sendGroupMessageAsync(argThat((Collection<Long> ids) -> 118 verify(imWebSocketService).sendGroupMessageAsync(argThat((Collection<Long> ids) ->
@@ -209,7 +209,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { @@ -209,7 +209,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
209 when(groupMemberService.getActiveGroupMemberUserIdsByGroupId(10L)).thenReturn(List.of(1L, 2L, 3L)); 209 when(groupMemberService.getActiveGroupMemberUserIdsByGroupId(10L)).thenReturn(List.of(1L, 2L, 3L));
210 // 被引用原消息:定向给 {1,2}(对发送人 1 可见,但不覆盖广播受众 {1,2,3}) 210 // 被引用原消息:定向给 {1,2}(对发送人 1 可见,但不覆盖广播受众 {1,2,3})
211 ImGroupMessageDO original = ImGroupMessageDO.builder().id(500L).groupId(10L).senderId(2L) 211 ImGroupMessageDO original = ImGroupMessageDO.builder().id(500L).groupId(10L).senderId(2L)
212 - .status(ImMessageStatusEnum.UNREAD.getStatus()).receiverUserIds(List.of(1L, 2L)) 212 + .status(ImMessageStatusEnum.NORMAL.getStatus()).receiverUserIds(List.of(1L, 2L))
213 .type(ImMessageTypeEnum.TEXT.getType()).content("{\"content\":\"密\"}") 213 .type(ImMessageTypeEnum.TEXT.getType()).content("{\"content\":\"密\"}")
214 .sendTime(LocalDateTime.now()).build(); 214 .sendTime(LocalDateTime.now()).build();
215 when(groupMessageMapper.selectById(500L)).thenReturn(original); 215 when(groupMessageMapper.selectById(500L)).thenReturn(original);
@@ -235,7 +235,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { @@ -235,7 +235,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
235 when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn(member); 235 when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn(member);
236 when(groupMemberService.getActiveGroupMemberUserIdsByGroupId(10L)).thenReturn(List.of(1L, 2L, 3L)); 236 when(groupMemberService.getActiveGroupMemberUserIdsByGroupId(10L)).thenReturn(List.of(1L, 2L, 3L));
237 ImGroupMessageDO original = ImGroupMessageDO.builder().id(500L).groupId(10L).senderId(2L) 237 ImGroupMessageDO original = ImGroupMessageDO.builder().id(500L).groupId(10L).senderId(2L)
238 - .status(ImMessageStatusEnum.UNREAD.getStatus()).receiverUserIds(List.of(1L, 2L, 3L)) 238 + .status(ImMessageStatusEnum.NORMAL.getStatus()).receiverUserIds(List.of(1L, 2L, 3L))
239 .type(ImMessageTypeEnum.TEXT.getType()).content("{\"content\":\"公开\"}") 239 .type(ImMessageTypeEnum.TEXT.getType()).content("{\"content\":\"公开\"}")
240 .sendTime(LocalDateTime.now()).build(); 240 .sendTime(LocalDateTime.now()).build();
241 when(groupMessageMapper.selectById(500L)).thenReturn(original); 241 when(groupMessageMapper.selectById(500L)).thenReturn(original);
@@ -253,8 +253,8 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { @@ -253,8 +253,8 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
253 // ========== pull 测试 ========== 253 // ========== pull 测试 ==========
254 254
255 @Test 255 @Test
256 - public void testPullMessages_pullSetsReadFromCursor() {  
257 - // 准备:当前在群 10,无退群;拉到两条消息,已读位置为 7 256 + public void testPullMessages_doesNotOverwriteStatus() {
  257 + // 准备:拉到两条消息;Phase 2 起 pull 不再按读位置覆盖 status,已读由前端按读位置判断
258 LocalDateTime now = LocalDateTime.now(); 258 LocalDateTime now = LocalDateTime.now();
259 ImGroupMemberDO member = ImGroupMemberDO.builder() 259 ImGroupMemberDO member = ImGroupMemberDO.builder()
260 .groupId(10L).userId(1L) 260 .groupId(10L).userId(1L)
@@ -264,25 +264,22 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { @@ -264,25 +264,22 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
264 264
265 ImGroupMessageDO low = ImGroupMessageDO.builder() 265 ImGroupMessageDO low = ImGroupMessageDO.builder()
266 .id(5L).groupId(10L).senderId(2L).receiverUserIds(List.of(1L)) 266 .id(5L).groupId(10L).senderId(2L).receiverUserIds(List.of(1L))
267 - .status(ImMessageStatusEnum.UNREAD.getStatus()) 267 + .status(ImMessageStatusEnum.NORMAL.getStatus())
268 .sendTime(now.minusHours(2)).build(); 268 .sendTime(now.minusHours(2)).build();
269 ImGroupMessageDO high = ImGroupMessageDO.builder() 269 ImGroupMessageDO high = ImGroupMessageDO.builder()
270 .id(10L).groupId(10L).senderId(2L).receiverUserIds(List.of(1L)) 270 .id(10L).groupId(10L).senderId(2L).receiverUserIds(List.of(1L))
271 - .status(ImMessageStatusEnum.UNREAD.getStatus()) 271 + .status(ImMessageStatusEnum.NORMAL.getStatus())
272 .sendTime(now.minusHours(1)).build(); 272 .sendTime(now.minusHours(1)).build();
273 when(groupMessageMapper.selectListByMinId(eq(1L), anyCollection(), eq(0L), 273 when(groupMessageMapper.selectListByMinId(eq(1L), anyCollection(), eq(0L),
274 any(LocalDateTime.class), eq(100))) 274 any(LocalDateTime.class), eq(100)))
275 .thenReturn(List.of(low, high)); 275 .thenReturn(List.of(low, high));
276 276
277 - when(conversationReadService.getConversationReadMessageId(eq(1L), eq(ImConversationTypeEnum.GROUP.getType()), eq(10L)))  
278 - .thenReturn(7L);  
279 -  
280 List<ImGroupMessageDO> result = groupMessageService.pullGroupMessageList(1L, 0L, 100); 277 List<ImGroupMessageDO> result = groupMessageService.pullGroupMessageList(1L, 0L, 100);
281 278
282 - // 断言:已读位置 7 → id<=7 为 READ,id>7 为 UNREAD 279 + // 断言:status 保持 DB 原值(都 NORMAL),pull 不覆盖
283 assertEquals(2, result.size()); 280 assertEquals(2, result.size());
284 - assertEquals(ImMessageStatusEnum.READ.getStatus(), result.get(0).getStatus());  
285 - assertEquals(ImMessageStatusEnum.UNREAD.getStatus(), result.get(1).getStatus()); 281 + assertEquals(ImMessageStatusEnum.NORMAL.getStatus(), result.get(0).getStatus());
  282 + assertEquals(ImMessageStatusEnum.NORMAL.getStatus(), result.get(1).getStatus());
286 } 283 }
287 284
288 @Test 285 @Test
@@ -298,15 +295,13 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { @@ -298,15 +295,13 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
298 ImGroupMessageDO receiptMsg = ImGroupMessageDO.builder() 295 ImGroupMessageDO receiptMsg = ImGroupMessageDO.builder()
299 .id(100L).groupId(10L).senderId(1L) 296 .id(100L).groupId(10L).senderId(1L)
300 .receiverUserIds(List.of(1L, 2L, 3L)) 297 .receiverUserIds(List.of(1L, 2L, 3L))
301 - .status(ImMessageStatusEnum.UNREAD.getStatus())  
302 - .receiptStatus(ImGroupMessageReceiptStatusEnum.PENDING.getStatus()) 298 + .status(ImMessageStatusEnum.NORMAL.getStatus())
  299 + .receiptStatus(ImMessageReceiptStatusEnum.PENDING.getStatus())
303 .sendTime(now.minusHours(1)).build(); 300 .sendTime(now.minusHours(1)).build();
304 when(groupMessageMapper.selectListByMinId(eq(1L), anyCollection(), eq(0L), 301 when(groupMessageMapper.selectListByMinId(eq(1L), anyCollection(), eq(0L),
305 any(LocalDateTime.class), eq(100))) 302 any(LocalDateTime.class), eq(100)))
306 .thenReturn(List.of(receiptMsg)); 303 .thenReturn(List.of(receiptMsg));
307 304
308 - when(conversationReadService.getConversationReadMessageId(eq(1L), eq(ImConversationTypeEnum.GROUP.getType()), eq(10L)))  
309 - .thenReturn(100L);  
310 Map<Long, Long> positions = new HashMap<>(); 305 Map<Long, Long> positions = new HashMap<>();
311 positions.put(1L, 100L); 306 positions.put(1L, 100L);
312 positions.put(2L, 100L); 307 positions.put(2L, 100L);
@@ -334,7 +329,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { @@ -334,7 +329,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
334 329
335 @Test 330 @Test
336 public void testPullMessages_noReceiptMessageSkipsReadCount() { 331 public void testPullMessages_noReceiptMessageSkipsReadCount() {
337 - // 准备:本人发送但不需要回执(NO_RECEIPT)的消息——只补已读位置,不补 readCount 332 + // 准备:本人发送但不需要回执(NO_RECEIPT)的消息——不补 readCount,status 也不被覆盖
338 LocalDateTime now = LocalDateTime.now(); 333 LocalDateTime now = LocalDateTime.now();
339 ImGroupMemberDO member = ImGroupMemberDO.builder() 334 ImGroupMemberDO member = ImGroupMemberDO.builder()
340 .groupId(10L).userId(1L) 335 .groupId(10L).userId(1L)
@@ -345,20 +340,18 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { @@ -345,20 +340,18 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
345 ImGroupMessageDO noReceiptMsg = ImGroupMessageDO.builder() 340 ImGroupMessageDO noReceiptMsg = ImGroupMessageDO.builder()
346 .id(100L).groupId(10L).senderId(1L) 341 .id(100L).groupId(10L).senderId(1L)
347 .receiverUserIds(List.of(1L, 2L, 3L)) 342 .receiverUserIds(List.of(1L, 2L, 3L))
348 - .status(ImMessageStatusEnum.UNREAD.getStatus())  
349 - .receiptStatus(ImGroupMessageReceiptStatusEnum.NO_RECEIPT.getStatus()) 343 + .status(ImMessageStatusEnum.NORMAL.getStatus())
  344 + .receiptStatus(ImMessageReceiptStatusEnum.NO_RECEIPT.getStatus())
350 .sendTime(now.minusHours(1)).build(); 345 .sendTime(now.minusHours(1)).build();
351 when(groupMessageMapper.selectListByMinId(eq(1L), anyCollection(), eq(0L), 346 when(groupMessageMapper.selectListByMinId(eq(1L), anyCollection(), eq(0L),
352 any(LocalDateTime.class), eq(100))) 347 any(LocalDateTime.class), eq(100)))
353 .thenReturn(List.of(noReceiptMsg)); 348 .thenReturn(List.of(noReceiptMsg));
354 - when(conversationReadService.getConversationReadMessageId(eq(1L), eq(ImConversationTypeEnum.GROUP.getType()), eq(10L)))  
355 - .thenReturn(100L);  
356 349
357 List<ImGroupMessageDO> result = groupMessageService.pullGroupMessageList(1L, 0L, 100); 350 List<ImGroupMessageDO> result = groupMessageService.pullGroupMessageList(1L, 0L, 100);
358 351
359 - // 断言:读位置照常补(status=READ),但 NO_RECEIPT 不补 readCount,也不查 readCount 相关的读位置映射 352 + // 断言:status 保持 NORMAL(pull 不覆盖),NO_RECEIPT 不补 readCount,也不查 readCount 相关的读位置映射
360 assertEquals(1, result.size()); 353 assertEquals(1, result.size());
361 - assertEquals(ImMessageStatusEnum.READ.getStatus(), result.get(0).getStatus()); 354 + assertEquals(ImMessageStatusEnum.NORMAL.getStatus(), result.get(0).getStatus());
362 assertNull(result.get(0).getReadCount()); 355 assertNull(result.get(0).getReadCount());
363 verify(conversationReadService, never()).getUserReadMessageIdMap(anyInt(), anyLong()); 356 verify(conversationReadService, never()).getUserReadMessageIdMap(anyInt(), anyLong());
364 } 357 }
@@ -374,7 +367,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { @@ -374,7 +367,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
374 // 准备:消息由用户 1 发送,刚发送 5 分钟内 367 // 准备:消息由用户 1 发送,刚发送 5 分钟内
375 ImGroupMessageDO message = ImGroupMessageDO.builder() 368 ImGroupMessageDO message = ImGroupMessageDO.builder()
376 .id(50L).senderId(1L).groupId(10L) 369 .id(50L).senderId(1L).groupId(10L)
377 - .status(ImMessageStatusEnum.UNREAD.getStatus()) 370 + .status(ImMessageStatusEnum.NORMAL.getStatus())
378 .sendTime(LocalDateTime.now()).build(); 371 .sendTime(LocalDateTime.now()).build();
379 when(groupMessageMapper.selectById(50L)).thenReturn(message); 372 when(groupMessageMapper.selectById(50L)).thenReturn(message);
380 // 撤回前需要校验用户仍在群中 373 // 撤回前需要校验用户仍在群中
@@ -407,7 +400,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { @@ -407,7 +400,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
407 // 准备 400 // 准备
408 ImGroupMessageDO message = ImGroupMessageDO.builder() 401 ImGroupMessageDO message = ImGroupMessageDO.builder()
409 .id(50L).senderId(2L).groupId(10L) 402 .id(50L).senderId(2L).groupId(10L)
410 - .status(ImMessageStatusEnum.UNREAD.getStatus()) 403 + .status(ImMessageStatusEnum.NORMAL.getStatus())
411 .sendTime(LocalDateTime.now()).build(); 404 .sendTime(LocalDateTime.now()).build();
412 when(groupMessageMapper.selectById(50L)).thenReturn(message); 405 when(groupMessageMapper.selectById(50L)).thenReturn(message);
413 when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn( 406 when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn(
@@ -443,7 +436,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { @@ -443,7 +436,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
443 // 准备:消息发送于 10 分钟前(超过 5 分钟窗口) 436 // 准备:消息发送于 10 分钟前(超过 5 分钟窗口)
444 ImGroupMessageDO message = ImGroupMessageDO.builder() 437 ImGroupMessageDO message = ImGroupMessageDO.builder()
445 .id(50L).senderId(1L).groupId(10L) 438 .id(50L).senderId(1L).groupId(10L)
446 - .status(ImMessageStatusEnum.UNREAD.getStatus()) 439 + .status(ImMessageStatusEnum.NORMAL.getStatus())
447 .sendTime(LocalDateTime.now().minusMinutes(10)).build(); 440 .sendTime(LocalDateTime.now().minusMinutes(10)).build();
448 when(groupMessageMapper.selectById(50L)).thenReturn(message); 441 when(groupMessageMapper.selectById(50L)).thenReturn(message);
449 when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn( 442 when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn(
@@ -569,7 +562,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { @@ -569,7 +562,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
569 ImGroupMessageDO result = groupMessageService.sendGroupMessage(1L, reqVO); 562 ImGroupMessageDO result = groupMessageService.sendGroupMessage(1L, reqVO);
570 563
571 // 断言:receipt=true → 回执状态为 PENDING 564 // 断言:receipt=true → 回执状态为 PENDING
572 - assertEquals(ImGroupMessageReceiptStatusEnum.PENDING.getStatus(), result.getReceiptStatus()); 565 + assertEquals(ImMessageReceiptStatusEnum.PENDING.getStatus(), result.getReceiptStatus());
573 } 566 }
574 } 567 }
575 568
@@ -613,7 +606,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { @@ -613,7 +606,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
613 // 准备:消息合法、可撤回时间内,但发送人已退群 606 // 准备:消息合法、可撤回时间内,但发送人已退群
614 ImGroupMessageDO message = ImGroupMessageDO.builder() 607 ImGroupMessageDO message = ImGroupMessageDO.builder()
615 .id(50L).senderId(1L).groupId(10L) 608 .id(50L).senderId(1L).groupId(10L)
616 - .status(ImMessageStatusEnum.UNREAD.getStatus()) 609 + .status(ImMessageStatusEnum.NORMAL.getStatus())
617 .sendTime(LocalDateTime.now()).build(); 610 .sendTime(LocalDateTime.now()).build();
618 when(groupMessageMapper.selectById(50L)).thenReturn(message); 611 when(groupMessageMapper.selectById(50L)).thenReturn(message);
619 when(groupMemberService.validateMemberInGroup(10L, 1L)) 612 when(groupMemberService.validateMemberInGroup(10L, 1L))
@@ -706,7 +699,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { @@ -706,7 +699,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
706 699
707 ImGroupMessageDO result = groupMessageService.sendGroupMessage(1L, reqVO); 700 ImGroupMessageDO result = groupMessageService.sendGroupMessage(1L, reqVO);
708 701
709 - assertEquals(ImGroupMessageReceiptStatusEnum.NO_RECEIPT.getStatus(), result.getReceiptStatus(), 702 + assertEquals(ImMessageReceiptStatusEnum.NO_RECEIPT.getStatus(), result.getReceiptStatus(),
710 "群已读关闭时即使发送方传 receipt=true 也强制落 NO_RECEIPT"); 703 "群已读关闭时即使发送方传 receipt=true 也强制落 NO_RECEIPT");
711 } 704 }
712 } 705 }
@@ -762,8 +755,8 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { @@ -762,8 +755,8 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
762 .id(100L).groupId(10L).senderId(5L) 755 .id(100L).groupId(10L).senderId(5L)
763 .receiverUserIds(List.of(5L, 1L, 2L)) 756 .receiverUserIds(List.of(5L, 1L, 2L))
764 .sendTime(LocalDateTime.now().minusMinutes(1)) 757 .sendTime(LocalDateTime.now().minusMinutes(1))
765 - .receiptStatus(ImGroupMessageReceiptStatusEnum.PENDING.getStatus())  
766 - .status(ImMessageStatusEnum.UNREAD.getStatus()).build(); 758 + .receiptStatus(ImMessageReceiptStatusEnum.PENDING.getStatus())
  759 + .status(ImMessageStatusEnum.NORMAL.getStatus()).build();
767 when(groupMessageMapper.selectListByGroupIdAndPendingReceipt(10L, 0L, 100L)) 760 when(groupMessageMapper.selectListByGroupIdAndPendingReceipt(10L, 0L, 100L))
768 .thenReturn(List.of(pending)); 761 .thenReturn(List.of(pending));
769 List<ImGroupMemberDO> activeMembers = List.of( 762 List<ImGroupMemberDO> activeMembers = List.of(
@@ -791,7 +784,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { @@ -791,7 +784,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
791 ArgumentCaptor<ImGroupMessageDO> captor = ArgumentCaptor.forClass(ImGroupMessageDO.class); 784 ArgumentCaptor<ImGroupMessageDO> captor = ArgumentCaptor.forClass(ImGroupMessageDO.class);
792 verify(groupMessageMapper).updateById(captor.capture()); 785 verify(groupMessageMapper).updateById(captor.capture());
793 assertEquals(100L, captor.getValue().getId()); 786 assertEquals(100L, captor.getValue().getId());
794 - assertEquals(ImGroupMessageReceiptStatusEnum.DONE.getStatus(), captor.getValue().getReceiptStatus()); 787 + assertEquals(ImMessageReceiptStatusEnum.DONE.getStatus(), captor.getValue().getReceiptStatus());
795 // 断言:给消息发送方(5)推送 RECEIPT 事件 788 // 断言:给消息发送方(5)推送 RECEIPT 事件
796 verify(imWebSocketService).sendGroupMessageAsync(eq(5L), any(ImGroupMessageDTO.class)); 789 verify(imWebSocketService).sendGroupMessageAsync(eq(5L), any(ImGroupMessageDTO.class));
797 } 790 }
@@ -803,8 +796,8 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { @@ -803,8 +796,8 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
803 .id(100L).groupId(10L).senderId(5L) 796 .id(100L).groupId(10L).senderId(5L)
804 .receiverUserIds(List.of(5L, 1L, 2L)) 797 .receiverUserIds(List.of(5L, 1L, 2L))
805 .sendTime(LocalDateTime.now().minusMinutes(1)) 798 .sendTime(LocalDateTime.now().minusMinutes(1))
806 - .receiptStatus(ImGroupMessageReceiptStatusEnum.PENDING.getStatus())  
807 - .status(ImMessageStatusEnum.UNREAD.getStatus()).build(); 799 + .receiptStatus(ImMessageReceiptStatusEnum.PENDING.getStatus())
  800 + .status(ImMessageStatusEnum.NORMAL.getStatus()).build();
808 when(groupMessageMapper.selectListByGroupIdAndPendingReceipt(10L, 0L, 100L)) 801 when(groupMessageMapper.selectListByGroupIdAndPendingReceipt(10L, 0L, 100L))
809 .thenReturn(List.of(pending)); 802 .thenReturn(List.of(pending));
810 List<ImGroupMemberDO> activeMembers = List.of( 803 List<ImGroupMemberDO> activeMembers = List.of(
@@ -910,8 +903,8 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { @@ -910,8 +903,8 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
910 assertEquals(10L, message.getGroupId()); 903 assertEquals(10L, message.getGroupId());
911 assertEquals(ImMessageTypeEnum.RECALL.getType(), message.getType()); 904 assertEquals(ImMessageTypeEnum.RECALL.getType(), message.getType());
912 assertEquals("{\"messageId\":50}", message.getContent()); 905 assertEquals("{\"messageId\":50}", message.getContent());
913 - assertEquals(ImMessageStatusEnum.UNREAD.getStatus(), message.getStatus());  
914 - assertEquals(ImGroupMessageReceiptStatusEnum.NO_RECEIPT.getStatus(), message.getReceiptStatus()); 906 + assertEquals(ImMessageStatusEnum.NORMAL.getStatus(), message.getStatus());
  907 + assertEquals(ImMessageReceiptStatusEnum.NO_RECEIPT.getStatus(), message.getReceiptStatus());
915 assertNotNull(message.getClientMessageId()); 908 assertNotNull(message.getClientMessageId());
916 assertNotNull(message.getSendTime()); 909 assertNotNull(message.getSendTime());
917 } 910 }
@@ -928,7 +921,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { @@ -928,7 +921,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
928 921
929 ArgumentCaptor<ImGroupMessageDO> captor = ArgumentCaptor.forClass(ImGroupMessageDO.class); 922 ArgumentCaptor<ImGroupMessageDO> captor = ArgumentCaptor.forClass(ImGroupMessageDO.class);
930 verify(groupMessageMapper).insert(captor.capture()); 923 verify(groupMessageMapper).insert(captor.capture());
931 - assertEquals(ImGroupMessageReceiptStatusEnum.PENDING.getStatus(), captor.getValue().getReceiptStatus()); 924 + assertEquals(ImMessageReceiptStatusEnum.PENDING.getStatus(), captor.getValue().getReceiptStatus());
932 } 925 }
933 926
934 @Test 927 @Test
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageServiceImplTest.java
@@ -7,6 +7,7 @@ import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivate @@ -7,6 +7,7 @@ import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivate
7 import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageSendReqVO; 7 import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageSendReqVO;
8 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO; 8 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO;
9 import cn.iocoder.yudao.module.im.dal.mysql.message.ImPrivateMessageMapper; 9 import cn.iocoder.yudao.module.im.dal.mysql.message.ImPrivateMessageMapper;
  10 +import cn.iocoder.yudao.module.im.enums.message.ImMessageReceiptStatusEnum;
10 import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum; 11 import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum;
11 import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; 12 import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
12 import cn.iocoder.yudao.module.im.framework.config.ImProperties; 13 import cn.iocoder.yudao.module.im.framework.config.ImProperties;
@@ -87,7 +88,9 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest { @@ -87,7 +88,9 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest {
87 assertEquals(1L, result.getSenderId()); 88 assertEquals(1L, result.getSenderId());
88 assertEquals(2L, result.getReceiverId()); 89 assertEquals(2L, result.getReceiverId());
89 assertEquals(ImMessageTypeEnum.TEXT.getType(), result.getType()); 90 assertEquals(ImMessageTypeEnum.TEXT.getType(), result.getType());
90 - assertEquals(ImMessageStatusEnum.UNREAD.getStatus(), result.getStatus()); 91 + assertEquals(ImMessageStatusEnum.NORMAL.getStatus(), result.getStatus());
  92 + assertEquals(ImMessageReceiptStatusEnum.PENDING.getStatus(), result.getReceiptStatus(),
  93 + "用户私聊消息默认需要回执(PENDING)");
91 assertNotNull(result.getSendTime()); 94 assertNotNull(result.getSendTime());
92 95
93 // 验证调用 96 // 验证调用
@@ -165,10 +168,10 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest { @@ -165,10 +168,10 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest {
165 168
166 @Test 169 @Test
167 public void testReadMessages_success() { 170 public void testReadMessages_success() {
168 - // 准备:前端上报已读到 messageId=5;mapper 返回更新行数 2 表示有未读被翻转  
169 - when(privateMessageMapper.updateBySenderIdAndReceiverIdAndIdLeAndStatus( 171 + // 准备:前端上报已读到 messageId=5;mapper 返回更新行数 2 表示有待回执消息被标记完成
  172 + when(privateMessageMapper.updateBySenderIdAndReceiverIdAndIdLeAndReceiptStatus(
170 eq(2L), eq(1L), eq(5L), 173 eq(2L), eq(1L), eq(5L),
171 - eq(ImMessageStatusEnum.UNREAD.getStatus()), any(ImPrivateMessageDO.class))) 174 + eq(ImMessageReceiptStatusEnum.PENDING.getStatus()), any(ImPrivateMessageDO.class)))
172 .thenReturn(2); 175 .thenReturn(2);
173 // 读位置前进 → 才下发事件 176 // 读位置前进 → 才下发事件
174 when(conversationReadService.updateConversationReadPosition(anyLong(), anyInt(), anyLong(), anyLong())) 177 when(conversationReadService.updateConversationReadPosition(anyLong(), anyInt(), anyLong(), anyLong()))
@@ -177,10 +180,10 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest { @@ -177,10 +180,10 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest {
177 // 调用 180 // 调用
178 privateMessageService.readPrivateMessages(1L, 2L, 5L); 181 privateMessageService.readPrivateMessages(1L, 2L, 5L);
179 182
180 - // 断言:更新了消息状态  
181 - verify(privateMessageMapper).updateBySenderIdAndReceiverIdAndIdLeAndStatus( 183 + // 断言:把待回执(PENDING)消息标记为已完成(DONE);status 不再写 READ
  184 + verify(privateMessageMapper).updateBySenderIdAndReceiverIdAndIdLeAndReceiptStatus(
182 eq(2L), eq(1L), eq(5L), 185 eq(2L), eq(1L), eq(5L),
183 - eq(ImMessageStatusEnum.UNREAD.getStatus()), any(ImPrivateMessageDO.class)); 186 + eq(ImMessageReceiptStatusEnum.PENDING.getStatus()), any(ImPrivateMessageDO.class));
184 187
185 // 断言:发送了 READ + RECEIPT 事件,payload 字段正确 188 // 断言:发送了 READ + RECEIPT 事件,payload 字段正确
186 ArgumentCaptor<Long> userCaptor = ArgumentCaptor.forClass(Long.class); 189 ArgumentCaptor<Long> userCaptor = ArgumentCaptor.forClass(Long.class);
@@ -210,7 +213,7 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest { @@ -210,7 +213,7 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest {
210 // 准备 213 // 准备
211 ImPrivateMessageDO message = ImPrivateMessageDO.builder() 214 ImPrivateMessageDO message = ImPrivateMessageDO.builder()
212 .id(10L).senderId(1L).receiverId(2L) 215 .id(10L).senderId(1L).receiverId(2L)
213 - .status(ImMessageStatusEnum.UNREAD.getStatus()) 216 + .status(ImMessageStatusEnum.NORMAL.getStatus())
214 .sendTime(LocalDateTime.now()).build(); // 刚发送,5 分钟内 217 .sendTime(LocalDateTime.now()).build(); // 刚发送,5 分钟内
215 when(privateMessageMapper.selectById(10L)).thenReturn(message); 218 when(privateMessageMapper.selectById(10L)).thenReturn(message);
216 when(privateMessageMapper.updateById(any(ImPrivateMessageDO.class))).thenReturn(1); 219 when(privateMessageMapper.updateById(any(ImPrivateMessageDO.class))).thenReturn(1);
@@ -233,7 +236,7 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest { @@ -233,7 +236,7 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest {
233 // 准备 236 // 准备
234 ImPrivateMessageDO message = ImPrivateMessageDO.builder() 237 ImPrivateMessageDO message = ImPrivateMessageDO.builder()
235 .id(10L).senderId(2L).receiverId(1L) 238 .id(10L).senderId(2L).receiverId(1L)
236 - .status(ImMessageStatusEnum.UNREAD.getStatus()) 239 + .status(ImMessageStatusEnum.NORMAL.getStatus())
237 .sendTime(LocalDateTime.now()).build(); 240 .sendTime(LocalDateTime.now()).build();
238 when(privateMessageMapper.selectById(10L)).thenReturn(message); 241 when(privateMessageMapper.selectById(10L)).thenReturn(message);
239 242
@@ -274,7 +277,7 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest { @@ -274,7 +277,7 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest {
274 // 准备:消息发送于 10 分钟前(超过 5 分钟窗口) 277 // 准备:消息发送于 10 分钟前(超过 5 分钟窗口)
275 ImPrivateMessageDO message = ImPrivateMessageDO.builder() 278 ImPrivateMessageDO message = ImPrivateMessageDO.builder()
276 .id(10L).senderId(1L).receiverId(2L) 279 .id(10L).senderId(1L).receiverId(2L)
277 - .status(ImMessageStatusEnum.UNREAD.getStatus()) 280 + .status(ImMessageStatusEnum.NORMAL.getStatus())
278 .sendTime(LocalDateTime.now().minusMinutes(10)).build(); 281 .sendTime(LocalDateTime.now().minusMinutes(10)).build();
279 when(privateMessageMapper.selectById(10L)).thenReturn(message); 282 when(privateMessageMapper.selectById(10L)).thenReturn(message);
280 283
@@ -314,7 +317,7 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest { @@ -314,7 +317,7 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest {
314 () -> privateMessageService.readPrivateMessages(1L, 2L, 5L)); 317 () -> privateMessageService.readPrivateMessages(1L, 2L, 5L));
315 assertEquals(MESSAGE_PRIVATE_READ_DISABLED.getCode(), exception.getCode()); 318 assertEquals(MESSAGE_PRIVATE_READ_DISABLED.getCode(), exception.getCode());
316 // 断言:不更新消息状态、不推送 319 // 断言:不更新消息状态、不推送
317 - verify(privateMessageMapper, never()).updateBySenderIdAndReceiverIdAndIdLeAndStatus( 320 + verify(privateMessageMapper, never()).updateBySenderIdAndReceiverIdAndIdLeAndReceiptStatus(
318 anyLong(), anyLong(), anyLong(), anyInt(), any(ImPrivateMessageDO.class)); 321 anyLong(), anyLong(), anyLong(), anyInt(), any(ImPrivateMessageDO.class));
319 verify(imWebSocketService, never()).sendPrivateMessageAsync(anyLong(), any(ImPrivateMessageDTO.class)); 322 verify(imWebSocketService, never()).sendPrivateMessageAsync(anyLong(), any(ImPrivateMessageDTO.class));
320 } 323 }
@@ -336,9 +339,9 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest { @@ -336,9 +339,9 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest {
336 339
337 @Test 340 @Test
338 public void testGetMaxReadMessageId_hit() { 341 public void testGetMaxReadMessageId_hit() {
339 - // 准备:对方读到我发的最大 id=10  
340 - when(privateMessageMapper.selectMaxIdBySenderIdAndReceiverIdAndStatus(  
341 - 1L, 2L, ImMessageStatusEnum.READ.getStatus())).thenReturn(10L); 342 + // 准备:对方(2) 在与我(1) 的会话里读位置=10
  343 + when(conversationReadService.getConversationReadMessageId(eq(2L), anyInt(), eq(1L)))
  344 + .thenReturn(10L);
342 345
343 // 调用 346 // 调用
344 Long result = privateMessageService.getMaxReadMessageId(1L, 2L); 347 Long result = privateMessageService.getMaxReadMessageId(1L, 2L);
@@ -349,9 +352,9 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest { @@ -349,9 +352,9 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest {
349 352
350 @Test 353 @Test
351 public void testGetMaxReadMessageId_miss() { 354 public void testGetMaxReadMessageId_miss() {
352 - // 准备:对方一条都没读过  
353 - when(privateMessageMapper.selectMaxIdBySenderIdAndReceiverIdAndStatus(  
354 - 1L, 2L, ImMessageStatusEnum.READ.getStatus())).thenReturn(null); 355 + // 准备:对方一条都没读过(读位置为 null)
  356 + when(conversationReadService.getConversationReadMessageId(eq(2L), anyInt(), eq(1L)))
  357 + .thenReturn(null);
355 358
356 // 调用 359 // 调用
357 Long result = privateMessageService.getMaxReadMessageId(1L, 2L); 360 Long result = privateMessageService.getMaxReadMessageId(1L, 2L);
@@ -390,7 +393,7 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest { @@ -390,7 +393,7 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest {
390 assertEquals(2L, message.getReceiverId()); 393 assertEquals(2L, message.getReceiverId());
391 assertEquals(ImMessageTypeEnum.RECALL.getType(), message.getType()); 394 assertEquals(ImMessageTypeEnum.RECALL.getType(), message.getType());
392 assertEquals("{\"messageId\":50}", message.getContent()); 395 assertEquals("{\"messageId\":50}", message.getContent());
393 - assertEquals(ImMessageStatusEnum.UNREAD.getStatus(), message.getStatus()); 396 + assertEquals(ImMessageStatusEnum.NORMAL.getStatus(), message.getStatus());
394 assertNotNull(message.getClientMessageId()); 397 assertNotNull(message.getClientMessageId());
395 assertNotNull(message.getSendTime()); 398 assertNotNull(message.getSendTime());
396 // 断言:sender + receiver 双端推送 399 // 断言:sender + receiver 双端推送
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 +6,7 @@ CREATE TABLE IF NOT EXISTS &quot;im_private_message&quot; (
6 "type" smallint NOT NULL COMMENT '消息类型', 6 "type" smallint NOT NULL COMMENT '消息类型',
7 "content" varchar(8192) DEFAULT NULL COMMENT '消息内容', 7 "content" varchar(8192) DEFAULT NULL COMMENT '消息内容',
8 "status" tinyint NOT NULL COMMENT '消息状态', 8 "status" tinyint NOT NULL COMMENT '消息状态',
  9 + "receipt_status" tinyint NOT NULL DEFAULT 0 COMMENT '回执状态',
9 "send_time" timestamp NOT NULL COMMENT '发送时间', 10 "send_time" timestamp NOT NULL COMMENT '发送时间',
10 "creator" varchar(64) DEFAULT '', 11 "creator" varchar(64) DEFAULT '',
11 "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 12 "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,