refactor: 重构 Community 目录,将公共组件迁移到 src/components/
- 迁移 klineDataCache.js 到 src/utils/stock/(被 StockChart 使用) - 迁移 InvestmentCalendar 到 src/components/InvestmentCalendar/(被 Navbar、Dashboard 使用) - 迁移 DynamicNewsDetail 到 src/components/EventDetailPanel/(被 EventDetail 使用) - 更新所有相关导入路径,使用路径别名 - 保持 Community 目录其余结构不变 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
288
src/utils/stock/klineDataCache.js
Normal file
288
src/utils/stock/klineDataCache.js
Normal file
@@ -0,0 +1,288 @@
|
||||
// src/views/Community/components/StockDetailPanel/utils/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(() => {
|
||||
// 静默处理错误,预加载失败不影响用户体验
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user