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.
220 lines
8.7 KiB
220 lines
8.7 KiB
<?php
|
|
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\Activity;
|
|
use App\Models\ActivityDay;
|
|
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';
|
|
private const BOOKING_MODE_GROUP = 'group';
|
|
private const BOOKING_MODE_BOTH = 'both';
|
|
|
|
public function show(Request $request, Activity $activity): JsonResponse
|
|
{
|
|
$this->ensureVenuePermission($request, $activity->venue_id);
|
|
|
|
return response()->json($this->formatBookingPayload($activity));
|
|
}
|
|
|
|
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'],
|
|
'days.*.activity_date' => ['required', 'date'],
|
|
'days.*.day_quota' => ['required', 'integer', 'min:0'],
|
|
'days.*.opens_at' => ['required', 'date'],
|
|
'days.*.closes_at' => ['required', 'date'],
|
|
]);
|
|
|
|
$mode = $data['booking_audience'];
|
|
$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 {
|
|
if ($maxPeople < $minPeople) {
|
|
throw ValidationException::withMessages([
|
|
'max_people_per_order' => ['最大预约人数不能小于最小预约人数'],
|
|
]);
|
|
}
|
|
}
|
|
|
|
$this->validateDaysWithinActivityRange($activity, $data['days']);
|
|
$this->validateUniqueDates($data['days']);
|
|
$this->validateBookingPeriodWithinActivityDay($data['days']);
|
|
|
|
$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();
|
|
|
|
$incomingDateKeys = collect($data['days'])->map(function (array $row) {
|
|
return Carbon::parse($row['activity_date'])->format('Y-m-d');
|
|
})->values();
|
|
|
|
$existing = ActivityDay::where('activity_id', $activity->id)->get()->keyBy(fn (ActivityDay $d) => $d->activity_date->format('Y-m-d'));
|
|
|
|
foreach ($data['days'] as $row) {
|
|
$dateKey = Carbon::parse($row['activity_date'])->format('Y-m-d');
|
|
$dayQuota = (int) $row['day_quota'];
|
|
$opensAt = Carbon::parse($row['opens_at']);
|
|
$closesAt = Carbon::parse($row['closes_at']);
|
|
|
|
if ($existing->has($dateKey)) {
|
|
/** @var ActivityDay $day */
|
|
$day = $existing->get($dateKey);
|
|
if ($day->booked_count > $dayQuota) {
|
|
throw ValidationException::withMessages([
|
|
'days' => ["{$dateKey} 当日已占用 {$day->booked_count} 张,放票数不能少于该值"],
|
|
]);
|
|
}
|
|
$day->day_quota = $dayQuota;
|
|
$day->opens_at = $opensAt;
|
|
$day->closes_at = $closesAt;
|
|
$day->save();
|
|
} else {
|
|
ActivityDay::create([
|
|
'activity_id' => $activity->id,
|
|
'activity_date' => $dateKey,
|
|
'day_quota' => $dayQuota,
|
|
'booked_count' => 0,
|
|
'opens_at' => $opensAt,
|
|
'closes_at' => $closesAt,
|
|
]);
|
|
}
|
|
}
|
|
|
|
foreach ($existing as $dateKey => $day) {
|
|
if (!$incomingDateKeys->contains($dateKey)) {
|
|
if ($day->booked_count > 0) {
|
|
throw ValidationException::withMessages([
|
|
'days' => ["{$dateKey} 已有预约占用,不能移除该放票日"],
|
|
]);
|
|
}
|
|
$day->delete();
|
|
}
|
|
}
|
|
});
|
|
|
|
return response()->json(array_merge(
|
|
['message' => '保存成功'],
|
|
$this->formatBookingPayload($activity->fresh())
|
|
));
|
|
}
|
|
|
|
/**
|
|
* @param array<int, array{activity_date: mixed, day_quota: int, opens_at: mixed}> $days
|
|
*/
|
|
private function validateDaysWithinActivityRange(Activity $activity, array $days): void
|
|
{
|
|
if (!$activity->start_at || !$activity->end_at) {
|
|
return;
|
|
}
|
|
$start = $activity->start_at->copy()->startOfDay();
|
|
$end = $activity->end_at->copy()->startOfDay();
|
|
foreach ($days as $row) {
|
|
$d = Carbon::parse($row['activity_date'])->startOfDay();
|
|
if ($d->lt($start) || $d->gt($end)) {
|
|
throw ValidationException::withMessages([
|
|
'days' => ['活动日 '.$d->format('Y-m-d').' 须在活动开始、结束日期范围内'],
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 开始/结束预约时刻的日期不能晚于活动日,且结束时刻需晚于开始时刻。
|
|
*
|
|
* @param array<int, array{activity_date: mixed, opens_at: mixed, closes_at: mixed}> $days
|
|
*/
|
|
private function validateBookingPeriodWithinActivityDay(array $days): void
|
|
{
|
|
foreach ($days as $row) {
|
|
$actDay = Carbon::parse($row['activity_date'])->startOfDay();
|
|
$opens = Carbon::parse($row['opens_at']);
|
|
$closes = Carbon::parse($row['closes_at']);
|
|
if ($opens->copy()->startOfDay()->gt($actDay) || $closes->copy()->startOfDay()->gt($actDay)) {
|
|
throw ValidationException::withMessages([
|
|
'days' => ['活动日 '.$actDay->format('Y-m-d').':预约开始/结束时刻的日期不能晚于活动日当天'],
|
|
]);
|
|
}
|
|
if ($closes->lte($opens)) {
|
|
throw ValidationException::withMessages([
|
|
'days' => ['活动日 '.$actDay->format('Y-m-d').':预约结束时刻必须晚于开始时刻'],
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param array<int, array{activity_date: mixed}> $days
|
|
*/
|
|
private function validateUniqueDates(array $days): void
|
|
{
|
|
$seen = [];
|
|
foreach ($days as $row) {
|
|
$key = Carbon::parse($row['activity_date'])->format('Y-m-d');
|
|
if (isset($seen[$key])) {
|
|
throw ValidationException::withMessages(['days' => ['活动日不能重复']]);
|
|
}
|
|
$seen[$key] = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return array{booking_audience: string|null, total_quota: int, days: array<int, array<string, mixed>>}
|
|
*/
|
|
private function formatBookingPayload(Activity $activity): array
|
|
{
|
|
$days = $activity->activityDays()->orderBy('activity_date')->get()->map(function (ActivityDay $d) {
|
|
return [
|
|
'id' => $d->id,
|
|
'activity_date' => $d->activity_date->format('Y-m-d'),
|
|
'day_quota' => $d->day_quota,
|
|
'booked_count' => $d->booked_count,
|
|
'opens_at' => $d->opens_at->format('Y-m-d H:i:s'),
|
|
'closes_at' => ($d->closes_at ?: $d->opens_at->copy()->endOfDay())->format('Y-m-d H:i:s'),
|
|
];
|
|
})->values()->all();
|
|
|
|
return [
|
|
'booking_audience' => $activity->booking_audience,
|
|
'total_quota' => (int) ($activity->total_quota ?? 0),
|
|
'min_people_per_order' => max(1, (int) ($activity->min_people_per_order ?? 1)),
|
|
'max_people_per_order' => max(1, (int) ($activity->max_people_per_order ?? 1)),
|
|
'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, '仅可操作已绑定场馆');
|
|
}
|
|
}
|