import axios, { AxiosError, type AxiosInstance, type AxiosRequestConfig, } from "axios"; import { getApiBaseUrl } from "@/lib/env"; /** * 业务后端接口约定: * - 一律使用 POST(含「查询类」路径),新接口请用 {@link apiPost}。 * - 响应体若包含 `code` 字段,仅当 `code === 200` 视为成功;否则抛出 {@link ApiError},优先使用 `msg`。 * 若接入后端 JWT,可把 token 写入该 key,请求会自动带 Authorization。 */ export const API_TOKEN_STORAGE_KEY = "jchl_api_token"; export const API_ACCESS_TOKEN_HEADER = "access-token"; export const api: AxiosInstance = axios.create({ timeout: 30_000, headers: { "Content-Type": "application/json", }, }); /** * 每次请求解析 baseURL,以便 `__ORIGIN__` 与 window / 部署域名一致。 * - `baseURL: ""`:走当前站点根路径(如 `/api/...` 的 Route Handler)。 * - 调用方已传入非空 `baseURL`(如 {@link postRemittance} 的 `/api-backend-remittance`):不得覆盖,否则会误打到主后端 `/api-backend`。 */ api.interceptors.request.use((config) => { if (config.baseURL === "") { config.baseURL = typeof window !== "undefined" ? undefined : (process.env.NEXT_PUBLIC_SITE_URL?.trim().replace(/\/$/, "") || undefined); return config; } const callerBase = config.baseURL !== undefined && config.baseURL !== null && String(config.baseURL).trim() !== ""; if (callerBase) { return config; } const base = getApiBaseUrl(); config.baseURL = base || undefined; return config; }); api.interceptors.request.use((config) => { if (typeof window !== "undefined") { const token = localStorage.getItem(API_TOKEN_STORAGE_KEY); if (token) { config.headers[API_ACCESS_TOKEN_HEADER] = token; config.headers.Authorization = `Bearer ${token}`; } } return config; }); export class ApiError extends Error { constructor( message: string, public status?: number, public payload?: unknown, ) { super(message); this.name = "ApiError"; } } let authTimeoutDialogShown = false; function notifySessionTimeoutAndRedirect() { if (typeof window === "undefined") return; if (authTimeoutDialogShown) return; authTimeoutDialogShown = true; window.alert("登录超时,请重新登录"); window.location.assign("/auth/login"); } function isPlainObject(v: unknown): v is Record { return typeof v === "object" && v !== null && !Array.isArray(v); } /** 业务层约定:响应体含 `code` 时,仅 `200` 为成功,其余走失败(含 HTTP 200 但 code≠200) */ function businessMessage(body: Record): string { const msg = body.msg; if (typeof msg === "string" && msg.trim()) return msg.trim(); const message = body.message; if (typeof message === "string" && message.trim()) return message.trim(); return "请求失败"; } function isBusinessSuccessCode(code: unknown): boolean { return code === 200 || code === "200"; } api.interceptors.response.use( (res) => { const body = res.data; if (!isPlainObject(body) || !("code" in body)) return res; if (body.code === 600 || body.code === "600") { setApiAuthToken(null); notifySessionTimeoutAndRedirect(); return Promise.reject(new ApiError("登录状态失效,请重新登录", 600, body)); } if (isBusinessSuccessCode(body.code)) return res; const msg = businessMessage(body); const codeNum = typeof body.code === "number" ? body.code : Number(body.code) || res.status || 400; return Promise.reject(new ApiError(msg, codeNum, body)); }, (error: AxiosError<{ message?: string; error?: string; msg?: string }>) => { const status = error.response?.status; const body = error.response?.data; const msg = (typeof body === "object" && body !== null && "msg" in body ? String((body as { msg?: unknown }).msg) : undefined) ?? (typeof body === "object" && body !== null && "message" in body ? body.message : undefined) ?? (typeof body === "object" && body !== null && "error" in body ? String((body as { error?: unknown }).error) : undefined) ?? (error.message?.trim() || "请求失败"); return Promise.reject(new ApiError(String(msg), status, body)); }, ); export function setApiAuthToken(token: string | null) { if (typeof window === "undefined") return; if (token) localStorage.setItem(API_TOKEN_STORAGE_KEY, token); else localStorage.removeItem(API_TOKEN_STORAGE_KEY); } export async function apiPost( url: string, body?: B, config?: AxiosRequestConfig, ): Promise { const { data } = await api.post(url, body, config); return data; } export async function apiPut( url: string, body?: B, config?: AxiosRequestConfig, ): Promise { const { data } = await api.put(url, body, config); return data; } export async function apiPatch( url: string, body?: B, config?: AxiosRequestConfig, ): Promise { const { data } = await api.patch(url, body, config); return data; } export async function apiDelete( url: string, config?: AxiosRequestConfig, ): Promise { const { data } = await api.delete(url, config); return data; }