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
};