feature/limeng-work-normal-product-price-changes #155

Closed
limeng wants to merge 2 commits from feature/limeng-work-normal-product-price-changes into main
4 changed files with 179 additions and 18 deletions

View File

@ -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,

View File

@ -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' => [

View File

@ -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<object>,
* 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<object> $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<object> $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(

View File

@ -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 @@
</div>
@endif
@if (!($productCatalogPurchasePriceSyncStatus['initialized'] ?? false) && !$productCatalogError)
@if (!($productCatalogPriceChangeSyncStatus['initialized'] ?? false) && !$productCatalogError)
<div
class="product-catalog-notice"
role="status"
data-purchase-price-sync-state="{{ $productCatalogPurchasePriceSyncStatus['code'] ?? 'unavailable' }}"
data-price-change-sync-state="{{ $productCatalogPriceChangeSyncStatus['code'] ?? 'unavailable' }}"
>
<span class="material-icons-outlined text-base" aria-hidden="true">schedule</span>
<span>
@if (($productCatalogPurchasePriceSyncStatus['code'] ?? '') === 'initializing')
1688代发单价正在执行首次同步,完成前当前仅显示普通商品采购价。
@elseif (($productCatalogPurchasePriceSyncStatus['code'] ?? '') === 'initial_failed')
1688代发单价首次同步失败,当前仅显示普通商品采购价。
@if (($productCatalogPriceChangeSyncStatus['code'] ?? '') === 'initializing')
商品价格变价记录正在执行首次同步,完成前变价前价格、变价后价格、差额与变价时间显示为空。
@elseif (($productCatalogPriceChangeSyncStatus['code'] ?? '') === 'initial_failed')
商品价格变价记录首次同步失败,当前不显示变价前价格、变价后价格、差额与变价时间。
@else
1688代发单价尚未完成首次同步,当前仅显示普通商品采购价。
商品价格变价记录尚未完成首次同步,当前不显示变价前价格、变价后价格、差额与变价时间。
@endif
</span>
</div>
@endif
@if (!($productCatalogWorkPlatformPriceSyncStatus['initialized'] ?? false) && !$productCatalogError)
<div
class="product-catalog-notice"
@ -557,7 +563,11 @@
<th class="is-numeric">普通商品采购价</th>
<th class="is-numeric">工作平台价格</th>
<th>工作平台价格生效日期</th>
<th class="is-numeric">1688代发单价</th>
<th>定价状态</th>
<th class="is-numeric">变价前价格</th>
<th class="is-numeric">变价后价格</th>
<th class="is-numeric">差额</th>
<th>变价时间</th>
<th>数据来源</th>
<th class="is-numeric">市场价</th>
<th>商品状态</th>
@ -594,7 +604,14 @@
<td class="is-numeric">{{ isset($row->purchase_price) && $row->purchase_price !== null ? number_format((float) $row->purchase_price, 2) : '-' }}</td>
<td class="is-numeric" title="{{ $row->work_platform_price_effect_time ?? '' }}">{{ isset($row->work_platform_price) && $row->work_platform_price !== null ? number_format((float) $row->work_platform_price, 2) : '-' }}</td>
<td class="is-code">{{ !empty($row->work_platform_price_effect_time) ? \Illuminate\Support\Carbon::parse($row->work_platform_price_effect_time)->format('Y-m-d') : '-' }}</td>
<td class="is-numeric" title="{{ $row->purchase_order_finished_at ?? '' }}">{{ isset($row->purchase_order_unit_price) && $row->purchase_order_unit_price !== null ? number_format((float) $row->purchase_order_unit_price, 2) : '-' }}</td>
<td>{!! (($row->pricing_status ?? '未定价') === '已定价')
? '<span style="color:#16a34a;font-weight:600;">已定价</span>'
: '<span style="color:#9ca3af;">未定价</span>' !!}</td>
@foreach (['price_change_before_price', 'price_change_after_price'] as $priceChangeField)
<td class="is-numeric">{{ isset($row->{$priceChangeField}) && $row->{$priceChangeField} !== null ? number_format((float) $row->{$priceChangeField}, 2) : '-' }}</td>
@endforeach
<td class="is-numeric">{{ isset($row->price_change_diff_amount) && $row->price_change_diff_amount !== null ? number_format((float) $row->price_change_diff_amount, 2) : '-' }}</td>
<td class="is-code">{{ !empty($row->price_change_effect_time) ? \Illuminate\Support\Carbon::parse($row->price_change_effect_time)->format('Y-m-d') : '-' }}</td>
<td class="product-catalog-source-list">
@forelse ($row->data_sources ?? [] as $source)
<span class="product-catalog-source-item">{{ $source }}</span>
@ -617,7 +634,7 @@
</tr>
@empty
<tr>
<td colspan="20" class="product-catalog-empty">
<td colspan="24" class="product-catalog-empty">
<span class="material-icons-outlined text-3xl block mb-2" aria-hidden="true">inventory_2</span>
@if ($productCatalogError)
数据暂时不可用。