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