CaiWuXiTong/app/Console/Commands/AutoComputeProductMonthlyWeightedAverage.php

247 lines
9.7 KiB
PHP

<?php
namespace App\Console\Commands;
use App\Business\ProductMonthlyWeightedAverageReporter;
use Carbon\CarbonImmutable;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
/**
* 检查上游完整性后,自动生成尚未计算的最新月份商品月加权平均单价。
*/
class AutoComputeProductMonthlyWeightedAverage extends Command
{
protected $signature = 'product:monthly-weighted-average:auto
{--month= : 指定月份;默认取最新已成功的进销存月份}
{--result-json= : 将执行结果写入指定 JSON 文件}';
protected $description = '上游完整后自动计算商品月加权平均单价,并保护已计算历史月份';
public function handle(ProductMonthlyWeightedAverageReporter $reporter): int
{
$connection = (string) config('upstream_mirrors.connection', 'business_mirror');
$month = $this->targetMonth($connection);
if ($month === null) {
return $this->finish('blocked', null, [
'message' => '未找到已成功导入的进销存月份。',
]);
}
$lock = Cache::lock('product-monthly-weighted-average-auto:' . $month->format('Y-m'), 7200);
if (! $lock->get()) {
return $this->finish('blocked', $month, [
'message' => '同月自动计算任务正在执行。',
]);
}
try {
$state = DB::connection($connection)->table('product_monthly_weighted_average_states')
->where('report_month', $month->toDateString())
->first();
if (($state?->status ?? null) === 'computed') {
$report = $reporter->build($connection, $month);
$report['calculation_status'] = $report['status'];
$report['status'] = 'already_computed';
return $this->finish('already_computed', $month, $report, $reporter);
}
$checks = $this->checks($connection, $month);
if (in_array(false, array_column($checks, 'passed'), true)) {
DB::connection($connection)->table('product_monthly_weighted_average_states')->updateOrInsert(
['report_month' => $month->toDateString()],
[
'status' => 'blocked',
'last_error' => '上游数据未通过完整性检查',
'updated_at' => now(),
'created_at' => now(),
],
);
return $this->finish('blocked', $month, [
'message' => '上游数据未更新或不完整,未执行计算。',
'checks' => $checks,
]);
}
$baseExit = $this->call('product:monthly-weighted-average:base', [
'month' => $month->format('Y-m'),
'--from-previous' => true,
// 自动任务只会在尚未 computed 的月份走到这里,可安全清理失败重试留下
// 的同月基表;已 computed 的历史月份在前面的门禁直接跳过。
'--force' => true,
]);
if ($baseExit !== self::SUCCESS) {
return $this->finish('failed', $month, [
'message' => '生成期初基数失败。',
]);
}
$computeExit = $this->call('product:monthly-weighted-average:compute', [
'month' => $month->format('Y-m'),
]);
if ($computeExit !== self::SUCCESS) {
return $this->finish('failed', $month, [
'message' => '加权平均单价计算失败。',
]);
}
$report = $reporter->build($connection, $month);
$report['checks'] = $checks;
return $this->finish(
($report['status'] ?? null) === 'review_required' ? 'review_required' : 'success',
$month,
$report,
$reporter,
);
} finally {
$lock->release();
}
}
private function targetMonth(string $connection): ?CarbonImmutable
{
$requested = trim((string) $this->option('month'));
if ($requested !== '') {
return CarbonImmutable::parse($requested)->startOfMonth();
}
$latest = DB::connection($connection)->table('inventory_report_authoritative_months')
->where('dataset_key', 'jiansun.inventory-report.authoritative-month')
->where('status', 'succeeded')
->whereNotNull('active_generation')
->orderByDesc('source_month')
->value('source_month');
return $latest === null ? null : CarbonImmutable::parse((string) $latest)->startOfMonth();
}
/** @return list<array{name:string,passed:bool,actual:mixed,expected:mixed}> */
private function checks(string $connection, CarbonImmutable $month): array
{
$db = DB::connection($connection);
$monthState = $db->table('inventory_report_authoritative_months')
->where('dataset_key', 'jiansun.inventory-report.authoritative-month')
->where('source_month', $month->toDateString())
->first();
$generation = $monthState?->active_generation;
$activeRows = $generation === null
? 0
: $db->table('inventory_report_authoritative_rows')
->where('generation', (string) $generation)
->count();
$expectedRows = (int) ($monthState?->source_row_count ?? 0);
$previous = $month->subMonthNoOverflow();
$previousState = $db->table('product_monthly_weighted_average_states')
->where('report_month', $previous->toDateString())
->value('status');
$previousRows = $db->table('product_monthly_weighted_average_rows')
->where('report_month', $previous->toDateString())
->count();
$inboundRows = $db->table('inventory_inbound_detail_rows')
->where('report_month', $month->toDateString())
->count();
$inboundMissingDimensions = $db->table('inventory_inbound_detail_rows')
->where('report_month', $month->toDateString())
->where(function ($query): void {
$query->whereNull('barcode')->orWhere('barcode', '')
->orWhereNull('warehouse_short_name')->orWhere('warehouse_short_name', '');
})
->count();
$inboundMissingAmounts = $db->table('inventory_inbound_detail_rows')
->where('report_month', $month->toDateString())
->whereNull('amount')
->whereNull('estimated_amount')
->count();
$productCatalogRows = $db->table('ops_jst_sku')->whereNotNull('sku_id')->count();
return [
[
'name' => '进销存月份状态成功',
'passed' => ($monthState?->status ?? null) === 'succeeded',
'actual' => $monthState?->status,
'expected' => 'succeeded',
],
[
'name' => '进销存激活明细行数完整',
'passed' => $expectedRows > 0 && $activeRows === $expectedRows,
'actual' => $activeRows,
'expected' => $expectedRows,
],
[
'name' => '上月加权平均结果已完成',
'passed' => $previousState === 'computed' && $previousRows > 0,
'actual' => ['status' => $previousState, 'rows' => $previousRows],
'expected' => ['status' => 'computed', 'rows' => '> 0'],
],
[
'name' => '入库明细已更新',
'passed' => $inboundRows > 0,
'actual' => $inboundRows,
'expected' => '> 0',
],
[
'name' => '入库商品编码与仓库简称无缺失',
'passed' => $inboundMissingDimensions === 0,
'actual' => $inboundMissingDimensions,
'expected' => 0,
],
[
'name' => '入库金额至少取金额或预估金额',
'passed' => $inboundMissingAmounts === 0,
'actual' => $inboundMissingAmounts,
'expected' => 0,
],
[
'name' => '普通商品资料已更新',
'passed' => $productCatalogRows > 0,
'actual' => $productCatalogRows,
'expected' => '> 0',
],
];
}
/**
* @param array<string,mixed> $payload
*/
private function finish(
string $status,
?CarbonImmutable $month,
array $payload,
?ProductMonthlyWeightedAverageReporter $reporter = null,
): int {
$result = ['status' => $status] + $payload;
if (! isset($result['report_month']) && $month !== null) {
$result['report_month'] = $month->toDateString();
}
$path = trim((string) $this->option('result-json'));
if ($path !== '') {
File::ensureDirectoryExists(dirname($path));
File::put(
$path,
json_encode(
$result,
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT,
) . PHP_EOL,
);
}
if ($reporter !== null && isset($result['totals'])) {
$this->line($reporter->brief($result));
} elseif (isset($result['message'])) {
$this->error((string) $result['message']);
}
return in_array($status, ['blocked', 'failed'], true)
? self::FAILURE
: self::SUCCESS;
}
}