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 */ 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 $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; } }