From 40f231b76d13bd2621cf417483448b280968494a Mon Sep 17 00:00:00 2001
From: "2569718930@qq.com" <2569718930@qq.com>
Date: Wed, 13 May 2026 14:23:04 +0800
Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=20Vercel=20Fluid=20CPU?=
=?UTF-8?q?=EF=BC=9AAPI=E5=8A=A0CDN=E7=BC=93=E5=AD=98=E3=80=81=E7=BC=A9?=
=?UTF-8?q?=E5=B0=8Fmiddleware=E8=8C=83=E5=9B=B4=E3=80=81=E8=B7=B3?=
=?UTF-8?q?=E8=BF=87=E5=85=AC=E5=85=B1API=E7=9A=84Supabase=E8=BA=AB?=
=?UTF-8?q?=E4=BB=BD=E8=BD=AC=E5=8F=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 7条GET路由加 s-maxage + stale-while-revalidate,fetch 改为 next revalidate
- middleware matcher 从全匹配缩小到9条需auth的路径,移除 isStaticAsset
- 8条公共API路由加 includeSupabaseIdentity: false
- backend-auth getSession 为空时跳过 getUser
- subscription-help 加 I18nProvider,移除 force-dynamic → 静态预渲染
- next.config.mjs 加静态资源 immutable 缓存头
Tested: npx tsc --noEmit 通过,npm run build 通过
---
frontend/app/api/analytics/events/route.ts | 4 ++-
frontend/app/api/cities/route.ts | 5 ++-
frontend/app/api/city/[name]/detail/route.ts | 13 +++++--
.../app/api/city/[name]/market-scan/route.ts | 17 +++++----
frontend/app/api/city/[name]/route.ts | 27 ++++++++------
frontend/app/api/history/[name]/route.ts | 4 ++-
frontend/app/api/payments/config/route.ts | 11 +++---
frontend/app/api/scan/terminal/route.ts | 18 +++++-----
frontend/app/api/system/status/route.ts | 15 +++++---
frontend/app/subscription-help/page.tsx | 9 +++--
frontend/lib/backend-auth.ts | 18 ++++++----
frontend/middleware.ts | 35 +++++++------------
frontend/next.config.mjs | 11 ++++++
13 files changed, 112 insertions(+), 75 deletions(-)
diff --git a/frontend/app/api/analytics/events/route.ts b/frontend/app/api/analytics/events/route.ts
index 01517a3c..3eb2c857 100644
--- a/frontend/app/api/analytics/events/route.ts
+++ b/frontend/app/api/analytics/events/route.ts
@@ -26,7 +26,9 @@ export async function POST(req: NextRequest) {
try {
const body = await req.json();
- const auth = await buildBackendRequestHeaders(req);
+ const auth = await buildBackendRequestHeaders(req, {
+ includeSupabaseIdentity: false,
+ });
const headers = new Headers(auth.headers);
headers.set("Content-Type", "application/json");
const res = await fetch(`${API_BASE}/api/analytics/events`, {
diff --git a/frontend/app/api/cities/route.ts b/frontend/app/api/cities/route.ts
index 8a9d2267..7f1f197b 100644
--- a/frontend/app/api/cities/route.ts
+++ b/frontend/app/api/cities/route.ts
@@ -10,7 +10,6 @@ import {
import { buildCachedJsonResponse } from "@/lib/http-cache";
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
-export const dynamic = "force-dynamic";
export async function GET(req: NextRequest) {
if (!API_BASE) {
@@ -27,7 +26,7 @@ export async function GET(req: NextRequest) {
});
const res = await fetch(`${API_BASE}/api/cities`, {
headers: auth.headers,
- cache: "no-store",
+ next: { revalidate: 60 },
});
if (!res.ok) {
const raw = await res.text();
@@ -38,7 +37,7 @@ export async function GET(req: NextRequest) {
const response = buildCachedJsonResponse(
req,
data,
- "no-store, max-age=0",
+ "public, max-age=0, s-maxage=60, stale-while-revalidate=300",
);
return applyAuthResponseCookies(response, auth.response);
} catch (error) {
diff --git a/frontend/app/api/city/[name]/detail/route.ts b/frontend/app/api/city/[name]/detail/route.ts
index 18c9c6cf..b62b2cad 100644
--- a/frontend/app/api/city/[name]/detail/route.ts
+++ b/frontend/app/api/city/[name]/detail/route.ts
@@ -7,6 +7,7 @@ import {
buildProxyExceptionResponse,
buildUpstreamErrorResponse,
} from "@/lib/api-proxy";
+import { buildCachedJsonResponse } from "@/lib/http-cache";
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
@@ -42,10 +43,12 @@ export async function GET(
const url = `${API_BASE}/api/city/${encodeURIComponent(name)}/detail?${searchParams.toString()}`;
try {
- const auth = await buildBackendRequestHeaders(req);
+ const auth = await buildBackendRequestHeaders(req, {
+ includeSupabaseIdentity: false,
+ });
const res = await fetch(url, {
headers: auth.headers,
- cache: "no-store",
+ next: { revalidate: 15 },
});
if (!res.ok) {
const raw = await res.text();
@@ -53,7 +56,11 @@ export async function GET(
return applyAuthResponseCookies(response, auth.response);
}
const data = await res.json();
- const response = NextResponse.json(data);
+ const response = buildCachedJsonResponse(
+ req,
+ data,
+ "public, max-age=0, s-maxage=15, stale-while-revalidate=45",
+ );
return applyAuthResponseCookies(response, auth.response);
} catch (error) {
const response = buildProxyExceptionResponse(error, {
diff --git a/frontend/app/api/city/[name]/market-scan/route.ts b/frontend/app/api/city/[name]/market-scan/route.ts
index 78b96827..83a0c7f3 100644
--- a/frontend/app/api/city/[name]/market-scan/route.ts
+++ b/frontend/app/api/city/[name]/market-scan/route.ts
@@ -7,6 +7,7 @@ import {
buildProxyExceptionResponse,
buildUpstreamErrorResponse,
} from "@/lib/api-proxy";
+import { buildCachedJsonResponse } from "@/lib/http-cache";
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
@@ -45,10 +46,12 @@ export async function GET(
const url = `${API_BASE}/api/city/${encodeURIComponent(name)}/market-scan?${params.toString()}`;
try {
- const auth = await buildBackendRequestHeaders(req);
+ const auth = await buildBackendRequestHeaders(req, {
+ includeSupabaseIdentity: false,
+ });
const res = await fetch(url, {
headers: auth.headers,
- cache: "no-store",
+ next: { revalidate: 20 },
});
if (!res.ok) {
const raw = await res.text();
@@ -59,11 +62,11 @@ export async function GET(
return applyAuthResponseCookies(response, auth.response);
}
const data = await res.json();
- const response = NextResponse.json(data, {
- headers: {
- "Cache-Control": "no-store",
- },
- });
+ const response = buildCachedJsonResponse(
+ req,
+ data,
+ "public, max-age=0, s-maxage=20, stale-while-revalidate=60",
+ );
return applyAuthResponseCookies(response, auth.response);
} catch (error) {
const response = buildProxyExceptionResponse(error, {
diff --git a/frontend/app/api/city/[name]/route.ts b/frontend/app/api/city/[name]/route.ts
index 44b5c469..205d392b 100644
--- a/frontend/app/api/city/[name]/route.ts
+++ b/frontend/app/api/city/[name]/route.ts
@@ -7,6 +7,7 @@ import {
buildProxyExceptionResponse,
buildUpstreamErrorResponse,
} from "@/lib/api-proxy";
+import { buildCachedJsonResponse } from "@/lib/http-cache";
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
@@ -131,26 +132,28 @@ export async function GET(
const url = `${API_BASE}/api/city/${encodeURIComponent(name)}?force_refresh=${forceRefresh}&depth=${encodeURIComponent(depth)}`;
try {
- const auth = await buildBackendRequestHeaders(req);
+ const auth = await buildBackendRequestHeaders(req, {
+ includeSupabaseIdentity: false,
+ });
const res = await fetch(url, {
headers: auth.headers,
- cache: "no-store",
+ next: { revalidate: 15 },
});
if (!res.ok) {
const raw = await res.text();
const summaryUrl = `${API_BASE}/api/city/${encodeURIComponent(name)}/summary?force_refresh=${forceRefresh}`;
const summaryRes = await fetch(summaryUrl, {
headers: auth.headers,
- cache: "no-store",
+ next: { revalidate: 10 },
});
if (summaryRes.ok) {
const summaryData = await summaryRes.json();
- const response = NextResponse.json(buildFallbackCityDetail(name, depth, summaryData), {
- headers: {
- "Cache-Control": "no-store",
- "X-PolyWeather-Fallback": "summary",
- },
- });
+ const response = buildCachedJsonResponse(
+ req,
+ buildFallbackCityDetail(name, depth, summaryData),
+ "public, max-age=0, s-maxage=10, stale-while-revalidate=30",
+ );
+ response.headers.set("X-PolyWeather-Fallback", "summary");
return applyAuthResponseCookies(response, auth.response);
}
@@ -158,7 +161,11 @@ export async function GET(
return applyAuthResponseCookies(response, auth.response);
}
const data = normalizeCityDetailPayload(await res.json());
- const response = NextResponse.json(data);
+ const response = buildCachedJsonResponse(
+ req,
+ data,
+ "public, max-age=0, s-maxage=15, stale-while-revalidate=45",
+ );
return applyAuthResponseCookies(response, auth.response);
} catch (error) {
const response = buildProxyExceptionResponse(error, {
diff --git a/frontend/app/api/history/[name]/route.ts b/frontend/app/api/history/[name]/route.ts
index e2ad2e49..e5d62ca5 100644
--- a/frontend/app/api/history/[name]/route.ts
+++ b/frontend/app/api/history/[name]/route.ts
@@ -27,7 +27,9 @@ export async function GET(
const url = `${API_BASE}/api/history/${encodeURIComponent(name)}`;
try {
- const auth = await buildBackendRequestHeaders(req);
+ const auth = await buildBackendRequestHeaders(req, {
+ includeSupabaseIdentity: false,
+ });
const fetchOptions = {
headers: auth.headers,
next: { revalidate: 60 },
diff --git a/frontend/app/api/payments/config/route.ts b/frontend/app/api/payments/config/route.ts
index 1ed6e9ae..d2c917be 100644
--- a/frontend/app/api/payments/config/route.ts
+++ b/frontend/app/api/payments/config/route.ts
@@ -7,6 +7,7 @@ import {
buildProxyExceptionResponse,
buildUpstreamErrorResponse,
} from "@/lib/api-proxy";
+import { buildCachedJsonResponse } from "@/lib/http-cache";
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
@@ -21,7 +22,7 @@ export async function GET(req: NextRequest) {
const auth = await buildBackendRequestHeaders(req);
const res = await fetch(`${API_BASE}/api/payments/config`, {
headers: auth.headers,
- cache: "no-store",
+ next: { revalidate: 300 },
});
if (!res.ok) {
const raw = await res.text();
@@ -31,9 +32,11 @@ export async function GET(req: NextRequest) {
return applyAuthResponseCookies(response, auth.response);
}
const data = await res.json();
- const response = NextResponse.json(data, {
- headers: { "Cache-Control": "no-store" },
- });
+ const response = buildCachedJsonResponse(
+ req,
+ data,
+ "public, max-age=0, s-maxage=300, stale-while-revalidate=900",
+ );
return applyAuthResponseCookies(response, auth.response);
} catch (error) {
return buildProxyExceptionResponse(error, {
diff --git a/frontend/app/api/scan/terminal/route.ts b/frontend/app/api/scan/terminal/route.ts
index f9f6f12c..ed239a25 100644
--- a/frontend/app/api/scan/terminal/route.ts
+++ b/frontend/app/api/scan/terminal/route.ts
@@ -7,13 +7,13 @@ import {
buildProxyExceptionResponse,
buildUpstreamErrorResponse,
} from "@/lib/api-proxy";
+import { buildCachedJsonResponse } from "@/lib/http-cache";
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
const SCAN_TERMINAL_PROXY_TIMEOUT_MS = Number(
process.env.POLYWEATHER_SCAN_TERMINAL_PROXY_TIMEOUT_MS || "28000",
);
-export const dynamic = "force-dynamic";
export const maxDuration = 30;
export async function GET(req: NextRequest) {
@@ -50,10 +50,12 @@ export async function GET(req: NextRequest) {
const timeoutId = setTimeout(() => controller.abort(), SCAN_TERMINAL_PROXY_TIMEOUT_MS);
try {
- auth = await buildBackendRequestHeaders(req);
+ auth = await buildBackendRequestHeaders(req, {
+ includeSupabaseIdentity: false,
+ });
const res = await fetch(url, {
headers: auth.headers,
- cache: "no-store",
+ next: { revalidate: 10 },
signal: controller.signal,
});
if (!res.ok) {
@@ -62,11 +64,11 @@ export async function GET(req: NextRequest) {
return applyAuthResponseCookies(response, auth.response);
}
const data = await res.json();
- const response = NextResponse.json(data, {
- headers: {
- "Cache-Control": "no-store",
- },
- });
+ const response = buildCachedJsonResponse(
+ req,
+ data,
+ "public, max-age=0, s-maxage=10, stale-while-revalidate=30",
+ );
return applyAuthResponseCookies(response, auth.response);
} catch (error) {
const timedOut = controller.signal.aborted;
diff --git a/frontend/app/api/system/status/route.ts b/frontend/app/api/system/status/route.ts
index 70e2a88d..c089d406 100644
--- a/frontend/app/api/system/status/route.ts
+++ b/frontend/app/api/system/status/route.ts
@@ -7,6 +7,7 @@ import {
buildProxyExceptionResponse,
buildUpstreamErrorResponse,
} from "@/lib/api-proxy";
+import { buildCachedJsonResponse } from "@/lib/http-cache";
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
@@ -19,10 +20,12 @@ export async function GET(req: NextRequest) {
}
try {
- const auth = await buildBackendRequestHeaders(req);
+ const auth = await buildBackendRequestHeaders(req, {
+ includeSupabaseIdentity: false,
+ });
const res = await fetch(`${API_BASE}/api/system/status`, {
headers: auth.headers,
- cache: "no-store",
+ next: { revalidate: 30 },
});
if (!res.ok) {
const raw = await res.text();
@@ -33,9 +36,11 @@ export async function GET(req: NextRequest) {
}
const data = await res.json();
- const response = NextResponse.json(data, {
- headers: { "Cache-Control": "no-store" },
- });
+ const response = buildCachedJsonResponse(
+ req,
+ data,
+ "public, max-age=0, s-maxage=30, stale-while-revalidate=120",
+ );
return applyAuthResponseCookies(response, auth.response);
} catch (error) {
return buildProxyExceptionResponse(error, {
diff --git a/frontend/app/subscription-help/page.tsx b/frontend/app/subscription-help/page.tsx
index e0248a54..58a8d927 100644
--- a/frontend/app/subscription-help/page.tsx
+++ b/frontend/app/subscription-help/page.tsx
@@ -1,7 +1,6 @@
import type { Metadata } from "next";
import { SubscriptionHelpClient } from "./SubscriptionHelpClient";
-
-export const dynamic = "force-dynamic";
+import { I18nProvider } from "@/hooks/useI18n";
export const metadata: Metadata = {
title: "PolyWeather | Subscription Help",
@@ -9,5 +8,9 @@ export const metadata: Metadata = {
};
export default function SubscriptionHelpPage() {
- return ;
+ return (
+
+
+
+ );
}
diff --git a/frontend/lib/backend-auth.ts b/frontend/lib/backend-auth.ts
index 5436f19f..028bd494 100644
--- a/frontend/lib/backend-auth.ts
+++ b/frontend/lib/backend-auth.ts
@@ -41,17 +41,21 @@ export async function buildBackendRequestHeaders(
const incomingAuth = extractBearerToken(request.headers.get("authorization"));
const includeSupabaseIdentity = options?.includeSupabaseIdentity !== false;
if (hasSupabaseServerEnv() && includeSupabaseIdentity) {
- const passthroughResponse = new NextResponse(null, { status: 200 });
- const supabase = createSupabaseRouteClient(request, passthroughResponse);
+ const supabase = createSupabaseRouteClient(request, new NextResponse(null, { status: 200 }));
const {
data: { session },
} = await supabase.auth.getSession();
- const {
- data: { user },
- } = await supabase.auth.getUser();
+ let user = session?.user ?? null;
+ if (session) {
+ const {
+ data: { user: validated },
+ } = await supabase.auth.getUser();
+ user = validated ?? session.user;
+ }
- const forwardedUserId = String(user?.id || session?.user?.id || "").trim();
- const forwardedEmail = String(user?.email || session?.user?.email || "").trim();
+ const passthroughResponse = new NextResponse(null, { status: 200 });
+ const forwardedUserId = String(user?.id || "").trim();
+ const forwardedEmail = String(user?.email || "").trim();
if (forwardedUserId) {
headers.set(FORWARDED_SUPABASE_USER_ID_HEADER, forwardedUserId);
}
diff --git a/frontend/middleware.ts b/frontend/middleware.ts
index 6ca128e0..e6545e33 100644
--- a/frontend/middleware.ts
+++ b/frontend/middleware.ts
@@ -20,23 +20,6 @@ const SUPABASE_AUTH_REQUIRED = readEnvBool(
SUPABASE_AUTH_ENABLED,
);
-function isStaticAsset(pathname: string) {
- return (
- pathname.startsWith("/_next/") ||
- pathname.startsWith("/favicon") ||
- pathname === "/apple-touch-icon.png" ||
- pathname === "/manifest.webmanifest" ||
- pathname === "/site.webmanifest" ||
- pathname.startsWith("/android-chrome-") ||
- pathname.startsWith("/robots.txt") ||
- pathname.startsWith("/sitemap.xml") ||
- pathname.startsWith("/icons/") ||
- pathname.startsWith("/images/") ||
- pathname.startsWith("/scenery/") ||
- pathname.startsWith("/static/")
- );
-}
-
function isPublicPage(pathname: string) {
return (
pathname === "/" ||
@@ -78,7 +61,7 @@ function handleLegacyTokenGate(request: NextRequest) {
}
const { pathname, searchParams } = request.nextUrl;
- if (isStaticAsset(pathname) || isPublicPage(pathname) || isPublicApi(pathname)) {
+ if (isPublicPage(pathname) || isPublicApi(pathname)) {
return NextResponse.next();
}
@@ -178,10 +161,6 @@ async function handleSupabaseOptionalSession(request: NextRequest) {
}
export async function middleware(request: NextRequest) {
- const { pathname } = request.nextUrl;
- if (isStaticAsset(pathname)) {
- return NextResponse.next();
- }
const requestHost =
request.headers.get("x-forwarded-host") ||
request.headers.get("host") ||
@@ -203,5 +182,15 @@ export async function middleware(request: NextRequest) {
}
export const config = {
- matcher: ["/((?!_next/static|_next/image).*)"],
+ matcher: [
+ "/account/:path*",
+ "/ops/:path*",
+ "/api/auth/:path*",
+ "/api/ops/:path*",
+ "/api/payments/:path*",
+ "/api/system/:path*",
+ "/api/history/:path*",
+ "/api/city/:path*/detail:path*",
+ "/api/scan/terminal/ai:path*",
+ ],
};
diff --git a/frontend/next.config.mjs b/frontend/next.config.mjs
index d5456a15..b8329483 100644
--- a/frontend/next.config.mjs
+++ b/frontend/next.config.mjs
@@ -1,6 +1,17 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
+ async headers() {
+ const cacheHeader = {
+ key: "Cache-Control",
+ value: "public, max-age=31536000, immutable",
+ };
+ const staticExts = ["jpg", "jpeg", "png", "gif", "ico", "svg", "webp", "avif", "woff2", "ttf", "eot", "css", "js"];
+ return staticExts.map((ext) => ({
+ source: `/:path(.+\\.${ext})`,
+ headers: [cacheHeader],
+ }));
+ },
};
export default nextConfig;