Android之支付宝设计与开发

来源:互联网 发布:北京房产资讯软件 编辑:程序博客网 时间:2024/06/10 08:06

背景 

     在移动支付领域,支付宝支付占用巨大份额,根据艾瑞咨询公布的报告数据:2014Q3,支付宝斩获了82.6%的市场份额,在移动支付的霸主地位越来越稳固。财付通支付的发力点在微信支付和手Q支付,在移动支付格局中取得了10.0%的市场份额,排名第二。 


     支付宝在移动支付领域的统治地位,使得我们有必要梳理支付宝移动开发流程。本文写作的目的就是梳理支付流程,从架构层面讲述如何在移动应用中嵌入支付宝支付功能,以及指出哪些地方存在开发陷阱。 

准备 

     首先,支付宝SDK下载主页的地址是:https://b.alipay.com/order/productDetail.htm?productId=2013080604609654&tabId=4#ps-tabinfo-hash。这个地址隐藏很深,所以这里有必要指出。 


     按照说明,首先需要申请支付宝支付账号。这方面根据网站说明进行申请即可。一般需要2周左右的时间批准下来。 


申请成功后账号信息包括 合作者身份ID partner, 卖家支付宝账号 seller_id,以及私钥 privateKey等。这三项将用于开发过程。 


     在官网上下载移动支付集成开发包。解压后, 发现其下包括三个文件夹(在英文Mac系统下文件名显示为乱码): 
“商户接入支付宝收银台界面展示标准”:讲的是如何使用支付宝Logo。
“支付宝钱包支付接口开发包2.0标准版”:用于支付,包括客户端和服务器端开发。
“即时到账批量退款有密接口refund_fastpay_by_platform_pwd”:用于到账及批量退款,只需要服务器端操作处理。
后两个文件夹,都包括4方面内容:接口文档,接入与使用规则,demo代码,以及版本更新说明。

架构设计 

首先,对于一个实际的App应用而言,可能会包括多种支付方式,因此可以采用设计模式中的策略Strategy模式来设计支付功能模块,支付宝支付作为其中的一个策略,pay方法是支付算法。 
如果除了支付方式payment method变化,订单order也可能会有不同的形式,如格式可能不同,有些支持可退款,有的不允许退款等,在这种多维度可变的情况下,支付模块的架构可以基于桥接模式。
其次,可以把支付宝支付的各个操作步骤,比如获取订单号,生成订单数据,进行支付,获取支付结果,处理异常等操作,根据状态进行划分。这样采用状态模式,提供设计的灵活性和扩展性。另外也可以设计状态机进行统一的状态切换管理。下面为参考代码:

