新增 AI 結果與資料庫整合
This commit is contained in:
@@ -1,8 +1,13 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { GeminiService } from '@/lib/services/gemini';
|
||||
import { CriteriaItemService } from '@/lib/services/database';
|
||||
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;
|
||||
@@ -10,11 +15,27 @@ export async function POST(request: NextRequest) {
|
||||
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()) {
|
||||
@@ -43,42 +64,102 @@ export async function POST(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
const templateId = templates[0].id;
|
||||
const criteria = templates[0].items || [];
|
||||
console.log('📊 評分項目:', criteria);
|
||||
console.log('✅ 評分標準載入完成,共', criteria.length, '個項目');
|
||||
|
||||
// 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) {
|
||||
// 處理文件上傳
|
||||
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 (!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 }
|
||||
);
|
||||
}
|
||||
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 }
|
||||
);
|
||||
// 檢查文件大小 (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);
|
||||
}
|
||||
|
||||
// 提取內容
|
||||
@@ -94,17 +175,300 @@ export async function POST(request: NextRequest) {
|
||||
專案描述: ${projectDescription || '無'}`;
|
||||
}
|
||||
|
||||
// 使用 Gemini AI 進行評分
|
||||
// 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 || '',
|
||||
criteria
|
||||
geminiCriteria
|
||||
);
|
||||
const analysisDuration = Math.round((Date.now() - startTime) / 1000);
|
||||
console.log(`⏱️ AI 分析耗時: ${analysisDuration} 秒`);
|
||||
|
||||
// 儲存評審結果到資料庫(可選)
|
||||
// TODO: 實作結果儲存功能
|
||||
// 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 criteriaItem = criteria.find(c => c.name === result.criteriaName);
|
||||
if (!criteriaItem) continue;
|
||||
|
||||
// 標準反饋
|
||||
await EvaluationFeedbackService.create({
|
||||
evaluation_id: evaluationId,
|
||||
criteria_item_id: criteriaItem.id,
|
||||
feedback_type: 'criteria',
|
||||
content: result.feedback,
|
||||
sort_order: sortOrder++
|
||||
});
|
||||
|
||||
// 詳細反饋
|
||||
await EvaluationFeedbackService.create({
|
||||
evaluation_id: evaluationId,
|
||||
criteria_item_id: criteriaItem.id,
|
||||
feedback_type: 'criteria',
|
||||
content: result.details,
|
||||
sort_order: sortOrder++
|
||||
});
|
||||
}
|
||||
|
||||
// 如果有 fullData,添加 strengths 和 improvements 反饋
|
||||
if (evaluation.fullData) {
|
||||
const fullData = evaluation.fullData;
|
||||
|
||||
// 為每個 criteria 添加 strengths 和 improvements
|
||||
for (const criteriaData of fullData.criteria) {
|
||||
const criteriaItem = criteria.find(c => c.name === criteriaData.name);
|
||||
if (!criteriaItem) continue;
|
||||
|
||||
// 添加 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('📊 最終評分結果:');
|
||||
@@ -114,10 +478,15 @@ export async function POST(request: NextRequest) {
|
||||
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 ? {
|
||||
@@ -131,6 +500,12 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 評審處理失敗:', error);
|
||||
|
||||
// 清理請求標識符(如果存在)
|
||||
if (requestKey) {
|
||||
processingRequests.delete(requestKey);
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ success: false, error: '評審處理失敗,請稍後再試' },
|
||||
{ status: 500 }
|
||||
|
@@ -111,8 +111,9 @@ export async function POST(request: NextRequest) {
|
||||
};
|
||||
|
||||
console.log('💾 創建檔案記錄...');
|
||||
await ProjectFileService.create(fileData);
|
||||
console.log('✅ 檔案記錄創建成功');
|
||||
const fileResult = await ProjectFileService.create(fileData);
|
||||
const projectFileId = (fileResult as any).insertId;
|
||||
console.log('✅ 檔案記錄創建成功,ID:', projectFileId);
|
||||
|
||||
// 更新專案狀態為 completed(檔案上傳完成)
|
||||
console.log('🔄 更新專案狀態為 completed...');
|
||||
@@ -123,6 +124,7 @@ export async function POST(request: NextRequest) {
|
||||
success: true,
|
||||
data: {
|
||||
projectId,
|
||||
projectFileId,
|
||||
fileName: file.name,
|
||||
fileSize: file.size,
|
||||
fileType: file.type
|
||||
|
Reference in New Issue
Block a user