|
|
@@ -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));
|
|
|
}
|
|
|
-}
|
|
|
+}
|