Commit 3361c757261d621af48206e3010f7e13f426a11d
1 parent
7057f471
✨ feat(message): 继续优化群聊的实现
Showing
11 changed files
with
237 additions
and
83 deletions
yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/enums/ImCommonConstants.java
| @@ -17,4 +17,21 @@ public interface ImCommonConstants { | @@ -17,4 +17,21 @@ public interface ImCommonConstants { | ||
| 17 | */ | 17 | */ |
| 18 | int MESSAGE_RECALL_TIMEOUT_MINUTES = 5; | 18 | int MESSAGE_RECALL_TIMEOUT_MINUTES = 5; |
| 19 | 19 | ||
| 20 | + /** | ||
| 21 | + * 私聊离线消息最大拉取天数 | ||
| 22 | + * <p> | ||
| 23 | + * 客户端通过 pull 接口增量拉取私聊离线消息时,仅返回最近 N 天内产生的消息, | ||
| 24 | + * 超过该窗口的老消息不再主动推送(可通过历史消息接口按需倒翻)。 | ||
| 25 | + */ | ||
| 26 | + int MESSAGE_PRIVATE_PULL_MAX_DAYS = 30; | ||
| 27 | + | ||
| 28 | + /** | ||
| 29 | + * 群聊离线消息最大拉取天数 | ||
| 30 | + * <p> | ||
| 31 | + * 客户端通过 pull 接口增量拉取群聊离线消息时,仅返回最近 N 天内产生的消息; | ||
| 32 | + * 退群前消息的补齐也以该窗口为基准(早于窗口的退群群不再扫描),避免老用户首次 | ||
| 33 | + * 拉取时对历史退群群做大量查询。 | ||
| 34 | + */ | ||
| 35 | + int MESSAGE_GROUP_PULL_MAX_DAYS = 30; | ||
| 36 | + | ||
| 20 | } | 37 | } |
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/ImGroupMessageController.java
| @@ -52,8 +52,10 @@ public class ImGroupMessageController { | @@ -52,8 +52,10 @@ public class ImGroupMessageController { | ||
| 52 | @PutMapping("/read") | 52 | @PutMapping("/read") |
| 53 | @Operation(summary = "标记群聊消息已读") | 53 | @Operation(summary = "标记群聊消息已读") |
| 54 | @Parameter(name = "groupId", description = "群编号", required = true, example = "1") | 54 | @Parameter(name = "groupId", description = "群编号", required = true, example = "1") |
| 55 | - public CommonResult<Boolean> readGroupMessages(@RequestParam("groupId") Long groupId) { | ||
| 56 | - groupMessageService.readGroupMessages(getLoginUserId(), groupId); | 55 | + @Parameter(name = "messageId", description = "已读到的消息编号", required = true, example = "100") |
| 56 | + public CommonResult<Boolean> readGroupMessages(@RequestParam("groupId") Long groupId, | ||
| 57 | + @RequestParam("messageId") Long messageId) { | ||
| 58 | + groupMessageService.readGroupMessages(getLoginUserId(), groupId, messageId); | ||
| 57 | return success(true); | 59 | return success(true); |
| 58 | } | 60 | } |
| 59 | 61 |
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/vo/group/ImGroupMessageSendReqVO.java
| @@ -33,10 +33,7 @@ public class ImGroupMessageSendReqVO { | @@ -33,10 +33,7 @@ public class ImGroupMessageSendReqVO { | ||
| 33 | @Schema(description = "@目标用户编号列表", example = "[1,2,3]") | 33 | @Schema(description = "@目标用户编号列表", example = "[1,2,3]") |
| 34 | private List<Long> atUserIds; | 34 | private List<Long> atUserIds; |
| 35 | 35 | ||
| 36 | - @Schema(description = "定向接收用户编号列表", example = "[1,2]") | ||
| 37 | - private List<Long> receiverUserIds; | ||
| 38 | - | ||
| 39 | @Schema(description = "是否需要回执", example = "false") | 36 | @Schema(description = "是否需要回执", example = "false") |
| 40 | - private Boolean needReceipt; | 37 | + private Boolean receipt; |
| 41 | 38 | ||
| 42 | } | 39 | } |
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/message/ImGroupMessageMapper.java
| @@ -20,17 +20,48 @@ import java.util.List; | @@ -20,17 +20,48 @@ import java.util.List; | ||
| 20 | public interface ImGroupMessageMapper extends BaseMapperX<ImGroupMessageDO> { | 20 | public interface ImGroupMessageMapper extends BaseMapperX<ImGroupMessageDO> { |
| 21 | 21 | ||
| 22 | /** | 22 | /** |
| 23 | - * 根据 minId 增量拉取群聊消息 | 23 | + * 根据 minId + 时间窗口增量拉取群聊消息(在群成员使用) |
| 24 | * | 24 | * |
| 25 | - * @param groupIds 用户所在群编号列表 | ||
| 26 | - * @param minId 最小消息 id(不含) | ||
| 27 | - * @param size 拉取数量 | ||
| 28 | - * @return 消息列表 | 25 | + * @param groupIds 用户当前仍在群内的群编号列表 |
| 26 | + * @param minId 最小消息 id(不含) | ||
| 27 | + * @param minSendTime 最早发送时间(不含),限制离线消息时间窗口 | ||
| 28 | + * @param size 拉取数量 | ||
| 29 | + * @return 消息列表(按 id 升序) | ||
| 29 | */ | 30 | */ |
| 30 | - default List<ImGroupMessageDO> selectListByMinId(List<Long> groupIds, Long minId, Integer size) { | 31 | + default List<ImGroupMessageDO> selectListByMinId(List<Long> groupIds, Long minId, |
| 32 | + LocalDateTime minSendTime, Integer size) { | ||
| 31 | QueryWrapperX<ImGroupMessageDO> wrapper = new QueryWrapperX<>(); | 33 | QueryWrapperX<ImGroupMessageDO> wrapper = new QueryWrapperX<>(); |
| 32 | wrapper.in("group_id", groupIds) | 34 | wrapper.in("group_id", groupIds) |
| 33 | .gt("id", minId) | 35 | .gt("id", minId) |
| 36 | + .gt("send_time", minSendTime) | ||
| 37 | + .ne("status", ImMessageStatusEnum.RECALL.getStatus()) | ||
| 38 | + .orderByAsc("id"); | ||
| 39 | + wrapper.limitN(size); | ||
| 40 | + return selectList(wrapper); | ||
| 41 | + } | ||
| 42 | + | ||
| 43 | + /** | ||
| 44 | + * 查询"退群前"的离线消息(退群成员使用) | ||
| 45 | + * <p> | ||
| 46 | + * 语义:用户已退出某群,但仍需把 {@code minId} 之后、{@code minSendTime} 之后、 | ||
| 47 | + * 不晚于退群时间的消息补齐到本地,便于前端看到完整上下文。 | ||
| 48 | + * | ||
| 49 | + * @param groupId 群编号 | ||
| 50 | + * @param minId 最小消息 id(不含) | ||
| 51 | + * @param minSendTime 最早发送时间(不含) | ||
| 52 | + * @param quitTime 退群时间(含),仅返回退群当时已存在的消息 | ||
| 53 | + * @param size 拉取数量 | ||
| 54 | + * @return 消息列表(按 id 升序) | ||
| 55 | + */ | ||
| 56 | + default List<ImGroupMessageDO> selectListByGroupIdAndMinIdAndQuitTimeBefore(Long groupId, Long minId, | ||
| 57 | + LocalDateTime minSendTime, | ||
| 58 | + LocalDateTime quitTime, | ||
| 59 | + Integer size) { | ||
| 60 | + QueryWrapperX<ImGroupMessageDO> wrapper = new QueryWrapperX<>(); | ||
| 61 | + wrapper.eq("group_id", groupId) | ||
| 62 | + .gt("id", minId) | ||
| 63 | + .gt("send_time", minSendTime) | ||
| 64 | + .le("send_time", quitTime) | ||
| 34 | .ne("status", ImMessageStatusEnum.RECALL.getStatus()) | 65 | .ne("status", ImMessageStatusEnum.RECALL.getStatus()) |
| 35 | .orderByAsc("id"); | 66 | .orderByAsc("id"); |
| 36 | wrapper.limitN(size); | 67 | wrapper.limitN(size); |
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/message/ImPrivateMessageMapper.java
| @@ -7,6 +7,7 @@ import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO; | @@ -7,6 +7,7 @@ import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO; | ||
| 7 | import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum; | 7 | import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum; |
| 8 | import org.apache.ibatis.annotations.Mapper; | 8 | import org.apache.ibatis.annotations.Mapper; |
| 9 | 9 | ||
| 10 | +import java.time.LocalDateTime; | ||
| 10 | import java.util.List; | 11 | import java.util.List; |
| 11 | 12 | ||
| 12 | /** | 13 | /** |
| @@ -18,19 +19,22 @@ import java.util.List; | @@ -18,19 +19,22 @@ import java.util.List; | ||
| 18 | public interface ImPrivateMessageMapper extends BaseMapperX<ImPrivateMessageDO> { | 19 | public interface ImPrivateMessageMapper extends BaseMapperX<ImPrivateMessageDO> { |
| 19 | 20 | ||
| 20 | /** | 21 | /** |
| 21 | - * 根据 minId 增量拉取私聊消息 | 22 | + * 根据 minId + 时间窗口增量拉取私聊消息 |
| 22 | * | 23 | * |
| 23 | - * @param userId 当前用户编号 | ||
| 24 | - * @param minId 最小消息 id(不含) | ||
| 25 | - * @param size 拉取数量 | 24 | + * @param userId 当前用户编号 |
| 25 | + * @param minId 最小消息 id(不含) | ||
| 26 | + * @param minSendTime 最早发送时间(不含),限制离线消息时间窗口 | ||
| 27 | + * @param size 拉取数量 | ||
| 26 | * @return 消息列表 | 28 | * @return 消息列表 |
| 27 | */ | 29 | */ |
| 28 | - default List<ImPrivateMessageDO> selectListByMinId(Long userId, Long minId, Integer size) { | 30 | + default List<ImPrivateMessageDO> selectListByMinId(Long userId, Long minId, |
| 31 | + LocalDateTime minSendTime, Integer size) { | ||
| 29 | QueryWrapperX<ImPrivateMessageDO> wrapper = new QueryWrapperX<>(); | 32 | QueryWrapperX<ImPrivateMessageDO> wrapper = new QueryWrapperX<>(); |
| 30 | wrapper.and(w -> w.eq("sender_id", userId) | 33 | wrapper.and(w -> w.eq("sender_id", userId) |
| 31 | .or() | 34 | .or() |
| 32 | .eq("receiver_id", userId)) | 35 | .eq("receiver_id", userId)) |
| 33 | .gt("id", minId) | 36 | .gt("id", minId) |
| 37 | + .gt("send_time", minSendTime) | ||
| 34 | .orderByAsc("id"); | 38 | .orderByAsc("id"); |
| 35 | wrapper.limitN(size); | 39 | wrapper.limitN(size); |
| 36 | return selectList(wrapper); | 40 | return selectList(wrapper); |
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageService.java
| @@ -35,10 +35,11 @@ public interface ImGroupMessageService { | @@ -35,10 +35,11 @@ public interface ImGroupMessageService { | ||
| 35 | /** | 35 | /** |
| 36 | * 标记群聊消息已读 | 36 | * 标记群聊消息已读 |
| 37 | * | 37 | * |
| 38 | - * @param userId 当前用户编号 | ||
| 39 | - * @param groupId 群编号 | 38 | + * @param userId 当前用户编号 |
| 39 | + * @param groupId 群编号 | ||
| 40 | + * @param messageId 已读到的消息编号 | ||
| 40 | */ | 41 | */ |
| 41 | - void readGroupMessages(Long userId, Long groupId); | 42 | + void readGroupMessages(Long userId, Long groupId, Long messageId); |
| 42 | 43 | ||
| 43 | /** | 44 | /** |
| 44 | * 撤回群聊消息 | 45 | * 撤回群聊消息 |
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImpl.java
| 1 | package cn.iocoder.yudao.module.im.service.message; | 1 | package cn.iocoder.yudao.module.im.service.message; |
| 2 | 2 | ||
| 3 | import cn.hutool.core.collection.CollUtil; | 3 | import cn.hutool.core.collection.CollUtil; |
| 4 | +import cn.hutool.core.lang.Assert; | ||
| 4 | import cn.hutool.core.util.IdUtil; | 5 | import cn.hutool.core.util.IdUtil; |
| 5 | import cn.hutool.core.util.ObjUtil; | 6 | import cn.hutool.core.util.ObjUtil; |
| 6 | import cn.hutool.extra.spring.SpringUtil; | 7 | import cn.hutool.extra.spring.SpringUtil; |
| @@ -35,8 +36,10 @@ import java.util.*; | @@ -35,8 +36,10 @@ import java.util.*; | ||
| 35 | 36 | ||
| 36 | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; | 37 | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| 37 | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; | 38 | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; |
| 39 | +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; | ||
| 38 | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; | 40 | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; |
| 39 | import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*; | 41 | import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*; |
| 42 | +import static cn.iocoder.yudao.module.im.enums.ImCommonConstants.MESSAGE_GROUP_PULL_MAX_DAYS; | ||
| 40 | import static cn.iocoder.yudao.module.im.enums.ImCommonConstants.MESSAGE_MAX_PULL_SIZE; | 43 | import static cn.iocoder.yudao.module.im.enums.ImCommonConstants.MESSAGE_MAX_PULL_SIZE; |
| 41 | import static cn.iocoder.yudao.module.im.enums.ImCommonConstants.MESSAGE_RECALL_TIMEOUT_MINUTES; | 44 | import static cn.iocoder.yudao.module.im.enums.ImCommonConstants.MESSAGE_RECALL_TIMEOUT_MINUTES; |
| 42 | 45 | ||
| @@ -101,21 +104,44 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { | @@ -101,21 +104,44 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { | ||
| 101 | if (size > MESSAGE_MAX_PULL_SIZE) { | 104 | if (size > MESSAGE_MAX_PULL_SIZE) { |
| 102 | throw exception(MESSAGE_PULL_SIZE_EXCEEDED); | 105 | throw exception(MESSAGE_PULL_SIZE_EXCEEDED); |
| 103 | } | 106 | } |
| 104 | - // 1. 获取用户所在的所有群 | ||
| 105 | - List<ImGroupMemberDO> members = groupMemberService.getGroupMembersByUserId(userId); | ||
| 106 | - if (CollUtil.isEmpty(members)) { | ||
| 107 | - return Collections.emptyList(); | 107 | + // 0. 拉取时间窗:超过窗口的老消息不再通过离线通道推送 |
| 108 | + LocalDateTime minSendTime = LocalDateTime.now().minusDays(MESSAGE_GROUP_PULL_MAX_DAYS); | ||
| 109 | + | ||
| 110 | + // 1. 主查询:仅用"当前仍在群"的成员记录驱动(已退群的群由步骤 2 补齐) | ||
| 111 | + List<ImGroupMemberDO> activeMembers = groupMemberService.getActiveGroupMemberListByUserId(userId); | ||
| 112 | + Map<Long, ImGroupMemberDO> memberMap = convertMap(activeMembers, ImGroupMemberDO::getGroupId); | ||
| 113 | + List<ImGroupMessageDO> messages = new ArrayList<>(); | ||
| 114 | + if (CollUtil.isNotEmpty(activeMembers)) { | ||
| 115 | + List<Long> groupIds = convertList(activeMembers, ImGroupMemberDO::getGroupId); | ||
| 116 | + messages.addAll(groupMessageMapper.selectListByMinId(groupIds, minId, minSendTime, size)); | ||
| 108 | } | 117 | } |
| 109 | - List<Long> groupIds = convertList(members, ImGroupMemberDO::getGroupId); | ||
| 110 | 118 | ||
| 111 | - // 2. 拉取消息 | ||
| 112 | - List<ImGroupMessageDO> messages = groupMessageMapper.selectListByMinId(groupIds, minId, size); | 119 | + // 2. 补齐"退群前"的消息: |
| 120 | + // - 退群时间早于窗口起点的群直接忽略(消息已不在拉取范围); | ||
| 121 | + // - 若 minId > 0 且能查到对应消息,则进一步把下限抬到该消息的 sendTime, | ||
| 122 | + // 避免把客户端已拥有的老消息再次推送。 | ||
| 123 | + LocalDateTime minQuitTime = minSendTime; | ||
| 124 | + if (minId != null && minId > 0) { | ||
| 125 | + ImGroupMessageDO minMessage = groupMessageMapper.selectById(minId); | ||
| 126 | + if (minMessage != null && minMessage.getSendTime() != null | ||
| 127 | + && minMessage.getSendTime().isAfter(minSendTime)) { | ||
| 128 | + minQuitTime = minMessage.getSendTime(); | ||
| 129 | + } | ||
| 130 | + } | ||
| 131 | + List<ImGroupMemberDO> quitMembers = groupMemberService.getQuitGroupMemberListByUserId(userId, minQuitTime); | ||
| 132 | + for (ImGroupMemberDO quitMember : quitMembers) { | ||
| 133 | + List<ImGroupMessageDO> quitGroupMessages = groupMessageMapper.selectListByGroupIdAndMinIdAndQuitTimeBefore( | ||
| 134 | + quitMember.getGroupId(), minId, minSendTime, quitMember.getQuitTime(), size); | ||
| 135 | + if (CollUtil.isNotEmpty(quitGroupMessages)) { | ||
| 136 | + messages.addAll(quitGroupMessages); | ||
| 137 | + memberMap.put(quitMember.getGroupId(), quitMember); | ||
| 138 | + } | ||
| 139 | + } | ||
| 113 | 140 | ||
| 114 | - // 3. 过滤:入群前不可见、退群后不可见、定向接收过滤 | ||
| 115 | - // TODO @AI:Java 层过滤在 SQL limit 之后,可能导致返回数量不足 size(跨群场景 joinTime 各异,无法下沉 SQL)【后续优化】 | ||
| 116 | - Map<Long, ImGroupMemberDO> memberMap = CollUtil.toMap(members, new HashMap<>(), ImGroupMemberDO::getGroupId); | 141 | + // 3. 按可见性过滤(入群前不可见、定向消息排除),并按 id 升序重排 |
| 117 | List<ImGroupMessageDO> result = messages.stream() | 142 | List<ImGroupMessageDO> result = messages.stream() |
| 118 | .filter(msg -> isMessageVisible(msg, memberMap.get(msg.getGroupId()), userId)) | 143 | .filter(msg -> isMessageVisible(msg, memberMap.get(msg.getGroupId()), userId)) |
| 144 | + .sorted(Comparator.comparing(ImGroupMessageDO::getId)) | ||
| 119 | .toList(); | 145 | .toList(); |
| 120 | log.info("[pullGroupMessageList][userId({}) minId({}) size({}) result({})]", | 146 | log.info("[pullGroupMessageList][userId({}) minId({}) size({}) result({})]", |
| 121 | userId, minId, size, result.size()); | 147 | userId, minId, size, result.size()); |
| @@ -178,6 +204,7 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { | @@ -178,6 +204,7 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { | ||
| 178 | return recallMessage; | 204 | return recallMessage; |
| 179 | } | 205 | } |
| 180 | 206 | ||
| 207 | + // TODO @芋艿:这个方法在优化下; | ||
| 181 | @Override | 208 | @Override |
| 182 | public List<Long> getGroupReadUsers(Long userId, Long groupId, Long messageId) { | 209 | public List<Long> getGroupReadUsers(Long userId, Long groupId, Long messageId) { |
| 183 | // 1.1 校验用户在群中(权限校验) | 210 | // 1.1 校验用户在群中(权限校验) |
| @@ -189,7 +216,7 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { | @@ -189,7 +216,7 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { | ||
| 189 | } | 216 | } |
| 190 | 217 | ||
| 191 | // 2. 获取所有成员和已读位置 | 218 | // 2. 获取所有成员和已读位置 |
| 192 | - List<ImGroupMemberDO> allMembers = groupMemberService.selectByGroupId(groupId); | 219 | + List<ImGroupMemberDO> allMembers = groupMemberService.getGroupMemberListByGroupId(groupId); |
| 193 | Map<Long, Long> allPositions = groupMessageReadRedisDAO.getReadMaxMessageIdMap(groupId); | 220 | Map<Long, Long> allPositions = groupMessageReadRedisDAO.getReadMaxMessageIdMap(groupId); |
| 194 | 221 | ||
| 195 | // 3. 计算该消息的可见成员集合(排除发送者自己) | 222 | // 3. 计算该消息的可见成员集合(排除发送者自己) |
| @@ -225,14 +252,11 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { | @@ -225,14 +252,11 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { | ||
| 225 | @Async | 252 | @Async |
| 226 | public void sendGroupMessageEvent(ImGroupMessageDO message) { | 253 | public void sendGroupMessageEvent(ImGroupMessageDO message) { |
| 227 | ImGroupMessageDTO dto = ImGroupMessageDTO.ofSend(message); | 254 | ImGroupMessageDTO dto = ImGroupMessageDTO.ofSend(message); |
| 228 | - // 广播给群内可见成员(含发送方自己,支持多端同步) | ||
| 229 | - List<ImGroupMemberDO> members = groupMemberService.selectByGroupId(message.getGroupId()); | 255 | + // 广播给群内有效成员(含发送方自己,支持多端同步) |
| 256 | + List<ImGroupMemberDO> members = groupMemberService.getActiveGroupMemberListByGroupId(message.getGroupId()); | ||
| 257 | + // 定向消息仍需过滤,非目标用户不应收到推送 | ||
| 230 | List<Long> receiverList = message.getReceiverUserIds(); | 258 | List<Long> receiverList = message.getReceiverUserIds(); |
| 231 | for (ImGroupMemberDO member : members) { | 259 | for (ImGroupMemberDO member : members) { |
| 232 | - if (CommonStatusEnum.DISABLE.getStatus().equals(member.getStatus())) { | ||
| 233 | - continue; | ||
| 234 | - } | ||
| 235 | - // 特殊:仅在存在定向接收列表时,才进行过滤;如果没有定向列表,则默认所有成员可见 | ||
| 236 | if (CollUtil.isNotEmpty(receiverList) && !receiverList.contains(member.getUserId()) | 260 | if (CollUtil.isNotEmpty(receiverList) && !receiverList.contains(member.getUserId()) |
| 237 | && ObjUtil.notEqual(member.getUserId(), message.getSenderId())) { | 261 | && ObjUtil.notEqual(member.getUserId(), message.getSenderId())) { |
| 238 | continue; | 262 | continue; |
| @@ -263,8 +287,7 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { | @@ -263,8 +287,7 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { | ||
| 263 | if (CollUtil.isEmpty(pendingMessages)) { | 287 | if (CollUtil.isEmpty(pendingMessages)) { |
| 264 | return; | 288 | return; |
| 265 | } | 289 | } |
| 266 | - // DONE @AI:收敛到 groupMemberService 方法里,会不会更通用;——已收敛为 getActiveMemberListByGroupId | ||
| 267 | - List<ImGroupMemberDO> activeMembers = groupMemberService.getActiveMemberListByGroupId(groupId); | 290 | + List<ImGroupMemberDO> activeMembers = groupMemberService.getActiveGroupMemberListByGroupId(groupId); |
| 268 | Map<Long, Long> allPositions = groupMessageReadRedisDAO.getReadMaxMessageIdMap(groupId); | 291 | Map<Long, Long> allPositions = groupMessageReadRedisDAO.getReadMaxMessageIdMap(groupId); |
| 269 | for (ImGroupMessageDO message : pendingMessages) { | 292 | for (ImGroupMessageDO message : pendingMessages) { |
| 270 | // 2.1.1 统计可见成员中的已读人数 | 293 | // 2.1.1 统计可见成员中的已读人数 |
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageServiceImpl.java
| @@ -32,6 +32,7 @@ import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionU | @@ -32,6 +32,7 @@ import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionU | ||
| 32 | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; | 32 | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; |
| 33 | import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*; | 33 | import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*; |
| 34 | import static cn.iocoder.yudao.module.im.enums.ImCommonConstants.MESSAGE_MAX_PULL_SIZE; | 34 | import static cn.iocoder.yudao.module.im.enums.ImCommonConstants.MESSAGE_MAX_PULL_SIZE; |
| 35 | +import static cn.iocoder.yudao.module.im.enums.ImCommonConstants.MESSAGE_PRIVATE_PULL_MAX_DAYS; | ||
| 35 | import static cn.iocoder.yudao.module.im.enums.ImCommonConstants.MESSAGE_RECALL_TIMEOUT_MINUTES; | 36 | import static cn.iocoder.yudao.module.im.enums.ImCommonConstants.MESSAGE_RECALL_TIMEOUT_MINUTES; |
| 36 | 37 | ||
| 37 | /** | 38 | /** |
| @@ -87,7 +88,11 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService { | @@ -87,7 +88,11 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService { | ||
| 87 | if (size > MESSAGE_MAX_PULL_SIZE) { | 88 | if (size > MESSAGE_MAX_PULL_SIZE) { |
| 88 | throw exception(MESSAGE_PULL_SIZE_EXCEEDED); | 89 | throw exception(MESSAGE_PULL_SIZE_EXCEEDED); |
| 89 | } | 90 | } |
| 90 | - List<ImPrivateMessageDO> messages = privateMessageMapper.selectListByMinId(userId, minId, size); | 91 | + // 0. 拉取时间窗:超过窗口的老消息不再通过离线通道推送 |
| 92 | + LocalDateTime minSendTime = LocalDateTime.now().minusDays(MESSAGE_PRIVATE_PULL_MAX_DAYS); | ||
| 93 | + | ||
| 94 | + // 根据 minId 和 minSendTime 拉取消息,避免 minId 恰好被发出后才拉取,导致漏消息 | ||
| 95 | + List<ImPrivateMessageDO> messages = privateMessageMapper.selectListByMinId(userId, minId, minSendTime, size); | ||
| 91 | log.info("[pullPrivateMessageList][userId({}) minId({}) size({}) result({})]", | 96 | log.info("[pullPrivateMessageList][userId({}) minId({}) size({}) result({})]", |
| 92 | userId, minId, size, messages.size()); | 97 | userId, minId, size, messages.size()); |
| 93 | return messages; | 98 | return messages; |
yudao-module-im/yudao-module-im-biz/src/test/java/cn/iocoder/yudao/module/im/dal/mysql/message/ImPrivateMessageMapperTest.java
| @@ -21,6 +21,11 @@ public class ImPrivateMessageMapperTest extends BaseDbUnitTest { | @@ -21,6 +21,11 @@ public class ImPrivateMessageMapperTest extends BaseDbUnitTest { | ||
| 21 | @Resource | 21 | @Resource |
| 22 | private ImPrivateMessageMapper mapper; | 22 | private ImPrivateMessageMapper mapper; |
| 23 | 23 | ||
| 24 | + /** | ||
| 25 | + * 一个足够早的时间作为 minSendTime,避免把测试数据过滤掉 | ||
| 26 | + */ | ||
| 27 | + private static final LocalDateTime FAR_PAST = LocalDateTime.of(2000, 1, 1, 0, 0, 0); | ||
| 28 | + | ||
| 24 | // ========== selectListByMinId ========== | 29 | // ========== selectListByMinId ========== |
| 25 | 30 | ||
| 26 | @Test | 31 | @Test |
| @@ -36,7 +41,7 @@ public class ImPrivateMessageMapperTest extends BaseDbUnitTest { | @@ -36,7 +41,7 @@ public class ImPrivateMessageMapperTest extends BaseDbUnitTest { | ||
| 36 | mapper.insert(msg3); | 41 | mapper.insert(msg3); |
| 37 | 42 | ||
| 38 | // 调用:用户 1 从 id=0 拉取 | 43 | // 调用:用户 1 从 id=0 拉取 |
| 39 | - List<ImPrivateMessageDO> result = mapper.selectListByMinId(1L, 0L, 100); | 44 | + List<ImPrivateMessageDO> result = mapper.selectListByMinId(1L, 0L, FAR_PAST, 100); |
| 40 | 45 | ||
| 41 | // 断言:只包含与用户 1 相关的 2 条 | 46 | // 断言:只包含与用户 1 相关的 2 条 |
| 42 | assertEquals(2, result.size()); | 47 | assertEquals(2, result.size()); |
| @@ -53,7 +58,7 @@ public class ImPrivateMessageMapperTest extends BaseDbUnitTest { | @@ -53,7 +58,7 @@ public class ImPrivateMessageMapperTest extends BaseDbUnitTest { | ||
| 53 | mapper.insert(msg2); | 58 | mapper.insert(msg2); |
| 54 | 59 | ||
| 55 | // 调用:从 msg1.id 之后拉取 | 60 | // 调用:从 msg1.id 之后拉取 |
| 56 | - List<ImPrivateMessageDO> result = mapper.selectListByMinId(1L, msg1.getId(), 100); | 61 | + List<ImPrivateMessageDO> result = mapper.selectListByMinId(1L, msg1.getId(), FAR_PAST, 100); |
| 57 | 62 | ||
| 58 | // 断言:只有 msg2 | 63 | // 断言:只有 msg2 |
| 59 | assertEquals(1, result.size()); | 64 | assertEquals(1, result.size()); |
| @@ -68,12 +73,31 @@ public class ImPrivateMessageMapperTest extends BaseDbUnitTest { | @@ -68,12 +73,31 @@ public class ImPrivateMessageMapperTest extends BaseDbUnitTest { | ||
| 68 | mapper.insert(buildMessage(1L, 2L, ImMessageStatusEnum.UNREAD)); | 73 | mapper.insert(buildMessage(1L, 2L, ImMessageStatusEnum.UNREAD)); |
| 69 | 74 | ||
| 70 | // 调用:limit 2 | 75 | // 调用:limit 2 |
| 71 | - List<ImPrivateMessageDO> result = mapper.selectListByMinId(1L, 0L, 2); | 76 | + List<ImPrivateMessageDO> result = mapper.selectListByMinId(1L, 0L, FAR_PAST, 2); |
| 72 | 77 | ||
| 73 | // 断言 | 78 | // 断言 |
| 74 | assertEquals(2, result.size()); | 79 | assertEquals(2, result.size()); |
| 75 | } | 80 | } |
| 76 | 81 | ||
| 82 | + @Test | ||
| 83 | + public void testSelectListByMinId_sendTimeFilter() { | ||
| 84 | + // 准备:一条落在窗口内,一条落在窗口外 | ||
| 85 | + ImPrivateMessageDO newMsg = buildMessage(1L, 2L, ImMessageStatusEnum.UNREAD); | ||
| 86 | + newMsg.setSendTime(LocalDateTime.now().minusDays(1)); | ||
| 87 | + mapper.insert(newMsg); | ||
| 88 | + ImPrivateMessageDO oldMsg = buildMessage(1L, 2L, ImMessageStatusEnum.UNREAD); | ||
| 89 | + oldMsg.setSendTime(LocalDateTime.now().minusDays(40)); | ||
| 90 | + mapper.insert(oldMsg); | ||
| 91 | + | ||
| 92 | + // 调用:窗口起点 = 30 天前 | ||
| 93 | + List<ImPrivateMessageDO> result = mapper.selectListByMinId(1L, 0L, | ||
| 94 | + LocalDateTime.now().minusDays(30), 100); | ||
| 95 | + | ||
| 96 | + // 断言:只返回窗口内的消息 | ||
| 97 | + assertEquals(1, result.size()); | ||
| 98 | + assertEquals(newMsg.getId(), result.get(0).getId()); | ||
| 99 | + } | ||
| 100 | + | ||
| 77 | // ========== selectHistoryList ========== | 101 | // ========== selectHistoryList ========== |
| 78 | 102 | ||
| 79 | @Test | 103 | @Test |
yudao-module-im/yudao-module-im-biz/src/test/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImplTest.java
| @@ -91,7 +91,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { | @@ -91,7 +91,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { | ||
| 91 | ImGroupMemberDO.builder().groupId(10L).userId(3L) | 91 | ImGroupMemberDO.builder().groupId(10L).userId(3L) |
| 92 | .status(CommonStatusEnum.ENABLE.getStatus()).build() | 92 | .status(CommonStatusEnum.ENABLE.getStatus()).build() |
| 93 | ); | 93 | ); |
| 94 | - when(groupMemberService.selectByGroupId(10L)).thenReturn(allMembers); | 94 | + when(groupMemberService.getActiveGroupMemberListByGroupId(10L)).thenReturn(allMembers); |
| 95 | when(groupMessageMapper.insert(any(ImGroupMessageDO.class))).thenAnswer(invocation -> { | 95 | when(groupMessageMapper.insert(any(ImGroupMessageDO.class))).thenAnswer(invocation -> { |
| 96 | ImGroupMessageDO msg = invocation.getArgument(0); | 96 | ImGroupMessageDO msg = invocation.getArgument(0); |
| 97 | msg.setId(99L); | 97 | msg.setId(99L); |
| @@ -154,23 +154,27 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { | @@ -154,23 +154,27 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { | ||
| 154 | 154 | ||
| 155 | @Test | 155 | @Test |
| 156 | public void testPullMessages_joinTimeFilter() { | 156 | public void testPullMessages_joinTimeFilter() { |
| 157 | - // 准备 | ||
| 158 | - LocalDateTime joinTime = LocalDateTime.of(2026, 4, 12, 10, 0, 0); | 157 | + // 准备:消息时间用"最近 1 天内"避免被 30 天窗口过滤 |
| 158 | + LocalDateTime now = LocalDateTime.now(); | ||
| 159 | + LocalDateTime joinTime = now.minusHours(2); | ||
| 159 | ImGroupMemberDO member = ImGroupMemberDO.builder() | 160 | ImGroupMemberDO member = ImGroupMemberDO.builder() |
| 160 | .groupId(10L).userId(1L) | 161 | .groupId(10L).userId(1L) |
| 161 | .status(CommonStatusEnum.ENABLE.getStatus()) | 162 | .status(CommonStatusEnum.ENABLE.getStatus()) |
| 162 | .joinTime(joinTime).build(); | 163 | .joinTime(joinTime).build(); |
| 163 | - when(groupMemberService.getGroupMembersByUserId(1L)).thenReturn(List.of(member)); | 164 | + when(groupMemberService.getActiveGroupMemberListByUserId(1L)).thenReturn(List.of(member)); |
| 164 | 165 | ||
| 165 | - // 模拟一条入群前的消息和一条入群后的消息 | 166 | + // 入群前 1 条 + 入群后 1 条 |
| 166 | ImGroupMessageDO beforeJoin = ImGroupMessageDO.builder() | 167 | ImGroupMessageDO beforeJoin = ImGroupMessageDO.builder() |
| 167 | .id(1L).groupId(10L).senderId(2L) | 168 | .id(1L).groupId(10L).senderId(2L) |
| 168 | - .sendTime(LocalDateTime.of(2026, 4, 12, 9, 0, 0)).build(); | 169 | + .sendTime(now.minusHours(3)).build(); |
| 169 | ImGroupMessageDO afterJoin = ImGroupMessageDO.builder() | 170 | ImGroupMessageDO afterJoin = ImGroupMessageDO.builder() |
| 170 | .id(2L).groupId(10L).senderId(2L) | 171 | .id(2L).groupId(10L).senderId(2L) |
| 171 | - .sendTime(LocalDateTime.of(2026, 4, 12, 11, 0, 0)).build(); | ||
| 172 | - when(groupMessageMapper.selectListByMinId(List.of(10L), 0L, 100)) | 172 | + .sendTime(now.minusHours(1)).build(); |
| 173 | + when(groupMessageMapper.selectListByMinId(eq(List.of(10L)), eq(0L), | ||
| 174 | + any(LocalDateTime.class), eq(100))) | ||
| 173 | .thenReturn(List.of(beforeJoin, afterJoin)); | 175 | .thenReturn(List.of(beforeJoin, afterJoin)); |
| 176 | + when(groupMemberService.getQuitGroupMemberListByUserId(eq(1L), any(LocalDateTime.class))) | ||
| 177 | + .thenReturn(List.of()); | ||
| 174 | 178 | ||
| 175 | // 调用 | 179 | // 调用 |
| 176 | List<ImGroupMessageDO> result = groupMessageService.pullGroupMessageList(1L, 0L, 100); | 180 | List<ImGroupMessageDO> result = groupMessageService.pullGroupMessageList(1L, 0L, 100); |
| @@ -181,54 +185,95 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { | @@ -181,54 +185,95 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { | ||
| 181 | } | 185 | } |
| 182 | 186 | ||
| 183 | @Test | 187 | @Test |
| 184 | - public void testPullMessages_quitTimeFilter() { | ||
| 185 | - // 准备 | ||
| 186 | - LocalDateTime joinTime = LocalDateTime.of(2026, 4, 10, 10, 0, 0); | ||
| 187 | - LocalDateTime quitTime = LocalDateTime.of(2026, 4, 12, 10, 0, 0); | ||
| 188 | - ImGroupMemberDO member = ImGroupMemberDO.builder() | 188 | + public void testPullMessages_quitMemberLoadsPreQuitMessages() { |
| 189 | + // 准备:当前群(在群)+ 一个最近退群 | ||
| 190 | + LocalDateTime now = LocalDateTime.now(); | ||
| 191 | + ImGroupMemberDO activeMember = ImGroupMemberDO.builder() | ||
| 189 | .groupId(10L).userId(1L) | 192 | .groupId(10L).userId(1L) |
| 193 | + .status(CommonStatusEnum.ENABLE.getStatus()) | ||
| 194 | + .joinTime(now.minusDays(10)).build(); | ||
| 195 | + when(groupMemberService.getActiveGroupMemberListByUserId(1L)).thenReturn(List.of(activeMember)); | ||
| 196 | + | ||
| 197 | + ImGroupMessageDO activeMsg = ImGroupMessageDO.builder() | ||
| 198 | + .id(5L).groupId(10L).senderId(2L) | ||
| 199 | + .sendTime(now.minusHours(2)).build(); | ||
| 200 | + when(groupMessageMapper.selectListByMinId(eq(List.of(10L)), eq(0L), | ||
| 201 | + any(LocalDateTime.class), eq(100))) | ||
| 202 | + .thenReturn(List.of(activeMsg)); | ||
| 203 | + | ||
| 204 | + // 已退群:退群时间在窗口内(minId=0,minQuitTime 直接用 minSendTime) | ||
| 205 | + LocalDateTime quitTime = now.minusDays(3); | ||
| 206 | + ImGroupMemberDO quitMember = ImGroupMemberDO.builder() | ||
| 207 | + .groupId(20L).userId(1L) | ||
| 190 | .status(CommonStatusEnum.DISABLE.getStatus()) | 208 | .status(CommonStatusEnum.DISABLE.getStatus()) |
| 191 | - .joinTime(joinTime).quitTime(quitTime).build(); | ||
| 192 | - when(groupMemberService.getGroupMembersByUserId(1L)).thenReturn(List.of(member)); | ||
| 193 | - | ||
| 194 | - // 退群前后各一条消息 | ||
| 195 | - ImGroupMessageDO beforeQuit = ImGroupMessageDO.builder() | ||
| 196 | - .id(1L).groupId(10L).senderId(2L) | ||
| 197 | - .sendTime(LocalDateTime.of(2026, 4, 11, 10, 0, 0)).build(); | ||
| 198 | - ImGroupMessageDO afterQuit = ImGroupMessageDO.builder() | ||
| 199 | - .id(2L).groupId(10L).senderId(2L) | ||
| 200 | - .sendTime(LocalDateTime.of(2026, 4, 13, 10, 0, 0)).build(); | ||
| 201 | - when(groupMessageMapper.selectListByMinId(List.of(10L), 0L, 100)) | ||
| 202 | - .thenReturn(List.of(beforeQuit, afterQuit)); | 209 | + .joinTime(now.minusDays(20)) |
| 210 | + .quitTime(quitTime).build(); | ||
| 211 | + when(groupMemberService.getQuitGroupMemberListByUserId(eq(1L), any(LocalDateTime.class))) | ||
| 212 | + .thenReturn(List.of(quitMember)); | ||
| 213 | + | ||
| 214 | + ImGroupMessageDO quitGroupMsg = ImGroupMessageDO.builder() | ||
| 215 | + .id(3L).groupId(20L).senderId(99L) | ||
| 216 | + .sendTime(now.minusDays(5)).build(); | ||
| 217 | + when(groupMessageMapper.selectListByGroupIdAndMinIdAndQuitTimeBefore(eq(20L), eq(0L), | ||
| 218 | + any(LocalDateTime.class), eq(quitTime), eq(100))) | ||
| 219 | + .thenReturn(List.of(quitGroupMsg)); | ||
| 203 | 220 | ||
| 204 | // 调用 | 221 | // 调用 |
| 205 | List<ImGroupMessageDO> result = groupMessageService.pullGroupMessageList(1L, 0L, 100); | 222 | List<ImGroupMessageDO> result = groupMessageService.pullGroupMessageList(1L, 0L, 100); |
| 206 | 223 | ||
| 207 | - // 断言:退群后消息不可见 | ||
| 208 | - assertEquals(1, result.size()); | ||
| 209 | - assertEquals(1L, result.get(0).getId()); | 224 | + // 断言:两条消息均返回,并按 id 升序 |
| 225 | + assertEquals(2, result.size()); | ||
| 226 | + assertEquals(3L, result.get(0).getId()); | ||
| 227 | + assertEquals(5L, result.get(1).getId()); | ||
| 228 | + } | ||
| 229 | + | ||
| 230 | + @Test | ||
| 231 | + public void testPullMessages_quitMemberFilteredByBoundarySendTime() { | ||
| 232 | + // 准备:minId > 0,边界消息 sendTime 晚于窗口起点,应当被用作 minQuitTime | ||
| 233 | + LocalDateTime now = LocalDateTime.now(); | ||
| 234 | + when(groupMemberService.getActiveGroupMemberListByUserId(1L)).thenReturn(List.of()); | ||
| 235 | + when(groupMessageMapper.selectListByMinId(anyList(), anyLong(), any(LocalDateTime.class), anyInt())) | ||
| 236 | + .thenReturn(List.of()); | ||
| 237 | + | ||
| 238 | + LocalDateTime boundarySendTime = now.minusDays(2); | ||
| 239 | + ImGroupMessageDO boundary = ImGroupMessageDO.builder() | ||
| 240 | + .id(8L).groupId(10L).senderId(2L).sendTime(boundarySendTime).build(); | ||
| 241 | + when(groupMessageMapper.selectById(8L)).thenReturn(boundary); | ||
| 242 | + when(groupMemberService.getQuitGroupMemberListByUserId(1L, boundarySendTime)) | ||
| 243 | + .thenReturn(List.of()); | ||
| 244 | + | ||
| 245 | + // 调用 | ||
| 246 | + List<ImGroupMessageDO> result = groupMessageService.pullGroupMessageList(1L, 8L, 100); | ||
| 247 | + | ||
| 248 | + // 断言:使用边界消息 sendTime 抬升 minQuitTime | ||
| 249 | + assertEquals(0, result.size()); | ||
| 250 | + verify(groupMemberService).getQuitGroupMemberListByUserId(1L, boundarySendTime); | ||
| 210 | } | 251 | } |
| 211 | 252 | ||
| 212 | @Test | 253 | @Test |
| 213 | public void testPullMessages_receiverUserIdsFilter() { | 254 | public void testPullMessages_receiverUserIdsFilter() { |
| 214 | // 准备 | 255 | // 准备 |
| 256 | + LocalDateTime now = LocalDateTime.now(); | ||
| 215 | ImGroupMemberDO member = ImGroupMemberDO.builder() | 257 | ImGroupMemberDO member = ImGroupMemberDO.builder() |
| 216 | .groupId(10L).userId(1L) | 258 | .groupId(10L).userId(1L) |
| 217 | .status(CommonStatusEnum.ENABLE.getStatus()) | 259 | .status(CommonStatusEnum.ENABLE.getStatus()) |
| 218 | - .joinTime(LocalDateTime.of(2026, 1, 1, 0, 0, 0)).build(); | ||
| 219 | - when(groupMemberService.getGroupMembersByUserId(1L)).thenReturn(List.of(member)); | 260 | + .joinTime(now.minusDays(10)).build(); |
| 261 | + when(groupMemberService.getActiveGroupMemberListByUserId(1L)).thenReturn(List.of(member)); | ||
| 220 | 262 | ||
| 221 | // 定向接收消息:只给用户 2 和 3 | 263 | // 定向接收消息:只给用户 2 和 3 |
| 222 | ImGroupMessageDO directedMsg = ImGroupMessageDO.builder() | 264 | ImGroupMessageDO directedMsg = ImGroupMessageDO.builder() |
| 223 | .id(1L).groupId(10L).senderId(5L) | 265 | .id(1L).groupId(10L).senderId(5L) |
| 224 | .receiverUserIds(List.of(2L, 3L)) | 266 | .receiverUserIds(List.of(2L, 3L)) |
| 225 | - .sendTime(LocalDateTime.of(2026, 4, 12, 10, 0, 0)).build(); | 267 | + .sendTime(now.minusHours(2)).build(); |
| 226 | // 全员消息 | 268 | // 全员消息 |
| 227 | ImGroupMessageDO allMsg = ImGroupMessageDO.builder() | 269 | ImGroupMessageDO allMsg = ImGroupMessageDO.builder() |
| 228 | .id(2L).groupId(10L).senderId(5L) | 270 | .id(2L).groupId(10L).senderId(5L) |
| 229 | - .sendTime(LocalDateTime.of(2026, 4, 12, 10, 0, 0)).build(); | ||
| 230 | - when(groupMessageMapper.selectListByMinId(List.of(10L), 0L, 100)) | 271 | + .sendTime(now.minusHours(1)).build(); |
| 272 | + when(groupMessageMapper.selectListByMinId(eq(List.of(10L)), eq(0L), | ||
| 273 | + any(LocalDateTime.class), eq(100))) | ||
| 231 | .thenReturn(List.of(directedMsg, allMsg)); | 274 | .thenReturn(List.of(directedMsg, allMsg)); |
| 275 | + when(groupMemberService.getQuitGroupMemberListByUserId(eq(1L), any(LocalDateTime.class))) | ||
| 276 | + .thenReturn(List.of()); | ||
| 232 | 277 | ||
| 233 | // 调用 | 278 | // 调用 |
| 234 | List<ImGroupMessageDO> result = groupMessageService.pullGroupMessageList(1L, 0L, 100); | 279 | List<ImGroupMessageDO> result = groupMessageService.pullGroupMessageList(1L, 0L, 100); |
| @@ -241,18 +286,22 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { | @@ -241,18 +286,22 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { | ||
| 241 | @Test | 286 | @Test |
| 242 | public void testPullMessages_atFieldCorrectReturn() { | 287 | public void testPullMessages_atFieldCorrectReturn() { |
| 243 | // 准备 | 288 | // 准备 |
| 289 | + LocalDateTime now = LocalDateTime.now(); | ||
| 244 | ImGroupMemberDO member = ImGroupMemberDO.builder() | 290 | ImGroupMemberDO member = ImGroupMemberDO.builder() |
| 245 | .groupId(10L).userId(1L) | 291 | .groupId(10L).userId(1L) |
| 246 | .status(CommonStatusEnum.ENABLE.getStatus()) | 292 | .status(CommonStatusEnum.ENABLE.getStatus()) |
| 247 | - .joinTime(LocalDateTime.of(2026, 1, 1, 0, 0, 0)).build(); | ||
| 248 | - when(groupMemberService.getGroupMembersByUserId(1L)).thenReturn(List.of(member)); | 293 | + .joinTime(now.minusDays(10)).build(); |
| 294 | + when(groupMemberService.getActiveGroupMemberListByUserId(1L)).thenReturn(List.of(member)); | ||
| 249 | 295 | ||
| 250 | ImGroupMessageDO atMsg = ImGroupMessageDO.builder() | 296 | ImGroupMessageDO atMsg = ImGroupMessageDO.builder() |
| 251 | .id(1L).groupId(10L).senderId(2L) | 297 | .id(1L).groupId(10L).senderId(2L) |
| 252 | .atUserIds(List.of(1L, 3L)) | 298 | .atUserIds(List.of(1L, 3L)) |
| 253 | - .sendTime(LocalDateTime.of(2026, 4, 12, 10, 0, 0)).build(); | ||
| 254 | - when(groupMessageMapper.selectListByMinId(List.of(10L), 0L, 100)) | 299 | + .sendTime(now.minusHours(1)).build(); |
| 300 | + when(groupMessageMapper.selectListByMinId(eq(List.of(10L)), eq(0L), | ||
| 301 | + any(LocalDateTime.class), eq(100))) | ||
| 255 | .thenReturn(List.of(atMsg)); | 302 | .thenReturn(List.of(atMsg)); |
| 303 | + when(groupMemberService.getQuitGroupMemberListByUserId(eq(1L), any(LocalDateTime.class))) | ||
| 304 | + .thenReturn(List.of()); | ||
| 256 | 305 | ||
| 257 | // 调用 | 306 | // 调用 |
| 258 | List<ImGroupMessageDO> result = groupMessageService.pullGroupMessageList(1L, 0L, 100); | 307 | List<ImGroupMessageDO> result = groupMessageService.pullGroupMessageList(1L, 0L, 100); |
| @@ -290,7 +339,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { | @@ -290,7 +339,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { | ||
| 290 | ImGroupMemberDO.builder().groupId(10L).userId(3L) | 339 | ImGroupMemberDO.builder().groupId(10L).userId(3L) |
| 291 | .status(CommonStatusEnum.DISABLE.getStatus()).build() | 340 | .status(CommonStatusEnum.DISABLE.getStatus()).build() |
| 292 | ); | 341 | ); |
| 293 | - when(groupMemberService.selectByGroupId(10L)).thenReturn(members); | 342 | + when(groupMemberService.getActiveGroupMemberListByGroupId(10L)).thenReturn(members); |
| 294 | 343 | ||
| 295 | // 调用 | 344 | // 调用 |
| 296 | ImGroupMessageDO result = groupMessageService.recallGroupMessage(1L, 50L); | 345 | ImGroupMessageDO result = groupMessageService.recallGroupMessage(1L, 50L); |
| @@ -396,7 +445,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { | @@ -396,7 +445,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { | ||
| 396 | .status(CommonStatusEnum.ENABLE.getStatus()) | 445 | .status(CommonStatusEnum.ENABLE.getStatus()) |
| 397 | .joinTime(LocalDateTime.of(2026, 1, 1, 0, 0, 0)).build() // 发送者 | 446 | .joinTime(LocalDateTime.of(2026, 1, 1, 0, 0, 0)).build() // 发送者 |
| 398 | ); | 447 | ); |
| 399 | - when(groupMemberService.selectByGroupId(10L)).thenReturn(allMembers); | 448 | + when(groupMemberService.getGroupMemberListByGroupId(10L)).thenReturn(allMembers); |
| 400 | 449 | ||
| 401 | // 准备:Redis 已读位置 — 用户 1 读到 100, 用户 2 读到 50, 用户 3 读到 200 | 450 | // 准备:Redis 已读位置 — 用户 1 读到 100, 用户 2 读到 50, 用户 3 读到 200 |
| 402 | Map<Long, Long> positions = new HashMap<>(); | 451 | Map<Long, Long> positions = new HashMap<>(); |
yudao-module-im/yudao-module-im-biz/src/test/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageServiceImplTest.java
| @@ -132,7 +132,8 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest { | @@ -132,7 +132,8 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest { | ||
| 132 | ImPrivateMessageDO.builder().id(1L).senderId(1L).receiverId(2L).build(), | 132 | ImPrivateMessageDO.builder().id(1L).senderId(1L).receiverId(2L).build(), |
| 133 | ImPrivateMessageDO.builder().id(2L).senderId(2L).receiverId(1L).build() | 133 | ImPrivateMessageDO.builder().id(2L).senderId(2L).receiverId(1L).build() |
| 134 | ); | 134 | ); |
| 135 | - when(imPrivateMessageMapper.selectListByMinId(1L, 0L, 100)).thenReturn(mockMessages); | 135 | + when(imPrivateMessageMapper.selectListByMinId(eq(1L), eq(0L), any(LocalDateTime.class), eq(100))) |
| 136 | + .thenReturn(mockMessages); | ||
| 136 | 137 | ||
| 137 | // 调用 | 138 | // 调用 |
| 138 | List<ImPrivateMessageDO> result = privateMessageService.pullPrivateMessageList(1L, 0L, 100); | 139 | List<ImPrivateMessageDO> result = privateMessageService.pullPrivateMessageList(1L, 0L, 100); |