公开契约族化(前端只需知道款号/族): - GET /public/goods 一族一条(goodId=族ID,price=族起价,分页作用于分组后); 无族商品(自定义)不进任何公开端点(列表/首页/分类树/标签统计) - GET /public/goods/:goodId 仅认族 ID;公共字段取代表 Good,变体=全体成员并集, 尺码表/包装规格=族物化并集;旧 SDS 链接 ID 寻址 404 - priceMatrix 严格五维:尺码×颜色×印花数量×工艺×物流;维度来源改为链接级 标签(人工接管按人工标签),弃用原始 craftLabel;CUSTOM 成员尊重显式标签 - 名称派生补裸「单面/双面」写法(直喷双面→双面印花+直喷,18 条存量链接修复) - family_price_overrides 加 print_count 列(五键唯一),PUT/DELETE/校验五键化 - admin 编辑弹窗矩阵消费适配(SKU 列直读 printCount,成员格子三维匹配, 改价 payload 带 printCount) - 存量 339 族已全量重算;api 162/162、admin 22/22、双端构建绿
391 lines
16 KiB
Plaintext
391 lines
16 KiB
Plaintext
// 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")
|
||
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")
|
||
}
|