$vars */ public function sendTemplate(string $to, string $project, array $vars): SmsSendResult { $appId = trim((string) config('sms.submail.app_id')); $appKey = trim((string) config('sms.submail.app_key')); $endpoint = rtrim((string) config('sms.submail.endpoint'), '?&'); $timeout = (int) config('sms.timeout', 5); $signName = $this->normalizeSignName((string) config('sms.submail.sign_name')); $payload = [ 'appid' => $appId, 'to' => $to, 'project' => $project, 'signature' => $appKey, 'vars' => json_encode($vars, JSON_UNESCAPED_UNICODE), ]; if ($signName !== '') { $payload['sms_signature'] = $signName; } try { $response = Http::timeout($timeout) ->acceptJson() ->asForm() ->post($endpoint, $payload); } catch (\Throwable $e) { Log::error('sms.submail network_error', [ 'project' => $project, 'mobile_mask' => $this->maskMobile($to), 'error' => $e->getMessage(), ]); return SmsSendResult::failure( SmsVerification::PROVIDER_SUBMAIL, null, 'NETWORK_ERROR', 'Submail SMS request failed: '.$e->getMessage(), ['error' => 'network_error', 'exception' => $e::class] ); } $json = $response->json(); $summary = $this->responseSummary(is_array($json) ? $json : [], $response->status()); $status = is_array($json) && isset($json['status']) ? (string) $json['status'] : ''; $sendId = is_array($json) && isset($json['send_id']) && is_scalar($json['send_id']) ? (string) $json['send_id'] : null; $code = is_array($json) && isset($json['code']) && is_scalar($json['code']) ? (string) $json['code'] : (string) $response->status(); $message = is_array($json) && isset($json['msg']) && is_scalar($json['msg']) ? (string) $json['msg'] : ''; if ($response->successful() && $status === 'success') { return SmsSendResult::success( SmsVerification::PROVIDER_SUBMAIL, $sendId, $code !== '' ? $code : 'success', $message !== '' ? $message : 'OK', $summary ); } if (! $response->successful() && $code === (string) $response->status()) { $code = 'HTTP_'.$response->status(); } return SmsSendResult::failure( SmsVerification::PROVIDER_SUBMAIL, $sendId, $code !== '' ? $code : 'SEND_FAILED', $message !== '' ? $message : 'Submail SMS send failed.', $summary ); } /** * @param array $query * @return array */ public function fetchLog(array $query = []): array { $appId = trim((string) config('sms.submail.app_id')); $appKey = trim((string) config('sms.submail.app_key')); $endpoint = rtrim((string) config('sms.submail.log_endpoint', 'https://log.mysubmail.com/sms/log.json'), '?&'); $timeout = (int) config('sms.timeout', 5); $payload = array_filter([ 'appid' => $appId, 'signature' => $appKey, 'start_date' => $query['start_date'] ?? null, 'end_date' => $query['end_date'] ?? null, 'to' => $query['to'] ?? null, 'send_id' => $query['send_id'] ?? null, 'status' => $query['status'] ?? null, 'rows' => $query['rows'] ?? 50, 'offset' => $query['offset'] ?? 0, ], fn ($value) => $value !== null && $value !== ''); try { $response = Http::timeout($timeout) ->acceptJson() ->asForm() ->post($endpoint, $payload); } catch (\Throwable $e) { throw new \RuntimeException('Submail log API request failed: '.$e->getMessage(), 0, $e); } $json = $response->json(); if (! is_array($json) || ($json['status'] ?? '') !== 'success') { $message = is_array($json) && isset($json['msg']) && is_scalar($json['msg']) ? (string) $json['msg'] : 'Submail log API failed.'; throw new \RuntimeException($message); } return $json; } /** * @param array $response * @return array */ private function responseSummary(array $response, int $httpStatus): array { return [ 'http_status' => $httpStatus, 'status' => $response['status'] ?? null, 'send_id' => $response['send_id'] ?? null, 'fee' => $response['fee'] ?? null, 'code' => $response['code'] ?? null, 'msg' => $response['msg'] ?? null, ]; } private function maskMobile(string $mobile): string { if (strlen($mobile) < 11) { return '***'; } return substr($mobile, 0, 3).'****'.substr($mobile, -4); } private function normalizeSignName(string $signName): string { $signName = trim($signName); $stripped = preg_replace('/^[\[【]+|[\]】]+$/u', '', $signName); return is_string($stripped) ? trim($stripped) : $signName; } }