feat: Initial commit of PDF Tools project

This commit is contained in:
2025-08-25 02:29:48 +08:00
parent af6827cd9e
commit 30180e50a2
48 changed files with 36364 additions and 1 deletions

200
server/routes/conversion.js Normal file
View File

@@ -0,0 +1,200 @@
const express = require('express');
const { v4: uuidv4 } = require('uuid');
const { optionalAuth } = require('../middleware/auth');
const conversionService = require('../services/conversionService');
const ConversionTask = require('../models/ConversionTask');
const File = require('../models/File');
const router = express.Router();
/**
* @route POST /api/convert/start
* @desc 开始一个新的文件转换任务
* @access Private (optional)
*/
router.post('/start', optionalAuth, async (req, res, next) => {
try {
const { fileId, outputFormat, options = {} } = req.body;
if (!fileId || !outputFormat) {
return res.status(400).json({ success: false, message: '缺少必要参数fileId和outputFormat' });
}
// 验证文件是否存在
const file = await File.findOne({ fileId });
if (!file) {
return res.status(404).json({ success: false, message: '文件未找到' });
}
// 创建转换任务
const task = await ConversionTask.create({
taskId: uuidv4(),
fileId,
outputFormat,
options,
userId: req.user?.userId || null,
sourceFile: {
name: file.fileName,
size: file.size,
type: file.mimeType
}
});
// 异步开始转换,不阻塞响应
conversionService.startConversion(task.taskId);
res.status(202).json({
success: true,
message: '转换任务已创建',
data: {
taskId: task.taskId,
status: task.status,
progress: task.progress
}
});
} catch (error) {
next(error);
}
});
/**
* @route GET /api/convert/status/:taskId
* @desc 查询转换任务的状态
* @access Public
*/
router.get('/status/:taskId', async (req, res, next) => {
try {
const { taskId } = req.params;
const task = await ConversionTask.findOne({ taskId }).lean();
if (!task) {
return res.status(404).json({ success: false, message: '转换任务未找到' });
}
res.json({ success: true, data: task });
} catch (error) {
next(error);
}
});
/**
* @route GET /api/convert/result/:taskId
* @desc 获取转换任务的结果
* @access Public
*/
router.get('/result/:taskId', async (req, res, next) => {
try {
const { taskId } = req.params;
const task = await ConversionTask.findOne({ taskId }).lean();
if (!task) {
return res.status(404).json({ success: false, message: '转换任务未找到' });
}
if (task.status !== 'completed') {
return res.status(400).json({ success: false, message: '转换尚未完成' });
}
res.json({
success: true,
data: {
taskId: task.taskId,
resultUrl: task.resultFile.downloadUrl,
fileName: task.resultFile.fileName,
fileSize: task.resultFile.fileSize
}
});
} catch (error) {
next(error);
}
});
/**
* @route POST /api/convert/batch
* @desc 开始批量转换任务
* @access Private (optional)
*/
router.post('/batch', optionalAuth, async (req, res, next) => {
try {
const { fileIds, outputFormat, options = {} } = req.body;
if (!fileIds || !Array.isArray(fileIds) || fileIds.length === 0) {
return res.status(400).json({ success: false, message: '请提供要转换的文件ID列表' });
}
if (fileIds.length > 10) {
return res.status(400).json({ success: false, message: '批量转换最多支持10个文件' });
}
const batchId = uuidv4();
const createdTasks = [];
for (const fileId of fileIds) {
const file = await File.findOne({ fileId });
if (file) {
const task = await ConversionTask.create({
taskId: uuidv4(),
batchId,
fileId,
outputFormat,
options,
userId: req.user?.userId || null,
sourceFile: {
name: file.fileName,
size: file.size,
type: file.mimeType
}
});
createdTasks.push(task);
// 异步开始转换
conversionService.startConversion(task.taskId);
}
}
res.status(202).json({
success: true,
message: '批量转换任务已创建',
data: {
batchId,
taskCount: createdTasks.length,
tasks: createdTasks.map(t => ({ taskId: t.taskId, status: t.status }))
}
});
} catch (error) {
next(error);
}
});
/**
* @route POST /api/convert/cancel/:taskId
* @desc 取消一个正在进行的转换任务
* @access Private (optional)
*/
router.post('/cancel/:taskId', optionalAuth, async (req, res, next) => {
try {
const { taskId } = req.params;
const task = await ConversionTask.findOne({ taskId });
if (!task) {
return res.status(404).json({ success: false, message: '转换任务未找到' });
}
// 权限检查:确保用户只能取消自己的任务
if (task.userId && (!req.user || task.userId !== req.user.userId)) {
return res.status(403).json({ success: false, message: '无权操作此任务' });
}
await task.cancel();
res.json({ success: true, message: '转换任务已取消' });
} catch (error) {
next(error);
}
});
module.exports = router;

