$results * @return array{allocations: array, 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 $winnerItems * @return array{allocations: array, 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 $winnerItems * @param array $snapshotExtra * @return array{allocations: array, 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 $winnerItems * @return array */ 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 $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 */ 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 !== '', )); } }