package com.yingpai.stock.task; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.yingpai.stock.domain.StockInfo; import com.yingpai.stock.mapper.StockInfoMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import cn.hutool.http.HttpUtil; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; /** * 股票信息同步定时任务 * 从东方财富接口同步全部A股列表到 stock_info 表 */ @Slf4j @Component @RequiredArgsConstructor public class StockSyncTask { private final StockInfoMapper stockInfoMapper; private final ObjectMapper objectMapper = new ObjectMapper(); // 东方财富全部A股列表接口 // 沪市:m:1+t:2 主板, m:1+t:23 科创板 // 深市:m:0+t:6 主板, m:0+t:80 创业板 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"; 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"; /** * 每天凌晨3点执行同步 */ @Scheduled(cron = "0 0 3 * * ?") public void syncAllStocks() { log.info("[股票同步] 开始同步全部A股信息..."); try { List allStocks = new ArrayList<>(); // 同步沪市股票 List shStocks = fetchStockList(SH_STOCK_URL, "SH"); allStocks.addAll(shStocks); log.info("[股票同步] 沪市股票数量: {}", shStocks.size()); // 同步深市股票 List szStocks = fetchStockList(SZ_STOCK_URL, "SZ"); allStocks.addAll(szStocks); log.info("[股票同步] 深市股票数量: {}", szStocks.size()); // 批量保存或更新 if (!allStocks.isEmpty()) { batchSaveOrUpdate(allStocks); log.info("[股票同步] 同步完成,共 {} 只股票", allStocks.size()); } } catch (Exception e) { log.error("[股票同步] 同步失败: {}", e.getMessage(), e); } } /** * 手动触发同步(供管理员调用) */ public void manualSync() { syncAllStocks(); } /** * 分页获取股票列表 */ private List fetchStockList(String urlTemplate, String market) { List stocks = new ArrayList<>(); int page = 1; while (true) { try { String url = String.format(urlTemplate, page); String responseBody = HttpUtil.createGet(url) .header("User-Agent", "Mozilla/5.0") .timeout(30000) .execute() .body(); if (responseBody != null && !responseBody.isEmpty()) { JsonNode root = objectMapper.readTree(responseBody); JsonNode data = root.path("data").path("diff"); if (data == null || data.isNull() || !data.isArray() || data.isEmpty()) { break; } for (JsonNode item : data) { String code = item.path("f12").asText(); String name = item.path("f14").asText(); if (code != null && !code.isEmpty() && name != null && !name.isEmpty()) { StockInfo stockInfo = new StockInfo(); stockInfo.setStockCode(code); stockInfo.setStockName(name); stockInfo.setMarket(market); stocks.add(stockInfo); } } // 如果返回数量少于500,说明已经是最后一页 if (data.size() < 500) { break; } page++; // 防止请求过快 Thread.sleep(200); } else { log.warn("[股票同步] 请求失败,响应为空"); break; } } catch (Exception e) { log.error("[股票同步] 获取第{}页数据失败: {}", page, e.getMessage()); break; } } return stocks; } /** * 批量保存或更新 */ private void batchSaveOrUpdate(List list) { if (list == null || list.isEmpty()) { return; } LocalDateTime now = LocalDateTime.now(); // 分批处理,每批500条 int batchSize = 500; for (int i = 0; i < list.size(); i += batchSize) { int end = Math.min(i + batchSize, list.size()); List batch = list.subList(i, end); for (StockInfo stock : batch) { // 查询是否存在 StockInfo existing = stockInfoMapper.selectOne( new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() .eq(StockInfo::getStockCode, stock.getStockCode()) ); if (existing != null) { // 更新 existing.setStockName(stock.getStockName()); existing.setMarket(stock.getMarket()); existing.setUpdateTime(now); stockInfoMapper.updateById(existing); } else { // 新增 stock.setCreateTime(now); stock.setUpdateTime(now); stockInfoMapper.insert(stock); } } } log.info("[股票信息] 批量保存完成,共 {} 条", list.size()); } }