Commit 210a8d5af65f9bcf4c46d6efe4a7c30784130ba6

Authored by YunaiV
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,6 +17,8 @@ import io.swagger.v3.oas.annotations.Parameters;
17 import io.swagger.v3.oas.annotations.tags.Tag; 17 import io.swagger.v3.oas.annotations.tags.Tag;
18 import jakarta.annotation.Resource; 18 import jakarta.annotation.Resource;
19 import jakarta.validation.Valid; 19 import jakarta.validation.Valid;
  20 +import jakarta.validation.constraints.Max;
  21 +import jakarta.validation.constraints.Min;
20 import jakarta.validation.constraints.NotNull; 22 import jakarta.validation.constraints.NotNull;
21 import org.springframework.validation.annotation.Validated; 23 import org.springframework.validation.annotation.Validated;
22 import org.springframework.web.bind.annotation.*; 24 import org.springframework.web.bind.annotation.*;
@@ -46,11 +48,25 @@ public class ImFriendController { @@ -46,11 +48,25 @@ public class ImFriendController {
46 @GetMapping("/list") 48 @GetMapping("/list")
47 @Operation(summary = "获得当前登录用户的好友列表") 49 @Operation(summary = "获得当前登录用户的好友列表")
48 public CommonResult<List<ImFriendRespVO>> getMyFriendList() { 50 public CommonResult<List<ImFriendRespVO>> getMyFriendList() {
49 - // 含 DISABLE 历史好友:保留给前端展示「已删除好友」的历史对话信息;前端按 status 决定会话级联清理  
50 List<ImFriendDO> friends = friendService.getFriendList(getLoginUserId()); 51 List<ImFriendDO> friends = friendService.getFriendList(getLoginUserId());
51 return success(buildFriendRespVOList(friends)); 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 @GetMapping("/get") 70 @GetMapping("/get")
55 @Operation(summary = "获得好友详情") 71 @Operation(summary = "获得好友详情")
56 @Parameter(name = "friendUserId", description = "好友的用户编号", required = true, example = "2048") 72 @Parameter(name = "friendUserId", description = "好友的用户编号", required = true, example = "2048")
@@ -62,8 +78,8 @@ public class ImFriendController { @@ -62,8 +78,8 @@ public class ImFriendController {
62 @DeleteMapping("/delete") 78 @DeleteMapping("/delete")
63 @Operation(summary = "删除好友(单向软删除)") 79 @Operation(summary = "删除好友(单向软删除)")
64 @Parameters({ 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 public CommonResult<Boolean> deleteFriend( 84 public CommonResult<Boolean> deleteFriend(
69 @RequestParam("friendUserId") @NotNull(message = "好友用户编号不能为空") Long friendUserId, 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,6 +13,7 @@ import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
13 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; 13 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
14 import io.swagger.v3.oas.annotations.Operation; 14 import io.swagger.v3.oas.annotations.Operation;
15 import io.swagger.v3.oas.annotations.Parameter; 15 import io.swagger.v3.oas.annotations.Parameter;
  16 +import io.swagger.v3.oas.annotations.Parameters;
16 import io.swagger.v3.oas.annotations.tags.Tag; 17 import io.swagger.v3.oas.annotations.tags.Tag;
17 import jakarta.annotation.Resource; 18 import jakarta.annotation.Resource;
18 import jakarta.validation.Valid; 19 import jakarta.validation.Valid;
@@ -79,15 +80,32 @@ public class ImFriendRequestController { @@ -79,15 +80,32 @@ public class ImFriendRequestController {
79 80
80 @GetMapping("/list") 81 @GetMapping("/list")
81 @Operation(summary = "查询「我相关」的好友申请列表(游标分页:传 maxId 加载更多)") 82 @Operation(summary = "查询「我相关」的好友申请列表(游标分页:传 maxId 加载更多)")
  83 + @Parameters({
  84 + @Parameter(name = "maxId", description = "当前列表最旧记录的 id;首页不传"),
  85 + @Parameter(name = "limit", description = "单次拉取条数", required = true)
  86 + })
82 public CommonResult<List<ImFriendRequestRespVO>> getMyFriendRequestList( 87 public CommonResult<List<ImFriendRequestRespVO>> getMyFriendRequestList(
83 - @Parameter(description = "当前列表最旧记录的 id;首页不传")  
84 @RequestParam(value = "maxId", required = false) Long maxId, 88 @RequestParam(value = "maxId", required = false) Long maxId,
85 - @Parameter(description = "单次拉取条数", required = true)  
86 @RequestParam("limit") @Min(1) @Max(200) Integer limit) { 89 @RequestParam("limit") @Min(1) @Max(200) Integer limit) {
87 List<ImFriendRequestDO> list = friendRequestService.getMyFriendRequestList(getLoginUserId(), maxId, limit); 90 List<ImFriendRequestDO> list = friendRequestService.getMyFriendRequestList(getLoginUserId(), maxId, limit);
88 return success(buildList(list)); 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 @GetMapping("/get") 109 @GetMapping("/get")
92 @Operation(summary = "按 id 单查「我相关」的申请记录(带越权过滤;WebSocket 通知到达后用)") 110 @Operation(summary = "按 id 单查「我相关」的申请记录(带越权过滤;WebSocket 通知到达后用)")
93 @Parameter(name = "id", description = "申请记录编号", required = true) 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,6 +47,9 @@ public class ImFriendRespVO {
47 @Schema(description = "删除好友时间") 47 @Schema(description = "删除好友时间")
48 private LocalDateTime deleteTime; 48 private LocalDateTime deleteTime;
49 49
  50 + @Schema(description = "最近更新时间(增量拉取游标用)")
  51 + private LocalDateTime updateTime;
  52 +
50 // ========== 下面是聚合字段,方便前端显示 ========== 53 // ========== 下面是聚合字段,方便前端显示 ==========
51 54
52 @Schema(description = "好友昵称(实时聚合自 AdminUser)", example = "芋道") 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,6 +41,9 @@ public class ImFriendRequestRespVO {
41 @Schema(description = "申请创建时间") 41 @Schema(description = "申请创建时间")
42 private LocalDateTime createTime; 42 private LocalDateTime createTime;
43 43
  44 + @Schema(description = "最近更新时间(增量拉取游标用)")
  45 + private LocalDateTime updateTime;
  46 +
44 // ========== 下面是聚合字段,方便前端显示 ========== 47 // ========== 下面是聚合字段,方便前端显示 ==========
45 48
46 @Schema(description = "发起方昵称(实时聚合自 AdminUser)", example = "芋道") 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,6 +21,7 @@ import jakarta.validation.Valid;
21 import org.springframework.validation.annotation.Validated; 21 import org.springframework.validation.annotation.Validated;
22 import org.springframework.web.bind.annotation.*; 22 import org.springframework.web.bind.annotation.*;
23 23
  24 +import java.util.Collections;
24 import java.util.List; 25 import java.util.List;
25 import java.util.Map; 26 import java.util.Map;
26 27
@@ -88,7 +89,7 @@ public class ImGroupMemberController { @@ -88,7 +89,7 @@ public class ImGroupMemberController {
88 } 89 }
89 90
90 @GetMapping("/list") 91 @GetMapping("/list")
91 - @Operation(summary = "获得指定群的成员列表") 92 + @Operation(summary = "获得指定群的成员列表(按群全量拉取,作为前端本地成员 cache 的完整基线)")
92 @Parameter(name = "groupId", description = "群编号", required = true, example = "1024") 93 @Parameter(name = "groupId", description = "群编号", required = true, example = "1024")
93 public CommonResult<List<ImGroupMemberRespVO>> getGroupMemberList(@RequestParam("groupId") Long groupId) { 94 public CommonResult<List<ImGroupMemberRespVO>> getGroupMemberList(@RequestParam("groupId") Long groupId) {
94 // 1.1 查询群成员列表(包含 DISABLE 已退群的成员,不按时间过滤) 95 // 1.1 查询群成员列表(包含 DISABLE 已退群的成员,不按时间过滤)
@@ -101,16 +102,30 @@ public class ImGroupMemberController { @@ -101,16 +102,30 @@ public class ImGroupMemberController {
101 throw exception(GROUP_MEMBER_NOT_IN_GROUP); 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 Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap( 120 Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
106 convertList(members, ImGroupMemberDO::getUserId)); 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 vo.setNickname(user.getNickname()).setAvatar(user.getAvatar())); 125 vo.setNickname(user.getNickname()).setAvatar(user.getAvatar()));
111 - hidePrivateFieldsIfNotSelf(vo, m.getUserId(), loginUserId); 126 + hidePrivateFieldsIfNotSelf(vo, member.getUserId(), loginUserId);
112 return vo; 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,9 +19,12 @@ import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
19 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; 19 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
20 import io.swagger.v3.oas.annotations.Operation; 20 import io.swagger.v3.oas.annotations.Operation;
21 import io.swagger.v3.oas.annotations.Parameter; 21 import io.swagger.v3.oas.annotations.Parameter;
  22 +import io.swagger.v3.oas.annotations.Parameters;
22 import io.swagger.v3.oas.annotations.tags.Tag; 23 import io.swagger.v3.oas.annotations.tags.Tag;
23 import jakarta.annotation.Resource; 24 import jakarta.annotation.Resource;
24 import jakarta.validation.Valid; 25 import jakarta.validation.Valid;
  26 +import jakarta.validation.constraints.Max;
  27 +import jakarta.validation.constraints.Min;
25 import jakarta.validation.constraints.NotNull; 28 import jakarta.validation.constraints.NotNull;
26 import jakarta.validation.constraints.Size; 29 import jakarta.validation.constraints.Size;
27 import org.springframework.validation.annotation.Validated; 30 import org.springframework.validation.annotation.Validated;
@@ -88,6 +91,22 @@ public class ImGroupRequestController { @@ -88,6 +91,22 @@ public class ImGroupRequestController {
88 return success(buildVOList(list)); 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 @GetMapping("/list-by-group") 110 @GetMapping("/list-by-group")
92 @Operation(summary = "查询指定群下的全部加群申请(含已处理);仅群主 / 管理员可查") 111 @Operation(summary = "查询指定群下的全部加群申请(含已处理);仅群主 / 管理员可查")
93 @Parameter(name = "groupId", description = "群编号", required = true, example = "1024") 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,6 +42,9 @@ public class ImGroupRequestRespVO {
42 @Schema(description = "申请创建时间") 42 @Schema(description = "申请创建时间")
43 private LocalDateTime createTime; 43 private LocalDateTime createTime;
44 44
  45 + @Schema(description = "申请更新时间;增量拉取游标")
  46 + private LocalDateTime updateTime;
  47 +
45 // ========== 下面是聚合字段,方便前端显示 ========== 48 // ========== 下面是聚合字段,方便前端显示 ==========
46 49
47 @Schema(description = "申请人 / 被邀请人昵称(实时聚合自 AdminUser)", example = "芋道") 50 @Schema(description = "申请人 / 被邀请人昵称(实时聚合自 AdminUser)", example = "芋道")
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/conversation/ImConversationReadMapper.java
1 package cn.iocoder.yudao.module.im.dal.mysql.conversation; 1 package cn.iocoder.yudao.module.im.dal.mysql.conversation;
2 2
  3 +import cn.hutool.core.date.LocalDateTimeUtil;
3 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; 4 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
4 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; 5 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
5 import cn.iocoder.yudao.module.im.dal.dataobject.conversation.ImConversationReadDO; 6 import cn.iocoder.yudao.module.im.dal.dataobject.conversation.ImConversationReadDO;
@@ -40,19 +41,41 @@ public interface ImConversationReadMapper extends BaseMapperX&lt;ImConversationRead @@ -40,19 +41,41 @@ public interface ImConversationReadMapper extends BaseMapperX&lt;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 * <p> 66 * <p>
45 * 通过 {@code read_message_id < messageId} 的 CAS 条件,保证乱序 / 并发上报时读位置不会回退。 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 * @return 影响行数;0 表示新位置不大于已有位置,未更新 72 * @return 影响行数;0 表示新位置不大于已有位置,未更新
51 */ 73 */
52 default int updateReadMessageIdToLarger(Long id, Long messageId, LocalDateTime readTime) { 74 default int updateReadMessageIdToLarger(Long id, Long messageId, LocalDateTime readTime) {
53 return update(null, Wrappers.<ImConversationReadDO>lambdaUpdate() 75 return update(null, Wrappers.<ImConversationReadDO>lambdaUpdate()
54 .set(ImConversationReadDO::getMessageId, messageId) 76 .set(ImConversationReadDO::getMessageId, messageId)
55 .set(ImConversationReadDO::getReadTime, readTime) 77 .set(ImConversationReadDO::getReadTime, readTime)
  78 + .set(ImConversationReadDO::getUpdateTime, readTime)
56 .eq(ImConversationReadDO::getId, id) 79 .eq(ImConversationReadDO::getId, id)
57 .lt(ImConversationReadDO::getMessageId, messageId)); 80 .lt(ImConversationReadDO::getMessageId, messageId));
58 } 81 }
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/friend/ImFriendMapper.java
1 package cn.iocoder.yudao.module.im.dal.mysql.friend; 1 package cn.iocoder.yudao.module.im.dal.mysql.friend;
2 2
  3 +import cn.hutool.core.date.LocalDateTimeUtil;
3 import cn.iocoder.yudao.framework.common.pojo.PageResult; 4 import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; 5 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
5 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; 6 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
@@ -31,6 +32,28 @@ public interface ImFriendMapper extends BaseMapperX&lt;ImFriendDO&gt; { @@ -31,6 +32,28 @@ public interface ImFriendMapper extends BaseMapperX&lt;ImFriendDO&gt; {
31 .eq(ImFriendDO::getUserId, userId) 32 .eq(ImFriendDO::getUserId, userId)
32 .orderByDesc(ImFriendDO::getId)); 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 default List<ImFriendDO> selectListByUserIdAndStatus(Long userId, Integer status) { 57 default List<ImFriendDO> selectListByUserIdAndStatus(Long userId, Integer status) {
35 return selectList(new LambdaQueryWrapperX<ImFriendDO>() 58 return selectList(new LambdaQueryWrapperX<ImFriendDO>()
36 .eq(ImFriendDO::getUserId, userId) 59 .eq(ImFriendDO::getUserId, userId)
@@ -68,12 +91,14 @@ public interface ImFriendMapper extends BaseMapperX&lt;ImFriendDO&gt; { @@ -68,12 +91,14 @@ public interface ImFriendMapper extends BaseMapperX&lt;ImFriendDO&gt; {
68 91
69 @SuppressWarnings("UnusedReturnValue") 92 @SuppressWarnings("UnusedReturnValue")
70 default int updateReAddFields(Long id, Integer status, LocalDateTime addTime, 93 default int updateReAddFields(Long id, Integer status, LocalDateTime addTime,
  94 + LocalDateTime updateTime,
71 Boolean silent, Boolean pinned, Boolean blocked, 95 Boolean silent, Boolean pinned, Boolean blocked,
72 String displayName, Integer addSource) { 96 String displayName, Integer addSource) {
73 return update(null, Wrappers.<ImFriendDO>lambdaUpdate() 97 return update(null, Wrappers.<ImFriendDO>lambdaUpdate()
74 .eq(ImFriendDO::getId, id) 98 .eq(ImFriendDO::getId, id)
75 .set(ImFriendDO::getStatus, status) 99 .set(ImFriendDO::getStatus, status)
76 .set(ImFriendDO::getAddTime, addTime) 100 .set(ImFriendDO::getAddTime, addTime)
  101 + .set(ImFriendDO::getUpdateTime, updateTime)
77 .set(ImFriendDO::getSilent, silent) 102 .set(ImFriendDO::getSilent, silent)
78 .set(ImFriendDO::getPinned, pinned) 103 .set(ImFriendDO::getPinned, pinned)
79 .set(ImFriendDO::getBlocked, blocked) 104 .set(ImFriendDO::getBlocked, blocked)
yudao-module-im/src/main/java/cn/iocoder/yudao/module/im/dal/mysql/friend/ImFriendRequestMapper.java
1 package cn.iocoder.yudao.module.im.dal.mysql.friend; 1 package cn.iocoder.yudao.module.im.dal.mysql.friend;
2 2
  3 +import cn.hutool.core.date.LocalDateTimeUtil;
3 import cn.iocoder.yudao.framework.common.pojo.PageResult; 4 import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; 5 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
5 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; 6 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
@@ -28,6 +29,12 @@ public interface ImFriendRequestMapper extends BaseMapperX&lt;ImFriendRequestDO&gt; { @@ -28,6 +29,12 @@ public interface ImFriendRequestMapper extends BaseMapperX&lt;ImFriendRequestDO&gt; {
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 default List<ImFriendRequestDO> selectMyList(Long userId, LocalDateTime maxRequestUpdateTime, 39 default List<ImFriendRequestDO> selectMyList(Long userId, LocalDateTime maxRequestUpdateTime,
33 Long maxId, int limit) { 40 Long maxId, int limit) {
@@ -45,6 +52,28 @@ public interface ImFriendRequestMapper extends BaseMapperX&lt;ImFriendRequestDO&gt; { @@ -45,6 +52,28 @@ public interface ImFriendRequestMapper extends BaseMapperX&lt;ImFriendRequestDO&gt; {
45 return selectList(wrapper); 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 default int updateByIdAndHandleResult(Long id, Integer handleResult, ImFriendRequestDO updateObj) { 77 default int updateByIdAndHandleResult(Long id, Integer handleResult, ImFriendRequestDO updateObj) {
49 return update(updateObj, new LambdaUpdateWrapper<ImFriendRequestDO>() 78 return update(updateObj, new LambdaUpdateWrapper<ImFriendRequestDO>()
50 .eq(ImFriendRequestDO::getId, id).eq(ImFriendRequestDO::getHandleResult, handleResult)); 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&lt;ImGroupMemberDO&gt; { @@ -130,7 +130,8 @@ public interface ImGroupMemberMapper extends BaseMapperX&lt;ImGroupMemberDO&gt; {
130 .set(ImGroupMemberDO::getAddSource, addSource) 130 .set(ImGroupMemberDO::getAddSource, addSource)
131 .set(ImGroupMemberDO::getInviterUserId, inviterUserId) 131 .set(ImGroupMemberDO::getInviterUserId, inviterUserId)
132 .set(ImGroupMemberDO::getQuitTime, null) 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 package cn.iocoder.yudao.module.im.dal.mysql.group; 1 package cn.iocoder.yudao.module.im.dal.mysql.group;
2 2
  3 +import cn.hutool.core.date.LocalDateTimeUtil;
3 import cn.iocoder.yudao.framework.common.pojo.PageResult; 4 import cn.iocoder.yudao.framework.common.pojo.PageResult;
4 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; 5 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
5 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; 6 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
@@ -43,6 +44,28 @@ public interface ImGroupRequestMapper extends BaseMapperX&lt;ImGroupRequestDO&gt; { @@ -43,6 +44,28 @@ public interface ImGroupRequestMapper extends BaseMapperX&lt;ImGroupRequestDO&gt; {
43 .orderByDesc(ImGroupRequestDO::getId)); 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 default int updateByIdAndHandleResult(Long id, Integer expectedHandleResult, ImGroupRequestDO updateObj) { 69 default int updateByIdAndHandleResult(Long id, Integer expectedHandleResult, ImGroupRequestDO updateObj) {
47 return update(updateObj, new LambdaUpdateWrapper<ImGroupRequestDO>() 70 return update(updateObj, new LambdaUpdateWrapper<ImGroupRequestDO>()
48 .eq(ImGroupRequestDO::getId, id).eq(ImGroupRequestDO::getHandleResult, expectedHandleResult)); 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 package cn.iocoder.yudao.module.im.service.conversation; 1 package cn.iocoder.yudao.module.im.service.conversation;
2 2
  3 +import cn.iocoder.yudao.module.im.dal.dataobject.conversation.ImConversationReadDO;
  4 +
3 import java.util.Collection; 5 import java.util.Collection;
  6 +import java.util.List;
4 import java.util.Map; 7 import java.util.Map;
5 8
6 /** 9 /**
@@ -50,4 +53,15 @@ public interface ImConversationReadService { @@ -50,4 +53,15 @@ public interface ImConversationReadService {
50 */ 53 */
51 Map<Long, Long> getConversationReadMessageIdMap(Long userId, Integer conversationType, Collection<Long> conversationIds); 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,4 +78,9 @@ public class ImConversationReadServiceImpl implements ImConversationReadService
78 return convertMap(list, ImConversationReadDO::getTargetId, ImConversationReadDO::getMessageId); 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,6 +51,17 @@ public interface ImFriendRequestService {
51 List<ImFriendRequestDO> getMyFriendRequestList(Long userId, Long maxId, Integer limit); 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 * 按 id 单查申请记录;通用读接口,调用方自行做越权过滤 65 * 按 id 单查申请记录;通用读接口,调用方自行做越权过滤
55 */ 66 */
56 ImFriendRequestDO getFriendRequest(Long id); 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,6 +234,11 @@ public class ImFriendRequestServiceImpl implements ImFriendRequestService {
234 } 234 }
235 235
236 @Override 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 public ImFriendRequestDO getFriendRequest(Long id) { 242 public ImFriendRequestDO getFriendRequest(Long id) {
238 return friendRequestMapper.selectById(id); 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,6 +43,11 @@ public interface ImFriendService {
43 List<ImFriendDO> getFriendList(Long userId); 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 * 获得当前用户的有效好友列表(仅 ENABLE 状态) 51 * 获得当前用户的有效好友列表(仅 ENABLE 状态)
47 */ 52 */
48 List<ImFriendDO> getEnableFriendList(Long userId); 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,6 +99,11 @@ public class ImFriendServiceImpl implements ImFriendService {
99 } 99 }
100 100
101 @Override 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 public List<ImFriendDO> getEnableFriendList(Long userId) { 107 public List<ImFriendDO> getEnableFriendList(Long userId) {
103 return friendMapper.selectListByUserIdAndStatus(userId, CommonStatusEnum.ENABLE.getStatus()); 108 return friendMapper.selectListByUserIdAndStatus(userId, CommonStatusEnum.ENABLE.getStatus());
104 } 109 }
@@ -294,7 +299,8 @@ public class ImFriendServiceImpl implements ImFriendService { @@ -294,7 +299,8 @@ public class ImFriendServiceImpl implements ImFriendService {
294 } 299 }
295 // 情况二:复用 DISABLE 旧记录 → 恢复 ENABLE + 重置 silent / pinned / blocked,对齐"重新加好友"语义 300 // 情况二:复用 DISABLE 旧记录 → 恢复 ENABLE + 重置 silent / pinned / blocked,对齐"重新加好友"语义
296 if (exists != null) { 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 false, false, false, displayName, addSource); 304 false, false, false, displayName, addSource);
299 return; 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,6 +64,19 @@ public interface ImGroupRequestService {
64 List<ImGroupRequestDO> getUnhandledRequestListByOwnerOrAdmin(Long userId); 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 * <p> 81 * <p>
69 * 用于群「进群申请」子页:最新一条卡片化突出 + 历史申请按 id 倒序 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,8 +212,7 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService {
212 // 1. 找出当前用户作为 OWNER / ADMIN 的所有群 212 // 1. 找出当前用户作为 OWNER / ADMIN 的所有群
213 List<ImGroupMemberDO> myMembers = groupMemberService.getActiveGroupMemberListByUserId(userId); 213 List<ImGroupMemberDO> myMembers = groupMemberService.getActiveGroupMemberListByUserId(userId);
214 Set<Long> ownerOrAdminGroupIds = convertSet(myMembers, 214 Set<Long> ownerOrAdminGroupIds = convertSet(myMembers,
215 - ImGroupMemberDO::getGroupId,  
216 - m -> ImGroupMemberRoleEnum.isOwnerOrAdmin(m.getRole())); 215 + ImGroupMemberDO::getGroupId, member -> ImGroupMemberRoleEnum.isOwnerOrAdmin(member.getRole()));
217 if (CollUtil.isEmpty(ownerOrAdminGroupIds)) { 216 if (CollUtil.isEmpty(ownerOrAdminGroupIds)) {
218 return Collections.emptyList(); 217 return Collections.emptyList();
219 } 218 }
@@ -223,6 +222,19 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService { @@ -223,6 +222,19 @@ public class ImGroupRequestServiceImpl implements ImGroupRequestService {
223 } 222 }
224 223
225 @Override 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 public List<ImGroupRequestDO> getGroupRequestListByGroupId(Long userId, Long groupId) { 238 public List<ImGroupRequestDO> getGroupRequestListByGroupId(Long userId, Long groupId) {
227 // 1. 校验群存在 + 当前用户是群主 / 管理员 239 // 1. 校验群存在 + 当前用户是群主 / 管理员
228 groupService.validateGroupExists(groupId); 240 groupService.validateGroupExists(groupId);