// InkReach Product Center Prisma Schema // Generated based on docs/dev/database-table-design.md // All TIMESTAMPTZ columns use DateTime @db.Timestamptz(6) // All BIGINT columns use BigInt // snake_case table & column names via @@map / @map generator client { provider = "prisma-client-js" binaryTargets = ["native", "debian-openssl-3.0.x"] } datasource db { provider = "postgresql" url = env("DATABASE_URL") } // ---------- Origin Goods ---------- enum OriginGoodSource { SDS CUSTOM } model OriginGood { id BigInt @id @default(autoincrement()) @map("origin_good_id") sdsGoodId String @unique @map("sds_good_id") // Cached SDS product metadata (filled during sync) sdsCategoryId String? @map("sds_category_id") goodName String? @map("good_name") goodImage String? @map("good_image") 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[] mergedIntoGoods GoodOriginGood[] // 链接级标签:自动派生(manual=false)或人工接管(tagsManual=true 后全部 manual) tagsManual Boolean @default(false) @map("tags_manual") originGoodTags OriginGoodTag[] @@index([sdsCategoryId]) @@index([source]) @@index([familyId]) @@index([skuCode]) @@index([logisticsLabel]) @@index([craftLabel]) @@map("origin_goods") } // ---------- OriginGood-Tag Junction(链接级标签:自动派生 + 人工修正) ---------- model OriginGoodTag { id BigInt @id @default(autoincrement()) originGoodId BigInt @map("origin_good_id") tagId BigInt @map("tag_id") // true = 人工配置(自动同步永不覆盖);false = 按链接名称自动派生 manual Boolean @default(false) createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) originGood OriginGood @relation(fields: [originGoodId], references: [id], onDelete: Cascade, onUpdate: NoAction) tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade, onUpdate: NoAction) @@unique([originGoodId, tagId]) @@index([tagId]) @@map("origin_good_tags") } // ---------- Origin Good Details (cached from SDS /products/{id}) ---------- model OriginGoodDetail { originGoodId BigInt @id @map("origin_good_id") productCode String? @map("product_code") englishName String? @map("english_name") blankDesignUrl String? @map("blank_design_url") detailsPageVideoUrl String? @map("details_page_video_url") textureName String? @map("texture_name") productionCycleHours Int? @map("production_cycle_hours") minWeightG Decimal? @map("min_weight_g") @db.Decimal(12, 3) reminder String? productionProcess String? @map("production_process") materialDescription String? @map("material_description") productPerformance String? @map("product_performance") applicableScenarios String? @map("applicable_scenarios") washingInstructions String? @map("washing_instructions") specialDescription String? @map("special_description") designExplanation String? @map("design_explanation") designArea String? @map("design_area") pictureRequest String? @map("picture_request") sizeChart Json? @map("size_chart") packageSpecs Json? @map("package_specs") options Json? media Json? upstreamUpdatedAt DateTime? @map("upstream_updated_at") @db.Timestamptz(6) syncedAt DateTime @default(now()) @map("synced_at") @db.Timestamptz(6) updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6) originGood OriginGood @relation(fields: [originGoodId], references: [id], onDelete: Cascade, onUpdate: NoAction) @@map("origin_good_details") } // ---------- Origin Good Variants (cached SDS child products / SKUs) ---------- model OriginGoodVariant { id BigInt @id @default(autoincrement()) @map("origin_good_variant_id") originGoodId BigInt @map("origin_good_id") sdsVariantId String @map("sds_variant_id") sku String sizeId String? @map("size_id") sizeName String? @map("size_name") colorId String? @map("color_id") colorName String? @map("color_name") colorHex String? @map("color_hex") imageUrl String? @map("image_url") price Decimal? @db.Decimal(12, 2) originalPrice Decimal? @map("original_price") @db.Decimal(12, 2) weightG Decimal? @map("weight_g") @db.Decimal(12, 3) boxLengthCm Decimal? @map("box_length_cm") @db.Decimal(12, 3) boxWidthCm Decimal? @map("box_width_cm") @db.Decimal(12, 3) boxHeightCm Decimal? @map("box_height_cm") @db.Decimal(12, 3) enabled Boolean @default(true) sortOrder Int @default(0) @map("sort_order") designData Json? @map("design_data") createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6) originGood OriginGood @relation(fields: [originGoodId], references: [id], onDelete: Cascade, onUpdate: NoAction) @@unique([originGoodId, sdsVariantId]) @@index([originGoodId, sortOrder]) @@index([sku]) @@map("origin_good_variants") } // ---------- Countries ---------- model Country { id BigInt @id @default(autoincrement()) @map("country_id") countryName String @unique @map("country_name") countryIcon String? @map("country_icon") sortOrder Int @default(0) @map("sort_order") createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6) goods Good[] positions Position[] families ProductFamily[] @@map("countries") } // ---------- Categories (self-referential tree) ---------- model Category { id BigInt @id @default(autoincrement()) @map("category_id") parentCategoryId BigInt? @map("parent_category_id") categoryName String @map("category_name") categoryIcon String? @map("category_icon") sdsCategoryId String? @unique @map("sds_category_id") createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6) parent Category? @relation("CategoryToCategory", fields: [parentCategoryId], references: [id], onDelete: Restrict, onUpdate: NoAction) children Category[] @relation("CategoryToCategory") goods Good[] positions Position[] families ProductFamily[] @@index([parentCategoryId]) @@map("categories") } // ---------- Tag Groups ---------- model TagGroup { id BigInt @id @default(autoincrement()) @map("tag_group_id") groupName String @unique @map("group_name") groupIcon String? @map("group_icon") groupColor String? @map("group_color") sortOrder Int @default(0) @map("sort_order") createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6) tags Tag[] @@map("tag_groups") } // ---------- Tags ---------- model Tag { id BigInt @id @default(autoincrement()) @map("tag_id") tagName String @unique @map("tag_name") tagColor String? @map("tag_color") tagFontColor String? @map("tag_font_color") timing String? tagGroupId BigInt? @map("tag_group_id") sortOrder Int @default(0) @map("sort_order") createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6) goods Good[] goodTags GoodTag[] originGoodTags OriginGoodTag[] tagGroup TagGroup? @relation(fields: [tagGroupId], references: [id], onDelete: SetNull, onUpdate: NoAction) @@index([tagGroupId]) @@index([tagGroupId, sortOrder]) @@map("tags") } // ---------- Positions ---------- model Position { id BigInt @id @default(autoincrement()) @map("position_id") indexVal Int @map("index_val") countryId BigInt? @map("country_id") categoryId BigInt? @map("category_id") createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6) country Country? @relation(fields: [countryId], references: [id], onDelete: Cascade, onUpdate: NoAction) category Category? @relation(fields: [categoryId], references: [id], onDelete: Cascade, onUpdate: NoAction) goods Good[] @@index([countryId]) @@index([categoryId]) @@index([countryId, categoryId]) @@map("positions") } // ---------- Goods ---------- model Good { id BigInt @id @default(autoincrement()) @map("good_id") originGoodId BigInt @map("origin_good_id") familyId BigInt? @map("family_id") countryId BigInt @map("country_id") categoryId BigInt @map("category_id") tagId BigInt? @map("tag_id") positionId BigInt? @map("position_id") goodName String @map("good_name") goodImage String? @map("good_image") goodPriority Int @default(0) @map("good_priority") createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6) originGood OriginGood @relation(fields: [originGoodId], references: [id], onDelete: Restrict, onUpdate: NoAction) family ProductFamily? @relation(fields: [familyId], references: [id], onDelete: SetNull, onUpdate: NoAction) country Country @relation(fields: [countryId], references: [id], onDelete: Restrict, onUpdate: NoAction) category Category @relation(fields: [categoryId], references: [id], onDelete: Restrict, onUpdate: NoAction) tag Tag? @relation(fields: [tagId], references: [id], onDelete: SetNull, onUpdate: NoAction) position Position? @relation(fields: [positionId], references: [id], onDelete: SetNull, onUpdate: NoAction) goodTags GoodTag[] mergedOriginGoods GoodOriginGood[] @@index([originGoodId]) @@index([familyId]) @@index([countryId]) @@index([categoryId]) @@index([tagId]) @@index([positionId]) @@index([goodPriority(sort: Desc)]) @@index([countryId, goodPriority(sort: Desc)]) @@index([categoryId, countryId]) @@map("goods") } // ---------- Good-Tag Junction (M:N) ---------- model GoodTag { goodId BigInt @map("good_id") tagId BigInt @map("tag_id") createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) good Good @relation(fields: [goodId], references: [id], onDelete: Cascade, onUpdate: NoAction) tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade, onUpdate: NoAction) @@id([goodId, tagId]) @@index([tagId]) @@map("good_tags") } // ---------- Good-OriginGood Junction (merged secondary sources, M:N) ---------- // Primary source stays on goods.origin_good_id and is NOT stored here. model GoodOriginGood { goodId BigInt @map("good_id") originGoodId BigInt @map("origin_good_id") createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) good Good @relation(fields: [goodId], references: [id], onDelete: Cascade, onUpdate: NoAction) originGood OriginGood @relation(fields: [originGoodId], references: [id], onDelete: Cascade, onUpdate: NoAction) @@id([goodId, originGoodId]) @@index([originGoodId]) @@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[] goods Good[] 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") printCount String @default("单面印花") @map("print_count") 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, printCount, craft, logistics]) @@map("family_price_overrides") } // ---------- Users (admin authentication) ---------- enum Role { ADMIN } model User { id BigInt @id @default(autoincrement()) username String @unique passwordHash String @map("password_hash") role Role @default(ADMIN) // Bumped on logout / revocation; JWTs carrying an older version are rejected. tokenVersion Int @default(0) @map("token_version") createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6) @@map("users") } // ---------- Sync Logs ---------- enum SyncType { CATEGORIES PRODUCTS PRODUCT_DETAILS } enum SyncStatus { RUNNING SUCCESS FAILED } model SyncLog { id BigInt @id @default(autoincrement()) type SyncType status SyncStatus message String? startedAt DateTime @default(now()) @map("started_at") @db.Timestamptz(6) finishedAt DateTime? @map("finished_at") @db.Timestamptz(6) @@index([type, startedAt]) @@map("sync_logs") }