|
|
|
|
<?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,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
}
|