From a9ac9cc44cef22cf8b7e8e07fd5f73192938f393 Mon Sep 17 00:00:00 2001 From: lion <120344285@qq.com> Date: Fri, 1 May 2026 13:29:37 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Api/ActivityBookingController.php | 3 + .../Controllers/Api/ActivityController.php | 20 ++- .../Controllers/Api/H5ContentController.php | 11 +- app/Http/Controllers/Api/H5HomeController.php | 2 +- .../Api/H5ReservationController.php | 4 +- app/Models/Activity.php | 151 ++++++++++++++++-- 6 files changed, 161 insertions(+), 30 deletions(-) diff --git a/app/Http/Controllers/Api/ActivityBookingController.php b/app/Http/Controllers/Api/ActivityBookingController.php index 9c3eea3..26eef75 100644 --- a/app/Http/Controllers/Api/ActivityBookingController.php +++ b/app/Http/Controllers/Api/ActivityBookingController.php @@ -153,6 +153,9 @@ class ActivityBookingController extends Controller } }); + $activity->load('activityDays'); + $activity->forceFill(['schedule_status' => Activity::computeScheduleStatusForDisplay($activity)])->saveQuietly(); + return response()->json(array_merge( ['message' => '保存成功'], $this->formatBookingPayload($activity->fresh()->load('venue:id,appointment_type')) diff --git a/app/Http/Controllers/Api/ActivityController.php b/app/Http/Controllers/Api/ActivityController.php index 3077cb5..65fa9c6 100644 --- a/app/Http/Controllers/Api/ActivityController.php +++ b/app/Http/Controllers/Api/ActivityController.php @@ -17,7 +17,7 @@ class ActivityController extends Controller use AuthorizesActivitySubmitter; public function index(Request $request): JsonResponse { - $query = Activity::with('venue:id,name')->orderByHotThenScheduleStatusPriority(); + $query = Activity::with(['venue:id,name', 'activityDays'])->orderByHotThenScheduleStatusPriority(); $this->restrictByVenue($request, $query); if ($request->filled('keyword')) { @@ -107,10 +107,6 @@ class ActivityController extends Controller } $this->normalizeActivityDates($data); - $data['schedule_status'] = Activity::computeScheduleStatusFromBounds( - $data['start_at'] ?? null, - $data['end_at'] ?? null - ); $auditStatus = $request->user()->isSuperAdmin() ? Activity::AUDIT_APPROVED : Activity::AUDIT_PENDING; @@ -129,6 +125,9 @@ class ActivityController extends Controller } VerifyPortalCode::ensureForActivity($activity); + $activity->load('activityDays'); + $activity->forceFill(['schedule_status' => Activity::computeScheduleStatusForDisplay($activity)])->saveQuietly(); + $act = $activity->load('venue:id,name'); if (! $request->user()?->isSuperAdmin()) { $act->makeHidden(['is_hot']); @@ -197,17 +196,16 @@ class ActivityController extends Controller $this->normalizeActivityDates($data); - $start = array_key_exists('start_at', $data) ? $data['start_at'] : $activity->start_at; - $end = array_key_exists('end_at', $data) ? $data['end_at'] : $activity->end_at; - $data['schedule_status'] = Activity::computeScheduleStatusFromBounds($start, $end); - $activity->fill($data)->save(); if (array_key_exists('tags', $data)) { $activity->tags = array_values($data['tags'] ?? []); $activity->save(); } - $fresh = $activity->fresh()->load('venue:id,name'); + $activity->load('activityDays'); + $activity->forceFill(['schedule_status' => Activity::computeScheduleStatusForDisplay($activity)])->saveQuietly(); + + $fresh = $activity->fresh()->load(['venue:id,name', 'activityDays']); if (! $request->user()?->isSuperAdmin()) { $fresh->makeHidden(['is_hot']); } @@ -237,7 +235,7 @@ class ActivityController extends Controller { $this->ensureVenuePermission($request, $activity->venue_id); $this->authorizeActivityEditForNonSuperAdmin($request, $activity); - $status = Activity::computeScheduleStatusFromBounds($activity->start_at, $activity->end_at); + $status = Activity::computeScheduleStatusForDisplay($activity->load('activityDays')); abort_unless($status === 'ended', 422, '仅已结束活动可上传花絮'); $data = $request->validate([ diff --git a/app/Http/Controllers/Api/H5ContentController.php b/app/Http/Controllers/Api/H5ContentController.php index 3705759..e03c392 100644 --- a/app/Http/Controllers/Api/H5ContentController.php +++ b/app/Http/Controllers/Api/H5ContentController.php @@ -63,7 +63,7 @@ class H5ContentController extends Controller 'venue_lng' => $a->venue?->lng, 'start_at' => optional($a->start_at)?->toIso8601String(), 'end_at' => optional($a->end_at)?->toIso8601String(), - 'schedule_status' => Activity::computeScheduleStatusFromBounds($a->start_at, $a->end_at), + 'schedule_status' => Activity::computeScheduleStatusForDisplay($a), 'registered_count' => (int) ($a->registered_count ?? 0), 'tags' => array_values($a->tags ?? []), 'is_bookable' => $isBookable, @@ -142,7 +142,7 @@ class H5ContentController extends Controller 'lng' => $a->lng, 'start_at' => optional($a->start_at)?->toIso8601String(), 'end_at' => optional($a->end_at)?->toIso8601String(), - 'schedule_status' => Activity::computeScheduleStatusFromBounds($a->start_at, $a->end_at), + 'schedule_status' => Activity::computeScheduleStatusForDisplay($a), 'registered_count' => (int) ($a->registered_count ?? 0), 'tags' => array_values($a->tags ?? []), 'reservation_notice' => $a->reservation_notice, @@ -414,9 +414,10 @@ class H5ContentController extends Controller ->where('venue_id', $v->id) ->visibleOnH5() ->orderByHotThenScheduleStatusPriority() + ->with('activityDays') ->with('venue:id,cover_image') ->limit(50) - ->get(['id', 'title', 'summary', 'cover_image', 'start_at', 'end_at', 'registered_count', 'address', 'location', 'venue_id', 'behind_scenes_media', 'is_hot']) + ->get(['id', 'title', 'summary', 'cover_image', 'start_at', 'end_at', 'registered_count', 'address', 'location', 'venue_id', 'behind_scenes_media', 'is_hot', 'reservation_type']) ->map(function (Activity $a) { return [ 'id' => $a->id, @@ -425,7 +426,7 @@ class H5ContentController extends Controller 'cover_image' => ActivityH5View::listCover($a), 'start_at' => optional($a->start_at)?->toIso8601String(), 'end_at' => optional($a->end_at)?->toIso8601String(), - 'schedule_status' => Activity::computeScheduleStatusFromBounds($a->start_at, $a->end_at), + 'schedule_status' => Activity::computeScheduleStatusForDisplay($a), 'registered_count' => (int) ($a->registered_count ?? 0), 'address' => $a->location ?: $a->address, 'has_behind_scenes' => $this->activityHasBehindScenes($a), @@ -727,7 +728,7 @@ class H5ContentController extends Controller 'venue_lng' => $a->venue?->lng, 'start_at' => optional($a->start_at)?->toIso8601String(), 'end_at' => optional($a->end_at)?->toIso8601String(), - 'schedule_status' => Activity::computeScheduleStatusFromBounds($a->start_at, $a->end_at), + 'schedule_status' => Activity::computeScheduleStatusForDisplay($a), 'registered_count' => (int) ($a->registered_count ?? 0), 'tags' => array_values($a->tags ?? []), 'is_bookable' => $isBookable, diff --git a/app/Http/Controllers/Api/H5HomeController.php b/app/Http/Controllers/Api/H5HomeController.php index 7a355d9..19ca231 100644 --- a/app/Http/Controllers/Api/H5HomeController.php +++ b/app/Http/Controllers/Api/H5HomeController.php @@ -127,7 +127,7 @@ class H5HomeController extends Controller 'address' => $a->address, 'start_at' => optional($a->start_at)?->toIso8601String(), 'end_at' => optional($a->end_at)?->toIso8601String(), - 'schedule_status' => Activity::computeScheduleStatusFromBounds($a->start_at, $a->end_at), + 'schedule_status' => Activity::computeScheduleStatusForDisplay($a), 'registered_count' => (int) ($a->registered_count ?? 0), 'is_bookable' => $isBookable, 'all_slots_full' => $a->areAllActivityDaySlotsExhausted(), diff --git a/app/Http/Controllers/Api/H5ReservationController.php b/app/Http/Controllers/Api/H5ReservationController.php index ac87106..36a84b9 100644 --- a/app/Http/Controllers/Api/H5ReservationController.php +++ b/app/Http/Controllers/Api/H5ReservationController.php @@ -51,7 +51,7 @@ class H5ReservationController extends Controller 'reservation_notice' => $activity->reservation_notice, 'start_at' => optional($activity->start_at)?->toIso8601String(), 'end_at' => optional($activity->end_at)?->toIso8601String(), - 'schedule_status' => Activity::computeScheduleStatusFromBounds($activity->start_at, $activity->end_at), + 'schedule_status' => Activity::computeScheduleStatusForDisplay($activity), 'venue' => $activity->venue, 'reservation_type' => $activity->reservation_type, ], @@ -109,7 +109,7 @@ class H5ReservationController extends Controller 'reservation_notice' => $activity->reservation_notice, 'start_at' => optional($activity->start_at)?->toIso8601String(), 'end_at' => optional($activity->end_at)?->toIso8601String(), - 'schedule_status' => Activity::computeScheduleStatusFromBounds($activity->start_at, $activity->end_at), + 'schedule_status' => Activity::computeScheduleStatusForDisplay($activity), 'venue' => $activity->venue, ], 'days' => $days, diff --git a/app/Models/Activity.php b/app/Models/Activity.php index 7a25dfb..cf67ecd 100644 --- a/app/Models/Activity.php +++ b/app/Models/Activity.php @@ -107,7 +107,7 @@ class Activity extends Model protected static function booted(): void { static::saving(function (Activity $activity) { - if (self::computeScheduleStatusFromBounds($activity->start_at, $activity->end_at) === 'ended') { + if (self::computeScheduleStatusForDisplay($activity) === 'ended') { $activity->is_hot = false; } }); @@ -120,11 +120,33 @@ class Activity extends Model { $tz = (string) config('app.timezone'); $today = Carbon::now($tz)->toDateString(); + $now = Carbon::now($tz); + $sessionDaySql = 'ad.activity_id = activities.id AND ad.session_start_at IS NOT NULL AND ad.session_end_at IS NOT NULL AND ad.booking_deadline_at IS NOT NULL'; return (int) static::query() ->where('is_hot', true) - ->whereNotNull('end_at') - ->whereDate('end_at', '<', $today) + ->where(function (Builder $q) use ($today, $now, $sessionDaySql) { + $q->where(function (Builder $sess) use ($now, $sessionDaySql) { + $sess->where(function (Builder $t) { + $t->whereNull('reservation_type') + ->orWhere('reservation_type', '') + ->orWhere('reservation_type', self::RESERVATION_TYPE_ONLINE); + })->whereRaw( + '(SELECT MAX(ad.session_end_at) FROM activity_days ad WHERE '.$sessionDaySql.') < ?', + [$now] + ); + })->orWhere(function (Builder $cls) use ($today, $sessionDaySql) { + $cls->where(function (Builder $x) use ($sessionDaySql) { + $x->where(function (Builder $off) { + $off->whereNotNull('reservation_type') + ->where('reservation_type', '!=', '') + ->where('reservation_type', '!=', self::RESERVATION_TYPE_ONLINE); + })->orWhereRaw( + '(SELECT COUNT(*) FROM activity_days ad WHERE '.$sessionDaySql.') = 0' + ); + })->whereNotNull('end_at')->whereDate('end_at', '<', $today); + }); + }) ->update(['is_hot' => false]); } @@ -257,20 +279,127 @@ class Activity extends Model return 'ongoing'; } - /** 列表筛选:按日期实时计算,不依赖 schedule_status 字段的缓存值。 */ + /** + * 平台内「需要报名」(online / 空类型)且存在场次模式活动日时,用各场 session 起止时间的 min/max + * 判断未开始 / 进行中 / 已结束;否则与 {@see computeScheduleStatusFromBounds} 一致(活动级日历日)。 + */ + public static function computeScheduleStatusForDisplay(Activity $activity): string + { + if (! self::isOnlineReservationType((string) ($activity->reservation_type ?? self::RESERVATION_TYPE_ONLINE))) { + return self::computeScheduleStatusFromBounds($activity->start_at, $activity->end_at); + } + + $days = $activity->relationLoaded('activityDays') + ? $activity->activityDays + : $activity->activityDays()->get(); + + $sessionDays = $days->filter(fn (ActivityDay $d) => $d->isSessionMode()); + if ($sessionDays->isEmpty()) { + return self::computeScheduleStatusFromBounds($activity->start_at, $activity->end_at); + } + + $tz = (string) config('app.timezone'); + $now = Carbon::now($tz); + $minStart = $sessionDays->min('session_start_at'); + $maxEnd = $sessionDays->max('session_end_at'); + + if (! $minStart instanceof Carbon || ! $maxEnd instanceof Carbon) { + return self::computeScheduleStatusFromBounds($activity->start_at, $activity->end_at); + } + + $minStart = $minStart->copy()->timezone($tz); + $maxEnd = $maxEnd->copy()->timezone($tz); + + if ($now->lt($minStart)) { + return 'not_started'; + } + if ($now->gt($maxEnd)) { + return 'ended'; + } + + return 'ongoing'; + } + + public static function isOnlineReservationType(string $reservationType): bool + { + $t = trim($reservationType); + + return $t === '' || $t === self::RESERVATION_TYPE_ONLINE; + } + + /** 列表筛选:实时计算,与 {@see computeScheduleStatusForDisplay} 一致(含场次结束时间)。 */ public function scopeWhereComputedScheduleStatus(Builder $query, string $status): Builder { $tz = (string) config('app.timezone'); $today = Carbon::now($tz)->toDateString(); + $now = Carbon::now($tz); + + $sessionDaySql = 'ad.activity_id = activities.id AND ad.session_start_at IS NOT NULL AND ad.session_end_at IS NOT NULL AND ad.booking_deadline_at IS NOT NULL'; + + $onlineReservation = function (Builder $q): void { + $q->where(function (Builder $t) { + $t->whereNull('reservation_type') + ->orWhere('reservation_type', '') + ->orWhere('reservation_type', self::RESERVATION_TYPE_ONLINE); + }); + }; + + $offlineOrNoSessionDays = function (Builder $q) use ($sessionDaySql): void { + $q->where(function (Builder $x) use ($sessionDaySql) { + $x->where(function (Builder $off) { + $off->whereNotNull('reservation_type') + ->where('reservation_type', '!=', '') + ->where('reservation_type', '!=', self::RESERVATION_TYPE_ONLINE); + })->orWhereRaw( + '(SELECT COUNT(*) FROM activity_days ad WHERE '.$sessionDaySql.') = 0' + ); + }); + }; return match ($status) { - 'not_started' => $query->whereNotNull('start_at')->whereDate('start_at', '>', $today), - 'ended' => $query->whereNotNull('end_at')->whereDate('end_at', '<', $today), - 'ongoing' => $query->where(function (Builder $q) use ($today) { - $q->where(function (Builder $q2) use ($today) { - $q2->whereNull('start_at')->orWhereDate('start_at', '<=', $today); - })->where(function (Builder $q2) use ($today) { - $q2->whereNull('end_at')->orWhereDate('end_at', '>=', $today); + 'not_started' => $query->where(function (Builder $q) use ($onlineReservation, $offlineOrNoSessionDays, $today, $now, $sessionDaySql) { + $q->where(function (Builder $sess) use ($onlineReservation, $now, $sessionDaySql) { + $onlineReservation($sess); + $sess->whereRaw( + '(SELECT MIN(ad.session_start_at) FROM activity_days ad WHERE '.$sessionDaySql.') > ?', + [$now] + ); + })->orWhere(function (Builder $cls) use ($offlineOrNoSessionDays, $today) { + $offlineOrNoSessionDays($cls); + $cls->whereNotNull('start_at')->whereDate('start_at', '>', $today); + }); + }), + 'ended' => $query->where(function (Builder $q) use ($onlineReservation, $offlineOrNoSessionDays, $today, $now, $sessionDaySql) { + $q->where(function (Builder $sess) use ($onlineReservation, $now, $sessionDaySql) { + $onlineReservation($sess); + $sess->whereRaw( + '(SELECT MAX(ad.session_end_at) FROM activity_days ad WHERE '.$sessionDaySql.') < ?', + [$now] + ); + })->orWhere(function (Builder $cls) use ($offlineOrNoSessionDays, $today) { + $offlineOrNoSessionDays($cls); + $cls->whereNotNull('end_at')->whereDate('end_at', '<', $today); + }); + }), + 'ongoing' => $query->where(function (Builder $q) use ($onlineReservation, $offlineOrNoSessionDays, $today, $now, $sessionDaySql) { + $q->where(function (Builder $sess) use ($onlineReservation, $now, $sessionDaySql) { + $onlineReservation($sess); + $sess->whereRaw('(SELECT COUNT(*) FROM activity_days ad WHERE '.$sessionDaySql.') > 0') + ->whereRaw( + '(SELECT MIN(ad.session_start_at) FROM activity_days ad WHERE '.$sessionDaySql.') <= ?', + [$now] + ) + ->whereRaw( + '(SELECT MAX(ad.session_end_at) FROM activity_days ad WHERE '.$sessionDaySql.') >= ?', + [$now] + ); + })->orWhere(function (Builder $cls) use ($offlineOrNoSessionDays, $today) { + $offlineOrNoSessionDays($cls); + $cls->where(function (Builder $c) use ($today) { + $c->whereNull('start_at')->orWhereDate('start_at', '<=', $today); + })->where(function (Builder $c) use ($today) { + $c->whereNull('end_at')->orWhereDate('end_at', '>=', $today); + }); }); }), default => $query,