聚合支付
第四章 银联扫码支付
前言
本章主要介绍银联扫码支付接口:ChinaPay 新一代商户支付接口接入为 ChinaPay 商户会员提供完善的网上支付解决方案,让商户更快捷、方便和安全的开展网上交易,Chinapay为商户提供了多种支付场景用的支付接口,本文只介绍其中一种-扫码支付。
提示:以下是本篇文章正文内容,下面案例可供参考
一、开发前的准备
首先商户需要申请在 ChinaPay 测试环境开户,这时商户会获得商户号、公钥、私钥、支付工具类以及一些ChinaPay的logo图片和开发技术人员需要的“商户接入手册”开发文档。
在开发文档中我们可找到“扫码支付”的流程图,根据流程图理解整个支付流程的支付实现逻辑。流程图如下:
二、java实现
1.pom中引入jar包
jar包是需要商户申请成为Chinapay的商户后,银联支付平台会提供这样一个工具类的离线jar包(主要是一些http请求方法的封装和签名和验签方法)以及支付实现的demo。
有两种方式使用这个加包:第一种是将离线jar包引入到本地maven仓库,可参考这位大佬的方法https://blog.csdn.net/kkagr/article/details/84502062 不过这种方式容易收到jdk版本的影响,可能jdk7及以下的不能用这种方法。第二种是将jar包解压将里面的工具类提取出来放到本地程序中。
由于本次支付接口是对接的“扫码支付”方式,最重要的是将获取到的二维码做成图片展示给客户进行支付,即流程图中的“根据返回的二维码信息生成二维码并展示给持卡人”,所以需要引入hutool工具将二维码做成图片兵役base64流的方式传至页面前端。
2.支付相关参数
代码如下(示例):
2.1 商户秘钥配置方法:
使用此方法需要将 security.properties 放在类路径下,并配置如下:
#交易证书路径
sign.file=D:/cert_cp/000000000000001.pfx
#交易证书密码
sign.file.password=XXXXXX
#交易证书的密钥容器格式
sign.cert.type=PKCS12
#报文中不参与签名的字段名称,多个字段用逗号进行分隔
sign.invalid.fields=Signature,CertId
#验签证书路径
verify.file=D:/cert_cp/cp_test.cer
#签名值字段名称
signature.field=Signature
2.2支付平台参数
public class chinaPayConstants {
public static final String VERSION = "20***8";
public static final String MERCHANT_ID = "8299***01";
public static final String PAGE_RET_URL = "http://localhost:8080/***pageretback";
public static final String BG_RET_URL = "http://localhost:8080/***pageretback";
public static final String PAY_SEND_URL = "https://******";
public static final String ORDER_QUERY = "https:******";
public static final String ORDER_DATE_QUERY = "https:*****";
}
3.支付实现
@RequestMapping(value = {"/chinaPay/payUrlChinaPay"})
public void payUrlChinaPay(HttpServletRequest request, HttpServletResponse response,
@RequestParam(value = "totalFee") int totalFee,
@RequestParam(value = "outTradeNo") String outTradeNo,
@RequestParam(value = "productId") String productId) throws Exception {
//模拟测试订单信息
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd");
SimpleDateFormat timeFormat = new SimpleDateFormat("HHmmss");
SimpleDateFormat format = new SimpleDateFormat("yyyyMMddHHmmss");
Date current = new Date();
Map<String, String> orderReserveMap = new HashMap<>();
orderReserveMap.put("OrderType","0001");
orderReserveMap.put("OrderValidTime",format.format(current));
orderReserveMap.put("qrPattern","link");
String orderReserve = JSON.toJSONString(orderReserveMap);
Map<String, Object> submitFromData = new HashMap();
submitFromData.put("Version", chinaPayConstants.VERSION);
submitFromData.put("AccessType","0");
submitFromData.put("MerId",chinaPayConstants.MERCHANT_ID);
submitFromData.put("MerOrderNo",outTradeNo);
submitFromData.put("TranDate",dateFormat.format(new Date()));
submitFromData.put("TranTime",timeFormat.format(new Date()));
submitFromData.put("OrderAmt",String.valueOf(totalFee));
submitFromData.put("TranType","0009");
submitFromData.put("BusiType","0001");
submitFromData.put("CurryNo","CNY");
submitFromData.put("MerPageUrl",chinaPayConstants.PAGE_RET_URL);
submitFromData.put("MerBgUrl",chinaPayConstants.BG_RET_URL);
submitFromData.put("MerResv",productId);
submitFromData.put("PayTimeOut","30");
submitFromData.put("OrderReserved",orderReserve);
submitFromData.put("CommodityMsg", "测试支付");//商品名称
//String MerKeyFile = ResourceUtils.getFile("classpath:security.properties").getParentFile().getAbsolutePath();
String MerKeyFile = NetpayChinaPayController.class.getClassLoader().getResource("/").getPath();
SecssUtil secssUtil = new SecssUtil();
secssUtil.init(MerKeyFile);
System.out.println("weee"+secssUtil.getErrCode()+secssUtil.getErrMsg());
secssUtil.sign(submitFromData);
String signature = secssUtil.getSign();
submitFromData.put("Signature", signature);
String params = CommonUtil.getURLParam(submitFromData, true);
HttpResponse httpRes = HttpHelper.doHttp(chinaPayConstants.PAY_SEND_URL, "POST", "UTF-8",params, "60000");
//获取二维码链接
String codeUrl = "";
if ((httpRes == null) || (httpRes.getResponseCode() != 200)){
request.setAttribute("msg", "调用接口失败,响应码[" + Objects.requireNonNull(httpRes).getResponseCode() + "]");
}else {
String result = httpRes.getBody();
Map resultMap = CommonUtil.strToMap(result);
String respCode = (String)resultMap.get("respCode");
if ("0000".equals(respCode)){
secssUtil.verify(resultMap);
if("00".equals(secssUtil.getErrCode())){
String payReserved = (String)resultMap.get("PayReserved");
Map payReservedMap = JSON.parseObject(payReserved, Map.class);
codeUrl = (String)payReservedMap.get("QrCode");
codeUrl = URLDecoder.decode(codeUrl,"UTF-8");
}
}
}
if (!StringUtils.isNotBlank(codeUrl)) {
System.out.println("----生成二维码失败----");
} else {
//根据链接生成二维码
BufferedImage bufferedImage = QrCodeUtil.generate(qrCode, 300, 300);
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
try {
ImageIO.write(bufferedImage, "png", outStream);
byte[] bytes = outStream.toByteArray();
imageBase64 = cn.hutool.core.codec.Base64.encode(bytes);
} catch (IOException e) {
e.printStackTrace();
}
request.setAttribute("imageBase64","data:image/png;base64,"+imageBase64);
}
}
4.查询实现
4.1二维码订单支付日期查询接口
因为订单查询接口需要上传交易日期,对于扫码付的商户,交易日期仅在消费类交易接口应答报文中返回,考虑到各种因素可能导致商户接收不到应答报文,估提供二维码订单支付日期查询接口,供商户查询使用。
@RequestMapping(value = {"/chinaPay/orderQueryChinaPay"})
public String orderQueryChinaPay(@RequestParam(value = "outTradeNo") String outTradeNo) throws Exception {
JSONObject responseObject = new JSONObject();
Order order = WxConfig.getOrderMap("order");
Date sendDate = order.getSendDate();
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd");
SimpleDateFormat timeFormat = new SimpleDateFormat("HHmmss");
Map<String, Object> submitFromData = new HashMap();
submitFromData.put("Version",chinaPayConstants.VERSION);
submitFromData.put("AccessType","0");
submitFromData.put("MerId",chinaPayConstants.MERCHANT_ID);
submitFromData.put("MerOrderNo",order.getOrderNo());
submitFromData.put("TranDate",dateFormat.format(sendDate));
submitFromData.put("TranTime",timeFormat.format(sendDate));
//String MerKeyFile = ResourceUtils.getFile("classpath:security.properties").getParentFile().getAbsolutePath();
String MerKeyFile = NetpayChinaPayController.class.getClassLoader().getResource("/").getPath();
SecssUtil secssUtil = new SecssUtil();
secssUtil.init(MerKeyFile);
secssUtil.sign(submitFromData);
String signature = secssUtil.getSign();
submitFromData.put("Signature",signature);
String paramsDate = CommonUtil.getURLParam(submitFromData, true);
HttpResponse httpRes = HttpHelper.doHttp(chinaPayConstants.ORDER_DATE_QUERY, "POST", "UTF-8",paramsDate, "60000");
if ((httpRes == null) || (httpRes.getResponseCode() != 200)){
responseObject.put("msg", "调用二维码订单交易日期查询接口失败,响应码[" + httpRes.getResponseCode() + "]");
}else {
String result = httpRes.getBody();
Map payDateMap = CommonUtil.strToMap(result);
String respCode = (String)payDateMap.get("respCode");
if ("0000".equals(respCode)){
secssUtil.verify(payDateMap);
if("00".equals(secssUtil.getErrCode())){
String payDate = (String)payDateMap.get("PayDate");
}
}
}
return responseObject.toString();
}
4.2交易查询接口
@RequestMapping(value = {"/chinaPay/orderQueryChinaPay"})
public String orderQueryChinaPay(@RequestParam(value = "payDate") String payDate) throws Exception {
JSONObject responseObject = new JSONObject();
Order order = WxConfig.getOrderMap("order");
Date sendDate = order.getSendDate();
Map<String, Object> queryFromData = new HashMap();
queryFromData.put("Version",chinaPayConstants.VERSION);
queryFromData.put("AccessType","0");
queryFromData.put("MerId",chinaPayConstants.MERCHANT_ID);
queryFromData.put("MerOrderNo",order.getOrderNo());
queryFromData.put("TranDate",payDate);
queryFromData.put("TranType","0502");
queryFromData.put("BusiType","0001");
//String MerKeyFile = ResourceUtils.getFile("classpath:security.properties").getParentFile().getAbsolutePath();
String MerKeyFile = NetpayChinaPayController.class.getClassLoader().getResource("/").getPath();
SecssUtil secssUtil = new SecssUtil();
secssUtil.init(MerKeyFile);
secssUtil.sign(queryFromData);
queryFromData.put("Signature",secssUtil.getSign());
String paramsQuery = CommonUtil.getURLParam(queryFromData, true);
HttpResponse httpResQuery = HttpHelper.doHttp(chinaPayConstants.ORDER_QUERY, "POST", "UTF-8",paramsQuery, "60000");
if ((httpResQuery == null) || (httpResQuery.getResponseCode() != 200)){
responseObject.put("msg", "调用交易查询接口失败,响应码[" + httpResQuery.getResponseCode() + "]");
}else {
String resultQuery = httpResQuery.getBody();
Map queryDataMap = CommonUtil.strToMap(resultQuery);
String queryRespCode = (String)queryDataMap.get("respCode");
if ("0000".equals(queryRespCode)){
secssUtil.verify(queryDataMap);
if("00".equals(secssUtil.getErrCode())){
String orderStatus = (String)queryDataMap.get("OrderStatus");
if ("0000".equals(orderStatus)){
responseObject.put("status","1");
System.out.println("支付成功");
}else {
responseObject.put("status","0");
}
}
}
}
return responseObject.toString();
}
5.支付成功回调
当 ChinaPay 交易平台处理完成时,ChinaPay 会将订单信息发送给商户,对于商户后台地址的异步通知,ChinaPay 通知商户交易结果后,如果收到商户服务器返回的 http 200 应答码,就认为是通知商户成功,否则当天会再次通知,当天再次通知最多 10 次。
@RequestMapping(value = {"/chinaPay/unifiedorderNotify"})
public void unifiedorderNotifyChinaPay(HttpServletRequest request, HttpServletResponse response) throws Exception {
Map<String, String> params = new HashMap<String, String>();
Map<String, String[]> requestParams = request.getParameterMap();
for (Iterator<String> iter = requestParams.keySet().iterator(); iter.hasNext(); ) {
String name = (String) iter.next();
String[] values = (String[]) requestParams.get(name);
String valueStr = "";
for (int i = 0; i < values.length; i++) {
valueStr = (i == values.length - 1) ? valueStr + values[i]
: valueStr + values[i] + ",";
}
params.put(name, valueStr);
}
//String MerKeyFile = ResourceUtils.getFile("classpath:security.properties").getParentFile().getAbsolutePath();
String MerKeyFile = NetpayRetController.class.getClassLoader().getResource("/").getPath();
SecssUtil secssUtil = new SecssUtil();
secssUtil.init(MerKeyFile);
secssUtil.verify(params);
if("00".equals(secssUtil.getErrCode())){
String orderStatus = params.get("OrderStatus");
if ("0000".equals(orderStatus)){
System.out.println("支付成功");
//回复支付公司
try {
response.getWriter().print("http 200");
} catch (IOException e) {
e.printStackTrace();
}
}else {
System.out.println("支付失败");
}
}
}
6、工具类
/**
* 获取最终路径
*/
private static String getFinalPath(String rootPath,String path){
rootPath = rootPath.replace('/', '\\');
rootPath = rootPath.replace("file:", "");
rootPath = rootPath.replace("classes\\", "");
rootPath = rootPath.substring(1);
rootPath = rootPath+path;
rootPath = rootPath.replace('\\', '/');
rootPath = "/"+rootPath;
return rootPath;
}