178 lines
5.4 KiB
TypeScript
178 lines
5.4 KiB
TypeScript
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<string, unknown>;
|
|
rawResponse: Record<string, unknown>;
|
|
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<string, unknown>) {
|
|
const sellerInfo = rawItem.seller_info;
|
|
return sellerInfo && typeof sellerInfo === "object" && !Array.isArray(sellerInfo)
|
|
? (sellerInfo as Record<string, unknown>)
|
|
: {};
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|