package com.xcong.excoin.modules.gateApi; import com.xcong.excoin.modules.station.model.GateStatsEvent; import com.xcong.excoin.modules.station.model.HeartbeatMsg; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import javax.annotation.Resource; import java.net.InetAddress; import java.security.MessageDigest; /** * JAR 侧 — 心跳定时发送(每 30s) */ @Slf4j @Component @EnableScheduling public class HeartbeatScheduler { @Resource private StatsEventProducer statsProducer; @Resource private GateWebSocketClientManager manager; private String apiKeyMd5; private String hostPort; // initialDelay = 0:启动后立即发第一次心跳,避免 Station 要等 30s 才能发现实例 @Scheduled(fixedRate = 30_000, initialDelay = 1_000) public void heartbeat() { try { if (apiKeyMd5 == null) { apiKeyMd5 = md5(manager.getConfig().getApiKey()); hostPort = resolveHostPort(); } // 策略未启动(gridTradeService == null)时也发送心跳, // 否则 Station 无法发现该 JAR 实例,也就无法对它下发 START 指令(死锁)。 GateGridTradeService strategy = manager.getGridTradeService(); String state = (strategy != null && strategy.getState() != null) ? strategy.getState().name() : "STOPPED"; int currentRound = strategy != null ? strategy.getCurrentRound() : 0; String cumulativePnl = (strategy != null && strategy.getCumulativePnl() != null) ? strategy.getCumulativePnl().toPlainString() : "0"; String principal = (strategy != null && strategy.getInitialPrincipal() != null) ? strategy.getInitialPrincipal().toPlainString() : "0"; HeartbeatMsg hb = HeartbeatMsg.builder() .contract(manager.getConfig().getContract()) .state(state) .leverage(manager.getConfig().getLeverage()) .currentRound(currentRound) .cumulativePnl(cumulativePnl) .principal(principal) .hostPort(hostPort) .build(); GateStatsEvent event = statsProducer.newHeartbeat(apiKeyMd5, hb); statsProducer.sendHeartbeat(event); log.info("[Gate] 心跳已发送, state={}, apiKeyMd5={}", state, apiKeyMd5); } catch (Exception e) { log.warn("[Gate] 心跳发送失败", e); } } private String resolveHostPort() { try { String host = InetAddress.getLocalHost().getHostAddress(); // port 从 Spring 环境变量获取,默认 8888 String port = System.getProperty("server.port", "8888"); return host + ":" + port; } catch (Exception e) { return "unknown"; } } private static String md5(String input) { try { MessageDigest md = MessageDigest.getInstance("MD5"); byte[] digest = md.digest(input.getBytes(java.nio.charset.StandardCharsets.UTF_8)); StringBuilder sb = new StringBuilder(); for (byte b : digest) sb.append(String.format("%02x", b)); return sb.toString(); } catch (Exception e) { return Integer.toHexString(input.hashCode()); } } }