Commit bb9a2b5382ae65e3bc92a7e4d93f620c24975b99
1 parent
513c1301
fix(im):加固好友、群成员与群消息边界流程
- 清理好友重加时的 deleteTime 和历史备注残留 - 清理群成员重入时的 quitTime、muteEndTime 和邀请来源残留 - 允许封禁群成员主动退群,仍拦截已解散群 - 校验群已读游标的消息归属和可见性 - 收窄群消息置顶通知为专用展示对象并同步前端展示 - 回填群成员单查接口的昵称和头像 - 补充相关回归测试并流转 IM bug 状态文档
Showing
13 changed files
with
199 additions
and
68 deletions
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupMemberController.java
| @@ -79,6 +79,10 @@ public class ImGroupMemberController { | @@ -79,6 +79,10 @@ public class ImGroupMemberController { | ||
| 79 | 79 | ||
| 80 | // 3. 转化 VO | 80 | // 3. 转化 VO |
| 81 | ImGroupMemberRespVO memberVO = BeanUtils.toBean(member, ImGroupMemberRespVO.class); | 81 | ImGroupMemberRespVO memberVO = BeanUtils.toBean(member, ImGroupMemberRespVO.class); |
| 82 | + AdminUserRespDTO user = adminUserApi.getUser(member.getUserId()); | ||
| 83 | + if (user != null) { | ||
| 84 | + memberVO.setNickname(user.getNickname()).setAvatar(user.getAvatar()); | ||
| 85 | + } | ||
| 82 | hidePrivateFieldsIfNotSelf(memberVO, member.getUserId(), loginUserId); | 86 | hidePrivateFieldsIfNotSelf(memberVO, member.getUserId(), loginUserId); |
| 83 | return success(memberVO); | 87 | return success(memberVO); |
| 84 | } | 88 | } |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/friend/ImFriendMapper.java
| @@ -5,8 +5,10 @@ import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; | @@ -5,8 +5,10 @@ import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; | ||
| 5 | import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; | 5 | import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; |
| 6 | import cn.iocoder.yudao.module.im.controller.admin.manager.friend.vo.ImFriendManagerPageReqVO; | 6 | import cn.iocoder.yudao.module.im.controller.admin.manager.friend.vo.ImFriendManagerPageReqVO; |
| 7 | import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO; | 7 | import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO; |
| 8 | +import com.baomidou.mybatisplus.core.toolkit.Wrappers; | ||
| 8 | import org.apache.ibatis.annotations.Mapper; | 9 | import org.apache.ibatis.annotations.Mapper; |
| 9 | 10 | ||
| 11 | +import java.time.LocalDateTime; | ||
| 10 | import java.util.Collection; | 12 | import java.util.Collection; |
| 11 | import java.util.List; | 13 | import java.util.List; |
| 12 | 14 | ||
| @@ -55,4 +57,20 @@ public interface ImFriendMapper extends BaseMapperX<ImFriendDO> { | @@ -55,4 +57,20 @@ public interface ImFriendMapper extends BaseMapperX<ImFriendDO> { | ||
| 55 | .orderByDesc(ImFriendDO::getId)); | 57 | .orderByDesc(ImFriendDO::getId)); |
| 56 | } | 58 | } |
| 57 | 59 | ||
| 60 | + @SuppressWarnings("UnusedReturnValue") | ||
| 61 | + default int updateReAddFields(Long id, Integer status, LocalDateTime addTime, | ||
| 62 | + Boolean silent, Boolean pinned, Boolean blocked, | ||
| 63 | + String displayName, Integer addSource) { | ||
| 64 | + return update(null, Wrappers.<ImFriendDO>lambdaUpdate() | ||
| 65 | + .eq(ImFriendDO::getId, id) | ||
| 66 | + .set(ImFriendDO::getStatus, status) | ||
| 67 | + .set(ImFriendDO::getAddTime, addTime) | ||
| 68 | + .set(ImFriendDO::getSilent, silent) | ||
| 69 | + .set(ImFriendDO::getPinned, pinned) | ||
| 70 | + .set(ImFriendDO::getBlocked, blocked) | ||
| 71 | + .set(ImFriendDO::getDisplayName, displayName) | ||
| 72 | + .set(ImFriendDO::getAddSource, addSource) | ||
| 73 | + .set(ImFriendDO::getDeleteTime, null)); | ||
| 74 | + } | ||
| 75 | + | ||
| 58 | } | 76 | } |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/group/ImGroupMemberMapper.java
| @@ -124,4 +124,18 @@ public interface ImGroupMemberMapper extends BaseMapperX<ImGroupMemberDO> { | @@ -124,4 +124,18 @@ public interface ImGroupMemberMapper extends BaseMapperX<ImGroupMemberDO> { | ||
| 124 | .set(ImGroupMemberDO::getMuteEndTime, null)); | 124 | .set(ImGroupMemberDO::getMuteEndTime, null)); |
| 125 | } | 125 | } |
| 126 | 126 | ||
| 127 | -} | ||
| 128 | \ No newline at end of file | 127 | \ No newline at end of file |
| 128 | + @SuppressWarnings("UnusedReturnValue") | ||
| 129 | + default int updateRejoinFields(Long id, Integer status, LocalDateTime joinTime, | ||
| 130 | + Integer role, Integer addSource, Long inviterUserId) { | ||
| 131 | + return update(null, Wrappers.<ImGroupMemberDO>lambdaUpdate() | ||
| 132 | + .eq(ImGroupMemberDO::getId, id) | ||
| 133 | + .set(ImGroupMemberDO::getStatus, status) | ||
| 134 | + .set(ImGroupMemberDO::getJoinTime, joinTime) | ||
| 135 | + .set(ImGroupMemberDO::getRole, role) | ||
| 136 | + .set(ImGroupMemberDO::getAddSource, addSource) | ||
| 137 | + .set(ImGroupMemberDO::getInviterUserId, inviterUserId) | ||
| 138 | + .set(ImGroupMemberDO::getQuitTime, null) | ||
| 139 | + .set(ImGroupMemberDO::getMuteEndTime, null)); | ||
| 140 | + } | ||
| 141 | + | ||
| 142 | +} |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImpl.java
| @@ -266,16 +266,8 @@ public class ImFriendServiceImpl implements ImFriendService { | @@ -266,16 +266,8 @@ public class ImFriendServiceImpl implements ImFriendService { | ||
| 266 | } | 266 | } |
| 267 | // 情况二:复用 DISABLE 旧记录 → 恢复 ENABLE + 重置 silent / pinned / blocked,对齐"重新加好友"语义 | 267 | // 情况二:复用 DISABLE 旧记录 → 恢复 ENABLE + 重置 silent / pinned / blocked,对齐"重新加好友"语义 |
| 268 | if (exists != null) { | 268 | if (exists != null) { |
| 269 | - ImFriendDO update = new ImFriendDO().setId(exists.getId()) | ||
| 270 | - .setStatus(CommonStatusEnum.ENABLE.getStatus()).setAddTime(LocalDateTime.now()) | ||
| 271 | - .setSilent(false).setPinned(false).setBlocked(false); | ||
| 272 | - if (displayName != null) { | ||
| 273 | - update.setDisplayName(displayName); | ||
| 274 | - } | ||
| 275 | - if (addSource != null) { | ||
| 276 | - update.setAddSource(addSource); | ||
| 277 | - } | ||
| 278 | - friendMapper.updateById(update); | 269 | + friendMapper.updateReAddFields(exists.getId(), CommonStatusEnum.ENABLE.getStatus(), LocalDateTime.now(), |
| 270 | + false, false, false, displayName, addSource); | ||
| 279 | return; | 271 | return; |
| 280 | } | 272 | } |
| 281 | // 情况三:不存在记录 → 直接插入新记录 | 273 | // 情况三:不存在记录 → 直接插入新记录 |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberServiceImpl.java
| @@ -128,7 +128,7 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { | @@ -128,7 +128,7 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { | ||
| 128 | * 并发安全:依靠 im_group_member 表的唯一索引 uk_im_group_member_group_user(group_id, user_id) 保证幂等, | 128 | * 并发安全:依靠 im_group_member 表的唯一索引 uk_im_group_member_group_user(group_id, user_id) 保证幂等, |
| 129 | * 当并发 insert 触发 {@link DuplicateKeyException} 时降级为 select + update。 | 129 | * 当并发 insert 触发 {@link DuplicateKeyException} 时降级为 select + update。 |
| 130 | * <p> | 130 | * <p> |
| 131 | - * 重置旧成员行时强制重置 role / addSource / inviterUserId,确保留痕反映「本次入群」事件 | 131 | + * 重置旧成员行时强制重置 role / addSource / inviterUserId / quitTime / muteEndTime |
| 132 | */ | 132 | */ |
| 133 | @Override | 133 | @Override |
| 134 | @CacheEvict(cacheNames = GROUP_MEMBER_IDS, key = "#groupId") | 134 | @CacheEvict(cacheNames = GROUP_MEMBER_IDS, key = "#groupId") |
| @@ -139,11 +139,11 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { | @@ -139,11 +139,11 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { | ||
| 139 | ImGroupMemberDO exists = groupMemberMapper.selectByGroupIdAndUserId(groupId, userId); | 139 | ImGroupMemberDO exists = groupMemberMapper.selectByGroupIdAndUserId(groupId, userId); |
| 140 | if (exists != null) { | 140 | if (exists != null) { |
| 141 | if (CommonStatusEnum.isDisable(exists.getStatus())) { | 141 | if (CommonStatusEnum.isDisable(exists.getStatus())) { |
| 142 | - groupMemberMapper.updateById(new ImGroupMemberDO().setId(exists.getId()) | ||
| 143 | - .setStatus(CommonStatusEnum.ENABLE.getStatus()).setJoinTime(now) | ||
| 144 | - .setRole(role).setAddSource(addSource).setInviterUserId(inviterUserId)); | 142 | + groupMemberMapper.updateRejoinFields(exists.getId(), CommonStatusEnum.ENABLE.getStatus(), now, |
| 143 | + role, addSource, inviterUserId); | ||
| 145 | exists.setStatus(CommonStatusEnum.ENABLE.getStatus()).setJoinTime(now).setRole(role) | 144 | exists.setStatus(CommonStatusEnum.ENABLE.getStatus()).setJoinTime(now).setRole(role) |
| 146 | - .setAddSource(addSource).setInviterUserId(inviterUserId); | 145 | + .setAddSource(addSource).setInviterUserId(inviterUserId) |
| 146 | + .setQuitTime(null).setMuteEndTime(null); | ||
| 147 | } | 147 | } |
| 148 | return exists; | 148 | return exists; |
| 149 | } | 149 | } |
| @@ -193,7 +193,10 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { | @@ -193,7 +193,10 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { | ||
| 193 | 193 | ||
| 194 | // 2.1 先做 update,update 没有并发冲突风险 | 194 | // 2.1 先做 update,update 没有并发冲突风险 |
| 195 | if (CollUtil.isNotEmpty(updates)) { | 195 | if (CollUtil.isNotEmpty(updates)) { |
| 196 | - groupMemberMapper.updateBatch(updates); | 196 | + for (ImGroupMemberDO update : updates) { |
| 197 | + groupMemberMapper.updateRejoinFields(update.getId(), update.getStatus(), update.getJoinTime(), | ||
| 198 | + update.getRole(), update.getAddSource(), update.getInviterUserId()); | ||
| 199 | + } | ||
| 197 | } | 200 | } |
| 198 | // 2.2 批量 insert。并发场景下若其它请求已先一步插入同一 (groupId, userId), | 201 | // 2.2 批量 insert。并发场景下若其它请求已先一步插入同一 (groupId, userId), |
| 199 | // 会触发唯一索引冲突,此时降级为逐个 addGroupMember(利用其兜底逻辑幂等处理)。 | 202 | // 会触发唯一索引冲突,此时降级为逐个 addGroupMember(利用其兜底逻辑幂等处理)。 |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImpl.java
| @@ -241,8 +241,14 @@ public class ImGroupServiceImpl implements ImGroupService { | @@ -241,8 +241,14 @@ public class ImGroupServiceImpl implements ImGroupService { | ||
| 241 | @Override | 241 | @Override |
| 242 | @Transactional(rollbackFor = Exception.class) | 242 | @Transactional(rollbackFor = Exception.class) |
| 243 | public void quitGroup(Long groupId, Long userId) { | 243 | public void quitGroup(Long groupId, Long userId) { |
| 244 | - // 1.1 校验群存在 | ||
| 245 | - ImGroupDO group = validateGroupExists(groupId); | 244 | + // 1.1 校验群存在且未解散 |
| 245 | + ImGroupDO group = getSelf().getGroup(groupId); | ||
| 246 | + if (group == null) { | ||
| 247 | + throw exception(GROUP_NOT_EXISTS); | ||
| 248 | + } | ||
| 249 | + if (CommonStatusEnum.DISABLE.getStatus().equals(group.getStatus())) { | ||
| 250 | + throw exception(GROUP_DISSOLVED); | ||
| 251 | + } | ||
| 246 | // 1.2 群主不可退群 | 252 | // 1.2 群主不可退群 |
| 247 | if (ObjUtil.equal(group.getOwnerUserId(), userId)) { | 253 | if (ObjUtil.equal(group.getOwnerUserId(), userId)) { |
| 248 | throw exception(GROUP_OWNER_CANNOT_QUIT); | 254 | throw exception(GROUP_OWNER_CANNOT_QUIT); |
| @@ -433,7 +439,7 @@ public class ImGroupServiceImpl implements ImGroupService { | @@ -433,7 +439,7 @@ public class ImGroupServiceImpl implements ImGroupService { | ||
| 433 | pinned.add(messageId); | 439 | pinned.add(messageId); |
| 434 | groupMapper.updateById(new ImGroupDO().setId(groupId).setPinnedMessageIds(pinned)); | 440 | groupMapper.updateById(new ImGroupDO().setId(groupId).setPinnedMessageIds(pinned)); |
| 435 | 441 | ||
| 436 | - // 3. 推送 GROUP_MESSAGE_PIN 通知给全员;payload 直接带消息对象,前端不用回查群详情绕开 @CacheEvict 时序 | 442 | + // 3. 推送 GROUP_MESSAGE_PIN 通知给全员 |
| 437 | groupMessageService.sendGroupMessage(userId, | 443 | groupMessageService.sendGroupMessage(userId, |
| 438 | ImGroupMessageSendDTO.ofGroupMessagePin(groupId, userId, message)); | 444 | ImGroupMessageSendDTO.ofGroupMessagePin(groupId, userId, message)); |
| 439 | } | 445 | } |
| @@ -694,4 +700,4 @@ public class ImGroupServiceImpl implements ImGroupService { | @@ -694,4 +700,4 @@ public class ImGroupServiceImpl implements ImGroupService { | ||
| 694 | .collect(Collectors.joining(",")); | 700 | .collect(Collectors.joining(",")); |
| 695 | } | 701 | } |
| 696 | 702 | ||
| 697 | -} | ||
| 698 | \ No newline at end of file | 703 | \ No newline at end of file |
| 704 | +} |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImpl.java
| @@ -354,8 +354,15 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { | @@ -354,8 +354,15 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { | ||
| 354 | throw exception(MESSAGE_GROUP_READ_DISABLED); | 354 | throw exception(MESSAGE_GROUP_READ_DISABLED); |
| 355 | } | 355 | } |
| 356 | Assert.notNull(messageId, "已读消息编号不能为空"); | 356 | Assert.notNull(messageId, "已读消息编号不能为空"); |
| 357 | - // 1. 校验用户在群中(权限校验) | ||
| 358 | - groupMemberService.validateMemberInGroup(groupId, userId); | 357 | + // 1.1 校验用户在群中(权限校验) |
| 358 | + ImGroupMemberDO member = groupMemberService.validateMemberInGroup(groupId, userId); | ||
| 359 | + // 1.2 校验消息属于当前群,且对当前用户可见 | ||
| 360 | + ImGroupMessageDO message = groupMessageMapper.selectById(messageId); | ||
| 361 | + if (message == null | ||
| 362 | + || ObjUtil.notEqual(message.getGroupId(), groupId) | ||
| 363 | + || !isMessageVisible(message, member, userId)) { | ||
| 364 | + throw exception(MESSAGE_NOT_IN_GROUP); | ||
| 365 | + } | ||
| 359 | 366 | ||
| 360 | // 2. 已读位置未前进,直接返回 | 367 | // 2. 已读位置未前进,直接返回 |
| 361 | Long prevMaxMessageId = groupMessageReadRedisDAO.getReadMaxMessageId(groupId, userId); | 368 | Long prevMaxMessageId = groupMessageReadRedisDAO.getReadMaxMessageId(groupId, userId); |
| @@ -664,4 +671,4 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { | @@ -664,4 +671,4 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { | ||
| 664 | } | 671 | } |
| 665 | } | 672 | } |
| 666 | 673 | ||
| 667 | -} | ||
| 668 | \ No newline at end of file | 674 | \ No newline at end of file |
| 675 | +} |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/dto/ImGroupMessageSendDTO.java
| 1 | package cn.iocoder.yudao.module.im.service.message.dto; | 1 | package cn.iocoder.yudao.module.im.service.message.dto; |
| 2 | 2 | ||
| 3 | +import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO; | ||
| 3 | import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; | 4 | import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; |
| 4 | import cn.iocoder.yudao.module.im.service.websocket.dto.notification.group.*; | 5 | import cn.iocoder.yudao.module.im.service.websocket.dto.notification.group.*; |
| 5 | import jakarta.validation.constraints.NotNull; | 6 | import jakarta.validation.constraints.NotNull; |
| @@ -166,11 +167,14 @@ public class ImGroupMessageSendDTO { | @@ -166,11 +167,14 @@ public class ImGroupMessageSendDTO { | ||
| 166 | .setType(ImMessageTypeEnum.GROUP_MEMBER_SETTING_UPDATE.getType()).setContent(notification); | 167 | .setType(ImMessageTypeEnum.GROUP_MEMBER_SETTING_UPDATE.getType()).setContent(notification); |
| 167 | } | 168 | } |
| 168 | 169 | ||
| 169 | - public static ImGroupMessageSendDTO ofGroupMessagePin(Long groupId, Long operatorUserId, | ||
| 170 | - cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO message) { | 170 | + public static ImGroupMessageSendDTO ofGroupMessagePin(Long groupId, Long operatorUserId, ImGroupMessageDO message) { |
| 171 | GroupMessagePinNotification notification = new GroupMessagePinNotification(); | 171 | GroupMessagePinNotification notification = new GroupMessagePinNotification(); |
| 172 | + GroupMessagePinNotification.PinnedMessage pinnedMessage = new GroupMessagePinNotification.PinnedMessage() | ||
| 173 | + .setId(message.getId()).setSenderId(message.getSenderId()).setGroupId(message.getGroupId()) | ||
| 174 | + .setType(message.getType()).setContent(message.getContent()).setSendTime(message.getSendTime()) | ||
| 175 | + .setAtUserIds(message.getAtUserIds()).setReceiverUserIds(message.getReceiverUserIds()); | ||
| 172 | notification.setOperatorUserId(operatorUserId); | 176 | notification.setOperatorUserId(operatorUserId); |
| 173 | - notification.setMessageId(message.getId()).setMessage(message); | 177 | + notification.setMessageId(message.getId()).setMessage(pinnedMessage); |
| 174 | return new ImGroupMessageSendDTO().setGroupId(groupId) | 178 | return new ImGroupMessageSendDTO().setGroupId(groupId) |
| 175 | .setType(ImMessageTypeEnum.GROUP_MESSAGE_PIN.getType()).setContent(notification); | 179 | .setType(ImMessageTypeEnum.GROUP_MESSAGE_PIN.getType()).setContent(notification); |
| 176 | } | 180 | } |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/group/GroupMessagePinNotification.java
| 1 | package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group; | 1 | package cn.iocoder.yudao.module.im.service.websocket.dto.notification.group; |
| 2 | 2 | ||
| 3 | -import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO; | ||
| 4 | import lombok.Data; | 3 | import lombok.Data; |
| 5 | 4 | ||
| 5 | +import java.time.LocalDateTime; | ||
| 6 | +import java.util.List; | ||
| 7 | + | ||
| 6 | /** | 8 | /** |
| 7 | * 群消息置顶事件通知 | 9 | * 群消息置顶事件通知 |
| 8 | - * <p> | ||
| 9 | - * payload 直接携带被置顶的完整消息对象,前端按 message 直接 push 到 pinnedMessages,避免 GET /im/group/get 命中 @CacheEvict 触发前的旧 cache | ||
| 10 | */ | 10 | */ |
| 11 | @Data | 11 | @Data |
| 12 | public class GroupMessagePinNotification extends BaseGroupNotification { | 12 | public class GroupMessagePinNotification extends BaseGroupNotification { |
| @@ -16,8 +16,49 @@ public class GroupMessagePinNotification extends BaseGroupNotification { | @@ -16,8 +16,49 @@ public class GroupMessagePinNotification extends BaseGroupNotification { | ||
| 16 | */ | 16 | */ |
| 17 | private Long messageId; | 17 | private Long messageId; |
| 18 | /** | 18 | /** |
| 19 | - * 被置顶的完整消息对象(避免前端回查群详情) | 19 | + * 被置顶的消息展示数据 |
| 20 | + */ | ||
| 21 | + private PinnedMessage message; | ||
| 22 | + | ||
| 23 | + /** | ||
| 24 | + * 被置顶的消息展示数据 | ||
| 20 | */ | 25 | */ |
| 21 | - private ImGroupMessageDO message; | 26 | + @Data |
| 27 | + public static class PinnedMessage { | ||
| 28 | + | ||
| 29 | + /** | ||
| 30 | + * 消息编号 | ||
| 31 | + */ | ||
| 32 | + private Long id; | ||
| 33 | + /** | ||
| 34 | + * 发送人编号 | ||
| 35 | + */ | ||
| 36 | + private Long senderId; | ||
| 37 | + /** | ||
| 38 | + * 群编号 | ||
| 39 | + */ | ||
| 40 | + private Long groupId; | ||
| 41 | + /** | ||
| 42 | + * 消息类型 | ||
| 43 | + */ | ||
| 44 | + private Integer type; | ||
| 45 | + /** | ||
| 46 | + * 消息内容 | ||
| 47 | + */ | ||
| 48 | + private String content; | ||
| 49 | + /** | ||
| 50 | + * 发送时间 | ||
| 51 | + */ | ||
| 52 | + private LocalDateTime sendTime; | ||
| 53 | + /** | ||
| 54 | + * @ 目标用户编号列表 | ||
| 55 | + */ | ||
| 56 | + private List<Long> atUserIds; | ||
| 57 | + /** | ||
| 58 | + * 定向接收用户编号列表 | ||
| 59 | + */ | ||
| 60 | + private List<Long> receiverUserIds; | ||
| 61 | + | ||
| 62 | + } | ||
| 22 | 63 | ||
| 23 | } | 64 | } |
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImplTest.java
| @@ -21,9 +21,7 @@ import java.util.List; | @@ -21,9 +21,7 @@ import java.util.List; | ||
| 21 | 21 | ||
| 22 | import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.FRIEND_NOT_FRIEND; | 22 | import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.FRIEND_NOT_FRIEND; |
| 23 | import static org.junit.jupiter.api.Assertions.*; | 23 | import static org.junit.jupiter.api.Assertions.*; |
| 24 | -import static org.mockito.ArgumentMatchers.any; | ||
| 25 | -import static org.mockito.ArgumentMatchers.anyLong; | ||
| 26 | -import static org.mockito.ArgumentMatchers.eq; | 24 | +import static org.mockito.ArgumentMatchers.*; |
| 27 | import static org.mockito.Mockito.never; | 25 | import static org.mockito.Mockito.never; |
| 28 | import static org.mockito.Mockito.verify; | 26 | import static org.mockito.Mockito.verify; |
| 29 | import static org.mockito.Mockito.when; | 27 | import static org.mockito.Mockito.when; |
| @@ -126,8 +124,8 @@ public class ImFriendServiceImplTest extends BaseMockitoUnitTest { | @@ -126,8 +124,8 @@ public class ImFriendServiceImplTest extends BaseMockitoUnitTest { | ||
| 126 | // ========== addFriend0(内部方法,被 becomeFriends / silentReAddFriend 调用) ========== | 124 | // ========== addFriend0(内部方法,被 becomeFriends / silentReAddFriend 调用) ========== |
| 127 | 125 | ||
| 128 | @Test | 126 | @Test |
| 129 | - public void testAddFriend0_existingEnabledResetsFields() { | ||
| 130 | - // 准备:已存在且启用 —— 走复用旧记录路径,重置 silent / pinned / blocked | 127 | + public void testAddFriend0_existingEnabledSkip() { |
| 128 | + // 准备:已存在且启用 | ||
| 131 | ImFriendDO exists = ImFriendDO.builder().id(10L).userId(1L).friendUserId(2L) | 129 | ImFriendDO exists = ImFriendDO.builder().id(10L).userId(1L).friendUserId(2L) |
| 132 | .status(CommonStatusEnum.ENABLE.getStatus()).silent(true).pinned(true).blocked(true).build(); | 130 | .status(CommonStatusEnum.ENABLE.getStatus()).silent(true).pinned(true).blocked(true).build(); |
| 133 | when(imFriendMapper.selectByUserIdAndFriendUserId(1L, 2L)).thenReturn(exists); | 131 | when(imFriendMapper.selectByUserIdAndFriendUserId(1L, 2L)).thenReturn(exists); |
| @@ -135,16 +133,11 @@ public class ImFriendServiceImplTest extends BaseMockitoUnitTest { | @@ -135,16 +133,11 @@ public class ImFriendServiceImplTest extends BaseMockitoUnitTest { | ||
| 135 | // 调用 | 133 | // 调用 |
| 136 | friendService.addFriend0(1L, 2L, null, null); | 134 | friendService.addFriend0(1L, 2L, null, null); |
| 137 | 135 | ||
| 138 | - // 断言:不插入;走 update 重置 silent / pinned / blocked 为 false,并刷新 addTime | 136 | + // 断言:不插入也不更新 |
| 139 | verify(imFriendMapper, never()).insert(any(ImFriendDO.class)); | 137 | verify(imFriendMapper, never()).insert(any(ImFriendDO.class)); |
| 140 | - ArgumentCaptor<ImFriendDO> captor = ArgumentCaptor.forClass(ImFriendDO.class); | ||
| 141 | - verify(imFriendMapper).updateById(captor.capture()); | ||
| 142 | - assertEquals(10L, captor.getValue().getId()); | ||
| 143 | - assertEquals(CommonStatusEnum.ENABLE.getStatus(), captor.getValue().getStatus()); | ||
| 144 | - assertFalse(captor.getValue().getSilent()); | ||
| 145 | - assertFalse(captor.getValue().getPinned()); | ||
| 146 | - assertFalse(captor.getValue().getBlocked()); | ||
| 147 | - assertNotNull(captor.getValue().getAddTime()); | 138 | + verify(imFriendMapper, never()).updateById(any(ImFriendDO.class)); |
| 139 | + verify(imFriendMapper, never()).updateReAddFields(anyLong(), anyInt(), any(LocalDateTime.class), | ||
| 140 | + anyBoolean(), anyBoolean(), anyBoolean(), any(), any()); | ||
| 148 | } | 141 | } |
| 149 | 142 | ||
| 150 | @Test | 143 | @Test |
| @@ -157,11 +150,9 @@ public class ImFriendServiceImplTest extends BaseMockitoUnitTest { | @@ -157,11 +150,9 @@ public class ImFriendServiceImplTest extends BaseMockitoUnitTest { | ||
| 157 | // 调用 | 150 | // 调用 |
| 158 | friendService.addFriend0(1L, 2L, null, null); | 151 | friendService.addFriend0(1L, 2L, null, null); |
| 159 | 152 | ||
| 160 | - // 断言:更新 status 为 ENABLE | ||
| 161 | - ArgumentCaptor<ImFriendDO> captor = ArgumentCaptor.forClass(ImFriendDO.class); | ||
| 162 | - verify(imFriendMapper).updateById(captor.capture()); | ||
| 163 | - assertEquals(10L, captor.getValue().getId()); | ||
| 164 | - assertEquals(CommonStatusEnum.ENABLE.getStatus(), captor.getValue().getStatus()); | 153 | + // 断言:恢复 ENABLE,并清空 deleteTime |
| 154 | + verify(imFriendMapper).updateReAddFields(eq(10L), eq(CommonStatusEnum.ENABLE.getStatus()), | ||
| 155 | + any(LocalDateTime.class), eq(false), eq(false), eq(false), isNull(), isNull()); | ||
| 165 | verify(imFriendMapper, never()).insert(any(ImFriendDO.class)); | 156 | verify(imFriendMapper, never()).insert(any(ImFriendDO.class)); |
| 166 | } | 157 | } |
| 167 | 158 |
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberServiceImplTest.java
| @@ -82,14 +82,12 @@ public class ImGroupMemberServiceImplTest extends BaseMockitoUnitTest { | @@ -82,14 +82,12 @@ public class ImGroupMemberServiceImplTest extends BaseMockitoUnitTest { | ||
| 82 | 82 | ||
| 83 | ImGroupMemberDO result = groupMemberService.addGroupMember(10L, 1L); | 83 | ImGroupMemberDO result = groupMemberService.addGroupMember(10L, 1L); |
| 84 | 84 | ||
| 85 | - ArgumentCaptor<ImGroupMemberDO> captor = ArgumentCaptor.forClass(ImGroupMemberDO.class); | ||
| 86 | - verify(groupMemberMapper).updateById(captor.capture()); | ||
| 87 | - assertEquals(50L, captor.getValue().getId()); | ||
| 88 | - assertEquals(CommonStatusEnum.ENABLE.getStatus(), captor.getValue().getStatus()); | ||
| 89 | - assertEquals(ImGroupMemberRoleEnum.NORMAL.getRole(), captor.getValue().getRole()); | ||
| 90 | - assertNotNull(captor.getValue().getJoinTime()); | 85 | + verify(groupMemberMapper).updateRejoinFields(eq(50L), eq(CommonStatusEnum.ENABLE.getStatus()), |
| 86 | + any(), eq(ImGroupMemberRoleEnum.NORMAL.getRole()), isNull(), isNull()); | ||
| 91 | assertEquals(CommonStatusEnum.ENABLE.getStatus(), result.getStatus()); | 87 | assertEquals(CommonStatusEnum.ENABLE.getStatus(), result.getStatus()); |
| 92 | assertEquals(ImGroupMemberRoleEnum.NORMAL.getRole(), result.getRole()); | 88 | assertEquals(ImGroupMemberRoleEnum.NORMAL.getRole(), result.getRole()); |
| 89 | + assertNull(result.getQuitTime()); | ||
| 90 | + assertNull(result.getMuteEndTime()); | ||
| 93 | } | 91 | } |
| 94 | 92 | ||
| 95 | @Test | 93 | @Test |
| @@ -124,9 +122,8 @@ public class ImGroupMemberServiceImplTest extends BaseMockitoUnitTest { | @@ -124,9 +122,8 @@ public class ImGroupMemberServiceImplTest extends BaseMockitoUnitTest { | ||
| 124 | groupMemberService.addGroupMembers(10L, List.of(2L, 3L, 4L)); | 122 | groupMemberService.addGroupMembers(10L, List.of(2L, 3L, 4L)); |
| 125 | 123 | ||
| 126 | // 断言:updates 只有用户 3;inserts 只有用户 2 | 124 | // 断言:updates 只有用户 3;inserts 只有用户 2 |
| 127 | - verify(groupMemberMapper).updateBatch(argThat((List<ImGroupMemberDO> list) -> | ||
| 128 | - list.size() == 1 && list.get(0).getId().equals(30L) | ||
| 129 | - && CommonStatusEnum.ENABLE.getStatus().equals(list.get(0).getStatus()))); | 125 | + verify(groupMemberMapper).updateRejoinFields(eq(30L), eq(CommonStatusEnum.ENABLE.getStatus()), |
| 126 | + any(), eq(ImGroupMemberRoleEnum.NORMAL.getRole()), isNull(), isNull()); | ||
| 130 | verify(groupMemberMapper).insertBatch(argThat((List<ImGroupMemberDO> list) -> | 127 | verify(groupMemberMapper).insertBatch(argThat((List<ImGroupMemberDO> list) -> |
| 131 | list.size() == 1 && list.get(0).getUserId().equals(2L))); | 128 | list.size() == 1 && list.get(0).getUserId().equals(2L))); |
| 132 | } | 129 | } |
| @@ -145,7 +142,8 @@ public class ImGroupMemberServiceImplTest extends BaseMockitoUnitTest { | @@ -145,7 +142,8 @@ public class ImGroupMemberServiceImplTest extends BaseMockitoUnitTest { | ||
| 145 | 142 | ||
| 146 | groupMemberService.addGroupMembers(10L, List.of(2L, 3L)); | 143 | groupMemberService.addGroupMembers(10L, List.of(2L, 3L)); |
| 147 | 144 | ||
| 148 | - verify(groupMemberMapper).updateBatch(anyList()); | 145 | + verify(groupMemberMapper, times(2)).updateRejoinFields(anyLong(), eq(CommonStatusEnum.ENABLE.getStatus()), |
| 146 | + any(), eq(ImGroupMemberRoleEnum.NORMAL.getRole()), isNull(), isNull()); | ||
| 149 | verify(groupMemberMapper, never()).insertBatch(anyList()); | 147 | verify(groupMemberMapper, never()).insertBatch(anyList()); |
| 150 | } | 148 | } |
| 151 | 149 | ||
| @@ -157,7 +155,7 @@ public class ImGroupMemberServiceImplTest extends BaseMockitoUnitTest { | @@ -157,7 +155,7 @@ public class ImGroupMemberServiceImplTest extends BaseMockitoUnitTest { | ||
| 157 | 155 | ||
| 158 | groupMemberService.addGroupMembers(10L, List.of(2L, 3L)); | 156 | groupMemberService.addGroupMembers(10L, List.of(2L, 3L)); |
| 159 | 157 | ||
| 160 | - verify(groupMemberMapper, never()).updateBatch(anyList()); | 158 | + verify(groupMemberMapper, never()).updateRejoinFields(anyLong(), anyInt(), any(), anyInt(), any(), any()); |
| 161 | verify(groupMemberMapper).insertBatch(anyList()); | 159 | verify(groupMemberMapper).insertBatch(anyList()); |
| 162 | } | 160 | } |
| 163 | 161 | ||
| @@ -173,7 +171,7 @@ public class ImGroupMemberServiceImplTest extends BaseMockitoUnitTest { | @@ -173,7 +171,7 @@ public class ImGroupMemberServiceImplTest extends BaseMockitoUnitTest { | ||
| 173 | 171 | ||
| 174 | groupMemberService.addGroupMembers(10L, List.of(2L)); | 172 | groupMemberService.addGroupMembers(10L, List.of(2L)); |
| 175 | 173 | ||
| 176 | - verify(groupMemberMapper, never()).updateBatch(anyList()); | 174 | + verify(groupMemberMapper, never()).updateRejoinFields(anyLong(), anyInt(), any(), anyInt(), any(), any()); |
| 177 | verify(groupMemberMapper, never()).insertBatch(anyList()); | 175 | verify(groupMemberMapper, never()).insertBatch(anyList()); |
| 178 | } | 176 | } |
| 179 | 177 |
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImplTest.java
| @@ -525,6 +525,24 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { | @@ -525,6 +525,24 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { | ||
| 525 | } | 525 | } |
| 526 | } | 526 | } |
| 527 | 527 | ||
| 528 | + @Test | ||
| 529 | + public void testQuitGroup_bannedGroupAllowed() { | ||
| 530 | + try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) { | ||
| 531 | + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class))) | ||
| 532 | + .thenReturn(groupService); | ||
| 533 | + | ||
| 534 | + ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(99L) | ||
| 535 | + .banned(true).status(CommonStatusEnum.ENABLE.getStatus()).build(); | ||
| 536 | + when(groupMapper.selectById(10L)).thenReturn(group); | ||
| 537 | + | ||
| 538 | + groupService.quitGroup(10L, 1L); | ||
| 539 | + | ||
| 540 | + verify(groupMemberService).removeGroupMember(10L, 1L); | ||
| 541 | + verify(groupMessageService).deleteReadMaxMessageId(10L, 1L); | ||
| 542 | + verify(groupMessageService).sendGroupMessage(eq(1L), any(ImGroupMessageSendDTO.class)); | ||
| 543 | + } | ||
| 544 | + } | ||
| 545 | + | ||
| 528 | // ========== removeGroupMember ========== | 546 | // ========== removeGroupMember ========== |
| 529 | 547 | ||
| 530 | @Test | 548 | @Test |
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImplTest.java
| @@ -11,6 +11,7 @@ import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; | @@ -11,6 +11,7 @@ import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; | ||
| 11 | import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO; | 11 | 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.dal.redis.message.ImGroupMessageReadRedisDAO; | 13 | import cn.iocoder.yudao.module.im.dal.redis.message.ImGroupMessageReadRedisDAO; |
| 14 | +import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum; | ||
| 14 | import cn.iocoder.yudao.module.im.enums.message.ImGroupMessageReceiptStatusEnum; | 15 | import cn.iocoder.yudao.module.im.enums.message.ImGroupMessageReceiptStatusEnum; |
| 15 | import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum; | 16 | import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum; |
| 16 | import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; | 17 | import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; |
| @@ -588,6 +589,9 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { | @@ -588,6 +589,9 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { | ||
| 588 | .status(ImMessageStatusEnum.UNREAD.getStatus()) | 589 | .status(ImMessageStatusEnum.UNREAD.getStatus()) |
| 589 | .sendTime(LocalDateTime.now()).build(); | 590 | .sendTime(LocalDateTime.now()).build(); |
| 590 | when(groupMessageMapper.selectById(50L)).thenReturn(message); | 591 | when(groupMessageMapper.selectById(50L)).thenReturn(message); |
| 592 | + when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn( | ||
| 593 | + ImGroupMemberDO.builder().groupId(10L).userId(1L) | ||
| 594 | + .role(ImGroupMemberRoleEnum.NORMAL.getRole()).build()); | ||
| 591 | 595 | ||
| 592 | // 调用并断言 | 596 | // 调用并断言 |
| 593 | ServiceException exception = assertThrows(ServiceException.class, | 597 | ServiceException exception = assertThrows(ServiceException.class, |
| @@ -603,6 +607,9 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { | @@ -603,6 +607,9 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { | ||
| 603 | .status(ImMessageStatusEnum.RECALL.getStatus()) | 607 | .status(ImMessageStatusEnum.RECALL.getStatus()) |
| 604 | .sendTime(LocalDateTime.now()).build(); | 608 | .sendTime(LocalDateTime.now()).build(); |
| 605 | when(groupMessageMapper.selectById(50L)).thenReturn(message); | 609 | when(groupMessageMapper.selectById(50L)).thenReturn(message); |
| 610 | + when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn( | ||
| 611 | + ImGroupMemberDO.builder().groupId(10L).userId(1L) | ||
| 612 | + .role(ImGroupMemberRoleEnum.NORMAL.getRole()).build()); | ||
| 606 | 613 | ||
| 607 | // 调用并断言 | 614 | // 调用并断言 |
| 608 | ServiceException exception = assertThrows(ServiceException.class, | 615 | ServiceException exception = assertThrows(ServiceException.class, |
| @@ -618,6 +625,9 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { | @@ -618,6 +625,9 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { | ||
| 618 | .status(ImMessageStatusEnum.UNREAD.getStatus()) | 625 | .status(ImMessageStatusEnum.UNREAD.getStatus()) |
| 619 | .sendTime(LocalDateTime.now().minusMinutes(10)).build(); | 626 | .sendTime(LocalDateTime.now().minusMinutes(10)).build(); |
| 620 | when(groupMessageMapper.selectById(50L)).thenReturn(message); | 627 | when(groupMessageMapper.selectById(50L)).thenReturn(message); |
| 628 | + when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn( | ||
| 629 | + ImGroupMemberDO.builder().groupId(10L).userId(1L) | ||
| 630 | + .role(ImGroupMemberRoleEnum.NORMAL.getRole()).build()); | ||
| 621 | 631 | ||
| 622 | // 调用并断言 | 632 | // 调用并断言 |
| 623 | ServiceException exception = assertThrows(ServiceException.class, | 633 | ServiceException exception = assertThrows(ServiceException.class, |
| @@ -641,12 +651,12 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { | @@ -641,12 +651,12 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { | ||
| 641 | 651 | ||
| 642 | @Test | 652 | @Test |
| 643 | public void testGetReadUserIds_withVisibleScope() { | 653 | public void testGetReadUserIds_withVisibleScope() { |
| 644 | - // 准备:用户 1 是群成员 | 654 | + // 准备:发送者用户 5 是群成员 |
| 645 | ImGroupMemberDO currentMember = ImGroupMemberDO.builder() | 655 | ImGroupMemberDO currentMember = ImGroupMemberDO.builder() |
| 646 | - .groupId(10L).userId(1L) | 656 | + .groupId(10L).userId(5L) |
| 647 | .status(CommonStatusEnum.ENABLE.getStatus()) | 657 | .status(CommonStatusEnum.ENABLE.getStatus()) |
| 648 | .joinTime(LocalDateTime.of(2026, 1, 1, 0, 0, 0)).build(); | 658 | .joinTime(LocalDateTime.of(2026, 1, 1, 0, 0, 0)).build(); |
| 649 | - when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn(currentMember); | 659 | + when(groupMemberService.validateMemberInGroup(10L, 5L)).thenReturn(currentMember); |
| 650 | 660 | ||
| 651 | // 准备:消息由用户 5 发,发送时间在 2026-04-12 10:00 | 661 | // 准备:消息由用户 5 发,发送时间在 2026-04-12 10:00 |
| 652 | ImGroupMessageDO message = ImGroupMessageDO.builder() | 662 | ImGroupMessageDO message = ImGroupMessageDO.builder() |
| @@ -661,6 +671,9 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { | @@ -661,6 +671,9 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { | ||
| 661 | // 用户 5: 发送者,不计入回执 | 671 | // 用户 5: 发送者,不计入回执 |
| 662 | List<ImGroupMemberDO> allMembers = List.of( | 672 | List<ImGroupMemberDO> allMembers = List.of( |
| 663 | currentMember, | 673 | currentMember, |
| 674 | + ImGroupMemberDO.builder().groupId(10L).userId(1L) | ||
| 675 | + .status(CommonStatusEnum.ENABLE.getStatus()) | ||
| 676 | + .joinTime(LocalDateTime.of(2026, 1, 1, 0, 0, 0)).build(), | ||
| 664 | ImGroupMemberDO.builder().groupId(10L).userId(2L) | 677 | ImGroupMemberDO.builder().groupId(10L).userId(2L) |
| 665 | .status(CommonStatusEnum.ENABLE.getStatus()) | 678 | .status(CommonStatusEnum.ENABLE.getStatus()) |
| 666 | .joinTime(LocalDateTime.of(2026, 1, 1, 0, 0, 0)).build(), | 679 | .joinTime(LocalDateTime.of(2026, 1, 1, 0, 0, 0)).build(), |
| @@ -681,7 +694,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { | @@ -681,7 +694,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { | ||
| 681 | when(groupMessageReadRedisDAO.getReadMaxMessageIdMap(10L)).thenReturn(positions); | 694 | when(groupMessageReadRedisDAO.getReadMaxMessageIdMap(10L)).thenReturn(positions); |
| 682 | 695 | ||
| 683 | // 调用:查询 messageId=80 的已读用户 | 696 | // 调用:查询 messageId=80 的已读用户 |
| 684 | - List<Long> readUsers = groupMessageService.getGroupReadUserIds(1L, 10L, 80L); | 697 | + List<Long> readUsers = groupMessageService.getGroupReadUserIds(5L, 10L, 80L); |
| 685 | 698 | ||
| 686 | // 断言: | 699 | // 断言: |
| 687 | // 用户 1: 可见 + readMaxId=100>=80 → 已读 ✓ | 700 | // 用户 1: 可见 + readMaxId=100>=80 → 已读 ✓ |
| @@ -804,6 +817,8 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { | @@ -804,6 +817,8 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { | ||
| 804 | .groupId(10L).userId(1L) | 817 | .groupId(10L).userId(1L) |
| 805 | .status(CommonStatusEnum.ENABLE.getStatus()).build(); | 818 | .status(CommonStatusEnum.ENABLE.getStatus()).build(); |
| 806 | when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn(member); | 819 | when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn(member); |
| 820 | + when(groupMessageMapper.selectById(100L)).thenReturn(ImGroupMessageDO.builder() | ||
| 821 | + .id(100L).groupId(10L).senderId(2L).sendTime(LocalDateTime.now()).build()); | ||
| 807 | when(groupMessageReadRedisDAO.getReadMaxMessageId(10L, 1L)).thenReturn(5L); | 822 | when(groupMessageReadRedisDAO.getReadMaxMessageId(10L, 1L)).thenReturn(5L); |
| 808 | // readGroupMessageEvent 内部会调 selectListByGroupIdAndPendingReceipt → 返回空简化流程 | 823 | // readGroupMessageEvent 内部会调 selectListByGroupIdAndPendingReceipt → 返回空简化流程 |
| 809 | when(groupMessageMapper.selectListByGroupIdAndPendingReceipt(10L, 5L, 100L)).thenReturn(List.of()); | 824 | when(groupMessageMapper.selectListByGroupIdAndPendingReceipt(10L, 5L, 100L)).thenReturn(List.of()); |
| @@ -876,6 +891,8 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { | @@ -876,6 +891,8 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { | ||
| 876 | .groupId(10L).userId(1L) | 891 | .groupId(10L).userId(1L) |
| 877 | .status(CommonStatusEnum.ENABLE.getStatus()).build(); | 892 | .status(CommonStatusEnum.ENABLE.getStatus()).build(); |
| 878 | when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn(member); | 893 | when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn(member); |
| 894 | + when(groupMessageMapper.selectById(100L)).thenReturn(ImGroupMessageDO.builder() | ||
| 895 | + .id(100L).groupId(10L).senderId(2L).sendTime(LocalDateTime.now()).build()); | ||
| 879 | when(groupMessageReadRedisDAO.getReadMaxMessageId(10L, 1L)).thenReturn(200L); | 896 | when(groupMessageReadRedisDAO.getReadMaxMessageId(10L, 1L)).thenReturn(200L); |
| 880 | 897 | ||
| 881 | // 调用 | 898 | // 调用 |
| @@ -886,6 +903,23 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { | @@ -886,6 +903,23 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { | ||
| 886 | verify(imWebSocketService, never()).sendGroupMessageAsync(anyLong(), any(ImGroupMessageDTO.class)); | 903 | verify(imWebSocketService, never()).sendGroupMessageAsync(anyLong(), any(ImGroupMessageDTO.class)); |
| 887 | } | 904 | } |
| 888 | 905 | ||
| 906 | + @Test | ||
| 907 | + public void testReadGroupMessages_messageNotInGroup() { | ||
| 908 | + // 准备:用户在群,但 messageId 属于其它群 | ||
| 909 | + ImGroupMemberDO member = ImGroupMemberDO.builder() | ||
| 910 | + .groupId(10L).userId(1L) | ||
| 911 | + .status(CommonStatusEnum.ENABLE.getStatus()).build(); | ||
| 912 | + when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn(member); | ||
| 913 | + when(groupMessageMapper.selectById(100L)).thenReturn(ImGroupMessageDO.builder() | ||
| 914 | + .id(100L).groupId(20L).senderId(2L).sendTime(LocalDateTime.now()).build()); | ||
| 915 | + | ||
| 916 | + // 调用并断言 | ||
| 917 | + ServiceException exception = assertThrows(ServiceException.class, | ||
| 918 | + () -> groupMessageService.readGroupMessages(1L, 10L, 100L)); | ||
| 919 | + assertEquals(MESSAGE_NOT_IN_GROUP.getCode(), exception.getCode()); | ||
| 920 | + verify(groupMessageReadRedisDAO, never()).updateReadMaxMessageId(anyLong(), anyLong(), anyLong()); | ||
| 921 | + } | ||
| 922 | + | ||
| 889 | // ========== 回执:DONE 状态迁移 ========== | 923 | // ========== 回执:DONE 状态迁移 ========== |
| 890 | 924 | ||
| 891 | @Test | 925 | @Test |
| @@ -1105,7 +1139,8 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { | @@ -1105,7 +1139,8 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { | ||
| 1105 | reqVO.setLimit(20); | 1139 | reqVO.setLimit(20); |
| 1106 | 1140 | ||
| 1107 | List<ImGroupMessageDO> mockList = List.of( | 1141 | List<ImGroupMessageDO> mockList = List.of( |
| 1108 | - ImGroupMessageDO.builder().id(99L).groupId(10L).senderId(2L).build() | 1142 | + ImGroupMessageDO.builder().id(99L).groupId(10L).senderId(2L) |
| 1143 | + .sendTime(joinTime.plusMinutes(1)).build() | ||
| 1109 | ); | 1144 | ); |
| 1110 | when(groupMessageMapper.selectHistoryList(10L, 100L, 20, joinTime)).thenReturn(mockList); | 1145 | when(groupMessageMapper.selectHistoryList(10L, 100L, 20, joinTime)).thenReturn(mockList); |
| 1111 | 1146 |