feat: 更新 API 路径配置并优化环境变量管理

- 修改 .env.example,更新玩家端本地配置说明,新增直连 Laravel 和局域网 IP 访问配置项,提升开发灵活性。
- 更新 middleware.ts,使用新的 LOTTERY_API_V1_BASE 常量构建 API 请求路径,简化代码结构。
- 在 next.config.ts 中引入 parseAllowedDevOrigins 函数,动态解析允许的开发来源,增强安全性。
- 重构多个 API 模块,移除 API_V1_PREFIX,直接使用相对路径,简化 API 调用逻辑,提高可维护性。
This commit is contained in:
2026-05-29 10:28:43 +08:00
parent 1316a62ce3
commit 03faed1db6
19 changed files with 71 additions and 42 deletions

View File

@@ -1,6 +1,11 @@
# ============================================================================= # =============================================================================
# 端本地配置示例 # 玩家端本地配置示例:复制为 .env.local 后按需修改
# ============================================================================= # =============================================================================
# 三端联调速查见 lotteryadmin/.env.example本端默认端口 3800。
# -----------------------------------------------------------------------------
# Laravel APINext rewrites/api/* → ${API_BASE_URL}/api/*
# -----------------------------------------------------------------------------
# 手动切换环境:保留一个生效,另一个注释掉 # 手动切换环境:保留一个生效,另一个注释掉
# 测试 # 测试
@@ -8,6 +13,13 @@ API_BASE_URL=http://127.0.0.1:8000
# 线上 # 线上
# API_BASE_URL=https://api.your-production-domain.com # API_BASE_URL=https://api.your-production-domain.com
# 可选:直连 Laravel不经 Next 反代)
# NEXT_PUBLIC_LOTTERY_API_BASE_URL=http://127.0.0.1:8000
# NEXT_PUBLIC_LOTTERY_API_PROXY_DISABLED=true
# Next 开发:局域网 IP 访问(逗号分隔 host无协议
# ALLOWED_DEV_ORIGINS=192.168.0.101
# 可选:大厅「玩法与赔率」接口 `/api/v1/play/effective` 的 ?currency=(如 NPR不设则由后端选默认可下注币种。 # 可选:大厅「玩法与赔率」接口 `/api/v1/play/effective` 的 ?currency=(如 NPR不设则由后端选默认可下注币种。
# NEXT_PUBLIC_LOTTERY_PLAY_CURRENCY=NPR # NEXT_PUBLIC_LOTTERY_PLAY_CURRENCY=NPR
@@ -16,9 +28,10 @@ API_BASE_URL=http://127.0.0.1:8000
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Laravel ReverbWebSocket。不配则 Echo 为空,会一直显示「降级模式 / 轮询」。 # Laravel ReverbWebSocket。不配则 Echo 为空,会一直显示「降级模式 / 轮询」。
# 须与 lotterLaravel .env 的 REVERB_APP_KEY / REVERB_HOST / REVERB_PORT / REVERB_SCHEME 一致。
# Laravel 终端另开:`php artisan reverb:start` # Laravel 终端另开:`php artisan reverb:start`
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# NEXT_PUBLIC_REVERB_APP_KEY=与 lotterLaravel .env 的 REVERB_APP_KEY 一致 # NEXT_PUBLIC_REVERB_APP_KEY=
# NEXT_PUBLIC_REVERB_HOST=127.0.0.1 # NEXT_PUBLIC_REVERB_HOST=127.0.0.1
# NEXT_PUBLIC_REVERB_PORT=8080 # NEXT_PUBLIC_REVERB_PORT=8080
# NEXT_PUBLIC_REVERB_SCHEME=http # NEXT_PUBLIC_REVERB_SCHEME=http

View File

