From ba9adad10d0cba4f29a19ca2f638920748a51127 Mon Sep 17 00:00:00 2001 From: lion <120344285@qq.com> Date: Sat, 2 May 2026 09:58:11 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ExpirePastReservationsCommand.php | 2 +- .../Api/ActivityRegistrationController.php | 9 ++- app/Services/NoShowBlacklistService.php | 23 +++++++- app/Services/ReservationExpiryService.php | 57 ++++++++++++++++--- 4 files changed, 77 insertions(+), 14 deletions(-) diff --git a/app/Console/Commands/ExpirePastReservationsCommand.php b/app/Console/Commands/ExpirePastReservationsCommand.php index be4ebd5..b0bbc46 100644 --- a/app/Console/Commands/ExpirePastReservationsCommand.php +++ b/app/Console/Commands/ExpirePastReservationsCommand.php @@ -9,7 +9,7 @@ class ExpirePastReservationsCommand extends Command { protected $signature = 'reservations:expire-past'; - protected $description = '将活动日已过仍未核销的待核销预约标记为已过期(expired)'; + protected $description = '将场次结束/活动日/入馆日已过仍未核销的待核销预约标记为已过期(expired)'; public function handle(ReservationExpiryService $expiry): int { diff --git a/app/Http/Controllers/Api/ActivityRegistrationController.php b/app/Http/Controllers/Api/ActivityRegistrationController.php index 27ce311..5742370 100644 --- a/app/Http/Controllers/Api/ActivityRegistrationController.php +++ b/app/Http/Controllers/Api/ActivityRegistrationController.php @@ -5,6 +5,7 @@ namespace App\Http\Controllers\Api; use App\Http\Controllers\Controller; use App\Models\Blacklist; use App\Models\Reservation; +use App\Services\ReservationExpiryService; use Carbon\Carbon; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -12,8 +13,10 @@ use Symfony\Component\HttpFoundation\StreamedResponse; class ActivityRegistrationController extends Controller { - public function index(Request $request): JsonResponse + public function index(Request $request, ReservationExpiryService $reservationExpiryService): JsonResponse { + $reservationExpiryService->expireStalePendingReservations(); + $kind = (string) $request->input('reservation_kind', 'activity'); if ($kind === 'ticket_grab') { $query = Reservation::with([ @@ -97,8 +100,10 @@ class ActivityRegistrationController extends Controller return response()->json($page); } - public function export(Request $request): StreamedResponse + public function export(Request $request, ReservationExpiryService $reservationExpiryService): StreamedResponse { + $reservationExpiryService->expireStalePendingReservations(); + $query = Reservation::with([ 'venue:id,name', 'activity:id,title', diff --git a/app/Services/NoShowBlacklistService.php b/app/Services/NoShowBlacklistService.php index 8b7bd89..3bc322d 100644 --- a/app/Services/NoShowBlacklistService.php +++ b/app/Services/NoShowBlacklistService.php @@ -117,15 +117,34 @@ class NoShowBlacklistService } /** - * 未履约场次:expired;或定时任务尚未执行时的 pending + 活动日已过。 + * 未履约场次:expired;或定时任务尚未执行时的 pending + 场次已结束/活动日已过(与 {@see ReservationExpiryService} 一致)。 */ private function noShowBaseQuery(string $today) { + $now = Carbon::now((string) config('app.timezone')); + return Reservation::query() ->join('activity_days', 'activity_days.id', '=', 'reservations.activity_day_id') ->whereNull('reservations.verified_at') ->whereNotNull('reservations.activity_day_id') - ->whereDate('activity_days.activity_date', '<', $today) + ->where(function ($q) use ($today, $now) { + $q->where(function ($q2) use ($now) { + $q2->whereNotNull('activity_days.session_start_at') + ->whereNotNull('activity_days.session_end_at') + ->whereNotNull('activity_days.booking_deadline_at') + ->where('activity_days.session_end_at', '<', $now); + })->orWhere(function ($q2) use ($today) { + $q2->where(function ($q3) { + $q3->whereNull('activity_days.session_start_at') + ->orWhereNull('activity_days.session_end_at') + ->orWhereNull('activity_days.booking_deadline_at'); + })->whereDate('activity_days.activity_date', '<', $today); + }); + }) + ->where(function ($q) { + $q->whereNull('reservations.reservation_kind') + ->orWhere('reservations.reservation_kind', Reservation::KIND_ACTIVITY); + }) ->where(function ($q) { $q->where('reservations.status', 'expired') ->orWhere('reservations.status', 'pending'); diff --git a/app/Services/ReservationExpiryService.php b/app/Services/ReservationExpiryService.php index 77711b6..9e6fb4c 100644 --- a/app/Services/ReservationExpiryService.php +++ b/app/Services/ReservationExpiryService.php @@ -4,25 +4,23 @@ namespace App\Services; use App\Models\Reservation; use Carbon\Carbon; +use Illuminate\Support\Collection; class ReservationExpiryService { /** - * 将「活动日已过、仍为待核销、未核销」的预约标记为 expired(与未履约统计一致)。 + * 将「场次/活动日已过、仍为待核销、未核销」的预约标记为 expired。 + * - 场次模式:以 activity_days.session_end_at 为准(晚于「活动日」日历的场次结束时刻)。 + * - 非场次(仅日历日):仍以 activity_date 早于今天为准。 + * - 抢票:入馆日 entry_date 早于今天。 */ public function expireStalePendingReservations(): int { $tz = (string) config('app.timezone'); $today = Carbon::now($tz)->toDateString(); + $now = Carbon::now($tz); - $ids = 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) - ->pluck('reservations.id'); - + $ids = $this->stalePendingReservationIds($today, $now); if ($ids->isEmpty()) { return 0; } @@ -32,4 +30,45 @@ class ReservationExpiryService 'updated_at' => now(), ]); } + + /** + * @return Collection + */ + private function stalePendingReservationIds(string $today, Carbon $now): Collection + { + $idsActivity = 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') + ->where(function ($q) use ($today, $now) { + $q->where(function ($q2) use ($now) { + $q2->whereNotNull('activity_days.session_start_at') + ->whereNotNull('activity_days.session_end_at') + ->whereNotNull('activity_days.booking_deadline_at') + ->where('activity_days.session_end_at', '<', $now); + })->orWhere(function ($q2) use ($today) { + $q2->where(function ($q3) { + $q3->whereNull('activity_days.session_start_at') + ->orWhereNull('activity_days.session_end_at') + ->orWhereNull('activity_days.booking_deadline_at'); + })->whereDate('activity_days.activity_date', '<', $today); + }); + }) + ->where(function ($q) { + $q->whereNull('reservations.reservation_kind') + ->orWhere('reservations.reservation_kind', Reservation::KIND_ACTIVITY); + }) + ->pluck('reservations.id'); + + $idsTicketGrab = Reservation::query() + ->where('reservation_kind', Reservation::KIND_TICKET_GRAB) + ->where('status', 'pending') + ->whereNull('verified_at') + ->whereNotNull('entry_date') + ->whereDate('entry_date', '<', $today) + ->pluck('id'); + + return $idsActivity->merge($idsTicketGrab)->unique()->values(); + } }