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; } }