StockSyncTask.java 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. package com.yingpai.stock.task;
  2. import com.fasterxml.jackson.databind.JsonNode;
  3. import com.fasterxml.jackson.databind.ObjectMapper;
  4. import com.yingpai.stock.domain.StockInfo;
  5. import com.yingpai.stock.mapper.StockInfoMapper;
  6. import lombok.RequiredArgsConstructor;
  7. import lombok.extern.slf4j.Slf4j;
  8. import org.springframework.scheduling.annotation.Scheduled;
  9. import org.springframework.stereotype.Component;
  10. import cn.hutool.http.HttpUtil;
  11. import java.time.LocalDateTime;
  12. import java.util.ArrayList;
  13. import java.util.List;
  14. /**
  15. * 股票信息同步定时任务
  16. * 从东方财富接口同步全部A股列表到 stock_info 表
  17. */
  18. @Slf4j
  19. @Component
  20. @RequiredArgsConstructor
  21. public class StockSyncTask {
  22. private final StockInfoMapper stockInfoMapper;
  23. private final ObjectMapper objectMapper = new ObjectMapper();
  24. // 东方财富全部A股列表接口
  25. // 沪市:m:1+t:2 主板, m:1+t:23 科创板
  26. // 深市:m:0+t:6 主板, m:0+t:80 创业板
  27. private static final String SH_STOCK_URL = "http://80.push2.eastmoney.com/api/qt/clist/get?pn=%d&pz=500&fs=m:1+t:2,m:1+t:23&fields=f12,f14";
  28. private static final String SZ_STOCK_URL = "http://80.push2.eastmoney.com/api/qt/clist/get?pn=%d&pz=500&fs=m:0+t:6,m:0+t:80&fields=f12,f14";
  29. /**
  30. * 每天凌晨3点执行同步
  31. */
  32. @Scheduled(cron = "0 0 3 * * ?")
  33. public void syncAllStocks() {
  34. log.info("[股票同步] 开始同步全部A股信息...");
  35. try {
  36. List<StockInfo> allStocks = new ArrayList<>();
  37. // 同步沪市股票
  38. List<StockInfo> shStocks = fetchStockList(SH_STOCK_URL, "SH");
  39. allStocks.addAll(shStocks);
  40. log.info("[股票同步] 沪市股票数量: {}", shStocks.size());
  41. // 同步深市股票
  42. List<StockInfo> szStocks = fetchStockList(SZ_STOCK_URL, "SZ");
  43. allStocks.addAll(szStocks);
  44. log.info("[股票同步] 深市股票数量: {}", szStocks.size());
  45. // 批量保存或更新
  46. if (!allStocks.isEmpty()) {
  47. batchSaveOrUpdate(allStocks);
  48. log.info("[股票同步] 同步完成,共 {} 只股票", allStocks.size());
  49. }
  50. } catch (Exception e) {
  51. log.error("[股票同步] 同步失败: {}", e.getMessage(), e);
  52. }
  53. }
  54. /**
  55. * 手动触发同步(供管理员调用)
  56. */
  57. public void manualSync() {
  58. syncAllStocks();
  59. }
  60. /**
  61. * 分页获取股票列表
  62. */
  63. private List<StockInfo> fetchStockList(String urlTemplate, String market) {
  64. List<StockInfo> stocks = new ArrayList<>();
  65. int page = 1;
  66. while (true) {
  67. try {
  68. String url = String.format(urlTemplate, page);
  69. String responseBody = HttpUtil.createGet(url)
  70. .header("User-Agent", "Mozilla/5.0")
  71. .timeout(30000)
  72. .execute()
  73. .body();
  74. if (responseBody != null && !responseBody.isEmpty()) {
  75. JsonNode root = objectMapper.readTree(responseBody);
  76. JsonNode data = root.path("data").path("diff");
  77. if (data == null || data.isNull() || !data.isArray() || data.isEmpty()) {
  78. break;
  79. }
  80. for (JsonNode item : data) {
  81. String code = item.path("f12").asText();
  82. String name = item.path("f14").asText();
  83. if (code != null && !code.isEmpty() && name != null && !name.isEmpty()) {
  84. StockInfo stockInfo = new StockInfo();
  85. stockInfo.setStockCode(code);
  86. stockInfo.setStockName(name);
  87. stockInfo.setMarket(market);
  88. stocks.add(stockInfo);
  89. }
  90. }
  91. // 如果返回数量少于500,说明已经是最后一页
  92. if (data.size() < 500) {
  93. break;
  94. }
  95. page++;
  96. // 防止请求过快
  97. Thread.sleep(200);
  98. } else {
  99. log.warn("[股票同步] 请求失败,响应为空");
  100. break;
  101. }
  102. } catch (Exception e) {
  103. log.error("[股票同步] 获取第{}页数据失败: {}", page, e.getMessage());
  104. break;
  105. }
  106. }
  107. return stocks;
  108. }
  109. /**
  110. * 批量保存或更新
  111. */
  112. private void batchSaveOrUpdate(List<StockInfo> list) {
  113. if (list == null || list.isEmpty()) {
  114. return;
  115. }
  116. LocalDateTime now = LocalDateTime.now();
  117. // 分批处理,每批500条
  118. int batchSize = 500;
  119. for (int i = 0; i < list.size(); i += batchSize) {
  120. int end = Math.min(i + batchSize, list.size());
  121. List<StockInfo> batch = list.subList(i, end);
  122. for (StockInfo stock : batch) {
  123. // 查询是否存在
  124. StockInfo existing = stockInfoMapper.selectOne(
  125. new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<StockInfo>()
  126. .eq(StockInfo::getStockCode, stock.getStockCode())
  127. );
  128. if (existing != null) {
  129. // 更新
  130. existing.setStockName(stock.getStockName());
  131. existing.setMarket(stock.getMarket());
  132. existing.setUpdateTime(now);
  133. stockInfoMapper.updateById(existing);
  134. } else {
  135. // 新增
  136. stock.setCreateTime(now);
  137. stock.setUpdateTime(now);
  138. stockInfoMapper.insert(stock);
  139. }
  140. }
  141. }
  142. log.info("[股票信息] 批量保存完成,共 {} 条", list.size());
  143. }
  144. }