建行支付对接(H5)

1. 前期准备

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

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

  1. 获取对接资料

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

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

建行支付对接(H5)

建行支付对接(H5)

  1. 开通权限

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

  1. 流程图

建行支付对接(H5)

2. 开始对接

2.1. 配置

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

2.2. 支付

  1. 参考建行给的支付文档,如下:
  1. 定义支付参数对象,代码如下:
  1. @Data
  2. @ApiModel(value="CCBPayDTO", description="CCBPayDTO对象")
  3. public class CCBPayDTO {
  4. @ApiModelProperty(value = "open_id")
  5. private String openId;
  6. @ApiModelProperty(value = "案件id")
  7. private Long evtId;
  8. @ApiModelProperty(value = "商户代码")
  9. private String merchantId;
  10. @ApiModelProperty(value = "商户柜台代码")
  11. private String postId;
  12. @ApiModelProperty(value = "分行代码")
  13. private String branchId;
  14. @ApiModelProperty(value = "订单号")
  15. private String orderId;
  16. @ApiModelProperty(value = "付款金额")
  17. private String payment;
  18. @ApiModelProperty(value = "币种")
  19. private String curCode;
  20. @ApiModelProperty(value = "备注信息1")
  21. private String remark1;
  22. @ApiModelProperty(value = "备注信息2")
  23. private String remark2;
  24. @ApiModelProperty(value = "交易码")
  25. private String txCode;
  26. @ApiModelProperty(value = "MAC 校验域")
  27. private String mac;
  28. @ApiModelProperty(value = "接口类型")
  29. private String type;
  30. @ApiModelProperty(value = "公钥后30位")
  31. private String pub;
  32. @ApiModelProperty(value = "网关类型")
  33. private String geteway;
  34. @ApiModelProperty(value = "客户端 IP")
  35. private String clientip;
  36. @ApiModelProperty(value = "客户注册信息")
  37. private String reginfo;
  38. @ApiModelProperty(value = "商品信息")
  39. private String proinfo;
  40. @ApiModelProperty(value = "商户 URL")
  41. private String eferer;
  42. @ApiModelProperty(value = "订单超时时间")
  43. private String timeout;
  44. }
建行支付对接(H5)
  1. 生成订单并获取建行支付链接,代码如下:
  • 根据支付参数获取建行支付链接
  1. public String pay(CCBPayDTO ccbPayDTO) {
  2. if (UnionUtils.isEmpty(ccbPayDTO.getPayment())) {
  3. throw new ServiceException("付款金额不能为空!");
  4. }
  5. String absHref = "";
  6. ccbPayDTO.setMerchantId(merchantId);
  7. ccbPayDTO.setBranchId(branchId);
  8. ccbPayDTO.setPostId(postId);
  9. ccbPayDTO.setOrderId(OrderUtil.randomOrderCode());
  10. ccbPayDTO.setCurCode(curCode);
  11. ccbPayDTO.setTxCode(txCode);
  12. ccbPayDTO.setType(type);
  13. String pub = pubKey;
  14. String pubSub = pub.substring(pub.length() - 30); //商户公钥后30
  15. ccbPayDTO.setPub(pubSub);
  16. ccbPayDTO.setGeteway(geteWay);
  17. if (UnionUtils.isEmpty(ccbPayDTO.getPayment())) {
  18. ccbPayDTO.setPayment("0.01");
  19. }
  20. StringBuffer str = new StringBuffer();
  21. str.append("MERCHANTID=");
  22. str.append(ccbPayDTO.getMerchantId());
  23. str.append("&POSID=");
  24. str.append(ccbPayDTO.getPostId());
  25. str.append("&BRANCHID=");
  26. str.append(ccbPayDTO.getBranchId());
  27. str.append("&ORDERID=");
  28. str.append(ccbPayDTO.getOrderId());
  29. str.append("&PAYMENT=");
  30. str.append(ccbPayDTO.getPayment());
  31. str.append("&CURCODE=");
  32. str.append(ccbPayDTO.getCurCode());
  33. str.append("&TXCODE=");
  34. str.append(ccbPayDTO.getTxCode());
  35. str.append("&REMARK1=");
  36. str.append("&REMARK2=");
  37. str.append("&TYPE=");
  38. str.append(ccbPayDTO.getType());
  39. str.append("&PUB=");
  40. str.append(ccbPayDTO.getPub());
  41. str.append("&GATEWAY=");
  42. str.append(ccbPayDTO.getGeteway());
  43. str.append("&CLIENTIP=");
  44. str.append("&REGINFO=");
  45. str.append("&PROINFO=");
  46. str.append("&REFERER=");
  47. Map map = new HashMap();
  48. map.put("MERCHANTID", ccbPayDTO.getMerchantId());
  49. map.put("POSID", ccbPayDTO.getPostId());
  50. map.put("BRANCHID", ccbPayDTO.getBranchId());
  51. map.put("ORDERID", ccbPayDTO.getOrderId());
  52. map.put("PAYMENT", ccbPayDTO.getPayment());
  53. map.put("CURCODE", ccbPayDTO.getCurCode());
  54. map.put("TXCODE", ccbPayDTO.getTxCode());
  55. map.put("REMARK1", "");
  56. map.put("REMARK2", "");
  57. map.put("TYPE", ccbPayDTO.getType());
  58. map.put("GATEWAY", ccbPayDTO.getGeteway());
  59. map.put("CLIENTIP", "");
  60. map.put("REGINFO", "");
  61. map.put("PROINFO", "");
  62. map.put("REFERER", "");
  63. map.put("MAC", Md5Util.md5Str(str.toString()));
  64. map.put("PAYMAP", payMap);
  65. String result = "";
  66. try {
  67. result = HttpUtil.post(payUrl, map);
  68. } catch (Exception e) {
  69. throw new ServiceException("建行接口连接超时,请稍后重试");
  70. }
  71. if (ObjectUtil.isNull(result)) {
  72. return null;
  73. }
  74. System.out.println("result:" + result);
  75. //解析XML 得到支付链接
  76. if (UnionUtils.isNotEmpty(result)) {
  77. Document doc = Jsoup.parse(result);
  78. Elements links = doc.select("form[action]");
  79. absHref = links.attr("abs:action");
  80. System.out.println("action: " + absHref);
  81. }
  82. return absHref;
  83. }
