AgentService.php 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. <?php
  2. namespace App\Services\Agent;
  3. use App\Models\Agent\Agent;
  4. use App\Models\Agent\AgentCommissionRule;
  5. use Illuminate\Support\Facades\DB;
  6. use Illuminate\Support\Facades\Hash;
  7. use Illuminate\Support\Str;
  8. class AgentService
  9. {
  10. public function create(array $data, ?int $createdBy = null, ?Agent $scopeAgent = null): Agent
  11. {
  12. return DB::transaction(function () use ($data, $createdBy, $scopeAgent): Agent {
  13. $parentId = isset($data['parent_id']) ? (int) $data['parent_id'] : null;
  14. if ($scopeAgent) {
  15. $parentId = $parentId ?: (int) $scopeAgent->id;
  16. $this->assertVisible($scopeAgent, $parentId);
  17. }
  18. $parent = $parentId ? Agent::query()->lockForUpdate()->findOrFail($parentId) : null;
  19. if ($scopeAgent && $parent && (int) $parent->status !== Agent::STATUS_ENABLED) {
  20. throw new \RuntimeException('不能在已禁用代理下新增账号');
  21. }
  22. if ($parent) {
  23. $this->assertRatesWithinParent($parent, $data);
  24. }
  25. $agent = Agent::query()->create([
  26. 'parent_id' => $parent?->id,
  27. 'path' => '/',
  28. 'level' => $parent ? ((int) $parent->level + 1) : 1,
  29. 'username' => trim((string) $data['username']),
  30. 'password' => Hash::make((string) $data['password']),
  31. 'withdraw_password' => empty($data['withdraw_password']) ? '' : Hash::make((string) $data['withdraw_password']),
  32. 'real_name' => trim((string) ($data['real_name'] ?? '')),
  33. 'invitation_code' => $this->invitationCode(),
  34. 'deposit_fee_rate' => (string) ($data['deposit_fee_rate'] ?? 0),
  35. 'withdraw_fee_rate' => (string) ($data['withdraw_fee_rate'] ?? 0),
  36. 'flow_commission_rate' => (string) ($data['flow_commission_rate'] ?? 0),
  37. 'profit_commission_rate' => (string) ($data['profit_commission_rate'] ?? 0),
  38. 'status' => (int) ($data['status'] ?? Agent::STATUS_ENABLED),
  39. 'created_by' => $createdBy,
  40. ]);
  41. $agent->path = ($parent ? rtrim((string) $parent->path, '/') . '/' : '/') . $agent->id . '/';
  42. $agent->save();
  43. AgentCommissionRule::query()->create([
  44. 'agent_id' => $agent->id,
  45. 'flow_rate' => $agent->flow_commission_rate,
  46. 'profit_rate' => $agent->profit_commission_rate,
  47. 'effective_from' => now()->toDateString(),
  48. 'status' => 1,
  49. 'created_by' => $createdBy,
  50. ]);
  51. return $agent->fresh();
  52. });
  53. }
  54. public function update(Agent $agent, array $data, ?Agent $scopeAgent = null): Agent
  55. {
  56. return DB::transaction(function () use ($agent, $data, $scopeAgent): Agent {
  57. $agent = Agent::query()->lockForUpdate()->findOrFail($agent->id);
  58. if ($scopeAgent) {
  59. $this->assertVisible($scopeAgent, (int) $agent->id);
  60. if ($agent->parent) {
  61. $this->assertRatesWithinParent($agent->parent, $data);
  62. }
  63. $this->assertRatesCoverChildren($agent, $data);
  64. } elseif (array_key_exists('parent_id', $data)) {
  65. $this->move($agent, $data['parent_id'] === null ? null : (int) $data['parent_id']);
  66. }
  67. foreach (['username', 'real_name', 'status', 'deposit_fee_rate', 'withdraw_fee_rate', 'flow_commission_rate', 'profit_commission_rate'] as $field) {
  68. if (array_key_exists($field, $data)) {
  69. $agent->{$field} = $data[$field];
  70. }
  71. }
  72. if (!empty($data['password'])) {
  73. $agent->password = Hash::make((string) $data['password']);
  74. }
  75. if (!empty($data['withdraw_password'])) {
  76. $agent->withdraw_password = Hash::make((string) $data['withdraw_password']);
  77. }
  78. $agent->save();
  79. return $agent->fresh();
  80. });
  81. }
  82. public function assertVisible(Agent $root, int $targetId): void
  83. {
  84. if ((int) $root->id === $targetId) {
  85. return;
  86. }
  87. $visible = Agent::query()->whereKey($targetId)->where('path', 'like', rtrim((string) $root->path, '/') . '/%')->exists();
  88. if (!$visible) {
  89. throw new \RuntimeException('无权访问该代理');
  90. }
  91. }
  92. private function invitationCode(): string
  93. {
  94. do {
  95. $code = strtoupper(Str::random(10));
  96. } while (Agent::query()->where('invitation_code', $code)->exists());
  97. return $code;
  98. }
  99. private function assertRatesWithinParent(Agent $parent, array $data): void
  100. {
  101. foreach (['flow_commission_rate', 'profit_commission_rate'] as $field) {
  102. if (array_key_exists($field, $data) && bccomp((string) $data[$field], (string) $parent->{$field}, 4) > 0) {
  103. throw new \RuntimeException('下级代理佣金比例不能高于上级');
  104. }
  105. }
  106. }
  107. private function assertRatesCoverChildren(Agent $agent, array $data): void
  108. {
  109. foreach (['flow_commission_rate', 'profit_commission_rate'] as $field) {
  110. if (!array_key_exists($field, $data)) {
  111. continue;
  112. }
  113. $maxChildRate = Agent::query()->where('parent_id', $agent->id)->max($field) ?? 0;
  114. if (bccomp((string) $data[$field], (string) $maxChildRate, 4) < 0) {
  115. throw new \RuntimeException('代理佣金比例不能低于现有下级比例');
  116. }
  117. }
  118. }
  119. private function move(Agent $agent, ?int $parentId): void
  120. {
  121. if ($parentId === (int) $agent->id) {
  122. throw new \RuntimeException('上级代理不能是自己');
  123. }
  124. $currentParentId = $agent->parent_id === null ? null : (int) $agent->parent_id;
  125. if ($parentId === $currentParentId) {
  126. return;
  127. }
  128. $parent = $parentId ? Agent::query()->lockForUpdate()->findOrFail($parentId) : null;
  129. if ($parent && str_starts_with((string) $parent->path, (string) $agent->path)) {
  130. throw new \RuntimeException('不能把代理移动到自己的下级');
  131. }
  132. if ($parent) {
  133. $this->assertRatesWithinParent($parent, [
  134. 'flow_commission_rate' => $agent->flow_commission_rate,
  135. 'profit_commission_rate' => $agent->profit_commission_rate,
  136. ]);
  137. }
  138. $oldPath = (string) $agent->path;
  139. $oldLevel = (int) $agent->level;
  140. $newLevel = $parent ? ((int) $parent->level + 1) : 1;
  141. $newPath = ($parent ? rtrim((string) $parent->path, '/') . '/' : '/') . $agent->id . '/';
  142. $descendants = Agent::query()->where('path', 'like', rtrim($oldPath, '/') . '/%')->lockForUpdate()->get();
  143. $agent->parent_id = $parent?->id;
  144. $agent->path = $newPath;
  145. $agent->level = $newLevel;
  146. foreach ($descendants as $descendant) {
  147. $descendant->path = $newPath . substr((string) $descendant->path, strlen($oldPath));
  148. $descendant->level = max(1, (int) $descendant->level + $newLevel - $oldLevel);
  149. $descendant->save();
  150. }
  151. }
  152. }