From ed9f23ae8c5575b572e3c5b0c4bd0ecfe1128660 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 3 May 2026 09:22:54 +0800 Subject: [PATCH] ✨ feat(im): 重构群通知相关,对齐 openim 的消息编号(继续优化代码) --- yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java | 9 ++++++--- yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/message/ImMessageTypeEnum.java | 18 +++++++++--------- yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberServiceImpl.java | 18 ++++++++++++------ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupService.java | 2 +- yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImpl.java | 53 +++++++++++++++++++++++++++++++++-------------------- yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageService.java | 4 +++- yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/dto/ImGroupMessageSendDTO.java | 79 ++++++++++++++++++++++++++++++++++++++++++++++++------------------------------- yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/BaseGroupNotification.java | 16 ++++++++++++++++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupAdminAddNotification.java | 19 ++----------------- yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupAdminRemoveNotification.java | 19 ++----------------- yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupCreateNotification.java | 19 ++----------------- yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupDissolveNotification.java | 11 +---------- yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupInfoUpdateNotification.java | 8 ++------ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberInviteNotification.java | 19 ++----------------- yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberKickNotification.java | 19 ++----------------- yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberNicknameUpdateNotification.java | 6 +----- yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberQuitNotification.java | 11 +---------- yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberSettingUpdateNotification.java | 12 +++++------- yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupNameUpdateNotification.java | 6 +----- yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupNoticeUpdateNotification.java | 6 +----- yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupOwnerTransferNotification.java | 6 +----- yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/MembersAffectedNotification.java | 18 ++++++++++++++++++ yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/sensitiveword/ImSensitiveWordServiceImplTest.java | 206 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------- 23 files changed, 362 insertions(+), 222 deletions(-) create mode 100644 yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/BaseGroupNotification.java create mode 100644 yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/MembersAffectedNotification.java diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java index 2ddf7e0..7194b5e 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java @@ -372,10 +372,13 @@ public class CollectionUtils { return false; } - public static List of(T t, Collection ts) { + /** + * 把单元素 head 与集合 tail 合并成新 List(head 在前,tail 顺序保留) + */ + public static List of(T head, Collection tail) { List list = new ArrayList<>(); - list.add(t); - CollUtil.addAll(list, ts); + list.add(head); + CollUtil.addAll(list, tail); return list; } diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/message/ImMessageTypeEnum.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/message/ImMessageTypeEnum.java index 52157fa..1af681b 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/message/ImMessageTypeEnum.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/message/ImMessageTypeEnum.java @@ -39,7 +39,7 @@ public enum ImMessageTypeEnum implements ArrayValuable { FRIEND_UPDATE(102, "好友更新", false, false), // 暂无对应类(客户端收到后自行拉取) // ========== 群事件(1501-1520 直接复用 OpenIM 段位编号;1530+ 我们独有扩展) ========== - // 1500 mirror OpenIM GroupNotificationBegin marker,不使用 + // 1500 对应 OpenIM GroupNotificationBegin 起始位,仅作占位,不使用 /** * 对应 OpenIM:sdkws.GroupCreatedTips(GroupCreatedNotification 1501) * 对应自己的类:GroupCreateNotification @@ -54,7 +54,7 @@ public enum ImMessageTypeEnum implements ArrayValuable { GROUP_INFO_UPDATE(1502, "群信息变更", true, false), /** * 对应 OpenIM:sdkws.JoinGroupApplicationTips(JoinGroupApplicationNotification 1503) - * TODO @AI 未实现:入群申请;本期 §1.3 Tier 3 不在范围,预留段位以便未来对齐 OpenIM + * TODO @AI 未实现:入群申请;本期不在范围,预留段位以便未来对齐 OpenIM */ GROUP_JOIN_APPLICATION(1503, "入群申请", true, false), /** @@ -65,12 +65,12 @@ public enum ImMessageTypeEnum implements ArrayValuable { GROUP_MEMBER_QUIT(1504, "成员退群", true, false), /** * 对应 OpenIM:sdkws.GroupApplicationAcceptedTips(GroupApplicationAcceptedNotification 1505) - * TODO @AI 未实现:入群申请通过;本期 §1.3 Tier 3 不在范围 + * TODO @AI 未实现:入群申请通过;本期不在范围 */ GROUP_APPLICATION_ACCEPTED(1505, "入群申请通过", true, false), /** * 对应 OpenIM:sdkws.GroupApplicationRejectedTips(GroupApplicationRejectedNotification 1506) - * TODO @AI 未实现:入群申请拒绝;本期 §1.3 Tier 3 不在范围 + * TODO @AI 未实现:入群申请拒绝;本期不在范围 */ GROUP_APPLICATION_REJECTED(1506, "入群申请拒绝", true, false), /** @@ -93,7 +93,7 @@ public enum ImMessageTypeEnum implements ArrayValuable { GROUP_MEMBER_INVITE(1509, "成员加入", true, false), /** * 对应 OpenIM:sdkws.MemberEnterTips(MemberEnterNotification 1510) - * TODO @AI 未实现:自由进群(链接 / 二维码进群);本期 §1.3 Tier 3 不在范围 + * TODO @AI 未实现:自由进群(链接 / 二维码进群);本期不在范围 */ GROUP_MEMBER_ENTER(1510, "自由进群", true, false), /** @@ -104,22 +104,22 @@ public enum ImMessageTypeEnum implements ArrayValuable { GROUP_DISSOLVE(1511, "群解散", true, false), /** * 对应 OpenIM:sdkws.GroupMemberMutedTips(GroupMemberMutedNotification 1512) - * TODO @AI 未实现:单成员禁言(管理员禁言某成员);本期 §1.3 Tier 3 不在范围【这个本期发版,是要实现的】 + * TODO @AI 未实现:单成员禁言(管理员禁言某成员);本期发版要实现 */ GROUP_MEMBER_MUTED(1512, "成员禁言", true, false), /** * 对应 OpenIM:sdkws.GroupMemberCancelMutedTips(GroupMemberCancelMutedNotification 1513) - * TODO @AI 未实现:单成员取消禁言;本期 §1.3 Tier 3 不在范围【这个本期发版,是要实现的】 + * TODO @AI 未实现:单成员取消禁言;本期发版要实现 */ GROUP_MEMBER_CANCEL_MUTED(1513, "成员取消禁言", true, false), /** * 对应 OpenIM:sdkws.GroupMutedTips(GroupMutedNotification 1514) - * TODO @AI 未实现:全群禁言(管理员把整个群设为禁言状态,所有成员都不能发消息);本期 §1.3 Tier 3 不在范围【这个本期发版,是要实现的】 + * TODO @AI 未实现:全群禁言(管理员把整个群设为禁言状态,所有成员都不能发消息);本期发版要实现 */ GROUP_MUTED(1514, "全群禁言", true, false), /** * 对应 OpenIM:sdkws.GroupCancelMutedTips(GroupCancelMutedNotification 1515) - * TODO @AI 未实现:全群取消禁言;本期 §1.3 Tier 3 不在范围【这个本期发版,是要实现的】 + * TODO @AI 未实现:全群取消禁言;本期发版要实现 */ GROUP_CANCEL_MUTED(1515, "全群取消禁言", true, false), /** diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberServiceImpl.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberServiceImpl.java index ce50626..b1ef7ca 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberServiceImpl.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberServiceImpl.java @@ -1,7 +1,7 @@ package cn.iocoder.yudao.module.im.service.group; import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.ObjUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberUpdateReqVO; @@ -23,7 +23,8 @@ import java.time.LocalDateTime; import java.util.*; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; import static cn.iocoder.yudao.module.im.dal.redis.RedisKeyConstants.GROUP_MEMBER_IDS; import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.GROUP_MEMBER_NOT_IN_GROUP; @@ -215,13 +216,18 @@ public class ImGroupMemberServiceImpl implements ImGroupMemberService { .setId(member.getId()); groupMemberMapper.updateById(updateObj); - // 3.1 displayUserName 是公开字段,单独走 GROUP_MEMBER_NICKNAME_UPDATE 广播给全员 - if (StrUtil.isNotEmpty(updateReqVO.getDisplayUserName())) { + // 3.1 displayUserName 是公开字段,单独走 GROUP_MEMBER_NICKNAME_UPDATE 广播给全员;空串视为「清空昵称」也要广播;与旧值相同跳过 + if (updateReqVO.getDisplayUserName() != null + && ObjUtil.notEqual(updateReqVO.getDisplayUserName(), member.getDisplayUserName())) { groupMessageService.sendGroupMessage(userId, ImGroupMessageSendDTO.ofGroupMemberNicknameUpdate( groupId, userId, updateReqVO.getDisplayUserName())); } - // 3.2 muted / groupRemark 是个人字段,target 显式指定为自己仅推自己多端同步;payload 携带变更字段,前端按非 null 局部更新 - if (updateReqVO.getMuted() != null || updateReqVO.getGroupRemark() != null) { + // 3.2 muted / groupRemark 是个人字段,仅推自己做多端同步;与旧值都相同跳过 + boolean mutedChanged = updateReqVO.getMuted() != null + && ObjUtil.notEqual(updateReqVO.getMuted(), member.getMuted()); + boolean groupRemarkChanged = updateReqVO.getGroupRemark() != null + && ObjUtil.notEqual(updateReqVO.getGroupRemark(), member.getGroupRemark()); + if (mutedChanged || groupRemarkChanged) { groupMessageService.sendGroupMessage(userId, List.of(userId), ImGroupMessageSendDTO.ofGroupMemberSettingUpdate( groupId, userId, updateReqVO.getMuted(), updateReqVO.getGroupRemark())); } diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupService.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupService.java index dcffda1..8f973a5 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupService.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupService.java @@ -18,7 +18,7 @@ import java.util.List; import java.util.Map; /** - * 群 Service 接口 + * 用户群群 Service 接口 * * @author 芋道源码 */ diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImpl.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImpl.java index 21ae9af..d213bd8 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImpl.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImpl.java @@ -10,7 +10,11 @@ import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.module.im.controller.admin.group.vo.*; +import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupAdminAddReqVO; +import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupAdminRemoveReqVO; +import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupCreateReqVO; +import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupTransferOwnerReqVO; +import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupUpdateReqVO; import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberInviteReqVO; import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberRemoveReqVO; 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.*; import static cn.iocoder.yudao.module.im.enums.ImCommonConstants.*; /** - * 群 Service 实现类 + * 用户群 Service 实现类 * * @author 芋道源码 */ @@ -125,19 +129,26 @@ public class ImGroupServiceImpl implements ImGroupService { ImGroupDO newGroup = BeanUtils.toBean(updateReqVO, ImGroupDO.class); groupMapper.updateById(newGroup); - // 3. 按变更字段分别推送 GROUP_NAME / NOTICE / INFO_UPDATE 通知 + // 3. 按变更字段分别推送 GROUP_NAME / NOTICE / INFO_UPDATE 通知;活跃成员只查一次复用,避免 3 次 Redis GET + // name / avatar 不允许空串(业务上必须非空),notice 允许空串(清空公告也是有效操作) Long groupId = group.getId(); - if (StrUtil.isNotEmpty(updateReqVO.getName())) { - groupMessageService.sendGroupMessage(userId, ImGroupMessageSendDTO.ofGroupNameUpdate( - groupId, userId, group.getName(), updateReqVO.getName())); - } - if (updateReqVO.getNotice() != null) { - groupMessageService.sendGroupMessage(userId, ImGroupMessageSendDTO.ofGroupNoticeUpdate( - groupId, userId, group.getNotice(), updateReqVO.getNotice())); - } - if (StrUtil.isNotEmpty(updateReqVO.getAvatar())) { - groupMessageService.sendGroupMessage(userId, ImGroupMessageSendDTO.ofGroupInfoUpdate( - groupId, userId, group.getAvatar(), updateReqVO.getAvatar())); + boolean nameChanged = StrUtil.isNotEmpty(updateReqVO.getName()); + boolean noticeChanged = updateReqVO.getNotice() != null; + boolean avatarChanged = StrUtil.isNotEmpty(updateReqVO.getAvatar()); + if (nameChanged || noticeChanged || avatarChanged) { + List memberUserIds = groupMemberService.getActiveGroupMemberUserIdsByGroupId(groupId); + if (nameChanged) { + groupMessageService.sendGroupMessage(userId, memberUserIds, ImGroupMessageSendDTO.ofGroupNameUpdate( + groupId, userId, group.getName(), updateReqVO.getName())); + } + if (noticeChanged) { + groupMessageService.sendGroupMessage(userId, memberUserIds, ImGroupMessageSendDTO.ofGroupNoticeUpdate( + groupId, userId, group.getNotice(), updateReqVO.getNotice())); + } + if (avatarChanged) { + groupMessageService.sendGroupMessage(userId, memberUserIds, ImGroupMessageSendDTO.ofGroupInfoUpdate( + groupId, userId, group.getAvatar(), updateReqVO.getAvatar())); + } } // 4. 返回合并后的新群信息(updateReqVO 非空字段覆盖 group) @@ -194,8 +205,10 @@ public class ImGroupServiceImpl implements ImGroupService { // 2. 批量添加群成员 groupMemberService.addGroupMembers(groupId, memberUserIds); - // 3. 发 GROUP_MEMBER_INVITE 通知给全员;新成员已 ENABLE,sendGroupMessage 查到的 active 自然包含 - groupMessageService.sendGroupMessage(userId, + // 3. 发 GROUP_MEMBER_INVITE 通知给全员;本地拼 receivers(已查的 active + 新邀请)避免缓存刚 evict 后强制走 DB + Set allReceivers = new HashSet<>(memberUserIds); + activeMembers.forEach(member -> allReceivers.add(member.getUserId())); + groupMessageService.sendGroupMessage(userId, allReceivers, ImGroupMessageSendDTO.ofGroupMemberInvite(groupId, userId, memberUserIds)); } @@ -208,7 +221,7 @@ public class ImGroupServiceImpl implements ImGroupService { throw exception(GROUP_OWNER_CANNOT_QUIT); } - // 3. 先发 GROUP_MEMBER_QUIT 通知:放在 quitter 移除前,sendGroupMessage 才能查到全员(含 quitter) + // 3. 先发广播,后移成员(见类 javadoc) groupMessageService.sendGroupMessage(userId, ImGroupMessageSendDTO.ofGroupMemberQuit(groupId, userId)); // 4.1 移除群成员 @@ -241,7 +254,7 @@ public class ImGroupServiceImpl implements ImGroupService { // 2. 先发 GROUP_MEMBER_KICK 通知:放在被踢者移除前,sendGroupMessage 才能查到全员(含被踢者) groupMessageService.sendGroupMessage(userId, - ImGroupMessageSendDTO.ofGroupMemberKick(groupId, userId, new ArrayList<>(targetUserIds))); + ImGroupMessageSendDTO.ofGroupMemberKick(groupId, userId, targetUserIds)); // 3.1 批量移除群成员 groupMemberService.removeGroupMembers(groupId, targetUserIds); @@ -279,7 +292,7 @@ public class ImGroupServiceImpl implements ImGroupService { // 3. 推送 GROUP_ADMIN_ADD 通知给全员 groupMessageService.sendGroupMessage(userId, - ImGroupMessageSendDTO.ofGroupAdminAdd(groupId, userId, new ArrayList<>(changedUserIds))); + ImGroupMessageSendDTO.ofGroupAdminAdd(groupId, userId, changedUserIds)); } @Override @@ -306,7 +319,7 @@ public class ImGroupServiceImpl implements ImGroupService { // 3. 推送 GROUP_ADMIN_REMOVE 通知给全员 groupMessageService.sendGroupMessage(userId, - ImGroupMessageSendDTO.ofGroupAdminRemove(groupId, userId, new ArrayList<>(changedUserIds))); + ImGroupMessageSendDTO.ofGroupAdminRemove(groupId, userId, changedUserIds)); } /** diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageService.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageService.java index 3dfd554..57cfce6 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageService.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageService.java @@ -30,7 +30,9 @@ public interface ImGroupMessageService { ImGroupMessageDO sendGroupMessage(Long senderId, ImGroupMessageSendReqVO reqVO); /** - * 【系统调用】发送群聊消息:自动算可见用户 + 推送 + * 【系统调用】发送群聊消息:内部查 active 成员 + 推送 + *

+ * 调用方批量调用会触发多次 active 成员查询;如果调用方已经持有成员快照,优先使用 {@link #sendGroupMessage(Long, Collection, ImGroupMessageSendDTO)} 方法,避免重复查询。 * * @param senderId 发送人编号 * @param dto 消息 DTO diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/dto/ImGroupMessageSendDTO.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/dto/ImGroupMessageSendDTO.java index 857eb35..cf29435 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/dto/ImGroupMessageSendDTO.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/dto/ImGroupMessageSendDTO.java @@ -5,6 +5,8 @@ import cn.iocoder.yudao.module.im.service.websocket.dto.notification.*; import jakarta.validation.constraints.NotNull; import lombok.Data; +import java.util.ArrayList; +import java.util.Collection; import java.util.List; /** @@ -52,93 +54,108 @@ public class ImGroupMessageSendDTO { // ========== 群广播事件静态工厂(对应 ImMessageTypeEnum 群事件) ========== - public static ImGroupMessageSendDTO ofGroupCreate(Long groupId, Long operatorUserId, List memberUserIds) { - GroupCreateNotification notification = new GroupCreateNotification() - .setOperatorUserId(operatorUserId).setMemberUserIds(memberUserIds); + public static ImGroupMessageSendDTO ofGroupCreate(Long groupId, Long operatorUserId, Collection memberUserIds) { + GroupCreateNotification notification = new GroupCreateNotification(); + notification.setOperatorUserId(operatorUserId); + notification.setMemberUserIds(new ArrayList<>(memberUserIds)); return new ImGroupMessageSendDTO().setGroupId(groupId) .setType(ImMessageTypeEnum.GROUP_CREATE.getType()).setContent(notification); } public static ImGroupMessageSendDTO ofGroupInfoUpdate(Long groupId, Long operatorUserId, String oldAvatar, String newAvatar) { - GroupInfoUpdateNotification notification = new GroupInfoUpdateNotification() - .setOperatorUserId(operatorUserId).setOldAvatar(oldAvatar).setNewAvatar(newAvatar); + GroupInfoUpdateNotification notification = new GroupInfoUpdateNotification(); + notification.setOperatorUserId(operatorUserId); + notification.setOldAvatar(oldAvatar); + notification.setNewAvatar(newAvatar); return new ImGroupMessageSendDTO().setGroupId(groupId) .setType(ImMessageTypeEnum.GROUP_INFO_UPDATE.getType()).setContent(notification); } public static ImGroupMessageSendDTO ofGroupMemberQuit(Long groupId, Long operatorUserId) { - GroupMemberQuitNotification notification = new GroupMemberQuitNotification() - .setOperatorUserId(operatorUserId); + GroupMemberQuitNotification notification = new GroupMemberQuitNotification(); + notification.setOperatorUserId(operatorUserId); return new ImGroupMessageSendDTO().setGroupId(groupId) .setType(ImMessageTypeEnum.GROUP_MEMBER_QUIT.getType()).setContent(notification); } public static ImGroupMessageSendDTO ofGroupOwnerTransfer(Long groupId, Long operatorUserId, Long newOwnerUserId) { - GroupOwnerTransferNotification notification = new GroupOwnerTransferNotification() - .setOperatorUserId(operatorUserId).setNewOwnerUserId(newOwnerUserId); + GroupOwnerTransferNotification notification = new GroupOwnerTransferNotification(); + notification.setOperatorUserId(operatorUserId); + notification.setNewOwnerUserId(newOwnerUserId); return new ImGroupMessageSendDTO().setGroupId(groupId) .setType(ImMessageTypeEnum.GROUP_OWNER_TRANSFER.getType()).setContent(notification); } - public static ImGroupMessageSendDTO ofGroupMemberKick(Long groupId, Long operatorUserId, List memberUserIds) { - GroupMemberKickNotification notification = new GroupMemberKickNotification() - .setOperatorUserId(operatorUserId).setMemberUserIds(memberUserIds); + public static ImGroupMessageSendDTO ofGroupMemberKick(Long groupId, Long operatorUserId, Collection memberUserIds) { + GroupMemberKickNotification notification = new GroupMemberKickNotification(); + notification.setOperatorUserId(operatorUserId); + notification.setMemberUserIds(new ArrayList<>(memberUserIds)); return new ImGroupMessageSendDTO().setGroupId(groupId) .setType(ImMessageTypeEnum.GROUP_MEMBER_KICK.getType()).setContent(notification); } - public static ImGroupMessageSendDTO ofGroupMemberInvite(Long groupId, Long operatorUserId, List memberUserIds) { - GroupMemberInviteNotification notification = new GroupMemberInviteNotification() - .setOperatorUserId(operatorUserId).setMemberUserIds(memberUserIds); + public static ImGroupMessageSendDTO ofGroupMemberInvite(Long groupId, Long operatorUserId, Collection memberUserIds) { + GroupMemberInviteNotification notification = new GroupMemberInviteNotification(); + notification.setOperatorUserId(operatorUserId); + notification.setMemberUserIds(new ArrayList<>(memberUserIds)); return new ImGroupMessageSendDTO().setGroupId(groupId) .setType(ImMessageTypeEnum.GROUP_MEMBER_INVITE.getType()).setContent(notification); } public static ImGroupMessageSendDTO ofGroupDissolve(Long groupId, Long operatorUserId) { - GroupDissolveNotification notification = new GroupDissolveNotification() - .setOperatorUserId(operatorUserId); + GroupDissolveNotification notification = new GroupDissolveNotification(); + notification.setOperatorUserId(operatorUserId); return new ImGroupMessageSendDTO().setGroupId(groupId) .setType(ImMessageTypeEnum.GROUP_DISSOLVE.getType()).setContent(notification); } public static ImGroupMessageSendDTO ofGroupMemberNicknameUpdate(Long groupId, Long operatorUserId, String displayUserName) { - GroupMemberNicknameUpdateNotification notification = new GroupMemberNicknameUpdateNotification() - .setOperatorUserId(operatorUserId).setDisplayUserName(displayUserName); + GroupMemberNicknameUpdateNotification notification = new GroupMemberNicknameUpdateNotification(); + notification.setOperatorUserId(operatorUserId); + notification.setDisplayUserName(displayUserName); return new ImGroupMessageSendDTO().setGroupId(groupId) .setType(ImMessageTypeEnum.GROUP_MEMBER_NICKNAME_UPDATE.getType()).setContent(notification); } - public static ImGroupMessageSendDTO ofGroupAdminAdd(Long groupId, Long operatorUserId, List memberUserIds) { - GroupAdminAddNotification notification = new GroupAdminAddNotification() - .setOperatorUserId(operatorUserId).setMemberUserIds(memberUserIds); + public static ImGroupMessageSendDTO ofGroupAdminAdd(Long groupId, Long operatorUserId, Collection memberUserIds) { + GroupAdminAddNotification notification = new GroupAdminAddNotification(); + notification.setOperatorUserId(operatorUserId); + notification.setMemberUserIds(new ArrayList<>(memberUserIds)); return new ImGroupMessageSendDTO().setGroupId(groupId) .setType(ImMessageTypeEnum.GROUP_ADMIN_ADD.getType()).setContent(notification); } - public static ImGroupMessageSendDTO ofGroupAdminRemove(Long groupId, Long operatorUserId, List memberUserIds) { - GroupAdminRemoveNotification notification = new GroupAdminRemoveNotification() - .setOperatorUserId(operatorUserId).setMemberUserIds(memberUserIds); + public static ImGroupMessageSendDTO ofGroupAdminRemove(Long groupId, Long operatorUserId, Collection memberUserIds) { + GroupAdminRemoveNotification notification = new GroupAdminRemoveNotification(); + notification.setOperatorUserId(operatorUserId); + notification.setMemberUserIds(new ArrayList<>(memberUserIds)); return new ImGroupMessageSendDTO().setGroupId(groupId) .setType(ImMessageTypeEnum.GROUP_ADMIN_REMOVE.getType()).setContent(notification); } public static ImGroupMessageSendDTO ofGroupNoticeUpdate(Long groupId, Long operatorUserId, String oldNotice, String newNotice) { - GroupNoticeUpdateNotification notification = new GroupNoticeUpdateNotification() - .setOperatorUserId(operatorUserId).setOldNotice(oldNotice).setNewNotice(newNotice); + GroupNoticeUpdateNotification notification = new GroupNoticeUpdateNotification(); + notification.setOperatorUserId(operatorUserId); + notification.setOldNotice(oldNotice); + notification.setNewNotice(newNotice); return new ImGroupMessageSendDTO().setGroupId(groupId) .setType(ImMessageTypeEnum.GROUP_NOTICE_UPDATE.getType()).setContent(notification); } public static ImGroupMessageSendDTO ofGroupNameUpdate(Long groupId, Long operatorUserId, String oldName, String newName) { - GroupNameUpdateNotification notification = new GroupNameUpdateNotification() - .setOperatorUserId(operatorUserId).setOldName(oldName).setNewName(newName); + GroupNameUpdateNotification notification = new GroupNameUpdateNotification(); + notification.setOperatorUserId(operatorUserId); + notification.setOldName(oldName); + notification.setNewName(newName); return new ImGroupMessageSendDTO().setGroupId(groupId) .setType(ImMessageTypeEnum.GROUP_NAME_UPDATE.getType()).setContent(notification); } public static ImGroupMessageSendDTO ofGroupMemberSettingUpdate(Long groupId, Long operatorUserId, Boolean muted, String groupRemark) { - GroupMemberSettingUpdateNotification notification = new GroupMemberSettingUpdateNotification() - .setOperatorUserId(operatorUserId).setMuted(muted).setGroupRemark(groupRemark); + GroupMemberSettingUpdateNotification notification = new GroupMemberSettingUpdateNotification(); + notification.setOperatorUserId(operatorUserId); + notification.setMuted(muted); + notification.setGroupRemark(groupRemark); return new ImGroupMessageSendDTO().setGroupId(groupId) .setType(ImMessageTypeEnum.GROUP_MEMBER_SETTING_UPDATE.getType()).setContent(notification); } diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/BaseGroupNotification.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/BaseGroupNotification.java new file mode 100644 index 0000000..781851d --- /dev/null +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/BaseGroupNotification.java @@ -0,0 +1,16 @@ +package cn.iocoder.yudao.module.im.service.websocket.dto.notification; + +import lombok.Data; + +/** + * 群事件通知基类:所有群事件 payload 共享 operatorUserId + */ +@Data +public abstract class BaseGroupNotification { + + /** + * 操作人用户编号 + */ + private Long operatorUserId; + +} diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupAdminAddNotification.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupAdminAddNotification.java index 5b70c05..be15f9a 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupAdminAddNotification.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupAdminAddNotification.java @@ -1,22 +1,7 @@ package cn.iocoder.yudao.module.im.service.websocket.dto.notification; -import lombok.Data; - -import java.util.List; - /** - * 添加管理员事件通知 + * 添加管理员事件通知(memberUserIds 为被设管理员的成员) */ -@Data -public class GroupAdminAddNotification { - - /** - * 操作人(群主)用户编号 - */ - private Long operatorUserId; - /** - * 被设为管理员的成员用户编号列表 - */ - private List memberUserIds; - +public class GroupAdminAddNotification extends MembersAffectedNotification { } diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupAdminRemoveNotification.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupAdminRemoveNotification.java index 85ae821..6620d5d 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupAdminRemoveNotification.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupAdminRemoveNotification.java @@ -1,22 +1,7 @@ package cn.iocoder.yudao.module.im.service.websocket.dto.notification; -import lombok.Data; - -import java.util.List; - /** - * 撤销管理员事件通知 + * 撤销管理员事件通知(memberUserIds 为被撤销管理员的成员) */ -@Data -public class GroupAdminRemoveNotification { - - /** - * 操作人(群主)用户编号 - */ - private Long operatorUserId; - /** - * 被撤销管理员的成员用户编号列表 - */ - private List memberUserIds; - +public class GroupAdminRemoveNotification extends MembersAffectedNotification { } diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupCreateNotification.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupCreateNotification.java index fdcb90d..862afe7 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupCreateNotification.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupCreateNotification.java @@ -1,22 +1,7 @@ package cn.iocoder.yudao.module.im.service.websocket.dto.notification; -import lombok.Data; - -import java.util.List; - /** - * 群创建事件通知 + * 群创建事件通知(memberUserIds 含创建者 + 初始邀请成员) */ -@Data -public class GroupCreateNotification { - - /** - * 操作人用户编号 - */ - private Long operatorUserId; - /** - * 群成员用户编号列表(含创建者 + 初始邀请成员) - */ - private List memberUserIds; - +public class GroupCreateNotification extends MembersAffectedNotification { } diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupDissolveNotification.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupDissolveNotification.java index cf066af..5392bf4 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupDissolveNotification.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupDissolveNotification.java @@ -1,16 +1,7 @@ package cn.iocoder.yudao.module.im.service.websocket.dto.notification; -import lombok.Data; - /** * 群解散事件通知 */ -@Data -public class GroupDissolveNotification { - - /** - * 操作人(群主)用户编号 - */ - private Long operatorUserId; - +public class GroupDissolveNotification extends BaseGroupNotification { } diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupInfoUpdateNotification.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupInfoUpdateNotification.java index 94c8d9b..3f629e5 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupInfoUpdateNotification.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupInfoUpdateNotification.java @@ -3,16 +3,12 @@ package cn.iocoder.yudao.module.im.service.websocket.dto.notification; import lombok.Data; /** - * 群信息变更事件通知(NAME / NOTICE 之外字段的兜底) + * 群信息变更事件通知(当前承载头像变更,NAME / NOTICE 走独立事件) */ @Data -public class GroupInfoUpdateNotification { +public class GroupInfoUpdateNotification extends BaseGroupNotification { /** - * 操作人用户编号 - */ - private Long operatorUserId; - /** * 旧群头像 */ private String oldAvatar; diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberInviteNotification.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberInviteNotification.java index 6eb3178..64a3bfd 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberInviteNotification.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberInviteNotification.java @@ -1,22 +1,7 @@ package cn.iocoder.yudao.module.im.service.websocket.dto.notification; -import lombok.Data; - -import java.util.List; - /** - * 成员加入事件通知 + * 成员加入事件通知(memberUserIds 为被邀请人) */ -@Data -public class GroupMemberInviteNotification { - - /** - * 操作人(邀请人)用户编号 - */ - private Long operatorUserId; - /** - * 被邀请人用户编号列表 - */ - private List memberUserIds; - +public class GroupMemberInviteNotification extends MembersAffectedNotification { } diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberKickNotification.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberKickNotification.java index eac854a..4f4a0e3 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberKickNotification.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberKickNotification.java @@ -1,22 +1,7 @@ package cn.iocoder.yudao.module.im.service.websocket.dto.notification; -import lombok.Data; - -import java.util.List; - /** - * 成员被移出事件通知 + * 成员被移出事件通知(memberUserIds 为被移出成员) */ -@Data -public class GroupMemberKickNotification { - - /** - * 操作人(群主 / 管理员)用户编号 - */ - private Long operatorUserId; - /** - * 被移出的成员用户编号列表 - */ - private List memberUserIds; - +public class GroupMemberKickNotification extends MembersAffectedNotification { } diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberNicknameUpdateNotification.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberNicknameUpdateNotification.java index f342b20..2d8020f 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberNicknameUpdateNotification.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberNicknameUpdateNotification.java @@ -6,13 +6,9 @@ import lombok.Data; * 成员组内昵称变更事件通知 */ @Data -public class GroupMemberNicknameUpdateNotification { +public class GroupMemberNicknameUpdateNotification extends BaseGroupNotification { /** - * 操作人(昵称变更者本人)用户编号 - */ - private Long operatorUserId; - /** * 群内昵称 */ private String displayUserName; diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberQuitNotification.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberQuitNotification.java index 4718513..406aed1 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberQuitNotification.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberQuitNotification.java @@ -1,16 +1,7 @@ package cn.iocoder.yudao.module.im.service.websocket.dto.notification; -import lombok.Data; - /** * 成员退群事件通知 */ -@Data -public class GroupMemberQuitNotification { - - /** - * 操作人(退群者本人)用户编号 - */ - private Long operatorUserId; - +public class GroupMemberQuitNotification extends BaseGroupNotification { } diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberSettingUpdateNotification.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberSettingUpdateNotification.java index 0a2496e..7183393 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberSettingUpdateNotification.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMemberSettingUpdateNotification.java @@ -4,20 +4,18 @@ import lombok.Data; /** * 群成员个人设置变更事件通知(个人多端同步) + *

+ * muted / groupRemark 字段 null 表示本次未变更,前端按非 null 局部更新 */ @Data -public class GroupMemberSettingUpdateNotification { +public class GroupMemberSettingUpdateNotification extends BaseGroupNotification { /** - * 操作人(设置变更者本人)用户编号 - */ - private Long operatorUserId; - /** - * 群免打扰,null 表示本次未变更 + * 群免打扰 */ private Boolean muted; /** - * 群备注,null 表示本次未变更 + * 群备注 */ private String groupRemark; diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupNameUpdateNotification.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupNameUpdateNotification.java index 3a076b7..0d95571 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupNameUpdateNotification.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupNameUpdateNotification.java @@ -6,13 +6,9 @@ import lombok.Data; * 群名变更事件通知 */ @Data -public class GroupNameUpdateNotification { +public class GroupNameUpdateNotification extends BaseGroupNotification { /** - * 操作人用户编号 - */ - private Long operatorUserId; - /** * 旧群名 */ private String oldName; diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupNoticeUpdateNotification.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupNoticeUpdateNotification.java index ac1b09f..c52fb1d 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupNoticeUpdateNotification.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupNoticeUpdateNotification.java @@ -6,13 +6,9 @@ import lombok.Data; * 群公告变更事件通知 */ @Data -public class GroupNoticeUpdateNotification { +public class GroupNoticeUpdateNotification extends BaseGroupNotification { /** - * 操作人用户编号 - */ - private Long operatorUserId; - /** * 旧群公告 */ private String oldNotice; diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupOwnerTransferNotification.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupOwnerTransferNotification.java index 0faa251..e099ab0 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupOwnerTransferNotification.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupOwnerTransferNotification.java @@ -6,13 +6,9 @@ import lombok.Data; * 群主转让事件通知 */ @Data -public class GroupOwnerTransferNotification { +public class GroupOwnerTransferNotification extends BaseGroupNotification { /** - * 操作人(旧群主)用户编号 - */ - private Long operatorUserId; - /** * 新群主用户编号 */ private Long newOwnerUserId; diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/MembersAffectedNotification.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/MembersAffectedNotification.java new file mode 100644 index 0000000..5c82a13 --- /dev/null +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/MembersAffectedNotification.java @@ -0,0 +1,18 @@ +package cn.iocoder.yudao.module.im.service.websocket.dto.notification; + +import lombok.Data; + +import java.util.List; + +/** + * 涉及成员列表的群事件通知基类:用于 Create / Invite / Kick / AdminAdd / AdminRemove + */ +@Data +public abstract class MembersAffectedNotification extends BaseGroupNotification { + + /** + * 受影响的成员用户编号列表 + */ + private List memberUserIds; + +} diff --git a/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/sensitiveword/ImSensitiveWordServiceImplTest.java b/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/sensitiveword/ImSensitiveWordServiceImplTest.java index f04a5df..4423626 100644 --- a/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/sensitiveword/ImSensitiveWordServiceImplTest.java +++ b/yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/sensitiveword/ImSensitiveWordServiceImplTest.java @@ -2,17 +2,23 @@ package cn.iocoder.yudao.module.im.service.sensitiveword; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.im.controller.admin.manager.sensitiveword.vo.ImSensitiveWordSaveReqVO; import cn.iocoder.yudao.module.im.dal.dataobject.sensitiveword.ImSensitiveWordDO; import cn.iocoder.yudao.module.im.dal.mysql.sensitiveword.ImSensitiveWordMapper; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; +import java.time.LocalDateTime; import java.util.List; import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.MESSAGE_SENSITIVE_WORD_BLOCKED; +import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.SENSITIVE_WORD_DUPLICATED; +import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.SENSITIVE_WORD_NOT_EXISTS; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; @@ -23,6 +29,8 @@ import static org.mockito.Mockito.*; */ public class ImSensitiveWordServiceImplTest extends BaseMockitoUnitTest { + private static final Long TENANT_ID = 1L; + @InjectMocks private ImSensitiveWordServiceImpl sensitiveWordService; @@ -31,15 +39,23 @@ public class ImSensitiveWordServiceImplTest extends BaseMockitoUnitTest { @BeforeEach public void setUp() { - // 模拟 @PostConstruct 初始化:加载 2 个启用的敏感词 - when(imSensitiveWordMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus())) + // 设置租户上下文;validateText 与 loadFresh 都依赖 TenantContextHolder + TenantContextHolder.setTenantId(TENANT_ID); + // mock 启用敏感词列表;LoadingCache 在首次 validateText 时懒加载,无需主动 init + // 用 lenient 避免 null / empty 等不触发 cache load 的用例报 UnnecessaryStubbing + lenient().when(imSensitiveWordMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus())) .thenReturn(List.of( ImSensitiveWordDO.builder().id(1L).word("badword") .status(CommonStatusEnum.ENABLE.getStatus()).build(), ImSensitiveWordDO.builder().id(2L).word("违禁词") .status(CommonStatusEnum.ENABLE.getStatus()).build() )); -// sensitiveWordService.init(); + } + + @AfterEach + public void tearDown() { + // 清理租户上下文,避免污染其它测试 + TenantContextHolder.clear(); } @Test @@ -74,24 +90,188 @@ public class ImSensitiveWordServiceImplTest extends BaseMockitoUnitTest { } @Test - public void testRefresh_reloadsFromMapper() { - // 刷新前:init 调用 mapper 一次 - verify(imSensitiveWordMapper).selectListByStatus(CommonStatusEnum.ENABLE.getStatus()); + public void testValidateText_lazyLoadsCacheOnFirstCall() { + // 调用:首次 validateText 应触发 cache load + sensitiveWordService.validateText("hello world"); + + // 断言:mapper 各调用 1 次(loadFresh 里先取 maxUpdateTime 再读词库) + verify(imSensitiveWordMapper, times(1)).selectMaxUpdateTime(TENANT_ID); + verify(imSensitiveWordMapper, times(1)).selectListByStatus(CommonStatusEnum.ENABLE.getStatus()); + } + + @Test + public void testValidateText_reusesCachedBsAcrossCalls() { + // 调用:连续两次 validateText + sensitiveWordService.validateText("hello world"); + sensitiveWordService.validateText("another text"); + + // 断言:第二次复用 cache,mapper 仍只被调用 1 次 + verify(imSensitiveWordMapper, times(1)).selectMaxUpdateTime(TENANT_ID); + verify(imSensitiveWordMapper, times(1)).selectListByStatus(CommonStatusEnum.ENABLE.getStatus()); + } + + @Test + public void testCreateSensitiveWord_invalidatesCacheAndReloadsOnNextValidate() { + // 准备:首次 validateText 触发 cache load(旧词库) + sensitiveWordService.validateText("hello world"); + verify(imSensitiveWordMapper, times(1)).selectListByStatus(CommonStatusEnum.ENABLE.getStatus()); + + // 准备:mapper 返回新词库(额外多一个 newbad),并让 createSensitiveWord 走通 + when(imSensitiveWordMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus())) + .thenReturn(List.of( + ImSensitiveWordDO.builder().id(1L).word("badword") + .status(CommonStatusEnum.ENABLE.getStatus()).build(), + ImSensitiveWordDO.builder().id(2L).word("违禁词") + .status(CommonStatusEnum.ENABLE.getStatus()).build(), + ImSensitiveWordDO.builder().id(3L).word("newbad") + .status(CommonStatusEnum.ENABLE.getStatus()).build() + )); + when(imSensitiveWordMapper.selectByWord("newbad")).thenReturn(null); + + // 调用:新增敏感词,触发 invalidate + ImSensitiveWordSaveReqVO reqVO = new ImSensitiveWordSaveReqVO(); + reqVO.setWord("newbad"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + sensitiveWordService.createSensitiveWord(reqVO); + + // 调用:再次 validateText,应触发重新 load + ServiceException exception = assertThrows(ServiceException.class, + () -> sensitiveWordService.validateText("contains newbad here")); + assertEquals(MESSAGE_SENSITIVE_WORD_BLOCKED.getCode(), exception.getCode()); + // 旧词依然命中 + assertThrows(ServiceException.class, + () -> sensitiveWordService.validateText("contains badword here")); + + // 断言:selectListByStatus 共被调用 2 次(首次 load + invalidate 后 reload) + verify(imSensitiveWordMapper, times(2)).selectListByStatus(CommonStatusEnum.ENABLE.getStatus()); + } + + @Test + public void testCreateSensitiveWord_duplicateWord_throws() { + // 准备:mock 已存在同名敏感词 + when(imSensitiveWordMapper.selectByWord("dup")).thenReturn( + ImSensitiveWordDO.builder().id(99L).word("dup") + .status(CommonStatusEnum.ENABLE.getStatus()).build()); + + // 调用 + 断言:重复敏感词抛 SENSITIVE_WORD_DUPLICATED + ImSensitiveWordSaveReqVO reqVO = new ImSensitiveWordSaveReqVO(); + reqVO.setWord("dup"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + ServiceException exception = assertThrows(ServiceException.class, + () -> sensitiveWordService.createSensitiveWord(reqVO)); + assertEquals(SENSITIVE_WORD_DUPLICATED.getCode(), exception.getCode()); + + // 断言:未走到 insert + verify(imSensitiveWordMapper, never()).insert(any(ImSensitiveWordDO.class)); + } + + @Test + public void testUpdateSensitiveWord_invalidatesCache() { + // 准备:首次 validateText 触发 cache load + sensitiveWordService.validateText("hello world"); + verify(imSensitiveWordMapper, times(1)).selectListByStatus(CommonStatusEnum.ENABLE.getStatus()); + + // 准备:让 update 校验通过 + when(imSensitiveWordMapper.selectById(1L)).thenReturn( + ImSensitiveWordDO.builder().id(1L).word("badword") + .status(CommonStatusEnum.ENABLE.getStatus()).build()); + when(imSensitiveWordMapper.selectByWord("updatedbad")).thenReturn(null); + // 准备:reload 时返回新词库 + when(imSensitiveWordMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus())) + .thenReturn(List.of( + ImSensitiveWordDO.builder().id(1L).word("updatedbad") + .status(CommonStatusEnum.ENABLE.getStatus()).build() + )); + + // 调用:更新敏感词,触发 invalidate + ImSensitiveWordSaveReqVO reqVO = new ImSensitiveWordSaveReqVO(); + reqVO.setId(1L); + reqVO.setWord("updatedbad"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + sensitiveWordService.updateSensitiveWord(reqVO); + + // 调用 + 断言:再次 validateText 应使用新词库 + ServiceException exception = assertThrows(ServiceException.class, + () -> sensitiveWordService.validateText("contains updatedbad here")); + assertEquals(MESSAGE_SENSITIVE_WORD_BLOCKED.getCode(), exception.getCode()); + + // 断言:selectListByStatus 共被调用 2 次 + verify(imSensitiveWordMapper, times(2)).selectListByStatus(CommonStatusEnum.ENABLE.getStatus()); + } + + @Test + public void testDeleteSensitiveWord_invalidatesCache() { + // 准备:首次 validateText 触发 cache load + sensitiveWordService.validateText("hello world"); + verify(imSensitiveWordMapper, times(1)).selectListByStatus(CommonStatusEnum.ENABLE.getStatus()); - // 准备:新列表不含原词 + // 准备:让 delete 校验通过 + when(imSensitiveWordMapper.selectById(1L)).thenReturn( + ImSensitiveWordDO.builder().id(1L).word("badword") + .status(CommonStatusEnum.ENABLE.getStatus()).build()); + // 准备:reload 时返回剩余词库(只剩中文那条) when(imSensitiveWordMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus())) .thenReturn(List.of( - ImSensitiveWordDO.builder().id(3L).word("new").status(CommonStatusEnum.ENABLE.getStatus()).build() + ImSensitiveWordDO.builder().id(2L).word("违禁词") + .status(CommonStatusEnum.ENABLE.getStatus()).build() )); - // 调用:刷新 -// sensitiveWordService.refresh(); + // 调用:删除 badword,触发 invalidate + sensitiveWordService.deleteSensitiveWord(1L); - // 断言:原敏感词不再命中,新词命中 - assertDoesNotThrow(() -> sensitiveWordService.validateText("contains badword")); + // 调用 + 断言:badword 已被删,不再命中 + assertDoesNotThrow(() -> sensitiveWordService.validateText("contains badword here")); + // 中文词依然在 assertThrows(ServiceException.class, - () -> sensitiveWordService.validateText("contains new")); + () -> sensitiveWordService.validateText("这条消息里有违禁词哦")); + + // 断言:selectListByStatus 共被调用 2 次 verify(imSensitiveWordMapper, times(2)).selectListByStatus(CommonStatusEnum.ENABLE.getStatus()); } + @Test + public void testDeleteSensitiveWord_notExists_throws() { + // 准备:selectById 返回 null + when(imSensitiveWordMapper.selectById(999L)).thenReturn(null); + + // 调用 + 断言:抛 SENSITIVE_WORD_NOT_EXISTS + ServiceException exception = assertThrows(ServiceException.class, + () -> sensitiveWordService.deleteSensitiveWord(999L)); + assertEquals(SENSITIVE_WORD_NOT_EXISTS.getCode(), exception.getCode()); + + // 断言:未走到 deleteById + verify(imSensitiveWordMapper, never()).deleteById(anyLong()); + } + + @Test + public void testDeleteSensitiveWordList_invalidatesCache() { + // 准备:首次 validateText 触发 cache load + sensitiveWordService.validateText("hello world"); + verify(imSensitiveWordMapper, times(1)).selectListByStatus(CommonStatusEnum.ENABLE.getStatus()); + + // 准备:reload 时返回空词库 + when(imSensitiveWordMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus())) + .thenReturn(List.of()); + + // 调用:批量删除,触发 invalidate + sensitiveWordService.deleteSensitiveWordList(List.of(1L, 2L)); + + // 调用 + 断言:所有词都不再命中 + assertDoesNotThrow(() -> sensitiveWordService.validateText("contains badword here")); + assertDoesNotThrow(() -> sensitiveWordService.validateText("这条消息里有违禁词哦")); + + // 断言:deleteByIds 被调用 1 次;selectListByStatus 共 2 次 + verify(imSensitiveWordMapper, times(1)).deleteByIds(List.of(1L, 2L)); + verify(imSensitiveWordMapper, times(2)).selectListByStatus(CommonStatusEnum.ENABLE.getStatus()); + } + + @Test + public void testDeleteSensitiveWordList_emptyIds_skip() { + // 调用:空列表直接返回 + sensitiveWordService.deleteSensitiveWordList(List.of()); + + // 断言:mapper 不被调用 + verify(imSensitiveWordMapper, never()).deleteByIds(anyList()); + } + } -- libgit2 0.21.4