| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216 |
- <?php
- namespace App\Services\Agent;
- use App\Models\Agent\Agent;
- use App\Models\Agent\AgentProfitCommissionProfile;
- use Illuminate\Database\Eloquent\Builder;
- use Illuminate\Support\Facades\DB;
- class AgentProfitCommissionProfileService
- {
- public function __construct(private AgentProfitCommissionAssignmentService $assignments)
- {
- }
- public function paginate(array $params, ?Agent $owner = null): array
- {
- $page = max(1, (int) ($params['page'] ?? 1));
- $limit = min(100, max(1, (int) ($params['limit'] ?? 20)));
- $query = $this->ownedQuery($owner);
- if (!empty($params['name'])) $query->where('name', 'like', '%' . $params['name'] . '%');
- $total = (clone $query)->count();
- $list = $query->orderByDesc('is_default')->orderByDesc('id')->forPage($page, $limit)->get();
- if ($owner) {
- $list->each(fn (AgentProfitCommissionProfile $profile) => $profile->setAttribute('editable', true));
- }
- return compact('total', 'page', 'limit', 'list');
- }
- public function paginateSelectable(Agent $agent, array $params): array
- {
- $agent->loadMissing('profitCommissionProfile');
- if (!$agent->profitCommissionProfile) throw new \RuntimeException('当前代理未关联盈亏佣金方案');
- $page = max(1, (int) ($params['page'] ?? 1));
- $limit = min(100, max(1, (int) ($params['limit'] ?? 20)));
- $query = AgentProfitCommissionProfile::query()->where(function (Builder $query) use ($agent) {
- $query->whereNull('owner_agent_id')->orWhere('owner_agent_id', $agent->id);
- });
- foreach (AgentProfitCommissionProfile::RATE_FIELDS as $field) {
- $query->where($field, '<=', (string) $agent->profitCommissionProfile->{$field});
- }
- if (!empty($params['name'])) $query->where('name', 'like', '%' . $params['name'] . '%');
- $total = (clone $query)->count();
- $list = $query->orderByRaw('owner_agent_id is null')->orderByDesc('is_default')->orderByDesc('id')
- ->forPage($page, $limit)->get();
- $list->each(function (AgentProfitCommissionProfile $profile) use ($agent) {
- $profile->setAttribute('editable', $profile->isOwnedBy((int) $agent->id));
- });
- return compact('total', 'page', 'limit', 'list');
- }
- public function save(array $data, ?int $adminId = null, ?Agent $owner = null): AgentProfitCommissionProfile
- {
- return DB::transaction(function () use ($data, $adminId, $owner) {
- $profiles = $this->ownedQuery($owner)->orderBy('id')->lockForUpdate()->get(['id', 'is_default']);
- $profile = empty($data['id'])
- ? new AgentProfitCommissionProfile()
- : AgentProfitCommissionProfile::query()->lockForUpdate()->findOrFail($data['id']);
- if ($profile->exists) $this->assertOwned($profile, $owner);
- else $profile->owner_agent_id = $owner?->id;
- $oldDefaultId = $profiles->firstWhere('is_default', 1)?->id;
- $isDefault = !empty($data['is_default']);
- if ($profile->exists && (int) $oldDefaultId === (int) $profile->id && !$isDefault) {
- throw new \RuntimeException('默认比例必须始终保留一条,请先将其他比例设为默认');
- }
- $name = trim((string) $data['name']);
- if ($name === '') throw new \InvalidArgumentException('盈亏比例名称不能为空');
- $duplicate = $this->ownedQuery($owner)->where('name', $name)
- ->when($profile->exists, fn ($query) => $query->where('id', '<>', $profile->id))->exists();
- if ($duplicate) throw new \InvalidArgumentException('盈亏比例名称已存在');
- $profile->name = $name;
- foreach (AgentProfitCommissionProfile::RATE_FIELDS as $field) $profile->{$field} = (string) $data[$field];
- if ($owner) $this->assertRatesWithinOwner($owner, $profile);
- $willBeDefault = $isDefault || (!$profile->exists && !$oldDefaultId);
- $profile->is_default = $willBeDefault ? 1 : 0;
- $snapshotChanged = !$profile->exists || $profile->isDirty(AgentProfitCommissionProfile::RATE_FIELDS);
- $affectedAgentIds = [];
- if ($snapshotChanged && $profile->exists) {
- $affectedAgentIds = Agent::query()->where('profit_commission_profile_id', $profile->id)
- ->pluck('id')->map(fn ($id) => (int) $id)->all();
- }
- if ($willBeDefault) {
- $defaultAgentIds = $this->defaultMigrationQuery($oldDefaultId, $profile, $owner)
- ->pluck('id')->map(fn ($id) => (int) $id)->all();
- $affectedAgentIds = array_values(array_unique(array_merge($affectedAgentIds, $defaultAgentIds)));
- }
- $this->assertHierarchy($affectedAgentIds, $profile);
- $profile->save();
- if ($willBeDefault) {
- $this->ownedQuery($owner)->where('id', '<>', $profile->id)->update(['is_default' => 0]);
- $this->defaultMigrationQuery($oldDefaultId, $profile, $owner)
- ->update(['profit_commission_profile_id' => $profile->id]);
- }
- $this->assignments->assignMany($affectedAgentIds, $profile, now()->toDateString(), $adminId);
- return $profile->fresh();
- });
- }
- public function delete(int $id, ?int $adminId = null, ?Agent $owner = null): void
- {
- DB::transaction(function () use ($id, $adminId, $owner) {
- $profile = AgentProfitCommissionProfile::query()->lockForUpdate()->findOrFail($id);
- $this->assertOwned($profile, $owner);
- if ((int) $profile->is_default === 1) throw new \RuntimeException('默认比例不能删除');
- $default = $this->ownedQuery($owner)->where('is_default', 1)->lockForUpdate()->first();
- if (!$default) throw new \RuntimeException('缺少默认比例,不能删除');
- $agentIds = $this->agentsUsingProfile($profile->id, $owner);
- $this->assertHierarchy($agentIds, $default);
- $this->assignments->assignMany($agentIds, $default, now()->toDateString(), $adminId);
- Agent::query()->where('profit_commission_profile_id', $profile->id)
- ->when($owner, fn ($query) => $query->where('parent_id', $owner->id))
- ->update(['profit_commission_profile_id' => $default->id]);
- $profile->delete();
- });
- }
- private function ownedQuery(?Agent $owner): Builder
- {
- $query = AgentProfitCommissionProfile::query();
- return $owner
- ? $query->where('owner_agent_id', $owner->id)
- : $query->whereNull('owner_agent_id');
- }
- private function defaultMigrationQuery(?int $oldDefaultId, AgentProfitCommissionProfile $profile, ?Agent $owner): Builder
- {
- $query = Agent::query()->where(function (Builder $query) use ($oldDefaultId, $profile) {
- $query->whereNull('profit_commission_profile_id');
- if ($oldDefaultId && (int) $oldDefaultId !== (int) $profile->id) {
- $query->orWhere('profit_commission_profile_id', $oldDefaultId);
- }
- });
- return $owner ? $query->where('parent_id', $owner->id) : $query;
- }
- private function agentsUsingProfile(int $profileId, ?Agent $owner): array
- {
- $ids = Agent::query()->where('profit_commission_profile_id', $profileId)
- ->pluck('id')->map(fn ($agentId) => (int) $agentId);
- if ($owner) {
- $childIds = Agent::query()->where('parent_id', $owner->id)
- ->pluck('id')->map(fn ($agentId) => (int) $agentId);
- if ($ids->diff($childIds)->isNotEmpty()) {
- throw new \RuntimeException('该盈亏方案仍被非直属下级使用,不能删除');
- }
- return $ids->intersect($childIds)->values()->all();
- }
- return $ids->all();
- }
- private function assertOwned(AgentProfitCommissionProfile $profile, ?Agent $owner): void
- {
- if ($owner) {
- if (!$profile->isOwnedBy((int) $owner->id)) {
- throw new \RuntimeException('无权操作该盈亏方案');
- }
- return;
- }
- if (!$profile->isOfficial()) {
- throw new \RuntimeException('总后台只能维护官方盈亏方案');
- }
- }
- private function assertRatesWithinOwner(Agent $owner, AgentProfitCommissionProfile $profile): void
- {
- $owner->loadMissing('profitCommissionProfile');
- if (!$owner->profitCommissionProfile) throw new \RuntimeException('当前代理未关联盈亏佣金方案');
- foreach (AgentProfitCommissionProfile::RATE_FIELDS as $field) {
- if (bccomp((string) $profile->{$field}, (string) $owner->profitCommissionProfile->{$field}, 4) > 0) {
- throw new \RuntimeException('下级盈亏方案不能高于当前代理方案');
- }
- }
- }
- private function assertHierarchy(array $agentIds, AgentProfitCommissionProfile $candidate): void
- {
- $agentIds = array_values(array_unique(array_map('intval', $agentIds)));
- if ($agentIds === []) return;
- $affected = array_fill_keys($agentIds, true);
- $agents = Agent::query()->with([
- 'parent.profitCommissionProfile',
- 'children.profitCommissionProfile',
- ])->whereIn('id', $agentIds)->get();
- foreach ($agents as $agent) {
- if ($agent->parent) {
- foreach (AgentProfitCommissionProfile::RATE_FIELDS as $field) {
- if (!isset($affected[(int) $agent->parent->id]) && !$agent->parent->profitCommissionProfile) {
- throw new \RuntimeException('上级代理未关联盈亏佣金方案');
- }
- $parentRate = isset($affected[(int) $agent->parent->id])
- ? (string) $candidate->{$field}
- : (string) $agent->parent->profitCommissionProfile->{$field};
- if (bccomp((string) $candidate->{$field}, $parentRate, 4) > 0) {
- throw new \RuntimeException('修改后的盈亏方案会高于上级代理方案');
- }
- }
- }
- foreach ($agent->children as $child) {
- foreach (AgentProfitCommissionProfile::RATE_FIELDS as $field) {
- if (!isset($affected[(int) $child->id]) && !$child->profitCommissionProfile) {
- throw new \RuntimeException('下级代理未关联盈亏佣金方案');
- }
- $childRate = isset($affected[(int) $child->id])
- ? (string) $candidate->{$field}
- : (string) $child->profitCommissionProfile->{$field};
- if (bccomp((string) $candidate->{$field}, $childRate, 4) < 0) {
- throw new \RuntimeException('修改后的盈亏方案会低于下级代理方案');
- }
- }
- }
- }
- }
- }
|