level !== Agent::LEVEL_ONE) { throw new \RuntimeException('二级代理不能继续新增下级代理'); } if ($parentId && $parentId !== (int) $scopeAgent->id) { throw new \RuntimeException('一级代理只能新增自己的直属二级代理'); } $parentId = (int) $scopeAgent->id; } elseif ($parentId) { throw new \RuntimeException('总后台只能新增一级代理'); } $parent = $parentId ? Agent::query()->lockForUpdate()->findOrFail($parentId) : null; if ($scopeAgent && $parent && (int) $parent->status !== Agent::STATUS_ENABLED) { throw new \RuntimeException('不能在已禁用代理下新增账号'); } $level = $parent ? ((int) $parent->level + 1) : Agent::LEVEL_ONE; if ($level > Agent::MAX_LEVEL) { throw new \RuntimeException('代理层级最多为二级'); } $flowProfileId = (int)($data['flow_commission_profile_id'] ?? $parent?->flow_commission_profile_id ?? AgentFlowCommissionProfile::officialDefaultId()); if ($flowProfileId <= 0) throw new \RuntimeException('请先配置默认代理流水佣金方案'); $flowProfile = AgentFlowCommissionProfile::query()->findOrFail($flowProfileId); $this->assertProfileAssignable($parent, $flowProfile, $scopeAgent); $profitProfileId = (int) ($data['profit_commission_profile_id'] ?? $parent?->profit_commission_profile_id ?? AgentProfitCommissionProfile::officialDefaultId()); if ($profitProfileId <= 0) throw new \RuntimeException('请先配置默认代理盈亏佣金比例'); $profitProfile = AgentProfitCommissionProfile::query()->findOrFail($profitProfileId); $this->assertProfileAssignable($parent, $profitProfile, $scopeAgent); $agent = Agent::query()->create([ 'parent_id' => $parent?->id, 'path' => '/', 'level' => $level, 'username' => trim((string) $data['username']), 'password' => Hash::make((string) $data['password']), 'withdraw_password' => empty($data['withdraw_password']) ? '' : Hash::make((string) $data['withdraw_password']), 'real_name' => trim((string) ($data['real_name'] ?? '')), 'invitation_code' => $this->invitationCode(), 'deposit_fee_rate' => (string) ($data['deposit_fee_rate'] ?? 0), 'withdraw_fee_rate' => (string) ($data['withdraw_fee_rate'] ?? 0), 'profit_commission_rate' => (string) ($data['profit_commission_rate'] ?? 0), 'flow_commission_profile_id' => $flowProfileId, 'profit_commission_profile_id' => $profitProfileId, 'status' => (int) ($data['status'] ?? Agent::STATUS_ENABLED), 'created_by' => $createdBy, ]); $agent->path = ($parent ? rtrim((string) $parent->path, '/') . '/' : '/') . $agent->id . '/'; $agent->save(); $this->flowAssignments->assign($agent, $flowProfile, now()->toDateString(), $createdBy); $this->profitAssignments->assign($agent, $profitProfile, now()->toDateString(), $createdBy); return $agent->fresh(['flowCommissionProfile', 'profitCommissionProfile']); }); } public function update(Agent $agent, array $data, ?Agent $scopeAgent = null, ?int $createdBy = null): Agent { return DB::transaction(function () use ($agent, $data, $scopeAgent, $createdBy): Agent { $agent = Agent::query()->lockForUpdate()->findOrFail($agent->id); $newFlowProfile = null; if (array_key_exists('flow_commission_profile_id', $data) && $data['flow_commission_profile_id'] !== null && (int)$data['flow_commission_profile_id'] !== (int)$agent->flow_commission_profile_id) { $newFlowProfile = AgentFlowCommissionProfile::query() ->findOrFail((int)$data['flow_commission_profile_id']); $this->assertFlowProfileCoversChildren($agent, $newFlowProfile); } $newProfitProfile = null; if (array_key_exists('profit_commission_profile_id', $data) && $data['profit_commission_profile_id'] !== null && (int) $data['profit_commission_profile_id'] !== (int) $agent->profit_commission_profile_id) { $newProfitProfile = AgentProfitCommissionProfile::query() ->findOrFail((int) $data['profit_commission_profile_id']); $this->assertProfitProfileCoversChildren($agent, $newProfitProfile); } if ($scopeAgent) { $this->assertVisible($scopeAgent, (int) $agent->id); } elseif (array_key_exists('parent_id', $data)) { $this->move( $agent, $data['parent_id'] === null ? null : (int)$data['parent_id'], $newFlowProfile, $newProfitProfile ); } $parent = $agent->parent_id ? Agent::query()->findOrFail($agent->parent_id) : null; if ($newFlowProfile) $this->assertProfileAssignable($parent, $newFlowProfile, $scopeAgent); if ($newProfitProfile) $this->assertProfileAssignable($parent, $newProfitProfile, $scopeAgent); if (array_key_exists('real_name', $data)) { $agent->real_name = trim((string) ($data['real_name'] ?? '')); } foreach (['username', 'status', 'deposit_fee_rate', 'withdraw_fee_rate', 'flow_commission_profile_id', 'profit_commission_profile_id'] as $field) { if (array_key_exists($field, $data) && $data[$field] !== null) { $agent->{$field} = $data[$field]; } } if (!empty($data['password'])) { $agent->password = Hash::make((string) $data['password']); } if (!empty($data['withdraw_password'])) { $agent->withdraw_password = Hash::make((string) $data['withdraw_password']); } $agent->save(); if (!empty($data['password']) || (int) $agent->status === Agent::STATUS_DISABLED) { AgentToken::query()->where('agent_id', $agent->id)->delete(); } if ($newFlowProfile) { $this->flowAssignments->assign($agent, $newFlowProfile, now()->toDateString(), $createdBy); } if ($newProfitProfile) { $this->profitAssignments->assign($agent, $newProfitProfile, now()->toDateString(), $createdBy); } return $agent->fresh(['flowCommissionProfile', 'profitCommissionProfile']); }); } public function assertVisible(Agent $root, int $targetId): void { if ((int) $root->id === $targetId) { return; } $visible = Agent::query()->whereKey($targetId)->where('path', 'like', rtrim((string) $root->path, '/') . '/%')->exists(); if (!$visible) { throw new \RuntimeException('无权访问该代理'); } } private function invitationCode(): string { do { $code = strtoupper(Str::random(10)); } while (Agent::query()->where('invitation_code', $code)->exists()); return $code; } private function assertProfileAssignable( ?Agent $parent, AgentFlowCommissionProfile|AgentProfitCommissionProfile $profile, ?Agent $scopeAgent = null ): void { $isFlow = $profile instanceof AgentFlowCommissionProfile; if ($scopeAgent) { if (!$parent) throw new \RuntimeException('一级代理只能给直属下级指定方案'); if (!$profile->isOfficial() && !$profile->isOwnedBy((int) $parent->id)) { throw new \RuntimeException($isFlow ? '只能使用官方方案或自己创建的流水方案' : '只能使用官方方案或自己创建的盈亏方案'); } } elseif (!$profile->isOfficial()) { throw new \RuntimeException($isFlow ? '总后台只能给代理指定官方流水方案' : '总后台只能给代理指定官方盈亏方案'); } if ($parent) { $isFlow ? $this->assertFlowProfileWithinParent($parent, $profile) : $this->assertProfitProfileWithinParent($parent, $profile); } } private function assertFlowProfileWithinParent(Agent $parent, AgentFlowCommissionProfile $profile): void { $parent->loadMissing('flowCommissionProfile'); foreach (AgentFlowCommissionProfile::RATE_FIELDS as $field) { if (!$parent->flowCommissionProfile) throw new \RuntimeException('上级代理未关联流水佣金方案'); $parentRate = (string)$parent->flowCommissionProfile->{$field}; if (bccomp((string)$profile->{$field}, $parentRate, 4) > 0) { throw new \RuntimeException('下级代理流水佣金方案不能高于上级方案'); } } } private function assertProfitProfileWithinParent(Agent $parent, AgentProfitCommissionProfile $profile): void { $parent->loadMissing('profitCommissionProfile'); foreach (AgentProfitCommissionProfile::RATE_FIELDS as $field) { if (!$parent->profitCommissionProfile) throw new \RuntimeException('上级代理未关联盈亏佣金方案'); $parentRate = (string) $parent->profitCommissionProfile->{$field}; if (bccomp((string) $profile->{$field}, $parentRate, 4) > 0) { throw new \RuntimeException('下级代理盈亏佣金方案不能高于上级方案'); } } } private function assertFlowProfileCoversChildren(Agent $agent, AgentFlowCommissionProfile $profile): void { $children = Agent::query()->with('flowCommissionProfile')->where('parent_id', $agent->id)->get(); foreach ($children as $child) { foreach (AgentFlowCommissionProfile::RATE_FIELDS as $field) { if (!$child->flowCommissionProfile) throw new \RuntimeException('下级代理未关联流水佣金方案'); $childRate = (string)$child->flowCommissionProfile->{$field}; if (bccomp((string)$profile->{$field}, $childRate, 4) < 0) { throw new \RuntimeException('代理流水佣金方案不能低于现有下级方案'); } } } } private function assertProfitProfileCoversChildren(Agent $agent, AgentProfitCommissionProfile $profile): void { $children = Agent::query()->with('profitCommissionProfile')->where('parent_id', $agent->id)->get(); foreach ($children as $child) { foreach (AgentProfitCommissionProfile::RATE_FIELDS as $field) { if (!$child->profitCommissionProfile) throw new \RuntimeException('下级代理未关联盈亏佣金方案'); $childRate = (string) $child->profitCommissionProfile->{$field}; if (bccomp((string) $profile->{$field}, $childRate, 4) < 0) { throw new \RuntimeException('代理盈亏佣金方案不能低于现有下级方案'); } } } } private function move( Agent $agent, ?int $parentId, ?AgentFlowCommissionProfile $flowProfile = null, ?AgentProfitCommissionProfile $profitProfile = null ): void { if ($parentId === (int) $agent->id) { throw new \RuntimeException('上级代理不能是自己'); } $currentParentId = $agent->parent_id === null ? null : (int) $agent->parent_id; if ($parentId === $currentParentId) { return; } $parent = $parentId ? Agent::query()->lockForUpdate()->findOrFail($parentId) : null; if ($parent && str_starts_with((string) $parent->path, (string) $agent->path)) { throw new \RuntimeException('不能把代理移动到自己的下级'); } if ($parent && (int) $parent->level !== Agent::LEVEL_ONE) { throw new \RuntimeException('二级代理不能作为上级代理'); } if ($parent) { $agent->loadMissing(['flowCommissionProfile', 'profitCommissionProfile']); $effectiveFlowProfile = $flowProfile ?: $agent->flowCommissionProfile; if (!$effectiveFlowProfile) throw new \RuntimeException('代理未关联流水佣金方案'); $this->assertProfileAssignable($parent, $effectiveFlowProfile); $effectiveProfitProfile = $profitProfile ?: $agent->profitCommissionProfile; if (!$effectiveProfitProfile) throw new \RuntimeException('代理未关联盈亏佣金方案'); $this->assertProfileAssignable($parent, $effectiveProfitProfile); } else { $agent->loadMissing(['flowCommissionProfile', 'profitCommissionProfile']); $effectiveFlowProfile = $flowProfile ?: $agent->flowCommissionProfile; if ($effectiveFlowProfile && !$effectiveFlowProfile->isOfficial()) { throw new \RuntimeException('一级代理只能使用官方流水方案'); } $effectiveProfitProfile = $profitProfile ?: $agent->profitCommissionProfile; if ($effectiveProfitProfile && !$effectiveProfitProfile->isOfficial()) { throw new \RuntimeException('一级代理只能使用官方盈亏方案'); } } $oldPath = (string) $agent->path; $oldLevel = (int) $agent->level; $newLevel = $parent ? ((int) $parent->level + 1) : 1; $newPath = ($parent ? rtrim((string) $parent->path, '/') . '/' : '/') . $agent->id . '/'; $descendants = Agent::query()->where('path', 'like', rtrim($oldPath, '/') . '/%')->lockForUpdate()->get(); $newMaxLevel = $descendants->max(fn(Agent $descendant) => (int) $descendant->level + $newLevel - $oldLevel ) ?? $newLevel; if ($newLevel > Agent::MAX_LEVEL || $newMaxLevel > Agent::MAX_LEVEL) { throw new \RuntimeException('移动后会产生三级代理,操作已拒绝'); } $agent->parent_id = $parent?->id; $agent->path = $newPath; $agent->level = $newLevel; foreach ($descendants as $descendant) { $descendant->path = $newPath . substr((string) $descendant->path, strlen($oldPath)); $descendant->level = max(1, (int) $descendant->level + $newLevel - $oldLevel); $descendant->save(); } } }