import Request from 'luch-request'; import { getCache } from '@/utils/cache'; import { clearSession } from '@/utils/session'; /** * 灵犀学 AI 通用 HTTP 客户端(基于 luch-request) * * - 自动注入 Bearer Token * - 自动注入租户头 x-tenant-id * - code !== 200 全局 toast 错误信息 * - 401 自动清缓存并跳转登录 * - 网络异常时弹窗提示 */ const HOST = (import.meta.env.VITE_BASE_URL as string) || ''; const BASE_URL = `${HOST}/api/main`; const TENANT_ID = '1966391196339204098'; let isReloginHandling = false; interface BackendResp { code: number; data: T; message?: string; msg?: string; [key: string]: any; } export class HttpError extends Error { code: number; data: any; constructor(message: string, code: number, data?: any) { super(message); this.code = code; this.data = data; } } const http = new Request({ baseURL: BASE_URL, timeout: 30000, header: { 'Content-Type': 'application/json;charset=UTF-8', 'x-tenant-id': TENANT_ID, }, }); const LOGIN_URL = '/pages/login/index'; const goLoginPage = () => { uni.reLaunch({ url: LOGIN_URL, fail: () => { uni.redirectTo({ url: LOGIN_URL }); }, complete: () => { isReloginHandling = false; }, }); }; export const handleUnauthorized = (message = '登录已失效,请重新登录') => { if (isReloginHandling) return; isReloginHandling = true; clearSession(); // 小程序中 showToast 期间 navigate 可能被拦截,先跳转再提示 goLoginPage(); setTimeout(() => { uni.showToast({ title: message, icon: 'none', duration: 2500 }); }, 100); }; const isUnauthorizedCode = (code: unknown) => Number(code) === 401; const normalizeResponseData = (raw: unknown): BackendResp | null => { if (!raw) return null; if (typeof raw === 'object') return raw as BackendResp; if (typeof raw === 'string') { try { const parsed = JSON.parse(raw); return parsed && typeof parsed === 'object' ? (parsed as BackendResp) : null; } catch { return null; } } return null; }; // ----------- 请求拦截 ----------- http.interceptors.request.use( (config) => { const token = getCache('token'); if (token && config.header) { // 默认带上 Authorization;接口可显式覆盖(传空字符串以匿名调用) if (!('Authorization' in config.header)) { (config.header as any).Authorization = `Bearer ${token}`; } } // 始终携带租户 ID if (config.header && !('x-tenant-id' in config.header)) { (config.header as any)['x-tenant-id'] = TENANT_ID; } return config; }, (err) => Promise.reject(err), ); // ----------- 响应拦截 ----------- http.interceptors.response.use( (response: any) => { const data = normalizeResponseData(response.data); if (!data) { return response; } // 兼容不同后端返回规范:部分接口成功码为 0(并可能带 success=true) if (data.code === 200 || data.code === 0) { return data; } if (isUnauthorizedCode(data.code)) { const msg401 = data.message || data.msg || '登录已失效,请重新登录'; handleUnauthorized(msg401); return Promise.reject(new HttpError(msg401, 401, data)); } // 业务错误 const msg = data.message || data.msg || '请求失败'; if (!(response.config as any)?.silent) { uni.showToast({ title: msg, icon: 'none', duration: 2500 }); } return Promise.reject(new HttpError(msg, data.code, data)); }, (err: any) => { const body = normalizeResponseData(err?.data); const status = err?.statusCode || err?.status; let msg = body?.message || body?.msg || err?.errMsg || err?.message || '网络异常'; if (isUnauthorizedCode(status) || isUnauthorizedCode(body?.code)) { msg = body?.message || body?.msg || '登录已失效,请重新登录'; handleUnauthorized(msg); return Promise.reject(new HttpError(msg, 401, body || err)); } if (status === 500) { msg = '服务异常,请稍后重试'; } uni.showToast({ title: msg, icon: 'none', duration: 2500 }); return Promise.reject(new HttpError(msg, status || -1, err)); }, ); /** * 默认导出 luch-request 实例 * 同时暴露通用调用方法,兼容老接口的 `request({ url, method, data, params, headers })` 风格 */ export interface RequestOptions { url: string; method?: 'GET' | 'POST' | 'PUT' | 'DELETE'; data?: anyObj; params?: anyObj; headers?: anyObj; silent?: boolean; } export const request = (opt: RequestOptions): Promise> => { const cfg: any = { url: opt.url, method: opt.method || 'GET', custom: { silent: opt.silent }, }; if (opt.headers) cfg.header = opt.headers; if (cfg.method === 'GET') { cfg.params = opt.params || opt.data; } else { cfg.data = opt.data; if (opt.params) cfg.params = opt.params; } if (opt.silent) cfg.silent = true; return http.request(cfg) as Promise>; }; export default request; export { http, HOST, BASE_URL, TENANT_ID };