接入币安,新增币安订单查询功能

This commit is contained in:
tianfeng 2026-03-02 17:52:55 +08:00
parent f686b95e30
commit 2f54f9a29b
10 changed files with 624 additions and 0 deletions

View File

@ -0,0 +1,15 @@
package com.red.circle.external.inner.endpoint.binance;
import com.red.circle.external.inner.endpoint.binance.api.BinanceClientApi;
import com.red.circle.external.inner.endpoint.censor.api.CensorImageClientApi;
import org.springframework.cloud.openfeign.FeignClient;
/**
* 图片安全鉴定.
*
* @author pengliang on 2023/10/17
*/
@FeignClient(name = "binanceClient", url = "${feign.external.url}" +
CensorImageClientApi.API_PREFIX)
public interface BinanceClient extends BinanceClientApi {
}

View File

@ -0,0 +1,31 @@
package com.red.circle.external.inner.endpoint.binance.api;
import com.red.circle.framework.dto.ResultResponse;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
/**
* 图片安全鉴定.
*
* @author pengliang on 2023/10/17
*/
public interface BinanceClientApi {
String API_PREFIX = "/binance/client";
/**
* 鉴定.
*
* @param txId 被鉴定的图片url
*/
@GetMapping("/deposit/history")
ResultResponse<String> depositHistory(@RequestParam("txId") String txId);
@GetMapping("/transactions")
ResultResponse<String> transactions(@RequestParam("orderId") String orderId)
throws UnsupportedEncodingException, NoSuchAlgorithmException, InvalidKeyException;
}

View File

@ -0,0 +1,54 @@
package com.red.circle.other.inner.enums.freight;
import java.math.BigDecimal;
import java.math.RoundingMode;
/**
* 币商政策.
*
* @author pengliang on 2019/10/24 14:35
*/
public enum FreightPolicyEnum {
G(0, 10000),
F(100, 11300),
E(300, 11400),
D(500, 11500),
C(1000, 11600),
B(2000, 12000),
A(3000, 12200),
S(5000, 12500);
private final Integer price;
private final Integer ratio;
FreightPolicyEnum(Integer price, Integer ratio) {
this.price = price;
this.ratio = ratio;
}
public static BigDecimal earnPoints(BigDecimal amount) {
FreightPolicyEnum[] values = FreightPolicyEnum.values();
for (int i = values.length - 1; i >= 0; i--) {
FreightPolicyEnum value = values[i];
if (value.price.compareTo(amount.intValue()) <= 0) {
return amount.multiply(new BigDecimal(value.ratio)).setScale(0, RoundingMode.FLOOR);
}
}
return BigDecimal.ZERO;
}
public static void main(String[] args) {
System.out.println(FreightPolicyEnum.earnPoints(new BigDecimal(12)));
}
}

View File

@ -51,6 +51,30 @@
<artifactId>component-push</artifactId>
</dependency>
<dependency>
<groupId>io.github.binance</groupId>
<artifactId>binance-pay</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>io.github.binance</groupId>
<artifactId>binance-wallet</artifactId>
<version>1.1.0</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>logging-interceptor</artifactId>
<version>4.12.0</version>
</dependency>
</dependencies>
<parent>

View File

@ -0,0 +1,23 @@
package com.red.circle.external.infra.props;
import lombok.AccessLevel;
import lombok.Data;
import lombok.experimental.FieldDefaults;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* @author pengliang on 2023/9/21
*/
@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
@Component
@ConfigurationProperties(BinanceProperties.PREFIX)
public class BinanceProperties {
public final static String PREFIX = "red-circle.binance";
String url;
String apiKey;
String secretKey;
}

View File

