完成競賽編輯功能
This commit is contained in:
@@ -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';
|
|
||||||
|
|
||||||
// 在管理頁面中使用
|
|
||||||
<DatabaseMonitor />
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📈 系統狀態
|
|
||||||
|
|
||||||
### 當前配置
|
|
||||||
|
|
||||||
- **備援功能**: ✅ 已啟用
|
|
||||||
- **健康檢查間隔**: 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" 問題,您的應用程式仍然可以正常運行!
|
|
@@ -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. 資料庫連接是否正常
|
|
@@ -230,3 +230,4 @@ import { DatabaseMonitor } from '@/components/admin/database-monitor';
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@@ -131,6 +131,9 @@ export async function PUT(request: NextRequest, { params }: { params: { id: stri
|
|||||||
if (body.teams !== undefined) {
|
if (body.teams !== undefined) {
|
||||||
await CompetitionService.addCompetitionTeams(id, body.teams || []);
|
await CompetitionService.addCompetitionTeams(id, body.teams || []);
|
||||||
}
|
}
|
||||||
|
if (body.participatingApps !== undefined) {
|
||||||
|
await CompetitionService.addCompetitionApps(id, body.participatingApps || []);
|
||||||
|
}
|
||||||
if (body.awardTypes !== undefined) {
|
if (body.awardTypes !== undefined) {
|
||||||
await CompetitionService.addCompetitionAwardTypes(id, body.awardTypes || []);
|
await CompetitionService.addCompetitionAwardTypes(id, body.awardTypes || []);
|
||||||
}
|
}
|
||||||
|
@@ -903,11 +903,8 @@ export function CompetitionManagement() {
|
|||||||
// Validate individual rules if there are individual participants and judges
|
// Validate individual rules if there are individual participants and judges
|
||||||
if (newCompetition.participatingApps.length > 0 && newCompetition.individualConfig.judges.length > 0) {
|
if (newCompetition.participatingApps.length > 0 && newCompetition.individualConfig.judges.length > 0) {
|
||||||
if (newCompetition.individualConfig.rules.length > 0) {
|
if (newCompetition.individualConfig.rules.length > 0) {
|
||||||
const individualTotalWeight = newCompetition.individualConfig.rules.reduce(
|
const individualTotalWeight = calculateTotalWeight(newCompetition.individualConfig.rules)
|
||||||
(sum, rule) => sum + rule.weight,
|
if (Math.abs(individualTotalWeight - 100) > 0.01) {
|
||||||
0,
|
|
||||||
)
|
|
||||||
if (individualTotalWeight !== 100) {
|
|
||||||
setCreateError("個人賽評比標準權重總和必須為 100%")
|
setCreateError("個人賽評比標準權重總和必須為 100%")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -917,8 +914,8 @@ export function CompetitionManagement() {
|
|||||||
// Validate team rules if there are team participants and judges
|
// Validate team rules if there are team participants and judges
|
||||||
if (newCompetition.participatingTeams.length > 0 && newCompetition.teamConfig.judges.length > 0) {
|
if (newCompetition.participatingTeams.length > 0 && newCompetition.teamConfig.judges.length > 0) {
|
||||||
if (newCompetition.teamConfig.rules.length > 0) {
|
if (newCompetition.teamConfig.rules.length > 0) {
|
||||||
const teamTotalWeight = newCompetition.teamConfig.rules.reduce((sum, rule) => sum + rule.weight, 0)
|
const teamTotalWeight = calculateTotalWeight(newCompetition.teamConfig.rules)
|
||||||
if (teamTotalWeight !== 100) {
|
if (Math.abs(teamTotalWeight - 100) > 0.01) {
|
||||||
setCreateError("團體賽評比標準權重總和必須為 100%")
|
setCreateError("團體賽評比標準權重總和必須為 100%")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -941,8 +938,8 @@ export function CompetitionManagement() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (newCompetition.rules.length > 0) {
|
if (newCompetition.rules.length > 0) {
|
||||||
const totalWeight = newCompetition.rules.reduce((sum, rule) => sum + rule.weight, 0)
|
const totalWeight = calculateTotalWeight(newCompetition.rules)
|
||||||
if (totalWeight !== 100) {
|
if (Math.abs(totalWeight - 100) > 0.01) {
|
||||||
setCreateError("評比標準權重總和必須為 100%")
|
setCreateError("評比標準權重總和必須為 100%")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1695,18 +1692,48 @@ export function CompetitionManagement() {
|
|||||||
|
|
||||||
const handleEditCompetition = (competition: any) => {
|
const handleEditCompetition = (competition: any) => {
|
||||||
setSelectedCompetitionForAction(competition)
|
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({
|
setNewCompetition({
|
||||||
name: competition.name,
|
name: competition.name,
|
||||||
type: competition.type,
|
type: competition.type,
|
||||||
year: competition.year,
|
year: competition.year,
|
||||||
month: competition.month,
|
month: competition.month,
|
||||||
startDate: competition.startDate,
|
startDate: formatDateForInput(startDate),
|
||||||
endDate: competition.endDate,
|
endDate: formatDateForInput(endDate),
|
||||||
description: competition.description,
|
description: competition.description,
|
||||||
status: competition.status,
|
status: competition.status,
|
||||||
judges: competition.judges || [],
|
// 將評審對象數組轉換為 ID 數組
|
||||||
participatingApps: competition.participatingApps || [],
|
judges: competition.judges ? competition.judges.map((judge: any) => judge.id) : [],
|
||||||
participatingTeams: competition.participatingTeams || [],
|
// 將團隊對象數組轉換為 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 || "",
|
evaluationFocus: competition.evaluationFocus || "",
|
||||||
rules: competition.rules || [],
|
rules: competition.rules || [],
|
||||||
awardTypes: competition.awardTypes || [],
|
awardTypes: competition.awardTypes || [],
|
||||||
|
@@ -321,6 +321,9 @@ class DatabaseSyncFixed {
|
|||||||
|
|
||||||
const connection = await this.slavePool.getConnection();
|
const connection = await this.slavePool.getConnection();
|
||||||
try {
|
try {
|
||||||
|
// 先刪除現有的關聯數據
|
||||||
|
await connection.execute(`DELETE FROM ${relationTable} WHERE competition_id = ?`, [competitionId]);
|
||||||
|
|
||||||
for (const data of relationData) {
|
for (const data of relationData) {
|
||||||
const [uuidResult] = await connection.execute('SELECT UUID() as id');
|
const [uuidResult] = await connection.execute('SELECT UUID() as id');
|
||||||
const relationId = uuidResult[0].id;
|
const relationId = uuidResult[0].id;
|
||||||
|
@@ -161,6 +161,7 @@ export class DatabaseSyncFixed {
|
|||||||
async smartDualInsertRelation(
|
async smartDualInsertRelation(
|
||||||
relationTable: string,
|
relationTable: string,
|
||||||
competitionId: string,
|
competitionId: string,
|
||||||
|
slaveCompetitionId: string,
|
||||||
relationData: any[],
|
relationData: any[],
|
||||||
relationIdField: string
|
relationIdField: string
|
||||||
): Promise<WriteResult> {
|
): Promise<WriteResult> {
|
||||||
@@ -171,14 +172,27 @@ export class DatabaseSyncFixed {
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
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 masterCompetitionId = await this.getMasterCompetitionId(competitionId);
|
||||||
const slaveCompetitionId = await this.getSlaveCompetitionId(competitionId);
|
|
||||||
|
|
||||||
if (!masterCompetitionId || !slaveCompetitionId) {
|
if (!masterCompetitionId || !slaveCompetitionId) {
|
||||||
throw new Error('找不到對應的競賽 ID');
|
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 masterPromise = this.insertRelationsToMaster(relationTable, masterCompetitionId, relationData, relationIdField);
|
||||||
const slavePromise = this.insertRelationsToSlave(relationTable, slaveCompetitionId, relationData, relationIdField);
|
const slavePromise = this.insertRelationsToSlave(relationTable, slaveCompetitionId, relationData, relationIdField);
|
||||||
@@ -191,9 +205,11 @@ export class DatabaseSyncFixed {
|
|||||||
|
|
||||||
if (masterResult.status === 'rejected') {
|
if (masterResult.status === 'rejected') {
|
||||||
result.masterError = masterResult.reason instanceof Error ? masterResult.reason.message : '主機關聯寫入失敗';
|
result.masterError = masterResult.reason instanceof Error ? masterResult.reason.message : '主機關聯寫入失敗';
|
||||||
|
console.error(`❌ 主機關聯寫入失敗:`, masterResult.reason);
|
||||||
}
|
}
|
||||||
if (slaveResult.status === 'rejected') {
|
if (slaveResult.status === 'rejected') {
|
||||||
result.slaveError = slaveResult.reason instanceof Error ? slaveResult.reason.message : '備機關聯寫入失敗';
|
result.slaveError = slaveResult.reason instanceof Error ? slaveResult.reason.message : '備機關聯寫入失敗';
|
||||||
|
console.error(`❌ 備機關聯寫入失敗:`, slaveResult.reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`📝 關聯雙寫結果: 主機${result.masterSuccess ? '✅' : '❌'} 備機${result.slaveSuccess ? '✅' : '❌'}`);
|
console.log(`📝 關聯雙寫結果: 主機${result.masterSuccess ? '✅' : '❌'} 備機${result.slaveSuccess ? '✅' : '❌'}`);
|
||||||
@@ -231,6 +247,44 @@ export class DatabaseSyncFixed {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 獲取競賽信息
|
||||||
|
private async getCompetitionById(competitionId: string): Promise<any> {
|
||||||
|
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<string | null> {
|
||||||
|
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(
|
private async insertRelationsToMaster(
|
||||||
relationTable: string,
|
relationTable: string,
|
||||||
@@ -263,15 +317,34 @@ export class DatabaseSyncFixed {
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!this.slavePool) return;
|
if (!this.slavePool) return;
|
||||||
|
|
||||||
|
console.log(`🔍 備機關聯寫入開始: ${relationTable}`);
|
||||||
|
console.log(` 備機競賽 ID: ${competitionId}`);
|
||||||
|
console.log(` 關聯數據:`, relationData);
|
||||||
|
console.log(` 關聯字段: ${relationIdField}`);
|
||||||
|
|
||||||
const connection = await this.slavePool.getConnection();
|
const connection = await this.slavePool.getConnection();
|
||||||
try {
|
try {
|
||||||
for (const data of relationData) {
|
for (const data of relationData) {
|
||||||
const [uuidResult] = await connection.execute('SELECT UUID() as id');
|
const [uuidResult] = await connection.execute('SELECT UUID() as id');
|
||||||
const relationId = (uuidResult as any)[0].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 (?, ?, ?)`;
|
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]]);
|
await connection.execute(sql, [relationId, competitionId, data[relationIdField]]);
|
||||||
|
console.log(`✅ 備機關聯數據插入成功`);
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ 備機關聯寫入失敗:`, error);
|
||||||
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
connection.release();
|
connection.release();
|
||||||
}
|
}
|
||||||
|
@@ -1250,7 +1250,14 @@ export class CompetitionService {
|
|||||||
const connection = await slavePool.getConnection();
|
const connection = await slavePool.getConnection();
|
||||||
try {
|
try {
|
||||||
const [rows] = await connection.execute('SELECT id FROM competitions WHERE name = ? ORDER BY created_at DESC LIMIT 1', [name]);
|
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 {
|
} finally {
|
||||||
connection.release();
|
connection.release();
|
||||||
}
|
}
|
||||||
@@ -1361,6 +1368,19 @@ export class CompetitionService {
|
|||||||
return await db.query(sql, [competitionId]);
|
return await db.query(sql, [competitionId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 獲取競賽的應用列表
|
||||||
|
static async getCompetitionApps(competitionId: string): Promise<any[]> {
|
||||||
|
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<boolean> {
|
static async addCompetitionTeams(competitionId: string, teamIds: string[]): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
@@ -1409,6 +1429,54 @@ export class CompetitionService {
|
|||||||
return result.affectedRows > 0;
|
return result.affectedRows > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 為競賽添加應用
|
||||||
|
static async addCompetitionApps(competitionId: string, appIds: string[]): Promise<boolean> {
|
||||||
|
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<boolean> {
|
||||||
|
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<any[]> {
|
static async getCompetitionAwardTypes(competitionId: string): Promise<any[]> {
|
||||||
const sql = `
|
const sql = `
|
||||||
@@ -1545,17 +1613,27 @@ export class CompetitionService {
|
|||||||
const competition = await this.getCompetitionById(competitionId);
|
const competition = await this.getCompetitionById(competitionId);
|
||||||
if (!competition) return null;
|
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.getCompetitionJudges(competitionId),
|
||||||
this.getCompetitionTeams(competitionId),
|
this.getCompetitionTeams(competitionId),
|
||||||
|
this.getCompetitionApps(competitionId),
|
||||||
this.getCompetitionAwardTypes(competitionId),
|
this.getCompetitionAwardTypes(competitionId),
|
||||||
this.getCompetitionRules(competitionId)
|
this.getCompetitionRules(competitionId)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// 轉換字段名稱以匹配前端期望的格式
|
||||||
return {
|
return {
|
||||||
...competition,
|
...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,
|
judges,
|
||||||
teams,
|
teams,
|
||||||
|
apps,
|
||||||
awardTypes,
|
awardTypes,
|
||||||
rules
|
rules
|
||||||
};
|
};
|
||||||
|
Reference in New Issue
Block a user