Administrator
9 hours ago 1980ad6ec3dedaf51f760ad478ea07ecaffe7287
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
package com.xcong.excoin.modules.gateApi;
 
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
 
import javax.annotation.PostConstruct;
import java.io.File;
import java.io.IOException;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
 
/**
 * Gate 配置持久化服务 — 按 apiKey 分文件存储。
 * 文件路径: config/gate-config-{md5(apiKey)}.json
 */
@Slf4j
@Service
public class GateConfigPersistenceService {
 
    private static final String CONFIG_DIR = "config";
    private static final String CONFIG_PREFIX = "gate-config-";
    private static final String JSON_SUFFIX = ".json";
 
    private final ObjectMapper mapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);
 
    @PostConstruct
    public void ensureConfigDir() {
        File dir = new File(CONFIG_DIR);
        if (!dir.exists()) {
            dir.mkdirs();
        }
    }
 
    /**
     * 确保指定 apiKey 的配置文件存在,不存在则用默认值创建。
     */
    public GateConfigDTO ensureExists(String apiKey, GateConfigDTO defaults) {
        GateConfigDTO existing = load(apiKey);
        if (existing != null) {
            return existing;
        }
        save(apiKey, defaults);
        log.info("[GateConfig] 为 apiKey={} 创建默认配置文件", maskKey(apiKey));
        return defaults;
    }
 
    /**
     * 保存配置。
     */
    public void save(String apiKey, GateConfigDTO dto) {
        try {
            mapper.writeValue(configFile(apiKey), dto);
            log.info("[GateConfig] 配置已保存, apiKey={}", maskKey(apiKey));
        } catch (IOException e) {
            log.error("[GateConfig] 保存配置失败, apiKey={}", maskKey(apiKey), e);
            throw new RuntimeException("保存配置失败", e);
        }
    }
 
    /**
     * 加载配置,文件不存在返回 null。
     */
    public GateConfigDTO load(String apiKey) {
        File file = configFile(apiKey);
        if (!file.exists()) {
            return null;
        }
        try {
            return mapper.readValue(file, GateConfigDTO.class);
        } catch (IOException e) {
            log.error("[GateConfig] 读取配置失败, apiKey={}", maskKey(apiKey), e);
            return null;
        }
    }
 
    /**
     * 从 DTO 构建 GateConfig(补齐 apiSecret)。
     */
    public GateConfig buildFromDTO(GateConfigDTO dto, String apiKey) {
        return GateConfig.builder()
                .apiKey(apiKey)
                .apiSecret(GateWebSocketClientManager.DEFAULT_API_SECRET)
                // 不可调参数 — 参考 GateWebSocketClientManager 顶部常量
                .contract(GateWebSocketClientManager.DEFAULT_CONTRACT)
                .leverage(GateWebSocketClientManager.DEFAULT_LEVERAGE)
                .marginMode(GateWebSocketClientManager.DEFAULT_MARGIN_MODE)
                .positionMode(GateWebSocketClientManager.DEFAULT_POSITION_MODE)
                .restartGridSpan(GateWebSocketClientManager.DEFAULT_RESTART_GRID_SPAN)
                .priceScale(GateWebSocketClientManager.DEFAULT_PRICE_SCALE)
                .contractMultiplier(new BigDecimal(GateWebSocketClientManager.DEFAULT_CONTRACT_MULTIPLIER))
                .unrealizedPnlPriceMode(GateConfig.PnLPriceMode.valueOf(GateWebSocketClientManager.DEFAULT_PNL_MODE))
                .isProduction(true)
                .reopenMaxRetries(GateWebSocketClientManager.DEFAULT_REOPEN_MAX_RETRIES)
                // 可调参数 — 从 DTO 读取
                .gridRate(nvl(dto.getGridRate(), new BigDecimal("0.005")))
                .expectedProfit(nvl(dto.getExpectedProfit(), new BigDecimal("0.15")))
                .maxLoss(nvl(dto.getMaxLoss(), new BigDecimal("1.5")))
                .baseQuantity(nvl(dto.getBaseQuantity(), "2"))
                .quantity(nvl(dto.getQuantity(), "2"))
                .maxPositionSize(dto.getMaxPositionSize())
                .stopLossCount(dto.getStopLossCount())
                .build();
    }
 
    private File configFile(String apiKey) {
        return new File(CONFIG_DIR, CONFIG_PREFIX + md5(apiKey) + JSON_SUFFIX);
    }
 
    private static String md5(String input) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] digest = md.digest(input.getBytes(StandardCharsets.UTF_8));
            StringBuilder sb = new StringBuilder();
            for (byte b : digest) {
                sb.append(String.format("%02x", b));
            }
            return sb.toString();
        } catch (NoSuchAlgorithmException e) {
            return Integer.toHexString(input.hashCode());
        }
    }
 
    private static String maskKey(String key) {
        if (key == null || key.length() <= 8) return "***";
        return key.substring(0, 4) + "****" + key.substring(key.length() - 4);
    }
 
    private static <T> T nvl(T value, T fallback) {
        return value != null ? value : fallback;
    }
}