建行支付对接(H5)

1. 前期准备

  1. 获取建行龙支付接入指南(接入前建行会发送相关资料)

龙支付商户接入简介-V0.2.pdficon-default.png?t=N7T8https://www.yuque.com/attachments/yuque/0/2024/pdf/38707295/1723599679508-754d3ccd-a719-4955-92be-a64ee9847538.pdf

如上PDF文档中介绍了PC网关支付、移动网关支付、二维码支付、无感支付、微信小程序/公众号支付、刷脸支付等6种支付方式,本文以移动网关支付(H5)进行开发对接。

  1. 获取对接资料

商户代码(merchantId)、商户登录密码(quPwd)、商户柜台代码(postId)、分行代码(branchId)、交易码(txCode)、公钥(pub)。

注:公钥需要登录商户后台(中国建设银行 商户服务平台)获取,登录进去点击服务管理-商户公钥下载,如下图:

  1. 开通权限

需要联系分管贵公司的建行工作人员,开通服务器实时反馈、IP白名单权限。

  1. 流程图

2. 开始对接

2.1. 配置

  1. 将netpay.jar引用至开发工程中,CCBSign.RSASig是签名包的封装类,验签时使用此类即可。
<dependency>
    <groupId>com.ccbsign.rsasig</groupId>
    <artifactId>netpay</artifactId>
    <version>1.0</version>
</dependency>
  1. 配置yml文件
thirdparty:
  #建行支付配置     
  ccb:
    payUrl: 建行支付地址
    merchantId: 商户代码
    branchId: 分行代码
    postId: 商户柜台代码
    curCode: 币种
    txCode: 交易码
    type: 接口类型
    pubKey: 公钥后30位
    geteWay: 网关类型
    payMap: 支付方式位图
    quPwd: 商户登录密码

2.2. 支付

  1. 参考建行给的支付文档,如下:

跳转至建行WEB网关下单接口(移动端使用)v2.4.1.pdficon-default.png?t=N7T8https://www.yuque.com/attachments/yuque/0/2024/pdf/38707295/1723605638185-a126a328-0c79-42b2-bdc2-17dfd9604bb1.pdf

  1. 定义支付参数对象,代码如下:
@Data
@ApiModel(value="CCBPayDTO", description="CCBPayDTO对象")
public class CCBPayDTO {

    @ApiModelProperty(value = "open_id")
    private String openId;

    @ApiModelProperty(value = "案件id")
    private Long evtId;


    @ApiModelProperty(value = "商户代码")
    private String merchantId;

    @ApiModelProperty(value = "商户柜台代码")
    private String postId;

    @ApiModelProperty(value = "分行代码")
    private String branchId;

    @ApiModelProperty(value = "订单号")
    private String orderId;

    @ApiModelProperty(value = "付款金额")
    private String payment;

    @ApiModelProperty(value = "币种")
    private String curCode;

    @ApiModelProperty(value = "备注信息1")
    private String remark1;

    @ApiModelProperty(value = "备注信息2")
    private String remark2;

    @ApiModelProperty(value = "交易码")
    private String txCode;

    @ApiModelProperty(value = "MAC 校验域")
    private String mac;

    @ApiModelProperty(value = "接口类型")
    private String type;

    @ApiModelProperty(value = "公钥后30位")
    private String pub;

    @ApiModelProperty(value = "网关类型")
    private String geteway;

    @ApiModelProperty(value = "客户端 IP")
    private String clientip;

    @ApiModelProperty(value = "客户注册信息")
    private String reginfo;

    @ApiModelProperty(value = "商品信息")
    private String proinfo;

    @ApiModelProperty(value = "商户 URL")
    private String eferer;

    @ApiModelProperty(value = "订单超时时间")
    private String timeout;

}
  1. 生成订单并获取建行支付链接,代码如下:
  • 根据支付参数获取建行支付链接
