export const dynamic = "force-dynamic"; import getDb from '@/lib/db'; import { getUserFromRequest, unauthorizedResponse } from '@/lib/auth'; import { v4 as uuidv4 } from 'uuid'; import path from 'path'; import fs from 'fs'; // POST /api/upload - 上传文件附件 export async function POST(request) { try { const user = getUserFromRequest(request); if (!user) return unauthorizedResponse(); const formData = await request.formData(); const file = formData.get('file'); const contract_id = formData.get('contract_id'); if (!file) { return Response.json( { error: '请选择要上传的文件' }, { status: 400 } ); } if (!contract_id) { return Response.json( { error: '请指定合同ID' }, { status: 400 } ); } // 确保上传目录存在 const uploadDir = path.join(process.cwd(), 'public', 'uploads', 'attachments'); if (!fs.existsSync(uploadDir)) { fs.mkdirSync(uploadDir, { recursive: true }); } // 生成唯一文件名 const ext = path.extname(file.name) || ''; const fileName = `${uuidv4()}${ext}`; const filePath = path.join(uploadDir, fileName); // 保存文件 const buffer = Buffer.from(await file.arrayBuffer()); fs.writeFileSync(filePath, buffer); // 相对于 public 的路径 const publicPath = `/api/upload?file=${fileName}`; // 插入附件记录 const db = await getDb(); const result = db.prepare( 'INSERT INTO contract_attachments (contract_id, file_name, file_path, file_size) VALUES (?, ?, ?, ?)' ).run(contract_id, file.name, publicPath, buffer.length); const attachment = db.prepare('SELECT * FROM contract_attachments WHERE id = ?').get(result.lastInsertRowid); return Response.json(attachment, { status: 201 }); } catch (error) { console.error('上传文件失败:', error); return Response.json( { error: '服务器内部错误' }, { status: 500 } ); } } // GET /api/upload - 动态读取并服务已上传的文件(规避 Next.js 静态文件缓存导致的 404) export async function GET(request) { try { const user = getUserFromRequest(request); if (!user) return unauthorizedResponse(); const { searchParams } = new URL(request.url); const file = searchParams.get('file'); if (!file) { return Response.json( { error: '缺少文件名' }, { status: 400 } ); } // 过滤路径,防止路径穿越安全漏洞 const cleanFile = path.basename(file); const filePath = path.join(process.cwd(), 'public', 'uploads', 'attachments', cleanFile); if (!fs.existsSync(filePath)) { return Response.json( { error: '文件不存在' }, { status: 404 } ); } const buffer = fs.readFileSync(filePath); const ext = path.extname(cleanFile).toLowerCase(); // 映射 Content-Type let contentType = 'application/octet-stream'; if (ext === '.png') contentType = 'image/png'; else if (ext === '.jpg' || ext === '.jpeg') contentType = 'image/jpeg'; else if (ext === '.gif') contentType = 'image/gif'; else if (ext === '.webp') contentType = 'image/webp'; else if (ext === '.bmp') contentType = 'image/bmp'; else if (ext === '.pdf') contentType = 'application/pdf'; else if (ext === '.xlsx') contentType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; else if (ext === '.xls') contentType = 'application/vnd.ms-excel'; else if (ext === '.docx') contentType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; else if (ext === '.txt') contentType = 'text/plain; charset=utf-8'; else if (ext === '.csv') contentType = 'text/csv; charset=utf-8'; else if (ext === '.json') contentType = 'application/json; charset=utf-8'; return new Response(buffer, { headers: { 'Content-Type': contentType, 'Content-Length': buffer.length.toString(), 'Content-Disposition': `inline; filename="${encodeURIComponent(cleanFile)}"`, 'Cache-Control': 'public, max-age=31536000, immutable' } }); } catch (error) { console.error('获取文件失败:', error); return Response.json( { error: '服务器内部错误' }, { status: 500 } ); } }