123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228 |
- <?php
- namespace App\Services\Client;
- use Illuminate\Support\Facades\Log;
- use App\Exceptions\BusinessException;
- use Illuminate\Support\Facades\Cache;
- use App\Services\Client\AccountService;
- use EasyWeChat\OfficialAccount\Application;
- use Overtrue\Socialite\Contracts\UserInterface;
- use Overtrue\Socialite\Contracts\ProviderInterface;
- class WechatService
- {
- protected Application $app;
- protected AccountService $accountService;
- protected ProviderInterface $socialite;
- public function __construct(AccountService $accountService)
- {
- $this->app = new Application(config('wechat.official_account.default'));
- $this->accountService = $accountService;
- $this->socialite = $this->app->getOAuth();
- }
- /**
- * 获取微信授权URL
- *
- * @param string $redirectUrl 授权后重定向地址
- * @param string $scope 授权范围 snsapi_base或snsapi_userinfo
- * @return array{auth_url: string, state: string}
- *
- * @throws BusinessException
- */
- public function getAuthUrl(string $redirectUrl, ?string $scope = null): array
- {
- $scope = $scope ?? config('wechat.auth.default_scope');
- try {
- $state = $this->generateState();
- $this->cacheAuthState($state);
- $url = $this->socialite
- ->scopes([$scope])
- ->withState($state)
- ->redirect($redirectUrl);
- return [
- 'auth_url' => $url,
- 'state' => $state,
- ];
- } catch (\Exception $e) {
- $this->logError('生成微信授权URL失败', $e);
- throw new BusinessException('生成授权链接失败,请稍后重试');
- }
- }
- /**
- * 处理微信授权回调
- *
- * @param string $code 授权码
- * @param string $state 状态码
- * @param string|null $inviteCode 邀请码
- * @return array{token: string, user: array}
- *
- * @throws BusinessException
- */
- public function handleAuthCallback(string $code, string $state, ?string $inviteCode = null): array
- {
- // 验证state
- $this->validateState($state);
- // 获取用户信息
- $user = $this->socialite->userFromCode($code);
- // 验证openid
- $openid = $user->getId();
- abort_if(empty($openid), 400, '获取微信openid失败');
- // 整理用户信息
- $userInfo = $this->formatUserInfo($user);
- // 添加邀请人信息
- if ($inviteCode) {
- $userInfo['invite_code'] = $inviteCode;
- }
- // 执行登录
- return $this->accountService->wxLogin($openid, $userInfo);
- }
- /**
- * 缓存授权state
- */
- protected function cacheAuthState(string $state): void
- {
- try {
- $states = Cache::get('wechat_auth_states', []);
- $states[$state] = true;
- Cache::put('wechat_auth_states', $states, now()->addMinutes(30));
- } catch (\Exception $e) {
- Log::error('缓存微信授权state失败', [
- 'state' => $state,
- 'error' => $e->getMessage(),
- ]);
- throw new BusinessException('系统错误,请稍后重试');
- }
- }
- /**
- * 验证state
- *
- * @throws BusinessException
- */
- protected function validateState(string $state): void
- {
- try {
- $states = Cache::get('wechat_auth_states', []);
- if (! isset($states[$state])) {
- Log::warning('微信授权state无效', ['state' => $state]);
- throw new BusinessException('授权已过期,请重新授权');
- }
- // 移除已使用的state
- unset($states[$state]);
- Cache::put('wechat_auth_states', $states, now()->addMinutes(30));
- } catch (BusinessException $e) {
- throw $e;
- } catch (\Exception $e) {
- Log::error('验证微信授权state失败', [
- 'state' => $state,
- 'error' => $e->getMessage(),
- ]);
- throw new BusinessException('系统错误,请稍后重试');
- }
- }
- /**
- * 生成随机state
- */
- protected function generateState(): string
- {
- return md5(uniqid((string) mt_rand(), true));
- }
- /**
- * 格式化用户信息
- *
- * @return array{
- * nickname: string,
- * avatar: string,
- * gender: string|null,
- * country: string|null,
- * province: string|null,
- * city: string|null
- * }
- */
- protected function formatUserInfo(UserInterface $user): array
- {
- return [
- 'nickname' => $user->getName(),
- 'avatar' => $user->getAvatar(),
- 'gender' => $this->formatGender($user->getRaw()['sex'] ?? null),
- 'country' => $user->getRaw()['country'] ?? null,
- 'province' => $user->getRaw()['province'] ?? null,
- 'city' => $user->getRaw()['city'] ?? null,
- ];
- }
- /**
- * 格式化性别信息
- *
- * @param int|null $gender 微信返回的性别值:1为男性,2为女性,0为未知
- * @return string|null male/female/null
- */
- protected function formatGender(?int $gender): ?string
- {
- return match ($gender) {
- 1 => 'male',
- 2 => 'female',
- default => null,
- };
- }
- /**
- * 记录错误日志
- */
- protected function logError(string $message, \Exception $e, array $context = []): void
- {
- Log::error($message, array_merge($context, [
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString(),
- ]));
- }
- protected function getAuthStateKey(string $state): string
- {
- return config('wechat.auth.cache_prefix') . $state;
- }
- /**
- * 获取JSAPI配置
- *
- * @param string $url 当前网页的URL,不包含#及其后面部分
- * @return array JSAPI配置信息
- *
- * @throws BusinessException
- */
- public function getJsConfig(string $url): array
- {
- $apis = [
- 'updateAppMessageShareData',
- 'updateTimelineShareData',
- 'chooseImage',
- 'previewImage',
- 'uploadImage',
- 'downloadImage',
- 'getLocation',
- 'openLocation',
- 'scanQRCode',
- ];
- return $this->app->getUtils()->buildJsSdkConfig($url, $apis);
- }
- }
|