// src/store/slices/communityDataSlice.js import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; import { eventService } from '../../services/eventService'; import { logger } from '../../utils/logger'; import { localCacheManager, CACHE_EXPIRY_STRATEGY } from '../../utils/CacheManager'; // ==================== 常量定义 ==================== // 缓存键名 const CACHE_KEYS = { POPULAR_KEYWORDS: 'community_popular_keywords', HOT_EVENTS: 'community_hot_events' }; // 请求去重:缓存正在进行的请求 const pendingRequests = new Map(); // ==================== 通用数据获取逻辑 ==================== /** * 通用的数据获取函数(支持三级缓存 + 请求去重) * @param {Object} options - 配置选项 * @param {string} options.cacheKey - 缓存键名 * @param {Function} options.fetchFn - API 获取函数 * @param {Function} options.getState - Redux getState 函数 * @param {string} options.stateKey - Redux state 中的键名 * @param {boolean} options.forceRefresh - 是否强制刷新 * @returns {Promise} 获取的数据 */ const fetchWithCache = async ({ cacheKey, fetchFn, getState, stateKey, forceRefresh = false }) => { // 请求去重:如果有正在进行的相同请求,直接返回该 Promise if (!forceRefresh && pendingRequests.has(cacheKey)) { logger.debug('CommunityData', `复用进行中的请求: ${stateKey}`); return pendingRequests.get(cacheKey); } const requestPromise = (async () => { try { // 第一级缓存:检查 Redux 状态(除非强制刷新) if (!forceRefresh) { const stateData = getState().communityData[stateKey]; if (stateData && stateData.length > 0) { logger.debug('CommunityData', `Redux 状态中已有${stateKey}数据`); return stateData; } // 第二级缓存:检查 localStorage const cachedData = localCacheManager.get(cacheKey); if (cachedData) { return cachedData; } } // 第三级:从 API 获取 logger.debug('CommunityData', `从 API 获取${stateKey}`, { forceRefresh }); const response = await fetchFn(); if (response.success && response.data) { // 保存到 localStorage(午夜过期) localCacheManager.set(cacheKey, response.data, CACHE_EXPIRY_STRATEGY.MIDNIGHT); return response.data; } logger.warn('CommunityData', `API 返回数据为空:${stateKey}`); return []; } catch (error) { logger.error('CommunityData', `获取${stateKey}失败`, error); throw error; } finally { // 请求完成后清除缓存 pendingRequests.delete(cacheKey); } })(); // 缓存请求 Promise if (!forceRefresh) { pendingRequests.set(cacheKey, requestPromise); } return requestPromise; }; // ==================== Reducer 工厂函数 ==================== /** * 创建通用的 reducer cases * @param {Object} builder - Redux Toolkit builder * @param {Object} asyncThunk - createAsyncThunk 返回的对象 * @param {string} dataKey - state 中的数据键名(如 'popularKeywords') */ const createDataReducers = (builder, asyncThunk, dataKey) => { builder .addCase(asyncThunk.pending, (state) => { state.loading[dataKey] = true; state.error[dataKey] = null; }) .addCase(asyncThunk.fulfilled, (state, action) => { state.loading[dataKey] = false; state[dataKey] = action.payload; state.lastUpdated[dataKey] = new Date().toISOString(); }) .addCase(asyncThunk.rejected, (state, action) => { state.loading[dataKey] = false; state.error[dataKey] = action.payload; logger.error('CommunityData', `${dataKey} 加载失败`, new Error(action.payload)); }); }; // ==================== Async Thunks ==================== /** * 获取热门关键词 * @param {boolean} forceRefresh - 是否强制刷新(跳过缓存) */ export const fetchPopularKeywords = createAsyncThunk( 'communityData/fetchPopularKeywords', async (forceRefresh = false, { getState, rejectWithValue }) => { try { return await fetchWithCache({ cacheKey: CACHE_KEYS.POPULAR_KEYWORDS, fetchFn: () => eventService.getPopularKeywords(20), getState, stateKey: 'popularKeywords', forceRefresh }); } catch (error) { return rejectWithValue(error.message || '获取热门关键词失败'); } } ); /** * 获取热点事件 * @param {boolean} forceRefresh - 是否强制刷新(跳过缓存) */ export const fetchHotEvents = createAsyncThunk( 'communityData/fetchHotEvents', async (forceRefresh = false, { getState, rejectWithValue }) => { try { return await fetchWithCache({ cacheKey: CACHE_KEYS.HOT_EVENTS, fetchFn: () => eventService.getHotEvents({ days: 5, limit: 20 }), getState, stateKey: 'hotEvents', forceRefresh }); } catch (error) { return rejectWithValue(error.message || '获取热点事件失败'); } } ); // ==================== Slice 定义 ==================== const communityDataSlice = createSlice({ name: 'communityData', initialState: { // 数据 popularKeywords: [], hotEvents: [], // 加载状态 loading: { popularKeywords: false, hotEvents: false }, // 错误信息 error: { popularKeywords: null, hotEvents: null }, // 最后更新时间 lastUpdated: { popularKeywords: null, hotEvents: null } }, reducers: { /** * 清除所有缓存(Redux + localStorage) */ clearCache: (state) => { // 清除 localStorage localCacheManager.removeMultiple(Object.values(CACHE_KEYS)); // 清除 Redux 状态 state.popularKeywords = []; state.hotEvents = []; state.lastUpdated.popularKeywords = null; state.lastUpdated.hotEvents = null; logger.info('CommunityData', '所有缓存已清除'); }, /** * 清除指定类型的缓存 * @param {string} payload - 缓存类型 ('popularKeywords' | 'hotEvents') */ clearSpecificCache: (state, action) => { const type = action.payload; if (type === 'popularKeywords') { localCacheManager.remove(CACHE_KEYS.POPULAR_KEYWORDS); state.popularKeywords = []; state.lastUpdated.popularKeywords = null; logger.info('CommunityData', '热门关键词缓存已清除'); } else if (type === 'hotEvents') { localCacheManager.remove(CACHE_KEYS.HOT_EVENTS); state.hotEvents = []; state.lastUpdated.hotEvents = null; logger.info('CommunityData', '热点事件缓存已清除'); } }, /** * 预加载数据(用于应用启动时) * 注意:这不是异步 action,只是触发标记 */ preloadData: (state) => { logger.info('CommunityData', '准备预加载数据'); // 实际的预加载逻辑在组件中调用 dispatch(fetchPopularKeywords()) 等 } }, extraReducers: (builder) => { // 使用工厂函数创建 reducers,消除重复代码 createDataReducers(builder, fetchPopularKeywords, 'popularKeywords'); createDataReducers(builder, fetchHotEvents, 'hotEvents'); } }); // ==================== 导出 ==================== export const { clearCache, clearSpecificCache, preloadData } = communityDataSlice.actions; // 基础选择器(Selectors) export const selectPopularKeywords = (state) => state.communityData.popularKeywords; export const selectHotEvents = (state) => state.communityData.hotEvents; export const selectLoading = (state) => state.communityData.loading; export const selectError = (state) => state.communityData.error; export const selectLastUpdated = (state) => state.communityData.lastUpdated; // 组合选择器 export const selectPopularKeywordsWithLoading = (state) => ({ data: state.communityData.popularKeywords, loading: state.communityData.loading.popularKeywords, error: state.communityData.error.popularKeywords, lastUpdated: state.communityData.lastUpdated.popularKeywords }); export const selectHotEventsWithLoading = (state) => ({ data: state.communityData.hotEvents, loading: state.communityData.loading.hotEvents, error: state.communityData.error.hotEvents, lastUpdated: state.communityData.lastUpdated.hotEvents }); // 工具函数:检查数据是否需要刷新(超过指定时间) export const shouldRefresh = (lastUpdated, thresholdMinutes = 30) => { if (!lastUpdated) return true; const elapsed = Date.now() - new Date(lastUpdated).getTime(); return elapsed > thresholdMinutes * 60 * 1000; }; export default communityDataSlice.reducer;