input('page_size', 20))); $query = $this->buildFilteredQuery($request); $paginator = $query->paginate($pageSize); $ids = collect($paginator->items())->pluck('id')->map(fn ($id) => (int) $id)->values()->all(); $paginator->setCollection(collect($this->mapWechatUserRows($paginator->items(), $ids))); return response()->json($paginator); } public function export(Request $request): StreamedResponse { abort_unless($request->user(), 403, '无权限'); $users = $this->buildFilteredQuery($request)->get(); $ids = $users->pluck('id')->map(fn ($id) => (int) $id)->values()->all(); $rows = $this->mapWechatUserRows($users->all(), $ids); $table = [[ '手机号', '昵称', '姓名', '注册时间', '访问量', '预约活动', '预约抢票', ]]; foreach ($rows as $row) { $table[] = [ (string) ($row['phone'] ?? ''), (string) ($row['nickname'] ?? ''), (string) ($row['real_name'] ?? ''), (string) ($row['created_at'] ?? ''), (int) ($row['visit_count'] ?? 0), $this->formatReservationBriefsForExport($row['activity_reservations'] ?? []), $this->formatReservationBriefsForExport($row['ticket_grab_reservations'] ?? []), ]; } $spreadsheet = new Spreadsheet; $sheet = $spreadsheet->getActiveSheet(); $sheet->setTitle('用户列表'); $sheet->fromArray($table, null, 'A1'); $writer = new Xlsx($spreadsheet); $filename = '微信用户列表-'.now()->format('Ymd-His').'.xlsx'; return response()->streamDownload(function () use ($writer) { $writer->save('php://output'); }, $filename, [ 'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', ]); } /** * @return Builder */ private function buildFilteredQuery(Request $request): Builder { $query = WechatUser::query()->orderByDesc('id'); if ($request->filled('keyword')) { $keyword = trim((string) $request->input('keyword')); $query->where(function ($q) use ($keyword) { $q->where('phone', 'like', "%{$keyword}%") ->orWhere('nickname', 'like', "%{$keyword}%") ->orWhere('real_name', 'like', "%{$keyword}%"); }); } if ($request->filled('has_activity_reservation')) { $has = filter_var($request->input('has_activity_reservation'), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); if ($has === true) { $query->whereExists(fn ($sub) => $this->activityReservationExistsSub($sub)); } elseif ($has === false) { $query->whereNotExists(fn ($sub) => $this->activityReservationExistsSub($sub)); } } if ($request->filled('has_ticket_grab_reservation')) { $has = filter_var($request->input('has_ticket_grab_reservation'), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); if ($has === true) { $query->whereExists(fn ($sub) => $this->ticketGrabReservationExistsSub($sub)); } elseif ($has === false) { $query->whereNotExists(fn ($sub) => $this->ticketGrabReservationExistsSub($sub)); } } $user = $request->user(); if ($user instanceof User && ! $user->isSuperAdmin()) { $venueIds = $user->venues()->pluck('venues.id')->all(); if (empty($venueIds)) { $query->whereRaw('1 = 0'); } else { $query->where(function ($outer) use ($venueIds) { $outer->whereExists(function ($sub) use ($venueIds) { $sub->selectRaw('1') ->from('reservations') ->whereNull('reservations.deleted_at') ->whereColumn('reservations.wechat_user_id', 'wechat_users.id') ->whereIn('reservations.venue_id', $venueIds); })->orWhereExists(function ($sub) use ($venueIds) { $sub->selectRaw('1') ->from('reservations') ->whereNull('reservations.deleted_at') ->whereIn('reservations.venue_id', $venueIds) ->whereNotNull('reservations.visitor_phone') ->whereNotNull('wechat_users.phone') ->whereColumn('reservations.visitor_phone', 'wechat_users.phone'); }); }); } } return $query; } /** * @param array $users * @param list $ids * @return list> */ private function mapWechatUserRows(array $users, array $ids): array { if ($ids === []) { return []; } $visitCounts = WechatUserHomeVisit::query() ->whereIn('wechat_user_id', $ids) ->groupBy('wechat_user_id') ->selectRaw('wechat_user_id, COUNT(*) as c') ->pluck('c', 'wechat_user_id'); $activityResMap = $this->loadReservationBriefs($ids, Reservation::KIND_ACTIVITY); $ticketGrabResMap = $this->loadReservationBriefs($ids, Reservation::KIND_TICKET_GRAB); return array_map(function (WechatUser $wu) use ($visitCounts, $activityResMap, $ticketGrabResMap) { $row = $wu->toArray(); $row['visit_count'] = (int) ($visitCounts[$wu->id] ?? 0); $row['activity_reservations'] = $activityResMap[$wu->id] ?? []; $row['ticket_grab_reservations'] = $ticketGrabResMap[$wu->id] ?? []; if ($row['created_at'] !== null) { $row['created_at'] = $wu->created_at?->timezone((string) config('app.timezone'))->format('Y-m-d H:i:s'); } return $row; }, $users); } /** * @param list $wechatUserIds * @return array> */ private function loadReservationBriefs(array $wechatUserIds, string $kind): array { if ($wechatUserIds === []) { return []; } $query = Reservation::query() ->whereIn('wechat_user_id', $wechatUserIds) ->whereNull('deleted_at') ->where('status', '!=', 'cancelled'); if ($kind === Reservation::KIND_TICKET_GRAB) { $query->where('reservation_kind', Reservation::KIND_TICKET_GRAB); } else { $query->where(function ($q) { $q->whereNull('reservation_kind') ->orWhere('reservation_kind', Reservation::KIND_ACTIVITY); }); } $rows = $query ->orderByDesc('id') ->get(['id', 'wechat_user_id', 'activity_id', 'ticket_grab_event_id', 'status']); $activityIds = $rows->pluck('activity_id')->filter()->unique()->values()->all(); $eventIds = $rows->pluck('ticket_grab_event_id')->filter()->unique()->values()->all(); $activityTitles = $activityIds === [] ? collect() : Activity::query()->whereIn('id', $activityIds)->pluck('title', 'id'); $eventTitles = $eventIds === [] ? collect() : TicketGrabEvent::query()->whereIn('id', $eventIds)->pluck('title', 'id'); $out = []; foreach ($rows as $r) { $uid = (int) $r->wechat_user_id; $title = $kind === Reservation::KIND_TICKET_GRAB ? (string) ($eventTitles[(int) $r->ticket_grab_event_id] ?? ('抢票#'.$r->ticket_grab_event_id)) : (string) ($activityTitles[(int) $r->activity_id] ?? ('活动#'.$r->activity_id)); $out[$uid][] = [ 'title' => $title, 'status' => (string) $r->status, 'status_label' => $this->reservationStatusLabel((string) $r->status), ]; } return $out; } private function reservationStatusLabel(string $status): string { return match ($status) { 'pending' => '待核销', 'verified' => '已核销', 'cancelled' => '已取消', 'expired' => '已过期', default => $status, }; } /** * @param list $items */ private function formatReservationBriefsForExport(array $items): string { if ($items === []) { return ''; } $parts = []; foreach ($items as $it) { $title = trim((string) ($it['title'] ?? '')); $label = trim((string) ($it['status_label'] ?? '')); $parts[] = $title !== '' ? "{$title}({$label})" : $label; } return implode(';', $parts); } private function activityReservationExistsSub($sub): void { $sub->selectRaw('1') ->from('reservations') ->whereNull('reservations.deleted_at') ->whereColumn('reservations.wechat_user_id', 'wechat_users.id') ->where(function ($q) { $q->whereNull('reservations.reservation_kind') ->orWhere('reservations.reservation_kind', Reservation::KIND_ACTIVITY); }); } private function ticketGrabReservationExistsSub($sub): void { $sub->selectRaw('1') ->from('reservations') ->whereNull('reservations.deleted_at') ->whereColumn('reservations.wechat_user_id', 'wechat_users.id') ->where('reservations.reservation_kind', Reservation::KIND_TICKET_GRAB); } }