public String pay(CCBPayDTO ccbPayDTO) {
        if (UnionUtils.isEmpty(ccbPayDTO.getPayment())) {
            throw new ServiceException("付款金额不能为空!");
        }
        String absHref = "";
        ccbPayDTO.setMerchantId(merchantId);
        ccbPayDTO.setBranchId(branchId);
        ccbPayDTO.setPostId(postId);
        ccbPayDTO.setOrderId(OrderUtil.randomOrderCode());
        ccbPayDTO.setCurCode(curCode);
        ccbPayDTO.setTxCode(txCode);
        ccbPayDTO.setType(type);

        String pub = pubKey;
        String pubSub = pub.substring(pub.length() - 30); //商户公钥后30位
        ccbPayDTO.setPub(pubSub);
        ccbPayDTO.setGeteway(geteWay);
        if (UnionUtils.isEmpty(ccbPayDTO.getPayment())) {
            ccbPayDTO.setPayment("0.01");
        }
        StringBuffer str = new StringBuffer();
        str.append("MERCHANTID=");
        str.append(ccbPayDTO.getMerchantId());
        str.append("&POSID=");
        str.append(ccbPayDTO.getPostId());
        str.append("&BRANCHID=");
        str.append(ccbPayDTO.getBranchId());
        str.append("&ORDERID=");
        str.append(ccbPayDTO.getOrderId());
        str.append("&PAYMENT=");
        str.append(ccbPayDTO.getPayment());
        str.append("&CURCODE=");
        str.append(ccbPayDTO.getCurCode());
        str.append("&TXCODE=");
        str.append(ccbPayDTO.getTxCode());

        str.append("&REMARK1=");
        str.append("&REMARK2=");
        str.append("&TYPE=");
        str.append(ccbPayDTO.getType());
        str.append("&PUB=");
        str.append(ccbPayDTO.getPub());
        str.append("&GATEWAY=");
        str.append(ccbPayDTO.getGeteway());
        str.append("&CLIENTIP=");
        str.append("&REGINFO=");
        str.append("&PROINFO=");
        str.append("&REFERER=");

        Map map = new HashMap();
        map.put("MERCHANTID", ccbPayDTO.getMerchantId());
        map.put("POSID", ccbPayDTO.getPostId());
        map.put("BRANCHID", ccbPayDTO.getBranchId());
        map.put("ORDERID", ccbPayDTO.getOrderId());
        map.put("PAYMENT", ccbPayDTO.getPayment());
        map.put("CURCODE", ccbPayDTO.getCurCode());
        map.put("TXCODE", ccbPayDTO.getTxCode());
        map.put("REMARK1", "");
        map.put("REMARK2", "");
        map.put("TYPE", ccbPayDTO.getType());
        map.put("GATEWAY", ccbPayDTO.getGeteway());
        map.put("CLIENTIP", "");
        map.put("REGINFO", "");
        map.put("PROINFO", "");
        map.put("REFERER", "");
        map.put("MAC", Md5Util.md5Str(str.toString())); 
        map.put("PAYMAP", payMap);
        
        String result = "";
        try {
            result = HttpUtil.post(payUrl, map);
        } catch (Exception e) {
            throw new ServiceException("建行接口连接超时,请稍后重试");
        }
        if (ObjectUtil.isNull(result)) {
            return null;
        }

        System.out.println("result:" + result);
        //解析XML 得到支付链接
        if (UnionUtils.isNotEmpty(result)) {
            Document doc = Jsoup.parse(result);
            Elements links = doc.select("form[action]");
            absHref = links.attr("abs:action");
            System.out.println("action: " + absHref);
        }
        return absHref;
}
  • 订单号工具类
public class OrderUtil {
    public static String randomOrderCode() {
        SimpleDateFormat dmDate = new SimpleDateFormat("yyyyMMddHHmmss");
        String randata = getRandom(6);
        Date date = new Date();
        String dateran = dmDate.format(date);
        String Xsode = dateran + randata;
        if (Xsode.length() < 24) {
            Xsode = Xsode + 0;
        }
        return Xsode;
    }

    public static String getRandom(int len) {
        Random r = new Random();
        StringBuilder rs = new StringBuilder();
        for (int i = 0; i < len; i++) {
            rs.append(r.nextInt(10));
        }
        return rs.toString();
    }
}
  • MD5工具类(用于生成mac校验域)
public class Md5Util {

    public static String md5Str(String str) {
        if (str == null) return "";
        return md5Str(str, 0);
    }

    public static String md5Str(String str, int offset) {
        try {
            MessageDigest md5 = MessageDigest.getInstance("MD5");
            byte[] b = str.getBytes("UTF8");
            md5.update(b, offset, b.length);
            return byteArrayToHexString(md5.digest());
        } catch (NoSuchAlgorithmException ex) {
            ex.printStackTrace();
            return null;
        } catch (UnsupportedEncodingException ex) {
            ex.printStackTrace();
            return null;
        }
    }

    public static String byteArrayToHexString(byte[] b) {
        String result = "";
        for (int i = 0; i < b.length; i++) {
            result += byteToHexString(b[i]);
        }
        return result;
    }
}

2.3. 通知

  1. 参考建行给的支付通知文档,如下:

