saveRates($agent, $flowRate, $profitRate, $effectiveFrom, $adminId); } public function saveFlowRate(Agent $agent, string $rate, string $effectiveFrom, ?int $adminId): AgentCommissionRule { return $this->saveRates($agent, $rate, null, $effectiveFrom, $adminId); } public function saveProfitRate(Agent $agent, string $rate, string $effectiveFrom, ?int $adminId): AgentCommissionRule { return $this->saveRates($agent, null, $rate, $effectiveFrom, $adminId); } public function settle(string $date, bool $credit = true): array { $day = Carbon::parse($date)->toDateString(); $created = 0; $credited = 0; Agent::query()->where('status', Agent::STATUS_ENABLED)->orderBy('id')->chunkById(100, function ($agents) use ($day, $credit, &$created, &$credited) { foreach ($agents as $agent) { $descendantIds = $this->reports->visibleAgentIds($agent); $base = AgentDailyStat::query()->where('stat_date', $day)->whereIn('agent_id', $descendantIds) ->selectRaw('COALESCE(SUM(valid_bet_amount),0) valid_bet_amount, COALESCE(SUM(company_profit),0) company_profit, COALESCE(SUM(bet_count),0) bet_count') ->first(); $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} 可用的佣金比例记录"); } $rates = [ 'flow' => (string) $rule->flow_rate, 'profit' => (string) $rule->profit_rate, ]; $bases = [ 'flow' => (string) ($base->valid_bet_amount ?? 0), 'profit' => max('0', (string) ($base->company_profit ?? 0)), ]; foreach (['flow', 'profit'] as $type) { $amount = bcdiv(bcmul($bases[$type], $rates[$type], 8), '100', 4); $commission = AgentCommission::query()->firstOrNew([ 'settlement_date' => $day, 'agent_id' => $agent->id, 'type' => $type, ]); $isNew = !$commission->exists; if ($isNew || $commission->status === 'pending') { $commission->fill([ 'base_amount' => $bases[$type], 'rate' => $rates[$type], 'amount' => $amount, 'bet_count' => (int) ($base->bet_count ?? 0), 'status' => 'pending', 'snapshot' => ['agent_ids' => $descendantIds], ]); $commission->save(); } if ($isNew) { $created++; } if ($credit && $commission->status === 'pending' && bccomp((string) $commission->amount, '0', 4) > 0) { $this->wallets->adjust( (int) $agent->id, (string) $commission->amount, 'commission', $day . ($type === 'flow' ? '流水佣金' : '盈亏佣金'), 'commission:' . $commission->id, 'system', null, 'agent_commission', (int) $commission->id ); $commission->status = 'credited'; $commission->credited_at = now(); $commission->save(); $credited++; } elseif ($credit && $commission->status === 'pending') { $commission->status = 'credited'; $commission->credited_at = now(); $commission->save(); } } AgentDailyStat::query()->where('stat_date', $day)->where('agent_id', $agent->id)->update([ 'commission_amount' => AgentCommission::query()->where('settlement_date', $day) ->where('agent_id', $agent->id)->where('status', 'credited')->sum('amount'), ]); } }); return compact('created', 'credited'); } private function rateAt(Agent $agent, string $field, 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->{$field}; } $agentField = $field === 'flow_rate' ? 'flow_commission_rate' : 'profit_commission_rate'; return (string) $agent->{$agentField}; } private function saveRates(Agent $agent, ?string $flowRate, ?string $profitRate, string $effectiveFrom, ?int $adminId): AgentCommissionRule { foreach (array_filter([$flowRate, $profitRate], fn($rate) => $rate !== null) as $rate) { if (bccomp((string) $rate, '0', 4) < 0 || bccomp((string) $rate, '100', 4) > 0) { throw new \InvalidArgumentException('佣金比例必须在0到100之间'); } } $day = Carbon::parse($effectiveFrom)->toDateString(); return DB::transaction(function () use ($agent, $flowRate, $profitRate, $day, $adminId) { $agent = Agent::query()->lockForUpdate()->findOrFail($agent->id); if (AgentCommission::query()->where('agent_id', $agent->id)->where('settlement_date', '>=', $day)->where('status', 'credited')->exists()) { throw new \RuntimeException('该日期佣金已入账,不能修改比例'); } $existing = AgentCommissionRule::query()->where('agent_id', $agent->id) ->where('effective_from', $day)->lockForUpdate()->first(); $flowRate = $flowRate ?? (string) ($existing?->flow_rate ?? $this->rateAt($agent, 'flow_rate', $day)); $profitRate = $profitRate ?? (string) ($existing?->profit_rate ?? $this->rateAt($agent, 'profit_rate', $day)); $parent = $agent->parent; if ($parent && ( bccomp($flowRate, $this->rateAt($parent, 'flow_rate', $day), 4) > 0 || bccomp($profitRate, $this->rateAt($parent, 'profit_rate', $day), 4) > 0 )) { throw new \RuntimeException('下级代理佣金比例不能高于上级'); } $children = Agent::query()->where('parent_id', $agent->id)->get(); $maxFlow = $children->map(fn(Agent $child) => $this->rateAt($child, 'flow_rate', $day))->max() ?? 0; $maxProfit = $children->map(fn(Agent $child) => $this->rateAt($child, 'profit_rate', $day))->max() ?? 0; if (bccomp($flowRate, (string) $maxFlow, 4) < 0 || bccomp($profitRate, (string) $maxProfit, 4) < 0) { throw new \RuntimeException('代理佣金比例不能低于现有下级比例'); } $agent->flow_commission_rate = $flowRate; $agent->profit_commission_rate = $profitRate; $agent->save(); return AgentCommissionRule::query()->updateOrCreate( ['agent_id' => $agent->id, 'effective_from' => $day], ['flow_rate' => $flowRate, 'profit_rate' => $profitRate, 'status' => 1, 'created_by' => $adminId] ); }); } }