lifetimeBusinessDateBounds(); } $dateFrom = $fromRaw !== '' ? $fromRaw : $toRaw; $dateTo = $toRaw !== '' ? $toRaw : $dateFrom; if ($dateFrom > $dateTo) { [$dateFrom, $dateTo] = [$dateTo, $dateFrom]; } return ['date_from' => $dateFrom, 'date_to' => $dateTo]; } /** * @return array{date_from: string, date_to: string} */ public function resolveDashboardPeriod(string $period, ?string $dateFrom, ?string $dateTo): array { $today = now()->toDateString(); $range = match ($period) { 'today' => ['date_from' => $today, 'date_to' => $today], 'last_7_days' => [ 'date_from' => now()->subDays(6)->toDateString(), 'date_to' => $today, ], 'last_30_days' => [ 'date_from' => now()->subDays(29)->toDateString(), 'date_to' => $today, ], 'this_month' => [ 'date_from' => now()->startOfMonth()->toDateString(), 'date_to' => $today, ], 'lifetime' => $this->lifetimeBusinessDateBounds(), 'custom' => [ 'date_from' => $dateFrom !== null && $dateFrom !== '' ? $dateFrom : $today, 'date_to' => $dateTo !== null && $dateTo !== '' ? $dateTo : $today, ], default => ['date_from' => $today, 'date_to' => $today], }; $from = $range['date_from']; $to = $range['date_to']; if ($from > $to) { [$from, $to] = [$to, $from]; } return ['date_from' => $from, 'date_to' => $to]; } /** * @return array{date_from: string, date_to: string} */ private function lifetimeBusinessDateBounds(): array { $today = now()->toDateString(); $bounds = DB::table('draws as d') ->join('ticket_orders as o', 'o.draw_id', '=', 'd.id') ->selectRaw('MIN(d.business_date) as date_from') ->selectRaw('MAX(d.business_date) as date_to') ->first(); $from = $this->formatBusinessDateValue($bounds?->date_from) ?? $today; $to = $this->formatBusinessDateValue($bounds?->date_to) ?? $today; return ['date_from' => $from, 'date_to' => $to]; } /** * @return array{ * total_bet_minor: int, * total_payout_minor: int, * approx_house_gross_minor: int, * draw_count: int, * business_day_count: int * } */ public function periodFinanceTotals(string $dateFrom, string $dateTo): array { $rows = $this->dailyProfitRows($dateFrom, $dateTo); $totalBet = 0; $totalPayout = 0; $totalGross = 0; foreach ($rows as $row) { $totalBet += (int) $row['total_bet_minor']; $totalPayout += (int) $row['total_payout_minor']; $totalGross += (int) $row['approx_house_gross_minor']; } $activity = DB::table('draws as d') ->join('ticket_orders as o', 'o.draw_id', '=', 'd.id') ->whereBetween('d.business_date', [$dateFrom, $dateTo]) ->selectRaw('COUNT(DISTINCT d.id) as draw_count') ->selectRaw('COUNT(DISTINCT d.business_date) as business_day_count') ->first(); return [ 'total_bet_minor' => $totalBet, 'total_payout_minor' => $totalPayout, 'approx_house_gross_minor' => $totalGross, 'draw_count' => (int) ($activity->draw_count ?? 0), 'business_day_count' => (int) ($activity->business_day_count ?? 0), ]; } /** * 连续业务日序列(无数据日补零),用于趋势图。 * * @return list> */ public function dailyProfitSeriesFilled(string $dateFrom, string $dateTo, int $maxDays = 90): array { $from = Carbon::parse($dateFrom)->startOfDay(); $to = Carbon::parse($dateTo)->startOfDay(); $spanDays = (int) $from->diffInDays($to) + 1; $chartFrom = $dateFrom; $chartTo = $dateTo; $truncated = false; if ($spanDays > $maxDays) { $chartFrom = $to->copy()->subDays($maxDays - 1)->format('Y-m-d'); $truncated = true; } $indexed = collect($this->dailyProfitRows($chartFrom, $chartTo))->keyBy('business_date'); $cursor = Carbon::parse($chartFrom)->startOfDay(); $end = Carbon::parse($chartTo)->startOfDay(); $series = []; while ($cursor <= $end) { $key = $cursor->format('Y-m-d'); $series[] = $indexed[$key] ?? [ 'business_date' => $key, 'total_bet_minor' => 0, 'total_payout_minor' => 0, 'approx_house_gross_minor' => 0, ]; $cursor->addDay(); } return [ 'series' => $series, 'chart_date_from' => $chartFrom, 'chart_date_to' => $chartTo, 'truncated' => $truncated, 'span_days' => $spanDays, ]; } /** * @return list> */ public function playDimensionBreakdownRows( string $dateFrom, string $dateTo, ?string $playCode = null, int $limit = 12, ): array { return $this->playDimensionBaseQuery($playCode, $dateFrom, $dateTo) ->orderByDesc('total_bet_minor') ->limit($limit) ->get() ->map(static function (object $row): array { return [ 'play_code' => (string) $row->play_code, 'dimension' => (int) $row->dimension, 'total_bet_minor' => (int) $row->total_bet_minor, 'total_payout_minor' => (int) $row->total_payout_minor, 'approx_house_gross_minor' => (int) $row->approx_house_gross_minor, ]; }) ->values() ->all(); } public function resolvePeriodCurrencyCode(string $dateFrom, string $dateTo): ?string { $currencyCode = (string) (DB::table('ticket_orders as o') ->join('draws as d', 'd.id', '=', 'o.draw_id') ->whereBetween('d.business_date', [$dateFrom, $dateTo]) ->orderByDesc('o.id') ->value('o.currency_code') ?? ''); return $currencyCode !== '' ? $currencyCode : null; } public function dailyProfitPaginated(string $dateFrom, string $dateTo, int $page, int $perPage): LengthAwarePaginator { $rows = $this->dailyProfitRows($dateFrom, $dateTo); $total = count($rows); $offset = max(0, ($page - 1) * $perPage); $items = array_slice($rows, $offset, $perPage); return new PaginatorInstance($items, $total, $perPage, $page, [ 'path' => PaginatorInstance::resolveCurrentPath(), ]); } /** * @return list> */ public function dailyProfitRows(string $dateFrom, string $dateTo): array { $betSub = DB::table('ticket_orders') ->selectRaw('draw_id, SUM(total_actual_deduct) as total_bet_minor') ->groupBy('draw_id'); $payoutSub = DB::table('ticket_items') ->selectRaw('draw_id, SUM(win_amount + jackpot_win_amount) as total_payout_minor') ->groupBy('draw_id'); return DB::table('draws as d') ->whereBetween('d.business_date', [$dateFrom, $dateTo]) ->leftJoinSub($betSub, 'b', 'b.draw_id', '=', 'd.id') ->leftJoinSub($payoutSub, 'p', 'p.draw_id', '=', 'd.id') ->groupBy('d.business_date') ->orderBy('d.business_date') ->get([ 'd.business_date', DB::raw('COALESCE(SUM(b.total_bet_minor), 0) as total_bet_minor'), DB::raw('COALESCE(SUM(p.total_payout_minor), 0) as total_payout_minor'), DB::raw('COALESCE(SUM(b.total_bet_minor), 0) - COALESCE(SUM(p.total_payout_minor), 0) as approx_house_gross_minor'), ]) ->map(static function (object $row): array { $businessDate = $row->business_date instanceof Carbon ? $row->business_date->format('Y-m-d') : (string) $row->business_date; return [ 'business_date' => $businessDate, 'total_bet_minor' => (int) $row->total_bet_minor, 'total_payout_minor' => (int) $row->total_payout_minor, 'approx_house_gross_minor' => (int) $row->approx_house_gross_minor, ]; }) ->values() ->all(); } /** * 全平台历史累计投注/派彩/盈亏(与 daily-profit 同口径,不限业务日)。 * * @return array{ * currency_code: ?string, * total_bet_minor: int, * total_payout_minor: int, * approx_house_gross_minor: int, * draw_count: int, * business_day_count: int, * date_from: ?string, * date_to: ?string * } */ public function platformLifetimeTotals(): array { $totalBetMinor = (int) DB::table('ticket_orders')->sum('total_actual_deduct'); $payoutAgg = DB::table('ticket_items') ->selectRaw('COALESCE(SUM(win_amount), 0) as win_minor, COALESCE(SUM(jackpot_win_amount), 0) as jackpot_minor') ->first(); $totalWinMinor = (int) ($payoutAgg->win_minor ?? 0); $totalJackpotMinor = (int) ($payoutAgg->jackpot_minor ?? 0); $totalPayoutMinor = $totalWinMinor + $totalJackpotMinor; $activity = DB::table('draws as d') ->join('ticket_orders as o', 'o.draw_id', '=', 'd.id') ->selectRaw('COUNT(DISTINCT d.id) as draw_count') ->selectRaw('COUNT(DISTINCT d.business_date) as business_day_count') ->selectRaw('MIN(d.business_date) as date_from') ->selectRaw('MAX(d.business_date) as date_to') ->first(); $drawCount = (int) ($activity->draw_count ?? 0); $businessDayCount = (int) ($activity->business_day_count ?? 0); $dateFrom = $this->formatBusinessDateValue($activity?->date_from); $dateTo = $this->formatBusinessDateValue($activity?->date_to); $currencyCode = (string) (DB::table('ticket_orders')->orderByDesc('id')->value('currency_code') ?? ''); return [ 'currency_code' => $currencyCode !== '' ? $currencyCode : null, 'total_bet_minor' => $totalBetMinor, 'total_win_minor' => $totalWinMinor, 'total_jackpot_minor' => $totalJackpotMinor, 'total_payout_minor' => $totalPayoutMinor, 'approx_house_gross_minor' => $totalBetMinor - $totalPayoutMinor, 'order_count' => (int) DB::table('ticket_orders')->count(), 'ticket_item_count' => (int) DB::table('ticket_items')->count(), 'draw_count' => $drawCount, 'business_day_count' => $businessDayCount, 'date_from' => $dateFrom, 'date_to' => $dateTo, ]; } public function playerWinLossPaginated( ?int $playerId, string $dateFrom, string $dateTo, int $page, int $perPage, ): LengthAwarePaginator { $query = $this->playerWinLossBaseQuery($playerId, $dateFrom, $dateTo); return $query->paginate($perPage, ['*'], 'page', $page); } public function playDimensionPaginated( ?string $playCode, string $dateFrom, string $dateTo, int $page, int $perPage, ): LengthAwarePaginator { $query = $this->playDimensionBaseQuery($playCode, $dateFrom, $dateTo); return $query->paginate($perPage, ['*'], 'page', $page); } public function rebateCommissionPaginated( ?string $playCode, string $dateFrom, string $dateTo, int $page, int $perPage, ): LengthAwarePaginator { $query = $this->rebateCommissionBaseQuery($playCode, $dateFrom, $dateTo); return $query->paginate($perPage, ['*'], 'page', $page); } /** * @return list> */ public function reportRows(string $reportType, ?array $filterJson): array { $range = $this->resolveDateRange($filterJson); $dateFrom = $range['date_from']; $dateTo = $range['date_to']; return match ($reportType) { 'draw_profit_summary' => $this->drawProfitExportRows($filterJson), 'daily_profit_summary' => $this->dailyProfitExportRows($dateFrom, $dateTo), 'player_win_loss' => $this->playerWinLossExportRows($filterJson, $dateFrom, $dateTo), 'play_dimension_report' => $this->playDimensionExportRows($filterJson, $dateFrom, $dateTo), 'rebate_commission_report' => $this->rebateCommissionExportRows($filterJson, $dateFrom, $dateTo), 'audit_operation_report' => $this->auditExportRows($filterJson, $dateFrom, $dateTo), 'wallet_transfer_report', 'transfer_orders_daily' => $this->transferOrdersExportRows($filterJson, $dateFrom, $dateTo), 'wallet_txns_daily' => $this->walletTxnsExportRows($filterJson, $dateFrom, $dateTo), 'hot_number_risk_report' => $this->hotNumberRiskExportRows($filterJson), 'sold_out_number_report' => $this->soldOutNumberExportRows($filterJson), default => [ ['报表类型', '开始日期', '结束日期'], [$this->reportLabel($reportType), $dateFrom, $dateTo], ], }; } public function resolveOutputPathSuffix(string $reportType, ?array $filterJson, string $dateFrom, string $dateTo): string { if (in_array($reportType, ['draw_profit_summary', 'hot_number_risk_report', 'sold_out_number_report'], true)) { $draw = $this->resolveDrawForReport($filterJson); if ($draw !== null) { $suffix = (string) $draw->draw_no; $number = $this->normalizedNumberFromFilters($filterJson); if ($reportType === 'hot_number_risk_report' && $number !== null) { return $suffix.'_'.$number; } return $suffix; } } return $dateFrom.'_'.$dateTo; } public function reportLabel(string $reportType): string { return match ($reportType) { 'draw_profit_summary' => '期号盈亏', 'daily_profit_summary' => '每日盈亏汇总', 'player_win_loss' => '玩家输赢报表', 'wallet_transfer_report', 'wallet_txns_daily', 'transfer_orders_daily' => '玩家转入转出报表', 'hot_number_risk_report' => '热门号码风险报表', 'play_dimension_report' => '玩法维度报表', 'sold_out_number_report' => '售罄号码报表', 'rebate_commission_report' => '佣金回水报表', 'audit_operation_report' => '后台操作审计报表', default => $reportType, }; } /** * @return list> */ private function dailyProfitExportRows(string $dateFrom, string $dateTo): array { $rows = [ ['日期', '下注', '派彩', '盈亏'], ]; foreach ($this->dailyProfitRows($dateFrom, $dateTo) as $row) { $rows[] = [ $row['business_date'], $row['total_bet_minor'], $row['total_payout_minor'], $row['approx_house_gross_minor'], ]; } return $rows; } /** * @return list> */ private function playerWinLossExportRows(?array $filterJson, string $dateFrom, string $dateTo): array { $playerId = isset($filterJson['player_id']) ? (int) $filterJson['player_id'] : null; $rows = [ ['玩家ID', '用户名', '下注', '派彩', '净输赢'], ]; $items = $this->playerWinLossBaseQuery($playerId, $dateFrom, $dateTo)->get(); foreach ($items as $row) { $rows[] = [ (int) $row->player_id, (string) $row->username, (int) $row->total_bet_minor, (int) $row->total_payout_minor, (int) $row->net_win_loss_minor, ]; } return $rows; } /** * @return list> */ private function playDimensionExportRows(?array $filterJson, string $dateFrom, string $dateTo): array { $playCode = isset($filterJson['play_code']) ? trim((string) $filterJson['play_code']) : null; $rows = [ ['玩法', '维度', '下注', '派彩', '盈亏'], ]; $items = $this->playDimensionBaseQuery($playCode !== '' ? $playCode : null, $dateFrom, $dateTo)->get(); foreach ($items as $row) { $rows[] = [ (string) $row->play_code, (int) $row->dimension, (int) $row->total_bet_minor, (int) $row->total_payout_minor, (int) $row->approx_house_gross_minor, ]; } return $rows; } /** * @return list> */ private function rebateCommissionExportRows(?array $filterJson, string $dateFrom, string $dateTo): array { $playCode = isset($filterJson['play_code']) ? trim((string) $filterJson['play_code']) : null; $rows = [ ['玩法', '回水', '订单数', '注单数'], ]; $items = $this->rebateCommissionBaseQuery($playCode !== '' ? $playCode : null, $dateFrom, $dateTo)->get(); foreach ($items as $row) { $rows[] = [ (string) $row->play_code, (int) $row->total_rebate_minor, (int) $row->order_count, (int) $row->ticket_item_count, ]; } return $rows; } /** * @return list> */ private function auditExportRows(?array $filterJson, string $dateFrom, string $dateTo): array { $operatorId = isset($filterJson['operator_id']) ? (int) $filterJson['operator_id'] : null; $rows = [ ['ID', '操作者类型', '操作者ID', '模块', '操作', 'IP', '时间'], ]; $q = AuditLog::query()->orderByDesc('id'); if ($operatorId !== null && $operatorId > 0) { $q->where('operator_id', $operatorId); } $q->whereDate('created_at', '>=', $dateFrom) ->whereDate('created_at', '<=', $dateTo); foreach ($q->limit(5000)->get() as $log) { $rows[] = [ (int) $log->id, (string) $log->operator_type, (int) $log->operator_id, (string) $log->module_code, (string) $log->action_code, (string) ($log->ip ?? ''), $log->created_at?->toIso8601String() ?? '', ]; } return $rows; } /** @return \Illuminate\Database\Query\Builder */ private function playerWinLossBaseQuery(?int $playerId, string $dateFrom, string $dateTo) { $query = DB::table('ticket_items as ti') ->join('ticket_orders as o', 'o.id', '=', 'ti.order_id') ->leftJoin('players as p', 'p.id', '=', 'ti.player_id') ->selectRaw('ti.player_id') ->selectRaw('p.username as username') ->selectRaw('SUM(ti.actual_deduct_amount) as total_bet_minor') ->selectRaw('SUM(ti.win_amount + ti.jackpot_win_amount) as total_payout_minor') ->selectRaw('SUM(ti.actual_deduct_amount) - SUM(ti.win_amount + ti.jackpot_win_amount) as net_win_loss_minor') ->whereDate('o.created_at', '>=', $dateFrom) ->whereDate('o.created_at', '<=', $dateTo) ->groupBy('ti.player_id', 'p.username') ->orderByDesc('net_win_loss_minor'); if ($playerId !== null && $playerId > 0) { $query->where('ti.player_id', $playerId); } return $query; } /** @return \Illuminate\Database\Query\Builder */ private function playDimensionBaseQuery(?string $playCode, string $dateFrom, string $dateTo) { $query = DB::table('ticket_items as ti') ->join('ticket_orders as o', 'o.id', '=', 'ti.order_id') ->selectRaw('ti.play_code') ->selectRaw('ti.dimension') ->selectRaw('SUM(ti.actual_deduct_amount) as total_bet_minor') ->selectRaw('SUM(ti.win_amount + ti.jackpot_win_amount) as total_payout_minor') ->selectRaw('SUM(ti.actual_deduct_amount) - SUM(ti.win_amount + ti.jackpot_win_amount) as approx_house_gross_minor') ->whereDate('o.created_at', '>=', $dateFrom) ->whereDate('o.created_at', '<=', $dateTo) ->groupBy('ti.play_code', 'ti.dimension') ->orderBy('ti.play_code') ->orderBy('ti.dimension'); if ($playCode !== null && $playCode !== '') { $query->where('ti.play_code', $playCode); } return $query; } /** @return \Illuminate\Database\Query\Builder */ private function rebateCommissionBaseQuery(?string $playCode, string $dateFrom, string $dateTo) { $query = DB::table('ticket_items as ti') ->join('ticket_orders as o', 'o.id', '=', 'ti.order_id') ->selectRaw('ti.play_code') ->selectRaw('SUM(ti.total_bet_amount - ti.actual_deduct_amount) as total_rebate_minor') ->selectRaw('COUNT(DISTINCT o.id) as order_count') ->selectRaw('COUNT(ti.id) as ticket_item_count') ->whereDate('o.created_at', '>=', $dateFrom) ->whereDate('o.created_at', '<=', $dateTo) ->groupBy('ti.play_code') ->orderBy('ti.play_code'); if ($playCode !== null && $playCode !== '') { $query->where('ti.play_code', $playCode); } return $query; } /** * @return list> */ private function drawProfitExportRows(?array $filterJson): array { $draw = $this->resolveDrawForReport($filterJson); if ($draw === null) { return [['提示', '请提供 draw_id 或 draw_no']]; } $drawId = (int) $draw->id; $totalBetMinor = (int) TicketOrder::query()->where('draw_id', $drawId)->sum('total_actual_deduct'); $orderCount = (int) TicketOrder::query()->where('draw_id', $drawId)->count(); $itemCount = (int) TicketItem::query()->where('draw_id', $drawId)->count(); $currencyCode = (string) (TicketOrder::query()->where('draw_id', $drawId)->value('currency_code') ?? ''); $totalWinMinor = (int) TicketItem::query()->where('draw_id', $drawId)->sum('win_amount'); $totalJackpotWinMinor = (int) TicketItem::query()->where('draw_id', $drawId)->sum('jackpot_win_amount'); $totalPayoutMinor = $totalWinMinor + $totalJackpotWinMinor; $approxHouseGrossMinor = $totalBetMinor - $totalPayoutMinor; $rows = [ [ '行类型', '期号', '状态', '币种', '订单数', '注单数', '下注', '派彩', '平台盈亏', '结算批次ID', '结算状态', '批次数', '中奖数', '批次派彩', '批次Jackpot', '完成时间', ], [ 'summary', $draw->draw_no, $draw->status, $currencyCode !== '' ? $currencyCode : null, $orderCount, $itemCount, $totalBetMinor, $totalPayoutMinor, $approxHouseGrossMinor, null, null, null, null, null, null, null, ], ]; $batches = SettlementBatch::query() ->where('draw_id', $drawId) ->orderByDesc('id') ->limit(100) ->get(); foreach ($batches as $batch) { $rows[] = [ 'settlement_batch', $draw->draw_no, $draw->status, $currencyCode !== '' ? $currencyCode : null, null, null, null, null, null, (int) $batch->id, $batch->status, (int) $batch->total_ticket_count, (int) $batch->total_win_count, (int) $batch->total_payout_amount, (int) $batch->total_jackpot_payout_amount, $batch->finished_at?->toIso8601String(), ]; } return $rows; } /** * @return list> */ private function soldOutNumberExportRows(?array $filterJson): array { $draw = $this->resolveDrawForReport($filterJson); if ($draw === null) { return [['提示', '请提供 draw_id 或 draw_no']]; } $rows = [ ['期号', '号码', '封顶', '已占用', '剩余', '售罄', '使用率%'], ]; $pools = RiskPool::query() ->where('draw_id', $draw->id) ->where('sold_out_status', 1) ->orderBy('normalized_number') ->limit(10000) ->get(); foreach ($pools as $pool) { $cap = (int) $pool->total_cap_amount; $locked = (int) $pool->locked_amount; $rows[] = [ $draw->draw_no, $pool->normalized_number, $cap, $locked, (int) $pool->remaining_amount, '是', $this->riskUsageRatioPercent($cap, $locked), ]; } return $rows; } /** * @return list> */ private function hotNumberRiskExportRows(?array $filterJson): array { $draw = $this->resolveDrawForReport($filterJson); if ($draw === null) { return [['提示', '请提供 draw_id 或 draw_no']]; } $number = $this->normalizedNumberFromFilters($filterJson); $rows = [ [ '行类型', '期号', '号码', '封顶', '已占用', '剩余', '售罄', '使用率%', '日志ID', '动作', '金额', '玩法', '注单号', '玩家ID', '原因', '时间', ], ]; if ($number !== null) { $pool = RiskPool::query() ->where('draw_id', $draw->id) ->where('normalized_number', $number) ->first(); if ($pool === null) { $rows[] = ['pool', $draw->draw_no, $number, null, null, null, null, null, null, null, null, null, null, null, null, null]; return $rows; } $cap = (int) $pool->total_cap_amount; $locked = (int) $pool->locked_amount; $rows[] = [ 'pool', $draw->draw_no, $pool->normalized_number, $cap, $locked, (int) $pool->remaining_amount, (int) $pool->sold_out_status === 1 ? '是' : '否', $this->riskUsageRatioPercent($cap, $locked), null, null, null, null, null, null, null, null, ]; $logs = RiskPoolLockLog::query() ->where('draw_id', $draw->id) ->where('normalized_number', $number) ->with(['ticketItem:id,ticket_no,play_code,player_id']) ->orderByDesc('id') ->limit(5000) ->get(); foreach ($logs as $log) { $rows[] = [ 'lock_log', $draw->draw_no, $number, null, null, null, null, null, (int) $log->id, $log->action_type, (int) $log->amount, $log->ticketItem?->play_code, $log->ticketItem?->ticket_no, $log->ticketItem?->player_id, $log->source_reason, $log->created_at?->toIso8601String(), ]; } return $rows; } $pools = RiskPool::query() ->where('draw_id', $draw->id) ->orderByDesc('locked_amount') ->limit(10000) ->get(); foreach ($pools as $pool) { $cap = (int) $pool->total_cap_amount; $locked = (int) $pool->locked_amount; $rows[] = [ 'pool', $draw->draw_no, $pool->normalized_number, $cap, $locked, (int) $pool->remaining_amount, (int) $pool->sold_out_status === 1 ? '是' : '否', $this->riskUsageRatioPercent($cap, $locked), null, null, null, null, null, null, null, null, ]; } return $rows; } /** * @return list> */ private function transferOrdersExportRows(?array $filterJson, string $dateFrom, string $dateTo): array { $rows = [ ['转账单号', '玩家ID', '用户名', '昵称', '方向', '币种', '金额', '状态', '外部单号', '失败原因', '创建时间', '完成时间'], ]; $query = TransferOrder::query() ->with(['player:id,username,nickname']) ->orderByDesc('id'); $playerId = isset($filterJson['player_id']) ? (int) $filterJson['player_id'] : null; if ($playerId !== null && $playerId > 0) { $query->where('player_id', $playerId); } $query->where('created_at', '>=', $dateFrom.' 00:00:00') ->where('created_at', '<=', $dateTo.' 23:59:59'); foreach ($query->limit(10000)->get() as $order) { $player = $order->player; $rows[] = [ $order->transfer_no, (int) $order->player_id, $player?->username, $player?->nickname, $order->direction, $order->currency_code, (int) $order->amount, $order->status, $order->external_ref_no, $order->fail_reason, $order->created_at?->toIso8601String(), $order->finished_at?->toIso8601String(), ]; } return $rows; } /** * @return list> */ private function walletTxnsExportRows(?array $filterJson, string $dateFrom, string $dateTo): array { $rows = [ ['流水号', '玩家ID', '用户名', '业务类型', '业务单号', '方向', '金额', '变动前余额', '变动后余额', '状态', '外部单号', '备注', '创建时间'], ]; $query = WalletTxn::query() ->with(['player:id,username']) ->orderByDesc('id'); $playerId = isset($filterJson['player_id']) ? (int) $filterJson['player_id'] : null; if ($playerId !== null && $playerId > 0) { $query->where('player_id', $playerId); } $query->where('created_at', '>=', $dateFrom.' 00:00:00') ->where('created_at', '<=', $dateTo.' 23:59:59'); foreach ($query->limit(10000)->get() as $txn) { $rows[] = [ $txn->txn_no, (int) $txn->player_id, $txn->player?->username, $txn->biz_type, $txn->biz_no, (int) $txn->direction, (int) $txn->amount, (int) $txn->balance_before, (int) $txn->balance_after, $txn->status, $txn->external_ref_no, $txn->remark, $txn->created_at?->toIso8601String(), ]; } return $rows; } private function resolveDrawForReport(?array $filterJson): ?Draw { if (! is_array($filterJson)) { return null; } if (! empty($filterJson['draw_id'])) { return Draw::query()->find((int) $filterJson['draw_id']); } $drawNo = trim((string) ($filterJson['draw_no'] ?? '')); if ($drawNo !== '') { return Draw::query()->where('draw_no', $drawNo)->first(); } return null; } private function normalizedNumberFromFilters(?array $filterJson): ?string { if (! is_array($filterJson)) { return null; } $number = trim((string) ($filterJson['normalized_number'] ?? '')); if (preg_match('/^[0-9]{4}$/', $number) === 1) { return $number; } return null; } private function riskUsageRatioPercent(int $cap, int $locked): ?string { if ($cap <= 0) { return null; } return (string) round($locked / $cap * 100, 2); } private function formatBusinessDateValue(mixed $value): ?string { if ($value === null) { return null; } if ($value instanceof Carbon) { return $value->format('Y-m-d'); } $raw = trim((string) $value); if ($raw === '') { return null; } if (preg_match('/^\d{4}-\d{2}-\d{2}/', $raw, $m) === 1) { return substr($m[0], 0, 10); } return $raw; } }