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

1 week ago
<?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;
}
}