193
server/routes/files.js Normal file
View File

@@ -0,0 +1,193 @@
const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const { v4: uuidv4 } = require('uuid');
const { optionalAuth } = require('../middleware/auth');
const router = express.Router();
// 确保上传目录存在
const uploadDir = path.join(__dirname, '../uploads');
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
// Multer配置
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, uploadDir);
},
filename: (req, file, cb) => {
const uniqueName = `${uuidv4()}-${file.originalname}`;
cb(null, uniqueName);
}
});
const fileFilter = (req, file, cb) => {
// 检查文件类型
if (file.mimetype === 'application/pdf') {
cb(null, true);
} else {
cb(new Error('只支持PDF文件格式'), false);
}
};
const upload = multer({
storage,
fileFilter,
limits: {
fileSize: 50 * 1024 * 1024, // 50MB限制
}
});
// 上传文件
router.post('/upload', optionalAuth, upload.single('file'), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({
success: false,
message: '未找到上传的文件'
});
}
const fileInfo = {
fileId: uuidv4(),
originalName: req.file.originalname,
fileName: req.file.filename,
fileSize: req.file.size,
mimeType: req.file.mimetype,
uploadTime: new Date(),
userId: req.user?.userId || null,
filePath: req.file.path
};
// 这里应该保存到数据库
// await FileModel.create(fileInfo);
res.json({
success: true,
message: '文件上传成功',
data: {
fileId: fileInfo.fileId,
originalName: fileInfo.originalName,
fileSize: fileInfo.fileSize,
uploadTime: fileInfo.uploadTime
}
});
} catch (error) {
console.error('文件上传错误:', error);
res.status(500).json({
success: false,
message: '文件上传失败'
});
}
});
// 获取文件信息
router.get('/:fileId', optionalAuth, async (req, res) => {
try {
const { fileId } = req.params;
// 这里应该从数据库查询
// const file = await FileModel.findOne({ fileId });
// 模拟数据
const file = {
fileId,
originalName: '示例文档.pdf',
fileSize: 2048576,
mimeType: 'application/pdf',
uploadTime: new Date(),
status: 'ready'
};
if (!file) {
return res.status(404).json({
success: false,
message: '文件未找到'
});
}
res.json({
success: true,
data: file
});
} catch (error) {
console.error('获取文件信息错误:', error);
res.status(500).json({
success: false,
message: '获取文件信息失败'
});
}
});
// 删除文件
router.delete('/:fileId', optionalAuth, async (req, res) => {
try {
const { fileId } = req.params;
// 这里应该从数据库查询文件信息
// const file = await FileModel.findOne({ fileId });
// 删除物理文件
const uploadDir = path.join(__dirname, '../uploads');
const files = fs.readdirSync(uploadDir);
const targetFile = files.find(file => file.includes(fileId));
if (targetFile) {
fs.unlinkSync(path.join(uploadDir, targetFile));
}
// 从数据库删除记录
// await FileModel.deleteOne({ fileId });
res.json({
success: true,
message: '文件删除成功'
});
} catch (error) {
console.error('删除文件错误:', error);
res.status(500).json({
success: false,
message: '删除文件失败'
});
}
});
// 下载文件
router.get('/download/:fileName', (req, res) => {
try {
const { fileName } = req.params;
const filePath = path.join(uploadDir, fileName);
if (!fs.existsSync(filePath)) {
return res.status(404).json({
success: false,
message: '文件不存在'
});
}
res.download(filePath, (err) => {
if (err) {
console.error('文件下载错误:', err);
res.status(500).json({
success: false,
message: '文件下载失败'
});
}
});
} catch (error) {
console.error('下载文件错误:', error);
res.status(500).json({
success: false,
message: '下载文件失败'
});
}
});
module.exports = router;

