argument('month'))->startOfMonth(); $connection = (string) config('upstream_mirrors.connection', 'business_mirror'); $table = 'product_monthly_weighted_average_rows'; $deadline = $month->addMonthNoOverflow()->day(2)->startOfDay(); $query = DB::connection($connection)->table($table)->where('report_month', $month->toDateString()); $codes = (clone $query)->distinct()->pluck('product_code')->map(fn ($code) => trim((string) $code))->unique()->values()->all(); $this->info('待计算 SKU:' . count($codes)); $cost = $this->costPrices($connection, $codes); $work = $this->workPlatformPrices($connection, $codes); $change = $this->priceChanges($connection, $codes); $total = (clone $query)->count(); $computed = 0; $now = now(); for ($offset = 0; $offset < $total; $offset += 1000) { $rows = (clone $query)->orderBy('row_key')->offset($offset)->limit(1000) ->get([ 'row_key', 'barcode_warehouse', 'warehouse_short_name', 'product_code', 'base_source', 'imported_at', 'opening_quantity', 'opening_unit_price', 'previous_pricing_status', 'purchase_quantity', ]); $updates = []; foreach ($rows as $row) { // 普通商品资料与进销存的商品编码大小写不一致,统一按大写匹配。 $code = mb_strtoupper(trim((string) $row->product_code)); $costPrice = is_array($cost) ? ($cost[$code] ?? null) : null; $workPrice = is_array($work) ? ($work[$code] ?? null) : null; $changePrice = is_array($change) ? ($change[$code] ?? null) : null; $payload = $this->calculate( $row, $costPrice, $workPrice, $changePrice, $deadline, ); $updates[] = array_merge([ 'report_month' => $month->toDateString(), 'row_key' => $row->row_key, 'barcode_warehouse' => $row->barcode_warehouse, 'warehouse_short_name' => $row->warehouse_short_name, 'product_code' => $row->product_code, 'base_source' => $row->base_source, 'imported_at' => $row->imported_at, 'computed_at' => $now, ], $payload); $computed++; } if ($updates !== []) { DB::connection($connection)->table($table)->upsert( $updates, ['report_month', 'row_key'], ['adjusted_unit_price', 'opening_amount', 'purchase_unit_price', 'purchase_amount', 'weighted_average_unit_price', 'pricing_status', 'computed_at'], ); } } DB::connection($connection)->table('product_monthly_weighted_average_states')->updateOrInsert( ['report_month' => $month->toDateString()], ['status' => 'computed', 'computed_rows' => $computed, 'last_computed_at' => $now, 'updated_at' => $now, 'created_at' => $now], ); $this->info($month->format('Y-m') . " 计算完成:{$computed} 行"); return self::SUCCESS; } /** * @return array */ private function costPrices(string $connection, array $codes): array { $map = []; foreach (array_chunk($codes, 2000) as $chunk) { foreach (DB::connection($connection)->table('ops_jst_sku') ->whereIn('sku_id', $chunk)->whereNotNull('cost_price')->get(['sku_id', 'cost_price']) as $row) { $map[mb_strtoupper(trim((string) $row->sku_id))] = (float) $row->cost_price; } } return $map; } /** * @return array */ private function workPlatformPrices(string $connection, array $codes): array { $map = []; $now = now(); foreach (array_chunk($codes, 2000) as $chunk) { $rows = DB::connection($connection)->table('js_pm_price_adjustment') ->whereIn('skuCode', $chunk) ->whereNotNull('purchasePrice') ->where(function ($query) use ($now): void { $query->whereNull('effectTime')->orWhere('effectTime', '<=', $now); }) ->orderBy('effectTime') ->get(['skuCode', 'purchasePrice', 'effectTime']); foreach ($rows as $row) { $map[mb_strtoupper(trim((string) $row->skuCode))] = [ 'price' => (float) $row->purchasePrice, 'effect' => $row->effectTime === null ? null : (string) $row->effectTime, ]; } } return $map; } /** * @return array */ private function priceChanges(string $connection, array $codes): array { $map = []; foreach (array_chunk($codes, 2000) as $chunk) { $rows = DB::connection($connection)->table('js_pm_price_adjustment_log') ->whereIn('skucode', $chunk) ->whereNotNull('afterPrice') ->orderBy('effectStartTime') ->get(['skucode', 'afterPrice', 'effectStartTime']); foreach ($rows as $row) { $map[mb_strtoupper(trim((string) $row->skucode))] = [ 'price' => (float) $row->afterPrice, 'time' => $row->effectStartTime === null ? null : (string) $row->effectStartTime, ]; } } return $map; } /** * @param array{price:float,effect:?string}|null $work * @param array{price:float,time:?string}|null $change * @return array */ private function calculate(object $row, ?float $cost, ?array $work, ?array $change, CarbonImmutable $deadline): array { $openingQuantity = $row->opening_quantity === null ? 0.0 : (float) $row->opening_quantity; $purchaseQuantity = $row->purchase_quantity === null ? 0.0 : (float) $row->purchase_quantity; $openingUnitPrice = $row->opening_unit_price === null ? null : (float) $row->opening_unit_price; $previousStatus = trim((string) ($row->previous_pricing_status ?? '')); $changePrice = $change['price'] ?? null; $workPrice = $work['price'] ?? null; // 更改价格 + 定价状态(表2 第10条) if ($openingUnitPrice !== null) { if ($previousStatus !== '预估价') { $adjusted = $openingUnitPrice; $status = $previousStatus !== '' ? $previousStatus : '已定价'; } elseif ($changePrice !== null) { $adjusted = $changePrice; $status = '已定价'; } else { $adjusted = $openingUnitPrice; $status = '预估价'; } } elseif ($changePrice !== null) { $adjusted = $changePrice; $status = '已定价'; } elseif ($cost !== null) { $adjusted = $cost; $status = $workPrice !== null ? '已定价' : '未定价'; } else { $adjusted = null; $status = '未定价'; } // 采购单价(表2 第13条):变价后价格优先,变价时间晚于次月2号取工作平台价格,兜底聚水潭成本价 $changeTime = $change['time'] ?? null; $lateChange = $changeTime !== null && CarbonImmutable::parse($changeTime)->greaterThanOrEqualTo($deadline); $purchaseUnitPrice = match (true) { $changePrice !== null && ! $lateChange => $changePrice, $lateChange && $workPrice !== null => $workPrice, $changePrice !== null => $changePrice, $workPrice !== null => $workPrice, $cost !== null => $cost, default => null, }; $openingAmount = $adjusted === null ? null : round($openingQuantity * $adjusted, 2); $purchaseAmount = $purchaseUnitPrice === null ? null : round($purchaseQuantity * $purchaseUnitPrice, 2); $denominator = $openingQuantity + $purchaseQuantity; if ($denominator === 0.0) { $weighted = $adjusted === null ? null : round($adjusted, 2); } else { $weighted = round((($openingAmount ?? 0) + ($purchaseAmount ?? 0)) / $denominator, 2); } return [ 'adjusted_unit_price' => $adjusted === null ? null : round($adjusted, 2), 'opening_amount' => $openingAmount, 'purchase_unit_price' => $purchaseUnitPrice === null ? null : round($purchaseUnitPrice, 2), 'purchase_amount' => $purchaseAmount, 'weighted_average_unit_price' => $weighted, 'pricing_status' => $status, ]; } }