// server/models/User.ts import mongoose, { Document } from 'mongoose'; import bcrypt from 'bcrypt'; import { log } from 'console'; // 用户统计信息接口 export interface UserStats { totalPracticeCount: number; // 练习总次数 totalWords: number; // 练习单词总数 totalAccuracy: number; // 累计正确率(用于计算平均值) accuracyHistory: number[]; // 正确率历史记录 todayPracticeTime: number; // 今日练习时长(秒) lastPracticeDate: Date; // 最后练习日期 totalSpeed: number; // 累计速度,用于计算平均值 } // 扩展用户接口,包含统计信息 export interface IUser extends Document { username: string; password: string; email: string; fullname: string; isAdmin: boolean; stats: UserStats; createdAt: Date; updatedAt: Date; // 方法 resetTodayPracticeTime(): Promise; updatePracticeStats(data: { words: number; accuracy: number; duration: number; speed: number }): Promise; getAccuracyTrend(limit?: number): number[]; comparePassword(candidatePassword: string): Promise; } const userStatsSchema = new mongoose.Schema({ totalPracticeCount: { type: Number, default: 0, min: 0, validate: { validator: Number.isInteger, message: '练习次数必须是整数' } }, totalWords: { type: Number, default: 0, min: 0, validate: { validator: Number.isInteger, message: '单词总数必须是整数' } }, totalAccuracy: { type: Number, default: 0, min: 0, max: 100 * 1000000, // 假设最多100万次练习,每次100% validate: { validator: Number.isFinite, message: '累计正确率必须是有限数字' } }, accuracyHistory: { type: [Number], default: [], validate: { validator: function (v: number[]) { return v.every(rate => rate >= 0 && rate <= 100); }, message: '正确率必须在0到100之间' } }, todayPracticeTime: { type: Number, default: 0, min: 0, validate: { validator: Number.isInteger, message: '练习时长必须是整数' } }, lastPracticeDate: { type: Date, default: () => new Date() }, totalSpeed: { type: Number, default: 0, min: 0, validate: { validator: Number.isFinite, message: '速度必须是有效数字' } } }, { _id: false }); const userSchema = new mongoose.Schema({ username: { type: String, required: [true, '用户名是必需的'], unique: true, trim: true, minlength: [2, '用户名至少需要2个字符'], maxlength: [20, '用户名最多20个字符'] }, fullname: { type: String, required: [true, '姓名是必需的'], trim: true, minlength: [2, '姓名至少需要2个字符'], maxlength: [50, '姓名最多50个字符'] }, password: { type: String, required: [true, '密码是必需的'], minlength: [6, '密码至少需要6个字符'] }, email: { type: String, required: [true, '邮箱是必需的'], unique: true, trim: true, lowercase: true, match: [/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/, '请输入有效的邮箱地址'] }, isAdmin: { type: Boolean, default: false }, stats: { type: userStatsSchema, default: () => ({ totalPracticeCount: 0, totalWords: 0, totalAccuracy: 0, totalSpeed: 0, accuracyHistory: [], todayPracticeTime: 0, lastPracticeDate: new Date() }) } }, { timestamps: true }); // 中间件 userSchema.pre('save', function (next) { if (!this.stats) { this.stats = { totalPracticeCount: 0, totalWords: 0, totalAccuracy: 0, totalSpeed: 0, accuracyHistory: [], todayPracticeTime: 0, lastPracticeDate: new Date() }; } next(); }); userSchema.pre('save', async function(next) { const user = this; // 只有在密码被修改时才进行加密 if (!user.isModified('password')) { return next(); } try { // 生成盐值并加密密码 const salt = await bcrypt.genSalt(10); const hashedPassword = await bcrypt.hash(user.password, salt); user.password = hashedPassword; next(); } catch (error) { next(error as Error); } }); userSchema.methods.comparePassword = async function(candidatePassword: string): Promise { try { const result = await bcrypt.compare(candidatePassword, this.password); // 添加详细的调试信息 console.log('Password Comparison Details:', { input: candidatePassword, storedHash: this.password, result: result }); const password = String(202410121); const rounds = 10; // 生成盐值和hash const salt1 = await bcrypt.genSalt(rounds); const hash1 = await bcrypt.hash(password, salt1); const salt2 = await bcrypt.genSalt(rounds); const hash2 = await bcrypt.hash(password, salt2); console.log('Test results:', { password, hash1, hash2, compareResult1: await bcrypt.compare('202410121', hash1), compareResult2: await bcrypt.compare(password, hash2) }); return result; } catch (error) { console.error('Password comparison error:', error); throw error; } }; userSchema.pre('findOneAndUpdate', function (next) { const update = this.getUpdate() as any; if (update && !update.stats) { update.stats = { totalPracticeCount: 0, totalWords: 0, totalAccuracy: 0, totalSpeed: 0, accuracyHistory: [], todayPracticeTime: 0, lastPracticeDate: new Date() }; } next(); }); async function handlePasswordUpdate(this: any, next: Function) { const update = this.getUpdate() as any; if (update && (update.password || (update.$set && update.$set.password))) { try { const salt = await bcrypt.genSalt(10); if (update.password) { update.password = await bcrypt.hash(update.password, salt); } else if (update.$set && update.$set.password) { update.$set.password = await bcrypt.hash(update.$set.password, salt); } next(); } catch (error) { next(error as Error); } } else { next(); } } // 为每个操作添加中间件,使用相同的处理函数 userSchema.pre('findOneAndUpdate', handlePasswordUpdate); // 虚拟属性 userSchema.virtual('averageAccuracy').get(function (this: IUser) { if (this.stats.totalPracticeCount === 0) return 0; return this.stats.totalAccuracy / this.stats.totalPracticeCount; }); userSchema.virtual('averageSpeed').get(function(this: IUser) { if (this.stats.totalPracticeCount === 0) return 0; return this.stats.totalSpeed / this.stats.totalPracticeCount; }); // 方法 userSchema.methods.resetTodayPracticeTime = async function (this: IUser) { this.stats.todayPracticeTime = 0; await this.save(); }; userSchema.methods.updatePracticeStats = async function (this: IUser, { words, accuracy, duration, speed }: { words: number; accuracy: number; duration: number; speed: number; }) { const today = new Date(); today.setHours(0, 0, 0, 0); if (!this.stats.lastPracticeDate || this.stats.lastPracticeDate < today) { this.stats.todayPracticeTime = 0; } this.stats.totalPracticeCount += 1; this.stats.totalWords += words; this.stats.totalAccuracy = ((this.stats.totalAccuracy * (this.stats.totalPracticeCount - 1)) + accuracy) / this.stats.totalPracticeCount; this.stats.accuracyHistory.push(accuracy); if (this.stats.accuracyHistory.length > 10) { this.stats.accuracyHistory.shift(); // 保持最近10次记录 } this.stats.todayPracticeTime += Math.round(duration); this.stats.lastPracticeDate = new Date(); this.stats.totalSpeed = ((this.stats.totalSpeed * (this.stats.totalPracticeCount - 1)) + speed) / this.stats.totalPracticeCount; await this.save(); }; userSchema.methods.getAccuracyTrend = function (this: IUser, limit = 10): number[] { return this.stats.accuracyHistory.slice(-limit); }; // 索引 userSchema.index({ username: 1 }, { unique: true }); userSchema.index({ email: 1 }, { unique: true }); userSchema.index({ 'stats.lastPracticeDate': 1 }); // 错误处理 userSchema.post('save', function (error: any, doc: any, next: any) { if (error.name === 'MongoError' || error.name === 'MongoServerError') { if (error.code === 11000) { next(new Error('用户名或邮箱已存在')); } else { next(error); } } else { next(error); } }); // 创建模型并导出 export const User = mongoose.model('User', userSchema);