完成評審評分機制

This commit is contained in:
2025-09-18 18:34:31 +08:00
parent 2101767690
commit ffa1e45f63
54 changed files with 5730 additions and 709 deletions

View File

@@ -0,0 +1,166 @@
// =====================================================
// 評分記錄管理 API
// =====================================================
import { NextRequest, NextResponse } from 'next/server';
import { ScoringService } from '@/lib/services/database-service';
// 更新評分記錄
export async function PUT(request: NextRequest, { params }: { params: { id: string } }) {
try {
const { id } = await params;
const body = await request.json();
const {
participantType,
scores,
comments
} = body;
// 驗證必填欄位
if (!participantType || !scores) {
return NextResponse.json({
success: false,
message: '缺少必填欄位',
error: 'participantType, scores 為必填欄位'
}, { status: 400 });
}
// 驗證評分類型
if (!['app', 'proposal'].includes(participantType)) {
return NextResponse.json({
success: false,
message: '無效的參賽者類型',
error: 'participantType 必須是 "app" 或 "proposal"'
}, { status: 400 });
}
let result;
if (participantType === 'app') {
// 驗證應用評分格式
const requiredScores = ['innovation_score', 'technical_score', 'usability_score', 'presentation_score', 'impact_score'];
const missingScores = requiredScores.filter(score => !(score in scores) || scores[score] < 1 || scores[score] > 10);
if (missingScores.length > 0) {
return NextResponse.json({
success: false,
message: '評分格式無效',
error: `缺少或無效的評分項目: ${missingScores.join(', ')}`
}, { status: 400 });
}
// 計算總分
const totalScore = (
scores.innovation_score +
scores.technical_score +
scores.usability_score +
scores.presentation_score +
scores.impact_score
) / 5;
result = await ScoringService.updateAppScore(id, {
innovation_score: scores.innovation_score,
technical_score: scores.technical_score,
usability_score: scores.usability_score,
presentation_score: scores.presentation_score,
impact_score: scores.impact_score,
total_score: totalScore,
comments: comments || null
});
} else {
// 驗證提案評分格式
const requiredScores = ['problem_identification_score', 'solution_feasibility_score', 'innovation_score', 'impact_score', 'presentation_score'];
const missingScores = requiredScores.filter(score => !(score in scores) || scores[score] < 1 || scores[score] > 10);
if (missingScores.length > 0) {
return NextResponse.json({
success: false,
message: '評分格式無效',
error: `缺少或無效的評分項目: ${missingScores.join(', ')}`
}, { status: 400 });
}
// 計算總分
const totalScore = (
scores.problem_identification_score +
scores.solution_feasibility_score +
scores.innovation_score +
scores.impact_score +
scores.presentation_score
) / 5;
result = await ScoringService.updateProposalScore(id, {
problem_identification_score: scores.problem_identification_score,
solution_feasibility_score: scores.solution_feasibility_score,
innovation_score: scores.innovation_score,
impact_score: scores.impact_score,
presentation_score: scores.presentation_score,
total_score: totalScore,
comments: comments || null
});
}
if (!result) {
return NextResponse.json({
success: false,
message: '評分更新失敗',
error: '找不到指定的評分記錄或更新失敗'
}, { status: 404 });
}
return NextResponse.json({
success: true,
message: '評分更新成功',
data: { updated: true }
});
} 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, { params }: { params: { id: string } }) {
try {
const { id } = await params;
const { searchParams } = new URL(request.url);
const participantType = searchParams.get('type');
if (!participantType || !['app', 'proposal'].includes(participantType)) {
return NextResponse.json({
success: false,
message: '缺少或無效的參賽者類型',
error: 'type 參數必須是 "app" 或 "proposal"'
}, { status: 400 });
}
const result = await ScoringService.deleteScore(id, participantType as 'app' | 'proposal');
if (!result) {
return NextResponse.json({
success: false,
message: '評分刪除失敗',
error: '找不到指定的評分記錄或刪除失敗'
}, { status: 404 });
}
return NextResponse.json({
success: true,
message: '評分刪除成功',
data: { deleted: true }
});
} catch (error) {
console.error('刪除評分失敗:', error);
return NextResponse.json({
success: false,
message: '刪除評分失敗',
error: error instanceof Error ? error.message : '未知錯誤'
}, { status: 500 });
}
}

View File

