- 将玩法相关的显示名称字段统一为 `display_name`,移除多语言字段。 - 在 `PlayTypePatchController` 中新增即时切换玩法开关的功能,并推送大厅更新。 - 优化多个控制器和服务中的权限检查与数据处理逻辑,提升代码可读性与维护性。
214 lines
6.9 KiB
PHP
214 lines
6.9 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);
|
||
$comboOk = $this->comboTriggerMet($pool, $winners);
|
||
if (! $thresholdOk && ! $gapOk && ! $comboOk) {
|
||
return ['allocations' => [], 'pool_payout' => 0, 'trigger' => null];
|
||
}
|
||
|
||
$trigger = $thresholdOk ? 'threshold' : ($gapOk ? 'forced_gap' : 'play_combo');
|
||
$releaseFullPool = $trigger === 'forced_gap';
|
||
|
||
$winnerItems = $winners->map(fn (array $r): TicketItem => $r['item'])->values();
|
||
|
||
return $this->burstToWinners(
|
||
$draw,
|
||
$pool,
|
||
$winnerItems,
|
||
$trigger,
|
||
$releaseFullPool,
|
||
[
|
||
'threshold_ok' => $thresholdOk,
|
||
'gap_ok' => $gapOk,
|
||
'combo_ok' => $comboOk,
|
||
'combo_trigger_play_codes' => $this->comboTriggerPlayCodes($pool),
|
||
],
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 超管手动爆池:跳过头奖触发条件校验,仍要求存在头奖中奖注单,并按配置派彩比例释放奖池。
|
||
*
|
||
* @param Collection<int, TicketItem> $winnerItems
|
||
* @return array{allocations: array<int, int>, pool_payout: int, trigger: string, log_id: int}
|
||
*/
|
||
public function burstManual(Draw $draw, JackpotPool $pool, Collection $winnerItems): array
|
||
{
|
||
if ($winnerItems->isEmpty()) {
|
||
throw new \RuntimeException('jackpot_manual_no_first_prize_winners');
|
||
}
|
||
|
||
$out = $this->burstToWinners($draw, $pool, $winnerItems, 'manual', false, ['manual' => true]);
|
||
|
||
return [
|
||
'allocations' => $out['allocations'],
|
||
'pool_payout' => $out['pool_payout'],
|
||
'trigger' => 'manual',
|
||
'log_id' => (int) ($out['log_id'] ?? 0),
|
||
];
|
||
}
|
||
|
||
/**
|
||
* @param Collection<int, TicketItem> $winnerItems
|
||
* @param array<string, mixed> $snapshotExtra
|
||
* @return array{allocations: array<int, int>, pool_payout: int, trigger: string, log_id: int}
|
||
*/
|
||
private function burstToWinners(
|
||
Draw $draw,
|
||
JackpotPool $pool,
|
||
Collection $winnerItems,
|
||
string $trigger,
|
||
bool $releaseFullPool,
|
||
array $snapshotExtra,
|
||
): array {
|
||
$poolBefore = (int) $pool->current_amount;
|
||
$poolPayout = $releaseFullPool
|
||
? $poolBefore
|
||
: (int) floor($poolBefore * (float) $pool->payout_rate);
|
||
|
||
if ($poolPayout <= 0) {
|
||
return ['allocations' => [], 'pool_payout' => 0, 'trigger' => $trigger, 'log_id' => 0];
|
||
}
|
||
|
||
$allocations = $this->distributeByBetWeight($winnerItems, $poolPayout);
|
||
if ($allocations === []) {
|
||
return ['allocations' => [], 'pool_payout' => 0, 'trigger' => $trigger, 'log_id' => 0];
|
||
}
|
||
|
||
$pool->forceFill([
|
||
'current_amount' => max(0, $poolBefore - $poolPayout),
|
||
'last_trigger_draw_id' => $draw->id,
|
||
])->save();
|
||
|
||
$log = 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' => array_merge($snapshotExtra, [
|
||
'pool_amount_before' => $poolBefore,
|
||
'payout_rate' => (string) $pool->payout_rate,
|
||
'release_full_pool' => $releaseFullPool,
|
||
]),
|
||
]);
|
||
|
||
return [
|
||
'allocations' => $allocations,
|
||
'pool_payout' => $poolPayout,
|
||
'trigger' => $trigger,
|
||
'log_id' => (int) $log->id,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* @param Collection<int, TicketItem> $winnerItems
|
||
* @return array<int, int>
|
||
*/
|
||
private function distributeByBetWeight(Collection $winnerItems, int $poolPayout): array
|
||
{
|
||
$list = $winnerItems->values()->all();
|
||
$weightTotal = 0;
|
||
foreach ($list as $item) {
|
||
$weightTotal += (int) $item->total_bet_amount;
|
||
}
|
||
if ($weightTotal <= 0) {
|
||
return [];
|
||
}
|
||
|
||
$allocations = [];
|
||
$remaining = $poolPayout;
|
||
$n = count($list);
|
||
foreach ($list as $idx => $item) {
|
||
$w = (int) $item->total_bet_amount;
|
||
if ($idx === $n - 1) {
|
||
$share = max(0, $remaining);
|
||
} else {
|
||
$share = (int) floor($poolPayout * $w / $weightTotal);
|
||
$remaining -= $share;
|
||
}
|
||
if ($share > 0) {
|
||
$allocations[(int) $item->id] = $share;
|
||
}
|
||
}
|
||
|
||
return $allocations;
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
/**
|
||
* @param Collection<int, array{item: TicketItem, matched_tier: ?string, gross_win: int}> $winners
|
||
*/
|
||
private function comboTriggerMet(JackpotPool $pool, Collection $winners): bool
|
||
{
|
||
$codes = $this->comboTriggerPlayCodes($pool);
|
||
if ($codes === []) {
|
||
return false;
|
||
}
|
||
|
||
return $winners->contains(
|
||
fn (array $r): bool => in_array((string) $r['item']->play_code, $codes, true),
|
||
);
|
||
}
|
||
|
||
/**
|
||
* @return list<string>
|
||
*/
|
||
private function comboTriggerPlayCodes(JackpotPool $pool): array
|
||
{
|
||
$raw = $pool->combo_trigger_play_codes;
|
||
if (! is_array($raw)) {
|
||
return [];
|
||
}
|
||
|
||
return array_values(array_filter(
|
||
array_map(fn ($v): string => strtolower(trim((string) $v)), $raw),
|
||
fn (string $v): bool => $v !== '',
|
||
));
|
||
}
|
||
}
|