Files
lotteryLaravel/app/Services/AgentSettlement/CreditLedgerBetFlowPresenter.php
kang 2d32f006c5 feat: 增强代理结算和账单管理功能
- 在多个控制器中引入 SettlementPartyEnrichment 服务,以优化代理结算和账单的处理逻辑。
- 更新 AgentSettlementBillIndexController 和 AgentSettlementBillShowController,支持根据账单 ID 和关键字进行查询。
- 在 AgentSettlementPeriodCloseController 中添加对站点管理权限的验证,确保只有具备相应权限的管理员能够关闭账期。
- 在 AgentSettlementPeriodIndexController 中更新账期数据的返回格式,提升数据的完整性和可用性。
- 引入对相对占成比例的支持,增强代理资料的管理能力,确保数据一致性。
2026-06-05 18:00:56 +08:00

238 lines
8.0 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Services\AgentSettlement;
use Carbon\Carbon;
/**
* 信用盘下注流水对外展示:待开奖「下注冻结」+ 已结算「开奖结算」(每注单一条,不重复展示占用)。
*/
final class CreditLedgerBetFlowPresenter
{
public const DISPLAY_BET_HOLD = 'bet_hold';
public const DISPLAY_GAME_SETTLEMENT = 'game_settlement';
private const SETTLEMENT_REASONS = ['bet_hold_release', 'game_settlement_loss'];
/**
* @param list<object> $rows credit_ledger 行(含 reason、ref_type、ref_id、amount、created_at
* @param array<int, array{play_code: string|null, draw_no: string|null, ticket_item_id: int, actual_deduct_amount?: int}> $ticketRefs
* @return list<array<string, mixed>>
*/
public function simplifyCreditRows(
array $rows,
array $ticketRefs,
callable $formatHold,
callable $formatSettlement,
): array {
/** @var list<object> $holdRows */
$holdRows = [];
/** @var array<int, list<object>> $byTicket */
$byTicket = [];
foreach ($rows as $row) {
$reason = (string) ($row->reason ?? '');
if ($reason === self::DISPLAY_BET_HOLD) {
$holdRows[] = $row;
continue;
}
if (! in_array($reason, self::SETTLEMENT_REASONS, true)) {
continue;
}
$ticketId = $this->ticketItemId($row);
if ($ticketId <= 0) {
continue;
}
$byTicket[$ticketId][] = $row;
}
/** @var list<object> $mergedSettlements */
$mergedSettlements = [];
foreach ($byTicket as $ticketId => $entries) {
$merged = $this->mergeSettlementEntries($ticketId, $entries, $ticketRefs);
if ($merged !== null) {
$mergedSettlements[] = $merged;
}
}
$visibleHolds = $this->holdsWithoutSettledMatch($holdRows, $mergedSettlements, $ticketRefs);
$holdItems = array_map($formatHold, $visibleHolds);
$settlementItems = array_map(
fn (object $merged): array => $formatSettlement($merged, $ticketRefs),
$mergedSettlements,
);
$all = array_merge($holdItems, $settlementItems);
usort($all, static function (array $a, array $b): int {
$ta = isset($a['created_at']) ? strtotime((string) $a['created_at']) : 0;
$tb = isset($b['created_at']) ? strtotime((string) $b['created_at']) : 0;
if ($ta === $tb) {
return (int) ($b['id'] ?? 0) <=> (int) ($a['id'] ?? 0);
}
return $tb <=> $ta;
});
return $all;
}
/**
* 已开奖注单不再展示下注占用,避免与开奖结算同额时出现「扣两次」误解。
*
* @param list<object> $holdRows
* @param list<object> $mergedSettlements
* @param array<int, array{actual_deduct_amount?: int}> $ticketRefs
* @return list<object>
*/
private function holdsWithoutSettledMatch(
array $holdRows,
array $mergedSettlements,
array $ticketRefs,
): array {
if ($holdRows === [] || $mergedSettlements === []) {
return $holdRows;
}
$sortedHolds = $holdRows;
usort($sortedHolds, static function (object $a, object $b): int {
$ta = $a->created_at ?? null;
$tb = $b->created_at ?? null;
if ($ta === null || $tb === null) {
return (int) ($a->id ?? 0) <=> (int) ($b->id ?? 0);
}
return Carbon::parse($ta)->getTimestamp() <=> Carbon::parse($tb)->getTimestamp();
});
$consumedHoldIds = [];
foreach ($mergedSettlements as $settlement) {
$playerId = (int) ($settlement->player_id ?? 0);
$ticketId = (int) ($settlement->ref_id ?? 0);
$stake = $this->stakeMinorForSettlement($settlement, $ticketId, $ticketRefs);
if ($playerId <= 0 || $stake <= 0) {
continue;
}
$settledAt = $settlement->created_at ?? null;
foreach ($sortedHolds as $hold) {
$holdId = (int) ($hold->id ?? 0);
if ($holdId <= 0 || isset($consumedHoldIds[$holdId])) {
continue;
}
if ((int) ($hold->player_id ?? 0) !== $playerId) {
continue;
}
if (abs((int) ($hold->amount ?? 0)) !== $stake) {
continue;
}
$holdAt = $hold->created_at ?? null;
if ($holdAt !== null && $settledAt !== null
&& Carbon::parse($holdAt)->gt(Carbon::parse($settledAt))) {
continue;
}
$consumedHoldIds[$holdId] = true;
break;
}
}
return array_values(array_filter(
$holdRows,
static fn (object $hold): bool => ! isset($consumedHoldIds[(int) ($hold->id ?? 0)]),
));
}
/**
* @param array<int, array{actual_deduct_amount?: int}> $ticketRefs
*/
private function stakeMinorForSettlement(object $settlement, int $ticketId, array $ticketRefs): int
{
$fromLoss = abs((int) ($settlement->amount ?? 0));
if ($fromLoss > 0) {
return $fromLoss;
}
return (int) ($ticketRefs[$ticketId]['actual_deduct_amount'] ?? 0);
}
/**
* @param list<object> $entries
* @param array<int, array{actual_deduct_amount?: int}> $ticketRefs
*/
private function mergeSettlementEntries(int $ticketId, array $entries, array $ticketRefs): ?object
{
$loss = null;
$release = null;
$latestAt = null;
foreach ($entries as $entry) {
$reason = (string) ($entry->reason ?? '');
if ($reason === 'game_settlement_loss') {
$loss = $entry;
} elseif ($reason === 'bet_hold_release') {
$release = $entry;
}
$at = $entry->created_at ?? null;
if ($at !== null && ($latestAt === null || Carbon::parse($at)->gt(Carbon::parse($latestAt)))) {
$latestAt = $at;
}
}
$primary = $loss ?? $release;
if ($primary === null) {
return null;
}
$signed = $loss !== null
? (int) $loss->amount
: 0;
return (object) [
'id' => (int) ($loss->id ?? $release->id ?? 0),
'amount' => $signed,
'reason' => self::DISPLAY_GAME_SETTLEMENT,
'ref_type' => 'ticket_item',
'ref_id' => $ticketId,
'created_at' => $latestAt ?? $primary->created_at,
'player_id' => $primary->player_id ?? null,
'site_code' => $primary->site_code ?? null,
'site_player_id' => $primary->site_player_id ?? null,
'username' => $primary->username ?? null,
'nickname' => $primary->nickname ?? null,
'agent_node_id' => $primary->agent_node_id ?? null,
'funding_mode' => $primary->funding_mode ?? null,
'auth_source' => $primary->auth_source ?? null,
'default_currency' => $primary->default_currency ?? null,
'direct_agent_id' => $primary->direct_agent_id ?? null,
'direct_agent_code' => $primary->direct_agent_code ?? null,
'direct_agent_name' => $primary->direct_agent_name ?? null,
'parent_agent_id' => $primary->parent_agent_id ?? null,
'parent_agent_code' => $primary->parent_agent_code ?? null,
'parent_agent_name' => $primary->parent_agent_name ?? null,
'stake_minor' => (int) ($ticketRefs[$ticketId]['actual_deduct_amount'] ?? abs($signed)),
];
}
private function ticketItemId(object $row): int
{
if ((string) ($row->ref_type ?? '') !== 'ticket_item') {
return 0;
}
return (int) ($row->ref_id ?? 0);
}
}