fix(mp): 患者端卡死深度审查修复 — CRITICAL 回归 + 并发保护 + 页栈溢出防护
CRITICAL: - 咨询详情页 loadData 引用已删除的 pollingRef → 移除残余引用 HIGH: - 401 重试递归改循环结构,避免并发限制器双 slot 占用 - 医生端 4 个列表页添加 loadingRef 防重入(consultation/alerts/dialysis/prescription) - 新增 safeNavigateTo 页栈溢出保护(栈≥9 自动 redirectTo) 前期修复一并提交: - 全局并发限制 MAX_CONCURRENT=8 - doRefresh 失败时完整清理 Storage + 重置缓存状态 - 401 跳转登录页修正 - 长轮询 generation counter 模式 - 首页/健康页 loadingRef + refreshToday 去重
This commit is contained in:
@@ -103,65 +103,102 @@ async function doRefresh(): Promise<boolean> {
|
||||
} catch {
|
||||
// token 刷新失败
|
||||
}
|
||||
isLoggingOut = true;
|
||||
secureRemove('access_token');
|
||||
secureRemove('refresh_token');
|
||||
secureRemove('user_data');
|
||||
secureRemove('user_roles');
|
||||
secureRemove('tenant_id');
|
||||
secureRemove('wechat_openid');
|
||||
Taro.removeStorageSync('current_patient');
|
||||
Taro.removeStorageSync('current_patient_id');
|
||||
clearRequestCache();
|
||||
cachedPatientId = '';
|
||||
headersCacheTs = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- Core request ---
|
||||
async function request<T>(method: string, path: string, data?: unknown, timeout?: number, _retryCount401 = 0): Promise<T> {
|
||||
const headers = await getHeaders();
|
||||
const url = `${BASE_URL}${path}`;
|
||||
let res: Taro.request.SuccessCallbackResult;
|
||||
try {
|
||||
res = await Taro.request({ url, method: method as any, data, header: headers, timeout: timeout || 15000 });
|
||||
} catch (err: any) {
|
||||
const msg = err?.errMsg || '';
|
||||
if (msg.includes('timeout')) {
|
||||
Taro.showToast({ title: '网络超时,请重试', icon: 'none' });
|
||||
throw new Error('网络超时');
|
||||
}
|
||||
Taro.showToast({ title: '网络异常,请检查连接', icon: 'none' });
|
||||
throw new Error('网络异常');
|
||||
}
|
||||
// 微信小程序并发请求限制为 10 个,超出会排队阻塞
|
||||
const MAX_CONCURRENT = 8;
|
||||
let activeRequests = 0;
|
||||
const pendingQueue: Array<() => void> = [];
|
||||
|
||||
if (res.statusCode === 401) {
|
||||
if (isLoggingOut || _retryCount401 >= MAX_401_RETRY) {
|
||||
throw new Error('登录已过期');
|
||||
}
|
||||
const hasToken = !!safeGet('access_token');
|
||||
if (hasToken) {
|
||||
const refreshed = await tryRefreshToken();
|
||||
if (refreshed) return request<T>(method, path, data, timeout, _retryCount401 + 1);
|
||||
const pages = Taro.getCurrentPages();
|
||||
const currentPath = pages[pages.length - 1]?.path || '';
|
||||
if (!currentPath.includes('pages/login')) {
|
||||
Taro.reLaunch({ url: '/pages/index/index' });
|
||||
function acquireSlot(): Promise<void> {
|
||||
if (activeRequests < MAX_CONCURRENT) {
|
||||
activeRequests++;
|
||||
return Promise.resolve();
|
||||
}
|
||||
return new Promise<void>((resolve) => { pendingQueue.push(resolve); });
|
||||
}
|
||||
|
||||
function releaseSlot(): void {
|
||||
activeRequests--;
|
||||
const next = pendingQueue.shift();
|
||||
if (next) { activeRequests++; next(); }
|
||||
}
|
||||
|
||||
async function request<T>(method: string, path: string, data?: unknown, timeout?: number): Promise<T> {
|
||||
let retryCount401 = 0;
|
||||
for (;;) {
|
||||
await acquireSlot();
|
||||
try {
|
||||
const headers = await getHeaders();
|
||||
const url = `${BASE_URL}${path}`;
|
||||
let res: Taro.request.SuccessCallbackResult;
|
||||
try {
|
||||
res = await Taro.request({ url, method: method as any, data, header: headers, timeout: timeout || 15000 });
|
||||
} catch (err: any) {
|
||||
const msg = err?.errMsg || '';
|
||||
if (msg.includes('timeout')) {
|
||||
Taro.showToast({ title: '网络超时,请重试', icon: 'none' });
|
||||
throw new Error('网络超时');
|
||||
}
|
||||
Taro.showToast({ title: '网络异常,请检查连接', icon: 'none' });
|
||||
throw new Error('网络异常');
|
||||
}
|
||||
|
||||
if (res.statusCode === 401) {
|
||||
if (isLoggingOut || retryCount401 >= MAX_401_RETRY) {
|
||||
throw new Error('登录已过期');
|
||||
}
|
||||
const hasToken = !!safeGet('access_token');
|
||||
if (hasToken) {
|
||||
const refreshed = await tryRefreshToken();
|
||||
if (refreshed) {
|
||||
isLoggingOut = false;
|
||||
retryCount401++;
|
||||
continue;
|
||||
}
|
||||
const pages = Taro.getCurrentPages();
|
||||
const currentPath = pages[pages.length - 1]?.path || '';
|
||||
if (!currentPath.includes('pages/login')) {
|
||||
Taro.reLaunch({ url: '/pages/login/index' });
|
||||
}
|
||||
}
|
||||
throw new Error('登录已过期');
|
||||
}
|
||||
|
||||
if (res.statusCode === 403) {
|
||||
Taro.showToast({ title: '权限不足', icon: 'none' });
|
||||
throw new Error('权限不足');
|
||||
}
|
||||
|
||||
if (res.statusCode >= 500) {
|
||||
Taro.showToast({ title: '服务器繁忙,请稍后重试', icon: 'none' });
|
||||
throw new Error('服务器错误');
|
||||
}
|
||||
|
||||
const body = res.data as ApiResponse<T>;
|
||||
if (!body.success) {
|
||||
const userMsg = body.error_code ? (ERROR_CODE_MAP[body.error_code] || '操作失败,请稍后重试') : '操作失败,请稍后重试';
|
||||
throw new Error(userMsg);
|
||||
}
|
||||
return body.data as T;
|
||||
} finally {
|
||||
releaseSlot();
|
||||
}
|
||||
throw new Error('登录已过期');
|
||||
}
|
||||
|
||||
if (res.statusCode === 403) {
|
||||
Taro.showToast({ title: '权限不足', icon: 'none' });
|
||||
throw new Error('权限不足');
|
||||
}
|
||||
|
||||
if (res.statusCode >= 500) {
|
||||
Taro.showToast({ title: '服务器繁忙,请稍后重试', icon: 'none' });
|
||||
throw new Error('服务器错误');
|
||||
}
|
||||
|
||||
const body = res.data as ApiResponse<T>;
|
||||
if (!body.success) {
|
||||
const userMsg = body.error_code ? (ERROR_CODE_MAP[body.error_code] || '操作失败,请稍后重试') : '操作失败,请稍后重试';
|
||||
throw new Error(userMsg);
|
||||
}
|
||||
return body.data as T;
|
||||
}
|
||||
|
||||
function buildQuery(params?: Record<string, string | number | undefined>): string {
|
||||
|
||||
Reference in New Issue
Block a user