Migrate project from typing_practiceweb
This commit is contained in:
38
scripts/convert_txt_csv.py
Normal file
38
scripts/convert_txt_csv.py
Normal file
@@ -0,0 +1,38 @@
|
||||
import csv
|
||||
import re
|
||||
|
||||
def process_words_file(input_file, output_file):
|
||||
# 读取文件内容
|
||||
with open(input_file, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# 去除类别信息 (如 A. (103), B. (105), C. (144)...)
|
||||
category_pattern = re.compile(r"^[A-Z]\.\s*\(\d+\)\s*$", re.MULTILINE)
|
||||
content = category_pattern.sub('', content)
|
||||
|
||||
# 使用更精确的模式匹配单词条目
|
||||
# 匹配格式: 序号. 单词 [发音]: 词义
|
||||
word_pattern = re.compile(r"^\d+\.\s+([^[\n]+?)\s*\[([^\]]+)\]:\s*(.+?)(?=\n\d+\.|$)", re.DOTALL | re.MULTILINE)
|
||||
|
||||
matches = word_pattern.findall(content)
|
||||
|
||||
# 创建 CSV 文件
|
||||
with open(output_file, 'w', encoding='utf-8', newline='') as csvfile:
|
||||
csv_writer = csv.writer(csvfile)
|
||||
# 写入表头
|
||||
csv_writer.writerow(['word', 'pronunciation', 'translation'])
|
||||
|
||||
for match in matches:
|
||||
word = match[0].strip()
|
||||
pronunciation = match[1].strip()
|
||||
# 处理多行翻译,替换换行符为空格
|
||||
translation = re.sub(r'\s+', ' ', match[2].strip())
|
||||
csv_writer.writerow([word, pronunciation, translation])
|
||||
|
||||
print(f"转换完成!CSV 文件已保存为: {output_file}")
|
||||
|
||||
# 输入文件路径和输出文件路径
|
||||
input_file = 'words.txt'
|
||||
output_file = 'words.csv'
|
||||
|
||||
process_words_file(input_file, output_file)
|
||||
230
scripts/migrate-complete.ts
Normal file
230
scripts/migrate-complete.ts
Normal file
@@ -0,0 +1,230 @@
|
||||
import mongoose from 'mongoose';
|
||||
import dotenv from 'dotenv';
|
||||
import path from 'path';
|
||||
|
||||
// 加载环境变量
|
||||
dotenv.config({ path: path.resolve(__dirname, '../server/.env') });
|
||||
|
||||
// 定义模型类型接口
|
||||
interface IModeStats {
|
||||
streak?: number;
|
||||
totalCorrect?: number;
|
||||
totalWrong?: number;
|
||||
mastered?: boolean;
|
||||
inWrongBook?: boolean;
|
||||
lastTestedAt?: Date;
|
||||
lastMasteredAt?: Date;
|
||||
}
|
||||
|
||||
interface IWordRecord {
|
||||
_id: mongoose.Types.ObjectId;
|
||||
user: mongoose.Types.ObjectId;
|
||||
word: mongoose.Types.ObjectId;
|
||||
multipleChoice?: IModeStats;
|
||||
audioToEnglish?: IModeStats;
|
||||
chineseToEnglish?: IModeStats;
|
||||
isFullyMastered?: boolean;
|
||||
lastFullyMasteredAt?: Date;
|
||||
createdAt?: Date;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
// 定义模型结构
|
||||
const ModeStatsSchema = new mongoose.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,
|
||||
lastMasteredAt: Date
|
||||
}, { _id: false });
|
||||
|
||||
const WordRecordSchema = new mongoose.Schema({
|
||||
user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
|
||||
word: { type: mongoose.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 }
|
||||
}, { strict: false });
|
||||
|
||||
// 注册模型
|
||||
const WordRecord = mongoose.model<IWordRecord>('WordRecord', WordRecordSchema);
|
||||
|
||||
// 获取数据库URI
|
||||
const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/typeskill';
|
||||
|
||||
console.log('使用数据库连接:', MONGODB_URI);
|
||||
|
||||
// 连接数据库
|
||||
mongoose.connect(MONGODB_URI)
|
||||
.then(() => {
|
||||
console.log('数据库连接成功,开始检查数据...');
|
||||
return migrateWordRecords();
|
||||
})
|
||||
.then(() => {
|
||||
console.log('处理完成!');
|
||||
mongoose.connection.close();
|
||||
process.exit(0);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('处理失败:', err);
|
||||
mongoose.connection.close();
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
async function migrateWordRecords() {
|
||||
try {
|
||||
// 获取所有记录
|
||||
const records = await WordRecord.find({}).lean();
|
||||
console.log(`共找到 ${records.length} 条记录`);
|
||||
|
||||
// 准备批量更新
|
||||
const updateOperations = [];
|
||||
let needUpdateCount = 0;
|
||||
|
||||
// 检查每个记录
|
||||
for (const record of records) {
|
||||
const modes = ['chineseToEnglish', 'audioToEnglish', 'multipleChoice'];
|
||||
|
||||
// 检查是否所有模式都存在并计算掌握状态
|
||||
let allModesExist = true;
|
||||
let allMastered = true;
|
||||
|
||||
for (const mode of modes) {
|
||||
if (!record[mode] || typeof record[mode] !== 'object') {
|
||||
allModesExist = false;
|
||||
allMastered = false;
|
||||
break;
|
||||
}
|
||||
if (record[mode].mastered !== true) {
|
||||
allMastered = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 计算是否完全掌握
|
||||
const shouldBeFullyMastered = allModesExist && allMastered;
|
||||
|
||||
// 计算最新掌握时间和最新测试时间
|
||||
let latestMasteredDate = null;
|
||||
let latestTestedDate = null;
|
||||
if (shouldBeFullyMastered) {
|
||||
for (const mode of modes) {
|
||||
const modeMasteredDate = record[mode]?.lastMasteredAt;
|
||||
if (modeMasteredDate && (!latestMasteredDate || new Date(modeMasteredDate) > new Date(latestMasteredDate))) {
|
||||
latestMasteredDate = modeMasteredDate;
|
||||
}
|
||||
const modeTestedDate = record[mode]?.lastTestedAt;
|
||||
if (modeTestedDate && (!latestTestedDate || new Date(modeTestedDate) > new Date(latestTestedDate))) {
|
||||
latestTestedDate = modeTestedDate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 取最大值
|
||||
let lastFullyMasteredAt = null;
|
||||
if (shouldBeFullyMastered) {
|
||||
if (latestMasteredDate && latestTestedDate) {
|
||||
lastFullyMasteredAt = new Date(latestMasteredDate) > new Date(latestTestedDate)
|
||||
? latestMasteredDate : latestTestedDate;
|
||||
} else if (latestMasteredDate) {
|
||||
lastFullyMasteredAt = latestMasteredDate;
|
||||
} else if (latestTestedDate) {
|
||||
lastFullyMasteredAt = latestTestedDate;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否需要更新
|
||||
let needsUpdate = false;
|
||||
|
||||
// 情况1: isFullyMastered字段与计算结果不一致
|
||||
if (record.isFullyMastered !== shouldBeFullyMastered) {
|
||||
needsUpdate = true;
|
||||
}
|
||||
|
||||
// 情况2: 应该完全掌握,但缺少lastFullyMasteredAt字段或值不正确
|
||||
if (shouldBeFullyMastered &&
|
||||
(!record.lastFullyMasteredAt ||
|
||||
(lastFullyMasteredAt && new Date(record.lastFullyMasteredAt).getTime() !== new Date(lastFullyMasteredAt).getTime()))) {
|
||||
needsUpdate = true;
|
||||
}
|
||||
|
||||
// 如果需要更新,添加到批量操作
|
||||
if (needsUpdate) {
|
||||
needUpdateCount++;
|
||||
|
||||
const updateOperation = {
|
||||
updateOne: {
|
||||
filter: { _id: record._id },
|
||||
update: {
|
||||
$set: {
|
||||
isFullyMastered: shouldBeFullyMastered,
|
||||
...(lastFullyMasteredAt ? { lastFullyMasteredAt: new Date(lastFullyMasteredAt) } : {})
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
updateOperations.push(updateOperation);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`检查完成,发现 ${needUpdateCount} 条记录需要更新`);
|
||||
|
||||
// 执行批量更新
|
||||
if (updateOperations.length > 0) {
|
||||
const result = await WordRecord.bulkWrite(updateOperations);
|
||||
console.log(`成功更新 ${result.modifiedCount} 条记录`);
|
||||
} else {
|
||||
console.log('没有记录需要更新');
|
||||
}
|
||||
|
||||
// 验证更新后的状态
|
||||
const missingFieldCount = await WordRecord.countDocuments({
|
||||
isFullyMastered: { $exists: false }
|
||||
});
|
||||
|
||||
if (missingFieldCount > 0) {
|
||||
console.log(`警告:仍有 ${missingFieldCount} 条记录缺少isFullyMastered字段`);
|
||||
} else {
|
||||
console.log('所有记录现在都包含isFullyMastered字段');
|
||||
}
|
||||
|
||||
// 检查掌握一致性
|
||||
const inconsistentRecords = await WordRecord.find({
|
||||
$or: [
|
||||
// 情况1: 三种模式都掌握了,但isFullyMastered为false
|
||||
{
|
||||
'chineseToEnglish.mastered': true,
|
||||
'audioToEnglish.mastered': true,
|
||||
'multipleChoice.mastered': true,
|
||||
isFullyMastered: false
|
||||
},
|
||||
// 情况2: 至少一种模式未掌握,但isFullyMastered为true
|
||||
{
|
||||
$or: [
|
||||
{ 'chineseToEnglish.mastered': { $ne: true } },
|
||||
{ 'audioToEnglish.mastered': { $ne: true } },
|
||||
{ 'multipleChoice.mastered': { $ne: true } }
|
||||
],
|
||||
isFullyMastered: true
|
||||
}
|
||||
]
|
||||
}).limit(5);
|
||||
|
||||
if (inconsistentRecords.length > 0) {
|
||||
console.log(`警告:发现 ${inconsistentRecords.length} 条记录的掌握状态不一致,显示前5条:`);
|
||||
inconsistentRecords.forEach(r => {
|
||||
console.log(`ID: ${r._id}, Word: ${r.word}, Modes: ${r.chineseToEnglish?.mastered}, ${r.audioToEnglish?.mastered}, ${r.multipleChoice?.mastered}, isFullyMastered: ${r.isFullyMastered}`);
|
||||
});
|
||||
} else {
|
||||
console.log('所有记录的掌握状态一致');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('处理过程中发生错误:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
91
scripts/migrate-word-records.ts
Normal file
91
scripts/migrate-word-records.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import mongoose from 'mongoose';
|
||||
import { config } from '../server/config';
|
||||
// 连接数据库
|
||||
mongoose.connect(config.MONGODB_URI)
|
||||
.then(() => {
|
||||
console.log('数据库连接成功,开始迁移...');
|
||||
migrateWordRecords();
|
||||
}).catch(err => {
|
||||
console.error('数据库连接失败:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// 导入 Vocabulary 模型定义
|
||||
import '../server/models/Vocabulary'; // 确保模型已注册
|
||||
|
||||
async function migrateWordRecords() {
|
||||
try {
|
||||
// 获取WordRecord模型
|
||||
const WordRecord = mongoose.model('WordRecord');
|
||||
|
||||
// 获取所有WordRecord记录
|
||||
console.log('正在获取所有单词记录...');
|
||||
const records = await WordRecord.find({});
|
||||
console.log(`共找到 ${records.length} 条记录需要迁移`);
|
||||
|
||||
// 批量处理,每批次1000条
|
||||
const batchSize = 1000;
|
||||
const totalBatches = Math.ceil(records.length / batchSize);
|
||||
|
||||
for (let batchIndex = 0; batchIndex < totalBatches; batchIndex++) {
|
||||
const start = batchIndex * batchSize;
|
||||
const end = Math.min(start + batchSize, records.length);
|
||||
const batch = records.slice(start, end);
|
||||
|
||||
console.log(`处理批次 ${batchIndex + 1}/${totalBatches}, 记录 ${start + 1} 到 ${end}`);
|
||||
|
||||
const updatePromises = batch.map(async (record) => {
|
||||
try {
|
||||
// 判断是否全部掌握
|
||||
const modes = ['chineseToEnglish', 'audioToEnglish', 'multipleChoice'];
|
||||
const isFullyMastered = modes.every(mode => record[mode]?.mastered === true);
|
||||
|
||||
// 如果全部掌握,找出最近的掌握时间
|
||||
let lastFullyMasteredAt: Date | null = null;
|
||||
if (isFullyMastered) {
|
||||
let latestDate: Date | null = null;
|
||||
modes.forEach(mode => {
|
||||
const modeDate = record[mode]?.lastMasteredAt;
|
||||
if (modeDate && (!latestDate || modeDate > latestDate)) {
|
||||
latestDate = modeDate;
|
||||
}
|
||||
});
|
||||
lastFullyMasteredAt = latestDate;
|
||||
}
|
||||
|
||||
// 修复: 显式定义updateData类型
|
||||
const updateData: {
|
||||
isFullyMastered: boolean;
|
||||
lastFullyMasteredAt?: Date
|
||||
} = {
|
||||
isFullyMastered: isFullyMastered
|
||||
};
|
||||
|
||||
if (lastFullyMasteredAt) {
|
||||
updateData.lastFullyMasteredAt = lastFullyMasteredAt;
|
||||
}
|
||||
|
||||
// 执行更新
|
||||
await WordRecord.updateOne({ _id: record._id }, { $set: updateData });
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`更新记录 ${record._id} 失败:`, error);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// 等待批次完成
|
||||
const results = await Promise.all(updatePromises);
|
||||
const successCount = results.filter(Boolean).length;
|
||||
console.log(`批次 ${batchIndex + 1} 完成: ${successCount}/${batch.length} 条记录成功更新`);
|
||||
}
|
||||
|
||||
console.log('迁移完成!');
|
||||
mongoose.connection.close();
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('迁移过程中发生错误:', error);
|
||||
mongoose.connection.close();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
105
scripts/migratePasswords.ts
Normal file
105
scripts/migratePasswords.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import mongoose, { Document } from 'mongoose';
|
||||
import bcrypt from 'bcrypt';
|
||||
import { config } from '../server/config';
|
||||
|
||||
// 定义用户接口
|
||||
interface IUser extends Document {
|
||||
username: string;
|
||||
password: string;
|
||||
email: string;
|
||||
isAdmin: boolean;
|
||||
created: Date;
|
||||
lastLogin: Date;
|
||||
}
|
||||
|
||||
// 创建 Schema
|
||||
const userSchema = new mongoose.Schema({
|
||||
username: String,
|
||||
password: String,
|
||||
email: String,
|
||||
isAdmin: { type: Boolean, default: false },
|
||||
created: { type: Date, default: Date.now },
|
||||
lastLogin: { type: Date, default: Date.now }
|
||||
});
|
||||
|
||||
// 使用相同的方式创建模型
|
||||
const User = mongoose.model<IUser>('User', userSchema);
|
||||
|
||||
async function migratePasswords() {
|
||||
try {
|
||||
// 连接数据库并等待连接完成
|
||||
await mongoose.connect(config.MONGODB_URI, {
|
||||
serverSelectionTimeoutMS: 5000,
|
||||
connectTimeoutMS: 10000,
|
||||
socketTimeoutMS: 45000,
|
||||
});
|
||||
|
||||
console.log('数据库连接成功: ' + config.MONGODB_URI);
|
||||
|
||||
// 使用 User 模型查询
|
||||
const users = await User.find({}).exec();
|
||||
console.log(`找到 ${users.length} 个用户需要迁移`);
|
||||
|
||||
// 记录成功和失败的数量
|
||||
let successCount = 0;
|
||||
let failCount = 0;
|
||||
|
||||
// 遍历所有用户
|
||||
for (const user of users) {
|
||||
try {
|
||||
console.log('处理用户:', user.username);
|
||||
|
||||
// 检查密码是否已经是哈希形式
|
||||
if (user.password && user.password.length === 60 && user.password.startsWith('$2b$')) {
|
||||
console.log(`用户 ${user.username} 的密码已经是加密格式`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 获取原始密码
|
||||
const originalPassword = user.password;
|
||||
|
||||
if (!originalPassword) {
|
||||
console.log(`用户 ${user.username} 没有密码,跳过`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 生成加密密码
|
||||
const salt = await bcrypt.genSalt(10);
|
||||
const hashedPassword = await bcrypt.hash(originalPassword, salt);
|
||||
|
||||
// 更新用户密码
|
||||
await User.updateOne(
|
||||
{ _id: user._id },
|
||||
{ $set: { password: hashedPassword } }
|
||||
);
|
||||
|
||||
console.log(`用户 ${user.username} 密码迁移成功`);
|
||||
successCount++;
|
||||
} catch (error) {
|
||||
console.error(`用户 ${user.username} 密码迁移失败:`, error);
|
||||
failCount++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n迁移完成统计:');
|
||||
console.log(`总用户数: ${users.length}`);
|
||||
console.log(`成功: ${successCount}`);
|
||||
console.log(`失败: ${failCount}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('迁移过程出错:', error);
|
||||
} finally {
|
||||
// 关闭数据库连接
|
||||
await mongoose.disconnect();
|
||||
console.log('数据库连接已关闭');
|
||||
}
|
||||
}
|
||||
|
||||
// 运行迁移
|
||||
migratePasswords().then(() => {
|
||||
console.log('迁移脚本执行完成');
|
||||
process.exit(0);
|
||||
}).catch((error) => {
|
||||
console.error('迁移脚本执行失败:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
64
scripts/resetPassword.js
Normal file
64
scripts/resetPassword.js
Normal file
@@ -0,0 +1,64 @@
|
||||
const bcrypt = require('bcrypt');
|
||||
const { MongoClient } = require('mongodb');
|
||||
|
||||
// 配置
|
||||
const MONGODB_URI = 'mongodb://localhost:27017/typeskill';
|
||||
const USERNAME = 'bobcoc';
|
||||
const NEW_PASSWORD = 'Admin123'; // 新密码
|
||||
const SALT_ROUNDS = 10;
|
||||
|
||||
async function resetPassword() {
|
||||
let client;
|
||||
|
||||
try {
|
||||
client = new MongoClient(MONGODB_URI);
|
||||
await client.connect();
|
||||
console.log('Connected to MongoDB');
|
||||
|
||||
const db = client.db();
|
||||
const users = db.collection('users');
|
||||
|
||||
// 查找用户
|
||||
const user = await users.findOne({ username: USERNAME });
|
||||
|
||||
if (!user) {
|
||||
console.error(`用户 "${USERNAME}" 不存在`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 生成新的密码哈希
|
||||
const passwordHash = await bcrypt.hash(NEW_PASSWORD, SALT_ROUNDS);
|
||||
|
||||
// 更新用户密码
|
||||
const result = await users.updateOne(
|
||||
{ username: USERNAME },
|
||||
{ $set: { password: passwordHash } }
|
||||
);
|
||||
|
||||
if (result.modifiedCount === 1) {
|
||||
console.log(`已成功重置 "${USERNAME}" 的密码为 "${NEW_PASSWORD}"`);
|
||||
|
||||
// 输出用户信息
|
||||
const updatedUser = await users.findOne({ username: USERNAME });
|
||||
console.log('用户信息:', {
|
||||
_id: updatedUser._id,
|
||||
username: updatedUser.username,
|
||||
email: updatedUser.email,
|
||||
isAdmin: updatedUser.isAdmin,
|
||||
updatedAt: updatedUser.updatedAt
|
||||
});
|
||||
} else {
|
||||
console.log('密码重置失败,请重试');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('重置密码时出错:', error);
|
||||
} finally {
|
||||
if (client) {
|
||||
await client.close();
|
||||
console.log('MongoDB 连接已关闭');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 执行密码重置
|
||||
resetPassword();
|
||||
66
scripts/test_iframe_logs.js
Normal file
66
scripts/test_iframe_logs.js
Normal file
@@ -0,0 +1,66 @@
|
||||
const puppeteer = require('puppeteer');
|
||||
|
||||
(async () => {
|
||||
const url = process.argv[2] || 'http://localhost:5001/tower-defense';
|
||||
console.log('Opening', url);
|
||||
const browser = await puppeteer.launch({headless: true, args: ['--no-sandbox','--disable-setuid-sandbox']});
|
||||
const page = await browser.newPage();
|
||||
|
||||
// Collect console messages from all frames
|
||||
page.on('console', msg => {
|
||||
try {
|
||||
const type = msg.type();
|
||||
const text = msg.text();
|
||||
console.log(`[PAGE][${type}] ${text}`);
|
||||
} catch (e) {
|
||||
console.log('[PAGE][console] (error reading message)');
|
||||
}
|
||||
});
|
||||
|
||||
// Also listen to page errors
|
||||
page.on('pageerror', err => {
|
||||
console.log('[PAGE][error]', err.toString());
|
||||
});
|
||||
|
||||
await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 });
|
||||
|
||||
// Try to focus the iframe and click center
|
||||
try {
|
||||
const frames = page.frames();
|
||||
const tdFrame = frames.find(f => f.url().includes('/tower-defense/td.html')) || frames[0];
|
||||
console.log('Found frames:', frames.map(f => f.url()));
|
||||
if (tdFrame) {
|
||||
// Evaluate in iframe: print some markers and simulate a click on canvas
|
||||
await tdFrame.evaluate(() => {
|
||||
try {
|
||||
console.log('Inside iframe: document.readyState=' + document.readyState);
|
||||
const canvas = document.querySelector('canvas');
|
||||
if (canvas) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
console.log('Inside iframe: canvas rect', rect.width, rect.height);
|
||||
// dispatch mousemove and click events at center
|
||||
const cx = rect.left + rect.width / 2;
|
||||
const cy = rect.top + rect.height / 2;
|
||||
const move = new MouseEvent('mousemove', {clientX: cx, clientY: cy, bubbles: true});
|
||||
const click = new MouseEvent('click', {clientX: cx, clientY: cy, bubbles: true});
|
||||
canvas.dispatchEvent(move);
|
||||
canvas.dispatchEvent(click);
|
||||
console.log('Inside iframe: dispatched synthetic events');
|
||||
} else {
|
||||
console.log('Inside iframe: canvas not found');
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Inside iframe: eval error', e.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Error interacting with iframe:', e.toString());
|
||||
}
|
||||
|
||||
// Wait a bit to collect logs
|
||||
await new Promise(r => setTimeout(r, 4000));
|
||||
|
||||
await browser.close();
|
||||
console.log('Done.');
|
||||
})();
|
||||
134
scripts/test_tower_e2e.js
Normal file
134
scripts/test_tower_e2e.js
Normal file
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* 塔防游戏成绩提交端到端测试
|
||||
* 测试流程:登录 -> 模拟成绩提交 -> 验证数据库存储 -> 检查排行榜
|
||||
*/
|
||||
const fetch = globalThis.fetch || require('node-fetch');
|
||||
|
||||
const BASE_URL = 'http://localhost:5001';
|
||||
const TEST_USERNAME = 'testuser';
|
||||
const TEST_PASSWORD = 'testpass123';
|
||||
|
||||
let token = null;
|
||||
let userId = null;
|
||||
let sessionCookie = null;
|
||||
|
||||
async function request(method, path, body = null, headers = {}) {
|
||||
const url = `${BASE_URL}${path}`;
|
||||
const options = {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...headers,
|
||||
},
|
||||
};
|
||||
|
||||
if (body) {
|
||||
options.body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(url, options);
|
||||
const text = await res.text();
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(text);
|
||||
} catch (e) {
|
||||
data = text;
|
||||
}
|
||||
return { status: res.status, data };
|
||||
} catch (e) {
|
||||
console.error(`Request error (${method} ${path}):`, e.message);
|
||||
return { status: 0, data: null };
|
||||
}
|
||||
}
|
||||
|
||||
async function test() {
|
||||
console.log('🎮 Tower Defense E2E Test Started\n');
|
||||
|
||||
// 1. 尝试注册或登录
|
||||
console.log('1️⃣ Registering/logging in...');
|
||||
const registerRes = await request('POST', '/api/auth/register', {
|
||||
username: TEST_USERNAME,
|
||||
password: TEST_PASSWORD,
|
||||
email: 'testuser@example.com',
|
||||
fullname: 'Test User',
|
||||
});
|
||||
|
||||
if (registerRes.status !== 201 && registerRes.status !== 409) {
|
||||
console.error('❌ Register failed:', registerRes);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 登录
|
||||
const loginRes = await request('POST', '/api/auth/login', {
|
||||
username: TEST_USERNAME,
|
||||
password: TEST_PASSWORD,
|
||||
});
|
||||
|
||||
if (loginRes.status !== 200) {
|
||||
console.error('❌ Login failed:', loginRes);
|
||||
return;
|
||||
}
|
||||
|
||||
token = loginRes.data?.token;
|
||||
userId = loginRes.data?.user?._id;
|
||||
console.log(`✅ Logged in. Token: ${token?.substring(0, 20)}... UserId: ${userId}`);
|
||||
|
||||
// 3. 提交成绩
|
||||
console.log('\n2️⃣ Submitting tower defense score...');
|
||||
const score = Math.floor(Math.random() * 5000) + 1000;
|
||||
const wave = Math.floor(Math.random() * 10) + 1;
|
||||
const timeSeconds = Math.floor(Math.random() * 300) + 60;
|
||||
|
||||
const submitRes = await request('POST', '/api/tower-defense/record',
|
||||
{ wave, score, timeSeconds },
|
||||
{ Authorization: `Bearer ${token}` }
|
||||
);
|
||||
|
||||
if (submitRes.status !== 201) {
|
||||
console.error('❌ Score submission failed:', submitRes);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`✅ Score submitted: wave=${wave}, score=${score}, time=${timeSeconds}s`);
|
||||
console.log(` Response:`, submitRes.data);
|
||||
|
||||
// 4. 获取个人最佳
|
||||
console.log('\n3️⃣ Fetching personal best...');
|
||||
const bestRes = await request('GET', '/api/tower-defense/personal-best', null, {
|
||||
Authorization: `Bearer ${token}`,
|
||||
});
|
||||
|
||||
if (bestRes.status === 200 && bestRes.data?.hasBest) {
|
||||
console.log(`✅ Personal best found: score=${bestRes.data.bestScore}, wave=${bestRes.data.bestWave}`);
|
||||
} else {
|
||||
console.error('⚠️ No personal best found:', bestRes);
|
||||
}
|
||||
|
||||
// 5. 获取排行榜
|
||||
console.log('\n4️⃣ Fetching leaderboard...');
|
||||
const leaderRes = await request('GET', '/api/tower-defense/leaderboard?page=1&limit=10');
|
||||
|
||||
if (leaderRes.status === 200 && leaderRes.data?.records?.length > 0) {
|
||||
console.log(`✅ Leaderboard retrieved (${leaderRes.data.records.length} records)`);
|
||||
console.log(' Top 3:');
|
||||
leaderRes.data.records.slice(0, 3).forEach((rec, i) => {
|
||||
console.log(` ${i + 1}. ${rec.fullname}: score=${rec.bestScore}, wave=${rec.bestWave}, games=${rec.totalGames}`);
|
||||
});
|
||||
|
||||
// 检查我们的用户是否在榜单中
|
||||
const found = leaderRes.data.records.some(r => String(r.userId) === String(userId));
|
||||
if (found) {
|
||||
console.log(`✅ Current user found in leaderboard!`);
|
||||
} else {
|
||||
console.log(`⚠️ Current user NOT found in leaderboard (may be filtered or paginated)`);
|
||||
}
|
||||
} else {
|
||||
console.error('❌ Leaderboard fetch failed:', leaderRes);
|
||||
}
|
||||
|
||||
console.log('\n✅ E2E Test Complete!\n');
|
||||
}
|
||||
|
||||
// 等待服务器启动并运行测试
|
||||
setTimeout(test, 3000);
|
||||
156
scripts/test_tower_e2e.ts
Normal file
156
scripts/test_tower_e2e.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* Tower Defense E2E Test
|
||||
* 测试完整链路:游戏发送 postMessage -> 前端接收 -> API 提交 -> 数据库存储
|
||||
*/
|
||||
|
||||
import axios from 'axios';
|
||||
import { MongoClient } from 'mongodb';
|
||||
|
||||
const API_BASE = 'http://localhost:5001';
|
||||
const MONGODB_URI = 'mongodb://localhost:27017/typeskill';
|
||||
|
||||
// 模拟用户登录信息(可根据实际调整)
|
||||
const TEST_USER = {
|
||||
_id: 'test-user-e2e-' + Date.now(),
|
||||
username: 'testuser_e2e',
|
||||
fullname: 'Test User E2E'
|
||||
};
|
||||
|
||||
const TEST_RECORD = {
|
||||
wave: 5,
|
||||
score: 1250,
|
||||
timeSeconds: 180
|
||||
};
|
||||
|
||||
async function runE2ETest() {
|
||||
console.log('🧪 Tower Defense E2E Test Started');
|
||||
console.log('=====================================\n');
|
||||
|
||||
let client: MongoClient | null = null;
|
||||
|
||||
try {
|
||||
// Step 1: Connect to MongoDB and setup test data
|
||||
console.log('📊 Step 1: Connecting to MongoDB...');
|
||||
client = new MongoClient(MONGODB_URI);
|
||||
await client.connect();
|
||||
const db = client.db();
|
||||
const usersCollection = db.collection('users');
|
||||
const recordsCollection = db.collection('towerdefenserecords');
|
||||
|
||||
// Clean up old test records
|
||||
await recordsCollection.deleteMany({ username: TEST_USER.username });
|
||||
console.log(' ✓ MongoDB connected, test records cleaned\n');
|
||||
|
||||
// Step 2: Insert test user (simulate logged-in user)
|
||||
console.log('👤 Step 2: Creating test user...');
|
||||
const userResult = await usersCollection.updateOne(
|
||||
{ username: TEST_USER.username },
|
||||
{
|
||||
$set: {
|
||||
...TEST_USER,
|
||||
email: `${TEST_USER.username}@test.local`,
|
||||
password: 'hashed-password',
|
||||
isAdmin: false,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
}
|
||||
},
|
||||
{ upsert: true }
|
||||
);
|
||||
const userId = userResult.upsertedId || (await usersCollection.findOne({ username: TEST_USER.username }))?._id;
|
||||
console.log(` ✓ Test user created/found: ${userId}\n`);
|
||||
|
||||
// Step 3: Simulate game completing and calling API with user auth
|
||||
console.log('🎮 Step 3: Simulating game completion & API submission...');
|
||||
console.log(` Submitting: wave=${TEST_RECORD.wave}, score=${TEST_RECORD.score}, timeSeconds=${TEST_RECORD.timeSeconds}`);
|
||||
|
||||
// In a real scenario, the session cookie would be set; here we'll use basic auth or mock
|
||||
const submitResponse = await axios.post(
|
||||
`${API_BASE}/api/tower-defense/record`,
|
||||
TEST_RECORD,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
// In real test, you'd have session/auth headers set
|
||||
},
|
||||
// For this mock test, axios will send with any existing cookies/auth
|
||||
withCredentials: true
|
||||
}
|
||||
);
|
||||
|
||||
if (submitResponse.status === 201) {
|
||||
console.log(' ✓ API submission successful (201)\n');
|
||||
} else {
|
||||
console.log(` ⚠ API response: ${submitResponse.status}\n`);
|
||||
}
|
||||
|
||||
// Step 4: Verify record was saved to database
|
||||
console.log('🔍 Step 4: Verifying record in database...');
|
||||
const savedRecord = await recordsCollection.findOne(
|
||||
{ username: TEST_USER.username, score: TEST_RECORD.score }
|
||||
);
|
||||
|
||||
if (savedRecord) {
|
||||
console.log(' ✓ Record found in database:');
|
||||
console.log(` - username: ${savedRecord.username}`);
|
||||
console.log(` - wave: ${savedRecord.wave}`);
|
||||
console.log(` - score: ${savedRecord.score}`);
|
||||
console.log(` - timeSeconds: ${savedRecord.timeSeconds}`);
|
||||
console.log(` - createdAt: ${savedRecord.createdAt}\n`);
|
||||
} else {
|
||||
console.log(' ✗ Record NOT found in database (check auth/session)\n');
|
||||
throw new Error('Record insertion failed');
|
||||
}
|
||||
|
||||
// Step 5: Test leaderboard endpoint
|
||||
console.log('🏆 Step 5: Testing leaderboard endpoint...');
|
||||
const leaderboardResponse = await axios.get(
|
||||
`${API_BASE}/api/tower-defense/leaderboard?page=1&limit=20`
|
||||
);
|
||||
|
||||
if (leaderboardResponse.status === 200) {
|
||||
const records = leaderboardResponse.data.records || [];
|
||||
console.log(` ✓ Leaderboard retrieved: ${records.length} record(s)`);
|
||||
if (records.length > 0) {
|
||||
console.log(` - Top: ${records[0].fullname} (score: ${records[0].bestScore})\n`);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 6: Test duplicate submission protection
|
||||
console.log('🛡 Step 6: Testing duplicate submission protection...');
|
||||
const dupeResponse = await axios.post(
|
||||
`${API_BASE}/api/tower-defense/record`,
|
||||
TEST_RECORD,
|
||||
{ withCredentials: true }
|
||||
);
|
||||
|
||||
if (dupeResponse.status === 200 && dupeResponse.data.message?.includes('重复')) {
|
||||
console.log(' ✓ Duplicate protection working (returned 200 with ignore message)\n');
|
||||
} else if (dupeResponse.status === 201) {
|
||||
console.log(' ℹ Duplicate was accepted (debounce window may have expired)\n');
|
||||
}
|
||||
|
||||
console.log('✅ E2E Test PASSED\n');
|
||||
console.log('Summary:');
|
||||
console.log(' - postMessage simulation: ✓');
|
||||
console.log(' - API submission: ✓');
|
||||
console.log(' - Database storage: ✓');
|
||||
console.log(' - Leaderboard retrieval: ✓');
|
||||
console.log(' - Duplicate protection: ✓');
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('\n❌ E2E Test FAILED');
|
||||
console.error('Error:', error.message);
|
||||
if (error.response?.data) {
|
||||
console.error('Response data:', error.response.data);
|
||||
}
|
||||
process.exit(1);
|
||||
} finally {
|
||||
if (client) {
|
||||
await client.close();
|
||||
console.log('\nMongoDB connection closed.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
runE2ETest();
|
||||
1544
scripts/words.csv
Normal file
1544
scripts/words.csv
Normal file
File diff suppressed because it is too large
Load Diff
1825
scripts/words.txt
Normal file
1825
scripts/words.txt
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user