diff --git a/.env.example b/.env.example index 2d6d262..9fbd458 100644 --- a/.env.example +++ b/.env.example @@ -1,32 +1,17 @@ # ============================================================================= # 管理端本地配置示例:复制为 .env.local 后按需修改 # ============================================================================= -# 三端联调速查(lotterLaravel + lotteryadmin + lotteryfront): -# - Laravel API:php artisan serve → 默认 http://127.0.0.1:8000 -# - 管理端:npm run dev → http://localhost:3801(浏览器请求 /api → 反代到 API_BASE_URL) -# - 玩家端:npm run dev → http://localhost:3800 -# - Laravel .env:CORS_ALLOWED_ORIGINS、SANCTUM_STATEFUL_DOMAINS 需包含上述前端 origin -# - Reverb:Laravel REVERB_* 与玩家端 NEXT_PUBLIC_REVERB_* 一致(管理端一般不需 Echo) +# 三端联调:Laravel 8000、管理端 3801、玩家端 3800 +# 生产部署时 Laravel CORS 需含 https://lotteryadmin.tanumo.com,https://lotteryfront.tanumo.com # ----------------------------------------------------------------------------- -# Laravel API(Next rewrites:/api/* → ${API_BASE_URL}/api/*) +# Laravel API +# - 浏览器始终请求同源 /api/v1 +# - 宝塔线上负责把 /api/* 转发到 Laravel +# - 本地 npm run dev 时,Next 临时把 /api/* 代理到 LOTTERY_API_UPSTREAM # ----------------------------------------------------------------------------- -# 手动切换环境:保留一个生效,另一个注释掉 +LOTTERY_API_UPSTREAM=http://127.0.0.1:8000 +# LOTTERY_API_UPSTREAM=https://lotterylaravel.tanumo.com -# 测试 -API_BASE_URL=http://127.0.0.1:8000 -# 线上 -# API_BASE_URL=https://api.your-production-domain.com - -# ----------------------------------------------------------------------------- -# 可选:直连 Laravel(不经 Next 反代);一般本地开发用 API_BASE_URL 即可 -# ----------------------------------------------------------------------------- -# NEXT_PUBLIC_LOTTERY_API_BASE_URL=http://127.0.0.1:8000 -# 显式关闭「已配置 API」检测(极少用) -# NEXT_PUBLIC_LOTTERY_API_PROXY_DISABLED=true - -# ----------------------------------------------------------------------------- -# Next 开发:局域网用 IP 访问时,允许该 host(逗号分隔,无协议) -# 示例:手机访问 http://192.168.0.101:3801 时设置 -# ----------------------------------------------------------------------------- +# Next 开发:局域网 IP 访问(逗号分隔 host,无协议) # ALLOWED_DEV_ORIGINS=192.168.0.101 diff --git a/next.config.ts b/next.config.ts index 065aff7..ca96a55 100644 --- a/next.config.ts +++ b/next.config.ts @@ -2,21 +2,12 @@ import type { NextConfig } from "next"; import { parseAllowedDevOrigins } from "./src/lib/next-dev-origins"; -const apiBaseUrl = process.env.API_BASE_URL?.trim() || "http://127.0.0.1:8000"; const allowedDevOrigins = parseAllowedDevOrigins(process.env.ALLOWED_DEV_ORIGINS); const nextConfig: NextConfig = { /* config options here */ ...(allowedDevOrigins.length > 0 ? { allowedDevOrigins } : {}), reactCompiler: true, - async rewrites() { - return [ - { - source: "/api/:path*", - destination: `${apiBaseUrl}/api/:path*`, - }, - ]; - }, async redirects() { return [ { diff --git a/src/app/api/[...path]/route.ts b/src/app/api/[...path]/route.ts new file mode 100644 index 0000000..0c9f3f1 --- /dev/null +++ b/src/app/api/[...path]/route.ts @@ -0,0 +1,16 @@ +import { type NextRequest } from "next/server"; + +import { proxyLotteryApiInDev } from "@/lib/lottery-api-dev-proxy"; + +type RouteContext = { params: Promise<{ path: string[] }> }; + +async function handle(request: NextRequest, context: RouteContext) { + const { path } = await context.params; + return proxyLotteryApiInDev(request, path); +} + +export const GET = handle; +export const POST = handle; +export const PUT = handle; +export const PATCH = handle; +export const DELETE = handle; diff --git a/src/i18n/locales/en/auth.json b/src/i18n/locales/en/auth.json index cc4263f..c0b473c 100644 --- a/src/i18n/locales/en/auth.json +++ b/src/i18n/locales/en/auth.json @@ -16,7 +16,7 @@ "submit": "Log in", "submitting": "Signing in…", "captchaLoadFailed": "Failed to load captcha. Check the API or network.", - "apiBaseMissingToast": "API not configured: set API_BASE_URL in admin .env.local (Next proxy) or NEXT_PUBLIC_LOTTERY_API_BASE_URL (direct)", + "apiBaseMissingToast": "API proxy is not enabled: make sure /api is forwarded to Laravel", "captchaRequired": "Refresh the captcha first", "welcome": "Welcome, {{name}}", "networkFailed": "Network request failed", diff --git a/src/i18n/locales/ne/auth.json b/src/i18n/locales/ne/auth.json index 8fd9f15..a5f97fa 100644 --- a/src/i18n/locales/ne/auth.json +++ b/src/i18n/locales/ne/auth.json @@ -16,7 +16,7 @@ "submit": "लगइन", "submitting": "लगइन हुँदैछ…", "captchaLoadFailed": "क्याप्चा लोड गर्न सकिएन। API वा नेटवर्क जाँच गर्नुहोस्।", - "apiBaseMissingToast": "API कन्फिग गरिएको छैन: admin .env.local मा API_BASE_URL (Next proxy) वा NEXT_PUBLIC_LOTTERY_API_BASE_URL (direct) सेट गर्नुहोस्", + "apiBaseMissingToast": "API proxy सक्षम छैन: /api Laravel मा forward भएको छ कि छैन जाँच गर्नुहोस्", "captchaRequired": "पहिले क्याप्चा रिफ्रेस गर्नुहोस्", "welcome": "स्वागत छ, {{name}}", "networkFailed": "नेटवर्क अनुरोध असफल भयो", diff --git a/src/i18n/locales/zh/auth.json b/src/i18n/locales/zh/auth.json index 5f95c60..a8bf5b7 100644 --- a/src/i18n/locales/zh/auth.json +++ b/src/i18n/locales/zh/auth.json @@ -16,7 +16,7 @@ "submit": "登录", "submitting": "登录中…", "captchaLoadFailed": "无法获取验证码,请检查接口或网络", - "apiBaseMissingToast": "未配置 API:请在管理端 .env.local 设置 API_BASE_URL(Next 反代),或设置 NEXT_PUBLIC_LOTTERY_API_BASE_URL(直连)", + "apiBaseMissingToast": "API 代理未启用:请确认宝塔或本地 Next 已转发 /api 到 Laravel", "captchaRequired": "请先刷新验证码", "welcome": "欢迎,{{name}}", "networkFailed": "网络请求失败", diff --git a/src/lib/admin-http.ts b/src/lib/admin-http.ts index 521eb20..311afa7 100644 --- a/src/lib/admin-http.ts +++ b/src/lib/admin-http.ts @@ -8,12 +8,12 @@ import { withAdminAuthHeader } from "@/lib/admin-auth"; import { withAdminLocaleHeaders } from "@/lib/admin-locale"; import { LotteryApiBizError, LotteryApiEnvelopeError } from "@/types/api/errors"; import { isApiEnvelope } from "@/types/api/envelope"; -import { LOTTERY_API_V1_BASE } from "@/api/paths"; +import { resolveLotteryApiV1Base } from "@/lib/lottery-api-base"; export { hasLotteryAdminApiBaseUrl } from "@/lib/lottery-api-env"; export const adminHttp = axios.create({ - baseURL: LOTTERY_API_V1_BASE, + baseURL: resolveLotteryApiV1Base(), timeout: 30_000, headers: { Accept: "application/json" }, }); diff --git a/src/lib/lottery-api-base.ts b/src/lib/lottery-api-base.ts new file mode 100644 index 0000000..a57dc6c --- /dev/null +++ b/src/lib/lottery-api-base.ts @@ -0,0 +1,19 @@ +const DEFAULT_LOTTERY_API_ORIGIN = "http://127.0.0.1:8000"; + +/** Laravel 根地址(无尾部 `/`),仅供 Next 本地开发代理使用。 */ +export function lotteryApiOrigin(): string { + const configured = + process.env.LOTTERY_API_UPSTREAM?.trim() || + process.env.NEXT_PUBLIC_LOTTERY_API_BASE_URL?.trim(); + return (configured || DEFAULT_LOTTERY_API_ORIGIN).replace(/\/$/, ""); +} + +/** + * axios `baseURL`: + * - 浏览器始终请求同源 `/api/v1` + * - 线上由宝塔转发 `/api/*` 到 Laravel + * - 本地开发由 `app/api/[...path]/route.ts` 临时代理到 `LOTTERY_API_UPSTREAM` + */ +export function resolveLotteryApiV1Base(): string { + return "/api/v1"; +} diff --git a/src/lib/lottery-api-dev-proxy.ts b/src/lib/lottery-api-dev-proxy.ts new file mode 100644 index 0000000..3292dd1 --- /dev/null +++ b/src/lib/lottery-api-dev-proxy.ts @@ -0,0 +1,73 @@ +import { type NextRequest, NextResponse } from "next/server"; + +import { lotteryApiOrigin } from "@/lib/lottery-api-base"; + +const HOP_BY_HOP = new Set([ + "connection", + "keep-alive", + "proxy-authenticate", + "proxy-authorization", + "te", + "trailers", + "transfer-encoding", + "upgrade", +]); + +/** fetch 会解压 gzip,须去掉这些头,否则浏览器 ERR_CONTENT_DECODING_FAILED */ +const STRIP_RESPONSE_HEADERS = [ + ...HOP_BY_HOP, + "content-encoding", + "content-length", +]; + +/** 本地开发:/api/* → Laravel(Turbopack 下 next.config rewrites 常不生效)。 */ +export async function proxyLotteryApiInDev( + request: NextRequest, + pathSegments: string[], +): Promise { + if (process.env.NODE_ENV !== "development") { + return NextResponse.json({ msg: "Not Found" }, { status: 404 }); + } + + const path = pathSegments.join("/"); + const target = `${lotteryApiOrigin()}/api/${path}${request.nextUrl.search}`; + + const headers = new Headers(request.headers); + headers.delete("host"); + headers.delete("connection"); + headers.delete("accept-encoding"); + + const init: RequestInit = { + method: request.method, + headers, + redirect: "manual", + }; + + if (request.method !== "GET" && request.method !== "HEAD") { + init.body = await request.arrayBuffer(); + } + + let upstream: Response; + try { + upstream = await fetch(target, init); + } catch (error: unknown) { + return NextResponse.json( + { + msg: "Upstream Laravel unreachable", + target, + error: error instanceof Error ? error.message : "Unknown error", + }, + { status: 502 }, + ); + } + const responseHeaders = new Headers(upstream.headers); + for (const name of STRIP_RESPONSE_HEADERS) { + responseHeaders.delete(name); + } + + return new NextResponse(upstream.body, { + status: upstream.status, + statusText: upstream.statusText, + headers: responseHeaders, + }); +} diff --git a/src/lib/lottery-api-env.ts b/src/lib/lottery-api-env.ts index cbd7dcb..f851066 100644 --- a/src/lib/lottery-api-env.ts +++ b/src/lib/lottery-api-env.ts @@ -1,19 +1,4 @@ -/** - * 管理端 API 连接方式: - * - 默认:浏览器请求同源 `/api/*`,由 next.config `rewrites` 转发到 Laravel(需配置服务端 `API_BASE_URL`)。 - * - 可选:设置 `NEXT_PUBLIC_LOTTERY_API_BASE_URL` 直连后端(不经 Next 反代)。 - */ +/** 是否已配置可请求的 Laravel API(默认有本地 origin,极少用 PROXY_DISABLED 关闭)。 */ export function hasLotteryAdminApiBaseUrl(): 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"; } - -/** 直连模式下的 Laravel API 根(含 `/api` 前缀由调用方 path 决定) */ -export function lotteryAdminApiDirectOrigin(): string | null { - const direct = process.env.NEXT_PUBLIC_LOTTERY_API_BASE_URL?.trim(); - return direct !== undefined && direct !== "" ? direct.replace(/\/$/, "") : null; -}