feat: 添加结算功能,更新 TicketItem 模型以支持最新结算详情,增强 DrawTickService 以自动处理结算,更新 TicketWalletService 以支持派彩入账,扩展 API 路由以管理结算批次和奖池
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Settlement\Matchers;
|
||||
|
||||
use App\Models\TicketCombination;
|
||||
use App\Models\TicketItem;
|
||||
use App\Services\Settlement\Contracts\SettlementPlayMatcher;
|
||||
use App\Services\Settlement\OddsSnapshotReader;
|
||||
use App\Services\Settlement\PublishedDrawResultBoard;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Big / 包号展开类:命中 23 档中**最优档**计奖(产品文档 Big / iBox / mBox / Box)。
|
||||
*/
|
||||
final class BigSpreadSettlementMatcher implements SettlementPlayMatcher
|
||||
{
|
||||
public function __construct(
|
||||
private readonly OddsSnapshotReader $odds,
|
||||
) {}
|
||||
|
||||
public function match(TicketItem $item, PublishedDrawResultBoard $board, Collection $combinations): array
|
||||
{
|
||||
$snapshot = is_array($item->odds_snapshot_json) ? $item->odds_snapshot_json : null;
|
||||
$lines = [];
|
||||
$total = 0;
|
||||
$bestTier = null;
|
||||
$bestRank = 99;
|
||||
|
||||
foreach ($combinations as $c) {
|
||||
/** @var TicketCombination $c */
|
||||
$hit = $board->bestTierForNumber((string) $c->number_4d);
|
||||
if ($hit === null) {
|
||||
continue;
|
||||
}
|
||||
$tier = $hit['tier'];
|
||||
$oddsVal = $this->odds->oddsValueForScope($snapshot, $tier);
|
||||
$bet = (int) $c->bet_amount;
|
||||
$payout = (int) floor($bet * ($oddsVal / 10_000));
|
||||
$total += $payout;
|
||||
$lines[] = [
|
||||
'number_4d' => $c->number_4d,
|
||||
'matched_tier' => $tier,
|
||||
'bet_amount' => $bet,
|
||||
'odds_value' => $oddsVal,
|
||||
'payout' => $payout,
|
||||
];
|
||||
if ($hit['rank'] < $bestRank) {
|
||||
$bestRank = $hit['rank'];
|
||||
$bestTier = $tier;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'win_amount' => $total,
|
||||
'matched_prize_tier' => $bestTier,
|
||||
'match_detail' => ['lines' => $lines],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Settlement\Matchers;
|
||||
|
||||
use App\Models\TicketCombination;
|
||||
use App\Models\TicketItem;
|
||||
use App\Services\Settlement\Contracts\SettlementPlayMatcher;
|
||||
use App\Services\Settlement\OddsSnapshotReader;
|
||||
use App\Services\Settlement\PublishedDrawResultBoard;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* head / tail / odd / even / digit_big / digit_small:展开组合中若有与**头奖 4D** 完全一致则中奖(赔率档 first)。
|
||||
*/
|
||||
final class FirstPrizeComboSettlementMatcher implements SettlementPlayMatcher
|
||||
{
|
||||
public function __construct(
|
||||
private readonly OddsSnapshotReader $odds,
|
||||
) {}
|
||||
|
||||
public function match(TicketItem $item, PublishedDrawResultBoard $board, Collection $combinations): array
|
||||
{
|
||||
$first = $board->firstPrizeNumber4d();
|
||||
if ($first === '') {
|
||||
return ['win_amount' => 0, 'matched_prize_tier' => null, 'match_detail' => ['reason' => 'no_first']];
|
||||
}
|
||||
|
||||
$snapshot = is_array($item->odds_snapshot_json) ? $item->odds_snapshot_json : null;
|
||||
$oddsVal = $this->odds->oddsValueForScope($snapshot, 'first');
|
||||
$lines = [];
|
||||
$total = 0;
|
||||
|
||||
foreach ($combinations as $c) {
|
||||
/** @var TicketCombination $c */
|
||||
if ((string) $c->number_4d !== $first) {
|
||||
continue;
|
||||
}
|
||||
$bet = (int) $c->bet_amount;
|
||||
$payout = (int) floor($bet * ($oddsVal / 10_000));
|
||||
$total += $payout;
|
||||
$lines[] = ['number_4d' => $c->number_4d, 'bet_amount' => $bet, 'payout' => $payout];
|
||||
}
|
||||
|
||||
return [
|
||||
'win_amount' => $total,
|
||||
'matched_prize_tier' => $total > 0 ? 'first' : null,
|
||||
'match_detail' => ['lines' => $lines, 'first_prize' => $first],
|
||||
];
|
||||
}
|
||||
}
|
||||
23
app/Services/Settlement/Matchers/NoopSettlementMatcher.php
Normal file
23
app/Services/Settlement/Matchers/NoopSettlementMatcher.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Settlement\Matchers;
|
||||
|
||||
use App\Models\TicketItem;
|
||||
use App\Services\Settlement\Contracts\SettlementPlayMatcher;
|
||||
use App\Services\Settlement\PublishedDrawResultBoard;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* 阶段 6 首轮未实现的玩法:不派奖(后续补位置类、单双等匹配器)。
|
||||
*/
|
||||
final class NoopSettlementMatcher implements SettlementPlayMatcher
|
||||
{
|
||||
public function match(TicketItem $item, PublishedDrawResultBoard $board, Collection $combinations): array
|
||||
{
|
||||
return [
|
||||
'win_amount' => 0,
|
||||
'matched_prize_tier' => null,
|
||||
'match_detail' => ['play_code' => $item->play_code, 'skipped' => true],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Settlement\Matchers;
|
||||
|
||||
use App\Models\TicketCombination;
|
||||
use App\Models\TicketItem;
|
||||
use App\Services\Settlement\Contracts\SettlementPlayMatcher;
|
||||
use App\Services\Settlement\OddsSnapshotReader;
|
||||
use App\Services\Settlement\PublishedDrawResultBoard;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/** pos_2abc:后二位命中头/二/三任意一档。 */
|
||||
final class Pos2AbcSettlementMatcher implements SettlementPlayMatcher
|
||||
{
|
||||
public function __construct(
|
||||
private readonly OddsSnapshotReader $odds,
|
||||
) {}
|
||||
|
||||
public function match(TicketItem $item, PublishedDrawResultBoard $board, Collection $combinations): array
|
||||
{
|
||||
$tiers = ['first', 'second', 'third'];
|
||||
$suffixByTier = [];
|
||||
foreach ($tiers as $t) {
|
||||
$s = $board->suffix2ForTier($t, 0);
|
||||
if ($s !== '') {
|
||||
$suffixByTier[$t] = $s;
|
||||
}
|
||||
}
|
||||
if ($suffixByTier === []) {
|
||||
return ['win_amount' => 0, 'matched_prize_tier' => null, 'match_detail' => ['reason' => 'no_suffix']];
|
||||
}
|
||||
|
||||
$snapshot = is_array($item->odds_snapshot_json) ? $item->odds_snapshot_json : null;
|
||||
$lines = [];
|
||||
$total = 0;
|
||||
$bestTier = null;
|
||||
$bestRank = 99;
|
||||
|
||||
foreach ($combinations as $c) {
|
||||
/** @var TicketCombination $c */
|
||||
$n = (string) $c->number_4d;
|
||||
if (strlen($n) < 2) {
|
||||
continue;
|
||||
}
|
||||
$suf = substr($n, -2);
|
||||
$hitTier = null;
|
||||
$rank = 99;
|
||||
foreach ($suffixByTier as $t => $sx) {
|
||||
if ($suf !== $sx) {
|
||||
continue;
|
||||
}
|
||||
$r = match ($t) {
|
||||
'first' => 0,
|
||||
'second' => 1,
|
||||
'third' => 2,
|
||||
default => 99,
|
||||
};
|
||||
if ($r < $rank) {
|
||||
$rank = $r;
|
||||
$hitTier = $t;
|
||||
}
|
||||
}
|
||||
if ($hitTier === null) {
|
||||
continue;
|
||||
}
|
||||
$oddsVal = $this->odds->oddsValueForScope($snapshot, $hitTier);
|
||||
$bet = (int) $c->bet_amount;
|
||||
$payout = (int) floor($bet * ($oddsVal / 10_000));
|
||||
$total += $payout;
|
||||
$lines[] = ['number_4d' => $n, 'tier' => $hitTier, 'payout' => $payout];
|
||||
if ($rank < $bestRank) {
|
||||
$bestRank = $rank;
|
||||
$bestTier = $hitTier;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'win_amount' => $total,
|
||||
'matched_prize_tier' => $bestTier,
|
||||
'match_detail' => ['lines' => $lines],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Settlement\Matchers;
|
||||
|
||||
use App\Models\TicketCombination;
|
||||
use App\Models\TicketItem;
|
||||
use App\Services\Settlement\Contracts\SettlementPlayMatcher;
|
||||
use App\Services\Settlement\OddsSnapshotReader;
|
||||
use App\Services\Settlement\PublishedDrawResultBoard;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/** pos_2a / pos_2b / pos_2c:后二位命中对应档。 */
|
||||
final class Pos2TierSettlementMatcher implements SettlementPlayMatcher
|
||||
{
|
||||
/** @var array<string, string> */
|
||||
private const PLAY_TO_TIER = [
|
||||
'pos_2a' => 'first',
|
||||
'pos_2b' => 'second',
|
||||
'pos_2c' => 'third',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private readonly OddsSnapshotReader $odds,
|
||||
) {}
|
||||
|
||||
public function match(TicketItem $item, PublishedDrawResultBoard $board, Collection $combinations): array
|
||||
{
|
||||
$tier = self::PLAY_TO_TIER[$item->play_code] ?? 'first';
|
||||
$suffix = $board->suffix2ForTier($tier, 0);
|
||||
if ($suffix === '') {
|
||||
return ['win_amount' => 0, 'matched_prize_tier' => null, 'match_detail' => ['reason' => 'no_suffix']];
|
||||
}
|
||||
|
||||
$snapshot = is_array($item->odds_snapshot_json) ? $item->odds_snapshot_json : null;
|
||||
$oddsVal = $this->odds->oddsValueForScope($snapshot, $tier);
|
||||
$lines = [];
|
||||
$total = 0;
|
||||
|
||||
foreach ($combinations as $c) {
|
||||
/** @var TicketCombination $c */
|
||||
$n = (string) $c->number_4d;
|
||||
if (strlen($n) < 2 || substr($n, -2) !== $suffix) {
|
||||
continue;
|
||||
}
|
||||
$bet = (int) $c->bet_amount;
|
||||
$payout = (int) floor($bet * ($oddsVal / 10_000));
|
||||
$total += $payout;
|
||||
$lines[] = ['number_4d' => $n, 'suffix2' => $suffix, 'payout' => $payout];
|
||||
}
|
||||
|
||||
return [
|
||||
'win_amount' => $total,
|
||||
'matched_prize_tier' => $total > 0 ? $tier : null,
|
||||
'match_detail' => ['lines' => $lines],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Settlement\Matchers;
|
||||
|
||||
use App\Models\TicketCombination;
|
||||
use App\Models\TicketItem;
|
||||
use App\Services\Settlement\Contracts\SettlementPlayMatcher;
|
||||
use App\Services\Settlement\OddsSnapshotReader;
|
||||
use App\Services\Settlement\PublishedDrawResultBoard;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/** pos_3abc:后三位命中头/二/三任意一档;取最优档赔率。 */
|
||||
final class Pos3AbcSettlementMatcher implements SettlementPlayMatcher
|
||||
{
|
||||
public function __construct(
|
||||
private readonly OddsSnapshotReader $odds,
|
||||
) {}
|
||||
|
||||
public function match(TicketItem $item, PublishedDrawResultBoard $board, Collection $combinations): array
|
||||
{
|
||||
$tiers = ['first', 'second', 'third'];
|
||||
$suffixByTier = [];
|
||||
foreach ($tiers as $t) {
|
||||
$s = $board->suffix3ForTier($t, 0);
|
||||
if ($s !== '') {
|
||||
$suffixByTier[$t] = $s;
|
||||
}
|
||||
}
|
||||
if ($suffixByTier === []) {
|
||||
return ['win_amount' => 0, 'matched_prize_tier' => null, 'match_detail' => ['reason' => 'no_suffix']];
|
||||
}
|
||||
|
||||
$snapshot = is_array($item->odds_snapshot_json) ? $item->odds_snapshot_json : null;
|
||||
$lines = [];
|
||||
$total = 0;
|
||||
$bestTier = null;
|
||||
$bestRank = 99;
|
||||
|
||||
foreach ($combinations as $c) {
|
||||
/** @var TicketCombination $c */
|
||||
$n = (string) $c->number_4d;
|
||||
if (strlen($n) < 3) {
|
||||
continue;
|
||||
}
|
||||
$suf = substr($n, -3);
|
||||
$hitTier = null;
|
||||
$rank = 99;
|
||||
foreach ($suffixByTier as $t => $sx) {
|
||||
if ($suf !== $sx) {
|
||||
continue;
|
||||
}
|
||||
$r = match ($t) {
|
||||
'first' => 0,
|
||||
'second' => 1,
|
||||
'third' => 2,
|
||||
default => 99,
|
||||
};
|
||||
if ($r < $rank) {
|
||||
$rank = $r;
|
||||
$hitTier = $t;
|
||||
}
|
||||
}
|
||||
if ($hitTier === null) {
|
||||
continue;
|
||||
}
|
||||
$oddsVal = $this->odds->oddsValueForScope($snapshot, $hitTier);
|
||||
$bet = (int) $c->bet_amount;
|
||||
$payout = (int) floor($bet * ($oddsVal / 10_000));
|
||||
$total += $payout;
|
||||
$lines[] = ['number_4d' => $n, 'tier' => $hitTier, 'payout' => $payout];
|
||||
if ($rank < $bestRank) {
|
||||
$bestRank = $rank;
|
||||
$bestTier = $hitTier;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'win_amount' => $total,
|
||||
'matched_prize_tier' => $bestTier,
|
||||
'match_detail' => ['lines' => $lines],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Settlement\Matchers;
|
||||
|
||||
use App\Models\TicketCombination;
|
||||
use App\Models\TicketItem;
|
||||
use App\Services\Settlement\Contracts\SettlementPlayMatcher;
|
||||
use App\Services\Settlement\OddsSnapshotReader;
|
||||
use App\Services\Settlement\PublishedDrawResultBoard;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/** pos_3a / pos_3b / pos_3c:后三位命中对应档。头奖命中时 `matched_prize_tier` 为 first(Jackpot 口径)。 */
|
||||
final class Pos3TierSettlementMatcher implements SettlementPlayMatcher
|
||||
{
|
||||
/** @var array<string, string> */
|
||||
private const PLAY_TO_TIER = [
|
||||
'pos_3a' => 'first',
|
||||
'pos_3b' => 'second',
|
||||
'pos_3c' => 'third',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private readonly OddsSnapshotReader $odds,
|
||||
) {}
|
||||
|
||||
public function match(TicketItem $item, PublishedDrawResultBoard $board, Collection $combinations): array
|
||||
{
|
||||
$tier = self::PLAY_TO_TIER[$item->play_code] ?? 'first';
|
||||
$suffix = $board->suffix3ForTier($tier, 0);
|
||||
if ($suffix === '') {
|
||||
return ['win_amount' => 0, 'matched_prize_tier' => null, 'match_detail' => ['reason' => 'no_suffix']];
|
||||
}
|
||||
|
||||
$snapshot = is_array($item->odds_snapshot_json) ? $item->odds_snapshot_json : null;
|
||||
$oddsVal = $this->odds->oddsValueForScope($snapshot, $tier);
|
||||
$lines = [];
|
||||
$total = 0;
|
||||
|
||||
foreach ($combinations as $c) {
|
||||
/** @var TicketCombination $c */
|
||||
$n = (string) $c->number_4d;
|
||||
if (strlen($n) < 3 || substr($n, -3) !== $suffix) {
|
||||
continue;
|
||||
}
|
||||
$bet = (int) $c->bet_amount;
|
||||
$payout = (int) floor($bet * ($oddsVal / 10_000));
|
||||
$total += $payout;
|
||||
$lines[] = ['number_4d' => $n, 'suffix3' => $suffix, 'payout' => $payout];
|
||||
}
|
||||
|
||||
return [
|
||||
'win_amount' => $total,
|
||||
'matched_prize_tier' => $total > 0 ? $tier : null,
|
||||
'match_detail' => ['lines' => $lines],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Settlement\Matchers;
|
||||
|
||||
use App\Models\TicketCombination;
|
||||
use App\Models\TicketItem;
|
||||
use App\Services\Settlement\Contracts\SettlementPlayMatcher;
|
||||
use App\Services\Settlement\OddsSnapshotReader;
|
||||
use App\Services\Settlement\PublishedDrawResultBoard;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/** pos_4a / pos_4b / pos_4c:与对应档完整 4D 一致。 */
|
||||
final class Pos4ExactTierSettlementMatcher implements SettlementPlayMatcher
|
||||
{
|
||||
/** @var array<string, string> */
|
||||
private const PLAY_TO_TIER = [
|
||||
'pos_4a' => 'first',
|
||||
'pos_4b' => 'second',
|
||||
'pos_4c' => 'third',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private readonly OddsSnapshotReader $odds,
|
||||
) {}
|
||||
|
||||
public function match(TicketItem $item, PublishedDrawResultBoard $board, Collection $combinations): array
|
||||
{
|
||||
$tier = self::PLAY_TO_TIER[$item->play_code] ?? 'first';
|
||||
$row = $board->row($tier, 0);
|
||||
if ($row === null) {
|
||||
return ['win_amount' => 0, 'matched_prize_tier' => null, 'match_detail' => ['reason' => 'no_row']];
|
||||
}
|
||||
$target = (string) $row->number_4d;
|
||||
$snapshot = is_array($item->odds_snapshot_json) ? $item->odds_snapshot_json : null;
|
||||
$oddsVal = $this->odds->oddsValueForScope($snapshot, $tier);
|
||||
$lines = [];
|
||||
$total = 0;
|
||||
|
||||
foreach ($combinations as $c) {
|
||||
/** @var TicketCombination $c */
|
||||
if ((string) $c->number_4d !== $target) {
|
||||
continue;
|
||||
}
|
||||
$bet = (int) $c->bet_amount;
|
||||
$payout = (int) floor($bet * ($oddsVal / 10_000));
|
||||
$total += $payout;
|
||||
$lines[] = ['number_4d' => $c->number_4d, 'bet_amount' => $bet, 'payout' => $payout];
|
||||
}
|
||||
|
||||
return [
|
||||
'win_amount' => $total,
|
||||
'matched_prize_tier' => $total > 0 ? $tier : null,
|
||||
'match_detail' => ['lines' => $lines, 'target' => $target],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Settlement\Matchers;
|
||||
|
||||
use App\Models\TicketCombination;
|
||||
use App\Models\TicketItem;
|
||||
use App\Services\Settlement\Contracts\SettlementPlayMatcher;
|
||||
use App\Services\Settlement\OddsSnapshotReader;
|
||||
use App\Services\Settlement\PublishedDrawResultBoard;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/** pos_4d(特别奖)/ pos_4e(安慰奖):命中任意一组即中奖。 */
|
||||
final class Pos4ListTierSettlementMatcher implements SettlementPlayMatcher
|
||||
{
|
||||
/** @var array<string, string> */
|
||||
private const PLAY_TO_TIER = [
|
||||
'pos_4d' => 'starter',
|
||||
'pos_4e' => 'consolation',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private readonly OddsSnapshotReader $odds,
|
||||
) {}
|
||||
|
||||
public function match(TicketItem $item, PublishedDrawResultBoard $board, Collection $combinations): array
|
||||
{
|
||||
$tier = self::PLAY_TO_TIER[$item->play_code] ?? 'starter';
|
||||
$targets = array_flip($board->numbersForPrizeType($tier));
|
||||
if ($targets === []) {
|
||||
return ['win_amount' => 0, 'matched_prize_tier' => null, 'match_detail' => ['reason' => 'no_targets']];
|
||||
}
|
||||
|
||||
$snapshot = is_array($item->odds_snapshot_json) ? $item->odds_snapshot_json : null;
|
||||
$oddsVal = $this->odds->oddsValueForScope($snapshot, $tier);
|
||||
$lines = [];
|
||||
$total = 0;
|
||||
|
||||
foreach ($combinations as $c) {
|
||||
/** @var TicketCombination $c */
|
||||
$n = (string) $c->number_4d;
|
||||
if (! isset($targets[$n])) {
|
||||
continue;
|
||||
}
|
||||
$bet = (int) $c->bet_amount;
|
||||
$payout = (int) floor($bet * ($oddsVal / 10_000));
|
||||
$total += $payout;
|
||||
$lines[] = ['number_4d' => $n, 'bet_amount' => $bet, 'payout' => $payout, 'tier' => $tier];
|
||||
}
|
||||
|
||||
return [
|
||||
'win_amount' => $total,
|
||||
'matched_prize_tier' => $total > 0 ? $tier : null,
|
||||
'match_detail' => ['lines' => $lines],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Settlement\Matchers;
|
||||
|
||||
use App\Models\TicketCombination;
|
||||
use App\Models\TicketItem;
|
||||
use App\Services\Settlement\Contracts\SettlementPlayMatcher;
|
||||
use App\Services\Settlement\OddsSnapshotReader;
|
||||
use App\Services\Settlement\PublishedDrawResultBoard;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Small:仅头 / 二 / 三奖(产品文档 Small)。
|
||||
*/
|
||||
final class SmallSpreadSettlementMatcher implements SettlementPlayMatcher
|
||||
{
|
||||
public function __construct(
|
||||
private readonly OddsSnapshotReader $odds,
|
||||
) {}
|
||||
|
||||
public function match(TicketItem $item, PublishedDrawResultBoard $board, Collection $combinations): array
|
||||
{
|
||||
$snapshot = is_array($item->odds_snapshot_json) ? $item->odds_snapshot_json : null;
|
||||
$lines = [];
|
||||
$total = 0;
|
||||
$bestTier = null;
|
||||
$bestRank = 99;
|
||||
|
||||
foreach ($combinations as $c) {
|
||||
/** @var TicketCombination $c */
|
||||
$hit = $board->bestSmallTierForNumber((string) $c->number_4d);
|
||||
if ($hit === null) {
|
||||
continue;
|
||||
}
|
||||
$tier = $hit['tier'];
|
||||
$oddsVal = $this->odds->oddsValueForScope($snapshot, $tier);
|
||||
$bet = (int) $c->bet_amount;
|
||||
$payout = (int) floor($bet * ($oddsVal / 10_000));
|
||||
$total += $payout;
|
||||
$lines[] = [
|
||||
'number_4d' => $c->number_4d,
|
||||
'matched_tier' => $tier,
|
||||
'bet_amount' => $bet,
|
||||
'odds_value' => $oddsVal,
|
||||
'payout' => $payout,
|
||||
];
|
||||
if ($hit['rank'] < $bestRank) {
|
||||
$bestRank = $hit['rank'];
|
||||
$bestTier = $tier;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'win_amount' => $total,
|
||||
'matched_prize_tier' => $bestTier,
|
||||
'match_detail' => ['lines' => $lines],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Settlement\Matchers;
|
||||
|
||||
use App\Models\TicketCombination;
|
||||
use App\Models\TicketItem;
|
||||
use App\Services\Settlement\Contracts\SettlementPlayMatcher;
|
||||
use App\Services\Settlement\OddsSnapshotReader;
|
||||
use App\Services\Settlement\PublishedDrawResultBoard;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* 直选类:仅与**头奖**号码完全一致中奖(产品文档 Straight / 头奖口径)。
|
||||
*
|
||||
* 适用于 `straight`、`roll`(组合已展开为多条 4D)。
|
||||
*/
|
||||
final class StraightLikeSettlementMatcher implements SettlementPlayMatcher
|
||||
{
|
||||
public function __construct(
|
||||
private readonly OddsSnapshotReader $odds,
|
||||
) {}
|
||||
|
||||
public function match(TicketItem $item, PublishedDrawResultBoard $board, Collection $combinations): array
|
||||
{
|
||||
$target = $board->firstPrizeNumber4d();
|
||||
if ($target === '') {
|
||||
return ['win_amount' => 0, 'matched_prize_tier' => null, 'match_detail' => ['reason' => 'no_first_prize']];
|
||||
}
|
||||
|
||||
$snapshot = is_array($item->odds_snapshot_json) ? $item->odds_snapshot_json : null;
|
||||
$oddsVal = $this->odds->oddsValueForScope($snapshot, 'first');
|
||||
$lines = [];
|
||||
$total = 0;
|
||||
|
||||
foreach ($combinations as $c) {
|
||||
/** @var TicketCombination $c */
|
||||
if ((string) $c->number_4d !== $target) {
|
||||
continue;
|
||||
}
|
||||
$bet = (int) $c->bet_amount;
|
||||
$payout = (int) floor($bet * ($oddsVal / 10_000));
|
||||
$total += $payout;
|
||||
$lines[] = [
|
||||
'number_4d' => $c->number_4d,
|
||||
'bet_amount' => $bet,
|
||||
'odds_value' => $oddsVal,
|
||||
'payout' => $payout,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'win_amount' => $total,
|
||||
'matched_prize_tier' => $total > 0 ? 'first' : null,
|
||||
'match_detail' => ['lines' => $lines, 'first_prize' => $target],
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user