带你走进Android微信支付和支付宝支付服务端操作放到客户端的故事

来源:互联网 发布:页游平台源码 编辑:程序博客网 时间:2024/06/04 19:16

*本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布

前言
现在的APP大部分需要接入支付功能,而支付的主流就是微信支付和支付宝支付,网上关于微信支付和支付支付资料很多,但是这些资料随着官方的变动可能变得毫无用处,所以我建议直接看官方文档,微信开放平台和支付宝开放平台。当然,一般官方不会闲的蛋疼随便改,大的改动周期在2年左右,小的改动不会有太大影响,所以,如果你不习惯官方的文档,那么你找的资料的新鲜度很重要。因为本文主要目的是介绍服务端的一些操作放到客户端,所以正常的接入就不重复造轮子了,有兴趣的可以参考郭霖公众号推送的文章微信支付接入指南 和 支付宝支付接入指南 ,或者直接访问作者原文 。

疑问
相信很多同学看到标题就有疑问了,为什么要将服务端的一些操作放到客户端来?放到客户端不安全,官方不是不推荐吗?我也想啊,为什么非要给自己制造麻烦,但是,我一个项目(外包)遇到了这样的情况,这个项目做服务端告诉他不会集成,让我放到客户端来,当时也解释了利害关系,但是对方执意如此。在此,我只想说……(此处省略一万字),这里我笑了,不会集成?那支付后那些回调操作你怎么要做了?为什么不也放到客户端来?哦,对了,客户端不方便修改数据库数据。请原谅我的吐槽,这根本不是会不会集成的原因,这是态度问题啊,相信有很多同学遇到过类似的事情,在这里还是希望大家吐槽可以,忍忍也就过去了。好像扯远了,回归正题吧。

支付宝服务端sign签名

首先我们有必要来看一下支付宝支付的流程图,以便了解服务端该做哪些操作:
支付宝支付流程图

注意第一点app携带支付信息(正常的流程这里服务端有一个签名过程,APP需要请求服务端提供的接口传入对应的参数获取签名后的数据,本文将服务端签名过程放到客户端)调用支付接口请求支付宝客户端调起支付界面。流程图其他步骤可以参考前言中推荐的文章,里面有详细的解释,这里不再解释了。

支付宝签名支付宝签名文档,其实,支付宝的集成很简单,所以签名也很简单,对照文档一会儿就弄出来了,我们来看一看就明白了:

