建行支付对接(H5)
- 随手记
- 9天前
- 19热度
- 0评论
1. 前期准备
- 获取建行龙支付接入指南(接入前建行会发送相关资料)
如上PDF文档中介绍了PC网关支付、移动网关支付、二维码支付、无感支付、微信小程序/公众号支付、刷脸支付等6种支付方式,本文以移动网关支付(H5)进行开发对接。
- 获取对接资料
商户代码(merchantId)、商户登录密码(quPwd)、商户柜台代码(postId)、分行代码(branchId)、交易码(txCode)、公钥(pub)。
注:公钥需要登录商户后台(中国建设银行 商户服务平台)获取,登录进去点击服务管理-商户公钥下载,如下图:
- 开通权限
需要联系分管贵公司的建行工作人员,开通服务器实时反馈、IP白名单权限。
- 流程图
2. 开始对接
2.1. 配置
- 将netpay.jar引用至开发工程中,CCBSign.RSASig是签名包的封装类,验签时使用此类即可。
- <dependency>
- <groupId>com.ccbsign.rsasig</groupId>
- <artifactId>netpay</artifactId>
- <version>1.0</version>
- </dependency>
- 配置yml文件
- thirdparty:
- #建行支付配置
- ccb:
- payUrl: 建行支付地址
- merchantId: 商户代码
- branchId: 分行代码
- postId: 商户柜台代码
- curCode: 币种
- txCode: 交易码
- type: 接口类型
- pubKey: 公钥后30位
- geteWay: 网关类型
- payMap: 支付方式位图
- quPwd: 商户登录密码
2.2. 支付
- 参考建行给的支付文档,如下:
- 定义支付参数对象,代码如下:
@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; }
- 生成订单并获取建行支付链接,代码如下:
- 根据支付参数获取建行支付链接
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("®INFO="); 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. 通知
- 参考建行给的支付通知文档,如下:
- 支付完成后,建行会自动调用回调地址(在建行官网商户平台配置,银行的客户经理也能配置),分为页面回调和服务器回调
- 页面反馈(方法: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; }
- 登录建行商户后台配置反馈地址,如下图:
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. 注意事项
- 生成MAC签名摘要时,需要商户的柜台公钥后30位;
- REMARK1和REMARK2可以传递两个备注,但长度不能超过30位,并且要求对中文需使用escape函数进行编码
- 在根据参数拼接MAC签名串时,要注意别把Null拼进去,就是说,要提前将Null 转成空值
- 回调验签坑1:文档中对于参数有返回值的意思是:包括空值,但不包括Null。再翻译一下:就算返回值是个空值,也算有返回值,但如果是Null就不算有返回值,就不参与验签;
- 回调验签坑2:在验签时还需要商户柜台公钥,如果还像上面那样只截取后面的30位,就会顺利入坑,因为这次是全部;
- 回调验签坑3:需要引入建行提供验签的jar包;
国内站:https://yulcell.com/aa 欢迎移步访问