95 lines
3.4 KiB
PHP
95 lines
3.4 KiB
PHP
|
|
<?php
|
|||
|
|
|
|||
|
|
namespace App\Service\wx;
|
|||
|
|
|
|||
|
|
use App\Service\common\RedisService;
|
|||
|
|
use App\Service\common\UtilsService;
|
|||
|
|
use Illuminate\Support\Facades\Http;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 微信小程序开放接口客户端
|
|||
|
|
*
|
|||
|
|
* AppID / AppSecret 来自 nl_wx_app(加密存储),不再硬编码在源码里。
|
|||
|
|
* access_token 有 7200 秒有效期且并发刷新会互相顶掉,所以缓存进 Redis 复用。
|
|||
|
|
*/
|
|||
|
|
class WxMiniService
|
|||
|
|
{
|
|||
|
|
private static mixed $_instance;
|
|||
|
|
|
|||
|
|
private string $baseUrl = 'https://api.weixin.qq.com/';
|
|||
|
|
|
|||
|
|
public static function getInstance(): null|static
|
|||
|
|
{
|
|||
|
|
$name = get_called_class();
|
|||
|
|
if (!isset(self::$_instance[$name])) {
|
|||
|
|
self::$_instance[$name] = new static();
|
|||
|
|
}
|
|||
|
|
return self::$_instance[$name];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* code 换 openid 与 session_key
|
|||
|
|
*/
|
|||
|
|
public function jscode2session(string $code): array
|
|||
|
|
{
|
|||
|
|
$app = WxAppService::getInstance()->current();
|
|||
|
|
if (($app['app_id'] ?? '') === '' || ($app['app_secret'] ?? '') === '') {
|
|||
|
|
UtilsService::getInstance()->errorThrow('小程序 AppID/AppSecret 未配置');
|
|||
|
|
}
|
|||
|
|
$result = Http::asJson()->get($this->baseUrl . 'sns/jscode2session', [
|
|||
|
|
'appid' => $app['app_id'],
|
|||
|
|
'secret' => $app['app_secret'],
|
|||
|
|
'js_code' => $code,
|
|||
|
|
'grant_type' => 'authorization_code',
|
|||
|
|
])->json();
|
|||
|
|
if (!empty($result['errcode'])) {
|
|||
|
|
UtilsService::getInstance()->errorThrow('微信登录失败:' . ($result['errmsg'] ?? '未知错误'));
|
|||
|
|
}
|
|||
|
|
if (empty($result['openid'])) {
|
|||
|
|
UtilsService::getInstance()->errorThrow('微信登录失败:未拿到 openid');
|
|||
|
|
}
|
|||
|
|
$result['app_code'] = (string) ($app['code'] ?? '');
|
|||
|
|
return $result;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 手机号快速验证:前端拿到的 code 换真实号码
|
|||
|
|
*/
|
|||
|
|
public function getPhoneNumber(string $phoneCode): string
|
|||
|
|
{
|
|||
|
|
$token = $this->accessToken();
|
|||
|
|
$result = Http::asJson()->post(
|
|||
|
|
$this->baseUrl . 'wxa/business/getuserphonenumber?access_token=' . $token,
|
|||
|
|
['code' => $phoneCode]
|
|||
|
|
)->json();
|
|||
|
|
if (!empty($result['errcode'])) {
|
|||
|
|
UtilsService::getInstance()->errorThrow('获取手机号失败:' . ($result['errmsg'] ?? '未知错误'));
|
|||
|
|
}
|
|||
|
|
return (string) ($result['phone_info']['phoneNumber'] ?? '');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 接口调用凭证,按 appid 缓存
|
|||
|
|
*/
|
|||
|
|
public function accessToken(): string
|
|||
|
|
{
|
|||
|
|
$app = WxAppService::getInstance()->current();
|
|||
|
|
$redis = RedisService::getInstance()->init(config('nl.redis.wechat_token'));
|
|||
|
|
$cached = $redis->get($app['app_id']);
|
|||
|
|
if (!empty($cached)) {
|
|||
|
|
return (string) $cached;
|
|||
|
|
}
|
|||
|
|
$result = Http::asJson()->get($this->baseUrl . 'cgi-bin/token', [
|
|||
|
|
'grant_type' => 'client_credential',
|
|||
|
|
'appid' => $app['app_id'],
|
|||
|
|
'secret' => $app['app_secret'],
|
|||
|
|
])->json();
|
|||
|
|
if (empty($result['access_token'])) {
|
|||
|
|
UtilsService::getInstance()->errorThrow('获取 access_token 失败:' . ($result['errmsg'] ?? '未知错误'));
|
|||
|
|
}
|
|||
|
|
// 提前 300 秒过期,避免临界点上拿到即将失效的 token
|
|||
|
|
$redis->set($app['app_id'], $result['access_token'], max(60, (int) ($result['expires_in'] ?? 7200) - 300));
|
|||
|
|
return (string) $result['access_token'];
|
|||
|
|
}
|
|||
|
|
}
|