Quellcode durchsuchen

add:增加gateio的订单列表v1

zhiqiang.lv vor 3 Wochen
Ursprung
Commit
68822b7357

+ 36 - 0
src/main/java/top/lvzhiqiang/config/RestTemplateConfig.java

@@ -0,0 +1,36 @@
+package top.lvzhiqiang.config;
+
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.client.SimpleClientHttpRequestFactory;
+import org.springframework.web.client.RestTemplate;
+
+/**
+ * RestTemplate配置
+ *
+ * @author: lvzhiqiang
+ * @date: 2026/01/22 16:39
+ */
+@Configuration
+public class RestTemplateConfig {
+
+    /**
+     * 自定义restTemplate
+     *
+     * @return: org.springframework.web.client.RestTemplate
+     * @author: lvzhiqiang
+     * @date: 2026/01/22 16:41
+     */
+    @Bean
+    public RestTemplate restTemplate() {
+        // 使用 Spring 内置的请求工厂
+        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
+        // 设置连接超时时间(毫秒),例如 5 秒
+        factory.setConnectTimeout(60000);
+        //设置读取超时时间(毫秒),例如 10 秒(外部 API 响应可能较慢)
+        factory.setReadTimeout(300000);
+
+        return new RestTemplate(factory);
+    }
+}

+ 32 - 0
src/main/java/top/lvzhiqiang/entity/CoinGateSpotOrders.java

@@ -0,0 +1,32 @@
+package top.lvzhiqiang.entity;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * GATE精简版现货订单表
+ *
+ * @author lvzhiqiang
+ * 2026/3/13 15:33
+ */
+@Data
+public class CoinGateSpotOrders implements Serializable {
+
+    private String id;
+    private String currencyPair;
+    private String side;
+    private String type;
+    private BigDecimal amount;
+    private BigDecimal price;
+    private BigDecimal filledAmount;
+    private BigDecimal avgDealPrice;
+    private BigDecimal filledTotal;
+    private BigDecimal fee;
+    private String feeCurrency;
+    private String status;
+    private String finishAs;
+    private Long createTimeMs;
+    private Long updateTimeMs;
+}

+ 44 - 0
src/main/java/top/lvzhiqiang/job/CoinGateJob.java

@@ -0,0 +1,44 @@
+package top.lvzhiqiang.job;
+
+import javax.annotation.Resource;
+
+import org.springframework.stereotype.Component;
+
+import com.xxl.job.core.biz.model.ReturnT;
+import com.xxl.job.core.handler.annotation.XxlJob;
+
+import lombok.extern.slf4j.Slf4j;
+import top.lvzhiqiang.service.CoinGateService;
+
+/**
+ * coin-gate任务
+ *
+ * @author lvzhiqiang
+ * 2026/3/13 15:33
+ */
+@Component
+@Slf4j
+public class CoinGateJob {
+
+    @Resource
+    private CoinGateService coinGateService;
+
+    /**
+     * 同步现货订单列表
+     *
+     * @return com.xxl.job.core.biz.model.ReturnT<java.lang.String>
+     * @author lvzhiqiang
+     * 2026/3/13 15:33
+     */
+    @XxlJob("syncSpotOrdersJobHandler")
+    public ReturnT<String> syncSpotOrdersJobHandler() {
+        try {
+            coinGateService.syncSpotOrders();
+            return ReturnT.SUCCESS;
+        } catch (Exception e) {
+            String errorMsg = e.getMessage() == null ? e.getClass().getName() : e.getMessage();
+            log.error("syncSpotOrders exception", e);
+            return new ReturnT<>(ReturnT.FAIL_CODE, "syncSpotOrdersJobHandler异常: " + errorMsg);
+        }
+    }
+}

+ 48 - 0
src/main/java/top/lvzhiqiang/mapper/CoinGateMapper.java