@@ -0,0 +1,399 @@
// =====================================================
// 評分管理 API
// =====================================================
import { NextRequest, NextResponse } from 'next/server';
import { ScoringService } from '@/lib/services/database-service';
// 獲取競賽評分記錄
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const competitionId = searchParams.get('competitionId');
const judgeId = searchParams.get('judgeId');
const status = searchParams.get('status');
const search = searchParams.get('search');
if (!competitionId) {
return NextResponse.json({
success: false,
message: '缺少競賽ID',
error: 'competitionId 參數是必需的'
}, { status: 400 });
}
let scores;
if (judgeId) {
// 獲取特定評審的評分記錄
scores = await ScoringService.getJudgeScores(judgeId, competitionId);
} else {
// 獲取競賽的所有評分記錄
scores = await ScoringService.getCompetitionScores(competitionId);
}
// 狀態篩選
if (status && status !== 'all') {
if (status === 'completed') {
scores = scores.filter(score => score.total_score > 0);
} else if (status === 'pending') {
scores = scores.filter(score => !score.total_score || score.total_score === 0);
}
}
// 搜尋篩選
if (search) {
const searchLower = search.toLowerCase();
scores = scores.filter(score =>
score.judge_name?.toLowerCase().includes(searchLower) ||
score.app_name?.toLowerCase().includes(searchLower) ||
score.creator_name?.toLowerCase().includes(searchLower)
);
}
// 獲取評分統計
const stats = await ScoringService.getCompetitionScoreStats(competitionId);
// 處理評分數據,將 score_details 轉換為前端期望的格式
const processedScores = scores.map((score: any) => {
// 解析 score_details 字符串
let scoreDetails: Record<string, number> = {};
if (score.score_details) {
const details = score.score_details.split(',');
details.forEach((detail: string) => {
const [ruleName, scoreValue] = detail.split(':');
if (ruleName && scoreValue) {
scoreDetails[ruleName] = parseInt(scoreValue);
}
});
}
// 映射到前端期望的字段
return {
...score,
innovation_score: scoreDetails['創新性'] || scoreDetails['innovation'] || 0,
technical_score: scoreDetails['技術性'] || scoreDetails['technical'] || 0,
usability_score: scoreDetails['實用性'] || scoreDetails['usability'] || 0,
presentation_score: scoreDetails['展示效果'] || scoreDetails['presentation'] || 0,
impact_score: scoreDetails['影響力'] || scoreDetails['impact'] || 0,
};
});
return NextResponse.json({
success: true,
message: '評分記錄獲取成功',
data: {
scores: processedScores,
stats,
total: processedScores.length
}
});
} catch (error) {
console.error('獲取評分記錄失敗:', error);
return NextResponse.json({
success: false,
message: '獲取評分記錄失敗',
error: error instanceof Error ? error.message : '未知錯誤'
}, { status: 500 });
}
}
// 處理評分的輔助函數
async function processScoringWithCompetitionId(participantId: string, judgeId: string, scores: any, comments: string, competitionId: string, isEdit: boolean = false, recordId: string | null = null) {
const rules = await ScoringService.getCompetitionRules(competitionId);
if (!rules || rules.length === 0) {
return NextResponse.json({
success: false,
message: '競賽評分規則未設置',
error: '請先設置競賽評分規則'
}, { status: 400 });
}
// 驗證評分格式(基於實際的競賽規則)
const providedScores = Object.keys(scores).filter(key => scores[key] > 0);
const invalidScores = providedScores.filter(score => scores[score] < 1 || scores[score] > 10);
if (invalidScores.length > 0) {
return NextResponse.json({
success: false,
message: '評分格式無效',
error: `無效的評分項目: ${invalidScores.join(', ')}`
}, { status: 400 });
}
if (providedScores.length === 0) {
return NextResponse.json({
success: false,
message: '評分格式無效',
error: '至少需要提供一個評分項目'
}, { status: 400 });
}
// 計算總分基於權重轉換為100分制
let totalScore = 0;
let totalWeight = 0;
rules.forEach((rule: any) => {
const score = scores[rule.name];
if (score && score > 0) {
totalScore += score * (rule.weight / 100);
totalWeight += rule.weight;
}
});
// 如果總權重為0使用平均分
if (totalWeight === 0) {
const validScores = Object.values(scores).filter(score => score > 0);
totalScore = validScores.length > 0 ? validScores.reduce((sum, score) => sum + score, 0) / validScores.length : 0;
}
// 轉換為100分制10分制 * 10 = 100分制
totalScore = totalScore * 10;
// 將自定義評分映射到標準字段
const validScoreData: any = {
judge_id: judgeId,
app_id: participantId,
competition_id: competitionId,
scores: scores, // 傳遞原始評分數據
total_score: totalScore,
comments: comments || null,
isEdit: isEdit || false,
recordId: recordId || null
};
// 按順序將自定義評分映射到標準字段
const standardFields = ['innovation_score', 'technical_score', 'usability_score', 'presentation_score', 'impact_score'];
const customScores = Object.entries(scores).filter(([key, value]) => value > 0);
customScores.forEach(([customKey, score], index) => {
if (index < standardFields.length) {
validScoreData[standardFields[index]] = score;
}
});
const result = await ScoringService.submitAppScore(validScoreData);
return NextResponse.json({
success: true,
message: '評分提交成功',
data: result
});
}
// 提交評分
export async function POST(request: NextRequest) {
try {
const body = await request.json();
console.log('🔍 API 接收到的請求數據:', JSON.stringify(body, null, 2));
const {
judgeId,
participantId,
participantType,
scores,
comments,
competitionId,
isEdit,
recordId
} = body;
console.log('🔍 解析後的參數:');
console.log('judgeId:', judgeId, typeof judgeId);
console.log('participantId:', participantId, typeof participantId);
console.log('participantType:', participantType, typeof participantType);
console.log('scores:', scores, typeof scores);
console.log('competitionId:', competitionId, typeof competitionId);
console.log('isEdit:', isEdit, typeof isEdit);
console.log('recordId:', recordId, typeof recordId);
// 驗證必填欄位
if (!judgeId || !participantId || !participantType || !scores) {
console.log('❌ 缺少必填欄位驗證失敗');
return NextResponse.json({
success: false,
message: '缺少必填欄位',
error: 'judgeId, participantId, participantType, scores 為必填欄位'
}, { status: 400 });
}
// 驗證評分類型
if (!['app', 'proposal', 'team'].includes(participantType)) {
return NextResponse.json({
success: false,
message: '無效的參賽者類型',
error: 'participantType 必須是 "app"、"proposal" 或 "team"'
}, { status: 400 });
}
let result;
if (participantType === 'app') {
// 獲取競賽規則來驗證評分格式
let finalCompetitionId = await ScoringService.getCompetitionIdByAppId(participantId);
if (!finalCompetitionId) {
// 如果找不到競賽關聯嘗試通過其他方式獲取競賽ID
console.log('⚠️ 找不到APP的競賽關聯嘗試其他方式...');
// 檢查是否有其他方式獲取競賽ID例如通過請求參數
if (competitionId) {
console.log('✅ 使用參數中的競賽ID:', competitionId);
finalCompetitionId = competitionId;
} else {
return NextResponse.json({
success: false,
message: '找不到對應的競賽',
error: 'APP未註冊到任何競賽中請先在競賽管理中將APP添加到競賽'
}, { status: 400 });
}
}
const rules = await ScoringService.getCompetitionRules(finalCompetitionId);
if (!rules || rules.length === 0) {
return NextResponse.json({
success: false,
message: '競賽評分規則未設置',
error: '請先設置競賽評分規則'
}, { status: 400 });
}
// 驗證評分格式(基於實際的競賽規則)
const providedScores = Object.keys(scores).filter(key => scores[key] > 0);
const invalidScores = providedScores.filter(score => scores[score] < 1 || scores[score] > 10);
if (invalidScores.length > 0) {
return NextResponse.json({
success: false,
message: '評分格式無效',
error: `無效的評分項目: ${invalidScores.join(', ')}`
}, { status: 400 });
}
if (providedScores.length === 0) {
return NextResponse.json({
success: false,
message: '評分格式無效',
error: '至少需要提供一個評分項目'
}, { status: 400 });
}
// 計算總分基於權重轉換為100分制
let totalScore = 0;
let totalWeight = 0;
rules.forEach((rule: any) => {
const score = scores[rule.name];
if (score && score > 0) {
totalScore += score * (rule.weight / 100);
totalWeight += rule.weight;
}
});
// 如果總權重為0使用平均分
if (totalWeight === 0) {
const validScores = Object.values(scores).filter(score => score > 0);
totalScore = validScores.length > 0 ? validScores.reduce((sum, score) => sum + score, 0) / validScores.length : 0;
}
// 轉換為100分制10分制 * 10 = 100分制
totalScore = totalScore * 10;
// 使用新的基於競賽規則的評分系統
const validScoreData = {
judge_id: judgeId,
app_id: participantId,
competition_id: finalCompetitionId,
scores: scores, // 直接使用原始評分數據
total_score: totalScore,
comments: comments || null,
isEdit: isEdit || false,
recordId: recordId || null
};
result = await ScoringService.submitAppScore(validScoreData);
} else if (participantType === 'proposal') {
// 驗證提案評分格式
const requiredScores = ['problem_identification_score', 'solution_feasibility_score', 'innovation_score', 'impact_score', 'presentation_score'];
const missingScores = requiredScores.filter(score => !(score in scores) || scores[score] < 1 || scores[score] > 10);
if (missingScores.length > 0) {
return NextResponse.json({
success: false,
message: '評分格式無效',
error: `缺少或無效的評分項目: ${missingScores.join(', ')}`
}, { status: 400 });
}
// 計算總分
const totalScore = (
scores.problem_identification_score +
scores.solution_feasibility_score +
scores.innovation_score +
scores.impact_score +
scores.presentation_score
) / 5;
result = await ScoringService.submitProposalScore({
judge_id: judgeId,
proposal_id: participantId,
problem_identification_score: scores.problem_identification_score,
solution_feasibility_score: scores.solution_feasibility_score,
innovation_score: scores.innovation_score,
impact_score: scores.impact_score,
presentation_score: scores.presentation_score,
total_score: totalScore,
comments: comments || null
});
} else if (participantType === 'team') {
// 驗證團隊評分格式
const requiredScores = ['innovation_score', 'technical_score', 'usability_score', 'presentation_score', 'impact_score'];
const missingScores = requiredScores.filter(score => !(score in scores) || scores[score] < 1 || scores[score] > 10);
if (missingScores.length > 0) {
return NextResponse.json({
success: false,
message: '評分格式無效',
error: `缺少或無效的評分項目: ${missingScores.join(', ')}`
}, { status: 400 });
}
// 計算總分
const totalScore = (
scores.innovation_score +
scores.technical_score +
scores.usability_score +
scores.presentation_score +
scores.impact_score
) / 5;
// 團隊評分使用應用評分表
result = await ScoringService.submitTeamScore({
judge_id: judgeId,
teamId: participantId,
innovation_score: scores.innovation_score,
technical_score: scores.technical_score,
usability_score: scores.usability_score,
presentation_score: scores.presentation_score,
impact_score: scores.impact_score,
total_score: totalScore,
comments: comments || null
});
}
return NextResponse.json({
success: true,
message: '評分提交成功',
data: result
});
} catch (error) {
console.error('提交評分失敗:', error);
return NextResponse.json({
success: false,
message: '提交評分失敗',
error: error instanceof Error ? error.message : '未知錯誤'
}, { status: 500 });
}
}

