fix: 增加了前端的缓存策略

master
Lxy 3 months ago
parent 07debec4ce
commit 229c10a225

@ -393,55 +393,62 @@ class TqSdkAdapter(BaseDataAdapter):
print(f"使用模拟主力合约数据: {mock_main_contracts}")
return mock_main_contracts
def get_last_trading_day(self, symbol: str) -> Optional[str]:
def get_last_trading_day(self) -> Optional[str]:
"""获取最后一个交易日
Args:
symbol: 合约代码 'CU2603'
Returns:
str: 最后一个交易日格式为 'YYYY-MM-DD'
"""
try:
if TQSDK_AVAILABLE and self.api:
# 转换合约代码为TQSDK格式
tq_symbol = self._convert_symbol(symbol)
print(f"使用TQSDK格式合约代码: {tq_symbol}")
# 获取日线数据
klines = self.api.get_kline_serial(tq_symbol, 86400, data_length=30) # 86400秒 = 1天
# 等待数据准备就绪
import time
start_time = time.time()
timeout = 5 # 5秒超时
# 计算日期范围
import datetime
end_date = datetime.datetime.now()
start_date = end_date - datetime.timedelta(days=10)
while True:
if hasattr(klines, 'datetime') and len(klines.datetime) > 0:
break
if time.time() - start_time > timeout:
print("获取K线数据超时")
return None
time.sleep(0.1)
# 格式化日期为字符串
start_date_str = start_date.strftime('%Y%m%d')
end_date_str = end_date.strftime('%Y%m%d')
# 转换为DataFrame
data = {
'datetime': klines.datetime,
'close': klines.close
}
df = pd.DataFrame(data)
df['datetime'] = pd.to_datetime(df['datetime'], unit='ns')
print(f"获取交易日历,开始日期: {start_date_str}, 结束日期: {end_date_str}")
# 过滤掉成交量为0的日期非交易日
# 注意TQSDK的K线数据中如果当天没有交易可能不会生成数据
# 所以我们只需要取最后一条数据的日期
if not df.empty:
last_trading_day = df['datetime'].iloc[-1].strftime('%Y-%m-%d')
print(f"最后一个交易日: {last_trading_day}")
return last_trading_day
else:
print("无法获取K线数据")
return None
# 使用get_trading_calendar获取交易日历
try:
# 获取交易日历
calendar = self.api.get_trading_calendar(start_date_str, end_date_str)
# 等待数据准备就绪
import time
start_time = time.time()
timeout = 5 # 5秒超时
while True:
if hasattr(calendar, 'dates') and len(calendar.dates) > 0:
break
if time.time() - start_time > timeout:
print("获取交易日历超时")
# 回退到K线数据方法
return ""
time.sleep(0.1)
# 处理交易日历数据
if hasattr(calendar, 'dates') and len(calendar.dates) > 0:
# 获取最后一个交易日
last_trading_date = calendar.dates[-1]
# 转换为YYYY-MM-DD格式
last_trading_day = last_trading_date.strftime('%Y-%m-%d')
print(f"从交易日历获取最后交易日: {last_trading_day}")
return last_trading_day
else:
print("交易日历数据为空")
# 回退到K线数据方法
return ""
except Exception as calendar_error:
print(f"使用get_trading_calendar失败{calendar_error}")
# 回退到K线数据方法
return ""
else:
# 返回模拟数据
print("无法获取真实数据,使用模拟最后交易日")
@ -458,6 +465,52 @@ class TqSdkAdapter(BaseDataAdapter):
print(f"使用模拟最后交易日: {last_trading_day}")
return last_trading_day
def _get_last_trading_day_from_kline(self, tq_symbol: str) -> Optional[str]:
"""从K线数据中获取最后交易日作为回退方法
Args:
tq_symbol: TQSDK格式的合约代码
Returns:
str: 最后一个交易日格式为 'YYYY-MM-DD'
"""
try:
# 获取日线数据
klines = self.api.get_kline_serial(tq_symbol, 86400, data_length=30) # 86400秒 = 1天
# 等待数据准备就绪
import time
start_time = time.time()
timeout = 5 # 5秒超时
while True:
if hasattr(klines, 'datetime') and len(klines.datetime) > 0:
break
if time.time() - start_time > timeout:
print("获取K线数据超时")
return None
time.sleep(0.1)
# 转换为DataFrame
data = {
'datetime': klines.datetime,
'close': klines.close
}
df = pd.DataFrame(data)
df['datetime'] = pd.to_datetime(df['datetime'], unit='ns')
# 取最后一条数据的日期
if not df.empty:
last_trading_day = df['datetime'].iloc[-1].strftime('%Y-%m-%d')
print(f"从K线数据获取最后交易日: {last_trading_day}")
return last_trading_day
else:
print("无法获取K线数据")
return None
except Exception as e:
print(f"从K线数据获取最后交易日失败{e}")
return None
def _get_mock_all_symbols(self) -> List[str]:
"""获取模拟品种列表"""
# 返回exchange_map中映射的所有品种

