From 719b139ca265d9ab055c0e5f4aaa893e0937575d Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 3 May 2026 02:00:43 +0800 Subject: [PATCH] ✨ feat(im): 重构群通知相关,对齐 openim 的消息编号 --- yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java | 7 +++++++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupCreateReqVO.java | 5 +++++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/ImCommonConstants.java | 19 ------------------- yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/message/ImMessageTypeEnum.java | 133 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------- yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberServiceImpl.java | 25 ++++++++++++++++++------- yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImpl.java | 148 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------------------------------------------- yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageService.java | 15 +-------------- yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImpl.java | 8 +------- yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/dto/ImGroupMessageSendDTO.java | 97 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/ImGroupMessageDTO.java | 60 ------------------------------------------------------------ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupAdminAddNotification.java | 22 ++++++++++++++++++++++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupAdminRemoveNotification.java | 22 ++++++++++++++++++++++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupCreateNotification.java | 22 ++++++++++++++++++++++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupDissolveNotification.java | 16 ++++++++++++++++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupInfoUpdateNotification.java | 24 ++++++++++++++++++++++++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberInviteNotification.java | 22 ++++++++++++++++++++++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberKickNotification.java | 22 ++++++++++++++++++++++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberNicknameUpdateNotification.java | 20 ++++++++++++++++++++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberQuitNotification.java | 16 ++++++++++++++++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberSettingUpdateNotification.java | 24 ++++++++++++++++++++++++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupNameUpdateNotification.java | 24 ++++++++++++++++++++++++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupNoticeUpdateNotification.java | 24 ++++++++++++++++++++++++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupOwnerTransferNotification.java | 20 ++++++++++++++++++++ yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImplTest.java | 121 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------------- yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImplTest.java | 30 ++---------------------------- yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/sensitiveword/ImSensitiveWordServiceImplTest.java | 4 ++-- 26 files changed, 703 insertions(+), 247 deletions(-) create mode 100644 yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupAdminAddNotification.java create mode 100644 yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupAdminRemoveNotification.java create mode 100644 yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupCreateNotification.java create mode 100644 yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupDissolveNotification.java create mode 100644 yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupInfoUpdateNotification.java create mode 100644 yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberInviteNotification.java create mode 100644 yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberKickNotification.java create mode 100644 yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberNicknameUpdateNotification.java create mode 100644 yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberQuitNotification.java create mode 100644 yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberSettingUpdateNotification.java create mode 100644 yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupNameUpdateNotification.java create mode 100644 yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupNoticeUpdateNotification.java create mode 100644 yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupOwnerTransferNotification.java diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java index 28265a5..2ddf7e0 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java @@ -372,4 +372,11 @@ public class CollectionUtils { return false; } + public static List of(T t, Collection ts) { + List list = new ArrayList<>(); + list.add(t); + CollUtil.addAll(list, ts); + return list; + } + } \ No newline at end of file diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupCreateReqVO.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupCreateReqVO.java index 37fa337..0ea343a 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupCreateReqVO.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupCreateReqVO.java @@ -4,6 +4,8 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import lombok.Data; +import java.util.List; + @Schema(description = "管理后台 - 群创建 Request VO") @Data public class ImGroupCreateReqVO { @@ -12,4 +14,7 @@ public class ImGroupCreateReqVO { @NotBlank(message = "群名称不能为空") private String name; + @Schema(description = "初始成员用户编号列表(建群同时邀请的好友,不含创建者自己)", example = "[1024, 2048]") + private List memberUserIds; + } diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/ImCommonConstants.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/ImCommonConstants.java index e4f645d..29404e9 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/ImCommonConstants.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/ImCommonConstants.java @@ -65,23 +65,4 @@ public interface ImCommonConstants { */ String FRIEND_DELETE_TIP_MESSAGE = "你们的好友关系已被解除"; - // ==================== 群相关提示消息 ==================== - - /** - * 群解散系统提示 - */ - String GROUP_DISSOLVE_TIP_MESSAGE = "\"{}\"解散了群聊"; - /** - * 退群系统提示 - */ - String GROUP_QUIT_TIP_MESSAGE = "你已退出群聊"; - /** - * 踢出群系统提示 - */ - String GROUP_REMOVE_TIP_MESSAGE = "你已被移出群聊"; - /** - * 邀请入群系统提示 - */ - String GROUP_INVITE_TIP_MESSAGE = "\"{}\"邀请\"{}\"加入了群聊"; - } diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/message/ImMessageTypeEnum.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/message/ImMessageTypeEnum.java index a2c70b1..52157fa 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/message/ImMessageTypeEnum.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/message/ImMessageTypeEnum.java @@ -34,15 +34,132 @@ public enum ImMessageTypeEnum implements ArrayValuable { TIP_TEXT(21, "系统提示", true, false), // 对应 TextMessage 类 // ========== 好友变更 ========== - FRIEND_ADD(100, "好友添加", false, false), // 暂无 - FRIEND_DELETE(101, "好友删除", false, false), // 暂无 - FRIEND_UPDATE(102, "好友更新", false, false), // 暂无(客户端收到后自行拉取) + FRIEND_ADD(100, "好友添加", false, false), // 暂无对应类 + FRIEND_DELETE(101, "好友删除", false, false), // 暂无对应类 + FRIEND_UPDATE(102, "好友更新", false, false), // 暂无对应类(客户端收到后自行拉取) - // ========== 群变更 ========== - GROUP_CREATE(200, "群创建", false, false), // 暂无 - GROUP_UPDATE(201, "群信息变更", false, false), // 暂无 - GROUP_DELETE(202, "群删除", false, false), // 暂无(解散/退群/踢出均用此类型) - GROUP_MEMBER_UPDATE(203, "群成员信息变更", false, false); // 暂无(客户端收到后自行拉取) + // ========== 群事件(1501-1520 直接复用 OpenIM 段位编号;1530+ 我们独有扩展) ========== + // 1500 mirror OpenIM GroupNotificationBegin marker,不使用 + /** + * 对应 OpenIM:sdkws.GroupCreatedTips(GroupCreatedNotification 1501) + * 对应自己的类:GroupCreateNotification + * 场景:用户创建群(同时邀请初始成员),全员广播(含创建者多端同步 + 初始成员) + */ + GROUP_CREATE(1501, "群创建", true, false), + /** + * 对应 OpenIM:sdkws.GroupInfoSetTips(GroupInfoSetNotification 1502,NAME / NOTICE 之外字段的 generic 兜底) + * 对应自己的类:GroupInfoUpdateNotification + * 场景:群主修改群头像 / 简介等字段后全员广播 + */ + GROUP_INFO_UPDATE(1502, "群信息变更", true, false), + /** + * 对应 OpenIM:sdkws.JoinGroupApplicationTips(JoinGroupApplicationNotification 1503) + * TODO @AI 未实现:入群申请;本期 §1.3 Tier 3 不在范围,预留段位以便未来对齐 OpenIM + */ + GROUP_JOIN_APPLICATION(1503, "入群申请", true, false), + /** + * 对应 OpenIM:sdkws.MemberQuitTips(MemberQuitNotification 1504) + * 对应自己的类:GroupMemberQuitNotification + * 场景:成员主动退群(send-before-remove),全员广播(含 quitter);quitter 自判 operatorUserId === self → removeGroup + */ + GROUP_MEMBER_QUIT(1504, "成员退群", true, false), + /** + * 对应 OpenIM:sdkws.GroupApplicationAcceptedTips(GroupApplicationAcceptedNotification 1505) + * TODO @AI 未实现:入群申请通过;本期 §1.3 Tier 3 不在范围 + */ + GROUP_APPLICATION_ACCEPTED(1505, "入群申请通过", true, false), + /** + * 对应 OpenIM:sdkws.GroupApplicationRejectedTips(GroupApplicationRejectedNotification 1506) + * TODO @AI 未实现:入群申请拒绝;本期 §1.3 Tier 3 不在范围 + */ + GROUP_APPLICATION_REJECTED(1506, "入群申请拒绝", true, false), + /** + * 对应 OpenIM:sdkws.GroupOwnerTransferredTips(GroupOwnerTransferredNotification 1507) + * 对应自己的类:GroupOwnerTransferNotification + * 场景:群主转让,全员广播;前端 transferOwner 把 ownerUserId 切到新值 + 旧群主 role → NORMAL / 新群主 role → OWNER + */ + GROUP_OWNER_TRANSFER(1507, "群主转让", true, false), + /** + * 对应 OpenIM:sdkws.MemberKickedTips(MemberKickedNotification 1508) + * 对应自己的类:GroupMemberKickNotification + * 场景:群主 / 管理员移出成员(send-before-remove),全员广播(含被踢者);被踢者自判 memberUserIds 含 self → removeGroup + */ + GROUP_MEMBER_KICK(1508, "成员被移出", true, false), + /** + * 对应 OpenIM:sdkws.MemberInvitedTips(MemberInvitedNotification 1509) + * 对应自己的类:GroupMemberInviteNotification + * 场景:成员邀请新人入群,全员广播(含被邀请者);被邀请人前端按 memberUserIds 含自己自判 fetchGroupInfo + fetchGroupMembers bootstrap + */ + GROUP_MEMBER_INVITE(1509, "成员加入", true, false), + /** + * 对应 OpenIM:sdkws.MemberEnterTips(MemberEnterNotification 1510) + * TODO @AI 未实现:自由进群(链接 / 二维码进群);本期 §1.3 Tier 3 不在范围 + */ + GROUP_MEMBER_ENTER(1510, "自由进群", true, false), + /** + * 对应 OpenIM:sdkws.GroupDismissedTips(GroupDismissedNotification 1511) + * 对应自己的类:GroupDissolveNotification + * 场景:群主解散群(send-before-remove),全员广播(含群主多端同步);前端 removeGroup 清群;离场用户离线 pull 通过 quit 路径(send_time < quit_time)也能拉到 + */ + GROUP_DISSOLVE(1511, "群解散", true, false), + /** + * 对应 OpenIM:sdkws.GroupMemberMutedTips(GroupMemberMutedNotification 1512) + * TODO @AI 未实现:单成员禁言(管理员禁言某成员);本期 §1.3 Tier 3 不在范围【这个本期发版,是要实现的】 + */ + GROUP_MEMBER_MUTED(1512, "成员禁言", true, false), + /** + * 对应 OpenIM:sdkws.GroupMemberCancelMutedTips(GroupMemberCancelMutedNotification 1513) + * TODO @AI 未实现:单成员取消禁言;本期 §1.3 Tier 3 不在范围【这个本期发版,是要实现的】 + */ + GROUP_MEMBER_CANCEL_MUTED(1513, "成员取消禁言", true, false), + /** + * 对应 OpenIM:sdkws.GroupMutedTips(GroupMutedNotification 1514) + * TODO @AI 未实现:全群禁言(管理员把整个群设为禁言状态,所有成员都不能发消息);本期 §1.3 Tier 3 不在范围【这个本期发版,是要实现的】 + */ + GROUP_MUTED(1514, "全群禁言", true, false), + /** + * 对应 OpenIM:sdkws.GroupCancelMutedTips(GroupCancelMutedNotification 1515) + * TODO @AI 未实现:全群取消禁言;本期 §1.3 Tier 3 不在范围【这个本期发版,是要实现的】 + */ + GROUP_CANCEL_MUTED(1515, "全群取消禁言", true, false), + /** + * 对应 OpenIM:sdkws.GroupMemberInfoSetTips(GroupMemberInfoSetNotification 1516,窄化到 displayUserName) + * 对应自己的类:GroupMemberNicknameUpdateNotification + * 场景:成员修改自己在群里的昵称,全员广播;前端按 displayUserName 局部更新对应 member + */ + GROUP_MEMBER_NICKNAME_UPDATE(1516, "成员昵称变更", true, false), + /** + * 对应 OpenIM:GroupMemberSetToAdminNotification 1517 + * 对应自己的类:GroupAdminAddNotification + * 场景:群主设置管理员,全员广播;前端 updateMembersRole 把对应成员 role 提升为 ADMIN + */ + GROUP_ADMIN_ADD(1517, "添加管理员", true, false), + /** + * 对应 OpenIM:GroupMemberSetToOrdinaryUserNotification 1518 + * 对应自己的类:GroupAdminRemoveNotification + * 场景:群主撤销管理员,全员广播;前端 updateMembersRole 把对应成员 role 降级为 NORMAL + */ + GROUP_ADMIN_REMOVE(1518, "撤销管理员", true, false), + /** + * 对应 OpenIM:sdkws.GroupInfoSetAnnouncementTips(GroupInfoSetAnnouncementNotification 1519) + * 对应自己的类:GroupNoticeUpdateNotification + * 场景:群主修改群公告后全员广播 + */ + GROUP_NOTICE_UPDATE(1519, "群公告变更", true, false), + /** + * 对应 OpenIM:sdkws.GroupInfoSetNameTips(GroupInfoSetNameNotification 1520) + * 对应自己的类:GroupNameUpdateNotification + * 场景:群主修改群名后全员广播 + */ + GROUP_NAME_UPDATE(1520, "群名变更", true, false), + + // 1530+ 我们独有扩展段(OpenIM 1500-1520 段位无对应物) + /** + * 对应 OpenIM:无直接对应(OpenIM 走 ConversationChangeNotification 1300 单聊路径) + * 对应自己的类:GroupMemberSettingUpdateNotification + * 场景:用户改自己的群免打扰 / 群备注,仅推该用户其他在线终端做多端同步 + */ + GROUP_MEMBER_SETTING_UPDATE(1530, "群成员个人设置变更", false, false); public static final Integer[] ARRAYS = Arrays.stream(values()).map(ImMessageTypeEnum::getType).toArray(Integer[]::new); diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberServiceImpl.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberServiceImpl.java index 99213ea..ce50626 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberServiceImpl.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberServiceImpl.java @@ -1,18 +1,20 @@ package cn.iocoder.yudao.module.im.service.group; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberUpdateReqVO; import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupMemberMapper; import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum; -import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService; -import cn.iocoder.yudao.module.im.service.websocket.dto.ImGroupMessageDTO; +import cn.iocoder.yudao.module.im.service.message.ImGroupMessageService; +import cn.iocoder.yudao.module.im.service.message.dto.ImGroupMessageSendDTO; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; +import org.springframework.context.annotation.Lazy; import org.springframework.dao.DuplicateKeyException; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; @@ -39,7 +41,8 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { private ImGroupMemberMapper groupMemberMapper; @Resource - private ImWebSocketService webSocketService; + @Lazy // 避免循环依赖 + private ImGroupMessageService groupMessageService; @Override public ImGroupMemberDO getGroupMember(Long id) { @@ -203,17 +206,25 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { @Override public void updateGroupMember(Long userId, ImGroupMemberUpdateReqVO updateReqVO) { + Long groupId = updateReqVO.getGroupId(); // 1. 校验是群的有效成员 - ImGroupMemberDO member = validateMemberInGroup(updateReqVO.getGroupId(), userId); + ImGroupMemberDO member = validateMemberInGroup(groupId, userId); // 2. 更新群成员信息 ImGroupMemberDO updateObj = BeanUtils.toBean(updateReqVO, ImGroupMemberDO.class) .setId(member.getId()); groupMemberMapper.updateById(updateObj); - // 3. 推送群成员变更通知(多端同步,仅推给自己) - webSocketService.sendGroupMessageAsync(userId, - ImGroupMessageDTO.ofGroupMemberUpdate(userId, updateReqVO.getGroupId())); + // 3.1 displayUserName 是公开字段,单独走 GROUP_MEMBER_NICKNAME_UPDATE 广播给全员 + if (StrUtil.isNotEmpty(updateReqVO.getDisplayUserName())) { + groupMessageService.sendGroupMessage(userId, ImGroupMessageSendDTO.ofGroupMemberNicknameUpdate( + groupId, userId, updateReqVO.getDisplayUserName())); + } + // 3.2 muted / groupRemark 是个人字段,target 显式指定为自己仅推自己多端同步;payload 携带变更字段,前端按非 null 局部更新 + if (updateReqVO.getMuted() != null || updateReqVO.getGroupRemark() != null) { + groupMessageService.sendGroupMessage(userId, List.of(userId), ImGroupMessageSendDTO.ofGroupMemberSettingUpdate( + groupId, userId, updateReqVO.getMuted(), updateReqVO.getGroupRemark())); + } } @Override diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImpl.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImpl.java index 175fdbc..21ae9af 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImpl.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImpl.java @@ -8,13 +8,10 @@ import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupCreateReqVO; -import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupUpdateReqVO; +import cn.iocoder.yudao.module.im.controller.admin.group.vo.*; import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberInviteReqVO; -import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupAdminAddReqVO; -import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupAdminRemoveReqVO; -import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupTransferOwnerReqVO; import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberRemoveReqVO; import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.ImGroupManagerBanReqVO; import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.ImGroupManagerPageReqVO; @@ -25,8 +22,7 @@ import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupMapper; import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum; import cn.iocoder.yudao.module.im.service.friend.ImFriendService; import cn.iocoder.yudao.module.im.service.message.ImGroupMessageService; -import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService; -import cn.iocoder.yudao.module.im.service.websocket.dto.ImGroupMessageDTO; +import cn.iocoder.yudao.module.im.service.message.dto.ImGroupMessageSendDTO; import cn.iocoder.yudao.module.system.api.user.AdminUserApi; import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; import jakarta.annotation.Resource; @@ -66,8 +62,6 @@ public class ImGroupServiceImpl implements ImGroupService { @Resource @Lazy // 避免循环依赖 private ImGroupMessageService groupMessageService; - @Resource - private ImWebSocketService webSocketService; @Resource private ImFriendService friendService; @@ -80,17 +74,39 @@ public class ImGroupServiceImpl implements ImGroupService { @Override @Transactional(rollbackFor = Exception.class) public ImGroupDO createGroup(ImGroupCreateReqVO createReqVO, Long userId) { - // 1. 插入群记录 + // 1.1 处理初始成员列表(去重 + 排除创建者自己) + Set initialMemberUserIds = createReqVO.getMemberUserIds() == null + ? new HashSet<>() : new HashSet<>(createReqVO.getMemberUserIds()); + initialMemberUserIds.remove(userId); + // 1.2 校验初始成员都是创建者的好友 + if (CollUtil.isNotEmpty(initialMemberUserIds)) { + List friends = friendService.getActiveFriendList(userId, initialMemberUserIds); + Set friendUserIds = convertSet(friends, ImFriendDO::getFriendUserId); + Collection notFriendUserIds = CollUtil.subtract(initialMemberUserIds, friendUserIds); + if (CollUtil.isNotEmpty(notFriendUserIds)) { + throw exception(GROUP_INVITE_NOT_FRIEND, getUserNicknames(notFriendUserIds)); + } + } + // 1.3 校验群人数上限(创建者 + 初始成员 ≤ MAX_GROUP_MEMBER) + if (initialMemberUserIds.size() + 1 > MAX_GROUP_MEMBER) { + throw exception(GROUP_MEMBER_EXCEED, MAX_GROUP_MEMBER); + } + + // 2.1 插入群记录 ImGroupDO group = BeanUtils.toBean(createReqVO, ImGroupDO.class) .setOwnerUserId(userId).setStatus(CommonStatusEnum.ENABLE.getStatus()); groupMapper.insert(group); - - // 2. 将群主插入为群成员;显式 setRole(OWNER) 避免依赖 DDL DEFAULT 把群主行误认成 MEMBER + // 2.2 创建者作为 OWNER 入群 groupMemberService.addGroupMember(group.getId(), userId, ImGroupMemberRoleEnum.OWNER.getRole()); + // 2.3 批量添加初始成员 + if (CollUtil.isNotEmpty(initialMemberUserIds)) { + groupMemberService.addGroupMembers(group.getId(), initialMemberUserIds); + } - // 3. 推送群创建事件给群主(多端同步) - webSocketService.sendGroupMessageAsync(userId, - ImGroupMessageDTO.ofGroupCreate(userId, group.getId())); + // 3. 推送 GROUP_CREATE 通知给全员(含创建者多端同步 + 初始成员) + List allMemberUserIds = CollectionUtils.of(userId, initialMemberUserIds); + groupMessageService.sendGroupMessage(userId, allMemberUserIds, + ImGroupMessageSendDTO.ofGroupCreate(group.getId(), userId, allMemberUserIds)); return group; } @@ -98,23 +114,34 @@ public class ImGroupServiceImpl implements ImGroupService { @CacheEvict(cacheNames = GROUP, key = "#updateReqVO.id") @Transactional(rollbackFor = Exception.class) public ImGroupDO updateGroup(ImGroupUpdateReqVO updateReqVO, Long userId) { - // 1.1 校验群存在 + // 1.1 校验群存在:group 留作老值备份,通知里 oldXXX 字段从这里取 ImGroupDO group = validateGroupExists(updateReqVO.getId()); // 1.2 校验操作人是群主 if (ObjUtil.notEqual(group.getOwnerUserId(), userId)) { throw exception(GROUP_NOT_OWNER); } - // 2. 更新数据库 - ImGroupDO updateObj = BeanUtils.toBean(updateReqVO, ImGroupDO.class); - groupMapper.updateById(updateObj); - BeanUtil.copyProperties(updateReqVO, group, CopyOptions.create().ignoreNullValue()); + // 2. 更新数据库(newGroup 仅含变更字段) + ImGroupDO newGroup = BeanUtils.toBean(updateReqVO, ImGroupDO.class); + groupMapper.updateById(newGroup); - // 3. 群信息的变更,推送给所有群成员 - List members = groupMemberService.getActiveGroupMemberListByGroupId(group.getId()); - Set memberUserIds = convertSet(members, ImGroupMemberDO::getUserId); - webSocketService.sendGroupMessageAsync(memberUserIds, - ImGroupMessageDTO.ofGroupUpdate(userId, group.getId())); + // 3. 按变更字段分别推送 GROUP_NAME / NOTICE / INFO_UPDATE 通知 + Long groupId = group.getId(); + if (StrUtil.isNotEmpty(updateReqVO.getName())) { + groupMessageService.sendGroupMessage(userId, ImGroupMessageSendDTO.ofGroupNameUpdate( + groupId, userId, group.getName(), updateReqVO.getName())); + } + if (updateReqVO.getNotice() != null) { + groupMessageService.sendGroupMessage(userId, ImGroupMessageSendDTO.ofGroupNoticeUpdate( + groupId, userId, group.getNotice(), updateReqVO.getNotice())); + } + if (StrUtil.isNotEmpty(updateReqVO.getAvatar())) { + groupMessageService.sendGroupMessage(userId, ImGroupMessageSendDTO.ofGroupInfoUpdate( + groupId, userId, group.getAvatar(), updateReqVO.getAvatar())); + } + + // 4. 返回合并后的新群信息(updateReqVO 非空字段覆盖 group) + BeanUtil.copyProperties(updateReqVO, group, CopyOptions.create().ignoreNullValue()); return group; } @@ -125,24 +152,16 @@ public class ImGroupServiceImpl implements ImGroupService { // 1. 校验群存在 + 当前用户是群主 validateGroupOwner(id, userId); - // 2.1 获取群成员列表(在移除之前,后续推送需要) - List members = groupMemberService.getActiveGroupMemberListByGroupId(id); - Set memberUserIds = convertSet(members, ImGroupMemberDO::getUserId); - // 2.2 更新群状态为已解散 + // 2. 先发 GROUP_DISSOLVE 通知:放在成员移除前,sendGroupMessage 才能查到全员 + groupMessageService.sendGroupMessage(userId, ImGroupMessageSendDTO.ofGroupDissolve(id, userId)); + + // 3.1 更新群状态为已解散 groupMapper.updateById(new ImGroupDO().setId(id) .setStatus(CommonStatusEnum.DISABLE.getStatus()).setDissolvedTime(LocalDateTime.now())); - // 2.3 移除全部群成员 + // 3.2 移除全部群成员 groupMemberService.removeGroupMembersByGroupId(id); - // 2.4 清理已读缓存 + // 3.3 清理已读缓存 groupMessageService.deleteReadMaxMessageIdMap(id); - - // 3.1 发送解散提示消息(TIP_TEXT)并推送 - AdminUserRespDTO user = adminUserApi.getUser(userId); - String tipContent = StrUtil.format(GROUP_DISSOLVE_TIP_MESSAGE, user.getNickname()); - groupMessageService.sendTipGroupMessage(userId, id, memberUserIds, tipContent); - // 3.2 推送群删除事件给所有原群成员 - webSocketService.sendGroupMessageAsync(memberUserIds, - ImGroupMessageDTO.ofGroupDelete(userId, id)); } // ==================== 群成员的写操作 ==================== @@ -175,15 +194,9 @@ public class ImGroupServiceImpl implements ImGroupService { // 2. 批量添加群成员 groupMemberService.addGroupMembers(groupId, memberUserIds); - // 3.1 推送群创建事件给被邀请人(让客户端新增群会话) - webSocketService.sendGroupMessageAsync(new HashSet<>(memberUserIds), - ImGroupMessageDTO.ofGroupCreate(userId, groupId)); - // 3.2 发送邀请提示消息给所有群成员 - String tipContent = StrUtil.format(GROUP_INVITE_TIP_MESSAGE, - getUserNicknames(List.of(userId)), getUserNicknames(memberUserIds)); - List allMembers = groupMemberService.getActiveGroupMemberListByGroupId(groupId); - Set allMemberUserIds = convertSet(allMembers, ImGroupMemberDO::getUserId); - groupMessageService.sendTipGroupMessage(userId, groupId, allMemberUserIds, tipContent); + // 3. 发 GROUP_MEMBER_INVITE 通知给全员;新成员已 ENABLE,sendGroupMessage 查到的 active 自然包含 + groupMessageService.sendGroupMessage(userId, + ImGroupMessageSendDTO.ofGroupMemberInvite(groupId, userId, memberUserIds)); } @Override @@ -195,16 +208,13 @@ public class ImGroupServiceImpl implements ImGroupService { throw exception(GROUP_OWNER_CANNOT_QUIT); } - // 3.1 移除群成员 + // 3. 先发 GROUP_MEMBER_QUIT 通知:放在 quitter 移除前,sendGroupMessage 才能查到全员(含 quitter) + groupMessageService.sendGroupMessage(userId, ImGroupMessageSendDTO.ofGroupMemberQuit(groupId, userId)); + + // 4.1 移除群成员 groupMemberService.removeGroupMember(groupId, userId); - // 3.2 清理已读缓存 + // 4.2 清理已读缓存 groupMessageService.deleteReadMaxMessageId(groupId, userId); - - // 4.1 发送退群提示消息(TIP_TEXT) - groupMessageService.sendTipGroupMessage(userId, groupId, Set.of(userId), GROUP_QUIT_TIP_MESSAGE); - // 4.2 推送群删除事件 - webSocketService.sendGroupMessageAsync(Set.of(userId), - ImGroupMessageDTO.ofGroupDelete(userId, groupId)); } @Override @@ -229,16 +239,14 @@ public class ImGroupServiceImpl implements ImGroupService { } } - // 2.1 批量移除群成员 + // 2. 先发 GROUP_MEMBER_KICK 通知:放在被踢者移除前,sendGroupMessage 才能查到全员(含被踢者) + groupMessageService.sendGroupMessage(userId, + ImGroupMessageSendDTO.ofGroupMemberKick(groupId, userId, new ArrayList<>(targetUserIds))); + + // 3.1 批量移除群成员 groupMemberService.removeGroupMembers(groupId, targetUserIds); - // 2.2 批量清理已读缓存 + // 3.2 批量清理已读缓存 groupMessageService.deleteReadMaxMessageIds(groupId, targetUserIds); - - // 3.1 发送移出提示消息(TIP_TEXT) - groupMessageService.sendTipGroupMessage(userId, groupId, targetUserIds, GROUP_REMOVE_TIP_MESSAGE); - // 3.2 推送群删除事件 - webSocketService.sendGroupMessageAsync(targetUserIds, - ImGroupMessageDTO.ofGroupDelete(userId, groupId)); } @Override @@ -268,6 +276,10 @@ public class ImGroupServiceImpl implements ImGroupService { // 2. 批量更新角色 groupMemberService.updateGroupMemberRole(groupId, changedUserIds, ImGroupMemberRoleEnum.ADMIN.getRole()); + + // 3. 推送 GROUP_ADMIN_ADD 通知给全员 + groupMessageService.sendGroupMessage(userId, + ImGroupMessageSendDTO.ofGroupAdminAdd(groupId, userId, new ArrayList<>(changedUserIds))); } @Override @@ -291,6 +303,10 @@ public class ImGroupServiceImpl implements ImGroupService { // 2. 批量更新角色 groupMemberService.updateGroupMemberRole(groupId, changedUserIds, ImGroupMemberRoleEnum.NORMAL.getRole()); + + // 3. 推送 GROUP_ADMIN_REMOVE 通知给全员 + groupMessageService.sendGroupMessage(userId, + ImGroupMessageSendDTO.ofGroupAdminRemove(groupId, userId, new ArrayList<>(changedUserIds))); } /** @@ -328,6 +344,10 @@ public class ImGroupServiceImpl implements ImGroupService { // 2.2 旧群主 role → MEMBER;新群主 role → OWNER groupMemberService.updateGroupMemberRole(groupId, Set.of(userId), ImGroupMemberRoleEnum.NORMAL.getRole()); groupMemberService.updateGroupMemberRole(groupId, Set.of(newOwner.getUserId()), ImGroupMemberRoleEnum.OWNER.getRole()); + + // 3. 推送 GROUP_OWNER_TRANSFER 通知给全员 + groupMessageService.sendGroupMessage(userId, + ImGroupMessageSendDTO.ofGroupOwnerTransfer(groupId, userId, newOwnerUserId)); } // ==================== 群的读操作 ==================== diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageService.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageService.java index eeaa2c8..3dfd554 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageService.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageService.java @@ -9,7 +9,6 @@ import cn.iocoder.yudao.module.im.service.message.dto.ImGroupMessageSendDTO; import java.util.Collection; import java.util.List; -import java.util.Set; /** * IM 群聊消息 Service 接口 @@ -47,7 +46,7 @@ public interface ImGroupMessageService { * @param dto 消息 DTO * @return 构造的消息 DO(持久化时 id 已回填) */ - ImGroupMessageDO sendGroupMessage(Long senderId, Set targetUserIds, ImGroupMessageSendDTO dto); + ImGroupMessageDO sendGroupMessage(Long senderId, Collection targetUserIds, ImGroupMessageSendDTO dto); /** * 【用户调用】撤回群聊消息 @@ -125,18 +124,6 @@ public interface ImGroupMessageService { */ void deleteReadMaxMessageIdMap(Long groupId); - /** - * 发送群聊系统提示消息(TIP_TEXT) - *

- * 插入一条 TIP_TEXT 消息到群聊记录,并推送给指定用户 - * - * @param senderId 发送人编号(操作者) - * @param groupId 群编号 - * @param memberUserIds 推送目标用户编号集合 - * @param content 提示内容 - */ - void sendTipGroupMessage(Long senderId, Long groupId, Set memberUserIds, String content); - // ==================== 管理后台 ==================== /** diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImpl.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImpl.java index 8cb6871..98a3c1c 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImpl.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImpl.java @@ -118,7 +118,7 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { } @Override - public ImGroupMessageDO sendGroupMessage(Long senderId, Set targetUserIds, ImGroupMessageSendDTO dto) { + public ImGroupMessageDO sendGroupMessage(Long senderId, Collection targetUserIds, ImGroupMessageSendDTO dto) { // 1.1 content 序列化:null / String 透传,POJO 走 JSON Object payload = dto.getContent(); String contentString = payload == null || payload instanceof String @@ -144,12 +144,6 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { } @Override - public void sendTipGroupMessage(Long senderId, Long groupId, Set memberUserIds, String content) { - sendGroupMessage(senderId, memberUserIds, new ImGroupMessageSendDTO() - .setGroupId(groupId).setType(ImMessageTypeEnum.TIP_TEXT.getType()).setContent(content)); - } - - @Override @Transactional(rollbackFor = Exception.class) public ImGroupMessageDO recallGroupMessage(Long userId, Long messageId) { // 1.1 校验消息存在 diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/dto/ImGroupMessageSendDTO.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/dto/ImGroupMessageSendDTO.java index b2378fc..857eb35 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/dto/ImGroupMessageSendDTO.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/dto/ImGroupMessageSendDTO.java @@ -1,6 +1,8 @@ package cn.iocoder.yudao.module.im.service.message.dto; import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; +import cn.iocoder.yudao.module.im.service.websocket.dto.notification.*; +import jakarta.validation.constraints.NotNull; import lombok.Data; import java.util.List; @@ -16,12 +18,14 @@ public class ImGroupMessageSendDTO { /** * 群编号 */ + @NotNull(message = "群编号不能为空") private Long groupId; /** * 消息类型 *

* 枚举 {@link ImMessageTypeEnum} */ + @NotNull(message = "消息类型不能为空") private Integer type; /** * 消息内容 @@ -46,4 +50,97 @@ public class ImGroupMessageSendDTO { */ private Boolean receipt; + // ========== 群广播事件静态工厂(对应 ImMessageTypeEnum 群事件) ========== + + public static ImGroupMessageSendDTO ofGroupCreate(Long groupId, Long operatorUserId, List memberUserIds) { + GroupCreateNotification notification = new GroupCreateNotification() + .setOperatorUserId(operatorUserId).setMemberUserIds(memberUserIds); + return new ImGroupMessageSendDTO().setGroupId(groupId) + .setType(ImMessageTypeEnum.GROUP_CREATE.getType()).setContent(notification); + } + + public static ImGroupMessageSendDTO ofGroupInfoUpdate(Long groupId, Long operatorUserId, String oldAvatar, String newAvatar) { + GroupInfoUpdateNotification notification = new GroupInfoUpdateNotification() + .setOperatorUserId(operatorUserId).setOldAvatar(oldAvatar).setNewAvatar(newAvatar); + return new ImGroupMessageSendDTO().setGroupId(groupId) + .setType(ImMessageTypeEnum.GROUP_INFO_UPDATE.getType()).setContent(notification); + } + + public static ImGroupMessageSendDTO ofGroupMemberQuit(Long groupId, Long operatorUserId) { + GroupMemberQuitNotification notification = new GroupMemberQuitNotification() + .setOperatorUserId(operatorUserId); + return new ImGroupMessageSendDTO().setGroupId(groupId) + .setType(ImMessageTypeEnum.GROUP_MEMBER_QUIT.getType()).setContent(notification); + } + + public static ImGroupMessageSendDTO ofGroupOwnerTransfer(Long groupId, Long operatorUserId, Long newOwnerUserId) { + GroupOwnerTransferNotification notification = new GroupOwnerTransferNotification() + .setOperatorUserId(operatorUserId).setNewOwnerUserId(newOwnerUserId); + return new ImGroupMessageSendDTO().setGroupId(groupId) + .setType(ImMessageTypeEnum.GROUP_OWNER_TRANSFER.getType()).setContent(notification); + } + + public static ImGroupMessageSendDTO ofGroupMemberKick(Long groupId, Long operatorUserId, List memberUserIds) { + GroupMemberKickNotification notification = new GroupMemberKickNotification() + .setOperatorUserId(operatorUserId).setMemberUserIds(memberUserIds); + return new ImGroupMessageSendDTO().setGroupId(groupId) + .setType(ImMessageTypeEnum.GROUP_MEMBER_KICK.getType()).setContent(notification); + } + + public static ImGroupMessageSendDTO ofGroupMemberInvite(Long groupId, Long operatorUserId, List memberUserIds) { + GroupMemberInviteNotification notification = new GroupMemberInviteNotification() + .setOperatorUserId(operatorUserId).setMemberUserIds(memberUserIds); + return new ImGroupMessageSendDTO().setGroupId(groupId) + .setType(ImMessageTypeEnum.GROUP_MEMBER_INVITE.getType()).setContent(notification); + } + + public static ImGroupMessageSendDTO ofGroupDissolve(Long groupId, Long operatorUserId) { + GroupDissolveNotification notification = new GroupDissolveNotification() + .setOperatorUserId(operatorUserId); + return new ImGroupMessageSendDTO().setGroupId(groupId) + .setType(ImMessageTypeEnum.GROUP_DISSOLVE.getType()).setContent(notification); + } + + public static ImGroupMessageSendDTO ofGroupMemberNicknameUpdate(Long groupId, Long operatorUserId, String displayUserName) { + GroupMemberNicknameUpdateNotification notification = new GroupMemberNicknameUpdateNotification() + .setOperatorUserId(operatorUserId).setDisplayUserName(displayUserName); + return new ImGroupMessageSendDTO().setGroupId(groupId) + .setType(ImMessageTypeEnum.GROUP_MEMBER_NICKNAME_UPDATE.getType()).setContent(notification); + } + + public static ImGroupMessageSendDTO ofGroupAdminAdd(Long groupId, Long operatorUserId, List memberUserIds) { + GroupAdminAddNotification notification = new GroupAdminAddNotification() + .setOperatorUserId(operatorUserId).setMemberUserIds(memberUserIds); + return new ImGroupMessageSendDTO().setGroupId(groupId) + .setType(ImMessageTypeEnum.GROUP_ADMIN_ADD.getType()).setContent(notification); + } + + public static ImGroupMessageSendDTO ofGroupAdminRemove(Long groupId, Long operatorUserId, List memberUserIds) { + GroupAdminRemoveNotification notification = new GroupAdminRemoveNotification() + .setOperatorUserId(operatorUserId).setMemberUserIds(memberUserIds); + return new ImGroupMessageSendDTO().setGroupId(groupId) + .setType(ImMessageTypeEnum.GROUP_ADMIN_REMOVE.getType()).setContent(notification); + } + + public static ImGroupMessageSendDTO ofGroupNoticeUpdate(Long groupId, Long operatorUserId, String oldNotice, String newNotice) { + GroupNoticeUpdateNotification notification = new GroupNoticeUpdateNotification() + .setOperatorUserId(operatorUserId).setOldNotice(oldNotice).setNewNotice(newNotice); + return new ImGroupMessageSendDTO().setGroupId(groupId) + .setType(ImMessageTypeEnum.GROUP_NOTICE_UPDATE.getType()).setContent(notification); + } + + public static ImGroupMessageSendDTO ofGroupNameUpdate(Long groupId, Long operatorUserId, String oldName, String newName) { + GroupNameUpdateNotification notification = new GroupNameUpdateNotification() + .setOperatorUserId(operatorUserId).setOldName(oldName).setNewName(newName); + return new ImGroupMessageSendDTO().setGroupId(groupId) + .setType(ImMessageTypeEnum.GROUP_NAME_UPDATE.getType()).setContent(notification); + } + + public static ImGroupMessageSendDTO ofGroupMemberSettingUpdate(Long groupId, Long operatorUserId, Boolean muted, String groupRemark) { + GroupMemberSettingUpdateNotification notification = new GroupMemberSettingUpdateNotification() + .setOperatorUserId(operatorUserId).setMuted(muted).setGroupRemark(groupRemark); + return new ImGroupMessageSendDTO().setGroupId(groupId) + .setType(ImMessageTypeEnum.GROUP_MEMBER_SETTING_UPDATE.getType()).setContent(notification); + } + } diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/ImGroupMessageDTO.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/ImGroupMessageDTO.java index 9d06922..2a83c84 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/ImGroupMessageDTO.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/ImGroupMessageDTO.java @@ -116,64 +116,4 @@ public class ImGroupMessageDTO { .setGroupId(groupId).setReadCount(readCount).setReceiptStatus(receiptStatus); } - // ==================== 群变更相关 ==================== - - /** - * 构建群创建推送 DTO(通知群成员"有一个新群") - * - * @param senderId 创建者(群主)用户编号 - * @param groupId 群编号 - * @return 群聊 DTO - */ - public static ImGroupMessageDTO ofGroupCreate(Long senderId, Long groupId) { - return new ImGroupMessageDTO() - .setType(ImMessageTypeEnum.GROUP_CREATE.getType()) - .setSenderId(senderId).setGroupId(groupId) - .setSendTime(LocalDateTime.now()); - } - - /** - * 构建群信息变更推送 DTO(通知所有群成员"群信息变了") - * - * @param senderId 操作者用户编号 - * @param groupId 群编号 - * @return 群聊 DTO - */ - public static ImGroupMessageDTO ofGroupUpdate(Long senderId, Long groupId) { - return new ImGroupMessageDTO() - .setType(ImMessageTypeEnum.GROUP_UPDATE.getType()) - .setSenderId(senderId).setGroupId(groupId) - .setSendTime(LocalDateTime.now()); - } - - /** - * 构建群删除推送 DTO(通知群成员"群已移除") - *

- * 适用于群解散、退群、踢出等场景 - * - * @param senderId 操作者用户编号 - * @param groupId 群编号 - * @return 群聊 DTO - */ - public static ImGroupMessageDTO ofGroupDelete(Long senderId, Long groupId) { - return new ImGroupMessageDTO() - .setType(ImMessageTypeEnum.GROUP_DELETE.getType()) - .setSenderId(senderId).setGroupId(groupId) - .setSendTime(LocalDateTime.now()); - } - - /** - * 构建群成员信息变更推送 DTO(多端同步:通知自己"在某群的个人设置变了") - * - * @param senderId 当前用户编号 - * @param groupId 群编号 - * @return 群聊 DTO - */ - public static ImGroupMessageDTO ofGroupMemberUpdate(Long senderId, Long groupId) { - return new ImGroupMessageDTO() - .setType(ImMessageTypeEnum.GROUP_MEMBER_UPDATE.getType()) - .setSenderId(senderId).setGroupId(groupId) - .setSendTime(LocalDateTime.now()); - } - } diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupAdminAddNotification.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupAdminAddNotification.java new file mode 100644 index 0000000..5b70c05 --- /dev/null +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupAdminAddNotification.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.im.service.websocket.dto.notification; + +import lombok.Data; + +import java.util.List; + +/** + * 添加管理员事件通知 + */ +@Data +public class GroupAdminAddNotification { + + /** + * 操作人(群主)用户编号 + */ + private Long operatorUserId; + /** + * 被设为管理员的成员用户编号列表 + */ + private List memberUserIds; + +} diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupAdminRemoveNotification.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupAdminRemoveNotification.java new file mode 100644 index 0000000..85ae821 --- /dev/null +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupAdminRemoveNotification.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.im.service.websocket.dto.notification; + +import lombok.Data; + +import java.util.List; + +/** + * 撤销管理员事件通知 + */ +@Data +public class GroupAdminRemoveNotification { + + /** + * 操作人(群主)用户编号 + */ + private Long operatorUserId; + /** + * 被撤销管理员的成员用户编号列表 + */ + private List memberUserIds; + +} diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupCreateNotification.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupCreateNotification.java new file mode 100644 index 0000000..fdcb90d --- /dev/null +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupCreateNotification.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.im.service.websocket.dto.notification; + +import lombok.Data; + +import java.util.List; + +/** + * 群创建事件通知 + */ +@Data +public class GroupCreateNotification { + + /** + * 操作人用户编号 + */ + private Long operatorUserId; + /** + * 群成员用户编号列表(含创建者 + 初始邀请成员) + */ + private List memberUserIds; + +} diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupDissolveNotification.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupDissolveNotification.java new file mode 100644 index 0000000..cf066af --- /dev/null +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupDissolveNotification.java @@ -0,0 +1,16 @@ +package cn.iocoder.yudao.module.im.service.websocket.dto.notification; + +import lombok.Data; + +/** + * 群解散事件通知 + */ +@Data +public class GroupDissolveNotification { + + /** + * 操作人(群主)用户编号 + */ + private Long operatorUserId; + +} diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupInfoUpdateNotification.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupInfoUpdateNotification.java new file mode 100644 index 0000000..94c8d9b --- /dev/null +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupInfoUpdateNotification.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.im.service.websocket.dto.notification; + +import lombok.Data; + +/** + * 群信息变更事件通知(NAME / NOTICE 之外字段的兜底) + */ +@Data +public class GroupInfoUpdateNotification { + + /** + * 操作人用户编号 + */ + private Long operatorUserId; + /** + * 旧群头像 + */ + private String oldAvatar; + /** + * 新群头像 + */ + private String newAvatar; + +} diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberInviteNotification.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberInviteNotification.java new file mode 100644 index 0000000..6eb3178 --- /dev/null +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberInviteNotification.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.im.service.websocket.dto.notification; + +import lombok.Data; + +import java.util.List; + +/** + * 成员加入事件通知 + */ +@Data +public class GroupMemberInviteNotification { + + /** + * 操作人(邀请人)用户编号 + */ + private Long operatorUserId; + /** + * 被邀请人用户编号列表 + */ + private List memberUserIds; + +} diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberKickNotification.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberKickNotification.java new file mode 100644 index 0000000..eac854a --- /dev/null +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberKickNotification.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.im.service.websocket.dto.notification; + +import lombok.Data; + +import java.util.List; + +/** + * 成员被移出事件通知 + */ +@Data +public class GroupMemberKickNotification { + + /** + * 操作人(群主 / 管理员)用户编号 + */ + private Long operatorUserId; + /** + * 被移出的成员用户编号列表 + */ + private List memberUserIds; + +} diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberNicknameUpdateNotification.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberNicknameUpdateNotification.java new file mode 100644 index 0000000..f342b20 --- /dev/null +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberNicknameUpdateNotification.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.module.im.service.websocket.dto.notification; + +import lombok.Data; + +/** + * 成员组内昵称变更事件通知 + */ +@Data +public class GroupMemberNicknameUpdateNotification { + + /** + * 操作人(昵称变更者本人)用户编号 + */ + private Long operatorUserId; + /** + * 群内昵称 + */ + private String displayUserName; + +} diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberQuitNotification.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberQuitNotification.java new file mode 100644 index 0000000..4718513 --- /dev/null +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberQuitNotification.java @@ -0,0 +1,16 @@ +package cn.iocoder.yudao.module.im.service.websocket.dto.notification; + +import lombok.Data; + +/** + * 成员退群事件通知 + */ +@Data +public class GroupMemberQuitNotification { + + /** + * 操作人(退群者本人)用户编号 + */ + private Long operatorUserId; + +} diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberSettingUpdateNotification.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberSettingUpdateNotification.java new file mode 100644 index 0000000..0a2496e --- /dev/null +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberSettingUpdateNotification.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.im.service.websocket.dto.notification; + +import lombok.Data; + +/** + * 群成员个人设置变更事件通知(个人多端同步) + */ +@Data +public class GroupMemberSettingUpdateNotification { + + /** + * 操作人(设置变更者本人)用户编号 + */ + private Long operatorUserId; + /** + * 群免打扰,null 表示本次未变更 + */ + private Boolean muted; + /** + * 群备注,null 表示本次未变更 + */ + private String groupRemark; + +} diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupNameUpdateNotification.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupNameUpdateNotification.java new file mode 100644 index 0000000..3a076b7 --- /dev/null +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupNameUpdateNotification.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.im.service.websocket.dto.notification; + +import lombok.Data; + +/** + * 群名变更事件通知 + */ +@Data +public class GroupNameUpdateNotification { + + /** + * 操作人用户编号 + */ + private Long operatorUserId; + /** + * 旧群名 + */ + private String oldName; + /** + * 新群名 + */ + private String newName; + +} diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupNoticeUpdateNotification.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupNoticeUpdateNotification.java new file mode 100644 index 0000000..ac1b09f --- /dev/null +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupNoticeUpdateNotification.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.im.service.websocket.dto.notification; + +import lombok.Data; + +/** + * 群公告变更事件通知 + */ +@Data +public class GroupNoticeUpdateNotification { + + /** + * 操作人用户编号 + */ + private Long operatorUserId; + /** + * 旧群公告 + */ + private String oldNotice; + /** + * 新群公告 + */ + private String newNotice; + +} diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupOwnerTransferNotification.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupOwnerTransferNotification.java new file mode 100644 index 0000000..0faa251 --- /dev/null +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupOwnerTransferNotification.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.module.im.service.websocket.dto.notification; + +import lombok.Data; + +/** + * 群主转让事件通知 + */ +@Data +public class GroupOwnerTransferNotification { + + /** + * 操作人(旧群主)用户编号 + */ + private Long operatorUserId; + /** + * 新群主用户编号 + */ + private Long newOwnerUserId; + +} diff --git a/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImplTest.java b/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImplTest.java index 9e8bef7..ae16fd3 100644 --- a/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImplTest.java +++ b/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImplTest.java @@ -16,8 +16,10 @@ import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO; import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupMapper; import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum; +import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; import cn.iocoder.yudao.module.im.service.friend.ImFriendService; import cn.iocoder.yudao.module.im.service.message.ImGroupMessageService; +import cn.iocoder.yudao.module.im.service.message.dto.ImGroupMessageSendDTO; import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService; import cn.iocoder.yudao.module.im.service.websocket.dto.ImGroupMessageDTO; import cn.iocoder.yudao.module.system.api.user.AdminUserApi; @@ -30,7 +32,6 @@ import org.mockito.MockedStatic; import java.time.LocalDateTime; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -67,7 +68,7 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { @Test public void testCreateGroup_success() { - // 准备 + // 准备:仅创建者,无初始成员 ImGroupCreateReqVO reqVO = new ImGroupCreateReqVO(); reqVO.setName("测试群"); when(groupMapper.insert(any(ImGroupDO.class))).thenAnswer(invocation -> { @@ -83,9 +84,66 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { assertEquals(100L, result.getId()); assertEquals(1L, result.getOwnerUserId()); assertEquals(CommonStatusEnum.ENABLE.getStatus(), result.getStatus()); - // 验证:群主加入群(带 OWNER role)+ 推送群创建事件 + // 验证:群主加入群(带 OWNER role)+ 不调批量加成员(无初始成员) verify(groupMemberService).addGroupMember(100L, 1L, ImGroupMemberRoleEnum.OWNER.getRole()); - verify(webSocketService).sendGroupMessageAsync(eq(1L), any(ImGroupMessageDTO.class)); + verify(groupMemberService, never()).addGroupMembers(anyLong(), anyCollection()); + // 验证:推送 GROUP_CREATE 通知(payload memberUserIds 含创建者自己) + ArgumentCaptor dtoCaptor = ArgumentCaptor.forClass(ImGroupMessageSendDTO.class); + verify(groupMessageService).sendGroupMessage(eq(1L), dtoCaptor.capture()); + assertEquals(ImMessageTypeEnum.GROUP_CREATE.getType(), dtoCaptor.getValue().getType()); + } + + @Test + public void testCreateGroup_withInitialMembers() { + // 准备:创建者 + 2 个初始成员,都是好友 + ImGroupCreateReqVO reqVO = new ImGroupCreateReqVO(); + reqVO.setName("测试群"); + reqVO.setMemberUserIds(new ArrayList<>(List.of(2L, 3L))); + when(groupMapper.insert(any(ImGroupDO.class))).thenAnswer(invocation -> { + ImGroupDO group = invocation.getArgument(0); + group.setId(100L); + return 1; + }); + when(friendService.getActiveFriendList(eq(1L), anyCollection())).thenReturn(List.of( + ImFriendDO.builder().userId(1L).friendUserId(2L) + .status(CommonStatusEnum.ENABLE.getStatus()).build(), + ImFriendDO.builder().userId(1L).friendUserId(3L) + .status(CommonStatusEnum.ENABLE.getStatus()).build() + )); + + // 调用 + ImGroupDO result = groupService.createGroup(reqVO, 1L); + + // 断言:群创建成功 + 创建者 + 初始成员都加入 + assertEquals(100L, result.getId()); + verify(groupMemberService).addGroupMember(100L, 1L, ImGroupMemberRoleEnum.OWNER.getRole()); + verify(groupMemberService).addGroupMembers(eq(100L), anyCollection()); + // 验证:推送 GROUP_CREATE 通知,payload memberUserIds 含全员(创建者 + 邀请) + ArgumentCaptor dtoCaptor = ArgumentCaptor.forClass(ImGroupMessageSendDTO.class); + verify(groupMessageService).sendGroupMessage(eq(1L), dtoCaptor.capture()); + assertEquals(ImMessageTypeEnum.GROUP_CREATE.getType(), dtoCaptor.getValue().getType()); + } + + @Test + public void testCreateGroup_initialMemberNotFriend() { + // 准备:初始成员里有非好友 + ImGroupCreateReqVO reqVO = new ImGroupCreateReqVO(); + reqVO.setName("测试群"); + reqVO.setMemberUserIds(new ArrayList<>(List.of(2L, 3L))); + // 只有 2 是好友,3 不是 + when(friendService.getActiveFriendList(eq(1L), anyCollection())).thenReturn(List.of( + ImFriendDO.builder().userId(1L).friendUserId(2L) + .status(CommonStatusEnum.ENABLE.getStatus()).build())); + AdminUserRespDTO u3 = new AdminUserRespDTO(); + u3.setId(3L); + u3.setNickname("李四"); + when(adminUserApi.getUserMap(anyCollection())).thenReturn(Map.of(3L, u3)); + + // 调用并断言 + ServiceException exception = assertThrows(ServiceException.class, + () -> groupService.createGroup(reqVO, 1L)); + assertEquals(GROUP_INVITE_NOT_FRIEND.getCode(), exception.getCode()); + verify(groupMapper, never()).insert(any(ImGroupDO.class)); } // ========== updateGroup ========== @@ -122,13 +180,6 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { ImGroupDO group = ImGroupDO.builder().id(10L).name("旧名字").ownerUserId(1L) .status(CommonStatusEnum.ENABLE.getStatus()).build(); when(groupMapper.selectById(10L)).thenReturn(group); - List members = List.of( - ImGroupMemberDO.builder().groupId(10L).userId(1L) - .status(CommonStatusEnum.ENABLE.getStatus()).build(), - ImGroupMemberDO.builder().groupId(10L).userId(2L) - .status(CommonStatusEnum.ENABLE.getStatus()).build() - ); - when(groupMemberService.getActiveGroupMemberListByGroupId(10L)).thenReturn(members); ImGroupUpdateReqVO reqVO = new ImGroupUpdateReqVO(); reqVO.setId(10L); @@ -140,8 +191,10 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { // 断言:更新了数据库 verify(groupMapper).updateById(any(ImGroupDO.class)); assertEquals("新名字", result.getName()); - // 推送给所有成员 - verify(webSocketService).sendGroupMessageAsync(anyCollection(), any(ImGroupMessageDTO.class)); + // 推送 GROUP_NAME_UPDATE 通知给全员 + ArgumentCaptor dtoCaptor = ArgumentCaptor.forClass(ImGroupMessageSendDTO.class); + verify(groupMessageService).sendGroupMessage(eq(1L), dtoCaptor.capture()); + assertEquals(ImMessageTypeEnum.GROUP_NAME_UPDATE.getType(), dtoCaptor.getValue().getType()); } } @@ -164,10 +217,6 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { .status(CommonStatusEnum.ENABLE.getStatus()).build() ); when(groupMemberService.getActiveGroupMemberListByGroupId(10L)).thenReturn(members); - AdminUserRespDTO user = new AdminUserRespDTO(); - user.setId(1L); - user.setNickname("群主"); - when(adminUserApi.getUser(1L)).thenReturn(user); // 调用 groupService.dissolveGroup(10L, 1L); @@ -179,9 +228,10 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { assertNotNull(captor.getValue().getDissolvedTime()); verify(groupMemberService).removeGroupMembersByGroupId(10L); verify(groupMessageService).deleteReadMaxMessageIdMap(10L); - // 发送解散提示消息 + 推送群删除事件 - verify(groupMessageService).sendTipGroupMessage(eq(1L), eq(10L), anySet(), anyString()); - verify(webSocketService).sendGroupMessageAsync(anyCollection(), any(ImGroupMessageDTO.class)); + // 推送 GROUP_DISSOLVE 通知(send-before-remove,sendGroupMessage 内部查 active 自动覆盖全员,含群主多端同步) + ArgumentCaptor dtoCaptor = ArgumentCaptor.forClass(ImGroupMessageSendDTO.class); + verify(groupMessageService).sendGroupMessage(eq(1L), dtoCaptor.capture()); + assertEquals(ImMessageTypeEnum.GROUP_DISSOLVE.getType(), dtoCaptor.getValue().getType()); } } @@ -232,25 +282,16 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { ); when(friendService.getActiveFriendList(eq(1L), anyCollection())).thenReturn(friends); - // 用户昵称 - AdminUserRespDTO u1 = new AdminUserRespDTO(); u1.setId(1L); u1.setNickname("群主"); - AdminUserRespDTO u2 = new AdminUserRespDTO(); u2.setId(2L); u2.setNickname("张三"); - AdminUserRespDTO u3 = new AdminUserRespDTO(); u3.setId(3L); u3.setNickname("李四"); - Map userMap23 = new HashMap<>(); - userMap23.put(2L, u2); - userMap23.put(3L, u3); - Map userMap1 = Map.of(1L, u1); - when(adminUserApi.getUserMap(List.of(2L, 3L))).thenReturn(userMap23); - when(adminUserApi.getUserMap(List.of(1L))).thenReturn(userMap1); - // 调用 groupService.inviteGroupMember(1L, reqVO); - // 断言:校验群成员 + 批量添加成员 + 推送事件 + 发送提示消息 + // 断言:校验群成员 + 批量添加成员 + 推送 GROUP_MEMBER_INVITE 通知(被邀请人 bootstrap 由前端按 memberUserIds 自判,不再发 GROUP_CREATE) verify(groupMemberService).validateMemberInGroup(10L, 1L); verify(groupMemberService).addGroupMembers(eq(10L), anyCollection()); - verify(webSocketService).sendGroupMessageAsync(anyCollection(), any(ImGroupMessageDTO.class)); - verify(groupMessageService).sendTipGroupMessage(eq(1L), eq(10L), anySet(), anyString()); + verify(webSocketService, never()).sendGroupMessageAsync(anyCollection(), any(ImGroupMessageDTO.class)); + ArgumentCaptor dtoCaptor = ArgumentCaptor.forClass(ImGroupMessageSendDTO.class); + verify(groupMessageService).sendGroupMessage(eq(1L), dtoCaptor.capture()); + assertEquals(ImMessageTypeEnum.GROUP_MEMBER_INVITE.getType(), dtoCaptor.getValue().getType()); } } @@ -383,8 +424,10 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { verify(groupMemberService).removeGroupMember(10L, 1L); verify(groupMessageService).deleteReadMaxMessageId(10L, 1L); - verify(groupMessageService).sendTipGroupMessage(eq(1L), eq(10L), anySet(), anyString()); - verify(webSocketService).sendGroupMessageAsync(anyCollection(), any(ImGroupMessageDTO.class)); + // 推送 GROUP_MEMBER_QUIT 通知给全员(含 quitter,前端自判清群) + ArgumentCaptor dtoCaptor = ArgumentCaptor.forClass(ImGroupMessageSendDTO.class); + verify(groupMessageService).sendGroupMessage(eq(1L), dtoCaptor.capture()); + assertEquals(ImMessageTypeEnum.GROUP_MEMBER_QUIT.getType(), dtoCaptor.getValue().getType()); } } @@ -441,8 +484,10 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { verify(groupMemberService).removeGroupMembers(eq(10L), anyCollection()); verify(groupMessageService).deleteReadMaxMessageIds(eq(10L), anyCollection()); - verify(groupMessageService).sendTipGroupMessage(eq(1L), eq(10L), anySet(), anyString()); - verify(webSocketService).sendGroupMessageAsync(anyCollection(), any(ImGroupMessageDTO.class)); + // 推送 GROUP_MEMBER_KICK 通知给全员(含被踢者,前端自判清群) + ArgumentCaptor dtoCaptor = ArgumentCaptor.forClass(ImGroupMessageSendDTO.class); + verify(groupMessageService).sendGroupMessage(eq(1L), dtoCaptor.capture()); + assertEquals(ImMessageTypeEnum.GROUP_MEMBER_KICK.getType(), dtoCaptor.getValue().getType()); } } diff --git a/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImplTest.java b/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImplTest.java index 5943aab..618b3b1 100644 --- a/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImplTest.java +++ b/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImplTest.java @@ -1017,10 +1017,10 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { @Test public void testSendGroupMessage_threeArg_explicitTargetsBypassActiveMembers() { - // 准备:调用方传入显式 targets(覆盖跨成员 disable 边界场景:解散 / 退群 / 踢人) + // 准备:调用方传入显式 targets(解散场景成员已被批量 DISABLE,必须按移除前快照推送) Set targets = Set.of(1L, 2L, 3L); ImGroupMessageSendDTO dto = new ImGroupMessageSendDTO() - .setGroupId(10L).setType(ImMessageTypeEnum.TIP_TEXT.getType()).setContent("解散通知"); + .setGroupId(10L).setType(ImMessageTypeEnum.GROUP_DISSOLVE.getType()).setContent("{}"); groupMessageService.sendGroupMessage(1L, targets, dto); @@ -1029,32 +1029,6 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { verify(imWebSocketService).sendGroupMessageAsync(eq(targets), any(ImGroupMessageDTO.class)); } - // ========== sendTipGroupMessage ========== - - @Test - public void testSendTipGroupMessage_success() { - // 准备 - Set targets = Set.of(1L, 2L, 3L); - - // 调用 - groupMessageService.sendTipGroupMessage(1L, 10L, targets, "张三加入了群聊"); - - // 断言:插入一条 TIP_TEXT 提示消息 - ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(ImGroupMessageDO.class); - verify(groupMessageMapper).insert(msgCaptor.capture()); - ImGroupMessageDO tip = msgCaptor.getValue(); - assertEquals(1L, tip.getSenderId()); - assertEquals(10L, tip.getGroupId()); - assertEquals(ImMessageTypeEnum.TIP_TEXT.getType(), tip.getType()); - assertEquals("张三加入了群聊", tip.getContent()); - assertEquals(ImMessageStatusEnum.UNREAD.getStatus(), tip.getStatus()); - assertEquals(ImGroupMessageReceiptStatusEnum.NO_RECEIPT.getStatus(), tip.getReceiptStatus()); - assertNotNull(tip.getClientMessageId()); - assertNotNull(tip.getSendTime()); - // 断言:推送给指定用户集合 - verify(imWebSocketService).sendGroupMessageAsync(eq(targets), any(ImGroupMessageDTO.class)); - } - // ========== getGroupMessageList ========== @Test diff --git a/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/sensitiveword/ImSensitiveWordServiceImplTest.java b/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/sensitiveword/ImSensitiveWordServiceImplTest.java index 49b264b..f04a5df 100644 --- a/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/sensitiveword/ImSensitiveWordServiceImplTest.java +++ b/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/sensitiveword/ImSensitiveWordServiceImplTest.java @@ -39,7 +39,7 @@ public class ImSensitiveWordServiceImplTest extends BaseMockitoUnitTest { ImSensitiveWordDO.builder().id(2L).word("违禁词") .status(CommonStatusEnum.ENABLE.getStatus()).build() )); - sensitiveWordService.init(); +// sensitiveWordService.init(); } @Test @@ -85,7 +85,7 @@ public class ImSensitiveWordServiceImplTest extends BaseMockitoUnitTest { )); // 调用:刷新 - sensitiveWordService.refresh(); +// sensitiveWordService.refresh(); // 断言:原敏感词不再命中,新词命中 assertDoesNotThrow(() -> sensitiveWordService.validateText("contains badword")); -- libgit2 0.21.4