完成競賽編輯功能

This commit is contained in:
2025-09-16 10:38:23 +08:00
parent 31ffaa1974
commit 1f2fb14bd0
8 changed files with 203 additions and 290 deletions

View File

@@ -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" 問題,您的應用程式仍然可以正常運行!

View File

@@ -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. 資料庫連接是否正常

View File

@@ -230,3 +230,4 @@ import { DatabaseMonitor } from '@/components/admin/database-monitor';

View File

@@ -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 || []);
} }

View File

@@ -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 || [],

View File

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

View File

@@ -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();
} }

View File

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