@ -464,18 +464,16 @@ class DataFetcher:
'SN': 'SN2603' # 锡
}
def get_last_trading_day(self, symbol: str) -> Optional[str]:
def get_last_trading_day(self) -> Optional[str]:
"""获取最后一个交易日
Args:
symbol: 合约代码 'CU2603'
Returns:
str: 最后一个交易日格式为 'YYYY-MM-DD'
"""
try:
# 使用适配器的get_last_trading_day方法
result = self.adapter.get_last_trading_day(symbol)
result = self.adapter.get_last_trading_day()
if result:
return result
else:

@ -72,13 +72,11 @@ def get_main_contracts():
@app.route('/api/last-trading-day', methods=['GET'])
def get_last_trading_day():
try:
symbol = request.args.get('symbol', 'CU2603') # 默认使用CU2603合约
print(f"正在获取最后交易日,合约:{symbol}")
last_trading_day = data_fetcher.get_last_trading_day(symbol)
print(f"获取到最后交易日:{last_trading_day}")
print(f"正在获取最后交易日")
last_trading_day = data_fetcher.get_last_trading_day()
print(f"获取到最后交易日")
return jsonify({'status': 'success', 'data': {
'symbol': symbol,
'last_trading_day': last_trading_day
}})
except Exception as e:

@ -101,7 +101,7 @@ def get_last_trading_day():
**示例请求**
```
GET http://localhost:5000/api/last-trading-day?symbol=AU2603
GET http://localhost:5000/api/last-trading-day
```
**示例响应**

