diff --git a/frontend/admin-web/next.config.ts b/frontend/admin-web/next.config.ts index c0ea6e9..8f91376 100644 --- a/frontend/admin-web/next.config.ts +++ b/frontend/admin-web/next.config.ts @@ -2,6 +2,10 @@ import type { NextConfig } from 'next'; const nextConfig: NextConfig = { output: 'standalone', + // 构建时注入时间戳,客户端和 /api/version 均使用此值判断版本是否更新 + env: { + NEXT_PUBLIC_BUILD_TIME: String(Date.now()), + }, reactStrictMode: true, images: { remotePatterns: [ diff --git a/frontend/admin-web/src/app/api/version/route.ts b/frontend/admin-web/src/app/api/version/route.ts new file mode 100644 index 0000000..7837d9c --- /dev/null +++ b/frontend/admin-web/src/app/api/version/route.ts @@ -0,0 +1,8 @@ +import { NextResponse } from 'next/server'; + +export function GET() { + return NextResponse.json( + { buildTime: process.env.NEXT_PUBLIC_BUILD_TIME }, + { headers: { 'Cache-Control': 'no-store' } }, + ); +} diff --git a/frontend/admin-web/src/app/providers.tsx b/frontend/admin-web/src/app/providers.tsx index 77bf7b4..9e1d7b2 100644 --- a/frontend/admin-web/src/app/providers.tsx +++ b/frontend/admin-web/src/app/providers.tsx @@ -3,6 +3,7 @@ import React, { useState } from 'react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { AuthProvider } from '@/lib/auth-context'; +import { DeployGuard } from '@/lib/deploy-guard'; export function Providers({ children }: { children: React.ReactNode }) { const [queryClient] = useState( @@ -20,7 +21,10 @@ export function Providers({ children }: { children: React.ReactNode }) { return ( - {children} + + + {children} + ); } diff --git a/frontend/admin-web/src/lib/deploy-guard.tsx b/frontend/admin-web/src/lib/deploy-guard.tsx new file mode 100644 index 0000000..01d2804 --- /dev/null +++ b/frontend/admin-web/src/lib/deploy-guard.tsx @@ -0,0 +1,40 @@ +'use client'; + +import { useEffect } from 'react'; + +const BUILD_TIME = process.env.NEXT_PUBLIC_BUILD_TIME ?? ''; +const POLL_INTERVAL = 3 * 60 * 1000; // 3 分钟 + +/** + * 部署版本守卫 + * + * 每 3 分钟向 /api/version 轮询服务器 build 时间戳。 + * 若与当前 bundle 的时间戳不一致,说明有新版本已部署, + * 立即静默刷新页面,用户无感知。 + * + * 解决:重新部署后浏览器持有旧 bundle → Next.js Server Action + * ID 不匹配 → "Application error" 崩溃。 + */ +export function DeployGuard() { + useEffect(() => { + if (!BUILD_TIME) return; + + const check = async () => { + try { + const res = await fetch('/api/version', { cache: 'no-store' }); + if (!res.ok) return; + const { buildTime } = await res.json(); + if (buildTime && buildTime !== BUILD_TIME) { + window.location.reload(); + } + } catch { + // 网络错误忽略,下次轮询再试 + } + }; + + const timer = setInterval(check, POLL_INTERVAL); + return () => clearInterval(timer); + }, []); + + return null; +}