Files
lotteryLaravel/app/Services/Settlement/SettlementOrchestrator.php
kang e27a00f260 feat: 更新玩法配置管理,简化字段并增强功能
- 将玩法相关的显示名称字段统一为 `display_name`,移除多语言字段。
- 在 `PlayTypePatchController` 中新增即时切换玩法开关的功能,并推送大厅更新。
- 优化多个控制器和服务中的权限检查与数据处理逻辑,提升代码可读性与维护性。
2026-05-25 14:34:24 +08:00

226 lines
8.6 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Services\Settlement;
use App\Models\Draw;
use App\Models\TicketItem;
use App\Lottery\DrawStatus;
use App\Models\JackpotPool;
use App\Models\DrawResultItem;
use App\Models\DrawResultBatch;
use App\Models\SettlementBatch;
use Illuminate\Support\Facades\DB;
use App\Lottery\DrawResultBatchStatus;
use App\Lottery\SettlementBatchStatus;
use App\Models\TicketSettlementDetail;
use App\Services\Draw\DrawHallSnapshotBuilder;
use App\Services\Draw\LotteryHallRealtimeBroadcaster;
use App\Services\Ticket\RiskPoolService;
use App\Services\Jackpot\JackpotBurstAllocator;
/**
* 阶段 6对已发布开奖、处于 `settling` 的期号执行结算(匹配 → 回水派彩调整 → Jackpot 爆池分配 → 明细 → 风险池释放 → 待审核)。
*
* 派彩入账由审核通过后的独立 payout 动作执行,避免未确认结果直接入账。
*/
final class SettlementOrchestrator
{
public function __construct(
private readonly SettlementMatcherRegistry $matchers,
private readonly SettlementPayoutAdjuster $payoutAdjuster,
private readonly JackpotBurstAllocator $jackpotBurst,
private readonly RiskPoolService $riskPool,
private readonly LotteryHallRealtimeBroadcaster $hallRealtime,
private readonly DrawHallSnapshotBuilder $hallSnapshot,
) {}
/**
* @return bool true 表示已处理(新结算或补全期号状态)
*/
public function trySettleDraw(Draw $draw): bool
{
return (bool) DB::transaction(function () use ($draw): bool {
/** @var Draw $locked */
$locked = Draw::query()->whereKey($draw->id)->lockForUpdate()->firstOrFail();
if ($locked->status === DrawStatus::Settled->value) {
return false;
}
if ($locked->status !== DrawStatus::Settling->value) {
return false;
}
$publishedBatch = DrawResultBatch::query()
->where('draw_id', $locked->id)
->where('status', DrawResultBatchStatus::Published->value)
->where('result_version', (int) $locked->current_result_version)
->orderByDesc('id')
->first();
if ($publishedBatch === null) {
return false;
}
$existingDone = SettlementBatch::query()
->where('draw_id', $locked->id)
->where('result_batch_id', $publishedBatch->id)
->whereIn('status', [
SettlementBatchStatus::PendingReview->value,
SettlementBatchStatus::Approved->value,
SettlementBatchStatus::Paid->value,
SettlementBatchStatus::Completed->value,
])
->first();
if ($existingDone !== null) {
$locked->forceFill([
'settle_version' => (int) $existingDone->settle_version,
])->save();
return true;
}
$items = DrawResultItem::query()
->where('result_batch_id', $publishedBatch->id)
->orderBy('id')
->get();
$board = new PublishedDrawResultBoard($items);
$nextSettleVersion = (int) $locked->settle_version + 1;
$batchRow = SettlementBatch::query()->create([
'draw_id' => $locked->id,
'result_batch_id' => $publishedBatch->id,
'settle_version' => $nextSettleVersion,
'status' => SettlementBatchStatus::Running->value,
'review_status' => 'pending',
'started_at' => now(),
]);
$ticketItems = TicketItem::query()
->where('draw_id', $locked->id)
->where('status', 'pending_draw')
->with(['combinations', 'order'])
->orderBy('id')
->get();
/** @var list<array{item: TicketItem, gross_win: int, matched_tier: ?string, net_win: int, match_detail: mixed}> $prepared */
$prepared = [];
foreach ($ticketItems as $item) {
$matcher = $this->matchers->for((string) $item->play_code);
$result = $matcher->match($item, $board, $item->combinations);
$gross = max(0, (int) $result['win_amount']);
$tier = $result['matched_prize_tier'] ?? null;
$tier = is_string($tier) ? $tier : null;
$net = $this->payoutAdjuster->adjustGrossWin($gross, $item);
$prepared[] = [
'item' => $item,
'gross_win' => $gross,
'matched_tier' => $tier,
'net_win' => $net,
'match_detail' => $result['match_detail'],
];
}
$currency = strtoupper((string) ($ticketItems->first()?->order?->currency_code ?? 'NPR'));
$pool = JackpotPool::query()
->where('currency_code', $currency)
->where('status', 1)
->lockForUpdate()
->first();
$allocations = [];
$totalJackpotPayout = 0;
$jackpotTrigger = null;
$jackpotPoolAfter = null;
if ($pool !== null) {
$burstInput = collect($prepared)->map(fn (array $p): array => [
'item' => $p['item'],
'matched_tier' => $p['matched_tier'],
'gross_win' => $p['gross_win'],
]);
$burstOut = $this->jackpotBurst->allocate($locked, $pool, $burstInput);
$allocations = $burstOut['allocations'];
$totalJackpotPayout = (int) $burstOut['pool_payout'];
$jackpotTrigger = $burstOut['trigger'];
$jackpotPoolAfter = (int) $pool->fresh()->current_amount;
}
$ticketCount = 0;
$winCount = 0;
$totalPayout = 0;
foreach ($prepared as $p) {
/** @var TicketItem $item */
$item = $p['item'];
$ticketCount++;
$net = (int) $p['net_win'];
$jackpotShare = (int) ($allocations[(int) $item->id] ?? 0);
$finalCredit = $net + $jackpotShare;
TicketSettlementDetail::query()->create([
'settlement_batch_id' => $batchRow->id,
'ticket_item_id' => $item->id,
'matched_prize_tier' => $p['matched_tier'],
'win_amount' => $net,
'jackpot_allocation_amount' => $jackpotShare,
'match_detail_json' => $p['match_detail'],
]);
$item->forceFill([
'win_amount' => $net,
'jackpot_win_amount' => $jackpotShare,
'settled_at' => null,
'status' => $finalCredit > 0 ? 'pending_payout' : 'settled_lose',
])->save();
if ($finalCredit > 0) {
$winCount++;
}
$totalPayout += $finalCredit;
$locks = [];
foreach ($item->combinations as $c) {
$locks[] = [
'number_4d' => (string) $c->number_4d,
'amount' => (int) $c->estimated_payout,
];
}
$this->riskPool->release((int) $locked->id, $item, $locks);
}
$batchRow->forceFill([
'status' => SettlementBatchStatus::PendingReview->value,
'total_ticket_count' => $ticketCount,
'total_win_count' => $winCount,
'total_payout_amount' => $totalPayout,
'total_jackpot_payout_amount' => $totalJackpotPayout,
'finished_at' => now(),
])->save();
$locked->forceFill([
'status' => DrawStatus::Settling->value,
'settle_version' => $nextSettleVersion,
])->save();
if ($pool !== null && $totalJackpotPayout > 0 && is_string($jackpotTrigger)) {
$this->hallRealtime->notifyJackpotBurst(
(int) $locked->id,
(string) $locked->draw_no,
$board->firstPrizeNumber4d(),
$currency,
$totalJackpotPayout,
count($allocations),
$jackpotTrigger,
(int) $jackpotPoolAfter,
);
$this->hallRealtime->notifyStatusChange($this->hallSnapshot->build());
}
return true;
});
}
}