From 1ccafe88e23989ec7b09b7663b9a8c7fc5f8efc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B2=A9=E4=BB=9488?= <> Date: Tue, 3 Mar 2026 22:13:32 +0800 Subject: [PATCH] =?UTF-8?q?=E7=9F=AD=E4=BF=A1=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Http/Controllers/API/SmsController.php | 36 ++++++ Laravel/app/Services/TencentSmsApiService.php | 122 +++++++++++++++--- Laravel/routes/api.php | 3 +- Laravel/routes/web.php | 3 + admin/src/router/index.js | 8 ++ admin/src/views/SystemMngr/SmsLog.vue | 8 ++ 6 files changed, 158 insertions(+), 22 deletions(-) create mode 100644 admin/src/views/SystemMngr/SmsLog.vue diff --git a/Laravel/app/Http/Controllers/API/SmsController.php b/Laravel/app/Http/Controllers/API/SmsController.php index c73042e..001537f 100644 --- a/Laravel/app/Http/Controllers/API/SmsController.php +++ b/Laravel/app/Http/Controllers/API/SmsController.php @@ -68,4 +68,40 @@ class SmsController } return $code; } + + public function CallBack(Request $request) + { + // 1. 解析 JSON 数据 (腾讯云回调是数组格式) + $dataList = json_decode($request->getContent(), true); + + // 兼容:如果不是数组,强制转为数组 + if (!is_array($dataList)) { + $dataList = $dataList ? [$dataList] : []; + } + + foreach ($dataList as $item) { + $sid = $item['sid'] ?? null; + + // 2. 只有当 sid 存在时才处理 + if ($sid) { + // 3. 查找并更新:找到就存 JSON,找不到自动忽略 (update 影响行数为 0) + DB::table('sms_log') + ->where('sid', $sid) + ->update([ + 'callback_content' => json_encode($item, JSON_UNESCAPED_UNICODE), + 'updated_at' => now() + ]); + } + } + + // 4. 必须按文档返回特定格式,否则腾讯云会重试 + return response()->json(['result' => 0, 'errmsg' => 'OK']); + } + + public function TestSend() + { + $service = new TencentSmsApiService(); + $ss= $service->send('19933509886',"2567513", ["小明","转入"]); + } + } diff --git a/Laravel/app/Services/TencentSmsApiService.php b/Laravel/app/Services/TencentSmsApiService.php index d252968..08cbbc2 100644 --- a/Laravel/app/Services/TencentSmsApiService.php +++ b/Laravel/app/Services/TencentSmsApiService.php @@ -2,6 +2,7 @@ namespace App\Services; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Http; use Illuminate\Support\Str; @@ -27,13 +28,14 @@ class TencentSmsApiService $this->templateId = "2564483"; } - public function send(string $phoneNumber,string $templateId, array $params = []) + 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, @@ -45,46 +47,124 @@ class TencentSmsApiService $body['TemplateParamSet'] = $params; } + // 注意:JSON_UNESCAPED_UNICODE 和 JSON_UNESCAPED_SLASHES 是腾讯云签名必须的 $payload = json_encode($body, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); -// Canonical Request + // 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}"; -// String to Sign + // 4. 构造待签名字符串 (String to Sign) $algorithm = 'TC3-HMAC-SHA256'; - $date = gmdate('Y-m-d', $timestamp); $credentialScope = "{$date}/{$this->service}/tc3_request"; $hashedCanonicalRequest = hash('sha256', $canonicalRequest); $stringToSign = "{$algorithm}\n{$timestamp}\n{$credentialScope}\n{$hashedCanonicalRequest}"; -// Signature + // 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); // 转为小写 hex 字符串! + $signature = bin2hex($signature); -// Authorization + // 6. 构造 Authorization 头 $authorization = "{$algorithm} Credential={$this->secretId}/{$credentialScope}, SignedHeaders={$signedHeaders}, Signature={$signature}"; -// 发送请求:必须用 $payload 字符串,不能传数组! - $response = Http::withHeaders([ - 'Authorization' => $authorization, - 'Content-Type' => 'application/json; charset=utf-8', - 'Host' => $this->host, - 'X-TC-Action' => $action, - 'X-TC-Timestamp' => $timestamp, - 'X-TC-Version' => $this->version, - 'X-TC-Region' => $this->region, - ])->withBody($payload, 'application/json; charset=utf-8') - ->post("https://{$this->endpoint}"); - - return $response->json(); + // 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; + } } } diff --git a/Laravel/routes/api.php b/Laravel/routes/api.php index 34d94ad..ef51213 100644 --- a/Laravel/routes/api.php +++ b/Laravel/routes/api.php @@ -93,5 +93,6 @@ Route::post('mp/InsertInfo','App\Http\Controllers\API\LiuYanController@Mp_Insert - +Route::any('/SmsCallBack','App\Http\Controllers\API\SmsController@CallBack' )->middleware('log'); +Route::any('/TestSendSms','App\Http\Controllers\API\SmsController@TestSend' )->middleware('log');//测试发送短信 diff --git a/Laravel/routes/web.php b/Laravel/routes/web.php index 08abe1d..eaecb15 100644 --- a/Laravel/routes/web.php +++ b/Laravel/routes/web.php @@ -25,4 +25,7 @@ Route::get('/wxLogin/{env}', function ($env) { }); //微信登录授权获取openid Route::get('/wxGetCode','App\Http\Controllers\API\mH5\LoginController@wxGetCode' ); + + + Route::get('/test','App\Http\Controllers\TestController@DBtest' ); diff --git a/admin/src/router/index.js b/admin/src/router/index.js index 7eb63f2..a920dad 100644 --- a/admin/src/router/index.js +++ b/admin/src/router/index.js @@ -136,6 +136,14 @@ const router = createRouter({ title: '日切' } }, + { + path: '/systemMngr/smsLog', + name: 'SystemMngrSmsLog', + component: () => import('../views/SystemMngr/SmsLog.vue'), + meta: { + title: '短信日志' + } + }, { path: '/yewu/weixinUserList', name: 'YeWuWeixinUserList', diff --git a/admin/src/views/SystemMngr/SmsLog.vue b/admin/src/views/SystemMngr/SmsLog.vue new file mode 100644 index 0000000..02409e3 --- /dev/null +++ b/admin/src/views/SystemMngr/SmsLog.vue @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file