'use strict'; const crypto = require('crypto'); const planningDatabase = require('./planningDatabase'); const { getSelectionFissionAiConfig } = require('./selectionFissionAiSettings'); const { SKU_DIMENSION_MODEL, requestDimensionChunk, } = require('./alibaba1688SkuDimensionAiService'); const CACHE_TABLE = 'planning_selection_1688_size_cache'; const CACHE_TTL_HOURS = 24; const MAX_PRODUCTS = 50; const MAX_SKUS_PER_REQUEST = 30; let schemaReady = false; let schemaPromise = null; function serviceError(message, statusCode = 400, code = 'INVALID_REQUEST') { const error = new Error(message); error.status = statusCode; error.statusCode = statusCode; error.code = code; return error; } function normalizeText(value, maxLength = 1000) { return String(value ?? '').trim().slice(0, maxLength); } function normalizeId(value, label) { const text = normalizeText(value, 120); if (!text || !/^[A-Za-z0-9_-]+$/.test(text)) { throw serviceError(`${label}格式不正确`, 400, 'ALIBABA_1688_SIZE_INVALID_ID'); } return text; } function formatDimension(value) { const number = Number(value); if (!Number.isFinite(number)) return ''; return Number.isInteger(number) ? String(number) : number.toFixed(2).replace(/0+$/, '').replace(/\.$/, ''); } function buildSizeKey(lengthCm, widthCm, heightCm) { return [lengthCm, widthCm, heightCm] .map(Number) .sort((left, right) => right - left) .map((value) => Math.round(value * 10) / 10) .join('x'); } function buildSizeLabel(lengthCm, widthCm, heightCm) { return `${formatDimension(lengthCm)} x ${formatDimension(widthCm)} x ${formatDimension(heightCm)} cm`; } function buildSourceHash(product) { return crypto.createHash('sha256').update(JSON.stringify({ productId: product.productId, skuId: product.skuId, title: product.title, skuTitle: product.skuTitle, industryName: product.industryName, })).digest('hex'); } function normalizeProducts(products) { if (!Array.isArray(products) || products.length === 0) { throw serviceError('没有可识别的1688相似商品', 400, 'ALIBABA_1688_SIZE_PRODUCTS_REQUIRED'); } if (products.length > MAX_PRODUCTS) { throw serviceError(`单次最多识别 ${MAX_PRODUCTS} 个1688相似商品`, 413, 'ALIBABA_1688_SIZE_PRODUCT_LIMIT'); } return products.map((product, index) => { const productId = normalizeId(product?.itemId || product?.productId, '1688商品ID'); const skuId = normalizeText(product?.skuId, 120) || `default-${index + 1}`; const row = { productId, skuId, title: normalizeText(product?.title, 1000), skuTitle: normalizeText(product?.skuTitle, 1000), industryName: normalizeText(product?.industryName, 300), }; return { ...row, sourceHash: buildSourceHash(row) }; }); } async function ensureSchema() { if (schemaReady) return; if (schemaPromise) return schemaPromise; schemaPromise = planningDatabase.execute( `CREATE TABLE IF NOT EXISTS ${CACHE_TABLE} ( id BIGINT PRIMARY KEY AUTO_INCREMENT, plan_id BIGINT NOT NULL, plan_item_id BIGINT NOT NULL, product_id VARCHAR(120) NOT NULL, sku_id VARCHAR(120) NOT NULL, source_hash CHAR(64) NOT NULL, sku_title VARCHAR(1000) NULL, length_cm DECIMAL(12,2) NOT NULL, width_cm DECIMAL(12,2) NOT NULL, height_cm DECIMAL(12,2) NOT NULL, size_key VARCHAR(120) NOT NULL, confidence VARCHAR(20) NOT NULL DEFAULT 'medium', evidence VARCHAR(500) NULL, ai_model VARCHAR(120) NULL, expires_at DATETIME NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_selection_1688_size_cache (plan_item_id, product_id, sku_id, source_hash), KEY idx_selection_1688_size_expiry (expires_at), KEY idx_selection_1688_size_filter (plan_item_id, size_key, expires_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='1688相似款SKU尺寸临时识别缓存'` ).then(() => { schemaReady = true; }).catch((error) => { schemaPromise = null; throw error; }); return schemaPromise; } function mapCacheRow(row) { return { productId: String(row.product_id), skuId: String(row.sku_id), sourceHash: String(row.source_hash), skuTitle: row.sku_title || '', lengthCm: Number(row.length_cm), widthCm: Number(row.width_cm), heightCm: Number(row.height_cm), sizeKey: row.size_key, sizeLabel: buildSizeLabel(row.length_cm, row.width_cm, row.height_cm), confidence: row.confidence || 'medium', evidence: row.evidence || '', model: row.ai_model || null, expiresAt: row.expires_at, }; } async function readCachedRows(planItemId, products) { const productIds = [...new Set(products.map((product) => product.productId))]; const placeholders = productIds.map(() => '?').join(', '); const rows = await planningDatabase.query( `SELECT * FROM ${CACHE_TABLE} WHERE plan_item_id = ? AND product_id IN (${placeholders}) AND expires_at > NOW()`, [planItemId, ...productIds] ); const expected = new Set(products.map((product) => ( `${product.productId}:${product.skuId}:${product.sourceHash}` ))); return rows.map(mapCacheRow).filter((row) => ( expected.has(`${row.productId}:${row.skuId}:${row.sourceHash}`) )); } async function saveRows(planId, planItemId, rows) { for (const row of rows) { await planningDatabase.execute( `INSERT INTO ${CACHE_TABLE} ( plan_id, plan_item_id, product_id, sku_id, source_hash, sku_title, length_cm, width_cm, height_cm, size_key, confidence, evidence, ai_model, expires_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, DATE_ADD(NOW(), INTERVAL ${CACHE_TTL_HOURS} HOUR)) ON DUPLICATE KEY UPDATE sku_title = VALUES(sku_title), length_cm = VALUES(length_cm), width_cm = VALUES(width_cm), height_cm = VALUES(height_cm), size_key = VALUES(size_key), confidence = VALUES(confidence), evidence = VALUES(evidence), ai_model = VALUES(ai_model), expires_at = VALUES(expires_at), updated_at = NOW()`, [ planId, planItemId, row.productId, row.skuId, row.sourceHash, row.skuTitle, row.lengthCm, row.widthCm, row.heightCm, row.sizeKey, row.confidence, row.evidence, row.model, ] ); } } function buildAnalysisRow(product, index) { return { skuRef: `S${index + 1}`, sourceSkuKey: `${product.productId}:${product.skuId}`, sourceIndex: index + 1, skuName: product.skuTitle || product.title, specs: [ { name: '商品标题', value: product.title }, { name: 'SKU规格', value: product.skuTitle || '当前搜索结果默认SKU' }, { name: '行业类目', value: product.industryName }, ].filter((row) => row.value), product, }; } async function analyzeMissingProducts(products, options = {}) { const config = await (options.getConfig || getSelectionFissionAiConfig)(); if (!config.enabled) throw serviceError('选品AI分析模型未启用', 409, 'SELECTION_AI_DISABLED'); if (!config.apiKey) throw serviceError('选品AI分析模型尚未配置API Key', 409, 'SELECTION_AI_API_KEY_MISSING'); const analysisRows = products.map(buildAnalysisRow); const chunks = []; for (let offset = 0; offset < analysisRows.length; offset += MAX_SKUS_PER_REQUEST) { chunks.push(analysisRows.slice(offset, offset + MAX_SKUS_PER_REQUEST)); } const requestChunk = options.requestChunk || requestDimensionChunk; const resolved = []; for (const chunk of chunks) { const result = await requestChunk(config, { purpose: '1688相似款同尺寸筛选比价', products: chunk.map((row) => ({ skuRef: row.skuRef, productId: row.product.productId, title: row.product.title, skuTitle: row.product.skuTitle, industryName: row.product.industryName, })), }, chunk, options); for (const row of result.rows) { const source = chunk.find((candidate) => candidate.skuRef === row.skuRef); resolved.push({ ...source.product, lengthCm: row.lengthCm, widthCm: row.widthCm, heightCm: row.heightCm, sizeKey: buildSizeKey(row.lengthCm, row.widthCm, row.heightCm), sizeLabel: buildSizeLabel(row.lengthCm, row.widthCm, row.heightCm), confidence: row.confidence, evidence: row.evidence, model: SKU_DIMENSION_MODEL, }); } } return resolved; } async function analyzeAlibaba1688SimilarProductSizes(planId, planItemId, input = {}, options = {}) { const normalizedPlanId = Number(planId); const normalizedPlanItemId = Number(planItemId); if (!Number.isInteger(normalizedPlanId) || normalizedPlanId <= 0 || !Number.isInteger(normalizedPlanItemId) || normalizedPlanItemId <= 0) { throw serviceError('选品计划或商品节点ID不正确', 400, 'ALIBABA_1688_SIZE_PLAN_REQUIRED'); } const products = normalizeProducts(input.products); const planItemExists = options.verifyPlanItem ? await options.verifyPlanItem(normalizedPlanId, normalizedPlanItemId) : (await planningDatabase.query( `SELECT id FROM planning_selection_fission_plan_items WHERE id = ? AND plan_id = ? LIMIT 1`, [normalizedPlanItemId, normalizedPlanId] )).length > 0; if (!planItemExists) { throw serviceError('选品计划中的商品节点不存在', 404, 'ALIBABA_1688_SIZE_PLAN_ITEM_NOT_FOUND'); } await (options.ensureSchema || ensureSchema)(); await (options.cleanupExpired || (() => planningDatabase.execute( `DELETE FROM ${CACHE_TABLE} WHERE expires_at <= NOW()` )))(); const cachedRows = await (options.readCachedRows || readCachedRows)(normalizedPlanItemId, products); const cachedKeys = new Set(cachedRows.map((row) => `${row.productId}:${row.skuId}:${row.sourceHash}`)); const missing = products.filter((row) => !cachedKeys.has(`${row.productId}:${row.skuId}:${row.sourceHash}`)); const analyzedRows = missing.length ? await analyzeMissingProducts(missing, options) : []; if (analyzedRows.length) { await (options.saveRows || saveRows)(normalizedPlanId, normalizedPlanItemId, analyzedRows); } const rows = [...cachedRows, ...analyzedRows]; return { rows, cachedCount: cachedRows.length, analyzedCount: analyzedRows.length, expiresInHours: CACHE_TTL_HOURS, model: analyzedRows.length ? SKU_DIMENSION_MODEL : null, }; } module.exports = { CACHE_TTL_HOURS, analyzeAlibaba1688SimilarProductSizes, buildSizeKey, buildSizeLabel, normalizeProducts, _test: { analyzeMissingProducts, buildSourceHash, mapCacheRow, }, };