feat: Initial commit of PDF Tools project
This commit is contained in:
249
server/models/User.js
Normal file
249
server/models/User.js
Normal file
@@ -0,0 +1,249 @@
|
||||
const mongoose = require('mongoose');
|
||||
const bcrypt = require('bcryptjs');
|
||||
|
||||
const userSchema = new mongoose.Schema({
|
||||
userId: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true,
|
||||
index: true
|
||||
},
|
||||
email: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true,
|
||||
lowercase: true,
|
||||
trim: true,
|
||||
match: [/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/, '请输入有效的邮箱地址']
|
||||
},
|
||||
username: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
minlength: 2,
|
||||
maxlength: 50
|
||||
},
|
||||
password: {
|
||||
type: String,
|
||||
required: true,
|
||||
minlength: 6
|
||||
},
|
||||
settings: {
|
||||
defaultOutputFormat: {
|
||||
type: String,
|
||||
enum: ['docx', 'html', 'txt', 'png', 'jpg'],
|
||||
default: 'docx'
|
||||
},
|
||||
imageQuality: {
|
||||
type: String,
|
||||
enum: ['low', 'medium', 'high'],
|
||||
default: 'medium'
|
||||
},
|
||||
autoDownload: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
language: {
|
||||
type: String,
|
||||
enum: ['zh-CN', 'en-US'],
|
||||
default: 'zh-CN'
|
||||
},
|
||||
autoDelete: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
deleteDelay: {
|
||||
type: Number,
|
||||
default: 24, // 小时
|
||||
min: 1,
|
||||
max: 168 // 7天
|
||||
},
|
||||
maxConcurrentTasks: {
|
||||
type: Number,
|
||||
default: 3,
|
||||
min: 1,
|
||||
max: 10
|
||||
},
|
||||
conversionTimeout: {
|
||||
type: Number,
|
||||
default: 10, // 分钟
|
||||
min: 5,
|
||||
max: 60
|
||||
}
|
||||
},
|
||||
profile: {
|
||||
avatar: String,
|
||||
bio: String,
|
||||
company: String,
|
||||
website: String
|
||||
},
|
||||
statistics: {
|
||||
totalConversions: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
successfulConversions: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
failedConversions: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
totalFileSize: {
|
||||
type: Number,
|
||||
default: 0 // 字节
|
||||
}
|
||||
},
|
||||
lastLoginAt: {
|
||||
type: Date,
|
||||
default: null
|
||||
},
|
||||
lastActiveAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
},
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
isEmailVerified: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
emailVerificationToken: String,
|
||||
passwordResetToken: String,
|
||||
passwordResetExpires: Date
|
||||
}, {
|
||||
timestamps: true,
|
||||
toJSON: {
|
||||
transform: function(doc, ret) {
|
||||
delete ret.password;
|
||||
delete ret.emailVerificationToken;
|
||||
delete ret.passwordResetToken;
|
||||
delete ret.passwordResetExpires;
|
||||
delete ret.__v;
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 索引
|
||||
userSchema.index({ email: 1 });
|
||||
userSchema.index({ username: 1 });
|
||||
userSchema.index({ createdAt: -1 });
|
||||
userSchema.index({ lastActiveAt: -1 });
|
||||
|
||||
// 密码加密中间件
|
||||
userSchema.pre('save', async function(next) {
|
||||
if (!this.isModified('password')) return next();
|
||||
|
||||
try {
|
||||
const salt = await bcrypt.genSalt(12);
|
||||
this.password = await bcrypt.hash(this.password, salt);
|
||||
next();
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// 实例方法:验证密码
|
||||
userSchema.methods.comparePassword = async function(candidatePassword) {
|
||||
try {
|
||||
return await bcrypt.compare(candidatePassword, this.password);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// 实例方法:更新最后活动时间
|
||||
userSchema.methods.updateLastActive = function() {
|
||||
this.lastActiveAt = new Date();
|
||||
return this.save();
|
||||
};
|
||||
|
||||
// 实例方法:更新统计信息
|
||||
userSchema.methods.updateStatistics = function(update) {
|
||||
Object.assign(this.statistics, update);
|
||||
return this.save();
|
||||
};
|
||||
|
||||
// 静态方法:根据邮箱查找用户
|
||||
userSchema.statics.findByEmail = function(email) {
|
||||
return this.findOne({ email: email.toLowerCase() });
|
||||
};
|
||||
|
||||
// 静态方法:获取活跃用户
|
||||
userSchema.statics.getActiveUsers = function(days = 30) {
|
||||
const cutoffDate = new Date();
|
||||
cutoffDate.setDate(cutoffDate.getDate() - days);
|
||||
|
||||
return this.find({
|
||||
lastActiveAt: { $gte: cutoffDate },
|
||||
isActive: true
|
||||
});
|
||||
};
|
||||
|
||||
// 静态方法:获取用户统计
|
||||
userSchema.statics.getUserStats = async function() {
|
||||
const pipeline = [
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
totalUsers: { $sum: 1 },
|
||||
activeUsers: {
|
||||
$sum: {
|
||||
$cond: [{ $eq: ['$isActive', true] }, 1, 0]
|
||||
}
|
||||
},
|
||||
verifiedUsers: {
|
||||
$sum: {
|
||||
$cond: [{ $eq: ['$isEmailVerified', true] }, 1, 0]
|
||||
}
|
||||
},
|
||||
totalConversions: { $sum: '$statistics.totalConversions' },
|
||||
totalFileSize: { $sum: '$statistics.totalFileSize' }
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const result = await this.aggregate(pipeline);
|
||||
return result[0] || {
|
||||
totalUsers: 0,
|
||||
activeUsers: 0,
|
||||
verifiedUsers: 0,
|
||||
totalConversions: 0,
|
||||
totalFileSize: 0
|
||||
};
|
||||
};
|
||||
|
||||
// 虚拟字段:转换成功率
|
||||
userSchema.virtual('conversionSuccessRate').get(function() {
|
||||
if (this.statistics.totalConversions === 0) return 0;
|
||||
return (this.statistics.successfulConversions / this.statistics.totalConversions * 100).toFixed(1);
|
||||
});
|
||||
|
||||
// 虚拟字段:格式化文件大小
|
||||
userSchema.virtual('formattedFileSize').get(function() {
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
if (this.statistics.totalFileSize === 0) return '0 Bytes';
|
||||
|
||||
const i = Math.floor(Math.log(this.statistics.totalFileSize) / Math.log(1024));
|
||||
return (this.statistics.totalFileSize / Math.pow(1024, i)).toFixed(1) + ' ' + sizes[i];
|
||||
});
|
||||
|
||||
// 中间件:删除前清理关联数据
|
||||
userSchema.pre('remove', async function(next) {
|
||||
try {
|
||||
// 这里可以添加删除用户相关文件和转换记录的逻辑
|
||||
// await FileModel.deleteMany({ userId: this.userId });
|
||||
// await ConversionTaskModel.deleteMany({ userId: this.userId });
|
||||
next();
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
const User = mongoose.model('User', userSchema);
|
||||
|
||||
module.exports = User;
|
||||
Reference in New Issue
Block a user