Commit ea29dea47c77260e76771224d95a21d8246ac730

Authored by YunaiV
1 parent 886f4fe1

✨ feat(group): 添加群解散功能及提示消息

实现群解散功能,允许群主解散群聊并通知所有成员。新增群解散提示消息格式,并在解散时推送给群成员。增加了校验用户是否为群主的方法,以确保操作的合法性。
yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/enums/ImCommonConstants.java
@@ -45,4 +45,11 @@ public interface ImCommonConstants { @@ -45,4 +45,11 @@ public interface ImCommonConstants {
45 */ 45 */
46 String FRIEND_DELETE_TIP_MESSAGE = "你们的好友关系已被解除"; 46 String FRIEND_DELETE_TIP_MESSAGE = "你们的好友关系已被解除";
47 47
  48 + // ==================== 群相关提示消息 ====================
  49 +
  50 + /**
  51 + * 群解散系统提示
  52 + */
  53 + String GROUP_DISSOLVE_TIP_MESSAGE = "'{}'解散了群聊";
  54 +
48 } 55 }
yudao-module-im/yudao-module-im-api/src/main/java/cn/iocoder/yudao/module/im/enums/message/ImMessageTypeEnum.java
@@ -33,7 +33,8 @@ public enum ImMessageTypeEnum implements ArrayValuable<Integer> { @@ -33,7 +33,8 @@ public enum ImMessageTypeEnum implements ArrayValuable<Integer> {
33 FRIEND_UPDATE(102, "好友更新"), // 对应 FriendUpdateMessage 类 33 FRIEND_UPDATE(102, "好友更新"), // 对应 FriendUpdateMessage 类
34 34
35 GROUP_CREATE(200, "群创建"), // 暂无 35 GROUP_CREATE(200, "群创建"), // 暂无
36 - GROUP_UPDATE(201, "群信息变更"); // 暂无 36 + GROUP_UPDATE(201, "群信息变更"), // 暂无
  37 + GROUP_DISSOLVE(202, "群解散"); // 暂无
37 38
38 public static final Integer[] ARRAYS = Arrays.stream(values()).map(ImMessageTypeEnum::getType).toArray(Integer[]::new); 39 public static final Integer[] ARRAYS = Arrays.stream(values()).map(ImMessageTypeEnum::getType).toArray(Integer[]::new);
39 40
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupService.java
@@ -61,6 +61,16 @@ public interface ImGroupService { @@ -61,6 +61,16 @@ public interface ImGroupService {
61 ImGroupDO validateGroupExists(Long groupId); 61 ImGroupDO validateGroupExists(Long groupId);
62 62
63 /** 63 /**
  64 + * 校验当前用户是否为群主
  65 + *
  66 + * @param groupId 群编号
  67 + * @param userId 用户编号
  68 + * @return 群信息
  69 + */
  70 + @SuppressWarnings("UnusedReturnValue")
  71 + ImGroupDO validateGroupOwner(Long groupId, Long userId);
  72 +
  73 + /**
64 * 获取指定用户加入的所有"有效"群(未退群且未封禁/解散) 74 * 获取指定用户加入的所有"有效"群(未退群且未封禁/解散)
65 * <p> 75 * <p>
66 * 返回当前登录用户在群内身份仍有效的所有群。 76 * 返回当前登录用户在群内身份仍有效的所有群。
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImpl.java
@@ -4,6 +4,7 @@ import cn.hutool.core.bean.BeanUtil; @@ -4,6 +4,7 @@ import cn.hutool.core.bean.BeanUtil;
4 import cn.hutool.core.bean.copier.CopyOptions; 4 import cn.hutool.core.bean.copier.CopyOptions;
5 import cn.hutool.core.collection.CollUtil; 5 import cn.hutool.core.collection.CollUtil;
6 import cn.hutool.core.util.ObjUtil; 6 import cn.hutool.core.util.ObjUtil;
  7 +import cn.hutool.core.util.StrUtil;
7 import cn.hutool.extra.spring.SpringUtil; 8 import cn.hutool.extra.spring.SpringUtil;
8 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; 9 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
9 import cn.iocoder.yudao.framework.common.util.object.BeanUtils; 10 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
@@ -13,6 +14,8 @@ import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO; @@ -13,6 +14,8 @@ import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO;
13 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; 14 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO;
14 import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupMapper; 15 import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupMapper;
15 import cn.iocoder.yudao.module.im.service.message.ImGroupMessageService; 16 import cn.iocoder.yudao.module.im.service.message.ImGroupMessageService;
  17 +import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
  18 +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
16 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService; 19 import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService;
17 import cn.iocoder.yudao.module.im.service.websocket.dto.ImGroupMessageDTO; 20 import cn.iocoder.yudao.module.im.service.websocket.dto.ImGroupMessageDTO;
18 import jakarta.annotation.Resource; 21 import jakarta.annotation.Resource;
@@ -32,6 +35,7 @@ import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionU @@ -32,6 +35,7 @@ import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionU
32 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; 35 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
33 import static cn.iocoder.yudao.module.im.dal.redis.RedisKeyConstants.GROUP; 36 import static cn.iocoder.yudao.module.im.dal.redis.RedisKeyConstants.GROUP;
34 import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*; 37 import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*;
  38 +import static cn.iocoder.yudao.module.im.enums.ImCommonConstants.GROUP_DISSOLVE_TIP_MESSAGE;
35 39
36 // TODO @芋艿:群主的调整,暂时不考虑。后续在弄; 40 // TODO @芋艿:群主的调整,暂时不考虑。后续在弄;
37 /** 41 /**
@@ -55,6 +59,9 @@ public class ImGroupServiceImpl implements ImGroupService { @@ -55,6 +59,9 @@ public class ImGroupServiceImpl implements ImGroupService {
55 @Resource 59 @Resource
56 private ImWebSocketService webSocketService; 60 private ImWebSocketService webSocketService;
57 61
  62 + @Resource
  63 + private AdminUserApi adminUserApi;
  64 +
58 @Override 65 @Override
59 @Transactional(rollbackFor = Exception.class) 66 @Transactional(rollbackFor = Exception.class)
60 public ImGroupDO createGroup(ImGroupCreateReqVO createReqVO, Long userId) { 67 public ImGroupDO createGroup(ImGroupCreateReqVO createReqVO, Long userId) {
@@ -112,29 +119,29 @@ public class ImGroupServiceImpl implements ImGroupService { @@ -112,29 +119,29 @@ public class ImGroupServiceImpl implements ImGroupService {
112 @CacheEvict(cacheNames = GROUP, key = "#id") 119 @CacheEvict(cacheNames = GROUP, key = "#id")
113 @Transactional(rollbackFor = Exception.class) 120 @Transactional(rollbackFor = Exception.class)
114 public void dissolveGroup(Long id, Long userId) { 121 public void dissolveGroup(Long id, Long userId) {
115 - // 1.1 校验群存在  
116 - ImGroupDO group = validateGroupExists(id);  
117 - // 1.2 校验当前用户是群主  
118 - if (ObjUtil.notEqual(group.getOwnerUserId(), userId)) {  
119 - throw exception(GROUP_NOT_OWNER);  
120 - }  
121 - // TODO @AI:增加一个是否为群主的 public 方法;因为别的模块也需要使用;  
122 -  
123 - // 3. 更新群状态为已解散  
124 - // TODO @AI:链式调用;  
125 - ImGroupDO updateObj = new ImGroupDO();  
126 - updateObj.setId(id);  
127 - updateObj.setStatus(CommonStatusEnum.DISABLE.getStatus());  
128 - updateObj.setDissolvedTime(LocalDateTime.now());  
129 - groupMapper.updateById(updateObj);  
130 -  
131 - // 4. 清理已读缓存 122 + // 1. 校验群存在 + 当前用户是群主
  123 + validateGroupOwner(id, userId);
  124 +
  125 + // 2.1 获取群成员列表(在移除之前,后续推送需要)
  126 + List<ImGroupMemberDO> members = groupMemberService.getActiveGroupMemberListByGroupId(id);
  127 + Set<Long> memberUserIds = convertSet(members, ImGroupMemberDO::getUserId);
  128 + // 2.2 更新群状态为已解散
  129 + groupMapper.updateById(new ImGroupDO().setId(id)
  130 + .setStatus(CommonStatusEnum.DISABLE.getStatus()).setDissolvedTime(LocalDateTime.now()));
  131 + // 2.3 移除全部群成员
  132 + groupMemberService.removeGroupMembersByGroupId(id);
  133 + // 2.4 清理已读缓存
132 groupMessageService.deleteReadMaxMessageIdMap(id); 134 groupMessageService.deleteReadMaxMessageIdMap(id);
133 - // TODO @AI:推送群解散的提示;  
134 - // TODO @AI:推送群信息变化; 135 +
  136 + // 3.1 发送解散提示消息(TIP_TEXT)并推送
  137 + AdminUserRespDTO user = adminUserApi.getUser(userId);
  138 + String tipContent = StrUtil.format(GROUP_DISSOLVE_TIP_MESSAGE, user.getNickname());
  139 + groupMessageService.sendTipGroupMessage(userId, id, memberUserIds, tipContent);
  140 + // 3.2 推送群解散事件给所有原群成员
  141 + webSocketService.sendGroupMessageAsync(memberUserIds,
  142 + ImGroupMessageDTO.ofGroupDissolve(userId, id));
135 } 143 }
136 144
137 - // TODO @AI:缓存还是加在这个方法上;不加在 getGroup 上;  
138 @Override 145 @Override
139 public ImGroupDO validateGroupExists(Long id) { 146 public ImGroupDO validateGroupExists(Long id) {
140 ImGroupDO group = getSelf().getGroup(id); 147 ImGroupDO group = getSelf().getGroup(id);
@@ -151,6 +158,15 @@ public class ImGroupServiceImpl implements ImGroupService { @@ -151,6 +158,15 @@ public class ImGroupServiceImpl implements ImGroupService {
151 } 158 }
152 159
153 @Override 160 @Override
  161 + public ImGroupDO validateGroupOwner(Long groupId, Long userId) {
  162 + ImGroupDO group = validateGroupExists(groupId);
  163 + if (ObjUtil.notEqual(group.getOwnerUserId(), userId)) {
  164 + throw exception(GROUP_NOT_OWNER);
  165 + }
  166 + return group;
  167 + }
  168 +
  169 + @Override
154 @Cacheable(cacheNames = GROUP, key = "#id", unless = "#result == null") 170 @Cacheable(cacheNames = GROUP, key = "#id", unless = "#result == null")
155 public ImGroupDO getGroup(Long id) { 171 public ImGroupDO getGroup(Long id) {
156 return groupMapper.selectById(id); 172 return groupMapper.selectById(id);
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageService.java
@@ -5,6 +5,7 @@ import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessa @@ -5,6 +5,7 @@ import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessa
5 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO; 5 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO;
6 6
7 import java.util.List; 7 import java.util.List;
  8 +import java.util.Set;
8 9
9 /** 10 /**
10 * IM 群聊消息 Service 接口 11 * IM 群聊消息 Service 接口
@@ -88,5 +89,17 @@ public interface ImGroupMessageService { @@ -88,5 +89,17 @@ public interface ImGroupMessageService {
88 */ 89 */
89 void deleteReadMaxMessageIdMap(Long groupId); 90 void deleteReadMaxMessageIdMap(Long groupId);
90 91
  92 + /**
  93 + * 发送群聊系统提示消息(TIP_TEXT)
  94 + * <p>
  95 + * 插入一条 TIP_TEXT 消息到群聊记录,并推送给指定用户
  96 + *
  97 + * @param senderId 发送人编号(操作者)
  98 + * @param groupId 群编号
  99 + * @param memberUserIds 推送目标用户编号集合
  100 + * @param content 提示内容
  101 + */
  102 + void sendTipGroupMessage(Long senderId, Long groupId, Set<Long> memberUserIds, String content);
  103 +
91 } 104 }
92 105
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImpl.java
@@ -481,6 +481,22 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { @@ -481,6 +481,22 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService {
481 groupMessageReadRedisDAO.deleteReadMaxMessageIdMap(groupId); 481 groupMessageReadRedisDAO.deleteReadMaxMessageIdMap(groupId);
482 } 482 }
483 483
  484 + @Override
  485 + public void sendTipGroupMessage(Long senderId, Long groupId, Set<Long> memberUserIds, String content) {
  486 + // 1. 插入 TIP_TEXT 消息
  487 + ImGroupMessageDO tipMessage = new ImGroupMessageDO()
  488 + .setClientMessageId(IdUtil.fastSimpleUUID())
  489 + .setSenderId(senderId).setGroupId(groupId)
  490 + .setType(ImMessageTypeEnum.TIP_TEXT.getType()).setContent(content)
  491 + .setStatus(ImMessageStatusEnum.UNREAD.getStatus()).setSendTime(LocalDateTime.now())
  492 + .setReceiptStatus(ImGroupMessageReceiptStatusEnum.NO_RECEIPT.getStatus());
  493 + groupMessageMapper.insert(tipMessage);
  494 +
  495 + // 2. 推送给指定用户
  496 + imWebSocketService.sendGroupMessageAsync(memberUserIds,
  497 + ImGroupMessageDTO.ofSend(tipMessage));
  498 + }
  499 +
484 private ImGroupMessageServiceImpl getSelf() { 500 private ImGroupMessageServiceImpl getSelf() {
485 return SpringUtil.getBean(getClass()); 501 return SpringUtil.getBean(getClass());
486 } 502 }
yudao-module-im/yudao-module-im-biz/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/ImGroupMessageDTO.java
@@ -146,4 +146,18 @@ public class ImGroupMessageDTO { @@ -146,4 +146,18 @@ public class ImGroupMessageDTO {
146 .setSendTime(LocalDateTime.now()); 146 .setSendTime(LocalDateTime.now());
147 } 147 }
148 148
  149 + /**
  150 + * 构建群解散推送 DTO(通知所有群成员"群已解散")
  151 + *
  152 + * @param senderId 操作者(群主)用户编号
  153 + * @param groupId 群编号
  154 + * @return 群聊 DTO
  155 + */
  156 + public static ImGroupMessageDTO ofGroupDissolve(Long senderId, Long groupId) {
  157 + return new ImGroupMessageDTO()
  158 + .setType(ImMessageTypeEnum.GROUP_DISSOLVE.getType())
  159 + .setSenderId(senderId).setGroupId(groupId)
  160 + .setSendTime(LocalDateTime.now());
  161 + }
  162 +
149 } 163 }