update pay ui

This commit is contained in:
2025-12-12 00:02:55 +08:00
parent 02ca4f48e6
commit eb961d83f1
6 changed files with 562 additions and 46 deletions

View File

@@ -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 中,由各自组件内部管理

View File

@@ -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 (
<>
<Tooltip
label={<TooltipContent subscriptionInfo={subscriptionInfo} />}
placement="bottom"
hasArrow
bg={tooltipBg}
borderRadius="lg"
border="1px solid"
borderColor={tooltipBorderColor}
boxShadow="lg"
p={3}
>
<span>
<UserAvatar
user={user}
subscriptionInfo={subscriptionInfo}
onClick={openSubscriptionModal}
/>
</span>
</Tooltip>
const handleAvatarClick = () => {
navigate('/home/pages/account/subscription');
};
{isSubscriptionModalOpen && (
<SubscriptionModal
isOpen={isSubscriptionModalOpen}
onClose={closeSubscriptionModal}
return (
<Tooltip
label={<TooltipContent subscriptionInfo={subscriptionInfo} />}
placement="bottom"
hasArrow
bg={tooltipBg}
borderRadius="lg"
border="1px solid"
borderColor={tooltipBorderColor}
boxShadow="lg"
p={3}
>
<span>
<UserAvatar
user={user}
subscriptionInfo={subscriptionInfo}
onClick={handleAvatarClick}
/>
)}
</>
</span>
</Tooltip>
);
});

162
stress_test/README.md Normal file
View File

@@ -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
```
---
## 方案三abApache 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.20748核 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+ |

150
stress_test/locustfile.py Normal file
View File

@@ -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=<master-ip>
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)

46
stress_test/quick_test.sh Normal file
View File

@@ -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 "=============================================="

View File

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