feat: 为赔率项增加 dimension 字段并支持按维度配置佣金,调整后台配置导航权限
This commit is contained in:
@@ -16,6 +16,7 @@ final class OddsItem extends Model
|
||||
'version_id',
|
||||
'play_code',
|
||||
'prize_scope',
|
||||
'dimension',
|
||||
'odds_value',
|
||||
'rebate_rate',
|
||||
'commission_rate',
|
||||
@@ -27,6 +28,7 @@ final class OddsItem extends Model
|
||||
{
|
||||
return [
|
||||
'version_id' => 'integer',
|
||||
'dimension' => 'integer',
|
||||
'odds_value' => 'integer',
|
||||
'rebate_rate' => 'decimal:4',
|
||||
'commission_rate' => 'decimal:4',
|
||||
|
||||
@@ -62,6 +62,7 @@ final class OddsStreamService
|
||||
'version_id' => $draft->id,
|
||||
'play_code' => $row->play_code,
|
||||
'prize_scope' => $row->prize_scope,
|
||||
'dimension' => $row->dimension,
|
||||
'odds_value' => $row->odds_value,
|
||||
'rebate_rate' => $row->rebate_rate,
|
||||
'commission_rate' => $row->commission_rate,
|
||||
@@ -77,6 +78,7 @@ final class OddsStreamService
|
||||
'version_id' => $draft->id,
|
||||
'play_code' => $pt->play_code,
|
||||
'prize_scope' => $scope,
|
||||
'dimension' => $pt->dimension,
|
||||
'odds_value' => $oddsValue,
|
||||
'rebate_rate' => 0,
|
||||
'commission_rate' => 0,
|
||||
@@ -107,6 +109,7 @@ final class OddsStreamService
|
||||
'version_id' => $draft->id,
|
||||
'play_code' => (string) $row['play_code'],
|
||||
'prize_scope' => (string) $row['prize_scope'],
|
||||
'dimension' => isset($row['dimension']) ? (int) $row['dimension'] : null,
|
||||
'odds_value' => (int) $row['odds_value'],
|
||||
'rebate_rate' => (float) ($row['rebate_rate'] ?? 0),
|
||||
'commission_rate' => (float) ($row['commission_rate'] ?? 0),
|
||||
@@ -221,6 +224,7 @@ final class OddsStreamService
|
||||
foreach ($items as $index => $row) {
|
||||
$playCode = (string) $row->play_code;
|
||||
$scope = (string) $row->prize_scope;
|
||||
$dimension = $row->dimension;
|
||||
$currencyCode = strtoupper((string) $row->currency_code);
|
||||
$oddsValue = (int) $row->odds_value;
|
||||
$rebateRate = (float) $row->rebate_rate;
|
||||
@@ -239,6 +243,10 @@ final class OddsStreamService
|
||||
$errors["items.$index.currency_code"][] = '币种不可下注';
|
||||
}
|
||||
|
||||
if ($dimension !== null && ! in_array($dimension, [2, 3, 4], true)) {
|
||||
$errors["items.$index.dimension"][] = '维度必须是 2、3 或 4';
|
||||
}
|
||||
|
||||
if (isset($seenKeys[$key])) {
|
||||
$errors["items.$index"][] = '同一玩法、档位、币种存在重复赔率项';
|
||||
}
|
||||
|
||||
@@ -48,9 +48,11 @@ final class PlayRuleEngine
|
||||
|
||||
$unitBetAmount = $this->resolveUnitBetAmount($playCode, $amount, $combinationCount);
|
||||
$totalBetAmount = $this->resolveTotalBetAmount($playCode, $amount, $unitBetAmount, $combinationCount);
|
||||
$dimensionInt = $this->toDimensionInt(is_string($dimension) ? $dimension : null, $playConfig);
|
||||
$primaryOdds = $this->pickPrimaryOdds($oddsItems);
|
||||
$rebateRate = (float) $primaryOdds->rebate_rate;
|
||||
$commissionRate = (float) $primaryOdds->commission_rate;
|
||||
// 佣金按维度(2D/3D/4D)配置:从 odds_items 中查找匹配维度的佣金率
|
||||
$commissionRate = $this->pickCommissionByDimension($oddsItems, $dimensionInt);
|
||||
$actualDeductAmount = max(0, (int) floor($totalBetAmount * (1 - $rebateRate)));
|
||||
$maxOdds = $oddsItems->max(fn (OddsItem $row) => (int) $row->odds_value) ?? 0;
|
||||
$estimatedPayoutPerCombo = (int) floor($unitBetAmount * ($maxOdds / 10000));
|
||||
@@ -302,6 +304,28 @@ final class PlayRuleEngine
|
||||
return $oddsItems->firstOrFail();
|
||||
}
|
||||
|
||||
/**
|
||||
* 按维度(2D/3D/4D)获取佣金率
|
||||
*
|
||||
* @param Collection<int, OddsItem> $oddsItems
|
||||
*/
|
||||
private function pickCommissionByDimension(Collection $oddsItems, ?int $dimension): float
|
||||
{
|
||||
if ($dimension === null) {
|
||||
// 如果维度为空,返回第一个 odds item 的佣金率(向后兼容)
|
||||
return (float) ($oddsItems->first()?->commission_rate ?? 0);
|
||||
}
|
||||
|
||||
// 查找匹配维度的 odds item
|
||||
$dimensionItem = $oddsItems->firstWhere('dimension', $dimension);
|
||||
if ($dimensionItem !== null) {
|
||||
return (float) $dimensionItem->commission_rate;
|
||||
}
|
||||
|
||||
// 如果没有找到匹配维度的,返回第一个的佣金率(向后兼容)
|
||||
return (float) ($oddsItems->first()?->commission_rate ?? 0);
|
||||
}
|
||||
|
||||
private function toDimensionInt(?string $dimension, PlayConfigItem $playConfig): ?int
|
||||
{
|
||||
return match ($dimension) {
|
||||
|
||||
@@ -38,14 +38,14 @@ final class AdminAuthorizationRegistry
|
||||
['slug' => 'prd.draw_result.view', 'name' => '开奖结果·查看', 'nav_segment' => 'draws', 'permission_codes' => ['draw.results.view', 'risk.monitor.view']],
|
||||
['slug' => 'prd.draw_reopen.manage', 'name' => '开奖结果重开·可管理', 'nav_segment' => 'draws', 'permission_codes' => ['draw.review.publish']],
|
||||
|
||||
['slug' => 'prd.play_switch.manage', 'name' => '玩法开关·可管理', 'nav_segment' => 'config', 'permission_codes' => ['config.play.manage']],
|
||||
['slug' => 'prd.odds.manage', 'name' => '赔率配置·可管理', 'nav_segment' => 'config', 'permission_codes' => ['config.odds.manage']],
|
||||
['slug' => 'prd.risk_cap.manage', 'name' => '封顶配置·可管理', 'nav_segment' => 'config', 'permission_codes' => ['config.risk_cap.manage']],
|
||||
['slug' => 'prd.risk_cap.view', 'name' => '封顶配置·查看', 'nav_segment' => 'config', 'permission_codes' => ['config.risk_cap.view']],
|
||||
['slug' => 'prd.rebate.manage', 'name' => '佣金/回水·可管理', 'nav_segment' => 'config', 'permission_codes' => ['config.odds.manage']],
|
||||
['slug' => 'prd.rebate.view', 'name' => '佣金/回水·查看', 'nav_segment' => 'config', 'permission_codes' => ['config.odds.manage']],
|
||||
['slug' => 'prd.jackpot.manage', 'name' => 'Jackpot 配置·可管理', 'nav_segment' => 'config', 'permission_codes' => ['config.jackpot.manage']],
|
||||
['slug' => 'prd.jackpot.view', 'name' => 'Jackpot 配置·查看', 'nav_segment' => 'config', 'permission_codes' => ['config.jackpot.view']],
|
||||
['slug' => 'prd.play_switch.manage', 'name' => '玩法开关·可管理', 'nav_segment' => 'rules_plays', 'permission_codes' => ['config.play.manage']],
|
||||
['slug' => 'prd.odds.manage', 'name' => '赔率配置·可管理', 'nav_segment' => 'rules_odds', 'permission_codes' => ['config.odds.manage']],
|
||||
['slug' => 'prd.risk_cap.manage', 'name' => '封顶配置·可管理', 'nav_segment' => 'risk_cap', 'permission_codes' => ['config.risk_cap.manage']],
|
||||
['slug' => 'prd.risk_cap.view', 'name' => '封顶配置·查看', 'nav_segment' => 'risk_cap', 'permission_codes' => ['config.risk_cap.view']],
|
||||
['slug' => 'prd.rebate.manage', 'name' => '佣金/回水·可管理', 'nav_segment' => 'rules_odds', 'permission_codes' => ['config.odds.manage']],
|
||||
['slug' => 'prd.rebate.view', 'name' => '佣金/回水·查看', 'nav_segment' => 'rules_odds', 'permission_codes' => ['config.odds.manage']],
|
||||
['slug' => 'prd.jackpot.manage', 'name' => 'Jackpot 配置·可管理', 'nav_segment' => 'jackpot', 'permission_codes' => ['config.jackpot.manage']],
|
||||
['slug' => 'prd.jackpot.view', 'name' => 'Jackpot 配置·查看', 'nav_segment' => 'jackpot', 'permission_codes' => ['config.jackpot.view']],
|
||||
|
||||
['slug' => 'prd.payout.manage', 'name' => '派彩确认·可管理', 'nav_segment' => 'settlement', 'permission_codes' => ['settlement.batch.manage', 'settlement.batch.view']],
|
||||
['slug' => 'prd.payout.review', 'name' => '派彩确认·可审核', 'nav_segment' => 'settlement', 'permission_codes' => ['settlement.batch.review', 'settlement.batch.view']],
|
||||
@@ -73,10 +73,12 @@ final class AdminAuthorizationRegistry
|
||||
'currencies' => '币种管理',
|
||||
'wallet' => '钱包流水',
|
||||
'draws' => '期号列表',
|
||||
'config' => '运营配置',
|
||||
'rules_plays' => '投注规则',
|
||||
'rules_odds' => '赔率与回水',
|
||||
'risk_cap' => '限额版本',
|
||||
'risk' => '风控',
|
||||
'settlement' => '结算',
|
||||
'jackpot' => 'Jackpot',
|
||||
'jackpot' => '奖池',
|
||||
'reconcile' => '对账',
|
||||
'tickets' => '玩家注单',
|
||||
'audit' => '审计日志',
|
||||
@@ -106,18 +108,26 @@ final class AdminAuthorizationRegistry
|
||||
public static function navigationDefinitions(): array
|
||||
{
|
||||
return [
|
||||
// 总览
|
||||
['segment' => 'dashboard', 'label' => 'Dashboard', 'href' => '/admin'],
|
||||
['segment' => 'admin_users', 'label' => 'Admin Users', 'href' => '/admin/admin-users', 'requiredAny' => ['prd.admin_user.manage']],
|
||||
['segment' => 'admin_roles', 'label' => 'Admin Roles', 'href' => '/admin/admin-roles', 'requiredAny' => ['prd.admin_role.manage']],
|
||||
['segment' => 'players', 'label' => 'Players', 'href' => '/admin/players', 'requiredAny' => ['prd.users.manage', 'prd.users.view_finance', 'prd.users.view_cs', 'prd.player_freeze.manage']],
|
||||
['segment' => 'currencies', 'label' => 'Currencies', 'href' => '/admin/currencies', 'requiredAny' => ['prd.currency.manage']],
|
||||
['segment' => 'wallet', 'label' => 'Wallet', 'href' => '/admin/wallet/transactions', 'activeMatchPrefix' => '/admin/wallet', 'requiredAny' => ['prd.wallet_reconcile.manage', 'prd.wallet_reconcile.view', 'prd.wallet_reconcile.view_cs', 'prd.wallet_adjust.manage', 'prd.users.manage', 'prd.users.view_finance']],
|
||||
// 日常运营:开奖 → 注单 → 玩家
|
||||
['segment' => 'draws', 'label' => 'Draws', 'href' => '/admin/draws', 'requiredAny' => ['prd.draw_result.manage', 'prd.draw_result.view', 'prd.draw_reopen.manage']],
|
||||
['segment' => 'config', 'label' => 'Configuration', 'href' => '/admin/config', 'requiredAny' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.risk_cap.manage', 'prd.risk_cap.view', 'prd.rebate.manage', 'prd.rebate.view', 'prd.jackpot.manage', 'prd.jackpot.view']],
|
||||
['segment' => 'risk', 'label' => 'Risk', 'href' => '/admin/risk', 'requiredAny' => ['prd.draw_result.view', 'prd.draw_result.manage']],
|
||||
['segment' => 'tickets', 'label' => 'Tickets', 'href' => '/admin/tickets', 'requiredAny' => ['prd.users.view_cs', 'prd.users.manage', 'prd.users.view_finance', 'prd.draw_result.view', 'prd.draw_result.manage', 'prd.payout.view', 'prd.payout.review', 'prd.payout.manage']],
|
||||
['segment' => 'players', 'label' => 'Players', 'href' => '/admin/players', 'requiredAny' => ['prd.users.manage', 'prd.users.view_finance', 'prd.users.view_cs', 'prd.player_freeze.manage']],
|
||||
// 规则与参数
|
||||
['segment' => 'rules_plays', 'label' => 'Play rules', 'href' => '/admin/rules/plays', 'requiredAny' => ['prd.play_switch.manage', 'prd.odds.manage']],
|
||||
['segment' => 'rules_odds', 'label' => 'Odds & rebate', 'href' => '/admin/rules/odds', 'requiredAny' => ['prd.odds.manage', 'prd.rebate.manage', 'prd.rebate.view']],
|
||||
['segment' => 'jackpot', 'label' => 'Jackpot', 'href' => '/admin/jackpot', 'activeMatchPrefix' => '/admin/jackpot', 'requiredAny' => ['prd.jackpot.manage', 'prd.jackpot.view']],
|
||||
['segment' => 'risk_cap', 'label' => 'Risk cap rules', 'href' => '/admin/risk/cap', 'activeMatchPrefix' => '/admin/risk/cap', 'requiredAny' => ['prd.risk_cap.manage', 'prd.risk_cap.view']],
|
||||
// 资金
|
||||
['segment' => 'wallet', 'label' => 'Wallet', 'href' => '/admin/wallet/transactions', 'activeMatchPrefix' => '/admin/wallet', 'requiredAny' => ['prd.wallet_reconcile.manage', 'prd.wallet_reconcile.view', 'prd.wallet_reconcile.view_cs', 'prd.wallet_adjust.manage', 'prd.users.manage', 'prd.users.view_finance']],
|
||||
['segment' => 'settlement', 'label' => 'Settlement', 'href' => '/admin/settlement-batches', 'requiredAny' => ['prd.payout.manage', 'prd.payout.review', 'prd.payout.view']],
|
||||
['segment' => 'reconcile', 'label' => 'Reconcile', 'href' => '/admin/reconcile', 'requiredAny' => ['prd.wallet_reconcile.manage', 'prd.wallet_reconcile.view', 'prd.wallet_reconcile.view_cs']],
|
||||
['segment' => 'tickets', 'label' => 'Tickets', 'href' => '/admin/tickets', 'requiredAny' => ['prd.users.view_cs', 'prd.users.manage', 'prd.users.view_finance', 'prd.draw_result.view', 'prd.draw_result.manage', 'prd.payout.view', 'prd.payout.review', 'prd.payout.manage']],
|
||||
['segment' => 'currencies', 'label' => 'Currencies', 'href' => '/admin/currencies', 'requiredAny' => ['prd.currency.manage']],
|
||||
// 权限与系统
|
||||
['segment' => 'admin_users', 'label' => 'Admin Users', 'href' => '/admin/admin-users', 'requiredAny' => ['prd.admin_user.manage']],
|
||||
['segment' => 'admin_roles', 'label' => 'Admin Roles', 'href' => '/admin/admin-roles', 'requiredAny' => ['prd.admin_role.manage']],
|
||||
['segment' => 'risk', 'label' => 'Risk', 'href' => '/admin/risk', 'requiredAny' => ['prd.draw_result.view', 'prd.draw_result.manage']],
|
||||
['segment' => 'audit', 'label' => 'Audit Logs', 'href' => '/admin/audit-logs', 'requiredAny' => ['prd.audit.all', 'prd.audit.self', 'prd.audit.finance']],
|
||||
['segment' => 'settings', 'label' => 'Settings', 'href' => '/admin/settings', 'requiredAny' => ['prd.wallet_reconcile.manage', 'prd.currency.manage']],
|
||||
];
|
||||
@@ -179,7 +189,10 @@ final class AdminAuthorizationRegistry
|
||||
'currencies' => ['prd.currency.manage'],
|
||||
'wallet' => ['prd.wallet_reconcile.manage', 'prd.wallet_reconcile.view', 'prd.wallet_reconcile.view_cs', 'prd.wallet_adjust.manage', 'prd.users.view_finance'],
|
||||
'draws' => ['prd.draw_result.manage', 'prd.draw_result.view', 'prd.draw_reopen.manage'],
|
||||
'config' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.risk_cap.manage', 'prd.risk_cap.view', 'prd.rebate.manage', 'prd.rebate.view', 'prd.jackpot.manage', 'prd.jackpot.view'],
|
||||
'rules_plays' => ['prd.play_switch.manage', 'prd.odds.manage'],
|
||||
'rules_odds' => ['prd.odds.manage', 'prd.rebate.manage', 'prd.rebate.view'],
|
||||
'jackpot' => ['prd.jackpot.manage', 'prd.jackpot.view'],
|
||||
'risk_cap' => ['prd.risk_cap.manage', 'prd.risk_cap.view'],
|
||||
'risk' => ['prd.draw_result.manage', 'prd.draw_result.view'],
|
||||
'settlement' => ['prd.payout.manage', 'prd.payout.review', 'prd.payout.view'],
|
||||
'reconcile' => ['prd.wallet_reconcile.manage', 'prd.wallet_reconcile.view', 'prd.wallet_reconcile.view_cs'],
|
||||
|
||||
@@ -30,6 +30,8 @@ final class OddsStandardScopes
|
||||
*
|
||||
* 仅对「该版本里已出现过的 (play_code, currency_code)」补全,避免给从未配置过的玩法凭空加行。
|
||||
* 若版本下无任何 odds_items,则用当前全部玩法 × 首个可下注币种补全。
|
||||
*
|
||||
* 佣金按维度(2D/3D/4D)配置:同一维度的所有玩法共享佣金率。
|
||||
*/
|
||||
public static function syncMissingForVersion(OddsVersion $version): void
|
||||
{
|
||||
@@ -54,26 +56,45 @@ final class OddsStandardScopes
|
||||
$pairs = PlayType::query()
|
||||
->orderBy('sort_order')
|
||||
->orderBy('play_code')
|
||||
->get(['play_code'])
|
||||
->get(['play_code', 'dimension'])
|
||||
->map(fn (PlayType $pt) => (object) [
|
||||
'play_code' => $pt->play_code,
|
||||
'dimension' => $pt->dimension,
|
||||
'currency_code' => $currencyCode,
|
||||
]);
|
||||
}
|
||||
|
||||
// 按维度分组,获取每个维度的佣金率
|
||||
$dimensionCommissions = [];
|
||||
foreach ($pairs as $pair) {
|
||||
$dimension = $pair->dimension ?? null;
|
||||
$currencyCode = strtoupper((string) $pair->currency_code);
|
||||
$key = $dimension.'|'.$currencyCode;
|
||||
|
||||
if (!isset($dimensionCommissions[$key])) {
|
||||
// 从现有记录中获取该维度的佣金率
|
||||
$anchor = OddsItem::query()
|
||||
->where('version_id', $vid)
|
||||
->where('currency_code', $currencyCode)
|
||||
->where('dimension', $dimension)
|
||||
->orderByDesc('id')
|
||||
->first();
|
||||
|
||||
$dimensionCommissions[$key] = [
|
||||
'rebate' => (float) ($anchor?->rebate_rate ?? 0),
|
||||
'commission' => (float) ($anchor?->commission_rate ?? 0),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($pairs as $pair) {
|
||||
$playCode = (string) $pair->play_code;
|
||||
$dimension = $pair->dimension ?? null;
|
||||
$currencyCode = strtoupper((string) $pair->currency_code);
|
||||
|
||||
$anchor = OddsItem::query()
|
||||
->where('version_id', $vid)
|
||||
->where('play_code', $playCode)
|
||||
->where('currency_code', $currencyCode)
|
||||
->orderByDesc('id')
|
||||
->first();
|
||||
|
||||
$rebate = (float) ($anchor?->rebate_rate ?? 0);
|
||||
$commission = (float) ($anchor?->commission_rate ?? 0);
|
||||
$key = $dimension.'|'.$currencyCode;
|
||||
|
||||
$rebate = $dimensionCommissions[$key]['rebate'] ?? 0;
|
||||
$commission = $dimensionCommissions[$key]['commission'] ?? 0;
|
||||
|
||||
foreach (self::PRESET_ODDS_BY_SCOPE as $scope => $oddsValue) {
|
||||
$exists = OddsItem::query()
|
||||
@@ -90,6 +111,7 @@ final class OddsStandardScopes
|
||||
'version_id' => $vid,
|
||||
'play_code' => $playCode,
|
||||
'prize_scope' => $scope,
|
||||
'dimension' => $dimension,
|
||||
'odds_value' => $oddsValue,
|
||||
'rebate_rate' => $rebate,
|
||||
'commission_rate' => $commission,
|
||||
|
||||
Reference in New Issue
Block a user