From e602822dfb40e4ab5c75dafcdbd0ffdaafd26050 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Tue, 25 Aug 2026 20:13:46 +0800 Subject: [PATCH] =?UTF-8?q?fix(staffchat):=20useStaffProfile=20=E5=9C=A8?= =?UTF-8?q?=E9=80=94Promise=E5=8E=BB=E9=87=8D(=E9=98=B2=E9=87=8D=E5=A4=8D?= =?UTF-8?q?=E8=AF=B7=E6=B1=82+=E7=BC=93=E5=AD=98=E6=8A=95=E6=AF=92)+load(f?= =?UTF-8?q?orce)=E7=9C=9F=E9=87=8D=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude --- yunkes/src/composables/useStaffProfile.ts | 59 ++++++++++++++--------- 1 file changed, 36 insertions(+), 23 deletions(-) diff --git a/yunkes/src/composables/useStaffProfile.ts b/yunkes/src/composables/useStaffProfile.ts index f5893be..9b9f470 100644 --- a/yunkes/src/composables/useStaffProfile.ts +++ b/yunkes/src/composables/useStaffProfile.ts @@ -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() +/** 在途去重:同 ownerId 的并发未命中共享同一 Promise;缓存只由该共享 Promise 的 settle 单点回写 */ +const inflight = new Map>() -/** 命中缓存直接回调;否则请求一页 wechat 账号,整页回写缓存后回调。 */ -async function fetchProfile(ownerId: string, apply: (a: Account | null) => void): Promise { - 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 { + 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): { profile: Ref - load: () => void + load: (force?: boolean) => void } { const idRef = typeof ownerId === 'string' ? ref(ownerId) : ownerId const profile = ref(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 } }