mirror of
https://github.com/ZeroCatDev/ClassworksKV.git
synced 2025-07-04 22:39:22 +00:00
Compare commits
9 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
f1dba22f75 | ||
![]() |
6cfa20fe76 | ||
![]() |
2b077553f7 | ||
![]() |
0b4fe90c94 | ||
![]() |
05ece2514b | ||
![]() |
d6ab4895d5 | ||
![]() |
ea21f0b8f3 | ||
![]() |
35d05df925 | ||
![]() |
0a8cd084d6 |
2
.github/dependabot.yml
vendored
2
.github/dependabot.yml
vendored
@ -5,7 +5,7 @@
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "" # See documentation for possible values
|
||||
- package-ecosystem: "npm" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
4
.github/workflows/docker-publish.yml
vendored
4
.github/workflows/docker-publish.yml
vendored
@ -12,7 +12,9 @@ env:
|
||||
REGISTRY: ghcr.io
|
||||
OWNER: zerocatdev
|
||||
PROJECT_NAME: classworks
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
|
147
.gitignore
vendored
147
.gitignore
vendored
@ -1,4 +1,147 @@
|
||||
node_modules
|
||||
# Keep environment variables out of version control
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/node
|
||||
# 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.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
|
||||
data/
|
||||
|
@ -2,6 +2,11 @@
|
||||
[Classworks](https://cs.houlangs.com)用于班级大屏的作业板小工具
|
||||
|
||||
|
||||
ClassworksKV 是 Classworks 的后端实现,这是一个KV存储服务,用于存储和查询数据信息,如作业、花名册等,也可以用于其他用途。
|
||||
|
||||
|
||||
此项目由[厚浪云](https://houlangs.com)提供,访问公开实例零配置使用 [Classworks](https://cs.houlangs.com)
|
||||
|
||||
[](https://app.rainyun.com/apps/rca/store/6229/wuyuan_)
|
||||
|
||||
## 文档
|
||||
|
2
app.js
2
app.js
@ -1,4 +1,4 @@
|
||||
import "./instrumentation.js";
|
||||
import "./utils/instrumentation.js";
|
||||
// import createError from "http-errors";
|
||||
import express from "express";
|
||||
import { join, dirname } from "path";
|
||||
|
4
bin/www
4
bin/www
@ -24,7 +24,7 @@ var server = createServer(app);
|
||||
* Listen on provided port, on all network interfaces.
|
||||
*/
|
||||
|
||||
server.listen(port);
|
||||
server.listen(port, '0.0.0.0');
|
||||
server.on('error', onError);
|
||||
server.on('listening', onListening);
|
||||
|
||||
@ -82,5 +82,5 @@ function onError(error) {
|
||||
|
||||
function onListening() {
|
||||
var addr = server.address();
|
||||
console.log("可以在 http://localhost:"+addr.port+" 访问");
|
||||
console.log(`Server running at http://0.0.0.0:${addr.port}`);
|
||||
}
|
||||
|
@ -13,6 +13,18 @@ const DATABASE_URL =
|
||||
? "file:/data/db.sqlite"
|
||||
: 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() {
|
||||
try {
|
||||
@ -58,11 +70,8 @@ function setupDatabase() {
|
||||
}
|
||||
console.log(`✅ 已复制 ${DATABASE_TYPE} 数据库配置文件和目录`);
|
||||
|
||||
// 设置 Prisma 的 DATABASE_URL
|
||||
process.env.DATABASE_URL = DATABASE_URL;
|
||||
|
||||
// 检查数据库表并执行必要的迁移
|
||||
execSync("npx prisma migrate deploy", { stdio: "inherit" });
|
||||
// 执行数据库迁移
|
||||
runDatabaseMigration();
|
||||
} catch (error) {
|
||||
console.error("❌ 数据库初始化失败:", error.message);
|
||||
process.exit(1);
|
||||
@ -72,6 +81,8 @@ function setupDatabase() {
|
||||
// 🔨 本地构建函数
|
||||
function buildLocal() {
|
||||
try {
|
||||
// 确保数据库迁移已执行
|
||||
runDatabaseMigration();
|
||||
execSync("npm install", { stdio: "inherit" }); // 安装依赖
|
||||
execSync("npx prisma generate", { stdio: "inherit" }); // 生成 Prisma 客户端
|
||||
console.log("✅ 构建完成");
|
||||
|
BIN
data/db.db
BIN
data/db.db
Binary file not shown.
@ -1,5 +1,4 @@
|
||||
import { siteKey } from "../config.js";
|
||||
import AppError from "../utils/errors.js";
|
||||
import { siteKey } from "../utils/config.js";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { DecodeAndVerifyPassword, verifySiteKey } from "../utils/crypto.js";
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { isDevelopment } from "../config.js";
|
||||
import { isDevelopment } from "../utils/config.js";
|
||||
|
||||
const errorHandler = (err, req, res, next) => {
|
||||
// 判断响应是否已经发送
|
||||
|
21
package.json
21
package.json
@ -1,32 +1,29 @@
|
||||
{
|
||||
"name": "ClassworksKV",
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.5",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "node ./bin/www",
|
||||
"prisma": "prisma generate",
|
||||
"prisma:pull": "prisma db pull",
|
||||
"dev": "NODE_ENV=development nodemon node .bin/www",
|
||||
"test:rate-limit": "node ./scripts/test-rate-limit.js",
|
||||
"test:stress": "node ./scripts/stress-test.js",
|
||||
"test:distributed": "node ./scripts/distributed-test.js",
|
||||
"test:all-limits": "node ./scripts/run-all-tests.js"
|
||||
"migrate": "node ./scripts/batchMigrate.js"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@opentelemetry/auto-instrumentations-node": "^0.58.1",
|
||||
"@opentelemetry/exporter-trace-otlp-proto": "^0.200.0",
|
||||
"@opentelemetry/resources": "^2.0.0",
|
||||
"@opentelemetry/sdk-node": "^0.200.0",
|
||||
"@opentelemetry/sdk-trace-base": "^2.0.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.33.0",
|
||||
"@opentelemetry/auto-instrumentations-node": "^0.59.0",
|
||||
"@opentelemetry/exporter-trace-otlp-proto": "^0.201.1",
|
||||
"@opentelemetry/resources": "^2.0.1",
|
||||
"@opentelemetry/sdk-node": "^0.201.1",
|
||||
"@opentelemetry/sdk-trace-base": "^2.0.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.34.0",
|
||||
"@prisma/client": "6.8.2",
|
||||
"axios": "^1.9.0",
|
||||
"bcrypt": "^6.0.0",
|
||||
"body-parser": "^2.2.0",
|
||||
"cookie-parser": "~1.4.4",
|
||||
"cors": "^2.8.5",
|
||||
"debug": "~4.4.0",
|
||||
"debug": "~4.4.1",
|
||||
"dotenv": "^16.5.0",
|
||||
"ejs": "^3.1.10",
|
||||
"express": "~5.1.0",
|
||||
|
1478
pnpm-lock.yaml
generated
1478
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
@ -1,6 +1,6 @@
|
||||
import { Router } from "express";
|
||||
const router = Router();
|
||||
import kvStore from "../models/kvStore.js";
|
||||
import kvStore from "../utils/kvStore.js";
|
||||
import { checkSiteKey } from "../middleware/auth.js";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import errors from "../utils/errors.js";
|
||||
|
@ -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`): 请求异常
|
||||
|
||||
测试完成后,会显示总体统计信息,包括总请求数、成功请求数、被限速请求数、错误请求数和限速比例。
|
@ -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);
|
@ -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);
|
@ -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);
|
@ -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);
|
@ -1,4 +1,4 @@
|
||||
import kvStore from "../models/kvStore.js";
|
||||
import kvStore from "./kvStore.js";
|
||||
|
||||
// 存储 readme 值的内存变量
|
||||
let readmeValue = null;
|
||||
|
Loading…
x
Reference in New Issue
Block a user