1
0
mirror of https://github.com/ZeroCatDev/ClassworksKV.git synced 2025-07-04 22:39:22 +00:00

Compare commits

...

9 Commits
v1.0.2 ... main

30 changed files with 943 additions and 3616 deletions

View File

@ -5,8 +5,8 @@
version: 2 version: 2
updates: updates:
- package-ecosystem: "" # See documentation for possible values - package-ecosystem: "npm" # See documentation for possible values
directory: "/" # Location of package manifests directory: "/" # Location of package manifests
schedule: schedule:
interval: "weekly" interval: "weekly"

View File

@ -12,7 +12,9 @@ env:
REGISTRY: ghcr.io REGISTRY: ghcr.io
OWNER: zerocatdev OWNER: zerocatdev
PROJECT_NAME: classworks PROJECT_NAME: classworks
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
jobs: jobs:
build-and-push: build-and-push:
runs-on: ubuntu-latest runs-on: ubuntu-latest

147
.gitignore vendored
View File

@ -1,4 +1,147 @@
node_modules # Created by https://www.toptal.com/developers/gitignore/api/node
# Keep environment variables out of version control # Edit at https://www.toptal.com/developers/gitignore?templates=node
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env .env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
### Node Patch ###
# Serverless Webpack directories
.webpack/
# Optional stylelint cache
# SvelteKit build / generate output
.svelte-kit
# End of https://www.toptal.com/developers/gitignore/api/node
prisma/database/data prisma/database/data
data/

View File

