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> $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>} */ 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, '仅可操作已绑定场馆'); } }