Files
anxinyan/server-api/app/support/MiniProgramAuthService.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'));
}
}