通联支付API集成(适用于SpringBoot)

目标:

  • 学习如何使用Java与通联支付API进行交互

  • 实现一个简单的支付下单和查询订单状态的示例

所需材料:

  • 通联支付API文档

官方文档icon-default.png?t=N7T8https://aipboss.allinpay.com/know/devhelp/main.php?pid=38#mid=313

  • 通联支付加签代码SybUtil

package com.allinpay.common;

import net.sf.json.JSONObject;
import org.apache.tomcat.util.codec.binary.Base64;

import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Map;
import java.util.Random;
import java.util.TreeMap;


@SuppressWarnings("all")
public class SybUtil {
    /**
     * js转化为实体
     *
     * @param <T>
     * @param jsonstr
     * @param cls
     * @return
     */
    public static <T> T json2Obj(String jsonstr, Class<T> cls) {
        JSONObject jo = JSONObject.fromObject(jsonstr);
        T obj = (T) JSONObject.toBean(jo, cls);
        return obj;
    }

    /**
     * md5
     *
     * @param b
     * @return
     */
    public static String md5(byte[] b) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            md.reset();
            md.update(b);
            byte[] hash = md.digest();
            StringBuffer outStrBuf = new StringBuffer(32);
            for (int i = 0; i < hash.length; i++) {
                int v = hash[i] & 0xFF;
                if (v < 16) {
                    outStrBuf.append('0');
                }
                outStrBuf.append(Integer.toString(v, 16).toLowerCase());
            }
            return outStrBuf.toString();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            return new String(b);
        }
    }

    /**
     * 判断字符串是否为空
     *
     * @param s
     * @return
     */
    public static boolean isEmpty(String s) {
        if (s == null || "".equals(s.trim()))
            return true;
        return false;
    }

    /**
     * 生成随机码
     *
     * @param n
     * @return
     */
    public static String getValidatecode(int n) {
        Random random = new Random();
        String sRand = "";
        n = n == 0 ? 4 : n;// default 4
        for (int i = 0; i < n; i++) {
            String rand = String.valueOf(random.nextInt(10));
            sRand += rand;
        }
        return sRand;
    }



    public static boolean validSign(TreeMap<String, String> param,
                                    String appkey, String signType) throws Exception {
        if (param != null && !param.isEmpty()) {
            if (!param.containsKey("sign"))
                return false;
            String sign = param.remove("sign");
            if ("MD5".equals(signType)) {// 如果是md5则需要把md5的key加入到排序
                param.put("key", appkey);
            }
            StringBuilder sb = new StringBuilder();
            for (Map.Entry<String, String> entry : param.entrySet()) {
                if (entry.getValue() != null && entry.getValue().length() > 0) {
                    sb.append(entry.getKey()).append("=")
                            .append(entry.getValue()).append("&");
                }
            }
            if (sb.length() > 0) {
                sb.deleteCharAt(sb.length() - 1);
            }
            if ("MD5".equals(signType)) {
                return sign.toLowerCase().equals(
                        md5(sb.toString().getBytes("UTF-8")).toLowerCase());
            } else {
                return rsaVerifyPublickey(sb.toString(), sign, appkey, "UTF-8");
            }
        }
        return false;
    }

    public static boolean rsaVerifyPublickey(String content, String sign,
                                             String publicKey, String charset) throws Exception {
        try {
            PublicKey pubKey = getPublicKeyFromX509("RSA",
                    Base64.decodeBase64(publicKey.getBytes()));
            return rsaVerifyPublickey(content, sign, pubKey, charset);
        } catch (Exception e) {
            e.printStackTrace();
            throw new Exception("RSAcontent = " + content + ",sign=" + sign
                    + ",charset = " + charset, e);
        }
    }

    public static boolean rsaVerifyPublickey(String content, String sign,
                                             PublicKey pubKey, String charset) throws Exception {
        try {
            java.security.Signature signature = java.security.Signature
                    .getInstance("SHA1WithRSA");

            signature.initVerify(pubKey);

            if (charset == null || "".equals(charset)) {
                signature.update(content.getBytes());
            } else {
                signature.update(content.getBytes(charset));
            }

            return signature.verify(Base64.decodeBase64(sign.getBytes()));
        } catch (Exception e) {
            throw e;
        }
    }
    public static String unionSign(TreeMap<String, String> params,String appkey,
                                   String signType) throws Exception {
        // TODO Auto-generated method stub

        params.remove("sign");
        if ("MD5".equals(signType)) {// 如果是md5则需要把md5的key加入到排序
            params.put("key", appkey);
        }
        StringBuilder sb = new StringBuilder();
        for (Map.Entry<String, String> entry : params.entrySet()) {
            if (entry.getValue() != null && entry.getValue().length() > 0) {
                sb.append(entry.getKey()).append("=").append(entry.getValue())
                        .append("&");
            }
        }
        if (sb.length() > 0) {
            sb.deleteCharAt(sb.length() - 1);
        }
        String sign = "";
        if ("MD5".equals(signType)) {
            System.out.println(sb.toString());
            sign = md5(sb.toString().getBytes("UTF-8"));// 记得是md5编码的加签
            params.remove("key");
        } else {
            sign = rsaSign(sb.toString(), appkey, "UTF-8");
        }
        return sign;
    }

    public static String rsaSign(String content, String privateKey,
                                 String charset) throws Exception {
        PrivateKey priKey = getPrivateKeyFromPKCS8("RSA",
                Base64.decodeBase64(privateKey.getBytes()));
        return rsaSign(content, priKey, charset);
    }

    public static String rsaSign(String content, byte[] privateKey,
                                 String charset) throws Exception {
        PrivateKey priKey = getPrivateKeyFromPKCS8("RSA", privateKey);
        return rsaSign(content, priKey, charset);
    }

    public static String rsaSign(String content, PrivateKey priKey,
                                 String charset) throws Exception {
        java.security.Signature signature = java.security.Signature
                .getInstance("SHA1WithRSA");
        signature.initSign(priKey);
        if (charset == null || "".equals(charset)) {
            signature.update(content.getBytes());
        } else {
            signature.update(content.getBytes(charset));
        }
        byte[] signed = signature.sign();

        return new String(Base64.encodeBase64(signed));
    }

    public static PrivateKey getPrivateKeyFromPKCS8(String algorithm,
                                                    byte[] encodedKey) throws Exception {

        KeyFactory keyFactory = KeyFactory.getInstance(algorithm);

        return keyFactory.generatePrivate(new PKCS8EncodedKeySpec(encodedKey));
    }

    public static PublicKey getPublicKeyFromX509(String algorithm,
                                                 byte[] encodedKey) throws Exception {
        KeyFactory keyFactory = KeyFactory.getInstance(algorithm);

        return keyFactory.generatePublic(new X509EncodedKeySpec(encodedKey));
    }
}
  • IDE(如IntelliJ IDEA或Eclipse)

  • JDK 8 或更高版本

  • 需要的maven
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>2.0.43</version>
        </dependency>

        <!-- SybUtil文件需要用到 -->
        <dependency>
            <groupId>net.sf.json-lib</groupId>
            <artifactId>json-lib</artifactId>
            <version>2.4</version>
            <classifier>jdk15</classifier>
        </dependency>

        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.20</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
  •  辅助类
  1. 通联支付需要的请求数据格式(Allinpay.java)
