false, 'msg' => '服务端未配置微信小程序 AppID/Secret']; } if ($phoneCode === '') { return ['ok' => false, 'msg' => '缺少手机号授权 code']; } $tokenRes = self::getAccessToken($appId, $secret); if (!$tokenRes['ok']) { return ['ok' => false, 'msg' => (string) $tokenRes['msg']]; } $token = (string) ($tokenRes['access_token'] ?? ''); if ($token === '') { return ['ok' => false, 'msg' => '获取微信 access_token 失败']; } $url = 'https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=' . rawurlencode($token); $httpErr = ''; $raw = self::httpPostJson($url, ['code' => $phoneCode], $httpErr); if ($raw === null || $raw === '') { return ['ok' => false, 'msg' => '请求微信手机号接口失败' . ($httpErr !== '' ? (':' . $httpErr) : '')]; } $data = json_decode($raw, true); if (!is_array($data)) { return ['ok' => false, 'msg' => '微信手机号接口返回异常']; } if (!empty($data['errcode'])) { $msg = (string) ($data['errmsg'] ?? '微信错误'); return ['ok' => false, 'msg' => $msg . ' (' . ($data['errcode'] ?? '') . ')']; } $phoneInfo = (array) ($data['phone_info'] ?? []); $phone = trim((string) ($phoneInfo['phoneNumber'] ?? $phoneInfo['purePhoneNumber'] ?? '')); if ($phone === '') { return ['ok' => false, 'msg' => '未获取到手机号']; } return ['ok' => true, 'msg' => 'ok', 'phone' => $phone]; } /** * @return array{ok:bool,msg:string,access_token?:string} */ private static function getAccessToken(string $appId, string $secret): array { $url = sprintf( 'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s', rawurlencode($appId), rawurlencode($secret) ); $httpErr = ''; $raw = self::httpGet($url, $httpErr); if ($raw === null || $raw === '') { return ['ok' => false, 'msg' => '获取微信 access_token 失败' . ($httpErr !== '' ? (':' . $httpErr) : '')]; } $data = json_decode($raw, true); if (!is_array($data)) { return ['ok' => false, 'msg' => '微信 access_token 返回异常']; } if (!empty($data['errcode'])) { $msg = (string) ($data['errmsg'] ?? '微信错误'); return ['ok' => false, 'msg' => $msg . ' (' . ($data['errcode'] ?? '') . ')']; } $token = (string) ($data['access_token'] ?? ''); if ($token === '') { return ['ok' => false, 'msg' => '微信 access_token 为空']; } return ['ok' => true, 'msg' => 'ok', 'access_token' => $token]; } /** * @return array{ok:bool,msg:string,openid?:string,unionid?:string,session_key?:string} */ public static function code2Session(string $jsCode): array { $appId = (string) config('wechat.mini_appid'); $secret = (string) config('wechat.mini_secret'); if ($appId === '' || $secret === '') { return [ 'ok' => false, 'msg' => '服务端未配置微信小程序 AppID/Secret,请在 backend/.env 填写 WECHAT_MINI_APPID 与 WECHAT_MINI_SECRET 后重启后端', ]; } $url = sprintf( 'https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code', rawurlencode($appId), rawurlencode($secret), rawurlencode($jsCode) ); $httpErr = ''; $raw = self::httpGet($url, $httpErr); if ($raw === null || $raw === '') { $msg = '请求微信接口失败(本机 PHP 无法访问 https://api.weixin.qq.com)'; if ($httpErr !== '' && (bool) env('APP_DEBUG', false)) { $msg .= ':' . $httpErr; } elseif ($httpErr !== '') { $msg .= '。可开启 APP_DEBUG 查看详情,或检查是否启用 php_curl / openssl、防火墙是否拦截出站 HTTPS'; } else { $msg .= '。Windows 常见原因:未启用 curl 扩展,或 php.ini 中 allow_url_fopen=Off;请启用 extension=curl 后重启 PHP'; } return ['ok' => false, 'msg' => $msg]; } $data = json_decode($raw, true); if (!is_array($data)) { return ['ok' => false, 'msg' => '微信接口返回异常']; } if (!empty($data['errcode'])) { $msg = (string) ($data['errmsg'] ?? '微信错误'); return ['ok' => false, 'msg' => $msg . ' (' . ($data['errcode'] ?? '') . ')']; } $openid = (string) ($data['openid'] ?? ''); if ($openid === '') { return ['ok' => false, 'msg' => '未获取到 openid']; } return [ 'ok' => true, 'msg' => 'ok', 'openid' => $openid, 'unionid' => (string) ($data['unionid'] ?? ''), 'session_key' => (string) ($data['session_key'] ?? ''), ]; } /** * 访问微信 HTTPS 接口:优先 cURL(Windows 上比 allow_url_fopen 更可靠),否则 file_get_contents。 * * @param-out string $errorDetail 失败时简要原因(调试展示用) */ private static function httpGet(string $url, string &$errorDetail = ''): ?string { $errorDetail = ''; if (function_exists('curl_init')) { $ch = curl_init($url); if ($ch !== false) { curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, ]); $body = curl_exec($ch); $errno = curl_errno($ch); $cerr = $errno ? (string) curl_error($ch) : ''; $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($body !== false && $body !== '' && $code >= 200 && $code < 300) { return $body; } $errorDetail = $cerr !== '' ? ('cURL ' . $cerr) : ('HTTP ' . $code); } } if (!ini_get('allow_url_fopen')) { $errorDetail = $errorDetail !== '' ? $errorDetail : 'allow_url_fopen=Off 且 cURL 未成功'; return null; } $ctx = stream_context_create([ 'http' => [ 'timeout' => 20, ], 'ssl' => [ 'verify_peer' => true, 'verify_peer_name' => true, ], ]); $body = @file_get_contents($url, false, $ctx); if ($body !== false && $body !== '') { return $body; } if ($errorDetail === '') { $errorDetail = 'file_get_contents 失败(可检查 openssl 扩展与网络)'; } return null; } private static function httpPostJson(string $url, array $payload, string &$errorDetail = ''): ?string { $errorDetail = ''; $json = json_encode($payload, JSON_UNESCAPED_UNICODE); if ($json === false) { $errorDetail = 'JSON 编码失败'; return null; } if (function_exists('curl_init')) { $ch = curl_init($url); if ($ch !== false) { curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POST => true, CURLOPT_POSTFIELDS => $json, CURLOPT_HTTPHEADER => ['Content-Type: application/json; charset=utf-8'], ]); $body = curl_exec($ch); $errno = curl_errno($ch); $cerr = $errno ? (string) curl_error($ch) : ''; $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($body !== false && $body !== '' && $code >= 200 && $code < 300) { return $body; } $errorDetail = $cerr !== '' ? ('cURL ' . $cerr) : ('HTTP ' . $code); } } if (!ini_get('allow_url_fopen')) { $errorDetail = $errorDetail !== '' ? $errorDetail : 'allow_url_fopen=Off 且 cURL 未成功'; return null; } $ctx = stream_context_create([ 'http' => [ 'method' => 'POST', 'header' => "Content-Type: application/json; charset=utf-8\r\n", 'content' => $json, 'timeout' => 20, ], 'ssl' => [ 'verify_peer' => true, 'verify_peer_name' => true, ], ]); $body = @file_get_contents($url, false, $ctx); if ($body !== false && $body !== '') { return $body; } if ($errorDetail === '') { $errorDetail = 'file_get_contents POST 失败'; } return null; } }