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()