|
@@ -6,28 +6,58 @@ use App\Models\Agent\Agent;
|
|
|
use App\Models\Agent\AgentCommission;
|
|
use App\Models\Agent\AgentCommission;
|
|
|
use App\Models\Agent\AgentCommissionRule;
|
|
use App\Models\Agent\AgentCommissionRule;
|
|
|
use App\Models\Agent\AgentDailyStat;
|
|
use App\Models\Agent\AgentDailyStat;
|
|
|
|
|
+use App\Models\Agent\AgentDailyGameStat;
|
|
|
|
|
+use App\Models\Agent\AgentRebateRecord;
|
|
|
use Carbon\Carbon;
|
|
use Carbon\Carbon;
|
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
|
|
|
|
class AgentCommissionService
|
|
class AgentCommissionService
|
|
|
{
|
|
{
|
|
|
- public function __construct(private AgentReportService $reports, private AgentWalletService $wallets)
|
|
|
|
|
- {
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- public function saveRule(Agent $agent, string $flowRate, string $profitRate, string $effectiveFrom, ?int $adminId): AgentCommissionRule
|
|
|
|
|
- {
|
|
|
|
|
- return $this->saveRates($agent, $flowRate, $profitRate, $effectiveFrom, $adminId);
|
|
|
|
|
|
|
+ public function __construct(
|
|
|
|
|
+ private AgentReportService $reports,
|
|
|
|
|
+ private AgentWalletService $wallets,
|
|
|
|
|
+ private AgentProfitCommissionAssignmentService $profitAssignments
|
|
|
|
|
+ ) {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
public function saveFlowRate(Agent $agent, string $rate, string $effectiveFrom, ?int $adminId): AgentCommissionRule
|
|
public function saveFlowRate(Agent $agent, string $rate, string $effectiveFrom, ?int $adminId): AgentCommissionRule
|
|
|
{
|
|
{
|
|
|
- return $this->saveRates($agent, $rate, null, $effectiveFrom, $adminId);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ 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('该日期流水佣金已入账,不能修改比例');
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- public function saveProfitRate(Agent $agent, string $rate, string $effectiveFrom, ?int $adminId): AgentCommissionRule
|
|
|
|
|
- {
|
|
|
|
|
- return $this->saveRates($agent, null, $rate, $effectiveFrom, $adminId);
|
|
|
|
|
|
|
+ $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
|
|
public function settle(string $date, bool $credit = true): array
|
|
@@ -35,125 +65,134 @@ class AgentCommissionService
|
|
|
$day = Carbon::parse($date)->toDateString();
|
|
$day = Carbon::parse($date)->toDateString();
|
|
|
$created = 0;
|
|
$created = 0;
|
|
|
$credited = 0;
|
|
$credited = 0;
|
|
|
- Agent::query()->where('status', Agent::STATUS_ENABLED)->orderBy('id')->chunkById(100, function ($agents) use ($day, $credit, &$created, &$credited) {
|
|
|
|
|
|
|
+ $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) {
|
|
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);
|
|
$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)
|
|
$rule = AgentCommissionRule::query()->where('agent_id', $agent->id)->where('status', 1)
|
|
|
->where('effective_from', '<=', $day)->orderByDesc('effective_from')->first();
|
|
->where('effective_from', '<=', $day)->orderByDesc('effective_from')->first();
|
|
|
if (!$rule) {
|
|
if (!$rule) {
|
|
|
throw new \RuntimeException("代理 {$agent->id} 缺少 {$day} 可用的佣金比例记录");
|
|
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,
|
|
|
|
|
|
|
+ $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 = !$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();
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ $isNew = !$record->exists;
|
|
|
if ($isNew) {
|
|
if ($isNew) {
|
|
|
- $created++;
|
|
|
|
|
|
|
+ $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,
|
|
|
|
|
+ ];
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
- 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();
|
|
|
|
|
|
|
+ $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([
|
|
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'),
|
|
|
|
|
|
|
+ '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');
|
|
return compact('created', 'credited');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private function rateAt(Agent $agent, string $field, string $day): string
|
|
|
|
|
|
|
+ private function creditRebatePart(AgentRebateRecord $record, string $type, string $day, int $agentId): int
|
|
|
{
|
|
{
|
|
|
- $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};
|
|
|
|
|
|
|
+ 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 saveRates(Agent $agent, ?string $flowRate, ?string $profitRate, string $effectiveFrom, ?int $adminId): AgentCommissionRule
|
|
|
|
|
|
|
+ private function rebateOrderNo(string $day, int $agentId, string $platform, int $gameType): string
|
|
|
{
|
|
{
|
|
|
- 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('代理佣金比例不能低于现有下级比例');
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ return 'AR' . str_replace('-', '', $day) . $agentId
|
|
|
|
|
+ . strtoupper(substr(sha1($platform . '|' . $gameType), 0, 10));
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- $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]
|
|
|
|
|
- );
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ 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;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|