♻️ 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)
})
}
// 查询「我相关」的好友申请列表(含我发起的、别人加我的)
export const getMyFriendRequestList = () => {
return request.get<ImFriendRequestRespVO[]>({ url: '/im/friend-request/list' })
// 查询「我相关」的好友申请列表(游标分页:传 lastRequestId 加载更多)
export const getMyFriendRequestList = (limit: number, lastRequestId?: number) => {
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>
<!-- 加载更多按本地最旧 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>
</template>
@ -108,4 +116,17 @@ const enrichedRequests = computed(() =>
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>

View File

@ -16,11 +16,16 @@ import {
agreeFriendRequest as apiAgreeFriendRequest,
refuseFriendRequest as apiRefuseFriendRequest,
getMyFriendRequestList as apiGetMyFriendRequestList,
getMyFriendRequest as apiGetMyFriendRequest,
type ImFriendRequestApplyReqVO,
type ImFriendRequestRespVO
} from '@/api/im/friend/request'
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 { getFriendDisplayName } from '../../utils/user'
import type { Friend, FriendRequest } from '../types'
@ -29,6 +34,8 @@ import type { Friend, FriendRequest } from '../types'
let pendingFetchFriends: Promise<void> | null = null
/** 当前正在进行的好友申请列表拉取;多端连续多条申请到达时复用同一 Promise避免雪崩重拉 */
let pendingFetchRequests: Promise<void> | null = null
/** 当前正在进行的「加载更多申请」请求 */
let pendingLoadMoreRequests: Promise<void> | null = null
/** 好友通知 payload对齐后端 BaseFriendNotification + 子类裁减后的字段) */
export interface FriendNotificationPayload {
@ -61,8 +68,10 @@ export const useFriendStore = defineStore('imFriendStore', {
friends: [] as Friend[],
// 仅 fetchFriends 成功后置位loadFriendsIDB不置位否则后台 SWR 刷新会被缓存命中跳过
loaded: false,
/** 我相关的好友申请列表(含我发起的 + 别人加我的;后端按 id 倒序,前端不再分页) */
friendRequests: [] as FriendRequest[]
/** 我相关的好友申请列表(含我发起的 + 别人加我的;后端按 id 倒序游标分页) */
friendRequests: [] as FriendRequest[],
/** 是否还有更早的申请记录可加载;返回不满 page size 即置 false */
hasMoreFriendRequests: true
}),
getters: {
@ -191,9 +200,8 @@ export const useFriendStore = defineStore('imFriendStore', {
request.handleResult = ImFriendRequestHandleResult.AGREED
request.handleTime = Date.now()
} else {
// 列表过期场景兜底重拉
// TODO @AI是不是只拉这个人避免拉所有
await this.fetchFriendRequests()
// 本地列表没这条,按 id 单查兜底
await this.loadFriendRequest(requestId)
}
},
@ -206,18 +214,21 @@ export const useFriendStore = defineStore('imFriendStore', {
request.handleContent = handleContent
request.handleTime = Date.now()
} else {
await this.fetchFriendRequests()
await this.loadFriendRequest(requestId)
}
},
/** 拉取「我相关」的好友申请列表(页面打开 / 收到 FRIEND_REQUEST_RECEIVED 时刷新pending 期间复用同一 Promise */
/** 拉取「我相关」的好友申请列表首页(页面打开 / 收到 FRIEND_REQUEST_RECEIVED 时刷新pending 期间复用同一 Promise */
async fetchFriendRequests() {
if (pendingFetchRequests) {
return pendingFetchRequests
}
pendingFetchRequests = apiGetMyFriendRequestList()
pendingFetchRequests = apiGetMyFriendRequestList(FRIEND_REQUEST_PAGE_SIZE)
.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(() => {
pendingFetchRequests = null
@ -225,11 +236,47 @@ export const useFriendStore = defineStore('imFriendStore', {
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 索引 */
findFriendRequest(requestId: number): FriendRequest | undefined {
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级联清理本地私聊会话 */
@ -364,8 +411,8 @@ export const useFriendStore = defineStore('imFriendStore', {
request.handleResult = ImFriendRequestHandleResult.AGREED
request.handleTime = Date.now()
} else {
// 本地列表可能未初始化(例如刚登录还没进 contact 页),兜底重拉
void this.fetchFriendRequests()
// 本地列表可能没这条(例如刚登录还没进 contact 页),按 id 单查兜底
void this.loadFriendRequest(payload.requestId!)
}
},
@ -377,7 +424,7 @@ export const useFriendStore = defineStore('imFriendStore', {
request.handleContent = payload.handleContent
request.handleTime = Date.now()
} else {
void this.fetchFriendRequests()
void this.loadFriendRequest(payload.requestId!)
}
},
@ -442,6 +489,7 @@ export const useFriendStore = defineStore('imFriendStore', {
this.friends = []
this.friendRequests = []
this.loaded = false
this.hasMoreFriendRequests = true
}
}
})

View File

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