| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173 |
- 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<string, unknown> {
- return typeof v === "object" && v !== null && !Array.isArray(v);
- }
- /** 业务层约定:响应体含 `code` 时,仅 `200` 为成功,其余走失败(含 HTTP 200 但 code≠200) */
- function businessMessage(body: Record<string, unknown>): 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<T, B = unknown>(
- url: string,
- body?: B,
- config?: AxiosRequestConfig,
- ): Promise<T> {
- const { data } = await api.post<T>(url, body, config);
- return data;
- }
- export async function apiPut<T, B = unknown>(
- url: string,
- body?: B,
- config?: AxiosRequestConfig,
- ): Promise<T> {
- const { data } = await api.put<T>(url, body, config);
- return data;
- }
- export async function apiPatch<T, B = unknown>(
- url: string,
- body?: B,
- config?: AxiosRequestConfig,
- ): Promise<T> {
- const { data } = await api.patch<T>(url, body, config);
- return data;
- }
- export async function apiDelete<T>(
- url: string,
- config?: AxiosRequestConfig,
- ): Promise<T> {
- const { data } = await api.delete<T>(url, config);
- return data;
- }
|