import Request from 'luch-request'; import { getCache, removeCache } from '@/utils/cache'; /** * 学小乐 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 handleUnauthorized = (message = '登录已失效,请重新登录') => { if (isReloginHandling) return; isReloginHandling = true; removeCache('token'); removeCache('userInfo'); removeCache('teacherInfo'); removeCache('role'); uni.showToast({ title: message, icon: 'none', duration: 2500 }); // 避免并发 401 触发多次跳转与多次弹窗 setTimeout(() => { uni.reLaunch({ url: '/pages/login/index' }); }, 200); }; // ----------- 请求拦截 ----------- 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 = response.data as BackendResp; if (!data || typeof data !== 'object') { return response; } // 兼容不同后端返回规范:部分接口成功码为 0(并可能带 success=true) if (data.code === 200 || data.code === 0) { return data; } if (data.code === 401) { 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 status = err?.statusCode || err?.status; let msg = err?.errMsg || err?.message || '网络异常'; if (status === 401) { msg = '登录已失效,请重新登录'; handleUnauthorized(msg); } else if (status === 500) { msg = '服务异常,请稍后重试'; } if (status !== 401) { 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 };