275 lines
8.6 KiB
JavaScript
275 lines
8.6 KiB
JavaScript
// 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<any>} 获取的数据
|
||
*/
|
||
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;
|