AgentCommissionService.php 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. <?php
  2. namespace App\Services\Agent;
  3. use App\Models\Agent\Agent;
  4. use App\Models\Agent\AgentCommission;
  5. use App\Models\Agent\AgentCommissionRule;
  6. use App\Models\Agent\AgentDailyStat;
  7. use Carbon\Carbon;
  8. use Illuminate\Support\Facades\DB;
  9. class AgentCommissionService
  10. {
  11. public function __construct(private AgentReportService $reports, private AgentWalletService $wallets)
  12. {
  13. }
  14. public function saveRule(Agent $agent, string $flowRate, string $profitRate, string $effectiveFrom, ?int $adminId): AgentCommissionRule
  15. {
  16. return $this->saveRates($agent, $flowRate, $profitRate, $effectiveFrom, $adminId);
  17. }
  18. public function saveFlowRate(Agent $agent, string $rate, string $effectiveFrom, ?int $adminId): AgentCommissionRule
  19. {
  20. return $this->saveRates($agent, $rate, null, $effectiveFrom, $adminId);
  21. }
  22. public function saveProfitRate(Agent $agent, string $rate, string $effectiveFrom, ?int $adminId): AgentCommissionRule
  23. {
  24. return $this->saveRates($agent, null, $rate, $effectiveFrom, $adminId);
  25. }
  26. public function settle(string $date, bool $credit = true): array
  27. {
  28. $day = Carbon::parse($date)->toDateString();
  29. $created = 0;
  30. $credited = 0;
  31. Agent::query()->where('status', Agent::STATUS_ENABLED)->orderBy('id')->chunkById(100, function ($agents) use ($day, $credit, &$created, &$credited) {
  32. foreach ($agents as $agent) {
  33. $descendantIds = $this->reports->visibleAgentIds($agent);
  34. $base = AgentDailyStat::query()->where('stat_date', $day)->whereIn('agent_id', $descendantIds)
  35. ->selectRaw('COALESCE(SUM(valid_bet_amount),0) valid_bet_amount, COALESCE(SUM(company_profit),0) company_profit, COALESCE(SUM(bet_count),0) bet_count')
  36. ->first();
  37. $rule = AgentCommissionRule::query()->where('agent_id', $agent->id)->where('status', 1)
  38. ->where('effective_from', '<=', $day)->orderByDesc('effective_from')->first();
  39. if (!$rule) {
  40. throw new \RuntimeException("代理 {$agent->id} 缺少 {$day} 可用的佣金比例记录");
  41. }
  42. $rates = [
  43. 'flow' => (string) $rule->flow_rate,
  44. 'profit' => (string) $rule->profit_rate,
  45. ];
  46. $bases = [
  47. 'flow' => (string) ($base->valid_bet_amount ?? 0),
  48. 'profit' => max('0', (string) ($base->company_profit ?? 0)),
  49. ];
  50. foreach (['flow', 'profit'] as $type) {
  51. $amount = bcdiv(bcmul($bases[$type], $rates[$type], 8), '100', 4);
  52. $commission = AgentCommission::query()->firstOrNew([
  53. 'settlement_date' => $day,
  54. 'agent_id' => $agent->id,
  55. 'type' => $type,
  56. ]);
  57. $isNew = !$commission->exists;
  58. if ($isNew || $commission->status === 'pending') {
  59. $commission->fill([
  60. 'base_amount' => $bases[$type], 'rate' => $rates[$type], 'amount' => $amount,
  61. 'bet_count' => (int) ($base->bet_count ?? 0), 'status' => 'pending',
  62. 'snapshot' => ['agent_ids' => $descendantIds],
  63. ]);
  64. $commission->save();
  65. }
  66. if ($isNew) {
  67. $created++;
  68. }
  69. if ($credit && $commission->status === 'pending' && bccomp((string) $commission->amount, '0', 4) > 0) {
  70. $this->wallets->adjust(
  71. (int) $agent->id,
  72. (string) $commission->amount,
  73. 'commission',
  74. $day . ($type === 'flow' ? '流水佣金' : '盈亏佣金'),
  75. 'commission:' . $commission->id,
  76. 'system',
  77. null,
  78. 'agent_commission',
  79. (int) $commission->id
  80. );
  81. $commission->status = 'credited';
  82. $commission->credited_at = now();
  83. $commission->save();
  84. $credited++;
  85. } elseif ($credit && $commission->status === 'pending') {
  86. $commission->status = 'credited';
  87. $commission->credited_at = now();
  88. $commission->save();
  89. }
  90. }
  91. AgentDailyStat::query()->where('stat_date', $day)->where('agent_id', $agent->id)->update([
  92. 'commission_amount' => AgentCommission::query()->where('settlement_date', $day)
  93. ->where('agent_id', $agent->id)->where('status', 'credited')->sum('amount'),
  94. ]);
  95. }
  96. });
  97. return compact('created', 'credited');
  98. }
  99. private function rateAt(Agent $agent, string $field, string $day): string
  100. {
  101. $rule = AgentCommissionRule::query()->where('agent_id', $agent->id)->where('status', 1)
  102. ->where('effective_from', '<=', $day)->orderByDesc('effective_from')->first();
  103. if ($rule) {
  104. return (string) $rule->{$field};
  105. }
  106. $agentField = $field === 'flow_rate' ? 'flow_commission_rate' : 'profit_commission_rate';
  107. return (string) $agent->{$agentField};
  108. }
  109. private function saveRates(Agent $agent, ?string $flowRate, ?string $profitRate, string $effectiveFrom, ?int $adminId): AgentCommissionRule
  110. {
  111. foreach (array_filter([$flowRate, $profitRate], fn($rate) => $rate !== null) as $rate) {
  112. if (bccomp((string) $rate, '0', 4) < 0 || bccomp((string) $rate, '100', 4) > 0) {
  113. throw new \InvalidArgumentException('佣金比例必须在0到100之间');
  114. }
  115. }
  116. $day = Carbon::parse($effectiveFrom)->toDateString();
  117. return DB::transaction(function () use ($agent, $flowRate, $profitRate, $day, $adminId) {
  118. $agent = Agent::query()->lockForUpdate()->findOrFail($agent->id);
  119. if (AgentCommission::query()->where('agent_id', $agent->id)->where('settlement_date', '>=', $day)->where('status', 'credited')->exists()) {
  120. throw new \RuntimeException('该日期佣金已入账,不能修改比例');
  121. }
  122. $existing = AgentCommissionRule::query()->where('agent_id', $agent->id)
  123. ->where('effective_from', $day)->lockForUpdate()->first();
  124. $flowRate = $flowRate ?? (string) ($existing?->flow_rate ?? $this->rateAt($agent, 'flow_rate', $day));
  125. $profitRate = $profitRate ?? (string) ($existing?->profit_rate ?? $this->rateAt($agent, 'profit_rate', $day));
  126. $parent = $agent->parent;
  127. if ($parent && (
  128. bccomp($flowRate, $this->rateAt($parent, 'flow_rate', $day), 4) > 0
  129. || bccomp($profitRate, $this->rateAt($parent, 'profit_rate', $day), 4) > 0
  130. )) {
  131. throw new \RuntimeException('下级代理佣金比例不能高于上级');
  132. }
  133. $children = Agent::query()->where('parent_id', $agent->id)->get();
  134. $maxFlow = $children->map(fn(Agent $child) => $this->rateAt($child, 'flow_rate', $day))->max() ?? 0;
  135. $maxProfit = $children->map(fn(Agent $child) => $this->rateAt($child, 'profit_rate', $day))->max() ?? 0;
  136. if (bccomp($flowRate, (string) $maxFlow, 4) < 0 || bccomp($profitRate, (string) $maxProfit, 4) < 0) {
  137. throw new \RuntimeException('代理佣金比例不能低于现有下级比例');
  138. }
  139. $agent->flow_commission_rate = $flowRate;
  140. $agent->profit_commission_rate = $profitRate;
  141. $agent->save();
  142. return AgentCommissionRule::query()->updateOrCreate(
  143. ['agent_id' => $agent->id, 'effective_from' => $day],
  144. ['flow_rate' => $flowRate, 'profit_rate' => $profitRate, 'status' => 1, 'created_by' => $adminId]
  145. );
  146. });
  147. }
  148. }