Files
lotteryFront/src/lib/lottery-http.ts
kang 7c283627d3 docs: 新增 AI 代理学习偏好与信用盘术语规范
feat: 优化登录与入口页面加载体验

- 为登录页面添加 Suspense 边界,统一 fallback 样式
- 重构入口守卫逻辑,在布局阶段处理未登录重定向,避免闪烁
- 登录页面支持会话过期提示,并自动清理 URL 参数

feat: 信用盘与钱包盘文案差异化展示

- 底部导航「钱包」标签在信用盘模式下显示为「信用」
- 大厅余额条在信用盘模式下使用「可
2026-06-12 20:48:10 +08:00

179 lines
6.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<T>(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<T>(res: AxiosResponse<unknown>): T {
return unwrapData<T>(res.data);
}
/**
* **第二层**:补齐玩家鉴权与语言头,用 `lotteryHttp` 发请求,再 `unwrapResponse`。
* 是否提示用户由各页面/特性自己 `catch` 决定。
*/
export async function request<T>(config: AxiosRequestConfig): Promise<T> {
const merged = withPlayerAuthHeader(withLotteryLocaleHeaders(config));
try {
const res = await lotteryHttp.request<unknown>(merged);
return unwrapResponse<T>(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: <T>(url: string, config?: Omit<AxiosRequestConfig, "url" | "method">) =>
request<T>({ ...config, url, method: "GET" }),
delete: <T>(url: string, config?: Omit<AxiosRequestConfig, "url" | "method">) =>
request<T>({ ...config, url, method: "DELETE" }),
post: <T>(
url: string,
data?: unknown,
config?: Omit<AxiosRequestConfig, "url" | "method" | "data">,
) => request<T>({ ...config, url, method: "POST", data }),
put: <T>(
url: string,
data?: unknown,
config?: Omit<AxiosRequestConfig, "url" | "method" | "data">,
) => request<T>({ ...config, url, method: "PUT", data }),
patch: <T>(
url: string,
data?: unknown,
config?: Omit<AxiosRequestConfig, "url" | "method" | "data">,
) => request<T>({ ...config, url, method: "PATCH", data }),
};