# 数据缓存功能开发文档 ## 概述 本文档描述了在 AdminConfig 管理配置页面中新增的数据缓存功能,包括浏览器本地缓存和数据库持久化缓存两大部分。 ## 功能清单 ### 1. 浏览器本地缓存(原有功能增强) - **一键获取数据并缓存**:获取市场概览、风险预警、热门品种详情和K线数据到浏览器 localStorage - **缓存统计**:显示有效缓存、过期缓存、缓存总数和上次更新时间 - **清除所有缓存**:一键清除浏览器本地缓存 ### 2. 数据库持久化缓存(新增功能) - **一键缓存所有合约到数据库**:批量获取所有合约详情和K线数据,存入 MySQL 和 Redis - **缓存指定合约到数据库**:针对单个合约进行强制刷新缓存 --- ## 前端实现 ### 文件位置 - `src/pages/admin/AdminConfig.jsx` ### 新增状态变量 ```javascript // 数据缓存相关状态 const [cacheLoading, setCacheLoading] = useState(false); const [cacheStats, setCacheStats] = useState({ total: 0, valid: 0, expired: 0 }); const [cacheProgress, setCacheProgress] = useState({ current: 0, total: 0, name: '' }); // 数据库缓存相关状态 const [dbCacheLoading, setDbCacheLoading] = useState(false); const [dbCacheProgress, setDbCacheProgress] = useState({ current: 0, total: 0, name: '' }); const [symbolInput, setSymbolInput] = useState(''); const [singleSymbolLoading, setSingleSymbolLoading] = useState(false); ``` ### 新增图标导入 ```javascript import { DatabaseOutlined, KeyOutlined, SettingOutlined, SaveOutlined, ToolOutlined, RobotOutlined, EditOutlined, CloudDownloadOutlined, ClearOutlined, ThunderboltOutlined, FileTextOutlined, CodeOutlined } from '@ant-design/icons'; ``` ### 核心功能函数 #### 1. 浏览器本地缓存 - 一键获取数据 ```javascript const fetchAllDataForCache = async () => { setCacheLoading(true); const API_BASE_URL = 'http://localhost:3007/api'; const cacheResults = []; try { // 1. 获取市场概览数据 setCacheProgress({ current: 1, total: 4, name: '市场概览数据' }); const overviewResponse = await fetch(`${API_BASE_URL}/market/overview`); if (overviewResponse.ok) { const overviewData = await overviewResponse.json(); localStorage.setItem('cached_overview', JSON.stringify({ data: overviewData.data, timestamp: Date.now(), expiresAt: Date.now() + 5 * 60 * 1000 // 5分钟过期 })); cacheResults.push({ name: '市场概览', status: 'success' }); } // 2. 获取风险预警数据 setCacheProgress({ current: 2, total: 4, name: '风险预警数据' }); // ... 类似处理 // 3. 获取热门品种详情(前5个) setCacheProgress({ current: 3, total: 4, name: '热门品种详情' }); // ... 处理详情缓存 // 4. 获取K线数据(主要周期) setCacheProgress({ current: 4, total: 4, name: 'K线数据' }); // ... 处理K线缓存 // 更新缓存统计 updateCacheStats(); messageApi.success(`数据缓存完成!`); } catch (error) { messageApi.error('缓存数据失败: ' + error.message); } finally { setCacheLoading(false); } }; ``` **缓存数据结构:** ```javascript { data: {...}, // 实际数据 timestamp: 1234567890, // 缓存时间戳 expiresAt: 1234567890 // 过期时间戳 } ``` **缓存内容:** - 市场概览数据(`cached_overview`):5分钟过期 - 风险预警数据(`cached_alerts`):3分钟过期 - 品种详情数据(`cached_detail_{code}`):10分钟过期 - K线数据(`cached_kline_{code}_{period}`):30分钟过期 #### 2. 数据库缓存 - 一键缓存所有合约 ```javascript const cacheAllContractsToDB = async () => { setDbCacheLoading(true); const API_BASE_URL = 'http://localhost:3007/api'; try { messageApi.info('开始批量缓存所有合约到数据库...'); // 调用后端批量缓存API const response = await fetch(`${API_BASE_URL}/market/cache-all`, { method: 'POST', headers: { 'Content-Type': 'application/json' } }); const result = await response.json(); if (result.success) { const { success, total, klineCached } = result.data; messageApi.success(`数据库缓存完成!成功: ${success}/${total},K线: ${klineCached}条`); } } catch (error) { messageApi.error('缓存到数据库失败: ' + error.message); } finally { setDbCacheLoading(false); } }; ``` #### 3. 数据库缓存 - 缓存指定合约 ```javascript const cacheSymbolToDB = async () => { if (!symbolInput.trim()) { messageApi.warning('请输入合约代码'); return; } setSingleSymbolLoading(true); const API_BASE_URL = 'http://localhost:3007/api'; const symbol = symbolInput.trim().toUpperCase(); try { // 调用后端单合约缓存API(强制刷新) const response = await fetch(`${API_BASE_URL}/market/cache/${symbol}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ periods: ['1H', '4H', '1D'] }) }); const result = await response.json(); if (result.success) { const klineSuccess = result.data.klines.filter((k) => k.success).length; messageApi.success(`合约 ${symbol} 数据缓存完成!详情: ✓,K线: ${klineSuccess}/${result.data.klines.length}`); setSymbolInput(''); } } catch (error) { messageApi.error('缓存合约到数据库失败: ' + error.message); } finally { setSingleSymbolLoading(false); } }; ``` ### UI 布局 数据缓存页签(`key: 'cache'`)包含以下卡片: 1. **缓存统计卡片** - 有效缓存数(绿色) - 过期缓存数(橙色) - 缓存总数(蓝色) - 上次更新时间(紫色) 2. **一键获取数据并缓存卡片** - 主要按钮:"一键获取数据" - 危险按钮:"清除所有缓存" - 进度条显示 - 缓存内容说明 3. **一键缓存所有合约到数据库卡片** - 警告类型说明 Alert - 危险主按钮:"缓存所有合约到数据库" - 进度条显示 - 数据存储位置说明 4. **缓存指定合约到数据库卡片** - 输入框:合约代码(带快捷选择按钮) - 主按钮:"缓存指定合约" - 常用合约代码快捷按钮(AU、CU、RB等) - 缓存内容说明 5. **缓存管理卡片** - 缓存存储位置说明 - 缓存策略说明 --- ## 后端实现 ### 文件位置 - `backend/src/api/market.ts` - API 路由 - `backend/src/services/cacheService.ts` - 缓存服务 ### CacheService 新增方法 #### 1. 直接保存数据到缓存 ```typescript /** * 直接保存数据到缓存(用于批量预热缓存) */ async saveDirect(symbol: string, type: string, data: any, options: CacheOptions = {}): Promise { try { const key = `market:${type}:${symbol}`; // 保存到Redis await this.set(key, data, options); // 保存到MySQL await this.saveToMySQL(symbol, type, data); logger.log(`直接保存缓存成功: ${symbol}, ${type}`); } catch (error) { logger.error(`直接保存缓存失败: ${symbol}, ${type}`, error); throw error; } } ``` #### 2. 获取数据库缓存统计 ```typescript /** * 获取数据库缓存统计 */ async getDBStats(): Promise<{ total: number; byType: Record }> { try { const [results]: any = await mysqlConnection.query( 'SELECT type, COUNT(*) as count FROM market_data GROUP BY type' ); const byType: Record = {}; let total = 0; results.forEach((row: any) => { byType[row.type] = row.count; total += row.count; }); return { total, byType }; } catch (error) { logger.error('获取数据库缓存统计失败:', error); return { total: 0, byType: {} }; } } ``` ### 新增 API 接口 #### 1. 批量缓存所有合约 ```typescript // POST /api/market/cache-all router.post('/cache-all', async (req, res) => { try { logger.info('start 批量缓存所有合约到数据库'); // 1. 获取所有合约列表 const overview = await fetchMarketOverview(); const contracts = overview || []; const results = { total: contracts.length, success: 0, failed: 0, details: [] as { code: string; status: string; error?: string }[] }; // 2. 批量获取并缓存每个合约的详情 for (let i = 0; i < contracts.length; i++) { const contract = contracts[i]; try { await fetchMarketDetail(contract.code); results.success++; results.details.push({ code: contract.code, status: 'success' }); } catch (error: any) { results.failed++; results.details.push({ code: contract.code, status: 'error', error: error.message }); } // 每10个合约延迟100ms,避免请求过快 if ((i + 1) % 10 === 0) { await new Promise(resolve => setTimeout(resolve, 100)); } } // 3. 缓存热门合约的K线数据(前10个) const topContracts = contracts.slice(0, 10); const periods = ['1H', '1D']; let klineCached = 0; for (const contract of topContracts) { for (const period of periods) { try { await fetchKlineData(contract.code, period); klineCached++; } catch (error) { logger.error(`缓存K线数据失败: ${contract.code} ${period}`); } } } res.status(200).json({ success: true, message: `批量缓存完成,成功: ${results.success}/${results.total}`, data: { ...results, klineCached } }); } catch (error) { logger.error('批量缓存所有合约失败:', error); res.status(500).json({ success: false, message: '批量缓存失败' }); } }); ``` #### 2. 缓存指定合约(强制刷新) ```typescript // POST /api/market/cache/:symbol router.post('/cache/:symbol', async (req, res) => { try { const { symbol } = req.params; const { periods = ['1H', '4H', '1D'] } = req.body; logger.info(`start 缓存合约 ${symbol} 到数据库`); // 1. 清除现有缓存(强制刷新) await cacheService.clearByType(symbol, 'detail'); // 2. 重新获取并缓存合约详情 const detail = await fetchMarketDetail(symbol); // 3. 缓存K线数据 const klineResults = []; for (const period of periods) { try { await cacheService.clearByType(symbol, `kline:${period}`); const kline = await fetchKlineData(symbol, period); klineResults.push({ period, success: true }); } catch (error: any) { klineResults.push({ period, success: false, error: error.message }); } } res.status(200).json({ success: true, message: `合约 ${symbol} 缓存成功`, data: { symbol, detail: !!detail, klines: klineResults } }); } catch (error: any) { logger.error(`缓存合约 ${req.params.symbol} 失败:`, error); res.status(500).json({ success: false, message: error.message || '缓存失败' }); } }); ``` #### 3. 获取缓存统计 ```typescript // GET /api/market/cache-stats router.get('/cache-stats', async (req, res) => { try { const stats = await cacheService.getDBStats(); res.status(200).json({ success: true, data: stats }); } catch (error) { logger.error('获取缓存统计失败:', error); res.status(500).json({ success: false, message: '获取缓存统计失败' }); } }); ``` --- ## 数据存储架构 ### 浏览器本地缓存(LocalStorage) ``` ┌─────────────────────────────────────────────────────────┐ │ Browser LocalStorage │ ├─────────────────────────────────────────────────────────┤ │ cached_overview │ {data, timestamp, expiresAt} │ │ cached_alerts │ {data, timestamp, expiresAt} │ │ cached_detail_AU │ {data, timestamp, expiresAt} │ │ cached_kline_AU_1D │ {data, timestamp, expiresAt} │ │ dataCacheStats │ {total, valid, expired} │ └─────────────────────────────────────────────────────────┘ ``` ### 数据库持久化缓存 ``` ┌─────────────────────────────────────────────────────────┐ │ MySQL - market_data │ ├─────────────────────────────────────────────────────────┤ │ id | symbol | type | data (JSON) | updated_at │ ├───┼─────────┼─────────┼─────────────────┼───────────────┤ │ 1 │ AU │ detail │ {...} │ 2024-01-01... │ │ 2 │ AU │ kline:1D│ {...} │ 2024-01-01... │ │ 3 │ CU │ detail │ {...} │ 2024-01-01... │ └───┴─────────┴─────────┴─────────────────┴───────────────┘ ┌─────────────────────────────────────────────────────────┐ │ Redis Cache │ ├─────────────────────────────────────────────────────────┤ │ Key │ Value │ ├────────────────────────┼───────────────────────────────┤ │ market:detail:AU │ {...} │ │ market:kline:AU:1D │ {...} │ │ market:overview │ {...} │ └────────────────────────┴───────────────────────────────┘ ``` --- ## 缓存策略 ### 三级缓存架构 ``` ┌─────────────────────────────────────────────────────────────┐ │ 数据请求流程 │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ 1. 浏览器 LocalStorage │ │ - 最快,无网络开销 │ │ - 适用于高频访问的数据 │ │ - 需要手动管理过期时间 │ └─────────────────────────────────────────────────────────────┘ │ 未命中 ▼ ┌─────────────────────────────────────────────────────────────┐ │ 2. Redis 缓存 │ │ - 内存级速度 │ │ - 自动过期管理 │ │ - 分布式共享 │ └─────────────────────────────────────────────────────────────┘ │ 未命中 ▼ ┌─────────────────────────────────────────────────────────────┐ │ 3. MySQL 数据库 │ │ - 持久化存储 │ │ - 数据可靠性高 │ │ - 自动同步到 Redis │ └─────────────────────────────────────────────────────────────┘ │ 未命中 ▼ ┌─────────────────────────────────────────────────────────────┐ │ 4. 数据源(TQSDK/ServiceImplementation) │ │ - 实时数据 │ │ - 自动写入 MySQL + Redis │ └─────────────────────────────────────────────────────────────┘ ``` ### 缓存有效期 | 数据类型 | LocalStorage | Redis | MySQL | |---------|-------------|-------|-------| | 市场概览 | 5分钟 | 5分钟 | 永久 | | 风险预警 | 3分钟 | 3分钟 | 永久 | | 品种详情 | 10分钟 | 5分钟 | 永久 | | K线数据 | 30分钟 | 10分钟 | 永久 | --- ## 使用指南 ### 浏览器本地缓存使用 1. 打开 AdminConfig 页面 2. 切换到"数据缓存"页签 3. 点击"一键获取数据"按钮 4. 等待进度完成 5. 查看缓存统计卡片确认缓存状态 ### 数据库缓存使用 #### 批量缓存所有合约 1. 在"数据缓存"页签中找到"一键缓存所有合约到数据库"卡片 2. 点击"缓存所有合约到数据库"按钮(红色按钮) 3. 等待批量处理完成(时间取决于合约数量) 4. 查看成功提示信息 #### 缓存指定合约 1. 在"缓存指定合约到数据库"卡片中 2. 输入合约代码(如 AU、CU、RB)或点击快捷按钮 3. 点击"缓存指定合约"按钮 4. 等待处理完成 --- ## API 列表 ### 前端调用 API | 方法 | 端点 | 说明 | |------|------|------| | GET | `/api/market/overview` | 获取市场概览 | | GET | `/api/market/detail/:symbol` | 获取品种详情 | | GET | `/api/market/klines/:symbol?period=1D` | 获取K线数据 | | POST | `/api/market/cache-all` | 批量缓存所有合约 | | POST | `/api/market/cache/:symbol` | 缓存指定合约 | | GET | `/api/market/cache-stats` | 获取缓存统计 | --- ## 注意事项 1. **性能考虑** - 批量缓存操作可能需要较长时间,请耐心等待 - 后端已实现请求节流(每10个合约延迟100ms) 2. **错误处理** - 单个合约缓存失败不会影响其他合约 - 网络错误会显示详细的错误信息 3. **数据一致性** - 数据库缓存采用"强制刷新"策略 - 会先清除旧缓存再写入新数据 4. **存储限制** - LocalStorage 有 5MB 限制 - 大量K线数据建议使用数据库缓存 --- ## 后续优化建议 1. 添加缓存预热定时任务 2. 实现缓存失效自动重试机制 3. 添加缓存命中率监控 4. 支持批量指定合约缓存 5. 添加缓存导出/导入功能 --- **文档版本**: 1.0 **最后更新**: 2026-03-02 **作者**: AI Assistant