ThirdGameBalanceUsers.php 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. <?php
  2. namespace App\Console\Commands;
  3. use App\Models\BalanceLog;
  4. use App\Services\ThirdGameBalanceService;
  5. use Carbon\Carbon;
  6. use Illuminate\Console\Command;
  7. use Illuminate\Support\Facades\File;
  8. class ThirdGameBalanceUsers extends Command
  9. {
  10. protected $signature = 'third-game:balance-users
  11. {--from= : 开始时间,例如 2026-07-14 10:00:00}
  12. {--to= : 结束时间,例如 2026-07-14 23:59:59}
  13. {--member_id=* : 只查询指定 member_id,可重复传入}
  14. {--csv= : CSV 输出路径,默认保存到 storage/app}
  15. {--show=100 : 控制台最多展示条数}';
  16. protected $description = '导出有三方游戏转入、失败退回或转出流水的玩家(只读,不回收余额)';
  17. public function handle(ThirdGameBalanceService $service): int
  18. {
  19. try {
  20. $from = $this->parseTimeOption('from');
  21. $to = $this->parseTimeOption('to');
  22. } catch (\InvalidArgumentException $e) {
  23. $this->error($e->getMessage());
  24. return self::FAILURE;
  25. }
  26. if ($from !== null && $to !== null && $from->greaterThan($to)) {
  27. $this->error('--from 不能晚于 --to');
  28. return self::FAILURE;
  29. }
  30. $transferInType = '三方游戏转入';
  31. $transferOutType = '三方游戏转出';
  32. $query = BalanceLog::query()
  33. ->whereNotNull('member_id')
  34. ->where('member_id', '<>', '')
  35. ->whereIn('change_type', [$transferInType, $transferOutType])
  36. ->select('member_id')
  37. ->selectRaw(
  38. 'SUM(CASE WHEN change_type = ? AND amount < 0 THEN 1 ELSE 0 END) AS transfer_in_count',
  39. [$transferInType]
  40. )
  41. ->selectRaw(
  42. 'SUM(CASE WHEN change_type = ? AND amount < 0 THEN -amount ELSE 0 END) AS transfer_in_total',
  43. [$transferInType]
  44. )
  45. ->selectRaw(
  46. 'SUM(CASE WHEN change_type = ? AND amount > 0 THEN 1 ELSE 0 END) AS refund_count',
  47. [$transferInType]
  48. )
  49. ->selectRaw(
  50. 'SUM(CASE WHEN change_type = ? AND amount > 0 THEN amount ELSE 0 END) AS refund_total',
  51. [$transferInType]
  52. )
  53. ->selectRaw(
  54. 'SUM(CASE WHEN change_type = ? AND amount > 0 THEN 1 ELSE 0 END) AS transfer_out_count',
  55. [$transferOutType]
  56. )
  57. ->selectRaw(
  58. 'SUM(CASE WHEN change_type = ? AND amount > 0 THEN amount ELSE 0 END) AS transfer_out_total',
  59. [$transferOutType]
  60. )
  61. ->selectRaw('MIN(created_at) AS first_at, MAX(created_at) AS last_at');
  62. if ($from !== null) {
  63. $query->where('created_at', '>=', $from->format('Y-m-d H:i:s'));
  64. }
  65. if ($to !== null) {
  66. $query->where('created_at', '<=', $to->format('Y-m-d H:i:s'));
  67. }
  68. $memberIds = array_values(array_filter(array_map('strval', (array) $this->option('member_id'))));
  69. if ($memberIds !== []) {
  70. $query->whereIn('member_id', $memberIds);
  71. }
  72. $rows = $query->groupBy('member_id')->orderBy('member_id')->get();
  73. $report = [];
  74. foreach ($rows as $row) {
  75. $transferInCount = (int) $row->transfer_in_count;
  76. $refundCount = (int) $row->refund_count;
  77. $transferOutCount = (int) $row->transfer_out_count;
  78. $flowTypes = [];
  79. if ($transferInCount > 0) {
  80. $flowTypes[] = '转入';
  81. }
  82. if ($refundCount > 0) {
  83. $flowTypes[] = '失败退款';
  84. }
  85. if ($transferOutCount > 0) {
  86. $flowTypes[] = '转出';
  87. }
  88. $report[] = [
  89. 'member_id' => (string) $row->member_id,
  90. 'player_id' => $service->playerId((string) $row->member_id),
  91. 'flow_type' => implode('+', $flowTypes),
  92. 'transfer_in_count' => $transferInCount,
  93. 'transfer_in_total' => $this->decimal($row->transfer_in_total),
  94. 'refund_count' => $refundCount,
  95. 'refund_total' => $this->decimal($row->refund_total),
  96. 'transfer_out_count' => $transferOutCount,
  97. 'transfer_out_total' => $this->decimal($row->transfer_out_total),
  98. 'first_at' => (string) $row->first_at,
  99. 'last_at' => (string) $row->last_at,
  100. ];
  101. }
  102. $csvPath = (string) $this->option('csv');
  103. if ($csvPath === '') {
  104. $csvPath = storage_path('app/third_game_balance_users_' . now()->format('Ymd_His') . '.csv');
  105. }
  106. if (!$this->writeCsv($csvPath, $report)) {
  107. return self::FAILURE;
  108. }
  109. $show = max(0, (int) $this->option('show'));
  110. if ($show > 0 && $report !== []) {
  111. $this->table(array_keys($report[0]), array_slice($report, 0, $show));
  112. }
  113. $this->info('共找到 ' . count($report) . ' 个玩家');
  114. $this->info('CSV:' . $csvPath);
  115. $this->comment('本命令只读:未请求三方接口,未修改钱包或流水。');
  116. return self::SUCCESS;
  117. }
  118. private function parseTimeOption(string $name): ?Carbon
  119. {
  120. $value = trim((string) $this->option($name));
  121. if ($value === '') {
  122. return null;
  123. }
  124. try {
  125. return Carbon::createFromFormat('Y-m-d H:i:s', $value);
  126. } catch (\Throwable $e) {
  127. throw new \InvalidArgumentException('--' . $name . ' 格式必须为 YYYY-MM-DD HH:MM:SS');
  128. }
  129. }
  130. private function decimal($value): string
  131. {
  132. return bcadd(is_numeric($value) ? (string) $value : '0', '0', 10);
  133. }
  134. private function writeCsv(string $path, array $rows): bool
  135. {
  136. try {
  137. File::ensureDirectoryExists(dirname($path));
  138. $handle = fopen($path, 'wb');
  139. if ($handle === false) {
  140. throw new \RuntimeException('无法创建 CSV');
  141. }
  142. fwrite($handle, "\xEF\xBB\xBF");
  143. $headers = $rows !== [] ? array_keys($rows[0]) : [
  144. 'member_id', 'player_id', 'flow_type', 'transfer_in_count', 'transfer_in_total',
  145. 'refund_count', 'refund_total', 'transfer_out_count', 'transfer_out_total',
  146. 'first_at', 'last_at',
  147. ];
  148. fputcsv($handle, $headers);
  149. foreach ($rows as $row) {
  150. fputcsv($handle, $row);
  151. }
  152. fclose($handle);
  153. return true;
  154. } catch (\Throwable $e) {
  155. if (isset($handle) && is_resource($handle)) {
  156. fclose($handle);
  157. }
  158. $this->error('写入 CSV 失败:' . $e->getMessage());
  159. return false;
  160. }
  161. }
  162. }