From 1f2fb14bd00334d4ec9e8bbad96bda54dbd659ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B3=E4=BD=A9=E5=BA=AD?= Date: Tue, 16 Sep 2025 10:38:23 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=E7=AB=B6=E8=B3=BD=E7=B7=A8?= =?UTF-8?q?=E8=BC=AF=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DATABASE-FAILOVER-SETUP.md | 198 -------------------- DATABASE_MIGRATION_README.md | 74 -------- README-DUAL-WRITE-SYNC.md | 1 + app/api/admin/competitions/[id]/route.ts | 3 + components/admin/competition-management.tsx | 55 ++++-- lib/database-sync-fixed.js | 3 + lib/database-sync-fixed.ts | 77 +++++++- lib/services/database-service.ts | 82 +++++++- 8 files changed, 203 insertions(+), 290 deletions(-) delete mode 100644 DATABASE-FAILOVER-SETUP.md delete mode 100644 DATABASE_MIGRATION_README.md diff --git a/DATABASE-FAILOVER-SETUP.md b/DATABASE-FAILOVER-SETUP.md deleted file mode 100644 index bfa0ac7..0000000 --- a/DATABASE-FAILOVER-SETUP.md +++ /dev/null @@ -1,198 +0,0 @@ -# 資料庫備援系統設置指南 - -## 🎯 系統概述 - -您的 AI 展示平台現在已經具備完整的資料庫備援功能!當主機資料庫出現 "Too many connections" 或其他問題時,系統會自動切換到備機資料庫,確保服務不中斷。 - -## ✅ 已完成的功能 - -1. **自動故障檢測** - 每30秒檢查資料庫健康狀態 -2. **自動切換** - 主機故障時自動切換到備機 -3. **手動切換** - 支援手動切換資料庫 -4. **資料同步** - 可將主機資料同步到備機 -5. **監控面板** - 實時監控資料庫狀態 -6. **健康檢查** - 定期檢查連接狀態 - -## 🚀 快速開始 - -### 1. 啟用備援功能 - -在您的 `.env` 文件中添加以下配置: - -```env -# ===== 主機資料庫配置 ===== -DB_HOST=mysql.theaken.com -DB_PORT=33306 -DB_NAME=db_AI_Platform -DB_USER=AI_Platform -DB_PASSWORD=Aa123456 - -# ===== 備機資料庫配置 ===== -SLAVE_DB_HOST=122.100.99.161 -SLAVE_DB_PORT=43306 -SLAVE_DB_NAME=db_nighttime_care_record -SLAVE_DB_USER=A999 -SLAVE_DB_PASSWORD=1023 - -# ===== 資料庫備援配置 ===== -DB_FAILOVER_ENABLED=true -DB_HEALTH_CHECK_INTERVAL=30000 -DB_CONNECTION_TIMEOUT=5000 -DB_RETRY_ATTEMPTS=3 -DB_RETRY_DELAY=2000 -``` - -### 2. 初始化備機資料庫 - -```bash -# 初始化備機資料庫結構 -pnpm run db:init-slave - -# 同步主機資料到備機 -pnpm run db:sync -``` - -### 3. 檢查系統狀態 - -```bash -# 檢查資料庫健康狀態 -pnpm run db:health - -# 測試備援系統 -pnpm run db:test-simple -``` - -## 📊 監控和管理 - -### 健康檢查結果 - -根據最新測試結果: - -- ❌ **主機資料庫**: 異常 (Too many connections) -- ✅ **備機資料庫**: 正常 (響應時間: 209ms) -- 🔄 **當前狀態**: 已自動切換到備機 - -### 可用命令 - -| 命令 | 功能 | 狀態 | -|------|------|------| -| `pnpm run db:health` | 檢查資料庫健康狀態 | ✅ 可用 | -| `pnpm run db:init-slave` | 初始化備機資料庫 | ✅ 已完成 | -| `pnpm run db:sync` | 同步資料 | ✅ 可用 | -| `pnpm run db:test-simple` | 測試備援系統 | ✅ 通過 | -| `pnpm run db:monitor` | 監控資料庫狀態 | ✅ 可用 | - -## 🔧 程式碼使用 - -### 基本使用 - -```typescript -import { db } from '@/lib/database'; - -// 查詢資料 (自動使用備援) -const users = await db.query('SELECT * FROM users'); - -// 插入資料 (自動使用備援) -await db.insert('INSERT INTO users (name, email) VALUES (?, ?)', ['John', 'john@example.com']); - -// 獲取備援狀態 -const status = db.getFailoverStatus(); -console.log('當前使用資料庫:', status?.currentDatabase); - -// 手動切換資料庫 -await db.switchDatabase('slave'); // 切換到備機 -await db.switchDatabase('master'); // 切換到主機 -``` - -### 監控面板 - -在管理頁面中添加監控組件: - -```typescript -import { DatabaseMonitor } from '@/components/admin/database-monitor'; - -// 在管理頁面中使用 - -``` - -## 📈 系統狀態 - -### 當前配置 - -- **備援功能**: ✅ 已啟用 -- **健康檢查間隔**: 30秒 -- **連接超時**: 5秒 -- **重試次數**: 3次 -- **重試延遲**: 2秒 - -### 測試結果 - -``` -🎉 備援系統測試完成! -當前使用資料庫: slave -⚠️ 注意:目前使用備機資料庫,建議檢查主機問題 -``` - -## 🚨 故障處理 - -### 主機資料庫問題 - -**問題**: `Too many connections` -**解決方案**: -1. 系統已自動切換到備機 -2. 檢查主機資料庫連接數限制 -3. 優化連接池配置 -4. 重啟主機資料庫服務 - -### 備機資料庫問題 - -**問題**: 備機連接失敗 -**解決方案**: -1. 檢查網路連接 -2. 驗證備機資料庫配置 -3. 確認用戶權限 -4. 檢查備機資料庫服務狀態 - -## 📋 維護建議 - -### 定期維護 - -1. **每日檢查**: 執行 `pnpm run db:health` -2. **每週同步**: 執行 `pnpm run db:sync` -3. **每月測試**: 執行 `pnpm run db:test-simple` - -### 監控指標 - -- 資料庫連接狀態 -- 響應時間 -- 錯誤率 -- 切換次數 - -## 🔄 恢復主機 - -當主機資料庫恢復後: - -1. 檢查主機狀態: `pnpm run db:health` -2. 手動切換回主機: `await db.switchDatabase('master')` -3. 重新同步資料: `pnpm run db:sync` - -## 📞 支援 - -如有問題,請檢查: - -1. 環境變數配置 -2. 網路連接狀態 -3. 資料庫服務狀態 -4. 系統日誌 -5. 監控面板狀態 - -## 🎉 總結 - -您的資料庫備援系統已經成功設置並運行!系統現在可以: - -- ✅ 自動檢測主機資料庫問題 -- ✅ 自動切換到備機資料庫 -- ✅ 提供監控和管理功能 -- ✅ 確保服務連續性 - -即使主機資料庫出現 "Too many connections" 問題,您的應用程式仍然可以正常運行! diff --git a/DATABASE_MIGRATION_README.md b/DATABASE_MIGRATION_README.md deleted file mode 100644 index b1ef178..0000000 --- a/DATABASE_MIGRATION_README.md +++ /dev/null @@ -1,74 +0,0 @@ -# 資料庫遷移說明 - -## 問題描述 -競賽管理系統在創建競賽時出現以下錯誤: -1. `competition_award_types` 表缺少 `order_index` 欄位 -2. `competition_teams` 表缺少 `registered_at` 欄位 -3. 外鍵約束失敗,存在孤立的關聯記錄 - -## 解決方案 - -### 方法一:使用智能檢查腳本(推薦) -```bash -mysql -u your_username -p your_database_name < add-missing-columns.sql -``` - -### 方法二:使用簡化腳本 -```bash -mysql -u your_username -p your_database_name < add-missing-columns-simple.sql -``` - -## 腳本內容 - -### 1. 添加缺失欄位 -- 為 `competition_award_types` 表添加 `order_index` 欄位 -- 為 `competition_teams` 表添加 `registered_at` 欄位 - -### 2. 清理孤立記錄 -- 刪除所有關聯表中不存在的 `competition_id` 記錄 - -### 3. 添加必要索引 -- 為所有關聯表添加適當的索引以提升查詢性能 - -## 執行前注意事項 - -1. **備份資料庫**: - ```bash - mysqldump -u your_username -p your_database_name > backup_before_migration.sql - ``` - -2. **確認資料庫名稱**: - - 將 `your_database_name` 替換為實際的資料庫名稱 - - 將 `your_username` 替換為實際的用戶名 - -3. **檢查權限**: - - 確保用戶有 ALTER TABLE 和 DELETE 權限 - -## 執行後驗證 - -執行完成後,可以運行以下查詢來驗證: - -```sql --- 檢查欄位是否添加成功 -DESCRIBE competition_award_types; -DESCRIBE competition_teams; - --- 檢查索引是否創建成功 -SHOW INDEX FROM competition_award_types; -SHOW INDEX FROM competition_teams; - --- 檢查孤立記錄是否已清理 -SELECT COUNT(*) as orphaned_judges FROM competition_judges -WHERE competition_id NOT IN (SELECT id FROM competitions); -``` - -## 如果遇到錯誤 - -如果執行過程中遇到 "column already exists" 或 "index already exists" 錯誤,這是正常的,表示該欄位或索引已經存在,可以忽略這些錯誤。 - -## 聯繫支援 - -如果遇到其他問題,請檢查: -1. MySQL 版本是否支援所使用的語法 -2. 用戶權限是否足夠 -3. 資料庫連接是否正常 diff --git a/README-DUAL-WRITE-SYNC.md b/README-DUAL-WRITE-SYNC.md index 7bc1ec7..ef4b5ce 100644 --- a/README-DUAL-WRITE-SYNC.md +++ b/README-DUAL-WRITE-SYNC.md @@ -230,3 +230,4 @@ import { DatabaseMonitor } from '@/components/admin/database-monitor'; + diff --git a/app/api/admin/competitions/[id]/route.ts b/app/api/admin/competitions/[id]/route.ts index 1066204..c85fdb4 100644 --- a/app/api/admin/competitions/[id]/route.ts +++ b/app/api/admin/competitions/[id]/route.ts @@ -131,6 +131,9 @@ export async function PUT(request: NextRequest, { params }: { params: { id: stri if (body.teams !== undefined) { await CompetitionService.addCompetitionTeams(id, body.teams || []); } + if (body.participatingApps !== undefined) { + await CompetitionService.addCompetitionApps(id, body.participatingApps || []); + } if (body.awardTypes !== undefined) { await CompetitionService.addCompetitionAwardTypes(id, body.awardTypes || []); } diff --git a/components/admin/competition-management.tsx b/components/admin/competition-management.tsx index 9a38e31..3056829 100644 --- a/components/admin/competition-management.tsx +++ b/components/admin/competition-management.tsx @@ -903,11 +903,8 @@ export function CompetitionManagement() { // Validate individual rules if there are individual participants and judges if (newCompetition.participatingApps.length > 0 && newCompetition.individualConfig.judges.length > 0) { if (newCompetition.individualConfig.rules.length > 0) { - const individualTotalWeight = newCompetition.individualConfig.rules.reduce( - (sum, rule) => sum + rule.weight, - 0, - ) - if (individualTotalWeight !== 100) { + const individualTotalWeight = calculateTotalWeight(newCompetition.individualConfig.rules) + if (Math.abs(individualTotalWeight - 100) > 0.01) { setCreateError("個人賽評比標準權重總和必須為 100%") return } @@ -917,8 +914,8 @@ export function CompetitionManagement() { // Validate team rules if there are team participants and judges if (newCompetition.participatingTeams.length > 0 && newCompetition.teamConfig.judges.length > 0) { if (newCompetition.teamConfig.rules.length > 0) { - const teamTotalWeight = newCompetition.teamConfig.rules.reduce((sum, rule) => sum + rule.weight, 0) - if (teamTotalWeight !== 100) { + const teamTotalWeight = calculateTotalWeight(newCompetition.teamConfig.rules) + if (Math.abs(teamTotalWeight - 100) > 0.01) { setCreateError("團體賽評比標準權重總和必須為 100%") return } @@ -941,8 +938,8 @@ export function CompetitionManagement() { } if (newCompetition.rules.length > 0) { - const totalWeight = newCompetition.rules.reduce((sum, rule) => sum + rule.weight, 0) - if (totalWeight !== 100) { + const totalWeight = calculateTotalWeight(newCompetition.rules) + if (Math.abs(totalWeight - 100) > 0.01) { setCreateError("評比標準權重總和必須為 100%") return } @@ -1695,18 +1692,48 @@ export function CompetitionManagement() { const handleEditCompetition = (competition: any) => { setSelectedCompetitionForAction(competition) + + // 調試信息 + console.log('🔍 handleEditCompetition 調試信息:'); + console.log('競賽數據:', competition); + console.log('startDate:', competition.startDate); + console.log('endDate:', competition.endDate); + console.log('start_date:', competition.start_date); + console.log('end_date:', competition.end_date); + + // 將 ISO 日期字符串轉換為 YYYY-MM-DD 格式 + const formatDateForInput = (dateString: string) => { + if (!dateString) return ''; + try { + const date = new Date(dateString); + return date.toISOString().split('T')[0]; + } catch (error) { + console.error('日期格式轉換錯誤:', error); + return ''; + } + }; + + const startDate = competition.startDate || competition.start_date; + const endDate = competition.endDate || competition.end_date; + + console.log('轉換後的 startDate:', formatDateForInput(startDate)); + console.log('轉換後的 endDate:', formatDateForInput(endDate)); + setNewCompetition({ name: competition.name, type: competition.type, year: competition.year, month: competition.month, - startDate: competition.startDate, - endDate: competition.endDate, + startDate: formatDateForInput(startDate), + endDate: formatDateForInput(endDate), description: competition.description, status: competition.status, - judges: competition.judges || [], - participatingApps: competition.participatingApps || [], - participatingTeams: competition.participatingTeams || [], + // 將評審對象數組轉換為 ID 數組 + judges: competition.judges ? competition.judges.map((judge: any) => judge.id) : [], + // 將團隊對象數組轉換為 ID 數組 + participatingTeams: competition.teams ? competition.teams.map((team: any) => team.id) : [], + // 將應用對象數組轉換為 ID 數組 + participatingApps: competition.apps ? competition.apps.map((app: any) => app.id) : [], evaluationFocus: competition.evaluationFocus || "", rules: competition.rules || [], awardTypes: competition.awardTypes || [], diff --git a/lib/database-sync-fixed.js b/lib/database-sync-fixed.js index 80c7f3f..7f43a38 100644 --- a/lib/database-sync-fixed.js +++ b/lib/database-sync-fixed.js @@ -321,6 +321,9 @@ class DatabaseSyncFixed { const connection = await this.slavePool.getConnection(); try { + // 先刪除現有的關聯數據 + await connection.execute(`DELETE FROM ${relationTable} WHERE competition_id = ?`, [competitionId]); + for (const data of relationData) { const [uuidResult] = await connection.execute('SELECT UUID() as id'); const relationId = uuidResult[0].id; diff --git a/lib/database-sync-fixed.ts b/lib/database-sync-fixed.ts index ae8f2b2..f57f660 100644 --- a/lib/database-sync-fixed.ts +++ b/lib/database-sync-fixed.ts @@ -161,6 +161,7 @@ export class DatabaseSyncFixed { async smartDualInsertRelation( relationTable: string, competitionId: string, + slaveCompetitionId: string, relationData: any[], relationIdField: string ): Promise { @@ -171,14 +172,27 @@ export class DatabaseSyncFixed { }; try { - // 先獲取主機和備機的競賽 ID 對應關係 + console.log(`🔍 smartDualInsertRelation 開始執行`); + console.log(` 表名: ${relationTable}`); + console.log(` 主機競賽 ID: ${competitionId}`); + console.log(` 備機競賽 ID: ${slaveCompetitionId}`); + console.log(` 關聯數據:`, relationData); + console.log(` 關聯字段: ${relationIdField}`); + + // 先獲取主機的競賽 ID const masterCompetitionId = await this.getMasterCompetitionId(competitionId); - const slaveCompetitionId = await this.getSlaveCompetitionId(competitionId); if (!masterCompetitionId || !slaveCompetitionId) { throw new Error('找不到對應的競賽 ID'); } + console.log(`🔍 關聯雙寫開始: ${relationTable}`); + console.log(` 主機競賽 ID: ${masterCompetitionId}`); + console.log(` 備機競賽 ID: ${slaveCompetitionId}`); + console.log(` 關聯數據數量: ${relationData.length}`); + console.log(` 主機競賽存在: ${!!masterCompetitionId}`); + console.log(` 備機競賽存在: ${!!slaveCompetitionId}`); + // 同時寫入關聯數據 const masterPromise = this.insertRelationsToMaster(relationTable, masterCompetitionId, relationData, relationIdField); const slavePromise = this.insertRelationsToSlave(relationTable, slaveCompetitionId, relationData, relationIdField); @@ -191,9 +205,11 @@ export class DatabaseSyncFixed { if (masterResult.status === 'rejected') { result.masterError = masterResult.reason instanceof Error ? masterResult.reason.message : '主機關聯寫入失敗'; + console.error(`❌ 主機關聯寫入失敗:`, masterResult.reason); } if (slaveResult.status === 'rejected') { result.slaveError = slaveResult.reason instanceof Error ? slaveResult.reason.message : '備機關聯寫入失敗'; + console.error(`❌ 備機關聯寫入失敗:`, slaveResult.reason); } console.log(`📝 關聯雙寫結果: 主機${result.masterSuccess ? '✅' : '❌'} 備機${result.slaveSuccess ? '✅' : '❌'}`); @@ -231,6 +247,44 @@ export class DatabaseSyncFixed { } } + // 獲取競賽信息 + private async getCompetitionById(competitionId: string): Promise { + if (!this.masterPool) return null; + + const connection = await this.masterPool.getConnection(); + try { + const [rows] = await connection.execute('SELECT * FROM competitions WHERE id = ?', [competitionId]); + return (rows as any[])[0] || null; + } finally { + connection.release(); + } + } + + // 根據名稱獲取備機競賽 ID + private async getSlaveCompetitionIdByName(name: string): Promise { + if (!this.slavePool) return null; + + console.log(`🔍 getSlaveCompetitionIdByName 調用,名稱:`, name); + + const connection = await this.slavePool.getConnection(); + try { + const [rows] = await connection.execute('SELECT id FROM competitions WHERE name = ? ORDER BY created_at DESC LIMIT 1', [name]); + console.log(`🔍 備機查詢結果:`, rows); + const result = (rows as any[])[0]?.id || null; + console.log(`🔍 備機競賽 ID 結果:`, result, typeof result); + + // 確保返回的是字符串 + if (result && typeof result !== 'string') { + console.log(`⚠️ 備機競賽 ID 不是字符串,轉換為字符串:`, String(result)); + return String(result); + } + + return result; + } finally { + connection.release(); + } + } + // 寫入主機關聯表 private async insertRelationsToMaster( relationTable: string, @@ -263,15 +317,34 @@ export class DatabaseSyncFixed { ): Promise { if (!this.slavePool) return; + console.log(`🔍 備機關聯寫入開始: ${relationTable}`); + console.log(` 備機競賽 ID: ${competitionId}`); + console.log(` 關聯數據:`, relationData); + console.log(` 關聯字段: ${relationIdField}`); + const connection = await this.slavePool.getConnection(); try { for (const data of relationData) { const [uuidResult] = await connection.execute('SELECT UUID() as id'); const relationId = (uuidResult as any)[0].id; + console.log(`🔍 準備插入關聯數據:`, { + relationId, + competitionId, + relationField: relationIdField, + relationValue: data[relationIdField] + }); + const sql = `INSERT INTO ${relationTable} (id, competition_id, ${relationIdField}) VALUES (?, ?, ?)`; + console.log(`🔍 執行 SQL:`, sql); + console.log(`🔍 參數:`, [relationId, competitionId, data[relationIdField]]); + await connection.execute(sql, [relationId, competitionId, data[relationIdField]]); + console.log(`✅ 備機關聯數據插入成功`); } + } catch (error) { + console.error(`❌ 備機關聯寫入失敗:`, error); + throw error; } finally { connection.release(); } diff --git a/lib/services/database-service.ts b/lib/services/database-service.ts index 0c84006..1341916 100644 --- a/lib/services/database-service.ts +++ b/lib/services/database-service.ts @@ -1250,7 +1250,14 @@ export class CompetitionService { const connection = await slavePool.getConnection(); try { const [rows] = await connection.execute('SELECT id FROM competitions WHERE name = ? ORDER BY created_at DESC LIMIT 1', [name]); - return (rows as any[])[0]?.id || null; + const result = (rows as any[])[0]?.id || null; + + // 確保返回的是字符串 + if (result && typeof result !== 'string') { + return String(result); + } + + return result; } finally { connection.release(); } @@ -1361,6 +1368,19 @@ export class CompetitionService { return await db.query(sql, [competitionId]); } + // 獲取競賽的應用列表 + static async getCompetitionApps(competitionId: string): Promise { + const sql = ` + SELECT a.*, ca.submitted_at, u.name as creator_name, u.department as creator_department + FROM competition_apps ca + JOIN apps a ON ca.app_id = a.id + LEFT JOIN users u ON a.creator_id = u.id + WHERE ca.competition_id = ? AND a.is_active = TRUE + ORDER BY ca.submitted_at ASC + `; + return await db.query(sql, [competitionId]); + } + // 為競賽添加團隊 static async addCompetitionTeams(competitionId: string, teamIds: string[]): Promise { try { @@ -1409,6 +1429,54 @@ export class CompetitionService { return result.affectedRows > 0; } + // 為競賽添加應用 + static async addCompetitionApps(competitionId: string, appIds: string[]): Promise { + try { + const dbSyncFixed = new DatabaseSyncFixed(); + + // 先刪除現有的應用關聯 + await db.delete('DELETE FROM competition_apps WHERE competition_id = ?', [competitionId]); + + // 添加新的應用關聯 + if (appIds.length > 0) { + // 獲取備機的競賽 ID - 通過名稱查找 + const competition = await this.getCompetitionById(competitionId); + const slaveCompetitionId = await this.getSlaveCompetitionIdByName(competition?.name || ''); + + if (!slaveCompetitionId) { + console.error('找不到備機競賽 ID'); + return false; + } + + const relationData = appIds.map(appId => ({ app_id: appId })); + const result = await dbSyncFixed.smartDualInsertRelation( + 'competition_apps', + competitionId, + slaveCompetitionId, + relationData, + 'app_id' + ); + + if (!result.success) { + console.error('添加競賽應用失敗:', result.masterError || result.slaveError); + return false; + } + } + + return true; + } catch (error) { + console.error('添加競賽應用失敗:', error); + return false; + } + } + + // 從競賽中移除應用 + static async removeCompetitionApp(competitionId: string, appId: string): Promise { + const sql = 'DELETE FROM competition_apps WHERE competition_id = ? AND app_id = ?'; + const result = await db.delete(sql, [competitionId, appId]); + return result.affectedRows > 0; + } + // 獲取競賽的獎項類型列表 static async getCompetitionAwardTypes(competitionId: string): Promise { const sql = ` @@ -1545,17 +1613,27 @@ export class CompetitionService { const competition = await this.getCompetitionById(competitionId); if (!competition) return null; - const [judges, teams, awardTypes, rules] = await Promise.all([ + const [judges, teams, apps, awardTypes, rules] = await Promise.all([ this.getCompetitionJudges(competitionId), this.getCompetitionTeams(competitionId), + this.getCompetitionApps(competitionId), this.getCompetitionAwardTypes(competitionId), this.getCompetitionRules(competitionId) ]); + // 轉換字段名稱以匹配前端期望的格式 return { ...competition, + startDate: competition.start_date, + endDate: competition.end_date, + evaluationFocus: competition.evaluation_focus, + maxTeamSize: competition.max_team_size, + isActive: competition.is_active, + createdAt: competition.created_at, + updatedAt: competition.updated_at, judges, teams, + apps, awardTypes, rules };