[html] view plain copy
 print?
  1. public class PayStateMachine {  
  2.     /* all possible state of payment */  
  3.     public enum PayState { PAY_INIT, PAY_GOT_CONTEXT, PAY_UPDATED_ORDER, PAY_APPLIED_  
  4.       ID, PAY_ORDER_CREATED, PAY_SUCCEED, ERROR_OCCURRED}  
  5.            
  6.            
  7.     /* errors may occurred during payment */  
  8.     public enum PayError {  
  9.         PAY_GET_CONTEXT_FAIL, PAY_UPDATE_ORDER_FAIL, PAY_APPLY_ID_FAIL, PAY_FAIL  
  10.     }  
  11.            
  12.            
  13.     private static PayStateMachine instance;  
  14.     private PayState state;  
  15.     private IOrder order;  
  16.     private IPayment payment;  
  17.            
  18.            
  19.     private PayStateMachine() {  
  20.     }  
  21.            
  22.            
  23.     public static PayStateMachine getInstance() {  
  24.         if (instance == null) {  
  25.             instance = new PayStateMachine();  
  26.         }  
  27.            
  28.            
  29.         return instance;  
  30.     }  
  31.            
  32.            
  33.     public void initPayment(IOrder order, IPayment payment) {  
  34.         this.order = order;  
  35.         this.payment = payment;  
  36.         this.state = PayState.PAY_INIT;  
  37.     }  
  38.            
  39.            
  40.     public void startPay() {  
  41.         changeState(PayState.PAY_INIT);  
  42.     }  
  43.            
  44.            
  45.     public void changeState(PayState state) {  
  46.         onStateChanged(this.state, state);  
  47.     }  
  48.            
  49.            
  50.     public void reportError(PayError error, String detail) {  
  51.         LogUtil.printPayLog("the error id is:" + error + " " + detail);  
  52.         changeState(PayState.ERROR_OCCURRED);  
  53.     }  
  54.            
  55.            
  56.     private void onStateChanged(PayState oldState, PayState newState) {  
  57.         LogUtil.printPayLog("oid state:" + oldState + " new state:" + newState);  
  58.         this.state = newState;  
  59.            
  60.         handlePayStateChange();  
  61.     }  
  62.            
  63.            
  64.     private void handlePayStateChange() {  
  65.         if (this.order == null || this.payment == null) {  
  66.             LogUtil.printPayLog("Have not initiated payment");  
  67.             return;  
  68.         }  
  69.            
  70.            
  71.         switch (this.state) {  
  72.             case PAY_INIT:  
  73.                 order.getPayContext();  
  74.                 break;  
  75.             case PAY_GOT_CONTEXT:  
  76.                 order.createOrder();  
  77.                 break;  
  78.             case PAY_UPDATED_ORDER:  
  79.             case PAY_APPLIED_ID:  
  80.             case PAY_ORDER_CREATED:  
  81.                 payment.pay(order);  
  82.                 break;  
  83.            
  84.            
  85.             case PAY_SUCCEED:  
  86.             case ERROR_OCCURRED:  
  87.                 finishProcess();  
  88.                 break;  
  89.             default:  
  90.                 LogUtil.printPayLog("state is not correct!");  
  91.                 finishProcess();  
  92.          }  
  93.     }  
  94.            
  95.            
  96.     private void finishProcess() {  
  97.         this.order = null;  
  98.         this.payment = null;  
  99.         this.state = PayState.PAY_INIT;  
  100.     }  
  101. }  

最后,订单类层次可以参考模板模式来设计,例如抽象基类负责定义订单的操作框架和流程,具体订单数据的生成延迟到子类中实现。
具体实现参考附件源码。

支付流程

本文针对Android版进行讲解主要的支付流程。IOS版流程类似。
从操作角度看支付流程:


操作2(调用支付接口)和操作7(接口返回支付结果):App与支付宝API的交互。
操作5(异步发送支付通知):支付宝服务器与App后台的交互。
从数据流角度看支付流程:


客户端实现
本文结合操作流程和数据流程,讲述主要的实现方案。
首先假设订单数据都已经存储在OrderPayModel中。

第一步:App客户端访问应用服务器,后者生成订单编号并返回客户端。

[html] view plain copy
 print?
  1. private void getOrderIdRequest() {  
  2.   JSONObject ob = new JSONObject();  
  3.   ob.put("amount", orderPayModel.getOrderPriceTotal());  
  4.                 
  5.   ob.put("productDescription", orderPayModel.getOrderName());  
  6.                 
  7.                 
  8.   ob.put("userId", orderPayModel.getUserId());  
  9.   ob.put("barCoupon", orderPayModel.getOrderId());  
  10.   ob.put("barId", orderPayModel.getBarId());  
  11.   ob.put("count", orderPayModel.getOrderNums());  
  12.   LogUtil.printPayLog("get order id request data:"  
  13.     + orderPayModel.toString());  
  14.                 
  15.                 
  16.   HttpRequestFactory.getInstance().doPostRequest(Urls.ALI_PAY_APPLY, ob,  
  17.     new AsyncHttpResponseHandler() {  
  18.                 
  19.                 
  20.      @Override  
  21.      public void onSuccess(String content) {  
  22.       super.onSuccess(content);  
  23.                 
  24.                 
  25.       LogUtil.printPayLog("get order id request is handled");  
  26.                 
  27.                 
  28.       PayNewOrderModel rm = new PayNewOrderModel();  
  29.       rm = JSON.parseObject(content, PayNewOrderModel.class);  
  30.                 
  31.                 
  32.       if (rm.getCode() != null  
  33.         && "200".equalsIgnoreCase(rm.getCode())) {  
  34.        tradeNo = rm.getResult().getTrade_no();  
  35.        LogUtil.printPayLog("succeed to get order id:"  
  36.          + tradeNo);  
  37.                 
  38.                 
  39.        orderStr = generateOrder();  
  40.        PayStateMachine.getInstance().changeState(  
  41.          PayState.PAY_APPLIED_ID);  
  42.                 
  43.                 
  44.       } else {  
  45.        PayStateMachine.getInstance().reportError(  
  46.          PayError.PAY_APPLY_ID_FAIL,  
  47.          "code is not right");  
  48.       }  
  49.      }  
  50.                 
  51.                 
  52.      @Override  
  53.      public void onFailure(Throwable error, String content) {  
  54.       PayStateMachine.getInstance().reportError(  
  55.         PayError.PAY_APPLY_ID_FAIL,  
  56.         "failed to get order id");  
  57.                 
  58.                 
  59.      };  
  60.                 
  61.                 
  62.      @Override  
  63.      public void onFinish() {  
  64.       LogUtil.LogDebug("Payment", "on get order id finish",  
  65.         null);  
  66.                 
  67.                 
  68.      };  
  69.     });  
  70.  }  

