| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198 |
- <?php
- namespace App\Services\Agent;
- use App\Models\Agent\Agent;
- use App\Models\Agent\AgentCommission;
- use App\Models\Agent\AgentCommissionRule;
- use App\Models\Agent\AgentDailyStat;
- use App\Models\Agent\AgentDailyGameStat;
- use App\Models\Agent\AgentRebateRecord;
- use Carbon\Carbon;
- use Illuminate\Support\Facades\DB;
- class AgentCommissionService
- {
- public function __construct(
- private AgentReportService $reports,
- private AgentWalletService $wallets,
- private AgentProfitCommissionAssignmentService $profitAssignments
- ) {
- }
- public function saveFlowRate(Agent $agent, string $rate, string $effectiveFrom, ?int $adminId): AgentCommissionRule
- {
- if (bccomp($rate, '0', 4) < 0 || bccomp($rate, '100', 4) > 0) {
- throw new \InvalidArgumentException('流水佣金比例必须在0到100之间');
- }
- $day = Carbon::parse($effectiveFrom)->toDateString();
- return DB::transaction(function () use ($agent, $rate, $day, $adminId) {
- $agent = Agent::query()->lockForUpdate()->findOrFail($agent->id);
- $legacyCredited = AgentCommission::query()->where('agent_id', $agent->id)
- ->where('type', 'flow')->where('settlement_date', '>=', $day)
- ->where('status', AgentRebateRecord::STATUS_CREDITED)->exists();
- $rebateCredited = AgentRebateRecord::query()->where('agent_id', $agent->id)
- ->where('settlement_date', '>=', $day)
- ->where('flow_status', AgentRebateRecord::STATUS_CREDITED)->exists();
- if ($legacyCredited || $rebateCredited) {
- throw new \RuntimeException('该日期流水佣金已入账,不能修改比例');
- }
- $parent = $agent->parent;
- if ($parent && bccomp($rate, $this->flowRateAt($parent, $day), 4) > 0) {
- throw new \RuntimeException('下级代理流水佣金比例不能高于上级');
- }
- $maxChildRate = Agent::query()->where('parent_id', $agent->id)->get()
- ->map(fn(Agent $child) => $this->flowRateAt($child, $day))->max() ?? 0;
- if (bccomp($rate, (string) $maxChildRate, 4) < 0) {
- throw new \RuntimeException('代理流水佣金比例不能低于现有下级比例');
- }
- $existing = AgentCommissionRule::query()->where('agent_id', $agent->id)
- ->where('effective_from', $day)->lockForUpdate()->first();
- $profitRate = (string) ($existing?->profit_rate ?? $agent->profit_commission_rate ?? 0);
- $agent->flow_commission_rate = $rate;
- $agent->save();
- return AgentCommissionRule::query()->updateOrCreate(
- ['agent_id' => $agent->id, 'effective_from' => $day],
- ['flow_rate' => $rate, 'profit_rate' => $profitRate, 'status' => 1, 'created_by' => $adminId]
- );
- });
- }
- public function settle(string $date, bool $credit = true): array
- {
- $day = Carbon::parse($date)->toDateString();
- $created = 0;
- $credited = 0;
- $legacyRowsByAgent = AgentCommission::query()->where('settlement_date', $day)
- ->get(['agent_id', 'type', 'status'])->groupBy('agent_id');
- Agent::query()->where('status', Agent::STATUS_ENABLED)->orderBy('id')->chunkById(100, function ($agents) use ($day, $credit, $legacyRowsByAgent, &$created, &$credited) {
- foreach ($agents as $agent) {
- $legacyRows = $legacyRowsByAgent->get($agent->id, collect());
- if ($legacyRows->isNotEmpty()) {
- $creditedTypes = $legacyRows->where('status', AgentRebateRecord::STATUS_CREDITED)
- ->pluck('type')->unique()->values();
- if ($creditedTypes->contains('flow') && $creditedTypes->contains('profit')) {
- continue;
- }
- throw new \RuntimeException("代理 {$agent->id} 的 {$day} 存在未完成旧佣金记录,请先人工核对");
- }
- $descendantIds = $this->reports->visibleAgentIds($agent);
- $rule = AgentCommissionRule::query()->where('agent_id', $agent->id)->where('status', 1)
- ->where('effective_from', '<=', $day)->orderByDesc('effective_from')->first();
- if (!$rule) {
- throw new \RuntimeException("代理 {$agent->id} 缺少 {$day} 可用的佣金比例记录");
- }
- $profitAssignment = $this->profitAssignments->at($agent, $day);
- $gameRows = AgentDailyGameStat::query()->where('stat_date', $day)->whereIn('agent_id', $descendantIds)
- ->selectRaw('platform, game_type, SUM(bet_count) bet_count, SUM(bet_amount) bet_amount, SUM(valid_bet_amount) valid_bet_amount, SUM(win_loss) win_loss')
- ->groupBy('platform', 'game_type')->get();
- foreach ($gameRows as $gameRow) {
- $flowRate = (string) $rule->flow_rate;
- $profitRate = $profitAssignment->rateForGameType((int) $gameRow->game_type);
- $flowAmount = bcdiv(bcmul((string) $gameRow->valid_bet_amount, $flowRate, 8), '100', 4);
- $profitBase = bccomp((string) $gameRow->win_loss, '0', 4) < 0
- ? bcsub('0', (string) $gameRow->win_loss, 4) : '0.0000';
- $profitAmount = bcdiv(bcmul($profitBase, $profitRate, 8), '100', 4);
- $record = AgentRebateRecord::query()->firstOrNew([
- 'settlement_date' => $day, 'agent_id' => $agent->id,
- 'platform' => (string) $gameRow->platform, 'game_type' => (int) $gameRow->game_type,
- ]);
- $isNew = !$record->exists;
- if ($isNew) {
- $record->fill([
- 'order_no' => $record->order_no ?: $this->rebateOrderNo($day, (int) $agent->id, (string) $gameRow->platform, (int) $gameRow->game_type),
- 'bet_count' => (int) $gameRow->bet_count, 'bet_amount' => (string) $gameRow->bet_amount,
- 'valid_bet_amount' => (string) $gameRow->valid_bet_amount, 'win_loss' => (string) $gameRow->win_loss,
- 'flow_rate' => $flowRate, 'flow_commission' => $flowAmount,
- 'flow_status' => AgentRebateRecord::STATUS_PENDING,
- 'profit_rate' => $profitRate, 'profit_commission' => $profitAmount,
- 'profit_status' => AgentRebateRecord::STATUS_PENDING,
- 'snapshot' => [
- 'agent_ids' => $descendantIds,
- 'profit_profile_id' => $profitAssignment->profit_commission_profile_id,
- 'profit_assignment_id' => $profitAssignment->id,
- ],
- ]);
- } else {
- if ($record->flow_status === AgentRebateRecord::STATUS_PENDING
- && $record->profit_status === AgentRebateRecord::STATUS_PENDING) {
- $record->flow_rate = $flowRate;
- $record->flow_commission = $flowAmount;
- $record->profit_rate = $profitRate;
- $record->profit_commission = $profitAmount;
- $record->bet_count = (int) $gameRow->bet_count;
- $record->bet_amount = (string) $gameRow->bet_amount;
- $record->valid_bet_amount = (string) $gameRow->valid_bet_amount;
- $record->win_loss = (string) $gameRow->win_loss;
- $record->snapshot = [
- 'agent_ids' => $descendantIds,
- 'profit_profile_id' => $profitAssignment->profit_commission_profile_id,
- 'profit_assignment_id' => $profitAssignment->id,
- ];
- }
- }
- $record->save();
- if ($isNew) $created++;
- if ($credit) {
- $credited += $this->creditRebatePart($record, 'flow', $day, (int) $agent->id);
- $credited += $this->creditRebatePart($record, 'profit', $day, (int) $agent->id);
- $record->refresh();
- if ($record->flow_status === AgentRebateRecord::STATUS_CREDITED
- && $record->profit_status === AgentRebateRecord::STATUS_CREDITED
- && $record->credited_at === null) {
- $record->credited_at = now();
- }
- $record->save();
- }
- }
- AgentDailyStat::query()->where('stat_date', $day)->where('agent_id', $agent->id)->update([
- 'commission_amount' => AgentRebateRecord::query()->where('settlement_date', $day)
- ->where('agent_id', $agent->id)
- ->selectRaw("COALESCE(SUM(CASE WHEN flow_status='credited' THEN flow_commission ELSE 0 END + CASE WHEN profit_status='credited' THEN profit_commission ELSE 0 END),0) total")
- ->value('total'),
- ]);
- }
- });
- return compact('created', 'credited');
- }
- private function creditRebatePart(AgentRebateRecord $record, string $type, string $day, int $agentId): int
- {
- return DB::transaction(function () use ($record, $type, $day, $agentId) {
- $locked = AgentRebateRecord::query()->lockForUpdate()->findOrFail($record->id);
- $statusField = $type . '_status';
- $amountField = $type . '_commission';
- if ($locked->{$statusField} !== AgentRebateRecord::STATUS_PENDING) return 0;
- $amount = (string) $locked->{$amountField};
- if (bccomp($amount, '0', 4) > 0) {
- $this->wallets->adjust(
- $agentId, $amount, 'commission',
- $day . ($type === 'flow' ? '流水佣金' : '盈亏佣金') . '-' . $locked->platform,
- 'rebate:' . $locked->id . ':' . $type, 'system', null,
- 'agent_rebate_record', (int) $locked->id
- );
- }
- $locked->{$statusField} = AgentRebateRecord::STATUS_CREDITED;
- $locked->save();
- return 1;
- });
- }
- private function rebateOrderNo(string $day, int $agentId, string $platform, int $gameType): string
- {
- return 'AR' . str_replace('-', '', $day) . $agentId
- . strtoupper(substr(sha1($platform . '|' . $gameType), 0, 10));
- }
- private function flowRateAt(Agent $agent, string $day): string
- {
- $rule = AgentCommissionRule::query()->where('agent_id', $agent->id)->where('status', 1)
- ->where('effective_from', '<=', $day)->orderByDesc('effective_from')->first();
- if ($rule) {
- return (string) $rule->flow_rate;
- }
- return (string) $agent->flow_commission_rate;
- }
- }
|