'参数不全', '-2' => '账户或密码错误', '-3' => '账户余额不足', '-4' => '账户余额已冻结', '30' => '账户密码错误', '40' => '账户不存在', '41' => '账户余额不足', '43' => '来源IP受限', '50' => '内容包含敏感词', '51' => '手机号码不正确', ]; public function send(string $phone, string $content): void { $signature = trim((string) config('mini_api.sms_signature', '')); if ($signature === '') { throw new ApiException(50301, '短信签名未配置,请联系管理员', 503); } if (!str_starts_with($signature, '【') || !str_ends_with($signature, '】')) { $signature = '【' . $signature . '】'; } $request = \request(); $requestId = $request ? ApiResponse::requestId($request) : null; $credential = (string) config('mini_api.sms_password', ''); $url = (string) config('mini_api.sms_endpoint', '') . '?' . http_build_query([ 'u' => (string) config('mini_api.sms_user', ''), 'p' => $credential, 'm' => $phone, 'c' => $signature . $content, ]); $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 10, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_FOLLOWLOCATION => false, ]); $raw = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); $errorNumber = curl_errno($ch); $error = curl_error($ch); curl_close($ch); if ($raw === false || $status !== 200) { $redactions = array_filter([$url, rawurlencode($credential), $credential]); Log::warning('mini_api.sms_transport', [ 'request_id' => $requestId, 'http' => $status, 'curl_errno' => $errorNumber, 'transport_error' => str_replace($redactions, '[redacted]', $error), ]); throw new ApiException(50302, '短信服务暂时不可用,请稍后再试', 503); } $code = trim((string) $raw); if ($code === '0') { return; } // Provider status only. The request URL carries the password, so it is never logged. Log::warning('mini_api.sms_rejected', [ 'request_id' => $requestId, 'status' => preg_match('/^-?\d+$/D', $code) ? $code : 'unexpected_response', 'reason' => self::FAILURES[$code] ?? 'unknown', ]); throw new ApiException(50302, '短信发送失败,请稍后再试', 503); } }