request.ts 3.11 KB
// REQ-USR-004: 统一 Axios 实例 + Result 拆包 + 错误拦截(docs/04 § 2.3 / § 2.4)
import axios, {
  AxiosError,
  type AxiosInstance,
  type InternalAxiosRequestConfig,
} from 'axios';

/** token 持久化键名(D6,跨 task 引用此常量,不写字面量) */
export const TOKEN_STORAGE_KEY = 'xly_erp_token';

/** 网络 / 超时 / 5xx 无可用业务码时的统一标识 */
export const NETWORK_ERROR_CODE = -1;

/** HTTP 未授权状态码(BR10 / D11,被动登录失效统一登出) */
export const HTTP_UNAUTHORIZED = 401;

/**
 * 401 统一登出回调单例(D11)。拦截器内无法使用 React hooks(useNavigate/message),
 * 故由外壳挂载时通过 registerUnauthorizedHandler 注册回调,拦截器捕获 HTTP 401 时调用之。
 */
let onUnauthorized: (() => void) | null = null;

/** 注册(或传 null 清除)被动 401 统一登出回调 */
export function registerUnauthorizedHandler(fn: (() => void) | null): void {
  onUnauthorized = fn;
}

/** 后端统一响应体 Result<T>(docs/04 § 1.4) */
export interface Result<T = unknown> {
  code: number;
  message: string;
  data: T;
}

/** 业务 / 网络错误,携带后端业务码供页面分流文案 */
export class ApiError extends Error {
  code: number;
  constructor(code: number, message: string) {
    super(message);
    this.name = 'ApiError';
    this.code = code;
  }
}

const request: AxiosInstance = axios.create({
  baseURL: '/api',
  timeout: 15000,
  headers: { 'Content-Type': 'application/json' },
});

// 请求拦截器:已登录态注入 Authorization;登录 / 版本端点无 token 时自然跳过
request.interceptors.request.use((config: InternalAxiosRequestConfig) => {
  const token = localStorage.getItem(TOKEN_STORAGE_KEY);
  if (token) {
    config.headers.set('Authorization', `Bearer ${token}`);
  }
  return config;
});

// 响应拦截器:拆 Result。code=0 取 data;非 0 抛 ApiError;无响应(网络/超时/5xx)兜底
request.interceptors.response.use(
  (response) => {
    const body = response.data as Result;
    if (body && typeof body === 'object' && 'code' in body) {
      if (body.code === 0) {
        return body.data;
      }
      return Promise.reject(new ApiError(body.code, body.message || '请求失败'));
    }
    // 非 Result 结构(理论上不应出现),原样返回 data
    return response.data;
  },
  (error: AxiosError) => {
    // 已是 ApiError 直接透传(极少数情况)
    if (error instanceof ApiError) {
      return Promise.reject(error);
    }
    // 被动 401:触发统一登出回调(若已注册),再走原 ApiError 映射(BR10 / D11)
    if (error.response?.status === HTTP_UNAUTHORIZED && onUnauthorized) {
      onUnauthorized();
    }
    const body = error.response?.data as Result | undefined;
    if (body && typeof body === 'object' && 'code' in body) {
      return Promise.reject(new ApiError(body.code, body.message || '请求失败'));
    }
    // 无响应:网络异常 / 超时 / 5xx 无 Result 体
    return Promise.reject(new ApiError(NETWORK_ERROR_CODE, '网络异常,请稍后重试'));
  },
);

export default request;