diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupController.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupController.java index 7063f6b..16605c6 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupController.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupController.java @@ -1,17 +1,25 @@ package cn.iocoder.yudao.module.im.controller.admin.group; +import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupAdminAddReqVO; import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupAdminRemoveReqVO; import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupCreateReqVO; +import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupMessagePinReqVO; import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupRespVO; import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupTransferOwnerReqVO; import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupUpdateReqVO; import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberInviteReqVO; import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberRemoveReqVO; +import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessageRespVO; import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO; +import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; +import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO; +import cn.iocoder.yudao.module.im.service.group.ImGroupMemberService; import cn.iocoder.yudao.module.im.service.group.ImGroupService; +import cn.iocoder.yudao.module.im.service.message.ImGroupMessageService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; @@ -20,9 +28,17 @@ import jakarta.validation.Valid; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSetByFlatMap; import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getLoginUserId; @Tag(name = "管理后台 - 群") @@ -33,6 +49,10 @@ public class ImGroupController { @Resource private ImGroupService groupService; + @Resource + private ImGroupMemberService groupMemberService; + @Resource + private ImGroupMessageService groupMessageService; // ==================== 群的写操作 ==================== @@ -40,6 +60,7 @@ public class ImGroupController { @Operation(summary = "创建群") public CommonResult createGroup(@Valid @RequestBody ImGroupCreateReqVO createReqVO) { ImGroupDO group = groupService.createGroup(createReqVO, getLoginUserId()); + // 新建群必无 pinnedMessages,跳过关联回填 return success(BeanUtils.toBean(group, ImGroupRespVO.class)); } @@ -47,7 +68,7 @@ public class ImGroupController { @Operation(summary = "更新群") public CommonResult updateGroup(@Valid @RequestBody ImGroupUpdateReqVO updateReqVO) { ImGroupDO group = groupService.updateGroup(updateReqVO, getLoginUserId()); - return success(BeanUtils.toBean(group, ImGroupRespVO.class)); + return success(buildGroupRespVO(group, getLoginUserId())); } @DeleteMapping("/dissolve") @@ -65,14 +86,15 @@ public class ImGroupController { @Parameter(name = "id", description = "编号", required = true, example = "1024") public CommonResult getGroup(@RequestParam("id") Long id) { ImGroupDO group = groupService.getGroup(id); - return success(BeanUtils.toBean(group, ImGroupRespVO.class)); + return success(buildGroupRespVO(group, getLoginUserId())); } @GetMapping("/list") @Operation(summary = "获得当前登录用户的群列表") public CommonResult> getMyGroupList() { - List groups = groupService.getMyGroupList(getLoginUserId()); - return success(BeanUtils.toBean(groups, ImGroupRespVO.class)); + Long loginUserId = getLoginUserId(); + List groups = groupService.getMyGroupList(loginUserId); + return success(buildGroupRespVOList(groups, loginUserId)); } // ==================== 群成员的写操作 ==================== @@ -120,4 +142,59 @@ public class ImGroupController { return success(true); } + // ==================== 群消息置顶 ==================== + + @PutMapping("/pin-message") + @Operation(summary = "置顶群消息(群主 / 管理员)") + public CommonResult pinGroupMessage(@Valid @RequestBody ImGroupMessagePinReqVO reqVO) { + groupService.pinGroupMessage(getLoginUserId(), reqVO.getGroupId(), reqVO.getMessageId()); + return success(true); + } + + @PutMapping("/unpin-message") + @Operation(summary = "取消置顶群消息(群主 / 管理员)") + public CommonResult unpinGroupMessage(@Valid @RequestBody ImGroupMessagePinReqVO reqVO) { + groupService.unpinGroupMessage(getLoginUserId(), reqVO.getGroupId(), reqVO.getMessageId()); + return success(true); + } + + /** 单群转 VO + 关联回填 pinnedMessages(仅当登录用户是该群有效成员) */ + private ImGroupRespVO buildGroupRespVO(ImGroupDO group, Long loginUserId) { + if (group == null) { + return null; + } + return buildGroupRespVOList(Collections.singletonList(group), loginUserId).get(0); + } + + /** + * 群列表批量转 VO + 关联回填 pinnedMessages + *

+ * 仅当登录用户是某群的有效成员时才回填该群的 pinnedMessages,避免非成员 / 已退群用户越权拿到置顶消息内容 + */ + private List buildGroupRespVOList(List groups, Long loginUserId) { + if (CollUtil.isEmpty(groups)) { + return Collections.emptyList(); + } + // 仅当前用户是有效成员的群才允许回填置顶消息(按 groupId 一次性 IN 查成员关系) + Set activeGroupIds = convertSet( + groupMemberService.getActiveGroupMemberListByUserId(loginUserId), ImGroupMemberDO::getGroupId); + // 把 active 群的 pinnedMessageIds 一次性聚合 IN 查询,避免 N+1 + Set allMessageIds = convertSetByFlatMap(groups, group -> activeGroupIds.contains(group.getId()) + ? CollUtil.emptyIfNull(group.getPinnedMessageIds()).stream() : Stream.empty()); + Map messageMap = groupMessageService.getGroupMessageMap(allMessageIds); + return CollectionUtils.convertList(groups, group -> { + ImGroupRespVO vo = BeanUtils.toBean(group, ImGroupRespVO.class); + if (!activeGroupIds.contains(group.getId()) || CollUtil.isEmpty(group.getPinnedMessageIds())) { + return vo; + } + // 按 pin 顺序输出,已被删除的消息(messageMap 没命中)跳过 + // TODO @AI:使用 CollUtils convert 进一步简化。 + List pinned = group.getPinnedMessageIds().stream() + .map(messageMap::get).filter(Objects::nonNull) + .map(msg -> BeanUtils.toBean(msg, ImGroupMessageRespVO.class)) + .collect(Collectors.toList()); + return vo.setPinnedMessages(pinned); + }); + } + } diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupMessagePinReqVO.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupMessagePinReqVO.java new file mode 100644 index 0000000..0e4dfac --- /dev/null +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupMessagePinReqVO.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.module.im.controller.admin.group.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * 管理后台 - 群消息置顶 / 取消置顶 Request VO + */ +@Schema(description = "管理后台 - 群消息置顶 / 取消置顶 Request VO") +@Data +public class ImGroupMessagePinReqVO { + + @Schema(description = "群编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13279") + @NotNull(message = "群编号不能为空") + private Long groupId; + + @Schema(description = "消息编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "9527") + @NotNull(message = "消息编号不能为空") + private Long messageId; + +} diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupRespVO.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupRespVO.java index 037daf0..3f76444 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupRespVO.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/ImGroupRespVO.java @@ -1,9 +1,11 @@ package cn.iocoder.yudao.module.im.controller.admin.group.vo; +import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessageRespVO; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.time.LocalDateTime; +import java.util.List; @Schema(description = "管理后台 - 群 Response VO") @Data @@ -39,4 +41,7 @@ public class ImGroupRespVO { @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime createTime; + @Schema(description = "群置顶消息列表,按 pin 顺序(最先置顶的在前);非该群有效成员时为空") + private List pinnedMessages; + } \ No newline at end of file diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/group/ImGroupDO.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/group/ImGroupDO.java index c304938..95a497e 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/dataobject/group/ImGroupDO.java +++ b/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; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler; import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.*; import java.time.LocalDateTime; +import java.util.List; /** * IM 群信息 DO * * @author 芋道源码 */ -@TableName("im_group") +@TableName(value = "im_group",autoResultMap = true) @KeySequence("im_group_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 @Data @EqualsAndHashCode(callSuper = true) @@ -70,5 +73,12 @@ public class ImGroupDO extends BaseDO { * 解散时间 */ private LocalDateTime dissolvedTime; + /** + * 群置顶消息编号列表 + *

+ * 仅存 messageId,操作人 / 置顶时间从对应 PIN 事件的消息记录里反查 + */ + @TableField(typeHandler = LongListTypeHandler.class) + private List pinnedMessageIds; } diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/ErrorCodeConstants.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/ErrorCodeConstants.java index b621d0f..3b3ef1e 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/ErrorCodeConstants.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/ErrorCodeConstants.java @@ -17,6 +17,7 @@ public interface ErrorCodeConstants { ErrorCode MESSAGE_PULL_SIZE_EXCEEDED = new ErrorCode(1_040_300_005, "单次拉取消息数量不能超过 {} 条"); ErrorCode MESSAGE_RECALL_TIMEOUT = new ErrorCode(1_040_300_007, "超过 {} 分钟的消息无法撤回"); ErrorCode MESSAGE_QUOTE_INVALID = new ErrorCode(1_040_300_008, "引用的消息不可用"); + ErrorCode MESSAGE_NOT_IN_GROUP = new ErrorCode(1_040_300_009, "消息不属于该群"); // ========== 群 (1-040-400-000) ========== ErrorCode GROUP_NOT_EXISTS = new ErrorCode(1_040_400_000, "群不存在"); @@ -25,6 +26,9 @@ public interface ErrorCodeConstants { ErrorCode GROUP_NOT_OWNER = new ErrorCode(1_040_400_003, "仅群主可执行该操作"); ErrorCode GROUP_NOT_OWNER_OR_ADMIN = new ErrorCode(1_040_400_004, "仅群主或管理员可执行该操作"); ErrorCode GROUP_TRANSFER_OWNER_TO_SELF = new ErrorCode(1_040_400_005, "不能将群主转让给自己"); + ErrorCode GROUP_MESSAGE_PIN_MAX_LIMIT = new ErrorCode(1_040_400_006, "群置顶消息数量不能超过 {} 条"); + ErrorCode GROUP_MESSAGE_ALREADY_PINNED = new ErrorCode(1_040_400_007, "该消息已置顶"); + ErrorCode GROUP_MESSAGE_NOT_PINNED = new ErrorCode(1_040_400_008, "该消息未置顶"); // ========== 群成员 (1-040-500-000) ========== ErrorCode GROUP_MEMBER_NOT_IN_GROUP = new ErrorCode(1_040_500_001, "您已不在该群中"); diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/message/ImMessageTypeEnum.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/message/ImMessageTypeEnum.java index 1af681b..f292de9 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/message/ImMessageTypeEnum.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/enums/message/ImMessageTypeEnum.java @@ -159,7 +159,16 @@ public enum ImMessageTypeEnum implements ArrayValuable { * 对应自己的类:GroupMemberSettingUpdateNotification * 场景:用户改自己的群免打扰 / 群备注,仅推该用户其他在线终端做多端同步 */ - GROUP_MEMBER_SETTING_UPDATE(1530, "群成员个人设置变更", false, false); + GROUP_MEMBER_SETTING_UPDATE(1530, "群成员个人设置变更", false, false), + // TODO @AI:注释风格,还是参考上面的 + /** + * 自有扩展:群主 / 管理员置顶一条群消息,全员广播;前端把 messageId 加入 group.pinnedMessageIds + */ + GROUP_MESSAGE_PIN(1531, "群消息置顶", true, false), + /** + * 自有扩展:群主 / 管理员取消置顶,全员广播;前端把 messageId 从 group.pinnedMessageIds 移除 + */ + GROUP_MESSAGE_UNPIN(1532, "群消息取消置顶", true, false); public static final Integer[] ARRAYS = Arrays.stream(values()).map(ImMessageTypeEnum::getType).toArray(Integer[]::new); diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupService.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupService.java index 8f973a5..25a80a8 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupService.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupService.java @@ -154,6 +154,26 @@ public interface ImGroupService { */ void transferGroupOwner(Long userId, @Valid ImGroupTransferOwnerReqVO transferReqVO); + /** + * 置顶群消息(仅群主或管理员可执行) + *

+ * 上限 {@link cn.iocoder.yudao.module.im.enums.ImCommonConstants#GROUP_PIN_MAX_COUNT};幂等失败时抛业务异常 + * + * @param userId 当前登录用户编号 + * @param groupId 群编号 + * @param messageId 被置顶的消息编号 + */ + void pinGroupMessage(Long userId, Long groupId, Long messageId); + + /** + * 取消置顶群消息(仅群主或管理员可执行) + * + * @param userId 当前登录用户编号 + * @param groupId 群编号 + * @param messageId 被取消置顶的消息编号 + */ + void unpinGroupMessage(Long userId, Long groupId, Long messageId); + // ==================== 管理后台 ==================== /** diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImpl.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImpl.java index d213bd8..7e0b642 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImpl.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImpl.java @@ -22,8 +22,11 @@ import cn.iocoder.yudao.module.im.controller.admin.manager.group.vo.ImGroupManag import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO; import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO; import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; +import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO; import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupMapper; import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum; +import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum; +import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; import cn.iocoder.yudao.module.im.service.friend.ImFriendService; import cn.iocoder.yudao.module.im.service.message.ImGroupMessageService; import cn.iocoder.yudao.module.im.service.message.dto.ImGroupMessageSendDTO; @@ -363,6 +366,68 @@ public class ImGroupServiceImpl implements ImGroupService { ImGroupMessageSendDTO.ofGroupOwnerTransfer(groupId, userId, newOwnerUserId)); } + @Override + @CacheEvict(cacheNames = GROUP, key = "#groupId") + @Transactional(rollbackFor = Exception.class) + public void pinGroupMessage(Long userId, Long groupId, Long messageId) { + // 1. 校验群主 / 管理员;同时拿到 group 复用,避免再走一次 @Cacheable + ImGroupDO group = validateOwnerOrAdminAndGetGroup(groupId, userId); + // 2. 校验消息属于该群、是普通聊天消息(绕过前端菜单不允许置顶群事件 / 撤回事件)、且未被撤回 + ImGroupMessageDO message = groupMessageService.getGroupMessage(messageId); + if (message == null || ObjUtil.notEqual(message.getGroupId(), groupId)) { + throw exception(MESSAGE_NOT_IN_GROUP); + } + if (!ImMessageTypeEnum.validate(message.getType()).isNormal() + || ImMessageStatusEnum.RECALL.getStatus().equals(message.getStatus())) { + throw exception(MESSAGE_NOT_IN_GROUP); + } + // 3. 幂等 + 上限校验 + // TODO @AI:并发读改写非原子(两个管理员同时 pin 不同消息会丢更新),后续考虑 @Version 乐观锁或带旧值条件的 SQL + List pinned = new ArrayList<>(CollUtil.emptyIfNull(group.getPinnedMessageIds())); + if (pinned.contains(messageId)) { + throw exception(GROUP_MESSAGE_ALREADY_PINNED); + } + if (pinned.size() >= GROUP_PIN_MAX_COUNT) { + throw exception(GROUP_MESSAGE_PIN_MAX_LIMIT, GROUP_PIN_MAX_COUNT); + } + pinned.add(messageId); + groupMapper.updateById(new ImGroupDO().setId(groupId).setPinnedMessageIds(pinned)); + + // 4. 推送 GROUP_MESSAGE_PIN 通知给全员;payload 直接带消息对象,前端不用回查群详情绕开 @CacheEvict 时序 + groupMessageService.sendGroupMessage(userId, + ImGroupMessageSendDTO.ofGroupMessagePin(groupId, userId, message)); + } + + @Override + @CacheEvict(cacheNames = GROUP, key = "#groupId") + @Transactional(rollbackFor = Exception.class) + public void unpinGroupMessage(Long userId, Long groupId, Long messageId) { + // 1. 校验群主 / 管理员;同时拿到 group 复用,避免再走一次 @Cacheable + ImGroupDO group = validateOwnerOrAdminAndGetGroup(groupId, userId); + // 2. 幂等校验 + // TODO @AI:并发读改写非原子,后续考虑 @Version 乐观锁或带旧值条件的 SQL + List pinned = new ArrayList<>(CollUtil.emptyIfNull(group.getPinnedMessageIds())); + if (!pinned.contains(messageId)) { + throw exception(GROUP_MESSAGE_NOT_PINNED); + } + pinned.remove(messageId); + groupMapper.updateById(new ImGroupDO().setId(groupId).setPinnedMessageIds(pinned)); + + // 3. 推送 GROUP_MESSAGE_UNPIN 通知给全员 + groupMessageService.sendGroupMessage(userId, + ImGroupMessageSendDTO.ofGroupMessageUnpin(groupId, userId, messageId)); + } + + /** 校验登录用户是群主 / 管理员,同时返回 group(复用 validateGroupExists 的查询,避免调用方再查一次) */ + private ImGroupDO validateOwnerOrAdminAndGetGroup(Long groupId, Long userId) { + ImGroupDO group = validateGroupExists(groupId); + ImGroupMemberDO member = groupMemberService.validateMemberInGroup(groupId, userId); + if (!ImGroupMemberRoleEnum.isOwnerOrAdmin(member.getRole())) { + throw exception(GROUP_NOT_OWNER_OR_ADMIN); + } + return group; + } + // ==================== 群的读操作 ==================== @Override diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageService.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageService.java index 57cfce6..51a28f1 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageService.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageService.java @@ -9,6 +9,7 @@ import cn.iocoder.yudao.module.im.service.message.dto.ImGroupMessageSendDTO; import java.util.Collection; import java.util.List; +import java.util.Map; /** * IM 群聊消息 Service 接口 @@ -138,5 +139,21 @@ public interface ImGroupMessageService { */ ImGroupMessageDO getGroupMessage(Long id); + /** + * 批量按消息编号查询群聊消息 + * + * @param ids 消息编号集合 + * @return 消息列表 + */ + List getGroupMessageList(Collection ids); + + /** + * 批量按消息编号查询群聊消息,返回 messageId → DO 映射 + * + * @param ids 消息编号集合 + * @return 消息 Map(key = 消息编号) + */ + Map getGroupMessageMap(Collection ids); + } diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImpl.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImpl.java index 98a3c1c..f00ac09 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImpl.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImpl.java @@ -588,6 +588,19 @@ public class ImGroupMessageServiceImpl implements ImGroupMessageService { return groupMessageMapper.selectById(id); } + @Override + public List getGroupMessageList(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return Collections.emptyList(); + } + return groupMessageMapper.selectByIds(ids); + } + + @Override + public Map getGroupMessageMap(Collection ids) { + return convertMap(getGroupMessageList(ids), ImGroupMessageDO::getId); + } + private ImGroupMessageServiceImpl getSelf() { return SpringUtil.getBean(getClass()); } diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/dto/ImGroupMessageSendDTO.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/dto/ImGroupMessageSendDTO.java index cf29435..33eeb83 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/dto/ImGroupMessageSendDTO.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/message/dto/ImGroupMessageSendDTO.java @@ -160,4 +160,22 @@ public class ImGroupMessageSendDTO { .setType(ImMessageTypeEnum.GROUP_MEMBER_SETTING_UPDATE.getType()).setContent(notification); } + public static ImGroupMessageSendDTO ofGroupMessagePin(Long groupId, Long operatorUserId, + cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO message) { + GroupMessagePinNotification notification = new GroupMessagePinNotification(); + notification.setOperatorUserId(operatorUserId); + notification.setMessageId(message.getId()); + notification.setMessage(message); + return new ImGroupMessageSendDTO().setGroupId(groupId) + .setType(ImMessageTypeEnum.GROUP_MESSAGE_PIN.getType()).setContent(notification); + } + + public static ImGroupMessageSendDTO ofGroupMessageUnpin(Long groupId, Long operatorUserId, Long messageId) { + GroupMessageUnpinNotification notification = new GroupMessageUnpinNotification(); + notification.setOperatorUserId(operatorUserId); + notification.setMessageId(messageId); + return new ImGroupMessageSendDTO().setGroupId(groupId) + .setType(ImMessageTypeEnum.GROUP_MESSAGE_UNPIN.getType()).setContent(notification); + } + } diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMessagePinNotification.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMessagePinNotification.java new file mode 100644 index 0000000..12d509e --- /dev/null +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMessagePinNotification.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.module.im.service.websocket.dto.notification; + +import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO; +import lombok.Data; + +/** + * 群消息置顶事件通知 + *

+ * payload 直接携带被置顶的完整消息对象,前端按 message 直接 push 到 pinnedMessages,避免 GET /im/group/get 命中 @CacheEvict 触发前的旧 cache + */ +@Data +public class GroupMessagePinNotification extends BaseGroupNotification { + + /** + * 被置顶的消息编号 + */ + private Long messageId; + /** + * 被置顶的完整消息对象(避免前端回查群详情) + */ + private ImGroupMessageDO message; + +} diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMessageUnpinNotification.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMessageUnpinNotification.java new file mode 100644 index 0000000..204fa2a --- /dev/null +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/websocket/dto/notification/GroupMessageUnpinNotification.java @@ -0,0 +1,16 @@ +package cn.iocoder.yudao.module.im.service.websocket.dto.notification; + +import lombok.Data; + +/** + * 群消息取消置顶事件通知 + */ +@Data +public class GroupMessageUnpinNotification extends BaseGroupNotification { + + /** + * 被取消置顶的消息编号 + */ + private Long messageId; + +} diff --git a/yudao-module-im/src/test/resources/sql/create_tables.sql b/yudao-module-im/src/test/resources/sql/create_tables.sql index 518447a..6689d38 100644 --- a/yudao-module-im/src/test/resources/sql/create_tables.sql +++ b/yudao-module-im/src/test/resources/sql/create_tables.sql @@ -3,7 +3,7 @@ CREATE TABLE IF NOT EXISTS "im_private_message" ( "client_message_id" varchar(64) DEFAULT NULL COMMENT '客户端消息编号', "sender_id" bigint NOT NULL COMMENT '发送人编号', "receiver_id" bigint NOT NULL COMMENT '接收人编号', - "type" tinyint NOT NULL COMMENT '消息类型', + "type" smallint NOT NULL COMMENT '消息类型', "content" varchar(4096) DEFAULT NULL COMMENT '消息内容', "status" tinyint NOT NULL COMMENT '消息状态', "send_time" timestamp NOT NULL COMMENT '发送时间', @@ -21,7 +21,7 @@ CREATE TABLE IF NOT EXISTS "im_group_message" ( "client_message_id" varchar(64) DEFAULT NULL COMMENT '客户端消息编号', "sender_id" bigint NOT NULL COMMENT '发送人编号', "group_id" bigint NOT NULL COMMENT '群编号', - "type" tinyint NOT NULL COMMENT '消息类型', + "type" smallint NOT NULL COMMENT '消息类型', "content" varchar(4096) DEFAULT NULL COMMENT '消息内容', "status" tinyint NOT NULL COMMENT '消息状态', "send_time" timestamp NOT NULL COMMENT '发送时间', @@ -48,6 +48,7 @@ CREATE TABLE IF NOT EXISTS "im_group" ( "banned_time" timestamp DEFAULT NULL COMMENT '封禁时间', "status" tinyint NOT NULL COMMENT '群状态', "dissolved_time" timestamp DEFAULT NULL COMMENT '解散时间', + "pinned_message_ids" varchar(128) DEFAULT NULL COMMENT '群置顶消息编号列表,逗号分隔', "creator" varchar(64) DEFAULT '', "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, "updater" varchar(64) DEFAULT '',