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.

221 lines
8.1 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 Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ActivityDay extends Model
{
protected $fillable = [
'activity_id',
'session_name',
'activity_date',
'day_quota',
'booked_count',
'opens_at',
'closes_at',
'session_start_at',
'session_end_at',
'booking_deadline_at',
];
protected $casts = [
'activity_date' => 'date',
'opens_at' => 'datetime',
'closes_at' => 'datetime',
'session_start_at' => 'datetime',
'session_end_at' => 'datetime',
'booking_deadline_at' => 'datetime',
'day_quota' => 'integer',
'booked_count' => 'integer',
];
/** 管理端/接口序列化时附带,与 H5 中 `time_range_text` 一致 */
protected $appends = ['time_range_text'];
public function getTimeRangeTextAttribute(): string
{
return $this->formatSessionTimeRangeZh();
}
protected static function booted(): void
{
static::saving(function (ActivityDay $day) {
if ($day->session_start_at) {
$tz = (string) config('app.timezone');
$day->activity_date = $day->session_start_at->copy()->timezone($tz)->format('Y-m-d');
}
});
}
public function activity(): BelongsTo
{
return $this->belongsTo(Activity::class);
}
/**
* 场次时间段展示同日则「Y年m月d日 H:i-H:i」。
*/
public function formatSessionTimeRangeZh(): string
{
if (!$this->session_start_at || !$this->session_end_at) {
return $this->activity_date?->format('Y年m月d日') ?? '';
}
$tz = (string) config('app.timezone');
$s = $this->session_start_at->copy()->timezone($tz);
$e = $this->session_end_at->copy()->timezone($tz);
if ($s->toDateString() === $e->toDateString()) {
return $s->format('Y年m月d日 H:i') . '-' . $e->format('H:i');
}
return $s->format('Y年m月d日 H:i') . ' ~ ' . $e->format('Y年m月d日 H:i');
}
public function isSessionMode(): bool
{
return $this->session_start_at !== null
&& $this->session_end_at !== null
&& $this->booking_deadline_at !== null;
}
/**
* 当前时刻是否可预约:截止前、有余量、未重复预约等由业务层与 H5 数组字段共同约束。
* 这里仅表示「未过预约截止且有余量」legacy 为原 opens/closes 窗口逻辑)。
*/
public function isCurrentlyBookable(): bool
{
$available = max(0, (int) $this->day_quota - (int) $this->booked_count);
if ($available <= 0) {
return false;
}
if ($this->isSessionMode()) {
return now()->lte($this->booking_deadline_at);
}
$closesAt = $this->closes_at ?: $this->opens_at?->copy()->endOfDay();
$now = now();
if (!$this->opens_at || !$closesAt) {
return false;
}
$isOpenWindow = $this->opens_at->lte($now) && $closesAt->gte($now);
return $isOpenWindow;
}
/**
* H5 预约页/活动详情:单场次展示与可约态。
*
* @return array<string, mixed>
*/
public function toH5BookingDayArray(bool $alreadyReserved = false): array
{
$available = max(0, (int) $this->day_quota - (int) $this->booked_count);
$now = now();
$enrolledCountText = '已报名数:' . (int) $this->booked_count;
$bookedPeopleText = '已约' . (int) $this->booked_count . '人';
if ($this->isSessionMode()) {
$deadline = $this->booking_deadline_at;
$unavailableReason = null;
$isBookable = false;
if ($alreadyReserved) {
$unavailableReason = 'already_reserved';
$actionHeadline = '已预约';
$statusText = '已约';
$statusKind = 'reserved';
} elseif (! $deadline) {
$unavailableReason = 'closed';
$actionHeadline = '预约已结束';
$statusText = '预约已结束';
$statusKind = 'closed';
} elseif ($now->gt($deadline)) {
$unavailableReason = 'deadline_passed';
$actionHeadline = '预约已结束';
$statusText = '已约' . (int) $this->booked_count . '人';
$statusKind = 'booked_info';
} elseif ($available <= 0) {
$unavailableReason = 'sold_out';
$actionHeadline = '预约已满';
$statusText = '预约已满';
$statusKind = 'full';
} else {
$isBookable = true;
$unavailableReason = null;
$actionHeadline = '立即预约';
$statusText = '已约' . (int) $this->booked_count . '人';
$statusKind = 'open';
}
$timeDisplay = $this->formatSessionTimeRangeZh();
} else {
// legacy无场次字段时沿用原预约窗口
$timeDisplay = $this->activity_date?->format('Y年m月d日') ?? '';
if ($alreadyReserved) {
$unavailableReason = 'already_reserved';
$actionHeadline = '已预约';
$statusText = '已约';
$statusKind = 'reserved';
} elseif ($available <= 0) {
$unavailableReason = 'sold_out';
$actionHeadline = '预约已满';
$statusText = '预约已满';
$statusKind = 'full';
} else {
$closesAt = $this->closes_at ?: $this->opens_at?->copy()->endOfDay();
if ($this->opens_at && $this->opens_at->gt($now)) {
$unavailableReason = 'not_started';
$actionHeadline = '未开放';
$statusText = '未开放';
$statusKind = 'closed';
} elseif ($closesAt && $closesAt->lt($now)) {
$unavailableReason = 'closed';
$actionHeadline = '预约已结束';
$statusText = '不可预约';
$statusKind = 'closed';
} else {
$unavailableReason = null;
$actionHeadline = '立即预约';
$statusText = '已约' . (int) $this->booked_count . '人';
$statusKind = 'open';
}
}
$isBookable = $this->isCurrentlyBookable() && ! $alreadyReserved;
}
if ($alreadyReserved) {
$isBookable = false;
}
$closesAtLegacy = $this->closes_at ?: $this->opens_at?->copy()->endOfDay();
$isOpenWindow = $this->opens_at && $closesAtLegacy
? ($this->opens_at->lte($now) && $closesAtLegacy->gte($now))
: false;
return [
'id' => $this->id,
'session_name' => (string) ($this->session_name ?? ''),
'session_start_at' => $this->session_start_at?->format('Y-m-d H:i:s'),
'session_end_at' => $this->session_end_at?->format('Y-m-d H:i:s'),
'booking_deadline_at' => $this->booking_deadline_at?->format('Y-m-d H:i:s'),
'time_display' => $timeDisplay,
'activity_date' => $this->activity_date?->format('Y-m-d'),
'opens_at' => $this->opens_at?->format('Y-m-d H:i:s'),
'closes_at' => $closesAtLegacy?->format('Y-m-d H:i:s'),
'day_quota' => (int) $this->day_quota,
'booked_count' => (int) $this->booked_count,
'available_count' => $available,
'is_open' => $isOpenWindow,
'is_bookable' => $isBookable,
'unavailable_reason' => $unavailableReason,
'status_text' => $statusText,
'status_kind' => $statusKind,
'already_reserved' => $alreadyReserved,
'action_headline' => $actionHeadline,
'enrolled_count_text' => $enrolledCountText,
'booked_people_text' => $bookedPeopleText,
];
}
}