refactor(env, i18n, http): 更新环境配置与错误提示信息
修改 .env.example,简化 API 配置说明并明确生产环境要求。更新多语言错误提示,确保用户在未启用 API 代理时获得清晰反馈。重构 admin-http.ts,优化 API 基础 URL 的解析逻辑,提升代码可维护性。
This commit is contained in:
33
.env.example
33
.env.example
@@ -1,32 +1,17 @@
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
# 管理端本地配置示例:复制为 .env.local 后按需修改
|
# 管理端本地配置示例:复制为 .env.local 后按需修改
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# 三端联调速查(lotterLaravel + lotteryadmin + lotteryfront):
|
# 三端联调:Laravel 8000、管理端 3801、玩家端 3800
|
||||||
# - Laravel API:php artisan serve → 默认 http://127.0.0.1:8000
|
# 生产部署时 Laravel CORS 需含 https://lotteryadmin.tanumo.com,https://lotteryfront.tanumo.com
|
||||||
# - 管理端: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 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
|
||||||
|
|
||||||
# 测试
|
# Next 开发:局域网 IP 访问(逗号分隔 host,无协议)
|
||||||
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 时设置
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# ALLOWED_DEV_ORIGINS=192.168.0.101
|
# ALLOWED_DEV_ORIGINS=192.168.0.101
|
||||||
|
|||||||
@@ -2,21 +2,12 @@ import type { NextConfig } from "next";
|
|||||||
|
|
||||||
import { parseAllowedDevOrigins } from "./src/lib/next-dev-origins";
|
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 allowedDevOrigins = parseAllowedDevOrigins(process.env.ALLOWED_DEV_ORIGINS);
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
/* config options here */
|
||||||
...(allowedDevOrigins.length > 0 ? { allowedDevOrigins } : {}),
|
...(allowedDevOrigins.length > 0 ? { allowedDevOrigins } : {}),
|
||||||
reactCompiler: true,
|
reactCompiler: true,
|
||||||
async rewrites() {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
source: "/api/:path*",
|
|
||||||
destination: `${apiBaseUrl}/api/:path*`,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
},
|
|
||||||
async redirects() {
|
async redirects() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|||||||
16
src/app/api/[...path]/route.ts
Normal file
16
src/app/api/[...path]/route.ts
Normal file
@@ -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;
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
"submit": "Log in",
|
"submit": "Log in",
|
||||||
"submitting": "Signing in…",
|
"submitting": "Signing in…",
|
||||||
"captchaLoadFailed": "Failed to load captcha. Check the API or network.",
|
"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",
|
"captchaRequired": "Refresh the captcha first",
|
||||||
"welcome": "Welcome, {{name}}",
|
"welcome": "Welcome, {{name}}",
|
||||||
"networkFailed": "Network request failed",
|
"networkFailed": "Network request failed",
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
"submit": "लगइन",
|
"submit": "लगइन",
|
||||||
"submitting": "लगइन हुँदैछ…",
|
"submitting": "लगइन हुँदैछ…",
|
||||||
"captchaLoadFailed": "क्याप्चा लोड गर्न सकिएन। API वा नेटवर्क जाँच गर्नुहोस्।",
|
"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": "पहिले क्याप्चा रिफ्रेस गर्नुहोस्",
|
"captchaRequired": "पहिले क्याप्चा रिफ्रेस गर्नुहोस्",
|
||||||
"welcome": "स्वागत छ, {{name}}",
|
"welcome": "स्वागत छ, {{name}}",
|
||||||
"networkFailed": "नेटवर्क अनुरोध असफल भयो",
|
"networkFailed": "नेटवर्क अनुरोध असफल भयो",
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
"submit": "登录",
|
"submit": "登录",
|
||||||
"submitting": "登录中…",
|
"submitting": "登录中…",
|
||||||
"captchaLoadFailed": "无法获取验证码,请检查接口或网络",
|
"captchaLoadFailed": "无法获取验证码,请检查接口或网络",
|
||||||
"apiBaseMissingToast": "未配置 API:请在管理端 .env.local 设置 API_BASE_URL(Next 反代),或设置 NEXT_PUBLIC_LOTTERY_API_BASE_URL(直连)",
|
"apiBaseMissingToast": "API 代理未启用:请确认宝塔或本地 Next 已转发 /api 到 Laravel",
|
||||||
"captchaRequired": "请先刷新验证码",
|
"captchaRequired": "请先刷新验证码",
|
||||||
"welcome": "欢迎,{{name}}",
|
"welcome": "欢迎,{{name}}",
|
||||||
"networkFailed": "网络请求失败",
|
"networkFailed": "网络请求失败",
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ import { withAdminAuthHeader } from "@/lib/admin-auth";
|
|||||||
import { withAdminLocaleHeaders } from "@/lib/admin-locale";
|
import { withAdminLocaleHeaders } from "@/lib/admin-locale";
|
||||||
import { LotteryApiBizError, LotteryApiEnvelopeError } from "@/types/api/errors";
|
import { LotteryApiBizError, LotteryApiEnvelopeError } from "@/types/api/errors";
|
||||||
import { isApiEnvelope } from "@/types/api/envelope";
|
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 { hasLotteryAdminApiBaseUrl } from "@/lib/lottery-api-env";
|
||||||
|
|
||||||
export const adminHttp = axios.create({
|
export const adminHttp = axios.create({
|
||||||
baseURL: LOTTERY_API_V1_BASE,
|
baseURL: resolveLotteryApiV1Base(),
|
||||||
timeout: 30_000,
|
timeout: 30_000,
|
||||||
headers: { Accept: "application/json" },
|
headers: { Accept: "application/json" },
|
||||||
});
|
});
|
||||||
|
|||||||
19
src/lib/lottery-api-base.ts
Normal file
19
src/lib/lottery-api-base.ts
Normal file
@@ -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";
|
||||||
|
}
|
||||||
73
src/lib/lottery-api-dev-proxy.ts
Normal file
73
src/lib/lottery-api-dev-proxy.ts
Normal file
@@ -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<NextResponse> {
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,19 +1,4 @@
|
|||||||
/**
|
/** 是否已配置可请求的 Laravel API(默认有本地 origin,极少用 PROXY_DISABLED 关闭)。 */
|
||||||
* 管理端 API 连接方式:
|
|
||||||
* - 默认:浏览器请求同源 `/api/*`,由 next.config `rewrites` 转发到 Laravel(需配置服务端 `API_BASE_URL`)。
|
|
||||||
* - 可选:设置 `NEXT_PUBLIC_LOTTERY_API_BASE_URL` 直连后端(不经 Next 反代)。
|
|
||||||
*/
|
|
||||||
export function hasLotteryAdminApiBaseUrl(): boolean {
|
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";
|
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;
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user