@@ -0,0 +1,48 @@
+package top.lvzhiqiang.mapper;
+
+import java.util.List;
+
+import org.apache.ibatis.annotations.Insert;
+import org.apache.ibatis.annotations.Param;
+
+import me.chanjar.weixin.cp.bean.oa.WxCpApprovalInfoQueryFilter.KEY;
+import top.lvzhiqiang.entity.CoinGateSpotOrders;
+
+/**
+ * coin-gate-Mapper
+ *
+ * @author lvzhiqiang 2026/3/13 15:33
+ */
+public interface CoinGateMapper {
+
+    /**
+     * 批量 Upsert (存在则更新,不存在则插入)
+     */
+    @Insert({"<script>" +
+            "INSERT INTO coin_gate_spot_orders (" +
+            "id, currency_pair, side, type, amount, price, " +
+            "filled_amount, avg_deal_price, filled_total, " +
+            "fee, fee_currency, status, finish_as, " +
+            "create_time_ms, update_time_ms" +
+            ") " +
+            "VALUES" +
+            " <foreach collection='orders' item='item' separator=\",\">" +
+            "  (" +
+            "   #{item.id}, #{item.currencyPair}, #{item.side}, #{item.type}, " +
+            "   #{item.amount}, #{item.price}, #{item.filledAmount}, " +
+            "    #{item.avgDealPrice}, #{item.filledTotal}, #{item.fee}, " +
+            "    #{item.feeCurrency}, #{item.status}, #{item.finishAs}, " +
+            "     #{item.createTimeMs}, #{item.updateTimeMs}" +
+            " )" +
+            " </foreach> " +
+            " ON DUPLICATE KEY UPDATE " +
+            "     filled_amount = VALUES(filled_amount), " +
+            "    avg_deal_price = VALUES(avg_deal_price), " +
+            "    filled_total = VALUES(filled_total), " +
+            "     fee = VALUES(fee), " +
+            "     status = VALUES(status), " +
+            "     finish_as = VALUES(finish_as), " +
+            "    update_time_ms = VALUES(update_time_ms)" +
+            " </script>" })
+    int batchUpsert(@Param("orders") List<CoinGateSpotOrders> fundrates);
+}

+ 15 - 0
src/main/java/top/lvzhiqiang/service/CoinGateService.java

@@ -0,0 +1,15 @@
+package top.lvzhiqiang.service;
+
+/**
+ * Coin Gate Service
+ *
+ * @author lvzhiqiang
+ * 2026/3/13 15:33
+ */
+public interface CoinGateService {
+
+    /**
+     * 同步现货订单列表
+     */
+    void syncSpotOrders();
+}

+ 159 - 0
src/main/java/top/lvzhiqiang/service/impl/CoinGateServiceImpl.java

