user(); $allowedVenueIds = $user->isSuperAdmin() ? Venue::query()->pluck('id') : $user->venues()->pluck('venues.id'); $selectedVenueId = $request->filled('venue_id') ? (int) $request->input('venue_id') : null; $venueIds = $allowedVenueIds; if ($selectedVenueId) { $venueIds = $venueIds->filter(fn ($id) => (int) $id === $selectedVenueId)->values(); } $activityId = $request->filled('activity_id') ? (int) $request->input('activity_id') : null; if ($activityId) { $allowed = Activity::query() ->whereKey($activityId) ->whereNull('deleted_at') ->whereIn('venue_id', $venueIds) ->exists(); if (! $allowed) { return response()->json(['message' => '活动不存在或无权查看'], 422); } } /** 顶部指标:账号可见场馆范围内的全量累计(不受「筛选场馆」影响) */ $scopeVenueIds = $allowedVenueIds; $userCount = (int) DB::table('reservations') ->whereIn('venue_id', $scopeVenueIds) ->whereNotNull('wechat_user_id') ->whereNull('deleted_at') ->selectRaw('COUNT(DISTINCT wechat_user_id) as c') ->value('c'); $activitySessions = Activity::query() ->whereIn('venue_id', $scopeVenueIds) ->whereNull('deleted_at') ->count(); $ticketGrabSessions = TicketGrabEvent::query() ->whereHas('venues', fn ($vq) => $vq->whereIn('venues.id', $scopeVenueIds)) ->count(); $blacklistedUniqueQuery = Blacklist::query() ->active() ->whereIn('venue_id', $scopeVenueIds) ->whereNotNull('visitor_phone') ->whereRaw("TRIM(visitor_phone) <> ''") ->whereExists(function ($q) use ($scopeVenueIds, $user) { $q->selectRaw('1') ->from('reservations') ->whereNull('reservations.deleted_at') ->whereColumn('reservations.visitor_phone', 'blacklists.visitor_phone') ->whereIn('reservations.venue_id', $scopeVenueIds); if ($user->isSuperAdmin() && Schema::hasTable('wechat_users')) { $q->whereNotNull('reservations.wechat_user_id') ->whereExists(function ($wq) { $wq->selectRaw('1') ->from('wechat_users') ->whereColumn('wechat_users.id', 'reservations.wechat_user_id'); }); } }); $blacklistedUnique = $blacklistedUniqueQuery ->distinct('visitor_phone') ->count('visitor_phone'); $venuesCount = $scopeVenueIds->isEmpty() ? 0 : (int) Venue::query()->whereIn('id', $scopeVenueIds)->count(); /** 与核心指标同范围:账号可见全部场馆(不受当前筛选场馆影响) */ $activityScheduleBase = Activity::query() ->whereIn('venue_id', $scopeVenueIds) ->whereNull('deleted_at'); $activityScheduleCounts = [ 'total' => (clone $activityScheduleBase)->count(), 'not_started' => (clone $activityScheduleBase)->whereComputedScheduleStatus('not_started')->count(), 'ongoing' => (clone $activityScheduleBase)->whereComputedScheduleStatus('ongoing')->count(), 'ended' => (clone $activityScheduleBase)->whereComputedScheduleStatus('ended')->count(), ]; $ticketGrabScheduleBase = TicketGrabEvent::query() ->whereHas('venues', fn ($vq) => $vq->whereIn('venues.id', $scopeVenueIds)); $ticketGrabEventIdsForScope = (clone $ticketGrabScheduleBase)->pluck('id'); $verifiedPeople = 0; $bookedPeople = 0; if ($ticketGrabEventIdsForScope->isNotEmpty()) { $tgReservations = Reservation::query() ->where('reservation_kind', Reservation::KIND_TICKET_GRAB) ->whereIn('ticket_grab_event_id', $ticketGrabEventIdsForScope) ->where('status', '!=', 'cancelled') ->whereNull('deleted_at') ->get(['ticket_count', 'verified_at']); $bookedPeople = (int) $tgReservations->sum(fn (Reservation $r) => max(1, (int) ($r->ticket_count ?? 1))); $verifiedPeople = (int) $tgReservations ->filter(fn (Reservation $r) => $r->verified_at !== null) ->sum(fn (Reservation $r) => max(1, (int) ($r->ticket_count ?? 1))); } $verifyRatePct = $bookedPeople > 0 ? round(100 * $verifiedPeople / $bookedPeople, 1) : null; $ticketGrabScheduleCounts = [ 'total' => (clone $ticketGrabScheduleBase)->count(), 'not_started' => (clone $ticketGrabScheduleBase)->whereComputedScheduleStatus('not_started')->count(), 'ongoing' => (clone $ticketGrabScheduleBase)->whereComputedScheduleStatus('ongoing')->count(), 'ended' => (clone $ticketGrabScheduleBase)->whereComputedScheduleStatus('ended')->count(), 'verify_rate_pct' => $verifyRatePct, 'verified_people' => $verifiedPeople, 'booked_people' => $bookedPeople, ]; $baseActivityQuery = Activity::query() ->whereIn('venue_id', $venueIds) ->whereNull('deleted_at'); if ($activityId !== null) { $baseActivityQuery->whereKey($activityId); } $totalViews = (int) (clone $baseActivityQuery)->sum('view_count'); $page = max(1, (int) $request->input('activity_stats_page', 1)); $pageSize = max(1, min(2000, (int) $request->input('activity_stats_page_size', 500))); $totalActivities = (clone $baseActivityQuery)->count(); $activityRows = (clone $baseActivityQuery) ->orderByDesc('view_count') ->orderByDesc('id') ->forPage($page, $pageSize) ->get(['id', 'title', 'venue_id', 'start_at', 'end_at', 'view_count']); $venueIdsForNames = $activityRows->pluck('venue_id')->unique()->filter()->values()->all(); $venueNameMap = $venueIdsForNames === [] ? collect() : Venue::query()->whereIn('id', $venueIdsForNames)->pluck('name', 'id'); $activityStatsActivities = $activityRows->map(function ($a) use ($venueNameMap) { $vid = (int) $a->venue_id; return [ 'id' => (int) $a->id, 'title' => (string) $a->title, 'venue_id' => $vid, 'venue_name' => (string) ($venueNameMap[$vid] ?? ('#'.$vid)), 'view_count' => (int) ($a->view_count ?? 0), 'start_at' => $a->start_at, 'end_at' => $a->end_at, ]; })->values()->all(); $pendingAudits = null; if ($user->isSuperAdmin()) { $activityPendingCount = (int) Activity::query() ->whereNull('deleted_at') ->where('audit_status', Activity::AUDIT_PENDING) ->count(); $activityItems = Activity::query() ->whereNull('deleted_at') ->where('audit_status', Activity::AUDIT_PENDING) ->with(['venue:id,name']) ->orderByDesc('updated_at') ->limit(8) ->get(['id', 'title', 'venue_id', 'updated_at']) ->map(function (Activity $a) { return [ 'id' => $a->id, 'title' => $a->title, 'venue_name' => (string) ($a->venue?->name ?? ''), 'updated_at' => $a->updated_at, ]; }) ->values() ->all(); $pendingAudits = [ 'activities' => [ 'count' => $activityPendingCount, 'items' => $activityItems, ], ]; } return response()->json([ 'scope' => [ 'role' => $user->role, 'venue_id' => $selectedVenueId, 'activity_id' => $activityId, ], 'summary' => [ 'activity_sessions' => (int) $activitySessions, 'venues_count' => $venuesCount, 'ticket_grab_sessions' => (int) $ticketGrabSessions, 'user_count' => (int) $userCount, 'blacklisted_unique' => (int) $blacklistedUnique, ], 'pending_audits' => $pendingAudits, 'activity_schedule_counts' => $activityScheduleCounts, 'ticket_grab_schedule_counts' => $ticketGrabScheduleCounts, 'activity_stats' => [ 'total_view_count' => $totalViews, ], 'activity_stats_activities' => [ 'data' => $activityStatsActivities, 'total' => $totalActivities, 'page' => $page, 'page_size' => $pageSize, ], ]); } public function ticketGrabStats(Request $request, DashboardTicketGrabStatsService $ticketGrabStats): JsonResponse { $validated = $request->validate([ 'ticket_grab_event_id' => ['required', 'integer', 'exists:ticket_grab_events,id'], 'date' => ['nullable', 'date_format:Y-m-d'], ]); $user = $request->user(); $allowedVenueIds = $user->isSuperAdmin() ? Venue::query()->pluck('id') : $user->venues()->pluck('venues.id'); $eventId = (int) $validated['ticket_grab_event_id']; $date = isset($validated['date']) && $validated['date'] !== '' ? (string) $validated['date'] : now()->format('Y-m-d'); $pivotVenues = TicketGrabEventVenue::query() ->where('ticket_grab_event_id', $eventId) ->pluck('venue_id'); if ($pivotVenues->isNotEmpty() && ! $user->isSuperAdmin()) { $allow = $allowedVenueIds->map(fn ($id) => (int) $id); $ok = false; foreach ($pivotVenues as $vid) { if ($allow->contains((int) $vid)) { $ok = true; break; } } if (! $ok) { return response()->json(['message' => '无权查看该抢票活动统计'], 403); } } $scoped = DashboardTicketGrabStatsService::scopedVenueIdsForEvent($eventId, $allowedVenueIds); if ($scoped->isEmpty()) { return response()->json(['message' => '当前账号下无可统计的场馆或未配置参与场馆'], 403); } $payload = $ticketGrabStats->build($eventId, $date, $scoped); if (isset($payload['error'])) { return response()->json(['message' => $payload['error']], 404); } $payload['data_updated_at'] = $date; return response()->json($payload); } /** * 抢票每日核销统计 Excel(行=场馆,列=活动期内各日 + 合计占比) */ public function ticketGrabDailyVerifyExport(Request $request, DashboardTicketGrabStatsService $ticketGrabStats): StreamedResponse { $validated = $request->validate([ 'ticket_grab_event_id' => ['required', 'integer', 'exists:ticket_grab_events,id'], ]); $user = $request->user(); $allowedVenueIds = $user->isSuperAdmin() ? Venue::query()->pluck('id') : $user->venues()->pluck('venues.id'); $eventId = (int) $validated['ticket_grab_event_id']; $pivotVenues = TicketGrabEventVenue::query() ->where('ticket_grab_event_id', $eventId) ->pluck('venue_id'); if ($pivotVenues->isNotEmpty() && ! $user->isSuperAdmin()) { $allow = $allowedVenueIds->map(fn ($id) => (int) $id); $ok = false; foreach ($pivotVenues as $vid) { if ($allow->contains((int) $vid)) { $ok = true; break; } } if (! $ok) { abort(403, '无权查看该抢票活动统计'); } } $scoped = DashboardTicketGrabStatsService::scopedVenueIdsForEvent($eventId, $allowedVenueIds); if ($scoped->isEmpty()) { abort(403, '当前账号下无可统计的场馆或未配置参与场馆'); } $matrix = $ticketGrabStats->buildDailyVerifyMatrixPublic($eventId, $scoped); if (isset($matrix['error'])) { abort(404, $matrix['error']); } $labels = $matrix['date_labels'] ?? []; $rows = $matrix['rows'] ?? []; $table = [array_merge(['场馆'], $labels, ['总人数和核销比'])]; foreach ($rows as $r) { $line = [$r['venue_name'] ?? '']; foreach (($r['cells'] ?? []) as $c) { $line[] = $c['display'] ?? ''; } $line[] = ($r['total_cell'] ?? [])['display'] ?? ''; $table[] = $line; } $spreadsheet = new Spreadsheet; $sheet = $spreadsheet->getActiveSheet(); $sheet->setTitle('每日核销统计'); $sheet->fromArray($table, null, 'A1'); $writer = new Xlsx($spreadsheet); $filename = 'ticket-grab-daily-verify-'.$eventId.'-'.now()->format('Ymd-His').'.xlsx'; return response()->streamDownload(function () use ($writer) { $writer->save('php://output'); }, $filename, [ 'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', ]); } }