orderByDesc('id'); $user = $request->user(); if (!$user->isSuperAdmin()) { $query->whereIn('venue_id', $user->venues()->pluck('venues.id')); } 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('id_card', 'like', "%{$keyword}%") ->orWhere('qr_token', 'like', "%{$keyword}%"); }); } 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')); } $pageSize = max(1, min(200, (int) $request->input('page_size', 10))); $page = $query->paginate($pageSize); $rows = collect($page->items()); if ($rows->isNotEmpty()) { $venueIds = $rows->pluck('venue_id')->filter()->map(fn ($v) => (int) $v)->unique()->values(); $phones = $rows->pluck('visitor_phone')->filter()->map(fn ($v) => (string) $v)->unique()->values(); $blackSet = Blacklist::query() ->whereIn('venue_id', $venueIds) ->whereIn('visitor_phone', $phones) ->get(['venue_id', 'visitor_phone']) ->mapWithKeys(fn ($b) => [$b->venue_id.'#'.$b->visitor_phone => true]); $page->setCollection($rows->map(function ($row) use ($blackSet) { $row->is_blacklisted = $row->visitor_phone ? isset($blackSet[$row->venue_id.'#'.$row->visitor_phone]) : false; return $row; })); } return response()->json($page); } public function export(Request $request): StreamedResponse { $query = Reservation::with([ 'venue:id,name', 'activity:id,title', 'activityDay:id,activity_id,activity_date', ])->orderByDesc('id'); $user = $request->user(); if (!$user->isSuperAdmin()) { $query->whereIn('venue_id', $user->venues()->pluck('venues.id')); } 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('id_card', 'like', "%{$keyword}%") ->orWhere('qr_token', 'like', "%{$keyword}%"); }); } 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')); } $rows = $query->limit(5000)->get(); $filename = 'activity-registrations-' . now()->format('Ymd-His') . '.csv'; return response()->streamDownload(function () use ($rows) { $out = fopen('php://output', 'w'); fprintf($out, chr(0xEF) . chr(0xBB) . chr(0xBF)); fputcsv($out, ['ID', '活动', '场馆', '报名人', '手机号', '身份证', '预约票数', '预约入馆日期', '状态', '预约时间', '核销时间', '二维码Token']); foreach ($rows as $row) { $entryDate = $row->activityDay?->activity_date; $entryDateStr = $entryDate ? Carbon::parse($entryDate)->timezone('Asia/Shanghai')->format('Y-m-d') : ''; fputcsv($out, [ $row->id, $row->activity?->title ?? '', $row->venue?->name ?? '', $row->visitor_name, $row->visitor_phone ?? '', $row->id_card ?? '', (string) ($row->ticket_count ?? 1), $entryDateStr, self::statusLabel($row->status), self::formatShanghai($row->created_at), self::formatShanghai($row->verified_at), $row->qr_token, ]); } fclose($out); }, $filename, ['Content-Type' => 'text/csv; charset=UTF-8']); } public function quickBlacklist(Request $request, Reservation $reservation): JsonResponse { $user = $request->user(); if (!$user->isSuperAdmin()) { $allowed = $user->venues()->where('venues.id', $reservation->venue_id)->exists(); abort_unless($allowed, 403, '仅可操作已绑定场馆'); } if (!$reservation->visitor_phone) { return response()->json(['message' => '该报名记录无手机号,无法加入黑名单'], 422); } $data = $request->validate([ 'reason' => ['required', 'string', 'max:255'], ]); $item = Blacklist::updateOrCreate( ['venue_id' => $reservation->venue_id, 'visitor_phone' => $reservation->visitor_phone], [ 'visitor_name' => $reservation->visitor_name, 'reason' => $data['reason'], ], ); return response()->json($item->load('venue:id,name'), 201); } public function batchQuickBlacklist(Request $request): JsonResponse { $data = $request->validate([ 'reservation_ids' => ['required', 'array', 'min:1'], 'reservation_ids.*' => ['required', 'integer', 'exists:reservations,id'], 'reason' => ['required', 'string', 'max:255'], ]); $user = $request->user(); $ids = collect($data['reservation_ids'])->map(fn ($v) => (int) $v)->unique()->values(); $reservations = Reservation::whereIn('id', $ids)->get(); $count = 0; $failed = []; foreach ($reservations as $reservation) { if (!$user->isSuperAdmin()) { $allowed = $user->venues()->where('venues.id', $reservation->venue_id)->exists(); if (!$allowed) { $failed[] = ['reservation_id' => $reservation->id, 'reason' => '无场馆权限']; continue; } } if (!$reservation->visitor_phone) { $failed[] = ['reservation_id' => $reservation->id, 'reason' => '缺少手机号']; continue; } Blacklist::updateOrCreate( ['venue_id' => $reservation->venue_id, 'visitor_phone' => $reservation->visitor_phone], [ 'visitor_name' => $reservation->visitor_name, 'reason' => $data['reason'], ], ); $count++; } return response()->json([ 'message' => '批量拉黑完成', 'count' => $count, 'failed_count' => count($failed), 'failed' => $failed, ]); } private static function formatShanghai(mixed $value): string { if ($value === null) { return ''; } return Carbon::parse($value)->timezone('Asia/Shanghai')->format('Y-m-d H:i:s'); } private static function statusLabel(string $status): string { return match ($status) { 'pending' => '待核销', 'verified' => '已核销', 'cancelled' => '已取消', default => $status, }; } }