validate([ 'code' => ['required', 'string'], ]); $appId = (string) config('wechat.app_id'); $secret = (string) config('wechat.app_secret'); if ($appId === '' || $secret === '') { return response()->json(['message' => '服务端未配置微信公众号 AppId/AppSecret'], 503); } $tokenRes = Http::timeout(15) ->get('https://api.weixin.qq.com/sns/oauth2/access_token', [ 'appid' => $appId, 'secret' => $secret, 'code' => $data['code'], 'grant_type' => 'authorization_code', ]) ->json(); if (! empty($tokenRes['errcode'])) { throw ValidationException::withMessages([ 'code' => [$tokenRes['errmsg'] ?? '微信 code 无效或已使用'], ]); } $accessToken = (string) ($tokenRes['access_token'] ?? ''); $openid = (string) ($tokenRes['openid'] ?? ''); if ($accessToken === '' || $openid === '') { return response()->json(['message' => '微信未返回 access_token'], 422); } $unionid = $tokenRes['unionid'] ?? null; $info = Http::timeout(15) ->get('https://api.weixin.qq.com/sns/userinfo', [ 'access_token' => $accessToken, 'openid' => $openid, 'lang' => 'zh_CN', ]) ->json(); if (! empty($info['errcode'])) { $info = []; } if ($unionid === null && ! empty($info['unionid'])) { $unionid = $info['unionid']; } $wu = WechatUser::query()->updateOrCreate( ['openid' => $openid], [ 'unionid' => $unionid, 'nickname' => $info['nickname'] ?? null, 'avatar_url' => $info['headimgurl'] ?? null, ] ); $plain = $wu->createToken('wechat-h5')->plainTextToken; return response()->json([ 'token' => $plain, 'user' => [ 'id' => $wu->id, 'nickname' => $wu->nickname, 'avatar_url' => $wu->avatar_url, ], ]); } /** * 公众号 JS-SDK 签名(分享、getLocation 等),与前端 wx.config 字段一致。 * * @see https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html */ public function jssdkSignature(Request $request): JsonResponse { $data = $request->validate([ 'url' => ['required', 'string', 'max:2048'], ]); $appId = (string) config('wechat.app_id'); $secret = (string) config('wechat.app_secret'); if ($appId === '' || $secret === '') { return response()->json(['message' => '服务端未配置微信公众号 AppId/AppSecret'], 503); } $url = $data['url']; $ticket = $this->getWechatJsapiTicket($appId, $secret); if ($ticket === '') { return response()->json(['message' => '获取微信 jsapi_ticket 失败,请检查 AppId/AppSecret 与网络'], 503); } $timestamp = time(); $nonceStr = Str::random(16); $plain = "jsapi_ticket={$ticket}&noncestr={$nonceStr}×tamp={$timestamp}&url={$url}"; $signature = sha1($plain); return response()->json([ 'appId' => $appId, 'timestamp' => $timestamp, 'nonceStr' => $nonceStr, 'signature' => $signature, ]); } private function getWechatJsapiTicket(string $appId, string $secret): string { try { return Cache::remember('wechat_jsapi_ticket_'.$appId, 7000, function () use ($appId, $secret) { $token = $this->getWechatAccessToken($appId, $secret); if ($token === '') { throw new \RuntimeException('empty access_token'); } $res = Http::timeout(15) ->get('https://api.weixin.qq.com/cgi-bin/ticket/getticket', [ 'access_token' => $token, 'type' => 'jsapi', ]) ->json(); if (! empty($res['errcode']) && (int) $res['errcode'] !== 0) { throw new \RuntimeException($res['errmsg'] ?? 'getticket failed'); } $ticket = (string) ($res['ticket'] ?? ''); if ($ticket === '') { throw new \RuntimeException('empty ticket'); } return $ticket; }); } catch (\Throwable) { Cache::forget('wechat_jsapi_ticket_'.$appId); return ''; } } private function getWechatAccessToken(string $appId, string $secret): string { try { return Cache::remember('wechat_access_token_'.$appId, 7000, function () use ($appId, $secret) { $res = Http::timeout(15) ->get('https://api.weixin.qq.com/cgi-bin/token', [ 'grant_type' => 'client_credential', 'appid' => $appId, 'secret' => $secret, ]) ->json(); if (! empty($res['errcode'])) { throw new \RuntimeException($res['errmsg'] ?? 'access_token failed'); } $token = (string) ($res['access_token'] ?? ''); if ($token === '') { throw new \RuntimeException('empty access_token'); } return $token; }); } catch (\Throwable) { Cache::forget('wechat_access_token_'.$appId); return ''; } } }