|
|
<?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}×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 '';
|
|
|
}
|
|
|
}
|
|
|
}
|