279
server/routes/system.js Normal file
View File

@@ -0,0 +1,279 @@
const express = require('express');
const os = require('os');
const fs = require('fs');
const path = require('path');
const router = express.Router();
// 系统健康检查
router.get('/health', (req, res) => {
try {
const uptime = process.uptime();
const memoryUsage = process.memoryUsage();
const cpuUsage = process.cpuUsage();
const healthData = {
status: 'OK',
timestamp: new Date().toISOString(),
uptime: {
seconds: Math.floor(uptime),
readable: formatUptime(uptime)
},
memory: {
rss: formatBytes(memoryUsage.rss),
heapTotal: formatBytes(memoryUsage.heapTotal),
heapUsed: formatBytes(memoryUsage.heapUsed),
external: formatBytes(memoryUsage.external)
},
cpu: {
user: cpuUsage.user,
system: cpuUsage.system
},
system: {
platform: os.platform(),
arch: os.arch(),
nodeVersion: process.version,
totalMemory: formatBytes(os.totalmem()),
freeMemory: formatBytes(os.freemem()),
loadAverage: os.loadavg(),
cpuCount: os.cpus().length
}
};
res.json(healthData);
} catch (error) {
console.error('健康检查错误:', error);
res.status(500).json({
status: 'ERROR',
message: '系统健康检查失败',
timestamp: new Date().toISOString()
});
}
});
// 系统统计信息
router.get('/stats', (req, res) => {
try {
const uploadDir = path.join(__dirname, '../uploads');
let filesCount = 0;
let totalSize = 0;
if (fs.existsSync(uploadDir)) {
const files = fs.readdirSync(uploadDir);
filesCount = files.length;
files.forEach(file => {
const filePath = path.join(uploadDir, file);
const stats = fs.statSync(filePath);
totalSize += stats.size;
});
}
const statsData = {
files: {
count: filesCount,
totalSize: formatBytes(totalSize)
},
conversions: {
total: 0, // 这里应该从数据库查询
successful: 0,
failed: 0,
inProgress: 0
},
users: {
total: 0, // 这里应该从数据库查询
active: 0,
newToday: 0
},
performance: {
averageConversionTime: '0s',
queueLength: 0,
errorRate: '0%'
}
};
res.json({
success: true,
data: statsData,
timestamp: new Date().toISOString()
});
} catch (error) {
console.error('获取系统统计错误:', error);
res.status(500).json({
success: false,
message: '获取系统统计失败'
});
}
});
// 支持的格式信息
router.get('/formats', (req, res) => {
try {
const supportedFormats = {
input: [
{
format: 'pdf',
mimeType: 'application/pdf',
description: 'PDF文档',
maxSize: '50MB'
}
],
output: [
{
format: 'docx',
mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
description: 'Microsoft Word文档',
features: ['保持布局', '提取图片', 'OCR支持']
},
{
format: 'html',
mimeType: 'text/html',
description: 'HTML网页',
features: ['响应式设计', '嵌入图片', 'CSS框架']
},
{
format: 'txt',
mimeType: 'text/plain',
description: '纯文本',
features: ['提取文本', '保留换行', '字符编码']
},
{
format: 'png',
mimeType: 'image/png',
description: 'PNG图片',
features: ['高质量', '透明背景', '无损压缩']
},
{
format: 'jpg',
mimeType: 'image/jpeg',
description: 'JPEG图片',
features: ['压缩率高', '质量可调', '广泛支持']
}
]
};
res.json({
success: true,
data: supportedFormats
});
} catch (error) {
console.error('获取格式信息错误:', error);
res.status(500).json({
success: false,
message: '获取格式信息失败'
});
}
});
// 系统配置信息
router.get('/config', (req, res) => {
try {
const config = {
upload: {
maxFileSize: '50MB',
allowedTypes: ['application/pdf'],
uploadDir: process.env.UPLOAD_DIR || './uploads'
},
conversion: {
timeout: process.env.CONVERSION_TIMEOUT || 300000,
maxConcurrent: process.env.MAX_CONCURRENT_CONVERSIONS || 5,
queueLimit: 100
},
security: {
rateLimiting: process.env.ENABLE_RATE_LIMITING === 'true',
rateLimit: {
window: process.env.RATE_LIMIT_WINDOW || 900000,
max: process.env.RATE_LIMIT_MAX || 100
}
},
features: {
userRegistration: true,
batchConversion: true,
previewSupport: false,
cloudStorage: false
}
};
res.json({
success: true,
data: config
});
} catch (error) {
console.error('获取系统配置错误:', error);
res.status(500).json({
success: false,
message: '获取系统配置失败'
});
}
});
// 清理临时文件
router.post('/cleanup', (req, res) => {
try {
const uploadDir = path.join(__dirname, '../uploads');
const maxAge = 24 * 60 * 60 * 1000; // 24小时
const now = Date.now();
let cleanedCount = 0;
let cleanedSize = 0;
if (fs.existsSync(uploadDir)) {
const files = fs.readdirSync(uploadDir);
files.forEach(file => {
const filePath = path.join(uploadDir, file);
const stats = fs.statSync(filePath);
if (now - stats.mtime.getTime() > maxAge) {
cleanedSize += stats.size;
fs.unlinkSync(filePath);
cleanedCount++;
}
});
}
res.json({
success: true,
message: '临时文件清理完成',
data: {
cleanedFiles: cleanedCount,
freedSpace: formatBytes(cleanedSize)
}
});
} catch (error) {
console.error('清理临时文件错误:', error);
res.status(500).json({
success: false,
message: '清理临时文件失败'
});
}
});
// 辅助函数:格式化字节大小
function formatBytes(bytes, decimals = 2) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
// 辅助函数:格式化运行时间
function formatUptime(uptime) {
const days = Math.floor(uptime / 86400);
const hours = Math.floor((uptime % 86400) / 3600);
const minutes = Math.floor((uptime % 3600) / 60);
const seconds = Math.floor(uptime % 60);
return `${days}d ${hours}h ${minutes}m ${seconds}s`;
}
module.exports = router;