建行支付对接(H5)
  • 订单号工具类
  1. public class OrderUtil {
  2. public static String randomOrderCode() {
  3. SimpleDateFormat dmDate = new SimpleDateFormat("yyyyMMddHHmmss");
  4. String randata = getRandom(6);
  5. Date date = new Date();
  6. String dateran = dmDate.format(date);
  7. String Xsode = dateran + randata;
  8. if (Xsode.length() < 24) {
  9. Xsode = Xsode + 0;
  10. }
  11. return Xsode;
  12. }
  13. public static String getRandom(int len) {
  14. Random r = new Random();
  15. StringBuilder rs = new StringBuilder();
  16. for (int i = 0; i < len; i++) {
  17. rs.append(r.nextInt(10));
  18. }
  19. return rs.toString();
  20. }
  21. }
建行支付对接(H5)
  • MD5工具类(用于生成mac校验域)
  1. public class Md5Util {
  2. public static String md5Str(String str) {
  3. if (str == null) return "";
  4. return md5Str(str, 0);
  5. }
  6. public static String md5Str(String str, int offset) {
  7. try {
  8. MessageDigest md5 = MessageDigest.getInstance("MD5");
  9. byte[] b = str.getBytes("UTF8");
  10. md5.update(b, offset, b.length);
  11. return byteArrayToHexString(md5.digest());
  12. } catch (NoSuchAlgorithmException ex) {
  13. ex.printStackTrace();
  14. return null;
  15. } catch (UnsupportedEncodingException ex) {
  16. ex.printStackTrace();
  17. return null;
  18. }
  19. }
  20. public static String byteArrayToHexString(byte[] b) {
  21. String result = "";
  22. for (int i = 0; i < b.length; i++) {
  23. result += byteToHexString(b[i]);
  24. }
  25. return result;
  26. }
  27. }
建行支付对接(H5)

