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.

232 lines
7.1 KiB

<?php
namespace App\Services\Sms;
use App\Models\AudienceRegistration;
use App\Models\AudienceSmsLog;
use App\Support\SmsConfig;
use Illuminate\Support\Facades\Log;
class AudienceRegistrationSmsNotifier
{
public const SCENE_PRE_REGISTER = AudienceSmsLog::SCENE_PRE_REGISTER;
public const SCENE_APPROVED = AudienceSmsLog::SCENE_APPROVED;
public function __construct(private readonly SubmailSmsClient $client)
{
}
public function notifyPreRegister(AudienceRegistration $registration): ?AudienceSmsLog
{
$registration->loadMissing('session');
$vars = [
'session' => $this->clip((string) ($registration->session?->name ?? ''), 40),
];
return $this->send(
$registration,
AudienceSmsLog::SCENE_PRE_REGISTER,
trim((string) config('sms.submail.audience_pre_register_template_id')),
$vars,
(string) config('sms.submail.audience_pre_register_content'),
'sms.audience_pre_register'
);
}
public function notifyApproved(AudienceRegistration $registration): ?AudienceSmsLog
{
$registration->loadMissing('session');
$vars = [
'session' => $this->clip((string) ($registration->session?->name ?? ''), 40),
'time' => $this->clip((string) ($registration->approved_event_time_text ?? ''), 40),
'address' => $this->clip((string) ($registration->approved_event_address_text ?? ''), 40),
'url' => $this->ticketUrl($registration),
];
return $this->send(
$registration,
AudienceSmsLog::SCENE_APPROVED,
trim((string) config('sms.submail.audience_approved_template_id')),
$vars,
(string) config('sms.submail.audience_approved_content'),
'sms.audience_approved'
);
}
/**
* @param array<string, string> $vars
*/
private function send(
AudienceRegistration $registration,
string $scene,
string $project,
array $vars,
string $contentTemplate,
string $logPrefix,
): ?AudienceSmsLog {
if (! (bool) config('sms.enabled')) {
return null;
}
if ((string) config('sms.driver') !== 'submail') {
return null;
}
$mobile = trim((string) $registration->phone);
if (preg_match('/^1[3-9]\d{9}$/', $mobile) !== 1) {
Log::warning($logPrefix.' skipped_invalid_mobile', [
'registration_id' => $registration->id,
]);
return null;
}
try {
SmsConfig::assertReadyForRealSending();
} catch (\Throwable $e) {
Log::error($logPrefix.' config_incomplete', [
'registration_id' => $registration->id,
'error' => $e->getMessage(),
]);
return $this->persistLog(
$registration,
$scene,
$project,
$mobile,
$vars,
$contentTemplate,
AudienceSmsLog::STATUS_FAILED,
null,
$e->getMessage(),
);
}
if ($project === '') {
Log::error($logPrefix.' missing_template');
return $this->persistLog(
$registration,
$scene,
$project,
$mobile,
$vars,
$contentTemplate,
AudienceSmsLog::STATUS_FAILED,
null,
'缺少短信模板',
);
}
$result = $this->client->sendTemplate($mobile, $project, $vars);
$fee = is_array($result->response) && isset($result->response['fee']) && is_numeric($result->response['fee'])
? (int) $result->response['fee']
: null;
if (! $result->success) {
Log::error($logPrefix.' failed', [
'registration_id' => $registration->id,
'mobile_mask' => substr($mobile, 0, 3).'****'.substr($mobile, -4),
'provider_code' => $result->providerCode,
'provider_message' => $result->providerMessage,
'provider_request_id' => $result->requestId,
]);
return $this->persistLog(
$registration,
$scene,
$project,
$mobile,
$vars,
$contentTemplate,
AudienceSmsLog::STATUS_FAILED,
$result->requestId,
$result->providerMessage,
$fee,
);
}
return $this->persistLog(
$registration,
$scene,
$project,
$mobile,
$vars,
$contentTemplate,
AudienceSmsLog::STATUS_PENDING,
$result->requestId,
null,
$fee,
);
}
/**
* @param array<string, string> $vars
*/
private function persistLog(
AudienceRegistration $registration,
string $scene,
string $project,
string $mobile,
array $vars,
string $contentTemplate,
string $status,
?string $sendId,
?string $droppedReason,
?int $fee = null,
): AudienceSmsLog {
return AudienceSmsLog::query()->create([
'competition_id' => $registration->competition_id,
'audience_registration_id' => $registration->id,
'scene' => $scene,
'mobile' => $mobile,
'template_id' => $project !== '' ? $project : null,
'content' => $this->renderContent($contentTemplate, $vars),
'vars_json' => $vars,
'send_id' => $sendId,
'status' => $status,
'fee' => $fee,
'dropped_reason' => $droppedReason,
'sent_at' => now(),
]);
}
/**
* @param array<string, string> $vars
*/
public function renderContent(string $template, array $vars): string
{
$content = $template;
foreach ($vars as $key => $value) {
$content = str_replace('{'.$key.'}', $value, $content);
}
return $content;
}
public function ticketUrl(AudienceRegistration $registration): string
{
$registration->loadMissing('competition');
$slug = rawurlencode((string) ($registration->competition?->slug ?? ''));
$ticket = rawurlencode((string) $registration->ticket_code);
$base = rtrim((string) (config('sms.audience_ticket_base_url') ?: config('app.url')), '/');
$path = (string) config('sms.audience_ticket_path', '/admin/c/{slug}/t/{ticket}');
$path = str_replace(['{slug}', '{ticket}'], [$slug, $ticket], $path);
if ($path === '' || $path[0] !== '/') {
$path = '/'.$path;
}
return $base.$path;
}
private function clip(string $value, int $max): string
{
$value = trim($value);
if (mb_strlen($value) <= $max) {
return $value;
}
return mb_substr($value, 0, $max);
}
}