// http.js import axios from "axios"; import { v4 as uuidv4 } from "uuid"; // ============ 配置 ============ const BASE_URL = import.meta.env.VITE_BASE_URL === "/" ? "/" : import.meta.env.VITE_BASE_URL.startsWith('localhost:') ? `http://${import.meta.env.VITE_BASE_URL}` : `https://${import.meta.env.VITE_BASE_URL}`; const DB_CONFIG = { name: "TokenDB", version: 2, store: "tokens", key: "userToken", } as const; const STORAGE_KEY = "token"; // ============ IndexedDB 操作 ============ class TokenDB { private static async open(): Promise { return new Promise((resolve, reject) => { const request = indexedDB.open(DB_CONFIG.name, DB_CONFIG.version); request.onerror = () => reject(request.error); request.onsuccess = () => resolve(request.result); request.onupgradeneeded = (event) => { const db = (event.target as IDBOpenDBRequest).result; if (db.objectStoreNames.contains(DB_CONFIG.store)) { db.deleteObjectStore(DB_CONFIG.store); } db.createObjectStore(DB_CONFIG.store, { keyPath: "key" }); }; }); } static async get(): Promise { try { const db = await this.open(); return new Promise((resolve) => { const tx = db.transaction(DB_CONFIG.store, "readonly"); const request = tx.objectStore(DB_CONFIG.store).get(DB_CONFIG.key); request.onsuccess = () => resolve(request.result?.value || null); request.onerror = () => resolve(null); tx.oncomplete = () => db.close(); tx.onabort = () => db.close(); }); } catch { return null; } } static async set(token: string): Promise { try { const db = await this.open(); return new Promise((resolve) => { const tx = db.transaction(DB_CONFIG.store, "readwrite"); tx.objectStore(DB_CONFIG.store).put({ key: DB_CONFIG.key, value: token }); tx.oncomplete = () => { db.close(); resolve(); }; tx.onerror = () => { db.close(); resolve(); }; }); } catch { // 静默失败,有其他存储兜底 } } } // ============ Token 管理器 ============ class TokenManager { private static cache: string | null = null; private static pending: Promise | null = null; // UUID v4 格式校验,防止脏数据 private static isValidToken(token: string | null): token is string { return !!token && /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(token); } // 安全地操作 Storage private static safeGet(storage: Storage): string | null { try { return storage.getItem(STORAGE_KEY); } catch { return null; } } private static safeSet(storage: Storage, token: string): void { try { storage.setItem(STORAGE_KEY, token); } catch { // 静默失败 } } // Cookie 操作(同步,iOS 上比 localStorage 更早可用) private static getFromCookie(): string | null { try { const match = document.cookie.match(new RegExp(`(?:^|; )${STORAGE_KEY}=([^;]*)`)); return match ? decodeURIComponent(match[1]) : null; } catch { return null; } } private static saveToCookie(token: string): void { try { // 有效期 400 天(Safari 上限),SameSite=Lax 兼容 WebView document.cookie = `${STORAGE_KEY}=${encodeURIComponent(token)};path=/;max-age=34560000;SameSite=Lax`; } catch { // 静默失败 } } // 同步到所有存储(后台执行,不阻塞) private static syncToAllStorages(token: string): void { this.safeSet(sessionStorage, token); this.safeSet(localStorage, token); this.saveToCookie(token); TokenDB.set(token).catch(() => { }); } // 从同步存储快速获取(cookie 优先,iOS 上最可靠的同步读取) private static getFromSyncStorage(): string | null { const token = this.getFromCookie() || this.safeGet(sessionStorage) || this.safeGet(localStorage); return this.isValidToken(token) ? token : null; } // 延迟后重试读取同步存储(iOS 冷启动时存储可能未就绪) private static waitAndRetrySync(ms: number): Promise { return new Promise(resolve => { setTimeout(() => resolve(this.getFromSyncStorage()), ms); }); } // 主入口:获取或创建 Token static async getToken(): Promise { // 1. 内存缓存(最快) if (this.cache) return this.cache; // 2. 等待进行中的创建(并发安全) if (this.pending) return this.pending; // 3. 同步存储快速路径 const syncToken = this.getFromSyncStorage(); if (syncToken) { this.cache = syncToken; this.syncToAllStorages(syncToken); return syncToken; } // 4. 异步获取或创建(带锁) this.pending = this.createToken(); return this.pending; } private static async createToken(): Promise { try { // 再次检查缓存 if (this.cache) return this.cache; // 尝试从 IndexedDB 恢复 const dbToken = await TokenDB.get(); if (dbToken && this.isValidToken(dbToken)) { this.cache = dbToken; this.syncToAllStorages(dbToken); return dbToken; } // iOS 冷启动兜底:等待一小段时间后重试同步存储 // (localStorage/cookie 数据可能存在,但初始化瞬间还未就绪) for (const delay of [50, 100, 150]) { const retryToken = await this.waitAndRetrySync(delay); if (retryToken) { this.cache = retryToken; this.syncToAllStorages(retryToken); return retryToken; } } // 所有恢复手段用尽,生成新 Token const newToken = uuidv4(); this.cache = newToken; this.syncToAllStorages(newToken); return newToken; } finally { this.pending = null; } } } // ============ Axios 实例 ============ const http = axios.create({ baseURL: BASE_URL, timeout: 15000, }); // 请求拦截器 http.interceptors.request.use( async (config) => { const token = await TokenManager.getToken(); config.headers["Token"] = token; config.headers["X-Token"] = token; config.params = { ...config.params, token }; return config; }, (error) => Promise.reject(error) ); // 响应拦截器 http.interceptors.response.use( (response) => response.data, (error) => { if (error.response) { console.error("Error:", error.response.status, error.response.data); } else { console.error("Error:", error.message); } return Promise.reject(error); } ); export default http;