WechatService.php 6.4 KB

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