controller
@GetMapping("/submit")
@Role(value = "admin,user")
@ApiOperation(value = "支付成功提交请求")
@ApiImplicitParams({
@ApiImplicitParam(name = "code",value = "公链符号",dataType = "String",required = true),
@ApiImplicitParam(name = "hash",value = "交易hash",dataType = "String",required = true)
})
@LimitRequest(time = 8000 ,count = 1)
public JSONObject pay(@NotBlank(message = "公链符号不可为空") String code,@javax.validation.constraints.Pattern(regexp ="^[A-Za-z0-9]{1,100}",message = "请输入正确的交易hash")@NotBlank(message = "交易hash不可为空") String hash){
//根据充值订单判断该hash是否已经被使用
PayOrder res = payOrderMapper.getBayHash(hash);
if(ObjectUtil.isNotNull(res)){
return failure("请勿重复充值");
}
Map<String,Object> map = payService.submit(code,hash,request);
if(!(Boolean) map.get("bool")){
return failure((String) map.get("msg"));
}
return success((String) map.get("msg"));
}
}
service
@Transactional
public Map<String,Object> submit(String code, String hash,HttpServletRequest userRequest){
Map<String, Object> map = new HashMap<>();
//用户信息
SessionEntity sessionEntiy = SessionUtils.getSessionEntiy(userRequest);
List<Pay> pays = payMapper.selectAll1();
String apiUrl = "https://api.trongrid.io/event/transaction/";//获取转账金额API
String url = apiUrl + hash;
String decodeUrl = "https://api.trongrid.io/wallet/gettransactionbyid";//获取收款方addr
String decodeParam = "{\"value\":\"" + hash + "\"}";
try {
OkHttpClient client = new OkHttpClient().newBuilder()
.build();
Request request = new Request.Builder()
.url(url)
.method("GET", null)
.build();
Response response = client.newCall(request).execute();
if(response.code() == 200){
ResponseBody body = response.body();
String data = body.string();
//将api 响应body转换为json后转Object再获取指定属性
Object succesResponse = JSON.parse(data);
Object unconfirmed = JsonUtils.getValueByKey(succesResponse, "_unconfirmed");
if(ObjectUtil.isNotNull(unconfirmed)){
if(StringUtils.equals(String.valueOf(unconfirmed),"true")){
map.put("msg","等待确认中,请稍后再提交");
map.put("bool",false);
return map;
}
}
Object result = JsonUtils.getValueByKey(succesResponse, "result");
Object value = JsonUtils.getValueByKey(result, "value");//value对应的值是金额 要除以1000000
if(ObjectUtil.isNull(value)){
map.put("msg","充值失败,请重试");
map.put("bool",false);
return map;
}
for (Pay pay:pays) {
Object to = JsonUtils.getValueByKey(result, "to");//to 对应的是收款方addr
String addr = String.valueOf(to);//api提供的
//将后台配置的apikey解析 在进行比对
String apiKey = null;
StringBuilder replace = null;
try {
apiKey = DecodeUtils.toHexAddress(pay.getApikey());
//api返回的收款方地址是0x开头 将后台配置的apikey解码后再替换掉成0x开头的码进行比对
StringBuilder stringBuilder = new StringBuilder(apiKey);
replace = stringBuilder.replace(0, 2, "0x");
//如果报空指针是因为后台配置的apikey是错误的 直接跳过本次循环
} catch (NullPointerException e) {
continue;
}
if(StringUtils.equals(replace,addr)){//将后台配置的apikey转化成0x开头的码和api接口返回的addr进行比对 一致再进行以下操作
String s = String.valueOf(value);
BigDecimal notAmount = new BigDecimal(s);//接口返回的充值金额 (需要除1000000)
BigDecimal multiple = BigDecimal.valueOf(1000000);//倍数
BigDecimal amount = notAmount.divide(multiple, 0, BigDecimal.ROUND_DOWN );//实际充值金额 向下取整 增加至用户余额
//如果交易成功则增加余额 该交易hash存入redis 以防重复提交
//更新该用户的余额
SysUser user = userMapper.selectByPrimaryKey(sessionEntiy.getId());
if(ObjectUtil.isNull(user)){
map.put("msg","当前用户不存在");
map.put("bool",false);
return map;
}
Integer version = user.getVersion();
//当前用户余额+卡密金额
BigDecimal leftAmount = user.getLeftAmount();
BigDecimal add = leftAmount.add(amount);
user.setLeftAmount(add);
Long verifyCarmi = JedisUtil.setnxWithTimeOut(hash, hash, 10);
if(verifyCarmi == 2){
map.put("msg","请勿重复充值");
map.put("bool",false);
return map;
}
PayOrder payOrder = new PayOrder();
payOrder.setAmount(amount);
payOrder.setApikey(pay.getApikey());
payOrder.setCode(code);
payOrder.setHash(hash);
payOrder.setUserId(user.getId());
payOrder.setUserName(user.getUserName());
payOrder.setId(IdUtils.getId());
payOrder.setIsEnable(true);
payOrder.setIsDelete(false);
payOrderMapper.insertSelective(payOrder);
userMapper.updateUserLeftAmount(user.getId(),leftAmount,add,version);
//生成日志
SysUserLog sysUserLog = new SysUserLog();
sysUserLog.setUserId(sessionEntiy.getId());
sysUserLog.setUserName(sessionEntiy.getUserName());
String localhost=userRequest.getRemoteAddr();
sysUserLog.setLogIp(localhost);
sysUserLog.setLogType("用户充值");
sysUserLog.setMessage("用户当前账户余额为 "+ leftAmount.toPlainString() +" 充值金额 "+ amount.toPlainString() +" 充值后余额为 "+ add.toPlainString());
sysUserLog.setId(IdUtils.getId());
userLogMapper.insertSelective(sysUserLog);
//生成充值记录
ProxyRechargeRecord proxyRechargeRecord = new ProxyRechargeRecord();
proxyRechargeRecord.setUserId(sessionEntiy.getId());
proxyRechargeRecord.setAmount(amount);
proxyRechargeRecord.setCdkey(hash);
proxyRechargeRecord.setId(IdUtils.getId());
proxyRechargeRecord.setCreateUserId(sessionEntiy.getId());
proxyRechargeRecord.setUserName(sessionEntiy.getUserName());
proxyRechargeRecord.setType(2);//USDT
rechargeRecordMapper.insertSelective(proxyRechargeRecord);
map.put("msg","充值成功");
map.put("bool",true);
return map;
}
}
map.put("msg","充值失败,请检查收款号码是否输错");
map.put("bool",false);
return map;
}
} catch (IOException e) {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
e.printStackTrace();
}
map.put("msg","充值失败,请重试");
map.put("bool",false);
return map;
}
- 用户向后台指定的账户进行转账后会有一条交易hash,将交易hash复制到接口里进行提交.
- 后台拿到这条交易hash先进行校验是否存在数据库支付订单表里面
- 然后调用第三方api https://api.trongrid.io/event/transaction/ 将交易hash拼接到url后面 再解析接口的响应body api响应参数如下
[
{
"caller_contract_address": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
"result": {
"0": "0xcebabc6a66da33d2135f11207d6ab88b82829813",
"1": "0xb5ace2dca05b7029b52f3486160b902e9c6b89a1",
"2": "523583321",
"from": "0xcebabc6a66da33d2135f11207d6ab88b82829813",
"to": "0xb5ace2dca05b7029b52f3486160b902e9c6b89a1",
"value": "523583321"
},
"transaction_id": "5c9e9614d05a8e3b978a51122f91ec9da4801c8d1dc28c496807a0abb3fcc399",
"result_type": {
"from": "address",
"to": "address",
"value": "uint256"
},
"block_timestamp": 1645151991000,
"block_number": 38202168,
"event_name": "Transfer",
"contract_address": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
"event": "Transfer(address indexed from, address indexed to, uint256 value)",
"event_index": 0
}
]
- 需要拿到响应json串的"_unconfirmed"字段的值 这个字段表示该笔交易未证实, 转账方可以取消这笔交易的.当交易证实之后该字段不会返回, 如果返回这个字段值为 true 则需要return 不允许转账方白嫖 对应代码
ResponseBody body = response.body();
String data = body.string();
//将api 响应body转换为json后转Object再获取指定属性
Object succesResponse = JSON.parse(data);
Object unconfirmed = JsonUtils.getValueByKey(succesResponse, "_unconfirmed");
if(ObjectUtil.isNotNull(unconfirmed)){
if(StringUtils.equals(String.valueOf(unconfirmed),"true")){
map.put("msg","等待确认中,请稍后再提交");
map.put("bool",false);
return map;
}
}
- 拿到api返回的交易金额字段 获取交易金额 金额对应的字段是result下的value 但是需要除1000000才是实际金额
- 解析这个交易哈希的收款方地址是否是后台设置的地址 因为接口返回的不是明文,所以需要进一步解析 api返回的收款方地址是0x开头 我需要将后台配置的收款地址(418350333ac95746a8c52e5d7a48965e11b134ed35)解码后再替换成0x开头的码(0x8350333ac95746a8c52e5d7a48965e11b134ed35)进行比对
解析地址需要用到Base58 和 DecodeUtils 两个工具类
Base58
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.util.Arrays;
public class Base58 {
public static final char[] ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
.toCharArray();
private static final int[] INDEXES = new int[128];
static {
Arrays.fill(INDEXES, -1);
for (int i = 0; i < ALPHABET.length; i++) {
INDEXES[ALPHABET[i]] = i;
}
}
/**
* Encodes the given bytes in base58. No checksum is appended.
*/
public static String encode(byte[] input) {
if (input.length == 0) {
return "";
}
input = copyOfRange(input, 0, input.length);
// Count leading zeroes.
int zeroCount = 0;
while (zeroCount < input.length && input[zeroCount] == 0) {
++zeroCount;
}
// The actual encoding.
byte[] temp = new byte[input.length * 2];
int j = temp.length;
int startAt = zeroCount;
while (startAt < input.length) {
byte mod = divmod58(input, startAt);
if (input[startAt] == 0) {
++startAt;
}
temp[--j] = (byte) ALPHABET[mod];
}
// Strip extra '1' if there are some after decoding.
while (j < temp.length && temp[j] == ALPHABET[0]) {
++j;
}
// Add as many leading '1' as there were leading zeros.
while (--zeroCount >= 0) {
temp[--j] = (byte) ALPHABET[0];
}
byte[] output = copyOfRange(temp, j, temp.length);
try {
return new String(output, "US-ASCII");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e); // Cannot happen.
}
}
public static byte[] decode(String input) throws IllegalArgumentException {
if (input.length() == 0) {
return new byte[0];
}
byte[] input58 = new byte[input.length()];
// Transform the String to a base58 byte sequence
for (int i = 0; i < input.length(); ++i) {
char c = input.charAt(i);
int digit58 = -1;
if (c >= 0 && c < 128) {
digit58 = INDEXES[c];
}
if (digit58 < 0) {
throw new IllegalArgumentException("Illegal character " + c + " at " + i);
}
input58[i] = (byte) digit58;
}
// Count leading zeroes
int zeroCount = 0;
while (zeroCount < input58.length && input58[zeroCount] == 0) {
++zeroCount;
}
// The encoding
byte[] temp = new byte[input.length()];
int j = temp.length;
int startAt = zeroCount;
while (startAt < input58.length) {
byte mod = divmod256(input58, startAt);
if (input58[startAt] == 0) {
++startAt;
}
temp[--j] = mod;
}
// Do no add extra leading zeroes, move j to first non null byte.
while (j < temp.length && temp[j] == 0) {
++j;
}
return copyOfRange(temp, j - zeroCount, temp.length);
}
public static BigInteger decodeToBigInteger(String input) throws IllegalArgumentException {
return new BigInteger(1, decode(input));
}
//
// number -> number / 58, returns number % 58
//
private static byte divmod58(byte[] number, int startAt) {
int remainder = 0;
for (int i = startAt; i < number.length; i++) {
int digit256 = (int) number[i] & 0xFF;
int temp = remainder * 256 + digit256;
number[i] = (byte) (temp / 58);
remainder = temp % 58;
}
return (byte) remainder;
}
//
// number -> number / 256, returns number % 256
//
private static byte divmod256(byte[] number58, int startAt) {
int remainder = 0;
for (int i = startAt; i < number58.length; i++) {
int digit58 = (int) number58[i] & 0xFF;
int temp = remainder * 58 + digit58;
number58[i] = (byte) (temp / 256);
remainder = temp % 256;
}
return (byte) remainder;
}
private static byte[] copyOfRange(byte[] source, int from, int to) {
byte[] range = new byte[to - from];
System.arraycopy(source, from, range, 0, range.length);
return range;
}
}
DecodeUtils
import org.apache.commons.lang3.StringUtils;
import org.spongycastle.crypto.digests.SM3Digest;
import org.spongycastle.util.encoders.Hex;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* @title: DecodeUtils
* @Author kunkun
* @Date: 2022/2/17 20:09
* @Version 1.0
*/
public class DecodeUtils {
public static String toHexAddress(String address) {
if (StringUtils.isEmpty(address)) {
throw new IllegalArgumentException("传入的地址不可为空");
}
if (!address.startsWith("T")) {
throw new IllegalArgumentException("传入地址不合法:" + address);
}
return Hex.toHexString(decodeFromBase58Check(address));
}
public static byte[] decodeFromBase58Check(String addressBase58) {
try {
byte[] address = decode58Check(addressBase58);
return address;
} catch (Throwable t) {
}
return null;
}
private static byte[] decode58Check(String input) throws Exception {
byte[] decodeCheck = Base58.decode(input);
if (decodeCheck.length <= 4) {
return null;
}
byte[] decodeData = new byte[decodeCheck.length - 4];
System.arraycopy(decodeCheck, 0, decodeData, 0, decodeData.length);
byte[] hash0 = hash(true, decodeData);
byte[] hash1 = hash(true, hash0);
if (hash1[0] == decodeCheck[decodeData.length] && hash1[1] == decodeCheck[decodeData.length + 1]
&& hash1[2] == decodeCheck[decodeData.length + 2] && hash1[3] == decodeCheck[decodeData.length + 3]) {
return decodeData;
}
return null;
}
public static byte[] hash(boolean isSha256, byte[] input) throws NoSuchAlgorithmException {
return hash(isSha256, input, 0, input.length);
}
public static byte[] hash(boolean isSha256, byte[] input, int offset, int length) throws NoSuchAlgorithmException, NoSuchAlgorithmException {
if (isSha256) {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
digest.update(input, offset, length);
return digest.digest();
} else {
SM3Digest digest = new SM3Digest();
digest.update(input, offset, length);
byte[] eHash = new byte[digest.getDigestSize()];
digest.doFinal(eHash, 0);
return eHash;
}
}
}
需要用到的依赖
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.7</version>
</dependency>
<dependency>
<groupId>com.madgag.spongycastle</groupId>
<artifactId>core</artifactId>
<version>1.58.0.0</version>
</dependency>
- 为了防止用户恶意充值,一条hash交易 用几十个线程同时调用充值接口 采取措施是 使用redis的setnx做校验
/**
* 添加一个键值对,如果键存在不在添加,如果不存在,添加完成以后设置键的有效期
* @param key
* @param value
* @param timeOut
*/
public static Long setnxWithTimeOut(String key,String value,int timeOut) {
Jedis jedis = null;//获取一个jedis实例
long expire = 2;
try {
jedis = jedisPool.getResource();//获取一个jedis实例
if(0!=jedis.setnx(key, value)){
expire = jedis.expire(key, timeOut);
}
} catch (Exception e) {
LogUtils.error("错误日志:"+e.getMessage());
} finally {
jedis.close();
}
return expire;
}
Long verifyCarmi = JedisUtil.setnxWithTimeOut(hash, hash, 10);
if(verifyCarmi == 2){
map.put("msg","请勿重复充值");
map.put("bool",false);
return map;
}