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 $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; } }