skysowe
skysowe
  • 发布:2019-06-17 17:33
  • 更新:2022-01-25 11:39
  • 阅读:29981

终于搞定了UniApp开发的微信小程序的支付,分享下有关微信支付踩的坑

分类:uni-app

之前用H5+做过微信支付和支付宝支付,是一年前做的,有些细节忘记了,本以为不会费太多时间,结果搞了12个小时

记录一下踩过的坑,包括:调用支付JSAPI缺少参数: total_fee,支付验证签名失败,不识别的参数body!!!等我都碰到了

如何去微信那边申请【公众平台】【商户平台】【开放平台】本来不想讲,但是这几个平台也确实容易把人搞晕,还是大致说说我的理解吧,也算是梳理下自己的思路,方便新手,老手绕道:

最早就一个【公众号平台:mp.weixin.qq.com】,2015-2016年那会儿做微信公众号特别火,所以大家最早接触的是它,此前微信支付还不对公众开放,后来有了服务号之后,可以做公众号商城,有了支付需求,微信推出了【商户平台:pay.weixin.qq.com】,从公众号平台,有一个按钮点击就进入了商户平台,,最早的微信支付就只有JSAPI方式,也就是在微信内置浏览器里支付,后来增加了H5网页支付、Native支付、App支付、付款码支付,见下图:

我们一般开发者大多数用到的是JSAPI和App两种,微信公众号服务号、公众号商城、包括后来的小程序,都是用的JSAPI方式,用HBuilder和HBuilderX开发的跨平台App用的是App方式。

【开放平台:open.weixin.qq.com】又是什么呢?如果我们想做一个App或微信小程序要集成微信支付功能,需要注册哪些呢?需要注册【微信开放平台】,在开放平台里注册开发者账号,把自己的应用(就是我们的manifest.json里的那个appid)登记上去,然后要申请支付功能,申请支付功能就等于是开通了【商户平台】,因为钱要去这里,如果已经有了【商户平台】就可不用再申请了,见下图:

当然开放平台不单单包括微信支付,还包括其他接入服务,包括(移动应用,网站应用,公众账号,第三方平台)

题外话:微信公众号平台是要年审的,费用300,,开放平台也是要年审的,也是300。

小程序是要单独注册的,但是也是在微信公众平台的页面mp.weixin.qq.com注册,不过要另外注册账号,不能使用已有的微信公众号账号,但是申请成功后,可以挂靠/绑定在某个已有的【公众号】上,同时可以关联已有的【商户平台】。

我个人感觉,小程序是微信公众号的JSSDK可以调用的一大堆接口+Vue的框架的一种集成,过去的微信公众号(服务号)的开发基本上纯后台的,前端技术的不断发展,尤其是Vue这类前端框架,让前端可以做更多的事,包括一些复杂的界面成为可能。

以上梳理了微信几大平台的关系,啰啰嗦嗦说了一堆自己的感受,还回到文章的题目上,说说微信小程序支付的坑。


正是因为有几大平台,所以好多知识点分散在各个平台的文档里,而且不断的版本迭代,文档之间也有所差异,之前做App支付,比较顺利,这次做小程序支付,就因为之前没把所有这些知识点贯通起来,所以碰到了点问题:

整个支付流程:
App或小程序,创建自己的订单------>后台访问统一下单接口:https://api.mch.weixin.qq.com/pay/unifiedorder,生成预支付订单,同时提供回调地址--------->预支付订单返回到App或小程序,调起支付,前台支付成功,跳转到相关页面-------->微信支付平台会异步给回调地址发送支付成功的结果通知,通知地址的程序接收到微信的通知,根据返回参数修改自己的订单的支付状态

0.【微信支付商户平台】设置支付目录:

  1. 先说uni-app的支付:官方文档链接:https://uniapp.dcloud.io/api/plugins/payment,这个接口只是唤起支付的接口,在此之前要做些准备的,以上的几大平台,需要注册开通,该注册的注册,该关联的关联,技术上最关键一点是要先创建预支付订单(创建预支付订单在App和微信小程序是一样的,支付宝也类似,也要先创建预支付订单);

  2. 再说H5+也就是App的支付:官方文档链接:http://www.html5plus.org/doc/zh_cn/payment.html,这个接口只是在App端唤起支付,前提条件依然是创建预支付订单,得到返回值,然后被这个接口调用,以唤起支付。

  3. 如何创建预支付订单,我借用的是DCloud提供的示例程序:https://github.com/dcloudio/H5P.Server/tree/master/payment/wxpayv3,PHP的示例代码,这部分代码的更新时间是2年前和4年前了,但是依然能用,不过有些小地方要做点修改,因为微信那边有些参数的名字有所改变,后面会提到。

