feat: 提交 Redux Slice

This commit is contained in:
zdl
2025-10-30 13:03:31 +08:00
parent c77061f36d
commit 3472d267af
2 changed files with 483 additions and 0 deletions

View File

@@ -3,6 +3,7 @@ import { configureStore } from '@reduxjs/toolkit';
import communityDataReducer from './slices/communityDataSlice';
import posthogReducer from './slices/posthogSlice';
import industryReducer from './slices/industrySlice';
import stockReducer from './slices/stockSlice';
import posthogMiddleware from './middleware/posthogMiddleware';
export const store = configureStore({
@@ -10,6 +11,7 @@ export const store = configureStore({
communityData: communityDataReducer,
posthog: posthogReducer, // ✅ PostHog Redux 状态管理
industry: industryReducer, // ✅ 行业分类数据管理
stock: stockReducer, // ✅ 股票和事件数据管理
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
@@ -19,6 +21,8 @@ export const store = configureStore({
'communityData/fetchPopularKeywords/fulfilled',
'communityData/fetchHotEvents/fulfilled',
'posthog/trackEvent/fulfilled', // ✅ PostHog 事件追踪
'stock/fetchEventStocks/fulfilled',
'stock/fetchStockQuotes/fulfilled',
],
},
}).concat(posthogMiddleware), // ✅ PostHog 自动追踪中间件

View File

@@ -0,0 +1,479 @@
// src/store/slices/stockSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import { eventService, stockService } from '../../services/eventService';
import { logger } from '../../utils/logger';
import { localCacheManager, CACHE_EXPIRY_STRATEGY } from '../../utils/CacheManager';
import { getApiBase } from '../../utils/apiConfig';
// ==================== 常量定义 ====================
// 缓存键名
const CACHE_KEYS = {
EVENT_STOCKS: 'event_stocks_',
EVENT_DETAIL: 'event_detail_',
HISTORICAL_EVENTS: 'historical_events_',
CHAIN_ANALYSIS: 'chain_analysis_',
EXPECTATION_SCORE: 'expectation_score_',
WATCHLIST: 'user_watchlist'
};
// 请求去重:缓存正在进行的请求
const pendingRequests = new Map();
// ==================== Async Thunks ====================
/**
* 获取事件相关股票(三级缓存)
*/
export const fetchEventStocks = createAsyncThunk(
'stock/fetchEventStocks',
async ({ eventId, forceRefresh = false }, { getState }) => {
logger.debug('stockSlice', 'fetchEventStocks', { eventId, forceRefresh });
// 1. Redux 状态缓存
if (!forceRefresh) {
const cached = getState().stock.eventStocksCache[eventId];
if (cached && cached.length > 0) {
logger.debug('stockSlice', 'Redux 缓存命中', { eventId });
return { eventId, stocks: cached };
}
}
// 2. LocalStorage 缓存
if (!forceRefresh) {
const localCached = localCacheManager.get(CACHE_KEYS.EVENT_STOCKS + eventId);
if (localCached) {
logger.debug('stockSlice', 'LocalStorage 缓存命中', { eventId });
return { eventId, stocks: localCached };
}
}
// 3. API 请求
const res = await eventService.getRelatedStocks(eventId);
if (res.success && res.data) {
logger.debug('stockSlice', 'API 请求成功', {
eventId,
stockCount: res.data.length
});
localCacheManager.set(
CACHE_KEYS.EVENT_STOCKS + eventId,
res.data,
CACHE_EXPIRY_STRATEGY.LONG // 1小时
);
return { eventId, stocks: res.data };
}
throw new Error(res.error || '获取股票数据失败');
}
);
/**
* 获取股票行情
*/
export const fetchStockQuotes = createAsyncThunk(
'stock/fetchStockQuotes',
async ({ codes, eventTime }) => {
logger.debug('stockSlice', 'fetchStockQuotes', {
codeCount: codes.length,
eventTime
});
const quotes = await stockService.getQuotes(codes, eventTime);
return quotes;
}
);
/**
* 获取事件详情
*/
export const fetchEventDetail = createAsyncThunk(
'stock/fetchEventDetail',
async ({ eventId, forceRefresh = false }, { getState }) => {
logger.debug('stockSlice', 'fetchEventDetail', { eventId });
// Redux 缓存
if (!forceRefresh) {
const cached = getState().stock.eventDetailsCache[eventId];
if (cached) {
logger.debug('stockSlice', 'Redux 缓存命中 - eventDetail', { eventId });
return { eventId, detail: cached };
}
}
// LocalStorage 缓存
if (!forceRefresh) {
const localCached = localCacheManager.get(CACHE_KEYS.EVENT_DETAIL + eventId);
if (localCached) {
logger.debug('stockSlice', 'LocalStorage 缓存命中 - eventDetail', { eventId });
return { eventId, detail: localCached };
}
}
// API 请求
const res = await eventService.getEventDetail(eventId);
if (res.success && res.data) {
localCacheManager.set(
CACHE_KEYS.EVENT_DETAIL + eventId,
res.data,
CACHE_EXPIRY_STRATEGY.LONG
);
return { eventId, detail: res.data };
}
throw new Error(res.error || '获取事件详情失败');
}
);
/**
* 获取历史事件对比
*/
export const fetchHistoricalEvents = createAsyncThunk(
'stock/fetchHistoricalEvents',
async ({ eventId, forceRefresh = false }, { getState }) => {
logger.debug('stockSlice', 'fetchHistoricalEvents', { eventId });
// Redux 缓存
if (!forceRefresh) {
const cached = getState().stock.historicalEventsCache[eventId];
if (cached) {
return { eventId, events: cached };
}
}
// LocalStorage 缓存
if (!forceRefresh) {
const localCached = localCacheManager.get(CACHE_KEYS.HISTORICAL_EVENTS + eventId);
if (localCached) {
return { eventId, events: localCached };
}
}
// API 请求
const res = await eventService.getHistoricalEvents(eventId);
if (res.success && res.data) {
localCacheManager.set(
CACHE_KEYS.HISTORICAL_EVENTS + eventId,
res.data,
CACHE_EXPIRY_STRATEGY.LONG
);
return { eventId, events: res.data };
}
return { eventId, events: [] };
}
);
/**
* 获取传导链分析
*/
export const fetchChainAnalysis = createAsyncThunk(
'stock/fetchChainAnalysis',
async ({ eventId, forceRefresh = false }, { getState }) => {
logger.debug('stockSlice', 'fetchChainAnalysis', { eventId });
// Redux 缓存
if (!forceRefresh) {
const cached = getState().stock.chainAnalysisCache[eventId];
if (cached) {
return { eventId, analysis: cached };
}
}
// LocalStorage 缓存
if (!forceRefresh) {
const localCached = localCacheManager.get(CACHE_KEYS.CHAIN_ANALYSIS + eventId);
if (localCached) {
return { eventId, analysis: localCached };
}
}
// API 请求
const res = await eventService.getTransmissionChainAnalysis(eventId);
if (res.success && res.data) {
localCacheManager.set(
CACHE_KEYS.CHAIN_ANALYSIS + eventId,
res.data,
CACHE_EXPIRY_STRATEGY.LONG
);
return { eventId, analysis: res.data };
}
return { eventId, analysis: null };
}
);
/**
* 获取超预期得分
*/
export const fetchExpectationScore = createAsyncThunk(
'stock/fetchExpectationScore',
async ({ eventId }) => {
logger.debug('stockSlice', 'fetchExpectationScore', { eventId });
if (eventService.getExpectationScore) {
const res = await eventService.getExpectationScore(eventId);
if (res.success && res.data) {
return { eventId, score: res.data.score };
}
}
return { eventId, score: null };
}
);
/**
* 加载用户自选股列表
*/
export const loadWatchlist = createAsyncThunk(
'stock/loadWatchlist',
async (_, { getState }) => {
logger.debug('stockSlice', 'loadWatchlist');
try {
const apiBase = getApiBase();
const response = await fetch(`${apiBase}/api/account/watchlist`, {
credentials: 'include'
});
const data = await response.json();
if (data.success && data.data) {
const stockCodes = data.data.map(item => item.stock_code);
logger.debug('stockSlice', '自选股列表加载成功', {
count: stockCodes.length
});
return stockCodes;
}
return [];
} catch (error) {
logger.error('stockSlice', 'loadWatchlist', error);
return [];
}
}
);
/**
* 切换自选股状态
*/
export const toggleWatchlist = createAsyncThunk(
'stock/toggleWatchlist',
async ({ stockCode, stockName, isInWatchlist }) => {
logger.debug('stockSlice', 'toggleWatchlist', {
stockCode,
stockName,
isInWatchlist
});
const apiBase = getApiBase();
let response;
if (isInWatchlist) {
// 移除自选股
response = await fetch(`${apiBase}/api/account/watchlist/${stockCode}`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
credentials: 'include'
});
} else {
// 添加自选股
response = await fetch(`${apiBase}/api/account/watchlist`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ stock_code: stockCode, stock_name: stockName })
});
}
const data = await response.json();
if (!data.success) {
throw new Error(data.error || '操作失败');
}
return { stockCode, isInWatchlist };
}
);
// ==================== Slice ====================
const stockSlice = createSlice({
name: 'stock',
initialState: {
// 事件相关股票缓存 { [eventId]: stocks[] }
eventStocksCache: {},
// 股票行情 { [stockCode]: quote }
quotes: {},
// 事件详情缓存 { [eventId]: detail }
eventDetailsCache: {},
// 历史事件缓存 { [eventId]: events[] }
historicalEventsCache: {},
// 传导链分析缓存 { [eventId]: analysis }
chainAnalysisCache: {},
// 超预期得分缓存 { [eventId]: score }
expectationScores: {},
// 自选股列表 Set<stockCode>
watchlist: [],
// 加载状态
loading: {
stocks: false,
quotes: false,
eventDetail: false,
historicalEvents: false,
chainAnalysis: false,
watchlist: false
},
// 错误信息
error: null
},
reducers: {
/**
* 更新单个股票行情
*/
updateQuote: (state, action) => {
const { stockCode, quote } = action.payload;
state.quotes[stockCode] = quote;
},
/**
* 批量更新股票行情
*/
updateQuotes: (state, action) => {
state.quotes = { ...state.quotes, ...action.payload };
},
/**
* 清空行情数据
*/
clearQuotes: (state) => {
state.quotes = {};
},
/**
* 清空指定事件的缓存
*/
clearEventCache: (state, action) => {
const { eventId } = action.payload;
delete state.eventStocksCache[eventId];
delete state.eventDetailsCache[eventId];
delete state.historicalEventsCache[eventId];
delete state.chainAnalysisCache[eventId];
delete state.expectationScores[eventId];
}
},
extraReducers: (builder) => {
builder
// ===== fetchEventStocks =====
.addCase(fetchEventStocks.pending, (state) => {
state.loading.stocks = true;
state.error = null;
})
.addCase(fetchEventStocks.fulfilled, (state, action) => {
const { eventId, stocks } = action.payload;
state.eventStocksCache[eventId] = stocks;
state.loading.stocks = false;
})
.addCase(fetchEventStocks.rejected, (state, action) => {
state.loading.stocks = false;
state.error = action.error.message;
})
// ===== fetchStockQuotes =====
.addCase(fetchStockQuotes.pending, (state) => {
state.loading.quotes = true;
})
.addCase(fetchStockQuotes.fulfilled, (state, action) => {
state.quotes = { ...state.quotes, ...action.payload };
state.loading.quotes = false;
})
.addCase(fetchStockQuotes.rejected, (state) => {
state.loading.quotes = false;
})
// ===== fetchEventDetail =====
.addCase(fetchEventDetail.pending, (state) => {
state.loading.eventDetail = true;
})
.addCase(fetchEventDetail.fulfilled, (state, action) => {
const { eventId, detail } = action.payload;
state.eventDetailsCache[eventId] = detail;
state.loading.eventDetail = false;
})
.addCase(fetchEventDetail.rejected, (state) => {
state.loading.eventDetail = false;
})
// ===== fetchHistoricalEvents =====
.addCase(fetchHistoricalEvents.pending, (state) => {
state.loading.historicalEvents = true;
})
.addCase(fetchHistoricalEvents.fulfilled, (state, action) => {
const { eventId, events } = action.payload;
state.historicalEventsCache[eventId] = events;
state.loading.historicalEvents = false;
})
.addCase(fetchHistoricalEvents.rejected, (state) => {
state.loading.historicalEvents = false;
})
// ===== fetchChainAnalysis =====
.addCase(fetchChainAnalysis.pending, (state) => {
state.loading.chainAnalysis = true;
})
.addCase(fetchChainAnalysis.fulfilled, (state, action) => {
const { eventId, analysis } = action.payload;
state.chainAnalysisCache[eventId] = analysis;
state.loading.chainAnalysis = false;
})
.addCase(fetchChainAnalysis.rejected, (state) => {
state.loading.chainAnalysis = false;
})
// ===== fetchExpectationScore =====
.addCase(fetchExpectationScore.fulfilled, (state, action) => {
const { eventId, score } = action.payload;
state.expectationScores[eventId] = score;
})
// ===== loadWatchlist =====
.addCase(loadWatchlist.pending, (state) => {
state.loading.watchlist = true;
})
.addCase(loadWatchlist.fulfilled, (state, action) => {
state.watchlist = action.payload;
state.loading.watchlist = false;
})
.addCase(loadWatchlist.rejected, (state) => {
state.loading.watchlist = false;
})
// ===== toggleWatchlist =====
.addCase(toggleWatchlist.fulfilled, (state, action) => {
const { stockCode, isInWatchlist } = action.payload;
if (isInWatchlist) {
// 移除
state.watchlist = state.watchlist.filter(code => code !== stockCode);
} else {
// 添加
if (!state.watchlist.includes(stockCode)) {
state.watchlist.push(stockCode);
}
}
});
}
});
export const {
updateQuote,
updateQuotes,
clearQuotes,
clearEventCache
} = stockSlice.actions;
export default stockSlice.reducer;