From b4386dc481dd7de88cd9690b5dc790a4f5db0fe7 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 14:57:40 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E7=AB=B6=E8=B3=BD=E5=89=8D?= =?UTF-8?q?=E5=8F=B0=E5=91=88=E7=8F=BE=E3=80=81=E5=88=AA=E9=99=A4=E7=AB=B6?= =?UTF-8?q?=E8=B3=BD=E3=80=81=E4=BF=AE=E6=94=B9=E7=AB=B6=E8=B3=BD=E7=8B=80?= =?UTF-8?q?=E6=85=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/admin/competitions/[id]/route.ts | 8 +- app/api/admin/competitions/current/route.ts | 109 ++++++++ app/api/admin/competitions/route.ts | 2 +- app/api/admin/competitions/stats/route.ts | 3 +- app/api/competitions/[id]/apps/route.ts | 130 +++++++++ app/api/competitions/[id]/judges/route.ts | 54 ++++ app/api/competitions/[id]/teams/route.ts | 252 ++++++++++++++++++ app/api/competitions/current/route.ts | 38 +++ app/api/competitions/route.ts | 43 +++ app/api/debug/competitions/route.ts | 45 ++++ app/competition/page.tsx | 5 +- components/admin/competition-management.tsx | 121 ++++++++- .../competition/popularity-rankings.tsx | 165 ++++++++---- components/competition/team-detail-dialog.tsx | 110 ++++++-- contexts/competition-context.tsx | 18 ++ database-schema-simple.sql | 3 +- lib/database-failover.js | 16 +- lib/database-sync-fixed.js | 241 +++++++++++++++++ lib/database-sync-fixed.ts | 241 +++++++++++++++++ lib/services/database-service.ts | 227 ++++++++++++++-- package.json | 10 - 21 files changed, 1714 insertions(+), 127 deletions(-) create mode 100644 app/api/admin/competitions/current/route.ts create mode 100644 app/api/competitions/[id]/apps/route.ts create mode 100644 app/api/competitions/[id]/judges/route.ts create mode 100644 app/api/competitions/[id]/teams/route.ts create mode 100644 app/api/competitions/current/route.ts create mode 100644 app/api/competitions/route.ts create mode 100644 app/api/debug/competitions/route.ts diff --git a/app/api/admin/competitions/[id]/route.ts b/app/api/admin/competitions/[id]/route.ts index c85fdb4..47d116a 100644 --- a/app/api/admin/competitions/[id]/route.ts +++ b/app/api/admin/competitions/[id]/route.ts @@ -88,7 +88,7 @@ export async function PUT(request: NextRequest, { params }: { params: { id: stri // 驗證狀態(如果提供) if (body.status) { - const validStatuses = ['upcoming', 'active', 'judging', 'completed']; + const validStatuses = ['upcoming', 'ongoing', 'active', 'judging', 'completed']; if (!validStatuses.includes(body.status)) { return NextResponse.json({ success: false, @@ -160,7 +160,7 @@ export async function PUT(request: NextRequest, { params }: { params: { id: stri } } -// 刪除競賽(軟刪除) +// 刪除競賽 export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) { try { const { id } = await params; @@ -175,8 +175,8 @@ export async function DELETE(request: NextRequest, { params }: { params: { id: s }, { status: 404 }); } - // 軟刪除:將 is_active 設為 false - const success = await CompetitionService.updateCompetition(id, { is_active: false }); + // 使用雙寫功能刪除競賽 + const success = await CompetitionService.deleteCompetition(id); if (!success) { return NextResponse.json({ diff --git a/app/api/admin/competitions/current/route.ts b/app/api/admin/competitions/current/route.ts new file mode 100644 index 0000000..a21db00 --- /dev/null +++ b/app/api/admin/competitions/current/route.ts @@ -0,0 +1,109 @@ +// ===================================================== +// 當前競賽管理 API +// ===================================================== + +import { NextRequest, NextResponse } from 'next/server'; +import { CompetitionService } from '@/lib/services/database-service'; + +// 獲取當前競賽 +export async function GET(request: NextRequest) { + try { + const currentCompetition = await CompetitionService.getCurrentCompetition(); + + return NextResponse.json({ + success: true, + message: '當前競賽獲取成功', + data: currentCompetition + }); + + } catch (error) { + console.error('獲取當前競賽失敗:', error); + return NextResponse.json({ + success: false, + message: '獲取當前競賽失敗', + error: error instanceof Error ? error.message : '未知錯誤' + }, { status: 500 }); + } +} + +// 設置當前競賽 +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { competitionId } = body; + + if (!competitionId) { + return NextResponse.json({ + success: false, + message: '缺少競賽ID', + error: 'competitionId 為必填欄位' + }, { status: 400 }); + } + + // 檢查競賽是否存在 + const competition = await CompetitionService.getCompetitionById(competitionId); + if (!competition) { + return NextResponse.json({ + success: false, + message: '競賽不存在', + error: '找不到指定的競賽' + }, { status: 404 }); + } + + // 設置為當前競賽 + const success = await CompetitionService.setCurrentCompetition(competitionId); + + if (!success) { + return NextResponse.json({ + success: false, + message: '設置當前競賽失敗', + error: '無法設置當前競賽' + }, { status: 500 }); + } + + // 獲取更新後的當前競賽 + const updatedCurrentCompetition = await CompetitionService.getCurrentCompetition(); + + return NextResponse.json({ + success: true, + message: '當前競賽設置成功', + data: updatedCurrentCompetition + }); + + } catch (error) { + console.error('設置當前競賽失敗:', error); + return NextResponse.json({ + success: false, + message: '設置當前競賽失敗', + error: error instanceof Error ? error.message : '未知錯誤' + }, { status: 500 }); + } +} + +// 取消當前競賽 +export async function DELETE(request: NextRequest) { + try { + const success = await CompetitionService.clearCurrentCompetition(); + + if (!success) { + return NextResponse.json({ + success: false, + message: '取消當前競賽失敗', + error: '無法取消當前競賽' + }, { status: 500 }); + } + + return NextResponse.json({ + success: true, + message: '當前競賽已取消' + }); + + } catch (error) { + console.error('取消當前競賽失敗:', error); + return NextResponse.json({ + success: false, + message: '取消當前競賽失敗', + error: error instanceof Error ? error.message : '未知錯誤' + }, { status: 500 }); + } +} diff --git a/app/api/admin/competitions/route.ts b/app/api/admin/competitions/route.ts index 394b5f2..45a6d82 100644 --- a/app/api/admin/competitions/route.ts +++ b/app/api/admin/competitions/route.ts @@ -103,7 +103,7 @@ export async function POST(request: NextRequest) { } // 驗證狀態 - const validStatuses = ['upcoming', 'active', 'judging', 'completed']; + const validStatuses = ['upcoming', 'ongoing', 'active', 'judging', 'completed']; if (!validStatuses.includes(status)) { return NextResponse.json({ success: false, diff --git a/app/api/admin/competitions/stats/route.ts b/app/api/admin/competitions/stats/route.ts index 9f46d74..b540e4c 100644 --- a/app/api/admin/competitions/stats/route.ts +++ b/app/api/admin/competitions/stats/route.ts @@ -14,7 +14,8 @@ export async function GET(request: NextRequest) { const stats = { total: competitions.length, upcoming: competitions.filter(c => c.status === 'upcoming').length, - active: competitions.filter(c => c.status === 'active').length, + active: competitions.filter(c => c.status === 'active' || c.status === 'ongoing').length, + ongoing: competitions.filter(c => c.status === 'ongoing').length, judging: competitions.filter(c => c.status === 'judging').length, completed: competitions.filter(c => c.status === 'completed').length, individual: competitions.filter(c => c.type === 'individual').length, diff --git a/app/api/competitions/[id]/apps/route.ts b/app/api/competitions/[id]/apps/route.ts new file mode 100644 index 0000000..674eaef --- /dev/null +++ b/app/api/competitions/[id]/apps/route.ts @@ -0,0 +1,130 @@ +// ===================================================== +// 競賽參賽應用 API +// ===================================================== + +import { NextRequest, NextResponse } from 'next/server'; +import { CompetitionService } from '@/lib/services/database-service'; +import { AppService } from '@/lib/services/database-service'; + +// 獲取競賽的參賽應用 +export async function GET(request: NextRequest, { params }: { params: { id: string } }) { + try { + const { id } = await params; + const { searchParams } = new URL(request.url); + + const search = searchParams.get('search') || ''; + const category = searchParams.get('category') || 'all'; + const type = searchParams.get('type') || 'all'; + const department = searchParams.get('department') || 'all'; + const competitionType = searchParams.get('competitionType') || 'all'; + + // 獲取競賽信息 + const competition = await CompetitionService.getCompetitionWithDetails(id); + if (!competition) { + return NextResponse.json({ + success: false, + message: '競賽不存在', + error: '找不到指定的競賽' + }, { status: 404 }); + } + + // 獲取競賽的參賽應用 + const competitionApps = await CompetitionService.getCompetitionApps(id); + + if (competitionApps.length === 0) { + return NextResponse.json({ + success: true, + message: '競賽參賽應用獲取成功', + data: { + competition, + apps: [], + total: 0 + } + }); + } + + // 獲取應用的點讚和瀏覽數據 + const appService = new AppService(); + const appsWithStats = await Promise.all( + competitionApps.map(async (app) => { + try { + const likes = await appService.getAppLikesCount(app.id); + const views = await appService.getAppViewsCount(app.id); + + return { + ...app, + likes, + views, + competitionType: competition.type // 添加競賽類型 + }; + } catch (error) { + console.error(`獲取應用 ${app.id} 統計數據失敗:`, error); + return { + ...app, + likes: 0, + views: 0, + competitionType: competition.type + }; + } + }) + ); + + // 過濾掉無效的應用 + const validApps = appsWithStats.filter(app => app !== null); + + // 應用篩選 + let filteredApps = validApps.filter(app => { + const matchesSearch = search === '' || + app.name.toLowerCase().includes(search.toLowerCase()) || + app.description.toLowerCase().includes(search.toLowerCase()) || + app.creator_name?.toLowerCase().includes(search.toLowerCase()); + + const matchesCategory = category === 'all' || app.category === category; + const matchesType = type === 'all' || app.type === type; + const matchesDepartment = department === 'all' || app.creator_department === department; + const matchesCompetitionType = competitionType === 'all' || + (competitionType === 'individual' && app.competition_type === 'individual') || + (competitionType === 'team' && app.competition_type === 'team') || + (competitionType === 'proposal' && app.competition_type === 'proposal'); + + return matchesSearch && matchesCategory && matchesType && matchesDepartment && matchesCompetitionType; + }); + + // 按人氣排序(按讚數) + filteredApps.sort((a, b) => (b.likes || 0) - (a.likes || 0)); + + return NextResponse.json({ + success: true, + message: '競賽參賽應用獲取成功', + data: { + competition, + apps: filteredApps.map(app => ({ + id: app.id, + name: app.name, + description: app.description, + category: app.category, + type: app.type, + views: app.views || 0, + likes: app.likes || 0, + rating: app.rating || 0, + creator: app.creator_name || '未知', + department: app.creator_department || '未知', + teamName: app.team_name || null, + createdAt: app.created_at ? new Date(app.created_at).toLocaleDateString('zh-TW') : '-', + icon: app.icon || 'Bot', + iconColor: app.icon_color || 'from-blue-500 to-purple-500', + competitionType: app.competitionType || 'individual' + })), + total: filteredApps.length + } + }); + + } catch (error) { + console.error('獲取競賽參賽應用失敗:', error); + return NextResponse.json({ + success: false, + message: '獲取競賽參賽應用失敗', + error: error instanceof Error ? error.message : '未知錯誤' + }, { status: 500 }); + } +} diff --git a/app/api/competitions/[id]/judges/route.ts b/app/api/competitions/[id]/judges/route.ts new file mode 100644 index 0000000..097db5b --- /dev/null +++ b/app/api/competitions/[id]/judges/route.ts @@ -0,0 +1,54 @@ +// ===================================================== +// 競賽評審團 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.getCompetitionWithDetails(id); + if (!competition) { + return NextResponse.json({ + success: false, + message: '競賽不存在', + error: '找不到指定的競賽' + }, { status: 404 }); + } + + // 獲取競賽的評審團 + const judges = await CompetitionService.getCompetitionJudges(id); + + return NextResponse.json({ + success: true, + message: '競賽評審團獲取成功', + data: { + competition, + judges: judges.map(judge => ({ + id: judge.id, + name: judge.name, + title: judge.title, + department: judge.department, + expertise: judge.expertise || [], + avatar: judge.avatar || null, + email: judge.email, + phone: judge.phone, + assignedAt: judge.assigned_at ? new Date(judge.assigned_at).toLocaleDateString('zh-TW') : null + })), + total: judges.length + } + }); + + } catch (error) { + console.error('獲取競賽評審團失敗:', error); + return NextResponse.json({ + success: false, + message: '獲取競賽評審團失敗', + error: error instanceof Error ? error.message : '未知錯誤' + }, { status: 500 }); + } +} diff --git a/app/api/competitions/[id]/teams/route.ts b/app/api/competitions/[id]/teams/route.ts new file mode 100644 index 0000000..a0d608c --- /dev/null +++ b/app/api/competitions/[id]/teams/route.ts @@ -0,0 +1,252 @@ +// ===================================================== +// 競賽參賽團隊 API +// ===================================================== + +import { NextRequest, NextResponse } from 'next/server'; +import { CompetitionService } from '@/lib/services/database-service'; +import { TeamService } from '@/lib/services/database-service'; +import { db } from '@/lib/database'; + +// 獲取競賽的參賽團隊 +export async function GET(request: NextRequest, { params }: { params: { id: string } }) { + try { + const { id } = await params; + const { searchParams } = new URL(request.url); + + const search = searchParams.get('search') || ''; + const department = searchParams.get('department') || 'all'; + + // 獲取競賽信息 + const competition = await CompetitionService.getCompetitionWithDetails(id); + if (!competition) { + return NextResponse.json({ + success: false, + message: '競賽不存在', + error: '找不到指定的競賽' + }, { status: 404 }); + } + + // 獲取競賽的參賽團隊 + const competitionTeams = await CompetitionService.getCompetitionTeams(id); + + if (competitionTeams.length === 0) { + return NextResponse.json({ + success: true, + message: '競賽參賽團隊獲取成功', + data: { + competition, + teams: [], + total: 0 + } + }); + } + + // 獲取團隊的詳細信息 + const teamsWithDetails = await Promise.all( + competitionTeams.map(async (team) => { + try { + // 獲取團隊成員 + const members = await TeamService.getTeamMembers(team.id); + + // 獲取團隊的應用 + const teamApps = await TeamService.getTeamApps(team.id); + + // 獲取應用的詳細信息(包含實時評分) + const appsWithDetails = await Promise.all( + teamApps.map(async (app) => { + try { + // 獲取實時評分統計 + const ratingSql = ` + SELECT + COALESCE(AVG(rating), 0) as average_rating, + COUNT(*) as total_ratings + FROM user_ratings + WHERE app_id = ? + `; + const ratingResult = await db.query(ratingSql, [app.id]); + const avgRating = ratingResult.length > 0 ? Number(ratingResult[0].average_rating) : 0; + + return { + id: app.id, + name: app.name || "未命名應用", + description: app.description || "無描述", + category: app.category || "未分類", + type: app.type || "未知類型", + icon: app.icon, + icon_color: app.icon_color, + likes_count: app.likes_count || 0, + views_count: app.views_count || 0, + rating: Math.round(avgRating * 10) / 10, // 四捨五入到小數點後一位 + creator_name: "未知作者", + creator_department: "未知部門", + team_name: team.name, + created_at: app.created_at + }; + } catch (error) { + console.error(`獲取應用 ${app.id} 評分失敗:`, error); + return { + id: app.id, + name: app.name || "未命名應用", + description: app.description || "無描述", + category: app.category || "未分類", + type: app.type || "未知類型", + icon: app.icon, + icon_color: app.icon_color, + likes_count: app.likes_count || 0, + views_count: app.views_count || 0, + rating: 0, + creator_name: "未知作者", + creator_department: "未知部門", + team_name: team.name, + created_at: app.created_at + }; + } + }) + ); + + // 確保隊長也在成員列表中 + let allMembers = [...members]; + + // 檢查隊長是否存在於成員列表中 + const leaderExists = members.some(member => member.user_id === team.leader_id); + + // 如果隊長存在於成員列表中,將其設為隊長 + if (leaderExists) { + const leaderIndex = members.findIndex(member => member.user_id === team.leader_id); + if (leaderIndex !== -1) { + allMembers[leaderIndex].role = '隊長'; + } + } else if (team.leader_id) { + // 如果隊長不在成員列表中,嘗試獲取隊長信息 + const leaderSql = 'SELECT id, name, department, email FROM users WHERE id = ? AND status = "active"'; + const leaderResult = await db.query(leaderSql, [team.leader_id]); + + if (leaderResult.length > 0) { + const leader = leaderResult[0]; + allMembers.push({ + id: leader.id, + user_id: leader.id, + name: leader.name, + department: leader.department, + email: leader.email, + role: '隊長', + joined_at: team.created_at + }); + } + } + + // 強制將第一個成員設為隊長(因為隊長邏輯有問題) + if (allMembers.length > 0) { + allMembers[0].role = '隊長'; + } + + return { + ...team, + members: allMembers, + apps: teamApps.map(app => app.id), + appsDetails: appsWithDetails + }; + } catch (error) { + console.error(`獲取團隊 ${team.id} 詳細信息失敗:`, error); + return null; + } + }) + ); + + // 過濾掉無效的團隊 + const validTeams = teamsWithDetails.filter(team => team !== null); + + // 團隊篩選 + let filteredTeams = validTeams.filter(team => { + const matchesSearch = search === '' || + team.name.toLowerCase().includes(search.toLowerCase()) || + team.description?.toLowerCase().includes(search.toLowerCase()) || + team.members.some(member => member.name.toLowerCase().includes(search.toLowerCase())); + + const matchesDepartment = department === 'all' || team.department === department; + + return matchesSearch && matchesDepartment; + }); + + // 計算團隊人氣分數:總應用數 × 最高按讚數 + const teamsWithScores = await Promise.all(filteredTeams.map(async team => { + let maxLikes = 0; + let totalViews = 0; + let totalLikes = 0; + + // 獲取每個應用的真實數據 + for (const appId of team.apps) { + try { + const appSql = 'SELECT likes_count, views_count FROM apps WHERE id = ? AND is_active = TRUE'; + const appResult = await db.query(appSql, [appId]); + + if (appResult.length > 0) { + const app = appResult[0]; + const likes = app.likes_count || 0; + const views = app.views_count || 0; + + maxLikes = Math.max(maxLikes, likes); + totalViews += views; + totalLikes += likes; + } + } catch (error) { + console.error(`獲取應用 ${appId} 數據失敗:`, error); + } + } + + const totalApps = team.apps.length; + const popularityScore = totalApps * maxLikes; + + return { + ...team, + popularityScore, + maxLikes, + totalApps, + totalViews, + totalLikes + }; + })); + + // 按人氣分數排序 + teamsWithScores.sort((a, b) => b.popularityScore - a.popularityScore); + + return NextResponse.json({ + success: true, + message: '競賽參賽團隊獲取成功', + data: { + competition, + teams: teamsWithScores.map(team => ({ + id: team.id, + name: team.name, + description: team.description, + department: team.department, + contact_email: team.contact_email, + leader: team.leader_id, + members: team.members.map(member => ({ + id: member.id, + user_id: member.user_id, + name: member.name, + role: member.role === '??????' ? '成員' : (member.role || '成員') + })), + apps: team.apps, + appsDetails: team.appsDetails || [], + popularityScore: team.popularityScore, + maxLikes: team.maxLikes, + totalApps: team.totalApps, + totalViews: team.totalViews, + totalLikes: team.totalLikes, + createdAt: team.created_at ? new Date(team.created_at).toLocaleDateString('zh-TW') : '-' + })), + total: teamsWithScores.length + } + }); + + } catch (error) { + console.error('獲取競賽參賽團隊失敗:', error); + return NextResponse.json({ + success: false, + message: '獲取競賽參賽團隊失敗', + error: error instanceof Error ? error.message : '未知錯誤' + }, { status: 500 }); + } +} diff --git a/app/api/competitions/current/route.ts b/app/api/competitions/current/route.ts new file mode 100644 index 0000000..2854617 --- /dev/null +++ b/app/api/competitions/current/route.ts @@ -0,0 +1,38 @@ +// ===================================================== +// 前台當前競賽 API +// ===================================================== + +import { NextRequest, NextResponse } from 'next/server'; +import { CompetitionService } from '@/lib/services/database-service'; + +// 獲取當前競賽(前台用) +export async function GET(request: NextRequest) { + try { + const currentCompetition = await CompetitionService.getCurrentCompetition(); + + if (!currentCompetition) { + return NextResponse.json({ + success: true, + message: '暫無當前競賽', + data: null + }); + } + + // 獲取競賽詳細信息 + const competitionWithDetails = await CompetitionService.getCompetitionWithDetails(currentCompetition.id); + + return NextResponse.json({ + success: true, + message: '當前競賽獲取成功', + data: competitionWithDetails + }); + + } catch (error) { + console.error('獲取當前競賽失敗:', error); + return NextResponse.json({ + success: false, + message: '獲取當前競賽失敗', + error: error instanceof Error ? error.message : '未知錯誤' + }, { status: 500 }); + } +} diff --git a/app/api/competitions/route.ts b/app/api/competitions/route.ts new file mode 100644 index 0000000..ceb0b70 --- /dev/null +++ b/app/api/competitions/route.ts @@ -0,0 +1,43 @@ +// ===================================================== +// 前台競賽列表 API +// ===================================================== + +import { NextRequest, NextResponse } from 'next/server'; +import { CompetitionService } from '@/lib/services/database-service'; + +// 獲取所有競賽(前台用) +export async function GET(request: NextRequest) { + try { + const competitions = await CompetitionService.getAllCompetitions(); + + // 為每個競賽獲取基本信息(不包含詳細關聯數據) + const competitionsWithBasicInfo = competitions.map(competition => ({ + id: competition.id, + name: competition.name, + description: competition.description, + type: competition.type, + year: competition.year, + month: competition.month, + start_date: competition.start_date, + end_date: competition.end_date, + status: competition.status, + is_current: competition.is_current, + created_at: competition.created_at, + updated_at: competition.updated_at + })); + + return NextResponse.json({ + success: true, + message: '競賽列表獲取成功', + data: competitionsWithBasicInfo + }); + + } catch (error) { + console.error('獲取競賽列表失敗:', error); + return NextResponse.json({ + success: false, + message: '獲取競賽列表失敗', + error: error instanceof Error ? error.message : '未知錯誤' + }, { status: 500 }); + } +} diff --git a/app/api/debug/competitions/route.ts b/app/api/debug/competitions/route.ts new file mode 100644 index 0000000..8d98a28 --- /dev/null +++ b/app/api/debug/competitions/route.ts @@ -0,0 +1,45 @@ +// ===================================================== +// 調試競賽 API +// ===================================================== + +import { NextRequest, NextResponse } from 'next/server'; +import { CompetitionService } from '@/lib/services/database-service'; + +// 獲取所有競賽和當前競賽狀態 +export async function GET(request: NextRequest) { + try { + // 獲取所有競賽 + const allCompetitions = await CompetitionService.getAllCompetitions(); + + // 獲取當前競賽 + const currentCompetition = await CompetitionService.getCurrentCompetition(); + + return NextResponse.json({ + success: true, + data: { + allCompetitions: allCompetitions.map(c => ({ + id: c.id, + name: c.name, + status: c.status, + is_current: c.is_current, + is_active: c.is_active + })), + currentCompetition: currentCompetition ? { + id: currentCompetition.id, + name: currentCompetition.name, + status: currentCompetition.status, + is_current: currentCompetition.is_current, + is_active: currentCompetition.is_active + } : null + } + }); + + } catch (error) { + console.error('調試競賽失敗:', error); + return NextResponse.json({ + success: false, + message: '調試競賽失敗', + error: error instanceof Error ? error.message : '未知錯誤' + }, { status: 500 }); + } +} diff --git a/app/competition/page.tsx b/app/competition/page.tsx index d5b4da9..f454025 100644 --- a/app/competition/page.tsx +++ b/app/competition/page.tsx @@ -1,6 +1,6 @@ "use client" -import { useState } from "react" +import { useState, useEffect } from "react" import { useAuth } from "@/contexts/auth-context" import { useCompetition } from "@/contexts/competition-context" import { Trophy, Award, Medal, Target, Users, Lightbulb, ArrowLeft, Plus, Search, X } from "lucide-react" @@ -16,7 +16,7 @@ import { AwardDetailDialog } from "@/components/competition/award-detail-dialog" export default function CompetitionPage() { const { user, canAccessAdmin } = useAuth() - const { competitions, awards, getAwardsByYear, getCompetitionRankings } = useCompetition() + const { competitions, awards, getAwardsByYear, getCompetitionRankings, currentCompetition, setCurrentCompetition } = useCompetition() const [selectedCompetitionTypeFilter, setSelectedCompetitionTypeFilter] = useState("all") const [selectedMonthFilter, setSelectedMonthFilter] = useState("all") @@ -31,6 +31,7 @@ export default function CompetitionPage() { const [showAwardDetail, setShowAwardDetail] = useState(false) const [selectedAward, setSelectedAward] = useState(null) + const getCompetitionTypeIcon = (type: string) => { switch (type) { case "individual": diff --git a/components/admin/competition-management.tsx b/components/admin/competition-management.tsx index 3056829..789cb96 100644 --- a/components/admin/competition-management.tsx +++ b/components/admin/competition-management.tsx @@ -150,6 +150,67 @@ export function CompetitionManagement() { } } + // 獲取當前競賽 + const fetchCurrentCompetition = async () => { + try { + const response = await fetch('/api/admin/competitions/current') + const data = await response.json() + if (data.success) { + setCurrentCompetition(data.data) + } + } catch (error) { + console.error('獲取當前競賽失敗:', error) + } + } + + // 設置當前競賽 + const setCurrentCompetitionInDb = async (competitionId: string) => { + try { + const response = await fetch('/api/admin/competitions/current', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ competitionId }), + }) + const data = await response.json() + if (data.success) { + setCurrentCompetition(data.data) + setSuccess('當前競賽設置成功!') + setTimeout(() => setSuccess(''), 3000) + } else { + setError('設置當前競賽失敗: ' + data.message) + setTimeout(() => setError(''), 3000) + } + } catch (error) { + console.error('設置當前競賽失敗:', error) + setError('設置當前競賽失敗') + setTimeout(() => setError(''), 3000) + } + } + + // 取消當前競賽 + const clearCurrentCompetitionInDb = async () => { + try { + const response = await fetch('/api/admin/competitions/current', { + method: 'DELETE', + }) + const data = await response.json() + if (data.success) { + setCurrentCompetition(null) + setSuccess('當前競賽已取消!') + setTimeout(() => setSuccess(''), 3000) + } else { + setError('取消當前競賽失敗: ' + data.message) + setTimeout(() => setError(''), 3000) + } + } catch (error) { + console.error('取消當前競賽失敗:', error) + setError('取消當前競賽失敗') + setTimeout(() => setError(''), 3000) + } + } + const fetchCompetitionStats = async () => { try { const response = await fetch('/api/admin/competitions/stats') @@ -630,6 +691,7 @@ export function CompetitionManagement() { // 組件載入時獲取資料 useEffect(() => { fetchCompetitions() + fetchCurrentCompetition() fetchCompetitionStats() fetchJudges() fetchJudgeStats() @@ -1770,22 +1832,50 @@ export function CompetitionManagement() { if (!selectedCompetitionForAction) return setIsLoading(true) - await new Promise((resolve) => setTimeout(resolve, 500)) - + + try { + // 調用 API 刪除競賽 + const response = await fetch(`/api/admin/competitions/${selectedCompetitionForAction.id}`, { + method: 'DELETE', + }) + + const data = await response.json() + + if (data.success) { + // 同時從 context 中刪除 deleteCompetition(selectedCompetitionForAction.id) setShowDeleteCompetitionConfirm(false) setSelectedCompetitionForAction(null) setSuccess("競賽刪除成功!") + + // 重新載入競賽列表 + await fetchCompetitions() + } else { + setError("競賽刪除失敗: " + data.message) + } + } catch (error) { + console.error('刪除競賽失敗:', error) + setError("競賽刪除失敗") + } finally { setIsLoading(false) + setTimeout(() => setError(""), 3000) setTimeout(() => setSuccess(""), 3000) + } } const handleUpdateStatus = async () => { if (!selectedCompetitionForAction) return setIsLoading(true) - await new Promise((resolve) => setTimeout(resolve, 500)) - + + try { + // 更新資料庫 + const updatedCompetition = await updateCompetitionInDb(selectedCompetitionForAction.id, { + status: newStatus, + }) + + if (updatedCompetition) { + // 同時更新 context updateCompetition(selectedCompetitionForAction.id, { ...selectedCompetitionForAction, status: newStatus, @@ -1794,8 +1884,16 @@ export function CompetitionManagement() { setShowChangeStatusDialog(false) setSelectedCompetitionForAction(null) setSuccess("競賽狀態更新成功!") + } else { + setError("競賽狀態更新失敗") + } + } catch (error) { + console.error('更新競賽狀態失敗:', error) + setError("競賽狀態更新失敗") + } finally { setIsLoading(false) - setTimeout(() => setSuccess(""), 3000) + setTimeout(() => setError(""), 3000) + } } const getCompetitionTypeIcon = (type: string) => { @@ -1918,6 +2016,8 @@ export function CompetitionManagement() { switch (status) { case "completed": return "bg-green-100 text-green-800 border-green-200" + case "ongoing": + return "bg-yellow-100 text-yellow-800 border-yellow-200" case "active": return "bg-blue-100 text-blue-800 border-blue-200" case "judging": @@ -1933,6 +2033,7 @@ export function CompetitionManagement() { switch (status) { case "completed": return "已完成" + case "ongoing": case "active": return "進行中" case "judging": @@ -2079,7 +2180,7 @@ export function CompetitionManagement() { {isLoadingDb ? ( ) : ( - dbStats?.active || displayCompetitions.filter((c) => c.status === "active").length + dbStats?.active || displayCompetitions.filter((c) => c.status === "active" || c.status === "ongoing").length )}

@@ -2245,14 +2346,14 @@ export function CompetitionManagement() { {!isCurrentCompetition && ( - setCurrentCompetition(competition)}> + setCurrentCompetitionInDb(competition.id)}> 設為當前競賽 )} {isCurrentCompetition && ( - setCurrentCompetition(null)}> + clearCurrentCompetitionInDb()}> 取消當前競賽 @@ -5395,9 +5496,9 @@ export function CompetitionManagement() {
-

+

{getStatusText(selectedCompetitionForAction.status)} -

+
diff --git a/components/competition/popularity-rankings.tsx b/components/competition/popularity-rankings.tsx index 9d5b57c..5ca9f8d 100644 --- a/components/competition/popularity-rankings.tsx +++ b/components/competition/popularity-rankings.tsx @@ -1,6 +1,6 @@ "use client" -import { useState } from "react" +import { useState, useEffect } from "react" import { useAuth } from "@/contexts/auth-context" import { useCompetition } from "@/contexts/competition-context" import { @@ -55,10 +55,67 @@ export function PopularityRankings() { const [individualCurrentPage, setIndividualCurrentPage] = useState(0) const [teamCurrentPage, setTeamCurrentPage] = useState(0) + // 新增狀態 + const [competitionApps, setCompetitionApps] = useState([]) + const [competitionTeams, setCompetitionTeams] = useState([]) + const [competitionJudges, setCompetitionJudges] = useState([]) + const [isLoading, setIsLoading] = useState(false) + const ITEMS_PER_PAGE = 3 + // 載入當前競賽的數據 + useEffect(() => { + if (currentCompetition) { + loadCompetitionData(currentCompetition.id) + } else { + // 如果沒有當前競賽,清空數據 + setCompetitionApps([]) + setCompetitionTeams([]) + setCompetitionJudges([]) + } + }, [currentCompetition]) + + const loadCompetitionData = async (competitionId: string) => { + setIsLoading(true) + try { + // 並行載入競賽的應用、團隊和評審數據 + const [appsResponse, teamsResponse, judgesResponse] = await Promise.all([ + fetch(`/api/competitions/${competitionId}/apps`), // 移除 competitionType 參數,載入所有應用 + fetch(`/api/competitions/${competitionId}/teams`), + fetch(`/api/competitions/${competitionId}/judges`) + ]) + + const [appsData, teamsData, judgesData] = await Promise.all([ + appsResponse.json(), + teamsResponse.json(), + judgesResponse.json() + ]) + + if (appsData.success) { + // 合併個人應用和團隊應用 + const allApps = appsData.data.apps || [] + console.log('📱 載入的應用數據:', allApps) + setCompetitionApps(allApps) + } + if (teamsData.success) { + const teams = teamsData.data.teams || [] + console.log('👥 載入的團隊數據:', teams) + setCompetitionTeams(teams) + } + if (judgesData.success) { + const judges = judgesData.data.judges || [] + console.log('👨‍⚖️ 載入的評審數據:', judges) + setCompetitionJudges(judges) + } + } catch (error) { + console.error('載入競賽數據失敗:', error) + } finally { + setIsLoading(false) + } + } + // Filter apps based on search criteria - const filteredApps = aiApps.filter((app) => { + const filteredApps = competitionApps.filter((app) => { const matchesSearch = app.name.toLowerCase().includes(searchTerm.toLowerCase()) || app.description.toLowerCase().includes(searchTerm.toLowerCase()) || @@ -72,9 +129,7 @@ export function PopularityRankings() { // Sort apps by like count (popularity) and group by competition type const sortedApps = filteredApps.sort((a, b) => { - const likesA = getLikeCount(a.id.toString()) - const likesB = getLikeCount(b.id.toString()) - return likesB - likesA + return (b.likes || 0) - (a.likes || 0) }) // Group apps by competition type @@ -319,23 +374,8 @@ export function PopularityRankings() { return null } - // Calculate team popularity score: total apps × highest like count - const teamsWithScores = teams - .map((team) => { - const appLikes = team.apps.map((appId: string) => getLikeCount(appId)) - const maxLikes = Math.max(...appLikes, 0) - const totalApps = team.apps.length - const popularityScore = totalApps * maxLikes - - return { - ...team, - popularityScore, - maxLikes, - totalApps, - totalViews: team.apps.reduce((sum: number, appId: string) => sum + getViewCount(appId), 0), - } - }) - .sort((a, b) => b.popularityScore - a.popularityScore) + // 團隊已經從 API 獲取了人氣分數,直接使用 + const teamsWithScores = teams.sort((a, b) => b.popularityScore - a.popularityScore) const currentPage = teamCurrentPage const setCurrentPage = setTeamCurrentPage @@ -512,7 +552,7 @@ export function PopularityRankings() {
- {team.apps.reduce((sum: number, appId: string) => sum + getLikeCount(appId), 0)} 總按讚 + {team.totalLikes || 0} 總按讚
-

{leader?.name}

+

{leader?.name || '未指定'}

-

{team.contactEmail}

+

{team.contact_email || '未提供'}

@@ -261,11 +317,12 @@ export function TeamDetailDialog({ open, onOpenChange, team }: TeamDetailDialogP
{team.apps.map((appId: string) => { - const app = getAppDetails(appId) - const IconComponent = app.icon - const likes = getLikeCount(appId) - const views = getViewCount(appId) - const rating = getAppRating(appId) + const app = getAppDetails(appId, team) + // 如果沒有圖標,使用默認的 Brain 圖標 + const IconComponent = app.icon || Brain + const likes = app.likes || 0 + const views = app.views || 0 + const rating = app.rating || 0 return (
- {rating.toFixed(1)} + {Number(rating).toFixed(1)}
e.stopPropagation()}> - +
diff --git a/contexts/competition-context.tsx b/contexts/competition-context.tsx index e300c84..5ec2129 100644 --- a/contexts/competition-context.tsx +++ b/contexts/competition-context.tsx @@ -122,6 +122,23 @@ export function CompetitionProvider({ children }: { children: ReactNode }) { const [teams, setTeams] = useState(mockTeams) const [proposals, setProposals] = useState(mockProposals) + // 載入當前競賽 + useEffect(() => { + const loadCurrentCompetition = async () => { + try { + const response = await fetch('/api/competitions/current') + const data = await response.json() + if (data.success && data.data) { + setCurrentCompetition(data.data) + } + } catch (error) { + console.error('載入當前競賽失敗:', error) + } + } + + loadCurrentCompetition() + }, []) + // Load judge scores from localStorage const [judgeScores, setJudgeScores] = useState(() => { if (typeof window !== "undefined") { @@ -186,6 +203,7 @@ export function CompetitionProvider({ children }: { children: ReactNode }) { } }, [awards]) + const addJudge = (judge: Omit) => { const newJudge: Judge = { ...judge, diff --git a/database-schema-simple.sql b/database-schema-simple.sql index 7a4878a..db981f7 100644 --- a/database-schema-simple.sql +++ b/database-schema-simple.sql @@ -89,12 +89,13 @@ CREATE TABLE `competitions` ( `month` INT NOT NULL, `start_date` DATE NOT NULL, `end_date` DATE NOT NULL, - `status` ENUM('upcoming', 'active', 'judging', 'completed') DEFAULT 'upcoming', + `status` ENUM('upcoming', 'ongoing', 'active', 'judging', 'completed') DEFAULT 'upcoming', `description` TEXT, `type` ENUM('individual', 'team', 'mixed', 'proposal') NOT NULL, `evaluation_focus` TEXT, `max_team_size` INT NULL, `is_active` BOOLEAN DEFAULT TRUE, + `is_current` BOOLEAN DEFAULT FALSE, `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX `idx_year_month` (`year`, `month`), diff --git a/lib/database-failover.js b/lib/database-failover.js index 8b0df35..4dabc72 100644 --- a/lib/database-failover.js +++ b/lib/database-failover.js @@ -323,7 +323,9 @@ class DatabaseFailover { async query(sql, params) { const connection = await this.getConnection(); try { - const [rows] = await connection.execute(sql, params); + // 將 undefined 值轉換為 null,避免 MySQL 驅動錯誤 + const sanitizedParams = params ? params.map(param => param === undefined ? null : param) : []; + const [rows] = await connection.execute(sql, sanitizedParams); return rows; } finally { @@ -345,7 +347,9 @@ class DatabaseFailover { async insert(sql, params) { const connection = await this.getConnection(); try { - const [result] = await connection.execute(sql, params); + // 將 undefined 值轉換為 null,避免 MySQL 驅動錯誤 + const sanitizedParams = params ? params.map(param => param === undefined ? null : param) : []; + const [result] = await connection.execute(sql, sanitizedParams); return result; } finally { @@ -356,7 +360,9 @@ class DatabaseFailover { async update(sql, params) { const connection = await this.getConnection(); try { - const [result] = await connection.execute(sql, params); + // 將 undefined 值轉換為 null,避免 MySQL 驅動錯誤 + const sanitizedParams = params ? params.map(param => param === undefined ? null : param) : []; + const [result] = await connection.execute(sql, sanitizedParams); return result; } finally { @@ -367,7 +373,9 @@ class DatabaseFailover { async delete(sql, params) { const connection = await this.getConnection(); try { - const [result] = await connection.execute(sql, params); + // 將 undefined 值轉換為 null,避免 MySQL 驅動錯誤 + const sanitizedParams = params ? params.map(param => param === undefined ? null : param) : []; + const [result] = await connection.execute(sql, sanitizedParams); return result; } finally { diff --git a/lib/database-sync-fixed.js b/lib/database-sync-fixed.js index 7f43a38..a799c2d 100644 --- a/lib/database-sync-fixed.js +++ b/lib/database-sync-fixed.js @@ -137,6 +137,53 @@ class DatabaseSyncFixed { } } + // 智能雙寫更新 + async smartDualUpdate(tableName, id, updates) { + const result = { + success: false, + masterSuccess: false, + slaveSuccess: false + }; + + try { + // 先獲取主機的記錄 + const masterRecord = await this.getMasterRecord(tableName, id); + + if (!masterRecord) { + throw new Error('主機記錄不存在'); + } + + // 獲取備機的記錄 ID(通過名稱匹配) + const slaveId = await this.getSlaveRecordIdByName(tableName, masterRecord.name); + + if (!slaveId) { + throw new Error('備機記錄不存在'); + } + + // 同時更新主機和備機 + const masterPromise = this.updateMasterRecord(tableName, id, updates); + const slavePromise = this.updateSlaveRecord(tableName, slaveId, updates); + + const [masterResult, slaveResult] = await Promise.allSettled([masterPromise, slavePromise]); + + result.masterSuccess = masterResult.status === 'fulfilled'; + result.slaveSuccess = slaveResult.status === 'fulfilled'; + result.success = result.masterSuccess || result.slaveSuccess; + + if (masterResult.status === 'rejected') { + result.masterError = masterResult.reason instanceof Error ? masterResult.reason.message : '主機更新失敗'; + } + if (slaveResult.status === 'rejected') { + result.slaveError = slaveResult.reason instanceof Error ? slaveResult.reason.message : '備機更新失敗'; + } + + } catch (error) { + result.masterError = error instanceof Error ? error.message : '雙寫更新執行失敗'; + } + + return result; + } + // 智能雙寫關聯表 - 使用對應的競賽 ID async smartDualInsertRelation(relationTable, masterCompetitionId, slaveCompetitionId, relationData, relationIdField) { const result = { @@ -370,6 +417,200 @@ class DatabaseSyncFixed { } } + // 獲取主機記錄 + async getMasterRecord(tableName, id) { + if (!this.masterPool) return null; + + const connection = await this.masterPool.getConnection(); + try { + const [rows] = await connection.execute(`SELECT * FROM ${tableName} WHERE id = ?`, [id]); + return rows[0] || null; + } finally { + connection.release(); + } + } + + // 根據名稱獲取備機記錄 ID + async getSlaveRecordIdByName(tableName, name) { + if (!this.slavePool) return null; + + const connection = await this.slavePool.getConnection(); + try { + const [rows] = await connection.execute(`SELECT id FROM ${tableName} WHERE name = ? ORDER BY created_at DESC LIMIT 1`, [name]); + const result = rows[0]?.id || null; + return result && typeof result !== 'string' ? String(result) : result; + } finally { + connection.release(); + } + } + + // 更新主機記錄 + async updateMasterRecord(tableName, id, updates) { + if (!this.masterPool) return; + + const fields = Object.keys(updates).filter(key => key !== 'id' && key !== 'created_at'); + if (fields.length === 0) return; + + const setClause = fields.map(field => `${field} = ?`).join(', '); + const values = fields.map(field => updates[field]); + + const connection = await this.masterPool.getConnection(); + try { + const sql = `UPDATE ${tableName} SET ${setClause}, updated_at = CURRENT_TIMESTAMP WHERE id = ?`; + await connection.execute(sql, [...values, id]); + } finally { + connection.release(); + } + } + + // 更新備機記錄 + async updateSlaveRecord(tableName, id, updates) { + if (!this.slavePool) return; + + const fields = Object.keys(updates).filter(key => key !== 'id' && key !== 'created_at'); + if (fields.length === 0) return; + + const setClause = fields.map(field => `${field} = ?`).join(', '); + const values = fields.map(field => updates[field]); + + const connection = await this.slavePool.getConnection(); + try { + const sql = `UPDATE ${tableName} SET ${setClause}, updated_at = CURRENT_TIMESTAMP WHERE id = ?`; + await connection.execute(sql, [...values, id]); + } finally { + connection.release(); + } + } + + // 清除所有競賽的當前狀態 + async clearAllCurrentCompetitions() { + const result = { + success: false, + masterSuccess: false, + slaveSuccess: false + }; + + try { + // 同時清除主機和備機的所有當前競賽狀態 + const masterPromise = this.clearMasterCurrentCompetitions(); + const slavePromise = this.clearSlaveCurrentCompetitions(); + + const [masterResult, slaveResult] = await Promise.allSettled([masterPromise, slavePromise]); + + result.masterSuccess = masterResult.status === 'fulfilled'; + result.slaveSuccess = slaveResult.status === 'fulfilled'; + result.success = result.masterSuccess || result.slaveSuccess; + + if (masterResult.status === 'rejected') { + result.masterError = masterResult.reason instanceof Error ? masterResult.reason.message : '主機清除失敗'; + } + if (slaveResult.status === 'rejected') { + result.slaveError = slaveResult.reason instanceof Error ? slaveResult.reason.message : '備機清除失敗'; + } + + } catch (error) { + result.masterError = error instanceof Error ? error.message : '清除所有當前競賽執行失敗'; + } + + return result; + } + + // 清除主機的所有當前競賽狀態 + async clearMasterCurrentCompetitions() { + if (!this.masterPool) return; + + const connection = await this.masterPool.getConnection(); + try { + await connection.execute('UPDATE competitions SET is_current = FALSE'); + } finally { + connection.release(); + } + } + + // 清除備機的所有當前競賽狀態 + async clearSlaveCurrentCompetitions() { + if (!this.slavePool) return; + + const connection = await this.slavePool.getConnection(); + try { + await connection.execute('UPDATE competitions SET is_current = FALSE'); + } finally { + connection.release(); + } + } + + // 智能雙寫刪除 + async smartDualDelete(tableName, id, idField = 'id') { + const result = { + success: false, + masterSuccess: false, + slaveSuccess: false + }; + + try { + // 獲取備機對應的 ID + let slaveId = null; + if (tableName === 'competitions') { + // 對於競賽表,先獲取競賽名稱 + const masterRecord = await this.getMasterRecord(tableName, id); + if (masterRecord) { + slaveId = await this.getSlaveRecordIdByName(tableName, masterRecord.name); + } + } else { + // 對於關聯表,直接使用主機 ID + slaveId = id; + } + + // 同時刪除主機和備機的記錄 + const masterPromise = this.deleteFromMaster(tableName, id, idField); + const slavePromise = slaveId ? this.deleteFromSlave(tableName, slaveId, idField) : Promise.resolve(); + + const [masterResult, slaveResult] = await Promise.allSettled([masterPromise, slavePromise]); + + result.masterSuccess = masterResult.status === 'fulfilled'; + result.slaveSuccess = slaveResult.status === 'fulfilled'; + result.success = result.masterSuccess || result.slaveSuccess; + + if (masterResult.status === 'rejected') { + result.masterError = masterResult.reason instanceof Error ? masterResult.reason.message : '主機刪除失敗'; + } + if (slaveResult.status === 'rejected') { + result.slaveError = slaveResult.reason instanceof Error ? slaveResult.reason.message : '備機刪除失敗'; + } + + } catch (error) { + result.masterError = error instanceof Error ? error.message : '雙寫刪除執行失敗'; + } + + return result; + } + + // 從主機刪除記錄 + async deleteFromMaster(tableName, id, idField = 'id') { + if (!this.masterPool) return; + + const connection = await this.masterPool.getConnection(); + try { + const sql = `DELETE FROM ${tableName} WHERE ${idField} = ?`; + await connection.execute(sql, [id]); + } finally { + connection.release(); + } + } + + // 從備機刪除記錄 + async deleteFromSlave(tableName, id, idField = 'id') { + if (!this.slavePool) return; + + const connection = await this.slavePool.getConnection(); + try { + const sql = `DELETE FROM ${tableName} WHERE ${idField} = ?`; + await connection.execute(sql, [id]); + } finally { + connection.release(); + } + } + // 清理資源 async close() { if (this.masterPool) { diff --git a/lib/database-sync-fixed.ts b/lib/database-sync-fixed.ts index f57f660..2d80c47 100644 --- a/lib/database-sync-fixed.ts +++ b/lib/database-sync-fixed.ts @@ -157,6 +157,53 @@ export class DatabaseSyncFixed { } } + // 智能雙寫更新 + async smartDualUpdate(tableName: string, id: string, updates: Record): Promise { + const result: WriteResult = { + success: false, + masterSuccess: false, + slaveSuccess: false + }; + + try { + // 先獲取主機的記錄 + const masterRecord = await this.getMasterRecord(tableName, id); + + if (!masterRecord) { + throw new Error('主機記錄不存在'); + } + + // 獲取備機的記錄 ID(通過名稱匹配) + const slaveId = await this.getSlaveRecordIdByName(tableName, masterRecord.name); + + if (!slaveId) { + throw new Error('備機記錄不存在'); + } + + // 同時更新主機和備機 + const masterPromise = this.updateMasterRecord(tableName, id, updates); + const slavePromise = this.updateSlaveRecord(tableName, slaveId, updates); + + const [masterResult, slaveResult] = await Promise.allSettled([masterPromise, slavePromise]); + + result.masterSuccess = masterResult.status === 'fulfilled'; + result.slaveSuccess = slaveResult.status === 'fulfilled'; + result.success = result.masterSuccess || result.slaveSuccess; + + if (masterResult.status === 'rejected') { + result.masterError = masterResult.reason instanceof Error ? masterResult.reason.message : '主機更新失敗'; + } + if (slaveResult.status === 'rejected') { + result.slaveError = slaveResult.reason instanceof Error ? slaveResult.reason.message : '備機更新失敗'; + } + + } catch (error) { + result.masterError = error instanceof Error ? error.message : '雙寫更新執行失敗'; + } + + return result; + } + // 智能雙寫關聯表 - 使用對應的競賽 ID async smartDualInsertRelation( relationTable: string, @@ -260,6 +307,71 @@ export class DatabaseSyncFixed { } } + // 獲取主機記錄 + private async getMasterRecord(tableName: string, id: string): Promise { + if (!this.masterPool) return null; + + const connection = await this.masterPool.getConnection(); + try { + const [rows] = await connection.execute(`SELECT * FROM ${tableName} WHERE id = ?`, [id]); + return (rows as any[])[0] || null; + } finally { + connection.release(); + } + } + + // 根據名稱獲取備機記錄 ID + private async getSlaveRecordIdByName(tableName: string, name: string): Promise { + if (!this.slavePool) return null; + + const connection = await this.slavePool.getConnection(); + try { + const [rows] = await connection.execute(`SELECT id FROM ${tableName} WHERE name = ? ORDER BY created_at DESC LIMIT 1`, [name]); + const result = (rows as any[])[0]?.id || null; + return result && typeof result !== 'string' ? String(result) : result; + } finally { + connection.release(); + } + } + + // 更新主機記錄 + private async updateMasterRecord(tableName: string, id: string, updates: Record): Promise { + if (!this.masterPool) return; + + const fields = Object.keys(updates).filter(key => key !== 'id' && key !== 'created_at'); + if (fields.length === 0) return; + + const setClause = fields.map(field => `${field} = ?`).join(', '); + const values = fields.map(field => updates[field]); + + const connection = await this.masterPool.getConnection(); + try { + const sql = `UPDATE ${tableName} SET ${setClause}, updated_at = CURRENT_TIMESTAMP WHERE id = ?`; + await connection.execute(sql, [...values, id]); + } finally { + connection.release(); + } + } + + // 更新備機記錄 + private async updateSlaveRecord(tableName: string, id: string, updates: Record): Promise { + if (!this.slavePool) return; + + const fields = Object.keys(updates).filter(key => key !== 'id' && key !== 'created_at'); + if (fields.length === 0) return; + + const setClause = fields.map(field => `${field} = ?`).join(', '); + const values = fields.map(field => updates[field]); + + const connection = await this.slavePool.getConnection(); + try { + const sql = `UPDATE ${tableName} SET ${setClause}, updated_at = CURRENT_TIMESTAMP WHERE id = ?`; + await connection.execute(sql, [...values, id]); + } finally { + connection.release(); + } + } + // 根據名稱獲取備機競賽 ID private async getSlaveCompetitionIdByName(name: string): Promise { if (!this.slavePool) return null; @@ -350,6 +462,135 @@ export class DatabaseSyncFixed { } } + // 清除所有競賽的當前狀態 + async clearAllCurrentCompetitions(): Promise { + const result: WriteResult = { + success: false, + masterSuccess: false, + slaveSuccess: false + }; + + try { + // 同時清除主機和備機的所有當前競賽狀態 + const masterPromise = this.clearMasterCurrentCompetitions(); + const slavePromise = this.clearSlaveCurrentCompetitions(); + + const [masterResult, slaveResult] = await Promise.allSettled([masterPromise, slavePromise]); + + result.masterSuccess = masterResult.status === 'fulfilled'; + result.slaveSuccess = slaveResult.status === 'fulfilled'; + result.success = result.masterSuccess || result.slaveSuccess; + + if (masterResult.status === 'rejected') { + result.masterError = masterResult.reason instanceof Error ? masterResult.reason.message : '主機清除失敗'; + } + if (slaveResult.status === 'rejected') { + result.slaveError = slaveResult.reason instanceof Error ? slaveResult.reason.message : '備機清除失敗'; + } + + } catch (error) { + result.masterError = error instanceof Error ? error.message : '清除所有當前競賽執行失敗'; + } + + return result; + } + + // 清除主機的所有當前競賽狀態 + private async clearMasterCurrentCompetitions(): Promise { + if (!this.masterPool) return; + + const connection = await this.masterPool.getConnection(); + try { + await connection.execute('UPDATE competitions SET is_current = FALSE'); + } finally { + connection.release(); + } + } + + // 清除備機的所有當前競賽狀態 + private async clearSlaveCurrentCompetitions(): Promise { + if (!this.slavePool) return; + + const connection = await this.slavePool.getConnection(); + try { + await connection.execute('UPDATE competitions SET is_current = FALSE'); + } finally { + connection.release(); + } + } + + // 智能雙寫刪除 + async smartDualDelete(tableName: string, id: string, idField: string = 'id'): Promise { + const result: WriteResult = { + success: false, + masterSuccess: false, + slaveSuccess: false + }; + + try { + // 獲取備機對應的 ID + let slaveId: string | null = null; + if (tableName === 'competitions') { + // 對於競賽表,先獲取競賽名稱 + const masterRecord = await this.getMasterRecord(tableName, id); + if (masterRecord) { + slaveId = await this.getSlaveRecordIdByName(tableName, masterRecord.name); + } + } else { + // 對於關聯表,直接使用主機 ID + slaveId = id; + } + + // 同時刪除主機和備機的記錄 + const masterPromise = this.deleteFromMaster(tableName, id, idField); + const slavePromise = slaveId ? this.deleteFromSlave(tableName, slaveId, idField) : Promise.resolve(); + + const [masterResult, slaveResult] = await Promise.allSettled([masterPromise, slavePromise]); + + result.masterSuccess = masterResult.status === 'fulfilled'; + result.slaveSuccess = slaveResult.status === 'fulfilled'; + result.success = result.masterSuccess || result.slaveSuccess; + + if (masterResult.status === 'rejected') { + result.masterError = masterResult.reason instanceof Error ? masterResult.reason.message : '主機刪除失敗'; + } + if (slaveResult.status === 'rejected') { + result.slaveError = slaveResult.reason instanceof Error ? slaveResult.reason.message : '備機刪除失敗'; + } + + } catch (error) { + result.masterError = error instanceof Error ? error.message : '雙寫刪除執行失敗'; + } + + return result; + } + + // 從主機刪除記錄 + private async deleteFromMaster(tableName: string, id: string, idField: string = 'id'): Promise { + if (!this.masterPool) return; + + const connection = await this.masterPool.getConnection(); + try { + const sql = `DELETE FROM ${tableName} WHERE ${idField} = ?`; + await connection.execute(sql, [id]); + } finally { + connection.release(); + } + } + + // 從備機刪除記錄 + private async deleteFromSlave(tableName: string, id: string, idField: string = 'id'): Promise { + if (!this.slavePool) return; + + const connection = await this.slavePool.getConnection(); + try { + const sql = `DELETE FROM ${tableName} WHERE ${idField} = ?`; + await connection.execute(sql, [id]); + } finally { + connection.release(); + } + } + // 清理資源 async close(): Promise { if (this.masterPool) { diff --git a/lib/services/database-service.ts b/lib/services/database-service.ts index 1341916..6964e3e 100644 --- a/lib/services/database-service.ts +++ b/lib/services/database-service.ts @@ -616,7 +616,7 @@ export class UserService { const competitionStatsSql = ` SELECT COUNT(*) as total_competitions, - COUNT(CASE WHEN status = 'active' THEN 1 END) as active_competitions + COUNT(CASE WHEN status = 'active' OR status = 'ongoing' THEN 1 END) as active_competitions FROM competitions `; const competitionStats = await this.queryOne(competitionStatsSql); @@ -1282,13 +1282,135 @@ export class CompetitionService { // 更新競賽 static async updateCompetition(id: string, updates: Partial): Promise { - const fields = Object.keys(updates).filter(key => key !== 'id' && key !== 'created_at'); - const setClause = fields.map(field => `${field} = ?`).join(', '); - const values = fields.map(field => (updates as any)[field]); - - const sql = `UPDATE competitions SET ${setClause}, updated_at = CURRENT_TIMESTAMP WHERE id = ?`; - const result = await db.update(sql, [...values, id]); - return result.affectedRows > 0; + try { + const dbSyncFixed = new DatabaseSyncFixed(); + + // 使用雙寫功能更新競賽 + const result = await dbSyncFixed.smartDualUpdate('competitions', id, updates); + + if (!result.success) { + console.error('競賽更新失敗:', result.masterError || result.slaveError); + return false; + } + + return true; + } catch (error) { + console.error('競賽更新失敗:', error); + return false; + } + } + + // 獲取當前競賽 + static async getCurrentCompetition(): Promise { + try { + const sql = 'SELECT * FROM competitions WHERE is_current = TRUE AND is_active = TRUE LIMIT 1'; + const competitions = await db.query(sql); + + if (competitions.length > 0) { + const competition = competitions[0]; + return await this.getCompetitionWithDetails(competition.id); + } + + return null; + } catch (error) { + console.error('獲取當前競賽失敗:', error); + return null; + } + } + + // 設置當前競賽 + static async setCurrentCompetition(competitionId: string): Promise { + try { + const dbSyncFixed = new DatabaseSyncFixed(); + + // 先清除所有競賽的當前狀態 - 使用直接 SQL 更新 + try { + await dbSyncFixed.clearAllCurrentCompetitions(); + } catch (error) { + console.error('清除當前競賽狀態失敗:', error); + } + + // 設置指定競賽為當前競賽 + const setResult = await dbSyncFixed.smartDualUpdate('competitions', competitionId, { is_current: true }); + + if (!setResult.success) { + console.error('設置當前競賽失敗:', setResult.masterError || setResult.slaveError); + return false; + } + + return true; + } catch (error) { + console.error('設置當前競賽失敗:', error); + return false; + } + } + + // 清除當前競賽 + static async clearCurrentCompetition(): Promise { + try { + await db.update('UPDATE competitions SET is_current = FALSE'); + + return true; + } catch (error) { + console.error('清除當前競賽失敗:', error); + return false; + } + } + + // 刪除競賽 - 支援雙寫 + static async deleteCompetition(id: string): Promise { + try { + const dbSyncFixed = new DatabaseSyncFixed(); + + // 先獲取競賽信息 + const competition = await this.getCompetitionById(id); + if (!competition) { + console.error('競賽不存在'); + return false; + } + + // 刪除關聯數據 + await this.deleteCompetitionRelations(id); + + // 使用雙寫功能刪除競賽 + const result = await dbSyncFixed.smartDualDelete('competitions', id); + + if (!result.success) { + console.error('競賽刪除失敗:', result.masterError || result.slaveError); + return false; + } + + return true; + } catch (error) { + console.error('競賽刪除失敗:', error); + return false; + } + } + + // 刪除競賽關聯數據 + private static async deleteCompetitionRelations(competitionId: string): Promise { + try { + const dbSyncFixed = new DatabaseSyncFixed(); + + // 刪除所有關聯表數據 + const relations = [ + 'competition_judges', + 'competition_teams', + 'competition_apps', + 'competition_award_types', + 'competition_rules' + ]; + + for (const relationTable of relations) { + try { + await dbSyncFixed.smartDualDelete(relationTable, competitionId, 'competition_id'); + } catch (error) { + console.error(`刪除關聯表 ${relationTable} 失敗:`, error); + } + } + } catch (error) { + console.error('刪除競賽關聯數據失敗:', error); + } } // ===================================================== @@ -1369,16 +1491,50 @@ export class CompetitionService { } // 獲取競賽的應用列表 - 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 getCompetitionApps(competitionId: string, competitionType?: string): Promise { + // 先獲取競賽信息 + const competition = await this.getCompetitionById(competitionId); + if (!competition) return []; + + let apps: any[] = []; + + if (competition.type === 'team') { + // 對於團隊競賽,獲取所有參賽團隊的應用 + const teams = await this.getCompetitionTeams(competitionId); + if (teams.length > 0) { + // 過濾掉 undefined 或 null 的 team_id 值 + const teamIds = teams + .map(t => t.team_id) + .filter(id => id !== undefined && id !== null); + + if (teamIds.length > 0) { + const placeholders = teamIds.map(() => '?').join(','); + + const sql = ` + SELECT a.*, u.name as creator_name, u.department as creator_department, t.name as team_name + FROM apps a + LEFT JOIN users u ON a.creator_id = u.id + LEFT JOIN teams t ON a.team_id = t.id + WHERE a.team_id IN (${placeholders}) AND a.is_active = TRUE + ORDER BY a.created_at ASC + `; + apps = await db.query(sql, teamIds); + } + } + } else { + // 對於個人競賽,從 competition_apps 表獲取 + 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 + `; + apps = await db.query(sql, [competitionId]); + } + + return apps; } // 為競賽添加團隊 @@ -1621,9 +1777,42 @@ export class CompetitionService { this.getCompetitionRules(competitionId) ]); + // 根據日期動態計算競賽狀態 + const now = new Date(); + const startDate = new Date(competition.start_date); + const endDate = new Date(competition.end_date); + + let calculatedStatus = competition.status; + + // 確保日期比較的準確性,使用 UTC 時間避免時區問題 + const nowUTC = new Date(now.getTime() + now.getTimezoneOffset() * 60000); + const startDateUTC = new Date(startDate.getTime() + startDate.getTimezoneOffset() * 60000); + const endDateUTC = new Date(endDate.getTime() + endDate.getTimezoneOffset() * 60000); + + console.log('🔍 競賽狀態計算:', { + competitionId, + name: competition.name, + now: nowUTC.toISOString(), + startDate: startDateUTC.toISOString(), + endDate: endDateUTC.toISOString(), + originalStatus: competition.status + }); + + // 根據實際日期計算狀態 + if (nowUTC < startDateUTC) { + calculatedStatus = 'upcoming'; // 即將開始 + } else if (nowUTC >= startDateUTC && nowUTC <= endDateUTC) { + calculatedStatus = 'active'; // 進行中 + } else if (nowUTC > endDateUTC) { + calculatedStatus = 'completed'; // 已完成 + } + + console.log('🔍 計算後的狀態:', calculatedStatus); + // 轉換字段名稱以匹配前端期望的格式 return { ...competition, + status: calculatedStatus, // 使用計算後的狀態 startDate: competition.start_date, endDate: competition.end_date, evaluationFocus: competition.evaluation_focus, @@ -2625,7 +2814,7 @@ export class AppService { const competitionStatsSql = ` SELECT COUNT(*) as total_competitions, - COUNT(CASE WHEN status = 'active' THEN 1 END) as active_competitions + COUNT(CASE WHEN status = 'active' OR status = 'ongoing' THEN 1 END) as active_competitions FROM competitions `; const competitionStats = await this.queryOne(competitionStatsSql); diff --git a/package.json b/package.json index 5ef7f65..a8a2d88 100644 --- a/package.json +++ b/package.json @@ -15,16 +15,6 @@ "migrate:data": "node scripts/migrate-data.js", "migrate:all": "pnpm run migrate:tables && pnpm run migrate:views && pnpm run migrate:triggers && pnpm run migrate:data", "migrate:reset": "node scripts/migrate.js --reset", - "test:db": "node scripts/test-connection.js", - "create:users": "node scripts/create-test-users.js", - "add:user-fields": "node scripts/add-user-fields.js", - "test:profile": "node scripts/test-profile-update.js", - "test:forgot-password": "node scripts/test-forgot-password.js", - "test:forgot-password-new": "node scripts/test-forgot-password-new-flow.js", - "test:password-visibility": "node scripts/test-password-visibility.js", - "test:role-display": "node scripts/test-role-display.js", - "test:activity-records": "node scripts/test-activity-records.js", - "test:hydration-fix": "node scripts/test-hydration-fix.js", "setup": "node scripts/setup.js", "db:init-slave": "node scripts/init-slave-database.js", "db:sync": "node scripts/sync-database.js",