chore: initialize WanderQ MiniAPP 2.0 repository
This commit is contained in:
1161
apps/api/prisma/content-release.ts
Normal file
1161
apps/api/prisma/content-release.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,19 @@
|
||||
CREATE TABLE "HomeModule" (
|
||||
"id" TEXT NOT NULL,
|
||||
"label" TEXT NOT NULL,
|
||||
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "HomeModule_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
INSERT INTO "HomeModule" ("id", "label", "sortOrder", "isActive", "createdAt", "updatedAt")
|
||||
VALUES
|
||||
('explore', '探索贵州', 0, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
('themes', '主题甄选', 1, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
('deals', '特价优惠', 2, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
('routes', '精选线路', 3, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
('hotels', '特色酒店', 4, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
|
||||
('vehicles', '万趣用车', 5, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE "Product"
|
||||
ADD COLUMN "keyFacts" JSONB,
|
||||
ADD COLUMN "contentBlocks" JSONB;
|
||||
@@ -0,0 +1,6 @@
|
||||
ALTER TABLE "HomeModule"
|
||||
ADD COLUMN "templateType" TEXT NOT NULL DEFAULT 'explore',
|
||||
ADD COLUMN "content" JSONB,
|
||||
ADD COLUMN "publishedConfig" JSONB,
|
||||
ADD COLUMN "isSystem" BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN "isDeleted" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -0,0 +1,8 @@
|
||||
CREATE TABLE "DestinationPageConfig" (
|
||||
"id" TEXT NOT NULL,
|
||||
"recommendedProductIds" JSONB NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "DestinationPageConfig_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
@@ -0,0 +1,12 @@
|
||||
CREATE TABLE "SearchPageConfig" (
|
||||
"id" TEXT NOT NULL DEFAULT 'search-page',
|
||||
"title" TEXT NOT NULL,
|
||||
"subtitle" TEXT NOT NULL,
|
||||
"placeholder" TEXT NOT NULL,
|
||||
"popularKeywords" JSONB NOT NULL,
|
||||
"groups" JSONB NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "SearchPageConfig_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
ALTER TABLE "Product"
|
||||
ADD COLUMN "volumeKey" TEXT,
|
||||
ADD COLUMN "durationDays" INTEGER,
|
||||
ADD COLUMN "durationNights" INTEGER,
|
||||
ADD COLUMN "departureDates" TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
|
||||
ADD COLUMN "remainingSpots" INTEGER,
|
||||
ADD COLUMN "recommendation" TEXT;
|
||||
|
||||
ALTER TABLE "Lead"
|
||||
ADD COLUMN "contactName" TEXT,
|
||||
ADD COLUMN "wechat" TEXT,
|
||||
ADD COLUMN "adultCount" INTEGER,
|
||||
ADD COLUMN "childCount" INTEGER,
|
||||
ADD COLUMN "roomType" TEXT,
|
||||
ADD COLUMN "plan" TEXT,
|
||||
ADD COLUMN "addOns" TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
|
||||
ADD COLUMN "sourceTheme" TEXT;
|
||||
@@ -0,0 +1,15 @@
|
||||
ALTER TABLE "SearchPageConfig"
|
||||
ADD COLUMN "modules" JSONB NOT NULL DEFAULT '{}';
|
||||
|
||||
UPDATE "SearchPageConfig"
|
||||
SET "modules" = jsonb_build_object(
|
||||
'seasonalInspiration', jsonb_build_object(
|
||||
'title', "title",
|
||||
'subtitle', "subtitle"
|
||||
),
|
||||
'preferenceDiscovery', jsonb_build_object(
|
||||
'title', '换一种方式找灵感',
|
||||
'subtitle', '按区域、主题和体验偏好,找到下一处想去的地方。'
|
||||
)
|
||||
)
|
||||
WHERE "modules" = '{}'::jsonb;
|
||||
@@ -0,0 +1,76 @@
|
||||
ALTER TABLE "Customer"
|
||||
ALTER COLUMN "phone" DROP NOT NULL;
|
||||
|
||||
CREATE TABLE "MiniProgramUser" (
|
||||
"id" TEXT NOT NULL,
|
||||
"appId" TEXT NOT NULL,
|
||||
"openId" TEXT NOT NULL,
|
||||
"unionId" TEXT,
|
||||
"nickname" TEXT,
|
||||
"avatarUrl" TEXT,
|
||||
"phone" TEXT,
|
||||
"phoneAuthorizedAt" TIMESTAMP(3),
|
||||
"note" TEXT,
|
||||
"source" TEXT NOT NULL DEFAULT 'mini_program',
|
||||
"status" TEXT NOT NULL DEFAULT 'active',
|
||||
"customerId" TEXT,
|
||||
"firstSeenAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"lastLoginAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "MiniProgramUser_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE "MiniProgramFavorite" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"productId" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "MiniProgramFavorite_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE "MiniProgramHistory" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"productId" TEXT NOT NULL,
|
||||
"viewCount" INTEGER NOT NULL DEFAULT 1,
|
||||
"lastViewedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "MiniProgramHistory_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
ALTER TABLE "Lead"
|
||||
ADD COLUMN "userId" TEXT;
|
||||
|
||||
CREATE UNIQUE INDEX "MiniProgramUser_appId_openId_key" ON "MiniProgramUser"("appId", "openId");
|
||||
CREATE UNIQUE INDEX "MiniProgramUser_customerId_key" ON "MiniProgramUser"("customerId");
|
||||
CREATE INDEX "MiniProgramUser_phone_idx" ON "MiniProgramUser"("phone");
|
||||
CREATE INDEX "MiniProgramUser_status_lastLoginAt_idx" ON "MiniProgramUser"("status", "lastLoginAt");
|
||||
CREATE UNIQUE INDEX "MiniProgramFavorite_userId_productId_key" ON "MiniProgramFavorite"("userId", "productId");
|
||||
CREATE INDEX "MiniProgramFavorite_productId_idx" ON "MiniProgramFavorite"("productId");
|
||||
CREATE UNIQUE INDEX "MiniProgramHistory_userId_productId_key" ON "MiniProgramHistory"("userId", "productId");
|
||||
CREATE INDEX "MiniProgramHistory_userId_lastViewedAt_idx" ON "MiniProgramHistory"("userId", "lastViewedAt");
|
||||
CREATE INDEX "Lead_userId_createdAt_idx" ON "Lead"("userId", "createdAt");
|
||||
CREATE INDEX "Lead_phone_idx" ON "Lead"("phone");
|
||||
|
||||
ALTER TABLE "MiniProgramUser"
|
||||
ADD CONSTRAINT "MiniProgramUser_customerId_fkey"
|
||||
FOREIGN KEY ("customerId") REFERENCES "Customer"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "MiniProgramFavorite"
|
||||
ADD CONSTRAINT "MiniProgramFavorite_userId_fkey"
|
||||
FOREIGN KEY ("userId") REFERENCES "MiniProgramUser"("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
ADD CONSTRAINT "MiniProgramFavorite_productId_fkey"
|
||||
FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "MiniProgramHistory"
|
||||
ADD CONSTRAINT "MiniProgramHistory_userId_fkey"
|
||||
FOREIGN KEY ("userId") REFERENCES "MiniProgramUser"("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
ADD CONSTRAINT "MiniProgramHistory_productId_fkey"
|
||||
FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "Lead"
|
||||
ADD CONSTRAINT "Lead_userId_fkey"
|
||||
FOREIGN KEY ("userId") REFERENCES "MiniProgramUser"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE "Product" ADD COLUMN "pricingTiers" JSONB;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE "Product"
|
||||
DROP COLUMN "volumeKey";
|
||||
@@ -0,0 +1,12 @@
|
||||
UPDATE "HomeModule"
|
||||
SET
|
||||
"isActive" = false,
|
||||
"isDeleted" = true,
|
||||
"publishedConfig" = CASE
|
||||
WHEN "publishedConfig" IS NULL THEN NULL
|
||||
ELSE jsonb_set(
|
||||
jsonb_set("publishedConfig", '{isActive}', 'false'::jsonb, true),
|
||||
'{isDeleted}', 'true'::jsonb, true
|
||||
)
|
||||
END
|
||||
WHERE "id" = 'routes';
|
||||
@@ -0,0 +1,67 @@
|
||||
ALTER TABLE "Lead"
|
||||
ADD COLUMN "wecomContactId" TEXT;
|
||||
|
||||
CREATE TABLE "WecomContactWay" (
|
||||
"id" TEXT NOT NULL,
|
||||
"configId" TEXT NOT NULL,
|
||||
"type" INTEGER NOT NULL DEFAULT 1,
|
||||
"scene" INTEGER NOT NULL DEFAULT 1,
|
||||
"style" INTEGER NOT NULL DEFAULT 1,
|
||||
"remark" TEXT,
|
||||
"state" TEXT,
|
||||
"wecomUserId" TEXT,
|
||||
"enabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"lastSyncedAt" TIMESTAMP(3),
|
||||
"lastSyncError" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "WecomContactWay_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE "WecomExternalContact" (
|
||||
"id" TEXT NOT NULL,
|
||||
"externalUserId" TEXT NOT NULL,
|
||||
"wecomUserId" TEXT NOT NULL,
|
||||
"contactWayId" TEXT,
|
||||
"miniProgramUserId" TEXT,
|
||||
"customerId" TEXT,
|
||||
"name" TEXT,
|
||||
"avatarUrl" TEXT,
|
||||
"type" INTEGER,
|
||||
"gender" INTEGER,
|
||||
"unionId" TEXT,
|
||||
"corpName" TEXT,
|
||||
"position" TEXT,
|
||||
"remark" TEXT,
|
||||
"description" TEXT,
|
||||
"profile" JSONB,
|
||||
"status" TEXT NOT NULL DEFAULT 'active',
|
||||
"addedAt" TIMESTAMP(3),
|
||||
"lastEventAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"deletedAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "WecomExternalContact_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "WecomContactWay_configId_key" ON "WecomContactWay"("configId");
|
||||
CREATE UNIQUE INDEX "WecomExternalContact_externalUserId_wecomUserId_key" ON "WecomExternalContact"("externalUserId", "wecomUserId");
|
||||
CREATE INDEX "Lead_wecomContactId_idx" ON "Lead"("wecomContactId");
|
||||
CREATE INDEX "WecomExternalContact_status_lastEventAt_idx" ON "WecomExternalContact"("status", "lastEventAt");
|
||||
CREATE INDEX "WecomExternalContact_miniProgramUserId_idx" ON "WecomExternalContact"("miniProgramUserId");
|
||||
CREATE INDEX "WecomExternalContact_customerId_idx" ON "WecomExternalContact"("customerId");
|
||||
CREATE INDEX "WecomExternalContact_contactWayId_idx" ON "WecomExternalContact"("contactWayId");
|
||||
|
||||
ALTER TABLE "Lead"
|
||||
ADD CONSTRAINT "Lead_wecomContactId_fkey"
|
||||
FOREIGN KEY ("wecomContactId") REFERENCES "WecomExternalContact"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "WecomExternalContact"
|
||||
ADD CONSTRAINT "WecomExternalContact_contactWayId_fkey"
|
||||
FOREIGN KEY ("contactWayId") REFERENCES "WecomContactWay"("id") ON DELETE SET NULL ON UPDATE CASCADE,
|
||||
ADD CONSTRAINT "WecomExternalContact_miniProgramUserId_fkey"
|
||||
FOREIGN KEY ("miniProgramUserId") REFERENCES "MiniProgramUser"("id") ON DELETE SET NULL ON UPDATE CASCADE,
|
||||
ADD CONSTRAINT "WecomExternalContact_customerId_fkey"
|
||||
FOREIGN KEY ("customerId") REFERENCES "Customer"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,5 @@
|
||||
ALTER TABLE "Lead"
|
||||
ADD COLUMN "requestType" TEXT NOT NULL DEFAULT 'custom';
|
||||
|
||||
CREATE INDEX "Lead_requestType_status_createdAt_idx"
|
||||
ON "Lead"("requestType", "status", "createdAt");
|
||||
@@ -0,0 +1,6 @@
|
||||
ALTER TABLE "Lead"
|
||||
ADD COLUMN "adminNote" TEXT,
|
||||
ADD COLUMN "processed" BOOLEAN NOT NULL DEFAULT false;
|
||||
|
||||
CREATE INDEX "Lead_processed_requestType_createdAt_idx"
|
||||
ON "Lead"("processed", "requestType", "createdAt");
|
||||
3
apps/api/prisma/migrations/migration_lock.toml
Normal file
3
apps/api/prisma/migrations/migration_lock.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "postgresql"
|
||||
396
apps/api/prisma/schema.prisma
Normal file
396
apps/api/prisma/schema.prisma
Normal file
@@ -0,0 +1,396 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model AdminUser {
|
||||
id String @id @default(uuid())
|
||||
email String @unique
|
||||
name String
|
||||
passwordHash String
|
||||
role String @default("admin")
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
auditLogs AuditLog[]
|
||||
assignedLeads Lead[] @relation("LeadAssignee")
|
||||
}
|
||||
|
||||
model MediaAsset {
|
||||
id String @id @default(uuid())
|
||||
url String @unique
|
||||
name String?
|
||||
mimeType String?
|
||||
sizeBytes Int?
|
||||
group String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model HeroSlide {
|
||||
id String @id @default(uuid())
|
||||
title String
|
||||
kicker String?
|
||||
actionLabel String?
|
||||
image String
|
||||
targetType String?
|
||||
targetValue String?
|
||||
sortOrder Int @default(0)
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Destination {
|
||||
id String @id @default(uuid())
|
||||
name String @unique
|
||||
slug String @unique
|
||||
region String?
|
||||
image String?
|
||||
isHot Boolean @default(false)
|
||||
sortOrder Int @default(0)
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
aliases DestinationAlias[]
|
||||
products Product[]
|
||||
}
|
||||
|
||||
model DestinationAlias {
|
||||
id String @id @default(uuid())
|
||||
alias String
|
||||
destinationId String
|
||||
destination Destination @relation(fields: [destinationId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([alias, destinationId])
|
||||
}
|
||||
|
||||
model ThemeCard {
|
||||
id String @id @default(uuid())
|
||||
label String
|
||||
image String
|
||||
targetType String?
|
||||
targetValue String?
|
||||
sortOrder Int @default(0)
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model CtaBanner {
|
||||
id String @id @default(uuid())
|
||||
alt String
|
||||
image String
|
||||
targetType String
|
||||
targetValue String?
|
||||
sortOrder Int @default(0)
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model DestinationPageConfig {
|
||||
id String @id @default("destination-home")
|
||||
recommendedProductIds Json
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model SearchPageConfig {
|
||||
id String @id @default("search-page")
|
||||
title String
|
||||
subtitle String
|
||||
placeholder String
|
||||
modules Json @default("{}")
|
||||
popularKeywords Json
|
||||
groups Json
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model HomeModule {
|
||||
id String @id
|
||||
label String
|
||||
templateType String @default("explore")
|
||||
content Json?
|
||||
publishedConfig Json?
|
||||
sortOrder Int @default(0)
|
||||
isActive Boolean @default(true)
|
||||
isSystem Boolean @default(false)
|
||||
isDeleted Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Product {
|
||||
id String @id @default(uuid())
|
||||
sourceId Int? @unique
|
||||
title String
|
||||
subtitle String?
|
||||
destinationId String?
|
||||
destination Destination? @relation(fields: [destinationId], references: [id], onDelete: SetNull)
|
||||
priceAmount Int?
|
||||
priceUnit String @default("起/人")
|
||||
pricingTiers Json?
|
||||
tags String[]
|
||||
coverImage String?
|
||||
summary String?
|
||||
durationDays Int?
|
||||
durationNights Int?
|
||||
departureDates String[] @default([])
|
||||
remainingSpots Int?
|
||||
recommendation String?
|
||||
keyFacts Json?
|
||||
contentBlocks Json?
|
||||
detailSections Json?
|
||||
status String @default("published")
|
||||
sortWeight Int @default(0)
|
||||
publishedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
images ProductImage[]
|
||||
campaignLinks CampaignProduct[]
|
||||
leads Lead[]
|
||||
favorites MiniProgramFavorite[]
|
||||
histories MiniProgramHistory[]
|
||||
orders Order[]
|
||||
}
|
||||
|
||||
model ProductImage {
|
||||
id String @id @default(uuid())
|
||||
productId String
|
||||
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
|
||||
url String
|
||||
alt String?
|
||||
sortOrder Int @default(0)
|
||||
}
|
||||
|
||||
model Campaign {
|
||||
id String @id @default(uuid())
|
||||
slug String @unique
|
||||
title String
|
||||
description String?
|
||||
coverImage String?
|
||||
status String @default("published")
|
||||
startsAt DateTime?
|
||||
endsAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
products CampaignProduct[]
|
||||
}
|
||||
|
||||
model CampaignProduct {
|
||||
campaignId String
|
||||
productId String
|
||||
sortOrder Int @default(0)
|
||||
campaign Campaign @relation(fields: [campaignId], references: [id], onDelete: Cascade)
|
||||
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@id([campaignId, productId])
|
||||
}
|
||||
|
||||
model Lead {
|
||||
id String @id @default(uuid())
|
||||
requestType String @default("custom")
|
||||
userId String?
|
||||
user MiniProgramUser? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
||||
wecomContactId String?
|
||||
wecomContact WecomExternalContact? @relation(fields: [wecomContactId], references: [id], onDelete: SetNull)
|
||||
destination String?
|
||||
phone String
|
||||
contactName String?
|
||||
wechat String?
|
||||
travelDate DateTime?
|
||||
peopleCount Int?
|
||||
adultCount Int?
|
||||
childCount Int?
|
||||
roomType String?
|
||||
plan String?
|
||||
addOns String[] @default([])
|
||||
budgetMin Int?
|
||||
budgetMax Int?
|
||||
note String?
|
||||
sourcePage String?
|
||||
sourceTheme String?
|
||||
sourceProductId String?
|
||||
sourceProduct Product? @relation(fields: [sourceProductId], references: [id], onDelete: SetNull)
|
||||
adminNote String?
|
||||
processed Boolean @default(false)
|
||||
status String @default("new")
|
||||
assignedUserId String?
|
||||
assignedUser AdminUser? @relation("LeadAssignee", fields: [assignedUserId], references: [id], onDelete: SetNull)
|
||||
followups LeadFollowup[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([userId, createdAt])
|
||||
@@index([phone])
|
||||
@@index([requestType, status, createdAt])
|
||||
@@index([wecomContactId])
|
||||
}
|
||||
|
||||
model LeadFollowup {
|
||||
id String @id @default(uuid())
|
||||
leadId String
|
||||
lead Lead @relation(fields: [leadId], references: [id], onDelete: Cascade)
|
||||
content String
|
||||
nextAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model MiniProgramUser {
|
||||
id String @id @default(uuid())
|
||||
appId String
|
||||
openId String
|
||||
unionId String?
|
||||
nickname String?
|
||||
avatarUrl String?
|
||||
phone String?
|
||||
phoneAuthorizedAt DateTime?
|
||||
note String?
|
||||
source String @default("mini_program")
|
||||
status String @default("active")
|
||||
customerId String? @unique
|
||||
customer Customer? @relation(fields: [customerId], references: [id], onDelete: SetNull)
|
||||
firstSeenAt DateTime @default(now())
|
||||
lastLoginAt DateTime @default(now())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
leads Lead[]
|
||||
favorites MiniProgramFavorite[]
|
||||
histories MiniProgramHistory[]
|
||||
wecomContacts WecomExternalContact[]
|
||||
|
||||
@@unique([appId, openId])
|
||||
@@index([phone])
|
||||
@@index([status, lastLoginAt])
|
||||
}
|
||||
|
||||
model MiniProgramFavorite {
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
productId String
|
||||
user MiniProgramUser @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@unique([userId, productId])
|
||||
@@index([productId])
|
||||
}
|
||||
|
||||
model MiniProgramHistory {
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
productId String
|
||||
viewCount Int @default(1)
|
||||
lastViewedAt DateTime @default(now())
|
||||
user MiniProgramUser @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([userId, productId])
|
||||
@@index([userId, lastViewedAt])
|
||||
}
|
||||
|
||||
model Customer {
|
||||
id String @id @default(uuid())
|
||||
phone String? @unique
|
||||
name String?
|
||||
wechat String?
|
||||
note String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
miniProgramUser MiniProgramUser?
|
||||
orders Order[]
|
||||
wecomContacts WecomExternalContact[]
|
||||
}
|
||||
|
||||
model WecomContactWay {
|
||||
id String @id @default(uuid())
|
||||
configId String @unique
|
||||
type Int @default(1)
|
||||
scene Int @default(1)
|
||||
style Int @default(1)
|
||||
remark String?
|
||||
state String?
|
||||
wecomUserId String?
|
||||
enabled Boolean @default(true)
|
||||
lastSyncedAt DateTime?
|
||||
lastSyncError String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
externalContacts WecomExternalContact[]
|
||||
}
|
||||
|
||||
model WecomExternalContact {
|
||||
id String @id @default(uuid())
|
||||
externalUserId String
|
||||
wecomUserId String
|
||||
contactWayId String?
|
||||
contactWay WecomContactWay? @relation(fields: [contactWayId], references: [id], onDelete: SetNull)
|
||||
miniProgramUserId String?
|
||||
miniProgramUser MiniProgramUser? @relation(fields: [miniProgramUserId], references: [id], onDelete: SetNull)
|
||||
customerId String?
|
||||
customer Customer? @relation(fields: [customerId], references: [id], onDelete: SetNull)
|
||||
name String?
|
||||
avatarUrl String?
|
||||
type Int?
|
||||
gender Int?
|
||||
unionId String?
|
||||
corpName String?
|
||||
position String?
|
||||
remark String?
|
||||
description String?
|
||||
profile Json?
|
||||
status String @default("active")
|
||||
addedAt DateTime?
|
||||
lastEventAt DateTime @default(now())
|
||||
deletedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
leads Lead[]
|
||||
|
||||
@@unique([externalUserId, wecomUserId])
|
||||
@@index([status, lastEventAt])
|
||||
@@index([miniProgramUserId])
|
||||
@@index([customerId])
|
||||
@@index([contactWayId])
|
||||
}
|
||||
|
||||
model Order {
|
||||
id String @id @default(uuid())
|
||||
customerId String?
|
||||
customer Customer? @relation(fields: [customerId], references: [id], onDelete: SetNull)
|
||||
productId String?
|
||||
product Product? @relation(fields: [productId], references: [id], onDelete: SetNull)
|
||||
status String @default("draft")
|
||||
travelDate DateTime?
|
||||
amount Int?
|
||||
note String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model SiteVersion {
|
||||
id String @id @default(uuid())
|
||||
title String
|
||||
status String @default("draft")
|
||||
snapshot Json
|
||||
publishedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model AuditLog {
|
||||
id String @id @default(uuid())
|
||||
actorId String?
|
||||
actor AdminUser? @relation(fields: [actorId], references: [id], onDelete: SetNull)
|
||||
action String
|
||||
entity String
|
||||
entityId String?
|
||||
before Json?
|
||||
after Json?
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
381
apps/api/prisma/seed.ts
Normal file
381
apps/api/prisma/seed.ts
Normal file
@@ -0,0 +1,381 @@
|
||||
import bcrypt from "bcryptjs";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { Prisma, PrismaClient } from "@prisma/client";
|
||||
import { bottomCtas, destinations, heroSlides, themeCards } from "../../../src/content.ts";
|
||||
import { DESTINATION_PAGE_CONFIG_ID, DESTINATION_RECOMMENDATION_LIMIT } from "../src/destination-page.ts";
|
||||
import { createHomeModuleSnapshot, DEFAULT_HOME_MODULES } from "../src/home-module-defaults.ts";
|
||||
import { collectImageUrls, materializeImageUrls } from "../src/media-migration.ts";
|
||||
import { mediaStorage } from "../src/media-storage.ts";
|
||||
import { DEFAULT_SEARCH_PAGE_CONFIG, SEARCH_PAGE_CONFIG_ID } from "../src/search-page.ts";
|
||||
import { getWecomContactWayConfigDefaults } from "../src/wecom.ts";
|
||||
|
||||
type SourceProduct = {
|
||||
id: number;
|
||||
title: string;
|
||||
price: string;
|
||||
tags: string[];
|
||||
image: string;
|
||||
summary?: string;
|
||||
destinationName?: string;
|
||||
priceUnit?: string;
|
||||
pricingTiers?: Array<{
|
||||
groupSize: number;
|
||||
adultPrice: number;
|
||||
child6PlusPrice: number;
|
||||
childUnder6Price: number;
|
||||
}>;
|
||||
images?: { url: string; alt?: string | null; sortOrder?: number }[];
|
||||
durationDays?: number | null;
|
||||
durationNights?: number | null;
|
||||
departureDates?: string[];
|
||||
remainingSpots?: number | null;
|
||||
recommendation?: string | null;
|
||||
keyFacts?: Array<{ label: string; value: string }>;
|
||||
contentBlocks?: Array<{ type: "title"; text: string } | { type: "image"; url: string; alt?: string | null }>;
|
||||
detailSections?: Array<{ key: string; label: string; title?: string | null; blocks: Array<{ type: "text"; text: string } | { type: "image"; url: string; alt?: string | null }> }>;
|
||||
};
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
|
||||
|
||||
function slugify(input: string) {
|
||||
return encodeURIComponent(input).replace(/%/g, "").toLowerCase();
|
||||
}
|
||||
|
||||
async function createMedia(url: string, group: string, name?: string) {
|
||||
if (!url) return;
|
||||
await prisma.mediaAsset.upsert({
|
||||
where: { url },
|
||||
update: { group, name },
|
||||
create: { url, group, name },
|
||||
});
|
||||
}
|
||||
|
||||
function defaultDetailSections(summary: string, location: string) {
|
||||
return [
|
||||
{
|
||||
key: "overview",
|
||||
label: "行程概述",
|
||||
title: "小包团专属概览",
|
||||
blocks: [{ type: "text", text: `${summary}。万趣会按同行人、预算、酒店偏好和体力强度重排细节,保留小车小团、错峰入园与在地向导服务。` }],
|
||||
},
|
||||
{
|
||||
key: "itinerary",
|
||||
label: "每日行程",
|
||||
title: `${location} 弹性安排`,
|
||||
blocks: [{ type: "text", text: "默认按抵达接站、核心景点游览、特色体验、酒店休整和返程送站安排每日节奏;具体天数、停留时长和餐食可在行前由服务管家二次确认。" }],
|
||||
},
|
||||
{
|
||||
key: "service",
|
||||
label: "包含/不含服务",
|
||||
title: "费用边界清晰",
|
||||
blocks: [{ type: "text", text: "通常包含当地用车、行程内住宿、列明门票/体验、必要讲解和服务管家跟进;大交通、个人消费、未列明餐食和自选项目以最终方案为准。" }],
|
||||
},
|
||||
{
|
||||
key: "notice",
|
||||
label: "出行须知",
|
||||
title: "贵州山地旅行提示",
|
||||
blocks: [{ type: "text", text: "贵州多山多雨,建议准备防滑鞋、轻便雨具和薄外套;溶洞、漂流、徒步等体验会按天气和同行人体力调整。" }],
|
||||
},
|
||||
{
|
||||
key: "price",
|
||||
label: "价格区间",
|
||||
title: "按人数、酒店和季节报价",
|
||||
blocks: [{ type: "text", text: "页面价格为参考起价,节假日、旺季房态、用车车型和体验资源会影响最终报价;提交需求后由服务管家给出可执行方案。" }],
|
||||
},
|
||||
{
|
||||
key: "manager",
|
||||
label: "服务管家",
|
||||
title: "直接添加服务管家",
|
||||
blocks: [{ type: "text", text: "点击底部“服务管家”或拨打 18786174929,可直接添加服务管家沟通出行人数、日期、酒店偏好和预算。" }],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function defaultKeyFacts(product: SourceProduct) {
|
||||
const duration = product.title.match(/\d+天\d+晚/)?.[0];
|
||||
return [
|
||||
duration ? { label: "行程时长", value: duration } : null,
|
||||
product.tags[0] ? { label: "线路类型", value: product.tags[0] } : null,
|
||||
{ label: "成团方式", value: "专属小包团" },
|
||||
].filter((fact): fact is { label: string; value: string } => Boolean(fact));
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await mediaStorage.ensureReady();
|
||||
const productFile = await readFile(resolve(rootDir, "src/generated-products.json"), "utf8");
|
||||
const products = await materializeImageUrls(JSON.parse(productFile) as SourceProduct[]);
|
||||
const seededHomeModules = await materializeImageUrls(DEFAULT_HOME_MODULES);
|
||||
const seededHeroSlides = await materializeImageUrls(heroSlides);
|
||||
const seededDestinations = await materializeImageUrls(destinations);
|
||||
const seededThemeCards = await materializeImageUrls(themeCards);
|
||||
const seededCtas = await materializeImageUrls(bottomCtas);
|
||||
const seededSearchPage = await materializeImageUrls(DEFAULT_SEARCH_PAGE_CONFIG);
|
||||
|
||||
await prisma.auditLog.deleteMany();
|
||||
await prisma.wecomExternalContact.deleteMany();
|
||||
await prisma.siteVersion.deleteMany();
|
||||
await prisma.homeModule.deleteMany();
|
||||
await prisma.searchPageConfig.deleteMany();
|
||||
await prisma.miniProgramHistory.deleteMany();
|
||||
await prisma.miniProgramFavorite.deleteMany();
|
||||
await prisma.miniProgramUser.deleteMany();
|
||||
await prisma.leadFollowup.deleteMany();
|
||||
await prisma.lead.deleteMany();
|
||||
await prisma.order.deleteMany();
|
||||
await prisma.customer.deleteMany();
|
||||
await prisma.campaignProduct.deleteMany();
|
||||
await prisma.campaign.deleteMany();
|
||||
await prisma.productImage.deleteMany();
|
||||
await prisma.product.deleteMany();
|
||||
await prisma.ctaBanner.deleteMany();
|
||||
await prisma.themeCard.deleteMany();
|
||||
await prisma.heroSlide.deleteMany();
|
||||
await prisma.destinationAlias.deleteMany();
|
||||
await prisma.destination.deleteMany();
|
||||
await prisma.mediaAsset.deleteMany();
|
||||
|
||||
const wecomDefaults = getWecomContactWayConfigDefaults();
|
||||
await prisma.wecomContactWay.upsert({
|
||||
where: { configId: wecomDefaults.configId },
|
||||
update: {},
|
||||
create: wecomDefaults,
|
||||
});
|
||||
|
||||
await prisma.homeModule.createMany({
|
||||
data: seededHomeModules.map((module) => ({
|
||||
...module,
|
||||
content: module.content as Prisma.InputJsonValue,
|
||||
publishedConfig: createHomeModuleSnapshot(module) as Prisma.InputJsonValue,
|
||||
})),
|
||||
});
|
||||
|
||||
for (const url of collectImageUrls(seededHomeModules)) await createMedia(url, "home-module");
|
||||
for (const url of collectImageUrls(seededSearchPage)) await createMedia(url, "search-page");
|
||||
|
||||
const passwordHash = await bcrypt.hash("ChangeMe123!", 12);
|
||||
await prisma.adminUser.upsert({
|
||||
where: { email: "admin@example.com" },
|
||||
update: { passwordHash, isActive: true },
|
||||
create: {
|
||||
email: "admin@example.com",
|
||||
name: "后台管理员",
|
||||
passwordHash,
|
||||
role: "super_admin",
|
||||
},
|
||||
});
|
||||
|
||||
for (const [index, slide] of seededHeroSlides.entries()) {
|
||||
await createMedia(slide.image, "hero", slide.title);
|
||||
await prisma.heroSlide.create({
|
||||
data: {
|
||||
title: slide.title,
|
||||
kicker: slide.kicker,
|
||||
actionLabel: slide.action,
|
||||
image: slide.image,
|
||||
targetType: slide.targetType,
|
||||
targetValue: slide.targetValue,
|
||||
sortOrder: index,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const destinationMap = new Map<string, string>();
|
||||
for (const [index, item] of seededDestinations.entries()) {
|
||||
if (item.image) await createMedia(item.image, "destination", item.label);
|
||||
const destination = await prisma.destination.create({
|
||||
data: {
|
||||
name: item.label,
|
||||
slug: slugify(item.label),
|
||||
image: item.image,
|
||||
isHot: index < 8,
|
||||
sortOrder: index,
|
||||
},
|
||||
});
|
||||
destinationMap.set(item.label, destination.id);
|
||||
}
|
||||
|
||||
const aliases: Record<string, string[]> = {
|
||||
贵阳: ["贵阳", "青岩", "花溪", "高坡", "天河潭"],
|
||||
黄果树: ["黄果树", "安顺", "坝陵河", "瀑布"],
|
||||
荔波小七孔: ["荔波", "小七孔", "茂兰", "水上森林"],
|
||||
西江苗寨: ["西江", "苗寨", "郎德", "雷山", "苗岭"],
|
||||
梵净山: ["梵净山", "铜仁", "云舍", "寨沙"],
|
||||
镇远: ["镇远", "青龙洞", "古城"],
|
||||
肇兴: ["肇兴", "侗寨", "堂安", "加榜", "黎平"],
|
||||
万峰林: ["万峰林", "万峰湖", "马岭河", "兴义", "黔西南"],
|
||||
织金洞: ["织金洞", "织金", "洞穴", "喀斯特"],
|
||||
赤水丹霞: ["赤水", "丹霞", "竹海", "丙安"],
|
||||
青岩古镇: ["青岩", "古镇", "屯堡"],
|
||||
百里杜鹃: ["百里杜鹃", "毕节", "花季"],
|
||||
乌蒙草原: ["乌蒙", "六盘水", "草原", "避暑"],
|
||||
中国天眼: ["中国天眼", "平塘", "观星", "科普"],
|
||||
遵义: ["遵义", "黔北", "红色文化"],
|
||||
茅台镇: ["茅台", "酱香", "酒旅"],
|
||||
};
|
||||
|
||||
for (const [label, values] of Object.entries(aliases)) {
|
||||
const destinationId = destinationMap.get(label);
|
||||
if (!destinationId) continue;
|
||||
for (const alias of values) {
|
||||
await prisma.destinationAlias.create({ data: { destinationId, alias } });
|
||||
}
|
||||
}
|
||||
|
||||
for (const [index, card] of seededThemeCards.entries()) {
|
||||
await createMedia(card.image, "theme", card.label);
|
||||
await prisma.themeCard.create({
|
||||
data: {
|
||||
label: card.label,
|
||||
image: card.image,
|
||||
targetType: card.targetType,
|
||||
targetValue: card.targetValue,
|
||||
sortOrder: index,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
for (const [index, cta] of seededCtas.entries()) {
|
||||
await createMedia(cta.image, "cta", cta.alt);
|
||||
await prisma.ctaBanner.create({
|
||||
data: {
|
||||
alt: cta.alt,
|
||||
image: cta.image,
|
||||
targetType: cta.targetType,
|
||||
targetValue: cta.targetValue,
|
||||
sortOrder: index,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
for (const product of products) {
|
||||
await createMedia(product.image, "product", product.title);
|
||||
for (const image of product.images ?? []) await createMedia(image.url, "product-gallery", image.alt ?? product.title);
|
||||
for (const block of product.contentBlocks ?? []) {
|
||||
if (block.type === "image") await createMedia(block.url, "product-content", block.alt ?? product.title);
|
||||
}
|
||||
const matchedDestination = product.destinationName && destinationMap.has(product.destinationName)
|
||||
? product.destinationName
|
||||
: [...destinationMap.keys()].find((label) => product.title.includes(label) || product.tags.includes(label));
|
||||
const summary = product.summary ?? product.tags.join(" · ");
|
||||
const created = await prisma.product.create({
|
||||
data: {
|
||||
sourceId: product.id,
|
||||
title: product.title,
|
||||
subtitle: product.title.replace(/^【.*?】\s*/, "").split("·")[0],
|
||||
destinationId: matchedDestination ? destinationMap.get(matchedDestination) : undefined,
|
||||
priceAmount: product.price ? Number(product.price) : null,
|
||||
priceUnit: product.priceUnit ?? "咨询价",
|
||||
pricingTiers: product.pricingTiers?.length ? (product.pricingTiers as Prisma.InputJsonValue) : undefined,
|
||||
tags: product.tags,
|
||||
coverImage: product.image,
|
||||
summary,
|
||||
durationDays: product.durationDays ?? null,
|
||||
durationNights: product.durationNights ?? null,
|
||||
departureDates: product.departureDates ?? [],
|
||||
remainingSpots: product.remainingSpots ?? null,
|
||||
recommendation: product.recommendation ?? null,
|
||||
keyFacts: (product.keyFacts?.length ? product.keyFacts : defaultKeyFacts(product)) as Prisma.InputJsonValue,
|
||||
contentBlocks: product.contentBlocks?.length ? (product.contentBlocks as Prisma.InputJsonValue) : undefined,
|
||||
status: "published",
|
||||
sortWeight: product.id,
|
||||
publishedAt: new Date(),
|
||||
},
|
||||
});
|
||||
const gallery = product.images?.length ? product.images : [{ url: product.image, alt: product.title, sortOrder: 0 }];
|
||||
await prisma.productImage.createMany({
|
||||
data: gallery.map((image, index) => ({ productId: created.id, url: image.url, alt: image.alt ?? product.title, sortOrder: image.sortOrder ?? index })),
|
||||
});
|
||||
}
|
||||
|
||||
const defaultRecommendedProducts = await prisma.product.findMany({
|
||||
where: { status: "published" },
|
||||
orderBy: [{ sortWeight: "asc" }, { createdAt: "asc" }],
|
||||
take: DESTINATION_RECOMMENDATION_LIMIT,
|
||||
select: { id: true },
|
||||
});
|
||||
await prisma.destinationPageConfig.upsert({
|
||||
where: { id: DESTINATION_PAGE_CONFIG_ID },
|
||||
update: { recommendedProductIds: defaultRecommendedProducts.map((product) => product.id) as Prisma.InputJsonValue },
|
||||
create: { id: DESTINATION_PAGE_CONFIG_ID, recommendedProductIds: defaultRecommendedProducts.map((product) => product.id) as Prisma.InputJsonValue },
|
||||
});
|
||||
|
||||
await prisma.searchPageConfig.upsert({
|
||||
where: { id: SEARCH_PAGE_CONFIG_ID },
|
||||
update: {
|
||||
title: seededSearchPage.modules.seasonalInspiration.title,
|
||||
subtitle: seededSearchPage.modules.seasonalInspiration.subtitle,
|
||||
placeholder: seededSearchPage.placeholder,
|
||||
modules: seededSearchPage.modules as Prisma.InputJsonValue,
|
||||
popularKeywords: seededSearchPage.popularKeywords as Prisma.InputJsonValue,
|
||||
groups: seededSearchPage.groups as Prisma.InputJsonValue,
|
||||
},
|
||||
create: {
|
||||
id: SEARCH_PAGE_CONFIG_ID,
|
||||
title: seededSearchPage.modules.seasonalInspiration.title,
|
||||
subtitle: seededSearchPage.modules.seasonalInspiration.subtitle,
|
||||
placeholder: seededSearchPage.placeholder,
|
||||
modules: seededSearchPage.modules as Prisma.InputJsonValue,
|
||||
popularKeywords: seededSearchPage.popularKeywords as Prisma.InputJsonValue,
|
||||
groups: seededSearchPage.groups as Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
|
||||
const campaignSeeds = [
|
||||
{ slug: "classic-deal", title: "经典打卡特惠", start: 0, end: 8, coverImage: seededHeroSlides[0]?.image },
|
||||
{ slug: "outdoor-deal", title: "山野野咖特惠", start: 8, end: 16, coverImage: seededHeroSlides[1]?.image },
|
||||
{ slug: "mixed-route", title: "人文户外混搭", start: 16, end: 24, coverImage: seededHeroSlides[2]?.image },
|
||||
];
|
||||
|
||||
for (const seed of campaignSeeds) {
|
||||
const campaign = await prisma.campaign.create({
|
||||
data: {
|
||||
slug: seed.slug,
|
||||
title: seed.title,
|
||||
description: "由当前 H5 万趣贵州小包团内容导入的活动专题。",
|
||||
coverImage: seed.coverImage,
|
||||
status: "published",
|
||||
},
|
||||
});
|
||||
|
||||
const linkedProducts = await prisma.product.findMany({
|
||||
where: { sourceId: { gte: seed.start + 1, lte: seed.end } },
|
||||
orderBy: { sourceId: "asc" },
|
||||
});
|
||||
|
||||
for (const [index, product] of linkedProducts.entries()) {
|
||||
await prisma.campaignProduct.create({
|
||||
data: { campaignId: campaign.id, productId: product.id, sortOrder: index },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const snapshot = await prisma.siteVersion.create({
|
||||
data: {
|
||||
title: "seed-initial",
|
||||
status: "published",
|
||||
publishedAt: new Date(),
|
||||
snapshot: {
|
||||
homeModules: seededHomeModules.length,
|
||||
heroSlides: seededHeroSlides.length,
|
||||
destinations: seededDestinations.length,
|
||||
themeCards: seededThemeCards.length,
|
||||
products: products.length,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`Seed complete. Admin login: admin@example.com / ChangeMe123!`);
|
||||
console.log(`Initial site version: ${snapshot.id}`);
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
Reference in New Issue
Block a user