第二步:组装订单数据,包括以下几个子步骤:

创建订单数据。

[html] view plain copy
 print?
  1. private String getOrderInfo(String partner, String seller) {  
  2.   String orderInfo;  
  3.                 
  4.    // 合作者身份ID  
  5.    orderInfo = "partner=" + "\"" + partner + "\"";  
  6.                 
  7.                 
  8.    // 卖家支付宝账号  
  9.    orderInfo += "&seller_id=" + "\"" + seller + "\"";  
  10.                 
  11.                 
  12.    // 商户网站唯一订单号  
  13.    orderInfo += "&out_trade_no=" + "\"" + tradeNo + "\"";  
  14.                 
  15.                 
  16.    // 商品名称  
  17.    orderInfo += "&subject=" + "\"" + orderName + "\"";  
  18.                 
  19.                 
  20.    // 商品详情  
  21.    orderInfo += "&body=" + "\"" + orderDetail + "\"";  
  22.                 
  23.                 
  24.    // 商品金额  
  25.    orderInfo += "&total_fee=" + "\"" + totalPrice + "\"";  
  26.    // orderInfo += "&total_fee=" + "\"" + "0.01" + "\"";  
  27.                 
  28.                 
  29.    // 服务器异步通知页面路径  
  30.    orderInfo += "¬ify_url=" + "\"" + Urls.ALI_PAY_NOTIFY + "\"";  
  31.                 
  32.                 
  33.    // 接口名称, 固定值  
  34.    orderInfo += "&service=\"mobile.securitypay.pay\"";  
  35.                 
  36.                 
  37.    // 支付类型, 固定值  
  38.    orderInfo += "&payment_type=\"1\"";  
  39.                 
  40.                 
  41.    // 参数编码, 固定值  
  42.    orderInfo += "&_input_charset=\"utf-8\"";  
  43.                 
  44.                 
  45.    // 设置未付款交易的超时时间  
  46.    // 默认30分钟,一旦超时,该笔交易就会自动被关闭。  
  47.    // 取值范围:1m~15d。  
  48.    // m-分钟,h-小时,d-天,1c-当天(无论交易何时创建,都在0点关闭)。  
  49.    // 该参数数值不接受小数点,如1.5h,可转换为90m。  
  50.    orderInfo += "&it_b_pay=\"30m\"";  
  51.                 
  52.                 
  53.    // 支付宝处理完请求后,当前页面跳转到商户指定页面的路径.  
  54.    // orderInfo += "&return_url=\"m.alipay.com\"";  
  55.    // Bill: this item must not be empty! though the api demo said it  
  56.    // can be.  
  57.    orderInfo += "&return_url=\"m.alipay.com\"";  
  58.                 
  59.                 
  60.    // 调用银行卡支付,需配置此参数,参与签名, 固定值  
  61.    // orderInfo += "&paymethod=\"expressGateway\"";  
  62.   }  
  63.                 
  64.                 
  65.   return orderInfo;  
  66.  }  

对订单做RSA签名:  demo代码中提供SingUtils类实现该功能,即SignUtils.sign(content, RSA_PRIVATE);
对签名做 URL编码:  调用java类库接口,即URLEncoder.encode来实现。
将订单数据和签名信息组合,生成符合支付宝参数规范的数据:  

