110 lines
2.9 KiB
TypeScript
110 lines
2.9 KiB
TypeScript
|
|
import { prisma } from "@/lib/db";
|
||
|
|
import {
|
||
|
|
attachBrandToTopic,
|
||
|
|
buildMergedProductContext,
|
||
|
|
migrateLegacyProductCatalog,
|
||
|
|
syncTopicProductSnapshot,
|
||
|
|
} from "@/lib/services/product-catalog";
|
||
|
|
import { hasProductContext, parseProductContext } from "@/lib/types/product-context";
|
||
|
|
|
||
|
|
/** 將主題內嵌的 productContext 補寫進 ProductProfile 表(舊資料相容)。 */
|
||
|
|
export async function migrateOrphanProductContexts(accountIds: string[]) {
|
||
|
|
if (accountIds.length === 0) return;
|
||
|
|
await migrateLegacyProductCatalog(accountIds);
|
||
|
|
|
||
|
|
const orphans = await prisma.topic.findMany({
|
||
|
|
where: {
|
||
|
|
accountId: { in: accountIds },
|
||
|
|
topicGoal: "placement",
|
||
|
|
productProfileId: null,
|
||
|
|
productContext: { not: null },
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
for (const topic of orphans) {
|
||
|
|
const context = topic.productContext?.trim();
|
||
|
|
if (!context || !hasProductContext(context)) continue;
|
||
|
|
|
||
|
|
const fields = parseProductContext(context);
|
||
|
|
const label =
|
||
|
|
[fields.brand, fields.product].filter(Boolean).join(" · ") || topic.label;
|
||
|
|
|
||
|
|
const existing = topic.accountId
|
||
|
|
? await prisma.productProfile.findFirst({
|
||
|
|
where: {
|
||
|
|
accountId: topic.accountId,
|
||
|
|
context,
|
||
|
|
},
|
||
|
|
})
|
||
|
|
: null;
|
||
|
|
|
||
|
|
const profile =
|
||
|
|
existing ??
|
||
|
|
(await prisma.productProfile.create({
|
||
|
|
data: {
|
||
|
|
accountId: topic.accountId ?? undefined,
|
||
|
|
label,
|
||
|
|
context,
|
||
|
|
},
|
||
|
|
}));
|
||
|
|
|
||
|
|
await prisma.topic.update({
|
||
|
|
where: { id: topic.id },
|
||
|
|
data: { productProfileId: profile.id },
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function syncProfileContextToTopics(profileId: string, context: string) {
|
||
|
|
await prisma.topic.updateMany({
|
||
|
|
where: { productProfileId: profileId },
|
||
|
|
data: { productContext: context.trim() || null },
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function attachProfileToTopic(topicId: string, profileId: string | null) {
|
||
|
|
if (!profileId) {
|
||
|
|
const topic = await prisma.topic.update({
|
||
|
|
where: { id: topicId },
|
||
|
|
data: { productProfileId: null },
|
||
|
|
});
|
||
|
|
await syncTopicProductSnapshot(topicId);
|
||
|
|
return topic;
|
||
|
|
}
|
||
|
|
|
||
|
|
const profile = await prisma.productProfile.findUnique({
|
||
|
|
where: { id: profileId },
|
||
|
|
include: { brand: true },
|
||
|
|
});
|
||
|
|
if (!profile) throw new Error("找不到產品");
|
||
|
|
|
||
|
|
if (profile.brandId) {
|
||
|
|
return attachBrandToTopic(topicId, profile.brandId, profileId);
|
||
|
|
}
|
||
|
|
|
||
|
|
const context = buildMergedProductContext(
|
||
|
|
profile.brand?.name ?? parseProductContext(profile.context).brand,
|
||
|
|
profile.context,
|
||
|
|
profile.label
|
||
|
|
);
|
||
|
|
|
||
|
|
return prisma.topic.update({
|
||
|
|
where: { id: topicId },
|
||
|
|
data: {
|
||
|
|
productProfileId: profileId,
|
||
|
|
productContext: context,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function attachBrandSelectionToTopic(
|
||
|
|
topicId: string,
|
||
|
|
brandId: string | null,
|
||
|
|
productId: string | null
|
||
|
|
) {
|
||
|
|
return attachBrandToTopic(topicId, brandId, productId);
|
||
|
|
}
|
||
|
|
|
||
|
|
export function validateProfileContext(context: string | null | undefined): boolean {
|
||
|
|
return hasProductContext(context);
|
||
|
|
}
|