feat: Initial commit of PDF Tools project
This commit is contained in:
200
server/routes/conversion.js
Normal file
200
server/routes/conversion.js
Normal 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
193
server/routes/files.js
Normal 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
279
server/routes/system.js
Normal 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
327
server/routes/users.js
Normal 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;
|
||||
Reference in New Issue
Block a user