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