generator client { provider = "prisma-client-js" previewFeatures = [] binaryTargets = ["native", "linux-musl-openssl-3.0.x"] engineType = "library" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } model User { id String @id @default(uuid()) email String @unique username String @unique password String role Role @default(USER) isActive Boolean @default(false) canDownload Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt department String @default("") dingtalkUserId String? @unique loginType String @default("normal") realName String? phone String? avatar String? managedDepartment String? isSuperAdmin Boolean @default(false) departmentId String? employmentStatus String? @default("active") resignationDate DateTime? transferDate DateTime? transferToDeptId String? employmentNote String? statusChangedBy String? statusChangedAt DateTime? status String @default("pending") lastLoginAt DateTime? lastLoginIp String? loginCount Int @default(0) archivedAt DateTime? managedDepartments Department[] @relation("DepartmentManager") downloadLogs MaterialDownloadLog[] favorites MaterialFavorite[] likes MaterialLike[] materialLogs MaterialLog[] materialTransfersFrom MaterialTransfer[] @relation("MaterialTransferFromUser") materialTransfersTo MaterialTransfer[] @relation("MaterialTransferToUser") materialTransfersAsOperator MaterialTransfer[] @relation("MaterialTransferOperator") materials Material[] @relation("MaterialOwner") taskDetailRequestedMaterials Material[] @relation("TaskDetailRequester") productPackageUploadJobs ProductPackageUploadJob[] @relation("ProductPackageUploadJobOperator") departmentRef Department? @relation("UserDepartment", fields: [departmentId], references: [id]) transferToDept Department? @relation("UserTransferDepartment", fields: [transferToDeptId], references: [id]) employeeBindings UserEmployeeBinding[] permissionInherits UserPermissionInherit[] submittedTaskDetailChangeRequests TaskDetailChangeRequest[] @relation("TaskDetailChangeRequesterUser") managedTaskDetailChangeRequests TaskDetailChangeRequest[] @relation("TaskDetailChangeManagerUser") brandKnowledgeUploads BrandKnowledge[] @relation("BrandKnowledgeUploader") brandKnowledgeLikes BrandKnowledgeLike[] brandKnowledgeFavorites BrandKnowledgeFavorite[] @@index([status]) @@index([archivedAt]) @@index([departmentId]) @@index([employmentStatus]) @@index([transferToDeptId]) @@map("users") } model Material { id String @id @default(uuid()) previewUrl String productModel String reportTitle String? @map("report_title") reportKind String? @map("report_kind") certificateExpiresOn DateTime? @db.Date @map("certificate_expires_on") certificateTestedOn DateTime? @db.Date @map("certificate_tested_on") certificateNumber String? @map("certificate_number") certificateUsageInstructions String? @map("certificate_usage_instructions") reportUsageRules String? @map("report_usage_rules") categoryName String sceneName String materialType String @default("3DMAX") renderer String @default("CR") authorName String zipFileName String @default("") zipFilePath String @default("") zipFileSize BigInt @default(0) downloadCount Int @default(0) likeCount Int @default(0) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt userId String displayImages String[] @default([]) platformName String @default("") uploadId String? videoSourceUrl String? videoPreview540Url String? videoPreviewClipUrl String? videoCoverUrl String? videoDuration Int? videoProcessingStatus String? transcodeLastError String? @map("transcodelasterror") transcodeAttempts Int? @map("transcodeattempts") transcodeLeaseToken String? @map("transcode_lease_token") transcodeLeaseExpiresAt DateTime? @map("transcode_lease_expires_at") deletedAt DateTime? productCategoryName String @default("") /// 直播切片专用:直播日期 / 主播 / 可选关联用户 / 直播平台 liveDate DateTime? @map("live_date") liveHost String? @map("live_host") liveHostUserId String? @map("live_host_user_id") livePlatform String? @map("live_platform") originalOwnerId String? transferredAt DateTime? transferredBy String? transferNote String? /// 原归属人离职后,素材托管给的部门。托管只改变"谁负责维护", /// 不改变 userId 与 authorName——素材是谁做的,永远记在谁名下。 custodyDepartmentId String? @map("custody_department_id") custodyAt DateTime? @map("custody_at") custodyReason String? @map("custody_reason") taskNo String? @map("task_no") dingtalkApprovalNo String? @map("dingtalk_approval_no") taskStatus String? @map("task_status") taskFetchedAt DateTime? @map("task_fetched_at") isFullSet Boolean @default(false) @map("is_full_set") isAiGenerated Boolean @default(false) @map("is_ai_generated") taskDetailSourceType String? @map("task_detail_source_type") taskDetailRequesterUserId String? @map("task_detail_requester_user_id") taskDetailRequesterName String? @map("task_detail_requester_name") taskDetailDemandType String? @map("task_detail_demand_type") taskDetailPrivate Boolean @default(false) @map("task_detail_private") /// 专用 init 建档时写 true,完成落库后写 false。列表/筛选只读这个标记, /// 不再每次去资产表现场判断是不是上传空壳。 isIncompleteShell Boolean @default(false) @map("is_incomplete_shell") assets MaterialAsset[] favorites MaterialFavorite[] likes MaterialLike[] skus MaterialSku[] transfers MaterialTransfer[] transcodeProgresses VideoTranscodeProgress[] @relation("MaterialTranscodeProgress") productPackageUploadJobs ProductPackageUploadJob[] taskDetailChangeRequests TaskDetailChangeRequest[] user User @relation("MaterialOwner", fields: [userId], references: [id], onDelete: Cascade) taskDetailRequester User? @relation("TaskDetailRequester", fields: [taskDetailRequesterUserId], references: [id], onDelete: SetNull) custodyDepartment Department? @relation("MaterialCustodyDepartment", fields: [custodyDepartmentId], references: [id], onDelete: SetNull) @@index([userId]) @@index([custodyDepartmentId]) @@index([taskDetailRequesterUserId]) @@index([createdAt]) @@index([categoryName]) @@index([sceneName]) @@index([platformName]) @@index([productCategoryName]) @@index([materialType]) @@index([liveDate]) @@index([downloadCount]) @@index([likeCount]) @@index([deletedAt]) @@index([originalOwnerId]) @@index([transferredAt]) @@index([taskNo]) @@index([dingtalkApprovalNo]) @@index([taskStatus]) @@index([isIncompleteShell]) @@index([isFullSet]) @@index([productModel]) @@index([reportTitle]) @@index([authorName]) @@index([transcodeLeaseExpiresAt, transcodeLeaseToken], map: "materials_transcode_lease_idx") @@map("materials") } model BrandKnowledge { id String @id @default(uuid()) title String knowledgeType String @map("knowledge_type") brand String @default("") productCategoryName String @default("") @map("product_category_name") fileName String @map("file_name") filePath String @map("file_path") mimeType String @map("mime_type") fileSize BigInt @map("file_size") previewImageName String? @map("preview_image_name") previewImagePath String? @map("preview_image_path") previewImageSlices String[] @default([]) @map("preview_image_slices") previewImageMimeType String? @map("preview_image_mime_type") previewImageSize BigInt? @map("preview_image_size") downloadCount Int @default(0) @map("download_count") uploadedBy String @map("uploaded_by") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") deletedAt DateTime? @map("deleted_at") uploader User @relation("BrandKnowledgeUploader", fields: [uploadedBy], references: [id], onDelete: Cascade) likes BrandKnowledgeLike[] favorites BrandKnowledgeFavorite[] @@index([knowledgeType]) @@index([brand]) @@index([productCategoryName]) @@index([createdAt]) @@index([uploadedBy]) @@index([deletedAt]) @@map("brand_knowledge") } model BrandKnowledgeLike { id String @id @default(uuid()) knowledgeId String @map("knowledge_id") userId String @map("user_id") createdAt DateTime @default(now()) @map("created_at") knowledge BrandKnowledge @relation(fields: [knowledgeId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([knowledgeId, userId]) @@index([knowledgeId]) @@index([userId]) @@map("brand_knowledge_likes") } model BrandKnowledgeFavorite { id String @id @default(uuid()) knowledgeId String @map("knowledge_id") userId String @map("user_id") createdAt DateTime @default(now()) @map("created_at") knowledge BrandKnowledge @relation(fields: [knowledgeId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([knowledgeId, userId]) @@index([knowledgeId]) @@index([userId]) @@map("brand_knowledge_favorites") } model ProductPackageUploadJob { id String @id @default(uuid()) materialId String @map("material_id") operatorUserId String @map("operator_user_id") taskNo String? @map("task_no") dingtalkApprovalNo String? @map("dingtalk_approval_no") assets Json options Json? status String stage String progress Int @default(0) errorMessage String? @map("error_message") createdAt DateTime @default(now()) @map("created_at") startedAt DateTime? @map("started_at") completedAt DateTime? @map("completed_at") updatedAt DateTime @updatedAt @map("updated_at") workerId String? @map("worker_id") leaseToken String? @map("lease_token") leaseExpiresAt DateTime? @map("lease_expires_at") material Material @relation(fields: [materialId], references: [id], onDelete: Cascade) operator User @relation("ProductPackageUploadJobOperator", fields: [operatorUserId], references: [id], onDelete: Cascade) @@index([materialId]) @@index([operatorUserId, createdAt]) @@index([status, createdAt]) @@index([status, leaseExpiresAt], map: "product_package_upload_jobs_lease_idx") @@map("product_package_upload_jobs") } model MaterialLike { id String @id @default(uuid()) materialId String userId String createdAt DateTime @default(now()) material Material @relation(fields: [materialId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([materialId, userId]) @@index([materialId]) @@index([userId]) @@map("material_likes") } model MaterialFavorite { id String @id @default(uuid()) materialId String userId String createdAt DateTime @default(now()) material Material @relation(fields: [materialId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([materialId, userId]) @@index([materialId]) @@index([userId]) @@map("material_favorites") } model MaterialDownloadLog { id String @id @default(uuid()) materialId String userId String downloadedAt DateTime @default(now()) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([materialId]) @@index([userId]) @@index([downloadedAt]) @@index([materialId, userId, downloadedAt]) @@map("material_download_logs") } model MaterialLog { id String @id @default(uuid()) materialId String userId String action String changes String? timestamp DateTime @default(now()) user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([materialId]) @@index([userId]) @@index([timestamp]) @@map("material_logs") } // 永久删除的持久化凭证;不关联 Material,素材删除后仍保留清理清单与操作人。 model MaterialPurgeJob { id String @id @default(uuid()) materialId String @unique @map("material_id") operatorUserId String @map("operator_user_id") materialSnapshot Json @map("material_snapshot") files Json status String @default("queued") attempts Int @default(0) errorMessage String? @map("error_message") leaseToken String? @map("lease_token") leaseExpiresAt DateTime? @map("lease_expires_at") nextAttemptAt DateTime @default(now()) @map("next_attempt_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") completedAt DateTime? @map("completed_at") audits MaterialPurgeAudit[] @@index([status, nextAttemptAt]) @@map("material_purge_jobs") } model MaterialPurgeAudit { id String @id @default(uuid()) jobId String @map("job_id") event String operatorUserId String? @map("operator_user_id") message String? createdAt DateTime @default(now()) @map("created_at") job MaterialPurgeJob @relation(fields: [jobId], references: [id], onDelete: Restrict) @@index([jobId, createdAt]) @@map("material_purge_audits") } model MaterialSku { id String @id @default(uuid()) materialId String @map("material_id") skuName String @map("sku_name") filePath String @map("file_path") fileSize BigInt @default(0) @map("file_size") sortOrder Int @default(0) @map("sort_order") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") fileName String? @map("file_name") previewUrl String? @map("preview_url") deletedAt DateTime? @map("deleted_at") whiteBackgroundSourceType String? @map("white_background_source_type") whiteBackgroundUploadedBy String? @map("white_background_uploaded_by") whiteBackgroundUploadedAt DateTime? @map("white_background_uploaded_at") barcodes MaterialSkuBarcode[] material Material @relation(fields: [materialId], references: [id], onDelete: Cascade) @@index([materialId]) @@index([skuName, deletedAt]) @@index([sortOrder]) @@index([deletedAt]) @@index([whiteBackgroundSourceType]) @@index([whiteBackgroundUploadedBy]) @@index([whiteBackgroundUploadedAt]) @@map("material_skus") } model ProductCategory { id String @id @default(uuid()) name String @unique createdAt DateTime @default(now()) @@map("product_categories") } model Category { id String @id @default(uuid()) name String @unique createdAt DateTime @default(now()) @@map("categories") } model Scene { id String @id @default(uuid()) name String @unique createdAt DateTime @default(now()) @@map("scenes") } model Platform { id String @id @default(uuid()) name String @unique createdAt DateTime @default(now()) @@map("platforms") } model SystemSettings { id String @id @default(uuid()) headerBgColor String @default("#1f2937") siteName String @default("由壹点3D素材") updatedAt DateTime @updatedAt createdAt DateTime @default(now()) headerLogoUrl String @default("/brand/header-logo.png") loginLogoUrl String @default("/brand/login-logo.png") s3AccessKey String @default("") s3Bucket String @default("") s3Endpoint String @default("") s3PublicUrl String @default("") s3Region String @default("us-east-1") s3SecretKey String @default("") maintenanceEnabled Boolean @default(false) @map("maintenance_enabled") maintenanceMessage String? @map("maintenance_message") maintenanceStartTime DateTime? @map("maintenance_start_time") maintenanceEndTime DateTime? @map("maintenance_end_time") @@map("system_settings") } model Department { id String @id @default(uuid()) name String @unique createdAt DateTime @default(now()) parentId String? level Int @default(1) status String @default("active") managerId String? code String? @unique description String? sortOrder Int @default(0) updatedAt DateTime @updatedAt manager User? @relation("DepartmentManager", fields: [managerId], references: [id]) parent Department? @relation("DepartmentHierarchy", fields: [parentId], references: [id]) children Department[] @relation("DepartmentHierarchy") users User[] @relation("UserDepartment") transferredUsers User[] @relation("UserTransferDepartment") custodyMaterials Material[] @relation("MaterialCustodyDepartment") @@index([parentId]) @@index([status]) @@index([level]) @@index([managerId]) @@map("departments") } model Danmaku { id String @id @default(uuid()) content String username String userId String createdAt DateTime @default(now()) @@index([createdAt]) @@map("danmakus") } model DepartmentMaterialPermission { id String @id @default(uuid()) department String materialType String canView Boolean @default(false) canDownload Boolean @default(false) canShare Boolean @default(true) linkedToDepartment String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt canDownloadSource Boolean @default(false) @map("can_download_source") @@unique([department, materialType]) @@index([department]) @@index([materialType]) @@index([linkedToDepartment]) @@map("department_material_permissions") } /// 权限组:一份权限,同时挂部门和人。部门进组是底子,人进组是加餐。 model MaterialPermissionPack { id String @id @default(uuid()) slug String @unique name String videoEarlyAccess Boolean @default(false) @map("video_early_access") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") items MaterialPermissionPackItem[] departments MaterialPermissionPackDepartment[] inheritors UserPermissionInherit[] @@map("material_permission_packs") } model MaterialPermissionPackItem { id String @id @default(uuid()) packId String @map("pack_id") materialType String @map("material_type") canView Boolean @default(false) @map("can_view") canDownload Boolean @default(false) @map("can_download") canDownloadSource Boolean @default(false) @map("can_download_source") canShare Boolean @default(true) @map("can_share") canEditAssets Boolean @default(false) @map("can_edit_assets") canAccessPrivate Boolean @default(false) @map("can_access_private") pack MaterialPermissionPack @relation(fields: [packId], references: [id], onDelete: Cascade) @@unique([packId, materialType], map: "material_permission_pack_item_pack_type_key") @@index([packId], map: "material_permission_pack_item_pack_idx") @@map("material_permission_pack_items") } /// 权限组里的部门。子部门没单独进组时,沿用最近的上级组。 model MaterialPermissionPackDepartment { id String @id @default(uuid()) packId String @map("pack_id") organizationUnitId String? @unique(map: "material_permission_pack_dept_unit_key") @map("organization_unit_id") /// 迁移前的路径快照,仅供失效关联的展示与清理;鉴权使用关联部门的当前路径。 departmentPath String @map("department_path") createdAt DateTime @default(now()) @map("created_at") pack MaterialPermissionPack @relation(fields: [packId], references: [id], onDelete: Cascade) organizationUnit OrganizationUnit? @relation(fields: [organizationUnitId], references: [id], onDelete: Restrict) @@index([packId]) @@map("material_permission_pack_departments") } /// 人可以进多组。只按 userId 绑定,不按姓名。 model UserPermissionInherit { id String @id @default(uuid()) userId String @map("user_id") packId String @map("pack_id") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) pack MaterialPermissionPack @relation(fields: [packId], references: [id], onDelete: Restrict) @@unique([userId, packId], map: "user_permission_inherit_user_pack_key") @@index([packId], map: "user_permission_inherit_pack_idx") @@index([userId], map: "user_permission_inherit_user_idx") @@map("user_permission_inherits") } model MaterialTransfer { id String @id @default(uuid()) materialId String fromUserId String toUserId String transferredBy String transferNote String? transferredAt DateTime @default(now()) fromUser User @relation("MaterialTransferFromUser", fields: [fromUserId], references: [id]) material Material @relation(fields: [materialId], references: [id], onDelete: Cascade) toUser User @relation("MaterialTransferToUser", fields: [toUserId], references: [id]) operator User @relation("MaterialTransferOperator", fields: [transferredBy], references: [id]) @@index([materialId]) @@index([fromUserId]) @@index([toUserId]) @@index([transferredAt]) @@map("material_transfers") } model MaterialAsset { id String @id @default(uuid()) materialId String @map("material_id") assetType String @map("asset_type") fileUrl String @map("file_url") fileName String? @map("file_name") fileSize BigInt @default(0) @map("file_size") sortOrder Int @default(0) @map("sort_order") skuCode String? @map("sku_code") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") material Material @relation(fields: [materialId], references: [id], onDelete: Cascade) barcodes MaterialAssetBarcode[] @@index([materialId]) @@index([assetType]) @@index([sortOrder]) @@map("material_assets") } model MaterialAssetBarcode { id String @id @default(uuid()) assetId String @map("asset_id") barcode String createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") asset MaterialAsset @relation(fields: [assetId], references: [id], onDelete: Cascade) @@unique([assetId, barcode]) @@index([barcode]) @@index([assetId]) @@map("material_asset_barcodes") } model MaterialSkuBarcode { id String @id @default(uuid()) skuId String @map("sku_id") barcode String deletedAt DateTime? @map("deleted_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") sku MaterialSku @relation(fields: [skuId], references: [id], onDelete: Cascade) @@index([barcode]) @@index([skuId]) @@index([deletedAt]) @@map("material_sku_barcodes") } model ApiKey { id String @id @default(uuid()) name String key String @unique secretCiphertext String? @map("secret_ciphertext") secretNonce String? @map("secret_nonce") secretAuthTag String? @map("secret_auth_tag") isActive Boolean @default(true) @map("is_active") createdBy String @map("created_by") lastUsedAt DateTime? @map("last_used_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") callLogs ApiCallLog[] externalMaterialImportJobs ExternalMaterialImportJob[] idempotencyRecords ExternalApiIdempotencyRecord[] @@index([key]) @@index([isActive]) @@map("api_keys") } model ApiKeyRevealAudit { id String @id @default(uuid()) apiKeyId String @map("api_key_id") viewerUserId String @map("viewer_user_id") createdAt DateTime @default(now()) @map("created_at") @@index([apiKeyId, createdAt]) @@index([viewerUserId, createdAt]) @@map("api_key_reveal_audits") } model ExternalApiIdempotencyRecord { id String @id @default(uuid()) apiKeyId String @map("api_key_id") operationType String @map("operation_type") clientRequestId String @map("client_request_id") requestHash String @map("request_hash") status String responseStatus Int? @map("response_status") responseBody Json? @map("response_body") completedAt DateTime? @map("completed_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") apiKey ApiKey @relation(fields: [apiKeyId], references: [id], onDelete: Cascade) @@unique([apiKeyId, operationType, clientRequestId]) @@index([apiKeyId]) @@index([operationType]) @@index([clientRequestId]) @@index([status]) @@index([createdAt]) @@map("external_api_idempotency_records") } model ApiCallLog { id String @id @default(uuid()) apiKeyId String @map("api_key_id") endpoint String method String taskNo String? @map("task_no") queryType String? @map("query_type") queryValue String? @map("query_value") statusCode Int @map("status_code") duration Int ipAddress String? @map("ip_address") userAgent String? @map("user_agent") createdAt DateTime @default(now()) @map("created_at") apiKey ApiKey @relation(fields: [apiKeyId], references: [id], onDelete: Cascade) @@index([apiKeyId]) @@index([taskNo]) @@index([queryType]) @@index([queryValue]) @@index([createdAt]) @@map("api_call_logs") } model ExternalMaterialImportJob { id String @id @default(uuid()) apiKeyId String @map("api_key_id") clientRequestId String @map("client_request_id") materialType String @map("material_type") taskNo String @map("task_no") status String resolvedUserId String? @map("resolved_user_id") requestPayload Json @map("request_payload") submissionSnapshot Json @map("submission_snapshot") resultMaterialId String? @map("result_material_id") errorCode String? @map("error_code") errorMessage String? @map("error_message") completedAt DateTime? @map("completed_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") apiKey ApiKey @relation(fields: [apiKeyId], references: [id], onDelete: Cascade) @@unique([apiKeyId, clientRequestId]) @@index([taskNo, status]) @@index([status]) @@index([createdAt]) @@map("external_material_import_jobs") } model PerformanceSurveyEmployeeProfile { id String @id @default(uuid()) dingtalkUserId String? @unique @map("dingtalk_user_id") dingEmail String? @map("ding_email") jobNumber String? @unique @map("job_number") flowerName String? @map("flower_name") realName String? @map("real_name") displayName String @map("display_name") departmentName String? @map("department_name") departmentPath String? @map("department_path") positionTitle String? @map("position_title") positionLevel String? @map("position_level") levelType String? @map("level_type") isLeadership Boolean @default(false) @map("is_leadership") isDingtalkAdmin Boolean @default(false) @map("is_dingtalk_admin") isDingtalkBoss Boolean @default(false) @map("is_dingtalk_boss") isDingtalkSenior Boolean @default(false) @map("is_dingtalk_senior") managerDingtalkUserId String? @map("manager_dingtalk_user_id") managerJobNumber String? @map("manager_job_number") managerName String? @map("manager_name") employmentStatus String? @map("employment_status") /// 规范化的在职状态:ACTIVE / RESIGNED / UNKNOWN。 /// employmentStatus 是上游原样值,口径混杂(有数字码 2/3/5,也有「正式」「离职」「试用」等中文, /// 还有大量空值),靠关键词匹配判断在职会大面积误判,因此单独规范化一列供运行时判定使用。 employmentState String? @default("UNKNOWN") @map("employment_state") /// 最后工作日,来自上游离职表 resignedAt DateTime? @map("resigned_at") /// 离职交接人的钉钉唯一值,上游填写率较低,仅作参考 handoverDingtalkUserId String? @map("handover_dingtalk_user_id") workPlace String? @map("work_place") sourcePayload Json? @map("source_payload") syncedAt DateTime? @map("synced_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") evaluatorAssignments PerformanceSurveyAssignment[] @relation("PerformanceSurveyEvaluator") targetRecords PerformanceSurveyTarget[] @relation("PerformanceSurveyTargetEmployee") responses PerformanceSurveyResponse[] @relation("PerformanceSurveyRespondentEmployee") submittedWorkItems PerformanceSurveyWorkItem[] @relation("PerformanceSurveyWorkItemSubmitter") workItemScores PerformanceSurveyWorkItemScore[] @relation("PerformanceSurveyWorkItemScoreEvaluator") notifications PerformanceSurveyNotification[] manualPositionAssignments PerformanceSurveyEmployeePositionAssignment[] departmentMemberships PerformanceSurveyEmployeeDepartmentMembership[] userBindings UserEmployeeBinding[] submittedWorkReviewProjects WorkReviewProject[] @relation("WorkReviewSubmitter") managedWorkReviewProjects WorkReviewProject[] @relation("WorkReviewManager") finalWorkReviewProjects WorkReviewProject[] @relation("WorkReviewFinalReviewer") workReviews WorkReviewReview[] @relation("WorkReviewReviewer") submittedTaskDetailChangeRequests TaskDetailChangeRequest[] @relation("TaskDetailChangeRequesterEmployee") managedTaskDetailChangeRequests TaskDetailChangeRequest[] @relation("TaskDetailChangeManagerEmployee") ledTeamFunctionConfigs TeamFunctionConfig[] @@index([displayName]) @@index([dingEmail]) @@index([departmentName]) @@index([positionLevel]) @@index([levelType]) @@index([isLeadership]) @@index([isDingtalkAdmin]) @@index([managerDingtalkUserId]) @@index([employmentStatus]) @@index([employmentState]) @@map("performance_survey_employee_profiles") } /// 共享组织架构,直接镜像上游 HR 系统的部门树(rpt.hr_groups)。 /// /// 此前系统没有真正的组织表:部门树是遍历已同步员工的 departmentPath 字符串 /// 现场拼出来的,因而只能看到"有人的部门",且同名部门会被合并、父子归属取决于 /// 谁先被遍历到。上游其实是一棵完整的单根树(单一根节点、零孤儿节点), /// 直接同步即可,不必倒推。 model OrganizationUnit { id String @id @default(uuid()) /// 上游 hr_groups.Id sourceDeptId Int @unique @map("source_dept_id") name String parentId String? @map("parent_id") /// 上游 hr_groups.parent_id,同步期间用于重建父子关系 sourceParentId Int? @map("source_parent_id") /// 以 / 分隔的全路径,展示与前缀匹配用 path String level Int @default(1) /// 本次同步是否仍存在于上游;上游删除的部门置为 false 而非物理删除 isActive Boolean @default(true) @map("is_active") syncedAt DateTime? @map("synced_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") parent OrganizationUnit? @relation("OrganizationHierarchy", fields: [parentId], references: [id]) children OrganizationUnit[] @relation("OrganizationHierarchy") permissionGroupDepartments MaterialPermissionPackDepartment[] teamFunctionConfig TeamFunctionConfig? @@index([parentId]) @@index([sourceParentId]) @@index([path]) @@index([isActive]) @@map("organization_units") } /// 职责配置独立于 HR 镜像;部门和负责人只保存稳定身份,名称在读取时取花名册。 model TeamFunctionConfig { id String @id @default(uuid()) organizationUnitId String @unique @map("organization_unit_id") leadEmployeeProfileId String? @map("lead_employee_profile_id") description String @default("") branches Json @default("[]") revision Int @default(1) updatedByUserId String @map("updated_by_user_id") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") organizationUnit OrganizationUnit @relation(fields: [organizationUnitId], references: [id], onDelete: Restrict) leadEmployeeProfile PerformanceSurveyEmployeeProfile? @relation(fields: [leadEmployeeProfileId], references: [id], onDelete: Restrict) audits TeamFunctionConfigAudit[] @@map("team_function_configs") } model TeamFunctionConfigAudit { id String @id @default(uuid()) configId String @map("config_id") actorUserId String @map("actor_user_id") before Json? after Json createdAt DateTime @default(now()) @map("created_at") config TeamFunctionConfig @relation(fields: [configId], references: [id], onDelete: Restrict) @@index([configId, createdAt]) @@map("team_function_config_audits") } /// 素材库账号与权威人员档案的正式绑定关系。 /// 取代此前"运行时按钉钉ID/邮箱/sourcePayload 逐层软匹配"的隐式关联做法。 /// 活跃绑定的唯一性由部分唯一索引保证(仅约束 status='ACTIVE',见对应 migration), /// 保证一个账号只对应一名权威人员、一名权威人员只对应一个活跃账号。 /// 冲突一律进人工处理,禁止按姓名自动合并。 model UserEmployeeBinding { id String @id @default(uuid()) userId String @map("user_id") employeeProfileId String @map("employee_profile_id") bindingSource String @map("binding_source") status String @default("ACTIVE") verifiedAt DateTime? @map("verified_at") note String? createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) employeeProfile PerformanceSurveyEmployeeProfile @relation(fields: [employeeProfileId], references: [id], onDelete: Cascade) @@index([userId], map: "user_employee_binding_user_idx") @@index([employeeProfileId], map: "user_employee_binding_profile_idx") @@index([status], map: "user_employee_binding_status_idx") @@map("user_employee_bindings") } model PerformanceSurveyEmployeeDepartmentMembership { id String @id @default(uuid()) employeeProfileId String @map("employee_profile_id") sourceDeptId String @map("source_dept_id") departmentName String? @map("department_name") departmentPath String? @map("department_path") isPrimary Boolean @default(false) @map("is_primary") syncedAt DateTime? @map("synced_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") employeeProfile PerformanceSurveyEmployeeProfile @relation(fields: [employeeProfileId], references: [id], onDelete: Cascade) @@unique([employeeProfileId, sourceDeptId], map: "ps_employee_dept_profile_dept_key") @@index([sourceDeptId], map: "ps_employee_dept_source_dept_idx") @@index([departmentName], map: "ps_employee_dept_name_idx") @@index([departmentPath], map: "ps_employee_dept_path_idx") @@index([employeeProfileId, isPrimary], map: "ps_employee_dept_profile_primary_idx") @@map("performance_survey_employee_department_memberships") } /// 员工组织与在职状态的时间快照。 /// /// 人员主档会被每日同步更新,不能据此还原历史周的编制。该表按小时保存 /// 完整人员状态,团队产能按周末前最近一份快照计算历史人数;快照不依赖 /// 当前主档是否仍存在,保留离职与调岗前的事实记录。 model PerformanceSurveyEmployeeRosterSnapshot { id String @id @default(uuid()) capturedAt DateTime @map("captured_at") profileCount Int @map("profile_count") employeeProfileId String @map("employee_profile_id") dingtalkUserId String? @map("dingtalk_user_id") name String departmentName String? @map("department_name") departmentPath String? @map("department_path") employmentState String? @map("employment_state") isBrandCenter Boolean @default(false) @map("is_brand_center") createdAt DateTime @default(now()) @map("created_at") @@unique([capturedAt, employeeProfileId], map: "ps_employee_roster_snapshot_capture_profile_key") @@index([capturedAt], map: "ps_employee_roster_snapshot_captured_at_idx") @@index([departmentName, capturedAt], map: "ps_employee_roster_snapshot_department_time_idx") @@index([dingtalkUserId, capturedAt], map: "ps_employee_roster_snapshot_dingtalk_time_idx") @@map("performance_survey_employee_roster_snapshots") } model PerformanceSurveyDepartmentProfile { id String @id @default(uuid()) name String @unique parentName String? @map("parent_name") sourceDeptId String? @map("source_dept_id") sortOrder Int @default(0) @map("sort_order") employeeCount Int @default(0) @map("employee_count") syncedAt DateTime? @map("synced_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@index([parentName]) @@index([sourceDeptId]) @@map("performance_survey_department_profiles") } model PerformanceSurveyTemplate { id String @id @default(uuid()) code String @unique name String description String? surveyType String @default("person_period") @map("survey_type") weight Float @default(1) questions Json scoreBands Json @map("score_bands") isActive Boolean @default(true) @map("is_active") archivedAt DateTime? @map("archived_at") archivedByUserId String? @map("archived_by_user_id") archivedByName String? @map("archived_by_name") archiveReason String? @map("archive_reason") @db.VarChar(500) createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") versions PerformanceSurveyTemplateVersion[] @@index([surveyType]) @@index([isActive]) @@index([archivedAt]) @@map("performance_survey_templates") } model PerformanceSurveyTemplateVersion { id String @id @default(uuid()) templateId String @map("template_id") version Int status String @default("draft") effectiveMonth String? @map("effective_month") definition Json publishedAt DateTime? @map("published_at") createdByUserId String? @map("created_by_user_id") createdByName String? @map("created_by_name") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") template PerformanceSurveyTemplate @relation(fields: [templateId], references: [id], onDelete: Cascade) positionPlanItems PerformanceSurveyPositionPlanItem[] surveys PerformanceSurvey[] @@unique([templateId, version], map: "performance_survey_template_version_unique") @@index([status, effectiveMonth], map: "performance_survey_template_version_status_month_idx") @@map("performance_survey_template_versions") } model PerformanceSurveyPositionGroup { id String @id @default(uuid()) code String @unique name String sortOrder Int @default(0) @map("sort_order") isActive Boolean @default(true) @map("is_active") isSystem Boolean @default(false) @map("is_system") archivedAt DateTime? @map("archived_at") archivedByUserId String? @map("archived_by_user_id") archivedByName String? @map("archived_by_name") archiveReason String? @map("archive_reason") @db.VarChar(500) createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") rules PerformanceSurveyPositionRule[] plans PerformanceSurveyPositionPlan[] surveys PerformanceSurvey[] manualAssignments PerformanceSurveyEmployeePositionAssignment[] @@index([isActive, sortOrder], map: "performance_survey_position_group_active_sort_idx") @@index([archivedAt, sortOrder], map: "performance_survey_position_group_archived_sort_idx") @@map("performance_survey_position_groups") } model PerformanceSurveyPositionRule { id String @id @default(uuid()) positionGroupId String @map("position_group_id") departmentPathPrefix String? @map("department_path_prefix") positionTitle String? @map("position_title") sortOrder Int @default(0) @map("sort_order") isActive Boolean @default(true) @map("is_active") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") positionGroup PerformanceSurveyPositionGroup @relation(fields: [positionGroupId], references: [id], onDelete: Cascade) @@index([positionGroupId, isActive, sortOrder], map: "performance_survey_position_rule_group_idx") @@index([positionTitle], map: "performance_survey_position_rule_title_idx") @@map("performance_survey_position_rules") } model PerformanceSurveyPositionPlan { id String @id @default(uuid()) positionGroupId String @map("position_group_id") version Int status String @default("draft") effectiveMonth String? @map("effective_month") matchRules Json @default("[]") @map("match_rules") groupConfig Json? @map("group_config") publishedAt DateTime? @map("published_at") createdByUserId String? @map("created_by_user_id") createdByName String? @map("created_by_name") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") positionGroup PerformanceSurveyPositionGroup @relation(fields: [positionGroupId], references: [id], onDelete: Cascade) items PerformanceSurveyPositionPlanItem[] @@unique([positionGroupId, version], map: "performance_survey_position_plan_version_unique") @@index([positionGroupId, status, effectiveMonth], map: "performance_survey_position_plan_group_status_month_idx") @@map("performance_survey_position_plans") } model PerformanceSurveyEmployeePositionAssignment { id String @id @default(uuid()) employeeProfileId String @map("employee_profile_id") assignmentType String @map("assignment_type") positionGroupId String? @map("position_group_id") startMonth String @map("start_month") endMonth String? @map("end_month") reason String @db.VarChar(500) status String @default("active") createdByUserId String @map("created_by_user_id") createdByName String @map("created_by_name") archivedAt DateTime? @map("archived_at") archivedByUserId String? @map("archived_by_user_id") archivedByName String? @map("archived_by_name") createdAt DateTime @default(now()) @map("created_at") employeeProfile PerformanceSurveyEmployeeProfile @relation(fields: [employeeProfileId], references: [id], onDelete: Restrict) positionGroup PerformanceSurveyPositionGroup? @relation(fields: [positionGroupId], references: [id], onDelete: Restrict) @@index([employeeProfileId, status, startMonth, endMonth], map: "performance_survey_manual_assignment_employee_range_idx") @@index([positionGroupId], map: "performance_survey_manual_assignment_group_idx") @@index([status, startMonth, endMonth], map: "performance_survey_manual_assignment_status_range_idx") @@map("performance_survey_employee_position_assignments") } model PerformanceSurveyPositionPlanItem { id String @id @default(uuid()) positionPlanId String @map("position_plan_id") templateVersionId String @map("template_version_id") sortOrder Int @default(0) @map("sort_order") isEnabled Boolean @default(true) @map("is_enabled") createdAt DateTime @default(now()) @map("created_at") positionPlan PerformanceSurveyPositionPlan @relation(fields: [positionPlanId], references: [id], onDelete: Cascade) templateVersion PerformanceSurveyTemplateVersion @relation(fields: [templateVersionId], references: [id], onDelete: Restrict) @@unique([positionPlanId, templateVersionId], map: "performance_survey_position_plan_item_unique") @@index([positionPlanId, isEnabled, sortOrder], map: "performance_survey_position_plan_item_sort_idx") @@map("performance_survey_position_plan_items") } model PerformanceSurvey { id String @id @default(uuid()) title String description String? surveyKind String @default("person_period") @map("survey_kind") templateCode String @map("template_code") templateSnapshot Json @map("template_snapshot") templateVersionId String? @map("template_version_id") positionGroupId String? @map("position_group_id") positionGroupCode String? @map("position_group_code") positionGroupSnapshot Json? @map("position_group_snapshot") status String @default("draft") creatorUserId String @map("creator_user_id") creatorName String @map("creator_name") creatorEmployeeId String? @map("creator_employee_id") departmentName String? @map("department_name") assessmentMonth String? @map("assessment_month") dueAt DateTime? @map("due_at") openedAt DateTime? @map("opened_at") closedAt DateTime? @map("closed_at") cancelledAt DateTime? @map("cancelled_at") cancelledByUserId String? @map("cancelled_by_user_id") cancelledByName String? @map("cancelled_by_name") cancelReason String? @db.VarChar(500) @map("cancel_reason") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") targets PerformanceSurveyTarget[] assignments PerformanceSurveyAssignment[] responses PerformanceSurveyResponse[] workItems PerformanceSurveyWorkItem[] workItemScores PerformanceSurveyWorkItemScore[] notifications PerformanceSurveyNotification[] templateVersion PerformanceSurveyTemplateVersion? @relation(fields: [templateVersionId], references: [id], onDelete: SetNull) positionGroup PerformanceSurveyPositionGroup? @relation(fields: [positionGroupId], references: [id], onDelete: SetNull) @@index([status]) @@index([surveyKind]) @@index([templateCode]) @@index([templateVersionId]) @@index([positionGroupId, assessmentMonth], map: "performance_surveys_position_group_month_idx") @@index([positionGroupCode, assessmentMonth], map: "performance_surveys_position_group_code_month_idx") @@index([templateCode, assessmentMonth], map: "performance_surveys_template_month_idx") @@index([creatorUserId]) @@index([departmentName]) @@index([assessmentMonth]) @@index([dueAt]) @@map("performance_surveys") } model PerformanceSurveyTarget { id String @id @default(uuid()) surveyId String @map("survey_id") targetType String @default("employee") @map("target_type") employeeProfileId String? @map("employee_profile_id") targetName String @map("target_name") targetSnapshot Json @map("target_snapshot") sortOrder Int @default(0) @map("sort_order") createdAt DateTime @default(now()) @map("created_at") survey PerformanceSurvey @relation(fields: [surveyId], references: [id], onDelete: Cascade) employeeProfile PerformanceSurveyEmployeeProfile? @relation("PerformanceSurveyTargetEmployee", fields: [employeeProfileId], references: [id]) assignments PerformanceSurveyAssignment[] responses PerformanceSurveyResponse[] workItems PerformanceSurveyWorkItem[] workItemScores PerformanceSurveyWorkItemScore[] notifications PerformanceSurveyNotification[] @@index([surveyId]) @@index([targetType]) @@index([employeeProfileId]) @@index([employeeProfileId, surveyId], map: "performance_survey_targets_employee_survey_idx") @@map("performance_survey_targets") } model PerformanceSurveyAssignment { id String @id @default(uuid()) surveyId String @map("survey_id") targetId String @map("target_id") evaluatorEmployeeId String? @map("evaluator_employee_id") evaluatorUserId String? @map("evaluator_user_id") evaluatorDingtalkUserId String? @map("evaluator_dingtalk_user_id") evaluatorKey String @map("evaluator_key") evaluatorName String @map("evaluator_name") evaluatorSnapshot Json @map("evaluator_snapshot") workflowStageKey String @default("legacy") @map("workflow_stage_key") workflowStageOrder Int @default(0) @map("workflow_stage_order") reviewerSource String @default("manual_selection") @map("reviewer_source") releasedAt DateTime? @map("released_at") status String @default("pending") submittedAt DateTime? @map("submitted_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") survey PerformanceSurvey @relation(fields: [surveyId], references: [id], onDelete: Cascade) target PerformanceSurveyTarget @relation(fields: [targetId], references: [id], onDelete: Cascade) evaluatorEmployee PerformanceSurveyEmployeeProfile? @relation("PerformanceSurveyEvaluator", fields: [evaluatorEmployeeId], references: [id]) response PerformanceSurveyResponse? workItemScores PerformanceSurveyWorkItemScore[] notifications PerformanceSurveyNotification[] @@unique([targetId, evaluatorKey, workflowStageKey], map: "performance_survey_assignment_target_evaluator_stage_key") @@index([surveyId]) @@index([targetId]) @@index([evaluatorEmployeeId]) @@index([evaluatorUserId]) @@index([evaluatorDingtalkUserId]) @@index([evaluatorKey]) @@index([surveyId, targetId, workflowStageOrder], map: "performance_survey_assignment_stage_idx") @@index([status]) @@map("performance_survey_assignments") } model PerformanceSurveyResponse { id String @id @default(uuid()) surveyId String @map("survey_id") targetId String @map("target_id") assignmentId String @unique @map("assignment_id") respondentUserId String @map("respondent_user_id") respondentEmployeeId String? @map("respondent_employee_id") respondentName String @map("respondent_name") respondentSnapshot Json @map("respondent_snapshot") scores Json comment String? totalScore Float @map("total_score") achievementScore Float @map("achievement_score") kpiScore Float @map("kpi_score") weightedScore Float @map("weighted_score") createdAt DateTime @default(now()) @map("created_at") survey PerformanceSurvey @relation(fields: [surveyId], references: [id], onDelete: Cascade) target PerformanceSurveyTarget @relation(fields: [targetId], references: [id], onDelete: Cascade) assignment PerformanceSurveyAssignment @relation(fields: [assignmentId], references: [id], onDelete: Cascade) respondentEmployee PerformanceSurveyEmployeeProfile? @relation("PerformanceSurveyRespondentEmployee", fields: [respondentEmployeeId], references: [id]) @@index([surveyId]) @@index([targetId]) @@index([respondentUserId]) @@index([respondentEmployeeId]) @@index([createdAt]) @@map("performance_survey_responses") } model PerformanceSurveyWorkItem { id String @id @default(uuid()) surveyId String @map("survey_id") targetId String @map("target_id") submitterUserId String? @map("submitter_user_id") submitterEmployeeId String? @map("submitter_employee_id") title String description String? imageUrls Json @map("image_urls") sortOrder Int @default(0) @map("sort_order") status String @default("submitted") submittedAt DateTime? @map("submitted_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") survey PerformanceSurvey @relation(fields: [surveyId], references: [id], onDelete: Cascade) target PerformanceSurveyTarget @relation(fields: [targetId], references: [id], onDelete: Cascade) submitterEmployee PerformanceSurveyEmployeeProfile? @relation("PerformanceSurveyWorkItemSubmitter", fields: [submitterEmployeeId], references: [id]) scores PerformanceSurveyWorkItemScore[] @@index([surveyId]) @@index([targetId]) @@index([submitterUserId]) @@index([submitterEmployeeId]) @@index([status]) @@map("performance_survey_work_items") } model PerformanceSurveyWorkItemScore { id String @id @default(uuid()) workItemId String @map("work_item_id") surveyId String @map("survey_id") targetId String @map("target_id") assignmentId String @map("assignment_id") evaluatorUserId String @map("evaluator_user_id") evaluatorEmployeeId String? @map("evaluator_employee_id") evaluatorName String @map("evaluator_name") scores Json comment String? totalScore Float @map("total_score") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") workItem PerformanceSurveyWorkItem @relation(fields: [workItemId], references: [id], onDelete: Cascade) survey PerformanceSurvey @relation(fields: [surveyId], references: [id], onDelete: Cascade) target PerformanceSurveyTarget @relation(fields: [targetId], references: [id], onDelete: Cascade) assignment PerformanceSurveyAssignment @relation(fields: [assignmentId], references: [id], onDelete: Cascade) evaluatorEmployee PerformanceSurveyEmployeeProfile? @relation("PerformanceSurveyWorkItemScoreEvaluator", fields: [evaluatorEmployeeId], references: [id]) @@unique([workItemId, assignmentId], map: "performance_survey_work_item_score_unique") @@index([surveyId]) @@index([targetId]) @@index([assignmentId]) @@index([evaluatorUserId]) @@index([evaluatorEmployeeId]) @@map("performance_survey_work_item_scores") } model PerformanceSurveyNotification { id String @id @default(uuid()) surveyId String? @map("survey_id") targetId String? @map("target_id") assignmentId String? @map("assignment_id") recipientEmployeeId String? @map("recipient_employee_id") recipientUserId String? @map("recipient_user_id") dingtalkUserId String? @map("dingtalk_user_id") recipientName String @map("recipient_name") eventType String @map("event_type") businessKey String @map("business_key") title String content String actionUrl String? @map("action_url") status String @default("pending") attemptCount Int @default(0) @map("attempt_count") maxAttempts Int @default(5) @map("max_attempts") nextRetryAt DateTime? @map("next_retry_at") lastStatusCode Int? @map("last_status_code") lastResponseBody String? @map("last_response_body") lastError String? @map("last_error") providerType String @default("work_notice") @map("provider_type") providerMessageId String? @map("provider_message_id") readStatus String @default("unknown") @map("read_status") readAt DateTime? @map("read_at") readStatusCheckedAt DateTime? @map("read_status_checked_at") sentAt DateTime? @map("sent_at") completedAt DateTime? @map("completed_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") survey PerformanceSurvey? @relation(fields: [surveyId], references: [id], onDelete: Cascade) target PerformanceSurveyTarget? @relation(fields: [targetId], references: [id], onDelete: Cascade) assignment PerformanceSurveyAssignment? @relation(fields: [assignmentId], references: [id], onDelete: Cascade) recipientEmployee PerformanceSurveyEmployeeProfile? @relation(fields: [recipientEmployeeId], references: [id]) @@unique([eventType, businessKey], map: "performance_survey_notification_event_key") @@index([surveyId]) @@index([targetId]) @@index([assignmentId]) @@index([recipientEmployeeId]) @@index([recipientUserId]) @@index([dingtalkUserId]) @@index([status, nextRetryAt]) @@index([readStatus, readStatusCheckedAt], map: "performance_survey_notifications_read_status_checked_at_idx") @@index([createdAt]) @@map("performance_survey_notifications") } model PerformanceSurveyConfigAuditLog { id String @id @default(uuid()) action String targetType String @map("target_type") targetId String? @map("target_id") actorUserId String @map("actor_user_id") actorName String @map("actor_name") reason String? @db.VarChar(500) beforeSnapshot Json? @map("before_snapshot") afterSnapshot Json? @map("after_snapshot") metadata Json? createdAt DateTime @default(now()) @map("created_at") @@index([targetType, targetId, createdAt], map: "performance_survey_config_audit_target_idx") @@index([actorUserId, createdAt], map: "performance_survey_config_audit_actor_idx") @@index([createdAt], map: "performance_survey_config_audit_created_idx") @@map("performance_survey_config_audit_logs") } model WorkReviewProject { id String @id @default(uuid()) title String brand String? description String? workDate DateTime @db.Date @map("work_date") submitterUserId String @map("submitter_user_id") submitterEmployeeId String @map("submitter_employee_id") submitterName String @map("submitter_name") submitterSnapshot Json @map("submitter_snapshot") managerEmployeeId String @map("manager_employee_id") managerUserId String? @map("manager_user_id") managerDingtalkUserId String? @map("manager_dingtalk_user_id") managerName String @map("manager_name") finalReviewerEmployeeId String @map("final_reviewer_employee_id") finalReviewerUserId String? @map("final_reviewer_user_id") finalReviewerDingtalkUserId String? @map("final_reviewer_dingtalk_user_id") finalReviewerName String @map("final_reviewer_name") status String @default("pending_review") latestVersion Int @default(1) @map("latest_version") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") submitterEmployee PerformanceSurveyEmployeeProfile @relation("WorkReviewSubmitter", fields: [submitterEmployeeId], references: [id], onDelete: Restrict) managerEmployee PerformanceSurveyEmployeeProfile @relation("WorkReviewManager", fields: [managerEmployeeId], references: [id], onDelete: Restrict) finalReviewerEmployee PerformanceSurveyEmployeeProfile @relation("WorkReviewFinalReviewer", fields: [finalReviewerEmployeeId], references: [id], onDelete: Restrict) versions WorkReviewVersion[] notifications WorkReviewNotification[] @@index([submitterUserId, workDate], map: "work_review_projects_submitter_date_idx") @@index([submitterEmployeeId, workDate], map: "work_review_projects_submitter_employee_date_idx") @@index([managerEmployeeId, status], map: "work_review_projects_manager_status_idx") @@index([finalReviewerEmployeeId, status], map: "work_review_projects_final_reviewer_status_idx") @@index([status, workDate], map: "work_review_projects_status_date_idx") @@index([updatedAt], map: "work_review_projects_updated_at_idx") @@map("work_review_projects") } model WorkReviewVersion { id String @id @default(uuid()) projectId String @map("project_id") version Int submissionNote String? @map("submission_note") imageUrls Json @map("image_urls") status String @default("pending_review") submittedAt DateTime @default(now()) @map("submitted_at") completedAt DateTime? @map("completed_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") project WorkReviewProject @relation(fields: [projectId], references: [id], onDelete: Cascade) reviews WorkReviewReview[] notifications WorkReviewNotification[] @@unique([projectId, version], map: "work_review_versions_project_version_key") @@index([projectId, submittedAt], map: "work_review_versions_project_submitted_idx") @@index([status, submittedAt], map: "work_review_versions_status_submitted_idx") @@map("work_review_versions") } model WorkReviewReview { id String @id @default(uuid()) versionId String @map("version_id") reviewerRole String @map("reviewer_role") reviewerEmployeeId String @map("reviewer_employee_id") reviewerUserId String? @map("reviewer_user_id") reviewerName String @map("reviewer_name") reviewerSnapshot Json @map("reviewer_snapshot") decision String imageComments Json @map("image_comments") generalComment String? @map("general_comment") reviewedAt DateTime @default(now()) @map("reviewed_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") version WorkReviewVersion @relation(fields: [versionId], references: [id], onDelete: Cascade) reviewerEmployee PerformanceSurveyEmployeeProfile @relation("WorkReviewReviewer", fields: [reviewerEmployeeId], references: [id], onDelete: Restrict) @@unique([versionId, reviewerRole], map: "work_review_reviews_version_role_key") @@index([reviewerEmployeeId, reviewedAt], map: "work_review_reviews_reviewer_reviewed_idx") @@index([decision], map: "work_review_reviews_decision_idx") @@map("work_review_reviews") } model WorkReviewNotification { id String @id @default(uuid()) projectId String @map("project_id") versionId String @map("version_id") recipientEmployeeId String? @map("recipient_employee_id") recipientUserId String? @map("recipient_user_id") dingtalkUserId String? @map("dingtalk_user_id") recipientName String @map("recipient_name") eventType String @map("event_type") businessKey String @map("business_key") title String content String actionUrl String? @map("action_url") status String @default("pending") attemptCount Int @default(0) @map("attempt_count") maxAttempts Int @default(5) @map("max_attempts") nextRetryAt DateTime? @map("next_retry_at") providerType String @default("work_notice") @map("provider_type") providerMessageId String? @map("provider_message_id") lastError String? @map("last_error") sentAt DateTime? @map("sent_at") completedAt DateTime? @map("completed_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") project WorkReviewProject @relation(fields: [projectId], references: [id], onDelete: Cascade) version WorkReviewVersion @relation(fields: [versionId], references: [id], onDelete: Cascade) @@unique([eventType, businessKey], map: "work_review_notifications_event_key") @@index([projectId, createdAt], map: "work_review_notifications_project_created_idx") @@index([versionId], map: "work_review_notifications_version_idx") @@index([recipientEmployeeId], map: "work_review_notifications_recipient_employee_idx") @@index([recipientUserId], map: "work_review_notifications_recipient_user_idx") @@index([dingtalkUserId], map: "work_review_notifications_dingtalk_user_idx") @@index([status, nextRetryAt], map: "work_review_notifications_status_retry_idx") @@map("work_review_notifications") } /// 任务详情图片变更申请。正式素材只在直属主管批准后才应用。 model TaskDetailChangeRequest { id String @id @default(uuid()) materialId String @map("material_id") requesterUserId String @map("requester_user_id") requesterEmployeeId String @map("requester_employee_id") requesterName String @map("requester_name") requesterSnapshot Json @map("requester_snapshot") requesterPositionLevel String? @map("requester_position_level") requesterLevelType String? @map("requester_level_type") requesterIsLeadership Boolean @default(false) @map("requester_is_leadership") managerEmployeeId String @map("manager_employee_id") managerUserId String? @map("manager_user_id") managerDingtalkUserId String @map("manager_dingtalk_user_id") managerName String @map("manager_name") managerSnapshot Json @map("manager_snapshot") status String @default("pending_manager_approval") operations Json baseMaterialUpdatedAt DateTime @map("base_material_updated_at") submittedAt DateTime @default(now()) @map("submitted_at") expiresAt DateTime @map("expires_at") decidedAt DateTime? @map("decided_at") decidedByUserId String? @map("decided_by_user_id") decisionComment String? @map("decision_comment") appliedAt DateTime? @map("applied_at") applyError String? @map("apply_error") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") material Material @relation(fields: [materialId], references: [id], onDelete: Cascade) requesterUser User @relation("TaskDetailChangeRequesterUser", fields: [requesterUserId], references: [id], onDelete: Restrict) managerUser User? @relation("TaskDetailChangeManagerUser", fields: [managerUserId], references: [id], onDelete: SetNull) requesterEmployee PerformanceSurveyEmployeeProfile @relation("TaskDetailChangeRequesterEmployee", fields: [requesterEmployeeId], references: [id], onDelete: Restrict) managerEmployee PerformanceSurveyEmployeeProfile @relation("TaskDetailChangeManagerEmployee", fields: [managerEmployeeId], references: [id], onDelete: Restrict) notifications TaskDetailChangeNotification[] @@index([materialId, createdAt], map: "task_detail_change_requests_material_created_idx") @@index([requesterUserId, createdAt], map: "task_detail_change_requests_requester_created_idx") @@index([managerDingtalkUserId, status], map: "task_detail_change_requests_manager_status_idx") @@index([status, expiresAt], map: "task_detail_change_requests_status_expires_idx") @@index([status, updatedAt], map: "task_detail_change_requests_status_updated_idx") @@map("task_detail_change_requests") } /// 任务详情变更审批通知 outbox;投递失败可恢复,不影响审批主状态。 model TaskDetailChangeNotification { id String @id @default(uuid()) requestId String @map("request_id") recipientUserId String? @map("recipient_user_id") dingtalkUserId String @map("dingtalk_user_id") recipientName String @map("recipient_name") eventType String @map("event_type") businessKey String @map("business_key") title String content String actionUrl String @map("action_url") status String @default("pending") attemptCount Int @default(0) @map("attempt_count") maxAttempts Int @default(5) @map("max_attempts") nextRetryAt DateTime? @map("next_retry_at") providerMessageId String? @map("provider_message_id") lastError String? @map("last_error") sentAt DateTime? @map("sent_at") completedAt DateTime? @map("completed_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") request TaskDetailChangeRequest @relation(fields: [requestId], references: [id], onDelete: Cascade) @@unique([eventType, businessKey], map: "task_detail_change_notifications_event_key") @@index([requestId, createdAt], map: "task_detail_change_notifications_request_created_idx") @@index([status, nextRetryAt], map: "task_detail_change_notifications_status_retry_idx") @@map("task_detail_change_notifications") } /// 钉钉审批流程的本地只读快照。 /// /// 上游 rpt.ding_Workflow / ding_WorkflowTask / ding_WorkflowScheme 均为无索引大表, /// 部门看板不得在用户请求链路中直连扫描。后台任务首次建立全量基线,之后每小时 /// 增量同步主流程,并每 24 小时全量校准流程与商品 ID 表单字段。 model DingWorkflowSnapshot { processInstanceId String @id @map("process_instance_id") businessId String? @map("business_id") title String? result String? originatorUserId String? @map("originator_user_id") originatorDeptId String? @map("originator_dept_id") originatorDeptName String? @map("originator_dept_name") status String bizAction String? @map("biz_action") processCode String? @map("process_code") flowType String? @map("flow_type") joinedAtRaw String? @map("joined_at_raw") sourceCreatedAtRaw String? @map("source_created_at_raw") sourceUpdatedAtRaw String? @map("source_updated_at_raw") sourceFinishedAtRaw String? @map("source_finished_at_raw") isOngoing Boolean @default(false) @map("is_ongoing") schemeFieldCount Int @default(0) @map("scheme_field_count") taskCount Int @default(0) @map("task_count") activeTaskCount Int @default(0) @map("active_task_count") latestTaskStatus String? @map("latest_task_status") latestTaskAtRaw String? @map("latest_task_at_raw") brand String? demandType String? @map("demand_type") requesterName String? @map("requester_name") requesterDepartment String? @map("requester_department") projectType String? @map("project_type") formFields Json? @map("form_fields") activeTasks Json? @map("active_tasks") syncedAt DateTime @map("synced_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@index([isOngoing, sourceUpdatedAtRaw], map: "ding_workflow_snapshots_ongoing_updated_idx") @@index([status], map: "ding_workflow_snapshots_status_idx") @@index([originatorDeptName], map: "ding_workflow_snapshots_department_idx") @@index([brand], map: "ding_workflow_snapshots_brand_idx") @@index([demandType], map: "ding_workflow_snapshots_demand_type_idx") @@index([requesterDepartment], map: "ding_workflow_snapshots_requester_department_idx") @@map("ding_workflow_snapshots") } /// 钉钉审批流程同步游标、租约与可观测状态。 model DingWorkflowSyncState { id String @id status String @default("idle") sourceCursorRaw String? @map("source_cursor_raw") lastStartedAt DateTime? @map("last_started_at") lastCompletedAt DateTime? @map("last_completed_at") lastError String? @map("last_error") leaseOwner String? @map("lease_owner") leaseExpiresAt DateTime? @map("lease_expires_at") rowsUpserted Int @default(0) @map("rows_upserted") currentOngoingCount Int @default(0) @map("current_ongoing_count") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@index([leaseExpiresAt], map: "ding_workflow_sync_states_lease_idx") @@map("ding_workflow_sync_states") } /// 业绩转化商品经营数据的本地只读快照。 /// /// 上游经营报表按商品和统计日期保存多行来源记录。快照保留来源行, /// 详情页再按日期聚合,避免把来源表的修订值固化成不可追溯的汇总值。 model PerformanceConversionMetricSnapshot { id String @id @default(uuid()) sourceKey String @unique @map("source_key") sourceRowId String? @map("source_row_id") productId String @map("product_id") metricDate DateTime @db.Date @map("metric_date") productName String? @map("product_name") sourceBrand String? @map("source_brand") category1 String? category2 String? category3 String? paymentAmount String? @map("payment_amount") paymentUsers String? @map("payment_users") cartUsers String? @map("cart_users") visitors String? conversionRate String? @map("conversion_rate") favorites String? impressions String? clicks String? clickRate String? @map("click_rate") promotionSpend String? @map("promotion_spend") promotionRevenue String? @map("promotion_revenue") roi String? syncedAt DateTime @map("synced_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@index([productId, metricDate], map: "performance_conversion_metric_product_date_idx") @@index([metricDate], map: "performance_conversion_metric_date_idx") @@map("performance_conversion_metric_snapshots") } /// 业绩转化经营数据每日增量同步游标、租约与可观测状态。 model PerformanceConversionSyncState { id String @id status String @default("idle") sourceCursorRaw String? @map("source_cursor_raw") lastStartedAt DateTime? @map("last_started_at") lastCompletedAt DateTime? @map("last_completed_at") lastError String? @map("last_error") leaseOwner String? @map("lease_owner") leaseExpiresAt DateTime? @map("lease_expires_at") rowsUpserted Int @default(0) @map("rows_upserted") productsSynced Int @default(0) @map("products_synced") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@index([leaseExpiresAt], map: "performance_conversion_sync_states_lease_idx") @@map("performance_conversion_sync_states") } model VideoTranscodeProgress { id String @id @default(uuid()) materialId String @map("material_id") stage String progress Int @default(0) status String @default("waiting") @map("status") priority Int @default(0) startedAt DateTime @default(now()) @map("started_at") completedAt DateTime? @map("completed_at") errorMessage String? @map("error_message") retryCount Int @default(0) @map("retry_count") lastHeartbeat DateTime? @map("last_heartbeat") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") material Material @relation("MaterialTranscodeProgress", fields: [materialId], references: [id], onDelete: Cascade) @@unique([materialId, stage]) @@index([status]) @@index([priority, status]) @@index([materialId]) @@index([lastHeartbeat]) @@map("video_transcode_progresses") } enum Role { USER ADMIN DEPT_ADMIN SYSTEM_ADMIN }