import "server-only"; import mysql from "mysql2/promise"; import type { ProductPlanningApiConfig } from "./config"; import type { ProductPlanningTaobaoItem } from "./types"; type SyncLegacyDetailParams = { config: ProductPlanningApiConfig; requestedItemId: string; rawItem: Record; rawResponse: Record; normalizedItem: ProductPlanningTaobaoItem; }; function hasPlanningDbConfig(config: ProductPlanningApiConfig) { return Boolean( config.planningDbHost && config.planningDbPort && config.planningDbDatabase && config.planningDbUser && config.planningDbPassword ); } function text(value: unknown, maxLength?: number) { const normalized = String(value || "").trim(); if (!maxLength || normalized.length <= maxLength) return normalized || null; return normalized.slice(0, maxLength); } function numberOrNull(value: unknown) { const normalized = Number(value); return Number.isFinite(normalized) ? normalized : null; } function json(value: unknown) { try { return JSON.stringify(value ?? {}); } catch { return "{}"; } } function getSellerInfo(rawItem: Record) { const sellerInfo = rawItem.seller_info; return sellerInfo && typeof sellerInfo === "object" && !Array.isArray(sellerInfo) ? (sellerInfo as Record) : {}; } export async function syncLegacyTaobaoDetailToPlanningDb({ config, requestedItemId, rawItem, rawResponse, normalizedItem, }: SyncLegacyDetailParams) { if (!hasPlanningDbConfig(config)) { return { ok: false, skipped: true, reason: "planning db config missing" }; } const sellerInfo = getSellerInfo(rawItem); const itemImagesJson = normalizedItem.itemImages.map((url) => ({ url })); const propsJson = normalizedItem.properties.map((entry) => ({ name: entry.name, value: entry.value, })); const mainItemInfo = { itemId: normalizedItem.itemId, title: normalizedItem.title, price: normalizedItem.price, originalPrice: normalizedItem.originalPrice, mainImage: normalizedItem.mainImage, detailUrl: normalizedItem.detailUrl, shopName: normalizedItem.shopName, }; const connection = await mysql.createConnection({ host: config.planningDbHost, port: Number(config.planningDbPort) || 3306, user: config.planningDbUser, password: config.planningDbPassword, database: config.planningDbDatabase, charset: config.planningDbCharset || "utf8mb4", connectTimeout: 3000, ssl: config.planningDbSsl ? { rejectUnauthorized: false } : undefined, }); try { await connection.execute( `INSERT INTO taobao_detail_plus_data ( requested_item_id, item_id, api_name, api_code, api_msg, api_version, source_url, title, nick, num, price, category_id, location, shop_id, shop_name, new_item_count, all_item_count, total_count, user_id, seller_type, video_url, detail_url, desc_html, left_nums, item_imgs_count, skus_count, attributes_count, props_count, item_imgs_json, skus_json, attribute_json, specs_json, props_json, main_item_info_json, raw_data_json, raw_response_json, fetched_at ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(3) )`, [ text(requestedItemId, 64), text(normalizedItem.itemId, 64), text(rawResponse.api_name || rawResponse.name, 128), text(rawResponse.error_code || rawResponse.code || "0000", 32), text(rawResponse.reason || rawResponse.error || rawResponse.msg || "legacy fallback synced", 255), text(rawResponse.api_version || rawResponse.version, 32), text(normalizedItem.sourceUrl || normalizedItem.detailUrl), text(normalizedItem.title, 1024), text(rawItem.nick || sellerInfo.shop_name || normalizedItem.shopName, 255), text(rawItem.num, 64), text(normalizedItem.price ?? rawItem.price, 128), text(normalizedItem.categoryId || rawItem.cid || rawItem.category_id, 64), text(normalizedItem.location || rawItem.location, 255), text(normalizedItem.shopId || sellerInfo.shop_id || rawItem.shop_id, 64), text(normalizedItem.shopName || sellerInfo.shop_name || rawItem.shop_name, 255), text(rawItem.new_item_count, 64), text(rawItem.all_item_count, 64), text(rawItem.total_count, 64), text(rawItem.user_id || sellerInfo.user_id, 64), text(rawItem.seller_type || (normalizedItem.isTmall ? "tmall" : "taobao"), 32), text(normalizedItem.videoUrl), text(normalizedItem.detailUrl), text(normalizedItem.descriptionHtml), numberOrNull(rawItem.left_nums), normalizedItem.itemImages.length, normalizedItem.skus.length, normalizedItem.properties.length, normalizedItem.properties.length, json(itemImagesJson), json(normalizedItem.skus), json(propsJson), json({}), json(propsJson), json(mainItemInfo), json(rawItem), json(rawResponse), ] ); return { ok: true, skipped: false }; } finally { await connection.end().catch(() => undefined); } }