AgentProfitCommissionProfileService.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. <?php
  2. namespace App\Services\Agent;
  3. use App\Models\Agent\Agent;
  4. use App\Models\Agent\AgentProfitCommissionProfile;
  5. use Illuminate\Database\Eloquent\Builder;
  6. use Illuminate\Support\Facades\DB;
  7. class AgentProfitCommissionProfileService
  8. {
  9. public function __construct(private AgentProfitCommissionAssignmentService $assignments)
  10. {
  11. }
  12. public function paginate(array $params, ?Agent $owner = null): array
  13. {
  14. $page = max(1, (int) ($params['page'] ?? 1));
  15. $limit = min(100, max(1, (int) ($params['limit'] ?? 20)));
  16. $query = $this->ownedQuery($owner);
  17. if (!empty($params['name'])) $query->where('name', 'like', '%' . $params['name'] . '%');
  18. $total = (clone $query)->count();
  19. $list = $query->orderByDesc('is_default')->orderByDesc('id')->forPage($page, $limit)->get();
  20. if ($owner) {
  21. $list->each(fn (AgentProfitCommissionProfile $profile) => $profile->setAttribute('editable', true));
  22. }
  23. return compact('total', 'page', 'limit', 'list');
  24. }
  25. public function paginateSelectable(Agent $agent, array $params): array
  26. {
  27. $agent->loadMissing('profitCommissionProfile');
  28. if (!$agent->profitCommissionProfile) throw new \RuntimeException('当前代理未关联盈亏佣金方案');
  29. $page = max(1, (int) ($params['page'] ?? 1));
  30. $limit = min(100, max(1, (int) ($params['limit'] ?? 20)));
  31. $query = AgentProfitCommissionProfile::query()->where(function (Builder $query) use ($agent) {
  32. $query->whereNull('owner_agent_id')->orWhere('owner_agent_id', $agent->id);
  33. });
  34. foreach (AgentProfitCommissionProfile::RATE_FIELDS as $field) {
  35. $query->where($field, '<=', (string) $agent->profitCommissionProfile->{$field});
  36. }
  37. if (!empty($params['name'])) $query->where('name', 'like', '%' . $params['name'] . '%');
  38. $total = (clone $query)->count();
  39. $list = $query->orderByRaw('owner_agent_id is null')->orderByDesc('is_default')->orderByDesc('id')
  40. ->forPage($page, $limit)->get();
  41. $list->each(function (AgentProfitCommissionProfile $profile) use ($agent) {
  42. $profile->setAttribute('editable', $profile->isOwnedBy((int) $agent->id));
  43. });
  44. return compact('total', 'page', 'limit', 'list');
  45. }
  46. public function save(array $data, ?int $adminId = null, ?Agent $owner = null): AgentProfitCommissionProfile
  47. {
  48. return DB::transaction(function () use ($data, $adminId, $owner) {
  49. $profiles = $this->ownedQuery($owner)->orderBy('id')->lockForUpdate()->get(['id', 'is_default']);
  50. $profile = empty($data['id'])
  51. ? new AgentProfitCommissionProfile()
  52. : AgentProfitCommissionProfile::query()->lockForUpdate()->findOrFail($data['id']);
  53. if ($profile->exists) $this->assertOwned($profile, $owner);
  54. else $profile->owner_agent_id = $owner?->id;
  55. $oldDefaultId = $profiles->firstWhere('is_default', 1)?->id;
  56. $isDefault = !empty($data['is_default']);
  57. if ($profile->exists && (int) $oldDefaultId === (int) $profile->id && !$isDefault) {
  58. throw new \RuntimeException('默认比例必须始终保留一条,请先将其他比例设为默认');
  59. }
  60. $name = trim((string) $data['name']);
  61. if ($name === '') throw new \InvalidArgumentException('盈亏比例名称不能为空');
  62. $duplicate = $this->ownedQuery($owner)->where('name', $name)
  63. ->when($profile->exists, fn ($query) => $query->where('id', '<>', $profile->id))->exists();
  64. if ($duplicate) throw new \InvalidArgumentException('盈亏比例名称已存在');
  65. $profile->name = $name;
  66. foreach (AgentProfitCommissionProfile::RATE_FIELDS as $field) $profile->{$field} = (string) $data[$field];
  67. if ($owner) $this->assertRatesWithinOwner($owner, $profile);
  68. $willBeDefault = $isDefault || (!$profile->exists && !$oldDefaultId);
  69. $profile->is_default = $willBeDefault ? 1 : 0;
  70. $snapshotChanged = !$profile->exists || $profile->isDirty(AgentProfitCommissionProfile::RATE_FIELDS);
  71. $affectedAgentIds = [];
  72. if ($snapshotChanged && $profile->exists) {
  73. $affectedAgentIds = Agent::query()->where('profit_commission_profile_id', $profile->id)
  74. ->pluck('id')->map(fn ($id) => (int) $id)->all();
  75. }
  76. if ($willBeDefault) {
  77. $defaultAgentIds = $this->defaultMigrationQuery($oldDefaultId, $profile, $owner)
  78. ->pluck('id')->map(fn ($id) => (int) $id)->all();
  79. $affectedAgentIds = array_values(array_unique(array_merge($affectedAgentIds, $defaultAgentIds)));
  80. }
  81. $this->assertHierarchy($affectedAgentIds, $profile);
  82. $profile->save();
  83. if ($willBeDefault) {
  84. $this->ownedQuery($owner)->where('id', '<>', $profile->id)->update(['is_default' => 0]);
  85. $this->defaultMigrationQuery($oldDefaultId, $profile, $owner)
  86. ->update(['profit_commission_profile_id' => $profile->id]);
  87. }
  88. $this->assignments->assignMany($affectedAgentIds, $profile, now()->toDateString(), $adminId);
  89. return $profile->fresh();
  90. });
  91. }
  92. public function delete(int $id, ?int $adminId = null, ?Agent $owner = null): void
  93. {
  94. DB::transaction(function () use ($id, $adminId, $owner) {
  95. $profile = AgentProfitCommissionProfile::query()->lockForUpdate()->findOrFail($id);
  96. $this->assertOwned($profile, $owner);
  97. if ((int) $profile->is_default === 1) throw new \RuntimeException('默认比例不能删除');
  98. $default = $this->ownedQuery($owner)->where('is_default', 1)->lockForUpdate()->first();
  99. if (!$default) throw new \RuntimeException('缺少默认比例,不能删除');
  100. $agentIds = $this->agentsUsingProfile($profile->id, $owner);
  101. $this->assertHierarchy($agentIds, $default);
  102. $this->assignments->assignMany($agentIds, $default, now()->toDateString(), $adminId);
  103. Agent::query()->where('profit_commission_profile_id', $profile->id)
  104. ->when($owner, fn ($query) => $query->where('parent_id', $owner->id))
  105. ->update(['profit_commission_profile_id' => $default->id]);
  106. $profile->delete();
  107. });
  108. }
  109. private function ownedQuery(?Agent $owner): Builder
  110. {
  111. $query = AgentProfitCommissionProfile::query();
  112. return $owner
  113. ? $query->where('owner_agent_id', $owner->id)
  114. : $query->whereNull('owner_agent_id');
  115. }
  116. private function defaultMigrationQuery(?int $oldDefaultId, AgentProfitCommissionProfile $profile, ?Agent $owner): Builder
  117. {
  118. $query = Agent::query()->where(function (Builder $query) use ($oldDefaultId, $profile) {
  119. $query->whereNull('profit_commission_profile_id');
  120. if ($oldDefaultId && (int) $oldDefaultId !== (int) $profile->id) {
  121. $query->orWhere('profit_commission_profile_id', $oldDefaultId);
  122. }
  123. });
  124. return $owner ? $query->where('parent_id', $owner->id) : $query;
  125. }
  126. private function agentsUsingProfile(int $profileId, ?Agent $owner): array
  127. {
  128. $ids = Agent::query()->where('profit_commission_profile_id', $profileId)
  129. ->pluck('id')->map(fn ($agentId) => (int) $agentId);
  130. if ($owner) {
  131. $childIds = Agent::query()->where('parent_id', $owner->id)
  132. ->pluck('id')->map(fn ($agentId) => (int) $agentId);
  133. if ($ids->diff($childIds)->isNotEmpty()) {
  134. throw new \RuntimeException('该盈亏方案仍被非直属下级使用,不能删除');
  135. }
  136. return $ids->intersect($childIds)->values()->all();
  137. }
  138. return $ids->all();
  139. }
  140. private function assertOwned(AgentProfitCommissionProfile $profile, ?Agent $owner): void
  141. {
  142. if ($owner) {
  143. if (!$profile->isOwnedBy((int) $owner->id)) {
  144. throw new \RuntimeException('无权操作该盈亏方案');
  145. }
  146. return;
  147. }
  148. if (!$profile->isOfficial()) {
  149. throw new \RuntimeException('总后台只能维护官方盈亏方案');
  150. }
  151. }
  152. private function assertRatesWithinOwner(Agent $owner, AgentProfitCommissionProfile $profile): void
  153. {
  154. $owner->loadMissing('profitCommissionProfile');
  155. if (!$owner->profitCommissionProfile) throw new \RuntimeException('当前代理未关联盈亏佣金方案');
  156. foreach (AgentProfitCommissionProfile::RATE_FIELDS as $field) {
  157. if (bccomp((string) $profile->{$field}, (string) $owner->profitCommissionProfile->{$field}, 4) > 0) {
  158. throw new \RuntimeException('下级盈亏方案不能高于当前代理方案');
  159. }
  160. }
  161. }
  162. private function assertHierarchy(array $agentIds, AgentProfitCommissionProfile $candidate): void
  163. {
  164. $agentIds = array_values(array_unique(array_map('intval', $agentIds)));
  165. if ($agentIds === []) return;
  166. $affected = array_fill_keys($agentIds, true);
  167. $agents = Agent::query()->with([
  168. 'parent.profitCommissionProfile',
  169. 'children.profitCommissionProfile',
  170. ])->whereIn('id', $agentIds)->get();
  171. foreach ($agents as $agent) {
  172. if ($agent->parent) {
  173. foreach (AgentProfitCommissionProfile::RATE_FIELDS as $field) {
  174. if (!isset($affected[(int) $agent->parent->id]) && !$agent->parent->profitCommissionProfile) {
  175. throw new \RuntimeException('上级代理未关联盈亏佣金方案');
  176. }
  177. $parentRate = isset($affected[(int) $agent->parent->id])
  178. ? (string) $candidate->{$field}
  179. : (string) $agent->parent->profitCommissionProfile->{$field};
  180. if (bccomp((string) $candidate->{$field}, $parentRate, 4) > 0) {
  181. throw new \RuntimeException('修改后的盈亏方案会高于上级代理方案');
  182. }
  183. }
  184. }
  185. foreach ($agent->children as $child) {
  186. foreach (AgentProfitCommissionProfile::RATE_FIELDS as $field) {
  187. if (!isset($affected[(int) $child->id]) && !$child->profitCommissionProfile) {
  188. throw new \RuntimeException('下级代理未关联盈亏佣金方案');
  189. }
  190. $childRate = isset($affected[(int) $child->id])
  191. ? (string) $candidate->{$field}
  192. : (string) $child->profitCommissionProfile->{$field};
  193. if (bccomp((string) $candidate->{$field}, $childRate, 4) < 0) {
  194. throw new \RuntimeException('修改后的盈亏方案会低于下级代理方案');
  195. }
  196. }
  197. }
  198. }
  199. }
  200. }