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; } }