$rows credit_ledger 行(含 reason、ref_type、ref_id、amount、created_at) * @param array $ticketRefs * @return list> */ public function simplifyCreditRows( array $rows, array $ticketRefs, callable $formatHold, callable $formatSettlement, ): array { /** @var list $holdRows */ $holdRows = []; /** @var array> $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 $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 $holdRows * @param list $mergedSettlements * @param array $ticketRefs * @return list */ 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 $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 $entries * @param array $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); } }