package com.xcong.excoin.modules.gateApi.wsHandler; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import com.xcong.excoin.modules.gateApi.GateGridTradeService; import lombok.extern.slf4j.Slf4j; import org.java_websocket.client.WebSocketClient; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; /** * 私有频道处理器的抽象基类。 * *

封装内容

* * *

签名算法

* {@code SIGN = Hex(HmacSHA512(secret_utf8, "channel={channel}&event={event}&time={timeSec}"_utf8))} * *

子类

* {@link com.xcong.excoin.modules.gateApi.wsHandler.handler.PositionsChannelHandler}、 * {@link com.xcong.excoin.modules.gateApi.wsHandler.handler.PositionClosesChannelHandler} * * @author Administrator */ @Slf4j public abstract class AbstractPrivateChannelHandler implements GateChannelHandler { private static final char[] HEX_ARRAY = "0123456789abcdef".toCharArray(); private final String channelName; private final String apiKey; private final String apiSecret; private final String contract; private final GateGridTradeService gridTradeService; public AbstractPrivateChannelHandler(String channelName, String apiKey, String apiSecret, String contract, GateGridTradeService gridTradeService) { this.channelName = channelName; this.apiKey = apiKey; this.apiSecret = apiSecret; this.contract = contract; this.gridTradeService = gridTradeService; } /** @return 频道名称(如 "futures.positions") */ @Override public String getChannelName() { return channelName; } /** * 发送带签名的订阅请求。 * *

请求格式

*
     * {
     *   "id":     <唯一请求ID>,
     *   "time":   <unix时间戳(秒)>,
     *   "channel":"futures.positions",
     *   "event":  "subscribe",
     *   "payload":[userId, contract],
     *   "auth":   {"method":"api_key", "KEY":<APIKEY>, "SIGN":<HMAC-SHA512签名>}
     * }
     * 
*/ @Override public void subscribe(WebSocketClient ws) { long timeSec = System.currentTimeMillis() / 1000; JSONObject msg = buildAuthRequest("subscribe", buildUid(), timeSec); ws.send(msg.toJSONString()); log.info("[{}] 订阅成功, 合约:{}", channelName, contract); } /** * 发送带签名的取消订阅请求,与 subscribe 结构一致。 * payload: [contract],无 userId(取消订阅不需要用户ID)。 */ @Override public void unsubscribe(WebSocketClient ws) { long timeSec = System.currentTimeMillis() / 1000; JSONObject msg = new JSONObject(); msg.put("id", timeSec * 1000000 + (System.currentTimeMillis() % 1000)); msg.put("time", timeSec); msg.put("channel", channelName); msg.put("event", "unsubscribe"); JSONArray payload = new JSONArray(); payload.add(contract); msg.put("payload", payload); JSONObject auth = new JSONObject(); auth.put("method", "api_key"); auth.put("KEY", apiKey); auth.put("SIGN", hs512Sign("unsubscribe", timeSec)); msg.put("auth", auth); ws.send(msg.toJSONString()); log.info("[{}] 取消订阅成功, 合约:{}", channelName, contract); } /** @return 网格交易服务实例 */ protected GateGridTradeService getGridTradeService() { return gridTradeService; } /** @return 当前订阅的合约名称 */ protected String getContract() { return contract; } /** * 从策略服务获取用户 ID,用于私有频道订阅的 payload[0]。 * * @return 用户 ID 字符串,获取失败返回空字符串 */ private String buildUid() { return gridTradeService != null && gridTradeService.getUserId() != null ? String.valueOf(gridTradeService.getUserId()) : ""; } /** * 构建认证请求 JSON。 * 包含 id、time、channel、event、payload[userId, contract]、auth 字段。 * * @param event 事件类型("subscribe" / "unsubscribe") * @param uid 认证用户 ID * @param timeSec unix 时间戳(秒) * @return 完整的认证请求 JSONObject */ private JSONObject buildAuthRequest(String event, String uid, long timeSec) { JSONObject msg = new JSONObject(); msg.put("id", timeSec * 1000000 + (System.currentTimeMillis() % 1000)); msg.put("time", timeSec); msg.put("channel", channelName); msg.put("event", event); JSONArray payload = new JSONArray(); payload.add(uid); payload.add(contract); msg.put("payload", payload); JSONObject auth = new JSONObject(); auth.put("method", "api_key"); auth.put("KEY", apiKey); auth.put("SIGN", hs512Sign(event, timeSec)); msg.put("auth", auth); return msg; } /** * HMAC-SHA512 签名计算。 * *

签名算法

*
     *   message = "channel={channelName}&event={event}&time={timeSec}"
     *   SIGN = Hex(HmacSHA512(apiSecret(UTF-8), message(UTF-8)))
     * 
* *

错误处理

* 签名计算失败时返回空字符串(日志记录错误),不抛异常, * 避免阻塞 WebSocket 回调线程。 * * @param event 事件类型 * @param timeSec unix 时间戳(秒) * @return 十六进制签名字符串,失败返回 "" */ private String hs512Sign(String event, long timeSec) { try { String message = "channel=" + channelName + "&event=" + event + "&time=" + timeSec; Mac mac = Mac.getInstance("HmacSHA512"); SecretKeySpec spec = new SecretKeySpec(apiSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA512"); mac.init(spec); byte[] hash = mac.doFinal(message.getBytes(StandardCharsets.UTF_8)); StringBuilder hex = new StringBuilder(hash.length * 2); for (byte b : hash) { hex.append(HEX_ARRAY[(b >> 4) & 0xF]); hex.append(HEX_ARRAY[b & 0xF]); } return hex.toString(); } catch (Exception e) { log.error("[{}] 签名计算失败", channelName, e); return ""; } } }