From 210a8d5af65f9bcf4c46d6efe4a7c30784130ba6 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 15 Jun 2026 08:26:32 +0800 Subject: [PATCH] feat(im): 增强消息拉取与状态补偿可靠性 --- yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/conversation/ImConversationReadController.java | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/conversation/vo/ImConversationReadRespVO.java | 32 ++++++++++++++++++++++++++++++++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/ImFriendController.java | 22 +++++++++++++++++++--- yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/ImFriendRequestController.java | 22 ++++++++++++++++++++-- yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/vo/ImFriendRespVO.java | 3 +++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/vo/request/ImFriendRequestRespVO.java | 3 +++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupMemberController.java | 29 ++++++++++++++++++++++------- yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupRequestController.java | 19 +++++++++++++++++++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/request/ImGroupRequestRespVO.java | 3 +++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/conversation/ImConversationReadMapper.java | 29 ++++++++++++++++++++++++++--- yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/friend/ImFriendMapper.java | 25 +++++++++++++++++++++++++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/friend/ImFriendRequestMapper.java | 29 +++++++++++++++++++++++++++++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/group/ImGroupMemberMapper.java | 3 ++- yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/group/ImGroupRequestMapper.java | 23 +++++++++++++++++++++++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/conversation/ImConversationReadService.java | 14 ++++++++++++++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/conversation/ImConversationReadServiceImpl.java | 5 +++++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendRequestService.java | 11 +++++++++++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendRequestServiceImpl.java | 5 +++++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendService.java | 5 +++++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImpl.java | 8 +++++++- yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupRequestService.java | 13 +++++++++++++ yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupRequestServiceImpl.java | 16 ++++++++++++++-- 22 files changed, 351 insertions(+), 19 deletions(-) create mode 100644 yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/conversation/ImConversationReadController.java create mode 100644 yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/conversation/vo/ImConversationReadRespVO.java diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/conversation/ImConversationReadController.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/conversation/ImConversationReadController.java new file mode 100644 index 0000000..aea2b9c --- /dev/null +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/conversation/ImConversationReadController.java @@ -0,0 +1,51 @@ +package cn.iocoder.yudao.module.im.controller.admin.conversation; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.im.controller.admin.conversation.vo.ImConversationReadRespVO; +import cn.iocoder.yudao.module.im.dal.dataobject.conversation.ImConversationReadDO; +import cn.iocoder.yudao.module.im.service.conversation.ImConversationReadService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - IM 会话读位置") +@RestController +@RequestMapping("/im/conversation-read") +@Validated +public class ImConversationReadController { + + @Resource + private ImConversationReadService conversationReadService; + + @GetMapping("/pull") + @Operation(summary = "增量拉取当前用户的会话读位置(重连 / 离线补偿)") + @Parameters({ + @Parameter(name = "lastUpdateTime", description = "上次拉取到的最新更新时间(毫秒时间戳);首次拉取不传"), + @Parameter(name = "lastId", description = "上次拉取到的最后一条记录 id;首次拉取不传"), + @Parameter(name = "limit", description = "单次拉取条数", required = true) + }) + public CommonResult> pullMyConversationRead( + @RequestParam(value = "lastUpdateTime", required = false) Long lastUpdateTime, + @RequestParam(value = "lastId", required = false) Long lastId, + @RequestParam("limit") @Min(1) @Max(200) Integer limit) { + List list = conversationReadService.pullConversationReadList( + getLoginUserId(), lastUpdateTime, lastId, limit); + return success(BeanUtils.toBean(list, ImConversationReadRespVO.class)); + } + +} diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/conversation/vo/ImConversationReadRespVO.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/conversation/vo/ImConversationReadRespVO.java new file mode 100644 index 0000000..2cafb46 --- /dev/null +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/conversation/vo/ImConversationReadRespVO.java @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.module.im.controller.admin.conversation.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * IM 会话读位置 Response VO + * + * @author 芋道源码 + */ +@Schema(description = "管理后台 - IM 会话读位置 Response VO") +@Data +public class ImConversationReadRespVO { + + @Schema(description = "读位置编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "会话类型", example = "1") + private Integer conversationType; // 参见 ImConversationTypeEnum 枚举 + + @Schema(description = "目标编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + private Long targetId; + + @Schema(description = "最大已读消息编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "4096") + private Long messageId; + + @Schema(description = "最近更新时间(增量拉取游标用)") + private LocalDateTime updateTime; + +} diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/ImFriendController.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/ImFriendController.java index 43668f8..1cb8913 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/ImFriendController.java +++ b/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; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.validation.Valid; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotNull; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @@ -46,11 +48,25 @@ public class ImFriendController { @GetMapping("/list") @Operation(summary = "获得当前登录用户的好友列表") public CommonResult> getMyFriendList() { - // 含 DISABLE 历史好友:保留给前端展示「已删除好友」的历史对话信息;前端按 status 决定会话级联清理 List friends = friendService.getFriendList(getLoginUserId()); return success(buildFriendRespVOList(friends)); } + @GetMapping("/pull") + @Operation(summary = "增量拉取当前用户的好友关系(重连 / 离线补偿)") + @Parameters({ + @Parameter(name = "lastUpdateTime", description = "上次拉取到的最新更新时间(毫秒时间戳);首次拉取不传"), + @Parameter(name = "lastId", description = "上次拉取到的最后一条记录 id;首次拉取不传"), + @Parameter(name = "limit", description = "单次拉取条数", required = true) + }) + public CommonResult> pullMyFriendList( + @RequestParam(value = "lastUpdateTime", required = false) Long lastUpdateTime, + @RequestParam(value = "lastId", required = false) Long lastId, + @RequestParam("limit") @Min(1) @Max(200) Integer limit) { + List list = friendService.pullFriendList(getLoginUserId(), lastUpdateTime, lastId, limit); + return success(buildFriendRespVOList(list)); + } + @GetMapping("/get") @Operation(summary = "获得好友详情") @Parameter(name = "friendUserId", description = "好友的用户编号", required = true, example = "2048") @@ -62,8 +78,8 @@ public class ImFriendController { @DeleteMapping("/delete") @Operation(summary = "删除好友(单向软删除)") @Parameters({ - @Parameter(description = "好友的用户编号", required = true, example = "2048"), - @Parameter(description = "是否级联清理本端相关数据(如私聊会话)") + @Parameter(name = "friendUserId", description = "好友的用户编号", required = true, example = "2048"), + @Parameter(name = "clear", description = "是否级联清理本端相关数据(如私聊会话)") }) public CommonResult deleteFriend( @RequestParam("friendUserId") @NotNull(message = "好友用户编号不能为空") Long friendUserId, diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/ImFriendRequestController.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/ImFriendRequestController.java index 10b2351..c074568 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/ImFriendRequestController.java +++ b/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; import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.validation.Valid; @@ -79,15 +80,32 @@ public class ImFriendRequestController { @GetMapping("/list") @Operation(summary = "查询「我相关」的好友申请列表(游标分页:传 maxId 加载更多)") + @Parameters({ + @Parameter(name = "maxId", description = "当前列表最旧记录的 id;首页不传"), + @Parameter(name = "limit", description = "单次拉取条数", required = true) + }) public CommonResult> getMyFriendRequestList( - @Parameter(description = "当前列表最旧记录的 id;首页不传") @RequestParam(value = "maxId", required = false) Long maxId, - @Parameter(description = "单次拉取条数", required = true) @RequestParam("limit") @Min(1) @Max(200) Integer limit) { List list = friendRequestService.getMyFriendRequestList(getLoginUserId(), maxId, limit); return success(buildList(list)); } + @GetMapping("/pull") + @Operation(summary = "增量拉取「我相关」的好友申请(重连 / 离线补偿)") + @Parameters({ + @Parameter(name = "lastUpdateTime", description = "上次拉取到的最新更新时间(毫秒时间戳);首次拉取不传"), + @Parameter(name = "lastId", description = "上次拉取到的最后一条记录 id;首次拉取不传"), + @Parameter(name = "limit", description = "单次拉取条数", required = true) + }) + public CommonResult> pullMyFriendRequestList( + @RequestParam(value = "lastUpdateTime", required = false) Long lastUpdateTime, + @RequestParam(value = "lastId", required = false) Long lastId, + @RequestParam("limit") @Min(1) @Max(200) Integer limit) { + List list = friendRequestService.pullFriendRequestList(getLoginUserId(), lastUpdateTime, lastId, limit); + return success(buildList(list)); + } + @GetMapping("/get") @Operation(summary = "按 id 单查「我相关」的申请记录(带越权过滤;WebSocket 通知到达后用)") @Parameter(name = "id", description = "申请记录编号", required = true) diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/vo/ImFriendRespVO.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/vo/ImFriendRespVO.java index 2e17b51..6f4b14e 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/vo/ImFriendRespVO.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/vo/ImFriendRespVO.java @@ -47,6 +47,9 @@ public class ImFriendRespVO { @Schema(description = "删除好友时间") private LocalDateTime deleteTime; + @Schema(description = "最近更新时间(增量拉取游标用)") + private LocalDateTime updateTime; + // ========== 下面是聚合字段,方便前端显示 ========== @Schema(description = "好友昵称(实时聚合自 AdminUser)", example = "芋道") diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/vo/request/ImFriendRequestRespVO.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/vo/request/ImFriendRequestRespVO.java index 6f8f93d..d32a098 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/friend/vo/request/ImFriendRequestRespVO.java +++ b/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 { @Schema(description = "申请创建时间") private LocalDateTime createTime; + @Schema(description = "最近更新时间(增量拉取游标用)") + private LocalDateTime updateTime; + // ========== 下面是聚合字段,方便前端显示 ========== @Schema(description = "发起方昵称(实时聚合自 AdminUser)", example = "芋道") diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupMemberController.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupMemberController.java index 4c62469..c752eeb 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupMemberController.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupMemberController.java @@ -21,6 +21,7 @@ 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; @@ -88,7 +89,7 @@ public class ImGroupMemberController { } @GetMapping("/list") - @Operation(summary = "获得指定群的成员列表") + @Operation(summary = "获得指定群的成员列表(按群全量拉取,作为前端本地成员 cache 的完整基线)") @Parameter(name = "groupId", description = "群编号", required = true, example = "1024") public CommonResult> getGroupMemberList(@RequestParam("groupId") Long groupId) { // 1.1 查询群成员列表(包含 DISABLE 已退群的成员,不按时间过滤) @@ -101,16 +102,30 @@ public class ImGroupMemberController { throw exception(GROUP_MEMBER_NOT_IN_GROUP); } - // 2.批量聚合 AdminUser 信息(昵称 / 头像) + // 2. 批量聚合昵称 / 头像,并对非本人置空私人字段 + return success(buildGroupMemberRespVOList(members)); + } + + // ========== 私有方法:VO 组装 ========== + + /** + * 群成员列表批量转 VO + 关联回填昵称 / 头像,并对非本人置空私人字段 + */ + private List buildGroupMemberRespVOList(List members) { + if (CollUtil.isEmpty(members)) { + return Collections.emptyList(); + } + Long loginUserId = getLoginUserId(); + // 批量聚合 AdminUser 信息(昵称 / 头像),避免 N+1 Map userMap = adminUserApi.getUserMap( convertList(members, ImGroupMemberDO::getUserId)); - return success(convertList(members, m -> { - ImGroupMemberRespVO vo = BeanUtils.toBean(m, ImGroupMemberRespVO.class); - MapUtils.findAndThen(userMap, m.getUserId(), user -> + return convertList(members, member -> { + ImGroupMemberRespVO vo = BeanUtils.toBean(member, ImGroupMemberRespVO.class); + MapUtils.findAndThen(userMap, member.getUserId(), user -> vo.setNickname(user.getNickname()).setAvatar(user.getAvatar())); - hidePrivateFieldsIfNotSelf(vo, m.getUserId(), loginUserId); + hidePrivateFieldsIfNotSelf(vo, member.getUserId(), loginUserId); return vo; - })); + }); } /** diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupRequestController.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupRequestController.java index 440e30b..6ffebe5 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/ImGroupRequestController.java +++ b/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; import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.validation.Valid; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; import org.springframework.validation.annotation.Validated; @@ -88,6 +91,22 @@ public class ImGroupRequestController { return success(buildVOList(list)); } + @GetMapping("/pull") + @Operation(summary = "增量拉取「我管理的群」下的加群申请(重连 / 离线补偿)") + @Parameters({ + @Parameter(name = "lastUpdateTime", description = "上次拉取到的最新更新时间(毫秒时间戳);首次拉取不传"), + @Parameter(name = "lastId", description = "上次拉取到的最后一条记录 id;首次拉取不传"), + @Parameter(name = "limit", description = "单次拉取条数", required = true) + }) + public CommonResult> pullMyGroupRequestList( + @RequestParam(value = "lastUpdateTime", required = false) Long lastUpdateTime, + @RequestParam(value = "lastId", required = false) Long lastId, + @RequestParam("limit") @Min(1) @Max(200) Integer limit) { + List list = groupRequestService.pullGroupRequestList( + getLoginUserId(), lastUpdateTime, lastId, limit); + return success(buildVOList(list)); + } + @GetMapping("/list-by-group") @Operation(summary = "查询指定群下的全部加群申请(含已处理);仅群主 / 管理员可查") @Parameter(name = "groupId", description = "群编号", required = true, example = "1024") diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/request/ImGroupRequestRespVO.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/request/ImGroupRequestRespVO.java index 587ca8d..9e21cae 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/controller/admin/group/vo/request/ImGroupRequestRespVO.java +++ b/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 { @Schema(description = "申请创建时间") private LocalDateTime createTime; + @Schema(description = "申请更新时间;增量拉取游标") + private LocalDateTime updateTime; + // ========== 下面是聚合字段,方便前端显示 ========== @Schema(description = "申请人 / 被邀请人昵称(实时聚合自 AdminUser)", example = "芋道") diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/conversation/ImConversationReadMapper.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/conversation/ImConversationReadMapper.java index 2e6a672..45a702f 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/conversation/ImConversationReadMapper.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/conversation/ImConversationReadMapper.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.im.dal.mysql.conversation; +import cn.hutool.core.date.LocalDateTimeUtil; import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; import cn.iocoder.yudao.module.im.dal.dataobject.conversation.ImConversationReadDO; @@ -40,19 +41,41 @@ public interface ImConversationReadMapper extends BaseMapperX selectPullListByUserId(Long userId, Long lastUpdateTime, Long lastId, Integer limit) { + LambdaQueryWrapperX query = new LambdaQueryWrapperX() + .eq(ImConversationReadDO::getUserId, userId); + if (lastUpdateTime != null && lastId != null) { + LocalDateTime lastTime = LocalDateTimeUtil.of(lastUpdateTime); + query.and(w -> w.gt(ImConversationReadDO::getUpdateTime, lastTime) + .or(n -> n.eq(ImConversationReadDO::getUpdateTime, lastTime).gt(ImConversationReadDO::getId, lastId))); + } + return selectList(query.orderByAsc(ImConversationReadDO::getUpdateTime).orderByAsc(ImConversationReadDO::getId) + .last("LIMIT " + limit)); + } + + /** * 单调递增更新读位置:仅当新位置更大时才更新 *

