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

View 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);

View 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);

View 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
);

View 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
);

View 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);

View 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
);

View 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
);

View 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
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);

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