diff --git a/app/Http/Controllers/Business/Pages.php b/app/Http/Controllers/Business/Pages.php index 962ab187..abe012ad 100644 --- a/app/Http/Controllers/Business/Pages.php +++ b/app/Http/Controllers/Business/Pages.php @@ -1808,7 +1808,7 @@ class Pages extends Controller fputcsv($csv, [ 'ID', '款式编码', '商品编码', '商品名称', '商品简称', '规格 / 颜色', '品牌', '类目', '供应商', '销售价', '成本价', '普通商品采购价', - '工作平台价格', '工作平台价格生效日期', '1688代发单价', + '工作平台价格', '工作平台价格生效日期', '定价状态', '变价前价格', '变价后价格', '差额', '变价时间', '数据来源', '市场价', '商品状态', '库存同步', '业务更新时间', ]); @@ -1839,7 +1839,7 @@ class Pages extends Controller $row->purchase_price, $row->work_platform_price ?? null, $this->priceChangeDate($row->work_platform_price_effect_time ?? null), - $row->purchase_order_unit_price, + $row->pricing_status ?? '未定价', $row->price_change_before_price ?? null, $row->price_change_after_price ?? null, $row->price_change_diff_amount ?? null, diff --git a/config/upstream_mirrors.php b/config/upstream_mirrors.php index bb07e994..d691a266 100644 --- a/config/upstream_mirrors.php +++ b/config/upstream_mirrors.php @@ -127,8 +127,8 @@ return [ // JianSun 旧业务库的商品价格记录表。页面按商品编码取最近一条有效记录, // 不改动普通商品自身的采购价与成本价口径。 'product_price_changes' => [ - 'dataset_key' => 'jiansun.dbo.op_priceRecord', - 'target_table' => 'js_op_price_record', + 'dataset_key' => 'jiansun.dbo.pm_priceAdjustment_log', + 'target_table' => 'js_pm_price_adjustment_log', ], // 工作平台采购价格按 SKU 补充到普通商品资料;页面只读取共享镜像。 @@ -469,11 +469,28 @@ return [ 'watermark' => null, 'columns' => ['Id', 'skuCode', 'purchasePrice', 'effectTime'], 'indexes' => [ - ['skuCode(64)', 'effectTime', 'Id'], + ['skuCode', 'effectTime', 'Id'], ['effectTime', 'Id'], ], ], + // 变价明细(采购价变价历史):skucode 对应 ops_jst_sku.sku_id(商品编码)。 + // 页面按商品编码取最近一条 priceType=1(变价)记录,填变价前/后/差额/时间。 + // 无可靠修改时间(createTime 多为空),按 Id 追加,周期全量校准修正历史。 + 'jiansun.dbo.pm_priceAdjustment_log' => [ + 'source_key' => 'jiansun', + 'schema' => 'dbo', + 'object' => 'pm_priceAdjustment_log', + 'target' => 'js_pm_price_adjustment_log', + 'keys' => ['Id'], + 'watermark' => null, + 'columns' => ['Id', 'skucode', 'priceType', 'originalPrice', 'afterPrice', 'diffPrice', 'effectStartTime', 'effectEndTime', 'createTime'], + 'indexes' => [ + ['skucode', 'effectStartTime', 'Id'], + ['Id'], + ], + ], + // 工作平台 2.0:快递资料及其网点、打款账户。页面只读取共享镜像, // 首次使用前需由刷新节点显式执行对应数据集的 --full 同步。 'jiansun.dbo.wp_express' => [ diff --git a/modules/EcommerceFinance/Repositories/ProductCatalogMirrorRepository.php b/modules/EcommerceFinance/Repositories/ProductCatalogMirrorRepository.php index b3b08afa..27bbe8f6 100644 --- a/modules/EcommerceFinance/Repositories/ProductCatalogMirrorRepository.php +++ b/modules/EcommerceFinance/Repositories/ProductCatalogMirrorRepository.php @@ -23,10 +23,23 @@ class ProductCatalogMirrorRepository public const WORK_PLATFORM_SOURCE_LABEL = '工作平台价格'; + /** + * 工作平台采购价来自 JianSun 旧业务库的价格调整表,按 SKU 编码 + * 关联普通商品资料;同一 SKU 取当前已生效的最新价格记录。 + */ public const WORK_PLATFORM_PRICE_DATASET_KEY = 'jiansun.dbo.pm_priceAdjustment'; private const WORK_PLATFORM_PRICE_TABLE = 'js_pm_price_adjustment'; + /** + * 普通商品资料的“变价前价格 / 变价后价格 / 差额 / 变价时间”来自 + * JianSun 旧业务库的商品价格记录表。它与普通商品采购价、1688 代发单价 + * 分属不同来源,只按商品编码取最近一条有效记录,不参与成本价口径。 + */ + public const PRICE_CHANGE_DATASET_KEY = 'jiansun.dbo.pm_priceAdjustment_log'; + + private const PRICE_CHANGE_TABLE = 'js_pm_price_adjustment_log'; + private readonly ConnectionInterface $connection; public function __construct( @@ -95,6 +108,7 @@ class ProductCatalogMirrorRepository * data:list, * total:int, * purchase_price_sync_status:array{code:string,initialized:bool,last_succeeded_at:?string}, + * price_change_sync_status:array{code:string,initialized:bool,last_succeeded_at:?string}, * work_platform_price_sync_status:array{code:string,initialized:bool,last_succeeded_at:?string} * } */ @@ -116,6 +130,8 @@ class ProductCatalogMirrorRepository $purchasePriceSyncStatus = $this->purchasePriceSyncStatus(); $this->attachPurchasePrices($rows, $purchasePriceSyncStatus['initialized']); + $priceChangeSyncStatus = $this->priceChangeSyncStatus(); + $this->attachPriceChanges($rows, $priceChangeSyncStatus['initialized']); $workPlatformPriceSyncStatus = $this->workPlatformPriceSyncStatus(); $this->attachWorkPlatformPrices($rows, $workPlatformPriceSyncStatus['initialized']); @@ -123,6 +139,7 @@ class ProductCatalogMirrorRepository 'data' => $rows, 'total' => $total, 'purchase_price_sync_status' => $purchasePriceSyncStatus, + 'price_change_sync_status' => $priceChangeSyncStatus, 'work_platform_price_sync_status' => $workPlatformPriceSyncStatus, ]; } @@ -139,6 +156,7 @@ class ProductCatalogMirrorRepository $batchSize = min(max($batchSize, 1), 5000); $purchasePriceSyncStatus = $this->purchasePriceSyncStatus(); + $priceChangeSyncStatus = $this->priceChangeSyncStatus(); $workPlatformPriceSyncStatus = $this->workPlatformPriceSyncStatus(); $lastAutoid = null; @@ -157,6 +175,7 @@ class ProductCatalogMirrorRepository } $this->attachPurchasePrices($rows, $purchasePriceSyncStatus['initialized']); + $this->attachPriceChanges($rows, $priceChangeSyncStatus['initialized']); $this->attachWorkPlatformPrices($rows, $workPlatformPriceSyncStatus['initialized']); foreach ($rows as $row) { yield $row; @@ -233,7 +252,7 @@ class ProductCatalogMirrorRepository /** * 工作平台采购价是普通商品资料的补充价格来源,不覆盖商品自身采购价。 - * 只取已生效记录;同一 SKU 按生效时间和稳定键取最新一条。 + * effectTime 为空的历史记录仍可作为兜底,但未来生效记录不能提前覆盖当前价。 * * @param list $rows */ @@ -242,6 +261,7 @@ class ProductCatalogMirrorRepository foreach ($rows as $row) { $row->work_platform_price = null; $row->work_platform_price_effect_time = null; + $row->pricing_status = '未定价'; } if ($rows === [] || ! $initialized) { @@ -281,10 +301,105 @@ class ProductCatalogMirrorRepository $row->work_platform_price = $price->purchasePrice; $row->work_platform_price_effect_time = $price->effectTime; + $row->pricing_status = '已定价'; $row->data_sources[] = self::WORK_PLATFORM_SOURCE_LABEL; } } + /** + * 变价记录与采购单价一样是补充来源:即使镜像尚未就绪,普通商品行仍必须 + * 完整显示,四个变价字段保持为空,由页面统一说明同步状态。 + * + * @param list $rows + */ + private function attachPriceChanges(array $rows, bool $initialized): void + { + foreach ($rows as $row) { + $row->price_change_before_price = null; + $row->price_change_after_price = null; + $row->price_change_diff_amount = null; + $row->price_change_effect_time = null; + } + + if ($rows === [] || ! $initialized) { + return; + } + + $skuIds = collect($rows) + ->pluck('sku_id') + ->filter(static fn (mixed $value): bool => is_string($value) && trim($value) !== '') + ->map(static fn (string $value): string => trim($value)) + ->unique() + ->values() + ->all(); + if ($skuIds === []) { + return; + } + + $candidates = $this->connection->table($this->priceChangeTable()) + ->whereIn('skucode', $skuIds) + ->where('priceType', '1') + ->orderByDesc('effectStartTime') + ->orderByDesc('Id') + ->get([ + 'skucode', + 'originalPrice', + 'afterPrice', + 'diffPrice', + 'effectStartTime', + ]) + ->unique('skucode') + ->keyBy('skucode'); + + foreach ($rows as $row) { + $change = $candidates->get(trim((string) ($row->sku_id ?? ''))); + if ($change === null) { + continue; + } + + $row->price_change_before_price = $change->originalPrice; + $row->price_change_after_price = $change->afterPrice; + $row->price_change_diff_amount = $change->diffPrice; + $row->price_change_effect_time = $change->effectStartTime; + } + } + + /** @return array{code:string,initialized:bool,last_succeeded_at:?string} */ + public function priceChangeSyncStatus(): array + { + $schema = Schema::connection($this->connection->getName()); + if (! $schema->hasTable('business_mirror_table_states') + || ! $schema->hasTable($this->priceChangeTable())) { + return ['code' => 'unavailable', 'initialized' => false, 'last_succeeded_at' => null]; + } + + $state = $this->connection->table('business_mirror_table_states') + ->where('dataset_key', self::PRICE_CHANGE_DATASET_KEY) + ->first(); + + if ($state === null || $state->initial_completed_at === null) { + $code = match ((string) ($state->status ?? 'pending')) { + 'running' => 'initializing', + 'failed' => 'initial_failed', + default => 'pending', + }; + + return [ + 'code' => $code, + 'initialized' => false, + 'last_succeeded_at' => null, + ]; + } + + return [ + 'code' => 'ready', + 'initialized' => true, + 'last_succeeded_at' => ($state->last_succeeded_at ?? null) === null + ? null + : (string) $state->last_succeeded_at, + ]; + } + /** @return array{code:string,initialized:bool,last_succeeded_at:?string} */ public function workPlatformPriceSyncStatus(): array { @@ -305,7 +420,11 @@ class ProductCatalogMirrorRepository default => 'pending', }; - return ['code' => $code, 'initialized' => false, 'last_succeeded_at' => null]; + return [ + 'code' => $code, + 'initialized' => false, + 'last_succeeded_at' => null, + ]; } return [ @@ -317,6 +436,14 @@ class ProductCatalogMirrorRepository ]; } + private function priceChangeTable(): string + { + return (string) config( + 'upstream_mirrors.product_price_changes.target_table', + self::PRICE_CHANGE_TABLE, + ); + } + private function workPlatformPriceTable(): string { return (string) config( diff --git a/resources/views/business/pages/product-inventory/normal-products.blade.php b/resources/views/business/pages/product-inventory/normal-products.blade.php index b68b0ff5..e0c8d91f 100644 --- a/resources/views/business/pages/product-inventory/normal-products.blade.php +++ b/resources/views/business/pages/product-inventory/normal-products.blade.php @@ -9,6 +9,11 @@ 'initialized' => false, 'last_succeeded_at' => null, ]; + $productCatalogPriceChangeSyncStatus = $content['productCatalogPriceChangeSyncStatus'] ?? [ + 'code' => 'unavailable', + 'initialized' => false, + 'last_succeeded_at' => null, + ]; $productCatalogWorkPlatformPriceSyncStatus = $content['productCatalogWorkPlatformPriceSyncStatus'] ?? [ 'code' => 'unavailable', 'initialized' => false, @@ -225,7 +230,7 @@ .product-catalog-table { width: 100%; - min-width: 2320px; + min-width: 2620px; border-collapse: collapse; font-size: 0.78125rem; } @@ -501,25 +506,26 @@ @endif - @if (!($productCatalogPurchasePriceSyncStatus['initialized'] ?? false) && !$productCatalogError) + @if (!($productCatalogPriceChangeSyncStatus['initialized'] ?? false) && !$productCatalogError)
- @if (($productCatalogPurchasePriceSyncStatus['code'] ?? '') === 'initializing') - 1688代发单价正在执行首次同步,完成前当前仅显示普通商品采购价。 - @elseif (($productCatalogPurchasePriceSyncStatus['code'] ?? '') === 'initial_failed') - 1688代发单价首次同步失败,当前仅显示普通商品采购价。 + @if (($productCatalogPriceChangeSyncStatus['code'] ?? '') === 'initializing') + 商品价格变价记录正在执行首次同步,完成前变价前价格、变价后价格、差额与变价时间显示为空。 + @elseif (($productCatalogPriceChangeSyncStatus['code'] ?? '') === 'initial_failed') + 商品价格变价记录首次同步失败,当前不显示变价前价格、变价后价格、差额与变价时间。 @else - 1688代发单价尚未完成首次同步,当前仅显示普通商品采购价。 + 商品价格变价记录尚未完成首次同步,当前不显示变价前价格、变价后价格、差额与变价时间。 @endif
@endif + @if (!($productCatalogWorkPlatformPriceSyncStatus['initialized'] ?? false) && !$productCatalogError)
普通商品采购价 工作平台价格 工作平台价格生效日期 - 1688代发单价 + 定价状态 + 变价前价格 + 变价后价格 + 差额 + 变价时间 数据来源 市场价 商品状态 @@ -594,7 +604,14 @@ {{ isset($row->purchase_price) && $row->purchase_price !== null ? number_format((float) $row->purchase_price, 2) : '-' }} {{ isset($row->work_platform_price) && $row->work_platform_price !== null ? number_format((float) $row->work_platform_price, 2) : '-' }} {{ !empty($row->work_platform_price_effect_time) ? \Illuminate\Support\Carbon::parse($row->work_platform_price_effect_time)->format('Y-m-d') : '-' }} - {{ isset($row->purchase_order_unit_price) && $row->purchase_order_unit_price !== null ? number_format((float) $row->purchase_order_unit_price, 2) : '-' }} + {!! (($row->pricing_status ?? '未定价') === '已定价') + ? '已定价' + : '未定价' !!} + @foreach (['price_change_before_price', 'price_change_after_price'] as $priceChangeField) + {{ isset($row->{$priceChangeField}) && $row->{$priceChangeField} !== null ? number_format((float) $row->{$priceChangeField}, 2) : '-' }} + @endforeach + {{ isset($row->price_change_diff_amount) && $row->price_change_diff_amount !== null ? number_format((float) $row->price_change_diff_amount, 2) : '-' }} + {{ !empty($row->price_change_effect_time) ? \Illuminate\Support\Carbon::parse($row->price_change_effect_time)->format('Y-m-d') : '-' }} @forelse ($row->data_sources ?? [] as $source) {{ $source }} @@ -617,7 +634,7 @@ @empty - + @if ($productCatalogError) 数据暂时不可用。