package com.allinpay.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;

import java.math.BigDecimal;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class Allinpay {

    private String cusid;

    private String appid;

    private int version;

    private BigDecimal trxamt;

    /*
        当指定为F02时,交易仅限分期交易。
        分期交易金额必须大于500元。
     */
    // ?
    private String paytype;

    /*
        3  花呗分期3期
        6  花呗分期6期
        12  花呗分期12期
        3-cc 支付宝信用卡分期3期
        6-cc 支付宝信用卡分期6期
        12-cc 支付宝信用卡分期12期
        暂只支持支付宝花呗分期,支付宝信用卡分期,仅支持A01/A02
     */
    // 可空参数
    private String fqnum;

    //订单号,商户唯一订单号
    private String reqsn;

    /*
        商户网站使用的编码格式,支持
        UTF-8、GBK
        跟商户网站的编码一致

    */
    //
    private String charset;

    /*
     必须为https协议地址,且不允许带参数
     页面跳转同步通知页面路径
    */
    private String returl;

    // 异步通知地址
    // ?
    private String notify_url;

    // 商品描述,长度最大100
    private String body;


    //? 通知会原样带上, 订单备注信息
    private String remark;

    // 随机字符串,自己生成,最大32位
    private String randomstr;

    //订单有效时间,以分为单位,默认为15
    private String validtime;

    // ? 支付限制(no_credit--指定不能使用信用卡支付)
    private String limit_pay;

    // 签名类型,目前支持RSA2和RSA
    private String signtype;

    // 签名 32位
    private String sign;

    // 关闭订单的时候需要
    private String oldreqsn;



}

 2. 由于本案列采用了多商家,所以暂时把配置也建成了一个类(PayConfig.java)

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;

