Wechat.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. <?php
  2. namespace app\common\controller;
  3. use think\Config;
  4. use think\Db;
  5. use think\Exception;
  6. use think\exception\DbException;
  7. use think\exception\PDOException;
  8. /**
  9. * 微信公众平台基类
  10. * Class Wechat
  11. * @package app\common\controller
  12. * @version 1.0
  13. * @author fuyelk <fuyelk@fuyelk.com>
  14. */
  15. class Wechat
  16. {
  17. public $appid = 'wx81f7b48cef8b4cfb';
  18. public $appsecret = 'e5d87a1aed0a553838286012a5685800';
  19. private $access_token = '54_XqfLmdbsNdBLQlCbxMNto2vBcDnQjUf9Uxt_U6CHXiPgtdlwP2ju3KP5DqK0UT6wsxKAHhw0bGcI5og1K25RFGH9v0AT7hnN-WFcJa0vaQJgcXzqqE75zbWHn5qG6HdNq-rFbGn60AK9peTwLYReAGAVIB';
  20. private $expires_time = 72000;
  21. private $_error = '';
  22. /**
  23. * WeChat 构造函数
  24. * @param boolean $refreshToken 为真,则刷新token
  25. */
  26. public function __construct($refreshToken = false)
  27. {
  28. $config = $this->getConfig();
  29. if (empty($config)) {
  30. exit($this->_error);
  31. }
  32. //var_dump($config);die;
  33. if (empty($config['appid']) || empty($config['appsecret'])) {
  34. exit('微信配置为空');
  35. }
  36. $this->appid = $config['appid'];
  37. $this->appsecret = $config['appsecret'];
  38. // 检查access_token是否有效
  39. if ($refreshToken || empty($config['access_token']) || $config['access_token_expire_time'] < strtotime('+5 minute')) {
  40. if (!$this->refreshAccessToken()) {
  41. exit($this->_error);
  42. }
  43. } else {
  44. $this->access_token = $config['access_token'];
  45. $this->expires_time = $config['access_token_expire_time'];
  46. }
  47. }
  48. /**
  49. * 获取微信配置
  50. * @return array|bool
  51. * @author fuyelk <fuyelk@fuyelk.com>
  52. */
  53. private function getConfig()
  54. {
  55. // 检查有没有wechat表
  56. try {
  57. $config = Db::name('wechat')->column('value', 'name');
  58. } catch (PDOException $e) {
  59. // 表不存在
  60. if ('1146' == $e->getData()['PDO Error Info']['Driver Error Code']) {
  61. // 创建表
  62. $prefix = Config::get('database.prefix');
  63. $sqlCreateTable = "CREATE TABLE `{$prefix}wechat` (`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',`name` varchar(100) CHARACTER SET utf8mb4 NOT NULL COMMENT '数据名',`value` varchar(255) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '值',`description` text CHARACTER SET utf8mb4 COMMENT '描述',`updatetime` int(10) DEFAULT NULL COMMENT '更新时间',PRIMARY KEY (`id`)) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='微信配置表';";
  64. $sqlInsertData = "insert into `{$prefix}wechat` (`id`, `name`, `value`, `description`, `updatetime`) values ('1','appid','','APPID',NULL),('2','appsecret','','APP SECRET',NULL),('3','access_token','','SCCESS TOKEN',NULL),('4','access_token_expire_time','','SCCESS TOKEN过期时间',NULL),('5','qrcode','','二维码',NULL);";
  65. try {
  66. Db::query($sqlCreateTable);
  67. Db::query($sqlInsertData);
  68. return Db::name('wechat')->column('value', 'name');
  69. } catch (Exception $e) {
  70. $this->_error = '微信配置表不存在,创建失败';
  71. return false;
  72. }
  73. }
  74. $this->_error = $e->getMessage();
  75. return false;
  76. } catch (DbException $e) {
  77. $this->_error = $e->getMessage();
  78. return false;
  79. }
  80. return $config;
  81. }
  82. /**
  83. * 向微信服务器请求获取accessToken,并存储
  84. * return bool
  85. */
  86. private function refreshAccessToken()
  87. {
  88. // 向微信服务器发access_token请求
  89. $data = $this->wechatRequest('https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=' . $this->appid . '&secret=' . $this->appsecret);
  90. if (empty($data['access_token'])) {
  91. $this->_error = 'access_token获取失败';
  92. return false;
  93. }
  94. $this->access_token = $data['access_token'];
  95. $this->expires_time = time() + $data['expires_in'];
  96. try {
  97. Db::name('wechat')->where('name', 'access_token')->update(['value' => $this->access_token, 'updatetime' => time()]);
  98. Db::name('wechat')->where('name', 'access_token_expire_time')->update(['value' => $this->expires_time, 'updatetime' => time()]);
  99. } catch (Exception $e) {
  100. $this->_error = $e->getMessage();
  101. return false;
  102. }
  103. return true;
  104. }
  105. /**
  106. * http请求
  107. * @param string $url http地址
  108. * @param string $method 请求方式
  109. * @param array $data 请求数据:
  110. * <pre>
  111. * $data = [
  112. * 'image' => new \CURLFile($filePath),
  113. * 'access_token' => 'this-is-access-token'
  114. * ...
  115. * ]
  116. * </pre>
  117. * @return bool|string
  118. * @throws \Exception
  119. * @author fuyelk <fuyelk@fuyelk.com>
  120. */
  121. public function http_request($url, $method = 'GET', $data = [])
  122. {
  123. $addHeader = [];
  124. $curl = curl_init();
  125. curl_setopt_array($curl, [
  126. CURLOPT_ACCEPT_ENCODING => 'gzip,deflate',
  127. CURLOPT_URL => $url,
  128. CURLOPT_CUSTOMREQUEST => strtoupper($method), // 请求方式
  129. CURLOPT_USERAGENT => "Mozilla / 5.0 (Windows NT 10.0; Win64; x64)",// 模拟常用浏览器的useragent
  130. CURLOPT_RETURNTRANSFER => true, // 获取的信息以文件流的形式返回,而不是直接输出
  131. CURLOPT_SSL_VERIFYPEER => false, // https请求不验证证书
  132. CURLOPT_SSL_VERIFYHOST => false, // https请求不验证hosts
  133. CURLOPT_MAXREDIRS => 10, // 最深允许重定向级数
  134. CURLOPT_CONNECTTIMEOUT => 10,// 最长等待连接成功时间
  135. CURLOPT_TIMEOUT => 30, // 最长等待响应完成时间
  136. ]);
  137. // 发送请求数据
  138. if ($data) {
  139. $data = json_encode($data, JSON_UNESCAPED_UNICODE);
  140. array_push($addHeader, 'Content-type:application/json');
  141. curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
  142. }
  143. curl_setopt($curl, CURLOPT_HTTPHEADER, $addHeader);
  144. $response = curl_exec($curl);
  145. $err = curl_error($curl);
  146. curl_close($curl);
  147. if ($err) throw new \Exception($err);
  148. return $response;
  149. }
  150. /**
  151. * 获取错误信息
  152. * @return string
  153. * @author doyle <doyle@doywb.com>
  154. * @date 2021/4/11 15:49
  155. */
  156. public function getError()
  157. {
  158. return $this->_error;
  159. }
  160. /**
  161. * 请求微信接口,并检查请求结果
  162. * @param string $url 接口地址
  163. * @param null $data [请求数据]
  164. * @param bool $callback [是否是回调]
  165. * @return bool|mixed|string
  166. * @author fuyelk <fuyelk@fuyelk.com>
  167. * @date 2021/07/18 22:49
  168. */
  169. public function wechatRequest($url, $data = null, $callback = false)
  170. {
  171. $url = $url . (strpos($url, '?') ? '&' : '?') . 'access_token=' . $this->access_token;
  172. try {
  173. if (!empty($data)) {
  174. $res = $this->http_request($url, 'POST', $data);
  175. } else {
  176. $res = $this->http_request($url);
  177. }
  178. } catch (\Exception $e) {
  179. $this->_error = $e->getMessage();
  180. return false;
  181. }
  182. $res = json_decode($res, true);
  183. // 接口出错
  184. if (!empty($res['errcode'])) {
  185. $log = [
  186. 'url' => $url,
  187. 'data' => $data,
  188. 'response' => $res
  189. ];
  190. // 非回调,则检查是否AccessToken错误
  191. if (!$callback && '40001' == $res['errcode'] && !empty($res['errmsg']) && strpos($res['errmsg'], 'access_token is invalid')) {
  192. // 刷新AccessToken
  193. if (!$this->refreshAccessToken()) {
  194. return false;
  195. }
  196. dta($log, '微信接口请求出错,AccessToken失效所致,正在尝试修复');
  197. // 重新请求微信接口
  198. return $this->wechatRequest($url, $data, true);
  199. }
  200. dta($log, '微信接口请求出错');
  201. if (!empty($res['errmsg'])) {
  202. $this->_error = $res['errmsg'];
  203. }
  204. return false;
  205. }
  206. return $res;
  207. }
  208. /**
  209. * 在公网接口处重写此方法:验证服务器有效性
  210. * @return int|mixed
  211. */
  212. public function serverValidation()
  213. {
  214. $TOKEN = 'milingerfuyelkdoywb';
  215. $signature = $_GET["signature"] ?? "";
  216. $timestamp = $_GET["timestamp"] ?? "";
  217. $nonce = $_GET["nonce"] ?? "";
  218. $tmpArr = array($TOKEN, $timestamp, $nonce);
  219. sort($tmpArr, SORT_STRING);
  220. $tmpStr = implode($tmpArr);
  221. $tmpStr = sha1($tmpStr);
  222. if ($tmpStr == $signature) {
  223. return $_GET["echostr"] ?? '';
  224. }
  225. return 'error';
  226. }
  227. /**
  228. * 在公网接口处重写此方法:引导用户进入授权页,并跳转到目标页
  229. * @param string $url 授权后需要跳转的页面地址,必须进行base64编码
  230. * @author fuyelk <fuyelk@fuyelk.com>
  231. */
  232. public function bootToUrl($url = '')
  233. {
  234. $redirect_uri = request()->domain() . '/index/wechat/wechatRedirect?redirect=' . $url;
  235. $appid = $this->wechat->appid;
  236. $redirect_uri = urlencode($redirect_uri);
  237. $scope = 'snsapi_base'; // 静默授权
  238. // $scope = 'snsapi_userinfo';
  239. $wechat_authorize = "http://2.ipfsfil168.com/get-weixin-code.html?appid={$appid}&redirect_uri={$redirect_uri}&response_type=code&scope={$scope}&state=123#wechat_redirect";
  240. $this->redirect($wechat_authorize);
  241. }
  242. /**
  243. * 在公网接口处重写此方法:用户授权后微信将用户重定向至此接口并携带code,
  244. * 使用code可获取access_token及用户openid,完成业务逻辑处理后拼接上openid将用户重定向指指定页面
  245. */
  246. public function wechatRedirect()
  247. {
  248. $code = input('code');
  249. $redirect = input('redirect');
  250. // 获取用户信息
  251. if ($code) {
  252. $wechat_url = 'https://api.weixin.qq.com/sns/oauth2/access_token' .
  253. '?appid=' . $this->wechat->appid .
  254. '&secret=' . $this->wechat->appsecret .
  255. '&code=' . input('code') .
  256. '&grant_type=authorization_code';
  257. try {
  258. $res = $this->wechat->http_request($wechat_url);
  259. } catch (\Exception $e) {
  260. $this->error($e->getMessage());
  261. }
  262. $res = json_decode($res, true);
  263. if (empty($res['openid'])) {
  264. $this->error('登录失败');
  265. }
  266. $redirect = base64_decode($redirect);
  267. $redirect = htmlspecialchars_decode($redirect);
  268. $query = parse_url(strstr($redirect, '?'), PHP_URL_QUERY);
  269. parse_str($query, $params);
  270. // 获取用户信息
  271. if ('snsapi_userinfo' == $res['scope']) {
  272. $userinfo = file_get_contents("https://api.weixin.qq.com/sns/userinfo?access_token={$res['access_token']}&openid={$res['openid']}&lang=zh_CN");
  273. $userinfo = json_decode($userinfo, true);
  274. if (!empty($userinfo['errcode']) && !empty($userinfo['errmsg'])) {
  275. exit($userinfo['errmsg']);
  276. }
  277. // [openid] => ou6NC6kBSloLS94gUhCyKeguIaYI
  278. // [nickname] => fuyelk
  279. // [sex] => 1 // 1=男,2=女,0=未知
  280. // [language] => zh_CN
  281. // [city] => 郑州
  282. // [province] => 河南
  283. // [country] => 中国
  284. // [headimgurl] => https://thirdwx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTKY3E1A2MFK46y1fNv7OElhtQFefO4mrBy8kcQoFCHP9diaULlpeINYB1FJ7KNh4fx5v3rzAJ2LzFg/132
  285. $invite_code = $params['sharecode'] ?? '';
  286. // TODO 业务处理
  287. // ...
  288. $token = 'abcdefg';
  289. if (false === $token) {
  290. exit($this->_error ?: '登录失败');
  291. }
  292. } else {
  293. exit('仅支持snsapi_userinfo');
  294. }
  295. $redirect = $redirect . (strpos($redirect, '?') ? '&' : '?') . 'token=' . $token;
  296. $this->redirect($redirect ? urldecode($redirect) : '/');
  297. }
  298. $this->error('授权失败');
  299. }
  300. }