feat(edit): 商品编辑页优化

This commit is contained in:
R524809
2026-09-21 17:44:50 +08:00
parent 75fa2b60f3
commit 05747d6f22
20 changed files with 1006 additions and 244 deletions
+197
View File
@@ -0,0 +1,197 @@
import type { PackageSpec, VariantPackageSpec } from '@atlas/contracts';
/** Ozon 商品导入所需的平台属性已经在发布快照中转换为该结构。 */
export type OzonProductImportAttribute = {
id: number;
complex_id: number;
values: Array<{
value?: string;
dictionary_value_id?: number;
}>;
};
/** `/v3/product/import` 单个 SKU 的稳定请求结构。 */
export type OzonProductImportItem = {
offer_id: string;
name: string;
description_category_id: number;
type_id: number;
price: number;
currency_code?: string;
description?: string;
barcode?: string;
images?: string[];
attributes: OzonProductImportAttribute[];
depth: number;
width: number;
height: number;
dimension_unit: 'mm';
weight: number;
weight_unit: 'g';
};
export type OzonProductImportPayload = { items: OzonProductImportItem[] };
export type OzonProductImportSource = {
default_package_spec: PackageSpec;
category: {
description_category_id: string | number;
type_id: string | number;
};
title: string;
description: string | null;
barcode: string | null;
/** is_aspect=false 的商品级 Ozon 属性。 */
common_attributes: OzonProductImportAttribute[];
variants: Array<{
variant_id: string;
offer_id: string | null;
price: string | null;
currency_code: string | null;
package_spec: VariantPackageSpec;
images: string[];
attributes: OzonProductImportAttribute[];
}>;
};
/** 发布快照不可转换为 Ozon payload 时抛出的确定性错误。 */
export class OzonProductImportPayloadError extends Error {
public constructor(
public readonly variantId: string,
public readonly field: string,
message: string,
) {
super(message);
}
}
/**
* 在 Worker 创建发布请求时按 SKU 展开包装规格。草稿只保存 SPU 默认值与
* SKU 覆盖值,禁止为发布便利而将 SPU 默认值回写、复制到全部 SKU。
*/
export function buildOzonProductImportPayload(
source: OzonProductImportSource,
): OzonProductImportPayload {
const categoryId = positiveInteger(
source.category.description_category_id,
'category.description_category_id',
);
const typeId = positiveInteger(source.category.type_id, 'category.type_id');
const name = requiredText(source.title, 'title');
return {
items: source.variants.map((variant) => {
const packageSpec = effectivePackageSpec(
source.default_package_spec,
variant.package_spec,
);
const currencyCode = optionalText(variant.currency_code);
const description = optionalText(source.description);
const barcode = optionalText(source.barcode);
return {
offer_id: requiredText(
variant.offer_id,
'offer_id',
variant.variant_id,
),
name,
description_category_id: categoryId,
type_id: typeId,
price: positiveDecimal(
variant.price,
'price',
variant.variant_id,
),
...(currencyCode ? { currency_code: currencyCode } : {}),
...(description ? { description } : {}),
...(barcode ? { barcode } : {}),
...(variant.images.length ? { images: variant.images } : {}),
// 每个 Ozon items[] 同时携带商品级属性和本 SKU 的 aspect 属性。
attributes: [
...source.common_attributes,
...variant.attributes,
],
depth: positiveDecimal(
packageSpec.length_mm,
'package_spec.length_mm',
variant.variant_id,
),
width: positiveDecimal(
packageSpec.width_mm,
'package_spec.width_mm',
variant.variant_id,
),
height: positiveDecimal(
packageSpec.height_mm,
'package_spec.height_mm',
variant.variant_id,
),
dimension_unit: 'mm',
weight: positiveDecimal(
packageSpec.shipping_weight_g,
'package_spec.shipping_weight_g',
variant.variant_id,
),
weight_unit: 'g',
};
}),
};
}
/** SKU 字段优先;空覆盖字段保留 SPU 默认包装规格作为最终物流值。 */
function effectivePackageSpec(
fallback: PackageSpec,
override: VariantPackageSpec,
): PackageSpec {
return {
length_mm: override.length_mm ?? fallback.length_mm,
width_mm: override.width_mm ?? fallback.width_mm,
height_mm: override.height_mm ?? fallback.height_mm,
shipping_weight_g:
override.shipping_weight_g ?? fallback.shipping_weight_g,
status: override.status ?? fallback.status,
};
}
function optionalText(value: string | null) {
const text = value?.trim();
return text || null;
}
function requiredText(
value: string | null,
field: string,
variantId = 'profile',
) {
const text = optionalText(value);
if (text) return text;
throw new OzonProductImportPayloadError(
variantId,
field,
`${field} 不能为空`,
);
}
function positiveInteger(value: string | number, field: string) {
const numberValue = typeof value === 'number' ? value : Number(value);
if (Number.isInteger(numberValue) && numberValue > 0) return numberValue;
throw new OzonProductImportPayloadError(
'profile',
field,
`${field} 必须为正整数`,
);
}
function positiveDecimal(
value: string | null,
field: string,
variantId: string,
) {
const numberValue = value == null ? Number.NaN : Number(value);
if (Number.isFinite(numberValue) && numberValue > 0) return numberValue;
throw new OzonProductImportPayloadError(
variantId,
field,
`${field} 必须大于 0`,
);
}