AgentService.php 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. <?php
  2. namespace App\Services\Agent;
  3. use App\Models\Agent\Agent;
  4. use App\Models\Agent\AgentCommissionRule;
  5. use App\Models\Agent\AgentProfitCommissionProfile;
  6. use Illuminate\Support\Facades\DB;
  7. use Illuminate\Support\Facades\Hash;
  8. use Illuminate\Support\Str;
  9. class AgentService
  10. {
  11. public function __construct(private AgentProfitCommissionAssignmentService $profitAssignments)
  12. {
  13. }
  14. public function create(array $data, ?int $createdBy = null, ?Agent $scopeAgent = null): Agent
  15. {
  16. return DB::transaction(function () use ($data, $createdBy, $scopeAgent): Agent {
  17. $parentId = isset($data['parent_id']) ? (int) $data['parent_id'] : null;
  18. if ($scopeAgent) {
  19. if ((int) $scopeAgent->level !== Agent::LEVEL_ONE) {
  20. throw new \RuntimeException('二级代理不能继续新增下级代理');
  21. }
  22. if ($parentId && $parentId !== (int) $scopeAgent->id) {
  23. throw new \RuntimeException('一级代理只能新增自己的直属二级代理');
  24. }
  25. $parentId = (int) $scopeAgent->id;
  26. } elseif ($parentId) {
  27. throw new \RuntimeException('总后台只能新增一级代理');
  28. }
  29. $parent = $parentId ? Agent::query()->lockForUpdate()->findOrFail($parentId) : null;
  30. if ($scopeAgent && $parent && (int) $parent->status !== Agent::STATUS_ENABLED) {
  31. throw new \RuntimeException('不能在已禁用代理下新增账号');
  32. }
  33. if ($parent) {
  34. $this->assertRatesWithinParent($parent, $data);
  35. }
  36. $level = $parent ? ((int) $parent->level + 1) : Agent::LEVEL_ONE;
  37. if ($level > Agent::MAX_LEVEL) {
  38. throw new \RuntimeException('代理层级最多为二级');
  39. }
  40. $profitProfileId = (int) ($data['profit_commission_profile_id']
  41. ?? $parent?->profit_commission_profile_id
  42. ?? AgentProfitCommissionProfile::query()->where('is_default', 1)->value('id'));
  43. if ($profitProfileId <= 0) throw new \RuntimeException('请先配置默认代理盈亏佣金比例');
  44. $profitProfile = AgentProfitCommissionProfile::query()->findOrFail($profitProfileId);
  45. $agent = Agent::query()->create([
  46. 'parent_id' => $parent?->id,
  47. 'path' => '/',
  48. 'level' => $level,
  49. 'username' => trim((string) $data['username']),
  50. 'password' => Hash::make((string) $data['password']),
  51. 'withdraw_password' => empty($data['withdraw_password']) ? '' : Hash::make((string) $data['withdraw_password']),
  52. 'real_name' => trim((string) ($data['real_name'] ?? '')),
  53. 'invitation_code' => $this->invitationCode(),
  54. 'deposit_fee_rate' => (string) ($data['deposit_fee_rate'] ?? 0),
  55. 'withdraw_fee_rate' => (string) ($data['withdraw_fee_rate'] ?? 0),
  56. 'flow_commission_rate' => (string) ($data['flow_commission_rate'] ?? 0),
  57. 'profit_commission_rate' => (string) ($data['profit_commission_rate'] ?? 0),
  58. 'profit_commission_profile_id' => $profitProfileId,
  59. 'status' => (int) ($data['status'] ?? Agent::STATUS_ENABLED),
  60. 'created_by' => $createdBy,
  61. ]);
  62. $agent->path = ($parent ? rtrim((string) $parent->path, '/') . '/' : '/') . $agent->id . '/';
  63. $agent->save();
  64. $this->profitAssignments->assign($agent, $profitProfile, now()->toDateString(), $createdBy);
  65. AgentCommissionRule::query()->create([
  66. 'agent_id' => $agent->id,
  67. 'flow_rate' => $agent->flow_commission_rate,
  68. 'profit_rate' => $agent->profit_commission_rate,
  69. 'effective_from' => now()->toDateString(),
  70. 'status' => 1,
  71. 'created_by' => $createdBy,
  72. ]);
  73. return $agent->fresh();
  74. });
  75. }
  76. public function update(Agent $agent, array $data, ?Agent $scopeAgent = null, ?int $createdBy = null): Agent
  77. {
  78. return DB::transaction(function () use ($agent, $data, $scopeAgent, $createdBy): Agent {
  79. $agent = Agent::query()->lockForUpdate()->findOrFail($agent->id);
  80. $newProfitProfile = null;
  81. if (array_key_exists('profit_commission_profile_id', $data)
  82. && $data['profit_commission_profile_id'] !== null
  83. && (int) $data['profit_commission_profile_id'] !== (int) $agent->profit_commission_profile_id) {
  84. $newProfitProfile = AgentProfitCommissionProfile::query()
  85. ->findOrFail((int) $data['profit_commission_profile_id']);
  86. }
  87. if ($scopeAgent) {
  88. $this->assertVisible($scopeAgent, (int) $agent->id);
  89. if ($agent->parent) {
  90. $this->assertRatesWithinParent($agent->parent, $data);
  91. }
  92. $this->assertRatesCoverChildren($agent, $data);
  93. } elseif (array_key_exists('parent_id', $data)) {
  94. $this->move($agent, $data['parent_id'] === null ? null : (int) $data['parent_id']);
  95. }
  96. if (array_key_exists('real_name', $data)) {
  97. $agent->real_name = trim((string) ($data['real_name'] ?? ''));
  98. }
  99. foreach (['username', 'status', 'deposit_fee_rate', 'withdraw_fee_rate', 'flow_commission_rate', 'profit_commission_profile_id'] as $field) {
  100. if (array_key_exists($field, $data) && $data[$field] !== null) {
  101. $agent->{$field} = $data[$field];
  102. }
  103. }
  104. if (!empty($data['password'])) {
  105. $agent->password = Hash::make((string) $data['password']);
  106. }
  107. if (!empty($data['withdraw_password'])) {
  108. $agent->withdraw_password = Hash::make((string) $data['withdraw_password']);
  109. }
  110. $agent->save();
  111. if ($newProfitProfile) {
  112. $this->profitAssignments->assign($agent, $newProfitProfile, now()->toDateString(), $createdBy);
  113. }
  114. return $agent->fresh();
  115. });
  116. }
  117. public function assertVisible(Agent $root, int $targetId): void
  118. {
  119. if ((int) $root->id === $targetId) {
  120. return;
  121. }
  122. $visible = Agent::query()->whereKey($targetId)->where('path', 'like', rtrim((string) $root->path, '/') . '/%')->exists();
  123. if (!$visible) {
  124. throw new \RuntimeException('无权访问该代理');
  125. }
  126. }
  127. private function invitationCode(): string
  128. {
  129. do {
  130. $code = strtoupper(Str::random(10));
  131. } while (Agent::query()->where('invitation_code', $code)->exists());
  132. return $code;
  133. }
  134. private function assertRatesWithinParent(Agent $parent, array $data): void
  135. {
  136. foreach (['flow_commission_rate'] as $field) {
  137. if (array_key_exists($field, $data) && bccomp((string) $data[$field], (string) $parent->{$field}, 4) > 0) {
  138. throw new \RuntimeException('下级代理佣金比例不能高于上级');
  139. }
  140. }
  141. }
  142. private function assertRatesCoverChildren(Agent $agent, array $data): void
  143. {
  144. foreach (['flow_commission_rate'] as $field) {
  145. if (!array_key_exists($field, $data)) {
  146. continue;
  147. }
  148. $maxChildRate = Agent::query()->where('parent_id', $agent->id)->max($field) ?? 0;
  149. if (bccomp((string) $data[$field], (string) $maxChildRate, 4) < 0) {
  150. throw new \RuntimeException('代理佣金比例不能低于现有下级比例');
  151. }
  152. }
  153. }
  154. private function move(Agent $agent, ?int $parentId): void
  155. {
  156. if ($parentId === (int) $agent->id) {
  157. throw new \RuntimeException('上级代理不能是自己');
  158. }
  159. $currentParentId = $agent->parent_id === null ? null : (int) $agent->parent_id;
  160. if ($parentId === $currentParentId) {
  161. return;
  162. }
  163. $parent = $parentId ? Agent::query()->lockForUpdate()->findOrFail($parentId) : null;
  164. if ($parent && str_starts_with((string) $parent->path, (string) $agent->path)) {
  165. throw new \RuntimeException('不能把代理移动到自己的下级');
  166. }
  167. if ($parent && (int) $parent->level !== Agent::LEVEL_ONE) {
  168. throw new \RuntimeException('二级代理不能作为上级代理');
  169. }
  170. if ($parent) {
  171. $this->assertRatesWithinParent($parent, [
  172. 'flow_commission_rate' => $agent->flow_commission_rate,
  173. ]);
  174. }
  175. $oldPath = (string) $agent->path;
  176. $oldLevel = (int) $agent->level;
  177. $newLevel = $parent ? ((int) $parent->level + 1) : 1;
  178. $newPath = ($parent ? rtrim((string) $parent->path, '/') . '/' : '/') . $agent->id . '/';
  179. $descendants = Agent::query()->where('path', 'like', rtrim($oldPath, '/') . '/%')->lockForUpdate()->get();
  180. $newMaxLevel = $descendants->max(fn(Agent $descendant) =>
  181. (int) $descendant->level + $newLevel - $oldLevel
  182. ) ?? $newLevel;
  183. if ($newLevel > Agent::MAX_LEVEL || $newMaxLevel > Agent::MAX_LEVEL) {
  184. throw new \RuntimeException('移动后会产生三级代理,操作已拒绝');
  185. }
  186. $agent->parent_id = $parent?->id;
  187. $agent->path = $newPath;
  188. $agent->level = $newLevel;
  189. foreach ($descendants as $descendant) {
  190. $descendant->path = $newPath . substr((string) $descendant->path, strlen($oldPath));
  191. $descendant->level = max(1, (int) $descendant->level + $newLevel - $oldLevel);
  192. $descendant->save();
  193. }
  194. }
  195. }