fix: 增加主动缓存等功能

master
Lxy 3 months ago
parent 0896f52f59
commit e8b78cc711

@ -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;

@ -122,6 +122,47 @@ class CacheService {
logger.error(`清除缓存失败: ${symbol}, ${type}`, error);
}
}
/**
*
*/
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;
}
}
/**
*
*/
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: {} };
}
}
}
export const cacheService = new CacheService();

Binary file not shown.

@ -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

@ -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<string, number>; // 按类型统计
}
```
---
**文档版本**: 1.0
**最后更新**: 2026-03-02

@ -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 缓存未命中
**现象**: 数据只存入 MySQLRedis 中没有
**排查步骤**:
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<T>(...) {
// 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

@ -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<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

@ -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 = () => {
</Card>
</>
)
},
{
label: <span><CloudDownloadOutlined /> 数据缓存</span>,
key: 'cache',
children: (
<>
{/* 缓存统计 */}
<Card title="缓存统计" className="admin-config-card" style={{ marginBottom: 24 }}>
<Row gutter={[16, 16]}>
<Col span={6}>
<div style={{ textAlign: 'center', padding: '20px', background: '#f6ffed', borderRadius: '8px' }}>
<div style={{ fontSize: '32px', fontWeight: 'bold', color: '#52c41a' }}>{cacheStats.valid}</div>
<div style={{ color: '#666', marginTop: '8px' }}>有效缓存</div>
</div>
</Col>
<Col span={6}>
<div style={{ textAlign: 'center', padding: '20px', background: '#fff7e6', borderRadius: '8px' }}>
<div style={{ fontSize: '32px', fontWeight: 'bold', color: '#fa8c16' }}>{cacheStats.expired}</div>
<div style={{ color: '#666', marginTop: '8px' }}>过期缓存</div>
</div>
</Col>
<Col span={6}>
<div style={{ textAlign: 'center', padding: '20px', background: '#e6f7ff', borderRadius: '8px' }}>
<div style={{ fontSize: '32px', fontWeight: 'bold', color: '#1890ff' }}>{cacheStats.total}</div>
<div style={{ color: '#666', marginTop: '8px' }}>缓存总数</div>
</div>
</Col>
<Col span={6}>
<div style={{ textAlign: 'center', padding: '20px', background: '#f9f0ff', borderRadius: '8px' }}>
<div style={{ fontSize: '14px', fontWeight: 'bold', color: '#722ed1' }}>
{cacheStats.lastUpdate ? new Date(cacheStats.lastUpdate).toLocaleString() : '从未'}
</div>
<div style={{ color: '#666', marginTop: '8px' }}>上次更新</div>
</div>
</Col>
</Row>
</Card>
{/* 一键缓存 */}
<Card title="一键获取数据并缓存" className="admin-config-card" style={{ marginBottom: 24 }}>
<Alert
message="数据缓存说明"
description="点击'一键获取数据'按钮系统将自动获取市场概览、风险预警、热门品种详情和K线数据并缓存到本地以提升后续访问速度。"
type="info"
showIcon
style={{ marginBottom: 16 }}
/>
{cacheProgress.total > 0 && (
<div style={{ marginBottom: 16, padding: '12px', background: '#f0f0f0', borderRadius: '6px' }}>
<div style={{ marginBottom: 8 }}>
<ThunderboltOutlined style={{ color: '#1890ff', marginRight: 8 }} />
正在缓存: {cacheProgress.name} ({cacheProgress.current}/{cacheProgress.total})
</div>
<div style={{ width: '100%', height: '8px', background: '#d9d9d9', borderRadius: '4px', overflow: 'hidden' }}>
<div
style={{
width: `${(cacheProgress.current / cacheProgress.total) * 100}%`,
height: '100%',
background: '#1890ff',
transition: 'width 0.3s ease'
}}
/>
</div>
</div>
)}
<Row gutter={[16, 16]}>
<Col span={24}>
<Button
type="primary"
icon={<CloudDownloadOutlined />}
loading={cacheLoading}
onClick={fetchAllDataForCache}
size="large"
style={{ marginRight: 16 }}
>
一键获取数据
</Button>
<Button
type="default"
icon={<ClearOutlined />}
onClick={clearAllCache}
size="large"
danger
>
清除所有缓存
</Button>
</Col>
</Row>
<Divider />
<div style={{ color: '#666' }}>
<h4>缓存内容包括</h4>
<ul style={{ lineHeight: '2' }}>
<li>📊 市场概览数据有效期5分钟</li>
<li> 风险预警数据有效期3分钟</li>
<li>📈 热门品种详情前5个品种有效期10分钟</li>
<li>📉 K线数据主要品种有效期30分钟</li>
</ul>
</div>
</Card>
{/* 一键缓存所有合约到数据库 */}
<Card title="一键缓存所有合约到数据库" className="admin-config-card" style={{ marginBottom: 24 }}>
<Alert
message="数据库缓存说明"
description="点击'缓存所有合约到数据库'按钮系统将自动获取所有合约的详情和K线数据并持久化存储到MySQL数据库和Redis缓存中。此操作会触发后端API自动存储数据。"
type="warning"
showIcon
style={{ marginBottom: 16 }}
/>
{dbCacheProgress.total > 0 && (
<div style={{ marginBottom: 16, padding: '12px', background: '#f0f0f0', borderRadius: '6px' }}>
<div style={{ marginBottom: 8 }}>
<ThunderboltOutlined style={{ color: '#fa8c16', marginRight: 8 }} />
正在处理: {dbCacheProgress.name} ({dbCacheProgress.current}/{dbCacheProgress.total})
</div>
<div style={{ width: '100%', height: '8px', background: '#d9d9d9', borderRadius: '4px', overflow: 'hidden' }}>
<div
style={{
width: `${(dbCacheProgress.current / dbCacheProgress.total) * 100}%`,
height: '100%',
background: '#fa8c16',
transition: 'width 0.3s ease'
}}
/>
</div>
</div>
)}
<Row gutter={[16, 16]}>
<Col span={24}>
<Button
type="primary"
icon={<DatabaseOutlined />}
loading={dbCacheLoading}
onClick={cacheAllContractsToDB}
size="large"
danger
style={{ marginRight: 16 }}
>
缓存所有合约到数据库
</Button>
</Col>
</Row>
<Divider />
<div style={{ color: '#666' }}>
<h4>数据存储位置</h4>
<ul style={{ lineHeight: '2' }}>
<li>🗄 MySQL数据库 - 持久化存储所有合约数据</li>
<li> Redis缓存 - 高速缓存热点数据</li>
<li>📊 合约详情数据market_data表</li>
<li>📈 K线数据多个周期</li>
</ul>
<p style={{ marginTop: 12, color: '#999' }}>
<FileTextOutlined style={{ marginRight: 8 }} />
注意此操作可能需要较长时间取决于合约数量和网络状况
</p>
</div>
</Card>
{/* 缓存指定合约到数据库 */}
<Card title="缓存指定合约到数据库" className="admin-config-card" style={{ marginBottom: 24 }}>
<Alert
message="单合约缓存"
description="输入合约代码(如 AU、CU、RB 等系统将获取该合约的详情和K线数据并缓存到数据库。"
type="info"
showIcon
style={{ marginBottom: 16 }}
/>
<Row gutter={[16, 16]} align="middle">
<Col span={12}>
<Input
placeholder="请输入合约代码AU"
value={symbolInput}
onChange={(e) => setSymbolInput(e.target.value)}
onPressEnter={cacheSymbolToDB}
size="large"
prefix={<CodeOutlined />}
style={{ textTransform: 'uppercase' }}
/>
</Col>
<Col span={12}>
<Button
type="primary"
icon={<CloudDownloadOutlined />}
loading={singleSymbolLoading}
onClick={cacheSymbolToDB}
size="large"
>
缓存指定合约
</Button>
</Col>
</Row>
<Divider />
<div style={{ color: '#666' }}>
<h4>常用合约代码示例</h4>
<Row gutter={[8, 8]}>
{['AU', 'CU', 'RB', 'AG', 'ZN', 'NI', 'AL', 'PB', 'SN', 'HC'].map(code => (
<Col key={code}>
<Button
size="small"
onClick={() => setSymbolInput(code)}
style={{ marginBottom: 8 }}
>
{code}
</Button>
</Col>
))}
</Row>
<p style={{ marginTop: 12 }}>
<FileTextOutlined style={{ marginRight: 8 }} />
缓存内容包括合约详情1小时K线4小时K线日线数据
</p>
</div>
</Card>
{/* 缓存管理 */}
<Card title="缓存管理" className="admin-config-card">
<Row gutter={[16, 16]}>
<Col span={12}>
<Card type="inner" title="缓存存储位置" size="small">
<p>数据缓存存储在浏览器的 <code>localStorage</code> </p>
<p>缓存键前缀: <code>cached_</code></p>
</Card>
</Col>
<Col span={12}>
<Card type="inner" title="缓存策略" size="small">
<p> 数据在有效期内优先使用缓存</p>
<p> 过期数据会自动重新获取</p>
<p> 支持强制刷新获取最新数据</p>
</Card>
</Col>
</Row>
</Card>
</>
)
}
]}
/>

Loading…
Cancel
Save