Commit 6e823f848d40eb9c830d1a9f7d711311a60de038

Authored by YunaiV
1 parent ed9f23ae

✨ feat(im): 增加群消息的置顶

yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupController.java
1 1 package cn.iocoder.yudao.module.im.controller.admin.group;
2 2  
  3 +import cn.hutool.core.collection.CollUtil;
3 4 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  5 +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
4 6 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
5 7 import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupAdminAddReqVO;
6 8 import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupAdminRemoveReqVO;
7 9 import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupCreateReqVO;
  10 +import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupMessagePinReqVO;
8 11 import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupRespVO;
9 12 import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupTransferOwnerReqVO;
10 13 import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupUpdateReqVO;
11 14 import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberInviteReqVO;
12 15 import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberRemoveReqVO;
  16 +import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessageRespVO;
13 17 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO;
  18 +import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO;
  19 +import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO;
  20 +import cn.iocoder.yudao.module.im.service.group.ImGroupMemberService;
14 21 import cn.iocoder.yudao.module.im.service.group.ImGroupService;
  22 +import cn.iocoder.yudao.module.im.service.message.ImGroupMessageService;
15 23 import io.swagger.v3.oas.annotations.Operation;
16 24 import io.swagger.v3.oas.annotations.Parameter;
17 25 import io.swagger.v3.oas.annotations.tags.Tag;
... ... @@ -20,9 +28,17 @@ import jakarta.validation.Valid;
20 28 import org.springframework.validation.annotation.Validated;
21 29 import org.springframework.web.bind.annotation.*;
22 30  
  31 +import java.util.Collections;
23 32 import java.util.List;
  33 +import java.util.Map;
  34 +import java.util.Objects;
  35 +import java.util.Set;
  36 +import java.util.stream.Collectors;
  37 +import java.util.stream.Stream;
24 38  
25 39 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
  40 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
  41 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSetByFlatMap;
