Files
lotteryLaravel/app/Services/Agent/AgentProfileService.php
kang 96545f87f6 feat: 增强代理节点和代理资料管理功能
- 在 AgentNodeProfileController 中添加对父代理能力授予的验证,确保子代理的权限在父代理范围内。
- 更新多个请求类,统一代理资料字段的验证逻辑,提升代码复用性。
- 引入 AgentProfileFieldRules 以简化代理资料更新请求的规则定义。
- 在 AgentProfile 模型中设置主键为 agent_node_id,确保与代理节点的关联性。
- 更新错误信息,增加对授信额度和占成比例的验证,确保数据一致性。
2026-06-04 10:15:10 +08:00

197 lines
7.3 KiB
PHP

<?php
namespace App\Services\Agent;
use App\Models\AdminUser;
use App\Models\AgentNode;
use App\Models\AgentProfile;
use App\Support\AdminAgentScope;
use App\Support\AgentSettlementCycle;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
final class AgentProfileService
{
public function __construct(
private readonly ShareRateValidator $shareRateValidator,
private readonly CreditAllocationValidator $creditAllocationValidator,
private readonly RebateLimitValidator $rebateLimitValidator,
) {}
/**
* @param array<string, mixed> $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);
if ($parent !== null) {
$this->shareRateValidator->assertChildWithinParent($parent, $totalShare);
}
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 (! $isNew && $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 \Illuminate\Validation\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) {
$parentProfile = AgentProfile::query()->where('agent_node_id', $parent->id)->first();
if ($parentProfile !== null) {
$creditDelta = $isNew ? $creditLimit : ($creditLimit - $previousCredit);
if ($creditDelta !== 0) {
$parentProfile->allocated_credit = max(0, (int) $parentProfile->allocated_credit + $creditDelta);
$parentProfile->save();
}
}
}
return $profile;
});
}
/**
* @return array<string, mixed>
*/
public function present(AgentProfile $profile): array
{
$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();
}
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'],
]);
}
}
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'],
]);
}
}
/**
* @param array<string, mixed> $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;
}
}