♻️ refactor(im):用户申请列表,增加流式查询,避免一次性加载过多,或者历史无法被加载到。

im
YunaiV 2026-05-04 22:46:20 +08:00
parent 9fc25b7109
commit 14e3f85cb0
4 changed files with 103 additions and 16 deletions

View File

@ -45,7 +45,22 @@ export const refuseFriendRequest = (id: number | string, handleContent?: string)
}) })
} }
// 查询「我相关」的好友申请列表(含我发起的、别人加我的) // 查询「我相关」的好友申请列表(游标分页:传 lastRequestId 加载更多)
export const getMyFriendRequestList = () => { export const getMyFriendRequestList = (limit: number, lastRequestId?: number) => {
return request.get<ImFriendRequestRespVO[]>({ url: '/im/friend-request/list' }) const params: Record<string, number> = { limit }
if (lastRequestId != null) {
params.lastRequestId = lastRequestId
}
return request.get<ImFriendRequestRespVO[]>({
url: '/im/friend-request/list',
params
})
}
// 按 id 单查「我相关」的申请记录带越权过滤WebSocket 通知到达后用)
export const getMyFriendRequest = (id: number) => {
return request.get<ImFriendRequestRespVO | null>({
url: '/im/friend-request/get',
params: { id }
})
} }

View File

@ -62,6 +62,14 @@
> >
暂无新的朋友 暂无新的朋友
</div> </div>
<!-- 加载更多按本地最旧 requestId 游标分页拉下一批hasMore=false 不展示 -->
<div
v-else-if="friendStore.hasMoreFriendRequests"
class="py-2 text-12px text-center cursor-pointer text-[var(--el-text-color-secondary)] hover:bg-[var(--el-fill-color-light)]"
@click="handleLoadMore"
>
{{ loadingMore ? '加载中…' : '加载更多' }}
</div>
</div> </div>
</div> </div>
</template> </template>
@ -108,4 +116,17 @@ const enrichedRequests = computed(() =>
props.requests.map((request) => ({ request, peer: getPeer(request) })) props.requests.map((request) => ({ request, peer: getPeer(request) }))
) )
/** 点击「加载更多」拉下一页store 内部按 lastRequestId 游标分页 + pending 去重 */
const loadingMore = ref(false)
async function handleLoadMore() {
if (loadingMore.value) {
return
}
loadingMore.value = true
try {
await friendStore.loadMoreFriendRequests()
} finally {
loadingMore.value = false
}
}
</script> </script>

View File

