Commit ed9f23ae8c5575b572e3c5b0c4bd0ecfe1128660
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 | 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 | 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 | 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<Integer> { |
| 39 | 39 | FRIEND_UPDATE(102, "好友更新", false, false), // 暂无对应类(客户端收到后自行拉取) |
| 40 | 40 | |
| 41 | 41 | // ========== 群事件(1501-1520 直接复用 OpenIM 段位编号;1530+ 我们独有扩展) ========== |
| 42 | - // 1500 mirror OpenIM GroupNotificationBegin marker,不使用 | |
| 42 | + // 1500 对应 OpenIM GroupNotificationBegin 起始位,仅作占位,不使用 | |
| 43 | 43 | /** |
| 44 | 44 | * 对应 OpenIM:sdkws.GroupCreatedTips(GroupCreatedNotification 1501) |
| 45 | 45 | * 对应自己的类:GroupCreateNotification |
| ... | ... | @@ -54,7 +54,7 @@ public enum ImMessageTypeEnum implements ArrayValuable<Integer> { |
| 54 | 54 | GROUP_INFO_UPDATE(1502, "群信息变更", true, false), |
| 55 | 55 | /** |
| 56 | 56 | * 对应 OpenIM:sdkws.JoinGroupApplicationTips(JoinGroupApplicationNotification 1503) |
| 57 | - * TODO @AI 未实现:入群申请;本期 §1.3 Tier 3 不在范围,预留段位以便未来对齐 OpenIM | |
| 57 | + * TODO @AI 未实现:入群申请;本期不在范围,预留段位以便未来对齐 OpenIM | |
| 58 | 58 | */ |
| 59 | 59 | GROUP_JOIN_APPLICATION(1503, "入群申请", true, false), |
| 60 | 60 | /** |
| ... | ... | @@ -65,12 +65,12 @@ public enum ImMessageTypeEnum implements ArrayValuable<Integer> { |
| 65 | 65 | GROUP_MEMBER_QUIT(1504, "成员退群", true, false), |
| 66 | 66 | /** |
| 67 | 67 | * 对应 OpenIM:sdkws.GroupApplicationAcceptedTips(GroupApplicationAcceptedNotification 1505) |
| 68 | - * TODO @AI 未实现:入群申请通过;本期 §1.3 Tier 3 不在范围 | |
| 68 | + * TODO @AI 未实现:入群申请通过;本期不在范围 | |
| 69 | 69 | */ |
| 70 | 70 | GROUP_APPLICATION_ACCEPTED(1505, "入群申请通过", true, false), |
| 71 | 71 | /** |
| 72 | 72 | * 对应 OpenIM:sdkws.GroupApplicationRejectedTips(GroupApplicationRejectedNotification 1506) |
| 73 | - * TODO @AI 未实现:入群申请拒绝;本期 §1.3 Tier 3 不在范围 | |
| 73 | + * TODO @AI 未实现:入群申请拒绝;本期不在范围 | |
| 74 | 74 | */ |
| 75 | 75 | GROUP_APPLICATION_REJECTED(1506, "入群申请拒绝", true, false), |
| 76 | 76 | /** |
| ... | ... | @@ -93,7 +93,7 @@ public enum ImMessageTypeEnum implements ArrayValuable<Integer> { |
| 93 | 93 | GROUP_MEMBER_INVITE(1509, "成员加入", true, false), |
| 94 | 94 | /** |
| 95 | 95 | * 对应 OpenIM:sdkws.MemberEnterTips(MemberEnterNotification 1510) |
| 96 | - * TODO @AI 未实现:自由进群(链接 / 二维码进群);本期 §1.3 Tier 3 不在范围 | |
| 96 | + * TODO @AI 未实现:自由进群(链接 / 二维码进群);本期不在范围 | |
| 97 | 97 | */ |
| 98 | 98 | GROUP_MEMBER_ENTER(1510, "自由进群", true, false), |
| 99 | 99 | /** |
| ... | ... | @@ -104,22 +104,22 @@ public enum ImMessageTypeEnum implements ArrayValuable<Integer> { |
| 104 | 104 | GROUP_DISSOLVE(1511, "群解散", true, false), |
| 105 | 105 | /** |
| 106 | 106 | * 对应 OpenIM:sdkws.GroupMemberMutedTips(GroupMemberMutedNotification 1512) |
| 107 | - * TODO @AI 未实现:单成员禁言(管理员禁言某成员);本期 §1.3 Tier 3 不在范围【这个本期发版,是要实现的】 | |
| 107 | + * TODO @AI 未实现:单成员禁言(管理员禁言某成员);本期发版要实现 | |
| 108 | 108 | */ |
| 109 | 109 | GROUP_MEMBER_MUTED(1512, "成员禁言", true, false), |
| 110 | 110 | /** |
| 111 | 111 | * 对应 OpenIM:sdkws.GroupMemberCancelMutedTips(GroupMemberCancelMutedNotification 1513) |
| 112 | - * TODO @AI 未实现:单成员取消禁言;本期 §1.3 Tier 3 不在范围【这个本期发版,是要实现的】 | |
| 112 | + * TODO @AI 未实现:单成员取消禁言;本期发版要实现 | |
| 113 | 113 | */ |
| 114 | 114 | GROUP_MEMBER_CANCEL_MUTED(1513, "成员取消禁言", true, false), |
| 115 | 115 | /** |
| 116 | 116 | * 对应 OpenIM:sdkws.GroupMutedTips(GroupMutedNotification 1514) |
| 117 | - * TODO @AI 未实现:全群禁言(管理员把整个群设为禁言状态,所有成员都不能发消息);本期 §1.3 Tier 3 不在范围【这个本期发版,是要实现的】 | |
| 117 | + * TODO @AI 未实现:全群禁言(管理员把整个群设为禁言状态,所有成员都不能发消息);本期发版要实现 | |
| 118 | 118 | */ |
| 119 | 119 | GROUP_MUTED(1514, "全群禁言", true, false), |
| 120 | 120 | /** |
| 121 | 121 | * 对应 OpenIM:sdkws.GroupCancelMutedTips(GroupCancelMutedNotification 1515) |
| 122 | - * TODO @AI 未实现:全群取消禁言;本期 §1.3 Tier 3 不在范围【这个本期发版,是要实现的】 | |
| 122 | + * TODO @AI 未实现:全群取消禁言;本期发版要实现 | |
| 123 | 123 | */ |
| 124 | 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 | 1 | package cn.iocoder.yudao.module.im.service.group; |
| 2 | 2 | |
| 3 | 3 | import cn.hutool.core.collection.CollUtil; |
| 4 | -import cn.hutool.core.util.StrUtil; | |
| 4 | +import cn.hutool.core.util.ObjUtil; | |
| 5 | 5 | import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; |
| 6 | 6 | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
| 7 | 7 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberUpdateReqVO; |
| ... | ... | @@ -23,7 +23,8 @@ import java.time.LocalDateTime; |
| 23 | 23 | import java.util.*; |
| 24 | 24 | |
| 25 | 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 | 28 | import static cn.iocoder.yudao.module.im.dal.redis.RedisKeyConstants.GROUP_MEMBER_IDS; |
| 28 | 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 | 216 | .setId(member.getId()); |
| 216 | 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 | 222 | groupMessageService.sendGroupMessage(userId, ImGroupMessageSendDTO.ofGroupMemberNicknameUpdate( |
| 221 | 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 | 231 | groupMessageService.sendGroupMessage(userId, List.of(userId), ImGroupMessageSendDTO.ofGroupMemberSettingUpdate( |
| 226 | 232 | groupId, userId, updateReqVO.getMuted(), updateReqVO.getGroupRemark())); |
| 227 | 233 | } | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupService.java
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 | 10 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 11 | 11 | import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; |
| 12 | 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 | 18 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberInviteReqVO; |
| 15 | 19 | import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberRemoveReqVO; |
| 16 | 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 | 49 | import static cn.iocoder.yudao.module.im.enums.ImCommonConstants.*; |
| 46 | 50 | |
| 47 | 51 | /** |
| 48 | - * 群 Service 实现类 | |
| 52 | + * 用户群 Service 实现类 | |
| 49 | 53 | * |
| 50 | 54 | * @author 芋道源码 |
| 51 | 55 | */ |
| ... | ... | @@ -125,19 +129,26 @@ public class ImGroupServiceImpl implements ImGroupService { |
| 125 | 129 | ImGroupDO newGroup = BeanUtils.toBean(updateReqVO, ImGroupDO.class); |
| 126 | 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 | 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 | 154 | // 4. 返回合并后的新群信息(updateReqVO 非空字段覆盖 group) |
| ... | ... | @@ -194,8 +205,10 @@ public class ImGroupServiceImpl implements ImGroupService { |
| 194 | 205 | // 2. 批量添加群成员 |
| 195 | 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 | 212 | ImGroupMessageSendDTO.ofGroupMemberInvite(groupId, userId, memberUserIds)); |
| 200 | 213 | } |
| 201 | 214 | |
| ... | ... | @@ -208,7 +221,7 @@ public class ImGroupServiceImpl implements ImGroupService { |
| 208 | 221 | throw exception(GROUP_OWNER_CANNOT_QUIT); |
| 209 | 222 | } |
| 210 | 223 | |
| 211 | - // 3. 先发 GROUP_MEMBER_QUIT 通知:放在 quitter 移除前,sendGroupMessage 才能查到全员(含 quitter) | |
| 224 | + // 3. 先发广播,后移成员(见类 javadoc) | |
| 212 | 225 | groupMessageService.sendGroupMessage(userId, ImGroupMessageSendDTO.ofGroupMemberQuit(groupId, userId)); |
| 213 | 226 | |
| 214 | 227 | // 4.1 移除群成员 |
| ... | ... | @@ -241,7 +254,7 @@ public class ImGroupServiceImpl implements ImGroupService { |
| 241 | 254 | |
| 242 | 255 | // 2. 先发 GROUP_MEMBER_KICK 通知:放在被踢者移除前,sendGroupMessage 才能查到全员(含被踢者) |
| 243 | 256 | groupMessageService.sendGroupMessage(userId, |
| 244 | - ImGroupMessageSendDTO.ofGroupMemberKick(groupId, userId, new ArrayList<>(targetUserIds))); | |
| 257 | + ImGroupMessageSendDTO.ofGroupMemberKick(groupId, userId, targetUserIds)); | |
| 245 | 258 | |
| 246 | 259 | // 3.1 批量移除群成员 |
| 247 | 260 | groupMemberService.removeGroupMembers(groupId, targetUserIds); |
| ... | ... | @@ -279,7 +292,7 @@ public class ImGroupServiceImpl implements ImGroupService { |
| 279 | 292 | |
| 280 | 293 | // 3. 推送 GROUP_ADMIN_ADD 通知给全员 |
| 281 | 294 | groupMessageService.sendGroupMessage(userId, |
| 282 | - ImGroupMessageSendDTO.ofGroupAdminAdd(groupId, userId, new ArrayList<>(changedUserIds))); | |
| 295 | + ImGroupMessageSendDTO.ofGroupAdminAdd(groupId, userId, changedUserIds)); | |
| 283 | 296 | } |
| 284 | 297 | |
| 285 | 298 | @Override |
| ... | ... | @@ -306,7 +319,7 @@ public class ImGroupServiceImpl implements ImGroupService { |
| 306 | 319 | |
| 307 | 320 | // 3. 推送 GROUP_ADMIN_REMOVE 通知给全员 |
| 308 | 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 | 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 | 37 | * @param senderId 发送人编号 |
| 36 | 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 | 5 | import jakarta.validation.constraints.NotNull; |
| 6 | 6 | import lombok.Data; |
| 7 | 7 | |
| 8 | +import java.util.ArrayList; | |
| 9 | +import java.util.Collection; | |
| 8 | 10 | import java.util.List; |
| 9 | 11 | |
| 10 | 12 | /** |
| ... | ... | @@ -52,93 +54,108 @@ public class ImGroupMessageSendDTO { |
| 52 | 54 | |
| 53 | 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 | 61 | return new ImGroupMessageSendDTO().setGroupId(groupId) |
| 59 | 62 | .setType(ImMessageTypeEnum.GROUP_CREATE.getType()).setContent(notification); |
| 60 | 63 | } |
| 61 | 64 | |
| 62 | 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 | 70 | return new ImGroupMessageSendDTO().setGroupId(groupId) |
| 66 | 71 | .setType(ImMessageTypeEnum.GROUP_INFO_UPDATE.getType()).setContent(notification); |
| 67 | 72 | } |
| 68 | 73 | |
| 69 | 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 | 77 | return new ImGroupMessageSendDTO().setGroupId(groupId) |
| 73 | 78 | .setType(ImMessageTypeEnum.GROUP_MEMBER_QUIT.getType()).setContent(notification); |
| 74 | 79 | } |
| 75 | 80 | |
| 76 | 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 | 85 | return new ImGroupMessageSendDTO().setGroupId(groupId) |
| 80 | 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 | 93 | return new ImGroupMessageSendDTO().setGroupId(groupId) |
| 87 | 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 | 101 | return new ImGroupMessageSendDTO().setGroupId(groupId) |
| 94 | 102 | .setType(ImMessageTypeEnum.GROUP_MEMBER_INVITE.getType()).setContent(notification); |
| 95 | 103 | } |
| 96 | 104 | |
| 97 | 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 | 108 | return new ImGroupMessageSendDTO().setGroupId(groupId) |
| 101 | 109 | .setType(ImMessageTypeEnum.GROUP_DISSOLVE.getType()).setContent(notification); |
| 102 | 110 | } |
| 103 | 111 | |
| 104 | 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 | 116 | return new ImGroupMessageSendDTO().setGroupId(groupId) |
| 108 | 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 | 124 | return new ImGroupMessageSendDTO().setGroupId(groupId) |
| 115 | 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 | 132 | return new ImGroupMessageSendDTO().setGroupId(groupId) |
| 122 | 133 | .setType(ImMessageTypeEnum.GROUP_ADMIN_REMOVE.getType()).setContent(notification); |
| 123 | 134 | } |
| 124 | 135 | |
| 125 | 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 | 141 | return new ImGroupMessageSendDTO().setGroupId(groupId) |
| 129 | 142 | .setType(ImMessageTypeEnum.GROUP_NOTICE_UPDATE.getType()).setContent(notification); |
| 130 | 143 | } |
| 131 | 144 | |
| 132 | 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 | 150 | return new ImGroupMessageSendDTO().setGroupId(groupId) |
| 136 | 151 | .setType(ImMessageTypeEnum.GROUP_NAME_UPDATE.getType()).setContent(notification); |
| 137 | 152 | } |
| 138 | 153 | |
| 139 | 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 | 159 | return new ImGroupMessageSendDTO().setGroupId(groupId) |
| 143 | 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 | 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 | 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 | 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 | 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 | 3 | import lombok.Data; |
| 4 | 4 | |
| 5 | 5 | /** |
| 6 | - * 群信息变更事件通知(NAME / NOTICE 之外字段的兜底) | |
| 6 | + * 群信息变更事件通知(当前承载头像变更,NAME / NOTICE 走独立事件) | |
| 7 | 7 | */ |
| 8 | 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 | 14 | private String oldAvatar; | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberInviteNotification.java
| 1 | 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 | 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 | 6 | * 成员组内昵称变更事件通知 |
| 7 | 7 | */ |
| 8 | 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 | 14 | private String displayUserName; | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberQuitNotification.java
| 1 | 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 | 4 | |
| 5 | 5 | /** |
| 6 | 6 | * 群成员个人设置变更事件通知(个人多端同步) |
| 7 | + * <p> | |
| 8 | + * muted / groupRemark 字段 null 表示本次未变更,前端按非 null 局部更新 | |
| 7 | 9 | */ |
| 8 | 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 | 16 | private Boolean muted; |
| 19 | 17 | /** |
| 20 | - * 群备注,null 表示本次未变更 | |
| 18 | + * 群备注 | |
| 21 | 19 | */ |
| 22 | 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 | 6 | * 群名变更事件通知 |
| 7 | 7 | */ |
| 8 | 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 | 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 | 6 | * 群公告变更事件通知 |
| 7 | 7 | */ |
| 8 | 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 | 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 | 6 | * 群主转让事件通知 |
| 7 | 7 | */ |
| 8 | 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 | 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 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; |
| 4 | 4 | import cn.iocoder.yudao.framework.common.exception.ServiceException; |
| 5 | +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; | |
| 5 | 6 | import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; |
| 7 | +import cn.iocoder.yudao.module.im.controller.admin.manager.sensitiveword.vo.ImSensitiveWordSaveReqVO; | |
| 6 | 8 | import cn.iocoder.yudao.module.im.dal.dataobject.sensitiveword.ImSensitiveWordDO; |
| 7 | 9 | import cn.iocoder.yudao.module.im.dal.mysql.sensitiveword.ImSensitiveWordMapper; |
| 10 | +import org.junit.jupiter.api.AfterEach; | |
| 8 | 11 | import org.junit.jupiter.api.BeforeEach; |
| 9 | 12 | import org.junit.jupiter.api.Test; |
| 10 | 13 | import org.mockito.InjectMocks; |
| 11 | 14 | import org.mockito.Mock; |
| 12 | 15 | |
| 16 | +import java.time.LocalDateTime; | |
| 13 | 17 | import java.util.List; |
| 14 | 18 | |
| 15 | 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 | 22 | import static org.junit.jupiter.api.Assertions.*; |
| 17 | 23 | import static org.mockito.Mockito.*; |
| 18 | 24 | |
| ... | ... | @@ -23,6 +29,8 @@ import static org.mockito.Mockito.*; |
| 23 | 29 | */ |
| 24 | 30 | public class ImSensitiveWordServiceImplTest extends BaseMockitoUnitTest { |
| 25 | 31 | |
| 32 | + private static final Long TENANT_ID = 1L; | |
| 33 | + | |
| 26 | 34 | @InjectMocks |
| 27 | 35 | private ImSensitiveWordServiceImpl sensitiveWordService; |
| 28 | 36 | |
| ... | ... | @@ -31,15 +39,23 @@ public class ImSensitiveWordServiceImplTest extends BaseMockitoUnitTest { |
| 31 | 39 | |
| 32 | 40 | @BeforeEach |
| 33 | 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 | 47 | .thenReturn(List.of( |
| 37 | 48 | ImSensitiveWordDO.builder().id(1L).word("badword") |
| 38 | 49 | .status(CommonStatusEnum.ENABLE.getStatus()).build(), |
| 39 | 50 | ImSensitiveWordDO.builder().id(2L).word("违禁词") |
| 40 | 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 | 61 | @Test |
| ... | ... | @@ -74,24 +90,188 @@ public class ImSensitiveWordServiceImplTest extends BaseMockitoUnitTest { |
| 74 | 90 | } |
| 75 | 91 | |
| 76 | 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 | 213 | when(imSensitiveWordMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus())) |
| 83 | 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 | 225 | assertThrows(ServiceException.class, |
| 93 | - () -> sensitiveWordService.validateText("contains new")); | |
| 226 | + () -> sensitiveWordService.validateText("这条消息里有违禁词哦")); | |
| 227 | + | |
| 228 | + // 断言:selectListByStatus 共被调用 2 次 | |
| 94 | 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 | } | ... | ... |