@AllArgsConstructor
@NoArgsConstructor
@Data
@Accessors(chain = true)
public class PayConfig {

    private String cusid;

    private String appid;

    private String privateKey;

}

 3.本案列设置了多方支付方式,所以还有一个支付载荷(PayLoad.java)

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;

import java.math.BigDecimal;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class PayLoad {

    private String orderId;

    private BigDecimal amount;

    private String payConfigKey;

    private String remark;

    private String title;

    private int payType;

    private String returnUrl;

    private String notifyUrl;


}
  •  H5支付下单代码

    //这个是PayService的Impl实现层
    private final String order = "https://syb.allinpay.com/apiweb/h5unionpay/unionorder";
    private final String rsaPrivateKey = "xxx";
    @SneakyThrows
    @Override
    public String h5Pay(PayLoad payLoad) {
        // String payConfigKey = payLoad.getPayConfigKey();
        PayConfig payConfig = new PayConfig()
            .setAppid("xxx")
            .setCusid("xxx")
            .setPrivateKey(rsaPrivateKey);
        Allinpay allinpay = new Allinpay()
                .setSigntype("RSA")
                .setTrxamt(new BigDecimal(2))
                .setReqsn(payLoad.getOrderId())
                .setRandomstr(SybUtil.getValidatecode(8))
                .setBody(payLoad.getTitle())
                .setRemark(payLoad.getRemark())
                .setCharset("UTF-8")
                .setAppid(payConfig.getAppid())
                .setCusid(payConfig.getCusid())
                .setReturl(payLoad.getReturnUrl());
        allinpay.setSign(URLEncoder.encode(SybUtil.unionSign(objectToTreeMap(allinpay), payConfig.getPrivateKey() , "RSA"), StandardCharsets.UTF_8));
        return order + "?" + treeMapToUrlParams(objectToTreeMap(allinpay));
    }
    //Controller代码
    @SneakyThrows
    @GetMapping("pay")
    public String pay(HttpServletResponse response) {
        String paymentUrl = (String) payService.h5Pay(new PayLoad()
                .setPayType(2)
                .setPayConfigKey("")
                .setReturnUrl("https://blog.csdn.net")
                .setOrderId(IdUtil.getSnowflakeNextIdStr())
                .setAmount(new BigDecimal(2))
                .setRemark("测试支付备注")
                .setTitle("测试H5支付")
        );
        String htmlResponse = "<!DOCTYPE html><html><head></head><body>"
                + "<script>window.location.href='" + paymentUrl + "'</script>"
                + "</body></html>";

        // 设置响应内容类型为HTML
        response.setContentType("text/html;charset=UTF-8");
        return htmlResponse;
    }

  • 关闭订单 

    // 关闭接口
    private final String close = "https://vsp.allinpay.com/apiweb/tranx/close";

    @SneakyThrows
    @Override
    public String closeOrder(PayLoad payLoad) {
        PayConfig payConfig = new PayConfig()
                .setAppid("xxx")
                .setCusid("xxxx")
                .setPrivateKey(rsaPrivateKey);
        Allinpay allinpay = new Allinpay()
                .setCusid(payConfig.getCusid())
                .setAppid(payConfig.getAppid())
                .setRandomstr(SybUtil.getValidatecode(8))
                .setVersion(11)
                .setOldreqsn(payLoad.getOrderId())
                .setSigntype("RSA");
        allinpay.setSign(SybUtil.unionSign(objectToTreeMap(allinpay), rsaPrivateKey, "RSA"));
        return HttpUtil.post(close, BeanUtil.beanToMap(allinpay));
    }
 
     /**
     * 关闭订单
     * @param outTradeNo 下单的订单号,也就是你支付下单类Allinpay的reqsn
     * @return {@link String}
     ***/
    @SneakyThrows
    @GetMapping("closeOrder/{outTradeNo}")
    public Map<String,String> closeOrder(@PathVariable Long outTradeNo) {
        return handleResult(payService.closeOrder(new PayLoad().setOrderId(outTradeNo.toString())));
    }
  •  查询订单

    // 查询接口
    private final String query = "https://vsp.allinpay.com/apiweb/tranx/query";

    @SneakyThrows
    @Override
    public String query(PayLoad payLoad) {
        PayConfig payConfig = new PayConfig()
                .setAppid("xxx")
                .setCusid("xxxx")
                .setPrivateKey(rsaPrivateKey);
        Allinpay allinpay = new Allinpay()
                .setCusid(payConfig.getCusid())
                .setAppid(payConfig.getAppid())
                .setRandomstr(SybUtil.getValidatecode(8))
                .setVersion(11)
                .setReqsn(payLoad.getOrderId())
                .setSigntype("RSA");
        allinpay.setSign(SybUtil.unionSign(objectToTreeMap(allinpay), rsaPrivateKey, "RSA"));
        return HttpUtil.post(query, BeanUtil.beanToMap(allinpay));
    }


    /**
     * 查询订单
     * @param outTradeNo 下单的订单号
     * @return {@link String}
     ***/
    @SneakyThrows
    @GetMapping("query/{outTradeNo}")
    public Map<String, String> query(@PathVariable Long outTradeNo) {
        return handleResult(payService.query(new PayLoad().setOrderId(outTradeNo.toString())));
    }
  • 退款接口,这个是只能退当天的交易 (全额退实时返回退款结果)

    // 取消当天交易退款接口
    private final String cancelDayUrl = "https://vsp.allinpay.com/apiweb/tranx/cancel";

    @SneakyThrows
    @Override
    public String cancelDay(PayLoad payLoad) {
        PayConfig payConfig = new PayConfig()
                .setAppid("xxx")
                .setCusid("xxxx")
                .setPrivateKey(rsaPrivateKey);
        Allinpay allinpay = new Allinpay()
                .setCusid(payConfig.getCusid())
                .setAppid(payConfig.getAppid())
                .setRandomstr(SybUtil.getValidatecode(8))
                .setVersion(11)
                .setTrxamt(new BigDecimal(2))
                .setReqsn(payLoad.getOrderId())
                .setOldreqsn(payLoad.getOrderId())
                .setSigntype("RSA");
        allinpay.setSign(SybUtil.unionSign(objectToTreeMap(allinpay), rsaPrivateKey, "RSA"));
        return HttpUtil.post(cancelDayUrl, BeanUtil.beanToMap(allinpay));
    }


    /**
     * 退款接口,只能退今天的,全额退款,实时返回退款结果
     * @return {@link String}
     */
    @SneakyThrows
    @GetMapping("cancelDay/{outTradeNo}")
    public Map<String, String> cancelDay(@PathVariable Long outTradeNo) {
        return handleResult(payService.cancelDay(new PayLoad().setOrderId(outTradeNo.toString())));
    }
  • 退款接口 ,可以退部分

    
    // 退款接口
    private final String refundUrl = "https://vsp.allinpay.com/apiweb/tranx/refund";

    @SneakyThrows
    @Override
    public String refund(PayLoad payLoad) {
        PayConfig payConfig = new PayConfig()
                .setAppid("xxx")
                .setCusid("xxxx")
                .setPrivateKey(rsaPrivateKey);
        Allinpay allinpay = new Allinpay()
                .setCusid(payConfig.getCusid())
                .setAppid(payConfig.getAppid())
                .setRandomstr(SybUtil.getValidatecode(8))
                .setVersion(11)
                .setTrxamt(new BigDecimal(2))
                .setReqsn(payLoad.getOrderId())
                .setOldreqsn(payLoad.getOrderId())
                .setSigntype("RSA");
        allinpay.setSign(SybUtil.unionSign(objectToTreeMap(allinpay), rsaPrivateKey, "RSA"));
        return HttpUtil.post(refundUrl, BeanUtil.beanToMap(allinpay));
    }

     // 可以和上面的接口进行整合处理

    /**
     * 退款接口,可以退部分
     * @return {@link String}
     */
    @SneakyThrows
    @GetMapping("refund/{outTradeNo}")
    public Map<String, String> refund(@PathVariable Long outTradeNo) {
        return handleResult(payService.refund(new PayLoad().setOrderId(outTradeNo.toString())));
    }

