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(), /** 在馆实时总人数;接入真实客流前固定为 0,勿用随机数 */ 'in_venue_total' => 0, ]; $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(); /** * 在馆 Top3:接入按场馆真实在馆人数前,不返回用「预约票量」汇总的替代数据,避免与「在馆」混淆。 * 需恢复时:按 reservations 或客流表重查,见 git 历史。 */ $topLiveVenues = []; $venueTypeColors = DictItem::query() ->where('dict_type', 'venue_type') ->where('is_active', true) ->pluck('item_remark', 'item_value'); // 全部场馆列表(不过滤经纬度,用于列表展示) $allVenues = Venue::query() ->visibleOnH5() ->orderBy('sort') ->orderByDesc('id') ->get() ->map(function ($v) use ($venueTypeColors) { $p = $v->toArray(); $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' => $p['lat'] ? (float) $p['lat'] : null, 'lng' => $p['lng'] ? (float) $p['lng'] : null, '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'], 'booking_mode' => $p['booking_mode'] ?? null, 'open_mode' => $p['open_mode'] ?? null, 'is_included_in_stats' => (bool) ($p['is_included_in_stats'] ?? false), 'venue_type_color' => $color, ]; }) ->values(); // 地图场馆:只返回有经纬度的(用于地图标点) $mapVenues = $allVenues->filter(function ($v) { return $v['lat'] !== null && $v['lng'] !== null; })->values(); $actRows = Activity::query() ->with('venue:id,name') ->with('activityDays') ->visibleOnH5() ->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 [ 'list_kind' => 'activity', '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 ?? []), ]; }); $tgRows = TicketGrabEvent::query() ->with('venues') ->visibleOnH5() ->get(['id', 'title', 'summary', 'cover_image', 'start_at', 'end_at', 'registered_count', 'address', 'tags', 'sort', 'booking_start_at', 'booking_end_at']) ->map(function ($e) { $first = $e->venues->first(); $t = now()->toDateString(); $bookable = $e->booking_start_at && $e->booking_end_at && $t >= $e->booking_start_at->toDateString() && $t <= $e->booking_end_at->toDateString() && (! $e->end_at || $e->end_at->toDateString() >= $t); return [ 'list_kind' => 'ticket_grab', 'id' => $e->id, 'title' => $e->title, 'summary' => $e->summary, 'image' => $e->cover_image, 'venue_name' => $first?->name, 'address' => $e->address, 'start_at' => CalendarDateFormat::ymdFromDatetime($e->start_at), 'end_at' => CalendarDateFormat::ymdFromDatetime($e->end_at), 'schedule_status' => TicketGrabEvent::computeScheduleStatusFromBounds($e->start_at, $e->end_at), 'registered_count' => (int) ($e->registered_count ?? 0), 'is_bookable' => $bookable, 'can_grab_today' => $e->canGrabToday(), 'venue_count' => $e->venues->count(), 'tags' => array_values($e->tags ?? []), ]; }); $controller = $this; $today = Carbon::now((string) config('app.timezone'))->toDateString(); $hotActivities = $actRows->merge($tgRows)->sort(function (array $x, array $y) use ($today, $controller) { $sx = $controller->homeScheduleRank($x['start_at'] ?? null, $x['end_at'] ?? null, $today); $sy = $controller->homeScheduleRank($y['start_at'] ?? null, $y['end_at'] ?? null, $today); if ($sx !== $sy) { return $sx <=> $sy; } $a = $x['start_at'] ?? ''; $b = $y['start_at'] ?? ''; if ($a !== $b) { if ($a === '') { return 1; } if ($b === '') { return -1; } return strcmp((string) $a, (string) $b); } return (int) $y['id'] <=> (int) $x['id']; })->values()->take(5)->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, 'all_venues' => $allVenues, '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_booking_mode' => DictItem::activeOptions('venue_booking_mode'), 'venue_open_mode' => DictItem::activeOptions('venue_open_mode'), 'ticket_type' => DictItem::activeOptions('ticket_type'), ], ]); } /** * 与活动列表 H5 混合排序:0 进行中/未结束, 1 未开始, 2 已结束. */ private function homeScheduleRank(?string $startIso, ?string $endIso, string $today): int { $s = $startIso ? Carbon::parse($startIso)->toDateString() : null; $e = $endIso ? Carbon::parse($endIso)->toDateString() : null; if (! $e && ! $s) { return 0; } if ($e && $e < $today) { return 2; } if ($s && $s > $today) { return 1; } return 0; } }