PHP微信公众号后台开发(Yii2实现)
本文内容较多,包括微信接入、获取微信用户信息、微信支付、JSSDK配置参数获取等部分。如果读者对微信开发没有一个主观上的认识,那么建议读者先研读微信公众平台开发者文档,然后再阅读本文,效果更佳!
微信开发的完整例子已经整理在Github,欢迎查看: yii2-wechat-demo。【八宝粥的博客】
接入微信
Yii2后台配置
1.在app/config/params.php中配置token参数
return [ //微信接入 \'wechat\' =>[ \'token\' => \'your token\', ], ];
2.在app/config/main.php中配置路由
因为接口模块使用的RESTful API,所以需要定义路由规则。
\'urlManager\' => [ \'enablePrettyUrl\' => true, \'enableStrictParsing\' => true, \'showScriptName\' => false, \'rules\' => [ [ \'class\' => \'yii\rest\UrlRule\', \'controller\' => \'wechat\', \'extraPatterns\' => [ \'GET valid\' => \'valid\', ], ], ], ],
3.在app/controllers中新建WechatController
<?php namespace api\controllers; use Yii; use yii\rest\ActiveController; class WechatController extends ActiveController { public $modelClass = \'\'; public function actionValid() { $echoStr = $_GET["echostr"]; $signature = $_GET["signature"]; $timestamp = $_GET["timestamp"]; $nonce = $_GET["nonce"]; //valid signature , option if($this->checkSignature($signature,$timestamp,$nonce)){ echo $echoStr; } } private function checkSignature($signature,$timestamp,$nonce) { // you must define TOKEN by yourself $token = Yii::$app->params[\'wechat\'][\'token\']; if (!$token) { echo \'TOKEN is not defined!\'; } else { $tmpArr = array($token, $timestamp, $nonce); // use SORT_STRING rule sort($tmpArr, SORT_STRING); $tmpStr = implode( $tmpArr ); $tmpStr = sha1( $tmpStr ); if( $tmpStr == $signature ){ return true; }else{ return false; } } } }
微信公众号后台配置
在微信公众号后台配置URL和Token,然后提交验证即可。
URL:http://app.demo.com/wechats/valid Token:your token
获取用户信息
用户表设计
CREATE TABLE `wechat_user` ( `id` int(11) NOT NULL, `openid` varchar(255) COLLATE utf8_unicode_ci NOT NULL, `nickname` varchar(50) COLLATE utf8_unicode_ci NOT NULL COMMENT \'微信昵称\', `sex` tinyint(4) NOT NULL COMMENT \'性别\', `headimgurl` varchar(255) COLLATE utf8_unicode_ci NOT NULL COMMENT \'头像\', `country` varchar(50) COLLATE utf8_unicode_ci NOT NULL COMMENT \'国家\', `province` varchar(50) COLLATE utf8_unicode_ci NOT NULL COMMENT \'省份\', `city` varchar(50) COLLATE utf8_unicode_ci NOT NULL COMMENT \'城市\', `access_token` varchar(255) COLLATE utf8_unicode_ci NOT NULL, `refresh_token` varchar(255) COLLATE utf8_unicode_ci NOT NULL, `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; ALTER TABLE `wechat_user` ADD PRIMARY KEY (`id`);
获取用户信息的相关接口
1.用户授权接口:获取access_token、openid等;获取并保存用户资料到数据库
public function actionAccesstoken() { $code = $_GET["code"]; $state = $_GET["state"]; $appid = Yii::$app->params[\'wechat\'][\'appid\']; $appsecret = Yii::$app->params[\'wechat\'][\'appsecret\']; $request_url = \'https://api.weixin.qq.com/sns/oauth2/access_token?appid=\'.$appid.\'&secret=\'.$appsecret.\'&code=\'.$code.\'&grant_type=authorization_code\'; //初始化一个curl会话 $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $request_url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $result = curl_exec($ch); curl_close($ch); $result = $this->response($result); //获取token和openid成功,数据解析 $access_token = $result[\'access_token\']; $refresh_token = $result[\'refresh_token\']; $openid = $result[\'openid\']; //请求微信接口,获取用户信息 $userInfo = $this->getUserInfo($access_token,$openid); $user_check = WechatUser::find()->where([\'openid\'=>$openid])->one(); if ($user_check) { //更新用户资料 } else { //保存用户资料 } //前端网页的重定向 if ($openid) { return $this->redirect($state.$openid); } else { return $this->redirect($state); } }
2.从微信获取用户资料
public function getUserInfo($access_token,$openid) { $request_url = \'https://api.weixin.qq.com/sns/userinfo?access_token=\'.$access_token.\'&openid=\'.$openid.\'&lang=zh_CN\'; //初始化一个curl会话 $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $request_url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $result = curl_exec($ch); curl_close($ch); $result = $this->response($result); return $result; }
3.获取用户资料接口
public function actionUserinfo() { if(isset($_REQUEST["openid"])){ $openid = $_REQUEST["openid"]; $user = WechatUser::find()->where([\'openid\'=>$openid])->one(); if ($user) { $result[\'error\'] = 0; $result[\'msg\'] = \'获取成功\'; $result[\'user\'] = $user; } else { $result[\'error\'] = 1; $result[\'msg\'] = \'没有该用户\'; } } else { $result[\'error\'] = 1; $result[\'msg\'] = \'openid为空\'; } return $result; }
微信支付
1.微信支付接口:打包支付数据
public function actionPay(){ if(isset($_REQUEST["uid"])&&isset($_REQUEST["oid"])&&isset($_REQUEST["totalFee"])){ //uid、oid、totalFee $uid = $_REQUEST["uid"]; $oid = $_REQUEST["oid"]; $totalFee = $_REQUEST["totalFee"]; $timestamp = time(); //微信支付参数 $appid = Yii::$app->params[\'wechat\'][\'appid\']; $mchid = Yii::$app->params[\'wechat\'][\'mchid\']; $key = Yii::$app->params[\'wechat\'][\'key\']; $notifyUrl = Yii::$app->params[\'wechat\'][\'notifyUrl\']; //支付打包 $wx_pay = new WechatPay($mchid, $appid, $key); $package = $wx_pay->createJsBizPackage($uid, $totalFee, $oid, $notifyUrl, $timestamp); $result[\'error\'] = 0; $result[\'msg\'] = \'支付打包成功\'; $result[\'package\'] = $package; return $result; }else{ $result[\'error\'] = 1; $result[\'msg\'] = \'请求参数错误\'; } return $result; }
2.接收微信发送的异步支付结果通知
public function actionNotify(){ $postStr = $GLOBALS["HTTP_RAW_POST_DATA"]; $postObj = simplexml_load_string($postStr, \'SimpleXMLElement\', LIBXML_NOCDATA); // if ($postObj === false) { die(\'parse xml error\'); } if ($postObj->return_code != \'SUCCESS\') { die($postObj->return_msg); } if ($postObj->result_code != \'SUCCESS\') { die($postObj->err_code); } //微信支付参数 $appid = Yii::$app->params[\'wechat\'][\'appid\']; $mchid = Yii::$app->params[\'wechat\'][\'mchid\']; $key = Yii::$app->params[\'wechat\'][\'key\']; $wx_pay = new WechatPay($mchid, $appid, $key); //验证签名 $arr = (array)$postObj; unset($arr[\'sign\']); if ($wx_pay->getSign($arr, $key) != $postObj->sign) { die("签名错误"); } //支付处理正确-判断是否已处理过支付状态 $orders = Order::find()->where([\'uid\'=>$postObj->openid, \'oid\'=>$postObj->out_trade_no, \'status\' => 0])->all(); if(count($orders) > 0){ //更新订单状态 foreach ($orders as $order) { //更新订单 $order[\'status\'] = 1; $order->update(); } return \'<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>\'; } else { //订单状态已更新,直接返回 return \'<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>\'; } }
3.微信支付类 WechatPay.php
<?php namespace api\sdk; use Yii; class WechatPay { protected $mchid; protected $appid; protected $key; public function __construct($mchid, $appid, $key){ $this->mchid = $mchid; $this->appid = $appid; $this->key = $key; } public function createJsBizPackage($openid, $totalFee, $outTradeNo, $orderName, $notifyUrl, $timestamp){ $config = array( \'mch_id\' => $this->mchid, \'appid\' => $this->appid, \'key\' => $this->key, ); $unified = array( \'appid\' => $config[\'appid\'], \'attach\' => \'支付\', \'body\' => $orderName, \'mch_id\' => $config[\'mch_id\'], \'nonce_str\' => self::createNonceStr(), \'notify_url\' => $notifyUrl, \'openid\' => $openid, \'out_trade_no\' => $outTradeNo, \'spbill_create_ip\' => \'127.0.0.1\', \'total_fee\' => intval($totalFee * 100), \'trade_type\' => \'JSAPI\', ); $unified[\'sign\'] = self::getSign($unified, $config[\'key\']); $responseXml = self::curlPost(\'https://api.mch.weixin.qq.com/pay/unifiedorder\', self::arrayToXml($unified)); $unifiedOrder = simplexml_load_string($responseXml, \'SimpleXMLElement\', LIBXML_NOCDATA); if ($unifiedOrder === false) { die(\'parse xml error\'); } if ($unifiedOrder->return_code != \'SUCCESS\') { die($unifiedOrder->return_msg); } if ($unifiedOrder->result_code != \'SUCCESS\') { die($unifiedOrder->err_code); } $arr = array( "appId" => $config[\'appid\'], "timeStamp" => $timestamp, "nonceStr" => self::createNonceStr(), "package" => "prepay_id=" . $unifiedOrder->prepay_id, "signType" => \'MD5\', ); $arr[\'paySign\'] = self::getSign($arr, $config[\'key\']); return $arr; } public static function curlGet($url = \'\', $options = array()){ $ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_TIMEOUT, 30); if (!empty($options)) { curl_setopt_array($ch, $options); } //https请求 不验证证书和host curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); $data = curl_exec($ch); curl_close($ch); return $data; } public static function curlPost($url = \'\', $postData = \'\', $options = array()){ if (is_array($postData)) { $postData = http_build_query($postData); } $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, $postData); curl_setopt($ch, CURLOPT_TIMEOUT, 30); //设置cURL允许执行的最长秒数 if (!empty($options)) { curl_setopt_array($ch, $options); } //https请求 不验证证书和host curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); $data = curl_exec($ch); curl_close($ch); return $data; } public static function createNonceStr($length = 16){ $chars = \'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\'; $str = \'\'; for ($i = 0; $i<$length; $i++){ $str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1); } return $str; } public static function arrayToXml($arr){ $xml = "<xml>"; foreach ($arr as $key => $val){ if (is_numeric($val)) { $xml .= "<" . $key . ">" . $val . "</" . $key . ">"; } else { $xml .= "<" . $key . "><![CDATA[" . $val . "]]></" . $key . ">"; } } $xml .= "</xml>"; return $xml; } public static function getSign($params, $key){ ksort($params, SORT_STRING); $unSignParaString = self::formatQueryParaMap($params, false); $signStr = strtoupper(md5($unSignParaString . "&key=" . $key)); return $signStr; } protected static function formatQueryParaMap($paraMap, $urlEncode = false){ $buff = ""; ksort($paraMap); foreach ($paraMap as $k => $v){ if (null != $v && "null" != $v) { if ($urlEncode) { $v = urlencode($v); } $buff .= $k . "=" . $v . "&"; } } $reqPar = \'\'; if (strlen($buff)>0) { $reqPar = substr($buff, 0, strlen($buff) - 1); } return $reqPar; } }
获取JS-SDK的config参数
根据微信公众平台开发者文档:
所有需要使用JS-SDK的页面必须先注入配置信息,否则将无法调用(同一个url仅需调用一次,对于变化url的SPA的web app可在每次url变化时进行调用,目前Android微信客户端不支持pushState的H5新特性,所以使用pushState来实现web app的页面会导致签名失败,此问题会在Android6.2中修复)。
即:
wx.config({ debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。 appId: \'\', // 必填,公众号的唯一标识 timestamp: , // 必填,生成签名的时间戳 nonceStr: \'\', // 必填,生成签名的随机串 signature: \'\',// 必填,签名,见附录1 jsApiList: [] // 必填,需要使用的JS接口列表,所有JS接口列表见附录2 });
1.微信支付类 WechatPay.php
<?php namespace api\sdk; use Yii; class WechatPay { public function getSignPackage($url) { $jsapiTicket = self::getJsApiTicket(); $timestamp = time(); $nonceStr = self::createNonceStr(); // 这里参数的顺序要按照 key 值 ASCII 码升序排序 $string = "jsapi_ticket=".$jsapiTicket."&noncestr=".$nonceStr."×tamp=".$timestamp."&url=".$url; $signature = sha1($string); $signPackage = array( "appId" => $this->appid, "nonceStr" => $nonceStr, "timestamp" => $timestamp, "url" => $url, "signature" => $signature, "rawString" => $string ); return $signPackage; } public static function getJsApiTicket() { //使用Redis缓存 jsapi_ticket $redis = Yii::$app->redis; $redis_ticket = $redis->get(\'wechat:jsapi_ticket\'); if ($redis_ticket) { $ticket = $redis_ticket; } else { $accessToken = self::getAccessToken(); $url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?type=jsapi&access_token=".$accessToken; $res = json_decode(self::curlGet($url)); $ticket = $res->ticket; if ($ticket) { $redis->set(\'wechat:jsapi_ticket\', $ticket); $redis->expire(\'wechat:jsapi_ticket\', 7000); } } return $ticket; } public static function getAccessToken() { //使用Redis缓存 access_token $redis = Yii::$app->redis; $redis_token = $redis->get(\'wechat:access_token\'); if ($redis_token) { $access_token = $redis_token; } else { $appid = Yii::$app->params[\'wechat\'][\'appid\']; $appsecret = Yii::$app->params[\'wechat\'][\'appsecret\']; $url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=".$appid."&secret=".$appsecret; $res = json_decode(self::curlGet($url)); $access_token = $res->access_token; if ($access_token) { $redis->set(\'wechat:access_token\', $access_token); $redis->expire(\'wechat:access_token\', 7000); } } return $access_token; } public static function curlGet($url = \'\', $options = array()){ $ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_TIMEOUT, 30); if (!empty($options)) { curl_setopt_array($ch, $options); } //https请求 不验证证书和host curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); $data = curl_exec($ch); curl_close($ch); return $data; } public static function curlPost($url = \'\', $postData = \'\', $options = array()){ if (is_array($postData)) { $postData = http_build_query($postData); } $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, $postData); curl_setopt($ch, CURLOPT_TIMEOUT, 30); //设置cURL允许执行的最长秒数 if (!empty($options)) { curl_setopt_array($ch, $options); } //https请求 不验证证书和host curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); $data = curl_exec($ch); curl_close($ch); return $data; } public static function createNonceStr($length = 16){ $chars = \'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\'; $str = \'\'; for ($i = 0; $i<$length; $i++){ $str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1); } return $str; } }
2.获取config参数接口
public function actionConfig(){ if (isset($_REQUEST[\'url\'])) { $url = $_REQUEST[\'url\']; //微信支付参数 $appid = Yii::$app->params[\'wechat\'][\'appid\']; $mchid = Yii::$app->params[\'wechat\'][\'mchid\']; $key = Yii::$app->params[\'wechat\'][\'key\']; $wx_pay = new WechatPay($mchid, $appid, $key); $package = $wx_pay->getSignPackage($url); $result[\'error\'] = 0; $result[\'msg\'] = \'获取成功\'; $result[\'config\'] = $package; } else { $result[\'error\'] = 1; $result[\'msg\'] = \'参数错误\'; } return $result; }