Reservation::query()->where('status', '!=', 'cancelled')->count(), 'verified_total' => Reservation::query()->where('status', 'verified')->count(), 'venue_total' => Venue::query()->visibleOnH5()->count(), 'activity_total' => Activity::query()->visibleOnH5()->count(), ]; $banners = Activity::query() ->visibleOnH5() ->whereNotNull('cover_image') ->where('cover_image', '!=', '') ->orderBy('sort') ->orderByDesc('id') ->limit(5) ->get(['id', 'title', 'summary', 'cover_image']) ->map(fn ($a) => [ 'id' => $a->id, 'title' => $a->title, 'summary' => $a->summary, 'image' => $a->cover_image, ]) ->values(); $topLiveVenues = DB::table('reservations') ->join('venues', 'venues.id', '=', 'reservations.venue_id') ->where('reservations.status', '!=', 'cancelled') ->where('venues.is_active', true) ->where('venues.audit_status', Venue::AUDIT_APPROVED) ->select('venues.id', 'venues.name', DB::raw('SUM(COALESCE(reservations.ticket_count, 1)) as people_count')) ->groupBy('venues.id', 'venues.name') ->orderByDesc('people_count') ->limit(3) ->get() ->map(fn ($r) => [ 'id' => (int) $r->id, 'name' => $r->name, 'people_count' => (int) $r->people_count, ]) ->values(); $venueTypeColors = DictItem::query() ->where('dict_type', 'venue_type') ->where('is_active', true) ->pluck('item_remark', 'item_value'); // 地图场馆:一次性返回全部有效坐标点(体量约百级,无需分页) $mapVenues = Venue::query() ->visibleOnH5() ->orderBy('sort') ->orderByDesc('id') ->get() ->map(function ($v) use ($venueTypeColors) { $p = $v->toH5Payload(); if ($p === null) { return null; } $lat = $p['lat'] ?? null; $lng = $p['lng'] ?? null; if ($lat === null || $lng === null || $lat === '' || $lng === '') { return null; } $types = $p['venue_types'] ?? null; $firstType = (is_array($types) && count($types)) ? (string) ($types[0] ?? '') : ($p['venue_type'] ?? ''); $raw = $venueTypeColors->get($firstType); $color = '#05c9ac'; if (is_string($raw) && trim($raw) !== '') { $t = trim($raw); if (! str_starts_with($t, '#')) { $t = '#'.$t; } if (preg_match('/^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/', $t)) { $color = $t; } } return [ 'id' => (int) $p['id'], 'name' => $p['name'], 'sort' => (int) ($p['sort'] ?? 0), 'district' => $p['district'], 'address' => $p['address'], 'lat' => (float) $lat, 'lng' => (float) $lng, 'image' => $p['cover_image'], 'venue_type' => $p['venue_type'], 'venue_types' => is_array($types) ? array_values($types) : [], 'ticket_type' => $p['ticket_type'], 'appointment_type' => $p['appointment_type'], 'open_mode' => $p['open_mode'] ?? null, 'venue_type_color' => $color, ]; }) ->filter() ->values(); $hotActivities = Activity::query() ->with('venue:id,name') ->with('activityDays') ->visibleOnH5() ->orderForH5Listing() ->limit(5) ->get(['id', 'venue_id', 'title', 'summary', 'cover_image', 'start_at', 'end_at', 'registered_count', 'address', 'tags', 'sort']) ->map(function ($a) { $isBookable = $a->activityDays->contains( fn (ActivityDay $d) => $d->isCurrentlyBookable() ); return [ 'id' => $a->id, 'title' => $a->title, 'summary' => $a->summary, 'image' => $a->cover_image, 'venue_name' => $a->venue?->name, 'address' => $a->address, 'start_at' => optional($a->start_at)?->toIso8601String(), 'end_at' => optional($a->end_at)?->toIso8601String(), 'schedule_status' => Activity::computeScheduleStatusFromBounds($a->start_at, $a->end_at), 'registered_count' => (int) ($a->registered_count ?? 0), 'is_bookable' => $isBookable, 'tags' => array_values($a->tags ?? []), ]; }) ->values(); $rankings = $hotActivities->take(2)->values(); $activeStudyTours = StudyTour::query() ->where('is_active', true) ->orderBy('sort') ->orderByDesc('id') ->limit(3) ->get(['id', 'name', 'tags', 'venue_ids', 'intro_html', 'cover_image']); $venueMap = Venue::query() ->whereIn('id', $activeStudyTours->pluck('venue_ids')->flatten()->filter()->values()->all()) ->get(['id', 'name', 'cover_image']) ->keyBy('id'); $studyTours = $activeStudyTours->map(function ($row) use ($venueMap) { $venueIds = collect($row->venue_ids ?? [])->values(); $venueNames = $venueIds->map(fn ($id) => $venueMap->get($id)?->name)->filter()->values(); $fallbackCover = $venueIds->map(fn ($id) => $venueMap->get($id)?->cover_image)->filter()->first(); $tourCover = trim((string) ($row->cover_image ?? '')); return [ 'id' => $row->id, 'name' => $row->name, 'tags' => array_values($row->tags ?? []), 'venue_names' => $venueNames, 'cover_image' => $tourCover !== '' ? $tourCover : $fallbackCover, ]; })->values(); return response()->json([ 'stats' => $stats, 'banners' => $banners, 'top_live_venues' => $topLiveVenues, 'map_venues' => $mapVenues, 'rankings' => $rankings, 'hot_activities' => $hotActivities, 'study_tours' => $studyTours, 'venue_dicts' => [ 'district' => DictItem::activeOptions('district'), 'venue_type' => DictItem::activeVenueTypeOptionsWithColor(), 'venue_appointment_type' => DictItem::activeOptions('venue_appointment_type'), 'venue_open_mode' => DictItem::activeOptions('venue_open_mode'), 'ticket_type' => DictItem::activeOptions('ticket_type'), ], ]); } }