|
|
<?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;
|
|
|
}
|
|
|
}
|