首先在WxPay.Config.php里配置自己的各项参数:

    const APPID = 'wxb1马赛克c90a';  
    const MCHID = '150马赛克021';  
    const KEY   = 'ET5g6马赛克E3dxMb7iUxL';     //这个KEY就是  
    const NOTIFY_URL = "https://www.马赛克.cn/order/WxPay/notify.php";

然后在index.php里填写自己app或小程序传来的参数,这个index.php也就是自己的app或小程序访问的入口:

    ……  
    不管是用Post还是Get方法,总之要获取从app或小程序传来的自己的参数和生成预支付订单所需要的参数  
    ……  

    /////////////////////////  
    //填写生成统一下单的各项参数  
    /////////////////////////  

    // 商品或支付单简要描述  
    $subject = 'XXX-订单号:' . $request_json['jxcbillnumber']; //自己的商城或平台的商品名或订单号  
    //之前有乱码问题,现在好像只要是utf-8就不会乱码,所以不用转成iso-8859-1了,至少php平台大家都会用utf-8来做开发,java平台对编码问题很敏感,可能要注意  
    //$subject = iconv("utf-8","iso-8859-1",$subject);    

    // 内部订单号,示例代码使用时间值作为唯一的订单ID号,如果使用自己创建的订单号,如果取消支付了,再次支付时会发生错误,所以可能要每次生成不同的内部订单号  
    //$out_trade_no = date('YmdHis', time());  

    //订单金额,以分为单位  
    $total = 1;  

    $unifiedOrder = new WxPayUnifiedOrder();  
    $unifiedOrder->SetBody($subject);               //商品或支付单简要描述  
    $unifiedOrder->SetOut_trade_no($out_trade_no);      //支付订单号  
    $unifiedOrder->SetTotal_fee($total);                //订单金额,也就支付金额,以分为单位  

    ////////////////////////////////////////////////////////////////////////////////////  
    //APP只要一个参数  
    //$unifiedOrder->SetTrade_type("APP");  

    //JSAPI需要再设置openid  
    $unifiedOrder->SetTrade_type("JSAPI");  
    $unifiedOrder->SetOpenid( $request_json['openid'] );    //openid是什么不用多解释了吧,做过微信公众号的都知道  
    ///////////////////////////////////////////////////////////////////////////////////  

    //查看生成订单前的各项数据  
    console_log_wx( 'unifiedOrder前::' . json_encode( $unifiedOrder->GetValues() ) );  

    $result1 = WxPayApi::unifiedOrder($unifiedOrder);  

    //查看生成订单后的各项数据  
    console_log_wx( 'unifiedOrder后::' . json_encode($result1) );  

    //生成的统一下单结果发回app或小程序  
    if (is_array($result1)) {  
        echo json_encode($result1);  
    }  

    //查询生成的统一下单情况,可以看到生成的订单有什么错误,但是这个错误也不准确,很不靠谱  
    $result2 = WxPayApi::orderQuery($unifiedOrder);  
    console_log_wx( 'orderQuery::'.json_encode($result2) );

生成了统一下单的参数,返回给App或小程序,App用plus.payment.request(), 小程序用uni.requestPayment(),可以唤起支付窗口,但是得到一个-1的提示,大家遇到的最多了,如果查询统一下单查询的日志:

$result2 = WxPayApi::orderQuery($unifiedOrder);  
console_log_wx( 'orderQuery::'.json_encode($result2) );

可能就会看到:调用支付JSAPI缺少参数: total_fee,支付验证签名失败,不识别的参数body!!!等奇怪错误,然后开始折腾total_fee,折腾utf-8转iso-8859-1,都没用,原因是微信那边在生成预支付订单后,不同的平台,对参数的个数和名字有所不同:

再仔细读一下这两个平台的流程和相关参数的说明:

