2026-08-05 15:01:46 +08:00
|
|
|
|
/**
|
2026-08-07 13:19:11 +08:00
|
|
|
|
* uni.request 封装 — 双 Token 无感刷新 + 并发请求队列
|
2026-08-07 11:44:20 +08:00
|
|
|
|
*
|
2026-08-13 10:34:56 +08:00
|
|
|
|
* 网络环境选择(三级优先级):
|
|
|
|
|
|
* 1. env_base_url 缓存(App 内运行时动态切换,最高优先级)
|
|
|
|
|
|
* 2. process.env.NODE_ENV 自动判断(development → 本地 IP / production → 公网域名)
|
|
|
|
|
|
* 3. 生产默认 PROD_URL
|
2026-08-05 15:01:46 +08:00
|
|
|
|
*/
|
|
|
|
|
|
|
2026-08-07 14:45:30 +08:00
|
|
|
|
const PROD_URL = "http://track_back.iris-rs.cn/api/v1";
|
2026-08-13 10:34:56 +08:00
|
|
|
|
const LOCAL_URL = "http://192.168.9.80:8011/api/v1";
|
2026-08-07 11:44:20 +08:00
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
2026-08-13 10:34:56 +08:00
|
|
|
|
// 环境 URL(自动感知:缓存配置 > NODE_ENV > 生产默认)
|
2026-08-07 11:44:20 +08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
2026-08-13 10:34:56 +08:00
|
|
|
|
export function getBaseUrl() {
|
|
|
|
|
|
// 1. 最高优先级:动态切换的缓存配置(如通过 App 内隐藏菜单切换)
|
|
|
|
|
|
const envUrl = uni.getStorageSync("env_base_url");
|
|
|
|
|
|
if (envUrl) {
|
|
|
|
|
|
return envUrl;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 自动环境判断
|
|
|
|
|
|
if (process.env.NODE_ENV === 'development') {
|
|
|
|
|
|
// 本地运行/真机调试时,自动使用本地开发机 IP
|
|
|
|
|
|
return LOCAL_URL;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 生产打包发行时,自动使用公网域名
|
|
|
|
|
|
return PROD_URL;
|
2026-08-07 11:44:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
// 双 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); },
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-08-05 15:01:46 +08:00
|
|
|
|
|
|
|
|
|
|
export default function request(options) {
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
2026-08-07 11:44:20 +08:00
|
|
|
|
const url = options.url.startsWith("http") ? options.url : getBaseUrl() + options.url;
|
|
|
|
|
|
const accessToken = getAccessToken();
|
2026-08-05 15:01:46 +08:00
|
|
|
|
|
|
|
|
|
|
uni.request({
|
|
|
|
|
|
url,
|
|
|
|
|
|
method: options.method || "GET",
|
|
|
|
|
|
data: options.data || {},
|
|
|
|
|
|
header: {
|
|
|
|
|
|
"Content-Type": "application/json",
|
2026-08-07 11:44:20 +08:00
|
|
|
|
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
|
2026-08-05 15:01:46 +08:00
|
|
|
|
...(options.header || {}),
|
|
|
|
|
|
},
|
|
|
|
|
|
timeout: 15000,
|
|
|
|
|
|
success(res) {
|
|
|
|
|
|
const code = res.statusCode;
|
2026-08-07 11:44:20 +08:00
|
|
|
|
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;
|
2026-08-05 15:01:46 +08:00
|
|
|
|
}
|
2026-08-07 11:44:20 +08:00
|
|
|
|
|
|
|
|
|
|
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);
|
2026-08-05 15:01:46 +08:00
|
|
|
|
},
|
2026-08-07 11:44:20 +08:00
|
|
|
|
fail() { uni.showToast({ title: "网络连接失败", icon: "none" }); reject(new Error("network")); },
|
2026-08-05 15:01:46 +08:00
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function get(url, params = {}) {
|
2026-08-07 11:44:20 +08:00
|
|
|
|
const query = Object.entries(params).filter(([, v]) => v != null && v !== "").map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join("&");
|
2026-08-05 15:01:46 +08:00
|
|
|
|
return request({ url: query ? `${url}?${query}` : url, method: "GET" });
|
|
|
|
|
|
}
|
2026-08-07 11:44:20 +08:00
|
|
|
|
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 }); }
|