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.

263 lines
11 KiB

1 week ago
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Activity;
use App\Models\ActivityDay;
1 week ago
use App\Models\Venue;
1 week ago
use Carbon\Carbon;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
class ActivityBookingController extends Controller
{
private const BOOKING_MODE_INDIVIDUAL = 'individual';
23 hours ago
1 week ago
private const BOOKING_MODE_GROUP = 'group';
23 hours ago
1 week ago
private const BOOKING_MODE_BOTH = 'both';
public function show(Request $request, Activity $activity): JsonResponse
{
$this->ensureVenuePermission($request, $activity->venue_id);
1 week ago
return response()->json($this->formatBookingPayload($activity->load('venue:id,appointment_type')));
1 week ago
}
public function update(Request $request, Activity $activity): JsonResponse
{
$this->ensureVenuePermission($request, $activity->venue_id);
$data = $request->validate([
'booking_audience' => ['required', 'in:individual,group,both'],
'min_people_per_order' => ['nullable', 'integer', 'min:1'],
'max_people_per_order' => ['nullable', 'integer', 'min:1'],
'days' => ['required', 'array', 'min:1'],
23 hours ago
'days.*.id' => ['nullable', 'integer', 'min:1'],
'days.*.session_name' => ['required', 'string', 'max:200'],
'days.*.session_start_at' => ['required', 'date'],
'days.*.session_end_at' => ['required', 'date'],
'days.*.booking_deadline_at' => ['required', 'date'],
'days.*.day_quota' => ['required', 'integer', 'min:1'],
1 week ago
]);
$mode = $data['booking_audience'];
1 week ago
$venue = Venue::query()->findOrFail($activity->venue_id);
if ($venue->appointment_type === 'team_only' && $mode !== self::BOOKING_MODE_GROUP) {
throw ValidationException::withMessages([
23 hours ago
'booking_audience' => ['该场馆预约类型为「仅团队」,场次设置只能选择团体'],
1 week ago
]);
}
1 week ago
$minPeople = (int) ($data['min_people_per_order'] ?? 1);
$maxPeople = (int) ($data['max_people_per_order'] ?? 1);
if ($mode === self::BOOKING_MODE_INDIVIDUAL) {
$minPeople = 1;
$maxPeople = 1;
} else {
1 week ago
$minPeople = max(1, $minPeople);
$maxPeople = max(1, $maxPeople);
1 week ago
if ($maxPeople < $minPeople) {
throw ValidationException::withMessages([
'max_people_per_order' => ['最大预约人数不能小于最小预约人数'],
]);
}
}
23 hours ago
$this->validateSessionRows($activity, $data['days']);
1 week ago
$sumDay = 0;
foreach ($data['days'] as $row) {
$sumDay += (int) $row['day_quota'];
}
DB::transaction(function () use ($activity, $data, $sumDay, $minPeople, $maxPeople) {
$activity->booking_audience = $data['booking_audience'];
$activity->total_quota = $sumDay;
$activity->min_people_per_order = $minPeople;
$activity->max_people_per_order = $maxPeople;
$activity->save();
23 hours ago
$incomingIds = [];
1 week ago
foreach ($data['days'] as $row) {
23 hours ago
$id = isset($row['id']) ? (int) $row['id'] : 0;
$sessionStart = Carbon::parse($row['session_start_at']);
$sessionEnd = Carbon::parse($row['session_end_at']);
$deadline = Carbon::parse($row['booking_deadline_at']);
1 week ago
$dayQuota = (int) $row['day_quota'];
23 hours ago
$name = trim((string) $row['session_name']);
if ($id > 0) {
$day = ActivityDay::query()
->where('activity_id', $activity->id)
->where('id', $id)
->first();
if (!$day) {
throw ValidationException::withMessages(['days' => ['存在无效的场次 id']]);
}
1 week ago
if ($day->booked_count > $dayQuota) {
throw ValidationException::withMessages([
23 hours ago
'days' => ['「' . $name . '」已占用 ' . (int) $day->booked_count . ' 人,总名额不能小于该值'],
1 week ago
]);
}
23 hours ago
$day->session_name = $name;
$day->session_start_at = $sessionStart;
$day->session_end_at = $sessionEnd;
$day->booking_deadline_at = $deadline;
1 week ago
$day->day_quota = $dayQuota;
23 hours ago
$this->fillLegacyOpenClose($activity, $day, $sessionStart, $sessionEnd, $deadline);
1 week ago
$day->save();
23 hours ago
$incomingIds[] = (int) $day->id;
1 week ago
} else {
23 hours ago
$d = new ActivityDay([
1 week ago
'activity_id' => $activity->id,
23 hours ago
'session_name' => $name,
1 week ago
'day_quota' => $dayQuota,
'booked_count' => 0,
23 hours ago
'session_start_at' => $sessionStart,
'session_end_at' => $sessionEnd,
'booking_deadline_at' => $deadline,
1 week ago
]);
23 hours ago
$this->fillLegacyOpenClose($activity, $d, $sessionStart, $sessionEnd, $deadline);
$d->save();
$incomingIds[] = (int) $d->id;
1 week ago
}
}
23 hours ago
$toDelete = ActivityDay::query()
->where('activity_id', $activity->id)
->whereNotIn('id', $incomingIds)
->get();
foreach ($toDelete as $d) {
if ((int) $d->booked_count > 0) {
throw ValidationException::withMessages(['days' => ['已有预约占用,不能删除该场次']]);
1 week ago
}
23 hours ago
$d->delete();
1 week ago
}
});
return response()->json(array_merge(
['message' => '保存成功'],
1 week ago
$this->formatBookingPayload($activity->fresh()->load('venue:id,appointment_type'))
1 week ago
));
}
/**
23 hours ago
* 部分旧逻辑仍读 opens_at/closes_at写入合理近似值以兼容H5/前台以场次、预约截止为准。
1 week ago
*/
23 hours ago
private function fillLegacyOpenClose(
Activity $activity,
ActivityDay $day,
Carbon $sessionStart,
Carbon $sessionEnd,
Carbon $deadline,
): void {
$open = $activity->start_at?->copy()->startOfDay() ?? $sessionStart->copy()->subDays(30);
if ($open->gt($sessionStart)) {
$open = $sessionStart->copy()->subDays(1);
1 week ago
}
23 hours ago
$day->opens_at = $open;
$day->closes_at = $deadline;
1 week ago
}
/**
23 hours ago
* @param array<int, array<string, mixed>> $days
1 week ago
*/
23 hours ago
private function validateSessionRows(Activity $activity, array $days): void
1 week ago
{
23 hours ago
$tz = (string) config('app.timezone');
$actStartD = $activity->start_at?->copy()->timezone($tz)->format('Y-m-d');
$actEndD = $activity->end_at?->copy()->timezone($tz)->format('Y-m-d');
1 week ago
foreach ($days as $row) {
23 hours ago
$name = (string) ($row['session_name'] ?? '');
$sessionStart = Carbon::parse($row['session_start_at'])->timezone($tz);
$sessionEnd = Carbon::parse($row['session_end_at'])->timezone($tz);
$deadline = Carbon::parse($row['booking_deadline_at'])->timezone($tz);
if (!$sessionStart->isSameDay($sessionEnd)) {
1 week ago
throw ValidationException::withMessages([
23 hours ago
'days' => ['「' . $name . '」场次的开始与结束须为同一天内'],
1 week ago
]);
}
23 hours ago
if ($sessionEnd->lte($sessionStart)) {
1 week ago
throw ValidationException::withMessages([
23 hours ago
'days' => ['「' . $name . '」场次结束时间须晚于开始时间'],
1 week ago
]);
}
23 hours ago
if ($actStartD || $actEndD) {
$dStr = $sessionStart->format('Y-m-d');
if ($actStartD && $dStr < $actStartD) {
throw ValidationException::withMessages([
'days' => ['「' . $name . '」场次开始日期不能早于活动开始日期'],
]);
}
if ($actEndD && $dStr > $actEndD) {
throw ValidationException::withMessages([
'days' => ['「' . $name . '」场次开始日期不能晚于活动结束日期'],
]);
}
}
if ($deadline->gt($sessionStart)) {
throw ValidationException::withMessages([
'days' => ['「' . $name . '」预约截止时间不能晚于场次开始时间'],
]);
1 week ago
}
}
}
/**
* @return array{booking_audience: string|null, total_quota: int, days: array<int, array<string, mixed>>}
*/
private function formatBookingPayload(Activity $activity): array
{
23 hours ago
$days = $activity->activityDays()->get()->map(function (ActivityDay $d) {
$deadline = $d->booking_deadline_at ?? $d->closes_at;
1 week ago
return [
'id' => $d->id,
23 hours ago
'session_name' => (string) ($d->session_name ?? ''),
'session_start_at' => $d->session_start_at?->format('Y-m-d H:i:s'),
'session_end_at' => $d->session_end_at?->format('Y-m-d H:i:s'),
'booking_deadline_at' => $d->booking_deadline_at?->format('Y-m-d H:i:s'),
'activity_date' => $d->activity_date?->format('Y-m-d'),
1 week ago
'day_quota' => $d->day_quota,
'booked_count' => $d->booked_count,
23 hours ago
'opens_at' => $d->opens_at?->format('Y-m-d H:i:s'),
'closes_at' => $deadline?->format('Y-m-d H:i:s') ?? $d->opens_at?->format('Y-m-d H:i:s'),
1 week ago
];
})->values()->all();
1 week ago
$audience = $activity->booking_audience ?: self::BOOKING_MODE_BOTH;
$venue = $activity->relationLoaded('venue') ? $activity->venue : $activity->venue()->first(['id', 'appointment_type']);
$minP = 1;
$maxP = 1;
if ($audience === self::BOOKING_MODE_GROUP || $audience === self::BOOKING_MODE_BOTH) {
$minP = max(1, (int) ($activity->min_people_per_order ?? 1));
$maxP = max($minP, (int) ($activity->max_people_per_order ?? $minP));
}
1 week ago
return [
1 week ago
'booking_audience' => $audience,
1 week ago
'total_quota' => (int) ($activity->total_quota ?? 0),
1 week ago
'min_people_per_order' => $minP,
'max_people_per_order' => $maxP,
'venue_appointment_type' => $venue?->appointment_type,
1 week ago
'days' => $days,
];
}
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, '仅可操作已绑定场馆');
}
}