From 9d50ef7847d0c7ddb1ef06430f543d9e4c49b15e Mon Sep 17 00:00:00 2001 From: iven Date: Sun, 17 May 2026 17:01:24 +0800 Subject: [PATCH] =?UTF-8?q?fix(mp):=20=E4=BF=AE=E5=A4=8D=E5=B9=B6=E5=8F=91?= =?UTF-8?q?=E8=AF=B7=E6=B1=82=E9=A5=A5=E9=A5=BF=E5=AF=BC=E8=87=B4=E5=BC=80?= =?UTF-8?q?=E5=8F=91=E8=80=85=E5=B7=A5=E5=85=B7=E5=8D=A1=E6=AD=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 长轮询走独立通道(requestUnlimited),不再占用 ConcurrencyLimiter 槽位 - ConcurrencyLimiter 上限 8→12,缓解 TabBar 切换请求风暴 - 新增 safeReLaunch 去重,防止并发 401 多次触发页面跳转 - maxFailures 50→10,后端不可用时快速止损而非持续 18 分钟重试 根因:咨询页长轮询每次占用槽位 25-30s,8 个槽位被占满后 所有新请求排队等待,叠加 401 场景形成死锁。 --- apps/miniprogram/src/hooks/useLongPolling.ts | 4 +-- apps/miniprogram/src/services/consultation.ts | 4 +-- .../src/services/doctor/consultation.ts | 4 +-- apps/miniprogram/src/services/request.ts | 26 +++++++++++++++---- 4 files changed, 27 insertions(+), 11 deletions(-) diff --git a/apps/miniprogram/src/hooks/useLongPolling.ts b/apps/miniprogram/src/hooks/useLongPolling.ts index 435a3bb..ea998f7 100644 --- a/apps/miniprogram/src/hooks/useLongPolling.ts +++ b/apps/miniprogram/src/hooks/useLongPolling.ts @@ -10,7 +10,7 @@ interface LongPollOptions { enabled: boolean; /** 成功轮询间隔 ms,默认 3000 */ intervalMs?: number; - /** 连续失败上限,默认 50 */ + /** 连续失败上限,默认 10 */ maxFailures?: number; } @@ -27,7 +27,7 @@ export function useLongPolling({ onData, enabled, intervalMs = 3000, - maxFailures = 50, + maxFailures = 10, }: LongPollOptions) { const generation = useRef(0); const mountedRef = useRef(true); diff --git a/apps/miniprogram/src/services/consultation.ts b/apps/miniprogram/src/services/consultation.ts index 515a228..1c101ef 100644 --- a/apps/miniprogram/src/services/consultation.ts +++ b/apps/miniprogram/src/services/consultation.ts @@ -1,4 +1,4 @@ -import { api, requestWithTimeout } from './request'; +import { api, requestUnlimited } from './request'; export interface ConsultationSession { id: string; @@ -72,5 +72,5 @@ export async function pollMessages(sessionId: string, afterId?: string) { params.set('timeout', '25'); const query = params.toString(); const path = `/health/consultation-sessions/${sessionId}/messages/poll${query ? '?' + query : ''}`; - return requestWithTimeout('GET', path, undefined, 30000); + return requestUnlimited('GET', path, undefined, 30000); } diff --git a/apps/miniprogram/src/services/doctor/consultation.ts b/apps/miniprogram/src/services/doctor/consultation.ts index 6896adb..c43f250 100644 --- a/apps/miniprogram/src/services/doctor/consultation.ts +++ b/apps/miniprogram/src/services/doctor/consultation.ts @@ -1,4 +1,4 @@ -import { api, requestWithTimeout } from '../request'; +import { api, requestUnlimited } from '../request'; // ── Consultation (doctor view) ───────────────────── @@ -71,7 +71,7 @@ export async function pollMessages(sessionId: string, afterId?: string) { params.set('timeout', '25'); const query = params.toString(); const path = `/health/consultation-sessions/${sessionId}/messages/poll${query ? '?' + query : ''}`; - return requestWithTimeout('GET', path, undefined, 30000); + return requestUnlimited('GET', path, undefined, 30000); } export interface ConsultationStats { diff --git a/apps/miniprogram/src/services/request.ts b/apps/miniprogram/src/services/request.ts index b3c4b17..19c6ab0 100644 --- a/apps/miniprogram/src/services/request.ts +++ b/apps/miniprogram/src/services/request.ts @@ -56,7 +56,7 @@ class ConcurrencyLimiter { } } -const limiter = new ConcurrencyLimiter(8); +const limiter = new ConcurrencyLimiter(12); // --- Response cache + deduplication --- @@ -221,13 +221,24 @@ async function doRefresh(): Promise { return false; } +// --- reLaunch 去重 --- + +let reLaunchPromise: Promise | null = null; + +function safeReLaunch(url: string): void { + if (reLaunchPromise) return; + reLaunchPromise = Taro.reLaunch({ url }).then(() => {}, () => {}).then(() => { + setTimeout(() => { reLaunchPromise = null; }, 2000); + }); +} + // --- Core request --- -async function request(method: string, path: string, data?: unknown, timeout?: number, signal?: AbortSignal): Promise { +async function request(method: string, path: string, data?: unknown, timeout?: number, signal?: AbortSignal, bypassLimiter = false): Promise { let retryCount401 = 0; for (;;) { if (signal?.aborted) throw new Error('请求已取消'); - await limiter.acquire(); + if (!bypassLimiter) await limiter.acquire(); try { const headers = await getHeaders(); const url = `${BASE_URL}${path}`; @@ -262,7 +273,7 @@ async function request(method: string, path: string, data?: unknown, timeout? const pages = Taro.getCurrentPages(); const currentPath = pages[pages.length - 1]?.path || ''; if (!currentPath.includes('pages/login')) { - Taro.reLaunch({ url: '/pages/login/index' }); + safeReLaunch('/pages/login/index'); } } throw new Error('登录已过期'); @@ -287,7 +298,7 @@ async function request(method: string, path: string, data?: unknown, timeout? } return body.data as T; } finally { - limiter.release(); + if (!bypassLimiter) limiter.release(); } } } @@ -349,6 +360,11 @@ export async function requestWithTimeout(method: string, path: string, data?: return request(method, path, data, timeout); } +/** 绕过并发限制的请求,用于长轮询等长时间 hang 的请求 */ +export async function requestUnlimited(method: string, path: string, data?: unknown, timeout?: number): Promise { + return request(method, path, data, timeout, undefined, true); +} + /** 重置所有模块级状态,用于测试隔离 */ export function resetForTesting(): void { limiter.reset();