feat: 接入玩家入口与API代理
- 新增 /api 重写代理,支持 LOTTERY_API_PROXY_TARGET 配置 - 玩家首页切换为 EntryGate,并移除 layout 对 PlayerAppShell 的包裹 - 请求层拆分语言头与玩家鉴权注入逻辑,引入 zustand 依赖 - 允许提交 .env.example 供本地配置参考
This commit is contained in:
36
src/lib/lottery-auth.ts
Normal file
36
src/lib/lottery-auth.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { AxiosHeaders, type AxiosRequestConfig } from "axios";
|
||||
|
||||
/** `Bearer ` 后的原始串:`dev:1`、JWT 等 */
|
||||
let playerBearerPayload: string | null = null;
|
||||
|
||||
/**
|
||||
* 设置玩家鉴权 Token(仅作用于彩票 API 请求)。
|
||||
* 传入 `null` 或 trim 后空串则清除。
|
||||
*/
|
||||
export function setPlayerBearerToken(token: string | null): void {
|
||||
if (token === null || token.trim() === "") {
|
||||
playerBearerPayload = null;
|
||||
return;
|
||||
}
|
||||
const t = token.trim();
|
||||
playerBearerPayload = t.startsWith("Bearer ") ? t.slice(7).trim() : t;
|
||||
}
|
||||
|
||||
export function getPlayerBearerTokenPayload(): string | null {
|
||||
return playerBearerPayload;
|
||||
}
|
||||
|
||||
export function withPlayerAuthHeader(
|
||||
config: AxiosRequestConfig,
|
||||
): AxiosRequestConfig {
|
||||
if (!playerBearerPayload) {
|
||||
return config;
|
||||
}
|
||||
const merged: AxiosRequestConfig = { ...config };
|
||||
const headers = AxiosHeaders.concat(
|
||||
merged.headers as Parameters<typeof AxiosHeaders.concat>[0],
|
||||
);
|
||||
headers.set("Authorization", `Bearer ${playerBearerPayload}`);
|
||||
merged.headers = headers;
|
||||
return merged;
|
||||
}
|
||||
@@ -1,52 +1,17 @@
|
||||
import axios, {
|
||||
AxiosHeaders,
|
||||
isAxiosError,
|
||||
type AxiosRequestConfig,
|
||||
type AxiosResponse,
|
||||
} from "axios";
|
||||
|
||||
import { withPlayerAuthHeader } from "@/lib/lottery-auth";
|
||||
import { withLotteryLocaleHeaders } from "@/lib/lottery-locale";
|
||||
import {
|
||||
LotteryApiBizError,
|
||||
LotteryApiEnvelopeError,
|
||||
} from "@/types/api/errors";
|
||||
import { isApiEnvelope } from "@/types/api/envelope";
|
||||
|
||||
export function setLotteryRequestLocale(locale: string | null): void {
|
||||
if (locale === null) {
|
||||
overrideLocale = null;
|
||||
return;
|
||||
}
|
||||
const p = locale.trim().toLowerCase().split("-")[0] ?? "";
|
||||
overrideLocale =
|
||||
p === "zh" || p === "en" || p === "ne" ? (p as "zh" | "en" | "ne") : null;
|
||||
}
|
||||
|
||||
let overrideLocale: "zh" | "en" | "ne" | null = null;
|
||||
|
||||
function requestLocale(): "zh" | "en" | "ne" {
|
||||
if (overrideLocale) {
|
||||
return overrideLocale;
|
||||
}
|
||||
if (typeof document !== "undefined") {
|
||||
const tag = document.documentElement.lang.trim().toLowerCase();
|
||||
const primary = tag.split("-")[0] ?? tag;
|
||||
if (primary === "zh" || primary === "en" || primary === "ne") {
|
||||
return primary;
|
||||
}
|
||||
}
|
||||
return "en";
|
||||
}
|
||||
|
||||
function acceptLanguage(loc: ReturnType<typeof requestLocale>): string {
|
||||
if (loc === "zh") {
|
||||
return "zh-CN,zh;q=0.9,en;q=0.8";
|
||||
}
|
||||
if (loc === "ne") {
|
||||
return "ne,ne-NP;q=0.9,en;q=0.8";
|
||||
}
|
||||
return "en-US,en;q=0.9";
|
||||
}
|
||||
|
||||
const baseURL = process.env.NEXT_PUBLIC_LOTTERY_API_BASE_URL?.trim();
|
||||
|
||||
/**
|
||||
@@ -58,21 +23,6 @@ export const lotteryHttp = axios.create({
|
||||
headers: { Accept: "application/json" },
|
||||
});
|
||||
|
||||
function mergeLocaleHeaders(
|
||||
config: AxiosRequestConfig,
|
||||
): AxiosRequestConfig {
|
||||
const loc = requestLocale();
|
||||
const merged: AxiosRequestConfig = { ...config };
|
||||
// Axios 的 RequestConfig.headers 类型比 concat 能接受的头对象更窄
|
||||
const headers = AxiosHeaders.concat(
|
||||
merged.headers as Parameters<typeof AxiosHeaders.concat>[0],
|
||||
);
|
||||
headers.set("X-Locale", loc);
|
||||
headers.set("Accept-Language", acceptLanguage(loc));
|
||||
merged.headers = headers;
|
||||
return merged;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对 **payload**(通常是 `response.data`)校验信封并成功时返回 `data`;
|
||||
* `code !== 0` 抛 {@link LotteryApiBizError}。
|
||||
@@ -93,11 +43,11 @@ export function unwrapResponse<T>(res: AxiosResponse<unknown>): T {
|
||||
}
|
||||
|
||||
/**
|
||||
* **第二层**:自动带语言头,用 `lotteryHttp` 发请求,再 `unwrapResponse`。
|
||||
* **第二层**:补齐玩家鉴权与语言头,用 `lotteryHttp` 发请求,再 `unwrapResponse`。
|
||||
* 是否提示用户由各页面/特性自己 `catch` 决定。
|
||||
*/
|
||||
export async function request<T>(config: AxiosRequestConfig): Promise<T> {
|
||||
const merged = mergeLocaleHeaders(config);
|
||||
const merged = withPlayerAuthHeader(withLotteryLocaleHeaders(config));
|
||||
try {
|
||||
const res = await lotteryHttp.request<unknown>(merged);
|
||||
return unwrapResponse<T>(res);
|
||||
@@ -139,4 +89,4 @@ export const lotteryRequest = {
|
||||
data?: unknown,
|
||||
config?: Omit<AxiosRequestConfig, "url" | "method" | "data">,
|
||||
) => request<T>({ ...config, url, method: "PATCH", data }),
|
||||
};
|
||||
};
|
||||
57
src/lib/lottery-locale.ts
Normal file
57
src/lib/lottery-locale.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { AxiosHeaders, type AxiosRequestConfig } from "axios";
|
||||
|
||||
type LotteryLocale = "zh" | "en" | "ne";
|
||||
|
||||
let overrideLocale: LotteryLocale | null = null;
|
||||
|
||||
export function setLotteryRequestLocale(locale: string | null): void {
|
||||
if (locale === null) {
|
||||
overrideLocale = null;
|
||||
return;
|
||||
}
|
||||
const p = locale.trim().toLowerCase().split("-")[0] ?? "";
|
||||
overrideLocale = isLotteryLocale(p) ? p : null;
|
||||
}
|
||||
|
||||
function isLotteryLocale(value: string): value is LotteryLocale {
|
||||
return value === "zh" || value === "en" || value === "ne";
|
||||
}
|
||||
|
||||
function requestLocale(): LotteryLocale {
|
||||
if (overrideLocale) {
|
||||
return overrideLocale;
|
||||
}
|
||||
if (typeof document !== "undefined") {
|
||||
const tag = document.documentElement.lang.trim().toLowerCase();
|
||||
const primary = tag.split("-")[0] ?? tag;
|
||||
if (isLotteryLocale(primary)) {
|
||||
return primary;
|
||||
}
|
||||
}
|
||||
return "en";
|
||||
}
|
||||
|
||||
function acceptLanguage(loc: LotteryLocale): string {
|
||||
if (loc === "zh") {
|
||||
return "zh-CN,zh;q=0.9,en;q=0.8";
|
||||
}
|
||||
if (loc === "ne") {
|
||||
return "ne,ne-NP;q=0.9,en;q=0.8";
|
||||
}
|
||||
return "en-US,en;q=0.9";
|
||||
}
|
||||
|
||||
export function withLotteryLocaleHeaders(
|
||||
config: AxiosRequestConfig,
|
||||
): AxiosRequestConfig {
|
||||
const loc = requestLocale();
|
||||
const merged: AxiosRequestConfig = { ...config };
|
||||
// Axios 的 RequestConfig.headers 类型比 concat 能接受的头对象更窄
|
||||
const headers = AxiosHeaders.concat(
|
||||
merged.headers as Parameters<typeof AxiosHeaders.concat>[0],
|
||||
);
|
||||
headers.set("X-Locale", loc);
|
||||
headers.set("Accept-Language", acceptLanguage(loc));
|
||||
merged.headers = headers;
|
||||
return merged;
|
||||
}
|
||||
26
src/lib/player-session.ts
Normal file
26
src/lib/player-session.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
const STORAGE_KEY = "lottery.player.bearer";
|
||||
|
||||
/** sessionStorage:标签页关闭即清空,符合 H5 嵌入主站场景 */
|
||||
export function persistPlayerBearerToken(raw: string): void {
|
||||
try {
|
||||
sessionStorage.setItem(STORAGE_KEY, raw.trim());
|
||||
} catch {
|
||||
/* ignore quota / private mode */
|
||||
}
|
||||
}
|
||||
|
||||
export function readPersistedPlayerBearerToken(): string | null {
|
||||
try {
|
||||
return sessionStorage.getItem(STORAGE_KEY);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function clearPersistedPlayerBearerToken(): void {
|
||||
try {
|
||||
sessionStorage.removeItem(STORAGE_KEY);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user