WechatService.php 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. <?php
  2. namespace App\Services\Client;
  3. use App\Exceptions\BusinessException;
  4. use EasyWeChat\OfficialAccount\Application;
  5. use Illuminate\Support\Facades\Cache;
  6. use Illuminate\Support\Facades\Log;
  7. use Overtrue\Socialite\Contracts\ProviderInterface;
  8. use Overtrue\Socialite\Contracts\UserInterface;
  9. class WechatService
  10. {
  11. protected Application $app;
  12. protected AccountService $accountService;
  13. protected ProviderInterface $socialite;
  14. public function __construct(AccountService $accountService)
  15. {
  16. $this->app = new Application(config('wechat.official_account.default'));
  17. $this->accountService = $accountService;
  18. $this->socialite = $this->app->getOAuth();
  19. }
  20. /**
  21. * 获取微信授权URL
  22. *
  23. * @param string $redirectUrl 授权后重定向地址
  24. * @param string $scope 授权范围 snsapi_base或snsapi_userinfo
  25. * @return array{auth_url: string, state: string}
  26. *
  27. * @throws BusinessException
  28. */
  29. public function getAuthUrl(string $redirectUrl, ?string $scope = null): array
  30. {
  31. $scope = $scope ?? config('wechat.auth.default_scope');
  32. try {
  33. $state = $this->generateState();
  34. $this->cacheAuthState($state);
  35. $url = $this->socialite
  36. ->scopes([$scope])
  37. ->withState($state)
  38. ->redirect($redirectUrl);
  39. return [
  40. 'auth_url' => $url,
  41. 'state' => $state,
  42. ];
  43. } catch (\Exception $e) {
  44. $this->logError('生成微信授权URL失败', $e);
  45. throw new BusinessException('生成授权链接失败,请稍后重试');
  46. }
  47. }
  48. /**
  49. * 处理微信授权回调
  50. *
  51. * @param string $code 授权码
  52. * @param string $state 状态码
  53. * @param string|null $inviteCode 邀请码
  54. * @return array{token: string, user: array}
  55. *
  56. * @throws BusinessException
  57. */
  58. public function handleAuthCallback(string $code, string $state, ?string $inviteCode = null): array
  59. {
  60. try {
  61. // 验证state
  62. $this->validateState($state);
  63. // 获取用户信息
  64. $user = $this->socialite->userFromCode($code);
  65. // 验证openid
  66. $openid = $user->getId();
  67. abort_if(empty($openid), 400, '获取微信openid失败');
  68. // 整理用户信息
  69. $userInfo = $this->formatUserInfo($user);
  70. // 添加邀请人信息
  71. if ($inviteCode) {
  72. $userInfo['invite_code'] = $inviteCode;
  73. }
  74. // 执行登录
  75. return $this->accountService->wxLogin($openid, $userInfo);
  76. } catch (BusinessException $e) {
  77. throw $e;
  78. } catch (\Exception $e) {
  79. $this->logError('微信授权回调处理失败', $e, [
  80. 'code' => $code,
  81. 'state' => $state,
  82. 'invite_code' => $inviteCode,
  83. 'user_info' => $user ?? null,
  84. 'openid' => $openid ?? null,
  85. ]);
  86. throw new BusinessException('微信授权失败,请稍后重试');
  87. }
  88. }
  89. /**
  90. * 缓存授权state
  91. */
  92. protected function cacheAuthState(string $state): void
  93. {
  94. try {
  95. $states = Cache::get('wechat_auth_states', []);
  96. $states[$state] = true;
  97. Cache::put('wechat_auth_states', $states, now()->addMinutes(30));
  98. } catch (\Exception $e) {
  99. Log::error('缓存微信授权state失败', [
  100. 'state' => $state,
  101. 'error' => $e->getMessage(),
  102. ]);
  103. throw new BusinessException('系统错误,请稍后重试');
  104. }
  105. }
  106. /**
  107. * 验证state
  108. *
  109. * @throws BusinessException
  110. */
  111. protected function validateState(string $state): void
  112. {
  113. try {
  114. $states = Cache::get('wechat_auth_states', []);
  115. if (! isset($states[$state])) {
  116. Log::warning('微信授权state无效', ['state' => $state]);
  117. throw new BusinessException('授权已过期,请重新授权');
  118. }
  119. // 移除已使用的state
  120. unset($states[$state]);
  121. Cache::put('wechat_auth_states', $states, now()->addMinutes(30));
  122. } catch (BusinessException $e) {
  123. throw $e;
  124. } catch (\Exception $e) {
  125. Log::error('验证微信授权state失败', [
  126. 'state' => $state,
  127. 'error' => $e->getMessage(),
  128. ]);
  129. throw new BusinessException('系统错误,请稍后重试');
  130. }
  131. }
  132. /**
  133. * 生成随机state
  134. */
  135. protected function generateState(): string
  136. {
  137. return md5(uniqid((string) mt_rand(), true));
  138. }
  139. /**
  140. * 格式化用户信息
  141. *
  142. * @return array{
  143. * nickname: string,
  144. * avatar: string,
  145. * gender: string|null,
  146. * country: string|null,
  147. * province: string|null,
  148. * city: string|null
  149. * }
  150. */
  151. protected function formatUserInfo(UserInterface $user): array
  152. {
  153. return [
  154. 'nickname' => $user->getName(),
  155. 'avatar' => $user->getAvatar(),
  156. 'gender' => $this->formatGender($user->getRaw()['sex'] ?? null),
  157. 'country' => $user->getRaw()['country'] ?? null,
  158. 'province' => $user->getRaw()['province'] ?? null,
  159. 'city' => $user->getRaw()['city'] ?? null,
  160. ];
  161. }
  162. /**
  163. * 格式化性别信息
  164. *
  165. * @param int|null $gender 微信返回的性别值:1为男性,2为女性,0为未知
  166. * @return string|null male/female/null
  167. */
  168. protected function formatGender(?int $gender): ?string
  169. {
  170. return match ($gender) {
  171. 1 => 'male',
  172. 2 => 'female',
  173. default => null,
  174. };
  175. }
  176. /**
  177. * 记录错误日志
  178. */
  179. protected function logError(string $message, \Exception $e, array $context = []): void
  180. {
  181. Log::error($message, array_merge($context, [
  182. 'error' => $e->getMessage(),
  183. 'trace' => $e->getTraceAsString(),
  184. ]));
  185. }
  186. protected function getAuthStateKey(string $state): string
  187. {
  188. return config('wechat.auth.cache_prefix').$state;
  189. }
  190. /**
  191. * 获取JSAPI配置
  192. *
  193. * @param string $url 当前网页的URL,不包含#及其后面部分
  194. * @return array JSAPI配置信息
  195. *
  196. * @throws BusinessException
  197. */
  198. public function getJsConfig(string $url): array
  199. {
  200. try {
  201. $apis = [
  202. 'updateAppMessageShareData',
  203. 'updateTimelineShareData',
  204. 'chooseImage',
  205. 'previewImage',
  206. 'uploadImage',
  207. 'downloadImage',
  208. 'getLocation',
  209. 'openLocation',
  210. 'scanQRCode',
  211. ];
  212. return $this->app->getUtils()->buildJsSdkConfig($url, $apis);
  213. } catch (\Throwable $e) {
  214. \Log::error('获取JSAPI配置失败', [
  215. 'url' => $url,
  216. 'error' => $e->getMessage(),
  217. ]);
  218. throw new BusinessException('获取JSAPI配置失败,请稍后重试');
  219. }
  220. }
  221. }