提示: 可以和上面的接口进行整合处理

  • 其中签名需要用到的转map方法 

    private TreeMap<String, String> objectToTreeMap(Object obj) {
        TreeMap<String, String> treeMap = new TreeMap<>();
        Class<?> clazz = obj.getClass();
        while (clazz != null) {
            for (Field field : clazz.getDeclaredFields()) {
                field.setAccessible(true);
                try {
                    Object fieldValue = field.get(obj);
                    if (fieldValue != null) {
                        treeMap.put(field.getName(), fieldValue.toString());
                    }
                } catch (IllegalAccessException e) {
                    log.error("Error accessing field " + field.getName() + ": " + e.getMessage());
                }
            }
            clazz = clazz.getSuperclass();
        }
        return treeMap;
    }
  •  其中map转路径参数方法

  private static String treeMapToUrlParams(TreeMap<String, String> treeMap) {
        StringBuilder sb = new StringBuilder();
        for (Map.Entry<String, String> entry : treeMap.entrySet()) {
            String key = entry.getKey();
            String value = entry.getValue();
            if (sb.length() > 0) {
                sb.append("&");
            }
            sb.append(key).append("=").append(value);
        }
        return sb.toString();
    }
  • 其中通联返回验签代码

    /**
     * 验签
     * @param result
     * @return {@link Map}<{@link String},{@link String}>
     * @throws Exception
     */
    @SuppressWarnings({ "rawtypes", "all" })
    public static Map<String,String> handleResult(String result) throws Exception{
        Map map = SybUtil.json2Obj(result, Map.class);
        if(map == null){
            throw new Exception("返回数据错误");
        }
        if("SUCCESS".equals(map.get("retcode"))) {
            TreeMap tmap = new TreeMap();
            tmap.putAll(map);
            if(SybUtil.validSign(
                     tmap,
                    "xxxxx",
                    "RSA")
            ){
                return map;
            }else{
                throw new Exception("验证签名失败");
            }
            // 验签成功,返回数据
        }else{
            throw new Exception(map.get("retmsg").toString());
        }
    }

提示:如果不考虑多商户,多支付通道,可以再方法中Allinpay类直接填参数请求,不用通过PayLoad和PayConfig类  

  • 完整代码克隆地址:

git clone https://gitee.com/byte1026/allin-pay.git
  • 6
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值