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.
129 lines
4.3 KiB
129 lines
4.3 KiB
|
20 hours ago
|
<?php
|
||
|
|
|
||
|
|
namespace App\Services;
|
||
|
|
|
||
|
|
use App\Models\PhoneBookingBan;
|
||
|
|
use App\Models\Reservation;
|
||
|
|
use Carbon\Carbon;
|
||
|
|
use Illuminate\Support\Collection;
|
||
|
|
|
||
|
|
class NoShowBlacklistService
|
||
|
|
{
|
||
|
|
/**
|
||
|
|
* 扫描「活动日已过、仍为待核销、从未核销」的预约,累计满 3 次则写入全平台禁约。
|
||
|
|
* 禁约截止 = 第 3 条对应活动的「活动开始日」+ 3 个月(应用时区日历)。
|
||
|
|
* 若该手机号已有未过期禁约,则跳过;若曾禁约已过期,只统计禁约期满日之后的未核销场次。
|
||
|
|
*
|
||
|
|
* @return int 本次新写入或更新的禁约条数
|
||
|
|
*/
|
||
|
|
public function syncAll(): int
|
||
|
|
{
|
||
|
|
$tz = (string) config('app.timezone');
|
||
|
|
$today = Carbon::now($tz)->toDateString();
|
||
|
|
|
||
|
|
$phones = Reservation::query()
|
||
|
|
->join('activity_days', 'activity_days.id', '=', 'reservations.activity_day_id')
|
||
|
|
->where('reservations.status', 'pending')
|
||
|
|
->whereNull('reservations.verified_at')
|
||
|
|
->whereNotNull('reservations.activity_day_id')
|
||
|
|
->whereDate('activity_days.activity_date', '<', $today)
|
||
|
|
->distinct()
|
||
|
|
->pluck('reservations.visitor_phone');
|
||
|
|
|
||
|
|
$applied = 0;
|
||
|
|
foreach ($phones as $phone) {
|
||
|
|
if (PhoneBookingBan::isGloballyBlocked((string) $phone)) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
if ($this->applyBanIfNeeded((string) $phone, $tz, $today)) {
|
||
|
|
$applied++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return $applied;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 预约前对单号同步一次,避免仅依赖定时任务产生空窗期。 */
|
||
|
|
public function syncForPhone(string $phone): bool
|
||
|
|
{
|
||
|
|
$tz = (string) config('app.timezone');
|
||
|
|
$today = Carbon::now($tz)->toDateString();
|
||
|
|
if (PhoneBookingBan::isGloballyBlocked($phone)) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
return $this->applyBanIfNeeded($phone, $tz, $today);
|
||
|
|
}
|
||
|
|
|
||
|
|
private function applyBanIfNeeded(string $phone, string $tz, string $today): bool
|
||
|
|
{
|
||
|
|
$cutoff = $this->noShowCutoffDateString($phone);
|
||
|
|
|
||
|
|
/** @var Collection<int, Reservation> $rows */
|
||
|
|
$rows = Reservation::query()
|
||
|
|
->join('activity_days', 'activity_days.id', '=', 'reservations.activity_day_id')
|
||
|
|
->join('activities', 'activities.id', '=', 'reservations.activity_id')
|
||
|
|
->where('reservations.visitor_phone', $phone)
|
||
|
|
->where('reservations.status', 'pending')
|
||
|
|
->whereNull('reservations.verified_at')
|
||
|
|
->whereNotNull('reservations.activity_day_id')
|
||
|
|
->whereDate('activity_days.activity_date', '<', $today)
|
||
|
|
->when($cutoff, fn ($q) => $q->whereDate('activity_days.activity_date', '>=', $cutoff))
|
||
|
|
->orderByRaw('activities.start_at IS NULL ASC')
|
||
|
|
->orderBy('activities.start_at')
|
||
|
|
->orderBy('activity_days.activity_date')
|
||
|
|
->orderBy('reservations.id')
|
||
|
|
->select('reservations.*')
|
||
|
|
->get();
|
||
|
|
|
||
|
|
if ($rows->count() < 3) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
$third = $rows->values()->get(2);
|
||
|
|
$third->loadMissing(['activity', 'activityDay']);
|
||
|
|
$activity = $third->activity;
|
||
|
|
$day = $third->activityDay;
|
||
|
|
if (! $activity || ! $day) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
$anchor = $activity->start_at
|
||
|
|
? $activity->start_at->copy()->timezone($tz)->startOfDay()
|
||
|
|
: Carbon::parse($day->activity_date->format('Y-m-d'), $tz)->startOfDay();
|
||
|
|
|
||
|
|
$banUntil = $anchor->copy()->addMonths(3)->startOfDay();
|
||
|
|
|
||
|
|
if ($banUntil->lte(now())) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
PhoneBookingBan::query()->updateOrCreate(
|
||
|
|
['visitor_phone' => $phone],
|
||
|
|
[
|
||
|
|
'ban_until' => $banUntil,
|
||
|
|
'reason' => '累计3次预约成功后未核销',
|
||
|
|
],
|
||
|
|
);
|
||
|
|
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 禁约期满后,新统计的「未核销」场次须从该日期(含)起算。
|
||
|
|
*/
|
||
|
|
private function noShowCutoffDateString(string $phone): ?string
|
||
|
|
{
|
||
|
|
$max = PhoneBookingBan::query()
|
||
|
|
->where('visitor_phone', $phone)
|
||
|
|
->where('ban_until', '<=', now())
|
||
|
|
->max('ban_until');
|
||
|
|
|
||
|
|
if (! $max) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
return Carbon::parse($max)->timezone(config('app.timezone'))->toDateString();
|
||
|
|
}
|
||
|
|
}
|