import axios, { isAxiosError, type AxiosError, type AxiosRequestConfig, type AxiosResponse, } from "axios"; import { isInIframe } from "@/components/iframe-bridge"; import { withPlayerAuthHeader } from "@/lib/lottery-auth"; import { usePlayerSessionStore } from "@/stores/player-session-store"; import { withLotteryLocaleHeaders } from "@/lib/lottery-locale"; import { LotteryApiBizError, LotteryApiEnvelopeError, } from "@/types/api/errors"; import { isApiEnvelope } from "@/types/api/envelope"; import { useErrorStore } from "@/stores/error-store"; import { resolveLotteryApiV1Base } from "@/lib/lottery-api-base"; import i18n from "@/i18n"; /** * **第一层**:`baseURL` 对齐 Laravel `api/v1`;各 `api/*.ts` 只写业务 path(如 `/currencies`)。 */ export const lotteryHttp = axios.create({ baseURL: resolveLotteryApiV1Base(), timeout: 30_000, headers: { Accept: "application/json" }, }); /** 凭据类 401(非 token 失效),不应触发「会话过期」跳转 */ const PLAYER_CREDENTIAL_BIZ_CODES = new Set([8006]); function isPlayerAuthLoginRequest(config: AxiosRequestConfig | undefined): boolean { if (!config || (config.method ?? "get").toLowerCase() !== "post") { return false; } return `${config.url ?? ""}`.includes("/player/auth/login"); } /** 是否应将 401 视为已登录态失效并跳转入口(见 {@link EntryGate} `session=expired`) */ function shouldRedirectPlayerSessionExpired(error: AxiosError): boolean { if (isPlayerAuthLoginRequest(error.config)) { return false; } if (typeof window !== "undefined" && window.location.pathname === "/login") { return false; } const body = error.response?.data; if (isApiEnvelope(body) && PLAYER_CREDENTIAL_BIZ_CODES.has(body.code)) { return false; } return true; } /** 站内接口 401:清本地会话并回入口,与 {@link EntryGate} `session=expired` 衔接 */ /** 500 错误:更新全局服务器错误状态 */ lotteryHttp.interceptors.response.use( (response) => response, (error: unknown) => { if (isAxiosError(error) && typeof window !== "undefined") { const status = error.response?.status; // 401: 已登录态 token 失效 → 清会话并回入口;登录凭据错误等由页面自行提示 if (status === 401) { const redirectSessionExpired = shouldRedirectPlayerSessionExpired(error); if (redirectSessionExpired || window.location.pathname === "/login") { usePlayerSessionStore.getState().clearBearerToken(); } if (redirectSessionExpired) { const pathname = window.location.pathname; const alreadyExpired = window.location.search.includes("session=expired"); const expiredTarget = isInIframe() ? "/?session=expired" : "/login?session=expired"; const onExpiredPage = (pathname === "/" || pathname === "/login") && alreadyExpired; if (!onExpiredPage) { window.location.replace(expiredTarget); } } } // 500/502/503: 服务器错误,更新全局错误状态 if (status === 500 || status === 502 || status === 503) { const setServerError = useErrorStore.getState().setServerError; let message = i18n.t("serverError.serverMessage", { ns: "player" }); // 尝试从响应中获取更详细的错误信息 const responseData = error.response?.data; if ( typeof responseData === "object" && responseData !== null && "msg" in responseData && typeof responseData.msg === "string" ) { message = responseData.msg; } setServerError(true, message); } // 网络错误 (无响应): 检查网络状态 if (!error.response && error.message?.includes("Network Error")) { const setIsOffline = useErrorStore.getState().setIsOffline; setIsOffline(true); } } return Promise.reject(error); }, ); /** * 对 **payload**(通常是 `response.data`)校验信封并成功时返回 `data`; * `code !== 0` 抛 {@link LotteryApiBizError}。 */ export function unwrapData(payload: unknown): T { if (!isApiEnvelope(payload)) { throw new LotteryApiEnvelopeError(); } if (payload.code !== 0) { throw new LotteryApiBizError(payload.msg, payload.code, payload.data); } return payload.data as T; } /** 对已拿到的 `AxiosResponse` 解一层 `unwrapData(response.data)` */ export function unwrapResponse(res: AxiosResponse): T { return unwrapData(res.data); } /** * **第二层**:补齐玩家鉴权与语言头,用 `lotteryHttp` 发请求,再 `unwrapResponse`。 * 是否提示用户由各页面/特性自己 `catch` 决定。 */ export async function request(config: AxiosRequestConfig): Promise { const merged = withPlayerAuthHeader(withLotteryLocaleHeaders(config)); try { const res = await lotteryHttp.request(merged); return unwrapResponse(res); } catch (err: unknown) { if (isAxiosError(err) && err.response?.data !== undefined) { const body = err.response.data; if (isApiEnvelope(body) && body.code !== 0) { throw new LotteryApiBizError(body.msg, body.code, body.data); } } throw err; } } /** 第二层常用动词(内部都是 `request`) */ export const lotteryRequest = { request, get: (url: string, config?: Omit) => request({ ...config, url, method: "GET" }), delete: (url: string, config?: Omit) => request({ ...config, url, method: "DELETE" }), post: ( url: string, data?: unknown, config?: Omit, ) => request({ ...config, url, method: "POST", data }), put: ( url: string, data?: unknown, config?: Omit, ) => request({ ...config, url, method: "PUT", data }), patch: ( url: string, data?: unknown, config?: Omit, ) => request({ ...config, url, method: "PATCH", data }), };