327
server/routes/users.js Normal file
View File

@@ -0,0 +1,327 @@
const express = require('express');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const { auth } = require('../middleware/auth');
const router = express.Router();
// 模拟用户数据存储(生产环境应使用数据库)
const users = new Map();
// 用户注册
router.post('/register', async (req, res) => {
try {
const { email, username, password } = req.body;
// 验证必要字段
if (!email || !username || !password) {
return res.status(400).json({
success: false,
message: '请提供邮箱、用户名和密码'
});
}
// 检查邮箱格式
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return res.status(400).json({
success: false,
message: '邮箱格式不正确'
});
}
// 检查密码强度
if (password.length < 6) {
return res.status(400).json({
success: false,
message: '密码长度至少6位'
});
}
// 检查用户是否已存在
for (const user of users.values()) {
if (user.email === email) {
return res.status(400).json({
success: false,
message: '邮箱已被注册'
});
}
if (user.username === username) {
return res.status(400).json({
success: false,
message: '用户名已被使用'
});
}
}
// 加密密码
const hashedPassword = await bcrypt.hash(password, 12);
// 创建用户
const user = {
userId: Date.now().toString(),
email,
username,
password: hashedPassword,
createdAt: new Date(),
lastLoginAt: null,
settings: {
defaultOutputFormat: 'docx',
imageQuality: 'medium',
autoDownload: true,
language: 'zh-CN'
}
};
users.set(user.userId, user);
// 生成JWT令牌
const token = jwt.sign(
{ userId: user.userId, email: user.email },
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRES_IN || '24h' }
);
res.status(201).json({
success: true,
message: '注册成功',
data: {
user: {
userId: user.userId,
email: user.email,
username: user.username,
createdAt: user.createdAt
},
token
}
});
} catch (error) {
console.error('用户注册错误:', error);
res.status(500).json({
success: false,
message: '注册失败'
});
}
});
// 用户登录
router.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({
success: false,
message: '请提供邮箱和密码'
});
}
// 查找用户
let foundUser = null;
for (const user of users.values()) {
if (user.email === email) {
foundUser = user;
break;
}
}
if (!foundUser) {
return res.status(401).json({
success: false,
message: '邮箱或密码错误'
});
}
// 验证密码
const isPasswordValid = await bcrypt.compare(password, foundUser.password);
if (!isPasswordValid) {
return res.status(401).json({
success: false,
message: '邮箱或密码错误'
});
}
// 更新最后登录时间
foundUser.lastLoginAt = new Date();
users.set(foundUser.userId, foundUser);
// 生成JWT令牌
const token = jwt.sign(
{ userId: foundUser.userId, email: foundUser.email },
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRES_IN || '24h' }
);
res.json({
success: true,
message: '登录成功',
data: {
user: {
userId: foundUser.userId,
email: foundUser.email,
username: foundUser.username,
lastLoginAt: foundUser.lastLoginAt
},
token
}
});
} catch (error) {
console.error('用户登录错误:', error);
res.status(500).json({
success: false,
message: '登录失败'
});
}
});
// 获取用户信息
router.get('/profile', auth, async (req, res) => {
try {
const user = users.get(req.user.userId);
if (!user) {
return res.status(404).json({
success: false,
message: '用户未找到'
});
}
res.json({
success: true,
data: {
userId: user.userId,
email: user.email,
username: user.username,
createdAt: user.createdAt,
lastLoginAt: user.lastLoginAt,
settings: user.settings
}
});
} catch (error) {
console.error('获取用户信息错误:', error);
res.status(500).json({
success: false,
message: '获取用户信息失败'
});
}
});
// 更新用户设置
router.put('/settings', auth, async (req, res) => {
try {
const user = users.get(req.user.userId);
if (!user) {
return res.status(404).json({
success: false,
message: '用户未找到'
});
}
const allowedSettings = [
'defaultOutputFormat',
'imageQuality',
'autoDownload',
'language',
'autoDelete',
'deleteDelay',
'maxConcurrentTasks',
'conversionTimeout'
];
const updatedSettings = { ...user.settings };
for (const [key, value] of Object.entries(req.body)) {
if (allowedSettings.includes(key)) {
updatedSettings[key] = value;
}
}
user.settings = updatedSettings;
users.set(user.userId, user);
res.json({
success: true,
message: '设置更新成功',
data: {
settings: user.settings
}
});
} catch (error) {
console.error('更新用户设置错误:', error);
res.status(500).json({
success: false,
message: '更新设置失败'
});
}
});
// 获取转换历史
router.get('/history', auth, async (req, res) => {
try {
const { page = 1, limit = 10, status } = req.query;
// 模拟历史数据
const mockHistory = [
{
taskId: 'task-1',
fileName: '项目报告.pdf',
outputFormat: 'docx',
status: 'completed',
createdAt: new Date(Date.now() - 86400000), // 1天前
fileSize: '2.5MB'
},
{
taskId: 'task-2',
fileName: '用户手册.pdf',
outputFormat: 'html',
status: 'completed',
createdAt: new Date(Date.now() - 172800000), // 2天前
fileSize: '1.8MB'
},
{
taskId: 'task-3',
fileName: '技术文档.pdf',
outputFormat: 'txt',
status: 'failed',
createdAt: new Date(Date.now() - 259200000), // 3天前
fileSize: '3.2MB'
}
];
let filteredHistory = mockHistory;
if (status && status !== 'all') {
filteredHistory = mockHistory.filter(item => item.status === status);
}
const startIndex = (page - 1) * limit;
const endIndex = startIndex + parseInt(limit);
const paginatedHistory = filteredHistory.slice(startIndex, endIndex);
res.json({
success: true,
data: {
history: paginatedHistory,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total: filteredHistory.length,
pages: Math.ceil(filteredHistory.length / limit)
}
}
});
} catch (error) {
console.error('获取转换历史错误:', error);
res.status(500).json({
success: false,
message: '获取转换历史失败'
});
}
});
module.exports = router;