feat(edit): 优化图片导入ozon问题

This commit is contained in:
2026-10-07 21:49:28 +08:00
parent 79b310efe7
commit 06dbd817f0
13 changed files with 289 additions and 70 deletions
+76 -14
View File
@@ -45,7 +45,7 @@ async function request(
});
return { status: res.status, body: (await res.json()) as any };
}
async function fixture(count = 2) {
async function fixture(count = 2, mediaCount = 1) {
const created = await request(
'/collection-items',
'POST',
@@ -106,16 +106,18 @@ async function fixture(count = 2) {
[profile, `TEST-${randomUUID()}`, quote, variant],
)
).rows[0].id;
const asset = (
await tx.query<{ id: string }>(
`INSERT INTO atlas.media_assets(workspace_id,storage_provider,bucket,object_key,content_type,byte_size,checksum_sha256) VALUES($1,'qiniu_kodo','test-bucket',$2,'image/png',100,$3) RETURNING id`,
[workspace, randomUUID(), randomUUID()],
)
).rows[0].id;
await tx.query(
`INSERT INTO atlas.product_variant_listing_media(workspace_id,product_id,profile_id,listing_id,product_variant_id,asset_id,sort_order) VALUES($1,$2,$3,$4,$5,$6,0)`,
[workspace, product, profile, listing, variant, asset],
);
for (let mediaIndex = 0; mediaIndex < mediaCount; mediaIndex++) {
const asset = (
await tx.query<{ id: string }>(
`INSERT INTO atlas.media_assets(workspace_id,storage_provider,bucket,object_key,content_type,byte_size,checksum_sha256) VALUES($1,'qiniu_kodo','test-bucket',$2,'image/png',100,$3) RETURNING id`,
[workspace, randomUUID(), randomUUID()],
)
).rows[0].id;
await tx.query(
`INSERT INTO atlas.product_variant_listing_media(workspace_id,product_id,profile_id,listing_id,product_variant_id,asset_id,sort_order) VALUES($1,$2,$3,$4,$5,$6,$7)`,
[workspace, product, profile, listing, variant, asset, mediaIndex],
);
}
}
});
return { profile, product, ids };
@@ -666,7 +668,9 @@ describe('首次发布 HTTP / 数据库 / Worker 闭环', () => {
});
assert.equal(job.items[0].status, 'polling');
const offers = job.items.map((i) => i.offer_id);
job = await run(job, async () => ({
job = await run(job, async (_c, _k, path) => path === '/v2/product/pictures/info' ? ({
items: [{ product_id: 101, primary_photo: ['https://images.example.test/fixture.png'], photo: [], errors: [] }],
}) : ({
result: {
items: [
{
@@ -735,12 +739,70 @@ describe('首次发布 HTTP / 数据库 / Worker 闭环', () => {
});
assert.equal(job.status, 'reconciling');
assert.equal(imports, 1);
job = await run(job, async () => ({
items: [{ offer_id: job.items[0].offer_id, id: 999 }],
job = await run(job, async (_c, _k, path) => ({
items: path === '/v2/product/pictures/info'
? [{ product_id: 999, primary_photo: ['https://images.example.test/fixture.png'], photo: [], errors: [] }]
: [{ offer_id: job.items[0].offer_id, id: 999 }],
}));
assert.equal(job.status, 'succeeded');
assert.equal(imports, 1);
});
it('导入成功后等待图片结果,缺图警告保留到 HTTP 结果与 SKU,并且不重复导入', async () => {
const f = await fixture(1, 3);
let job = (await publish(f)).body.data.publish_job as ProductPublishJob;
let imports = 0;
job = await run(job, async (_c, _k, path) => {
if (path === '/v3/product/info/list') return { items: [] };
assert.equal(path, '/v3/product/import');
imports++;
return { result: { task_id: 456 } };
});
const imported = { result: { items: [{ offer_id: job.items[0].offer_id, product_id: 102,
status: 'imported', errors: [] }] } };
job = await run(job, async (_c, _k, path) => path === '/v1/product/import/info' ? imported
: { items: [{ product_id: 102, primary_photo: [], photo: [], errors: [] }] });
assert.equal(job.status, 'running');
assert.equal(job.items[0].status, 'polling');
job = await run(job, async (_c, _k, path) => path === '/v1/product/import/info' ? {
result: { items: [{ ...imported.result.items[0], errors: [{ code: 'primary_image_load_failed',
message: 'https://private.example.test/primary.png?token=hidden' }] }] },
} : { items: [{ product_id: 102, primary_photo: ['https://images.example.test/second.png'], photo: [],
errors: [{ url: 'https://private.example.test/primary.png?token=hidden', message: 'request process failed: unknown error host unreachable' },
{ url: 'https://private.example.test/third.png?token=hidden', message: 'failed https://private.example.test/third.png?token=hidden' }] }] });
assert.equal(job.status, 'succeeded');
assert.equal(job.items[0].status, 'imported');
assert.equal(job.items[0].platform_product_id, '102');
assert.ok(job.items[0].errors.some((error) => error.code === 'OZON_IMAGES_INCOMPLETE' && error.message.includes('1/3')));
assert.ok(job.items[0].errors.some((error) => error.code === 'primary_image_load_failed'));
assert.ok(job.items[0].errors.some((error) => error.message.includes('全球加速')));
assert.ok(!JSON.stringify(job).includes('token='));
assert.ok(!JSON.stringify(job).includes('private.example.test'));
const listing = (await db.workspaceTransaction(workspace, tx => tx.query(
'SELECT status,last_error FROM atlas.product_variant_platform_listings WHERE profile_id=$1', [f.profile],
))).rows[0];
assert.equal(listing.status, 'imported');
assert.deepEqual(listing.last_error, job.items[0].errors);
await run(job, async () => { imports++; throw new Error('终态不得重复上报'); });
assert.equal(imports, 1);
});
it('图片全部到齐才报告无警告成功,长时间未齐时以已导入并提示缺图结束', async () => {
for (const completePictures of [true, false]) {
const f = await fixture(1, 3);
let job = (await publish(f)).body.data.publish_job as ProductPublishJob;
job = await run(job, async (_c, _k, path) => path === '/v3/product/info/list'
? { items: [] } : { result: { task_id: 457 } });
if (!completePictures) await db.workspaceTransaction(workspace, tx => tx.query(
'UPDATE atlas.product_publish_jobs SET poll_count=121 WHERE id=$1', [job.id],
));
job = await run(job, async (_c, _k, path) => path === '/v1/product/import/info'
? { result: { items: [{ offer_id: job.items[0].offer_id, product_id: 103, status: 'imported', errors: [] }] } }
: { items: [{ product_id: 103, primary_photo: completePictures ? ['https://images.example.test/1.png'] : [],
photo: completePictures ? ['https://images.example.test/2.png', 'https://images.example.test/3.png'] : [], errors: [] }] });
assert.equal(job.status, 'succeeded');
assert.equal(job.items[0].status, 'imported');
assert.equal(job.items[0].errors.length, completePictures ? 0 : 1);
}
});
it('任一图片准备失败时整批不提交,并隐藏存储路径', async () => {
const f = await fixture(2);
const created = (await publish(f)).body.data
@@ -42,6 +42,7 @@ import { ProductImageSuiteDrawer } from './components/ProductImageSuiteDrawer';
import type { SkuEditorImage } from './components/SkuImageCard';
import { displayBrandName, OZON_NO_BRAND_VALUE } from './brandDisplay';
import { composeSkuOfferId, lockedSkuOfferIds, skuOfferSuffix, syncSkuOfferIds } from './skuOfferId';
import { useDraftSaveShortcut } from './useDraftSaveShortcut';
import {
ProductCopyComparisonModal,
SkuCopyAiModal,
@@ -498,6 +499,8 @@ const ProductEditPage = () => {
const [catalogRefresh, setCatalogRefresh] = useState(0);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const savingLock = useRef(false);
useDraftSaveShortcut(!!snapshot && !!currentPlatform && !loading && !saving, () => form.submit());
const [savingSourceDescription, setSavingSourceDescription] = useState(false);
const [dirty, setDirty] = useState(false);
const [publishStatusContainer, setPublishStatusContainer] = useState<HTMLDivElement | null>(null);
@@ -902,7 +905,7 @@ const ProductEditPage = () => {
/** 保存草稿:携带快照版本号做乐观锁;来源更新/版本冲突时重新加载合并结果。 */
const save = async (values: EditorFormValues) => {
values = { ...values, ...form.getFieldsValue(true) };
if (!productId || !currentPlatform || !snapshot) return false;
if (!productId || !currentPlatform || !snapshot || savingLock.current) return false;
const draft = normalizedValues({
...values,
ai_source_material: sourceDraft ?? snapshot.source_material,
@@ -922,6 +925,7 @@ const ProductEditPage = () => {
message.error('完整货号(含型号前缀)最多 50 字符,请缩短型号或 SKU 后缀');
return false;
}
savingLock.current = true;
setSaving(true);
try {
await productEditorApi.save(
@@ -959,6 +963,7 @@ const ProductEditPage = () => {
} else message.error(apiErrorMessage(error));
return false;
} finally {
savingLock.current = false;
setSaving(false);
}
};
@@ -1699,6 +1704,8 @@ const ProductEditPage = () => {
htmlType="submit"
loading={saving}
icon={<SaveOutlined />}
title="保存草稿(Command / Ctrl + S)"
aria-keyshortcuts="Meta+S Control+S"
>
保存草稿
</Button>
@@ -216,8 +216,10 @@ export function ProductPublishActions({
),
]
: [];
const hasImportWarnings = job?.items.some((item) => item.status === 'imported' && item.errors.length > 0);
const resultLabel = job?.status === 'succeeded' && hasImportWarnings ? '已导入,有提示' : job ? jobLabels[job.status] : '';
const statusTone = job?.status === 'succeeded'
? 'success'
? hasImportWarnings ? 'warning' : 'success'
: job?.status === 'failed'
? 'error'
: job?.status === 'partially_succeeded' || job?.status === 'reconciling'
@@ -243,14 +245,14 @@ export function ProductPublishActions({
type="button"
className={`editor-publish-status editor-publish-status--${statusTone}`}
onClick={showJob}
aria-label={`查看发布任务详情:${jobLabels[job.status]},${job.item_count} 个 SKU`}
aria-label={`查看发布任务详情:${resultLabel},${job.item_count} 个 SKU`}
>
<span className="editor-publish-status-icon" aria-hidden="true">
{statusIcon}
</span>
<span className="editor-publish-status-content">
<span className="editor-publish-status-title">
发布任务 · {jobLabels[job.status]}
发布任务 · {resultLabel}
</span>
<span className="editor-publish-status-count">{job.item_count} 个 SKU</span>
</span>
@@ -350,18 +352,20 @@ export function ProductPublishActions({
type={
job.status === 'failed'
? 'error'
: job.status === 'reconciling'
: job.status === 'reconciling' || job.status === 'partially_succeeded' || hasImportWarnings
? 'warning'
: terminal(job)
? 'success'
: 'info'
}
showIcon
title={`${jobLabels[job.status]} · ${job.item_count} 个 SKU`}
title={`${resultLabel} · ${job.item_count} 个 SKU`}
description={
job.status === 'reconciling'
? '平台受理结果尚未确认,系统将继续对账,请勿重复导入。'
: '已导入表示商品资料已进入 Ozon;审核、可见性及库存请前往在线商品或 Ozon 后台查看。'
: hasImportWarnings
? '商品已导入,部分项目仍有提示,请按下方结果说明检查图片或商品资料。'
: '已导入表示商品资料已进入 Ozon;审核、可见性及库存请前往在线商品或 Ozon 后台查看。'
}
style={{ marginBottom: 16 }}
/>
@@ -378,13 +382,13 @@ export function ProductPublishActions({
<Tag
color={
i.status === 'imported'
? 'green'
? i.errors.length ? 'gold' : 'green'
: i.status === 'failed'
? 'red'
: undefined
}
>
{itemLabels[i.status]}
{i.status === 'imported' && i.errors.length ? '已导入(有提示)' : itemLabels[i.status]}
</Tag>
),
},
@@ -694,20 +694,11 @@ export function ProductVariantSection({
[image],
collectionRecord?.id ?? null
);
const derivative = await createWatermarkedImage(
return createWatermarkedImage(
assetId,
template.id,
snapshot.product.id
);
const asset = derivative.output_asset;
if (!asset) throw new Error('水印处理结果不可用');
return {
id: `asset:${asset.id}`,
url: asset.preview_url,
label: `${image.label} · ${template.name}`,
origin: 'derivative',
assetId: asset.id,
};
}}
onResolveAsset={async (image) => {
const [assetId] = await resolveSkuImageSelection(
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
import { App as AntdApp, Alert, Button, Dropdown, Empty, Image, Modal, Spin } from 'antd';
import type { MenuProps } from 'antd';
import { CheckOutlined, DownOutlined, UploadOutlined } from '@ant-design/icons';
import type { CollectionItemInspection, ProductImageDeletion, ProductImageDerivativeRecord, WatermarkTemplate } from '@atlas/contracts';
import type { CollectionItemInspection, ProductImageDeletion, ProductImageDerivativeRecord, WatermarkDerivative, WatermarkTemplate } from '@atlas/contracts';
import { apiErrorMessage } from '@/services/api';
import { mediaApi } from '@/services/media';
import { watermarksApi } from '@/services/watermarks';
@@ -29,7 +29,7 @@ type Props = {
onConfirm: (images: SkuEditorImage[]) => Promise<void>;
onUploadFile: (file: File) => Promise<SkuEditorImage>;
onMakeThreeFour: (image: SkuEditorImage) => Promise<SkuEditorImage>;
onApplyWatermark: (image: SkuEditorImage, template: WatermarkTemplate) => Promise<SkuEditorImage>;
onApplyWatermark: (image: SkuEditorImage, template: WatermarkTemplate) => Promise<WatermarkDerivative>;
onResolveAsset: (image: SkuEditorImage) => Promise<string>;
onImageDeleted?: (image: SkuEditorImage) => void;
onDownloadImage: (image: SkuEditorImage) => Promise<void>;
@@ -82,7 +82,8 @@ export function SkuImageEditorModal({
const [processingThreeFour, setProcessingThreeFour] = useState(false);
const [processingWatermark, setProcessingWatermark] = useState(false);
const [watermarkTargets, setWatermarkTargets] = useState<SkuEditorImage[]>([]);
const [watermarkUndo, setWatermarkUndo] = useState<Record<string, SkuEditorImage | null>>({});
const [watermarkUndo, setWatermarkUndo] = useState<Record<string, SkuEditorImage>>({});
const [watermarkReplacements, setWatermarkReplacements] = useState<Record<string, SkuEditorImage>>({});
const [aiTarget, setAiTarget] = useState<SkuEditorImage | null>(null);
const [aiImages, setAiImages] = useState<SkuEditorImage[]>([]);
const [processedRecords, setProcessedRecords] = useState<ProductImageDerivativeRecord[]>([]);
@@ -97,8 +98,11 @@ export function SkuImageEditorModal({
assetId: task.output_asset.id,
}] : []));
}, [productId]);
const processedRequestVersion = useRef(0);
const refreshProcessedImages = useCallback(async () => {
const version = ++processedRequestVersion.current;
const records = await mediaApi.listProductImageDerivatives(productId);
if (version !== processedRequestVersion.current) return;
setProcessedRecords(records);
setProcessedError(false);
}, [productId]);
@@ -136,6 +140,9 @@ export function SkuImageEditorModal({
setPreview(null);
setWatermarkTargets([]);
setWatermarkUndo({});
setWatermarkReplacements({});
setProcessedRecords([]);
setProcessedError(false);
setAiTarget(null);
return () => {
if (editorSession.current === session) {
@@ -143,6 +150,7 @@ export function SkuImageEditorModal({
deleteDialog.current?.destroy();
deleteDialog.current = null;
defaultWatermarkRequest.current = null;
processedRequestVersion.current += 1;
}
};
}, [open, productId, skuKey]);
@@ -276,45 +284,60 @@ export function SkuImageEditorModal({
if (processingLock.current || !targets.length) return;
processingLock.current = true;
setProcessingWatermark(true);
const session = editorSession.current;
let succeeded = 0;
let selectedCount = images.length;
const selectedAssetIds = new Set(images.flatMap((image) => image.assetId ? [image.assetId] : []));
try {
for (const image of targets) {
const originalSelected = images.some((item) => sameImage(item, image));
if (!originalSelected && selectedCount >= MAX_IMAGES) {
message.warning(`每个 SKU 最多选择 ${MAX_IMAGES} 张图片`);
break;
}
try {
const output = await onApplyWatermark(image, template);
const derivative = await onApplyWatermark(image, template);
if (session !== editorSession.current) return;
const asset = derivative.output_asset;
if (!asset) throw new Error('水印处理结果不可用');
const output: SkuEditorImage = {
id: `asset:${asset.id}`, url: asset.preview_url,
label: `${image.label} · ${template.name}`, origin: 'derivative', assetId: asset.id,
};
// 成功产物立即归档,较早发出的列表请求不得覆盖本次结果。
processedRequestVersion.current += 1;
setProcessedRecords((current) => [{
id: derivative.id, kind: 'watermark', source_asset_id: derivative.source_asset_id,
created_at: asset.created_at, output_asset: asset,
}, ...current.filter((record) => record.id !== derivative.id)]);
setProcessedError(false);
setWatermarkReplacements((current) => {
const sourceId = Object.keys(current).find((id) => sameImage(current[id], image)) ?? image.id;
return { ...current, [sourceId]: output };
});
setWatermarkUndo((current) => ({
...current, [output.id]: current[image.id] ?? image,
}));
succeeded += 1;
if (output.assetId && selectedAssetIds.has(output.assetId)
&& output.assetId !== image.assetId) {
message.info(`${image.label} 的水印图片已在已选区,保留原图`);
continue;
}
if (originalSelected && image.assetId) selectedAssetIds.delete(image.assetId);
const originalSelected = images.some((item) => sameImage(item, image));
// 候选图处理只更新当前卡片;已选图仅替换原位置,不自动增加选图。
if (!originalSelected) continue;
if (image.assetId) selectedAssetIds.delete(image.assetId);
if (output.assetId) selectedAssetIds.add(output.assetId);
if (!originalSelected) selectedCount += 1;
setImages((current) => {
const index = current.findIndex((item) => sameImage(item, image));
if (index < 0) return [...current, output].slice(0, MAX_IMAGES);
if (index < 0) return current;
const next = [...current];
next[index] = output;
return next;
});
setMarkedIds((current) => current.map((id) => id === image.id ? output.id : id));
setWatermarkUndo((current) => ({
...current,
[output.id]: image.id in current ? current[image.id] : originalSelected ? image : null,
}));
succeeded += 1;
} catch (error) {
if (session !== editorSession.current) return;
message.error(`${image.label} 添加水印失败:${apiErrorMessage(error)}`);
}
}
if (succeeded) {
message.success(`已生成 ${succeeded} 张水印图片,请确认后保存草稿`);
message.success(`已生成 ${succeeded} 张水印图片,已存入图片处理记录`);
void refreshProcessedImages().catch(() => setProcessedError(true));
}
else throw new Error('没有图片成功添加水印,请检查模板和素材后重试');
@@ -351,12 +374,11 @@ export function SkuImageEditorModal({
const undoWatermark = (image: SkuEditorImage) => {
if (processingLock.current || !(image.id in watermarkUndo)) return;
const original = watermarkUndo[image.id];
setImages((current) => original
? current.map((item) => item.id === image.id ? original : item)
: current.filter((item) => item.id !== image.id));
setMarkedIds((current) => original
? current.map((id) => id === image.id ? original.id : id)
: current.filter((id) => id !== image.id));
setImages((current) => current.map((item) => item.id === image.id ? original : item));
setMarkedIds((current) => current.map((id) => id === image.id ? original.id : id));
setWatermarkReplacements((current) => Object.fromEntries(
Object.entries(current).filter(([, output]) => !sameImage(output, image))
));
setWatermarkUndo((current) => {
const next = { ...current };
delete next[image.id];
@@ -530,7 +552,8 @@ export function SkuImageEditorModal({
const showPreview = (items: SkuEditorImage[], index: number) =>
setPreview({ images: items, index });
const renderSourceImages = (candidates: SkuEditorImage[]) => {
const items = candidates.filter(image => !removedImages.some(removed => sameImage(removed, image)));
const items = candidates.map((image) => watermarkReplacements[image.id] ?? image)
.filter(image => !removedImages.some(removed => sameImage(removed, image)));
return items.length ? (
<div className="sku-image-source-grid">
{items.map((image, index) => {
@@ -562,7 +585,7 @@ export function SkuImageEditorModal({
}));
const renderProcessedImages = () => {
if (processedLoading) return <div className="sku-image-processed-status"><Spin /></div>;
if (processedError) return <Empty description="图片处理记录加载失败">
if (processedError && !visibleProcessedRecords.length) return <Empty description="图片处理记录加载失败">
<Button onClick={() => void refreshProcessedImages().catch(() => setProcessedError(true))}>重试</Button>
</Empty>;
if (!visibleProcessedRecords.length) return <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无图片处理记录" />;
@@ -0,0 +1,19 @@
import { useEffect } from 'react';
/** 页面保存复用表单提交;弹窗内尚未确认的编辑不应触发背景草稿保存。 */
export function useDraftSaveShortcut(enabled: boolean, onSave: () => void) {
useEffect(() => {
const onKeyDown = (event: KeyboardEvent) => {
if (!(event.metaKey || event.ctrlKey) || event.altKey || event.shiftKey
|| (event.code !== 'KeyS' && event.key.toLowerCase() !== 's')) return;
if (event.defaultPrevented) return;
event.preventDefault();
if (!enabled || event.repeat || event.isComposing) return;
const hasDialog = [...document.querySelectorAll<HTMLElement>('[role="dialog"], [role="alertdialog"]')]
.some((dialog) => dialog.getClientRects().length > 0 && getComputedStyle(dialog).visibility !== 'hidden');
if (!hasDialog) onSave();
};
window.addEventListener('keydown', onKeyDown);
return () => window.removeEventListener('keydown', onKeyDown);
}, [enabled, onSave]);
}
+49 -6
View File
@@ -36,6 +36,11 @@ type Job = {
poll_count: number;
};
type ErrorInfo = { code: string; field?: string; message: string };
const imageErrorMessages: Record<string, string> = {
primary_image_load_failed: 'Ozon 下载主图失败,已用附加图片替代,请检查图片交付域名并重新上传。',
pics_http_error: 'Ozon 无法下载图片,请检查下载域名的海外连通性及访问限制。',
some_image_failed: '部分图片未被 Ozon 下载,请检查图片并重新上传。',
};
export type PublishDependencies = {
request: typeof requestOzon;
images: (
@@ -116,7 +121,7 @@ export function safePublishErrors(input: unknown): ErrorInfo[] {
? { field: clean(item.field, 200) }
: {}),
message: clean(
item.message ??
imageErrorMessages[String(item.code)] ?? item.message ??
item.description ??
'Ozon 拒绝该 SKU,请核对商品资料',
800,
@@ -312,6 +317,45 @@ export async function runProductPublish(
credentials.secret_ciphertext,
credentials.secret_nonce,
);
const completeImported = async (item: Item, productId: string, importErrors: unknown = []) => {
const errors = safePublishErrors(importErrors);
try {
const pictures = await dependencies.request(
credentials.platform_client_id, apiKey, '/v2/product/pictures/info',
{ product_id: [productId] },
) as { items?: Array<{
product_id: number | string; primary_photo?: string[]; photo?: string[];
errors?: Array<{ message?: string }>;
}> };
if (!Array.isArray(pictures.items)) throw new Error('Ozon 图片状态响应格式异常');
const picture = pictures.items.find((entry) => String(entry.product_id) === productId);
const count = new Set([...(picture?.primary_photo ?? []), ...(picture?.photo ?? [])]).size;
const pictureErrors = picture?.errors ?? [];
// 商品导入与图片抓取异步完成,图片尚未返回时继续轮询,不能提前报全量成功。
if (count < item.snapshot.media.length && !pictureErrors.length
&& !errors.some((entry) => entry.code in imageErrorMessages)
&& job.poll_count <= 120) return;
if (count < item.snapshot.media.length) errors.push({
code: 'OZON_IMAGES_INCOMPLETE', field: 'images',
message: `商品已导入,但 Ozon 当前仅接收 ${count}/${item.snapshot.media.length} 张图片,请检查图片交付域名后重新上传。`,
});
errors.push(...safePublishErrors(pictureErrors.map((entry) => ({
code: 'OZON_IMAGE_DOWNLOAD_FAILED', field: 'images',
message: entry.message?.includes('host unreachable')
? 'Ozon 无法连接图片下载域名,请检查海外或全球加速配置后重新上传图片。'
: entry.message || 'Ozon 图片下载失败,请检查图片直链并重新上传。',
}))));
} catch (error) {
if (job.poll_count <= 120) throw error;
errors.push({
code: 'OZON_IMAGE_STATUS_UNCONFIRMED', field: 'images',
message: '商品已导入,但暂未确认图片是否全部接收,请在 Ozon 后台核对图片。',
});
}
const unique = errors.filter((entry, index) => errors.findIndex((other) =>
other.code === entry.code && other.field === entry.field && other.message === entry.message) === index);
await complete(item, 'imported', productId, unique.slice(0, 30));
};
const remote = async () =>
parseInfoProducts(
await dependencies.request(
@@ -349,11 +393,10 @@ export async function runProductPublish(
outcome.product_id &&
String(outcome.product_id) !== '0'
)
await complete(
await completeImported(
item,
'imported',
String(outcome.product_id),
[],
outcome.errors,
);
else if (outcome.status === 'failed')
await complete(
@@ -374,7 +417,7 @@ export async function runProductPublish(
(r) => r.offer_id === item.offer_id,
);
if (found)
await complete(item, 'imported', found.id, [
await completeImported(item, found.id, [
{
code: 'OZON_IMPORT_RECONCILED',
message:
@@ -397,7 +440,7 @@ export async function runProductPublish(
for (const item of items) {
const match = found.find((r) => r.offer_id === item.offer_id);
if (match)
await complete(item, 'imported', match.id, [
await completeImported(item, match.id, [
{
code: 'OZON_IMPORT_RECONCILED',
message: