173 lines
7.4 KiB
PHP
173 lines
7.4 KiB
PHP
<?php
|
||
|
||
namespace App\Console\Commands;
|
||
|
||
use Illuminate\Console\Command;
|
||
use Illuminate\Support\Carbon;
|
||
use Illuminate\Support\Facades\DB;
|
||
use Illuminate\Support\Facades\Schema;
|
||
|
||
class VerifyErpDailyBillCoverage extends Command
|
||
{
|
||
protected $signature = 'business-mirror:verify-erp-daily-coverage
|
||
{--from=2026-07-01 : 起始日期 YYYY-MM-DD}
|
||
{--to= : 截止日期 YYYY-MM-DD,默认昨日}
|
||
{--json : 输出 JSON}';
|
||
|
||
protected $description = '核对京东日账单、京东钱包、头条放心购的月份与截止日覆盖是否完整';
|
||
|
||
private const TARGETS = [
|
||
'jd-daily' => [
|
||
'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;
|
||
}
|
||
}
|