diff --git a/.env.example b/.env.example index cf62d6d..8389770 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,11 @@ # ============================================================================= -# 前端本地配置示例 +# 玩家端本地配置示例:复制为 .env.local 后按需修改 # ============================================================================= +# 三端联调速查见 lotteryadmin/.env.example;本端默认端口 3800。 + +# ----------------------------------------------------------------------------- +# Laravel API(Next 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 Reverb(WebSocket)。不配则 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 \ No newline at end of file +# NEXT_PUBLIC_REVERB_SCHEME=http diff --git a/middleware.ts b/middleware.ts index 6261f06..abd79f5 100644 --- a/middleware.ts +++ b/middleware.ts @@ -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 { 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", diff --git a/next.config.ts b/next.config.ts index d69ab6e..ea2dff0 100644 --- a/next.config.ts +++ b/next.config.ts @@ -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 按后台接入站点白名单动态生成。 diff --git a/src/api/currency.ts b/src/api/currency.ts index d512560..0c04d60 100644 --- a/src/api/currency.ts +++ b/src/api/currency.ts @@ -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 { - return lotteryRequest.get(`${API_V1_PREFIX}/currencies`); + return lotteryRequest.get(`/currencies`); } diff --git a/src/api/draw.ts b/src/api/draw.ts index dbe882e..35bad84 100644 --- a/src/api/draw.ts +++ b/src/api/draw.ts @@ -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 { return lotteryRequest.get( - `${API_V1_PREFIX}/draw/current`, + `/draw/current`, { params: params?.currency ? { currency: params.currency } : undefined }, ); } @@ -35,7 +34,7 @@ export function getDrawResults( params?: GetDrawResultsParams, ): Promise { return lotteryRequest.get( - `${API_V1_PREFIX}/draw/results`, + `/draw/results`, { params: { page: params?.page, @@ -58,7 +57,7 @@ export function getDrawResultByNo( ): Promise { const encoded = encodeURIComponent(drawNo); return lotteryRequest.get( - `${API_V1_PREFIX}/draw/results/${encoded}`, + `/draw/results/${encoded}`, { params: params?.currency ? { currency: params.currency } : undefined }, ); } diff --git a/src/api/health.ts b/src/api/health.ts index b232bae..721779f 100644 --- a/src/api/health.ts +++ b/src/api/health.ts @@ -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 { - return lotteryRequest.get(`${API_V1_PREFIX}/health`); + return lotteryRequest.get(`/health`); } diff --git a/src/api/index.ts b/src/api/index.ts index 7861907..cdc6fa9 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -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"; diff --git a/src/api/jackpot.ts b/src/api/jackpot.ts index 4ba300c..69415f2 100644 --- a/src/api/jackpot.ts +++ b/src/api/jackpot.ts @@ -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 { return lotteryRequest.get( - `${API_V1_PREFIX}/jackpot/summary`, + `/jackpot/summary`, { params: { currency_code: currencyCode } }, ); } diff --git a/src/api/paths.ts b/src/api/paths.ts index fd23d1a..3af8364 100644 --- a/src/api/paths.ts +++ b/src/api/paths.ts @@ -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"; diff --git a/src/api/play.ts b/src/api/play.ts index 5f21d54..a43db48 100644 --- a/src/api/play.ts +++ b/src/api/play.ts @@ -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 { return lotteryRequest.get( - `${API_V1_PREFIX}/play/effective`, + `/play/effective`, { params: params?.currency !== undefined && params.currency !== "" diff --git a/src/api/player.ts b/src/api/player.ts index 25e8cbd..de460e1 100644 --- a/src/api/player.ts +++ b/src/api/player.ts @@ -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 { - return lotteryRequest.get(`${API_V1_PREFIX}/player/ping`); + return lotteryRequest.get(`/player/ping`); } /** `GET /api/v1/player/me`(需玩家 Token) */ export function getPlayerMe(): Promise { - return lotteryRequest.get(`${API_V1_PREFIX}/player/me`); + return lotteryRequest.get(`/player/me`); } diff --git a/src/api/settings.ts b/src/api/settings.ts index bae4078..0d36d43 100644 --- a/src/api/settings.ts +++ b/src/api/settings.ts @@ -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 { - return lotteryRequest.get(`${API_V1_PREFIX}/settings`, { + return lotteryRequest.get(`/settings`, { params: { group }, }); } diff --git a/src/api/ticket-items.ts b/src/api/ticket-items.ts index dbbf24b..b3ee177 100644 --- a/src/api/ticket-items.ts +++ b/src/api/ticket-items.ts @@ -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 { return lotteryRequest.get( - `${API_V1_PREFIX}/ticket/items`, + `/ticket/items`, { params: { page: params?.page, @@ -42,7 +41,7 @@ export function getTicketItemDetail( ): Promise { const enc = encodeURIComponent(ticketNo); return lotteryRequest.get( - `${API_V1_PREFIX}/ticket/items/${enc}`, + `/ticket/items/${enc}`, ); } @@ -52,6 +51,6 @@ export function getTicketDrawMyMatch( ): Promise { const enc = encodeURIComponent(drawNo); return lotteryRequest.get( - `${API_V1_PREFIX}/ticket/draws/${enc}/my-match`, + `/ticket/draws/${enc}/my-match`, ); } diff --git a/src/api/ticket.ts b/src/api/ticket.ts index e113228..43481d2 100644 --- a/src/api/ticket.ts +++ b/src/api/ticket.ts @@ -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 { return lotteryRequest.post( - `${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 { return lotteryRequest.post( - `${API_V1_PREFIX}/ticket/place`, + `/ticket/place`, body, ); } diff --git a/src/api/wallet.ts b/src/api/wallet.ts index 5c00357..10475d8 100644 --- a/src/api/wallet.ts +++ b/src/api/wallet.ts @@ -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 { return lotteryRequest.get( - `${API_V1_PREFIX}/wallet/balance`, + `/wallet/balance`, { params }, ); } @@ -27,7 +26,7 @@ export function getWalletLogs( params?: GetWalletLogsParams, ): Promise { return lotteryRequest.get( - `${API_V1_PREFIX}/wallet/logs`, + `/wallet/logs`, { params }, ); } @@ -37,7 +36,7 @@ export function postWalletTransferIn( body: WalletTransferBody, ): Promise { return lotteryRequest.post( - `${API_V1_PREFIX}/wallet/transfer-in`, + `/wallet/transfer-in`, body, ); } @@ -47,7 +46,7 @@ export function postWalletTransferOut( body: WalletTransferBody, ): Promise { return lotteryRequest.post( - `${API_V1_PREFIX}/wallet/transfer-out`, + `/wallet/transfer-out`, body, ); } diff --git a/src/lib/iframe-origins.ts b/src/lib/iframe-origins.ts index 02fd5a9..c757bf3 100644 --- a/src/lib/iframe-origins.ts +++ b/src/lib/iframe-origins.ts @@ -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 { } pendingOrigins ??= lotteryHttp - .get(`${API_V1_PREFIX}/integration/runtime-origins`) + .get("/integration/runtime-origins") .then((response) => { const data = unwrapData(response.data); cachedOrigins = data.iframe_allowed_origins diff --git a/src/lib/lottery-api-env.ts b/src/lib/lottery-api-env.ts new file mode 100644 index 0000000..5b08786 --- /dev/null +++ b/src/lib/lottery-api-env.ts @@ -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"; +} diff --git a/src/lib/lottery-http.ts b/src/lib/lottery-http.ts index ec29f52..91fabe4 100644 --- a/src/lib/lottery-http.ts +++ b/src/lib/lottery-http.ts @@ -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" }, }); diff --git a/src/lib/next-dev-origins.ts b/src/lib/next-dev-origins.ts new file mode 100644 index 0000000..599ae92 --- /dev/null +++ b/src/lib/next-dev-origins.ts @@ -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 !== ""); +}