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

171 lines
6.7 KiB
PHP

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?php
namespace App\Services;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
class TencentSmsApiService
{
protected string $secretId;
protected string $secretKey;
protected string $sdkAppId;
protected string $signName;
protected string $templateId;
protected string $endpoint = 'sms.tencentcloudapi.com';
protected string $service = 'sms';
protected string $host = 'sms.tencentcloudapi.com';
protected string $region = 'ap-guangzhou'; // 根据你的需求调整
protected string $version = '2021-01-11';
public function __construct()
{
$this->secretId ="AKIDvxuqiU3OYbR3RcZJrBIDuOegU5YxEQWa";
$this->secretKey = "xnUyGK9lAnHYcS3SecOyLi2GOOOH4h7K";
$this->sdkAppId = "1401058974";
$this->signName ="秦皇岛安尔然";
$this->templateId = "2564483";
}
public function send(string $phoneNumber, string $templateId, array $params = [])
{
// 1. 基础配置与时间戳
$action = 'SendSms';
$timestamp = time();
$date = gmdate('Y-m-d', $timestamp);
// 2. 构造请求体 (Body)
$body = [
'PhoneNumberSet' => [$phoneNumber],
'SmsSdkAppId' => $this->sdkAppId,
'SignName' => $this->signName,
'TemplateId' => $templateId,
];
if (!empty($params)) {
$body['TemplateParamSet'] = $params;
}
// 注意JSON_UNESCAPED_UNICODE 和 JSON_UNESCAPED_SLASHES 是腾讯云签名必须的
$payload = json_encode($body, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
// 3. 构造规范请求 (Canonical Request)
$canonicalUri = '/';
$canonicalQueryString = '';
$canonicalHeaders = "content-type:application/json; charset=utf-8\nhost:{$this->host}\n";
$signedHeaders = 'content-type;host';
$hashedPayload = hash('sha256', $payload);
$canonicalRequest = "POST\n{$canonicalUri}\n{$canonicalQueryString}\n{$canonicalHeaders}\n{$signedHeaders}\n{$hashedPayload}";
// 4. 构造待签名字符串 (String to Sign)
$algorithm = 'TC3-HMAC-SHA256';
$credentialScope = "{$date}/{$this->service}/tc3_request";
$hashedCanonicalRequest = hash('sha256', $canonicalRequest);
$stringToSign = "{$algorithm}\n{$timestamp}\n{$credentialScope}\n{$hashedCanonicalRequest}";
// 5. 计算签名 (Signature)
$kSecret = 'TC3' . $this->secretKey;
$kDate = hash_hmac('sha256', $date, $kSecret, true);
$kService = hash_hmac('sha256', $this->service, $kDate, true);
$kSigning = hash_hmac('sha256', 'tc3_request', $kService, true);
$signature = hash_hmac('sha256', $stringToSign, $kSigning, true);
$signature = bin2hex($signature);
// 6. 构造 Authorization 头
$authorization = "{$algorithm} Credential={$this->secretId}/{$credentialScope}, SignedHeaders={$signedHeaders}, Signature={$signature}";
// 7. 准备数据库初始数据 (此时尚未插入,等待请求结果)
$logData = [
'phone' => $phoneNumber,
'content' => json_encode($params, JSON_UNESCAPED_UNICODE),
'sid' => null,
'status' => 'sending', // 初始状态
'remark' => null
];
$msgId = null;
try {
// 8. 发送 HTTP 请求
$response = Http::withHeaders([
'Authorization' => $authorization,
'Content-Type' => 'application/json; charset=utf-8',
'Host' => $this->host,
'X-TC-Action' => $action,
'X-TC-Timestamp' => (string)$timestamp,
'X-TC-Version' => $this->version,
'X-TC-Region' => $this->region,
])
->withBody($payload, 'application/json; charset=utf-8')
->post("https://{$this->endpoint}");
// 9. 解析响应数据
$responseData = $response->json();
// 初始化提取变量
$serialNo = null;
$code = 'Unknown';
$message = 'Unknown response format';
$isSuccess = false;
// 防御性检查:确保 Response 结构和 SendStatusSet 存在
if (isset($responseData['Response'])
&& isset($responseData['Response']['SendStatusSet'])
&& is_array($responseData['Response']['SendStatusSet'])
&& count($responseData['Response']['SendStatusSet']) > 0) {
$statusInfo = $responseData['Response']['SendStatusSet'][0];
$serialNo = $statusInfo['SerialNo'] ?? null;
$code = $statusInfo['Code'] ?? 'UnknownCode';
$message = $statusInfo['Message'] ?? 'No message returned';
// 腾讯云成功标识通常为 "Ok"
if ($code === 'Ok') {
$isSuccess = true;
}
} else {
// 如果返回结构完全不对(例如鉴权失败直接返回 Error 对象)
if (isset($responseData['Response']['Error'])) {
$code = $responseData['Response']['Error']['Code'] ?? 'ApiError';
$message = $responseData['Response']['Error']['Message'] ?? 'API Error occurred';
} else {
$message = 'Invalid response structure from provider';
}
}
// 10. 更新日志数据
$logData['sid'] = $serialNo;
$logData['status'] = $code; // 存储业务状态码 (Ok 或 FailedOperation.xxx)
$logData['remark'] = $message; // 存储具体错误信息
// 11. 写入数据库 (一次性插入完整记录)
$msgId = DB::table('sms_log')->insertGetId($logData);
// 如果业务逻辑失败(有 sid 但 code 不是 Ok可以选择记录警告日志
if (!$isSuccess) {
// 这里可以根据需求选择是否抛出异常,或者仅记录日志
// \Log::warning("SMS Send Failed: {$phoneNumber}, Code: {$code}, Msg: {$message}, Sid: {$serialNo}");
}
return $responseData;
} catch (\Exception $e) {
// 12. 异常处理:网络错误、超时、签名计算错误等
// 更新日志数据为失败状态
$logData['status'] = 'RequestFailed'; // 标记为请求级失败
$logData['remark'] = $e->getMessage();
// sid 保持 null
// 写入数据库,确保不留脏数据
$msgId = DB::table('sms_log')->insertGetId($logData);
// 重新抛出异常,让上层调用者知道发送失败了
throw $e;
}
}
}