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;
|
|
/**
|
* 私有频道处理器的抽象基类。
|
*
|
* <h3>封装内容</h3>
|
* <ul>
|
* <li>HMAC-SHA512 签名计算(UTF-8 编码)</li>
|
* <li>认证请求 JSON 构建(id/time/channel/payload/auth)</li>
|
* <li>subscribe / unsubscribe 的默认实现(含签名)</li>
|
* <li>用户 ID 获取(从 {@link GateGridTradeService#getUserId()})</li>
|
* </ul>
|
*
|
* <h3>签名算法</h3>
|
* {@code SIGN = Hex(HmacSHA512(secret_utf8, "channel={channel}&event={event}&time={timeSec}"_utf8))}
|
*
|
* <h3>子类</h3>
|
* {@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; }
|
|
/**
|
* 发送带签名的订阅请求。
|
*
|
* <h3>请求格式</h3>
|
* <pre>
|
* {
|
* "id": <唯一请求ID>,
|
* "time": <unix时间戳(秒)>,
|
* "channel":"futures.positions",
|
* "event": "subscribe",
|
* "payload":[userId, contract],
|
* "auth": {"method":"api_key", "KEY":<APIKEY>, "SIGN":<HMAC-SHA512签名>}
|
* }
|
* </pre>
|
*/
|
@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 签名计算。
|
*
|
* <h3>签名算法</h3>
|
* <pre>
|
* message = "channel={channelName}&event={event}&time={timeSec}"
|
* SIGN = Hex(HmacSHA512(apiSecret(UTF-8), message(UTF-8)))
|
* </pre>
|
*
|
* <h3>错误处理</h3>
|
* 签名计算失败时返回空字符串(日志记录错误),不抛异常,
|
* 避免阻塞 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 "";
|
}
|
}
|
}
|