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) ->where('created_at', '<', Carbon::parse($day)->addDay()->startOfDay()) ->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) { $result = DB::transaction(function () use ($agent, $row, $day, $credit) { $record = AgentRebateRecord::query()->lockForUpdate()->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(); $partsCredited = 0; if ($credit) { $partsCredited += $this->creditRebatePart($record, 'flow', $day, (int) $agent->id); $partsCredited += $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(); } return ['created' => $isNew ? 1 : 0, 'credited' => $partsCredited]; }, 3); $created += $result['created']; $credited += $result['credited']; } 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 = [(int) $agent->id]; $rows = []; $this->addSegment($rows, $this->gameStats($day, [(int) $agent->id]), $flowAssignment, $profitAssignment, false); $children = Agent::query()->where('parent_id', $agent->id) ->where('created_at', '<', Carbon::parse($day)->addDay()->startOfDay()) ->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); $descendantIds = array_merge($descendantIds, $childIds); $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)); } }