parseTimeOption('from'); $to = $this->parseTimeOption('to'); } catch (\InvalidArgumentException $e) { $this->error($e->getMessage()); return self::FAILURE; } if ($from !== null && $to !== null && $from->greaterThan($to)) { $this->error('--from 不能晚于 --to'); return self::FAILURE; } $transferInType = '三方游戏转入'; $transferOutType = '三方游戏转出'; $query = BalanceLog::query() ->whereNotNull('member_id') ->where('member_id', '<>', '') ->whereIn('change_type', [$transferInType, $transferOutType]) ->select('member_id') ->selectRaw( 'SUM(CASE WHEN change_type = ? AND amount < 0 THEN 1 ELSE 0 END) AS transfer_in_count', [$transferInType] ) ->selectRaw( 'SUM(CASE WHEN change_type = ? AND amount < 0 THEN -amount ELSE 0 END) AS transfer_in_total', [$transferInType] ) ->selectRaw( 'SUM(CASE WHEN change_type = ? AND amount > 0 THEN 1 ELSE 0 END) AS refund_count', [$transferInType] ) ->selectRaw( 'SUM(CASE WHEN change_type = ? AND amount > 0 THEN amount ELSE 0 END) AS refund_total', [$transferInType] ) ->selectRaw( 'SUM(CASE WHEN change_type = ? AND amount > 0 THEN 1 ELSE 0 END) AS transfer_out_count', [$transferOutType] ) ->selectRaw( 'SUM(CASE WHEN change_type = ? AND amount > 0 THEN amount ELSE 0 END) AS transfer_out_total', [$transferOutType] ) ->selectRaw('MIN(created_at) AS first_at, MAX(created_at) AS last_at'); if ($from !== null) { $query->where('created_at', '>=', $from->format('Y-m-d H:i:s')); } if ($to !== null) { $query->where('created_at', '<=', $to->format('Y-m-d H:i:s')); } $memberIds = array_values(array_filter(array_map('strval', (array) $this->option('member_id')))); if ($memberIds !== []) { $query->whereIn('member_id', $memberIds); } $rows = $query->groupBy('member_id')->orderBy('member_id')->get(); $verifyProvider = !$this->option('skip-provider'); $progress = null; if ($verifyProvider && $rows->isNotEmpty()) { $this->info('正在逐个查询三方当前余额……'); $progress = $this->output->createProgressBar($rows->count()); $progress->start(); } $report = []; foreach ($rows as $row) { $transferInCount = (int) $row->transfer_in_count; $refundCount = (int) $row->refund_count; $transferOutCount = (int) $row->transfer_out_count; $flowTypes = []; if ($transferInCount > 0) { $flowTypes[] = '转入'; } if ($refundCount > 0) { $flowTypes[] = '失败退款'; } if ($transferOutCount > 0) { $flowTypes[] = '转出'; } $item = [ 'member_id' => (string) $row->member_id, 'player_id' => $service->playerId((string) $row->member_id), 'flow_type' => implode('+', $flowTypes), 'transfer_in_count' => $transferInCount, 'transfer_in_total' => $this->decimal($row->transfer_in_total), 'refund_count' => $refundCount, 'refund_total' => $this->decimal($row->refund_total), 'transfer_out_count' => $transferOutCount, 'transfer_out_total' => $this->decimal($row->transfer_out_total), 'first_at' => (string) $row->first_at, 'last_at' => (string) $row->last_at, ]; if ($verifyProvider) { $detail = $service->detail((string) $row->member_id, true); $item['provider_status'] = ($detail['ok'] ?? false) ? '成功' : '失败'; $item['provider_total_balance'] = ($detail['ok'] ?? false) ? $this->decimal($detail['total_game_balance'] ?? 0) : ''; $item['provider_balances'] = ($detail['ok'] ?? false) ? $this->providerBalances($detail['game_list'] ?? []) : ''; $item['provider_message'] = ($detail['ok'] ?? false) ? '' : (string) ($detail['msg'] ?? '查询失败'); } $report[] = $item; if ($progress !== null) { $progress->advance(); } } if ($progress !== null) { $progress->finish(); $this->newLine(2); } $csvPath = (string) $this->option('csv'); if ($csvPath === '') { $csvPath = storage_path('app/third_game_balance_users_' . now()->format('Ymd_His') . '.csv'); } if (!$this->writeCsv($csvPath, $report)) { return self::FAILURE; } $show = max(0, (int) $this->option('show')); if ($show > 0 && $report !== []) { $this->table(array_keys($report[0]), array_slice($report, 0, $show)); } $this->info('共找到 ' . count($report) . ' 个玩家'); $this->info('CSV:' . $csvPath); $this->comment($verifyProvider ? '本命令只读:已请求三方当前余额,未回收余额,未修改钱包或流水。' : '本命令只读:未请求三方接口,未修改钱包或流水。'); return self::SUCCESS; } private function parseTimeOption(string $name): ?Carbon { $value = trim((string) $this->option($name)); if ($value === '') { return null; } try { return Carbon::createFromFormat('Y-m-d H:i:s', $value); } catch (\Throwable $e) { throw new \InvalidArgumentException('--' . $name . ' 格式必须为 YYYY-MM-DD HH:MM:SS'); } } private function decimal($value): string { return bcadd(is_numeric($value) ? (string) $value : '0', '0', 10); } private function writeCsv(string $path, array $rows): bool { try { File::ensureDirectoryExists(dirname($path)); $handle = fopen($path, 'wb'); if ($handle === false) { throw new \RuntimeException('无法创建 CSV'); } fwrite($handle, "\xEF\xBB\xBF"); $headers = $rows !== [] ? array_keys($rows[0]) : [ 'member_id', 'player_id', 'flow_type', 'transfer_in_count', 'transfer_in_total', 'refund_count', 'refund_total', 'transfer_out_count', 'transfer_out_total', 'first_at', 'last_at', 'provider_status', 'provider_total_balance', 'provider_balances', 'provider_message', ]; fputcsv($handle, $headers); foreach ($rows as $row) { fputcsv($handle, $row); } fclose($handle); return true; } catch (\Throwable $e) { if (isset($handle) && is_resource($handle)) { fclose($handle); } $this->error('写入 CSV 失败:' . $e->getMessage()); return false; } } private function providerBalances(array $balances): string { $result = []; foreach ($balances as $balance) { if (!is_array($balance) || !isset($balance['platform']) || !is_numeric($balance['balance'] ?? null)) { continue; } $result[(string) $balance['platform']] = $this->decimal($balance['balance']); } return (string) json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); } }