diff --git a/backend/service_implementation/qihuo_analyzer/data/api_adapters/__pycache__/akshare_adapter.cpython-311.pyc b/backend/service_implementation/qihuo_analyzer/data/api_adapters/__pycache__/akshare_adapter.cpython-311.pyc index 29d2617..37c5654 100644 Binary files a/backend/service_implementation/qihuo_analyzer/data/api_adapters/__pycache__/akshare_adapter.cpython-311.pyc and b/backend/service_implementation/qihuo_analyzer/data/api_adapters/__pycache__/akshare_adapter.cpython-311.pyc differ diff --git a/backend/service_implementation/service/data/futures_analysis.db b/backend/service_implementation/service/data/futures_analysis.db index bcfc811..4aa2d72 100644 Binary files a/backend/service_implementation/service/data/futures_analysis.db and b/backend/service_implementation/service/data/futures_analysis.db differ diff --git a/backend/src/api/market.ts b/backend/src/api/market.ts index 56d3de4..52940f4 100644 --- a/backend/src/api/market.ts +++ b/backend/src/api/market.ts @@ -1,5 +1,6 @@ import express from 'express'; import { fetchMarketOverview, fetchMarketDetail, fetchKlineData, fetchMarketHotspots, fetchRiskAlerts } from '../services/marketService'; +import { cacheService } from '../services/cacheService'; import { logger } from '../utils/logger'; const router = express.Router(); @@ -72,4 +73,132 @@ router.get('/alerts', async (req, res) => { } }); +// 批量缓存所有合约数据到数据库 +router.post('/cache-all', async (req, res) => { + try { + logger.info('start 批量缓存所有合约到数据库'); + + // 1. 获取所有合约列表 + const overview = await fetchMarketOverview(); + const contracts = overview || []; + + logger.info(`获取到 ${contracts.length} 个合约,开始批量缓存`); + + 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 { + logger.log(`缓存合约 ${contract.code} (${i + 1}/${contracts.length})...`); + await fetchMarketDetail(contract.code); + results.success++; + results.details.push({ code: contract.code, status: 'success' }); + } catch (error: any) { + logger.error(`缓存合约 ${contract.code} 失败:`, error); + 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}`); + } + } + } + + logger.info('end 批量缓存所有合约到数据库'); + 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: '批量缓存失败' }); + } +}); + +// 缓存指定合约到数据库(强制刷新) +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 { + // 清除K线缓存 + await cacheService.clearByType(symbol, `kline:${period}`); + // 重新获取 + const kline = await fetchKlineData(symbol, period); + klineResults.push({ period, success: true }); + } catch (error: any) { + logger.error(`缓存K线数据失败: ${symbol} ${period}`, error); + klineResults.push({ period, success: false, error: error.message }); + } + } + + logger.info(`end 缓存合约 ${symbol} 到数据库`); + 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 || '缓存失败' }); + } +}); + +// 获取数据库缓存统计 +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: '获取缓存统计失败' }); + } +}); + export default router; \ No newline at end of file diff --git a/backend/src/services/cacheService.ts b/backend/src/services/cacheService.ts index 7ebf0f0..57210f8 100644 --- a/backend/src/services/cacheService.ts +++ b/backend/src/services/cacheService.ts @@ -122,6 +122,47 @@ class CacheService { logger.error(`清除缓存失败: ${symbol}, ${type}`, error); } } + + /** + * 直接保存数据到缓存(用于批量预热缓存) + */ + 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; + } + } + + /** + * 获取数据库缓存统计 + */ + 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: {} }; + } + } } export const cacheService = new CacheService(); diff --git a/docs/AG0_kline_data.db b/docs/AG0_kline_data.db new file mode 100644 index 0000000..0bbab9b Binary files /dev/null and b/docs/AG0_kline_data.db differ diff --git a/docs/CHEATSHEET.md b/docs/CHEATSHEET.md new file mode 100644 index 0000000..8968e59 --- /dev/null +++ b/docs/CHEATSHEET.md @@ -0,0 +1,197 @@ +# 数据缓存功能 - 快速参考卡 + +## 📋 文件变更清单 + +### 前端文件 +- `src/pages/admin/AdminConfig.jsx` - 主要修改文件 + +### 后端文件 +- `backend/src/api/market.ts` - 新增3个API +- `backend/src/services/cacheService.ts` - 新增2个方法 + +--- + +## 🚀 快速启动 + +### 编译后端 +```bash +cd backend && npm run build +``` + +### 验证API +```bash +# 批量缓存 +curl -X POST http://localhost:3007/api/market/cache-all + +# 单合约缓存 +curl -X POST http://localhost:3007/api/market/cache/AU + +# 缓存统计 +curl http://localhost:3007/api/market/cache-stats +``` + +--- + +## 📡 API 速查 + +| 操作 | 方法 | 端点 | 说明 | +|------|------|------|------| +| 批量缓存 | POST | `/market/cache-all` | 缓存所有合约 | +| 单合约缓存 | POST | `/market/cache/:symbol` | 强制刷新指定合约 | +| 缓存统计 | GET | `/market/cache-stats` | 获取数据库统计 | +| 市场概览 | GET | `/market/overview` | 获取合约列表 | +| 合约详情 | GET | `/market/detail/:symbol` | 获取单个合约详情 | +| K线数据 | GET | `/market/klines/:symbol?period=1D` | 获取K线数据 | + +--- + +## 🗄️ 缓存键命名规范 + +### LocalStorage +``` +cached_overview - 市场概览 +cached_alerts - 风险预警 +cached_detail_{code} - 合约详情 +cached_kline_{code}_{period} - K线数据 +``` + +### Redis +``` +market:overview - 市场概览 +market:detail:{symbol} - 合约详情 +market:kline:{symbol}:{period} - K线数据 +``` + +### MySQL +```sql +-- market_data 表 +symbol | type | data +-------|-------------|-------- +AU | detail | {...} +AU | kline:1D | {...} +CU | detail | {...} +``` + +--- + +## ⏱️ 缓存有效期 + +| 数据类型 | LocalStorage | Redis | MySQL | +|---------|-------------|-------|-------| +| 市场概览 | 5分钟 | 5分钟 | 永久 | +| 风险预警 | 3分钟 | 3分钟 | 永久 | +| 合约详情 | 10分钟 | 5分钟 | 永久 | +| K线数据 | 30分钟 | 10分钟 | 永久 | + +--- + +## 🔧 常用操作 + +### 查看 MySQL 缓存 +```sql +-- 查看所有缓存 +SELECT symbol, type, updated_at FROM market_data ORDER BY updated_at DESC; + +-- 按类型统计 +SELECT type, COUNT(*) FROM market_data GROUP BY type; + +-- 查看特定合约 +SELECT * FROM market_data WHERE symbol = 'AU'; +``` + +### 查看 Redis 缓存 +```bash +# 查看所有 key +KEYS market:* + +# 查看过期时间 +TTL market:detail:AU + +# 删除特定 key +DEL market:detail:AU + +# 清空所有 +FLUSHDB +``` + +### 清除 LocalStorage +```javascript +// 清除所有缓存 +Object.keys(localStorage).forEach(key => { + if (key.startsWith('cached_')) { + localStorage.removeItem(key); + } +}); +``` + +--- + +## 🐛 快速排错 + +| 问题 | 解决方案 | +|------|---------| +| 页面加载失败 | 清除 vite 缓存 `rm -rf node_modules/.vite` | +| API 返回 500 | 检查数据源配置和数据源服务状态 | +| 缓存未写入 DB | 检查 MySQL 连接和 market_data 表 | +| Redis 未命中 | 检查 Redis 连接 `redis-cli ping` | +| 批量缓存超时 | 查看后端日志,减少并发或分批处理 | + +--- + +## 📊 监控命令 + +```bash +# 实时监控后端日志 +tail -f backend/logs/app.log | grep -E "cache|Cache|缓存" + +# 监控 MySQL 查询 +mysql -u root -p -e "SELECT COUNT(*), type FROM market_data GROUP BY type;" + +# 监控 Redis 内存 +redis-cli INFO memory | grep used_memory_human + +# 查看当前连接的合约数 +curl -s http://localhost:3007/api/market/overview | jq '.data | length' +``` + +--- + +## 📝 代码片段 + +### 前端调用批量缓存 +```javascript +const cacheAll = async () => { + const res = await fetch('/api/market/cache-all', { method: 'POST' }); + const data = await res.json(); + console.log(`成功: ${data.data.success}/${data.data.total}`); +}; +``` + +### 前端调用单合约缓存 +```javascript +const cacheSymbol = async (symbol) => { + const res = await fetch(`/api/market/cache/${symbol}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ periods: ['1H', '4H', '1D'] }) + }); + return await res.json(); +}; +``` + +### 后端直接保存缓存 +```typescript +await cacheService.saveDirect('AU', 'detail', data, { expireTime: 300 }); +``` + +--- + +## 🔗 相关文档 + +- 完整开发文档: `docs/data-cache-feature.md` +- API 参考文档: `docs/api-reference.md` +- 部署指南: `docs/cache-deployment-guide.md` + +--- + +**最后更新**: 2026-03-02 diff --git a/docs/api-reference.md b/docs/api-reference.md new file mode 100644 index 0000000..97387cc --- /dev/null +++ b/docs/api-reference.md @@ -0,0 +1,375 @@ +# API 接口文档 - 数据缓存模块 + +## 基础信息 + +- **Base URL**: `http://localhost:3007/api` +- **Content-Type**: `application/json` + +--- + +## 数据缓存接口 + +### 1. 批量缓存所有合约 + +批量获取所有合约的详情和K线数据,并持久化存储到数据库。 + +```http +POST /market/cache-all +``` + +**请求参数**: 无 + +**响应示例**: +```json +{ + "success": true, + "message": "批量缓存完成,成功: 25/30", + "data": { + "total": 30, + "success": 25, + "failed": 5, + "details": [ + { "code": "AU", "status": "success" }, + { "code": "CU", "status": "success" }, + { "code": "XX", "status": "error", "error": "合约不存在" } + ], + "klineCached": 50 + } +} +``` + +**说明**: +- 自动获取市场概览中的所有合约 +- 逐个缓存合约详情到 MySQL 和 Redis +- 缓存前10个热门合约的1H和1D周期K线数据 +- 每10个合约延迟100ms避免请求过快 + +--- + +### 2. 缓存指定合约 + +针对单个合约进行强制刷新缓存。 + +```http +POST /market/cache/:symbol +``` + +**路径参数**: +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| symbol | string | 是 | 合约代码,如 AU、CU、RB | + +**请求体**: +```json +{ + "periods": ["1H", "4H", "1D"] +} +``` + +**请求体参数**: +| 参数 | 类型 | 必填 | 默认值 | 说明 | +|------|------|------|--------|------| +| periods | string[] | 否 | ["1H", "4H", "1D"] | 要缓存的K线周期 | + +**响应示例**: +```json +{ + "success": true, + "message": "合约 AU 缓存成功", + "data": { + "symbol": "AU", + "detail": true, + "klines": [ + { "period": "1H", "success": true }, + { "period": "4H", "success": true }, + { "period": "1D", "success": false, "error": "数据获取失败" } + ] + } +} +``` + +**说明**: +- 会先清除该合约的现有缓存(强制刷新) +- 缓存合约详情和指定周期的K线数据 +- 各周期K线缓存相互独立,单个失败不影响其他 + +--- + +### 3. 获取缓存统计 + +获取数据库中的缓存统计信息。 + +```http +GET /market/cache-stats +``` + +**响应示例**: +```json +{ + "success": true, + "data": { + "total": 150, + "byType": { + "detail": 50, + "kline:1H": 30, + "kline:1D": 30, + "kline:4H": 20, + "overview": 1, + "alerts": 1 + } + } +} +``` + +--- + +### 4. 获取市场概览 + +获取所有期货合约的市场概览数据。 + +```http +GET /market/overview +``` + +**响应示例**: +```json +{ + "success": true, + "data": [ + { + "code": "AU", + "name": "黄金", + "currentPrice": 485.32, + "changePercent": 1.25, + "winRate": 65, + "atr": 2.34, + "adx": 35, + "trends": { + "5MIN": { "direction": "看多", "status": "多头趋势", "rsi": 58 }, + "1DAY": { "direction": "看多", "status": "多头趋势", "rsi": 62 } + } + } + ] +} +``` + +**缓存策略**: +- Redis: 5分钟 +- MySQL: 永久 + +--- + +### 5. 获取品种详情 + +获取指定合约的详细信息。 + +```http +GET /market/detail/:symbol +``` + +**路径参数**: +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| symbol | string | 是 | 合约代码 | + +**响应示例**: +```json +{ + "success": true, + "data": { + "code": "AU", + "name": "黄金", + "currentPrice": 485.32, + "changePercent": 1.25, + "winRate": 65, + "indicators": { + "macd": "金叉向上", + "rsi": "58(中性)" + }, + "tradingAdvice": { + "entry": 485.0, + "stopLoss": 475.0, + "target": 500.0 + } + } +} +``` + +**缓存策略**: +- Redis: 5分钟 +- MySQL: 永久 + +--- + +### 6. 获取K线数据 + +获取指定合约的K线数据。 + +```http +GET /market/klines/:symbol?period=1D +``` + +**路径参数**: +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| symbol | string | 是 | 合约代码 | + +**查询参数**: +| 参数 | 类型 | 必填 | 默认值 | 说明 | +|------|------|------|--------|------| +| period | string | 否 | "1H" | K线周期: 1M, 5M, 15M, 30M, 1H, 4H, 1D, 1W | + +**响应示例**: +```json +{ + "success": true, + "data": [ + { + "timestamp": 1704067200, + "open": 480.5, + "high": 485.2, + "low": 479.8, + "close": 485.0, + "volume": 15234 + } + ] +} +``` + +**缓存策略**: +- Redis: 10分钟 +- MySQL: 永久 + +--- + +## 错误处理 + +### 错误响应格式 + +```json +{ + "success": false, + "message": "错误描述信息" +} +``` + +### 常见错误码 + +| HTTP 状态码 | 说明 | 场景 | +|------------|------|------| +| 200 | 成功 | 请求处理成功 | +| 500 | 服务器内部错误 | 数据源连接失败、数据库错误 | +| 404 | 未找到 | 合约不存在 | + +### 常见错误消息 + +| 错误消息 | 说明 | 解决方案 | +|---------|------|---------| +| 无可用数据源 | 未配置或启用数据源 | 在 AdminConfig 中启用至少一个数据源 | +| 合约不存在 | 指定的合约代码无效 | 检查合约代码是否正确 | +| 获取市场概览失败 | 数据源连接问题 | 检查数据源配置和网络连接 | +| 批量缓存失败 | 批量处理过程中出错 | 查看服务器日志获取详细错误 | + +--- + +## 调用示例 + +### JavaScript/Fetch + +```javascript +// 批量缓存所有合约 +const cacheAllContracts = async () => { + const response = await fetch('http://localhost:3007/api/market/cache-all', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }); + return await response.json(); +}; + +// 缓存指定合约 +const cacheSymbol = async (symbol) => { + const response = await fetch(`http://localhost:3007/api/market/cache/${symbol}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + periods: ['1H', '4H', '1D'] + }) + }); + return await response.json(); +}; + +// 获取缓存统计 +const getCacheStats = async () => { + const response = await fetch('http://localhost:3007/api/market/cache-stats'); + return await response.json(); +}; +``` + +--- + +## 数据类型定义 + +### ContractDetail + +```typescript +interface ContractDetail { + code: string; // 合约代码 + name: string; // 合约名称 + currentPrice: number; // 当前价格 + changePercent: number; // 涨跌幅百分比 + winRate: number; // 胜率 + atr: number; // 平均真实波幅 + adx: number; // ADX指标值 + trends: { // 各周期趋势 + [period: string]: { + direction: string; // 方向:看多/看空/观望 + status: string; // 状态:多头趋势/空头趋势/震荡 + rsi: number; // RSI值 + } + }; + indicators: { // 技术指标 + macd: string; + rsi: string; + bollinger: string; + kdj: string; + }; + tradingAdvice: { // 交易建议 + entry: number; // 入场价 + stopLoss: number; // 止损价 + target: number; // 目标价 + resistance: number; // 阻力位 + support: number; // 支撑位 + }; +} +``` + +### KlineData + +```typescript +interface KlineData { + timestamp: number; // 时间戳(秒) + open: number; // 开盘价 + high: number; // 最高价 + low: number; // 最低价 + close: number; // 收盘价 + volume: number; // 成交量 +} +``` + +### CacheStats + +```typescript +interface CacheStats { + total: number; // 缓存总数 + byType: Record; // 按类型统计 +} +``` + +--- + +**文档版本**: 1.0 +**最后更新**: 2026-03-02 diff --git a/docs/cache-deployment-guide.md b/docs/cache-deployment-guide.md new file mode 100644 index 0000000..5f44eb5 --- /dev/null +++ b/docs/cache-deployment-guide.md @@ -0,0 +1,371 @@ +# 数据缓存功能部署与操作指南 + +## 一、部署步骤 + +### 1. 后端部署 + +#### 编译 TypeScript + +```bash +cd backend +npm run build +``` + +**预期输出**: +``` +> alpha-futures-backend@1.0.0 build +> tsc +``` + +#### 验证编译结果 + +检查以下文件是否生成: +- `backend/dist/api/market.js` - 包含新增的三个 API 路由 +- `backend/dist/services/cacheService.js` - 包含新增的两个方法 + +```bash +# 检查 API 路由 +grep -n "cache-all\|cache/:symbol\|cache-stats" backend/dist/api/market.js + +# 检查服务方法 +grep -n "saveDirect\|getDBStats" backend/dist/services/cacheService.js +``` + +#### 重启后端服务 + +```bash +# 如果使用 pm2 +pm2 restart alpha-futures-backend + +# 或者手动启动 +npm start +``` + +### 2. 前端部署 + +前端代码无需编译,直接生效。刷新浏览器页面即可。 + +```bash +# 如果是生产环境构建 +npm run build +``` + +--- + +## 二、功能验证 + +### 1. API 接口测试 + +使用 curl 或 Postman 测试新增接口: + +#### 测试批量缓存接口 + +```bash +curl -X POST http://localhost:3007/api/market/cache-all +``` + +**预期响应**: +```json +{ + "success": true, + "message": "批量缓存完成,成功: 25/30", + "data": { + "total": 30, + "success": 25, + "failed": 5, + "klineCached": 50 + } +} +``` + +#### 测试单合约缓存接口 + +```bash +curl -X POST http://localhost:3007/api/market/cache/AU \ + -H "Content-Type: application/json" \ + -d '{"periods": ["1H", "1D"]}' +``` + +**预期响应**: +```json +{ + "success": true, + "message": "合约 AU 缓存成功", + "data": { + "symbol": "AU", + "detail": true, + "klines": [ + { "period": "1H", "success": true }, + { "period": "1D", "success": true } + ] + } +} +``` + +#### 测试缓存统计接口 + +```bash +curl http://localhost:3007/api/market/cache-stats +``` + +**预期响应**: +```json +{ + "success": true, + "data": { + "total": 150, + "byType": { + "detail": 50, + "kline:1H": 30, + "kline:1D": 30 + } + } +} +``` + +### 2. 前端页面验证 + +1. 打开浏览器访问 `http://localhost:5173`(开发环境) +2. 进入 AdminConfig 页面 +3. 切换到"数据缓存"页签 +4. 验证以下元素是否存在: + - [ ] 缓存统计卡片(4个统计项) + - [ ] "一键获取数据"按钮 + - [ ] "缓存所有合约到数据库"按钮 + - [ ] 合约代码输入框和"缓存指定合约"按钮 + - [ ] 常用合约代码快捷按钮 + +--- + +## 三、数据库检查 + +### MySQL 数据验证 + +```sql +-- 查看缓存的数据 +SELECT symbol, type, updated_at +FROM market_data +ORDER BY updated_at DESC +LIMIT 20; + +-- 按类型统计 +SELECT type, COUNT(*) as count +FROM market_data +GROUP BY type; + +-- 查看特定合约的缓存 +SELECT * FROM market_data +WHERE symbol = 'AU'; +``` + +### Redis 数据验证 + +```bash +# 连接到 Redis +redis-cli + +# 查看所有市场数据相关的 key +KEYS market:* + +# 查看特定 key 的过期时间 +TTL market:detail:AU + +# 获取特定 key 的数据 +GET market:detail:AU +``` + +--- + +## 四、常见问题排查 + +### 问题1: 批量缓存请求超时 + +**现象**: 点击"缓存所有合约到数据库"后长时间无响应 + +**原因**: 合约数量过多,请求处理时间过长 + +**解决方案**: +1. 检查后端日志查看进度 +2. 分批处理(先缓存部分热门合约) +3. 增加超时时间配置 + +### 问题2: 数据库缓存未生效 + +**现象**: API 返回成功,但数据库中无数据 + +**排查步骤**: +1. 检查 MySQL 连接是否正常 +```bash +# 查看后端日志 +tail -f backend/logs/app.log | grep -i "mysql\|save" +``` + +2. 检查 market_data 表是否存在 +```sql +SHOW TABLES LIKE 'market_data'; +``` + +3. 检查表结构 +```sql +DESCRIBE market_data; +``` + +### 问题3: 前端页面报错 + +**现象**: 打开 AdminConfig 页面显示空白或报错 + +**排查步骤**: +1. 清除 Vite 缓存 +```bash +rm -rf node_modules/.vite +``` + +2. 检查浏览器控制台错误信息 + +3. 检查是否存在语法错误 +```bash +# 在 backend 目录编译时检查 +npm run build +``` + +### 问题4: Redis 缓存未命中 + +**现象**: 数据只存入 MySQL,Redis 中没有 + +**排查步骤**: +1. 检查 Redis 连接 +```bash +redis-cli ping +# 应返回 PONG +``` + +2. 查看后端日志 +```bash +tail -f backend/logs/app.log | grep -i "redis\|cache" +``` + +3. 检查 Redis 配置 +```bash +# 查看 backend/src/config/database/redis.ts +cat backend/src/config/database/redis.ts +``` + +--- + +## 五、性能优化建议 + +### 1. 批量缓存优化 + +当前实现:串行处理合约 +```typescript +for (const contract of contracts) { + await fetchMarketDetail(contract.code); +} +``` + +优化方案:并行处理(限制并发数) +```typescript +const batchSize = 5; +for (let i = 0; i < contracts.length; i += batchSize) { + const batch = contracts.slice(i, i + batchSize); + await Promise.all(batch.map(c => fetchMarketDetail(c.code))); + await delay(100); +} +``` + +### 2. 缓存预热定时任务 + +建议添加定时任务,在非交易时间自动预热缓存: + +```typescript +// 每天早上 8:30 预热缓存 +import { CronJob } from 'cron'; + +const cacheWarmupJob = new CronJob('30 8 * * *', async () => { + console.log('开始缓存预热...'); + await warmupCache(); +}); + +cacheWarmupJob.start(); +``` + +### 3. 缓存监控 + +添加缓存命中率监控: + +```typescript +// 在 cacheService 中添加统计 +class CacheService { + private stats = { hits: 0, misses: 0 }; + + async get(...) { + // Redis 命中 + if (redisData) { + this.stats.hits++; + return JSON.parse(redisData); + } + + // MySQL 命中 + if (mysqlData) { + this.stats.hits++; + return mysqlData; + } + + // 未命中 + this.stats.misses++; + return fetchFromSource(); + } + + getHitRate() { + const total = this.stats.hits + this.stats.misses; + return total === 0 ? 0 : (this.stats.hits / total * 100).toFixed(2); + } +} +``` + +--- + +## 六、安全注意事项 + +1. **API 限流**: 批量缓存 API 可能会产生大量请求,建议添加限流 +2. **权限控制**: 确保只有管理员可以访问缓存管理功能 +3. **数据清理**: 定期清理过期的 Redis 缓存,避免内存溢出 +4. **错误处理**: 批量操作时单个失败不应影响整体流程 + +--- + +## 七、回滚方案 + +如果需要回滚到之前的版本: + +### 1. 后端回滚 + +```bash +cd backend +git checkout HEAD -- src/api/market.ts src/services/cacheService.ts +npm run build +pm2 restart alpha-futures-backend +``` + +### 2. 前端回滚 + +```bash +git checkout HEAD -- src/pages/admin/AdminConfig.jsx +``` + +### 3. 数据库清理(可选) + +```sql +-- 清除所有缓存数据 +TRUNCATE TABLE market_data; +``` + +```bash +# 清除 Redis 缓存 +redis-cli FLUSHDB +``` + +--- + +**文档版本**: 1.0 +**最后更新**: 2026-03-02 diff --git a/docs/data-cache-feature.md b/docs/data-cache-feature.md new file mode 100644 index 0000000..8433112 --- /dev/null +++ b/docs/data-cache-feature.md @@ -0,0 +1,578 @@ +# 数据缓存功能开发文档 + +## 概述 + +本文档描述了在 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 diff --git a/src/pages/admin/AdminConfig.jsx b/src/pages/admin/AdminConfig.jsx index ec88f0d..c24b9fc 100644 --- a/src/pages/admin/AdminConfig.jsx +++ b/src/pages/admin/AdminConfig.jsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from 'react'; import { Card, Row, Col, Form, Input, Button, Select, Switch, InputNumber, Alert, Divider, Tabs, Table, Modal } from 'antd'; import { message } from 'antd'; -import { DatabaseOutlined, KeyOutlined, SettingOutlined, SaveOutlined, ToolOutlined, RobotOutlined, EditOutlined } from '@ant-design/icons'; +import { DatabaseOutlined, KeyOutlined, SettingOutlined, SaveOutlined, ToolOutlined, RobotOutlined, EditOutlined, CloudDownloadOutlined, ClearOutlined, ThunderboltOutlined, FileTextOutlined, CodeOutlined } from '@ant-design/icons'; import './AdminConfig.css'; const { Option } = Select; @@ -14,12 +14,269 @@ const AdminConfig = () => { const [aiModelModalVisible, setAiModelModalVisible] = useState(false); const [currentAiModel, setCurrentAiModel] = useState(null); const [aiModelForm] = Form.useForm(); + 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); - // 组件挂载时获取配置 + // 组件挂载时获取配置和缓存统计 useEffect(() => { fetchConfig(); + updateCacheStats(); }, []); + // 更新缓存统计 + const updateCacheStats = () => { + const stats = JSON.parse(localStorage.getItem('dataCacheStats') || '{"total":0,"valid":0,"expired":0}'); + setCacheStats(stats); + }; + + // 一键获取数据并缓存 + const fetchAllDataForCache = async () => { + setCacheLoading(true); + const API_BASE_URL = 'http://localhost:3007/api'; + const cacheResults = []; + + try { + messageApi.info('开始获取数据并缓存...'); + + // 1. 获取市场概览数据 + setCacheProgress({ current: 1, total: 4, name: '市场概览数据' }); + try { + 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' }); + } + } catch (error) { + console.error('缓存市场概览失败:', error); + cacheResults.push({ name: '市场概览', status: 'error', error: error.message }); + } + + // 2. 获取风险预警数据 + setCacheProgress({ current: 2, total: 4, name: '风险预警数据' }); + try { + const alertsResponse = await fetch(`${API_BASE_URL}/market/alerts`); + if (alertsResponse.ok) { + const alertsData = await alertsResponse.json(); + localStorage.setItem('cached_alerts', JSON.stringify({ + data: alertsData.data, + timestamp: Date.now(), + expiresAt: Date.now() + 3 * 60 * 1000 // 3分钟过期 + })); + cacheResults.push({ name: '风险预警', status: 'success' }); + } + } catch (error) { + console.error('缓存风险预警失败:', error); + cacheResults.push({ name: '风险预警', status: 'error', error: error.message }); + } + + // 3. 获取热门品种详情(取前5个) + setCacheProgress({ current: 3, total: 4, name: '热门品种详情' }); + try { + const overviewItem = localStorage.getItem('cached_overview'); + if (overviewItem) { + const overview = JSON.parse(overviewItem); + const hotFutures = overview.data?.slice(0, 5) || []; + const detailPromises = hotFutures.map(async (item) => { + try { + const detailResponse = await fetch(`${API_BASE_URL}/market/detail/${item.code}`); + if (detailResponse.ok) { + const detailData = await detailResponse.json(); + localStorage.setItem(`cached_detail_${item.code}`, JSON.stringify({ + data: detailData.data, + timestamp: Date.now(), + expiresAt: Date.now() + 10 * 60 * 1000 // 10分钟过期 + })); + return { code: item.code, status: 'success' }; + } + } catch (e) { + return { code: item.code, status: 'error' }; + } + }); + await Promise.all(detailPromises); + cacheResults.push({ name: '热门品种详情', status: 'success', count: hotFutures.length }); + } + } catch (error) { + console.error('缓存品种详情失败:', error); + cacheResults.push({ name: '热门品种详情', status: 'error', error: error.message }); + } + + // 4. 获取K线数据(主要周期) + setCacheProgress({ current: 4, total: 4, name: 'K线数据' }); + try { + const overviewItem = localStorage.getItem('cached_overview'); + if (overviewItem) { + const overview = JSON.parse(overviewItem); + const topFutures = overview.data?.slice(0, 3) || []; + const periods = ['1D', '1H']; + const klinePromises = []; + + topFutures.forEach(item => { + periods.forEach(period => { + klinePromises.push( + fetch(`${API_BASE_URL}/market/klines/${item.code}?period=${period}`) + .then(res => res.ok ? res.json() : null) + .then(data => { + if (data) { + localStorage.setItem(`cached_kline_${item.code}_${period}`, JSON.stringify({ + data: data.data, + timestamp: Date.now(), + expiresAt: Date.now() + 30 * 60 * 1000 // 30分钟过期 + })); + } + }) + .catch(() => {}) + ); + }); + }); + + await Promise.all(klinePromises); + cacheResults.push({ name: 'K线数据', status: 'success', count: topFutures.length * periods.length }); + } + } catch (error) { + console.error('缓存K线数据失败:', error); + cacheResults.push({ name: 'K线数据', status: 'error', error: error.message }); + } + + // 更新缓存统计 + const successCount = cacheResults.filter(r => r.status === 'success').length; + const newStats = { + total: cacheResults.length, + valid: successCount, + expired: cacheResults.length - successCount, + lastUpdate: Date.now() + }; + localStorage.setItem('dataCacheStats', JSON.stringify(newStats)); + setCacheStats(newStats); + + messageApi.success(`数据缓存完成!成功: ${successCount}/${cacheResults.length}`); + } catch (error) { + console.error('缓存数据失败:', error); + messageApi.error('缓存数据失败: ' + error.message); + } finally { + setCacheLoading(false); + setCacheProgress({ current: 0, total: 0, name: '' }); + } + }; + + // 清除所有缓存 + const clearAllCache = () => { + try { + // 清除所有缓存相关的localStorage项 + const keysToRemove = []; + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key && (key.startsWith('cached_') || key === 'dataCacheStats')) { + keysToRemove.push(key); + } + } + keysToRemove.forEach(key => localStorage.removeItem(key)); + + // 更新统计 + const newStats = { total: 0, valid: 0, expired: 0, lastUpdate: null }; + localStorage.setItem('dataCacheStats', JSON.stringify(newStats)); + setCacheStats(newStats); + + messageApi.success('所有缓存已清除'); + } catch (error) { + messageApi.error('清除缓存失败: ' + error.message); + } + }; + + // 一键缓存所有合约数据到数据库 + const cacheAllContractsToDB = async () => { + setDbCacheLoading(true); + const API_BASE_URL = 'http://localhost:3007/api'; + + try { + messageApi.info('开始批量缓存所有合约到数据库...'); + setDbCacheProgress({ current: 1, total: 3, name: '获取合约列表' }); + + // 调用后端批量缓存API + const response = await fetch(`${API_BASE_URL}/market/cache-all`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }); + + if (!response.ok) { + throw new Error('批量缓存请求失败'); + } + + const result = await response.json(); + + if (result.success) { + const { success, total, klineCached } = result.data; + messageApi.success(`数据库缓存完成!成功: ${success}/${total},K线: ${klineCached}条`); + } else { + throw new Error(result.message || '批量缓存失败'); + } + } catch (error) { + console.error('缓存到数据库失败:', error); + messageApi.error('缓存到数据库失败: ' + error.message); + } finally { + setDbCacheLoading(false); + setDbCacheProgress({ current: 0, total: 0, name: '' }); + } + }; + + // 缓存指定合约数据到数据库 + 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 { + messageApi.info(`开始缓存合约 ${symbol} 数据到数据库...`); + + // 调用后端单合约缓存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'] + }) + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.message || '缓存失败'); + } + + 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(''); // 清空输入 + } else { + throw new Error(result.message || '缓存失败'); + } + } catch (error) { + console.error('缓存合约到数据库失败:', error); + messageApi.error('缓存合约到数据库失败: ' + error.message); + } finally { + setSingleSymbolLoading(false); + } + }; + // 数据库配置 const databaseConfig = { // MySQL配置 @@ -1600,6 +1857,251 @@ const AdminConfig = () => { ) + }, + { + label: 数据缓存, + key: 'cache', + children: ( + <> + {/* 缓存统计 */} + + + +
+
{cacheStats.valid}
+
有效缓存
+
+ + +
+
{cacheStats.expired}
+
过期缓存
+
+ + +
+
{cacheStats.total}
+
缓存总数
+
+ + +
+
+ {cacheStats.lastUpdate ? new Date(cacheStats.lastUpdate).toLocaleString() : '从未'} +
+
上次更新
+
+ +
+
+ + {/* 一键缓存 */} + + + + {cacheProgress.total > 0 && ( +
+
+ + 正在缓存: {cacheProgress.name} ({cacheProgress.current}/{cacheProgress.total}) +
+
+
+
+
+ )} + + + + + + + + + + +
+

