Commit d6d90b2bd54bd52230b836ee1bf63a6f3a38beae

Authored by YunaiV
1 parent bee33379

✨ feat(im): 优化 private message 单挑的发送接口

Showing 19 changed files with 388 additions and 183 deletions
yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/enums/ErrorCodeConstants.java
@@ -37,6 +37,8 @@ public interface ErrorCodeConstants { @@ -37,6 +37,8 @@ public interface ErrorCodeConstants {
37 // ========== 好友 (1-040-600-000) ========== 37 // ========== 好友 (1-040-600-000) ==========
38 ErrorCode FRIEND_NOT_EXISTS = new ErrorCode(1_040_600_000, "好友关系不存在"); 38 ErrorCode FRIEND_NOT_EXISTS = new ErrorCode(1_040_600_000, "好友关系不存在");
39 ErrorCode FRIEND_NOT_FRIEND = new ErrorCode(1_040_600_001, "对方不是您的好友"); 39 ErrorCode FRIEND_NOT_FRIEND = new ErrorCode(1_040_600_001, "对方不是您的好友");
  40 + ErrorCode FRIEND_ADD_SELF = new ErrorCode(1_040_600_002, "不允许添加自己为好友");
  41 + ErrorCode FRIEND_ALREADY = new ErrorCode(1_040_600_003, "你们已经是好友了");
40 42
41 // ========== 敏感词 (1-040-700-000) ========== 43 // ========== 敏感词 (1-040-700-000) ==========
42 ErrorCode SENSITIVE_WORD_NOT_EXISTS = new ErrorCode(1_040_700_000, "敏感词不存在"); 44 ErrorCode SENSITIVE_WORD_NOT_EXISTS = new ErrorCode(1_040_700_000, "敏感词不存在");
yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/enums/message/ImMessageStatusEnum.java
@@ -7,10 +7,10 @@ import lombok.RequiredArgsConstructor; @@ -7,10 +7,10 @@ import lombok.RequiredArgsConstructor;
7 import java.util.Arrays; 7 import java.util.Arrays;
8 8
9 /** 9 /**
10 - * IM 私聊消息状态枚举 10 + * IM 消息状态枚举
11 * <p> 11 * <p>
12 - * 私聊:UNREAD / READ / RECALL  
13 - * 群聊:NORMAL / RECALL 12 + * 私聊:SENDING(-1, 仅客户端) / UNREAD(0) / RECALL(2) / READ(3)
  13 + * 群聊:SENDING(-1, 仅客户端) / UNREAD(0, 作为正常状态) / RECALL(2)
14 * 14 *
15 * @author 芋道源码 15 * @author 芋道源码
16 */ 16 */
@@ -18,12 +18,10 @@ import java.util.Arrays; @@ -18,12 +18,10 @@ import java.util.Arrays;
18 @Getter 18 @Getter
19 public enum ImMessageStatusEnum implements ArrayValuable<Integer> { 19 public enum ImMessageStatusEnum implements ArrayValuable<Integer> {
20 20
21 - // ========== 私聊状态 ==========  
22 - UNREAD(0, "未读"),  
23 - READ(1, "已读"),  
24 -  
25 - // ========== 通用状态 ==========  
26 - RECALL(2, "已撤回"); 21 + SENDING(-1, "发送中"), // 仅客户端使用
  22 + UNREAD(0, "未读"), // 私聊=未读,群聊=正常(初始状态)
  23 + RECALL(2, "已撤回"),
  24 + READ(3, "已读"); // 仅私聊使用;群聊已读通过 Redis 已读位置实现
27 25
28 public static final Integer[] ARRAYS = Arrays.stream(values()).map(ImMessageStatusEnum::getStatus).toArray(Integer[]::new); 26 public static final Integer[] ARRAYS = Arrays.stream(values()).map(ImMessageStatusEnum::getStatus).toArray(Integer[]::new);
29 27
@@ -31,7 +29,6 @@ public enum ImMessageStatusEnum implements ArrayValuable&lt;Integer&gt; { @@ -31,7 +29,6 @@ public enum ImMessageStatusEnum implements ArrayValuable&lt;Integer&gt; {
31 * 状态 29 * 状态
32 */ 30 */
33 private final Integer status; 31 private final Integer status;
34 -  
35 /** 32 /**
36 * 名字 33 * 名字
37 */ 34 */
yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/enums/message/ImWebSocketTypeConstants.java deleted
1 -package cn.iocoder.yudao.module.im.enums.message;  
2 -  
3 -// DONE @芋艿:已 review,事件类型定义正确  
4 -/**  
5 - * IM WebSocket 事件类型常量  
6 - *  
7 - * @author 芋道源码  
8 - */  
9 -public interface ImWebSocketTypeConstants {  
10 -  
11 - /**  
12 - * 私聊消息事件  
13 - */  
14 - String PRIVATE_MESSAGE = "im-private-message";  
15 - /**  
16 - * 群聊消息事件  
17 - */  
18 - String GROUP_MESSAGE = "im-group-message";  
19 - /**  
20 - * 已读事件  
21 - */  
22 - String READ = "im-read";  
23 - /**  
24 - * 回执事件  
25 - */  
26 - String RECEIPT = "im-receipt";  
27 - /**  
28 - * 撤回事件  
29 - */  
30 - String RECALL = "im-recall";  
31 -  
32 -}  
33 \ No newline at end of file 0 \ No newline at end of file
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/ImPrivateMessageController.java
@@ -29,12 +29,13 @@ import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUti @@ -29,12 +29,13 @@ import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUti
29 public class ImPrivateMessageController { 29 public class ImPrivateMessageController {
30 30
31 @Resource 31 @Resource
32 - private ImPrivateMessageService imPrivateMessageService; 32 + private ImPrivateMessageService privateMessageService;
33 33
34 @PostMapping("/send") 34 @PostMapping("/send")
35 @Operation(summary = "发送私聊消息") 35 @Operation(summary = "发送私聊消息")
36 - public CommonResult<ImPrivateMessageRespVO> sendPrivateMessage(@Valid @RequestBody ImPrivateMessageSendReqVO reqVO) {  
37 - ImPrivateMessageDO message = imPrivateMessageService.sendPrivateMessage(getLoginUserId(), reqVO); 36 + public CommonResult<ImPrivateMessageRespVO> sendPrivateMessage(
  37 + @Valid @RequestBody ImPrivateMessageSendReqVO reqVO) {
  38 + ImPrivateMessageDO message = privateMessageService.sendPrivateMessage(getLoginUserId(), reqVO);
38 return success(BeanUtils.toBean(message, ImPrivateMessageRespVO.class)); 39 return success(BeanUtils.toBean(message, ImPrivateMessageRespVO.class));
39 } 40 }
40 41
@@ -44,7 +45,7 @@ public class ImPrivateMessageController { @@ -44,7 +45,7 @@ public class ImPrivateMessageController {
44 @Parameter(name = "size", description = "拉取数量", required = true, example = "100") 45 @Parameter(name = "size", description = "拉取数量", required = true, example = "100")
45 public CommonResult<List<ImPrivateMessageRespVO>> pullPrivateMessages(@RequestParam("minId") Long minId, 46 public CommonResult<List<ImPrivateMessageRespVO>> pullPrivateMessages(@RequestParam("minId") Long minId,
46 @RequestParam("size") Integer size) { 47 @RequestParam("size") Integer size) {
47 - List<ImPrivateMessageDO> messages = imPrivateMessageService.pullPrivateMessages(getLoginUserId(), minId, size); 48 + List<ImPrivateMessageDO> messages = privateMessageService.pullPrivateMessages(getLoginUserId(), minId, size);
48 return success(BeanUtils.toBean(messages, ImPrivateMessageRespVO.class)); 49 return success(BeanUtils.toBean(messages, ImPrivateMessageRespVO.class));
49 } 50 }
50 51
@@ -52,7 +53,7 @@ public class ImPrivateMessageController { @@ -52,7 +53,7 @@ public class ImPrivateMessageController {
52 @Operation(summary = "标记私聊消息已读") 53 @Operation(summary = "标记私聊消息已读")
53 @Parameter(name = "friendId", description = "好友用户编号", required = true, example = "2") 54 @Parameter(name = "friendId", description = "好友用户编号", required = true, example = "2")
54 public CommonResult<Boolean> readPrivateMessages(@RequestParam("friendId") Long friendId) { 55 public CommonResult<Boolean> readPrivateMessages(@RequestParam("friendId") Long friendId) {
55 - imPrivateMessageService.readPrivateMessages(getLoginUserId(), friendId); 56 + privateMessageService.readPrivateMessages(getLoginUserId(), friendId);
56 return success(true); 57 return success(true);
57 } 58 }
58 59
@@ -60,7 +61,7 @@ public class ImPrivateMessageController { @@ -60,7 +61,7 @@ public class ImPrivateMessageController {
60 @Operation(summary = "撤回私聊消息") 61 @Operation(summary = "撤回私聊消息")
61 @Parameter(name = "id", description = "消息编号", required = true, example = "1") 62 @Parameter(name = "id", description = "消息编号", required = true, example = "1")
62 public CommonResult<Boolean> recallPrivateMessage(@RequestParam("id") Long id) { 63 public CommonResult<Boolean> recallPrivateMessage(@RequestParam("id") Long id) {
63 - imPrivateMessageService.recallPrivateMessage(getLoginUserId(), id); 64 + privateMessageService.recallPrivateMessage(getLoginUserId(), id);
64 return success(true); 65 return success(true);
65 } 66 }
66 67
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/friend/ImFriendDO.java
1 package cn.iocoder.yudao.module.im.dal.dataobject.friend; 1 package cn.iocoder.yudao.module.im.dal.dataobject.friend;
2 2
3 -import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;  
4 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; 3 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
5 import com.baomidou.mybatisplus.annotation.KeySequence; 4 import com.baomidou.mybatisplus.annotation.KeySequence;
6 import com.baomidou.mybatisplus.annotation.TableId; 5 import com.baomidou.mybatisplus.annotation.TableId;
7 import com.baomidou.mybatisplus.annotation.TableName; 6 import com.baomidou.mybatisplus.annotation.TableName;
8 import lombok.*; 7 import lombok.*;
9 8
10 -import java.time.LocalDateTime;  
11 -  
12 /** 9 /**
13 * IM 好友关系 DO 10 * IM 好友关系 DO
  11 + * <p>
  12 + * 业务语义:
  13 + * - 双向关系:A-B 互为好友会存 2 条记录(userId=A, friendUserId=B 和 userId=B, friendUserId=A)
  14 + * - 软删除:`deleted=1` 表示单向解除好友;如果需要"全部不看到",双向都要 deleted=1
  15 + * - 免打扰:`muted` 控制是否屏蔽来自该好友的通知
14 * 16 *
15 * @author 芋道源码 17 * @author 芋道源码
16 */ 18 */
@@ -31,33 +33,23 @@ public class ImFriendDO extends BaseDO { @@ -31,33 +33,23 @@ public class ImFriendDO extends BaseDO {
31 private Long id; 33 private Long id;
32 /** 34 /**
33 * 用户编号 35 * 用户编号
34 - * 36 + * <p>
35 * 关联 AdminUserDO 的 id 字段 37 * 关联 AdminUserDO 的 id 字段
36 */ 38 */
37 private Long userId; 39 private Long userId;
38 /** 40 /**
39 * 好友用户编号 41 * 好友用户编号
40 - * 42 + * <p>
41 * 关联 AdminUserDO 的 id 字段 43 * 关联 AdminUserDO 的 id 字段
42 */ 44 */
43 private Long friendUserId; 45 private Long friendUserId;
44 /** 46 /**
45 - * 好友状态  
46 - * <p>  
47 - * 枚举 {@link CommonStatusEnum}  
48 - */  
49 - private Integer status;  
50 - /**  
51 * 是否免打扰 47 * 是否免打扰
52 */ 48 */
53 private Boolean muted; 49 private Boolean muted;
54 - /**  
55 - * 添加好友时间  
56 - */  
57 - private LocalDateTime addTime;  
58 - /**  
59 - * 删除好友时间  
60 - */  
61 - private LocalDateTime deleteTime; 50 +
  51 + // 说明:
  52 + // - 删除状态用 BaseDO 的 deleted 字段表达(0=正常,1=已删除)
  53 + // - 添加时间用 BaseDO 的 createTime;删除时间对应 updateTime
62 54
63 } 55 }
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/ImGroupMessageDO.java
@@ -67,7 +67,6 @@ public class ImGroupMessageDO extends BaseDO { @@ -67,7 +67,6 @@ public class ImGroupMessageDO extends BaseDO {
67 * 消息状态 67 * 消息状态
68 * <p> 68 * <p>
69 * 枚举 {@link ImMessageStatusEnum} 69 * 枚举 {@link ImMessageStatusEnum}
70 - * 群聊状态:NORMAL(0) / RECALL(2)  
71 * 70 *
72 * 为什么没有 READ 状态?与单聊的差异:单聊用 UNREAD/READ 跟踪每条消息状态,群聊用 Redis 存储每个成员的已读位置(游标模型) 71 * 为什么没有 READ 状态?与单聊的差异:单聊用 UNREAD/READ 跟踪每条消息状态,群聊用 Redis 存储每个成员的已读位置(游标模型)
73 */ 72 */
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/ImPrivateMessageDO.java
@@ -62,7 +62,6 @@ public class ImPrivateMessageDO extends BaseDO { @@ -62,7 +62,6 @@ public class ImPrivateMessageDO extends BaseDO {
62 * 消息状态 62 * 消息状态
63 * <p> 63 * <p>
64 * 枚举 {@link ImMessageStatusEnum} 64 * 枚举 {@link ImMessageStatusEnum}
65 - * 私聊状态:UNREAD(0) / READ(1) / RECALL(2)  
66 */ 65 */
67 private Integer status; 66 private Integer status;
68 /** 67 /**
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/friend/ImFriendMapper.java
@@ -5,6 +5,8 @@ import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; @@ -5,6 +5,8 @@ import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
5 import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO; 5 import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO;
6 import org.apache.ibatis.annotations.Mapper; 6 import org.apache.ibatis.annotations.Mapper;
7 7
  8 +import java.util.List;
  9 +
8 /** 10 /**
9 * IM 好友关系 Mapper 11 * IM 好友关系 Mapper
10 * 12 *
@@ -13,17 +15,16 @@ import org.apache.ibatis.annotations.Mapper; @@ -13,17 +15,16 @@ import org.apache.ibatis.annotations.Mapper;
13 @Mapper 15 @Mapper
14 public interface ImFriendMapper extends BaseMapperX<ImFriendDO> { 16 public interface ImFriendMapper extends BaseMapperX<ImFriendDO> {
15 17
16 - /**  
17 - * 查询好友关系  
18 - *  
19 - * @param userId 用户编号  
20 - * @param friendUserId 好友用户编号  
21 - * @return 好友关系  
22 - */  
23 default ImFriendDO selectByUserIdAndFriendUserId(Long userId, Long friendUserId) { 18 default ImFriendDO selectByUserIdAndFriendUserId(Long userId, Long friendUserId) {
24 return selectOne(new LambdaQueryWrapperX<ImFriendDO>() 19 return selectOne(new LambdaQueryWrapperX<ImFriendDO>()
25 .eq(ImFriendDO::getUserId, userId) 20 .eq(ImFriendDO::getUserId, userId)
26 .eq(ImFriendDO::getFriendUserId, friendUserId)); 21 .eq(ImFriendDO::getFriendUserId, friendUserId));
27 } 22 }
28 23
  24 + default List<ImFriendDO> selectListByUserId(Long userId) {
  25 + return selectList(new LambdaQueryWrapperX<ImFriendDO>()
  26 + .eq(ImFriendDO::getUserId, userId)
  27 + .orderByDesc(ImFriendDO::getId));
  28 + }
  29 +
29 } 30 }
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageServiceImpl.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.extra.spring.SpringUtil;
3 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; 4 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
  5 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
4 import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageSendReqVO; 6 import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageSendReqVO;
5 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO; 7 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO;
6 import cn.iocoder.yudao.module.im.dal.mysql.message.ImPrivateMessageMapper; 8 import cn.iocoder.yudao.module.im.dal.mysql.message.ImPrivateMessageMapper;
7 import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum; 9 import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum;
8 import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; 10 import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
9 -import cn.iocoder.yudao.module.im.enums.message.ImWebSocketTypeConstants;  
10 import cn.iocoder.yudao.module.im.service.friend.ImFriendService; 11 import cn.iocoder.yudao.module.im.service.friend.ImFriendService;
11 import cn.iocoder.yudao.module.im.service.sensitiveword.ImSensitiveWordService; 12 import cn.iocoder.yudao.module.im.service.sensitiveword.ImSensitiveWordService;
  13 +import cn.iocoder.yudao.module.im.websocket.ImMessageReadMessage;
  14 +import cn.iocoder.yudao.module.im.websocket.ImMessageReceiptMessage;
  15 +import cn.iocoder.yudao.module.im.websocket.ImMessageRecallMessage;
  16 +import cn.iocoder.yudao.module.im.websocket.ImPrivateMessageSendMessage;
12 import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender; 17 import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender;
13 import jakarta.annotation.Resource; 18 import jakarta.annotation.Resource;
14 import lombok.extern.slf4j.Slf4j; 19 import lombok.extern.slf4j.Slf4j;
  20 +import org.springframework.scheduling.annotation.Async;
15 import org.springframework.stereotype.Service; 21 import org.springframework.stereotype.Service;
16 import org.springframework.validation.annotation.Validated; 22 import org.springframework.validation.annotation.Validated;
17 23
18 import java.time.LocalDateTime; 24 import java.time.LocalDateTime;
19 -import java.util.HashMap;  
20 import java.util.List; 25 import java.util.List;
21 -import java.util.Map;  
22 26
23 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; 27 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
24 import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*; 28 import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*;
@@ -39,12 +43,12 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService { @@ -39,12 +43,12 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService {
39 private static final int MAX_PULL_SIZE = 1000; 43 private static final int MAX_PULL_SIZE = 1000;
40 44
41 @Resource 45 @Resource
42 - private ImPrivateMessageMapper imPrivateMessageMapper; 46 + private ImPrivateMessageMapper privateMessageMapper;
43 47
44 @Resource 48 @Resource
45 - private ImFriendService imFriendService; 49 + private ImFriendService friendService;
46 @Resource 50 @Resource
47 - private ImSensitiveWordService imSensitiveWordService; 51 + private ImSensitiveWordService sensitiveWordService;
48 52
49 @Resource 53 @Resource
50 private WebSocketMessageSender webSocketMessageSender; 54 private WebSocketMessageSender webSocketMessageSender;
@@ -52,30 +56,27 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService { @@ -52,30 +56,27 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService {
52 @Override 56 @Override
53 public ImPrivateMessageDO sendPrivateMessage(Long senderId, ImPrivateMessageSendReqVO reqVO) { 57 public ImPrivateMessageDO sendPrivateMessage(Long senderId, ImPrivateMessageSendReqVO reqVO) {
54 // 1.1 幂等校验:根据 senderId + clientMessageId 查重 58 // 1.1 幂等校验:根据 senderId + clientMessageId 查重
55 - ImPrivateMessageDO existing = imPrivateMessageMapper.selectBySenderIdAndClientMessageId( 59 + ImPrivateMessageDO existing = privateMessageMapper.selectBySenderIdAndClientMessageId(
56 senderId, reqVO.getClientMessageId()); 60 senderId, reqVO.getClientMessageId());
57 if (existing != null) { 61 if (existing != null) {
  62 + log.info("[sendPrivateMessage][幂等命中 senderId({}) clientMessageId({}) 已存在消息({})]",
  63 + senderId, reqVO.getClientMessageId(), existing.getId());
58 return existing; 64 return existing;
59 } 65 }
60 // 1.2 好友校验 66 // 1.2 好友校验
61 - imFriendService.validateIsFriend(senderId, reqVO.getReceiverId()); 67 + friendService.validateFriendExists(senderId, reqVO.getReceiverId());
62 // 1.3 文本消息敏感词过滤 68 // 1.3 文本消息敏感词过滤
63 if (ImMessageTypeEnum.TEXT.getType().equals(reqVO.getType())) { 69 if (ImMessageTypeEnum.TEXT.getType().equals(reqVO.getType())) {
64 - imSensitiveWordService.validateText(reqVO.getContent()); 70 + sensitiveWordService.validateText(reqVO.getContent());
65 } 71 }
66 72
67 // 2. 构建并保存消息 73 // 2. 构建并保存消息
68 - ImPrivateMessageDO message = ImPrivateMessageDO.builder()  
69 - .clientMessageId(reqVO.getClientMessageId())  
70 - .senderId(senderId).receiverId(reqVO.getReceiverId())  
71 - .type(reqVO.getType()).content(reqVO.getContent())  
72 - .status(ImMessageStatusEnum.UNREAD.getStatus())  
73 - .sendTime(LocalDateTime.now())  
74 - .build();  
75 - imPrivateMessageMapper.insert(message);  
76 -  
77 - // 3. WebSocket 推送给接收方  
78 - sendPrivateMessageEvent(message); 74 + ImPrivateMessageDO message = BeanUtils.toBean(reqVO, ImPrivateMessageDO.class, m -> m
  75 + .setSenderId(senderId).setStatus(ImMessageStatusEnum.UNREAD.getStatus()).setSendTime(LocalDateTime.now()));
  76 + privateMessageMapper.insert(message);
  77 +
  78 + // 3. WebSocket 推送给接收方(异步)
  79 + getSelf().sendPrivateMessageEvent(message);
79 return message; 80 return message;
80 } 81 }
81 82
@@ -84,39 +85,37 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService { @@ -84,39 +85,37 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService {
84 if (size > MAX_PULL_SIZE) { 85 if (size > MAX_PULL_SIZE) {
85 throw exception(MESSAGE_PULL_SIZE_EXCEEDED); 86 throw exception(MESSAGE_PULL_SIZE_EXCEEDED);
86 } 87 }
87 - return imPrivateMessageMapper.selectListByMinId(userId, minId, size); 88 + return privateMessageMapper.selectListByMinId(userId, minId, size);
88 } 89 }
89 90
90 // DONE @芋艿:已 review,逻辑正确 91 // DONE @芋艿:已 review,逻辑正确
91 @Override 92 @Override
92 public void readPrivateMessages(Long userId, Long friendId) { 93 public void readPrivateMessages(Long userId, Long friendId) {
93 // 1. 批量更新消息状态为已读 94 // 1. 批量更新消息状态为已读
94 - int updated = imPrivateMessageMapper.updateStatusToRead(userId, friendId); 95 + int updated = privateMessageMapper.updateStatusToRead(userId, friendId);
95 if (updated == 0) { 96 if (updated == 0) {
96 return; // 没有需要更新的消息 97 return; // 没有需要更新的消息
97 } 98 }
98 // 2. 发送 READ 事件给自己的其他终端(多端同步) 99 // 2. 发送 READ 事件给自己的其他终端(多端同步)
99 - Map<String, Object> readEvent = new HashMap<>();  
100 - // DONE @AI:需要整理下,相关的 websocket DTO。结论:当前用 Map 更灵活,多场景复用,后续可抽取 DTO  
101 - readEvent.put("friendId", friendId.toString());  
102 - readEvent.put("messageScene", "private"); 100 + ImMessageReadMessage readMessage = new ImMessageReadMessage()
  101 + .setFriendId(friendId.toString())
  102 + .setMessageScene("private");
103 webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), userId, 103 webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), userId,
104 - ImWebSocketTypeConstants.READ, readEvent); 104 + ImMessageReadMessage.TYPE, readMessage);
105 105
106 // 3. 发送 RECEIPT 事件给对方(已读回执) 106 // 3. 发送 RECEIPT 事件给对方(已读回执)
107 - Map<String, Object> receiptEvent = new HashMap<>();  
108 - // DONE @AI:需要整理下,相关的 websocket DTO。结论:当前用 Map 更灵活,多场景复用,后续可抽取 DTO  
109 - receiptEvent.put("userId", userId.toString());  
110 - receiptEvent.put("messageScene", "private"); 107 + ImMessageReceiptMessage receiptMessage = new ImMessageReceiptMessage()
  108 + .setUserId(userId.toString())
  109 + .setMessageScene("private");
111 webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), friendId, 110 webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), friendId,
112 - ImWebSocketTypeConstants.RECEIPT, receiptEvent); 111 + ImMessageReceiptMessage.TYPE, receiptMessage);
113 } 112 }
114 113
115 // DONE @芋艿:已 review,逻辑正确 114 // DONE @芋艿:已 review,逻辑正确
116 @Override 115 @Override
117 public void recallPrivateMessage(Long userId, Long messageId) { 116 public void recallPrivateMessage(Long userId, Long messageId) {
118 // 1.1 校验消息存在 117 // 1.1 校验消息存在
119 - ImPrivateMessageDO message = imPrivateMessageMapper.selectById(messageId); 118 + ImPrivateMessageDO message = privateMessageMapper.selectById(messageId);
120 if (message == null) { 119 if (message == null) {
121 throw exception(MESSAGE_NOT_EXISTS); 120 throw exception(MESSAGE_NOT_EXISTS);
122 } 121 }
@@ -131,44 +130,38 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService { @@ -131,44 +130,38 @@ public class ImPrivateMessageServiceImpl implements ImPrivateMessageService {
131 } 130 }
132 131
133 // 2. 更新消息状态为撤回 132 // 2. 更新消息状态为撤回
134 - imPrivateMessageMapper.updateById(new ImPrivateMessageDO().setId(messageId) 133 + privateMessageMapper.updateById(new ImPrivateMessageDO().setId(messageId)
135 .setStatus(ImMessageStatusEnum.RECALL.getStatus())); 134 .setStatus(ImMessageStatusEnum.RECALL.getStatus()));
136 135
137 - // 3.1 发送 RECALL 事件给接收方  
138 - // DONE @AI:需要整理下,相关的 websocket DTO。结论:当前用 Map 更灵活,多场景复用,后续可抽取 DTO  
139 - Map<String, Object> recallEvent = new HashMap<>();  
140 - recallEvent.put("messageId", messageId.toString());  
141 - recallEvent.put("messageScene", "private");  
142 - recallEvent.put("senderId", userId.toString()); 136 + // 3. 发送 RECALL 事件
  137 + ImMessageRecallMessage recallMessage = new ImMessageRecallMessage()
  138 + .setMessageId(messageId.toString())
  139 + .setSenderId(userId.toString())
  140 + .setMessageScene("private");
  141 + // 3.1 通知接收方
143 webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), message.getReceiverId(), 142 webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), message.getReceiverId(),
144 - ImWebSocketTypeConstants.RECALL, recallEvent);  
145 - // 3.2 也通知发送方自己的其他终端  
146 - // DONE @AI:需要整理下,相关的 websocket DTO。结论:当前用 Map 更灵活,多场景复用,后续可抽取 DTO 143 + ImMessageRecallMessage.TYPE, recallMessage);
  144 + // 3.2 通知发送方自己的其他终端
147 webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), userId, 145 webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), userId,
148 - ImWebSocketTypeConstants.RECALL, recallEvent); 146 + ImMessageRecallMessage.TYPE, recallMessage);
149 } 147 }
150 148
151 /** 149 /**
152 * 发送私聊消息 WebSocket 事件 150 * 发送私聊消息 WebSocket 事件
153 */ 151 */
154 - private void sendPrivateMessageEvent(ImPrivateMessageDO message) {  
155 - Map<String, Object> event = new HashMap<>();  
156 - // DONE @AI:需要整理下,相关的 websocket DTO。结论:当前用 Map 更灵活,多场景复用,后续可抽取 DTO  
157 - event.put("id", message.getId().toString());  
158 - event.put("clientMessageId", message.getClientMessageId());  
159 - event.put("senderId", message.getSenderId().toString());  
160 - event.put("receiverId", message.getReceiverId().toString());  
161 - event.put("type", message.getType());  
162 - event.put("content", message.getContent());  
163 - event.put("status", message.getStatus());  
164 - event.put("sendTime", message.getSendTime().toString());  
165 - event.put("messageScene", "private"); 152 + @Async
  153 + public void sendPrivateMessageEvent(ImPrivateMessageDO message) {
  154 + ImPrivateMessageSendMessage websocketMessage = BeanUtils.toBean(message, ImPrivateMessageSendMessage.class);
166 // 推送给接收方 155 // 推送给接收方
167 webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), message.getReceiverId(), 156 webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), message.getReceiverId(),
168 - ImWebSocketTypeConstants.PRIVATE_MESSAGE, event); 157 + ImPrivateMessageSendMessage.TYPE, websocketMessage);
169 // 推送给发送方自己的其他终端(多端同步) 158 // 推送给发送方自己的其他终端(多端同步)
170 webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), message.getSenderId(), 159 webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), message.getSenderId(),
171 - ImWebSocketTypeConstants.PRIVATE_MESSAGE, event); 160 + ImPrivateMessageSendMessage.TYPE, websocketMessage);
  161 + }
  162 +
  163 + private ImPrivateMessageServiceImpl getSelf() {
  164 + return SpringUtil.getBean(getClass());
172 } 165 }
173 166
174 } 167 }
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/sensitiveword/ImSensitiveWordService.java
@@ -9,8 +9,6 @@ public interface ImSensitiveWordService { @@ -9,8 +9,6 @@ public interface ImSensitiveWordService {
9 9
10 /** 10 /**
11 * 校验文本是否包含敏感词 11 * 校验文本是否包含敏感词
12 - * <p>  
13 - * 如果包含,直接抛出业务异常  
14 * 12 *
15 * @param text 待校验文本 13 * @param text 待校验文本
16 */ 14 */
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/websocket/ImGroupMessageSendMessage.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.websocket;
  2 +
  3 +import lombok.Data;
  4 +import lombok.experimental.Accessors;
  5 +
  6 +import java.time.LocalDateTime;
  7 +import java.util.List;
  8 +
  9 +/**
  10 + * IM 群聊消息 WebSocket 推送消息
  11 + *
  12 + * @author 芋道源码
  13 + */
  14 +@Data
  15 +@Accessors(chain = true)
  16 +public class ImGroupMessageSendMessage {
  17 +
  18 + public static final String TYPE = "im-group-message-send";
  19 +
  20 + /**
  21 + * 消息编号
  22 + */
  23 + private Long id;
  24 + /**
  25 + * 客户端消息编号
  26 + */
  27 + private String clientMessageId;
  28 + /**
  29 + * 发送人编号
  30 + */
  31 + private Long senderId;
  32 + /**
  33 + * 群编号
  34 + */
  35 + private Long groupId;
  36 + /**
  37 + * 消息类型
  38 + */
  39 + private Integer type;
  40 + /**
  41 + * 消息内容
  42 + */
  43 + private String content;
  44 + /**
  45 + * 消息状态
  46 + */
  47 + private Integer status;
  48 + /**
  49 + * 发送时间
  50 + */
  51 + private LocalDateTime sendTime;
  52 + /**
  53 + * @ 的用户编号列表
  54 + */
  55 + private List<Long> atUserIds;
  56 + /**
  57 + * 定向接收用户编号列表
  58 + */
  59 + private List<Long> receiverUserIds;
  60 + /**
  61 + * 群回执状态
  62 + */
  63 + private Integer receiptStatus;
  64 +
  65 +}
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/websocket/ImMessageReadMessage.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.websocket;
  2 +
  3 +import lombok.Data;
  4 +import lombok.experimental.Accessors;
  5 +
  6 +/**
  7 + * IM 消息已读 WebSocket 推送消息
  8 + * <p>
  9 + * 用于多端同步:通知自己的其他终端"我已经读了某个会话"
  10 + *
  11 + * @author 芋道源码
  12 + */
  13 +@Data
  14 +@Accessors(chain = true)
  15 +public class ImMessageReadMessage {
  16 +
  17 + public static final String TYPE = "im-message-read";
  18 +
  19 + /**
  20 + * 私聊场景:好友编号
  21 + */
  22 + private String friendId;
  23 + /**
  24 + * 群聊场景:群编号
  25 + */
  26 + private String groupId;
  27 + /**
  28 + * 消息场景:private / group
  29 + */
  30 + private String messageScene;
  31 +
  32 +}
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/websocket/ImMessageRecallMessage.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.websocket;
  2 +
  3 +import lombok.Data;
  4 +import lombok.experimental.Accessors;
  5 +
  6 +/**
  7 + * IM 消息撤回 WebSocket 推送消息
  8 + *
  9 + * @author 芋道源码
  10 + */
  11 +@Data
  12 +@Accessors(chain = true)
  13 +public class ImMessageRecallMessage {
  14 +
  15 + public static final String TYPE = "im-message-recall";
  16 +
  17 + /**
  18 + * 消息编号
  19 + */
  20 + private String messageId;
  21 + /**
  22 + * 发送人编号
  23 + */
  24 + private String senderId;
  25 + /**
  26 + * 群聊场景:群编号
  27 + */
  28 + private String groupId;
  29 + /**
  30 + * 消息场景:private / group
  31 + */
  32 + private String messageScene;
  33 +
  34 +}
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/websocket/ImMessageReceiptMessage.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.websocket;
  2 +
  3 +import lombok.Data;
  4 +import lombok.experimental.Accessors;
  5 +
  6 +/**
  7 + * IM 已读回执 WebSocket 推送消息
  8 + * <p>
  9 + * 私聊:通知对方"我已读了你的消息"
  10 + * 群聊:广播回执状态和已读人数
  11 + *
  12 + * @author 芋道源码
  13 + */
  14 +@Data
  15 +@Accessors(chain = true)
  16 +public class ImMessageReceiptMessage {
  17 +
  18 + public static final String TYPE = "im-message-receipt";
  19 +
  20 + // ========== 私聊场景 ==========
  21 +
  22 + /**
  23 + * 私聊场景:已读方的用户编号
  24 + */
  25 + private String userId;
  26 +
  27 + // ========== 群聊场景 ==========
  28 +
  29 + /**
  30 + * 群聊场景:消息编号
  31 + */
  32 + private String messageId;
  33 + /**
  34 + * 群聊场景:群编号
  35 + */
  36 + private String groupId;
  37 + /**
  38 + * 群聊场景:已读人数
  39 + */
  40 + private Integer readCount;
  41 + /**
  42 + * 群聊场景:回执状态
  43 + */
  44 + private Integer receiptStatus;
  45 +
  46 + // ========== 通用 ==========
  47 +
  48 + /**
  49 + * 消息场景:private / group
  50 + */
  51 + private String messageScene;
  52 +
  53 +}
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/websocket/ImPrivateMessageSendMessage.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.websocket;
  2 +
  3 +import lombok.Data;
  4 +import lombok.experimental.Accessors;
  5 +
  6 +import java.time.LocalDateTime;
  7 +
  8 +/**
  9 + * IM 私聊消息 WebSocket 推送消息
  10 + *
  11 + * @author 芋道源码
  12 + */
  13 +@Data
  14 +@Accessors(chain = true)
  15 +public class ImPrivateMessageSendMessage {
  16 +
  17 + public static final String TYPE = "im-private-message-send";
  18 +
  19 + /**
  20 + * 消息编号
  21 + */
  22 + private Long id;
  23 + /**
  24 + * 客户端消息编号
  25 + */
  26 + private String clientMessageId;
  27 + /**
  28 + * 发送人编号
  29 + */
  30 + private Long senderId;
  31 + /**
  32 + * 接收人编号
  33 + */
  34 + private Long receiverId;
  35 + /**
  36 + * 消息类型
  37 + */
  38 + private Integer type;
  39 + /**
  40 + * 消息内容
  41 + */
  42 + private String content;
  43 + /**
  44 + * 消息状态
  45 + */
  46 + private Integer status;
  47 + /**
  48 + * 发送时间
  49 + */
  50 + private LocalDateTime sendTime;
  51 +
  52 +}
yudao-module-im/yudao-module-im-biz/src/test/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageServiceImplTest.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.extra.spring.SpringUtil;
3 import cn.iocoder.yudao.framework.common.exception.ServiceException; 4 import cn.iocoder.yudao.framework.common.exception.ServiceException;
4 import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageSendReqVO; 5 import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageSendReqVO;
5 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO; 6 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO;
@@ -12,6 +13,7 @@ import org.junit.jupiter.api.Test; @@ -12,6 +13,7 @@ import org.junit.jupiter.api.Test;
12 import org.junit.jupiter.api.extension.ExtendWith; 13 import org.junit.jupiter.api.extension.ExtendWith;
13 import org.mockito.InjectMocks; 14 import org.mockito.InjectMocks;
14 import org.mockito.Mock; 15 import org.mockito.Mock;
  16 +import org.mockito.MockedStatic;
15 import org.mockito.junit.jupiter.MockitoExtension; 17 import org.mockito.junit.jupiter.MockitoExtension;
16 18
17 import java.time.LocalDateTime; 19 import java.time.LocalDateTime;
@@ -55,32 +57,37 @@ public class ImPrivateMessageServiceImplTest { @@ -55,32 +57,37 @@ public class ImPrivateMessageServiceImplTest {
55 57
56 @Test 58 @Test
57 public void testSendMessage_success() { 59 public void testSendMessage_success() {
58 - // 准备  
59 - ImPrivateMessageSendReqVO reqVO = buildSendReqVO();  
60 - when(imPrivateMessageMapper.selectBySenderIdAndClientMessageId(1L, "test-uuid-001"))  
61 - .thenReturn(null);  
62 - when(imPrivateMessageMapper.insert(any(ImPrivateMessageDO.class))).thenAnswer(invocation -> {  
63 - ImPrivateMessageDO msg = invocation.getArgument(0);  
64 - msg.setId(99L);  
65 - return 1;  
66 - });  
67 -  
68 - // 调用  
69 - ImPrivateMessageDO result = privateMessageService.sendPrivateMessage(1L, reqVO);  
70 -  
71 - // 断言  
72 - assertNotNull(result);  
73 - assertEquals(1L, result.getSenderId());  
74 - assertEquals(2L, result.getReceiverId());  
75 - assertEquals(0, result.getType());  
76 - assertEquals(ImMessageStatusEnum.UNREAD.getStatus(), result.getStatus());  
77 - assertNotNull(result.getSendTime());  
78 -  
79 - // 验证调用  
80 - verify(imFriendService).validateIsFriend(1L, 2L);  
81 - verify(imSensitiveWordService).validateText(reqVO.getContent());  
82 - verify(imPrivateMessageMapper).insert(any(ImPrivateMessageDO.class));  
83 - verify(webSocketMessageSender, times(2)).sendObject(anyInt(), anyLong(), anyString(), any()); 60 + try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
  61 + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImPrivateMessageServiceImpl.class)))
  62 + .thenReturn(privateMessageService);
  63 +
  64 + // 准备
  65 + ImPrivateMessageSendReqVO reqVO = buildSendReqVO();
  66 + when(imPrivateMessageMapper.selectBySenderIdAndClientMessageId(1L, "test-uuid-001"))
  67 + .thenReturn(null);
  68 + when(imPrivateMessageMapper.insert(any(ImPrivateMessageDO.class))).thenAnswer(invocation -> {
  69 + ImPrivateMessageDO msg = invocation.getArgument(0);
  70 + msg.setId(99L);
  71 + return 1;
  72 + });
  73 +
  74 + // 调用
  75 + ImPrivateMessageDO result = privateMessageService.sendPrivateMessage(1L, reqVO);
  76 +
  77 + // 断言
  78 + assertNotNull(result);
  79 + assertEquals(1L, result.getSenderId());
  80 + assertEquals(2L, result.getReceiverId());
  81 + assertEquals(0, result.getType());
  82 + assertEquals(ImMessageStatusEnum.UNREAD.getStatus(), result.getStatus());
  83 + assertNotNull(result.getSendTime());
  84 +
  85 + // 验证调用
  86 + verify(imFriendService).validateFriendExists(1L, 2L);
  87 + verify(imSensitiveWordService).validateText(reqVO.getContent());
  88 + verify(imPrivateMessageMapper).insert(any(ImPrivateMessageDO.class));
  89 + verify(webSocketMessageSender, times(2)).sendObject(anyInt(), anyLong(), anyString(), any());
  90 + }
84 } 91 }
85 92
86 @Test 93 @Test
@@ -110,7 +117,7 @@ public class ImPrivateMessageServiceImplTest { @@ -110,7 +117,7 @@ public class ImPrivateMessageServiceImplTest {
110 when(imPrivateMessageMapper.selectBySenderIdAndClientMessageId(1L, "test-uuid-001")) 117 when(imPrivateMessageMapper.selectBySenderIdAndClientMessageId(1L, "test-uuid-001"))
111 .thenReturn(null); 118 .thenReturn(null);
112 doThrow(new ServiceException(FRIEND_NOT_FRIEND)) 119 doThrow(new ServiceException(FRIEND_NOT_FRIEND))
113 - .when(imFriendService).validateIsFriend(1L, 2L); 120 + .when(imFriendService).validateFriendExists(1L, 2L);
114 121
115 // 调用并断言 122 // 调用并断言
116 ServiceException exception = assertThrows(ServiceException.class, 123 ServiceException exception = assertThrows(ServiceException.class,
yudao-module-im/yudao-module-im-biz/src/test/java/cn/iocoder/yudao/module/im/service/message/ImWebSocketEventTest.java
@@ -3,7 +3,10 @@ package cn.iocoder.yudao.module.im.service.message; @@ -3,7 +3,10 @@ package cn.iocoder.yudao.module.im.service.message;
3 import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageSendReqVO; 3 import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageSendReqVO;
4 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO; 4 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO;
5 import cn.iocoder.yudao.module.im.dal.mysql.message.ImPrivateMessageMapper; 5 import cn.iocoder.yudao.module.im.dal.mysql.message.ImPrivateMessageMapper;
6 -import cn.iocoder.yudao.module.im.enums.message.ImWebSocketTypeConstants; 6 +import cn.iocoder.yudao.module.im.websocket.ImMessageReadMessage;
  7 +import cn.iocoder.yudao.module.im.websocket.ImMessageReceiptMessage;
  8 +import cn.iocoder.yudao.module.im.websocket.ImMessageRecallMessage;
  9 +import cn.iocoder.yudao.module.im.websocket.ImPrivateMessageSendMessage;
7 import cn.iocoder.yudao.module.im.service.friend.ImFriendService; 10 import cn.iocoder.yudao.module.im.service.friend.ImFriendService;
8 import cn.iocoder.yudao.module.im.service.sensitiveword.ImSensitiveWordService; 11 import cn.iocoder.yudao.module.im.service.sensitiveword.ImSensitiveWordService;
9 import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender; 12 import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender;
@@ -14,8 +17,6 @@ import org.mockito.InjectMocks; @@ -14,8 +17,6 @@ import org.mockito.InjectMocks;
14 import org.mockito.Mock; 17 import org.mockito.Mock;
15 import org.mockito.junit.jupiter.MockitoExtension; 18 import org.mockito.junit.jupiter.MockitoExtension;
16 19
17 -import java.util.Map;  
18 -  
19 import static org.junit.jupiter.api.Assertions.*; 20 import static org.junit.jupiter.api.Assertions.*;
20 import static org.mockito.ArgumentMatchers.*; 21 import static org.mockito.ArgumentMatchers.*;
21 import static org.mockito.Mockito.*; 22 import static org.mockito.Mockito.*;
@@ -69,16 +70,14 @@ public class ImWebSocketEventTest { @@ -69,16 +70,14 @@ public class ImWebSocketEventTest {
69 typeCaptor.capture(), contentCaptor.capture()); 70 typeCaptor.capture(), contentCaptor.capture());
70 71
71 // 验证消息事件类型 72 // 验证消息事件类型
72 - assertEquals(ImWebSocketTypeConstants.PRIVATE_MESSAGE, typeCaptor.getAllValues().get(0));  
73 - assertEquals(ImWebSocketTypeConstants.PRIVATE_MESSAGE, typeCaptor.getAllValues().get(1));  
74 -  
75 - // 验证 payload 中 id 字段按 String 返回  
76 - @SuppressWarnings("unchecked")  
77 - Map<String, Object> payload = (Map<String, Object>) contentCaptor.getAllValues().get(0);  
78 - assertTrue(payload.get("id") instanceof String, "id 应为 String 类型");  
79 - assertTrue(payload.get("senderId") instanceof String, "senderId 应为 String 类型");  
80 - assertTrue(payload.get("receiverId") instanceof String, "receiverId 应为 String 类型");  
81 - assertEquals("private", payload.get("messageScene")); 73 + assertEquals(ImPrivateMessageSendMessage.TYPE, typeCaptor.getAllValues().get(0));
  74 + assertEquals(ImPrivateMessageSendMessage.TYPE, typeCaptor.getAllValues().get(1));
  75 +
  76 + // 验证 payload 是 DTO 类型
  77 + ImPrivateMessageSendMessage payload = (ImPrivateMessageSendMessage) contentCaptor.getAllValues().get(0);
  78 + assertNotNull(payload.getId(), "id 不应为空");
  79 + assertEquals("1", payload.getSenderId());
  80 + assertEquals("2", payload.getReceiverId());
82 } 81 }
83 82
84 @Test 83 @Test
@@ -96,10 +95,10 @@ public class ImWebSocketEventTest { @@ -96,10 +95,10 @@ public class ImWebSocketEventTest {
96 typeCaptor.capture(), any()); 95 typeCaptor.capture(), any());
97 96
98 // 第一次:发给自己的 READ 事件 97 // 第一次:发给自己的 READ 事件
99 - assertEquals(ImWebSocketTypeConstants.READ, typeCaptor.getAllValues().get(0)); 98 + assertEquals(ImMessageReadMessage.TYPE, typeCaptor.getAllValues().get(0));
100 assertEquals(1L, userCaptor.getAllValues().get(0)); 99 assertEquals(1L, userCaptor.getAllValues().get(0));
101 // 第二次:发给对方的 RECEIPT 事件 100 // 第二次:发给对方的 RECEIPT 事件
102 - assertEquals(ImWebSocketTypeConstants.RECEIPT, typeCaptor.getAllValues().get(1)); 101 + assertEquals(ImMessageReceiptMessage.TYPE, typeCaptor.getAllValues().get(1));
103 assertEquals(2L, userCaptor.getAllValues().get(1)); 102 assertEquals(2L, userCaptor.getAllValues().get(1));
104 } 103 }
105 104
@@ -121,13 +120,12 @@ public class ImWebSocketEventTest { @@ -121,13 +120,12 @@ public class ImWebSocketEventTest {
121 typeCaptor.capture(), contentCaptor.capture()); 120 typeCaptor.capture(), contentCaptor.capture());
122 121
123 // 验证 RECALL 事件类型 122 // 验证 RECALL 事件类型
124 - assertEquals(ImWebSocketTypeConstants.RECALL, typeCaptor.getAllValues().get(0)); 123 + assertEquals(ImMessageRecallMessage.TYPE, typeCaptor.getAllValues().get(0));
125 124
126 // 验证 payload 125 // 验证 payload
127 - @SuppressWarnings("unchecked")  
128 - Map<String, Object> payload = (Map<String, Object>) contentCaptor.getAllValues().get(0);  
129 - assertEquals("10", payload.get("messageId"));  
130 - assertEquals("private", payload.get("messageScene")); 126 + ImMessageRecallMessage payload = (ImMessageRecallMessage) contentCaptor.getAllValues().get(0);
  127 + assertEquals("10", payload.getMessageId());
  128 + assertEquals("private", payload.getMessageScene());
