Migrate project from typing_practiceweb

This commit is contained in:
2026-05-14 16:04:14 +08:00
parent dbc5425706
commit 0e87d5546d
225 changed files with 92811 additions and 0 deletions

329
server/models/User.ts Normal file
View 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);