Migrate project from typing_practiceweb
This commit is contained in:
329
server/models/User.ts
Normal file
329
server/models/User.ts
Normal file
@@ -0,0 +1,329 @@
|
||||
// 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);
|
||||
Reference in New Issue
Block a user