修复 7 处导入路径问题: - EventHeaderInfo.js: StockChangeIndicators 和 EventFollowButton 路径 - klineDataCache.js: stockService 和 logger 路径别名 - EventDescriptionSection.js: professionalTheme 路径别名 - CollapsibleSection.js: professionalTheme 路径别名 - RelatedConceptsSection/index.js: logger 路径别名 - CompactMetaBar.js: EventFollowButton 路径 - EventDetailScrollPanel.js: DynamicNewsDetailPanel 路径 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
289 lines
9.6 KiB
JavaScript
289 lines
9.6 KiB
JavaScript
// src/utils/stock/klineDataCache.js
|
||
import dayjs from 'dayjs';
|
||
import { stockService } from '@services/eventService';
|
||
import { logger } from '@utils/logger';
|
||
|
||
// ================= 全局缓存和请求管理 =================
|
||
export const klineDataCache = new Map(); // 缓存K线数据: key = `${code}|${date}|${chartType}` -> data
|
||
export const pendingRequests = new Map(); // 正在进行的请求: key = `${code}|${date}|${chartType}` -> Promise
|
||
export const lastRequestTime = new Map(); // 最后请求时间: key = `${code}|${date}|${chartType}` -> timestamp
|
||
export const batchPendingRequests = new Map(); // 批量请求的 Promise: key = `${eventTime}|${chartType}` -> Promise
|
||
|
||
// 请求间隔限制(毫秒)
|
||
const REQUEST_INTERVAL = 30000; // 30秒内不重复请求同一只股票的数据
|
||
|
||
/**
|
||
* 获取缓存键
|
||
* @param {string} stockCode - 股票代码
|
||
* @param {string} eventTime - 事件时间
|
||
* @param {string} chartType - 图表类型(timeline/daily)
|
||
* @returns {string} 缓存键
|
||
*/
|
||
export const getCacheKey = (stockCode, eventTime, chartType = 'timeline') => {
|
||
const date = eventTime ? dayjs(eventTime).format('YYYY-MM-DD') : dayjs().format('YYYY-MM-DD');
|
||
return `${stockCode}|${date}|${chartType}`;
|
||
};
|
||
|
||
/**
|
||
* 检查是否需要刷新数据
|
||
* @param {string} cacheKey - 缓存键
|
||
* @returns {boolean} 是否需要刷新
|
||
*/
|
||
export const shouldRefreshData = (cacheKey) => {
|
||
const lastTime = lastRequestTime.get(cacheKey);
|
||
if (!lastTime) return true;
|
||
|
||
const now = Date.now();
|
||
const elapsed = now - lastTime;
|
||
|
||
// 如果是今天的数据且交易时间内,允许更频繁的更新
|
||
const today = dayjs().format('YYYY-MM-DD');
|
||
const isToday = cacheKey.includes(today);
|
||
const currentHour = new Date().getHours();
|
||
const isTradingHours = currentHour >= 9 && currentHour < 16;
|
||
|
||
if (isToday && isTradingHours) {
|
||
return elapsed > REQUEST_INTERVAL;
|
||
}
|
||
|
||
// 历史数据不需要频繁更新
|
||
return elapsed > 3600000; // 1小时
|
||
};
|
||
|
||
/**
|
||
* 获取K线数据(带缓存和防重复请求)
|
||
* @param {string} stockCode - 股票代码
|
||
* @param {string} eventTime - 事件时间
|
||
* @param {string} chartType - 图表类型(timeline/daily)
|
||
* @returns {Promise<Array>} K线数据
|
||
*/
|
||
export const fetchKlineData = async (stockCode, eventTime, chartType = 'timeline') => {
|
||
const cacheKey = getCacheKey(stockCode, eventTime, chartType);
|
||
|
||
// 1. 检查缓存
|
||
if (klineDataCache.has(cacheKey)) {
|
||
// 检查是否需要刷新
|
||
if (!shouldRefreshData(cacheKey)) {
|
||
logger.debug('klineDataCache', '使用缓存数据', { cacheKey });
|
||
return klineDataCache.get(cacheKey);
|
||
}
|
||
}
|
||
|
||
// 2. 检查是否有正在进行的请求
|
||
if (pendingRequests.has(cacheKey)) {
|
||
logger.debug('klineDataCache', '等待进行中的请求', { cacheKey });
|
||
return pendingRequests.get(cacheKey);
|
||
}
|
||
|
||
// 3. 发起新请求
|
||
logger.debug('klineDataCache', '发起新K线数据请求', { cacheKey, chartType });
|
||
const normalizedEventTime = eventTime ? dayjs(eventTime).format('YYYY-MM-DD HH:mm') : undefined;
|
||
const requestPromise = stockService
|
||
.getKlineData(stockCode, chartType, normalizedEventTime)
|
||
.then((res) => {
|
||
const data = Array.isArray(res?.data) ? res.data : [];
|
||
// 更新缓存
|
||
klineDataCache.set(cacheKey, data);
|
||
lastRequestTime.set(cacheKey, Date.now());
|
||
// 清除pending状态
|
||
pendingRequests.delete(cacheKey);
|
||
logger.debug('klineDataCache', 'K线数据请求完成并缓存', {
|
||
cacheKey,
|
||
chartType,
|
||
dataPoints: data.length
|
||
});
|
||
return data;
|
||
})
|
||
.catch((error) => {
|
||
logger.error('klineDataCache', 'fetchKlineData', error, { stockCode, chartType, cacheKey });
|
||
// 清除pending状态
|
||
pendingRequests.delete(cacheKey);
|
||
// 如果有旧缓存,返回旧数据
|
||
if (klineDataCache.has(cacheKey)) {
|
||
return klineDataCache.get(cacheKey);
|
||
}
|
||
return [];
|
||
});
|
||
|
||
// 保存pending请求
|
||
pendingRequests.set(cacheKey, requestPromise);
|
||
|
||
return requestPromise;
|
||
};
|
||
|
||
/**
|
||
* 清除指定股票的缓存
|
||
* @param {string} stockCode - 股票代码
|
||
* @param {string} eventTime - 事件时间(可选)
|
||
*/
|
||
export const clearCache = (stockCode, eventTime = null) => {
|
||
if (eventTime) {
|
||
const cacheKey = getCacheKey(stockCode, eventTime);
|
||
klineDataCache.delete(cacheKey);
|
||
lastRequestTime.delete(cacheKey);
|
||
pendingRequests.delete(cacheKey);
|
||
logger.debug('klineDataCache', '清除缓存', { cacheKey });
|
||
} else {
|
||
// 清除该股票的所有缓存
|
||
const prefix = `${stockCode}|`;
|
||
for (const key of klineDataCache.keys()) {
|
||
if (key.startsWith(prefix)) {
|
||
klineDataCache.delete(key);
|
||
lastRequestTime.delete(key);
|
||
pendingRequests.delete(key);
|
||
}
|
||
}
|
||
logger.debug('klineDataCache', '清除股票所有缓存', { stockCode });
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 清除所有缓存
|
||
*/
|
||
export const clearAllCache = () => {
|
||
klineDataCache.clear();
|
||
lastRequestTime.clear();
|
||
pendingRequests.clear();
|
||
logger.debug('klineDataCache', '清除所有缓存');
|
||
};
|
||
|
||
/**
|
||
* 获取缓存统计信息
|
||
* @returns {Object} 缓存统计
|
||
*/
|
||
export const getCacheStats = () => {
|
||
return {
|
||
totalCached: klineDataCache.size,
|
||
pendingRequests: pendingRequests.size,
|
||
cacheKeys: Array.from(klineDataCache.keys())
|
||
};
|
||
};
|
||
|
||
/**
|
||
* 批量获取多只股票的K线数据(一次API请求)
|
||
* @param {string[]} stockCodes - 股票代码数组
|
||
* @param {string} eventTime - 事件时间
|
||
* @param {string} chartType - 图表类型(timeline/daily)
|
||
* @returns {Promise<Object>} 股票代码到K线数据的映射 { [stockCode]: data[] }
|
||
*/
|
||
export const fetchBatchKlineData = async (stockCodes, eventTime, chartType = 'timeline') => {
|
||
if (!stockCodes || stockCodes.length === 0) {
|
||
return {};
|
||
}
|
||
|
||
const normalizedEventTime = eventTime ? dayjs(eventTime).format('YYYY-MM-DD HH:mm') : undefined;
|
||
const batchKey = `${normalizedEventTime || 'today'}|${chartType}`;
|
||
|
||
// 过滤出未缓存的股票
|
||
const uncachedCodes = stockCodes.filter(code => {
|
||
const cacheKey = getCacheKey(code, eventTime, chartType);
|
||
return !klineDataCache.has(cacheKey) || shouldRefreshData(cacheKey);
|
||
});
|
||
|
||
logger.debug('klineDataCache', '批量请求分析', {
|
||
totalCodes: stockCodes.length,
|
||
uncachedCodes: uncachedCodes.length,
|
||
cachedCodes: stockCodes.length - uncachedCodes.length
|
||
});
|
||
|
||
// 如果所有股票都有缓存,直接返回缓存数据
|
||
if (uncachedCodes.length === 0) {
|
||
const result = {};
|
||
stockCodes.forEach(code => {
|
||
const cacheKey = getCacheKey(code, eventTime, chartType);
|
||
result[code] = klineDataCache.get(cacheKey) || [];
|
||
});
|
||
logger.debug('klineDataCache', '所有股票数据来自缓存', { stockCount: stockCodes.length });
|
||
return result;
|
||
}
|
||
|
||
// 检查是否有正在进行的批量请求
|
||
if (batchPendingRequests.has(batchKey)) {
|
||
logger.debug('klineDataCache', '等待进行中的批量请求', { batchKey });
|
||
return batchPendingRequests.get(batchKey);
|
||
}
|
||
|
||
// 发起批量请求
|
||
logger.debug('klineDataCache', '发起批量K线数据请求', {
|
||
batchKey,
|
||
stockCount: uncachedCodes.length,
|
||
chartType
|
||
});
|
||
|
||
const requestPromise = stockService
|
||
.getBatchKlineData(uncachedCodes, chartType, normalizedEventTime)
|
||
.then((response) => {
|
||
const batchData = response?.data || {};
|
||
const now = Date.now();
|
||
|
||
// 将批量数据存入缓存
|
||
Object.entries(batchData).forEach(([code, stockData]) => {
|
||
const data = Array.isArray(stockData?.data) ? stockData.data : [];
|
||
const cacheKey = getCacheKey(code, eventTime, chartType);
|
||
klineDataCache.set(cacheKey, data);
|
||
lastRequestTime.set(cacheKey, now);
|
||
});
|
||
|
||
// 对于请求中没有返回数据的股票,设置空数组
|
||
uncachedCodes.forEach(code => {
|
||
if (!batchData[code]) {
|
||
const cacheKey = getCacheKey(code, eventTime, chartType);
|
||
if (!klineDataCache.has(cacheKey)) {
|
||
klineDataCache.set(cacheKey, []);
|
||
lastRequestTime.set(cacheKey, now);
|
||
}
|
||
}
|
||
});
|
||
|
||
// 清除批量请求状态
|
||
batchPendingRequests.delete(batchKey);
|
||
|
||
logger.debug('klineDataCache', '批量K线数据请求完成', {
|
||
batchKey,
|
||
stockCount: Object.keys(batchData).length
|
||
});
|
||
|
||
// 返回所有请求股票的数据(包括之前缓存的)
|
||
const result = {};
|
||
stockCodes.forEach(code => {
|
||
const cacheKey = getCacheKey(code, eventTime, chartType);
|
||
result[code] = klineDataCache.get(cacheKey) || [];
|
||
});
|
||
return result;
|
||
})
|
||
.catch((error) => {
|
||
logger.error('klineDataCache', 'fetchBatchKlineData', error, {
|
||
stockCount: uncachedCodes.length,
|
||
chartType
|
||
});
|
||
// 清除批量请求状态
|
||
batchPendingRequests.delete(batchKey);
|
||
|
||
// 返回已缓存的数据
|
||
const result = {};
|
||
stockCodes.forEach(code => {
|
||
const cacheKey = getCacheKey(code, eventTime, chartType);
|
||
result[code] = klineDataCache.get(cacheKey) || [];
|
||
});
|
||
return result;
|
||
});
|
||
|
||
// 保存批量请求
|
||
batchPendingRequests.set(batchKey, requestPromise);
|
||
|
||
return requestPromise;
|
||
};
|
||
|
||
/**
|
||
* 预加载多只股票的K线数据(后台执行,不阻塞UI)
|
||
* @param {string[]} stockCodes - 股票代码数组
|
||
* @param {string} eventTime - 事件时间
|
||
* @param {string} chartType - 图表类型(timeline/daily)
|
||
*/
|
||
export const preloadBatchKlineData = (stockCodes, eventTime, chartType = 'timeline') => {
|
||
// 异步执行,不返回Promise,不阻塞调用方
|
||
fetchBatchKlineData(stockCodes, eventTime, chartType).catch(() => {
|
||
// 静默处理错误,预加载失败不影响用户体验
|
||
});
|
||
};
|