修復 too many connection 問題
This commit is contained in:
371
app/admin/connection-monitor/page.tsx
Normal file
371
app/admin/connection-monitor/page.tsx
Normal file
@@ -0,0 +1,371 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Loader2, Database, Trash2, Settings, Activity, Clock, Users } from 'lucide-react';
|
||||||
|
|
||||||
|
interface ConnectionStats {
|
||||||
|
totalConnections: number;
|
||||||
|
idleConnections: number;
|
||||||
|
oldConnections: number;
|
||||||
|
maxIdleTime: number;
|
||||||
|
maxConnectionAge: number;
|
||||||
|
connections: Array<{
|
||||||
|
createdAt: string;
|
||||||
|
lastUsed: string;
|
||||||
|
idleTime: number;
|
||||||
|
age: number;
|
||||||
|
userId?: string;
|
||||||
|
sessionId?: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ConnectionMonitorPage() {
|
||||||
|
const [stats, setStats] = useState<ConnectionStats | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [message, setMessage] = useState<string>('');
|
||||||
|
const [error, setError] = useState<string>('');
|
||||||
|
const [maxIdleTime, setMaxIdleTime] = useState<number>(300000); // 5分鐘
|
||||||
|
const [maxConnectionAge, setMaxConnectionAge] = useState<number>(1800000); // 30分鐘
|
||||||
|
|
||||||
|
// 獲取連線統計
|
||||||
|
const fetchStats = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetch('/api/connection-monitor?action=stats');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
setStats(data.data);
|
||||||
|
setMessage('統計更新成功');
|
||||||
|
setError('');
|
||||||
|
} else {
|
||||||
|
setError(data.error || '獲取統計失敗');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError('網路錯誤: ' + (err instanceof Error ? err.message : '未知錯誤'));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 強制清理連線
|
||||||
|
const forceCleanup = async () => {
|
||||||
|
if (!confirm('確定要強制清理所有連線嗎?這可能會影響正在進行的操作!')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetch('/api/connection-monitor?action=cleanup');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
setMessage('連線清理完成');
|
||||||
|
setError('');
|
||||||
|
await fetchStats(); // 重新獲取統計
|
||||||
|
} else {
|
||||||
|
setError(data.error || '清理失敗');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError('清理錯誤: ' + (err instanceof Error ? err.message : '未知錯誤'));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 測試智能連線
|
||||||
|
const testConnection = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetch('/api/connection-monitor?action=test');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
setMessage('智能連線測試成功');
|
||||||
|
setError('');
|
||||||
|
await fetchStats(); // 重新獲取統計
|
||||||
|
} else {
|
||||||
|
setError(data.error || '測試失敗');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError('測試錯誤: ' + (err instanceof Error ? err.message : '未知錯誤'));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新清理配置
|
||||||
|
const updateConfig = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetch('/api/connection-monitor', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
action: 'config',
|
||||||
|
maxIdleTime,
|
||||||
|
maxConnectionAge
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
setMessage('清理配置更新成功');
|
||||||
|
setError('');
|
||||||
|
await fetchStats(); // 重新獲取統計
|
||||||
|
} else {
|
||||||
|
setError(data.error || '配置更新失敗');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError('配置更新錯誤: ' + (err instanceof Error ? err.message : '未知錯誤'));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 格式化時間
|
||||||
|
const formatTime = (ms: number) => {
|
||||||
|
const seconds = Math.floor(ms / 1000);
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
|
||||||
|
if (hours > 0) return `${hours}h ${minutes % 60}m`;
|
||||||
|
if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
|
||||||
|
return `${seconds}s`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 組件載入時獲取統計
|
||||||
|
useEffect(() => {
|
||||||
|
fetchStats();
|
||||||
|
|
||||||
|
// 每30秒自動更新統計
|
||||||
|
const interval = setInterval(fetchStats, 30000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-6 space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold">連線監控管理</h1>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
監控和管理資料庫連線的生命週期
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={fetchStats}
|
||||||
|
disabled={loading}
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Activity className="h-4 w-4" />}
|
||||||
|
重新整理
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 統計概覽 */}
|
||||||
|
{stats && (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Database className="h-5 w-5 text-blue-500" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-muted-foreground">總連線數</p>
|
||||||
|
<p className="text-2xl font-bold">{stats.totalConnections}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Clock className="h-5 w-5 text-yellow-500" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-muted-foreground">空閒連線</p>
|
||||||
|
<p className="text-2xl font-bold">{stats.idleConnections}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Users className="h-5 w-5 text-red-500" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-muted-foreground">舊連線</p>
|
||||||
|
<p className="text-2xl font-bold">{stats.oldConnections}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Activity className="h-5 w-5 text-green-500" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-muted-foreground">健康狀態</p>
|
||||||
|
<p className="text-2xl font-bold">
|
||||||
|
{stats.idleConnections + stats.oldConnections === 0 ? '良好' : '需清理'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 操作按鈕 */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Settings className="h-5 w-5" />
|
||||||
|
連線管理
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
管理和清理資料庫連線
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<Button
|
||||||
|
onClick={testConnection}
|
||||||
|
disabled={loading}
|
||||||
|
variant="outline"
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Database className="h-4 w-4" />}
|
||||||
|
測試智能連線
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={forceCleanup}
|
||||||
|
disabled={loading}
|
||||||
|
variant="destructive"
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Trash2 className="h-4 w-4" />}
|
||||||
|
強制清理連線
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={updateConfig}
|
||||||
|
disabled={loading}
|
||||||
|
variant="secondary"
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Settings className="h-4 w-4" />}
|
||||||
|
更新配置
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 配置設定 */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 pt-4 border-t">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="maxIdleTime">最大空閒時間 (毫秒)</Label>
|
||||||
|
<Input
|
||||||
|
id="maxIdleTime"
|
||||||
|
type="number"
|
||||||
|
value={maxIdleTime}
|
||||||
|
onChange={(e) => setMaxIdleTime(Number(e.target.value))}
|
||||||
|
placeholder="300000 (5分鐘)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="maxConnectionAge">最大連線年齡 (毫秒)</Label>
|
||||||
|
<Input
|
||||||
|
id="maxConnectionAge"
|
||||||
|
type="number"
|
||||||
|
value={maxConnectionAge}
|
||||||
|
onChange={(e) => setMaxConnectionAge(Number(e.target.value))}
|
||||||
|
placeholder="1800000 (30分鐘)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 連線詳情 */}
|
||||||
|
{stats && stats.connections.length > 0 && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>連線詳情</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
當前活躍的資料庫連線列表
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{stats.connections.map((conn, index) => (
|
||||||
|
<div key={index} className="flex items-center justify-between p-3 border rounded-lg">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Badge variant="outline">#{index + 1}</Badge>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium">
|
||||||
|
創建時間: {new Date(conn.createdAt).toLocaleString()}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
最後使用: {new Date(conn.lastUsed).toLocaleString()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Badge variant={conn.idleTime > stats.maxIdleTime ? "destructive" : "default"}>
|
||||||
|
空閒: {formatTime(conn.idleTime)}
|
||||||
|
</Badge>
|
||||||
|
<Badge variant={conn.age > stats.maxConnectionAge ? "destructive" : "default"}>
|
||||||
|
年齡: {formatTime(conn.age)}
|
||||||
|
</Badge>
|
||||||
|
{conn.userId && (
|
||||||
|
<Badge variant="secondary">用戶: {conn.userId}</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 訊息顯示 */}
|
||||||
|
{message && (
|
||||||
|
<Alert>
|
||||||
|
<Activity className="h-4 w-4" />
|
||||||
|
<AlertDescription>{message}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<Database className="h-4 w-4" />
|
||||||
|
<AlertDescription>{error}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 說明資訊 */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>智能連線管理說明</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-2 text-sm text-muted-foreground">
|
||||||
|
<p>• <strong>自動清理:</strong> 系統每30秒自動檢查並清理空閒或過期的連線</p>
|
||||||
|
<p>• <strong>智能追蹤:</strong> 追蹤每個連線的創建時間、最後使用時間和用戶信息</p>
|
||||||
|
<p>• <strong>即時監控:</strong> 實時顯示連線狀態,幫助識別連線洩漏問題</p>
|
||||||
|
<p>• <strong>自動釋放:</strong> 查詢完成後自動釋放連線,避免連線累積</p>
|
||||||
|
<p>• <strong>用戶關閉網頁:</strong> 空閒連線會在設定的時間後自動清理</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
267
app/admin/database-shutdown/page.tsx
Normal file
267
app/admin/database-shutdown/page.tsx
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
|
import { Loader2, Database, Power, AlertTriangle, CheckCircle } from 'lucide-react';
|
||||||
|
|
||||||
|
interface ShutdownStatus {
|
||||||
|
isShuttingDown: boolean;
|
||||||
|
handlerCount: number;
|
||||||
|
registeredHandlers: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DatabaseShutdownPage() {
|
||||||
|
const [status, setStatus] = useState<ShutdownStatus | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [message, setMessage] = useState<string>('');
|
||||||
|
const [error, setError] = useState<string>('');
|
||||||
|
|
||||||
|
// 獲取關閉狀態
|
||||||
|
const fetchStatus = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetch('/api/test-shutdown?action=status');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
setStatus(data.data);
|
||||||
|
setMessage('狀態更新成功');
|
||||||
|
setError('');
|
||||||
|
} else {
|
||||||
|
setError(data.error || '獲取狀態失敗');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError('網路錯誤: ' + (err instanceof Error ? err.message : '未知錯誤'));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 測試關閉機制
|
||||||
|
const testShutdown = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetch('/api/test-shutdown?action=test');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
setMessage('關閉機制測試成功');
|
||||||
|
setError('');
|
||||||
|
await fetchStatus(); // 重新獲取狀態
|
||||||
|
} else {
|
||||||
|
setError(data.error || '測試失敗');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError('測試錯誤: ' + (err instanceof Error ? err.message : '未知錯誤'));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 強制關閉測試
|
||||||
|
const forceShutdown = async () => {
|
||||||
|
if (!confirm('確定要執行強制關閉測試嗎?這可能會影響應用程式運行!')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetch('/api/test-shutdown?action=force');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
setMessage('強制關閉測試完成');
|
||||||
|
setError('');
|
||||||
|
await fetchStatus(); // 重新獲取狀態
|
||||||
|
} else {
|
||||||
|
setError(data.error || '強制關閉測試失敗');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError('強制關閉錯誤: ' + (err instanceof Error ? err.message : '未知錯誤'));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 優雅關閉測試
|
||||||
|
const gracefulShutdown = async () => {
|
||||||
|
if (!confirm('確定要執行優雅關閉測試嗎?這會關閉所有資料庫連線!')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetch('/api/test-shutdown', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ action: 'graceful' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
setMessage('優雅關閉測試完成');
|
||||||
|
setError('');
|
||||||
|
await fetchStatus(); // 重新獲取狀態
|
||||||
|
} else {
|
||||||
|
setError(data.error || '優雅關閉測試失敗');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError('優雅關閉錯誤: ' + (err instanceof Error ? err.message : '未知錯誤'));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 組件載入時獲取狀態
|
||||||
|
useEffect(() => {
|
||||||
|
fetchStatus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-6 space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold">資料庫關閉管理</h1>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
監控和管理資料庫連線的關閉機制
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={fetchStatus}
|
||||||
|
disabled={loading}
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Database className="h-4 w-4" />}
|
||||||
|
重新整理狀態
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 狀態顯示 */}
|
||||||
|
{status && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Database className="h-5 w-5" />
|
||||||
|
關閉狀態
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
當前資料庫關閉管理器的運行狀態
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm font-medium">關閉狀態:</span>
|
||||||
|
<Badge variant={status.isShuttingDown ? "destructive" : "default"}>
|
||||||
|
{status.isShuttingDown ? "關閉中" : "正常"}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm font-medium">處理器數量:</span>
|
||||||
|
<Badge variant="outline">{status.handlerCount}</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm font-medium">狀態:</span>
|
||||||
|
<Badge variant={status.isShuttingDown ? "destructive" : "default"}>
|
||||||
|
{status.isShuttingDown ? "異常" : "正常"}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span className="text-sm font-medium">已註冊的處理器:</span>
|
||||||
|
<div className="mt-2 flex flex-wrap gap-2">
|
||||||
|
{status.registeredHandlers.map((handler, index) => (
|
||||||
|
<Badge key={index} variant="secondary">
|
||||||
|
{handler}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 操作按鈕 */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Power className="h-5 w-5" />
|
||||||
|
關閉操作
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
測試和管理資料庫關閉機制
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<Button
|
||||||
|
onClick={testShutdown}
|
||||||
|
disabled={loading}
|
||||||
|
variant="outline"
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <CheckCircle className="h-4 w-4" />}
|
||||||
|
測試關閉機制
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={forceShutdown}
|
||||||
|
disabled={loading}
|
||||||
|
variant="destructive"
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <AlertTriangle className="h-4 w-4" />}
|
||||||
|
強制關閉測試
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={gracefulShutdown}
|
||||||
|
disabled={loading}
|
||||||
|
variant="destructive"
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Power className="h-4 w-4" />}
|
||||||
|
優雅關閉測試
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 訊息顯示 */}
|
||||||
|
{message && (
|
||||||
|
<Alert>
|
||||||
|
<CheckCircle className="h-4 w-4" />
|
||||||
|
<AlertDescription>{message}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<AlertTriangle className="h-4 w-4" />
|
||||||
|
<AlertDescription>{error}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 說明資訊 */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>使用說明</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-2 text-sm text-muted-foreground">
|
||||||
|
<p>• <strong>測試關閉機制:</strong> 檢查關閉處理器是否正常註冊,不會實際關閉連線</p>
|
||||||
|
<p>• <strong>強制關閉測試:</strong> 模擬強制關閉所有資料庫連線(用於測試)</p>
|
||||||
|
<p>• <strong>優雅關閉測試:</strong> 執行完整的優雅關閉流程(會實際關閉連線)</p>
|
||||||
|
<p>• 當應用程式收到 SIGINT 或 SIGTERM 信號時,會自動執行優雅關閉</p>
|
||||||
|
<p>• 關閉管理器會確保所有資料庫連線池都被正確關閉</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
281
app/admin/emergency-cleanup/page.tsx
Normal file
281
app/admin/emergency-cleanup/page.tsx
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
|
import { Loader2, Database, AlertTriangle, Trash2, Eye, Zap } from 'lucide-react';
|
||||||
|
|
||||||
|
interface ConnectionDetail {
|
||||||
|
ID: number;
|
||||||
|
USER: string;
|
||||||
|
HOST: string;
|
||||||
|
DB: string;
|
||||||
|
COMMAND: string;
|
||||||
|
TIME: number;
|
||||||
|
STATE: string;
|
||||||
|
INFO?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ConnectionData {
|
||||||
|
connectionCount: number;
|
||||||
|
connections: ConnectionDetail[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function EmergencyCleanupPage() {
|
||||||
|
const [connectionData, setConnectionData] = useState<ConnectionData | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [message, setMessage] = useState<string>('');
|
||||||
|
const [error, setError] = useState<string>('');
|
||||||
|
|
||||||
|
// 獲取連線詳情
|
||||||
|
const fetchConnectionDetails = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetch('/api/emergency-cleanup?action=details');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
setConnectionData(data.data);
|
||||||
|
setMessage('連線詳情更新成功');
|
||||||
|
setError('');
|
||||||
|
} else {
|
||||||
|
setError(data.error || '獲取連線詳情失敗');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError('網路錯誤: ' + (err instanceof Error ? err.message : '未知錯誤'));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 執行緊急清理
|
||||||
|
const executeEmergencyCleanup = async () => {
|
||||||
|
if (!confirm('確定要執行緊急清理嗎?這會關閉所有資料庫連線!')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetch('/api/emergency-cleanup', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ action: 'cleanup' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
setMessage('緊急清理完成');
|
||||||
|
setError('');
|
||||||
|
await fetchConnectionDetails(); // 重新獲取詳情
|
||||||
|
} else {
|
||||||
|
setError(data.error || '緊急清理失敗');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError('緊急清理錯誤: ' + (err instanceof Error ? err.message : '未知錯誤'));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 強制殺死所有連線
|
||||||
|
const forceKillAllConnections = async () => {
|
||||||
|
if (!confirm('確定要強制殺死所有連線嗎?這會立即終止所有資料庫連線!')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetch('/api/emergency-cleanup', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ action: 'kill-all' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
setMessage('強制殺死連線完成');
|
||||||
|
setError('');
|
||||||
|
await fetchConnectionDetails(); // 重新獲取詳情
|
||||||
|
} else {
|
||||||
|
setError(data.error || '強制殺死連線失敗');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError('強制殺死連線錯誤: ' + (err instanceof Error ? err.message : '未知錯誤'));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 組件載入時獲取連線詳情
|
||||||
|
useEffect(() => {
|
||||||
|
fetchConnectionDetails();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-6 space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold text-red-600">緊急連線清理</h1>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
緊急清理和強制關閉資料庫連線
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={fetchConnectionDetails}
|
||||||
|
disabled={loading}
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Eye className="h-4 w-4" />}
|
||||||
|
重新整理
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 警告提示 */}
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<AlertTriangle className="h-4 w-4" />
|
||||||
|
<AlertDescription>
|
||||||
|
<strong>警告:</strong> 這些操作會立即關閉所有資料庫連線,可能影響正在運行的應用程式。請謹慎使用!
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
{/* 連線統計 */}
|
||||||
|
{connectionData && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Database className="h-5 w-5" />
|
||||||
|
當前連線狀態
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
資料庫連線的詳細信息
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Badge variant="outline" className="text-lg px-3 py-1">
|
||||||
|
總連線數: {connectionData.connectionCount}
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
variant={connectionData.connectionCount > 10 ? "destructive" : "default"}
|
||||||
|
className="text-lg px-3 py-1"
|
||||||
|
>
|
||||||
|
狀態: {connectionData.connectionCount > 10 ? "異常" : "正常"}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 緊急操作 */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2 text-red-600">
|
||||||
|
<Zap className="h-5 w-5" />
|
||||||
|
緊急操作
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
立即清理和關閉資料庫連線
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<Button
|
||||||
|
onClick={executeEmergencyCleanup}
|
||||||
|
disabled={loading}
|
||||||
|
variant="destructive"
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Trash2 className="h-4 w-4" />}
|
||||||
|
緊急清理連線
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={forceKillAllConnections}
|
||||||
|
disabled={loading}
|
||||||
|
variant="destructive"
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <AlertTriangle className="h-4 w-4" />}
|
||||||
|
強制殺死所有連線
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 連線詳情列表 */}
|
||||||
|
{connectionData && connectionData.connections.length > 0 && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>連線詳情列表</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
當前所有資料庫連線的詳細信息
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-2 max-h-96 overflow-y-auto">
|
||||||
|
{connectionData.connections.map((conn, index) => (
|
||||||
|
<div key={conn.ID} className="flex items-center justify-between p-3 border rounded-lg">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Badge variant="outline">#{conn.ID}</Badge>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium">
|
||||||
|
用戶: {conn.USER} | 主機: {conn.HOST}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
資料庫: {conn.DB} | 命令: {conn.COMMAND}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
時間: {conn.TIME}s | 狀態: {conn.STATE}
|
||||||
|
</p>
|
||||||
|
{conn.INFO && (
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
查詢: {conn.INFO.length > 100 ? conn.INFO.substring(0, 100) + '...' : conn.INFO}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 訊息顯示 */}
|
||||||
|
{message && (
|
||||||
|
<Alert>
|
||||||
|
<Database className="h-4 w-4" />
|
||||||
|
<AlertDescription>{message}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<AlertTriangle className="h-4 w-4" />
|
||||||
|
<AlertDescription>{error}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 使用說明 */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>使用說明</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-2 text-sm text-muted-foreground">
|
||||||
|
<p>• <strong>緊急清理連線:</strong> 停止監控並關閉所有連線池,這是較溫和的方式</p>
|
||||||
|
<p>• <strong>強制殺死所有連線:</strong> 直接殺死資料庫中的所有連線,這是更激進的方式</p>
|
||||||
|
<p>• <strong>重新整理:</strong> 獲取最新的連線狀態和詳情</p>
|
||||||
|
<p>• 建議先嘗試「緊急清理連線」,如果無效再使用「強制殺死所有連線」</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
442
app/admin/ip-cleanup/page.tsx
Normal file
442
app/admin/ip-cleanup/page.tsx
Normal file
@@ -0,0 +1,442 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Loader2, Database, AlertTriangle, Trash2, Eye, Zap, Globe } from 'lucide-react';
|
||||||
|
|
||||||
|
interface ConnectionDetail {
|
||||||
|
ID: number;
|
||||||
|
USER: string;
|
||||||
|
HOST: string;
|
||||||
|
DB: string;
|
||||||
|
COMMAND: string;
|
||||||
|
TIME: number;
|
||||||
|
STATE: string;
|
||||||
|
INFO?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ConnectionStatus {
|
||||||
|
ip: string;
|
||||||
|
connectionCount: number;
|
||||||
|
connections: ConnectionDetail[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LocalStats {
|
||||||
|
clientIP: string;
|
||||||
|
trackedConnections: number;
|
||||||
|
connections: Array<{
|
||||||
|
connectionId: string;
|
||||||
|
createdAt: string;
|
||||||
|
lastUsed: string;
|
||||||
|
userAgent?: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function IPCleanupPage() {
|
||||||
|
const [connectionStatus, setConnectionStatus] = useState<ConnectionStatus | null>(null);
|
||||||
|
const [localStats, setLocalStats] = useState<LocalStats | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [message, setMessage] = useState<string>('');
|
||||||
|
const [error, setError] = useState<string>('');
|
||||||
|
const [targetIP, setTargetIP] = useState<string>('');
|
||||||
|
|
||||||
|
// 獲取當前 IP 的連線狀態
|
||||||
|
const fetchCurrentIPStatus = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetch('/api/ip-cleanup?action=status');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
setConnectionStatus(data.data);
|
||||||
|
setMessage('當前 IP 連線狀態更新成功');
|
||||||
|
setError('');
|
||||||
|
} else {
|
||||||
|
setError(data.error || '獲取當前 IP 連線狀態失敗');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError('網路錯誤: ' + (err instanceof Error ? err.message : '未知錯誤'));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 獲取指定 IP 的連線狀態
|
||||||
|
const fetchSpecificIPStatus = async () => {
|
||||||
|
if (!targetIP.trim()) {
|
||||||
|
setError('請輸入目標 IP 地址');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetch(`/api/ip-cleanup?action=status-specific&ip=${encodeURIComponent(targetIP)}`);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
setConnectionStatus(data.data);
|
||||||
|
setMessage(`指定 IP ${targetIP} 的連線狀態更新成功`);
|
||||||
|
setError('');
|
||||||
|
} else {
|
||||||
|
setError(data.error || '獲取指定 IP 連線狀態失敗');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError('網路錯誤: ' + (err instanceof Error ? err.message : '未知錯誤'));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 獲取本地連線統計
|
||||||
|
const fetchLocalStats = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetch('/api/ip-cleanup?action=local-stats');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
setLocalStats(data.data);
|
||||||
|
setMessage('本地連線統計更新成功');
|
||||||
|
setError('');
|
||||||
|
} else {
|
||||||
|
setError(data.error || '獲取本地連線統計失敗');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError('網路錯誤: ' + (err instanceof Error ? err.message : '未知錯誤'));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 清理當前 IP 的連線
|
||||||
|
const cleanupCurrentIP = async () => {
|
||||||
|
if (!confirm('確定要清理當前 IP 的所有連線嗎?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetch('/api/ip-cleanup', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ action: 'cleanup-current' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
setMessage(data.message || '當前 IP 連線清理完成');
|
||||||
|
setError('');
|
||||||
|
await fetchCurrentIPStatus(); // 重新獲取狀態
|
||||||
|
} else {
|
||||||
|
setError(data.error || '當前 IP 連線清理失敗');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError('當前 IP 連線清理錯誤: ' + (err instanceof Error ? err.message : '未知錯誤'));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 清理指定 IP 的連線
|
||||||
|
const cleanupSpecificIP = async () => {
|
||||||
|
if (!targetIP.trim()) {
|
||||||
|
setError('請輸入目標 IP 地址');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confirm(`確定要清理 IP ${targetIP} 的所有連線嗎?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetch('/api/ip-cleanup', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
action: 'cleanup-specific',
|
||||||
|
targetIP: targetIP.trim()
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
setMessage(data.message || `IP ${targetIP} 連線清理完成`);
|
||||||
|
setError('');
|
||||||
|
await fetchSpecificIPStatus(); // 重新獲取狀態
|
||||||
|
} else {
|
||||||
|
setError(data.error || `IP ${targetIP} 連線清理失敗`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError(`IP ${targetIP} 連線清理錯誤: ` + (err instanceof Error ? err.message : '未知錯誤'));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 清理本地追蹤的連線
|
||||||
|
const cleanupLocalConnections = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetch('/api/ip-cleanup', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ action: 'cleanup-local' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
setMessage(data.message || '本地連線清理完成');
|
||||||
|
setError('');
|
||||||
|
await fetchLocalStats(); // 重新獲取統計
|
||||||
|
} else {
|
||||||
|
setError(data.error || '本地連線清理失敗');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError('本地連線清理錯誤: ' + (err instanceof Error ? err.message : '未知錯誤'));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 組件載入時獲取狀態
|
||||||
|
useEffect(() => {
|
||||||
|
fetchCurrentIPStatus();
|
||||||
|
fetchLocalStats();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-6 space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold">IP 連線管理</h1>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
基於 IP 地址的智能連線清理和管理
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
onClick={fetchCurrentIPStatus}
|
||||||
|
disabled={loading}
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Eye className="h-4 w-4" />}
|
||||||
|
當前 IP
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={fetchLocalStats}
|
||||||
|
disabled={loading}
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Database className="h-4 w-4" />}
|
||||||
|
本地統計
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 當前 IP 連線狀態 */}
|
||||||
|
{connectionStatus && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Globe className="h-5 w-5" />
|
||||||
|
IP 連線狀態
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
IP: {connectionStatus.ip} | 連線數: {connectionStatus.connectionCount}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Badge variant="outline" className="text-lg px-3 py-1">
|
||||||
|
連線數: {connectionStatus.connectionCount}
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
variant={connectionStatus.connectionCount > 5 ? "destructive" : "default"}
|
||||||
|
className="text-lg px-3 py-1"
|
||||||
|
>
|
||||||
|
狀態: {connectionStatus.connectionCount > 5 ? "異常" : "正常"}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 本地連線統計 */}
|
||||||
|
{localStats && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Database className="h-5 w-5" />
|
||||||
|
本地連線統計
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
客戶端 IP: {localStats.clientIP} | 追蹤連線數: {localStats.trackedConnections}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{localStats.connections.map((conn, index) => (
|
||||||
|
<div key={index} className="flex items-center justify-between p-2 border rounded">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium">連線 ID: {conn.connectionId}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
創建: {new Date(conn.createdAt).toLocaleString()} |
|
||||||
|
最後使用: {new Date(conn.lastUsed).toLocaleString()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 操作按鈕 */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Trash2 className="h-5 w-5" />
|
||||||
|
IP 連線清理
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
清理指定 IP 地址的資料庫連線
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<Button
|
||||||
|
onClick={cleanupCurrentIP}
|
||||||
|
disabled={loading}
|
||||||
|
variant="destructive"
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Trash2 className="h-4 w-4" />}
|
||||||
|
清理當前 IP 連線
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={cleanupLocalConnections}
|
||||||
|
disabled={loading}
|
||||||
|
variant="outline"
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Database className="h-4 w-4" />}
|
||||||
|
清理本地追蹤連線
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 指定 IP 清理 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="targetIP">指定 IP 地址</Label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
id="targetIP"
|
||||||
|
value={targetIP}
|
||||||
|
onChange={(e) => setTargetIP(e.target.value)}
|
||||||
|
placeholder="例如: 192.168.1.100"
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={fetchSpecificIPStatus}
|
||||||
|
disabled={loading || !targetIP.trim()}
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
<Eye className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={cleanupSpecificIP}
|
||||||
|
disabled={loading || !targetIP.trim()}
|
||||||
|
variant="destructive"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 連線詳情列表 */}
|
||||||
|
{connectionStatus && connectionStatus.connections.length > 0 && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>連線詳情列表</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
IP {connectionStatus.ip} 的所有資料庫連線
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-2 max-h-96 overflow-y-auto">
|
||||||
|
{connectionStatus.connections.map((conn, index) => (
|
||||||
|
<div key={conn.ID} className="flex items-center justify-between p-3 border rounded-lg">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Badge variant="outline">#{conn.ID}</Badge>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium">
|
||||||
|
用戶: {conn.USER} | 主機: {conn.HOST}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
資料庫: {conn.DB} | 命令: {conn.COMMAND}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
時間: {conn.TIME}s | 狀態: {conn.STATE}
|
||||||
|
</p>
|
||||||
|
{conn.INFO && (
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
查詢: {conn.INFO.length > 100 ? conn.INFO.substring(0, 100) + '...' : conn.INFO}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 訊息顯示 */}
|
||||||
|
{message && (
|
||||||
|
<Alert>
|
||||||
|
<Database className="h-4 w-4" />
|
||||||
|
<AlertDescription>{message}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<AlertTriangle className="h-4 w-4" />
|
||||||
|
<AlertDescription>{error}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 使用說明 */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>IP 連線管理說明</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-2 text-sm text-muted-foreground">
|
||||||
|
<p>• <strong>當前 IP 連線:</strong> 顯示和清理當前訪問者的所有資料庫連線</p>
|
||||||
|
<p>• <strong>指定 IP 連線:</strong> 輸入特定 IP 地址來查看和清理該 IP 的連線</p>
|
||||||
|
<p>• <strong>本地追蹤連線:</strong> 清理應用程式內部追蹤的連線記錄</p>
|
||||||
|
<p>• <strong>智能清理:</strong> 只清理指定 IP 的連線,不影響其他用戶</p>
|
||||||
|
<p>• <strong>關閉網站時:</strong> 系統會自動清理當前 IP 的所有連線</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
314
app/admin/ultimate-kill/page.tsx
Normal file
314
app/admin/ultimate-kill/page.tsx
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
|
import { Loader2, Database, AlertTriangle, Trash2, Eye, Zap, Skull } from 'lucide-react';
|
||||||
|
|
||||||
|
interface ConnectionStatus {
|
||||||
|
currentConnections: number;
|
||||||
|
maxConnections: number;
|
||||||
|
usagePercentage: number;
|
||||||
|
connectionDetails: Array<{
|
||||||
|
ID: number;
|
||||||
|
USER: string;
|
||||||
|
HOST: string;
|
||||||
|
DB: string;
|
||||||
|
COMMAND: string;
|
||||||
|
TIME: number;
|
||||||
|
STATE: string;
|
||||||
|
INFO?: string;
|
||||||
|
}>;
|
||||||
|
connectionCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function UltimateKillPage() {
|
||||||
|
const [status, setStatus] = useState<ConnectionStatus | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [message, setMessage] = useState<string>('');
|
||||||
|
const [error, setError] = useState<string>('');
|
||||||
|
|
||||||
|
// 獲取連線狀態
|
||||||
|
const fetchStatus = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetch('/api/ultimate-kill?action=status');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
setStatus(data.data);
|
||||||
|
setMessage('狀態更新成功');
|
||||||
|
setError('');
|
||||||
|
} else {
|
||||||
|
setError(data.error || '獲取狀態失敗');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError('網路錯誤: ' + (err instanceof Error ? err.message : '未知錯誤'));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 終極清理 - 殺死所有連線
|
||||||
|
const ultimateKill = async () => {
|
||||||
|
if (!confirm('確定要執行終極清理嗎?這會強制殺死所有資料庫連線!')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetch('/api/ultimate-kill', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ action: 'kill-all' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
setMessage(data.message || '終極清理完成');
|
||||||
|
setError('');
|
||||||
|
await fetchStatus(); // 重新獲取狀態
|
||||||
|
} else {
|
||||||
|
setError(data.error || '終極清理失敗');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError('終極清理錯誤: ' + (err instanceof Error ? err.message : '未知錯誤'));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 強制重啟資料庫連線
|
||||||
|
const forceRestart = async () => {
|
||||||
|
if (!confirm('確定要強制重啟資料庫連線嗎?這會先殺死所有連線然後重新建立!')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetch('/api/ultimate-kill', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ action: 'restart' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
setMessage(data.message || '強制重啟完成');
|
||||||
|
setError('');
|
||||||
|
await fetchStatus(); // 重新獲取狀態
|
||||||
|
} else {
|
||||||
|
setError(data.error || '強制重啟失敗');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError('強制重啟錯誤: ' + (err instanceof Error ? err.message : '未知錯誤'));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 組件載入時獲取狀態
|
||||||
|
useEffect(() => {
|
||||||
|
fetchStatus();
|
||||||
|
|
||||||
|
// 每10秒自動更新狀態
|
||||||
|
const interval = setInterval(fetchStatus, 10000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-6 space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold text-red-600">終極連線清理</h1>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
強制殺死所有資料庫連線 - 最後手段
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={fetchStatus}
|
||||||
|
disabled={loading}
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Eye className="h-4 w-4" />}
|
||||||
|
重新整理
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 嚴重警告 */}
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<Skull className="h-4 w-4" />
|
||||||
|
<AlertDescription>
|
||||||
|
<strong>嚴重警告:</strong> 這些操作會立即強制殺死所有資料庫連線,可能導致正在運行的應用程式崩潰。請確保沒有重要操作正在進行!
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
{/* 連線狀態概覽 */}
|
||||||
|
{status && (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Database className="h-5 w-5 text-blue-500" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-muted-foreground">總連線數</p>
|
||||||
|
<p className="text-2xl font-bold">{status.currentConnections}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<AlertTriangle className="h-5 w-5 text-yellow-500" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-muted-foreground">最大連線數</p>
|
||||||
|
<p className="text-2xl font-bold">{status.maxConnections}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Zap className="h-5 w-5 text-red-500" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-muted-foreground">使用率</p>
|
||||||
|
<p className="text-2xl font-bold">{status.usagePercentage.toFixed(1)}%</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Trash2 className="h-5 w-5 text-green-500" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-muted-foreground">狀態</p>
|
||||||
|
<p className="text-2xl font-bold">
|
||||||
|
{status.connectionCount <= 1 ? '正常' : '異常'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 終極操作 */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2 text-red-600">
|
||||||
|
<Skull className="h-5 w-5" />
|
||||||
|
終極操作
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
強制殺死所有資料庫連線 - 最後手段
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<Button
|
||||||
|
onClick={ultimateKill}
|
||||||
|
disabled={loading}
|
||||||
|
variant="destructive"
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Skull className="h-4 w-4" />}
|
||||||
|
終極清理 - 殺死所有連線
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={forceRestart}
|
||||||
|
disabled={loading}
|
||||||
|
variant="destructive"
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Zap className="h-4 w-4" />}
|
||||||
|
強制重啟資料庫連線
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 連線詳情列表 */}
|
||||||
|
{status && status.connectionDetails.length > 0 && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>連線詳情列表</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
當前所有資料庫連線的詳細信息
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-2 max-h-96 overflow-y-auto">
|
||||||
|
{status.connectionDetails.map((conn, index) => (
|
||||||
|
<div key={conn.ID} className="flex items-center justify-between p-3 border rounded-lg">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Badge variant="outline">#{conn.ID}</Badge>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium">
|
||||||
|
用戶: {conn.USER} | 主機: {conn.HOST}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
資料庫: {conn.DB} | 命令: {conn.COMMAND}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
時間: {conn.TIME}s | 狀態: {conn.STATE}
|
||||||
|
</p>
|
||||||
|
{conn.INFO && (
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
查詢: {conn.INFO.length > 100 ? conn.INFO.substring(0, 100) + '...' : conn.INFO}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 訊息顯示 */}
|
||||||
|
{message && (
|
||||||
|
<Alert>
|
||||||
|
<Database className="h-4 w-4" />
|
||||||
|
<AlertDescription>{message}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<AlertTriangle className="h-4 w-4" />
|
||||||
|
<AlertDescription>{error}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 使用說明 */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>終極清理說明</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-2 text-sm text-muted-foreground">
|
||||||
|
<p>• <strong>終極清理:</strong> 直接殺死資料庫中的所有連線,立即生效</p>
|
||||||
|
<p>• <strong>強制重啟:</strong> 先殺死所有連線,等待系統穩定,然後重新建立連線</p>
|
||||||
|
<p>• <strong>自動更新:</strong> 頁面每10秒自動更新連線狀態</p>
|
||||||
|
<p>• <strong>最後手段:</strong> 這些操作是最後的手段,會立即終止所有連線</p>
|
||||||
|
<p>• 建議先嘗試「終極清理」,如果問題持續再使用「強制重啟」</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
37
app/api/competitions/[id]/route.ts
Normal file
37
app/api/competitions/[id]/route.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
// =====================================================
|
||||||
|
// 獲取競賽詳情 API
|
||||||
|
// =====================================================
|
||||||
|
|
||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { CompetitionService } from '@/lib/services/database-service';
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest, { params }: { params: { id: string } }) {
|
||||||
|
try {
|
||||||
|
const { id } = await params;
|
||||||
|
|
||||||
|
// 獲取競賽詳情
|
||||||
|
const competition = await CompetitionService.getCompetitionById(id);
|
||||||
|
|
||||||
|
if (!competition) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
message: '競賽不存在',
|
||||||
|
error: '找不到指定的競賽'
|
||||||
|
}, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: '競賽詳情獲取成功',
|
||||||
|
data: competition
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('獲取競賽詳情失敗:', error);
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
message: '獲取競賽詳情失敗',
|
||||||
|
error: error instanceof Error ? error.message : '未知錯誤'
|
||||||
|
}, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
122
app/api/connection-monitor/route.ts
Normal file
122
app/api/connection-monitor/route.ts
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
// =====================================================
|
||||||
|
// 連線監控 API
|
||||||
|
// =====================================================
|
||||||
|
|
||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { smartPool } from '@/lib/smart-connection-pool';
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const { searchParams } = new URL(request.url);
|
||||||
|
const action = searchParams.get('action') || 'stats';
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case 'stats':
|
||||||
|
// 獲取連線統計
|
||||||
|
const stats = smartPool.getConnectionStats();
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: '連線統計獲取成功',
|
||||||
|
data: stats
|
||||||
|
});
|
||||||
|
|
||||||
|
case 'cleanup':
|
||||||
|
// 強制清理連線
|
||||||
|
console.log('🧹 執行強制連線清理...');
|
||||||
|
smartPool.forceCleanup();
|
||||||
|
const newStats = smartPool.getConnectionStats();
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: '連線清理完成',
|
||||||
|
data: newStats
|
||||||
|
});
|
||||||
|
|
||||||
|
case 'test':
|
||||||
|
// 測試智能連線
|
||||||
|
try {
|
||||||
|
const testResult = await smartPool.executeQueryOne(
|
||||||
|
'SELECT 1 as test_value',
|
||||||
|
[],
|
||||||
|
{ userId: 'test', sessionId: 'test-session', requestId: 'test-request' }
|
||||||
|
);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: '智能連線測試成功',
|
||||||
|
data: {
|
||||||
|
testResult,
|
||||||
|
stats: smartPool.getConnectionStats()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: '智能連線測試失敗',
|
||||||
|
details: error instanceof Error ? error.message : '未知錯誤'
|
||||||
|
}, { status: 500 });
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: '無效的操作參數',
|
||||||
|
availableActions: ['stats', 'cleanup', 'test']
|
||||||
|
}, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 連線監控 API 錯誤:', error);
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: 'API 請求失敗',
|
||||||
|
details: error instanceof Error ? error.message : '未知錯誤'
|
||||||
|
}, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
const { action, maxIdleTime, maxConnectionAge } = body;
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case 'config':
|
||||||
|
// 更新清理配置
|
||||||
|
smartPool.setCleanupParams(maxIdleTime, maxConnectionAge);
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: '清理配置更新成功',
|
||||||
|
data: {
|
||||||
|
maxIdleTime: maxIdleTime || '未變更',
|
||||||
|
maxConnectionAge: maxConnectionAge || '未變更'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
case 'cleanup':
|
||||||
|
// 強制清理連線
|
||||||
|
console.log('🧹 執行強制連線清理...');
|
||||||
|
smartPool.forceCleanup();
|
||||||
|
const stats = smartPool.getConnectionStats();
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: '連線清理完成',
|
||||||
|
data: stats
|
||||||
|
});
|
||||||
|
|
||||||
|
default:
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: '無效的操作參數',
|
||||||
|
availableActions: ['config', 'cleanup']
|
||||||
|
}, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 連線監控 POST API 錯誤:', error);
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: 'API 請求失敗',
|
||||||
|
details: error instanceof Error ? error.message : '未知錯誤'
|
||||||
|
}, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
59
app/api/debug-ip/route.ts
Normal file
59
app/api/debug-ip/route.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
// =====================================================
|
||||||
|
// IP 調試 API
|
||||||
|
// =====================================================
|
||||||
|
|
||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { smartIPDetector } from '@/lib/smart-ip-detector';
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
// 使用智能 IP 偵測器
|
||||||
|
const ipDetection = smartIPDetector.detectClientIP(request);
|
||||||
|
|
||||||
|
// 收集所有可能的 IP 信息
|
||||||
|
const ipInfo = {
|
||||||
|
// 智能偵測結果
|
||||||
|
smartDetection: ipDetection,
|
||||||
|
// 請求標頭
|
||||||
|
headers: {
|
||||||
|
'x-forwarded-for': request.headers.get('x-forwarded-for'),
|
||||||
|
'x-real-ip': request.headers.get('x-real-ip'),
|
||||||
|
'cf-connecting-ip': request.headers.get('cf-connecting-ip'),
|
||||||
|
'x-client-ip': request.headers.get('x-client-ip'),
|
||||||
|
'x-forwarded': request.headers.get('x-forwarded'),
|
||||||
|
'x-cluster-client-ip': request.headers.get('x-cluster-client-ip'),
|
||||||
|
'x-original-forwarded-for': request.headers.get('x-original-forwarded-for'),
|
||||||
|
'x-remote-addr': request.headers.get('x-remote-addr'),
|
||||||
|
'remote-addr': request.headers.get('remote-addr'),
|
||||||
|
'client-ip': request.headers.get('client-ip'),
|
||||||
|
'user-agent': request.headers.get('user-agent'),
|
||||||
|
'host': request.headers.get('host'),
|
||||||
|
},
|
||||||
|
// NextRequest 的 IP
|
||||||
|
nextRequestIP: request.ip,
|
||||||
|
// 環境變數
|
||||||
|
env: {
|
||||||
|
NODE_ENV: process.env.NODE_ENV,
|
||||||
|
VERCEL: process.env.VERCEL,
|
||||||
|
VERCEL_REGION: process.env.VERCEL_REGION,
|
||||||
|
},
|
||||||
|
// 所有標頭(用於調試)
|
||||||
|
allHeaders: Object.fromEntries(request.headers.entries()),
|
||||||
|
};
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: 'IP 調試信息獲取成功',
|
||||||
|
data: ipInfo,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ IP 調試 API 錯誤:', error);
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: 'IP 調試失敗',
|
||||||
|
details: error instanceof Error ? error.message : '未知錯誤'
|
||||||
|
}, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
104
app/api/emergency-cleanup/route.ts
Normal file
104
app/api/emergency-cleanup/route.ts
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
// =====================================================
|
||||||
|
// 緊急連線清理 API
|
||||||
|
// =====================================================
|
||||||
|
|
||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { emergencyCleanup } from '@/lib/emergency-connection-cleanup';
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
const { action } = body;
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case 'cleanup':
|
||||||
|
// 執行緊急清理
|
||||||
|
console.log('🚨 收到緊急清理請求');
|
||||||
|
await emergencyCleanup.emergencyCleanup();
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: '緊急清理完成',
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
case 'kill-all':
|
||||||
|
// 強制殺死所有連線
|
||||||
|
console.log('💀 收到強制殺死連線請求');
|
||||||
|
await emergencyCleanup.forceKillAllConnections();
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: '強制殺死連線完成',
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
case 'details':
|
||||||
|
// 獲取連線詳情
|
||||||
|
const connections = await emergencyCleanup.getConnectionDetails();
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: '連線詳情獲取成功',
|
||||||
|
data: {
|
||||||
|
connectionCount: connections.length,
|
||||||
|
connections: connections
|
||||||
|
},
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
default:
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: '無效的操作參數',
|
||||||
|
availableActions: ['cleanup', 'kill-all', 'details']
|
||||||
|
}, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 緊急清理 API 錯誤:', error);
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: '緊急清理失敗',
|
||||||
|
details: error instanceof Error ? error.message : '未知錯誤'
|
||||||
|
}, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const { searchParams } = new URL(request.url);
|
||||||
|
const action = searchParams.get('action') || 'details';
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case 'details':
|
||||||
|
// 獲取連線詳情
|
||||||
|
const connections = await emergencyCleanup.getConnectionDetails();
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: '連線詳情獲取成功',
|
||||||
|
data: {
|
||||||
|
connectionCount: connections.length,
|
||||||
|
connections: connections
|
||||||
|
},
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
default:
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: '無效的操作參數',
|
||||||
|
availableActions: ['details']
|
||||||
|
}, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 緊急清理 GET API 錯誤:', error);
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: '獲取連線詳情失敗',
|
||||||
|
details: error instanceof Error ? error.message : '未知錯誤'
|
||||||
|
}, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
177
app/api/ip-cleanup/route.ts
Normal file
177
app/api/ip-cleanup/route.ts
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
// =====================================================
|
||||||
|
// 基於 IP 的連線清理 API
|
||||||
|
// =====================================================
|
||||||
|
|
||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { smartIPPool } from '@/lib/smart-ip-connection-pool';
|
||||||
|
import { smartIPDetector } from '@/lib/smart-ip-detector';
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
const { action, targetIP } = body;
|
||||||
|
// 使用智能 IP 偵測器
|
||||||
|
const ipDetection = smartIPDetector.detectClientIP(request);
|
||||||
|
const clientIP = ipDetection.detectedIP;
|
||||||
|
|
||||||
|
console.log('🎯 智能 IP 偵測結果:', {
|
||||||
|
detectedIP: clientIP,
|
||||||
|
confidence: ipDetection.confidence,
|
||||||
|
source: ipDetection.source,
|
||||||
|
isPublicIP: ipDetection.isPublicIP,
|
||||||
|
allCandidates: ipDetection.allCandidates
|
||||||
|
});
|
||||||
|
|
||||||
|
// 設置客戶端 IP
|
||||||
|
smartIPPool.setClientIP(clientIP);
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case 'cleanup-current':
|
||||||
|
// 清理當前 IP 的連線
|
||||||
|
console.log(`🧹 收到清理當前 IP 連線請求: ${clientIP}`);
|
||||||
|
const cleanupResult = await smartIPPool.cleanupCurrentIPConnections();
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: cleanupResult.success,
|
||||||
|
message: cleanupResult.message,
|
||||||
|
data: {
|
||||||
|
clientIP,
|
||||||
|
killedCount: cleanupResult.killedCount
|
||||||
|
},
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
case 'cleanup-specific':
|
||||||
|
// 清理指定 IP 的連線
|
||||||
|
if (!targetIP) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: '缺少目標 IP 參數'
|
||||||
|
}, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`🧹 收到清理指定 IP 連線請求: ${targetIP}`);
|
||||||
|
const specificCleanupResult = await smartIPPool.cleanupIPConnections(targetIP);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: specificCleanupResult.success,
|
||||||
|
message: specificCleanupResult.message,
|
||||||
|
data: {
|
||||||
|
targetIP,
|
||||||
|
killedCount: specificCleanupResult.killedCount
|
||||||
|
},
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
case 'cleanup-local':
|
||||||
|
// 清理本地追蹤的連線
|
||||||
|
console.log('🧹 收到清理本地連線請求');
|
||||||
|
const localCleanupCount = smartIPPool.cleanupLocalConnections();
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: `已清理 ${localCleanupCount} 個本地追蹤的連線`,
|
||||||
|
data: {
|
||||||
|
clientIP,
|
||||||
|
cleanedCount: localCleanupCount
|
||||||
|
},
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
default:
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: '無效的操作參數',
|
||||||
|
availableActions: ['cleanup-current', 'cleanup-specific', 'cleanup-local']
|
||||||
|
}, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ IP 清理 API 錯誤:', error);
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: 'IP 清理失敗',
|
||||||
|
details: error instanceof Error ? error.message : '未知錯誤'
|
||||||
|
}, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const { searchParams } = new URL(request.url);
|
||||||
|
const action = searchParams.get('action') || 'status';
|
||||||
|
const targetIP = searchParams.get('ip');
|
||||||
|
// 使用智能 IP 偵測器
|
||||||
|
const ipDetection = smartIPDetector.detectClientIP(request);
|
||||||
|
const clientIP = ipDetection.detectedIP;
|
||||||
|
|
||||||
|
// 設置客戶端 IP
|
||||||
|
smartIPPool.setClientIP(clientIP);
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case 'status':
|
||||||
|
// 獲取當前 IP 的連線狀態
|
||||||
|
const currentIPStatus = await smartIPPool.getCurrentIPConnections();
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: '連線狀態獲取成功',
|
||||||
|
data: {
|
||||||
|
detectedIP: clientIP,
|
||||||
|
...currentIPStatus
|
||||||
|
},
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
case 'status-specific':
|
||||||
|
// 獲取指定 IP 的連線狀態
|
||||||
|
if (!targetIP) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: '缺少目標 IP 參數'
|
||||||
|
}, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const specificIPStatus = await smartIPPool.getIPConnections(targetIP);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: '指定 IP 連線狀態獲取成功',
|
||||||
|
data: {
|
||||||
|
targetIP,
|
||||||
|
...specificIPStatus
|
||||||
|
},
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
case 'local-stats':
|
||||||
|
// 獲取本地連線統計
|
||||||
|
const localStats = smartIPPool.getLocalConnectionStats();
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: '本地連線統計獲取成功',
|
||||||
|
data: {
|
||||||
|
detectedIP: clientIP,
|
||||||
|
...localStats
|
||||||
|
},
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
default:
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: '無效的操作參數',
|
||||||
|
availableActions: ['status', 'status-specific', 'local-stats']
|
||||||
|
}, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ IP 清理 GET API 錯誤:', error);
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: '獲取連線狀態失敗',
|
||||||
|
details: error instanceof Error ? error.message : '未知錯誤'
|
||||||
|
}, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
78
app/api/set-client-ip/route.ts
Normal file
78
app/api/set-client-ip/route.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
// =====================================================
|
||||||
|
// 手動設置客戶端 IP API
|
||||||
|
// =====================================================
|
||||||
|
|
||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { smartIPPool } from '@/lib/smart-ip-connection-pool';
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
const { clientIP } = body;
|
||||||
|
|
||||||
|
if (!clientIP) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: '缺少客戶端 IP 參數'
|
||||||
|
}, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🔧 手動設置客戶端 IP:', clientIP);
|
||||||
|
|
||||||
|
// 設置客戶端 IP
|
||||||
|
smartIPPool.setClientIP(clientIP);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: `客戶端 IP 已設置為: ${clientIP}`,
|
||||||
|
data: {
|
||||||
|
clientIP,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 設置客戶端 IP 失敗:', error);
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: '設置客戶端 IP 失敗',
|
||||||
|
details: error instanceof Error ? error.message : '未知錯誤'
|
||||||
|
}, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const { searchParams } = new URL(request.url);
|
||||||
|
const clientIP = searchParams.get('ip');
|
||||||
|
|
||||||
|
if (!clientIP) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: '缺少 IP 參數'
|
||||||
|
}, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🔧 通過 GET 設置客戶端 IP:', clientIP);
|
||||||
|
|
||||||
|
// 設置客戶端 IP
|
||||||
|
smartIPPool.setClientIP(clientIP);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: `客戶端 IP 已設置為: ${clientIP}`,
|
||||||
|
data: {
|
||||||
|
clientIP,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 設置客戶端 IP 失敗:', error);
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: '設置客戶端 IP 失敗',
|
||||||
|
details: error instanceof Error ? error.message : '未知錯誤'
|
||||||
|
}, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
100
app/api/test-shutdown/route.ts
Normal file
100
app/api/test-shutdown/route.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
// =====================================================
|
||||||
|
// 測試資料庫關閉機制 API
|
||||||
|
// =====================================================
|
||||||
|
|
||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { dbShutdownManager } from '@/lib/database-shutdown-manager';
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const { searchParams } = new URL(request.url);
|
||||||
|
const action = searchParams.get('action') || 'status';
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case 'status':
|
||||||
|
// 獲取關閉狀態
|
||||||
|
const status = dbShutdownManager.getShutdownStatus();
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: '關閉狀態查詢成功',
|
||||||
|
data: status
|
||||||
|
});
|
||||||
|
|
||||||
|
case 'test':
|
||||||
|
// 測試關閉機制(不會真的關閉)
|
||||||
|
console.log('🧪 測試關閉機制...');
|
||||||
|
const testStatus = dbShutdownManager.getShutdownStatus();
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: '關閉機制測試成功',
|
||||||
|
data: {
|
||||||
|
...testStatus,
|
||||||
|
testTime: new Date().toISOString()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
case 'force':
|
||||||
|
// 強制關閉(僅用於測試)
|
||||||
|
console.log('🚨 執行強制關閉測試...');
|
||||||
|
dbShutdownManager.forceShutdown();
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: '強制關閉測試完成',
|
||||||
|
data: {
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
default:
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: '無效的操作參數',
|
||||||
|
availableActions: ['status', 'test', 'force']
|
||||||
|
}, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 測試關閉機制時發生錯誤:', error);
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: '測試失敗',
|
||||||
|
details: error instanceof Error ? error.message : '未知錯誤'
|
||||||
|
}, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
const { action } = body;
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case 'graceful':
|
||||||
|
// 優雅關閉(僅用於測試)
|
||||||
|
console.log('🔄 執行優雅關閉測試...');
|
||||||
|
await dbShutdownManager.gracefulShutdown();
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: '優雅關閉測試完成',
|
||||||
|
data: {
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
default:
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: '無效的操作參數',
|
||||||
|
availableActions: ['graceful']
|
||||||
|
}, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 執行關閉測試時發生錯誤:', error);
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: '關閉測試失敗',
|
||||||
|
details: error instanceof Error ? error.message : '未知錯誤'
|
||||||
|
}, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
96
app/api/ultimate-kill/route.ts
Normal file
96
app/api/ultimate-kill/route.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
// =====================================================
|
||||||
|
// 終極連線清理 API
|
||||||
|
// =====================================================
|
||||||
|
|
||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { ultimateKiller } from '@/lib/ultimate-connection-killer';
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
const { action } = body;
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case 'kill-all':
|
||||||
|
// 終極清理 - 殺死所有連線
|
||||||
|
console.log('💀 收到終極清理請求');
|
||||||
|
const killResult = await ultimateKiller.ultimateKill();
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: killResult.success,
|
||||||
|
message: killResult.message || '終極清理完成',
|
||||||
|
data: killResult,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
case 'restart':
|
||||||
|
// 強制重啟資料庫連線
|
||||||
|
console.log('🔄 收到強制重啟請求');
|
||||||
|
const restartResult = await ultimateKiller.forceRestart();
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: restartResult.success,
|
||||||
|
message: restartResult.message || '強制重啟完成',
|
||||||
|
data: restartResult,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
default:
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: '無效的操作參數',
|
||||||
|
availableActions: ['kill-all', 'restart']
|
||||||
|
}, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 終極清理 API 錯誤:', error);
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: '終極清理失敗',
|
||||||
|
details: error instanceof Error ? error.message : '未知錯誤'
|
||||||
|
}, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const { searchParams } = new URL(request.url);
|
||||||
|
const action = searchParams.get('action') || 'status';
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case 'status':
|
||||||
|
// 檢查連線狀態
|
||||||
|
const status = await ultimateKiller.checkStatus();
|
||||||
|
|
||||||
|
if (status) {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: '連線狀態獲取成功',
|
||||||
|
data: status,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: '無法獲取連線狀態'
|
||||||
|
}, { status: 500 });
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: '無效的操作參數',
|
||||||
|
availableActions: ['status']
|
||||||
|
}, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 終極清理 GET API 錯誤:', error);
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: '獲取連線狀態失敗',
|
||||||
|
details: error instanceof Error ? error.message : '未知錯誤'
|
||||||
|
}, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
297
app/auto-ip-test/page.tsx
Normal file
297
app/auto-ip-test/page.tsx
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
|
import { Loader2, Database, CheckCircle, AlertTriangle, RefreshCw, Trash2 } from 'lucide-react';
|
||||||
|
|
||||||
|
interface IPDetectionResult {
|
||||||
|
detectedIP: string;
|
||||||
|
confidence: 'high' | 'medium' | 'low';
|
||||||
|
source: string;
|
||||||
|
isPublicIP: boolean;
|
||||||
|
allCandidates: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ConnectionStatus {
|
||||||
|
ip: string;
|
||||||
|
connectionCount: number;
|
||||||
|
connections: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AutoIPTestPage() {
|
||||||
|
const [ipDetection, setIpDetection] = useState<IPDetectionResult | null>(null);
|
||||||
|
const [connectionStatus, setConnectionStatus] = useState<ConnectionStatus | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [message, setMessage] = useState<string>('');
|
||||||
|
const [error, setError] = useState<string>('');
|
||||||
|
|
||||||
|
// 自動偵測 IP
|
||||||
|
const detectIP = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetch('/api/debug-ip');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success && data.data.smartDetection) {
|
||||||
|
setIpDetection(data.data.smartDetection);
|
||||||
|
setMessage(`IP 偵測成功: ${data.data.smartDetection.detectedIP}`);
|
||||||
|
setError('');
|
||||||
|
} else {
|
||||||
|
setError('IP 偵測失敗');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError('IP 偵測錯誤: ' + (err instanceof Error ? err.message : '未知錯誤'));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 獲取連線狀態
|
||||||
|
const getConnectionStatus = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetch('/api/ip-cleanup?action=status');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
setConnectionStatus(data.data);
|
||||||
|
setMessage(`連線狀態獲取成功: ${data.data.connectionCount} 個連線`);
|
||||||
|
setError('');
|
||||||
|
} else {
|
||||||
|
setError('獲取連線狀態失敗');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError('獲取連線狀態錯誤: ' + (err instanceof Error ? err.message : '未知錯誤'));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 清理當前 IP 連線
|
||||||
|
const cleanupConnections = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetch('/api/ip-cleanup', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ action: 'cleanup-current' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
setMessage(`清理成功: ${data.message}`);
|
||||||
|
setError('');
|
||||||
|
// 重新獲取連線狀態
|
||||||
|
await getConnectionStatus();
|
||||||
|
} else {
|
||||||
|
setError(data.error || '清理失敗');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError('清理錯誤: ' + (err instanceof Error ? err.message : '未知錯誤'));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 組件載入時自動偵測
|
||||||
|
useEffect(() => {
|
||||||
|
detectIP();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-6 space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold">自動 IP 偵測測試</h1>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
測試智能 IP 偵測和自動連線清理功能
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
onClick={detectIP}
|
||||||
|
disabled={loading}
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <RefreshCw className="h-4 w-4" />}
|
||||||
|
偵測 IP
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={getConnectionStatus}
|
||||||
|
disabled={loading}
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Database className="h-4 w-4" />}
|
||||||
|
檢查連線
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* IP 偵測結果 */}
|
||||||
|
{ipDetection && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<CheckCircle className="h-5 w-5" />
|
||||||
|
智能 IP 偵測結果
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
系統自動偵測到的客戶端 IP 地址
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Badge
|
||||||
|
variant={ipDetection.isPublicIP ? "default" : "secondary"}
|
||||||
|
className="text-lg px-3 py-1"
|
||||||
|
>
|
||||||
|
{ipDetection.detectedIP}
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
variant={
|
||||||
|
ipDetection.confidence === 'high' ? "default" :
|
||||||
|
ipDetection.confidence === 'medium' ? "secondary" : "destructive"
|
||||||
|
}
|
||||||
|
className="text-lg px-3 py-1"
|
||||||
|
>
|
||||||
|
可信度: {ipDetection.confidence}
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="outline" className="text-lg px-3 py-1">
|
||||||
|
來源: {ipDetection.source}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{ipDetection.allCandidates.length > 1 && (
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium mb-2">所有候選 IP:</p>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{ipDetection.allCandidates.map((ip, index) => (
|
||||||
|
<Badge
|
||||||
|
key={index}
|
||||||
|
variant={ip === ipDetection.detectedIP ? "default" : "outline"}
|
||||||
|
className="text-sm"
|
||||||
|
>
|
||||||
|
{ip}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 連線狀態 */}
|
||||||
|
{connectionStatus && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Database className="h-5 w-5" />
|
||||||
|
連線狀態
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
當前 IP 的資料庫連線狀態
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Badge variant="default" className="text-lg px-3 py-1">
|
||||||
|
IP: {connectionStatus.ip}
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
variant={connectionStatus.connectionCount > 0 ? "destructive" : "default"}
|
||||||
|
className="text-lg px-3 py-1"
|
||||||
|
>
|
||||||
|
連線數: {connectionStatus.connectionCount}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{connectionStatus.connections.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium mb-2">連線詳情:</p>
|
||||||
|
<div className="space-y-2 max-h-48 overflow-y-auto">
|
||||||
|
{connectionStatus.connections.map((conn, index) => (
|
||||||
|
<div key={index} className="flex items-center justify-between p-2 border rounded text-sm">
|
||||||
|
<span>ID: {conn.ID}</span>
|
||||||
|
<span>HOST: {conn.HOST}</span>
|
||||||
|
<span>時間: {conn.TIME}s</span>
|
||||||
|
<span>狀態: {conn.STATE || 'Sleep'}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 操作按鈕 */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>操作</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
測試自動 IP 偵測和連線清理功能
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<Button
|
||||||
|
onClick={cleanupConnections}
|
||||||
|
disabled={loading || !ipDetection}
|
||||||
|
variant="destructive"
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Trash2 className="h-4 w-4" />}
|
||||||
|
清理我的連線
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={getConnectionStatus}
|
||||||
|
disabled={loading}
|
||||||
|
variant="outline"
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <RefreshCw className="h-4 w-4" />}
|
||||||
|
重新檢查
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 使用說明 */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>使用說明</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-2 text-sm text-muted-foreground">
|
||||||
|
<p>• <strong>自動偵測:</strong> 頁面載入時會自動偵測你的 IP 地址</p>
|
||||||
|
<p>• <strong>智能算法:</strong> 使用多種方法偵測最準確的 IP 地址</p>
|
||||||
|
<p>• <strong>連線檢查:</strong> 檢查你的 IP 在資料庫中的連線狀態</p>
|
||||||
|
<p>• <strong>自動清理:</strong> 點擊「清理我的連線」來清理你的 IP 連線</p>
|
||||||
|
<p>• <strong>關閉頁面:</strong> 關閉此頁面時會自動觸發連線清理</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 訊息顯示 */}
|
||||||
|
{message && (
|
||||||
|
<Alert>
|
||||||
|
<CheckCircle className="h-4 w-4" />
|
||||||
|
<AlertDescription>{message}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<AlertTriangle className="h-4 w-4" />
|
||||||
|
<AlertDescription>{error}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
325
app/debug-ip/page.tsx
Normal file
325
app/debug-ip/page.tsx
Normal file
@@ -0,0 +1,325 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
|
import { Loader2, Database, Eye, RefreshCw } from 'lucide-react';
|
||||||
|
|
||||||
|
interface IPInfo {
|
||||||
|
smartDetection?: {
|
||||||
|
detectedIP: string;
|
||||||
|
confidence: 'high' | 'medium' | 'low';
|
||||||
|
source: string;
|
||||||
|
allCandidates: string[];
|
||||||
|
isPublicIP: boolean;
|
||||||
|
};
|
||||||
|
headers: Record<string, string | null>;
|
||||||
|
nextRequestIP: string | undefined;
|
||||||
|
env: Record<string, string | undefined>;
|
||||||
|
allHeaders: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DebugIPPage() {
|
||||||
|
const [ipInfo, setIpInfo] = useState<IPInfo | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [message, setMessage] = useState<string>('');
|
||||||
|
const [error, setError] = useState<string>('');
|
||||||
|
const [recommendedIP, setRecommendedIP] = useState<string>('');
|
||||||
|
|
||||||
|
// 獲取 IP 調試信息
|
||||||
|
const fetchIPInfo = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetch('/api/debug-ip');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
setIpInfo(data.data);
|
||||||
|
|
||||||
|
// 提取智能偵測結果
|
||||||
|
if (data.data.smartDetection) {
|
||||||
|
const smartDetection = data.data.smartDetection;
|
||||||
|
setRecommendedIP(smartDetection.detectedIP);
|
||||||
|
setMessage(`智能偵測結果: ${smartDetection.detectedIP} (可信度: ${smartDetection.confidence}, 來源: ${smartDetection.source})`);
|
||||||
|
} else {
|
||||||
|
setMessage('IP 調試信息更新成功');
|
||||||
|
}
|
||||||
|
setError('');
|
||||||
|
} else {
|
||||||
|
setError(data.error || '獲取 IP 調試信息失敗');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError('網路錯誤: ' + (err instanceof Error ? err.message : '未知錯誤'));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 組件載入時獲取 IP 信息
|
||||||
|
useEffect(() => {
|
||||||
|
fetchIPInfo();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 獲取推薦的 IP
|
||||||
|
const getRecommendedIP = () => {
|
||||||
|
if (!ipInfo) return 'unknown';
|
||||||
|
|
||||||
|
// 優先使用智能偵測結果
|
||||||
|
if (ipInfo.smartDetection) {
|
||||||
|
return ipInfo.smartDetection.detectedIP;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 回退到舊的邏輯
|
||||||
|
const { headers, nextRequestIP } = ipInfo;
|
||||||
|
|
||||||
|
// 優先順序檢查
|
||||||
|
if (headers['cf-connecting-ip']) return headers['cf-connecting-ip'];
|
||||||
|
if (headers['x-real-ip']) return headers['x-real-ip'];
|
||||||
|
if (headers['x-client-ip']) return headers['x-client-ip'];
|
||||||
|
if (headers['x-cluster-client-ip']) return headers['x-cluster-client-ip'];
|
||||||
|
if (headers['x-forwarded']) return headers['x-forwarded'];
|
||||||
|
if (headers['x-forwarded-for']) {
|
||||||
|
const ips = headers['x-forwarded-for']!.split(',').map(ip => ip.trim());
|
||||||
|
const publicIPs = ips.filter(ip =>
|
||||||
|
ip &&
|
||||||
|
ip !== '127.0.0.1' &&
|
||||||
|
ip !== '::1' &&
|
||||||
|
!ip.startsWith('192.168.') &&
|
||||||
|
!ip.startsWith('10.') &&
|
||||||
|
!ip.startsWith('172.')
|
||||||
|
);
|
||||||
|
if (publicIPs.length > 0) return publicIPs[0];
|
||||||
|
return ips[0];
|
||||||
|
}
|
||||||
|
if (nextRequestIP && nextRequestIP !== '::1' && nextRequestIP !== '127.0.0.1') {
|
||||||
|
return nextRequestIP;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'unknown';
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-6 space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold">IP 調試工具</h1>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
調試和檢查客戶端 IP 地址獲取
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={fetchIPInfo}
|
||||||
|
disabled={loading}
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <RefreshCw className="h-4 w-4" />}
|
||||||
|
重新整理
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 智能偵測結果 */}
|
||||||
|
{ipInfo?.smartDetection && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Eye className="h-5 w-5" />
|
||||||
|
智能 IP 偵測結果
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
使用智能算法偵測的客戶端 IP 地址
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Badge
|
||||||
|
variant={ipInfo.smartDetection.isPublicIP ? "default" : "secondary"}
|
||||||
|
className="text-lg px-3 py-1"
|
||||||
|
>
|
||||||
|
{ipInfo.smartDetection.detectedIP}
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
variant={
|
||||||
|
ipInfo.smartDetection.confidence === 'high' ? "default" :
|
||||||
|
ipInfo.smartDetection.confidence === 'medium' ? "secondary" : "destructive"
|
||||||
|
}
|
||||||
|
className="text-lg px-3 py-1"
|
||||||
|
>
|
||||||
|
可信度: {ipInfo.smartDetection.confidence}
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="outline" className="text-lg px-3 py-1">
|
||||||
|
來源: {ipInfo.smartDetection.source}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{ipInfo.smartDetection.allCandidates.length > 1 && (
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium mb-2">所有候選 IP:</p>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{ipInfo.smartDetection.allCandidates.map((ip, index) => (
|
||||||
|
<Badge
|
||||||
|
key={index}
|
||||||
|
variant={ip === ipInfo.smartDetection.detectedIP ? "default" : "outline"}
|
||||||
|
className="text-sm"
|
||||||
|
>
|
||||||
|
{ip}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 推薦的 IP (回退顯示) */}
|
||||||
|
{ipInfo && !ipInfo.smartDetection && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Eye className="h-5 w-5" />
|
||||||
|
推薦的 IP 地址
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
系統推薦使用的客戶端 IP 地址
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Badge variant="default" className="text-lg px-3 py-1">
|
||||||
|
{getRecommendedIP()}
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
variant={getRecommendedIP() === 'unknown' ? "destructive" : "default"}
|
||||||
|
className="text-lg px-3 py-1"
|
||||||
|
>
|
||||||
|
狀態: {getRecommendedIP() === 'unknown' ? "無法識別" : "已識別"}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 請求標頭 */}
|
||||||
|
{ipInfo && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>請求標頭</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
從 HTTP 請求標頭中獲取的 IP 相關信息
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{Object.entries(ipInfo.headers).map(([key, value]) => (
|
||||||
|
<div key={key} className="flex items-center justify-between p-2 border rounded">
|
||||||
|
<span className="font-medium text-sm">{key}:</span>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{value || 'null'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* NextRequest IP */}
|
||||||
|
{ipInfo && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>NextRequest IP</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Next.js 框架提供的 IP 地址
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Badge variant="outline" className="text-lg px-3 py-1">
|
||||||
|
{ipInfo.nextRequestIP || 'undefined'}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 環境信息 */}
|
||||||
|
{ipInfo && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>環境信息</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
部署環境相關信息
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{Object.entries(ipInfo.env).map(([key, value]) => (
|
||||||
|
<div key={key} className="flex items-center justify-between p-2 border rounded">
|
||||||
|
<span className="font-medium text-sm">{key}:</span>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{value || 'undefined'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 所有標頭(用於調試) */}
|
||||||
|
{ipInfo && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>所有請求標頭</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
完整的 HTTP 請求標頭信息
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-2 max-h-96 overflow-y-auto">
|
||||||
|
{Object.entries(ipInfo.allHeaders).map(([key, value]) => (
|
||||||
|
<div key={key} className="flex items-center justify-between p-2 border rounded text-xs">
|
||||||
|
<span className="font-medium">{key}:</span>
|
||||||
|
<span className="text-muted-foreground break-all">
|
||||||
|
{value}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 訊息顯示 */}
|
||||||
|
{message && (
|
||||||
|
<Alert>
|
||||||
|
<Database className="h-4 w-4" />
|
||||||
|
<AlertDescription>{message}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<Eye className="h-4 w-4" />
|
||||||
|
<AlertDescription>{error}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 使用說明 */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>使用說明</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-2 text-sm text-muted-foreground">
|
||||||
|
<p>• <strong>推薦的 IP:</strong> 系統根據優先順序推薦使用的 IP 地址</p>
|
||||||
|
<p>• <strong>請求標頭:</strong> 檢查各種代理和負載均衡器設置的 IP 標頭</p>
|
||||||
|
<p>• <strong>NextRequest IP:</strong> Next.js 框架直接提供的 IP 地址</p>
|
||||||
|
<p>• <strong>環境信息:</strong> 部署環境相關的配置信息</p>
|
||||||
|
<p>• 如果推薦的 IP 是 "unknown",請檢查代理設置或網路配置</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@@ -5,6 +5,8 @@ import { AuthProvider } from "@/contexts/auth-context"
|
|||||||
import { CompetitionProvider } from "@/contexts/competition-context"
|
import { CompetitionProvider } from "@/contexts/competition-context"
|
||||||
import { Toaster } from "@/components/ui/toaster"
|
import { Toaster } from "@/components/ui/toaster"
|
||||||
import { ChatBot } from "@/components/chat-bot"
|
import { ChatBot } from "@/components/chat-bot"
|
||||||
|
import { ClientConnectionCleanup } from "@/components/client-connection-cleanup"
|
||||||
|
import "@/lib/app-initializer" // 自動初始化應用程式
|
||||||
|
|
||||||
const inter = Inter({ subsets: ["latin"] })
|
const inter = Inter({ subsets: ["latin"] })
|
||||||
|
|
||||||
@@ -28,6 +30,7 @@ export default function RootLayout({
|
|||||||
{children}
|
{children}
|
||||||
<Toaster />
|
<Toaster />
|
||||||
<ChatBot />
|
<ChatBot />
|
||||||
|
<ClientConnectionCleanup />
|
||||||
</CompetitionProvider>
|
</CompetitionProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</body>
|
</body>
|
||||||
|
174
app/set-ip/page.tsx
Normal file
174
app/set-ip/page.tsx
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
|
import { Loader2, Database, CheckCircle, AlertTriangle } from 'lucide-react';
|
||||||
|
|
||||||
|
export default function SetIPPage() {
|
||||||
|
const [clientIP, setClientIP] = useState('61-227-253-171');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [message, setMessage] = useState<string>('');
|
||||||
|
const [error, setError] = useState<string>('');
|
||||||
|
|
||||||
|
// 設置客戶端 IP
|
||||||
|
const setClientIP = async () => {
|
||||||
|
if (!clientIP.trim()) {
|
||||||
|
setError('請輸入客戶端 IP 地址');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetch('/api/set-client-ip', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ clientIP: clientIP.trim() }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
setMessage(data.message || '客戶端 IP 設置成功');
|
||||||
|
setError('');
|
||||||
|
} else {
|
||||||
|
setError(data.error || '設置客戶端 IP 失敗');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError('設置錯誤: ' + (err instanceof Error ? err.message : '未知錯誤'));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 快速設置你的 IP
|
||||||
|
const setYourIP = () => {
|
||||||
|
setClientIP('61-227-253-171');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 測試清理功能
|
||||||
|
const testCleanup = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetch('/api/ip-cleanup', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ action: 'cleanup-current' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
setMessage(`清理完成: ${data.message}`);
|
||||||
|
setError('');
|
||||||
|
} else {
|
||||||
|
setError(data.error || '清理失敗');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError('清理錯誤: ' + (err instanceof Error ? err.message : '未知錯誤'));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-6 space-y-6">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold">設置客戶端 IP</h1>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
手動設置你的真實 IP 地址,用於連線清理
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* IP 設置 */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Database className="h-5 w-5" />
|
||||||
|
設置客戶端 IP
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
輸入你的真實 IP 地址,系統將使用此 IP 來清理連線
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="clientIP">客戶端 IP 地址</Label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
id="clientIP"
|
||||||
|
value={clientIP}
|
||||||
|
onChange={(e) => setClientIP(e.target.value)}
|
||||||
|
placeholder="例如: 61-227-253-171"
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={setYourIP}
|
||||||
|
disabled={loading}
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
使用你的 IP
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
onClick={setClientIP}
|
||||||
|
disabled={loading || !clientIP.trim()}
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <CheckCircle className="h-4 w-4" />}
|
||||||
|
設置 IP
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={testCleanup}
|
||||||
|
disabled={loading}
|
||||||
|
variant="destructive"
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <AlertTriangle className="h-4 w-4" />}
|
||||||
|
測試清理
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 使用說明 */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>使用說明</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-2 text-sm text-muted-foreground">
|
||||||
|
<p>• <strong>你的 IP:</strong> 61-227-253-171(從資料庫連線列表中獲取)</p>
|
||||||
|
<p>• <strong>設置 IP:</strong> 點擊「設置 IP」按鈕來設置你的客戶端 IP</p>
|
||||||
|
<p>• <strong>測試清理:</strong> 點擊「測試清理」按鈕來清理你的 IP 連線</p>
|
||||||
|
<p>• <strong>自動清理:</strong> 設置後,關閉頁面時會自動清理你的 IP 連線</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 訊息顯示 */}
|
||||||
|
{message && (
|
||||||
|
<Alert>
|
||||||
|
<CheckCircle className="h-4 w-4" />
|
||||||
|
<AlertDescription>{message}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<AlertTriangle className="h-4 w-4" />
|
||||||
|
<AlertDescription>{error}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
226
app/test-ip-cleanup/page.tsx
Normal file
226
app/test-ip-cleanup/page.tsx
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
|
import { Loader2, Database, Trash2, Eye, Zap } from 'lucide-react';
|
||||||
|
import { clientCleanup } from '@/lib/client-connection-cleanup';
|
||||||
|
|
||||||
|
export default function TestIPCleanupPage() {
|
||||||
|
const [status, setStatus] = useState<any>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [message, setMessage] = useState<string>('');
|
||||||
|
const [error, setError] = useState<string>('');
|
||||||
|
|
||||||
|
// 獲取連線狀態
|
||||||
|
const fetchStatus = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const status = await clientCleanup.getConnectionStatus();
|
||||||
|
setStatus(status);
|
||||||
|
setMessage('連線狀態更新成功');
|
||||||
|
setError('');
|
||||||
|
} catch (err) {
|
||||||
|
setError('獲取狀態失敗: ' + (err instanceof Error ? err.message : '未知錯誤'));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 手動清理
|
||||||
|
const manualCleanup = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const success = await clientCleanup.manualCleanup();
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
setMessage('手動清理完成');
|
||||||
|
setError('');
|
||||||
|
await fetchStatus(); // 重新獲取狀態
|
||||||
|
} else {
|
||||||
|
setError('手動清理失敗');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError('手動清理錯誤: ' + (err instanceof Error ? err.message : '未知錯誤'));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 模擬關閉頁面
|
||||||
|
const simulatePageClose = () => {
|
||||||
|
if (confirm('確定要模擬關閉頁面嗎?這會觸發自動清理機制。')) {
|
||||||
|
// 觸發 beforeunload 事件
|
||||||
|
window.dispatchEvent(new Event('beforeunload'));
|
||||||
|
|
||||||
|
// 等待一下再觸發 unload 事件
|
||||||
|
setTimeout(() => {
|
||||||
|
window.dispatchEvent(new Event('unload'));
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 組件載入時獲取狀態
|
||||||
|
useEffect(() => {
|
||||||
|
fetchStatus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-6 space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold">IP 連線清理測試</h1>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
測試基於 IP 的智能連線清理功能
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={fetchStatus}
|
||||||
|
disabled={loading}
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Eye className="h-4 w-4" />}
|
||||||
|
重新整理
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 客戶端信息 */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Database className="h-5 w-5" />
|
||||||
|
客戶端信息
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p><strong>客戶端 ID:</strong> {clientCleanup.getClientId()}</p>
|
||||||
|
<p><strong>用戶代理:</strong> {navigator.userAgent}</p>
|
||||||
|
<p><strong>當前時間:</strong> {new Date().toLocaleString()}</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 連線狀態 */}
|
||||||
|
{status && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Eye className="h-5 w-5" />
|
||||||
|
連線狀態
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
IP: {status.ip} | 連線數: {status.connectionCount}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Badge variant="outline" className="text-lg px-3 py-1">
|
||||||
|
連線數: {status.connectionCount}
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
variant={status.connectionCount > 5 ? "destructive" : "default"}
|
||||||
|
className="text-lg px-3 py-1"
|
||||||
|
>
|
||||||
|
狀態: {status.connectionCount > 5 ? "異常" : "正常"}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{status.connections && status.connections.length > 0 && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h4 className="font-medium">連線詳情:</h4>
|
||||||
|
{status.connections.slice(0, 5).map((conn: any, index: number) => (
|
||||||
|
<div key={index} className="p-2 border rounded text-sm">
|
||||||
|
<p>ID: {conn.ID} | HOST: {conn.HOST} | 時間: {conn.TIME}s</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{status.connections.length > 5 && (
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
... 還有 {status.connections.length - 5} 個連線
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 測試操作 */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Zap className="h-5 w-5" />
|
||||||
|
測試操作
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
測試各種連線清理功能
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<Button
|
||||||
|
onClick={manualCleanup}
|
||||||
|
disabled={loading}
|
||||||
|
variant="outline"
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{loading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Trash2 className="h-4 w-4" />}
|
||||||
|
手動清理
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={simulatePageClose}
|
||||||
|
disabled={loading}
|
||||||
|
variant="destructive"
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<Zap className="h-4 w-4" />
|
||||||
|
模擬關閉頁面
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={() => window.location.reload()}
|
||||||
|
disabled={loading}
|
||||||
|
variant="secondary"
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<Database className="h-4 w-4" />
|
||||||
|
重新載入頁面
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 自動清理說明 */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>自動清理機制</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-2 text-sm text-muted-foreground">
|
||||||
|
<p>• <strong>頁面關閉前:</strong> 自動清理當前 IP 的所有連線</p>
|
||||||
|
<p>• <strong>頁面隱藏時:</strong> 當切換到其他標籤頁時清理連線</p>
|
||||||
|
<p>• <strong>定期清理:</strong> 每5分鐘檢查並清理多餘連線</p>
|
||||||
|
<p>• <strong>手動清理:</strong> 可以隨時手動觸發清理</p>
|
||||||
|
<p>• <strong>智能識別:</strong> 只清理當前 IP 的連線,不影響其他用戶</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 訊息顯示 */}
|
||||||
|
{message && (
|
||||||
|
<Alert>
|
||||||
|
<Database className="h-4 w-4" />
|
||||||
|
<AlertDescription>{message}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
<AlertDescription>{error}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
28
components/client-connection-cleanup.tsx
Normal file
28
components/client-connection-cleanup.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { clientCleanup } from '@/lib/client-connection-cleanup';
|
||||||
|
|
||||||
|
// 客戶端連線清理組件
|
||||||
|
export function ClientConnectionCleanup() {
|
||||||
|
useEffect(() => {
|
||||||
|
// 確保在瀏覽器環境中執行
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
|
// 初始化客戶端清理
|
||||||
|
console.log('🖥️ 客戶端連線清理組件已載入');
|
||||||
|
|
||||||
|
// 可以在這裡添加額外的初始化邏輯
|
||||||
|
const clientId = clientCleanup.getClientId();
|
||||||
|
console.log('🆔 客戶端 ID:', clientId);
|
||||||
|
|
||||||
|
// 清理函數(組件卸載時)
|
||||||
|
return () => {
|
||||||
|
console.log('🔄 客戶端連線清理組件卸載');
|
||||||
|
// 可以在這裡添加清理邏輯
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 這個組件不渲染任何內容
|
||||||
|
return null;
|
||||||
|
}
|
39
lib/app-initializer.ts
Normal file
39
lib/app-initializer.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
// =====================================================
|
||||||
|
// 應用程式初始化器
|
||||||
|
// =====================================================
|
||||||
|
|
||||||
|
import { dbShutdownManager } from './database-shutdown-manager';
|
||||||
|
import { startConnectionMonitoring } from './database-middleware';
|
||||||
|
import { smartPool } from './smart-connection-pool';
|
||||||
|
|
||||||
|
// 應用程式初始化
|
||||||
|
export function initializeApp() {
|
||||||
|
console.log('🚀 正在初始化應用程式...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 初始化資料庫關閉管理器
|
||||||
|
console.log('📋 初始化資料庫關閉管理器...');
|
||||||
|
const shutdownStatus = dbShutdownManager.getShutdownStatus();
|
||||||
|
console.log('✅ 資料庫關閉管理器已初始化:', shutdownStatus);
|
||||||
|
|
||||||
|
// 啟動資料庫連線監控
|
||||||
|
console.log('📊 啟動資料庫連線監控...');
|
||||||
|
startConnectionMonitoring();
|
||||||
|
|
||||||
|
// 初始化智能連線池
|
||||||
|
console.log('🧠 初始化智能連線池...');
|
||||||
|
const poolStats = smartPool.getConnectionStats();
|
||||||
|
console.log('✅ 智能連線池已初始化:', poolStats);
|
||||||
|
|
||||||
|
console.log('✅ 應用程式初始化完成');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 應用程式初始化失敗:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在模組載入時自動初始化
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
// 只在伺服器端執行
|
||||||
|
initializeApp();
|
||||||
|
}
|
231
lib/client-connection-cleanup.ts
Normal file
231
lib/client-connection-cleanup.ts
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
// =====================================================
|
||||||
|
// 客戶端連線清理腳本
|
||||||
|
// =====================================================
|
||||||
|
|
||||||
|
export class ClientConnectionCleanup {
|
||||||
|
private static instance: ClientConnectionCleanup;
|
||||||
|
private isCleaning = false;
|
||||||
|
private clientId: string;
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
this.clientId = this.generateClientId();
|
||||||
|
this.setupEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getInstance(): ClientConnectionCleanup {
|
||||||
|
if (!ClientConnectionCleanup.instance) {
|
||||||
|
ClientConnectionCleanup.instance = new ClientConnectionCleanup();
|
||||||
|
}
|
||||||
|
return ClientConnectionCleanup.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成客戶端唯一標識符
|
||||||
|
private generateClientId(): string {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const random = Math.random().toString(36).substring(2);
|
||||||
|
return `client_${timestamp}_${random}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 設置事件監聽器
|
||||||
|
private setupEventListeners() {
|
||||||
|
// 確保在瀏覽器環境中執行
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
console.log('🖥️ 客戶端連線清理跳過(服務端環境)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 頁面關閉前清理
|
||||||
|
window.addEventListener('beforeunload', () => {
|
||||||
|
this.cleanupOnUnload();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 頁面隱藏時清理
|
||||||
|
document.addEventListener('visibilitychange', () => {
|
||||||
|
if (document.hidden) {
|
||||||
|
this.cleanupOnHidden();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 頁面卸載時清理
|
||||||
|
window.addEventListener('unload', () => {
|
||||||
|
this.cleanupOnUnload();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 定期清理(每5分鐘)
|
||||||
|
setInterval(() => {
|
||||||
|
this.periodicCleanup();
|
||||||
|
}, 5 * 60 * 1000);
|
||||||
|
|
||||||
|
console.log('🖥️ 客戶端連線清理已啟動');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 頁面關閉前清理
|
||||||
|
private async cleanupOnUnload() {
|
||||||
|
if (this.isCleaning) return;
|
||||||
|
|
||||||
|
// 確保在瀏覽器環境中執行
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
|
this.isCleaning = true;
|
||||||
|
console.log('🔄 頁面關閉前清理連線...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 使用 sendBeacon 確保請求能夠發送
|
||||||
|
const data = JSON.stringify({
|
||||||
|
action: 'cleanup-current',
|
||||||
|
clientId: this.clientId,
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
|
||||||
|
// 嘗試使用 sendBeacon
|
||||||
|
if (navigator.sendBeacon) {
|
||||||
|
navigator.sendBeacon('/api/ip-cleanup', data);
|
||||||
|
console.log('✅ 使用 sendBeacon 發送清理請求');
|
||||||
|
} else {
|
||||||
|
// 備用方案:使用 fetch
|
||||||
|
await fetch('/api/ip-cleanup', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: data,
|
||||||
|
keepalive: true
|
||||||
|
});
|
||||||
|
console.log('✅ 使用 fetch 發送清理請求');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 清理請求發送失敗:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 頁面隱藏時清理
|
||||||
|
private async cleanupOnHidden() {
|
||||||
|
// 確保在瀏覽器環境中執行
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
|
console.log('🔄 頁面隱藏時清理連線...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fetch('/api/ip-cleanup', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
action: 'cleanup-current',
|
||||||
|
clientId: this.clientId,
|
||||||
|
reason: 'page-hidden'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
console.log('✅ 頁面隱藏清理完成');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 頁面隱藏清理失敗:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定期清理
|
||||||
|
private async periodicCleanup() {
|
||||||
|
// 確保在瀏覽器環境中執行
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
|
console.log('🔄 定期清理連線...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/ip-cleanup?action=local-stats');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success && data.data.trackedConnections > 10) {
|
||||||
|
// 如果追蹤的連線數超過10個,執行清理
|
||||||
|
await fetch('/api/ip-cleanup', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
action: 'cleanup-local',
|
||||||
|
clientId: this.clientId,
|
||||||
|
reason: 'periodic-cleanup'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
console.log('✅ 定期清理完成');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 定期清理失敗:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 手動清理
|
||||||
|
public async manualCleanup(): Promise<boolean> {
|
||||||
|
// 確保在瀏覽器環境中執行
|
||||||
|
if (typeof window === 'undefined') return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/ip-cleanup', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
action: 'cleanup-current',
|
||||||
|
clientId: this.clientId,
|
||||||
|
reason: 'manual-cleanup'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
console.log('✅ 手動清理完成:', data.message);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
console.error('❌ 手動清理失敗:', data.error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 手動清理錯誤:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 獲取連線狀態
|
||||||
|
public async getConnectionStatus() {
|
||||||
|
// 確保在瀏覽器環境中執行
|
||||||
|
if (typeof window === 'undefined') return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/ip-cleanup?action=status');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
console.log('📊 連線狀態:', data.data);
|
||||||
|
return data.data;
|
||||||
|
} else {
|
||||||
|
console.error('❌ 獲取連線狀態失敗:', data.error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 獲取連線狀態錯誤:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 獲取客戶端 ID
|
||||||
|
public getClientId(): string {
|
||||||
|
return this.clientId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 導出單例實例
|
||||||
|
export const clientCleanup = ClientConnectionCleanup.getInstance();
|
||||||
|
|
||||||
|
// 在模組載入時自動初始化(只在瀏覽器環境中)
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
// 延遲初始化,確保 DOM 已載入
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
clientCleanup;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
clientCleanup;
|
||||||
|
}
|
||||||
|
}
|
178
lib/connection-lifecycle-manager.ts
Normal file
178
lib/connection-lifecycle-manager.ts
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
// =====================================================
|
||||||
|
// 連線生命週期管理器
|
||||||
|
// =====================================================
|
||||||
|
|
||||||
|
import { db } from './database';
|
||||||
|
import { dbFailover } from './database-failover';
|
||||||
|
|
||||||
|
export class ConnectionLifecycleManager {
|
||||||
|
private static instance: ConnectionLifecycleManager;
|
||||||
|
private activeConnections = new Map<string, {
|
||||||
|
connection: any;
|
||||||
|
createdAt: number;
|
||||||
|
lastUsed: number;
|
||||||
|
userId?: string;
|
||||||
|
sessionId?: string;
|
||||||
|
}>();
|
||||||
|
private cleanupInterval: NodeJS.Timeout | null = null;
|
||||||
|
private maxIdleTime = 5 * 60 * 1000; // 5分鐘空閒超時
|
||||||
|
private maxConnectionAge = 30 * 60 * 1000; // 30分鐘最大連線時間
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
this.startCleanupProcess();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getInstance(): ConnectionLifecycleManager {
|
||||||
|
if (!ConnectionLifecycleManager.instance) {
|
||||||
|
ConnectionLifecycleManager.instance = new ConnectionLifecycleManager();
|
||||||
|
}
|
||||||
|
return ConnectionLifecycleManager.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 註冊連線
|
||||||
|
public registerConnection(connectionId: string, connection: any, metadata?: {
|
||||||
|
userId?: string;
|
||||||
|
sessionId?: string;
|
||||||
|
}) {
|
||||||
|
this.activeConnections.set(connectionId, {
|
||||||
|
connection,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
lastUsed: Date.now(),
|
||||||
|
userId: metadata?.userId,
|
||||||
|
sessionId: metadata?.sessionId
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`📝 註冊連線: ${connectionId} (總數: ${this.activeConnections.size})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新連線使用時間
|
||||||
|
public updateConnectionUsage(connectionId: string) {
|
||||||
|
const conn = this.activeConnections.get(connectionId);
|
||||||
|
if (conn) {
|
||||||
|
conn.lastUsed = Date.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 釋放連線
|
||||||
|
public releaseConnection(connectionId: string) {
|
||||||
|
const conn = this.activeConnections.get(connectionId);
|
||||||
|
if (conn) {
|
||||||
|
try {
|
||||||
|
if (conn.connection && typeof conn.connection.release === 'function') {
|
||||||
|
conn.connection.release();
|
||||||
|
}
|
||||||
|
this.activeConnections.delete(connectionId);
|
||||||
|
console.log(`🗑️ 釋放連線: ${connectionId} (剩餘: ${this.activeConnections.size})`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ 釋放連線失敗: ${connectionId}`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 開始清理程序
|
||||||
|
private startCleanupProcess() {
|
||||||
|
// 每30秒檢查一次空閒連線
|
||||||
|
this.cleanupInterval = setInterval(() => {
|
||||||
|
this.cleanupIdleConnections();
|
||||||
|
}, 30 * 1000);
|
||||||
|
|
||||||
|
console.log('🧹 連線生命週期管理器已啟動');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理空閒連線
|
||||||
|
private cleanupIdleConnections() {
|
||||||
|
const now = Date.now();
|
||||||
|
const toRemove: string[] = [];
|
||||||
|
|
||||||
|
for (const [connectionId, conn] of this.activeConnections) {
|
||||||
|
const idleTime = now - conn.lastUsed;
|
||||||
|
const connectionAge = now - conn.createdAt;
|
||||||
|
|
||||||
|
// 檢查空閒時間
|
||||||
|
if (idleTime > this.maxIdleTime) {
|
||||||
|
console.log(`⏰ 連線 ${connectionId} 空閒時間過長 (${Math.round(idleTime / 1000)}s),準備釋放`);
|
||||||
|
toRemove.push(connectionId);
|
||||||
|
}
|
||||||
|
// 檢查連線年齡
|
||||||
|
else if (connectionAge > this.maxConnectionAge) {
|
||||||
|
console.log(`⏰ 連線 ${connectionId} 存在時間過長 (${Math.round(connectionAge / 1000)}s),準備釋放`);
|
||||||
|
toRemove.push(connectionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 釋放需要清理的連線
|
||||||
|
toRemove.forEach(connectionId => {
|
||||||
|
this.releaseConnection(connectionId);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (toRemove.length > 0) {
|
||||||
|
console.log(`🧹 清理了 ${toRemove.length} 個空閒連線`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 強制清理所有連線
|
||||||
|
public forceCleanupAll() {
|
||||||
|
console.log('🚨 強制清理所有連線...');
|
||||||
|
|
||||||
|
const connectionIds = Array.from(this.activeConnections.keys());
|
||||||
|
connectionIds.forEach(connectionId => {
|
||||||
|
this.releaseConnection(connectionId);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`✅ 已清理 ${connectionIds.length} 個連線`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 獲取連線統計
|
||||||
|
public getConnectionStats() {
|
||||||
|
const now = Date.now();
|
||||||
|
const connections = Array.from(this.activeConnections.values());
|
||||||
|
|
||||||
|
const idleConnections = connections.filter(conn =>
|
||||||
|
now - conn.lastUsed > this.maxIdleTime
|
||||||
|
).length;
|
||||||
|
|
||||||
|
const oldConnections = connections.filter(conn =>
|
||||||
|
now - conn.createdAt > this.maxConnectionAge
|
||||||
|
).length;
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalConnections: this.activeConnections.size,
|
||||||
|
idleConnections,
|
||||||
|
oldConnections,
|
||||||
|
maxIdleTime: this.maxIdleTime,
|
||||||
|
maxConnectionAge: this.maxConnectionAge,
|
||||||
|
connections: connections.map(conn => ({
|
||||||
|
createdAt: new Date(conn.createdAt).toISOString(),
|
||||||
|
lastUsed: new Date(conn.lastUsed).toISOString(),
|
||||||
|
idleTime: now - conn.lastUsed,
|
||||||
|
age: now - conn.createdAt,
|
||||||
|
userId: conn.userId,
|
||||||
|
sessionId: conn.sessionId
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 停止清理程序
|
||||||
|
public stop() {
|
||||||
|
if (this.cleanupInterval) {
|
||||||
|
clearInterval(this.cleanupInterval);
|
||||||
|
this.cleanupInterval = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理所有連線
|
||||||
|
this.forceCleanupAll();
|
||||||
|
|
||||||
|
console.log('⏹️ 連線生命週期管理器已停止');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 設置清理參數
|
||||||
|
public setCleanupParams(maxIdleTime?: number, maxConnectionAge?: number) {
|
||||||
|
if (maxIdleTime) this.maxIdleTime = maxIdleTime;
|
||||||
|
if (maxConnectionAge) this.maxConnectionAge = maxConnectionAge;
|
||||||
|
|
||||||
|
console.log(`⚙️ 更新清理參數: 空閒時間=${this.maxIdleTime}ms, 最大年齡=${this.maxConnectionAge}ms`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 導出單例實例
|
||||||
|
export const connectionLifecycleManager = ConnectionLifecycleManager.getInstance();
|
@@ -5,6 +5,7 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { db } from './database';
|
import { db } from './database';
|
||||||
import { dbMonitor } from './database-monitor';
|
import { dbMonitor } from './database-monitor';
|
||||||
|
import { dbShutdownManager } from './database-shutdown-manager';
|
||||||
|
|
||||||
// 連線池狀態追蹤
|
// 連線池狀態追蹤
|
||||||
let connectionCount = 0;
|
let connectionCount = 0;
|
||||||
@@ -135,22 +136,24 @@ export function startConnectionMonitoring() {
|
|||||||
console.log('🔍 資料庫連線監控已啟動');
|
console.log('🔍 資料庫連線監控已啟動');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 優雅的關閉
|
// 優雅的關閉(使用新的關閉管理器)
|
||||||
export async function gracefulShutdown() {
|
export async function gracefulShutdown() {
|
||||||
console.log('🔄 正在優雅關閉資料庫連線...');
|
console.log('🔄 使用資料庫關閉管理器進行優雅關閉...');
|
||||||
|
await dbShutdownManager.gracefulShutdown();
|
||||||
try {
|
|
||||||
dbMonitor.stopMonitoring();
|
|
||||||
await db.close();
|
|
||||||
console.log('✅ 資料庫連線已關閉');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ 關閉資料庫連線時發生錯誤:', error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 處理程序退出事件
|
// 強制關閉
|
||||||
if (typeof process !== 'undefined') {
|
export function forceShutdown() {
|
||||||
process.on('SIGINT', gracefulShutdown);
|
console.log('🚨 強制關閉資料庫連線...');
|
||||||
process.on('SIGTERM', gracefulShutdown);
|
dbShutdownManager.forceShutdown();
|
||||||
process.on('exit', gracefulShutdown);
|
}
|
||||||
|
|
||||||
|
// 獲取關閉狀態
|
||||||
|
export function getShutdownStatus() {
|
||||||
|
return dbShutdownManager.getShutdownStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 測試關閉機制
|
||||||
|
export async function testShutdown() {
|
||||||
|
return await dbShutdownManager.testShutdown();
|
||||||
}
|
}
|
||||||
|
@@ -50,21 +50,25 @@ export class DatabaseMonitor {
|
|||||||
|
|
||||||
// 檢查連線狀態
|
// 檢查連線狀態
|
||||||
private async checkConnectionStatus() {
|
private async checkConnectionStatus() {
|
||||||
|
let connection = null;
|
||||||
try {
|
try {
|
||||||
|
// 使用單一連線來檢查狀態,避免建立多個連線
|
||||||
|
connection = await db.getConnection();
|
||||||
|
|
||||||
// 檢查當前連線數
|
// 檢查當前連線數
|
||||||
const statusResult = await db.queryOne(`
|
const [statusResult] = await connection.execute(`
|
||||||
SHOW STATUS LIKE 'Threads_connected'
|
SHOW STATUS LIKE 'Threads_connected'
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const currentConnections = statusResult?.Value || 0;
|
const currentConnections = statusResult[0]?.Value || 0;
|
||||||
this.connectionCount = parseInt(currentConnections);
|
this.connectionCount = parseInt(currentConnections);
|
||||||
|
|
||||||
// 檢查最大連線數
|
// 檢查最大連線數
|
||||||
const maxConnResult = await db.queryOne(`
|
const [maxConnResult] = await connection.execute(`
|
||||||
SHOW VARIABLES LIKE 'max_connections'
|
SHOW VARIABLES LIKE 'max_connections'
|
||||||
`);
|
`);
|
||||||
|
|
||||||
this.maxConnections = parseInt(maxConnResult?.Value || '100');
|
this.maxConnections = parseInt(maxConnResult[0]?.Value || '100');
|
||||||
|
|
||||||
// 檢查連線使用率
|
// 檢查連線使用率
|
||||||
const usagePercentage = (this.connectionCount / this.maxConnections) * 100;
|
const usagePercentage = (this.connectionCount / this.maxConnections) * 100;
|
||||||
@@ -80,43 +84,68 @@ export class DatabaseMonitor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 檢查長時間運行的查詢
|
// 檢查長時間運行的查詢
|
||||||
await this.checkLongRunningQueries();
|
await this.checkLongRunningQueries(connection);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ 資料庫監控錯誤:', error);
|
console.error('❌ 資料庫監控錯誤:', error);
|
||||||
|
} finally {
|
||||||
|
// 確保釋放連線
|
||||||
|
if (connection) {
|
||||||
|
try {
|
||||||
|
connection.release();
|
||||||
|
} catch (releaseError) {
|
||||||
|
console.error('❌ 釋放監控連線失敗:', releaseError);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 檢查長時間運行的查詢
|
// 檢查長時間運行的查詢
|
||||||
private async checkLongRunningQueries() {
|
private async checkLongRunningQueries(connection?: any) {
|
||||||
try {
|
try {
|
||||||
const longQueries = await db.query(`
|
// 如果沒有傳入連線,則建立一個新的
|
||||||
SELECT
|
let tempConnection = connection;
|
||||||
ID,
|
let shouldRelease = false;
|
||||||
USER,
|
|
||||||
HOST,
|
|
||||||
DB,
|
|
||||||
COMMAND,
|
|
||||||
TIME,
|
|
||||||
STATE,
|
|
||||||
INFO
|
|
||||||
FROM information_schema.PROCESSLIST
|
|
||||||
WHERE TIME > 30
|
|
||||||
AND COMMAND != 'Sleep'
|
|
||||||
ORDER BY TIME DESC
|
|
||||||
LIMIT 5
|
|
||||||
`);
|
|
||||||
|
|
||||||
if (longQueries.length > 0) {
|
if (!tempConnection) {
|
||||||
console.warn(`⚠️ 發現 ${longQueries.length} 個長時間運行的查詢:`);
|
tempConnection = await db.getConnection();
|
||||||
longQueries.forEach((query, index) => {
|
shouldRelease = true;
|
||||||
console.warn(` ${index + 1}. 用戶: ${query.USER}, 時間: ${query.TIME}s, 狀態: ${query.STATE}`);
|
}
|
||||||
if (query.INFO && query.INFO.length > 100) {
|
|
||||||
console.warn(` 查詢: ${query.INFO.substring(0, 100)}...`);
|
try {
|
||||||
} else if (query.INFO) {
|
const [longQueries] = await tempConnection.execute(`
|
||||||
console.warn(` 查詢: ${query.INFO}`);
|
SELECT
|
||||||
}
|
ID,
|
||||||
});
|
USER,
|
||||||
|
HOST,
|
||||||
|
DB,
|
||||||
|
COMMAND,
|
||||||
|
TIME,
|
||||||
|
STATE,
|
||||||
|
INFO
|
||||||
|
FROM information_schema.PROCESSLIST
|
||||||
|
WHERE TIME > 30
|
||||||
|
AND COMMAND != 'Sleep'
|
||||||
|
ORDER BY TIME DESC
|
||||||
|
LIMIT 5
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (longQueries.length > 0) {
|
||||||
|
console.warn(`⚠️ 發現 ${longQueries.length} 個長時間運行的查詢:`);
|
||||||
|
longQueries.forEach((query: any, index: number) => {
|
||||||
|
console.warn(` ${index + 1}. 用戶: ${query.USER}, 時間: ${query.TIME}s, 狀態: ${query.STATE}`);
|
||||||
|
if (query.INFO && query.INFO.length > 100) {
|
||||||
|
console.warn(` 查詢: ${query.INFO.substring(0, 100)}...`);
|
||||||
|
} else if (query.INFO) {
|
||||||
|
console.warn(` 查詢: ${query.INFO}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
// 只有當我們建立了新連線時才釋放
|
||||||
|
if (shouldRelease && tempConnection) {
|
||||||
|
tempConnection.release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ 檢查長時間查詢時發生錯誤:', error);
|
console.error('❌ 檢查長時間查詢時發生錯誤:', error);
|
||||||
|
89
lib/database-service-smart.ts
Normal file
89
lib/database-service-smart.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
// =====================================================
|
||||||
|
// 智能資料庫服務範例
|
||||||
|
// =====================================================
|
||||||
|
|
||||||
|
import { smartPool } from './smart-connection-pool';
|
||||||
|
|
||||||
|
// 這是一個範例,展示如何使用智能連線池
|
||||||
|
// 你可以將現有的資料庫服務改為使用這個方式
|
||||||
|
|
||||||
|
export class SmartDatabaseService {
|
||||||
|
// 智能查詢範例
|
||||||
|
static async getUserById(userId: string, requestId?: string) {
|
||||||
|
return await smartPool.executeQueryOne(
|
||||||
|
'SELECT * FROM users WHERE id = ?',
|
||||||
|
[userId],
|
||||||
|
{
|
||||||
|
userId,
|
||||||
|
sessionId: requestId,
|
||||||
|
requestId: `getUserById_${Date.now()}`
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 智能插入範例
|
||||||
|
static async createUser(userData: any, requestId?: string) {
|
||||||
|
return await smartPool.executeInsert(
|
||||||
|
'INSERT INTO users (name, email, department) VALUES (?, ?, ?)',
|
||||||
|
[userData.name, userData.email, userData.department],
|
||||||
|
{
|
||||||
|
userId: userData.id,
|
||||||
|
sessionId: requestId,
|
||||||
|
requestId: `createUser_${Date.now()}`
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 智能更新範例
|
||||||
|
static async updateUser(userId: string, userData: any, requestId?: string) {
|
||||||
|
return await smartPool.executeUpdate(
|
||||||
|
'UPDATE users SET name = ?, email = ?, department = ? WHERE id = ?',
|
||||||
|
[userData.name, userData.email, userData.department, userId],
|
||||||
|
{
|
||||||
|
userId,
|
||||||
|
sessionId: requestId,
|
||||||
|
requestId: `updateUser_${Date.now()}`
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 智能刪除範例
|
||||||
|
static async deleteUser(userId: string, requestId?: string) {
|
||||||
|
return await smartPool.executeDelete(
|
||||||
|
'DELETE FROM users WHERE id = ?',
|
||||||
|
[userId],
|
||||||
|
{
|
||||||
|
userId,
|
||||||
|
sessionId: requestId,
|
||||||
|
requestId: `deleteUser_${Date.now()}`
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 獲取連線統計
|
||||||
|
static getConnectionStats() {
|
||||||
|
return smartPool.getConnectionStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 強制清理連線
|
||||||
|
static forceCleanup() {
|
||||||
|
return smartPool.forceCleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用範例:
|
||||||
|
/*
|
||||||
|
// 在 API 路由中使用
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
const { searchParams } = new URL(request.url);
|
||||||
|
const userId = searchParams.get('userId');
|
||||||
|
const requestId = request.headers.get('x-request-id') || 'unknown';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = await SmartDatabaseService.getUserById(userId, requestId);
|
||||||
|
return NextResponse.json({ success: true, data: user });
|
||||||
|
} catch (error) {
|
||||||
|
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
197
lib/database-shutdown-manager.ts
Normal file
197
lib/database-shutdown-manager.ts
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
// =====================================================
|
||||||
|
// 資料庫關閉管理器
|
||||||
|
// =====================================================
|
||||||
|
|
||||||
|
import { db } from './database';
|
||||||
|
import { dbFailover } from './database-failover';
|
||||||
|
import { dbMonitor } from './database-monitor';
|
||||||
|
import { smartPool } from './smart-connection-pool';
|
||||||
|
|
||||||
|
export class DatabaseShutdownManager {
|
||||||
|
private static instance: DatabaseShutdownManager;
|
||||||
|
private isShuttingDown = false;
|
||||||
|
private shutdownHandlers: (() => Promise<void>)[] = [];
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
this.registerShutdownHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getInstance(): DatabaseShutdownManager {
|
||||||
|
if (!DatabaseShutdownManager.instance) {
|
||||||
|
DatabaseShutdownManager.instance = new DatabaseShutdownManager();
|
||||||
|
}
|
||||||
|
return DatabaseShutdownManager.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 註冊關閉處理器
|
||||||
|
private registerShutdownHandlers() {
|
||||||
|
// 添加資料庫關閉處理器
|
||||||
|
this.addShutdownHandler('database', async () => {
|
||||||
|
console.log('🔄 正在關閉主要資料庫連線池...');
|
||||||
|
try {
|
||||||
|
await db.close();
|
||||||
|
console.log('✅ 主要資料庫連線池已關閉');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 關閉主要資料庫連線池時發生錯誤:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加備援資料庫關閉處理器
|
||||||
|
this.addShutdownHandler('failover', async () => {
|
||||||
|
console.log('🔄 正在關閉備援資料庫連線池...');
|
||||||
|
try {
|
||||||
|
await dbFailover.close();
|
||||||
|
console.log('✅ 備援資料庫連線池已關閉');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 關閉備援資料庫連線池時發生錯誤:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加監控服務關閉處理器
|
||||||
|
this.addShutdownHandler('monitor', async () => {
|
||||||
|
console.log('🔄 正在停止資料庫監控服務...');
|
||||||
|
try {
|
||||||
|
dbMonitor.stopMonitoring();
|
||||||
|
console.log('✅ 資料庫監控服務已停止');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 停止資料庫監控服務時發生錯誤:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加智能連線池關閉處理器
|
||||||
|
this.addShutdownHandler('smart-pool', async () => {
|
||||||
|
console.log('🔄 正在清理智能連線池...');
|
||||||
|
try {
|
||||||
|
smartPool.forceCleanup();
|
||||||
|
console.log('✅ 智能連線池已清理');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 清理智能連線池時發生錯誤:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 註冊系統信號處理器
|
||||||
|
this.registerSystemHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加關閉處理器
|
||||||
|
public addShutdownHandler(name: string, handler: () => Promise<void>) {
|
||||||
|
this.shutdownHandlers.push(async () => {
|
||||||
|
try {
|
||||||
|
console.log(`🔄 執行關閉處理器: ${name}`);
|
||||||
|
await handler();
|
||||||
|
console.log(`✅ 關閉處理器完成: ${name}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ 關閉處理器失敗: ${name}`, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 註冊系統信號處理器
|
||||||
|
private registerSystemHandlers() {
|
||||||
|
if (typeof process === 'undefined') return;
|
||||||
|
|
||||||
|
// SIGINT (Ctrl+C)
|
||||||
|
process.on('SIGINT', () => {
|
||||||
|
console.log('\n🛑 收到 SIGINT 信號,開始優雅關閉...');
|
||||||
|
this.gracefulShutdown();
|
||||||
|
});
|
||||||
|
|
||||||
|
// SIGTERM (終止信號)
|
||||||
|
process.on('SIGTERM', () => {
|
||||||
|
console.log('\n🛑 收到 SIGTERM 信號,開始優雅關閉...');
|
||||||
|
this.gracefulShutdown();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 未捕獲的異常
|
||||||
|
process.on('uncaughtException', (error) => {
|
||||||
|
console.error('❌ 未捕獲的異常:', error);
|
||||||
|
this.gracefulShutdown();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 未處理的 Promise 拒絕
|
||||||
|
process.on('unhandledRejection', (reason, promise) => {
|
||||||
|
console.error('❌ 未處理的 Promise 拒絕:', reason);
|
||||||
|
this.gracefulShutdown();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 程序退出
|
||||||
|
process.on('exit', () => {
|
||||||
|
if (!this.isShuttingDown) {
|
||||||
|
console.log('🛑 程序即將退出,強制關閉資料庫連線...');
|
||||||
|
this.forceShutdown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ 系統信號處理器已註冊');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 優雅關閉
|
||||||
|
public async gracefulShutdown() {
|
||||||
|
if (this.isShuttingDown) {
|
||||||
|
console.log('⚠️ 關閉程序已在進行中,跳過重複請求');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isShuttingDown = true;
|
||||||
|
console.log('🔄 開始優雅關閉資料庫連線...');
|
||||||
|
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 並行執行所有關閉處理器
|
||||||
|
await Promise.allSettled(
|
||||||
|
this.shutdownHandlers.map(handler => handler())
|
||||||
|
);
|
||||||
|
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
console.log(`✅ 資料庫連線關閉完成,耗時: ${duration}ms`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 優雅關閉過程中發生錯誤:', error);
|
||||||
|
} finally {
|
||||||
|
// 強制退出程序
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('🛑 強制退出程序');
|
||||||
|
process.exit(0);
|
||||||
|
}, 5000); // 5秒後強制退出
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 強制關閉
|
||||||
|
public forceShutdown() {
|
||||||
|
console.log('🚨 強制關閉所有資料庫連線...');
|
||||||
|
|
||||||
|
// 同步執行關閉處理器(不等待)
|
||||||
|
this.shutdownHandlers.forEach(handler => {
|
||||||
|
try {
|
||||||
|
handler();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 強制關閉時發生錯誤:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ 強制關閉完成');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 獲取關閉狀態
|
||||||
|
public getShutdownStatus() {
|
||||||
|
return {
|
||||||
|
isShuttingDown: this.isShuttingDown,
|
||||||
|
handlerCount: this.shutdownHandlers.length,
|
||||||
|
registeredHandlers: this.shutdownHandlers.map((_, index) => `handler-${index}`)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 測試關閉機制
|
||||||
|
public async testShutdown() {
|
||||||
|
console.log('🧪 測試資料庫關閉機制...');
|
||||||
|
await this.gracefulShutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 導出單例實例
|
||||||
|
export const dbShutdownManager = DatabaseShutdownManager.getInstance();
|
||||||
|
|
||||||
|
// 導出便捷函數
|
||||||
|
export const gracefulShutdown = () => dbShutdownManager.gracefulShutdown();
|
||||||
|
export const forceShutdown = () => dbShutdownManager.forceShutdown();
|
175
lib/emergency-connection-cleanup.ts
Normal file
175
lib/emergency-connection-cleanup.ts
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
// =====================================================
|
||||||
|
// 緊急連線清理工具
|
||||||
|
// =====================================================
|
||||||
|
|
||||||
|
import { db } from './database';
|
||||||
|
import { dbFailover } from './database-failover';
|
||||||
|
import { dbMonitor } from './database-monitor';
|
||||||
|
|
||||||
|
export class EmergencyConnectionCleanup {
|
||||||
|
private static instance: EmergencyConnectionCleanup;
|
||||||
|
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
public static getInstance(): EmergencyConnectionCleanup {
|
||||||
|
if (!EmergencyConnectionCleanup.instance) {
|
||||||
|
EmergencyConnectionCleanup.instance = new EmergencyConnectionCleanup();
|
||||||
|
}
|
||||||
|
return EmergencyConnectionCleanup.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 立即停止所有監控和清理所有連線
|
||||||
|
public async emergencyCleanup() {
|
||||||
|
console.log('🚨 執行緊急連線清理...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 立即停止所有監控
|
||||||
|
console.log('⏹️ 停止資料庫監控...');
|
||||||
|
dbMonitor.stopMonitoring();
|
||||||
|
|
||||||
|
// 2. 強制關閉所有連線池
|
||||||
|
console.log('🔌 關閉主要資料庫連線池...');
|
||||||
|
await db.close();
|
||||||
|
|
||||||
|
console.log('🔌 關閉備援資料庫連線池...');
|
||||||
|
await dbFailover.close();
|
||||||
|
|
||||||
|
// 3. 等待一段時間讓連線完全關閉
|
||||||
|
console.log('⏳ 等待連線關閉...');
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
|
||||||
|
// 4. 檢查連線狀態
|
||||||
|
await this.checkConnectionStatus();
|
||||||
|
|
||||||
|
console.log('✅ 緊急清理完成');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 緊急清理失敗:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 檢查連線狀態(不建立新連線)
|
||||||
|
private async checkConnectionStatus() {
|
||||||
|
try {
|
||||||
|
// 使用一個臨時連線來檢查狀態
|
||||||
|
const tempConnection = await db.getConnection();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [statusResult] = await tempConnection.execute(`
|
||||||
|
SHOW STATUS LIKE 'Threads_connected'
|
||||||
|
`);
|
||||||
|
|
||||||
|
const [maxConnResult] = await tempConnection.execute(`
|
||||||
|
SHOW VARIABLES LIKE 'max_connections'
|
||||||
|
`);
|
||||||
|
|
||||||
|
const currentConnections = statusResult[0]?.Value || 0;
|
||||||
|
const maxConnections = maxConnResult[0]?.Value || 100;
|
||||||
|
const usagePercentage = (currentConnections / maxConnections) * 100;
|
||||||
|
|
||||||
|
console.log(`📊 清理後連線狀態: ${currentConnections}/${maxConnections} (${usagePercentage.toFixed(1)}%)`);
|
||||||
|
|
||||||
|
if (currentConnections > 5) {
|
||||||
|
console.warn(`⚠️ 仍有 ${currentConnections} 個連線未關閉`);
|
||||||
|
} else {
|
||||||
|
console.log('✅ 連線已成功清理');
|
||||||
|
}
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
tempConnection.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 檢查連線狀態失敗:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 強制殺死所有資料庫連線
|
||||||
|
public async forceKillAllConnections() {
|
||||||
|
console.log('💀 強制殺死所有資料庫連線...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tempConnection = await db.getConnection();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 獲取所有連線ID
|
||||||
|
const [connections] = await tempConnection.execute(`
|
||||||
|
SELECT ID FROM information_schema.PROCESSLIST
|
||||||
|
WHERE USER = ? AND COMMAND != 'Sleep'
|
||||||
|
`, [process.env.DB_USER || 'A999']);
|
||||||
|
|
||||||
|
console.log(`🔍 找到 ${connections.length} 個活躍連線`);
|
||||||
|
|
||||||
|
// 殺死所有連線(除了當前連線)
|
||||||
|
for (const conn of connections) {
|
||||||
|
if (conn.ID !== tempConnection.threadId) {
|
||||||
|
try {
|
||||||
|
await tempConnection.execute(`KILL ${conn.ID}`);
|
||||||
|
console.log(`💀 已殺死連線 ${conn.ID}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`⚠️ 無法殺死連線 ${conn.ID}:`, error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待連線關閉
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
|
// 再次檢查狀態
|
||||||
|
await this.checkConnectionStatus();
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
tempConnection.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 強制殺死連線失敗:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 獲取連線詳情
|
||||||
|
public async getConnectionDetails() {
|
||||||
|
try {
|
||||||
|
const tempConnection = await db.getConnection();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [connections] = await tempConnection.execute(`
|
||||||
|
SELECT
|
||||||
|
ID,
|
||||||
|
USER,
|
||||||
|
HOST,
|
||||||
|
DB,
|
||||||
|
COMMAND,
|
||||||
|
TIME,
|
||||||
|
STATE,
|
||||||
|
INFO
|
||||||
|
FROM information_schema.PROCESSLIST
|
||||||
|
WHERE USER = ?
|
||||||
|
ORDER BY TIME DESC
|
||||||
|
`, [process.env.DB_USER || 'A999']);
|
||||||
|
|
||||||
|
console.log('📋 當前資料庫連線詳情:');
|
||||||
|
connections.forEach((conn, index) => {
|
||||||
|
console.log(`${index + 1}. ID: ${conn.ID}, 用戶: ${conn.USER}, 時間: ${conn.TIME}s, 狀態: ${conn.STATE}`);
|
||||||
|
if (conn.INFO && conn.INFO.length > 50) {
|
||||||
|
console.log(` 查詢: ${conn.INFO.substring(0, 50)}...`);
|
||||||
|
} else if (conn.INFO) {
|
||||||
|
console.log(` 查詢: ${conn.INFO}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return connections;
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
tempConnection.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 獲取連線詳情失敗:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 導出單例實例
|
||||||
|
export const emergencyCleanup = EmergencyConnectionCleanup.getInstance();
|
251
lib/ip-based-connection-manager.ts
Normal file
251
lib/ip-based-connection-manager.ts
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
// =====================================================
|
||||||
|
// 基於 IP 的連線管理器
|
||||||
|
// =====================================================
|
||||||
|
|
||||||
|
import mysql from 'mysql2/promise';
|
||||||
|
import { db } from './database';
|
||||||
|
|
||||||
|
export class IPBasedConnectionManager {
|
||||||
|
private static instance: IPBasedConnectionManager;
|
||||||
|
private clientIP: string | null = null;
|
||||||
|
private connectionTracker = new Map<string, {
|
||||||
|
connectionId: string;
|
||||||
|
createdAt: number;
|
||||||
|
lastUsed: number;
|
||||||
|
userAgent?: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
this.detectClientIP();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getInstance(): IPBasedConnectionManager {
|
||||||
|
if (!IPBasedConnectionManager.instance) {
|
||||||
|
IPBasedConnectionManager.instance = new IPBasedConnectionManager();
|
||||||
|
}
|
||||||
|
return IPBasedConnectionManager.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 檢測客戶端 IP
|
||||||
|
private detectClientIP() {
|
||||||
|
// 在瀏覽器環境中,我們無法直接獲取真實 IP
|
||||||
|
// 但我們可以通過其他方式來識別連線
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
// 客戶端:生成一個唯一標識符
|
||||||
|
this.clientIP = this.generateClientId();
|
||||||
|
console.log('🖥️ 客戶端標識符:', this.clientIP);
|
||||||
|
} else {
|
||||||
|
// 服務端:嘗試從環境變數或請求中獲取
|
||||||
|
this.clientIP = process.env.CLIENT_IP || 'server-side';
|
||||||
|
console.log('🖥️ 服務端標識符:', this.clientIP);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成客戶端唯一標識符
|
||||||
|
private generateClientId(): string {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const random = Math.random().toString(36).substring(2);
|
||||||
|
return `client_${timestamp}_${random}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 設置客戶端 IP(從請求中獲取)
|
||||||
|
public setClientIP(ip: string) {
|
||||||
|
this.clientIP = ip;
|
||||||
|
console.log('🖥️ 設置客戶端 IP:', ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 獲取客戶端 IP
|
||||||
|
public getClientIP(): string | null {
|
||||||
|
return this.clientIP;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 註冊連線(帶 IP 標識)
|
||||||
|
public registerConnection(connectionId: string, metadata?: {
|
||||||
|
userAgent?: string;
|
||||||
|
requestId?: string;
|
||||||
|
}) {
|
||||||
|
if (!this.clientIP) return;
|
||||||
|
|
||||||
|
this.connectionTracker.set(connectionId, {
|
||||||
|
connectionId,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
lastUsed: Date.now(),
|
||||||
|
userAgent: metadata?.userAgent,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`📝 註冊連線: ${connectionId} (IP: ${this.clientIP})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新連線使用時間
|
||||||
|
public updateConnectionUsage(connectionId: string) {
|
||||||
|
const conn = this.connectionTracker.get(connectionId);
|
||||||
|
if (conn) {
|
||||||
|
conn.lastUsed = Date.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 釋放連線
|
||||||
|
public releaseConnection(connectionId: string) {
|
||||||
|
const conn = this.connectionTracker.get(connectionId);
|
||||||
|
if (conn) {
|
||||||
|
this.connectionTracker.delete(connectionId);
|
||||||
|
console.log(`🗑️ 釋放連線: ${connectionId} (IP: ${this.clientIP})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根據 IP 清理連線
|
||||||
|
public async cleanupConnectionsByIP(targetIP?: string): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
killedCount: number;
|
||||||
|
message: string;
|
||||||
|
}> {
|
||||||
|
const ipToClean = targetIP || this.clientIP;
|
||||||
|
|
||||||
|
if (!ipToClean) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
killedCount: 0,
|
||||||
|
message: '無法識別客戶端 IP'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`🧹 開始清理 IP ${ipToClean} 的連線...`);
|
||||||
|
|
||||||
|
let connection = null;
|
||||||
|
let killedCount = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 建立臨時連線
|
||||||
|
connection = await db.getConnection();
|
||||||
|
|
||||||
|
// 獲取指定 IP 的所有連線(使用更寬鬆的匹配)
|
||||||
|
const [connections] = await connection.execute(`
|
||||||
|
SELECT ID, USER, HOST, DB, COMMAND, TIME, STATE, INFO
|
||||||
|
FROM information_schema.PROCESSLIST
|
||||||
|
WHERE USER = ? AND (HOST LIKE ? OR HOST LIKE ? OR HOST LIKE ?)
|
||||||
|
ORDER BY TIME DESC
|
||||||
|
`, [
|
||||||
|
process.env.DB_USER || 'A999',
|
||||||
|
`%${ipToClean}%`, // 包含 IP 的 HOST
|
||||||
|
`%${ipToClean}.%`, // IP 開頭的 HOST
|
||||||
|
`%${ipToClean}-%` // IP 帶連字符的 HOST
|
||||||
|
]);
|
||||||
|
|
||||||
|
console.log(`🔍 找到 ${connections.length} 個來自 ${ipToClean} 的連線`);
|
||||||
|
|
||||||
|
// 顯示連線詳情
|
||||||
|
connections.forEach((conn: any, index: number) => {
|
||||||
|
console.log(`${index + 1}. ID: ${conn.ID}, HOST: ${conn.HOST}, 時間: ${conn.TIME}s, 狀態: ${conn.STATE}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 殺死指定 IP 的連線(除了當前連線)
|
||||||
|
for (const conn of connections) {
|
||||||
|
if (conn.ID !== connection.threadId) {
|
||||||
|
try {
|
||||||
|
await connection.execute(`KILL ${conn.ID}`);
|
||||||
|
console.log(`💀 已殺死連線 ${conn.ID} (HOST: ${conn.HOST})`);
|
||||||
|
killedCount++;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log(`⚠️ 無法殺死連線 ${conn.ID}: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理本地追蹤的連線
|
||||||
|
this.connectionTracker.clear();
|
||||||
|
|
||||||
|
console.log(`✅ 已清理 ${killedCount} 個來自 ${ipToClean} 的連線`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
killedCount,
|
||||||
|
message: `已清理 ${killedCount} 個來自 ${ipToClean} 的連線`
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 清理連線失敗:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
killedCount: 0,
|
||||||
|
message: `清理失敗: ${error instanceof Error ? error.message : '未知錯誤'}`
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
if (connection) {
|
||||||
|
connection.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 獲取指定 IP 的連線狀態
|
||||||
|
public async getConnectionsByIP(targetIP?: string): Promise<{
|
||||||
|
ip: string;
|
||||||
|
connectionCount: number;
|
||||||
|
connections: any[];
|
||||||
|
}> {
|
||||||
|
const ipToCheck = targetIP || this.clientIP;
|
||||||
|
|
||||||
|
if (!ipToCheck) {
|
||||||
|
return {
|
||||||
|
ip: 'unknown',
|
||||||
|
connectionCount: 0,
|
||||||
|
connections: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let connection = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
connection = await db.getConnection();
|
||||||
|
|
||||||
|
const [connections] = await connection.execute(`
|
||||||
|
SELECT ID, USER, HOST, DB, COMMAND, TIME, STATE, INFO
|
||||||
|
FROM information_schema.PROCESSLIST
|
||||||
|
WHERE USER = ? AND HOST LIKE ?
|
||||||
|
ORDER BY TIME DESC
|
||||||
|
`, [process.env.DB_USER || 'A999', `%${ipToCheck}%`]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
ip: ipToCheck,
|
||||||
|
connectionCount: connections.length,
|
||||||
|
connections: connections
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 獲取連線狀態失敗:', error);
|
||||||
|
return {
|
||||||
|
ip: ipToCheck,
|
||||||
|
connectionCount: 0,
|
||||||
|
connections: []
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
if (connection) {
|
||||||
|
connection.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理所有本地追蹤的連線
|
||||||
|
public cleanupLocalConnections() {
|
||||||
|
const count = this.connectionTracker.size;
|
||||||
|
this.connectionTracker.clear();
|
||||||
|
console.log(`🧹 已清理 ${count} 個本地追蹤的連線`);
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 獲取本地連線統計
|
||||||
|
public getLocalConnectionStats() {
|
||||||
|
return {
|
||||||
|
clientIP: this.clientIP,
|
||||||
|
trackedConnections: this.connectionTracker.size,
|
||||||
|
connections: Array.from(this.connectionTracker.values()).map(conn => ({
|
||||||
|
connectionId: conn.connectionId,
|
||||||
|
createdAt: new Date(conn.createdAt).toISOString(),
|
||||||
|
lastUsed: new Date(conn.lastUsed).toISOString(),
|
||||||
|
userAgent: conn.userAgent
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 導出單例實例
|
||||||
|
export const ipConnectionManager = IPBasedConnectionManager.getInstance();
|
202
lib/smart-connection-pool.ts
Normal file
202
lib/smart-connection-pool.ts
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
// =====================================================
|
||||||
|
// 智能連線池包裝器
|
||||||
|
// =====================================================
|
||||||
|
|
||||||
|
import { db } from './database';
|
||||||
|
import { dbFailover } from './database-failover';
|
||||||
|
import { connectionLifecycleManager } from './connection-lifecycle-manager';
|
||||||
|
|
||||||
|
export class SmartConnectionPool {
|
||||||
|
private static instance: SmartConnectionPool;
|
||||||
|
private connectionCounter = 0;
|
||||||
|
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
public static getInstance(): SmartConnectionPool {
|
||||||
|
if (!SmartConnectionPool.instance) {
|
||||||
|
SmartConnectionPool.instance = new SmartConnectionPool();
|
||||||
|
}
|
||||||
|
return SmartConnectionPool.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 獲取智能連線
|
||||||
|
public async getSmartConnection(metadata?: {
|
||||||
|
userId?: string;
|
||||||
|
sessionId?: string;
|
||||||
|
requestId?: string;
|
||||||
|
}): Promise<{
|
||||||
|
connection: any;
|
||||||
|
connectionId: string;
|
||||||
|
release: () => void;
|
||||||
|
}> {
|
||||||
|
const connectionId = `conn_${++this.connectionCounter}_${Date.now()}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 獲取實際連線
|
||||||
|
const connection = await db.getConnection();
|
||||||
|
|
||||||
|
// 註冊到生命週期管理器
|
||||||
|
connectionLifecycleManager.registerConnection(connectionId, connection, metadata);
|
||||||
|
|
||||||
|
// 創建包裝的釋放函數
|
||||||
|
const release = () => {
|
||||||
|
connectionLifecycleManager.releaseConnection(connectionId);
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(`🔗 獲取智能連線: ${connectionId}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
connection,
|
||||||
|
connectionId,
|
||||||
|
release
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ 獲取智能連線失敗: ${connectionId}`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 執行智能查詢
|
||||||
|
public async executeQuery<T = any>(
|
||||||
|
sql: string,
|
||||||
|
params?: any[],
|
||||||
|
metadata?: {
|
||||||
|
userId?: string;
|
||||||
|
sessionId?: string;
|
||||||
|
requestId?: string;
|
||||||
|
}
|
||||||
|
): Promise<T[]> {
|
||||||
|
const { connection, connectionId, release } = await this.getSmartConnection(metadata);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 更新連線使用時間
|
||||||
|
connectionLifecycleManager.updateConnectionUsage(connectionId);
|
||||||
|
|
||||||
|
// 執行查詢
|
||||||
|
const [rows] = await connection.execute(sql, params);
|
||||||
|
return rows as T[];
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ 智能查詢失敗: ${connectionId}`, error);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
// 確保釋放連線
|
||||||
|
release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 執行智能單一查詢
|
||||||
|
public async executeQueryOne<T = any>(
|
||||||
|
sql: string,
|
||||||
|
params?: any[],
|
||||||
|
metadata?: {
|
||||||
|
userId?: string;
|
||||||
|
sessionId?: string;
|
||||||
|
requestId?: string;
|
||||||
|
}
|
||||||
|
): Promise<T | null> {
|
||||||
|
const results = await this.executeQuery<T>(sql, params, metadata);
|
||||||
|
return results.length > 0 ? results[0] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 執行智能插入
|
||||||
|
public async executeInsert(
|
||||||
|
sql: string,
|
||||||
|
params?: any[],
|
||||||
|
metadata?: {
|
||||||
|
userId?: string;
|
||||||
|
sessionId?: string;
|
||||||
|
requestId?: string;
|
||||||
|
}
|
||||||
|
): Promise<any> {
|
||||||
|
const { connection, connectionId, release } = await this.getSmartConnection(metadata);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 更新連線使用時間
|
||||||
|
connectionLifecycleManager.updateConnectionUsage(connectionId);
|
||||||
|
|
||||||
|
// 執行插入
|
||||||
|
const [result] = await connection.execute(sql, params);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ 智能插入失敗: ${connectionId}`, error);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
// 確保釋放連線
|
||||||
|
release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 執行智能更新
|
||||||
|
public async executeUpdate(
|
||||||
|
sql: string,
|
||||||
|
params?: any[],
|
||||||
|
metadata?: {
|
||||||
|
userId?: string;
|
||||||
|
sessionId?: string;
|
||||||
|
requestId?: string;
|
||||||
|
}
|
||||||
|
): Promise<any> {
|
||||||
|
const { connection, connectionId, release } = await this.getSmartConnection(metadata);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 更新連線使用時間
|
||||||
|
connectionLifecycleManager.updateConnectionUsage(connectionId);
|
||||||
|
|
||||||
|
// 執行更新
|
||||||
|
const [result] = await connection.execute(sql, params);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ 智能更新失敗: ${connectionId}`, error);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
// 確保釋放連線
|
||||||
|
release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 執行智能刪除
|
||||||
|
public async executeDelete(
|
||||||
|
sql: string,
|
||||||
|
params?: any[],
|
||||||
|
metadata?: {
|
||||||
|
userId?: string;
|
||||||
|
sessionId?: string;
|
||||||
|
requestId?: string;
|
||||||
|
}
|
||||||
|
): Promise<any> {
|
||||||
|
const { connection, connectionId, release } = await this.getSmartConnection(metadata);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 更新連線使用時間
|
||||||
|
connectionLifecycleManager.updateConnectionUsage(connectionId);
|
||||||
|
|
||||||
|
// 執行刪除
|
||||||
|
const [result] = await connection.execute(sql, params);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ 智能刪除失敗: ${connectionId}`, error);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
// 確保釋放連線
|
||||||
|
release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 獲取連線統計
|
||||||
|
public getConnectionStats() {
|
||||||
|
return connectionLifecycleManager.getConnectionStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 強制清理所有連線
|
||||||
|
public forceCleanup() {
|
||||||
|
connectionLifecycleManager.forceCleanupAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 設置清理參數
|
||||||
|
public setCleanupParams(maxIdleTime?: number, maxConnectionAge?: number) {
|
||||||
|
connectionLifecycleManager.setCleanupParams(maxIdleTime, maxConnectionAge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 導出單例實例
|
||||||
|
export const smartPool = SmartConnectionPool.getInstance();
|
153
lib/smart-ip-connection-pool.ts
Normal file
153
lib/smart-ip-connection-pool.ts
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
// =====================================================
|
||||||
|
// 智能 IP 連線池
|
||||||
|
// =====================================================
|
||||||
|
|
||||||
|
import { db } from './database';
|
||||||
|
import { ipConnectionManager } from './ip-based-connection-manager';
|
||||||
|
|
||||||
|
export class SmartIPConnectionPool {
|
||||||
|
private static instance: SmartIPConnectionPool;
|
||||||
|
private connectionCounter = 0;
|
||||||
|
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
public static getInstance(): SmartIPConnectionPool {
|
||||||
|
if (!SmartIPConnectionPool.instance) {
|
||||||
|
SmartIPConnectionPool.instance = new SmartIPConnectionPool();
|
||||||
|
}
|
||||||
|
return SmartIPConnectionPool.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 設置客戶端 IP
|
||||||
|
public setClientIP(ip: string) {
|
||||||
|
ipConnectionManager.setClientIP(ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 獲取智能連線(帶 IP 追蹤)
|
||||||
|
public async getSmartConnection(metadata?: {
|
||||||
|
userAgent?: string;
|
||||||
|
requestId?: string;
|
||||||
|
clientIP?: string;
|
||||||
|
}): Promise<{
|
||||||
|
connection: any;
|
||||||
|
connectionId: string;
|
||||||
|
release: () => void;
|
||||||
|
}> {
|
||||||
|
const connectionId = `conn_${++this.connectionCounter}_${Date.now()}`;
|
||||||
|
|
||||||
|
// 設置客戶端 IP
|
||||||
|
if (metadata?.clientIP) {
|
||||||
|
ipConnectionManager.setClientIP(metadata.clientIP);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 獲取實際連線
|
||||||
|
const connection = await db.getConnection();
|
||||||
|
|
||||||
|
// 註冊到 IP 連線管理器
|
||||||
|
ipConnectionManager.registerConnection(connectionId, {
|
||||||
|
userAgent: metadata?.userAgent,
|
||||||
|
requestId: metadata?.requestId
|
||||||
|
});
|
||||||
|
|
||||||
|
// 創建包裝的釋放函數
|
||||||
|
const release = () => {
|
||||||
|
ipConnectionManager.releaseConnection(connectionId);
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(`🔗 獲取智能 IP 連線: ${connectionId}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
connection,
|
||||||
|
connectionId,
|
||||||
|
release
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ 獲取智能 IP 連線失敗: ${connectionId}`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 執行智能查詢(帶 IP 追蹤)
|
||||||
|
public async executeQuery<T = any>(
|
||||||
|
sql: string,
|
||||||
|
params?: any[],
|
||||||
|
metadata?: {
|
||||||
|
userAgent?: string;
|
||||||
|
requestId?: string;
|
||||||
|
clientIP?: string;
|
||||||
|
}
|
||||||
|
): Promise<T[]> {
|
||||||
|
const { connection, connectionId, release } = await this.getSmartConnection(metadata);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 更新連線使用時間
|
||||||
|
ipConnectionManager.updateConnectionUsage(connectionId);
|
||||||
|
|
||||||
|
// 執行查詢
|
||||||
|
const [rows] = await connection.execute(sql, params);
|
||||||
|
return rows as T[];
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ 智能 IP 查詢失敗: ${connectionId}`, error);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
// 確保釋放連線
|
||||||
|
release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 執行智能單一查詢
|
||||||
|
public async executeQueryOne<T = any>(
|
||||||
|
sql: string,
|
||||||
|
params?: any[],
|
||||||
|
metadata?: {
|
||||||
|
userAgent?: string;
|
||||||
|
requestId?: string;
|
||||||
|
clientIP?: string;
|
||||||
|
}
|
||||||
|
): Promise<T | null> {
|
||||||
|
const results = await this.executeQuery<T>(sql, params, metadata);
|
||||||
|
return results.length > 0 ? results[0] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理當前 IP 的所有連線
|
||||||
|
public async cleanupCurrentIPConnections(): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
killedCount: number;
|
||||||
|
message: string;
|
||||||
|
}> {
|
||||||
|
return await ipConnectionManager.cleanupConnectionsByIP();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理指定 IP 的所有連線
|
||||||
|
public async cleanupIPConnections(targetIP: string): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
killedCount: number;
|
||||||
|
message: string;
|
||||||
|
}> {
|
||||||
|
return await ipConnectionManager.cleanupConnectionsByIP(targetIP);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 獲取當前 IP 的連線狀態
|
||||||
|
public async getCurrentIPConnections() {
|
||||||
|
return await ipConnectionManager.getConnectionsByIP();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 獲取指定 IP 的連線狀態
|
||||||
|
public async getIPConnections(targetIP: string) {
|
||||||
|
return await ipConnectionManager.getConnectionsByIP(targetIP);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 獲取本地連線統計
|
||||||
|
public getLocalConnectionStats() {
|
||||||
|
return ipConnectionManager.getLocalConnectionStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理本地追蹤的連線
|
||||||
|
public cleanupLocalConnections() {
|
||||||
|
return ipConnectionManager.cleanupLocalConnections();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 導出單例實例
|
||||||
|
export const smartIPPool = SmartIPConnectionPool.getInstance();
|
208
lib/smart-ip-detector.ts
Normal file
208
lib/smart-ip-detector.ts
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
// =====================================================
|
||||||
|
// 智能 IP 偵測器
|
||||||
|
// =====================================================
|
||||||
|
|
||||||
|
import { NextRequest } from 'next/server';
|
||||||
|
|
||||||
|
export interface IPDetectionResult {
|
||||||
|
detectedIP: string;
|
||||||
|
confidence: 'high' | 'medium' | 'low';
|
||||||
|
source: string;
|
||||||
|
allCandidates: string[];
|
||||||
|
isPublicIP: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SmartIPDetector {
|
||||||
|
private static instance: SmartIPDetector;
|
||||||
|
|
||||||
|
public static getInstance(): SmartIPDetector {
|
||||||
|
if (!SmartIPDetector.instance) {
|
||||||
|
SmartIPDetector.instance = new SmartIPDetector();
|
||||||
|
}
|
||||||
|
return SmartIPDetector.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 檢查是否為公網 IP
|
||||||
|
private isPublicIP(ip: string): boolean {
|
||||||
|
if (!ip || ip === 'unknown') return false;
|
||||||
|
|
||||||
|
// 本地地址
|
||||||
|
if (ip === '127.0.0.1' || ip === '::1' || ip === 'localhost') return false;
|
||||||
|
|
||||||
|
// 私有地址範圍
|
||||||
|
if (ip.startsWith('192.168.') ||
|
||||||
|
ip.startsWith('10.') ||
|
||||||
|
ip.startsWith('172.') ||
|
||||||
|
ip.startsWith('169.254.')) return false;
|
||||||
|
|
||||||
|
// IPv6 本地地址
|
||||||
|
if (ip.startsWith('fe80:') ||
|
||||||
|
ip.startsWith('::1') ||
|
||||||
|
ip.startsWith('::ffff:127.0.0.1')) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 從 x-forwarded-for 解析 IP
|
||||||
|
private parseForwardedFor(forwarded: string): string[] {
|
||||||
|
if (!forwarded) return [];
|
||||||
|
|
||||||
|
return forwarded
|
||||||
|
.split(',')
|
||||||
|
.map(ip => ip.trim())
|
||||||
|
.filter(ip => ip && ip !== 'unknown');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 智能偵測客戶端 IP
|
||||||
|
public detectClientIP(request: NextRequest): IPDetectionResult {
|
||||||
|
const candidates: string[] = [];
|
||||||
|
const sources: { [key: string]: string } = {};
|
||||||
|
|
||||||
|
// 1. 收集所有可能的 IP 來源
|
||||||
|
const headers = {
|
||||||
|
'x-forwarded-for': request.headers.get('x-forwarded-for'),
|
||||||
|
'x-real-ip': request.headers.get('x-real-ip'),
|
||||||
|
'cf-connecting-ip': request.headers.get('cf-connecting-ip'),
|
||||||
|
'x-client-ip': request.headers.get('x-client-ip'),
|
||||||
|
'x-forwarded': request.headers.get('x-forwarded'),
|
||||||
|
'x-cluster-client-ip': request.headers.get('x-cluster-client-ip'),
|
||||||
|
'x-original-forwarded-for': request.headers.get('x-original-forwarded-for'),
|
||||||
|
'x-remote-addr': request.headers.get('x-remote-addr'),
|
||||||
|
'remote-addr': request.headers.get('remote-addr'),
|
||||||
|
'client-ip': request.headers.get('client-ip'),
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2. 從各個標頭提取 IP
|
||||||
|
Object.entries(headers).forEach(([header, value]) => {
|
||||||
|
if (value) {
|
||||||
|
if (header === 'x-forwarded-for') {
|
||||||
|
const ips = this.parseForwardedFor(value);
|
||||||
|
ips.forEach(ip => {
|
||||||
|
candidates.push(ip);
|
||||||
|
sources[ip] = header;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
candidates.push(value);
|
||||||
|
sources[value] = header;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. 添加 NextRequest 的 IP (如果可用)
|
||||||
|
const nextIP = (request as any).ip;
|
||||||
|
if (nextIP) {
|
||||||
|
candidates.push(nextIP);
|
||||||
|
sources[nextIP] = 'next-request';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 去重並過濾
|
||||||
|
const uniqueCandidates = [...new Set(candidates)].filter(ip =>
|
||||||
|
ip && ip !== 'unknown' && ip !== '::1' && ip !== '127.0.0.1'
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('🔍 IP 偵測候選:', {
|
||||||
|
candidates: uniqueCandidates,
|
||||||
|
sources: sources,
|
||||||
|
allHeaders: Object.fromEntries(request.headers.entries())
|
||||||
|
});
|
||||||
|
|
||||||
|
// 5. 智能選擇最佳 IP
|
||||||
|
let selectedIP = 'unknown';
|
||||||
|
let confidence: 'high' | 'medium' | 'low' = 'low';
|
||||||
|
let source = 'unknown';
|
||||||
|
|
||||||
|
// 優先級 1: Cloudflare IP (最高可信度)
|
||||||
|
const cfIP = uniqueCandidates.find(ip => sources[ip] === 'cf-connecting-ip');
|
||||||
|
if (cfIP && this.isPublicIP(cfIP)) {
|
||||||
|
selectedIP = cfIP;
|
||||||
|
confidence = 'high';
|
||||||
|
source = 'cf-connecting-ip';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 優先級 2: 其他代理標頭中的公網 IP
|
||||||
|
if (selectedIP === 'unknown') {
|
||||||
|
const proxyHeaders = ['x-real-ip', 'x-client-ip', 'x-cluster-client-ip'];
|
||||||
|
for (const header of proxyHeaders) {
|
||||||
|
const ip = uniqueCandidates.find(candidate => sources[candidate] === header);
|
||||||
|
if (ip && this.isPublicIP(ip)) {
|
||||||
|
selectedIP = ip;
|
||||||
|
confidence = 'high';
|
||||||
|
source = header;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 優先級 3: x-forwarded-for 中的第一個公網 IP
|
||||||
|
if (selectedIP === 'unknown') {
|
||||||
|
const forwardedIPs = uniqueCandidates.filter(ip => sources[ip] === 'x-forwarded-for');
|
||||||
|
const publicForwardedIP = forwardedIPs.find(ip => this.isPublicIP(ip));
|
||||||
|
if (publicForwardedIP) {
|
||||||
|
selectedIP = publicForwardedIP;
|
||||||
|
confidence = 'medium';
|
||||||
|
source = 'x-forwarded-for';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 優先級 4: 任何公網 IP
|
||||||
|
if (selectedIP === 'unknown') {
|
||||||
|
const publicIP = uniqueCandidates.find(ip => this.isPublicIP(ip));
|
||||||
|
if (publicIP) {
|
||||||
|
selectedIP = publicIP;
|
||||||
|
confidence = 'medium';
|
||||||
|
source = sources[publicIP] || 'unknown';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 優先級 5: 任何非本地 IP
|
||||||
|
if (selectedIP === 'unknown') {
|
||||||
|
const nonLocalIP = uniqueCandidates.find(ip =>
|
||||||
|
ip !== '::1' && ip !== '127.0.0.1' && !ip.startsWith('192.168.') &&
|
||||||
|
!ip.startsWith('10.') && !ip.startsWith('172.')
|
||||||
|
);
|
||||||
|
if (nonLocalIP) {
|
||||||
|
selectedIP = nonLocalIP;
|
||||||
|
confidence = 'low';
|
||||||
|
source = sources[nonLocalIP] || 'unknown';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 優先級 6: 第一個候選 IP
|
||||||
|
if (selectedIP === 'unknown' && uniqueCandidates.length > 0) {
|
||||||
|
selectedIP = uniqueCandidates[0];
|
||||||
|
confidence = 'low';
|
||||||
|
source = sources[selectedIP] || 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: IPDetectionResult = {
|
||||||
|
detectedIP: selectedIP,
|
||||||
|
confidence,
|
||||||
|
source,
|
||||||
|
allCandidates: uniqueCandidates,
|
||||||
|
isPublicIP: this.isPublicIP(selectedIP)
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('🎯 IP 偵測結果:', result);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 驗證 IP 格式
|
||||||
|
public isValidIP(ip: string): boolean {
|
||||||
|
if (!ip || ip === 'unknown') return false;
|
||||||
|
|
||||||
|
// IPv4 格式檢查
|
||||||
|
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
|
||||||
|
if (ipv4Regex.test(ip)) {
|
||||||
|
const parts = ip.split('.').map(Number);
|
||||||
|
return parts.every(part => part >= 0 && part <= 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPv6 格式檢查(簡化)
|
||||||
|
const ipv6Regex = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/;
|
||||||
|
return ipv6Regex.test(ip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 導出單例實例
|
||||||
|
export const smartIPDetector = SmartIPDetector.getInstance();
|
186
lib/ultimate-connection-killer.ts
Normal file
186
lib/ultimate-connection-killer.ts
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
// =====================================================
|
||||||
|
// 終極連線殺手 - 強制清理所有連線
|
||||||
|
// =====================================================
|
||||||
|
|
||||||
|
import mysql from 'mysql2/promise';
|
||||||
|
|
||||||
|
export class UltimateConnectionKiller {
|
||||||
|
private static instance: UltimateConnectionKiller;
|
||||||
|
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
public static getInstance(): UltimateConnectionKiller {
|
||||||
|
if (!UltimateConnectionKiller.instance) {
|
||||||
|
UltimateConnectionKiller.instance = new UltimateConnectionKiller();
|
||||||
|
}
|
||||||
|
return UltimateConnectionKiller.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 終極清理 - 殺死所有連線
|
||||||
|
public async ultimateKill() {
|
||||||
|
console.log('💀 執行終極連線清理...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 建立一個臨時連線來執行清理
|
||||||
|
const tempConnection = await mysql.createConnection({
|
||||||
|
host: process.env.DB_HOST || '122.100.99.161',
|
||||||
|
port: parseInt(process.env.DB_PORT || '43306'),
|
||||||
|
user: process.env.DB_USER || 'A999',
|
||||||
|
password: process.env.DB_PASSWORD || '1023',
|
||||||
|
database: process.env.DB_NAME || 'db_AI_Platform',
|
||||||
|
charset: 'utf8mb4',
|
||||||
|
timezone: '+08:00',
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 2. 獲取所有連線
|
||||||
|
const [connections] = await tempConnection.execute(`
|
||||||
|
SELECT ID, USER, HOST, DB, COMMAND, TIME, STATE, INFO
|
||||||
|
FROM information_schema.PROCESSLIST
|
||||||
|
WHERE USER = ?
|
||||||
|
ORDER BY TIME DESC
|
||||||
|
`, [process.env.DB_USER || 'A999']);
|
||||||
|
|
||||||
|
console.log(`🔍 找到 ${connections.length} 個連線需要清理`);
|
||||||
|
|
||||||
|
// 3. 殺死所有連線(除了當前連線)
|
||||||
|
let killedCount = 0;
|
||||||
|
for (const conn of connections) {
|
||||||
|
if (conn.ID !== tempConnection.threadId) {
|
||||||
|
try {
|
||||||
|
await tempConnection.execute(`KILL ${conn.ID}`);
|
||||||
|
console.log(`💀 已殺死連線 ${conn.ID} (用戶: ${conn.USER}, 時間: ${conn.TIME}s)`);
|
||||||
|
killedCount++;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.log(`⚠️ 無法殺死連線 ${conn.ID}: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✅ 已殺死 ${killedCount} 個連線`);
|
||||||
|
|
||||||
|
// 4. 等待連線完全關閉
|
||||||
|
console.log('⏳ 等待連線完全關閉...');
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||||
|
|
||||||
|
// 5. 再次檢查狀態
|
||||||
|
const [finalConnections] = await tempConnection.execute(`
|
||||||
|
SELECT COUNT(*) as count FROM information_schema.PROCESSLIST
|
||||||
|
WHERE USER = ?
|
||||||
|
`, [process.env.DB_USER || 'A999']);
|
||||||
|
|
||||||
|
const remainingConnections = finalConnections[0].count;
|
||||||
|
console.log(`📊 清理後剩餘連線: ${remainingConnections}`);
|
||||||
|
|
||||||
|
if (remainingConnections <= 1) {
|
||||||
|
console.log('🎉 連線清理成功!');
|
||||||
|
} else {
|
||||||
|
console.warn(`⚠️ 仍有 ${remainingConnections - 1} 個連線未清理`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
killedCount,
|
||||||
|
remainingConnections: remainingConnections - 1, // 減去當前連線
|
||||||
|
message: `已殺死 ${killedCount} 個連線,剩餘 ${remainingConnections - 1} 個`
|
||||||
|
};
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
// 6. 關閉臨時連線
|
||||||
|
await tempConnection.end();
|
||||||
|
console.log('🔌 臨時連線已關閉');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 終極清理失敗:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : '未知錯誤'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 檢查連線狀態
|
||||||
|
public async checkStatus() {
|
||||||
|
try {
|
||||||
|
const tempConnection = await mysql.createConnection({
|
||||||
|
host: process.env.DB_HOST || '122.100.99.161',
|
||||||
|
port: parseInt(process.env.DB_PORT || '43306'),
|
||||||
|
user: process.env.DB_USER || 'A999',
|
||||||
|
password: process.env.DB_PASSWORD || '1023',
|
||||||
|
database: process.env.DB_NAME || 'db_AI_Platform',
|
||||||
|
charset: 'utf8mb4',
|
||||||
|
timezone: '+08:00',
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 獲取連線統計
|
||||||
|
const [statusResult] = await tempConnection.execute(`
|
||||||
|
SHOW STATUS LIKE 'Threads_connected'
|
||||||
|
`);
|
||||||
|
|
||||||
|
const [maxConnResult] = await tempConnection.execute(`
|
||||||
|
SHOW VARIABLES LIKE 'max_connections'
|
||||||
|
`);
|
||||||
|
|
||||||
|
const [connections] = await tempConnection.execute(`
|
||||||
|
SELECT ID, USER, HOST, DB, COMMAND, TIME, STATE, INFO
|
||||||
|
FROM information_schema.PROCESSLIST
|
||||||
|
WHERE USER = ?
|
||||||
|
ORDER BY TIME DESC
|
||||||
|
`, [process.env.DB_USER || 'A999']);
|
||||||
|
|
||||||
|
const currentConnections = statusResult[0]?.Value || 0;
|
||||||
|
const maxConnections = maxConnResult[0]?.Value || 100;
|
||||||
|
const usagePercentage = (currentConnections / maxConnections) * 100;
|
||||||
|
|
||||||
|
return {
|
||||||
|
currentConnections: parseInt(currentConnections),
|
||||||
|
maxConnections: parseInt(maxConnections),
|
||||||
|
usagePercentage,
|
||||||
|
connectionDetails: connections,
|
||||||
|
connectionCount: connections.length
|
||||||
|
};
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
await tempConnection.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 檢查狀態失敗:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 強制重啟資料庫連線
|
||||||
|
public async forceRestart() {
|
||||||
|
console.log('🔄 強制重啟資料庫連線...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 先殺死所有連線
|
||||||
|
await this.ultimateKill();
|
||||||
|
|
||||||
|
// 2. 等待一段時間
|
||||||
|
console.log('⏳ 等待系統穩定...');
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||||
|
|
||||||
|
// 3. 檢查狀態
|
||||||
|
const status = await this.checkStatus();
|
||||||
|
|
||||||
|
if (status && status.connectionCount <= 1) {
|
||||||
|
console.log('✅ 資料庫連線重啟成功');
|
||||||
|
return { success: true, message: '資料庫連線重啟成功' };
|
||||||
|
} else {
|
||||||
|
console.warn('⚠️ 資料庫連線重啟後仍有連線存在');
|
||||||
|
return { success: false, message: '重啟後仍有連線存在' };
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 強制重啟失敗:', error);
|
||||||
|
return { success: false, error: error instanceof Error ? error.message : '未知錯誤' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 導出單例實例
|
||||||
|
export const ultimateKiller = UltimateConnectionKiller.getInstance();
|
@@ -1,50 +0,0 @@
|
|||||||
-- =====================================================
|
|
||||||
-- 插入團隊成員測試數據
|
|
||||||
-- =====================================================
|
|
||||||
|
|
||||||
-- 首先查看現有的團隊
|
|
||||||
SELECT '=== 現有團隊 ===' as info;
|
|
||||||
SELECT id, name, leader_id FROM teams WHERE is_active = 1;
|
|
||||||
|
|
||||||
-- 查看現有用戶
|
|
||||||
SELECT '=== 現有用戶 ===' as info;
|
|
||||||
SELECT id, name, department FROM users WHERE status = 'active' LIMIT 10;
|
|
||||||
|
|
||||||
-- 為每個團隊插入成員數據
|
|
||||||
-- 團隊 1: aaa (ID: t1757702332911zcl6iafq1)
|
|
||||||
INSERT IGNORE INTO team_members (id, team_id, user_id, role, joined_at) VALUES
|
|
||||||
('tm_1_1', 't1757702332911zcl6iafq1', 'db19b491-8f63-44b5-a28a-1f8eeb4fdd3c', '隊長', NOW()),
|
|
||||||
('tm_1_2', 't1757702332911zcl6iafq1', 'db19b491-8f63-44b5-a28a-1f8eeb4fdd3c', '成員', NOW());
|
|
||||||
|
|
||||||
-- 如果有其他團隊,請按照相同格式添加
|
|
||||||
-- 團隊 2: (如果有的話)
|
|
||||||
-- INSERT IGNORE INTO team_members (id, team_id, user_id, role, joined_at) VALUES
|
|
||||||
-- ('tm_2_1', 'team_id_2', 'user_id_2', '隊長', NOW()),
|
|
||||||
-- ('tm_2_2', 'team_id_2', 'user_id_3', '成員', NOW());
|
|
||||||
|
|
||||||
-- 驗證插入結果
|
|
||||||
SELECT '=== 團隊成員插入結果 ===' as info;
|
|
||||||
SELECT
|
|
||||||
tm.id,
|
|
||||||
tm.team_id,
|
|
||||||
t.name as team_name,
|
|
||||||
tm.user_id,
|
|
||||||
u.name as user_name,
|
|
||||||
tm.role,
|
|
||||||
tm.joined_at
|
|
||||||
FROM team_members tm
|
|
||||||
LEFT JOIN teams t ON tm.team_id = t.id
|
|
||||||
LEFT JOIN users u ON tm.user_id = u.id
|
|
||||||
ORDER BY tm.team_id, tm.role;
|
|
||||||
|
|
||||||
-- 檢查團隊成員統計
|
|
||||||
SELECT '=== 團隊成員統計 ===' as info;
|
|
||||||
SELECT
|
|
||||||
t.id,
|
|
||||||
t.name as team_name,
|
|
||||||
COUNT(tm.id) as member_count
|
|
||||||
FROM teams t
|
|
||||||
LEFT JOIN team_members tm ON t.id = tm.team_id
|
|
||||||
WHERE t.is_active = 1
|
|
||||||
GROUP BY t.id, t.name
|
|
||||||
ORDER BY t.name;
|
|
@@ -1,82 +0,0 @@
|
|||||||
const mysql = require('mysql2/promise');
|
|
||||||
|
|
||||||
async function insertTeamMembers() {
|
|
||||||
const connection = await mysql.createConnection({
|
|
||||||
host: 'mysql.theaken.com',
|
|
||||||
port: 33306,
|
|
||||||
user: 'AI_Platform',
|
|
||||||
password: 'Aa123456',
|
|
||||||
database: 'db_AI_Platform',
|
|
||||||
charset: 'utf8mb4',
|
|
||||||
timezone: '+08:00'
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log('🔍 開始插入團隊成員測試數據...');
|
|
||||||
|
|
||||||
// 查看現有團隊
|
|
||||||
const [teams] = await connection.execute('SELECT id, name, leader_id FROM teams WHERE is_active = 1 LIMIT 5');
|
|
||||||
console.log('現有團隊:', teams);
|
|
||||||
|
|
||||||
// 查看現有用戶
|
|
||||||
const [users] = await connection.execute('SELECT id, name FROM users WHERE status = "active" LIMIT 5');
|
|
||||||
console.log('現有用戶:', users);
|
|
||||||
|
|
||||||
if (teams.length > 0 && users.length > 0) {
|
|
||||||
// 為每個團隊插入成員
|
|
||||||
for (let i = 0; i < Math.min(teams.length, 3); i++) {
|
|
||||||
const team = teams[i];
|
|
||||||
const teamId = team.id;
|
|
||||||
|
|
||||||
// 插入隊長(使用團隊的 leader_id)
|
|
||||||
await connection.execute(
|
|
||||||
'INSERT IGNORE INTO team_members (id, team_id, user_id, role, joined_at) VALUES (?, ?, ?, ?, NOW())',
|
|
||||||
[`tm_${Date.now()}_${i}_1`, teamId, team.leader_id, '隊長']
|
|
||||||
);
|
|
||||||
|
|
||||||
// 插入成員(使用其他用戶)
|
|
||||||
for (let j = 1; j < Math.min(users.length, 3); j++) {
|
|
||||||
if (users[j].id !== team.leader_id) {
|
|
||||||
await connection.execute(
|
|
||||||
'INSERT IGNORE INTO team_members (id, team_id, user_id, role, joined_at) VALUES (?, ?, ?, ?, NOW())',
|
|
||||||
[`tm_${Date.now()}_${i}_${j+1}`, teamId, users[j].id, '成員']
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`✅ 團隊 ${team.name} 成員插入成功`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 驗證結果
|
|
||||||
const [members] = await connection.execute(`
|
|
||||||
SELECT tm.*, t.name as team_name, u.name as user_name
|
|
||||||
FROM team_members tm
|
|
||||||
LEFT JOIN teams t ON tm.team_id = t.id
|
|
||||||
LEFT JOIN users u ON tm.user_id = u.id
|
|
||||||
ORDER BY tm.team_id
|
|
||||||
`);
|
|
||||||
console.log('📊 團隊成員統計:', members);
|
|
||||||
|
|
||||||
// 檢查團隊成員數量
|
|
||||||
const [counts] = await connection.execute(`
|
|
||||||
SELECT
|
|
||||||
t.id,
|
|
||||||
t.name as team_name,
|
|
||||||
COUNT(tm.id) as member_count
|
|
||||||
FROM teams t
|
|
||||||
LEFT JOIN team_members tm ON t.id = tm.team_id
|
|
||||||
WHERE t.is_active = 1
|
|
||||||
GROUP BY t.id, t.name
|
|
||||||
ORDER BY t.name
|
|
||||||
`);
|
|
||||||
console.log('📈 團隊成員數量:', counts);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ 錯誤:', error);
|
|
||||||
} finally {
|
|
||||||
await connection.end();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
insertTeamMembers();
|
|
@@ -1,70 +0,0 @@
|
|||||||
const mysql = require('mysql2/promise');
|
|
||||||
|
|
||||||
async function testTeamData() {
|
|
||||||
const connection = await mysql.createConnection({
|
|
||||||
host: 'mysql.theaken.com',
|
|
||||||
port: 33306,
|
|
||||||
user: 'AI_Platform',
|
|
||||||
password: 'Aa123456',
|
|
||||||
database: 'db_AI_Platform',
|
|
||||||
charset: 'utf8mb4',
|
|
||||||
timezone: '+08:00'
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log('🔍 測試團隊數據查詢...');
|
|
||||||
|
|
||||||
// 使用與 getAllTeams 相同的查詢
|
|
||||||
const [results] = await connection.execute(`
|
|
||||||
SELECT t.*,
|
|
||||||
u.name as leader_name,
|
|
||||||
u.phone as leader_phone,
|
|
||||||
t.leader_id as leader,
|
|
||||||
COUNT(DISTINCT tm.id) as member_count,
|
|
||||||
COUNT(DISTINCT a.id) as app_count,
|
|
||||||
t.created_at as submissionDate
|
|
||||||
FROM teams t
|
|
||||||
LEFT JOIN users u ON t.leader_id = u.id
|
|
||||||
LEFT JOIN team_members tm ON t.id = tm.team_id
|
|
||||||
LEFT JOIN apps a ON t.id = a.team_id AND a.is_active = 1
|
|
||||||
WHERE t.is_active = 1
|
|
||||||
GROUP BY t.id, t.name, t.leader_id, t.department, t.contact_email, t.total_likes, t.is_active, t.created_at, t.updated_at, u.name, u.phone
|
|
||||||
ORDER BY t.created_at DESC
|
|
||||||
LIMIT 5
|
|
||||||
`);
|
|
||||||
|
|
||||||
console.log('📊 查詢結果:');
|
|
||||||
results.forEach((team, index) => {
|
|
||||||
console.log(`\n團隊 ${index + 1}:`);
|
|
||||||
console.log(` 名稱: ${team.name}`);
|
|
||||||
console.log(` 隊長ID: ${team.leader}`);
|
|
||||||
console.log(` 隊長姓名: ${team.leader_name || 'NULL'}`);
|
|
||||||
console.log(` 成員數量: ${team.member_count || 0}`);
|
|
||||||
console.log(` 提交日期: ${team.submissionDate || 'NULL'}`);
|
|
||||||
console.log(` 部門: ${team.department}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 檢查特定團隊的成員
|
|
||||||
if (results.length > 0) {
|
|
||||||
const teamId = results[0].id;
|
|
||||||
const [members] = await connection.execute(`
|
|
||||||
SELECT tm.*, u.name as user_name
|
|
||||||
FROM team_members tm
|
|
||||||
LEFT JOIN users u ON tm.user_id = u.id
|
|
||||||
WHERE tm.team_id = ?
|
|
||||||
`, [teamId]);
|
|
||||||
|
|
||||||
console.log(`\n🔍 團隊 ${results[0].name} 的成員:`);
|
|
||||||
members.forEach(member => {
|
|
||||||
console.log(` - ${member.user_name} (${member.role})`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ 錯誤:', error);
|
|
||||||
} finally {
|
|
||||||
await connection.end();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
testTeamData();
|
|
93
scripts/ultimate-kill.js
Normal file
93
scripts/ultimate-kill.js
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
// =====================================================
|
||||||
|
// 終極連線清理腳本
|
||||||
|
// =====================================================
|
||||||
|
|
||||||
|
const mysql = require('mysql2/promise');
|
||||||
|
|
||||||
|
async function ultimateKill() {
|
||||||
|
console.log('💀 執行終極連線清理腳本...');
|
||||||
|
|
||||||
|
let connection = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 建立連線
|
||||||
|
connection = await mysql.createConnection({
|
||||||
|
host: process.env.DB_HOST || '122.100.99.161',
|
||||||
|
port: parseInt(process.env.DB_PORT || '43306'),
|
||||||
|
user: process.env.DB_USER || 'A999',
|
||||||
|
password: process.env.DB_PASSWORD || '1023',
|
||||||
|
database: process.env.DB_NAME || 'db_AI_Platform',
|
||||||
|
charset: 'utf8mb4',
|
||||||
|
timezone: '+08:00',
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ 已連接到資料庫');
|
||||||
|
|
||||||
|
// 獲取所有連線
|
||||||
|
const [connections] = await connection.execute(`
|
||||||
|
SELECT ID, USER, HOST, DB, COMMAND, TIME, STATE, INFO
|
||||||
|
FROM information_schema.PROCESSLIST
|
||||||
|
WHERE USER = ?
|
||||||
|
ORDER BY TIME DESC
|
||||||
|
`, [process.env.DB_USER || 'A999']);
|
||||||
|
|
||||||
|
console.log(`🔍 找到 ${connections.length} 個連線需要清理`);
|
||||||
|
|
||||||
|
// 顯示連線詳情
|
||||||
|
connections.forEach((conn, index) => {
|
||||||
|
console.log(`${index + 1}. ID: ${conn.ID}, 用戶: ${conn.USER}, 時間: ${conn.TIME}s, 狀態: ${conn.STATE}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 殺死所有連線(除了當前連線)
|
||||||
|
let killedCount = 0;
|
||||||
|
for (const conn of connections) {
|
||||||
|
if (conn.ID !== connection.threadId) {
|
||||||
|
try {
|
||||||
|
await connection.execute(`KILL ${conn.ID}`);
|
||||||
|
console.log(`💀 已殺死連線 ${conn.ID}`);
|
||||||
|
killedCount++;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`⚠️ 無法殺死連線 ${conn.ID}: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✅ 已殺死 ${killedCount} 個連線`);
|
||||||
|
|
||||||
|
// 等待連線關閉
|
||||||
|
console.log('⏳ 等待連線完全關閉...');
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||||
|
|
||||||
|
// 檢查最終狀態
|
||||||
|
const [finalConnections] = await connection.execute(`
|
||||||
|
SELECT COUNT(*) as count FROM information_schema.PROCESSLIST
|
||||||
|
WHERE USER = ?
|
||||||
|
`, [process.env.DB_USER || 'A999']);
|
||||||
|
|
||||||
|
const remainingConnections = finalConnections[0].count;
|
||||||
|
console.log(`📊 清理後剩餘連線: ${remainingConnections}`);
|
||||||
|
|
||||||
|
if (remainingConnections <= 1) {
|
||||||
|
console.log('🎉 連線清理成功!');
|
||||||
|
} else {
|
||||||
|
console.warn(`⚠️ 仍有 ${remainingConnections - 1} 個連線未清理`);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 終極清理失敗:', error);
|
||||||
|
} finally {
|
||||||
|
if (connection) {
|
||||||
|
await connection.end();
|
||||||
|
console.log('🔌 連線已關閉');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 執行清理
|
||||||
|
ultimateKill().then(() => {
|
||||||
|
console.log('✅ 腳本執行完成');
|
||||||
|
process.exit(0);
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error('❌ 腳本執行失敗:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
Reference in New Issue
Block a user