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.

233 lines
7.7 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?php
namespace App\Models;
use App\Services\TicketGrabReleaseDayService;
use App\Support\CalendarDateFormat;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class TicketGrabEvent extends Model
{
use HasFactory, SoftDeletes;
public const AUDIT_APPROVED = 'approved';
public const AUDIT_PENDING = 'pending';
public const AUDIT_REJECTED = 'rejected';
public const AUDIENCE_ALL = 'all';
public const AUDIENCE_SCHOOL_AGE = 'school_age';
protected $fillable = [
'title',
'summary',
'start_at',
'end_at',
'booking_start_at',
'booking_end_at',
'daily_release_start_time',
'daily_release_end_time',
'total_quota',
'registered_count',
'booking_audience',
'age_limit_start',
'age_limit_end',
'address',
'detail_html',
'reservation_notice',
'cover_image',
'gallery_media',
'tags',
'sort',
'is_active',
'schedule_status',
'audit_status',
'audit_remark',
'last_approved_snapshot',
];
protected $casts = [
'start_at' => 'datetime',
'end_at' => 'datetime',
'booking_start_at' => 'date',
'booking_end_at' => 'date',
'age_limit_start' => 'date',
'age_limit_end' => 'date',
'gallery_media' => 'array',
'tags' => 'array',
'sort' => 'integer',
'is_active' => 'boolean',
'total_quota' => 'integer',
'registered_count' => 'integer',
'last_approved_snapshot' => 'array',
];
public function scopeVisibleOnH5(Builder $query): Builder
{
return $query->where('is_active', true)->where('audit_status', self::AUDIT_APPROVED);
}
public function venues(): BelongsToMany
{
return $this->belongsToMany(Venue::class, 'ticket_grab_event_venue')
->withPivot('venue_total_quota', 'id')
->withTimestamps();
}
public function eventVenuePivots(): HasMany
{
return $this->hasMany(TicketGrabEventVenue::class, 'ticket_grab_event_id');
}
public function releaseDays(): HasMany
{
return $this->hasMany(TicketGrabVenueReleaseDay::class, 'ticket_grab_event_id');
}
public function reservations(): HasMany
{
return $this->hasMany(Reservation::class, 'ticket_grab_event_id');
}
public function scopeOrderByScheduleStatusPriority(Builder $query): Builder
{
$today = Carbon::now((string) config('app.timezone'))->toDateString();
return $query
->orderByRaw(
'CASE
WHEN end_at IS NOT NULL AND DATE(end_at) < ? THEN 2
WHEN start_at IS NOT NULL AND DATE(start_at) > ? THEN 1
ELSE 0
END ASC',
[$today, $today]
)
->orderByRaw('start_at IS NULL ASC')
->orderBy('start_at', 'asc')
->orderByDesc('id');
}
public function scopeWhereComputedScheduleStatus(Builder $query, string $status): Builder
{
$tz = (string) config('app.timezone');
$today = Carbon::now($tz)->toDateString();
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);
});
}),
default => $query,
};
}
public static function computeScheduleStatusFromBounds(?Carbon $start, ?Carbon $end, ?string $tz = null): string
{
return Activity::computeScheduleStatusFromBounds($start, $end, $tz);
}
public static function refreshRegisteredCountFromReservations(int $eventId): void
{
$total = (int) Reservation::query()
->where('ticket_grab_event_id', $eventId)
->where('reservation_kind', 'ticket_grab')
->where('status', '!=', 'cancelled')
->get()
->sum(fn (Reservation $r) => max(1, (int) ($r->ticket_count ?? 1)));
static::query()->where('id', $eventId)->update(['registered_count' => $total]);
}
/**
* 与 H5 场馆页 bookingInfo 的 can_book_now 一致:在预约日窗口内、活动未结束、
* 处于每日放票时段内,且任一场馆「当日」放票行经滚入同步后仍有余量。
*/
public function canGrabToday(): bool
{
$tz = (string) config('app.timezone');
$today = Carbon::now($tz)->toDateString();
if (! $this->booking_start_at || ! $this->booking_end_at) {
return false;
}
if ($today < $this->booking_start_at->toDateString() || $today > $this->booking_end_at->toDateString()) {
return false;
}
if ($this->end_at && $this->end_at->toDateString() < $today) {
return false;
}
if (! self::isDailyReleaseTimeWindowOpen($this)) {
return false;
}
TicketGrabReleaseDayService::syncCarryInChain($this->id);
$venueIds = TicketGrabEventVenue::query()
->where('ticket_grab_event_id', $this->id)
->pluck('venue_id');
foreach ($venueIds as $vid) {
$r = TicketGrabVenueReleaseDay::query()
->where('ticket_grab_event_id', $this->id)
->where('venue_id', $vid)
->whereDate('release_date', $today)
->first();
if ($r && $r->remainingForDisplay() > 0) {
return true;
}
}
return false;
}
public static function isDailyReleaseTimeWindowOpen(self $e): bool
{
$start = (string) ($e->daily_release_start_time ?? '10:00');
$end = (string) ($e->daily_release_end_time ?? '23:59');
$now = now();
[$sh, $sm] = array_map('intval', explode(':', $start) + [0, 0]);
[$eh, $em] = array_map('intval', explode(':', $end) + [0, 0]);
$a = $now->copy()->setTime($sh, $sm, 0);
$b = $now->copy()->setTime($eh, $em, 59);
return $now->between($a, $b);
}
/**
* JSON 输出中活动起止、预约、年龄限制等均为 Y-m-d避免前端的 ISO+UTC 导致展示差一天。
*/
public function toArray()
{
$a = parent::toArray();
if ($this->start_at) {
$a['start_at'] = CalendarDateFormat::ymdFromDatetime($this->start_at);
}
if ($this->end_at) {
$a['end_at'] = CalendarDateFormat::ymdFromDatetime($this->end_at);
}
if ($this->booking_start_at) {
$a['booking_start_at'] = CalendarDateFormat::ymdFromDateValue($this->booking_start_at);
}
if ($this->booking_end_at) {
$a['booking_end_at'] = CalendarDateFormat::ymdFromDateValue($this->booking_end_at);
}
if ($this->age_limit_start) {
$a['age_limit_start'] = CalendarDateFormat::ymdFromDateValue($this->age_limit_start);
}
if ($this->age_limit_end) {
$a['age_limit_end'] = CalendarDateFormat::ymdFromDateValue($this->age_limit_end);
}
return $a;
}
}