Files
vf_react/docs/StockDetailPanel_BUSINESS_LOGIC.md
zdl 09db05c448 docs: 将所有文档迁移到 docs/ 目录
- 移动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>
2025-10-30 14:51:22 +08:00

826 lines
22 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# StockDetailPanel 原始业务逻辑文档
> **文档版本**: 1.0
> **组件文件**: `src/views/Community/components/StockDetailPanel.js`
> **原始行数**: 1067 行
> **创建日期**: 2025-10-30
> **重构前快照**: 用于记录重构前的完整业务逻辑
---
## 📋 目录
1. [组件概述](#1-组件概述)
2. [权限控制系统](#2-权限控制系统)
3. [数据加载流程](#3-数据加载流程)
4. [K线数据缓存机制](#4-k线数据缓存机制)
5. [自选股管理](#5-自选股管理)
6. [实时监控功能](#6-实时监控功能)
7. [搜索和过滤](#7-搜索和过滤)
8. [UI 交互逻辑](#8-ui-交互逻辑)
9. [状态管理](#9-状态管理)
10. [API 端点清单](#10-api-端点清单)
---
## 1. 组件概述
### 1.1 功能描述
StockDetailPanel 是一个 Ant Design Drawer 组件,用于展示事件相关的详细信息,包括:
- **相关标的**: 事件关联的股票列表、实时行情、分时图
- **相关概念**: 事件涉及的概念板块
- **历史事件对比**: 类似历史事件的表现分析
- **传导链分析**: 事件的传导路径和影响链Max 会员功能)
### 1.2 组件属性
```javascript
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 权限检查流程
```javascript
// Hook 初始化
const { hasFeatureAccess, getRequiredLevel, getUpgradeRecommendation } = useSubscription();
// Tab 渲染时检查
hasFeatureAccess('related_stocks') ? (
// 渲染完整功能
) : (
// 渲染锁定提示 UI
renderLockedContent('related_stocks', '相关标的')
)
```
### 2.3 权限拦截机制
**Tab 点击拦截**(已注释,未使用):
```javascript
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 渲染
```javascript
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 升级模态框
```javascript
<SubscriptionUpgradeModal
isOpen={upgradeModalOpen}
onClose={() => setUpgradeModalOpen(false)}
requiredLevel={upgradeFeature} // 'pro' | 'max'
featureName={upgradeFeature === 'pro' ? '相关分析功能' : '传导链分析'}
/>
```
---
## 3. 数据加载流程
### 3.1 加载时机
```javascript
useEffect(() => {
if (visible && event) {
setActiveTab('stocks');
loadAllData();
}
}, [visible, event]);
```
**触发条件**: Drawer 可见 `visible=true``event` 对象存在
### 3.2 并发加载策略
`loadAllData()` 函数同时发起 **5 个独立 API 请求**:
```javascript
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 数据依赖关系
```mermaid
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 加载状态管理
```javascript
// 主加载状态
const [loading, setLoading] = useState(false); // 相关标的加载中
const [detailLoading, setDetailLoading] = useState(false); // 事件详情加载中
// 使用示例
setLoading(true);
eventService.getRelatedStocks(event.id)
.finally(() => setLoading(false));
```
### 3.5 错误处理
```javascript
// 使用 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 缓存**:
```javascript
// 全局缓存(组件级别,不跨实例)
const klineDataCache = new Map(); // 数据缓存: key → data[]
const pendingRequests = new Map(); // 请求去重: key → Promise
const lastRequestTime = new Map(); // 时间戳: key → timestamp
```
### 4.2 缓存键生成
```javascript
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 智能刷新策略
```javascript
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 请求去重机制
```javascript
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 使用缓存
```javascript
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 加载自选股列表
```javascript
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 响应格式**:
```json
{
"success": true,
"data": [
{"stock_code": "600000.SH", "stock_name": "浦发银行"},
{"stock_code": "000001.SZ", "stock_name": "平安银行"}
]
}
```
### 5.2 添加/移除自选股
```javascript
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 集成
```javascript
// 在 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 监控机制
```javascript
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 监控控制
```javascript
const handleMonitoringToggle = () => {
setIsMonitoring(prev => !prev);
};
```
**UI 表现**:
```javascript
<Button
className={`monitoring-button ${isMonitoring ? 'monitoring' : ''}`}
onClick={handleMonitoringToggle}
>
{isMonitoring ? '停止监控' : '实时监控'}
</Button>
<div>每5秒自动更新行情数据</div>
```
### 6.3 组件卸载清理
```javascript
useEffect(() => {
return () => {
// 组件卸载时清理定时器,防止内存泄漏
if (monitoringIntervalRef.current) {
clearInterval(monitoringIntervalRef.current);
}
};
}, []);
```
---
## 7. 搜索和过滤
### 7.1 搜索状态
```javascript
const [searchText, setSearchText] = useState('');
const [filteredStocks, setFilteredStocks] = useState([]);
```
### 7.2 过滤逻辑
```javascript
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
```javascript
<Input
placeholder="搜索股票代码或名称..."
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
allowClear // 显示清除按钮
/>
```
---
## 8. UI 交互逻辑
### 8.1 Tab 切换
```javascript
const [activeTab, setActiveTab] = useState('stocks');
<AntdTabs
activeKey={activeTab}
onChange={setActiveTab} // 直接设置,无拦截
items={tabItems}
/>
```
**Tab 列表**:
```javascript
const tabItems = [
{ key: 'stocks', label: '相关标的', children: ... },
{ key: 'concepts', label: '相关概念', children: ... },
{ key: 'historical', label: '历史事件对比', children: ... },
{ key: 'chain', label: '传导链分析', children: ... }
];
```
### 8.2 固定图表管理
**添加固定图表** (行点击):
```javascript
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' }
});
```
**移除固定图表**:
```javascript
const handleUnfixChart = (stock) => {
setFixedCharts((prev) =>
prev.filter(item => item.stock.stock_code !== stock.stock_code)
);
};
```
**渲染固定图表**:
```javascript
{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 行展开/收起逻辑
```javascript
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 讨论模态框
```javascript
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 (股票)
```typescript
interface Stock {
stock_code: string; // 股票代码, 如 "600000.SH"
stock_name: string; // 股票名称, 如 "浦发银行"
relation_desc: string | { // 关联描述
data: Array<{
query_part?: string;
sentences?: string;
}>
};
}
```
#### Quote (行情)
```typescript
interface Quote {
change: number; // 涨跌幅 (百分比)
price: number; // 当前价格
volume: number; // 成交量
// ... 其他字段
}
```
#### Event (事件)
```typescript
interface Event {
id: string; // 事件 ID
title: string; // 事件标题
start_time: string; // 事件开始时间 (ISO 8601)
created_at: string; // 创建时间
// ... 其他字段
}
```
### B. 性能优化要点
1. **请求去重**: 使用 `pendingRequests` Map 防止重复请求
2. **智能缓存**: 根据交易时段动态调整刷新策略
3. **并发加载**: 5 个 API 请求并发执行
4. **乐观更新**: 自选股操作立即更新 UI无需等待后端响应
5. **定时器清理**: 组件卸载时清理定时器,防止内存泄漏
### C. 安全要点
1. **认证**: 所有 API 请求携带 credentials: 'include'
2. **权限检查**: 每个 Tab 渲染前检查用户权限
3. **错误处理**: 所有 API 调用都有 catch 错误处理
4. **日志记录**: 使用 logger 记录关键操作和错误
---
**文档结束**
> 该文档记录了重构前 StockDetailPanel.js 的完整业务逻辑,可作为重构验证的参考基准。