Administrator
8 hours ago 635199d881d45a8c92abea57e89b69634ab4b366
feat(newPrice): 新增OKX交易所相关功能模块

- 添加CoinTypeConvert工具类,支持币种符号转换为K线键值
- 创建DateUtils日期处理工具类,支持多种日期格式化与解析
- 新增DefaultUrls枚举类,定义交易所相关URL常量
- 添加ExchangeInfoEnum枚举类,管理交易所账户信息配置
- 创建ExchangeLoginEventService接口及实现类,处理交易所登录事件
- 新增ExchangeLoginService单例类,统一管理交易所服务实例
- 添加FebsException自定义异常类,处理系统内部错误
- 创建HttpMethod枚举类,定义HTTP请求方法类型
- 添加JSONParser工具类,提供JSON数据解析功能
- 新增KlineVo实体类,封装K线请求参数
- 创建OkHttpUtils工具类,封装OkHttp客户端请求逻辑
- 添加OKXAccount账户管理类,处理OKX账户相关操作
- 新增OKXContants常量类,定义OKX接口路径常量
- 创建ParameterChecker参数检查工具类,验证接口参数合法性
- 添加RequestBuilder请求构建工具类,生成带签名的请求头
20 files added
4 files modified
1535 ■■■■■ changed files
src/main/java/com/xcong/excoin/common/exception/FebsException.java 15 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/newPrice/ExchangeInfoEnum.java 34 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/newPrice/ExchangeLoginEventService.java 27 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/newPrice/ExchangeLoginService.java 36 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/newPrice/KlineVo.java 23 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/newPrice/OKXAccount.java 21 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/newPrice/RequestHandler.java 125 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/newPrice/ResponseHandler.java 66 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/newPrice/enums/DefaultUrls.java 38 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/newPrice/enums/HttpMethod.java 9 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/newPrice/enums/RequestType.java 7 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/newPrice/impl/ExchangeLoginEventServiceImpl.java 50 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/newPrice/utils/DateUtils.java 80 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/newPrice/utils/JSONParser.java 40 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/newPrice/utils/OKXContants.java 185 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/newPrice/utils/OkHttpUtils.java 330 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/newPrice/utils/ParameterChecker.java 36 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/newPrice/utils/RequestBuilder.java 150 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/newPrice/utils/SignUtils.java 55 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/newPrice/utils/UrlBuilder.java 128 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/symbols/controller/SymbolsController.java 10 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/symbols/service/SymbolsService.java 2 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/modules/symbols/service/impl/SymbolsServiceImpl.java 30 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/utils/CoinTypeConvert.java 38 ●●●●● patch | view | raw | blame | history
src/main/java/com/xcong/excoin/common/exception/FebsException.java
New file
@@ -0,0 +1,15 @@
package com.xcong.excoin.common.exception;
/**
 * FEBS系统内部异常
 *
 * @author MrBird
 */
