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