@ -16,11 +16,16 @@ import {
agreeFriendRequest as apiAgreeFriendRequest, agreeFriendRequest as apiAgreeFriendRequest,
refuseFriendRequest as apiRefuseFriendRequest, refuseFriendRequest as apiRefuseFriendRequest,
getMyFriendRequestList as apiGetMyFriendRequestList, getMyFriendRequestList as apiGetMyFriendRequestList,
getMyFriendRequest as apiGetMyFriendRequest,
type ImFriendRequestApplyReqVO, type ImFriendRequestApplyReqVO,
type ImFriendRequestRespVO type ImFriendRequestRespVO
} from '@/api/im/friend/request' } from '@/api/im/friend/request'
import { useConversationStore } from './conversationStore' import { useConversationStore } from './conversationStore'
import { ImConversationType, ImFriendRequestHandleResult } from '../../utils/constants' import {
FRIEND_REQUEST_PAGE_SIZE,
ImConversationType,
ImFriendRequestHandleResult
} from '../../utils/constants'
import { getCurrentUserId, imStorage, setQuietly, StorageKeys } from '../../utils/storage' import { getCurrentUserId, imStorage, setQuietly, StorageKeys } from '../../utils/storage'
import { getFriendDisplayName } from '../../utils/user' import { getFriendDisplayName } from '../../utils/user'
import type { Friend, FriendRequest } from '../types' import type { Friend, FriendRequest } from '../types'
@ -29,6 +34,8 @@ import type { Friend, FriendRequest } from '../types'
let pendingFetchFriends: Promise<void> | null = null let pendingFetchFriends: Promise<void> | null = null
/** 当前正在进行的好友申请列表拉取;多端连续多条申请到达时复用同一 Promise避免雪崩重拉 */ /** 当前正在进行的好友申请列表拉取;多端连续多条申请到达时复用同一 Promise避免雪崩重拉 */
let pendingFetchRequests: Promise<void> | null = null let pendingFetchRequests: Promise<void> | null = null
/** 当前正在进行的「加载更多申请」请求 */
let pendingLoadMoreRequests: Promise<void> | null = null
/** 好友通知 payload对齐后端 BaseFriendNotification + 子类裁减后的字段) */ /** 好友通知 payload对齐后端 BaseFriendNotification + 子类裁减后的字段) */
export interface FriendNotificationPayload { export interface FriendNotificationPayload {
@ -61,8 +68,10 @@ export const useFriendStore = defineStore('imFriendStore', {
friends: [] as Friend[], friends: [] as Friend[],
// 仅 fetchFriends 成功后置位loadFriendsIDB不置位否则后台 SWR 刷新会被缓存命中跳过 // 仅 fetchFriends 成功后置位loadFriendsIDB不置位否则后台 SWR 刷新会被缓存命中跳过
loaded: false, loaded: false,
/** 我相关的好友申请列表(含我发起的 + 别人加我的;后端按 id 倒序,前端不再分页) */ /** 我相关的好友申请列表(含我发起的 + 别人加我的;后端按 id 倒序游标分页) */
friendRequests: [] as FriendRequest[] friendRequests: [] as FriendRequest[],
/** 是否还有更早的申请记录可加载;返回不满 page size 即置 false */
hasMoreFriendRequests: true
}), }),
getters: { getters: {
@ -191,9 +200,8 @@ export const useFriendStore = defineStore('imFriendStore', {
request.handleResult = ImFriendRequestHandleResult.AGREED request.handleResult = ImFriendRequestHandleResult.AGREED
request.handleTime = Date.now() request.handleTime = Date.now()
} else { } else {
// 列表过期场景兜底重拉 // 本地列表没这条,按 id 单查兜底
// TODO @AI是不是只拉这个人避免拉所有 await this.loadFriendRequest(requestId)
await this.fetchFriendRequests()
} }
}, },
@ -206,18 +214,21 @@ export const useFriendStore = defineStore('imFriendStore', {
request.handleContent = handleContent request.handleContent = handleContent
request.handleTime = Date.now() request.handleTime = Date.now()
} else { } else {
await this.fetchFriendRequests() await this.loadFriendRequest(requestId)
} }
}, },
/** 拉取「我相关」的好友申请列表(页面打开 / 收到 FRIEND_REQUEST_RECEIVED 时刷新pending 期间复用同一 Promise */ /** 拉取「我相关」的好友申请列表首页(页面打开 / 收到 FRIEND_REQUEST_RECEIVED 时刷新pending 期间复用同一 Promise */
async fetchFriendRequests() { async fetchFriendRequests() {
if (pendingFetchRequests) { if (pendingFetchRequests) {
return pendingFetchRequests return pendingFetchRequests
} }
pendingFetchRequests = apiGetMyFriendRequestList() pendingFetchRequests = apiGetMyFriendRequestList(FRIEND_REQUEST_PAGE_SIZE)
.then((list) => { .then((list) => {
this.friendRequests = (list || []).map(convertFriendRequest) const items = (list || []).map(convertFriendRequest)
this.friendRequests = items
// 不足一页即没有更多;满页可能还有,等 loadMore 拉到 0 条再确定
this.hasMoreFriendRequests = items.length >= FRIEND_REQUEST_PAGE_SIZE
}) })
.finally(() => { .finally(() => {
pendingFetchRequests = null pendingFetchRequests = null
@ -225,11 +236,47 @@ export const useFriendStore = defineStore('imFriendStore', {
return pendingFetchRequests return pendingFetchRequests
}, },
/** 加载更多申请(按本地最旧 requestId 游标分页);无更多 / pending 中直接返回 */
async loadMoreFriendRequests() {
if (!this.hasMoreFriendRequests || pendingLoadMoreRequests || pendingFetchRequests) {
return
}
const oldest = this.friendRequests[this.friendRequests.length - 1]
if (!oldest) {
return this.fetchFriendRequests()
}
pendingLoadMoreRequests = apiGetMyFriendRequestList(FRIEND_REQUEST_PAGE_SIZE, oldest.id)
.then((list) => {
const items = (list || []).map(convertFriendRequest)
this.friendRequests.push(...items)
this.hasMoreFriendRequests = items.length >= FRIEND_REQUEST_PAGE_SIZE
})
.finally(() => {
pendingLoadMoreRequests = null
})
return pendingLoadMoreRequests
},
/** 按 id 查申请记录;列表是按 id 倒序的小列表O(n) find 即可,不再维护 Map 索引 */ /** 按 id 查申请记录;列表是按 id 倒序的小列表O(n) find 即可,不再维护 Map 索引 */
findFriendRequest(requestId: number): FriendRequest | undefined { findFriendRequest(requestId: number): FriendRequest | undefined {
return this.friendRequests.find((request) => request.id === requestId) return this.friendRequests.find((request) => request.id === requestId)
}, },
/** 按 id 从后端单查并 upsert 到本地dispatcher 兜底用,避免全量重拉);后端带越权过滤 */
async loadFriendRequest(requestId: number) {
const data = await apiGetMyFriendRequest(requestId)
if (!data) {
return
}
const next = convertFriendRequest(data)
const existing = this.findFriendRequest(requestId)
if (existing) {
Object.assign(existing, next)
} else {
this.friendRequests.unshift(next)
}
},
// ==================== 好友关系操作 ==================== // ==================== 好友关系操作 ====================
/** 删除好友(单向软删,本端置 DISABLE级联清理本地私聊会话 */ /** 删除好友(单向软删,本端置 DISABLE级联清理本地私聊会话 */
@ -364,8 +411,8 @@ export const useFriendStore = defineStore('imFriendStore', {
request.handleResult = ImFriendRequestHandleResult.AGREED request.handleResult = ImFriendRequestHandleResult.AGREED
request.handleTime = Date.now() request.handleTime = Date.now()
} else { } else {
// 本地列表可能未初始化(例如刚登录还没进 contact 页),兜底重拉 // 本地列表可能没这条(例如刚登录还没进 contact 页),按 id 单查兜底
void this.fetchFriendRequests() void this.loadFriendRequest(payload.requestId!)
} }
}, },
@ -377,7 +424,7 @@ export const useFriendStore = defineStore('imFriendStore', {
request.handleContent = payload.handleContent request.handleContent = payload.handleContent
request.handleTime = Date.now() request.handleTime = Date.now()
} else { } else {
void this.fetchFriendRequests() void this.loadFriendRequest(payload.requestId!)
} }
}, },
@ -442,6 +489,7 @@ export const useFriendStore = defineStore('imFriendStore', {
this.friends = [] this.friends = []
this.friendRequests = [] this.friendRequests = []
this.loaded = false this.loaded = false
this.hasMoreFriendRequests = true
} }
} }
}) })

View File

@ -141,6 +141,9 @@ export const PRIVATE_MESSAGE_PULL_SIZE = 100
/** 每次拉取群聊消息的最大条数(后端上限 1000前端取保守值 100 */ /** 每次拉取群聊消息的最大条数(后端上限 1000前端取保守值 100 */
export const GROUP_MESSAGE_PULL_SIZE = 100 export const GROUP_MESSAGE_PULL_SIZE = 100
/** 「我相关」好友申请列表的单次拉取条数(游标分页 page size前端控制 */
export const FRIEND_REQUEST_PAGE_SIZE = 100
/** 消息之间渲染「时间分隔条」的阈值10 分钟 */ /** 消息之间渲染「时间分隔条」的阈值10 分钟 */
export const TIME_TIP_GAP_MS = 10 * 60 * 1000 export const TIME_TIP_GAP_MS = 10 * 60 * 1000