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

评论
发表评论