WechatService.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. <?php
  2. // +----------------------------------------------------------------------
  3. // | ThinkAdmin
  4. // +----------------------------------------------------------------------
  5. // | 版权所有 2014~2021 广州楚才信息科技有限公司 [ http://www.cuci.cc ]
  6. // +----------------------------------------------------------------------
  7. // | 官方网站: https://thinkadmin.top
  8. // +----------------------------------------------------------------------
  9. // | 开源协议 ( https://mit-license.org )
  10. // | 免费声明 ( https://thinkadmin.top/disclaimer )
  11. // +----------------------------------------------------------------------
  12. // | gitee 代码仓库:https://gitee.com/zoujingli/ThinkAdmin
  13. // | github 代码仓库:https://github.com/zoujingli/ThinkAdmin
  14. // +----------------------------------------------------------------------
  15. namespace app\wechat\service;
  16. use think\admin\extend\JsonRpcClient;
  17. use think\admin\Service;
  18. use think\admin\storage\LocalStorage;
  19. use think\exception\HttpResponseException;
  20. /**
  21. * Class WechatService
  22. * @package app\wechat\serivce
  23. *
  24. * @method \WeChat\Card WeChatCard() static 微信卡券管理
  25. * @method \WeChat\Custom WeChatCustom() static 微信客服消息
  26. * @method \WeChat\Limit WeChatLimit() static 接口调用频次限制
  27. * @method \WeChat\Media WeChatMedia() static 微信素材管理
  28. * @method \WeChat\Menu WeChatMenu() static 微信菜单管理
  29. * @method \WeChat\Oauth WeChatOauth() static 微信网页授权
  30. * @method \WeChat\Pay WeChatPay() static 微信支付商户
  31. * @method \WeChat\Product WeChatProduct() static 微信商店管理
  32. * @method \WeChat\Qrcode WeChatQrcode() static 微信二维码管理
  33. * @method \WeChat\Receive WeChatReceive() static 微信推送管理
  34. * @method \WeChat\Scan WeChatScan() static 微信扫一扫接入管理
  35. * @method \WeChat\Script WeChatScript() static 微信前端支持
  36. * @method \WeChat\Shake WeChatShake() static 微信揺一揺周边
  37. * @method \WeChat\Tags WeChatTags() static 微信用户标签管理
  38. * @method \WeChat\Template WeChatTemplate() static 微信模板消息
  39. * @method \WeChat\User WeChatUser() static 微信粉丝管理
  40. * @method \WeChat\Wifi WeChatWifi() static 微信门店WIFI管理
  41. *
  42. * ----- WeMini -----
  43. * @method \WeMini\Account WeMiniAccount() static 小程序账号管理
  44. * @method \WeMini\Basic WeMiniBasic() static 小程序基础信息设置
  45. * @method \WeMini\Code WeMiniCode() static 小程序代码管理
  46. * @method \WeMini\Domain WeMiniDomain() static 小程序域名管理
  47. * @method \WeMini\Tester WeMinitester() static 小程序成员管理
  48. * @method \WeMini\User WeMiniUser() static 小程序帐号管理
  49. * --------------------
  50. * @method \WeMini\Crypt WeMiniCrypt() static 小程序数据加密处理
  51. * @method \WeMini\Delivery WeMiniDelivery() static 小程序即时配送
  52. * @method \WeMini\Image WeMiniImage() static 小程序图像处理
  53. * @method \WeMini\Logistics WeMiniLogistics() static 小程序物流助手
  54. * @method \WeMini\Message WeMiniMessage() static 小程序动态消息
  55. * @method \WeMini\Ocr WeMiniOcr() static 小程序ORC服务
  56. * @method \WeMini\Plugs WeMiniPlugs() static 小程序插件管理
  57. * @method \WeMini\Poi WeMiniPoi() static 小程序地址管理
  58. * @method \WeMini\Qrcode WeMiniQrcode() static 小程序二维码管理
  59. * @method \WeMini\Security WeMiniSecurity() static 小程序内容安全
  60. * @method \WeMini\Soter WeMiniSoter() static 小程序生物认证
  61. * @method \WeMini\Template WeMiniTemplate() static 小程序模板消息支持
  62. * @method \WeMini\Total WeMiniTotal() static 小程序数据接口
  63. * @method \WeMini\Newtmpl WeMiniNewtmpl() static 小程序订阅消息支持
  64. *
  65. * ----- WePay -----
  66. * @method \WePay\Bill WePayBill() static 微信商户账单及评论
  67. * @method \WePay\Order WePayOrder() static 微信商户订单
  68. * @method \WePay\Refund WePayRefund() static 微信商户退款
  69. * @method \WePay\Coupon WePayCoupon() static 微信商户代金券
  70. * @method \WePay\Redpack WePayRedpack() static 微信红包支持
  71. * @method \WePay\Transfers WePayTransfers() static 微信商户打款到零钱
  72. * @method \WePay\TransfersBank WePayTransfersBank() static 微信商户打款到银行卡
  73. *
  74. * ----- WeOpen -----
  75. * @method \WeOpen\Login WeOpenLogin() static 第三方微信登录
  76. * @method \WeOpen\Service WeOpenService() static 第三方服务
  77. *
  78. * ----- ThinkService -----
  79. * @method mixed ThinkServiceConfig() static 平台服务配置
  80. */
  81. class WechatService extends Service
  82. {
  83. /**
  84. * 静态初始化对象
  85. * @param string $name
  86. * @param array $arguments
  87. * @return mixed
  88. * @throws \think\admin\Exception
  89. * @throws \think\db\exception\DataNotFoundException
  90. * @throws \think\db\exception\DbException
  91. * @throws \think\db\exception\ModelNotFoundException
  92. */
  93. public static function __callStatic(string $name, array $arguments)
  94. {
  95. [$type, $class, $classname] = static::parseName($name);
  96. if ("{$type}{$class}" !== $name) {
  97. throw new \think\admin\Exception("抱歉,实例 {$name} 不在符合规则!");
  98. }
  99. if (sysconf('wechat.type') === 'api' || $type === 'WePay') {
  100. if ($type === 'ThinkService') {
  101. throw new \think\admin\Exception("抱歉,接口模式不能实例 {$classname} 对象!");
  102. }
  103. return new $classname(static::instance()->getConfig());
  104. } else {
  105. [$appid, $appkey] = [sysconf('wechat.thr_appid'), sysconf('wechat.thr_appkey')];
  106. $data = ['class' => $name, 'appid' => $appid, 'time' => time(), 'nostr' => uniqid()];
  107. $data['sign'] = md5("{$data['class']}#{$appid}#{$appkey}#{$data['time']}#{$data['nostr']}");
  108. // 创建远程连接,默认使用 JSON RPC 方式
  109. $token = enbase64url(json_encode($data, JSON_UNESCAPED_UNICODE));
  110. $client = new JsonRpcClient("https://open.cuci.cc/service/api.client/jsonrpc?not_init_session=1&token={$token}");
  111. if (is_numeric(stripos($client->__call('_get_class_name_', []), 'Exception'))) {
  112. throw new \think\admin\Exception($client->getMessage(), $client->getCode());
  113. }
  114. return $client;
  115. }
  116. }
  117. /**
  118. * 解析调用对象名称
  119. * @param string $name
  120. * @return array
  121. */
  122. private static function parseName(string $name): array
  123. {
  124. foreach (['WeChat', 'WeMini', 'WeOpen', 'WePay', 'ThinkService'] as $type) {
  125. if (strpos($name, $type) === 0) {
  126. [, $class] = explode($type, $name);
  127. return [$type, $class, "\\{$type}\\{$class}"];
  128. }
  129. }
  130. return ['-', '-', $name];
  131. }
  132. /**
  133. * 获取当前微信APPID
  134. * @return string
  135. * @throws \think\admin\Exception
  136. * @throws \think\db\exception\DataNotFoundException
  137. * @throws \think\db\exception\DbException
  138. * @throws \think\db\exception\ModelNotFoundException
  139. */
  140. public function getAppid(): string
  141. {
  142. if ($this->getType() === 'api') {
  143. return sysconf('wechat.appid');
  144. } else {
  145. return sysconf('wechat.thr_appid');
  146. }
  147. }
  148. /**
  149. * 获取接口授权模式
  150. * @return string
  151. * @throws \think\admin\Exception
  152. * @throws \think\db\exception\DataNotFoundException
  153. * @throws \think\db\exception\DbException
  154. * @throws \think\db\exception\ModelNotFoundException
  155. */
  156. public function getType(): string
  157. {
  158. $type = strtolower(sysconf('wechat.type'));
  159. if (in_array($type, ['api', 'thr'])) return $type;
  160. throw new \think\admin\Exception('请在后台配置微信对接授权模式');
  161. }
  162. /**
  163. * 获取公众号配置参数
  164. * @return array
  165. * @throws \think\admin\Exception
  166. * @throws \think\db\exception\DataNotFoundException
  167. * @throws \think\db\exception\DbException
  168. * @throws \think\db\exception\ModelNotFoundException
  169. */
  170. public function getConfig(): array
  171. {
  172. $options = [
  173. 'appid' => $this->getAppid(),
  174. 'token' => sysconf('wechat.token'),
  175. 'appsecret' => sysconf('wechat.appsecret'),
  176. 'encodingaeskey' => sysconf('wechat.encodingaeskey'),
  177. 'mch_id' => sysconf('wechat.mch_id'),
  178. 'mch_key' => sysconf('wechat.mch_key'),
  179. 'cache_path' => $this->app->getRuntimePath() . 'wechat',
  180. ];
  181. $local = LocalStorage::instance();
  182. switch (strtolower(sysconf('wechat.mch_ssl_type'))) {
  183. case 'p12':
  184. $options['ssl_p12'] = $local->path(sysconf('wechat.mch_ssl_p12'), true);
  185. break;
  186. case 'pem':
  187. $options['ssl_key'] = $local->path(sysconf('wechat.mch_ssl_key'), true);
  188. $options['ssl_cer'] = $local->path(sysconf('wechat.mch_ssl_cer'), true);
  189. break;
  190. }
  191. return $options;
  192. }
  193. /**
  194. * 通过网页授权获取粉丝信息
  195. * @param string $source 回跳URL地址
  196. * @param integer $isfull 获取资料模式
  197. * @param boolean $redirect 是否直接跳转
  198. * @return array
  199. * @throws \WeChat\Exceptions\InvalidResponseException
  200. * @throws \WeChat\Exceptions\LocalCacheException
  201. * @throws \think\admin\Exception
  202. * @throws \think\db\exception\DataNotFoundException
  203. * @throws \think\db\exception\DbException
  204. * @throws \think\db\exception\ModelNotFoundException
  205. */
  206. public function getWebOauthInfo(string $source, $isfull = 0, $redirect = true): array
  207. {
  208. $appid = $this->getAppid();
  209. $openid = $this->app->session->get("{$appid}_openid");
  210. $userinfo = $this->app->session->get("{$appid}_fansinfo");
  211. if ((empty($isfull) && !empty($openid)) || (!empty($isfull) && !empty($openid) && !empty($userinfo))) {
  212. empty($userinfo) || FansService::instance()->set($userinfo, $appid);
  213. return ['openid' => $openid, 'fansinfo' => $userinfo];
  214. }
  215. if ($this->getType() === 'api') {
  216. // 解析 GET 参数
  217. parse_str(parse_url($source, PHP_URL_QUERY), $params);
  218. $getVars = [
  219. 'code' => $params['code'] ?? input('code', ''),
  220. 'rcode' => $params['rcode'] ?? input('rcode', ''),
  221. 'state' => $params['state'] ?? input('state', ''),
  222. ];
  223. $wechat = static::WeChatOauth();
  224. if ($getVars['state'] !== $appid || empty($getVars['code'])) {
  225. $params['rcode'] = enbase64url($source);
  226. $location = strstr("{$source}?", '?', true) . '?' . http_build_query($params);
  227. $oauthurl = $wechat->getOauthRedirect($location, $appid, $isfull ? 'snsapi_userinfo' : 'snsapi_base');
  228. throw new HttpResponseException($redirect ? redirect($oauthurl, 301) : response("location.href='{$oauthurl}'"));
  229. } elseif (($token = $wechat->getOauthAccessToken($getVars['code'])) && isset($token['openid'])) {
  230. $this->app->session->set("{$appid}_openid", $openid = $token['openid']);
  231. if ($isfull && isset($token['access_token'])) {
  232. $userinfo = $wechat->getUserInfo($token['access_token'], $openid);
  233. $this->app->session->set("{$appid}_fansinfo", $userinfo);
  234. empty($userinfo) || FansService::instance()->set($userinfo, $appid);
  235. }
  236. }
  237. if ($getVars['rcode']) {
  238. $location = debase64url($getVars['rcode']);
  239. throw new HttpResponseException($redirect ? redirect($location, 301) : response("location.href='{$location}'"));
  240. } elseif ((empty($isfull) && !empty($openid)) || (!empty($isfull) && !empty($openid) && !empty($userinfo))) {
  241. return ['openid' => $openid, 'fansinfo' => $userinfo];
  242. } else {
  243. throw new \think\admin\Exception('Query params [rcode] not find.');
  244. }
  245. } else {
  246. $result = static::ThinkServiceConfig()->oauth($this->app->session->getId(), $source, $isfull);
  247. $this->app->session->set("{$appid}_openid", $openid = $result['openid']);
  248. $this->app->session->set("{$appid}_fansinfo", $userinfo = $result['fans']);
  249. if ((empty($isfull) && !empty($openid)) || (!empty($isfull) && !empty($openid) && !empty($userinfo))) {
  250. empty($userinfo) || FansService::instance()->set($userinfo, $appid);
  251. return ['openid' => $openid, 'fansinfo' => $userinfo];
  252. }
  253. if ($redirect) {
  254. throw new HttpResponseException(redirect($result['url'], 301));
  255. } else {
  256. throw new HttpResponseException(response("location.href='{$result['url']}'"));
  257. }
  258. }
  259. }
  260. /**
  261. * 获取微信网页JSSDK签名参数
  262. * @param null|string $location 签名地址
  263. * @return array
  264. * @throws \WeChat\Exceptions\InvalidResponseException
  265. * @throws \WeChat\Exceptions\LocalCacheException
  266. * @throws \think\admin\Exception
  267. * @throws \think\db\exception\DataNotFoundException
  268. * @throws \think\db\exception\DbException
  269. * @throws \think\db\exception\ModelNotFoundException
  270. */
  271. public function getWebJssdkSign(?string $location = null): array
  272. {
  273. $location = $location ?: $this->app->request->url(true);
  274. if ($this->getType() === 'api') {
  275. return static::WeChatScript()->getJsSign($location);
  276. } else {
  277. return static::ThinkServiceConfig()->jsSign($location);
  278. }
  279. }
  280. }