[html] view plain copy
 print?
  1. final String payInfo = orderInfo + "&sign=\"" + sign + "\"&" + getSignType();  

第三步:在子线程里调用PayTask的pay接口,将请求数据发送出去

[html] view plain copy
 print?
  1. PayTask alipay = new PayTask(PayDemoActivity.this);  
  2. // 调用支付接口,获取支付结果  
  3. String result = alipay.pay(payInfo);  

第四步:收到支付处理结果的消息。支付结果的状态码的意义如下:

    值为“9000”,代表支付成功;
    值为“8000”,代表等待支付结果确认,这可能由于系统原因或者渠道支付原因。支付的最终结果需要由服务器端的异步通知为准(支付宝将向)。
    值为其他,代表失败。客户端需要提示用户。
注意事项:
本文特别需要指出的是,也就是最容易出问题的就是订单数据的生成。在demo代码的 PayDemoActivity类中,定义了getOrderInfo方法。 其中“orderInfo += "&return_url=\"m.alipay.com\"”;”在该demo代码的注释中,虽然说是可以为空,但实际情况,如果为空,将导致支付失败。而且凭借失败状态码,难以识别具体原因。
支付结果,除了支付宝服务器发通知到客户端外,也会异步通知应用服务器。考虑到安全性,客户端可以根据支付宝服务器的通知,进行商业逻辑的处理,比如订单更新等,但是支付的数据入库,需要由应用服务器端根据异步通知进行操作。
服务端实现 
服务端基本操作包括:获取支付宝账号信息(为了安全,该信息放置在服务器,而不是客户端),创建订单,支付结果异步回调,申请退款等基本操作外。另外也可能包括:更新订单(对于支持订单可修改的应用),验证消费码,查询订单记录,删除订单等操作。
本文介绍基于Java平台的服务器方案。目前比较流行的框架组合是SpingMVC+Mybatis+Mysql。
订单的创建。当用户下订单时,如果是新订单(请求的数据没有包括订单编号信息),需要创建,并返回订单号给客户端。订单类示例:
[html] view plain copy
 print?
  1. public class PayOrder {  
  2.               
  3.  public String tradeNo;      //随机编号  
  4.  public String amount;      //付款金额  
  5.  public String status;      //操作状态  
  6.  public String statusCode;     //操作状态代码,0-未支付,10-已支付,4000-退款中,5000-已退款,6000-付款失败,6001-取消付款,7000-已消费  
  7.  public String orderNo;      //支付宝流水号  
  8.  public String productDescription;   //商品名称  
  9.  public String payNo;      //消费码  
  10.  public String isRefund;      //是否申请退款  
  11.  public String createTime;     //创建时间  
  12.  public String modifyTime;     //修改时间  
  13.  public String userId;      //用户id  
  14.  public Integer id;       //主键  
  15.  public String pId;       //商品id  
  16.  public int buyNumber;       //存储购买数量  
  17.  public int vendorId;       //商家ID  
  18. }  

