247 lines
9.7 KiB
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;
|
|
}
|
|
}
|