import Taro from '@tarojs/taro'; import { secureGet, secureSet, secureRemove } from '@/utils/secure-storage'; const BASE_URL = process.env.TARO_APP_API_URL || 'http://localhost:3000/api/v1'; interface ApiResponse { success: boolean; data?: T; message?: string; } async function getHeaders(): Promise> { const headers: Record = { 'Content-Type': 'application/json' }; const token = secureGet('access_token'); if (token) headers['Authorization'] = `Bearer ${token}`; const patientId = Taro.getStorageSync('current_patient_id'); if (patientId) headers['X-Patient-Id'] = patientId; const tenantId = secureGet('tenant_id'); if (tenantId) headers['X-Tenant-Id'] = tenantId; return headers; } // --- Token refresh deduplication --- let refreshPromise: Promise | null = null; async function tryRefreshToken(): Promise { if (refreshPromise) return refreshPromise; refreshPromise = doRefresh(); refreshPromise.finally(() => { refreshPromise = null; }); return refreshPromise; } async function doRefresh(): Promise { const refreshToken = secureGet('refresh_token'); if (!refreshToken) return false; try { const res = await Taro.request({ url: `${BASE_URL}/auth/refresh`, method: 'POST', data: { refresh_token: refreshToken }, }); if (res.statusCode === 200 && res.data?.success) { secureSet('access_token', res.data.data.access_token); secureSet('refresh_token', res.data.data.refresh_token); return true; } } catch { // token 刷新失败 } secureRemove('access_token'); secureRemove('refresh_token'); secureRemove('user_data'); secureRemove('user_roles'); secureRemove('tenant_id'); secureRemove('wechat_openid'); return false; } // --- Core request --- async function request(method: string, path: string, data?: unknown): Promise { const headers = await getHeaders(); const url = `${BASE_URL}${path}`; const res = await Taro.request({ url, method: method as any, data, header: headers, timeout: 15000 }); if (res.statusCode === 401) { const refreshed = await tryRefreshToken(); if (refreshed) return request(method, path, data); 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('登录已过期'); } const body = res.data as ApiResponse; if (!body.success) throw new Error(body.message || '请求失败'); return body.data as T; } function buildQuery(params?: Record): string { if (!params) return ''; const entries = Object.entries(params).filter(([, v]) => v !== undefined && v !== ''); return entries.length > 0 ? '?' + entries .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`) .join('&') : ''; } // --- GET request cache + deduplication --- interface CacheEntry { data: unknown; expiry: number } const responseCache = new Map(); const inflightRequests = new Map>(); const DEFAULT_CACHE_TTL = 60_000; function getCacheKey(url: string): string { const patientId = Taro.getStorageSync('current_patient_id') || ''; return `${url}#${patientId}`; } export function clearRequestCache(prefix?: string): void { if (prefix) { for (const key of responseCache.keys()) { if (key.includes(prefix)) responseCache.delete(key); } } else { responseCache.clear(); } } export const api = { get: (path: string, params?: Record, cacheTtl?: number): Promise => { const url = `${path}${buildQuery(params)}`; const cacheKey = getCacheKey(url); const cached = responseCache.get(cacheKey); if (cached && Date.now() < cached.expiry) { return Promise.resolve(cached.data as T); } const inflight = inflightRequests.get(cacheKey); if (inflight) return inflight as Promise; const promise = request('GET', url).then((data) => { inflightRequests.delete(cacheKey); const ttl = cacheTtl ?? DEFAULT_CACHE_TTL; if (ttl > 0) { responseCache.set(cacheKey, { data, expiry: Date.now() + ttl }); } return data; }).catch((err) => { inflightRequests.delete(cacheKey); throw err; }); inflightRequests.set(cacheKey, promise); return promise; }, post: (path: string, data?: unknown) => request('POST', path, data), put: (path: string, data?: unknown) => request('PUT', path, data), delete: (path: string, data?: unknown) => request('DELETE', path, data), };