import type { ApiErrorResponse, ApiResponse, AuthPayload, AdminProfile, Dashboard, Lead, SiteConfig, WanfaCategory, ConciergeAdvisor } from "@/types"; const API_BASE = import.meta.env.VITE_API_BASE_URL ?? ""; let accessToken: string | null = null; let refreshPromise: Promise | null = null; export class ApiRequestError extends Error { constructor( message: string, readonly statusCode: number, readonly errorCode?: string, readonly details?: unknown, ) { super(message); this.name = "ApiRequestError"; } } export class AuthExpiredError extends ApiRequestError { constructor(message = "登录已失效,请重新登录") { super(message, 401, "AUTH_EXPIRED"); this.name = "AuthExpiredError"; } } export function setAccessToken(token: string | null) { accessToken = token; } export function getAccessToken() { return accessToken; } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null; } async function parseJson(response: Response): Promise { return response.json().catch(() => null); } function unwrap(payload: unknown, statusCode: number): T { if (!isRecord(payload) || typeof payload.code !== "number" || payload.code !== statusCode || typeof payload.msg !== "string" || !Object.prototype.hasOwnProperty.call(payload, "data")) { throw new ApiRequestError("接口响应格式不正确", statusCode, "API_PROTOCOL_ERROR"); } if (statusCode < 200 || statusCode >= 300) { if (payload.data !== null) throw new ApiRequestError("接口响应格式不正确", statusCode, "API_PROTOCOL_ERROR"); const error = payload as unknown as ApiErrorResponse; throw new ApiRequestError(error.msg, statusCode, error.errorCode, error.details); } if (payload.msg !== "success") throw new ApiRequestError("接口响应格式不正确", statusCode, "API_PROTOCOL_ERROR"); return payload.data as T; } async function rawRequest(path: string, options: RequestInit = {}): Promise { const headers = new Headers(options.headers); if (!(options.body instanceof FormData)) headers.set("Content-Type", "application/json"); if (accessToken) headers.set("Authorization", `Bearer ${accessToken}`); return fetch(`${API_BASE}${path}`, { ...options, headers, credentials: "include" }); } export async function refreshAccessToken(): Promise { const response = await rawRequest("/api/admin/auth/refresh", { method: "POST" }); const payload = unwrap(await parseJson(response), response.status); setAccessToken(payload.accessToken || payload.token); return payload; } async function refreshOnce() { if (!refreshPromise) { refreshPromise = refreshAccessToken().finally(() => { refreshPromise = null; }); } return refreshPromise; } export async function request(path: string, options: RequestInit = {}, retry = true): Promise { const response = await rawRequest(path, options); if (response.status === 401 && retry && !path.includes("/auth/login") && !path.includes("/auth/refresh") && !path.includes("/auth/logout")) { try { await refreshOnce(); return request(path, options, false); } catch { setAccessToken(null); throw new AuthExpiredError(); } } return unwrap(await parseJson(response), response.status); } export const login = (email: string, password: string) => request("/api/admin/auth/login", { method: "POST", body: JSON.stringify({ email, password }) }, false); export const getProfile = () => request("/api/admin/system/profile"); export const logout = () => request<{ ok: boolean }>("/api/admin/auth/logout", { method: "POST" }, false); export const getDashboard = () => request("/api/admin/dashboard"); export const getSiteConfig = () => request("/api/admin/site-config"); export const getWanfa = async () => (await request<{ categories: WanfaCategory[] }>("/api/admin/wanfa/categories")).categories; export const getConcierge = async () => (await request<{ advisors: ConciergeAdvisor[] }>("/api/admin/concierge/advisors")).advisors; export const getLeads = () => request<{ items: Lead[] }>("/api/admin/leads?take=50"); export const updateLeadStatus = (leadId: string, status: string) => request(`/api/admin/leads/${leadId}/status`, { method: "PATCH", body: JSON.stringify({ status }), }); export const getSystemResource = (resource: string) => request<{ items: unknown[] }>(`/api/admin/system/${resource}`);