* 通过 {@code read_message_id < messageId} 的 CAS 条件,保证乱序 / 并发上报时读位置不会回退。 * - * @param id 记录编号 - * @param messageId 新的最大已读消息编号 - * @param readTime 已读时间 + * @param id 记录编号 + * @param messageId 新的最大已读消息编号 + * @param readTime 已读时间 * @return 影响行数;0 表示新位置不大于已有位置,未更新 */ default int updateReadMessageIdToLarger(Long id, Long messageId, LocalDateTime readTime) { return update(null, Wrappers.lambdaUpdate() .set(ImConversationReadDO::getMessageId, messageId) .set(ImConversationReadDO::getReadTime, readTime) + .set(ImConversationReadDO::getUpdateTime, readTime) .eq(ImConversationReadDO::getId, id) .lt(ImConversationReadDO::getMessageId, messageId)); } diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/friend/ImFriendMapper.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/friend/ImFriendMapper.java index 83752a9..8673100 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/friend/ImFriendMapper.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/friend/ImFriendMapper.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.im.dal.mysql.friend; +import cn.hutool.core.date.LocalDateTimeUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; @@ -31,6 +32,28 @@ public interface ImFriendMapper extends BaseMapperX { .eq(ImFriendDO::getUserId, userId) .orderByDesc(ImFriendDO::getId)); } + + /** + * 增量拉取当前用户的好友关系(含已删除,按 update_time + id 正向游标) + * + * @param userId 当前用户编号 + * @param lastUpdateTime 上次拉取到的更新时间;首次拉取传 null + * @param lastId 上次拉取到的记录编号;首次拉取传 null + * @param limit 拉取数量 + * @return 好友关系列表 + */ + default List selectPullListByUserId(Long userId, Long lastUpdateTime, Long lastId, Integer limit) { + LambdaQueryWrapperX query = new LambdaQueryWrapperX() + .eq(ImFriendDO::getUserId, userId); + if (lastUpdateTime != null && lastId != null) { + LocalDateTime lastTime = LocalDateTimeUtil.of(lastUpdateTime); + query.and(w -> w.gt(ImFriendDO::getUpdateTime, lastTime) + .or(n -> n.eq(ImFriendDO::getUpdateTime, lastTime).gt(ImFriendDO::getId, lastId))); + } + return selectList(query.orderByAsc(ImFriendDO::getUpdateTime).orderByAsc(ImFriendDO::getId) + .last("LIMIT " + limit)); + } + default List selectListByUserIdAndStatus(Long userId, Integer status) { return selectList(new LambdaQueryWrapperX() .eq(ImFriendDO::getUserId, userId) @@ -68,12 +91,14 @@ public interface ImFriendMapper extends BaseMapperX { @SuppressWarnings("UnusedReturnValue") default int updateReAddFields(Long id, Integer status, LocalDateTime addTime, + LocalDateTime updateTime, Boolean silent, Boolean pinned, Boolean blocked, String displayName, Integer addSource) { return update(null, Wrappers.lambdaUpdate() .eq(ImFriendDO::getId, id) .set(ImFriendDO::getStatus, status) .set(ImFriendDO::getAddTime, addTime) + .set(ImFriendDO::getUpdateTime, updateTime) .set(ImFriendDO::getSilent, silent) .set(ImFriendDO::getPinned, pinned) .set(ImFriendDO::getBlocked, blocked) diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/friend/ImFriendRequestMapper.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/friend/ImFriendRequestMapper.java index e81def0..6dffaa5 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/friend/ImFriendRequestMapper.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/friend/ImFriendRequestMapper.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.im.dal.mysql.friend; +import cn.hutool.core.date.LocalDateTimeUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; @@ -28,6 +29,12 @@ public interface ImFriendRequestMapper extends BaseMapperX { /** * 拉取「我相关」的好友申请列表 + * + * @param userId 当前用户编号 + * @param maxRequestUpdateTime 起始更新时间;首次拉取传 null + * @param maxId 起始记录编号;首次拉取传 null + * @param limit 拉取数量 + * @return 好友申请列表 */ default List selectMyList(Long userId, LocalDateTime maxRequestUpdateTime, Long maxId, int limit) { @@ -45,6 +52,28 @@ public interface ImFriendRequestMapper extends BaseMapperX { return selectList(wrapper); } + /** + * 增量拉取「我相关」的好友申请(双向 OR,按 update_time + id 正向游标) + * + * @param userId 当前用户编号 + * @param lastUpdateTime 上次拉取到的更新时间;首次拉取传 null + * @param lastId 上次拉取到的记录编号;首次拉取传 null + * @param limit 拉取数量 + * @return 好友申请列表 + */ + default List selectPullListByUserId(Long userId, Long lastUpdateTime, Long lastId, Integer limit) { + LambdaQueryWrapperX query = new LambdaQueryWrapperX<>(); + query.and(w -> w.eq(ImFriendRequestDO::getFromUserId, userId) + .or().eq(ImFriendRequestDO::getToUserId, userId)); + if (lastUpdateTime != null && lastId != null) { + LocalDateTime lastTime = LocalDateTimeUtil.of(lastUpdateTime); + query.and(w -> w.gt(ImFriendRequestDO::getUpdateTime, lastTime) + .or(n -> n.eq(ImFriendRequestDO::getUpdateTime, lastTime).gt(ImFriendRequestDO::getId, lastId))); + } + return selectList(query.orderByAsc(ImFriendRequestDO::getUpdateTime).orderByAsc(ImFriendRequestDO::getId) + .last("LIMIT " + limit)); + } + default int updateByIdAndHandleResult(Long id, Integer handleResult, ImFriendRequestDO updateObj) { return update(updateObj, new LambdaUpdateWrapper() .eq(ImFriendRequestDO::getId, id).eq(ImFriendRequestDO::getHandleResult, handleResult)); diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/group/ImGroupMemberMapper.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/group/ImGroupMemberMapper.java index 4a40292..2afa559 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/group/ImGroupMemberMapper.java +++ b/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 { .set(ImGroupMemberDO::getAddSource, addSource) .set(ImGroupMemberDO::getInviterUserId, inviterUserId) .set(ImGroupMemberDO::getQuitTime, null) - .set(ImGroupMemberDO::getMuteEndTime, null)); + .set(ImGroupMemberDO::getMuteEndTime, null) + .set(ImGroupMemberDO::getUpdateTime, joinTime)); } } diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/group/ImGroupRequestMapper.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/group/ImGroupRequestMapper.java index 72fe756..c344493 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/group/ImGroupRequestMapper.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/group/ImGroupRequestMapper.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.im.dal.mysql.group; +import cn.hutool.core.date.LocalDateTimeUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; @@ -43,6 +44,28 @@ public interface ImGroupRequestMapper extends BaseMapperX { .orderByDesc(ImGroupRequestDO::getId)); } + /** + * 增量拉取「我管理的群」下的加群申请(含已处理,按 update_time + id 正向游标) + * + * @param groupIds 群编号集合 + * @param lastUpdateTime 上次拉取到的更新时间;首次拉取传 null + * @param lastId 上次拉取到的记录编号;首次拉取传 null + * @param limit 拉取数量 + * @return 加群申请列表 + */ + default List selectPullListByGroupIds(Collection groupIds, Long lastUpdateTime, + Long lastId, Integer limit) { + LambdaQueryWrapperX query = new LambdaQueryWrapperX() + .in(ImGroupRequestDO::getGroupId, groupIds); + if (lastUpdateTime != null && lastId != null) { + LocalDateTime lastTime = LocalDateTimeUtil.of(lastUpdateTime); + query.and(w -> w.gt(ImGroupRequestDO::getUpdateTime, lastTime) + .or(n -> n.eq(ImGroupRequestDO::getUpdateTime, lastTime).gt(ImGroupRequestDO::getId, lastId))); + } + return selectList(query.orderByAsc(ImGroupRequestDO::getUpdateTime).orderByAsc(ImGroupRequestDO::getId) + .last("LIMIT " + limit)); + } + default int updateByIdAndHandleResult(Long id, Integer expectedHandleResult, ImGroupRequestDO updateObj) { return update(updateObj, new LambdaUpdateWrapper() .eq(ImGroupRequestDO::getId, id).eq(ImGroupRequestDO::getHandleResult, expectedHandleResult)); diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/conversation/ImConversationReadService.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/conversation/ImConversationReadService.java index fd843ff..9c057ae 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/conversation/ImConversationReadService.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/conversation/ImConversationReadService.java @@ -1,6 +1,9 @@ package cn.iocoder.yudao.module.im.service.conversation; +import cn.iocoder.yudao.module.im.dal.dataobject.conversation.ImConversationReadDO; + import java.util.Collection; +import java.util.List; import java.util.Map; /** @@ -50,4 +53,15 @@ public interface ImConversationReadService { */ Map getConversationReadMessageIdMap(Long userId, Integer conversationType, Collection conversationIds); + /** + * 增量拉取当前用户的会话读位置(重连 / 离线补偿:按 update_time + id 游标) + * + * @param userId 用户编号 + * @param lastUpdateTime 上次拉取到的最新更新时间(毫秒时间戳);首次拉取传 null + * @param lastId 上次拉取到的最后一条记录 id;首次拉取传 null + * @param limit 单次拉取条数 + * @return 会话读位置列表,按更新时间、id 正序 + */ + List pullConversationReadList(Long userId, Long lastUpdateTime, Long lastId, Integer limit); + } diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/conversation/ImConversationReadServiceImpl.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/conversation/ImConversationReadServiceImpl.java index bcfb945..a22d616 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/conversation/ImConversationReadServiceImpl.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/conversation/ImConversationReadServiceImpl.java @@ -78,4 +78,9 @@ public class ImConversationReadServiceImpl implements ImConversationReadService return convertMap(list, ImConversationReadDO::getTargetId, ImConversationReadDO::getMessageId); } + @Override + public List pullConversationReadList(Long userId, Long lastUpdateTime, Long lastId, Integer limit) { + return conversationReadMapper.selectPullListByUserId(userId, lastUpdateTime, lastId, limit); + } + } diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendRequestService.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendRequestService.java index a78bb38..8470206 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendRequestService.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendRequestService.java @@ -51,6 +51,17 @@ public interface ImFriendRequestService { List getMyFriendRequestList(Long userId, Long maxId, Integer limit); /** + * 增量拉取「我相关」的好友申请(重连 / 离线补偿:双向 OR,按 update_time + id 游标) + * + * @param userId 用户编号 + * @param lastUpdateTime 上次拉取到的最新更新时间(毫秒时间戳);首次拉取传 null + * @param lastId 上次拉取到的最后一条记录 id;首次拉取传 null + * @param limit 单次拉取条数 + * @return 申请记录列表,按更新时间、id 正序 + */ + List pullFriendRequestList(Long userId, Long lastUpdateTime, Long lastId, Integer limit); + + /** * 按 id 单查申请记录;通用读接口,调用方自行做越权过滤 */ ImFriendRequestDO getFriendRequest(Long id); diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendRequestServiceImpl.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendRequestServiceImpl.java index d024e9d..0a682cf 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendRequestServiceImpl.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendRequestServiceImpl.java @@ -234,6 +234,11 @@ public class ImFriendRequestServiceImpl implements ImFriendRequestService { } @Override + public List pullFriendRequestList(Long userId, Long lastUpdateTime, Long lastId, Integer limit) { + return friendRequestMapper.selectPullListByUserId(userId, lastUpdateTime, lastId, limit); + } + + @Override public ImFriendRequestDO getFriendRequest(Long id) { return friendRequestMapper.selectById(id); } diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendService.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendService.java index 84a6de9..1d1fc57 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendService.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendService.java @@ -43,6 +43,11 @@ public interface ImFriendService { List getFriendList(Long userId); /** + * 增量拉取当前用户的好友关系(重连 / 离线补偿:含已删除,按 update_time + id 游标) + */ + List pullFriendList(Long userId, Long lastUpdateTime, Long lastId, Integer limit); + + /** * 获得当前用户的有效好友列表(仅 ENABLE 状态) */ List getEnableFriendList(Long userId); diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImpl.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImpl.java index 1919aae..69d70e1 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImpl.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImpl.java @@ -99,6 +99,11 @@ public class ImFriendServiceImpl implements ImFriendService { } @Override + public List pullFriendList(Long userId, Long lastUpdateTime, Long lastId, Integer limit) { + return friendMapper.selectPullListByUserId(userId, lastUpdateTime, lastId, limit); + } + + @Override public List getEnableFriendList(Long userId) { return friendMapper.selectListByUserIdAndStatus(userId, CommonStatusEnum.ENABLE.getStatus()); } @@ -294,7 +299,8 @@ public class ImFriendServiceImpl implements ImFriendService { } // 情况二:复用 DISABLE 旧记录 → 恢复 ENABLE + 重置 silent / pinned / blocked,对齐"重新加好友"语义 if (exists != null) { - friendMapper.updateReAddFields(exists.getId(), CommonStatusEnum.ENABLE.getStatus(), LocalDateTime.now(), + LocalDateTime now = LocalDateTime.now(); + friendMapper.updateReAddFields(exists.getId(), CommonStatusEnum.ENABLE.getStatus(), now, now, false, false, false, displayName, addSource); return; } diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupRequestService.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupRequestService.java index bd604f6..1c15d9b 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupRequestService.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupRequestService.java @@ -64,6 +64,19 @@ public interface ImGroupRequestService { List getUnhandledRequestListByOwnerOrAdmin(Long userId); /** + * 增量拉取「我管理的群」下的加群申请(重连 / 离线补偿:含已处理,按 update_time + id 游标) + *

+ * 作用域与 {@link #getUnhandledRequestListByOwnerOrAdmin} 一致:按 ImGroupMember.role 取我作为 OWNER / ADMIN 的群 + * + * @param userId 当前用户编号 + * @param lastUpdateTime 游标:上次拉取的最后一条更新时间戳(毫秒) + * @param lastId 游标:上次拉取的最后一条申请编号 + * @param limit 单次拉取条数 + * @return 申请记录列表 + */ + List pullGroupRequestList(Long userId, Long lastUpdateTime, Long lastId, Integer limit); + + /** * 拉取指定群下的全部加群申请(含已处理);仅群主 / 管理员可查 *

* 用于群「进群申请」子页:最新一条卡片化突出 + 历史申请按 id 倒序 diff --git a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupRequestServiceImpl.java b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupRequestServiceImpl.java index 71d3d76..ff23b13 100644 --- a/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupRequestServiceImpl.java +++ b/yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/service/group/ImGroupRequestServiceImpl.java @@ -212,8 +212,7 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService { // 1. 找出当前用户作为 OWNER / ADMIN 的所有群 List myMembers = groupMemberService.getActiveGroupMemberListByUserId(userId); Set ownerOrAdminGroupIds = convertSet(myMembers, - ImGroupMemberDO::getGroupId, - m -> ImGroupMemberRoleEnum.isOwnerOrAdmin(m.getRole())); + ImGroupMemberDO::getGroupId, member -> ImGroupMemberRoleEnum.isOwnerOrAdmin(member.getRole())); if (CollUtil.isEmpty(ownerOrAdminGroupIds)) { return Collections.emptyList(); } @@ -223,6 +222,19 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService { } @Override + public List pullGroupRequestList(Long userId, Long lastUpdateTime, Long lastId, Integer limit) { + // 1. 找出当前用户作为 OWNER / ADMIN 的所有群 + List myMembers = groupMemberService.getActiveGroupMemberListByUserId(userId); + Set ownerOrAdminGroupIds = convertSet(myMembers, + ImGroupMemberDO::getGroupId, member -> ImGroupMemberRoleEnum.isOwnerOrAdmin(member.getRole())); + if (CollUtil.isEmpty(ownerOrAdminGroupIds)) { + return Collections.emptyList(); + } + // 2. 按游标增量拉取这些群下的申请 + return groupRequestMapper.selectPullListByGroupIds(ownerOrAdminGroupIds, lastUpdateTime, lastId, limit); + } + + @Override public List getGroupRequestListByGroupId(Long userId, Long groupId) { // 1. 校验群存在 + 当前用户是群主 / 管理员 groupService.validateGroupExists(groupId); -- libgit2 0.21.4