Your Name 987ab3f6a0
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
feat: support batch competitor planning lookup
2026-04-30 17:35:25 +08:00

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);
}
}