feat(agent-profile): 限制代理及玩家返点和分成范围,增强权限校验
Some checks failed
lotterLaravel CI / test (push) Has been cancelled
lotterLaravel E2E / e2e-api (push) Has been cancelled

- 添加ensureSuperAdmin方法,限制管理员设置操作权限
- 在AdminSettingController接口中新增权限检查,防止非超级管理员操作
- AdminPlayerIndexController新增direct agent筛选支持
- AdminPlayerUpdateController新增对信用玩家默认币别变更的拒绝逻辑
- 新增WalletSettlementBillsController,实现玩家信用盘账期账单摘要接口
- AgentProfileService调整,新增返点限额和分享比例自动下调机制,保持子代理及玩家配置不超父级
- AgentProfileService增加can_grant_extra_rebate权限继承限制,阻止无权限代理开启
- AgentSettlementPeriodCloseService增加玩家账单回水信用释放逻辑,确保信用额度同步
- PlayerCreditService新增释放账单回水对应信用逻辑,维护账期信用一致性
- TicketPlacementService和TicketPreviewService新增信用玩家投注币别匹配校验,防止币别不符
- PlayerLedgerLogsService优化信用额度计算,增加可用额度上下限限制,防止负值和超限
- 调整后台管理导航,仅超管可见设置入口,强化权限隔离
- 路由新增玩家信用账单查询接口
- 补充多项AgentProfile相关单元测试覆盖额度限制、返点继承、返点下调场景及权限限制
- 增加站点管理员登录测试,验证系统设置菜单不可见,提升用户权限体验
This commit is contained in:
2026-07-01 15:35:46 +08:00
parent ed5a983003
commit 992195b00c
21 changed files with 1218 additions and 35 deletions

View File

@@ -118,6 +118,17 @@ final class AgentProfileService
$profile->used_credit = 0;
}
$profile->save();
$this->clampDescendantAgentShareRates($node, (float) $profile->total_share_rate);
$this->clampDirectPlayerRebates(
$node,
(float) $profile->rebate_limit,
(bool) $profile->can_grant_extra_rebate,
);
$this->clampDescendantAgentRebates(
$node,
(float) $profile->rebate_limit,
(bool) $profile->can_grant_extra_rebate,
);
if ($parent !== null) {
$this->allocatedSync->syncForAgent($parent);
@@ -290,11 +301,18 @@ final class AgentProfileService
*/
public function assertChildCapabilityGrantsWithinParent(AgentNode $parent, array $childPayload, AdminUser $actor): void
{
$parentProfile = $this->profileForNode((int) $parent->id);
if ((bool) ($childPayload['can_grant_extra_rebate'] ?? false)
&& ! ($parentProfile?->can_grant_extra_rebate ?? false)) {
throw ValidationException::withMessages([
'can_grant_extra_rebate' => ['parent_cannot_delegate'],
]);
}
if ($actor->isSuperAdmin() || \App\Support\AdminAgentSettlementScope::canManageSitePeriods($actor)) {
return;
}
$parentProfile = $this->profileForNode((int) $parent->id);
if ((bool) ($childPayload['can_create_child_agent'] ?? false)
&& ! ($parentProfile?->can_create_child_agent ?? false)) {
throw ValidationException::withMessages([
@@ -324,6 +342,113 @@ final class AgentProfileService
return (bool) ($profile?->can_create_player ?? false);
}
private function clampDescendantAgentShareRates(AgentNode $parent, float $parentTotalShareRate): void
{
$children = AgentNode::query()
->where('parent_id', (int) $parent->id)
->orderBy('id')
->get();
foreach ($children as $child) {
$profile = AgentProfile::query()
->where('agent_node_id', (int) $child->id)
->first();
if ($profile === null) {
continue;
}
$totalShareRate = max(0.0, (float) $profile->total_share_rate);
$nextTotalShareRate = min($totalShareRate, $parentTotalShareRate);
if (abs($nextTotalShareRate - $totalShareRate) >= 1e-9) {
$profile->forceFill([
'total_share_rate' => $nextTotalShareRate,
])->save();
}
$this->clampDescendantAgentShareRates($child, $nextTotalShareRate);
}
}
private function clampDirectPlayerRebates(AgentNode $agent, float $rebateLimit, bool $canGrantExtraRebate): void
{
$rows = DB::table('player_rebate_profiles as prp')
->join('players as p', 'p.id', '=', 'prp.player_id')
->where('p.agent_node_id', (int) $agent->id)
->where('prp.inherit_from_agent', false)
->select([
'prp.id',
'prp.rebate_rate',
'prp.extra_rebate_rate',
])
->get();
$now = now();
foreach ($rows as $row) {
$rebateRate = max(0.0, (float) $row->rebate_rate);
$extraRate = max(0.0, (float) $row->extra_rebate_rate);
$nextRebate = min($rebateRate, $rebateLimit);
$remaining = max(0.0, $rebateLimit - $nextRebate);
$nextExtra = $canGrantExtraRebate ? min($extraRate, $remaining) : 0.0;
if (abs($nextRebate - $rebateRate) < 1e-9 && abs($nextExtra - $extraRate) < 1e-9) {
continue;
}
DB::table('player_rebate_profiles')
->where('id', (int) $row->id)
->update([
'rebate_rate' => $nextRebate,
'extra_rebate_rate' => $nextExtra,
'updated_at' => $now,
]);
}
}
private function clampDescendantAgentRebates(
AgentNode $parent,
float $parentRebateLimit,
bool $parentCanGrantExtraRebate,
): void {
$children = AgentNode::query()
->where('parent_id', (int) $parent->id)
->orderBy('id')
->get();
foreach ($children as $child) {
$profile = AgentProfile::query()
->where('agent_node_id', (int) $child->id)
->first();
if ($profile === null) {
continue;
}
$rebateLimit = max(0.0, (float) $profile->rebate_limit);
$defaultRebate = max(0.0, (float) $profile->default_player_rebate);
$canGrantExtraRebate = (bool) $profile->can_grant_extra_rebate;
$nextRebateLimit = min($rebateLimit, $parentRebateLimit);
$nextDefaultRebate = min($defaultRebate, $nextRebateLimit);
$nextCanGrantExtraRebate = $parentCanGrantExtraRebate && $canGrantExtraRebate;
if (abs($nextRebateLimit - $rebateLimit) >= 1e-9
|| abs($nextDefaultRebate - $defaultRebate) >= 1e-9
|| $nextCanGrantExtraRebate !== $canGrantExtraRebate) {
$profile->forceFill([
'rebate_limit' => $nextRebateLimit,
'default_player_rebate' => $nextDefaultRebate,
'can_grant_extra_rebate' => $nextCanGrantExtraRebate,
])->save();
}
$this->clampDirectPlayerRebates($child, $nextRebateLimit, $nextCanGrantExtraRebate);
$this->clampDescendantAgentRebates($child, $nextRebateLimit, $nextCanGrantExtraRebate);
}
}
private function assertAgentProfileExists(AgentNode $agent): void
{
if (AgentProfile::query()->where('agent_node_id', $agent->id)->exists()) {