fix(staffchat): useStaffProfile 在途Promise去重(防重复请求+缓存投毒)+load(force)真重载

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
浅念
2026-08-25 20:13:46 +08:00
parent 50635d0df8
commit e602822dfb
+36 -23
View File
@@ -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 }
}