[ 'label' => '京东日账单', 'periods' => 'jd_daily_bill_periods', 'rows' => 'jd_daily_bill_rows', ], 'jd-wallet' => [ 'label' => '京东钱包', 'periods' => 'jd_wallet_bill_periods', 'rows' => 'jd_wallet_bill_rows', ], 'douyin' => [ 'label' => '头条放心购', 'periods' => 'douyin_authoritative_periods', 'rows' => 'douyin_authoritative_bills', ], ]; public function handle(): int { $fromText = (string) $this->option('from'); $toText = (string) ($this->option('to') ?: now('Asia/Shanghai')->subDay()->toDateString()); $from = $this->date($fromText); $to = $this->date($toText); if ($from === null || $to === null || $from->gt($to)) { $this->error('日期范围无效,应为 from <= to,格式 YYYY-MM-DD。'); return self::FAILURE; } $db = DB::connection(config('upstream_mirrors.connection', 'business_mirror')); $schema = Schema::connection(config('upstream_mirrors.connection', 'business_mirror')); $results = []; $failed = false; foreach (self::TARGETS as $key => $target) { if (! $schema->hasTable($target['periods']) || ! $schema->hasTable($target['rows'])) { $results[] = [ 'dataset' => $key, 'label' => $target['label'], 'month' => null, 'required_start' => $from->toDateString(), 'required_through' => $to->toDateString(), 'imported_start' => null, 'imported_end' => null, 'row_count' => 0, 'registered_rows' => null, 'income' => null, 'expense' => null, 'status' => 'missing_table', 'issue' => '镜像表不存在', ]; $failed = true; continue; } $month = $from->copy()->startOfMonth(); while ($month->lte($to)) { $monthEnd = $month->copy()->endOfMonth(); $requiredStart = $month->lt($from) ? $from->copy() : $month->copy(); $requiredThrough = $monthEnd->gt($to) ? $to->copy() : $monthEnd->copy(); $requiredEndExclusive = $requiredThrough->copy()->addDay(); $period = $db->table($target['periods']) ->where('period_start', $month->toDateString()) ->first(); $status = 'ok'; $rowCount = 0; $issue = null; $summary = null; if ($period === null) { $status = 'missing_month'; $issue = '缺少该月份导入记录'; } elseif (Carbon::parse((string) $period->period_end)->lt($requiredEndExclusive)) { $status = 'partial'; $issue = '导入截止日早于要求日期'; } else { $summary = $db->table($target['rows']) ->where('generation', $period->active_generation) ->selectRaw('COUNT(*) AS rows_count, COALESCE(SUM(income), 0) AS income, COALESCE(SUM(expense), 0) AS expense') ->first(); $rowCount = (int) ($summary->rows_count ?? 0); if ($rowCount === 0) { $status = 'empty'; $issue = '有效版本没有明细'; } elseif ($rowCount !== (int) $period->row_count) { $status = 'row_count_mismatch'; $issue = '有效版本实际行数与期间登记不一致'; } elseif (abs(round((float) $summary->income, 2) - (float) $period->income) > 0.005 || abs(round((float) $summary->expense, 2) - (float) $period->expense) > 0.005) { $status = 'amount_mismatch'; $issue = '有效版本收支合计与期间登记不一致'; } } if ($status !== 'ok') { $failed = true; } $results[] = [ 'dataset' => $key, 'label' => $target['label'], 'month' => $month->format('Y-m'), 'required_start' => $requiredStart->toDateString(), 'required_through' => $requiredThrough->toDateString(), 'imported_start' => $period?->period_start, 'imported_end' => $period?->period_end, 'row_count' => $rowCount, 'registered_rows' => $period === null ? null : (int) $period->row_count, 'income' => isset($summary) ? round((float) $summary->income, 2) : null, 'expense' => isset($summary) ? round((float) $summary->expense, 2) : null, 'status' => $status, 'issue' => $issue, ]; unset($summary); $month->addMonthNoOverflow()->startOfMonth(); } } if ($this->option('json')) { $this->line(json_encode($results, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); } else { $this->table( ['数据集', '月份', '要求覆盖', '导入覆盖', '实际行数', '登记行数', '收入', '支出', '状态', '说明'], array_map(static fn (array $row): array => [ $row['label'], $row['month'] ?? '—', ($row['required_start'] ?? '—') . ' ~ ' . ($row['required_through'] ?? '—'), ($row['imported_start'] ?? '—') . ' ~ ' . ($row['imported_end'] ?? '—'), number_format((int) $row['row_count']), $row['registered_rows'] === null ? '—' : number_format((int) $row['registered_rows']), $row['income'] === null ? '—' : number_format((float) $row['income'], 2), $row['expense'] === null ? '—' : number_format((float) $row['expense'], 2), $row['status'], $row['issue'] ?? '', ], $results), ); } return $failed ? self::FAILURE : self::SUCCESS; } private function date(string $value): ?Carbon { if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $value) !== 1) { return null; } $date = Carbon::createFromFormat('!Y-m-d', $value, 'Asia/Shanghai'); return $date !== false && $date->format('Y-m-d') === $value ? $date : null; } }