You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

190 lines
6.3 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\WechatUser;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
class H5WechatController extends Controller
{
/**
* 公众号网页授权 code 换用户信息,并签发 Sanctum 令牌WechatUser
*/
public function oauth(Request $request): JsonResponse
{
$data = $request->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}&timestamp={$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 '';
}
}
}