OfficialAccount.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652
  1. <?php
  2. // +----------------------------------------------------------------------
  3. // | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
  4. // +----------------------------------------------------------------------
  5. // | Copyright (c) 2016~2020 https://www.crmeb.com All rights reserved.
  6. // +----------------------------------------------------------------------
  7. // | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
  8. // +----------------------------------------------------------------------
  9. // | Author: CRMEB Team <admin@crmeb.com>
  10. // +----------------------------------------------------------------------
  11. namespace crmeb\services\wechat;
  12. use crmeb\services\wechat\config\OfficialAccountConfig;
  13. use crmeb\services\wechat\config\OpenAppConfig;
  14. use crmeb\services\wechat\config\OpenWebConfig;
  15. use EasyWeChat\BasicService\Url\Client;
  16. use EasyWeChat\Factory;
  17. use EasyWeChat\Kernel\Exceptions\BadRequestException;
  18. use EasyWeChat\Kernel\Exceptions\InvalidArgumentException;
  19. use EasyWeChat\Kernel\Exceptions\InvalidConfigException;
  20. use EasyWeChat\Kernel\Support\Collection;
  21. use EasyWeChat\OfficialAccount\Application;
  22. use EasyWeChat\OfficialAccount\Card\Card;
  23. use EasyWeChat\OfficialAccount\User\TagClient;
  24. use EasyWeChat\OfficialAccount\User\UserClient;
  25. use GuzzleHttp\Exception\GuzzleException;
  26. use Overtrue\Socialite\Providers\WeChat;
  27. use Psr\Http\Message\ResponseInterface;
  28. use think\facade\Cache;
  29. use think\Response;
  30. use Yurun\Util\Swoole\Guzzle\SwooleHandler;
  31. use Symfony\Component\HttpFoundation\Request;
  32. use Symfony\Component\Cache\Adapter\RedisAdapter;
  33. /**
  34. * 公众号服务
  35. * Class OfficialAccount
  36. * @package crmeb\services\wechat
  37. * @method \EasyWeChat\OfficialAccount\Material\Client materialService() 永久素材
  38. * @method \EasyWeChat\BasicService\Media\Client mediaService() 临时素材
  39. * @method \EasyWeChat\BasicService\QrCode\Client qrcodeService() 微信二维码生成接口
  40. * @method UserClient userService() 用户接口
  41. * @method \EasyWeChat\OfficialAccount\CustomerService\Client staffService() 客服管理
  42. * @method \EasyWeChat\OfficialAccount\Menu\Client menuService() 微信公众号菜单接口
  43. * @method Client urlService() 短链接生成接口
  44. * @method WeChat oauthService() 用户授权
  45. * @method \EasyWeChat\OfficialAccount\TemplateMessage\Client templateService() 模板消息
  46. * @method Card cardServices() 卡券接口
  47. * @method TagClient userTagService() 用户标签
  48. */
  49. class OfficialAccount extends BaseApplication
  50. {
  51. /**
  52. * 配置
  53. * @var OfficialAccountConfig
  54. */
  55. protected $config;
  56. /**
  57. * @var array
  58. */
  59. protected $application;
  60. /**
  61. * @var string[]
  62. */
  63. protected static $property = [
  64. 'materialService' => 'material',
  65. 'mediaService' => 'media',
  66. 'qrcodeService' => 'qrcode',
  67. 'userService' => 'user',
  68. 'staffService' => 'customer_service',
  69. 'menuService' => 'menu',
  70. 'urlService' => 'url',
  71. 'oauthService' => 'oauth',
  72. 'templateService' => 'template_message',
  73. 'cardServices' => 'card',
  74. 'userTagService' => 'user_tag'
  75. ];
  76. /**
  77. * OfficialAccount constructor.
  78. */
  79. public function __construct()
  80. {
  81. /** @var OfficialAccountConfig config */
  82. $this->config = app(OfficialAccountConfig::class);
  83. $this->debug = DefaultConfig::value('logger');
  84. }
  85. /**
  86. * 初始化
  87. * @return Application
  88. */
  89. public function application()
  90. {
  91. $request = request();
  92. switch ($accessEnd = $this->getAuthAccessEnd($request)) {
  93. case self::APP:
  94. /** @var OpenAppConfig $meke */
  95. $meke = app()->make(OpenAppConfig::class);
  96. $config = $meke->all();
  97. break;
  98. case self::PC:
  99. /** @var OpenWebConfig $meke */
  100. $meke = app()->make(OpenWebConfig::class);
  101. $config = $meke->all();
  102. break;
  103. default:
  104. $config = $this->config->all();
  105. break;
  106. }
  107. if (!isset($this->application[$accessEnd])) {
  108. $this->application[$accessEnd] = Factory::officialAccount($config);
  109. $this->application[$accessEnd]['guzzle_handler'] = SwooleHandler::class;
  110. $this->application[$accessEnd]->rebind('request', new Request($request->get(), $request->post(), [], [], [], $request->server(), $request->getContent()));
  111. $this->application[$accessEnd]->rebind('cache', new RedisAdapter(Cache::store('redis')->handler()));
  112. }
  113. return $this->application[$accessEnd];
  114. }
  115. /**
  116. * 服务端
  117. * @return Response
  118. * @throws BadRequestException
  119. * @throws InvalidArgumentException
  120. * @throws InvalidConfigException
  121. * @throws \ReflectionException
  122. */
  123. public static function serve(): Response
  124. {
  125. $make = self::instance();
  126. $make->application()->server->push($make->pushMessageHandler);
  127. $response = $make->application()->server->serve();
  128. return response($response->getContent());
  129. }
  130. /**
  131. * @return OfficialAccount
  132. */
  133. public static function instance()
  134. {
  135. return app()->make(self::class);
  136. }
  137. /**
  138. * 获取js的SDK
  139. * @param string $url
  140. * @return string
  141. * @throws GuzzleException
  142. * @throws \Psr\SimpleCache\InvalidArgumentException
  143. */
  144. public static function jsSdk($url = '')
  145. {
  146. $apiList = ['openAddress', 'updateTimelineShareData', 'updateAppMessageShareData', 'onMenuShareTimeline', 'onMenuShareAppMessage', 'onMenuShareQQ', 'onMenuShareWeibo', 'onMenuShareQZone', 'startRecord', 'stopRecord', 'onVoiceRecordEnd', 'playVoice', 'pauseVoice', 'stopVoice', 'onVoicePlayEnd', 'uploadVoice', 'downloadVoice', 'chooseImage', 'previewImage', 'uploadImage', 'downloadImage', 'translateVoice', 'getNetworkType', 'openLocation', 'getLocation', 'hideOptionMenu', 'showOptionMenu', 'hideMenuItems', 'showMenuItems', 'hideAllNonBaseMenuItem', 'showAllNonBaseMenuItem', 'closeWindow', 'scanQRCode', 'chooseWXPay', 'openProductSpecificView', 'addCard', 'chooseCard', 'openCard'];
  147. $jsService = self::instance()->application()->jssdk;
  148. if ($url) $jsService->setUrl($url);
  149. try {
  150. return $jsService->buildConfig($apiList, false, true);
  151. } catch (\Exception $e) {
  152. self::error($e);
  153. return '{}';
  154. }
  155. }
  156. /**
  157. * 获取微信用户信息
  158. * @param $openid
  159. * @return array|Collection|mixed|object|ResponseInterface|string
  160. */
  161. public static function getUserInfo($openid)
  162. {
  163. $userService = self::userService();
  164. $userInfo = [];
  165. try {
  166. if (is_array($openid)) {
  167. $res = $userService->select($openid);
  168. if (isset($res['user_info_list'])) {
  169. $userInfo = $res['user_info_list'];
  170. } else {
  171. throw new WechatException($res['errmsg'] ?? '获取微信粉丝信息失败');
  172. }
  173. } else {
  174. $userInfo = $userService->get($openid);
  175. $userInfo = is_object($userInfo) ? $userInfo->toArray() : $userInfo;
  176. }
  177. } catch (\Throwable $e) {
  178. throw new WechatException(ErrorMessage::getMessage($e->getMessage()));
  179. }
  180. self::logger('获取微信用户信息', compact('openid'), $userInfo);
  181. return $userInfo;
  182. }
  183. /**
  184. * 获取会员卡列表
  185. * @param int $offset
  186. * @param int $count
  187. * @param string $statusList
  188. * @return mixed
  189. * @throws GuzzleException
  190. */
  191. public static function getCardList($offset = 0, $count = 10, $statusList = 'CARD_STATUS_VERIFY_OK')
  192. {
  193. try {
  194. $res = self::cardServices()->list($offset, $count, $statusList);
  195. self::logger('获取会员卡列表', compact('offset', 'count', 'statusList'), $res);
  196. if (isset($res['errcode']) && $res['errcode'] == 0 && isset($res['card_id_list'])) {
  197. return $res['card_id_list'];
  198. } else {
  199. throw new WechatException($res['errmsg']);
  200. }
  201. } catch (\Exception $e) {
  202. throw new WechatException(ErrorMessage::getMessage($e->getMessage()));
  203. }
  204. }
  205. /**
  206. * 获取卡券颜色
  207. * @return mixed
  208. */
  209. public static function getCardColors()
  210. {
  211. try {
  212. $response = self::cardServices()->colors();
  213. self::logger('获取卡券颜色', [], $response);
  214. return $response;
  215. } catch (\Exception $e) {
  216. throw new WechatException(ErrorMessage::getMessage($e->getMessage()));
  217. }
  218. }
  219. /**
  220. * 创建卡券
  221. * @param string $cardType
  222. * @param array $baseInfo
  223. * @param array $especial
  224. * @param array $advancedInfo
  225. * @return mixed
  226. * @throws GuzzleException
  227. */
  228. public static function createCard(string $cardType, array $baseInfo, array $especial = [], array $advancedInfo = [])
  229. {
  230. try {
  231. $res = self::cardServices()->create($cardType, array_merge(['base_info' => $baseInfo, 'advanced_info' => $advancedInfo], $especial));
  232. self::logger('创建卡券', compact('cardType', 'baseInfo', 'especial', 'advancedInfo'), $res);
  233. if (isset($res['errcode']) && $res['errcode'] == 0 && isset($res['card_id'])) {
  234. return $res;
  235. } else {
  236. throw new WechatException($res['errmsg']);
  237. }
  238. } catch (\Exception $e) {
  239. throw new WechatException(ErrorMessage::getMessage($e->getMessage()));
  240. }
  241. }
  242. /**
  243. * 获取卡券信息
  244. * @param $cardId
  245. * @return mixed
  246. * @throws GuzzleException
  247. */
  248. public static function getCard($cardId)
  249. {
  250. try {
  251. $res = self::cardServices()->get($cardId);
  252. self::logger('获取卡券信息', compact('cardId'), $res);
  253. if (isset($res['errcode']) && $res['errcode'] == 0) {
  254. return $res;
  255. } else {
  256. throw new WechatException($res['errmsg']);
  257. }
  258. } catch (\Exception $e) {
  259. throw new WechatException(ErrorMessage::getMessage($e->getMessage()));
  260. }
  261. }
  262. /**
  263. * 修改卡券
  264. * @param string $cardId
  265. * @param string $type
  266. * @param array $baseInfo
  267. * @param array $especial
  268. * @return mixed
  269. * @throws GuzzleException
  270. */
  271. public static function updateCard(string $cardId, string $type, array $baseInfo = [], array $especial = [])
  272. {
  273. try {
  274. $res = self::cardServices()->update($cardId, $type, array_merge(['base_info' => $baseInfo], $especial));
  275. self::logger('修改卡券', compact('cardId', 'type', 'baseInfo', 'especial'), $res);
  276. if (isset($res['errcode']) && $res['errcode'] == 0) {
  277. return $res;
  278. } else {
  279. throw new WechatException($res['errmsg']);
  280. }
  281. } catch (\Exception $e) {
  282. throw new WechatException(ErrorMessage::getMessage($e->getMessage()));
  283. }
  284. }
  285. /**
  286. * 获取领卡券二维码
  287. * @param string $card_id 卡券ID
  288. * @param string $outer_id 生成二维码标识参数
  289. * @param string $code 自动移code
  290. * @param int $expire_time
  291. * @return mixed
  292. * @throws GuzzleException
  293. */
  294. public static function getCardQRCode(string $card_id, string $outer_id, string $code = '', int $expire_time = 1800)
  295. {
  296. $data = [
  297. 'action_name' => 'QR_CARD',
  298. 'expire_seconds' => $expire_time,
  299. 'action_info' => [
  300. 'card' => [
  301. 'card_id' => $card_id,
  302. 'is_unique_code' => false,
  303. 'outer_id' => $outer_id
  304. ]
  305. ]
  306. ];
  307. if ($code) $data['action_info']['card']['code'] = $code;
  308. try {
  309. $res = self::cardServices()->createQrCode($data);
  310. self::logger('获取领卡券二维码', compact('data'), $res);
  311. if (isset($res['errcode']) && $res['errcode'] == 0 && isset($res['url'])) {
  312. return $res;
  313. } else {
  314. throw new WechatException($res['errmsg']);
  315. }
  316. } catch (\Exception $e) {
  317. throw new WechatException(ErrorMessage::getMessage($e->getMessage()));
  318. }
  319. }
  320. /**
  321. * 设置会员卡激活字段
  322. * @param string $cardId
  323. * @param array $requiredForm
  324. * @param array $optionalForm
  325. * @return mixed
  326. * @throws GuzzleException
  327. */
  328. public static function cardActivateUserForm(string $cardId, array $requiredForm = [], array $optionalForm = [])
  329. {
  330. try {
  331. $res = self::cardServices()->member_card->setActivationForm($cardId, array_merge($requiredForm, $optionalForm));
  332. self::logger('设置会员卡激活字段', compact('cardId', 'requiredForm', 'optionalForm'), $res);
  333. if (isset($res['errcode']) && $res['errcode'] == 0) {
  334. return $res;
  335. } else {
  336. throw new WechatException($res['errmsg']);
  337. }
  338. } catch (\Exception $e) {
  339. throw new WechatException(ErrorMessage::getMessage($e->getMessage()));
  340. }
  341. }
  342. /**
  343. * 会员卡激活
  344. * @param string $card_id
  345. * @param string $code
  346. * @param string $membership_number
  347. * @return mixed
  348. * @throws GuzzleException
  349. */
  350. public static function cardActivate(string $card_id, string $code, $membership_number = '')
  351. {
  352. $info = [
  353. 'membership_number' => $membership_number ? $membership_number : $code, //会员卡编号,由开发者填入,作为序列号显示在用户的卡包里。可与Code码保持等值。
  354. 'code' => $code, //创建会员卡时获取的初始code。
  355. 'activate_begin_time' => '', //激活后的有效起始时间。若不填写默认以创建时的 data_info 为准。Unix时间戳格式
  356. 'activate_end_time' => '', //激活后的有效截至时间。若不填写默认以创建时的 data_info 为准。Unix时间戳格式。
  357. 'init_bonus' => '0', //初始积分,不填为0。
  358. 'init_balance' => '0', //初始余额,不填为0。
  359. ];
  360. try {
  361. $res = self::cardServices()->member_card->activate($info);
  362. self::logger('会员卡激活', compact('info'), $res);
  363. if (isset($res['errcode']) && $res['errcode'] == 0 && isset($res['url'])) {
  364. return $res;
  365. } else {
  366. throw new WechatException($res['errmsg']);
  367. }
  368. } catch (\Exception $e) {
  369. throw new WechatException(ErrorMessage::getMessage($e->getMessage()));
  370. }
  371. }
  372. /**
  373. * 获取会员信息
  374. * @param string $cardId
  375. * @param string $code
  376. * @return mixed
  377. * @throws GuzzleException
  378. */
  379. public static function getMemberCardUser(string $cardId, string $code)
  380. {
  381. try {
  382. $res = self::cardServices()->member_card->getUser($cardId, $code);
  383. self::logger('获取会员信息', compact('cardId', 'code'), $res);
  384. if (isset($res['errcode']) && $res['errcode'] == 0 && isset($res['user_info'])) {
  385. return $res;
  386. } else {
  387. throw new WechatException($res['errmsg']);
  388. }
  389. } catch (\Exception $e) {
  390. throw new WechatException(ErrorMessage::getMessage($e->getMessage()));
  391. }
  392. }
  393. /**
  394. * 更新会员信息
  395. * @param array $data
  396. * @return mixed
  397. * @throws GuzzleException
  398. */
  399. public static function updateMemberCardUser(array $data)
  400. {
  401. try {
  402. $res = self::cardServices()->member_card->updateUser($data);
  403. self::logger('更新会员信息', compact('data'), $res);
  404. if (isset($res['errcode']) && $res['errcode'] == 0 && isset($res['user_info'])) {
  405. return $res;
  406. } else {
  407. throw new WechatException($res['errmsg']);
  408. }
  409. } catch (\Exception $e) {
  410. throw new WechatException(ErrorMessage::getMessage($e->getMessage()));
  411. }
  412. }
  413. /**
  414. * 设置模版消息行业
  415. * @param int $industryOne
  416. * @param int $industryTwo
  417. * @return array|Collection|object|ResponseInterface|string
  418. * @throws GuzzleException
  419. * @throws InvalidConfigException
  420. */
  421. public static function setIndustry(int $industryOne, int $industryTwo)
  422. {
  423. $response = self::templateService()->setIndustry($industryOne, $industryTwo);
  424. self::logger('设置模版消息行业', compact('industryOne', 'industryTwo'), $response);
  425. return $response;
  426. }
  427. /**
  428. * 获得添加模版ID
  429. * @param $templateIdShort
  430. * @return array|Collection|object|ResponseInterface|string
  431. * @throws GuzzleException
  432. */
  433. public static function addTemplateId($templateIdShort, $keywordList)
  434. {
  435. try {
  436. $response = self::templateService()->addTemplate($templateIdShort, $keywordList);
  437. self::logger('获得添加模版ID', compact('templateIdShort'), $response);
  438. return $response;
  439. } catch (\Exception $e) {
  440. throw new WechatException(ErrorMessage::getMessage($e->getMessage()));
  441. }
  442. }
  443. /**
  444. * 获取模板列表
  445. * @return array|Collection|object|ResponseInterface|string
  446. * @throws GuzzleException
  447. */
  448. public static function getPrivateTemplates()
  449. {
  450. try {
  451. $response = self::templateService()->getPrivateTemplates();
  452. self::logger('获取模板列表', [], $response);
  453. return $response;
  454. } catch (\Exception $e) {
  455. throw new WechatException(ErrorMessage::getMessage($e->getMessage()));
  456. }
  457. }
  458. /**
  459. * 根据模版ID删除模版
  460. * @param string $templateId
  461. * @return array|Collection|object|ResponseInterface|string
  462. * @throws GuzzleException
  463. */
  464. public static function deleleTemplate(string $templateId)
  465. {
  466. try {
  467. return self::templateService()->deletePrivateTemplate($templateId);
  468. } catch (\Exception $e) {
  469. throw new WechatException(ErrorMessage::getMessage($e->getMessage()));
  470. }
  471. }
  472. /**
  473. * 获取行业
  474. * @return array|Collection|object|ResponseInterface|string
  475. */
  476. public static function getIndustry()
  477. {
  478. try {
  479. $response = self::templateService()->getIndustry();
  480. self::logger('获取行业', [], $response);
  481. return $response;
  482. } catch (\Throwable $e) {
  483. throw new WechatException(ErrorMessage::getMessage($e->getMessage()));
  484. }
  485. }
  486. /**
  487. * 发送模板消息
  488. * @param string $openid
  489. * @param string $templateId
  490. * @param array $data
  491. * @param string|null $url
  492. * @param string|null $defaultColor
  493. * @return array|Collection|object|ResponseInterface|string
  494. * @throws GuzzleException
  495. * @throws InvalidArgumentException
  496. * @throws InvalidConfigException
  497. */
  498. public static function sendTemplate(string $openid, string $templateId, array $data, string $url = null, string $defaultColor = null)
  499. {
  500. $response = self::templateService()->send([
  501. 'touser' => $openid,
  502. 'template_id' => $templateId,
  503. 'data' => $data,
  504. 'url' => $url
  505. ]);
  506. self::logger('发送模板消息', compact('openid', 'templateId', 'data', 'url'), $response);
  507. return $response;
  508. }
  509. /**
  510. * 静默授权-使用code获取用户授权信息
  511. * @param string|null $code
  512. * @return array
  513. */
  514. public static function tokenFromCode(string $code = null)
  515. {
  516. $code = $code ?: request()->param('code');
  517. if (!$code) {
  518. throw new WechatException('无效CODE');
  519. }
  520. try {
  521. $response = self::oauthService()->setGuzzleOptions(['verify' => false])->tokenFromCode($code);
  522. self::logger('静默授权-使用code获取用户授权信息', compact('code'), $response);
  523. return $response;
  524. } catch (\Throwable $e) {
  525. throw new WechatException('授权失败' . $e->getMessage() . 'line' . $e->getLine());
  526. }
  527. }
  528. /**
  529. * 使用code获取用户授权信息
  530. * @param string|null $code
  531. * @return array
  532. */
  533. public static function userFromCode(string $code = null)
  534. {
  535. $code = $code ?: request()->param('code');
  536. if (!$code) {
  537. throw new WechatException('无效CODE');
  538. }
  539. try {
  540. $response = self::oauthService()->setGuzzleOptions(['verify' => false])->userFromCode($code);
  541. self::logger('使用code获取用户授权信息', compact('code'), $response);
  542. return $response->getRaw();
  543. } catch (\Throwable $e) {
  544. throw new WechatException('授权失败' . $e->getMessage() . 'line' . $e->getLine());
  545. }
  546. }
  547. /**
  548. * 永久素材上传
  549. * @param string $path
  550. * @return WechatResponse
  551. * @throws GuzzleException
  552. * @throws InvalidArgumentException
  553. * @throws InvalidConfigException
  554. */
  555. public static function uploadImage(string $path)
  556. {
  557. $response = self::materialService()->uploadImage($path);
  558. self::logger('素材管理-上传附件', compact('path'), $response);
  559. return new WechatResponse($response);
  560. }
  561. /**
  562. * 临时素材上传
  563. * @param string $path
  564. * @param string $type
  565. * @return WechatResponse
  566. * @throws GuzzleException
  567. * @throws InvalidArgumentException
  568. * @throws InvalidConfigException
  569. */
  570. public static function temporaryUpload(string $path, string $type = 'image')
  571. {
  572. $response = self::mediaService()->upload($type, $path);
  573. self::logger('临时素材上传', compact('path', 'type'), $response);
  574. return new WechatResponse($response);
  575. }
  576. }