// src/store/slices/stockSlice.js import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; import { eventService, stockService } from '../../services/eventService'; import { logger } from '../../utils/logger'; import { getApiBase } from '../../utils/apiConfig'; // ==================== Async Thunks ==================== /** * 获取事件相关股票(Redux 缓存) */ export const fetchEventStocks = createAsyncThunk( 'stock/fetchEventStocks', async ({ eventId, forceRefresh = false }, { getState }) => { logger.debug('stockSlice', 'fetchEventStocks', { eventId, forceRefresh }); // Redux 状态缓存 if (!forceRefresh) { const cached = getState().stock.eventStocksCache[eventId]; if (cached && cached.length > 0) { logger.debug('stockSlice', 'Redux 缓存命中', { eventId }); return { eventId, stocks: cached }; } } // API 请求 const res = await eventService.getRelatedStocks(eventId); if (res.success && res.data) { logger.debug('stockSlice', 'API 请求成功', { eventId, stockCount: res.data.length }); 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; } ); /** * 获取事件详情(Redux 缓存) */ 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 }; } } // API 请求 const res = await eventService.getEventDetail(eventId); if (res.success && res.data) { return { eventId, detail: res.data }; } throw new Error(res.error || '获取事件详情失败'); } ); /** * 获取历史事件对比(Redux 缓存) */ 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 }; } } // API 请求 const res = await eventService.getHistoricalEvents(eventId); if (res.success && res.data) { return { eventId, events: res.data }; } return { eventId, events: [] }; } ); /** * 获取传导链分析(Redux 缓存) */ 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 }; } } // API 请求 const res = await eventService.getTransmissionChainAnalysis(eventId); if (res.success && res.data) { 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 () => { 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) { // 返回完整的股票信息,而不仅仅是 stock_code const watchlistData = data.data.map(item => ({ stock_code: item.stock_code, stock_name: item.stock_name, })); logger.debug('stockSlice', '自选股列表加载成功', { count: watchlistData.length }); return watchlistData; } return []; } catch (error) { logger.error('stockSlice', 'loadWatchlist', error); return []; } } ); /** * 加载全部股票列表(用于前端模糊搜索) */ export const loadAllStocks = createAsyncThunk( 'stock/loadAllStocks', async (_, { getState }) => { // 检查缓存 const cached = getState().stock.allStocks; if (cached && cached.length > 0) { logger.debug('stockSlice', 'allStocks 缓存命中', { count: cached.length }); return cached; } logger.debug('stockSlice', 'loadAllStocks'); try { const apiBase = getApiBase(); const response = await fetch(`${apiBase}/api/stocklist`, { credentials: 'include' }); const data = await response.json(); if (Array.isArray(data)) { logger.debug('stockSlice', '全部股票列表加载成功', { count: data.length }); return data; } return []; } catch (error) { logger.error('stockSlice', 'loadAllStocks', 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, stockName, 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: {}, // 自选股列表 [{ stock_code, stock_name }] watchlist: [], // 全部股票列表(用于前端模糊搜索)[{ code, name }] allStocks: [], // 加载状态 loading: { stocks: false, quotes: false, eventDetail: false, historicalEvents: false, chainAnalysis: false, watchlist: false, allStocks: 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]; }, /** * 乐观更新:添加自选股(同步) */ optimisticAddWatchlist: (state, action) => { const { stockCode, stockName } = action.payload; // 避免重复添加 const exists = state.watchlist.some(item => item.stock_code === stockCode); if (!exists) { state.watchlist.push({ stock_code: stockCode, stock_name: stockName || '' }); } }, /** * 乐观更新:移除自选股(同步) */ optimisticRemoveWatchlist: (state, action) => { const { stockCode } = action.payload; state.watchlist = state.watchlist.filter(item => item.stock_code !== stockCode); } }, 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; }) // ===== loadAllStocks ===== .addCase(loadAllStocks.pending, (state) => { state.loading.allStocks = true; }) .addCase(loadAllStocks.fulfilled, (state, action) => { state.allStocks = action.payload; state.loading.allStocks = false; }) .addCase(loadAllStocks.rejected, (state) => { state.loading.allStocks = false; }) // ===== toggleWatchlist(乐观更新)===== // pending: 立即更新状态 .addCase(toggleWatchlist.pending, (state, action) => { const { stockCode, stockName, isInWatchlist } = action.meta.arg; if (isInWatchlist) { // 移除 state.watchlist = state.watchlist.filter(item => item.stock_code !== stockCode); } else { // 添加 const exists = state.watchlist.some(item => item.stock_code === stockCode); if (!exists) { state.watchlist.push({ stock_code: stockCode, stock_name: stockName }); } } }) // rejected: 回滚状态 .addCase(toggleWatchlist.rejected, (state, action) => { const { stockCode, stockName, isInWatchlist } = action.meta.arg; // 回滚:与 pending 操作相反 if (isInWatchlist) { // 之前移除了,现在加回来 const exists = state.watchlist.some(item => item.stock_code === stockCode); if (!exists) { state.watchlist.push({ stock_code: stockCode, stock_name: stockName }); } } else { // 之前添加了,现在移除 state.watchlist = state.watchlist.filter(item => item.stock_code !== stockCode); } }) // fulfilled: 乐观更新模式下状态已在 pending 更新,这里无需操作 .addCase(toggleWatchlist.fulfilled, () => { // 状态已在 pending 时更新 }); } }); export const { updateQuote, updateQuotes, clearQuotes, clearEventCache, optimisticAddWatchlist, optimisticRemoveWatchlist } = stockSlice.actions; export default stockSlice.reducer;