tradeNo代码订单编号。payNo代码消费编号(消费码)。orderNo是支付宝服务器端生成的订单号。
[html] view plain copy
 print?
  1. @RequestMapping(value = "/payorder")  
  2. @ResponseBody  
  3. public Map<String, Object> pay(HttpServletRequest request, HttpServletResponse response) {  
  4.  Map<String, Object> map = JsonPUtil.pToMap(request);  
  5.  Map<String, Object> msgMap = new HashMap<String, Object>();  
  6.  if (!map.isEmpty()) {  
  7.   try {  
  8.    log.info("执行购买前确认操作:" + map);  
  9.    String now = String.valueOf(System.currentTimeMillis());  
  10.    String trade_no = map.get("barId").toString() + "-" + map.get("barCoupon").toString() + "-" + map.get("count").toString() + "-" + now.substring(now.length() - 6);  
  11.         
  12.    PayOrder alipay = new PayOrder();  
  13.    alipay.setAmount(map.get("amount").toString());  
  14.    alipay.setTradeNo(trade_no);  
  15.    alipay.setProductDescription(map.get("productDescription").toString());  
  16.    alipay.setCreateTime(now);  
  17.    alipay.setStatus("待支付");  
  18.    alipay.setStatusCode("0");  
  19.    alipay.setExtInt1(Integer.parseInt(map.get("count").toString()));  
  20.    alipay.setUserId(map.get("userId").toString());  
  21.    alipay.setpId(map.get("barCoupon").toString());   
  22.    alipay.setExtInt2(Integer.parseInt(map.get("barId").toString()));  
  23.         
  24.    int flag = alipayServiceImpl.pay(alipay);  
  25.    log.info("确认操作执行结果:" + flag);  
  26.         
  27.    Map<String, Object> m = new HashMap<String, Object>();  
  28.    m.put("trade_no", trade_no);  
  29.    m.put("seller", new String(Base64.encode(AlipayConfig.SELLER.getBytes())));  
  30.    m.put("partner", new String(Base64.encode(AlipayConfig.partner.getBytes())));  
  31.    m.put("privateKey", new String(Base64.encode(AlipayConfig.ios_private_key.getBytes())));  
  32.         
  33.    if (flag > 0)  
  34.     msgMap = ResponseMessageUtil.respMsg(Constance.BASE_SUCCESS_CODE, "success", m);  
  35.    else  
  36.     msgMap = ResponseMessageUtil.respMsg(Constance.BASE_SERVER_WRONG_CODE, "fail");  
  37.   } catch (Exception e) {  
  38.    e.printStackTrace();  
  39.    msgMap = ResponseMessageUtil.respMsg(Constance.BASE_SERVER_WRONG_CODE, "fail");  
  40.    log.error("购买前确认失败", e);  
  41.   }  
  42.  } else {  
  43.   log.info("请求参数不完整");  
  44.   msgMap = ResponseMessageUtil.respMsg(Constance.ILLEGAL_OPERATE, "fail");  
  45.  }  
  46.  return msgMap;  
  47. }  
支付宝服务器回调App服务器,通知支付结果。App服务器将相应的数据入库后,通知支付宝服务器"success" or "fail"。 

[html] view plain copy
 print?
  1. @RequestMapping(value = "/payOver")  
  2.  @ResponseBody  
  3.  public String payOver(HttpServletRequest request, HttpServletResponse response) {  
  4.   Map<String, String> map = JsonPUtil.buildMap(request);  
  5.   String result_str = "fail";  
  6.   if (!map.isEmpty()) {  
  7.    if (AlipayUtils.checkAlipay(map, false) > 0) {// 通过支付宝验证  
  8.     try {  
  9.      log.info("执行付款的回调函数传递参数:" + map);  
  10.      String now = String.valueOf(System.currentTimeMillis());  
  11.      String status = map.get("trade_status").toString();  
  12.      PayOrder alipay = new PayOrder();  
  13.      alipay.setTradeNo(String.valueOf(map.get("out_trade_no")));  
  14.      log.info("支付状态:" + status);  
  15.      if (Constance.ALIPAY_SUCCESS_CODE.equals(status) || Constance.ALIPAY_FINISHED_CODE.equals(status)) {// 支付成功  
  16.       List<Alipay> ali = alipayServiceImpl.search(alipay);  
  17.       if (ali.size() == 1 && (ali.get(0).getPayNo() == null || ali.get(0).getPayNo().equals(""))) {// 消息未处理  
  18.        Alipay pay = new Alipay();  
  19.        pay.setTradeNo(String.valueOf(map.get("out_trade_no")));  
  20.        pay.setStatus("已支付");  
  21.        pay.setStatusCode("10");  
  22.        pay.setIsRefund("0");  
  23.        pay.setModifyTime(String.valueOf(System.currentTimeMillis()));  
  24.        pay.setPayNo(new String(Base64.encode(now.substring(now.length() - 10).getBytes())));  
  25.        pay.setOrderNo(String.valueOf(map.get("trade_no")));  
  26.        int flag = alipayServiceImpl.payOver(pay);  
  27.        log.info("用户付款成功" + map);  
  28.        if (flag > 0)  
  29.         result_str = "success";  
  30.       }  
  31.      } else {  
  32.       return result_str;  
  33.      }  
  34.     } catch (Exception e) {  
  35.      e.printStackTrace();  
  36.      log.error("回调函数获取参数失败", e);  
  37.      return result_str;  
  38.     }  
  39.    }  
  40.   }  
  41.   return result_str;  
  42.  }  
0 0
原创粉丝点击