$payload */ public function upsertForNode(AgentNode $node, array $payload, ?AgentNode $parent = null): AgentProfile { $parent = $parent ?? ($node->parent_id !== null ? AgentNode::query()->find($node->parent_id) : null); $totalShare = (float) ($payload['total_share_rate'] ?? 0); $creditLimit = (int) ($payload['credit_limit'] ?? 0); $rebateLimit = (float) ($payload['rebate_limit'] ?? 0); $defaultRebate = (float) ($payload['default_player_rebate'] ?? 0); $useRelative = $parent !== null && array_key_exists('relative_share_rate', $payload); // 如果提供了相对占成比例,计算绝对总占成 if ($useRelative) { $relativeShare = (float) $payload['relative_share_rate']; $this->shareRateValidator->assertRelativeShareWithinBounds($relativeShare); $parentRate = $this->shareRateValidator->totalShareRateForNode($parent); $totalShare = round($parentRate * $relativeShare / 100, 2); } if ($parent !== null) { $this->shareRateValidator->assertChildWithinParent( $parent, $totalShare, $useRelative ? 'relative_share_rate' : 'total_share_rate', ); } return DB::transaction(function () use ($node, $payload, $parent, $totalShare, $creditLimit, $rebateLimit, $defaultRebate): AgentProfile { $profile = AgentProfile::query()->firstOrNew(['agent_node_id' => $node->id]); $previousCredit = (int) $profile->credit_limit; $isNew = ! $profile->exists; if ($parent !== null && ! $isNew) { $this->allocatedSync->syncForAgent($parent); } if (! $isNew) { $this->allocatedSync->syncForAgent($node); if ($creditLimit < (int) $profile->allocated_credit) { throw ValidationException::withMessages([ 'credit_limit' => ['below_allocated'], ]); } } if ($parent !== null) { $delta = $isNew ? $creditLimit : max(0, $creditLimit - $previousCredit); if ($delta > 0) { $this->creditAllocationValidator->assertAllocationWithinParent($parent, $delta); } } if ($defaultRebate > $rebateLimit && $rebateLimit > 0) { throw ValidationException::withMessages([ 'default_player_rebate' => ['exceeds_limit'], ]); } $profile->fill([ 'total_share_rate' => $totalShare, 'credit_limit' => $creditLimit, 'rebate_limit' => $rebateLimit, 'default_player_rebate' => $defaultRebate, 'settlement_cycle' => AgentSettlementCycle::normalize( $payload['settlement_cycle'] ?? $profile->settlement_cycle ?? 'weekly', ), 'can_grant_extra_rebate' => (bool) ($payload['can_grant_extra_rebate'] ?? $profile->can_grant_extra_rebate ?? false), 'can_create_child_agent' => (bool) ($payload['can_create_child_agent'] ?? ($isNew ? false : $profile->can_create_child_agent)), 'can_create_player' => (bool) ($payload['can_create_player'] ?? ($isNew ? true : $profile->can_create_player ?? true)), ]); if (! $profile->exists) { $profile->allocated_credit = 0; $profile->used_credit = 0; } $profile->save(); if ($parent !== null) { $this->allocatedSync->syncForAgent($parent); } return $profile; }); } /** * @return array */ public function present(AgentProfile $profile): array { $this->allocatedSync->syncForAgentId((int) $profile->agent_node_id); $profile->refresh(); $available = max(0, (int) $profile->credit_limit - (int) $profile->allocated_credit); return [ 'agent_node_id' => (int) $profile->agent_node_id, 'total_share_rate' => (float) $profile->total_share_rate, 'credit_limit' => (int) $profile->credit_limit, 'allocated_credit' => (int) $profile->allocated_credit, 'used_credit' => (int) $profile->used_credit, 'available_credit' => $available, 'rebate_limit' => (float) $profile->rebate_limit, 'default_player_rebate' => (float) $profile->default_player_rebate, 'settlement_cycle' => AgentSettlementCycle::normalize($profile->settlement_cycle), 'can_grant_extra_rebate' => (bool) $profile->can_grant_extra_rebate, 'can_create_child_agent' => (bool) $profile->can_create_child_agent, 'can_create_player' => (bool) $profile->can_create_player, ]; } public function profileForNode(int $agentNodeId): ?AgentProfile { return AgentProfile::query()->where('agent_node_id', $agentNodeId)->first(); } /** * @return array|null */ public function parentCapsForNode(?AgentNode $parent): ?array { if ($parent === null) { return null; } $this->allocatedSync->syncForAgent($parent); $profile = $this->profileForNode((int) $parent->id); if ($profile === null) { return null; } return [ 'agent_node_id' => (int) $parent->id, 'total_share_rate' => (float) $profile->total_share_rate, 'rebate_limit' => (float) $profile->rebate_limit, 'available_credit' => max(0, (int) $profile->credit_limit - (int) $profile->allocated_credit), ]; } /** 玩家授信写入前:校验代理可下发是否足够(按当前库内已占用重算)。 */ public function assertMayIncreasePlayerCredit(AgentNode $agent, int $additionalCredit): void { if ($additionalCredit <= 0) { return; } $this->assertAgentProfileExists($agent); $this->allocatedSync->syncForAgent($agent); $this->creditAllocationValidator->assertPlayerCreditDeltaWithinAgent($agent, $additionalCredit); } /** 玩家授信变更后:按直属玩家+直属下级代理重算已下发额度(无 profile 时跳过)。 */ public function refreshAllocatedCredit(AgentNode $agent): void { $this->allocatedSync->syncForAgent($agent); } public function adjustPlayerCreditAllocation(AgentNode $agent, int $previousLimit, int $newLimit, int $playerUsedCredit = 0): void { if ($newLimit < $playerUsedCredit) { throw ValidationException::withMessages([ 'credit_limit' => ['below_player_used'], ]); } $delta = $newLimit - $previousLimit; $this->assertAgentProfileExists($agent); $this->allocatedSync->syncForAgent($agent); if ($delta > 0) { $this->creditAllocationValidator->assertPlayerCreditDeltaWithinAgent($agent, $delta); } } public function assertActorMayCreateChildAgent(AdminUser $admin): void { if ($admin->isSuperAdmin()) { return; } $node = AdminAgentScope::primaryAgentNode($admin); if ($node === null) { return; } if (! $this->nodeMayCreateChildAgent($node->id)) { throw ValidationException::withMessages([ 'parent_id' => ['cannot_create_child_agent'], ]); } if (AgentOverdueGuard::agentHasOverdueBills((int) $node->id)) { throw ValidationException::withMessages([ 'parent_id' => ['agent_overdue'], ]); } } public function assertActorMayCreatePlayer(AdminUser $admin): void { if ($admin->isSuperAdmin()) { return; } $node = AdminAgentScope::primaryAgentNode($admin); if ($node === null) { return; } if (! $this->nodeMayCreatePlayer($node->id)) { throw ValidationException::withMessages([ 'site_code' => ['cannot_create_player'], ]); } if (AgentOverdueGuard::agentHasOverdueBills((int) $node->id)) { throw ValidationException::withMessages([ 'site_code' => ['agent_overdue'], ]); } } /** * @param array $childPayload */ public function assertChildCapabilityGrantsWithinParent(AgentNode $parent, array $childPayload, AdminUser $actor): void { if ($actor->isSuperAdmin()) { return; } $parentProfile = $this->profileForNode((int) $parent->id); if ((bool) ($childPayload['can_create_child_agent'] ?? false) && ! ($parentProfile?->can_create_child_agent ?? false)) { throw ValidationException::withMessages([ 'can_create_child_agent' => ['parent_cannot_delegate'], ]); } if ((bool) ($childPayload['can_create_player'] ?? true) && ! ($parentProfile?->can_create_player ?? false)) { throw ValidationException::withMessages([ 'can_create_player' => ['parent_cannot_delegate'], ]); } } public function nodeMayCreateChildAgent(int $agentNodeId): bool { $profile = $this->profileForNode($agentNodeId); return $profile === null || $profile->can_create_child_agent; } public function nodeMayCreatePlayer(int $agentNodeId): bool { $profile = $this->profileForNode($agentNodeId); return $profile === null || $profile->can_create_player; } private function assertAgentProfileExists(AgentNode $agent): void { if (AgentProfile::query()->where('agent_node_id', $agent->id)->exists()) { return; } throw ValidationException::withMessages([ 'credit_limit' => ['agent_profile_required'], ]); } }