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

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

前言

在一个不算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://www.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://www.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://www.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://www.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支付