- 在多个控制器中引入 SettlementPartyEnrichment 服务,以优化代理结算和账单的处理逻辑。 - 更新 AgentSettlementBillIndexController 和 AgentSettlementBillShowController,支持根据账单 ID 和关键字进行查询。 - 在 AgentSettlementPeriodCloseController 中添加对站点管理权限的验证,确保只有具备相应权限的管理员能够关闭账期。 - 在 AgentSettlementPeriodIndexController 中更新账期数据的返回格式,提升数据的完整性和可用性。 - 引入对相对占成比例的支持,增强代理资料的管理能力,确保数据一致性。
238 lines
8.0 KiB
PHP
238 lines
8.0 KiB
PHP
<?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);
|
||
}
|
||
}
|