329 lines
8.5 KiB
TypeScript
329 lines
8.5 KiB
TypeScript
// 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); |