Commit ed9f23ae8c5575b572e3c5b0c4bd0ecfe1128660

Authored by YunaiV
1 parent 719b139c

✨ feat(im): 重构群通知相关,对齐 openim 的消息编号(继续优化代码)

Showing 23 changed files with 362 additions and 222 deletions
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java
@@ -372,10 +372,13 @@ public class CollectionUtils { @@ -372,10 +372,13 @@ 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) { 375 + /**
  376 + * 把单元素 head 与集合 tail 合并成新 List(head 在前,tail 顺序保留)
  377 + */
  378 + public static <T> List<T> of(T head, Collection<T> tail) {
376 List<T> list = new ArrayList<>(); 379 List<T> list = new ArrayList<>();
377 - list.add(t);  
378 - CollUtil.addAll(list, ts); 380 + list.add(head);
  381 + CollUtil.addAll(list, tail);
379 return list; 382 return list;
380 } 383 }
381 384
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/message/ImMessageTypeEnum.java
@@ -39,7 +39,7 @@ public enum ImMessageTypeEnum implements ArrayValuable&lt;Integer&gt; { @@ -39,7 +39,7 @@ public enum ImMessageTypeEnum implements ArrayValuable&lt;Integer&gt; {
39 FRIEND_UPDATE(102, "好友更新", false, false), // 暂无对应类(客户端收到后自行拉取) 39 FRIEND_UPDATE(102, "好友更新", false, false), // 暂无对应类(客户端收到后自行拉取)
40 40
41 // ========== 群事件(1501-1520 直接复用 OpenIM 段位编号;1530+ 我们独有扩展) ========== 41 // ========== 群事件(1501-1520 直接复用 OpenIM 段位编号;1530+ 我们独有扩展) ==========
42 - // 1500 mirror OpenIM GroupNotificationBegin marker,不使用 42 + // 1500 对应 OpenIM GroupNotificationBegin 起始位,仅作占位,不使用
43 /** 43 /**
44 * 对应 OpenIM:sdkws.GroupCreatedTips(GroupCreatedNotification 1501) 44 * 对应 OpenIM:sdkws.GroupCreatedTips(GroupCreatedNotification 1501)
45 * 对应自己的类:GroupCreateNotification 45 * 对应自己的类:GroupCreateNotification
@@ -54,7 +54,7 @@ public enum ImMessageTypeEnum implements ArrayValuable&lt;Integer&gt; { @@ -54,7 +54,7 @@ public enum ImMessageTypeEnum implements ArrayValuable&lt;Integer&gt; {
54 GROUP_INFO_UPDATE(1502, "群信息变更", true, false), 54 GROUP_INFO_UPDATE(1502, "群信息变更", true, false),
55 /** 55 /**
56 * 对应 OpenIM:sdkws.JoinGroupApplicationTips(JoinGroupApplicationNotification 1503) 56 * 对应 OpenIM:sdkws.JoinGroupApplicationTips(JoinGroupApplicationNotification 1503)
57 - * TODO @AI 未实现:入群申请;本期 §1.3 Tier 3 不在范围,预留段位以便未来对齐 OpenIM 57 + * TODO @AI 未实现:入群申请;本期不在范围,预留段位以便未来对齐 OpenIM
58 */ 58 */
59 GROUP_JOIN_APPLICATION(1503, "入群申请", true, false), 59 GROUP_JOIN_APPLICATION(1503, "入群申请", true, false),
60 /** 60 /**
@@ -65,12 +65,12 @@ public enum ImMessageTypeEnum implements ArrayValuable&lt;Integer&gt; { @@ -65,12 +65,12 @@ public enum ImMessageTypeEnum implements ArrayValuable&lt;Integer&gt; {
65 GROUP_MEMBER_QUIT(1504, "成员退群", true, false), 65 GROUP_MEMBER_QUIT(1504, "成员退群", true, false),
66 /** 66 /**
67 * 对应 OpenIM:sdkws.GroupApplicationAcceptedTips(GroupApplicationAcceptedNotification 1505) 67 * 对应 OpenIM:sdkws.GroupApplicationAcceptedTips(GroupApplicationAcceptedNotification 1505)
68 - * TODO @AI 未实现:入群申请通过;本期 §1.3 Tier 3 不在范围 68 + * TODO @AI 未实现:入群申请通过;本期不在范围
69 */ 69 */
70 GROUP_APPLICATION_ACCEPTED(1505, "入群申请通过", true, false), 70 GROUP_APPLICATION_ACCEPTED(1505, "入群申请通过", true, false),
71 /** 71 /**
72 * 对应 OpenIM:sdkws.GroupApplicationRejectedTips(GroupApplicationRejectedNotification 1506) 72 * 对应 OpenIM:sdkws.GroupApplicationRejectedTips(GroupApplicationRejectedNotification 1506)
73 - * TODO @AI 未实现:入群申请拒绝;本期 §1.3 Tier 3 不在范围 73 + * TODO @AI 未实现:入群申请拒绝;本期不在范围
74 */ 74 */
75 GROUP_APPLICATION_REJECTED(1506, "入群申请拒绝", true, false), 75 GROUP_APPLICATION_REJECTED(1506, "入群申请拒绝", true, false),
76 /** 76 /**
@@ -93,7 +93,7 @@ public enum ImMessageTypeEnum implements ArrayValuable&lt;Integer&gt; { @@ -93,7 +93,7 @@ public enum ImMessageTypeEnum implements ArrayValuable&lt;Integer&gt; {
93 GROUP_MEMBER_INVITE(1509, "成员加入", true, false), 93 GROUP_MEMBER_INVITE(1509, "成员加入", true, false),
94 /** 94 /**
95 * 对应 OpenIM:sdkws.MemberEnterTips(MemberEnterNotification 1510) 95 * 对应 OpenIM:sdkws.MemberEnterTips(MemberEnterNotification 1510)
96 - * TODO @AI 未实现:自由进群(链接 / 二维码进群);本期 §1.3 Tier 3 不在范围 96 + * TODO @AI 未实现:自由进群(链接 / 二维码进群);本期不在范围
97 */ 97 */
98 GROUP_MEMBER_ENTER(1510, "自由进群", true, false), 98 GROUP_MEMBER_ENTER(1510, "自由进群", true, false),
99 /** 99 /**
@@ -104,22 +104,22 @@ public enum ImMessageTypeEnum implements ArrayValuable&lt;Integer&gt; { @@ -104,22 +104,22 @@ public enum ImMessageTypeEnum implements ArrayValuable&lt;Integer&gt; {
104 GROUP_DISSOLVE(1511, "群解散", true, false), 104 GROUP_DISSOLVE(1511, "群解散", true, false),
105 /** 105 /**
106 * 对应 OpenIM:sdkws.GroupMemberMutedTips(GroupMemberMutedNotification 1512) 106 * 对应 OpenIM:sdkws.GroupMemberMutedTips(GroupMemberMutedNotification 1512)
107 - * TODO @AI 未实现:单成员禁言(管理员禁言某成员);本期 §1.3 Tier 3 不在范围【这个本期发版,是要实现的】 107 + * TODO @AI 未实现:单成员禁言(管理员禁言某成员);本期发版要实现
108 */ 108 */
109 GROUP_MEMBER_MUTED(1512, "成员禁言", true, false), 109 GROUP_MEMBER_MUTED(1512, "成员禁言", true, false),
110 /** 110 /**
111 * 对应 OpenIM:sdkws.GroupMemberCancelMutedTips(GroupMemberCancelMutedNotification 1513) 111 * 对应 OpenIM:sdkws.GroupMemberCancelMutedTips(GroupMemberCancelMutedNotification 1513)
112 - * TODO @AI 未实现:单成员取消禁言;本期 §1.3 Tier 3 不在范围【这个本期发版,是要实现的】 112 + * TODO @AI 未实现:单成员取消禁言;本期发版要实现
113 */ 113 */
114 GROUP_MEMBER_CANCEL_MUTED(1513, "成员取消禁言", true, false), 114 GROUP_MEMBER_CANCEL_MUTED(1513, "成员取消禁言", true, false),
115 /** 115 /**
116 * 对应 OpenIM:sdkws.GroupMutedTips(GroupMutedNotification 1514) 116 * 对应 OpenIM:sdkws.GroupMutedTips(GroupMutedNotification 1514)
117 - * TODO @AI 未实现:全群禁言(管理员把整个群设为禁言状态,所有成员都不能发消息);本期 §1.3 Tier 3 不在范围【这个本期发版,是要实现的】 117 + * TODO @AI 未实现:全群禁言(管理员把整个群设为禁言状态,所有成员都不能发消息);本期发版要实现
118 */ 118 */
119 GROUP_MUTED(1514, "全群禁言", true, false), 119 GROUP_MUTED(1514, "全群禁言", true, false),
120 /** 120 /**
121 * 对应 OpenIM:sdkws.GroupCancelMutedTips(GroupCancelMutedNotification 1515) 121 * 对应 OpenIM:sdkws.GroupCancelMutedTips(GroupCancelMutedNotification 1515)
122 - * TODO @AI 未实现:全群取消禁言;本期 §1.3 Tier 3 不在范围【这个本期发版,是要实现的】 122 + * TODO @AI 未实现:全群取消禁言;本期发版要实现
123 */ 123 */
124 GROUP_CANCEL_MUTED(1515, "全群取消禁言", true, false), 124 GROUP_CANCEL_MUTED(1515, "全群取消禁言", true, false),
125 /** 125 /**
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.hutool.core.util.ObjUtil;
5 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; 5 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
6 import cn.iocoder.yudao.framework.common.util.object.BeanUtils; 6 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
7 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;
@@ -23,7 +23,8 @@ import java.time.LocalDateTime; @@ -23,7 +23,8 @@ import java.time.LocalDateTime;
23 import java.util.*; 23 import java.util.*;
24 24
25 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; 25 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
26 -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; 26 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
  27 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
27 import static cn.iocoder.yudao.module.im.dal.redis.RedisKeyConstants.GROUP_MEMBER_IDS; 28 import static cn.iocoder.yudao.module.im.dal.redis.RedisKeyConstants.GROUP_MEMBER_IDS;
28 import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.GROUP_MEMBER_NOT_IN_GROUP; 29 import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.GROUP_MEMBER_NOT_IN_GROUP;
29 30
@@ -215,13 +216,18 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { @@ -215,13 +216,18 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService {
215 .setId(member.getId()); 216 .setId(member.getId());
216 groupMemberMapper.updateById(updateObj); 217 groupMemberMapper.updateById(updateObj);
217 218
218 - // 3.1 displayUserName 是公开字段,单独走 GROUP_MEMBER_NICKNAME_UPDATE 广播给全员  
219 - if (StrUtil.isNotEmpty(updateReqVO.getDisplayUserName())) { 219 + // 3.1 displayUserName 是公开字段,单独走 GROUP_MEMBER_NICKNAME_UPDATE 广播给全员;空串视为「清空昵称」也要广播;与旧值相同跳过
  220 + if (updateReqVO.getDisplayUserName() != null
  221 + && ObjUtil.notEqual(updateReqVO.getDisplayUserName(), member.getDisplayUserName())) {
220 groupMessageService.sendGroupMessage(userId, ImGroupMessageSendDTO.ofGroupMemberNicknameUpdate( 222 groupMessageService.sendGroupMessage(userId, ImGroupMessageSendDTO.ofGroupMemberNicknameUpdate(
221 groupId, userId, updateReqVO.getDisplayUserName())); 223 groupId, userId, updateReqVO.getDisplayUserName()));
222 } 224 }
223 - // 3.2 muted / groupRemark 是个人字段,target 显式指定为自己仅推自己多端同步;payload 携带变更字段,前端按非 null 局部更新  
224 - if (updateReqVO.getMuted() != null || updateReqVO.getGroupRemark() != null) { 225 + // 3.2 muted / groupRemark 是个人字段,仅推自己做多端同步;与旧值都相同跳过
  226 + boolean mutedChanged = updateReqVO.getMuted() != null
  227 + && ObjUtil.notEqual(updateReqVO.getMuted(), member.getMuted());
  228 + boolean groupRemarkChanged = updateReqVO.getGroupRemark() != null
  229 + && ObjUtil.notEqual(updateReqVO.getGroupRemark(), member.getGroupRemark());
  230 + if (mutedChanged || groupRemarkChanged) {
225 groupMessageService.sendGroupMessage(userId, List.of(userId), ImGroupMessageSendDTO.ofGroupMemberSettingUpdate( 231 groupMessageService.sendGroupMessage(userId, List.of(userId), ImGroupMessageSendDTO.ofGroupMemberSettingUpdate(
226 groupId, userId, updateReqVO.getMuted(), updateReqVO.getGroupRemark())); 232 groupId, userId, updateReqVO.getMuted(), updateReqVO.getGroupRemark()));
227 } 233 }
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupService.java
@@ -18,7 +18,7 @@ import java.util.List; @@ -18,7 +18,7 @@ import java.util.List;
18 import java.util.Map; 18 import java.util.Map;
19 19
20 /** 20 /**
21 - * 群 Service 接口 21 + * 用户群群 Service 接口
22 * 22 *
23 * @author 芋道源码 23 * @author 芋道源码
24 */ 24 */
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImpl.java
@@ -10,7 +10,11 @@ import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; @@ -10,7 +10,11 @@ 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.collection.CollectionUtils;
12 import cn.iocoder.yudao.framework.common.util.object.BeanUtils; 12 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
13 -import cn.iocoder.yudao.module.im.controller.admin.group.vo.*; 13 +import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupAdminAddReqVO;
  14 +import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupAdminRemoveReqVO;
  15 +import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupCreateReqVO;
  16 +import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupTransferOwnerReqVO;
  17 +import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupUpdateReqVO;
14 import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberInviteReqVO; 18 import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberInviteReqVO;
15 import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberRemoveReqVO; 19 import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberRemoveReqVO;
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.ImGroupManagerBanReqVO;
@@ -45,7 +49,7 @@ import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*; @@ -45,7 +49,7 @@ import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*;
45 import static cn.iocoder.yudao.module.im.enums.ImCommonConstants.*; 49 import static cn.iocoder.yudao.module.im.enums.ImCommonConstants.*;
46 50
47 /** 51 /**
48 - * 群 Service 实现类 52 + * 用户群 Service 实现类
49 * 53 *
50 * @author 芋道源码 54 * @author 芋道源码
51 */ 55 */
@@ -125,19 +129,26 @@ public class ImGroupServiceImpl implements ImGroupService { @@ -125,19 +129,26 @@ public class ImGroupServiceImpl implements ImGroupService {
125 ImGroupDO newGroup = BeanUtils.toBean(updateReqVO, ImGroupDO.class); 129 ImGroupDO newGroup = BeanUtils.toBean(updateReqVO, ImGroupDO.class);
126 groupMapper.updateById(newGroup); 130 groupMapper.updateById(newGroup);
127 131
128 - // 3. 按变更字段分别推送 GROUP_NAME / NOTICE / INFO_UPDATE 通知 132 + // 3. 按变更字段分别推送 GROUP_NAME / NOTICE / INFO_UPDATE 通知;活跃成员只查一次复用,避免 3 次 Redis GET
  133 + // name / avatar 不允许空串(业务上必须非空),notice 允许空串(清空公告也是有效操作)
129 Long groupId = group.getId(); 134 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())); 135 + boolean nameChanged = StrUtil.isNotEmpty(updateReqVO.getName());
  136 + boolean noticeChanged = updateReqVO.getNotice() != null;
  137 + boolean avatarChanged = StrUtil.isNotEmpty(updateReqVO.getAvatar());
  138 + if (nameChanged || noticeChanged || avatarChanged) {
  139 + List<Long> memberUserIds = groupMemberService.getActiveGroupMemberUserIdsByGroupId(groupId);
  140 + if (nameChanged) {
  141 + groupMessageService.sendGroupMessage(userId, memberUserIds, ImGroupMessageSendDTO.ofGroupNameUpdate(
  142 + groupId, userId, group.getName(), updateReqVO.getName()));
  143 + }
  144 + if (noticeChanged) {
  145 + groupMessageService.sendGroupMessage(userId, memberUserIds, ImGroupMessageSendDTO.ofGroupNoticeUpdate(
  146 + groupId, userId, group.getNotice(), updateReqVO.getNotice()));
  147 + }
  148 + if (avatarChanged) {
  149 + groupMessageService.sendGroupMessage(userId, memberUserIds, ImGroupMessageSendDTO.ofGroupInfoUpdate(
  150 + groupId, userId, group.getAvatar(), updateReqVO.getAvatar()));
  151 + }
141 } 152 }
142 153
143 // 4. 返回合并后的新群信息(updateReqVO 非空字段覆盖 group) 154 // 4. 返回合并后的新群信息(updateReqVO 非空字段覆盖 group)
@@ -194,8 +205,10 @@ public class ImGroupServiceImpl implements ImGroupService { @@ -194,8 +205,10 @@ public class ImGroupServiceImpl implements ImGroupService {
194 // 2. 批量添加群成员 205 // 2. 批量添加群成员
195 groupMemberService.addGroupMembers(groupId, memberUserIds); 206 groupMemberService.addGroupMembers(groupId, memberUserIds);
196 207
197 - // 3. 发 GROUP_MEMBER_INVITE 通知给全员;新成员已 ENABLE,sendGroupMessage 查到的 active 自然包含  
198 - groupMessageService.sendGroupMessage(userId, 208 + // 3. 发 GROUP_MEMBER_INVITE 通知给全员;本地拼 receivers(已查的 active + 新邀请)避免缓存刚 evict 后强制走 DB
  209 + Set<Long> allReceivers = new HashSet<>(memberUserIds);
  210 + activeMembers.forEach(member -> allReceivers.add(member.getUserId()));
  211 + groupMessageService.sendGroupMessage(userId, allReceivers,
199 ImGroupMessageSendDTO.ofGroupMemberInvite(groupId, userId, memberUserIds)); 212 ImGroupMessageSendDTO.ofGroupMemberInvite(groupId, userId, memberUserIds));
200 } 213 }
201 214
@@ -208,7 +221,7 @@ public class ImGroupServiceImpl implements ImGroupService { @@ -208,7 +221,7 @@ public class ImGroupServiceImpl implements ImGroupService {
208 throw exception(GROUP_OWNER_CANNOT_QUIT); 221 throw exception(GROUP_OWNER_CANNOT_QUIT);
209 } 222 }
210 223
211 - // 3. 先发 GROUP_MEMBER_QUIT 通知:放在 quitter 移除前,sendGroupMessage 才能查到全员(含 quitter) 224 + // 3. 先发广播,后移成员(见类 javadoc)
212 groupMessageService.sendGroupMessage(userId, ImGroupMessageSendDTO.ofGroupMemberQuit(groupId, userId)); 225 groupMessageService.sendGroupMessage(userId, ImGroupMessageSendDTO.ofGroupMemberQuit(groupId, userId));
213 226
214 // 4.1 移除群成员 227 // 4.1 移除群成员
@@ -241,7 +254,7 @@ public class ImGroupServiceImpl implements ImGroupService { @@ -241,7 +254,7 @@ public class ImGroupServiceImpl implements ImGroupService {
241 254
242 // 2. 先发 GROUP_MEMBER_KICK 通知:放在被踢者移除前,sendGroupMessage 才能查到全员(含被踢者) 255 // 2. 先发 GROUP_MEMBER_KICK 通知:放在被踢者移除前,sendGroupMessage 才能查到全员(含被踢者)
243 groupMessageService.sendGroupMessage(userId, 256 groupMessageService.sendGroupMessage(userId,
244 - ImGroupMessageSendDTO.ofGroupMemberKick(groupId, userId, new ArrayList<>(targetUserIds))); 257 + ImGroupMessageSendDTO.ofGroupMemberKick(groupId, userId, targetUserIds));
245 258
246 // 3.1 批量移除群成员 259 // 3.1 批量移除群成员
247 groupMemberService.removeGroupMembers(groupId, targetUserIds); 260 groupMemberService.removeGroupMembers(groupId, targetUserIds);
@@ -279,7 +292,7 @@ public class ImGroupServiceImpl implements ImGroupService { @@ -279,7 +292,7 @@ public class ImGroupServiceImpl implements ImGroupService {
279 292
280 // 3. 推送 GROUP_ADMIN_ADD 通知给全员 293 // 3. 推送 GROUP_ADMIN_ADD 通知给全员
281 groupMessageService.sendGroupMessage(userId, 294 groupMessageService.sendGroupMessage(userId,
282 - ImGroupMessageSendDTO.ofGroupAdminAdd(groupId, userId, new ArrayList<>(changedUserIds))); 295 + ImGroupMessageSendDTO.ofGroupAdminAdd(groupId, userId, changedUserIds));
283 } 296 }
284 297
285 @Override 298 @Override
@@ -306,7 +319,7 @@ public class ImGroupServiceImpl implements ImGroupService { @@ -306,7 +319,7 @@ public class ImGroupServiceImpl implements ImGroupService {
306 319
307 // 3. 推送 GROUP_ADMIN_REMOVE 通知给全员 320 // 3. 推送 GROUP_ADMIN_REMOVE 通知给全员
308 groupMessageService.sendGroupMessage(userId, 321 groupMessageService.sendGroupMessage(userId,
309 - ImGroupMessageSendDTO.ofGroupAdminRemove(groupId, userId, new ArrayList<>(changedUserIds))); 322 + ImGroupMessageSendDTO.ofGroupAdminRemove(groupId, userId, changedUserIds));
310 } 323 }
311 324
312 /** 325 /**
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageService.java
@@ -30,7 +30,9 @@ public interface ImGroupMessageService { @@ -30,7 +30,9 @@ public interface ImGroupMessageService {
30 ImGroupMessageDO sendGroupMessage(Long senderId, ImGroupMessageSendReqVO reqVO); 30 ImGroupMessageDO sendGroupMessage(Long senderId, ImGroupMessageSendReqVO reqVO);
31 31
32 /** 32 /**
33 - * 【系统调用】发送群聊消息:自动算可见用户 + 推送 33 + * 【系统调用】发送群聊消息:内部查 active 成员 + 推送
  34 + * <p>
  35 + * 调用方批量调用会触发多次 active 成员查询;如果调用方已经持有成员快照,优先使用 {@link #sendGroupMessage(Long, Collection, ImGroupMessageSendDTO)} 方法,避免重复查询。
34 * 36 *
35 * @param senderId 发送人编号 37 * @param senderId 发送人编号
36 * @param dto 消息 DTO 38 * @param dto 消息 DTO
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/dto/ImGroupMessageSendDTO.java
@@ -5,6 +5,8 @@ import cn.iocoder.yudao.module.im.service.websocket.dto.notification.*; @@ -5,6 +5,8 @@ import cn.iocoder.yudao.module.im.service.websocket.dto.notification.*;
5 import jakarta.validation.constraints.NotNull; 5 import jakarta.validation.constraints.NotNull;
6 import lombok.Data; 6 import lombok.Data;
7 7
  8 +import java.util.ArrayList;
  9 +import java.util.Collection;
8 import java.util.List; 10 import java.util.List;
9 11
10 /** 12 /**
@@ -52,93 +54,108 @@ public class ImGroupMessageSendDTO { @@ -52,93 +54,108 @@ public class ImGroupMessageSendDTO {
52 54
53 // ========== 群广播事件静态工厂(对应 ImMessageTypeEnum 群事件) ========== 55 // ========== 群广播事件静态工厂(对应 ImMessageTypeEnum 群事件) ==========
54 56
55 - public static ImGroupMessageSendDTO ofGroupCreate(Long groupId, Long operatorUserId, List<Long> memberUserIds) {  
56 - GroupCreateNotification notification = new GroupCreateNotification()  
57 - .setOperatorUserId(operatorUserId).setMemberUserIds(memberUserIds); 57 + public static ImGroupMessageSendDTO ofGroupCreate(Long groupId, Long operatorUserId, Collection<Long> memberUserIds) {
  58 + GroupCreateNotification notification = new GroupCreateNotification();
  59 + notification.setOperatorUserId(operatorUserId);
  60 + notification.setMemberUserIds(new ArrayList<>(memberUserIds));
58 return new ImGroupMessageSendDTO().setGroupId(groupId) 61 return new ImGroupMessageSendDTO().setGroupId(groupId)
59 .setType(ImMessageTypeEnum.GROUP_CREATE.getType()).setContent(notification); 62 .setType(ImMessageTypeEnum.GROUP_CREATE.getType()).setContent(notification);
60 } 63 }
61 64
62 public static ImGroupMessageSendDTO ofGroupInfoUpdate(Long groupId, Long operatorUserId, String oldAvatar, String newAvatar) { 65 public static ImGroupMessageSendDTO ofGroupInfoUpdate(Long groupId, Long operatorUserId, String oldAvatar, String newAvatar) {
63 - GroupInfoUpdateNotification notification = new GroupInfoUpdateNotification()  
64 - .setOperatorUserId(operatorUserId).setOldAvatar(oldAvatar).setNewAvatar(newAvatar); 66 + GroupInfoUpdateNotification notification = new GroupInfoUpdateNotification();
  67 + notification.setOperatorUserId(operatorUserId);
  68 + notification.setOldAvatar(oldAvatar);
  69 + notification.setNewAvatar(newAvatar);
65 return new ImGroupMessageSendDTO().setGroupId(groupId) 70 return new ImGroupMessageSendDTO().setGroupId(groupId)
66 .setType(ImMessageTypeEnum.GROUP_INFO_UPDATE.getType()).setContent(notification); 71 .setType(ImMessageTypeEnum.GROUP_INFO_UPDATE.getType()).setContent(notification);
67 } 72 }
68 73
69 public static ImGroupMessageSendDTO ofGroupMemberQuit(Long groupId, Long operatorUserId) { 74 public static ImGroupMessageSendDTO ofGroupMemberQuit(Long groupId, Long operatorUserId) {
70 - GroupMemberQuitNotification notification = new GroupMemberQuitNotification()  
71 - .setOperatorUserId(operatorUserId); 75 + GroupMemberQuitNotification notification = new GroupMemberQuitNotification();
  76 + notification.setOperatorUserId(operatorUserId);
72 return new ImGroupMessageSendDTO().setGroupId(groupId) 77 return new ImGroupMessageSendDTO().setGroupId(groupId)
73 .setType(ImMessageTypeEnum.GROUP_MEMBER_QUIT.getType()).setContent(notification); 78 .setType(ImMessageTypeEnum.GROUP_MEMBER_QUIT.getType()).setContent(notification);
74 } 79 }
75 80
76 public static ImGroupMessageSendDTO ofGroupOwnerTransfer(Long groupId, Long operatorUserId, Long newOwnerUserId) { 81 public static ImGroupMessageSendDTO ofGroupOwnerTransfer(Long groupId, Long operatorUserId, Long newOwnerUserId) {
77 - GroupOwnerTransferNotification notification = new GroupOwnerTransferNotification()  
78 - .setOperatorUserId(operatorUserId).setNewOwnerUserId(newOwnerUserId); 82 + GroupOwnerTransferNotification notification = new GroupOwnerTransferNotification();
  83 + notification.setOperatorUserId(operatorUserId);
  84 + notification.setNewOwnerUserId(newOwnerUserId);
79 return new ImGroupMessageSendDTO().setGroupId(groupId) 85 return new ImGroupMessageSendDTO().setGroupId(groupId)
80 .setType(ImMessageTypeEnum.GROUP_OWNER_TRANSFER.getType()).setContent(notification); 86 .setType(ImMessageTypeEnum.GROUP_OWNER_TRANSFER.getType()).setContent(notification);
81 } 87 }
82 88
83 - public static ImGroupMessageSendDTO ofGroupMemberKick(Long groupId, Long operatorUserId, List<Long> memberUserIds) {  
84 - GroupMemberKickNotification notification = new GroupMemberKickNotification()  
85 - .setOperatorUserId(operatorUserId).setMemberUserIds(memberUserIds); 89 + public static ImGroupMessageSendDTO ofGroupMemberKick(Long groupId, Long operatorUserId, Collection<Long> memberUserIds) {
  90 + GroupMemberKickNotification notification = new GroupMemberKickNotification();
  91 + notification.setOperatorUserId(operatorUserId);
  92 + notification.setMemberUserIds(new ArrayList<>(memberUserIds));
86 return new ImGroupMessageSendDTO().setGroupId(groupId) 93 return new ImGroupMessageSendDTO().setGroupId(groupId)
87 .setType(ImMessageTypeEnum.GROUP_MEMBER_KICK.getType()).setContent(notification); 94 .setType(ImMessageTypeEnum.GROUP_MEMBER_KICK.getType()).setContent(notification);
88 } 95 }
89 96
90 - public static ImGroupMessageSendDTO ofGroupMemberInvite(Long groupId, Long operatorUserId, List<Long> memberUserIds) {  
91 - GroupMemberInviteNotification notification = new GroupMemberInviteNotification()  
92 - .setOperatorUserId(operatorUserId).setMemberUserIds(memberUserIds); 97 + public static ImGroupMessageSendDTO ofGroupMemberInvite(Long groupId, Long operatorUserId, Collection<Long> memberUserIds) {
  98 + GroupMemberInviteNotification notification = new GroupMemberInviteNotification();
  99 + notification.setOperatorUserId(operatorUserId);
  100 + notification.setMemberUserIds(new ArrayList<>(memberUserIds));
93 return new ImGroupMessageSendDTO().setGroupId(groupId) 101 return new ImGroupMessageSendDTO().setGroupId(groupId)
94 .setType(ImMessageTypeEnum.GROUP_MEMBER_INVITE.getType()).setContent(notification); 102 .setType(ImMessageTypeEnum.GROUP_MEMBER_INVITE.getType()).setContent(notification);
95 } 103 }
96 104
97 public static ImGroupMessageSendDTO ofGroupDissolve(Long groupId, Long operatorUserId) { 105 public static ImGroupMessageSendDTO ofGroupDissolve(Long groupId, Long operatorUserId) {
98 - GroupDissolveNotification notification = new GroupDissolveNotification()  
99 - .setOperatorUserId(operatorUserId); 106 + GroupDissolveNotification notification = new GroupDissolveNotification();
  107 + notification.setOperatorUserId(operatorUserId);
100 return new ImGroupMessageSendDTO().setGroupId(groupId) 108 return new ImGroupMessageSendDTO().setGroupId(groupId)
101 .setType(ImMessageTypeEnum.GROUP_DISSOLVE.getType()).setContent(notification); 109 .setType(ImMessageTypeEnum.GROUP_DISSOLVE.getType()).setContent(notification);
102 } 110 }
103 111
104 public static ImGroupMessageSendDTO ofGroupMemberNicknameUpdate(Long groupId, Long operatorUserId, String displayUserName) { 112 public static ImGroupMessageSendDTO ofGroupMemberNicknameUpdate(Long groupId, Long operatorUserId, String displayUserName) {
105 - GroupMemberNicknameUpdateNotification notification = new GroupMemberNicknameUpdateNotification()  
106 - .setOperatorUserId(operatorUserId).setDisplayUserName(displayUserName); 113 + GroupMemberNicknameUpdateNotification notification = new GroupMemberNicknameUpdateNotification();
  114 + notification.setOperatorUserId(operatorUserId);
  115 + notification.setDisplayUserName(displayUserName);
107 return new ImGroupMessageSendDTO().setGroupId(groupId) 116 return new ImGroupMessageSendDTO().setGroupId(groupId)
108 .setType(ImMessageTypeEnum.GROUP_MEMBER_NICKNAME_UPDATE.getType()).setContent(notification); 117 .setType(ImMessageTypeEnum.GROUP_MEMBER_NICKNAME_UPDATE.getType()).setContent(notification);
109 } 118 }
110 119
111 - public static ImGroupMessageSendDTO ofGroupAdminAdd(Long groupId, Long operatorUserId, List<Long> memberUserIds) {  
112 - GroupAdminAddNotification notification = new GroupAdminAddNotification()  
113 - .setOperatorUserId(operatorUserId).setMemberUserIds(memberUserIds); 120 + public static ImGroupMessageSendDTO ofGroupAdminAdd(Long groupId, Long operatorUserId, Collection<Long> memberUserIds) {
  121 + GroupAdminAddNotification notification = new GroupAdminAddNotification();
  122 + notification.setOperatorUserId(operatorUserId);
  123 + notification.setMemberUserIds(new ArrayList<>(memberUserIds));
114 return new ImGroupMessageSendDTO().setGroupId(groupId) 124 return new ImGroupMessageSendDTO().setGroupId(groupId)
115 .setType(ImMessageTypeEnum.GROUP_ADMIN_ADD.getType()).setContent(notification); 125 .setType(ImMessageTypeEnum.GROUP_ADMIN_ADD.getType()).setContent(notification);
116 } 126 }
117 127
118 - public static ImGroupMessageSendDTO ofGroupAdminRemove(Long groupId, Long operatorUserId, List<Long> memberUserIds) {  
119 - GroupAdminRemoveNotification notification = new GroupAdminRemoveNotification()  
120 - .setOperatorUserId(operatorUserId).setMemberUserIds(memberUserIds); 128 + public static ImGroupMessageSendDTO ofGroupAdminRemove(Long groupId, Long operatorUserId, Collection<Long> memberUserIds) {
  129 + GroupAdminRemoveNotification notification = new GroupAdminRemoveNotification();
  130 + notification.setOperatorUserId(operatorUserId);
  131 + notification.setMemberUserIds(new ArrayList<>(memberUserIds));
121 return new ImGroupMessageSendDTO().setGroupId(groupId) 132 return new ImGroupMessageSendDTO().setGroupId(groupId)
122 .setType(ImMessageTypeEnum.GROUP_ADMIN_REMOVE.getType()).setContent(notification); 133 .setType(ImMessageTypeEnum.GROUP_ADMIN_REMOVE.getType()).setContent(notification);
123 } 134 }
124 135
125 public static ImGroupMessageSendDTO ofGroupNoticeUpdate(Long groupId, Long operatorUserId, String oldNotice, String newNotice) { 136 public static ImGroupMessageSendDTO ofGroupNoticeUpdate(Long groupId, Long operatorUserId, String oldNotice, String newNotice) {
126 - GroupNoticeUpdateNotification notification = new GroupNoticeUpdateNotification()  
127 - .setOperatorUserId(operatorUserId).setOldNotice(oldNotice).setNewNotice(newNotice); 137 + GroupNoticeUpdateNotification notification = new GroupNoticeUpdateNotification();
  138 + notification.setOperatorUserId(operatorUserId);
  139 + notification.setOldNotice(oldNotice);
  140 + notification.setNewNotice(newNotice);
128 return new ImGroupMessageSendDTO().setGroupId(groupId) 141 return new ImGroupMessageSendDTO().setGroupId(groupId)
129 .setType(ImMessageTypeEnum.GROUP_NOTICE_UPDATE.getType()).setContent(notification); 142 .setType(ImMessageTypeEnum.GROUP_NOTICE_UPDATE.getType()).setContent(notification);
130 } 143 }
131 144
132 public static ImGroupMessageSendDTO ofGroupNameUpdate(Long groupId, Long operatorUserId, String oldName, String newName) { 145 public static ImGroupMessageSendDTO ofGroupNameUpdate(Long groupId, Long operatorUserId, String oldName, String newName) {
133 - GroupNameUpdateNotification notification = new GroupNameUpdateNotification()  
134 - .setOperatorUserId(operatorUserId).setOldName(oldName).setNewName(newName); 146 + GroupNameUpdateNotification notification = new GroupNameUpdateNotification();
  147 + notification.setOperatorUserId(operatorUserId);
  148 + notification.setOldName(oldName);
  149 + notification.setNewName(newName);
135 return new ImGroupMessageSendDTO().setGroupId(groupId) 150 return new ImGroupMessageSendDTO().setGroupId(groupId)
136 .setType(ImMessageTypeEnum.GROUP_NAME_UPDATE.getType()).setContent(notification); 151 .setType(ImMessageTypeEnum.GROUP_NAME_UPDATE.getType()).setContent(notification);
137 } 152 }
138 153
139 public static ImGroupMessageSendDTO ofGroupMemberSettingUpdate(Long groupId, Long operatorUserId, Boolean muted, String groupRemark) { 154 public static ImGroupMessageSendDTO ofGroupMemberSettingUpdate(Long groupId, Long operatorUserId, Boolean muted, String groupRemark) {
140 - GroupMemberSettingUpdateNotification notification = new GroupMemberSettingUpdateNotification()  
141 - .setOperatorUserId(operatorUserId).setMuted(muted).setGroupRemark(groupRemark); 155 + GroupMemberSettingUpdateNotification notification = new GroupMemberSettingUpdateNotification();
  156 + notification.setOperatorUserId(operatorUserId);
  157 + notification.setMuted(muted);
  158 + notification.setGroupRemark(groupRemark);
142 return new ImGroupMessageSendDTO().setGroupId(groupId) 159 return new ImGroupMessageSendDTO().setGroupId(groupId)
143 .setType(ImMessageTypeEnum.GROUP_MEMBER_SETTING_UPDATE.getType()).setContent(notification); 160 .setType(ImMessageTypeEnum.GROUP_MEMBER_SETTING_UPDATE.getType()).setContent(notification);
144 } 161 }
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/BaseGroupNotification.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.service.websocket.dto.notification;
  2 +
  3 +import lombok.Data;
  4 +
  5 +/**
  6 + * 群事件通知基类:所有群事件 payload 共享 operatorUserId
  7 + */
  8 +@Data
  9 +public abstract class BaseGroupNotification {
  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/GroupAdminAddNotification.java
1 package cn.iocoder.yudao.module.im.service.websocket.dto.notification; 1 package cn.iocoder.yudao.module.im.service.websocket.dto.notification;
2 2
3 -import lombok.Data;  
4 -  
5 -import java.util.List;  
6 -  
7 /** 3 /**
8 - * 添加管理员事件通知 4 + * 添加管理员事件通知(memberUserIds 为被设管理员的成员)
9 */ 5 */
10 -@Data  
11 -public class GroupAdminAddNotification {  
12 -  
13 - /**  
14 - * 操作人(群主)用户编号  
15 - */  
16 - private Long operatorUserId;  
17 - /**  
18 - * 被设为管理员的成员用户编号列表  
19 - */  
20 - private List<Long> memberUserIds;  
21 - 6 +public class GroupAdminAddNotification extends MembersAffectedNotification {
22 } 7 }
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupAdminRemoveNotification.java
1 package cn.iocoder.yudao.module.im.service.websocket.dto.notification; 1 package cn.iocoder.yudao.module.im.service.websocket.dto.notification;
2 2
3 -import lombok.Data;  
4 -  
5 -import java.util.List;  
6 -  
7 /** 3 /**
8 - * 撤销管理员事件通知 4 + * 撤销管理员事件通知(memberUserIds 为被撤销管理员的成员)
9 */ 5 */
10 -@Data  
11 -public class GroupAdminRemoveNotification {  
12 -  
13 - /**  
14 - * 操作人(群主)用户编号  
15 - */  
16 - private Long operatorUserId;  
17 - /**  
18 - * 被撤销管理员的成员用户编号列表  
19 - */  
20 - private List<Long> memberUserIds;  
21 - 6 +public class GroupAdminRemoveNotification extends MembersAffectedNotification {
22 } 7 }
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupCreateNotification.java
1 package cn.iocoder.yudao.module.im.service.websocket.dto.notification; 1 package cn.iocoder.yudao.module.im.service.websocket.dto.notification;
2 2
3 -import lombok.Data;  
4 -  
5 -import java.util.List;  
6 -  
7 /** 3 /**
8 - * 群创建事件通知 4 + * 群创建事件通知(memberUserIds 含创建者 + 初始邀请成员)
9 */ 5 */
10 -@Data  
11 -public class GroupCreateNotification {  
12 -  
13 - /**  
14 - * 操作人用户编号  
15 - */  
16 - private Long operatorUserId;  
17 - /**  
18 - * 群成员用户编号列表(含创建者 + 初始邀请成员)  
19 - */  
20 - private List<Long> memberUserIds;  
21 - 6 +public class GroupCreateNotification extends MembersAffectedNotification {
22 } 7 }
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupDissolveNotification.java
1 package cn.iocoder.yudao.module.im.service.websocket.dto.notification; 1 package cn.iocoder.yudao.module.im.service.websocket.dto.notification;
2 2
3 -import lombok.Data;  
4 -  
5 /** 3 /**
6 * 群解散事件通知 4 * 群解散事件通知
7 */ 5 */
8 -@Data  
9 -public class GroupDissolveNotification {  
10 -  
11 - /**  
12 - * 操作人(群主)用户编号  
13 - */  
14 - private Long operatorUserId;  
15 - 6 +public class GroupDissolveNotification extends BaseGroupNotification {
16 } 7 }
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupInfoUpdateNotification.java
@@ -3,16 +3,12 @@ package cn.iocoder.yudao.module.im.service.websocket.dto.notification; @@ -3,16 +3,12 @@ package cn.iocoder.yudao.module.im.service.websocket.dto.notification;
3 import lombok.Data; 3 import lombok.Data;
4 4
5 /** 5 /**
6 - * 群信息变更事件通知(NAME / NOTICE 之外字段的兜底) 6 + * 群信息变更事件通知(当前承载头像变更,NAME / NOTICE 走独立事件)
7 */ 7 */
8 @Data 8 @Data
9 -public class GroupInfoUpdateNotification { 9 +public class GroupInfoUpdateNotification extends BaseGroupNotification {
10 10
11 /** 11 /**
12 - * 操作人用户编号  
13 - */  
14 - private Long operatorUserId;  
15 - /**  
16 * 旧群头像 12 * 旧群头像
17 */ 13 */
18 private String oldAvatar; 14 private String oldAvatar;
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberInviteNotification.java
1 package cn.iocoder.yudao.module.im.service.websocket.dto.notification; 1 package cn.iocoder.yudao.module.im.service.websocket.dto.notification;
2 2
3 -import lombok.Data;  
4 -  
5 -import java.util.List;  
6 -  
7 /** 3 /**
8 - * 成员加入事件通知 4 + * 成员加入事件通知(memberUserIds 为被邀请人)
9 */ 5 */
10 -@Data  
11 -public class GroupMemberInviteNotification {  
12 -  
13 - /**  
14 - * 操作人(邀请人)用户编号  
15 - */  
16 - private Long operatorUserId;  
17 - /**  
18 - * 被邀请人用户编号列表  
19 - */  
20 - private List<Long> memberUserIds;  
21 - 6 +public class GroupMemberInviteNotification extends MembersAffectedNotification {
22 } 7 }
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberKickNotification.java
1 package cn.iocoder.yudao.module.im.service.websocket.dto.notification; 1 package cn.iocoder.yudao.module.im.service.websocket.dto.notification;
2 2
3 -import lombok.Data;  
4 -  
5 -import java.util.List;  
6 -  
7 /** 3 /**
8 - * 成员被移出事件通知 4 + * 成员被移出事件通知(memberUserIds 为被移出成员)
9 */ 5 */
10 -@Data  
11 -public class GroupMemberKickNotification {  
12 -  
13 - /**  
14 - * 操作人(群主 / 管理员)用户编号  
15 - */  
16 - private Long operatorUserId;  
17 - /**  
18 - * 被移出的成员用户编号列表  
19 - */  
20 - private List<Long> memberUserIds;  
21 - 6 +public class GroupMemberKickNotification extends MembersAffectedNotification {
22 } 7 }
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberNicknameUpdateNotification.java
@@ -6,13 +6,9 @@ import lombok.Data; @@ -6,13 +6,9 @@ import lombok.Data;
6 * 成员组内昵称变更事件通知 6 * 成员组内昵称变更事件通知
7 */ 7 */
8 @Data 8 @Data
9 -public class GroupMemberNicknameUpdateNotification { 9 +public class GroupMemberNicknameUpdateNotification extends BaseGroupNotification {
10 10
11 /** 11 /**
12 - * 操作人(昵称变更者本人)用户编号  
13 - */  
14 - private Long operatorUserId;  
15 - /**  
16 * 群内昵称 12 * 群内昵称
17 */ 13 */
18 private String displayUserName; 14 private String displayUserName;
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberQuitNotification.java
1 package cn.iocoder.yudao.module.im.service.websocket.dto.notification; 1 package cn.iocoder.yudao.module.im.service.websocket.dto.notification;
2 2
3 -import lombok.Data;  
4 -  
5 /** 3 /**
6 * 成员退群事件通知 4 * 成员退群事件通知
7 */ 5 */
8 -@Data  
9 -public class GroupMemberQuitNotification {  
10 -  
11 - /**  
12 - * 操作人(退群者本人)用户编号  
13 - */  
14 - private Long operatorUserId;  
15 - 6 +public class GroupMemberQuitNotification extends BaseGroupNotification {
16 } 7 }
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberSettingUpdateNotification.java
@@ -4,20 +4,18 @@ import lombok.Data; @@ -4,20 +4,18 @@ import lombok.Data;
4 4
5 /** 5 /**
6 * 群成员个人设置变更事件通知(个人多端同步) 6 * 群成员个人设置变更事件通知(个人多端同步)
  7 + * <p>
  8 + * muted / groupRemark 字段 null 表示本次未变更,前端按非 null 局部更新
7 */ 9 */
8 @Data 10 @Data
9 -public class GroupMemberSettingUpdateNotification { 11 +public class GroupMemberSettingUpdateNotification extends BaseGroupNotification {
10 12
11 /** 13 /**
12 - * 操作人(设置变更者本人)用户编号  
13 - */  
14 - private Long operatorUserId;  
15 - /**  
16 - * 群免打扰,null 表示本次未变更 14 + * 群免打扰
17 */ 15 */
18 private Boolean muted; 16 private Boolean muted;
19 /** 17 /**
20 - * 群备注,null 表示本次未变更 18 + * 群备注
21 */ 19 */
22 private String groupRemark; 20 private String groupRemark;
23 21
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupNameUpdateNotification.java
@@ -6,13 +6,9 @@ import lombok.Data; @@ -6,13 +6,9 @@ import lombok.Data;
6 * 群名变更事件通知 6 * 群名变更事件通知
7 */ 7 */
8 @Data 8 @Data
9 -public class GroupNameUpdateNotification { 9 +public class GroupNameUpdateNotification extends BaseGroupNotification {
10 10
11 /** 11 /**
12 - * 操作人用户编号  
13 - */  
14 - private Long operatorUserId;  
15 - /**  
16 * 旧群名 12 * 旧群名
17 */ 13 */
18 private String oldName; 14 private String oldName;
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupNoticeUpdateNotification.java
@@ -6,13 +6,9 @@ import lombok.Data; @@ -6,13 +6,9 @@ import lombok.Data;
6 * 群公告变更事件通知 6 * 群公告变更事件通知
7 */ 7 */
8 @Data 8 @Data
9 -public class GroupNoticeUpdateNotification { 9 +public class GroupNoticeUpdateNotification extends BaseGroupNotification {
10 10
11 /** 11 /**
12 - * 操作人用户编号  
13 - */  
14 - private Long operatorUserId;  
15 - /**  
16 * 旧群公告 12 * 旧群公告
17 */ 13 */
18 private String oldNotice; 14 private String oldNotice;
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupOwnerTransferNotification.java
@@ -6,13 +6,9 @@ import lombok.Data; @@ -6,13 +6,9 @@ import lombok.Data;
6 * 群主转让事件通知 6 * 群主转让事件通知
7 */ 7 */
8 @Data 8 @Data
9 -public class GroupOwnerTransferNotification { 9 +public class GroupOwnerTransferNotification extends BaseGroupNotification {
10 10
11 /** 11 /**
12 - * 操作人(旧群主)用户编号  
13 - */  
14 - private Long operatorUserId;  
15 - /**  
16 * 新群主用户编号 12 * 新群主用户编号
17 */ 13 */
18 private Long newOwnerUserId; 14 private Long newOwnerUserId;
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/MembersAffectedNotification.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 + * 涉及成员列表的群事件通知基类:用于 Create / Invite / Kick / AdminAdd / AdminRemove
  9 + */
  10 +@Data
  11 +public abstract class MembersAffectedNotification extends BaseGroupNotification {
  12 +
  13 + /**
  14 + * 受影响的成员用户编号列表
  15 + */
  16 + private List<Long> memberUserIds;
  17 +
  18 +}
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/sensitiveword/ImSensitiveWordServiceImplTest.java
@@ -2,17 +2,23 @@ package cn.iocoder.yudao.module.im.service.sensitiveword; @@ -2,17 +2,23 @@ package cn.iocoder.yudao.module.im.service.sensitiveword;
2 2
3 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; 3 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
4 import cn.iocoder.yudao.framework.common.exception.ServiceException; 4 import cn.iocoder.yudao.framework.common.exception.ServiceException;
  5 +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
5 import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; 6 import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
  7 +import cn.iocoder.yudao.module.im.controller.admin.manager.sensitiveword.vo.ImSensitiveWordSaveReqVO;
6 import cn.iocoder.yudao.module.im.dal.dataobject.sensitiveword.ImSensitiveWordDO; 8 import cn.iocoder.yudao.module.im.dal.dataobject.sensitiveword.ImSensitiveWordDO;
7 import cn.iocoder.yudao.module.im.dal.mysql.sensitiveword.ImSensitiveWordMapper; 9 import cn.iocoder.yudao.module.im.dal.mysql.sensitiveword.ImSensitiveWordMapper;
  10 +import org.junit.jupiter.api.AfterEach;
8 import org.junit.jupiter.api.BeforeEach; 11 import org.junit.jupiter.api.BeforeEach;
9 import org.junit.jupiter.api.Test; 12 import org.junit.jupiter.api.Test;
10 import org.mockito.InjectMocks; 13 import org.mockito.InjectMocks;
11 import org.mockito.Mock; 14 import org.mockito.Mock;
12 15
  16 +import java.time.LocalDateTime;
13 import java.util.List; 17 import java.util.List;
14 18
15 import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.MESSAGE_SENSITIVE_WORD_BLOCKED; 19 import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.MESSAGE_SENSITIVE_WORD_BLOCKED;
  20 +import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.SENSITIVE_WORD_DUPLICATED;
  21 +import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.SENSITIVE_WORD_NOT_EXISTS;
16 import static org.junit.jupiter.api.Assertions.*; 22 import static org.junit.jupiter.api.Assertions.*;
17 import static org.mockito.Mockito.*; 23 import static org.mockito.Mockito.*;
18 24
@@ -23,6 +29,8 @@ import static org.mockito.Mockito.*; @@ -23,6 +29,8 @@ import static org.mockito.Mockito.*;
23 */ 29 */
24 public class ImSensitiveWordServiceImplTest extends BaseMockitoUnitTest { 30 public class ImSensitiveWordServiceImplTest extends BaseMockitoUnitTest {
25 31
  32 + private static final Long TENANT_ID = 1L;
  33 +
26 @InjectMocks 34 @InjectMocks
27 private ImSensitiveWordServiceImpl sensitiveWordService; 35 private ImSensitiveWordServiceImpl sensitiveWordService;
28 36
@@ -31,15 +39,23 @@ public class ImSensitiveWordServiceImplTest extends BaseMockitoUnitTest { @@ -31,15 +39,23 @@ public class ImSensitiveWordServiceImplTest extends BaseMockitoUnitTest {
31 39
32 @BeforeEach 40 @BeforeEach
33 public void setUp() { 41 public void setUp() {
34 - // 模拟 @PostConstruct 初始化:加载 2 个启用的敏感词  
35 - when(imSensitiveWordMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus())) 42 + // 设置租户上下文;validateText 与 loadFresh 都依赖 TenantContextHolder
  43 + TenantContextHolder.setTenantId(TENANT_ID);
  44 + // mock 启用敏感词列表;LoadingCache 在首次 validateText 时懒加载,无需主动 init
  45 + // 用 lenient 避免 null / empty 等不触发 cache load 的用例报 UnnecessaryStubbing
  46 + lenient().when(imSensitiveWordMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus()))
36 .thenReturn(List.of( 47 .thenReturn(List.of(
37 ImSensitiveWordDO.builder().id(1L).word("badword") 48 ImSensitiveWordDO.builder().id(1L).word("badword")
38 .status(CommonStatusEnum.ENABLE.getStatus()).build(), 49 .status(CommonStatusEnum.ENABLE.getStatus()).build(),
39 ImSensitiveWordDO.builder().id(2L).word("违禁词") 50 ImSensitiveWordDO.builder().id(2L).word("违禁词")
40 .status(CommonStatusEnum.ENABLE.getStatus()).build() 51 .status(CommonStatusEnum.ENABLE.getStatus()).build()
41 )); 52 ));
42 -// sensitiveWordService.init(); 53 + }
  54 +
  55 + @AfterEach
  56 + public void tearDown() {
  57 + // 清理租户上下文,避免污染其它测试
  58 + TenantContextHolder.clear();
43 } 59 }
44 60
45 @Test 61 @Test
@@ -74,24 +90,188 @@ public class ImSensitiveWordServiceImplTest extends BaseMockitoUnitTest { @@ -74,24 +90,188 @@ public class ImSensitiveWordServiceImplTest extends BaseMockitoUnitTest {
74 } 90 }
75 91
76 @Test 92 @Test
77 - public void testRefresh_reloadsFromMapper() {  
78 - // 刷新前:init 调用 mapper 一次  
79 - verify(imSensitiveWordMapper).selectListByStatus(CommonStatusEnum.ENABLE.getStatus()); 93 + public void testValidateText_lazyLoadsCacheOnFirstCall() {
  94 + // 调用:首次 validateText 应触发 cache load
  95 + sensitiveWordService.validateText("hello world");
  96 +
  97 + // 断言:mapper 各调用 1 次(loadFresh 里先取 maxUpdateTime 再读词库)
  98 + verify(imSensitiveWordMapper, times(1)).selectMaxUpdateTime(TENANT_ID);
  99 + verify(imSensitiveWordMapper, times(1)).selectListByStatus(CommonStatusEnum.ENABLE.getStatus());
  100 + }
  101 +
  102 + @Test
  103 + public void testValidateText_reusesCachedBsAcrossCalls() {
  104 + // 调用:连续两次 validateText
  105 + sensitiveWordService.validateText("hello world");
  106 + sensitiveWordService.validateText("another text");
  107 +
  108 + // 断言:第二次复用 cache,mapper 仍只被调用 1 次
  109 + verify(imSensitiveWordMapper, times(1)).selectMaxUpdateTime(TENANT_ID);
  110 + verify(imSensitiveWordMapper, times(1)).selectListByStatus(CommonStatusEnum.ENABLE.getStatus());
  111 + }
  112 +
  113 + @Test
  114 + public void testCreateSensitiveWord_invalidatesCacheAndReloadsOnNextValidate() {
  115 + // 准备:首次 validateText 触发 cache load(旧词库)
  116 + sensitiveWordService.validateText("hello world");
  117 + verify(imSensitiveWordMapper, times(1)).selectListByStatus(CommonStatusEnum.ENABLE.getStatus());
  118 +
  119 + // 准备:mapper 返回新词库(额外多一个 newbad),并让 createSensitiveWord 走通
  120 + when(imSensitiveWordMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus()))
  121 + .thenReturn(List.of(
  122 + ImSensitiveWordDO.builder().id(1L).word("badword")
  123 + .status(CommonStatusEnum.ENABLE.getStatus()).build(),
  124 + ImSensitiveWordDO.builder().id(2L).word("违禁词")
  125 + .status(CommonStatusEnum.ENABLE.getStatus()).build(),
  126 + ImSensitiveWordDO.builder().id(3L).word("newbad")
  127 + .status(CommonStatusEnum.ENABLE.getStatus()).build()
  128 + ));
  129 + when(imSensitiveWordMapper.selectByWord("newbad")).thenReturn(null);
  130 +
  131 + // 调用:新增敏感词,触发 invalidate
  132 + ImSensitiveWordSaveReqVO reqVO = new ImSensitiveWordSaveReqVO();
  133 + reqVO.setWord("newbad");
  134 + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus());
  135 + sensitiveWordService.createSensitiveWord(reqVO);
  136 +
  137 + // 调用:再次 validateText,应触发重新 load
  138 + ServiceException exception = assertThrows(ServiceException.class,
  139 + () -> sensitiveWordService.validateText("contains newbad here"));
  140 + assertEquals(MESSAGE_SENSITIVE_WORD_BLOCKED.getCode(), exception.getCode());
  141 + // 旧词依然命中
  142 + assertThrows(ServiceException.class,
  143 + () -> sensitiveWordService.validateText("contains badword here"));
  144 +
  145 + // 断言:selectListByStatus 共被调用 2 次(首次 load + invalidate 后 reload)
  146 + verify(imSensitiveWordMapper, times(2)).selectListByStatus(CommonStatusEnum.ENABLE.getStatus());
  147 + }
  148 +
  149 + @Test
  150 + public void testCreateSensitiveWord_duplicateWord_throws() {
  151 + // 准备:mock 已存在同名敏感词
  152 + when(imSensitiveWordMapper.selectByWord("dup")).thenReturn(
  153 + ImSensitiveWordDO.builder().id(99L).word("dup")
  154 + .status(CommonStatusEnum.ENABLE.getStatus()).build());
  155 +
  156 + // 调用 + 断言:重复敏感词抛 SENSITIVE_WORD_DUPLICATED
  157 + ImSensitiveWordSaveReqVO reqVO = new ImSensitiveWordSaveReqVO();
  158 + reqVO.setWord("dup");
  159 + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus());
  160 + ServiceException exception = assertThrows(ServiceException.class,
  161 + () -> sensitiveWordService.createSensitiveWord(reqVO));
  162 + assertEquals(SENSITIVE_WORD_DUPLICATED.getCode(), exception.getCode());
  163 +
  164 + // 断言:未走到 insert
  165 + verify(imSensitiveWordMapper, never()).insert(any(ImSensitiveWordDO.class));
  166 + }
  167 +
  168 + @Test
  169 + public void testUpdateSensitiveWord_invalidatesCache() {
  170 + // 准备:首次 validateText 触发 cache load
  171 + sensitiveWordService.validateText("hello world");
  172 + verify(imSensitiveWordMapper, times(1)).selectListByStatus(CommonStatusEnum.ENABLE.getStatus());
  173 +
  174 + // 准备:让 update 校验通过
  175 + when(imSensitiveWordMapper.selectById(1L)).thenReturn(
  176 + ImSensitiveWordDO.builder().id(1L).word("badword")
  177 + .status(CommonStatusEnum.ENABLE.getStatus()).build());
  178 + when(imSensitiveWordMapper.selectByWord("updatedbad")).thenReturn(null);
  179 + // 准备:reload 时返回新词库
  180 + when(imSensitiveWordMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus()))
  181 + .thenReturn(List.of(
  182 + ImSensitiveWordDO.builder().id(1L).word("updatedbad")
  183 + .status(CommonStatusEnum.ENABLE.getStatus()).build()
  184 + ));
  185 +
  186 + // 调用:更新敏感词,触发 invalidate
  187 + ImSensitiveWordSaveReqVO reqVO = new ImSensitiveWordSaveReqVO();
  188 + reqVO.setId(1L);
  189 + reqVO.setWord("updatedbad");
  190 + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus());
  191 + sensitiveWordService.updateSensitiveWord(reqVO);
  192 +
  193 + // 调用 + 断言:再次 validateText 应使用新词库
  194 + ServiceException exception = assertThrows(ServiceException.class,
  195 + () -> sensitiveWordService.validateText("contains updatedbad here"));
  196 + assertEquals(MESSAGE_SENSITIVE_WORD_BLOCKED.getCode(), exception.getCode());
  197 +
  198 + // 断言:selectListByStatus 共被调用 2 次
  199 + verify(imSensitiveWordMapper, times(2)).selectListByStatus(CommonStatusEnum.ENABLE.getStatus());
  200 + }
  201 +
  202 + @Test
  203 + public void testDeleteSensitiveWord_invalidatesCache() {
  204 + // 准备:首次 validateText 触发 cache load
  205 + sensitiveWordService.validateText("hello world");
  206 + verify(imSensitiveWordMapper, times(1)).selectListByStatus(CommonStatusEnum.ENABLE.getStatus());
80 207
81 - // 准备:新列表不含原词 208 + // 准备:让 delete 校验通过
  209 + when(imSensitiveWordMapper.selectById(1L)).thenReturn(
  210 + ImSensitiveWordDO.builder().id(1L).word("badword")
  211 + .status(CommonStatusEnum.ENABLE.getStatus()).build());
  212 + // 准备:reload 时返回剩余词库(只剩中文那条)
82 when(imSensitiveWordMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus())) 213 when(imSensitiveWordMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus()))
83 .thenReturn(List.of( 214 .thenReturn(List.of(
84 - ImSensitiveWordDO.builder().id(3L).word("new").status(CommonStatusEnum.ENABLE.getStatus()).build() 215 + ImSensitiveWordDO.builder().id(2L).word("违禁词")
  216 + .status(CommonStatusEnum.ENABLE.getStatus()).build()
85 )); 217 ));
86 218
87 - // 调用:刷新  
88 -// sensitiveWordService.refresh(); 219 + // 调用:删除 badword,触发 invalidate
  220 + sensitiveWordService.deleteSensitiveWord(1L);
89 221
90 - // 断言:原敏感词不再命中,新词命中  
91 - assertDoesNotThrow(() -> sensitiveWordService.validateText("contains badword")); 222 + // 调用 + 断言:badword 已被删,不再命中
  223 + assertDoesNotThrow(() -> sensitiveWordService.validateText("contains badword here"));
  224 + // 中文词依然在
92 assertThrows(ServiceException.class, 225 assertThrows(ServiceException.class,
93 - () -> sensitiveWordService.validateText("contains new")); 226 + () -> sensitiveWordService.validateText("这条消息里有违禁词哦"));
  227 +
  228 + // 断言:selectListByStatus 共被调用 2 次
94 verify(imSensitiveWordMapper, times(2)).selectListByStatus(CommonStatusEnum.ENABLE.getStatus()); 229 verify(imSensitiveWordMapper, times(2)).selectListByStatus(CommonStatusEnum.ENABLE.getStatus());
95 } 230 }
96 231
  232 + @Test
  233 + public void testDeleteSensitiveWord_notExists_throws() {
  234 + // 准备:selectById 返回 null
  235 + when(imSensitiveWordMapper.selectById(999L)).thenReturn(null);
  236 +
  237 + // 调用 + 断言:抛 SENSITIVE_WORD_NOT_EXISTS
  238 + ServiceException exception = assertThrows(ServiceException.class,
  239 + () -> sensitiveWordService.deleteSensitiveWord(999L));
  240 + assertEquals(SENSITIVE_WORD_NOT_EXISTS.getCode(), exception.getCode());
  241 +
  242 + // 断言:未走到 deleteById
  243 + verify(imSensitiveWordMapper, never()).deleteById(anyLong());
  244 + }
  245 +
  246 + @Test
  247 + public void testDeleteSensitiveWordList_invalidatesCache() {
  248 + // 准备:首次 validateText 触发 cache load
  249 + sensitiveWordService.validateText("hello world");
  250 + verify(imSensitiveWordMapper, times(1)).selectListByStatus(CommonStatusEnum.ENABLE.getStatus());
  251 +
  252 + // 准备:reload 时返回空词库
  253 + when(imSensitiveWordMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus()))
  254 + .thenReturn(List.of());
  255 +
  256 + // 调用:批量删除,触发 invalidate
  257 + sensitiveWordService.deleteSensitiveWordList(List.of(1L, 2L));
  258 +
  259 + // 调用 + 断言:所有词都不再命中
  260 + assertDoesNotThrow(() -> sensitiveWordService.validateText("contains badword here"));
  261 + assertDoesNotThrow(() -> sensitiveWordService.validateText("这条消息里有违禁词哦"));
  262 +
  263 + // 断言:deleteByIds 被调用 1 次;selectListByStatus 共 2 次
  264 + verify(imSensitiveWordMapper, times(1)).deleteByIds(List.of(1L, 2L));
  265 + verify(imSensitiveWordMapper, times(2)).selectListByStatus(CommonStatusEnum.ENABLE.getStatus());
  266 + }
  267 +
  268 + @Test
  269 + public void testDeleteSensitiveWordList_emptyIds_skip() {
  270 + // 调用:空列表直接返回
  271 + sensitiveWordService.deleteSensitiveWordList(List.of());
  272 +
  273 + // 断言:mapper 不被调用
  274 + verify(imSensitiveWordMapper, never()).deleteByIds(anyList());
  275 + }
  276 +
97 } 277 }