缓存内容包括:

+
    +
  • 📊 市场概览数据(有效期:5分钟)
  • +
  • ⚠️ 风险预警数据(有效期:3分钟)
  • +
  • 📈 热门品种详情(前5个品种,有效期:10分钟)
  • +
  • 📉 K线数据(主要品种,有效期:30分钟)
  • +
+
+ + + {/* 一键缓存所有合约到数据库 */} + + + + {dbCacheProgress.total > 0 && ( +
+
+ + 正在处理: {dbCacheProgress.name} ({dbCacheProgress.current}/{dbCacheProgress.total}) +
+
+
+
+
+ )} + + + + + + + + + +
+

数据存储位置:

+
    +
  • 🗄️ MySQL数据库 - 持久化存储所有合约数据
  • +
  • ⚡ Redis缓存 - 高速缓存热点数据
  • +
  • 📊 合约详情数据(market_data表)
  • +
  • 📈 K线数据(多个周期)
  • +
+

+ + 注意:此操作可能需要较长时间,取决于合约数量和网络状况。 +

+
+ + + {/* 缓存指定合约到数据库 */} + + + + + + setSymbolInput(e.target.value)} + onPressEnter={cacheSymbolToDB} + size="large" + prefix={} + style={{ textTransform: 'uppercase' }} + /> + + + + + + + + +
+

常用合约代码示例:

+ + {['AU', 'CU', 'RB', 'AG', 'ZN', 'NI', 'AL', 'PB', 'SN', 'HC'].map(code => ( + + + + ))} + +

+ + 缓存内容包括:合约详情、1小时K线、4小时K线、日线数据 +

+
+
+ + {/* 缓存管理 */} + + + + +

数据缓存存储在浏览器的 localStorage

+

缓存键前缀: cached_

+
+ + + +

• 数据在有效期内优先使用缓存

+

• 过期数据会自动重新获取

+

• 支持强制刷新获取最新数据

+
+ +
+
+ + ) } ]} />