|
|
<?php
|
|
|
|
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
|
|
use App\Http\Controllers\Controller;
|
|
|
use App\Models\Activity;
|
|
|
use App\Models\ActivityDay;
|
|
|
use App\Models\Venue;
|
|
|
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->load('venue:id,appointment_type')));
|
|
|
}
|
|
|
|
|
|
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.*.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'],
|
|
|
]);
|
|
|
|
|
|
$mode = $data['booking_audience'];
|
|
|
$venue = Venue::query()->findOrFail($activity->venue_id);
|
|
|
if ($venue->appointment_type === 'team_only' && $mode !== self::BOOKING_MODE_GROUP) {
|
|
|
throw ValidationException::withMessages([
|
|
|
'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 {
|
|
|
$minPeople = max(1, $minPeople);
|
|
|
$maxPeople = max(1, $maxPeople);
|
|
|
if ($maxPeople < $minPeople) {
|
|
|
throw ValidationException::withMessages([
|
|
|
'max_people_per_order' => ['最大预约人数不能小于最小预约人数'],
|
|
|
]);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
$this->validateSessionRows($activity, $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();
|
|
|
|
|
|
$incomingIds = [];
|
|
|
foreach ($data['days'] as $row) {
|
|
|
$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']);
|
|
|
$dayQuota = (int) $row['day_quota'];
|
|
|
$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']]);
|
|
|
}
|
|
|
if ($day->booked_count > $dayQuota) {
|
|
|
throw ValidationException::withMessages([
|
|
|
'days' => ['「' . $name . '」已占用 ' . (int) $day->booked_count . ' 人,总名额不能小于该值'],
|
|
|
]);
|
|
|
}
|
|
|
$day->session_name = $name;
|
|
|
$day->session_start_at = $sessionStart;
|
|
|
$day->session_end_at = $sessionEnd;
|
|
|
$day->booking_deadline_at = $deadline;
|
|
|
$day->day_quota = $dayQuota;
|
|
|
$this->fillLegacyOpenClose($activity, $day, $sessionStart, $sessionEnd, $deadline);
|
|
|
$day->save();
|
|
|
$incomingIds[] = (int) $day->id;
|
|
|
} else {
|
|
|
$d = new ActivityDay([
|
|
|
'activity_id' => $activity->id,
|
|
|
'session_name' => $name,
|
|
|
'day_quota' => $dayQuota,
|
|
|
'booked_count' => 0,
|
|
|
'session_start_at' => $sessionStart,
|
|
|
'session_end_at' => $sessionEnd,
|
|
|
'booking_deadline_at' => $deadline,
|
|
|
]);
|
|
|
$this->fillLegacyOpenClose($activity, $d, $sessionStart, $sessionEnd, $deadline);
|
|
|
$d->save();
|
|
|
$incomingIds[] = (int) $d->id;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
$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' => ['已有预约占用,不能删除该场次']]);
|
|
|
}
|
|
|
$d->delete();
|
|
|
}
|
|
|
});
|
|
|
|
|
|
return response()->json(array_merge(
|
|
|
['message' => '保存成功'],
|
|
|
$this->formatBookingPayload($activity->fresh()->load('venue:id,appointment_type'))
|
|
|
));
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 部分旧逻辑仍读 opens_at/closes_at,写入合理近似值以兼容;H5/前台以场次、预约截止为准。
|
|
|
*/
|
|
|
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);
|
|
|
}
|
|
|
$day->opens_at = $open;
|
|
|
$day->closes_at = $deadline;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* @param array<int, array<string, mixed>> $days
|
|
|
*/
|
|
|
private function validateSessionRows(Activity $activity, array $days): void
|
|
|
{
|
|
|
$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');
|
|
|
|
|
|
foreach ($days as $row) {
|
|
|
$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)) {
|
|
|
throw ValidationException::withMessages([
|
|
|
'days' => ['「' . $name . '」场次的开始与结束须为同一天内'],
|
|
|
]);
|
|
|
}
|
|
|
if ($sessionEnd->lte($sessionStart)) {
|
|
|
throw ValidationException::withMessages([
|
|
|
'days' => ['「' . $name . '」场次结束时间须晚于开始时间'],
|
|
|
]);
|
|
|
}
|
|
|
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 . '」预约截止时间不能晚于场次开始时间'],
|
|
|
]);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* @return array{booking_audience: string|null, total_quota: int, days: array<int, array<string, mixed>>}
|
|
|
*/
|
|
|
private function formatBookingPayload(Activity $activity): array
|
|
|
{
|
|
|
$days = $activity->activityDays()->get()->map(function (ActivityDay $d) {
|
|
|
$deadline = $d->booking_deadline_at ?? $d->closes_at;
|
|
|
|
|
|
return [
|
|
|
'id' => $d->id,
|
|
|
'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'),
|
|
|
'day_quota' => $d->day_quota,
|
|
|
'booked_count' => $d->booked_count,
|
|
|
'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'),
|
|
|
];
|
|
|
})->values()->all();
|
|
|
|
|
|
$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));
|
|
|
}
|
|
|
|
|
|
return [
|
|
|
'booking_audience' => $audience,
|
|
|
'total_quota' => (int) ($activity->total_quota ?? 0),
|
|
|
'min_people_per_order' => $minP,
|
|
|
'max_people_per_order' => $maxP,
|
|
|
'venue_appointment_type' => $venue?->appointment_type,
|
|
|
'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, '仅可操作已绑定场馆');
|
|
|
}
|
|
|
}
|