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,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<ImConversationRead | @@ -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 | * <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<ImFriendDO> { | @@ -31,6 +32,28 @@ public interface ImFriendMapper extends BaseMapperX<ImFriendDO> { | ||
| 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<ImFriendDO> { | @@ -68,12 +91,14 @@ public interface ImFriendMapper extends BaseMapperX<ImFriendDO> { | ||
| 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<ImFriendRequestDO> { | @@ -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 | 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<ImFriendRequestDO> { | @@ -45,6 +52,28 @@ public interface ImFriendRequestMapper extends BaseMapperX<ImFriendRequestDO> { | ||
| 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<ImGroupMemberDO> { | @@ -130,7 +130,8 @@ public interface ImGroupMemberMapper extends BaseMapperX<ImGroupMemberDO> { | ||
| 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<ImGroupRequestDO> { | @@ -43,6 +44,28 @@ public interface ImGroupRequestMapper extends BaseMapperX<ImGroupRequestDO> { | ||
| 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); |