PHP开发时,在微信支付上遇到的坑

I hate WeChat Pay

最近客户突然提出该需求,说要添加微信支付功能,于是奋不顾身的跳进了微信支付的大坑中。虽说有高人指点,但是微信支付的文档不是一般的烂,还有一大堆的“不成文的规定”。

前言

在一个不算 PM 的 PM 的折磨下,靠着自己顽强的生命力,终于把项目一期快啃下来了。

最坑爹的是在开发中途,客户突然提出该需求,说要添加微信支付功能。

一听到这个需求问题我就炸毛了,但是,没办法,有钱就是大爷。

在不得不的情况下,奋不顾身的跳进了微信支付的大坑中。

虽说有高人指点,但是微信支付的文档不是一般的烂,还有一大堆的“不成文的规定”。


正文

我们开发项目时使用的框架 Yii2 ,所以之后我也会吐槽一下不懂 autoload 机制的蛋疼。

这个项目是纯 Web 项目,由 PC & Mobile 组成,其中 移动端 使用的是 JS API 支付方式, PC站 使用的是 NATIVE 的支付方式。

JS API & 移动端

流程大致如此:

跳转到微信接口 -> 获取 User Code -> 获取 access_token & Open ID -> POST 统一支付订单 -> 调起微信支付 JS -> 同步\异步消息回调

跳转到微信接口,获取 User Code

由于项目使用的是 Yii2,所有例子都从真实代码中摘取出来,下不做说明。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
/**
 * 调起微信支付的第一跳,获取用户code
 * 获得code之后跳到`actionWechatPay`这个action
 */
public function actionWechatCall(){
    
    /* 验权代码 */

    $url_base = 'https://open.weixin.qq.com/connect/oauth2/authorize?';
    $url_appid = Config::AppID ;
    $url_redirect = Yii::$app->urlManager->createUrl([
        'user/wechat-pay',
        'response_type'=>'code',
        'scope'=>'snsapi_userinfo',
        'state'=>$order_sn,
        '#'=>'wechat_redirect',
    ]);
    $url = $url_base.'appid='.$url_appid.'&redirect_uri=http://example.com'.$url_redirect;
    $this->redirect($url);
}

下面的 URI 就是我们要跳转到的地方,然后微信会按照回调地址跳转回来并带上 Code 。

1
https://open.weixin.qq.com/connect/oauth2/authorize?appid=wx8888888888888888&redirect_uri=http://davex.pw/wxpay/js_api_call.php&response_type=code&scope=snsapi_base&state=STATE#wechat_redirect

提示:这里的回调URI可以带参数

获取 access_token & Open ID & POST 统一支付订单

这部分使用了微信开发给的 SDK,可以在官网上找到。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/**
 * @return mixed|string
 * @throws \common\models\WxPayException
 * 用于微信支付的页面API调起
 */
public function actionWechatPay(){
    $state = Yii::$app->request->get('state');
    $code = Yii::$app->request->get('code');
    
    /* 验权代码 */
    
    $wechat = new WeChat(); // 微信操作类
    $apiurl = 'https://api.weixin.qq.com/sns/oauth2/access_token?appid=';
    $appid  = Config::AppID ;
    $secret = Config::Secret ;
    $url    = $apiurl.$appid.'&secret='.$secret.'&code='.$code.'&grant_type=authorization_code';
    
    $ret_json = $wechat->curl_get_contents($url); // 返回的 Json 数据,包含 Open ID
    $ret    = json_decode($ret_json);
    $access_token = $ret->access_token; 
    $openid = $ret->openid; // 用户的 Open ID

    $time = time();

    $unifiedOrder = new WxPayUnifiedOrder(); //统一支付接口中,trade_type为JSAPI时,openid为必填参数!
    $unifiedOrder->SetAppid(WxPayConfig::APPID); //商户 App ID
    $unifiedOrder->SetMch_id(WxPayConfig::MCHID);
    $unifiedOrder->SetDevice_info('WEB');
    $unifiedOrder->SetNonce_str(<!-- 随机字符串 -->);
    $unifiedOrder->SetOpenid($openid);//用户标识
    $unifiedOrder->SetBody('Dave WeChat Pay Test Case');//商品简要描述
    $unifiedOrder->SetDetail('Dave WeChat Pay Test Case');//商品描述
    $unifiedOrder->SetOut_trade_no(<!-- 商品订单编号 -->);//商品订单编号,不可重复
    $unifiedOrder->SetFee_type('CNY');//商品支付类型
    $unifiedOrder->SetTotal_fee(<!-- 费用 单位:分 -->);
    //商品总金额,交易金额默认为人民币交易,接口中参数支付金额单位为【分】,参数值不能带小数。对账单中的交易金额单位为【元】。
    $unifiedOrder->SetSpbill_create_ip(<!-- 用户IP -->);//用户创建订单的IP
    $unifiedOrder->SetTime_start(date('YmdHis',$time));//交易起始时间
    $unifiedOrder->SetTime_expire(date('YmdHis',$time + 30*60));//交易结束时间
    $unifiedOrder->SetNotify_url(<!-- 用户回调URL -->); //接收微信支付异步通知回调地址
    $unifiedOrder->SetTrade_type('JSAPI');//交易类型,取值如下:JSAPI,NATIVE,APP

    $result = WxPayApi::unifiedOrder($unifiedOrder);
    if($result['return_code']=='SUCCESS'){
        if($result['result_code']=='SUCCESS'){
            /* 订单创建成功代码 */
        }
    }

    $tools = new JsApiPay();
    $jsApiParameters = $tools->GetJsApiParameters($result);
    
    return $this->render('pay',[
        'jsApiParameters' => $jsApiParameters, // 传递这些参数到显示页面 View 层
    ]);

}

