haixunMaster/lib/services/product-catalog.ts

162 lines
4.6 KiB
TypeScript

import "server-only";
import { prisma } from "@/lib/db";
import { parseProductContext } from "@/lib/types/product-context";
import { parseMatchTags } from "@/lib/types/product-match";
import {
buildMergedProductContext,
formatCatalogProductSummary,
recommendProductFromCatalog,
type CatalogBrand,
type CatalogProduct,
} from "@/lib/types/product-catalog";
export type { CatalogBrand, CatalogProduct };
export { buildMergedProductContext, formatCatalogProductSummary, recommendProductFromCatalog };
export async function loadBrandCatalog(brandId: string): Promise<CatalogBrand | null> {
const brand = await prisma.brandProfile.findUnique({
where: { id: brandId },
include: { products: { orderBy: { updatedAt: "desc" } } },
});
if (!brand) return null;
return {
id: brand.id,
name: brand.name,
notes: brand.notes,
products: brand.products.map((p) => ({
id: p.id,
label: p.label,
context: p.context,
matchTags: parseMatchTags(p.matchTags),
})),
};
}
export async function resolveProductContextForPlacement(input: {
brandProfileId?: string | null;
productProfileId?: string | null;
fallbackContext?: string | null;
searchTag?: string | null;
}): Promise<string | null> {
if (input.brandProfileId) {
const catalog = await loadBrandCatalog(input.brandProfileId);
if (catalog && catalog.products.length > 0) {
const picked = recommendProductFromCatalog(catalog.products, {
searchTag: input.searchTag,
defaultProductId: input.productProfileId,
});
if (picked) {
return buildMergedProductContext(catalog.name, picked.context, picked.label);
}
}
}
if (input.productProfileId) {
const product = await prisma.productProfile.findUnique({
where: { id: input.productProfileId },
include: { brand: true },
});
if (product) {
const brandName = product.brand?.name ?? parseProductContext(product.context).brand;
return buildMergedProductContext(brandName, product.context, product.label);
}
}
return input.fallbackContext?.trim() || null;
}
export async function syncTopicProductSnapshot(topicId: string) {
const topic = await prisma.topic.findUnique({ where: { id: topicId } });
if (!topic) return;
const context = await resolveProductContextForPlacement({
brandProfileId: topic.brandProfileId,
productProfileId: topic.productProfileId,
fallbackContext: topic.productContext,
});
await prisma.topic.update({
where: { id: topicId },
data: { productContext: context },
});
}
export async function attachBrandToTopic(
topicId: string,
brandId: string | null,
productId: string | null
) {
if (!brandId) {
return prisma.topic.update({
where: { id: topicId },
data: { brandProfileId: null, productProfileId: productId },
});
}
const brand = await prisma.brandProfile.findUnique({
where: { id: brandId },
include: { products: true },
});
if (!brand) throw new Error("找不到品牌");
if (productId) {
const product = brand.products.find((p) => p.id === productId);
if (!product) throw new Error("此品牌下找不到該產品");
}
const context = await resolveProductContextForPlacement({
brandProfileId: brandId,
productProfileId: productId,
});
return prisma.topic.update({
where: { id: topicId },
data: {
brandProfileId: brandId,
productProfileId: productId,
productContext: context,
},
});
}
/** 將舊版扁平 ProductProfile 整理成品牌 + 產品結構。 */
export async function migrateLegacyProductCatalog(accountIds: string[]) {
if (accountIds.length === 0) return;
const orphans = await prisma.productProfile.findMany({
where: { accountId: { in: accountIds }, brandId: null },
});
for (const row of orphans) {
const fields = parseProductContext(row.context);
const brandName = fields.brand?.trim() || row.label.split("·")[0]?.trim() || row.label;
let brand = await prisma.brandProfile.findFirst({
where: { accountId: row.accountId, name: brandName },
});
if (!brand) {
brand = await prisma.brandProfile.create({
data: {
accountId: row.accountId ?? undefined,
name: brandName,
},
});
}
await prisma.productProfile.update({
where: { id: row.id },
data: {
brandId: brand.id,
label: fields.product?.trim() || row.label,
matchTags: row.matchTags,
},
});
await prisma.topic.updateMany({
where: { productProfileId: row.id, brandProfileId: null },
data: { brandProfileId: brand.id },
});
}
}