public class FebsException extends RuntimeException  {
    private static final long serialVersionUID = -994962710559017255L;
    public FebsException(String message) {
        super(message);
    }
}
src/main/java/com/xcong/excoin/modules/newPrice/ExchangeInfoEnum.java
New file
@@ -0,0 +1,34 @@
package com.xcong.excoin.modules.newPrice;
import lombok.Getter;
@Getter
public enum ExchangeInfoEnum {
    /**
     *      模拟盘
     *        String apiKey = "16c882b0-2853-46c5-8af0-5269e3218d72";
     *        String passphrase = "Zh12345@@";
     *        String secretKey = "A13F7BC5721DB6D1F492B0FC333D4D9C";
     */
    OKX_UAT("0769b50c-2c36-4310-8bd9-cad6bc6c9d8f",
            "7AF4A574BC44907CE76BBFF91F53852D",
            "Aa123456@",
            false);
    private final String apiKey;
    private final String secretKey;
    private final String passphrase;
//    @Setting(label = "账户类型", order = 40, type = FieldType.SELECT, options = {"实盘账户", "模拟账户"}, optionsVal = {"true", "false"})
    private final boolean accountType;
    ExchangeInfoEnum(String apiKey, String secretKey, String passphrase, boolean accountType) {
        this.apiKey = apiKey;
        this.secretKey = secretKey;
        this.passphrase = passphrase;
        this.accountType = accountType;
    }
}
src/main/java/com/xcong/excoin/modules/newPrice/ExchangeLoginEventService.java
New file
@@ -0,0 +1,27 @@
package com.xcong.excoin.modules.newPrice;
import java.util.LinkedHashMap;
public interface ExchangeLoginEventService {
    /**
     * 获取交易产品基础信息
     * 获取所有可交易产品的信息列表。
     * <br><br>
     * GET /api/v5/public/instruments
     * <br>
     *
     * @param parameters LinkedHashedMap of String,Object pair
     *                   where String is the name of the parameter and Object is the value of the parameter
     *                   <br><br>
     *                   instType    -- String    是    产品类型 SPOT:币币 MARGIN:币币杠杆 SWAP:永续合约 FUTURES:交割合约 OPTION:期权 <br>
     *                   uly      -- String    可选    标的指数,仅适用于交割/永续/期权,期权必填 <br>
     *                   instFamily -- String    否    交易品种,仅适用于交割/永续/期权 <br>
     *                   instId -- String    否    产品ID <br>
     * @return String
     * @see <a href="https://www.okx.com/docs-v5/zh/#rest-api-public-data-get-instruments">
     * https://www.okx.com/docs-v5/zh/#rest-api-public-data-get-instruments</a>
     */
    String exchangeInfo(LinkedHashMap<String, Object> parameters);
    String lineHistory(LinkedHashMap<String, Object> parameters);
}
src/main/java/com/xcong/excoin/modules/newPrice/ExchangeLoginService.java
New file
@@ -0,0 +1,36 @@
package com.xcong.excoin.modules.newPrice;
import com.xcong.excoin.common.exception.FebsException;
import com.xcong.excoin.modules.newPrice.impl.ExchangeLoginEventServiceImpl;
import java.util.HashMap;
import java.util.Map;
public class ExchangeLoginService {
    private final static Map<String, ExchangeLoginEventService> eventMap = new HashMap<>();
    static {
        for (ExchangeInfoEnum infoEnum : ExchangeInfoEnum.values()) {
            eventMap.put(infoEnum.name(), new ExchangeLoginEventServiceImpl(
                    infoEnum.getApiKey(),
                    infoEnum.getSecretKey(),
                    infoEnum.getPassphrase(),
                    infoEnum.isAccountType()));
        }
    }
    private ExchangeLoginService() {
    }
    public final static ExchangeLoginService INSTANCE = new ExchangeLoginService();
    public static ExchangeLoginEventService getInstance(String exchangeType) {
        ExchangeLoginEventService exchange = eventMap.get(exchangeType);
        if (exchange == null) {
            throw new FebsException("参数错误");
        }
        return exchange;
    }
}
src/main/java/com/xcong/excoin/modules/newPrice/KlineVo.java
New file
@@ -0,0 +1,23 @@
package com.xcong.excoin.modules.newPrice;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
@Data
@ApiModel(value = "KlineVo", description = "请求K线类")
public class KlineVo implements Serializable {
    @NotNull
    @ApiModelProperty(value = "币种", example = "BTC/USDT")
    private String instId;
    @NotNull
    @ApiModelProperty(value = "类型 1-币币2-合约", example = "1")
    private Integer type;
}
src/main/java/com/xcong/excoin/modules/newPrice/OKXAccount.java
New file
@@ -0,0 +1,21 @@
package com.xcong.excoin.modules.newPrice;
import lombok.Data;
@Data
public class OKXAccount {
    String passPhrase;
    boolean isSimluate;
    public String baseUrl;
    public RequestHandler requestHandler;
    public boolean showLimitUsage;
    public OKXAccount(){}
    public OKXAccount(String baseUrl, String apiKey, String secretKey, String passPhrase,boolean isSimluate) {
        this.baseUrl = baseUrl;
        this.requestHandler = new RequestHandler(apiKey, secretKey,passPhrase);
        this.isSimluate = isSimluate;
    }
}
src/main/java/com/xcong/excoin/modules/newPrice/RequestHandler.java
New file
@@ -0,0 +1,125 @@
package com.xcong.excoin.modules.newPrice;
import com.alibaba.fastjson.JSON;
import com.xcong.excoin.common.exception.FebsException;
import com.xcong.excoin.modules.newPrice.enums.HttpMethod;
import com.xcong.excoin.modules.newPrice.enums.RequestType;
import com.xcong.excoin.modules.newPrice.utils.DateUtils;
import com.xcong.excoin.modules.newPrice.utils.RequestBuilder;
import com.xcong.excoin.modules.newPrice.utils.SignUtils;
import com.xcong.excoin.modules.newPrice.utils.UrlBuilder;
import lombok.extern.slf4j.Slf4j;
import okhttp3.Request;
import java.util.Date;
import java.util.LinkedHashMap;
@Slf4j
public class  RequestHandler {
    private final String apiKey;
    private final String secretKey;
    private final String passphrase;
    public RequestHandler(String apiKey) {
        this.apiKey = apiKey;
        this.secretKey = null;
        this.passphrase = null;
    }
    public RequestHandler(String apiKey, String secretKey, String passphrase) {
        this.apiKey = apiKey;
        this.secretKey = secretKey;
        this.passphrase = passphrase;
    }
    public static void main(String[] args) {
        LinkedHashMap<String, Object> balanceParameters = new LinkedHashMap<>();
        String queryString = UrlBuilder.joinQueryParameters(new StringBuilder("/api/v5/account/balance"), balanceParameters).toString();
        String balanceParameters1 = UrlBuilder.buildFullUrl("/api/v5/account/balance","" , balanceParameters, null);
        System.out.println(queryString);
        System.out.println(balanceParameters1);
    }
    /**
     * Build request based on request type and send the requests to server.
     *
     * @param baseUrl
     * @param urlPath
     * @param parameters
     * @param httpMethod
     * @param requestType
     * @return String - response from server
     */
    private String sendApiRequest(String baseUrl, String urlPath, LinkedHashMap<String, Object> parameters,
                                  HttpMethod httpMethod, RequestType requestType, boolean isSimluate) {
        String fullUrl = UrlBuilder.buildFullUrl(baseUrl, urlPath, parameters, null);
        log.debug("{} {}", httpMethod, fullUrl);
        //System.out.println("sendApiRequest:fullUrl"+fullUrl);
        Request request;
        switch (requestType) {
            case PUBLIC:
                request = RequestBuilder.buildPublicRequest(fullUrl, httpMethod, isSimluate).build();
                break;
            case WITH_API_KEY:
            case SIGNED:
                // 获取签名
                String timestamp = DateUtils.format(DateUtils.FORMAT_UTC_ISO8601, new Date(), 0);
                String queryString = UrlBuilder.joinQueryParameters(new StringBuilder(urlPath), parameters).toString();
                // String timestamp = System.currentTimeMillis()+"";
//                System.out.println("timestamp:"+timestamp);
//                System.out.println("timestamp:"+timestamp);
//                System.out.println("secretKey:"+secretKey);
//                System.out.println("httpMethod.toString():"+httpMethod.toString());
//                System.out.println("queryString:"+queryString);
//                System.out.println("passphrase:"+passphrase);
                // 组装body
                String body = "";
                if (HttpMethod.POST.equals(httpMethod)) {
                    body = JSON.toJSONString(parameters);
                    queryString = UrlBuilder.joinQueryParameters(new StringBuilder(urlPath), null).toString();
                    fullUrl = UrlBuilder.buildFullUrl(baseUrl, urlPath, null, null);
                }
                if (HttpMethod.GET.equals(httpMethod)) {
                    queryString = UrlBuilder.buildFullUrl(urlPath,"" , parameters, null);
//                    queryString = UrlBuilder.buildFullUrl(null, urlPath, parameters, null);
                }
                String sign = SignUtils.signRest(secretKey,
                        timestamp,
                        httpMethod.toString(),
                        queryString, body);
                request = RequestBuilder.buildApiKeyRequest(fullUrl, body, passphrase, sign, timestamp, httpMethod, apiKey,isSimluate);
                break;
            default:
                throw new FebsException("[RequestHandler] Invalid request type: " + requestType);
        }
        return ResponseHandler.handleResponse(request, isSimluate);
    }
    public String sendPublicRequest(String baseUrl, String urlPath, LinkedHashMap<String, Object> parameters,
                                    HttpMethod httpMethod, boolean isSimluate) {
        return sendApiRequest(baseUrl, urlPath, parameters, httpMethod, RequestType.PUBLIC, isSimluate);
    }
    public String sendWithApiKeyRequest(String baseUrl, String urlPath, LinkedHashMap<String, Object> parameters,
                                        HttpMethod httpMethod, boolean isSimluate) {
        if (null == apiKey || apiKey.isEmpty()) {
            throw new FebsException("[RequestHandler] API key cannot be null or empty!");
        }
        return sendApiRequest(baseUrl, urlPath, parameters, httpMethod, RequestType.WITH_API_KEY, isSimluate);
    }
    public String sendSignedRequest(String baseUrl, String urlPath, LinkedHashMap<String, Object> parameters,
                                    HttpMethod httpMethod, boolean isSimluate) {
        if (null == secretKey || secretKey.isEmpty() || null == apiKey || apiKey.isEmpty()) {
            throw new FebsException("[RequestHandler] Secret key/API key cannot be null or empty!");
        }
        //parameters.put("timestamp", UrlBuilder.buildTimestamp());
        //String queryString = UrlBuilder.joinQueryParameters(parameters);
        //String signature = SignatureGenerator.getSignature(queryString, secretKey);
        return sendApiRequest(baseUrl, urlPath, parameters, httpMethod, RequestType.SIGNED, isSimluate);
    }
}
src/main/java/com/xcong/excoin/modules/newPrice/ResponseHandler.java
New file
@@ -0,0 +1,66 @@
package com.xcong.excoin.modules.newPrice;
import cn.hutool.json.JSONException;
import com.xcong.excoin.common.exception.FebsException;
import com.xcong.excoin.modules.newPrice.utils.JSONParser;
import com.xcong.excoin.modules.newPrice.utils.OkHttpUtils;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import java.io.IOException;
public final class ResponseHandler {
    private static final int HTTP_STATUS_CODE_400 = 400;
    private static final int HTTP_STATUS_CODE_499 = 499;
    private static final int HTTP_STATUS_CODE_500 = 500;
    private ResponseHandler() {
    }
    public static String handleResponse(Request request, boolean showLimitUsage) {
        try {
            OkHttpUtils.builder();
            try (Response response = OkHttpUtils.okHttpClient.newCall(request).execute()) {//OkHttpUtils.builder().okHttpClient
                String responseAsString = getResponseBodyAsString(response.body());
                if (response.code() >= HTTP_STATUS_CODE_400 && response.code() <= HTTP_STATUS_CODE_499) {
                    throw handleErrorResponse(responseAsString, response.code());
                } else if (response.code() >= HTTP_STATUS_CODE_500) {
                    System.out.println("handleResponse:"+response.code());
                    throw new FebsException("responseAsString-"+responseAsString+";handleResponse-"+response.code());
                }
                return responseAsString;
    //            if (showLimitUsage) {
    //                return getlimitUsage(response, responseAsString);
    //            } else {
    //                return responseAsString;
    //            }
            }
        } catch (IOException | IllegalStateException e) {
            e.printStackTrace();
            throw new FebsException("[ResponseHandler] OKHTTP Error: " + e.getMessage());
        }
    }
    private static FebsException handleErrorResponse(String responseBody, int responseCode) {
        try {
            String errorMsg = JSONParser.getJSONStringValue(responseBody, "msg");
            int errorCode = JSONParser.getJSONIntValue(responseBody, "code");
            return new FebsException("responseBody-"+responseBody+";errorMsg-"+errorMsg+";responseCode-"+responseCode+";errorCode-"+errorCode);
        } catch (JSONException e) {
            throw new FebsException("responseBody-"+responseBody+";responseCode-"+responseCode);
        }
    }
    private static String getResponseBodyAsString(ResponseBody body) throws IOException {
        if (null != body) {
            return body.string();
        } else {
            return "";
        }
    }
}
src/main/java/com/xcong/excoin/modules/newPrice/enums/DefaultUrls.java
New file
@@ -0,0 +1,38 @@
package com.xcong.excoin.modules.newPrice.enums;
public final class DefaultUrls {
    public static final String USDM_UAT_URL = "https://www.okx.com";
    public static final String USDM_UAT_WSS_URL = "wss://wspap.okx.com:8443";
    //public static final String USDM_UAT_WSS_URL = "wss://ws.okx.com:8443";
    //USD-M Futures
    public static final String USDM_PROD_URL = "https://aws.okx.com";
    public static final String USDM_PROD_WS_URL = "wss://ws.okx.com:8443";
    //比特币买入数量
    public static final String BTC_BUYNUMBER = "0.001";
    //以太坊买入数量
    public static final String ETH_BUYNUMBER = "0.01";
    //狗狗币买入数量
    public static final String DOGE_BUYNUMBER = "100";
    /**
     * 全部卖出
     */
    public static final String OPERATION_ALLSOLD = "allsold";
    /**
     * 卖出
     */
    public static final String OPERATION_SOLD = "sell";
    /**
     * 买入
     */
    public static final String OPERATION_BUY = "buy";
    /**
     * 不买入
     */
    public static final String OPERATION_NOBUY = "nobuy";
    private DefaultUrls() {
    }
}
src/main/java/com/xcong/excoin/modules/newPrice/enums/HttpMethod.java
New file
@@ -0,0 +1,9 @@
package com.xcong.excoin.modules.newPrice.enums;
public enum HttpMethod {
    POST,
    GET,
    PUT,
    DELETE,
    INVALID
}
src/main/java/com/xcong/excoin/modules/newPrice/enums/RequestType.java
New file
@@ -0,0 +1,7 @@
package com.xcong.excoin.modules.newPrice.enums;
public enum RequestType {
    PUBLIC,
    WITH_API_KEY,
    SIGNED
}
src/main/java/com/xcong/excoin/modules/newPrice/impl/ExchangeLoginEventServiceImpl.java
New file
@@ -0,0 +1,50 @@
package com.xcong.excoin.modules.newPrice.impl;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.xcong.excoin.modules.newPrice.ExchangeLoginEventService;
import com.xcong.excoin.modules.newPrice.OKXAccount;
import com.xcong.excoin.modules.newPrice.enums.DefaultUrls;
import com.xcong.excoin.modules.newPrice.enums.HttpMethod;
import com.xcong.excoin.modules.newPrice.utils.OKXContants;
import lombok.extern.slf4j.Slf4j;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
@Slf4j
public class ExchangeLoginEventServiceImpl implements ExchangeLoginEventService {
    private final com.xcong.excoin.modules.newPrice.OKXAccount OKXAccount;
    private final String apiKey;
    private final String secretKey;
    private final String passphrase;
    private final boolean accountType;
    public ExchangeLoginEventServiceImpl(String apiKey, String secretKey, String passphrase, boolean accountType) {
        this.apiKey = apiKey;
        this.secretKey = secretKey;
        this.passphrase = passphrase;
        this.accountType = accountType;
        OKXAccount = new OKXAccount(
                accountType ? DefaultUrls.USDM_PROD_URL : DefaultUrls.USDM_UAT_URL,
                apiKey,
                secretKey,
                passphrase,
                !accountType);
    }
    @Override
    public String exchangeInfo(LinkedHashMap<String, Object> parameters) {
        return OKXAccount.requestHandler.sendPublicRequest(OKXAccount.baseUrl, OKXContants.INSTRUMENTS,parameters, HttpMethod.GET, OKXAccount.isSimluate());
    }
    @Override
    public String lineHistory(LinkedHashMap<String, Object> parameters) {
        return OKXAccount.requestHandler.sendPublicRequest(OKXAccount.baseUrl, OKXContants.K_LINE_HISTORY,parameters, HttpMethod.GET, OKXAccount.isSimluate());
    }
}
src/main/java/com/xcong/excoin/modules/newPrice/utils/DateUtils.java
New file
@@ -0,0 +1,80 @@
package com.xcong.excoin.modules.newPrice.utils;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.ZoneId;
import java.util.Date;
import java.util.TimeZone;
public class DateUtils {
    public static final String FORMAT_Y = "yyyy";
    public static final String FORMAT_D_1 = "yyyy/MM/dd";
    public static final String FORMAT_D_2 = "yyyy-MM-dd";
    public static final String FORMAT_D_3 = "yyyyMMdd";
    public static final String FORMAT_D_4 = "yyyy.MM.dd";
    public static final String FORMAT_D = "dd";
    public static final String FORMAT_DT_1 = "yyyy/MM/dd HH:mm:ss";
    public static final String FORMAT_DT_2 = "yyyy-MM-dd HH:mm:ss";
    public static final String FORMAT_DT_3 = "yyyyMMdd HH:mm:ss";
    public static final String FORMAT_DT_4 = "yyyy-MM-dd HH:mm";
    public static final String FORMAT_DT_5 = "yyyy.MM.dd HH:mm:ss";
    public static final String FORMAT_DT_6 = "yyyyMMddHHmmss";
    public static final String FORMAT_DT_7 = "yyyyMMddHH";
    public static final String FORMAT_M_1 = "yyyy/MM";
    public static final String FORMAT_M_2 = "yyyy-MM";
    public static final String FORMAT_M_3 = "yyyyMM";
    public static final String FORMAT_M = "MM";
    public static final String FORMAT_MD_1 = "MM/dd";
    public static final String FORMAT_MD_2 = "MM-dd";
    public static final String FORMAT_MD_3 = "MMdd";
    public static final String FORMAT_T_1 = "HH:mm:ss";
    public static final String FORMAT_T_2 = "HH:mm";
    public static final String FORMAT_TH = "HH";
    public static final String FORMAT_TM = "mm";
    public static final String FORMAT_TS = "ss";
    public static final String FORMAT_UTC_ISO8601 = "yyyy-MM-dd'T'HH:mm:ss'Z'";
    public static String format(String format, Date date) {
        SimpleDateFormat sdf = new SimpleDateFormat(format);
        return sdf.format(date);
    }
    /**
     * @param format   format
     * @param date     date
     * @param timeZone 时区数字 -8, 0, 8 等
     * @return date string
     */
    public static String format(String format, Date date, int timeZone) {
        timeZone = timeZone % 13;
        SimpleDateFormat sdf = new SimpleDateFormat(format);
        ZoneId zoneId = ZoneId.of("GMT" + (timeZone >= 0 ? "+" : "") + timeZone);
        TimeZone tz = TimeZone.getTimeZone(zoneId);
        sdf.setTimeZone(tz);
        return sdf.format(date);
    }
    public static Date parse(String dateString, String format) {
        SimpleDateFormat sdf = new SimpleDateFormat(format);
        try {
            return sdf.parse(dateString);
        } catch (ParseException var4) {
            return null;
        }
    }
    public static Date parse(String dateString, String format, int timeZone) {
        SimpleDateFormat sdf = new SimpleDateFormat(format);
        ZoneId zoneId = ZoneId.of("GMT" + (timeZone >= 0 ? "+" : "") + timeZone);
        TimeZone tz = TimeZone.getTimeZone(zoneId);
        sdf.setTimeZone(tz);
        try {
            return sdf.parse(dateString);
        } catch (ParseException var4) {
            return null;
        }
    }
}
src/main/java/com/xcong/excoin/modules/newPrice/utils/JSONParser.java
New file
@@ -0,0 +1,40 @@
package com.xcong.excoin.modules.newPrice.utils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
public final class JSONParser {
    private JSONParser() {
    }
    public static String getJSONStringValue(String json, String key) {
        try {
            JSONObject obj = new JSONObject(json);
            return obj.getString(key);
        } catch (JSONException e) {
            throw new JSONException(String.format("[JSONParser] Failed to get \"%s\"  from JSON object", key));
        }
    }
    public static int getJSONIntValue(String json, String key) {
        try {
            JSONObject obj = new JSONObject(json);
            return obj.getInt(key);
        } catch (JSONException e) {
            throw new JSONException(String.format("[JSONParser] Failed to get \"%s\" from JSON object", key));
        }
    }
    public static String getJSONArray(ArrayList<?> symbols, String key) {
        try {
            JSONArray arr = new JSONArray(symbols);
            return arr.toString();
        } catch (JSONException e) {
            throw new JSONException(String.format("[JSONParser] Failed to convert \"%s\" to JSON array", key));
        }
    }
}
src/main/java/com/xcong/excoin/modules/newPrice/utils/OKXContants.java
New file
@@ -0,0 +1,185 @@
package com.xcong.excoin.modules.newPrice.utils;
public class OKXContants {
    /**
     * 获取交易产品基础信息
     * 获取所有可交易产品的信息列表。
     * <br><br>
     * GET /api/v5/public/instruments
     * <br>
     *
     * @param parameters LinkedHashedMap of String,Object pair
     *                   where String is the name of the parameter and Object is the value of the parameter
     *                   <br><br>
     *                   instType    -- String    是    产品类型 SPOT:币币 MARGIN:币币杠杆 SWAP:永续合约 FUTURES:交割合约 OPTION:期权 <br>
     *                   uly      -- String    可选    标的指数,仅适用于交割/永续/期权,期权必填 <br>
     *                   instFamily -- String    否    交易品种,仅适用于交割/永续/期权 <br>
     *                   instId -- String    否    产品ID <br>
     * @return String
     * @see <a href="https://www.okx.com/docs-v5/zh/#rest-api-public-data-get-instruments">
     * https://www.okx.com/docs-v5/zh/#rest-api-public-data-get-instruments</a>
     */
    public static final String INSTRUMENTS = "/api/v5/public/instruments";
//    public static final String K_LINE_HISTORY = "/api/v5/market/history-candles";
    public static final String K_LINE_HISTORY = "/api/v5/market/history-mark-price-candles";
    /**
     * 查看账户余额
     * 获取交易账户中资金余额信息。
     * <br><br>
     * GET /api/v5/account/balance
     * <br>
     * @param
     * parameters LinkedHashedMap of String,Object pair
     *            where String is the name of the parameter and Object is the value of the parameter
     * <br><br>
     * ccy -- String    否    币种,如 BTC 支持多币种查询(不超过20个),币种之间半角逗号分隔 <br>
     * @return String
     * @see <a href="https://www.okx.com/docs-v5/zh/#rest-api-account-get-balance">
     *     https://www.okx.com/docs-v5/zh/#rest-api-account-get-balance</a>
     */
    public static final String BALANCE = "/api/v5/account/balance";
    /**
     * 查看持仓信息
     * 获取该账户下拥有实际持仓的信息。账户为单向持仓模式会显示净持仓(net),账户为双向持仓模式下会分别返回多头(long)或空头(short)的仓位。按照仓位创建时间倒序排列。
     * <br><br>
     * GET /api/v5/account/positions
     * <br>
     * @param
     * parameters LinkedHashedMap of String,Object pair
     *            where String is the name of the parameter and Object is the value of the parameter
     * <br><br>
     * instType -- String    否    产品类型
     * MARGIN:币币杠杆
     * SWAP:永续合约
     * FUTURES:交割合约
     * OPTION:期权
     * instType和instId同时传入的时候会校验instId与instType是否一致。<br>
     * instId -- String    否    交易产品ID,如:BTC-USD-190927-5000-C
     * 支持多个instId查询(不超过10个),半角逗号分隔<br>
     * posId --    String    否    持仓ID
     * 支持多个posId查询(不超过20个),半角逗号分割<br>
     * @return String <br>
     * note: 如果该 instId 拥有过仓位且当前持仓量为0,传 instId 时,会返回仓位信息;不传 instId 时,仓位信息不返回。
     *  逐仓交易设置中,如果设置为自主划转模式,逐仓转入保证金后,会生成一个持仓量为0的仓位 <br>
     * @see <a href="https://www.okx.com/docs-v5/zh/#rest-api-account-get-positions">
     *     https://www.okx.com/docs-v5/zh/#rest-api-account-get-positions</a>
     */
    public static final String POSITIONS = "/api/v5/account/positions";
    /**
     * 查看历史持仓信息
     * 获取最近3个月有更新的仓位信息,按照仓位更新时间倒序排列。
     * <br><br>
     * GET /api/v5/account/positions-history
     * <br>
     * @param
     * parameters LinkedHashedMap of String,Object pair
     *            where String is the name of the parameter and Object is the value of the parameter
     * <br><br>
     * instType -- String    否    产品类型
     * MARGIN:币币杠杆
     * SWAP:永续合约
     * FUTURES:交割合约
     * OPTION:期权 <br>
     * instId -- String    否    交易产品ID,如:BTC-USD-SWAP <br>
     * mgnMode -- String    否    保证金模式
     * cross:全仓,isolated:逐仓
     * type -- String    否    平仓类型
     * 1:部分平仓;2:完全平仓;3:强平;4:强减; 5:ADL自动减仓;
     * 状态叠加时,以最新的平仓类型为准状态为准。 <br>
     * posId --    String    否    持仓ID <br>
     * after --    String    否    查询仓位更新 (uTime) 之前的内容,值为时间戳,Unix 时间戳为毫秒数格式,如 1597026383085 <br>
     * before -- String    否    查询仓位更新 (uTime) 之后的内容,值为时间戳,Unix 时间戳为毫秒数格式,如 1597026383085 <br>
     * limit --    String    否    分页返回结果的数量,最大为100,默认100条 <br>
     * @return String
     * @see <a href="https://www.okx.com/docs-v5/zh/#rest-api-account-get-positions-history">
     *     https://www.okx.com/docs-v5/zh/#rest-api-account-get-positions-history</a>
     */
    public static final String POSITIONS_HISTORY = "/api/v5/account/positions-history";
    /**
     * 撤单
     * 撤销之前下的未完成订单。
     *
     * <br><br>
     * GET /api/v5/trade/cancel-order
     * <br>
     *
     * @param parameters LinkedHashedMap of String,Object pair
     *                   where String is the name of the parameter and Object is the value of the parameter
     *                   <br><br>
     *                   instId -- String    是    产品ID,如 BTC-USD-190927 <br>
     *                   ordId --    String    可选    订单ID, ordId和clOrdId必须传一个,若传两个,以ordId为主 <br>
     *                   clOrdId -- String    可选    用户自定义ID <br>
     * @return String
     * @see <a href="https://www.okx.com/docs-v5/zh/#rest-api-trade-cancel-order">
     * https://www.okx.com/docs-v5/zh/#rest-api-trade-cancel-order</a>
     */
    public static final String CANCEL_ORDER = "/api/v5/trade/cancel-order";
    /**
     * 下单
     * 只有当您的账户有足够的资金才能下单。
     *
     * <br><br>
     * GET /api/v5/trade/order
     * <br>
     *
     * @param parameters LinkedHashedMap of String,Object pair
     *                   where String is the name of the parameter and Object is the value of the parameter
     *                   <br><br>
     *                   instId -- String    是    产品ID,如 BTC-USD-190927-5000-C <br>
     *                   tdMode -- String    是    交易模式
     *                   保证金模式:isolated:逐仓 ;cross:全仓
     *                   非保证金模式:cash:非保证金 <br>
     *                   ccy -- String    否    保证金币种,仅适用于单币种保证金模式下的全仓杠杆订单 <br>
     *                   clOrdId -- String    否    客户自定义订单ID
     *                   字母(区分大小写)与数字的组合,可以是纯字母、纯数字且长度要在1-32位之间。<br>
     *                   tag -- String    否    订单标签
     *                   字母(区分大小写)与数字的组合,可以是纯字母、纯数字,且长度在1-16位之间。 <br>
     *                   side -- String    是    订单方向
     *                   buy:买, sell:卖 <br>
     *                   posSide -- String    可选    持仓方向
     *                   在双向持仓模式下必填,且仅可选择 long 或 short。 仅适用交割、永续。 <br>
     *                   ordType -- String    是    订单类型
     *                   market:市价单
     *                   limit:限价单
     *                   post_only:只做maker单
     *                   fok:全部成交或立即取消
     *                   ioc:立即成交并取消剩余
     *                   optimal_limit_ioc:市价委托立即成交并取消剩余(仅适用交割、永续) <br>
     *                   sz -- String    是    委托数量 <br>
     *                   px -- String    可选    委托价格,仅适用于limit、post_only、fok、ioc类型的订单 <br>
     *                   reduceOnly -- Boolean    否    是否只减仓,true 或 false,默认false
     *                   仅适用于币币杠杆,以及买卖模式下的交割/永续
     *                   仅适用于单币种保证金模式和跨币种保证金模式 <br>
     *                   tgtCcy -- String    否    市价单委托数量sz的单位,仅适用于币币市价订单
     *                   base_ccy: 交易货币 ;quote_ccy:计价货币
     *                   买单默认quote_ccy, 卖单默认base_ccy <br>
     *                   banAmend -- Boolean    否    是否禁止币币市价改单,true 或 false,默认false
     *                   为true时,余额不足时,系统不会改单,下单会失败,仅适用于币币市价单 <br>
     *                   tpTriggerPx -- String    否    止盈触发价,如果填写此参数,必须填写 止盈委托价 <br>
     *                   tpOrdPx -- String    否    止盈委托价,如果填写此参数,必须填写 止盈触发价
     *                   委托价格为-1时,执行市价止盈 <br>
     *                   slTriggerPx -- String    否    止损触发价,如果填写此参数,必须填写 止损委托价 <br>
     *                   slOrdPx -- String    否    止损委托价,如果填写此参数,必须填写 止损触发价
     *                   委托价格为-1时,执行市价止损 <br>
     *                   tpTriggerPxType -- String    否    止盈触发价类型
     *                   last:最新价格
     *                   index:指数价格
     *                   mark:标记价格
     *                   默认为last <br>
     *                   slTriggerPxType -- String    否    止损触发价类型
     *                   last:最新价格
     *                   index:指数价格
     *                   mark:标记价格
     *                   默认为last <br>
     * @return String
     * @see <a href="https://www.okx.com/docs-v5/zh/#rest-api-trade-place-order">
     * https://www.okx.com/docs-v5/zh/#rest-api-trade-place-order</a>
     */
    public static final String ORDER = "/api/v5/trade/order";
    /**
     * 获取币种价格信息
     */
    public static final String TICKER = "/api/v5/market/ticker";
}
src/main/java/com/xcong/excoin/modules/newPrice/utils/OkHttpUtils.java
New file
@@ -0,0 +1,330 @@
package com.xcong.excoin.modules.newPrice.utils;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import org.json.JSONObject;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.IOException;
import java.net.URLEncoder;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
/**
 * OkHttp请求工具封装
 * 参考:https://blog.csdn.net/DwZ735660836/article/details/119976068
 */
