toDateString(); if ((string) ($reservation->reservation_kind ?? 'activity') === Reservation::KIND_TICKET_GRAB) { $ed = $reservation->entry_date; $dateStr = $ed instanceof CarbonInterface ? $ed->format('Y-m-d') : ($ed ? (string) $ed : ''); if ($dateStr === '') { return '该抢票预约未填写入馆日,无法核销'; } if ($dateStr !== $today) { return '仅可核销入馆日为今日的预约。该预约入馆日为:'.$dateStr; } return null; } $reservation->loadMissing('activityDay'); if (! $reservation->activity_day_id || ! $reservation->activityDay) { return '该预约未关联活动场次,仅支持核销「活动日为今日」的预约'; } $ad = $reservation->activityDay->activity_date; $dateStr = $ad instanceof CarbonInterface ? $ad->format('Y-m-d') : (string) $ad; if ($dateStr !== $today) { return '仅可核销活动日为今日的预约。该预约活动日为:'.$dateStr; } return null; } private function actingPortalCredential(Request $request): ?VerifyPortalCredential { $u = $request->user(); return $u instanceof VerifyPortalCredential ? $u : null; } private function assertPortalActiveOrAbort(VerifyPortalCredential $c): void { if (! $c->portalAcceptsVerification()) { abort(403, '活动已结束,核销登录已失效'); } } private function applyPortalScope(Builder $query, VerifyPortalCredential $cred): void { $query->where('venue_id', $cred->venue_id); if ($cred->portal_kind === VerifyPortalCredential::KIND_ACTIVITY) { $query->where('activity_id', $cred->portal_id) ->where(function ($q) { $q->whereNull('reservation_kind') ->orWhere('reservation_kind', Reservation::KIND_ACTIVITY); }); } else { $query->where('reservation_kind', Reservation::KIND_TICKET_GRAB) ->where('ticket_grab_event_id', $cred->portal_id); } } private function ensureReservationAccess(Request $request, Reservation $reservation): void { $cred = $this->actingPortalCredential($request); if ($cred !== null) { $this->assertPortalActiveOrAbort($cred); if ((int) $reservation->venue_id !== (int) $cred->venue_id) { abort(403, '无权核销该预约'); } if ($cred->portal_kind === VerifyPortalCredential::KIND_ACTIVITY) { if ((int) ($reservation->activity_id ?? 0) !== (int) $cred->portal_id) { abort(403, '该预约不属于本活动'); } if ((string) ($reservation->reservation_kind ?? '') === Reservation::KIND_TICKET_GRAB) { abort(403, '该预约不属于本活动'); } } else { if ((string) $reservation->reservation_kind !== Reservation::KIND_TICKET_GRAB || (int) ($reservation->ticket_grab_event_id ?? 0) !== (int) $cred->portal_id) { abort(403, '该预约不属于本抢票活动'); } } return; } /** @var User $user */ $user = $request->user(); if ($user->isSuperAdmin()) { return; } $allowed = $user->venues()->where('venues.id', $reservation->venue_id)->exists(); abort_unless($allowed, 403, '仅可核销已绑定场馆预约'); } /** * 扫码后先查询预约信息,不修改状态(用于移动端「确认后再核销」)。 */ public function preview(Request $request, ReservationExpiryService $reservationExpiryService): JsonResponse { $reservationExpiryService->expireStalePendingReservations(); $data = $request->validate([ 'qr_token' => ['required', 'string'], ]); $reservation = Reservation::with([ 'venue:id,name,address,lat,lng,reservation_notice', 'activity:id,title,summary,cover_image,address,start_at,end_at,tags,lat,lng', 'ticketGrabEvent:id,title,summary,cover_image,address,start_at,end_at,tags', 'activityDay:id,activity_id,activity_date,session_name,session_start_at,session_end_at,booking_deadline_at', ])->where('qr_token', $data['qr_token'])->first(); if (! $reservation) { return response()->json(['message' => '无效的二维码或预约不存在'], 404); } $this->ensureReservationAccess($request, $reservation); $blockReason = null; if ($reservation->status === 'verified') { $blockReason = '该预约已核销'; } elseif ($reservation->status === 'cancelled') { $blockReason = '该预约已取消,不能核销'; } elseif ($reservation->status === 'expired') { $blockReason = '该预约已过期(活动日未核销),不能核销'; } $canVerify = $reservation->status === 'pending'; if ($canVerify) { $dayMsg = $this->activityDayNotTodayMessage($reservation); if ($dayMsg !== null) { $canVerify = false; $blockReason = $dayMsg; } } $h5 = app(H5ReservationController::class); return response()->json([ 'reservation' => $h5->reservationToH5Array($reservation), 'can_verify' => $canVerify, 'verify_block_reason' => $blockReason, ]); } public function index(Request $request): JsonResponse { $cred = $this->actingPortalCredential($request); $query = Reservation::with([ 'venue:id,name,address,lat,lng,reservation_notice', 'activity:id,title,summary,cover_image,address,start_at,end_at,tags,lat,lng', 'ticketGrabEvent:id,title,summary,cover_image,address,start_at,end_at,tags', 'activityDay:id,activity_id,activity_date,session_name,session_start_at,session_end_at,booking_deadline_at', ]) ->orderByRaw("CASE status WHEN 'pending' THEN 0 WHEN 'verified' THEN 1 WHEN 'expired' THEN 2 WHEN 'cancelled' THEN 3 ELSE 4 END") ->orderByDesc('id'); if ($cred !== null) { $this->assertPortalActiveOrAbort($cred); $this->applyPortalScope($query, $cred); } else { /** @var User $user */ $user = $request->user(); $rk = (string) $request->input('reservation_kind', ''); if ($rk === 'ticket_grab') { $query->where('reservation_kind', Reservation::KIND_TICKET_GRAB); } elseif ($rk === 'activity') { $query->where(function ($q) { $q->whereNull('reservation_kind') ->orWhere('reservation_kind', Reservation::KIND_ACTIVITY); }); } if (! $user->isSuperAdmin()) { $venueIds = $user->venues()->pluck('venues.id'); $query->whereIn('venue_id', $venueIds); } } if ($request->filled('status') && $request->string('status')->toString() !== 'all') { $query->where('status', (string) $request->string('status')); } if ($request->filled('keyword')) { $keyword = trim((string) $request->input('keyword')); $query->where(function ($q) use ($keyword) { $q->where('visitor_name', 'like', "%{$keyword}%") ->orWhere('visitor_phone', 'like', "%{$keyword}%") ->orWhere('qr_token', 'like', "%{$keyword}%") ->orWhere('id_card', 'like', "%{$keyword}%"); }); } $dateField = (string) $request->input('date_field', 'created_at'); if ($request->filled('start_date') || $request->filled('end_date')) { if ($dateField === 'activity_day') { $start = $request->filled('start_date') ? (string) $request->input('start_date') : null; $end = $request->filled('end_date') ? (string) $request->input('end_date') : $start; $query->where(function ($q) use ($start, $end) { $q->where(function ($q2) use ($start, $end) { $q2->where(function ($q3) { $q3->whereNull('reservation_kind') ->orWhere('reservation_kind', '!=', Reservation::KIND_TICKET_GRAB); })->whereHas('activityDay', function ($q4) use ($start, $end) { if ($start !== null && $start !== '') { $q4->whereDate('activity_date', '>=', $start); } if ($end !== null && $end !== '') { $q4->whereDate('activity_date', '<=', $end); } }); })->orWhere(function ($q2) use ($start, $end) { $q2->where('reservation_kind', Reservation::KIND_TICKET_GRAB); if ($start !== null && $start !== '') { $q2->whereDate('entry_date', '>=', $start); } if ($end !== null && $end !== '') { $q2->whereDate('entry_date', '<=', $end); } }); }); } elseif ($dateField === 'entry_date') { $start = $request->filled('start_date') ? (string) $request->input('start_date') : null; $end = $request->filled('end_date') ? (string) $request->input('end_date') : $start; if ($start !== null && $start !== '') { $query->whereDate('entry_date', '>=', $start); } if ($end !== null && $end !== '') { $query->whereDate('entry_date', '<=', $end); } } else { if ($request->filled('start_date')) { $query->whereDate('created_at', '>=', (string) $request->input('start_date')); } if ($request->filled('end_date')) { $query->whereDate('created_at', '<=', (string) $request->input('end_date')); } } } $h5 = app(H5ReservationController::class); return response()->json( $query->limit(500)->get()->map(fn (Reservation $r) => $h5->reservationToH5Array($r))->values() ); } /** * 今日(活动日为当天)预约单数、已核销单数,与移动端「今日报名」同权限与场馆范围。 * 一大一小按 1 单计算(按预约订单数统计)。 */ public function todaySummary(Request $request): JsonResponse { $cred = $this->actingPortalCredential($request); $today = now()->toDateString(); $tg = Reservation::KIND_TICKET_GRAB; if ($cred !== null) { $this->assertPortalActiveOrAbort($cred); if ($cred->portal_kind === VerifyPortalCredential::KIND_ACTIVITY) { $base = Reservation::query() ->where('venue_id', $cred->venue_id) ->where('activity_id', $cred->portal_id) ->where(function ($q) { $q->whereNull('reservation_kind') ->orWhere('reservation_kind', Reservation::KIND_ACTIVITY); }) ->whereHas('activityDay', function ($q3) use ($today) { $q3->whereDate('activity_date', $today); }); } else { $base = Reservation::query() ->where('venue_id', $cred->venue_id) ->where('reservation_kind', $tg) ->where('ticket_grab_event_id', $cred->portal_id) ->whereDate('entry_date', $today); } } else { /** @var User $user */ $user = $request->user(); $base = Reservation::query() ->where(function ($q) use ($today, $tg) { $q->where('reservation_kind', $tg) ->whereDate('entry_date', $today); })->orWhere(function ($q) use ($today, $tg) { $q->where(function ($q2) use ($tg) { $q2->whereNull('reservation_kind') ->orWhere('reservation_kind', '!=', $tg); })->whereHas('activityDay', function ($q3) use ($today) { $q3->whereDate('activity_date', $today); }); }); if (! $user->isSuperAdmin()) { $venueIds = $user->venues()->pluck('venues.id'); $base->whereIn('venue_id', $venueIds); } } $totalOrders = (clone $base)->whereNotIn('status', ['cancelled'])->count(); $verifiedOrders = (clone $base)->where('status', 'verified')->count(); return response()->json([ 'total_orders' => (int) $totalOrders, 'verified_orders' => (int) $verifiedOrders, ]); } public function verifyByToken(Request $request, ReservationExpiryService $reservationExpiryService): JsonResponse { $reservationExpiryService->expireStalePendingReservations(); $data = $request->validate([ 'qr_token' => ['required', 'string'], ]); $reservation = Reservation::with([ 'venue:id,name,address,lat,lng,reservation_notice', 'activity:id,title,summary,cover_image,address,start_at,end_at,tags,lat,lng', 'ticketGrabEvent:id,title,summary,cover_image,address,start_at,end_at,tags', 'activityDay:id,activity_id,activity_date,session_name,session_start_at,session_end_at,booking_deadline_at', ])->where('qr_token', $data['qr_token'])->firstOrFail(); $this->ensureReservationAccess($request, $reservation); if ($reservation->status === 'verified') { return response()->json(['message' => '该预约已核销'], 422); } if ($reservation->status === 'cancelled') { return response()->json(['message' => '该预约已取消,不能核销'], 422); } if ($reservation->status === 'expired') { return response()->json(['message' => '该预约已过期(活动日未核销),不能核销'], 422); } $dayBlock = $this->activityDayNotTodayMessage($reservation); if ($dayBlock !== null) { return response()->json(['message' => $dayBlock], 422); } $cred = $this->actingPortalCredential($request); $payload = [ 'status' => 'verified', 'verified_at' => now(), ]; if ($cred !== null) { $payload['verified_by'] = null; $payload['verify_credential_id'] = $cred->id; } else { /** @var User $user */ $user = $request->user(); $payload['verified_by'] = $user->id; $payload['verify_credential_id'] = null; } $reservation->update($payload); $fresh = $reservation->fresh()->load([ 'venue:id,name,address,lat,lng,reservation_notice', 'activity:id,title,summary,cover_image,address,start_at,end_at,tags,lat,lng', 'ticketGrabEvent:id,title,summary,cover_image,address,start_at,end_at,tags', 'activityDay:id,activity_id,activity_date,session_name,session_start_at,session_end_at,booking_deadline_at', 'verifier:id,name,username', ]); $h5 = app(H5ReservationController::class); return response()->json([ 'message' => '核销成功', 'reservation' => $h5->reservationToH5Array($fresh), ]); } }