SmsbaoSender.php 3.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485
  1. <?php
  2. declare(strict_types=1);
  3. namespace app\miniapi\service;
  4. use app\miniapi\exception\ApiException;
  5. use app\miniapi\http\ApiResponse;
  6. use support\Log;
  7. /**
  8. * The provider credential is an MD5 password or API key, passed through unchanged.
  9. * Provider status codes are logged without exposing credentials or message contents.
  10. */
  11. final class SmsbaoSender implements SmsSender
  12. {
  13. private const FAILURES = [
  14. '-1' => '参数不全',
  15. '-2' => '账户或密码错误',
  16. '-3' => '账户余额不足',
  17. '-4' => '账户余额已冻结',
  18. '30' => '账户密码错误',
  19. '40' => '账户不存在',
  20. '41' => '账户余额不足',
  21. '43' => '来源IP受限',
  22. '50' => '内容包含敏感词',
  23. '51' => '手机号码不正确',
  24. ];
  25. public function send(string $phone, string $content): void
  26. {
  27. $signature = trim((string) config('mini_api.sms_signature', ''));
  28. if ($signature === '') {
  29. throw new ApiException(50301, '短信签名未配置,请联系管理员', 503);
  30. }
  31. if (!str_starts_with($signature, '【') || !str_ends_with($signature, '】')) {
  32. $signature = '【' . $signature . '】';
  33. }
  34. $request = \request();
  35. $requestId = $request ? ApiResponse::requestId($request) : null;
  36. $credential = (string) config('mini_api.sms_password', '');
  37. $url = (string) config('mini_api.sms_endpoint', '') . '?' . http_build_query([
  38. 'u' => (string) config('mini_api.sms_user', ''),
  39. 'p' => $credential,
  40. 'm' => $phone,
  41. 'c' => $signature . $content,
  42. ]);
  43. $ch = curl_init($url);
  44. curl_setopt_array($ch, [
  45. CURLOPT_RETURNTRANSFER => true,
  46. CURLOPT_CONNECTTIMEOUT => 5,
  47. CURLOPT_TIMEOUT => 10,
  48. CURLOPT_SSL_VERIFYPEER => true,
  49. CURLOPT_SSL_VERIFYHOST => 2,
  50. CURLOPT_FOLLOWLOCATION => false,
  51. ]);
  52. $raw = curl_exec($ch);
  53. $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  54. $errorNumber = curl_errno($ch);
  55. $error = curl_error($ch);
  56. curl_close($ch);
  57. if ($raw === false || $status !== 200) {
  58. $redactions = array_filter([$url, rawurlencode($credential), $credential]);
  59. Log::warning('mini_api.sms_transport', [
  60. 'request_id' => $requestId,
  61. 'http' => $status,
  62. 'curl_errno' => $errorNumber,
  63. 'transport_error' => str_replace($redactions, '[redacted]', $error),
  64. ]);
  65. throw new ApiException(50302, '短信服务暂时不可用,请稍后再试', 503);
  66. }
  67. $code = trim((string) $raw);
  68. if ($code === '0') {
  69. return;
  70. }
  71. // Provider status only. The request URL carries the password, so it is never logged.
  72. Log::warning('mini_api.sms_rejected', [
  73. 'request_id' => $requestId,
  74. 'status' => preg_match('/^-?\d+$/D', $code) ? $code : 'unexpected_response',
  75. 'reason' => self::FAILURES[$code] ?? 'unknown',
  76. ]);
  77. throw new ApiException(50302, '短信发送失败,请稍后再试', 503);
  78. }
  79. }