|
|
<?php
|
|
|
|
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
|
|
use App\Http\Controllers\Controller;
|
|
|
use App\Models\Activity;
|
|
|
use App\Models\Reservation;
|
|
|
use App\Models\TicketGrabEvent;
|
|
|
use App\Models\User;
|
|
|
use App\Models\WechatUser;
|
|
|
use App\Models\WechatUserHomeVisit;
|
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
|
use Illuminate\Http\JsonResponse;
|
|
|
use Illuminate\Http\Request;
|
|
|
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
|
|
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
|
|
|
use Symfony\Component\HttpFoundation\StreamedResponse;
|
|
|
|
|
|
class WechatUserController extends Controller
|
|
|
{
|
|
|
public function index(Request $request): JsonResponse
|
|
|
{
|
|
|
$pageSize = max(1, min(100, (int) $request->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<WechatUser>
|
|
|
*/
|
|
|
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<int, WechatUser> $users
|
|
|
* @param list<int> $ids
|
|
|
* @return list<array<string, mixed>>
|
|
|
*/
|
|
|
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<int> $wechatUserIds
|
|
|
* @return array<int, list<array{title: string, status: string, status_label: string}>>
|
|
|
*/
|
|
|
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<array{title?: string, status_label?: string}> $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);
|
|
|
}
|
|
|
}
|