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.

211 lines
7.6 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Reservation;
use Carbon\CarbonInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ReservationVerifyController extends Controller
{
/**
* 移动端「今日」核销仅允许活动日activity_days.activity_date为当天的预约核销。
*/
private function activityDayNotTodayMessage(Reservation $reservation): ?string
{
$reservation->loadMissing('activityDay');
$today = now()->toDateString();
if (!$reservation->activity_day_id || !$reservation->activityDay) {
return '该预约未关联活动场次,仅支持核销「活动日为今日」的预约';
}
$ad = $reservation->activityDay->activity_date;
$dateStr = $ad instanceof CarbonInterface ? $ad->format('Y-m-d') : (string) $ad;
if ($dateStr !== $today) {
return '仅可核销活动日为今日的预约。该预约活动日为:'.$dateStr;
}
return null;
}
/**
* 扫码后先查询预约信息,不修改状态(用于移动端「确认后再核销」)。
*/
public function preview(Request $request): JsonResponse
{
$data = $request->validate([
'qr_token' => ['required', 'string'],
]);
$reservation = Reservation::with([
'venue:id,name,reservation_notice',
'activity:id,title',
'activityDay:id,activity_id,activity_date',
])->where('qr_token', $data['qr_token'])->first();
if (!$reservation) {
return response()->json(['message' => '无效的二维码或预约不存在'], 404);
}
$this->ensureVenuePermission($request, $reservation->venue_id);
$blockReason = null;
if ($reservation->status === 'verified') {
$blockReason = '该预约已核销';
} elseif ($reservation->status === 'cancelled') {
$blockReason = '该预约已取消,不能核销';
}
$canVerify = $reservation->status === 'pending';
if ($canVerify) {
$dayMsg = $this->activityDayNotTodayMessage($reservation);
if ($dayMsg !== null) {
$canVerify = false;
$blockReason = $dayMsg;
}
}
return response()->json([
'reservation' => $reservation,
'can_verify' => $canVerify,
'verify_block_reason' => $blockReason,
]);
}
public function index(Request $request): JsonResponse
{
$user = $request->user();
$query = Reservation::with([
'venue:id,name,reservation_notice',
'activity:id,title',
'activityDay:id,activity_id,activity_date',
])
// 核销端列表:待核销在前,同状态内按 id 倒序
->orderByRaw("CASE WHEN status = 'pending' THEN 0 ELSE 1 END")
->orderByDesc('id');
if (!$user->isSuperAdmin()) {
$venueIds = $user->venues()->pluck('venues.id');
$query->whereIn('venue_id', $venueIds);
}
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}%");
});
}
$dateField = (string) $request->input('date_field', 'created_at');
if ($request->filled('start_date') || $request->filled('end_date')) {
if ($dateField === 'activity_day') {
$start = $request->filled('start_date') ? (string) $request->input('start_date') : null;
$end = $request->filled('end_date') ? (string) $request->input('end_date') : $start;
$query->whereHas('activityDay', function ($q) use ($start, $end) {
if ($start !== null && $start !== '') {
$q->whereDate('activity_date', '>=', $start);
}
if ($end !== null && $end !== '') {
$q->whereDate('activity_date', '<=', $end);
}
});
} else {
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'));
}
}
}
return response()->json($query->limit(500)->get());
}
/**
* 今日(活动日为当天)预约单数、已核销单数,与移动端「今日报名」同权限与场馆范围。
* 一大一小按 1 单计算(按预约订单数统计)。
*/
public function todaySummary(Request $request): JsonResponse
{
$user = $request->user();
$today = now()->toDateString();
$base = Reservation::query()
->whereHas('activityDay', function ($q) use ($today) {
$q->whereDate('activity_date', $today);
});
if (!$user->isSuperAdmin()) {
$venueIds = $user->venues()->pluck('venues.id');
$base->whereIn('venue_id', $venueIds);
}
$totalOrders = (clone $base)->whereNotIn('status', ['cancelled'])->count();
$verifiedOrders = (clone $base)->where('status', 'verified')->count();
return response()->json([
'total_orders' => (int) $totalOrders,
'verified_orders' => (int) $verifiedOrders,
]);
}
public function verifyByToken(Request $request): JsonResponse
{
$data = $request->validate([
'qr_token' => ['required', 'string'],
]);
$reservation = Reservation::with('venue')->where('qr_token', $data['qr_token'])->firstOrFail();
$this->ensureVenuePermission($request, $reservation->venue_id);
if ($reservation->status === 'verified') {
return response()->json(['message' => '该预约已核销'], 422);
}
if ($reservation->status === 'cancelled') {
return response()->json(['message' => '该预约已取消,不能核销'], 422);
}
$dayBlock = $this->activityDayNotTodayMessage($reservation);
if ($dayBlock !== null) {
return response()->json(['message' => $dayBlock], 422);
}
$reservation->update([
'status' => 'verified',
'verified_by' => $request->user()->id,
'verified_at' => now(),
]);
return response()->json([
'message' => '核销成功',
'reservation' => $reservation->fresh()->load([
'venue:id,name,reservation_notice',
'activity:id,title',
'activityDay:id,activity_id,activity_date',
'verifier:id,name,username',
]),
]);
}
private function ensureVenuePermission(Request $request, int $venueId): void
{
$user = $request->user();
if ($user->isSuperAdmin()) {
return;
}
$allowed = $user->venues()->where('venues.id', $venueId)->exists();
abort_unless($allowed, 403, '仅可核销已绑定场馆预约');
}
}