@ -2,6 +2,11 @@
[Classworks](https://cs.houlangs.com)用于班级大屏的作业板小工具 [Classworks](https://cs.houlangs.com)用于班级大屏的作业板小工具
ClassworksKV 是 Classworks 的后端实现这是一个KV存储服务用于存储和查询数据信息如作业、花名册等也可以用于其他用途。
此项目由[厚浪云](https://houlangs.com)提供,访问公开实例零配置使用 [Classworks](https://cs.houlangs.com)
[![通过雨云一键部署](https://rainyun-apps.cn-nb1.rains3.com/materials/deploy-on-rainyun-cn.svg)](https://app.rainyun.com/apps/rca/store/6229/wuyuan_) [![通过雨云一键部署](https://rainyun-apps.cn-nb1.rains3.com/materials/deploy-on-rainyun-cn.svg)](https://app.rainyun.com/apps/rca/store/6229/wuyuan_)
## 文档 ## 文档

2
app.js
View File

@ -1,4 +1,4 @@
import "./instrumentation.js"; import "./utils/instrumentation.js";
// import createError from "http-errors"; // import createError from "http-errors";
import express from "express"; import express from "express";
import { join, dirname } from "path"; import { join, dirname } from "path";

View File

@ -24,7 +24,7 @@ var server = createServer(app);
* Listen on provided port, on all network interfaces. * Listen on provided port, on all network interfaces.
*/ */
server.listen(port); server.listen(port, '0.0.0.0');
server.on('error', onError); server.on('error', onError);
server.on('listening', onListening); server.on('listening', onListening);
@ -82,5 +82,5 @@ function onError(error) {
function onListening() { function onListening() {
var addr = server.address(); var addr = server.address();
console.log("可以在 http://localhost:"+addr.port+" 访问"); console.log(`Server running at http://0.0.0.0:${addr.port}`);
} }

View File

@ -13,6 +13,18 @@ const DATABASE_URL =
? "file:/data/db.sqlite" ? "file:/data/db.sqlite"
: process.env.DATABASE_URL; : process.env.DATABASE_URL;
// 🔄 执行数据库迁移函数
function runDatabaseMigration() {
try {
console.log("🔄 执行数据库迁移...");
execSync("npx prisma migrate deploy", { stdio: "inherit" });
console.log("✅ 数据库迁移完成");
} catch (error) {
console.error("❌ 数据库迁移失败:", error.message);
process.exit(1);
}
}
// 🧱 数据库初始化函数 // 🧱 数据库初始化函数
function setupDatabase() { function setupDatabase() {
try { try {
@ -58,11 +70,8 @@ function setupDatabase() {
} }
console.log(`✅ 已复制 ${DATABASE_TYPE} 数据库配置文件和目录`); console.log(`✅ 已复制 ${DATABASE_TYPE} 数据库配置文件和目录`);
// 设置 Prisma 的 DATABASE_URL // 执行数据库迁移
process.env.DATABASE_URL = DATABASE_URL; runDatabaseMigration();
// 检查数据库表并执行必要的迁移
execSync("npx prisma migrate deploy", { stdio: "inherit" });
} catch (error) { } catch (error) {
console.error("❌ 数据库初始化失败:", error.message); console.error("❌ 数据库初始化失败:", error.message);
process.exit(1); process.exit(1);
@ -72,6 +81,8 @@ function setupDatabase() {
// 🔨 本地构建函数 // 🔨 本地构建函数
function buildLocal() { function buildLocal() {
try { try {
// 确保数据库迁移已执行
runDatabaseMigration();
execSync("npm install", { stdio: "inherit" }); // 安装依赖 execSync("npm install", { stdio: "inherit" }); // 安装依赖
execSync("npx prisma generate", { stdio: "inherit" }); // 生成 Prisma 客户端 execSync("npx prisma generate", { stdio: "inherit" }); // 生成 Prisma 客户端
console.log("✅ 构建完成"); console.log("✅ 构建完成");

Binary file not shown.

View File

@ -1,5 +1,4 @@
import { siteKey } from "../config.js"; import { siteKey } from "../utils/config.js";
import AppError from "../utils/errors.js";
import { PrismaClient } from "@prisma/client"; import { PrismaClient } from "@prisma/client";
import { DecodeAndVerifyPassword, verifySiteKey } from "../utils/crypto.js"; import { DecodeAndVerifyPassword, verifySiteKey } from "../utils/crypto.js";

View File

@ -1,4 +1,4 @@
import { isDevelopment } from "../config.js"; import { isDevelopment } from "../utils/config.js";
const errorHandler = (err, req, res, next) => { const errorHandler = (err, req, res, next) => {
// 判断响应是否已经发送 // 判断响应是否已经发送

View File

@ -1,32 +1,29 @@
{ {
"name": "ClassworksKV", "name": "ClassworksKV",
"version": "1.0.2", "version": "1.0.5",
"private": true, "private": true,
"scripts": { "scripts": {
"start": "node ./bin/www", "start": "node ./bin/www",
"prisma": "prisma generate", "prisma": "prisma generate",
"prisma:pull": "prisma db pull", "prisma:pull": "prisma db pull",
"dev": "NODE_ENV=development nodemon node .bin/www", "dev": "NODE_ENV=development nodemon node .bin/www",
"test:rate-limit": "node ./scripts/test-rate-limit.js", "migrate": "node ./scripts/batchMigrate.js"
"test:stress": "node ./scripts/stress-test.js",
"test:distributed": "node ./scripts/distributed-test.js",
"test:all-limits": "node ./scripts/run-all-tests.js"
}, },
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@opentelemetry/auto-instrumentations-node": "^0.58.1", "@opentelemetry/auto-instrumentations-node": "^0.59.0",
"@opentelemetry/exporter-trace-otlp-proto": "^0.200.0", "@opentelemetry/exporter-trace-otlp-proto": "^0.201.1",
"@opentelemetry/resources": "^2.0.0", "@opentelemetry/resources": "^2.0.1",
"@opentelemetry/sdk-node": "^0.200.0", "@opentelemetry/sdk-node": "^0.201.1",
"@opentelemetry/sdk-trace-base": "^2.0.0", "@opentelemetry/sdk-trace-base": "^2.0.1",
"@opentelemetry/semantic-conventions": "^1.33.0", "@opentelemetry/semantic-conventions": "^1.34.0",
"@prisma/client": "6.8.2", "@prisma/client": "6.8.2",
"axios": "^1.9.0", "axios": "^1.9.0",
"bcrypt": "^6.0.0", "bcrypt": "^6.0.0",
"body-parser": "^2.2.0", "body-parser": "^2.2.0",
"cookie-parser": "~1.4.4", "cookie-parser": "~1.4.4",
"cors": "^2.8.5", "cors": "^2.8.5",
"debug": "~4.4.0", "debug": "~4.4.1",
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"ejs": "^3.1.10", "ejs": "^3.1.10",
"express": "~5.1.0", "express": "~5.1.0",

1478
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

View File

@ -1,6 +1,6 @@
import { Router } from "express"; import { Router } from "express";
const router = Router(); const router = Router();
import kvStore from "../models/kvStore.js"; import kvStore from "../utils/kvStore.js";
import { checkSiteKey } from "../middleware/auth.js"; import { checkSiteKey } from "../middleware/auth.js";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import errors from "../utils/errors.js"; import errors from "../utils/errors.js";

View File

@ -1,75 +0,0 @@
# 接口限速测试脚本
这个目录包含了用于测试API接口限速功能的脚本。
## 前置条件
在运行测试脚本之前,请确保安装了所需的依赖:
```bash
npm install axios
```
## 可用测试脚本
### 1. 功能测试 (test-rate-limit.js)
测试不同类型的限速功能是否正常工作包括全局限速、API限速、写操作限速和删除操作限速。
```bash
npm run test:rate-limit
# 或直接运行
node scripts/test-rate-limit.js
```
### 2. 压力测试 (stress-test.js)
对指定端点进行高并发请求,测试限速在高负载下的表现。
```bash
npm run test:stress
# 或直接运行
node scripts/stress-test.js
```
### 3. 分布式测试 (distributed-test.js)
模拟多个不同IP地址的请求测试基于IP的限速是否有效。
```bash
npm run test:distributed
# 或直接运行
node scripts/distributed-test.js
```
### 4. 运行所有测试 (run-all-tests.js)
按顺序运行所有测试,并在测试之间添加适当的延迟以重置限速计数器。
```bash
npm run test:all-limits
# 或直接运行
node scripts/run-all-tests.js
```
## 配置测试参数
每个测试脚本的开头都有配置参数,可以根据需要进行调整:
- `BASE_URL`: API服务器的基础URL默认为 http://localhost:3000
- `CONCURRENT_REQUESTS`: 并发请求数(仅适用于压力测试)
- `TOTAL_REQUESTS`: 总请求数
- `SIMULATED_IPS`: 模拟的IP数量仅适用于分布式测试
- `REQUESTS_PER_IP`: 每个IP的请求数仅适用于分布式测试
- `TEST_ENDPOINT`: 测试的API端点
## 测试结果说明
测试脚本会输出彩色的测试进度和结果统计信息:
- 绿色点(`.`): 成功的请求
- 黄色L(`L`): 被限速的请求
- 红色E(`E`): 错误的请求
- 红色X(`X`): 请求异常
测试完成后,会显示总体统计信息,包括总请求数、成功请求数、被限速请求数、错误请求数和限速比例。

View File

@ -1,182 +0,0 @@
import axios from 'axios';
import { v4 as uuidv4 } from 'uuid';
// 配置
const BASE_URL = 'http://localhost:3000'; // 修改为你的服务器地址和端口
const TEST_NAMESPACE = uuidv4(); // 生成随机命名空间用于测试
const SIMULATED_IPS = 10; // 模拟的IP数量
const REQUESTS_PER_IP = 30; // 每个IP的请求数
const TEST_ENDPOINT = '/check'; // 测试端点
const DELAY_BETWEEN_BATCHES = 500; // 批次间延迟(毫秒)
// 测试结果统计
const stats = {
totalRequests: 0,
success: 0,
rateLimited: 0,
errors: 0,
ipStats: {} // 每个IP的统计信息
};
// 颜色输出
const colors = {
reset: '\x1b[0m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
cyan: '\x1b[36m'
};
/**
* 延迟函数
* @param {number} ms 延迟毫秒数
*/
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
/**
* 生成随机IP地址
*/
function generateRandomIP() {
const ip = [];
for (let i = 0; i < 4; i++) {
ip.push(Math.floor(Math.random() * 256));
}
return ip.join('.');
}
/**
* 初始化IP统计
*/
function initIPStats() {
const ips = [];
for (let i = 0; i < SIMULATED_IPS; i++) {
const ip = generateRandomIP();
ips.push(ip);
stats.ipStats[ip] = {
success: 0,
rateLimited: 0,
errors: 0,
totalRequests: 0
};
}
return ips;
}
/**
* 发送单个请求
* @param {string} ip 模拟的IP地址
* @param {number} index 请求索引
*/
async function sendRequest(ip, index) {
stats.totalRequests++;
stats.ipStats[ip].totalRequests++;
const url = `${BASE_URL}${TEST_ENDPOINT}`;
try {
const response = await axios({
method: 'GET',
url,
headers: {
'X-Forwarded-For': ip, // 模拟不同的IP地址
'User-Agent': `TestBot/${ip}` // 模拟不同的用户代理
},
validateStatus: () => true // 不抛出HTTP错误
});
// 处理响应
if (response.status === 429) { // 请求被限速
stats.rateLimited++;
stats.ipStats[ip].rateLimited++;
process.stdout.write(`${colors.yellow}L${colors.reset}`);
} else if (response.status >= 200 && response.status < 300) { // 成功
stats.success++;
stats.ipStats[ip].success++;
process.stdout.write(`${colors.green}.${colors.reset}`);
} else { // 其他错误
stats.errors++;
stats.ipStats[ip].errors++;
process.stdout.write(`${colors.red}E${colors.reset}`);
}
return response.status;
} catch (error) {
stats.errors++;
stats.ipStats[ip].errors++;
process.stdout.write(`${colors.red}X${colors.reset}`);
return 0;
}
}
/**
* 打印统计信息
* @param {Array<string>} ips IP地址列表
*/
function printStats(ips) {
console.log(`\n\n${colors.magenta}分布式测试结果:${colors.reset}`);
console.log(`总请求数: ${stats.totalRequests}`);
console.log(`成功请求: ${stats.success}`);
console.log(`被限速请求: ${stats.rateLimited}`);
console.log(`错误请求: ${stats.errors}`);
console.log(`总限速比例: ${Math.round((stats.rateLimited / stats.totalRequests) * 100)}%`);
console.log(`\n${colors.magenta}各IP测试结果:${colors.reset}`);
ips.forEach((ip, index) => {
const ipStat = stats.ipStats[ip];
const limitedPercent = Math.round((ipStat.rateLimited / ipStat.totalRequests) * 100);
console.log(`IP-${index+1} (${ip}): 总请求=${ipStat.totalRequests}, 成功=${ipStat.success}, 限速=${ipStat.rateLimited} (${limitedPercent}%), 错误=${ipStat.errors}`);
});
}
/**
* 为单个IP发送多个请求
* @param {string} ip IP地址
*/
async function sendRequestsForIP(ip) {
const promises = [];
for (let i = 0; i < REQUESTS_PER_IP; i++) {
promises.push(sendRequest(ip, i));
}
await Promise.all(promises);
}
/**
* 主函数
*/
async function main() {
console.log(`${colors.cyan}开始分布式测试限速功能...${colors.reset}`);
console.log(`目标端点: ${TEST_ENDPOINT}`);
console.log(`模拟IP数量: ${SIMULATED_IPS}`);
console.log(`每个IP请求数: ${REQUESTS_PER_IP}`);
console.log(`总请求数: ${SIMULATED_IPS * REQUESTS_PER_IP}`);
try {
// 初始化IP统计
const ips = initIPStats();
console.log(`\n${colors.cyan}测试进度:${colors.reset}`);
// 为每个IP发送请求
for (let i = 0; i < ips.length; i++) {
const ip = ips[i];
console.log(`\n${colors.blue}测试IP-${i+1} (${ip}):${colors.reset} `);
await sendRequestsForIP(ip);
// 在IP批次之间添加延迟
if (i < ips.length - 1) {
await delay(DELAY_BETWEEN_BATCHES);
}
}
printStats(ips);
console.log(`\n${colors.green}分布式测试完成!${colors.reset}`);
} catch (error) {
console.error(`\n${colors.red}测试过程中发生错误: ${error.message}${colors.reset}`);
}
}
// 执行主函数
main().catch(console.error);

View File

@ -1,83 +0,0 @@
import { spawn } from 'child_process';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
// 获取当前文件的目录路径
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// 颜色输出
const colors = {
reset: '\x1b[0m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
cyan: '\x1b[36m'
};
/**
* 运行脚本
* @param {string} scriptName 脚本文件名
* @param {string} description 测试描述
*/
function runScript(scriptName, description) {
return new Promise((resolve, reject) => {
console.log(`\n${colors.cyan}=======================================${colors.reset}`);
console.log(`${colors.cyan}运行: ${description}${colors.reset}`);
console.log(`${colors.cyan}=======================================${colors.reset}\n`);
const scriptPath = join(__dirname, scriptName);
const child = spawn('node', [scriptPath], { stdio: 'inherit' });
child.on('close', (code) => {
if (code === 0) {
console.log(`\n${colors.green}${description}完成,退出码: ${code}${colors.reset}`);
resolve();
} else {
console.error(`\n${colors.red}${description}失败,退出码: ${code}${colors.reset}`);
reject(new Error(`脚本退出码: ${code}`));
}
});
child.on('error', (error) => {
console.error(`\n${colors.red}启动脚本时出错: ${error.message}${colors.reset}`);
reject(error);
});
});
}
/**
* 主函数
*/
async function main() {
console.log(`${colors.magenta}开始运行所有限速测试...${colors.reset}`);
try {
// 运行功能测试
await runScript('test-rate-limit.js', '功能测试');
// 等待一段时间以确保限速计数器重置
console.log(`\n${colors.yellow}等待30秒以确保限速计数器重置...${colors.reset}`);
await new Promise(resolve => setTimeout(resolve, 30000));
// 运行压力测试
await runScript('stress-test.js', '压力测试');
// 等待一段时间以确保限速计数器重置
console.log(`\n${colors.yellow}等待30秒以确保限速计数器重置...${colors.reset}`);
await new Promise(resolve => setTimeout(resolve, 30000));
// 运行分布式测试
await runScript('distributed-test.js', '分布式测试');
console.log(`\n${colors.green}所有测试已完成!${colors.reset}`);
} catch (error) {
console.error(`\n${colors.red}测试过程中发生错误: ${error.message}${colors.reset}`);
process.exit(1);
}
}
// 执行主函数
main().catch(console.error);

View File

@ -1,135 +0,0 @@
import axios from 'axios';
import { v4 as uuidv4 } from 'uuid';
// 配置
const BASE_URL = 'http://localhost:3000'; // 修改为你的服务器地址和端口
const TEST_NAMESPACE = uuidv4(); // 生成随机命名空间用于测试
const CONCURRENT_REQUESTS = 50; // 并发请求数
const TOTAL_REQUESTS = 500; // 总请求数
const TEST_ENDPOINT = '/check'; // 测试端点
// 测试结果统计
const stats = {
success: 0,
rateLimited: 0,
errors: 0,
totalRequests: 0,
startTime: 0,
endTime: 0
};
// 颜色输出
const colors = {
reset: '\x1b[0m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
cyan: '\x1b[36m'
};
/**
* 发送单个请求
* @param {number} index 请求索引
*/
async function sendRequest(index) {
stats.totalRequests++;
const url = `${BASE_URL}${TEST_ENDPOINT}`;
try {
const response = await axios({
method: 'GET',
url,
validateStatus: () => true // 不抛出HTTP错误
});
// 处理响应
if (response.status === 429) { // 请求被限速
stats.rateLimited++;
process.stdout.write(`${colors.yellow}L${colors.reset}`);
} else if (response.status >= 200 && response.status < 300) { // 成功
stats.success++;
process.stdout.write(`${colors.green}.${colors.reset}`);
} else { // 其他错误
stats.errors++;
process.stdout.write(`${colors.red}E${colors.reset}`);
}
// 每50个请求换行
if (index % 50 === 0 && index > 0) {
process.stdout.write('\n');
}
return response.status;
} catch (error) {
stats.errors++;
process.stdout.write(`${colors.red}X${colors.reset}`);
return 0;
}
}
/**
* 打印统计信息
*/
function printStats() {
const duration = (stats.endTime - stats.startTime) / 1000;
const rps = Math.round(stats.totalRequests / duration);
console.log(`\n\n${colors.magenta}压力测试结果:${colors.reset}`);
console.log(`总请求数: ${stats.totalRequests}`);
console.log(`成功请求: ${stats.success}`);
console.log(`被限速请求: ${stats.rateLimited}`);
console.log(`错误请求: ${stats.errors}`);
console.log(`限速比例: ${Math.round((stats.rateLimited / stats.totalRequests) * 100)}%`);
console.log(`测试持续时间: ${duration.toFixed(2)}`);
console.log(`平均请求速率: ${rps} 请求/秒`);
}
/**
* 批量发送请求
* @param {number} batchSize 批次大小
* @param {number} startIndex 起始索引
*/
async function sendBatch(batchSize, startIndex) {
const promises = [];
for (let i = 0; i < batchSize; i++) {
const index = startIndex + i;
if (index < TOTAL_REQUESTS) {
promises.push(sendRequest(index));
}
}
await Promise.all(promises);
}
/**
* 主函数
*/
async function main() {
console.log(`${colors.cyan}开始压力测试限速功能...${colors.reset}`);
console.log(`目标端点: ${TEST_ENDPOINT}`);
console.log(`并发请求数: ${CONCURRENT_REQUESTS}`);
console.log(`总请求数: ${TOTAL_REQUESTS}`);
console.log(`\n${colors.cyan}测试进度:${colors.reset}`);
try {
stats.startTime = Date.now();
// 分批发送请求
const batches = Math.ceil(TOTAL_REQUESTS / CONCURRENT_REQUESTS);
for (let i = 0; i < batches; i++) {
const startIndex = i * CONCURRENT_REQUESTS;
await sendBatch(CONCURRENT_REQUESTS, startIndex);
}
stats.endTime = Date.now();
printStats();
console.log(`\n${colors.green}压力测试完成!${colors.reset}`);
} catch (error) {
console.error(`\n${colors.red}测试过程中发生错误: ${error.message}${colors.reset}`);
}
}
// 执行主函数
main().catch(console.error);

View File

@ -1,250 +0,0 @@
import axios from 'axios';
import { v4 as uuidv4 } from 'uuid';
// 配置
const BASE_URL = 'http://localhost:3030'; // 修改为你的服务器地址和端口
const TEST_NAMESPACE = uuidv4(); // 生成随机命名空间用于测试
const DELAY_BETWEEN_REQUESTS = 100; // 请求间隔时间(毫秒)
// 测试结果统计
const stats = {
success: 0,
rateLimited: 0,
errors: 0,
totalRequests: 0
};
// 颜色输出
const colors = {
reset: '\x1b[0m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
cyan: '\x1b[36m'
};
/**
* 延迟函数
* @param {number} ms 延迟毫秒数
*/
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
/**
* 发送请求并处理响应
* @param {string} method HTTP方法
* @param {string} endpoint 请求路径
* @param {object} data 请求数据
* @param {number} index 请求索引
*/
async function sendRequest(method, endpoint, data = null, index) {
stats.totalRequests++;
const url = `${BASE_URL}${endpoint}`;
try {
const startTime = Date.now();
const response = await axios({
method,
url,
data,
validateStatus: () => true // 不抛出HTTP错误
});
const duration = Date.now() - startTime;
// 处理响应
if (response.status === 429) { // 请求被限速
stats.rateLimited++;
console.log(`${colors.yellow}[${index}] ${method} ${endpoint} - 被限速 (${duration}ms)${colors.reset}`);
return { limited: true, status: response.status };
} else if (response.status >= 200 && response.status < 300) { // 成功
stats.success++;
console.log(`${colors.green}[${index}] ${method} ${endpoint} - 成功: ${response.status} (${duration}ms)${colors.reset}`);
return { limited: false, status: response.status, data: response.data };
} else { // 其他错误
stats.errors++;
console.log(`${colors.red}[${index}] ${method} ${endpoint} - 错误: ${response.status} (${duration}ms)${colors.reset}`);
return { limited: false, status: response.status, error: response.data };
}
} catch (error) {
stats.errors++;
console.log(`${colors.red}[${index}] ${method} ${endpoint} - 异常: ${error.message}${colors.reset}`);
return { limited: false, error: error.message };
}
}
/**
* 测试全局限速
*/
async function testGlobalRateLimit() {
console.log(`\n${colors.cyan}===== 测试全局限速 (200/15分钟) =====${colors.reset}`);
const requests = [];
// 发送250个请求 (应该有50个被限速)
for (let i = 0; i < 250; i++) {
requests.push(sendRequest('GET', '/check', null, i));
await delay(DELAY_BETWEEN_REQUESTS);
}
await Promise.all(requests);
printStats('全局限速测试');
}
/**
* 测试API限速
*/
async function testApiRateLimit() {
console.log(`\n${colors.cyan}===== 测试API限速 (50/5分钟) =====${colors.reset}`);
resetStats();
const requests = [];
// 发送60个请求 (应该有10个被限速)
for (let i = 0; i < 60; i++) {
requests.push(sendRequest('GET', '/check', null, i));
await delay(DELAY_BETWEEN_REQUESTS);
}
await Promise.all(requests);
printStats('API限速测试');
}
/**
* 测试写操作限速
*/
async function testWriteRateLimit() {
console.log(`\n${colors.cyan}===== 测试写操作限速 (10/分钟) =====${colors.reset}`);
resetStats();
const requests = [];
// 发送15个写请求 (应该有5个被限速)
for (let i = 0; i < 15; i++) {
const key = `test-key-${i}`;
const data = { value: `test-value-${i}`, timestamp: Date.now() };
requests.push(sendRequest('POST', `/${TEST_NAMESPACE}/${key}`, data, i));
await delay(DELAY_BETWEEN_REQUESTS);
}
await Promise.all(requests);
printStats('写操作限速测试');
}
/**
* 测试删除操作限速
*/
async function testDeleteRateLimit() {
console.log(`\n${colors.cyan}===== 测试删除操作限速 (5/5分钟) =====${colors.reset}`);
resetStats();
// 先创建几个键值对
for (let i = 0; i < 10; i++) {
const key = `delete-key-${i}`;
const data = { value: `delete-value-${i}` };
await sendRequest('POST', `/${TEST_NAMESPACE}/${key}`, data, `创建-${i}`);
await delay(DELAY_BETWEEN_REQUESTS);
}
resetStats();
const requests = [];
// 发送8个删除请求 (应该有3个被限速)
for (let i = 0; i < 8; i++) {
const key = `delete-key-${i}`;
requests.push(sendRequest('DELETE', `/${TEST_NAMESPACE}/${key}`, null, i));
await delay(DELAY_BETWEEN_REQUESTS);
}
await Promise.all(requests);
printStats('删除操作限速测试');
}
/**
* 测试认证限速
*/
async function testAuthRateLimit() {
console.log(`\n${colors.cyan}===== 测试认证限速 (5/30分钟) =====${colors.reset}`);
resetStats();
const requests = [];
// 发送8个认证请求 (应该有3个被限速)
for (let i = 0; i < 8; i++) {
requests.push(sendRequest('POST', '/auth', { username: 'test', password: 'wrong' }, i));
await delay(DELAY_BETWEEN_REQUESTS);
}
await Promise.all(requests);
printStats('认证限速测试');
}
/**
* 重置统计数据
*/
function resetStats() {
stats.success = 0;
stats.rateLimited = 0;
stats.errors = 0;
stats.totalRequests = 0;
}
/**
* 打印统计信息
*/
function printStats(testName) {
console.log(`\n${colors.magenta}${testName}结果:${colors.reset}`);
console.log(`总请求数: ${stats.totalRequests}`);
console.log(`成功请求: ${stats.success}`);
console.log(`被限速请求: ${stats.rateLimited}`);
console.log(`错误请求: ${stats.errors}`);
console.log(`限速比例: ${Math.round((stats.rateLimited / stats.totalRequests) * 100)}%`);
}
/**
* 主函数
*/
async function main() {
console.log(`${colors.cyan}开始测试限速功能...${colors.reset}`);
console.log(`使用测试命名空间: ${TEST_NAMESPACE}`);
try {
// 测试全局限速
await testGlobalRateLimit();
// 重置统计并等待一段时间
resetStats();
await delay(2000);
// 测试API限速
await testApiRateLimit();
// 重置统计并等待一段时间
resetStats();
await delay(2000);
// 测试写操作限速
await testWriteRateLimit();
// 重置统计并等待一段时间
resetStats();
await delay(2000);
// 测试删除操作限速
await testDeleteRateLimit();
// 重置统计并等待一段时间
resetStats();
await delay(2000);
// 测试认证限速
await testAuthRateLimit();
// 清理测试数据
console.log(`\n${colors.cyan}清理测试数据...${colors.reset}`);
await sendRequest('DELETE', `/${TEST_NAMESPACE}`, null, 'cleanup');
console.log(`\n${colors.green}所有测试完成!${colors.reset}`);
} catch (error) {
console.error(`${colors.red}测试过程中发生错误: ${error.message}${colors.reset}`);
}
}
// 执行主函数
main().catch(console.error);

View File

@ -1,4 +1,4 @@
import kvStore from "../models/kvStore.js"; import kvStore from "./kvStore.js";
// 存储 readme 值的内存变量 // 存储 readme 值的内存变量
let readmeValue = null; let readmeValue = null;

2139
yarn.lock

File diff suppressed because it is too large Load Diff