ThinkPHP5集成微信公众号接口开发
一、使用方法
首先使用控制器所在命名空间,我这个控制器放在公共模块下:
use app\common\controller\Weixin;实例化:
$weixin = new Weixin;OAuth 授权跳转接口:
$get = input('get.'); #带上当前请求的参数 $get_var_param = http_build_query($get); #授权成功跳转地址 $url = 'http://' . $_SERVER['HTTP_HOST'] . '/index/login/dologin?' . $get_var_param; #OAuth 授权跳转接口 获取code $wx_url = $weixin->getOAuthRedirect($url,'','snsapi_userinfo'); $this->redirect($wx_url);获取用户信息:
#通过code获取Access Token $jsonStr=$weixin->getOauthAccessToken(); #网页获取用户信息(没有关注公众号也可以获取用户基本信息) $info=$weixin->getOauthUserInfo($jsonStr['access_token'],$jsonStr["openid"]);
二、接口集成控制器代码
<?php namespace app\common\controller; use think\Controller; class Weixin extends Controller { /* 获取ACCESS_TOKEN URL */ const AUTH_URL = 'https://api.weixin.qq.com/cgi-bin/token'; /* 用户及用户分组URL */ const USER_GET_URL = 'https://api.weixin.qq.com/cgi-bin/user/get'; const USER_INFO_URL = 'https://api.weixin.qq.com/cgi-bin/user/info'; /* 二维码生成 URL*/ const QRCODE_URL = 'https://api.weixin.qq.com/cgi-bin/qrcode/create'; const QRCODE_SHOW_URL = 'https://mp.weixin.qq.com/cgi-bin/showqrcode'; /* OAuth2.0授权地址 */ const OAUTH_AUTHORIZE_URL = 'https://open.weixin.qq.com/connect/oauth2/authorize'; const OAUTH_USER_TOKEN_URL = 'https://api.weixin.qq.com/sns/oauth2/access_token'; const OAUTH_GET_USERINFO = 'https://api.weixin.qq.com/sns/userinfo'; /* 获取模板ID */ const GET_TEMPLATE_ID = 'https://api.weixin.qq.com/cgi-bin/template/api_add_template'; /* 消息模板 */ const TEMPLATE_SEND = 'https://api.weixin.qq.com/cgi-bin/message/template/send'; /* JSAPI_TICKET获取地址 */ const JSAPI_TICKET_URL = 'https://api.weixin.qq.com/cgi-bin/ticket/getticket'; /* 企业付款 用户提现*/ const PAY_TRANSFERS_URL = 'https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers'; private $account; private $token; private $appid; private $secret; private $access_token; private $error; private $ticket; private $result; private $mch_id; private $payKey; private $pemCret; private $pemKey; public function __construct($options = array()) { $this->account = ''; $this->token = ''; $this->appid = isset($options['appid'])?$options['appid']:config('wx_appid');// 微信appid,读取配置 $this->secret = isset($options['secret'])?$options['secret']:config('wx_appsecret');// 微信appsecret,读取配置; $this->pem = 'apiclient'; } /** * 动态设置参数 * @param string $config 配置名称 * @param string $value 配置内容 */ public function setConfig($config, $value) { $this->$config = $value; } public function __get($key) { return $this->$key; } public function __set($key, $value) { $this->$key = $value; } /** * 取得 access_token * @return string|boolean */ public function getToken() { $access_token = $this->access_token; if (!empty($access_token)) { return $this->access_token; }else { if ($this->getAccessToken()) { return $this->access_token; }else { return false; } } } /** * 从远端接口获取ACCESS_TOKEN * @return string|boolean */ private function getAccessToken() { $data = db('token')->where(array('type'=>1))->find(); if ($data['expire_time'] < time()){ $params = array( 'grant_type' => 'client_credential', 'appid' => $this->appid, 'secret' => $this->secret ); $jsonStr = $this->http(self::AUTH_URL, $params); if ($jsonStr) { $jsonArr = $this->parseJson($jsonStr); if ($jsonArr) { $data['type'] = 1; $data['expire_time'] = time() + 3600; $data['access_token'] = $jsonArr['access_token']; /*写入数据库*/ if(db('token')->where(array('type'=>1))->update($data) == 0){ db('token')->insertGetId($data); } return $this->access_token = $jsonArr['access_token']; }else { return false; } }else { return false; } }else{ return $this->access_token = $data['access_token']; } } /** * 获取jsapi_ticket */ public function getJsapiTicket() { $data = db('token')->where(array('type'=>2))->find(); if ($data['expire_time'] < time()){ $params = array( 'access_token' => $this->access_token, 'type' => 'jsapi' ); $jsonStr = $this->http(self::JSAPI_TICKET_URL, $params); $jsonArr = $this->parseJson($jsonStr); if ($jsonArr) { $data['type'] = 2; $data['expire_time'] = time() + 3600; $data['access_token'] = $this->result['ticket']; /*写入数据库*/ if(db('token')->where(array('type'=>2))->update($data) == 0){ db('token')->insertGetId($data); } return $this->ticket = $this->result['ticket']; }else { return false; } }else{ return $this->ticket = $data['access_token']; } } /* * JS-SDK签名算法 */ public function getSignPackage() { $jsapiTicket = $this->getJsapiTicket(); $url = 'http://'.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI']; $timestamp = time(); $nonceStr = self::_getRandomStr(); // 这里参数的顺序要按照 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; } /** * 获取关注者列表 * @param sting $next_openid 第一个拉取的OPENID,不填默认从头开始拉取 * @return array|boolean 返回用户信息的一个数组 */ public function users($next_openid = '') { !empty($next_openid) && $params['next_openid'] = $next_openid; $params['access_token'] = $this->access_token; $jsonStr = $this->http(self::USER_GET_URL, $params); $jsonArr = $this->parseJson($jsonStr); if ($jsonArr) { //优化返回数组的结构 $openId = $jsonArr['data']['openid']; unset($jsonArr['data']); while ($jsonArr['count'] == 10000) { $next = self::users($jsonArr['next_openid']); $openId = array_merge($openId, $next); } unset($jsonArr['count']); unset($jsonArr['next_openid']); $jsonArr['openid'] = $openId; return $jsonArr; }else { return false; } } /** * 获取用户基本信息 * @param string $openid 用户的OPENID * @return array|boolean 返回用户信息的一个数组 */ public function user($openid = '') { if (empty($openid)) { $this->error = '请输入一个用户的OpenID'; return false; } $params = array( 'access_token' => $this->access_token, 'lang' => 'zh_CN', 'openid' => $openid ); $jsonStr = $this->http(self::USER_INFO_URL, $params); $jsonArr = $this->parseJson($jsonStr); return $jsonArr; if ($jsonArr['subscribe'] == 1) { return true; } else { // $this->error = '用户未关注'; return false; } } /** * XML文档解析成数组,并将键值转成小写 * @param xml $xml * @return array */ private function _extractXml($xml) { $data = (array)simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA); return array_change_key_case($data, CASE_LOWER); } /** * 对数据进行SHA1签名 * @return string */ public function getSHA1($encrypt_msg, $nonce = '') { $array = array($encrypt_msg, $this->token, NOW_TIME, $nonce); sort($array, SORT_STRING); $str = implode($array); return sha1($str); } /** * 将数组转换成XML */ private function _array2Xml($array) { $xml = new \SimpleXMLElement('<xml></xml>'); $this->_data2xml($xml, $array); return $xml->asXML(); } /** * 数据XML编码 * @param object $xml XML对象 * @param mixed $data 数据 * @param string $item 数字索引时的节点名称 * @return string xml */ private function _data2xml($xml, $data, $item = 'item') { foreach ($data as $key => $value) { /* 指定默认的数字key */ is_numeric($key) && $key = $item; /* 添加子元素 */ if(is_array($value) || is_object($value)){ $child = $xml->addChild($key); $this->_data2xml($child, $value, $item); } else { if(is_numeric($value)){ $child = $xml->addChild($key, $value); } else { $child = $xml->addChild($key); $node = dom_import_simplexml($child); $node->appendChild($node->ownerDocument->createCDATASection($value)); } } } } /** * 获取模板ID * @param string $tplId MP中的模板编号 * @return string 模板ID */ public function getTemplateId($tplId) { $params = array( 'template_id_short:' => $tplId ); $params = json_encode($params); $url = self::GET_TEMPLATE_ID . '?access_token=' . $this->access_token; $jsonStr = $this->http($url, $params, 'POST'); $jsonArr = $this->parseJson($jsonStr); if ($jsonArr) { return $jsonArr['template_id']; }else { return false; } } /** * 发送模板消息 * @param array * @return boolean */ public function sendTemplate($content) { $params = self::json_encode($content); $url = self::TEMPLATE_SEND . '?access_token=' . $this->access_token; $jsonStr = $this->http($url, $params, 'POST'); $jsonArr = $this->parseJson($jsonStr); if ($jsonArr) { return true; }else { return false; } } /** * OAuth 授权跳转接口 * @param string $callback 回调URI,填写完整地址,带http:// * @param sting $state 重定向后会带上state参数,开发者可以填写a-zA-Z0-9的参数值 * @param string snsapi_userinfo获取用户授权信息,snsapi_base直接返回openid * @return string */ public function getOAuthRedirect($callback, $state='', $scope='snsapi_base'){ return self::OAUTH_AUTHORIZE_URL.'?appid='.$this->appid.'&redirect_uri='.urlencode($callback).'&response_type=code&scope='.$scope.'&state='.$state.'#wechat_redirect'; } /** * 通过code获取Access Token * @return array|boolean */ public function getOauthAccessToken(){ $code = isset($_GET['code']) ? $_GET['code'] : ''; if (!$code) return false; $params = array( 'appid' => $this->appid, 'secret'=> $this->secret, 'code' => $code, 'grant_type' => 'authorization_code' ); $jsonStr = $this->http(self::OAUTH_USER_TOKEN_URL, $params); $jsonArr = $this->parseJson($jsonStr); if ($jsonArr) { return $jsonArr; }else { return false; } } /** * 网页获取用户信息(没有关注公众号也可以获取用户基本信息) * @param string $access_token 通过getOauthAccessToken方法获取到的token * @param string $openid 用户的OPENID * @return array */ public function getOauthUserInfo($access_token, $openid) { $params = array( 'access_token' => $access_token, 'openid' => $openid, 'lang' => 'zh_CN' ); $jsonStr = $this->http(self::OAUTH_GET_USERINFO, $params); $jsonArr = $this->parseJson($jsonStr); if ($jsonArr) { return $jsonArr; }else { return false; } } /** * 不转义中文字符和\/的 json 编码方法 * @param array $array * @return json */ private function json_encode($array = array()) { $array = str_replace("\\/", "/", json_encode($array)); $search = '#\\\u([0-9a-f]+)#ie'; if (strpos(strtoupper(PHP_OS), 'WIN') === false) { $replace = "iconv('UCS-2BE', 'UTF-8', pack('H4', '\\1'))";//LINUX } else { $replace = "iconv('UCS-2', 'UTF-8', pack('H4', '\\1'))";//WINDOWS } return preg_replace($search, $replace, $array); } /** * 解析JSON编码,如果有错误,则返回错误并设置错误信息d * @param json $json json数据 * @return array */ private function parseJson($json) { $jsonArr = json_decode($json, true); if (isset($jsonArr['errcode'])) { if ($jsonArr['errcode'] == 0) { $this->result = $jsonArr; return true; } else { $this->error = $this->ErrorCode($jsonArr['errcode']); return false; } }else { return $jsonArr; } } /** * @param appid 是 公众号的唯一标识 * @param redirect_uri 是 授权后重定向的回调链接地址,请使用urlencode对链接进行处理 * @param response_type 是 返回类型,请填写code * @param scope 是 应用授权作用域,snsapi_base (不弹出授权页面,直接跳转,只能获取用户openid),snsapi_userinfo (未关注也可以得到信息) * @param state 否 重定向后会带上state参数,开发者可以填写a-zA-Z0-9的参数值 * @param fun 授权成功以后的地址 * #wechat_redirect 是 无论直接打开还是做页面302重定向时候,必须带此参数 */ public function oauth2($userid = '', $scope='snsapi_base',$fun){ $arr = array( "appid" => $this->appid, "redirect_uri" => 'http://wx.cnskl.com/'.$fun, "response_type" => 'code', "scope" => $scope, 'state' => $userid ); return self::OAUTH_AUTHORIZE_URL . '?' . http_build_query($arr).'#wechat_redirect'; } /** * AES 解密方法 * @param string $encrypted 加密后的字符串 * @return xml|boolean */ public function AESdecode($encrypted) { $key = base64_decode($this->AESKey . "="); // 使用BASE64对需要解密的字符串进行解码 $ciphertext_dec = base64_decode($encrypted); $module = mcrypt_module_open(MCRYPT_RIJNDAEL_128, '', MCRYPT_MODE_CBC, ''); $iv = substr($key, 0, 16); mcrypt_generic_init($module, $key, $iv); // 解密 $decrypted = mdecrypt_generic($module, $ciphertext_dec); mcrypt_generic_deinit($module); mcrypt_module_close($module); // 去除补位字符 $pad = ord(substr($decrypted, -1)); if ($pad < 1 || $pad > 32) { $pad = 0; } $result = substr($decrypted, 0, (strlen($decrypted) - $pad)); // 去除16位随机字符串,网络字节序和AppId if (strlen($result) < 16) { $this->error = 'AESdecode Result Length Error'; return false; } $content = substr($result, 16); $len_list = unpack("N", substr($content, 0, 4)); $xml_len = $len_list[1]; $xml_content = substr($content, 4, $xml_len); $from_appid = substr($content, $xml_len + 4); if ($from_appid != $this->appid) { $this->errir = 'AESdecode AppId Error'; return false; } else { return self::_extractXml($xml_content); } } /** * AES 加密方法 * @param string $text 需要加密的字符串 * @return boolean */ public function AESencode($text) { $key = base64_decode($this->AESKey . "="); $random = self::_getRandomStr(); $text = $random . pack("N", strlen($text)) . $text . $this->appid; $size = mcrypt_get_block_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC); $module = mcrypt_module_open(MCRYPT_RIJNDAEL_128, '', MCRYPT_MODE_CBC, ''); $iv = substr($key, 0, 16); // 使用自定义的填充方式对明文进行补位填充 $text_length = strlen($text); //计算需要填充的位数 $amount_to_pad = 32 - ($text_length % 32); if ($amount_to_pad == 0) { $amount_to_pad = 32; } //获得补位所用的字符 $pad_chr = chr($amount_to_pad); $tmp = ""; for ($index = 0; $index < $amount_to_pad; $index++) { $tmp .= $pad_chr; } $text = $text . $tmp; mcrypt_generic_init($module, $key, $iv); // 加密 $encrypted = mcrypt_generic($module, $text); mcrypt_generic_deinit($module); mcrypt_module_close($module); // 使用BASE64对加密后的字符串进行编码 return base64_encode($encrypted); } /** * 返回随机填充的字符串 */ private function _getRandomStr($lenght = 16) { $str_pol = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz"; return substr(str_shuffle($str_pol), 0, $lenght); } /** * 发送HTTP请求方法,目前只支持CURL发送请求 * @param string $url 请求URL * @param array $params 请求参数 * @param string $method 请求方法GET/POST * @param boolean $ssl 是否进行SSL双向认证 * @return array $data 响应数据 */ private function http($url, $params = array(), $method = 'GET', $ssl = false){ $opts = array( CURLOPT_TIMEOUT => 30, CURLOPT_RETURNTRANSFER => 1, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYHOST => false ); /* 根据请求类型设置特定参数 */ switch(strtoupper($method)){ case 'GET': $getQuerys = !empty($params) ? '?'. http_build_query($params) : ''; $opts[CURLOPT_URL] = $url . $getQuerys; break; case 'POST': $opts[CURLOPT_URL] = $url; $opts[CURLOPT_POST] = 1; $opts[CURLOPT_POSTFIELDS] = $params; break; } if ($ssl) { $pemPath = dirname(SITE_PATH).'/Wx/Apps/Common/Library/WxPayPubHelper/cert/'; $pemCret = $pemPath.$this->pem.'_cert.pem'; $pemKey = $pemPath.$this->pem.'_key.pem'; if (!file_exists($pemCret)) { $this->error = '证书不存在'; return false; } if (!file_exists($pemKey)) { $this->error = '密钥不存在'; return false; } $opts[CURLOPT_SSLCERTTYPE] = 'PEM'; $opts[CURLOPT_SSLCERT] = $pemCret; $opts[CURLOPT_SSLKEYTYPE] = 'PEM'; $opts[CURLOPT_SSLKEY] = $pemKey; } /* 初始化并执行curl请求 */ $ch = curl_init(); curl_setopt_array($ch, $opts); $data = curl_exec($ch); $err = curl_errno($ch); $errmsg = curl_error($ch); curl_close($ch); if ($err > 0) { $this->error = $errmsg; return false; }else { return $data; } } /** * 创建一个商户订单号 * @return integer 28位订单号 */ private function createMchBillNo() { $micro = microtime(true) * 100; $micro = ceil($micro); $rand = substr($micro, -8) . sprintf("%02d", mt_rand(0, 99)); return $this->mch_id . date('Ymd') . $rand; } /** * 向用户付款 * @param string $openid 收款用户OPENID * @param float $amount 付款金额 * @param string $check_name 真实姓名校验 * NO_CHECK:不校验真实姓名 * FORCE_CHECK:强校验真实姓名(未实名认证的用户会校验失败,无法转账) * OPTION_CHECK:针对已实名认证的用户才校验真实姓名(未实名认证用户不校验,可以转账成功) * @param string $desc 描述 * @return boolean|array */ public function transfers($openid, $amount, $desc, $check_name = 'NO_CHECK') { $params['openid'] = $openid; if ($check_name == 'NO_CHECK') { $params['check_name'] = $check_name; } else { $params['check_name'] = 'OPTION_CHECK'; $params['re_user_name'] = $check_name; } $params['amount'] = $amount * 100; $params['desc'] = $desc; $params['spbill_create_ip'] = get_client_ip(); $params['partner_trade_no'] = self::createMchBillNo(); //商户订单号 $params['mch_appid'] = $this->appid; //商户appid $params['mchid'] = $this->mch_id; //商户号 $params['nonce_str'] = self::_getRandomStr(); //随机字符串 $params['sign'] = self::_getOrderMd5($params);//签名 $data = self::_array2Xml($params); $data = $this->http(self::PAY_TRANSFERS_URL, $data, 'POST', true); return self::parsePayRequest($data); } /** * 解析支付接口的返回结果 */ public function parsePayRequest($data) { $data = self::_extractXml($data); if (empty($data)) { $this->error = '支付返回内容解析失败'; return false; } // 有返回结果 并且是SUCCESS的时候 if ($data['return_code'] == 'SUCCESS') { if (!self::_checkSign($data)) { return false; } elseif ($data['result_code'] == 'SUCCESS') { return $data; } else { $this->error = $data['err_code']; return false; } } else { $this->error = $data['return_msg']; return false; } return true; } /** * 接口通知接收 */ public function getNotify() { $data = $GLOBALS["HTTP_RAW_POST_DATA"]; return self::parsePayRequest($data); } /** * 接收数据签名校验 */ private function _checkSign($data) { // $sign = $data['sign']; // unset($data['sign']); // echo $sign; // if (self::_getOrderMd5($data) != $sign) { // $this->error = '签名校验失败'; // return false; // } else { // return true; // } return true; } /** * 本地MD5签名 */ private function _getOrderMd5($params) { ksort($params); $params['key'] = $this->payKey; return strtoupper(md5(urldecode(http_build_query($params)))); } /** * 捕获错误信息 * @return string 中文错误信息 * @author 、小陈叔叔 <cjango@163.com> */ public function getError() { return $this->error; } /** * 获取全局返回错误码 * @param integer $code 错误码 * @return string 错误信息 * @author 、小陈叔叔 <cjango@163.com> */ private function ErrorCode($code) { switch ($code) { case -1 : return '系统繁忙 '; case 40001 : return '获取access_token时AppSecret错误,或者access_token无效 '; case 40002 : return '不合法的凭证类型'; case 40003 : return '不合法的OpenID '; case 40004 : return '不合法的媒体文件类型'; case 40005 : return '不合法的文件类型'; case 40006 : return '不合法的文件大小'; case 40007 : return '不合法的媒体文件id '; case 40008 : return '不合法的消息类型 '; case 40009 : return '不合法的图片文件大小'; case 40010 : return '不合法的语音文件大小'; case 40011 : return '不合法的视频文件大小'; case 40012 : return '不合法的缩略图文件大小'; case 40013 : return '不合法的APPID'; case 40014 : return '不合法的access_token '; case 40015 : return '不合法的菜单类型 '; case 40016 : return '不合法的按钮个数 '; case 40017 : return '不合法的按钮个数'; case 40018 : return '不合法的按钮名字长度'; case 40019 : return '不合法的按钮KEY长度 '; case 40020 : return '不合法的按钮URL长度 '; case 40021 : return '不合法的菜单版本号'; case 40022 : return '不合法的子菜单级数'; case 40023 : return '不合法的子菜单按钮个数'; case 40024 : return '不合法的子菜单按钮类型'; case 40025 : return '不合法的子菜单按钮名字长度'; case 40026 : return '不合法的子菜单按钮KEY长度 '; case 40027 : return '不合法的子菜单按钮URL长度 '; case 40028 : return '不合法的自定义菜单使用用户'; case 40029 : return '不合法的oauth_code'; case 40030 : return '不合法的refresh_token'; case 40031 : return '不合法的openid列表 '; case 40032 : return '不合法的openid列表长度 '; case 40033 : return '不合法的请求字符,不能包含\uxxxx格式的字符 '; case 40035 : return '不合法的参数'; case 40038 : return '不合法的请求格式'; case 40039 : return '不合法的URL长度 '; case 40050 : return '不合法的分组id'; case 40051 : return '分组名字不合法'; case 41001 : return '缺少access_token参数'; case 41002 : return '缺少appid参数'; case 41003 : return '缺少refresh_token参数'; case 41004 : return '缺少secret参数'; case 41005 : return '缺少多媒体文件数据'; case 41006 : return '缺少media_id参数'; case 41007 : return '缺少子菜单数据'; case 41008 : return '缺少oauth code'; case 41009 : return '缺少openid'; case 42001 : return 'access_token超时'; case 42002 : return 'refresh_token超时'; case 42003 : return 'oauth_code超时'; case 43001 : return '需要GET请求'; case 43002 : return '需要POST请求'; case 43003 : return '需要HTTPS请求'; case 43004 : return '需要接收者关注'; case 43005 : return '需要好友关系'; case 44001 : return '多媒体文件为空'; case 44002 : return 'POST的数据包为空'; case 44003 : return '图文消息内容为空'; case 44004 : return '文本消息内容为空'; case 45001 : return '多媒体文件大小超过限制'; case 45002 : return '消息内容超过限制'; case 45003 : return '标题字段超过限制'; case 45004 : return '描述字段超过限制'; case 45005 : return '链接字段超过限制'; case 45006 : return '图片链接字段超过限制'; case 45007 : return '语音播放时间超过限制'; case 45008 : return '图文消息超过限制'; case 45009 : return '接口调用超过限制'; case 45010 : return '创建菜单个数超过限制'; case 45015 : return '回复时间超过限制'; case 45016 : return '系统分组,不允许修改'; case 45017 : return '分组名字过长'; case 45018 : return '分组数量超过上限'; case 46001 : return '不存在媒体数据'; case 46002 : return '不存在的菜单版本'; case 46003 : return '不存在的菜单数据'; case 46004 : return '不存在的用户'; case 47001 : return '解析JSON/XML内容错误'; case 48001 : return 'api功能未授权'; case 50001 : return '用户未授权该api'; default : return '未知错误'; } } }