Commit ae36d1ad68c5ff7f5e84cb0e8e20bc30fb844e42
1 parent
5f88c9ca
✨ feat(im): 增加【消息引用】的功能
Showing
15 changed files
with
268 additions
and
16 deletions
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/JsonUtils.java
| ... | ... | @@ -22,6 +22,7 @@ import java.lang.reflect.Type; |
| 22 | 22 | import java.time.LocalDateTime; |
| 23 | 23 | import java.util.ArrayList; |
| 24 | 24 | import java.util.List; |
| 25 | +import java.util.Map; | |
| 25 | 26 | |
| 26 | 27 | /** |
| 27 | 28 | * JSON 工具类 |
| ... | ... | @@ -173,6 +174,23 @@ public class JsonUtils { |
| 173 | 174 | } |
| 174 | 175 | } |
| 175 | 176 | |
| 177 | + /** | |
| 178 | + * 解析 JSON 字符串成 Map,空字符串或解析失败返回 null | |
| 179 | + * | |
| 180 | + * @param text JSON 字符串 | |
| 181 | + * @return Map 对象 | |
| 182 | + */ | |
| 183 | + public static Map<String, Object> parseMap(String text) { | |
| 184 | + if (StrUtil.isEmpty(text)) { | |
| 185 | + return null; | |
| 186 | + } | |
| 187 | + try { | |
| 188 | + return objectMapper.readValue(text, new TypeReference<Map<String, Object>>() {}); | |
| 189 | + } catch (IOException e) { | |
| 190 | + return null; | |
| 191 | + } | |
| 192 | + } | |
| 193 | + | |
| 176 | 194 | public static <T> List<T> parseArray(String text, Class<T> clazz) { |
| 177 | 195 | if (StrUtil.isEmpty(text)) { |
| 178 | 196 | return new ArrayList<>(); | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/ImGroupMessageController.java
| ... | ... | @@ -64,13 +64,14 @@ public class ImGroupMessageController { |
| 64 | 64 | return success(BeanUtils.toBean(message, ImGroupMessageRespVO.class)); |
| 65 | 65 | } |
| 66 | 66 | |
| 67 | + // TODO @AI:改成 get-read-user-ids | |
| 67 | 68 | @GetMapping("/read-users") |
| 68 | 69 | @Operation(summary = "获取群消息已读用户列表") |
| 69 | 70 | @Parameter(name = "groupId", description = "群编号", required = true, example = "1") |
| 70 | 71 | @Parameter(name = "messageId", description = "消息编号", required = true, example = "1") |
| 71 | - public CommonResult<List<Long>> getGroupReadUsers(@RequestParam("groupId") Long groupId, | |
| 72 | - @RequestParam("messageId") Long messageId) { | |
| 73 | - return success(groupMessageService.getGroupReadUsers(getLoginUserId(), groupId, messageId)); | |
| 72 | + public CommonResult<List<Long>> getGroupReadUserIds(@RequestParam("groupId") Long groupId, | |
| 73 | + @RequestParam("messageId") Long messageId) { | |
| 74 | + return success(groupMessageService.getGroupReadUserIds(getLoginUserId(), groupId, messageId)); | |
| 74 | 75 | } |
| 75 | 76 | |
| 76 | 77 | @GetMapping("/list") | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/ErrorCodeConstants.java
| ... | ... | @@ -16,6 +16,7 @@ public interface ErrorCodeConstants { |
| 16 | 16 | ErrorCode MESSAGE_SENSITIVE_WORD_BLOCKED = new ErrorCode(1_040_300_004, "消息包含敏感词,无法发送"); |
| 17 | 17 | ErrorCode MESSAGE_PULL_SIZE_EXCEEDED = new ErrorCode(1_040_300_005, "单次拉取消息数量不能超过 {} 条"); |
| 18 | 18 | ErrorCode MESSAGE_RECALL_TIMEOUT = new ErrorCode(1_040_300_007, "超过 {} 分钟的消息无法撤回"); |
| 19 | + ErrorCode MESSAGE_QUOTE_INVALID = new ErrorCode(1_040_300_008, "引用的消息不可用"); | |
| 19 | 20 | |
| 20 | 21 | // ========== 群 (1-040-400-000) ========== |
| 21 | 22 | ErrorCode GROUP_NOT_EXISTS = new ErrorCode(1_040_400_000, "群不存在"); | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageService.java
| ... | ... | @@ -62,7 +62,7 @@ public interface ImGroupMessageService { |
| 62 | 62 | * @param messageId 消息编号 |
| 63 | 63 | * @return 已读用户编号列表 |
| 64 | 64 | */ |
| 65 | - List<Long> getGroupReadUsers(Long userId, Long groupId, Long messageId); | |
| 65 | + List<Long> getGroupReadUserIds(Long userId, Long groupId, Long messageId); | |
| 66 | 66 | |
| 67 | 67 | /** |
| 68 | 68 | * 查询群聊历史消息(游标拉取) | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImpl.java
| ... | ... | @@ -24,7 +24,9 @@ import cn.iocoder.yudao.module.im.service.group.ImGroupService; |
| 24 | 24 | import cn.iocoder.yudao.module.im.service.sensitiveword.ImSensitiveWordService; |
| 25 | 25 | import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService; |
| 26 | 26 | import cn.iocoder.yudao.module.im.service.websocket.dto.ImGroupMessageDTO; |
| 27 | +import cn.iocoder.yudao.module.im.service.websocket.dto.message.QuoteMessage; | |
| 27 | 28 | import cn.iocoder.yudao.module.im.service.websocket.dto.message.RecallMessage; |
| 29 | +import cn.iocoder.yudao.module.im.util.ImMessageUtils; | |
| 28 | 30 | import jakarta.annotation.Resource; |
| 29 | 31 | import lombok.extern.slf4j.Slf4j; |
| 30 | 32 | import org.springframework.scheduling.annotation.Async; |
| ... | ... | @@ -84,13 +86,15 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { |
| 84 | 86 | } |
| 85 | 87 | // 1.2 校验群存在、发送人仍在群中 |
| 86 | 88 | groupService.validateGroupExists(reqVO.getGroupId()); |
| 87 | - groupMemberService.validateMemberInGroup(reqVO.getGroupId(), senderId); | |
| 89 | + ImGroupMemberDO senderMember = groupMemberService.validateMemberInGroup(reqVO.getGroupId(), senderId); | |
| 88 | 90 | // 1.3 文本消息敏感词过滤 |
| 89 | 91 | if (ImMessageTypeEnum.TEXT.getType().equals(reqVO.getType())) { |
| 90 | 92 | sensitiveWordService.validateText(reqVO.getContent()); |
| 91 | 93 | } |
| 92 | 94 | |
| 93 | - // 2. 构建并保存消息 | |
| 95 | + // 2.1 引用 quote 消息规范化 | |
| 96 | + reqVO.setContent(normalizeQuoteContent(reqVO, senderMember)); | |
| 97 | + // 2.2 构建并保存消息 | |
| 94 | 98 | ImGroupMessageDO message = BeanUtils.toBean(reqVO, ImGroupMessageDO.class, m -> m |
| 95 | 99 | .setSenderId(senderId).setStatus(ImMessageStatusEnum.UNREAD.getStatus()).setSendTime(LocalDateTime.now()) |
| 96 | 100 | .setReceiptStatus(Boolean.TRUE.equals(reqVO.getReceipt()) |
| ... | ... | @@ -319,9 +323,8 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { |
| 319 | 323 | return recallMessage; |
| 320 | 324 | } |
| 321 | 325 | |
| 322 | - // TODO @芋艿:这个方法在优化下; | |
| 323 | 326 | @Override |
| 324 | - public List<Long> getGroupReadUsers(Long userId, Long groupId, Long messageId) { | |
| 327 | + public List<Long> getGroupReadUserIds(Long userId, Long groupId, Long messageId) { | |
| 325 | 328 | // 1.1 校验用户在群中(权限校验) |
| 326 | 329 | groupMemberService.validateMemberInGroup(groupId, userId); |
| 327 | 330 | // 1.2 获取消息 |
| ... | ... | @@ -433,6 +436,42 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { |
| 433 | 436 | // ========== 私有工具方法 ========== |
| 434 | 437 | |
| 435 | 438 | /** |
| 439 | + * 群聊引用消息规范化 | |
| 440 | + * | |
| 441 | + * @param reqVO 发送请求 | |
| 442 | + * @param senderMember 发送人成员 | |
| 443 | + * @return 规范化后的 content | |
| 444 | + */ | |
| 445 | + private String normalizeQuoteContent(ImGroupMessageSendReqVO reqVO, ImGroupMemberDO senderMember) { | |
| 446 | + // 解析客户端 content 里的 quote.messageId | |
| 447 | + Long quoteMessageId = ImMessageUtils.parseQuoteMessageId(reqVO.getContent()); | |
| 448 | + | |
| 449 | + // 情况一:没有 quoteMessageId,直接 remove 掉 content 里可能伪造的 quote 字段 | |
| 450 | + if (quoteMessageId == null) { | |
| 451 | + return ImMessageUtils.removeQuote(reqVO.getContent()); | |
| 452 | + } | |
| 453 | + | |
| 454 | + // 情况二:有 quoteMessageId,加载原消息并校验 | |
| 455 | + ImGroupMessageDO original = groupMessageMapper.selectById(quoteMessageId); | |
| 456 | + if (original == null | |
| 457 | + || ImMessageStatusEnum.RECALL.getStatus().equals(original.getStatus())) { | |
| 458 | + throw exception(MESSAGE_QUOTE_INVALID); | |
| 459 | + } | |
| 460 | + // 校验是同群 | |
| 461 | + if (ObjUtil.notEqual(original.getGroupId(), reqVO.getGroupId())) { | |
| 462 | + throw exception(MESSAGE_QUOTE_INVALID); | |
| 463 | + } | |
| 464 | + // 校验对发送人可见(入群时间 / 退群时间 / 定向接收) | |
| 465 | + if (!isMessageVisible(original, senderMember, senderMember.getUserId())) { | |
| 466 | + throw exception(MESSAGE_QUOTE_INVALID); | |
| 467 | + } | |
| 468 | + // 构建 quote 对象并注入 content | |
| 469 | + QuoteMessage quote = ImMessageUtils.buildQuote(original.getId(), | |
| 470 | + original.getSenderId(), original.getType(), original.getContent()); | |
| 471 | + return ImMessageUtils.appendQuote(reqVO.getContent(), quote); | |
| 472 | + } | |
| 473 | + | |
| 474 | + /** | |
| 436 | 475 | * 校验群聊消息存在 |
| 437 | 476 | * |
| 438 | 477 | * @param messageId 消息编号 | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageServiceImpl.java
| ... | ... | @@ -17,7 +17,9 @@ import cn.iocoder.yudao.module.im.service.friend.ImFriendService; |
| 17 | 17 | import cn.iocoder.yudao.module.im.service.sensitiveword.ImSensitiveWordService; |
| 18 | 18 | import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService; |
| 19 | 19 | import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO; |
| 20 | +import cn.iocoder.yudao.module.im.service.websocket.dto.message.QuoteMessage; | |
| 20 | 21 | import cn.iocoder.yudao.module.im.service.websocket.dto.message.RecallMessage; |
| 22 | +import cn.iocoder.yudao.module.im.util.ImMessageUtils; | |
| 21 | 23 | import jakarta.annotation.Resource; |
| 22 | 24 | import lombok.extern.slf4j.Slf4j; |
| 23 | 25 | import org.springframework.stereotype.Service; |
| ... | ... | @@ -71,7 +73,10 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService { |
| 71 | 73 | sensitiveWordService.validateText(reqVO.getContent()); |
| 72 | 74 | } |
| 73 | 75 | |
| 74 | - // 2. 构建并保存消息 | |
| 76 | + | |
| 77 | + // 2.1 引用 quote 消息规范化 | |
| 78 | + reqVO.setContent(normalizeQuoteContent(reqVO, senderId)); | |
| 79 | + // 2.2 构建并保存消息 | |
| 75 | 80 | ImPrivateMessageDO message = BeanUtils.toBean(reqVO, ImPrivateMessageDO.class, m -> m |
| 76 | 81 | .setSenderId(senderId).setStatus(ImMessageStatusEnum.UNREAD.getStatus()).setSendTime(LocalDateTime.now())); |
| 77 | 82 | privateMessageMapper.insert(message); |
| ... | ... | @@ -83,6 +88,42 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService { |
| 83 | 88 | return message; |
| 84 | 89 | } |
| 85 | 90 | |
| 91 | + /** | |
| 92 | + * 私聊引用消息规范化 | |
| 93 | + * | |
| 94 | + * @param reqVO 发送请求 | |
| 95 | + * @param senderId 发送人编号 | |
| 96 | + * @return 规范化后的 content | |
| 97 | + */ | |
| 98 | + private String normalizeQuoteContent(ImPrivateMessageSendReqVO reqVO, Long senderId) { | |
| 99 | + // 解析客户端 content 里的 quote.messageId | |
| 100 | + Long quoteMessageId = ImMessageUtils.parseQuoteMessageId(reqVO.getContent()); | |
| 101 | + | |
| 102 | + // 情况一:没有 quoteMessageId,直接 remove 掉 content 里可能伪造的 quote 字段 | |
| 103 | + if (quoteMessageId == null) { | |
| 104 | + return ImMessageUtils.removeQuote(reqVO.getContent()); | |
| 105 | + } | |
| 106 | + | |
| 107 | + // 情况二:有 quoteMessageId,加载原消息并校验 | |
| 108 | + ImPrivateMessageDO original = privateMessageMapper.selectById(quoteMessageId); | |
| 109 | + if (original == null | |
| 110 | + || ImMessageStatusEnum.RECALL.getStatus().equals(original.getStatus())) { | |
| 111 | + throw exception(MESSAGE_QUOTE_INVALID); | |
| 112 | + } | |
| 113 | + // 校验是同对话 | |
| 114 | + boolean sameConversation = (ObjUtil.equal(original.getSenderId(), senderId) // 发送人是当前用户,接收人是对方 | |
| 115 | + && ObjUtil.equal(original.getReceiverId(), reqVO.getReceiverId())) | |
| 116 | + || (ObjUtil.equal(original.getSenderId(), reqVO.getReceiverId()) // 发送人是对方,接收人是当前用户 | |
| 117 | + && ObjUtil.equal(original.getReceiverId(), senderId)); | |
| 118 | + if (!sameConversation) { | |
| 119 | + throw exception(MESSAGE_QUOTE_INVALID); | |
| 120 | + } | |
| 121 | + // 构建 quote 对象并注入 content | |
| 122 | + QuoteMessage quote = ImMessageUtils.buildQuote(original.getId(), | |
| 123 | + original.getSenderId(), original.getType(), original.getContent()); | |
| 124 | + return ImMessageUtils.appendQuote(reqVO.getContent(), quote); | |
| 125 | + } | |
| 126 | + | |
| 86 | 127 | @Override |
| 87 | 128 | public List<ImPrivateMessageDO> pullPrivateMessageList(Long userId, Long minId, Integer size) { |
| 88 | 129 | if (size > MESSAGE_MAX_PULL_SIZE) { | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/AudioMessage.java
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/FileMessage.java
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/ImageMessage.java
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/QuoteMessage.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.service.websocket.dto.message; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; | |
| 4 | +import lombok.Data; | |
| 5 | +import lombok.experimental.Accessors; | |
| 6 | + | |
| 7 | +/** | |
| 8 | + * 引用消息内容 | |
| 9 | + * <p> | |
| 10 | + * 客户端在 content.quote 里写入 messageId 表达"引用了哪条消息",服务端反查原消息后重算 QuoteMessage 覆盖客户端写入, | |
| 11 | + * 避免伪造发送人 / 类型 / 摘要 | |
| 12 | + * | |
| 13 | + * @see TextMessage 等等消息内容里 quote 字段 | |
| 14 | + */ | |
| 15 | +@Data | |
| 16 | +@Accessors(chain = true) | |
| 17 | +public class QuoteMessage { | |
| 18 | + | |
| 19 | + /** | |
| 20 | + * content JSON 里 quote 字段名 | |
| 21 | + */ | |
| 22 | + public static final String FIELD_NAME = "quote"; | |
| 23 | + | |
| 24 | + /** | |
| 25 | + * 被引用消息编号 | |
| 26 | + */ | |
| 27 | + private Long messageId; | |
| 28 | + /** | |
| 29 | + * 被引用消息发送人编号 | |
| 30 | + * <p> | |
| 31 | + * 关联 AdminUserDO 的 id 字段 | |
| 32 | + */ | |
| 33 | + private Long senderId; | |
| 34 | + /** | |
| 35 | + * 被引用消息类型 | |
| 36 | + * <p> | |
| 37 | + * 枚举 {@link ImMessageTypeEnum} | |
| 38 | + */ | |
| 39 | + private Integer type; | |
| 40 | + /** | |
| 41 | + * 原消息完整 content(JSON);服务端复制时会 removeQuote 防嵌套 | |
| 42 | + */ | |
| 43 | + private String content; | |
| 44 | + | |
| 45 | +} | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/TextMessage.java
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/message/VideoMessage.java
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/util/ImMessageUtils.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.util; | |
| 2 | + | |
| 3 | +import cn.hutool.core.convert.Convert; | |
| 4 | +import cn.hutool.core.util.StrUtil; | |
| 5 | +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; | |
| 6 | +import cn.iocoder.yudao.module.im.service.websocket.dto.message.QuoteMessage; | |
| 7 | + | |
| 8 | +import java.util.LinkedHashMap; | |
| 9 | +import java.util.Map; | |
| 10 | + | |
| 11 | +/** | |
| 12 | + * IM 消息内容相关工具类 | |
| 13 | + */ | |
| 14 | +public class ImMessageUtils { | |
| 15 | + | |
| 16 | + /** | |
| 17 | + * 从 content 解析客户端写入的 quote.messageId | |
| 18 | + * | |
| 19 | + * @param content 原 content(JSON) | |
| 20 | + * @return messageId,不存在 / 非法返回 null | |
| 21 | + */ | |
| 22 | + public static Long parseQuoteMessageId(String content) { | |
| 23 | + Map<String, Object> map = JsonUtils.parseMap(content); | |
| 24 | + if (map == null) { | |
| 25 | + return null; | |
| 26 | + } | |
| 27 | + Object quoteRaw = map.get(QuoteMessage.FIELD_NAME); | |
| 28 | + if (!(quoteRaw instanceof Map)) { | |
| 29 | + return null; | |
| 30 | + } | |
| 31 | + return Convert.toLong(((Map<?, ?>) quoteRaw).get("messageId")); | |
| 32 | + } | |
| 33 | + | |
| 34 | + /** | |
| 35 | + * 把 quote 写入 content 的 quote 字段 | |
| 36 | + * | |
| 37 | + * @param content 原 content(JSON) | |
| 38 | + * @param quote QuoteMessage 对象 | |
| 39 | + * @return 注入 quote 后的 content(JSON) | |
| 40 | + */ | |
| 41 | + public static String appendQuote(String content, QuoteMessage quote) { | |
| 42 | + Map<String, Object> map = JsonUtils.parseMap(content); | |
| 43 | + if (map == null) { | |
| 44 | + map = new LinkedHashMap<>(); | |
| 45 | + } | |
| 46 | + map.put(QuoteMessage.FIELD_NAME, quote); | |
| 47 | + return JsonUtils.toJsonString(map); | |
| 48 | + } | |
| 49 | + | |
| 50 | + /** | |
| 51 | + * 移除 content 里的 quote 字段 | |
| 52 | + * | |
| 53 | + * @param content 原 content(JSON) | |
| 54 | + * @return remove quote 后的 content(JSON) | |
| 55 | + */ | |
| 56 | + public static String removeQuote(String content) { | |
| 57 | + if (StrUtil.isBlank(content) || !StrUtil.contains(content, "\"" + QuoteMessage.FIELD_NAME + "\"")) { | |
| 58 | + return content; | |
| 59 | + } | |
| 60 | + Map<String, Object> map = JsonUtils.parseMap(content); | |
| 61 | + if (map == null || !map.containsKey(QuoteMessage.FIELD_NAME)) { | |
| 62 | + return content; | |
| 63 | + } | |
| 64 | + map.remove(QuoteMessage.FIELD_NAME); | |
| 65 | + return JsonUtils.toJsonString(map); | |
| 66 | + } | |
| 67 | + | |
| 68 | + /** | |
| 69 | + * 构建 QuoteMessage 对象 | |
| 70 | + * | |
| 71 | + * @param messageId 被引用消息编号 | |
| 72 | + * @param senderId 被引用消息发送人编号 | |
| 73 | + * @param type 被引用消息类型 | |
| 74 | + * @param originalContent 被引用消息原 content(JSON),服务端会 removeQuote 防止嵌套 | |
| 75 | + * @return QuoteMessage 对象 | |
| 76 | + */ | |
| 77 | + public static QuoteMessage buildQuote(Long messageId, Long senderId, Integer type, String originalContent) { | |
| 78 | + return new QuoteMessage().setMessageId(messageId).setSenderId(senderId).setType(type) | |
| 79 | + .setContent(removeQuote(originalContent)); | |
| 80 | + } | |
| 81 | + | |
| 82 | +} | ... | ... |
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImplTest.java
| ... | ... | @@ -137,7 +137,7 @@ public class ImFriendServiceImplTest extends BaseMockitoUnitTest { |
| 137 | 137 | |
| 138 | 138 | @Test |
| 139 | 139 | public void testUpdateFriend_emptyRequest() { |
| 140 | - // 准备:muted 和 displayName 都不传 —— 短路返回,不打 SQL 也不发推送 | |
| 140 | + // 准备:muted 和 displayName 都不传 —— 直接返回,不打 SQL 也不发推送 | |
| 141 | 141 | ImFriendUpdateReqVO reqVO = new ImFriendUpdateReqVO(); |
| 142 | 142 | reqVO.setFriendUserId(2L); |
| 143 | 143 | ImFriendDO friend = ImFriendDO.builder().id(100L).userId(1L).friendUserId(2L) | ... | ... |
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImplTest.java
| ... | ... | @@ -636,7 +636,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { |
| 636 | 636 | } |
| 637 | 637 | |
| 638 | 638 | @Test |
| 639 | - public void testGetReadUsers_withVisibleScope() { | |
| 639 | + public void testGetReadUserIds_withVisibleScope() { | |
| 640 | 640 | // 准备:用户 1 是群成员 |
| 641 | 641 | ImGroupMemberDO currentMember = ImGroupMemberDO.builder() |
| 642 | 642 | .groupId(10L).userId(1L) |
| ... | ... | @@ -677,7 +677,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { |
| 677 | 677 | when(groupMessageReadRedisDAO.getReadMaxMessageIdMap(10L)).thenReturn(positions); |
| 678 | 678 | |
| 679 | 679 | // 调用:查询 messageId=80 的已读用户 |
| 680 | - List<Long> readUsers = groupMessageService.getGroupReadUsers(1L, 10L, 80L); | |
| 680 | + List<Long> readUsers = groupMessageService.getGroupReadUserIds(1L, 10L, 80L); | |
| 681 | 681 | |
| 682 | 682 | // 断言: |
| 683 | 683 | // 用户 1: 可见 + readMaxId=100>=80 → 已读 ✓ |
| ... | ... | @@ -692,14 +692,14 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { |
| 692 | 692 | } |
| 693 | 693 | |
| 694 | 694 | @Test |
| 695 | - public void testGetReadUsers_notInGroup() { | |
| 695 | + public void testGetReadUserIds_notInGroup() { | |
| 696 | 696 | // 准备:当前用户不在群中 |
| 697 | 697 | when(groupMemberService.validateMemberInGroup(10L, 99L)) |
| 698 | 698 | .thenThrow(new ServiceException(GROUP_MEMBER_NOT_IN_GROUP.getCode(), GROUP_MEMBER_NOT_IN_GROUP.getMsg())); |
| 699 | 699 | |
| 700 | 700 | // 调用并断言:越权校验 |
| 701 | 701 | ServiceException exception = assertThrows(ServiceException.class, |
| 702 | - () -> groupMessageService.getGroupReadUsers(99L, 10L, 80L)); | |
| 702 | + () -> groupMessageService.getGroupReadUserIds(99L, 10L, 80L)); | |
| 703 | 703 | assertEquals(GROUP_MEMBER_NOT_IN_GROUP.getCode(), exception.getCode()); |
| 704 | 704 | } |
| 705 | 705 | |
| ... | ... | @@ -787,7 +787,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { |
| 787 | 787 | verify(groupMessageMapper, never()).insert(any(ImGroupMessageDO.class)); |
| 788 | 788 | } |
| 789 | 789 | |
| 790 | - // ========== 已读:happy path + 短路 ========== | |
| 790 | + // ========== 已读:happy path + 跳过 ========== | |
| 791 | 791 | |
| 792 | 792 | @Test |
| 793 | 793 | public void testReadGroupMessages_success() { |
| ... | ... | @@ -815,7 +815,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { |
| 815 | 815 | |
| 816 | 816 | @Test |
| 817 | 817 | public void testReadGroupMessages_cursorAlreadyAhead() { |
| 818 | - // 准备:已读游标已 >= 目标,直接短路返回 | |
| 818 | + // 准备:已读游标已 >= 目标,直接返回 | |
| 819 | 819 | ImGroupMemberDO member = ImGroupMemberDO.builder() |
| 820 | 820 | .groupId(10L).userId(1L) |
| 821 | 821 | .status(CommonStatusEnum.ENABLE.getStatus()).build(); | ... | ... |