- 移动42个文档文件到 docs/ 目录 - 更新 .gitignore 允许 docs/ 下的 .md 文件 - 删除根目录下的重复文档文件 📁 文档分类: - StockDetailPanel 重构文档(3个) - PostHog 集成文档(6个) - 系统架构和API文档(33个) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
22 KiB
22 KiB
StockDetailPanel 原始业务逻辑文档
文档版本: 1.0 组件文件:
src/views/Community/components/StockDetailPanel.js原始行数: 1067 行 创建日期: 2025-10-30 重构前快照: 用于记录重构前的完整业务逻辑
📋 目录
1. 组件概述
1.1 功能描述
StockDetailPanel 是一个 Ant Design Drawer 组件,用于展示事件相关的详细信息,包括:
- 相关标的: 事件关联的股票列表、实时行情、分时图
- 相关概念: 事件涉及的概念板块
- 历史事件对比: 类似历史事件的表现分析
- 传导链分析: 事件的传导路径和影响链(Max 会员功能)
1.2 组件属性
StockDetailPanel({
visible, // boolean - 是否显示 Drawer
event, // Object - 事件对象 {id, title, start_time, created_at, ...}
onClose // Function - 关闭回调
})
1.3 核心依赖
- useSubscription: 订阅权限管理 hook
- eventService: 事件数据 API 服务
- stockService: 股票数据 API 服务
- logger: 日志工具
2. 权限控制系统
2.1 权限层级
系统采用三层订阅模型:
| 功能 | 权限标识 | 所需版本 | 图标 |
|---|---|---|---|
| 相关标的 | related_stocks |
Pro | 🔒 |
| 相关概念 | related_concepts |
Pro | 🔒 |
| 历史事件对比 | historical_events_full |
Pro | 🔒 |
| 传导链分析 | transmission_chain |
Max | 👑 |
2.2 权限检查流程
// Hook 初始化
const { hasFeatureAccess, getRequiredLevel, getUpgradeRecommendation } = useSubscription();
// Tab 渲染时检查
hasFeatureAccess('related_stocks') ? (
// 渲染完整功能
) : (
// 渲染锁定提示 UI
renderLockedContent('related_stocks', '相关标的')
)
2.3 权限拦截机制
Tab 点击拦截(已注释,未使用):
const handleTabAccess = (featureName, tabKey) => {
if (!hasFeatureAccess(featureName)) {
const recommendation = getUpgradeRecommendation(featureName);
setUpgradeFeature(recommendation?.required || 'pro');
setUpgradeModalOpen(true);
return false; // 阻止 Tab 切换
}
setActiveTab(tabKey);
return true;
};
2.4 锁定 UI 渲染
const renderLockedContent = (featureName, description) => {
const recommendation = getUpgradeRecommendation(featureName);
const isProRequired = recommendation?.required === 'pro';
return (
<div>
{/* 图标: Pro版显示🔒, Max版显示👑 */}
<LockOutlined /> or <CrownOutlined />
{/* 提示消息 */}
<Alert message={`${description}功能已锁定`} />
{/* 升级按钮 */}
<Button onClick={() => setUpgradeModalOpen(true)}>
升级到 {isProRequired ? 'Pro版' : 'Max版'}
</Button>
</div>
);
};
2.5 升级模态框
<SubscriptionUpgradeModal
isOpen={upgradeModalOpen}
onClose={() => setUpgradeModalOpen(false)}
requiredLevel={upgradeFeature} // 'pro' | 'max'
featureName={upgradeFeature === 'pro' ? '相关分析功能' : '传导链分析'}
/>
3. 数据加载流程
3.1 加载时机
useEffect(() => {
if (visible && event) {
setActiveTab('stocks');
loadAllData();
}
}, [visible, event]);
触发条件: Drawer 可见 visible=true 且 event 对象存在
3.2 并发加载策略
loadAllData() 函数同时发起 5 个独立 API 请求:
const loadAllData = () => {
// 1. 加载用户自选股列表 (独立调用)
loadWatchlist();
// 2. 加载相关标的 → 连锁加载行情数据
eventService.getRelatedStocks(event.id)
.then(res => {
setRelatedStocks(res.data);
// 2.1 如果有股票,立即加载行情
if (res.data.length > 0) {
const codes = res.data.map(s => s.stock_code);
stockService.getQuotes(codes, event.created_at)
.then(quotes => setStockQuotes(quotes));
}
});
// 3. 加载事件详情
eventService.getEventDetail(event.id)
.then(res => setEventDetail(res.data));
// 4. 加载历史事件
eventService.getHistoricalEvents(event.id)
.then(res => setHistoricalEvents(res.data));
// 5. 加载传导链分析
eventService.getTransmissionChainAnalysis(event.id)
.then(res => setChainAnalysis(res.data));
// 6. 加载超预期得分
eventService.getExpectationScore(event.id)
.then(res => setExpectationScore(res.data));
};
3.3 数据依赖关系
graph TD
A[loadAllData] --> B[getRelatedStocks]
A --> C[getEventDetail]
A --> D[getHistoricalEvents]
A --> E[getTransmissionChainAnalysis]
A --> F[getExpectationScore]
A --> G[loadWatchlist]
B -->|成功且有数据| H[getQuotes]
B --> I[setRelatedStocks]
H --> J[setStockQuotes]
C --> K[setEventDetail]
D --> L[setHistoricalEvents]
E --> M[setChainAnalysis]
F --> N[setExpectationScore]
G --> O[setWatchlistStocks]
3.4 加载状态管理
// 主加载状态
const [loading, setLoading] = useState(false); // 相关标的加载中
const [detailLoading, setDetailLoading] = useState(false); // 事件详情加载中
// 使用示例
setLoading(true);
eventService.getRelatedStocks(event.id)
.finally(() => setLoading(false));
3.5 错误处理
// 使用 logger 记录错误
stockService.getQuotes(codes, event.created_at)
.catch(error => logger.error('StockDetailPanel', 'getQuotes', error, {
stockCodes: codes,
eventTime: event.created_at
}));
4. K线数据缓存机制
4.1 缓存架构
三层 Map 缓存:
// 全局缓存(组件级别,不跨实例)
const klineDataCache = new Map(); // 数据缓存: key → data[]
const pendingRequests = new Map(); // 请求去重: key → Promise
const lastRequestTime = new Map(); // 时间戳: key → timestamp
4.2 缓存键生成
const getCacheKey = (stockCode, eventTime) => {
const date = eventTime
? moment(eventTime).format('YYYY-MM-DD')
: moment().format('YYYY-MM-DD');
return `${stockCode}|${date}`;
};
// 示例: "600000.SH|2024-10-30"
4.3 智能刷新策略
const shouldRefreshData = (cacheKey) => {
const lastTime = lastRequestTime.get(cacheKey);
if (!lastTime) return true; // 无缓存,需要刷新
const now = Date.now();
const elapsed = now - lastTime;
// 检测是否为当日交易时段
const today = moment().format('YYYY-MM-DD');
const isToday = cacheKey.includes(today);
const currentHour = new Date().getHours();
const isTradingHours = currentHour >= 9 && currentHour < 16;
if (isToday && isTradingHours) {
return elapsed > 30000; // 交易时段: 30秒刷新
}
return elapsed > 3600000; // 非交易时段/历史数据: 1小时刷新
};
| 场景 | 刷新间隔 | 原因 |
|---|---|---|
| 当日 + 交易时段 (9:00-16:00) | 30 秒 | 实时性要求高 |
| 当日 + 非交易时段 | 1 小时 | 数据不会变化 |
| 历史日期 | 1 小时 | 数据固定不变 |
4.4 请求去重机制
const fetchKlineData = async (stockCode, eventTime) => {
const cacheKey = getCacheKey(stockCode, eventTime);
// 1️⃣ 检查缓存
if (klineDataCache.has(cacheKey) && !shouldRefreshData(cacheKey)) {
return klineDataCache.get(cacheKey); // 直接返回缓存
}
// 2️⃣ 检查是否有进行中的请求(防止重复请求)
if (pendingRequests.has(cacheKey)) {
return pendingRequests.get(cacheKey); // 返回同一个 Promise
}
// 3️⃣ 发起新请求
const requestPromise = stockService
.getKlineData(stockCode, 'minute', eventTime)
.then((res) => {
const data = Array.isArray(res?.data) ? res.data : [];
// 更新缓存
klineDataCache.set(cacheKey, data);
lastRequestTime.set(cacheKey, Date.now());
// 清除 pending 状态
pendingRequests.delete(cacheKey);
return data;
})
.catch((error) => {
pendingRequests.delete(cacheKey);
// 如果有旧缓存,返回旧数据
if (klineDataCache.has(cacheKey)) {
return klineDataCache.get(cacheKey);
}
return [];
});
// 保存到 pending
pendingRequests.set(cacheKey, requestPromise);
return requestPromise;
};
去重效果:
- 同时有 10 个组件请求同一只股票的同一天数据
- 实际只会发出 1 个 API 请求
- 其他 9 个请求共享同一个 Promise
4.5 MiniTimelineChart 使用缓存
const MiniTimelineChart = ({ stockCode, eventTime }) => {
useEffect(() => {
// 检查缓存
const cacheKey = getCacheKey(stockCode, eventTime);
const cachedData = klineDataCache.get(cacheKey);
if (cachedData && cachedData.length > 0) {
setData(cachedData); // 使用缓存
return;
}
// 无缓存,发起请求
fetchKlineData(stockCode, eventTime)
.then(result => setData(result));
}, [stockCode, eventTime]);
};
5. 自选股管理
5.1 加载自选股列表
const loadWatchlist = async () => {
const apiBase = getApiBase(); // 根据环境获取 API base URL
const response = await fetch(`${apiBase}/api/account/watchlist`, {
credentials: 'include' // ⚠️ 关键: 发送 cookies 进行认证
});
const data = await response.json();
if (data.success && data.data) {
// 转换为 Set 数据结构,便于快速查找
const watchlistSet = new Set(data.data.map(item => item.stock_code));
setWatchlistStocks(watchlistSet);
}
};
API 响应格式:
{
"success": true,
"data": [
{"stock_code": "600000.SH", "stock_name": "浦发银行"},
{"stock_code": "000001.SZ", "stock_name": "平安银行"}
]
}
5.2 添加/移除自选股
const handleWatchlistToggle = async (stockCode, 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 {
// ➕ 添加操作
const stockInfo = relatedStocks.find(s => s.stock_code === stockCode);
response = await fetch(`${apiBase}/api/account/watchlist`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
stock_code: stockCode,
stock_name: stockInfo?.stock_name || stockCode
})
});
}
const data = await response.json();
if (data.success) {
message.success(isInWatchlist ? '已从自选股移除' : '已加入自选股');
// 更新本地状态(乐观更新)
setWatchlistStocks(prev => {
const newSet = new Set(prev);
isInWatchlist ? newSet.delete(stockCode) : newSet.add(stockCode);
return newSet;
});
} else {
message.error(data.error || '操作失败');
}
};
5.3 UI 集成
// 在 StockTable 的"操作"列中
{
title: '操作',
render: (_, record) => {
const isInWatchlist = watchlistStocks.has(record.stock_code);
return (
<Button
type={isInWatchlist ? 'default' : 'primary'}
icon={isInWatchlist ? <StarFilled /> : <StarOutlined />}
onClick={(e) => {
e.stopPropagation(); // 防止触发行点击
handleWatchlistToggle(record.stock_code, isInWatchlist);
}}
>
{isInWatchlist ? '已关注' : '加自选'}
</Button>
);
}
}
6. 实时监控功能
6.1 监控机制
const [isMonitoring, setIsMonitoring] = useState(false);
const monitoringIntervalRef = useRef(null);
useEffect(() => {
// 清理旧定时器
if (monitoringIntervalRef.current) {
clearInterval(monitoringIntervalRef.current);
monitoringIntervalRef.current = null;
}
if (isMonitoring && relatedStocks.length > 0) {
// 定义更新函数
const updateQuotes = () => {
const codes = relatedStocks.map(s => s.stock_code);
stockService.getQuotes(codes, event?.created_at)
.then(quotes => setStockQuotes(quotes))
.catch(error => logger.error('...', error));
};
// 立即执行一次
updateQuotes();
// 设置定时器: 每 5 秒刷新
monitoringIntervalRef.current = setInterval(updateQuotes, 5000);
}
// 清理函数
return () => {
if (monitoringIntervalRef.current) {
clearInterval(monitoringIntervalRef.current);
monitoringIntervalRef.current = null;
}
};
}, [isMonitoring, relatedStocks, event]);
6.2 监控控制
const handleMonitoringToggle = () => {
setIsMonitoring(prev => !prev);
};
UI 表现:
<Button
className={`monitoring-button ${isMonitoring ? 'monitoring' : ''}`}
onClick={handleMonitoringToggle}
>
{isMonitoring ? '停止监控' : '实时监控'}
</Button>
<div>每5秒自动更新行情数据</div>
6.3 组件卸载清理
useEffect(() => {
return () => {
// 组件卸载时清理定时器,防止内存泄漏
if (monitoringIntervalRef.current) {
clearInterval(monitoringIntervalRef.current);
}
};
}, []);
7. 搜索和过滤
7.1 搜索状态
const [searchText, setSearchText] = useState('');
const [filteredStocks, setFilteredStocks] = useState([]);
7.2 过滤逻辑
useEffect(() => {
if (!searchText.trim()) {
setFilteredStocks(relatedStocks); // 无搜索词,显示全部
} else {
const filtered = relatedStocks.filter(stock =>
stock.stock_code.toLowerCase().includes(searchText.toLowerCase()) ||
stock.stock_name.toLowerCase().includes(searchText.toLowerCase())
);
setFilteredStocks(filtered);
}
}, [searchText, relatedStocks]);
搜索特性:
- 不区分大小写
- 同时匹配股票代码和股票名称
- 实时过滤(每次输入都触发)
7.3 搜索 UI
<Input
placeholder="搜索股票代码或名称..."
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
allowClear // 显示清除按钮
/>
8. UI 交互逻辑
8.1 Tab 切换
const [activeTab, setActiveTab] = useState('stocks');
<AntdTabs
activeKey={activeTab}
onChange={setActiveTab} // 直接设置,无拦截
items={tabItems}
/>
Tab 列表:
const tabItems = [
{ key: 'stocks', label: '相关标的', children: ... },
{ key: 'concepts', label: '相关概念', children: ... },
{ key: 'historical', label: '历史事件对比', children: ... },
{ key: 'chain', label: '传导链分析', children: ... }
];
8.2 固定图表管理
添加固定图表 (行点击):
const handleRowEvents = (record) => ({
onClick: () => {
setFixedCharts((prev) => {
// 防止重复添加
if (prev.find(item => item.stock.stock_code === record.stock_code)) {
return prev;
}
return [...prev, { stock: record, chartType: 'timeline' }];
});
},
style: { cursor: 'pointer' }
});
移除固定图表:
const handleUnfixChart = (stock) => {
setFixedCharts((prev) =>
prev.filter(item => item.stock.stock_code !== stock.stock_code)
);
};
渲染固定图表:
{fixedCharts.map(({ stock }, index) => (
<StockChartAntdModal
key={`fixed-chart-${stock.stock_code}-${index}`}
open={true}
onCancel={() => handleUnfixChart(stock)}
stock={stock}
eventTime={formattedEventTime}
fixed={true}
/>
))}
8.3 行展开/收起逻辑
const [expandedRows, setExpandedRows] = useState(new Set());
const toggleRowExpand = (stockCode) => {
setExpandedRows(prev => {
const newSet = new Set(prev);
newSet.has(stockCode) ? newSet.delete(stockCode) : newSet.add(stockCode);
return newSet;
});
};
应用场景: 关联描述文本过长时的展开/收起
8.4 讨论模态框
const [discussionModalVisible, setDiscussionModalVisible] = useState(false);
const [discussionType, setDiscussionType] = useState('事件讨论');
<Button onClick={() => {
setDiscussionType('事件讨论');
setDiscussionModalVisible(true);
}}>
查看事件讨论
</Button>
<EventDiscussionModal
isOpen={discussionModalVisible}
onClose={() => setDiscussionModalVisible(false)}
eventId={event?.id}
eventTitle={event?.title}
discussionType={discussionType}
/>
9. 状态管理
9.1 状态清单
| 状态名 | 类型 | 初始值 | 用途 |
|---|---|---|---|
activeTab |
string | 'stocks' |
当前激活的 Tab |
loading |
boolean | false |
相关标的加载状态 |
detailLoading |
boolean | false |
事件详情加载状态 |
relatedStocks |
Array | [] |
相关股票列表 |
stockQuotes |
Object | {} |
股票行情字典 |
selectedStock |
Object | null |
当前选中的股票(未使用) |
chartData |
Object | null |
图表数据(未使用) |
eventDetail |
Object | null |
事件详情 |
historicalEvents |
Array | [] |
历史事件列表 |
chainAnalysis |
Object | null |
传导链分析数据 |
posts |
Array | [] |
讨论帖子(未使用) |
fixedCharts |
Array | [] |
固定图表列表 |
searchText |
string | '' |
搜索文本 |
isMonitoring |
boolean | false |
实时监控开关 |
filteredStocks |
Array | [] |
过滤后的股票列表 |
expectationScore |
Object | null |
超预期得分 |
watchlistStocks |
Set | new Set() |
自选股集合 |
discussionModalVisible |
boolean | false |
讨论模态框可见性 |
discussionType |
string | '事件讨论' |
讨论类型 |
upgradeModalOpen |
boolean | false |
升级模态框可见性 |
upgradeFeature |
string | '' |
需要升级的功能 |
9.2 Ref 引用
| Ref 名 | 用途 |
|---|---|
monitoringIntervalRef |
存储监控定时器 ID |
tableRef |
Table 组件引用(未使用) |
10. API 端点清单
10.1 事件相关 API
| API | 方法 | 参数 | 返回数据 | 用途 |
|---|---|---|---|---|
eventService.getRelatedStocks(eventId) |
GET | 事件ID | { success, data: Stock[] } |
获取相关股票 |
eventService.getEventDetail(eventId) |
GET | 事件ID | { success, data: EventDetail } |
获取事件详情 |
eventService.getHistoricalEvents(eventId) |
GET | 事件ID | { success, data: Event[] } |
获取历史事件 |
eventService.getTransmissionChainAnalysis(eventId) |
GET | 事件ID | { success, data: ChainAnalysis } |
获取传导链分析 |
eventService.getExpectationScore(eventId) |
GET | 事件ID | { success, data: Score } |
获取超预期得分 |
10.2 股票相关 API
| API | 方法 | 参数 | 返回数据 | 用途 |
|---|---|---|---|---|
stockService.getQuotes(codes[], eventTime) |
GET | 股票代码数组, 事件时间 | { [code]: Quote } |
批量获取行情 |
stockService.getKlineData(code, type, eventTime) |
GET | 股票代码, K线类型, 事件时间 | { success, data: Kline[] } |
获取K线数据 |
K线类型: 'minute' (分时), 'day' (日K), 'week' (周K), 'month' (月K)
10.3 自选股 API
| API | 方法 | 请求体 | 返回数据 | 用途 |
|---|---|---|---|---|
GET /api/account/watchlist |
GET | - | { success, data: Watchlist[] } |
获取自选股列表 |
POST /api/account/watchlist |
POST | { stock_code, stock_name } |
{ success } |
添加自选股 |
DELETE /api/account/watchlist/:code |
DELETE | - | { success } |
移除自选股 |
认证方式: 所有 API 都使用 credentials: 'include' 携带 cookies
📝 附录
A. 数据结构定义
Stock (股票)
interface Stock {
stock_code: string; // 股票代码, 如 "600000.SH"
stock_name: string; // 股票名称, 如 "浦发银行"
relation_desc: string | { // 关联描述
data: Array<{
query_part?: string;
sentences?: string;
}>
};
}
Quote (行情)
interface Quote {
change: number; // 涨跌幅 (百分比)
price: number; // 当前价格
volume: number; // 成交量
// ... 其他字段
}
Event (事件)
interface Event {
id: string; // 事件 ID
title: string; // 事件标题
start_time: string; // 事件开始时间 (ISO 8601)
created_at: string; // 创建时间
// ... 其他字段
}
B. 性能优化要点
- 请求去重: 使用
pendingRequestsMap 防止重复请求 - 智能缓存: 根据交易时段动态调整刷新策略
- 并发加载: 5 个 API 请求并发执行
- 乐观更新: 自选股操作立即更新 UI,无需等待后端响应
- 定时器清理: 组件卸载时清理定时器,防止内存泄漏
C. 安全要点
- 认证: 所有 API 请求携带 credentials: 'include'
- 权限检查: 每个 Tab 渲染前检查用户权限
- 错误处理: 所有 API 调用都有 catch 错误处理
- 日志记录: 使用 logger 记录关键操作和错误
文档结束
该文档记录了重构前 StockDetailPanel.js 的完整业务逻辑,可作为重构验证的参考基准。