From eb961d83f16af5265170bd4d6f01b7dff618732f Mon Sep 17 00:00:00 2001 From: zzlgreat Date: Fri, 12 Dec 2025 00:02:55 +0800 Subject: [PATCH] update pay ui --- src/components/Navbars/HomeNavbar.js | 11 +- .../components/UserMenu/DesktopUserMenu.js | 62 +++--- stress_test/README.md | 162 ++++++++++++++++ stress_test/locustfile.py | 150 +++++++++++++++ stress_test/quick_test.sh | 46 +++++ stress_test/websocket_test.py | 177 ++++++++++++++++++ 6 files changed, 562 insertions(+), 46 deletions(-) create mode 100644 stress_test/README.md create mode 100644 stress_test/locustfile.py create mode 100644 stress_test/quick_test.sh create mode 100644 stress_test/websocket_test.py diff --git a/src/components/Navbars/HomeNavbar.js b/src/components/Navbars/HomeNavbar.js index ab43deaf..e864a34c 100644 --- a/src/components/Navbars/HomeNavbar.js +++ b/src/components/Navbars/HomeNavbar.js @@ -37,9 +37,6 @@ import BrandLogo from './components/BrandLogo'; import LoginButton from './components/LoginButton'; import CalendarButton from './components/CalendarButton'; -// Phase 2 优化: 使用 Redux 管理订阅数据 -import { useSubscription } from '../../hooks/useSubscription'; - // Phase 3 优化: 提取的用户菜单组件 import { DesktopUserMenu, TabletUserMenu } from './components/UserMenu'; @@ -125,13 +122,7 @@ export default function HomeNavbar() { } }; - // Phase 2: 使用 Redux 订阅数据 - const { - subscriptionInfo, - isSubscriptionModalOpen, - openSubscriptionModal, - closeSubscriptionModal - } = useSubscription(); + // Phase 2: 订阅数据管理已移至 UserMenu 子组件内部 // Phase 6: loadWatchlistQuotes, loadFollowingEvents, handleRemoveFromWatchlist, // handleUnfollowEvent 已移至自定义 Hooks 中,由各自组件内部管理 diff --git a/src/components/Navbars/components/UserMenu/DesktopUserMenu.js b/src/components/Navbars/components/UserMenu/DesktopUserMenu.js index 7c9c1aee..a7520992 100644 --- a/src/components/Navbars/components/UserMenu/DesktopUserMenu.js +++ b/src/components/Navbars/components/UserMenu/DesktopUserMenu.js @@ -1,61 +1,51 @@ // src/components/Navbars/components/UserMenu/DesktopUserMenu.js -// 桌面版用户菜单 - 头像 + Tooltip + 订阅弹窗 +// 桌面版用户菜单 - 头像点击跳转到订阅页面 import React, { memo } from 'react'; import { Tooltip, useColorModeValue } from '@chakra-ui/react'; +import { useNavigate } from 'react-router-dom'; import UserAvatar from './UserAvatar'; -import SubscriptionModal from '../../../Subscription/SubscriptionModal'; import { TooltipContent } from '../../../Subscription/CrownTooltip'; import { useSubscription } from '../../../../hooks/useSubscription'; /** * 桌面版用户菜单组件 - * 大屏幕 (md+) 显示,头像点击打开订阅弹窗 + * 大屏幕 (md+) 显示,头像点击跳转到订阅页面 * * @param {Object} props * @param {Object} props.user - 用户信息 */ const DesktopUserMenu = memo(({ user }) => { - const { - subscriptionInfo, - isSubscriptionModalOpen, - openSubscriptionModal, - closeSubscriptionModal - } = useSubscription(); + const navigate = useNavigate(); + const { subscriptionInfo } = useSubscription(); const tooltipBg = useColorModeValue('white', 'gray.800'); const tooltipBorderColor = useColorModeValue('gray.200', 'gray.600'); - return ( - <> - } - placement="bottom" - hasArrow - bg={tooltipBg} - borderRadius="lg" - border="1px solid" - borderColor={tooltipBorderColor} - boxShadow="lg" - p={3} - > - - - - + const handleAvatarClick = () => { + navigate('/home/pages/account/subscription'); + }; - {isSubscriptionModalOpen && ( - } + placement="bottom" + hasArrow + bg={tooltipBg} + borderRadius="lg" + border="1px solid" + borderColor={tooltipBorderColor} + boxShadow="lg" + p={3} + > + + - )} - + + ); }); diff --git a/stress_test/README.md b/stress_test/README.md new file mode 100644 index 00000000..c97b7bb9 --- /dev/null +++ b/stress_test/README.md @@ -0,0 +1,162 @@ +# 压力测试指南 + +## 测试工具选择 + +| 工具 | 特点 | 适用场景 | +|------|------|---------| +| **Locust** | Python 编写,Web UI,可模拟复杂用户行为 | 综合压测(推荐) | +| **wrk** | C 语言,性能极高,轻量级 | 极限性能测试 | +| **ab** | Apache 自带,简单易用 | 快速测试 | +| **k6** | 现代化,JavaScript 脚本 | CI/CD 集成 | + +--- + +## 方案一:Locust(推荐) + +### 安装 +```bash +pip install locust +``` + +### 启动 Web UI +```bash +cd stress_test +locust -f locustfile.py --host=https://valuefrontier.cn +``` +然后访问 http://localhost:8089 + +### 命令行模式(无 UI) +```bash +# 1000 用户,每秒增加 50 用户,运行 5 分钟 +locust -f locustfile.py --host=https://valuefrontier.cn \ + --users 1000 --spawn-rate 50 --run-time 5m --headless +``` + +### 分布式压测(多机) +```bash +# 主节点 +locust -f locustfile.py --master --host=https://valuefrontier.cn + +# 从节点(在其他机器执行) +locust -f locustfile.py --worker --master-host=<主节点IP> +``` + +--- + +## 方案二:wrk(极限性能测试) + +### 安装(Linux) +```bash +# Ubuntu/Debian +apt-get install wrk + +# CentOS/RHEL +yum install wrk + +# 或从源码编译 +git clone https://github.com/wg/wrk.git +cd wrk && make +``` + +### 基础测试 +```bash +# 12 线程,400 连接,持续 30 秒 +wrk -t12 -c400 -d30s https://valuefrontier.cn/api/stocks + +# 带 Lua 脚本的复杂测试 +wrk -t12 -c400 -d30s -s post.lua https://valuefrontier.cn/api/endpoint +``` + +### 逐步加压测试 +```bash +# 测试脚本 +for connections in 100 500 1000 2000 5000 10000; do + echo "=== Testing with $connections connections ===" + wrk -t12 -c$connections -d30s https://valuefrontier.cn/api/stocks + sleep 10 +done +``` + +--- + +## 方案三:ab(Apache Bench) + +### 安装 +```bash +# Ubuntu/Debian +apt-get install apache2-utils + +# CentOS/RHEL +yum install httpd-tools +``` + +### 基础测试 +```bash +# 1000 请求,100 并发 +ab -n 1000 -c 100 https://valuefrontier.cn/api/stocks + +# 持续测试 60 秒 +ab -t 60 -c 100 https://valuefrontier.cn/api/stocks +``` + +--- + +## 测试指标解读 + +### 关键指标 + +| 指标 | 含义 | 理想值 | +|------|------|--------| +| **RPS (Requests/sec)** | 每秒请求数 | 越高越好 | +| **Latency (ms)** | 响应延迟 | P99 < 500ms | +| **Error Rate** | 错误率 | < 1% | +| **Throughput** | 吞吐量 | 越高越好 | + +### 性能等级参考 + +| 等级 | RPS | 延迟 P99 | 说明 | +|------|-----|---------|------| +| 优秀 | > 10,000 | < 100ms | 极限性能 | +| 良好 | 5,000-10,000 | < 200ms | 高性能 | +| 合格 | 1,000-5,000 | < 500ms | 正常水平 | +| 较差 | < 1,000 | > 500ms | 需要优化 | + +--- + +## 测试建议 + +### 测试前准备 +1. 确保服务器系统优化已完成(sysctl、limits.conf) +2. 关闭不必要的日志输出(loglevel 改为 warning) +3. 确保 Redis 正常运行 +4. 准备监控工具(htop、iotop、nethogs) + +### 测试步骤 +1. **预热阶段**:100 用户,运行 2 分钟 +2. **正常负载**:500 用户,运行 5 分钟 +3. **高负载**:2000 用户,运行 5 分钟 +4. **极限测试**:5000+ 用户,运行 10 分钟 +5. **恢复测试**:降到 100 用户,观察系统恢复 + +### 监控命令 +```bash +# 服务器上同时运行 +htop # CPU/内存监控 +iotop # 磁盘 IO 监控 +nethogs # 网络流量监控 +watch -n 1 'ss -s' # 连接数统计 +tail -f /var/log/nginx/error.log # Nginx 错误日志 +``` + +--- + +## 预期性能 + +基于 110.42.32.207(48核 128GB)+ Eventlet 16 Workers 配置: + +| 场景 | 预期 RPS | 预期并发 | +|------|---------|---------| +| 静态页面 | 50,000+ | 100,000+ | +| API 请求 | 10,000-20,000 | 50,000+ | +| WebSocket | - | 100,000+ 连接 | +| 混合场景 | 5,000-10,000 | 30,000+ | diff --git a/stress_test/locustfile.py b/stress_test/locustfile.py new file mode 100644 index 00000000..9eb97072 --- /dev/null +++ b/stress_test/locustfile.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- +""" +Locust 压力测试脚本 - VF React 网站 + +使用方式: + # 安装依赖 + pip install locust + + # 启动 Web UI 模式(推荐) + locust -f locustfile.py --host=https://valuefrontier.cn + + # 命令行模式(无 UI) + locust -f locustfile.py --host=https://valuefrontier.cn \ + --users 1000 --spawn-rate 50 --run-time 5m --headless + + # 分布式模式(多机压测) + # 主节点 + locust -f locustfile.py --master --host=https://valuefrontier.cn + # 从节点(在其他机器运行) + locust -f locustfile.py --worker --master-host= + +Web UI 访问: http://localhost:8089 +""" + +from locust import HttpUser, task, between, events +import random +import time +import json + + +class WebsiteUser(HttpUser): + """模拟普通网站用户行为""" + + # 用户请求间隔(1-3秒,模拟真实用户) + wait_time = between(1, 3) + + def on_start(self): + """用户启动时执行(可用于登录)""" + self.stock_codes = [ + "600000", "600036", "601318", "000001", "000002", + "300750", "002594", "601888", "600519", "000858" + ] + + @task(10) + def view_homepage(self): + """访问首页(权重 10)""" + self.client.get("/", name="首页") + + @task(8) + def get_stock_list(self): + """获取股票列表(权重 8)""" + self.client.get("/api/stocks", name="股票列表 API") + + @task(5) + def get_stock_detail(self): + """获取个股详情(权重 5)""" + code = random.choice(self.stock_codes) + self.client.get(f"/api/stocks/{code}", name="个股详情 API") + + @task(3) + def get_community_events(self): + """获取社区事件(权重 3)""" + self.client.get("/api/community/events", name="社区事件 API") + + @task(2) + def get_market_overview(self): + """获取市场概览(权重 2)""" + self.client.get("/api/market/overview", name="市场概览 API") + + +class APIUser(HttpUser): + """模拟 API 密集型用户(重度用户)""" + + wait_time = between(0.5, 1.5) + + def on_start(self): + self.stock_codes = [ + "600000", "600036", "601318", "000001", "000002", + "300750", "002594", "601888", "600519", "000858" + ] + + @task(5) + def get_realtime_quote(self): + """获取实时行情""" + code = random.choice(self.stock_codes) + self.client.get(f"/api/quote/{code}", name="实时行情 API") + + @task(3) + def get_kline_data(self): + """获取 K 线数据""" + code = random.choice(self.stock_codes) + self.client.get(f"/api/kline/{code}?period=day", name="K线数据 API") + + @task(2) + def get_concept_stocks(self): + """获取概念板块股票""" + self.client.get("/api/concepts", name="概念板块 API") + + +class AuthenticatedUser(HttpUser): + """模拟登录用户行为""" + + wait_time = between(2, 5) + + def on_start(self): + """登录获取 session""" + # 如果有测试账号,可以在这里登录 + # response = self.client.post("/api/auth/login", json={ + # "username": "test_user", + # "password": "test_pass" + # }) + pass + + @task(3) + def view_portfolio(self): + """查看投资组合""" + self.client.get("/api/portfolio", name="投资组合 API") + + @task(2) + def view_watchlist(self): + """查看自选股""" + self.client.get("/api/watchlist", name="自选股 API") + + +# ==================== 自定义统计 ==================== + +@events.request.add_listener +def on_request(request_type, name, response_time, response_length, exception, **kwargs): + """请求完成后的回调(可用于自定义监控)""" + if exception: + print(f"❌ 请求失败: {name} - {exception}") + elif response_time > 1000: # 超过 1 秒的慢请求 + print(f"⚠️ 慢请求: {name} - {response_time:.0f}ms") + + +@events.test_start.add_listener +def on_test_start(environment, **kwargs): + """测试开始时执行""" + print("=" * 60) + print("🚀 压力测试开始!") + print(f" 目标: {environment.host}") + print("=" * 60) + + +@events.test_stop.add_listener +def on_test_stop(environment, **kwargs): + """测试结束时执行""" + print("=" * 60) + print("🛑 压力测试结束!") + print("=" * 60) diff --git a/stress_test/quick_test.sh b/stress_test/quick_test.sh new file mode 100644 index 00000000..4cf0749f --- /dev/null +++ b/stress_test/quick_test.sh @@ -0,0 +1,46 @@ +#!/bin/bash +# 快速压力测试脚本 + +# 配置 +HOST="${1:-https://valuefrontier.cn}" +DURATION="${2:-30s}" + +echo "==============================================" +echo "🚀 快速压力测试" +echo " 目标: $HOST" +echo " 时长: $DURATION" +echo "==============================================" + +# 检查 wrk 是否安装 +if ! command -v wrk &> /dev/null; then + echo "❌ wrk 未安装,正在安装..." + if command -v apt-get &> /dev/null; then + apt-get update && apt-get install -y wrk + elif command -v yum &> /dev/null; then + yum install -y wrk + else + echo "请手动安装 wrk: https://github.com/wg/wrk" + exit 1 + fi +fi + +echo "" +echo "=== 测试 1: 首页 (100 连接) ===" +wrk -t4 -c100 -d$DURATION "$HOST/" + +echo "" +echo "=== 测试 2: API 接口 (500 连接) ===" +wrk -t8 -c500 -d$DURATION "$HOST/api/stocks" + +echo "" +echo "=== 测试 3: 高并发 (2000 连接) ===" +wrk -t12 -c2000 -d$DURATION "$HOST/api/stocks" + +echo "" +echo "=== 测试 4: 极限测试 (5000 连接) ===" +wrk -t12 -c5000 -d$DURATION "$HOST/api/stocks" + +echo "" +echo "==============================================" +echo "✅ 压力测试完成!" +echo "==============================================" diff --git a/stress_test/websocket_test.py b/stress_test/websocket_test.py new file mode 100644 index 00000000..71ec1980 --- /dev/null +++ b/stress_test/websocket_test.py @@ -0,0 +1,177 @@ +# -*- coding: utf-8 -*- +""" +WebSocket 压力测试脚本 + +使用方式: + pip install python-socketio[client] websocket-client + + # 测试 1000 个 WebSocket 连接 + python websocket_test.py --url wss://valuefrontier.cn --connections 1000 + + # 测试 5000 个连接,持续 5 分钟 + python websocket_test.py --url wss://valuefrontier.cn --connections 5000 --duration 300 +""" + +import argparse +import asyncio +import time +import statistics +from datetime import datetime +import socketio + +# 统计数据 +stats = { + "connected": 0, + "disconnected": 0, + "messages_received": 0, + "errors": 0, + "connect_times": [], +} + + +async def create_client(client_id, url, namespace="/"): + """创建单个 WebSocket 客户端""" + sio = socketio.AsyncClient( + reconnection=False, + logger=False, + engineio_logger=False + ) + + start_time = time.time() + + @sio.event + async def connect(): + connect_time = (time.time() - start_time) * 1000 + stats["connected"] += 1 + stats["connect_times"].append(connect_time) + + @sio.event + async def disconnect(): + stats["disconnected"] += 1 + + @sio.event + async def message(data): + stats["messages_received"] += 1 + + @sio.on("*") + async def catch_all(event, data): + stats["messages_received"] += 1 + + try: + await sio.connect(url, namespaces=[namespace]) + return sio + except Exception as e: + stats["errors"] += 1 + return None + + +async def run_test(url, num_connections, duration, batch_size=100): + """运行 WebSocket 压力测试""" + print("=" * 60) + print(f"🚀 WebSocket 压力测试") + print(f" 目标: {url}") + print(f" 连接数: {num_connections}") + print(f" 持续时间: {duration} 秒") + print(f" 批量大小: {batch_size}") + print("=" * 60) + + clients = [] + start_time = time.time() + + # 分批创建连接 + print(f"\n📡 开始创建 {num_connections} 个 WebSocket 连接...") + + for i in range(0, num_connections, batch_size): + batch_end = min(i + batch_size, num_connections) + batch_tasks = [ + create_client(j, url) + for j in range(i, batch_end) + ] + + batch_results = await asyncio.gather(*batch_tasks, return_exceptions=True) + + for result in batch_results: + if result and not isinstance(result, Exception): + clients.append(result) + + # 打印进度 + progress = (batch_end / num_connections) * 100 + print(f" 进度: {batch_end}/{num_connections} ({progress:.1f}%) - " + f"成功: {stats['connected']}, 失败: {stats['errors']}") + + # 短暂暂停,避免连接风暴 + await asyncio.sleep(0.1) + + connect_duration = time.time() - start_time + print(f"\n✅ 连接阶段完成!") + print(f" 耗时: {connect_duration:.2f} 秒") + print(f" 成功连接: {stats['connected']}") + print(f" 连接失败: {stats['errors']}") + + if stats["connect_times"]: + print(f" 平均连接时间: {statistics.mean(stats['connect_times']):.2f} ms") + print(f" P99 连接时间: {statistics.quantiles(stats['connect_times'], n=100)[98]:.2f} ms") + + # 保持连接一段时间 + print(f"\n⏳ 保持连接 {duration} 秒...") + messages_before = stats["messages_received"] + + await asyncio.sleep(duration) + + messages_after = stats["messages_received"] + messages_during = messages_after - messages_before + + # 断开所有连接 + print("\n📴 断开所有连接...") + disconnect_tasks = [ + client.disconnect() + for client in clients + if client and client.connected + ] + await asyncio.gather(*disconnect_tasks, return_exceptions=True) + + # 打印最终统计 + total_duration = time.time() - start_time + print("\n" + "=" * 60) + print("📊 测试结果") + print("=" * 60) + print(f" 总耗时: {total_duration:.2f} 秒") + print(f" 成功连接: {stats['connected']}") + print(f" 连接失败: {stats['errors']}") + print(f" 连接成功率: {(stats['connected'] / num_connections * 100):.2f}%") + print(f" 收到消息数: {messages_during}") + print(f" 消息速率: {(messages_during / duration):.2f} msg/s") + + if stats["connect_times"]: + print(f"\n 连接延迟统计:") + print(f" 最小: {min(stats['connect_times']):.2f} ms") + print(f" 最大: {max(stats['connect_times']):.2f} ms") + print(f" 平均: {statistics.mean(stats['connect_times']):.2f} ms") + print(f" 中位数: {statistics.median(stats['connect_times']):.2f} ms") + + print("=" * 60) + + +def main(): + parser = argparse.ArgumentParser(description="WebSocket 压力测试") + parser.add_argument("--url", default="wss://valuefrontier.cn", + help="WebSocket URL (default: wss://valuefrontier.cn)") + parser.add_argument("--connections", type=int, default=1000, + help="并发连接数 (default: 1000)") + parser.add_argument("--duration", type=int, default=60, + help="测试持续时间(秒) (default: 60)") + parser.add_argument("--batch", type=int, default=100, + help="批量创建连接数 (default: 100)") + + args = parser.parse_args() + + asyncio.run(run_test( + url=args.url, + num_connections=args.connections, + duration=args.duration, + batch_size=args.batch + )) + + +if __name__ == "__main__": + main()