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.
20 KiB
20 KiB
数据缓存功能开发文档
概述
本文档描述了在 AdminConfig 管理配置页面中新增的数据缓存功能,包括浏览器本地缓存和数据库持久化缓存两大部分。
功能清单
1. 浏览器本地缓存(原有功能增强)
- 一键获取数据并缓存:获取市场概览、风险预警、热门品种详情和K线数据到浏览器 localStorage
- 缓存统计:显示有效缓存、过期缓存、缓存总数和上次更新时间
- 清除所有缓存:一键清除浏览器本地缓存
2. 数据库持久化缓存(新增功能)
- 一键缓存所有合约到数据库:批量获取所有合约详情和K线数据,存入 MySQL 和 Redis
- 缓存指定合约到数据库:针对单个合约进行强制刷新缓存
前端实现
文件位置
src/pages/admin/AdminConfig.jsx
新增状态变量
// 数据缓存相关状态
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);
新增图标导入
import {
DatabaseOutlined,
KeyOutlined,
SettingOutlined,
SaveOutlined,
ToolOutlined,
RobotOutlined,
EditOutlined,
CloudDownloadOutlined,
ClearOutlined,
ThunderboltOutlined,
FileTextOutlined,
CodeOutlined
} from '@ant-design/icons';
核心功能函数
1. 浏览器本地缓存 - 一键获取数据
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);
}
};
缓存数据结构:
{
data: {...}, // 实际数据
timestamp: 1234567890, // 缓存时间戳
expiresAt: 1234567890 // 过期时间戳
}
缓存内容:
- 市场概览数据(
cached_overview):5分钟过期 - 风险预警数据(
cached_alerts):3分钟过期 - 品种详情数据(
cached_detail_{code}):10分钟过期 - K线数据(
cached_kline_{code}_{period}):30分钟过期
2. 数据库缓存 - 一键缓存所有合约
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. 数据库缓存 - 缓存指定合约
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')包含以下卡片:
-
缓存统计卡片
- 有效缓存数(绿色)
- 过期缓存数(橙色)
- 缓存总数(蓝色)
- 上次更新时间(紫色)
-
一键获取数据并缓存卡片
- 主要按钮:"一键获取数据"
- 危险按钮:"清除所有缓存"
- 进度条显示
- 缓存内容说明
-
一键缓存所有合约到数据库卡片
- 警告类型说明 Alert
- 危险主按钮:"缓存所有合约到数据库"
- 进度条显示
- 数据存储位置说明
-
缓存指定合约到数据库卡片
- 输入框:合约代码(带快捷选择按钮)
- 主按钮:"缓存指定合约"
- 常用合约代码快捷按钮(AU、CU、RB等)
- 缓存内容说明
-
缓存管理卡片
- 缓存存储位置说明
- 缓存策略说明
后端实现
文件位置
backend/src/api/market.ts- API 路由backend/src/services/cacheService.ts- 缓存服务
CacheService 新增方法
1. 直接保存数据到缓存
/**
* 直接保存数据到缓存(用于批量预热缓存)
*/
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. 获取数据库缓存统计
/**
* 获取数据库缓存统计
*/
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. 批量缓存所有合约
// 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. 缓存指定合约(强制刷新)
// 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. 获取缓存统计
// 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分钟 | 永久 |
使用指南
浏览器本地缓存使用
- 打开 AdminConfig 页面
- 切换到"数据缓存"页签
- 点击"一键获取数据"按钮
- 等待进度完成
- 查看缓存统计卡片确认缓存状态
数据库缓存使用
批量缓存所有合约
- 在"数据缓存"页签中找到"一键缓存所有合约到数据库"卡片
- 点击"缓存所有合约到数据库"按钮(红色按钮)
- 等待批量处理完成(时间取决于合约数量)
- 查看成功提示信息
缓存指定合约
- 在"缓存指定合约到数据库"卡片中
- 输入合约代码(如 AU、CU、RB)或点击快捷按钮
- 点击"缓存指定合约"按钮
- 等待处理完成
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 |
获取缓存统计 |
注意事项
-
性能考虑
- 批量缓存操作可能需要较长时间,请耐心等待
- 后端已实现请求节流(每10个合约延迟100ms)
-
错误处理
- 单个合约缓存失败不会影响其他合约
- 网络错误会显示详细的错误信息
-
数据一致性
- 数据库缓存采用"强制刷新"策略
- 会先清除旧缓存再写入新数据
-
存储限制
- LocalStorage 有 5MB 限制
- 大量K线数据建议使用数据库缓存
后续优化建议
- 添加缓存预热定时任务
- 实现缓存失效自动重试机制
- 添加缓存命中率监控
- 支持批量指定合约缓存
- 添加缓存导出/导入功能
文档版本: 1.0
最后更新: 2026-03-02
作者: AI Assistant