目录
前言
一、银联支付java sdk
二、官方DEMO
三、springboot项目使用银联支付
3.1、新建项目
3.2、配置
3.3、封装客户端
写在后面
前言
项目里使用了微信支付,支付宝支付。但是还不满足!我们还需要银联支付!!那就去看看呗。
https://open.unionpay.com/tjweb/api/list?bussId=52,这是银联的开放平台,看了一会,觉得在线网关支付符合要求,虽然看不懂这个名词。先把他的sdk和demo下载下来,下完我就懵逼了。demo竟然是用jsp和servlet写的,这和微信支付和支付宝支付比起来有点差距啊。不过没事,一点一点来就是了。
一、银联支付java sdk
我估计maven中央仓库没有银联支付的sdk,所以就只能自给自足了。新建个项目,先把银联支付的sdk抽出来,可以参考我的码云 https://gitee.com/waynelee/unionpay-java-sdk,然后打入maven本地仓库就可以用了。
mvn install:install-file -Dfile=E:\unionpay-sdk-java-0.0.1.jar -DgroupId=com.unionpay -DartifactId=unionpay-sdk-java -Dversion=0.0.1 -Dpackaging=jar
二、官方DEMO
把银联支付DEMO下载后导入Eclipse,在src/assets/测试环境证书文件夹里是测试环境的证书,把四个证书拷贝至D盘certs文件下,然后确保src/acp_sdk.properties配置文件中配置的四个证书路径是正确的。然后把项目运行在tomcat上,浏览器打开localhost:8081/ACPSample_B2C
如上图,点击消费样例->消费,提交,会跳转至银联支付页面
说明整个流程没问题,接下来就是把这些代码放进springboot项目里了。
三、springboot项目使用银联支付
3.1、新建项目
新建springboot项目,pom文件引如自己打好的银联支付的jar包
<dependency><groupId>com.unionpay</groupId><artifactId>unionpay-sdk-java</artifactId><version>0.0.1</version>
</dependency>
拷贝官方DEMO的 src/acp_sdk.properties至新建项目的src/main/resources/acp_sdk.properties
拷贝官方DEMO的 src/com/unionpay/acp/demo/DemoBase.java至新建项目的src/main/java/com/yuyi/lwq/unionpay/DemoBase.java
3.2、配置
通过阅读银联支付的官方DEMO,发现支付是通过src/com/unionpay/acp/demo/consume/Form_6_2_FrontConsume.java这个类实现的。我们发现在servlet的init方法中注释了一段代码
SDKConfig.getConfig().loadPropertiesFromSrc(); //从classpath加载acp_sdk.properties文件
所以,在springboot项目启动的时候,我们也要从classpath下加载拷贝过来的acp_sdk.properties文件,所以项目启动类需要实现CommandLineRunner,在项目启动时加载配置文件。
@SpringBootApplication
public class UnionpayApplication implements CommandLineRunner {public static void main(String[] args) {SpringApplication.run(UnionpayApplication.class, args);}@Overridepublic void run(String... args) throws Exception {SDKConfig.getConfig().loadPropertiesFromSrc();}
}
新建配置类,用于存储一些常量,也可以写在application.properties里,然后在这个类里引用。我就省事写死。
public class UnionpayConfig {/**商户号*/public static final String MER_ID = "777290058110048";/*** 前端异步通知地址*/public static String FRONT_URL = "http://localhost:8080/front.html";/*** 后台异步通知地址*/public static String BACK_URL = "http://yourdomain/union/notifyurl";}
3.3、封装客户端
新建UnionPayClient.java,此类的代码主要是拷贝自官方DEMO里的代码,在此基础上做一些修改,毕竟官方已经实现了,所以我们就不必再写过多的代码了。
public class UnionPayClient {/*** 银联支付* @param orderId 商户订单号* @param txnAmt 金额 (分)* @param goodsName 商品名称* @return*/public static String pay(String orderId,String txnAmt,String goodsName){String txnTime = DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(LocalDateTime.now());System.out.println(txnTime);Map<String, String> requestData = new HashMap<String, String>();System.out.println("encoding"+DemoBase.encoding);/***银联全渠道系统,产品参数,除了encoding自行选择外其他不需修改***/requestData.put("version", "5.1.0"); //版本号,全渠道默认值requestData.put("encoding", DemoBase.encoding); //字符集编码,可以使用UTF-8,GBK两种方式requestData.put("signMethod", SDKConfig.getConfig().getSignMethod()); //签名方法requestData.put("txnType", "01"); //交易类型 ,01:消费requestData.put("txnSubType", "01"); //交易子类型, 01:自助消费requestData.put("bizType", "000201"); //业务类型,B2C网关支付,手机wap支付requestData.put("channelType", "07"); //渠道类型,这个字段区分B2C网关支付和手机wap支付;07:PC,平板 08:手机/***商户接入参数***/requestData.put("merId", UnionpayConfig.MER_ID); //商户号码,请改成自己申请的正式商户号或者open上注册得来的777测试商户号requestData.put("accessType", "0"); //接入类型,0:直连商户 requestData.put("orderId",orderId); //商户订单号,8-40位数字字母,不能含“-”或“_”,可以自行定制规则 requestData.put("txnTime", txnTime); //订单发送时间,取系统时间,格式为YYYYMMDDhhmmss,必须取当前时间,否则会报txnTime无效requestData.put("currencyCode", "156"); //交易币种(境内商户一般是156 人民币) requestData.put("txnAmt", txnAmt); //交易金额,单位分,不要带小数点//requestData.put("reqReserved", "透传字段"); //请求方保留域,如需使用请启用即可;透传字段(可以实现商户自定义参数的追踪)本交易的后台通知,对本交易的交易状态查询交易、对账文件中均会原样返回,商户可以按需上传,长度为1-1024个字节。出现&={}[]符号时可能导致查询接口应答报文解析失败,建议尽量只传字母数字并使用|分割,或者可以最外层做一次base64编码(base64编码之后出现的等号不会导致解析失败可以不用管)。 requestData.put("riskRateInfo", "{commodityName="+goodsName+"}");//前台通知地址 (需设置为外网能访问 http https均可),支付成功后的页面 点击“返回商户”按钮的时候将异步通知报文post到该地址//如果想要实现过几秒中自动跳转回商户页面权限,需联系银联业务申请开通自动返回商户权限//异步通知参数详见open.unionpay.com帮助中心 下载 产品接口规范 网关支付产品接口规范 消费交易 商户通知requestData.put("frontUrl", UnionpayConfig.FRONT_URL);//后台通知地址(需设置为【外网】能访问 http https均可),支付成功后银联会自动将异步通知报文post到商户上送的该地址,失败的交易银联不会发送后台通知//后台通知参数详见open.unionpay.com帮助中心 下载 产品接口规范 网关支付产品接口规范 消费交易 商户通知//注意:1.需设置为外网能访问,否则收不到通知 2.http https均可 3.收单后台通知后需要10秒内返回http200或302状态码 // 4.如果银联通知服务器发送通知后10秒内未收到返回状态码或者应答码非http200,那么银联会间隔一段时间再次发送。总共发送5次,每次的间隔时间为0,1,2,4分钟。// 5.后台通知地址如果上送了带有?的参数,例如:http://abc/web?a=b&c=d 在后台通知处理程序验证签名之前需要编写逻辑将这些字段去掉再验签,否则将会验签失败requestData.put("backUrl", UnionpayConfig.BACK_URL);// 订单超时时间。// 超过此时间后,除网银交易外,其他交易银联系统会拒绝受理,提示超时。 跳转银行网银交易如果超时后交易成功,会自动退款,大约5个工作日金额返还到持卡人账户。// 此时间建议取支付时的北京时间加15分钟。// 超过超时时间调查询接口应答origRespCode不是A6或者00的就可以判断为失败。requestData.put("payTimeout", new SimpleDateFormat("yyyyMMddHHmmss").format(new Date().getTime() + 15 * 60 * 1000));/**请求参数设置完毕,以下对请求参数进行签名并生成html表单,将表单写入浏览器跳转打开银联页面**/Map<String, String> submitFromData = AcpService.sign(requestData,DemoBase.encoding); //报文中certId,signature的值是在signData方法中获取并自动赋值的,只要证书配置正确即可。String requestFrontUrl = SDKConfig.getConfig().getFrontRequestUrl(); //获取请求银联的前台地址:对应属性文件acp_sdk.properties文件中的acpsdk.frontTransUrlString html = AcpService.createAutoFormHtml(requestFrontUrl, submitFromData,DemoBase.encoding); //生成自动跳转的Html表单//LogUtil.writeLog("打印请求HTML,此为请求报文,为联调排查问题的依据:"+html);return html;}/*** 查询订单状态* @param orderId 订单号* @param txnTime 订单发送时间* @return*/public static Map<String, String> query(String orderId,String txnTime){Map<String, String> data = new HashMap<String, String>();/***银联全渠道系统,产品参数,除了encoding自行选择外其他不需修改***/data.put("version", "5.1.0"); //版本号data.put("encoding", DemoBase.encoding); //字符集编码 可以使用UTF-8,GBK两种方式data.put("signMethod", SDKConfig.getConfig().getSignMethod()); //签名方法data.put("txnType", "00"); //交易类型 00-默认data.put("txnSubType", "00"); //交易子类型 默认00data.put("bizType", "000201"); //业务类型 B2C网关支付,手机wap支付/***商户接入参数***/data.put("merId", UnionpayConfig.MER_ID); //商户号码,请改成自己申请的商户号或者open上注册得来的777商户号测试data.put("accessType", "0"); //接入类型,商户接入固定填0,不需修改/***要调通交易以下字段必须修改***/data.put("orderId", orderId); //****商户订单号,每次发交易测试需修改为被查询的交易的订单号data.put("txnTime", txnTime); //****订单发送时间,每次发交易测试需修改为被查询的交易的订单发送时间/**请求参数设置完毕,以下对请求参数进行签名并发送http post请求,接收同步应答报文------------->**/Map<String, String> reqData = AcpService.sign(data,DemoBase.encoding);//报文中certId,signature的值是在signData方法中获取并自动赋值的,只要证书配置正确即可。String url = SDKConfig.getConfig().getSingleQueryUrl();// 交易请求url从配置文件读取对应属性文件acp_sdk.properties中的 acpsdk.singleQueryUrl//这里调用signData之后,调用submitUrl之前不能对submitFromData中的键值对做任何修改,如果修改会导致验签不通过Map<String, String> rspData = AcpService.post(reqData,url,DemoBase.encoding);return rspData;}/*** 订单退款* @param refundOrderId (退款单号)* @param txnAmt 退款金额(分)* @param origQryId 原消费交易返回的的queryId* @return*/public static Map<String, String> refund(String refundOrderId,String txnAmt,String origQryId){String txnTime = DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(LocalDateTime.now());Map<String, String> data = new HashMap<String, String>();/***银联全渠道系统,产品参数,除了encoding自行选择外其他不需修改***/data.put("version","5.1.0"); //版本号data.put("encoding", DemoBase.encoding); //字符集编码 可以使用UTF-8,GBK两种方式data.put("signMethod", SDKConfig.getConfig().getSignMethod()); //签名方法data.put("txnType", "04"); //交易类型 04-退货 data.put("txnSubType", "00"); //交易子类型 默认00 data.put("bizType", "000201"); //业务类型 B2C网关支付,手机wap支付 data.put("channelType", "07"); //渠道类型,07-PC,08-手机 /***商户接入参数***/data.put("merId", UnionpayConfig.MER_ID); //商户号码,请改成自己申请的商户号或者open上注册得来的777商户号测试data.put("accessType", "0"); //接入类型,商户接入固定填0,不需修改 data.put("orderId",refundOrderId); //商户订单号,8-40位数字字母,不能含“-”或“_”,可以自行定制规则,重新产生,不同于原消费 data.put("txnTime", txnTime); //订单发送时间,格式为YYYYMMDDhhmmss,必须取当前时间,否则会报txnTime无效 data.put("currencyCode", "156"); //交易币种(境内商户一般是156 人民币) data.put("txnAmt", txnAmt); //****退货金额,单位分,不要带小数点。退货金额小于等于原消费金额,当小于的时候可以多次退货至退货累计金额等于原消费金额 //data.put("reqReserved", "透传信息"); //请求方保留域,如需使用请启用即可;透传字段(可以实现商户自定义参数的追踪)本交易的后台通知,对本交易的交易状态查询交易、对账文件中均会原样返回,商户可以按需上传,长度为1-1024个字节。出现&={}[]符号时可能导致查询接口应答报文解析失败,建议尽量只传字母数字并使用|分割,或者可以最外层做一次base64编码(base64编码之后出现的等号不会导致解析失败可以不用管)。 data.put("backUrl", UnionpayConfig.BACK_URL); //后台通知地址,后台通知参数详见open.unionpay.com帮助中心 下载 产品接口规范 网关支付产品接口规范 退货交易 商户通知,其他说明同消费交易的后台通知/***要调通交易以下字段必须修改***/data.put("origQryId", origQryId); //****原消费交易返回的的queryId,可以从消费交易后台通知接口中或者交易状态查询接口中获取/**请求参数设置完毕,以下对请求参数进行签名并发送http post请求,接收同步应答报文------------->**/Map<String, String> reqData = AcpService.sign(data,DemoBase.encoding);//报文中certId,signature的值是在signData方法中获取并自动赋值的,只要证书配置正确即可。String url = SDKConfig.getConfig().getBackRequestUrl();//交易请求url从配置文件读取对应属性文件acp_sdk.properties中的 acpsdk.backTransUrlMap<String, String> rspData = AcpService.post(reqData,url,DemoBase.encoding);//这里调用signData之后,调用submitUrl之前不能对submitFromData中的键值对做任何修改,如果修改会导致验签不通过return rspData;}}
我们把验证签名逻辑放在controller层,验证签名成功后再根据返回码判断订单的状态。官方DEMO里的注释已经很详细了,就不做过多的介绍了。
@RestController
@RequestMapping("/union")
public class UnionpayController {/*** 支付* @param orderNumber 商户订单号* @param txnAmt 金额(分)* @return* @throws Exception*/@PostMapping("/orderpay")public ResultBO<Object> pay(@RequestParam String orderNumber,@RequestParam Integer txnAmt,@RequestParam String goodsName) throws Exception{String payhtml = UnionPayClient.pay(orderNumber, txnAmt.toString(),goodsName);//生成自动跳转的form表单,直接返给前端,让前端做页面的跳转return ResultTool.success(payhtml);}/*** 订单状态查询* @param orderNumber* @param txnTime* @return* @throws Exception*/@GetMapping("/orderquery")public ResultBO<Object> query(@RequestParam String orderNumber,@RequestParam String txnTime)throws Exception{//参数限制逻辑Map<String, String> rspData = UnionPayClient.query(orderNumber, txnTime);//返回参数处理if(!rspData.isEmpty()){//验证签名if(AcpService.validate(rspData, DemoBase.encoding)){LogUtil.writeLog("验证签名成功");if("00".equals(rspData.get("respCode"))){//如果查询交易成功//处理被查询交易的应答码逻辑String origRespCode = rspData.get("origRespCode");if("00".equals(origRespCode)){System.out.println("交易成功了!!!!!!!!");//交易成功,更新商户订单状态//数据库修改成功后告诉前端,用户支付成功return ResultTool.success();}else if("03".equals(origRespCode) ||"04".equals(origRespCode) ||"05".equals(origRespCode)){//需再次发起交易状态查询交易 }else{//其他应答码为失败请排查原因}}else{//查询交易本身失败,或者未查到原交易,检查查询交易报文要素}}else{LogUtil.writeErrorLog("验证签名失败");//检查验证签名失败的原因}}else{//未返回正确的http状态LogUtil.writeErrorLog("未获取到返回报文或返回http状态码非200");}return ResultTool.success();}@PostMapping("/orderrefund")public ResultBO<Object> refund(@RequestParam String refundOrderId,@RequestParam String txnAmt,@RequestParam String queryId){Map<String, String> rspData = UnionPayClient.refund(refundOrderId, txnAmt, queryId);/**对应答码的处理,请根据您的业务逻辑来编写程序,以下应答码处理逻辑仅供参考------------->**///应答码规范参考open.unionpay.com帮助中心 下载 产品接口规范 《平台接入接口规范-第5部分-附录》if(!rspData.isEmpty()){if(AcpService.validate(rspData, DemoBase.encoding)){LogUtil.writeLog("验证签名成功");String respCode = rspData.get("respCode");if("00".equals(respCode)){//交易已受理,等待接收后台通知更新订单状态,也可以主动发起 查询交易确定交易状态。return ResultTool.success();}else if("03".equals(respCode)|| "04".equals(respCode)||"05".equals(respCode)){//后续需发起交易状态查询交易确定交易状态}else{//其他应答码为失败请排查原因}}else{LogUtil.writeErrorLog("验证签名失败");}}else{//未返回正确的http状态LogUtil.writeErrorLog("未获取到返回报文或返回http状态码非200");}return ResultTool.success();}}
银联支付的支付方式和支付宝支付差不多,都是返回自动提交的form表单跳转至自己官方页面进行支付。微信支付因为没有官方页面,所以只能扫码了。但是三者的异步通知还是基本一样的。
@RestController
@RequestMapping("/union")
public class NotifyUrlController {/*** 后台异步通知路径* @param req* @return*/@PostMapping("/notifyurl")public ResponseEntity<Object> back(HttpServletRequest req){LogUtil.writeLog("BackRcvResponse接收后台通知开始");String encoding = req.getParameter(SDKConstants.param_encoding);// 获取银联通知服务器发送的后台通知参数Map<String, String> reqParam = getAllRequestParam(req);Map<String, String> valideData = null;if (null != reqParam && !reqParam.isEmpty()) {Iterator<Entry<String, String>> it = reqParam.entrySet().iterator();valideData = new HashMap<String, String>(reqParam.size());while (it.hasNext()) {Entry<String, String> e = it.next();String key = (String) e.getKey();String value = (String) e.getValue();valideData.put(key, value);}}//重要!验证签名前不要修改reqParam中的键值对的内容,否则会验签不过if (!AcpService.validate(valideData, encoding)) {LogUtil.writeLog("验证签名结果[失败].");//验签失败,需解决验签问题} else {LogUtil.writeLog("验证签名结果[成功].");//【注:为了安全验签成功才应该写商户的成功处理逻辑】交易成功,更新商户订单状态String orderId =valideData.get("orderId"); //获取后台通知的数据,其他字段也可用类似方式获取String respCode = valideData.get("respCode");System.out.println(orderId+respCode);}LogUtil.writeLog("BackRcvResponse接收后台通知结束");//返回给银联服务器http 200 状态码return new ResponseEntity<Object>(HttpStatus.OK);}/*** 获取参数集合* @param request* @return*/public static Map<String, String> getAllRequestParam(final HttpServletRequest request) {Map<String, String> res = new HashMap<String, String>();Enumeration<?> temp = request.getParameterNames();if (null != temp) {while (temp.hasMoreElements()) {String en = (String) temp.nextElement();String value = request.getParameter(en);res.put(en, value);// 在报文上送时,如果字段的值为空,则不上送<下面的处理为在获取所有参数数据时,判断若值为空,则删除这个字段>if (res.get(en) == null || "".equals(res.get(en))) {// System.out.println("======为空的字段名===="+en);res.remove(en);}}}return res;}}
以上所有代码,基本都是官方DEMO里的代码,我只是进行了改动和再次封装,使其更适用于springboot前后端分离的开发模式。
写在后面
其实银联支付也不算是太难,和支付宝支付有异曲同工之妙。微信、支付宝、银联,这三个支付,整体流程不会相差太多,多看看官方文档还是能解决问题的。但是有些问题确实是有点棘手,尤其是微信支付。
详细代码:https://gitee.com/waynelee/unionpay