diff --git a/frontend/src/pages/Maintenance/index.tsx b/frontend/src/pages/Maintenance/index.tsx new file mode 100644 index 00000000..a9a0e7df --- /dev/null +++ b/frontend/src/pages/Maintenance/index.tsx @@ -0,0 +1,167 @@ +import { useState, useEffect } from 'react'; +import { Settings, RefreshCw, Clock, AlertCircle } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { maintenanceDetector } from '@/services/maintenanceDetector'; + +/** + * 维护模式页面 + * + * 功能: + * 1. 显示系统维护状态 + * 2. 后台自动检测服务恢复(静默) + * 3. 支持手动重试连接 + */ +const MaintenancePage = () => { + const [checking, setChecking] = useState(false); + const [lastCheckTime, setLastCheckTime] = useState(new Date()); + + // 后台静默检测(每30秒一次,不打扰用户) + useEffect(() => { + const timer = setInterval(() => { + setLastCheckTime(new Date()); + // 静默检测,不显示loading状态 + maintenanceDetector.checkRecovery(); + }, 30000); // 30秒检测一次 + + return () => clearInterval(timer); + }, []); + + // 手动检测服务器状态 + const handleManualCheck = async () => { + setChecking(true); + setLastCheckTime(new Date()); + + try { + const isRecovered = await maintenanceDetector.checkRecovery(); + + if (!isRecovered) { + console.log('服务尚未恢复,请稍后再试'); + } + // 如果恢复了,maintenanceDetector 会自动跳转 + } catch (error) { + console.error('检测服务恢复失败:', error); + } finally { + setChecking(false); + } + }; + + // 获取维护开始时间(用于显示已维护时长) + const getMaintenanceDuration = () => { + const startTime = maintenanceDetector.getMaintenanceStartTime(); + if (!startTime) return '未知'; + + const duration = Math.floor((Date.now() - startTime) / 1000 / 60); + return duration < 1 ? '刚刚开始' : `${duration} 分钟`; + }; + + return ( +
+
+ {/* 动画图标 */} +
+ +
+
+
+
+ + {/* 标题 */} +

系统升级中

+ +

+ 系统暂时不可用,请稍后再试 +

+ +

+ 可能原因:系统升级、服务器维护或临时故障 +

+ + {/* 信息卡片 */} +
+ {/* 状态信息 */} +
+
+ + 已等待 {getMaintenanceDuration()} +
+
+
+ 后台自动检测中 +
+
+ + {/* 重试按钮 */} + + + {/* 说明文字 */} +

+ 系统会自动检测服务恢复,恢复后将自动刷新页面 +

+
+ + {/* 提示信息 */} +
+
+ +
+

您可以:

+
    +
  • 稍等片刻,页面会自动恢复
  • +
  • 点击"手动重试"按钮尝试重新连接
  • +
  • 如长时间未恢复,请联系系统管理员
  • +
+
+
+
+
+ + {/* 添加动画样式 */} + +
+ ); +}; + +export default MaintenancePage; diff --git a/frontend/src/router/index.tsx b/frontend/src/router/index.tsx index 6f8f02fb..5bf27bde 100644 --- a/frontend/src/router/index.tsx +++ b/frontend/src/router/index.tsx @@ -14,6 +14,9 @@ const Forbidden = lazy(() => import('@/pages/Error/403')); const NotFound = lazy(() => import('@/pages/Error/404')); const NoPermission = lazy(() => import('@/pages/Error/NoPermission')); +// 维护页面 +const Maintenance = lazy(() => import('@/pages/Maintenance')); + // 表单设计器测试页面(写死的路由) const FormDesignerTest = lazy(() => import('../pages/FormDesigner')); @@ -78,6 +81,15 @@ export const createDynamicRouter = () => { path: '/login', element: , }, + // 维护页面(独立路由,无需布局和权限验证) + { + path: '/maintenance', + element: ( + }> + + + ), + }, { path: '/', element: , diff --git a/frontend/src/services/maintenanceDetector.ts b/frontend/src/services/maintenanceDetector.ts new file mode 100644 index 00000000..5a58b4f4 --- /dev/null +++ b/frontend/src/services/maintenanceDetector.ts @@ -0,0 +1,184 @@ +/** + * 维护模式检测服务 + * + * 功能: + * 1. 检测后端服务是否可用 + * 2. 连续失败达到阈值时判定为维护模式 + * 3. 自动轮询检测服务恢复 + */ + +class MaintenanceDetector { + private failureCount = 0; + private readonly FAILURE_THRESHOLD = 3; // 连续失败3次判定为维护 + private readonly RESET_INTERVAL = 60000; // 60秒后重置计数 + private resetTimer: NodeJS.Timeout | null = null; + private recoveryCheckTimer: NodeJS.Timeout | null = null; + + /** + * 记录请求失败 + */ + recordFailure() { + this.failureCount++; + console.log(`[MaintenanceDetector] 请求失败次数: ${this.failureCount}/${this.FAILURE_THRESHOLD}`); + + // 清除之前的重置计时器 + if (this.resetTimer) { + clearTimeout(this.resetTimer); + } + + // 60秒后自动重置计数(避免误判网络抖动) + this.resetTimer = setTimeout(() => { + console.log('[MaintenanceDetector] 重置失败计数'); + this.failureCount = 0; + }, this.RESET_INTERVAL); + + // 达到阈值,判定为维护模式 + if (this.failureCount >= this.FAILURE_THRESHOLD) { + this.enterMaintenanceMode(); + } + } + + /** + * 记录请求成功 + */ + recordSuccess() { + if (this.failureCount > 0) { + console.log('[MaintenanceDetector] 请求成功,重置失败计数'); + this.failureCount = 0; + } + + // 清除重置计时器 + if (this.resetTimer) { + clearTimeout(this.resetTimer); + this.resetTimer = null; + } + } + + /** + * 进入维护模式 + */ + private enterMaintenanceMode() { + console.log('[MaintenanceDetector] ========== 进入维护模式 =========='); + console.log('[MaintenanceDetector] 当前路径:', window.location.pathname); + + // 标记维护状态到 sessionStorage + sessionStorage.setItem('maintenance_mode', 'true'); + sessionStorage.setItem('maintenance_start_time', Date.now().toString()); + console.log('[MaintenanceDetector] sessionStorage 已设置'); + + // 跳转到维护页面 + if (window.location.pathname !== '/maintenance') { + console.log('[MaintenanceDetector] 准备跳转到 /maintenance'); + console.log('[MaintenanceDetector] 使用 window.location.replace 跳转...'); + + // 使用 setTimeout 确保跳转执行 + setTimeout(() => { + window.location.replace('/maintenance'); + }, 100); + } else { + console.log('[MaintenanceDetector] 已在维护页面,无需跳转'); + } + + // 开始轮询检测恢复 + this.startRecoveryCheck(); + } + + /** + * 定期检测服务恢复 + */ + private startRecoveryCheck() { + // 避免重复启动 + if (this.recoveryCheckTimer) { + return; + } + + console.log('[MaintenanceDetector] 开始轮询检测服务恢复(每30秒)'); + + this.recoveryCheckTimer = setInterval(async () => { + try { + // 尝试请求一个轻量级接口 + // 使用原生fetch避免触发axios拦截器 + const response = await fetch('/api/health', { + method: 'GET', + cache: 'no-cache', + signal: AbortSignal.timeout(3000) + }); + + // 只有返回 200-299 才算真正恢复 + // 500+ 错误不算恢复 + if (response.ok) { + // 服务恢复 + console.log('[MaintenanceDetector] 服务已恢复!'); + this.exitMaintenanceMode(); + } else { + console.log(`[MaintenanceDetector] 服务仍不可用 (状态码: ${response.status})`); + } + } catch (error) { + console.log('[MaintenanceDetector] 服务检测失败,继续等待...'); + } + }, 30000); // 每30秒检查一次 + } + + /** + * 退出维护模式 + */ + private exitMaintenanceMode() { + console.log('[MaintenanceDetector] 退出维护模式'); + + // 清除维护状态 + sessionStorage.removeItem('maintenance_mode'); + sessionStorage.removeItem('maintenance_start_time'); + + // 停止轮询 + if (this.recoveryCheckTimer) { + clearInterval(this.recoveryCheckTimer); + this.recoveryCheckTimer = null; + } + + // 重置失败计数 + this.failureCount = 0; + + // 刷新页面(返回应用) + window.location.href = '/'; + } + + /** + * 手动检测恢复(供维护页面调用) + */ + async checkRecovery(): Promise { + try { + const response = await fetch('/api/health', { + method: 'GET', + cache: 'no-cache', + signal: AbortSignal.timeout(3000) + }); + + // 只有返回 200-299 才算真正恢复 + if (response.ok) { + this.exitMaintenanceMode(); + return true; + } + return false; + } catch { + return false; + } + } + + /** + * 获取维护开始时间 + */ + getMaintenanceStartTime(): number | null { + const startTime = sessionStorage.getItem('maintenance_start_time'); + return startTime ? parseInt(startTime, 10) : null; + } + + /** + * 检查是否在维护模式 + */ + isInMaintenanceMode(): boolean { + return sessionStorage.getItem('maintenance_mode') === 'true'; + } +} + +// 导出单例 +export const maintenanceDetector = new MaintenanceDetector(); diff --git a/frontend/src/utils/request.ts b/frontend/src/utils/request.ts index 3c36541d..28697dc2 100644 --- a/frontend/src/utils/request.ts +++ b/frontend/src/utils/request.ts @@ -1,6 +1,7 @@ import axios, {AxiosRequestConfig, AxiosResponse} from 'axios'; import {toast} from '@/components/ui/use-toast'; import { API_BASE_URL, isDev } from '@/config/env'; +import { maintenanceDetector } from '@/services/maintenanceDetector'; export interface Response { code: number; @@ -47,6 +48,9 @@ const createRequestConfig = (options?: Partial): RequestOptions }; const responseHandler = (response: AxiosResponse>) => { + // 请求成功,记录到维护检测器 + maintenanceDetector.recordSuccess(); + const result = response.data; if (result.success && result.code === 200) { return result.data; @@ -65,12 +69,12 @@ const responseHandler = (response: AxiosResponse>) => { const errorHandler = (error: any) => { + // 检测网络错误(后端完全不可达) if (!error.response) { - toast({ - title: '网络错误', - description: '网络连接异常,请检查网络', - variant: 'destructive' - }); + // 记录失败到维护检测器 + maintenanceDetector.recordFailure(); + + // 不显示toast,因为会跳转到维护页面 return Promise.reject(error); } @@ -79,6 +83,12 @@ const errorHandler = (error: any) => { } const status = error.response.status; + + // 检测严重的服务器错误(500+),可能是服务器故障或维护 + if (status >= 500) { + maintenanceDetector.recordFailure(); + // 继续执行下面的错误处理,显示具体的错误信息 + } let errorMessage = ''; switch (status) { case 401: @@ -139,6 +149,16 @@ const errorHandler = (error: any) => { request.interceptors.request.use( (config) => { + // 检查是否处于维护模式 + if (maintenanceDetector.isInMaintenanceMode()) { + // 如果在维护模式且当前不在维护页面,跳转 + if (window.location.pathname !== '/maintenance') { + window.location.replace('/maintenance'); + } + // 取消请求,避免继续发送 + return Promise.reject(new Error('System in maintenance mode')); + } + const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = `Bearer ${token}`;