*/ public function build(string $connection, CarbonImmutable $month): array { $month = $month->startOfMonth(); $db = DB::connection($connection); $baseQuery = static fn () => $db->table(self::ROW_TABLE) ->where('report_month', $month->toDateString()); $totals = $baseQuery() ->selectRaw('count(*) as total_rows') ->selectRaw('coalesce(sum(opening_quantity), 0) as opening_quantity') ->selectRaw('coalesce(sum(opening_amount), 0) as opening_amount') ->selectRaw('coalesce(sum(purchase_quantity), 0) as purchase_quantity') ->selectRaw('coalesce(sum(purchase_amount), 0) as purchase_amount') ->selectRaw('sum(case when weighted_average_unit_price is not null then 1 else 0 end) as weighted_price_rows') ->selectRaw('sum(case when weighted_average_unit_price is null then 1 else 0 end) as missing_weighted_rows') ->selectRaw('sum(case when pricing_status = ? then 1 else 0 end) as manual_review_rows', ['待人工核查']) ->first(); if ($totals === null || (int) $totals->total_rows === 0) { return [ 'status' => 'missing', 'report_month' => $month->toDateString(), 'message' => '该月份尚未生成商品月加权平均单价。', 'generated_at' => now()->toDateTimeString(), ]; } $weightedKeys = []; foreach ($baseQuery()->select('product_code', 'warehouse_short_name')->cursor() as $row) { $weightedKeys[$this->key($row->product_code, $row->warehouse_short_name)] = true; } $inbound = [ 'source_rows' => 0, 'source_quantity' => 0.0, 'source_amount' => 0.0, 'matched_rows' => 0, 'matched_quantity' => 0.0, 'matched_amount' => 0.0, 'unmatched_rows' => 0, 'unmatched_quantity' => 0.0, 'unmatched_amount' => 0.0, 'unmatched_groups' => [], ]; foreach ($db->table(self::INBOUND_TABLE) ->where('report_month', $month->toDateString()) ->selectRaw('upper(barcode) as code, warehouse_short_name, count(*) as rows_count') ->selectRaw('coalesce(sum(quantity), 0) as quantity') ->selectRaw('coalesce(sum(coalesce(amount, 0) + coalesce(estimated_amount, 0)), 0) as amount') ->groupByRaw('upper(barcode), warehouse_short_name') ->cursor() as $row) { $rows = (int) $row->rows_count; $quantity = (float) $row->quantity; $amount = (float) $row->amount; $inbound['source_rows'] += $rows; $inbound['source_quantity'] += $quantity; $inbound['source_amount'] += $amount; if (isset($weightedKeys[$this->key($row->code, $row->warehouse_short_name)])) { $inbound['matched_rows'] += $rows; $inbound['matched_quantity'] += $quantity; $inbound['matched_amount'] += $amount; continue; } $inbound['unmatched_rows'] += $rows; $inbound['unmatched_quantity'] += $quantity; $inbound['unmatched_amount'] += $amount; $inbound['unmatched_groups'][] = [ 'product_code' => trim((string) $row->code), 'warehouse_short_name' => trim((string) $row->warehouse_short_name), 'source_rows' => $rows, 'quantity' => $this->number($quantity), 'amount' => $this->number($amount), 'reason' => '入库明细未匹配到商品编码 + 仓库简称行', ]; } usort( $inbound['unmatched_groups'], static fn (array $left, array $right): int => abs((float) $right['amount']) <=> abs((float) $left['amount']), ); $inbound = $this->roundNestedNumbers($inbound); $monthState = $db->table(self::INVENTORY_MONTH_TABLE) ->where('dataset_key', 'jiansun.inventory-report.authoritative-month') ->where('source_month', $month->toDateString()) ->first(); $generation = $monthState?->active_generation; $actualSourceRows = $generation === null ? 0 : $db->table(self::INVENTORY_ROW_TABLE) ->where('generation', (string) $generation) ->count(); $expectedSourceRows = (int) ($monthState?->source_row_count ?? 0); $weightedAnomalyRows = $baseQuery() ->where(function ($query): void { $query->whereNull('weighted_average_unit_price') ->orWhere('pricing_status', '待人工核查'); }) ->count(); $reasons = []; if ((int) $totals->missing_weighted_rows > 0) { $reasons[] = [ 'code' => 'missing_weighted_price', 'label' => '加权平均单价为空', 'rows' => (int) $totals->missing_weighted_rows, ]; } if ((int) $totals->manual_review_rows > 0) { $reasons[] = [ 'code' => 'manual_review', 'label' => '待人工核查', 'rows' => (int) $totals->manual_review_rows, ]; } if ((int) $inbound['unmatched_rows'] > 0) { $reasons[] = [ 'code' => 'inbound_without_inventory_dimension', 'label' => '入库明细未匹配到商品编码 + 仓库简称行', 'rows' => (int) $inbound['unmatched_rows'], 'groups' => count($inbound['unmatched_groups']), ]; } if ($actualSourceRows !== $expectedSourceRows) { $reasons[] = [ 'code' => 'upstream_source_row_mismatch', 'label' => '上游进销存源表行数与已激活明细不一致', 'rows' => abs($actualSourceRows - $expectedSourceRows), ]; } $anomalyRows = $weightedAnomalyRows + (int) $inbound['unmatched_rows']; $threshold = max( self::ANOMALY_THRESHOLD_MIN_ROWS, (int) ceil((int) $totals->total_rows * self::ANOMALY_THRESHOLD_RATE), ); return [ 'status' => $reasons === [] ? 'computed' : 'review_required', 'report_month' => $month->toDateString(), 'totals' => [ 'rows' => (int) $totals->total_rows, 'opening_quantity' => $this->number((float) $totals->opening_quantity), 'opening_amount' => $this->number((float) $totals->opening_amount), 'purchase_quantity' => $this->number((float) $totals->purchase_quantity), 'purchase_amount' => $this->number((float) $totals->purchase_amount), 'weighted_price_rows' => (int) $totals->weighted_price_rows, 'missing_weighted_rows' => (int) $totals->missing_weighted_rows, 'manual_review_rows' => (int) $totals->manual_review_rows, ], 'inbound' => $inbound, 'source' => [ 'expected_rows' => $expectedSourceRows, 'active_rows' => $actualSourceRows, 'row_difference' => $actualSourceRows - $expectedSourceRows, 'status' => (string) ($monthState?->status ?? 'missing'), 'active_generation' => $generation, 'source_filename' => $monthState?->source_filename, ], 'anomalies' => [ 'affected_rows' => $anomalyRows, 'threshold_rows' => $threshold, 'threshold_exceeded' => $anomalyRows > $threshold, 'reasons' => $reasons, ], 'state' => $db->table(self::STATE_TABLE) ->where('report_month', $month->toDateString()) ->first(), 'generated_at' => now()->toDateTimeString(), ]; } /** @param array $report */ public function brief(array $report): string { if (($report['status'] ?? null) === 'missing') { return (string) ($report['message'] ?? '商品月加权平均单价尚未生成'); } $totals = $report['totals'] ?? []; $inbound = $report['inbound'] ?? []; $anomalies = $report['anomalies'] ?? []; $reasonLabels = array_map( static fn (array $reason): string => (string) ($reason['label'] ?? $reason['code'] ?? '未知异常'), $anomalies['reasons'] ?? [], ); return sprintf( '%s:总行数 %s;期初 %s 件 / %s;入库 %s 件 / %s;加权单价 %s 行;异常 %s 行%s', substr((string) ($report['report_month'] ?? ''), 0, 7), number_format((int) ($totals['rows'] ?? 0)), number_format((float) ($totals['opening_quantity'] ?? 0), 2), number_format((float) ($totals['opening_amount'] ?? 0), 2), number_format((float) ($inbound['matched_quantity'] ?? 0), 2), number_format((float) ($inbound['matched_amount'] ?? 0), 2), number_format((int) ($totals['weighted_price_rows'] ?? 0)), number_format((int) ($anomalies['affected_rows'] ?? 0)), $reasonLabels === [] ? '' : '(' . implode('、', $reasonLabels) . ')', ); } private function key(mixed $productCode, mixed $warehouseShortName): string { return mb_strtoupper(trim((string) $productCode)) . "\0" . trim((string) $warehouseShortName); } private function number(float $value): float { return round($value, 6); } /** * @param array $value * @return array */ private function roundNestedNumbers(array $value): array { foreach ($value as $key => $item) { if (is_array($item)) { $value[$key] = $this->roundNestedNumbers($item); } elseif (is_float($item)) { $value[$key] = round($item, 6); } } return $value; } }