Commit d6d90b2bd54bd52230b836ee1bf63a6f3a38beae
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<Integer> { | @@ -31,7 +29,6 @@ public enum ImMessageStatusEnum implements ArrayValuable<Integer> { | ||
| 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
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 | } |