2
0
Selaa lähdekoodia

✨ feat(stock): 增强股票数据服务功能

- 添加新的dev:stock脚本,支持调试模式下运行服务器
- 定义StockPriceData、StockStopPrice和StockIndicator数据接口
- 实现新的股票API接口调用,支持获取历史分时交易数据
- 添加getStockStopPrice方法获取股票历史涨跌停价格
- 添加getStockIndicators方法获取股票行情指标
- 添加getStockCompleteInfo方法整合股票完整信息
- 优化缓存机制,使用更精确的cacheKey管理不同参数组合的数据
- 重构getStockHistory和getLatestStockData方法,支持更多查询参数

♻️ refactor(stock): 改进数据服务架构

- 提取API基础URL和授权信息到独立方法
- 优化数据库查询条件,使用cacheKey替代简单股票代码
- 添加旧方法的兼容实现,确保平滑过渡
- 改进错误处理和日志记录,提高调试效率
yourname 7 kuukautta sitten
vanhempi
sitoutus
6cfa22833b
2 muutettua tiedostoa jossa 296 lisäystä ja 19 poistoa
  1. 1 0
      package.json
  2. 295 19
      src/server/modules/stock/stock-data.service.ts

+ 1 - 0
package.json

@@ -5,6 +5,7 @@
   "type": "module",
   "scripts": {
     "dev": "PORT=8080 node server",
+    "dev:stock": "PORT=8080 cross-env DEBUG=backend:* node server",
     "build": "npm run build:client && npm run build:server && npm run build:api",
     "build:client": "vite build --outDir dist/client --manifest",
     "build:server": "vite build --ssr src/server/index.tsx --outDir dist/server",

+ 295 - 19
src/server/modules/stock/stock-data.service.ts

@@ -9,43 +9,108 @@ const log = {
   db: debug('backend:db:stock'),
 };
 
+interface StockPriceData {
+  t: string;  // 交易时间
+  o: number;  // 开盘价
+  h: number;  // 最高价
+  l: number;  // 最低价
+  c: number;  // 收盘价
+  v: number;  // 成交量
+  a: number;  // 成交额
+  pc: number; // 前收盘价
+  sf: number; // 停牌状态
+}
+
+interface StockStopPrice {
+  t: string;  // 交易日期
+  h: number;  // 涨停价格
+  l: number;  // 跌停价格
+}
+
+interface StockIndicator {
+  time: string;  // 更新时间
+  lb: number;    // 量比
+  om: number;    // 1分钟涨速(%)
+  fm: number;    // 5分钟涨速(%)
+  '3d': number;  // 3日涨幅(%)
+  '5d': number;  // 5日涨幅(%)
+  '10d': number; // 10日涨幅(%)
+  '3t': number;  // 3日换手(%)
+  '5t': number;  // 5日换手(%)
+  '10t': number; // 10日换手(%)
+}
+
 export class StockDataService extends GenericCrudService<StockData> {
   constructor(dataSource: DataSource) {
     super(dataSource, StockData);
   }
 
+  private getApiBaseUrl(): string {
+    return 'https://api.mairuiapi.com/hsstock';
+  }
+
+  private getLicense(): string {
+    const license = process.env.STOCK_API_LICENSE;
+    if (!license) {
+      throw new Error('STOCK_API_LICENSE environment variable not set');
+    }
+    return license;
+  }
+
   /**
    * 获取股票历史数据
    * 优先从数据库获取,如果没有则调用外部API
+   * 使用新的股票API接口获取历史分时交易数据
    * @param code 股票代码
+   * @param market 市场代码(如SZ、SH)
+   * @param level 分时级别(d日线、w周线、m月线、y年线、5/15/30/60分钟)
+   * @param startDate 开始时间(YYYYMMDD格式)
+   * @param endDate 结束时间(YYYYMMDD格式)
+   * @param limit 获取数据条数
    * @returns 股票历史数据
    */
-  async getStockHistory(code: string = '001339'): Promise<any> {
+  async getStockHistory(
+    code: string,
+    market: string = 'SZ',
+    level: string = 'd',
+    startDate?: string,
+    endDate?: string,
+    limit?: number
+  ): Promise<StockPriceData[]> {
     try {
+      // 构建缓存键
+      const cacheKey = `${code}.${market}_${level}_${startDate || ''}_${endDate || ''}_${limit || ''}`;
+      
       // 查询数据库中是否存在今天的数据
       const today = dayjs().format('YYYY-MM-DD');
       const existingData = await this.repository
         .createQueryBuilder('stock')
-        .where('stock.code = :code', { code })
+        .where('stock.code = :cacheKey', { cacheKey })
         .andWhere('stock.updated_at >= :today', { today: `${today} 00:00:00` })
         .getOne();
 
       if (existingData) {
-        log.db(`Found existing data for ${code} on ${today}`);
-        return existingData.data;
+        log.db(`Found existing data for ${cacheKey} on ${today}`);
+        return existingData.data as StockPriceData[];
       }
 
       // 如果没有今天的数据,调用外部API
-      log.api(`Fetching fresh data for ${code} from external API`);
-      const dh = 'dn'; // 固定值
+      log.api(`Fetching fresh data for ${cacheKey} from external API`);
+      const license = this.getLicense();
+      let apiUrl = `${this.getApiBaseUrl()}/history/${code}.${market}/${level}/n/${license}`;
+      
+      const params = new URLSearchParams();
+      if (startDate) params.append('st', startDate);
+      if (endDate) params.append('et', endDate);
+      if (limit) params.append('lt', limit.toString());
       
-      const license = process.env.STOCK_API_LICENSE;
-      if (!license) {
-        throw new Error('STOCK_API_LICENSE environment variable not set');
+      const queryString = params.toString();
+      if (queryString) {
+        apiUrl += `?${queryString}`;
       }
 
-      const apiUrl = `http://api.mairui.club/hszbl/fsjy/${code}/${dh}/${license}`;
-      console.log('apiUrl', apiUrl)
+      log.api(`Fetching stock history from: ${apiUrl}`);
+      
       const response = await fetch(apiUrl, {
         method: 'GET',
         headers: {
@@ -57,11 +122,11 @@ export class StockDataService extends GenericCrudService<StockData> {
         throw new Error(`API request failed with status ${response.status}`);
       }
 
-      const newData = await response.json();
-
+      const newData: StockPriceData[] = await response.json();
+      
       // 更新或插入数据库
       const stockData = new StockData();
-      stockData.code = code;
+      stockData.code = cacheKey;
       stockData.data = newData;
       
       await this.repository
@@ -74,9 +139,8 @@ export class StockDataService extends GenericCrudService<StockData> {
         )
         .execute();
 
-      log.api(`Successfully saved fresh data for ${code}`);
+      log.api(`Successfully saved fresh data for ${cacheKey}`);
       return newData;
-
     } catch (error) {
       log.api('Error getting stock history:', error);
       throw error;
@@ -85,10 +149,186 @@ export class StockDataService extends GenericCrudService<StockData> {
 
   /**
    * 获取股票最新数据
+   * 优先从数据库获取,如果没有则调用外部API
+   * 使用新的股票API接口获取最新分时交易数据
    * @param code 股票代码
+   * @param market 市场代码(如SZ、SH)
+   * @param level 分时级别(d日线、w周线、m月线、y年线、5/15/30/60分钟)
+   * @param limit 获取数据条数
    * @returns 股票最新数据
    */
-  async getLatestStockData(code: string): Promise<StockData | null> {
+  async getLatestStockData(
+    code: string,
+    market: string = 'SZ',
+    level: string = 'd',
+    limit: number = 1
+  ): Promise<StockPriceData[]> {
+    try {
+      // 构建缓存键
+      const cacheKey = `latest_${code}.${market}_${level}_${limit}`;
+      
+      // 查询数据库中是否存在今天的数据
+      const today = dayjs().format('YYYY-MM-DD');
+      const existingData = await this.repository
+        .createQueryBuilder('stock')
+        .where('stock.code = :cacheKey', { cacheKey })
+        .andWhere('stock.updated_at >= :today', { today: `${today} 00:00:00` })
+        .getOne();
+
+      if (existingData) {
+        log.db(`Found existing latest data for ${cacheKey} on ${today}`);
+        return existingData.data as StockPriceData[];
+      }
+
+      // 如果没有今天的数据,调用外部API
+      log.api(`Fetching fresh latest data for ${cacheKey} from external API`);
+      const license = this.getLicense();
+      const apiUrl = `${this.getApiBaseUrl()}/latest/${code}.${market}/${level}/n/${license}?lt=${limit}`;
+
+      log.api(`Fetching latest stock data from: ${apiUrl}`);
+      
+      const response = await fetch(apiUrl, {
+        method: 'GET',
+        headers: {
+          'Accept': 'application/json'
+        }
+      });
+
+      if (!response.ok) {
+        throw new Error(`API request failed with status ${response.status}`);
+      }
+
+      const newData: StockPriceData[] = await response.json();
+      
+      // 更新或插入数据库
+      const stockData = new StockData();
+      stockData.code = cacheKey;
+      stockData.data = newData;
+      
+      await this.repository
+        .createQueryBuilder()
+        .insert()
+        .values(stockData)
+        .orUpdate(
+          ['data', 'updated_at'],
+          ['code']
+        )
+        .execute();
+
+      log.api(`Successfully saved fresh latest data for ${cacheKey}`);
+      return newData;
+    } catch (error) {
+      log.api('Error getting latest stock data:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 获取股票历史涨跌停价格
+   * @param code 股票代码
+   * @param market 市场代码(如SZ、SH)
+   * @param startDate 开始时间(YYYYMMDD格式)
+   * @param endDate 结束时间(YYYYMMDD格式)
+   * @returns 涨跌停价格数据
+   */
+  async getStockStopPrice(
+    code: string,
+    market: string = 'SZ',
+    startDate?: string,
+    endDate?: string
+  ): Promise<StockStopPrice[]> {
+    try {
+      const license = this.getLicense();
+      let apiUrl = `${this.getApiBaseUrl()}/stopprice/history/${code}.${market}/${license}`;
+      
+      const params = new URLSearchParams();
+      if (startDate) params.append('st', startDate);
+      if (endDate) params.append('et', endDate);
+      
+      const queryString = params.toString();
+      if (queryString) {
+        apiUrl += `?${queryString}`;
+      }
+
+      log.api(`Fetching stock stop price from: ${apiUrl}`);
+      
+      const response = await fetch(apiUrl, {
+        method: 'GET',
+        headers: {
+          'Accept': 'application/json'
+        }
+      });
+
+      if (!response.ok) {
+        throw new Error(`API request failed with status ${response.status}`);
+      }
+
+      const data: StockStopPrice[] = await response.json();
+      log.api(`Successfully fetched ${data.length} stop price records for ${code}.${market}`);
+      
+      return data;
+    } catch (error) {
+      log.api('Error getting stock stop price:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 获取股票行情指标
+   * @param code 股票代码
+   * @param market 市场代码(如SZ、SH)
+   * @param startDate 开始时间(YYYYMMDD格式)
+   * @param endDate 结束时间(YYYYMMDD格式)
+   * @returns 行情指标数据
+   */
+  async getStockIndicators(
+    code: string,
+    market: string = 'SZ',
+    startDate?: string,
+    endDate?: string
+  ): Promise<StockIndicator[]> {
+    try {
+      const license = this.getLicense();
+      let apiUrl = `${this.getApiBaseUrl()}/indicators/${code}.${market}/${license}`;
+      
+      const params = new URLSearchParams();
+      if (startDate) params.append('st', startDate);
+      if (endDate) params.append('et', endDate);
+      
+      const queryString = params.toString();
+      if (queryString) {
+        apiUrl += `?${queryString}`;
+      }
+
+      log.api(`Fetching stock indicators from: ${apiUrl}`);
+      
+      const response = await fetch(apiUrl, {
+        method: 'GET',
+        headers: {
+          'Accept': 'application/json'
+        }
+      });
+
+      if (!response.ok) {
+        throw new Error(`API request failed with status ${response.status}`);
+      }
+
+      const data: StockIndicator[] = await response.json();
+      log.api(`Successfully fetched ${data.length} indicator records for ${code}.${market}`);
+      
+      return data;
+    } catch (error) {
+      log.api('Error getting stock indicators:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * 获取股票最新数据(兼容旧方法)
+   * @param code 股票代码
+   * @returns 股票最新数据
+   */
+  async getLatestStockDataOld(code: string): Promise<StockData | null> {
     try {
       const stockData = await this.repository
         .createQueryBuilder('stock')
@@ -104,7 +344,7 @@ export class StockDataService extends GenericCrudService<StockData> {
   }
 
   /**
-   * 获取多个股票的历史数据
+   * 获取多个股票的历史数据(兼容旧方法)
    * @param codes 股票代码数组
    * @returns 股票历史数据映射
    */
@@ -113,7 +353,8 @@ export class StockDataService extends GenericCrudService<StockData> {
     
     for (const code of codes) {
       try {
-        results[code] = await this.getStockHistory(code);
+        const [stockCode, market] = code.includes('.') ? code.split('.') : [code, 'SZ'];
+        results[code] = await this.getStockHistory(stockCode, market);
       } catch (error) {
         log.api(`Error fetching data for ${code}:`, error);
         results[code] = null;
@@ -122,4 +363,39 @@ export class StockDataService extends GenericCrudService<StockData> {
     
     return results;
   }
+
+  /**
+   * 获取股票完整信息
+   * @param code 股票代码
+   * @param market 市场代码
+   * @returns 包含历史数据、最新数据、涨跌停价格和指标的完整信息
+   */
+  async getStockCompleteInfo(
+    code: string,
+    market: string = 'SZ'
+  ): Promise<{
+    history: StockPriceData[];
+    latest: StockPriceData[];
+    stopPrice: StockStopPrice[];
+    indicators: StockIndicator[];
+  }> {
+    try {
+      const [history, latest, stopPrice, indicators] = await Promise.all([
+        this.getStockHistory(code, market),
+        this.getLatestStockData(code, market),
+        this.getStockStopPrice(code, market),
+        this.getStockIndicators(code, market)
+      ]);
+
+      return {
+        history,
+        latest,
+        stopPrice,
+        indicators
+      };
+    } catch (error) {
+      log.api('Error getting stock complete info:', error);
+      throw error;
+    }
+  }
 }