Commit 719b139ca265d9ab055c0e5f4aaa893e0937575d

Authored by YunaiV
1 parent 1c29398d

✨ feat(im): 重构群通知相关,对齐 openim 的消息编号

Showing 26 changed files with 703 additions and 247 deletions
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java
@@ -372,4 +372,11 @@ public class CollectionUtils { @@ -372,4 +372,11 @@ public class CollectionUtils {
372 return false; 372 return false;
373 } 373 }
374 374
  375 + public static <T> List<T> of(T t, Collection<T> ts) {
  376 + List<T> list = new ArrayList<>();
  377 + list.add(t);
  378 + CollUtil.addAll(list, ts);
  379 + return list;
  380 + }
  381 +
375 } 382 }
376 \ No newline at end of file 383 \ No newline at end of file
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; @@ -4,6 +4,8 @@ import io.swagger.v3.oas.annotations.media.Schema;
4 import jakarta.validation.constraints.NotBlank; 4 import jakarta.validation.constraints.NotBlank;
5 import lombok.Data; 5 import lombok.Data;
6 6
  7 +import java.util.List;
  8 +
7 @Schema(description = "管理后台 - 群创建 Request VO") 9 @Schema(description = "管理后台 - 群创建 Request VO")
8 @Data 10 @Data
9 public class ImGroupCreateReqVO { 11 public class ImGroupCreateReqVO {
@@ -12,4 +14,7 @@ public class ImGroupCreateReqVO { @@ -12,4 +14,7 @@ public class ImGroupCreateReqVO {
12 @NotBlank(message = "群名称不能为空") 14 @NotBlank(message = "群名称不能为空")
13 private String name; 15 private String name;
14 16
  17 + @Schema(description = "初始成员用户编号列表(建群同时邀请的好友,不含创建者自己)", example = "[1024, 2048]")
  18 + private List<Long> memberUserIds;
  19 +
15 } 20 }
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/ImCommonConstants.java
@@ -65,23 +65,4 @@ public interface ImCommonConstants { @@ -65,23 +65,4 @@ public interface ImCommonConstants {
65 */ 65 */
66 String FRIEND_DELETE_TIP_MESSAGE = "你们的好友关系已被解除"; 66 String FRIEND_DELETE_TIP_MESSAGE = "你们的好友关系已被解除";
67 67
68 - // ==================== 群相关提示消息 ====================  
69 -  
70 - /**  
71 - * 群解散系统提示  
72 - */  
73 - String GROUP_DISSOLVE_TIP_MESSAGE = "\"{}\"解散了群聊";  
74 - /**  
75 - * 退群系统提示  
76 - */  
77 - String GROUP_QUIT_TIP_MESSAGE = "你已退出群聊";  
78 - /**  
79 - * 踢出群系统提示  
80 - */  
81 - String GROUP_REMOVE_TIP_MESSAGE = "你已被移出群聊";  
82 - /**  
83 - * 邀请入群系统提示  
84 - */  
85 - String GROUP_INVITE_TIP_MESSAGE = "\"{}\"邀请\"{}\"加入了群聊";  
86 -  
87 } 68 }
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/message/ImMessageTypeEnum.java
@@ -34,15 +34,132 @@ public enum ImMessageTypeEnum implements ArrayValuable&lt;Integer&gt; { @@ -34,15 +34,132 @@ public enum ImMessageTypeEnum implements ArrayValuable&lt;Integer&gt; {
34 TIP_TEXT(21, "系统提示", true, false), // 对应 TextMessage 类 34 TIP_TEXT(21, "系统提示", true, false), // 对应 TextMessage 类
35 35
36 // ========== 好友变更 ========== 36 // ========== 好友变更 ==========
37 - FRIEND_ADD(100, "好友添加", false, false), // 暂无  
38 - FRIEND_DELETE(101, "好友删除", false, false), // 暂无  
39 - FRIEND_UPDATE(102, "好友更新", false, false), // 暂无(客户端收到后自行拉取) 37 + FRIEND_ADD(100, "好友添加", false, false), // 暂无对应类
  38 + FRIEND_DELETE(101, "好友删除", false, false), // 暂无对应类
  39 + FRIEND_UPDATE(102, "好友更新", false, false), // 暂无对应类(客户端收到后自行拉取)
40 40
41 - // ========== 群变更 ==========  
42 - GROUP_CREATE(200, "群创建", false, false), // 暂无  
43 - GROUP_UPDATE(201, "群信息变更", false, false), // 暂无  
44 - GROUP_DELETE(202, "群删除", false, false), // 暂无(解散/退群/踢出均用此类型)  
45 - GROUP_MEMBER_UPDATE(203, "群成员信息变更", false, false); // 暂无(客户端收到后自行拉取) 41 + // ========== 群事件(1501-1520 直接复用 OpenIM 段位编号;1530+ 我们独有扩展) ==========
  42 + // 1500 mirror OpenIM GroupNotificationBegin marker,不使用
  43 + /**
  44 + * 对应 OpenIM:sdkws.GroupCreatedTips(GroupCreatedNotification 1501)
  45 + * 对应自己的类:GroupCreateNotification
  46 + * 场景:用户创建群(同时邀请初始成员),全员广播(含创建者多端同步 + 初始成员)
  47 + */
  48 + GROUP_CREATE(1501, "群创建", true, false),
  49 + /**
  50 + * 对应 OpenIM:sdkws.GroupInfoSetTips(GroupInfoSetNotification 1502,NAME / NOTICE 之外字段的 generic 兜底)
  51 + * 对应自己的类:GroupInfoUpdateNotification
  52 + * 场景:群主修改群头像 / 简介等字段后全员广播
  53 + */
  54 + GROUP_INFO_UPDATE(1502, "群信息变更", true, false),
  55 + /**
  56 + * 对应 OpenIM:sdkws.JoinGroupApplicationTips(JoinGroupApplicationNotification 1503)
  57 + * TODO @AI 未实现:入群申请;本期 §1.3 Tier 3 不在范围,预留段位以便未来对齐 OpenIM
  58 + */
  59 + GROUP_JOIN_APPLICATION(1503, "入群申请", true, false),
  60 + /**
  61 + * 对应 OpenIM:sdkws.MemberQuitTips(MemberQuitNotification 1504)
  62 + * 对应自己的类:GroupMemberQuitNotification
  63 + * 场景:成员主动退群(send-before-remove),全员广播(含 quitter);quitter 自判 operatorUserId === self → removeGroup
  64 + */
  65 + GROUP_MEMBER_QUIT(1504, "成员退群", true, false),
  66 + /**
  67 + * 对应 OpenIM:sdkws.GroupApplicationAcceptedTips(GroupApplicationAcceptedNotification 1505)
  68 + * TODO @AI 未实现:入群申请通过;本期 §1.3 Tier 3 不在范围
  69 + */
  70 + GROUP_APPLICATION_ACCEPTED(1505, "入群申请通过", true, false),
  71 + /**
  72 + * 对应 OpenIM:sdkws.GroupApplicationRejectedTips(GroupApplicationRejectedNotification 1506)
  73 + * TODO @AI 未实现:入群申请拒绝;本期 §1.3 Tier 3 不在范围
  74 + */
  75 + GROUP_APPLICATION_REJECTED(1506, "入群申请拒绝", true, false),
  76 + /**
  77 + * 对应 OpenIM:sdkws.GroupOwnerTransferredTips(GroupOwnerTransferredNotification 1507)
  78 + * 对应自己的类:GroupOwnerTransferNotification
  79 + * 场景:群主转让,全员广播;前端 transferOwner 把 ownerUserId 切到新值 + 旧群主 role → NORMAL / 新群主 role → OWNER
  80 + */
  81 + GROUP_OWNER_TRANSFER(1507, "群主转让", true, false),
  82 + /**
  83 + * 对应 OpenIM:sdkws.MemberKickedTips(MemberKickedNotification 1508)
  84 + * 对应自己的类:GroupMemberKickNotification
  85 + * 场景:群主 / 管理员移出成员(send-before-remove),全员广播(含被踢者);被踢者自判 memberUserIds 含 self → removeGroup
  86 + */
  87 + GROUP_MEMBER_KICK(1508, "成员被移出", true, false),
  88 + /**
  89 + * 对应 OpenIM:sdkws.MemberInvitedTips(MemberInvitedNotification 1509)
  90 + * 对应自己的类:GroupMemberInviteNotification
  91 + * 场景:成员邀请新人入群,全员广播(含被邀请者);被邀请人前端按 memberUserIds 含自己自判 fetchGroupInfo + fetchGroupMembers bootstrap
  92 + */
  93 + GROUP_MEMBER_INVITE(1509, "成员加入", true, false),
  94 + /**
  95 + * 对应 OpenIM:sdkws.MemberEnterTips(MemberEnterNotification 1510)
  96 + * TODO @AI 未实现:自由进群(链接 / 二维码进群);本期 §1.3 Tier 3 不在范围
  97 + */
  98 + GROUP_MEMBER_ENTER(1510, "自由进群", true, false),
  99 + /**
  100 + * 对应 OpenIM:sdkws.GroupDismissedTips(GroupDismissedNotification 1511)
  101 + * 对应自己的类:GroupDissolveNotification
  102 + * 场景:群主解散群(send-before-remove),全员广播(含群主多端同步);前端 removeGroup 清群;离场用户离线 pull 通过 quit 路径(send_time < quit_time)也能拉到
  103 + */
  104 + GROUP_DISSOLVE(1511, "群解散", true, false),
  105 + /**
  106 + * 对应 OpenIM:sdkws.GroupMemberMutedTips(GroupMemberMutedNotification 1512)
  107 + * TODO @AI 未实现:单成员禁言(管理员禁言某成员);本期 §1.3 Tier 3 不在范围【这个本期发版,是要实现的】
  108 + */
  109 + GROUP_MEMBER_MUTED(1512, "成员禁言", true, false),
  110 + /**
  111 + * 对应 OpenIM:sdkws.GroupMemberCancelMutedTips(GroupMemberCancelMutedNotification 1513)
  112 + * TODO @AI 未实现:单成员取消禁言;本期 §1.3 Tier 3 不在范围【这个本期发版,是要实现的】
  113 + */
  114 + GROUP_MEMBER_CANCEL_MUTED(1513, "成员取消禁言", true, false),
  115 + /**
  116 + * 对应 OpenIM:sdkws.GroupMutedTips(GroupMutedNotification 1514)
  117 + * TODO @AI 未实现:全群禁言(管理员把整个群设为禁言状态,所有成员都不能发消息);本期 §1.3 Tier 3 不在范围【这个本期发版,是要实现的】
  118 + */
  119 + GROUP_MUTED(1514, "全群禁言", true, false),
  120 + /**
  121 + * 对应 OpenIM:sdkws.GroupCancelMutedTips(GroupCancelMutedNotification 1515)
  122 + * TODO @AI 未实现:全群取消禁言;本期 §1.3 Tier 3 不在范围【这个本期发版,是要实现的】
  123 + */
  124 + GROUP_CANCEL_MUTED(1515, "全群取消禁言", true, false),
  125 + /**
  126 + * 对应 OpenIM:sdkws.GroupMemberInfoSetTips(GroupMemberInfoSetNotification 1516,窄化到 displayUserName)
  127 + * 对应自己的类:GroupMemberNicknameUpdateNotification
  128 + * 场景:成员修改自己在群里的昵称,全员广播;前端按 displayUserName 局部更新对应 member
  129 + */
  130 + GROUP_MEMBER_NICKNAME_UPDATE(1516, "成员昵称变更", true, false),
  131 + /**
  132 + * 对应 OpenIM:GroupMemberSetToAdminNotification 1517
  133 + * 对应自己的类:GroupAdminAddNotification
  134 + * 场景:群主设置管理员,全员广播;前端 updateMembersRole 把对应成员 role 提升为 ADMIN
  135 + */
  136 + GROUP_ADMIN_ADD(1517, "添加管理员", true, false),
  137 + /**
  138 + * 对应 OpenIM:GroupMemberSetToOrdinaryUserNotification 1518
  139 + * 对应自己的类:GroupAdminRemoveNotification
  140 + * 场景:群主撤销管理员,全员广播;前端 updateMembersRole 把对应成员 role 降级为 NORMAL
  141 + */
  142 + GROUP_ADMIN_REMOVE(1518, "撤销管理员", true, false),
  143 + /**
  144 + * 对应 OpenIM:sdkws.GroupInfoSetAnnouncementTips(GroupInfoSetAnnouncementNotification 1519)
  145 + * 对应自己的类:GroupNoticeUpdateNotification
  146 + * 场景:群主修改群公告后全员广播
  147 + */
  148 + GROUP_NOTICE_UPDATE(1519, "群公告变更", true, false),
  149 + /**
  150 + * 对应 OpenIM:sdkws.GroupInfoSetNameTips(GroupInfoSetNameNotification 1520)
  151 + * 对应自己的类:GroupNameUpdateNotification
  152 + * 场景:群主修改群名后全员广播
  153 + */
  154 + GROUP_NAME_UPDATE(1520, "群名变更", true, false),
  155 +
  156 + // 1530+ 我们独有扩展段(OpenIM 1500-1520 段位无对应物)
  157 + /**
  158 + * 对应 OpenIM:无直接对应(OpenIM 走 ConversationChangeNotification 1300 单聊路径)
  159 + * 对应自己的类:GroupMemberSettingUpdateNotification
  160 + * 场景:用户改自己的群免打扰 / 群备注,仅推该用户其他在线终端做多端同步
  161 + */
  162 + GROUP_MEMBER_SETTING_UPDATE(1530, "群成员个人设置变更", false, false);
46 163
47 public static final Integer[] ARRAYS = Arrays.stream(values()).map(ImMessageTypeEnum::getType).toArray(Integer[]::new); 164 public static final Integer[] ARRAYS = Arrays.stream(values()).map(ImMessageTypeEnum::getType).toArray(Integer[]::new);
48 165
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberServiceImpl.java
1 package cn.iocoder.yudao.module.im.service.group; 1 package cn.iocoder.yudao.module.im.service.group;
2 2
3 import cn.hutool.core.collection.CollUtil; 3 import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.util.StrUtil;
4 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; 5 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
5 import cn.iocoder.yudao.framework.common.util.object.BeanUtils; 6 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
6 import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberUpdateReqVO; 7 import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberUpdateReqVO;
7 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; 8 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO;
8 import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupMemberMapper; 9 import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupMemberMapper;
9 import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum; 10 import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum;
10 -import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService;  
11 -import cn.iocoder.yudao.module.im.service.websocket.dto.ImGroupMessageDTO; 11 +import cn.iocoder.yudao.module.im.service.message.ImGroupMessageService;
  12 +import cn.iocoder.yudao.module.im.service.message.dto.ImGroupMessageSendDTO;
12 import jakarta.annotation.Resource; 13 import jakarta.annotation.Resource;
13 import lombok.extern.slf4j.Slf4j; 14 import lombok.extern.slf4j.Slf4j;
14 import org.springframework.cache.annotation.CacheEvict; 15 import org.springframework.cache.annotation.CacheEvict;
15 import org.springframework.cache.annotation.Cacheable; 16 import org.springframework.cache.annotation.Cacheable;
  17 +import org.springframework.context.annotation.Lazy;
16 import org.springframework.dao.DuplicateKeyException; 18 import org.springframework.dao.DuplicateKeyException;
17 import org.springframework.stereotype.Service; 19 import org.springframework.stereotype.Service;
18 import org.springframework.validation.annotation.Validated; 20 import org.springframework.validation.annotation.Validated;
@@ -39,7 +41,8 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { @@ -39,7 +41,8 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService {
39 private ImGroupMemberMapper groupMemberMapper; 41 private ImGroupMemberMapper groupMemberMapper;
40 42
41 @Resource 43 @Resource
42 - private ImWebSocketService webSocketService; 44 + @Lazy // 避免循环依赖
  45 + private ImGroupMessageService groupMessageService;
43 46
44 @Override 47 @Override
45 public ImGroupMemberDO getGroupMember(Long id) { 48 public ImGroupMemberDO getGroupMember(Long id) {
@@ -203,17 +206,25 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { @@ -203,17 +206,25 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService {
203 206
204 @Override 207 @Override
205 public void updateGroupMember(Long userId, ImGroupMemberUpdateReqVO updateReqVO) { 208 public void updateGroupMember(Long userId, ImGroupMemberUpdateReqVO updateReqVO) {
  209 + Long groupId = updateReqVO.getGroupId();
206 // 1. 校验是群的有效成员 210 // 1. 校验是群的有效成员
207 - ImGroupMemberDO member = validateMemberInGroup(updateReqVO.getGroupId(), userId); 211 + ImGroupMemberDO member = validateMemberInGroup(groupId, userId);
208 212
209 // 2. 更新群成员信息 213 // 2. 更新群成员信息
210 ImGroupMemberDO updateObj = BeanUtils.toBean(updateReqVO, ImGroupMemberDO.class) 214 ImGroupMemberDO updateObj = BeanUtils.toBean(updateReqVO, ImGroupMemberDO.class)
211 .setId(member.getId()); 215 .setId(member.getId());
212 groupMemberMapper.updateById(updateObj); 216 groupMemberMapper.updateById(updateObj);
213 217
214 - // 3. 推送群成员变更通知(多端同步,仅推给自己)  
215 - webSocketService.sendGroupMessageAsync(userId,  
216 - ImGroupMessageDTO.ofGroupMemberUpdate(userId, updateReqVO.getGroupId())); 218 + // 3.1 displayUserName 是公开字段,单独走 GROUP_MEMBER_NICKNAME_UPDATE 广播给全员
  219 + if (StrUtil.isNotEmpty(updateReqVO.getDisplayUserName())) {
  220 + groupMessageService.sendGroupMessage(userId, ImGroupMessageSendDTO.ofGroupMemberNicknameUpdate(
  221 + groupId, userId, updateReqVO.getDisplayUserName()));
  222 + }
  223 + // 3.2 muted / groupRemark 是个人字段,target 显式指定为自己仅推自己多端同步;payload 携带变更字段,前端按非 null 局部更新
  224 + if (updateReqVO.getMuted() != null || updateReqVO.getGroupRemark() != null) {
  225 + groupMessageService.sendGroupMessage(userId, List.of(userId), ImGroupMessageSendDTO.ofGroupMemberSettingUpdate(
  226 + groupId, userId, updateReqVO.getMuted(), updateReqVO.getGroupRemark()));
  227 + }
217 } 228 }
218 229
219 @Override 230 @Override
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; @@ -8,13 +8,10 @@ import cn.hutool.core.util.StrUtil;
8 import cn.hutool.extra.spring.SpringUtil; 8 import cn.hutool.extra.spring.SpringUtil;
9 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; 9 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
10 import cn.iocoder.yudao.framework.common.pojo.PageResult; 10 import cn.iocoder.yudao.framework.common.pojo.PageResult;
  11 +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
11 import cn.iocoder.yudao.framework.common.util.object.BeanUtils; 12 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
12 -import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupCreateReqVO;  
13 -import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupUpdateReqVO; 13 +import cn.iocoder.yudao.module.im.controller.admin.group.vo.*;
14 import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberInviteReqVO; 14 import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberInviteReqVO;
15 -import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupAdminAddReqVO;  
16 -import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupAdminRemoveReqVO;  
17 -import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupTransferOwnerReqVO;  
18 import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberRemoveReqVO; 15 import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberRemoveReqVO;
19 import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.ImGroupManagerBanReqVO; 16 import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.ImGroupManagerBanReqVO;
20 import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.ImGroupManagerPageReqVO; 17 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; @@ -25,8 +22,7 @@ import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupMapper;
25 import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum; 22 import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum;
26 import cn.iocoder.yudao.module.im.service.friend.ImFriendService; 23 import cn.iocoder.yudao.module.im.service.friend.ImFriendService;
27 import cn.iocoder.yudao.module.im.service.message.ImGroupMessageService; 24 import cn.iocoder.yudao.module.im.service.message.ImGroupMessageService;
28 -import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService;  
29 -import cn.iocoder.yudao.module.im.service.websocket.dto.ImGroupMessageDTO; 25 +import cn.iocoder.yudao.module.im.service.message.dto.ImGroupMessageSendDTO;
30 import cn.iocoder.yudao.module.system.api.user.AdminUserApi; 26 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
31 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; 27 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
32 import jakarta.annotation.Resource; 28 import jakarta.annotation.Resource;
@@ -66,8 +62,6 @@ public class ImGroupServiceImpl implements ImGroupService { @@ -66,8 +62,6 @@ public class ImGroupServiceImpl implements ImGroupService {
66 @Resource 62 @Resource
67 @Lazy // 避免循环依赖 63 @Lazy // 避免循环依赖
68 private ImGroupMessageService groupMessageService; 64 private ImGroupMessageService groupMessageService;
69 - @Resource  
70 - private ImWebSocketService webSocketService;  
71 65
72 @Resource 66 @Resource
73 private ImFriendService friendService; 67 private ImFriendService friendService;
@@ -80,17 +74,39 @@ public class ImGroupServiceImpl implements ImGroupService { @@ -80,17 +74,39 @@ public class ImGroupServiceImpl implements ImGroupService {
80 @Override 74 @Override
81 @Transactional(rollbackFor = Exception.class) 75 @Transactional(rollbackFor = Exception.class)
82 public ImGroupDO createGroup(ImGroupCreateReqVO createReqVO, Long userId) { 76 public ImGroupDO createGroup(ImGroupCreateReqVO createReqVO, Long userId) {
83 - // 1. 插入群记录 77 + // 1.1 处理初始成员列表(去重 + 排除创建者自己)
  78 + Set<Long> initialMemberUserIds = createReqVO.getMemberUserIds() == null
  79 + ? new HashSet<>() : new HashSet<>(createReqVO.getMemberUserIds());
  80 + initialMemberUserIds.remove(userId);
  81 + // 1.2 校验初始成员都是创建者的好友
  82 + if (CollUtil.isNotEmpty(initialMemberUserIds)) {
  83 + List<ImFriendDO> friends = friendService.getActiveFriendList(userId, initialMemberUserIds);
  84 + Set<Long> friendUserIds = convertSet(friends, ImFriendDO::getFriendUserId);
  85 + Collection<Long> notFriendUserIds = CollUtil.subtract(initialMemberUserIds, friendUserIds);
  86 + if (CollUtil.isNotEmpty(notFriendUserIds)) {
  87 + throw exception(GROUP_INVITE_NOT_FRIEND, getUserNicknames(notFriendUserIds));
  88 + }
  89 + }
  90 + // 1.3 校验群人数上限(创建者 + 初始成员 ≤ MAX_GROUP_MEMBER)
  91 + if (initialMemberUserIds.size() + 1 > MAX_GROUP_MEMBER) {
  92 + throw exception(GROUP_MEMBER_EXCEED, MAX_GROUP_MEMBER);
  93 + }
  94 +
  95 + // 2.1 插入群记录
84 ImGroupDO group = BeanUtils.toBean(createReqVO, ImGroupDO.class) 96 ImGroupDO group = BeanUtils.toBean(createReqVO, ImGroupDO.class)
85 .setOwnerUserId(userId).setStatus(CommonStatusEnum.ENABLE.getStatus()); 97 .setOwnerUserId(userId).setStatus(CommonStatusEnum.ENABLE.getStatus());
86 groupMapper.insert(group); 98 groupMapper.insert(group);
87 -  
88 - // 2. 将群主插入为群成员;显式 setRole(OWNER) 避免依赖 DDL DEFAULT 把群主行误认成 MEMBER 99 + // 2.2 创建者作为 OWNER 入群
89 groupMemberService.addGroupMember(group.getId(), userId, ImGroupMemberRoleEnum.OWNER.getRole()); 100 groupMemberService.addGroupMember(group.getId(), userId, ImGroupMemberRoleEnum.OWNER.getRole());
  101 + // 2.3 批量添加初始成员
  102 + if (CollUtil.isNotEmpty(initialMemberUserIds)) {
  103 + groupMemberService.addGroupMembers(group.getId(), initialMemberUserIds);
  104 + }
90 105
91 - // 3. 推送群创建事件给群主(多端同步)  
92 - webSocketService.sendGroupMessageAsync(userId,  
93 - ImGroupMessageDTO.ofGroupCreate(userId, group.getId())); 106 + // 3. 推送 GROUP_CREATE 通知给全员(含创建者多端同步 + 初始成员)
  107 + List<Long> allMemberUserIds = CollectionUtils.of(userId, initialMemberUserIds);
  108 + groupMessageService.sendGroupMessage(userId, allMemberUserIds,
  109 + ImGroupMessageSendDTO.ofGroupCreate(group.getId(), userId, allMemberUserIds));
94 return group; 110 return group;
95 } 111 }
96 112
@@ -98,23 +114,34 @@ public class ImGroupServiceImpl implements ImGroupService { @@ -98,23 +114,34 @@ public class ImGroupServiceImpl implements ImGroupService {
98 @CacheEvict(cacheNames = GROUP, key = "#updateReqVO.id") 114 @CacheEvict(cacheNames = GROUP, key = "#updateReqVO.id")
99 @Transactional(rollbackFor = Exception.class) 115 @Transactional(rollbackFor = Exception.class)
100 public ImGroupDO updateGroup(ImGroupUpdateReqVO updateReqVO, Long userId) { 116 public ImGroupDO updateGroup(ImGroupUpdateReqVO updateReqVO, Long userId) {
101 - // 1.1 校验群存在 117 + // 1.1 校验群存在:group 留作老值备份,通知里 oldXXX 字段从这里取
102 ImGroupDO group = validateGroupExists(updateReqVO.getId()); 118 ImGroupDO group = validateGroupExists(updateReqVO.getId());
103 // 1.2 校验操作人是群主 119 // 1.2 校验操作人是群主
104 if (ObjUtil.notEqual(group.getOwnerUserId(), userId)) { 120 if (ObjUtil.notEqual(group.getOwnerUserId(), userId)) {
105 throw exception(GROUP_NOT_OWNER); 121 throw exception(GROUP_NOT_OWNER);
106 } 122 }
107 123
108 - // 2. 更新数据库  
109 - ImGroupDO updateObj = BeanUtils.toBean(updateReqVO, ImGroupDO.class);  
110 - groupMapper.updateById(updateObj);  
111 - BeanUtil.copyProperties(updateReqVO, group, CopyOptions.create().ignoreNullValue()); 124 + // 2. 更新数据库(newGroup 仅含变更字段)
  125 + ImGroupDO newGroup = BeanUtils.toBean(updateReqVO, ImGroupDO.class);
  126 + groupMapper.updateById(newGroup);
112 127
113 - // 3. 群信息的变更,推送给所有群成员  
114 - List<ImGroupMemberDO> members = groupMemberService.getActiveGroupMemberListByGroupId(group.getId());  
115 - Set<Long> memberUserIds = convertSet(members, ImGroupMemberDO::getUserId);  
116 - webSocketService.sendGroupMessageAsync(memberUserIds,  
117 - ImGroupMessageDTO.ofGroupUpdate(userId, group.getId())); 128 + // 3. 按变更字段分别推送 GROUP_NAME / NOTICE / INFO_UPDATE 通知
  129 + Long groupId = group.getId();
  130 + if (StrUtil.isNotEmpty(updateReqVO.getName())) {
  131 + groupMessageService.sendGroupMessage(userId, ImGroupMessageSendDTO.ofGroupNameUpdate(
  132 + groupId, userId, group.getName(), updateReqVO.getName()));
  133 + }
  134 + if (updateReqVO.getNotice() != null) {
  135 + groupMessageService.sendGroupMessage(userId, ImGroupMessageSendDTO.ofGroupNoticeUpdate(
  136 + groupId, userId, group.getNotice(), updateReqVO.getNotice()));
  137 + }
  138 + if (StrUtil.isNotEmpty(updateReqVO.getAvatar())) {
  139 + groupMessageService.sendGroupMessage(userId, ImGroupMessageSendDTO.ofGroupInfoUpdate(
  140 + groupId, userId, group.getAvatar(), updateReqVO.getAvatar()));
  141 + }
  142 +
  143 + // 4. 返回合并后的新群信息(updateReqVO 非空字段覆盖 group)
  144 + BeanUtil.copyProperties(updateReqVO, group, CopyOptions.create().ignoreNullValue());
118 return group; 145 return group;
119 } 146 }
120 147
@@ -125,24 +152,16 @@ public class ImGroupServiceImpl implements ImGroupService { @@ -125,24 +152,16 @@ public class ImGroupServiceImpl implements ImGroupService {
125 // 1. 校验群存在 + 当前用户是群主 152 // 1. 校验群存在 + 当前用户是群主
126 validateGroupOwner(id, userId); 153 validateGroupOwner(id, userId);
127 154
128 - // 2.1 获取群成员列表(在移除之前,后续推送需要)  
129 - List<ImGroupMemberDO> members = groupMemberService.getActiveGroupMemberListByGroupId(id);  
130 - Set<Long> memberUserIds = convertSet(members, ImGroupMemberDO::getUserId);  
131 - // 2.2 更新群状态为已解散 155 + // 2. 先发 GROUP_DISSOLVE 通知:放在成员移除前,sendGroupMessage 才能查到全员
  156 + groupMessageService.sendGroupMessage(userId, ImGroupMessageSendDTO.ofGroupDissolve(id, userId));
  157 +
  158 + // 3.1 更新群状态为已解散
132 groupMapper.updateById(new ImGroupDO().setId(id) 159 groupMapper.updateById(new ImGroupDO().setId(id)
133 .setStatus(CommonStatusEnum.DISABLE.getStatus()).setDissolvedTime(LocalDateTime.now())); 160 .setStatus(CommonStatusEnum.DISABLE.getStatus()).setDissolvedTime(LocalDateTime.now()));
134 - // 2.3 移除全部群成员 161 + // 3.2 移除全部群成员
135 groupMemberService.removeGroupMembersByGroupId(id); 162 groupMemberService.removeGroupMembersByGroupId(id);
136 - // 2.4 清理已读缓存 163 + // 3.3 清理已读缓存
137 groupMessageService.deleteReadMaxMessageIdMap(id); 164 groupMessageService.deleteReadMaxMessageIdMap(id);
138 -  
139 - // 3.1 发送解散提示消息(TIP_TEXT)并推送  
140 - AdminUserRespDTO user = adminUserApi.getUser(userId);  
141 - String tipContent = StrUtil.format(GROUP_DISSOLVE_TIP_MESSAGE, user.getNickname());  
142 - groupMessageService.sendTipGroupMessage(userId, id, memberUserIds, tipContent);  
143 - // 3.2 推送群删除事件给所有原群成员  
144 - webSocketService.sendGroupMessageAsync(memberUserIds,  
145 - ImGroupMessageDTO.ofGroupDelete(userId, id));  
146 } 165 }
147 166
148 // ==================== 群成员的写操作 ==================== 167 // ==================== 群成员的写操作 ====================
@@ -175,15 +194,9 @@ public class ImGroupServiceImpl implements ImGroupService { @@ -175,15 +194,9 @@ public class ImGroupServiceImpl implements ImGroupService {
175 // 2. 批量添加群成员 194 // 2. 批量添加群成员
176 groupMemberService.addGroupMembers(groupId, memberUserIds); 195 groupMemberService.addGroupMembers(groupId, memberUserIds);
177 196
178 - // 3.1 推送群创建事件给被邀请人(让客户端新增群会话)  
179 - webSocketService.sendGroupMessageAsync(new HashSet<>(memberUserIds),  
180 - ImGroupMessageDTO.ofGroupCreate(userId, groupId));  
181 - // 3.2 发送邀请提示消息给所有群成员  
182 - String tipContent = StrUtil.format(GROUP_INVITE_TIP_MESSAGE,  
183 - getUserNicknames(List.of(userId)), getUserNicknames(memberUserIds));  
184 - List<ImGroupMemberDO> allMembers = groupMemberService.getActiveGroupMemberListByGroupId(groupId);  
185 - Set<Long> allMemberUserIds = convertSet(allMembers, ImGroupMemberDO::getUserId);  
186 - groupMessageService.sendTipGroupMessage(userId, groupId, allMemberUserIds, tipContent); 197 + // 3. 发 GROUP_MEMBER_INVITE 通知给全员;新成员已 ENABLE,sendGroupMessage 查到的 active 自然包含
  198 + groupMessageService.sendGroupMessage(userId,
  199 + ImGroupMessageSendDTO.ofGroupMemberInvite(groupId, userId, memberUserIds));
187 } 200 }
188 201
189 @Override 202 @Override
@@ -195,16 +208,13 @@ public class ImGroupServiceImpl implements ImGroupService { @@ -195,16 +208,13 @@ public class ImGroupServiceImpl implements ImGroupService {
195 throw exception(GROUP_OWNER_CANNOT_QUIT); 208 throw exception(GROUP_OWNER_CANNOT_QUIT);
196 } 209 }
197 210
198 - // 3.1 移除群成员 211 + // 3. 先发 GROUP_MEMBER_QUIT 通知:放在 quitter 移除前,sendGroupMessage 才能查到全员(含 quitter)
  212 + groupMessageService.sendGroupMessage(userId, ImGroupMessageSendDTO.ofGroupMemberQuit(groupId, userId));
  213 +
  214 + // 4.1 移除群成员
199 groupMemberService.removeGroupMember(groupId, userId); 215 groupMemberService.removeGroupMember(groupId, userId);
200 - // 3.2 清理已读缓存 216 + // 4.2 清理已读缓存
201 groupMessageService.deleteReadMaxMessageId(groupId, userId); 217 groupMessageService.deleteReadMaxMessageId(groupId, userId);
202 -  
203 - // 4.1 发送退群提示消息(TIP_TEXT)  
204 - groupMessageService.sendTipGroupMessage(userId, groupId, Set.of(userId), GROUP_QUIT_TIP_MESSAGE);  
205 - // 4.2 推送群删除事件  
206 - webSocketService.sendGroupMessageAsync(Set.of(userId),  
207 - ImGroupMessageDTO.ofGroupDelete(userId, groupId));  
208 } 218 }
209 219
210 @Override 220 @Override
@@ -229,16 +239,14 @@ public class ImGroupServiceImpl implements ImGroupService { @@ -229,16 +239,14 @@ public class ImGroupServiceImpl implements ImGroupService {
229 } 239 }
230 } 240 }
231 241
232 - // 2.1 批量移除群成员 242 + // 2. 先发 GROUP_MEMBER_KICK 通知:放在被踢者移除前,sendGroupMessage 才能查到全员(含被踢者)
  243 + groupMessageService.sendGroupMessage(userId,
  244 + ImGroupMessageSendDTO.ofGroupMemberKick(groupId, userId, new ArrayList<>(targetUserIds)));
  245 +
  246 + // 3.1 批量移除群成员
233 groupMemberService.removeGroupMembers(groupId, targetUserIds); 247 groupMemberService.removeGroupMembers(groupId, targetUserIds);
234 - // 2.2 批量清理已读缓存 248 + // 3.2 批量清理已读缓存
235 groupMessageService.deleteReadMaxMessageIds(groupId, targetUserIds); 249 groupMessageService.deleteReadMaxMessageIds(groupId, targetUserIds);
236 -  
237 - // 3.1 发送移出提示消息(TIP_TEXT)  
238 - groupMessageService.sendTipGroupMessage(userId, groupId, targetUserIds, GROUP_REMOVE_TIP_MESSAGE);  
239 - // 3.2 推送群删除事件  
240 - webSocketService.sendGroupMessageAsync(targetUserIds,  
241 - ImGroupMessageDTO.ofGroupDelete(userId, groupId));  
242 } 250 }
243 251
244 @Override 252 @Override
@@ -268,6 +276,10 @@ public class ImGroupServiceImpl implements ImGroupService { @@ -268,6 +276,10 @@ public class ImGroupServiceImpl implements ImGroupService {
268 276
269 // 2. 批量更新角色 277 // 2. 批量更新角色
270 groupMemberService.updateGroupMemberRole(groupId, changedUserIds, ImGroupMemberRoleEnum.ADMIN.getRole()); 278 groupMemberService.updateGroupMemberRole(groupId, changedUserIds, ImGroupMemberRoleEnum.ADMIN.getRole());
  279 +
  280 + // 3. 推送 GROUP_ADMIN_ADD 通知给全员
  281 + groupMessageService.sendGroupMessage(userId,
  282 + ImGroupMessageSendDTO.ofGroupAdminAdd(groupId, userId, new ArrayList<>(changedUserIds)));
271 } 283 }
272 284
273 @Override 285 @Override
@@ -291,6 +303,10 @@ public class ImGroupServiceImpl implements ImGroupService { @@ -291,6 +303,10 @@ public class ImGroupServiceImpl implements ImGroupService {
291 303
292 // 2. 批量更新角色 304 // 2. 批量更新角色
293 groupMemberService.updateGroupMemberRole(groupId, changedUserIds, ImGroupMemberRoleEnum.NORMAL.getRole()); 305 groupMemberService.updateGroupMemberRole(groupId, changedUserIds, ImGroupMemberRoleEnum.NORMAL.getRole());
  306 +
  307 + // 3. 推送 GROUP_ADMIN_REMOVE 通知给全员
  308 + groupMessageService.sendGroupMessage(userId,
  309 + ImGroupMessageSendDTO.ofGroupAdminRemove(groupId, userId, new ArrayList<>(changedUserIds)));
294 } 310 }
295 311
296 /** 312 /**
@@ -328,6 +344,10 @@ public class ImGroupServiceImpl implements ImGroupService { @@ -328,6 +344,10 @@ public class ImGroupServiceImpl implements ImGroupService {
328 // 2.2 旧群主 role → MEMBER;新群主 role → OWNER 344 // 2.2 旧群主 role → MEMBER;新群主 role → OWNER
329 groupMemberService.updateGroupMemberRole(groupId, Set.of(userId), ImGroupMemberRoleEnum.NORMAL.getRole()); 345 groupMemberService.updateGroupMemberRole(groupId, Set.of(userId), ImGroupMemberRoleEnum.NORMAL.getRole());
330 groupMemberService.updateGroupMemberRole(groupId, Set.of(newOwner.getUserId()), ImGroupMemberRoleEnum.OWNER.getRole()); 346 groupMemberService.updateGroupMemberRole(groupId, Set.of(newOwner.getUserId()), ImGroupMemberRoleEnum.OWNER.getRole());
  347 +
  348 + // 3. 推送 GROUP_OWNER_TRANSFER 通知给全员
  349 + groupMessageService.sendGroupMessage(userId,
  350 + ImGroupMessageSendDTO.ofGroupOwnerTransfer(groupId, userId, newOwnerUserId));
331 } 351 }
332 352
333 // ==================== 群的读操作 ==================== 353 // ==================== 群的读操作 ====================
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; @@ -9,7 +9,6 @@ import cn.iocoder.yudao.module.im.service.message.dto.ImGroupMessageSendDTO;
9 9
10 import java.util.Collection; 10 import java.util.Collection;
11 import java.util.List; 11 import java.util.List;
12 -import java.util.Set;  
13 12
14 /** 13 /**
15 * IM 群聊消息 Service 接口 14 * IM 群聊消息 Service 接口
@@ -47,7 +46,7 @@ public interface ImGroupMessageService { @@ -47,7 +46,7 @@ public interface ImGroupMessageService {
47 * @param dto 消息 DTO 46 * @param dto 消息 DTO
48 * @return 构造的消息 DO(持久化时 id 已回填) 47 * @return 构造的消息 DO(持久化时 id 已回填)
49 */ 48 */
50 - ImGroupMessageDO sendGroupMessage(Long senderId, Set<Long> targetUserIds, ImGroupMessageSendDTO dto); 49 + ImGroupMessageDO sendGroupMessage(Long senderId, Collection<Long> targetUserIds, ImGroupMessageSendDTO dto);
51 50
52 /** 51 /**
53 * 【用户调用】撤回群聊消息 52 * 【用户调用】撤回群聊消息
@@ -125,18 +124,6 @@ public interface ImGroupMessageService { @@ -125,18 +124,6 @@ public interface ImGroupMessageService {
125 */ 124 */
126 void deleteReadMaxMessageIdMap(Long groupId); 125 void deleteReadMaxMessageIdMap(Long groupId);
127 126
128 - /**  
129 - * 发送群聊系统提示消息(TIP_TEXT)  
130 - * <p>  
131 - * 插入一条 TIP_TEXT 消息到群聊记录,并推送给指定用户  
132 - *  
133 - * @param senderId 发送人编号(操作者)  
134 - * @param groupId 群编号  
135 - * @param memberUserIds 推送目标用户编号集合  
136 - * @param content 提示内容  
137 - */  
138 - void sendTipGroupMessage(Long senderId, Long groupId, Set<Long> memberUserIds, String content);  
139 -  
140 // ==================== 管理后台 ==================== 127 // ==================== 管理后台 ====================
141 128
142 /** 129 /**
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImpl.java
@@ -118,7 +118,7 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { @@ -118,7 +118,7 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService {
118 } 118 }
119 119
120 @Override 120 @Override
121 - public ImGroupMessageDO sendGroupMessage(Long senderId, Set<Long> targetUserIds, ImGroupMessageSendDTO dto) { 121 + public ImGroupMessageDO sendGroupMessage(Long senderId, Collection<Long> targetUserIds, ImGroupMessageSendDTO dto) {
122 // 1.1 content 序列化:null / String 透传,POJO 走 JSON 122 // 1.1 content 序列化:null / String 透传,POJO 走 JSON
123 Object payload = dto.getContent(); 123 Object payload = dto.getContent();
124 String contentString = payload == null || payload instanceof String 124 String contentString = payload == null || payload instanceof String
@@ -144,12 +144,6 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { @@ -144,12 +144,6 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService {
144 } 144 }
145 145
146 @Override 146 @Override
147 - public void sendTipGroupMessage(Long senderId, Long groupId, Set<Long> memberUserIds, String content) {  
148 - sendGroupMessage(senderId, memberUserIds, new ImGroupMessageSendDTO()  
149 - .setGroupId(groupId).setType(ImMessageTypeEnum.TIP_TEXT.getType()).setContent(content));  
150 - }  
151 -  
152 - @Override  
153 @Transactional(rollbackFor = Exception.class) 147 @Transactional(rollbackFor = Exception.class)
154 public ImGroupMessageDO recallGroupMessage(Long userId, Long messageId) { 148 public ImGroupMessageDO recallGroupMessage(Long userId, Long messageId) {
155 // 1.1 校验消息存在 149 // 1.1 校验消息存在
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/dto/ImGroupMessageSendDTO.java
1 package cn.iocoder.yudao.module.im.service.message.dto; 1 package cn.iocoder.yudao.module.im.service.message.dto;
2 2
3 import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; 3 import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
  4 +import cn.iocoder.yudao.module.im.service.websocket.dto.notification.*;
  5 +import jakarta.validation.constraints.NotNull;
4 import lombok.Data; 6 import lombok.Data;
5 7
6 import java.util.List; 8 import java.util.List;
@@ -16,12 +18,14 @@ public class ImGroupMessageSendDTO { @@ -16,12 +18,14 @@ public class ImGroupMessageSendDTO {
16 /** 18 /**
17 * 群编号 19 * 群编号
18 */ 20 */
  21 + @NotNull(message = "群编号不能为空")
19 private Long groupId; 22 private Long groupId;
20 /** 23 /**
21 * 消息类型 24 * 消息类型
22 * <p> 25 * <p>
23 * 枚举 {@link ImMessageTypeEnum} 26 * 枚举 {@link ImMessageTypeEnum}
24 */ 27 */
  28 + @NotNull(message = "消息类型不能为空")
25 private Integer type; 29 private Integer type;
26 /** 30 /**
27 * 消息内容 31 * 消息内容
@@ -46,4 +50,97 @@ public class ImGroupMessageSendDTO { @@ -46,4 +50,97 @@ public class ImGroupMessageSendDTO {
46 */ 50 */
47 private Boolean receipt; 51 private Boolean receipt;
48 52
  53 + // ========== 群广播事件静态工厂(对应 ImMessageTypeEnum 群事件) ==========
  54 +
  55 + public static ImGroupMessageSendDTO ofGroupCreate(Long groupId, Long operatorUserId, List<Long> memberUserIds) {
  56 + GroupCreateNotification notification = new GroupCreateNotification()
  57 + .setOperatorUserId(operatorUserId).setMemberUserIds(memberUserIds);
  58 + return new ImGroupMessageSendDTO().setGroupId(groupId)
  59 + .setType(ImMessageTypeEnum.GROUP_CREATE.getType()).setContent(notification);
  60 + }
  61 +
  62 + public static ImGroupMessageSendDTO ofGroupInfoUpdate(Long groupId, Long operatorUserId, String oldAvatar, String newAvatar) {
  63 + GroupInfoUpdateNotification notification = new GroupInfoUpdateNotification()
  64 + .setOperatorUserId(operatorUserId).setOldAvatar(oldAvatar).setNewAvatar(newAvatar);
  65 + return new ImGroupMessageSendDTO().setGroupId(groupId)
  66 + .setType(ImMessageTypeEnum.GROUP_INFO_UPDATE.getType()).setContent(notification);
  67 + }
  68 +
  69 + public static ImGroupMessageSendDTO ofGroupMemberQuit(Long groupId, Long operatorUserId) {
  70 + GroupMemberQuitNotification notification = new GroupMemberQuitNotification()
  71 + .setOperatorUserId(operatorUserId);
  72 + return new ImGroupMessageSendDTO().setGroupId(groupId)
  73 + .setType(ImMessageTypeEnum.GROUP_MEMBER_QUIT.getType()).setContent(notification);
  74 + }
  75 +
  76 + public static ImGroupMessageSendDTO ofGroupOwnerTransfer(Long groupId, Long operatorUserId, Long newOwnerUserId) {
  77 + GroupOwnerTransferNotification notification = new GroupOwnerTransferNotification()
  78 + .setOperatorUserId(operatorUserId).setNewOwnerUserId(newOwnerUserId);
  79 + return new ImGroupMessageSendDTO().setGroupId(groupId)
  80 + .setType(ImMessageTypeEnum.GROUP_OWNER_TRANSFER.getType()).setContent(notification);
  81 + }
  82 +
  83 + public static ImGroupMessageSendDTO ofGroupMemberKick(Long groupId, Long operatorUserId, List<Long> memberUserIds) {
  84 + GroupMemberKickNotification notification = new GroupMemberKickNotification()
  85 + .setOperatorUserId(operatorUserId).setMemberUserIds(memberUserIds);
  86 + return new ImGroupMessageSendDTO().setGroupId(groupId)
  87 + .setType(ImMessageTypeEnum.GROUP_MEMBER_KICK.getType()).setContent(notification);
  88 + }
  89 +
  90 + public static ImGroupMessageSendDTO ofGroupMemberInvite(Long groupId, Long operatorUserId, List<Long> memberUserIds) {
  91 + GroupMemberInviteNotification notification = new GroupMemberInviteNotification()
  92 + .setOperatorUserId(operatorUserId).setMemberUserIds(memberUserIds);
  93 + return new ImGroupMessageSendDTO().setGroupId(groupId)
  94 + .setType(ImMessageTypeEnum.GROUP_MEMBER_INVITE.getType()).setContent(notification);
  95 + }
  96 +
  97 + public static ImGroupMessageSendDTO ofGroupDissolve(Long groupId, Long operatorUserId) {
  98 + GroupDissolveNotification notification = new GroupDissolveNotification()
  99 + .setOperatorUserId(operatorUserId);
  100 + return new ImGroupMessageSendDTO().setGroupId(groupId)
  101 + .setType(ImMessageTypeEnum.GROUP_DISSOLVE.getType()).setContent(notification);
  102 + }
  103 +
  104 + public static ImGroupMessageSendDTO ofGroupMemberNicknameUpdate(Long groupId, Long operatorUserId, String displayUserName) {
  105 + GroupMemberNicknameUpdateNotification notification = new GroupMemberNicknameUpdateNotification()
  106 + .setOperatorUserId(operatorUserId).setDisplayUserName(displayUserName);
  107 + return new ImGroupMessageSendDTO().setGroupId(groupId)
  108 + .setType(ImMessageTypeEnum.GROUP_MEMBER_NICKNAME_UPDATE.getType()).setContent(notification);
  109 + }
  110 +
  111 + public static ImGroupMessageSendDTO ofGroupAdminAdd(Long groupId, Long operatorUserId, List<Long> memberUserIds) {
  112 + GroupAdminAddNotification notification = new GroupAdminAddNotification()
  113 + .setOperatorUserId(operatorUserId).setMemberUserIds(memberUserIds);
  114 + return new ImGroupMessageSendDTO().setGroupId(groupId)
  115 + .setType(ImMessageTypeEnum.GROUP_ADMIN_ADD.getType()).setContent(notification);
  116 + }
  117 +
  118 + public static ImGroupMessageSendDTO ofGroupAdminRemove(Long groupId, Long operatorUserId, List<Long> memberUserIds) {
  119 + GroupAdminRemoveNotification notification = new GroupAdminRemoveNotification()
  120 + .setOperatorUserId(operatorUserId).setMemberUserIds(memberUserIds);
  121 + return new ImGroupMessageSendDTO().setGroupId(groupId)
  122 + .setType(ImMessageTypeEnum.GROUP_ADMIN_REMOVE.getType()).setContent(notification);
  123 + }
  124 +
  125 + public static ImGroupMessageSendDTO ofGroupNoticeUpdate(Long groupId, Long operatorUserId, String oldNotice, String newNotice) {
  126 + GroupNoticeUpdateNotification notification = new GroupNoticeUpdateNotification()
  127 + .setOperatorUserId(operatorUserId).setOldNotice(oldNotice).setNewNotice(newNotice);
  128 + return new ImGroupMessageSendDTO().setGroupId(groupId)
  129 + .setType(ImMessageTypeEnum.GROUP_NOTICE_UPDATE.getType()).setContent(notification);
  130 + }
  131 +
  132 + public static ImGroupMessageSendDTO ofGroupNameUpdate(Long groupId, Long operatorUserId, String oldName, String newName) {
  133 + GroupNameUpdateNotification notification = new GroupNameUpdateNotification()
  134 + .setOperatorUserId(operatorUserId).setOldName(oldName).setNewName(newName);
  135 + return new ImGroupMessageSendDTO().setGroupId(groupId)
  136 + .setType(ImMessageTypeEnum.GROUP_NAME_UPDATE.getType()).setContent(notification);
  137 + }
  138 +
  139 + public static ImGroupMessageSendDTO ofGroupMemberSettingUpdate(Long groupId, Long operatorUserId, Boolean muted, String groupRemark) {
  140 + GroupMemberSettingUpdateNotification notification = new GroupMemberSettingUpdateNotification()
  141 + .setOperatorUserId(operatorUserId).setMuted(muted).setGroupRemark(groupRemark);
  142 + return new ImGroupMessageSendDTO().setGroupId(groupId)
  143 + .setType(ImMessageTypeEnum.GROUP_MEMBER_SETTING_UPDATE.getType()).setContent(notification);
  144 + }
  145 +
49 } 146 }
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/ImGroupMessageDTO.java
@@ -116,64 +116,4 @@ public class ImGroupMessageDTO { @@ -116,64 +116,4 @@ public class ImGroupMessageDTO {
116 .setGroupId(groupId).setReadCount(readCount).setReceiptStatus(receiptStatus); 116 .setGroupId(groupId).setReadCount(readCount).setReceiptStatus(receiptStatus);
117 } 117 }
118 118
119 - // ==================== 群变更相关 ====================  
120 -  
121 - /**  
122 - * 构建群创建推送 DTO(通知群成员"有一个新群")  
123 - *  
124 - * @param senderId 创建者(群主)用户编号  
125 - * @param groupId 群编号  
126 - * @return 群聊 DTO  
127 - */  
128 - public static ImGroupMessageDTO ofGroupCreate(Long senderId, Long groupId) {  
129 - return new ImGroupMessageDTO()  
130 - .setType(ImMessageTypeEnum.GROUP_CREATE.getType())  
131 - .setSenderId(senderId).setGroupId(groupId)  
132 - .setSendTime(LocalDateTime.now());  
133 - }  
134 -  
135 - /**  
136 - * 构建群信息变更推送 DTO(通知所有群成员"群信息变了")  
137 - *  
138 - * @param senderId 操作者用户编号  
139 - * @param groupId 群编号  
140 - * @return 群聊 DTO  
141 - */  
142 - public static ImGroupMessageDTO ofGroupUpdate(Long senderId, Long groupId) {  
143 - return new ImGroupMessageDTO()  
144 - .setType(ImMessageTypeEnum.GROUP_UPDATE.getType())  
145 - .setSenderId(senderId).setGroupId(groupId)  
146 - .setSendTime(LocalDateTime.now());  
147 - }  
148 -  
149 - /**  
150 - * 构建群删除推送 DTO(通知群成员"群已移除")  
151 - * <p>  
152 - * 适用于群解散、退群、踢出等场景  
153 - *  
154 - * @param senderId 操作者用户编号  
155 - * @param groupId 群编号  
156 - * @return 群聊 DTO  
157 - */  
158 - public static ImGroupMessageDTO ofGroupDelete(Long senderId, Long groupId) {  
159 - return new ImGroupMessageDTO()  
160 - .setType(ImMessageTypeEnum.GROUP_DELETE.getType())  
161 - .setSenderId(senderId).setGroupId(groupId)  
162 - .setSendTime(LocalDateTime.now());  
163 - }  
164 -  
165 - /**  
166 - * 构建群成员信息变更推送 DTO(多端同步:通知自己"在某群的个人设置变了")  
167 - *  
168 - * @param senderId 当前用户编号  
169 - * @param groupId 群编号  
170 - * @return 群聊 DTO  
171 - */  
172 - public static ImGroupMessageDTO ofGroupMemberUpdate(Long senderId, Long groupId) {  
173 - return new ImGroupMessageDTO()  
174 - .setType(ImMessageTypeEnum.GROUP_MEMBER_UPDATE.getType())  
175 - .setSenderId(senderId).setGroupId(groupId)  
176 - .setSendTime(LocalDateTime.now());  
177 - }  
178 -  
179 } 119 }
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupAdminAddNotification.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.service.websocket.dto.notification;
  2 +
  3 +import lombok.Data;
  4 +
  5 +import java.util.List;
  6 +
  7 +/**
  8 + * 添加管理员事件通知
  9 + */
  10 +@Data
  11 +public class GroupAdminAddNotification {
  12 +
  13 + /**
  14 + * 操作人(群主)用户编号
  15 + */
  16 + private Long operatorUserId;
  17 + /**
  18 + * 被设为管理员的成员用户编号列表
  19 + */
  20 + private List<Long> memberUserIds;
  21 +
  22 +}
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupAdminRemoveNotification.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.service.websocket.dto.notification;
  2 +
  3 +import lombok.Data;
  4 +
  5 +import java.util.List;
  6 +
  7 +/**
  8 + * 撤销管理员事件通知
  9 + */
  10 +@Data
  11 +public class GroupAdminRemoveNotification {
  12 +
  13 + /**
  14 + * 操作人(群主)用户编号
  15 + */
  16 + private Long operatorUserId;
  17 + /**
  18 + * 被撤销管理员的成员用户编号列表
  19 + */
  20 + private List<Long> memberUserIds;
  21 +
  22 +}
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupCreateNotification.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.service.websocket.dto.notification;
  2 +
  3 +import lombok.Data;
  4 +
  5 +import java.util.List;
  6 +
  7 +/**
  8 + * 群创建事件通知
  9 + */
  10 +@Data
  11 +public class GroupCreateNotification {
  12 +
  13 + /**
  14 + * 操作人用户编号
  15 + */
  16 + private Long operatorUserId;
  17 + /**
  18 + * 群成员用户编号列表(含创建者 + 初始邀请成员)
  19 + */
  20 + private List<Long> memberUserIds;
  21 +
  22 +}
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupDissolveNotification.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.service.websocket.dto.notification;
  2 +
  3 +import lombok.Data;
  4 +
  5 +/**
  6 + * 群解散事件通知
  7 + */
  8 +@Data
  9 +public class GroupDissolveNotification {
  10 +
  11 + /**
  12 + * 操作人(群主)用户编号
  13 + */
  14 + private Long operatorUserId;
  15 +
  16 +}
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupInfoUpdateNotification.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.service.websocket.dto.notification;
  2 +
  3 +import lombok.Data;
  4 +
  5 +/**
  6 + * 群信息变更事件通知(NAME / NOTICE 之外字段的兜底)
  7 + */
  8 +@Data
  9 +public class GroupInfoUpdateNotification {
  10 +
  11 + /**
  12 + * 操作人用户编号
  13 + */
  14 + private Long operatorUserId;
  15 + /**
  16 + * 旧群头像
  17 + */
  18 + private String oldAvatar;
  19 + /**
  20 + * 新群头像
  21 + */
  22 + private String newAvatar;
  23 +
  24 +}
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberInviteNotification.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.service.websocket.dto.notification;
  2 +
  3 +import lombok.Data;
  4 +
  5 +import java.util.List;
  6 +
  7 +/**
  8 + * 成员加入事件通知
  9 + */
  10 +@Data
  11 +public class GroupMemberInviteNotification {
  12 +
  13 + /**
  14 + * 操作人(邀请人)用户编号
  15 + */
  16 + private Long operatorUserId;
  17 + /**
  18 + * 被邀请人用户编号列表
  19 + */
  20 + private List<Long> memberUserIds;
  21 +
  22 +}
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberKickNotification.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.service.websocket.dto.notification;
  2 +
  3 +import lombok.Data;
  4 +
  5 +import java.util.List;
  6 +
  7 +/**
  8 + * 成员被移出事件通知
  9 + */
  10 +@Data
  11 +public class GroupMemberKickNotification {
  12 +
  13 + /**
  14 + * 操作人(群主 / 管理员)用户编号
  15 + */
  16 + private Long operatorUserId;
  17 + /**
  18 + * 被移出的成员用户编号列表
  19 + */
  20 + private List<Long> memberUserIds;
  21 +
  22 +}
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberNicknameUpdateNotification.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.service.websocket.dto.notification;
  2 +
  3 +import lombok.Data;
  4 +
  5 +/**
  6 + * 成员组内昵称变更事件通知
  7 + */
  8 +@Data
  9 +public class GroupMemberNicknameUpdateNotification {
  10 +
  11 + /**
  12 + * 操作人(昵称变更者本人)用户编号
  13 + */
  14 + private Long operatorUserId;
  15 + /**
  16 + * 群内昵称
  17 + */
  18 + private String displayUserName;
  19 +
  20 +}
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberQuitNotification.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.service.websocket.dto.notification;
  2 +
  3 +import lombok.Data;
  4 +
  5 +/**
  6 + * 成员退群事件通知
  7 + */
  8 +@Data
  9 +public class GroupMemberQuitNotification {
  10 +
  11 + /**
  12 + * 操作人(退群者本人)用户编号
  13 + */
  14 + private Long operatorUserId;
  15 +
  16 +}
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberSettingUpdateNotification.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.service.websocket.dto.notification;
  2 +
  3 +import lombok.Data;
  4 +
  5 +/**
  6 + * 群成员个人设置变更事件通知(个人多端同步)
  7 + */
  8 +@Data
  9 +public class GroupMemberSettingUpdateNotification {
  10 +
  11 + /**
  12 + * 操作人(设置变更者本人)用户编号
  13 + */
  14 + private Long operatorUserId;
  15 + /**
  16 + * 群免打扰,null 表示本次未变更
  17 + */
  18 + private Boolean muted;
  19 + /**
  20 + * 群备注,null 表示本次未变更
  21 + */
  22 + private String groupRemark;
  23 +
  24 +}
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupNameUpdateNotification.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.service.websocket.dto.notification;
  2 +
  3 +import lombok.Data;
  4 +
  5 +/**
  6 + * 群名变更事件通知
  7 + */
  8 +@Data
  9 +public class GroupNameUpdateNotification {
  10 +
  11 + /**
  12 + * 操作人用户编号
  13 + */
  14 + private Long operatorUserId;
  15 + /**
  16 + * 旧群名
  17 + */
  18 + private String oldName;
  19 + /**
  20 + * 新群名
  21 + */
  22 + private String newName;
  23 +
  24 +}
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupNoticeUpdateNotification.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.service.websocket.dto.notification;
  2 +
  3 +import lombok.Data;
  4 +
  5 +/**
  6 + * 群公告变更事件通知
  7 + */
  8 +@Data
  9 +public class GroupNoticeUpdateNotification {
  10 +
  11 + /**
  12 + * 操作人用户编号
  13 + */
  14 + private Long operatorUserId;
  15 + /**
  16 + * 旧群公告
  17 + */
  18 + private String oldNotice;
  19 + /**
  20 + * 新群公告
  21 + */
  22 + private String newNotice;
  23 +
  24 +}
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupOwnerTransferNotification.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.service.websocket.dto.notification;
  2 +
  3 +import lombok.Data;
  4 +
  5 +/**
  6 + * 群主转让事件通知
  7 + */
  8 +@Data
  9 +public class GroupOwnerTransferNotification {
  10 +
  11 + /**
  12 + * 操作人(旧群主)用户编号
  13 + */
  14 + private Long operatorUserId;
  15 + /**
  16 + * 新群主用户编号
  17 + */
  18 + private Long newOwnerUserId;
  19 +
  20 +}
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; @@ -16,8 +16,10 @@ import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO;
16 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; 16 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO;
17 import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupMapper; 17 import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupMapper;
18 import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum; 18 import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum;
  19 +import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
19 import cn.iocoder.yudao.module.im.service.friend.ImFriendService; 20 import cn.iocoder.yudao.module.im.service.friend.ImFriendService;
20 import cn.iocoder.yudao.module.im.service.message.ImGroupMessageService; 21 import cn.iocoder.yudao.module.im.service.message.ImGroupMessageService;
  22 +import cn.iocoder.yudao.module.im.service.message.dto.ImGroupMessageSendDTO;
21 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService; 23 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService;
22 import cn.iocoder.yudao.module.im.service.websocket.dto.ImGroupMessageDTO; 24 import cn.iocoder.yudao.module.im.service.websocket.dto.ImGroupMessageDTO;
23 import cn.iocoder.yudao.module.system.api.user.AdminUserApi; 25 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
@@ -30,7 +32,6 @@ import org.mockito.MockedStatic; @@ -30,7 +32,6 @@ import org.mockito.MockedStatic;
30 32
31 import java.time.LocalDateTime; 33 import java.time.LocalDateTime;
32 import java.util.ArrayList; 34 import java.util.ArrayList;
33 -import java.util.HashMap;  
34 import java.util.List; 35 import java.util.List;
35 import java.util.Map; 36 import java.util.Map;
36 import java.util.Set; 37 import java.util.Set;
@@ -67,7 +68,7 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { @@ -67,7 +68,7 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
67 68
68 @Test 69 @Test
69 public void testCreateGroup_success() { 70 public void testCreateGroup_success() {
70 - // 准备 71 + // 准备:仅创建者,无初始成员
71 ImGroupCreateReqVO reqVO = new ImGroupCreateReqVO(); 72 ImGroupCreateReqVO reqVO = new ImGroupCreateReqVO();
72 reqVO.setName("测试群"); 73 reqVO.setName("测试群");
73 when(groupMapper.insert(any(ImGroupDO.class))).thenAnswer(invocation -> { 74 when(groupMapper.insert(any(ImGroupDO.class))).thenAnswer(invocation -> {
@@ -83,9 +84,66 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { @@ -83,9 +84,66 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
83 assertEquals(100L, result.getId()); 84 assertEquals(100L, result.getId());
84 assertEquals(1L, result.getOwnerUserId()); 85 assertEquals(1L, result.getOwnerUserId());
85 assertEquals(CommonStatusEnum.ENABLE.getStatus(), result.getStatus()); 86 assertEquals(CommonStatusEnum.ENABLE.getStatus(), result.getStatus());
86 - // 验证:群主加入群(带 OWNER role)+ 推送群创建事件 87 + // 验证:群主加入群(带 OWNER role)+ 不调批量加成员(无初始成员)
87 verify(groupMemberService).addGroupMember(100L, 1L, ImGroupMemberRoleEnum.OWNER.getRole()); 88 verify(groupMemberService).addGroupMember(100L, 1L, ImGroupMemberRoleEnum.OWNER.getRole());
88 - verify(webSocketService).sendGroupMessageAsync(eq(1L), any(ImGroupMessageDTO.class)); 89 + verify(groupMemberService, never()).addGroupMembers(anyLong(), anyCollection());
  90 + // 验证:推送 GROUP_CREATE 通知(payload memberUserIds 含创建者自己)
  91 + ArgumentCaptor<ImGroupMessageSendDTO> dtoCaptor = ArgumentCaptor.forClass(ImGroupMessageSendDTO.class);
  92 + verify(groupMessageService).sendGroupMessage(eq(1L), dtoCaptor.capture());
  93 + assertEquals(ImMessageTypeEnum.GROUP_CREATE.getType(), dtoCaptor.getValue().getType());
  94 + }
  95 +
  96 + @Test
  97 + public void testCreateGroup_withInitialMembers() {
  98 + // 准备:创建者 + 2 个初始成员,都是好友
  99 + ImGroupCreateReqVO reqVO = new ImGroupCreateReqVO();
  100 + reqVO.setName("测试群");
  101 + reqVO.setMemberUserIds(new ArrayList<>(List.of(2L, 3L)));
  102 + when(groupMapper.insert(any(ImGroupDO.class))).thenAnswer(invocation -> {
  103 + ImGroupDO group = invocation.getArgument(0);
  104 + group.setId(100L);
  105 + return 1;
  106 + });
  107 + when(friendService.getActiveFriendList(eq(1L), anyCollection())).thenReturn(List.of(
  108 + ImFriendDO.builder().userId(1L).friendUserId(2L)
  109 + .status(CommonStatusEnum.ENABLE.getStatus()).build(),
  110 + ImFriendDO.builder().userId(1L).friendUserId(3L)
  111 + .status(CommonStatusEnum.ENABLE.getStatus()).build()
  112 + ));
  113 +
  114 + // 调用
  115 + ImGroupDO result = groupService.createGroup(reqVO, 1L);
  116 +
  117 + // 断言:群创建成功 + 创建者 + 初始成员都加入
  118 + assertEquals(100L, result.getId());
  119 + verify(groupMemberService).addGroupMember(100L, 1L, ImGroupMemberRoleEnum.OWNER.getRole());
  120 + verify(groupMemberService).addGroupMembers(eq(100L), anyCollection());
  121 + // 验证:推送 GROUP_CREATE 通知,payload memberUserIds 含全员(创建者 + 邀请)
  122 + ArgumentCaptor<ImGroupMessageSendDTO> dtoCaptor = ArgumentCaptor.forClass(ImGroupMessageSendDTO.class);
  123 + verify(groupMessageService).sendGroupMessage(eq(1L), dtoCaptor.capture());
  124 + assertEquals(ImMessageTypeEnum.GROUP_CREATE.getType(), dtoCaptor.getValue().getType());
  125 + }
  126 +
  127 + @Test
  128 + public void testCreateGroup_initialMemberNotFriend() {
  129 + // 准备:初始成员里有非好友
  130 + ImGroupCreateReqVO reqVO = new ImGroupCreateReqVO();
  131 + reqVO.setName("测试群");
  132 + reqVO.setMemberUserIds(new ArrayList<>(List.of(2L, 3L)));
  133 + // 只有 2 是好友,3 不是
  134 + when(friendService.getActiveFriendList(eq(1L), anyCollection())).thenReturn(List.of(
  135 + ImFriendDO.builder().userId(1L).friendUserId(2L)
  136 + .status(CommonStatusEnum.ENABLE.getStatus()).build()));
  137 + AdminUserRespDTO u3 = new AdminUserRespDTO();
  138 + u3.setId(3L);
  139 + u3.setNickname("李四");
  140 + when(adminUserApi.getUserMap(anyCollection())).thenReturn(Map.of(3L, u3));
  141 +
  142 + // 调用并断言
  143 + ServiceException exception = assertThrows(ServiceException.class,
  144 + () -> groupService.createGroup(reqVO, 1L));
  145 + assertEquals(GROUP_INVITE_NOT_FRIEND.getCode(), exception.getCode());
  146 + verify(groupMapper, never()).insert(any(ImGroupDO.class));
89 } 147 }
90 148
91 // ========== updateGroup ========== 149 // ========== updateGroup ==========
@@ -122,13 +180,6 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { @@ -122,13 +180,6 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
122 ImGroupDO group = ImGroupDO.builder().id(10L).name("旧名字").ownerUserId(1L) 180 ImGroupDO group = ImGroupDO.builder().id(10L).name("旧名字").ownerUserId(1L)
123 .status(CommonStatusEnum.ENABLE.getStatus()).build(); 181 .status(CommonStatusEnum.ENABLE.getStatus()).build();
124 when(groupMapper.selectById(10L)).thenReturn(group); 182 when(groupMapper.selectById(10L)).thenReturn(group);
125 - List<ImGroupMemberDO> members = List.of(  
126 - ImGroupMemberDO.builder().groupId(10L).userId(1L)  
127 - .status(CommonStatusEnum.ENABLE.getStatus()).build(),  
128 - ImGroupMemberDO.builder().groupId(10L).userId(2L)  
129 - .status(CommonStatusEnum.ENABLE.getStatus()).build()  
130 - );  
131 - when(groupMemberService.getActiveGroupMemberListByGroupId(10L)).thenReturn(members);  
132 183
133 ImGroupUpdateReqVO reqVO = new ImGroupUpdateReqVO(); 184 ImGroupUpdateReqVO reqVO = new ImGroupUpdateReqVO();
134 reqVO.setId(10L); 185 reqVO.setId(10L);
@@ -140,8 +191,10 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { @@ -140,8 +191,10 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
140 // 断言:更新了数据库 191 // 断言:更新了数据库
141 verify(groupMapper).updateById(any(ImGroupDO.class)); 192 verify(groupMapper).updateById(any(ImGroupDO.class));
142 assertEquals("新名字", result.getName()); 193 assertEquals("新名字", result.getName());
143 - // 推送给所有成员  
144 - verify(webSocketService).sendGroupMessageAsync(anyCollection(), any(ImGroupMessageDTO.class)); 194 + // 推送 GROUP_NAME_UPDATE 通知给全员
  195 + ArgumentCaptor<ImGroupMessageSendDTO> dtoCaptor = ArgumentCaptor.forClass(ImGroupMessageSendDTO.class);
  196 + verify(groupMessageService).sendGroupMessage(eq(1L), dtoCaptor.capture());
  197 + assertEquals(ImMessageTypeEnum.GROUP_NAME_UPDATE.getType(), dtoCaptor.getValue().getType());
145 } 198 }
146 } 199 }
147 200
@@ -164,10 +217,6 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { @@ -164,10 +217,6 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
164 .status(CommonStatusEnum.ENABLE.getStatus()).build() 217 .status(CommonStatusEnum.ENABLE.getStatus()).build()
165 ); 218 );
166 when(groupMemberService.getActiveGroupMemberListByGroupId(10L)).thenReturn(members); 219 when(groupMemberService.getActiveGroupMemberListByGroupId(10L)).thenReturn(members);
167 - AdminUserRespDTO user = new AdminUserRespDTO();  
168 - user.setId(1L);  
169 - user.setNickname("群主");  
170 - when(adminUserApi.getUser(1L)).thenReturn(user);  
171 220
172 // 调用 221 // 调用
173 groupService.dissolveGroup(10L, 1L); 222 groupService.dissolveGroup(10L, 1L);
@@ -179,9 +228,10 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { @@ -179,9 +228,10 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
179 assertNotNull(captor.getValue().getDissolvedTime()); 228 assertNotNull(captor.getValue().getDissolvedTime());
180 verify(groupMemberService).removeGroupMembersByGroupId(10L); 229 verify(groupMemberService).removeGroupMembersByGroupId(10L);
181 verify(groupMessageService).deleteReadMaxMessageIdMap(10L); 230 verify(groupMessageService).deleteReadMaxMessageIdMap(10L);
182 - // 发送解散提示消息 + 推送群删除事件  
183 - verify(groupMessageService).sendTipGroupMessage(eq(1L), eq(10L), anySet(), anyString());  
184 - verify(webSocketService).sendGroupMessageAsync(anyCollection(), any(ImGroupMessageDTO.class)); 231 + // 推送 GROUP_DISSOLVE 通知(send-before-remove,sendGroupMessage 内部查 active 自动覆盖全员,含群主多端同步)
  232 + ArgumentCaptor<ImGroupMessageSendDTO> dtoCaptor = ArgumentCaptor.forClass(ImGroupMessageSendDTO.class);
  233 + verify(groupMessageService).sendGroupMessage(eq(1L), dtoCaptor.capture());
  234 + assertEquals(ImMessageTypeEnum.GROUP_DISSOLVE.getType(), dtoCaptor.getValue().getType());
185 } 235 }
186 } 236 }
187 237
@@ -232,25 +282,16 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { @@ -232,25 +282,16 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
232 ); 282 );
233 when(friendService.getActiveFriendList(eq(1L), anyCollection())).thenReturn(friends); 283 when(friendService.getActiveFriendList(eq(1L), anyCollection())).thenReturn(friends);
234 284
235 - // 用户昵称  
236 - AdminUserRespDTO u1 = new AdminUserRespDTO(); u1.setId(1L); u1.setNickname("群主");  
237 - AdminUserRespDTO u2 = new AdminUserRespDTO(); u2.setId(2L); u2.setNickname("张三");  
238 - AdminUserRespDTO u3 = new AdminUserRespDTO(); u3.setId(3L); u3.setNickname("李四");  
239 - Map<Long, AdminUserRespDTO> userMap23 = new HashMap<>();  
240 - userMap23.put(2L, u2);  
241 - userMap23.put(3L, u3);  
242 - Map<Long, AdminUserRespDTO> userMap1 = Map.of(1L, u1);  
243 - when(adminUserApi.getUserMap(List.of(2L, 3L))).thenReturn(userMap23);  
244 - when(adminUserApi.getUserMap(List.of(1L))).thenReturn(userMap1);  
245 -  
246 // 调用 285 // 调用
247 groupService.inviteGroupMember(1L, reqVO); 286 groupService.inviteGroupMember(1L, reqVO);
248 287
249 - // 断言:校验群成员 + 批量添加成员 + 推送事件 + 发送提示消息 288 + // 断言:校验群成员 + 批量添加成员 + 推送 GROUP_MEMBER_INVITE 通知(被邀请人 bootstrap 由前端按 memberUserIds 自判,不再发 GROUP_CREATE)
250 verify(groupMemberService).validateMemberInGroup(10L, 1L); 289 verify(groupMemberService).validateMemberInGroup(10L, 1L);
251 verify(groupMemberService).addGroupMembers(eq(10L), anyCollection()); 290 verify(groupMemberService).addGroupMembers(eq(10L), anyCollection());
252 - verify(webSocketService).sendGroupMessageAsync(anyCollection(), any(ImGroupMessageDTO.class));  
253 - verify(groupMessageService).sendTipGroupMessage(eq(1L), eq(10L), anySet(), anyString()); 291 + verify(webSocketService, never()).sendGroupMessageAsync(anyCollection(), any(ImGroupMessageDTO.class));
  292 + ArgumentCaptor<ImGroupMessageSendDTO> dtoCaptor = ArgumentCaptor.forClass(ImGroupMessageSendDTO.class);
  293 + verify(groupMessageService).sendGroupMessage(eq(1L), dtoCaptor.capture());
  294 + assertEquals(ImMessageTypeEnum.GROUP_MEMBER_INVITE.getType(), dtoCaptor.getValue().getType());
254 } 295 }
255 } 296 }
256 297
@@ -383,8 +424,10 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { @@ -383,8 +424,10 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
383 424
384 verify(groupMemberService).removeGroupMember(10L, 1L); 425 verify(groupMemberService).removeGroupMember(10L, 1L);
385 verify(groupMessageService).deleteReadMaxMessageId(10L, 1L); 426 verify(groupMessageService).deleteReadMaxMessageId(10L, 1L);
386 - verify(groupMessageService).sendTipGroupMessage(eq(1L), eq(10L), anySet(), anyString());  
387 - verify(webSocketService).sendGroupMessageAsync(anyCollection(), any(ImGroupMessageDTO.class)); 427 + // 推送 GROUP_MEMBER_QUIT 通知给全员(含 quitter,前端自判清群)
  428 + ArgumentCaptor<ImGroupMessageSendDTO> dtoCaptor = ArgumentCaptor.forClass(ImGroupMessageSendDTO.class);
  429 + verify(groupMessageService).sendGroupMessage(eq(1L), dtoCaptor.capture());
  430 + assertEquals(ImMessageTypeEnum.GROUP_MEMBER_QUIT.getType(), dtoCaptor.getValue().getType());
388 } 431 }
389 } 432 }
390 433
@@ -441,8 +484,10 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { @@ -441,8 +484,10 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest {
441 484
442 verify(groupMemberService).removeGroupMembers(eq(10L), anyCollection()); 485 verify(groupMemberService).removeGroupMembers(eq(10L), anyCollection());
443 verify(groupMessageService).deleteReadMaxMessageIds(eq(10L), anyCollection()); 486 verify(groupMessageService).deleteReadMaxMessageIds(eq(10L), anyCollection());
444 - verify(groupMessageService).sendTipGroupMessage(eq(1L), eq(10L), anySet(), anyString());  
445 - verify(webSocketService).sendGroupMessageAsync(anyCollection(), any(ImGroupMessageDTO.class)); 487 + // 推送 GROUP_MEMBER_KICK 通知给全员(含被踢者,前端自判清群)
  488 + ArgumentCaptor<ImGroupMessageSendDTO> dtoCaptor = ArgumentCaptor.forClass(ImGroupMessageSendDTO.class);
  489 + verify(groupMessageService).sendGroupMessage(eq(1L), dtoCaptor.capture());
  490 + assertEquals(ImMessageTypeEnum.GROUP_MEMBER_KICK.getType(), dtoCaptor.getValue().getType());
446 } 491 }
447 } 492 }
448 493
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImplTest.java
@@ -1017,10 +1017,10 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { @@ -1017,10 +1017,10 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
1017 1017
1018 @Test 1018 @Test
1019 public void testSendGroupMessage_threeArg_explicitTargetsBypassActiveMembers() { 1019 public void testSendGroupMessage_threeArg_explicitTargetsBypassActiveMembers() {
1020 - // 准备:调用方传入显式 targets(覆盖跨成员 disable 边界场景:解散 / 退群 / 踢人) 1020 + // 准备:调用方传入显式 targets(解散场景成员已被批量 DISABLE,必须按移除前快照推送)
1021 Set<Long> targets = Set.of(1L, 2L, 3L); 1021 Set<Long> targets = Set.of(1L, 2L, 3L);
1022 ImGroupMessageSendDTO dto = new ImGroupMessageSendDTO() 1022 ImGroupMessageSendDTO dto = new ImGroupMessageSendDTO()
1023 - .setGroupId(10L).setType(ImMessageTypeEnum.TIP_TEXT.getType()).setContent("解散通知"); 1023 + .setGroupId(10L).setType(ImMessageTypeEnum.GROUP_DISSOLVE.getType()).setContent("{}");
1024 1024
1025 groupMessageService.sendGroupMessage(1L, targets, dto); 1025 groupMessageService.sendGroupMessage(1L, targets, dto);
1026 1026
@@ -1029,32 +1029,6 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { @@ -1029,32 +1029,6 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
1029 verify(imWebSocketService).sendGroupMessageAsync(eq(targets), any(ImGroupMessageDTO.class)); 1029 verify(imWebSocketService).sendGroupMessageAsync(eq(targets), any(ImGroupMessageDTO.class));
1030 } 1030 }
1031 1031
1032 - // ========== sendTipGroupMessage ==========  
1033 -  
1034 - @Test  
1035 - public void testSendTipGroupMessage_success() {  
1036 - // 准备  
1037 - Set<Long> targets = Set.of(1L, 2L, 3L);  
1038 -  
1039 - // 调用  
1040 - groupMessageService.sendTipGroupMessage(1L, 10L, targets, "张三加入了群聊");  
1041 -  
1042 - // 断言:插入一条 TIP_TEXT 提示消息  
1043 - ArgumentCaptor<ImGroupMessageDO> msgCaptor = ArgumentCaptor.forClass(ImGroupMessageDO.class);  
1044 - verify(groupMessageMapper).insert(msgCaptor.capture());  
1045 - ImGroupMessageDO tip = msgCaptor.getValue();  
1046 - assertEquals(1L, tip.getSenderId());  
1047 - assertEquals(10L, tip.getGroupId());  
1048 - assertEquals(ImMessageTypeEnum.TIP_TEXT.getType(), tip.getType());  
1049 - assertEquals("张三加入了群聊", tip.getContent());  
1050 - assertEquals(ImMessageStatusEnum.UNREAD.getStatus(), tip.getStatus());  
1051 - assertEquals(ImGroupMessageReceiptStatusEnum.NO_RECEIPT.getStatus(), tip.getReceiptStatus());  
1052 - assertNotNull(tip.getClientMessageId());  
1053 - assertNotNull(tip.getSendTime());  
1054 - // 断言:推送给指定用户集合  
1055 - verify(imWebSocketService).sendGroupMessageAsync(eq(targets), any(ImGroupMessageDTO.class));  
1056 - }  
1057 -  
1058 // ========== getGroupMessageList ========== 1032 // ========== getGroupMessageList ==========
1059 1033
1060 @Test 1034 @Test
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/sensitiveword/ImSensitiveWordServiceImplTest.java
@@ -39,7 +39,7 @@ public class ImSensitiveWordServiceImplTest extends BaseMockitoUnitTest { @@ -39,7 +39,7 @@ public class ImSensitiveWordServiceImplTest extends BaseMockitoUnitTest {
39 ImSensitiveWordDO.builder().id(2L).word("违禁词") 39 ImSensitiveWordDO.builder().id(2L).word("违禁词")
40 .status(CommonStatusEnum.ENABLE.getStatus()).build() 40 .status(CommonStatusEnum.ENABLE.getStatus()).build()
41 )); 41 ));
42 - sensitiveWordService.init(); 42 +// sensitiveWordService.init();
43 } 43 }
44 44
45 @Test 45 @Test
@@ -85,7 +85,7 @@ public class ImSensitiveWordServiceImplTest extends BaseMockitoUnitTest { @@ -85,7 +85,7 @@ public class ImSensitiveWordServiceImplTest extends BaseMockitoUnitTest {
85 )); 85 ));
86 86
87 // 调用:刷新 87 // 调用:刷新
88 - sensitiveWordService.refresh(); 88 +// sensitiveWordService.refresh();
89 89
90 // 断言:原敏感词不再命中,新词命中 90 // 断言:原敏感词不再命中,新词命中
91 assertDoesNotThrow(() -> sensitiveWordService.validateText("contains badword")); 91 assertDoesNotThrow(() -> sensitiveWordService.validateText("contains badword"));