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