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