// 新的 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("") barcodeCreatedAt DateTime? postStopSkuStatus String @default("") sales30dWithBig String @default("") sales30dWithBigSku String @default("") 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 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]) }