SendSmsMessage.php 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. <?php
  2. namespace App\Jobs;
  3. use Illuminate\Bus\Queueable;
  4. use Illuminate\Contracts\Queue\ShouldQueue;
  5. use Illuminate\Foundation\Bus\Dispatchable;
  6. use Illuminate\Queue\InteractsWithQueue;
  7. use Illuminate\Queue\SerializesModels;
  8. use RuntimeException;
  9. class SendSmsMessage implements ShouldQueue
  10. {
  11. use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
  12. public function __construct(public int $messageId) {}
  13. public function handle()
  14. {
  15. $msg = \App\Models\SmsMessage::with(['recipients'])->findOrFail($this->messageId);
  16. $msg->update(['status' => 'processing']);
  17. foreach ($msg->recipients as $r) {
  18. if (in_array($r->status, ['sent', 'failed'])) continue;
  19. try {
  20. $phone = $r->phone;
  21. $message = $msg->content;
  22. $params = array(
  23. 'to' => '+39' . $phone,
  24. 'from' => env('SMS_FROM', 'Leezard'),
  25. 'message' => $message,
  26. 'format' => 'json',
  27. );
  28. $this->sms_send($params);
  29. $r->update(['status' => 'sent', 'sent_at' => now(), 'error_message' => null]);
  30. } catch (\Throwable $e) {
  31. $r->update(['status' => 'failed', 'error_message' => $e->getMessage()]);
  32. }
  33. }
  34. $total = $msg->recipients()->count();
  35. $sent = $msg->recipients()->where('status', 'sent')->count();
  36. $failed = $msg->recipients()->where('status', 'failed')->count();
  37. $newStatus = 'draft';
  38. if ($total === 0) {
  39. $newStatus = 'draft';
  40. } elseif ($sent === $total) {
  41. $newStatus = 'sent';
  42. } elseif ($sent > 0 && $failed > 0) {
  43. $newStatus = 'partial';
  44. } else {
  45. $newStatus = 'failed';
  46. }
  47. $msg->update([
  48. 'status' => $newStatus,
  49. 'sent_at' => $sent > 0 ? now() : $msg->sent_at,
  50. ]);
  51. }
  52. public function sms_send(array $params, bool $backup = false)
  53. {
  54. if (!isset($params['format'])) {
  55. $params['format'] = 'json';
  56. }
  57. $url = $backup
  58. ? 'https://api2.smsapi.com/sms.do'
  59. : 'https://api.smsapi.com/sms.do';
  60. $ch = curl_init($url);
  61. curl_setopt_array($ch, [
  62. CURLOPT_POST => true,
  63. CURLOPT_POSTFIELDS => $params,
  64. CURLOPT_RETURNTRANSFER => true,
  65. CURLOPT_HTTPHEADER => [
  66. 'Authorization: Bearer ' . env('SMS_TOKEN'),
  67. ],
  68. CURLOPT_TIMEOUT => 15,
  69. CURLOPT_CONNECTTIMEOUT => 5,
  70. ]);
  71. $content = curl_exec($ch);
  72. $httpStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  73. $curlError = curl_error($ch);
  74. curl_close($ch);
  75. if ($content === false) {
  76. if (!$backup) {
  77. return $this->sms_send($params, true);
  78. }
  79. throw new RuntimeException('SMS API cURL error: ' . $curlError);
  80. }
  81. if ($httpStatus !== 200) {
  82. if (!$backup && $httpStatus >= 500 && $httpStatus < 600) {
  83. return $this->sms_send($params, true);
  84. }
  85. throw new RuntimeException("SMS API HTTP {$httpStatus}: {$content}");
  86. }
  87. $data = json_decode($content, true);
  88. if (json_last_error() !== JSON_ERROR_NONE) {
  89. throw new RuntimeException('SMS API invalid JSON response: ' . $content);
  90. }
  91. if (isset($data['error'])) {
  92. $msg = is_array($data['error'])
  93. ? ($data['error']['message'] ?? json_encode($data['error']))
  94. : $data['error'];
  95. throw new RuntimeException('SMS API error: ' . $msg);
  96. }
  97. if (isset($data['list'][0]['status'])) {
  98. $status = strtoupper($data['list'][0]['status']);
  99. if (in_array($status, ['ERROR', 'FAILED', 'REJECTED'], true)) {
  100. $errMsg = $data['list'][0]['error_message'] ?? 'Unknown SMS API error';
  101. throw new RuntimeException("SMS API message error ({$status}): {$errMsg}");
  102. }
  103. }
  104. return $data;
  105. }
  106. }