Files
vf_react/src/store/slices/communityDataSlice.js
2025-10-27 17:22:03 +08:00

275 lines
8.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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;