You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

162 lines
5.6 KiB

<?php
namespace App\Services\Sms;
use App\Models\SmsVerification;
use App\Support\SmsConfig;
use Illuminate\Support\Facades\Http;
class TencentCloudSmsSender implements SmsSender
{
public function sendVerification(SmsVerification $record): SmsSendResult
{
try {
SmsConfig::assertReadyForRealSending();
} catch (\Throwable $e) {
return SmsSendResult::failure(
SmsVerification::PROVIDER_TENCENTCLOUD,
null,
'CONFIG_INCOMPLETE',
$e->getMessage(),
['error' => 'config_incomplete']
);
}
$endpoint = rtrim((string) config('sms.tencentcloud.endpoint'), '?&');
$sdkAppId = (string) config('sms.tencentcloud.sdk_app_id');
$appKey = (string) config('sms.tencentcloud.app_key');
$timeout = (int) config('sms.timeout', 5);
$random = (string) random_int(100000, 999999999);
$timestamp = time();
$mobile = (string) $record->mobile;
$payload = [
'ext' => '',
'extend' => '',
'params' => [
(string) $record->code,
(string) data_get($record->request_payload_json, 'ttl_minutes', '5'),
],
'sig' => hash(
'sha256',
"appkey={$appKey}&random={$random}&time={$timestamp}&mobile={$mobile}"
),
'sign' => (string) config('sms.tencentcloud.sign_name'),
'tel' => [
'mobile' => $mobile,
'nationcode' => '86',
],
'time' => $timestamp,
'tpl_id' => (int) config('sms.tencentcloud.template_id'),
];
$url = $endpoint.'?'.http_build_query([
'sdkappid' => $sdkAppId,
'random' => $random,
], '', '&', PHP_QUERY_RFC3986);
try {
$response = Http::timeout($timeout)
->acceptJson()
->asJson()
->post($url, $payload);
} catch (\Throwable $e) {
return SmsSendResult::failure(
SmsVerification::PROVIDER_TENCENTCLOUD,
null,
'NETWORK_ERROR',
'Tencent Cloud SMS request failed: '.$e->getMessage(),
['error' => 'network_error', 'exception' => $e::class]
);
}
$json = $response->json();
if (! $response->successful()) {
$responseSummary = is_array($json)
? $this->responseSummary($json, $response->status())
: ['http_status' => $response->status(), 'error' => 'http_error'];
$providerMessage = is_array($json) && isset($json['errmsg']) && is_scalar($json['errmsg'])
? (string) $json['errmsg']
: 'Tencent Cloud SMS returned HTTP '.$response->status().'.';
return SmsSendResult::failure(
SmsVerification::PROVIDER_TENCENTCLOUD,
is_array($json) ? $this->requestId($json) : null,
'HTTP_'.$response->status(),
$providerMessage,
$responseSummary
);
}
if (! is_array($json)) {
return SmsSendResult::failure(
SmsVerification::PROVIDER_TENCENTCLOUD,
null,
'INVALID_JSON_RESPONSE',
'Tencent Cloud SMS returned invalid JSON.',
['error' => 'invalid_json_response', 'http_status' => $response->status()]
);
}
$rawResult = $json['result'] ?? null;
if (! is_int($rawResult) && (! is_string($rawResult) || preg_match('/^-?\d+$/', $rawResult) !== 1)) {
return SmsSendResult::failure(
SmsVerification::PROVIDER_TENCENTCLOUD,
$this->requestId($json),
'PROTOCOL_ERROR',
'Tencent Cloud SMS response is missing an integer result field.',
$this->responseSummary($json, $response->status())
);
}
$result = (int) $rawResult;
$requestId = $this->requestId($json);
$providerCode = (string) $result;
$providerMessage = isset($json['errmsg']) && is_scalar($json['errmsg'])
? (string) $json['errmsg']
: '';
if ($result === 0) {
return SmsSendResult::success(
SmsVerification::PROVIDER_TENCENTCLOUD,
$requestId,
$providerCode,
$providerMessage,
$this->responseSummary($json, $response->status())
);
}
return SmsSendResult::failure(
SmsVerification::PROVIDER_TENCENTCLOUD,
$requestId,
$providerCode,
$providerMessage !== '' ? $providerMessage : 'Tencent Cloud SMS send failed.',
$this->responseSummary($json, $response->status())
);
}
/** @param array<string, mixed> $response */
private function requestId(array $response): ?string
{
$sid = $response['sid'] ?? null;
return is_string($sid) || is_numeric($sid) ? (string) $sid : null;
}
/**
* @param array<string, mixed> $response
* @return array<string, mixed>
*/
private function responseSummary(array $response, int $httpStatus): array
{
return [
'http_status' => $httpStatus,
'result' => $response['result'] ?? null,
'errmsg' => $response['errmsg'] ?? null,
'sid' => $response['sid'] ?? null,
'fee' => $response['fee'] ?? null,
'ext' => $response['ext'] ?? null,
];
}
}