/** * uni.request 封装 — 智能环境切换 + 双 Token 无感刷新 + 并发请求队列 * * 🚀 自动检测:启动时 ping 本地服务器,可达则用本地,否则走生产。 * 无需手动复制 request.local.js / request.prod.js。 */ const LOCAL_URL = "http://localhost:8011/api/v1"; const LAN_URL = "http://192.168.9.80:8011/api/v1"; const PROD_URL = "http://172.16.0.198:8011/api/v1"; // ============================================================ // 环境自动检测(仅首次启动时执行一次) // ============================================================ let BASE_URL = uni.getStorageSync("env_base_url") || ""; let envChecked = !!BASE_URL; function getBaseUrl() { if (!BASE_URL) BASE_URL = PROD_URL; // 兜底 return BASE_URL; } async function detectEnv() { if (envChecked) return; envChecked = true; // 并行 ping 两个服务器,谁先响应就用谁 const probe = (url) => new Promise((resolve) => { const start = Date.now(); uni.request({ url: url + "/auth/login", method: "POST", data: { username: "_ping_", password: "_ping_" }, timeout: 3000, complete() { resolve({ url, ms: Date.now() - start }); }, }); }); const [localResult, lanResult, prodResult] = await Promise.all([ probe(LOCAL_URL), probe(LAN_URL), probe(PROD_URL), ]); // 优先级: localhost > 局域网IP > 生产 if (localResult.ms < 3000) { BASE_URL = LOCAL_URL; } else if (lanResult.ms < 3000) { BASE_URL = LAN_URL; } else { BASE_URL = PROD_URL; } uni.setStorageSync("env_base_url", BASE_URL); console.log("[Env] 自动选择:", BASE_URL, `(localhost ${localResult.ms}ms, 局域网 ${lanResult.ms}ms, 生产 ${prodResult.ms}ms)`); } // App 启动时立即触发检测 detectEnv(); // ============================================================ // 双 Token 存储 // ============================================================ function getAccessToken() { return uni.getStorageSync("access_token") || ""; } function getRefreshToken() { return uni.getStorageSync("refresh_token") || ""; } function setTokens(accessToken, refreshToken) { uni.setStorageSync("access_token", accessToken); uni.setStorageSync("refresh_token", refreshToken); } function clearTokens() { uni.removeStorageSync("access_token"); uni.removeStorageSync("refresh_token"); uni.removeStorageSync("token"); uni.removeStorageSync("user"); } // ============================================================ // 并发请求队列 // ============================================================ let isRefreshing = false; let retryQueue = []; function processQueue(error, newToken) { retryQueue.forEach((p) => { if (newToken) p.resolve(newToken); else p.reject(error); }); retryQueue = []; } async function refreshAccessToken() { const refreshToken = getRefreshToken(); if (!refreshToken) throw new Error("无 Refresh Token"); return new Promise((resolve, reject) => { uni.request({ url: getBaseUrl() + "/auth/refresh", method: "POST", data: { refresh_token: refreshToken }, header: { "Content-Type": "application/json" }, timeout: 10000, success(res) { if (res.statusCode >= 200 && res.statusCode < 300) resolve(res.data); else reject(res); }, fail(err) { reject(err); }, }); }); } export default function request(options) { return new Promise((resolve, reject) => { const url = options.url.startsWith("http") ? options.url : getBaseUrl() + options.url; const accessToken = getAccessToken(); uni.request({ url, method: options.method || "GET", data: options.data || {}, header: { "Content-Type": "application/json", ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), ...(options.header || {}), }, timeout: 15000, success(res) { const code = res.statusCode; if (code >= 200 && code < 300) { resolve(res.data); return; } if (code === 401) { if (url.includes("/auth/refresh") || url.includes("/auth/login")) { reject(res); return; } if (isRefreshing) { retryQueue.push({ resolve: (newToken) => { options.header = options.header || {}; options.header.Authorization = `Bearer ${newToken}`; request(options).then(resolve).catch(reject); }, reject: (err) => reject(err), }); return; } isRefreshing = true; refreshAccessToken() .then((data) => { setTokens(data.access_token, getRefreshToken()); processQueue(null, data.access_token); options.header = options.header || {}; options.header.Authorization = `Bearer ${data.access_token}`; request(options).then(resolve).catch(reject); }) .catch(() => { console.error("[Request] Refresh Token 也过期,清除登录态"); processQueue(new Error("refresh_failed"), null); clearTokens(); uni.showToast({ title: "登录已过期,请重新登录", icon: "none" }); setTimeout(() => uni.reLaunch({ url: "/pages/login/login" }), 1000); reject(res); }) .finally(() => { isRefreshing = false; }); return; } if (code === 403) uni.showToast({ title: res.data?.detail || "无权操作", icon: "none", duration: 3000 }); else if (code === 400) uni.showToast({ title: res.data?.detail || "请求参数有误", icon: "none", duration: 2500 }); else if (code === 409) uni.showToast({ title: res.data?.detail || "操作冲突", icon: "none", duration: 3000 }); else { const d = res.data?.detail || ""; uni.showToast({ title: d || `请求失败 (${code})`, icon: "none", duration: 3000 }); } reject(res); }, fail() { uni.showToast({ title: "网络连接失败", icon: "none" }); reject(new Error("network")); }, }); }); } export function get(url, params = {}) { const query = Object.entries(params).filter(([, v]) => v != null && v !== "").map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join("&"); return request({ url: query ? `${url}?${query}` : url, method: "GET" }); } export function post(url, data = {}) { return request({ url, method: "POST", data }); } export function patch(url, data = {}) { return request({ url, method: "PATCH", data }); } export function put(url, data = {}) { return request({ url, method: "PUT", data }); }