26 42 import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getLoginUserId;
27 43  
28 44 @Tag(name = "管理后台 - 群")
... ... @@ -33,6 +49,10 @@ public class ImGroupController {
33 49  
34 50 @Resource
35 51 private ImGroupService groupService;
  52 + @Resource
  53 + private ImGroupMemberService groupMemberService;
  54 + @Resource
  55 + private ImGroupMessageService groupMessageService;
36 56  
37 57 // ==================== 群的写操作 ====================
38 58  
... ... @@ -40,6 +60,7 @@ public class ImGroupController {
40 60 @Operation(summary = "创建群")
41 61 public CommonResult<ImGroupRespVO> createGroup(@Valid @RequestBody ImGroupCreateReqVO createReqVO) {
42 62 ImGroupDO group = groupService.createGroup(createReqVO, getLoginUserId());
  63 + // 新建群必无 pinnedMessages,跳过关联回填
43 64 return success(BeanUtils.toBean(group, ImGroupRespVO.class));
44 65 }
45 66  
... ... @@ -47,7 +68,7 @@ public class ImGroupController {
47 68 @Operation(summary = "更新群")
48 69 public CommonResult<ImGroupRespVO> updateGroup(@Valid @RequestBody ImGroupUpdateReqVO updateReqVO) {
49 70 ImGroupDO group = groupService.updateGroup(updateReqVO, getLoginUserId());
50   - return success(BeanUtils.toBean(group, ImGroupRespVO.class));
  71 + return success(buildGroupRespVO(group, getLoginUserId()));
51 72 }
52 73  
53 74 @DeleteMapping("/dissolve")
... ... @@ -65,14 +86,15 @@ public class ImGroupController {
65 86 @Parameter(name = "id", description = "编号", required = true, example = "1024")
66 87 public CommonResult<ImGroupRespVO> getGroup(@RequestParam("id") Long id) {
67 88 ImGroupDO group = groupService.getGroup(id);
68   - return success(BeanUtils.toBean(group, ImGroupRespVO.class));
  89 + return success(buildGroupRespVO(group, getLoginUserId()));
69 90 }
70 91  
71 92 @GetMapping("/list")
72 93 @Operation(summary = "获得当前登录用户的群列表")
73 94 public CommonResult<List<ImGroupRespVO>> getMyGroupList() {
74   - List<ImGroupDO> groups = groupService.getMyGroupList(getLoginUserId());
75   - return success(BeanUtils.toBean(groups, ImGroupRespVO.class));
  95 + Long loginUserId = getLoginUserId();
  96 + List<ImGroupDO> groups = groupService.getMyGroupList(loginUserId);
  97 + return success(buildGroupRespVOList(groups, loginUserId));
76 98 }
77 99  
78 100 // ==================== 群成员的写操作 ====================
... ... @@ -120,4 +142,59 @@ public class ImGroupController {
120 142 return success(true);
121 143 }
122 144  
  145 + // ==================== 群消息置顶 ====================
  146 +
  147 + @PutMapping("/pin-message")
  148 + @Operation(summary = "置顶群消息(群主 / 管理员)")
  149 + public CommonResult<Boolean> pinGroupMessage(@Valid @RequestBody ImGroupMessagePinReqVO reqVO) {
  150 + groupService.pinGroupMessage(getLoginUserId(), reqVO.getGroupId(), reqVO.getMessageId());
  151 + return success(true);
  152 + }
  153 +
  154 + @PutMapping("/unpin-message")
  155 + @Operation(summary = "取消置顶群消息(群主 / 管理员)")
  156 + public CommonResult<Boolean> unpinGroupMessage(@Valid @RequestBody ImGroupMessagePinReqVO reqVO) {
  157 + groupService.unpinGroupMessage(getLoginUserId(), reqVO.getGroupId(), reqVO.getMessageId());
  158 + return success(true);
  159 + }
  160 +
  161 + /** 单群转 VO + 关联回填 pinnedMessages(仅当登录用户是该群有效成员) */
  162 + private ImGroupRespVO buildGroupRespVO(ImGroupDO group, Long loginUserId) {
  163 + if (group == null) {
  164 + return null;
  165 + }
  166 + return buildGroupRespVOList(Collections.singletonList(group), loginUserId).get(0);
  167 + }
  168 +
  169 + /**
  170 + * 群列表批量转 VO + 关联回填 pinnedMessages
  171 + * <p>
  172 + * 仅当登录用户是某群的有效成员时才回填该群的 pinnedMessages,避免非成员 / 已退群用户越权拿到置顶消息内容
  173 + */
  174 + private List<ImGroupRespVO> buildGroupRespVOList(List<ImGroupDO> groups, Long loginUserId) {
  175 + if (CollUtil.isEmpty(groups)) {
  176 + return Collections.emptyList();
  177 + }
  178 + // 仅当前用户是有效成员的群才允许回填置顶消息(按 groupId 一次性 IN 查成员关系)
  179 + Set<Long> activeGroupIds = convertSet(
  180 + groupMemberService.getActiveGroupMemberListByUserId(loginUserId), ImGroupMemberDO::getGroupId);
  181 + // 把 active 群的 pinnedMessageIds 一次性聚合 IN 查询,避免 N+1
  182 + Set<Long> allMessageIds = convertSetByFlatMap(groups, group -> activeGroupIds.contains(group.getId())
  183 + ? CollUtil.emptyIfNull(group.getPinnedMessageIds()).stream() : Stream.empty());
  184 + Map<Long, ImGroupMessageDO> messageMap = groupMessageService.getGroupMessageMap(allMessageIds);
  185 + return CollectionUtils.convertList(groups, group -> {
  186 + ImGroupRespVO vo = BeanUtils.toBean(group, ImGroupRespVO.class);
  187 + if (!activeGroupIds.contains(group.getId()) || CollUtil.isEmpty(group.getPinnedMessageIds())) {
  188 + return vo;
  189 + }
  190 + // 按 pin 顺序输出,已被删除的消息(messageMap 没命中)跳过
  191 + // TODO @AI:使用 CollUtils convert 进一步简化。
  192 + List<ImGroupMessageRespVO> pinned = group.getPinnedMessageIds().stream()
  193 + .map(messageMap::get).filter(Objects::nonNull)
  194 + .map(msg -> BeanUtils.toBean(msg, ImGroupMessageRespVO.class))
  195 + .collect(Collectors.toList());
  196 + return vo.setPinnedMessages(pinned);
  197 + });
  198 + }
  199 +
123 200 }
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupMessagePinReqVO.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.controller.admin.group.vo;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +import jakarta.validation.constraints.NotNull;
  5 +import lombok.Data;
  6 +
  7 +/**
  8 + * 管理后台 - 群消息置顶 / 取消置顶 Request VO
  9 + */
  10 +@Schema(description = "管理后台 - 群消息置顶 / 取消置顶 Request VO")
  11 +@Data
  12 +public class ImGroupMessagePinReqVO {
  13 +
  14 + @Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13279")
  15 + @NotNull(message = "群编号不能为空")
  16 + private Long groupId;
  17 +
  18 + @Schema(description = "消息编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "9527")
  19 + @NotNull(message = "消息编号不能为空")
  20 + private Long messageId;
  21 +
  22 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupRespVO.java
1 1 package cn.iocoder.yudao.module.im.controller.admin.group.vo;
2 2  
  3 +import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessageRespVO;
3 4 import io.swagger.v3.oas.annotations.media.Schema;
4 5 import lombok.*;
5 6  
6 7 import java.time.LocalDateTime;
  8 +import java.util.List;
7 9  
8 10 @Schema(description = "管理后台 - 群 Response VO")
9 11 @Data
... ... @@ -39,4 +41,7 @@ public class ImGroupRespVO {
39 41 @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
40 42 private LocalDateTime createTime;
41 43  
  44 + @Schema(description = "群置顶消息列表,按 pin 顺序(最先置顶的在前);非该群有效成员时为空")
  45 + private List<ImGroupMessageRespVO> pinnedMessages;
  46 +
42 47 }
43 48 \ No newline at end of file
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/group/ImGroupDO.java
... ... @@ -2,19 +2,22 @@ package cn.iocoder.yudao.module.im.dal.dataobject.group;
2 2  
3 3 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
4 4 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
  5 +import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler;
5 6 import com.baomidou.mybatisplus.annotation.KeySequence;
  7 +import com.baomidou.mybatisplus.annotation.TableField;
6 8 import com.baomidou.mybatisplus.annotation.TableId;
7 9 import com.baomidou.mybatisplus.annotation.TableName;
8 10 import lombok.*;
9 11  
10 12 import java.time.LocalDateTime;
  13 +import java.util.List;
11 14  
12 15 /**
13 16 * IM 群信息 DO
14 17 *
15 18 * @author 芋道源码
16 19 */
17   -@TableName("im_group")
  20 +@TableName(value = "im_group",autoResultMap = true)
18 21 @KeySequence("im_group_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
19 22 @Data
20 23 @EqualsAndHashCode(callSuper = true)
... ... @@ -70,5 +73,12 @@ public class ImGroupDO extends BaseDO {
70 73 * 解散时间
71 74 */
72 75 private LocalDateTime dissolvedTime;
  76 + /**
  77 + * 群置顶消息编号列表
  78 + * <p>
  79 + * 仅存 messageId,操作人 / 置顶时间从对应 PIN 事件的消息记录里反查
  80 + */
  81 + @TableField(typeHandler = LongListTypeHandler.class)
  82 + private List<Long> pinnedMessageIds;
73 83  
74 84 }
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/ErrorCodeConstants.java
... ... @@ -17,6 +17,7 @@ public interface ErrorCodeConstants {
17 17 ErrorCode MESSAGE_PULL_SIZE_EXCEEDED = new ErrorCode(1_040_300_005, "单次拉取消息数量不能超过 {} 条");
18 18 ErrorCode MESSAGE_RECALL_TIMEOUT = new ErrorCode(1_040_300_007, "超过 {} 分钟的消息无法撤回");
19 19 ErrorCode MESSAGE_QUOTE_INVALID = new ErrorCode(1_040_300_008, "引用的消息不可用");
  20 + ErrorCode MESSAGE_NOT_IN_GROUP = new ErrorCode(1_040_300_009, "消息不属于该群");
20 21  
21 22 // ========== 群 (1-040-400-000) ==========
22 23 ErrorCode GROUP_NOT_EXISTS = new ErrorCode(1_040_400_000, "群不存在");
... ... @@ -25,6 +26,9 @@ public interface ErrorCodeConstants {
25 26 ErrorCode GROUP_NOT_OWNER = new ErrorCode(1_040_400_003, "仅群主可执行该操作");
26 27 ErrorCode GROUP_NOT_OWNER_OR_ADMIN = new ErrorCode(1_040_400_004, "仅群主或管理员可执行该操作");
27 28 ErrorCode GROUP_TRANSFER_OWNER_TO_SELF = new ErrorCode(1_040_400_005, "不能将群主转让给自己");
  29 + ErrorCode GROUP_MESSAGE_PIN_MAX_LIMIT = new ErrorCode(1_040_400_006, "群置顶消息数量不能超过 {} 条");
  30 + ErrorCode GROUP_MESSAGE_ALREADY_PINNED = new ErrorCode(1_040_400_007, "该消息已置顶");
  31 + ErrorCode GROUP_MESSAGE_NOT_PINNED = new ErrorCode(1_040_400_008, "该消息未置顶");
28 32  
29 33 // ========== 群成员 (1-040-500-000) ==========
30 34 ErrorCode GROUP_MEMBER_NOT_IN_GROUP = new ErrorCode(1_040_500_001, "您已不在该群中");
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/message/ImMessageTypeEnum.java
... ... @@ -159,7 +159,16 @@ public enum ImMessageTypeEnum implements ArrayValuable&lt;Integer&gt; {
159 159 * 对应自己的类:GroupMemberSettingUpdateNotification
160 160 * 场景:用户改自己的群免打扰 / 群备注,仅推该用户其他在线终端做多端同步
161 161 */
162   - GROUP_MEMBER_SETTING_UPDATE(1530, "群成员个人设置变更", false, false);
  162 + GROUP_MEMBER_SETTING_UPDATE(1530, "群成员个人设置变更", false, false),
  163 + // TODO @AI:注释风格,还是参考上面的
  164 + /**
  165 + * 自有扩展:群主 / 管理员置顶一条群消息,全员广播;前端把 messageId 加入 group.pinnedMessageIds
  166 + */
  167 + GROUP_MESSAGE_PIN(1531, "群消息置顶", true, false),
  168 + /**
  169 + * 自有扩展:群主 / 管理员取消置顶,全员广播;前端把 messageId 从 group.pinnedMessageIds 移除
  170 + */
  171 + GROUP_MESSAGE_UNPIN(1532, "群消息取消置顶", true, false);
163 172  
164 173 public static final Integer[] ARRAYS = Arrays.stream(values()).map(ImMessageTypeEnum::getType).toArray(Integer[]::new);
165 174  
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupService.java
... ... @@ -154,6 +154,26 @@ public interface ImGroupService {
154 154 */
155 155 void transferGroupOwner(Long userId, @Valid ImGroupTransferOwnerReqVO transferReqVO);
156 156  
  157 + /**
  158 + * 置顶群消息(仅群主或管理员可执行)
  159 + * <p>
  160 + * 上限 {@link cn.iocoder.yudao.module.im.enums.ImCommonConstants#GROUP_PIN_MAX_COUNT};幂等失败时抛业务异常
  161 + *
  162 + * @param userId 当前登录用户编号
  163 + * @param groupId 群编号
  164 + * @param messageId 被置顶的消息编号
  165 + */
  166 + void pinGroupMessage(Long userId, Long groupId, Long messageId);
  167 +
  168 + /**
  169 + * 取消置顶群消息(仅群主或管理员可执行)
  170 + *
  171 + * @param userId 当前登录用户编号
  172 + * @param groupId 群编号
  173 + * @param messageId 被取消置顶的消息编号
  174 + */
  175 + void unpinGroupMessage(Long userId, Long groupId, Long messageId);
  176 +
157 177 // ==================== 管理后台 ====================
158 178  
159 179 /**
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImpl.java
... ... @@ -22,8 +22,11 @@ import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.ImGroupManag
22 22 import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO;
23 23 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO;
24 24 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO;
  25 +import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO;
25 26 import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupMapper;
26 27 import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum;
  28 +import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum;
  29 +import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum;
27 30 import cn.iocoder.yudao.module.im.service.friend.ImFriendService;
28 31 import cn.iocoder.yudao.module.im.service.message.ImGroupMessageService;
29 32 import cn.iocoder.yudao.module.im.service.message.dto.ImGroupMessageSendDTO;
... ... @@ -363,6 +366,68 @@ public class ImGroupServiceImpl implements ImGroupService {
363 366 ImGroupMessageSendDTO.ofGroupOwnerTransfer(groupId, userId, newOwnerUserId));
364 367 }
365 368  
  369 + @Override
  370 + @CacheEvict(cacheNames = GROUP, key = "#groupId")
  371 + @Transactional(rollbackFor = Exception.class)
  372 + public void pinGroupMessage(Long userId, Long groupId, Long messageId) {
  373 + // 1. 校验群主 / 管理员;同时拿到 group 复用,避免再走一次 @Cacheable
  374 + ImGroupDO group = validateOwnerOrAdminAndGetGroup(groupId, userId);
  375 + // 2. 校验消息属于该群、是普通聊天消息(绕过前端菜单不允许置顶群事件 / 撤回事件)、且未被撤回
  376 + ImGroupMessageDO message = groupMessageService.getGroupMessage(messageId);
  377 + if (message == null || ObjUtil.notEqual(message.getGroupId(), groupId)) {
  378 + throw exception(MESSAGE_NOT_IN_GROUP);
  379 + }
  380 + if (!ImMessageTypeEnum.validate(message.getType()).isNormal()
  381 + || ImMessageStatusEnum.RECALL.getStatus().equals(message.getStatus())) {
  382 + throw exception(MESSAGE_NOT_IN_GROUP);
  383 + }
  384 + // 3. 幂等 + 上限校验
  385 + // TODO @AI:并发读改写非原子(两个管理员同时 pin 不同消息会丢更新),后续考虑 @Version 乐观锁或带旧值条件的 SQL
  386 + List<Long> pinned = new ArrayList<>(CollUtil.emptyIfNull(group.getPinnedMessageIds()));
  387 + if (pinned.contains(messageId)) {
  388 + throw exception(GROUP_MESSAGE_ALREADY_PINNED);
  389 + }
  390 + if (pinned.size() >= GROUP_PIN_MAX_COUNT) {
  391 + throw exception(GROUP_MESSAGE_PIN_MAX_LIMIT, GROUP_PIN_MAX_COUNT);
  392 + }
  393 + pinned.add(messageId);
  394 + groupMapper.updateById(new ImGroupDO().setId(groupId).setPinnedMessageIds(pinned));
  395 +
  396 + // 4. 推送 GROUP_MESSAGE_PIN 通知给全员;payload 直接带消息对象,前端不用回查群详情绕开 @CacheEvict 时序
  397 + groupMessageService.sendGroupMessage(userId,
  398 + ImGroupMessageSendDTO.ofGroupMessagePin(groupId, userId, message));
  399 + }
  400 +
  401 + @Override
  402 + @CacheEvict(cacheNames = GROUP, key = "#groupId")
  403 + @Transactional(rollbackFor = Exception.class)
  404 + public void unpinGroupMessage(Long userId, Long groupId, Long messageId) {
  405 + // 1. 校验群主 / 管理员;同时拿到 group 复用,避免再走一次 @Cacheable
  406 + ImGroupDO group = validateOwnerOrAdminAndGetGroup(groupId, userId);
  407 + // 2. 幂等校验
  408 + // TODO @AI:并发读改写非原子,后续考虑 @Version 乐观锁或带旧值条件的 SQL
  409 + List<Long> pinned = new ArrayList<>(CollUtil.emptyIfNull(group.getPinnedMessageIds()));
  410 + if (!pinned.contains(messageId)) {
  411 + throw exception(GROUP_MESSAGE_NOT_PINNED);
  412 + }
  413 + pinned.remove(messageId);
  414 + groupMapper.updateById(new ImGroupDO().setId(groupId).setPinnedMessageIds(pinned));
  415 +
  416 + // 3. 推送 GROUP_MESSAGE_UNPIN 通知给全员
  417 + groupMessageService.sendGroupMessage(userId,
  418 + ImGroupMessageSendDTO.ofGroupMessageUnpin(groupId, userId, messageId));
  419 + }
  420 +
  421 + /** 校验登录用户是群主 / 管理员,同时返回 group(复用 validateGroupExists 的查询,避免调用方再查一次) */
  422 + private ImGroupDO validateOwnerOrAdminAndGetGroup(Long groupId, Long userId) {
  423 + ImGroupDO group = validateGroupExists(groupId);
  424 + ImGroupMemberDO member = groupMemberService.validateMemberInGroup(groupId, userId);
  425 + if (!ImGroupMemberRoleEnum.isOwnerOrAdmin(member.getRole())) {
  426 + throw exception(GROUP_NOT_OWNER_OR_ADMIN);
  427 + }
  428 + return group;
  429 + }
  430 +
366 431 // ==================== 群的读操作 ====================
367 432  
368 433 @Override
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageService.java
... ... @@ -9,6 +9,7 @@ import cn.iocoder.yudao.module.im.service.message.dto.ImGroupMessageSendDTO;
9 9  
10 10 import java.util.Collection;
11 11 import java.util.List;
  12 +import java.util.Map;
12 13  
13 14 /**
14 15 * IM 群聊消息 Service 接口
... ... @@ -138,5 +139,21 @@ public interface ImGroupMessageService {
138 139 */
139 140 ImGroupMessageDO getGroupMessage(Long id);
140 141  
  142 + /**
  143 + * 批量按消息编号查询群聊消息
  144 + *
  145 + * @param ids 消息编号集合
  146 + * @return 消息列表
  147 + */
  148 + List<ImGroupMessageDO> getGroupMessageList(Collection<Long> ids);
  149 +
  150 + /**
  151 + * 批量按消息编号查询群聊消息,返回 messageId → DO 映射
  152 + *
  153 + * @param ids 消息编号集合
  154 + * @return 消息 Map(key = 消息编号)
  155 + */
  156 + Map<Long, ImGroupMessageDO> getGroupMessageMap(Collection<Long> ids);
  157 +
141 158 }
142 159  
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImpl.java
... ... @@ -588,6 +588,19 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService {
588 588 return groupMessageMapper.selectById(id);
589 589 }
590 590  
  591 + @Override
  592 + public List<ImGroupMessageDO> getGroupMessageList(Collection<Long> ids) {
  593 + if (CollUtil.isEmpty(ids)) {
  594 + return Collections.emptyList();
  595 + }
  596 + return groupMessageMapper.selectByIds(ids);
  597 + }
  598 +
  599 + @Override
  600 + public Map<Long, ImGroupMessageDO> getGroupMessageMap(Collection<Long> ids) {
  601 + return convertMap(getGroupMessageList(ids), ImGroupMessageDO::getId);
  602 + }
  603 +
591 604 private ImGroupMessageServiceImpl getSelf() {
592 605 return SpringUtil.getBean(getClass());
593 606 }
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/dto/ImGroupMessageSendDTO.java
... ... @@ -160,4 +160,22 @@ public class ImGroupMessageSendDTO {
160 160 .setType(ImMessageTypeEnum.GROUP_MEMBER_SETTING_UPDATE.getType()).setContent(notification);
161 161 }
162 162  
  163 + public static ImGroupMessageSendDTO ofGroupMessagePin(Long groupId, Long operatorUserId,
  164 + cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO message) {
  165 + GroupMessagePinNotification notification = new GroupMessagePinNotification();
  166 + notification.setOperatorUserId(operatorUserId);
  167 + notification.setMessageId(message.getId());
  168 + notification.setMessage(message);
  169 + return new ImGroupMessageSendDTO().setGroupId(groupId)
  170 + .setType(ImMessageTypeEnum.GROUP_MESSAGE_PIN.getType()).setContent(notification);
  171 + }
  172 +
  173 + public static ImGroupMessageSendDTO ofGroupMessageUnpin(Long groupId, Long operatorUserId, Long messageId) {
  174 + GroupMessageUnpinNotification notification = new GroupMessageUnpinNotification();
  175 + notification.setOperatorUserId(operatorUserId);
  176 + notification.setMessageId(messageId);
  177 + return new ImGroupMessageSendDTO().setGroupId(groupId)
  178 + .setType(ImMessageTypeEnum.GROUP_MESSAGE_UNPIN.getType()).setContent(notification);
  179 + }
  180 +
163 181 }
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMessagePinNotification.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.service.websocket.dto.notification;
  2 +
  3 +import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO;
  4 +import lombok.Data;
  5 +
  6 +/**
  7 + * 群消息置顶事件通知
  8 + * <p>
  9 + * payload 直接携带被置顶的完整消息对象,前端按 message 直接 push 到 pinnedMessages,避免 GET /im/group/get 命中 @CacheEvict 触发前的旧 cache
  10 + */
  11 +@Data
  12 +public class GroupMessagePinNotification extends BaseGroupNotification {
  13 +
  14 + /**
  15 + * 被置顶的消息编号
  16 + */
  17 + private Long messageId;
  18 + /**
  19 + * 被置顶的完整消息对象(避免前端回查群详情)
  20 + */
  21 + private ImGroupMessageDO message;
  22 +
  23 +}
... ...
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMessageUnpinNotification.java 0 → 100644
  1 +package cn.iocoder.yudao.module.im.service.websocket.dto.notification;
  2 +
  3 +import lombok.Data;
  4 +
  5 +/**
  6 + * 群消息取消置顶事件通知
  7 + */
  8 +@Data
  9 +public class GroupMessageUnpinNotification extends BaseGroupNotification {
  10 +
  11 + /**
  12 + * 被取消置顶的消息编号
  13 + */
  14 + private Long messageId;
  15 +
  16 +}
... ...
yudao-module-im/src/test/resources/sql/create_tables.sql
... ... @@ -3,7 +3,7 @@ CREATE TABLE IF NOT EXISTS &quot;im_private_message&quot; (
3 3 "client_message_id" varchar(64) DEFAULT NULL COMMENT '客户端消息编号',
4 4 "sender_id" bigint NOT NULL COMMENT '发送人编号',
5 5 "receiver_id" bigint NOT NULL COMMENT '接收人编号',
6   - "type" tinyint NOT NULL COMMENT '消息类型',
  6 + "type" smallint NOT NULL COMMENT '消息类型',
7 7 "content" varchar(4096) DEFAULT NULL COMMENT '消息内容',
8 8 "status" tinyint NOT NULL COMMENT '消息状态',
9 9 "send_time" timestamp NOT NULL COMMENT '发送时间',
... ... @@ -21,7 +21,7 @@ CREATE TABLE IF NOT EXISTS &quot;im_group_message&quot; (
21 21 "client_message_id" varchar(64) DEFAULT NULL COMMENT '客户端消息编号',
22 22 "sender_id" bigint NOT NULL COMMENT '发送人编号',
23 23 "group_id" bigint NOT NULL COMMENT '群编号',
24   - "type" tinyint NOT NULL COMMENT '消息类型',
  24 + "type" smallint NOT NULL COMMENT '消息类型',
25 25 "content" varchar(4096) DEFAULT NULL COMMENT '消息内容',
26 26 "status" tinyint NOT NULL COMMENT '消息状态',
27 27 "send_time" timestamp NOT NULL COMMENT '发送时间',
... ... @@ -48,6 +48,7 @@ CREATE TABLE IF NOT EXISTS &quot;im_group&quot; (
48 48 "banned_time" timestamp DEFAULT NULL COMMENT '封禁时间',
49 49 "status" tinyint NOT NULL COMMENT '群状态',
50 50 "dissolved_time" timestamp DEFAULT NULL COMMENT '解散时间',
  51 + "pinned_message_ids" varchar(128) DEFAULT NULL COMMENT '群置顶消息编号列表,逗号分隔',
51 52 "creator" varchar(64) DEFAULT '',
52 53 "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
53 54 "updater" varchar(64) DEFAULT '',
... ...