131 } 129 }
132 130
133 } 131 }
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/user/AdminUserApi.java
@@ -56,6 +56,14 @@ public interface AdminUserApi { @@ -56,6 +56,14 @@ public interface AdminUserApi {
56 List<AdminUserRespDTO> getUserListByPostIds(Collection<Long> postIds); 56 List<AdminUserRespDTO> getUserListByPostIds(Collection<Long> postIds);
57 57
58 /** 58 /**
  59 + * 根据昵称模糊搜索用户
  60 + *
  61 + * @param nickname 昵称关键词
  62 + * @return 用户列表
  63 + */
  64 + List<AdminUserRespDTO> getUserListByNickname(String nickname);
  65 +
  66 + /**
59 * 获得用户 Map 67 * 获得用户 Map
60 * 68 *
61 * @param ids 用户编号数组 69 * @param ids 用户编号数组
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/user/AdminUserApiImpl.java
@@ -81,6 +81,12 @@ public class AdminUserApiImpl implements AdminUserApi { @@ -81,6 +81,12 @@ public class AdminUserApiImpl implements AdminUserApi {
81 } 81 }
82 82
83 @Override 83 @Override
  84 + public List<AdminUserRespDTO> getUserListByNickname(String nickname) {
  85 + List<AdminUserDO> users = userService.getUserListByNickname(nickname);
  86 + return BeanUtils.toBean(users, AdminUserRespDTO.class);
  87 + }
  88 +
  89 + @Override
84 public void validateUserList(Collection<Long> ids) { 90 public void validateUserList(Collection<Long> ids) {
85 userService.validateUserList(ids); 91 userService.validateUserList(ids);
86 } 92 }