2.3. 通知

  1. 参考建行给的支付通知文档,如下:
  1. 支付完成后,建行会自动调用回调地址(在建行官网商户平台配置,银行的客户经理也能配置),分为页面回调和服务器回调
  • 页面反馈(方法:get):付款人付款完成后,点击“返回商户网站”按钮,触发页面反馈
  • 服务器反馈(方法:post):只要支付成功,无需触发,由建行支付网关,以post 方法,发信息给反馈URL
  1. /**
  2. * 支付页面回调(页面反馈 get)付款人付款完成后,点击“返回商户网站”按钮,触发页面反馈
  3. *
  4. * @return
  5. */
  6. @GetMapping("/payCallBackForPage")
  7. @ResponseBody
  8. public String payCallBackForPage(PayCallBackDTO payCallBackDTO, HttpServletResponse response) throws Exception {
  9. //此处可返回回调页面地址
  10. return "SUCCESS";
  11. }
  12. /**
  13. * 支付服务器回调(服务器反馈 Post)付款人付款完成后,触发服务器反馈
  14. *
  15. * @return
  16. */
  17. @PostMapping("/payCallBackForServer")
  18. @ResponseBody
  19. public String payCallBackForServer(PayCallBackDTO payCallBackDTO, HttpServletResponse response) throws Exception {
  20. System.out.println("payCallBackDTO = " + payCallBackDTO);
  21. // 验签
  22. rsaSig.setPublicKey(PUBLICKEY);
  23. String src = "POSID=" + payCallBackDTO.getPOSID() + "&BRANCHID=" + payCallBackDTO.getBRANCHID() + "&ORDERID=" + payCallBackDTO.getORDERID()
  24. + "&PAYMENT=" + payCallBackDTO.getPAYMENT() + "&CURCODE=" + payCallBackDTO.getCURCODE() + "&REMARK1=" + payCallBackDTO.getREMARK1()
  25. + "&REMARK2=" + payCallBackDTO.getREMARK2() + "&ACC_TYPE=" + payCallBackDTO.getACC_TYPE() + "&SUCCESS=" + payCallBackDTO.getSUCCESS();
  26. // 验签结果
  27. boolean signResult = rsaSig.verifySigature(payCallBackDTO.getSIGN(), src);
  28. if (!signResult) {
  29. System.out.println("验签失败!");
  30. return "SUCCESS";
  31. }
  32. String success = payCallBackDTO.getSUCCESS();
  33. String orderId = payCallBackDTO.getORDERID();
  34. String money = payCallBackDTO.getPAYMENT();
  35. System.out.println("success: -" + success);
  36. System.out.println("orderId: -" + orderId);
  37. if ("Y".equals(success)) {
  38. // 更新支付状态 记录收款日志
  39. } else {
  40. System.out.println("支付失败");
  41. }
  42. // 不论支付成功失败,给银行一个返回结果
  43. return "SUCCESS";
  44. }
建行支付对接(H5)
  • 支付回调实体类
  1. @Data
  2. @ApiModel(value="CCBPayDTO", description="CCBPayDTO对象")
  3. public class PayCallBackDTO {
  4. @ApiModelProperty(value = "商户柜台代码")
  5. @JsonProperty("POSTID")
  6. private String POSTID;
  7. @ApiModelProperty(value = "分行代码")
  8. @JsonProperty("BRANCHID")
  9. private String BRANCHID;
  10. @ApiModelProperty(value = "订单号")
  11. @JsonProperty("ORDERID")
  12. private String ORDERID;
  13. @ApiModelProperty(value = "付款金额")
  14. @JsonProperty("PAYMENT")
  15. private String PAYMENT;
  16. @ApiModelProperty(value = "币种")
  17. @JsonProperty("CURCODE")
  18. private String CURCODE;
  19. @ApiModelProperty(value = "备注信息1")
  20. @JsonProperty("REMARK1")
  21. private String REMARK1;
  22. @ApiModelProperty(value = "备注信息2")
  23. @JsonProperty("REMARK2")
  24. private String REMARK2;
  25. @ApiModelProperty(value = "账户类型")
  26. @JsonProperty("ACCTYPE")
  27. private String ACCTYPE;
  28. @ApiModelProperty(value = "成功标志 成功-Y,失败-N")
  29. @JsonProperty("SUCCESS")
  30. private String SUCCESS;
  31. @ApiModelProperty(value = "数字签名")
  32. @JsonProperty("SIGN")
  33. private String SIGN;
  34. }
建行支付对接(H5)
  1. 登录建行商户后台配置反馈地址,如下图:

    建行支付对接(H5)

