108 lines
3.6 KiB
PHP
108 lines
3.6 KiB
PHP
<?php
|
||
|
||
namespace App\Services\Jackpot;
|
||
|
||
use App\Models\Draw;
|
||
use App\Models\TicketItem;
|
||
use App\Lottery\DrawStatus;
|
||
use App\Models\JackpotPool;
|
||
use App\Models\JackpotPayoutLog;
|
||
use Illuminate\Support\Collection;
|
||
|
||
/**
|
||
* 产品文档 §5.11.2–5.11.3:中头奖且满足阈值或连续未爆期数 → 按比例释放奖池,按注项 `total_bet_amount` 比例分配。
|
||
*/
|
||
final class JackpotBurstAllocator
|
||
{
|
||
/**
|
||
* @param Collection<int, array{item: TicketItem, matched_tier: ?string, gross_win: int}> $results
|
||
* @return array{allocations: array<int, int>, pool_payout: int, trigger: ?string}
|
||
*/
|
||
public function allocate(Draw $draw, JackpotPool $pool, Collection $results): array
|
||
{
|
||
$winners = $results->filter(
|
||
fn (array $r) => ($r['matched_tier'] ?? null) === 'first' && (int) $r['gross_win'] > 0,
|
||
);
|
||
|
||
if ($winners->isEmpty()) {
|
||
return ['allocations' => [], 'pool_payout' => 0, 'trigger' => null];
|
||
}
|
||
|
||
$thresholdOk = (int) $pool->current_amount >= (int) $pool->trigger_threshold;
|
||
$gapOk = $this->gapTriggerMet($pool);
|
||
if (! $thresholdOk && ! $gapOk) {
|
||
return ['allocations' => [], 'pool_payout' => 0, 'trigger' => null];
|
||
}
|
||
|
||
$trigger = $thresholdOk ? 'threshold' : 'forced_gap';
|
||
|
||
$poolBefore = (int) $pool->current_amount;
|
||
$poolPayout = (int) floor($poolBefore * (float) $pool->payout_rate);
|
||
if ($poolPayout <= 0) {
|
||
return ['allocations' => [], 'pool_payout' => 0, 'trigger' => null];
|
||
}
|
||
|
||
$list = $winners->values()->all();
|
||
$weightTotal = 0;
|
||
foreach ($list as $r) {
|
||
$weightTotal += (int) $r['item']->total_bet_amount;
|
||
}
|
||
if ($weightTotal <= 0) {
|
||
return ['allocations' => [], 'pool_payout' => 0, 'trigger' => null];
|
||
}
|
||
|
||
$allocations = [];
|
||
$remaining = $poolPayout;
|
||
$n = count($list);
|
||
foreach ($list as $idx => $r) {
|
||
/** @var TicketItem $item */
|
||
$item = $r['item'];
|
||
$w = (int) $item->total_bet_amount;
|
||
if ($idx === $n - 1) {
|
||
$share = max(0, $remaining);
|
||
} else {
|
||
$share = (int) floor($poolPayout * $w / $weightTotal);
|
||
$remaining -= $share;
|
||
}
|
||
$allocations[(int) $item->id] = $share;
|
||
}
|
||
|
||
$pool->forceFill([
|
||
'current_amount' => max(0, $poolBefore - $poolPayout),
|
||
'last_trigger_draw_id' => $draw->id,
|
||
])->save();
|
||
|
||
JackpotPayoutLog::query()->create([
|
||
'draw_id' => $draw->id,
|
||
'jackpot_pool_id' => $pool->id,
|
||
'trigger_type' => $trigger,
|
||
'total_payout_amount' => $poolPayout,
|
||
'winner_count' => count($allocations),
|
||
'trigger_snapshot_json' => [
|
||
'threshold_ok' => $thresholdOk,
|
||
'gap_ok' => $gapOk,
|
||
'pool_amount_before' => $poolBefore,
|
||
'payout_rate' => (string) $pool->payout_rate,
|
||
],
|
||
]);
|
||
|
||
return ['allocations' => $allocations, 'pool_payout' => $poolPayout, 'trigger' => $trigger];
|
||
}
|
||
|
||
private function gapTriggerMet(JackpotPool $pool): bool
|
||
{
|
||
$gap = (int) $pool->force_trigger_draw_gap;
|
||
if ($gap <= 0) {
|
||
return false;
|
||
}
|
||
|
||
$lastId = (int) ($pool->last_trigger_draw_id ?? 0);
|
||
$count = Draw::query()
|
||
->where('status', DrawStatus::Settled->value)
|
||
->when($lastId > 0, fn ($q) => $q->where('id', '>', $lastId))
|
||
->count();
|
||
|
||
return $count >= $gap;
|
||
}
|
||
}
|