@@ -0,0 +1,159 @@
+package top.lvzhiqiang.service.impl;
+
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.xxl.job.core.context.XxlJobHelper;
+import lombok.extern.slf4j.Slf4j;
+
+import org.apache.commons.codec.binary.Hex;
+import org.jsoup.Connection;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Service;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.util.StopWatch;
+import org.springframework.web.client.RestTemplate;
+
+import top.lvzhiqiang.config.InitRunner;
+import top.lvzhiqiang.entity.CoinBitgetMixFundrate;
+import top.lvzhiqiang.entity.CoinGateSpotOrders;
+import top.lvzhiqiang.mapper.CoinBitgetMapper;
+import top.lvzhiqiang.mapper.CoinGateMapper;
+import top.lvzhiqiang.service.CoinBitgetService;
+import top.lvzhiqiang.service.CoinGateService;
+import top.lvzhiqiang.util.JsoupUtil;
+
+import javax.annotation.Resource;
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
+import java.math.BigDecimal;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * Coin Gate ServiceImpl
+ *
+ * @author lvzhiqiang
+ * 2026/3/13 15:33
+ */
+@Service
+@Slf4j
+public class CoinGateServiceImpl implements CoinGateService {
+
+    @Resource
+    private CoinGateMapper coinGateMapper;
+
+    private final RestTemplate restTemplate;
+    private final ObjectMapper objectMapper;
+
+    public CoinGateServiceImpl(RestTemplate restTemplate, ObjectMapper objectMapper) {
+        this.restTemplate = restTemplate;
+        this.objectMapper = objectMapper;
+    }
+
+    /**
+     * 同步现货订单列表
+     *
+     * @return com.xxl.job.core.biz.model.ReturnT<java.lang.String>
+     * @author lvzhiqiang
+     * 2026/3/13 15:33
+     */
+    @Override
+    public void syncSpotOrders() {
+        String apiKey = InitRunner.dicCodeMap.get("gateio_api_key").getCodeValue();
+        String apiSecret = InitRunner.dicCodeMap.get("gateio_api_secret").getCodeValue();
+        String host = InitRunner.dicCodeMap.get("gateio_host").getCodeValue();
+        String prefix = InitRunner.dicCodeMap.get("gateio_prefix").getCodeValue();
+
+        String path = "/spot/orders";
+        // 关键:API 要求必须带上状态参数查历史记录
+        String query = "currency_pair=" + "&status=finished";
+        String url = host + prefix + path + "?" + query;
+
+        try {
+            // 1. 生成鉴权 Headers
+            HttpHeaders headers = generateAuthHeaders("GET", prefix + path, query, "", apiKey, apiSecret);
+
+            // 2. 发起 HTTP 请求
+            HttpEntity<String> entity = new HttpEntity<>(null, headers);
+            ResponseEntity<String> response = restTemplate.exchange(URI.create(url), HttpMethod.GET, entity, String.class);
+
+            // 3. 解析 JSON 响应
+            JsonNode rootNode = objectMapper.readTree(response.getBody());
+            List<CoinGateSpotOrders> orderList = new ArrayList<>();
+
+            for (JsonNode node : rootNode) {
+                CoinGateSpotOrders order = new CoinGateSpotOrders();
+                order.setId(node.get("id").asText());
+                order.setCurrencyPair(node.get("currency_pair").asText());
+                order.setSide(node.get("side").asText());
+                order.setType(node.get("type").asText());
+                order.setAmount(new BigDecimal(node.get("amount").asText()));
+                order.setPrice(new BigDecimal(node.get("price").asText()));
+                order.setFilledAmount(new BigDecimal(node.get("filled_amount").asText()));
+                order.setAvgDealPrice(new BigDecimal(node.get("avg_deal_price").asText()));
+                order.setFilledTotal(new BigDecimal(node.get("filled_total").asText()));
+                order.setFee(new BigDecimal(node.get("fee").asText()));
+                order.setFeeCurrency(node.get("fee_currency").asText());
+                order.setStatus(node.get("status").asText());
+                order.setFinishAs(node.get("finish_as").asText());
+                order.setCreateTimeMs(node.get("create_time_ms").asLong());
+                order.setUpdateTimeMs(node.get("update_time_ms").asLong());
+                orderList.add(order);
+            }
+
+            // 4. 批量保存到 MySQL (由于设置了 ID,JPA 会自动执行 Upsert 操作)
+            if (!orderList.isEmpty()) {
+                coinGateMapper.batchUpsert(orderList);
+                log.info("成功同步 {} 条 {} 订单记录到数据库。", orderList.size(), "Gate.io");
+            }
+        } catch (Exception e) {
+            log.error("同步 Gate.io 订单失败: " + e.getMessage(), e);
+            // 生产环境建议使用 log.error() 并对接报警系统
+            throw new RuntimeException("订单同步异常", e);
+        }
+    }
+
+    /**
+     * 核心鉴权逻辑:生成 Gate.io APIv4 要求的签名 Header
+     */
+    private HttpHeaders generateAuthHeaders(String method, String url, String queryString, String bodyString, String apiKey, String apiSecret) throws Exception {
+        // 1. 生成 body 的 SHA512 哈希 (GET 请求通常为空字符串的哈希)
+        MessageDigest md = MessageDigest.getInstance("SHA-512");
+        md.update(bodyString.getBytes(StandardCharsets.UTF_8));
+        String hashedPayload = Hex.encodeHexString(md.digest());
+
+        // 2. 构造签名字符串
+        String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
+        String signatureString = method + "\n" +
+                url + "\n" +
+                (queryString == null ? "" : queryString) + "\n" +
+                hashedPayload + "\n" +
+                timestamp;
+
+        // 3. 使用 API Secret 进行 HMAC-SHA512 加密
+        Mac hmacSha512 = Mac.getInstance("HmacSHA512");
+        SecretKeySpec secretKeySpec = new SecretKeySpec(apiSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA512");
+        hmacSha512.init(secretKeySpec);
+        String signature = Hex.encodeHexString(hmacSha512.doFinal(signatureString.getBytes(StandardCharsets.UTF_8)));
+
+        // 4. 组装 Spring HttpHeaders
+        HttpHeaders headers = new HttpHeaders();
+        headers.set("KEY", apiKey);
+        headers.set("Timestamp", timestamp);
+        headers.set("SIGN", signature);
+        headers.set("Content-Type", "application/json");
+
+        return headers;
+    }
+}

+ 34 - 0
src/test/java/top/lvzhiqiang/TestCoinGate.java

@@ -0,0 +1,34 @@
+package top.lvzhiqiang;
+
+import javax.annotation.Resource;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+
+import lombok.extern.slf4j.Slf4j;
+import top.lvzhiqiang.service.CoinGateService;
+
+/**
+ * Gate.io单元测试类
+ *
+ * @author lvzhiqiang 
+ * 2025/12/8 19:31
+ */
+@Slf4j
+@RunWith(SpringJUnit4ClassRunner.class)
+@SpringBootTest(properties = {
+        "spring.profiles.active=dev",
+        "logging.level.top.lvzhiqiang=DEBUG"
+})
+public class TestCoinGate {
+
+    @Resource
+    private CoinGateService coinGateService;
+
+    @Test
+    public void testSyncSpotOrders() throws Exception {
+        coinGateService.syncSpotOrders();
+    }
+}