========================
APP
========================
App的业务流程:https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=8_3
App的参数说明:https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=9_12

没时间看文档,我们就直接贴图吧,这是供:plus.payment.request()在App唤起支付调用的参数,注意这里变量名都是小写的,而且没有下划线,没有驼峰命名
并且prepayid的内容就是prepayid的内容,不需要写成prepay_id=xxxxxx

========================
JSAPI
========================
JSAPI也就是公众号或小程序的业务流程:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_4
JSAPI也就是公众号或小程序的参数说明:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_7&index=6

***这些参数也就是供uni.requestPayment()在公众号或小程序里唤起支付调用的参数,注意这里的变量名跟App有所不同,个数也不同,而且package要写成prepay_id=xxxxxx的格式,真是个大坑啊!!!如果这里有一个字符不对或者大小写不对,就会支付失败,然后给出莫名其妙的错误


直接上图:

两种方式里都提到了签名,签名又是什么呢?看官方的说明,其实二者是一样的算法,只是参数个数不同,参数的名字也不同,参数的内容细微差别,两个文档里都提到:注意:签名方式一定要与统一下单接口使用的一致:

APP的签名说明:https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=4_3
JSAPI的签名说明:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_3

是时候回去再看:https://github.com/dcloudio/H5P.Server/tree/master/payment/wxpayv3里的代码了,在WxPay.Api.php这个文件里,有关签名的部分:

    public static function unifiedOrder($inputObj, $timeOut = 6)  
    {  
        $url = "https://api.mch.weixin.qq.com/pay/unifiedorder";  
        //检测必填参数  
        if(!$inputObj->IsOut_trade_noSet()) {  
            throw new WxPayException("缺少统一支付接口必填参数out_trade_no!");  
        }else if(!$inputObj->IsBodySet()){  
            throw new WxPayException("缺少统一支付接口必填参数body!");  
        }else if(!$inputObj->IsTotal_feeSet()) {  
            throw new WxPayException("缺少统一支付接口必填参数total_fee!");  
        }else if(!$inputObj->IsTrade_typeSet()) {  
            throw new WxPayException("缺少统一支付接口必填参数trade_type!");  
        }  

        //关联参数  
        if($inputObj->GetTrade_type() == "JSAPI" && !$inputObj->IsOpenidSet()){  
            throw new WxPayException("统一支付接口中,缺少必填参数openid!trade_type为JSAPI时,openid为必填参数!");  
        }  
        if($inputObj->GetTrade_type() == "NATIVE" && !$inputObj->IsProduct_idSet()){  
            throw new WxPayException("统一支付接口中,缺少必填参数product_id!trade_type为JSAPI时,product_id为必填参数!");  
        }  

        //异步通知url未设置,则使用配置文件中的url  
        if(!$inputObj->IsNotify_urlSet()){  
            $inputObj->SetNotify_url(WxPayConfig::NOTIFY_URL);//异步通知url  
        }  

        $inputObj->SetAppid(WxPayConfig::APPID);//公众账号ID  
        $inputObj->SetMch_id(WxPayConfig::MCHID);//商户号  
        $inputObj->SetSpbill_create_ip($_SERVER['REMOTE_ADDR']);//终端ip      
        //$inputObj->SetSpbill_create_ip("1.1.1.1");          
        $inputObj->SetNonce_str(self::getNonceStr());//随机字符串  

        //签名  
        $inputObj->SetSign();  
        $xml = $inputObj->ToXml();  

        $startTimeStamp = self::getMillisecond();//请求开始时间  
        $response = self::postXmlCurl($xml, $url, false, $timeOut);  
        $result = WxPayResults::Init($response);  
        // 统一下单接口返回正常的prepay_id,再按签名规范重新生成签名后,将数据传输给APP。  
        // 参与签名的字段名为appId,partnerId,prepayId,nonceStr,timeStamp,package。注意:package的值格式为Sign=WXPay  
        $time_stamp = time();  
        $pack   = 'Sign=WXPay';  

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////  
        ///重点在下面:这段代码是App的签名用的,小程序的要改成小程序的参数格式,根据自己的环境来选用哪一段  
        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////  

        //////////////////////////////////////////////////////  
        // App输出参数列表  
        $prePayParams =array();  
        $prePayParams['appid']      =$result['appid'];  
        $prePayParams['partnerid']  =$result['mch_id'];  
        $prePayParams['prepayid']   =$result['prepay_id'];  
        $prePayParams['noncestr']   =$result['nonce_str'];  //这里要改成 WxPayApi::getNonceStr();   
        $prePayParams['package']    =$pack;  
        $prePayParams['timestamp']  =$time_stamp;  
        //////////////////////////////////////////////////////  

        //////////////////////////////////////////////////////  
        //JSAPI小程序输出参数列表  
        //输出参数列表,与App的参数有所不同,请参考最新的文档来确定参数的个数和大小写,注意!!!  
        $prePayParams =array();  
        $prePayParams['appId']      = $result['appid'];  
        $prePayParams['timeStamp']  = time();  
        $prePayParams['nonceStr']   = WxPayApi::getNonceStr();  
        $prePayParams['package']    = 'prepay_id=' . $result['prepay_id'];  
        $prePayParams['signType']   = 'MD5';  

        //echo json_encode($prePayParams);  
        $result = WxPayResults::InitFromArray($prePayParams,true)->GetValues();  
        self::reportCostTime($url, $startTimeStamp, $result);//上报请求花费时间  
        //////////////////////////////////////////////////////  

        return $result;  
    }

