From c07b28def7e943c75790cf19eab674333991a368 Mon Sep 17 00:00:00 2001 From: yeuimu <2197651308@qq.com> Date: Fri, 28 Aug 2026 12:24:24 +0800 Subject: [PATCH] feat(api): add product_families schema and migration --- .../migration.sql | 79 +++++++++++++++++++ apps/api/prisma/schema.prisma | 62 +++++++++++++++ 2 files changed, 141 insertions(+) create mode 100644 apps/api/prisma/migrations/20260828042358_add_product_families/migration.sql diff --git a/apps/api/prisma/migrations/20260828042358_add_product_families/migration.sql b/apps/api/prisma/migrations/20260828042358_add_product_families/migration.sql new file mode 100644 index 0000000..55b1e9a --- /dev/null +++ b/apps/api/prisma/migrations/20260828042358_add_product_families/migration.sql @@ -0,0 +1,79 @@ +-- AlterTable +ALTER TABLE "origin_goods" ADD COLUMN "craft_label" TEXT, +ADD COLUMN "family_id" BIGINT, +ADD COLUMN "logistics_label" TEXT, +ADD COLUMN "sku_code" TEXT, +ADD COLUMN "warehouse_label" TEXT; + +-- CreateTable +CREATE TABLE "product_families" ( + "family_id" BIGSERIAL NOT NULL, + "family_code" TEXT, + "family_name" TEXT NOT NULL, + "family_image" TEXT, + "country_id" BIGINT, + "category_id" BIGINT, + "primary_origin_good_id" BIGINT, + "detail" JSONB, + "size_chart" JSONB, + "package_specs" JSONB, + "price_matrix" JSONB, + "auto_managed" BOOLEAN NOT NULL DEFAULT true, + "stale" BOOLEAN NOT NULL DEFAULT false, + "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(6) NOT NULL, + + CONSTRAINT "product_families_pkey" PRIMARY KEY ("family_id") +); + +-- CreateTable +CREATE TABLE "family_price_overrides" ( + "family_price_override_id" BIGSERIAL NOT NULL, + "family_id" BIGINT NOT NULL, + "size_id" TEXT NOT NULL, + "color_id" TEXT NOT NULL, + "craft" TEXT NOT NULL, + "logistics" TEXT NOT NULL, + "price" DECIMAL(12,2) NOT NULL, + "note" TEXT, + "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ(6) NOT NULL, + + CONSTRAINT "family_price_overrides_pkey" PRIMARY KEY ("family_price_override_id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "product_families_family_code_key" ON "product_families"("family_code"); + +-- CreateIndex +CREATE INDEX "product_families_country_id_idx" ON "product_families"("country_id"); + +-- CreateIndex +CREATE INDEX "product_families_category_id_idx" ON "product_families"("category_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "family_price_overrides_family_id_size_id_color_id_craft_log_key" ON "family_price_overrides"("family_id", "size_id", "color_id", "craft", "logistics"); + +-- CreateIndex +CREATE INDEX "origin_goods_family_id_idx" ON "origin_goods"("family_id"); + +-- CreateIndex +CREATE INDEX "origin_goods_sku_code_idx" ON "origin_goods"("sku_code"); + +-- CreateIndex +CREATE INDEX "origin_goods_logistics_label_idx" ON "origin_goods"("logistics_label"); + +-- CreateIndex +CREATE INDEX "origin_goods_craft_label_idx" ON "origin_goods"("craft_label"); + +-- AddForeignKey +ALTER TABLE "origin_goods" ADD CONSTRAINT "origin_goods_family_id_fkey" FOREIGN KEY ("family_id") REFERENCES "product_families"("family_id") ON DELETE SET NULL ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "product_families" ADD CONSTRAINT "product_families_country_id_fkey" FOREIGN KEY ("country_id") REFERENCES "countries"("country_id") ON DELETE SET NULL ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "product_families" ADD CONSTRAINT "product_families_category_id_fkey" FOREIGN KEY ("category_id") REFERENCES "categories"("category_id") ON DELETE SET NULL ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "family_price_overrides" ADD CONSTRAINT "family_price_overrides_family_id_fkey" FOREIGN KEY ("family_id") REFERENCES "product_families"("family_id") ON DELETE CASCADE ON UPDATE NO ACTION; diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index 25b9408..5929a2c 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -30,9 +30,15 @@ model OriginGood { goodPrice Decimal? @map("good_price") @db.Decimal(12, 2) delisted Boolean @default(false) source OriginGoodSource @default(SDS) + familyId BigInt? @map("family_id") + skuCode String? @map("sku_code") + logisticsLabel String? @map("logistics_label") + craftLabel String? @map("craft_label") + warehouseLabel String? @map("warehouse_label") createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6) + family ProductFamily? @relation(fields: [familyId], references: [id], onDelete: SetNull, onUpdate: NoAction) goods Good[] detail OriginGoodDetail? variants OriginGoodVariant[] @@ -40,6 +46,10 @@ model OriginGood { @@index([sdsCategoryId]) @@index([source]) + @@index([familyId]) + @@index([skuCode]) + @@index([logisticsLabel]) + @@index([craftLabel]) @@map("origin_goods") } @@ -118,6 +128,7 @@ model Country { goods Good[] positions Position[] + families ProductFamily[] @@map("countries") } @@ -136,6 +147,7 @@ model Category { children Category[] @relation("CategoryToCategory") goods Good[] positions Position[] + families ProductFamily[] @@index([parentCategoryId]) @@map("categories") @@ -258,6 +270,56 @@ model GoodOriginGood { @@map("good_origin_goods") } +// ---------- Product Families (SPU layer over origin goods) ---------- +// 设计文档:docs/superpowers/specs/2026-08-28-product-family-merge-design.md +// primaryOriginGoodId 刻意不建 FK:避免与 origin_goods 相互依赖,主链接被同步删除时由服务层清理。 +model ProductFamily { + id BigInt @id @default(autoincrement()) @map("family_id") + familyCode String? @unique @map("family_code") + familyName String @map("family_name") + familyImage String? @map("family_image") + countryId BigInt? @map("country_id") + categoryId BigInt? @map("category_id") + primaryOriginGoodId BigInt? @map("primary_origin_good_id") + detail Json? + sizeChart Json? @map("size_chart") + packageSpecs Json? @map("package_specs") + priceMatrix Json? @map("price_matrix") + autoManaged Boolean @default(true) @map("auto_managed") + stale Boolean @default(false) + createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(6) + + originGoods OriginGood[] + priceOverrides FamilyPriceOverride[] + country Country? @relation(fields: [countryId], references: [id], onDelete: SetNull, onUpdate: NoAction) + category Category? @relation(fields: [categoryId], references: [id], onDelete: SetNull, onUpdate: NoAction) + + @@index([countryId]) + @@index([categoryId]) + @@map("product_families") +} + +// ---------- Family Price Overrides (manual per-cell price) ---------- +// 独立于推导矩阵:重算只重建推导部分,本表永不被动覆盖。 +model FamilyPriceOverride { + id BigInt @id @default(autoincrement()) @map("family_price_override_id") + familyId BigInt @map("family_id") + sizeId String @map("size_id") + colorId String @map("color_id") + craft String + logistics String + price Decimal @db.Decimal(12, 2) + note String? + createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) + updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(6) + + family ProductFamily @relation(fields: [familyId], references: [id], onDelete: Cascade, onUpdate: NoAction) + + @@unique([familyId, sizeId, colorId, craft, logistics]) + @@map("family_price_overrides") +} + // ---------- Users (admin authentication) ---------- enum Role { ADMIN