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 nvl(T value, T fallback) { return value != null ? value : fallback; } }