['transfer_in'], 'transfer_out' => ['transfer_out'], 'refund' => ['transfer_out_refund'], 'bet' => ['bet'], 'prize' => ['prize'], ]; public function __invoke(Request $request): JsonResponse { $player = $request->lotteryPlayer(); abort_if($player === null, 500, 'lottery_player missing'); $perPage = $this->perPage($request, 'size', 20, 100); $page = $this->page($request); $pendingPayload = $this->pendingReconcilePayload((int) $player->id); $bizFilter = $this->resolveBizTypeFilter((string) $request->query('type', '')); if (is_array($bizFilter) && $bizFilter === []) { return ApiResponse::success([ 'items' => [], 'total' => 0, 'page' => $page, 'per_page' => $perPage, 'pending_reconcile' => $pendingPayload, ]); } $query = WalletTxn::query() ->where('player_id', $player->id) ->with('wallet') ->orderByDesc('id'); if ($bizFilter !== null) { $query->whereIn('biz_type', $bizFilter); } $paginator = $query->paginate($perPage, ['*'], 'page', $page); $items = $paginator->getCollection()->map(fn (WalletTxn $txn) => $this->formatTxn($txn)); return ApiResponse::success([ 'items' => $items, 'total' => $paginator->total(), 'page' => $paginator->currentPage(), 'per_page' => $paginator->perPage(), 'pending_reconcile' => $pendingPayload, ]); } /** * @return list> */ private function pendingReconcilePayload(int $playerId): array { return TransferOrder::query() ->where('player_id', $playerId) ->where('status', 'pending_reconcile') ->orderByDesc('id') ->limit(50) ->get() ->map(fn (TransferOrder $o) => $this->formatPendingOrder($o)) ->all(); } /** * @return list|null null 表示不过滤;空列表表示过滤后无合法 type(结果应为空) */ private function resolveBizTypeFilter(string $raw): ?array { $raw = trim($raw); if ($raw === '') { return null; } $parts = array_filter(array_map('trim', explode(',', $raw))); if ($parts === []) { return null; } $biz = []; foreach ($parts as $p) { $key = Str::lower($p); if (! isset(self::TYPE_TO_BIZ[$key])) { continue; } foreach (self::TYPE_TO_BIZ[$key] as $b) { $biz[] = $b; } } return array_values(array_unique($biz)); } /** * @return array */ private function formatTxn(WalletTxn $txn): array { $currency = $txn->wallet?->currency_code ?? ''; $amount = (int) $txn->amount; $balanceAfter = (int) $txn->balance_after; return [ 'log_id' => $txn->txn_no, 'type' => $this->bizToPublicType((string) $txn->biz_type), 'biz_type' => $txn->biz_type, 'amount' => $this->signedAmount($txn), 'amount_formatted' => CurrencyFormatter::fromMinor($amount), 'amount_abs' => $amount, 'amount_abs_formatted' => CurrencyFormatter::fromMinor($amount), 'direction' => (int) $txn->direction === 1 ? 'in' : 'out', 'currency_code' => $currency, 'balance_after' => $balanceAfter, 'balance_after_formatted' => CurrencyFormatter::fromMinor($balanceAfter), 'ref_id' => $txn->biz_no, 'idempotent_key' => $txn->idempotent_key, 'external_ref_no' => $txn->external_ref_no, 'status' => $txn->status, 'remark' => $txn->remark, 'created_at' => $txn->created_at?->toIso8601String(), ]; } private function bizToPublicType(string $biz): string { return match ($biz) { 'transfer_out_refund' => 'refund', default => $biz, }; } private function signedAmount(WalletTxn $txn): int { $a = (int) $txn->amount; return (int) $txn->direction === 1 ? $a : -$a; } /** * @return array */ private function formatPendingOrder(TransferOrder $order): array { $amount = (int) $order->amount; return [ 'transfer_no' => $order->transfer_no, 'direction' => $order->direction, 'type' => $order->direction === 'in' ? 'transfer_in' : 'transfer_out', 'currency_code' => $order->currency_code, 'amount' => $amount, 'amount_formatted' => CurrencyFormatter::fromMinor($amount), 'status' => $order->status, 'fail_reason' => $order->fail_reason, 'idempotent_key' => $order->idempotent_key, 'created_at' => $order->created_at?->toIso8601String(), ]; } }