请严重注意:接受微信支付异步通知回调地址不准带参数,只允许 http://davex.pw/wechat/mobile_pay.php

这就是微信所谓的不成文规定,我们当初因为这个问题花费了一个小时,最后才知道的。

上面那一长串的代码,实质上就是生成一个 XML 文档。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<xml>
  <openid><![CDATA[]]></openid>
  <body><![CDATA[DaveX]]></body>
  <out_trade_no><![CDATA[]]></out_trade_no>
  <total_fee>1</total_fee>
  <notify_url><![CDATA[http://davex.pw/wechat/mobile_pay.php]]></notify_url>
  <trade_type><![CDATA[JSAPI]]></trade_type>
  <appid><![CDATA[]]></appid>
  <mch_id>1008686</mch_id>
  <spbill_create_ip><![CDATA[127.0.0.1]]></spbill_create_ip>
  <nonce_str><![CDATA[]]></nonce_str>
  <sign><![CDATA[]]></sign>
</xml>

然后这个 XML 会被 POST 到 https://api.mch.weixin.qq.com/pay/unifiedorder

然后会返回如下模样的数据:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<xml>
  <return_code><![CDATA[SUCCESS]]></return_code>  
  <return_msg><![CDATA[OK]]></return_msg>  
  <appid><![CDATA[wx8888888888888888]]></appid>  
  <mch_id><![CDATA[10012345]]></mch_id>  
  <nonce_str><![CDATA[Be8YX7gjCdtCT7cr]]></nonce_str>  
  <sign><![CDATA[885B6D84635AE6C020EF753A00C8EEDB]]></sign>  
  <result_code><![CDATA[SUCCESS]]></result_code>  
  <prepay_id><![CDATA[wx201410272009395522657a690389285100]]></prepay_id>  
  <trade_type><![CDATA[JSAPI]]></trade_type> 
</xml>

其中有一个非常重要的数据 prepay_id

1
wx201410272009395522657a690389285100

调起微信支付 JS

1
$jsApiParameters = $tools->GetJsApiParameters($result);

上面这部分代码会生成如下 json 数据

1
2
3
4
5
6
7
8
{
    "appId": "wx8888888888888888",
    "timeStamp": "1414411784",
    "nonceStr": "gbwr71b5no6q6ne18c8up1u7l7he2y75",
    "package": "prepay_id=wx201410272009395522657a690389285100",
    "signType": "MD5",
    "paySign": "9C6747193720F851EB876299D59F6C7D"
}

在微信浏览器中调用如下 JS,完整 HTML 如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<html>
<head>
    <meta http-equiv="content-type" content="text/html;charset=utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1"/>
    <title>微信支付</title>
    <script type="text/javascript">
        //调用微信JS api 支付
        function jsApiCall()
        {
            WeixinJSBridge.invoke(
                'getBrandWCPayRequest',
                <?php echo $jsApiParameters; ?>,
                function(res){
                    WeixinJSBridge.log(res.err_msg);
                    alert(res.err_code+'|'+res.err_desc+'|'+res.err_msg);
                    if(res.err_msg == "get_brand_wcpay_request:ok" ) {
                        // 使用以上方式判断前端返回,微信团队郑重提示:res.err_msg将在用户支付成功后返回    ok,但并不保证它绝对可靠。
                        // get_brand_wcpay_request:ok 支付成功后的操作
                    }
                }
            );
        }

        function callpay()
        {
            if (typeof WeixinJSBridge == "undefined"){
                if( document.addEventListener ){
                    document.addEventListener('WeixinJSBridgeReady', jsApiCall, false);
                }else if (document.attachEvent){
                    document.attachEvent('WeixinJSBridgeReady', jsApiCall);
                    document.attachEvent('onWeixinJSBridgeReady', jsApiCall);
                }
            }else{
                jsApiCall();
            }
        }
    </script>
</head>
<body>
    <input onclick="callpay()" type="button" class="ticket_payment_buton" value="确认付款" />
</body>
</html>

点击 确认付款 就能看到这个神奇的魔法噜。

同步\异步消息回调

假设 http://davex.pw/wechat/mobile_pay.php 是我们的回调地址,支付成功后会有如下信息返回

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<xml>
  <appid><![CDATA[wx8888888888888888]]></appid>  
  <bank_type><![CDATA[CFT]]></bank_type>  
  <fee_type><![CDATA[CNY]]></fee_type>  
  <is_subscribe><![CDATA[Y]]></is_subscribe>  
  <mch_id><![CDATA[10012345]]></mch_id>  
  <nonce_str><![CDATA[60uf9sh6nmppr9azveb2bn7arhy79izk]]></nonce_str>  
  <openid><![CDATA[ou9dHt0L8qFLI1foP-kj5x1mDWsM]]></openid>  
  <out_trade_no><![CDATA[wx88888888888888881414411779]]></out_trade_no>  
  <result_code><![CDATA[SUCCESS]]></result_code>  
  <return_code><![CDATA[SUCCESS]]></return_code>  
  <sign><![CDATA[0C1D7F2534F1473247550A5A138F0CEB]]></sign>  
  <sub_mch_id><![CDATA[10012345]]></sub_mch_id>  
  <time_end><![CDATA[20141027200958]]></time_end>  
  <total_fee>1</total_fee>  
  <trade_type><![CDATA[JSAPI]]></trade_type>  
  <transaction_id><![CDATA[1002750185201410270005514026]]></transaction_id> 
</xml>

NATIVE & PC

参考

微信支付开发(1) JS API支付

微信支付开发(4) 动态链接Native支付

使用 Hugo 构建
主题 StackJimmy 设计