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(); } $datesBlank = ! $request->filled('start_date') || ! $request->filled('end_date'); $startDate = $datesBlank ? null : trim((string) $request->input('start_date')); $endDate = $datesBlank ? null : trim((string) $request->input('end_date')); $activityId = null; if (! $datesBlank && $request->filled('activity_id')) { $activityId = (int) $request->input('activity_id'); $allowed = Activity::query() ->whereKey($activityId) ->whereNull('deleted_at') ->whereIn('venue_id', $venueIds) ->exists(); if (! $allowed) { return response()->json(['message' => '活动不存在或无权查看'], 422); } } $dateOnReservation = 'COALESCE(activity_days.activity_date, DATE(reservations.created_at))'; /** 顶部五项:账号可见场馆范围内的全量累计,不受日期与「筛选场馆」影响 */ $scopeVenueIds = $allowedVenueIds; $activeVenueCount = (int) DB::table('reservations') ->whereIn('venue_id', $scopeVenueIds) ->distinct('venue_id') ->count('venue_id'); $userCount = (int) DB::table('reservations') ->whereIn('venue_id', $scopeVenueIds) ->whereNotNull('wechat_user_id') ->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() ->whereIn('venue_id', $scopeVenueIds) ->whereNotNull('visitor_phone') ->whereRaw("TRIM(visitor_phone) <> ''") ->whereExists(function ($q) use ($scopeVenueIds, $user) { $q->selectRaw('1') ->from('reservations') ->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'); $publishedCount = 0; $activitySums = null; $arTotal = 0; $arVerified = 0; $arCancelled = 0; $arPending = 0; $arExpired = 0; $arVerifyRate = 0.0; $activityStatsByVenue = []; if (! $datesBlank) { $activityOverlap = function ($q) use ($startDate, $endDate) { $q->where(function ($w) use ($startDate, $endDate) { $w->whereNotNull('start_at') ->whereNotNull('end_at') ->whereDate('start_at', '<=', $endDate) ->whereDate('end_at', '>=', $startDate); })->orWhere(function ($w) use ($startDate, $endDate) { $w->whereNull('start_at') ->whereNull('end_at') ->whereDate('created_at', '>=', $startDate) ->whereDate('created_at', '<=', $endDate); }); }; $statsActivityIdsQuery = Activity::query() ->whereIn('venue_id', $venueIds) ->whereNull('deleted_at'); if ($activityId !== null) { $statsActivityIdsQuery->whereKey($activityId); } else { $statsActivityIdsQuery->where($activityOverlap); } $statsActivityIds = $statsActivityIdsQuery->pluck('id'); $publishedCount = $statsActivityIds->isEmpty() ? 0 : (int) Activity::query() ->whereIn('id', $statsActivityIds) ->whereDate('created_at', '>=', $startDate) ->whereDate('created_at', '<=', $endDate) ->count(); $activitySums = $statsActivityIds->isEmpty() ? null : Activity::query() ->whereIn('id', $statsActivityIds) ->selectRaw('COALESCE(SUM(view_count),0) as v') ->selectRaw('COALESCE(SUM(external_link_click_count),0) as l') ->first(); if ($statsActivityIds->isNotEmpty()) { $ar = DB::table('reservations') ->leftJoin('activity_days', 'activity_days.id', '=', 'reservations.activity_day_id') ->whereIn('reservations.venue_id', $venueIds) ->where('reservations.reservation_kind', Reservation::KIND_ACTIVITY) ->whereIn('reservations.activity_id', $statsActivityIds) ->whereRaw("{$dateOnReservation} >= ?", [$startDate]) ->whereRaw("{$dateOnReservation} <= ?", [$endDate]) ->selectRaw('COUNT(*) as total_count') ->selectRaw("SUM(CASE WHEN reservations.status = 'verified' THEN 1 ELSE 0 END) as verified_count") ->selectRaw("SUM(CASE WHEN reservations.status = 'cancelled' THEN 1 ELSE 0 END) as cancelled_count") ->selectRaw("SUM(CASE WHEN reservations.status = 'pending' THEN 1 ELSE 0 END) as pending_count") ->selectRaw("SUM(CASE WHEN reservations.status = 'expired' THEN 1 ELSE 0 END) as expired_count") ->first(); $arTotal = (int) ($ar->total_count ?? 0); $arVerified = (int) ($ar->verified_count ?? 0); $arCancelled = (int) ($ar->cancelled_count ?? 0); $arPending = (int) ($ar->pending_count ?? 0); $arExpired = (int) ($ar->expired_count ?? 0); $effective = max(0, $arTotal - $arCancelled); $arVerifyRate = $effective > 0 ? round($arVerified / $effective, 4) : 0; } if ($statsActivityIds->isNotEmpty()) { $actByVenueRows = Activity::query() ->whereIn('id', $statsActivityIds) ->selectRaw('venue_id') ->selectRaw('COALESCE(SUM(view_count), 0) as total_view_count') ->selectRaw('COALESCE(SUM(external_link_click_count), 0) as total_external_link_click_count') ->selectRaw('SUM(CASE WHEN DATE(created_at) >= ? AND DATE(created_at) <= ? THEN 1 ELSE 0 END) as published_count', [$startDate, $endDate]) ->groupBy('venue_id') ->get(); $actByVenue = []; foreach ($actByVenueRows as $row) { $actByVenue[(int) $row->venue_id] = $row; } $resByVenueRows = DB::table('reservations') ->leftJoin('activity_days', 'activity_days.id', '=', 'reservations.activity_day_id') ->whereIn('reservations.venue_id', $venueIds) ->where('reservations.reservation_kind', Reservation::KIND_ACTIVITY) ->whereIn('reservations.activity_id', $statsActivityIds) ->whereRaw("{$dateOnReservation} >= ?", [$startDate]) ->whereRaw("{$dateOnReservation} <= ?", [$endDate]) ->selectRaw('reservations.venue_id as venue_id') ->selectRaw('COUNT(*) as total_count') ->selectRaw("SUM(CASE WHEN reservations.status = 'verified' THEN 1 ELSE 0 END) as verified_count") ->selectRaw("SUM(CASE WHEN reservations.status = 'cancelled' THEN 1 ELSE 0 END) as cancelled_count") ->selectRaw("SUM(CASE WHEN reservations.status = 'pending' THEN 1 ELSE 0 END) as pending_count") ->selectRaw("SUM(CASE WHEN reservations.status = 'expired' THEN 1 ELSE 0 END) as expired_count") ->groupBy('reservations.venue_id') ->get(); $resByVenue = []; foreach ($resByVenueRows as $row) { $resByVenue[(int) $row->venue_id] = $row; } $allVenueIds = collect(array_keys($actByVenue))->merge(array_keys($resByVenue))->unique()->sort()->values(); $names = Venue::query()->whereIn('id', $allVenueIds)->pluck('name', 'id'); foreach ($allVenueIds as $vid) { $vid = (int) $vid; $a = $actByVenue[$vid] ?? null; $r = $resByVenue[$vid] ?? null; $total = (int) ($r->total_count ?? 0); $verified = (int) ($r->verified_count ?? 0); $cancelled = (int) ($r->cancelled_count ?? 0); $pending = (int) ($r->pending_count ?? 0); $expired = (int) ($r->expired_count ?? 0); $effective = max(0, $total - $cancelled); $rate = $effective > 0 ? round($verified / $effective, 4) : 0.0; $activityStatsByVenue[] = [ 'venue_id' => (int) $vid, 'venue_name' => (string) ($names[$vid] ?? ('#'.$vid)), 'published_count' => (int) ($a->published_count ?? 0), 'total_view_count' => (int) ($a->total_view_count ?? 0), 'total_external_link_click_count' => (int) ($a->total_external_link_click_count ?? 0), 'total_count' => $total, 'verified_count' => $verified, 'cancelled_count' => $cancelled, 'pending_count' => $pending, 'expired_count' => $expired, 'verify_rate' => $rate, ]; } usort($activityStatsByVenue, fn (array $x, array $y) => strcmp($x['venue_name'], $y['venue_name'])); } } return response()->json([ 'scope' => [ 'role' => $user->role, 'venue_id' => $selectedVenueId, 'start_date' => $startDate, 'end_date' => $endDate, 'activity_id' => $activityId, 'dates_applied' => ! $datesBlank, ], 'summary' => [ 'active_venue_count' => (int) $activeVenueCount, 'activity_sessions' => (int) $activitySessions, 'ticket_grab_sessions' => (int) $ticketGrabSessions, 'user_count' => (int) $userCount, 'blacklisted_unique' => (int) $blacklistedUnique, ], 'activity_stats' => [ 'published_count' => $publishedCount, 'total_view_count' => (int) ($activitySums->v ?? 0), 'total_external_link_click_count' => (int) ($activitySums->l ?? 0), 'total_count' => $arTotal, 'verified_count' => $arVerified, 'cancelled_count' => $arCancelled, 'pending_count' => $arPending, 'expired_count' => $arExpired, 'verify_rate' => $arVerifyRate, ], 'activity_stats_venues' => $activityStatsByVenue, ]); } public function ticketGrabStats(Request $request, DashboardTicketGrabStatsService $ticketGrabStats): JsonResponse { $validated = $request->validate([ 'ticket_grab_event_id' => ['required', 'integer', 'exists:ticket_grab_events,id'], 'date' => ['required', '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 = $validated['date']; $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); } }