feat: 提交 Redux Slice
This commit is contained in:
@@ -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 自动追踪中间件
|
||||
|
||||
479
src/store/slices/stockSlice.js
Normal file
479
src/store/slices/stockSlice.js
Normal 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;
|
||||
Reference in New Issue
Block a user