212 lines
7.8 KiB
PHP
212 lines
7.8 KiB
PHP
<?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)),
|
||
));
|
||
}
|
||
}
|