Przeglądaj źródła

股票搜索修改

Zhangbw 3 miesięcy temu
rodzic
commit
5b86ffa2d1

+ 11 - 0
src/main/java/com/yingpai/gupiao/domain/vo/StockDetailResponse.java

@@ -15,6 +15,17 @@ public class StockDetailResponse {
     private Double score;
     private List<HistoryScore> history;
     private List<Factor> factors;
+    
+    // 实时行情数据
+    private String currentPrice;    // 当前价格
+    private String priceChange;     // 涨跌额
+    private String changePercent;   // 涨跌幅
+    private String openPrice;       // 开盘价
+    private String highPrice;       // 最高价
+    private String lowPrice;        // 最低价
+    private String volume;          // 成交量
+    private String amount;          // 成交额
+    private String turnoverRate;    // 换手率
 
     @Data
     @NoArgsConstructor

+ 10 - 0
src/main/java/com/yingpai/gupiao/domain/vo/StockPoolVO.java

@@ -33,4 +33,14 @@ public class StockPoolVO {
      * 加入日期
      */
     private String addDate;
+    
+    /**
+     * 当前价格
+     */
+    private String currentPrice;
+    
+    /**
+     * 涨跌幅
+     */
+    private String changePercent;
 }

+ 124 - 3
src/main/java/com/yingpai/gupiao/service/impl/StockPoolServiceImpl.java

@@ -1,17 +1,30 @@
 package com.yingpai.gupiao.service.impl;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import com.yingpai.gupiao.domain.po.StockPool;
 import com.yingpai.gupiao.domain.vo.StockPoolVO;
 import com.yingpai.gupiao.mapper.StockPoolMapper;
 import com.yingpai.gupiao.service.StockPoolService;
+import com.yingpai.gupiao.util.StockUtils;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.time.Duration;
 import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
 
 /**
  * 股票池服务实现
@@ -25,6 +38,15 @@ public class StockPoolServiceImpl implements StockPoolService {
     
     private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
     
+    // 东方财富批量行情接口
+    private static final String BATCH_QUOTE_URL = "http://push2.eastmoney.com/api/qt/ulist.np/get";
+    
+    private final HttpClient httpClient = HttpClient.newBuilder()
+            .connectTimeout(Duration.ofSeconds(5))
+            .build();
+    
+    private final ObjectMapper objectMapper = new ObjectMapper();
+    
     @Override
     public List<StockPoolVO> getPoolStocks(Integer poolType) {
         log.info("[获取股票池] poolType={}", poolType);
@@ -37,17 +59,116 @@ public class StockPoolServiceImpl implements StockPoolService {
         List<StockPool> stocks = stockPoolMapper.selectList(wrapper);
         log.info("[获取股票池] 查询到 {} 条记录", stocks.size());
         
+        // 获取实时行情
+        Map<String, QuoteData> quoteMap = fetchBatchQuotes(stocks);
+        
         List<StockPoolVO> result = new ArrayList<>();
         for (StockPool stock : stocks) {
-            StockPoolVO vo = StockPoolVO.builder()
+            StockPoolVO.StockPoolVOBuilder builder = StockPoolVO.builder()
                     .code(stock.getStockCode())
                     .name(stock.getStockName())
                     .addPrice(stock.getAddPrice() != null ? stock.getAddPrice().toString() : "")
-                    .addDate(stock.getAddDate() != null ? stock.getAddDate().format(DATE_FORMATTER) : "")
+                    .addDate(stock.getAddDate() != null ? stock.getAddDate().format(DATE_FORMATTER) : "");
+            
+            // 填充实时行情
+            QuoteData quote = quoteMap.get(stock.getStockCode());
+            if (quote != null) {
+                builder.currentPrice(quote.currentPrice)
+                       .changePercent(quote.changePercent);
+            }
+            
+            result.add(builder.build());
+        }
+        
+        return result;
+    }
+    
+    /**
+     * 批量获取股票行情
+     */
+    private Map<String, QuoteData> fetchBatchQuotes(List<StockPool> stocks) {
+        Map<String, QuoteData> result = new HashMap<>();
+        if (stocks == null || stocks.isEmpty()) {
+            return result;
+        }
+        
+        try {
+            // 构建secids参数
+            String secids = stocks.stream()
+                    .map(s -> StockUtils.getAShareSecId(s.getStockCode()))
+                    .filter(s -> s != null)
+                    .collect(Collectors.joining(","));
+            
+            if (secids.isEmpty()) {
+                return result;
+            }
+            
+            // f12:代码, f14:名称, f2:最新价, f3:涨跌幅
+            String url = BATCH_QUOTE_URL + "?fltt=2&fields=f12,f14,f2,f3&secids=" + secids;
+            
+            HttpRequest request = HttpRequest.newBuilder()
+                    .uri(URI.create(url))
+                    .GET()
                     .build();
-            result.add(vo);
+            
+            HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
+            
+            if (response.statusCode() == 200) {
+                JsonNode root = objectMapper.readTree(response.body());
+                JsonNode data = root.get("data");
+                
+                if (data != null && !data.isNull()) {
+                    JsonNode diff = data.get("diff");
+                    if (diff != null && diff.isArray()) {
+                        for (JsonNode item : diff) {
+                            String code = item.has("f12") ? item.get("f12").asText() : null;
+                            if (code != null) {
+                                QuoteData quote = new QuoteData();
+                                quote.currentPrice = formatPrice(item, "f2");
+                                quote.changePercent = formatPercent(item, "f3");
+                                result.put(code, quote);
+                            }
+                        }
+                    }
+                }
+            }
+        } catch (Exception e) {
+            log.error("[批量行情] 获取失败: {}", e.getMessage());
         }
         
         return result;
     }
+    
+    private String formatPrice(JsonNode node, String field) {
+        if (!node.has(field) || node.get(field).isNull()) {
+            return "-";
+        }
+        try {
+            BigDecimal val = new BigDecimal(node.get(field).asText());
+            return val.setScale(2, RoundingMode.HALF_UP).toString();
+        } catch (Exception e) {
+            return "-";
+        }
+    }
+    
+    private String formatPercent(JsonNode node, String field) {
+        if (!node.has(field) || node.get(field).isNull()) {
+            return "-";
+        }
+        try {
+            BigDecimal val = new BigDecimal(node.get(field).asText());
+            String sign = val.compareTo(BigDecimal.ZERO) >= 0 ? "+" : "";
+            return sign + val.setScale(2, RoundingMode.HALF_UP) + "%";
+        } catch (Exception e) {
+            return "-";
+        }
+    }
+    
+    /**
+     * 行情数据内部类
+     */
+    private static class QuoteData {
+        String currentPrice;
+        String changePercent;
+    }
 }

+ 185 - 0
src/main/java/com/yingpai/gupiao/service/impl/StockSearchServiceImpl.java

@@ -1,24 +1,45 @@
 package com.yingpai.gupiao.service.impl;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import com.yingpai.gupiao.domain.dto.StockSearchRequest;
 import com.yingpai.gupiao.domain.dto.StockSuggestionDTO;
 import com.yingpai.gupiao.domain.po.StockInfo;
 import com.yingpai.gupiao.domain.vo.StockDetailResponse;
 import com.yingpai.gupiao.mapper.StockInfoMapper;
 import com.yingpai.gupiao.service.StockSearchService;
+import com.yingpai.gupiao.util.StockUtils;
 import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 import org.springframework.util.StringUtils;
 
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.time.Duration;
 import java.util.List;
 import java.util.stream.Collectors;
 
+@Slf4j
 @Service
 @AllArgsConstructor
 public class StockSearchServiceImpl implements StockSearchService {
 
     private final StockInfoMapper stockInfoMapper;
+    
+    // 东方财富通用行情接口
+    private static final String BASE_URL = "http://push2.eastmoney.com/api/qt/stock/get";
+    
+    private final HttpClient httpClient = HttpClient.newBuilder()
+            .connectTimeout(Duration.ofSeconds(3))
+            .build();
+
+    private final ObjectMapper objectMapper = new ObjectMapper();
 
     @Override
     public StockDetailResponse search(StockSearchRequest request) {
@@ -111,7 +132,171 @@ public class StockSearchServiceImpl implements StockSearchService {
         // 简化返回,暂时不返回历史数据和因子数据
         response.setHistory(null);
         response.setFactors(null);
+        
+        // 获取实时行情数据
+        fetchRealTimeQuote(stockInfo.getStockCode(), response);
 
         return response;
     }
+    
+    /**
+     * 从东方财富API获取实时行情数据
+     */
+    private void fetchRealTimeQuote(String stockCode, StockDetailResponse response) {
+        String secId = StockUtils.getAShareSecId(stockCode);
+        if (secId == null) {
+            log.warn("[实时行情] 无法获取secId: {}", stockCode);
+            return;
+        }
+        
+        // f43: 最新价, f44: 最高价, f45: 最低价, f46: 开盘价
+        // f47: 成交量, f48: 成交额, f168: 换手率
+        // f169: 涨跌额, f170: 涨跌幅
+        String params = String.format(
+            "?invt=2&fltt=2&fields=f43,f44,f45,f46,f47,f48,f168,f169,f170&secid=%s", 
+            secId
+        );
+        String fullUrl = BASE_URL + params;
+        
+        HttpRequest request = HttpRequest.newBuilder()
+                .uri(URI.create(fullUrl))
+                .GET()
+                .build();
+        
+        try {
+            HttpResponse<String> httpResponse = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
+            log.debug("[实时行情 {}] 响应: {}", stockCode, httpResponse.body());
+            
+            if (httpResponse.statusCode() == 200) {
+                JsonNode root = objectMapper.readTree(httpResponse.body());
+                JsonNode data = root.get("data");
+                
+                if (data != null && !data.isNull()) {
+                    // 当前价格
+                    BigDecimal currentPrice = parseBigDecimal(data, "f43");
+                    response.setCurrentPrice(currentPrice != null ? currentPrice.toString() : null);
+                    
+                    // 涨跌额
+                    BigDecimal priceChange = parseBigDecimal(data, "f169");
+                    response.setPriceChange(formatWithSign(priceChange));
+                    
+                    // 涨跌幅
+                    BigDecimal changePercent = parseBigDecimal(data, "f170");
+                    response.setChangePercent(formatPercent(changePercent));
+                    
+                    // 开盘价
+                    BigDecimal openPrice = parseBigDecimal(data, "f46");
+                    response.setOpenPrice(openPrice != null ? openPrice.toString() : null);
+                    
+                    // 最高价
+                    BigDecimal highPrice = parseBigDecimal(data, "f44");
+                    response.setHighPrice(highPrice != null ? highPrice.toString() : null);
+                    
+                    // 最低价
+                    BigDecimal lowPrice = parseBigDecimal(data, "f45");
+                    response.setLowPrice(lowPrice != null ? lowPrice.toString() : null);
+                    
+                    // 成交量(手)
+                    BigDecimal volume = parseBigDecimal(data, "f47");
+                    response.setVolume(formatVolume(volume));
+                    
+                    // 成交额(元)
+                    BigDecimal amount = parseBigDecimal(data, "f48");
+                    response.setAmount(formatAmount(amount));
+                    
+                    // 换手率
+                    BigDecimal turnoverRate = parseBigDecimal(data, "f168");
+                    response.setTurnoverRate(formatPercent(turnoverRate));
+                }
+            }
+        } catch (Exception e) {
+            log.error("[实时行情 {}] 获取失败: {}", stockCode, e.getMessage());
+        }
+    }
+    
+    /**
+     * 安全地从 JsonNode 中解析 BigDecimal
+     */
+    private BigDecimal parseBigDecimal(JsonNode data, String fieldName) {
+        if (!data.has(fieldName)) {
+            return null;
+        }
+        
+        JsonNode fieldNode = data.get(fieldName);
+        if (fieldNode == null || fieldNode.isNull()) {
+            return null;
+        }
+        
+        String value = fieldNode.asText();
+        if (value == null || value.trim().isEmpty() || value.equals("-")) {
+            return null;
+        }
+        
+        try {
+            return new BigDecimal(value);
+        } catch (NumberFormatException e) {
+            log.warn("无法解析数字字段 [{}]: {}", fieldName, value);
+            return null;
+        }
+    }
+    
+    /**
+     * 格式化带符号的数字(涨跌额)
+     */
+    private String formatWithSign(BigDecimal value) {
+        if (value == null) {
+            return null;
+        }
+        if (value.compareTo(BigDecimal.ZERO) > 0) {
+            return "+" + value.setScale(2, RoundingMode.HALF_UP);
+        }
+        return value.setScale(2, RoundingMode.HALF_UP).toString();
+    }
+    
+    /**
+     * 格式化百分比(涨跌幅、换手率)
+     */
+    private String formatPercent(BigDecimal value) {
+        if (value == null) {
+            return null;
+        }
+        if (value.compareTo(BigDecimal.ZERO) > 0) {
+            return "+" + value.setScale(2, RoundingMode.HALF_UP) + "%";
+        }
+        return value.setScale(2, RoundingMode.HALF_UP) + "%";
+    }
+    
+    /**
+     * 格式化成交量(手)
+     */
+    private String formatVolume(BigDecimal value) {
+        if (value == null) {
+            return null;
+        }
+        // 转换为万手
+        if (value.compareTo(new BigDecimal("10000")) >= 0) {
+            return value.divide(new BigDecimal("10000"), 2, RoundingMode.HALF_UP) + "万手";
+        }
+        return value.setScale(0, RoundingMode.HALF_UP) + "手";
+    }
+    
+    /**
+     * 格式化成交额(元)
+     */
+    private String formatAmount(BigDecimal value) {
+        if (value == null) {
+            return null;
+        }
+        // 转换为亿
+        BigDecimal yi = new BigDecimal("100000000");
+        if (value.compareTo(yi) >= 0) {
+            return value.divide(yi, 2, RoundingMode.HALF_UP) + "亿";
+        }
+        // 转换为万
+        BigDecimal wan = new BigDecimal("10000");
+        if (value.compareTo(wan) >= 0) {
+            return value.divide(wan, 2, RoundingMode.HALF_UP) + "万";
+        }
+        return value.setScale(2, RoundingMode.HALF_UP).toString();
+    }
 }