416 lines
14 KiB
TypeScript
416 lines
14 KiB
TypeScript
// =====================================================
|
||
// 評分管理 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 });
|
||
}
|
||
|
||
// 驗證評分格式(基於實際的競賽規則)
|
||
console.log('🔍 競賽規則:', rules);
|
||
console.log('🔍 提交的評分:', scores);
|
||
|
||
const providedScores = Object.keys(scores).filter(key => scores[key] > 0);
|
||
const invalidScores = providedScores.filter(score => scores[score] < 1 || scores[score] > 10);
|
||
|
||
console.log('🔍 提供的評分項目:', providedScores);
|
||
console.log('🔍 無效的評分項目:', invalidScores);
|
||
|
||
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 });
|
||
}
|
||
|
||
// 驗證所有競賽規則都有對應的評分
|
||
const ruleNames = rules.map((rule: any) => rule.name);
|
||
const missingRules = ruleNames.filter(ruleName => !(ruleName in scores) || scores[ruleName] <= 0);
|
||
|
||
if (missingRules.length > 0) {
|
||
console.log('⚠️ 缺少評分的規則:', missingRules);
|
||
console.log('可用規則:', ruleNames);
|
||
console.log('提供的評分:', Object.keys(scores));
|
||
}
|
||
|
||
// 計算總分(基於權重,轉換為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 });
|
||
}
|
||
}
|