WuYongJie 6e12f0318d feat(production-stop-demands): 完善档案字段、筛选、视图与通知中心联动
- 明细新增图片、产品标签、含大单近30日日销(条码/SKU 数值快照)

- 表头漏斗筛选(模糊/多选/数值比较/日期区间)、批量搜索、筛选设置

- 列设置拖动排序、快捷查询保存与套用

- 视图统计:日/周/月趋势、部门/需求人/原因/状态分布、饼图与明细下钻

- 通知中心:停产确认/待走流程/需求驳回通知、待办快照同步、钉钉全屏链接

- 新增迁移与历史数据回填脚本
2026-10-20 14:26:38 +08:00

5271 lines
186 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 SampleProgressItem {
id String @id @default(cuid())
orderDate DateTime @db.Date
imageUrl String?
openingCode String @unique
productDevelopmentFlowUrl String? @db.Text
productSize String
color String
material String
sampleQuantity Int?
brand String
sampleDueDate DateTime @db.Date
sampleStandardDays Float?
oaSampleActualDays Float?
actualSampleCompletedAt DateTime?
oaReachedAt DateTime?
supplier String
supplierConfirmedAt DateTime?
sampleNotifiedAt DateTime?
courierTrackingNumber String?
finalSampleFactory String?
status String
flowStatus String @default("")
designer String
purchaseOwner String?
merchandiserOwner String?
progressDescription String?
autoGenerated Boolean @default(false)
requestedCost Decimal? @db.Decimal(12, 2)
actualCost Decimal? @db.Decimal(12, 2)
followUpOwner String?
createdBy String?
updatedBy String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
changeLogs SampleProgressChangeLog[]
suppliers SampleProgressSupplier[]
@@index([status])
@@index([brand])
@@index([sampleDueDate])
@@index([actualSampleCompletedAt])
@@index([oaReachedAt])
@@index([designer])
}
model SampleProgressSupplier {
id String @id @default(cuid())
itemId String
item SampleProgressItem @relation(fields: [itemId], references: [id], onDelete: Cascade)
sortOrder Int @default(0)
supplier String
supplierConfirmedAt DateTime?
sampleNotifiedAt DateTime?
sampleDueDate DateTime @db.Date
courierTrackingNumber String?
status String
requestedCost Decimal? @db.Decimal(12, 2)
actualCost Decimal? @db.Decimal(12, 2)
progressDescription String?
actualSampleCompletedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([itemId, sortOrder])
@@index([itemId])
@@index([status])
@@index([sampleDueDate])
}
model SampleProgressChangeLog {
id String @id @default(cuid())
itemId String
item SampleProgressItem @relation(fields: [itemId], references: [id], onDelete: Cascade)
changedFields Json
beforeSnapshot Json
afterSnapshot Json
changeReason String?
changedById String?
changedByName String
createdAt DateTime @default(now())
@@index([itemId, createdAt])
}
model FinishedProductProgressItem {
id String @id @default(cuid())
sourceOpeningCode String?
sourceProductId String?
sourceSkuId String?
sourceGroupKey String?
sourceSupplierCode String?
sourceImportedAt DateTime?
supplierCode String @default("")
contractDateNumber String @default("")
orderPlacedDate DateTime? @db.Date
orderSequenceNo String @default("")
contractNumber String
followUpOwner String @default("")
imageUrl String? @db.Text
itemNumber String @default("")
barcode String?
productName String @default("")
supplier String @default("")
brand String @default("")
orderedQuantity Int @default(0)
inTransitQuantity Int @default(0)
firstDueDate DateTime? @db.Date
orderDueDate DateTime? @db.Date
realtimeDate DateTime? @db.Date
deliveryQuantity Int @default(0)
currentNode String @default("")
completedAt DateTime?
remarks String @default("") @db.Text
createdById String?
createdByName String @default("")
updatedById String?
updatedByName String @default("")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
changeLogs FinishedProductProgressChangeLog[]
deliveryRecords FinishedProductDeliveryRecord[]
@@index([contractDateNumber])
@@unique([sourceOpeningCode, sourceProductId, sourceSkuId], map: "FinishedProductProgressItem_factory_source_key")
@@index([sourceGroupKey])
@@index([sourceImportedAt])
@@index([orderSequenceNo])
@@index([itemNumber])
@@index([supplier])
@@index([brand])
@@index([currentNode])
@@index([orderDueDate])
@@index([updatedAt])
}
model FinishedProductProgressChangeLog {
id String @id @default(cuid())
itemId String
item FinishedProductProgressItem @relation(fields: [itemId], references: [id], onDelete: Cascade)
operation String
actorId String?
actorName String
changes Json
changeReason String?
createdAt DateTime @default(now())
@@index([itemId, createdAt])
@@index([createdAt])
}
model FinishedProductDeliveryRecord {
id String @id @default(cuid())
itemId String
item FinishedProductProgressItem @relation(fields: [itemId], references: [id], onDelete: Cascade)
deliveryDate DateTime @db.Date
quantity Int
status String @default("ACTIVE")
createdById String?
createdByName String @default("")
updatedById String?
updatedByName String @default("")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([itemId, status, deliveryDate])
@@index([itemId, createdAt])
}
model SupplierRecommendedProduct {
id String @id @default(cuid())
recommendationDate DateTime? @db.Date
category String @default("")
supplier String @default("")
purchaser String @default("")
productCode String @default("")
imageUrl String? @db.Text
sampleRequest String @default("")
dimensions String @default("")
price String @default("")
material String @default("")
boardGrade String @default("")
weight String @default("")
orderQuantity Int @default(0)
productionCycle String @default("")
shippingMode String @default("")
dropShippingFee String @default("")
selectionStatus String @default("UNSELECTED")
selectedById String?
selectedByName String @default("")
selectedAt DateTime?
createdById String?
createdByName String @default("")
updatedById String?
updatedByName String @default("")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
changeLogs SupplierRecommendedProductChangeLog[]
@@index([recommendationDate])
@@index([category])
@@index([supplier])
@@index([purchaser])
@@index([productCode])
@@index([shippingMode])
@@index([selectionStatus])
}
model SupplierRecommendedProductChangeLog {
id String @id @default(cuid())
itemId String
item SupplierRecommendedProduct @relation(fields: [itemId], references: [id], onDelete: Cascade)
operation String
actorId String?
actorName String
changes Json
createdAt DateTime @default(now())
@@index([itemId, createdAt])
@@index([createdAt])
}
// ============================================
// 产品分类
// ============================================
// 产品分类(动态管理)
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[]
fixedVirtualInventoryTasks IntakeJushuitanFixedVirtualInventoryTask[]
profile IntakeNonIntegratedProductProfile?
modelAssets IntakeProductModelAsset[]
photoUploadGrants IntakeProductPhotoUploadGrant[]
@@index([spu])
@@index([spuBase])
@@index([createdBy])
@@index([departmentId])
@@index([teamId])
@@index([visibility])
@@index([deletedAt, createdAt])
}
model IntakeProductPhotoUploadGrant {
id String @id
productId String
userId String
grantedBy String
createdAt DateTime @default(now())
product IntakeNonIntegratedProduct @relation(fields: [productId], references: [id], onDelete: Cascade)
user User @relation("IntakePhotoUploadRecipient", fields: [userId], references: [id], onDelete: Cascade)
grantor User @relation("IntakePhotoUploadGrantor", fields: [grantedBy], references: [id], onDelete: Restrict)
@@unique([productId, userId])
@@index([userId])
}
model BarcodePrintImportRecord {
id String @id @default(cuid())
kind String
// System-created barcode rows point to their SKU source. Imported rows keep
// this null, so identical Excel rows remain independently valid.
sourceSkuItemId String? @unique @map("sourcePricingRowId")
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("")
productionType 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("")
productAttribute String @default("")
productTag String @default("")
installDocId String @default("")
installMethod String @default("")
hasDrawing String @default("")
hasVideo String @default("")
spec String @default("")
brand String @default("")
orderType String @default("")
inOutWarehouse String @default("")
storageType String @default("")
isSeasonal String @default("")
replaceCount String @default("")
region String @default("")
supplierName String @default("")
opsDirector String @default("")
opsDeptHead String @default("")
opsHead String @default("")
purchaseOwner String @default("")
supplierType String @default("")
supplierStatus String @default("")
minimumOrderQuantity String @default("")
settlementMethod String @default("")
boardProvider String @default("")
penaltyStatus String @default("")
warehouseType String @default("")
warehouseHead String @default("")
firstSourceCode String @default("PRODUCT_DEVELOPMENT")
firstLeadTime String @default("")
stableLeadTime String @default("")
palletSpec String @default("")
backupSupplier String @default("")
dailySales String @default("")
sales3d String @default("")
sales7d String @default("")
sales15d String @default("")
sales30d String @default("0.00")
sales30dWithBig String @default("0.00")
stockJianshang String @default("")
stockClearance String @default("")
stockFactory String @default("")
purchasePrice String @default("")
costPrice String @default("")
costPriceSecondary String @default("")
sabcRating String @default("") @db.VarChar(1)
sabcUpdatedAt DateTime?
fieldSourceMap Json @default("{}")
sources SupplyChainProductArchiveSource[]
imageEmbedding ProductArchiveImageEmbedding?
imageCache SupplyChainProductArchiveImageCache?
salesPerformance SupplyChainProductArchiveSalesPerformance?
sabcResults SupplyChainProductArchiveSabcResult[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([spu])
@@index([sku])
@@index([businessCreatedAt])
@@index([categoryL1])
@@index([categoryL2])
@@index([productStatus])
@@index([inOutWarehouse])
@@index([stockJianshang])
@@index([firstSourceCode, createdAt])
}
// 产品全量档案日销同步状态:保存来源指纹和固定每日同步计划。
model ProductArchiveMarketingSalesState {
id String @id
state Json
snapshot Json
updatedAt DateTime @updatedAt
}
model ProductArchiveMarketingSalesCheck {
id String @id @default(cuid())
checkedAt DateTime @default(now())
status String
fingerprint String @default("")
sourceDate String @default("")
rowCount Int @default(0)
skuCount Int @default(0)
changedRows Int @default(0)
errorCode String @default("")
@@index([checkedAt])
}
// 月度 SABC 核算快照。主表只保留当前标签,历史月份和计算过程在这里追溯。
model SupplyChainProductArchiveSabcResult {
id String @id @default(cuid())
archiveId String
archive SupplyChainProductArchive @relation(fields: [archiveId], references: [id], onDelete: Cascade)
barcode String @db.VarChar(100)
periodStart DateTime @db.Date
periodEnd DateTime @db.Date
eligible Boolean
sabcRating String @default("") @db.VarChar(1)
category String @default("")
performanceScale String @default("")
salesScale String @default("")
finalScale String @default("")
profitLabel String @default("")
averageDailySales Decimal? @db.Decimal(24, 8)
feeRatio Decimal? @db.Decimal(20, 8)
grossMargin Decimal? @db.Decimal(20, 8)
shipAmount Decimal @db.Decimal(24, 4)
realPerformance Decimal @db.Decimal(24, 4)
shipQty BigInt
grossProfit Decimal @db.Decimal(24, 4)
afterSaleAmount Decimal @db.Decimal(24, 4)
status String @default("CALCULATED") @db.VarChar(24)
reason String @default("") @db.Text
// 生成时的整行明细快照:核算结果一经生成即冻结,不再随档案/销售状态变化。
snapshot Json?
calculatedAt DateTime @default(now())
@@unique([barcode, periodStart, periodEnd])
@@index([periodStart, periodEnd])
@@index([sabcRating, periodEnd])
@@index([archiveId, periodEnd])
}
model SupplyChainProductArchiveSabcRun {
id String @id @default(cuid())
periodStart DateTime @db.Date
periodEnd DateTime @db.Date
mode String @db.VarChar(16)
status String @db.VarChar(24)
sourceRows Int @default(0)
eligibleRows Int @default(0)
taggedRows Int @default(0)
comparedRows Int @default(0)
mismatchRows Int @default(0)
errorMessage String? @db.Text
startedAt DateTime @default(now())
completedAt DateTime?
@@unique([periodStart, periodEnd, mode])
@@index([status, startedAt])
}
// 销售宽表按条码汇总的每日快照。
model SupplyChainProductArchiveSalesPerformance {
archiveId String @id
archive SupplyChainProductArchive @relation(fields: [archiveId], references: [id], onDelete: Cascade)
barcode String @unique
periodStart DateTime @db.Date
periodEnd DateTime @db.Date
shipAmount Decimal @db.Decimal(24, 4)
returnAmount Decimal @db.Decimal(24, 4)
realPerformance Decimal @db.Decimal(24, 4)
shipQty BigInt
returnQty BigInt
grossProfit Decimal @db.Decimal(24, 4)
grossMargin Decimal? @db.Decimal(20, 8)
feeRatio Decimal? @db.Decimal(20, 8)
operatingProfit Decimal? @db.Decimal(24, 4)
syncedAt DateTime @default(now())
@@index([periodEnd])
@@index([syncedAt])
}
// Source sales facts retained at barcode/day grain for period-aware archive views.
// This table intentionally has no archive FK so unmatched upstream barcodes are
// retained until the product archive catches up.
model SupplyChainProductArchiveSalesPerformanceDaily {
barcode String @db.VarChar(100)
statDate DateTime @db.Date
// DF daily enrichment; empty/null until the source history is backfilled.
skuOperator String @default("")
unitCost Decimal? @db.Decimal(24, 4)
estimatedFreight Decimal? @db.Decimal(24, 4)
// Separate from shipAmount; NULL denotes history not yet refreshed.
salesAmount Decimal? @db.Decimal(24, 4)
shipAmount Decimal @db.Decimal(24, 4)
returnAmount Decimal @db.Decimal(24, 4)
realPerformance Decimal @db.Decimal(24, 4)
shipQty BigInt
returnQty BigInt
clearSalesCost Decimal @db.Decimal(24, 4)
sendFreight Decimal @db.Decimal(24, 4)
platformCommission Decimal @db.Decimal(24, 4)
xyCost Decimal @db.Decimal(24, 4)
afterSaleAmount Decimal @db.Decimal(24, 4)
pushFeeTotal Decimal @db.Decimal(24, 4)
marketingFeeTotal Decimal @db.Decimal(24, 4)
advertisementFeeTotal Decimal @db.Decimal(24, 4)
sharedWarehousingSum Decimal @db.Decimal(24, 4)
otherFee Decimal @db.Decimal(24, 4)
operatingFee Decimal @db.Decimal(24, 4)
sourceRowCount Int
syncedAt DateTime @default(now())
@@id([barcode, statDate])
@@index([statDate])
@@index([syncedAt])
}
// 1688 代发商品的采购寻源承接快照。
// 不建立业务外键:商品档案或账号变化后,仍需保留承接时的条码、人员和成本依据。
model Dropship1688SourcingTask {
id String @id @default(cuid())
sourceArchiveId String
sourceSpu String @default("")
sourceBarcode String @db.VarChar(100)
sourceCostPrice Decimal? @db.Decimal(24, 4)
sourceSkuOperator String @default("")
sourceDateFrom DateTime @db.Date
sourceDateTo DateTime @db.Date
purchaserUserId String
purchaserName String
supplierName String @default("")
quotedPriceTaxIncluded Decimal? @map("quoted_price_tax_included") @db.Decimal(24, 4)
invoiceTaxPoint Decimal? @db.Decimal(8, 4)
sourcingMode String @default("")
jianshangBarcode String @default("")
acceptedAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([sourceBarcode, purchaserUserId])
@@index([sourceArchiveId])
@@index([purchaserName])
@@index([sourceSpu])
@@index([sourceSkuOperator])
@@index([purchaserUserId, acceptedAt])
@@index([acceptedAt])
}
model SupplyChainProductArchiveSalesSyncRun {
id String @id @default(cuid())
startedAt DateTime @default(now())
completedAt DateTime?
status String @db.VarChar(24)
rangeStart DateTime @db.Date
rangeEnd DateTime @db.Date
sourceLatestDate DateTime? @db.Date
rowsRead Int @default(0)
rowsWritten Int @default(0)
unmatchedBarcodes Int @default(0)
errorMessage String? @db.Text
@@index([startedAt])
@@index([status, startedAt])
}
// Operations-maintained checklist per dropship (DF) SPU: invoice type,
// return address and assembly type (整装/拆装) are filled in on the 1688
// dropship issues page. Read-only columns (image, ops owner, drawing/video
// flags) always come live from the product archive, so only these fields
// live here.
model SupplyChainDropshipIssue {
id String @id @default(cuid())
spu String @unique @db.VarChar(120)
invoiceType String @default("") @db.VarChar(16)
returnAddress String @default("") @db.Text
assemblyType String @default("") @db.VarChar(16)
updatedByName String @default("")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([updatedAt])
}
// 图纸与视频导出来源:按条码保存当前有效的图纸/视频状态,支持月度增量覆盖。
model SupplyChainProductArchiveImageCache {
id String @id @default(cuid())
archiveId String @unique
archive SupplyChainProductArchive @relation(fields: [archiveId], references: [id], onDelete: Cascade)
barcode String @default("")
spu String @default("")
sku String @default("")
previewUrl String? @db.Text
source String @default("")
viewKey String @default("")
viewLabel String @default("")
status String @default("NOT_FOUND")
checkedAt DateTime @default(now())
error String @default("")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([barcode])
@@index([spu, sku])
@@index([status, checkedAt])
}
model SupplyChainProductArchiveDrawingMedia {
id String @id @default(cuid())
barcode String @unique
drawingId String @default("")
skuModel String @default("")
factoryModel String @default("")
completedAt DateTime?
externalLink String? @db.Text
lineDrawingStatus String @default("")
explodedDrawingStatus String @default("")
installationDrawingStatus String @default("")
installationVideoStatus String @default("")
sourceFileName String @default("")
sourceRowNumber Int?
sourceUpdatedAt DateTime?
lastSyncedAt DateTime @default(now())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([drawingId])
@@index([completedAt])
@@index([sourceFileName, sourceRowNumber])
}
model SupplyChainProductArchiveCategoryConfig {
id String @id @default(cuid())
categoryL2 String @unique
categoryL1 String
categoryGroup String @default("")
scaleLabel String @default("")
performanceStart Decimal? @db.Decimal(14, 2)
performanceEnd Decimal? @db.Decimal(14, 2)
dailySalesStart Decimal? @db.Decimal(12, 2)
dailySalesEnd Decimal? @db.Decimal(12, 2)
status String @default("ACTIVE")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([categoryL1])
@@index([categoryGroup])
@@index([status])
}
model SupplyChainProductArchiveCategoryScaleRow {
id String @id @default(cuid())
categoryL1 String @default("")
categoryL2 String
categoryGroup String @default("")
scaleLabel String
performanceStart Decimal? @db.Decimal(14, 2)
performanceEnd Decimal? @db.Decimal(14, 2)
dailySalesStart Decimal? @db.Decimal(12, 2)
dailySalesEnd Decimal? @db.Decimal(12, 2)
sortOrder Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([categoryL2, scaleLabel])
@@index([categoryL2])
@@index([categoryGroup])
}
model SupplyChainProductArchiveWarehouseConfig {
id String @id @default(cuid())
warehouseName String @unique
warehouseOwnership String @default("")
warehouseHead String @default("")
storageType String
status String @default("ACTIVE")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([storageType])
@@index([status])
}
/// 仓库属性人工覆盖:聚水潭 remark2 异常时,在这里改成正确的展示值。
model SupplyChainProductArchiveWarehouseAttributeOverride {
warehouseName String @unique
attribute String @default("")
updatedById String?
updatedByName String @default("")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([updatedAt])
}
model DownloadTask {
id String @id @default(cuid())
userId String
userName String @default("")
type String
title String @default("")
paramsJson String @default("{}")
status String @default("pending")
progress Int @default(0)
fileName String @default("")
filePath String @default("")
fileSize Int @default(0)
rowCount Int @default(0)
error String @default("")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
startedAt DateTime?
finishedAt DateTime?
@@index([userId, createdAt(sort: Desc)])
@@index([status])
}
model SupplyChainManagementOwnerConfig {
id String @id @default(cuid())
sourceEmployeeId String? @unique
sourceEmployee EmployeeDirectory? @relation(fields: [sourceEmployeeId], references: [id], onDelete: Cascade)
operationOwner String @default("")
department String @default("")
secondaryDepartment String @default("")
profitCenter String @default("")
departmentOwner String @default("")
generalOwner String @default("")
manualOverride Boolean @default(false)
isHidden Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([isHidden, updatedAt])
}
/// 商品SABC打标「各部门数据分类」的可维护配置(品牌范围、负责人归属例外、数据权限例外、目标参数)。
model SupplyChainProductArchiveDepartmentSetting {
id String @id @default(cuid())
key String @unique @db.VarChar(64)
value Json
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
/// 商品SABC打标「各部门数据分类」月度快照:核算生成时落库,之后页面直接读结果。
model SupplyChainProductArchiveDepartmentSnapshot {
id String @id @default(cuid())
periodStart DateTime @db.Date
periodEnd DateTime @db.Date
payload Json
rowTotal Int @default(0)
generatedAt DateTime @default(now())
@@unique([periodStart, periodEnd])
@@index([generatedAt])
}
/// 简尚「替换款管理」换货规则:sku = 原编码(老),replaceSku = 替换编码(新)。
/// 商品SABC打标的「同款商品编码(业绩&费比合并计算)」与业绩合并都依赖这张表。
model SupplyChainProductArchiveReplacementRule {
id String @id
sourceId Int @unique
sku String
replaceSku String @default("")
replaceType Int @default(0)
isSwapping Int @default(0)
differentPoints String @default("")
creator String @default("")
sourceCreatedAt DateTime? @db.Timestamp(6)
syncedAt DateTime @default(now()) @db.Timestamp(6)
@@index([sku])
@@index([replaceSku])
}
model ProductionStopDemand {
id String @id @default(cuid())
demandNo String @unique
requesterId String
requesterName String
department String @default("")
reasonCategory String
reasonDetail String @default("") @db.Text
submittedAt DateTime @default(now())
items ProductionStopDemandItem[]
confirmationDeadline DateTime?
confirmationStatus String @default("PENDING")
confirmationAt DateTime?
confirmationReasons String @default("") @db.Text
confirmationPushedAt DateTime?
confirmationPushMessage String @default("") @db.Text
confirmationVotes ProductionStopDemandConfirmationVote[]
demandStatus String @default("PENDING_CONFIRMATION")
demandStatusAt DateTime?
demandStatusNote String @default("") @db.Text
batchNo String @default("")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([submittedAt])
@@index([requesterId, submittedAt])
@@index([confirmationStatus, confirmationDeadline])
@@index([demandStatus, updatedAt])
@@index([batchNo])
}
// 停产需求明细行(一条码一行):档案快照 + 替换信息 + 采购反馈。
model ProductionStopDemandItem {
id String @id @default(cuid())
demandId String
demand ProductionStopDemand @relation(fields: [demandId], references: [id], onDelete: Cascade)
barcode String
categoryL1 String @default("")
categoryL2 String @default("")
spu String @default("")
sku String @default("")
productName String @default("")
productStatus String @default("")
supplierName String @default("")
purchaseOwner String @default("")
brand String @default("")
productTag String @default("")
productLabel String @default("")
image String @default("")
barcodeCreatedAt DateTime?
postStopSkuStatus String @default("")
sales30dWithBig String @default("")
sales30dWithBigSku String @default("")
sales30dWithBigValue Decimal? @db.Decimal(18, 4)
sales30dWithBigSkuValue Decimal? @db.Decimal(18, 4)
opsDirector String @default("")
topSalesOpsDirector String @default("")
stopSuggestion String @default("")
replaceType String @default("")
replaceBarcode String @default("")
imageChange String @default("")
modificationPoint String @default("") @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([demandId, barcode])
@@index([barcode])
@@index([sku])
@@index([purchaseOwner])
@@index([updatedAt])
}
// 停产原因分类基础选项配置(系统管理员维护,需求人只能选择已配置内容)。
model ProductionStopReasonCategory {
id String @id @default(cuid())
name String @unique
sortOrder Int @default(0)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([isActive, sortOrder])
}
// 停产需求确认人(指定运营总监/指定其他人),由系统管理员维护,用于推送与确认权限判定。
model ProductionStopConfirmationApprover {
id String @id @default(cuid())
userId String @unique
userName String
dingtalkUnionId String @default("")
isActive Boolean @default(true)
sortOrder Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
votes ProductionStopDemandConfirmationVote[]
@@index([isActive, sortOrder])
}
// 停产需求「是否同意停产」确认票:一人一票,超时未反馈按默认同意记录来源 TIMEOUT。
model ProductionStopDemandConfirmationVote {
id String @id @default(cuid())
demandId String
demand ProductionStopDemand @relation(fields: [demandId], references: [id], onDelete: Cascade)
approverId String
approver ProductionStopConfirmationApprover @relation(fields: [approverId], references: [id], onDelete: Restrict)
approverUserId String
approverName String
decision String @default("AGREED")
reason String @default("") @db.Text
source String @default("USER")
respondedAt DateTime @default(now())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([demandId, approverId])
@@index([demandId, decision])
@@index([respondedAt])
}
// 停产流程负责人:负责「走流程」与强制停产确认,权限单独授予(系统管理员维护)。
model ProductionStopFlowOwner {
id String @id @default(cuid())
userId String @unique
userName String
isActive Boolean @default(true)
sortOrder Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([isActive, sortOrder])
}
// 通知中心待办同步状态:按「待办类型 + 接收人」记录上次推送的待办快照。
// 待办集合变化时推送新的条数卡片;集合清空时推送一次「已全部处理完成」。
model ProductionStopNotificationSync {
id String @id @default(cuid())
kind String
recipientKey String
recipientName String @default("")
pendingCount Int @default(0)
snapshot String @default("")
lastSentAt DateTime?
completedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([kind, recipientKey])
}
// 盘停建议配置:人工批量维护「条码 → 盘停建议」,提单时按条码自动带出。
model ProductionStopSuggestionConfig {
id String @id @default(cuid())
barcode String @unique
suggestion String @default("")
updatedById String @default("")
updatedByName String @default("")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([updatedAt])
}
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 IntakeJushuitanFixedVirtualInventoryTask {
id String @id @default(cuid())
productId String
product IntakeNonIntegratedProduct @relation(fields: [productId], references: [id], onDelete: Cascade)
productSpu String @default("")
skuItemId String
skuCode String @default("")
barcode String @default("")
status String @default("pending")
scheduledAt DateTime
retryCount Int @default(0)
lastAttemptAt DateTime?
pushedAt DateTime?
error String @default("")
requestId String @default("")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([productId, skuItemId])
@@index([status, scheduledAt])
@@index([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[]
scenarios BomCostingScenario[]
@@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
requestedCategorySkillId String @default("")
resolvedCategorySkillId String @default("")
resolvedCategorySkillVersion String @default("")
resolvedStructureKnowledgeVersion String @default("")
categorySkillSelection Json?
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("")
costFamilyKey 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[]
scenarios BomCostingScenario[]
@@unique([caseId, externalSkuId])
@@index([caseId, sortOrder])
@@index([structureGroupKey])
@@index([costFamilyKey])
}
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[]
scenarioLines BomCostingScenarioLine[]
@@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])
}
// Local, auditable mirror of the upstream rpt.pm_bomBaseInfo view. The
// costing request path only reads these rows; the SQL Server source is used by
// the scheduled sync job and never by a user request.
model BomCostingReferenceSyncRun {
id String @id @default(cuid())
sourceSystem String @default("JSReportReplica")
sourceDatabase String @default("")
sourceSchema String @default("rpt")
sourceObject String @default("pm_bomBaseInfo")
status BomCostingReferenceSyncStatus @default(RUNNING)
batchKey String @default("")
configFingerprint String @default("") @db.VarChar(64)
cursorStart String @default("")
cursorEnd String @default("")
rowsRead Int @default(0)
rowsWritten Int @default(0)
rowsCreated Int @default(0)
rowsUpdated Int @default(0)
rowsUnchanged Int @default(0)
rowsSkipped Int @default(0)
errorCount Int @default(0)
errorMessage String @default("") @db.Text
durationMs Int @default(0)
startedAt DateTime @default(now())
completedAt DateTime?
sourceAsOf DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
referenceRows BomCostingReferenceRow[]
scenarios BomCostingScenario[]
@@index([status, startedAt])
@@index([status, completedAt])
@@index([completedAt])
@@index([batchKey])
}
model BomCostingReferenceRow {
id String @id @default(cuid())
sourceRowId String
sourceSku String @default("")
bomVersionKey String @default("")
category String @default("")
categoryCode String @default("")
partNumber String @default("")
partName String @default("")
side String @default("")
materialName String @default("")
materialCode String @default("")
materialSpec String @default("")
processName String @default("")
unit String @default("")
rawLength String @default("")
rawWidth String @default("")
rawLongEdge String @default("")
rawShortEdge String @default("")
rawQuantity String @default("")
rawCoefficient String @default("")
rawArea String @default("")
rawEdgeLength String @default("")
rawUnitPrice String @default("")
rawAmount String @default("")
rawNote String @default("") @db.Text
rawCreatedAt String @default("")
lengthMm Decimal? @db.Decimal(18, 6)
widthMm Decimal? @db.Decimal(18, 6)
longEdgeMm Decimal? @db.Decimal(18, 6)
shortEdgeMm Decimal? @db.Decimal(18, 6)
quantity Decimal? @db.Decimal(18, 6)
coefficient Decimal? @db.Decimal(18, 6)
areaSqm Decimal? @db.Decimal(18, 6)
edgeLengthM Decimal? @db.Decimal(18, 6)
unitPrice Decimal? @db.Decimal(18, 6)
amount Decimal? @db.Decimal(18, 6)
sourceCreatedAt DateTime?
sourceFingerprint String @db.VarChar(64)
isSummaryRow Boolean @default(false)
syncRunId String
syncRun BomCostingReferenceSyncRun @relation(fields: [syncRunId], references: [id], onDelete: Cascade)
syncedAt DateTime @default(now())
// A run is written as an isolated snapshot so failed/new runs cannot mutate
// the visible batch. After activation, only the newest operational mirror is
// retained; scenario lines keep immutable historical calculation snapshots.
@@unique([syncRunId, sourceRowId, sourceSku, bomVersionKey], map: "BomCostingReferenceRow_source_key")
@@index([syncRunId, sourceSku, categoryCode, materialName], map: "BomCostingReferenceRow_syncRunId_sku_category_material_idx")
@@index([syncRunId, materialName, materialSpec, unit], map: "BomCostingReferenceRow_syncRunId_material_spec_unit_idx")
@@index([syncRunId, categoryCode, partNumber], map: "BomCostingReferenceRow_syncRunId_category_part_number_idx")
@@index([syncRunId, categoryCode, lengthMm, widthMm], map: "BomCostingReferenceRow_syncRunId_category_dimensions_idx")
@@index([sourceSku, categoryCode, materialName], map: "BomCostingReferenceRow_sku_category_material_idx")
@@index([materialName, materialSpec, unit], map: "BomCostingReferenceRow_material_spec_unit_idx")
@@index([syncRunId])
@@index([syncRunId, isSummaryRow, categoryCode], map: "BomCostingReferenceRow_syncRunId_summary_category_idx")
@@index([sourceCreatedAt])
}
model BomCostingMaterialOption {
id String @id @default(cuid())
code String @unique
name String
normalizedName String
category String @default("BOARD")
aliases Json @default("[]")
enabled Boolean @default(true)
sortOrder Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
substitutionScenarios BomCostingScenario[]
@@index([enabled, sortOrder])
@@index([normalizedName])
}
model BomCostingScenario {
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: Cascade)
kind BomCostingScenarioKind
scenarioKey String @default("")
revision Int @default(1)
isCurrent Boolean @default(true)
targetMaterialId String?
targetMaterial BomCostingMaterialOption? @relation(fields: [targetMaterialId], references: [id], onDelete: SetNull)
targetMaterialName String @default("")
status BomCostingScenarioStatus @default(PROVISIONAL)
totalCost Decimal? @db.Decimal(18, 2)
estimateLow Decimal? @db.Decimal(18, 2)
estimateHigh Decimal? @db.Decimal(18, 2)
estimateBasis String @default("")
sampleCount Int @default(0)
estimatedLineCount Int @default(0)
knownCost Decimal? @db.Decimal(18, 2)
materialCost Decimal? @db.Decimal(18, 2)
processingCost Decimal? @db.Decimal(18, 2)
laborCost Decimal? @db.Decimal(18, 2)
packagingCost Decimal? @db.Decimal(18, 2)
lossCost Decimal? @db.Decimal(18, 2)
competitorPrice Decimal? @db.Decimal(18, 2)
priceDelta Decimal? @db.Decimal(18, 2)
realtimeMarketQuote Json?
wholeProductEstimateAudit Json?
confidence Decimal? @db.Decimal(5, 4)
warning String @default("") @db.Text
unresolvedCount Int @default(0)
dataAsOf DateTime?
syncRunId String?
syncRun BomCostingReferenceSyncRun? @relation(fields: [syncRunId], references: [id], onDelete: SetNull)
rulesetVersion String @default("dynamic-bom-cost/v5")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
lines BomCostingScenarioLine[]
@@unique([skuId, kind, scenarioKey, revision], map: "BomCostingScenario_sku_kind_scenarioKey_revision_key")
@@index([caseId, kind])
@@index([skuId, isCurrent, updatedAt])
@@index([status, dataAsOf])
@@index([syncRunId])
}
model BomCostingScenarioLine {
id String @id @default(cuid())
scenarioId String
scenario BomCostingScenario @relation(fields: [scenarioId], references: [id], onDelete: Cascade)
sourceLineId String?
sourceLine BomCostingLine? @relation(fields: [sourceLineId], references: [id], onDelete: SetNull)
sourceRowId String @default("")
category String @default("")
partName String @default("")
originalMaterial String @default("")
appliedMaterial String @default("")
materialSpec String @default("")
unit String @default("")
quantity Decimal? @db.Decimal(18, 6)
coefficient Decimal? @db.Decimal(18, 6)
pricingQuantity Decimal? @db.Decimal(18, 6)
unitPrice Decimal? @db.Decimal(18, 6)
subtotal Decimal? @db.Decimal(18, 2)
estimateLow Decimal? @db.Decimal(18, 2)
estimateHigh Decimal? @db.Decimal(18, 2)
estimateBasis String @default("")
sampleCount Int @default(0)
priceSource String @default("")
sourceSku String @default("")
referenceSkus Json @default("[]")
referenceRowIds Json @default("[]")
sourceFingerprint String @default("") @db.VarChar(64)
sourceCreatedAt DateTime?
sourceSyncedAt DateTime?
calculationRule String @default("")
confidence Decimal? @db.Decimal(5, 4)
status BomCostingScenarioStatus @default(PROVISIONAL)
warning String @default("") @db.Text
rawSnapshot Json?
createdAt DateTime @default(now())
@@index([scenarioId, category])
@@index([sourceRowId])
@@index([status])
}
// ============================================
// 装配相关
// ============================================
// 装配步骤(默认产品级,可选绑定到 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])
}
/// Durable execution state for image generation. Business results remain in their owning module.
model NativeImageGenerationJob {
id String @id
createdBy String
providerConfigId String
providerFingerprint String
requestHash String
request Json
completion Json
progress Json @default("[]")
status String @default("QUEUED")
retryCount Int @default(0)
nextRunAt DateTime @default(now())
leaseOwner String?
leaseUntil DateTime?
errorMessage String? @db.Text
finishedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([status, nextRunAt, leaseUntil])
@@index([createdBy, createdAt])
}
model DiyScheme {
id String @id @default(cuid())
code String @unique
name String
parentCode String?
createdBy String
creator User @relation("DiySchemeCreator", fields: [createdBy], references: [id], onDelete: Cascade)
snapshot Json
createdAt DateTime @default(now())
@@index([createdBy, createdAt, id])
}
/// DIY 共享参考目录;一个导入事务发布完整目录,不创建正式产品或 SKU。
model DiyCatalog {
spu String @id
name String
version Int @default(1)
sourceHash String
importedAt DateTime @default(now())
styles DiyCabinetStyle[]
patterns DiyDoorPattern[]
materials DiyMaterial[]
files DiyAssetFile[]
}
/// panels/cells 是专用几何数据,身份、实际尺寸使用结构化列。
model DiyCabinetStyle {
code String @id
spu String
catalog DiyCatalog @relation(fields: [spu], references: [spu], onDelete: Restrict)
name String
displaySize String
width Float
height Float
depth Float
cellCount Int
panelCount Int
panels Json
cells Json
sortOrder Int
variants DiyCabinetVariant[]
modelFileId String?
model DiyAssetFile? @relation("DiyStyleModel", fields: [modelFileId], references: [id], onDelete: Restrict)
mainNodeNames String[] @default([])
shelfNodeNames String[] @default([])
modelRotationY Float @default(0)
@@index([spu, sortOrder])
}
model DiyCabinetVariant {
itemCode String @id
styleCode String
style DiyCabinetStyle @relation(fields: [styleCode], references: [code], onDelete: Restrict)
name String
barcodes String[]
factoryModel String
factoryCode String
orderQty Int
sortOrder Int
thumbnailFileId String
thumbnail DiyAssetFile @relation(fields: [thumbnailFileId], references: [id], onDelete: Restrict)
mainMaterialCode String?
mainMaterial DiyMaterial? @relation("DiyVariantMainMaterial", fields: [mainMaterialCode], references: [itemCode], onDelete: Restrict)
shelfMaterialCode String?
shelfMaterial DiyMaterial? @relation("DiyVariantShelfMaterial", fields: [shelfMaterialCode], references: [itemCode], onDelete: Restrict)
@@unique([styleCode, name])
}
model DiyDoorPattern {
itemCode String @id
spu String
catalog DiyCatalog @relation(fields: [spu], references: [spu], onDelete: Restrict)
name String
barcodes String[]
factoryModel String
factoryCode String
orderQty Int
sortOrder Int
imageFileId String
image DiyAssetFile @relation(fields: [imageFileId], references: [id], onDelete: Restrict)
width Float?
height Float?
thickness Float?
deletedAt DateTime?
deletedBy String?
@@index([spu, sortOrder])
@@index([deletedAt])
}
model DiyMaterial {
itemCode String @id
spu String
catalog DiyCatalog @relation(fields: [spu], references: [spu], onDelete: Restrict)
name String
kind String
colorHex String
barcodes String[]
factoryModel String
factoryCode String
sortOrder Int
textureFileId String?
texture DiyAssetFile? @relation(fields: [textureFileId], references: [id], onDelete: Restrict)
mainVariants DiyCabinetVariant[] @relation("DiyVariantMainMaterial")
shelfVariants DiyCabinetVariant[] @relation("DiyVariantShelfMaterial")
deletedAt DateTime?
deletedBy String?
@@unique([spu, name])
@@index([deletedAt])
}
/// 文件索引;url 为平台 /api/storage/object,校验和用于幂等导入。
model DiyAssetFile {
id String @id
spu String
catalog DiyCatalog @relation(fields: [spu], references: [spu], onDelete: Restrict)
storageKey String @unique
url String
sourcePath String
sha256 String
bytes Int
contentType String
kind String
createdAt DateTime @default(now())
variants DiyCabinetVariant[]
patterns DiyDoorPattern[]
materials DiyMaterial[]
models DiyCabinetStyle[] @relation("DiyStyleModel")
@@index([spu])
}
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 BomCostingReferenceSyncStatus {
RUNNING
SUCCEEDED
FAILED
}
enum BomCostingScenarioKind {
SAME_MATERIAL
MATERIAL_SUBSTITUTION
}
enum BomCostingScenarioStatus {
FINAL
PROVISIONAL
INSUFFICIENT_DATA
STALE
}
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([])
permissionRoleId String?
permissionRole PermissionRole? @relation(fields: [permissionRoleId], references: [id], onDelete: SetNull)
// 组织架构
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")
intakePhotoUploadGrants IntakeProductPhotoUploadGrant[] @relation("IntakePhotoUploadRecipient")
issuedIntakePhotoUploadGrants IntakeProductPhotoUploadGrant[] @relation("IntakePhotoUploadGrantor")
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")
createdDiySchemes DiyScheme[] @relation("DiySchemeCreator")
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([permissionRoleId])
@@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?
directoryManagerDingtalkUserIds String[] @default([])
// 层级关系
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?
managerDingtalkUserId String?
sourceStatus EmployeeSourceStatus @default(EMPLOYED)
primaryDepartmentExternalId String?
departmentExternalIds Json?
syncedAt DateTime @default(now())
rawSnapshot Json?
user User?
supplyChainManagementOwnerConfig SupplyChainManagementOwnerConfig?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([name])
@@index([jobNumber])
@@index([mobile])
@@index([email])
@@index([sourceStatus])
@@index([managerDingtalkUserId])
@@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
SUPPLY_CHAIN_DROPSHIP_ISSUE_WRITE
PRODUCTION_STOP_DEMAND_DELETE
PRODUCTION_STOP_DEMAND_UPDATE
}
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])
@@index([workspaceCategory, source, status, createdAt], map: "OperationLog_retention_scope_createdAt_idx")
}
// 家具系统发往统一通知中心的本地审计副本。
// 不建立 User 外键,确保账号后续停用或删除后仍可追溯当时的收件人与内容。
model NotificationDeliveryLog {
id String @id @default(cuid())
idempotencyKey String @unique
payloadHash String
externalDeliveryId String? @unique
businessEvent String
businessRecordId String?
source String @default("SYSTEM")
initiatedById String?
initiatedByName String
recipientUserId String
recipientName String
recipientEmail String
recipientDingtalkUserId String
title String
renderedTitle String?
content String @db.Text
actionTitle String
actionUrl String @db.Text
status String @default("pending")
lastErrorCode String?
lastErrorMessage String? @db.Text
sentAt DateTime?
readAt DateTime?
lastSyncedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([status, createdAt])
@@index([recipientUserId, createdAt])
@@index([businessEvent, createdAt])
@@index([createdAt])
}
// 产品开发提醒的一次性业务尝试标记。通知正文仍由 NotificationDeliveryLog 审计。
model ProductDevelopmentTodoAttempt {
id String @id @default(cuid())
attemptKey String @unique
ruleId String
productId String
recipientKey String
status String @default("claimed")
lastErrorCode String?
lastErrorMessage String? @db.Text
attemptedAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([productId, ruleId])
@@index([recipientKey, attemptedAt])
}
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("")
workFrequency String @default("")
initiativeType 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([workFrequency, sortOrder])
@@index([initiativeType, sortOrder])
@@index([isActive, sortOrder])
}
model WorkRecordBusinessOption {
id String @id @default(cuid())
type String
value String
sortOrder Int @default(0)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([type, value])
@@index([type, sortOrder])
@@index([type, isActive, sortOrder])
}
model EmployeeHoursRecord {
id String @id @default(cuid())
workDate DateTime @db.Date
nickname String
businessContent String
businessNote String @default("") @db.Text
businessCategory String
workFrequency String
businessSource String
startTime String @db.VarChar(16)
endTime String @db.VarChar(16)
minutes Int
hours Decimal @db.Decimal(10, 2)
estimatedScore Decimal @db.Decimal(12, 4)
approvedScore Decimal? @db.Decimal(12, 4)
department String @default("")
attachment Json?
createdById String
updatedById String
deletedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([workDate, deletedAt])
@@index([nickname, workDate])
@@index([businessCategory, workDate])
@@index([department, workDate])
}
model EmployeeHoursAccessGrant {
userId String @id
scope String @default("SELF")
updatedById String
updatedAt DateTime @updatedAt
}
// Retained for migration compatibility; employee-hours no longer writes or exposes audit logs.
model EmployeeHoursAuditLog {
id String @id @default(cuid())
area String
action String
status String
actorId String
actorName String
targetId String?
message String @db.Text
details Json?
createdAt DateTime @default(now())
@@index([area, createdAt])
@@index([actorId, createdAt])
@@index([status, createdAt])
}
model EmployeeHoursBusinessOption {
id String @id @default(cuid())
type String
value String
sortOrder Int @default(0)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([type, value])
@@index([type, sortOrder])
@@index([type, isActive, sortOrder])
}
model EmployeeHoursScoreRule {
id String @id @default(cuid())
primary String
secondary String @default("")
workFrequency String @default("")
initiativeType String @default("")
unitScore Decimal? @db.Decimal(12, 4)
sortOrder Int @default(0)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([primary, sortOrder])
@@index([workFrequency, sortOrder])
@@index([initiativeType, 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 PermissionRole {
id String @id @default(cuid())
name String
normalizedName String @unique
description String @default("")
sortOrder Int @default(0)
isActive Boolean @default(true)
createdBy String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
users User[]
@@index([isActive, sortOrder])
}
model SupplyChainPermissionOverride {
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 ProductArchiveFieldPermissionSubject {
subjectKey String @id
createdBy String?
updatedBy String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
blocks ProductArchiveFieldPermissionBlock[]
}
/// 产品全量档案字段权限(黑名单):subjectKey(岗位/角色/个人)对 fieldKey(列 prop)的屏蔽条目。
/// 只存被屏蔽的字段;未出现即默认可见,与页面级权限的「先有页面可见权,再看字段」层级一致。
model ProductArchiveFieldPermissionBlock {
id String @id @default(cuid())
subjectKey String
fieldKey String
subject ProductArchiveFieldPermissionSubject @relation(fields: [subjectKey], references: [subjectKey], onDelete: Cascade)
createdBy String?
updatedBy String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([subjectKey, fieldKey])
@@index([subjectKey])
}
// Store account data is maintained manually in this system.
model StoreAccount {
id String @id @default(cuid())
storeName String
operationOwner String @default("")
operationDepartmentHead String @default("")
operationDirector String @default("")
profitCenter String @default("")
accountType String
account String
passwordCiphertext String @default("") @db.Text
boundPhone String @default("")
verificationContact String @default("")
jstOrderService String @default("")
accountGroupName String @default("")
shopState Int?
subject String @default("")
boundBankAccountNumberCiphertext String @default("") @db.Text
createdById String?
updatedById String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([storeName, accountType, account])
@@index([storeName])
@@index([account])
@@index([accountType])
@@index([updatedAt])
}
// Dedicated append-only access audit. Never store password/ciphertext/request bodies.
model StoreAccountPasswordAccessAudit {
requestId String @id @db.Uuid
requesterId String? @db.VarChar(128)
serviceId String? @db.VarChar(64)
accountId String? @db.VarChar(128)
purpose String @db.VarChar(8)
status Int
createdAt DateTime @default(dbgenerated("clock_timestamp()")) @db.Timestamptz(3)
@@index([serviceId, createdAt])
@@index([accountId, createdAt])
}
model StoreAccountStoreProfile {
id String @id @default(cuid())
storeName String
platform String
merchantNumber String @default("")
secondaryMerchantNumber String @default("")
merchantStoreId String @default("")
companyId String @default("")
createdById String?
updatedById String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([storeName, platform])
@@index([storeName])
@@index([platform])
}
model StoreAccountChangeLog {
id String @id @default(cuid())
storeAccountId String?
storeName String
operation String
actorId String?
actorName String
changes Json
createdAt DateTime @default(now())
@@index([storeAccountId, createdAt])
@@index([createdAt])
@@index([storeName, createdAt])
}
model StoreAccountFieldPermission {
id String @id @default(cuid())
fieldKey String
subjectKey String
visibility String @default("READONLY")
updatedById String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([fieldKey, subjectKey])
@@index([fieldKey])
@@index([subjectKey])
}
model NewProductTerminationFeedback {
id String @id @default(cuid())
openingCode String @unique
reasonCategory String
reason String @db.Text
feedbackById String?
feedbackByName String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([updatedAt])
}
model NewProductTerminationReasonArchive {
id String @id @default(cuid())
processInstanceId String @unique
openingCode String?
reasonCategory String
reason String? @db.Text
fetchedAt DateTime @default(now())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([openingCode])
@@index([fetchedAt])
}
// ============================================
// 五金件库(标准五金件复用系统)
// ============================================
// 五金件分类
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?
motionType String @default("spin")
motionPivot Json?
motionAxis Json?
spinTurns Float @default(3)
strikeDistanceRatio Float @default(0.2)
articulationConfig Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// ============================================
// 供应链脚本监控
// ============================================
enum ScriptMonitorNetworkMode {
INTRANET
INTERNET
AUTO
}
enum ScriptMonitorConnectionKind {
INTRANET
INTERNET
}
enum ScriptMonitorJobType {
CRON
PM2
SYSTEMD
MANUAL
OTHER
}
enum ScriptMonitorJobStatus {
UNKNOWN
RUNNING
SUCCESS
FAILED
SILENT
DISABLED
}
enum ScriptMonitorRunStatus {
RUNNING
SUCCESS
FAILED
}
enum ScriptMonitorCommandStatus {
PENDING
RUNNING
SUCCESS
FAILED
EXPIRED
}
model ScriptMonitorAgent {
id String @id @default(cuid())
name String
hostname String?
networkMode ScriptMonitorNetworkMode @default(AUTO)
lastConnectionKind ScriptMonitorConnectionKind?
tokenHash String @unique
tokenHint String
isActive Boolean @default(true)
os String?
agentVersion String?
reportIntervalSec Int @default(60)
lastSeenAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
scripts ScriptMonitorScript[]
runs ScriptMonitorRun[]
commands ScriptMonitorCommand[]
@@index([isActive, lastSeenAt])
}
model ScriptMonitorScript {
id String @id @default(cuid())
agentId String
agent ScriptMonitorAgent @relation(fields: [agentId], references: [id], onDelete: Cascade)
externalKey String
name String
taskDescription String @default("") @db.Text
jobType ScriptMonitorJobType
schedule String?
commandPreview String? @db.Text
expectedIntervalSec Int?
status ScriptMonitorJobStatus @default(UNKNOWN)
isEnabled Boolean @default(true)
canManualRun Boolean @default(false)
lastStartedAt DateTime?
lastFinishedAt DateTime?
lastDurationMs Int?
lastExitCode Int?
lastResultSummary String? @db.Text
lastOutputTail String? @db.Text
lastErrorTail String? @db.Text
lastReportedAt DateTime
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
runs ScriptMonitorRun[]
commands ScriptMonitorCommand[]
@@unique([agentId, externalKey])
@@index([agentId, status])
@@index([status, lastReportedAt])
}
model ScriptMonitorRun {
id String @id @default(cuid())
agentId String
agent ScriptMonitorAgent @relation(fields: [agentId], references: [id], onDelete: Cascade)
scriptId String
script ScriptMonitorScript @relation(fields: [scriptId], references: [id], onDelete: Cascade)
externalRunId String
status ScriptMonitorRunStatus
trigger String?
startedAt DateTime
finishedAt DateTime?
durationMs Int?
exitCode Int?
resultSummary String? @db.Text
outputTail String? @db.Text
errorTail String? @db.Text
createdAt DateTime @default(now())
@@unique([agentId, externalRunId])
@@index([scriptId, startedAt(sort: Desc)])
@@index([agentId, startedAt(sort: Desc)])
}
model ScriptMonitorCommand {
id String @id @default(cuid())
agentId String
agent ScriptMonitorAgent @relation(fields: [agentId], references: [id], onDelete: Cascade)
scriptId String
script ScriptMonitorScript @relation(fields: [scriptId], references: [id], onDelete: Cascade)
status ScriptMonitorCommandStatus @default(PENDING)
requestedById String?
requestedByName String?
requestedAt DateTime @default(now())
expiresAt DateTime
claimedAt DateTime?
finishedAt DateTime?
externalRunId String?
errorMessage String? @db.Text
@@index([agentId, status, requestedAt])
@@index([scriptId, requestedAt(sort: Desc)])
}
/// 首页快捷网址(管理员配置,内部人员在首页下拉框中点击跳转)
model HomeQuickLink {
id String @id @default(cuid())
title String
description String @default("")
url String
iconKey String @default("link")
sortOrder Int @default(0)
enabled Boolean @default(true)
createdBy String
updatedBy String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([enabled, sortOrder])
}
/// BOM agent 旁路引擎的单 SKU 估算缓存(工程域;主键语义:skuId + engineVersion)。
/// 纯新增表,不与现有业务模型建外键关系;在 agent 流程过实测闸门前仅供工程侧工具使用。
model BomCostingAgentSkuEstimate {
id String @id @default(cuid())
caseId String
skuId String
externalSkuId String @default("")
engineVersion String
status String
estimate Json
usage Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([skuId, engineVersion])
@@index([caseId])
}
/// 物流到货签收时长看板的订单明细快照批次。
/// 每日由 scripts/sync-logistics-arrival-signing-duration-snapshot.ts 生成一批;
/// 看板只读取 status = COMPLETED 的最新批次,写入中途的批次对读路径不可见。
model LogisticsArrivalSigningDurationSnapshotRun {
id String @id @default(cuid())
snapshotDate DateTime @unique @db.Date
status String @default("RUNNING")
windowFrom String @default("")
windowTo String @default("")
/// 该批快照实际覆盖到的最大签收时间('YYYY-MM-DD HH:MM:SS')。跑批当天数据只到跑批那一刻,
/// 读路径据此把"声明窗口"收窄到"实际覆盖",避免把当天误判为已完整覆盖。
dataThrough String @default("")
rowCount Int @default(0)
message String @default("")
startedAt DateTime @default(now())
finishedAt DateTime?
@@index([status, snapshotDate], map: "LogisticsArrivalSigningDurationSnapshotRun_status_date_idx")
}
/// 物流到货签收时长看板的订单明细快照行。
/// 字段与远端报表视图 rpt.jst_order_express_details 的看板口径一一对应,
/// 条码/供应商/采购在快照时点固化,避免看板每次刷新都回查远端明细与产品档案。
model LogisticsArrivalSigningDurationSnapshot {
id String @id @default(cuid())
snapshotDate DateTime @db.Date
rowKey String
innerOrderId String
soId String @default("")
logisticId String @default("")
logisticCompanyName String @default("")
logisticStatusName String @default("")
logisticReturnFlag String @default("")
orderLabels String @default("")
remark String @default("")
buyerMessage String @default("")
shopName String @default("")
platformName String @default("")
warehouseName String @default("")
receiverAddress String @default("")
payTime String?
sendTime String?
fetchTime String?
signTime String?
barcode String @default("")
productName String @default("")
supplier String @default("")
purchaser String @default("")
productItems Json?
createdAt DateTime @default(now())
@@unique([snapshotDate, rowKey], map: "LogisticsArrivalSigningDurationSnapshot_date_row_key_key")
@@index([snapshotDate, signTime], map: "LogisticsArrivalSigningDurationSnapshot_date_sign_time_idx")
/// 看板热路径的覆盖索引:时间戳与 4 个维度列直接随索引读取,避免整表扫描。
@@index([snapshotDate, signTime], map: "LogisticsArrivalSigningDurationSnapshot_cover_idx")
}
/// 限价管理 · 破价订单审核:聚水潭订单实收与限价表比对后生成的破价单
model SupplyChainBreachOrder {
id String @id @default(cuid())
platform String @default("")
shopName String @default("")
soId String @default("")
oId String @default("")
payDate DateTime?
itemCode String @default("")
shopItemCode String @default("")
shopStyleCode String @default("")
linkGroupKey String @default("")
qty Int @default(1)
buyerPaidAmount Decimal @default(0) @db.Decimal(18, 4)
platformBearAmount Decimal @default(0) @db.Decimal(18, 4)
sellerIncomeAmount Decimal @default(0) @db.Decimal(18, 4)
limitUnitPrice Decimal? @db.Decimal(18, 4)
limitTotal Decimal? @db.Decimal(18, 4)
gapAmount Decimal? @db.Decimal(18, 4)
ownerName String @default("")
profitCenter String @default("")
orderStatus String @default("")
refundStatus String @default("")
listedAt DateTime?
flowEndDate DateTime?
status String @default("PENDING_APPEAL")
appealType String @default("")
appealScope String @default("ORDER")
processNo String @default("")
appealRound Int @default(1)
previousAppeal Json?
appealDesc String @default("") @db.Text
appealImages Json @default("[]")
appliedFromLink Boolean @default(false)
autoSuggested String @default("")
autoSuggestedNote String @default("") @db.Text
reviewedBy String @default("")
reviewedAt DateTime?
reviewDecision String @default("")
reviewReason String @default("")
reviewNote String @default("") @db.Text
fineAmount Decimal @default(0) @db.Decimal(10, 2)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([oId, itemCode])
@@index([status, payDate(sort: Desc)])
@@index([linkGroupKey])
@@index([ownerName, status])
@@index([shopName, itemCode])
}
/// 破价单操作日志(系统生成、申诉、审核)
model SupplyChainBreachOrderLog {
id String @id @default(cuid())
orderId String
action String
operatorId String @default("")
operatorName String @default("")
detail Json @default("{}")
createdAt DateTime @default(now())
@@index([orderId, createdAt])
}
/// 限价表(链接限价):店铺名称 + 商品编码 → 限价单价、上架时间、限价流程结束日期
model SupplyChainLinkLimitPrice {
id String @id @default(cuid())
shopName String
itemCode String
spu String @default("")
limitUnitPrice Decimal @db.Decimal(18, 4)
listedAt DateTime?
flowEndDate DateTime?
source String @default("MANUAL")
ownerName String @default("")
updatedById String @default("")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([shopName, itemCode])
@@index([itemCode])
}
/// 限价管理 · 破价判定规则配置(单行 JSON + 修改留痕)
model SupplyChainBreachRuleConfig {
id String @id @default("default")
data Json @default("{}")
updatedById String @default("")
updatedByName String @default("")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
/// 限价流程同步 · 货号负责人限价(按货号维度;来源:限价流程的「期望日常限价」,
/// 同价申请 / 活动限价类流程不覆盖)。
model SupplyChainStyleLimitPrice {
id String @id @default(cuid())
styleCode String @unique
ownerLimitPrice Decimal? @db.Decimal(18, 4)
processInstanceId String @default("")
processNo String @default("")
applyType String @default("")
initiator String @default("")
/// 流程审核通过时间(限价生效时间)
approvedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
/// 限价流程同步 · 价格授权(同价申请 / 活动限价;供破价判定豁免与超期判定)。
model SupplyChainLimitFlowAuth {
id String @id @default(cuid())
processInstanceId String
processNo String @default("")
applyType String @default("")
styleCode String @default("")
spu String @default("")
/// 同价开始时间 = 流程审核通过时间
approvedAt DateTime?
/// 活动开始时间(同价类型为空)
startDate DateTime?
/// 结束时间(同价 / 活动共用)
endDate DateTime?
/// 活动限价:调整后限价(价格 ≥ 该值不判破价)
adjustedLimitPrice Decimal? @db.Decimal(18, 4)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([processInstanceId, styleCode, applyType])
@@index([styleCode])
@@index([applyType])
}
/// 商品库存快照:条码 × 实际仓库粒度的库存数据,由 15 分钟定时任务全量替换写入。
/// 数据来源于聚水潭库存明细/云仓链路与产品档案,失败时保留上一份快照。
model ProductInventorySnapshot {
id String @id @default(cuid())
barcode String
wmsCoId Int
productName String @default("")
category String @default("")
sku String @default("")
productionStatus String @default("")
supplier String @default("")
purchaser String @default("")
stockQty Int @default(0)
orderLockQty Int @default(0)
availableQty Int @default(0)
cloudAvailableQty Int @default(0)
warehouseName String @default("")
warehouseAttribute String @default("")
warehouseOwnership String @default("")
warehouseHead String @default("")
syncedAt DateTime
createdAt DateTime @default(now())
@@unique([barcode, wmsCoId])
@@index([warehouseName])
@@index([syncedAt])
}
/// 商品库存快照同步运行记录:用于排查定时任务状态与失败原因,不记录连接串或 SQL 载荷。
model ProductInventorySnapshotRun {
id String @id @default(cuid())
startedAt DateTime @default(now())
completedAt DateTime?
status String @db.VarChar(24)
rowsRead Int @default(0)
rowsWritten Int @default(0)
syncedAt DateTime?
errorMessage String? @db.Text
@@index([startedAt])
@@index([status, startedAt])
}