- 在多个控制器中更新权限检查逻辑,确保管理员能够更灵活地管理代理和玩家。 - 在 AdminPlayerStoreController 中引入对玩家创建能力的验证,确保只有具备相应权限的管理员能够创建玩家。 - 更新请求验证逻辑,新增 credit_limit、rebate_rate 和 extra_rebate_rate 字段,以支持更细粒度的玩家管理。 - 在 AgentNodeProfileController 中添加对父代理能力授予的验证,确保子代理的权限在父代理范围内。 - 引入 AgentProfileFieldRules 以简化代理资料更新请求的规则定义,提升代码复用性。
284 lines
10 KiB
PHP
284 lines
10 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\AgentOverdueGuard;
|
|
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,
|
|
private readonly AgentCreditAllocatedSyncService $allocatedSync,
|
|
) {}
|
|
|
|
/**
|
|
* @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 ($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<string, mixed>
|
|
*/
|
|
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<string, mixed>|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<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;
|
|
}
|
|
|
|
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'],
|
|
]);
|
|
}
|
|
}
|