feat(edit): 商品编辑页优化
This commit is contained in:
@@ -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`,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user