appId = config('services.wechat_open.app_id', env('WECHAT_OPEN_APP_ID', '')); $this->appSecret = config('services.wechat_open.app_secret', env('WECHAT_OPEN_APP_SECRET', '')); } /** * 获取微信登录二维码 * * @return array ['success' => bool, 'data' => ['scene_str' => string, 'qr_url' => string, 'ticket' => string]] */ public function getLoginQr(): array { if (!$this->isConfigured()) { // 未配置时返回模拟二维码 return $this->getMockQr(); } try { // 获取 Access Token (用于生成二维码) $accessToken = $this->getAccessToken(); if (!$accessToken) { return ['success' => false, 'message' => '获取access_token失败']; } // 创建二维码 ticket $sceneStr = 'wx_' . Str::random(16); $expireSeconds = 300; // 5分钟 $response = Http::post("https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token={$accessToken}", [ 'expire_seconds' => $expireSeconds, 'action_name' => 'QR_STR', 'action_info' => [ 'scene' => [ 'scene_str' => $sceneStr ] ] ]); $result = $response->json(); if (isset($result['ticket'])) { $qrUrl = "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=" . urlencode($result['ticket']); // 缓存 ticket -> scene_str 映射,用于后续验证 Cache::put("wechat_qr:{$result['ticket']}", [ 'scene_str' => $sceneStr, 'created_at' => time(), ], $expireSeconds); return [ 'success' => true, 'data' => [ 'scene_str' => $sceneStr, 'qr_url' => $qrUrl, 'ticket' => $result['ticket'], ] ]; } return ['success' => false, 'message' => $result['errmsg'] ?? '创建二维码失败']; } catch (\Exception $e) { Log::error('微信获取二维码异常: ' . $e->getMessage()); return ['success' => false, 'message' => '微信服务异常']; } } /** * 通过 ticket 换取二维码链接 */ public function getQrUrlByTicket(string $ticket): ?string { try { $response = Http::get("https://mp.weixin.qq.com/cgi-bin/showqrcode", [ 'ticket' => $ticket ]); if ($response->status() === 200) { return $response->header('Location') ?? null; } } catch (\Exception $e) { Log::error('获取二维码URL失败: ' . $e->getMessage()); } return null; } /** * 轮询检查扫码状态 (模拟 - 实际项目中微信需要使用被动回调) * * @param string $sceneStr 场景字符串 * @return array */ public function checkScanStatus(string $sceneStr): array { // 实际项目中,微信扫码后是回调到开发者服务器 // 这里提供模拟实现,用于开发和测试 // 模拟:检查是否有模拟登录标记 $mockScanKey = "wechat_mock_scan:{$sceneStr}"; $mockData = Cache::get($mockScanKey); if ($mockData) { Cache::forget($mockScanKey); return [ 'success' => true, 'data' => [ 'scanned' => true, 'openid' => $mockData['openid'], 'unionid' => $mockData['unionid'] ?? null, 'nickname' => $mockData['nickname'] ?? '微信用户', ] ]; } return [ 'success' => true, 'data' => [ 'scanned' => false, ] ]; } /** * 模拟扫码(开发测试用) * * @param string $sceneStr 场景字符串 * @param string $openid 模拟的openid */ public function mockScan(string $sceneStr, string $openid = 'mock_openid_123456'): void { Cache::put("wechat_mock_scan:{$sceneStr}", [ 'openid' => $openid, 'unionid' => 'mock_unionid_' . substr(md5($openid), 0, 16), 'nickname' => '测试用户', ], now()->addMinutes(5)); } /** * 处理微信开放平台回调 * * @param string $code 授权码 * @return array ['success' => bool, 'data' => [...]] */ public function handleCallback(string $code): array { if (!$this->isConfigured()) { return ['success' => false, 'message' => '微信未配置']; } try { // 通过 code 获取 access_token 和 openid $tokenResponse = Http::get("https://api.weixin.qq.com/sns/oauth2/access_token", [ 'appid' => $this->appId, 'secret' => $this->appSecret, 'code' => $code, 'grant_type' => 'authorization_code', ]); $tokenResult = $tokenResponse->json(); if (isset($tokenResult['errcode'])) { return ['success' => false, 'message' => $tokenResult['errmsg'] ?? '获取access_token失败']; } $openid = $tokenResult['openid']; $unionid = $tokenResult['unionid'] ?? null; $accessToken = $tokenResult['access_token']; // 获取用户信息 $userInfo = $this->getUserInfo($accessToken, $openid); // 查找或创建用户 $user = $this->findOrCreateUser($openid, $unionid, $userInfo); // 生成 token $token = $user->createToken('wechat_auth')->plainTextToken; return [ 'success' => true, 'data' => [ 'user' => $user, 'token' => $token, 'openid' => $openid, 'unionid' => $unionid, ] ]; } catch (\Exception $e) { Log::error('微信回调处理异常: ' . $e->getMessage()); return ['success' => false, 'message' => '处理失败']; } } /** * 获取用户信息 */ public function getUserInfo(string $accessToken, string $openid): array { try { $response = Http::get("https://api.weixin.qq.com/sns/userinfo", [ 'access_token' => $accessToken, 'openid' => $openid, ]); return $response->json(); } catch (\Exception $e) { Log::error('获取微信用户信息失败: ' . $e->getMessage()); return []; } } /** * 查找或创建微信用户 */ public function findOrCreateUser(string $openid, ?string $unionid, array $userInfo): User { // 优先通过 unionid 查找 if ($unionid) { $user = User::where('wechat_unionid', $unionid)->first(); if ($user) { // 更新 openid(一个 unionid 可能对应多个 openid) $user->update(['wechat_openid' => $openid]); return $user; } } // 通过 openid 查找 $user = User::where('wechat_openid', $openid)->first(); if ($user) { return $user; } // 创建新用户 $user = User::create([ 'name' => $userInfo['nickname'] ?? ('微信用户_' . substr($openid, 0, 8)), 'email' => "wechat_{$openid}@wechat.local", 'password' => bcrypt(Str::random(16)), 'wechat_openid' => $openid, 'wechat_unionid' => $unionid, 'avatar' => $userInfo['headimgurl'] ?? null, ]); return $user; } /** * 绑定微信到已有账号 */ public function bindToUser(User $user, string $openid, ?string $unionid): bool { // 检查是否已被其他账号绑定 $exists = User::where('wechat_openid', $openid) ->where('id', '!=', $user->id) ->exists(); if ($exists) { return false; } $user->update([ 'wechat_openid' => $openid, 'wechat_unionid' => $unionid, ]); return true; } /** * 解绑微信 */ public function unbind(User $user): bool { $user->update([ 'wechat_openid' => null, 'wechat_unionid' => null, ]); return true; } /** * 获取 Access Token */ private function getAccessToken(): ?string { $cacheKey = 'wechat_access_token'; $token = Cache::get($cacheKey); if ($token) { return $token; } try { $response = Http::get("https://api.weixin.qq.com/cgi-bin/token", [ 'grant_type' => 'client_credential', 'appid' => $this->appId, 'secret' => $this->appSecret, ]); $result = $response->json(); if (isset($result['access_token'])) { Cache::put($cacheKey, $result['access_token'], $result['expires_in'] - 200); return $result['access_token']; } } catch (\Exception $e) { Log::error('获取微信access_token失败: ' . $e->getMessage()); } return null; } /** * 检查是否已配置 */ public function isConfigured(): bool { return !empty($this->appId) && !empty($this->appSecret); } /** * 获取模拟二维码(未配置时使用) */ private function getMockQr(): array { $sceneStr = 'mock_' . Str::random(16); // 生成一个 data URI 格式的模拟二维码图片 // 这是一个简单的模拟二维码,实际项目中应该显示真实二维码 $mockQrData = [ 'scene_str' => $sceneStr, 'qr_url' => null, // 前端可以自己生成二维码 'ticket' => null, 'mock' => true, ]; return [ 'success' => true, 'data' => $mockQrData, ]; } }