@Slf4j
public class OkHttpUtils {
    public static volatile OkHttpClient okHttpClient = null;
    private static volatile Semaphore semaphore = null;
    private Map<String, String> headerMap;
    private Map<String, String> paramMap;
    private String url;
    private Request.Builder request;
    // 开发环境用的 ShadowsocksR-dotnet4.0 免费版本 正式环境得使用外网服务器
    // 安易代理  http://127.0.0.1:10809/ http://127.0.0.1:10808/
    /**
     * 初始化okHttpClient,并且允许https访问
     */
    private OkHttpUtils() {
        if (okHttpClient == null) {
            synchronized (OkHttpUtils.class) {
                if (okHttpClient == null) {
                    TrustManager[] trustManagers = buildTrustManagers();
                    okHttpClient = new OkHttpClient.Builder()
                            .connectTimeout(30, TimeUnit.SECONDS)
                            .writeTimeout(20, TimeUnit.SECONDS)
                            .readTimeout(60, TimeUnit.SECONDS)
                            .sslSocketFactory(createSSLSocketFactory(trustManagers), (X509TrustManager) trustManagers[0])
                            //.hostnameVerifier((hostName, session) -> true)
                            //配置自定义连接池参数
                            .connectionPool(new ConnectionPool(5, 60, TimeUnit.SECONDS))
                            .retryOnConnectionFailure(true)
                            .build();
                    addHeader("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36");
                    addHeader("Connection", "close");
                    addHeader("Accept-Encoding", "identity");
                }
            }
        }
    }
    /**
     * 用于异步请求时,控制访问线程数,返回结果
     *
     * @return
     */
    private static Semaphore getSemaphoreInstance() {
        //只能1个线程同时访问
        synchronized (OkHttpUtils.class) {
            if (semaphore == null) {
                semaphore = new Semaphore(0);
            }
        }
        return semaphore;
    }
    /**
     * 创建OkHttpUtils
     *
     * @return
     */
    public static OkHttpUtils builder() {
        return new OkHttpUtils();
    }
    /**
     * 添加url
     *
     * @param url
     * @return
     */
    public OkHttpUtils url(String url) {
        this.url = url;
        return this;
    }
    /**
     * 添加参数
     *
     * @param key   参数名
     * @param value 参数值
     * @return
     */
    public OkHttpUtils addParam(String key, String value) {
        if (paramMap == null) {
            paramMap = new LinkedHashMap<>(16);
        }
        paramMap.put(key, value);
        return this;
    }
    /**
     * 添加请求头
     *
     * @param key   参数名
     * @param value 参数值
     * @return
     */
    public OkHttpUtils addHeader(String key, String value) {
        if (headerMap == null) {
            headerMap = new LinkedHashMap<>(16);
        }
        headerMap.put(key, value);
        return this;
    }
    /**
     * 初始化get方法
     *
     * @return
     */
    public OkHttpUtils get() {
        request = new Request.Builder().get();
        StringBuilder urlBuilder = new StringBuilder(url);
        if (paramMap != null) {
            urlBuilder.append("?");
            try {
                for (Map.Entry<String, String> entry : paramMap.entrySet()) {
                    urlBuilder.append(URLEncoder.encode(entry.getKey(), "utf-8")).
                            append("=").
                            append(URLEncoder.encode(entry.getValue(), "utf-8")).
                            append("&");
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            urlBuilder.deleteCharAt(urlBuilder.length() - 1);
        }
        request.url(urlBuilder.toString());
        return this;
    }
    /**
     * 初始化post方法
     *
     * @param isJsonPost true等于json的方式提交数据,类似postman里post方法的raw
     *                   false等于普通的表单提交
     * @return
     */
    public OkHttpUtils post(boolean isJsonPost) {
        RequestBody requestBody;
        if (isJsonPost) {
            String json = "";
            if (paramMap != null) {
                json = JSONObject.valueToString(paramMap);
            }
            requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), json);
        } else {
            FormBody.Builder formBody = new FormBody.Builder();
            if (paramMap != null) {
                paramMap.forEach(formBody::add);
            }
            requestBody = formBody.build();
        }
        request = new Request.Builder().post(requestBody).url(url);
        return this;
    }
    /**
     * 同步请求
     *
     * @return
     */
    public Request.Builder sync() {
        return setHeader(request);
    }
    /**
     * 同步请求
     *
     * @return
     */
    public String syncStr() {
        setHeader(request);
        try {
            Response response = okHttpClient.newCall(request.build()).execute();
            assert response.body() != null;
            return response.body().string();
        } catch (IOException e) {
            e.printStackTrace();
            return "请求失败:" + e.getMessage();
        }
    }
    /**
     * 异步请求,有返回值
     */
    public String async() {
        StringBuilder buffer = new StringBuilder();
        setHeader(request);
        okHttpClient.newCall(request.build()).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                buffer.append("请求出错:").append(e.getMessage());
            }
            @Override
            public void onResponse(Call call, Response response) throws IOException {
                assert response.body() != null;
                buffer.append(response.body().string());
                getSemaphoreInstance().release();
            }
        });
        try {
            getSemaphoreInstance().acquire();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return buffer.toString();
    }
    /**
     * 异步请求,带有接口回调
     *
     * @param callBack
     */
    public void async(ICallBack callBack) {
        setHeader(request);
        okHttpClient.newCall(request.build()).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                callBack.onFailure(call, e.getMessage());
            }
            @Override
            public void onResponse(Call call, Response response) throws IOException {
                assert response.body() != null;
                callBack.onSuccessful(call, response.body().string());
            }
        });
    }
    /**
     * 为request添加请求头
     *
     * @param request
     */
    private Request.Builder setHeader(Request.Builder request) {
        if (headerMap != null) {
            try {
                for (Map.Entry<String, String> entry : headerMap.entrySet()) {
                    request.addHeader(entry.getKey(), entry.getValue());
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return request;
    }
    /**
     * 生成安全套接字工厂,用于https请求的证书跳过
     *
     * @return
     */
    private static SSLSocketFactory createSSLSocketFactory(TrustManager[] trustAllCerts) {
        SSLSocketFactory ssfFactory = null;
        try {
            SSLContext sc = SSLContext.getInstance("SSL");
            sc.init(null, trustAllCerts, new SecureRandom());
            ssfFactory = sc.getSocketFactory();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return ssfFactory;
    }
    private static TrustManager[] buildTrustManagers() {
        return new TrustManager[]{
                new X509TrustManager() {
                    @Override
                    public void checkClientTrusted(X509Certificate[] chain, String authType) {
                    }
                    @Override
                    public void checkServerTrusted(X509Certificate[] chain, String authType) {
                    }
                    @Override
                    public X509Certificate[] getAcceptedIssuers() {
                        return new X509Certificate[]{};
                    }
                }
        };
    }
    /**
     * 自定义一个接口回调
     */
    public interface ICallBack {
        void onSuccessful(Call call, String data);
        void onFailure(Call call, String errorMsg);
    }
    public static void main(String[] args) {
        String url = "https://api2.binance.com/api/v3/ticker/24hr?symbol=BNBUSDT&type=MINI";
        String result = OkHttpUtils.builder()
                .url(url)
                .addHeader("Content-Type", "application/x-www-form-urlencoded")
                .get()
                .syncStr();
        System.out.println(result);
    }
}
src/main/java/com/xcong/excoin/modules/newPrice/utils/ParameterChecker.java
New file
@@ -0,0 +1,36 @@
package com.xcong.excoin.modules.newPrice.utils;
import cc.mrbird.febs.common.exception.FebsException;
import java.util.LinkedHashMap;
public final class ParameterChecker {
    private ParameterChecker() {
    }
    public static void checkParameter(LinkedHashMap<String, Object> parameters, String parameter, Class t) {
        checkRequiredParameter(parameters, parameter);
        checkParameterType(parameters.get(parameter), t, parameter);
    }
    public static void checkOrParameters(LinkedHashMap<String, Object> parameters, String parameter, String parameter2) {
        if (!parameters.containsKey(parameter) && (!parameters.containsKey(parameter2))) {
            throw new FebsException(String.format("Either \"%s\" or \"%s\" is required!", parameter, parameter2));
        }
    }
    public static void checkRequiredParameter(LinkedHashMap<String, Object> parameters, String parameter) {
        if (!parameters.containsKey(parameter)) {
            throw new FebsException(String.format("\"%s\" is a mandatory parameter!", parameter));
        }
    }
    public static void checkParameterType(Object parameter, Class t, String name) {
        if (!t.isInstance(parameter)) {
            throw new FebsException(String.format("\"%s\" must be of %s type.", name, t));
        } else if (t == String.class && parameter.toString().trim().equals("")) {
            throw new FebsException(String.format("\"%s\" must not be empty.", name));
        }
    }
}
src/main/java/com/xcong/excoin/modules/newPrice/utils/RequestBuilder.java
New file
@@ -0,0 +1,150 @@
package com.xcong.excoin.modules.newPrice.utils;
import com.xcong.excoin.common.exception.FebsException;
import com.xcong.excoin.modules.newPrice.enums.HttpMethod;
import okhttp3.MediaType;
import okhttp3.Request;
import okhttp3.RequestBody;
public final class RequestBuilder {
    private static final MediaType JSON_TYPE = MediaType.parse("application/json; charset=utf-8");
    private RequestBuilder() {
    }
    public static Request.Builder buildPublicRequest(String fullUrl, HttpMethod httpMethod, boolean issimulated) {
        try {
            final Request.Builder result;
            switch (httpMethod) {
                case POST:
                    OkHttpUtils builder = OkHttpUtils.builder();
                    if(issimulated){
                        builder.addHeader("x-simulated-trading","1");
                    }
                    result = builder
                            .url(fullUrl)
                            .post(true)
                            .sync();
                    break;
                case GET:
                    OkHttpUtils builder1 = OkHttpUtils.builder();
                    if(issimulated){
                        builder1.addHeader("x-simulated-trading","1");
                    }
                    result = builder1
                            .url(fullUrl)
                            .addHeader("Content-Type", "application/x-www-form-urlencoded")
                            .get()
                            .sync();
                    break;
                case PUT:
                    OkHttpUtils builder2 = OkHttpUtils.builder();
                    if(issimulated){
                        builder2.addHeader("x-simulated-trading","1");
                    }
                    result = builder2
                            .url(fullUrl)
                            .addHeader("Content-Type", "application/x-www-form-urlencoded")
                            .post(false)
                            .sync();
                    break;
                case DELETE:
                    OkHttpUtils builder3 = OkHttpUtils.builder();
                    if(issimulated){
                        builder3.addHeader("x-simulated-trading","1");
                    }
                    result = builder3
                            .url(fullUrl)
                            .post(false)
                            .sync();
                    break;
                default:
                    throw new FebsException("Invalid HTTP method: " + httpMethod);
            }
            return result;
        } catch (IllegalArgumentException e) {
            throw new FebsException("Invalid URL: " + e.getMessage());
        }
    }
    public static Request buildApiKeyRequest(String fullUrl,String body,String passphrase,String sign,String timeStamp, HttpMethod httpMethod, String apiKey,boolean issimulate) {
        try {
            final Request request;
            switch (httpMethod) {
                case POST:
                    Request.Builder builder = new Request.Builder();
                    if(issimulate){
                        builder.addHeader("x-simulated-trading","1");
                    }
                    request = builder
                            .url(fullUrl)
                            .post(RequestBody.create(JSON_TYPE, body))
                            .addHeader("OK-ACCESS-KEY", apiKey)
                            .addHeader("OK-ACCESS-SIGN", sign)
                            .addHeader("OK-ACCESS-TIMESTAMP", timeStamp)
                            .addHeader("OK-ACCESS-PASSPHRASE", passphrase)
                            .build();
                    break;
                case GET:
                    Request.Builder builder1 = new Request.Builder();
                    if(issimulate){
                        builder1.addHeader("x-simulated-trading","1");
                    }
                    request = builder1
                            .url(fullUrl)
                            .get()
                            .addHeader("Content-Type", "application/x-www-form-urlencoded")
                            .addHeader("OK-ACCESS-KEY", apiKey)
                            .addHeader("OK-ACCESS-KEY", apiKey)
                            .addHeader("OK-ACCESS-SIGN", sign)
                            .addHeader("OK-ACCESS-TIMESTAMP", timeStamp)
                            .addHeader("OK-ACCESS-PASSPHRASE", passphrase)
                            .build();
                    break;
                case PUT:
                    Request.Builder builder2 = new Request.Builder();
                    if(issimulate){
                        builder2.addHeader("x-simulated-trading","1");
                    }
                    request = builder2
                            .url(fullUrl)
                            .put(RequestBody.create(JSON_TYPE, ""))
                            .addHeader("Content-Type", "application/x-www-form-urlencoded")
                            .addHeader("OK-ACCESS-KEY", apiKey)
                            .addHeader("OK-ACCESS-KEY", apiKey)
                            .addHeader("OK-ACCESS-SIGN", sign)
                            .addHeader("OK-ACCESS-TIMESTAMP", timeStamp)
                            .addHeader("OK-ACCESS-PASSPHRASE", passphrase)
                            .build();
                    break;
                case DELETE:
                    Request.Builder builder3 = new Request.Builder();
                    if(issimulate){
                        builder3.addHeader("x-simulated-trading","1");
                    }
                    request = builder3
                            .url(fullUrl)
                            .delete()
                            .addHeader("Content-Type", "application/x-www-form-urlencoded")
                            .addHeader("OK-ACCESS-KEY", apiKey)
                            .addHeader("OK-ACCESS-KEY", apiKey)
                            .addHeader("OK-ACCESS-SIGN", sign)
                            .addHeader("OK-ACCESS-TIMESTAMP", timeStamp)
                            .addHeader("OK-ACCESS-PASSPHRASE", passphrase)
                            .build();
                    break;
                default:
                    throw new FebsException("Invalid HTTP method: " + httpMethod);
            }
            return request;
        } catch (IllegalArgumentException e) {
            throw new FebsException("Invalid URL: " + e.getMessage());
        }
    }
    public static Request buildWebsocketRequest(String fullUrl) {
        return new Request.Builder().url(fullUrl).build();
    }
}
src/main/java/com/xcong/excoin/modules/newPrice/utils/SignUtils.java
New file
@@ -0,0 +1,55 @@
package com.xcong.excoin.modules.newPrice.utils;
import lombok.extern.slf4j.Slf4j;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
@Slf4j
public class SignUtils {
    public static String signRest(String secretKey, String timestamp, String method, String path, String body) {
        String str = String.format("%s%s%s%s",
                timestamp, // timestamp
                method,  // method GET/POST
                path, // requestPath
                body // body
        );
        try {
            return Base64.getEncoder().encodeToString(hmacSHA256(secretKey.getBytes(), str.getBytes()));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    /**
     * HmacSHA256算法,返回的结果始终是32位
     *
     * @param key     加密的键,可以是任何数据
     * @param content 待加密的内容
     * @return 加密后的内容
     * @throws Exception ex
     */
    public static byte[] hmacSHA256(byte[] key, byte[] content) throws Exception {
        Mac hmacSha256 = Mac.getInstance("HmacSHA256");
        hmacSha256.init(new SecretKeySpec(key, 0, key.length, "HmacSHA256"));
        return hmacSha256.doFinal(content);
    }
    public static String signWebsocket(String timestamp, String secretKey) {
        String str = String.format("%s%s%s",
                timestamp,
                "GET",
                "/users/self/verify");
        try {
            return Base64.getEncoder().encodeToString(hmacSHA256(secretKey.getBytes(), str.getBytes()));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}
src/main/java/com/xcong/excoin/modules/newPrice/utils/UrlBuilder.java
New file
@@ -0,0 +1,128 @@
package com.xcong.excoin.modules.newPrice.utils;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.Map;
public final class UrlBuilder {
    private static final int MAX_DECIMAL_DIGITS = 30;
    private static DecimalFormat df;
    private UrlBuilder() {
    }
    public static String buildFullUrl(String baseUrl, String urlPath, LinkedHashMap<String, Object> parameters, String signature) {
        if (parameters != null && !parameters.isEmpty()) {
            StringBuilder sb = new StringBuilder(baseUrl);
            sb.append(urlPath).append('?');
            joinQueryParameters(sb, parameters);
            if (null != signature) {
                sb.append("&signature=").append(signature);
            }
            return sb.toString();
        } else {
            return baseUrl + urlPath;
        }
    }
    public static String buildStreamUrl(String baseUrl, ArrayList<String> streams) {
        StringBuilder sb = new StringBuilder(baseUrl);
        sb.append("?streams=");
        return joinStreamUrls(sb, streams);
    }
    //concatenate query parameters
    public static String joinQueryParameters(LinkedHashMap<String, Object> parameters) {
        return joinQueryParameters(new StringBuilder(), parameters).toString();
    }
    public static StringBuilder joinQueryParameters(StringBuilder urlPath, LinkedHashMap<String, Object> parameters) {
        if (parameters == null || parameters.isEmpty()) {
            return urlPath;
        }
        boolean isFirst = true;
        for (Map.Entry<String, Object> mapElement : parameters.entrySet()) {
            if (mapElement.getValue() instanceof Double) {
                parameters.replace(mapElement.getKey(), getFormatter().format(mapElement.getValue()));
            } else if (mapElement.getValue() instanceof ArrayList) {
                if (((ArrayList<?>) mapElement.getValue()).isEmpty()) {
                    continue;
                }
                String key = mapElement.getKey();
                joinArrayListParameters(key, urlPath, (ArrayList<?>) mapElement.getValue(), isFirst);
                isFirst = false;
                continue;
            }
            if (isFirst) {
                isFirst = false;
            } else {
                urlPath.append('&');
            }
            urlPath.append(mapElement.getKey())
                .append('=')
                .append(urlEncode(mapElement.getValue().toString()));
        }
        return urlPath;
    }
    private static void joinArrayListParameters(String key, StringBuilder urlPath, ArrayList<?> values, boolean isFirst) {
        for (Object value: values) {
            if (isFirst) {
                isFirst = false;
            } else {
                urlPath.append('&');
            }
            urlPath.append(key)
                    .append('=')
                    .append(urlEncode(value.toString()));
        }
    }
    private static String joinStreamUrls(StringBuilder urlPath, ArrayList<String> streams) {
        boolean isFirst = true;
        for (String stream: streams) {
            if (isFirst) {
                isFirst = false;
            } else {
                urlPath.append('/');
            }
            urlPath.append(stream);
        }
        return urlPath.toString();
    }
    public static String urlEncode(String s) {
        try {
            return URLEncoder.encode(s, StandardCharsets.UTF_8.name());
        } catch (UnsupportedEncodingException e) {
            // UTF-8 being unsuppored is unlikely
            // Replace with a unchecked exception to tidy up exception handling
            throw new RuntimeException(StandardCharsets.UTF_8.name() + " is unsupported", e);
        }
    }
    private static DecimalFormat getFormatter() {
        if (null == df) {
            df = new DecimalFormat();
            df.setMaximumFractionDigits(MAX_DECIMAL_DIGITS);
            df.setGroupingUsed(false);
        }
        return df;
    }
    public static String buildTimestamp() {
        return String.valueOf(System.currentTimeMillis());
    }
}
src/main/java/com/xcong/excoin/modules/symbols/controller/SymbolsController.java
@@ -2,6 +2,7 @@
import com.huobi.client.model.Candlestick;
import com.xcong.excoin.common.response.Result;
import com.xcong.excoin.modules.newPrice.KlineVo;
import com.xcong.excoin.modules.symbols.parameter.dto.KlineDetailDto;
import com.xcong.excoin.modules.symbols.parameter.vo.HomeSymbolsVo;
import com.xcong.excoin.modules.symbols.parameter.vo.KlineDataVo;
@@ -61,6 +62,15 @@
        return symbolsService.findKlineDetails(klineDetailDto);
    }
    @ApiOperation(value = "查询历史OKXK线数据", notes = "查询历史OKXK线数据")
    @ApiResponses({
            @ApiResponse(code = 0, message = "success", response = KlineDataVo.class)
    })
    @PostMapping(value = "/klineDetail")
    public Result klineDetail(@RequestBody @Valid KlineVo klineDetailDto) {
        return symbolsService.findKlineList(klineDetailDto);
    }
    @ApiOperation(value = "查询当日最高最低价")
    @GetMapping(value = "/getDayHighAndLow")
    public Result getDayHighAndLow(@ApiParam(name = "symbol", value = "币种", required = true, example = "BTC/USDT") @RequestParam(value = "symbol") String symbol) {
src/main/java/com/xcong/excoin/modules/symbols/service/SymbolsService.java
@@ -2,6 +2,7 @@
import com.xcong.excoin.common.response.Result;
import com.xcong.excoin.modules.newPrice.KlineVo;
import com.xcong.excoin.modules.symbols.parameter.dto.KlineDetailDto;
/**
@@ -18,4 +19,5 @@
    public Result findKlineDetails(KlineDetailDto klineDetailDto);
    public Result findKlineList(KlineVo klineDetailDto);
}
src/main/java/com/xcong/excoin/modules/symbols/service/impl/SymbolsServiceImpl.java
@@ -1,12 +1,18 @@
package com.xcong.excoin.modules.symbols.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.huobi.client.model.Candlestick;
import com.xcong.excoin.common.contants.AppContants;
import com.xcong.excoin.common.enumerates.SymbolEnum;
import com.xcong.excoin.common.response.Result;
import com.xcong.excoin.common.system.mapper.CandlestickMapper;
import com.xcong.excoin.modules.newPrice.ExchangeInfoEnum;
import com.xcong.excoin.modules.newPrice.ExchangeLoginService;
import com.xcong.excoin.modules.newPrice.KlineVo;
import com.xcong.excoin.modules.platform.dao.PlatformCnyUsdtExchangeDao;
import com.xcong.excoin.modules.platform.entity.PlatformCnyUsdtExchangeEntity;
import com.xcong.excoin.modules.symbols.parameter.dto.KlineDetailDto;
@@ -14,6 +20,7 @@
import com.xcong.excoin.modules.symbols.parameter.vo.KlineDataVo;
import com.xcong.excoin.modules.symbols.service.SymbolsService;
import com.xcong.excoin.utils.CoinTypeConvert;
import com.xcong.excoin.utils.MessageSourceUtils;
import com.xcong.excoin.utils.RedisUtils;
import com.xcong.excoin.utils.api.ApiClient;
import com.xcong.excoin.utils.api.response.Kline;
@@ -24,6 +31,8 @@
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
/**
@@ -179,4 +188,25 @@
        }
        return Result.fail("获取数据失败");
    }
    @Override
    public Result findKlineList(KlineVo klineDetailDto) {
        Integer type = klineDetailDto.getType();
        String instId = null;
        if (AppContants.HOME_SYMBOLS_COIN == type){
            instId = CoinTypeConvert.convertToCoinKlineKey(klineDetailDto.getInstId());
        }else if (AppContants.HOME_SYMBOLS_CONTRACT == type){
            instId = CoinTypeConvert.convertToKlineKey(klineDetailDto.getInstId());
        }else{
            return Result.fail("参数错误");
        }
        LinkedHashMap<String, Object> requestParam = new LinkedHashMap<>();
        requestParam.put("instId",instId);
        String result = ExchangeLoginService.getInstance(ExchangeInfoEnum.OKX_UAT.name()).lineHistory(requestParam);
        log.info("加载OKX-KLINE,{}", result);
        JSONObject json = JSON.parseObject(result);
        String data = json.getString("data");
        List<String[]> klinesList = JSON.parseArray(data, String[].class);
        return Result.ok(klinesList);
    }
}
src/main/java/com/xcong/excoin/utils/CoinTypeConvert.java
@@ -53,6 +53,44 @@
        }
    }
    public static String convertToCoinKlineKey(String symbol) {
        switch (symbol) {
            case "BTC/USDT":
                return "BTC-USDT";
            case "ETH/USDT":
                return "ETH-USDT";
            case "XRP/USDT":
                return "XRP-USDT";
            case "LTC/USDT":
                return "LTC-USDT";
            case "BCH/USDT":
                return "BCH-USDT";
            case "ETC/USDT":
                return "ETC-USDT";
            default:
                return null;
        }
    }
    public static String convertToKlineKey(String symbol) {
        switch (symbol) {
            case "BTC/USDT":
                return "BTC-USDT-SWAP";
            case "ETH/USDT":
                return "ETH-USDT-SWAP";
            case "XRP/USDT":
                return "XRP-USDT-SWAP";
            case "LTC/USDT":
                return "LTC-USDT-SWAP";
            case "BCH/USDT":
                return "BCH-USDT-SWAP";
            case "ETC/USDT":
                return "ETC-USDT-SWAP";
            default:
                return null;
        }
    }
    public static String convertToOpenKey(String symbol) {