Android 支付实践(三)之银联支付功能(客户端 + 服务端)

3,156 阅读11分钟

前言:由于支付宝和微信支付都须要提供这个那个的认证材料,对于个人开发者想尝试,确实有不少麻烦,今天介绍的银联支付,对于个人开发者,可以说是福音了。来自chentravelling的实践银联支付,chentravelling对应的blog地址为:http://blog.csdn.net/chentravelling。点击【阅读原文】,可看对应原文。

一.功能描述

因为是自己开发了一个app应用,没资格去申请微信支付和支付宝支付,于是就采用了银联支付功能,银联支付分为了两种环境:测试环境和生产环境,一般前期开发的时候都是使用测试环境,数据都是测试数据,不会发生真实交易。第一次做Android项目+IDE为Android Studio+第一次集成支付功能,所以个人觉得整个集成过程可能有点复杂,而且银联支付产品众多:网关支付产品、手机控件支付、手机网页支付等等,第一次看的时候真是云里雾里,不知道选哪个,不知道他们的区别,最后自己选择了手机控件支付,先试试,光看没有用。自己做了多少写多少,好记性不如烂笔头,就怕后面想记录的时候忘记了前面的过程。

二.实现过程

第一步:注册;

第二步:然后在帮助中心界面的产品分类下载里选择手机控件支付; 

第三步:下载安卓版的开发包。 

(2)下载的文件如下

2.2集成过程

个人建议可以先把服务器端的工程跑一下,这样结合代码的时候就很容易明白怎么集成到自己的工程里了。所以这一小部分内容是官方Demo的运行情况,需要修改的配置很少,但是还是有小地方需要调整一下。

(1)先试官方Demo

将这三个路径修改为测试环境证书的路径(证书在assets文件夹下),可以使用相对路径或者绝对路径都行,下面图中是绝对路径,我把assets/测试环境证书下的三个文件移动到了C盘。 

我的测试环境证书地址: 

启动tomcat过程中比较关键的一处就是 


最后打印的报文

(2)集成到自己的工程里

先讲服务器端,因为自己也才完成了这部分工作。运行了官方Demo以及相关说明文档后,整体思路其实就有了。 
首先试试配置!!!

将Demo工程lib中的jar包复制到自己工程的lib里(如果已经有jar包了,就不需要复制了) 

  • 修改acp_sdk_propertise和log4j.properties 

    主要是一些路径的修改,因为我的服务器上只有C盘,所以我必须得改,acp_sdk_properties的修改见(1)运行Demo部分)

之所以写用import的方式是为了少出现乱码的问题,见下图 

  • 第一部分 controller对应于Demo中的ACPSample_AppServer\src\com\unionpay\acp\demo,其中PayController是我自己写的Controller。 

  • 第二部分model对应于Demo中的ACPSample_AppServer\src\com\unionpay\acp\sdk 

  • 第三部分的两个java文件对应于Demo中的\ACPSample_AppServer\src\web中的两个java文件

  • web.xml配置 


    autoLoadServlet
    com.XXX.component.pay.AutoLoadServlet
    0


    autoLoadServlet
    /autoLoadServlet

ok,配置过程就算完事了,现在就该写PayController来接受请求了。

  • PayController控制器 
    其实就是Form05_6_2_AppConsume.java中的代码,因为使用是SSM架构,就改了改架构而已。

