generator client { provider = "prisma-client-js" binaryTargets = ["native", "linux-musl-openssl-3.0.x"] } datasource db { provider = "mysql" url = env("DATABASE_URL") } model User { id String @id @default(cuid()) email String? @db.VarChar(255) nickname String? @db.VarChar(100) avatarUrl String? @db.VarChar(500) role String @default("USER") @db.VarChar(32) status String @default("active") @db.VarChar(32) onboardingCompleted Boolean @default(false) lastLoginAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt deletedAt DateTime? authAccounts AuthAccount[] refreshTokens RefreshToken[] profile UserProfile? preferences UserPreference? consents UserConsent[] knowledgeBases KnowledgeBase[] knowledgeItems KnowledgeItem[] knowledgeItemRelations KnowledgeItemRelation[] tags Tag[] uploadedFiles UploadedFile[] documentImports DocumentImport[] learningSessions LearningSession[] learningRecords LearningRecord[] activeRecallQuestions ActiveRecallQuestion[] activeRecallAnswers ActiveRecallAnswer[] aiAnalysisJobs AiAnalysisJob[] aiAnalysisResults AiAnalysisResult[] focusItems FocusItem[] reviewCards ReviewCard[] reviewLogs ReviewLog[] reviewPlans ReviewPlan[] dailyLearningActivities DailyLearningActivity[] notifications Notification[] feedbacks Feedback[] aiUsageLogs AiUsageLog[] knowledgeSources KnowledgeSource[] knowledgeChunks KnowledgeChunk[] importCandidates ImportCandidate[] @@index([email]) @@index([status]) } model AuthAccount { id String @id @default(cuid()) userId String provider String @db.VarChar(32) providerUserId String @db.VarChar(255) email String? @db.VarChar(255) rawProfileJson Json? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id]) @@unique([provider, providerUserId]) @@index([userId]) } model RefreshToken { id String @id @default(cuid()) userId String tokenHash String @db.VarChar(255) deviceId String? @db.VarChar(255) deviceName String? @db.VarChar(255) expiresAt DateTime revokedAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id]) @@index([userId]) @@index([tokenHash]) } model UserProfile { id String @id @default(cuid()) userId String @unique learningIdentity String? @db.VarChar(100) learningDirection String? @db.VarChar(255) bio String? @db.Text currentGoal String? @db.VarChar(255) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id]) } model UserPreference { id String @id @default(cuid()) userId String @unique preferredMethods Json? defaultFocusMinutes Int @default(25) aiSuggestionLevel String @default("normal") @db.VarChar(32) language String @default("zh-CN") @db.VarChar(32) appearance String @default("system") @db.VarChar(32) notificationEnabled Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id]) } model UserConsent { id String @id @default(cuid()) userId String consentType String @db.VarChar(32) version String @db.VarChar(50) acceptedAt DateTime ipAddress String? @db.VarChar(100) userAgent String? @db.VarChar(500) createdAt DateTime @default(now()) user User @relation(fields: [userId], references: [id]) @@index([userId]) @@index([consentType]) } model KnowledgeBase { id String @id @default(cuid()) userId String title String @db.VarChar(255) description String? @db.Text coverKey String? @db.VarChar(100) status String @default("active") @db.VarChar(32) itemCount Int @default(0) lastStudiedAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt deletedAt DateTime? user User @relation(fields: [userId], references: [id]) items KnowledgeItem[] sources KnowledgeSource[] candidates ImportCandidate[] chunks KnowledgeChunk[] focusItems FocusItem[] @@index([userId]) @@index([status]) } model KnowledgeItem { id String @id @default(cuid()) userId String knowledgeBaseId String parentId String? itemType String @db.VarChar(32) title String @db.VarChar(255) content String? @db.LongText summary String? @db.Text sourceType String? @db.VarChar(32) sourceRef String? @db.VarChar(500) sourceDeleted Boolean @default(false) sourceTitleSnapshot String? @db.VarChar(255) sourceSnippetSnapshot String? @db.Text orderIndex Int @default(0) status String @default("active") @db.VarChar(32) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt deletedAt DateTime? user User @relation(fields: [userId], references: [id]) knowledgeBase KnowledgeBase @relation(fields: [knowledgeBaseId], references: [id]) parent KnowledgeItem? @relation("KnowledgeItemRelations", fields: [parentId], references: [id]) children KnowledgeItem[] @relation("KnowledgeItemRelations") tags KnowledgeItemTag[] @@index([userId]) @@index([knowledgeBaseId]) @@index([parentId]) @@index([itemType]) } model KnowledgeItemRelation { id String @id @default(cuid()) userId String sourceItemId String targetItemId String relationType String @db.VarChar(32) confidence Decimal? @db.Decimal(5, 2) reason String? @db.Text createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id]) @@index([sourceItemId]) @@index([targetItemId]) } model Tag { id String @id @default(cuid()) userId String name String @db.VarChar(100) color String? @db.VarChar(32) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id]) items KnowledgeItemTag[] @@unique([userId, name]) } model KnowledgeItemTag { id String @id @default(cuid()) knowledgeItemId String tagId String createdAt DateTime @default(now()) knowledgeItem KnowledgeItem @relation(fields: [knowledgeItemId], references: [id]) tag Tag @relation(fields: [tagId], references: [id]) @@unique([knowledgeItemId, tagId]) } model UploadedFile { id String @id @default(cuid()) userId String filename String @db.VarChar(255) mimeType String? @db.VarChar(100) storagePath String @db.VarChar(500) objectKey String? @db.VarChar(500) bucket String? @db.VarChar(100) sizeBytes BigInt @default(0) checksum String? @db.VarChar(255) sha256 String? @db.VarChar(64) purpose String? @db.VarChar(32) createdAt DateTime @default(now()) user User @relation(fields: [userId], references: [id]) sources KnowledgeSource[] @@index([userId]) @@index([objectKey]) @@index([sha256]) } model DocumentImport { id String @id @default(cuid()) userId String knowledgeBaseId String? sourceId String? fileId String? sourceType String @db.VarChar(32) sourceName String? @db.VarChar(255) sourceUrl String? @db.VarChar(500) rawText String? @db.LongText status String @default("QUEUED") @db.VarChar(32) step String? @db.VarChar(32) progress Int @default(0) workerId String? @db.VarChar(255) retryCount Int @default(0) maxRetries Int @default(3) heartbeatAt DateTime? errorCode String? @db.VarChar(32) errorMessage String? @db.Text resultJson Json? startedAt DateTime? completedAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id]) source KnowledgeSource? @relation(fields: [sourceId], references: [id]) candidates ImportCandidate[] @@index([userId]) @@index([status]) @@index([sourceId]) @@index([workerId]) } model LearningSession { id String @id @default(cuid()) userId String knowledgeBaseId String? knowledgeItemId String? mode String @db.VarChar(32) status String @default("active") @db.VarChar(32) startedAt DateTime endedAt DateTime? durationSeconds Int @default(0) focusMinutes Int? metadata Json? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id]) @@index([userId]) @@index([knowledgeItemId]) @@index([startedAt]) } model LearningRecord { id String @id @default(cuid()) userId String sessionId String? recordType String @db.VarChar(32) title String @db.VarChar(255) description String? @db.Text durationSeconds Int @default(0) occurredAt DateTime metadata Json? createdAt DateTime @default(now()) user User @relation(fields: [userId], references: [id]) @@index([userId]) @@index([occurredAt]) } model ActiveRecallQuestion { id String @id @default(cuid()) userId String knowledgeItemId String? questionText String @db.Text difficulty String? @db.VarChar(32) createdBy String @default("ai") @db.VarChar(32) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id]) answers ActiveRecallAnswer[] @@index([userId]) @@index([knowledgeItemId]) } model ActiveRecallAnswer { id String @id @default(cuid()) userId String questionId String? sessionId String? answerType String @default("text") @db.VarChar(32) answerText String? @db.LongText audioFileId String? submittedAt DateTime createdAt DateTime @default(now()) user User @relation(fields: [userId], references: [id]) question ActiveRecallQuestion? @relation(fields: [questionId], references: [id]) @@index([userId]) @@index([questionId]) @@index([sessionId]) } model AiAnalysisJob { id String @id @default(cuid()) userId String sessionId String? answerId String? jobType String @db.VarChar(32) status String @default("pending") @db.VarChar(32) progress Int @default(0) errorMessage String? @db.Text queuedAt DateTime? startedAt DateTime? completedAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id]) results AiAnalysisResult[] @@index([userId]) @@index([status]) @@index([sessionId]) } model AiAnalysisResult { id String @id @default(cuid()) userId String jobId String sessionId String? answerId String? summary String? @db.Text masteryScore Int? strengths Json? weaknesses Json? suggestions Json? nextActions Json? rawResult Json? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id]) job AiAnalysisJob @relation(fields: [jobId], references: [id]) @@index([userId]) @@index([jobId]) @@index([sessionId]) } model FocusItem { id String @id @default(cuid()) userId String knowledgeBaseId String? knowledgeItemId String? analysisResultId String? title String @db.VarChar(255) reason String? @db.Text suggestion String? @db.Text priority String @default("normal") @db.VarChar(32) status String @default("open") @db.VarChar(32) masteryScore Int? dueAt DateTime? completedAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt deletedAt DateTime? user User @relation(fields: [userId], references: [id]) knowledgeBase KnowledgeBase? @relation(fields: [knowledgeBaseId], references: [id]) @@index([userId]) @@index([status]) @@index([dueAt]) } model ReviewCard { id String @id @default(cuid()) userId String knowledgeItemId String? focusItemId String? frontText String @db.Text backText String? @db.Text difficulty String? @db.VarChar(32) status String @default("active") @db.VarChar(32) nextReviewAt DateTime? intervalDays Int @default(1) easeFactor Decimal @default(2.50) @db.Decimal(4, 2) repetitionCount Int @default(0) lapseCount Int @default(0) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt deletedAt DateTime? user User @relation(fields: [userId], references: [id]) logs ReviewLog[] @@index([userId]) @@index([nextReviewAt]) @@index([focusItemId]) } model ReviewLog { id String @id @default(cuid()) userId String reviewCardId String sessionId String? rating String @db.VarChar(32) responseText String? @db.Text reviewedAt DateTime nextReviewAt DateTime? createdAt DateTime @default(now()) user User @relation(fields: [userId], references: [id]) reviewCard ReviewCard @relation(fields: [reviewCardId], references: [id]) @@index([userId]) @@index([reviewCardId]) @@index([reviewedAt]) } model ReviewPlan { id String @id @default(cuid()) userId String title String @db.VarChar(255) status String @default("active") @db.VarChar(32) scheduledAt DateTime? completedAt DateTime? cardCount Int @default(0) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id]) @@index([userId]) @@index([scheduledAt]) } model DailyLearningActivity { id String @id @default(cuid()) userId String activityDate DateTime @db.Date durationSeconds Int @default(0) sessionsCount Int @default(0) activeRecallCount Int @default(0) reviewCount Int @default(0) aiAnalysisCount Int @default(0) completedLoopCount Int @default(0) activityLevel Int @default(0) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id]) @@unique([userId, activityDate]) @@index([userId]) } model Notification { id String @id @default(cuid()) userId String type String @db.VarChar(32) title String @db.VarChar(255) content String? @db.Text data Json? readAt DateTime? createdAt DateTime @default(now()) user User @relation(fields: [userId], references: [id]) @@index([userId]) @@index([readAt]) @@index([type]) } model Feedback { id String @id @default(cuid()) userId String? email String? @db.VarChar(255) category String @db.VarChar(64) content String @db.Text deviceInfo Json? status String @default("open") @db.VarChar(32) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User? @relation(fields: [userId], references: [id]) @@index([userId]) @@index([status]) } model AiUsageLog { id String @id @default(cuid()) userId String feature String @db.VarChar(64) provider String @db.VarChar(32) model String @db.VarChar(100) tier String @db.VarChar(32) promptKey String @db.VarChar(128) promptVersion String @db.VarChar(32) inputTokens Int @default(0) outputTokens Int @default(0) estimatedCost Float @default(0) latencyMs Int @default(0) success Boolean @default(true) errorMessage String? @db.VarChar(500) createdAt DateTime @default(now()) user User @relation(fields: [userId], references: [id]) @@index([userId]) @@index([feature]) @@index([createdAt]) } model WaitlistEntry { id String @id @default(cuid()) nickname String @db.VarChar(100) email String @db.VarChar(255) devices Json? interests Json? painpoint String? @db.Text willingBeta Boolean @default(false) createdAt DateTime @default(now()) @@index([email]) } model AppChangelog { id String @id @default(cuid()) version String @db.VarChar(50) title String @db.VarChar(255) content String @db.Text platform String @default("ios") @db.VarChar(32) publishedAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } // ── 知识库新增模型 ── model KnowledgeSource { id String @id @default(cuid()) userId String knowledgeBaseId String fileId String? type String @default("file") @db.VarChar(32) title String? @db.VarChar(255) originalFilename String? @db.VarChar(255) mimeType String? @db.VarChar(100) sizeBytes BigInt @default(0) textLength Int @default(0) parseStatus String @default("pending") @db.VarChar(32) indexStatus String @default("pending") @db.VarChar(32) learningStatus String @default("pending") @db.VarChar(32) parsedObjectKey String? @db.VarChar(500) metadataObjectKey String? @db.VarChar(500) originalObjectKey String? @db.VarChar(500) version Int @default(1) parentSourceId String? replacedBySourceId String? errorCode String? @db.VarChar(32) errorMessage String? @db.Text createdAt DateTime @default(now()) updatedAt DateTime @updatedAt deletedAt DateTime? user User @relation(fields: [userId], references: [id]) knowledgeBase KnowledgeBase @relation(fields: [knowledgeBaseId], references: [id]) file UploadedFile? @relation(fields: [fileId], references: [id]) chunks KnowledgeChunk[] imports DocumentImport[] candidates ImportCandidate[] @@index([userId]) @@index([knowledgeBaseId]) @@index([fileId]) @@index([parseStatus]) @@index([indexStatus]) } model KnowledgeChunk { id String @id @default(cuid()) userId String knowledgeBaseId String sourceId String content String @db.LongText chunkIndex Int pageNumber Int? sectionTitle String? @db.VarChar(500) tokenCount Int @default(0) externalVectorId String? @db.VarChar(255) embeddingModel String? @db.VarChar(100) embeddingStatus String @default("pending") @db.VarChar(32) metadataJson Json? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt deletedAt DateTime? user User @relation(fields: [userId], references: [id]) knowledgeBase KnowledgeBase @relation(fields: [knowledgeBaseId], references: [id]) source KnowledgeSource @relation(fields: [sourceId], references: [id]) @@index([userId]) @@index([sourceId]) @@index([knowledgeBaseId]) @@index([externalVectorId]) } model ImportCandidate { id String @id @default(cuid()) userId String knowledgeBaseId String sourceId String importId String title String @db.VarChar(255) summary String? @db.Text content String? @db.LongText tagsJson Json? recallQuestionsJson Json? sourceTextSnippet String? @db.Text sourceChunkIds Json? confidence Decimal @default(0) @db.Decimal(4, 3) difficulty String? @db.VarChar(16) orderIndex Int @default(0) status String @default("PENDING") @db.VarChar(16) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id]) knowledgeBase KnowledgeBase @relation(fields: [knowledgeBaseId], references: [id]) source KnowledgeSource @relation(fields: [sourceId], references: [id]) import DocumentImport @relation(fields: [importId], references: [id]) @@index([userId]) @@index([sourceId]) @@index([importId]) @@index([status]) } model BackupJob { id String @id @default(cuid()) type String @db.VarChar(16) status String @default("RUNNING") @db.VarChar(16) localPath String? @db.VarChar(500) cosObjectKey String? @db.VarChar(500) fileSizeBytes BigInt @default(0) startedAt DateTime @default(now()) completedAt DateTime? errorMessage String? @db.Text createdAt DateTime @default(now()) } model AdminUser { id String @id @default(cuid()) email String @unique @db.VarChar(255) passwordHash String @db.VarChar(255) displayName String @db.VarChar(100) role String @default("ADMIN") @db.VarChar(32) status String @default("ACTIVE") @db.VarChar(32) twoFactorEnabled Boolean @default(false) twoFactorSecret String? @db.VarChar(100) lastLoginAt DateTime? lastLoginIp String? @db.VarChar(45) failedLoginCount Int @default(0) lockedUntil DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt deletedAt DateTime? sessions AdminSession[] conversations AdminConversation[] auditLogs AdminAuditLog[] @@index([email]) @@index([status]) } model AdminSession { id String @id @default(cuid()) adminUserId String refreshTokenHash String @db.VarChar(255) ip String? @db.VarChar(45) userAgent String? @db.VarChar(500) expiresAt DateTime revokedAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt adminUser AdminUser @relation(fields: [adminUserId], references: [id]) @@index([adminUserId]) @@index([refreshTokenHash]) } model AdminAuditLog { id String @id @default(cuid()) adminUserId String action String @db.VarChar(64) resourceType String? @db.VarChar(64) resourceId String? @db.VarChar(255) beforeJson Json? afterJson Json? ip String? @db.VarChar(45) userAgent String? @db.VarChar(500) riskLevel String? @db.VarChar(16) reason String? @db.VarChar(500) createdAt DateTime @default(now()) adminUser AdminUser @relation(fields: [adminUserId], references: [id]) @@index([adminUserId]) @@index([action]) @@index([createdAt]) } model MembershipPlan { id String @id @default(cuid()) code String @unique @db.VarChar(32) name String @db.VarChar(100) priceMonthly Int @default(0) priceYearly Int @default(0) maxKnowledgeBases Int @default(1) maxStorageBytes BigInt @default(0) maxFileSizeBytes BigInt @default(0) monthlyOcrPages Int @default(0) monthlyVisionPages Int @default(0) monthlyChatCount Int @default(0) monthlyAiAnalysisCount Int @default(0) monthlyRecallCount Int @default(0) monthlyCardGenCount Int @default(0) isActive Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model AdminConversation { id String @id @default(cuid()) adminUserId String title String @default("新对话") @db.VarChar(200) hermesSessionId String @unique @db.VarChar(64) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt deletedAt DateTime? messages AdminMessage[] adminUser AdminUser @relation(fields: [adminUserId], references: [id]) @@index([adminUserId]) @@index([hermesSessionId]) } model AdminMessage { id String @id @default(cuid()) conversationId String role String @db.VarChar(16) content String @db.LongText createdAt DateTime @default(now()) conversation AdminConversation @relation(fields: [conversationId], references: [id]) @@index([conversationId]) @@index([createdAt]) } model AdminCostItem { id String @id @default(cuid()) name String @db.VarChar(100) category String @default("other") @db.VarChar(32) amount Float currency String @default("CNY") @db.VarChar(8) purchaseDate DateTime expiryDate DateTime? billingCycle String @default("once") @db.VarChar(16) note String? @db.VarChar(255) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([category]) @@index([expiryDate]) } model AppConfig { id String @id @default(cuid()) key String @unique @db.VarChar(100) value String @db.Text description String? @db.VarChar(500) environment String @default("production") @db.VarChar(32) updatedBy String? @db.VarChar(100) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([key]) } model FeatureFlag { id String @id @default(cuid()) name String @unique @db.VarChar(100) enabled Boolean @default(false) description String? @db.VarChar(500) rolloutPct Int @default(100) whitelist String? @db.Text updatedBy String? @db.VarChar(100) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([name]) } model ConfigChangeLog { id String @id @default(cuid()) entityType String @db.VarChar(32) entityId String @db.VarChar(100) field String @db.VarChar(100) oldValue String? @db.Text newValue String? @db.Text changedBy String? @db.VarChar(100) createdAt DateTime @default(now()) @@index([entityType, entityId]) @@index([createdAt]) } model SecurityEvent { id String @id @default(cuid()) userId String? adminUserId String? eventType String @db.VarChar(64) severity String @default("low") @db.VarChar(16) ip String? @db.VarChar(45) userAgent String? @db.VarChar(500) detail Json? handled Boolean @default(false) handledBy String? @db.VarChar(100) createdAt DateTime @default(now()) @@index([userId]) @@index([eventType]) @@index([createdAt]) } model SensitiveWord { id String @id @default(cuid()) word String @unique @db.VarChar(100) category String @default("general") @db.VarChar(32) riskLevel String @default("medium") @db.VarChar(16) enabled Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([word]) @@index([category]) } model ContentSafetyCheck { id String @id @default(cuid()) userId String? @db.VarChar(100) contentType String @db.VarChar(32) content String @db.Text riskLevel String @db.VarChar(16) matchedWords String? @db.Text result String @default("pending") @db.VarChar(16) reviewerId String? @db.VarChar(100) reviewNote String? @db.VarChar(500) createdAt DateTime @default(now()) reviewedAt DateTime? @@index([userId]) @@index([result]) @@index([createdAt]) } model ContentReport { id String @id @default(cuid()) reporterId String targetType String @db.VarChar(32) targetId String @db.VarChar(100) reason String @db.VarChar(500) status String @default("pending") @db.VarChar(16) handledBy String? @db.VarChar(100) handleNote String? @db.VarChar(500) createdAt DateTime @default(now()) handledAt DateTime? @@index([status]) @@index([createdAt]) }