Files
lotteryLaravel/app/Services/AgentSettlement/AgentPeriodAggregator.php
kang a44679665d feat: 增强代理和玩家管理功能
- 在多个控制器中更新权限检查逻辑,确保管理员能够更灵活地管理代理和玩家。
- 在 AdminPlayerStoreController 中引入对玩家创建能力的验证,确保只有具备相应权限的管理员能够创建玩家。
- 更新请求验证逻辑,新增 credit_limit、rebate_rate 和 extra_rebate_rate 字段,以支持更细粒度的玩家管理。
- 在 AgentNodeProfileController 中添加对父代理能力授予的验证,确保子代理的权限在父代理范围内。
- 引入 AgentProfileFieldRules 以简化代理资料更新请求的规则定义,提升代码复用性。
2026-06-04 18:00:50 +08:00

233 lines
8.1 KiB
PHP

<?php
namespace App\Services\AgentSettlement;
use App\Models\AgentNode;
use App\Models\Player;
use Illuminate\Support\Facades\DB;
final class AgentPeriodAggregator
{
public function __construct(
private readonly ShareSettlementCalculator $calculator,
private readonly BetSettlementSnapshotBuilder $snapshotBuilder,
) {}
/**
* @return array{
* players: array<int, array<string, mixed>>,
* agent_edges: array<string, int>,
* agent_subtrees: array<int, array<string, mixed>>,
* platform_share_profit: int,
* }
*/
public function aggregate(int $adminSiteId, string $periodStart, string $periodEnd): array
{
$siteCode = (string) DB::table('admin_sites')->where('id', $adminSiteId)->value('code');
$codeToId = AgentNode::query()
->where('admin_site_id', $adminSiteId)
->pluck('id', 'code')
->mapWithKeys(fn ($id, $code): array => [(string) $code => (int) $id])
->all();
$rows = DB::table('share_ledger as sl')
->join('players as p', 'p.id', '=', 'sl.player_id')
->where('p.site_code', $siteCode)
->whereBetween('sl.settled_at', [$periodStart, $periodEnd])
->select([
'sl.player_id',
'sl.ticket_item_id',
'sl.agent_node_id',
'sl.share_snapshot',
'sl.game_win_loss',
'sl.basic_rebate',
])
->orderBy('sl.id')
->get();
$players = [];
$agentEdges = [];
$agentSubtrees = [];
$platformShareProfit = 0;
foreach ($rows as $row) {
$playerId = (int) $row->player_id;
$snapshot = $this->resolveSnapshotFromLedgerRow($row);
if ($snapshot === null) {
$player = Player::query()->find($playerId);
if ($player === null) {
continue;
}
$built = $this->snapshotBuilder->buildForPlayer($player);
$snapshot = [
'total_shares' => $built['total_shares'],
'chain_codes' => $built['chain_codes'],
];
}
$gameWinLoss = (int) $row->game_win_loss;
$basicRebate = (int) $row->basic_rebate;
$extraRebate = $this->extraRebateForTicketItem(
(int) $row->ticket_item_id,
$periodStart,
$periodEnd,
);
$extraByCode = [];
if ($extraRebate > 0 && $snapshot['chain_codes'] !== []) {
$extraByCode[$snapshot['chain_codes'][0]] = $extraRebate;
}
$result = $this->calculator->calculate(
sharedNetWinLoss: 0,
totalSharesByCode: $snapshot['total_shares'],
extraRebateByCode: $extraByCode,
gameWinLoss: $gameWinLoss,
basicRebate: $basicRebate,
chainFromPlayer: $snapshot['chain_codes'],
);
$net = (int) round($result->playerNetSettlement);
if (! isset($players[$playerId])) {
$players[$playerId] = [
'agent_node_id' => (int) $row->agent_node_id,
'game_win_loss' => 0,
'basic_rebate' => 0,
'extra_rebate' => 0,
'net_amount' => 0,
];
}
$players[$playerId]['game_win_loss'] += $gameWinLoss;
$players[$playerId]['basic_rebate'] += $basicRebate;
$players[$playerId]['extra_rebate'] += $extraRebate;
$players[$playerId]['net_amount'] += $net;
foreach ($result->tierSettlements as $edge => $amount) {
$agentEdges[$edge] = ($agentEdges[$edge] ?? 0) + (int) round($amount);
}
$platformShareProfit += (int) round($result->finalProfits['platform'] ?? 0);
$pathIds = $this->resolveAgentPathIds($row, $snapshot, $codeToId);
foreach ($pathIds as $agentId) {
if (! isset($agentSubtrees[$agentId])) {
$agentSubtrees[$agentId] = [
'gross_win_loss' => 0,
'basic_rebate' => 0,
'extra_rebate' => 0,
'share_profit' => 0,
'player_count' => 0,
'_players_seen' => [],
];
}
$agentSubtrees[$agentId]['gross_win_loss'] += $gameWinLoss;
$agentSubtrees[$agentId]['basic_rebate'] += $basicRebate;
$agentSubtrees[$agentId]['extra_rebate'] += $extraRebate;
if (! in_array($playerId, $agentSubtrees[$agentId]['_players_seen'], true)) {
$agentSubtrees[$agentId]['_players_seen'][] = $playerId;
$agentSubtrees[$agentId]['player_count']++;
}
}
foreach ($snapshot['chain_codes'] as $code) {
$agentId = $codeToId[$code] ?? 0;
if ($agentId <= 0) {
continue;
}
$profit = (int) round($result->finalProfits[$code] ?? 0);
$agentSubtrees[$agentId]['share_profit'] = ($agentSubtrees[$agentId]['share_profit'] ?? 0) + $profit;
}
}
foreach ($agentSubtrees as $id => $subtree) {
unset($agentSubtrees[$id]['_players_seen']);
}
return [
'players' => $players,
'agent_edges' => $agentEdges,
'agent_subtrees' => $agentSubtrees,
'platform_share_profit' => $platformShareProfit,
];
}
/**
* @param array{chain_codes: list<string>, total_shares: array<string, float>} $snapshot
* @param array<string, int> $codeToId
* @return list<int>
*/
private function resolveAgentPathIds(object $row, array $snapshot, array $codeToId): array
{
$raw = $row->share_snapshot ?? null;
if ($raw !== null && $raw !== '') {
$decoded = is_string($raw) ? json_decode($raw, true) : $raw;
if (is_array($decoded) && is_array($decoded['agent_path'] ?? null)) {
return array_values(array_map(intval(...), $decoded['agent_path']));
}
}
$ids = [];
foreach ($snapshot['chain_codes'] as $code) {
$id = $codeToId[$code] ?? 0;
if ($id > 0) {
$ids[] = $id;
}
}
return $ids;
}
public function siteIdForPeriod(int $periodId): int
{
return (int) DB::table('settlement_periods')->where('id', $periodId)->value('admin_site_id');
}
private function extraRebateForTicketItem(int $ticketItemId, string $periodStart, string $periodEnd): int
{
if ($ticketItemId <= 0) {
return 0;
}
return (int) DB::table('rebate_records')
->where('ticket_item_id', $ticketItemId)
->where('rebate_type', 'extra')
->whereIn('status', ['accrued', 'reversed'])
->whereBetween('created_at', [$periodStart, $periodEnd])
->sum('rebate_amount');
}
/**
* @return array{total_shares: array<string, float>, chain_codes: list<string>}|null
*/
private function resolveSnapshotFromLedgerRow(object $row): ?array
{
$raw = $row->share_snapshot ?? null;
if ($raw === null || $raw === '') {
return null;
}
$decoded = is_string($raw) ? json_decode($raw, true) : $raw;
if (! is_array($decoded)) {
return null;
}
$totalShares = $decoded['total_shares'] ?? null;
$chainCodes = $decoded['chain_codes'] ?? null;
if (! is_array($totalShares) || ! is_array($chainCodes) || $chainCodes === []) {
return null;
}
$shares = [];
foreach ($totalShares as $code => $rate) {
$shares[(string) $code] = (float) $rate;
}
return [
'total_shares' => $shares,
'chain_codes' => array_values(array_map(strval(...), $chainCodes)),
];
}
}