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.

207 lines
7.3 KiB

<?php
namespace App\Services;
use App\Models\AdminUser;
use App\Models\AudienceRegistration;
use App\Models\AudienceSession;
use App\Models\Competition;
use App\Models\CompetitionAdmin;
use App\Models\User;
use App\Services\Sms\AudienceRegistrationSmsNotifier;
use App\Support\BizDateTime;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\ValidationException;
class AudienceRegistrationService
{
public function __construct(private readonly AudienceRegistrationSmsNotifier $smsNotifier)
{
}
/**
* @param array{audience_session_id: int, name: string, company?: string|null, job?: string|null, purpose: string} $data
*/
public function register(User $user, Competition $competition, array $data): AudienceRegistration
{
$registration = DB::transaction(function () use ($user, $competition, $data) {
/** @var AudienceSession|null $session */
$session = AudienceSession::query()
->where('competition_id', $competition->id)
->whereKey($data['audience_session_id'])
->lockForUpdate()
->first();
if ($session === null) {
throw ValidationException::withMessages([
'audience_session_id' => ['场次不存在'],
]);
}
if (! $session->enabled) {
throw ValidationException::withMessages([
'audience_session_id' => ['该场次暂未开放报名'],
]);
}
$now = now()->timezone(BizDateTime::TIMEZONE);
$status = $session->windowStatus($now);
if ($status === AudienceSession::STATUS_NOT_STARTED) {
throw ValidationException::withMessages([
'audience_session_id' => ['该场次尚未开始报名'],
]);
}
if ($status === AudienceSession::STATUS_EXPIRED) {
throw ValidationException::withMessages([
'audience_session_id' => ['该场次报名已截止'],
]);
}
$already = AudienceRegistration::query()
->where('audience_session_id', $session->id)
->where('user_id', $user->id)
->exists();
if ($already) {
throw ValidationException::withMessages([
'audience_session_id' => ['您已报名该场次'],
]);
}
$count = AudienceRegistration::query()
->where('audience_session_id', $session->id)
->count();
if ($count >= (int) $session->capacity) {
throw ValidationException::withMessages([
'audience_session_id' => ['该场次已满额'],
]);
}
return AudienceRegistration::query()->create([
'competition_id' => $competition->id,
'audience_session_id' => $session->id,
'user_id' => $user->id,
'name' => $data['name'],
'phone' => trim((string) $user->mobile),
'company' => $this->nullableText($data['company'] ?? null),
'job' => $this->nullableText($data['job'] ?? null),
'purpose' => $data['purpose'],
'answers_json' => is_array($data['answers_json'] ?? null) ? $data['answers_json'] : null,
'review_status' => AudienceRegistration::STATUS_PENDING,
'registered_at' => now(),
]);
});
try {
$this->smsNotifier->notifyPreRegister($registration);
} catch (\Throwable $e) {
Log::error('sms.audience_pre_register unexpected_error', [
'registration_id' => $registration->id,
'error' => $e->getMessage(),
]);
}
return $registration;
}
/**
* @param list<int> $ids
* @return array{approved_count: int, sms_sent: int, sms_failed: int, skipped_count: int}
*/
public function approve(
Competition $competition,
array $ids,
string $eventTimeText,
string $eventAddressText,
AdminUser|CompetitionAdmin $admin,
): array {
$ids = array_values(array_unique(array_filter(array_map('intval', $ids))));
if ($ids === []) {
throw ValidationException::withMessages([
'ids' => ['请选择待审核的观众'],
]);
}
$eventTimeText = trim($eventTimeText);
$eventAddressText = trim($eventAddressText);
if ($eventTimeText === '') {
throw ValidationException::withMessages([
'event_time_text' => ['请填写路演时间'],
]);
}
if ($eventAddressText === '') {
throw ValidationException::withMessages([
'event_address_text' => ['请填写路演地点'],
]);
}
$approved = DB::transaction(function () use ($competition, $ids, $eventTimeText, $eventAddressText, $admin) {
$rows = AudienceRegistration::query()
->where('competition_id', $competition->id)
->whereIn('id', $ids)
->lockForUpdate()
->get();
if ($rows->count() !== count($ids)) {
throw ValidationException::withMessages([
'ids' => ['存在无效的报名记录'],
]);
}
$pending = $rows->filter(fn (AudienceRegistration $row) => $row->isPending());
if ($pending->isEmpty()) {
throw ValidationException::withMessages([
'ids' => ['所选记录均已审核通过'],
]);
}
$now = now();
foreach ($pending as $row) {
$row->review_status = AudienceRegistration::STATUS_APPROVED;
$row->approved_event_time_text = $eventTimeText;
$row->approved_event_address_text = $eventAddressText;
$row->reviewed_at = $now;
if ($admin instanceof CompetitionAdmin) {
$row->reviewed_by_competition_admin_id = $admin->id;
$row->reviewed_by_admin_id = null;
} else {
$row->reviewed_by_admin_id = $admin->id;
$row->reviewed_by_competition_admin_id = null;
}
$row->save();
}
return $pending->values();
});
$smsSent = 0;
$smsFailed = 0;
foreach ($approved as $row) {
try {
$this->smsNotifier->notifyApproved($row);
$smsSent++;
} catch (\Throwable $e) {
$smsFailed++;
Log::error('sms.audience_approved unexpected_error', [
'registration_id' => $row->id,
'error' => $e->getMessage(),
]);
}
}
return [
'approved_count' => $approved->count(),
'sms_sent' => $smsSent,
'sms_failed' => $smsFailed,
'skipped_count' => count($ids) - $approved->count(),
];
}
private function nullableText(mixed $value): ?string
{
$s = trim((string) ($value ?? ''));
return $s === '' ? null : $s;
}
}