const mongoose = require('mongoose'); const conversionTaskSchema = new mongoose.Schema({ taskId: { type: String, required: true, unique: true, index: true }, batchId: { type: String, index: true // 用于批量转换任务分组 }, userId: { type: String, required: false, // 允许匿名用户 index: true }, fileId: { type: String, required: true, index: true }, sourceFile: { name: String, size: Number, type: String }, outputFormat: { type: String, required: true, enum: ['docx', 'html', 'txt', 'png', 'jpg', 'xlsx', 'pptx'] }, options: { // Word转换选项 preserveLayout: Boolean, includeImages: Boolean, imageQuality: { type: String, enum: ['low', 'medium', 'high'] }, ocrEnabled: Boolean, // HTML转换选项 responsive: Boolean, embedImages: Boolean, cssFramework: { type: String, enum: ['none', 'bootstrap', 'tailwind'] }, includeMetadata: Boolean, // 图片转换选项 resolution: { type: Number, min: 72, max: 300 }, format: { type: String, enum: ['png', 'jpg', 'webp'] }, quality: { type: Number, min: 1, max: 100 }, pageRange: { type: String, enum: ['all', 'first', 'custom'] }, customRange: { start: Number, end: Number }, // 文本转换选项 preserveLineBreaks: Boolean, encoding: { type: String, enum: ['utf8', 'gbk', 'ascii'] }, // 批量处理选项 mergeOutput: Boolean, zipResults: Boolean, namePattern: String }, status: { type: String, enum: ['pending', 'processing', 'completed', 'failed', 'cancelled'], default: 'pending', index: true }, progress: { type: Number, min: 0, max: 100, default: 0 }, priority: { type: Number, default: 0, index: true // 用于优先级队列 }, resultFile: { fileName: String, filePath: String, fileSize: Number, downloadUrl: String }, processingMetrics: { startTime: Date, endTime: Date, duration: Number, // 毫秒 peakMemoryUsage: Number, // 字节 cpuTime: Number // 毫秒 }, errorInfo: { code: String, message: String, stack: String, step: String // 失败的转换步骤 }, retryCount: { type: Number, default: 0, max: 3 }, queuePosition: Number, estimatedCompletionTime: Date, actualCompletionTime: Date, qualityScore: { type: Number, min: 0, max: 100 }, userRating: { score: { type: Number, min: 1, max: 5 }, feedback: String, ratedAt: Date } }, { timestamps: true }); // 复合索引 conversionTaskSchema.index({ userId: 1, createdAt: -1 }); conversionTaskSchema.index({ status: 1, priority: -1, createdAt: 1 }); conversionTaskSchema.index({ batchId: 1, status: 1 }); conversionTaskSchema.index({ fileId: 1, outputFormat: 1 }); // 虚拟字段:转换时长 conversionTaskSchema.virtual('durationFormatted').get(function() { if (!this.processingMetrics.duration) return null; const seconds = Math.floor(this.processingMetrics.duration / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); if (hours > 0) { return `${hours}h ${minutes % 60}m ${seconds % 60}s`; } else if (minutes > 0) { return `${minutes}m ${seconds % 60}s`; } else { return `${seconds}s`; } }); // 虚拟字段:是否可重试 conversionTaskSchema.virtual('canRetry').get(function() { return this.status === 'failed' && this.retryCount < 3; }); // 虚拟字段:是否可取消 conversionTaskSchema.virtual('canCancel').get(function() { return ['pending', 'processing'].includes(this.status); }); // 实例方法:开始处理 conversionTaskSchema.methods.startProcessing = function() { this.status = 'processing'; this.progress = 0; this.processingMetrics.startTime = new Date(); return this.save(); }; // 实例方法:更新进度 conversionTaskSchema.methods.updateProgress = function(progress, step) { this.progress = Math.min(100, Math.max(0, progress)); if (step) { this.errorInfo.step = step; } return this.save(); }; // 实例方法:标记完成 conversionTaskSchema.methods.markCompleted = function(resultFile) { this.status = 'completed'; this.progress = 100; this.resultFile = resultFile; this.processingMetrics.endTime = new Date(); this.actualCompletionTime = new Date(); if (this.processingMetrics.startTime) { this.processingMetrics.duration = this.processingMetrics.endTime - this.processingMetrics.startTime; } return this.save(); }; // 实例方法:标记失败 conversionTaskSchema.methods.markFailed = function(error) { this.status = 'failed'; this.errorInfo = { code: error.code || 'UNKNOWN_ERROR', message: error.message, stack: error.stack, step: error.step || 'unknown' }; this.processingMetrics.endTime = new Date(); if (this.processingMetrics.startTime) { this.processingMetrics.duration = this.processingMetrics.endTime - this.processingMetrics.startTime; } return this.save(); }; // 实例方法:取消任务 conversionTaskSchema.methods.cancel = function() { if (this.canCancel) { this.status = 'cancelled'; this.processingMetrics.endTime = new Date(); return this.save(); } throw new Error('任务无法取消'); }; // 实例方法:重试任务 conversionTaskSchema.methods.retry = function() { if (this.canRetry) { this.retryCount += 1; this.status = 'pending'; this.progress = 0; this.errorInfo = {}; this.processingMetrics = {}; return this.save(); } throw new Error('任务无法重试'); }; // 实例方法:设置用户评分 conversionTaskSchema.methods.setUserRating = function(score, feedback) { this.userRating = { score, feedback, ratedAt: new Date() }; return this.save(); }; // 静态方法:获取队列中的任务 conversionTaskSchema.statics.getQueuedTasks = function(limit = 10) { return this.find({ status: 'pending' }) .sort({ priority: -1, createdAt: 1 }) .limit(limit); }; // 静态方法:获取用户任务历史 conversionTaskSchema.statics.getUserTasks = function(userId, options = {}) { const { page = 1, limit = 10, status, outputFormat, sortBy = 'createdAt', sortOrder = -1 } = options; const query = { userId }; if (status) query.status = status; if (outputFormat) query.outputFormat = outputFormat; const skip = (page - 1) * limit; const sort = { [sortBy]: sortOrder }; return this.find(query) .sort(sort) .skip(skip) .limit(limit) .populate('fileId') .lean(); }; // 静态方法:获取转换统计 conversionTaskSchema.statics.getConversionStats = async function(timeRange = '7d') { const cutoffDate = new Date(); switch (timeRange) { case '1d': cutoffDate.setDate(cutoffDate.getDate() - 1); break; case '7d': cutoffDate.setDate(cutoffDate.getDate() - 7); break; case '30d': cutoffDate.setDate(cutoffDate.getDate() - 30); break; case '90d': cutoffDate.setDate(cutoffDate.getDate() - 90); break; } const pipeline = [ { $match: { createdAt: { $gte: cutoffDate } } }, { $group: { _id: '$status', count: { $sum: 1 }, avgDuration: { $avg: '$processingMetrics.duration' }, avgRating: { $avg: '$userRating.score' } } } ]; const statusStats = await this.aggregate(pipeline); const formatStats = await this.aggregate([ { $match: { createdAt: { $gte: cutoffDate } } }, { $group: { _id: '$outputFormat', count: { $sum: 1 }, successRate: { $avg: { $cond: [{ $eq: ['$status', 'completed'] }, 1, 0] } } } } ]); return { byStatus: statusStats, byFormat: formatStats, timeRange }; }; // 静态方法:清理旧任务 conversionTaskSchema.statics.cleanupOldTasks = async function(daysOld = 30) { const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - daysOld); const oldTasks = await this.find({ createdAt: { $lt: cutoffDate }, status: { $in: ['completed', 'failed', 'cancelled'] } }); // 删除关联的结果文件 const fs = require('fs'); let cleanedFiles = 0; for (const task of oldTasks) { if (task.resultFile && task.resultFile.filePath) { try { if (fs.existsSync(task.resultFile.filePath)) { fs.unlinkSync(task.resultFile.filePath); cleanedFiles++; } } catch (error) { console.error(`删除结果文件失败: ${task.resultFile.filePath}`, error); } } } // 删除数据库记录 const deleteResult = await this.deleteMany({ createdAt: { $lt: cutoffDate }, status: { $in: ['completed', 'failed', 'cancelled'] } }); return { deletedTasks: deleteResult.deletedCount, cleanedFiles }; }; // 中间件:保存后更新用户统计 conversionTaskSchema.post('save', async function(doc) { if (doc.userId && doc.isModified('status')) { try { const User = require('./User'); const updateData = {}; if (doc.status === 'completed') { updateData['$inc'] = { 'statistics.totalConversions': 1, 'statistics.successfulConversions': 1 }; } else if (doc.status === 'failed') { updateData['$inc'] = { 'statistics.totalConversions': 1, 'statistics.failedConversions': 1 }; } if (Object.keys(updateData).length > 0) { await User.findOneAndUpdate({ userId: doc.userId }, updateData); } } catch (error) { console.error('更新用户统计失败:', error); } } }); const ConversionTask = mongoose.model('ConversionTask', conversionTaskSchema); module.exports = ConversionTask;