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;
    }

  1. 用户向后台指定的账户进行转账后会有一条交易hash,将交易hash复制到接口里进行提交.
  2. 后台拿到这条交易hash先进行校验是否存在数据库支付订单表里面
  3. 然后调用第三方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
    }
]
  1. 需要拿到响应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;
                    }
                }
  1. 拿到api返回的交易金额字段 获取交易金额 金额对应的字段是result下的value 但是需要除1000000才是实际金额
  2. 解析这个交易哈希的收款方地址是否是后台设置的地址 因为接口返回的不是明文,所以需要进一步解析 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>
  1. 为了防止用户恶意充值,一条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;
                        }
上一篇 下一篇