檔案上傳新增至資料庫
This commit is contained in:
140
app/api/upload/route.ts
Normal file
140
app/api/upload/route.ts
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import { writeFile, mkdir } from 'fs/promises';
|
||||||
|
import { ProjectService, ProjectFileService, CriteriaItemService } from '@/lib/services/database';
|
||||||
|
import {
|
||||||
|
getUploadAbsolutePath,
|
||||||
|
getUploadRelativePath,
|
||||||
|
getUploadDirPath,
|
||||||
|
generateUniqueFileName,
|
||||||
|
isValidFileType,
|
||||||
|
isValidFileSize
|
||||||
|
} from '@/lib/utils/file-path';
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
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;
|
||||||
|
|
||||||
|
console.log('🚀 開始處理檔案上傳...');
|
||||||
|
console.log('📝 專案標題:', projectTitle);
|
||||||
|
console.log('📋 專案描述:', projectDescription);
|
||||||
|
console.log('📁 上傳文件:', file ? file.name : '無');
|
||||||
|
|
||||||
|
// 驗證必填欄位
|
||||||
|
if (!projectTitle?.trim()) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: '請填寫專案標題' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: '請上傳文件' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 驗證檔案類型
|
||||||
|
if (!isValidFileType(file.type)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: '不支援的檔案類型' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 驗證檔案大小(100MB)
|
||||||
|
if (!isValidFileSize(file.size, 100)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: '檔案大小超過 100MB 限制' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 獲取預設評分標準模板
|
||||||
|
const templates = await CriteriaItemService.getAllTemplates();
|
||||||
|
if (!templates || templates.length === 0) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: '未找到評分標準,請先設定評分標準' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const templateId = templates[0].id;
|
||||||
|
console.log('📊 使用評分標準模板 ID:', templateId);
|
||||||
|
|
||||||
|
// 創建專案記錄
|
||||||
|
const projectData = {
|
||||||
|
user_id: 1, // 暫時使用固定用戶 ID
|
||||||
|
template_id: templateId,
|
||||||
|
title: projectTitle,
|
||||||
|
description: projectDescription || null,
|
||||||
|
status: 'uploading' as const, // 初始狀態為上傳中
|
||||||
|
analysis_started_at: null,
|
||||||
|
analysis_completed_at: null
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('💾 創建專案記錄...');
|
||||||
|
const projectResult = await ProjectService.create(projectData);
|
||||||
|
const projectId = (projectResult as any).insertId;
|
||||||
|
console.log('✅ 專案記錄創建成功,ID:', projectId);
|
||||||
|
|
||||||
|
// 準備檔案儲存
|
||||||
|
const uploadDir = getUploadDirPath(projectId);
|
||||||
|
await mkdir(uploadDir, { recursive: true });
|
||||||
|
|
||||||
|
// 生成唯一檔案名稱
|
||||||
|
const uniqueFileName = generateUniqueFileName(file.name);
|
||||||
|
const filePath = getUploadAbsolutePath(projectId, uniqueFileName);
|
||||||
|
const relativeFilePath = getUploadRelativePath(projectId, uniqueFileName);
|
||||||
|
|
||||||
|
// 儲存檔案
|
||||||
|
console.log('💾 儲存檔案到:', filePath);
|
||||||
|
console.log('📁 相對路徑:', relativeFilePath);
|
||||||
|
const bytes = await file.arrayBuffer();
|
||||||
|
await writeFile(filePath, Buffer.from(bytes));
|
||||||
|
|
||||||
|
// 創建檔案記錄
|
||||||
|
const fileExtension = file.name.split('.').pop();
|
||||||
|
const fileData = {
|
||||||
|
project_id: projectId,
|
||||||
|
original_name: file.name,
|
||||||
|
file_name: uniqueFileName,
|
||||||
|
file_path: relativeFilePath, // 使用相對路徑
|
||||||
|
file_size: file.size,
|
||||||
|
file_type: fileExtension || '',
|
||||||
|
mime_type: file.type,
|
||||||
|
upload_status: 'completed' as const,
|
||||||
|
upload_progress: 100
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('💾 創建檔案記錄...');
|
||||||
|
await ProjectFileService.create(fileData);
|
||||||
|
console.log('✅ 檔案記錄創建成功');
|
||||||
|
|
||||||
|
// 更新專案狀態為 completed(檔案上傳完成)
|
||||||
|
console.log('🔄 更新專案狀態為 completed...');
|
||||||
|
await ProjectService.update(projectId, { status: 'completed' });
|
||||||
|
console.log('✅ 專案狀態更新完成');
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
projectId,
|
||||||
|
fileName: file.name,
|
||||||
|
fileSize: file.size,
|
||||||
|
fileType: file.type
|
||||||
|
},
|
||||||
|
message: '檔案上傳成功'
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 檔案上傳失敗:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false, error: '檔案上傳失敗,請稍後再試' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -135,25 +135,56 @@ export default function UploadPage() {
|
|||||||
setIsAnalyzing(true)
|
setIsAnalyzing(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('🚀 開始 AI 評審流程...')
|
console.log('🚀 開始上傳和評審流程...')
|
||||||
console.log('📝 專案標題:', projectTitle)
|
console.log('📝 專案標題:', projectTitle)
|
||||||
console.log('📋 專案描述:', projectDescription)
|
console.log('📋 專案描述:', projectDescription)
|
||||||
console.log('📁 上傳文件數量:', files.length)
|
console.log('📁 上傳文件數量:', files.length)
|
||||||
console.log('🌐 網站連結:', websiteUrl)
|
console.log('🌐 網站連結:', websiteUrl)
|
||||||
|
|
||||||
// 準備表單數據
|
let projectId = null;
|
||||||
|
|
||||||
|
// 如果有文件,先上傳到資料庫
|
||||||
|
if (files.length > 0) {
|
||||||
|
console.log('📤 上傳文件到資料庫...')
|
||||||
|
const firstFile = files[0]
|
||||||
|
if (firstFile.file) {
|
||||||
|
const uploadFormData = new FormData()
|
||||||
|
uploadFormData.append('projectTitle', projectTitle)
|
||||||
|
uploadFormData.append('projectDescription', projectDescription)
|
||||||
|
uploadFormData.append('file', firstFile.file)
|
||||||
|
|
||||||
|
const uploadResponse = await fetch('/api/upload', {
|
||||||
|
method: 'POST',
|
||||||
|
body: uploadFormData,
|
||||||
|
})
|
||||||
|
|
||||||
|
const uploadResult = await uploadResponse.json()
|
||||||
|
|
||||||
|
if (uploadResult.success) {
|
||||||
|
projectId = uploadResult.data.projectId
|
||||||
|
console.log('✅ 文件上傳成功,專案 ID:', projectId)
|
||||||
|
toast({
|
||||||
|
title: "文件上傳成功",
|
||||||
|
description: `專案已創建,ID: ${projectId}`,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
throw new Error(uploadResult.error || '文件上傳失敗')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('文件對象遺失,請重新上傳')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 準備 AI 評審數據
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('projectTitle', projectTitle)
|
formData.append('projectTitle', projectTitle)
|
||||||
formData.append('projectDescription', projectDescription)
|
formData.append('projectDescription', projectDescription)
|
||||||
|
|
||||||
if (files.length > 0) {
|
if (files.length > 0) {
|
||||||
// 只處理第一個文件(可以後續擴展支援多文件)
|
|
||||||
const firstFile = files[0]
|
const firstFile = files[0]
|
||||||
if (firstFile.file) {
|
if (firstFile.file) {
|
||||||
formData.append('file', firstFile.file)
|
formData.append('file', firstFile.file)
|
||||||
console.log('📄 處理文件:', firstFile.name, '大小:', firstFile.size)
|
console.log('📄 處理文件:', firstFile.name, '大小:', firstFile.size)
|
||||||
} else {
|
|
||||||
throw new Error('文件對象遺失,請重新上傳')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,8 +193,8 @@ export default function UploadPage() {
|
|||||||
console.log('🌐 處理網站連結:', websiteUrl)
|
console.log('🌐 處理網站連結:', websiteUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 發送評審請求
|
// 發送 AI 評審請求
|
||||||
console.log('📤 發送評審請求到 API...')
|
console.log('🤖 開始 AI 評審...')
|
||||||
const response = await fetch('/api/evaluate', {
|
const response = await fetch('/api/evaluate', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData,
|
body: formData,
|
||||||
@@ -195,9 +226,9 @@ export default function UploadPage() {
|
|||||||
throw new Error(result.error || '評審失敗')
|
throw new Error(result.error || '評審失敗')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ AI 評審失敗:', error)
|
console.error('❌ 上傳或評審失敗:', error)
|
||||||
toast({
|
toast({
|
||||||
title: "評審失敗",
|
title: "操作失敗",
|
||||||
description: error instanceof Error ? error.message : "請稍後再試",
|
description: error instanceof Error ? error.message : "請稍後再試",
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
})
|
})
|
||||||
|
67
lib/utils/file-path.ts
Normal file
67
lib/utils/file-path.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 檔案路徑工具函數
|
||||||
|
* 用於處理檔案上傳和路徑管理
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 獲取檔案上傳的絕對路徑(用於實際儲存)
|
||||||
|
*/
|
||||||
|
export function getUploadAbsolutePath(projectId: string | number, fileName: string): string {
|
||||||
|
return join(process.cwd(), 'uploads', 'projects', projectId.toString(), fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 獲取檔案上傳的相對路徑(用於資料庫儲存)
|
||||||
|
*/
|
||||||
|
export function getUploadRelativePath(projectId: string | number, fileName: string): string {
|
||||||
|
return `uploads/projects/${projectId}/${fileName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 獲取檔案上傳目錄的絕對路徑
|
||||||
|
*/
|
||||||
|
export function getUploadDirPath(projectId: string | number): string {
|
||||||
|
return join(process.cwd(), 'uploads', 'projects', projectId.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 從相對路徑獲取絕對路徑
|
||||||
|
*/
|
||||||
|
export function getAbsolutePathFromRelative(relativePath: string): string {
|
||||||
|
return join(process.cwd(), relativePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成唯一檔案名稱
|
||||||
|
*/
|
||||||
|
export function generateUniqueFileName(originalName: string): string {
|
||||||
|
const fileExtension = originalName.split('.').pop();
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const randomString = Math.random().toString(36).substr(2, 9);
|
||||||
|
return `${timestamp}_${randomString}.${fileExtension}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 驗證檔案類型
|
||||||
|
*/
|
||||||
|
export function isValidFileType(mimeType: string): boolean {
|
||||||
|
const allowedTypes = [
|
||||||
|
'application/vnd.ms-powerpoint',
|
||||||
|
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||||
|
'video/mp4',
|
||||||
|
'video/avi',
|
||||||
|
'video/quicktime',
|
||||||
|
'application/pdf'
|
||||||
|
];
|
||||||
|
return allowedTypes.includes(mimeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 驗證檔案大小
|
||||||
|
*/
|
||||||
|
export function isValidFileSize(size: number, maxSizeMB: number = 100): boolean {
|
||||||
|
const maxSize = maxSizeMB * 1024 * 1024; // 轉換為位元組
|
||||||
|
return size <= maxSize;
|
||||||
|
}
|
BIN
uploads/projects/1/1758622832603_gwou09ew5.pptx
Normal file
BIN
uploads/projects/1/1758622832603_gwou09ew5.pptx
Normal file
Binary file not shown.
BIN
uploads/projects/2/1758623148684_oaa90wfdy.pptx
Normal file
BIN
uploads/projects/2/1758623148684_oaa90wfdy.pptx
Normal file
Binary file not shown.
Reference in New Issue
Block a user