Files
contractApproval/src/app/api/upload/route.js
T
2026-06-26 15:56:49 +08:00

128 lines
4.2 KiB
JavaScript

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 }
);
}
}