另外notify.php也要做一些修改,这是接受微信支付成功之后的异步回传的接口,大家发挥自己的创造力吧,啰嗦了一大堆,只是记录自己踩过的坑,以及浪费的12个小时。

9 关注 分享
李复卿 8***@qq.com 2***@qq.com damdmen yucheng0377 老火 hws007 h***@sjbyg.com 2***@qq.com

要回复文章请先登录注册

s***@163.com

s***@163.com

php?
2022-01-25 11:39
hws007

hws007

好东西!谢谢分亨!先点赞,后收藏
2021-12-05 23:33
老火

老火

非常感谢,现在去试一下
2021-12-05 22:40
f***@126.com

f***@126.com

sign的值 是后端提供还是前端生成啊?
2021-07-26 23:17
1***@qq.com

1***@qq.com

回复 ObjectUs :
后端还有二次验证吗,所以加起来有三次验证吗
2021-03-20 13:58
z***@126.com

z***@126.com

调用支付JSAPI缺少参数: total_fee,报这个错倒是解决了,但是现在报:支付场景非法
2020-07-13 13:35
z***@126.com

z***@126.com

我也是报这个错,我写了这些代码,不知道哪里不对了
uni.requestPayment({
provider: _this.provider,
orderInfo: {
appid: `${res.payconfig.appid}`,
mch_id: `${res.payconfig.partnerid}`,
device_info: 'WEB',
nonce_str: `${res.payconfig.noncestr}`,
sign: `${res.payconfig.sign}`,
sign_type: 'MD5',
body: '人工报告',
out_trade_no: `${_this.date}`,
total_fee: _this.datalist.price,
spbill_create_ip: '',
notify_url: '',
trade_type: ''
},
timeStamp: `${res.payconfig.timestamp}`,
nonceStr: res.payconfig.noncestr,
package: res.payconfig.packageValue,
signType: 'MD5',
paySign: res.payconfig.sign,
success: function (arr) {
debugger
console.log('success:' + JSON.stringify(arr));
},
fail: function (err) {
console.log('fail:' + JSON.stringify(err));
}
});
2020-07-09 16:33
ObjectUs

ObjectUs

回复 skysowe :
我的好了解决了3Q,是我后端二次签名验证出的问题
2019-07-12 10:30
skysowe

skysowe (作者)

回复 ObjectUs :
自己的支付url,要放在白名单里,否则可能也会有问题
2019-07-12 08:41
skysowe

skysowe (作者)

回复 ObjectUs :
你前面的步骤都对,然后拿到prepay_id之后,最关键的,自己的程序在服务器端还需要做一次签名,把签名的结果和prepay_id一起发给客户端(APP或小程序)用uni.requestPayment调起支付的窗口

最坑的地方就是签名,这里文档说的语焉不详,我不知道你的后台是什么,能否看懂php的代码,能拿到prepay_id还有错的话,就仔细看我最后一段程序。

另外App要打包之后,才能真正调起支付,,小程序(JSAPI)调试模式也可以支付。
2019-07-12 08:37