'use strict'; const { PRODUCT_TABLE, getAlibaba1688Product, } = require('./alibaba1688ProductService'); const planningDatabase = require('./planningDatabase'); const { buildAlibaba1688SkuKey, buildAlibaba1688SkuFingerprint, } = require('./alibaba1688SkuIdentity'); const productCodeClient = require('./alibaba1688ProductCodeClient'); const encodingRegistry = require('./alibaba1688ProductEncodingRegistry'); const MAX_CODING_SKUS = 200; const MAX_PRODUCT_NAME_LENGTH = 255; const MAX_COLOR_SPEC_LENGTH = 255; let metaCache = null; function encodingError(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 requiredText(value, label, maxLength = 255) { const raw = String(value ?? '').trim(); if (!raw) throw encodingError(`${label}不能为空`, 400, 'PRODUCT_CODE_FIELD_REQUIRED'); if (raw.length > maxLength) { throw encodingError(`${label}不能超过 ${maxLength} 个字符`, 400, 'PRODUCT_CODE_FIELD_TOO_LONG'); } return raw; } function optionalNumber(value, label, { allowZero = false } = {}) { if (value === null || value === undefined || value === '') return null; const number = Number(value); if (!Number.isFinite(number) || (allowZero ? number < 0 : number <= 0)) { throw encodingError(`${label}${allowZero ? '不能小于 0' : '必须大于 0'}`, 400, 'PRODUCT_CODE_NUMBER_INVALID'); } return number; } function sourceSkuKey(sku, position) { return buildAlibaba1688SkuKey(sku, position); } function normalizeCodingRequest(input = {}, productRecord, actor = {}) { const sourceSkus = Array.isArray(productRecord?.product?.skus) ? productRecord.product.skus : []; if (!sourceSkus.length) { throw encodingError('该 1688 商品没有可编码的 SKU', 422, 'PRODUCT_CODE_SKUS_REQUIRED'); } if (sourceSkus.length > MAX_CODING_SKUS) { throw encodingError( `该商品共有 ${sourceSkus.length} 个 SKU,编码接口单次最多支持 ${MAX_CODING_SKUS} 个;请先确认最终上架规格`, 422, 'PRODUCT_CODE_SKU_LIMIT_EXCEEDED' ); } const inputSkus = Array.isArray(input.skuItems) ? input.skuItems : []; if (inputSkus.length !== sourceSkus.length) { throw encodingError( `必须一次提交该商品全部 ${sourceSkus.length} 个 SKU,当前提交了 ${inputSkus.length} 个`, 422, 'PRODUCT_CODE_SKU_COUNT_MISMATCH' ); } const selections = { operationCenter: requiredText(input.operationCenter, '运营中心', 120), categoryName: requiredText(input.categoryName, '编码分类', 255), taxStatus: requiredText(input.taxStatus, '是否含税', 80), invoiceType: requiredText(input.invoiceType, '开票类型', 80), installationService: requiredText(input.installationService, '安装服务', 120), installationMode: requiredText(input.installationMode, '安装方式', 80), }; const normalizedSkus = sourceSkus.map((sourceSku, index) => { const raw = inputSkus[index] || {}; const expectedSourceSkuKey = sourceSkuKey(sourceSku, index); if (requiredText(raw.sourceSkuKey, `第 ${index + 1} 个 SKU 的来源标识`, 4000) !== expectedSourceSkuKey) { throw encodingError( `第 ${index + 1} 个 SKU 已发生变化,请关闭编码窗口后重新加载`, 409, 'PRODUCT_CODE_SKU_SNAPSHOT_CHANGED' ); } const productName = requiredText(raw.productName, `第 ${index + 1} 个 SKU 的商品名称`, MAX_PRODUCT_NAME_LENGTH); const colorSpecRaw = String(raw.colorSpec ?? '').trim(); const colorNameRaw = String(raw.colorName ?? '').trim(); if (!colorSpecRaw && !colorNameRaw) { throw encodingError(`第 ${index + 1} 个 SKU 缺少颜色及规格`, 400, 'PRODUCT_CODE_SKU_SPEC_REQUIRED'); } if (colorSpecRaw.length > MAX_COLOR_SPEC_LENGTH || colorNameRaw.length > MAX_COLOR_SPEC_LENGTH) { throw encodingError( `第 ${index + 1} 个 SKU 的颜色及规格不能超过 ${MAX_COLOR_SPEC_LENGTH} 个字符`, 400, 'PRODUCT_CODE_FIELD_TOO_LONG' ); } const lengthCm = optionalNumber(raw.lengthCm, `第 ${index + 1} 个 SKU 的长度`); const widthCm = optionalNumber(raw.widthCm, `第 ${index + 1} 个 SKU 的宽度`); const heightCm = optionalNumber(raw.heightCm, `第 ${index + 1} 个 SKU 的高度`); if (lengthCm == null || widthCm == null || heightCm == null) { throw encodingError( `第 ${index + 1} 个 SKU 的长宽高必须填写完整`, 400, 'PRODUCT_CODE_SKU_DIMENSIONS_REQUIRED' ); } const weight = optionalNumber(raw.weight, `第 ${index + 1} 个 SKU 的重量`, { allowZero: true }); return { sourceSku, sourceSkuKey: expectedSourceSkuKey, requestItem: { productName, ...(colorSpecRaw ? { colorSpec: colorSpecRaw } : { colorName: colorNameRaw }), ...(lengthCm == null ? {} : { lengthCm }), ...(widthCm == null ? {} : { widthCm }), ...(heightCm == null ? {} : { heightCm }), ...(weight == null ? {} : { weight }), }, }; }); const operatorName = normalizeText(actor.name || actor.username, 100) || 'maixiang'; const requestPayload = { ...selections, ali1688ProductId: requiredText(productRecord.productId, '1688 商品 ID', 100), ali1688SupplierName: normalizeText(productRecord.storeName, 255), sourceSystem: 'maixiang', operatorName, operatorDepartment: normalizeText(actor.departmentName, 100), skuItems: normalizedSkus.map(item => item.requestItem), }; return { requestPayload, sourceSkus: normalizedSkus, sourceSkuFingerprint: buildAlibaba1688SkuFingerprint(sourceSkus), }; } function resolveUpstreamSkuOrder(upstreamData, responseItems, normalizedRequest) { const fallbackCodes = Array.isArray(upstreamData.productCodes) ? upstreamData.productCodes : []; const itemsWithCodes = responseItems.map((item, index) => ({ ...item, productCode: normalizeText(item?.productCode || fallbackCodes[index], 200), })); if (upstreamData.created !== false) { const productCodes = (fallbackCodes.length ? fallbackCodes : itemsWithCodes.map(item => item.productCode)) .map(value => normalizeText(value, 200)) .filter(Boolean); return { orderedItems: itemsWithCodes, productCodes }; } if (itemsWithCodes.length !== normalizedRequest.sourceSkus.length) { throw encodingError( '该 1688 商品已在编码服务中存在,但历史 SKU 明细不足以安全匹配当前规格', 409, 'PRODUCT_CODE_REUSED_MAPPING_CONFLICT' ); } const usedIndexes = new Set(); const orderedItems = normalizedRequest.sourceSkus.map((entry) => { const requestName = normalizeText(entry.requestItem.productName, MAX_PRODUCT_NAME_LENGTH); const requestSpec = normalizeText(entry.requestItem.colorSpec || entry.requestItem.colorName, MAX_COLOR_SPEC_LENGTH); const candidateIndexes = itemsWithCodes .map((item, index) => ({ item, index })) .filter(({ item, index }) => { if (usedIndexes.has(index) || !item.productCode) return false; if (normalizeText(item.productName, MAX_PRODUCT_NAME_LENGTH) !== requestName) return false; const responseSpec = normalizeText(item.colorSpec, MAX_COLOR_SPEC_LENGTH); return !responseSpec || responseSpec === requestSpec; }) .map(({ index }) => index); if (candidateIndexes.length !== 1) { throw encodingError( `已有编码无法唯一匹配 SKU“${requestName}”,已停止后续上架`, 409, 'PRODUCT_CODE_REUSED_MAPPING_CONFLICT' ); } usedIndexes.add(candidateIndexes[0]); return itemsWithCodes[candidateIndexes[0]]; }); return { orderedItems, productCodes: orderedItems.map(item => item.productCode), }; } function normalizeUpstreamResult(upstreamData = {}, normalizedRequest) { const responseItems = Array.isArray(upstreamData.items) ? upstreamData.items : []; const { orderedItems, productCodes } = resolveUpstreamSkuOrder( upstreamData, responseItems, normalizedRequest ); if (productCodes.length !== normalizedRequest.sourceSkus.length) { throw encodingError( `编码服务返回 ${productCodes.length} 个商品编码,但当前商品有 ${normalizedRequest.sourceSkus.length} 个 SKU;已停止后续上架,请核对该 1688 ID 的历史编码`, 409, 'PRODUCT_CODE_RESPONSE_COUNT_MISMATCH' ); } if (new Set(productCodes).size !== productCodes.length) { throw encodingError( '编码服务返回了重复商品编码,已停止后续上架', 409, 'PRODUCT_CODE_RESPONSE_DUPLICATE' ); } const mismatchedProductId = responseItems .map(item => normalizeText(item?.ali1688ProductId, 100)) .filter(Boolean) .find(value => value !== normalizedRequest.requestPayload.ali1688ProductId); if (mismatchedProductId) { throw encodingError( '编码服务返回的 1688 商品 ID 与当前商品不一致,已停止后续上架', 409, 'PRODUCT_CODE_RESPONSE_PRODUCT_MISMATCH' ); } const styleCodes = [...new Set([ ...(Array.isArray(upstreamData.styleCodes) ? upstreamData.styleCodes : []), upstreamData.styleCode, ...orderedItems.map(item => item?.styleCode), ].map(value => normalizeText(value, 200)).filter(Boolean))]; const skuMappings = normalizedRequest.sourceSkus.map((entry, index) => ({ sourceSkuKey: entry.sourceSkuKey, sourceSkuIndex: Number(entry.sourceSku?.index) || index + 1, sourceSkuName: normalizeText(entry.sourceSku?.name, 1200), sourceSpecs: Array.isArray(entry.sourceSku?.specs) ? entry.sourceSku.specs : [], productName: entry.requestItem.productName, colorSpec: entry.requestItem.colorSpec || entry.requestItem.colorName || '', lengthCm: entry.requestItem.lengthCm ?? null, widthCm: entry.requestItem.widthCm ?? null, heightCm: entry.requestItem.heightCm ?? null, weight: entry.requestItem.weight ?? null, styleCode: normalizeText(orderedItems[index]?.styleCode || upstreamData.styleCode || styleCodes[0], 200), productCode: productCodes[index], })); return { styleCode: normalizeText(upstreamData.styleCode || styleCodes[0], 160), styleCodes, productCodes, skuMappings, sourceSkuCount: normalizedRequest.sourceSkus.length, sourceSkuFingerprint: normalizedRequest.sourceSkuFingerprint, upstreamCreated: upstreamData.created !== false, }; } async function getAlibaba1688ProductCodeMeta(options = {}) { const now = Date.now(); if (!options.force && metaCache && metaCache.expiresAt > now) return metaCache.value; const response = await (options.getMeta || productCodeClient.getProductCodeMeta)(options); metaCache = { value: response.data, expiresAt: now + 5 * 60 * 1000, }; return response.data; } async function encodeAlibaba1688Product(productId, input = {}, actor = {}, options = {}) { const productRecord = await (options.getProduct || getAlibaba1688Product)(productId); const shouldLookupExisting = Boolean(options.getExistingEncoding) || (!options.getProduct && !options.claim); if (shouldLookupExisting) { const existing = await ( options.getExistingEncoding || encodingRegistry.getEncodingByProductId )(productId); if (existing?.status === 'encoded') { return { alreadyEncoded: true, encoding: existing, }; } } const normalized = normalizeCodingRequest(input, productRecord, actor); if (!options.getProduct || options.persistProductFingerprint) { await (options.persistProductFingerprint || planningDatabase.execute)( `UPDATE ${PRODUCT_TABLE} SET sku_count = ?, sku_fingerprint = ? WHERE product_id = ? AND sync_version = ? AND (sku_fingerprint IS NULL OR sku_fingerprint = '' OR sku_fingerprint = ?)`, [ normalized.sourceSkus.length, normalized.sourceSkuFingerprint, productRecord.productId, Number(productRecord.syncVersion) || 1, normalized.sourceSkuFingerprint, ] ); } const preparedProduct = await (options.getPreparedProduct || options.getProduct || getAlibaba1688Product)( productRecord.productId ); const preparedFingerprint = preparedProduct.skuFingerprint || buildAlibaba1688SkuFingerprint(preparedProduct.product?.skus); if (preparedFingerprint !== normalized.sourceSkuFingerprint) { throw encodingError( '商品在编码确认前发生了 SKU 变化,请关闭编码窗口后重新加载', 409, 'PRODUCT_CODE_SKU_SNAPSHOT_CHANGED' ); } const claim = await (options.claim || encodingRegistry.claimEncoding)( productRecord.productId, normalized.requestPayload, actor, { sourceSkuCount: normalized.sourceSkus.length, sourceSkuFingerprint: normalized.sourceSkuFingerprint, } ); if (!claim.claimed) { return { alreadyEncoded: true, encoding: claim.record, }; } let upstreamResponse = null; try { const outboundPayload = claim.requestPayload || normalized.requestPayload; upstreamResponse = await (options.createCodes || productCodeClient.createProductCodes)(outboundPayload, options); const result = normalizeUpstreamResult(upstreamResponse.data, normalized); const latestProduct = await (options.getLatestProduct || options.getProduct || getAlibaba1688Product)( productRecord.productId ); const latestFingerprint = latestProduct.skuFingerprint || buildAlibaba1688SkuFingerprint(latestProduct.product?.skus); if (latestFingerprint !== normalized.sourceSkuFingerprint) { throw encodingError( '商品在编码请求执行期间发生了 SKU 变化;远端编码已保留,但已禁止自动匹配和上架', 409, 'PRODUCT_CODE_SKU_CHANGED_DURING_ENCODING' ); } const encoding = await (options.complete || encodingRegistry.completeEncoding)( productRecord.productId, claim.token, { ...result, response: upstreamResponse.data, } ); return { alreadyEncoded: false, encoding, }; } catch (error) { try { const conflictCodes = new Set([ 'PRODUCT_CODE_RESPONSE_COUNT_MISMATCH', 'PRODUCT_CODE_RESPONSE_DUPLICATE', 'PRODUCT_CODE_RESPONSE_PRODUCT_MISMATCH', 'PRODUCT_CODE_REUSED_MAPPING_CONFLICT', 'PRODUCT_CODE_SKU_CHANGED_DURING_ENCODING', ]); const saveFailure = conflictCodes.has(error.code) ? (options.conflict || encodingRegistry.conflictEncoding) : (options.fail || encodingRegistry.failEncoding); await saveFailure( productRecord.productId, claim.token, error, upstreamResponse?.data || null ); } catch (saveError) { console.error('保存1688商品编码失败状态失败:', saveError.message); } throw error; } } async function assertAlibaba1688PlanItemsEncoded(items, options = {}) { const rows = Array.isArray(items) ? items : []; const hydratedRows = []; for (const row of rows) { const sourceRow = row?.sourceRow && typeof row.sourceRow === 'object' ? row.sourceRow : {}; const declaredAlibaba1688 = normalizeText(row?.channelName, 40).toLowerCase() === '1688' || normalizeText(sourceRow.sourceChannel, 40).toLowerCase() === '1688' || normalizeText(sourceRow.source, 80) === 'alibaba_1688_products'; if (!declaredAlibaba1688) { hydratedRows.push(row); continue; } const productId = normalizeText(row?.marketItemId || row?.sourceRow?.productId, 80); if (!productId || !/^[A-Za-z0-9_-]+$/.test(productId)) { throw encodingError('1688 选品缺少有效商品 ID', 400, 'ALIBABA_1688_PRODUCT_ID_INVALID'); } const product = await (options.getProduct || getAlibaba1688Product)(productId, { required: false }); if (!product) { throw encodingError( `未找到 1688 商品 ${productId},不能进入选品流程`, 404, 'ALIBABA_1688_PRODUCT_NOT_FOUND' ); } const encoding = await (options.getEncoding || encodingRegistry.getEncodingByProductId)(productId); if (!encoding || encoding.status !== 'encoded') { throw encodingError( `1688 商品 ${productId} 尚未完成编码,不能进入选品流程`, 409, 'ALIBABA_1688_PRODUCT_NOT_ENCODED' ); } const sourceSkus = Array.isArray(product.product?.skus) ? product.product.skus : []; const currentSkuCount = sourceSkus.length; const calculatedFingerprint = currentSkuCount ? buildAlibaba1688SkuFingerprint(sourceSkus) : ''; const currentFingerprint = product.skuFingerprint || calculatedFingerprint; const expectedSourceSkuKeys = sourceSkus.map((sku, index) => sourceSkuKey(sku, index)); const productCodes = Array.isArray(encoding.productCodes) ? encoding.productCodes : []; const skuMappings = Array.isArray(encoding.skuMappings) ? encoding.skuMappings : []; const mappingSourceSkuKeys = skuMappings.map(mapping => normalizeText(mapping?.sourceSkuKey, 4000)); const mappingProductCodes = skuMappings.map(mapping => normalizeText(mapping?.productCode, 200)); const expectedSourceSkuKeySet = new Set(expectedSourceSkuKeys); const productCodeSet = new Set(productCodes); const encodingIsComplete = currentSkuCount > 0 && Number(product.skuCount) === currentSkuCount && (!product.skuFingerprint || product.skuFingerprint === calculatedFingerprint) && Number(encoding.sourceSkuCount) === currentSkuCount && encoding.sourceSkuFingerprint === currentFingerprint && productCodes.length === currentSkuCount && new Set(productCodes).size === currentSkuCount && productCodes.every(Boolean) && skuMappings.length === currentSkuCount && new Set(mappingSourceSkuKeys).size === currentSkuCount && new Set(mappingProductCodes).size === currentSkuCount && mappingSourceSkuKeys.every(key => expectedSourceSkuKeySet.has(key)) && mappingProductCodes.every(code => productCodeSet.has(code)); if (!encodingIsComplete) { throw encodingError( `1688 商品 ${productId} 的 SKU 已在编码后发生变化,请先处理编码映射冲突`, 409, 'ALIBABA_1688_PRODUCT_ENCODING_STALE' ); } hydratedRows.push({ ...row, channelName: '1688', sourceRow: { ...(row.sourceRow && typeof row.sourceRow === 'object' ? row.sourceRow : {}), ...product, encoding: { status: encoding.status, styleCode: encoding.styleCode, styleCodes: encoding.styleCodes, productCodes: encoding.productCodes, skuMappings: encoding.skuMappings, sourceSkuCount: encoding.sourceSkuCount, sourceSkuFingerprint: encoding.sourceSkuFingerprint, encodedAt: encoding.encodedAt, }, source: 'alibaba_1688_products', sourceChannel: '1688', }, }); } return hydratedRows; } module.exports = { MAX_CODING_SKUS, MAX_PRODUCT_NAME_LENGTH, MAX_COLOR_SPEC_LENGTH, sourceSkuKey, normalizeCodingRequest, resolveUpstreamSkuOrder, normalizeUpstreamResult, getAlibaba1688ProductCodeMeta, encodeAlibaba1688Product, assertAlibaba1688PlanItemsEncoded, };