2.4. 查询

  • 根据订单号获取支付详情
  1. public CCBPayStatusVO getOrderStatusById(String orderId) {
  2. CCBPayStatusVO vo = new CCBPayStatusVO();
  3. String ORDERDATE = "";
  4. String BEGORDERTIME = "";
  5. String ENDORDERTIME = "";
  6. String TXCODE = "410408";
  7. String SEL_TYPE = "3";
  8. String OPERATOR = "";
  9. String TYPE = "0";
  10. String KIND = "0";
  11. String STATUS = "1";
  12. String ORDERID = orderId;
  13. String PAGE = "1";
  14. String CHANNEL = "";
  15. String param = "MERCHANTID=" + merchantId + "&BRANCHID=" + branchId + "&POSID=" + postId + "&ORDERDATE="
  16. + ORDERDATE + "&BEGORDERTIME=" + BEGORDERTIME + "&ENDORDERTIME=" + ENDORDERTIME + "&ORDERID="
  17. + ORDERID + "&QUPWD=&TXCODE=" + TXCODE + "&TYPE=" + TYPE + "&KIND=" + KIND + "&STATUS=" + STATUS +
  18. "&SEL_TYPE=" + SEL_TYPE + "&PAGE=" + PAGE + "&OPERATOR=" + OPERATOR + "&CHANNEL=" + CHANNEL;
  19. Map map = new HashMap();
  20. map.put("MERCHANTID", merchantId);
  21. map.put("BRANCHID", branchId);
  22. map.put("POSID", postId);
  23. map.put("ORDERDATE", ORDERDATE);
  24. map.put("BEGORDERTIME", BEGORDERTIME);
  25. map.put("ENDORDERTIME", ENDORDERTIME);
  26. map.put("QUPWD", quPwd);
  27. map.put("TXCODE", TXCODE);
  28. map.put("TYPE", TYPE);
  29. map.put("KIND", KIND);
  30. map.put("STATUS", STATUS);
  31. map.put("ORDERID", ORDERID);
  32. map.put("PAGE", PAGE);
  33. map.put("CHANNEL", CHANNEL);
  34. map.put("SEL_TYPE", SEL_TYPE);
  35. map.put("OPERATOR", OPERATOR);
  36. map.put("MAC", Md5Util.md5Str(param.toString()));
  37. try {
  38. String ret = HttpUtil.post(payUrl, map);
  39. if (UnionUtils.isNotEmpty(ret)) {
  40. Document doc = Jsoup.parse(ret);
  41. String returnCode = doc.getElementsByTag("RETURN_CODE").first().text();
  42. System.out.println("获取支付结果:" + ret);
  43. if ("000000".equals(returnCode)) {
  44. vo.setMerchantId(doc.getElementsByTag("MERCHANTID").first().text());
  45. vo.setBranchId(doc.getElementsByTag("BRANCHID").first().text());
  46. vo.setPosId(doc.getElementsByTag("POSID").first().text());
  47. vo.setOrderId(doc.getElementsByTag("ORDERID").first().text());
  48. String timestampStr = doc.getElementsByTag("ORDERDATE").first().text();
  49. String year = timestampStr.substring(0, 4);
  50. String month = timestampStr.substring(4, 6);
  51. String day = timestampStr.substring(6, 8);
  52. String hour = timestampStr.substring(8, 10);
  53. String minute = timestampStr.substring(10, 12);
  54. String second = timestampStr.substring(12);
  55. vo.setOrderDate(year + "-" + month + "-" + day + " " + hour + ":" + minute + ":" + second);
  56. vo.setAccDate(doc.getElementsByTag("ACCDATE").first().text());
  57. vo.setAmount(doc.getElementsByTag("AMOUNT").first().text());
  58. vo.setStatusCode(doc.getElementsByTag("STATUSCODE").first().text());
  59. vo.setStatus(doc.getElementsByTag("STATUS").first().text());
  60. vo.setRefund(doc.getElementsByTag("REFUND").first().text());
  61. vo.setSign(doc.getElementsByTag("SIGN").first().text());
  62. }
  63. }
  64. } catch (Exception e) {
  65. e.printStackTrace();
  66. }
  67. return vo;
  68. }
建行支付对接(H5)
  • 支付详情实体类
  1. @Data
  2. public class CCBPayStatusVO {
  3. private String merchantId;
  4. private String branchId;
  5. private String posId;
  6. private String orderId;
  7. private String orderDate;
  8. private String accDate;
  9. private String amount;
  10. private String statusCode;
  11. private String status;
  12. private String refund;
  13. private String sign;
  14. }

3. 注意事项

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