商户通知V2.1.8.pdficon-default.png?t=N7T8https://www.yuque.com/attachments/yuque/0/2024/pdf/38707295/1723617109114-80f09bff-9c35-4373-9d3a-b53a914912db.pdf

  1. 支付完成后,建行会自动调用回调地址(在建行官网商户平台配置,银行的客户经理也能配置),分为页面回调和服务器回调
  • 页面反馈(方法:get):付款人付款完成后,点击“返回商户网站”按钮,触发页面反馈
  • 服务器反馈(方法:post):只要支付成功,无需触发,由建行支付网关,以post 方法,发信息给反馈URL
  /**
     * 支付页面回调(页面反馈 get)付款人付款完成后,点击“返回商户网站”按钮,触发页面反馈
     *
     * @return
     */
    @GetMapping("/payCallBackForPage")
    @ResponseBody
    public String payCallBackForPage(PayCallBackDTO payCallBackDTO, HttpServletResponse response) throws Exception {
        //此处可返回回调页面地址
        return "SUCCESS";
    }

    /**
     * 支付服务器回调(服务器反馈 Post)付款人付款完成后,触发服务器反馈
     *
     * @return
     */
    @PostMapping("/payCallBackForServer")
    @ResponseBody
    public String payCallBackForServer(PayCallBackDTO payCallBackDTO, HttpServletResponse response) throws Exception {
        System.out.println("payCallBackDTO = " + payCallBackDTO);
        // 验签
        rsaSig.setPublicKey(PUBLICKEY);
        String src = "POSID=" + payCallBackDTO.getPOSID() + "&BRANCHID=" + payCallBackDTO.getBRANCHID() + "&ORDERID=" + payCallBackDTO.getORDERID()
                + "&PAYMENT=" + payCallBackDTO.getPAYMENT() + "&CURCODE=" + payCallBackDTO.getCURCODE() + "&REMARK1=" + payCallBackDTO.getREMARK1()
                + "&REMARK2=" + payCallBackDTO.getREMARK2() + "&ACC_TYPE=" + payCallBackDTO.getACC_TYPE() + "&SUCCESS=" + payCallBackDTO.getSUCCESS();
        // 验签结果
        boolean signResult = rsaSig.verifySigature(payCallBackDTO.getSIGN(), src);
        if (!signResult) {
            System.out.println("验签失败!");
            return "SUCCESS";
        }
        String success = payCallBackDTO.getSUCCESS();
        String orderId = payCallBackDTO.getORDERID();
        String money = payCallBackDTO.getPAYMENT();
        System.out.println("success: -" + success);
        System.out.println("orderId: -" + orderId);
        if ("Y".equals(success)) {
            // 更新支付状态  记录收款日志
         } else {
              System.out.println("支付失败");
         }
        // 不论支付成功失败,给银行一个返回结果
        return "SUCCESS";
    }
  • 支付回调实体类
@Data
@ApiModel(value="CCBPayDTO", description="CCBPayDTO对象")
public class PayCallBackDTO {

    @ApiModelProperty(value = "商户柜台代码")
    @JsonProperty("POSTID")
    private String POSTID;

    @ApiModelProperty(value = "分行代码")
    @JsonProperty("BRANCHID")
    private String BRANCHID;

    @ApiModelProperty(value = "订单号")
    @JsonProperty("ORDERID")
    private String ORDERID;

    @ApiModelProperty(value = "付款金额")
    @JsonProperty("PAYMENT")
    private String PAYMENT;

    @ApiModelProperty(value = "币种")
    @JsonProperty("CURCODE")
    private String CURCODE;

    @ApiModelProperty(value = "备注信息1")
    @JsonProperty("REMARK1")
    private String REMARK1;

    @ApiModelProperty(value = "备注信息2")
    @JsonProperty("REMARK2")
    private String REMARK2;

    @ApiModelProperty(value = "账户类型")
    @JsonProperty("ACCTYPE")
    private String ACCTYPE;

    @ApiModelProperty(value = "成功标志  成功-Y,失败-N")
    @JsonProperty("SUCCESS")
    private String SUCCESS;

    @ApiModelProperty(value = "数字签名")
    @JsonProperty("SIGN")
    private String SIGN;
}
  1. 登录建行商户后台配置反馈地址,如下图:

2.4. 查询

  • 根据订单号获取支付详情
