Migrate project from typing_practiceweb
This commit is contained in:
38
server/models/CodeExample.ts
Normal file
38
server/models/CodeExample.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
// models/CodeExample.ts
|
||||
import mongoose, { Schema, Document } from 'mongoose';
|
||||
|
||||
export interface ICodeExample extends Document {
|
||||
title: string;
|
||||
content: string;
|
||||
level: 'basic' | 'intermediate' | 'advanced';
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
const codeExampleSchema = new Schema({
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
content: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
level: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: ['basic', 'intermediate', 'advanced']
|
||||
},
|
||||
createdAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
},
|
||||
updatedAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
}
|
||||
}, {
|
||||
timestamps: true // 自动管理 createdAt 和 updatedAt
|
||||
});
|
||||
|
||||
export const CodeExample = mongoose.model<ICodeExample>('CodeExample', codeExampleSchema);
|
||||
8
server/models/Keywords.ts
Normal file
8
server/models/Keywords.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import mongoose from 'mongoose';
|
||||
|
||||
const keywordsSchema = new mongoose.Schema({
|
||||
content: String,
|
||||
updatedAt: { type: Date, default: Date.now }
|
||||
});
|
||||
|
||||
export const Keywords = mongoose.model('Keywords', keywordsSchema);
|
||||
133
server/models/MinesweeperRecord.ts
Normal file
133
server/models/MinesweeperRecord.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
// server/models/MinesweeperRecord.ts
|
||||
|
||||
import mongoose, { Document, Schema } from 'mongoose';
|
||||
|
||||
// 扫雷难度级别
|
||||
export type MinesweeperDifficulty = 'beginner' | 'intermediate' | 'expert' | 'brutal';
|
||||
|
||||
// 扫雷游戏记录接口
|
||||
export interface IMinesweeperRecord extends Document {
|
||||
userId: mongoose.Types.ObjectId;
|
||||
username: string;
|
||||
fullname: string;
|
||||
difficulty: MinesweeperDifficulty;
|
||||
timeSeconds: number; // 完成时间(秒)
|
||||
won: boolean; // 是否获胜
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
// 排行榜聚合结果接口
|
||||
export interface IMinesweeperLeaderboardRecord {
|
||||
userId: mongoose.Types.ObjectId;
|
||||
username: string;
|
||||
fullname: string;
|
||||
bestTime: number; // 最佳时间
|
||||
totalGames: number; // 总游戏次数
|
||||
wonGames: number; // 获胜次数
|
||||
winRate: number; // 胜率
|
||||
lastPlayed: Date; // 最后游玩时间
|
||||
}
|
||||
|
||||
// 创建 Schema
|
||||
const minesweeperRecordSchema = new Schema<IMinesweeperRecord>({
|
||||
userId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true
|
||||
},
|
||||
username: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
fullname: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
difficulty: {
|
||||
type: String,
|
||||
enum: ['beginner', 'intermediate', 'expert', 'brutal'],
|
||||
required: true
|
||||
},
|
||||
timeSeconds: {
|
||||
type: Number,
|
||||
required: true,
|
||||
min: 0
|
||||
},
|
||||
won: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
// 创建索引以优化查询性能
|
||||
minesweeperRecordSchema.index({ userId: 1, difficulty: 1 });
|
||||
minesweeperRecordSchema.index({ difficulty: 1, won: 1, timeSeconds: 1 });
|
||||
|
||||
// 获取排行榜的静态方法
|
||||
minesweeperRecordSchema.statics.getLeaderboard = function(
|
||||
difficulty: MinesweeperDifficulty,
|
||||
skipCount: number,
|
||||
pageSize: number
|
||||
): mongoose.Aggregate<IMinesweeperLeaderboardRecord[]> {
|
||||
return this.aggregate([
|
||||
{
|
||||
$match: {
|
||||
difficulty // 统计所有记录(包括获胜和失败)
|
||||
}
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: '$userId',
|
||||
username: { $first: '$username' },
|
||||
fullname: { $first: '$fullname' },
|
||||
// 最佳时间只统计获胜的记录
|
||||
bestTime: {
|
||||
$min: {
|
||||
$cond: [{ $eq: ['$won', true] }, '$timeSeconds', null]
|
||||
}
|
||||
},
|
||||
totalGames: { $sum: 1 }, // 所有游戏次数(获胜+失败)
|
||||
wonGames: { $sum: { $cond: ['$won', 1, 0] } }, // 获胜次数
|
||||
lastPlayed: { $max: '$createdAt' }
|
||||
}
|
||||
},
|
||||
{
|
||||
// 过滤掉没有获胜记录的用户(因为排行榜需要最佳时间)
|
||||
$match: {
|
||||
bestTime: { $ne: null }
|
||||
}
|
||||
},
|
||||
{
|
||||
$addFields: {
|
||||
userId: '$_id',
|
||||
winRate: {
|
||||
$multiply: [
|
||||
{ $divide: ['$wonGames', '$totalGames'] },
|
||||
100
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{ $sort: { bestTime: 1 } }, // 按最佳时间升序排序(时间越短越好)
|
||||
{ $skip: skipCount },
|
||||
{ $limit: pageSize }
|
||||
]);
|
||||
};
|
||||
|
||||
// 扩展模型的静态方法类型
|
||||
interface MinesweeperRecordModel extends mongoose.Model<IMinesweeperRecord> {
|
||||
getLeaderboard(
|
||||
difficulty: MinesweeperDifficulty,
|
||||
skipCount: number,
|
||||
pageSize: number
|
||||
): mongoose.Aggregate<IMinesweeperLeaderboardRecord[]>;
|
||||
}
|
||||
|
||||
// 创建并导出模型
|
||||
export const MinesweeperRecord = mongoose.model<IMinesweeperRecord, MinesweeperRecordModel>(
|
||||
'MinesweeperRecord',
|
||||
minesweeperRecordSchema
|
||||
);
|
||||
163
server/models/PracticeRecord.ts
Normal file
163
server/models/PracticeRecord.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
// server/models/PracticeRecord.ts
|
||||
|
||||
import mongoose, { Document, Schema } from 'mongoose';
|
||||
|
||||
// 练习记录的统计数据接口
|
||||
interface IPracticeStats {
|
||||
totalWords: number; // 总单词数
|
||||
correctWords: number; // 正确单词数
|
||||
accuracy: number; // 正确率
|
||||
wordsPerMinute: number; // 每分钟单词数
|
||||
duration: number; // 持续时间(秒)
|
||||
startTime: Date; // 开始时间
|
||||
endTime: Date; // 结束时间
|
||||
}
|
||||
|
||||
// 练习记录接口
|
||||
export interface IPracticeRecord extends Document {
|
||||
userId: mongoose.Types.ObjectId; // 用户ID
|
||||
username: string; // 用户名
|
||||
fullname: string;// 姓名
|
||||
type: 'keyword' | 'basic' | 'intermediate' | 'advanced'; // 练习类型
|
||||
stats: IPracticeStats; // 练习统计数据
|
||||
createdAt: Date; // 记录创建时间
|
||||
updatedAt: Date; // 记录更新时间
|
||||
}
|
||||
// 排行榜聚合结果接口
|
||||
export interface ILeaderboardRecord {
|
||||
_id: mongoose.Types.ObjectId;
|
||||
userId: mongoose.Types.ObjectId;
|
||||
username: string;
|
||||
fullname: string;
|
||||
type: string;
|
||||
stats: {
|
||||
totalWords: number;
|
||||
accuracy: number;
|
||||
wordsPerMinute: number;
|
||||
duration: number;
|
||||
endTime: Date;
|
||||
};
|
||||
score: number;
|
||||
}
|
||||
// 创建 Schema
|
||||
const practiceRecordSchema = new Schema<IPracticeRecord>({
|
||||
userId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true
|
||||
},
|
||||
username: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
fullname: { // 姓名
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
enum: ['keyword', 'basic', 'intermediate', 'advanced'],
|
||||
required: true
|
||||
},
|
||||
stats: {
|
||||
totalWords: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
correctWords: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
accuracy: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
wordsPerMinute: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
duration: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
startTime: {
|
||||
type: Date,
|
||||
required: true
|
||||
},
|
||||
endTime: {
|
||||
type: Date,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
}, {
|
||||
timestamps: true // 自动添加 createdAt 和 updatedAt
|
||||
});
|
||||
practiceRecordSchema.statics.getLeaderboard = function(
|
||||
type: string,
|
||||
skipCount: number,
|
||||
pageSize: number
|
||||
): mongoose.Aggregate<ILeaderboardRecord[]> {
|
||||
return this.aggregate([
|
||||
{ $match: { type } },
|
||||
{
|
||||
$group: {
|
||||
_id: '$userId',
|
||||
username: { $first: '$username' },
|
||||
fullname: { $first: '$fullname' },
|
||||
totalDuration: { $sum: '$stats.duration' },
|
||||
avgAccuracy: { $avg: '$stats.accuracy' },
|
||||
totalWords: { $sum: '$stats.totalWords' },
|
||||
avgSpeed: { $avg: '$stats.wordsPerMinute' },
|
||||
lastPractice: { $max: '$stats.endTime' },
|
||||
originalRecord: { $first: '$$ROOT' }
|
||||
}
|
||||
},
|
||||
{
|
||||
$addFields: {
|
||||
score: {
|
||||
$add: [
|
||||
{ $multiply: [{ $divide: ['$avgAccuracy', 100] }, 40] },
|
||||
{ $multiply: [{ $divide: ['$avgSpeed', 100] }, 30] },
|
||||
{ $multiply: [{ $divide: ['$totalWords', 1000] }, 20] },
|
||||
{ $multiply: [{ $divide: ['$totalDuration', 3600] }, 10] }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{ $sort: { score: -1 } },
|
||||
{ $skip: skipCount },
|
||||
{ $limit: pageSize },
|
||||
{
|
||||
$project: {
|
||||
_id: '$originalRecord._id',
|
||||
userId: '$_id',
|
||||
username: 1,
|
||||
fullname: 1,
|
||||
type: '$originalRecord.type',
|
||||
stats: {
|
||||
totalWords: '$totalWords',
|
||||
accuracy: '$avgAccuracy',
|
||||
wordsPerMinute: '$avgSpeed',
|
||||
duration: '$totalDuration',
|
||||
endTime: '$lastPractice'
|
||||
},
|
||||
score: 1
|
||||
}
|
||||
}
|
||||
]);
|
||||
};
|
||||
|
||||
// 扩展模型的静态方法类型
|
||||
interface PracticeRecordModel extends mongoose.Model<IPracticeRecord> {
|
||||
getLeaderboard(
|
||||
type: string,
|
||||
skipCount: number,
|
||||
pageSize: number
|
||||
): mongoose.Aggregate<ILeaderboardRecord[]>;
|
||||
}
|
||||
|
||||
// 创建并导出模型
|
||||
export const PracticeRecord = mongoose.model<IPracticeRecord, PracticeRecordModel>(
|
||||
'PracticeRecord',
|
||||
practiceRecordSchema
|
||||
);
|
||||
13
server/models/PracticeType.ts
Normal file
13
server/models/PracticeType.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import mongoose from 'mongoose';
|
||||
|
||||
const practiceTypeSchema = new mongoose.Schema({
|
||||
title: { type: String, required: true },
|
||||
description: { type: String, required: true },
|
||||
level: {
|
||||
type: String,
|
||||
enum: ['basic', 'intermediate', 'advanced', 'keyword'],
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
export const PracticeType = mongoose.model('PracticeType', practiceTypeSchema);
|
||||
121
server/models/SudokuRecord.ts
Normal file
121
server/models/SudokuRecord.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import mongoose, { Document, Schema } from 'mongoose';
|
||||
|
||||
export type SudokuDifficulty = 'easy' | 'medium' | 'hard';
|
||||
|
||||
export interface ISudokuRecord extends Document {
|
||||
userId: mongoose.Types.ObjectId;
|
||||
username: string;
|
||||
fullname: string;
|
||||
difficulty: SudokuDifficulty;
|
||||
timeSeconds: number;
|
||||
won: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface ISudokuLeaderboardRecord {
|
||||
userId: mongoose.Types.ObjectId;
|
||||
username: string;
|
||||
fullname: string;
|
||||
bestTime: number;
|
||||
totalGames: number;
|
||||
wonGames: number;
|
||||
winRate: number;
|
||||
lastPlayed: Date;
|
||||
}
|
||||
|
||||
const sudokuRecordSchema = new Schema<ISudokuRecord>({
|
||||
userId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true
|
||||
},
|
||||
username: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
fullname: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
difficulty: {
|
||||
type: String,
|
||||
enum: ['easy', 'medium', 'hard'],
|
||||
required: true
|
||||
},
|
||||
timeSeconds: {
|
||||
type: Number,
|
||||
required: true,
|
||||
min: 0
|
||||
},
|
||||
won: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
sudokuRecordSchema.index({ userId: 1, difficulty: 1 });
|
||||
sudokuRecordSchema.index({ difficulty: 1, won: 1, timeSeconds: 1 });
|
||||
|
||||
sudokuRecordSchema.statics.getLeaderboard = function(
|
||||
difficulty: SudokuDifficulty,
|
||||
skipCount: number,
|
||||
pageSize: number
|
||||
): mongoose.Aggregate<ISudokuLeaderboardRecord[]> {
|
||||
return this.aggregate([
|
||||
{
|
||||
$match: {
|
||||
difficulty
|
||||
}
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: '$userId',
|
||||
username: { $first: '$username' },
|
||||
fullname: { $first: '$fullname' },
|
||||
bestTime: {
|
||||
$min: {
|
||||
$cond: [{ $eq: ['$won', true] }, '$timeSeconds', null]
|
||||
}
|
||||
},
|
||||
totalGames: { $sum: 1 },
|
||||
wonGames: { $sum: { $cond: ['$won', 1, 0] } },
|
||||
lastPlayed: { $max: '$createdAt' }
|
||||
}
|
||||
},
|
||||
{
|
||||
$match: {
|
||||
bestTime: { $ne: null }
|
||||
}
|
||||
},
|
||||
{
|
||||
$addFields: {
|
||||
userId: '$_id',
|
||||
winRate: {
|
||||
$multiply: [
|
||||
{ $cond: [{ $eq: ['$totalGames', 0] }, 0, { $divide: ['$wonGames', '$totalGames'] }] },
|
||||
100
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{ $sort: { bestTime: 1 } },
|
||||
{ $skip: skipCount },
|
||||
{ $limit: pageSize }
|
||||
]);
|
||||
};
|
||||
|
||||
interface SudokuRecordModel extends mongoose.Model<ISudokuRecord> {
|
||||
getLeaderboard(
|
||||
difficulty: SudokuDifficulty,
|
||||
skipCount: number,
|
||||
pageSize: number
|
||||
): mongoose.Aggregate<ISudokuLeaderboardRecord[]>;
|
||||
}
|
||||
|
||||
export const SudokuRecord = mongoose.model<ISudokuRecord, SudokuRecordModel>(
|
||||
'SudokuRecord',
|
||||
sudokuRecordSchema
|
||||
);
|
||||
102
server/models/TowerDefenseRecord.ts
Normal file
102
server/models/TowerDefenseRecord.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
// server/models/TowerDefenseRecord.ts
|
||||
|
||||
import mongoose, { Document, Schema } from 'mongoose';
|
||||
|
||||
export interface ITowerDefenseRecord extends Document {
|
||||
userId: mongoose.Types.ObjectId;
|
||||
username: string;
|
||||
fullname: string;
|
||||
wave: number; // 到达的波次
|
||||
score: number; // 最终积分
|
||||
timeSeconds: number; // 游戏持续时间(秒)
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface ITowerDefenseLeaderboardRecord {
|
||||
userId: mongoose.Types.ObjectId;
|
||||
username: string;
|
||||
fullname: string;
|
||||
bestWave: number; // 最高波次
|
||||
bestScore: number; // 最高积分
|
||||
totalGames: number; // 总游戏次数
|
||||
lastPlayed: Date; // 最后游玩时间
|
||||
}
|
||||
|
||||
const towerDefenseRecordSchema = new Schema<ITowerDefenseRecord>({
|
||||
userId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true
|
||||
},
|
||||
username: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
fullname: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
wave: {
|
||||
type: Number,
|
||||
required: true,
|
||||
min: 0
|
||||
},
|
||||
score: {
|
||||
type: Number,
|
||||
required: true,
|
||||
min: 0
|
||||
},
|
||||
timeSeconds: {
|
||||
type: Number,
|
||||
required: true,
|
||||
min: 0
|
||||
}
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
// 索引优化
|
||||
towerDefenseRecordSchema.index({ userId: 1, score: -1 });
|
||||
towerDefenseRecordSchema.index({ score: -1 });
|
||||
towerDefenseRecordSchema.index({ wave: -1 });
|
||||
|
||||
// 获取排行榜的静态方法
|
||||
towerDefenseRecordSchema.statics.getLeaderboard = function(
|
||||
skipCount: number,
|
||||
pageSize: number
|
||||
): mongoose.Aggregate<ITowerDefenseLeaderboardRecord[]> {
|
||||
return this.aggregate([
|
||||
{
|
||||
$group: {
|
||||
_id: '$userId',
|
||||
username: { $first: '$username' },
|
||||
fullname: { $first: '$fullname' },
|
||||
bestWave: { $max: '$wave' },
|
||||
bestScore: { $max: '$score' },
|
||||
totalGames: { $sum: 1 },
|
||||
lastPlayed: { $max: '$createdAt' }
|
||||
}
|
||||
},
|
||||
{ $sort: { bestScore: -1 } }, // 优先按积分排
|
||||
{ $skip: skipCount },
|
||||
{ $limit: pageSize },
|
||||
{
|
||||
$addFields: {
|
||||
userId: '$_id'
|
||||
}
|
||||
}
|
||||
]);
|
||||
};
|
||||
|
||||
interface TowerDefenseRecordModel extends mongoose.Model<ITowerDefenseRecord> {
|
||||
getLeaderboard(
|
||||
skipCount: number,
|
||||
pageSize: number
|
||||
): mongoose.Aggregate<ITowerDefenseLeaderboardRecord[]>;
|
||||
}
|
||||
|
||||
export const TowerDefenseRecord = mongoose.model<ITowerDefenseRecord, TowerDefenseRecordModel>(
|
||||
'TowerDefenseRecord',
|
||||
towerDefenseRecordSchema
|
||||
);
|
||||
36
server/models/TowerDefenseSave.ts
Normal file
36
server/models/TowerDefenseSave.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
// server/models/TowerDefenseSave.ts
|
||||
|
||||
import mongoose, { Document, Schema } from 'mongoose';
|
||||
|
||||
export interface ITowerDefenseSave extends Document {
|
||||
userId: mongoose.Types.ObjectId;
|
||||
name: string; // e.g. timestamp string
|
||||
state: any; // arbitrary game state JSON
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
const towerDefenseSaveSchema = new Schema<ITowerDefenseSave>({
|
||||
userId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
state: {
|
||||
type: Schema.Types.Mixed,
|
||||
required: true
|
||||
}
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
towerDefenseSaveSchema.index({ userId: 1, createdAt: -1 });
|
||||
|
||||
export const TowerDefenseSave = mongoose.model<ITowerDefenseSave>(
|
||||
'TowerDefenseSave',
|
||||
towerDefenseSaveSchema
|
||||
);
|
||||
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);
|
||||
137
server/models/Vocabulary.ts
Normal file
137
server/models/Vocabulary.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import mongoose, { Document, Schema } from 'mongoose';
|
||||
|
||||
// 单词接口
|
||||
export interface IWord extends Document {
|
||||
word: string;
|
||||
translation: string;
|
||||
pronunciation?: string;
|
||||
example?: string;
|
||||
wordSet: mongoose.Types.ObjectId;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
// 单词集接口
|
||||
export interface IWordSet extends Document {
|
||||
name: string;
|
||||
description?: string;
|
||||
owner: mongoose.Types.ObjectId;
|
||||
totalWords: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
// 单词学习记录接口
|
||||
export interface IWordRecord extends Document {
|
||||
user: mongoose.Types.ObjectId;
|
||||
word: mongoose.Types.ObjectId;
|
||||
mode: 'chinese-to-english' | 'audio-to-english' | 'multiple-choice';
|
||||
streak: number;
|
||||
totalCorrect: number;
|
||||
totalWrong: number;
|
||||
mastered: boolean;
|
||||
inWrongBook: boolean;
|
||||
lastTestedAt: Date;
|
||||
lastMasteredAt: Date;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
// 单词测试记录接口
|
||||
export interface IVocabularyTestRecord extends Document {
|
||||
user: mongoose.Types.ObjectId;
|
||||
wordSet: mongoose.Types.ObjectId;
|
||||
testType: 'chinese-to-english' | 'audio-to-english' | 'multiple-choice';
|
||||
stats: {
|
||||
totalWords: number;
|
||||
correctWords: number;
|
||||
accuracy: number;
|
||||
startTime: Date;
|
||||
endTime: Date;
|
||||
duration: number;
|
||||
};
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
// 单词模式
|
||||
const WordSchema = new Schema<IWord>({
|
||||
word: { type: String, required: true, trim: true },
|
||||
translation: { type: String, required: true, trim: true },
|
||||
pronunciation: { type: String, trim: true },
|
||||
example: { type: String, trim: true },
|
||||
wordSet: { type: Schema.Types.ObjectId, ref: 'WordSet', required: true },
|
||||
}, { timestamps: true });
|
||||
|
||||
// 为单词模型添加索引
|
||||
WordSchema.index({ word: 1, wordSet: 1 }, { unique: true });
|
||||
WordSchema.index({ translation: 'text', word: 'text' });
|
||||
|
||||
// 单词集模式
|
||||
const WordSetSchema = new Schema<IWordSet>({
|
||||
name: { type: String, required: true, trim: true },
|
||||
description: { type: String, trim: true },
|
||||
owner: { type: Schema.Types.ObjectId, ref: 'User', required: true },
|
||||
totalWords: { type: Number, default: 0 },
|
||||
}, { timestamps: true });
|
||||
|
||||
// 为单词集模型添加索引
|
||||
WordSetSchema.index({ name: 1, owner: 1 }, { unique: true });
|
||||
|
||||
// 简化的嵌套模式统计
|
||||
const ModeStatsSchema = new Schema({
|
||||
streak: { type: Number, default: 0 },
|
||||
totalCorrect: { type: Number, default: 0 },
|
||||
totalWrong: { type: Number, default: 0 },
|
||||
mastered: { type: Boolean, default: false },
|
||||
inWrongBook: { type: Boolean, default: false },
|
||||
lastTestedAt: Date
|
||||
}, { _id: false });
|
||||
|
||||
// 简化后的主记录模式
|
||||
const WordRecordSchema = new Schema({
|
||||
user: { type: Schema.Types.ObjectId, ref: 'User', required: true },
|
||||
word: { type: Schema.Types.ObjectId, ref: 'Word', required: true },
|
||||
|
||||
// 各模式的基础统计数据
|
||||
multipleChoice: { type: ModeStatsSchema, default: () => ({}) },
|
||||
audioToEnglish: { type: ModeStatsSchema, default: () => ({}) },
|
||||
chineseToEnglish: { type: ModeStatsSchema, default: () => ({}) },
|
||||
|
||||
// 只在顶层保留整体掌握状态
|
||||
isFullyMastered: { type: Boolean, default: false },
|
||||
lastFullyMasteredAt: Date,
|
||||
|
||||
createdAt: { type: Date, default: Date.now }
|
||||
});
|
||||
|
||||
// 单词测试记录模式
|
||||
const VocabularyTestRecordSchema = new Schema<IVocabularyTestRecord>({
|
||||
user: { type: Schema.Types.ObjectId, ref: 'User', required: true },
|
||||
wordSet: { type: Schema.Types.ObjectId, ref: 'WordSet', required: true },
|
||||
testType: {
|
||||
type: String,
|
||||
enum: ['chinese-to-english', 'audio-to-english', 'multiple-choice'],
|
||||
required: true
|
||||
},
|
||||
stats: {
|
||||
totalWords: { type: Number, required: true },
|
||||
correctWords: { type: Number, required: true },
|
||||
accuracy: { type: Number, required: true },
|
||||
startTime: { type: Date, required: true },
|
||||
endTime: { type: Date, required: true },
|
||||
duration: { type: Number, required: true }
|
||||
},
|
||||
createdAt: { type: Date, default: Date.now }
|
||||
});
|
||||
|
||||
// 创建和导出模型
|
||||
export const Word = mongoose.model<IWord>('Word', WordSchema);
|
||||
export const WordSet = mongoose.model<IWordSet>('WordSet', WordSetSchema);
|
||||
export const WordRecord = mongoose.model('WordRecord', WordRecordSchema);
|
||||
export const VocabularyTestRecord = mongoose.model<IVocabularyTestRecord>('VocabularyTestRecord', VocabularyTestRecordSchema);
|
||||
|
||||
export default {
|
||||
Word,
|
||||
WordSet,
|
||||
WordRecord,
|
||||
VocabularyTestRecord
|
||||
};
|
||||
42
server/models/oauth2.ts
Normal file
42
server/models/oauth2.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Schema, model } from 'mongoose';
|
||||
|
||||
// OAuth2客户端应用
|
||||
const OAuth2ClientSchema = new Schema({
|
||||
clientId: { type: String, required: true, unique: true },
|
||||
clientSecret: { type: String, required: true },
|
||||
name: { type: String, required: true },
|
||||
redirectUris: [{ type: String }],
|
||||
grants: [{ type: String }],
|
||||
scope: [{ type: String }],
|
||||
createdAt: { type: Date, default: Date.now },
|
||||
linkedUsers: [{
|
||||
userId: String,
|
||||
username: String,
|
||||
email: String
|
||||
}]
|
||||
});
|
||||
|
||||
// OAuth2授权码
|
||||
const OAuth2AuthorizationCodeSchema = new Schema({
|
||||
code: { type: String, required: true, unique: true },
|
||||
clientId: { type: String, required: true },
|
||||
userId: { type: Schema.Types.ObjectId, ref: 'User', required: true },
|
||||
scope: [{ type: String }],
|
||||
redirectUri: { type: String, required: true },
|
||||
expiresAt: { type: Date, required: true },
|
||||
createdAt: { type: Date, default: Date.now },
|
||||
});
|
||||
|
||||
// OAuth2访问令牌
|
||||
const OAuth2AccessTokenSchema = new Schema({
|
||||
accessToken: { type: String, required: true, unique: true },
|
||||
clientId: { type: String, required: true },
|
||||
userId: { type: Schema.Types.ObjectId, ref: 'User', required: true },
|
||||
scope: [{ type: String }],
|
||||
expiresAt: { type: Date, required: true },
|
||||
createdAt: { type: Date, default: Date.now },
|
||||
});
|
||||
|
||||
export const OAuth2Client = model('OAuth2Client', OAuth2ClientSchema);
|
||||
export const OAuth2AuthorizationCode = model('OAuth2AuthorizationCode', OAuth2AuthorizationCodeSchema);
|
||||
export const OAuth2AccessToken = model('OAuth2AccessToken', OAuth2AccessTokenSchema);
|
||||
Reference in New Issue
Block a user