toDateString(); $phones = Reservation::query() ->join('activity_days', 'activity_days.id', '=', 'reservations.activity_day_id') ->where('reservations.status', 'pending') ->whereNull('reservations.verified_at') ->whereNotNull('reservations.activity_day_id') ->whereDate('activity_days.activity_date', '<', $today) ->distinct() ->pluck('reservations.visitor_phone'); $applied = 0; foreach ($phones as $phone) { if (PhoneBookingBan::isGloballyBlocked((string) $phone)) { continue; } if ($this->applyBanIfNeeded((string) $phone, $tz, $today)) { $applied++; } } return $applied; } /** 预约前对单号同步一次,避免仅依赖定时任务产生空窗期。 */ public function syncForPhone(string $phone): bool { $tz = (string) config('app.timezone'); $today = Carbon::now($tz)->toDateString(); if (PhoneBookingBan::isGloballyBlocked($phone)) { return false; } return $this->applyBanIfNeeded($phone, $tz, $today); } private function applyBanIfNeeded(string $phone, string $tz, string $today): bool { $cutoff = $this->noShowCutoffDateString($phone); /** @var Collection $rows */ $rows = Reservation::query() ->join('activity_days', 'activity_days.id', '=', 'reservations.activity_day_id') ->join('activities', 'activities.id', '=', 'reservations.activity_id') ->where('reservations.visitor_phone', $phone) ->where('reservations.status', 'pending') ->whereNull('reservations.verified_at') ->whereNotNull('reservations.activity_day_id') ->whereDate('activity_days.activity_date', '<', $today) ->when($cutoff, fn ($q) => $q->whereDate('activity_days.activity_date', '>=', $cutoff)) ->orderByRaw('activities.start_at IS NULL ASC') ->orderBy('activities.start_at') ->orderBy('activity_days.activity_date') ->orderBy('reservations.id') ->select('reservations.*') ->get(); if ($rows->count() < 3) { return false; } $third = $rows->values()->get(2); $third->loadMissing(['activity', 'activityDay']); $activity = $third->activity; $day = $third->activityDay; if (! $activity || ! $day) { return false; } $anchor = $activity->start_at ? $activity->start_at->copy()->timezone($tz)->startOfDay() : Carbon::parse($day->activity_date->format('Y-m-d'), $tz)->startOfDay(); $banUntil = $anchor->copy()->addMonths(3)->startOfDay(); if ($banUntil->lte(now())) { return false; } PhoneBookingBan::query()->updateOrCreate( ['visitor_phone' => $phone], [ 'ban_until' => $banUntil, 'reason' => '累计3次预约成功后未核销', ], ); return true; } /** * 禁约期满后,新统计的「未核销」场次须从该日期(含)起算。 */ private function noShowCutoffDateString(string $phone): ?string { $max = PhoneBookingBan::query() ->where('visitor_phone', $phone) ->where('ban_until', '<=', now()) ->max('ban_until'); if (! $max) { return null; } return Carbon::parse($max)->timezone(config('app.timezone'))->toDateString(); } }