Commit 2d277765e7002b4b5cfc26bb089ae2da12ea9299
1 parent
98431230
📝 feat(im): 添加 IM 相关服务接口和实现,更新消息处理逻辑,增强敏感词校验功能
Showing
55 changed files
with
2831 additions
and
272 deletions
sql/mysql/im/im20240316.sql deleted
| 1 | -/* | ||
| 2 | - Navicat Premium Data Transfer | ||
| 3 | - | ||
| 4 | - Source Server : mysql8_root | ||
| 5 | - Source Server Type : MySQL | ||
| 6 | - Source Server Version : 80200 | ||
| 7 | - Source Host : chaojiniu.top:23306 | ||
| 8 | - Source Schema : ruoyi-vue-pro | ||
| 9 | - | ||
| 10 | - Target Server Type : MySQL | ||
| 11 | - Target Server Version : 80200 | ||
| 12 | - File Encoding : 65001 | ||
| 13 | - | ||
| 14 | - Date: 16/03/2024 15:45:29 | ||
| 15 | -*/ | ||
| 16 | - | ||
| 17 | -SET NAMES utf8mb4; | ||
| 18 | -SET FOREIGN_KEY_CHECKS = 0; | ||
| 19 | - | ||
| 20 | --- ---------------------------- | ||
| 21 | --- Table structure for im_conversation | ||
| 22 | --- ---------------------------- | ||
| 23 | -DROP TABLE IF EXISTS `im_conversation`; | ||
| 24 | -CREATE TABLE `im_conversation` ( | ||
| 25 | - `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号', | ||
| 26 | - `user_id` bigint NOT NULL COMMENT '所属用户', | ||
| 27 | - `conversation_type` tinyint NOT NULL COMMENT '类型:1 单聊;2 群聊;4 通知会话(预留)', | ||
| 28 | - `target_id` bigint NOT NULL COMMENT '单聊时,用户编号;群聊时,群编号', | ||
| 29 | - `no` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '会话标志 单聊:s_{userId}_{targetId},需要排序 userId 和 targetId 群聊:g_groupId', | ||
| 30 | - `pinned` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否置顶 0否 1是', | ||
| 31 | - `last_read_time` datetime DEFAULT NULL COMMENT '最后已读时间', | ||
| 32 | - `creator` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '创建者', | ||
| 33 | - `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', | ||
| 34 | - `updater` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '更新者', | ||
| 35 | - `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', | ||
| 36 | - `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', | ||
| 37 | - `tenant_id` bigint NOT NULL DEFAULT '0' COMMENT '租户编号', | ||
| 38 | - PRIMARY KEY (`id`) | ||
| 39 | -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='会话表'; | ||
| 40 | - | ||
| 41 | --- ---------------------------- | ||
| 42 | --- Table structure for im_group | ||
| 43 | --- ---------------------------- | ||
| 44 | -DROP TABLE IF EXISTS `im_group`; | ||
| 45 | -CREATE TABLE `im_group` ( | ||
| 46 | - `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号', | ||
| 47 | - `group_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '群名字', | ||
| 48 | - `owner_id` bigint NOT NULL COMMENT '群主id', | ||
| 49 | - `head_image` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '群头像', | ||
| 50 | - `head_image_thumb` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '群头像缩略图', | ||
| 51 | - `notice` varchar(1024) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '群公告', | ||
| 52 | - `remark` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '群备注', | ||
| 53 | - `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '创建者', | ||
| 54 | - `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', | ||
| 55 | - `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '更新者', | ||
| 56 | - `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', | ||
| 57 | - `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', | ||
| 58 | - `tenant_id` bigint NOT NULL DEFAULT '0' COMMENT '租户编号', | ||
| 59 | - PRIMARY KEY (`id`) | ||
| 60 | -) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='群'; | ||
| 61 | - | ||
| 62 | --- ---------------------------- | ||
| 63 | --- Table structure for im_group_member | ||
| 64 | --- ---------------------------- | ||
| 65 | -DROP TABLE IF EXISTS `im_group_member`; | ||
| 66 | -CREATE TABLE `im_group_member` ( | ||
| 67 | - `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号', | ||
| 68 | - `group_id` bigint DEFAULT NULL COMMENT '群 id', | ||
| 69 | - `user_id` bigint NOT NULL COMMENT '用户id', | ||
| 70 | - `nickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '昵称', | ||
| 71 | - `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '头像', | ||
| 72 | - `alias_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '组内显示名称', | ||
| 73 | - `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注', | ||
| 74 | - `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '创建者', | ||
| 75 | - `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', | ||
| 76 | - `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '更新者', | ||
| 77 | - `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', | ||
| 78 | - `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', | ||
| 79 | - `tenant_id` bigint NOT NULL DEFAULT '0' COMMENT '租户编号', | ||
| 80 | - PRIMARY KEY (`id`) | ||
| 81 | -) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='群成员'; | ||
| 82 | - | ||
| 83 | --- ---------------------------- | ||
| 84 | --- Table structure for im_inbox | ||
| 85 | --- ---------------------------- | ||
| 86 | -DROP TABLE IF EXISTS `im_inbox`; | ||
| 87 | -CREATE TABLE `im_inbox` ( | ||
| 88 | - `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号', | ||
| 89 | - `user_id` bigint NOT NULL COMMENT '用户编号', | ||
| 90 | - `message_id` bigint NOT NULL COMMENT '消息编号', | ||
| 91 | - `sequence` bigint NOT NULL COMMENT '序号,按照 user 递增', | ||
| 92 | - `creator` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '创建者', | ||
| 93 | - `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', | ||
| 94 | - `updater` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '更新者', | ||
| 95 | - `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', | ||
| 96 | - `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', | ||
| 97 | - `tenant_id` bigint NOT NULL DEFAULT '0' COMMENT '租户编号', | ||
| 98 | - PRIMARY KEY (`id`) | ||
| 99 | -) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='收件箱表'; | ||
| 100 | - | ||
| 101 | --- ---------------------------- | ||
| 102 | --- Table structure for im_message | ||
| 103 | --- ---------------------------- | ||
| 104 | -DROP TABLE IF EXISTS `im_message`; | ||
| 105 | -CREATE TABLE `im_message` ( | ||
| 106 | - `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号', | ||
| 107 | - `client_message_id` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '客户端消息编号 uuid,用于排重', | ||
| 108 | - `sender_id` bigint NOT NULL COMMENT '发送人编号', | ||
| 109 | - `receiver_id` bigint NOT NULL COMMENT '接收人编号', | ||
| 110 | - `sender_nickname` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '发送人昵称', | ||
| 111 | - `sender_avatar` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '发送人头像', | ||
| 112 | - `conversation_type` tinyint NOT NULL COMMENT '会话类型', | ||
| 113 | - `conversation_no` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '会话标志', | ||
| 114 | - `content_type` tinyint NOT NULL COMMENT '消息类型', | ||
| 115 | - `content` text COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '消息内容', | ||
| 116 | - `send_time` datetime DEFAULT NULL COMMENT '发送时间', | ||
| 117 | - `send_from` tinyint NOT NULL COMMENT '消息来源 100-用户发送;200-系统发送(一般是通知);', | ||
| 118 | - `message_status` tinyint DEFAULT NULL COMMENT '消息状态', | ||
| 119 | - `creator` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '创建者', | ||
| 120 | - `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', | ||
| 121 | - `updater` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '更新者', | ||
| 122 | - `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', | ||
| 123 | - `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', | ||
| 124 | - `tenant_id` bigint NOT NULL DEFAULT '0' COMMENT '租户编号', | ||
| 125 | - PRIMARY KEY (`id`) | ||
| 126 | -) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='消息表'; | ||
| 127 | - | ||
| 128 | -SET FOREIGN_KEY_CHECKS = 1; |
yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/enums/ErrorCodeConstants.java
| @@ -18,11 +18,27 @@ public interface ErrorCodeConstants { | @@ -18,11 +18,27 @@ public interface ErrorCodeConstants { | ||
| 18 | // ========== 消息 (1-040-300-000) ========== | 18 | // ========== 消息 (1-040-300-000) ========== |
| 19 | ErrorCode MESSAGE_NOT_EXISTS = new ErrorCode(1_040_300_000, "消息不存在"); | 19 | ErrorCode MESSAGE_NOT_EXISTS = new ErrorCode(1_040_300_000, "消息不存在"); |
| 20 | ErrorCode MESSAGE_RECEIVER_NOT_EXISTS = new ErrorCode(1_040_300_001, "接收人不存在"); | 20 | ErrorCode MESSAGE_RECEIVER_NOT_EXISTS = new ErrorCode(1_040_300_001, "接收人不存在"); |
| 21 | + ErrorCode MESSAGE_RECALL_DENIED = new ErrorCode(1_040_300_002, "只能撤回自己发送的消息"); | ||
| 22 | + ErrorCode MESSAGE_ALREADY_RECALLED = new ErrorCode(1_040_300_003, "消息已撤回"); | ||
| 23 | + ErrorCode MESSAGE_SENSITIVE_WORD_BLOCKED = new ErrorCode(1_040_300_004, "消息包含敏感词,无法发送"); | ||
| 24 | + ErrorCode MESSAGE_PULL_SIZE_EXCEEDED = new ErrorCode(1_040_300_005, "单次拉取消息数量超出限制"); | ||
| 25 | + ErrorCode MESSAGE_CLIENT_ID_DUPLICATE = new ErrorCode(1_040_300_006, "消息已发送,请勿重复提交"); | ||
| 21 | 26 | ||
| 22 | // ========== 群 (1-040-400-000) ========== | 27 | // ========== 群 (1-040-400-000) ========== |
| 23 | ErrorCode GROUP_NOT_EXISTS = new ErrorCode(1_040_400_000, "群不存在"); | 28 | ErrorCode GROUP_NOT_EXISTS = new ErrorCode(1_040_400_000, "群不存在"); |
| 29 | + ErrorCode GROUP_BANNED = new ErrorCode(1_040_400_001, "群已被封禁"); | ||
| 30 | + ErrorCode GROUP_DISSOLVED = new ErrorCode(1_040_400_002, "群已解散"); | ||
| 24 | 31 | ||
| 25 | // ========== 群成员 (1-040-500-000) ========== | 32 | // ========== 群成员 (1-040-500-000) ========== |
| 26 | ErrorCode GROUP_MEMBER_NOT_EXISTS = new ErrorCode(1_040_500_000, "群成员不存在"); | 33 | ErrorCode GROUP_MEMBER_NOT_EXISTS = new ErrorCode(1_040_500_000, "群成员不存在"); |
| 34 | + ErrorCode GROUP_MEMBER_NOT_IN_GROUP = new ErrorCode(1_040_500_001, "您已不在该群中"); | ||
| 35 | + ErrorCode GROUP_MEMBER_ALREADY_QUIT = new ErrorCode(1_040_500_002, "您已退出该群"); | ||
| 36 | + | ||
| 37 | + // ========== 好友 (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, "对方不是您的好友"); | ||
| 40 | + | ||
| 41 | + // ========== 敏感词 (1-040-700-000) ========== | ||
| 42 | + ErrorCode SENSITIVE_WORD_NOT_EXISTS = new ErrorCode(1_040_700_000, "敏感词不存在"); | ||
| 27 | 43 | ||
| 28 | } | 44 | } |
yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/enums/conversation/ImConversationTypeEnum.java
| 1 | package cn.iocoder.yudao.module.im.enums.conversation; | 1 | package cn.iocoder.yudao.module.im.enums.conversation; |
| 2 | 2 | ||
| 3 | -import cn.iocoder.yudao.framework.common.core.IntArrayValuable; | 3 | +import cn.iocoder.yudao.framework.common.core.ArrayValuable; |
| 4 | import lombok.AllArgsConstructor; | 4 | import lombok.AllArgsConstructor; |
| 5 | import lombok.Getter; | 5 | import lombok.Getter; |
| 6 | 6 | ||
| 7 | import java.util.Arrays; | 7 | import java.util.Arrays; |
| 8 | 8 | ||
| 9 | +// TODO @AI:还需要这个枚举么? | ||
| 9 | /** | 10 | /** |
| 10 | * IM 会话类型枚举 | 11 | * IM 会话类型枚举 |
| 11 | * 参考 <a href="https://doc.rentsoft.cn/zh-Hans/sdks/enum/conversationType">“会话类型”</a> 文档 | 12 | * 参考 <a href="https://doc.rentsoft.cn/zh-Hans/sdks/enum/conversationType">“会话类型”</a> 文档 |
| @@ -14,13 +15,13 @@ import java.util.Arrays; | @@ -14,13 +15,13 @@ import java.util.Arrays; | ||
| 14 | */ | 15 | */ |
| 15 | @Getter | 16 | @Getter |
| 16 | @AllArgsConstructor | 17 | @AllArgsConstructor |
| 17 | -public enum ImConversationTypeEnum implements IntArrayValuable { | 18 | +public enum ImConversationTypeEnum implements ArrayValuable<Integer> { |
| 18 | 19 | ||
| 19 | SINGLE(1, "单聊"), | 20 | SINGLE(1, "单聊"), |
| 20 | GROUP(3, "群聊"), | 21 | GROUP(3, "群聊"), |
| 21 | NOTIFICATION(4, "通知会话"); | 22 | NOTIFICATION(4, "通知会话"); |
| 22 | 23 | ||
| 23 | - public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(ImConversationTypeEnum::getType).toArray(); | 24 | + public static final Integer[] ARRAYS = Arrays.stream(values()).map(ImConversationTypeEnum::getType).toArray(Integer[]::new); |
| 24 | 25 | ||
| 25 | /** | 26 | /** |
| 26 | * 类型 | 27 | * 类型 |
| @@ -57,7 +58,7 @@ public enum ImConversationTypeEnum implements IntArrayValuable { | @@ -57,7 +58,7 @@ public enum ImConversationTypeEnum implements IntArrayValuable { | ||
| 57 | } | 58 | } |
| 58 | 59 | ||
| 59 | @Override | 60 | @Override |
| 60 | - public int[] array() { | 61 | + public Integer[] array() { |
| 61 | return ARRAYS; | 62 | return ARRAYS; |
| 62 | } | 63 | } |
| 63 | 64 |
yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/enums/group/ImGroupMemberStatusEnum.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.enums.group; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.common.core.ArrayValuable; | ||
| 4 | +import lombok.AllArgsConstructor; | ||
| 5 | +import lombok.Getter; | ||
| 6 | + | ||
| 7 | +import java.util.Arrays; | ||
| 8 | + | ||
| 9 | +// TODO @AI:要不使用 commonstatusenum 替代掉这个; | ||
| 10 | +/** | ||
| 11 | + * IM 群成员状态枚举 | ||
| 12 | + * | ||
| 13 | + * @author 芋道源码 | ||
| 14 | + */ | ||
| 15 | +@Getter | ||
| 16 | +@AllArgsConstructor | ||
| 17 | +public enum ImGroupMemberStatusEnum implements ArrayValuable<Integer> { | ||
| 18 | + | ||
| 19 | + NORMAL(0, "正常"), | ||
| 20 | + QUIT(1, "已退出"); | ||
| 21 | + | ||
| 22 | + public static final Integer[] ARRAYS = Arrays.stream(values()).map(ImGroupMemberStatusEnum::getStatus).toArray(Integer[]::new); | ||
| 23 | + | ||
| 24 | + /** | ||
| 25 | + * 状态 | ||
| 26 | + */ | ||
| 27 | + private final Integer status; | ||
| 28 | + | ||
| 29 | + /** | ||
| 30 | + * 名字 | ||
| 31 | + */ | ||
| 32 | + private final String name; | ||
| 33 | + | ||
| 34 | + @Override | ||
| 35 | + public Integer[] array() { | ||
| 36 | + return ARRAYS; | ||
| 37 | + } | ||
| 38 | + | ||
| 39 | +} |
yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/enums/message/ImGroupMessageReceiptStatusEnum.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.enums.message; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.common.core.ArrayValuable; | ||
| 4 | +import lombok.AllArgsConstructor; | ||
| 5 | +import lombok.Getter; | ||
| 6 | + | ||
| 7 | +import java.util.Arrays; | ||
| 8 | + | ||
| 9 | +/** | ||
| 10 | + * IM 群消息回执状态枚举 | ||
| 11 | + * | ||
| 12 | + * @author 芋道源码 | ||
| 13 | + */ | ||
| 14 | +@Getter | ||
| 15 | +@AllArgsConstructor | ||
| 16 | +public enum ImGroupMessageReceiptStatusEnum implements ArrayValuable<Integer> { | ||
| 17 | + | ||
| 18 | + NO_RECEIPT(0, "不需要回执"), | ||
| 19 | + PENDING(1, "待完成"), | ||
| 20 | + DONE(2, "已完成"); | ||
| 21 | + | ||
| 22 | + public static final Integer[] ARRAYS = Arrays.stream(values()).map(ImGroupMessageReceiptStatusEnum::getStatus).toArray(Integer[]::new); | ||
| 23 | + | ||
| 24 | + /** | ||
| 25 | + * 状态 | ||
| 26 | + */ | ||
| 27 | + private final Integer status; | ||
| 28 | + | ||
| 29 | + /** | ||
| 30 | + * 名字 | ||
| 31 | + */ | ||
| 32 | + private final String name; | ||
| 33 | + | ||
| 34 | + @Override | ||
| 35 | + public Integer[] array() { | ||
| 36 | + return ARRAYS; | ||
| 37 | + } | ||
| 38 | + | ||
| 39 | +} |
yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/enums/message/ImMessageContentTypeEnum.java
| 1 | package cn.iocoder.yudao.module.im.enums.message; | 1 | package cn.iocoder.yudao.module.im.enums.message; |
| 2 | 2 | ||
| 3 | -import cn.iocoder.yudao.framework.common.core.IntArrayValuable; | 3 | +import cn.iocoder.yudao.framework.common.core.ArrayValuable; |
| 4 | import lombok.AllArgsConstructor; | 4 | import lombok.AllArgsConstructor; |
| 5 | import lombok.Getter; | 5 | import lombok.Getter; |
| 6 | 6 | ||
| 7 | import java.util.Arrays; | 7 | import java.util.Arrays; |
| 8 | 8 | ||
| 9 | +// TODO @AI:还需要这个枚举么? | ||
| 9 | /** | 10 | /** |
| 10 | * IM 消息的类型枚举 | 11 | * IM 消息的类型枚举 |
| 11 | * <p> | 12 | * <p> |
| @@ -15,7 +16,7 @@ import java.util.Arrays; | @@ -15,7 +16,7 @@ import java.util.Arrays; | ||
| 15 | */ | 16 | */ |
| 16 | @Getter | 17 | @Getter |
| 17 | @AllArgsConstructor | 18 | @AllArgsConstructor |
| 18 | -public enum ImMessageContentTypeEnum implements IntArrayValuable { | 19 | +public enum ImMessageContentTypeEnum implements ArrayValuable<Integer> { |
| 19 | 20 | ||
| 20 | TEXT(101, "文本消息"), | 21 | TEXT(101, "文本消息"), |
| 21 | PICTURE(102, "图片消息"), | 22 | PICTURE(102, "图片消息"), |
| @@ -60,7 +61,7 @@ public enum ImMessageContentTypeEnum implements IntArrayValuable { | @@ -60,7 +61,7 @@ public enum ImMessageContentTypeEnum implements IntArrayValuable { | ||
| 60 | BURN_CHANGE(1701, "阅后即焚开启或关闭通知"), | 61 | BURN_CHANGE(1701, "阅后即焚开启或关闭通知"), |
| 61 | REVOKE(2101, "撤回消息通知");; | 62 | REVOKE(2101, "撤回消息通知");; |
| 62 | 63 | ||
| 63 | - public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(ImMessageContentTypeEnum::getType).toArray(); | 64 | + public static final Integer[] ARRAYS = Arrays.stream(values()).map(ImMessageContentTypeEnum::getType).toArray(Integer[]::new); |
| 64 | 65 | ||
| 65 | /** | 66 | /** |
| 66 | * 类型 | 67 | * 类型 |
| @@ -73,7 +74,7 @@ public enum ImMessageContentTypeEnum implements IntArrayValuable { | @@ -73,7 +74,7 @@ public enum ImMessageContentTypeEnum implements IntArrayValuable { | ||
| 73 | private final String name; | 74 | private final String name; |
| 74 | 75 | ||
| 75 | @Override | 76 | @Override |
| 76 | - public int[] array() { | 77 | + public Integer[] array() { |
| 77 | return ARRAYS; | 78 | return ARRAYS; |
| 78 | } | 79 | } |
| 79 | 80 |
yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/enums/message/ImMessageSourceEnum.java
| 1 | package cn.iocoder.yudao.module.im.enums.message; | 1 | package cn.iocoder.yudao.module.im.enums.message; |
| 2 | 2 | ||
| 3 | -import cn.iocoder.yudao.framework.common.core.IntArrayValuable; | 3 | +import cn.iocoder.yudao.framework.common.core.ArrayValuable; |
| 4 | import lombok.Getter; | 4 | import lombok.Getter; |
| 5 | import lombok.RequiredArgsConstructor; | 5 | import lombok.RequiredArgsConstructor; |
| 6 | 6 | ||
| 7 | import java.util.Arrays; | 7 | import java.util.Arrays; |
| 8 | 8 | ||
| 9 | +// TODO @AI:还需要这个枚举么? | ||
| 9 | /** | 10 | /** |
| 10 | * IM 消息的消息来源 | 11 | * IM 消息的消息来源 |
| 11 | */ | 12 | */ |
| 12 | @RequiredArgsConstructor | 13 | @RequiredArgsConstructor |
| 13 | @Getter | 14 | @Getter |
| 14 | -public enum ImMessageSourceEnum implements IntArrayValuable { | 15 | +public enum ImMessageSourceEnum implements ArrayValuable<Integer> { |
| 15 | 16 | ||
| 16 | USER_SEND(100, "用户发送"), | 17 | USER_SEND(100, "用户发送"), |
| 17 | SYSTEM_SEND(200, "系统发送"); | 18 | SYSTEM_SEND(200, "系统发送"); |
| 18 | 19 | ||
| 19 | - public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(ImMessageSourceEnum::getSource).toArray(); | 20 | + public static final Integer[] ARRAYS = Arrays.stream(values()).map(ImMessageSourceEnum::getSource).toArray(Integer[]::new); |
| 20 | 21 | ||
| 21 | /** | 22 | /** |
| 22 | * 状态 | 23 | * 状态 |
| @@ -28,7 +29,7 @@ public enum ImMessageSourceEnum implements IntArrayValuable { | @@ -28,7 +29,7 @@ public enum ImMessageSourceEnum implements IntArrayValuable { | ||
| 28 | private final String name; | 29 | private final String name; |
| 29 | 30 | ||
| 30 | @Override | 31 | @Override |
| 31 | - public int[] array() { | 32 | + public Integer[] array() { |
| 32 | return ARRAYS; | 33 | return ARRAYS; |
| 33 | } | 34 | } |
| 34 | 35 |
yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/enums/message/ImMessageStatusEnum.java
| 1 | package cn.iocoder.yudao.module.im.enums.message; | 1 | package cn.iocoder.yudao.module.im.enums.message; |
| 2 | 2 | ||
| 3 | -import cn.iocoder.yudao.framework.common.core.IntArrayValuable; | 3 | +import cn.iocoder.yudao.framework.common.core.ArrayValuable; |
| 4 | import lombok.Getter; | 4 | import lombok.Getter; |
| 5 | import lombok.RequiredArgsConstructor; | 5 | import lombok.RequiredArgsConstructor; |
| 6 | 6 | ||
| 7 | import java.util.Arrays; | 7 | import java.util.Arrays; |
| 8 | 8 | ||
| 9 | /** | 9 | /** |
| 10 | - * IM 消息的状态枚举 | 10 | + * IM 私聊消息状态枚举 |
| 11 | + * <p> | ||
| 12 | + * 私聊:UNREAD / READ / RECALL | ||
| 13 | + * 群聊:NORMAL / RECALL | ||
| 14 | + * | ||
| 15 | + * @author 芋道源码 | ||
| 11 | */ | 16 | */ |
| 12 | @RequiredArgsConstructor | 17 | @RequiredArgsConstructor |
| 13 | @Getter | 18 | @Getter |
| 14 | -public enum ImMessageStatusEnum implements IntArrayValuable { | 19 | +public enum ImMessageStatusEnum implements ArrayValuable<Integer> { |
| 15 | 20 | ||
| 16 | - DRAFT(0, "草稿"), | ||
| 17 | - SENDING(1, "发送中"), | ||
| 18 | - SUCCESS(2, "发送成功"), | ||
| 19 | - FAILURE(3, "发送失败"), | ||
| 20 | - DELETED(4, "已删除"), | ||
| 21 | - RECALL(5, "已撤回"); | 21 | + // ========== 私聊状态 ========== |
| 22 | + UNREAD(0, "未读"), | ||
| 23 | + READ(1, "已读"), | ||
| 22 | 24 | ||
| 23 | - public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(ImMessageStatusEnum::getStatus).toArray(); | 25 | + // ========== 通用状态 ========== |
| 26 | + RECALL(2, "已撤回"); | ||
| 27 | + | ||
| 28 | + // TODO @AI:0 这个,不是有 UNREAD 可以用了?! | ||
| 29 | + /** | ||
| 30 | + * 群聊消息的正常状态,与 UNREAD 共用值 0 | ||
| 31 | + */ | ||
| 32 | + public static final int NORMAL = 0; | ||
| 33 | + | ||
| 34 | + public static final Integer[] ARRAYS = Arrays.stream(values()).map(ImMessageStatusEnum::getStatus).toArray(Integer[]::new); | ||
| 24 | 35 | ||
| 25 | /** | 36 | /** |
| 26 | * 状态 | 37 | * 状态 |
| @@ -33,7 +44,7 @@ public enum ImMessageStatusEnum implements IntArrayValuable { | @@ -33,7 +44,7 @@ public enum ImMessageStatusEnum implements IntArrayValuable { | ||
| 33 | private final String name; | 44 | private final String name; |
| 34 | 45 | ||
| 35 | @Override | 46 | @Override |
| 36 | - public int[] array() { | 47 | + public Integer[] array() { |
| 37 | return ARRAYS; | 48 | return ARRAYS; |
| 38 | } | 49 | } |
| 39 | 50 |
yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/enums/message/ImMessageTypeEnum.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.enums.message; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.common.core.ArrayValuable; | ||
| 4 | +import lombok.AllArgsConstructor; | ||
| 5 | +import lombok.Getter; | ||
| 6 | + | ||
| 7 | +import java.util.Arrays; | ||
| 8 | + | ||
| 9 | +/** | ||
| 10 | + * IM 消息类型枚举 | ||
| 11 | + * | ||
| 12 | + * @author 芋道源码 | ||
| 13 | + */ | ||
| 14 | +@Getter | ||
| 15 | +@AllArgsConstructor | ||
| 16 | +public enum ImMessageTypeEnum implements ArrayValuable<Integer> { | ||
| 17 | + | ||
| 18 | + TEXT(0, "文本"), | ||
| 19 | + IMAGE(1, "图片"), | ||
| 20 | + FILE(2, "文件"), | ||
| 21 | + VOICE(3, "语音"), | ||
| 22 | + VIDEO(4, "视频"), | ||
| 23 | + | ||
| 24 | + RECALL(10, "撤回"), | ||
| 25 | + READ(11, "已读"), | ||
| 26 | + RECEIPT(12, "回执"), | ||
| 27 | + TIP_TEXT(21, "提示文本"); | ||
| 28 | + | ||
| 29 | + public static final Integer[] ARRAYS = Arrays.stream(values()).map(ImMessageTypeEnum::getType).toArray(Integer[]::new); | ||
| 30 | + | ||
| 31 | + /** | ||
| 32 | + * 类型 | ||
| 33 | + */ | ||
| 34 | + private final Integer type; | ||
| 35 | + | ||
| 36 | + /** | ||
| 37 | + * 名字 | ||
| 38 | + */ | ||
| 39 | + private final String name; | ||
| 40 | + | ||
| 41 | + @Override | ||
| 42 | + public Integer[] array() { | ||
| 43 | + return ARRAYS; | ||
| 44 | + } | ||
| 45 | + | ||
| 46 | +} |
yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/enums/message/ImWebSocketTypeConstants.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.enums.message; | ||
| 2 | + | ||
| 3 | +// TODO @芋艿:稍后在 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 | +} | ||
| 0 | \ No newline at end of file | 33 | \ No newline at end of file |
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupPageReqVO.java
| @@ -14,24 +14,15 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_ | @@ -14,24 +14,15 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_ | ||
| 14 | @ToString(callSuper = true) | 14 | @ToString(callSuper = true) |
| 15 | public class ImGroupPageReqVO extends PageParam { | 15 | public class ImGroupPageReqVO extends PageParam { |
| 16 | 16 | ||
| 17 | - @Schema(description = "群名字", example = "芋艿") | ||
| 18 | - private String groupName; | 17 | + @Schema(description = "群名称", example = "芋艿") |
| 18 | + private String name; | ||
| 19 | 19 | ||
| 20 | - @Schema(description = "群主id", example = "31460") | ||
| 21 | - private Long ownerId; | ||
| 22 | - | ||
| 23 | - @Schema(description = "群头像") | ||
| 24 | - private String headImage; | ||
| 25 | - | ||
| 26 | - @Schema(description = "群头像缩略图") | ||
| 27 | - private String headImageThumb; | 20 | + @Schema(description = "群主用户编号", example = "31460") |
| 21 | + private Long ownerUserId; | ||
| 28 | 22 | ||
| 29 | @Schema(description = "群公告") | 23 | @Schema(description = "群公告") |
| 30 | private String notice; | 24 | private String notice; |
| 31 | 25 | ||
| 32 | - @Schema(description = "群备注", example = "你说的对") | ||
| 33 | - private String remark; | ||
| 34 | - | ||
| 35 | @Schema(description = "创建时间") | 26 | @Schema(description = "创建时间") |
| 36 | @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) | 27 | @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) |
| 37 | private LocalDateTime[] createTime; | 28 | private LocalDateTime[] createTime; |
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupRespVO.java
| @@ -2,10 +2,11 @@ package cn.iocoder.yudao.module.im.controller.admin.group.vo; | @@ -2,10 +2,11 @@ package cn.iocoder.yudao.module.im.controller.admin.group.vo; | ||
| 2 | 2 | ||
| 3 | import io.swagger.v3.oas.annotations.media.Schema; | 3 | import io.swagger.v3.oas.annotations.media.Schema; |
| 4 | import lombok.*; | 4 | import lombok.*; |
| 5 | +import com.alibaba.excel.annotation.*; | ||
| 5 | 6 | ||
| 6 | import java.time.LocalDateTime; | 7 | import java.time.LocalDateTime; |
| 7 | -import com.alibaba.excel.annotation.*; | ||
| 8 | 8 | ||
| 9 | +// TODO @AI:不需要导出的注解; | ||
| 9 | @Schema(description = "管理后台 - 群 Response VO") | 10 | @Schema(description = "管理后台 - 群 Response VO") |
| 10 | @Data | 11 | @Data |
| 11 | @ExcelIgnoreUnannotated | 12 | @ExcelIgnoreUnannotated |
| @@ -15,29 +16,29 @@ public class ImGroupRespVO { | @@ -15,29 +16,29 @@ public class ImGroupRespVO { | ||
| 15 | @ExcelProperty("编号") | 16 | @ExcelProperty("编号") |
| 16 | private Long id; | 17 | private Long id; |
| 17 | 18 | ||
| 18 | - @Schema(description = "群名字", example = "芋艿") | ||
| 19 | - @ExcelProperty("群名字") | ||
| 20 | - private String groupName; | 19 | + @Schema(description = "群名称", example = "芋艿") |
| 20 | + @ExcelProperty("群名称") | ||
| 21 | + private String name; | ||
| 21 | 22 | ||
| 22 | - @Schema(description = "群主id", requiredMode = Schema.RequiredMode.REQUIRED, example = "31460") | ||
| 23 | - @ExcelProperty("群主id") | ||
| 24 | - private Long ownerId; | 23 | + @Schema(description = "群主用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "31460") |
| 24 | + @ExcelProperty("群主用户编号") | ||
| 25 | + private Long ownerUserId; | ||
| 25 | 26 | ||
| 26 | @Schema(description = "群头像") | 27 | @Schema(description = "群头像") |
| 27 | @ExcelProperty("群头像") | 28 | @ExcelProperty("群头像") |
| 28 | - private String headImage; | ||
| 29 | - | ||
| 30 | - @Schema(description = "群头像缩略图") | ||
| 31 | - @ExcelProperty("群头像缩略图") | ||
| 32 | - private String headImageThumb; | 29 | + private String avatar; |
| 33 | 30 | ||
| 34 | @Schema(description = "群公告") | 31 | @Schema(description = "群公告") |
| 35 | @ExcelProperty("群公告") | 32 | @ExcelProperty("群公告") |
| 36 | private String notice; | 33 | private String notice; |
| 37 | 34 | ||
| 38 | - @Schema(description = "群备注", example = "你说的对") | ||
| 39 | - @ExcelProperty("群备注") | ||
| 40 | - private String remark; | 35 | + @Schema(description = "是否封禁") |
| 36 | + @ExcelProperty("是否封禁") | ||
| 37 | + private Boolean banned; | ||
| 38 | + | ||
| 39 | + @Schema(description = "是否解散") | ||
| 40 | + @ExcelProperty("是否解散") | ||
| 41 | + private Boolean dissolved; | ||
| 41 | 42 | ||
| 42 | @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) | 43 | @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) |
| 43 | @ExcelProperty("创建时间") | 44 | @ExcelProperty("创建时间") |
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupSaveReqVO.java
| @@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema; | @@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema; | ||
| 4 | import lombok.*; | 4 | import lombok.*; |
| 5 | import jakarta.validation.constraints.*; | 5 | import jakarta.validation.constraints.*; |
| 6 | 6 | ||
| 7 | +// TODO @AI:去掉部分不需要的 save 字段; | ||
| 7 | @Schema(description = "管理后台 - 群新增/修改 Request VO") | 8 | @Schema(description = "管理后台 - 群新增/修改 Request VO") |
| 8 | @Data | 9 | @Data |
| 9 | public class ImGroupSaveReqVO { | 10 | public class ImGroupSaveReqVO { |
| @@ -11,23 +12,26 @@ public class ImGroupSaveReqVO { | @@ -11,23 +12,26 @@ public class ImGroupSaveReqVO { | ||
| 11 | @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1003") | 12 | @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1003") |
| 12 | private Long id; | 13 | private Long id; |
| 13 | 14 | ||
| 14 | - @Schema(description = "群名字", example = "芋艿") | ||
| 15 | - private String groupName; | 15 | + @Schema(description = "群名称", example = "芋艿") |
| 16 | + private String name; | ||
| 16 | 17 | ||
| 17 | - @Schema(description = "群主id", requiredMode = Schema.RequiredMode.REQUIRED, example = "31460") | ||
| 18 | - @NotNull(message = "群主id不能为空") | ||
| 19 | - private Long ownerId; | 18 | + @Schema(description = "群主用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "31460") |
| 19 | + @NotNull(message = "群主用户编号不能为空") | ||
| 20 | + private Long ownerUserId; | ||
| 20 | 21 | ||
| 21 | @Schema(description = "群头像") | 22 | @Schema(description = "群头像") |
| 22 | - private String headImage; | ||
| 23 | - | ||
| 24 | - @Schema(description = "群头像缩略图") | ||
| 25 | - private String headImageThumb; | 23 | + private String avatar; |
| 26 | 24 | ||
| 27 | @Schema(description = "群公告") | 25 | @Schema(description = "群公告") |
| 28 | private String notice; | 26 | private String notice; |
| 29 | 27 | ||
| 30 | - @Schema(description = "群备注", example = "你说的对") | ||
| 31 | - private String remark; | 28 | + @Schema(description = "是否封禁") |
| 29 | + private Boolean banned; | ||
| 30 | + | ||
| 31 | + @Schema(description = "封禁原因") | ||
| 32 | + private String bannedReason; | ||
| 33 | + | ||
| 34 | + @Schema(description = "是否解散") | ||
| 35 | + private Boolean dissolved; | ||
| 32 | 36 | ||
| 33 | } | 37 | } |
| 34 | \ No newline at end of file | 38 | \ No newline at end of file |
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/groupmember/vo/ImGroupMemberPageReqVO.java
| @@ -17,23 +17,14 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_ | @@ -17,23 +17,14 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_ | ||
| 17 | @ToString(callSuper = true) | 17 | @ToString(callSuper = true) |
| 18 | public class ImGroupMemberPageReqVO extends PageParam { | 18 | public class ImGroupMemberPageReqVO extends PageParam { |
| 19 | 19 | ||
| 20 | - @Schema(description = "群 id", example = "13279") | 20 | + @Schema(description = "群编号", example = "13279") |
| 21 | private Long groupId; | 21 | private Long groupId; |
| 22 | 22 | ||
| 23 | - @Schema(description = "用户id", example = "21730") | 23 | + @Schema(description = "用户编号", example = "21730") |
| 24 | private Long userId; | 24 | private Long userId; |
| 25 | 25 | ||
| 26 | - @Schema(description = "昵称", example = "芋艿") | ||
| 27 | - private String nickname; | ||
| 28 | - | ||
| 29 | - @Schema(description = "头像") | ||
| 30 | - private String avatar; | ||
| 31 | - | ||
| 32 | - @Schema(description = "组内显示名称", example = "芋艿") | ||
| 33 | - private String aliasName; | ||
| 34 | - | ||
| 35 | - @Schema(description = "备注", example = "你说的对") | ||
| 36 | - private String remark; | 26 | + @Schema(description = "成员状态", example = "0") |
| 27 | + private Integer status; | ||
| 37 | 28 | ||
| 38 | @Schema(description = "创建时间") | 29 | @Schema(description = "创建时间") |
| 39 | @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) | 30 | @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) |
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/groupmember/vo/ImGroupMemberRespVO.java
| @@ -7,6 +7,7 @@ import lombok.Data; | @@ -7,6 +7,7 @@ import lombok.Data; | ||
| 7 | 7 | ||
| 8 | import java.time.LocalDateTime; | 8 | import java.time.LocalDateTime; |
| 9 | 9 | ||
| 10 | +// TODO @AI:去掉部分导出字段 | ||
| 10 | @Schema(description = "管理后台 - 群成员 Response VO") | 11 | @Schema(description = "管理后台 - 群成员 Response VO") |
| 11 | @Data | 12 | @Data |
| 12 | @ExcelIgnoreUnannotated | 13 | @ExcelIgnoreUnannotated |
| @@ -16,29 +17,37 @@ public class ImGroupMemberRespVO { | @@ -16,29 +17,37 @@ public class ImGroupMemberRespVO { | ||
| 16 | @ExcelProperty("编号") | 17 | @ExcelProperty("编号") |
| 17 | private Long id; | 18 | private Long id; |
| 18 | 19 | ||
| 19 | - @Schema(description = "群 id", example = "13279") | ||
| 20 | - @ExcelProperty("群 id") | 20 | + @Schema(description = "群编号", example = "13279") |
| 21 | + @ExcelProperty("群编号") | ||
| 21 | private Long groupId; | 22 | private Long groupId; |
| 22 | 23 | ||
| 23 | - @Schema(description = "用户id", requiredMode = Schema.RequiredMode.REQUIRED, example = "21730") | ||
| 24 | - @ExcelProperty("用户id") | 24 | + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "21730") |
| 25 | + @ExcelProperty("用户编号") | ||
| 25 | private Long userId; | 26 | private Long userId; |
| 26 | 27 | ||
| 27 | - @Schema(description = "昵称", example = "芋艿") | ||
| 28 | - @ExcelProperty("昵称") | ||
| 29 | - private String nickname; | 28 | + @Schema(description = "组内显示名", example = "芋艿") |
| 29 | + @ExcelProperty("组内显示名") | ||
| 30 | + private String displayUserName; | ||
| 30 | 31 | ||
| 31 | - @Schema(description = "头像") | ||
| 32 | - @ExcelProperty("头像") | ||
| 33 | - private String avatar; | 32 | + @Schema(description = "群显示备注", example = "核心群") |
| 33 | + @ExcelProperty("群显示备注") | ||
| 34 | + private String displayGroupName; | ||
| 34 | 35 | ||
| 35 | - @Schema(description = "组内显示名称", example = "芋艿") | ||
| 36 | - @ExcelProperty("组内显示名称") | ||
| 37 | - private String aliasName; | 36 | + @Schema(description = "是否免打扰") |
| 37 | + @ExcelProperty("是否免打扰") | ||
| 38 | + private Boolean muted; | ||
| 38 | 39 | ||
| 39 | - @Schema(description = "备注", example = "你说的对") | ||
| 40 | - @ExcelProperty("备注") | ||
| 41 | - private String remark; | 40 | + @Schema(description = "成员状态", example = "0") |
| 41 | + @ExcelProperty("成员状态") | ||
| 42 | + private Integer status; | ||
| 43 | + | ||
| 44 | + @Schema(description = "入群时间") | ||
| 45 | + @ExcelProperty("入群时间") | ||
| 46 | + private LocalDateTime joinTime; | ||
| 47 | + | ||
| 48 | + @Schema(description = "退群时间") | ||
| 49 | + @ExcelProperty("退群时间") | ||
| 50 | + private LocalDateTime quitTime; | ||
| 42 | 51 | ||
| 43 | @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) | 52 | @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) |
| 44 | @ExcelProperty("创建时间") | 53 | @ExcelProperty("创建时间") |
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/groupmember/vo/ImGroupMemberSaveReqVO.java
| @@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema; | @@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema; | ||
| 4 | import jakarta.validation.constraints.NotNull; | 4 | import jakarta.validation.constraints.NotNull; |
| 5 | import lombok.Data; | 5 | import lombok.Data; |
| 6 | 6 | ||
| 7 | +// TODO @AI:去掉部分不需要的 save 字段; | ||
| 7 | @Schema(description = "管理后台 - 群成员新增/修改 Request VO") | 8 | @Schema(description = "管理后台 - 群成员新增/修改 Request VO") |
| 8 | @Data | 9 | @Data |
| 9 | public class ImGroupMemberSaveReqVO { | 10 | public class ImGroupMemberSaveReqVO { |
| @@ -11,23 +12,23 @@ public class ImGroupMemberSaveReqVO { | @@ -11,23 +12,23 @@ public class ImGroupMemberSaveReqVO { | ||
| 11 | @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "17071") | 12 | @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "17071") |
| 12 | private Long id; | 13 | private Long id; |
| 13 | 14 | ||
| 14 | - @Schema(description = "群 id", example = "13279") | 15 | + @Schema(description = "群编号", example = "13279") |
| 15 | private Long groupId; | 16 | private Long groupId; |
| 16 | 17 | ||
| 17 | - @Schema(description = "用户id", requiredMode = Schema.RequiredMode.REQUIRED, example = "21730") | ||
| 18 | - @NotNull(message = "用户id不能为空") | 18 | + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "21730") |
| 19 | + @NotNull(message = "用户编号不能为空") | ||
| 19 | private Long userId; | 20 | private Long userId; |
| 20 | 21 | ||
| 21 | - @Schema(description = "昵称", example = "芋艿") | ||
| 22 | - private String nickname; | 22 | + @Schema(description = "组内显示名", example = "芋艿") |
| 23 | + private String displayUserName; | ||
| 23 | 24 | ||
| 24 | - @Schema(description = "头像") | ||
| 25 | - private String avatar; | 25 | + @Schema(description = "群显示备注", example = "核心群") |
| 26 | + private String displayGroupName; | ||
| 26 | 27 | ||
| 27 | - @Schema(description = "组内显示名称", example = "芋艿") | ||
| 28 | - private String aliasName; | 28 | + @Schema(description = "是否免打扰") |
| 29 | + private Boolean muted; | ||
| 29 | 30 | ||
| 30 | - @Schema(description = "备注", example = "你说的对") | ||
| 31 | - private String remark; | 31 | + @Schema(description = "成员状态", example = "0") |
| 32 | + private Integer status; | ||
| 32 | 33 | ||
| 33 | } | 34 | } |
| 34 | \ No newline at end of file | 35 | \ No newline at end of file |
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/ImGroupMessageController.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.controller.admin.message; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.common.pojo.CommonResult; | ||
| 4 | +import cn.iocoder.yudao.module.im.controller.admin.message.vo.*; | ||
| 5 | +import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessageReadReqVO; | ||
| 6 | +import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessageRespVO; | ||
| 7 | +import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessageSendReqVO; | ||
| 8 | +import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO; | ||
| 9 | +import cn.iocoder.yudao.module.im.service.message.ImGroupMessageService; | ||
| 10 | +import io.swagger.v3.oas.annotations.Operation; | ||
| 11 | +import io.swagger.v3.oas.annotations.Parameter; | ||
| 12 | +import io.swagger.v3.oas.annotations.tags.Tag; | ||
| 13 | +import jakarta.annotation.Resource; | ||
| 14 | +import jakarta.validation.Valid; | ||
| 15 | +import org.springframework.validation.annotation.Validated; | ||
| 16 | +import org.springframework.web.bind.annotation.*; | ||
| 17 | + | ||
| 18 | +import java.util.List; | ||
| 19 | +import java.util.stream.Collectors; | ||
| 20 | + | ||
| 21 | +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; | ||
| 22 | +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; | ||
| 23 | + | ||
| 24 | +/** | ||
| 25 | + * 管理后台 - IM 群聊消息 | ||
| 26 | + */ | ||
| 27 | +@Tag(name = "管理后台 - IM 群聊消息") | ||
| 28 | +@RestController | ||
| 29 | +@RequestMapping("/im/message/group") | ||
| 30 | +@Validated | ||
| 31 | +public class ImGroupMessageController { | ||
| 32 | + | ||
| 33 | + @Resource | ||
| 34 | + private ImGroupMessageService imGroupMessageService; | ||
| 35 | + | ||
| 36 | + @PostMapping("/send") | ||
| 37 | + @Operation(summary = "发送群聊消息") | ||
| 38 | + public CommonResult<ImGroupMessageRespVO> sendMessage(@Valid @RequestBody ImGroupMessageSendReqVO reqVO) { | ||
| 39 | + ImGroupMessageDO message = imGroupMessageService.sendMessage(getLoginUserId(), reqVO); | ||
| 40 | + return success(toRespVO(message)); | ||
| 41 | + } | ||
| 42 | + | ||
| 43 | + // TODO @AI:需要在评估下,是否需要 nextMinId | ||
| 44 | + @GetMapping("/pull") | ||
| 45 | + @Operation(summary = "拉取群聊消息(增量)") | ||
| 46 | + @Parameter(name = "minId", description = "最小消息 id", required = true, example = "0") | ||
| 47 | + @Parameter(name = "size", description = "拉取数量", required = true, example = "100") | ||
| 48 | + public CommonResult<ImMessagePullRespVO<ImGroupMessageRespVO>> pullMessages( | ||
| 49 | + @RequestParam("minId") Long minId, | ||
| 50 | + @RequestParam("size") Integer size) { | ||
| 51 | + List<ImGroupMessageDO> messages = imGroupMessageService.pullMessages(getLoginUserId(), minId, size); | ||
| 52 | + ImMessagePullRespVO<ImGroupMessageRespVO> resp = new ImMessagePullRespVO<>(); | ||
| 53 | + resp.setList(messages.stream().map(this::toRespVO).collect(Collectors.toList())); | ||
| 54 | + resp.setNextMinId(messages.isEmpty() ? minId.toString() | ||
| 55 | + : messages.get(messages.size() - 1).getId().toString()); | ||
| 56 | + return success(resp); | ||
| 57 | + } | ||
| 58 | + | ||
| 59 | + // TODO @AI:它是不是不用 VO | ||
| 60 | + @PutMapping("/read") | ||
| 61 | + @Operation(summary = "标记群聊消息已读") | ||
| 62 | + public CommonResult<Boolean> readMessages(@Valid @RequestBody ImGroupMessageReadReqVO reqVO) { | ||
| 63 | + imGroupMessageService.readMessages(getLoginUserId(), reqVO.getGroupId()); | ||
| 64 | + return success(true); | ||
| 65 | + } | ||
| 66 | + | ||
| 67 | + // TODO @AI:不使用 path variable | ||
| 68 | + @DeleteMapping("/recall/{id}") | ||
| 69 | + @Operation(summary = "撤回群聊消息") | ||
| 70 | + @Parameter(name = "id", description = "消息编号", required = true, example = "1") | ||
| 71 | + public CommonResult<Boolean> recallMessage(@PathVariable("id") Long id) { | ||
| 72 | + imGroupMessageService.recallMessage(getLoginUserId(), id); | ||
| 73 | + return success(true); | ||
| 74 | + } | ||
| 75 | + | ||
| 76 | + // TODO @AI:这个接口,确认下是否需要! | ||
| 77 | + @GetMapping("/read-users") | ||
| 78 | + @Operation(summary = "获取群消息已读用户列表") | ||
| 79 | + @Parameter(name = "groupId", description = "群编号", required = true, example = "1") | ||
| 80 | + @Parameter(name = "messageId", description = "消息编号", required = true, example = "1") | ||
| 81 | + public CommonResult<List<String>> getReadUsers( | ||
| 82 | + @RequestParam("groupId") Long groupId, | ||
| 83 | + @RequestParam("messageId") Long messageId) { | ||
| 84 | + List<Long> readUserIds = imGroupMessageService.getReadUsers(getLoginUserId(), groupId, messageId); | ||
| 85 | + // Long -> String 转换 | ||
| 86 | + return success(readUserIds.stream().map(String::valueOf).collect(Collectors.toList())); | ||
| 87 | + } | ||
| 88 | + | ||
| 89 | + /** | ||
| 90 | + * DO 转换为 RespVO(Long id 统一转 String) | ||
| 91 | + */ | ||
| 92 | + // TODO @AI:BeanUtils? | ||
| 93 | + private ImGroupMessageRespVO toRespVO(ImGroupMessageDO message) { | ||
| 94 | + ImGroupMessageRespVO vo = new ImGroupMessageRespVO(); | ||
| 95 | + vo.setId(message.getId().toString()); | ||
| 96 | + vo.setClientMessageId(message.getClientMessageId()); | ||
| 97 | + vo.setSenderId(message.getSenderId().toString()); | ||
| 98 | + vo.setGroupId(message.getGroupId().toString()); | ||
| 99 | + vo.setType(message.getType()); | ||
| 100 | + vo.setContent(message.getContent()); | ||
| 101 | + vo.setStatus(message.getStatus()); | ||
| 102 | + vo.setSendTime(message.getSendTime()); | ||
| 103 | + vo.setAtUserIds(message.getAtUserIds()); | ||
| 104 | + vo.setReceiverUserIds(message.getReceiverUserIds()); | ||
| 105 | + vo.setReceiptStatus(message.getReceiptStatus()); | ||
| 106 | + return vo; | ||
| 107 | + } | ||
| 108 | + | ||
| 109 | +} |
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/ImPrivateMessageController.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.controller.admin.message; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.common.pojo.CommonResult; | ||
| 4 | +import cn.iocoder.yudao.module.im.controller.admin.message.vo.*; | ||
| 5 | +import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageReadReqVO; | ||
| 6 | +import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageRespVO; | ||
| 7 | +import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageSendReqVO; | ||
| 8 | +import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO; | ||
| 9 | +import cn.iocoder.yudao.module.im.service.message.ImPrivateMessageService; | ||
| 10 | +import io.swagger.v3.oas.annotations.Operation; | ||
| 11 | +import io.swagger.v3.oas.annotations.Parameter; | ||
| 12 | +import io.swagger.v3.oas.annotations.tags.Tag; | ||
| 13 | +import jakarta.annotation.Resource; | ||
| 14 | +import jakarta.validation.Valid; | ||
| 15 | +import org.springframework.validation.annotation.Validated; | ||
| 16 | +import org.springframework.web.bind.annotation.*; | ||
| 17 | + | ||
| 18 | +import java.util.List; | ||
| 19 | +import java.util.stream.Collectors; | ||
| 20 | + | ||
| 21 | +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; | ||
| 22 | +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; | ||
| 23 | + | ||
| 24 | +/** | ||
| 25 | + * 管理后台 - IM 私聊消息 | ||
| 26 | + */ | ||
| 27 | +@Tag(name = "管理后台 - IM 私聊消息") | ||
| 28 | +@RestController | ||
| 29 | +@RequestMapping("/im/message/private") | ||
| 30 | +@Validated | ||
| 31 | +public class ImPrivateMessageController { | ||
| 32 | + | ||
| 33 | + @Resource | ||
| 34 | + private ImPrivateMessageService imPrivateMessageService; | ||
| 35 | + | ||
| 36 | + @PostMapping("/send") | ||
| 37 | + @Operation(summary = "发送私聊消息") | ||
| 38 | + public CommonResult<ImPrivateMessageRespVO> sendMessage(@Valid @RequestBody ImPrivateMessageSendReqVO reqVO) { | ||
| 39 | + ImPrivateMessageDO message = imPrivateMessageService.sendMessage(getLoginUserId(), reqVO); | ||
| 40 | + return success(toRespVO(message)); | ||
| 41 | + } | ||
| 42 | + | ||
| 43 | + // TODO @AI:需要在评估下,是否需要 nextMinId | ||
| 44 | + @GetMapping("/pull") | ||
| 45 | + @Operation(summary = "拉取私聊消息(增量)") | ||
| 46 | + @Parameter(name = "minId", description = "最小消息 id", required = true, example = "0") | ||
| 47 | + @Parameter(name = "size", description = "拉取数量", required = true, example = "100") | ||
| 48 | + public CommonResult<ImMessagePullRespVO<ImPrivateMessageRespVO>> pullMessages( | ||
| 49 | + @RequestParam("minId") Long minId, | ||
| 50 | + @RequestParam("size") Integer size) { | ||
| 51 | + List<ImPrivateMessageDO> messages = imPrivateMessageService.pullMessages(getLoginUserId(), minId, size); | ||
| 52 | + ImMessagePullRespVO<ImPrivateMessageRespVO> resp = new ImMessagePullRespVO<>(); | ||
| 53 | + resp.setList(messages.stream().map(this::toRespVO).collect(Collectors.toList())); | ||
| 54 | + resp.setNextMinId(messages.isEmpty() ? minId.toString() | ||
| 55 | + : messages.get(messages.size() - 1).getId().toString()); | ||
| 56 | + return success(resp); | ||
| 57 | + } | ||
| 58 | + | ||
| 59 | + // TODO @AI:它是不是不用 VO | ||
| 60 | + @PutMapping("/read") | ||
| 61 | + @Operation(summary = "标记私聊消息已读") | ||
| 62 | + public CommonResult<Boolean> readMessages(@Valid @RequestBody ImPrivateMessageReadReqVO reqVO) { | ||
| 63 | + imPrivateMessageService.readMessages(getLoginUserId(), reqVO.getFriendId()); | ||
| 64 | + return success(true); | ||
| 65 | + } | ||
| 66 | + | ||
| 67 | + // TODO @AI:不使用 path variable | ||
| 68 | + @DeleteMapping("/recall/{id}") | ||
| 69 | + @Operation(summary = "撤回私聊消息") | ||
| 70 | + @Parameter(name = "id", description = "消息编号", required = true, example = "1") | ||
| 71 | + public CommonResult<Boolean> recallMessage(@PathVariable("id") Long id) { | ||
| 72 | + imPrivateMessageService.recallMessage(getLoginUserId(), id); | ||
| 73 | + return success(true); | ||
| 74 | + } | ||
| 75 | + | ||
| 76 | + // TODO @AI:beanutils | ||
| 77 | + /** | ||
| 78 | + * DO 转换为 RespVO(Long id 统一转 String) | ||
| 79 | + */ | ||
| 80 | + private ImPrivateMessageRespVO toRespVO(ImPrivateMessageDO message) { | ||
| 81 | + ImPrivateMessageRespVO vo = new ImPrivateMessageRespVO(); | ||
| 82 | + vo.setId(message.getId().toString()); | ||
| 83 | + vo.setClientMessageId(message.getClientMessageId()); | ||
| 84 | + vo.setSenderId(message.getSenderId().toString()); | ||
| 85 | + vo.setReceiverId(message.getReceiverId().toString()); | ||
| 86 | + vo.setType(message.getType()); | ||
| 87 | + vo.setContent(message.getContent()); | ||
| 88 | + vo.setStatus(message.getStatus()); | ||
| 89 | + vo.setSendTime(message.getSendTime()); | ||
| 90 | + return vo; | ||
| 91 | + } | ||
| 92 | + | ||
| 93 | +} |
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/vo/ImMessagePullRespVO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.controller.admin.message.vo; | ||
| 2 | + | ||
| 3 | +import io.swagger.v3.oas.annotations.media.Schema; | ||
| 4 | +import lombok.Data; | ||
| 5 | + | ||
| 6 | +import java.util.List; | ||
| 7 | + | ||
| 8 | +/** | ||
| 9 | + * 消息拉取 Response VO | ||
| 10 | + */ | ||
| 11 | +@Schema(description = "管理后台 - 消息拉取 Response VO") | ||
| 12 | +@Data | ||
| 13 | +public class ImMessagePullRespVO<T> { | ||
| 14 | + | ||
| 15 | + @Schema(description = "消息列表") | ||
| 16 | + private List<T> list; | ||
| 17 | + | ||
| 18 | + @Schema(description = "下次拉取的最小 id", example = "100") | ||
| 19 | + private String nextMinId; | ||
| 20 | + | ||
| 21 | +} |
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/vo/group/ImGroupMessageReadReqVO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.controller.admin.message.vo.group; | ||
| 2 | + | ||
| 3 | +import io.swagger.v3.oas.annotations.media.Schema; | ||
| 4 | +import jakarta.validation.constraints.NotNull; | ||
| 5 | +import lombok.Data; | ||
| 6 | + | ||
| 7 | +/** | ||
| 8 | + * 群聊已读 Request VO | ||
| 9 | + */ | ||
| 10 | +@Schema(description = "管理后台 - 群聊已读 Request VO") | ||
| 11 | +@Data | ||
| 12 | +public class ImGroupMessageReadReqVO { | ||
| 13 | + | ||
| 14 | + @Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") | ||
| 15 | + @NotNull(message = "群编号不能为空") | ||
| 16 | + private Long groupId; | ||
| 17 | + | ||
| 18 | +} |
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/vo/group/ImGroupMessageRespVO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.controller.admin.message.vo.group; | ||
| 2 | + | ||
| 3 | +import io.swagger.v3.oas.annotations.media.Schema; | ||
| 4 | +import lombok.Data; | ||
| 5 | + | ||
| 6 | +import java.time.LocalDateTime; | ||
| 7 | + | ||
| 8 | +/** | ||
| 9 | + * 群聊消息 Response VO | ||
| 10 | + */ | ||
| 11 | +@Schema(description = "管理后台 - 群聊消息 Response VO") | ||
| 12 | +@Data | ||
| 13 | +public class ImGroupMessageRespVO { | ||
| 14 | + | ||
| 15 | + @Schema(description = "消息编号", example = "1") | ||
| 16 | + private String id; | ||
| 17 | + | ||
| 18 | + @Schema(description = "客户端消息编号", example = "uuid-xxx") | ||
| 19 | + private String clientMessageId; | ||
| 20 | + | ||
| 21 | + @Schema(description = "发送人编号", example = "1") | ||
| 22 | + private String senderId; | ||
| 23 | + | ||
| 24 | + @Schema(description = "群编号", example = "1") | ||
| 25 | + private String groupId; | ||
| 26 | + | ||
| 27 | + @Schema(description = "消息类型", example = "0") | ||
| 28 | + private Integer type; | ||
| 29 | + | ||
| 30 | + @Schema(description = "消息内容", example = "{\"content\":\"你好\"}") | ||
| 31 | + private String content; | ||
| 32 | + | ||
| 33 | + @Schema(description = "消息状态", example = "0") | ||
| 34 | + private Integer status; | ||
| 35 | + | ||
| 36 | + @Schema(description = "发送时间") | ||
| 37 | + private LocalDateTime sendTime; | ||
| 38 | + | ||
| 39 | + @Schema(description = "@目标用户编号列表", example = "1,2,3") | ||
| 40 | + private String atUserIds; | ||
| 41 | + | ||
| 42 | + @Schema(description = "定向接收用户编号列表", example = "1,2") | ||
| 43 | + private String receiverUserIds; | ||
| 44 | + | ||
| 45 | + @Schema(description = "回执状态", example = "0") | ||
| 46 | + private Integer receiptStatus; | ||
| 47 | + | ||
| 48 | + @Schema(description = "已读人数(群回执使用)", example = "5") | ||
| 49 | + private Integer readCount; | ||
| 50 | + | ||
| 51 | +} |
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/vo/group/ImGroupMessageSendReqVO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.controller.admin.message.vo.group; | ||
| 2 | + | ||
| 3 | +import io.swagger.v3.oas.annotations.media.Schema; | ||
| 4 | +import jakarta.validation.constraints.NotEmpty; | ||
| 5 | +import jakarta.validation.constraints.NotNull; | ||
| 6 | +import lombok.Data; | ||
| 7 | + | ||
| 8 | +import java.util.List; | ||
| 9 | + | ||
| 10 | +/** | ||
| 11 | + * 群聊消息发送 Request VO | ||
| 12 | + */ | ||
| 13 | +@Schema(description = "管理后台 - 群聊消息发送 Request VO") | ||
| 14 | +@Data | ||
| 15 | +public class ImGroupMessageSendReqVO { | ||
| 16 | + | ||
| 17 | + @Schema(description = "客户端消息编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "uuid-xxx") | ||
| 18 | + @NotEmpty(message = "客户端消息编号不能为空") | ||
| 19 | + private String clientMessageId; | ||
| 20 | + | ||
| 21 | + @Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") | ||
| 22 | + @NotNull(message = "群编号不能为空") | ||
| 23 | + private Long groupId; | ||
| 24 | + | ||
| 25 | + @Schema(description = "消息类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") | ||
| 26 | + @NotNull(message = "消息类型不能为空") | ||
| 27 | + private Integer type; | ||
| 28 | + | ||
| 29 | + @Schema(description = "消息内容,JSON 格式", requiredMode = Schema.RequiredMode.REQUIRED, example = "{\"content\":\"你好\"}") | ||
| 30 | + @NotEmpty(message = "消息内容不能为空") | ||
| 31 | + private String content; | ||
| 32 | + | ||
| 33 | + @Schema(description = "@目标用户编号列表", example = "[1,2,3]") | ||
| 34 | + private List<Long> atUserIds; | ||
| 35 | + | ||
| 36 | + @Schema(description = "定向接收用户编号列表", example = "[1,2]") | ||
| 37 | + private List<Long> receiverUserIds; | ||
| 38 | + | ||
| 39 | + @Schema(description = "是否需要回执", example = "false") | ||
| 40 | + private Boolean needReceipt; | ||
| 41 | + | ||
| 42 | +} |
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/vo/privates/ImPrivateMessageReadReqVO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.controller.admin.message.vo.privates; | ||
| 2 | + | ||
| 3 | +import io.swagger.v3.oas.annotations.media.Schema; | ||
| 4 | +import jakarta.validation.constraints.NotNull; | ||
| 5 | +import lombok.Data; | ||
| 6 | + | ||
| 7 | +/** | ||
| 8 | + * 私聊已读 Request VO | ||
| 9 | + */ | ||
| 10 | +@Schema(description = "管理后台 - 私聊已读 Request VO") | ||
| 11 | +@Data | ||
| 12 | +public class ImPrivateMessageReadReqVO { | ||
| 13 | + | ||
| 14 | + @Schema(description = "好友用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") | ||
| 15 | + @NotNull(message = "好友用户编号不能为空") | ||
| 16 | + private Long friendId; | ||
| 17 | + | ||
| 18 | +} |
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/vo/privates/ImPrivateMessageRespVO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.controller.admin.message.vo.privates; | ||
| 2 | + | ||
| 3 | +import io.swagger.v3.oas.annotations.media.Schema; | ||
| 4 | +import lombok.Data; | ||
| 5 | + | ||
| 6 | +import java.time.LocalDateTime; | ||
| 7 | + | ||
| 8 | +/** | ||
| 9 | + * 私聊消息 Response VO | ||
| 10 | + */ | ||
| 11 | +@Schema(description = "管理后台 - 私聊消息 Response VO") | ||
| 12 | +@Data | ||
| 13 | +public class ImPrivateMessageRespVO { | ||
| 14 | + | ||
| 15 | + // TODO @AI:类型不对; | ||
| 16 | + @Schema(description = "消息编号", example = "1") | ||
| 17 | + private String id; | ||
| 18 | + | ||
| 19 | + @Schema(description = "客户端消息编号", example = "uuid-xxx") | ||
| 20 | + private String clientMessageId; | ||
| 21 | + | ||
| 22 | + @Schema(description = "发送人编号", example = "1") | ||
| 23 | + private String senderId; | ||
| 24 | + | ||
| 25 | + @Schema(description = "接收人编号", example = "2") | ||
| 26 | + private String receiverId; | ||
| 27 | + | ||
| 28 | + @Schema(description = "消息类型", example = "0") | ||
| 29 | + private Integer type; | ||
| 30 | + | ||
| 31 | + @Schema(description = "消息内容", example = "{\"content\":\"你好\"}") | ||
| 32 | + private String content; | ||
| 33 | + | ||
| 34 | + @Schema(description = "消息状态", example = "0") | ||
| 35 | + private Integer status; | ||
| 36 | + | ||
| 37 | + @Schema(description = "发送时间") | ||
| 38 | + private LocalDateTime sendTime; | ||
| 39 | + | ||
| 40 | +} |
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/controller/admin/message/vo/privates/ImPrivateMessageSendReqVO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.controller.admin.message.vo.privates; | ||
| 2 | + | ||
| 3 | +import io.swagger.v3.oas.annotations.media.Schema; | ||
| 4 | +import jakarta.validation.constraints.NotEmpty; | ||
| 5 | +import jakarta.validation.constraints.NotNull; | ||
| 6 | +import lombok.Data; | ||
| 7 | + | ||
| 8 | +/** | ||
| 9 | + * 私聊消息发送 Request VO | ||
| 10 | + */ | ||
| 11 | +@Schema(description = "管理后台 - 私聊消息发送 Request VO") | ||
| 12 | +@Data | ||
| 13 | +public class ImPrivateMessageSendReqVO { | ||
| 14 | + | ||
| 15 | + @Schema(description = "客户端消息编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "uuid-xxx") | ||
| 16 | + @NotEmpty(message = "客户端消息编号不能为空") | ||
| 17 | + private String clientMessageId; | ||
| 18 | + | ||
| 19 | + @Schema(description = "接收人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") | ||
| 20 | + @NotNull(message = "接收人编号不能为空") | ||
| 21 | + private Long receiverId; | ||
| 22 | + | ||
| 23 | + @Schema(description = "消息类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") | ||
| 24 | + @NotNull(message = "消息类型不能为空") | ||
| 25 | + private Integer type; | ||
| 26 | + | ||
| 27 | + @Schema(description = "消息内容,JSON 格式", requiredMode = Schema.RequiredMode.REQUIRED, example = "{\"content\":\"你好\"}") | ||
| 28 | + @NotEmpty(message = "消息内容不能为空") | ||
| 29 | + private String content; | ||
| 30 | + | ||
| 31 | +} |
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/friend/ImFriendDO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.dal.dataobject.friend; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; | ||
| 4 | +import com.baomidou.mybatisplus.annotation.KeySequence; | ||
| 5 | +import com.baomidou.mybatisplus.annotation.TableId; | ||
| 6 | +import com.baomidou.mybatisplus.annotation.TableName; | ||
| 7 | +import lombok.*; | ||
| 8 | + | ||
| 9 | +// TODO @AI:好友关系是逻辑删除,群成员是 status 标识,感觉是不是要统一下?感觉都改成 status 会不会好点?这样增加一个添加时间、和删除时间;都使用 statusenum 标识; | ||
| 10 | +/** | ||
| 11 | + * IM 好友关系 DO | ||
| 12 | + * <p> | ||
| 13 | + * 删除好友采用逻辑删除,沿用 BaseDO 的 deleted。 | ||
| 14 | + * 重新添加好友时优先恢复原关系记录,不新插重复行。 | ||
| 15 | + * | ||
| 16 | + * @author 芋道源码 | ||
| 17 | + */ | ||
| 18 | +@TableName("im_friend") | ||
| 19 | +@KeySequence("im_friend_seq") | ||
| 20 | +@Data | ||
| 21 | +@EqualsAndHashCode(callSuper = true) | ||
| 22 | +@ToString(callSuper = true) | ||
| 23 | +@Builder | ||
| 24 | +@NoArgsConstructor | ||
| 25 | +@AllArgsConstructor | ||
| 26 | +public class ImFriendDO extends BaseDO { | ||
| 27 | + | ||
| 28 | + /** | ||
| 29 | + * 编号 | ||
| 30 | + */ | ||
| 31 | + @TableId | ||
| 32 | + private Long id; | ||
| 33 | + /** | ||
| 34 | + * 用户编号 | ||
| 35 | + * | ||
| 36 | + * 关联 AdminUserDO 的 id 字段 | ||
| 37 | + */ | ||
| 38 | + private Long userId; | ||
| 39 | + /** | ||
| 40 | + * 好友用户编号 | ||
| 41 | + * | ||
| 42 | + * 关联 AdminUserDO 的 id 字段 | ||
| 43 | + */ | ||
| 44 | + private Long friendUserId; | ||
| 45 | + /** | ||
| 46 | + * 是否免打扰 | ||
| 47 | + */ | ||
| 48 | + private Boolean muted; | ||
| 49 | + | ||
| 50 | +} |
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/group/ImGroupDO.java
| @@ -26,32 +26,37 @@ public class ImGroupDO extends BaseDO { | @@ -26,32 +26,37 @@ public class ImGroupDO extends BaseDO { | ||
| 26 | */ | 26 | */ |
| 27 | @TableId | 27 | @TableId |
| 28 | private Long id; | 28 | private Long id; |
| 29 | - // TODO @hao:name,如果这个表已经是 group 了,不用在带额外的 | ||
| 30 | /** | 29 | /** |
| 31 | - * 群名字 | 30 | + * 群名称 |
| 32 | */ | 31 | */ |
| 33 | - private String groupName; | ||
| 34 | - // TODO @hao:关联字段; | 32 | + private String name; |
| 35 | /** | 33 | /** |
| 36 | - * 群主编号 | 34 | + * 群主用户编号 |
| 35 | + * <p> | ||
| 36 | + * 关联 AdminUserDO 的 id 字段 | ||
| 37 | */ | 37 | */ |
| 38 | - private Long ownerId; | ||
| 39 | - // TODO @hao:头像使用 avatar 好了,整个项目统一;然后 Thumb 是不是不用存,这个更多是文件服务做裁剪 | 38 | + private Long ownerUserId; |
| 40 | /** | 39 | /** |
| 41 | * 群头像 | 40 | * 群头像 |
| 42 | */ | 41 | */ |
| 43 | - private String headImage; | ||
| 44 | - /** | ||
| 45 | - * 群头像缩略图 | ||
| 46 | - */ | ||
| 47 | - private String headImageThumb; | 42 | + private String avatar; |
| 48 | /** | 43 | /** |
| 49 | * 群公告 | 44 | * 群公告 |
| 50 | */ | 45 | */ |
| 51 | private String notice; | 46 | private String notice; |
| 52 | /** | 47 | /** |
| 53 | - * 群备注 | 48 | + * 是否封禁 |
| 49 | + */ | ||
| 50 | + private Boolean banned; | ||
| 51 | + /** | ||
| 52 | + * 封禁原因 | ||
| 53 | + */ | ||
| 54 | + private String bannedReason; | ||
| 55 | + // TODO @AI:封禁时间; | ||
| 56 | + /** | ||
| 57 | + * 是否解散 | ||
| 54 | */ | 58 | */ |
| 55 | - private String remark; | 59 | + private Boolean dissolved; |
| 60 | + // TODO @AI:解散时间 | ||
| 56 | 61 | ||
| 57 | } | 62 | } |
| 58 | \ No newline at end of file | 63 | \ No newline at end of file |
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/group/ImGroupMemberDO.java
| 1 | package cn.iocoder.yudao.module.im.dal.dataobject.group; | 1 | package cn.iocoder.yudao.module.im.dal.dataobject.group; |
| 2 | 2 | ||
| 3 | -import lombok.*; | ||
| 4 | -import com.baomidou.mybatisplus.annotation.*; | ||
| 5 | import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; | 3 | import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; |
| 4 | +import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberStatusEnum; | ||
| 5 | +import com.baomidou.mybatisplus.annotation.KeySequence; | ||
| 6 | +import com.baomidou.mybatisplus.annotation.TableId; | ||
| 7 | +import com.baomidou.mybatisplus.annotation.TableName; | ||
| 8 | +import lombok.*; | ||
| 6 | 9 | ||
| 10 | +import java.time.LocalDateTime; | ||
| 11 | + | ||
| 12 | +// TODO @AI:好友关系是逻辑删除,群成员是 status 标识,感觉是不是要统一下? | ||
| 7 | /** | 13 | /** |
| 8 | * IM 群成员 DO | 14 | * IM 群成员 DO |
| 9 | * | 15 | * |
| @@ -24,31 +30,43 @@ public class ImGroupMemberDO extends BaseDO { | @@ -24,31 +30,43 @@ public class ImGroupMemberDO extends BaseDO { | ||
| 24 | */ | 30 | */ |
| 25 | @TableId | 31 | @TableId |
| 26 | private Long id; | 32 | private Long id; |
| 27 | - // TODO @hao:groupId 和 userId 都写下关联字段哈 | ||
| 28 | /** | 33 | /** |
| 29 | * 群编号 | 34 | * 群编号 |
| 35 | + * <p> | ||
| 36 | + * 关联 {@link ImGroupDO#getId()} | ||
| 30 | */ | 37 | */ |
| 31 | private Long groupId; | 38 | private Long groupId; |
| 32 | /** | 39 | /** |
| 33 | * 用户编号 | 40 | * 用户编号 |
| 41 | + * <p> | ||
| 42 | + * 关联 AdminUserDO 的 id 字段 | ||
| 34 | */ | 43 | */ |
| 35 | private Long userId; | 44 | private Long userId; |
| 36 | - // TODO @hao:nickname 和 avatar 是不是不用存储哈; | ||
| 37 | /** | 45 | /** |
| 38 | - * 昵称 | 46 | + * 组内显示名 |
| 47 | + */ | ||
| 48 | + private String displayUserName; | ||
| 49 | + /** | ||
| 50 | + * 群显示备注 | ||
| 51 | + */ | ||
| 52 | + private String displayGroupName; | ||
| 53 | + /** | ||
| 54 | + * 是否免打扰 | ||
| 39 | */ | 55 | */ |
| 40 | - private String nickname; | 56 | + private Boolean muted; |
| 41 | /** | 57 | /** |
| 42 | - * 头像 | 58 | + * 成员状态 |
| 59 | + * <p> | ||
| 60 | + * 枚举 {@link ImGroupMemberStatusEnum} | ||
| 43 | */ | 61 | */ |
| 44 | - private String avatar; | 62 | + private Integer status; |
| 45 | /** | 63 | /** |
| 46 | - * 组内显示名称 | 64 | + * 入群时间 |
| 47 | */ | 65 | */ |
| 48 | - private String aliasName; | 66 | + private LocalDateTime joinTime; |
| 49 | /** | 67 | /** |
| 50 | - * 备注 | 68 | + * 退群时间 |
| 51 | */ | 69 | */ |
| 52 | - private String remark; | 70 | + private LocalDateTime quitTime; |
| 53 | 71 | ||
| 54 | } | 72 | } |
| 55 | \ No newline at end of file | 73 | \ No newline at end of file |
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/ImGroupMessageDO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.dal.dataobject.message; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; | ||
| 4 | +import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO; | ||
| 5 | +import cn.iocoder.yudao.module.im.enums.message.ImGroupMessageReceiptStatusEnum; | ||
| 6 | +import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum; | ||
| 7 | +import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; | ||
| 8 | +import com.baomidou.mybatisplus.annotation.KeySequence; | ||
| 9 | +import com.baomidou.mybatisplus.annotation.TableId; | ||
| 10 | +import com.baomidou.mybatisplus.annotation.TableName; | ||
| 11 | +import lombok.*; | ||
| 12 | + | ||
| 13 | +import java.time.LocalDateTime; | ||
| 14 | + | ||
| 15 | +/** | ||
| 16 | + * IM 群聊消息 DO | ||
| 17 | + * | ||
| 18 | + * @author 芋道源码 | ||
| 19 | + */ | ||
| 20 | +@TableName("im_group_message") | ||
| 21 | +@KeySequence("im_group_message_seq") | ||
| 22 | +@Data | ||
| 23 | +@EqualsAndHashCode(callSuper = true) | ||
| 24 | +@ToString(callSuper = true) | ||
| 25 | +@Builder | ||
| 26 | +@NoArgsConstructor | ||
| 27 | +@AllArgsConstructor | ||
| 28 | +public class ImGroupMessageDO extends BaseDO { | ||
| 29 | + | ||
| 30 | + /** | ||
| 31 | + * 编号 | ||
| 32 | + */ | ||
| 33 | + @TableId | ||
| 34 | + private Long id; | ||
| 35 | + /** | ||
| 36 | + * 客户端消息编号,用于发送幂等 | ||
| 37 | + */ | ||
| 38 | + private String clientMessageId; | ||
| 39 | + /** | ||
| 40 | + * 发送人编号 | ||
| 41 | + * | ||
| 42 | + * 关联 AdminUserDO 的 id 字段 | ||
| 43 | + */ | ||
| 44 | + private Long senderId; | ||
| 45 | + /** | ||
| 46 | + * 群编号 | ||
| 47 | + * | ||
| 48 | + * 关联 {@link ImGroupDO#getId()} | ||
| 49 | + */ | ||
| 50 | + private Long groupId; | ||
| 51 | + /** | ||
| 52 | + * 消息类型 | ||
| 53 | + * <p> | ||
| 54 | + * 枚举 {@link ImMessageTypeEnum} | ||
| 55 | + */ | ||
| 56 | + private Integer type; | ||
| 57 | + /** | ||
| 58 | + * 消息内容,JSON 格式 | ||
| 59 | + * | ||
| 60 | + * TODO @AI:/Users/yunai/Java/yudao-all-in-im/ruoyi-vue-pro/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content | ||
| 61 | + */ | ||
| 62 | + private String content; | ||
| 63 | + /** | ||
| 64 | + * 消息状态 | ||
| 65 | + * <p> | ||
| 66 | + * 枚举 {@link ImMessageStatusEnum} | ||
| 67 | + * 群聊状态:NORMAL(0) / RECALL(2) | ||
| 68 | + */ | ||
| 69 | + // TODO @AI:会不会存在 READ 的情况?如果不存在,最好说明下,和单聊的差异; | ||
| 70 | + private Integer status; | ||
| 71 | + /** | ||
| 72 | + * 发送时间 | ||
| 73 | + */ | ||
| 74 | + private LocalDateTime sendTime; | ||
| 75 | + /** | ||
| 76 | + * 定向接收用户编号列表,逗号分隔 | ||
| 77 | + * <p> | ||
| 78 | + * 为空表示全员可见 | ||
| 79 | + * | ||
| 80 | + * 关联 AdminUserDO 的 id 字段 | ||
| 81 | + */ | ||
| 82 | + // TODO @AI:使用 List<Long> 存储,避免逗号分隔的字符串解析问题 | ||
| 83 | + private String receiverUserIds; | ||
| 84 | + /** | ||
| 85 | + * @ 目标用户编号列表,逗号分隔 | ||
| 86 | + * | ||
| 87 | + * 关联 AdminUserDO 的 id 字段 | ||
| 88 | + */ | ||
| 89 | + // TODO @AI:使用 List<Long> 存储,避免逗号分隔的字符串解析问题 | ||
| 90 | + private String atUserIds; | ||
| 91 | + /** | ||
| 92 | + * 回执状态 | ||
| 93 | + * <p> | ||
| 94 | + * 枚举 {@link ImGroupMessageReceiptStatusEnum} | ||
| 95 | + */ | ||
| 96 | + private Integer receiptStatus; | ||
| 97 | + | ||
| 98 | +} |
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/ImPrivateMessageDO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.dal.dataobject.message; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; | ||
| 4 | +import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum; | ||
| 5 | +import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; | ||
| 6 | +import com.baomidou.mybatisplus.annotation.KeySequence; | ||
| 7 | +import com.baomidou.mybatisplus.annotation.TableId; | ||
| 8 | +import com.baomidou.mybatisplus.annotation.TableName; | ||
| 9 | +import lombok.*; | ||
| 10 | + | ||
| 11 | +import java.time.LocalDateTime; | ||
| 12 | + | ||
| 13 | +/** | ||
| 14 | + * IM 私聊消息 DO | ||
| 15 | + * | ||
| 16 | + * @author 芋道源码 | ||
| 17 | + */ | ||
| 18 | +@TableName("im_private_message") | ||
| 19 | +@KeySequence("im_private_message_seq") | ||
| 20 | +@Data | ||
| 21 | +@EqualsAndHashCode(callSuper = true) | ||
| 22 | +@ToString(callSuper = true) | ||
| 23 | +@Builder | ||
| 24 | +@NoArgsConstructor | ||
| 25 | +@AllArgsConstructor | ||
| 26 | +public class ImPrivateMessageDO extends BaseDO { | ||
| 27 | + | ||
| 28 | + /** | ||
| 29 | + * 编号 | ||
| 30 | + */ | ||
| 31 | + @TableId | ||
| 32 | + private Long id; | ||
| 33 | + /** | ||
| 34 | + * 客户端消息编号,用于发送幂等 | ||
| 35 | + */ | ||
| 36 | + private String clientMessageId; | ||
| 37 | + /** | ||
| 38 | + * 发送人编号 | ||
| 39 | + * | ||
| 40 | + * 关联 AdminUserDO 的 id 字段 | ||
| 41 | + */ | ||
| 42 | + private Long senderId; | ||
| 43 | + /** | ||
| 44 | + * 接收人编号 | ||
| 45 | + * | ||
| 46 | + * 关联 AdminUserDO 的 id 字段 | ||
| 47 | + */ | ||
| 48 | + private Long receiverId; | ||
| 49 | + /** | ||
| 50 | + * 消息类型 | ||
| 51 | + * <p> | ||
| 52 | + * 枚举 {@link ImMessageTypeEnum} | ||
| 53 | + */ | ||
| 54 | + private Integer type; | ||
| 55 | + /** | ||
| 56 | + * 消息内容,JSON 格式 | ||
| 57 | + * | ||
| 58 | + * TODO @AI:/Users/yunai/Java/yudao-all-in-im/ruoyi-vue-pro/yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/message/content | ||
| 59 | + */ | ||
| 60 | + private String content; | ||
| 61 | + /** | ||
| 62 | + * 消息状态 | ||
| 63 | + * <p> | ||
| 64 | + * 枚举 {@link ImMessageStatusEnum} | ||
| 65 | + * 私聊状态:UNREAD(0) / READ(1) / RECALL(2) | ||
| 66 | + */ | ||
| 67 | + private Integer status; | ||
| 68 | + /** | ||
| 69 | + * 发送时间 | ||
| 70 | + */ | ||
| 71 | + private LocalDateTime sendTime; | ||
| 72 | + | ||
| 73 | +} |
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/sensitiveword/ImSensitiveWordDO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.dal.dataobject.sensitiveword; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; | ||
| 4 | +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; | ||
| 5 | +import com.baomidou.mybatisplus.annotation.KeySequence; | ||
| 6 | +import com.baomidou.mybatisplus.annotation.TableId; | ||
| 7 | +import com.baomidou.mybatisplus.annotation.TableName; | ||
| 8 | +import lombok.*; | ||
| 9 | + | ||
| 10 | +/** | ||
| 11 | + * IM 敏感词 DO | ||
| 12 | + * | ||
| 13 | + * @author 芋道源码 | ||
| 14 | + */ | ||
| 15 | +@TableName("im_sensitive_word") | ||
| 16 | +@KeySequence("im_sensitive_word_seq") | ||
| 17 | +@Data | ||
| 18 | +@EqualsAndHashCode(callSuper = true) | ||
| 19 | +@ToString(callSuper = true) | ||
| 20 | +@Builder | ||
| 21 | +@NoArgsConstructor | ||
| 22 | +@AllArgsConstructor | ||
| 23 | +public class ImSensitiveWordDO extends BaseDO { | ||
| 24 | + | ||
| 25 | + /** | ||
| 26 | + * 编号 | ||
| 27 | + */ | ||
| 28 | + @TableId | ||
| 29 | + private Long id; | ||
| 30 | + /** | ||
| 31 | + * 敏感词 | ||
| 32 | + */ | ||
| 33 | + private String word; | ||
| 34 | + /** | ||
| 35 | + * 状态 | ||
| 36 | + * <p> | ||
| 37 | + * 枚举 {@link CommonStatusEnum} | ||
| 38 | + */ | ||
| 39 | + private Integer status; | ||
| 40 | + | ||
| 41 | +} |
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/friend/ImFriendMapper.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.dal.mysql.friend; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; | ||
| 4 | +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; | ||
| 5 | +import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO; | ||
| 6 | +import org.apache.ibatis.annotations.Mapper; | ||
| 7 | + | ||
| 8 | +/** | ||
| 9 | + * IM 好友关系 Mapper | ||
| 10 | + * | ||
| 11 | + * @author 芋道源码 | ||
| 12 | + */ | ||
| 13 | +@Mapper | ||
| 14 | +public interface ImFriendMapper extends BaseMapperX<ImFriendDO> { | ||
| 15 | + | ||
| 16 | + /** | ||
| 17 | + * 查询好友关系 | ||
| 18 | + * | ||
| 19 | + * @param userId 用户编号 | ||
| 20 | + * @param friendUserId 好友用户编号 | ||
| 21 | + * @return 好友关系 | ||
| 22 | + */ | ||
| 23 | + default ImFriendDO selectByUserIdAndFriendUserId(Long userId, Long friendUserId) { | ||
| 24 | + return selectOne(new LambdaQueryWrapperX<ImFriendDO>() | ||
| 25 | + .eq(ImFriendDO::getUserId, userId) | ||
| 26 | + .eq(ImFriendDO::getFriendUserId, friendUserId)); | ||
| 27 | + } | ||
| 28 | + | ||
| 29 | +} |
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/group/ImGroupMapper.java
| @@ -7,9 +7,8 @@ import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO; | @@ -7,9 +7,8 @@ import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO; | ||
| 7 | import org.apache.ibatis.annotations.Mapper; | 7 | import org.apache.ibatis.annotations.Mapper; |
| 8 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.*; | 8 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.*; |
| 9 | 9 | ||
| 10 | -// TODO @hao:这个也要有 Im 前缀 | ||
| 11 | /** | 10 | /** |
| 12 | - * 群 Mapper | 11 | + * IM 群 Mapper |
| 13 | * | 12 | * |
| 14 | * @author 芋道源码 | 13 | * @author 芋道源码 |
| 15 | */ | 14 | */ |
| @@ -18,12 +17,9 @@ public interface ImGroupMapper extends BaseMapperX<ImGroupDO> { | @@ -18,12 +17,9 @@ public interface ImGroupMapper extends BaseMapperX<ImGroupDO> { | ||
| 18 | 17 | ||
| 19 | default PageResult<ImGroupDO> selectPage(ImGroupPageReqVO reqVO) { | 18 | default PageResult<ImGroupDO> selectPage(ImGroupPageReqVO reqVO) { |
| 20 | return selectPage(reqVO, new LambdaQueryWrapperX<ImGroupDO>() | 19 | return selectPage(reqVO, new LambdaQueryWrapperX<ImGroupDO>() |
| 21 | - .likeIfPresent(ImGroupDO::getGroupName, reqVO.getGroupName()) | ||
| 22 | - .eqIfPresent(ImGroupDO::getOwnerId, reqVO.getOwnerId()) | ||
| 23 | - .eqIfPresent(ImGroupDO::getHeadImage, reqVO.getHeadImage()) | ||
| 24 | - .eqIfPresent(ImGroupDO::getHeadImageThumb, reqVO.getHeadImageThumb()) | 20 | + .likeIfPresent(ImGroupDO::getName, reqVO.getName()) |
| 21 | + .eqIfPresent(ImGroupDO::getOwnerUserId, reqVO.getOwnerUserId()) | ||
| 25 | .eqIfPresent(ImGroupDO::getNotice, reqVO.getNotice()) | 22 | .eqIfPresent(ImGroupDO::getNotice, reqVO.getNotice()) |
| 26 | - .eqIfPresent(ImGroupDO::getRemark, reqVO.getRemark()) | ||
| 27 | .betweenIfPresent(ImGroupDO::getCreateTime, reqVO.getCreateTime()) | 23 | .betweenIfPresent(ImGroupDO::getCreateTime, reqVO.getCreateTime()) |
| 28 | .orderByDesc(ImGroupDO::getId)); | 24 | .orderByDesc(ImGroupDO::getId)); |
| 29 | } | 25 | } |
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/groupmember/ImGroupMemberMapper.java
| @@ -10,7 +10,7 @@ import org.apache.ibatis.annotations.Mapper; | @@ -10,7 +10,7 @@ import org.apache.ibatis.annotations.Mapper; | ||
| 10 | import java.util.List; | 10 | import java.util.List; |
| 11 | 11 | ||
| 12 | /** | 12 | /** |
| 13 | - * 群成员 Mapper | 13 | + * IM 群成员 Mapper |
| 14 | * | 14 | * |
| 15 | * @author 芋道源码 | 15 | * @author 芋道源码 |
| 16 | */ | 16 | */ |
| @@ -21,10 +21,7 @@ public interface ImGroupMemberMapper extends BaseMapperX<ImGroupMemberDO> { | @@ -21,10 +21,7 @@ public interface ImGroupMemberMapper extends BaseMapperX<ImGroupMemberDO> { | ||
| 21 | return selectPage(reqVO, new LambdaQueryWrapperX<ImGroupMemberDO>() | 21 | return selectPage(reqVO, new LambdaQueryWrapperX<ImGroupMemberDO>() |
| 22 | .eqIfPresent(ImGroupMemberDO::getGroupId, reqVO.getGroupId()) | 22 | .eqIfPresent(ImGroupMemberDO::getGroupId, reqVO.getGroupId()) |
| 23 | .eqIfPresent(ImGroupMemberDO::getUserId, reqVO.getUserId()) | 23 | .eqIfPresent(ImGroupMemberDO::getUserId, reqVO.getUserId()) |
| 24 | - .likeIfPresent(ImGroupMemberDO::getNickname, reqVO.getNickname()) | ||
| 25 | - .eqIfPresent(ImGroupMemberDO::getAvatar, reqVO.getAvatar()) | ||
| 26 | - .likeIfPresent(ImGroupMemberDO::getAliasName, reqVO.getAliasName()) | ||
| 27 | - .eqIfPresent(ImGroupMemberDO::getRemark, reqVO.getRemark()) | 24 | + .eqIfPresent(ImGroupMemberDO::getStatus, reqVO.getStatus()) |
| 28 | .betweenIfPresent(ImGroupMemberDO::getCreateTime, reqVO.getCreateTime()) | 25 | .betweenIfPresent(ImGroupMemberDO::getCreateTime, reqVO.getCreateTime()) |
| 29 | .orderByDesc(ImGroupMemberDO::getId)); | 26 | .orderByDesc(ImGroupMemberDO::getId)); |
| 30 | } | 27 | } |
| @@ -32,4 +29,14 @@ public interface ImGroupMemberMapper extends BaseMapperX<ImGroupMemberDO> { | @@ -32,4 +29,14 @@ public interface ImGroupMemberMapper extends BaseMapperX<ImGroupMemberDO> { | ||
| 32 | default List<ImGroupMemberDO> selectListByGroupId(Long groupId) { | 29 | default List<ImGroupMemberDO> selectListByGroupId(Long groupId) { |
| 33 | return selectList(new LambdaQueryWrapperX<ImGroupMemberDO>().eq(ImGroupMemberDO::getGroupId, groupId)); | 30 | return selectList(new LambdaQueryWrapperX<ImGroupMemberDO>().eq(ImGroupMemberDO::getGroupId, groupId)); |
| 34 | } | 31 | } |
| 32 | + | ||
| 33 | + default ImGroupMemberDO selectByGroupIdAndUserId(Long groupId, Long userId) { | ||
| 34 | + return selectOne(new LambdaQueryWrapperX<ImGroupMemberDO>() | ||
| 35 | + .eq(ImGroupMemberDO::getGroupId, groupId) | ||
| 36 | + .eq(ImGroupMemberDO::getUserId, userId)); | ||
| 37 | + } | ||
| 38 | + | ||
| 39 | + default List<ImGroupMemberDO> selectListByUserId(Long userId) { | ||
| 40 | + return selectList(new LambdaQueryWrapperX<ImGroupMemberDO>().eq(ImGroupMemberDO::getUserId, userId)); | ||
| 41 | + } | ||
| 35 | } | 42 | } |
| 36 | \ No newline at end of file | 43 | \ No newline at end of file |
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/message/ImGroupMessageMapper.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.dal.mysql.message; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; | ||
| 4 | +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; | ||
| 5 | +import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO; | ||
| 6 | +import org.apache.ibatis.annotations.Mapper; | ||
| 7 | + | ||
| 8 | +import java.util.List; | ||
| 9 | + | ||
| 10 | +/** | ||
| 11 | + * IM 群聊消息 Mapper | ||
| 12 | + * | ||
| 13 | + * @author 芋道源码 | ||
| 14 | + */ | ||
| 15 | +@Mapper | ||
| 16 | +public interface ImGroupMessageMapper extends BaseMapperX<ImGroupMessageDO> { | ||
| 17 | + | ||
| 18 | + /** | ||
| 19 | + * 根据群编号和 minId 增量拉取群聊消息 | ||
| 20 | + * | ||
| 21 | + * @param groupIds 用户所在的群编号列表 | ||
| 22 | + * @param minId 最小消息 id(不含) | ||
| 23 | + * @param size 拉取数量 | ||
| 24 | + * @return 消息列表 | ||
| 25 | + */ | ||
| 26 | + default List<ImGroupMessageDO> selectListByMinId(List<Long> groupIds, Long minId, Integer size) { | ||
| 27 | + return selectList(new LambdaQueryWrapperX<ImGroupMessageDO>() | ||
| 28 | + .in(ImGroupMessageDO::getGroupId, groupIds) | ||
| 29 | + .gt(ImGroupMessageDO::getId, minId) | ||
| 30 | + .orderByAsc(ImGroupMessageDO::getId) | ||
| 31 | + .last("LIMIT " + size)); | ||
| 32 | + } | ||
| 33 | + | ||
| 34 | + /** | ||
| 35 | + * 根据 senderId 和 clientMessageId 查询(幂等校验) | ||
| 36 | + */ | ||
| 37 | + default ImGroupMessageDO selectBySenderIdAndClientMessageId(Long senderId, String clientMessageId) { | ||
| 38 | + return selectOne(new LambdaQueryWrapperX<ImGroupMessageDO>() | ||
| 39 | + .eq(ImGroupMessageDO::getSenderId, senderId) | ||
| 40 | + .eq(ImGroupMessageDO::getClientMessageId, clientMessageId)); | ||
| 41 | + } | ||
| 42 | + | ||
| 43 | + /** | ||
| 44 | + * 获取群内最新消息 id | ||
| 45 | + */ | ||
| 46 | + default Long selectMaxIdByGroupId(Long groupId) { | ||
| 47 | + ImGroupMessageDO msg = selectOne(new LambdaQueryWrapperX<ImGroupMessageDO>() | ||
| 48 | + .eq(ImGroupMessageDO::getGroupId, groupId) | ||
| 49 | + .orderByDesc(ImGroupMessageDO::getId) | ||
| 50 | + .last("LIMIT 1")); | ||
| 51 | + return msg == null ? null : msg.getId(); | ||
| 52 | + } | ||
| 53 | + | ||
| 54 | + /** | ||
| 55 | + * 获取群内需要回执的消息(状态为 PENDING) | ||
| 56 | + */ | ||
| 57 | + default List<ImGroupMessageDO> selectPendingReceiptMessages(Long groupId) { | ||
| 58 | + return selectList(new LambdaQueryWrapperX<ImGroupMessageDO>() | ||
| 59 | + .eq(ImGroupMessageDO::getGroupId, groupId) | ||
| 60 | + .eq(ImGroupMessageDO::getReceiptStatus, 1)); // PENDING | ||
| 61 | + } | ||
| 62 | + | ||
| 63 | +} |
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/message/ImPrivateMessageMapper.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.dal.mysql.message; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; | ||
| 4 | +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; | ||
| 5 | +import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO; | ||
| 6 | +import org.apache.ibatis.annotations.Mapper; | ||
| 7 | + | ||
| 8 | +import java.util.List; | ||
| 9 | + | ||
| 10 | +/** | ||
| 11 | + * IM 私聊消息 Mapper | ||
| 12 | + * | ||
| 13 | + * @author 芋道源码 | ||
| 14 | + */ | ||
| 15 | +@Mapper | ||
| 16 | +public interface ImPrivateMessageMapper extends BaseMapperX<ImPrivateMessageDO> { | ||
| 17 | + | ||
| 18 | + /** | ||
| 19 | + * 根据 minId 增量拉取私聊消息 | ||
| 20 | + * | ||
| 21 | + * @param userId 当前用户编号 | ||
| 22 | + * @param minId 最小消息 id(不含) | ||
| 23 | + * @param size 拉取数量 | ||
| 24 | + * @return 消息列表 | ||
| 25 | + */ | ||
| 26 | + default List<ImPrivateMessageDO> selectListByMinId(Long userId, Long minId, Integer size) { | ||
| 27 | + return selectList(new LambdaQueryWrapperX<ImPrivateMessageDO>() | ||
| 28 | + .and(w -> w.eq(ImPrivateMessageDO::getSenderId, userId) | ||
| 29 | + .or() | ||
| 30 | + .eq(ImPrivateMessageDO::getReceiverId, userId)) | ||
| 31 | + .gt(ImPrivateMessageDO::getId, minId) | ||
| 32 | + .orderByAsc(ImPrivateMessageDO::getId) | ||
| 33 | + .last("LIMIT " + size)); | ||
| 34 | + } | ||
| 35 | + | ||
| 36 | + /** | ||
| 37 | + * 根据 senderId 和 clientMessageId 查询(幂等校验) | ||
| 38 | + */ | ||
| 39 | + default ImPrivateMessageDO selectBySenderIdAndClientMessageId(Long senderId, String clientMessageId) { | ||
| 40 | + return selectOne(new LambdaQueryWrapperX<ImPrivateMessageDO>() | ||
| 41 | + .eq(ImPrivateMessageDO::getSenderId, senderId) | ||
| 42 | + .eq(ImPrivateMessageDO::getClientMessageId, clientMessageId)); | ||
| 43 | + } | ||
| 44 | + | ||
| 45 | + /** | ||
| 46 | + * 批量更新私聊消息已读状态 | ||
| 47 | + * 将发送方 = friendId、接收方 = userId 且状态为 UNREAD 的消息更新为 READ | ||
| 48 | + */ | ||
| 49 | + default int updateStatusToRead(Long userId, Long friendId) { | ||
| 50 | + return update(new ImPrivateMessageDO().setStatus(1), // READ | ||
| 51 | + new LambdaQueryWrapperX<ImPrivateMessageDO>() | ||
| 52 | + .eq(ImPrivateMessageDO::getSenderId, friendId) | ||
| 53 | + .eq(ImPrivateMessageDO::getReceiverId, userId) | ||
| 54 | + .eq(ImPrivateMessageDO::getStatus, 0)); // UNREAD | ||
| 55 | + } | ||
| 56 | + | ||
| 57 | +} |
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/sensitiveword/ImSensitiveWordMapper.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.dal.mysql.sensitiveword; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; | ||
| 4 | +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; | ||
| 5 | +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; | ||
| 6 | +import cn.iocoder.yudao.module.im.dal.dataobject.sensitiveword.ImSensitiveWordDO; | ||
| 7 | +import org.apache.ibatis.annotations.Mapper; | ||
| 8 | + | ||
| 9 | +import java.util.List; | ||
| 10 | + | ||
| 11 | +/** | ||
| 12 | + * IM 敏感词 Mapper | ||
| 13 | + * | ||
| 14 | + * @author 芋道源码 | ||
| 15 | + */ | ||
| 16 | +@Mapper | ||
| 17 | +public interface ImSensitiveWordMapper extends BaseMapperX<ImSensitiveWordDO> { | ||
| 18 | + | ||
| 19 | + // TODO @AI:应该 selectListByStatus | ||
| 20 | + default List<ImSensitiveWordDO> selectListByEnabled() { | ||
| 21 | + return selectList(new LambdaQueryWrapperX<ImSensitiveWordDO>() | ||
| 22 | + .eq(ImSensitiveWordDO::getStatus, CommonStatusEnum.ENABLE.getStatus())); | ||
| 23 | + } | ||
| 24 | + | ||
| 25 | +} |
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/redis/RedisKeyConstants.java
| @@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.im.dal.redis; | @@ -2,7 +2,7 @@ package cn.iocoder.yudao.module.im.dal.redis; | ||
| 2 | 2 | ||
| 3 | 3 | ||
| 4 | /** | 4 | /** |
| 5 | - * im Redis Key 枚举类 | 5 | + * IM Redis Key 枚举类 |
| 6 | * | 6 | * |
| 7 | * @author 芋道源码 | 7 | * @author 芋道源码 |
| 8 | */ | 8 | */ |
| @@ -21,4 +21,12 @@ public interface RedisKeyConstants { | @@ -21,4 +21,12 @@ public interface RedisKeyConstants { | ||
| 21 | * VALUE 数据类型: String | 21 | * VALUE 数据类型: String |
| 22 | */ | 22 | */ |
| 23 | String INBOX_LOCK = "im_inbox_lock:%s"; | 23 | String INBOX_LOCK = "im_inbox_lock:%s"; |
| 24 | + | ||
| 25 | + /** | ||
| 26 | + * 群已读位置 | ||
| 27 | + * KEY 格式: im:group:read:{groupId} | ||
| 28 | + * VALUE 数据类型: Hash (field: userId, value: maxReadMessageId) | ||
| 29 | + */ | ||
| 30 | + String GROUP_READ_POSITION = "im:group:read:%s"; | ||
| 31 | + | ||
| 24 | } | 32 | } |
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/dal/redis/group/GroupReadPositionRedisDAO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.dal.redis.group; | ||
| 2 | + | ||
| 3 | +import jakarta.annotation.Resource; | ||
| 4 | +import org.springframework.data.redis.core.StringRedisTemplate; | ||
| 5 | +import org.springframework.stereotype.Repository; | ||
| 6 | + | ||
| 7 | +import java.util.Map; | ||
| 8 | + | ||
| 9 | +import static cn.iocoder.yudao.module.im.dal.redis.RedisKeyConstants.GROUP_READ_POSITION; | ||
| 10 | + | ||
| 11 | +/** | ||
| 12 | + * 群已读位置 Redis DAO | ||
| 13 | + * <p> | ||
| 14 | + * 采用 Redis Hash 存储:key=im:group:read:{groupId}, field=userId, value=maxReadMessageId | ||
| 15 | + * | ||
| 16 | + * @author 芋道源码 | ||
| 17 | + */ | ||
| 18 | +@Repository | ||
| 19 | +public class GroupReadPositionRedisDAO { | ||
| 20 | + | ||
| 21 | + @Resource | ||
| 22 | + private StringRedisTemplate stringRedisTemplate; | ||
| 23 | + | ||
| 24 | + /** | ||
| 25 | + * 更新用户在某群的已读位置 | ||
| 26 | + * | ||
| 27 | + * @param groupId 群编号 | ||
| 28 | + * @param userId 用户编号 | ||
| 29 | + * @param maxMessageId 最大已读消息编号 | ||
| 30 | + */ | ||
| 31 | + public void updateReadPosition(Long groupId, Long userId, Long maxMessageId) { | ||
| 32 | + String key = String.format(GROUP_READ_POSITION, groupId); | ||
| 33 | + // 只在新值大于旧值时更新 | ||
| 34 | + Object currentVal = stringRedisTemplate.opsForHash().get(key, userId.toString()); | ||
| 35 | + if (currentVal == null || Long.parseLong(currentVal.toString()) < maxMessageId) { | ||
| 36 | + stringRedisTemplate.opsForHash().put(key, userId.toString(), maxMessageId.toString()); | ||
| 37 | + } | ||
| 38 | + } | ||
| 39 | + | ||
| 40 | + /** | ||
| 41 | + * 获取用户在某群的已读位置 | ||
| 42 | + */ | ||
| 43 | + public Long getReadPosition(Long groupId, Long userId) { | ||
| 44 | + String key = String.format(GROUP_READ_POSITION, groupId); | ||
| 45 | + Object val = stringRedisTemplate.opsForHash().get(key, userId.toString()); | ||
| 46 | + return val == null ? 0L : Long.parseLong(val.toString()); | ||
| 47 | + } | ||
| 48 | + | ||
| 49 | + /** | ||
| 50 | + * 获取某群所有用户的已读位置 | ||
| 51 | + */ | ||
| 52 | + public Map<Object, Object> getAllReadPositions(Long groupId) { | ||
| 53 | + String key = String.format(GROUP_READ_POSITION, groupId); | ||
| 54 | + return stringRedisTemplate.opsForHash().entries(key); | ||
| 55 | + } | ||
| 56 | + | ||
| 57 | + /** | ||
| 58 | + * 删除某群所有用户的已读位置(群解散时调用) | ||
| 59 | + */ | ||
| 60 | + public void deleteGroupPositions(Long groupId) { | ||
| 61 | + String key = String.format(GROUP_READ_POSITION, groupId); | ||
| 62 | + stringRedisTemplate.delete(key); | ||
| 63 | + } | ||
| 64 | + | ||
| 65 | + /** | ||
| 66 | + * 删除用户在某群的已读位置(退群时调用) | ||
| 67 | + */ | ||
| 68 | + public void deleteUserPosition(Long groupId, Long userId) { | ||
| 69 | + String key = String.format(GROUP_READ_POSITION, groupId); | ||
| 70 | + stringRedisTemplate.opsForHash().delete(key, userId.toString()); | ||
| 71 | + } | ||
| 72 | + | ||
| 73 | +} |
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendService.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.service.friend; | ||
| 2 | + | ||
| 3 | +/** | ||
| 4 | + * IM 好友关系 Service 接口 | ||
| 5 | + * | ||
| 6 | + * @author 芋道源码 | ||
| 7 | + */ | ||
| 8 | +public interface ImFriendService { | ||
| 9 | + | ||
| 10 | + /** | ||
| 11 | + * 校验两个用户是否是好友关系(未删除状态) | ||
| 12 | + * <p> | ||
| 13 | + * 如果不是好友,直接抛出业务异常 | ||
| 14 | + * | ||
| 15 | + * @param userId 用户编号 | ||
| 16 | + * @param friendUserId 好友用户编号 | ||
| 17 | + */ | ||
| 18 | + void validateIsFriend(Long userId, Long friendUserId); | ||
| 19 | + | ||
| 20 | +} |
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImpl.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.service.friend; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO; | ||
| 4 | +import cn.iocoder.yudao.module.im.dal.mysql.friend.ImFriendMapper; | ||
| 5 | +import jakarta.annotation.Resource; | ||
| 6 | +import org.springframework.stereotype.Service; | ||
| 7 | +import org.springframework.validation.annotation.Validated; | ||
| 8 | + | ||
| 9 | +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; | ||
| 10 | +import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.FRIEND_NOT_FRIEND; | ||
| 11 | + | ||
| 12 | +/** | ||
| 13 | + * IM 好友关系 Service 实现类 | ||
| 14 | + * | ||
| 15 | + * @author 芋道源码 | ||
| 16 | + */ | ||
| 17 | +@Service | ||
| 18 | +@Validated | ||
| 19 | +public class ImFriendServiceImpl implements ImFriendService { | ||
| 20 | + | ||
| 21 | + @Resource | ||
| 22 | + private ImFriendMapper imFriendMapper; | ||
| 23 | + | ||
| 24 | + // TODO @AI:逻辑这块,在优化下; | ||
| 25 | + @Override | ||
| 26 | + public void validateIsFriend(Long userId, Long friendUserId) { | ||
| 27 | + // 好友关系表中,deleted = false 的记录表示仍是好友 | ||
| 28 | + // BaseMapperX 默认会过滤 deleted = true 的记录 | ||
| 29 | + ImFriendDO friend = imFriendMapper.selectByUserIdAndFriendUserId(userId, friendUserId); | ||
| 30 | + if (friend == null) { | ||
| 31 | + throw exception(FRIEND_NOT_FRIEND); | ||
| 32 | + } | ||
| 33 | + } | ||
| 34 | + | ||
| 35 | +} |
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupService.java
| @@ -50,4 +50,12 @@ public interface ImGroupService { | @@ -50,4 +50,12 @@ public interface ImGroupService { | ||
| 50 | */ | 50 | */ |
| 51 | PageResult<ImGroupDO> getGroupPage(ImGroupPageReqVO pageReqVO); | 51 | PageResult<ImGroupDO> getGroupPage(ImGroupPageReqVO pageReqVO); |
| 52 | 52 | ||
| 53 | + /** | ||
| 54 | + * 校验群存在且未封禁、未解散 | ||
| 55 | + * | ||
| 56 | + * @param groupId 群编号 | ||
| 57 | + * @return 群信息 | ||
| 58 | + */ | ||
| 59 | + ImGroupDO validateGroupExists(Long groupId); | ||
| 60 | + | ||
| 53 | } | 61 | } |
| 54 | \ No newline at end of file | 62 | \ No newline at end of file |
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImpl.java
| @@ -52,10 +52,19 @@ public class ImGroupServiceImpl implements ImGroupService { | @@ -52,10 +52,19 @@ public class ImGroupServiceImpl implements ImGroupService { | ||
| 52 | imGroupMapper.deleteById(id); | 52 | imGroupMapper.deleteById(id); |
| 53 | } | 53 | } |
| 54 | 54 | ||
| 55 | - private void validateGroupExists(Long id) { | ||
| 56 | - if (imGroupMapper.selectById(id) == null) { | 55 | + @Override |
| 56 | + public ImGroupDO validateGroupExists(Long id) { | ||
| 57 | + ImGroupDO group = imGroupMapper.selectById(id); | ||
| 58 | + if (group == null) { | ||
| 57 | throw exception(GROUP_NOT_EXISTS); | 59 | throw exception(GROUP_NOT_EXISTS); |
| 58 | } | 60 | } |
| 61 | + if (Boolean.TRUE.equals(group.getBanned())) { | ||
| 62 | + throw exception(GROUP_BANNED); | ||
| 63 | + } | ||
| 64 | + if (Boolean.TRUE.equals(group.getDissolved())) { | ||
| 65 | + throw exception(GROUP_DISSOLVED); | ||
| 66 | + } | ||
| 67 | + return group; | ||
| 59 | } | 68 | } |
| 60 | 69 | ||
| 61 | @Override | 70 | @Override |
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/groupmember/ImGroupMemberService.java
| @@ -60,4 +60,21 @@ public interface ImGroupMemberService { | @@ -60,4 +60,21 @@ public interface ImGroupMemberService { | ||
| 60 | * @return 群成员列表 | 60 | * @return 群成员列表 |
| 61 | */ | 61 | */ |
| 62 | List<ImGroupMemberDO> selectByGroupId(Long groupId); | 62 | List<ImGroupMemberDO> selectByGroupId(Long groupId); |
| 63 | + | ||
| 64 | + /** | ||
| 65 | + * 根据群编号和用户编号查询群成员 | ||
| 66 | + * | ||
| 67 | + * @param groupId 群编号 | ||
| 68 | + * @param userId 用户编号 | ||
| 69 | + * @return 群成员 | ||
| 70 | + */ | ||
| 71 | + ImGroupMemberDO getGroupMember(Long groupId, Long userId); | ||
| 72 | + | ||
| 73 | + /** | ||
| 74 | + * 查询用户所在的所有群的成员记录 | ||
| 75 | + * | ||
| 76 | + * @param userId 用户编号 | ||
| 77 | + * @return 群成员记录列表 | ||
| 78 | + */ | ||
| 79 | + List<ImGroupMemberDO> getGroupMembersByUserId(Long userId); | ||
| 63 | } | 80 | } |
| 64 | \ No newline at end of file | 81 | \ No newline at end of file |
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/groupmember/ImGroupMemberServiceImpl.java
| @@ -75,4 +75,14 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { | @@ -75,4 +75,14 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { | ||
| 75 | return imGroupMemberMapper.selectListByGroupId(groupId); | 75 | return imGroupMemberMapper.selectListByGroupId(groupId); |
| 76 | } | 76 | } |
| 77 | 77 | ||
| 78 | + @Override | ||
| 79 | + public ImGroupMemberDO getGroupMember(Long groupId, Long userId) { | ||
| 80 | + return imGroupMemberMapper.selectByGroupIdAndUserId(groupId, userId); | ||
| 81 | + } | ||
| 82 | + | ||
| 83 | + @Override | ||
| 84 | + public List<ImGroupMemberDO> getGroupMembersByUserId(Long userId) { | ||
| 85 | + return imGroupMemberMapper.selectListByUserId(userId); | ||
| 86 | + } | ||
| 87 | + | ||
| 78 | } | 88 | } |
| 79 | \ No newline at end of file | 89 | \ No newline at end of file |
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/inbox/ImInboxService.java
| @@ -19,7 +19,7 @@ public interface ImInboxService { | @@ -19,7 +19,7 @@ public interface ImInboxService { | ||
| 19 | void saveInboxAndSendMessage(ImMessageDO imMessageDO); | 19 | void saveInboxAndSendMessage(ImMessageDO imMessageDO); |
| 20 | 20 | ||
| 21 | /** | 21 | /** |
| 22 | - * 获得大于 sequence 的消息ids | 22 | + * 获得大于 sequence 的消息 ids |
| 23 | * | 23 | * |
| 24 | * @param userId 用户编号 | 24 | * @param userId 用户编号 |
| 25 | * @param sequence 序列号 | 25 | * @param sequence 序列号 |
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageService.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.service.message; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessageSendReqVO; | ||
| 4 | +import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO; | ||
| 5 | + | ||
| 6 | +import java.util.List; | ||
| 7 | + | ||
| 8 | +/** | ||
| 9 | + * IM 群聊消息 Service 接口 | ||
| 10 | + * | ||
| 11 | + * @author 芋道源码 | ||
| 12 | + */ | ||
| 13 | +public interface ImGroupMessageService { | ||
| 14 | + | ||
| 15 | + /** | ||
| 16 | + * 发送群聊消息 | ||
| 17 | + * | ||
| 18 | + * @param senderId 发送人编号 | ||
| 19 | + * @param reqVO 发送请求 | ||
| 20 | + * @return 消息 | ||
| 21 | + */ | ||
| 22 | + ImGroupMessageDO sendMessage(Long senderId, ImGroupMessageSendReqVO reqVO); | ||
| 23 | + | ||
| 24 | + /** | ||
| 25 | + * 拉取群聊消息(增量) | ||
| 26 | + * | ||
| 27 | + * @param userId 当前用户编号 | ||
| 28 | + * @param minId 最小消息 id(不含) | ||
| 29 | + * @param size 拉取数量 | ||
| 30 | + * @return 消息列表 | ||
| 31 | + */ | ||
| 32 | + List<ImGroupMessageDO> pullMessages(Long userId, Long minId, Integer size); | ||
| 33 | + | ||
| 34 | + /** | ||
| 35 | + * 标记群聊消息已读 | ||
| 36 | + * | ||
| 37 | + * @param userId 当前用户编号 | ||
| 38 | + * @param groupId 群编号 | ||
| 39 | + */ | ||
| 40 | + void readMessages(Long userId, Long groupId); | ||
| 41 | + | ||
| 42 | + /** | ||
| 43 | + * 撤回群聊消息 | ||
| 44 | + * | ||
| 45 | + * @param userId 当前用户编号 | ||
| 46 | + * @param messageId 消息编号 | ||
| 47 | + */ | ||
| 48 | + void recallMessage(Long userId, Long messageId); | ||
| 49 | + | ||
| 50 | + /** | ||
| 51 | + * 获取群消息的已读用户列表 | ||
| 52 | + * | ||
| 53 | + * @param userId 当前用户编号 | ||
| 54 | + * @param groupId 群编号 | ||
| 55 | + * @param messageId 消息编号 | ||
| 56 | + * @return 已读用户编号列表 | ||
| 57 | + */ | ||
| 58 | + List<Long> getReadUsers(Long userId, Long groupId, Long messageId); | ||
| 59 | + | ||
| 60 | +} |
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImpl.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.service.message; | ||
| 2 | + | ||
| 3 | +import cn.hutool.core.collection.CollUtil; | ||
| 4 | +import cn.hutool.core.util.StrUtil; | ||
| 5 | +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; | ||
| 6 | +import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessageSendReqVO; | ||
| 7 | +import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO; | ||
| 8 | +import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; | ||
| 9 | +import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO; | ||
| 10 | +import cn.iocoder.yudao.module.im.dal.mysql.message.ImGroupMessageMapper; | ||
| 11 | +import cn.iocoder.yudao.module.im.dal.redis.group.GroupReadPositionRedisDAO; | ||
| 12 | +import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberStatusEnum; | ||
| 13 | +import cn.iocoder.yudao.module.im.enums.message.ImGroupMessageReceiptStatusEnum; | ||
| 14 | +import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum; | ||
| 15 | +import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; | ||
| 16 | +import cn.iocoder.yudao.module.im.enums.message.ImWebSocketTypeConstants; | ||
| 17 | +import cn.iocoder.yudao.module.im.service.group.ImGroupService; | ||
| 18 | +import cn.iocoder.yudao.module.im.service.groupmember.ImGroupMemberService; | ||
| 19 | +import cn.iocoder.yudao.module.im.service.sensitiveword.ImSensitiveWordService; | ||
| 20 | +import cn.iocoder.yudao.module.infra.api.websocket.WebSocketSenderApi; | ||
| 21 | +import jakarta.annotation.Resource; | ||
| 22 | +import lombok.extern.slf4j.Slf4j; | ||
| 23 | +import org.springframework.stereotype.Service; | ||
| 24 | +import org.springframework.validation.annotation.Validated; | ||
| 25 | + | ||
| 26 | +import java.time.LocalDateTime; | ||
| 27 | +import java.util.*; | ||
| 28 | +import java.util.stream.Collectors; | ||
| 29 | + | ||
| 30 | +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; | ||
| 31 | +import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*; | ||
| 32 | + | ||
| 33 | +// TODO @AI:参考 private message 先优化一把! | ||
| 34 | +/** | ||
| 35 | + * IM 群聊消息 Service 实现类 | ||
| 36 | + * | ||
| 37 | + * @author 芋道源码 | ||
| 38 | + */ | ||
| 39 | +@Service | ||
| 40 | +@Validated | ||
| 41 | +@Slf4j | ||
| 42 | +public class ImGroupMessageServiceImpl implements ImGroupMessageService { | ||
| 43 | + | ||
| 44 | + private static final int MAX_PULL_SIZE = 1000; | ||
| 45 | + | ||
| 46 | + @Resource | ||
| 47 | + private ImGroupMessageMapper imGroupMessageMapper; | ||
| 48 | + @Resource | ||
| 49 | + private ImGroupService imGroupService; | ||
| 50 | + @Resource | ||
| 51 | + private ImGroupMemberService imGroupMemberService; | ||
| 52 | + @Resource | ||
| 53 | + private ImSensitiveWordService imSensitiveWordService; | ||
| 54 | + @Resource | ||
| 55 | + private GroupReadPositionRedisDAO groupReadPositionRedisDAO; | ||
| 56 | + @Resource | ||
| 57 | + private WebSocketSenderApi webSocketSenderApi; | ||
| 58 | + | ||
| 59 | + @Override | ||
| 60 | + public ImGroupMessageDO sendMessage(Long senderId, ImGroupMessageSendReqVO reqVO) { | ||
| 61 | + // 1. 幂等校验 | ||
| 62 | + ImGroupMessageDO existing = imGroupMessageMapper.selectBySenderIdAndClientMessageId( | ||
| 63 | + senderId, reqVO.getClientMessageId()); | ||
| 64 | + if (existing != null) { | ||
| 65 | + return existing; | ||
| 66 | + } | ||
| 67 | + | ||
| 68 | + // 2. 校验群存在且未解散 | ||
| 69 | + ImGroupDO group = imGroupService.validateGroupExists(reqVO.getGroupId()); | ||
| 70 | + | ||
| 71 | + // 3. 校验发送人在群中 | ||
| 72 | + ImGroupMemberDO member = imGroupMemberService.getGroupMember(reqVO.getGroupId(), senderId); | ||
| 73 | + if (member == null || ImGroupMemberStatusEnum.QUIT.getStatus().equals(member.getStatus())) { | ||
| 74 | + throw exception(GROUP_MEMBER_NOT_IN_GROUP); | ||
| 75 | + } | ||
| 76 | + | ||
| 77 | + // 4. 文本消息敏感词过滤 | ||
| 78 | + if (ImMessageTypeEnum.TEXT.getType().equals(reqVO.getType())) { | ||
| 79 | + imSensitiveWordService.validateText(reqVO.getContent()); | ||
| 80 | + } | ||
| 81 | + | ||
| 82 | + // 5. 构建消息 | ||
| 83 | + ImGroupMessageDO message = ImGroupMessageDO.builder() | ||
| 84 | + .clientMessageId(reqVO.getClientMessageId()) | ||
| 85 | + .senderId(senderId) | ||
| 86 | + .groupId(reqVO.getGroupId()) | ||
| 87 | + .type(reqVO.getType()) | ||
| 88 | + .content(reqVO.getContent()) | ||
| 89 | + .status(ImMessageStatusEnum.NORMAL) // 群聊正常状态 = 0 | ||
| 90 | + .sendTime(LocalDateTime.now()) | ||
| 91 | + .atUserIds(CollUtil.isNotEmpty(reqVO.getAtUserIds()) | ||
| 92 | + ? StrUtil.join(",", reqVO.getAtUserIds()) : null) | ||
| 93 | + .receiverUserIds(CollUtil.isNotEmpty(reqVO.getReceiverUserIds()) | ||
| 94 | + ? StrUtil.join(",", reqVO.getReceiverUserIds()) : null) | ||
| 95 | + .receiptStatus(Boolean.TRUE.equals(reqVO.getNeedReceipt()) | ||
| 96 | + ? ImGroupMessageReceiptStatusEnum.PENDING.getStatus() | ||
| 97 | + : ImGroupMessageReceiptStatusEnum.NO_RECEIPT.getStatus()) | ||
| 98 | + .build(); | ||
| 99 | + imGroupMessageMapper.insert(message); | ||
| 100 | + | ||
| 101 | + // 6. WebSocket 推送给群成员 | ||
| 102 | + sendGroupMessageEvent(message, reqVO.getGroupId()); | ||
| 103 | + return message; | ||
| 104 | + } | ||
| 105 | + | ||
| 106 | + @Override | ||
| 107 | + public List<ImGroupMessageDO> pullMessages(Long userId, Long minId, Integer size) { | ||
| 108 | + if (size > MAX_PULL_SIZE) { | ||
| 109 | + throw exception(MESSAGE_PULL_SIZE_EXCEEDED); | ||
| 110 | + } | ||
| 111 | + // 获取用户所在的所有群 | ||
| 112 | + List<ImGroupMemberDO> members = imGroupMemberService.getGroupMembersByUserId(userId); | ||
| 113 | + if (CollUtil.isEmpty(members)) { | ||
| 114 | + return Collections.emptyList(); | ||
| 115 | + } | ||
| 116 | + List<Long> groupIds = members.stream() | ||
| 117 | + .map(ImGroupMemberDO::getGroupId) | ||
| 118 | + .collect(Collectors.toList()); | ||
| 119 | + | ||
| 120 | + // 拉取消息 | ||
| 121 | + List<ImGroupMessageDO> messages = imGroupMessageMapper.selectListByMinId(groupIds, minId, size); | ||
| 122 | + | ||
| 123 | + // 过滤:入群前消息不可见、定向接收过滤、退群后消息边界 | ||
| 124 | + Map<Long, ImGroupMemberDO> memberMap = members.stream() | ||
| 125 | + .collect(Collectors.toMap(ImGroupMemberDO::getGroupId, m -> m, (a, b) -> a)); | ||
| 126 | + | ||
| 127 | + return messages.stream().filter(msg -> { | ||
| 128 | + ImGroupMemberDO m = memberMap.get(msg.getGroupId()); | ||
| 129 | + if (m == null) { | ||
| 130 | + return false; | ||
| 131 | + } | ||
| 132 | + // 入群前消息不可见 | ||
| 133 | + if (m.getJoinTime() != null && msg.getSendTime().isBefore(m.getJoinTime())) { | ||
| 134 | + return false; | ||
| 135 | + } | ||
| 136 | + // 退群后消息边界 | ||
| 137 | + if (ImGroupMemberStatusEnum.QUIT.getStatus().equals(m.getStatus()) | ||
| 138 | + && m.getQuitTime() != null && msg.getSendTime().isAfter(m.getQuitTime())) { | ||
| 139 | + return false; | ||
| 140 | + } | ||
| 141 | + // 定向接收过滤 | ||
| 142 | + if (StrUtil.isNotBlank(msg.getReceiverUserIds())) { | ||
| 143 | + Set<String> receivers = new HashSet<>(Arrays.asList(msg.getReceiverUserIds().split(","))); | ||
| 144 | + if (!receivers.contains(userId.toString()) && !msg.getSenderId().equals(userId)) { | ||
| 145 | + return false; | ||
| 146 | + } | ||
| 147 | + } | ||
| 148 | + return true; | ||
| 149 | + }).collect(Collectors.toList()); | ||
| 150 | + } | ||
| 151 | + | ||
| 152 | + @Override | ||
| 153 | + public void readMessages(Long userId, Long groupId) { | ||
| 154 | + // 1. 校验用户在群中(权限校验) | ||
| 155 | + ImGroupMemberDO member = imGroupMemberService.getGroupMember(groupId, userId); | ||
| 156 | + if (member == null || ImGroupMemberStatusEnum.QUIT.getStatus().equals(member.getStatus())) { | ||
| 157 | + throw exception(GROUP_MEMBER_NOT_IN_GROUP); | ||
| 158 | + } | ||
| 159 | + | ||
| 160 | + // 2. 高效获取群内最新消息 id(倒序 LIMIT 1,非全量扫描) | ||
| 161 | + Long maxMessageId = imGroupMessageMapper.selectMaxIdByGroupId(groupId); | ||
| 162 | + if (maxMessageId == null) { | ||
| 163 | + return; | ||
| 164 | + } | ||
| 165 | + | ||
| 166 | + // 3. 更新 Redis 群已读位置 | ||
| 167 | + groupReadPositionRedisDAO.updateReadPosition(groupId, userId, maxMessageId); | ||
| 168 | + | ||
| 169 | + // 4. 发送 READ 事件给自己的其他终端 | ||
| 170 | + Map<String, Object> readEvent = new HashMap<>(); | ||
| 171 | + readEvent.put("groupId", groupId.toString()); | ||
| 172 | + readEvent.put("messageScene", "group"); | ||
| 173 | + webSocketSenderApi.sendObject(UserTypeEnum.ADMIN.getValue(), userId, | ||
| 174 | + ImWebSocketTypeConstants.READ, readEvent); | ||
| 175 | + | ||
| 176 | + // 5. 刷新群回执 | ||
| 177 | + refreshGroupReceipts(groupId); | ||
| 178 | + } | ||
| 179 | + | ||
| 180 | + @Override | ||
| 181 | + public void recallMessage(Long userId, Long messageId) { | ||
| 182 | + // 1. 校验消息存在 | ||
| 183 | + ImGroupMessageDO message = imGroupMessageMapper.selectById(messageId); | ||
| 184 | + if (message == null) { | ||
| 185 | + throw exception(MESSAGE_NOT_EXISTS); | ||
| 186 | + } | ||
| 187 | + // 2. 只能撤回自己发送的消息 | ||
| 188 | + if (!message.getSenderId().equals(userId)) { | ||
| 189 | + throw exception(MESSAGE_RECALL_DENIED); | ||
| 190 | + } | ||
| 191 | + // 3. 不能重复撤回 | ||
| 192 | + if (ImMessageStatusEnum.RECALL.getStatus().equals(message.getStatus())) { | ||
| 193 | + throw exception(MESSAGE_ALREADY_RECALLED); | ||
| 194 | + } | ||
| 195 | + // 4. 更新消息状态 | ||
| 196 | + ImGroupMessageDO updateObj = new ImGroupMessageDO(); | ||
| 197 | + updateObj.setId(messageId); | ||
| 198 | + updateObj.setStatus(ImMessageStatusEnum.RECALL.getStatus()); | ||
| 199 | + imGroupMessageMapper.updateById(updateObj); | ||
| 200 | + | ||
| 201 | + // 5. 发送 RECALL 事件给所有群成员 | ||
| 202 | + List<ImGroupMemberDO> members = imGroupMemberService.selectByGroupId(message.getGroupId()); | ||
| 203 | + Map<String, Object> recallEvent = new HashMap<>(); | ||
| 204 | + recallEvent.put("messageId", messageId.toString()); | ||
| 205 | + recallEvent.put("groupId", message.getGroupId().toString()); | ||
| 206 | + recallEvent.put("messageScene", "group"); | ||
| 207 | + recallEvent.put("senderId", userId.toString()); | ||
| 208 | + for (ImGroupMemberDO m : members) { | ||
| 209 | + if (ImGroupMemberStatusEnum.QUIT.getStatus().equals(m.getStatus())) { | ||
| 210 | + continue; | ||
| 211 | + } | ||
| 212 | + webSocketSenderApi.sendObject(UserTypeEnum.ADMIN.getValue(), m.getUserId(), | ||
| 213 | + ImWebSocketTypeConstants.RECALL, recallEvent); | ||
| 214 | + } | ||
| 215 | + } | ||
| 216 | + | ||
| 217 | + @Override | ||
| 218 | + public List<Long> getReadUsers(Long userId, Long groupId, Long messageId) { | ||
| 219 | + // 1. 校验用户在群中(权限校验) | ||
| 220 | + ImGroupMemberDO currentMember = imGroupMemberService.getGroupMember(groupId, userId); | ||
| 221 | + if (currentMember == null || ImGroupMemberStatusEnum.QUIT.getStatus().equals(currentMember.getStatus())) { | ||
| 222 | + throw exception(GROUP_MEMBER_NOT_IN_GROUP); | ||
| 223 | + } | ||
| 224 | + | ||
| 225 | + // 2. 获取消息 | ||
| 226 | + ImGroupMessageDO message = imGroupMessageMapper.selectById(messageId); | ||
| 227 | + if (message == null) { | ||
| 228 | + return Collections.emptyList(); | ||
| 229 | + } | ||
| 230 | + | ||
| 231 | + // 2. 获取所有成员和已读位置 | ||
| 232 | + List<ImGroupMemberDO> allMembers = imGroupMemberService.selectByGroupId(groupId); | ||
| 233 | + Map<Object, Object> allPositions = groupReadPositionRedisDAO.getAllReadPositions(groupId); | ||
| 234 | + | ||
| 235 | + // 3. 计算该消息的可见成员集合 | ||
| 236 | + Set<Long> visibleUserIds = getVisibleUserIds(message, allMembers); | ||
| 237 | + | ||
| 238 | + // 4. 只返回在可见范围内且已读位置 >= messageId 的用户 | ||
| 239 | + List<Long> readUserIds = new ArrayList<>(); | ||
| 240 | + for (Map.Entry<Object, Object> entry : allPositions.entrySet()) { | ||
| 241 | + Long uid = Long.parseLong(entry.getKey().toString()); | ||
| 242 | + Long readMaxId = Long.parseLong(entry.getValue().toString()); | ||
| 243 | + if (readMaxId >= messageId && visibleUserIds.contains(uid) | ||
| 244 | + && !uid.equals(message.getSenderId())) { // 排除发送者自己 | ||
| 245 | + readUserIds.add(uid); | ||
| 246 | + } | ||
| 247 | + } | ||
| 248 | + return readUserIds; | ||
| 249 | + } | ||
| 250 | + | ||
| 251 | + /** | ||
| 252 | + * 刷新群回执(仅处理 PENDING 状态的消息,无需全量扫描) | ||
| 253 | + */ | ||
| 254 | + private void refreshGroupReceipts(Long groupId) { | ||
| 255 | + // 1. 只查 PENDING 状态的回执消息 | ||
| 256 | + List<ImGroupMessageDO> pendingMessages = imGroupMessageMapper.selectPendingReceiptMessages(groupId); | ||
| 257 | + if (CollUtil.isEmpty(pendingMessages)) { | ||
| 258 | + return; | ||
| 259 | + } | ||
| 260 | + | ||
| 261 | + // 2. 加载成员和已读位置 | ||
| 262 | + List<ImGroupMemberDO> allMembers = imGroupMemberService.selectByGroupId(groupId); | ||
| 263 | + Map<Object, Object> allPositions = groupReadPositionRedisDAO.getAllReadPositions(groupId); | ||
| 264 | + | ||
| 265 | + for (ImGroupMessageDO msg : pendingMessages) { | ||
| 266 | + // 3. 计算该消息的可见成员(排除发送者自己) | ||
| 267 | + Set<Long> visibleUserIds = getVisibleUserIds(msg, allMembers); | ||
| 268 | + visibleUserIds.remove(msg.getSenderId()); // 发送者不计入回执统计 | ||
| 269 | + | ||
| 270 | + if (visibleUserIds.isEmpty()) { | ||
| 271 | + continue; | ||
| 272 | + } | ||
| 273 | + | ||
| 274 | + // 4. 统计可见成员中的已读人数 | ||
| 275 | + int readCount = 0; | ||
| 276 | + for (Long uid : visibleUserIds) { | ||
| 277 | + Object val = allPositions.get(uid.toString()); | ||
| 278 | + if (val != null && Long.parseLong(val.toString()) >= msg.getId()) { | ||
| 279 | + readCount++; | ||
| 280 | + } | ||
| 281 | + } | ||
| 282 | + | ||
| 283 | + // 5. 判断是否全部已读 | ||
| 284 | + boolean allRead = readCount >= visibleUserIds.size(); | ||
| 285 | + if (allRead) { | ||
| 286 | + ImGroupMessageDO update = new ImGroupMessageDO(); | ||
| 287 | + update.setId(msg.getId()); | ||
| 288 | + update.setReceiptStatus(ImGroupMessageReceiptStatusEnum.DONE.getStatus()); | ||
| 289 | + imGroupMessageMapper.updateById(update); | ||
| 290 | + } | ||
| 291 | + | ||
| 292 | + // 6. 广播 RECEIPT 事件给活跃成员 | ||
| 293 | + Map<String, Object> receiptEvent = new HashMap<>(); | ||
| 294 | + receiptEvent.put("messageId", msg.getId().toString()); | ||
| 295 | + receiptEvent.put("groupId", groupId.toString()); | ||
| 296 | + receiptEvent.put("readCount", readCount); | ||
| 297 | + receiptEvent.put("receiptStatus", allRead | ||
| 298 | + ? ImGroupMessageReceiptStatusEnum.DONE.getStatus() | ||
| 299 | + : ImGroupMessageReceiptStatusEnum.PENDING.getStatus()); | ||
| 300 | + | ||
| 301 | + for (ImGroupMemberDO m : allMembers) { | ||
| 302 | + if (ImGroupMemberStatusEnum.QUIT.getStatus().equals(m.getStatus())) { | ||
| 303 | + continue; | ||
| 304 | + } | ||
| 305 | + webSocketSenderApi.sendObject(UserTypeEnum.ADMIN.getValue(), m.getUserId(), | ||
| 306 | + ImWebSocketTypeConstants.RECEIPT, receiptEvent); | ||
| 307 | + } | ||
| 308 | + } | ||
| 309 | + } | ||
| 310 | + | ||
| 311 | + /** | ||
| 312 | + * 计算一条群消息的可见成员集合 | ||
| 313 | + * <p> | ||
| 314 | + * 规则: | ||
| 315 | + * 1. 成员必须是 NORMAL 状态,或虽已 QUIT 但消息发送时间在退群之前 | ||
| 316 | + * 2. 成员的入群时间必须在消息发送之前(入群后才能看到消息) | ||
| 317 | + * 3. 如果消息有 receiverUserIds 定向接收,则只有列表中的人可见 | ||
| 318 | + */ | ||
| 319 | + private Set<Long> getVisibleUserIds(ImGroupMessageDO msg, List<ImGroupMemberDO> allMembers) { | ||
| 320 | + // 定向接收集合 | ||
| 321 | + Set<String> receiverSet = null; | ||
| 322 | + if (StrUtil.isNotBlank(msg.getReceiverUserIds())) { | ||
| 323 | + receiverSet = new HashSet<>(Arrays.asList(msg.getReceiverUserIds().split(","))); | ||
| 324 | + } | ||
| 325 | + | ||
| 326 | + Set<Long> visible = new HashSet<>(); | ||
| 327 | + for (ImGroupMemberDO m : allMembers) { | ||
| 328 | + // 入群时间在消息发送之后 → 不可见 | ||
| 329 | + if (m.getJoinTime() != null && msg.getSendTime().isBefore(m.getJoinTime())) { | ||
| 330 | + continue; | ||
| 331 | + } | ||
| 332 | + // 已退群且退群时间在消息发送之前 → 不可见 | ||
| 333 | + if (ImGroupMemberStatusEnum.QUIT.getStatus().equals(m.getStatus()) | ||
| 334 | + && m.getQuitTime() != null && msg.getSendTime().isAfter(m.getQuitTime())) { | ||
| 335 | + continue; | ||
| 336 | + } | ||
| 337 | + // 定向接收过滤 | ||
| 338 | + if (receiverSet != null && !receiverSet.contains(m.getUserId().toString()) | ||
| 339 | + && !m.getUserId().equals(msg.getSenderId())) { | ||
| 340 | + continue; | ||
| 341 | + } | ||
| 342 | + visible.add(m.getUserId()); | ||
| 343 | + } | ||
| 344 | + return visible; | ||
| 345 | + } | ||
| 346 | + | ||
| 347 | + /** | ||
| 348 | + * 发送群聊消息 WebSocket 事件 | ||
| 349 | + */ | ||
| 350 | + private void sendGroupMessageEvent(ImGroupMessageDO message, Long groupId) { | ||
| 351 | + List<ImGroupMemberDO> members = imGroupMemberService.selectByGroupId(groupId); | ||
| 352 | + Map<String, Object> event = new HashMap<>(); | ||
| 353 | + event.put("id", message.getId().toString()); | ||
| 354 | + event.put("clientMessageId", message.getClientMessageId()); | ||
| 355 | + event.put("senderId", message.getSenderId().toString()); | ||
| 356 | + event.put("groupId", message.getGroupId().toString()); | ||
| 357 | + event.put("type", message.getType()); | ||
| 358 | + event.put("content", message.getContent()); | ||
| 359 | + event.put("status", message.getStatus()); | ||
| 360 | + event.put("sendTime", message.getSendTime().toString()); | ||
| 361 | + event.put("messageScene", "group"); | ||
| 362 | + if (StrUtil.isNotBlank(message.getAtUserIds())) { | ||
| 363 | + event.put("atUserIds", message.getAtUserIds()); | ||
| 364 | + } | ||
| 365 | + if (StrUtil.isNotBlank(message.getReceiverUserIds())) { | ||
| 366 | + event.put("receiverUserIds", message.getReceiverUserIds()); | ||
| 367 | + } | ||
| 368 | + event.put("receiptStatus", message.getReceiptStatus()); | ||
| 369 | + | ||
| 370 | + // 确定推送目标 | ||
| 371 | + Set<String> receiverSet = null; | ||
| 372 | + if (StrUtil.isNotBlank(message.getReceiverUserIds())) { | ||
| 373 | + receiverSet = new HashSet<>(Arrays.asList(message.getReceiverUserIds().split(","))); | ||
| 374 | + } | ||
| 375 | + | ||
| 376 | + for (ImGroupMemberDO member : members) { | ||
| 377 | + if (ImGroupMemberStatusEnum.QUIT.getStatus().equals(member.getStatus())) { | ||
| 378 | + continue; | ||
| 379 | + } | ||
| 380 | + // 定向接收过滤 | ||
| 381 | + if (receiverSet != null && !receiverSet.contains(member.getUserId().toString()) | ||
| 382 | + && !member.getUserId().equals(message.getSenderId())) { | ||
| 383 | + continue; | ||
| 384 | + } | ||
| 385 | + webSocketSenderApi.sendObject(UserTypeEnum.ADMIN.getValue(), member.getUserId(), | ||
| 386 | + ImWebSocketTypeConstants.GROUP_MESSAGE, event); | ||
| 387 | + } | ||
| 388 | + } | ||
| 389 | + | ||
| 390 | +} |
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageService.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.service.message; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageSendReqVO; | ||
| 4 | +import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO; | ||
| 5 | + | ||
| 6 | +import java.util.List; | ||
| 7 | + | ||
| 8 | +/** | ||
| 9 | + * IM 私聊消息 Service 接口 | ||
| 10 | + * | ||
| 11 | + * @author 芋道源码 | ||
| 12 | + */ | ||
| 13 | +public interface ImPrivateMessageService { | ||
| 14 | + | ||
| 15 | + /** | ||
| 16 | + * 发送私聊消息 | ||
| 17 | + * | ||
| 18 | + * @param senderId 发送人编号 | ||
| 19 | + * @param reqVO 发送请求 | ||
| 20 | + * @return 消息 | ||
| 21 | + */ | ||
| 22 | + ImPrivateMessageDO sendMessage(Long senderId, ImPrivateMessageSendReqVO reqVO); | ||
| 23 | + | ||
| 24 | + /** | ||
| 25 | + * 拉取私聊消息(增量) | ||
| 26 | + * | ||
| 27 | + * @param userId 当前用户编号 | ||
| 28 | + * @param minId 最小消息 id(不含) | ||
| 29 | + * @param size 拉取数量 | ||
| 30 | + * @return 消息列表 | ||
| 31 | + */ | ||
| 32 | + List<ImPrivateMessageDO> pullMessages(Long userId, Long minId, Integer size); | ||
| 33 | + | ||
| 34 | + /** | ||
| 35 | + * 标记私聊消息已读 | ||
| 36 | + * | ||
| 37 | + * @param userId 当前用户编号 | ||
| 38 | + * @param friendId 好友用户编号 | ||
| 39 | + */ | ||
| 40 | + void readMessages(Long userId, Long friendId); | ||
| 41 | + | ||
| 42 | + /** | ||
| 43 | + * 撤回私聊消息 | ||
| 44 | + * | ||
| 45 | + * @param userId 当前用户编号 | ||
| 46 | + * @param messageId 消息编号 | ||
| 47 | + */ | ||
| 48 | + void recallMessage(Long userId, Long messageId); | ||
| 49 | + | ||
| 50 | +} |
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageServiceImpl.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.service.message; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; | ||
| 4 | +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.mysql.message.ImPrivateMessageMapper; | ||
| 7 | +import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum; | ||
| 8 | +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.sensitiveword.ImSensitiveWordService; | ||
| 12 | +import cn.iocoder.yudao.module.infra.api.websocket.WebSocketSenderApi; | ||
| 13 | +import jakarta.annotation.Resource; | ||
| 14 | +import lombok.extern.slf4j.Slf4j; | ||
| 15 | +import org.springframework.stereotype.Service; | ||
| 16 | +import org.springframework.validation.annotation.Validated; | ||
| 17 | + | ||
| 18 | +import java.time.LocalDateTime; | ||
| 19 | +import java.util.HashMap; | ||
| 20 | +import java.util.List; | ||
| 21 | +import java.util.Map; | ||
| 22 | + | ||
| 23 | +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; | ||
| 24 | +import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*; | ||
| 25 | + | ||
| 26 | +/** | ||
| 27 | + * IM 私聊消息 Service 实现类 | ||
| 28 | + * | ||
| 29 | + * @author 芋道源码 | ||
| 30 | + */ | ||
| 31 | +@Service | ||
| 32 | +@Validated | ||
| 33 | +@Slf4j | ||
| 34 | +public class ImPrivateMessageServiceImpl implements ImPrivateMessageService { | ||
| 35 | + | ||
| 36 | + /** | ||
| 37 | + * pull 最大拉取数量 | ||
| 38 | + */ | ||
| 39 | + private static final int MAX_PULL_SIZE = 1000; | ||
| 40 | + | ||
| 41 | + @Resource | ||
| 42 | + private ImPrivateMessageMapper imPrivateMessageMapper; | ||
| 43 | + | ||
| 44 | + @Resource | ||
| 45 | + private ImFriendService imFriendService; | ||
| 46 | + @Resource | ||
| 47 | + private ImSensitiveWordService imSensitiveWordService; | ||
| 48 | + | ||
| 49 | + // TODO @AI:这个类貌似路径不对?! | ||
| 50 | + @Resource | ||
| 51 | + private WebSocketSenderApi webSocketSenderApi; | ||
| 52 | + | ||
| 53 | + @Override | ||
| 54 | + public ImPrivateMessageDO sendMessage(Long senderId, ImPrivateMessageSendReqVO reqVO) { | ||
| 55 | + // 1.1 幂等校验:根据 senderId + clientMessageId 查重 | ||
| 56 | + ImPrivateMessageDO existing = imPrivateMessageMapper.selectBySenderIdAndClientMessageId( | ||
| 57 | + senderId, reqVO.getClientMessageId()); | ||
| 58 | + if (existing != null) { | ||
| 59 | + return existing; | ||
| 60 | + } | ||
| 61 | + // 1.2 好友校验 | ||
| 62 | + imFriendService.validateIsFriend(senderId, reqVO.getReceiverId()); | ||
| 63 | + // 1.3 文本消息敏感词过滤 | ||
| 64 | + if (ImMessageTypeEnum.TEXT.getType().equals(reqVO.getType())) { | ||
| 65 | + imSensitiveWordService.validateText(reqVO.getContent()); | ||
| 66 | + } | ||
| 67 | + | ||
| 68 | + // 2. 构建并保存消息 | ||
| 69 | + // TODO @AI:相同类型的字段:放在一行,避免过长; | ||
| 70 | + ImPrivateMessageDO message = ImPrivateMessageDO.builder() | ||
| 71 | + .clientMessageId(reqVO.getClientMessageId()) | ||
| 72 | + .senderId(senderId) | ||
| 73 | + .receiverId(reqVO.getReceiverId()) | ||
| 74 | + .type(reqVO.getType()) | ||
| 75 | + .content(reqVO.getContent()) | ||
| 76 | + .status(ImMessageStatusEnum.UNREAD.getStatus()) | ||
| 77 | + .sendTime(LocalDateTime.now()) | ||
| 78 | + .build(); | ||
| 79 | + imPrivateMessageMapper.insert(message); | ||
| 80 | + | ||
| 81 | + // 3. WebSocket 推送给接收方 | ||
| 82 | + sendPrivateMessageEvent(message); | ||
| 83 | + return message; | ||
| 84 | + } | ||
| 85 | + | ||
| 86 | + @Override | ||
| 87 | + public List<ImPrivateMessageDO> pullMessages(Long userId, Long minId, Integer size) { | ||
| 88 | + // pull 分页保护 | ||
| 89 | + // TODO @AI:limit 这个参考校验,放在 controller 里做; | ||
| 90 | + if (size > MAX_PULL_SIZE) { | ||
| 91 | + throw exception(MESSAGE_PULL_SIZE_EXCEEDED); | ||
| 92 | + } | ||
| 93 | + // TODO @AI:可能要加一个,最大返回几个月的?! | ||
| 94 | + return imPrivateMessageMapper.selectListByMinId(userId, minId, size); | ||
| 95 | + } | ||
| 96 | + | ||
| 97 | + // TODO @芋艿:这里需要多 review 下; | ||
| 98 | + @Override | ||
| 99 | + public void readMessages(Long userId, Long friendId) { | ||
| 100 | + // 1. 批量更新消息状态为已读 | ||
| 101 | + int updated = imPrivateMessageMapper.updateStatusToRead(userId, friendId); | ||
| 102 | + if (updated == 0) { | ||
| 103 | + return; // 没有需要更新的消息 | ||
| 104 | + } | ||
| 105 | + // 2. 发送 READ 事件给自己的其他终端(多端同步) | ||
| 106 | + Map<String, Object> readEvent = new HashMap<>(); | ||
| 107 | + // TODO @AI:需要整理下,相关的 websocket DTO;可维护性变强! | ||
| 108 | + readEvent.put("friendId", friendId.toString()); | ||
| 109 | + readEvent.put("messageScene", "private"); | ||
| 110 | + webSocketSenderApi.sendObject(UserTypeEnum.ADMIN.getValue(), userId, | ||
| 111 | + ImWebSocketTypeConstants.READ, readEvent); | ||
| 112 | + | ||
| 113 | + // 3. 发送 RECEIPT 事件给对方(已读回执) | ||
| 114 | + Map<String, Object> receiptEvent = new HashMap<>(); | ||
| 115 | + // TODO @AI:需要整理下,相关的 websocket DTO;可维护性变强! | ||
| 116 | + receiptEvent.put("userId", userId.toString()); | ||
| 117 | + receiptEvent.put("messageScene", "private"); | ||
| 118 | + webSocketSenderApi.sendObject(UserTypeEnum.ADMIN.getValue(), friendId, | ||
| 119 | + ImWebSocketTypeConstants.RECEIPT, receiptEvent); | ||
| 120 | + } | ||
| 121 | + | ||
| 122 | + // TODO @芋艿:这里需要多 review 下; | ||
| 123 | + @Override | ||
| 124 | + public void recallMessage(Long userId, Long messageId) { | ||
| 125 | + // 1.1 校验消息存在 | ||
| 126 | + ImPrivateMessageDO message = imPrivateMessageMapper.selectById(messageId); | ||
| 127 | + if (message == null) { | ||
| 128 | + throw exception(MESSAGE_NOT_EXISTS); | ||
| 129 | + } | ||
| 130 | + // 1.2 只能撤回自己发送的消息 | ||
| 131 | + // TODO @AI:Notequals; | ||
| 132 | + if (!message.getSenderId().equals(userId)) { | ||
| 133 | + throw exception(MESSAGE_RECALL_DENIED); | ||
| 134 | + } | ||
| 135 | + // 1.3 不能重复撤回 | ||
| 136 | + if (ImMessageStatusEnum.RECALL.getStatus().equals(message.getStatus())) { | ||
| 137 | + throw exception(MESSAGE_ALREADY_RECALLED); | ||
| 138 | + } | ||
| 139 | + | ||
| 140 | + // 2. 更新消息状态为撤回 | ||
| 141 | + imPrivateMessageMapper.updateById(new ImPrivateMessageDO().setId(messageId) | ||
| 142 | + .setStatus(ImMessageStatusEnum.RECALL.getStatus())); | ||
| 143 | + | ||
| 144 | + // 3.1 发送 RECALL 事件给接收方 | ||
| 145 | + // TODO @AI:需要整理下,相关的 websocket DTO;可维护性变强! | ||
| 146 | + Map<String, Object> recallEvent = new HashMap<>(); | ||
| 147 | + recallEvent.put("messageId", messageId.toString()); | ||
| 148 | + recallEvent.put("messageScene", "private"); | ||
| 149 | + recallEvent.put("senderId", userId.toString()); | ||
| 150 | + webSocketSenderApi.sendObject(UserTypeEnum.ADMIN.getValue(), message.getReceiverId(), | ||
| 151 | + ImWebSocketTypeConstants.RECALL, recallEvent); | ||
| 152 | + // 3.2 也通知发送方自己的其他终端 | ||
| 153 | + // TODO @AI:需要整理下,相关的 websocket DTO;可维护性变强! | ||
| 154 | + webSocketSenderApi.sendObject(UserTypeEnum.ADMIN.getValue(), userId, | ||
| 155 | + ImWebSocketTypeConstants.RECALL, recallEvent); | ||
| 156 | + } | ||
| 157 | + | ||
| 158 | + /** | ||
| 159 | + * 发送私聊消息 WebSocket 事件 | ||
| 160 | + */ | ||
| 161 | + private void sendPrivateMessageEvent(ImPrivateMessageDO message) { | ||
| 162 | + Map<String, Object> event = new HashMap<>(); | ||
| 163 | + // TODO @AI:需要整理下,相关的 websocket DTO;可维护性变强! | ||
| 164 | + event.put("id", message.getId().toString()); | ||
| 165 | + event.put("clientMessageId", message.getClientMessageId()); | ||
| 166 | + event.put("senderId", message.getSenderId().toString()); | ||
| 167 | + event.put("receiverId", message.getReceiverId().toString()); | ||
| 168 | + event.put("type", message.getType()); | ||
| 169 | + event.put("content", message.getContent()); | ||
| 170 | + event.put("status", message.getStatus()); | ||
| 171 | + event.put("sendTime", message.getSendTime().toString()); | ||
| 172 | + event.put("messageScene", "private"); | ||
| 173 | + // 推送给接收方 | ||
| 174 | + webSocketSenderApi.sendObject(UserTypeEnum.ADMIN.getValue(), message.getReceiverId(), | ||
| 175 | + ImWebSocketTypeConstants.PRIVATE_MESSAGE, event); | ||
| 176 | + // 推送给发送方自己的其他终端(多端同步) | ||
| 177 | + webSocketSenderApi.sendObject(UserTypeEnum.ADMIN.getValue(), message.getSenderId(), | ||
| 178 | + ImWebSocketTypeConstants.PRIVATE_MESSAGE, event); | ||
| 179 | + } | ||
| 180 | + | ||
| 181 | +} |
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/sensitiveword/ImSensitiveWordService.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.service.sensitiveword; | ||
| 2 | + | ||
| 3 | +/** | ||
| 4 | + * IM 敏感词 Service 接口 | ||
| 5 | + * | ||
| 6 | + * @author 芋道源码 | ||
| 7 | + */ | ||
| 8 | +public interface ImSensitiveWordService { | ||
| 9 | + | ||
| 10 | + /** | ||
| 11 | + * 校验文本是否包含敏感词 | ||
| 12 | + * <p> | ||
| 13 | + * 如果包含,直接抛出业务异常 | ||
| 14 | + * | ||
| 15 | + * @param text 待校验文本 | ||
| 16 | + */ | ||
| 17 | + void validateText(String text); | ||
| 18 | + | ||
| 19 | +} |
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/sensitiveword/ImSensitiveWordServiceImpl.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.service.sensitiveword; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.module.im.dal.dataobject.sensitiveword.ImSensitiveWordDO; | ||
| 4 | +import cn.iocoder.yudao.module.im.dal.mysql.sensitiveword.ImSensitiveWordMapper; | ||
| 5 | +import jakarta.annotation.Resource; | ||
| 6 | +import org.springframework.stereotype.Service; | ||
| 7 | +import org.springframework.validation.annotation.Validated; | ||
| 8 | + | ||
| 9 | +import java.util.List; | ||
| 10 | + | ||
| 11 | +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; | ||
| 12 | +import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.MESSAGE_SENSITIVE_WORD_BLOCKED; | ||
| 13 | + | ||
| 14 | +// TODO @AI:需要内存化;晚点处理; | ||
| 15 | +/** | ||
| 16 | + * IM 敏感词 Service 实现类 | ||
| 17 | + * | ||
| 18 | + * @author 芋道源码 | ||
| 19 | + */ | ||
| 20 | +@Service | ||
| 21 | +@Validated | ||
| 22 | +public class ImSensitiveWordServiceImpl implements ImSensitiveWordService { | ||
| 23 | + | ||
| 24 | + @Resource | ||
| 25 | + private ImSensitiveWordMapper imSensitiveWordMapper; | ||
| 26 | + | ||
| 27 | + @Override | ||
| 28 | + public void validateText(String text) { | ||
| 29 | + if (text == null || text.isEmpty()) { | ||
| 30 | + return; | ||
| 31 | + } | ||
| 32 | + // 查询所有启用状态的敏感词 | ||
| 33 | + List<ImSensitiveWordDO> enabledWords = imSensitiveWordMapper.selectListByEnabled(); | ||
| 34 | + // 逐词匹配 | ||
| 35 | + for (ImSensitiveWordDO wordDO : enabledWords) { | ||
| 36 | + if (text.contains(wordDO.getWord())) { | ||
| 37 | + throw exception(MESSAGE_SENSITIVE_WORD_BLOCKED); | ||
| 38 | + } | ||
| 39 | + } | ||
| 40 | + } | ||
| 41 | + | ||
| 42 | +} |
yudao-module-im/yudao-module-im-biz/src/test/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImplTest.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.service.message; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.common.exception.ServiceException; | ||
| 4 | +import cn.iocoder.yudao.module.im.controller.admin.message.vo.ImGroupMessageSendReqVO; | ||
| 5 | +import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO; | ||
| 6 | +import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; | ||
| 7 | +import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO; | ||
| 8 | +import cn.iocoder.yudao.module.im.dal.mysql.message.ImGroupMessageMapper; | ||
| 9 | +import cn.iocoder.yudao.module.im.dal.redis.group.GroupReadPositionRedisDAO; | ||
| 10 | +import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberStatusEnum; | ||
| 11 | +import cn.iocoder.yudao.module.im.enums.message.ImGroupMessageReceiptStatusEnum; | ||
| 12 | +import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum; | ||
| 13 | +import cn.iocoder.yudao.module.im.service.group.ImGroupService; | ||
| 14 | +import cn.iocoder.yudao.module.im.service.groupmember.ImGroupMemberService; | ||
| 15 | +import cn.iocoder.yudao.module.im.service.sensitiveword.ImSensitiveWordService; | ||
| 16 | +import cn.iocoder.yudao.module.infra.api.websocket.WebSocketSenderApi; | ||
| 17 | +import org.junit.jupiter.api.Test; | ||
| 18 | +import org.junit.jupiter.api.extension.ExtendWith; | ||
| 19 | +import org.mockito.InjectMocks; | ||
| 20 | +import org.mockito.Mock; | ||
| 21 | +import org.mockito.junit.jupiter.MockitoExtension; | ||
| 22 | + | ||
| 23 | +import java.time.LocalDateTime; | ||
| 24 | +import java.util.*; | ||
| 25 | + | ||
| 26 | +import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*; | ||
| 27 | +import static org.junit.jupiter.api.Assertions.*; | ||
| 28 | +import static org.mockito.ArgumentMatchers.*; | ||
| 29 | +import static org.mockito.Mockito.*; | ||
| 30 | + | ||
| 31 | +// TODO @芋艿:这个需要在评估下; | ||
| 32 | +/** | ||
| 33 | + * IM 群聊消息 Service 单元测试 | ||
| 34 | + * | ||
| 35 | + * @author 芋道源码 | ||
| 36 | + */ | ||
| 37 | +@ExtendWith(MockitoExtension.class) | ||
| 38 | +public class ImGroupMessageServiceImplTest { | ||
| 39 | + | ||
| 40 | + @InjectMocks | ||
| 41 | + private ImGroupMessageServiceImpl groupMessageService; | ||
| 42 | + | ||
| 43 | + @Mock | ||
| 44 | + private ImGroupMessageMapper imGroupMessageMapper; | ||
| 45 | + @Mock | ||
| 46 | + private ImGroupService imGroupService; | ||
| 47 | + @Mock | ||
| 48 | + private ImGroupMemberService imGroupMemberService; | ||
| 49 | + @Mock | ||
| 50 | + private ImSensitiveWordService imSensitiveWordService; | ||
| 51 | + @Mock | ||
| 52 | + private GroupReadPositionRedisDAO groupReadPositionRedisDAO; | ||
| 53 | + @Mock | ||
| 54 | + private WebSocketSenderApi webSocketSenderApi; | ||
| 55 | + | ||
| 56 | + private ImGroupMessageSendReqVO buildSendReqVO() { | ||
| 57 | + ImGroupMessageSendReqVO reqVO = new ImGroupMessageSendReqVO(); | ||
| 58 | + reqVO.setClientMessageId("test-uuid-group-001"); | ||
| 59 | + reqVO.setGroupId(10L); | ||
| 60 | + reqVO.setType(0); // TEXT | ||
| 61 | + reqVO.setContent("{\"content\":\"群聊你好\"}"); | ||
| 62 | + return reqVO; | ||
| 63 | + } | ||
| 64 | + | ||
| 65 | + // ========== 发送测试 ========== | ||
| 66 | + | ||
| 67 | + @Test | ||
| 68 | + public void testSendMessage_success() { | ||
| 69 | + // 准备 | ||
| 70 | + ImGroupMessageSendReqVO reqVO = buildSendReqVO(); | ||
| 71 | + when(imGroupMessageMapper.selectBySenderIdAndClientMessageId(1L, "test-uuid-group-001")) | ||
| 72 | + .thenReturn(null); | ||
| 73 | + ImGroupDO group = new ImGroupDO(); | ||
| 74 | + group.setId(10L); | ||
| 75 | + group.setName("测试群"); | ||
| 76 | + when(imGroupService.validateGroupExists(10L)).thenReturn(group); | ||
| 77 | + | ||
| 78 | + ImGroupMemberDO member = new ImGroupMemberDO(); | ||
| 79 | + member.setGroupId(10L); | ||
| 80 | + member.setUserId(1L); | ||
| 81 | + member.setStatus(ImGroupMemberStatusEnum.NORMAL.getStatus()); | ||
| 82 | + when(imGroupMemberService.getGroupMember(10L, 1L)).thenReturn(member); | ||
| 83 | + | ||
| 84 | + List<ImGroupMemberDO> allMembers = List.of( | ||
| 85 | + member, | ||
| 86 | + ImGroupMemberDO.builder().groupId(10L).userId(2L) | ||
| 87 | + .status(ImGroupMemberStatusEnum.NORMAL.getStatus()).build(), | ||
| 88 | + ImGroupMemberDO.builder().groupId(10L).userId(3L) | ||
| 89 | + .status(ImGroupMemberStatusEnum.NORMAL.getStatus()).build() | ||
| 90 | + ); | ||
| 91 | + when(imGroupMemberService.selectByGroupId(10L)).thenReturn(allMembers); | ||
| 92 | + when(imGroupMessageMapper.insert(any())).thenReturn(1); | ||
| 93 | + | ||
| 94 | + // 调用 | ||
| 95 | + ImGroupMessageDO result = groupMessageService.sendMessage(1L, reqVO); | ||
| 96 | + | ||
| 97 | + // 断言 | ||
| 98 | + assertNotNull(result); | ||
| 99 | + assertEquals(1L, result.getSenderId()); | ||
| 100 | + assertEquals(10L, result.getGroupId()); | ||
| 101 | + assertEquals(ImMessageStatusEnum.NORMAL, result.getStatus()); | ||
| 102 | + assertEquals(ImGroupMessageReceiptStatusEnum.NO_RECEIPT.getStatus(), result.getReceiptStatus()); | ||
| 103 | + | ||
| 104 | + // 验证推送给 3 个群成员 | ||
| 105 | + verify(webSocketSenderApi, times(3)).sendObject(anyInt(), anyLong(), anyString(), any()); | ||
| 106 | + } | ||
| 107 | + | ||
| 108 | + @Test | ||
| 109 | + public void testSendMessage_clientMessageIdIdempotent() { | ||
| 110 | + // 准备 | ||
| 111 | + ImGroupMessageSendReqVO reqVO = buildSendReqVO(); | ||
| 112 | + ImGroupMessageDO existing = ImGroupMessageDO.builder() | ||
| 113 | + .id(100L).clientMessageId("test-uuid-group-001").senderId(1L).groupId(10L) | ||
| 114 | + .type(0).content("{\"content\":\"群聊你好\"}").status(0) | ||
| 115 | + .sendTime(LocalDateTime.now()).build(); | ||
| 116 | + when(imGroupMessageMapper.selectBySenderIdAndClientMessageId(1L, "test-uuid-group-001")) | ||
| 117 | + .thenReturn(existing); | ||
| 118 | + | ||
| 119 | + // 调用 | ||
| 120 | + ImGroupMessageDO result = groupMessageService.sendMessage(1L, reqVO); | ||
| 121 | + | ||
| 122 | + // 断言 | ||
| 123 | + assertEquals(100L, result.getId()); | ||
| 124 | + verify(imGroupMessageMapper, never()).insert(any()); | ||
| 125 | + } | ||
| 126 | + | ||
| 127 | + @Test | ||
| 128 | + public void testSendMessage_notInGroup() { | ||
| 129 | + // 准备 | ||
| 130 | + ImGroupMessageSendReqVO reqVO = buildSendReqVO(); | ||
| 131 | + when(imGroupMessageMapper.selectBySenderIdAndClientMessageId(1L, "test-uuid-group-001")) | ||
| 132 | + .thenReturn(null); | ||
| 133 | + ImGroupDO group = new ImGroupDO(); | ||
| 134 | + group.setId(10L); | ||
| 135 | + when(imGroupService.validateGroupExists(10L)).thenReturn(group); | ||
| 136 | + when(imGroupMemberService.getGroupMember(10L, 1L)).thenReturn(null); | ||
| 137 | + | ||
| 138 | + // 调用并断言 | ||
| 139 | + ServiceException exception = assertThrows(ServiceException.class, | ||
| 140 | + () -> groupMessageService.sendMessage(1L, reqVO)); | ||
| 141 | + assertEquals(GROUP_MEMBER_NOT_IN_GROUP.getCode(), exception.getCode()); | ||
| 142 | + } | ||
| 143 | + | ||
| 144 | + // ========== pull 测试 ========== | ||
| 145 | + | ||
| 146 | + @Test | ||
| 147 | + public void testPullMessages_joinTimeFilter() { | ||
| 148 | + // 准备 | ||
| 149 | + LocalDateTime joinTime = LocalDateTime.of(2026, 4, 12, 10, 0, 0); | ||
| 150 | + ImGroupMemberDO member = ImGroupMemberDO.builder() | ||
| 151 | + .groupId(10L).userId(1L) | ||
| 152 | + .status(ImGroupMemberStatusEnum.NORMAL.getStatus()) | ||
| 153 | + .joinTime(joinTime).build(); | ||
| 154 | + when(imGroupMemberService.getGroupMembersByUserId(1L)).thenReturn(List.of(member)); | ||
| 155 | + | ||
| 156 | + // 模拟一条入群前的消息和一条入群后的消息 | ||
| 157 | + ImGroupMessageDO beforeJoin = ImGroupMessageDO.builder() | ||
| 158 | + .id(1L).groupId(10L).senderId(2L) | ||
| 159 | + .sendTime(LocalDateTime.of(2026, 4, 12, 9, 0, 0)).build(); | ||
| 160 | + ImGroupMessageDO afterJoin = ImGroupMessageDO.builder() | ||
| 161 | + .id(2L).groupId(10L).senderId(2L) | ||
| 162 | + .sendTime(LocalDateTime.of(2026, 4, 12, 11, 0, 0)).build(); | ||
| 163 | + when(imGroupMessageMapper.selectListByMinId(List.of(10L), 0L, 100)) | ||
| 164 | + .thenReturn(List.of(beforeJoin, afterJoin)); | ||
| 165 | + | ||
| 166 | + // 调用 | ||
| 167 | + List<ImGroupMessageDO> result = groupMessageService.pullMessages(1L, 0L, 100); | ||
| 168 | + | ||
| 169 | + // 断言:入群前消息不可见 | ||
| 170 | + assertEquals(1, result.size()); | ||
| 171 | + assertEquals(2L, result.get(0).getId()); | ||
| 172 | + } | ||
| 173 | + | ||
| 174 | + @Test | ||
| 175 | + public void testPullMessages_quitTimeFilter() { | ||
| 176 | + // 准备 | ||
| 177 | + LocalDateTime joinTime = LocalDateTime.of(2026, 4, 10, 10, 0, 0); | ||
| 178 | + LocalDateTime quitTime = LocalDateTime.of(2026, 4, 12, 10, 0, 0); | ||
| 179 | + ImGroupMemberDO member = ImGroupMemberDO.builder() | ||
| 180 | + .groupId(10L).userId(1L) | ||
| 181 | + .status(ImGroupMemberStatusEnum.QUIT.getStatus()) | ||
| 182 | + .joinTime(joinTime).quitTime(quitTime).build(); | ||
| 183 | + when(imGroupMemberService.getGroupMembersByUserId(1L)).thenReturn(List.of(member)); | ||
| 184 | + | ||
| 185 | + // 退群前后各一条消息 | ||
| 186 | + ImGroupMessageDO beforeQuit = ImGroupMessageDO.builder() | ||
| 187 | + .id(1L).groupId(10L).senderId(2L) | ||
| 188 | + .sendTime(LocalDateTime.of(2026, 4, 11, 10, 0, 0)).build(); | ||
| 189 | + ImGroupMessageDO afterQuit = ImGroupMessageDO.builder() | ||
| 190 | + .id(2L).groupId(10L).senderId(2L) | ||
| 191 | + .sendTime(LocalDateTime.of(2026, 4, 13, 10, 0, 0)).build(); | ||
| 192 | + when(imGroupMessageMapper.selectListByMinId(List.of(10L), 0L, 100)) | ||
| 193 | + .thenReturn(List.of(beforeQuit, afterQuit)); | ||
| 194 | + | ||
| 195 | + // 调用 | ||
| 196 | + List<ImGroupMessageDO> result = groupMessageService.pullMessages(1L, 0L, 100); | ||
| 197 | + | ||
| 198 | + // 断言:退群后消息不可见 | ||
| 199 | + assertEquals(1, result.size()); | ||
| 200 | + assertEquals(1L, result.get(0).getId()); | ||
| 201 | + } | ||
| 202 | + | ||
| 203 | + @Test | ||
| 204 | + public void testPullMessages_receiverUserIdsFilter() { | ||
| 205 | + // 准备 | ||
| 206 | + ImGroupMemberDO member = ImGroupMemberDO.builder() | ||
| 207 | + .groupId(10L).userId(1L) | ||
| 208 | + .status(ImGroupMemberStatusEnum.NORMAL.getStatus()) | ||
| 209 | + .joinTime(LocalDateTime.of(2026, 1, 1, 0, 0, 0)).build(); | ||
| 210 | + when(imGroupMemberService.getGroupMembersByUserId(1L)).thenReturn(List.of(member)); | ||
| 211 | + | ||
| 212 | + // 定向接收消息:只给用户 2 和 3 | ||
| 213 | + ImGroupMessageDO directedMsg = ImGroupMessageDO.builder() | ||
| 214 | + .id(1L).groupId(10L).senderId(5L) | ||
| 215 | + .receiverUserIds("2,3") | ||
| 216 | + .sendTime(LocalDateTime.of(2026, 4, 12, 10, 0, 0)).build(); | ||
| 217 | + // 全员消息 | ||
| 218 | + ImGroupMessageDO allMsg = ImGroupMessageDO.builder() | ||
| 219 | + .id(2L).groupId(10L).senderId(5L) | ||
| 220 | + .sendTime(LocalDateTime.of(2026, 4, 12, 10, 0, 0)).build(); | ||
| 221 | + when(imGroupMessageMapper.selectListByMinId(List.of(10L), 0L, 100)) | ||
| 222 | + .thenReturn(List.of(directedMsg, allMsg)); | ||
| 223 | + | ||
| 224 | + // 调用 | ||
| 225 | + List<ImGroupMessageDO> result = groupMessageService.pullMessages(1L, 0L, 100); | ||
| 226 | + | ||
| 227 | + // 断言:定向接收的消息用户 1 看不到,只能看到全员消息 | ||
| 228 | + assertEquals(1, result.size()); | ||
| 229 | + assertEquals(2L, result.get(0).getId()); | ||
| 230 | + } | ||
| 231 | + | ||
| 232 | + @Test | ||
| 233 | + public void testPullMessages_atFieldCorrectReturn() { | ||
| 234 | + // 准备 | ||
| 235 | + ImGroupMemberDO member = ImGroupMemberDO.builder() | ||
| 236 | + .groupId(10L).userId(1L) | ||
| 237 | + .status(ImGroupMemberStatusEnum.NORMAL.getStatus()) | ||
| 238 | + .joinTime(LocalDateTime.of(2026, 1, 1, 0, 0, 0)).build(); | ||
| 239 | + when(imGroupMemberService.getGroupMembersByUserId(1L)).thenReturn(List.of(member)); | ||
| 240 | + | ||
| 241 | + ImGroupMessageDO atMsg = ImGroupMessageDO.builder() | ||
| 242 | + .id(1L).groupId(10L).senderId(2L) | ||
| 243 | + .atUserIds("1,3") | ||
| 244 | + .sendTime(LocalDateTime.of(2026, 4, 12, 10, 0, 0)).build(); | ||
| 245 | + when(imGroupMessageMapper.selectListByMinId(List.of(10L), 0L, 100)) | ||
| 246 | + .thenReturn(List.of(atMsg)); | ||
| 247 | + | ||
| 248 | + // 调用 | ||
| 249 | + List<ImGroupMessageDO> result = groupMessageService.pullMessages(1L, 0L, 100); | ||
| 250 | + | ||
| 251 | + // 断言:@ 字段正确返回 | ||
| 252 | + assertEquals(1, result.size()); | ||
| 253 | + assertEquals("1,3", result.get(0).getAtUserIds()); | ||
| 254 | + } | ||
| 255 | + | ||
| 256 | + // ========== 撤回测试 ========== | ||
| 257 | + | ||
| 258 | + @Test | ||
| 259 | + public void testRecallMessage_success() { | ||
| 260 | + // 准备 | ||
| 261 | + ImGroupMessageDO message = ImGroupMessageDO.builder() | ||
| 262 | + .id(50L).senderId(1L).groupId(10L) | ||
| 263 | + .status(ImMessageStatusEnum.NORMAL).build(); | ||
| 264 | + when(imGroupMessageMapper.selectById(50L)).thenReturn(message); | ||
| 265 | + when(imGroupMessageMapper.updateById(any())).thenReturn(1); | ||
| 266 | + | ||
| 267 | + List<ImGroupMemberDO> members = List.of( | ||
| 268 | + ImGroupMemberDO.builder().groupId(10L).userId(1L) | ||
| 269 | + .status(ImGroupMemberStatusEnum.NORMAL.getStatus()).build(), | ||
| 270 | + ImGroupMemberDO.builder().groupId(10L).userId(2L) | ||
| 271 | + .status(ImGroupMemberStatusEnum.NORMAL.getStatus()).build(), | ||
| 272 | + ImGroupMemberDO.builder().groupId(10L).userId(3L) | ||
| 273 | + .status(ImGroupMemberStatusEnum.QUIT.getStatus()).build() | ||
| 274 | + ); | ||
| 275 | + when(imGroupMemberService.selectByGroupId(10L)).thenReturn(members); | ||
| 276 | + | ||
| 277 | + // 调用 | ||
| 278 | + groupMessageService.recallMessage(1L, 50L); | ||
| 279 | + | ||
| 280 | + // 断言:只给 2 个活跃成员发送 RECALL 事件(userId=3 已退群) | ||
| 281 | + verify(webSocketSenderApi, times(2)).sendObject(anyInt(), anyLong(), anyString(), any()); | ||
| 282 | + } | ||
| 283 | + | ||
| 284 | + // ========== 群已读测试 ========== | ||
| 285 | + | ||
| 286 | + @Test | ||
| 287 | + public void testReadMessages_notInGroup() { | ||
| 288 | + // 准备:当前用户不在群中 | ||
| 289 | + when(imGroupMemberService.getGroupMember(10L, 1L)).thenReturn(null); | ||
| 290 | + | ||
| 291 | + // 调用并断言:越权校验 | ||
| 292 | + ServiceException exception = assertThrows(ServiceException.class, | ||
| 293 | + () -> groupMessageService.readMessages(1L, 10L)); | ||
| 294 | + assertEquals(GROUP_MEMBER_NOT_IN_GROUP.getCode(), exception.getCode()); | ||
| 295 | + } | ||
| 296 | + | ||
| 297 | + @Test | ||
| 298 | + public void testGetReadUsers_withVisibleScope() { | ||
| 299 | + // 准备:用户 1 是群成员 | ||
| 300 | + ImGroupMemberDO currentMember = ImGroupMemberDO.builder() | ||
| 301 | + .groupId(10L).userId(1L) | ||
| 302 | + .status(ImGroupMemberStatusEnum.NORMAL.getStatus()) | ||
| 303 | + .joinTime(LocalDateTime.of(2026, 1, 1, 0, 0, 0)).build(); | ||
| 304 | + when(imGroupMemberService.getGroupMember(10L, 1L)).thenReturn(currentMember); | ||
| 305 | + | ||
| 306 | + // 准备:消息由用户 5 发,发送时间在 2026-04-12 10:00 | ||
| 307 | + ImGroupMessageDO message = ImGroupMessageDO.builder() | ||
| 308 | + .id(80L).groupId(10L).senderId(5L) | ||
| 309 | + .sendTime(LocalDateTime.of(2026, 4, 12, 10, 0, 0)).build(); | ||
| 310 | + when(imGroupMessageMapper.selectById(80L)).thenReturn(message); | ||
| 311 | + | ||
| 312 | + // 准备:群成员一览 | ||
| 313 | + // 用户 1: 正常,入群在消息之前 | ||
| 314 | + // 用户 2: 正常,入群在消息之前 | ||
| 315 | + // 用户 3: 正常,但入群在消息之后 → 不可见 | ||
| 316 | + // 用户 5: 发送者,不计入回执 | ||
| 317 | + List<ImGroupMemberDO> allMembers = List.of( | ||
| 318 | + currentMember, | ||
| 319 | + ImGroupMemberDO.builder().groupId(10L).userId(2L) | ||
| 320 | + .status(ImGroupMemberStatusEnum.NORMAL.getStatus()) | ||
| 321 | + .joinTime(LocalDateTime.of(2026, 1, 1, 0, 0, 0)).build(), | ||
| 322 | + ImGroupMemberDO.builder().groupId(10L).userId(3L) | ||
| 323 | + .status(ImGroupMemberStatusEnum.NORMAL.getStatus()) | ||
| 324 | + .joinTime(LocalDateTime.of(2026, 4, 13, 0, 0, 0)).build(), // 消息之后才入群 | ||
| 325 | + ImGroupMemberDO.builder().groupId(10L).userId(5L) | ||
| 326 | + .status(ImGroupMemberStatusEnum.NORMAL.getStatus()) | ||
| 327 | + .joinTime(LocalDateTime.of(2026, 1, 1, 0, 0, 0)).build() // 发送者 | ||
| 328 | + ); | ||
| 329 | + when(imGroupMemberService.selectByGroupId(10L)).thenReturn(allMembers); | ||
| 330 | + | ||
| 331 | + // 准备:Redis 已读位置 — 用户 1 读到 100, 用户 2 读到 50, 用户 3 读到 200 | ||
| 332 | + Map<Object, Object> positions = new HashMap<>(); | ||
| 333 | + positions.put("1", "100"); | ||
| 334 | + positions.put("2", "50"); | ||
| 335 | + positions.put("3", "200"); | ||
| 336 | + when(groupReadPositionRedisDAO.getAllReadPositions(10L)).thenReturn(positions); | ||
| 337 | + | ||
| 338 | + // 调用:查询 messageId=80 的已读用户 | ||
| 339 | + List<Long> readUsers = groupMessageService.getReadUsers(1L, 10L, 80L); | ||
| 340 | + | ||
| 341 | + // 断言: | ||
| 342 | + // 用户 1: 可见 + readMaxId=100>=80 → 已读 ✓(但排除发送者?不是发送者,所以算入) | ||
| 343 | + // 用户 2: 可见 + readMaxId=50<80 → 未读 ✗ | ||
| 344 | + // 用户 3: 入群在消息之后 → 不可见 → 不算 | ||
| 345 | + // 用户 5: 发送者 → 排除 | ||
| 346 | + assertEquals(1, readUsers.size()); | ||
| 347 | + assertTrue(readUsers.contains(1L)); | ||
| 348 | + assertFalse(readUsers.contains(2L)); | ||
| 349 | + assertFalse(readUsers.contains(3L)); | ||
| 350 | + assertFalse(readUsers.contains(5L)); | ||
| 351 | + } | ||
| 352 | + | ||
| 353 | + @Test | ||
| 354 | + public void testGetReadUsers_notInGroup() { | ||
| 355 | + // 准备:当前用户不在群中 | ||
| 356 | + when(imGroupMemberService.getGroupMember(10L, 99L)).thenReturn(null); | ||
| 357 | + | ||
| 358 | + // 调用并断言:越权校验 | ||
| 359 | + ServiceException exception = assertThrows(ServiceException.class, | ||
| 360 | + () -> groupMessageService.getReadUsers(99L, 10L, 80L)); | ||
| 361 | + assertEquals(GROUP_MEMBER_NOT_IN_GROUP.getCode(), exception.getCode()); | ||
| 362 | + } | ||
| 363 | + | ||
| 364 | +} |
yudao-module-im/yudao-module-im-biz/src/test/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageServiceImplTest.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.service.message; | ||
| 2 | + | ||
| 3 | +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.dal.dataobject.message.ImPrivateMessageDO; | ||
| 6 | +import cn.iocoder.yudao.module.im.dal.mysql.message.ImPrivateMessageMapper; | ||
| 7 | +import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum; | ||
| 8 | +import cn.iocoder.yudao.module.im.service.friend.ImFriendService; | ||
| 9 | +import cn.iocoder.yudao.module.im.service.sensitiveword.ImSensitiveWordService; | ||
| 10 | +import cn.iocoder.yudao.module.infra.api.websocket.WebSocketSenderApi; | ||
| 11 | +import org.junit.jupiter.api.Test; | ||
| 12 | +import org.junit.jupiter.api.extension.ExtendWith; | ||
| 13 | +import org.mockito.InjectMocks; | ||
| 14 | +import org.mockito.Mock; | ||
| 15 | +import org.mockito.junit.jupiter.MockitoExtension; | ||
| 16 | + | ||
| 17 | +import java.time.LocalDateTime; | ||
| 18 | +import java.util.List; | ||
| 19 | + | ||
| 20 | +import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*; | ||
| 21 | +import static org.junit.jupiter.api.Assertions.*; | ||
| 22 | +import static org.mockito.ArgumentMatchers.*; | ||
| 23 | +import static org.mockito.Mockito.*; | ||
| 24 | + | ||
| 25 | +/** | ||
| 26 | + * IM 私聊消息 Service 单元测试 | ||
| 27 | + * | ||
| 28 | + * @author 芋道源码 | ||
| 29 | + */ | ||
| 30 | +@ExtendWith(MockitoExtension.class) | ||
| 31 | +public class ImPrivateMessageServiceImplTest { | ||
| 32 | + | ||
| 33 | + @InjectMocks | ||
| 34 | + private ImPrivateMessageServiceImpl privateMessageService; | ||
| 35 | + | ||
| 36 | + @Mock | ||
| 37 | + private ImPrivateMessageMapper imPrivateMessageMapper; | ||
| 38 | + @Mock | ||
| 39 | + private ImFriendService imFriendService; | ||
| 40 | + @Mock | ||
| 41 | + private ImSensitiveWordService imSensitiveWordService; | ||
| 42 | + @Mock | ||
| 43 | + private WebSocketSenderApi webSocketSenderApi; | ||
| 44 | + | ||
| 45 | + private ImPrivateMessageSendReqVO buildSendReqVO() { | ||
| 46 | + ImPrivateMessageSendReqVO reqVO = new ImPrivateMessageSendReqVO(); | ||
| 47 | + reqVO.setClientMessageId("test-uuid-001"); | ||
| 48 | + reqVO.setReceiverId(2L); | ||
| 49 | + reqVO.setType(0); // TEXT | ||
| 50 | + reqVO.setContent("{\"content\":\"你好\"}"); | ||
| 51 | + return reqVO; | ||
| 52 | + } | ||
| 53 | + | ||
| 54 | + // ========== 发送测试 ========== | ||
| 55 | + | ||
| 56 | + @Test | ||
| 57 | + 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())).thenReturn(1); | ||
| 63 | + | ||
| 64 | + // 调用 | ||
| 65 | + ImPrivateMessageDO result = privateMessageService.sendMessage(1L, reqVO); | ||
| 66 | + | ||
| 67 | + // 断言 | ||
| 68 | + assertNotNull(result); | ||
| 69 | + assertEquals(1L, result.getSenderId()); | ||
| 70 | + assertEquals(2L, result.getReceiverId()); | ||
| 71 | + assertEquals(0, result.getType()); | ||
| 72 | + assertEquals(ImMessageStatusEnum.UNREAD.getStatus(), result.getStatus()); | ||
| 73 | + assertNotNull(result.getSendTime()); | ||
| 74 | + | ||
| 75 | + // 验证调用 | ||
| 76 | + verify(imFriendService).validateIsFriend(1L, 2L); | ||
| 77 | + verify(imSensitiveWordService).validateText(reqVO.getContent()); | ||
| 78 | + verify(imPrivateMessageMapper).insert(any()); | ||
| 79 | + verify(webSocketSenderApi, times(2)).sendObject(anyInt(), anyLong(), anyString(), any()); | ||
| 80 | + } | ||
| 81 | + | ||
| 82 | + @Test | ||
| 83 | + public void testSendMessage_clientMessageIdIdempotent() { | ||
| 84 | + // 准备:模拟已存在消息 | ||
| 85 | + ImPrivateMessageSendReqVO reqVO = buildSendReqVO(); | ||
| 86 | + ImPrivateMessageDO existingMessage = ImPrivateMessageDO.builder() | ||
| 87 | + .id(100L).clientMessageId("test-uuid-001").senderId(1L).receiverId(2L) | ||
| 88 | + .type(0).content("{\"content\":\"你好\"}").status(0) | ||
| 89 | + .sendTime(LocalDateTime.now()).build(); | ||
| 90 | + when(imPrivateMessageMapper.selectBySenderIdAndClientMessageId(1L, "test-uuid-001")) | ||
| 91 | + .thenReturn(existingMessage); | ||
| 92 | + | ||
| 93 | + // 调用 | ||
| 94 | + ImPrivateMessageDO result = privateMessageService.sendMessage(1L, reqVO); | ||
| 95 | + | ||
| 96 | + // 断言:返回已存在的消息 | ||
| 97 | + assertEquals(100L, result.getId()); | ||
| 98 | + // 验证不会重复插入 | ||
| 99 | + verify(imPrivateMessageMapper, never()).insert(any()); | ||
| 100 | + } | ||
| 101 | + | ||
| 102 | + @Test | ||
| 103 | + public void testSendMessage_notFriend() { | ||
| 104 | + // 准备 | ||
| 105 | + ImPrivateMessageSendReqVO reqVO = buildSendReqVO(); | ||
| 106 | + when(imPrivateMessageMapper.selectBySenderIdAndClientMessageId(1L, "test-uuid-001")) | ||
| 107 | + .thenReturn(null); | ||
| 108 | + doThrow(new ServiceException(FRIEND_NOT_FRIEND)) | ||
| 109 | + .when(imFriendService).validateIsFriend(1L, 2L); | ||
| 110 | + | ||
| 111 | + // 调用并断言 | ||
| 112 | + ServiceException exception = assertThrows(ServiceException.class, | ||
| 113 | + () -> privateMessageService.sendMessage(1L, reqVO)); | ||
| 114 | + assertEquals(FRIEND_NOT_FRIEND.getCode(), exception.getCode()); | ||
| 115 | + } | ||
| 116 | + | ||
| 117 | + // ========== pull 测试 ========== | ||
| 118 | + | ||
| 119 | + @Test | ||
| 120 | + public void testPullMessages_success() { | ||
| 121 | + // 准备 | ||
| 122 | + List<ImPrivateMessageDO> mockMessages = List.of( | ||
| 123 | + ImPrivateMessageDO.builder().id(1L).senderId(1L).receiverId(2L).build(), | ||
| 124 | + ImPrivateMessageDO.builder().id(2L).senderId(2L).receiverId(1L).build() | ||
| 125 | + ); | ||
| 126 | + when(imPrivateMessageMapper.selectListByMinId(1L, 0L, 100)).thenReturn(mockMessages); | ||
| 127 | + | ||
| 128 | + // 调用 | ||
| 129 | + List<ImPrivateMessageDO> result = privateMessageService.pullMessages(1L, 0L, 100); | ||
| 130 | + | ||
| 131 | + // 断言 | ||
| 132 | + assertEquals(2, result.size()); | ||
| 133 | + } | ||
| 134 | + | ||
| 135 | + @Test | ||
| 136 | + public void testPullMessages_sizeExceeded() { | ||
| 137 | + // 调用并断言 | ||
| 138 | + ServiceException exception = assertThrows(ServiceException.class, | ||
| 139 | + () -> privateMessageService.pullMessages(1L, 0L, 1001)); | ||
| 140 | + assertEquals(MESSAGE_PULL_SIZE_EXCEEDED.getCode(), exception.getCode()); | ||
| 141 | + } | ||
| 142 | + | ||
| 143 | + // ========== 已读测试 ========== | ||
| 144 | + | ||
| 145 | + @Test | ||
| 146 | + public void testReadMessages_success() { | ||
| 147 | + // 准备 | ||
| 148 | + when(imPrivateMessageMapper.updateStatusToRead(1L, 2L)).thenReturn(3); | ||
| 149 | + | ||
| 150 | + // 调用 | ||
| 151 | + privateMessageService.readMessages(1L, 2L); | ||
| 152 | + | ||
| 153 | + // 断言:发送了 READ 和 RECEIPT 事件 | ||
| 154 | + verify(webSocketSenderApi, times(2)).sendObject(anyInt(), anyLong(), anyString(), any()); | ||
| 155 | + } | ||
| 156 | + | ||
| 157 | + // ========== 撤回测试 ========== | ||
| 158 | + | ||
| 159 | + @Test | ||
| 160 | + public void testRecallMessage_success() { | ||
| 161 | + // 准备 | ||
| 162 | + ImPrivateMessageDO message = ImPrivateMessageDO.builder() | ||
| 163 | + .id(10L).senderId(1L).receiverId(2L) | ||
| 164 | + .status(ImMessageStatusEnum.UNREAD.getStatus()).build(); | ||
| 165 | + when(imPrivateMessageMapper.selectById(10L)).thenReturn(message); | ||
| 166 | + when(imPrivateMessageMapper.updateById(any())).thenReturn(1); | ||
| 167 | + | ||
| 168 | + // 调用 | ||
| 169 | + privateMessageService.recallMessage(1L, 10L); | ||
| 170 | + | ||
| 171 | + // 断言 | ||
| 172 | + verify(imPrivateMessageMapper).updateById(any()); | ||
| 173 | + // 验证推送了 RECALL 事件(给接收方和发送方) | ||
| 174 | + verify(webSocketSenderApi, times(2)).sendObject(anyInt(), anyLong(), anyString(), any()); | ||
| 175 | + } | ||
| 176 | + | ||
| 177 | + @Test | ||
| 178 | + public void testRecallMessage_notOwn() { | ||
| 179 | + // 准备 | ||
| 180 | + ImPrivateMessageDO message = ImPrivateMessageDO.builder() | ||
| 181 | + .id(10L).senderId(2L).receiverId(1L) | ||
| 182 | + .status(ImMessageStatusEnum.UNREAD.getStatus()).build(); | ||
| 183 | + when(imPrivateMessageMapper.selectById(10L)).thenReturn(message); | ||
| 184 | + | ||
| 185 | + // 调用并断言 | ||
| 186 | + ServiceException exception = assertThrows(ServiceException.class, | ||
| 187 | + () -> privateMessageService.recallMessage(1L, 10L)); | ||
| 188 | + assertEquals(MESSAGE_RECALL_DENIED.getCode(), exception.getCode()); | ||
| 189 | + } | ||
| 190 | + | ||
| 191 | + @Test | ||
| 192 | + public void testRecallMessage_alreadyRecalled() { | ||
| 193 | + // 准备 | ||
| 194 | + ImPrivateMessageDO message = ImPrivateMessageDO.builder() | ||
| 195 | + .id(10L).senderId(1L).receiverId(2L) | ||
| 196 | + .status(ImMessageStatusEnum.RECALL.getStatus()).build(); | ||
| 197 | + when(imPrivateMessageMapper.selectById(10L)).thenReturn(message); | ||
| 198 | + | ||
| 199 | + // 调用并断言 | ||
| 200 | + ServiceException exception = assertThrows(ServiceException.class, | ||
| 201 | + () -> privateMessageService.recallMessage(1L, 10L)); | ||
| 202 | + assertEquals(MESSAGE_ALREADY_RECALLED.getCode(), exception.getCode()); | ||
| 203 | + } | ||
| 204 | + | ||
| 205 | +} |
yudao-module-im/yudao-module-im-biz/src/test/java/cn/iocoder/yudao/module/im/service/message/ImWebSocketEventTest.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.service.message; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageSendReqVO; | ||
| 4 | +import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO; | ||
| 5 | +import cn.iocoder.yudao.module.im.dal.mysql.message.ImPrivateMessageMapper; | ||
| 6 | +import cn.iocoder.yudao.module.im.enums.message.ImWebSocketTypeConstants; | ||
| 7 | +import cn.iocoder.yudao.module.im.service.friend.ImFriendService; | ||
| 8 | +import cn.iocoder.yudao.module.im.service.sensitiveword.ImSensitiveWordService; | ||
| 9 | +import cn.iocoder.yudao.module.infra.api.websocket.WebSocketSenderApi; | ||
| 10 | +import org.junit.jupiter.api.Test; | ||
| 11 | +import org.junit.jupiter.api.extension.ExtendWith; | ||
| 12 | +import org.mockito.ArgumentCaptor; | ||
| 13 | +import org.mockito.InjectMocks; | ||
| 14 | +import org.mockito.Mock; | ||
| 15 | +import org.mockito.junit.jupiter.MockitoExtension; | ||
| 16 | + | ||
| 17 | +import java.util.Map; | ||
| 18 | + | ||
| 19 | +import static org.junit.jupiter.api.Assertions.*; | ||
| 20 | +import static org.mockito.ArgumentMatchers.*; | ||
| 21 | +import static org.mockito.Mockito.*; | ||
| 22 | + | ||
| 23 | +/** | ||
| 24 | + * IM WebSocket 事件单元测试 | ||
| 25 | + * <p> | ||
| 26 | + * 验证各事件的 payload 字段是否正确 | ||
| 27 | + * | ||
| 28 | + * @author 芋道源码 | ||
| 29 | + */ | ||
| 30 | +@ExtendWith(MockitoExtension.class) | ||
| 31 | +public class ImWebSocketEventTest { | ||
| 32 | + | ||
| 33 | + @InjectMocks | ||
| 34 | + private ImPrivateMessageServiceImpl privateMessageService; | ||
| 35 | + | ||
| 36 | + @Mock | ||
| 37 | + private ImPrivateMessageMapper imPrivateMessageMapper; | ||
| 38 | + @Mock | ||
| 39 | + private ImFriendService imFriendService; | ||
| 40 | + @Mock | ||
| 41 | + private ImSensitiveWordService imSensitiveWordService; | ||
| 42 | + @Mock | ||
| 43 | + private WebSocketSenderApi webSocketSenderApi; | ||
| 44 | + | ||
| 45 | + @Test | ||
| 46 | + public void testSendMessage_websocketEvent() { | ||
| 47 | + // 准备 | ||
| 48 | + ImPrivateMessageSendReqVO reqVO = new ImPrivateMessageSendReqVO(); | ||
| 49 | + reqVO.setClientMessageId("evt-uuid"); | ||
| 50 | + reqVO.setReceiverId(2L); | ||
| 51 | + reqVO.setType(0); | ||
| 52 | + reqVO.setContent("{\"content\":\"事件测试\"}"); | ||
| 53 | + | ||
| 54 | + when(imPrivateMessageMapper.selectBySenderIdAndClientMessageId(1L, "evt-uuid")).thenReturn(null); | ||
| 55 | + when(imPrivateMessageMapper.insert(any())).thenReturn(1); | ||
| 56 | + | ||
| 57 | + // 调用 | ||
| 58 | + privateMessageService.sendMessage(1L, reqVO); | ||
| 59 | + | ||
| 60 | + // 捕获 WebSocket 调用 | ||
| 61 | + ArgumentCaptor<String> typeCaptor = ArgumentCaptor.forClass(String.class); | ||
| 62 | + ArgumentCaptor<Object> contentCaptor = ArgumentCaptor.forClass(Object.class); | ||
| 63 | + verify(webSocketSenderApi, times(2)).sendObject(anyInt(), anyLong(), | ||
| 64 | + typeCaptor.capture(), contentCaptor.capture()); | ||
| 65 | + | ||
| 66 | + // 验证消息事件类型 | ||
| 67 | + assertEquals(ImWebSocketTypeConstants.PRIVATE_MESSAGE, typeCaptor.getAllValues().get(0)); | ||
| 68 | + assertEquals(ImWebSocketTypeConstants.PRIVATE_MESSAGE, typeCaptor.getAllValues().get(1)); | ||
| 69 | + | ||
| 70 | + // 验证 payload 中 id 字段按 String 返回 | ||
| 71 | + @SuppressWarnings("unchecked") | ||
| 72 | + Map<String, Object> payload = (Map<String, Object>) contentCaptor.getAllValues().get(0); | ||
| 73 | + assertTrue(payload.get("id") instanceof String, "id 应为 String 类型"); | ||
| 74 | + assertTrue(payload.get("senderId") instanceof String, "senderId 应为 String 类型"); | ||
| 75 | + assertTrue(payload.get("receiverId") instanceof String, "receiverId 应为 String 类型"); | ||
| 76 | + assertEquals("private", payload.get("messageScene")); | ||
| 77 | + } | ||
| 78 | + | ||
| 79 | + @Test | ||
| 80 | + public void testReadMessage_readAndReceiptEvents() { | ||
| 81 | + // 准备 | ||
| 82 | + when(imPrivateMessageMapper.updateStatusToRead(1L, 2L)).thenReturn(1); | ||
| 83 | + | ||
| 84 | + // 调用 | ||
| 85 | + privateMessageService.readMessages(1L, 2L); | ||
| 86 | + | ||
| 87 | + // 捕获 | ||
| 88 | + ArgumentCaptor<String> typeCaptor = ArgumentCaptor.forClass(String.class); | ||
| 89 | + ArgumentCaptor<Long> userCaptor = ArgumentCaptor.forClass(Long.class); | ||
| 90 | + verify(webSocketSenderApi, times(2)).sendObject(anyInt(), userCaptor.capture(), | ||
| 91 | + typeCaptor.capture(), any()); | ||
| 92 | + | ||
| 93 | + // 第一次:发给自己的 READ 事件 | ||
| 94 | + assertEquals(ImWebSocketTypeConstants.READ, typeCaptor.getAllValues().get(0)); | ||
| 95 | + assertEquals(1L, userCaptor.getAllValues().get(0)); | ||
| 96 | + // 第二次:发给对方的 RECEIPT 事件 | ||
| 97 | + assertEquals(ImWebSocketTypeConstants.RECEIPT, typeCaptor.getAllValues().get(1)); | ||
| 98 | + assertEquals(2L, userCaptor.getAllValues().get(1)); | ||
| 99 | + } | ||
| 100 | + | ||
| 101 | + @Test | ||
| 102 | + public void testRecallMessage_recallEvent() { | ||
| 103 | + // 准备 | ||
| 104 | + ImPrivateMessageDO message = ImPrivateMessageDO.builder() | ||
| 105 | + .id(10L).senderId(1L).receiverId(2L).status(0).build(); | ||
| 106 | + when(imPrivateMessageMapper.selectById(10L)).thenReturn(message); | ||
| 107 | + when(imPrivateMessageMapper.updateById(any())).thenReturn(1); | ||
| 108 | + | ||
| 109 | + // 调用 | ||
| 110 | + privateMessageService.recallMessage(1L, 10L); | ||
| 111 | + | ||
| 112 | + // 捕获 | ||
| 113 | + ArgumentCaptor<String> typeCaptor = ArgumentCaptor.forClass(String.class); | ||
| 114 | + ArgumentCaptor<Object> contentCaptor = ArgumentCaptor.forClass(Object.class); | ||
| 115 | + verify(webSocketSenderApi, times(2)).sendObject(anyInt(), anyLong(), | ||
| 116 | + typeCaptor.capture(), contentCaptor.capture()); | ||
| 117 | + | ||
| 118 | + // 验证 RECALL 事件类型 | ||
| 119 | + assertEquals(ImWebSocketTypeConstants.RECALL, typeCaptor.getAllValues().get(0)); | ||
| 120 | + | ||
| 121 | + // 验证 payload | ||
| 122 | + @SuppressWarnings("unchecked") | ||
| 123 | + Map<String, Object> payload = (Map<String, Object>) contentCaptor.getAllValues().get(0); | ||
| 124 | + assertEquals("10", payload.get("messageId")); | ||
| 125 | + assertEquals("private", payload.get("messageScene")); | ||
| 126 | + } | ||
| 127 | + | ||
| 128 | +} |