174 lines
5.6 KiB
PHP
174 lines
5.6 KiB
PHP
<?php
|
|
|
|
namespace app\support;
|
|
|
|
use support\think\Db;
|
|
|
|
class MiniProgramAuthService
|
|
{
|
|
public const AUTH_TYPE = 'wechat_mini_program';
|
|
|
|
public function bindOpenid(int $userId, string $code): array
|
|
{
|
|
$code = trim($code);
|
|
if ($userId <= 0) {
|
|
throw new \RuntimeException('用户登录状态无效');
|
|
}
|
|
if ($code === '') {
|
|
throw new \RuntimeException('小程序登录 code 不能为空');
|
|
}
|
|
|
|
$identity = $this->fetchOpenidByCode($code);
|
|
$openid = (string)$identity['openid'];
|
|
$unionid = (string)($identity['unionid'] ?? '');
|
|
$now = date('Y-m-d H:i:s');
|
|
|
|
Db::startTrans();
|
|
try {
|
|
$existing = Db::name('user_auths')
|
|
->where('auth_type', self::AUTH_TYPE)
|
|
->where('auth_key', $openid)
|
|
->lock(true)
|
|
->find();
|
|
if ($existing && (int)$existing['user_id'] !== $userId) {
|
|
throw new \RuntimeException('该小程序微信身份已绑定其他账号');
|
|
}
|
|
|
|
if ($unionid !== '') {
|
|
$unionAuth = Db::name('user_auths')
|
|
->where('auth_type', self::AUTH_TYPE)
|
|
->where('auth_union_id', $unionid)
|
|
->lock(true)
|
|
->find();
|
|
if ($unionAuth && (int)$unionAuth['user_id'] !== $userId) {
|
|
throw new \RuntimeException('该小程序微信身份已绑定其他账号');
|
|
}
|
|
if (!$existing && $unionAuth) {
|
|
$existing = $unionAuth;
|
|
}
|
|
}
|
|
|
|
$payload = [
|
|
'user_id' => $userId,
|
|
'auth_type' => self::AUTH_TYPE,
|
|
'auth_key' => $openid,
|
|
'auth_open_id' => $openid,
|
|
'auth_union_id' => $unionid,
|
|
'auth_extra' => json_encode([
|
|
'session_key_present' => ((string)($identity['session_key'] ?? '')) !== '',
|
|
'bound_at' => $now,
|
|
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
|
'updated_at' => $now,
|
|
];
|
|
|
|
if ($existing) {
|
|
Db::name('user_auths')->where('id', (int)$existing['id'])->update($payload);
|
|
} else {
|
|
$payload['created_at'] = $now;
|
|
Db::name('user_auths')->insert($payload);
|
|
}
|
|
|
|
Db::commit();
|
|
} catch (\Throwable $e) {
|
|
Db::rollback();
|
|
throw $e;
|
|
}
|
|
|
|
return [
|
|
'openid' => $openid,
|
|
'unionid' => $unionid,
|
|
];
|
|
}
|
|
|
|
public function openidForUser(int $userId): string
|
|
{
|
|
if ($userId <= 0) {
|
|
return '';
|
|
}
|
|
|
|
return (string)Db::name('user_auths')
|
|
->where('user_id', $userId)
|
|
->where('auth_type', self::AUTH_TYPE)
|
|
->order('id', 'desc')
|
|
->value('auth_open_id');
|
|
}
|
|
|
|
private function fetchOpenidByCode(string $code): array
|
|
{
|
|
if (str_starts_with($code, 'mock_mp_')) {
|
|
return [
|
|
'openid' => 'mock_mp_openid_' . substr($code, 8),
|
|
'unionid' => '',
|
|
'session_key' => 'mock_session_key',
|
|
];
|
|
}
|
|
|
|
$appId = $this->systemConfig('mini_program', 'app_id');
|
|
$appSecret = $this->systemConfig('mini_program', 'app_secret');
|
|
if ($appId === '' || $appSecret === '') {
|
|
throw new \RuntimeException('小程序 AppID 或 AppSecret 未配置');
|
|
}
|
|
|
|
$url = 'https://api.weixin.qq.com/sns/jscode2session?' . http_build_query([
|
|
'appid' => $appId,
|
|
'secret' => $appSecret,
|
|
'js_code' => $code,
|
|
'grant_type' => 'authorization_code',
|
|
]);
|
|
|
|
$payload = $this->wechatApiGet($url);
|
|
$openid = trim((string)($payload['openid'] ?? ''));
|
|
if ($openid === '') {
|
|
throw new \RuntimeException('微信小程序登录返回缺少 openid');
|
|
}
|
|
|
|
return [
|
|
'openid' => $openid,
|
|
'unionid' => trim((string)($payload['unionid'] ?? '')),
|
|
'session_key' => (string)($payload['session_key'] ?? ''),
|
|
];
|
|
}
|
|
|
|
private function wechatApiGet(string $url): array
|
|
{
|
|
$ch = curl_init($url);
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_TIMEOUT => 8,
|
|
CURLOPT_CONNECTTIMEOUT => 4,
|
|
]);
|
|
|
|
$response = curl_exec($ch);
|
|
$errno = curl_errno($ch);
|
|
$error = curl_error($ch);
|
|
$httpStatus = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
curl_close($ch);
|
|
|
|
if ($errno) {
|
|
throw new \RuntimeException('微信小程序登录换取 openid 失败:' . $error);
|
|
}
|
|
if ($httpStatus < 200 || $httpStatus >= 300) {
|
|
throw new \RuntimeException('微信小程序登录接口 HTTP 状态异常:' . $httpStatus);
|
|
}
|
|
|
|
$payload = json_decode((string)$response, true);
|
|
if (!is_array($payload)) {
|
|
throw new \RuntimeException('微信小程序登录接口返回格式异常');
|
|
}
|
|
$errcode = (int)($payload['errcode'] ?? 0);
|
|
if ($errcode !== 0) {
|
|
throw new \RuntimeException((string)($payload['errmsg'] ?? '微信小程序登录接口返回错误'));
|
|
}
|
|
|
|
return $payload;
|
|
}
|
|
|
|
private function systemConfig(string $group, string $key): string
|
|
{
|
|
return trim((string)Db::name('system_configs')
|
|
->where('config_group', $group)
|
|
->where('config_key', $key)
|
|
->value('config_value'));
|
|
}
|
|
}
|