@Controller
public class PayController extends BasicController{
    @RequestMapping(value = "/pay/pay")
    @ResponseBody
    public void pay(HttpServletRequest request, HttpServletResponse response) throws UnsupportedEncodingException {
        request.setCharacterEncoding(DemoBase.encoding_UTF8);//
        response.setContentType("text/html; charset="+ DemoBase.encoding_UTF8);//这两句是我临时加的,因为出现了乱码
        Map contentData = new HashMap();
        /***银联全渠道系统,产品参数,除了encoding自行选择外其他不需修改***/
        contentData.put("version", DemoBase.version);            //版本号 全渠道默认值
        contentData.put("encoding", DemoBase.encoding_UTF8);     //字符集编码 可以使用UTF-8,GBK两种方式
        contentData.put("signMethod", "01");                    //签名方法 目前只支持01:RSA方式证书加密
        contentData.put("txnType", "01");                       //交易类型 01:消费
        contentData.put("txnSubType", "01");                    //交易子类 01:消费
        contentData.put("bizType", "000201");                   //填写000201
        contentData.put("channelType", "08");                   //渠道类型 08手机
        String merId = request.getParameter("merId");
        String txnAmt = request.getParameter("txnAmt");
        String orderId = request.getParameter("orderId");
        String txnTime = request.getParameter("txnTime");
        /***商户接入参数***/
        contentData.put("merId", merId);                        //商户号码,请改成自己申请的商户号或者open上注册得来的777商户号测试
        contentData.put("accessType", "0");                     //接入类型,商户接入填0 ,不需修改(0:直连商户, 1: 收单机构 2:平台商户)
        contentData.put("orderId", orderId);                    //商户订单号,8-40位数字字母,不能含“-”或“_”,可以自行定制规则   
        contentData.put("txnTime", txnTime);                    //订单发送时间,取系统时间,格式为YYYYMMDDhhmmss,必须取当前时间,否则会报txnTime无效
        contentData.put("accType", "01");                       //账号类型 01:银行卡02:存折03:IC卡帐号类型(卡介质)
        contentData.put("txnAmt", txnAmt);                      //交易金额 单位为分,不能带小数点
        contentData.put("currencyCode", "156");                 //境内商户固定 156 人民币
        //contentData.put("reqReserved", "透传字段");              //商户自定义保留域,交易应答时会原样返回
        //后台通知地址(需设置为外网能访问 http https均可),支付成功后银联会自动将异步通知报文post到商户上送的该地址,【支付失败的交易银联不会发送后台通知】
                //后台通知参数详见open.unionpay.com帮助中心 下载  产品接口规范  网关支付产品接口规范 消费交易 商户通知
                //注意:1.需设置为外网能访问,否则收不到通知    2.http https均可  3.收单后台通知后需要10秒内返回http200或302状态码 
                //    4.如果银联通知服务器发送通知后10秒内未收到返回状态码或者应答码非http200或302,那么银联会间隔一段时间再次发送。总共发送5次,银联后续间隔1、2、4、5 分钟后会再次通知。
                //    5.后台通知地址如果上送了带有?的参数,例如:http://abc/web?a=b&c=d 在后台通知处理程序验证签名之前需要编写逻辑将这些字段去掉再验签,否则将会验签失败
        contentData.put("backUrl", DemoBase.backUrl);//[其实还没搞明白这个地址的作用]
        /**对请求参数进行签名并发送http post请求,接收同步应答报文**/
        Map reqData = AcpService.sign(contentData,DemoBase.encoding_UTF8);           //报文中certId,signature的值是在signData方法中获取并自动赋值的,只要证书配置正确即可。
        String requestAppUrl = SDKConfig.getConfig().getAppRequestUrl();                                 //交易请求url从配置文件读取对应属性文件acp_sdk.properties中的 acpsdk.backTransUrl
        Map rspData = AcpService.post(reqData,requestAppUrl,DemoBase.encoding_UTF8);  //发送请求报文并接受同步应答(默认连接超时时间30秒,读取返回结果超时时间30秒);这里调用signData之后,调用submitUrl之前不能对submitFromData中的键值对做任何修改,如果修改会导致验签不通过
        /**对应答码的处理,请根据您的业务逻辑来编写程序,以下应答码处理逻辑仅供参考------------->**/
        //应答码规范参考open.unionpay.com帮助中心 下载  产品接口规范  《平台接入接口规范-第5部分-附录》
        if(!rspData.isEmpty()){
            if(AcpService.validate(rspData, DemoBase.encoding_UTF8)){
                LogUtil.writeLog("验证签名成功");
                String respCode = rspData.get("respCode") ;
                if(("00").equals(respCode)){
                    //成功,获取tn号
                    //String tn = resmap.get("tn");//这里应该是rspData.get("tn");
                    //TODO
                }else{
                    //其他应答码为失败请排查原因或做失败处理
                    //TODO
                }
            }else{
                LogUtil.writeErrorLog("验证签名失败");
                //TODO 检查验证签名失败的原因
            }
        }else{
            //未返回正确的http状态
            LogUtil.writeErrorLog("未获取到返回报文或返回http状态码非200");
        }
        String reqMessage = DemoBase.genHtmlResult(reqData);
        String rspMessage = DemoBase.genHtmlResult(rspData);
        try {
            response.getWriter().write("请求报文:
"+reqMessage+"
" + "应答报文:
"+rspMessage+""); //response.getOutputStream().write(("请求报文:
"+reqMessage+"
" + "应答报文:
"+rspMessage+"").getBytes("UTF-8")); } catch (UnsupportedEncodingException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
  • 运行

然后浏览器输入(get方式,在Form05_6_2_AppConsume.java中get和post方式是一样的,我的PayController因为没有指定请求方式,所以两种方式也是兼容的,而且只是为了测试采用的get,后面把客户端搭好,就用post方式) 
:
http://123.XX.XXX.127:8080/intveh/pay/pay?merId=777290058110048&txnTime=20160505092851&orderId=20160505092851&txnAmt=5 


效果和官方Demo一样: 

目前就把服务器端的tn获取搭好了.

2016年5月6日补充

服务器端还应该有“后台通知接收处理”部分。可参见Demo中ACPSample_AppServer\src\com\unionpay\acp\demo\BackRcvResponse.java。这部分的功能很重要:见下图所示(银联支付流程图)

见下图所示(银联支付流程图)

第七步是银联后台将支付结果返回给我们的服务器端,我们将根据返回结果更新订单的状态,请注意:虽然第八步是将客户端的支付控件也会支付结果,但是能作为订单支付结果的只有后台的通知,官方文档中有一处说的有歧义,客服是这样说的: 

关于客户端支付结果+验签,在下一篇文档中说明。

3.3后台通知接收处理配置

(1)修改DemoBase.java中的backUrl

请注意:这个backUrl必须填写真实ip地址,回路地址不行、localhost不行,银联后台返回通知必须能post到你的backUrl。

2)后台通知处理部分

/**
* 后台通知处理
* @param sign
* @param request
* @param response
*/
@RequestMapping(value = "/pay/backRcvResponse")
@ResponseBody
public void backRcvResponse(HttpServletRequest request, HttpServletResponse response) {
        System.out.println("后台通知验签开始");
        //return AcpService.validateAppResponse(sign, DemoBase.encoding_UTF8);
        //System.out.println("验签开始");
        String encoding = request.getParameter(SDKConstants.param_encoding);
        // 获取银联通知服务器发送的后台通知参数
        Map reqParam = Tool.getAllRequestParam(request);
        LogUtil.printRequestLog(reqParam);
        Map valideData = null;
        try
        {
            if (null != reqParam && !reqParam.isEmpty()) {
                Iterator> it = reqParam.entrySet().iterator();
                valideData = new HashMap(reqParam.size());
                while (it.hasNext()) {
                    Entry e = it.next();
                    String key = (String) e.getKey();
                    String value = (String) e.getValue();
                    value = new String(value.getBytes(encoding), encoding);
                    valideData.put(key, value);
                }
            }
            //重要!验证签名前不要修改reqParam中的键值对的内容,否则会验签不过
            if (!AcpService.validate(valideData, encoding)) {
                LogUtil.writeLog("验证签名结果[失败].");
                //验签失败,需解决验签问题
            } else {
                LogUtil.writeLog("验证签名结果[成功].");
                //【注:为了安全验签成功才应该写商户的成功处理逻辑】交易成功,更新商户订单状态
                //valideData里封装了很多数据,可参考官方文档,做相应后续处理
                }
            }
            LogUtil.writeLog("BackRcvResponse接收后台通知结束");
            //返回给银联服务器http 200  状态码
            response.getWriter().print("ok");
        }
        catch(Exception e){}
}

ok,backUrl配置正确,那么进行支付后,商户后台就会得到银联后台的支付通知,如下图: 

这部分工作不能少,一定要记住后台通知处理的作用:真正作为接收订单支付结果的地方。

以上是服务端的过程,接下来看客户端的过程

一.功能描述

银联支付流程如下所示: 

上一篇尝试了tn的获取,本篇将跑通整个流程。 

二.实现部分

先说一下我的IDE是as(Android Studio)+win7 64位 

2.1配置,依旧是配置

按照官方说明文档,就可以完成,我在这里贴上结构图,因为as的特殊性。

  • 因为我的工程资源有点多了,一张图截不下,所以我只给出文件夹的结构图。 

  • 拷贝upmp_android/sdkPro/jar/data.bin到工程的assets文件夹中;

  • 拷贝upmp_android/sdkPro/jar/xxx/libentryexpro.so和upmp_android/sdkPro/jar/xxx/libuptsmaddon.so到工程的armeabi-v7a文件夹中(官方说v8 v7 abi都行)

    拷贝upmp_android/sdkPro/UPPayAssistEx.jar到工程的libs/目录下;

    拷贝upmp_android/sdkPro/jar/UPPayPluginExPro.jar到工程的libs/目录下;

  • AndroidManifest.xml添加配置

  • 
    
    
    
    
    
    
    
    
    
    
    
    
    
    

    2.2.1调用支付接口

    import com.unionpay.UPPayAssistEx;
    //在调用支付控件的代码按以下方式调用支付控件
    //比如onclick或者handler等等...
    /*参数说明:
    activity —— 用于启动支付控件的活动对象
    spId —— 保留使用,这里输入null
    sysProvider —— 保留使用,这里输入null
    orderInfo —— 订单信息为交易流水号,即TN,为商户后台从银联后台获取。 
    mode —— 银联后台环境标识,“00”将在银联正式环境发起交易,“01”将在银联测试环境发起交易
    返回值:
    UPPayAssistEx.PLUGIN_VALID —— 该终端已经安装控件,并启动控件
    UPPayAssistEx.PLUGIN_NOT_FOUND — 手机终端尚未安装支付控件,需要先安装支付控件
    */
    String serverMode = "01";
    UPPayAssistEx.startPay (activity, null, null, tn, serverMode);


    2.2.2接收支付结果

    支付完成后,获取支付控件支付结果,并添加相应处理逻辑,只需实现调用Activity中的onActivityResult()方法即可,支付成功时会返回商户客户端支付结果的签名信息。

    • 官方Demo是这样的: 
      其中有一句:此处的verify,商户需送去商户后台做验签 ,请注意并不是在这里向后台请求验签,因为后台也会接收到银联后台的支付结果通知,验签是在商户后台做.

    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (data == null) {
            return;
        }
        String str = data.getExtras().getString("pay_result");
        if (str.equalsIgnoreCase(R_SUCCESS)) {
            // 支付成功后,extra中如果存在result_data,取出校验
            // result_data结构见c)result_data参数说明
            if (data.hasExtra("result_data")) {
                String sign = data.getExtras().getString("result_data");
                // 验签证书同后台验签证书
                // 此处的verify,商户需送去商户后台做验签
                if (verify(sign)) {
                    //验证通过后,显示支付结果
                    showResultDialog(" 支付成功! ");
                } else {
                    // 验证不通过后的处理
                    // 建议通过商户后台查询支付结果
                }
            } else {
                // 未收到签名信息
                // 建议通过商户后台查询支付结果
            }
        } else if (str.equalsIgnoreCase(R_FAIL)) {
            showResultDialog(" 支付失败! ");
        } else if (str.equalsIgnoreCase(R_CANCEL)) {
            showResultDialog(" 你已取消了本次订单的支付! ");
        }
    }

    由此就基本完成了客户端的集成,当然还有一些:支付失败的处理、取消订单的处理,以及退货、退款等等处理,等有时间再完善了。

    本想截图的!!手机暂时出现了无法截图的毛病...

    第一时间获得博客更新提醒,以及更多android干货,源码分析,欢迎关注我的微信公众号,扫一扫下方二维码或者长按识别二维码,即可关注。