Commit 927b64fc4e7dbc582c31ee80a15d610b832daff9
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<ImGroupMessageDO> { | @@ -84,7 +84,7 @@ public interface ImGroupMessageMapper extends BaseMapperX<ImGroupMessageDO> { | ||
| 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<ImPrivateMessageDO> | @@ -67,23 +67,13 @@ public interface ImPrivateMessageMapper extends BaseMapperX<ImPrivateMessageDO> | ||
| 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 "im_private_message" ( | @@ -6,6 +6,7 @@ CREATE TABLE IF NOT EXISTS "im_private_message" ( | ||
| 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, |