/**     * 获取签名     */    public static String getSign(String content, String privateKey) {        try {            PKCS8EncodedKeySpec priPKCS8 = new PKCS8EncodedKeySpec(Base64.decode(privateKey));            KeyFactory keyf = KeyFactory.getInstance("RSA");            PrivateKey priKey = keyf.generatePrivate(priPKCS8);            java.security.Signature signature = java.security.Signature.getInstance("SHA1WithRSA");            signature.initSign(priKey);            signature.update(content.getBytes("UTF-8"));            byte[] signed = signature.sign();            return Base64.encode(signed);        } catch (Exception e) {            e.printStackTrace();        }        return null;    }/**     * 创建订单信息     * @param payEntity 订单支付实体     */    private static String getOrderInfo(PayEntity payEntity){        StringBuffer orderInfo = new StringBuffer();        // 签约合作者身份ID        orderInfo.append("partner=" + "\"" + AliPayConstans.PARTNER + "\"");        // 签约卖家支付宝账号        orderInfo.append("&seller_id=" + "\"" + AliPayConstans.SELLER + "\"");        // 商户网站唯一订单号        orderInfo.append("&out_trade_no=" + "\"" + payEntity.getOut_trade_no() + "\"");        // 商品名称        orderInfo.append("&subject=" + "\"" + payEntity.getSubject() + "\"");        // 商品详情        orderInfo.append("&body=" + "\"" + payEntity.getBody() + "\"");        // 商品金额        orderInfo.append("&total_fee=" + "\"" + payEntity.getTotal_fee() + "\"");        // 服务器异步通知页面路径        orderInfo.append("&notify_url=" + "\"" + AliPayConstans.NOTIFY_URL  + "\"");        // 服务接口名称, 固定值        orderInfo.append("&service=\"mobile.securitypay.pay\"");        // 支付类型, 固定值        orderInfo.append("&payment_type=\"1\"");        // 参数编码, 固定值        orderInfo.append("&_input_charset=\"utf-8\"");        // 设置未付款交易的超时时间        // 默认30分钟,一旦超时,该笔交易就会自动被关闭。        // 取值范围:1m~15d。        // m-分钟,h-小时,d-天,1c-当天(无论交易何时创建,都在0点关闭)。        // 该参数数值不接受小数点,如1.5h,可转换为90m。        orderInfo.append("&it_b_pay=\"30m\"");        // extern_token为经过快登授权获取到的alipay_open_id,带上此参数用户将使用授权的账户进行支付        // orderInfo += "&extern_token=" + "\"" + extern_token + "\"";        // 支付宝处理完请求后,当前页面跳转到商户指定页面的路径,可空        orderInfo.append("&return_url=\"m.alipay.com\"");        // 调用银行卡支付,需配置此参数,参与签名, 固定值 (需要签约《无线银行卡快捷支付》才能使用)        // orderInfo += "&paymethod=\"expressGateway\"";        return orderInfo.toString();    }

AliPayConstans相关:

    /** 商户PID */    public static final String PARTNER = "";    /** 商户收款账号 */    public static final String SELLER = "";    /** 商户私钥,pkcs8格式 */    public static final String RSA_PRIVATE = "";    /** 支付宝公钥 */        public static final String RSA_PUBLIC = "";    /** 支付回调接口,需要服务器端支持 */    public static final String NOTIFY_URL = "";

微信服务端统一下单及签名

老规矩先看微信支付流程图,不得不说微信支付的流程还挺复杂的,这也许是大家吐槽微信支付的原因之一吧:

微信支付流程图

我们需要注意2-7之间的步骤,这是关键所在,其中会有两次签名。

1. 构建支付订单信息(第一次签名)
2. 调用统一下单接口获取预付ID(prepay_id)
3. 生成带签名的客户端支付信息(第二次签名)

1、构建支付订单信息

/**     * 微信支付,构建统一下单请求参数     */    public String genEntity() {        String nonceStr = genNonceStr();        List<NameValuePair> packageParams = new ArrayList<NameValuePair>();        // APPID        packageParams                .add(new BasicNameValuePair("appid", WeChatConstans.APP_ID));        // 商品描述        packageParams.add(new BasicNameValuePair("body", body));        // 商户ID        packageParams.add(new BasicNameValuePair("mch_id",                WeChatConstans.PARTNER_ID));        // 随机字符串        packageParams.add(new BasicNameValuePair("nonce_str", nonceStr));        // 回调接口地址        packageParams.add(new BasicNameValuePair("notify_url",                WeChatConstans.NOTIFY_URL));        // 我们的订单号        packageParams.add(new BasicNameValuePair("out_trade_no", out_trade_no));        // 提交用户端ip        packageParams.add(new BasicNameValuePair("spbill_create_ip",                getIPAddress()));        BigDecimal totalFeeBig = new BigDecimal(total_fee);        int totalFee = totalFeeBig.multiply(new BigDecimal(100)).intValue();        // 总金额,单位为 分 !        packageParams.add(new BasicNameValuePair("total_fee", String                .valueOf(totalFee)));        // 支付类型, APP        packageParams.add(new BasicNameValuePair("trade_type", "APP"));        // 生成签名        String sign = genPackageSign(packageParams);        packageParams.add(new BasicNameValuePair("sign", sign));        String xmlstring = XmlUtil.toXml(packageParams);        try {            //避免商品描述中文字符编码格式造成支付失败            return new String(xmlstring.toString().getBytes(), "ISO-8859-1");        } catch (UnsupportedEncodingException e) {            e.printStackTrace();        }        return null;    }

生成签名:

    /**     * 生成签名     */    public static String genPackageSign(List<NameValuePair> params) {        try {            StringBuilder sb = new StringBuilder();            for (int i = 0; i < params.size(); i++) {                sb.append(params.get(i).getName());                sb.append('=');                sb.append(params.get(i).getValue());                sb.append('&');            }            sb.append("key=");            sb.append(WeChatConstans.PARTNER_KEY);            String packageSign = MD5Util.getMessageDigest(                    sb.toString().getBytes("utf-8")).toUpperCase();            return packageSign;        } catch (UnsupportedEncodingException e) {            e.printStackTrace();            return null;        }    }

随机字符串:

/**     * 微信支付调用统一下单接口,随机字符串     */    public static String genNonceStr() {        try {            Random random = new Random();            String rStr = MD5Util.getMessageDigest(String.valueOf(                    random.nextInt(10000)).getBytes("utf-8"));            return rStr;        } catch (UnsupportedEncodingException e) {            e.printStackTrace();            return null;        }    }

因为微信接收参数的格式只支持xml格式,所以我们要将参数转为xml格式,具体参照统一下单 接口,生成xml格式:

/**     * 生成 XML     */    public static String toXml(List<NameValuePair> params) {        StringBuilder sb = new StringBuilder();        sb.append("<xml>");        for (int i = 0; i < params.size(); i++) {            sb.append("<" + params.get(i).getName() + ">");            sb.append(params.get(i).getValue());            sb.append("</" + params.get(i).getName() + ">");        }        sb.append("</xml>");        return sb.toString();    }

2、调用统一下单接口获取预付ID

将我们第一步构建支付订单信息作为参数请求统一下单接口:

/**     * 异步网络请求获取预付Id     */    private class GetPrepayIdTask extends AsyncTask<String, Void, String> {        @Override        protected void onPreExecute() {        }        @Override        protected void onPostExecute(String result) {            // 第三步, 发送支付请求            sendPayReq(result);        }        @Override        protected String doInBackground(String... params) {            // 网络请求获取预付Id            String url = String.format(WeChatConstans.WECHAT_UNIFIED_ORDER);            String entity = genEntity();            byte[] buf = WeChatHttpClient.httpPost(url, entity);            if (buf != null && buf.length > 0) {                try {                    Map map = XmlUtil.doXMLParse(new String(buf));                    return (String) map.get("prepay_id");                } catch (Exception e) {                    e.printStackTrace();                }            }            return null;        }    }

由于返回的参数格式是xml格式,所以我们要解析数据,获取prepay_id:

/**     * 解析xml,返回第一级元素键值对。如果第一级元素有子节点,则此节点的值是子节点的xml数据。     *      * @param strxml     * @return     * @throws JDOMException     * @throws IOException     */    public static Map doXMLParse(String strxml) throws Exception {        if (null == strxml || "".equals(strxml)) {            return null;        }        Map m = new HashMap();        InputStream in = new ByteArrayInputStream(strxml.getBytes());        SAXBuilder builder = new SAXBuilder();        Document doc = builder.build(in);        Element root = doc.getRootElement();        List list = root.getChildren();        Iterator it = list.iterator();        while (it.hasNext()) {            Element e = (Element) it.next();            String k = e.getName();            String v = "";            List children = e.getChildren();            if (children.isEmpty()) {                v = e.getTextNormalize();            } else {                v = getChildrenText(children);            }            m.put(k, v);        }        // 关闭流        in.close();        return m;    }    /**     * 获取子结点的xml     *      * @param children     * @return String     */    public static String getChildrenText(List children) {        StringBuffer sb = new StringBuffer();        if (!children.isEmpty()) {            Iterator it = children.iterator();            while (it.hasNext()) {                Element e = (Element) it.next();                String name = e.getName();                String value = e.getTextNormalize();                List list = e.getChildren();                sb.append("<" + name + ">");                if (!list.isEmpty()) {                    sb.append(getChildrenText(list));                }                sb.append(value);                sb.append("</" + name + ">");            }        }        return sb.toString();    }

* 3、生成带签名的客户端支付信息(直接调起支付)*

/**     * 发送支付请求     * @param prepayId 预付Id     */    private void sendPayReq(String prepayId) {        PayReq req = new PayReq();        req.appId = WeChatConstans.APP_ID;        req.partnerId = WeChatConstans.PARTNER_ID;        req.prepayId = prepayId;        req.nonceStr = genNonceStr();        req.timeStamp = String.valueOf(genTimeStamp());        req.packageValue = "Sign=WXPay";        List<NameValuePair> signParams = new LinkedList<NameValuePair>();        signParams.add(new BasicNameValuePair("appid", req.appId));        signParams.add(new BasicNameValuePair("noncestr", req.nonceStr));        signParams.add(new BasicNameValuePair("package", req.packageValue));        signParams.add(new BasicNameValuePair("partnerid", req.partnerId));        signParams.add(new BasicNameValuePair("prepayid", req.prepayId));        signParams.add(new BasicNameValuePair("timestamp", req.timeStamp));        //再次签名        req.sign = genPackageSign(signParams);        // 传递的额外信息,字符串信息,自定义格式        // req.extData = type +"#" + out_trade_no + "#" +total_fee;        // 微信支付结果界面对调起支付Activity的处理        // APPCache.payActivity.put("调起支付的Activity",(调起支付的Activity)context);        // 在支付之前,如果应用没有注册到微信,应该先调用IWXMsg.registerApp将应用注册到微信        api.registerApp(WeChatConstans.APP_ID);        api.sendReq(req);        // 支付完成后微信会回调 wxapi包下 WXPayEntryActivity 的public void onResp(BaseResp        // resp)方法,所以后续操作,放在这个回调函数中操作就可以了    }

微信支付成功调起支付后回调到WXPayEntryActivity就是正常微信支付流程了。当我们调起了支付,就要进行一些处理了,比如支付成功了要把调起支付的Activity finish掉,或者你要传递一些订单信息去支付成功界面,那就要用req.extData了,代码中可以看到,我传了type (订单类型)+out_trade_no(订单号) +total_fee(费用),并用”#”分割,这种就是自定义数据格式了。

WXPayEntryActivity:

package com.example.payment.wxapi;import android.app.Activity;import android.content.Intent;import android.os.Bundle;import com.example.payment.R;import com.tencent.mm.sdk.constants.ConstantsAPI;import com.tencent.mm.sdk.modelbase.BaseReq;import com.tencent.mm.sdk.modelbase.BaseResp;import com.tencent.mm.sdk.openapi.IWXAPI;import com.tencent.mm.sdk.openapi.IWXAPIEventHandler;import com.tencent.mm.sdk.openapi.WXAPIFactory;public class WXPayEntryActivity extends Activity implements IWXAPIEventHandler{    private IWXAPI api;    /**     * 支付类型     */    private String type;    /**     * 订单号     */    private String orderId;    /**     * 支付费用     */    private String fee;    @Override    public void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.pay_result);        String data = getIntent().getStringExtra("_wxapi_payresp_extdata");        String[] str = data.split("#");        type = str[0];        orderId = str[1];        fee = str[2];        api = WXAPIFactory.createWXAPI(this, WeChatConstans.APP_ID);        api.handleIntent(getIntent(), this);    }    @Override    protected void onNewIntent(Intent intent) {        super.onNewIntent(intent);        setIntent(intent);        api.handleIntent(intent, this);    }    @Override    public void onReq(BaseReq req) {    }    @Override    public void onResp(BaseResp resp) {        // 支付结果回调...        if (resp.getType() == ConstantsAPI.COMMAND_PAY_BY_WX) {            if(resp.errCode == 0){//支付成功                //type 有多种,进行不同的跳转            }else{                WXPayEntryActivity.this.finish();            }        }else{            WXPayEntryActivity.this.finish();        }    }}

好了,最后我们来看下Activity怎么调起支付吧:

public class MainActivity extends Activity implements OnClickListener{    /**     * 相匹配的msg.what     */    private int payFlag = 11;    /**     * 订单ID     */    private String orderId;    /**     * 最后需要支付的金额     */    private String fastAmount;    /**     * 不同类型的订单     */    int type = 1;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        findViewById(R.id.btn_alipay).setOnClickListener(this);        findViewById(R.id.btn_wechatpay).setOnClickListener(this);    }    @Override    public void onClick(View v) {        switch(v.getId()){        case R.id.btn_alipay:            String subject  = "测试名称";            AliPayService.pay(new PayEntity(orderId, subject, "测试商品不描述", fastAmount), mHandler, payFlag , this);            break;        case R.id.btn_wechatpay:            String body = "测试商品不描述";            WeChatPayService weChatPay = new WeChatPayService(this,type, orderId, body, fastAmount);            weChatPay.pay();            break;        default:            break;        }    }    /**     * 支付宝支付异步通知     */    private Handler mHandler = new Handler() {        public void handleMessage(Message msg) {            if(msg.what == payFlag){                String resultStatus = new PayResult((String) msg.obj).getResultStatus();                // 判断resultStatus 为“9000”则代表支付成功,具体状态码代表含义可参考接口文档                if ("9000".equals(resultStatus)) {                    showShortToast("支付成功!");                    //后续业务处理                } else if("8000".equals(resultStatus)){ //"8000"代表支付结果因为支付渠道原因或者系统原因还在等待支付结果确认,最终交易是否成功以服务端异步通知为准(小概率状态)                    showShortToast("支付结果确认中");                } else { //其他值就可以判断为支付失败,包括用户主动取消支付,或者系统返回的错误                    showShortToast("支付失败");                }            }        };    };    private void showShortToast(String text) {        Toast.makeText(this, text, Toast.LENGTH_SHORT).show();    }}

总结
文章重点介绍了微信支付服务端操作放到客户端而支付宝的服务端操作比较简单,由于篇幅原因,所以只贴了部分关键代码,下面会给出模板demo(Eclispce和Studio都有),Demo不能直接调起支付,但是替换为具体项目非常方便。通过本文可以了解到,其实不管客户端还是服务端,只要按照官方文档来做,一般还是能实现的,不过官方文档也有不足的地方,文档排版不好、内容混杂、文档更新不及时、demo杂而乱(PS:这里只觉得微信demo太杂太乱了)。但是,只要你用心,这些都不是问题。

如果……有同学遇到服务端蛮不讲理的把这些操作丢给你,你可以和他理论一番,他改正也就好了,如果还是一意孤心,你还是忍忍吧……毕竟,你要做一个大度的人,哈哈哈!

The good seaman is known in bad weather.

Demo传送门

1 0
原创粉丝点击