// 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} 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} 股票代码到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(() => { // 静默处理错误,预加载失败不影响用户体验 }); };