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
# 可选:直连 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不设则由后端选默认可下注币种。
# NEXT_PUBLIC_LOTTERY_PLAY_CURRENCY=NPR
@@ -16,9 +28,10 @@ API_BASE_URL=http://127.0.0.1:8000
# -----------------------------------------------------------------------------
# Laravel ReverbWebSocket。不配则 Echo 为空,会一直显示「降级模式 / 轮询」。
# 须与 lotterLaravel .env 的 REVERB_APP_KEY / REVERB_HOST / REVERB_PORT / REVERB_SCHEME 一致。
# 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_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 { LOTTERY_API_V1_BASE } from "./src/api/paths";
import { generateCSP, nonCspSecurityHeaders } from "./src/lib/csp-config";
type RuntimeOriginsEnvelope = {
@@ -11,7 +12,7 @@ type RuntimeOriginsEnvelope = {
async function loadRuntimeOrigins(request: NextRequest): Promise<string[]> {
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, {
headers: { Accept: "application/json" },
cache: "no-store",

View File

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

View File

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

View File

@@ -1,9 +1,8 @@
import { lotteryRequest } from "@/lib/lottery-http";
import type { HealthData } from "@/types/api/health";
import { API_V1_PREFIX } from "@/api/paths";
/** `GET /api/v1/health` */
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 { getPublicCurrencies } from "@/api/currency";
export { getPlayerPing, getPlayerMe } from "@/api/player";

View File

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

View File

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

View File

@@ -1,5 +1,4 @@
import { lotteryRequest } from "@/lib/lottery-http";
import { API_V1_PREFIX } from "@/api/paths";
import type { PlayEffectivePayload } from "@/types/api/play-effective";
export type GetPlayEffectiveParams = {
@@ -15,7 +14,7 @@ export function getPlayEffective(
params?: GetPlayEffectiveParams,
): Promise<PlayEffectivePayload> {
return lotteryRequest.get<PlayEffectivePayload>(
`${API_V1_PREFIX}/play/effective`,
`/play/effective`,
{
params:
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 { ScopePingData } from "@/types/api/ping";
import { API_V1_PREFIX } from "@/api/paths";
/** `GET /api/v1/player/ping`(无需登录) */
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 */
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 { API_V1_PREFIX } from "@/api/paths";
export type SettingItem = {
key: string;
@@ -13,7 +12,7 @@ export type SettingListResponse = {
};
export async function getPublicSettings(group: string): Promise<SettingListResponse> {
return lotteryRequest.get<SettingListResponse>(`${API_V1_PREFIX}/settings`, {
return lotteryRequest.get<SettingListResponse>(`/settings`, {
params: { group },
});
}

View File

@@ -1,5 +1,4 @@
import { lotteryRequest } from "@/lib/lottery-http";
import { API_V1_PREFIX } from "@/api/paths";
import type {
TicketDrawMyMatchPayload,
TicketItemDetailPayload,
@@ -21,7 +20,7 @@ export function getTicketItems(
params?: GetTicketItemsParams,
): Promise<TicketItemsListPayload> {
return lotteryRequest.get<TicketItemsListPayload>(
`${API_V1_PREFIX}/ticket/items`,
`/ticket/items`,
{
params: {
page: params?.page,
@@ -42,7 +41,7 @@ export function getTicketItemDetail(
): Promise<TicketItemDetailPayload> {
const enc = encodeURIComponent(ticketNo);
return lotteryRequest.get<TicketItemDetailPayload>(
`${API_V1_PREFIX}/ticket/items/${enc}`,
`/ticket/items/${enc}`,
);
}
@@ -52,6 +51,6 @@ export function getTicketDrawMyMatch(
): Promise<TicketDrawMyMatchPayload> {
const enc = encodeURIComponent(drawNo);
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 { API_V1_PREFIX } from "@/api/paths";
import type {
TicketPlaceData,
TicketPlacePayload,
@@ -12,7 +11,7 @@ export function postTicketPreview(
body: TicketPreviewPayload,
): Promise<TicketPreviewData> {
return lotteryRequest.post<TicketPreviewData>(
`${API_V1_PREFIX}/ticket/preview`,
`/ticket/preview`,
body,
);
}
@@ -20,7 +19,7 @@ export function postTicketPreview(
/** `POST /api/v1/ticket/place` — 真实下注 */
export function postTicketPlace(body: TicketPlacePayload): Promise<TicketPlaceData> {
return lotteryRequest.post<TicketPlaceData>(
`${API_V1_PREFIX}/ticket/place`,
`/ticket/place`,
body,
);
}

View File

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

View File

@@ -1,7 +1,6 @@
"use client";
import { lotteryHttp, unwrapData } from "@/lib/lottery-http";
import { API_V1_PREFIX } from "@/api/paths";
type RuntimeOriginsResponse = {
iframe_allowed_origins: string[];
@@ -49,7 +48,7 @@ export async function loadIframeAllowedOrigins(): Promise<string[]> {
}
pendingOrigins ??= lotteryHttp
.get(`${API_V1_PREFIX}/integration/runtime-origins`)
.get("/integration/runtime-origins")
.then((response) => {
const data = unwrapData<RuntimeOriginsResponse>(response.data);
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";
import { isApiEnvelope } from "@/types/api/envelope";
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({
// 统一走 Next 同源 /api 代理,由 next.config.ts 的 API_BASE_URL 转发到后端。
baseURL: "/api",
baseURL: LOTTERY_API_V1_BASE,
timeout: 30_000,
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 !== "");
}