| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238 |
- <?php
- namespace App\Services\Agent;
- use App\Models\Agent\Agent;
- use App\Models\Agent\AgentCommission;
- 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 AgentFlowCommissionAssignmentService $flowAssignments,
- private AgentProfitCommissionAssignmentService $profitAssignments
- ) {
- }
- 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} 存在未完成旧佣金记录,请先人工核对");
- }
- $rows = $this->spreadRows($agent, $day);
- foreach ($rows as $row) {
- $record = AgentRebateRecord::query()->firstOrNew([
- 'settlement_date' => $day, 'agent_id' => $agent->id,
- 'platform' => (string) $row['platform'], 'game_type' => (int) $row['game_type'],
- ]);
- $isNew = !$record->exists;
- if ($isNew) {
- $record->fill([
- 'order_no' => $record->order_no ?: $this->rebateOrderNo($day, (int) $agent->id, (string) $row['platform'], (int) $row['game_type']),
- 'bet_count' => (int) $row['bet_count'], 'bet_amount' => $row['bet_amount'],
- 'valid_bet_amount' => $row['valid_bet_amount'], 'win_loss' => $row['win_loss'],
- 'flow_rate' => $row['flow_rate'], 'flow_commission' => $row['flow_commission'],
- 'flow_status' => AgentRebateRecord::STATUS_PENDING,
- 'profit_rate' => $row['profit_rate'], 'profit_commission' => $row['profit_commission'],
- 'profit_status' => AgentRebateRecord::STATUS_PENDING,
- 'snapshot' => $row['snapshot'],
- ]);
- } elseif ($record->flow_status === AgentRebateRecord::STATUS_PENDING
- && $record->profit_status === AgentRebateRecord::STATUS_PENDING) {
- $record->bet_count = (int) $row['bet_count'];
- $record->bet_amount = $row['bet_amount'];
- $record->valid_bet_amount = $row['valid_bet_amount'];
- $record->win_loss = $row['win_loss'];
- $record->flow_rate = $row['flow_rate'];
- $record->flow_commission = $row['flow_commission'];
- $record->profit_rate = $row['profit_rate'];
- $record->profit_commission = $row['profit_commission'];
- $record->snapshot = $row['snapshot'];
- }
- $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');
- }
- public static function rateSpread(string $parentRate, string $childRate): string
- {
- $diff = bcsub($parentRate, $childRate, 4);
- return bccomp($diff, '0', 4) > 0 ? $diff : '0.0000';
- }
- public static function amountByRate(string $base, string $rate): string
- {
- return bcdiv(bcmul($base, $rate, 8), '100', 4);
- }
- public static function profitBase(string $winLoss): string
- {
- return bccomp($winLoss, '0', 4) < 0 ? bcsub('0', $winLoss, 4) : '0.0000';
- }
- private function spreadRows(Agent $agent, string $day): array
- {
- $flowAssignment = $this->flowAssignments->at($agent, $day);
- $profitAssignment = $this->profitAssignments->at($agent, $day);
- $descendantIds = $this->reports->visibleAgentIds($agent);
- $rows = [];
- $this->addSegment($rows, $this->gameStats($day, [(int) $agent->id]), $flowAssignment, $profitAssignment, false);
- $children = Agent::query()->where('parent_id', $agent->id)->orderBy('id')->get();
- $childSnapshots = [];
- foreach ($children as $child) {
- $childFlow = $this->flowAssignments->at($child, $day);
- $childProfit = $this->profitAssignments->at($child, $day);
- $childIds = $this->reports->visibleAgentIds($child);
- $this->addSegment($rows, $this->gameStats($day, $childIds), $flowAssignment, $profitAssignment, true, $childFlow, $childProfit);
- $childSnapshots[] = [
- 'agent_id' => (int) $child->id,
- 'agent_ids' => $childIds,
- 'flow_profile_id' => $childFlow->flow_commission_profile_id,
- 'flow_assignment_id' => $childFlow->id,
- 'profit_profile_id' => $childProfit->profit_commission_profile_id,
- 'profit_assignment_id' => $childProfit->id,
- ];
- }
- $snapshot = [
- 'mode' => 'spread',
- 'agent_ids' => $descendantIds,
- 'flow_profile_id' => $flowAssignment->flow_commission_profile_id,
- 'flow_assignment_id' => $flowAssignment->id,
- 'profit_profile_id' => $profitAssignment->profit_commission_profile_id,
- 'profit_assignment_id' => $profitAssignment->id,
- 'children' => $childSnapshots,
- ];
- foreach ($rows as &$row) {
- $row['flow_rate'] = $flowAssignment->rateForGameType((int) $row['game_type']);
- $row['profit_rate'] = $profitAssignment->rateForGameType((int) $row['game_type']);
- $row['snapshot'] = $snapshot;
- }
- unset($row);
- return array_values($rows);
- }
- private function addSegment(
- array &$rows,
- $stats,
- $flowAssignment,
- $profitAssignment,
- bool $spread,
- $childFlow = null,
- $childProfit = null
- ): void {
- foreach ($stats as $stat) {
- $gameType = (int) $stat->game_type;
- $flowRate = $spread
- ? self::rateSpread($flowAssignment->rateForGameType($gameType), $childFlow->rateForGameType($gameType))
- : $flowAssignment->rateForGameType($gameType);
- $profitRate = $spread
- ? self::rateSpread($profitAssignment->rateForGameType($gameType), $childProfit->rateForGameType($gameType))
- : $profitAssignment->rateForGameType($gameType);
- $key = (string) $stat->platform . "\0" . $gameType;
- if (!isset($rows[$key])) {
- $rows[$key] = [
- 'platform' => (string) $stat->platform,
- 'game_type' => $gameType,
- 'bet_count' => 0,
- 'bet_amount' => '0.0000',
- 'valid_bet_amount' => '0.0000',
- 'win_loss' => '0.0000',
- 'flow_commission' => '0.0000',
- 'profit_commission' => '0.0000',
- ];
- }
- $rows[$key]['bet_count'] += (int) $stat->bet_count;
- $rows[$key]['bet_amount'] = bcadd($rows[$key]['bet_amount'], (string) $stat->bet_amount, 4);
- $rows[$key]['valid_bet_amount'] = bcadd($rows[$key]['valid_bet_amount'], (string) $stat->valid_bet_amount, 4);
- $rows[$key]['win_loss'] = bcadd($rows[$key]['win_loss'], (string) $stat->win_loss, 4);
- $rows[$key]['flow_commission'] = bcadd(
- $rows[$key]['flow_commission'],
- self::amountByRate((string) $stat->valid_bet_amount, $flowRate),
- 4
- );
- $rows[$key]['profit_commission'] = bcadd(
- $rows[$key]['profit_commission'],
- self::amountByRate(self::profitBase((string) $stat->win_loss), $profitRate),
- 4
- );
- }
- }
- private function gameStats(string $day, array $agentIds)
- {
- $agentIds = array_values(array_unique(array_filter(array_map('intval', $agentIds))));
- if ($agentIds === []) return collect();
- return AgentDailyGameStat::query()->where('stat_date', $day)->whereIn('agent_id', $agentIds)
- ->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();
- }
- 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));
- }
- }
|