2026-04-17 12:50:06 +08:00

1701 lines
47 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.

// 新的 Prisma schema - Product-SKU 结构
// 针对电商企业的产品管理需求设计
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
}
// ============================================
// 产品分类
// ============================================
// 产品分类(动态管理)
model ProductCategory {
id String @id @default(cuid())
name String @unique // 分类名称:沙发、餐桌、书柜...
description String? @db.Text // 分类描述
icon String? // 图标(可选)
sortOrder Int @default(0) // 排序
isActive Boolean @default(true) // 是否启用
// 关联产品
products Product[]
planningTemplates PlanningTemplate[] @relation("PlanningTemplateCategory")
planningWorkspaces PlanningWorkspace[] @relation("PlanningWorkspaceCategory")
assetFamilies AssetFamily[] @relation("AssetFamilyCategory")
seriesTemplates SeriesTemplate[] @relation("SeriesTemplateCategory")
draftGroups DraftGroup[] @relation("DraftGroupCategory")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([sortOrder])
@@index([isActive])
}
// ============================================
// 产品管理(Product-SKU结构)
// ============================================
// 产品(型号/SPU)
model Product {
id String @id @default(cuid())
modelNumber String @unique // 型号,如 "A001"
name String // 产品名称,如 "现代简约沙发"
description String? @db.Text // 产品描述
// 产品分类(关联 ProductCategory)
categoryId String?
category ProductCategory? @relation(fields: [categoryId], references: [id])
// 3D模型相关(型号级别的基础模型)
modelUrl String? // 基础3D模型URL (glTF)
thumbnailUrl String? // 缩略图
// 归属信息(型号级别 - 多人协作)
createdBy String // 创建者
creator User @relation("ProductCreator", fields: [createdBy], references: [id])
// 协作者关系(多人可编辑)
collaboratorUsers ProductCollaborator[]
// 部门归属
departmentId String?
department Department? @relation(fields: [departmentId], references: [id])
// 可见性
visibility String @default("TEAM") // PRIVATE, TEAM, DEPARTMENT, PUBLIC
// SKU列表
skus ProductSKU[]
// 装配步骤(型号级别,所有SKU共享)
assemblySteps AssemblyStep[]
// 装配版本历史
assemblyVersions AssemblyVersion[]
// 板件(型号级别)
panels Panel[]
// 分享链接
shareLinks ShareLink[]
// 包装方案
packagingPlans PackagingPlan[]
// 问题反馈
feedbacks Feedback[]
// 五金件
productHardwares ProductHardware[]
// 元数据
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime? // 软删除
@@index([modelNumber])
@@index([categoryId])
@@index([createdBy])
@@index([departmentId])
}
// 产品SKU(库存单位)
model ProductSKU {
id String @id @default(cuid())
productId String // 关联的产品ID
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
// SKU标识
skuCode String @unique // SKU编码,如 "A001-RED-3S"
// 规格属性(灵活的JSON结构)
// 示例: { "color": "红色", "size": "三人座", "material": "布艺" }
attributes Json
// 价格和库存
price Decimal @db.Decimal(10, 2) // 售价
costPrice Decimal? @db.Decimal(10, 2) // 成本价
stock Int @default(0) // 库存数量
// 图片(SKU特定的图片)
images String[] // 图片URL数组
// 3D模型(如果SKU有特定的3D模型变体,否则使用Product的基础模型)
modelUrl String? // SKU特定的3D模型URL
// 归属信息(SKU 级别 - 单人负责)
ownerId String // 负责人ID
owner User @relation("SKUOwner", fields: [ownerId], references: [id])
// 交接记录
transferHistory Json? // [{ fromUserId, toUserId, transferredAt, reason, operatorId }]
// 状态
isActive Boolean @default(true) // 是否上架
// SKU 级装配步骤(与产品级步骤隔离)
assemblySteps AssemblyStep[]
// SKU 级板件 / 孔位 / 五金结构数据
panels Panel[]
holes Hole[]
productHardwares ProductHardware[]
sourcePreSkuId String? @unique
sourcePreSku PreSku? @relation("PreSkuFormalizedSku", fields: [sourcePreSkuId], references: [id], onDelete: SetNull)
warehouseLocationId String?
warehouseLocation WarehouseLocation? @relation("ProductSKUWarehouseLocation", fields: [warehouseLocationId], references: [id], onDelete: SetNull)
packagedItems PackagedItem[] @relation("PackagedItemSku")
// 元数据
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime? // 软删除
@@index([productId])
@@index([skuCode])
@@index([isActive])
@@index([ownerId])
@@index([warehouseLocationId])
}
// 产品协作者关系表
model ProductCollaborator {
productId String
userId String
role String @default("editor")
addedAt DateTime @default(now())
addedBy String?
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
user User @relation("ProductCollaboratorUser", fields: [userId], references: [id], onDelete: Cascade)
addedByUser User? @relation("ProductCollaboratorAddedBy", fields: [addedBy], references: [id], onDelete: SetNull)
@@id([productId, userId])
@@index([productId])
@@index([userId])
@@index([addedBy])
}
model IntakeNonIntegratedProduct {
id String @id
spu String
spuBase String @unique
brand String
designer String
operator String
purchaser String
payload Json
visibility String @default("DEPARTMENT") // PRIVATE, TEAM, DEPARTMENT, PUBLIC
departmentId String?
teamId String?
createdBy String
updatedBy String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
@@index([spu])
@@index([createdBy])
@@index([departmentId])
@@index([teamId])
@@index([visibility])
@@index([deletedAt, createdAt])
}
// ============================================
// 装配相关
// ============================================
// 装配步骤(默认产品级,可选绑定到 SKU)
model AssemblyStep {
id String @id @default(cuid())
productId String
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
skuId String?
sku ProductSKU? @relation(fields: [skuId], references: [id], onDelete: Cascade)
order Int // 步骤顺序
title String // 步骤标题
description String? @db.Text // 步骤说明
// ===== 步骤类型 =====
// panel=安装板件, hardware=安装五金件, custom=自定义
stepType String @default("panel")
// 关联的板件(stepType=panel 时)
panelId String?
// 关联的五金件 ID 列表(stepType=hardware 时)
hardwareIds String[]
// 动画数据(JSON)
partIds String[] // 参与的部件 ID(兼容旧数据)
transform Json // 目标变换 {position, rotation, scale}
duration Int @default(1500) // 动画时长(ms)
easing String @default("power2.inOut") // 缓动函数
// ===== 摄像机配置 =====
// { position: [x, y, z], target: [x, y, z] }
cameraPosition Json?
// smooth=平滑过渡, cut=直接切换, orbit=环绕
cameraTransition String @default("smooth")
// ===== 信息卡片配置 =====
showInfoCard Boolean @default(false)
// { title: string, items: [{ name, quantity, tips, thumbnailUrl }] }
infoCardData Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([productId, order])
@@index([productId, skuId, order])
@@index([skuId])
@@index([stepType])
}
// 装配版本历史(最多保存7个版本)
model AssemblyVersion {
id String @id @default(cuid())
productId String
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
version Int // 版本号,递增
snapshot Json // 完整的 steps 快照
description String? // 版本描述(如 "调整了门板安装顺序")
createdBy String? // 创建者 ID
createdAt DateTime @default(now())
@@unique([productId, version])
@@index([productId])
}
// 板件(属于Product)
model Panel {
id String @id @default(cuid())
productId String
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
skuId String?
sku ProductSKU? @relation(fields: [skuId], references: [id], onDelete: Cascade)
code String // 编号
name String // 名称
gltfNodeName String // glTF 节点名
partType String? // 部件类型:顶板/底板/左侧板/右侧板/背板/门板/抽屉/层板
// 尺寸
length Float
width Float
thickness Float
// 安装相关
installOrder Int? @default(0) // 安装序号
explosionDirection String? // 爆炸方向:auto/up/down/left/right/front/back
isAnchor Boolean @default(false) // 是否为基准件(不移动)
assemblySemantic Json? // 当前生效的装配语义(SKU scoped)
// 材质关联
panelMaterial PanelMaterial?
// 安装在此板件上的五金件
productHardwares ProductHardware[]
// 检测到的孔位
holes Hole[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([productId])
@@index([skuId])
@@index([productId, skuId])
@@index([installOrder])
}
// ============================================
// 分享功能
// ============================================
// 分享链接
model ShareLink {
id String @id @default(cuid())
productId String
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
token String @unique // 分享令牌
expiresAt DateTime? // 过期时间(可选)
viewCount Int @default(0) // 查看次数
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([productId])
@@index([token])
@@index([expiresAt]) // DB-006: 支持过期时间过滤查询
}
// 包装方案
model PackagingPlan {
id String @id @default(cuid())
productId String
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
name String // 方案名称,如 "标准包装"、"精装版"
description String? @db.Text // 方案描述
boxCount Int @default(1) // 箱数
// 箱子详情(JSON数组)
// [{ boxNumber: 1, length: 100, width: 50, height: 30, weight: 5.5, panelIds: ["P001", "P002"], notes: "..." }]
boxes Json @default("[]")
// 汇总信息
totalWeight Float? // 总重量(kg)
totalVolume Float? // 总体积(m³)
notes String? @db.Text // 备注
isDefault Boolean @default(false) // 是否为默认方案
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([productId])
}
// ============================================
// 产品规划域
// ============================================
model PlanningTemplate {
id String @id @default(cuid())
code String @unique
name String
description String? @db.Text
categoryId String?
category ProductCategory? @relation("PlanningTemplateCategory", fields: [categoryId], references: [id])
createdBy String?
creator User? @relation("PlanningTemplateCreator", fields: [createdBy], references: [id])
namePattern String? @db.Text
slots Json
rules Json?
isPreset Boolean @default(false)
isActive Boolean @default(true)
workspaces PlanningWorkspace[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([categoryId])
@@index([createdBy])
@@index([isPreset])
@@index([isActive])
}
model PlanningWorkspace {
id String @id @default(cuid())
name String
description String? @db.Text
templateId String
template PlanningTemplate @relation(fields: [templateId], references: [id], onDelete: Cascade)
categoryId String?
category ProductCategory? @relation("PlanningWorkspaceCategory", fields: [categoryId], references: [id])
createdBy String
creator User @relation("PlanningWorkspaceCreator", fields: [createdBy], references: [id])
departmentId String?
department Department? @relation("PlanningWorkspaceDepartment", fields: [departmentId], references: [id])
visibility String @default("TEAM")
slotSelections Json @default("{}")
metadata Json?
drafts PlanningDraft[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([templateId])
@@index([categoryId])
@@index([createdBy])
@@index([departmentId])
@@index([visibility])
}
model PlanningDraft {
id String @id @default(cuid())
workspaceId String
workspace PlanningWorkspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
name String
selectedCandidateKey String
selectedAssets Json
previewNodes Json
summary Json
warnings Json?
notes String? @db.Text
sourceSnapshot Json?
createdBy String
creator User @relation("PlanningDraftCreator", fields: [createdBy], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([workspaceId])
@@index([createdBy])
}
model Substrate {
id String @id @default(cuid())
code String @unique
name String
description String? @db.Text
categoryId String
category SubstrateCategory @relation(fields: [categoryId], references: [id])
environmentGradeId String
environmentGrade SubstrateEnvironmentGrade @relation(fields: [environmentGradeId], references: [id])
thicknessSpecId String
thicknessSpec SubstrateThicknessSpec @relation(fields: [thicknessSpecId], references: [id])
surfaceOptionId String?
surfaceOption SubstrateSurfaceOption? @relation(fields: [surfaceOptionId], references: [id])
systemNameOptionId String?
systemNameOption SubstrateSystemNameOption? @relation(fields: [systemNameOptionId], references: [id])
suggestedPrice Decimal? @db.Decimal(10, 2)
weightKg Float?
metadata Json?
isActive Boolean @default(true)
createdBy String?
creator User? @relation("SubstrateCreator", fields: [createdBy], references: [id], onDelete: SetNull)
assetVariants AssetVariant[]
finishes SubstrateFinish[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([isActive])
@@index([createdBy])
@@index([categoryId])
@@index([environmentGradeId])
@@index([thicknessSpecId])
@@index([surfaceOptionId])
@@index([systemNameOptionId])
// 4字段规格由服务层收口:相同规格+相同花色拒绝重复,相同规格+不同花色并入已有基材
}
model SubstrateFinish {
id String @id @default(cuid())
substrateId String
substrate Substrate @relation(fields: [substrateId], references: [id], onDelete: Cascade)
finishId String
finish Material @relation(fields: [finishId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
@@unique([substrateId, finishId])
@@index([substrateId])
@@index([finishId])
}
model SubstrateCategory {
id String @id @default(cuid())
name String @unique
sortOrder Int @default(0)
isActive Boolean @default(true)
substrates Substrate[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([sortOrder])
@@index([isActive])
}
model SubstrateEnvironmentGrade {
id String @id @default(cuid())
name String @unique
sortOrder Int @default(0)
isActive Boolean @default(true)
substrates Substrate[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([sortOrder])
@@index([isActive])
}
model SubstrateThicknessSpec {
id String @id @default(cuid())
name String @unique
thicknessMm Float
sheetLengthMm Float @default(2440)
sheetWidthMm Float @default(1220)
sortOrder Int @default(0)
isActive Boolean @default(true)
substrates Substrate[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([sortOrder])
@@index([isActive])
}
model SubstrateSurfaceOption {
id String @id @default(cuid())
name String @unique
sortOrder Int @default(0)
isActive Boolean @default(true)
substrates Substrate[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([sortOrder])
@@index([isActive])
}
model SubstrateSystemNameOption {
id String @id @default(cuid())
name String @unique
sortOrder Int @default(0)
isActive Boolean @default(true)
substrates Substrate[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([sortOrder])
@@index([isActive])
}
model AssetFamily {
id String @id @default(cuid())
type AssetType
code String @unique
name String
description String? @db.Text
productCategoryId String?
productCategory ProductCategory? @relation("AssetFamilyCategory", fields: [productCategoryId], references: [id])
applicableSeriesCodes String[] @default([])
connectionRoles String[] @default([])
metadata Json?
status AssetStatus @default(DRAFT)
createdBy String?
creator User? @relation("AssetFamilyCreator", fields: [createdBy], references: [id], onDelete: SetNull)
submittedAt DateTime?
submittedById String?
submitter User? @relation("AssetFamilySubmitter", fields: [submittedById], references: [id], onDelete: SetNull)
reviewedAt DateTime?
reviewedById String?
reviewer User? @relation("AssetFamilyReviewer", fields: [reviewedById], references: [id], onDelete: SetNull)
rejectedAt DateTime?
rejectedById String?
rejecter User? @relation("AssetFamilyRejecter", fields: [rejectedById], references: [id], onDelete: SetNull)
rejectionReason String? @db.Text
lastModifiedById String?
lastModifier User? @relation("AssetFamilyLastModifier", fields: [lastModifiedById], references: [id], onDelete: SetNull)
variants AssetVariant[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([type])
@@index([status])
@@index([productCategoryId])
@@index([createdBy])
@@index([submittedById])
@@index([reviewedById])
@@index([rejectedById])
@@index([lastModifiedById])
}
model AssetVariant {
id String @id @default(cuid())
assetFamilyId String
assetFamily AssetFamily @relation(fields: [assetFamilyId], references: [id], onDelete: Cascade)
code String @unique
name String
identityKey String
assetType AssetType
status AssetStatus @default(DRAFT)
sizeLabel String?
substrateId String?
substrate Substrate? @relation(fields: [substrateId], references: [id], onDelete: SetNull)
finishId String?
finish Material? @relation("AssetVariantFinish", fields: [finishId], references: [id], onDelete: SetNull)
dimensions Json
modelUrl String
thumbnailUrl String?
mountPoints Json
assemblyBasis Json?
compatibilityRules Json?
namingFields Json?
businessData Json?
weightKg Float?
costPrice Decimal? @db.Decimal(10, 2)
lossFactor Float?
estimatedCostPrice Decimal? @db.Decimal(10, 2)
inheritedFromVariantId String?
inheritedFromVariant AssetVariant? @relation("AssetVariantInheritance", fields: [inheritedFromVariantId], references: [id], onDelete: SetNull)
derivedVariants AssetVariant[] @relation("AssetVariantInheritance")
locationBindings AssetVariantLocationBinding[]
createdBy String?
creator User? @relation("AssetVariantCreator", fields: [createdBy], references: [id], onDelete: SetNull)
submittedAt DateTime?
submittedById String?
submitter User? @relation("AssetVariantSubmitter", fields: [submittedById], references: [id], onDelete: SetNull)
reviewedAt DateTime?
reviewedById String?
reviewer User? @relation("AssetVariantReviewer", fields: [reviewedById], references: [id], onDelete: SetNull)
rejectedAt DateTime?
rejectedById String?
rejecter User? @relation("AssetVariantRejecter", fields: [rejectedById], references: [id], onDelete: SetNull)
rejectionReason String? @db.Text
lastModifiedById String?
lastModifier User? @relation("AssetVariantLastModifier", fields: [lastModifiedById], references: [id], onDelete: SetNull)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([assetFamilyId, identityKey])
@@index([assetFamilyId])
@@index([status])
@@index([substrateId])
@@index([finishId])
@@index([createdBy])
@@index([submittedById])
@@index([reviewedById])
@@index([rejectedById])
@@index([lastModifiedById])
@@index([inheritedFromVariantId])
}
model SeriesTemplate {
id String @id @default(cuid())
code String @unique
name String
seriesName String
description String? @db.Text
categoryId String?
category ProductCategory? @relation("SeriesTemplateCategory", fields: [categoryId], references: [id])
slotSchema Json
namingRule Json?
recommendationRule Json?
previewRule Json?
intakeRule Json?
isPreset Boolean @default(false)
isActive Boolean @default(true)
createdBy String?
creator User? @relation("SeriesTemplateCreator", fields: [createdBy], references: [id], onDelete: SetNull)
draftGroups DraftGroup[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([categoryId])
@@index([createdBy])
@@index([isPreset])
@@index([isActive])
}
model DraftGroup {
id String @id @default(cuid())
code String @unique
name String
description String? @db.Text
categoryId String?
category ProductCategory? @relation("DraftGroupCategory", fields: [categoryId], references: [id])
seriesTemplateId String?
seriesTemplate SeriesTemplate? @relation(fields: [seriesTemplateId], references: [id], onDelete: SetNull)
status DraftGroupStatus @default(DRAFT)
summary Json
notes String? @db.Text
createdBy String
creator User @relation("DraftGroupCreator", fields: [createdBy], references: [id])
preSkus PreSku[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([categoryId])
@@index([seriesTemplateId])
@@index([createdBy])
@@index([status])
}
model PreSku {
id String @id @default(cuid())
draftGroupId String
draftGroup DraftGroup @relation(fields: [draftGroupId], references: [id], onDelete: Cascade)
sequence Int
defaultName String
customName String?
status PreSkuStatus @default(DRAFT)
sourceCandidateKey String
familySelection Json
variantSelection Json
recommendedHardware Json?
previewNodes Json
displaySummary Json
bomPreview Json
weightPreview Float?
componentCodes String[]
explosionPreview Json?
installStepPreview Json
packagingSnapshot Json?
notes String? @db.Text
packagingPlan PreSkuPackagingPlan?
formalizedSku ProductSKU? @relation("PreSkuFormalizedSku")
locationBindings PreSkuLocationBinding[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([draftGroupId, sequence])
@@index([draftGroupId])
@@index([status])
}
model PreSkuPackagingPlan {
id String @id @default(cuid())
preSkuId String @unique
preSku PreSku @relation(fields: [preSkuId], references: [id], onDelete: Cascade)
sourceType PackagingSourceType @default(PRE_SKU)
sourceRefId String?
status String @default("DRAFT")
boxes Json @default("[]")
costSummary Json?
processDraft Json?
notes String? @db.Text
packagedItems PackagedItem[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([sourceType])
}
model WarehouseLocation {
id String @id @default(cuid())
code String @unique
name String
zone String?
aisle String?
shelf String?
bin String?
notes String? @db.Text
isActive Boolean @default(true)
skus ProductSKU[] @relation("ProductSKUWarehouseLocation")
packagedItems PackagedItem[] @relation("PackagedItemWarehouseLocation")
assetVariantBindings AssetVariantLocationBinding[]
brandMaterialBindings BrandMaterialBinding[]
preSkuBindings PreSkuLocationBinding[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([isActive])
}
model AssetVariantLocationBinding {
id String @id @default(cuid())
assetVariantId String
warehouseLocationId String
assetVariant AssetVariant @relation(fields: [assetVariantId], references: [id], onDelete: Cascade)
warehouseLocation WarehouseLocation @relation(fields: [warehouseLocationId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
@@unique([assetVariantId, warehouseLocationId])
@@index([assetVariantId])
@@index([warehouseLocationId])
}
model BrandMaterialBinding {
id String @id @default(cuid())
brand String
materialType String
warehouseLocationId String?
warehouseLocation WarehouseLocation? @relation(fields: [warehouseLocationId], references: [id], onDelete: SetNull)
fileUrl String?
fileName String?
fileSize Int?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([brand, materialType])
@@index([brand])
@@index([warehouseLocationId])
}
model PreSkuLocationBinding {
id String @id @default(cuid())
preSkuId String
warehouseLocationId String
preSku PreSku @relation(fields: [preSkuId], references: [id], onDelete: Cascade)
warehouseLocation WarehouseLocation @relation(fields: [warehouseLocationId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
@@unique([preSkuId, warehouseLocationId])
@@index([preSkuId])
@@index([warehouseLocationId])
}
model PackagedItem {
id String @id @default(cuid())
code String @unique
name String
preSkuPackagingPlanId String?
preSkuPackagingPlan PreSkuPackagingPlan? @relation(fields: [preSkuPackagingPlanId], references: [id], onDelete: SetNull)
skuId String?
sku ProductSKU? @relation("PackagedItemSku", fields: [skuId], references: [id], onDelete: SetNull)
warehouseLocationId String?
warehouseLocation WarehouseLocation? @relation("PackagedItemWarehouseLocation", fields: [warehouseLocationId], references: [id], onDelete: SetNull)
quantityPerPack Int @default(1)
stockQuantity Int @default(0)
length Float?
width Float?
height Float?
weight Float?
payload Json?
status String @default("DRAFT")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([preSkuPackagingPlanId])
@@index([skuId])
@@index([warehouseLocationId])
}
// ============================================
// 用户管理
// ============================================
// 角色枚举
enum Role {
SUPER_ADMIN // 超级管理员
ADMIN // 管理员
SUPERVISOR // 主管(按部门/小组管理锚点区分范围)
DESIGNER // 设计师
USER // 普通用户
}
// 用户状态枚举
enum UserStatus {
ACTIVE // 在职
PENDING // 待激活
INACTIVE // 离职
SUSPENDED // 暂停
}
enum EmployeeSourceStatus {
EMPLOYED
DIMISSIONED
}
enum AssetType {
BOARD
COMPONENT
HARDWARE
PACKAGING
}
enum AssetStatus {
DRAFT
PENDING_REVIEW
REJECTED
AVAILABLE
DISABLED
}
enum DraftGroupStatus {
DRAFT
PACKAGING
READY_FOR_INTAKE
FORMALIZED
ARCHIVED
}
enum PreSkuStatus {
DRAFT
PREVIEW_CONFIRMED
FORMALIZED
ARCHIVED
}
enum PackagingSourceType {
PRODUCT
PRE_SKU
DRAFT_GROUP
}
// 用户
model User {
id String @id @default(cuid())
// 登录凭证
email String @unique // 邮箱(用于登录)
name String @unique // 姓名/花名(用于登录和显示)
password String
loginSource String @default("LOCAL")
dingtalkUserId String? @unique
dingtalkUnionId String? @unique
dingtalkBoundAt DateTime?
employeeDirectoryId String? @unique
employeeDirectory EmployeeDirectory? @relation(fields: [employeeDirectoryId], references: [id], onDelete: SetNull)
// 首次登录标识
isFirstLogin Boolean @default(true) // 超级管理员首次登录需强制修改信息
// 其他信息
avatar String?
phone String?
// 角色与权限
role Role @default(USER)
// 组织架构
departmentId String?
department Department? @relation("DepartmentMembers", fields: [departmentId], references: [id])
teamId String?
team Team? @relation("TeamMembers", fields: [teamId], references: [id])
// 管理关系
managedDepartment Department? @relation("DepartmentManager")
managedTeam Team? @relation("TeamLeader")
// 状态
status UserStatus @default(PENDING)
// 关联
createdProducts Product[] @relation("ProductCreator")
ownedSKUs ProductSKU[] @relation("SKUOwner")
createdPlanningTemplates PlanningTemplate[] @relation("PlanningTemplateCreator")
createdPlanningWorkspaces PlanningWorkspace[] @relation("PlanningWorkspaceCreator")
createdPlanningDrafts PlanningDraft[] @relation("PlanningDraftCreator")
createdSubstrates Substrate[] @relation("SubstrateCreator")
createdAssetFamilies AssetFamily[] @relation("AssetFamilyCreator")
submittedAssetFamilies AssetFamily[] @relation("AssetFamilySubmitter")
reviewedAssetFamilies AssetFamily[] @relation("AssetFamilyReviewer")
rejectedAssetFamilies AssetFamily[] @relation("AssetFamilyRejecter")
modifiedAssetFamilies AssetFamily[] @relation("AssetFamilyLastModifier")
createdAssetVariants AssetVariant[] @relation("AssetVariantCreator")
submittedAssetVariants AssetVariant[] @relation("AssetVariantSubmitter")
reviewedAssetVariants AssetVariant[] @relation("AssetVariantReviewer")
rejectedAssetVariants AssetVariant[] @relation("AssetVariantRejecter")
modifiedAssetVariants AssetVariant[] @relation("AssetVariantLastModifier")
createdSeriesTemplates SeriesTemplate[] @relation("SeriesTemplateCreator")
createdDraftGroups DraftGroup[] @relation("DraftGroupCreator")
collaboratedProducts ProductCollaborator[] @relation("ProductCollaboratorUser")
addedProductCollaborators ProductCollaborator[] @relation("ProductCollaboratorAddedBy")
feedbacks Feedback[] @relation("FeedbackSubmitter")
assignedFeedbacks Feedback[] @relation("FeedbackAssignee")
// 操作日志
activityLogs ActivityLog[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
lastLoginAt DateTime?
@@index([email])
@@index([name])
@@index([departmentId])
@@index([teamId])
@@index([status])
@@index([loginSource])
@@index([employeeDirectoryId])
}
// 部门
model Department {
id String @id @default(cuid())
name String @unique
fullPath String // 完整路径,如 "设计部-3D组-建模小组"
externalId String? @unique
syncedAt DateTime?
rawSnapshot Json?
// 层级关系
parentId String?
parent Department? @relation("DepartmentTree", fields: [parentId], references: [id])
children Department[] @relation("DepartmentTree")
// 部门负责人
managerId String? @unique
manager User? @relation("DepartmentManager", fields: [managerId], references: [id])
// 成员
members User[] @relation("DepartmentMembers")
// 小组
teams Team[]
// 产品
products Product[]
planningWorkspaces PlanningWorkspace[] @relation("PlanningWorkspaceDepartment")
// 排序
orderIndex Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([parentId])
@@index([externalId])
}
// 小组
model Team {
id String @id @default(cuid())
name String
description String?
// 所属部门
departmentId String
department Department @relation(fields: [departmentId], references: [id], onDelete: Cascade)
// 组长
leaderId String? @unique
leader User? @relation("TeamLeader", fields: [leaderId], references: [id])
// 成员
members User[] @relation("TeamMembers")
// 排序
orderIndex Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([departmentId, name])
@@index([departmentId])
}
model EmployeeDirectory {
id String @id @default(cuid())
dingtalkUserId String? @unique
dingtalkUnionId String? @unique
jobNumber String?
name String
mobile String?
email String?
title String?
sourceStatus EmployeeSourceStatus @default(EMPLOYED)
primaryDepartmentExternalId String?
departmentExternalIds Json?
syncedAt DateTime @default(now())
rawSnapshot Json?
user User?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([name])
@@index([jobNumber])
@@index([mobile])
@@index([email])
@@index([sourceStatus])
@@index([primaryDepartmentExternalId])
}
// ============================================
// 问题反馈
// ============================================
// 反馈状态枚举
enum FeedbackStatus {
PENDING // 待处理
IN_PROGRESS // 处理中
RESOLVED // 已解决
CLOSED // 已关闭
}
// 反馈优先级枚举
enum FeedbackPriority {
LOW // 低
MEDIUM // 中
HIGH // 高
URGENT // 紧急
}
// 问题反馈
model Feedback {
id String @id @default(cuid())
// 关联产品
productId String
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
skuId String? // 可选:具体的 SKU
// 反馈内容
title String
description String @db.Text
images String[] // 问题截图
// 问题分类
category String // 模型错误、尺寸错误、材质错误、装配步骤错误等
priority FeedbackPriority @default(MEDIUM)
// 提交者
submitterId String
submitter User @relation("FeedbackSubmitter", fields: [submitterId], references: [id])
// 处理者
assigneeId String?
assignee User? @relation("FeedbackAssignee", fields: [assigneeId], references: [id])
// 状态
status FeedbackStatus @default(PENDING)
// 处理记录
comments Json? // [{ userId, content, createdAt }]
// 解决方案
resolution String? @db.Text
resolvedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([productId])
@@index([submitterId])
@@index([assigneeId])
@@index([status])
}
// ============================================
// 操作日志
// ============================================
// 操作类型枚举
enum ActivityType {
USER_REGISTER
USER_LOGIN
USER_LOGIN_FAILED
USER_LOGOUT
USER_FIRST_LOGIN_UPDATE
USER_CREATE
USER_UPDATE
USER_DELETE
PRODUCT_CREATE
PRODUCT_UPDATE
PRODUCT_DELETE
SKU_CREATE
SKU_UPDATE
SKU_DELETE
SKU_TRANSFER
FILE_UPLOAD
FILE_DOWNLOAD
SHARE_LINK_CREATE
SHARE_LINK_ACCESS
FEEDBACK_CREATE
FEEDBACK_UPDATE
DATA_EXPORT
}
// 操作日志
model ActivityLog {
id String @id @default(cuid())
// 操作者
userId String
user User @relation(fields: [userId], references: [id])
// 操作类型
type ActivityType
// 操作对象
targetType String? // Product, ProductSKU, User, Feedback
targetId String?
// 操作详情
action String // 具体操作描述
changes Json? // 变更内容
metadata Json? // 额外信息(IP、设备等)
// 时间
createdAt DateTime @default(now())
@@index([userId, createdAt])
@@index([targetType, targetId])
@@index([type, createdAt])
@@index([createdAt]) // DB-005: 支持时间范围过滤查询
}
// ============================================
// 材质系统
// ============================================
// 材质分类
model MaterialCategory {
id String @id @default(cuid())
name String @unique // 分类名称:实木/金属/玻璃/亚克力/皮革/布艺/石材
icon String? // 分类图标
sortOrder Int @default(0) // 排序
isActive Boolean @default(true) // 是否启用
// 关联材质
materials Material[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([sortOrder])
@@index([isActive])
}
// 材质库(由专业人员管理)
model Material {
id String @id @default(cuid())
// 基本信息
name String // 材质名称,如 "橡木纹-自然色"
code String @unique // 花色编码
categoryId String // 分类 ID
category MaterialCategory @relation(fields: [categoryId], references: [id])
// 预览图(可选;没有真实缩略图时由服务层统一生成预览)
thumbnailUrl String?
// 纹理贴图
textureUrl String? // 纹理贴图 URL
normalMapUrl String? // 法线贴图(可选)
// PBR 参数
color String @default("#808080") // 基础颜色 #RRGGBB
metalness Float @default(0) // 金属度 0-1
roughness Float @default(0.5) // 粗糙度 0-1
opacity Float @default(1) // 透明度 0-1
// 纹理参数
textureRepeat Float @default(1) // 纹理重复次数
textureRotation Float @default(0) // 纹理旋转角度(弧度)
textureSizeMm Float @default(600) // 纹理实物宽度(毫米)
// 管理标识
isPreset Boolean @default(false) // 是否系统预设(预设不可删除)
isActive Boolean @default(true) // 是否启用
// 创建者
createdBy String? // 创建者 ID
// 关联板件材质
panelMaterials PanelMaterial[]
assetVariants AssetVariant[] @relation("AssetVariantFinish")
substrateFinishes SubstrateFinish[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([categoryId])
@@index([isPreset])
@@index([isActive])
}
// 板件-材质关联(普通用户操作)
model PanelMaterial {
id String @id @default(cuid())
// 关联板件
panelId String
panel Panel @relation(fields: [panelId], references: [id], onDelete: Cascade)
// 关联材质
materialId String
material Material @relation(fields: [materialId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([panelId]) // 每个板件只能有一个材质
@@index([panelId])
@@index([materialId])
}
// ============================================
// 主题配置(管理员可视化配置)
// ============================================
// 主题配置
model ThemeConfig {
id String @id @default(cuid())
// 基本信息
name String // 主题名称,如 "春节主题"、"默认主题"
description String? @db.Text // 主题描述
// 激活状态
isActive Boolean @default(false) // 是否激活
isDefault Boolean @default(false) // 是否为默认主题(系统 fallback)
// 定时发布
startDate DateTime? // 定时生效开始时间
endDate DateTime? // 定时生效结束时间
// 首页背景配置
// { type: 'image' | 'gradient' | 'video', value: string, overlay?: string }
homeBackground Json?
// 登录页右侧面板配置
// { type: 'image' | 'video' | 'animation', value: string, overlay?: string, animationConfig?: object }
loginPanel Json?
// 创建者
createdBy String
// 元数据
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([isActive])
@@index([startDate, endDate])
}
// ============================================
// 五金件库(标准五金件复用系统)
// ============================================
// 五金件分类
model HardwareCategory {
id String @id @default(cuid())
name String @unique // 分类名称:合页、螺丝、导轨、拉手...
description String? @db.Text // 分类描述
icon String? // 图标(emoji 或 URL)
sortOrder Int @default(0) // 排序顺序
isActive Boolean @default(true) // 是否启用
// 关联五金件
hardwares Hardware[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([sortOrder])
@@index([isActive])
}
// 五金件(标准件库)
model Hardware {
id String @id @default(cuid())
// 基本信息
name String // 五金件名称,如 "直角合页 50mm"
code String @unique // 编码,如 "HW-HINGE-001"
// 分类
categoryId String
category HardwareCategory @relation(fields: [categoryId], references: [id])
// 规格参数(JSON)
// { size: "50x50mm", material: "不锈钢", weight: "25g", ... }
specifications Json?
// 3D 模型
modelUrl String // GLB 模型文件路径
thumbnailUrl String? // 缩略图
// 尺寸信息(用于碰撞检测和定位)
dimensions Json? // { width, height, depth, unit: "mm" }
// 几何特征(用于自动匹配,从 CAD 导入时自动计算)
// 尺寸已排序:width >= height >= depth
geoWidth Float? // 边界框宽度 (mm) - 最大尺寸
geoHeight Float? // 边界框高度 (mm) - 中间尺寸
geoDepth Float? // 边界框深度 (mm) - 最小尺寸
geoVolume Float? // 体积 (mm³)
// 安装方向配置(用于动画生成)
// 五金件模型必须以标准轴向上传,此字段记录"头"的默认朝向
defaultHeadDirection String? // "+X" | "-X" | "+Y" | "-Y" | "+Z" | "-Z"
// 安装提示(显示在信息卡片上)
installTip String? @db.Text // "从上往下拧入" | "需要预钻孔"
// ===== 组合件相关 =====
// 是否为组合件主件(如把手、铰链等需要配套螺丝的五金件)
isComposite Boolean @default(false)
// 配件定义(仅当 isComposite=true 时有效)
// 示例: [{ hardwareCode: "M4x25", quantity: 2, relativePositionTolerance: 50 }]
// hardwareCode: 配件的五金件编码
// quantity: 需要的数量
// relativePositionTolerance: 允许的位置偏差(mm),用于自动关联
accessories Json?
// 安装点配置(用于自动对齐)
// { points: [{ name: "center", position: [0, 0, 0], rotation: [0, 0, 0] }] }
mountPoints Json?
// 材质预设(引用材质 ID)
defaultMaterialId String?
// 管理标识
isPreset Boolean @default(false) // 是否系统预设
isActive Boolean @default(true) // 是否启用
// 创建者
createdBy String?
// 关联产品五金件
productHardwares ProductHardware[]
// 关联的孔位(匹配后)
holes Hole[]
// 映射关系(用于自动匹配记忆)
hardwareMappings HardwareMapping[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([categoryId])
@@index([code])
@@index([isPreset])
@@index([isActive])
@@index([geoWidth, geoHeight, geoDepth]) // 几何特征索引,加速匹配查询
}
// 产品-五金件关联(记录产品使用的五金件及其位置)
model ProductHardware {
id String @id @default(cuid())
// 关联产品
productId String
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
skuId String?
sku ProductSKU? @relation(fields: [skuId], references: [id], onDelete: Cascade)
// 关联五金件(可为空,未匹配时为 null)
hardwareId String?
hardware Hardware? @relation(fields: [hardwareId], references: [id])
// 位置信息
// { position: [x, y, z], rotation: [rx, ry, rz], scale: [sx, sy, sz] }
transform Json
// 关联的板件(可选,指定安装在哪个板件上)
panelId String?
panel Panel? @relation(fields: [panelId], references: [id])
// 连接的第二个板件(五金件通常连接两个板材)
connectedPanelId String?
// ===== 孔位关联 =====
// 关联的孔位 ID(从 CAD 导入时自动关联)
associatedHoleId String?
// 与孔位的距离(mm),用于验证关联质量
holeDistance Float?
// ===== 组合件关联 =====
// 如果此五金件是某个组合件的配件,记录主件的 ID
parentHardwareId String?
// 是否为配件(属于某个组合件)
isAccessory Boolean @default(false)
// 安装序号(用于动画)
installOrder Int?
// ===== 匹配状态相关字段 =====
// 匹配状态:matched=已匹配(绿), pending=待确认(黄), unmatched=未匹配(红)
matchStatus String @default("unmatched")
// 匹配置信度 0-1
matchConfidence Float?
// 匹配方法:blockName=图块名, mapping=映射记忆, size=尺寸匹配, manual=手动选择
matchMethod String?
// CAD 导入时的原始信息(用于匹配和调试)
sourceBlockName String? // CAD 中的图块名
sourceSignature Json? // 几何特征签名 { width, height, depth }
// 备注
notes String? @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([productId])
@@index([skuId])
@@index([hardwareId])
@@index([panelId])
@@index([matchStatus])
}
// 五金件映射关系(用于自动匹配记忆)
model HardwareMapping {
id String @id @default(cuid())
// 匹配条件(二选一)
blockName String? @unique // 图块名匹配(精确)
signatureHash String? @unique // 尺寸特征哈希(width_height_depth)
// 映射目标
hardwareId String
hardware Hardware @relation(fields: [hardwareId], references: [id])
// 元数据
createdBy String @default("manual") // "auto" | "manual"
usageCount Int @default(0) // 使用次数,越高优先级越高
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([hardwareId])
}
// 通用组件分类
model CommonComponentCategory {
id String @id @default(cuid())
name String @unique
description String? @db.Text
icon String?
sortOrder Int @default(0)
isActive Boolean @default(true)
components CommonComponent[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([sortOrder])
@@index([isActive])
}
// 通用组件(抽屉、门组件、柜脚组件等)
model CommonComponent {
id String @id @default(cuid())
name String
code String @unique
categoryId String
category CommonComponentCategory @relation(fields: [categoryId], references: [id])
description String? @db.Text
modelUrl String
thumbnailUrl String?
dimensions Json?
specifications Json?
mountPoints Json?
isPreset Boolean @default(false)
isActive Boolean @default(true)
createdBy String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([categoryId])
@@index([code])
@@index([isPreset])
@@index([isActive])
}
// ============================================
// 孔位检测系统
// ============================================
// 检测到的孔位
model Hole {
id String @id @default(cuid())
// 关联板件
panelId String
panel Panel @relation(fields: [panelId], references: [id], onDelete: Cascade)
skuId String?
sku ProductSKU? @relation(fields: [skuId], references: [id], onDelete: Cascade)
// 孔位信息(相对于板件坐标系)
positionX Float // X 坐标 (mm)
positionY Float // Y 坐标 (mm)
positionZ Float // Z 坐标 (mm)
diameter Float // 直径 (mm)
depth Float // 深度 (mm)
// 孔轴方向(法线)
normalX Float @default(0)
normalY Float @default(0)
normalZ Float @default(1)
// 孔类型(自动推断或手动设置)
// hinge, connector, shelf_pin, screw, dowel, cable, unknown
holeType String?
// 检测置信度 0-1
confidence Float @default(1)
// 是否手动修正
isManual Boolean @default(false)
// 关联五金件(匹配后)
hardwareId String?
hardware Hardware? @relation(fields: [hardwareId], references: [id])
// 关联的孔组 ID(同类孔分组)
groupId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([panelId])
@@index([skuId])
@@index([panelId, skuId])
@@index([holeType])
@@index([hardwareId])
@@index([groupId])
}