@ -0,0 +1,40 @@
package com.red.circle.external.inner.endpoint.binance;
import com.red.circle.external.inner.endpoint.binance.api.BinanceClientApi;
import com.red.circle.external.inner.endpoint.censor.api.CensorImageClientApi;
import com.red.circle.external.inner.service.binance.BinanceClientService;
import com.red.circle.framework.dto.ResultResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
/**
* 图片安全鉴定.
*
* @author pengliang on 2023/10/17
*/
@Validated
@RestController
@RequestMapping(value = CensorImageClientApi.API_PREFIX, produces = MediaType.APPLICATION_JSON_VALUE)
@RequiredArgsConstructor
public class BinanceClientEndpoint implements BinanceClientApi {
private final BinanceClientService binanceClientService;
@Override
public ResultResponse<String> depositHistory(String txId) {
return ResultResponse.success(binanceClientService.depositHistory(txId));
}
@Override
public ResultResponse<String> transactions(String orderId)
throws UnsupportedEncodingException, NoSuchAlgorithmException, InvalidKeyException {
return ResultResponse.success(binanceClientService.transactions(orderId));
}
}

View File

@ -0,0 +1,19 @@
package com.red.circle.external.inner.service.binance;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
/**
* minimax 相关服务.
*
* @author pengliang on 2021/3/25
*/
public interface BinanceClientService {
String depositHistory(String txId);
String transactions(String orderId)
throws UnsupportedEncodingException, InvalidKeyException, NoSuchAlgorithmException;
}

View File

