feat: 移动端双Token队列拦截器 + 通知列表 + 登录页 + TabBar红点 + v1.0.1
This commit is contained in:
@ -1,14 +1,122 @@
|
||||
/**
|
||||
* uni.request 封装 — 统一的 HTTP 客户端
|
||||
* 自动携带 Token、401 跳转登录
|
||||
* uni.request 封装 — 智能环境切换 + 双 Token 无感刷新 + 并发请求队列
|
||||
*
|
||||
* 🚀 自动检测:启动时 ping 本地服务器,可达则用本地,否则走生产。
|
||||
* 无需手动复制 request.local.js / request.prod.js。
|
||||
*/
|
||||
|
||||
const BASE_URL = "http://192.168.9.80:8011/api/v1";
|
||||
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 : BASE_URL + options.url;
|
||||
const token = uni.getStorageSync("token") || "";
|
||||
const url = options.url.startsWith("http") ? options.url : getBaseUrl() + options.url;
|
||||
const accessToken = getAccessToken();
|
||||
|
||||
uni.request({
|
||||
url,
|
||||
@ -16,59 +124,65 @@ export default function request(options) {
|
||||
data: options.data || {},
|
||||
header: {
|
||||
"Content-Type": "application/json",
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
|
||||
...(options.header || {}),
|
||||
},
|
||||
timeout: 15000,
|
||||
success(res) {
|
||||
const code = res.statusCode;
|
||||
if (code >= 200 && code < 300) {
|
||||
resolve(res.data);
|
||||
} else if (code === 401) {
|
||||
uni.removeStorageSync("token");
|
||||
uni.removeStorageSync("user");
|
||||
uni.showToast({ title: "登录已过期,请重新登录", icon: "none" });
|
||||
setTimeout(() => uni.reLaunch({ url: "/pages/login/login" }), 1000);
|
||||
reject(res);
|
||||
} else if (code === 403) {
|
||||
uni.showToast({ title: res.data?.detail || "无权操作", icon: "none", duration: 3000 });
|
||||
reject(res);
|
||||
} else if (code === 400) {
|
||||
uni.showToast({ title: res.data?.detail || "请求参数有误", icon: "none", duration: 2500 });
|
||||
reject(res);
|
||||
} else if (code === 409) {
|
||||
uni.showToast({ title: res.data?.detail || "操作冲突", icon: "none", duration: 3000 });
|
||||
reject(res);
|
||||
} else {
|
||||
const detail = res.data?.detail || "";
|
||||
uni.showToast({ title: detail ? `${detail}` : `请求失败 (${code})`, icon: "none", duration: 3000 });
|
||||
reject(res);
|
||||
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"));
|
||||
},
|
||||
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("&");
|
||||
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 });
|
||||
}
|
||||
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 }); }
|
||||
|
||||
Reference in New Issue
Block a user