You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

271 lines
10 KiB

1 month ago
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
4 days ago
use App\Models\Activity;
use App\Models\Reservation;
use App\Models\TicketGrabEvent;
use App\Models\User;
1 month ago
use App\Models\WechatUser;
4 days ago
use App\Models\WechatUserHomeVisit;
use Illuminate\Database\Eloquent\Builder;
1 month ago
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
4 days ago
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use Symfony\Component\HttpFoundation\StreamedResponse;
1 month ago
class WechatUserController extends Controller
{
public function index(Request $request): JsonResponse
{
$pageSize = max(1, min(100, (int) $request->input('page_size', 20)));
4 days ago
$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)));
1 month ago
4 days ago
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
{
1 month ago
$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}%");
});
}
4 days ago
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));
}
}
1 month ago
$user = $request->user();
4 days ago
if ($user instanceof User && ! $user->isSuperAdmin()) {
1 month ago
$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');
});
});
}
1 month ago
}
4 days ago
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);
1 month ago
}
}