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

22 KiB
Raw Blame History

StockDetailPanel 原始业务逻辑文档

文档版本: 1.0 组件文件: src/views/Community/components/StockDetailPanel.js 原始行数: 1067 行 创建日期: 2025-10-30 重构前快照: 用于记录重构前的完整业务逻辑


📋 目录

  1. 组件概述
  2. 权限控制系统
  3. 数据加载流程
  4. K线数据缓存机制
  5. 自选股管理
  6. 实时监控功能
  7. 搜索和过滤
  8. UI 交互逻辑
  9. 状态管理
  10. API 端点清单

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=trueevent 对象存在

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. 性能优化要点

  1. 请求去重: 使用 pendingRequests Map 防止重复请求
  2. 智能缓存: 根据交易时段动态调整刷新策略
  3. 并发加载: 5 个 API 请求并发执行
  4. 乐观更新: 自选股操作立即更新 UI无需等待后端响应
  5. 定时器清理: 组件卸载时清理定时器,防止内存泄漏

C. 安全要点

  1. 认证: 所有 API 请求携带 credentials: 'include'
  2. 权限检查: 每个 Tab 渲染前检查用户权限
  3. 错误处理: 所有 API 调用都有 catch 错误处理
  4. 日志记录: 使用 logger 记录关键操作和错误

文档结束

该文档记录了重构前 StockDetailPanel.js 的完整业务逻辑,可作为重构验证的参考基准。