3246 lines
108 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")
assetSeries AssetSeries[] @relation("AssetSeriesCategory")
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"
name String? // SKU名称
// 规格属性(灵活的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) // 是否上架
originalSkuCode String?
deletedCodeAt DateTime?
deletedByProductId String?
productDeletedAt DateTime?
// SKU 级装配步骤(与产品级步骤隔离)
assemblySteps AssemblyStep[]
// SKU 级板件 / 孔位 / 五金结构数据
panels Panel[]
holes Hole[]
productHardwares ProductHardware[]
sourcePreSkuId String? @unique
sourcePreSku PreSku? @relation("PreSkuFormalizedSku", fields: [sourcePreSkuId], references: [id], onDelete: SetNull)
sourceAssetVariantId String?
sourceAssetVariant AssetVariant? @relation("ProductSKUSourceAssetVariant", fields: [sourceAssetVariantId], references: [id], onDelete: SetNull)
sourceAssetVariantIds String[] @default([])
assetProfile ProductSkuAssetProfile?
warehouseLocationId String?
warehouseLocation WarehouseLocation? @relation("ProductSKUWarehouseLocation", fields: [warehouseLocationId], references: [id], onDelete: SetNull)
packagedItems PackagedItem[] @relation("PackagedItemSku")
qualityInspections ProductSkuQualityInspection[] @relation("ProductSKUQualityInspections")
// 元数据
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime? // 软删除
@@index([productId])
@@index([skuCode])
@@index([name])
@@index([isActive])
@@index([ownerId])
@@index([sourceAssetVariantId])
@@index([sourceAssetVariantIds], type: Gin)
@@index([warehouseLocationId])
}
// 正式 SKU 的资产资料结构化同步副本
model ProductSkuAssetProfile {
id String @id @default(cuid())
skuId String @unique
sku ProductSKU @relation(fields: [skuId], references: [id], onDelete: Cascade)
primarySourceAssetVariantId String?
sourceAssetVariantIds String[] @default([])
sourceAssetFamilyId String?
sourceAssetSeriesId String?
model String?
modelName String?
specCode String?
specName String?
barcode String?
size Json
color String?
material String?
imageUrl String?
sourceStatus ProductSkuAssetSourceStatus @default(ACTIVE)
skuDeletedAt DateTime?
syncedAt DateTime
lockedFields String[] @default([])
lockedReasons Json
sourceVariants Json
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([primarySourceAssetVariantId])
@@index([sourceAssetVariantIds], type: Gin)
@@index([sourceAssetFamilyId])
@@index([sourceAssetSeriesId])
@@index([specCode])
@@index([barcode])
@@index([sourceStatus])
@@index([skuDeletedAt])
}
// 产品协作者关系表
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
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?
approvalWorkflows ApprovalWorkflow[]
skuItems IntakeNonIntegratedSkuItem[]
pricingRows IntakeNonIntegratedPricingRow[]
productLimitRows IntakeNonIntegratedProductLimitRow[]
profile IntakeNonIntegratedProductProfile?
modelAssets IntakeProductModelAsset[]
@@index([spu])
@@index([spuBase])
@@index([createdBy])
@@index([departmentId])
@@index([teamId])
@@index([visibility])
@@index([deletedAt, createdAt])
}
model BarcodePrintImportRecord {
id String @id @default(cuid())
kind String
// System-created barcode rows point to their pricing-row source. Imported
// rows keep this null, so identical Excel rows remain independently valid.
sourcePricingRowId String? @unique
skuCode String
normalizedSku String
supplierCode String @default("")
supplierName String @default("")
normalizedSupplierName String @default("")
sourceFileName String @default("")
rowNumber Int @default(0)
importedAt DateTime @default(now())
updates Json
createdBy String
creator User @relation("BarcodePrintImportRecordCreator", fields: [createdBy], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([kind])
@@index([normalizedSku])
@@index([supplierCode])
@@index([normalizedSupplierName])
@@index([createdBy])
@@index([createdAt])
@@index([kind, createdAt, id])
@@index([kind, normalizedSupplierName, createdAt, id])
@@index([kind, supplierCode, createdAt, id])
@@index([kind, normalizedSku, createdAt, id])
}
model IntakeNonIntegratedProductProfile {
id String @id @default(cuid())
productId String @unique
product IntakeNonIntegratedProduct @relation(fields: [productId], references: [id], onDelete: Cascade)
productName String @default("")
productImageUrl String? @db.Text
productCategory String @default("")
salesCategory String @default("")
productYear String @default("")
productStatus String @default("")
productDevelopmentKind String @default("")
goodsType String @default("")
materialType String @default("")
productionType String @default("")
size String @default("")
color String @default("")
tagMaterial String @default("")
priceBand String @default("")
demandFlowNo String @default("")
processTemplate String @default("")
productPlanning String @default("")
brandPlanning String @default("")
demandDepartment String @default("")
spuOwner String @default("")
operationsOwner String @default("")
purchaseOwner String @default("")
visualDirector String @default("")
visualPlanning String @default("")
structureDesigner String @default("")
sourceSpu String @default("")
sourceSkuId String @default("")
sourceCategory String @default("")
sourceItemType String @default("")
sourceSalesStatus String @default("")
sourceEnabled String @default("")
sourceStockDisabled String @default("")
stockSkuCount Int @default(0)
stockHasOnSaleSku Boolean @default(false)
source String @default("PAYLOAD_SYNC")
syncedAt DateTime @default(now())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([productName])
@@index([productCategory])
@@index([salesCategory])
@@index([productStatus])
@@index([productDevelopmentKind])
@@index([spuOwner])
@@index([purchaseOwner])
@@index([sourceSpu])
@@index([sourceSkuId])
@@index([sourceCategory])
@@index([sourceItemType])
@@index([sourceSalesStatus])
@@index([sourceEnabled])
}
model IntakeNonIntegratedSkuItem {
id String @id
productId String
product IntakeNonIntegratedProduct @relation(fields: [productId], references: [id], onDelete: Cascade)
pricingRow IntakeNonIntegratedPricingRow? @relation("IntakeNonIntegratedPricingSkuItem")
productLimitRow IntakeNonIntegratedProductLimitRow? @relation("IntakeNonIntegratedLimitSkuItem")
skuItemId String
sortOrder Int
skuSequence Int @default(0)
batchNo Int @default(1)
demandFlowNo String @default("")
purchaseOwner String @default("")
packageGroupId String @default("")
setCount String @default("1")
image String? @db.Text
imageName String @default("")
model String @default("")
skuCode String @default("")
specName String @default("")
barcode String @default("")
size String @default("")
color String @default("")
material String @default("")
boardMaterialType String @default("")
versionNo String @default("V01")
attributeCode String @default("")
drawingId String @default("")
installMethod String @default("")
executeStandard String @default("")
nationalStandardCompliant String @default("")
manufacturer String @default("")
manufacturerCode String @default("")
factoryWarehouse String @default("")
shippingMethod String @default("")
purchaseOrderMethod String @default("")
minimumOrderQuantity String @default("")
taxIncludedUnitPrice String @default("")
leadTime String @default("")
sampleWeightKg Decimal? @db.Decimal(18, 3)
sampleLengthCm Decimal? @db.Decimal(18, 3)
sampleWidthCm Decimal? @db.Decimal(18, 3)
sampleHeightCm Decimal? @db.Decimal(18, 3)
salesStatus String @default("")
tagMaterial String @default("")
model3dDataUrl String? @db.Text
source String @default("")
sourceTable String @default("")
sourceSkuId String @default("")
sourceWeight Decimal? @db.Decimal(18, 3)
sourceBrand String @default("")
sourceCreatedAt DateTime?
sourceUpdatedAt DateTime?
contentLocked Boolean @default(false)
dataQualityStatus IntakeNonIntegratedSkuDataQualityStatus @default(INCOMPLETE)
missingRequiredFields String[] @default([])
aiProfile IntakeSkuAiProfile?
aiRenders AssistedAssemblyAiRender[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([productId, skuItemId])
@@index([productId])
@@index([productId, purchaseOwner])
@@index([demandFlowNo])
@@index([skuCode])
@@index([barcode])
@@index([model])
@@index([dataQualityStatus])
@@index([sourceTable, sourceSkuId])
}
model ProductArchiveImageEmbedding {
id String @id @default(cuid())
archiveId String @unique
archive SupplyChainProductArchive @relation(fields: [archiveId], references: [id], onDelete: Cascade)
imageUrl String @db.Text
imageHash String @db.VarChar(64)
embedding Bytes
dimensions Int
modelVersion String @db.VarChar(120)
status String @default("READY") @db.VarChar(24)
errorMessage String? @db.Text
indexedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([status])
@@index([imageHash])
}
model SupplyChainProductArchive {
id String @id @default(cuid())
barcode String @unique
spu String @default("")
sku String @default("")
image String? @db.Text
businessCreatedAt DateTime
categoryL1 String @default("")
categoryL2 String @default("")
productName String @default("")
designer String @default("")
productStatus String @default("")
material String @default("")
color String @default("")
weight Decimal? @db.Decimal(18, 3)
length Decimal? @db.Decimal(18, 3)
width Decimal? @db.Decimal(18, 3)
height Decimal? @db.Decimal(18, 3)
volume Decimal? @db.Decimal(24, 3)
packType String @default("")
productTag String @default("")
installDocId String @default("")
spec String @default("")
brand String @default("")
orderType String @default("")
storageType String @default("")
isSeasonal String @default("")
replaceCount String @default("")
supplierName String @default("")
purchaseOwner String @default("")
minimumOrderQuantity String @default("")
firstSourceCode String @default("PRODUCT_DEVELOPMENT")
fieldSourceMap Json @default("{}")
sources SupplyChainProductArchiveSource[]
imageEmbedding ProductArchiveImageEmbedding?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([spu])
@@index([sku])
@@index([businessCreatedAt])
@@index([categoryL1])
@@index([categoryL2])
@@index([productStatus])
@@index([firstSourceCode, createdAt])
}
model SupplyChainProductArchiveCategoryConfig {
id String @id @default(cuid())
categoryL2 String @unique
categoryL1 String
status String @default("ACTIVE")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([categoryL1])
@@index([status])
}
model SupplyChainProductArchiveSource {
id String @id @default(cuid())
archiveId String
archive SupplyChainProductArchive @relation(fields: [archiveId], references: [id], onDelete: Cascade)
sourceCode String
entryMode String @default("AUTO")
sourceRecordId String
sourceProductId String @default("")
sourceSkuItemId String @default("")
sourceSystem String @default("")
sourceSchema String @default("")
sourceObject String @default("")
sourceCreatedAt DateTime?
lastSyncedAt DateTime @default(now())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([sourceCode, sourceRecordId])
@@index([archiveId, sourceCode])
@@index([sourceProductId])
@@index([sourceSkuItemId])
}
model IntakeProductModelAsset {
id String @id @default(cuid())
productId String
product IntakeNonIntegratedProduct @relation(fields: [productId], references: [id], onDelete: Cascade)
modelKey String
displayName String @default("")
versions IntakeProductModelVersion[]
createdBy String
creator User @relation("IntakeProductModelAssetCreator", fields: [createdBy], references: [id], onDelete: Restrict)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([productId, modelKey])
@@index([productId, createdAt])
@@index([createdBy])
}
model IntakeProductModelVersion {
id String @id @default(cuid())
assetId String
asset IntakeProductModelAsset @relation(fields: [assetId], references: [id], onDelete: Cascade)
versionNo Int
originalFileName String
mimeType String
fileSize Int
checksumSha256 String @default("")
sourceStorageKey String @db.Text
sourceUrl String @db.Text
previewStorageKey String? @db.Text
previewUrl String? @db.Text
createdBy String
creator User @relation("IntakeProductModelVersionCreator", fields: [createdBy], references: [id], onDelete: Restrict)
skuProfiles IntakeSkuAiProfile[]
aiRenders AssistedAssemblyAiRender[]
createdAt DateTime @default(now())
@@unique([assetId, versionNo])
@@index([assetId, createdAt])
@@index([checksumSha256])
@@index([createdBy])
}
model IntakeSkuAiProfile {
id String @id @default(cuid())
skuItemId String @unique
skuItem IntakeNonIntegratedSkuItem @relation(fields: [skuItemId], references: [id], onDelete: Cascade)
currentModelVersionId String?
currentModelVersion IntakeProductModelVersion? @relation(fields: [currentModelVersionId], references: [id], onDelete: SetNull)
currentMaterialVersionId String? @unique
currentMaterialVersion IntakeSkuAiMaterialVersion? @relation("CurrentIntakeSkuAiMaterialVersion", fields: [currentMaterialVersionId], references: [id], onDelete: SetNull)
materialVersions IntakeSkuAiMaterialVersion[] @relation("IntakeSkuAiMaterialVersions")
createdBy String
updatedBy String?
creator User @relation("IntakeSkuAiProfileCreator", fields: [createdBy], references: [id], onDelete: Restrict)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([currentModelVersionId])
@@index([createdBy])
}
model IntakeSkuAiMaterialVersion {
id String @id @default(cuid())
profileId String
profile IntakeSkuAiProfile @relation("IntakeSkuAiMaterialVersions", fields: [profileId], references: [id], onDelete: Cascade)
currentForProfile IntakeSkuAiProfile? @relation("CurrentIntakeSkuAiMaterialVersion")
versionNo Int
materialColor String @default("#d8c6ad")
textureStorageKey String? @db.Text
textureUrl String? @db.Text
nodeMaterials Json?
uvConfig Json?
createdBy String
creator User @relation("IntakeSkuAiMaterialVersionCreator", fields: [createdBy], references: [id], onDelete: Restrict)
aiRenders AssistedAssemblyAiRender[]
createdAt DateTime @default(now())
@@unique([profileId, versionNo])
@@index([profileId, createdAt])
@@index([createdBy])
}
model IntakeNonIntegratedPricingRow {
id String @id
productId String
product IntakeNonIntegratedProduct @relation(fields: [productId], references: [id], onDelete: Cascade)
skuItemId String
skuItem IntakeNonIntegratedSkuItem @relation("IntakeNonIntegratedPricingSkuItem", fields: [productId, skuItemId], references: [productId, skuItemId], onDelete: Cascade, onUpdate: Cascade, map: "IntakeNonIntegratedPricingRow_product_sku_item_fkey")
skuCode String @default("")
barcode String @default("")
costPrice Decimal? @db.Decimal(10, 2)
productTag String @default("")
pricingConfirmed Boolean @default(false)
pricingConfirmedAt DateTime?
systemEnteredAt DateTime?
jstVirtualInventoryStatus String @default("")
jstVirtualInventoryScheduledAt DateTime?
jstVirtualInventoryPushedAt DateTime?
jstVirtualInventoryRetryCount Int @default(0)
jstVirtualInventoryLastAttemptAt DateTime?
jstVirtualInventoryError String @default("")
jstVirtualInventorySoId String @default("")
source String @default("PAYLOAD_SYNC")
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
@@unique([productId, skuItemId])
@@index([productId])
@@index([skuCode])
@@index([barcode])
@@index([pricingConfirmed])
@@index([systemEnteredAt, productId])
}
model IntakeNonIntegratedProductLimitRow {
id String @id
productId String
product IntakeNonIntegratedProduct @relation(fields: [productId], references: [id], onDelete: Cascade)
skuItemId String
skuItem IntakeNonIntegratedSkuItem @relation("IntakeNonIntegratedLimitSkuItem", fields: [productId, skuItemId], references: [productId, skuItemId], onDelete: Cascade, onUpdate: Cascade, map: "IntakeNonIntegratedProductLimitRow_product_sku_item_fkey")
skuCode String @default("")
barcode String @default("")
expectedDailyLimitPrice String @default("")
expectedLimitGrossMarginRate String @default("")
nonSpuOwnerLimitPrice String @default("")
limitConfirmed Boolean @default(false)
limitConfirmedAt DateTime?
source String @default("PAYLOAD_SYNC")
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
@@unique([productId, skuItemId])
@@index([productId])
@@index([skuCode])
@@index([barcode])
@@index([limitConfirmed])
}
model MasterDataQualityDecision {
id String @id @default(cuid())
targetType String
targetId String
targetBusinessKey String @default("")
status String @default("APPROVED_INCOMPLETE")
missingFields String[] @default([])
reason String @db.Text
decidedBy String
decidedAt DateTime @default(now())
resolvedBy String?
resolvedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([targetType, targetId])
@@index([targetBusinessKey])
@@index([status])
@@index([decidedAt])
}
model ProductPatentRecord {
id String @id
patentGroupKey String @default("")
productId String @default("")
linkedProductIds String[] @default([])
linkedProductNames String[] @default([])
linkedSpus String[] @default([])
linkedBrands String[] @default([])
productDisplayName String @default("")
productImage String? @db.Text
productImageName String @default("")
patentStatus String @default("待确认")
patentType String @default("外观设计")
patentName String @default("")
spu String @default("")
saRequired String @default("否")
salesStatus String @default("在售")
designer String @default("")
applicationNo String @default("")
patentNo String @default("")
applicationDate String @default("")
authorizationDate String @default("")
firstFeeDate String @default("")
annualFees Json @default("{}")
annualFeeDates Json @default("{}")
nextFeeDueDate String @default("")
feeAmount String @default("")
patentCertificateFileName String @default("")
patentCertificateFileUrl String? @db.Text
patentCertificateFileMimeType String @default("")
patentCertificateStorageKey String @default("")
annualFeeNote String @default("")
continueDecision String @default("待判断")
decisionReason String @default("")
owner String @default("")
remark String @default("")
sortOrder Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([patentGroupKey])
@@index([productId])
@@index([spu])
@@index([patentStatus])
@@index([patentNo])
@@index([applicationNo])
@@index([designer])
@@index([nextFeeDueDate])
@@index([sortOrder])
}
model IntakeReferenceLibraryItem {
id String @id @default(cuid())
libraryType String
itemKey String
code String @default("")
name String @default("")
category String @default("")
letter String @default("")
payload Json @default("{}")
sortOrder Int @default(0)
isActive Boolean @default(true)
createdBy String?
updatedBy String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([libraryType, itemKey])
@@index([libraryType, isActive, sortOrder])
@@index([libraryType, code])
@@index([libraryType, name])
@@index([category])
}
model BomCostingCase {
id String @id @default(cuid())
sourceUrl String @db.Text
sourceHost String
sourceSnapshotId String?
sourceSnapshot BomCostingSourceSnapshot? @relation(fields: [sourceSnapshotId], references: [id])
sourceCacheHit Boolean @default(false)
title String @default("")
providerCode String @default("")
providerTaskId String @default("")
analysisSchemaVersion String @default("competitor-analysis/v1")
status BomCostingCaseStatus @default(DRAFT)
progress Int @default(0)
statusMessage String @default("")
targetErrorPercent Decimal @default(10) @db.Decimal(5, 2)
currency String @default("CNY")
rawAnalysisSnapshot Json?
createdById String
creator User @relation("BomCostingCaseCreator", fields: [createdById], references: [id])
processingStartedAt DateTime?
processingFinishedAt DateTime?
deletedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
skus BomCostingSku[]
taskEvents BomCostingTaskEvent[]
analysisRuns BomCostingAnalysisRun[]
reviewInputs BomCostingReviewInput[]
@@index([createdById, deletedAt, createdAt])
@@index([status, deletedAt])
@@index([sourceSnapshotId])
@@index([providerTaskId])
}
model BomCostingSourceSnapshot {
id String @id @default(cuid())
sourceKey String @unique
sourceUrl String @db.Text
sourceHost String
channel String
externalItemId String
title String @default("")
shopName String @default("")
detailUrl String? @db.Text
status BomCostingSourceStatus @default(PENDING)
skuCount Int @default(0)
imageCount Int @default(0)
storedImageCount Int @default(0)
failedImageCount Int @default(0)
rawSnapshot Json?
errorMessage String @default("")
fetchedAt DateTime?
completedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
cases BomCostingCase[]
assets BomCostingSourceAsset[]
factAnalyses BomCostingSourceFactAnalysis[]
@@index([status, updatedAt])
@@index([channel, externalItemId])
}
model BomCostingSourceAsset {
id String @id @default(cuid())
sourceSnapshotId String
sourceSnapshot BomCostingSourceSnapshot @relation(fields: [sourceSnapshotId], references: [id], onDelete: Cascade)
assetKey String
kind BomCostingSourceAssetKind
status BomCostingSourceAssetStatus @default(PENDING)
originalUrl String @db.Text
storedUrl String @default("") @db.Text
storageKey String @default("") @db.Text
contentType String @default("")
byteSize Int?
externalSkuIds Json @default("[]")
sortOrder Int @default(0)
errorMessage String @default("")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([sourceSnapshotId, assetKey])
@@index([sourceSnapshotId, status, sortOrder])
}
model BomCostingSourceFactAnalysis {
id String @id @default(cuid())
sourceSnapshotId String
sourceSnapshot BomCostingSourceSnapshot @relation(fields: [sourceSnapshotId], references: [id], onDelete: Cascade)
schemaVersion String
revision Int @default(1)
origin BomCostingFactAnalysisOrigin @default(INITIAL)
baseAnalysisId String?
baseAnalysis BomCostingSourceFactAnalysis? @relation("BomCostingFactAnalysisRevisions", fields: [baseAnalysisId], references: [id], onDelete: SetNull)
derivedAnalyses BomCostingSourceFactAnalysis[] @relation("BomCostingFactAnalysisRevisions")
selectedAssetIds Json @default("[]")
assetFingerprint String
status BomCostingFactAnalysisStatus @default(PENDING)
model String @default("")
factSnapshot Json?
analyzedImageCount Int @default(0)
totalImageCount Int @default(0)
batchCount Int @default(0)
promptTokens Int @default(0)
completionTokens Int @default(0)
durationMs Int @default(0)
errorMessage String @default("")
startedAt DateTime?
completedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
analysisRuns BomCostingAnalysisRun[]
@@unique([sourceSnapshotId, schemaVersion, revision])
@@index([sourceSnapshotId, status])
@@index([status, updatedAt])
@@index([baseAnalysisId])
}
model BomCostingAnalysisRun {
id String @id @default(cuid())
caseId String
case BomCostingCase @relation(fields: [caseId], references: [id], onDelete: Cascade)
revision Int
mode BomCostingAnalysisRunMode
status BomCostingAnalysisRunStatus @default(PENDING)
factAnalysisId String?
factAnalysis BomCostingSourceFactAnalysis? @relation(fields: [factAnalysisId], references: [id], onDelete: SetNull)
selectedAssetIds Json @default("[]")
instruction String @default("") @db.Text
resultSnapshot Json?
model String @default("")
callId String @default("")
promptTokens Int @default(0)
completionTokens Int @default(0)
durationMs Int @default(0)
errorMessage String @default("")
createdById String?
creator User? @relation("BomCostingAnalysisRunCreator", fields: [createdById], references: [id], onDelete: SetNull)
startedAt DateTime?
completedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([caseId, revision])
@@index([caseId, status])
@@index([factAnalysisId])
@@index([status, updatedAt])
@@index([createdById])
}
model BomCostingSku {
id String @id @default(cuid())
caseId String
case BomCostingCase @relation(fields: [caseId], references: [id], onDelete: Cascade)
externalSkuId String
name String @default("")
structureGroupKey String @default("")
imageUrl String? @db.Text
attributes Json?
lengthMm Decimal? @db.Decimal(12, 3)
widthMm Decimal? @db.Decimal(12, 3)
heightMm Decimal? @db.Decimal(12, 3)
weightKg Decimal? @db.Decimal(12, 3)
packageLengthMm Decimal? @db.Decimal(12, 3)
packageWidthMm Decimal? @db.Decimal(12, 3)
packageHeightMm Decimal? @db.Decimal(12, 3)
packageWeightKg Decimal? @db.Decimal(12, 3)
estimatedCost Decimal? @db.Decimal(12, 2)
costCalculatedAt DateTime?
sortOrder Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
bomLines BomCostingLine[]
reviewInputs BomCostingReviewInput[]
@@unique([caseId, externalSkuId])
@@index([caseId, sortOrder])
@@index([structureGroupKey])
}
model BomCostingLine {
id String @id @default(cuid())
skuId String
sku BomCostingSku @relation(fields: [skuId], references: [id], onDelete: Cascade)
externalLineId String
category BomCostingLineCategory
partName String
materialCode String @default("")
materialName String @default("")
materialSpec String @default("")
processName String @default("")
packagingKind String @default("")
lengthMm Decimal? @db.Decimal(12, 3)
widthMm Decimal? @db.Decimal(12, 3)
heightMm Decimal? @db.Decimal(12, 3)
quantity Decimal @default(1) @db.Decimal(12, 4)
unit String @default("")
weightKg Decimal? @db.Decimal(12, 4)
lossFactor Decimal @default(1) @db.Decimal(8, 4)
sourceConfidence Decimal? @db.Decimal(5, 4)
evidence Json?
matchStatus BomCostingMatchStatus @default(UNMATCHED)
matchedLibraryType String @default("")
matchedLibraryRecordId String @default("")
matchedLibraryName String @default("")
matchedLibrarySpec String @default("")
resolvedUnitPrice Decimal? @db.Decimal(12, 4)
calculatedQuantity Decimal? @db.Decimal(14, 6)
subtotal Decimal? @db.Decimal(12, 2)
sortOrder Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
priceSnapshots BomCostingPriceSnapshot[]
reviewInputs BomCostingReviewInput[]
@@unique([skuId, externalLineId])
@@index([skuId, sortOrder])
@@index([category, matchStatus])
@@index([matchedLibraryType, matchedLibraryRecordId])
}
model BomCostingPriceSnapshot {
id String @id @default(cuid())
lineId String
line BomCostingLine @relation(fields: [lineId], references: [id], onDelete: Cascade)
sourceType String
sourceRecordId String
sourceName String
sourceSpec String @default("")
unitPrice Decimal @db.Decimal(12, 4)
unit String
currency String @default("CNY")
lossFactor Decimal @default(1) @db.Decimal(8, 4)
isCurrent Boolean @default(true)
matchedAt DateTime @default(now())
@@index([lineId, isCurrent])
@@index([sourceType, sourceRecordId])
}
model BomCostingReviewInput {
id String @id @default(cuid())
caseId String
case BomCostingCase @relation(fields: [caseId], references: [id], onDelete: Cascade)
skuId String?
sku BomCostingSku? @relation(fields: [skuId], references: [id], onDelete: SetNull)
lineId String?
line BomCostingLine? @relation(fields: [lineId], references: [id], onDelete: SetNull)
skuExternalId String @default("")
lineExternalId String @default("")
skuName String @default("")
partName String @default("")
createdById String?
creator User? @relation("BomCostingReviewInputCreator", fields: [createdById], references: [id], onDelete: SetNull)
source BomCostingReviewInputSource @default(MANUAL)
lengthMm Decimal? @db.Decimal(12, 3)
widthMm Decimal? @db.Decimal(12, 3)
heightMm Decimal? @db.Decimal(12, 3)
quantity Decimal? @db.Decimal(12, 4)
materialCode String @default("")
materialName String @default("")
materialSpec String @default("")
unitPrice Decimal? @db.Decimal(12, 4)
priceUnit String @default("")
currency String @default("CNY")
lossFactor Decimal? @db.Decimal(8, 4)
subtotal Decimal? @db.Decimal(12, 2)
note String @default("") @db.Text
createdAt DateTime @default(now())
@@index([caseId, createdAt])
@@index([lineId, createdAt])
@@index([caseId, skuExternalId, lineExternalId, createdAt])
@@index([createdById, createdAt])
}
model BomCostingTaskEvent {
id String @id @default(cuid())
caseId String
case BomCostingCase @relation(fields: [caseId], references: [id], onDelete: Cascade)
eventType String
status String
progress Int @default(0)
message String @default("")
providerTaskId String @default("")
payload Json?
createdAt DateTime @default(now())
@@index([caseId, createdAt])
@@index([providerTaskId])
}
// ============================================
// 装配相关
// ============================================
// 装配步骤(默认产品级,可选绑定到 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
// 4字段规格由服务层收口:相同规格+相同花色拒绝重复,相同规格+不同花色并入已有基材
@@index([isActive])
@@index([createdBy])
@@index([categoryId])
@@index([environmentGradeId])
@@index([thicknessSpecId])
@@index([surfaceOptionId])
@@index([systemNameOptionId])
}
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])
assetSeriesId String?
assetSeries AssetSeries? @relation("AssetFamilySeries", fields: [assetSeriesId], references: [id], onDelete: SetNull)
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)
deletedAt DateTime?
deletedById String?
deleter User? @relation("AssetFamilyDeleter", fields: [deletedById], references: [id], onDelete: SetNull)
variants AssetVariant[]
compatibleSeries AssetFamilySeriesCompatibility[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([type])
@@index([status])
@@index([productCategoryId])
@@index([assetSeriesId])
@@index([createdBy])
@@index([submittedById])
@@index([reviewedById])
@@index([rejectedById])
@@index([lastModifiedById])
@@index([deletedAt])
@@index([deletedById])
}
model AssetSeries {
id String @id @default(cuid())
code String @unique
name String
description String? @db.Text
productCategoryId String?
category ProductCategory? @relation("AssetSeriesCategory", fields: [productCategoryId], references: [id])
sortOrder Int @default(0)
isActive Boolean @default(true)
metadata Json?
createdBy String?
creator User? @relation("AssetSeriesCreator", fields: [createdBy], references: [id], onDelete: SetNull)
compatibleFamilies AssetFamilySeriesCompatibility[]
assetFamilies AssetFamily[] @relation("AssetFamilySeries")
draftGroups DraftGroup[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([productCategoryId])
@@index([createdBy])
@@index([sortOrder])
@@index([isActive])
}
model AssetFamilySeriesCompatibility {
id String @id @default(cuid())
assetFamilyId String
assetFamily AssetFamily @relation(fields: [assetFamilyId], references: [id], onDelete: Cascade)
assetSeriesId String
assetSeries AssetSeries @relation(fields: [assetSeriesId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
@@unique([assetFamilyId, assetSeriesId])
@@index([assetFamilyId])
@@index([assetSeriesId])
}
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?
barcode String?
supplierName String?
supplierCode String?
versionNo 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[]
sourceProductSkus ProductSKU[] @relation("ProductSKUSourceAssetVariant")
skuSyncEvents AssetSkuSyncEvent[]
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)
deletedAt DateTime?
deletedById String?
deleter User? @relation("AssetVariantDeleter", fields: [deletedById], references: [id], onDelete: SetNull)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([assetFamilyId, identityKey])
@@index([assetFamilyId])
@@index([status])
@@index([barcode])
@@index([substrateId])
@@index([finishId])
@@index([createdBy])
@@index([submittedById])
@@index([reviewedById])
@@index([rejectedById])
@@index([lastModifiedById])
@@index([inheritedFromVariantId])
@@index([deletedAt])
@@index([deletedById])
}
model AssetSkuSyncEvent {
id String @id @default(cuid())
assetVariantId String
assetVariant AssetVariant @relation(fields: [assetVariantId], references: [id], onDelete: Cascade)
reason String
status AssetSkuSyncEventStatus @default(PENDING)
payload Json?
progress Json?
affectedCount Int @default(0)
syncedCount Int @default(0)
failedCount Int @default(0)
errorMessage String? @db.Text
retryCount Int @default(0)
lockedAt DateTime?
processedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([assetVariantId])
@@index([status])
@@index([status, createdAt])
@@index([createdAt])
}
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])
assetSeriesId String?
assetSeries AssetSeries? @relation(fields: [assetSeriesId], 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([assetSeriesId])
@@index([createdBy])
@@index([status])
}
model AssistedAssemblyAiRender {
id String @id @default(cuid())
createdBy String
creator User @relation("AssistedAssemblyAiRenderCreator", fields: [createdBy], references: [id], onDelete: Cascade)
workbenchSpuId String?
workbenchSpu AiRenderWorkbenchSpu? @relation(fields: [workbenchSpuId], references: [id], onDelete: SetNull)
workbenchSkuId String?
workbenchSku AiRenderWorkbenchSku? @relation(fields: [workbenchSkuId], references: [id], onDelete: SetNull)
intakeSkuItemId String?
intakeSkuItem IntakeNonIntegratedSkuItem? @relation(fields: [intakeSkuItemId], references: [id], onDelete: SetNull)
modelVersionId String?
modelVersion IntakeProductModelVersion? @relation(fields: [modelVersionId], references: [id], onDelete: SetNull)
materialVersionId String?
materialVersion IntakeSkuAiMaterialVersion? @relation(fields: [materialVersionId], references: [id], onDelete: SetNull)
outputMode IntakeAiRenderOutputMode?
batchId String?
viewName String?
viewIndex Int?
storageClass IntakeAiRenderStorageClass @default(TEMPORARY)
prompt String @db.Text
negativePrompt String? @db.Text
params Json
referenceImageUrl String?
resultImages Json @default("[]")
workspaceSnapshot Json
assetSnapshot Json
externalRequestId String?
attempts Json?
status AssistedAssemblyAiRenderStatus @default(PENDING)
errorMessage String? @db.Text
durationMs Int?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([createdBy, createdAt])
@@index([workbenchSpuId, createdAt])
@@index([workbenchSkuId, createdAt])
@@index([intakeSkuItemId, createdAt])
@@index([intakeSkuItemId, outputMode, createdAt])
@@index([modelVersionId])
@@index([materialVersionId])
@@index([storageClass, createdAt])
@@index([status, createdAt])
}
model AiRenderWorkbenchSpu {
id String @id @default(cuid())
name String
description String @default("") @db.Text
sortOrder Int @default(0)
createdBy String
creator User @relation("AiRenderWorkbenchSpuCreator", fields: [createdBy], references: [id], onDelete: Cascade)
updatedBy String?
skus AiRenderWorkbenchSku[]
renders AssistedAssemblyAiRender[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([createdBy])
@@index([sortOrder, createdAt])
}
model AiRenderWorkbenchSku {
id String @id @default(cuid())
spuId String
spu AiRenderWorkbenchSpu @relation(fields: [spuId], references: [id], onDelete: Cascade)
name String
modelUrl String @default("") @db.Text
thumbnailUrl String? @db.Text
note String? @db.Text
materialColor String @default("#d8c6ad")
materialTextureUrl String? @db.Text
nodeMaterials Json?
sortOrder Int @default(0)
createdBy String
creator User @relation("AiRenderWorkbenchSkuCreator", fields: [createdBy], references: [id], onDelete: Cascade)
updatedBy String?
renders AssistedAssemblyAiRender[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([spuId, sortOrder, createdAt])
@@index([createdBy])
}
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
basicInfo PreSkuBasicInfo?
packagingPlan PreSkuPackagingPlan?
formalizedSku ProductSKU? @relation("PreSkuFormalizedSku")
locationBindings PreSkuLocationBinding[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([draftGroupId, sequence])
@@index([draftGroupId])
@@index([status])
}
model PreSkuBasicInfo {
id String @id @default(cuid())
preSkuId String @unique
preSku PreSku @relation(fields: [preSkuId], references: [id], onDelete: Cascade)
model String?
formalSkuCode String?
barcode String?
size String?
color String?
material String?
specName String?
versionNo String?
setCount String?
attributeCode String?
drawingId String?
installMethod String?
manualUpdatedBy String?
manualUpdatedByName String?
manualUpdatedAt DateTime?
source String @default("DRAFT_GROUP")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([model])
@@index([formalSkuCode])
@@index([barcode])
@@index([specName, versionNo])
}
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])
}
model ProductSkuQualityInspection {
id String @id @default(cuid())
skuId String
sku ProductSKU @relation("ProductSKUQualityInspections", fields: [skuId], references: [id], onDelete: Cascade)
recordKey String @unique
sourceType String @default("WAREHOUSE")
sourceProductId String?
sourceProductSpu String?
sourceSkuItemId String?
batchNo String
inspectedAt DateTime?
inspectorName String?
conclusion String
detail String? @db.Text
inspectedQuantity Int?
qualifiedQuantity Int?
qualifiedRate Float?
metrics Json?
attachments Json?
status String @default("DRAFT")
createdBy String?
updatedBy String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([skuId])
@@index([sourceType])
@@index([batchNo])
@@index([inspectedAt])
@@index([createdAt])
}
// ============================================
// 用户管理
// ============================================
// 角色枚举
enum Role {
SUPER_ADMIN // 超级管理员
ADMIN // 管理员
SUPERVISOR // 主管(按部门/小组管理锚点区分范围)
USER // 普通用户
}
// 用户状态枚举
enum UserStatus {
ACTIVE // 在职
PENDING // 待激活
INACTIVE // 离职
SUSPENDED // 暂停
}
enum EmployeeSourceStatus {
EMPLOYED
DIMISSIONED
}
enum BusinessAccountType {
GENERAL
CUSTOMER_SERVICE
DESIGN
PLANNING
STRATEGY
PURCHASING
OPERATIONS
WAREHOUSE
MANAGEMENT
SUPPLIER
EXTERNAL_INTAKE
}
enum BomCostingCaseStatus {
DRAFT
QUEUED
ANALYZING
READY
FAILED
}
enum BomCostingSourceStatus {
PENDING
FETCHING
ARCHIVING
READY
FAILED
}
enum BomCostingSourceAssetKind {
MAIN
DETAIL
SKU
}
enum BomCostingSourceAssetStatus {
PENDING
STORED
FAILED
}
enum BomCostingFactAnalysisStatus {
PENDING
PROCESSING
READY
FAILED
}
enum BomCostingFactAnalysisOrigin {
INITIAL
SELECTED_REANALYSIS
FULL_REANALYSIS
}
enum BomCostingAnalysisRunMode {
INITIAL
REUSE_FACTS
REANALYZE_SELECTED
REANALYZE_ALL
}
enum BomCostingAnalysisRunStatus {
PENDING
PROCESSING
READY
FAILED
}
enum BomCostingLineCategory {
BOARD
STEEL
ACCESSORY
PACKAGING
PROCESSING
LABOR
}
enum BomCostingMatchStatus {
UNMATCHED
MATCHED
NEEDS_CONFIRMATION
}
enum BomCostingReviewInputSource {
MANUAL
LIBRARY_MATCH
}
enum ApprovalWorkflowStatus {
DRAFT
PENDING
IN_PROGRESS
COMPLETED
REJECTED
ARCHIVED
}
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 AssistedAssemblyAiRenderStatus {
PENDING
SUCCEEDED
FAILED
}
enum IntakeAiRenderOutputMode {
WHITE_BACKGROUND
EIGHT_VIEW
SCENE
}
enum IntakeAiRenderStorageClass {
TEMPORARY
PERMANENT
}
enum AssetSkuSyncEventStatus {
PENDING
PROCESSING
COMPLETED
FAILED
}
enum ProductSkuAssetSourceStatus {
ACTIVE
DELETED
MISSING
}
enum IntakeNonIntegratedSkuDataQualityStatus {
READY
INCOMPLETE
CONFLICT
}
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?
supplierCode String?
supplierName String?
// 角色与权限
role Role @default(USER)
businessRoles BusinessAccountType[] @default([])
// 组织架构
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")
deletedAssetFamilies AssetFamily[] @relation("AssetFamilyDeleter")
createdAssetVariants AssetVariant[] @relation("AssetVariantCreator")
submittedAssetVariants AssetVariant[] @relation("AssetVariantSubmitter")
reviewedAssetVariants AssetVariant[] @relation("AssetVariantReviewer")
rejectedAssetVariants AssetVariant[] @relation("AssetVariantRejecter")
modifiedAssetVariants AssetVariant[] @relation("AssetVariantLastModifier")
deletedAssetVariants AssetVariant[] @relation("AssetVariantDeleter")
createdAssetSeries AssetSeries[] @relation("AssetSeriesCreator")
createdDraftGroups DraftGroup[] @relation("DraftGroupCreator")
createdAiRenderWorkbenchSpus AiRenderWorkbenchSpu[] @relation("AiRenderWorkbenchSpuCreator")
createdAiRenderWorkbenchSkus AiRenderWorkbenchSku[] @relation("AiRenderWorkbenchSkuCreator")
assistedAssemblyAiRenders AssistedAssemblyAiRender[] @relation("AssistedAssemblyAiRenderCreator")
createdIntakeProductModelAssets IntakeProductModelAsset[] @relation("IntakeProductModelAssetCreator")
createdIntakeProductModelVersions IntakeProductModelVersion[] @relation("IntakeProductModelVersionCreator")
createdIntakeSkuAiProfiles IntakeSkuAiProfile[] @relation("IntakeSkuAiProfileCreator")
createdIntakeSkuAiMaterialVersions IntakeSkuAiMaterialVersion[] @relation("IntakeSkuAiMaterialVersionCreator")
collaboratedProducts ProductCollaborator[] @relation("ProductCollaboratorUser")
addedProductCollaborators ProductCollaborator[] @relation("ProductCollaboratorAddedBy")
feedbacks Feedback[] @relation("FeedbackSubmitter")
createdBomCostingCases BomCostingCase[] @relation("BomCostingCaseCreator")
createdBomCostingAnalysisRuns BomCostingAnalysisRun[] @relation("BomCostingAnalysisRunCreator")
createdBomCostingReviewInputs BomCostingReviewInput[] @relation("BomCostingReviewInputCreator")
assignedFeedbacks Feedback[] @relation("FeedbackAssignee")
createdApprovalWorkflows ApprovalWorkflow[] @relation("ApprovalWorkflowCreator")
approvalWorkflowChanges ApprovalWorkflowFieldChange[]
barcodePrintImportRecords BarcodePrintImportRecord[] @relation("BarcodePrintImportRecordCreator")
workRecordEntries WorkRecord[] @relation("WorkRecordDesigner")
// 操作日志
activityLogs ActivityLog[]
operationLogs OperationLog[] @relation("OperationLogActor")
createdReleaseNotes ReleaseNote[] @relation("ReleaseNoteCreator")
releaseNoteAcknowledgements ReleaseNoteAcknowledgement[] @relation("ReleaseNoteAcknowledgementReader")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
lastLoginAt DateTime?
@@index([email])
@@index([name])
@@index([departmentId])
@@index([teamId])
@@index([status])
@@index([loginSource])
@@index([employeeDirectoryId])
@@index([supplierCode])
}
model EmployeeLookupCache {
id String @id @default(cuid())
cacheKey String @unique
keyType String
lookupValue String
result Json?
sourceStatus EmployeeSourceStatus?
expiresAt DateTime
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([keyType])
@@index([expiresAt])
}
// 部门
model Department {
id String @id @default(cuid())
name String @unique
fullPath String // 完整路径,如 "设计部-3D组-建模小组"
description String?
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)
isActive Boolean @default(true)
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)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([departmentId, name])
@@index([departmentId])
}
model ApprovalWorkflow {
id String @id @default(cuid())
approvalNumber String @unique
title String
status ApprovalWorkflowStatus @default(DRAFT)
productId String
product IntakeNonIntegratedProduct @relation(fields: [productId], references: [id], onDelete: Cascade)
accessKey String @unique
businessRoles BusinessAccountType[] @default([])
participantUserIds String[] @default([])
initiatorId String
initiator User @relation("ApprovalWorkflowCreator", fields: [initiatorId], references: [id])
lastSyncedAt DateTime?
fieldMatrix Json?
summary Json?
metadata Json?
fieldChanges ApprovalWorkflowFieldChange[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([productId, createdAt])
@@index([status, updatedAt])
@@index([initiatorId])
}
model ApprovalWorkflowFieldChange {
id String @id @default(cuid())
workflowId String
workflow ApprovalWorkflow @relation(fields: [workflowId], references: [id], onDelete: Cascade)
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
fieldKey String
oldValue Json?
newValue Json?
source String
sourcePage String?
fromDingtalkPage Boolean @default(false)
metadata Json?
createdAt DateTime @default(now())
@@index([workflowId, createdAt])
@@index([userId, createdAt])
@@index([fieldKey])
}
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
}
enum OperationLogAction {
INTAKE_PRODUCT_CREATE
INTAKE_PRODUCT_WRITE
INTAKE_PRODUCT_DELETE
INTAKE_FILE_UPLOAD
INTAKE_WORKSPACE_CONFIRMATION
JUSHUITAN_PRODUCT_PUSH
JUSHUITAN_VIRTUAL_INVENTORY_PUSH
}
enum OperationLogStatus {
SUCCESS
FAILED
RETRY
}
enum OperationLogSource {
USER
SYSTEM
}
enum ReleaseNoteStatus {
DRAFT
PUBLISHED
ARCHIVED
}
model OperationLog {
id String @id @default(cuid())
action OperationLogAction
status OperationLogStatus
source OperationLogSource @default(USER)
actorId String?
actor User? @relation("OperationLogActor", fields: [actorId], references: [id], onDelete: SetNull)
actorName String
productId String?
productSpu String?
skuId String?
skuCode String?
workspaceCategory String?
errorCode String?
message String @db.Text
requestId String?
details Json?
createdAt DateTime @default(now())
@@index([status, createdAt])
@@index([action, createdAt])
@@index([actorId, createdAt])
@@index([productSpu, createdAt])
@@index([productId, createdAt])
}
model ReleaseNote {
id String @id @default(cuid())
title String
summary String @db.Text
content Json
// Empty means the release note is visible to every active account.
targetBusinessRoles BusinessAccountType[] @default([])
status ReleaseNoteStatus @default(DRAFT)
publishedAt DateTime?
createdById String?
createdBy User? @relation("ReleaseNoteCreator", fields: [createdById], references: [id], onDelete: SetNull)
acknowledgements ReleaseNoteAcknowledgement[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([status, publishedAt])
@@index([createdById])
}
model ReleaseNoteAcknowledgement {
id String @id @default(cuid())
releaseNoteId String
releaseNote ReleaseNote @relation(fields: [releaseNoteId], references: [id], onDelete: Cascade)
readerId String
reader User @relation("ReleaseNoteAcknowledgementReader", fields: [readerId], references: [id], onDelete: Cascade)
readAt DateTime @default(now())
@@unique([releaseNoteId, readerId])
@@index([readerId, readAt])
}
// 产品部工作记录与月度归档
model WorkRecordScoreRule {
id String @id @default(cuid())
primary String
secondary String @default("")
unitScore Decimal? @db.Decimal(12, 4)
sortOrder Int @default(0)
isActive Boolean @default(true)
records WorkRecord[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([primary, sortOrder])
@@index([isActive, sortOrder])
}
model WorkRecord {
id String @id @default(cuid())
workDate DateTime @db.Date
designerId String
designerName String
designer User @relation("WorkRecordDesigner", fields: [designerId], references: [id], onDelete: Restrict)
primary String
secondary String @default("")
scoreRuleId String?
scoreRule WorkRecordScoreRule? @relation(fields: [scoreRuleId], references: [id], onDelete: SetNull)
approvalOrTask String @default("") @db.Text
spu String @default("")
quantity Decimal @default(1) @db.Decimal(12, 2)
unitScore Decimal? @db.Decimal(12, 4)
taskTotal Decimal? @db.Decimal(14, 4)
fileStoragePath String @default("") @db.Text
createdById String
updatedById String
deletedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([workDate, deletedAt])
@@index([designerId, workDate])
@@index([primary, workDate])
@@index([scoreRuleId])
}
model WorkRecordMonthlyArchive {
id String @id @default(cuid())
monthKey String @unique
periodStart DateTime @db.Date
periodEnd DateTime @db.Date
fileName String
summary Json
records Json
recordCount Int
generatedAt DateTime @default(now())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([periodStart])
}
// 操作日志
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 IntakeNodePermissionOverride {
id String @id @default(cuid())
targetKey String
subjectKey String
action String
enabled Boolean @default(true)
createdBy String?
updatedBy String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([targetKey, subjectKey, action])
@@index([targetKey])
@@index([subjectKey])
}
// ============================================
// 五金件库(标准五金件复用系统)
// ============================================
// 五金件分类
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])
}
// ============================================
// 系统配置(通用键值存储)
// ============================================
model SystemConfig {
key String @id
value Json
description String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// ============================================
// AI 服务商配置(多份配置 + 激活切换)
// ============================================
model ProviderConfig {
id String @id @default(cuid())
serviceType String // ai-render | prompt-optimizer
name String // 用户自定义名称,如 "生产环境"、"测试 Key"
baseUrl String
apiKey String
model String?
extra Json? // 扩展字段
isActive Boolean @default(false)
sortOrder Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([serviceType])
@@index([serviceType, isActive])
}
// 安装演示工作台配置(按图纸存)
model InstallationDemoConfig {
id String @id @default(cuid())
drawingId String @unique
parts Json
steps Json
numbering Json?
toolIds Json?
uploadedModel Json?
explodeTemplate Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// Global installation toolbox. Drawings only keep references to tool IDs.
model InstallationDemoTool {
id String @id @default(cuid())
toolCode String @unique
name String
modelUrl String
fileName String
mountOffset Json?
mountRotation Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}