View File

@@ -0,0 +1,39 @@
// =====================================================
// 評分統計 API
// =====================================================
import { NextRequest, NextResponse } from 'next/server';
import { ScoringService } from '@/lib/services/database-service';
// 獲取評分統計數據
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const competitionId = searchParams.get('competitionId');
if (!competitionId) {
return NextResponse.json({
success: false,
message: '缺少競賽ID',
error: 'competitionId 參數是必需的'
}, { status: 400 });
}
// 獲取評分統計
const stats = await ScoringService.getCompetitionScoreStats(competitionId);
return NextResponse.json({
success: true,
message: '評分統計獲取成功',
data: stats
});
} catch (error) {
console.error('獲取評分統計失敗:', error);
return NextResponse.json({
success: false,
message: '獲取評分統計失敗',
error: error instanceof Error ? error.message : '未知錯誤'
}, { status: 500 });
}
}

View File

@@ -0,0 +1,34 @@
import { NextRequest, NextResponse } from 'next/server';
import { ScoringService } from '@/lib/services/database-service';
// 獲取評分完成度匯總
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const competitionId = searchParams.get('competitionId');
if (!competitionId) {
return NextResponse.json({
success: false,
message: '缺少競賽ID參數'
}, { status: 400 });
}
// 獲取評分完成度匯總數據
const summary = await ScoringService.getScoringSummary(competitionId);
return NextResponse.json({
success: true,
message: '評分完成度匯總獲取成功',
data: summary
});
} catch (error) {
console.error('獲取評分完成度匯總失敗:', error);
return NextResponse.json({
success: false,
message: '獲取評分完成度匯總失敗',
error: error instanceof Error ? error.message : '未知錯誤'
}, { status: 500 });
}
}