const mongoose = require('mongoose'); const fileSchema = new mongoose.Schema({ fileId: { type: String, required: true, unique: true, index: true }, userId: { type: String, required: false, // 允许匿名用户上传 index: true }, originalName: { type: String, required: true, trim: true }, fileName: { type: String, required: true, unique: true }, fileSize: { type: Number, required: true, min: 0 }, mimeType: { type: String, required: true, enum: [ 'application/pdf', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'text/html', 'text/plain', 'image/png', 'image/jpeg' ] }, filePath: { type: String, required: true }, status: { type: String, enum: ['uploading', 'ready', 'processing', 'completed', 'failed', 'deleted'], default: 'ready' }, metadata: { pageCount: Number, width: Number, height: Number, hasImages: Boolean, hasText: Boolean, isEncrypted: Boolean, pdfVersion: String, creationDate: Date, modificationDate: Date, author: String, title: String, subject: String, keywords: String }, checksum: { type: String, index: true // 用于去重 }, downloadCount: { type: Number, default: 0 }, lastDownloaded: Date, tags: [String], isPublic: { type: Boolean, default: false }, expiresAt: { type: Date, index: { expireAfterSeconds: 0 } // MongoDB TTL索引,自动删除过期文件 } }, { timestamps: true }); // 复合索引 fileSchema.index({ userId: 1, createdAt: -1 }); fileSchema.index({ status: 1, createdAt: -1 }); fileSchema.index({ mimeType: 1, status: 1 }); fileSchema.index({ fileSize: 1 }); // 虚拟字段:格式化文件大小 fileSchema.virtual('formattedFileSize').get(function() { const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; if (this.fileSize === 0) return '0 Bytes'; const i = Math.floor(Math.log(this.fileSize) / Math.log(1024)); const size = (this.fileSize / Math.pow(1024, i)).toFixed(1); return size + ' ' + sizes[i]; }); // 虚拟字段:文件扩展名 fileSchema.virtual('fileExtension').get(function() { const mimeToExt = { 'application/pdf': 'pdf', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'docx', 'text/html': 'html', 'text/plain': 'txt', 'image/png': 'png', 'image/jpeg': 'jpg' }; return mimeToExt[this.mimeType] || 'unknown'; }); // 虚拟字段:是否已过期 fileSchema.virtual('isExpired').get(function() { if (!this.expiresAt) return false; return new Date() > this.expiresAt; }); // 实例方法:设置过期时间 fileSchema.methods.setExpiration = function(hours = 24) { this.expiresAt = new Date(Date.now() + hours * 60 * 60 * 1000); return this.save(); }; // 实例方法:增加下载次数 fileSchema.methods.incrementDownload = function() { this.downloadCount += 1; this.lastDownloaded = new Date(); return this.save(); }; // 实例方法:更新状态 fileSchema.methods.updateStatus = function(status) { this.status = status; return this.save(); }; // 静态方法:根据校验和查找重复文件 fileSchema.statics.findByChecksum = function(checksum) { return this.findOne({ checksum, status: { $ne: 'deleted' } }); }; // 静态方法:获取用户文件 fileSchema.statics.getUserFiles = function(userId, options = {}) { const { page = 1, limit = 10, status, mimeType, sortBy = 'createdAt', sortOrder = -1 } = options; const query = { userId }; if (status) query.status = status; if (mimeType) query.mimeType = mimeType; const skip = (page - 1) * limit; const sort = { [sortBy]: sortOrder }; return this.find(query) .sort(sort) .skip(skip) .limit(limit) .lean(); }; // 静态方法:清理过期文件 fileSchema.statics.cleanupExpiredFiles = async function() { const expiredFiles = await this.find({ expiresAt: { $lt: new Date() }, status: { $ne: 'deleted' } }); const fs = require('fs'); const path = require('path'); let cleanedCount = 0; let freedSpace = 0; for (const file of expiredFiles) { try { // 删除物理文件 if (fs.existsSync(file.filePath)) { freedSpace += file.fileSize; fs.unlinkSync(file.filePath); } // 更新数据库记录 await this.findByIdAndUpdate(file._id, { status: 'deleted' }); cleanedCount++; } catch (error) { console.error(`清理文件失败: ${file.fileName}`, error); } } return { cleanedCount, freedSpace }; }; // 静态方法:获取文件统计 fileSchema.statics.getFileStats = async function() { const pipeline = [ { $group: { _id: '$status', count: { $sum: 1 }, totalSize: { $sum: '$fileSize' } } } ]; const statusStats = await this.aggregate(pipeline); const typeStats = await this.aggregate([ { $group: { _id: '$mimeType', count: { $sum: 1 }, totalSize: { $sum: '$fileSize' } } } ]); const totalStats = await this.aggregate([ { $group: { _id: null, totalFiles: { $sum: 1 }, totalSize: { $sum: '$fileSize' }, totalDownloads: { $sum: '$downloadCount' } } } ]); return { byStatus: statusStats, byType: typeStats, total: totalStats[0] || { totalFiles: 0, totalSize: 0, totalDownloads: 0 } }; }; // 静态方法:搜索文件 fileSchema.statics.searchFiles = function(searchTerm, options = {}) { const { userId, page = 1, limit = 10, status = { $ne: 'deleted' } } = options; const query = { status, $or: [ { originalName: { $regex: searchTerm, $options: 'i' } }, { tags: { $in: [new RegExp(searchTerm, 'i')] } }, { 'metadata.title': { $regex: searchTerm, $options: 'i' } }, { 'metadata.subject': { $regex: searchTerm, $options: 'i' } } ] }; if (userId) query.userId = userId; const skip = (page - 1) * limit; return this.find(query) .sort({ createdAt: -1 }) .skip(skip) .limit(limit) .lean(); }; // 中间件:删除前清理物理文件 fileSchema.pre('remove', async function(next) { try { const fs = require('fs'); if (fs.existsSync(this.filePath)) { fs.unlinkSync(this.filePath); } next(); } catch (error) { console.error('删除物理文件失败:', error); next(error); } }); // 中间件:保存后更新用户统计 fileSchema.post('save', async function(doc) { if (doc.userId && doc.isNew) { try { const User = require('./User'); await User.findOneAndUpdate( { userId: doc.userId }, { $inc: { 'statistics.totalFileSize': doc.fileSize } } ); } catch (error) { console.error('更新用户统计失败:', error); } } }); const File = mongoose.model('File', fileSchema); module.exports = File;