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.

221 lines
8.5 KiB

<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Blacklist;
use App\Models\Reservation;
use Carbon\Carbon;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\StreamedResponse;
class ActivityRegistrationController extends Controller
{
public function index(Request $request): JsonResponse
{
$query = Reservation::with([
'venue:id,name',
'activity:id,title',
'activityDay:id,activity_id,activity_date',
])
->orderByDesc('id');
$user = $request->user();
if (!$user->isSuperAdmin()) {
$query->whereIn('venue_id', $user->venues()->pluck('venues.id'));
}
if ($request->filled('status') && $request->string('status')->toString() !== 'all') {
$query->where('status', (string) $request->string('status'));
}
if ($request->filled('keyword')) {
$keyword = trim((string) $request->input('keyword'));
$query->where(function ($q) use ($keyword) {
$q->where('visitor_name', 'like', "%{$keyword}%")
->orWhere('visitor_phone', 'like', "%{$keyword}%")
->orWhere('id_card', 'like', "%{$keyword}%")
->orWhere('qr_token', 'like', "%{$keyword}%");
});
}
if ($request->filled('start_date')) {
$query->whereDate('created_at', '>=', (string) $request->input('start_date'));
}
if ($request->filled('end_date')) {
$query->whereDate('created_at', '<=', (string) $request->input('end_date'));
}
$pageSize = max(1, min(200, (int) $request->input('page_size', 10)));
$page = $query->paginate($pageSize);
$rows = collect($page->items());
if ($rows->isNotEmpty()) {
$venueIds = $rows->pluck('venue_id')->filter()->map(fn ($v) => (int) $v)->unique()->values();
$phones = $rows->pluck('visitor_phone')->filter()->map(fn ($v) => (string) $v)->unique()->values();
$blackSet = Blacklist::query()
->whereIn('venue_id', $venueIds)
->whereIn('visitor_phone', $phones)
->get(['venue_id', 'visitor_phone'])
->mapWithKeys(fn ($b) => [$b->venue_id.'#'.$b->visitor_phone => true]);
$page->setCollection($rows->map(function ($row) use ($blackSet) {
$row->is_blacklisted = $row->visitor_phone
? isset($blackSet[$row->venue_id.'#'.$row->visitor_phone])
: false;
return $row;
}));
}
return response()->json($page);
}
public function export(Request $request): StreamedResponse
{
$query = Reservation::with([
'venue:id,name',
'activity:id,title',
'activityDay:id,activity_id,activity_date',
])->orderByDesc('id');
$user = $request->user();
if (!$user->isSuperAdmin()) {
$query->whereIn('venue_id', $user->venues()->pluck('venues.id'));
}
if ($request->filled('status') && $request->string('status')->toString() !== 'all') {
$query->where('status', (string) $request->string('status'));
}
if ($request->filled('keyword')) {
$keyword = trim((string) $request->input('keyword'));
$query->where(function ($q) use ($keyword) {
$q->where('visitor_name', 'like', "%{$keyword}%")
->orWhere('visitor_phone', 'like', "%{$keyword}%")
->orWhere('id_card', 'like', "%{$keyword}%")
->orWhere('qr_token', 'like', "%{$keyword}%");
});
}
if ($request->filled('start_date')) {
$query->whereDate('created_at', '>=', (string) $request->input('start_date'));
}
if ($request->filled('end_date')) {
$query->whereDate('created_at', '<=', (string) $request->input('end_date'));
}
$rows = $query->limit(5000)->get();
$filename = 'activity-registrations-' . now()->format('Ymd-His') . '.csv';
return response()->streamDownload(function () use ($rows) {
$out = fopen('php://output', 'w');
fprintf($out, chr(0xEF) . chr(0xBB) . chr(0xBF));
fputcsv($out, ['ID', '活动', '场馆', '报名人', '手机号', '身份证', '预约票数', '预约入馆日期', '状态', '预约时间', '核销时间', '二维码Token']);
foreach ($rows as $row) {
$entryDate = $row->activityDay?->activity_date;
$entryDateStr = $entryDate ? Carbon::parse($entryDate)->timezone('Asia/Shanghai')->format('Y-m-d') : '';
fputcsv($out, [
$row->id,
$row->activity?->title ?? '',
$row->venue?->name ?? '',
$row->visitor_name,
$row->visitor_phone ?? '',
$row->id_card ?? '',
(string) ($row->ticket_count ?? 1),
$entryDateStr,
self::statusLabel($row->status),
self::formatShanghai($row->created_at),
self::formatShanghai($row->verified_at),
$row->qr_token,
]);
}
fclose($out);
}, $filename, ['Content-Type' => 'text/csv; charset=UTF-8']);
}
public function quickBlacklist(Request $request, Reservation $reservation): JsonResponse
{
$user = $request->user();
if (!$user->isSuperAdmin()) {
$allowed = $user->venues()->where('venues.id', $reservation->venue_id)->exists();
abort_unless($allowed, 403, '仅可操作已绑定场馆');
}
if (!$reservation->visitor_phone) {
return response()->json(['message' => '该报名记录无手机号,无法加入黑名单'], 422);
}
$data = $request->validate([
'reason' => ['required', 'string', 'max:255'],
]);
$item = Blacklist::updateOrCreate(
['venue_id' => $reservation->venue_id, 'visitor_phone' => $reservation->visitor_phone],
[
'visitor_name' => $reservation->visitor_name,
'reason' => $data['reason'],
],
);
return response()->json($item->load('venue:id,name'), 201);
}
public function batchQuickBlacklist(Request $request): JsonResponse
{
$data = $request->validate([
'reservation_ids' => ['required', 'array', 'min:1'],
'reservation_ids.*' => ['required', 'integer', 'exists:reservations,id'],
'reason' => ['required', 'string', 'max:255'],
]);
$user = $request->user();
$ids = collect($data['reservation_ids'])->map(fn ($v) => (int) $v)->unique()->values();
$reservations = Reservation::whereIn('id', $ids)->get();
$count = 0;
$failed = [];
foreach ($reservations as $reservation) {
if (!$user->isSuperAdmin()) {
$allowed = $user->venues()->where('venues.id', $reservation->venue_id)->exists();
if (!$allowed) {
$failed[] = ['reservation_id' => $reservation->id, 'reason' => '无场馆权限'];
continue;
}
}
if (!$reservation->visitor_phone) {
$failed[] = ['reservation_id' => $reservation->id, 'reason' => '缺少手机号'];
continue;
}
Blacklist::updateOrCreate(
['venue_id' => $reservation->venue_id, 'visitor_phone' => $reservation->visitor_phone],
[
'visitor_name' => $reservation->visitor_name,
'reason' => $data['reason'],
],
);
$count++;
}
return response()->json([
'message' => '批量拉黑完成',
'count' => $count,
'failed_count' => count($failed),
'failed' => $failed,
]);
}
private static function formatShanghai(mixed $value): string
{
if ($value === null) {
return '';
}
return Carbon::parse($value)->timezone('Asia/Shanghai')->format('Y-m-d H:i:s');
}
private static function statusLabel(string $status): string
{
return match ($status) {
'pending' => '待核销',
'verified' => '已核销',
'cancelled' => '已取消',
default => $status,
};
}
}