Commit 210a8d5af65f9bcf4c46d6efe4a7c30784130ba6
1 parent
927b64fc
feat(im): 增强消息拉取与状态补偿可靠性
- 新增会话读位置持久化接口与前端同步逻辑 - 增加好友、好友申请、加群申请的增量拉取补偿 - 统一前端 pull 编排,增加回扫窗口、落库等待和账号切换守卫 - 调整群成员为按群懒加载缓存,并移除全局成员增量链路 - 修复消息落库、读位置补偿、READ 事件乱序下的未读状态一致性 - 完善群申请红点快照刷新和管理员角色变化补偿 - 更新消息存储设计与修复记录文档
Showing
22 changed files
with
351 additions
and
19 deletions
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/conversation/ImConversationReadController.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.controller.admin.conversation; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.framework.common.pojo.CommonResult; | |
| 4 | +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; | |
| 5 | +import cn.iocoder.yudao.module.im.controller.admin.conversation.vo.ImConversationReadRespVO; | |
| 6 | +import cn.iocoder.yudao.module.im.dal.dataobject.conversation.ImConversationReadDO; | |
| 7 | +import cn.iocoder.yudao.module.im.service.conversation.ImConversationReadService; | |
| 8 | +import io.swagger.v3.oas.annotations.Operation; | |
| 9 | +import io.swagger.v3.oas.annotations.Parameter; | |
| 10 | +import io.swagger.v3.oas.annotations.Parameters; | |
| 11 | +import io.swagger.v3.oas.annotations.tags.Tag; | |
| 12 | +import jakarta.annotation.Resource; | |
| 13 | +import jakarta.validation.constraints.Max; | |
| 14 | +import jakarta.validation.constraints.Min; | |
| 15 | +import org.springframework.validation.annotation.Validated; | |
| 16 | +import org.springframework.web.bind.annotation.GetMapping; | |
| 17 | +import org.springframework.web.bind.annotation.RequestMapping; | |
| 18 | +import org.springframework.web.bind.annotation.RequestParam; | |
| 19 | +import org.springframework.web.bind.annotation.RestController; | |
| 20 | + | |
| 21 | +import java.util.List; | |
| 22 | + | |
| 23 | +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; | |
| 24 | +import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getLoginUserId; | |
| 25 | + | |
| 26 | +@Tag(name = "管理后台 - IM 会话读位置") | |
| 27 | +@RestController | |
| 28 | +@RequestMapping("/im/conversation-read") | |
| 29 | +@Validated | |
| 30 | +public class ImConversationReadController { | |
| 31 | + | |
| 32 | + @Resource | |
| 33 | + private ImConversationReadService conversationReadService; | |
| 34 | + | |
| 35 | + @GetMapping("/pull") | |
| 36 | + @Operation(summary = "增量拉取当前用户的会话读位置(重连 / 离线补偿)") | |
| 37 | + @Parameters({ | |
| 38 | + @Parameter(name = "lastUpdateTime", description = "上次拉取到的最新更新时间(毫秒时间戳);首次拉取不传"), | |
| 39 | + @Parameter(name = "lastId", description = "上次拉取到的最后一条记录 id;首次拉取不传"), | |
| 40 | + @Parameter(name = "limit", description = "单次拉取条数", required = true) | |
| 41 | + }) | |
| 42 | + public CommonResult<List<ImConversationReadRespVO>> pullMyConversationRead( | |
| 43 | + @RequestParam(value = "lastUpdateTime", required = false) Long lastUpdateTime, | |
| 44 | + @RequestParam(value = "lastId", required = false) Long lastId, | |
| 45 | + @RequestParam("limit") @Min(1) @Max(200) Integer limit) { | |
| 46 | + List<ImConversationReadDO> list = conversationReadService.pullConversationReadList( | |
| 47 | + getLoginUserId(), lastUpdateTime, lastId, limit); | |
| 48 | + return success(BeanUtils.toBean(list, ImConversationReadRespVO.class)); | |
| 49 | + } | |
| 50 | + | |
| 51 | +} | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/conversation/vo/ImConversationReadRespVO.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.controller.admin.conversation.vo; | |
| 2 | + | |
| 3 | +import io.swagger.v3.oas.annotations.media.Schema; | |
| 4 | +import lombok.Data; | |
| 5 | + | |
| 6 | +import java.time.LocalDateTime; | |
| 7 | + | |
| 8 | +/** | |
| 9 | + * IM 会话读位置 Response VO | |
| 10 | + * | |
| 11 | + * @author 芋道源码 | |
| 12 | + */ | |
| 13 | +@Schema(description = "管理后台 - IM 会话读位置 Response VO") | |
| 14 | +@Data | |
| 15 | +public class ImConversationReadRespVO { | |
| 16 | + | |
| 17 | + @Schema(description = "读位置编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") | |
| 18 | + private Long id; | |
| 19 | + | |
| 20 | + @Schema(description = "会话类型", example = "1") | |
| 21 | + private Integer conversationType; // 参见 ImConversationTypeEnum 枚举 | |
| 22 | + | |
| 23 | + @Schema(description = "目标编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") | |
| 24 | + private Long targetId; | |
| 25 | + | |
| 26 | + @Schema(description = "最大已读消息编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "4096") | |
| 27 | + private Long messageId; | |
| 28 | + | |
| 29 | + @Schema(description = "最近更新时间(增量拉取游标用)") | |
| 30 | + private LocalDateTime updateTime; | |
| 31 | + | |
| 32 | +} | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/ImFriendController.java
| ... | ... | @@ -17,6 +17,8 @@ import io.swagger.v3.oas.annotations.Parameters; |
| 17 | 17 | import io.swagger.v3.oas.annotations.tags.Tag; |
| 18 | 18 | import jakarta.annotation.Resource; |
| 19 | 19 | import jakarta.validation.Valid; |
| 20 | +import jakarta.validation.constraints.Max; | |
| 21 | +import jakarta.validation.constraints.Min; | |
| 20 | 22 | import jakarta.validation.constraints.NotNull; |
| 21 | 23 | import org.springframework.validation.annotation.Validated; |
| 22 | 24 | import org.springframework.web.bind.annotation.*; |
| ... | ... | @@ -46,11 +48,25 @@ public class ImFriendController { |
| 46 | 48 | @GetMapping("/list") |
| 47 | 49 | @Operation(summary = "获得当前登录用户的好友列表") |
| 48 | 50 | public CommonResult<List<ImFriendRespVO>> getMyFriendList() { |
| 49 | - // 含 DISABLE 历史好友:保留给前端展示「已删除好友」的历史对话信息;前端按 status 决定会话级联清理 | |
| 50 | 51 | List<ImFriendDO> friends = friendService.getFriendList(getLoginUserId()); |
| 51 | 52 | return success(buildFriendRespVOList(friends)); |
| 52 | 53 | } |
| 53 | 54 | |
| 55 | + @GetMapping("/pull") | |
| 56 | + @Operation(summary = "增量拉取当前用户的好友关系(重连 / 离线补偿)") | |
| 57 | + @Parameters({ | |
| 58 | + @Parameter(name = "lastUpdateTime", description = "上次拉取到的最新更新时间(毫秒时间戳);首次拉取不传"), | |
| 59 | + @Parameter(name = "lastId", description = "上次拉取到的最后一条记录 id;首次拉取不传"), | |
| 60 | + @Parameter(name = "limit", description = "单次拉取条数", required = true) | |
| 61 | + }) | |
| 62 | + public CommonResult<List<ImFriendRespVO>> pullMyFriendList( | |
| 63 | + @RequestParam(value = "lastUpdateTime", required = false) Long lastUpdateTime, | |
| 64 | + @RequestParam(value = "lastId", required = false) Long lastId, | |
| 65 | + @RequestParam("limit") @Min(1) @Max(200) Integer limit) { | |
| 66 | + List<ImFriendDO> list = friendService.pullFriendList(getLoginUserId(), lastUpdateTime, lastId, limit); | |
| 67 | + return success(buildFriendRespVOList(list)); | |
| 68 | + } | |
| 69 | + | |
| 54 | 70 | @GetMapping("/get") |
| 55 | 71 | @Operation(summary = "获得好友详情") |
| 56 | 72 | @Parameter(name = "friendUserId", description = "好友的用户编号", required = true, example = "2048") |
| ... | ... | @@ -62,8 +78,8 @@ public class ImFriendController { |
| 62 | 78 | @DeleteMapping("/delete") |
| 63 | 79 | @Operation(summary = "删除好友(单向软删除)") |
| 64 | 80 | @Parameters({ |
| 65 | - @Parameter(description = "好友的用户编号", required = true, example = "2048"), | |
| 66 | - @Parameter(description = "是否级联清理本端相关数据(如私聊会话)") | |
| 81 | + @Parameter(name = "friendUserId", description = "好友的用户编号", required = true, example = "2048"), | |
| 82 | + @Parameter(name = "clear", description = "是否级联清理本端相关数据(如私聊会话)") | |
| 67 | 83 | }) |
| 68 | 84 | public CommonResult<Boolean> deleteFriend( |
| 69 | 85 | @RequestParam("friendUserId") @NotNull(message = "好友用户编号不能为空") Long friendUserId, | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/ImFriendRequestController.java
| ... | ... | @@ -13,6 +13,7 @@ import cn.iocoder.yudao.module.system.api.user.AdminUserApi; |
| 13 | 13 | import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; |
| 14 | 14 | import io.swagger.v3.oas.annotations.Operation; |
| 15 | 15 | import io.swagger.v3.oas.annotations.Parameter; |
| 16 | +import io.swagger.v3.oas.annotations.Parameters; | |
| 16 | 17 | import io.swagger.v3.oas.annotations.tags.Tag; |
| 17 | 18 | import jakarta.annotation.Resource; |
| 18 | 19 | import jakarta.validation.Valid; |
| ... | ... | @@ -79,15 +80,32 @@ public class ImFriendRequestController { |
| 79 | 80 | |
| 80 | 81 | @GetMapping("/list") |
| 81 | 82 | @Operation(summary = "查询「我相关」的好友申请列表(游标分页:传 maxId 加载更多)") |
| 83 | + @Parameters({ | |
| 84 | + @Parameter(name = "maxId", description = "当前列表最旧记录的 id;首页不传"), | |
| 85 | + @Parameter(name = "limit", description = "单次拉取条数", required = true) | |
| 86 | + }) | |
| 82 | 87 | public CommonResult<List<ImFriendRequestRespVO>> getMyFriendRequestList( |
| 83 | - @Parameter(description = "当前列表最旧记录的 id;首页不传") | |
| 84 | 88 | @RequestParam(value = "maxId", required = false) Long maxId, |
| 85 | - @Parameter(description = "单次拉取条数", required = true) | |
| 86 | 89 | @RequestParam("limit") @Min(1) @Max(200) Integer limit) { |
| 87 | 90 | List<ImFriendRequestDO> list = friendRequestService.getMyFriendRequestList(getLoginUserId(), maxId, limit); |
| 88 | 91 | return success(buildList(list)); |
| 89 | 92 | } |
| 90 | 93 | |
| 94 | + @GetMapping("/pull") | |
| 95 | + @Operation(summary = "增量拉取「我相关」的好友申请(重连 / 离线补偿)") | |
| 96 | + @Parameters({ | |
| 97 | + @Parameter(name = "lastUpdateTime", description = "上次拉取到的最新更新时间(毫秒时间戳);首次拉取不传"), | |
| 98 | + @Parameter(name = "lastId", description = "上次拉取到的最后一条记录 id;首次拉取不传"), | |
| 99 | + @Parameter(name = "limit", description = "单次拉取条数", required = true) | |
| 100 | + }) | |
| 101 | + public CommonResult<List<ImFriendRequestRespVO>> pullMyFriendRequestList( | |
| 102 | + @RequestParam(value = "lastUpdateTime", required = false) Long lastUpdateTime, | |
| 103 | + @RequestParam(value = "lastId", required = false) Long lastId, | |
| 104 | + @RequestParam("limit") @Min(1) @Max(200) Integer limit) { | |
| 105 | + List<ImFriendRequestDO> list = friendRequestService.pullFriendRequestList(getLoginUserId(), lastUpdateTime, lastId, limit); | |
| 106 | + return success(buildList(list)); | |
| 107 | + } | |
| 108 | + | |
| 91 | 109 | @GetMapping("/get") |
| 92 | 110 | @Operation(summary = "按 id 单查「我相关」的申请记录(带越权过滤;WebSocket 通知到达后用)") |
| 93 | 111 | @Parameter(name = "id", description = "申请记录编号", required = true) | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/vo/ImFriendRespVO.java
| ... | ... | @@ -47,6 +47,9 @@ public class ImFriendRespVO { |
| 47 | 47 | @Schema(description = "删除好友时间") |
| 48 | 48 | private LocalDateTime deleteTime; |
| 49 | 49 | |
| 50 | + @Schema(description = "最近更新时间(增量拉取游标用)") | |
| 51 | + private LocalDateTime updateTime; | |
| 52 | + | |
| 50 | 53 | // ========== 下面是聚合字段,方便前端显示 ========== |
| 51 | 54 | |
| 52 | 55 | @Schema(description = "好友昵称(实时聚合自 AdminUser)", example = "芋道") | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/vo/request/ImFriendRequestRespVO.java
| ... | ... | @@ -41,6 +41,9 @@ public class ImFriendRequestRespVO { |
| 41 | 41 | @Schema(description = "申请创建时间") |
| 42 | 42 | private LocalDateTime createTime; |
| 43 | 43 | |
| 44 | + @Schema(description = "最近更新时间(增量拉取游标用)") | |
| 45 | + private LocalDateTime updateTime; | |
| 46 | + | |
| 44 | 47 | // ========== 下面是聚合字段,方便前端显示 ========== |
| 45 | 48 | |
| 46 | 49 | @Schema(description = "发起方昵称(实时聚合自 AdminUser)", example = "芋道") | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupMemberController.java
| ... | ... | @@ -21,6 +21,7 @@ import jakarta.validation.Valid; |
| 21 | 21 | import org.springframework.validation.annotation.Validated; |
| 22 | 22 | import org.springframework.web.bind.annotation.*; |
| 23 | 23 | |
| 24 | +import java.util.Collections; | |
| 24 | 25 | import java.util.List; |
| 25 | 26 | import java.util.Map; |
| 26 | 27 | |
| ... | ... | @@ -88,7 +89,7 @@ public class ImGroupMemberController { |
| 88 | 89 | } |
| 89 | 90 | |
| 90 | 91 | @GetMapping("/list") |
| 91 | - @Operation(summary = "获得指定群的成员列表") | |
| 92 | + @Operation(summary = "获得指定群的成员列表(按群全量拉取,作为前端本地成员 cache 的完整基线)") | |
| 92 | 93 | @Parameter(name = "groupId", description = "群编号", required = true, example = "1024") |
| 93 | 94 | public CommonResult<List<ImGroupMemberRespVO>> getGroupMemberList(@RequestParam("groupId") Long groupId) { |
| 94 | 95 | // 1.1 查询群成员列表(包含 DISABLE 已退群的成员,不按时间过滤) |
| ... | ... | @@ -101,16 +102,30 @@ public class ImGroupMemberController { |
| 101 | 102 | throw exception(GROUP_MEMBER_NOT_IN_GROUP); |
| 102 | 103 | } |
| 103 | 104 | |
| 104 | - // 2.批量聚合 AdminUser 信息(昵称 / 头像) | |
| 105 | + // 2. 批量聚合昵称 / 头像,并对非本人置空私人字段 | |
| 106 | + return success(buildGroupMemberRespVOList(members)); | |
| 107 | + } | |
| 108 | + | |
| 109 | + // ========== 私有方法:VO 组装 ========== | |
| 110 | + | |
| 111 | + /** | |
| 112 | + * 群成员列表批量转 VO + 关联回填昵称 / 头像,并对非本人置空私人字段 | |
| 113 | + */ | |
| 114 | + private List<ImGroupMemberRespVO> buildGroupMemberRespVOList(List<ImGroupMemberDO> members) { | |
| 115 | + if (CollUtil.isEmpty(members)) { | |
| 116 | + return Collections.emptyList(); | |
| 117 | + } | |
| 118 | + Long loginUserId = getLoginUserId(); | |
| 119 | + // 批量聚合 AdminUser 信息(昵称 / 头像),避免 N+1 | |
| 105 | 120 | Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap( |
| 106 | 121 | convertList(members, ImGroupMemberDO::getUserId)); |
| 107 | - return success(convertList(members, m -> { | |
| 108 | - ImGroupMemberRespVO vo = BeanUtils.toBean(m, ImGroupMemberRespVO.class); | |
| 109 | - MapUtils.findAndThen(userMap, m.getUserId(), user -> | |
| 122 | + return convertList(members, member -> { | |
| 123 | + ImGroupMemberRespVO vo = BeanUtils.toBean(member, ImGroupMemberRespVO.class); | |
| 124 | + MapUtils.findAndThen(userMap, member.getUserId(), user -> | |
| 110 | 125 | vo.setNickname(user.getNickname()).setAvatar(user.getAvatar())); |
| 111 | - hidePrivateFieldsIfNotSelf(vo, m.getUserId(), loginUserId); | |
| 126 | + hidePrivateFieldsIfNotSelf(vo, member.getUserId(), loginUserId); | |
| 112 | 127 | return vo; |
| 113 | - })); | |
| 128 | + }); | |
| 114 | 129 | } |
| 115 | 130 | |
| 116 | 131 | /** | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupRequestController.java
| ... | ... | @@ -19,9 +19,12 @@ import cn.iocoder.yudao.module.system.api.user.AdminUserApi; |
| 19 | 19 | import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; |
| 20 | 20 | import io.swagger.v3.oas.annotations.Operation; |
| 21 | 21 | import io.swagger.v3.oas.annotations.Parameter; |
| 22 | +import io.swagger.v3.oas.annotations.Parameters; | |
| 22 | 23 | import io.swagger.v3.oas.annotations.tags.Tag; |
| 23 | 24 | import jakarta.annotation.Resource; |
| 24 | 25 | import jakarta.validation.Valid; |
| 26 | +import jakarta.validation.constraints.Max; | |
| 27 | +import jakarta.validation.constraints.Min; | |
| 25 | 28 | import jakarta.validation.constraints.NotNull; |
| 26 | 29 | import jakarta.validation.constraints.Size; |
| 27 | 30 | import org.springframework.validation.annotation.Validated; |
| ... | ... | @@ -88,6 +91,22 @@ public class ImGroupRequestController { |
| 88 | 91 | return success(buildVOList(list)); |
| 89 | 92 | } |
| 90 | 93 | |
| 94 | + @GetMapping("/pull") | |
| 95 | + @Operation(summary = "增量拉取「我管理的群」下的加群申请(重连 / 离线补偿)") | |
| 96 | + @Parameters({ | |
| 97 | + @Parameter(name = "lastUpdateTime", description = "上次拉取到的最新更新时间(毫秒时间戳);首次拉取不传"), | |
| 98 | + @Parameter(name = "lastId", description = "上次拉取到的最后一条记录 id;首次拉取不传"), | |
| 99 | + @Parameter(name = "limit", description = "单次拉取条数", required = true) | |
| 100 | + }) | |
| 101 | + public CommonResult<List<ImGroupRequestRespVO>> pullMyGroupRequestList( | |
| 102 | + @RequestParam(value = "lastUpdateTime", required = false) Long lastUpdateTime, | |
| 103 | + @RequestParam(value = "lastId", required = false) Long lastId, | |
| 104 | + @RequestParam("limit") @Min(1) @Max(200) Integer limit) { | |
| 105 | + List<ImGroupRequestDO> list = groupRequestService.pullGroupRequestList( | |
| 106 | + getLoginUserId(), lastUpdateTime, lastId, limit); | |
| 107 | + return success(buildVOList(list)); | |
| 108 | + } | |
| 109 | + | |
| 91 | 110 | @GetMapping("/list-by-group") |
| 92 | 111 | @Operation(summary = "查询指定群下的全部加群申请(含已处理);仅群主 / 管理员可查") |
| 93 | 112 | @Parameter(name = "groupId", description = "群编号", required = true, example = "1024") | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/request/ImGroupRequestRespVO.java
| ... | ... | @@ -42,6 +42,9 @@ public class ImGroupRequestRespVO { |
| 42 | 42 | @Schema(description = "申请创建时间") |
| 43 | 43 | private LocalDateTime createTime; |
| 44 | 44 | |
| 45 | + @Schema(description = "申请更新时间;增量拉取游标") | |
| 46 | + private LocalDateTime updateTime; | |
| 47 | + | |
| 45 | 48 | // ========== 下面是聚合字段,方便前端显示 ========== |
| 46 | 49 | |
| 47 | 50 | @Schema(description = "申请人 / 被邀请人昵称(实时聚合自 AdminUser)", example = "芋道") | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/conversation/ImConversationReadMapper.java
| 1 | 1 | package cn.iocoder.yudao.module.im.dal.mysql.conversation; |
| 2 | 2 | |
| 3 | +import cn.hutool.core.date.LocalDateTimeUtil; | |
| 3 | 4 | import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; |
| 4 | 5 | import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; |
| 5 | 6 | import cn.iocoder.yudao.module.im.dal.dataobject.conversation.ImConversationReadDO; |
| ... | ... | @@ -40,19 +41,41 @@ public interface ImConversationReadMapper extends BaseMapperX<ImConversationRead |
| 40 | 41 | } |
| 41 | 42 | |
| 42 | 43 | /** |
| 44 | + * 增量拉取当前用户的会话读位置(按 update_time + id 正向游标) | |
| 45 | + * | |
| 46 | + * @param userId 当前用户编号 | |
| 47 | + * @param lastUpdateTime 上次拉取到的更新时间;首次拉取传 null | |
| 48 | + * @param lastId 上次拉取到的记录编号;首次拉取传 null | |
| 49 | + * @param limit 拉取数量 | |
| 50 | + * @return 会话读位置列表 | |
| 51 | + */ | |
| 52 | + default List<ImConversationReadDO> selectPullListByUserId(Long userId, Long lastUpdateTime, Long lastId, Integer limit) { | |
| 53 | + LambdaQueryWrapperX<ImConversationReadDO> query = new LambdaQueryWrapperX<ImConversationReadDO>() | |
| 54 | + .eq(ImConversationReadDO::getUserId, userId); | |
| 55 | + if (lastUpdateTime != null && lastId != null) { | |
| 56 | + LocalDateTime lastTime = LocalDateTimeUtil.of(lastUpdateTime); | |
| 57 | + query.and(w -> w.gt(ImConversationReadDO::getUpdateTime, lastTime) | |
| 58 | + .or(n -> n.eq(ImConversationReadDO::getUpdateTime, lastTime).gt(ImConversationReadDO::getId, lastId))); | |
| 59 | + } | |
| 60 | + return selectList(query.orderByAsc(ImConversationReadDO::getUpdateTime).orderByAsc(ImConversationReadDO::getId) | |
| 61 | + .last("LIMIT " + limit)); | |
| 62 | + } | |
| 63 | + | |
| 64 | + /** | |
| 43 | 65 | * 单调递增更新读位置:仅当新位置更大时才更新 |
| 44 | 66 | * <p> |
| 45 | 67 | * 通过 {@code read_message_id < messageId} 的 CAS 条件,保证乱序 / 并发上报时读位置不会回退。 |
| 46 | 68 | * |
| 47 | - * @param id 记录编号 | |
| 48 | - * @param messageId 新的最大已读消息编号 | |
| 49 | - * @param readTime 已读时间 | |
| 69 | + * @param id 记录编号 | |
| 70 | + * @param messageId 新的最大已读消息编号 | |
| 71 | + * @param readTime 已读时间 | |
| 50 | 72 | * @return 影响行数;0 表示新位置不大于已有位置,未更新 |
| 51 | 73 | */ |
| 52 | 74 | default int updateReadMessageIdToLarger(Long id, Long messageId, LocalDateTime readTime) { |
| 53 | 75 | return update(null, Wrappers.<ImConversationReadDO>lambdaUpdate() |
| 54 | 76 | .set(ImConversationReadDO::getMessageId, messageId) |
| 55 | 77 | .set(ImConversationReadDO::getReadTime, readTime) |
| 78 | + .set(ImConversationReadDO::getUpdateTime, readTime) | |
| 56 | 79 | .eq(ImConversationReadDO::getId, id) |
| 57 | 80 | .lt(ImConversationReadDO::getMessageId, messageId)); |
| 58 | 81 | } | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/friend/ImFriendMapper.java
| 1 | 1 | package cn.iocoder.yudao.module.im.dal.mysql.friend; |
| 2 | 2 | |
| 3 | +import cn.hutool.core.date.LocalDateTimeUtil; | |
| 3 | 4 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 4 | 5 | import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; |
| 5 | 6 | import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; |
| ... | ... | @@ -31,6 +32,28 @@ public interface ImFriendMapper extends BaseMapperX<ImFriendDO> { |
| 31 | 32 | .eq(ImFriendDO::getUserId, userId) |
| 32 | 33 | .orderByDesc(ImFriendDO::getId)); |
| 33 | 34 | } |
| 35 | + | |
| 36 | + /** | |
| 37 | + * 增量拉取当前用户的好友关系(含已删除,按 update_time + id 正向游标) | |
| 38 | + * | |
| 39 | + * @param userId 当前用户编号 | |
| 40 | + * @param lastUpdateTime 上次拉取到的更新时间;首次拉取传 null | |
| 41 | + * @param lastId 上次拉取到的记录编号;首次拉取传 null | |
| 42 | + * @param limit 拉取数量 | |
| 43 | + * @return 好友关系列表 | |
| 44 | + */ | |
| 45 | + default List<ImFriendDO> selectPullListByUserId(Long userId, Long lastUpdateTime, Long lastId, Integer limit) { | |
| 46 | + LambdaQueryWrapperX<ImFriendDO> query = new LambdaQueryWrapperX<ImFriendDO>() | |
| 47 | + .eq(ImFriendDO::getUserId, userId); | |
| 48 | + if (lastUpdateTime != null && lastId != null) { | |
| 49 | + LocalDateTime lastTime = LocalDateTimeUtil.of(lastUpdateTime); | |
| 50 | + query.and(w -> w.gt(ImFriendDO::getUpdateTime, lastTime) | |
| 51 | + .or(n -> n.eq(ImFriendDO::getUpdateTime, lastTime).gt(ImFriendDO::getId, lastId))); | |
| 52 | + } | |
| 53 | + return selectList(query.orderByAsc(ImFriendDO::getUpdateTime).orderByAsc(ImFriendDO::getId) | |
| 54 | + .last("LIMIT " + limit)); | |
| 55 | + } | |
| 56 | + | |
| 34 | 57 | default List<ImFriendDO> selectListByUserIdAndStatus(Long userId, Integer status) { |
| 35 | 58 | return selectList(new LambdaQueryWrapperX<ImFriendDO>() |
| 36 | 59 | .eq(ImFriendDO::getUserId, userId) |
| ... | ... | @@ -68,12 +91,14 @@ public interface ImFriendMapper extends BaseMapperX<ImFriendDO> { |
| 68 | 91 | |
| 69 | 92 | @SuppressWarnings("UnusedReturnValue") |
| 70 | 93 | default int updateReAddFields(Long id, Integer status, LocalDateTime addTime, |
| 94 | + LocalDateTime updateTime, | |
| 71 | 95 | Boolean silent, Boolean pinned, Boolean blocked, |
| 72 | 96 | String displayName, Integer addSource) { |
| 73 | 97 | return update(null, Wrappers.<ImFriendDO>lambdaUpdate() |
| 74 | 98 | .eq(ImFriendDO::getId, id) |
| 75 | 99 | .set(ImFriendDO::getStatus, status) |
| 76 | 100 | .set(ImFriendDO::getAddTime, addTime) |
| 101 | + .set(ImFriendDO::getUpdateTime, updateTime) | |
| 77 | 102 | .set(ImFriendDO::getSilent, silent) |
| 78 | 103 | .set(ImFriendDO::getPinned, pinned) |
| 79 | 104 | .set(ImFriendDO::getBlocked, blocked) | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/friend/ImFriendRequestMapper.java
| 1 | 1 | package cn.iocoder.yudao.module.im.dal.mysql.friend; |
| 2 | 2 | |
| 3 | +import cn.hutool.core.date.LocalDateTimeUtil; | |
| 3 | 4 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 4 | 5 | import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; |
| 5 | 6 | import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; |
| ... | ... | @@ -28,6 +29,12 @@ public interface ImFriendRequestMapper extends BaseMapperX<ImFriendRequestDO> { |
| 28 | 29 | |
| 29 | 30 | /** |
| 30 | 31 | * 拉取「我相关」的好友申请列表 |
| 32 | + * | |
| 33 | + * @param userId 当前用户编号 | |
| 34 | + * @param maxRequestUpdateTime 起始更新时间;首次拉取传 null | |
| 35 | + * @param maxId 起始记录编号;首次拉取传 null | |
| 36 | + * @param limit 拉取数量 | |
| 37 | + * @return 好友申请列表 | |
| 31 | 38 | */ |
| 32 | 39 | default List<ImFriendRequestDO> selectMyList(Long userId, LocalDateTime maxRequestUpdateTime, |
| 33 | 40 | Long maxId, int limit) { |
| ... | ... | @@ -45,6 +52,28 @@ public interface ImFriendRequestMapper extends BaseMapperX<ImFriendRequestDO> { |
| 45 | 52 | return selectList(wrapper); |
| 46 | 53 | } |
| 47 | 54 | |
| 55 | + /** | |
| 56 | + * 增量拉取「我相关」的好友申请(双向 OR,按 update_time + id 正向游标) | |
| 57 | + * | |
| 58 | + * @param userId 当前用户编号 | |
| 59 | + * @param lastUpdateTime 上次拉取到的更新时间;首次拉取传 null | |
| 60 | + * @param lastId 上次拉取到的记录编号;首次拉取传 null | |
| 61 | + * @param limit 拉取数量 | |
| 62 | + * @return 好友申请列表 | |
| 63 | + */ | |
| 64 | + default List<ImFriendRequestDO> selectPullListByUserId(Long userId, Long lastUpdateTime, Long lastId, Integer limit) { | |
| 65 | + LambdaQueryWrapperX<ImFriendRequestDO> query = new LambdaQueryWrapperX<>(); | |
| 66 | + query.and(w -> w.eq(ImFriendRequestDO::getFromUserId, userId) | |
| 67 | + .or().eq(ImFriendRequestDO::getToUserId, userId)); | |
| 68 | + if (lastUpdateTime != null && lastId != null) { | |
| 69 | + LocalDateTime lastTime = LocalDateTimeUtil.of(lastUpdateTime); | |
| 70 | + query.and(w -> w.gt(ImFriendRequestDO::getUpdateTime, lastTime) | |
| 71 | + .or(n -> n.eq(ImFriendRequestDO::getUpdateTime, lastTime).gt(ImFriendRequestDO::getId, lastId))); | |
| 72 | + } | |
| 73 | + return selectList(query.orderByAsc(ImFriendRequestDO::getUpdateTime).orderByAsc(ImFriendRequestDO::getId) | |
| 74 | + .last("LIMIT " + limit)); | |
| 75 | + } | |
| 76 | + | |
| 48 | 77 | default int updateByIdAndHandleResult(Long id, Integer handleResult, ImFriendRequestDO updateObj) { |
| 49 | 78 | return update(updateObj, new LambdaUpdateWrapper<ImFriendRequestDO>() |
| 50 | 79 | .eq(ImFriendRequestDO::getId, id).eq(ImFriendRequestDO::getHandleResult, handleResult)); | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/group/ImGroupMemberMapper.java
| ... | ... | @@ -130,7 +130,8 @@ public interface ImGroupMemberMapper extends BaseMapperX<ImGroupMemberDO> { |
| 130 | 130 | .set(ImGroupMemberDO::getAddSource, addSource) |
| 131 | 131 | .set(ImGroupMemberDO::getInviterUserId, inviterUserId) |
| 132 | 132 | .set(ImGroupMemberDO::getQuitTime, null) |
| 133 | - .set(ImGroupMemberDO::getMuteEndTime, null)); | |
| 133 | + .set(ImGroupMemberDO::getMuteEndTime, null) | |
| 134 | + .set(ImGroupMemberDO::getUpdateTime, joinTime)); | |
| 134 | 135 | } |
| 135 | 136 | |
| 136 | 137 | } | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/group/ImGroupRequestMapper.java
| 1 | 1 | package cn.iocoder.yudao.module.im.dal.mysql.group; |
| 2 | 2 | |
| 3 | +import cn.hutool.core.date.LocalDateTimeUtil; | |
| 3 | 4 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 4 | 5 | import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; |
| 5 | 6 | import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; |
| ... | ... | @@ -43,6 +44,28 @@ public interface ImGroupRequestMapper extends BaseMapperX<ImGroupRequestDO> { |
| 43 | 44 | .orderByDesc(ImGroupRequestDO::getId)); |
| 44 | 45 | } |
| 45 | 46 | |
| 47 | + /** | |
| 48 | + * 增量拉取「我管理的群」下的加群申请(含已处理,按 update_time + id 正向游标) | |
| 49 | + * | |
| 50 | + * @param groupIds 群编号集合 | |
| 51 | + * @param lastUpdateTime 上次拉取到的更新时间;首次拉取传 null | |
| 52 | + * @param lastId 上次拉取到的记录编号;首次拉取传 null | |
| 53 | + * @param limit 拉取数量 | |
| 54 | + * @return 加群申请列表 | |
| 55 | + */ | |
| 56 | + default List<ImGroupRequestDO> selectPullListByGroupIds(Collection<Long> groupIds, Long lastUpdateTime, | |
| 57 | + Long lastId, Integer limit) { | |
| 58 | + LambdaQueryWrapperX<ImGroupRequestDO> query = new LambdaQueryWrapperX<ImGroupRequestDO>() | |
| 59 | + .in(ImGroupRequestDO::getGroupId, groupIds); | |
| 60 | + if (lastUpdateTime != null && lastId != null) { | |
| 61 | + LocalDateTime lastTime = LocalDateTimeUtil.of(lastUpdateTime); | |
| 62 | + query.and(w -> w.gt(ImGroupRequestDO::getUpdateTime, lastTime) | |
| 63 | + .or(n -> n.eq(ImGroupRequestDO::getUpdateTime, lastTime).gt(ImGroupRequestDO::getId, lastId))); | |
| 64 | + } | |
| 65 | + return selectList(query.orderByAsc(ImGroupRequestDO::getUpdateTime).orderByAsc(ImGroupRequestDO::getId) | |
| 66 | + .last("LIMIT " + limit)); | |
| 67 | + } | |
| 68 | + | |
| 46 | 69 | default int updateByIdAndHandleResult(Long id, Integer expectedHandleResult, ImGroupRequestDO updateObj) { |
| 47 | 70 | return update(updateObj, new LambdaUpdateWrapper<ImGroupRequestDO>() |
| 48 | 71 | .eq(ImGroupRequestDO::getId, id).eq(ImGroupRequestDO::getHandleResult, expectedHandleResult)); | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/conversation/ImConversationReadService.java
| 1 | 1 | package cn.iocoder.yudao.module.im.service.conversation; |
| 2 | 2 | |
| 3 | +import cn.iocoder.yudao.module.im.dal.dataobject.conversation.ImConversationReadDO; | |
| 4 | + | |
| 3 | 5 | import java.util.Collection; |
| 6 | +import java.util.List; | |
| 4 | 7 | import java.util.Map; |
| 5 | 8 | |
| 6 | 9 | /** |
| ... | ... | @@ -50,4 +53,15 @@ public interface ImConversationReadService { |
| 50 | 53 | */ |
| 51 | 54 | Map<Long, Long> getConversationReadMessageIdMap(Long userId, Integer conversationType, Collection<Long> conversationIds); |
| 52 | 55 | |
| 56 | + /** | |
| 57 | + * 增量拉取当前用户的会话读位置(重连 / 离线补偿:按 update_time + id 游标) | |
| 58 | + * | |
| 59 | + * @param userId 用户编号 | |
| 60 | + * @param lastUpdateTime 上次拉取到的最新更新时间(毫秒时间戳);首次拉取传 null | |
| 61 | + * @param lastId 上次拉取到的最后一条记录 id;首次拉取传 null | |
| 62 | + * @param limit 单次拉取条数 | |
| 63 | + * @return 会话读位置列表,按更新时间、id 正序 | |
| 64 | + */ | |
| 65 | + List<ImConversationReadDO> pullConversationReadList(Long userId, Long lastUpdateTime, Long lastId, Integer limit); | |
| 66 | + | |
| 53 | 67 | } | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/conversation/ImConversationReadServiceImpl.java
| ... | ... | @@ -78,4 +78,9 @@ public class ImConversationReadServiceImpl implements ImConversationReadService |
| 78 | 78 | return convertMap(list, ImConversationReadDO::getTargetId, ImConversationReadDO::getMessageId); |
| 79 | 79 | } |
| 80 | 80 | |
| 81 | + @Override | |
| 82 | + public List<ImConversationReadDO> pullConversationReadList(Long userId, Long lastUpdateTime, Long lastId, Integer limit) { | |
| 83 | + return conversationReadMapper.selectPullListByUserId(userId, lastUpdateTime, lastId, limit); | |
| 84 | + } | |
| 85 | + | |
| 81 | 86 | } | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendRequestService.java
| ... | ... | @@ -51,6 +51,17 @@ public interface ImFriendRequestService { |
| 51 | 51 | List<ImFriendRequestDO> getMyFriendRequestList(Long userId, Long maxId, Integer limit); |
| 52 | 52 | |
| 53 | 53 | /** |
| 54 | + * 增量拉取「我相关」的好友申请(重连 / 离线补偿:双向 OR,按 update_time + id 游标) | |
| 55 | + * | |
| 56 | + * @param userId 用户编号 | |
| 57 | + * @param lastUpdateTime 上次拉取到的最新更新时间(毫秒时间戳);首次拉取传 null | |
| 58 | + * @param lastId 上次拉取到的最后一条记录 id;首次拉取传 null | |
| 59 | + * @param limit 单次拉取条数 | |
| 60 | + * @return 申请记录列表,按更新时间、id 正序 | |
| 61 | + */ | |
| 62 | + List<ImFriendRequestDO> pullFriendRequestList(Long userId, Long lastUpdateTime, Long lastId, Integer limit); | |
| 63 | + | |
| 64 | + /** | |
| 54 | 65 | * 按 id 单查申请记录;通用读接口,调用方自行做越权过滤 |
| 55 | 66 | */ |
| 56 | 67 | ImFriendRequestDO getFriendRequest(Long id); | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendRequestServiceImpl.java
| ... | ... | @@ -234,6 +234,11 @@ public class ImFriendRequestServiceImpl implements ImFriendRequestService { |
| 234 | 234 | } |
| 235 | 235 | |
| 236 | 236 | @Override |
| 237 | + public List<ImFriendRequestDO> pullFriendRequestList(Long userId, Long lastUpdateTime, Long lastId, Integer limit) { | |
| 238 | + return friendRequestMapper.selectPullListByUserId(userId, lastUpdateTime, lastId, limit); | |
| 239 | + } | |
| 240 | + | |
| 241 | + @Override | |
| 237 | 242 | public ImFriendRequestDO getFriendRequest(Long id) { |
| 238 | 243 | return friendRequestMapper.selectById(id); |
| 239 | 244 | } | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendService.java
| ... | ... | @@ -43,6 +43,11 @@ public interface ImFriendService { |
| 43 | 43 | List<ImFriendDO> getFriendList(Long userId); |
| 44 | 44 | |
| 45 | 45 | /** |
| 46 | + * 增量拉取当前用户的好友关系(重连 / 离线补偿:含已删除,按 update_time + id 游标) | |
| 47 | + */ | |
| 48 | + List<ImFriendDO> pullFriendList(Long userId, Long lastUpdateTime, Long lastId, Integer limit); | |
| 49 | + | |
| 50 | + /** | |
| 46 | 51 | * 获得当前用户的有效好友列表(仅 ENABLE 状态) |
| 47 | 52 | */ |
| 48 | 53 | List<ImFriendDO> getEnableFriendList(Long userId); | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImpl.java
| ... | ... | @@ -99,6 +99,11 @@ public class ImFriendServiceImpl implements ImFriendService { |
| 99 | 99 | } |
| 100 | 100 | |
| 101 | 101 | @Override |
| 102 | + public List<ImFriendDO> pullFriendList(Long userId, Long lastUpdateTime, Long lastId, Integer limit) { | |
| 103 | + return friendMapper.selectPullListByUserId(userId, lastUpdateTime, lastId, limit); | |
| 104 | + } | |
| 105 | + | |
| 106 | + @Override | |
| 102 | 107 | public List<ImFriendDO> getEnableFriendList(Long userId) { |
| 103 | 108 | return friendMapper.selectListByUserIdAndStatus(userId, CommonStatusEnum.ENABLE.getStatus()); |
| 104 | 109 | } |
| ... | ... | @@ -294,7 +299,8 @@ public class ImFriendServiceImpl implements ImFriendService { |
| 294 | 299 | } |
| 295 | 300 | // 情况二:复用 DISABLE 旧记录 → 恢复 ENABLE + 重置 silent / pinned / blocked,对齐"重新加好友"语义 |
| 296 | 301 | if (exists != null) { |
| 297 | - friendMapper.updateReAddFields(exists.getId(), CommonStatusEnum.ENABLE.getStatus(), LocalDateTime.now(), | |
| 302 | + LocalDateTime now = LocalDateTime.now(); | |
| 303 | + friendMapper.updateReAddFields(exists.getId(), CommonStatusEnum.ENABLE.getStatus(), now, now, | |
| 298 | 304 | false, false, false, displayName, addSource); |
| 299 | 305 | return; |
| 300 | 306 | } | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupRequestService.java
| ... | ... | @@ -64,6 +64,19 @@ public interface ImGroupRequestService { |
| 64 | 64 | List<ImGroupRequestDO> getUnhandledRequestListByOwnerOrAdmin(Long userId); |
| 65 | 65 | |
| 66 | 66 | /** |
| 67 | + * 增量拉取「我管理的群」下的加群申请(重连 / 离线补偿:含已处理,按 update_time + id 游标) | |
| 68 | + * <p> | |
| 69 | + * 作用域与 {@link #getUnhandledRequestListByOwnerOrAdmin} 一致:按 ImGroupMember.role 取我作为 OWNER / ADMIN 的群 | |
| 70 | + * | |
| 71 | + * @param userId 当前用户编号 | |
| 72 | + * @param lastUpdateTime 游标:上次拉取的最后一条更新时间戳(毫秒) | |
| 73 | + * @param lastId 游标:上次拉取的最后一条申请编号 | |
| 74 | + * @param limit 单次拉取条数 | |
| 75 | + * @return 申请记录列表 | |
| 76 | + */ | |
| 77 | + List<ImGroupRequestDO> pullGroupRequestList(Long userId, Long lastUpdateTime, Long lastId, Integer limit); | |
| 78 | + | |
| 79 | + /** | |
| 67 | 80 | * 拉取指定群下的全部加群申请(含已处理);仅群主 / 管理员可查 |
| 68 | 81 | * <p> |
| 69 | 82 | * 用于群「进群申请」子页:最新一条卡片化突出 + 历史申请按 id 倒序 | ... | ... |
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupRequestServiceImpl.java
| ... | ... | @@ -212,8 +212,7 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService { |
| 212 | 212 | // 1. 找出当前用户作为 OWNER / ADMIN 的所有群 |
| 213 | 213 | List<ImGroupMemberDO> myMembers = groupMemberService.getActiveGroupMemberListByUserId(userId); |
| 214 | 214 | Set<Long> ownerOrAdminGroupIds = convertSet(myMembers, |
| 215 | - ImGroupMemberDO::getGroupId, | |
| 216 | - m -> ImGroupMemberRoleEnum.isOwnerOrAdmin(m.getRole())); | |
| 215 | + ImGroupMemberDO::getGroupId, member -> ImGroupMemberRoleEnum.isOwnerOrAdmin(member.getRole())); | |
| 217 | 216 | if (CollUtil.isEmpty(ownerOrAdminGroupIds)) { |
| 218 | 217 | return Collections.emptyList(); |
| 219 | 218 | } |
| ... | ... | @@ -223,6 +222,19 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService { |
| 223 | 222 | } |
| 224 | 223 | |
| 225 | 224 | @Override |
| 225 | + public List<ImGroupRequestDO> pullGroupRequestList(Long userId, Long lastUpdateTime, Long lastId, Integer limit) { | |
| 226 | + // 1. 找出当前用户作为 OWNER / ADMIN 的所有群 | |
| 227 | + List<ImGroupMemberDO> myMembers = groupMemberService.getActiveGroupMemberListByUserId(userId); | |
| 228 | + Set<Long> ownerOrAdminGroupIds = convertSet(myMembers, | |
| 229 | + ImGroupMemberDO::getGroupId, member -> ImGroupMemberRoleEnum.isOwnerOrAdmin(member.getRole())); | |
| 230 | + if (CollUtil.isEmpty(ownerOrAdminGroupIds)) { | |
| 231 | + return Collections.emptyList(); | |
| 232 | + } | |
| 233 | + // 2. 按游标增量拉取这些群下的申请 | |
| 234 | + return groupRequestMapper.selectPullListByGroupIds(ownerOrAdminGroupIds, lastUpdateTime, lastId, limit); | |
| 235 | + } | |
| 236 | + | |
| 237 | + @Override | |
| 226 | 238 | public List<ImGroupRequestDO> getGroupRequestListByGroupId(Long userId, Long groupId) { |
| 227 | 239 | // 1. 校验群存在 + 当前用户是群主 / 管理员 |
| 228 | 240 | groupService.validateGroupExists(groupId); | ... | ... |