cangku/backend/src/services/alibaba1688ProductService.js

632 lines
24 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use strict';
const planningDatabase = require('./planningDatabase');
const { buildAlibaba1688SkuFingerprint } = require('./alibaba1688SkuIdentity');
const PRODUCT_TABLE = 'alibaba_1688_products';
const MAX_PRODUCT_SNAPSHOT_BYTES = 12 * 1024 * 1024;
const MAX_PAGE_SIZE = 200;
const MAX_SKU_COUNT = 5000;
const PUBLISH_STATUSES = new Set(['pending', 'ready', 'publishing', 'published', 'failed', 'disabled']);
const IMPORT_TAGS = new Set(['测款', '风格化']);
const SOURCE_TYPES = new Set(['product', 'store']);
let schemaReady = false;
let schemaPromise = null;
function serviceError(message, statusCode = 400, code = 'INVALID_REQUEST') {
const error = new Error(message);
error.statusCode = statusCode;
error.code = code;
return error;
}
function normalizeText(value, maxLength = 1000) {
return String(value ?? '').trim().slice(0, maxLength);
}
function normalizeProductId(value) {
const productId = normalizeText(value, 80);
if (!productId) throw serviceError('缺少 1688 商品 ID', 400, 'PRODUCT_ID_REQUIRED');
if (!/^[A-Za-z0-9_-]+$/.test(productId)) {
throw serviceError('1688 商品 ID 格式不正确', 400, 'INVALID_PRODUCT_ID');
}
return productId;
}
function normalizeNumber(value) {
if (value === null || value === undefined || value === '') return null;
const numeric = Number(value);
return Number.isFinite(numeric) ? numeric : null;
}
function uniqueTextValues(values, limit, maxLength = 2000) {
return [...new Set((Array.isArray(values) ? values : [])
.map(value => normalizeText(value, maxLength))
.filter(Boolean))].slice(0, limit);
}
function normalizeAlibaba1688ImageUrl(value) {
return normalizeText(value, 2000).replace(/_sum\.jpg(?=([?#]|$))/gi, '');
}
function uniqueImageValues(values, limit) {
return [...new Set((Array.isArray(values) ? values : [])
.map(normalizeAlibaba1688ImageUrl)
.filter(Boolean))].slice(0, limit);
}
function normalizeWebUrl(value, maxLength = 4000) {
const text = normalizeText(value, maxLength);
if (!text) return '';
try {
const url = new URL(text);
return ['http:', 'https:'].includes(url.protocol) ? url.href : '';
} catch {
return '';
}
}
function normalizeVideos(videos) {
const result = [];
for (const video of Array.isArray(videos) ? videos : []) {
const url = normalizeWebUrl(video?.url || video?.src || video?.downloadUrl || video?.previewUrl);
if (!url || result.some(item => item.url === url)) continue;
const duration = normalizeNumber(video?.duration);
const width = normalizeNumber(video?.width || video?.videoWidth);
const height = normalizeNumber(video?.height || video?.videoHeight);
result.push({
url,
coverUrl: normalizeAlibaba1688ImageUrl(video?.coverUrl || video?.poster),
duration: duration !== null && duration >= 0 ? duration : null,
width: width !== null && width > 0 ? width : null,
height: height !== null && height > 0 ? height : null,
mimeType: normalizeText(video?.mimeType || video?.type, 120),
source: normalizeText(video?.source, 120) || 'main-gallery',
});
}
return result.slice(0, 20);
}
function normalizeSpecs(specs) {
return (Array.isArray(specs) ? specs : [])
.map(spec => ({
name: normalizeText(spec?.name, 120),
value: normalizeText(spec?.value, 500),
}))
.filter(spec => spec.name && spec.value)
.slice(0, 4);
}
function normalizeSkus(skus) {
if (!Array.isArray(skus)) return [];
if (skus.length > MAX_SKU_COUNT) {
throw serviceError(`SKU 数量不能超过 ${MAX_SKU_COUNT}`, 413, 'SKU_LIMIT_EXCEEDED');
}
return skus.map((sku, index) => ({
index: Number(sku?.index) || index + 1,
name: normalizeText(sku?.name, 1200),
specs: normalizeSpecs(sku?.specs),
image: normalizeAlibaba1688ImageUrl(sku?.image),
quantity: Number(sku?.quantity) || 1,
priceText: normalizeText(sku?.priceText, 300),
price: normalizeNumber(sku?.price),
rowPriceText: normalizeText(sku?.rowPriceText, 300),
freightText: normalizeText(sku?.freightText, 300),
freight: normalizeNumber(sku?.freight),
stockText: normalizeText(sku?.stockText, 200),
stock: normalizeNumber(sku?.stock),
stockUnit: normalizeText(sku?.stockUnit, 80),
deliveryText: normalizeText(sku?.deliveryText, 500),
selectedText: normalizeText(sku?.selectedText, 1200),
available: sku?.available !== false,
collectedAt: normalizeText(sku?.collectedAt, 40),
}));
}
function normalizeDimensions(dimensions) {
return (Array.isArray(dimensions) ? dimensions : [])
.map(dimension => ({
name: normalizeText(dimension?.name, 120),
values: uniqueTextValues(dimension?.values, 500, 500),
}))
.filter(dimension => dimension.name && dimension.values.length)
.slice(0, 4);
}
function normalizeAttributes(attributes) {
return (Array.isArray(attributes) ? attributes : [])
.map(attribute => ({
name: normalizeText(attribute?.name, 160),
value: normalizeText(attribute?.value, 2000),
}))
.filter(attribute => attribute.name && attribute.value)
.slice(0, 500);
}
function toMysqlDateTime(value) {
if (!value) return null;
const date = new Date(value);
if (Number.isNaN(date.getTime())) return null;
return date.toISOString().slice(0, 19).replace('T', ' ');
}
function normalizeAlibaba1688Product(input = {}, routeProductId = '') {
const envelope = input && typeof input === 'object' ? input : {};
const raw = envelope.product && typeof envelope.product === 'object' ? envelope.product : envelope;
const productId = normalizeProductId(raw.productId || routeProductId);
if (routeProductId && productId !== normalizeProductId(routeProductId)) {
throw serviceError('请求路径与商品数据中的 1688 商品 ID 不一致', 409, 'PRODUCT_ID_MISMATCH');
}
const title = normalizeText(raw.title, 500);
if (!title) throw serviceError('商品标题不能为空', 422, 'TITLE_REQUIRED');
const detailImages = uniqueImageValues(raw.detailImages, 300);
if (!detailImages.length) {
throw serviceError('详情图不能为空,禁止同步不完整商品', 422, 'DETAIL_IMAGES_REQUIRED');
}
const mainImages = uniqueImageValues(
Array.isArray(raw.mainImages) && raw.mainImages.length ? raw.mainImages : [raw.mainImage],
100
);
if (!mainImages.length) {
throw serviceError('主图不能为空', 422, 'MAIN_IMAGES_REQUIRED');
}
const skus = normalizeSkus(raw.skus);
const baseSkuRows = normalizeSkus(raw.baseSkuRows);
const skuImages = uniqueImageValues(
Array.isArray(raw.skuImages) && raw.skuImages.length
? raw.skuImages
: [...skus, ...baseSkuRows].map(sku => sku.image),
MAX_SKU_COUNT
);
const skuDimensions = normalizeDimensions(raw.skuDimensions);
const attributes = normalizeAttributes(raw.attributes);
const videos = normalizeVideos(
Array.isArray(raw.videos) && raw.videos.length ? raw.videos : raw.skuScan?.videos
);
const importTag = IMPORT_TAGS.has(raw.importTag) ? raw.importTag : (envelope.sourceType === 'store' ? '风格化' : '测款');
const snapshot = {
schemaVersion: 2,
productId,
title,
description: normalizeText(raw.description, 20000),
sourceUrl: normalizeText(raw.sourceUrl || envelope.sourceUrl, 2000),
sourceStoreUrl: normalizeText(raw.sourceStoreUrl, 2000),
priceText: normalizeText(raw.priceText, 300),
minPrice: normalizeNumber(raw.minPrice),
maxPrice: normalizeNumber(raw.maxPrice),
images: uniqueImageValues(raw.images, 500),
mainImages,
mainImage: normalizeAlibaba1688ImageUrl(raw.mainImage || mainImages[0]),
detailImages,
videos,
skus,
skuImages,
baseSkuRows,
skuDimensions,
skuCombinationCount: Number(raw.skuCombinationCount) || skus.length,
skuScan: raw.skuScan && typeof raw.skuScan === 'object' ? raw.skuScan : null,
attributes,
skuValues: uniqueTextValues(raw.skuValues, 1000, 500),
storeName: normalizeText(raw.storeName, 300),
storeCategory: normalizeText(raw.storeCategory, 500),
platformCategory: normalizeText(raw.platformCategory, 500),
category: normalizeText(raw.category || raw.storeCategory || raw.platformCategory, 500),
dropshipSupported: raw.dropshipSupported == null ? null : Boolean(raw.dropshipSupported),
importTag,
collectedAt: normalizeText(raw.collectedAt, 40),
};
const snapshotJson = JSON.stringify(snapshot);
if (Buffer.byteLength(snapshotJson, 'utf8') > MAX_PRODUCT_SNAPSHOT_BYTES) {
throw serviceError('商品快照超过 12MB,禁止同步', 413, 'SNAPSHOT_TOO_LARGE');
}
return {
productId,
sourceUrl: snapshot.sourceUrl,
title,
description: snapshot.description,
storeName: snapshot.storeName,
category: snapshot.category,
importTag,
priceText: snapshot.priceText,
minPrice: snapshot.minPrice,
maxPrice: snapshot.maxPrice,
mainImageUrl: snapshot.mainImage,
mainImageCount: mainImages.length,
detailImageCount: detailImages.length,
skuCount: skus.length,
skuFingerprint: buildAlibaba1688SkuFingerprint(skus),
attributeCount: attributes.length,
snapshot,
snapshotJson,
sourceCollectedAt: toMysqlDateTime(snapshot.collectedAt),
syncOperatorName: normalizeText(
envelope.dingtalkUserName || envelope.operatorName || envelope.syncOperatorName,
120
),
sourceType: envelope.sourceType === 'store' ? 'store' : 'product',
sourceImportId: normalizeText(envelope.sourceImportId, 80),
};
}
async function ensureAlibaba1688ProductSchema() {
if (schemaReady) return;
if (schemaPromise) return schemaPromise;
schemaPromise = (async () => {
// Production planning DB accounts commonly have DML permissions only.
// MariaDB can still require CREATE permission for CREATE TABLE IF NOT EXISTS.
const existing = await planningDatabase.query(
`SELECT 1
FROM information_schema.tables
WHERE table_schema = DATABASE()
AND table_name = ?
LIMIT 1`,
[PRODUCT_TABLE]
);
if (existing.length) {
const fingerprintColumn = await planningDatabase.query(
`SELECT 1 AS ok
FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = ?
AND column_name = 'sku_fingerprint'
LIMIT 1`,
[PRODUCT_TABLE]
);
if (!fingerprintColumn.length) {
await planningDatabase.execute(
`ALTER TABLE ${PRODUCT_TABLE}
ADD COLUMN sku_fingerprint CHAR(64) NULL COMMENT '有序 SKU 规格指纹' AFTER sku_count`
);
}
return;
}
await planningDatabase.execute(
`CREATE TABLE IF NOT EXISTS ${PRODUCT_TABLE} (
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '服务端主键',
product_id VARCHAR(80) NOT NULL COMMENT '1688 商品 ID,全局唯一',
source_url VARCHAR(2000) NULL COMMENT '1688 商品链接',
title VARCHAR(500) NOT NULL COMMENT '商品标题',
description MEDIUMTEXT NULL COMMENT '商品描述',
store_name VARCHAR(300) NULL COMMENT '1688 店铺/厂家',
category VARCHAR(500) NULL COMMENT '1688 商品分类',
import_tag VARCHAR(40) NOT NULL DEFAULT '测款' COMMENT '测款/风格化',
price_text VARCHAR(300) NULL COMMENT '价格展示文本',
min_price DECIMAL(18,2) NULL COMMENT '最低采购价',
max_price DECIMAL(18,2) NULL COMMENT '最高采购价',
main_image_url VARCHAR(2000) NULL COMMENT '首张主图',
main_image_count INT NOT NULL DEFAULT 0 COMMENT '主图数量',
detail_image_count INT NOT NULL DEFAULT 0 COMMENT '详情图数量',
sku_count INT NOT NULL DEFAULT 0 COMMENT 'SKU 组合数量',
sku_fingerprint CHAR(64) NULL COMMENT '有序 SKU 规格指纹',
attribute_count INT NOT NULL DEFAULT 0 COMMENT '属性数量',
product_snapshot_json LONGTEXT NOT NULL COMMENT '客户端采集完整快照',
source_collected_at DATETIME NULL COMMENT '客户端采集时间',
sync_operator_name VARCHAR(120) NULL COMMENT '同步运营花名',
source_type VARCHAR(30) NOT NULL DEFAULT 'product' COMMENT 'product/store',
source_import_id VARCHAR(80) NULL COMMENT '客户端导入任务 ID',
api_key_id VARCHAR(120) NULL COMMENT '同步 API Key ID',
api_key_name VARCHAR(120) NULL COMMENT '同步 API Key 名称',
publish_status VARCHAR(30) NOT NULL DEFAULT 'pending' COMMENT '后续上架状态',
last_publish_error VARCHAR(1000) NULL COMMENT '最近上架失败原因',
published_goods_json LONGTEXT NULL COMMENT '各店铺上架商品 ID',
last_published_at DATETIME NULL COMMENT '最近上架完成时间',
sync_version INT NOT NULL DEFAULT 1 COMMENT '完整覆盖版本号',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
UNIQUE KEY uk_alibaba_1688_product_id (product_id),
KEY idx_alibaba_1688_product_store (store_name),
KEY idx_alibaba_1688_product_tag (import_tag, updated_at),
KEY idx_alibaba_1688_product_publish (publish_status, updated_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='1688 分销商品服务端快照'`
);
})().then(() => {
schemaReady = true;
}).catch((error) => {
schemaPromise = null;
throw error;
});
return schemaPromise;
}
function parseSnapshot(value) {
try {
return typeof value === 'string' ? JSON.parse(value) : value;
} catch {
return null;
}
}
function mapProductRow(row, includeSnapshot = true) {
if (!row) return null;
const product = includeSnapshot ? parseSnapshot(row.product_snapshot_json) : undefined;
return {
productId: String(row.product_id),
title: row.title || '',
sourceUrl: row.source_url || '',
storeName: row.store_name || '',
category: row.category || '',
importTag: row.import_tag || '测款',
priceText: row.price_text || '',
minPrice: row.min_price == null ? null : Number(row.min_price),
maxPrice: row.max_price == null ? null : Number(row.max_price),
mainImage: row.main_image_url || '',
mainImageCount: Number(row.main_image_count) || 0,
detailImageCount: Number(row.detail_image_count) || 0,
skuCount: Number(row.sku_count) || 0,
skuFingerprint: row.sku_fingerprint || (
includeSnapshot ? buildAlibaba1688SkuFingerprint(product?.skus) : ''
),
attributeCount: Number(row.attribute_count) || 0,
publishStatus: row.publish_status || 'pending',
lastPublishError: row.last_publish_error || '',
publishedGoods: parseSnapshot(row.published_goods_json) || [],
syncVersion: Number(row.sync_version) || 1,
sourceCollectedAt: row.source_collected_at || null,
syncOperatorName: row.sync_operator_name || '',
sourceType: row.source_type || 'product',
sourceImportId: row.source_import_id || '',
createdAt: row.created_at || null,
updatedAt: row.updated_at || null,
...(includeSnapshot ? { product } : {}),
};
}
async function getAlibaba1688Product(productId, options = {}) {
await ensureAlibaba1688ProductSchema();
const normalizedId = normalizeProductId(productId);
const rows = await planningDatabase.query(
`SELECT * FROM ${PRODUCT_TABLE} WHERE product_id = ? LIMIT 1`,
[normalizedId]
);
if (!rows[0] && options.required !== false) {
throw serviceError('未找到该 1688 商品', 404, 'PRODUCT_NOT_FOUND');
}
return mapProductRow(rows[0], options.includeSnapshot !== false);
}
async function listAlibaba1688Products(input = {}) {
await ensureAlibaba1688ProductSchema();
const page = Math.max(1, Number.parseInt(input.page, 10) || 1);
const pageSize = Math.min(MAX_PAGE_SIZE, Math.max(1, Number.parseInt(input.pageSize, 10) || 20));
const baseConditions = [];
const baseParams = [];
const keyword = normalizeText(input.keyword, 200);
if (keyword) {
baseConditions.push('(product_id LIKE ? OR title LIKE ? OR store_name LIKE ? OR category LIKE ? OR sync_operator_name LIKE ?)');
const pattern = `%${keyword}%`;
baseParams.push(pattern, pattern, pattern, pattern, pattern);
}
if (IMPORT_TAGS.has(input.importTag)) {
baseConditions.push('import_tag = ?');
baseParams.push(input.importTag);
}
const sourceType = normalizeText(input.sourceType, 30);
if (SOURCE_TYPES.has(sourceType)) {
baseConditions.push('source_type = ?');
baseParams.push(sourceType);
}
const syncOperatorName = normalizeText(input.syncOperatorName, 120);
if (syncOperatorName) {
baseConditions.push('sync_operator_name LIKE ?');
baseParams.push(`%${syncOperatorName}%`);
}
const publishStatuses = uniqueTextValues(
Array.isArray(input.publishStatus)
? input.publishStatus
: String(input.publishStatus || '').split(','),
PUBLISH_STATUSES.size,
30
).filter(status => PUBLISH_STATUSES.has(status));
const conditions = [...baseConditions];
const params = [...baseParams];
if (publishStatuses.length) {
conditions.push(`publish_status IN (${publishStatuses.map(() => '?').join(', ')})`);
params.push(...publishStatuses);
}
const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
const baseWhere = baseConditions.length ? `WHERE ${baseConditions.join(' AND ')}` : '';
const countRows = await planningDatabase.query(
`SELECT COUNT(*) AS total FROM ${PRODUCT_TABLE} ${where}`,
params
);
const summaryRows = await planningDatabase.query(
`SELECT publish_status, COUNT(*) AS total, MAX(updated_at) AS latest_updated_at
FROM ${PRODUCT_TABLE}
${baseWhere}
GROUP BY publish_status`,
baseParams
);
const rows = await planningDatabase.query(
`SELECT product_id, source_url, title, store_name, category, import_tag, price_text,
min_price, max_price, main_image_url, main_image_count, detail_image_count,
sku_count, sku_fingerprint, attribute_count, publish_status, last_publish_error,
published_goods_json, sync_version, source_collected_at, sync_operator_name,
source_type, source_import_id, created_at, updated_at
FROM ${PRODUCT_TABLE}
${where}
ORDER BY updated_at DESC, id DESC
LIMIT ? OFFSET ?`,
[...params, pageSize, (page - 1) * pageSize]
);
return {
items: rows.map(row => mapProductRow(row, false)),
page,
pageSize,
total: Number(countRows[0]?.total) || 0,
summary: Object.fromEntries(
[...PUBLISH_STATUSES].map(status => [
status,
Number(summaryRows.find(row => row.publish_status === status)?.total) || 0,
])
),
latestUpdatedAt: summaryRows.reduce((latest, row) => {
const value = row.latest_updated_at;
if (!value) return latest;
return !latest || new Date(value) > new Date(latest) ? value : latest;
}, null),
};
}
function productSqlValues(product, context = {}) {
return [
product.productId,
product.sourceUrl || null,
product.title,
product.description || null,
product.storeName || null,
product.category || null,
product.importTag,
product.priceText || null,
product.minPrice,
product.maxPrice,
product.mainImageUrl || null,
product.mainImageCount,
product.detailImageCount,
product.skuCount,
product.skuFingerprint,
product.attributeCount,
product.snapshotJson,
product.sourceCollectedAt,
product.syncOperatorName || null,
product.sourceType,
product.sourceImportId || null,
normalizeText(context.apiKeyId, 120) || null,
normalizeText(context.apiKeyName, 120) || null,
];
}
async function createAlibaba1688Product(input, context = {}) {
await ensureAlibaba1688ProductSchema();
const product = normalizeAlibaba1688Product(input);
const existing = await getAlibaba1688Product(product.productId, { required: false, includeSnapshot: false });
if (existing) throw serviceError('该 1688 商品 ID 已存在', 409, 'PRODUCT_ALREADY_EXISTS');
try {
await planningDatabase.execute(
`INSERT INTO ${PRODUCT_TABLE} (
product_id, source_url, title, description, store_name, category, import_tag,
price_text, min_price, max_price, main_image_url, main_image_count,
detail_image_count, sku_count, sku_fingerprint, attribute_count, product_snapshot_json,
source_collected_at, sync_operator_name, source_type, source_import_id,
api_key_id, api_key_name
) VALUES (${Array(23).fill('?').join(', ')})`,
productSqlValues(product, context)
);
} catch (error) {
if (error.code === 'ER_DUP_ENTRY') {
throw serviceError('该 1688 商品 ID 已存在', 409, 'PRODUCT_ALREADY_EXISTS');
}
throw error;
}
return { operation: 'created', item: await getAlibaba1688Product(product.productId) };
}
async function replaceAlibaba1688Product(productId, input, context = {}) {
await ensureAlibaba1688ProductSchema();
const product = normalizeAlibaba1688Product(input, productId);
const result = await planningDatabase.execute(
`INSERT INTO ${PRODUCT_TABLE} (
product_id, source_url, title, description, store_name, category, import_tag,
price_text, min_price, max_price, main_image_url, main_image_count,
detail_image_count, sku_count, sku_fingerprint, attribute_count, product_snapshot_json,
source_collected_at, sync_operator_name, source_type, source_import_id,
api_key_id, api_key_name
) VALUES (${Array(23).fill('?').join(', ')})
ON DUPLICATE KEY UPDATE
source_url = VALUES(source_url),
title = VALUES(title),
description = VALUES(description),
store_name = VALUES(store_name),
category = VALUES(category),
import_tag = VALUES(import_tag),
price_text = VALUES(price_text),
min_price = VALUES(min_price),
max_price = VALUES(max_price),
main_image_url = VALUES(main_image_url),
main_image_count = VALUES(main_image_count),
detail_image_count = VALUES(detail_image_count),
sku_count = VALUES(sku_count),
sku_fingerprint = VALUES(sku_fingerprint),
attribute_count = VALUES(attribute_count),
product_snapshot_json = VALUES(product_snapshot_json),
source_collected_at = VALUES(source_collected_at),
sync_operator_name = VALUES(sync_operator_name),
source_type = VALUES(source_type),
source_import_id = VALUES(source_import_id),
api_key_id = VALUES(api_key_id),
api_key_name = VALUES(api_key_name),
sync_version = sync_version + 1`,
productSqlValues(product, context)
);
return {
operation: Number(result?.affectedRows) === 1 ? 'created' : 'replaced',
item: await getAlibaba1688Product(product.productId),
};
}
async function updateAlibaba1688Product(productId, input = {}) {
await ensureAlibaba1688ProductSchema();
const normalizedId = normalizeProductId(productId);
const existing = await getAlibaba1688Product(normalizedId);
const updates = [];
const params = [];
if (input.importTag !== undefined) {
if (!IMPORT_TAGS.has(input.importTag)) throw serviceError('商品标记仅支持测款或风格化');
updates.push('import_tag = ?');
params.push(input.importTag);
updates.push('product_snapshot_json = ?');
params.push(JSON.stringify({ ...(existing.product || {}), importTag: input.importTag }));
}
if (input.publishStatus !== undefined) {
if (!PUBLISH_STATUSES.has(input.publishStatus)) throw serviceError('上架状态不正确');
updates.push('publish_status = ?');
params.push(input.publishStatus);
}
if (input.lastPublishError !== undefined) {
updates.push('last_publish_error = ?');
params.push(normalizeText(input.lastPublishError, 1000) || null);
}
if (input.publishedGoods !== undefined) {
updates.push('published_goods_json = ?');
params.push(JSON.stringify(Array.isArray(input.publishedGoods) ? input.publishedGoods : []));
}
if (!updates.length) throw serviceError('没有可修改的字段');
params.push(normalizedId);
const result = await planningDatabase.execute(
`UPDATE ${PRODUCT_TABLE} SET ${updates.join(', ')} WHERE product_id = ?`,
params
);
if (!result.affectedRows) return existing;
return getAlibaba1688Product(normalizedId);
}
async function deleteAlibaba1688Product(productId) {
await ensureAlibaba1688ProductSchema();
const normalizedId = normalizeProductId(productId);
const result = await planningDatabase.execute(
`DELETE FROM ${PRODUCT_TABLE} WHERE product_id = ?`,
[normalizedId]
);
if (!result.affectedRows) throw serviceError('未找到该 1688 商品', 404, 'PRODUCT_NOT_FOUND');
return { productId: normalizedId, deleted: true };
}
module.exports = {
PRODUCT_TABLE,
ensureAlibaba1688ProductSchema,
normalizeAlibaba1688ImageUrl,
normalizeAlibaba1688Product,
mapProductRow,
listAlibaba1688Products,
getAlibaba1688Product,
createAlibaba1688Product,
replaceAlibaba1688Product,
updateAlibaba1688Product,
deleteAlibaba1688Product,
};