public CCBPayStatusVO getOrderStatusById(String orderId) {
    CCBPayStatusVO vo = new CCBPayStatusVO();
    String ORDERDATE = "";
    String BEGORDERTIME = "";
    String ENDORDERTIME = "";
    String TXCODE = "410408";
    String SEL_TYPE = "3";
    String OPERATOR = "";

    String TYPE = "0";
    String KIND = "0";
    String STATUS = "1";
    String ORDERID = orderId;
    String PAGE = "1";
    String CHANNEL = "";

    String param = "MERCHANTID=" + merchantId + "&BRANCHID=" + branchId + "&POSID=" + postId + "&ORDERDATE="
            + ORDERDATE + "&BEGORDERTIME=" + BEGORDERTIME + "&ENDORDERTIME=" + ENDORDERTIME + "&ORDERID="
            + ORDERID + "&QUPWD=&TXCODE=" + TXCODE + "&TYPE=" + TYPE + "&KIND=" + KIND + "&STATUS=" + STATUS +
            "&SEL_TYPE=" + SEL_TYPE + "&PAGE=" + PAGE + "&OPERATOR=" + OPERATOR + "&CHANNEL=" + CHANNEL;
    Map map = new HashMap();
    map.put("MERCHANTID", merchantId);
    map.put("BRANCHID", branchId);
    map.put("POSID", postId);
    map.put("ORDERDATE", ORDERDATE);
    map.put("BEGORDERTIME", BEGORDERTIME);
    map.put("ENDORDERTIME", ENDORDERTIME);
    map.put("QUPWD", quPwd);
    map.put("TXCODE", TXCODE);
    map.put("TYPE", TYPE);
    map.put("KIND", KIND);
    map.put("STATUS", STATUS);
    map.put("ORDERID", ORDERID);
    map.put("PAGE", PAGE);
    map.put("CHANNEL", CHANNEL);
    map.put("SEL_TYPE", SEL_TYPE);
    map.put("OPERATOR", OPERATOR);
    map.put("MAC", Md5Util.md5Str(param.toString()));
    try {
        String ret = HttpUtil.post(payUrl, map);
        if (UnionUtils.isNotEmpty(ret)) {
            Document doc = Jsoup.parse(ret);
            String returnCode = doc.getElementsByTag("RETURN_CODE").first().text();
            System.out.println("获取支付结果:" + ret);
            if ("000000".equals(returnCode)) {
                vo.setMerchantId(doc.getElementsByTag("MERCHANTID").first().text());
                vo.setBranchId(doc.getElementsByTag("BRANCHID").first().text());
                vo.setPosId(doc.getElementsByTag("POSID").first().text());
                vo.setOrderId(doc.getElementsByTag("ORDERID").first().text());
                String timestampStr = doc.getElementsByTag("ORDERDATE").first().text();
                String year = timestampStr.substring(0, 4);
                String month = timestampStr.substring(4, 6);
                String day = timestampStr.substring(6, 8);
                String hour = timestampStr.substring(8, 10);
                String minute = timestampStr.substring(10, 12);
                String second = timestampStr.substring(12);
                vo.setOrderDate(year + "-" + month + "-" + day + " " + hour + ":" + minute + ":" + second);
                vo.setAccDate(doc.getElementsByTag("ACCDATE").first().text());
                vo.setAmount(doc.getElementsByTag("AMOUNT").first().text());
                vo.setStatusCode(doc.getElementsByTag("STATUSCODE").first().text());
                vo.setStatus(doc.getElementsByTag("STATUS").first().text());
                vo.setRefund(doc.getElementsByTag("REFUND").first().text());
                vo.setSign(doc.getElementsByTag("SIGN").first().text());
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    return vo;
}
  • 支付详情实体类
@Data
public class CCBPayStatusVO {
    private String merchantId;
    private String branchId;
    private String posId;
    private String orderId;
    private String orderDate;
    private String accDate;
    private String amount;
    private String statusCode;
    private String status;
    private String refund;
    private String sign;
}

3. 注意事项

  1. 生成MAC签名摘要时,需要商户的柜台公钥后30位;
  2. REMARK1和REMARK2可以传递两个备注,但长度不能超过30位,并且要求对中文需使用escape函数进行编码
  3. 在根据参数拼接MAC签名串时,要注意别把Null拼进去,就是说,要提前将Null 转成空值
  4. 回调验签坑1:文档中对于参数有返回值的意思是:包括空值,但不包括Null。再翻译一下:就算返回值是个空值,也算有返回值,但如果是Null就不算有返回值,就不参与验签;
  5. 回调验签坑2:在验签时还需要商户柜台公钥,如果还像上面那样只截取后面的30位,就会顺利入坑,因为这次是全部;
  6. 回调验签坑3:需要引入建行提供验签的jar包;

  • 20
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值