@ -1,11 +1,160 @@
import React, { useState } from 'react';
import { Card, Row, Col, Form, Input, Button, Select, Switch, Slider, Tag, Alert, InputNumber, Radio, Space, Divider, Statistic } from 'antd';
import { SettingOutlined, SlidersOutlined, SaveOutlined, CloseOutlined, SafetyOutlined } from '@ant-design/icons';
import React, { useState, useEffect } from 'react';
import { Card, Row, Col, Form, Input, Button, Select, Switch, Slider, Tag, Alert, InputNumber, Radio, Space, Divider, Statistic, Table } from 'antd';
import { SettingOutlined, SlidersOutlined, SaveOutlined, CloseOutlined, SafetyOutlined, DatabaseOutlined, ClearOutlined } from '@ant-design/icons';
import dataCache from '../../utils/cache';
import './Config.css';
const { Option } = Select;
const { Item } = Form;
//
const CacheManager = () => {
const [stats, setStats] = useState({ total: 0, valid: 0, expired: 0 });
const [cacheKeys, setCacheKeys] = useState([]);
const [message, setMessage] = useState('');
const refreshStats = () => {
const currentStats = dataCache.getStats();
setStats(currentStats);
//
const keys = [];
// Map
const knownKeys = [
{ key: 'futures_overview', name: '市场概览数据', ttl: '5分钟' },
{ key: 'risk_alerts', name: '风险预警数据', ttl: '3分钟' },
{ key: 'ai_market_analysis', name: 'AI市场分析', ttl: '10分钟' },
];
const dynamicKeys = [];
// K线code
const mockCodes = ['AU', 'AG', 'CU', 'RB', 'MA', 'SC'];
mockCodes.forEach(code => {
const detailKey = `future_detail_${code}`;
dynamicKeys.push({ key: detailKey, name: `${code}品种详情`, ttl: '10分钟' });
['5M', '30M', '1H', '1D'].forEach(period => {
dynamicKeys.push({ key: `kline_${code}_${period}`, name: `${code} K线(${period})`, ttl: '30分钟' });
});
});
//
const allKeys = [...knownKeys, ...dynamicKeys];
const existingKeys = allKeys.filter(item => dataCache.has(item.key));
setCacheKeys(existingKeys);
};
useEffect(() => {
refreshStats();
// 5
const interval = setInterval(refreshStats, 5000);
return () => clearInterval(interval);
}, []);
const handleClearCache = () => {
dataCache.clear();
refreshStats();
setMessage('所有缓存已清除');
setTimeout(() => setMessage(''), 3000);
};
const handleClearSpecificCache = (key) => {
dataCache.delete(key);
refreshStats();
setMessage(`缓存 ${key} 已清除`);
setTimeout(() => setMessage(''), 3000);
};
const columns = [
{
title: '缓存名称',
dataIndex: 'name',
key: 'name',
},
{
title: '缓存键',
dataIndex: 'key',
key: 'key',
render: (key) => <code style={{ fontSize: '12px' }}>{key}</code>,
},
{
title: '有效期',
dataIndex: 'ttl',
key: 'ttl',
},
{
title: '操作',
key: 'action',
render: (_, record) => (
<Button
type="link"
danger
size="small"
onClick={() => handleClearSpecificCache(record.key)}
>
清除
</Button>
),
},
];
return (
<div>
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
<Col span={6}>
<Card>
<Statistic title="总缓存数" value={stats.total} />
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic title="有效缓存" value={stats.valid} valueStyle={{ color: '#52c41a' }} />
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic title="过期缓存" value={stats.expired} valueStyle={{ color: '#ff4d4f' }} />
</Card>
</Col>
<Col span={6}>
<Card>
<Button
type="primary"
danger
icon={<ClearOutlined />}
onClick={handleClearCache}
style={{ marginTop: 8 }}
>
清除所有缓存
</Button>
</Card>
</Col>
</Row>
{message && (
<Alert message={message} type="success" showIcon style={{ marginBottom: 16 }} />
)}
<Alert
message="缓存说明"
description="系统会自动缓存API请求数据减少重复请求。市场概览缓存5分钟品种详情缓存10分钟K线数据缓存30分钟。点击刷新按钮可强制更新缓存。"
type="info"
showIcon
style={{ marginBottom: 16 }}
/>
<h4>当前有效缓存列表</h4>
<Table
dataSource={cacheKeys}
columns={columns}
rowKey="key"
size="small"
pagination={false}
locale={{ emptyText: '暂无缓存数据' }}
/>
</div>
);
};
const Config = () => {
const [form] = Form.useForm();
@ -234,6 +383,15 @@ const Config = () => {
</Form>
</Card>
{/* 缓存管理 */}
<Card
title={<span><DatabaseOutlined /> 缓存管理</span>}
className="config-card"
style={{ marginTop: 24 }}
>
<CacheManager />
</Card>
{/* 风控管理 */}
<Card
title={<span><SafetyOutlined /> 风控管理</span>}

@ -3,10 +3,14 @@
}
.dashboard-header {
margin-bottom: 24px;
}
.dashboard-header-top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
margin-bottom: 16px;
}
.dashboard-header h2 {
@ -14,6 +18,32 @@
color: #262626;
}
.date-info {
display: flex;
gap: 20px;
font-size: 14px;
color: #8c8c8c;
}
.date-item {
display: flex;
align-items: center;
}
.date-item::before {
content: '';
display: inline-block;
width: 8px;
height: 8px;
background: #1890ff;
border-radius: 50%;
margin-right: 8px;
}
.date-info .date-item:nth-child(2)::before {
background: #52c41a;
}
.dashboard-header-actions {
display: flex;
align-items: center;

@ -20,9 +20,37 @@ const Dashboard = () => {
const [currentFuture, setCurrentFuture] = useState(null);
const [pushForm] = Form.useForm();
const [messageApi, contextHolder] = message.useMessage();
const [lastTradingDay, setLastTradingDay] = useState('');
const [currentDate, setCurrentDate] = useState('');
// refAPI
const fetchingRef = React.useRef(false);
//
const fetchLastTradingDay = async () => {
try {
// 使
const response = await fetch('/api/last-trading-day');
const data = await response.json();
if (data.status === 'success') {
setLastTradingDay(data.data.last_trading_day);
}
} catch (error) {
console.error('获取最后交易日失败:', error);
// 使fallback
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
const formattedDate = yesterday.toISOString().split('T')[0];
setLastTradingDay(formattedDate);
}
};
//
const setCurrentDateValue = () => {
const now = new Date();
const formattedDate = now.toISOString().split('T')[0];
setCurrentDate(formattedDate);
};
useEffect(() => {
if (!fetchingRef.current) {
@ -30,15 +58,22 @@ const Dashboard = () => {
dispatch(fetchFuturesOverview());
dispatch(fetchRiskAlerts());
dispatch(fetchAIMarketAnalysis());
//
fetchLastTradingDay();
setCurrentDateValue();
}
}, [dispatch]);
const handleRefresh = () => {
fetchingRef.current = false;
dispatch(fetchFuturesOverview());
dispatch(fetchRiskAlerts());
dispatch(fetchAIMarketAnalysis());
messageApi.success('数据已刷新');
// 使
dispatch(fetchFuturesOverview({ forceRefresh: true }));
dispatch(fetchRiskAlerts({ forceRefresh: true }));
dispatch(fetchAIMarketAnalysis({ forceRefresh: true }));
//
fetchLastTradingDay();
setCurrentDateValue();
messageApi.success('数据已刷新(已强制更新缓存)');
};
const handleFutureClick = (future) => {
@ -124,7 +159,13 @@ const Dashboard = () => {
<div className="dashboard">
{/* 页面头部 */}
<div className="dashboard-header">
<h2>市场概览</h2>
<div className="dashboard-header-top">
<h2>市场概览</h2>
<div className="date-info">
<span className="date-item">当前日期: {currentDate}</span>
<span className="date-item">最后交易日: {lastTradingDay || '加载中...'}</span>
</div>
</div>
<div className="dashboard-header-actions">
<Select
defaultValue="all"

@ -3,6 +3,7 @@ import { Card, Button, Row, Col, Select, Tabs, Tag, Statistic, Alert, Spin } fro
import { useParams, useNavigate } from 'react-router-dom';
import { LineChartOutlined, BarChartOutlined, AreaChartOutlined, ArrowUpOutlined, AlertOutlined, RobotOutlined, SafetyOutlined } from '@ant-design/icons';
import { generateFutureData, generateKlineData } from '../../utils/mockData';
import dataCache from '../../utils/cache';
import * as echarts from 'echarts';
import './Detail.css';
@ -22,9 +23,23 @@ const Detail = () => {
console.log('Detail page loaded with code:', code);
useEffect(() => {
// API
// API
const fetchFutureDetail = async () => {
setLoading(true);
const cacheKey = `future_detail_${code}`;
try {
//
const cached = dataCache.get(cacheKey);
if (cached) {
console.log('[Cache] Detail页面使用缓存数据:', code);
setData(cached);
setLoading(false);
return;
}
//
console.log('Fetching detail for code:', code);
const response = await fetch(`http://localhost:3007/api/market/detail/${code}`);
if (!response.ok) {
@ -32,12 +47,16 @@ const Detail = () => {
}
const result = await response.json();
console.log('Received detail data:', result);
// 10
dataCache.set(cacheKey, result.data, 10 * 60 * 1000);
setData(result.data);
setLoading(false);
} catch (error) {
console.error('获取品种详情失败:', error);
// 使
const futureData = generateFutureData(code, code); // 使code
const futureData = generateFutureData(code, code);
setData(futureData);
setLoading(false);
}
@ -55,9 +74,19 @@ const Detail = () => {
chartInstance.current.dispose();
}
// APIK线
// APIK线
const fetchKlineData = async () => {
const cacheKey = `kline_${code}_${currentPeriod}`;
try {
//
const cached = dataCache.get(cacheKey);
if (cached) {
console.log('[Cache] K线数据使用缓存:', code, currentPeriod);
return cached;
}
//
console.log('Fetching Kline data for code:', code, 'period:', currentPeriod);
const response = await fetch(`http://localhost:3007/api/market/klines/${code}?period=${currentPeriod}`);
if (!response.ok) {
@ -68,15 +97,15 @@ const Detail = () => {
//
if (result.data && Array.isArray(result.data) && result.data.length > 0) {
// K线30
dataCache.set(cacheKey, result.data, 30 * 60 * 1000);
return result.data;
} else {
console.warn('Kline data is empty or invalid, using mock data');
// 使
return generateKlineData(30);
}
} catch (error) {
console.error('获取K线数据失败:', error);
// 使
return generateKlineData(30);
}
};

@ -25,8 +25,9 @@ const Watchlist = () => {
}, [dispatch]);
const handleRefresh = () => {
dispatch(fetchFuturesOverview());
messageApi.success('数据已刷新');
// 使
dispatch(fetchFuturesOverview({ forceRefresh: true }));
messageApi.success('数据已刷新(已强制更新缓存)');
};
const handleFutureClick = (future) => {

@ -1,61 +1,196 @@
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import dataCache from '../utils/cache';
// 后端API基础URL
const API_BASE_URL = 'http://localhost:3007/api';
// 缓存有效期配置(毫秒)
const CACHE_TTL = {
OVERVIEW: 5 * 60 * 1000, // 市场概览5分钟
DETAIL: 10 * 60 * 1000, // 品种详情10分钟
RISK_ALERTS: 3 * 60 * 1000, // 风险预警3分钟
AI_ANALYSIS: 10 * 60 * 1000, // AI分析10分钟
};
// 异步获取期货概览数据
export const fetchFuturesOverview = createAsyncThunk(
'futures/fetchOverview',
async () => {
const response = await fetch(`${API_BASE_URL}/market/overview`);
if (!response.ok) {
throw new Error('获取市场概览失败');
async ({ forceRefresh = false } = {}, { rejectWithValue }) => {
const cacheKey = 'futures_overview';
// 检查缓存
if (!forceRefresh) {
const cached = dataCache.get(cacheKey);
if (cached) {
console.log('[Cache] 使用缓存的市场概览数据');
return cached;
}
}
try {
const response = await fetch(`${API_BASE_URL}/market/overview`);
if (!response.ok) {
throw new Error('获取市场概览失败');
}
const data = await response.json();
// 缓存数据
dataCache.set(cacheKey, data.data, CACHE_TTL.OVERVIEW);
console.log('[Cache] 市场概览数据已缓存');
return data.data;
} catch (error) {
return rejectWithValue(error.message);
}
const data = await response.json();
return data.data;
}
);
// 异步获取单个期货详情
export const fetchFutureDetail = createAsyncThunk(
'futures/fetchDetail',
async ({ code, name }) => {
const response = await fetch(`${API_BASE_URL}/market/detail/${code}`);
if (!response.ok) {
throw new Error('获取品种详情失败');
async ({ code, forceRefresh = false }, { rejectWithValue }) => {
const cacheKey = `future_detail_${code}`;
// 检查缓存
if (!forceRefresh) {
const cached = dataCache.get(cacheKey);
if (cached) {
console.log(`[Cache] 使用缓存的品种详情数据: ${code}`);
return cached;
}
}
try {
const response = await fetch(`${API_BASE_URL}/market/detail/${code}`);
if (!response.ok) {
throw new Error('获取品种详情失败');
}
const data = await response.json();
// 缓存数据
dataCache.set(cacheKey, data.data, CACHE_TTL.DETAIL);
console.log(`[Cache] 品种详情数据已缓存: ${code}`);
return data.data;
} catch (error) {
return rejectWithValue(error.message);
}
const data = await response.json();
return data.data;
}
);
// 异步获取风险预警
export const fetchRiskAlerts = createAsyncThunk(
'futures/fetchRiskAlerts',
async () => {
const response = await fetch(`${API_BASE_URL}/market/alerts`);
if (!response.ok) {
throw new Error('获取风险预警失败');
async ({ forceRefresh = false } = {}, { rejectWithValue }) => {
const cacheKey = 'risk_alerts';
// 检查缓存
if (!forceRefresh) {
const cached = dataCache.get(cacheKey);
if (cached) {
console.log('[Cache] 使用缓存的风险预警数据');
return cached;
}
}
try {
const response = await fetch(`${API_BASE_URL}/market/alerts`);
if (!response.ok) {
throw new Error('获取风险预警失败');
}
const data = await response.json();
// 缓存数据
dataCache.set(cacheKey, data.data, CACHE_TTL.RISK_ALERTS);
console.log('[Cache] 风险预警数据已缓存');
return data.data;
} catch (error) {
return rejectWithValue(error.message);
}
const data = await response.json();
return data.data;
}
);
// 异步获取AI市场分析
export const fetchAIMarketAnalysis = createAsyncThunk(
'futures/fetchAIMarketAnalysis',
async () => {
// 由于后端没有专门的市场分析接口,我们使用模拟数据
return {
async ({ forceRefresh = false } = {}, { rejectWithValue }) => {
const cacheKey = 'ai_market_analysis';
// 检查缓存
if (!forceRefresh) {
const cached = dataCache.get(cacheKey);
if (cached) {
console.log('[Cache] 使用缓存的AI市场分析数据');
return cached;
}
}
// 由于后端没有专门的市场分析接口,使用模拟数据
const data = {
overallTrend: '震荡偏弱',
keyFactors: ['原油价格波动', '宏观经济数据', '政策面变化'],
recommendations: ['控制仓位', '关注原油走势', '做好止损'],
confidence: 75
};
// 缓存数据
dataCache.set(cacheKey, data, CACHE_TTL.AI_ANALYSIS);
console.log('[Cache] AI市场分析数据已缓存');
return data;
}
);
// 异步获取K线数据
export const fetchKlineData = createAsyncThunk(
'futures/fetchKlineData',
async ({ code, period = '1D', forceRefresh = false }, { rejectWithValue }) => {
const cacheKey = `kline_${code}_${period}`;
// 检查缓存
if (!forceRefresh) {
const cached = dataCache.get(cacheKey);
if (cached) {
console.log(`[Cache] 使用缓存的K线数据: ${code} ${period}`);
return { code, period, data: cached };
}
}
try {
const response = await fetch(`${API_BASE_URL}/market/klines/${code}?period=${period}`);
if (!response.ok) {
throw new Error('获取K线数据失败');
}
const result = await response.json();
// 缓存数据K线数据缓存时间较长
dataCache.set(cacheKey, result.data, 30 * 60 * 1000); // 30分钟
console.log(`[Cache] K线数据已缓存: ${code} ${period}`);
return { code, period, data: result.data };
} catch (error) {
return rejectWithValue(error.message);
}
}
);
// 清除指定缓存
export const clearCache = (key) => {
if (key) {
dataCache.delete(key);
console.log(`[Cache] 已清除缓存: ${key}`);
} else {
dataCache.clear();
console.log('[Cache] 已清除所有缓存');
}
};
// 获取缓存统计
export const getCacheStats = () => {
return dataCache.getStats();
};
const futuresSlice = createSlice({
name: 'futures',
initialState: {
@ -63,9 +198,11 @@ const futuresSlice = createSlice({
selectedFuture: null,
riskAlerts: [],
aiAnalysis: null,
klineData: {}, // 存储不同品种的K线数据 { [code_period]: data }
loading: false,
error: null,
watchlist: []
watchlist: [],
cacheStats: null // 缓存统计
},
reducers: {
selectFuture: (state, action) => {
@ -91,6 +228,12 @@ const futuresSlice = createSlice({
if (state.selectedFuture && state.selectedFuture.code === code) {
state.selectedFuture.isInWatchlist = state.watchlist.includes(code);
}
},
clearCacheStats: (state) => {
state.cacheStats = null;
},
updateCacheStats: (state) => {
state.cacheStats = dataCache.getStats();
}
},
extraReducers: (builder) => {
@ -159,9 +302,24 @@ const futuresSlice = createSlice({
.addCase(fetchAIMarketAnalysis.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message;
})
// 处理fetchKlineData
.addCase(fetchKlineData.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(fetchKlineData.fulfilled, (state, action) => {
state.loading = false;
const { code, period, data } = action.payload;
state.klineData[`${code}_${period}`] = data;
})
.addCase(fetchKlineData.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message;
});
}
});
export const { selectFuture, clearSelectedFuture, toggleWatchlist } = futuresSlice.actions;
export const { selectFuture, clearSelectedFuture, toggleWatchlist, clearCacheStats, updateCacheStats } = futuresSlice.actions;
export default futuresSlice.reducer;

@ -0,0 +1,198 @@
/**
* 数据缓存工具类
* 用于缓存API请求数据减少重复请求
*/
// 默认缓存有效期5分钟
const DEFAULT_CACHE_TTL = 5 * 60 * 1000;
class DataCache {
constructor() {
this.cache = new Map();
}
/**
* 生成缓存键
* @param {string} key - 缓存标识
* @param {object} params - 请求参数
* @returns {string} 缓存键
*/
generateKey(key, params = {}) {
if (Object.keys(params).length === 0) {
return key;
}
const sortedParams = Object.keys(params)
.sort()
.map(k => `${k}=${params[k]}`)
.join('&');
return `${key}?${sortedParams}`;
}
/**
* 获取缓存数据
* @param {string} key - 缓存键
* @returns {object|null} 缓存数据或null
*/
get(key) {
const cached = this.cache.get(key);
if (!cached) {
return null;
}
// 检查缓存是否过期
if (Date.now() > cached.expiresAt) {
this.cache.delete(key);
return null;
}
return cached.data;
}
/**
* 设置缓存数据
* @param {string} key - 缓存键
* @param {any} data - 要缓存的数据
* @param {number} ttl - 缓存有效期毫秒默认5分钟
*/
set(key, data, ttl = DEFAULT_CACHE_TTL) {
this.cache.set(key, {
data,
expiresAt: Date.now() + ttl
});
}
/**
* 检查缓存是否存在且有效
* @param {string} key - 缓存键
* @returns {boolean}
*/
has(key) {
const cached = this.cache.get(key);
if (!cached) {
return false;
}
if (Date.now() > cached.expiresAt) {
this.cache.delete(key);
return false;
}
return true;
}
/**
* 删除指定缓存
* @param {string} key - 缓存键
*/
delete(key) {
this.cache.delete(key);
}
/**
* 清空所有缓存
*/
clear() {
this.cache.clear();
}
/**
* 获取缓存统计信息
* @returns {object} 统计信息
*/
getStats() {
let valid = 0;
let expired = 0;
for (const [key, value] of this.cache.entries()) {
if (Date.now() <= value.expiresAt) {
valid++;
} else {
expired++;
}
}
return {
total: this.cache.size,
valid,
expired
};
}
}
// 创建单例实例
const dataCache = new DataCache();
export default dataCache;
/**
* 带缓存的fetch函数
* @param {string} url - 请求URL
* @param {object} options - fetch选项
* @param {object} cacheOptions - 缓存选项
* @param {string} cacheOptions.key - 缓存键
* @param {number} cacheOptions.ttl - 缓存有效期毫秒
* @param {boolean} cacheOptions.forceRefresh - 是否强制刷新
* @returns {Promise<any>} 响应数据
*/
export async function fetchWithCache(url, options = {}, cacheOptions = {}) {
const { key, ttl = DEFAULT_CACHE_TTL, forceRefresh = false } = cacheOptions;
// 如果没有指定缓存键使用URL作为键
const cacheKey = key || url;
// 检查缓存(非强制刷新时)
if (!forceRefresh) {
const cached = dataCache.get(cacheKey);
if (cached) {
console.log(`[Cache] 命中缓存: ${cacheKey}`);
return cached;
}
}
// 发起请求
console.log(`[Cache] 发起请求: ${url}`);
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`请求失败: ${response.status}`);
}
const data = await response.json();
// 缓存数据
dataCache.set(cacheKey, data, ttl);
return data;
}
/**
* 创建带缓存的Redux Thunk
* @param {string} type - action类型
* @param {Function} fetchFn - 获取数据的函数
* @param {object} cacheOptions - 缓存选项
*/
export function createCachedThunk(type, fetchFn, cacheOptions = {}) {
const { ttl = DEFAULT_CACHE_TTL, keyGenerator } = cacheOptions;
return (params = {}, { forceRefresh = false } = {}) => {
return async (dispatch, getState) => {
const cacheKey = keyGenerator ? keyGenerator(params) : type;
// 检查缓存
if (!forceRefresh) {
const cached = dataCache.get(cacheKey);
if (cached) {
console.log(`[Cache] Redux Thunk 命中缓存: ${cacheKey}`);
// 直接返回缓存数据,不发起请求
return { payload: cached };
}
}
// 发起请求
const result = await fetchFn(params);
// 缓存结果
dataCache.set(cacheKey, result, ttl);
return result;
};
};
}

@ -4,4 +4,13 @@ import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/api': {
target: 'http://localhost:5000',
changeOrigin: true,
secure: false,
}
}
}
})

@ -0,0 +1,78 @@
# 前端设计方案
## 1. 导航栏设计
### 1.1 左侧导航栏
- **移除**:详情分析入口
- **保留**:首页、数据管理、策略管理、回测分析
### 1.2 顶部导航栏
- **新增**:配置管理按钮(位于暗黑模式切换按钮旁边)
- **功能**:点击后跳转到配置管理页面
## 2. 页面结构
### 2.1 配置管理页面
- **整合**:将风控管理整个内容合并到配置管理中
- **布局**:保持原有配置管理功能,新增风控管理部分
### 2.2 首页/仪表盘
- **新增**:显示当前最后一个交易日日期
- **新增**:显示当前日期
- **数据来源**通过API接口获取最后交易日数据
## 3. API接口
### 3.1 最后交易日接口
- **端点**/api/last-trading-day
- **方法**GET
- **参数**:无需传递合约代码、交易所等参数
- **返回**
```json
{
"last_trading_day": "2026-02-24"
}
```
### 3.2 主力合约接口
- **端点**/api/main-contracts
- **方法**GET
- **参数**:无
- **返回**:主力合约列表
## 4. 错误处理
### 4.1 API请求失败处理
- **策略**当API请求失败时使用前一天作为模拟数据
- **实现**在前端代码中添加try-catch逻辑捕获fetch错误并使用备用数据
## 5. 技术实现
### 5.1 前端技术栈
- React
- TypeScript
- Vite
### 5.2 代理配置
- **Vite配置**:添加代理规则,将/api请求代理到http://localhost:5000
- **配置文件**vite.config.js
### 5.3 代码文件
- **导航栏**src/components/layout/MainLayout.jsx
- **配置管理**src/pages/config/Config.jsx
- **仪表盘**src/pages/dashboard/Dashboard.jsx
- **API调用**使用相对路径进行API请求
## 6. 响应式设计
- **适配**:支持不同屏幕尺寸
- **布局**:在小屏幕设备上自动调整导航栏布局
## 7. 用户体验
- **加载状态**API请求时显示加载指示器
- **错误提示**API请求失败时显示友好的错误提示
- **数据更新**:页面加载时自动获取最新数据
## 8. 后续优化
- **缓存策略**考虑在前端添加本地缓存减少API调用
- **数据可视化**:增加更多数据可视化组件
- **性能优化**:优化页面加载速度和响应时间
Loading…
Cancel
Save