@@ -1,5 +1,6 @@
import { NextResponse, type NextRequest } from "next/server"; import { NextResponse, type NextRequest } from "next/server";
import { LOTTERY_API_V1_BASE } from "./src/api/paths";
import { generateCSP, nonCspSecurityHeaders } from "./src/lib/csp-config"; import { generateCSP, nonCspSecurityHeaders } from "./src/lib/csp-config";
type RuntimeOriginsEnvelope = { type RuntimeOriginsEnvelope = {
@@ -11,7 +12,7 @@ type RuntimeOriginsEnvelope = {
async function loadRuntimeOrigins(request: NextRequest): Promise<string[]> { async function loadRuntimeOrigins(request: NextRequest): Promise<string[]> {
try { try {
const url = new URL("/api/v1/integration/runtime-origins", request.url); const url = new URL(`${LOTTERY_API_V1_BASE}/integration/runtime-origins`, request.url);
const response = await fetch(url, { const response = await fetch(url, {
headers: { Accept: "application/json" }, headers: { Accept: "application/json" },
cache: "no-store", cache: "no-store",

View File

@@ -1,12 +1,14 @@
import type { NextConfig } from "next"; import type { NextConfig } from "next";
import { nonCspSecurityHeaders } from "./src/lib/csp-config"; import { nonCspSecurityHeaders } from "./src/lib/csp-config";
import { parseAllowedDevOrigins } from "./src/lib/next-dev-origins";
const lotteryApiProxyTarget = const lotteryApiProxyTarget =
process.env.API_BASE_URL?.trim() || "http://127.0.0.1:8000"; process.env.API_BASE_URL?.trim() || "http://127.0.0.1:8000";
const allowedDevOrigins = parseAllowedDevOrigins(process.env.ALLOWED_DEV_ORIGINS);
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
allowedDevOrigins: ["192.168.0.101"], ...(allowedDevOrigins.length > 0 ? { allowedDevOrigins } : {}),
reactCompiler: true, reactCompiler: true,
// 非 CSP 安全头CSP 由 middleware 按后台接入站点白名单动态生成。 // 非 CSP 安全头CSP 由 middleware 按后台接入站点白名单动态生成。

View File

@@ -1,8 +1,7 @@
import { API_V1_PREFIX } from "@/api/paths";
import { lotteryRequest } from "@/lib/lottery-http"; import { lotteryRequest } from "@/lib/lottery-http";
import type { PublicCurrencyListData } from "@/types/api/currency"; import type { PublicCurrencyListData } from "@/types/api/currency";
/** `GET /api/v1/currencies`(公开) */ /** `GET /api/v1/currencies`(公开) */
export function getPublicCurrencies(): Promise<PublicCurrencyListData> { export function getPublicCurrencies(): Promise<PublicCurrencyListData> {
return lotteryRequest.get<PublicCurrencyListData>(`${API_V1_PREFIX}/currencies`); return lotteryRequest.get<PublicCurrencyListData>(`/currencies`);
} }

View File

@@ -1,5 +1,4 @@
import { lotteryRequest } from "@/lib/lottery-http"; import { lotteryRequest } from "@/lib/lottery-http";
import { API_V1_PREFIX } from "@/api/paths";
import type { DrawCurrentResponse } from "@/types/api/draw-current"; import type { DrawCurrentResponse } from "@/types/api/draw-current";
import type { import type {
DrawResultDetailPayload, DrawResultDetailPayload,
@@ -16,7 +15,7 @@ export function getDrawCurrent(
params?: GetDrawCurrentParams, params?: GetDrawCurrentParams,
): Promise<DrawCurrentResponse> { ): Promise<DrawCurrentResponse> {
return lotteryRequest.get<DrawCurrentResponse>( return lotteryRequest.get<DrawCurrentResponse>(
`${API_V1_PREFIX}/draw/current`, `/draw/current`,
{ params: params?.currency ? { currency: params.currency } : undefined }, { params: params?.currency ? { currency: params.currency } : undefined },
); );
} }
@@ -35,7 +34,7 @@ export function getDrawResults(
params?: GetDrawResultsParams, params?: GetDrawResultsParams,
): Promise<DrawResultsListPayload> { ): Promise<DrawResultsListPayload> {
return lotteryRequest.get<DrawResultsListPayload>( return lotteryRequest.get<DrawResultsListPayload>(
`${API_V1_PREFIX}/draw/results`, `/draw/results`,
{ {
params: { params: {
page: params?.page, page: params?.page,
@@ -58,7 +57,7 @@ export function getDrawResultByNo(
): Promise<DrawResultDetailPayload> { ): Promise<DrawResultDetailPayload> {
const encoded = encodeURIComponent(drawNo); const encoded = encodeURIComponent(drawNo);
return lotteryRequest.get<DrawResultDetailPayload>( return lotteryRequest.get<DrawResultDetailPayload>(
`${API_V1_PREFIX}/draw/results/${encoded}`, `/draw/results/${encoded}`,
{ params: params?.currency ? { currency: params.currency } : undefined }, { params: params?.currency ? { currency: params.currency } : undefined },
); );
} }

View File

@@ -1,9 +1,8 @@
import { lotteryRequest } from "@/lib/lottery-http"; import { lotteryRequest } from "@/lib/lottery-http";
import type { HealthData } from "@/types/api/health"; import type { HealthData } from "@/types/api/health";
import { API_V1_PREFIX } from "@/api/paths";
/** `GET /api/v1/health` */ /** `GET /api/v1/health` */
export function getHealth(): Promise<HealthData> { export function getHealth(): Promise<HealthData> {
return lotteryRequest.get<HealthData>(`${API_V1_PREFIX}/health`); return lotteryRequest.get<HealthData>(`/health`);
} }

View File

@@ -1,4 +1,4 @@
export { API_V1_PREFIX } from "@/api/paths"; export { LOTTERY_API_V1_BASE } from "@/api/paths";
export { getHealth } from "@/api/health"; export { getHealth } from "@/api/health";
export { getPublicCurrencies } from "@/api/currency"; export { getPublicCurrencies } from "@/api/currency";
export { getPlayerPing, getPlayerMe } from "@/api/player"; export { getPlayerPing, getPlayerMe } from "@/api/player";

View File

@@ -1,5 +1,4 @@
import { lotteryRequest } from "@/lib/lottery-http"; import { lotteryRequest } from "@/lib/lottery-http";
import { API_V1_PREFIX } from "@/api/paths";
import type { JackpotSummaryData } from "@/types/api/jackpot"; import type { JackpotSummaryData } from "@/types/api/jackpot";
/** `GET /api/v1/jackpot/summary`(无需登录) */ /** `GET /api/v1/jackpot/summary`(无需登录) */
@@ -7,7 +6,7 @@ export function getJackpotSummary(
currencyCode = "NPR", currencyCode = "NPR",
): Promise<JackpotSummaryData> { ): Promise<JackpotSummaryData> {
return lotteryRequest.get<JackpotSummaryData>( return lotteryRequest.get<JackpotSummaryData>(
`${API_V1_PREFIX}/jackpot/summary`, `/jackpot/summary`,
{ params: { currency_code: currencyCode } }, { params: { currency_code: currencyCode } },
); );
} }

View File

@@ -1,2 +1,2 @@
/** Laravel `routes/api.php``api` 前缀 + `v1` 分组 */ /** Laravel `routes/api.php``api` 前缀 + `v1` 分组;与 {@link lotteryHttp} `baseURL` 一致。 */
export const API_V1_PREFIX = "/api/v1"; export const LOTTERY_API_V1_BASE = "/api/v1";

View File

@@ -1,5 +1,4 @@
import { lotteryRequest } from "@/lib/lottery-http"; import { lotteryRequest } from "@/lib/lottery-http";
import { API_V1_PREFIX } from "@/api/paths";
import type { PlayEffectivePayload } from "@/types/api/play-effective"; import type { PlayEffectivePayload } from "@/types/api/play-effective";
export type GetPlayEffectiveParams = { export type GetPlayEffectiveParams = {
@@ -15,7 +14,7 @@ export function getPlayEffective(
params?: GetPlayEffectiveParams, params?: GetPlayEffectiveParams,
): Promise<PlayEffectivePayload> { ): Promise<PlayEffectivePayload> {
return lotteryRequest.get<PlayEffectivePayload>( return lotteryRequest.get<PlayEffectivePayload>(
`${API_V1_PREFIX}/play/effective`, `/play/effective`,
{ {
params: params:
params?.currency !== undefined && params.currency !== "" params?.currency !== undefined && params.currency !== ""

View File

@@ -2,14 +2,13 @@ import { lotteryRequest } from "@/lib/lottery-http";
import type { PlayerMeData } from "@/types/api/player-me"; import type { PlayerMeData } from "@/types/api/player-me";
import type { ScopePingData } from "@/types/api/ping"; import type { ScopePingData } from "@/types/api/ping";
import { API_V1_PREFIX } from "@/api/paths";
/** `GET /api/v1/player/ping`(无需登录) */ /** `GET /api/v1/player/ping`(无需登录) */
export function getPlayerPing(): Promise<ScopePingData> { export function getPlayerPing(): Promise<ScopePingData> {
return lotteryRequest.get<ScopePingData>(`${API_V1_PREFIX}/player/ping`); return lotteryRequest.get<ScopePingData>(`/player/ping`);
} }
/** `GET /api/v1/player/me`(需玩家 Token */ /** `GET /api/v1/player/me`(需玩家 Token */
export function getPlayerMe(): Promise<PlayerMeData> { export function getPlayerMe(): Promise<PlayerMeData> {
return lotteryRequest.get<PlayerMeData>(`${API_V1_PREFIX}/player/me`); return lotteryRequest.get<PlayerMeData>(`/player/me`);
} }

View File

@@ -1,5 +1,4 @@
import { lotteryRequest } from "@/lib/lottery-http"; import { lotteryRequest } from "@/lib/lottery-http";
import { API_V1_PREFIX } from "@/api/paths";
export type SettingItem = { export type SettingItem = {
key: string; key: string;
@@ -13,7 +12,7 @@ export type SettingListResponse = {
}; };
export async function getPublicSettings(group: string): Promise<SettingListResponse> { export async function getPublicSettings(group: string): Promise<SettingListResponse> {
return lotteryRequest.get<SettingListResponse>(`${API_V1_PREFIX}/settings`, { return lotteryRequest.get<SettingListResponse>(`/settings`, {
params: { group }, params: { group },
}); });
} }

View File

@@ -1,5 +1,4 @@
import { lotteryRequest } from "@/lib/lottery-http"; import { lotteryRequest } from "@/lib/lottery-http";
import { API_V1_PREFIX } from "@/api/paths";
import type { import type {
TicketDrawMyMatchPayload, TicketDrawMyMatchPayload,
TicketItemDetailPayload, TicketItemDetailPayload,
@@ -21,7 +20,7 @@ export function getTicketItems(
params?: GetTicketItemsParams, params?: GetTicketItemsParams,
): Promise<TicketItemsListPayload> { ): Promise<TicketItemsListPayload> {
return lotteryRequest.get<TicketItemsListPayload>( return lotteryRequest.get<TicketItemsListPayload>(
`${API_V1_PREFIX}/ticket/items`, `/ticket/items`,
{ {
params: { params: {
page: params?.page, page: params?.page,
@@ -42,7 +41,7 @@ export function getTicketItemDetail(
): Promise<TicketItemDetailPayload> { ): Promise<TicketItemDetailPayload> {
const enc = encodeURIComponent(ticketNo); const enc = encodeURIComponent(ticketNo);
return lotteryRequest.get<TicketItemDetailPayload>( return lotteryRequest.get<TicketItemDetailPayload>(
`${API_V1_PREFIX}/ticket/items/${enc}`, `/ticket/items/${enc}`,
); );
} }
@@ -52,6 +51,6 @@ export function getTicketDrawMyMatch(
): Promise<TicketDrawMyMatchPayload> { ): Promise<TicketDrawMyMatchPayload> {
const enc = encodeURIComponent(drawNo); const enc = encodeURIComponent(drawNo);
return lotteryRequest.get<TicketDrawMyMatchPayload>( return lotteryRequest.get<TicketDrawMyMatchPayload>(
`${API_V1_PREFIX}/ticket/draws/${enc}/my-match`, `/ticket/draws/${enc}/my-match`,
); );
} }

View File

@@ -1,5 +1,4 @@
import { lotteryRequest } from "@/lib/lottery-http"; import { lotteryRequest } from "@/lib/lottery-http";
import { API_V1_PREFIX } from "@/api/paths";
import type { import type {
TicketPlaceData, TicketPlaceData,
TicketPlacePayload, TicketPlacePayload,
@@ -12,7 +11,7 @@ export function postTicketPreview(
body: TicketPreviewPayload, body: TicketPreviewPayload,
): Promise<TicketPreviewData> { ): Promise<TicketPreviewData> {
return lotteryRequest.post<TicketPreviewData>( return lotteryRequest.post<TicketPreviewData>(
`${API_V1_PREFIX}/ticket/preview`, `/ticket/preview`,
body, body,
); );
} }
@@ -20,7 +19,7 @@ export function postTicketPreview(
/** `POST /api/v1/ticket/place` — 真实下注 */ /** `POST /api/v1/ticket/place` — 真实下注 */
export function postTicketPlace(body: TicketPlacePayload): Promise<TicketPlaceData> { export function postTicketPlace(body: TicketPlacePayload): Promise<TicketPlaceData> {
return lotteryRequest.post<TicketPlaceData>( return lotteryRequest.post<TicketPlaceData>(
`${API_V1_PREFIX}/ticket/place`, `/ticket/place`,
body, body,
); );
} }

View File

@@ -1,5 +1,4 @@
import { lotteryRequest } from "@/lib/lottery-http"; import { lotteryRequest } from "@/lib/lottery-http";
import { API_V1_PREFIX } from "@/api/paths";
import type { WalletBalanceData } from "@/types/api/wallet-balance"; import type { WalletBalanceData } from "@/types/api/wallet-balance";
import type { import type {
WalletTransferBody, WalletTransferBody,
@@ -17,7 +16,7 @@ export function getWalletBalance(
params?: GetWalletBalanceParams, params?: GetWalletBalanceParams,
): Promise<WalletBalanceData> { ): Promise<WalletBalanceData> {
return lotteryRequest.get<WalletBalanceData>( return lotteryRequest.get<WalletBalanceData>(
`${API_V1_PREFIX}/wallet/balance`, `/wallet/balance`,
{ params }, { params },
); );
} }
@@ -27,7 +26,7 @@ export function getWalletLogs(
params?: GetWalletLogsParams, params?: GetWalletLogsParams,
): Promise<WalletLogsData> { ): Promise<WalletLogsData> {
return lotteryRequest.get<WalletLogsData>( return lotteryRequest.get<WalletLogsData>(
`${API_V1_PREFIX}/wallet/logs`, `/wallet/logs`,
{ params }, { params },
); );
} }
@@ -37,7 +36,7 @@ export function postWalletTransferIn(
body: WalletTransferBody, body: WalletTransferBody,
): Promise<WalletTransferResultData> { ): Promise<WalletTransferResultData> {
return lotteryRequest.post<WalletTransferResultData>( return lotteryRequest.post<WalletTransferResultData>(
`${API_V1_PREFIX}/wallet/transfer-in`, `/wallet/transfer-in`,
body, body,
); );
} }
@@ -47,7 +46,7 @@ export function postWalletTransferOut(
body: WalletTransferBody, body: WalletTransferBody,
): Promise<WalletTransferResultData> { ): Promise<WalletTransferResultData> {
return lotteryRequest.post<WalletTransferResultData>( return lotteryRequest.post<WalletTransferResultData>(
`${API_V1_PREFIX}/wallet/transfer-out`, `/wallet/transfer-out`,
body, body,
); );
} }

View File

@@ -1,7 +1,6 @@
"use client"; "use client";
import { lotteryHttp, unwrapData } from "@/lib/lottery-http"; import { lotteryHttp, unwrapData } from "@/lib/lottery-http";
import { API_V1_PREFIX } from "@/api/paths";
type RuntimeOriginsResponse = { type RuntimeOriginsResponse = {
iframe_allowed_origins: string[]; iframe_allowed_origins: string[];
@@ -49,7 +48,7 @@ export async function loadIframeAllowedOrigins(): Promise<string[]> {
} }
pendingOrigins ??= lotteryHttp pendingOrigins ??= lotteryHttp
.get(`${API_V1_PREFIX}/integration/runtime-origins`) .get("/integration/runtime-origins")
.then((response) => { .then((response) => {
const data = unwrapData<RuntimeOriginsResponse>(response.data); const data = unwrapData<RuntimeOriginsResponse>(response.data);
cachedOrigins = data.iframe_allowed_origins cachedOrigins = data.iframe_allowed_origins

View File

@@ -0,0 +1,13 @@
/**
* 玩家端 API 连接方式(与管理端一致):
* - 默认:同源 `/api` + Next `rewrites` → Laravel`API_BASE_URL` 写在 .env.local
* - 可选:`NEXT_PUBLIC_LOTTERY_API_BASE_URL` 直连。
*/
export function hasLotteryPlayerApiBaseUrl(): boolean {
const direct = process.env.NEXT_PUBLIC_LOTTERY_API_BASE_URL?.trim();
if (direct !== undefined && direct !== "") {
return true;
}
return process.env.NEXT_PUBLIC_LOTTERY_API_PROXY_DISABLED !== "true";
}

View File

@@ -13,13 +13,13 @@ import {
} from "@/types/api/errors"; } from "@/types/api/errors";
import { isApiEnvelope } from "@/types/api/envelope"; import { isApiEnvelope } from "@/types/api/envelope";
import { useErrorStore } from "@/stores/error-store"; import { useErrorStore } from "@/stores/error-store";
import { LOTTERY_API_V1_BASE } from "@/api/paths";
/** /**
* **第一层**只挂 `baseURL` / `timeout` / 默认 `Accept`。业务解析、toast 都不在这里 * **第一层**`baseURL` 对齐 Laravel `api/v1`;各 `api/*.ts` 只写业务 path如 `/currencies`
*/ */
export const lotteryHttp = axios.create({ export const lotteryHttp = axios.create({
// 统一走 Next 同源 /api 代理,由 next.config.ts 的 API_BASE_URL 转发到后端。 baseURL: LOTTERY_API_V1_BASE,
baseURL: "/api",
timeout: 30_000, timeout: 30_000,
headers: { Accept: "application/json" }, headers: { Accept: "application/json" },
}); });

View File

@@ -0,0 +1,11 @@
/** 解析 `ALLOWED_DEV_ORIGINS`(逗号分隔),供 next.config `allowedDevOrigins` 使用 */
export function parseAllowedDevOrigins(envValue: string | undefined): string[] {
if (envValue === undefined || envValue.trim() === "") {
return [];
}
return envValue
.split(",")
.map((origin) => origin.trim())
.filter((origin) => origin !== "");
}