249 lines
5.5 KiB
JavaScript
249 lines
5.5 KiB
JavaScript
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; |