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

5 days ago
<?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, '仅可核销已绑定场馆预约');
}
}