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