parseDate((string)($this->option('end-date') ?: date('Y-m-d'))); $start = $this->parseDate((string)($this->option('start-date') ?: $end->copy()->subDays(30)->toDateString())); if ($start->gt($end) || $start->diffInDays($end) > 366) { throw new \InvalidArgumentException('日期范围须先后有效,单次不超过366天'); } $chunk = filter_var($this->option('chunk'), FILTER_VALIDATE_INT); if ($chunk === false || $chunk < 1 || $chunk > 2000) { throw new \InvalidArgumentException('chunk 必须是1至2000的整数'); } $write = (bool)$this->option('write'); $counts = ['scanned' => 0, 'created' => 0, 'pending' => 0, 'existing' => 0, 'skipped' => 0, 'incomplete_metadata' => 0]; $reasons = []; $this->info(($write ? '写入登记' : '只读预览') . ":{$start->toDateString()} 至 {$end->toDateString()}"); BalanceLog::where('change_type', '充值返现')->where('type', 1)->where('amount', '>', 0) ->whereBetween('created_at', [$start->toDateString() . ' 00:00:00', $end->toDateString() . ' 23:59:59']) ->chunkById($chunk, function ($rows) use ($recorder, $write, &$counts, &$reasons) { foreach ($rows as $row) { $result = $write ? $recorder->record($row) : $recorder->preview($row); $counts['scanned']++; $counts[$result['status']]++; if (!empty($result['incomplete_metadata'])) $counts['incomplete_metadata']++; if ($result['status'] === 'skipped') { $reason = $result['reason']; $reasons[$reason] = ($reasons[$reason] ?? 0) + 1; } } }); $this->table(['项目', '条数'], array_map(fn ($key, $count) => [$key, $count], array_keys($counts), array_values($counts))); foreach ($reasons as $reason => $count) $this->line("跳过 {$reason}: {$count}"); if (!$write) $this->info('预览未写入数据;确认后使用相同日期并加 --write 登记,钱包不会再次入账。'); return self::SUCCESS; } catch (\Throwable $e) { $this->error($e->getMessage()); return self::FAILURE; } } private function parseDate(string $value): Carbon { $date = Carbon::createFromFormat('!Y-m-d', $value, config('app.timezone', 'Asia/Shanghai')); if (!$date || $date->format('Y-m-d') !== $value) { throw new \InvalidArgumentException('日期必须为有效的 Y-m-d'); } return $date; } }