CaiWuXiTong/app/Console/Commands/MaterializeStoreReceiptDifferences.php

212 lines
7.8 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Console\Commands;
use App\Business\StoreReceiptDifferenceMaterialization as Materialization;
use App\Http\Controllers\Business\Pages;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class MaterializeStoreReceiptDifferences extends Command
{
protected $signature = 'store-receipt:materialize-differences
{month? : 账期 YYYY-MM;留空则物化镜像里已有数据的全部月份}
{--chunk=5000 : 每批更新的行数}
{--dry-run : 只统计与比对,不写库}';
protected $description = '把业绩明细的差额备注分类与有效差额物化到镜像表,页面改为读列';
public function handle(): int
{
if (! Materialization::available(true)) {
$this->components->error('物化列或状态表不存在,请先执行数据库迁移。');
return self::FAILURE;
}
$months = $this->targetMonths();
if ($months === []) {
$this->components->warn('镜像里没有可物化的账期。');
return self::SUCCESS;
}
$chunk = max(200, (int) $this->option('chunk'));
$dryRun = (bool) $this->option('dry-run');
foreach ($months as $month) {
$this->line('');
$this->components->info(($dryRun ? '[dry-run] ' : '') . '账期 ' . substr($month, 0, 7));
if ($dryRun) {
$this->reportDryRun($month);
continue;
}
$started = microtime(true);
$updated = $this->materializeMonth($month, $chunk);
if ($updated === null) {
continue;
}
Materialization::markMaterialized($month, $updated);
$this->line(sprintf(' %s 行,用时 %.1f 秒', number_format($updated), microtime(true) - $started));
}
if ($dryRun) {
$this->line('');
$this->components->info('dry-run 完成,未写入任何数据。');
}
return self::SUCCESS;
}
/**
* @return list<string>
*/
private function targetMonths(): array
{
$month = trim((string) $this->argument('month'));
if ($month !== '') {
if (preg_match('/^\d{4}-\d{2}$/', $month) === 1) {
$month .= '-01';
}
// 跨月备注规则依赖相邻月份,指定月份时一起重算前后两个月。
return array_values(array_unique(Materialization::adjacentMonths($month)));
}
return DB::connection(Materialization::connection())
->table(Materialization::table())
->select('report_month')
->distinct()
->orderBy('report_month')
->pluck('report_month')
->map(static fn ($value): string => substr((string) $value, 0, 10))
->all();
}
/**
* 分批把规则结果写进物化列;返回更新行数。
*/
private function materializeMonth(string $month, int $chunk): ?int
{
$connection = Materialization::connection();
$table = Materialization::table();
$exists = DB::connection($connection)->table($table)->where('report_month', $month)->exists();
if (! $exists) {
$this->components->warn(' 镜像里没有该账期的数据,跳过。');
return null;
}
$baseExpression = Pages::receiptDefaultDifferenceSql();
$indexExpression = Pages::receiptDifferenceRemarkIndexSql();
$updated = 0;
$lastKey = '';
$batch = 0;
while (true) {
$keys = DB::connection($connection)->table($table)
->where('report_month', $month)
->where('row_key', '>', $lastKey)
->orderBy('row_key')
->limit($chunk)
->pluck('row_key')
->all();
if ($keys === []) {
break;
}
$lastKey = (string) end($keys);
$batch++;
// 第一遍:规则表达式只算一次,结果落列。
DB::connection($connection)->table($table)
->where('report_month', $month)
->whereIn('row_key', $keys)
->update([
Materialization::BASE_DIFFERENCE_COLUMN => DB::raw($baseExpression),
Materialization::REASON_INDEX_COLUMN => DB::raw($indexExpression),
]);
// 第二遍:直接用刚写入的列推导「有/无差额」,避免重复执行规则链,
// 也避免依赖 SET 子句从左到右求值的库间差异。
DB::connection($connection)->table($table)
->where('report_month', $month)
->whereIn('row_key', $keys)
->update([
Materialization::HAS_DIFFERENCE_COLUMN => DB::raw($this->hasDifferenceExpression()),
]);
$updated += count($keys);
$this->output->write('.');
}
if ($updated === 0) {
$this->line('');
} else {
$this->line(' (' . $batch . ' 批)');
}
return $updated;
}
/**
* 基于物化列的「有/无差额」推导:口径兜底行按 0 计。
*/
private function hasDifferenceExpression(): string
{
$basisMarks = implode(', ', array_map(
static fn (string $remark): string => "'" . str_replace("'", "''", $remark) . "'",
Pages::RECEIPT_BASIS_FALLBACK_REMARKS,
));
return 'CASE WHEN ABS(CASE WHEN remark IN (' . $basisMarks . ')'
. ' AND COALESCE(' . Materialization::REASON_INDEX_COLUMN . ', 0) = 0 THEN 0'
. ' ELSE COALESCE(' . Materialization::BASE_DIFFERENCE_COLUMN . ', 0) END) > 0.005 THEN 1 ELSE 0 END';
}
/**
* dry-run:对比「库里的物化值」与「按当前规则重算」的差异,不写库。
*/
private function reportDryRun(string $month): void
{
$connection = Materialization::connection();
$table = Materialization::table();
$total = (int) DB::connection($connection)->table($table)->where('report_month', $month)->count();
if ($total === 0) {
$this->components->warn(' 镜像里没有该账期的数据,跳过。');
return;
}
$baseExpression = Pages::receiptDefaultDifferenceSql();
$indexExpression = Pages::receiptDifferenceRemarkIndexSql();
$row = DB::connection($connection)->table($table)
->where('report_month', $month)
->selectRaw('COUNT(*) AS total')
->selectRaw('SUM(CASE WHEN ' . Materialization::REASON_INDEX_COLUMN
. ' IS NULL OR ' . Materialization::REASON_INDEX_COLUMN . ' <> (' . $indexExpression . ') THEN 1 ELSE 0 END) AS reason_mismatch')
->selectRaw('SUM(CASE WHEN ' . Materialization::BASE_DIFFERENCE_COLUMN
. ' IS NULL OR ABS(' . Materialization::BASE_DIFFERENCE_COLUMN . ' - (' . $baseExpression . ')) > 0.005 THEN 1 ELSE 0 END) AS base_mismatch')
->selectRaw('SUM(CASE WHEN ' . Materialization::HAS_DIFFERENCE_COLUMN . ' IS NULL THEN 1 ELSE 0 END) AS unreasoned')
->selectRaw('SUM(CASE WHEN ' . Materialization::HAS_DIFFERENCE_COLUMN . ' IS NOT NULL AND '
. Materialization::HAS_DIFFERENCE_COLUMN . ' <> (' . $this->hasDifferenceExpression() . ') THEN 1 ELSE 0 END) AS state_mismatch')
->first();
$this->line(sprintf(' 行数 %s|备注序号不一致 %s|差额不一致 %s|有/无不一致 %s|未物化 %s',
number_format((int) ($row->total ?? 0)),
number_format((int) ($row->reason_mismatch ?? 0)),
number_format((int) ($row->base_mismatch ?? 0)),
number_format((int) ($row->state_mismatch ?? 0)),
number_format((int) ($row->unreasoned ?? 0)),
));
}
}