Administrator
12 hours ago a986f3571c7e18ade4665fe5999b445b5762264d
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
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":     &lt;唯一请求ID&gt;,
     *   "time":   &lt;unix时间戳(秒)&gt;,
     *   "channel":"futures.positions",
     *   "event":  "subscribe",
     *   "payload":[userId, contract],
     *   "auth":   {"method":"api_key", "KEY":&lt;APIKEY&gt;, "SIGN":&lt;HMAC-SHA512签名&gt;}
     * }
     * </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 "";
        }
    }
}