Files
d1kt/server/models/User.ts

329 lines
8.5 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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<void>;
updatePracticeStats(data: { words: number; accuracy: number; duration: number; speed: number }): Promise<void>;
getAccuracyTrend(limit?: number): number[];
comparePassword(candidatePassword: string): Promise<boolean>;
}
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<boolean> {
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<IUser>('User', userSchema);