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
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,
|
|
};
|
|
}
|
|
}
|