WechatAuth.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681
  1. <?php
  2. /**
  3. * Created by PhpStorm.
  4. * User: Tinywan
  5. * Date: 2016/9/11
  6. * Time: 9:55
  7. */
  8. namespace app\common\plugin;
  9. class WechatAuth
  10. {
  11. /* 消息类型常量 */
  12. const MSG_TYPE_TEXT = 'text';
  13. const MSG_TYPE_IMAGE = 'image';
  14. const MSG_TYPE_VOICE = 'voice';
  15. const MSG_TYPE_VIDEO = 'video';
  16. const MSG_TYPE_SHORTVIDEO = 'shortvideo';
  17. const MSG_TYPE_LOCATION = 'location';
  18. const MSG_TYPE_LINK = 'link';
  19. const MSG_TYPE_MUSIC = 'music';
  20. const MSG_TYPE_NEWS = 'news';
  21. const MSG_TYPE_EVENT = 'event';
  22. /* 二维码类型常量 */
  23. const QR_SCENE = 'QR_SCENE';
  24. const QR_STR_SCENE = 'QR_STR_SCENE';
  25. const QR_LIMIT_SCENE = 'QR_LIMIT_SCENE';
  26. const QR_LIMIT_STR_SCENE = 'QR_LIMIT_STR_SCENE';
  27. /**
  28. * 微信开发者申请的appID
  29. * @var string
  30. */
  31. private $appId = '';
  32. /**
  33. * 微信开发者申请的appSecret
  34. * @var string
  35. */
  36. private $appSecret = '';
  37. /**
  38. * 获取到的access_token
  39. * @var string
  40. */
  41. private $accessToken = '';
  42. /**
  43. * 微信api根路径
  44. * @var string
  45. */
  46. private $apiURL = 'https://api.weixin.qq.com/cgi-bin';
  47. /**
  48. * 微信二维码根路径
  49. * @var string
  50. */
  51. private $qrcodeURL = 'https://mp.weixin.qq.com/cgi-bin';
  52. //授权地址
  53. private $requestCodeURL = 'https://open.weixin.qq.com/connect/oauth2/authorize';
  54. private $oauthApiURL = 'https://api.weixin.qq.com/sns';
  55. /**
  56. * 构造方法,调用微信高级接口时实例化SDK
  57. * @param string $appid 微信appid
  58. * @param string $secret 微信appsecret
  59. * @param string $token 获取到的access_token
  60. */
  61. public function __construct($appid, $secret, $token = null)
  62. {
  63. if ($appid && $secret) {
  64. $this->appId = $appid;
  65. $this->appSecret = $secret;
  66. if (!empty($token)) $this->accessToken = $token;
  67. } else {
  68. throw new \Exception('缺少参数 APP_ID 和 APP_SECRET!');
  69. }
  70. }
  71. // 是为了拼接成一URL地址,什么地址,拼接一个
  72. public function getRequestCodeURL($redirect_uri, $state = null, $scope = 'snsapi_userinfo')
  73. {
  74. $query = array(
  75. 'appid' => $this->appId,
  76. 'redirect_uri' => $redirect_uri,
  77. 'response_type' => 'code',
  78. 'scope' => $scope,
  79. );
  80. if (!is_null($state) && preg_match('/[a-zA-Z0-9]+/', $state)) $query['state'] = $state;
  81. //生成 URL-encode 之后的请求字符串 :foo=bar&baz=boom&cow=milk&php=hypertext+pro
  82. $query = http_build_query($query);
  83. return "{$this->requestCodeURL}?{$query}#wechat_redirect";
  84. }
  85. /**
  86. * 获取access_token,用于后续接口访问
  87. * @return array access_token信息,包含 token 和有效期
  88. */
  89. public function getAccessToken($type = 'client', $code = null)
  90. {
  91. $param = array(
  92. 'appid' => $this->appId,
  93. 'secret' => $this->appSecret
  94. );
  95. switch ($type) {
  96. case 'client':
  97. $param['grant_type'] = 'client_credential';
  98. $url = "{$this->apiURL}/token";
  99. break;
  100. case 'code':
  101. $param['code'] = $code;
  102. $param['grant_type'] = 'authorization_code';
  103. $url = "{$this->oauthApiURL}/oauth2/access_token";
  104. break;
  105. default:
  106. throw new \Exception('不支持的grant_type类型!');
  107. break;
  108. }
  109. $token = self::http($url, $param);
  110. $token = json_decode($token, true);
  111. if (is_array($token)) {
  112. if (isset($token['errcode'])) {
  113. throw new \Exception($token['errmsg']);
  114. } else {
  115. $this->accessToken = $token['access_token'];
  116. return $token;
  117. }
  118. } else {
  119. throw new \Exception('获取微信access_token失败!');
  120. }
  121. }
  122. /**
  123. * 获取授权用户信息
  124. * @param string $openid 用户的OpenID
  125. * @param string $lang 指定的语言
  126. * @return array 用户信息数据,具体参见微信文档
  127. */
  128. public function getUserInfo($openid, $lang = 'zh_CN')
  129. {
  130. $query = array(
  131. 'access_token' => $this->accessToken,
  132. 'openid' => $openid,
  133. 'lang' => $lang,
  134. );
  135. $info = self::http("{$this->oauthApiURL}/userinfo", $query);
  136. return json_decode($info, true);
  137. }
  138. /**
  139. * 上传零时媒体资源
  140. * @param string $filename 媒体资源本地路径
  141. * @param string $type 媒体资源类型,具体请参考微信开发手册
  142. */
  143. public function mediaUpload($filename, $type)
  144. {
  145. $filename = realpath($filename);
  146. if (!$filename) throw new \Exception('资源路径错误!');
  147. $data = array(
  148. 'type' => $type,
  149. 'media' => "@{$filename}"
  150. );
  151. return $this->api('media/upload', $data, 'POST', '', false);
  152. }
  153. /**
  154. * 上传永久媒体资源
  155. * @param string $filename 媒体资源本地路径
  156. * @param string $type 媒体资源类型,具体请参考微信开发手册
  157. * @param string $description 资源描述,仅资源类型为 video 时有效
  158. */
  159. public function materialAddMaterial($filename, $type, $description = '')
  160. {
  161. $filename = realpath($filename);
  162. if (!$filename) throw new \Exception('资源路径错误!');
  163. $data = array(
  164. 'type' => $type,
  165. 'media' => "@{$filename}",
  166. );
  167. if ($type == 'video') {
  168. if (is_array($description)) {
  169. //保护中文,微信api不支持中文转义的json结构
  170. array_walk_recursive($description, function (&$value) {
  171. $value = urlencode($value);
  172. });
  173. $description = urldecode(json_encode($description));
  174. }
  175. $data['description'] = $description;
  176. }
  177. return $this->api('material/add_material', $data, 'POST', '', false);
  178. }
  179. /**
  180. * 获取媒体资源下载地址
  181. * 注意:视频资源不允许下载
  182. * @param string $media_id 媒体资源id
  183. * @return string 媒体资源下载地址
  184. */
  185. public function mediaGet($media_id)
  186. {
  187. $param = array(
  188. 'access_token' => $this->accessToken,
  189. 'media_id' => $media_id
  190. );
  191. $url = "{$this->apiURL}/media/get?";
  192. return $url . http_build_query($param);
  193. }
  194. /**
  195. * 给指定用户推送信息
  196. * 注意:微信规则只允许给在48小时内给公众平台发送过消息的用户推送信息
  197. * @param string $openid 用户的openid
  198. * @param array $content 发送的数据,不同类型的数据结构可能不同
  199. * @param string $type 推送消息类型
  200. */
  201. public function messageCustomSend($openid, $content, $type = self::MSG_TYPE_TEXT)
  202. {
  203. //基础数据
  204. $data = array(
  205. 'touser' => $openid,
  206. 'msgtype' => $type,
  207. );
  208. //根据类型附加额外数据
  209. $data[$type] = call_user_func(array(self, $type), $content);
  210. return $this->api('message/custom/send', $data);
  211. }
  212. /**
  213. * 发送文本消息
  214. * @param string $openid 用户的openid
  215. * @param string $text 发送的文字
  216. */
  217. public function sendText($openid, $text)
  218. {
  219. return $this->messageCustomSend($openid, $text, self::MSG_TYPE_TEXT);
  220. }
  221. /**
  222. * 发送图片消息
  223. * @param string $openid 用户的openid
  224. * @param string $media 图片ID
  225. */
  226. public function sendImage($openid, $media)
  227. {
  228. return $this->messageCustomSend($openid, $media, self::MSG_TYPE_IMAGE);
  229. }
  230. /**
  231. * 发送语音消息
  232. * @param string $openid 用户的openid
  233. * @param string $media 音频ID
  234. */
  235. public function sendVoice($openid, $media)
  236. {
  237. return $this->messageCustomSend($openid, $media, self::MSG_TYPE_VOICE);
  238. }
  239. /**
  240. * 发送视频消息
  241. * @param string $openid 用户的openid
  242. * @param string $media_id 视频ID
  243. * @param string $title 视频标题
  244. * @param string $discription 视频描述
  245. */
  246. public function sendVideo()
  247. {
  248. $video = func_get_args();
  249. $openid = array_shift($video);
  250. return $this->messageCustomSend($openid, $video, self::MSG_TYPE_VIDEO);
  251. }
  252. /**
  253. * 发送音乐消息
  254. * @param string $openid 用户的openid
  255. * @param string $title 音乐标题
  256. * @param string $discription 音乐描述
  257. * @param string $musicurl 音乐链接
  258. * @param string $hqmusicurl 高品质音乐链接
  259. * @param string $thumb_media_id 缩略图ID
  260. */
  261. public function sendMusic()
  262. {
  263. $music = func_get_args();
  264. $openid = array_shift($music);
  265. return $this->messageCustomSend($openid, $music, self::MSG_TYPE_MUSIC);
  266. }
  267. /**
  268. * 发送图文消息
  269. * @param string $openid 用户的openid
  270. * @param array $news 图文内容 [标题,描述,URL,缩略图]
  271. * @param array $news1 图文内容 [标题,描述,URL,缩略图]
  272. * @param array $news2 图文内容 [标题,描述,URL,缩略图]
  273. * ... ...
  274. * @param array $news9 图文内容 [标题,描述,URL,缩略图]
  275. */
  276. public function sendNews()
  277. {
  278. $news = func_get_args();
  279. $openid = array_shift($news);
  280. return $this->messageCustomSend($openid, $news, self::MSG_TYPE_NEWS);
  281. }
  282. /**
  283. * 发送一条图文消息
  284. * @param string $openid 用户的openid
  285. * @param string $title 文章标题
  286. * @param string $discription 文章简介
  287. * @param string $url 文章连接
  288. * @param string $picurl 文章缩略图
  289. */
  290. public function sendNewsOnce()
  291. {
  292. $news = func_get_args();
  293. $openid = array_shift($news);
  294. $news = array($news);
  295. return $this->messageCustomSend($openid, $news, self::MSG_TYPE_NEWS);
  296. }
  297. /**
  298. * 创建用户组
  299. * @param string $name 组名称
  300. */
  301. public function groupsCreate($name)
  302. {
  303. $data = array('group' => array('name' => $name));
  304. return $this->api('groups/create', $data);
  305. }
  306. /**
  307. * 查询所有分组
  308. * @return array 分组列表
  309. */
  310. public function groupsGet()
  311. {
  312. return $this->api('groups/get', '', 'GET');
  313. }
  314. /**
  315. * 查询用户所在的分组
  316. * @param string $openid 用户的OpenID
  317. * @return number 分组ID
  318. */
  319. public function groupsGetid($openid)
  320. {
  321. $data = array('openid' => $openid);
  322. return $this->api('groups/getid', $data);
  323. }
  324. /**
  325. * 修改分组
  326. * @param number $id 分组ID
  327. * @param string $name 分组名称
  328. * @return array 修改成功或失败信息
  329. */
  330. public function groupsUpdate($id, $name)
  331. {
  332. $data = array('id' => $id, 'name' => $name);
  333. return $this->api('groups/update', $data);
  334. }
  335. /**
  336. * 移动用户分组
  337. * @param string $openid 用户的OpenID
  338. * @param number $to_groupid 要移动到的分组ID
  339. * @return array 移动成功或失败信息
  340. */
  341. public function groupsMemberUpdate($openid, $to_groupid)
  342. {
  343. $data = array('openid' => $openid, 'to_groupid' => $to_groupid);
  344. return $this->api('groups/member/update', $data);
  345. }
  346. /**
  347. * 用户设备注名
  348. * @param string $openid 用户的OpenID
  349. * @param string $remark 设备注名
  350. * @return array 执行成功失败信息
  351. */
  352. public function userInfoUpdateremark($openid, $remark)
  353. {
  354. $data = array('openid' => $openid, 'remark' => $remark);
  355. return $this->api('user/info/updateremark', $data);
  356. }
  357. /**
  358. * 获取指定用户的详细信息
  359. * @param string $openid 用户的openid
  360. * @param string $lang 需要获取数据的语言
  361. */
  362. public function userInfo($openid, $lang = 'zh_CN')
  363. {
  364. $param = array('openid' => $openid, 'lang' => $lang);
  365. return $this->api('user/info', '', 'GET', $param);
  366. }
  367. /**
  368. * 获取关注者列表
  369. * @param string $next_openid 下一个openid,在用户数大于10000时有效
  370. * @return array 用户列表
  371. */
  372. public function userGet($next_openid = '')
  373. {
  374. $param = array('next_openid' => $next_openid);
  375. return $this->api('user/get', '', 'GET', $param);
  376. }
  377. /**
  378. * 创建自定义菜单
  379. * @param array $button 符合规则的菜单数组,规则参见微信手册
  380. */
  381. public function menuCreate($button)
  382. {
  383. $data = array('button' => $button);
  384. return $this->api('menu/create', $data);
  385. }
  386. /**
  387. * 获取所有的自定义菜单
  388. * @return array 自定义菜单数组
  389. */
  390. public function menuGet()
  391. {
  392. return $this->api('menu/get', '', 'GET');
  393. }
  394. /**
  395. * 删除自定义菜单
  396. */
  397. public function menuDelete()
  398. {
  399. return $this->api('menu/delete', '', 'GET');
  400. }
  401. /**
  402. * 创建二维码,可创建指定有效期的二维码和永久二维码
  403. * @param integer $scene_id 二维码参数
  404. * @param integer $expire_seconds 二维码有效期,0-永久有效
  405. */
  406. public function qrcodeCreate($q_type,$scene_id, $expire_seconds = 0){
  407. $data = array();
  408. switch($q_type){
  409. case self::QR_SCENE:
  410. $data['expire_seconds'] = $expire_seconds;
  411. $data['action_name'] = self::QR_SCENE;
  412. $data['action_info']['scene']['scene_id'] = $scene_id;
  413. break;
  414. case self::QR_STR_SCENE:
  415. $data['action_name'] = self::QR_STR_SCENE;
  416. $data['action_info']['scene']['scene_str'] = $scene_id;
  417. break;
  418. case self::QR_LIMIT_SCENE:
  419. $data['action_name'] = self::QR_LIMIT_SCENE;
  420. $data['action_info']['scene']['scene_id'] = $scene_id;
  421. break;
  422. case self::QR_LIMIT_STR_SCENE:
  423. $data['action_name'] = self::QR_LIMIT_STR_SCENE;
  424. $data['action_info']['scene']['scene_str'] = $scene_id;
  425. break;
  426. }
  427. return $this->api('qrcode/create', $data);
  428. }
  429. /**
  430. * 根据ticket获取二维码URL
  431. * @param string $ticket 通过 qrcodeCreate接口获取到的ticket
  432. * @return string 二维码URL
  433. */
  434. public function showqrcode($ticket)
  435. {
  436. return "{$this->qrcodeURL}/showqrcode?ticket={$ticket}";
  437. }
  438. /**
  439. * 长链接转短链接
  440. * @param string $long_url 长链接
  441. * @return string 短链接
  442. */
  443. public function shorturl($long_url)
  444. {
  445. $data = array(
  446. 'action' => 'long2short',
  447. 'long_url' => $long_url
  448. );
  449. return $this->api('shorturl', $data);
  450. }
  451. /**
  452. * 调用微信api获取响应数据
  453. * @param string $name API名称
  454. * @param string $data POST请求数据
  455. * @param string $method 请求方式
  456. * @param string $param GET请求参数
  457. * @return array api返回结果
  458. */
  459. protected function api($name, $data = '', $method = 'POST', $param = '', $json = true)
  460. {
  461. $params = array('access_token' => $this->accessToken);
  462. if (!empty($param) && is_array($param)) {
  463. $params = array_merge($params, $param);
  464. }
  465. $url = "{$this->apiURL}/{$name}";
  466. if ($json && !empty($data)) {
  467. //保护中文,微信api不支持中文转义的json结构
  468. array_walk_recursive($data, function (&$value) {
  469. $value = urlencode($value);
  470. });
  471. $data = urldecode(json_encode($data));
  472. }
  473. $data = self::http($url, $params, $data, $method);
  474. return json_decode($data, true);
  475. }
  476. /**
  477. * 发送HTTP请求方法,目前只支持CURL发送请求
  478. * @param string $url 请求URL
  479. * @param array $param GET参数数组
  480. * @param array $data POST的数据,GET请求时该参数无效
  481. * @param string $method 请求方法GET/POST
  482. * @return array 响应数据
  483. */
  484. protected static function http($url, $param, $data = '', $method = 'GET')
  485. {
  486. $opts = array(
  487. CURLOPT_TIMEOUT => 30,
  488. CURLOPT_RETURNTRANSFER => 1,
  489. CURLOPT_SSL_VERIFYPEER => false,
  490. CURLOPT_SSL_VERIFYHOST => false,
  491. );
  492. /* 根据请求类型设置特定参数 */
  493. $opts[CURLOPT_URL] = $url . '?' . http_build_query($param);
  494. if (strtoupper($method) == 'POST') {
  495. $opts[CURLOPT_POST] = 1;
  496. $opts[CURLOPT_POSTFIELDS] = $data;
  497. if (is_string($data)) { //发送JSON数据
  498. $opts[CURLOPT_HTTPHEADER] = array(
  499. 'Content-Type: application/json; charset=utf-8',
  500. 'Content-Length: ' . strlen($data),
  501. );
  502. }
  503. }
  504. /* 初始化并执行curl请求 */
  505. $ch = curl_init();
  506. curl_setopt_array($ch, $opts);
  507. $data = curl_exec($ch);
  508. $error = curl_error($ch);
  509. curl_close($ch);
  510. //发生错误,抛出异常
  511. if ($error) throw new \Exception('请求发生错误:' . $error);
  512. return $data;
  513. }
  514. /**
  515. * 构造文本信息
  516. * @param string $content 要回复的文本
  517. */
  518. private static function text($content)
  519. {
  520. $data['content'] = $content;
  521. return $data;
  522. }
  523. /**
  524. * 构造图片信息
  525. * @param integer $media 图片ID
  526. */
  527. private static function image($media)
  528. {
  529. $data['media_id'] = $media;
  530. return $data;
  531. }
  532. /**
  533. * 构造音频信息
  534. * @param integer $media 语音ID
  535. */
  536. private static function voice($media)
  537. {
  538. $data['media_id'] = $media;
  539. return $data;
  540. }
  541. /**
  542. * 构造视频信息
  543. * @param array $video 要回复的视频 [视频ID,标题,说明]
  544. */
  545. private static function video($video)
  546. {
  547. $data = array();
  548. list(
  549. $data['media_id'],
  550. $data['title'],
  551. $data['description'],
  552. ) = $video;
  553. return $data;
  554. }
  555. /**
  556. * 构造音乐信息
  557. * @param array $music 要回复的音乐[标题,说明,链接,高品质链接,缩略图ID]
  558. */
  559. private static function music($music)
  560. {
  561. $data = array();
  562. list(
  563. $data['title'],
  564. $data['description'],
  565. $data['musicurl'],
  566. $data['hqmusicurl'],
  567. $data['thumb_media_id'],
  568. ) = $music;
  569. return $data;
  570. }
  571. /**
  572. * 构造图文信息
  573. * @param array $news 要回复的图文内容
  574. * [
  575. * 0 => 第一条图文信息[标题,说明,图片链接,全文连接],
  576. * 1 => 第二条图文信息[标题,说明,图片链接,全文连接],
  577. * 2 => 第三条图文信息[标题,说明,图片链接,全文连接],
  578. * ]
  579. */
  580. private static function news($news)
  581. {
  582. $articles = array();
  583. foreach ($news as $key => $value) {
  584. list(
  585. $articles[$key]['title'],
  586. $articles[$key]['description'],
  587. $articles[$key]['url'],
  588. $articles[$key]['picurl']
  589. ) = $value;
  590. if ($key >= 9) break; //最多只允许10条图文信息
  591. }
  592. $data['articles'] = $articles;
  593. return $data;
  594. }
  595. }