533 lines
20 KiB
TypeScript
533 lines
20 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
||
import { GeminiService } from '@/lib/services/gemini';
|
||
import { ProjectService, ProjectFileService, EvaluationService, EvaluationScoreService, EvaluationFeedbackService, CriteriaItemService } from '@/lib/services/database';
|
||
|
||
// 用於防止重複請求的簡單緩存
|
||
const processingRequests = new Set<string>();
|
||
|
||
export async function POST(request: NextRequest) {
|
||
let requestKey = '';
|
||
|
||
try {
|
||
const formData = await request.formData();
|
||
const projectTitle = formData.get('projectTitle') as string;
|
||
const projectDescription = formData.get('projectDescription') as string;
|
||
const file = formData.get('file') as File;
|
||
const websiteUrl = formData.get('websiteUrl') as string;
|
||
|
||
// 創建請求的唯一標識符
|
||
requestKey = `${projectTitle}_${file?.name || websiteUrl}_${Date.now()}`;
|
||
|
||
// 檢查是否正在處理相同的請求
|
||
if (processingRequests.has(requestKey)) {
|
||
console.log('⚠️ 檢測到重複請求,跳過處理');
|
||
return NextResponse.json(
|
||
{ success: false, error: '請求正在處理中,請勿重複提交' },
|
||
{ status: 429 }
|
||
);
|
||
}
|
||
|
||
// 標記請求為處理中
|
||
processingRequests.add(requestKey);
|
||
|
||
console.log('🚀 開始處理評審請求...');
|
||
console.log('📝 專案標題:', projectTitle);
|
||
console.log('📋 專案描述:', projectDescription);
|
||
console.log('📁 上傳文件:', file ? file.name : '無');
|
||
console.log('🌐 網站連結:', websiteUrl || '無');
|
||
console.log('🔑 請求標識符:', requestKey);
|
||
|
||
// 驗證必填欄位
|
||
if (!projectTitle?.trim()) {
|
||
return NextResponse.json(
|
||
{ success: false, error: '請填寫專案標題' },
|
||
{ status: 400 }
|
||
);
|
||
}
|
||
|
||
if (!file && !websiteUrl?.trim()) {
|
||
return NextResponse.json(
|
||
{ success: false, error: '請上傳文件或提供網站連結' },
|
||
{ status: 400 }
|
||
);
|
||
}
|
||
|
||
// 獲取評分標準
|
||
console.log('📊 載入評分標準...');
|
||
const templates = await CriteriaItemService.getAllTemplates();
|
||
console.log('📊 載入的模板:', templates);
|
||
|
||
if (!templates || templates.length === 0) {
|
||
return NextResponse.json(
|
||
{ success: false, error: '未找到評分標準,請先設定評分標準' },
|
||
{ status: 400 }
|
||
);
|
||
}
|
||
|
||
const templateId = templates[0].id;
|
||
const criteria = templates[0].items || [];
|
||
console.log('📊 評分項目:', criteria);
|
||
|
||
// 1. 檢查是否已有專案記錄(從前端傳入)
|
||
let projectId: number | null = null;
|
||
const projectIdStr = formData.get('projectId') as string;
|
||
if (projectIdStr) {
|
||
projectId = parseInt(projectIdStr);
|
||
}
|
||
|
||
if (!projectId) {
|
||
// 如果沒有傳入專案 ID,則創建新的專案記錄
|
||
console.log('💾 創建專案記錄...');
|
||
const projectData = {
|
||
user_id: 1, // 暫時使用固定用戶 ID
|
||
template_id: templateId,
|
||
title: projectTitle,
|
||
description: projectDescription || undefined,
|
||
status: 'analyzing' as const, // 狀態設為分析中
|
||
analysis_started_at: new Date(),
|
||
analysis_completed_at: undefined
|
||
};
|
||
|
||
const projectResult = await ProjectService.create(projectData);
|
||
projectId = (projectResult as any).insertId;
|
||
console.log('✅ 專案記錄創建成功,ID:', projectId);
|
||
} else {
|
||
console.log('✅ 使用現有專案記錄,ID:', projectId);
|
||
// 更新專案狀態為分析中
|
||
await ProjectService.update(projectId, {
|
||
status: 'analyzing',
|
||
analysis_started_at: new Date()
|
||
});
|
||
}
|
||
|
||
// 2. 處理文件上傳(如果有文件)
|
||
let content = '';
|
||
let projectFileId: number | null = null;
|
||
const projectFileIdStr = formData.get('projectFileId') as string;
|
||
if (projectFileIdStr) {
|
||
projectFileId = parseInt(projectFileIdStr);
|
||
}
|
||
|
||
if (file) {
|
||
// 檢查是否已有文件記錄(從前端傳入)
|
||
if (!projectFileId) {
|
||
// 如果沒有傳入文件 ID,則創建新的文件記錄
|
||
console.log('📄 開始處理上傳文件...');
|
||
|
||
// 檢查文件類型
|
||
const allowedTypes = [
|
||
'application/vnd.ms-powerpoint',
|
||
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||
'video/mp4',
|
||
'video/avi',
|
||
'video/quicktime',
|
||
'application/pdf',
|
||
'text/plain' // 添加純文字格式用於測試
|
||
];
|
||
|
||
if (!allowedTypes.includes(file.type)) {
|
||
console.log('❌ 不支援的文件格式:', file.type);
|
||
return NextResponse.json(
|
||
{ success: false, error: `不支援的文件格式: ${file.type}` },
|
||
{ status: 400 }
|
||
);
|
||
}
|
||
|
||
// 檢查文件大小 (100MB)
|
||
const maxSize = 100 * 1024 * 1024;
|
||
if (file.size > maxSize) {
|
||
return NextResponse.json(
|
||
{ success: false, error: '文件大小超過 100MB 限制' },
|
||
{ status: 400 }
|
||
);
|
||
}
|
||
|
||
// 創建文件記錄
|
||
const fileData = {
|
||
project_id: projectId!,
|
||
original_name: file.name,
|
||
file_name: file.name, // 簡化處理,實際應該生成唯一檔名
|
||
file_path: `/uploads/${projectId}/${file.name}`, // 簡化路徑
|
||
file_size: file.size,
|
||
file_type: file.name.split('.').pop() || '',
|
||
mime_type: file.type,
|
||
upload_status: 'completed' as const,
|
||
upload_progress: 100
|
||
};
|
||
|
||
const fileResult = await ProjectFileService.create(fileData);
|
||
projectFileId = (fileResult as any).insertId;
|
||
console.log('✅ 文件記錄創建成功,ID:', projectFileId);
|
||
} else {
|
||
console.log('✅ 使用現有文件記錄,ID:', projectFileId);
|
||
}
|
||
|
||
// 提取內容
|
||
content = await GeminiService.extractPPTContent(file);
|
||
} else if (websiteUrl) {
|
||
// 處理網站連結
|
||
console.log('🌐 開始處理網站連結...');
|
||
content = `網站內容分析: ${websiteUrl}
|
||
|
||
由於目前是簡化版本,無法直接抓取網站內容。
|
||
實際應用中應該使用網頁抓取技術來獲取網站內容進行分析。
|
||
|
||
專案描述: ${projectDescription || '無'}`;
|
||
}
|
||
|
||
// 3. 使用 Gemini AI 進行評分
|
||
console.log('🤖 開始 AI 評分...');
|
||
const startTime = Date.now();
|
||
|
||
// 轉換 criteria 格式以符合 GeminiService 的期望
|
||
const geminiCriteria = criteria.map(item => ({
|
||
id: item.id.toString(),
|
||
name: item.name,
|
||
description: item.description || '',
|
||
weight: item.weight,
|
||
maxScore: item.max_score
|
||
}));
|
||
|
||
const evaluation = await GeminiService.analyzePresentation(
|
||
content,
|
||
projectTitle,
|
||
projectDescription || '',
|
||
geminiCriteria
|
||
);
|
||
const analysisDuration = Math.round((Date.now() - startTime) / 1000);
|
||
console.log(`⏱️ AI 分析耗時: ${analysisDuration} 秒`);
|
||
|
||
// 4. 上傳評審結果到資料庫
|
||
console.log('💾 開始上傳評審結果到資料庫...');
|
||
|
||
try {
|
||
// 創建 evaluations 記錄
|
||
const evaluationData = {
|
||
project_id: projectId!,
|
||
overall_score: evaluation.totalScore,
|
||
max_possible_score: evaluation.maxTotalScore,
|
||
grade: evaluation.fullData?.grade || 'N/A',
|
||
performance_status: evaluation.fullData?.performanceStatus || 'N/A',
|
||
recommended_stars: evaluation.fullData?.recommendedStars || 0,
|
||
excellent_items: evaluation.fullData?.overview?.excellentItems || 0,
|
||
improvement_items: evaluation.fullData?.overview?.improvementItems || 0,
|
||
analysis_duration: analysisDuration,
|
||
ai_model_version: 'gemini-1.5-flash',
|
||
status: 'completed' as const,
|
||
error_message: undefined
|
||
};
|
||
|
||
const evaluationResult = await EvaluationService.create(evaluationData);
|
||
const evaluationId = (evaluationResult as any).insertId;
|
||
console.log('✅ Evaluations 記錄創建成功,ID:', evaluationId);
|
||
|
||
// 創建 evaluation_scores 記錄
|
||
// 確保為所有 criteria 創建記錄,即使 AI 沒有返回對應的結果
|
||
for (const criteriaItem of criteria) {
|
||
// 從 evaluation.results 中尋找對應的評分結果
|
||
console.log(`🔍 尋找評分標準: "${criteriaItem.name}"`);
|
||
console.log(`📋 可用的 AI 結果:`, evaluation.results.map(r => `"${r.criteriaName}"`));
|
||
|
||
// 使用更寬鬆的匹配方式,去除前後空格和不可見字符
|
||
const cleanCriteriaName = criteriaItem.name.trim().replace(/[\u200B-\u200D\uFEFF]/g, '');
|
||
console.log(`🧹 清理後的資料庫名稱: "${cleanCriteriaName}"`);
|
||
|
||
const result = evaluation.results.find(r => {
|
||
const cleanAiName = r.criteriaName.trim().replace(/[\u200B-\u200D\uFEFF]/g, '');
|
||
console.log(`🧹 清理後的 AI 名稱: "${cleanAiName}"`);
|
||
const isMatch = cleanAiName === cleanCriteriaName;
|
||
console.log(`🔍 匹配結果: ${isMatch}`);
|
||
return isMatch;
|
||
});
|
||
|
||
let score, maxScore, feedback, details;
|
||
|
||
if (result) {
|
||
// 如果有 AI 評分結果,使用 AI 的評分
|
||
score = result.score;
|
||
maxScore = result.maxScore;
|
||
feedback = result.feedback;
|
||
details = result.details;
|
||
} else {
|
||
// 如果沒有 AI 評分結果,使用預設值
|
||
console.warn(`⚠️ 找不到評分標準 "${criteriaItem.name}" 的 AI 評分結果,使用預設值`);
|
||
score = Math.floor(criteriaItem.max_score * 0.7); // 預設 70% 分數
|
||
maxScore = criteriaItem.max_score;
|
||
feedback = '基於資料庫評分標準的預設評語';
|
||
details = '基於資料庫評分標準的預設說明';
|
||
}
|
||
|
||
const scoreData = {
|
||
evaluation_id: evaluationId,
|
||
criteria_item_id: criteriaItem.id,
|
||
score: score,
|
||
max_score: maxScore,
|
||
weight: criteriaItem.weight,
|
||
weighted_score: (score / maxScore) * criteriaItem.weight,
|
||
percentage: (score / maxScore) * 100
|
||
};
|
||
|
||
// 調試日誌:檢查是否有 undefined 值
|
||
console.log(`🔍 檢查評分數據: ${criteriaItem.name}`, {
|
||
evaluation_id: evaluationId,
|
||
criteria_item_id: criteriaItem.id,
|
||
score: score,
|
||
max_score: maxScore,
|
||
weight: criteriaItem.weight,
|
||
weighted_score: (score / maxScore) * criteriaItem.weight,
|
||
percentage: (score / maxScore) * 100
|
||
});
|
||
|
||
// 驗證所有必要的值都存在
|
||
if (evaluationId === undefined || criteriaItem.id === undefined || score === undefined || maxScore === undefined || criteriaItem.weight === undefined) {
|
||
console.error(`❌ 評分數據驗證失敗: ${criteriaItem.name}`, scoreData);
|
||
throw new Error(`評分數據驗證失敗: ${criteriaItem.name} - 存在 undefined 值`);
|
||
}
|
||
|
||
await EvaluationScoreService.create(scoreData);
|
||
console.log(`✅ 創建評分記錄: ${criteriaItem.name} (ID: ${criteriaItem.id}) - ${score}/${maxScore}`);
|
||
}
|
||
console.log('✅ Evaluation Scores 記錄創建成功');
|
||
|
||
// 創建 evaluation_feedback 記錄
|
||
let sortOrder = 1;
|
||
|
||
// 整體反饋
|
||
await EvaluationFeedbackService.create({
|
||
evaluation_id: evaluationId,
|
||
criteria_item_id: undefined,
|
||
feedback_type: 'overall',
|
||
content: evaluation.overallFeedback,
|
||
sort_order: sortOrder++
|
||
});
|
||
|
||
// 各項標準的反饋
|
||
for (const result of evaluation.results) {
|
||
// 使用更寬鬆的匹配方式,去除前後空格和不可見字符
|
||
const cleanResultName = result.criteriaName.trim().replace(/[\u200B-\u200D\uFEFF]/g, '');
|
||
const criteriaItem = criteria.find(c => {
|
||
const cleanCriteriaName = c.name.trim().replace(/[\u200B-\u200D\uFEFF]/g, '');
|
||
return cleanCriteriaName === cleanResultName;
|
||
});
|
||
|
||
if (!criteriaItem) {
|
||
console.warn(`⚠️ 找不到對應的評分標準: "${result.criteriaName}"`);
|
||
continue;
|
||
}
|
||
|
||
// 合併 feedback 和 details 為一條記錄
|
||
let combinedContent = result.feedback;
|
||
if (result.details && result.details.trim()) {
|
||
combinedContent += `\n\n詳細說明:${result.details}`;
|
||
}
|
||
|
||
await EvaluationFeedbackService.create({
|
||
evaluation_id: evaluationId,
|
||
criteria_item_id: criteriaItem.id,
|
||
feedback_type: 'criteria',
|
||
content: combinedContent,
|
||
sort_order: sortOrder++
|
||
});
|
||
|
||
console.log(`✅ 創建 criteria feedback: ${criteriaItem.name} (ID: ${criteriaItem.id})`);
|
||
}
|
||
|
||
// 如果有 fullData,添加 strengths 和 improvements 反饋
|
||
if (evaluation.fullData) {
|
||
const fullData = evaluation.fullData;
|
||
|
||
// 為每個 criteria 添加 strengths 和 improvements
|
||
for (const criteriaData of fullData.criteria) {
|
||
// 使用清理後的名稱進行匹配
|
||
const cleanCriteriaDataName = criteriaData.name.trim().replace(/[\u200B-\u200D\uFEFF]/g, '');
|
||
const criteriaItem = criteria.find(c => {
|
||
const cleanCriteriaName = c.name.trim().replace(/[\u200B-\u200D\uFEFF]/g, '');
|
||
return cleanCriteriaName === cleanCriteriaDataName;
|
||
});
|
||
|
||
if (!criteriaItem) {
|
||
console.warn(`⚠️ 找不到對應的評分標準: "${criteriaData.name}" (清理後: "${cleanCriteriaDataName}")`);
|
||
continue;
|
||
}
|
||
|
||
console.log(`🔍 處理 criteria: "${criteriaData.name}" -> 匹配到: "${criteriaItem.name}" (ID: ${criteriaItem.id})`);
|
||
|
||
// 添加 strengths
|
||
if (criteriaData.strengths && criteriaData.strengths.length > 0) {
|
||
for (const strength of criteriaData.strengths) {
|
||
await EvaluationFeedbackService.create({
|
||
evaluation_id: evaluationId,
|
||
criteria_item_id: criteriaItem.id,
|
||
feedback_type: 'strength',
|
||
content: strength,
|
||
sort_order: sortOrder++
|
||
});
|
||
}
|
||
}
|
||
|
||
// 添加 improvements
|
||
if (criteriaData.improvements && criteriaData.improvements.length > 0) {
|
||
for (const improvement of criteriaData.improvements) {
|
||
await EvaluationFeedbackService.create({
|
||
evaluation_id: evaluationId,
|
||
criteria_item_id: criteriaItem.id,
|
||
feedback_type: 'improvement',
|
||
content: improvement,
|
||
sort_order: sortOrder++
|
||
});
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 如果有 fullData,添加額外的反饋
|
||
if (evaluation.fullData) {
|
||
const fullData = evaluation.fullData;
|
||
|
||
// 詳細分析摘要
|
||
if (fullData.detailedAnalysis?.summary) {
|
||
await EvaluationFeedbackService.create({
|
||
evaluation_id: evaluationId,
|
||
criteria_item_id: undefined,
|
||
feedback_type: 'overall',
|
||
content: fullData.detailedAnalysis.summary,
|
||
sort_order: sortOrder++
|
||
});
|
||
}
|
||
|
||
// 關鍵發現
|
||
if (fullData.detailedAnalysis?.keyFindings) {
|
||
for (const finding of fullData.detailedAnalysis.keyFindings) {
|
||
await EvaluationFeedbackService.create({
|
||
evaluation_id: evaluationId,
|
||
criteria_item_id: undefined,
|
||
feedback_type: 'overall',
|
||
content: finding,
|
||
sort_order: sortOrder++
|
||
});
|
||
}
|
||
}
|
||
|
||
// 改進建議
|
||
if (fullData.improvementSuggestions?.overallSuggestions) {
|
||
await EvaluationFeedbackService.create({
|
||
evaluation_id: evaluationId,
|
||
criteria_item_id: undefined,
|
||
feedback_type: 'improvement',
|
||
content: fullData.improvementSuggestions.overallSuggestions,
|
||
sort_order: sortOrder++
|
||
});
|
||
}
|
||
|
||
// 保持優勢 (maintainStrengths)
|
||
if (fullData.improvementSuggestions?.maintainStrengths) {
|
||
for (const strength of fullData.improvementSuggestions.maintainStrengths) {
|
||
await EvaluationFeedbackService.create({
|
||
evaluation_id: evaluationId,
|
||
criteria_item_id: undefined,
|
||
feedback_type: 'strength',
|
||
content: `${strength.title}: ${strength.description}`,
|
||
sort_order: sortOrder++
|
||
});
|
||
}
|
||
}
|
||
|
||
// 關鍵改進建議 (keyImprovements)
|
||
if (fullData.improvementSuggestions?.keyImprovements) {
|
||
for (const improvement of fullData.improvementSuggestions.keyImprovements) {
|
||
// 主要改進建議
|
||
await EvaluationFeedbackService.create({
|
||
evaluation_id: evaluationId,
|
||
criteria_item_id: undefined,
|
||
feedback_type: 'improvement',
|
||
content: `${improvement.title}: ${improvement.description}`,
|
||
sort_order: sortOrder++
|
||
});
|
||
|
||
// 具體建議 (suggestions)
|
||
if (improvement.suggestions && improvement.suggestions.length > 0) {
|
||
for (const suggestion of improvement.suggestions) {
|
||
await EvaluationFeedbackService.create({
|
||
evaluation_id: evaluationId,
|
||
criteria_item_id: undefined,
|
||
feedback_type: 'improvement',
|
||
content: `• ${suggestion}`,
|
||
sort_order: sortOrder++
|
||
});
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 行動計劃 (actionPlan)
|
||
if (fullData.improvementSuggestions?.actionPlan) {
|
||
for (const plan of fullData.improvementSuggestions.actionPlan) {
|
||
await EvaluationFeedbackService.create({
|
||
evaluation_id: evaluationId,
|
||
criteria_item_id: undefined,
|
||
feedback_type: 'improvement',
|
||
content: `${plan.phase}: ${plan.description}`,
|
||
sort_order: sortOrder++
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
console.log('✅ Evaluation Feedback 記錄創建成功');
|
||
|
||
// 5. 更新專案狀態為分析完成
|
||
await ProjectService.update(projectId!, {
|
||
status: 'completed',
|
||
analysis_completed_at: new Date()
|
||
});
|
||
console.log('✅ 專案狀態更新為分析完成');
|
||
|
||
} catch (dbError) {
|
||
console.error('❌ 資料庫上傳失敗:', dbError);
|
||
// 即使資料庫上傳失敗,也返回 AI 分析結果
|
||
// 但記錄錯誤以便後續處理
|
||
}
|
||
|
||
console.log('🎉 評審完成!');
|
||
console.log('📊 最終評分結果:');
|
||
console.log(' 總分:', evaluation.totalScore, '/', evaluation.maxTotalScore);
|
||
console.log(' 各項目評分:');
|
||
evaluation.results.forEach(result => {
|
||
console.log(` - ${result.criteriaName}: ${result.score}/${result.maxScore}`);
|
||
});
|
||
|
||
// 清理請求標識符
|
||
processingRequests.delete(requestKey);
|
||
|
||
return NextResponse.json({
|
||
success: true,
|
||
data: {
|
||
evaluation,
|
||
projectId: projectId,
|
||
projectFileId: projectFileId,
|
||
projectTitle,
|
||
projectDescription,
|
||
fileInfo: file ? {
|
||
name: file.name,
|
||
size: file.size,
|
||
type: file.type
|
||
} : null,
|
||
websiteUrl: websiteUrl || null
|
||
}
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('❌ 評審處理失敗:', error);
|
||
|
||
// 清理請求標識符(如果存在)
|
||
if (requestKey) {
|
||
processingRequests.delete(requestKey);
|
||
}
|
||
|
||
return NextResponse.json(
|
||
{ success: false, error: '評審處理失敗,請稍後再試' },
|
||
{ status: 500 }
|
||
);
|
||
}
|
||
}
|