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