|
|
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
|
|
namespace App\Models;
|
|
|
|
|
|
|
|
|
|
|
|
use Carbon\Carbon;
|
|
|
|
|
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
|
|
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
|
|
|
|
use Illuminate\Database\Eloquent\Model;
|
|
|
|
|
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
|
|
|
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
|
|
|
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
|
|
|
|
|
|
|
|
|
|
|
class Activity extends Model
|
|
|
|
|
|
{
|
|
|
|
|
|
use HasFactory, SoftDeletes;
|
|
|
|
|
|
|
|
|
|
|
|
protected $fillable = [
|
|
|
|
|
|
'venue_id',
|
|
|
|
|
|
'title',
|
|
|
|
|
|
'summary',
|
|
|
|
|
|
'category',
|
|
|
|
|
|
'quota',
|
|
|
|
|
|
'registered_count',
|
|
|
|
|
|
'start_at',
|
|
|
|
|
|
'end_at',
|
|
|
|
|
|
'address',
|
|
|
|
|
|
'contact_phone',
|
|
|
|
|
|
'lat',
|
|
|
|
|
|
'lng',
|
|
|
|
|
|
'detail_html',
|
|
|
|
|
|
'cover_image',
|
|
|
|
|
|
'gallery_media',
|
|
|
|
|
|
'tags',
|
|
|
|
|
|
'reservation_notice',
|
|
|
|
|
|
'open_time',
|
|
|
|
|
|
'sort',
|
|
|
|
|
|
'is_active',
|
|
|
|
|
|
'schedule_status',
|
|
|
|
|
|
'booking_audience',
|
|
|
|
|
|
'total_quota',
|
|
|
|
|
|
'min_people_per_order',
|
|
|
|
|
|
'max_people_per_order',
|
|
|
|
|
|
'audit_status',
|
|
|
|
|
|
'audit_remark',
|
|
|
|
|
|
'last_approved_snapshot',
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
public const AUDIT_APPROVED = 'approved';
|
|
|
|
|
|
|
|
|
|
|
|
public const AUDIT_PENDING = 'pending';
|
|
|
|
|
|
|
|
|
|
|
|
public const AUDIT_REJECTED = 'rejected';
|
|
|
|
|
|
|
|
|
|
|
|
protected $casts = [
|
|
|
|
|
|
'start_at' => 'datetime',
|
|
|
|
|
|
'end_at' => 'datetime',
|
|
|
|
|
|
'lat' => 'float',
|
|
|
|
|
|
'lng' => 'float',
|
|
|
|
|
|
'gallery_media' => 'array',
|
|
|
|
|
|
'tags' => 'array',
|
|
|
|
|
|
'sort' => 'integer',
|
|
|
|
|
|
'is_active' => 'boolean',
|
|
|
|
|
|
'total_quota' => 'integer',
|
|
|
|
|
|
'min_people_per_order' => 'integer',
|
|
|
|
|
|
'max_people_per_order' => 'integer',
|
|
|
|
|
|
'last_approved_snapshot' => 'array',
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
/** 活动待审期间若用快照会与 activity_days 不一致,故前台仅展示已审核通过的记录。 */
|
|
|
|
|
|
public function scopeVisibleOnH5(Builder $query): Builder
|
|
|
|
|
|
{
|
|
|
|
|
|
return $query->where('is_active', true)->where('audit_status', self::AUDIT_APPROVED);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public function venue(): BelongsTo
|
|
|
|
|
|
{
|
|
|
|
|
|
return $this->belongsTo(Venue::class);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public function reservations(): HasMany
|
|
|
|
|
|
{
|
|
|
|
|
|
return $this->hasMany(Reservation::class);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public function activityDays(): HasMany
|
|
|
|
|
|
{
|
|
|
|
|
|
return $this->hasMany(ActivityDay::class)->orderBy('activity_date');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* H5 活动列表/热门:未结束优先(按开始日期升序,空开始日置后);已结束置底(按结束日期降序)。
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function scopeOrderForH5Listing(Builder $query): Builder
|
|
|
|
|
|
{
|
|
|
|
|
|
$today = now()->startOfDay();
|
|
|
|
|
|
|
|
|
|
|
|
return $query
|
|
|
|
|
|
->orderByRaw('CASE WHEN end_at IS NOT NULL AND end_at < ? THEN 1 ELSE 0 END ASC', [$today])
|
|
|
|
|
|
->orderByRaw('CASE WHEN end_at IS NOT NULL AND end_at < ? THEN end_at END DESC', [$today])
|
|
|
|
|
|
->orderByRaw('CASE WHEN (end_at IS NULL OR end_at >= ?) AND start_at IS NULL THEN 1 ELSE 0 END ASC', [$today])
|
|
|
|
|
|
->orderByRaw('CASE WHEN end_at IS NULL OR end_at >= ? THEN start_at END ASC', [$today])
|
|
|
|
|
|
->orderByDesc('id');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 按活动开始/结束日(应用时区下的日历日)计算「未开始 / 进行中 / 已结束」,与后台列表筛选一致。
|
|
|
|
|
|
*/
|
|
|
|
|
|
public static function computeScheduleStatusFromBounds(?Carbon $start, ?Carbon $end, ?string $tz = null): string
|
|
|
|
|
|
{
|
|
|
|
|
|
$tz = $tz ?? (string) config('app.timezone');
|
|
|
|
|
|
$today = Carbon::now($tz)->toDateString();
|
|
|
|
|
|
$startD = $start ? $start->copy()->timezone($tz)->toDateString() : null;
|
|
|
|
|
|
$endD = $end ? $end->copy()->timezone($tz)->toDateString() : null;
|
|
|
|
|
|
|
|
|
|
|
|
if (! $startD && ! $endD) {
|
|
|
|
|
|
return 'ongoing';
|
|
|
|
|
|
}
|
|
|
|
|
|
if ($startD && ! $endD) {
|
|
|
|
|
|
return $today < $startD ? 'not_started' : 'ongoing';
|
|
|
|
|
|
}
|
|
|
|
|
|
if (! $startD && $endD) {
|
|
|
|
|
|
return $today > $endD ? 'ended' : 'ongoing';
|
|
|
|
|
|
}
|
|
|
|
|
|
if ($today < $startD) {
|
|
|
|
|
|
return 'not_started';
|
|
|
|
|
|
}
|
|
|
|
|
|
if ($today > $endD) {
|
|
|
|
|
|
return 'ended';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return 'ongoing';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 列表筛选:按日期实时计算,不依赖 schedule_status 字段的缓存值。 */
|
|
|
|
|
|
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,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 按未取消预约的票数合计,回写 activities.registered_count(展示用「已预约总人数」)。
|
|
|
|
|
|
*/
|
|
|
|
|
|
public static function refreshRegisteredCountFromReservations(int $activityId): void
|
|
|
|
|
|
{
|
|
|
|
|
|
$total = (int) Reservation::query()
|
|
|
|
|
|
->where('activity_id', $activityId)
|
|
|
|
|
|
->where('status', '!=', 'cancelled')
|
|
|
|
|
|
->get()
|
|
|
|
|
|
->sum(fn (Reservation $r) => max(1, (int) ($r->ticket_count ?? 1)));
|
|
|
|
|
|
|
|
|
|
|
|
static::query()->where('id', $activityId)->update(['registered_count' => $total]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|