You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

579 lines
20 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 数据缓存功能开发文档
## 概述
本文档描述了在 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<void> {
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<string, number> }> {
try {
const [results]: any = await mysqlConnection.query(
'SELECT type, COUNT(*) as count FROM market_data GROUP BY type'
);
const byType: Record<string, number> = {};
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