ソースを参照

我的股票功能更新

Zhangbw 3 ヶ月 前
コミット
26fe5335d0

+ 167 - 6
src/main/java/com/yingpai/gupiao/controller/StockDataController.java

@@ -25,6 +25,9 @@ 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))
@@ -43,8 +46,9 @@ public class StockDataController {
      */
     @GetMapping("/fetch")
     public Result<List<StockInfoVO>> fetchStockData(@RequestParam("codes") String codes) {
+        System.out.println("[股票查询] 请求参数: " + codes);
+        
         List<StockInfoVO> dataList = new ArrayList<>();
-
         String[] codeArray = codes.split(",");
 
         for (String code : codeArray) {
@@ -59,9 +63,82 @@ public class StockDataController {
             }
         }
 
+        System.out.println("[股票查询] 返回结果: " + dataList);
         return Result.success(dataList);
     }
 
+    /**
+     * 获取指数数据(上证指数、深证成指、创业板指)
+     * 调用示例:
+     * - 上证指数: 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) {
+        System.out.println("[指数查询] 请求参数: " + code);
+        
+        StockInfoVO indexInfo = fetchIndexInfo(code);
+        
+        System.out.println("[指数查询] 返回结果: " + 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());
+            System.out.println("[指数 " + 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) {
+            System.err.println("[指数 " + code + "] 异常: " + e.getMessage());
+        }
+
+        return null;
+    }
+
     /**
      * 获取单个A股数据
      */
@@ -75,8 +152,8 @@ public class StockDataController {
         }
 
         // 2. 构建 URL
-        // f58: 名称, f169: 涨跌额, f170: 涨跌幅
-        String params = String.format("?invt=2&fltt=2&fields=f58,f169,f170&secid=%s", secId);
+        // 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. 发送请求
@@ -87,27 +164,111 @@ public class StockDataController {
 
         try {
             HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
+            System.out.println("[" + code + "] 响应: " + response.body());
 
             if (response.statusCode() == 200) {
                 JsonNode root = objectMapper.readTree(response.body());
                 JsonNode data = root.get("data");
 
                 if (data != null && !data.isNull()) {
-                    BigDecimal priceChange = data.has("f169") ? new BigDecimal(data.get("f169").asText()) : null;
-                    BigDecimal changePercent = data.has("f170") ? new BigDecimal(data.get("f170").asText()) : null;
+                    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) {
-            System.err.println("获取股票数据异常 [" + code + "]: " + e.getMessage());
+            System.err.println("[" + 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()) {
+                        // 提取价格数据(trends数组中每个元素格式:时间,价格,均价,成交量...)
+                        for (JsonNode trend : trends) {
+                            String trendStr = trend.asText();
+                            String[] parts = trendStr.split(",");
+                            if (parts.length >= 2) {
+                                try {
+                                    BigDecimal price = new BigDecimal(parts[1]);  // 第二个字段是价格
+                                    trendData.add(price);
+                                } catch (NumberFormatException e) {
+                                    // 忽略解析失败的数据点
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        } catch (Exception e) {
+            System.err.println("[趋势数据] 获取失败: " + 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) {
+            System.err.println("无法解析数字字段 [" + fieldName + "]: " + value);
+            return null;
+        }
+    }
 }

+ 86 - 0
src/main/java/com/yingpai/gupiao/controller/UserStockController.java

@@ -0,0 +1,86 @@
+package com.yingpai.gupiao.controller;
+
+import com.yingpai.gupiao.domain.dto.AddUserStockDTO;
+import com.yingpai.gupiao.domain.vo.Result;
+import com.yingpai.gupiao.domain.vo.UserStockVO;
+import com.yingpai.gupiao.service.UserStockService;
+import com.yingpai.gupiao.util.UserContext;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 用户自选股票控制器
+ */
+@RestController
+@RequestMapping("/v1/user/stock")
+@RequiredArgsConstructor
+public class UserStockController {
+    
+    private final UserStockService userStockService;
+    
+    /**
+     * 获取用户自选股票列表
+     */
+    @GetMapping("/list")
+    public Result<List<UserStockVO>> getUserStocks() {
+        Long userId = UserContext.getUserId();
+        if (userId == null) {
+            return Result.error(401, "请先登录");
+        }
+        
+        List<UserStockVO> stocks = userStockService.getUserStocks(userId);
+        return Result.success(stocks);
+    }
+    
+    /**
+     * 添加自选股票
+     */
+    @PostMapping("/add")
+    public Result<Boolean> addStock(@RequestBody AddUserStockDTO dto) {
+        Long userId = UserContext.getUserId();
+        if (userId == null) {
+            return Result.error(401, "请先登录");
+        }
+        
+        if (dto.getStockCode() == null || dto.getStockCode().isEmpty()) {
+            return Result.error(400, "股票代码不能为空");
+        }
+        
+        boolean success = userStockService.addStock(userId, dto);
+        if (success) {
+            return Result.success(true);
+        } else {
+            return Result.error(400, "股票已存在或添加失败");
+        }
+    }
+    
+    /**
+     * 删除自选股票
+     */
+    @DeleteMapping("/delete")
+    public Result<Boolean> deleteStock(@RequestParam("stockCode") String stockCode) {
+        Long userId = UserContext.getUserId();
+        if (userId == null) {
+            return Result.error(401, "请先登录");
+        }
+        
+        boolean success = userStockService.deleteStock(userId, stockCode);
+        return Result.success(success);
+    }
+    
+    /**
+     * 检查股票是否已添加
+     */
+    @GetMapping("/check")
+    public Result<Boolean> checkStock(@RequestParam("stockCode") String stockCode) {
+        Long userId = UserContext.getUserId();
+        if (userId == null) {
+            return Result.success(false);
+        }
+        
+        boolean added = userStockService.isStockAdded(userId, stockCode);
+        return Result.success(added);
+    }
+}

+ 25 - 0
src/main/java/com/yingpai/gupiao/domain/dto/AddUserStockDTO.java

@@ -0,0 +1,25 @@
+package com.yingpai.gupiao.domain.dto;
+
+import lombok.Data;
+
+/**
+ * 添加用户自选股票DTO
+ */
+@Data
+public class AddUserStockDTO {
+    
+    /**
+     * 股票代码
+     */
+    private String stockCode;
+    
+    /**
+     * 股票名称
+     */
+    private String stockName;
+    
+    /**
+     * 当前价格
+     */
+    private String currentPrice;
+}

+ 62 - 0
src/main/java/com/yingpai/gupiao/domain/po/UserStock.java

@@ -0,0 +1,62 @@
+package com.yingpai.gupiao.domain.po;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+/**
+ * 用户自选股票实体类
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@TableName("user_stock")
+public class UserStock {
+    
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    
+    /**
+     * 用户ID
+     */
+    private Long userId;
+    
+    /**
+     * 股票代码
+     */
+    private String stockCode;
+    
+    /**
+     * 股票名称
+     */
+    private String stockName;
+    
+    /**
+     * 加入时的价格
+     */
+    private BigDecimal addPrice;
+    
+    /**
+     * 加入日期
+     */
+    private LocalDate addDate;
+    
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTime;
+    
+    /**
+     * 更新时间
+     */
+    private LocalDateTime updateTime;
+}

+ 12 - 0
src/main/java/com/yingpai/gupiao/domain/vo/StockInfoVO.java

@@ -6,6 +6,7 @@ import lombok.Data;
 import lombok.NoArgsConstructor;
 
 import java.math.BigDecimal;
+import java.util.List;
 
 /**
  * 股票详情VO
@@ -26,6 +27,11 @@ public class StockInfoVO {
      */
     private String stockName;
 
+    /**
+     * 当前价格
+     */
+    private String currentPrice;
+
     /**
      * 涨跌额 (带正负号,如 +12.50 或 -3.20)
      */
@@ -36,6 +42,12 @@ public class StockInfoVO {
      */
     private String changePercent;
 
+    /**
+     * 分时趋势数据(用于绘制折线图)
+     * 包含当日分时价格数据点
+     */
+    private List<BigDecimal> trendData;
+
     /**
      * 格式化数值,正数加+号
      */

+ 23 - 0
src/main/java/com/yingpai/gupiao/domain/vo/SubscriptionVO.java

@@ -0,0 +1,23 @@
+package com.yingpai.gupiao.domain.vo;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class SubscriptionVO {
+
+    /**
+     * 订阅ID
+     */
+    private Long orderId;
+
+    /**
+     * 订阅名
+     */
+    private String orderName;
+}

+ 64 - 0
src/main/java/com/yingpai/gupiao/domain/vo/UserStockVO.java

@@ -0,0 +1,64 @@
+package com.yingpai.gupiao.domain.vo;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 用户自选股票VO
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class UserStockVO {
+    
+    /**
+     * 股票代码
+     */
+    private String stockCode;
+    
+    /**
+     * 股票名称
+     */
+    private String stockName;
+    
+    /**
+     * 加入时的价格
+     */
+    private String addPrice;
+    
+    /**
+     * 加入日期 (yyyy-MM-dd)
+     */
+    private String addDate;
+    
+    /**
+     * 当前价格
+     */
+    private String currentPrice;
+    
+    /**
+     * 自选收益率 (相对于加入价格的涨跌幅)
+     */
+    private String profitPercent;
+    
+    /**
+     * 涨跌额
+     */
+    private String priceChange;
+    
+    /**
+     * 涨跌幅
+     */
+    private String changePercent;
+    
+    /**
+     * 分时趋势数据
+     */
+    private List<BigDecimal> trendData;
+}

+ 12 - 0
src/main/java/com/yingpai/gupiao/mapper/UserStockMapper.java

@@ -0,0 +1,12 @@
+package com.yingpai.gupiao.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.yingpai.gupiao.domain.po.UserStock;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 用户自选股票Mapper接口
+ */
+@Mapper
+public interface UserStockMapper extends BaseMapper<UserStock> {
+}

+ 32 - 0
src/main/java/com/yingpai/gupiao/service/UserStockService.java

@@ -0,0 +1,32 @@
+package com.yingpai.gupiao.service;
+
+import com.yingpai.gupiao.domain.dto.AddUserStockDTO;
+import com.yingpai.gupiao.domain.vo.UserStockVO;
+
+import java.util.List;
+
+/**
+ * 用户自选股票服务接口
+ */
+public interface UserStockService {
+    
+    /**
+     * 获取用户自选股票列表
+     */
+    List<UserStockVO> getUserStocks(Long userId);
+    
+    /**
+     * 添加自选股票
+     */
+    boolean addStock(Long userId, AddUserStockDTO dto);
+    
+    /**
+     * 删除自选股票
+     */
+    boolean deleteStock(Long userId, String stockCode);
+    
+    /**
+     * 检查股票是否已添加
+     */
+    boolean isStockAdded(Long userId, String stockCode);
+}

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

@@ -35,7 +35,7 @@ public class StockSearchServiceImpl implements StockSearchService {
             return List.of();
         }
 
-        //  查询股票信息(新表结构不再有日期字段,直接查询)
+        //  查询股票信息
         LambdaQueryWrapper<StockInfo> wrapper = new LambdaQueryWrapper<StockInfo>()
                 .and(w -> w
                         .likeRight(StockInfo::getStockCode, keyword) // 代码前缀匹配

+ 95 - 0
src/main/java/com/yingpai/gupiao/service/impl/UserStockServiceImpl.java

@@ -0,0 +1,95 @@
+package com.yingpai.gupiao.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.yingpai.gupiao.domain.dto.AddUserStockDTO;
+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 lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+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");
+    
+    @Override
+    public List<UserStockVO> getUserStocks(Long userId) {
+        LambdaQueryWrapper<UserStock> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(UserStock::getUserId, userId)
+               .orderByDesc(UserStock::getAddDate);
+        
+        List<UserStock> stocks = userStockMapper.selectList(wrapper);
+        List<UserStockVO> result = new ArrayList<>();
+        
+        for (UserStock stock : stocks) {
+            UserStockVO vo = UserStockVO.builder()
+                    .stockCode(stock.getStockCode())
+                    .stockName(stock.getStockName())
+                    .addPrice(stock.getAddPrice().toString())
+                    .addDate(stock.getAddDate().format(DATE_FORMATTER))
+                    .build();
+            result.add(vo);
+        }
+        
+        return result;
+    }
+    
+    @Override
+    public boolean addStock(Long userId, AddUserStockDTO dto) {
+        // 检查是否已存在
+        if (isStockAdded(userId, dto.getStockCode())) {
+            return false;
+        }
+        
+        BigDecimal price = BigDecimal.ZERO;
+        try {
+            if (dto.getCurrentPrice() != null && !dto.getCurrentPrice().isEmpty()) {
+                price = new BigDecimal(dto.getCurrentPrice());
+            }
+        } catch (NumberFormatException e) {
+            // 忽略解析错误
+        }
+        
+        UserStock userStock = UserStock.builder()
+                .userId(userId)
+                .stockCode(dto.getStockCode())
+                .stockName(dto.getStockName())
+                .addPrice(price)
+                .addDate(LocalDate.now())
+                .build();
+        
+        return userStockMapper.insert(userStock) > 0;
+    }
+    
+    @Override
+    public boolean deleteStock(Long userId, String stockCode) {
+        LambdaQueryWrapper<UserStock> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(UserStock::getUserId, userId)
+               .eq(UserStock::getStockCode, stockCode);
+        
+        return userStockMapper.delete(wrapper) > 0;
+    }
+    
+    @Override
+    public boolean isStockAdded(Long userId, String stockCode) {
+        LambdaQueryWrapper<UserStock> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(UserStock::getUserId, userId)
+               .eq(UserStock::getStockCode, stockCode);
+        
+        return userStockMapper.selectCount(wrapper) > 0;
+    }
+}

+ 26 - 0
src/main/java/com/yingpai/gupiao/util/StockUtils.java

@@ -28,4 +28,30 @@ public class StockUtils {
         // 其他开头 (如 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;
+    }
 }

+ 14 - 0
src/main/resources/sql/user_stock.sql

@@ -0,0 +1,14 @@
+-- 用户自选股票表
+CREATE TABLE IF NOT EXISTS `user_stock` (
+    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+    `user_id` BIGINT NOT NULL COMMENT '用户ID',
+    `stock_code` VARCHAR(10) NOT NULL COMMENT '股票代码',
+    `stock_name` VARCHAR(50) NOT NULL COMMENT '股票名称',
+    `add_price` DECIMAL(10, 2) NOT NULL COMMENT '加入时的价格',
+    `add_date` DATE NOT NULL COMMENT '加入日期',
+    `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    PRIMARY KEY (`id`),
+    UNIQUE KEY `uk_user_stock` (`user_id`, `stock_code`),
+    KEY `idx_user_id` (`user_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户自选股票表';