第一次做微信支付记录一下:
- 使用企业执照申请, 获得APPID, mch_id, key 等 (https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=8_3 微信支付文档里有时序图)
- 流程很简单: 用户下单后, 后端生成订单, 然后调用统一下单api, 微信支付系统会生成预付单, 并返回给商户服务器, 支付成功后,微信会通过notify_url返回支付结果给商户后端. 后端可以进一步处理.
-
统一下单api:
接口地址:https://api.mch.weixin.qq.com/pay/unifiedorder
请求参数: APPID, mch_id .......(大约10个左右必填参数)
转换成XML数据格式, 通过curl 请求.
如果微信返回成功标识, 还需要对参数进行二次签名 - 成功会返回几个参数, 其中包括但不限于: prepay_id, appid, mch_id (把返回的参数进行二次签名返回给前端, 调起支付)
- $xml = file_get_contents('php://input'); //PHP版本大于5.6 $xml = $GLOBALS['HTTP_RAW_POST_DATA']; //PHP版本小于5.6 //用来接收微信通知的数据,
- 遇到的错误: 发送请求, 微信返回 "XML格式错误", 开始不知道怎么排错. 后来在网上看到了一个例子: 使用 $xml = htmlspecialchars($xml); //把XML格式数据显示出来, 看看XML数据有没有少什么参数. 还有一些要注意的地方: 比如说XML编码要求是utf-8的.(我这里默认是utf-8, 不用设置)
- 签名失败: 可能的原因很多, 我遇到的有: (1) 因为key 填写的错误(要保证这个是设置秘钥的那个key) (2) 因为微信文档说了参数值为空的不参与签名,需要过滤掉, 但是total_fee为0的也被过滤掉了, 所以少了这个参数导致签名失败, 解决办法是total_fee为0的就不要下单了.
- libxml_disable_entity_loader(true); //修复XXE漏洞 参考: https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=23_5
- 我使用yii框架做支付遇到的问题: yii配置的路由是默认格式, 因为最开始没有配置美化的URL. 所以导致一个 notify_url 的问题, 事情是这样的: 微信文档里说notify_url 是不能带有参数的, 但是yii默认的URL是 ?r=moduleID/controllerID/actionID 的形式, 使用这个微信是不能通知结果的, 但是如果改成美化的URL, 之前的接口都得变, 后端没什么, 前端就得改改改了. 为了前端不改改改. 只能后端想想办法, 后来的解决办法就是: 在项目的web目录下创建一个PHP文件, 这个是可以直接访问的, 比如web/wxpay.php ,那么就可以通过https://hostName/web/wxpay.php 访问到. (这里注意,域名配置的时候, 配置到项目的根目录 , 如果配置到了index.php,那这个办法就用不了了,只能使用rewrite或者美化URL或者别的方法了)
$_GET['r'] = 'course/wx-pay/test'; require __DIR__ . '/index.php'; //在wxpay.php 添加这行代码,微信访问这个文件,在转发到处理结果的控制器方法里
这里成功了, 紧接着又出现了新的问题, 接受不到微信的通知, 查看nginx/error.log 没发现什么错误, 查看nginx/access.log 发现微信请求报400, 原因: 微信是通过post 请求的, 但是yii 框架接受post请求,会验证csrf, 而微信是么有这个参数的, 所以需要关闭csrf验证机制,只在某个控制器里关闭csrf验证(局部禁止csrf验证), 在/course/wxpay控制器里写下一行代码
public $enableCsrfValidation = false; //关闭csrf验证 在控制器中加入这行代码就可以关闭csrf验证, 就能接收微信支付通知结果啦
- 能够接受到微信的结果通知了, 但是我给微信返回结果,微信没有接收到, 导致微信一直给我发支付通知, 找原因, 发现是因为签名出现了错误, 微信通知结果的参数里有sign, 是为了验证用的. 把这个参数也进行签名了, 所以导致验证失败, 没执行应答微信的逻辑, 只好重新写一个签名的代码
ksort($arr); $buff = ''; foreach ($arr as $k => $v){ if($k != 'sign'){ $buff .= $k . '=' . $v . '&'; } } $stringSignTemp = $buff . 'key=vrjwwwikogt1zs1i0ih3rp2mmayw24';//key为证书密钥 $sign = strtoupper(md5($stringSignTemp));
- 特别注意: 保存微信参数的时候, 出现了500, 后来发现因为微信返回的time_end是14位的字符串, 而我数据库设置的是int(11), 所以导致服务器错误, 总结: 有时候服务器500, 找问题的时候不能只看代码,忽略了数据库, 极有可能是数据库设置字段约束的时候, 存储字节长度,类型等等有问题.
- 附上代码(只供参考, 不能直接使用)
1 public $enableCsrfValidation = false; //关闭csrf验证(yii接收微信post请求时做的处理) 2 /** 3 * 接收用户下单数据 4 * @param string $token : 用来验证 5 * @param int $store_id : 区分快应用 6 * @param int $cid : 课程id 7 * @param string $phone 用户账号 8 * @return mixed data 9 */ 10 public function actionOrder($store_id=null, $cid=null, $phone=null) 11 { 12 if($store_id == null){ 13 $data = [ 14 'code'=>1, 15 'msg'=> 'store_id不能为空', 16 'data'=>[], 17 ]; 18 return $data; 19 } 20 if($cid == null){ 21 $data = [ 22 'code'=>1, 23 'msg'=> 'cid不能为空', 24 'data'=>[], 25 ]; 26 return $data; 27 } 28 if($cid == null){ 29 $data = [ 30 'code'=>1, 31 'msg'=> '请传一个价格', 32 'data'=>[], 33 ]; 34 return $data; 35 } 36 $quick_app_id = Store::findOne($store_id)['wechat_app_id']; //根据store_id查找quick_app_id,获取指定的APPID等信息 37 if(empty($quick_app_id)){ 38 $data = [ 39 'code'=>1, 40 'msg'=> '快应用不存在,请确认store_id参数正确并且后台快应用配置成功', 41 'data'=>[], 42 ]; 43 return $data; 44 } 45 //根据id查询课程信息 46 $course_info = Course::find()->select('fee')->where(['id'=>$cid,])->one(); //防止用户修改付款价格 47 if(!$course_info){ 48 $data = [ 49 'code'=>1, 50 'msg'=> '没有数据, 请确认cid参数是否正确', 51 'data'=>[], 52 ]; 53 return $data; 54 } 55 //生成随机字符串(下面的才是统一下单代码) 56 $rand = mt_rand(1000,9999).time(); 57 $nonce_str = strval($rand); 58 $ip = $_SERVER['REMOTE_ADDR']; //客户端IP 59 $out_trade_no = date('YmdHis').'-'.date('si-Hm-dY'); 60 $wxpay = WxPay::findOne($quick_app_id); 61 $wxpay->notify_url = 'https://'.$_SERVER['HTTP_HOST'].'/web/wx-pay.php'; 62 $wxpay->nonce_str = $nonce_str; 63 //$wxpay->trade_type = 'APP'; 64 $wxpay->total_fee = $course_info['fee']; 65 $wxpay->spbill_create_ip = $ip; 66 $wxpay->time_start = date('YmdHis'); 67 $wxpay->time_expire = date('YmdHis', time()+3600); 68 $wxpay->body = $wxpay->name.'-购买课程'; //需传入应用市场上的APP名字-实际商品名称 69 $wxpay->out_trade_no = $out_trade_no; 70 //把订单信息保存到数据库 71 $order = new Order(); 72 $order->out_trade_no = $out_trade_no; 73 $order->total_fee = $wxpay->total_fee; 74 $order->order_time = time(); 75 $order->body = $wxpay->body; 76 $order->store_id = $store_id; 77 $order->phone = $phone; //根据phone 和 store_id就可以定位用户了 78 $order->cid = $cid; 79 if(!$order->save()){ 80 // 把错误信息添加到log表中保存 81 } 82 $dataXML = $wxpay->UniformOrder(); 83 $arr = (array)simplexml_load_string($dataXML, 'SimpleXMLElement', LIBXML_NOCDATA); 84 //print_r($arr); 85 if($arr['return_code'] == 'SUCCESS' && $arr['result_code'] == 'SUCCESS'){ 86 //需要进行二次签名 87 $twoSign['appid'] = $arr['appid']; 88 $twoSign['partnerid'] = $arr['mch_id']; 89 $twoSign['prepayid'] = $arr['prepay_id']; 90 $twoSign['noncestr'] = $arr['nonce_str']; 91 $twoSign['package'] = 'Sign=WXPay'; //目前固定值 92 $twoSign['timestamp'] = time(); 93 $twoSign['sign'] = $wxpay->getSign($twoSign, $wxpay->key); 94 $data = [ 95 'code' => 0, 96 'msg' => 'success', 97 'data' => $twoSign, 98 ]; 99 return $data; 100 }else{ 101 //fail 102 $data = [ 103 'code' => 1, 104 'msg' => 'fail', 105 'data' => ['errmsg'=>$arr], 106 ]; 107 return $data; 108 } 109 }
/** * 微信回调 */ public function actionNotify() { libxml_disable_entity_loader(true); //修复XXE漏洞 $postStr = file_get_contents('php://input'); //php raw data , require php version > 5.6 $postObj = simplexml_load_string($postStr, 'SimpleXMLElement', LIBXML_NOCDATA); $arr = json_decode(json_encode($postObj), true); #对象转成数组 //处理一下key匹配对应的快应用问题, 通过订单号查询订单, 在订单表有store_id,根据store_id可以查询到对应的快应用配置的key $order = Order::findOne(['out_trade_no'=>$arr['out_trade_no']]); if(!$order){ return false; } $store = Store::findOne($order->store_id); if(!$store){ return false; } $wxpay = WxPay::findOne($store->wechat_app_id); if(!$wxpay){ return false; } if($arr['return_code'] != 'SUCCESS'){ echo '<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>'; return ; } //签名 ksort($arr); $buff = ''; foreach ($arr as $k => $v){ if($k != 'sign'){ $buff .= $k . '=' . $v . '&'; } } $stringSignTemp = $buff . "key={$wxpay->key}";//key为证书密钥 //$stringSignTemp = $buff . "key=vrjwwwidkogt1zs1i0ih3rp2mmayw24j";//key为证书密钥 $sign = strtoupper(md5($stringSignTemp)); if($sign == $arr['sign']){ //验证签名成功, 处理商户订单逻辑, 注意需要给微信返回接收信息成功的通知 signature successfully $session = Yii::$app->session; $session->set('notify', $arr['return_code']); //先把结果写在文件里, 看一下结果 //$paylog = Yii::$app->basePath.'/web/paylog.txt'; //$str_arr = var_export($arr, true); //$res = file_put_contents($paylog, $str_arr, FILE_APPEND); //$order = Order::findOne(['out_trade_no'=>$arr['out_trade_no']]);//上面查询了 $order->is_pay = 1; $order->pay_time = $arr['time_end']; //数据库里这个存储的是int11,而微信返回的是字符串14位 $order->transaction_id = $arr['transaction_id']; $order->openid = $arr['openid']; $order->save(); //用户支付成功了, 把课程添加到用户订阅课程里user_course表 $r = (new UserCourse())->addSubscribe($order->phone, $order->cid);// 是这一行的原因吗 if(!$order->save()){ return false; } return '<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>'; }else{ //fail return '<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>'; } }
参考: https://www.jianshu.com/p/52bcadca67bc