@ -0,0 +1,191 @@
package com.red.circle.external.inner.service.binance.impl;
import com.binance.connector.client.common.ApiClient;
import com.binance.connector.client.common.ApiResponse;
import com.binance.connector.client.common.auth.BinanceAuthenticationFactory;
import com.binance.connector.client.common.configuration.ClientConfiguration;
import com.binance.connector.client.common.configuration.SignatureConfiguration;
import com.binance.connector.client.wallet.rest.api.CapitalApi;
import com.binance.connector.client.wallet.rest.model.DepositHistoryResponse;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.red.circle.external.infra.props.BinanceProperties;
import com.red.circle.external.inner.service.binance.BinanceClientService;
import com.red.circle.tool.core.collection.CollectionUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.*;
/**
* minimax 实现.
*
* @author pengliang on 2023/7/12
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class BinanceClientServiceImpl implements BinanceClientService {
private final BinanceProperties binanceProperties;
@Override
public String depositHistory(String txId) {
if (StringUtils.isBlank(txId)) {
return "";
}
ClientConfiguration clientConfiguration = new ClientConfiguration();
clientConfiguration.setUrl(binanceProperties.getUrl());
SignatureConfiguration signatureConfiguration = new SignatureConfiguration();
signatureConfiguration.setApiKey(binanceProperties.getApiKey());
signatureConfiguration.setSecretKey(binanceProperties.getSecretKey());
clientConfiguration.setSignatureConfiguration(signatureConfiguration);
ApiClient apiClient = new ApiClient(clientConfiguration, new BinanceAuthenticationFactory());
CapitalApi api = new CapitalApi(apiClient);
// 获取当前时间戳毫秒级
long currentTimeStamp = System.currentTimeMillis();
// 计算24小时前的时间戳毫秒级
long twentyFourHoursInMillis = 24 * 60 * 60 * 1000L;
long twentyFourHoursAgoTimeStamp = currentTimeStamp - twentyFourHoursInMillis;
ApiResponse<DepositHistoryResponse> depositHistoryResponseApiResponse =
api.depositHistory(true, "USDT", 1L, twentyFourHoursAgoTimeStamp, currentTimeStamp, 0L, 1000L, 5000L, txId);
try {
return depositHistoryResponseApiResponse.getData().get(0).getAmount();
} catch (Exception e) {
return "";
}
}
@Override
public String transactions(String orderId)
throws UnsupportedEncodingException, InvalidKeyException, NoSuchAlgorithmException {
long currentTimeStamp = System.currentTimeMillis();
long twentyFourHoursInMillis = 24 * 60 * 60 * 1000L;
long twentyFourHoursAgoTimeStamp = currentTimeStamp - twentyFourHoursInMillis;
Map<String, String> params = new HashMap<>();
params.put("timestamp", String.valueOf(System.currentTimeMillis()));
params.put("recvWindow", "5000");
params.put("startTime", String.valueOf(twentyFourHoursAgoTimeStamp));
params.put("endTime", String.valueOf(currentTimeStamp));
List<String> sortedKeys = new ArrayList<>(params.keySet());
Collections.sort(sortedKeys);
StringBuilder queryString = new StringBuilder();
for (String key : sortedKeys) {
if (!queryString.isEmpty()) {
queryString.append("&");
}
queryString.append(java.net.URLEncoder.encode(key, "UTF-8"))
.append("=")
.append(java.net.URLEncoder.encode(params.get(key), "UTF-8"));
}
Mac hmacSha256 = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(binanceProperties.getSecretKey().getBytes("UTF-8"), "HmacSHA256");
hmacSha256.init(secretKey);
byte[] signatureBytes = hmacSha256.doFinal(queryString.toString().getBytes("UTF-8"));
String signature = Hex.encodeHexString(signatureBytes);
OkHttpClient client = new OkHttpClient();
String finalUrl = binanceProperties.getUrl() + "/sapi/v1/pay/transactions" + "?" + queryString + "&signature=" + signature;
Request request = new Request.Builder()
.url(finalUrl)
.addHeader("X-MBX-APIKEY", binanceProperties.getApiKey())
.build();
try (Response response = client.newCall(request).execute()) {
assert response.body() != null;
String string = response.body().string();
Map<String, String> map = parseOrderIdAndAmount(string);
return CollectionUtils.isEmpty(map) ? "" : map.get(orderId);
} catch (IOException e) {
return "";
}
}
public static Map<String, String> parseOrderIdAndAmount(String responseBody) {
Map<String, String> orderMap = new HashMap<>();
try {
// 创建ObjectMapper实例
ObjectMapper objectMapper = new ObjectMapper();
// 解析JSON
JsonNode rootNode = objectMapper.readTree(responseBody);
// 获取data数组
JsonNode dataNode = rootNode.get("data");
// 检查data是否存在且是数组
if (dataNode != null && dataNode.isArray()) {
// 遍历数组中的每个元素
for (JsonNode node : dataNode) {
// 提取orderId
JsonNode orderIdNode = node.get("orderId");
// 提取amount
JsonNode amountNode = node.get("amount");
// 只有当两个字段都存在且为字符串类型时才添加到Map
if (orderIdNode != null && orderIdNode.isTextual() &&
amountNode != null && amountNode.isTextual()) {
orderMap.put(orderIdNode.asText(), amountNode.asText());
}
}
}
} catch (Exception e) {
return orderMap;
}
return orderMap;
}
public static void main(String[] args) {
ClientConfiguration clientConfiguration = new ClientConfiguration();
clientConfiguration.setUrl("https://api.binance.com");
SignatureConfiguration signatureConfiguration = new SignatureConfiguration();
signatureConfiguration.setApiKey("币安的Key");
signatureConfiguration.setSecretKey("币安的密钥");
clientConfiguration.setSignatureConfiguration(signatureConfiguration);
ApiClient apiClient = new ApiClient(clientConfiguration, new BinanceAuthenticationFactory());
CapitalApi api = new CapitalApi(apiClient);
// 获取当前时间戳毫秒级
long currentTimeStamp = System.currentTimeMillis();
// 计算24小时前的时间戳毫秒级
long twentyFourHoursInMillis = 24 * 60 * 60 * 1000L;
long twentyFourHoursAgoTimeStamp = currentTimeStamp - twentyFourHoursInMillis;
System.out.println("currentTimeStamp: " + currentTimeStamp);
System.out.println("twentyFourHoursAgoTimeStamp: " + twentyFourHoursAgoTimeStamp);
ApiResponse<DepositHistoryResponse> depositHistoryResponseApiResponse =
api.depositHistory(true, "USDT", 1L, twentyFourHoursAgoTimeStamp, currentTimeStamp, 0L, 1000L, 5000L, "");
System.out.println(depositHistoryResponseApiResponse.getStatusCode());
System.out.println(depositHistoryResponseApiResponse.getData().get(0).getAmount());
}
}

View File

@ -0,0 +1,205 @@
package com.red.circle.other.app.command.user;
import com.red.circle.common.business.core.enums.SysOriginPlatformEnum;
import com.red.circle.component.redis.service.RedisService;
import com.red.circle.external.inner.endpoint.binance.BinanceClient;
import com.red.circle.framework.core.asserts.ResponseAssert;
import com.red.circle.framework.core.response.CommonErrorCode;
import com.red.circle.order.inner.endpoint.UserFreightRechargeRecordClient;
import com.red.circle.other.app.dto.cmd.user.user.FreightRechargeCmd;
import com.red.circle.other.app.scheduler.MyAmazingBot;
import com.red.circle.other.domain.gateway.user.UserProfileGateway;
import com.red.circle.other.domain.model.user.UserProfile;
import com.red.circle.other.inner.asserts.ImErrorCode;
import com.red.circle.other.inner.asserts.user.UserErrorCode;
import com.red.circle.other.inner.endpoint.material.props.BadgeBackpackClient;
import com.red.circle.other.inner.endpoint.material.props.BadgeSourceClient;
import com.red.circle.other.inner.enums.freight.FreightPolicyEnum;
import com.red.circle.tool.core.num.ArithmeticUtils;
import com.red.circle.wallet.inner.endpoint.freight.FreightGoldClient;
import com.red.circle.wallet.inner.error.FreightErrorCode;
import com.red.circle.wallet.inner.model.cmd.AddFreightBalanceRunningWaterCmd;
import com.red.circle.wallet.inner.model.cmd.FreightAgentReviewCmd;
import com.red.circle.wallet.inner.model.dto.UserFreightBalanceDTO;
import com.red.circle.wallet.inner.model.enums.FreightBalanceOrigin;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Objects;
import java.util.Optional;
/**
* @author longli on 2024/3/22
*/
@Component
@RequiredArgsConstructor
public class FreightRechargeCmdExe {
private final BadgeBackpackClient badgeBackpackClient;
private final RedisService redisService;
private final UserProfileGateway userProfileGateway;
private final BinanceClient binanceClient;
private final FreightGoldClient freightGoldClient;
private final UserFreightRechargeRecordClient userFreightRechargeRecordClient;
private final BadgeSourceClient badgeSourceClient;
public void recharge(FreightRechargeCmd cmd) {
UserProfile userProfile = userProfileGateway.getByUserId(cmd.getReqUserId());
ResponseAssert.notNull(ImErrorCode.NOT_FOUND_USER_INFO, userProfile);
ResponseAssert.isTrue(UserErrorCode.USER_INFO_NOT_FOUND,
Objects.equals(userProfile.getOriginSys(), cmd.requireReqSysOrigin()));
ResponseAssert.isFalse(FreightErrorCode.THE_GOLD_COINS_HAVE_ARRIVED, freightGoldClient.existsRecord(cmd.getTxId()).getBody());
String lockKey = "back_send_freight_ship:" + cmd.getTxId();
try {
// 操作冲突
ResponseAssert.isTrue(CommonErrorCode.OPERATION_CONFLICT, redisService.lock(lockKey, 60));
String dollar = "";
if (cmd.getTxId().length() == 18) {
dollar = binanceClient.transactions(cmd.getTxId()).getBody();
} else {
dollar = binanceClient.depositHistory(cmd.getTxId()).getBody();
}
ResponseAssert.isTrue(FreightErrorCode.INCORRECT_ORDER_NUMBER, StringUtils.isNotBlank(dollar));
BigDecimal bigDecimal = new BigDecimal(dollar);
BigDecimal earnPoints = FreightPolicyEnum.earnPoints(bigDecimal);
BigDecimal rechargeRatio = freightGoldClient.rechargeRatio(cmd.getReqUserId()).getBody();
if (rechargeRatio.compareTo(BigDecimal.ZERO) > 0) {
earnPoints = bigDecimal.multiply(rechargeRatio).setScale(0, RoundingMode.FLOOR);
}
// 操作失败
ResponseAssert.isTrue(CommonErrorCode.OPERATING_FAILURE,
incrCandyBalance(cmd.requireReqSysOrigin(), cmd.getReqUserId(), earnPoints));
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("Hooka 币商充值").append("\n")
.append("用户ID:").append(userProfile.getAccount()).append("\n")
.append("充值金额: ").append(bigDecimal).append("\n")
.append("自动到账金币: ").append(earnPoints).append("\n")
.append("订单号: ").append(cmd.getTxId());
sendTelegram(stringBuilder);
// 插入流水
if (ArithmeticUtils.gte(earnPoints, BigDecimal.ONE)) {
BigDecimal balance = getAvailableBalance(cmd.getReqUserId());
freightGoldClient.addRunningWater(new AddFreightBalanceRunningWaterCmd()
.setSysOrigin(cmd.requireReqSysOrigin())
.setUserId(cmd.getReqUserId())
.setAcceptUserId(cmd.getReqUserId())
.setType(0)
.setQuantity(earnPoints)
.setUsdQuantity(BigDecimal.ZERO)
.setBalance(balance)
.setRechargeType("USDT-进货")
.setAmount(bigDecimal)
.setOrigin(FreightBalanceOrigin.PURCHASE.name())
.setRemark(cmd.getTxId())
.setCreateUser(cmd.getReqUserId())
.setUpdateUser(cmd.getReqUserId()));
// 记录充值金额: 货运代理充值500以上则发送货运代理徽章
BigDecimal amount = userFreightRechargeRecordClient.inrNowMonthAmount(cmd.getReqUserId(),
ArithmeticUtils.lteZero(bigDecimal) ? BigDecimal.ZERO : bigDecimal)
.getBody();
sendFreightShipBadge(cmd.getReqUserId(), amount);
}
} catch (UnsupportedEncodingException | NoSuchAlgorithmException | InvalidKeyException e) {
throw new RuntimeException(e);
} finally {
redisService.unlock(lockKey);
}
}
private void sendTelegram(StringBuilder stringBuilder) {
//机器人来咯
MyAmazingBot myAmazingBot = new MyAmazingBot();
myAmazingBot.sendMsg("机器人群组", stringBuilder.toString());
}
private BigDecimal getAvailableBalance(Long userId) {
return Optional.ofNullable(
ResponseAssert.requiredSuccess(freightGoldClient.getFreightBalance(userId)))
.map(balance -> balance.getEarnPoints()
.subtract(balance.getConsumptionPoints())
.setScale(2, RoundingMode.DOWN))
.orElse(BigDecimal.ZERO);
}
/**
* 余额如果大于1250则发送货运代理徽章.
*
* @param userId 代理用户id.
* @param balance 代理余额.
*/
public void sendFreightShipBadge(Long userId, BigDecimal balance) {
if (Objects.isNull(balance)) {
return;
}
Long freightBadgeId = getFreightBadgeId();
if (Objects.isNull(freightBadgeId)) {
return;
}
if (balance.compareTo(BigDecimal.valueOf(500)) < 0) {
return;
}
badgeBackpackClient.giveBadgePermanent(freightBadgeId, userId);
}
private boolean incrCandyBalance(String sysOrigin, Long userId,
BigDecimal quantity) {
UserFreightBalanceDTO freightBalance = freightGoldClient.getFreightBalance(userId)
.getBody();
if (Objects.isNull(freightBalance)) {
ResponseAssert.isFalse(
UserErrorCode.USER_IS_SELLER, ResponseAssert.requiredSuccess(
freightGoldClient.existsSeller(userId)));
//获得货运代理徽章
Long badgeId = getFreightBadgeId();
if (Objects.nonNull(badgeId) && quantity.compareTo(BigDecimal.valueOf(1250)) >= 0) {
//发送货运代理徽章
badgeBackpackClient.giveBadgePermanent(badgeId, userId);
}
// 增加新开货运代理审查记录
freightGoldClient.addFreightAgentReview(new FreightAgentReviewCmd()
.setSysOrigin(sysOrigin)
.setUserId(userId)
);
}
if (Objects.equals(Boolean.TRUE, ResponseAssert.requiredSuccess(
freightGoldClient.incrCandyBalance(SysOriginPlatformEnum.valueOf(sysOrigin), userId,
quantity)))) {
return Boolean.TRUE;
}
return Boolean.FALSE;
}
private Long getFreightBadgeId() {
return badgeSourceClient.getFreightBadgeId().getBody();
}
}

View File

@ -0,0 +1,22 @@
package com.red.circle.other.app.dto.cmd.user.user;
import com.red.circle.common.business.dto.cmd.AppExtCommand;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* <p>
* 填写邀请用户.
* </p>
*
* @author pengshigang
* @since 2023-12-20
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class FreightRechargeCmd extends AppExtCommand {
@NotBlank(message = "txId required.")
private String txId;
}