Files
2026-04-26 09:24:08 +08:00

332 lines
12 KiB
PHP

<?php
declare(strict_types=1);
namespace app\smt\service;
use app\smt\model\MiniProgram;
use app\smt\model\SmokeUserProfile;
use app\smt\model\User;
class AuthService
{
private const DEFAULT_AVATAR_URL = 'https://linghu-wmr.oss-cn-beijing.aliyuncs.com/smt/avatar.png';
public function loginWithCode(array $data): array
{
$miniProgramId = (int) ($data['mini_program_id'] ?? 0);
$code = trim((string) ($data['code'] ?? ''));
if ($miniProgramId <= 0) {
throw new \RuntimeException('mini_program_id is required', 400);
}
if ($code === '') {
throw new \RuntimeException('code is required', 400);
}
$miniProgram = MiniProgram::findActiveById($miniProgramId);
if (!$miniProgram) {
throw new \RuntimeException('mini program not found', 400);
}
$session = $this->fetchWechatSession((string) $miniProgram->app_id, (string) $miniProgram->app_secret, $code);
$openId = trim((string) ($session['openid'] ?? ''));
if ($openId === '') {
throw new \RuntimeException('微信登录失败,未获取到 openid', 502);
}
$user = User::findByMiniProgramOpenId($miniProgramId, $openId);
$isNew = $user === null;
if (!$user) {
$user = new User();
$user->mini_program_id = $miniProgramId;
$user->open_id = $openId;
$user->created_at = Support::now()->format(Support::DATETIME_LAYOUT);
}
$avatarUrl = trim((string) ($data['avatar_url'] ?? ''));
$user->union_id = (string) ($session['unionid'] ?? '');
$user->nick_name = trim((string) ($data['nickname'] ?? (string) $user->nick_name));
$user->avatar_url = $avatarUrl !== '' ? $avatarUrl : ((string) $user->avatar_url !== '' ? (string) $user->avatar_url : self::DEFAULT_AVATAR_URL);
$user->gender = array_key_exists('gender', $data) ? (int) ($data['gender'] ?? 0) : (int) ($user->gender ?? 0);
$user->phone = trim((string) ($data['phone'] ?? (string) $user->phone));
$user->session_key = (string) ($session['session_key'] ?? '');
$user->updated_at = Support::now()->format(Support::DATETIME_LAYOUT);
$user->save();
$payload = $this->formatUser($user);
$payload['is_new_user'] = $isNew;
$mode = $this->getSmokeMode((int) $user->id);
if ($mode !== '') {
$payload['mode'] = $mode;
}
return [
'user' => $payload,
'session_key' => (string) $user->session_key,
'mini_program' => [
'id' => (int) $miniProgram->id,
'name' => (string) $miniProgram->name,
'app_id' => (string) $miniProgram->app_id,
'description' => (string) $miniProgram->description,
],
];
}
public function devLogin(int $miniProgramId): array
{
if ($miniProgramId <= 0) {
throw new \RuntimeException('mini_program_id is required', 400);
}
$miniProgram = MiniProgram::findActiveById($miniProgramId);
if (!$miniProgram) {
throw new \RuntimeException('mini program not found', 400);
}
$openId = 'dev_test_user';
$sessionKey = 'dev_session_' . $miniProgramId;
$user = User::findByMiniProgramOpenId($miniProgramId, $openId);
if (!$user) {
$user = new User();
$user->mini_program_id = $miniProgramId;
$user->open_id = $openId;
$user->nick_name = '开发测试用户';
$user->avatar_url = self::DEFAULT_AVATAR_URL;
$user->created_at = Support::now()->format(Support::DATETIME_LAYOUT);
}
$user->session_key = $sessionKey;
$user->updated_at = Support::now()->format(Support::DATETIME_LAYOUT);
$user->save();
$payload = $this->formatUser($user);
$mode = $this->getSmokeMode((int) $user->id);
if ($mode !== '') {
$payload['mode'] = $mode;
}
return [
'user' => $payload,
'session_key' => $sessionKey,
'mini_program' => [
'id' => (int) $miniProgram->id,
'name' => (string) $miniProgram->name,
'app_id' => (string) $miniProgram->app_id,
'description' => (string) $miniProgram->description,
],
];
}
public function getUserInfo(int $userId): array
{
$user = User::findActiveById($userId);
if (!$user) {
throw new \RuntimeException('用户不存在', 404);
}
$payload = $this->formatUser($user);
$mode = $this->getSmokeMode((int) $user->id);
if ($mode !== '') {
$payload['mode'] = $mode;
}
return $payload;
}
public function updateProfile(int $userId, array $data): array
{
$user = User::findActiveById($userId);
if (!$user) {
throw new \RuntimeException('用户不存在', 404);
}
$nickname = trim((string) ($data['nickname'] ?? ''));
$avatarUrl = trim((string) ($data['avatar_url'] ?? ''));
if ($nickname === '' && $avatarUrl === '') {
throw new \RuntimeException('请提供昵称或头像', 400);
}
if ($nickname !== '') {
$user->nick_name = $nickname;
}
if ($avatarUrl !== '') {
$user->avatar_url = $avatarUrl;
}
$user->save();
return [
'id' => (int) $user->id,
'nickname' => (string) $user->nick_name,
'avatar_url' => (string) $user->avatar_url,
];
}
public function getMiniProgramTestCode(int $userId, string $path, int $width): string
{
$user = User::findActiveById($userId);
if (!$user) {
throw new \RuntimeException('用户不存在', 404);
}
$miniProgramId = (int) $user->mini_program_id;
if ($miniProgramId <= 0) {
throw new \RuntimeException('用户小程序配置缺失', 500);
}
$miniProgram = MiniProgram::findActiveById($miniProgramId);
if (!$miniProgram) {
throw new \RuntimeException('mini program not found', 400);
}
$path = trim($path);
if ($path === '') {
$path = 'pages/nsti/test?resume=0';
}
if ($width <= 0) {
$width = 280;
}
return $this->fetchWXACode((string) $miniProgram->app_id, (string) $miniProgram->app_secret, $path, $width);
}
private function getSmokeMode(int $userId): string
{
$profile = SmokeUserProfile::findByUid($userId);
if (!$profile) {
return '';
}
return Support::normalizedMode((string) $profile->mode);
}
private function formatUser(User $user): array
{
return [
'id' => (int) $user->id,
'mini_program_id' => (int) $user->mini_program_id,
'open_id' => (string) $user->open_id,
'union_id' => (string) $user->union_id,
'nickname' => (string) $user->nick_name,
'avatar_url' => (string) $user->avatar_url,
'gender' => (int) $user->gender,
'phone' => (string) $user->phone,
];
}
private function fetchWechatSession(string $appId, string $appSecret, string $code): array
{
$url = sprintf(
'https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code',
urlencode($appId),
urlencode($appSecret),
urlencode($code)
);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
$response = curl_exec($ch);
$error = curl_error($ch);
curl_close($ch);
if ($response === false || $response === '' || $error) {
throw new \RuntimeException('微信登录请求失败', 502);
}
$data = json_decode($response, true);
if (!is_array($data)) {
throw new \RuntimeException('微信登录响应解析失败', 502);
}
if (!empty($data['errcode'])) {
throw new \RuntimeException(sprintf('微信登录失败:%s', (string) ($data['errmsg'] ?? 'unknown error')), 400);
}
return $data;
}
private function fetchAccessToken(string $appId, string $appSecret): string
{
$url = sprintf(
'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s',
urlencode($appId),
urlencode($appSecret)
);
$data = $this->getJson($url, '微信 access_token 请求失败');
if (!empty($data['errcode'])) {
throw new \RuntimeException(sprintf('微信 access_token 获取失败:%s', (string) ($data['errmsg'] ?? 'unknown error')), 502);
}
$token = trim((string) ($data['access_token'] ?? ''));
if ($token === '') {
throw new \RuntimeException('微信 access_token 缺失', 502);
}
return $token;
}
private function fetchWXACode(string $appId, string $appSecret, string $path, int $width): string
{
$accessToken = $this->fetchAccessToken($appId, $appSecret);
$url = sprintf('https://api.weixin.qq.com/wxa/getwxacode?access_token=%s', urlencode($accessToken));
$payload = json_encode([
'path' => $path,
'width' => $width,
'auto_color' => false,
'is_hyaline' => true,
], JSON_UNESCAPED_UNICODE);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
$response = curl_exec($ch);
$error = curl_error($ch);
$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($response === false || $response === '' || $error) {
throw new \RuntimeException('微信小程序码请求失败', 502);
}
if ($status !== 200) {
throw new \RuntimeException('微信小程序码接口状态异常', 502);
}
$decoded = json_decode((string) $response, true);
if (is_array($decoded) && !empty($decoded['errcode'])) {
throw new \RuntimeException(sprintf('微信小程序码获取失败:%s', (string) ($decoded['errmsg'] ?? 'unknown error')), 502);
}
return (string) $response;
}
private function getJson(string $url, string $failMessage): array
{
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
$response = curl_exec($ch);
$error = curl_error($ch);
$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($response === false || $response === '' || $error || $status !== 200) {
throw new \RuntimeException($failMessage, 502);
}
$data = json_decode((string) $response, true);
if (!is_array($data)) {
throw new \RuntimeException($failMessage, 502);
}
return $data;
}
}