105 lines
3.6 KiB
TypeScript
105 lines
3.6 KiB
TypeScript
/**
|
||
* 后端 HTTP 客户端 —— 仅 background 使用(有 host_permissions,不受 CORS 约束)。
|
||
* 契约对齐 server 端 /api/materials(见 docs/v2/api.md)。
|
||
*/
|
||
import type { ScanResult } from '../collector/scan';
|
||
import type { TextEdits } from '../export/builder';
|
||
|
||
export interface MaterialsPayload {
|
||
product_id: string | null;
|
||
source: {
|
||
platform: string;
|
||
itemId: string | null;
|
||
url: string;
|
||
collectedAt: number;
|
||
};
|
||
texts: Array<{ kind: string; content: string; pairs?: Array<{ key: string; value: string }> | null }>;
|
||
images: Array<{
|
||
groupKey: string;
|
||
groupName: string;
|
||
variantName?: string | null;
|
||
url: string;
|
||
index: number;
|
||
type: string;
|
||
dedupeKey?: string | null;
|
||
}>;
|
||
refererOrigin?: string;
|
||
}
|
||
|
||
/** 用(可能已二次修改的)文本 + 已勾选图片,组装 /api/materials 请求体 */
|
||
export function buildMaterialsPayload(
|
||
result: ScanResult,
|
||
selectedKeys: Set<string>,
|
||
edits?: TextEdits,
|
||
): MaterialsPayload {
|
||
const text = (kind: string) => result.texts.find((t) => t.kind === kind)?.content ?? '';
|
||
|
||
const title = edits?.title ?? text('title');
|
||
const price = edits?.price ?? text('price');
|
||
const brand = edits?.brand ?? text('brand');
|
||
const desc = edits?.desc ?? text('desc');
|
||
const sellingPoints = edits?.sellingPoints ?? text('selling_point');
|
||
const params: Array<{ key: string; value: string }> = [
|
||
...(edits?.params ?? result.texts.find((t) => t.kind === 'params')?.pairs ?? []),
|
||
];
|
||
// 包装重量 / 包装尺寸合并进参数(后端存到 raw.params,studio 里再映射为 Ozon 字段)
|
||
if (edits?.weight) params.push({ key: '包装重量', value: edits.weight });
|
||
const dimSuffix = edits?.dimsUnit === 'mm' ? ' mm' : ' cm';
|
||
if (edits?.dims?.l) params.push({ key: '包装长度', value: `${edits.dims.l}${dimSuffix}` });
|
||
if (edits?.dims?.w) params.push({ key: '包装宽度', value: `${edits.dims.w}${dimSuffix}` });
|
||
if (edits?.dims?.h) params.push({ key: '包装高度', value: `${edits.dims.h}${dimSuffix}` });
|
||
|
||
const texts: MaterialsPayload['texts'] = [];
|
||
if (title) texts.push({ kind: 'title', content: title });
|
||
if (price) texts.push({ kind: 'price', content: price });
|
||
if (brand) texts.push({ kind: 'brand', content: brand });
|
||
if (params.length) texts.push({ kind: 'params', content: '', pairs: params });
|
||
if (sellingPoints) texts.push({ kind: 'selling_point', content: sellingPoints });
|
||
if (desc) texts.push({ kind: 'desc', content: desc });
|
||
|
||
const images = result.images
|
||
.filter((img) => selectedKeys.has(img.key))
|
||
.map((img) => ({
|
||
groupKey: img.groupKey,
|
||
groupName: img.groupName,
|
||
variantName: img.variantName ?? null,
|
||
url: img.url,
|
||
index: img.index,
|
||
type: img.type,
|
||
dedupeKey: img.url,
|
||
}));
|
||
|
||
return {
|
||
product_id: null,
|
||
source: {
|
||
platform: result.platform,
|
||
itemId: result.itemId,
|
||
url: result.url,
|
||
collectedAt: result.scannedAt,
|
||
},
|
||
texts,
|
||
images,
|
||
refererOrigin: 'https://www.ozon.ru',
|
||
};
|
||
}
|
||
|
||
export async function uploadMaterials(
|
||
baseUrl: string,
|
||
token: string,
|
||
payload: MaterialsPayload,
|
||
): Promise<{ product_id: string; stage: string; assets_queued: number }> {
|
||
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
||
if (token) headers.Authorization = `Bearer ${token}`; // 单用户宽松模式:token 可空
|
||
|
||
const res = await fetch(`${baseUrl.replace(/\/$/, '')}/api/materials`, {
|
||
method: 'POST',
|
||
headers,
|
||
body: JSON.stringify(payload),
|
||
});
|
||
const data = await res.json().catch(() => ({}));
|
||
if (!res.ok) {
|
||
throw new Error(data?.detail || `上传失败 HTTP ${res.status}`);
|
||
}
|
||
return data;
|
||
}
|