Explorar el Código

股票信息api更换

Zhangbw hace 2 meses
padre
commit
707fac2b2f

+ 3 - 1
.claude/settings.local.json

@@ -7,7 +7,9 @@
       "Bash(tasklist:*)",
       "Bash(findstr:*)",
       "Bash(netstat:*)",
-      "Bash(taskkill:*)"
+      "Bash(taskkill:*)",
+      "Bash(curl:*)",
+      "Bash(git restore:*)"
     ]
   }
 }

+ 7 - 0
pom.xml

@@ -31,6 +31,13 @@
         <mybatis-plus.version>3.5.8</mybatis-plus.version>
     </properties>
     <dependencies>
+
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-all</artifactId>
+            <version>5.8.36</version>
+        </dependency>
+
         <!-- 基础 Spring Boot -->
         <dependency>
             <groupId>org.springframework.boot</groupId>

+ 2 - 0
src/main/java/com/yingpai/gupiao/GupiaoApplication.java

@@ -11,6 +11,8 @@ import org.springframework.scheduling.annotation.EnableScheduling;
 public class GupiaoApplication {
 
     public static void main(String[] args) {
+        // 优先使用IPv4,解决某些网络环境下的连接问题
+        System.setProperty("java.net.preferIPv4Stack", "true");
         SpringApplication.run(GupiaoApplication.class, args);
     }
 

+ 4 - 6
src/main/java/com/yingpai/gupiao/config/WebMvcConfig.java

@@ -27,15 +27,13 @@ public class WebMvcConfig implements WebMvcConfigurer {
                 // 需要认证的路径(按优先级排列)
                 .addPathPatterns("/v1/user/**")           // 用户相关(包括自选股票)
                 .addPathPatterns("/v1/order/**")          // 订单相关
-                .addPathPatterns("/v1/stock/history/**")  // 历史查询(需要订阅)
                 .addPathPatterns("/v1/file/**")           // 文件上传
+                .addPathPatterns("/v1/stock/history/**")  // 历史查询
+                .addPathPatterns("/v1/stock/search")      // 股票搜索
+                .addPathPatterns("/v1/stock/suggestion")  // 搜索建议
                 // 排除不需要认证的路径
                 .excludePathPatterns("/v1/auth/**")       // 认证接口
-                .excludePathPatterns("/v1/stock/pool/**") // 股票池列表(公开)
-                .excludePathPatterns("/v1/stock/search")  // 股票搜索(公开)
-                .excludePathPatterns("/v1/stock/suggestion") // 搜索建议(公开)
-                .excludePathPatterns("/v1/stock/history/stats") // 历史统计(公开,用于性能指标展示)
-                .excludePathPatterns("/v1/stock/history/search") // 历史搜索(公开,用于打分查询)
+                .excludePathPatterns("/v1/stock/pool/**") // 股票池列表(强势池公开,超短池在Controller中检查)
                 .excludePathPatterns("/v1/order/notify/**")  // 支付回调
                 .excludePathPatterns("/v1/order/config")     // 支付配置(公开)
                 .excludePathPatterns("/v1/order/check-subscription") // 订阅检查

+ 8 - 260
src/main/java/com/yingpai/gupiao/controller/StockDataController.java

@@ -1,292 +1,40 @@
 package com.yingpai.gupiao.controller;
 
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
 import com.yingpai.gupiao.domain.vo.Result;
 import com.yingpai.gupiao.domain.vo.StockInfoVO;
-import com.yingpai.gupiao.util.StockUtils;
-import lombok.extern.slf4j.Slf4j;
+import com.yingpai.gupiao.service.StockDataService;
+import lombok.RequiredArgsConstructor;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 
-import java.math.BigDecimal;
-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.ArrayList;
 import java.util.List;
 
-@Slf4j
 @RestController
 @RequestMapping("/api/stock")
+@RequiredArgsConstructor
 public class StockDataController {
 
-    // 东方财富通用行情接口
-    private static final String BASE_URL = "http://push2.eastmoney.com/api/qt/stock/get";
-    
-    // 东方财富分时数据接口
-    private static final String TREND_URL = "http://push2his.eastmoney.com/api/qt/stock/trends2/get";
-
-    private final HttpClient httpClient = HttpClient.newBuilder()
-            .connectTimeout(Duration.ofSeconds(3))
-            .build();
-
-    private final ObjectMapper objectMapper = new ObjectMapper();
+    private final StockDataService stockDataService;
 
     /**
      * 获取股票数据
-     * 调用示例:
-     * - 单个股票: GET /api/stock/fetch?codes=600519
-     * - 多个股票: GET /api/stock/fetch?codes=600519,300059,000001
-     *
      * @param codes 股票代码,多个用逗号分隔
      * @return 股票数据列表
      */
     @GetMapping("/fetch")
     public Result<List<StockInfoVO>> fetchStockData(@RequestParam("codes") String codes) {
-        log.info("[股票查询] 请求参数: {}", codes);
-        
-        List<StockInfoVO> dataList = new ArrayList<>();
-        String[] codeArray = codes.split(",");
-
-        for (String code : codeArray) {
-            code = code.trim();
-            if (code.isEmpty()) {
-                continue;
-            }
-
-            StockInfoVO stockInfo = fetchAShareData(code);
-            if (stockInfo != null) {
-                dataList.add(stockInfo);
-            }
-        }
-
-        log.debug("[股票查询] 返回结果: {}", dataList);
-        return Result.success(dataList);
+        return Result.success(stockDataService.fetchStockData(codes));
     }
 
     /**
-     * 获取指数数据(上证指数、深证成指、创业板指)
-     * 调用示例:
-     * - 上证指数: GET /api/stock/index?code=000001
-     * - 深证成指: GET /api/stock/index?code=399001
-     * - 创业板指: GET /api/stock/index?code=399006
-     *
+     * 获取指数数据
      * @param code 指数代码
      * @return 指数数据
      */
     @GetMapping("/index")
     public Result<StockInfoVO> fetchIndexData(@RequestParam("code") String code) {
-        log.info("[指数查询] 请求参数: {}", code);
-        
-        StockInfoVO indexInfo = fetchIndexInfo(code);
-        
-        log.debug("[指数查询] 返回结果: {}", indexInfo);
-        return Result.success(indexInfo);
-    }
-
-    /**
-     * 获取指数信息
-     */
-    private StockInfoVO fetchIndexInfo(String code) {
-        String secId = StockUtils.getIndexSecId(code);
-        
-        if (secId == null) {
-            return null;
-        }
-
-        // 构建 URL
-        String params = String.format("?invt=2&fltt=2&fields=f58,f43,f169,f170&secid=%s", secId);
-        String fullUrl = BASE_URL + params;
-
-        HttpRequest request = HttpRequest.newBuilder()
-                .uri(URI.create(fullUrl))
-                .GET()
-                .build();
-
-        try {
-            HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
-            log.debug("[指数 {}] 响应: {}", code, response.body());
-
-            if (response.statusCode() == 200) {
-                JsonNode root = objectMapper.readTree(response.body());
-                JsonNode data = root.get("data");
-
-                if (data != null && !data.isNull()) {
-                    BigDecimal currentPrice = parseBigDecimal(data, "f43");
-                    BigDecimal priceChange = parseBigDecimal(data, "f169");
-                    BigDecimal changePercent = parseBigDecimal(data, "f170");
-
-                    // 获取分时趋势数据
-                    List<BigDecimal> trendData = fetchTrendData(secId);
-
-                    return StockInfoVO.builder()
-                            .stockCode(code)
-                            .stockName(data.has("f58") ? data.get("f58").asText() : null)
-                            .currentPrice(currentPrice != null ? currentPrice.toString() : null)
-                            .priceChange(StockInfoVO.formatWithSign(priceChange))
-                            .changePercent(StockInfoVO.formatPercent(changePercent))
-                            .trendData(trendData)
-                            .build();
-                }
-            }
-        } catch (Exception e) {
-            log.error("[指数 {}] 异常: {}", code, e.getMessage());
-        }
-
-        return null;
-    }
-
-    /**
-     * 获取单个A股数据
-     */
-    private StockInfoVO fetchAShareData(String code) {
-        // 1. 获取 secid (只允许 A 股)
-        String secId = StockUtils.getAShareSecId(code);
-
-        // 如果返回 null,说明不是沪深 A 股
-        if (secId == null) {
-            return null;
-        }
-
-        // 2. 构建 URL
-        // f58: 名称, f43: 最新价, f169: 涨跌额, f170: 涨跌幅
-        String params = String.format("?invt=2&fltt=2&fields=f58,f43,f169,f170&secid=%s", secId);
-        String fullUrl = BASE_URL + params;
-
-        // 3. 发送请求
-        HttpRequest request = HttpRequest.newBuilder()
-                .uri(URI.create(fullUrl))
-                .GET()
-                .build();
-
-        try {
-            HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
-            log.debug("[{}] 响应: {}", code, response.body());
-
-            if (response.statusCode() == 200) {
-                JsonNode root = objectMapper.readTree(response.body());
-                JsonNode data = root.get("data");
-
-                if (data != null && !data.isNull()) {
-                    BigDecimal currentPrice = parseBigDecimal(data, "f43");
-                    BigDecimal priceChange = parseBigDecimal(data, "f169");
-                    BigDecimal changePercent = parseBigDecimal(data, "f170");
-
-                    // 获取分时趋势数据
-                    List<BigDecimal> trendData = fetchTrendData(secId);
-
-                    return StockInfoVO.builder()
-                            .stockCode(code)
-                            .stockName(data.has("f58") ? data.get("f58").asText() : null)
-                            .currentPrice(currentPrice != null ? currentPrice.toString() : null)
-                            .priceChange(StockInfoVO.formatWithSign(priceChange))
-                            .changePercent(StockInfoVO.formatPercent(changePercent))
-                            .trendData(trendData)
-                            .build();
-                }
-            }
-        } catch (Exception e) {
-            log.error("[{}] 异常: {}", code, e.getMessage());
-        }
-
-        return null;
-    }
-
-    /**
-     * 获取分时趋势数据
-     */
-    private List<BigDecimal> fetchTrendData(String secId) {
-        List<BigDecimal> trendData = new ArrayList<>();
-        
-        try {
-            // 构建分时数据URL
-            // fields: 时间,价格,均价,成交量,成交额,涨跌幅
-            String params = String.format("?fields1=f1,f2,f3,f4,f5,f6,f7,f8,f9,f10,f11,f12,f13&fields2=f51,f52,f53,f54,f55,f56,f57,f58&secid=%s&iscr=0", secId);
-            String fullUrl = TREND_URL + params;
-            
-            HttpRequest request = HttpRequest.newBuilder()
-                    .uri(URI.create(fullUrl))
-                    .GET()
-                    .build();
-            
-            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 trends = data.get("trends");
-                    
-                    if (trends != null && trends.isArray()) {
-                        List<BigDecimal> allPrices = new ArrayList<>();
-                        // 提取价格数据(trends数组中每个元素格式:时间,价格,均价,成交量...)
-                        for (JsonNode trend : trends) {
-                            String trendStr = trend.asText();
-                            String[] parts = trendStr.split(",");
-                            if (parts.length >= 2) {
-                                try {
-                                    BigDecimal price = new BigDecimal(parts[1]);  // 第二个字段是价格
-                                    allPrices.add(price);
-                                } catch (NumberFormatException e) {
-                                    // 忽略解析失败的数据点
-                                }
-                            }
-                        }
-                        
-                        // 采样:将数据点减少到约30个,让图表更直观
-                        int targetPoints = 30;
-                        if (allPrices.size() <= targetPoints) {
-                            trendData = allPrices;
-                        } else {
-                            // 等间隔采样
-                            double step = (double) (allPrices.size() - 1) / (targetPoints - 1);
-                            for (int i = 0; i < targetPoints; i++) {
-                                int index = (int) Math.round(i * step);
-                                if (index < allPrices.size()) {
-                                    trendData.add(allPrices.get(index));
-                                }
-                            }
-                        }
-                    }
-                }
-            }
-        } catch (Exception e) {
-            log.error("[趋势数据] 获取失败: {}", e.getMessage());
-        }
-        
-        // 如果没有获取到趋势数据,返回空列表
-        return trendData;
-    }
-
-    /**
-     * 安全地从 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;
-        }
+        return Result.success(stockDataService.fetchIndexData(code));
     }
-}
+}

+ 14 - 6
src/main/java/com/yingpai/gupiao/controller/StockHistoryController.java

@@ -104,19 +104,27 @@ public class StockHistoryController {
     }
     
     /**
-     * 根据股票代码或名称模糊查询最新的历史记录
+     * 根据股票代码或名称模糊查询历史记录
      * @param keyword 搜索关键词(股票代码或名称)
-     * @return 最新的历史记录
+     * @param recordDate 可选的记录日期 (yyyy-MM-dd),不传则查询最新记录
+     * @return 历史记录
      */
     @GetMapping("/search")
-    public Result<Map<String, Object>> searchLatestHistory(@RequestParam("keyword") String keyword) {
-        
+    public Result<Map<String, Object>> searchLatestHistory(
+            @RequestParam("keyword") String keyword,
+            @RequestParam(value = "recordDate", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate recordDate) {
+
         if (keyword == null || keyword.trim().isEmpty()) {
             return Result.error(400, "请输入搜索关键词");
         }
-        
+
         try {
-            Map<String, Object> result = stockHistoryService.queryLatestHistoryByKeyword(keyword.trim());
+            Map<String, Object> result;
+            if (recordDate != null) {
+                result = stockHistoryService.queryHistoryByKeywordAndDate(keyword.trim(), recordDate);
+            } else {
+                result = stockHistoryService.queryLatestHistoryByKeyword(keyword.trim());
+            }
             return Result.success(result);
         } catch (Exception e) {
             log.error("查询历史数据失败", e);

+ 20 - 3
src/main/java/com/yingpai/gupiao/controller/StockPoolController.java

@@ -4,8 +4,10 @@ import com.yingpai.gupiao.domain.po.User;
 import com.yingpai.gupiao.domain.vo.Result;
 import com.yingpai.gupiao.domain.vo.StockPoolVO;
 import com.yingpai.gupiao.mapper.UserMapper;
+import com.yingpai.gupiao.service.OrderService;
 import com.yingpai.gupiao.service.StockPoolService;
 import com.yingpai.gupiao.util.JwtUtil;
+import com.yingpai.gupiao.util.UserContext;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.web.bind.annotation.*;
@@ -21,11 +23,12 @@ import java.util.Map;
 @RequestMapping("/v1/stock/pool")
 @RequiredArgsConstructor
 public class StockPoolController {
-    
+
     private final StockPoolService stockPoolService;
     private final UserMapper userMapper;
     private final JwtUtil jwtUtil;
-    
+    private final OrderService orderService;
+
     /**
      * 获取股票池列表
      * @param poolType 池类型:1-超短池,2-强势池
@@ -35,7 +38,21 @@ public class StockPoolController {
         if (poolType == null || (poolType != 1 && poolType != 2)) {
             return Result.error(400, "池类型无效,1=超短池,2=强势池");
         }
-        
+
+        // 超短池需要订阅权限检查
+        if (poolType == 1) {
+            Long userId = UserContext.getUserId();
+            if (userId == null) {
+                return Result.error(401, "未登录");
+            }
+
+            boolean hasSubscription = orderService.hasActiveSubscription(userId, poolType);
+            if (!hasSubscription) {
+                return Result.error(403, "超短池需要订阅后查看");
+            }
+        }
+
+        // 强势池只需要登录(已通过拦截器验证)
         List<StockPoolVO> stocks = stockPoolService.getPoolStocks(poolType);
         return Result.success(stocks);
     }

+ 8 - 0
src/main/java/com/yingpai/gupiao/mapper/StockPoolHistoryMapper.java

@@ -63,4 +63,12 @@ public interface StockPoolHistoryMapper {
      * @return 最新的历史记录
      */
     StockHistoryVO selectLatestByKeyword(@Param("keyword") String keyword);
+
+    /**
+     * 根据股票代码或名称和日期查询历史记录
+     * @param keyword 搜索关键词(股票代码或名称)
+     * @param recordDate 记录日期
+     * @return 指定日期的历史记录
+     */
+    StockHistoryVO selectByKeywordAndDate(@Param("keyword") String keyword, @Param("recordDate") LocalDate recordDate);
 }

+ 25 - 0
src/main/java/com/yingpai/gupiao/service/StockDataService.java

@@ -0,0 +1,25 @@
+package com.yingpai.gupiao.service;
+
+import com.yingpai.gupiao.domain.vo.StockInfoVO;
+
+import java.util.List;
+
+/**
+ * 股票数据服务接口
+ */
+public interface StockDataService {
+
+    /**
+     * 批量获取股票数据
+     * @param codes 股票代码,多个用逗号分隔
+     * @return 股票数据列表
+     */
+    List<StockInfoVO> fetchStockData(String codes);
+
+    /**
+     * 获取指数数据
+     * @param code 指数代码
+     * @return 指数数据
+     */
+    StockInfoVO fetchIndexData(String code);
+}

+ 8 - 0
src/main/java/com/yingpai/gupiao/service/StockHistoryService.java

@@ -34,4 +34,12 @@ public interface StockHistoryService {
      * @return 最新的历史记录,包含评分等信息
      */
     Map<String, Object> queryLatestHistoryByKeyword(String keyword);
+
+    /**
+     * 根据股票代码或名称和日期查询历史记录
+     * @param keyword 搜索关键词(股票代码或名称)
+     * @param recordDate 记录日期
+     * @return 指定日期的历史记录,包含评分等信息
+     */
+    Map<String, Object> queryHistoryByKeywordAndDate(String keyword, LocalDate recordDate);
 }

+ 238 - 0
src/main/java/com/yingpai/gupiao/service/impl/StockDataServiceImpl.java

@@ -0,0 +1,238 @@
+package com.yingpai.gupiao.service.impl;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.yingpai.gupiao.domain.vo.StockInfoVO;
+import com.yingpai.gupiao.service.StockDataService;
+import com.yingpai.gupiao.util.StockUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+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.ArrayList;
+import java.util.List;
+
+@Slf4j
+@Service
+public class StockDataServiceImpl implements StockDataService {
+
+    //实时股票数据接口
+    private static final String BASE_URL = "http://qt.gtimg.cn/q=";
+    //股票分时数据接口
+    private static final String TREND_URL = "https://web.ifzq.gtimg.cn/appstock/app/minute/query";
+    //指数数据接口
+    private static final String INDEX_URL = "https://web.ifzq.gtimg.cn/appstock/app/fqkline/get";
+
+    private final HttpClient httpClient = HttpClient.newBuilder()
+            .connectTimeout(Duration.ofSeconds(3))
+            .build();
+
+    private final ObjectMapper objectMapper = new ObjectMapper();
+
+    @Override
+    public List<StockInfoVO> fetchStockData(String codes) {
+        log.info("[股票查询] 请求参数: {}", codes);
+
+        List<StockInfoVO> dataList = new ArrayList<>();
+        String[] codeArray = codes.split(",");
+
+        StringBuilder queryBuilder = new StringBuilder();
+        for (String code : codeArray) {
+            code = code.trim();
+            if (code.isEmpty()) {
+                continue;
+            }
+
+            String marketPrefix = StockUtils.getMarketPrefix(code);
+            if (marketPrefix != null) {
+                if (queryBuilder.length() > 0) {
+                    queryBuilder.append(",");
+                }
+                queryBuilder.append(marketPrefix).append(code);
+            }
+        }
+
+        if (queryBuilder.length() == 0) {
+            return dataList;
+        }
+
+        String fullUrl = BASE_URL + queryBuilder.toString();
+        HttpRequest request = HttpRequest.newBuilder()
+                .uri(URI.create(fullUrl))
+                .GET()
+                .build();
+
+        try {
+            HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
+            log.debug("[批量查询] 响应: {}", response.body());
+
+            if (response.statusCode() == 200) {
+                String body = response.body();
+                String[] lines = body.split(";");
+
+                for (String line : lines) {
+                    line = line.trim();
+                    if (line.isEmpty()) {
+                        continue;
+                    }
+
+                    StockInfoVO stockInfo = parseStockData(line);
+                    if (stockInfo != null) {
+                        dataList.add(stockInfo);
+                    }
+                }
+            }
+        } catch (Exception e) {
+            log.error("[批量查询] 异常: {}", e.getMessage());
+        }
+
+        log.debug("[股票查询] 返回结果: {}", dataList);
+        return dataList;
+    }
+
+    @Override
+    public StockInfoVO fetchIndexData(String code) {
+        log.info("[指数查询] 请求参数: {}", code);
+
+        try {
+            String today = java.time.LocalDate.now().toString();
+            String fullUrl = INDEX_URL + "?param=sh" + code + ",day," + today + ",,640,qfq";
+
+            HttpRequest request = HttpRequest.newBuilder()
+                    .uri(URI.create(fullUrl))
+                    .GET()
+                    .build();
+
+            HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
+            log.debug("[指数查询] 响应: {}", response.body());
+
+            if (response.statusCode() == 200) {
+                JsonNode root = objectMapper.readTree(response.body());
+                JsonNode qtData = root.path("data").path("sh" + code).path("qt").path("sh" + code);
+
+                if (qtData != null && qtData.isArray() && qtData.size() > 32) {
+                    String indexName = qtData.get(1).asText();
+                    String currentPrice = qtData.get(3).asText();
+                    String priceChange = qtData.get(31).asText();
+                    String changePercent = qtData.get(32).asText();
+
+                    List<BigDecimal> trendData = fetchTrendData(code);
+
+                    StockInfoVO indexInfo = StockInfoVO.builder()
+                            .stockCode(code)
+                            .stockName(indexName)
+                            .currentPrice(currentPrice)
+                            .priceChange(StockInfoVO.formatWithSign(new BigDecimal(priceChange)))
+                            .changePercent(StockInfoVO.formatPercent(new BigDecimal(changePercent)))
+                            .trendData(trendData)
+                            .build();
+
+                    log.debug("[指数查询] 返回结果: {}", indexInfo);
+                    return indexInfo;
+                }
+            }
+        } catch (Exception e) {
+            log.error("[指数查询] 异常: {}", e.getMessage());
+        }
+
+        return null;
+    }
+
+    private StockInfoVO parseStockData(String line) {
+        try {
+            int startIndex = line.indexOf("=\"");
+            int endIndex = line.lastIndexOf("\"");
+
+            if (startIndex != -1 && endIndex != -1 && endIndex > startIndex) {
+                String data = line.substring(startIndex + 2, endIndex);
+                String[] fields = data.split("~");
+
+                if (fields.length > 32) {
+                    String stockCode = fields[2];
+                    String stockName = fields[1];
+                    String currentPrice = fields[3];
+                    String priceChange = fields[31];
+                    String changePercent = fields[32];
+
+                    List<BigDecimal> trendData = fetchTrendData(stockCode);
+
+                    return StockInfoVO.builder()
+                            .stockCode(stockCode)
+                            .stockName(stockName)
+                            .currentPrice(currentPrice)
+                            .priceChange(StockInfoVO.formatWithSign(new BigDecimal(priceChange)))
+                            .changePercent(StockInfoVO.formatPercent(new BigDecimal(changePercent)))
+                            .trendData(trendData)
+                            .build();
+                }
+            }
+        } catch (Exception e) {
+            log.error("[解析股票数据] 异常: {}", e.getMessage());
+        }
+
+        return null;
+    }
+
+    private List<BigDecimal> fetchTrendData(String stockCode) {
+        List<BigDecimal> trendData = new ArrayList<>();
+
+        try {
+            String marketPrefix = StockUtils.getMarketPrefix(stockCode);
+            if (marketPrefix == null) {
+                return trendData;
+            }
+
+            String fullUrl = TREND_URL + "?code=" + marketPrefix + stockCode;
+
+            HttpRequest request = HttpRequest.newBuilder()
+                    .uri(URI.create(fullUrl))
+                    .GET()
+                    .build();
+
+            HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
+
+            if (response.statusCode() == 200) {
+                JsonNode root = objectMapper.readTree(response.body());
+                JsonNode data = root.path("data").path(marketPrefix + stockCode).path("data").path("data");
+
+                if (data != null && data.isArray()) {
+                    List<BigDecimal> allPrices = new ArrayList<>();
+                    for (JsonNode item : data) {
+                        String itemStr = item.asText();
+                        String[] parts = itemStr.split(" ");
+                        if (parts.length >= 2) {
+                            try {
+                                BigDecimal price = new BigDecimal(parts[1]);
+                                allPrices.add(price);
+                            } catch (NumberFormatException e) {
+                                // 忽略解析失败的数据点
+                            }
+                        }
+                    }
+
+                    int targetPoints = 30;
+                    if (allPrices.size() <= targetPoints) {
+                        trendData = allPrices;
+                    } else {
+                        double step = (double) (allPrices.size() - 1) / (targetPoints - 1);
+                        for (int i = 0; i < targetPoints; i++) {
+                            int index = (int) Math.round(i * step);
+                            if (index < allPrices.size()) {
+                                trendData.add(allPrices.get(index));
+                            }
+                        }
+                    }
+                }
+            }
+        } catch (Exception e) {
+            log.error("[趋势数据] 获取失败: {}", e.getMessage());
+        }
+
+        return trendData;
+    }
+}

+ 50 - 5
src/main/java/com/yingpai/gupiao/service/impl/StockHistoryServiceImpl.java

@@ -96,21 +96,21 @@ public class StockHistoryServiceImpl implements StockHistoryService {
     @Override
     public Map<String, Object> queryLatestHistoryByKeyword(String keyword) {
         Map<String, Object> result = new HashMap<>();
-        
+
         if (keyword == null || keyword.trim().isEmpty()) {
             result.put("found", false);
             result.put("message", "请输入搜索关键词");
             return result;
         }
-        
+
         StockHistoryVO history = stockPoolHistoryMapper.selectLatestByKeyword(keyword.trim());
-        
+
         if (history == null) {
             result.put("found", false);
             result.put("message", "未找到该股票的历史数据");
             return result;
         }
-        
+
         result.put("found", true);
         result.put("stockCode", history.getStockCode());
         result.put("stockName", history.getStockName());
@@ -128,7 +128,52 @@ public class StockHistoryServiceImpl implements StockHistoryService {
         result.put("dayAvgPrice", history.getDayAvgPrice());
         result.put("dayClosePrice", history.getDayClosePrice());
         result.put("highTrend", history.getHighTrend());
-        
+
+        return result;
+    }
+
+    @Override
+    public Map<String, Object> queryHistoryByKeywordAndDate(String keyword, LocalDate recordDate) {
+        Map<String, Object> result = new HashMap<>();
+
+        if (keyword == null || keyword.trim().isEmpty()) {
+            result.put("found", false);
+            result.put("message", "请输入搜索关键词");
+            return result;
+        }
+
+        if (recordDate == null) {
+            result.put("found", false);
+            result.put("message", "请输入查询日期");
+            return result;
+        }
+
+        StockHistoryVO history = stockPoolHistoryMapper.selectByKeywordAndDate(keyword.trim(), recordDate);
+
+        if (history == null) {
+            result.put("found", false);
+            result.put("message", "未找到该股票在指定日期的历史数据");
+            return result;
+        }
+
+        result.put("found", true);
+        result.put("stockCode", history.getStockCode());
+        result.put("stockName", history.getStockName());
+        result.put("recordDate", history.getRecordDate());
+        result.put("closePrice", history.getClosePrice());
+        result.put("changePercent", history.getChangePercent());
+        result.put("strengthScore", history.getStrengthScore());
+        result.put("totalAmount", history.getTotalAmount());
+        result.put("circulationMarketValue", history.getCirculationMarketValue());
+        result.put("mainRisePeriod", history.getMainRisePeriod());
+        result.put("recentRiseHand", history.getRecentRiseHand());
+        result.put("recentLimitUp", history.getRecentLimitUp());
+        result.put("dayHighestPrice", history.getDayHighestPrice());
+        result.put("dayLowestPrice", history.getDayLowestPrice());
+        result.put("dayAvgPrice", history.getDayAvgPrice());
+        result.put("dayClosePrice", history.getDayClosePrice());
+        result.put("highTrend", history.getHighTrend());
+
         return result;
     }
 }

+ 62 - 69
src/main/java/com/yingpai/gupiao/service/impl/StockPoolServiceImpl.java

@@ -1,8 +1,6 @@
 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.StockInfo;
 import com.yingpai.gupiao.domain.po.StockPool;
 import com.yingpai.gupiao.domain.vo.StockPoolVO;
@@ -43,15 +41,13 @@ 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 static final String BASE_URL = "http://qt.gtimg.cn/q=";
+
     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);
@@ -96,43 +92,51 @@ public class StockPoolServiceImpl implements StockPoolService {
         if (stocks == null || stocks.isEmpty()) {
             return result;
         }
-        
+
         try {
-            // 构建secids参数
-            String secids = stocks.stream()
-                    .map(s -> StockUtils.getAShareSecId(s.getStockCode()))
-                    .filter(s -> s != null)
+            // 构建批量查询参数
+            String codes = stocks.stream()
+                    .map(s -> StockUtils.getMarketPrefix(s.getStockCode()) + s.getStockCode())
+                    .filter(s -> s != null && !s.equals("null"))
                     .collect(Collectors.joining(","));
-            
-            if (secids.isEmpty()) {
+
+            if (codes.isEmpty()) {
                 return result;
             }
-            
-            // f12:代码, f14:名称, f2:最新价, f3:涨跌幅
-            String url = BATCH_QUOTE_URL + "?fltt=2&fields=f12,f14,f2,f3&secids=" + secids;
-            
+
+            String url = BASE_URL + codes;
+
             HttpRequest request = HttpRequest.newBuilder()
                     .uri(URI.create(url))
                     .GET()
                     .build();
-            
+
             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);
-                            }
+                String body = response.body();
+                String[] lines = body.split(";");
+
+                for (String line : lines) {
+                    line = line.trim();
+                    if (line.isEmpty()) {
+                        continue;
+                    }
+
+                    // 解析腾讯API返回格式: v_sh600519="1~贵州茅台~600519~2076.91~..."
+                    int startIndex = line.indexOf("=\"");
+                    int endIndex = line.lastIndexOf("\"");
+
+                    if (startIndex != -1 && endIndex != -1 && endIndex > startIndex) {
+                        String data = line.substring(startIndex + 2, endIndex);
+                        String[] fields = data.split("~");
+
+                        if (fields.length > 32) {
+                            String code = fields[2];
+                            QuoteData quote = new QuoteData();
+                            quote.currentPrice = fields[3];
+                            quote.changePercent = formatPercent(new BigDecimal(fields[32]));
+                            result.put(code, quote);
                         }
                     }
                 }
@@ -140,30 +144,17 @@ public class StockPoolServiceImpl implements StockPoolService {
         } catch (Exception e) {
             log.error("[批量行情] 获取失败: {}", e.getMessage());
         }
-        
+
         return result;
     }
     
-    private String formatPrice(JsonNode node, String field) {
-        if (!node.has(field) || node.get(field).isNull()) {
+    private String formatPercent(BigDecimal value) {
+        if (value == null) {
             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) + "%";
+            String sign = value.compareTo(BigDecimal.ZERO) >= 0 ? "+" : "";
+            return sign + value.setScale(2, RoundingMode.HALF_UP) + "%";
         } catch (Exception e) {
             return "-";
         }
@@ -255,31 +246,33 @@ public class StockPoolServiceImpl implements StockPoolService {
      */
     private BigDecimal fetchCurrentPrice(String stockCode) {
         try {
-            String secId = StockUtils.getAShareSecId(stockCode);
-            if (secId == null) {
+            String marketPrefix = StockUtils.getMarketPrefix(stockCode);
+            if (marketPrefix == null) {
                 return BigDecimal.ZERO;
             }
-            
-            String url = BATCH_QUOTE_URL + "?fltt=2&fields=f2&secids=" + secId;
-            
+
+            String url = BASE_URL + marketPrefix + stockCode;
+
             HttpRequest request = HttpRequest.newBuilder()
                     .uri(URI.create(url))
                     .GET()
                     .build();
-            
+
             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() && diff.size() > 0) {
-                        JsonNode item = diff.get(0);
-                        if (item.has("f2") && !item.get("f2").isNull()) {
-                            return new BigDecimal(item.get("f2").asText()).setScale(2, RoundingMode.HALF_UP);
-                        }
+                String body = response.body();
+
+                // 解析腾讯API返回格式: v_sh600519="1~贵州茅台~600519~2076.91~..."
+                int startIndex = body.indexOf("=\"");
+                int endIndex = body.lastIndexOf("\"");
+
+                if (startIndex != -1 && endIndex != -1 && endIndex > startIndex) {
+                    String data = body.substring(startIndex + 2, endIndex);
+                    String[] fields = data.split("~");
+
+                    if (fields.length > 3) {
+                        return new BigDecimal(fields[3]).setScale(2, RoundingMode.HALF_UP);
                     }
                 }
             }

+ 56 - 89
src/main/java/com/yingpai/gupiao/service/impl/StockSearchServiceImpl.java

@@ -1,8 +1,6 @@
 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;
@@ -31,16 +29,14 @@ import java.util.stream.Collectors;
 public class StockSearchServiceImpl implements StockSearchService {
 
     private final StockInfoMapper stockInfoMapper;
-    
-    // 东方财富通用行情接口
-    private static final String BASE_URL = "http://push2.eastmoney.com/api/qt/stock/get";
-    
+
+    // 腾讯股票行情接口
+    private static final String BASE_URL = "http://qt.gtimg.cn/q=";
+
     private final HttpClient httpClient = HttpClient.newBuilder()
             .connectTimeout(Duration.ofSeconds(3))
             .build();
 
-    private final ObjectMapper objectMapper = new ObjectMapper();
-
     @Override
     public StockDetailResponse search(StockSearchRequest request) {
         String keyword = request.getKeyword();
@@ -140,73 +136,70 @@ public class StockSearchServiceImpl implements StockSearchService {
     }
     
     /**
-     * 从东方财富API获取实时行情数据
+     * 从腾讯API获取实时行情数据
      */
     private void fetchRealTimeQuote(String stockCode, StockDetailResponse response) {
-        String secId = StockUtils.getAShareSecId(stockCode);
-        if (secId == null) {
-            log.warn("[实时行情] 无法获取secId: {}", stockCode);
+        String marketPrefix = StockUtils.getMarketPrefix(stockCode);
+        if (marketPrefix == null) {
+            log.warn("[实时行情] 无法获取市场前缀: {}", 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;
-        
+
+        String fullUrl = BASE_URL + marketPrefix + stockCode;
+
         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));
+                String body = httpResponse.body();
+
+                // 解析腾讯API返回格式: v_sh600519="1~贵州茅台~600519~2076.91~..."
+                int startIndex = body.indexOf("=\"");
+                int endIndex = body.lastIndexOf("\"");
+
+                if (startIndex != -1 && endIndex != -1 && endIndex > startIndex) {
+                    String data = body.substring(startIndex + 2, endIndex);
+                    String[] fields = data.split("~");
+
+                    if (fields.length > 32) {
+                        // 当前价格 [3]
+                        response.setCurrentPrice(fields[3]);
+
+                        // 涨跌额 [31]
+                        BigDecimal priceChange = new BigDecimal(fields[31]);
+                        response.setPriceChange(formatWithSign(priceChange));
+
+                        // 涨跌幅 [32]
+                        BigDecimal changePercent = new BigDecimal(fields[32]);
+                        response.setChangePercent(formatPercent(changePercent));
+
+                        // 开盘价 [5]
+                        response.setOpenPrice(fields[5]);
+
+                        // 最高价 [33]
+                        response.setHighPrice(fields[33]);
+
+                        // 最低价 [34]
+                        response.setLowPrice(fields[34]);
+
+                        // 成交量(手)[6]
+                        BigDecimal volume = new BigDecimal(fields[6]);
+                        response.setVolume(formatVolume(volume));
+
+                        // 成交额(元)[37]
+                        BigDecimal amount = new BigDecimal(fields[37]);
+                        response.setAmount(formatAmount(amount));
+
+                        // 换手率 [38]
+                        BigDecimal turnoverRate = new BigDecimal(fields[38]);
+                        response.setTurnoverRate(formatPercent(turnoverRate));
+                    }
                 }
             }
         } catch (Exception e) {
@@ -214,32 +207,6 @@ public class StockSearchServiceImpl implements StockSearchService {
         }
     }
     
-    /**
-     * 安全地从 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;
-        }
-    }
-    
     /**
      * 格式化带符号的数字(涨跌额)
      */

+ 137 - 11
src/main/java/com/yingpai/gupiao/service/impl/UserStockServiceImpl.java

@@ -6,15 +6,25 @@ import com.yingpai.gupiao.domain.po.UserStock;
 import com.yingpai.gupiao.domain.vo.UserStockVO;
 import com.yingpai.gupiao.mapper.UserStockMapper;
 import com.yingpai.gupiao.service.UserStockService;
+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.LocalDate;
 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;
 
 /**
  * 用户自选股票服务实现
@@ -23,39 +33,155 @@ import java.util.List;
 @Service
 @RequiredArgsConstructor
 public class UserStockServiceImpl implements UserStockService {
-    
+
     private final UserStockMapper userStockMapper;
-    
+
     private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
-    
+    private static final String BASE_URL = "http://qt.gtimg.cn/q=";
+
+    private final HttpClient httpClient = HttpClient.newBuilder()
+            .connectTimeout(Duration.ofSeconds(3))
+            .build();
+
     @Override
     public List<UserStockVO> getUserStocks(Long userId) {
         log.info("[查询股票] userId={}", userId);
-        
+
         LambdaQueryWrapper<UserStock> wrapper = new LambdaQueryWrapper<>();
         wrapper.eq(UserStock::getUserId, userId)
                .orderByDesc(UserStock::getAddDate);
-        
+
         List<UserStock> stocks = userStockMapper.selectList(wrapper);
         log.info("[查询股票] 查询到 {} 条记录", stocks.size());
-        
+
+        // 批量获取实时行情
+        Map<String, StockQuote> quoteMap = fetchBatchQuotes(stocks);
+
         List<UserStockVO> result = new ArrayList<>();
-        
+
         for (UserStock stock : stocks) {
             log.debug("[查询股票] 股票: {} - {}", stock.getStockCode(), stock.getStockName());
-            UserStockVO vo = UserStockVO.builder()
+
+            UserStockVO.UserStockVOBuilder builder = UserStockVO.builder()
                     .stockCode(stock.getStockCode())
                     .stockName(stock.getStockName())
                     .poolType(stock.getPoolType())
                     .poolTypeName(stock.getPoolType() == 1 ? "超短池" : "强势池")
                     .addPrice(stock.getAddPrice().toString())
-                    .addDate(stock.getAddDate().format(DATE_FORMATTER))
+                    .addDate(stock.getAddDate().format(DATE_FORMATTER));
+
+            // 填充实时行情和计算收益率
+            StockQuote quote = quoteMap.get(stock.getStockCode());
+            if (quote != null) {
+                builder.currentPrice(quote.currentPrice)
+                       .priceChange(quote.priceChange)
+                       .changePercent(quote.changePercent);
+
+                // 计算收益率
+                try {
+                    BigDecimal addPrice = stock.getAddPrice();
+                    BigDecimal currentPrice = new BigDecimal(quote.currentPrice);
+                    BigDecimal profit = currentPrice.subtract(addPrice)
+                            .divide(addPrice, 4, RoundingMode.HALF_UP)
+                            .multiply(new BigDecimal("100"));
+
+                    String sign = profit.compareTo(BigDecimal.ZERO) >= 0 ? "+" : "";
+                    builder.profitPercent(sign + profit.setScale(2, RoundingMode.HALF_UP) + "%");
+                } catch (Exception e) {
+                    log.error("[计算收益率] 失败: {}", e.getMessage());
+                }
+            }
+
+            result.add(builder.build());
+        }
+
+        return result;
+    }
+
+    /**
+     * 批量获取股票行情
+     */
+    private Map<String, StockQuote> fetchBatchQuotes(List<UserStock> stocks) {
+        Map<String, StockQuote> result = new HashMap<>();
+        if (stocks == null || stocks.isEmpty()) {
+            return result;
+        }
+
+        try {
+            String codes = stocks.stream()
+                    .map(s -> StockUtils.getMarketPrefix(s.getStockCode()) + s.getStockCode())
+                    .filter(s -> s != null && !s.equals("null"))
+                    .collect(Collectors.joining(","));
+
+            if (codes.isEmpty()) {
+                return result;
+            }
+
+            String url = BASE_URL + codes;
+
+            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) {
+                String body = response.body();
+                String[] lines = body.split(";");
+
+                for (String line : lines) {
+                    line = line.trim();
+                    if (line.isEmpty()) {
+                        continue;
+                    }
+
+                    int startIndex = line.indexOf("=\"");
+                    int endIndex = line.lastIndexOf("\"");
+
+                    if (startIndex != -1 && endIndex != -1 && endIndex > startIndex) {
+                        String data = line.substring(startIndex + 2, endIndex);
+                        String[] fields = data.split("~");
+
+                        if (fields.length > 32) {
+                            String code = fields[2];
+                            StockQuote quote = new StockQuote();
+                            quote.currentPrice = fields[3];
+                            quote.priceChange = formatWithSign(new BigDecimal(fields[31]));
+                            quote.changePercent = formatPercent(new BigDecimal(fields[32]));
+                            result.put(code, quote);
+                        }
+                    }
+                }
+            }
+        } catch (Exception e) {
+            log.error("[批量行情] 获取失败: {}", e.getMessage());
         }
-        
+
         return result;
     }
+
+    private String formatWithSign(BigDecimal value) {
+        if (value == null) {
+            return null;
+        }
+        String sign = value.compareTo(BigDecimal.ZERO) > 0 ? "+" : "";
+        return sign + value.setScale(2, RoundingMode.HALF_UP);
+    }
+
+    private String formatPercent(BigDecimal value) {
+        if (value == null) {
+            return null;
+        }
+        String sign = value.compareTo(BigDecimal.ZERO) > 0 ? "+" : "";
+        return sign + value.setScale(2, RoundingMode.HALF_UP) + "%";
+    }
+
+    private static class StockQuote {
+        String currentPrice;
+        String priceChange;
+        String changePercent;
+    }
     
     @Override
     public boolean addStock(Long userId, AddUserStockDTO dto) {

+ 5 - 37
src/main/java/com/yingpai/gupiao/util/StockUtils.java

@@ -3,55 +3,23 @@ package com.yingpai.gupiao.util;
 public class StockUtils {
 
     /**
-     * 仅处理沪深A股代码,将其转换为东方财富 API 需要的 secid
+     * 获取股票市场前缀(用于腾讯API)
      * @param code 股票代码
-     * @return secid (例如 "1.600519"),如果不是沪深A股则返回 null
+     * @return 市场前缀 "sh" 或 "sz",如果不是A股则返回 null
      */
-    public static String getAShareSecId(String code) {
-        // 1. 基础校验:A股代码必须是 6 位数字
-        // 港股是5位,这里直接过滤
+    public static String getMarketPrefix(String code) {
         if (code == null || code.length() != 6) {
             return null;
         }
 
-        // 2. 判断市场前缀
-        // 沪市:6开头 (主板600/601/603, 科创板688) -> 市场ID: 1
         if (code.startsWith("6")) {
-            return "1." + code;
+            return "sh";
         }
 
-        // 深市:0开头 (主板/中小板), 3开头 (创业板) -> 市场ID: 0
         if (code.startsWith("0") || code.startsWith("3")) {
-            return "0." + code;
+            return "sz";
         }
 
-        // 其他开头 (如 8/4 开头的北交所,如果不需要可以不处理,返回 null)
-        return null;
-    }
-
-    /**
-     * 获取指数的 secid
-     * @param code 指数代码
-     * @return secid (例如 "1.000001" 上证指数)
-     */
-    public static String getIndexSecId(String code) {
-        if (code == null) {
-            return null;
-        }
-        
-        // 上证指数 000001 -> 1.000001
-        if ("000001".equals(code)) {
-            return "1.000001";
-        }
-        // 深证成指 399001 -> 0.399001
-        if ("399001".equals(code)) {
-            return "0.399001";
-        }
-        // 创业板指 399006 -> 0.399006
-        if ("399006".equals(code)) {
-            return "0.399006";
-        }
-        
         return null;
     }
 }

+ 4 - 4
src/main/resources/application.yml

@@ -18,10 +18,10 @@ spring:
   
   datasource:
     url: jdbc:mysql://localhost:3306/ry_vue_5.x?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
-#    username: root
-    username: gupiao
-#    password: 121380
-    password: zQpdD3AmAPNCTFMyYARWMXnHSAbeHKEDNDHDYDZrFTAYGXGNVs
+    username: root
+#    username: gupiao
+    password: 121380
+#    password: zQpdD3AmAPNCTFMyYARWMXnHSAbeHKEDNDHDYDZrFTAYGXGNVs
     driver-class-name: com.mysql.cj.jdbc.Driver
 
   servlet:

+ 29 - 2
src/main/resources/mapper/StockPoolHistoryMapper.xml

@@ -101,7 +101,7 @@
 
     <!-- 根据股票代码或名称模糊查询最新的历史记录 -->
     <select id="selectLatestByKeyword" resultType="com.yingpai.gupiao.domain.vo.StockHistoryVO">
-        SELECT 
+        SELECT
             h.id,
             h.record_date AS recordDate,
             h.stock_code AS stockCode,
@@ -120,10 +120,37 @@
             h.day_close_price AS dayClosePrice,
             h.high_trend AS highTrend
         FROM stock_pool_history h
-        WHERE (h.stock_code LIKE CONCAT('%', #{keyword}, '%') 
+        WHERE (h.stock_code LIKE CONCAT('%', #{keyword}, '%')
                OR h.stock_name LIKE CONCAT('%', #{keyword}, '%'))
         ORDER BY h.record_date DESC
         LIMIT 1
     </select>
 
+    <!-- 根据股票代码或名称和日期查询历史记录 -->
+    <select id="selectByKeywordAndDate" resultType="com.yingpai.gupiao.domain.vo.StockHistoryVO">
+        SELECT
+            h.id,
+            h.record_date AS recordDate,
+            h.stock_code AS stockCode,
+            h.stock_name AS stockName,
+            h.change_percent AS changePercent,
+            h.close_price AS closePrice,
+            h.total_amount AS totalAmount,
+            h.strength_score AS strengthScore,
+            h.circulation_market_value AS circulationMarketValue,
+            h.main_rise_period AS mainRisePeriod,
+            h.recent_rise_hand AS recentRiseHand,
+            h.recent_limit_up AS recentLimitUp,
+            h.day_highest_price AS dayHighestPrice,
+            h.day_lowest_price AS dayLowestPrice,
+            h.day_avg_price AS dayAvgPrice,
+            h.day_close_price AS dayClosePrice,
+            h.high_trend AS highTrend
+        FROM stock_pool_history h
+        WHERE (h.stock_code LIKE CONCAT('%', #{keyword}, '%')
+               OR h.stock_name LIKE CONCAT('%', #{keyword}, '%'))
+          AND h.record_date = #{recordDate}
+        LIMIT 1
+    </select>
+
 </mapper>

+ 5 - 0
src/test/java/com/yingpai/gupiao/GupiaoApplicationTests.java

@@ -1,5 +1,7 @@
 package com.yingpai.gupiao;
 
+import cn.hutool.http.HttpRequest;
+import cn.hutool.http.HttpResponse;
 import org.junit.jupiter.api.Test;
 import org.springframework.boot.test.context.SpringBootTest;
 
@@ -8,6 +10,9 @@ class GupiaoApplicationTests {
 
     @Test
     void contextLoads() {
+
+
+
     }
 
 }