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

# 数据缓存功能开发文档
## 概述
本文档描述了在 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