Files
inkreach-official-website/apps/api/prisma/schema.prisma
T
yeuimu 1e07c62fc8 feat(admin): country drag-sort + always-available member detail sync
- Country 加 sort_order(默认 0 保持 id 序);PATCH /countries/sort 批量保存
  顺序(对齐 /tags/sort 模式);findAll 与公开 /public/countries 均按
  sortOrder 排序
- CountriesView 表格改为可拖拽行列表:拖动松开即全量保存新顺序,失败回滚
- 商品编辑弹窗成员展开面板:同步详情按钮常驻(已同步显示 重新同步详情),
  不再只在未同步态出现
- goods.service.spec 的 FamilyRecomputeService mock 补齐 syncFamilyTags 等
  方法(全量并行时其他套件的扫名归族会把本套件夹具收进族,create/update
  会调用到,mock 缺方法导致偶发 TypeError)
- api 164/164、admin typecheck+22/22+构建全绿
2026-08-28 20:00:56 +08:00

392 lines
16 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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")
}