奇迹每天都在发生,而你能把握的便是当下,未来已来.............
--------------------------------------------------------------------------------------------------------------
一、概念简介
理解什么是UnionPay、ChinaPay
这两个概念如果搞不清楚,绝对够你瞎折腾一段时间的。讲真,拿到这个改换ChinaPay银联支付需求时,我还以为产品经理给错我们官网地址了。
1、UnionPay
中国银联,最大的机构;他本身也提供系统接口但都是B2B的,对于单个商户他们不提供客服,也不提供技术解决,更不会提供商户后台(可查消费记录等);但他的技术接口文档比较齐全,而且也可以使用,警惕不要使用这些接口。
官网:https://open.unionpay.com
2、ChinaPay
银联电子支付公司,第三方的支付公司,UnionPay的所有接口和服务都托管给类似的第三方公司,ChinaPay再向商户服务,ChinaPay有自己的接口标准,但是技术很老,JSP + Servlet 等,官网提供的资料列表很乱,代码样例 for java版本可以说没有,下图中的是Java版示例,但是和接口文档完全不对应,后面是发邮件问才给一个Java示例,与最新接口开发文档也是有出入的;并且从UnionPay官网开通商户后,默认根据地区会自动转到诸如“ChinaPay”这样的第三方支付公司,后面的事情全由ChinaPay代管。
官网:https://service.chinapay.com
二、ChinaPay商户后台
开通商户之后,一般销售会发送邮件给你,一般邮件内容包含开户名称、技术支持联系方式等,另附带一些压缩包,商户登录账号信息,多个账号,不同的账号对应不同的业务接口,略坑,还有接口开发文档,不同语言的SDK、logo等。
主要包括如下内容:
拿到ChinaPay销售发邮件提供的商户号之后,需要到官网登录“商户服务管理系统”,自行申请、下载证书签名等文件,这个也够折腾一段时间的,各种浏览器不支持它们的插件...
注:商户服务管理系统仅支持IE(8、9、10、11)、Firefox、Safari浏览器,其他浏览器暂不支持。
三、获取交易证书
登录商户服务管理系统地址
生产:https://merchant.chinapay.com
测试:https://newpayment-test.chinapay.com
建议用IE或者FireFox登录,需要先下载和安装证书,安装ActiveX控件,登录证书只提供两份,多余两份的请联系销售;登录成功之后可以看到订单、退款单等账单;要实现支付接口,必须先获取一个“交易证书”,如下,到交易证书管理下,点击,弹出选项配置,然后下载即可生成一个 xxx.pfx 交易证书:
点击交易申请证书,获取到证书之后,需要将证书的上传回商户后台,同时本地需要导出证书私钥(带密码),以备用。
四、RSA公钥私钥加密
平台商城与ChinaPay接口通信的时候,都需要对数据进行签名与验签,验证对方身份。这里就涉及到加密算法的知识,其中,RSA加密算法是一种非对称加密算法,在公开密钥加密和电子商业中RSA被广泛使用。
RSA公钥私钥知识只需要记住下面这三点即可:
-
一个端有公钥和私钥两个文件(或者两个字符串),通信时候可以用公钥或者私钥加密。
-
公钥加密数据发送,私钥解密:保证信息的完整性和保密性,保证加密后的信息第三方劫持后无法查看内容,例如邮件等。
-
私钥加密数据发送,公钥解密:保证数据来源可靠,对信息进行签名,ChinaPay即是利用此方式,常用于公告、群发等操作。
五、消费类交易流程
参考接口文档
B2C 、 B2B 、无卡支付交易流程:
支付查询与退款流程图:
demo运行测试:
交易查询:
六、支付接口开发
新建一个SpringBoot测试工程,调试测试成功再整合移动到实际项目中,将ChinaPay提供的xxx.cer、xxx.pfx证书文件拷贝放到指定的路径下,新建一个security.properties放到resources下,便于读取,加载ChinaPay的配置等信息。
1、security.properties
2、导入SDK
将ChinaPay提供的SDK jar包导入本地maven仓库,然后在工程pom文件引入:
注:IDEA 导入Jar 到本地 maven 仓库:
3、支付请求接口
@Slf4j
@Controller
@RequestMapping("/chinaPay")
public class ChinaPayController {
public static final String url = "https://sfj-test.chinapay.com/dac/BatchPayTrade";
public static final String frontPayUrl = "https://newpayment-test.chinapay.com/CTITS/service/rest/page/nref/000000000017/0/0/0/0/0";
public static final String endPayUrl = "https://newpayment-test.chinapay.com/CTITS/service/rest/forward/syn/000000000017/0/0/0/0/0";
@RequestMapping("/toChinaPay")
@ResponseBody
public String toChinaPay(HttpServletRequest req, HttpServletResponse resp) throws Exception {
log.info("####################开始支付####################");
Map<String, Object> paramMap = new TreeMap<>();
Date nowDate = new Date();
paramMap.put("Version", "20140728");
paramMap.put("AccessType","0"); //接入类型 0:商户身份接入(默认)1:机构身份接入
paramMap.put("MerId", "换成cp提供的商户号");
// paramMap.put("MerId", "000091908248636");
paramMap.put("MerOrderNo", "换成订单流水号");
paramMap.put("TranDate", TimeUtil.dateToStr(nowDate,TimeUtil.YYYYMMDD));
paramMap.put("TranTime", TimeUtil.dateToStr(nowDate,TimeUtil.HHMMSS));
paramMap.put("OrderAmt", "1");//单位:分
paramMap.put("BusiType", "0001");//业务类型,固定值
paramMap.put("CommodityMsg","大米100斤");
paramMap.put("MerBgUrl", "http://yl.foodsyoyo.com/chinaPay/notify");//支付异步通知地址:用来接收交易结果后台通知的地址
paramMap.put("MerPageUrl", "http://yl.foodsyoyo.com/chinaPay/payResult");//支付完成跳转的页面
paramMap.put("RemoteAddr", "xxxIP");
System.out.println("==============订单号===========:"+paramMap.get("MerOrderNo"));
SecssUtil secssUtil = ChinaPayUtil.secssUtil;
//签名
secssUtil.sign(paramMap);
if (!SecssConstants.SUCCESS.equals(secssUtil.getErrCode()))
{
log.error(secssUtil.getErrCode() + "=" + secssUtil.getErrMsg());
return secssUtil.getErrMsg();
}
String signature = secssUtil.getSign();
paramMap.put("Signature", signature);
System.out.println("####################请求总参数####################");
System.out.println(paramMap);
//必须构建成【自动提交form表单】html,返回商城前端自动跳转到网银支付页面
String buildRequest = MerchantApiUtil.buildRequest(paramMap,frontPayUrl, "post", "确定");
System.out.println("####################构建的表单####################");
System.out.println(buildRequest);
req.setAttribute("result",buildRequest);
//请求--不能直接使用http工具发起支付请求,需要构建form表单请求自动提交
// String result = HttpUtils.send(frontPayUrl, paramMap);
// System.out.println("返回结果:"+result);
// return "toPay";
return buildRequest;
}
}
注意:支付接口,ChinaPay要求必须构建成【自动提交form表单】html,返回商城前端自动跳转到网银支付页面,,不能直接使用Http工具发起post支付请求,否则报错。
其中,ChinaPayUtil 工具类如下,主要是初始化商户签名、验签配置信息:
@Slf4j
public class ChinaPayUtil {
public static final SecssUtil secssUtil;
//初始化
static {
secssUtil = new SecssUtil();
Resource resource = new ClassPathResource("./security.properties");
File file = null;
try {
file = resource.getFile();
} catch (IOException e) {
e.printStackTrace();
}
boolean bool = secssUtil.init(file.getPath());
if (bool) {
PaymentLog.info("ChinaPay交易证书、验签证书初始化成功!");
} else {
PaymentLog.error("ChinaPay交易证书、验签证书初始化失败:"+secssUtil.getErrCode() + "=" + secssUtil.getErrMsg());
}
}
}
NetPayClient for Java 使用说明:
详细请参考ChinaPay开发接口文档说明。
ChinaPay 针对不同的开发平台和运行平台提供对应的 NetPayClient 版本,实现消费者、商户和银行间的网上安全支付。功能如下:
-
商户对交易请求信息进行签名
-
商户对 ChinaPay 返回的交易应答信息进行签名验证
-
商户对一段字符串进行签名
(1)核心文件清单:
(2)SecssUtil
我们实际开发,主要是调用SDK的SecssUtil中的方法即可:
4、支付请求-结果通知(异步)
分前台与后台地址回调:当交易完成时,ChinaPay 会通过 http post 方式将应答信息发送给商户发送请求报文中提供的前、后台地址(MerPageUrl 、MerBgUrl),应答的数据包括如下内容:
(参数形式是 key-value 形式,用&分割, 注意大小写, 编码方式统一用 UTF-8)
注:具体参考接口文档,这里就不一一贴出来了,对着接口文档从request中读取返回的参数解析、验签、处理后续业务即可。
5、Signature说明:
商户发送给 ChinaPay 的信息均需要用商户的交易证书进行数字签名。ChinaPay 收
到商户的信息后会用商户的公钥证书进行验签,以确定是商户发送的信息。支付交易完成后,支付应答会分前台页面跳转和后台 Http 通知方式返回给商户,商户收到应答信息后,需要用 ChinaPay 提供的证书对返回信息进行验签,以确定此报文是由 ChinaPay 发出。
后台返回字段需要通过 urldecode 进行解码后还原原始报文,前台返回报文无需此操作。
-
JAVA 解码方法:URLDecoder.decode(value, "utf-8")
-
PHP 解码方法:urldecode(value)
-
.NET 解码方法:HttpUtility.UrlDecode(value,"utf-8");
因此这里,针对后台回调,在取出返回的数据时,需要验证签名,以确定此报文是由 ChinaPay 发出,如下,验签方法:
/**
* 回调验签--验证 chinapay 返回的应答数据
* 参考文档Signature说明:后台返回的所有字段每个都需要通过 urldecode 进行解码后还原原始报文,
* 前台返回报文无需此操作
*
* @param jsonObject
* @param notifyMap 支付回调的信息
* @return
*/
@Override
public boolean verifyNotify(JSONObject jsonObject, Map<String, String> notifyMap) {
SecssUtil secssUtil = ChinaPayUtil.secssUtil;
try {
//验签
String sign = notifyMap.get("Signature");
if (StringUtils.isNotEmpty(sign)) {
secssUtil.verify(notifyMap);//入参:返回商户报文中的所有参数
}
if (!SecssConstants.SUCCESS.equals(secssUtil.getErrCode())) {
PaymentLog.error(secssUtil.getErrCode() + "=" + secssUtil.getErrMsg());
PaymentLog.info("ChinaPay返回的应答数据【验签】失败:{}", secssUtil.getErrMsg());
return false;
}
return true;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
至于post回调,ChinaPay返回的数据,从request中取出解析即可:
Map<String, String[]> requestParams = httpServletRequest.getParameterMap();
6、订单交易查询接口:
/**
* 订单查询
* @return
*/
@RequestMapping("/orderQuery")
@ResponseBody
public QueryOrderResultDto orderQuery(){
QueryOrderResultDto orderResultDto = new QueryOrderResultDto();
orderResultDto.setFlowTradeNo("xxxxxx");
orderResultDto.setResult(false);
Map<String, String> paramMap = new TreeMap<>();
paramMap.put("Version", "20140728");
paramMap.put("TranType", "0502");//交易类型,固定值:0502
paramMap.put("BusiType", "0001");//业务类型,固定值
// 商户订单号可包含字母和数字,与 MerId 和 TranDate 一起,ChinaPay是根据这三个字段唯一确定一笔订单
paramMap.put("MerOrderNo", "xxxxxxx");
paramMap.put("MerId", "xxxxxxxxxx");
// 原交易日期 , 格式 :yyyyMMdd TODO 替换
paramMap.put("TranDate", TimeUtil.dateToStr(new Date(), TimeUtil.YYYYMMDD));
//签名
SecssUtil secssUtil = ChinaPayUtil.secssUtil;
secssUtil.sign(paramMap);
if (!SecssConstants.SUCCESS.equals(secssUtil.getErrCode())) {
log.error("ChinaPay签名失败:" + secssUtil.getErrCode() + "=" + secssUtil.getErrMsg());
orderResultDto.setMessage("ChinaPay签名失败");
return orderResultDto;
}
// TODO 商户向ChinaPay的支付接入地址提交订单信息,【表单】采用“post”方式提交
//String buildFormRequest = MerchantApiUtil.buildRequest(paramMap,payQueryUrl, "post", "确定");
// TODO 同步请求
String resp = HttpUtils.send(payQueryUrl, paramMap);
log.info("################交易查询结果:{}",resp);
// 解析同步应答字段
String[] strs = resp.split("&", -1);
Map<String, String> resultMap = new TreeMap<String, String>();
for (String str : strs) {
String[] keyValues = str.split("=", -1);
if (keyValues.length < 2) {
continue;
}
String key = keyValues[0];
String value = keyValues[1];
if (StringUtil.isEmpty(value)) {
continue;
}
//响应字段解码
try {
value = URLDecoder.decode(value, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
resultMap.put(key, value);
}
//返回数据验签
boolean verifyFlag = verifyNotify(resultMap);
if (!verifyFlag) {
log.error("ChinaPay支付查询--返回数据验签失败!");
orderResultDto.setMessage("ChinaPay支付查询--返回数据验签失败!");
return orderResultDto;
}
orderResultDto.setResult(true);
orderResultDto.setMessage("查询成功");
orderResultDto.setPayType("ChinaPay银联支付");
// orderResultDto.setTradeState(OrderStatus);
return orderResultDto;
}
注:其它接口开发参考ChinaPay文档就可以,这里就不一一粘贴示例了0.0
七、测试支付
点击ChinaPay银联支付,商城后台会组装请求参数到form表单,自动提交到ChinaPay
这里测试的商户号只开通了企业网银(B2B),所以只显示一个
这里因为是ChinaPay测试环境,所以可以不填写任何信息,直接点击“支付成功”或“支付失败”即可,完成后续的回调测试:
八、总结
这里演示了一个支付请求接口,包括核心的配置与接口参数详解,只要调通一个,后面的支付查询、退款、扫码支付、分账等就基本类似的套路。主要是ChinaPay的技术支持只提供邮件沟通,沟通效率太慢,遇到对接问题时,发邮件,他们的回答很简单,主要是去多看接口文档,然而接口文档写得马马哈哈,文档有些地方过于简略,只能去尝试调试,有些坑自己踩过才知道,多问ChinaPay。
九、附录-常见接入问题
-
开发过程中,商户签名报 SecurityException 异常
检查交易证书密码是否配置正确,并检查配置文件 security.properties 是否配置正确。需要将 security.properties 放在类路径下,详细配置请参考上面。
-
开发过程中,商户签名报“未初始化配置”异常
检查签名或验签之前是否调用 SecssUtil.init();方法,如果未调用,则调用SecssUtil.init()方法初始化安全签名配置
-
签名过程出现 Illegal key size 错误
请检查下载的 pfx 证书是否正确,或者重新申请 CFCA 交易证书
-
交易失败!错误信息:证书未配置
这个报错通常有两个原因,一是没有申请交易证书,二是交易地址提交错误,测试地址和正式地址搞错了。
-
交易失败!错误信息:验签失败
需要确认交易证书在 Internet 选项---内容---证书---颁发给的名字和在商户服务管理系统里面查到的交易证书的证书名称是一样的。证书确认没有问题,需要确认参加签名的字段和传过来的字段是一样的。
-
支付请求,结果通知(异步)回调,验签失败
请参考Signature字段说明,后台返回的所有字段每个都需要通过 urldecode 进行解码后还原原始报文,前台返回报文无需此操作。
注:当然,开发中还会遇到其它各种不同的问题,请参考文档,解决不了请及时询问ChinaPay技术人员。