fix(staffchat): useStaffProfile 在途Promise去重(防重复请求+缓存投毒)+load(force)真重载
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -9,50 +9,63 @@ import type { Account } from '@/api/types'
|
||||
* getAccounts(wechat, 100) + find 实现——提取为 composable 并加模块级缓存。
|
||||
* 一次响应的 100 条整页入缓存,同页其它员工切换 owner 直接命中不再请求;
|
||||
* 未命中/失败写 null(负缓存),避免重复打失败请求。
|
||||
* 注: 沿用原实现只拉第一页 page_size=100——员工数超过 100 时之后的查不到,
|
||||
* 属提取前就存在的历史限制,行为与提取前一致。
|
||||
*/
|
||||
const cache = new Map<string, Account | null>()
|
||||
/** 在途去重:同 ownerId 的并发未命中共享同一 Promise;缓存只由该共享 Promise 的 settle 单点回写 */
|
||||
const inflight = new Map<string, Promise<Account | null>>()
|
||||
|
||||
/** 命中缓存直接回调;否则请求一页 wechat 账号,整页回写缓存后回调。 */
|
||||
async function fetchProfile(ownerId: string, apply: (a: Account | null) => void): Promise<void> {
|
||||
if (cache.has(ownerId)) {
|
||||
apply(cache.get(ownerId) ?? null)
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await getAccounts({ channel: 'wechat', page: 1, page_size: 100 })
|
||||
for (const a of res.items) cache.set(a.account_id, a)
|
||||
// find 不到也写 null(负缓存)
|
||||
const hit = cache.get(ownerId) ?? null
|
||||
cache.set(ownerId, hit)
|
||||
apply(hit)
|
||||
} catch {
|
||||
cache.set(ownerId, null)
|
||||
apply(null)
|
||||
}
|
||||
/**
|
||||
* 命中缓存直接返回;否则发起(或共享在途的)一页 wechat 账号请求,整页回写缓存后返回。
|
||||
* force=true 跳过缓存命中(含负缓存)强制刷新;在途请求本身即新鲜,仍共享不重复发。
|
||||
*/
|
||||
async function fetchProfile(ownerId: string, force = false): Promise<Account | null> {
|
||||
if (!force && cache.has(ownerId)) return cache.get(ownerId) ?? null
|
||||
const pending = inflight.get(ownerId)
|
||||
if (pending) return pending
|
||||
const p = (async () => {
|
||||
try {
|
||||
const res = await getAccounts({ channel: 'wechat', page: 1, page_size: 100 })
|
||||
for (const a of res.items) cache.set(a.account_id, a)
|
||||
// find 不到也写 null(负缓存)
|
||||
const hit = cache.get(ownerId) ?? null
|
||||
cache.set(ownerId, hit)
|
||||
return hit
|
||||
} catch {
|
||||
cache.set(ownerId, null)
|
||||
return null
|
||||
} finally {
|
||||
inflight.delete(ownerId)
|
||||
}
|
||||
})()
|
||||
inflight.set(ownerId, p)
|
||||
return p
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ownerId 员工 account_id(wxid);响应式传入时自动随变更重载
|
||||
* @returns profile 资料卡(null=未命中/失败,调用方自行兜底显示 ownerId);
|
||||
* load 手动重载入口(watch immediate 已覆盖常规场景,一般无需调用)
|
||||
* load(force=false) 手动重载入口:默认读缓存(命中即时返回,watch immediate
|
||||
* 已覆盖常规场景);传 true 强制重新请求,可穿透负缓存恢复(如后端瞬时故障后刷新)
|
||||
*/
|
||||
export function useStaffProfile(ownerId: string | Ref<string>): {
|
||||
profile: Ref<Account | null>
|
||||
load: () => void
|
||||
load: (force?: boolean) => void
|
||||
} {
|
||||
const idRef = typeof ownerId === 'string' ? ref(ownerId) : ownerId
|
||||
const profile = ref<Account | null>(cache.get(idRef.value) ?? null)
|
||||
// token 守卫: ownerId 快速切换时,旧 in-flight 响应不回写新 profile
|
||||
let token = 0
|
||||
|
||||
function load(): void {
|
||||
function load(force = false): void {
|
||||
const my = ++token
|
||||
const id = idRef.value
|
||||
void fetchProfile(id, (a) => {
|
||||
void fetchProfile(idRef.value, force).then((a) => {
|
||||
if (my === token) profile.value = a
|
||||
})
|
||||
}
|
||||
|
||||
watch(idRef, load, { immediate: true })
|
||||
// 包一层箭头:watch 回调首参是新 ownerId,直传 load 会被误当成 force 参数
|
||||
watch(idRef, () => load(), { immediate: true })
|
||||
return { profile, load }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user