api-server/prisma/schema.prisma
wangdl 0889e3af46
All checks were successful
Deploy API Server / build (push) Successful in 36s
Deploy API Server / deploy (push) Successful in 1m0s
feat: knowledge generation workflow + drop import-candidate module + migrations
2026-07-08 19:34:10 +08:00

2508 lines
80 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// M-AI-02: Unified AiJob Expand — see docs/architecture/adr-002-ai-job-database-expand.md
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?
// M-MEMBER-01-02: Apple IAP 账号绑定令牌。UUID服务端生成与 userId 解耦,跨设备稳定。
appAccountToken String? @unique @db.Char(36)
authAccounts AuthAccount[]
refreshTokens RefreshToken[]
memberships UserMembership[]
profile UserProfile?
preferences UserPreference?
consents UserConsent[]
knowledgeBases KnowledgeBase[]
subscriptions KnowledgeBaseSubscription[]
quizzes Quiz[]
knowledgeItems KnowledgeItem[]
knowledgeItemRelations KnowledgeItemRelation[]
tags Tag[]
uploadedFiles UploadedFile[]
uploadSessions UploadSession[]
documentImports DocumentImport[]
learningSessions LearningSession[]
learningRecords LearningRecord[]
activeRecallQuestions ActiveRecallQuestion[]
quizAttempts QuizAttempt[]
activeRecallAnswers ActiveRecallAnswer[]
aiJobs AiJob[]
aiAnalysisResults AiAnalysisResult[]
focusItems FocusItem[]
reviewCards ReviewCard[]
reviewLogs ReviewLog[]
reviewPlans ReviewPlan[]
dailyLearningActivities DailyLearningActivity[]
notifications Notification[]
feedbacks Feedback[]
aiUsageLogs AiUsageLog[]
knowledgeSources KnowledgeSource[]
knowledgeChunks KnowledgeChunk[]
knowledgeGenerationTasks KnowledgeGenerationTask[]
readingEvents ReadingEvent[]
materialReadingProgresses MaterialReadingProgress[]
temporaryReadingMaterials TemporaryReadingMaterial[]
learningProfile UserLearningProfile?
aiSettings UserAiSettings?
modelCredentials UserModelCredential[]
aiRuntimeJobs AiRuntimeJob[]
aiRuntimeResults AiRuntimeResult[]
aiAnalysisResultsNew AiLearningAnalysis[]
appStoreTransactions AppStoreTransaction[]
appStoreNotifications AppStoreNotification[]
@@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 Workspace {
id String @id @default(cuid())
userId String
name String @db.VarChar(255)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId])
}
model KnowledgeFolder {
id String @id @default(cuid())
knowledgeBaseId String
parentId String?
name String @db.VarChar(255)
sortOrder Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
knowledgeBase KnowledgeBase @relation(fields: [knowledgeBaseId], references: [id])
parent KnowledgeFolder? @relation("FolderTree", fields: [parentId], references: [id])
children KnowledgeFolder[] @relation("FolderTree")
@@index([knowledgeBaseId])
@@index([parentId])
}
model KnowledgeBase {
id String @id @default(cuid())
userId String
title String @db.VarChar(255)
description String? @db.Text
coverKey String? @db.VarChar(100)
coverType String @default("custom") @db.VarChar(32)
coverIcon String? @db.VarChar(50)
coverColor String? @db.VarChar(20)
visibility String @default("private") @db.VarChar(16)
isPinned Boolean @default(false)
ownerType String @default("user") @db.VarChar(16)
isVerified Boolean @default(false)
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[]
chunks KnowledgeChunk[]
knowledgeGenerationTasks KnowledgeGenerationTask[]
focusItems FocusItem[]
folders KnowledgeFolder[]
subscriptions KnowledgeBaseSubscription[]
quizzes Quiz[]
@@index([userId])
@@index([status])
@@index([visibility])
@@index([ownerType])
}
model KnowledgeBaseSubscription {
id String @id @default(cuid())
userId String
knowledgeBaseId String
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
knowledgeBase KnowledgeBase @relation(fields: [knowledgeBaseId], references: [id])
@@unique([userId, knowledgeBaseId])
@@index([userId])
@@index([knowledgeBaseId])
}
model Artifact {
id String @id @default(cuid())
userId String
kbId String
type String @db.VarChar(32)
title String @db.VarChar(255)
configJson Json?
status String @default("draft") @db.VarChar(16)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId])
@@index([kbId])
}
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
learnable Boolean @default(true)
sourceId String?
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)
durationSeconds Int @default(0)
fileSize BigInt?
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])
source KnowledgeSource? @relation(fields: [sourceId], references: [id])
parent KnowledgeItem? @relation("KnowledgeItemRelations", fields: [parentId], references: [id])
children KnowledgeItem[] @relation("KnowledgeItemRelations")
tags KnowledgeItemTag[]
@@index([userId])
@@index([knowledgeBaseId])
@@index([parentId])
@@index([itemType])
@@index([sourceId])
}
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? @unique @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[]
uploadSession UploadSession?
@@index([userId])
@@index([objectKey])
@@index([sha256])
}
model UploadSession {
id String @id @default(cuid())
userId String
uploadedFileId String? @unique
objectKey String @unique @db.VarChar(500)
declaredSize BigInt
reservedBytes BigInt
quotaOperationId String @unique @db.VarChar(255)
status String @default("created") @db.VarChar(32)
expiresAt DateTime
uploadedAt DateTime?
completedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id])
uploadedFile UploadedFile? @relation(fields: [uploadedFileId], references: [id])
@@index([userId, status])
@@index([expiresAt])
}
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])
@@index([userId])
@@index([status])
@@index([sourceId])
@@index([workerId])
}
model ImportStepLog {
id String @id @default(cuid())
importId String
step String @db.VarChar(32)
status String @db.VarChar(16)
detail String? @db.VarChar(500)
startedAt DateTime?
completedAt DateTime?
createdAt DateTime @default(now())
@@index([importId])
@@index([createdAt])
}
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?
// ── M8 新增字段 ──
clientSessionId String?
materialId String?
readingTargetType String? @db.VarChar(32)
totalActiveSeconds Int @default(0)
lastPosition Json?
lastEventAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id])
@@index([userId])
@@index([knowledgeItemId])
@@index([startedAt])
@@index([clientSessionId])
@@index([materialId])
@@index([status])
@@index([lastEventAt])
@@index([userId, 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])
@@index([createdAt])
@@index([recordType, occurredAt])
@@index([userId, occurredAt])
}
model ReadingEvent {
id String @id @default(cuid())
userId String
eventId String
clientSessionId String
readingTargetType String @db.VarChar(32)
materialId String
knowledgeBaseId String?
eventType String @db.VarChar(32)
position Json?
activeSecondsDelta Int @default(0)
clientTimestampMs BigInt
clientTimezoneOffsetMinutes Int?
sequence Int
platform String? @db.VarChar(16)
appVersion String? @db.VarChar(32)
status String @default("pending") @db.VarChar(32)
errorCode String? @db.VarChar(32)
warningCodes Json?
serverReceivedAt DateTime @default(now())
processedAt DateTime?
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
@@unique([userId, eventId])
@@index([userId, clientSessionId])
@@index([userId, readingTargetType, materialId, clientTimestampMs])
@@index([status, createdAt])
@@index([userId, createdAt])
@@index([serverReceivedAt])
@@index([materialId])
@@index([activeSecondsDelta])
}
model MaterialReadingProgress {
id String @id @default(cuid())
userId String
readingTargetType String @db.VarChar(32)
materialId String
knowledgeBaseId String?
lastClientSessionId String?
lastPosition Json?
lastProgress Float?
totalActiveSeconds Int @default(0)
sessionCount Int @default(0)
status String @default("not_started") @db.VarChar(32)
firstOpenedAt DateTime?
lastOpenedAt DateTime?
lastReadAt DateTime?
isMarkedRead Boolean @default(false)
markedReadAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id])
@@unique([userId, materialId])
@@index([userId])
@@index([knowledgeBaseId])
@@index([status])
@@index([isMarkedRead])
}
model TemporaryReadingMaterial {
id String @id @default(cuid())
userId String
title String? @db.VarChar(255)
originalFilename String? @db.VarChar(255)
mimeType String? @db.VarChar(100)
sizeBytes BigInt @default(0)
storageKey String? @db.VarChar(500)
sourceStatus String @default("active") @db.VarChar(32)
expiresAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
user User @relation(fields: [userId], references: [id])
@@index([userId])
@@index([expiresAt])
}
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 AiJob {
id String @id @default(cuid())
userId String
sessionId String?
answerId String?
jobType String @db.VarChar(32)
// ── M-AI-02-03: 身份与路由 ──
triggerType String? @db.VarChar(32)
queueName String @default("ai-interactive") @db.VarChar(64)
priority Int @default(0)
targetType String? @db.VarChar(32)
targetId String? @db.VarChar(255)
parentJobId String? @db.VarChar(255)
idempotencyKey String? @db.VarChar(255)
retriedFromJobId String? @db.VarChar(255)
inputRef String? @db.VarChar(255)
inputSchemaVersion String? @db.VarChar(100)
// ── 原有字段 ──
status String @default("pending") @db.VarChar(32)
// ── M-AI-02-04: 生命周期与执行 ──
lifecycleStatus String? @db.VarChar(32)
lockedBy String? @db.VarChar(100)
lockedAt DateTime?
lockUntil DateTime?
cancelRequestedAt DateTime?
attemptCount Int @default(0)
maxAttempts Int @default(3)
timeoutMs Int @default(120000)
// ── M-AI-02-04: 凭据与模型 ──
credentialMode String @default("platform_key") @db.VarChar(32)
credentialId String? @db.VarChar(255)
modelTier String @default("primary") @db.VarChar(32)
modelProvider String @default("deepseek") @db.VarChar(32)
modelName String @default("deepseek-chat") @db.VarChar(64)
// ── M-AI-02-04: 输入输出 ──
promptKey String? @db.VarChar(128)
promptVersion String? @db.VarChar(100)
outputSchemaVersion String? @db.VarChar(100)
outputHash String? @db.VarChar(255)
validatedOutput Json?
// ── M-AI-02-04: 错误信息 ──
errorCode String? @db.VarChar(100)
publicErrorMessage String? @db.VarChar(500)
internalErrorMessage String? @map("errorMessage") @db.Text
// ── 时间戳 ──
progress Int @default(0)
queuedAt DateTime?
startedAt DateTime?
finishedAt DateTime? @map("completedAt")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id])
results AiAnalysisResult[]
snapshot AiJobSnapshot?
artifacts AiJobArtifact[]
knowledgeGenerationTasks KnowledgeGenerationTask[]
usageLogs AiUsageLog[]
parentJob AiJob? @relation("JobChildren", fields: [parentJobId], references: [id], onDelete: SetNull, onUpdate: Cascade)
children AiJob[] @relation("JobChildren")
@@index([userId])
@@index([status])
@@index([sessionId])
@@index([userId, jobType, createdAt])
@@index([targetType, targetId])
@@index([parentJobId])
@@index([lifecycleStatus, createdAt])
@@index([queueName, lifecycleStatus, createdAt])
@@unique([userId, jobType, idempotencyKey])
@@map("AiAnalysisJob")
}
// ── M-AI-02-05: 不可变 Job 输入快照 ──
model AiJobSnapshot {
id String @id @default(cuid())
jobId String @unique
jobType String @db.VarChar(64)
schemaVersion String @db.VarChar(100)
redactionVersion String? @db.VarChar(100)
content Json
contentHash String? @db.VarChar(255)
expiresAt DateTime?
createdAt DateTime @default(now())
job AiJob @relation(fields: [jobId], references: [id], onDelete: Cascade, onUpdate: Cascade)
@@index([jobType])
@@index([expiresAt])
@@index([createdAt])
}
// ── M-AI-02-06: 产物关联模型(多态引用)──
model AiJobArtifact {
id String @id @default(cuid())
jobId String
artifactType String @db.VarChar(32)
artifactId String @db.VarChar(255)
ordinal Int @default(0)
metadata Json?
createdAt DateTime @default(now())
job AiJob @relation(fields: [jobId], references: [id], onDelete: Cascade, onUpdate: Cascade)
@@unique([jobId, artifactType, artifactId])
@@index([jobId, ordinal])
@@index([artifactType, artifactId])
}
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 AiJob @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)
source String? @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])
@@index([createdAt])
}
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)
scheduleState String? @db.VarChar(16)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
user User @relation(fields: [userId], references: [id])
logs ReviewLog[]
@@index([userId])
@@index([nextReviewAt])
@@index([focusItemId])
@@index([createdAt])
}
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])
@@index([createdAt])
}
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)
// ── M8 新增字段 ──
readingSeconds Int @default(0)
materialsReadCount Int @default(0)
markedReadCount Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id])
@@unique([userId, activityDate])
@@index([userId])
@@index([activityDate])
}
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?
scope String @default("user") @db.VarChar(16)
readAt DateTime?
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
@@index([userId])
@@index([readAt])
@@index([type])
@@index([createdAt])
}
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)
// ── M-AI-02-08: Job 关联与凭据归属 ──
jobId String? @db.VarChar(191)
attemptNo Int @default(0)
credentialMode String @default("platform_key") @db.VarChar(32)
errorCode String? @db.VarChar(100)
providerRequestId String? @db.VarChar(255)
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
job AiJob? @relation(fields: [jobId], references: [id], onDelete: SetNull, onUpdate: Cascade)
@@index([userId])
@@index([feature])
@@index([createdAt])
@@index([jobId, attemptNo])
@@index([provider, model, createdAt])
@@index([errorCode, createdAt])
}
model ModelRoute {
id String @id @default(cuid())
tier String @db.VarChar(32)
taskType String @default("*") @db.VarChar(32)
preferredProvider String @db.VarChar(32)
preferredModel String @db.VarChar(100)
fallbackProvider String @db.VarChar(32)
fallbackModel String @db.VarChar(100)
maxRetries Int @default(2)
isActive Boolean @default(true)
createdBy String? @db.VarChar(100)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([tier, taskType])
}
model ProviderConfig {
id String @id @default(cuid())
name String @unique @db.VarChar(32)
enabled Boolean @default(true)
baseUrl String? @db.VarChar(255)
updatedBy String? @db.VarChar(100)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model FallbackEvent {
id String @id @default(cuid())
tier String @db.VarChar(32)
taskType String @db.VarChar(32)
fromProvider String @db.VarChar(32)
fromModel String @db.VarChar(100)
toProvider String @db.VarChar(32)
toModel String @db.VarChar(100)
errorMessage String? @db.VarChar(500)
createdAt DateTime @default(now())
}
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[]
items KnowledgeItem[]
imports DocumentImport[]
knowledgeGenerationTasks KnowledgeGenerationTask[]
references SourceReference[]
@@index([userId])
@@index([knowledgeBaseId])
@@index([fileId])
@@index([parseStatus])
@@index([indexStatus])
}
model KnowledgeGenerationTask {
id String @id @default(cuid())
userId String
knowledgeBaseId String
sourceId String?
chatSessionId String?
chatMessageId String?
aiJobId String? @unique
requestKind String @default("manual_source") @db.VarChar(32)
status String @default("queued") @db.VarChar(32)
idempotencyKey String? @db.VarChar(255)
knowledgeItemCount Int @default(0)
errorCode String? @db.VarChar(100)
errorMessage String? @db.Text
requestedAt DateTime @default(now())
startedAt DateTime?
completedAt DateTime?
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])
chatMessage ChatMessage? @relation(fields: [chatMessageId], references: [id])
aiJob AiJob? @relation(fields: [aiJobId], references: [id])
@@index([userId, status])
@@index([knowledgeBaseId, status])
@@index([sourceId, status])
@@index([chatMessageId, status])
@@index([sourceId, requestedAt])
@@index([chatSessionId, requestedAt])
@@unique([userId, sourceId, idempotencyKey])
@@unique([userId, chatMessageId, idempotencyKey])
}
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])
references SourceReference[]
@@index([userId])
@@index([sourceId])
@@index([knowledgeBaseId])
@@index([externalVectorId])
}
model SourceReference {
id String @id @default(cuid())
sourceId String
chunkId String?
artifactType String @db.VarChar(32)
artifactId String @db.VarChar(100)
pageNumber Int?
sectionTitle String? @db.VarChar(500)
excerptText String? @db.VarChar(2000)
createdAt DateTime @default(now())
source KnowledgeSource @relation(fields: [sourceId], references: [id])
chunk KnowledgeChunk? @relation(fields: [chunkId], references: [id])
@@index([artifactType, artifactId])
@@index([sourceId])
}
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 CleanupJob {
id String @id @default(cuid())
type String @db.VarChar(32)
status String @default("RUNNING") @db.VarChar(16)
target String? @db.VarChar(255)
rowsAffected Int @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[]
apiKeys AdminApiKey[]
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 AdminApiKey {
id String @id @default(cuid())
adminUserId String
name String @db.VarChar(100)
keyHash String @unique @db.VarChar(255)
prefix String @db.VarChar(8)
expiresAt DateTime?
lastUsedAt DateTime?
createdBy String? @db.VarChar(100)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
adminUser AdminUser @relation(fields: [adminUserId], references: [id])
@@index([adminUserId])
@@index([keyHash])
}
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)
monthlyQuizCount Int @default(0) // M-MEMBER-01-02
isActive Boolean @default(true)
memberships UserMembership[]
storeProducts StoreProduct[]
planQuotas PlanQuota[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// M-MEMBER-01-02: Apple Product ID → MembershipPlan 映射
model StoreProduct {
id String @id @default(cuid())
platform String @db.VarChar(16) // "apple"
productId String @db.VarChar(100) // Apple Product ID
planId String
billingPeriod String @db.VarChar(16) // "monthly" | "yearly"
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
plan MembershipPlan @relation(fields: [planId], references: [id])
@@unique([platform, productId])
}
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 ChatSession {
id String @id @default(cuid())
userId String
knowledgeBaseId String?
scopeType String @default("knowledge_base") @db.VarChar(32)
scopeId String?
parentKnowledgeBaseId String?
title String @default("新对话") @db.VarChar(200)
knowledgeItemIds Json? @default("[]")
createdFrom String @default("legacy_migration") @db.VarChar(32)
isPinned Boolean @default(false)
isArchived Boolean @default(false)
isDeleted Boolean @default(false)
modelMode String @default("normal") @db.VarChar(16)
modelId String? @db.VarChar(64)
lastMessageAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
messages ChatMessage[]
@@index([userId])
@@index([knowledgeBaseId])
@@index([userId, scopeType, scopeId])
@@index([userId, parentKnowledgeBaseId])
@@index([userId, isDeleted])
@@index([lastMessageAt])
}
model ChatMessage {
id String @id @default(cuid())
sessionId String
role String @db.VarChar(16)
content String @db.LongText
tokens Int @default(0)
scopeSnapshot Json?
createdAt DateTime @default(now())
session ChatSession @relation(fields: [sessionId], references: [id])
citations ChatCitation[]
knowledgeGenerationTasks KnowledgeGenerationTask[]
@@index([sessionId])
}
model ChatCitation {
id String @id @default(cuid())
messageId String
chunkId String?
sourceId String?
sourceTitle String? @db.VarChar(255)
excerptText String? @db.VarChar(2000)
pageNumber Int?
lineStart Int?
lineEnd Int?
createdAt DateTime @default(now())
message ChatMessage @relation(fields: [messageId], references: [id])
@@index([messageId])
@@index([sourceId])
@@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])
}
model ViolationRecord {
id String @id @default(cuid())
userId String
contentType String @db.VarChar(32)
content String @db.VarChar(1000)
riskLevel String @db.VarChar(16)
penalty String @default("none") @db.VarChar(32)
appliedBy String? @db.VarChar(100)
appliedAt DateTime?
createdAt DateTime @default(now())
@@index([userId])
@@index([createdAt])
}
model ApiMetric {
id String @id @default(cuid())
path String @db.VarChar(255)
method String @db.VarChar(10)
statusCode Int
duration Int
userId String? @db.VarChar(100)
ip String? @db.VarChar(45)
createdAt DateTime @default(now())
@@index([path])
@@index([createdAt])
}
model TaskLog {
id String @id @default(cuid())
queueName String @db.VarChar(64)
jobId String @db.VarChar(100)
status String @default("enqueued") @db.VarChar(16)
payload Json?
error String? @db.Text
attempts Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([queueName])
@@index([status])
@@index([createdAt])
}
model UserMembership {
id String @id @default(cuid())
userId String
planId String
// M-MEMBER-01-02: 会员来源与状态
source String @default("admin") @db.VarChar(32) // "apple" | "admin" | "trial" | "compensation"
status String @default("active") @db.VarChar(32) // active | grace_period | billing_retry | expired | revoked
startedAt DateTime @default(now())
expiresAt DateTime?
// M-MEMBER-01-02: active Boolean 保留兼容,由 status + expiresAt 计算
active Boolean @default(true)
// Apple IAP 字段(非 Apple 来源为 null
productId String? @db.VarChar(100)
originalTransactionId String? @db.VarChar(255)
transactionId String? @db.VarChar(255)
environment String? @db.VarChar(16) // "Sandbox" | "Production"
purchaseDate DateTime?
expiresDate DateTime? // Apple 签名的到期时间
gracePeriodExpiresAt DateTime?
autoRenewStatus String? @db.VarChar(32) // "on" | "off"
revocationDate DateTime?
revocationReason String? @db.VarChar(64)
ownershipType String? @db.VarChar(32)
appAccountToken String? @db.VarChar(255) // 发放时的用户令牌快照
serverNotificationId String? // 最后一次通知引用
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id])
plan MembershipPlan @relation(fields: [planId], references: [id])
@@unique([originalTransactionId])
@@unique([transactionId])
@@index([userId])
@@index([expiresDate])
@@index([source])
@@index([status])
}
// M-MEMBER-01-02: 通用额度模型,避免 daily/monthly 混乱
model PlanQuota {
id String @id @default(cuid())
planId String
quotaType String @db.VarChar(64) // knowledge_base_count | storage_bytes | ocr_pages | ai_chat_messages | active_recall 等
limit Int @default(0) // 额度上限isUnlimited=true 时忽略)
unit String @db.VarChar(32) // "count" | "bytes" | "pages"
windowType String @db.VarChar(32) // "daily" | "monthly" | "lifetime" | "per_request"
isUnlimited Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
plan MembershipPlan @relation(fields: [planId], references: [id])
@@unique([planId, quotaType])
@@index([quotaType])
}
model UserDevice {
id String @id @default(cuid())
userId String
deviceId String @db.VarChar(255)
deviceName String? @db.VarChar(100)
osVersion String? @db.VarChar(50)
pushToken String? @db.VarChar(500)
lastSeenAt DateTime @default(now())
createdAt DateTime @default(now())
@@unique([userId, deviceId])
@@index([userId])
}
model AccountDeletionRequest {
id String @id @default(cuid())
userId String
status String @default("pending") @db.VarChar(16)
requestedAt DateTime @default(now())
coolingEndsAt DateTime
reviewedBy String? @db.VarChar(100)
reviewedAt DateTime?
completedAt DateTime?
cancelledAt DateTime?
createdAt DateTime @default(now())
@@index([userId])
@@index([status])
}
model QuotaUsage {
id String @id @default(cuid())
userId String
quotaType String @db.VarChar(32)
amount Int
// N1: @unique 防止并发重复预占
resource String? @unique @db.VarChar(255)
createdAt DateTime @default(now())
@@index([userId, quotaType])
@@index([createdAt])
}
model CostDailySummary {
id String @id @default(cuid())
date DateTime
provider String @db.VarChar(32)
model String? @db.VarChar(100)
calls Int @default(0)
tokens Int @default(0)
cost Float @default(0)
createdAt DateTime @default(now())
@@unique([date, provider, model])
}
model SecretRecord {
id String @id @default(cuid())
name String @unique @db.VarChar(100)
provider String @db.VarChar(32)
encrypted String @db.Text
maskLast4 String @db.VarChar(4)
status String @default("active") @db.VarChar(16)
expiresAt DateTime?
rotatedFrom String? @db.VarChar(100)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([provider])
}
model SecretAccessLog {
id String @id @default(cuid())
secretId String
secretName String @db.VarChar(100)
accessedBy String @db.VarChar(100)
createdAt DateTime @default(now())
@@index([secretId])
@@index([createdAt])
}
model RecentItem {
id String @id @default(cuid())
userId String
targetType String @db.VarChar(32)
targetId String @db.VarChar(255)
title String @db.VarChar(255)
metadata Json?
accessedAt DateTime @updatedAt
createdAt DateTime @default(now())
@@index([userId, accessedAt])
@@index([userId, targetType])
}
model Favorite {
id String @id @default(cuid())
userId String
targetType String @db.VarChar(32)
targetId String @db.VarChar(255)
title String? @db.VarChar(255)
metadata Json?
createdAt DateTime @default(now())
@@unique([userId, targetType, targetId])
@@index([userId])
}
model SearchHistory {
id String @id @default(cuid())
userId String
query String @db.VarChar(500)
resultsCount Int @default(0)
createdAt DateTime @default(now())
@@index([userId, createdAt])
}
model NotificationPreference {
id String @id @default(cuid())
userId String @unique
reviewReminder Boolean @default(true)
learningReminder Boolean @default(true)
streakAlert Boolean @default(true)
pushEnabled Boolean @default(true)
quietStartHour Int?
quietEndHour Int?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model PushToken {
id String @id @default(cuid())
userId String
token String @db.VarChar(500)
platform String @db.VarChar(32)
deviceId String? @db.VarChar(255)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([userId, token])
@@index([userId])
}
model NotificationTemplate {
id String @id @default(cuid())
name String @db.VarChar(100)
type String @db.VarChar(32)
title String @db.VarChar(255)
content String @db.Text
channel String @default("in_app") @db.VarChar(32)
enabled Boolean @default(true)
createdBy String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([type])
@@index([createdAt])
}
model LearningGoal {
id String @id @default(cuid())
userId String @unique
dailyCardTarget Int @default(10)
dailyMinuteTarget Int @default(30)
weeklyCardTarget Int @default(50)
favoriteSubject String? @db.VarChar(100)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model StreakRecord {
id String @id @default(cuid())
userId String
streakType String @db.VarChar(32)
length Int @default(0)
startDate DateTime
endDate DateTime
createdAt DateTime @default(now())
@@index([userId])
@@index([streakType])
}
model LearningStats {
id String @id @default(cuid())
userId String
date DateTime
totalCards Int @default(0)
reviewedCards Int @default(0)
newCards Int @default(0)
totalMinutes Int @default(0)
avgMasteryScore Decimal? @db.Decimal(5, 2)
createdAt DateTime @default(now())
@@unique([userId, date])
@@index([userId])
}
model ServiceHealth {
id String @id @default(cuid())
serverName String @db.VarChar(100)
serviceName String @db.VarChar(64)
status String @db.VarChar(16)
message String? @db.VarChar(500)
checkedAt DateTime @default(now())
@@index([serverName])
@@index([serviceName])
@@index([checkedAt])
}
model ExportJob {
id String @id @default(cuid())
type String @db.VarChar(32)
status String @default("pending") @db.VarChar(16)
format String @default("csv") @db.VarChar(8)
filePath String? @db.VarChar(500)
fileSize Int @default(0)
startedAt DateTime?
completedAt DateTime?
errorMessage String? @db.Text
createdAt DateTime @default(now())
}
model AgentTask {
id String @id @default(cuid())
userId String?
type String @db.VarChar(32)
status String @default("pending") @db.VarChar(16)
input String? @db.Text
output String? @db.Text
sessionId String? @db.VarChar(255)
approvedBy String?
approvedAt DateTime?
giteaUrl String? @db.VarChar(500)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([status])
@@index([userId])
}
model AgentArtifact {
id String @id @default(cuid())
taskId String?
type String @db.VarChar(32)
title String @db.VarChar(255)
content String? @db.Text
status String @default("draft") @db.VarChar(16)
createdBy String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([taskId])
@@index([type])
@@index([createdAt])
}
model DecisionRecord {
id String @id @default(cuid())
title String @db.VarChar(255)
context String? @db.Text
decision String? @db.Text
rationale String? @db.Text
status String @default("proposed") @db.VarChar(32)
createdBy String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([status])
}
model ReleaseChecklist {
id String @id @default(cuid())
version String @db.VarChar(50)
item String @db.VarChar(255)
checked Boolean @default(false)
sortOrder Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([version])
}
model PrivacyPolicy {
id String @id @default(cuid())
version String @db.VarChar(50)
title String @db.VarChar(255)
content String @db.Text
effectiveAt DateTime
published Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([version])
}
model UserAgreement {
id String @id @default(cuid())
version String @db.VarChar(50)
title String @db.VarChar(255)
content String @db.Text
effectiveAt DateTime
published Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([version])
}
model FilingRecord {
id String @id @default(cuid())
type String @db.VarChar(64)
title String @db.VarChar(255)
filePath String? @db.VarChar(500)
status String @default("pending") @db.VarChar(16)
notes String? @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model DataExportRequest {
id String @id @default(cuid())
userId String
status String @default("pending") @db.VarChar(16)
filePath String? @db.VarChar(500)
completedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId])
@@index([status])
}
model VendorBill {
id String @id @default(cuid())
provider String @db.VarChar(32)
billMonth String @db.VarChar(7)
amount Decimal @db.Decimal(10, 2)
currency String @default("CNY") @db.VarChar(8)
usageSummary Json?
paidAt DateTime?
notes String? @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([provider, billMonth])
@@index([provider])
}
// ── Quiz ──
model Quiz {
id String @id @default(cuid())
userId String
knowledgeBaseId String
title String @db.VarChar(255)
description String? @db.Text
questionCount Int @default(0)
sourceType String @default("kb") @db.VarChar(16)
sourceId String? @db.VarChar(100)
status String @default("ready") @db.VarChar(16)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id])
knowledgeBase KnowledgeBase @relation(fields: [knowledgeBaseId], references: [id])
questions QuizQuestion[]
attempts QuizAttempt[]
@@index([userId])
@@index([knowledgeBaseId])
}
model QuizQuestion {
id String @id @default(cuid())
quizId String
type String @db.VarChar(16)
stem String @db.Text
options Json?
answer String @db.VarChar(500)
explanation String? @db.Text
sourceBlockIds Json?
orderIndex Int @default(0)
createdAt DateTime @default(now())
quiz Quiz @relation(fields: [quizId], references: [id])
answers QuizAnswer[]
@@index([quizId])
}
model QuizAttempt {
id String @id @default(cuid())
quizId String
userId String
totalQuestions Int @default(0)
correctCount Int @default(0)
score Int @default(0)
startedAt DateTime @default(now())
finishedAt DateTime?
quiz Quiz @relation(fields: [quizId], references: [id])
user User @relation(fields: [userId], references: [id])
answers QuizAnswer[]
@@index([quizId])
@@index([userId])
}
model QuizAnswer {
id String @id @default(cuid())
attemptId String
questionId String
userAnswer String @db.VarChar(500)
isCorrect Boolean @default(false)
answeredAt DateTime @default(now())
attempt QuizAttempt @relation(fields: [attemptId], references: [id])
question QuizQuestion @relation(fields: [questionId], references: [id])
@@index([attemptId])
@@index([questionId])
}
// ═══════════════════════════════════════════════════════
// M-API-AI-RUNTIME: AI Runtime 调度与落库
// ═══════════════════════════════════════════════════════
// ── API-AI-004: UserLearningProfile ──
model UserLearningProfile {
id String @id @default(cuid())
userId String @unique
learningGoal String? @db.VarChar(255)
currentLevel String? @db.VarChar(32)
dailyAvailableMinutes Int?
qualityPreference String? @db.VarChar(32)
ageRange String? @db.VarChar(32)
occupation String? @db.VarChar(100)
examTarget String? @db.VarChar(255)
learningDeadline DateTime?
learningStyle String? @db.VarChar(100)
aiAcceptanceLevel String? @db.VarChar(32)
digitalSkillLevel String? @db.VarChar(32)
preferredQuestionTypes Json?
preferredLanguage String? @db.VarChar(32)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id])
}
// ── API-AI-005: UserAiSettings ──
model UserAiSettings {
id String @id @default(cuid())
userId String @unique
allowAiAnalysis Boolean @default(true)
allowUseLearningBehavior Boolean @default(true)
allowUseUserProfile Boolean @default(true)
allowUseDocumentContent Boolean @default(false)
allowStoreAiAnalysisHistory Boolean @default(true)
apiKeyMode String @default("platform_key") @db.VarChar(32)
defaultCredentialId String?
fallbackToPlatformKey Boolean @default(true)
maxDailyAiJobs Int @default(20)
maxDailyTokenBudget Int @default(100000)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id])
}
// ── API-AI-006: UserModelCredential ──
model UserModelCredential {
id String @id @default(cuid())
userId String
provider String @default("deepseek") @db.VarChar(32)
keyAlias String? @db.VarChar(100)
encryptedApiKey String @db.Text
keyHash String @db.VarChar(255)
maskedKey String @db.VarChar(20)
status String @default("active") @db.VarChar(32)
lastTestedAt DateTime?
lastUsedAt DateTime?
lastErrorCode String? @db.VarChar(100)
lastErrorMessage String? @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
user User @relation(fields: [userId], references: [id])
@@index([userId])
@@index([keyHash])
}
// ── API-AI-007: AiRuntimeJob ──
model AiRuntimeJob {
id String @id @default(cuid())
userId String
jobType String @db.VarChar(64)
targetType String @db.VarChar(32)
targetId String @db.VarChar(255)
snapshotId String?
status String @default("pending") @db.VarChar(32)
priority Int @default(0)
idempotencyKey String? @unique @db.VarChar(255)
apiKeyMode String @default("platform_key") @db.VarChar(32)
credentialId String?
modelProvider String @default("deepseek") @db.VarChar(32)
modelName String @default("deepseek-chat") @db.VarChar(64)
promptVersion String? @db.VarChar(100)
outputSchemaVersion String? @db.VarChar(100)
attemptNo Int @default(0)
retriedFromJobId String? @db.VarChar(255)
lockedBy String? @db.VarChar(100)
lockedAt DateTime?
lockUntil DateTime?
startedAt DateTime?
finishedAt DateTime?
retryCount Int @default(0)
maxRetryCount Int @default(3)
timeoutSeconds Int @default(120)
errorCode String? @db.VarChar(100)
errorMessage String? @db.Text
cancelRequestedAt DateTime?
cancelledAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id])
result AiRuntimeResult?
@@index([status])
@@index([jobType])
@@index([userId])
@@index([targetType, targetId])
@@index([lockUntil])
}
// ── API-AI-007: AiRuntimeResult ──
model AiRuntimeResult {
id String @id @default(cuid())
jobId String @unique
userId String
runtimeInstanceId String @db.VarChar(100)
status String @db.VarChar(32)
attemptNo Int @default(0)
resultIdempotencyKey String? @db.VarChar(255)
outputHash String? @db.VarChar(255)
rawOutput Json?
validatedOutput Json?
schemaVersion String @db.VarChar(100)
validationErrors Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id])
job AiRuntimeJob @relation(fields: [jobId], references: [id])
@@index([userId])
@@index([jobId])
@@index([resultIdempotencyKey])
}
// ── API-AI-008: LearningAnalysisSnapshot ──
model LearningAnalysisSnapshot {
id String @id @default(cuid())
userId String
scopeType String @db.VarChar(32)
scopeId String @db.VarChar(255)
snapshotVersion String @db.VarChar(100)
sourceDataVersion String? @db.VarChar(100)
privacyScope Json?
userProfile Json?
aiSettings Json?
deviceContext Json?
learningBehaviorSummary Json?
materialProgressSummary Json?
contentStructureSummary Json?
behaviorSignals Json?
scoreSignals Json?
constraints Json?
allowedModelFields Json?
expiresAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId])
@@index([scopeType, scopeId])
@@index([expiresAt])
}
// ── API-AI-009: ModelInvocationLog ──
model ModelInvocationLog {
id String @id @default(cuid())
userId String
jobId String
provider String @db.VarChar(32)
model String @db.VarChar(64)
apiKeyMode String @db.VarChar(32)
credentialId String? @db.VarChar(255)
promptName String @db.VarChar(100)
promptVersion String @db.VarChar(100)
outputSchemaVersion String @db.VarChar(100)
inputTokens Int @default(0)
outputTokens Int @default(0)
totalTokens Int @default(0)
latencyMs Int @default(0)
costEstimate Int?
success Boolean @default(false)
errorCode String? @db.VarChar(100)
errorMessage String? @db.Text
retryCount Int @default(0)
runtimeInstanceId String @db.VarChar(100)
traceId String? @db.VarChar(255)
correlationId String? @db.VarChar(255)
createdAt DateTime @default(now())
@@index([userId])
@@index([jobId])
@@index([provider, model])
@@index([createdAt])
}
// ── P0 增补: UserAiUsageDaily (API-AI-070) ──
model UserAiUsageDaily {
id String @id @default(cuid())
userId String
localDate DateTime
apiKeyMode String @db.VarChar(32)
jobCount Int @default(0)
inputTokens Int @default(0)
outputTokens Int @default(0)
totalTokens Int @default(0)
costEstimate Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([userId, localDate, apiKeyMode])
@@index([userId])
}
// ── P0 增补: PlatformAiBudgetDaily (API-AI-071) ──
model PlatformAiBudgetDaily {
id String @id @default(cuid())
localDate DateTime
provider String @db.VarChar(32)
model String @db.VarChar(64)
inputTokens Int @default(0)
outputTokens Int @default(0)
totalTokens Int @default(0)
costEstimate Int @default(0)
jobCount Int @default(0)
failedCount Int @default(0)
circuitBreakerStatus String @default("closed") @db.VarChar(32)
circuitBreakerReason String? @db.VarChar(255)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([localDate, provider, model])
}
// ── P0 增补: RuntimeInstance (API-AI-075) ──
model RuntimeInstance {
id String @id @default(cuid())
runtimeInstanceId String @unique @db.VarChar(100)
runtimeVersion String? @db.VarChar(50)
status String @default("active") @db.VarChar(32)
lastHeartbeatAt DateTime?
capabilities Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// ── P1 预留: AiLearningAnalysis ──
model AiLearningAnalysis {
id String @id @default(cuid())
userId String
jobId String
user User @relation(fields: [userId], references: [id])
snapshotId String?
targetType String @db.VarChar(32)
targetId String @db.VarChar(255)
learningState String? @db.VarChar(32)
summary String? @db.Text
riskLevel String? @db.VarChar(32)
confidence Float?
evidence Json?
nextActionIds Json?
promptVersion String? @db.VarChar(100)
schemaVersion String? @db.VarChar(100)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId])
@@index([jobId])
}
// ── P1 预留: WeakPointCandidate ──
model WeakPointCandidate {
id String @id @default(cuid())
userId String
jobId String
snapshotId String?
targetType String @db.VarChar(32)
targetId String @db.VarChar(255)
knowledgePointId String? @db.VarChar(255)
title String @db.VarChar(255)
reason String? @db.Text
confidence Float?
evidence Json?
status String @default("active") @db.VarChar(32)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId])
@@index([jobId])
}
// ── P1 预留: NextActionRecommendation ──
model NextActionRecommendation {
id String @id @default(cuid())
userId String
jobId String
snapshotId String?
actionType String @db.VarChar(32)
targetType String? @db.VarChar(32)
targetId String? @db.VarChar(255)
title String @db.VarChar(255)
reason String? @db.Text
priority Int @default(0)
estimatedMinutes Int?
deviceSuitability String? @db.VarChar(32)
status String @default("active") @db.VarChar(32)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId])
@@index([jobId])
}
// ── P1 预留: QuestionGenerationPlan ──
model QuestionGenerationPlan {
id String @id @default(cuid())
userId String
jobId String
snapshotId String?
targetType String? @db.VarChar(32)
targetId String? @db.VarChar(255)
sourceIds Json?
questionTypes Json?
difficultyLevel String? @db.VarChar(32)
count Int @default(5)
reason String? @db.Text
status String @default("pending") @db.VarChar(32)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId])
}
// ── P1 预留: FlashcardGenerationPlan ──
model FlashcardGenerationPlan {
id String @id @default(cuid())
userId String
jobId String
snapshotId String?
targetType String? @db.VarChar(32)
targetId String? @db.VarChar(255)
knowledgePointIds Json?
count Int @default(5)
difficultyLevel String? @db.VarChar(32)
reason String? @db.Text
status String @default("pending") @db.VarChar(32)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId])
}
// ── P1 预留: Flashcard ──
model Flashcard {
id String @id @default(cuid())
userId String
sourceType String? @db.VarChar(32)
sourceId String? @db.VarChar(255)
knowledgePointId String? @db.VarChar(255)
front String @db.Text
back String @db.Text
hint String? @db.Text
difficultyLevel String? @db.VarChar(32)
sourceBlockIds Json?
generatedByJobId String? @db.VarChar(255)
promptVersion String? @db.VarChar(100)
schemaVersion String? @db.VarChar(100)
status String @default("draft") @db.VarChar(32)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
@@index([userId])
}
// ── P1 预留: AiArtifactFeedback ──
model AiArtifactFeedback {
id String @id @default(cuid())
userId String
artifactType String @db.VarChar(32)
artifactId String @db.VarChar(255)
feedbackType String @db.VarChar(32)
reason String? @db.Text
createdAt DateTime @default(now())
@@index([userId])
@@index([artifactType])
}
// ── M-AI-02-07: Transactional Outbox ──
model OutboxEvent {
id String @id @default(cuid())
eventType String @db.VarChar(128)
aggregateType String @db.VarChar(64)
aggregateId String @db.VarChar(255)
dedupeKey String @unique @db.VarChar(255)
payload Json
status String @default("pending") @db.VarChar(32)
attemptCount Int @default(0)
availableAt DateTime @default(now())
lockedAt DateTime?
lockedBy String? @db.VarChar(100)
publishedAt DateTime?
lastErrorCode String? @db.VarChar(100)
lastErrorMessage String? @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([status, availableAt])
@@index([lockedAt])
@@index([aggregateType, aggregateId])
}
// ═══════════════════════════════════════════════
// M-MEMBER-01-03: App Store 交易与通知审计模型
// ═══════════════════════════════════════════════
// 不可变交易记录。每次续订产生新记录,禁止 update 覆盖历史。
model AppStoreTransaction {
id String @id @default(cuid())
userId String
membershipId String? // → UserMembership.id发放时写入
productId String @db.VarChar(100)
transactionId String @unique @db.VarChar(255)
originalTransactionId String @db.VarChar(255)
appAccountToken String @db.VarChar(255)
environment String @db.VarChar(16) // "Sandbox" | "Production"
purchaseDate DateTime
originalPurchaseDate DateTime
expiresDate DateTime?
revocationDate DateTime?
revocationReason String? @db.VarChar(64)
ownershipType String? @db.VarChar(32)
signedDate DateTime
transactionReason String? @db.VarChar(64)
webOrderLineItemId String? @db.VarChar(255)
rawPayloadHash String @db.Char(64) // SHA-256 hex (64 chars)
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
@@index([originalTransactionId])
@@index([userId])
@@index([productId])
@@index([expiresDate])
}
// Server Notification V2 Durable Inbox
model AppStoreNotification {
id String @id @default(cuid())
notificationUuid String @unique @db.VarChar(255)
notificationType String @db.VarChar(64) // SUBSCRIBED | DID_RENEW | REFUND | REVOKE 等
subtype String? @db.VarChar(64)
environment String @db.VarChar(16)
signedDate DateTime
originalTransactionId String? @db.VarChar(255) // 可能尚未解析
transactionId String? @db.VarChar(255) // 可能尚未解析
userId String? // 解析后写入
// Durable Inbox 状态机received → processing → processed | failed | ignored
processedStatus String @default("received") @db.VarChar(32)
processingStartedAt DateTime?
processedAt DateTime?
retryCount Int @default(0)
nextRetryAt DateTime?
errorCode String? @db.VarChar(64)
errorMessage String? @db.VarChar(1000)
// Payload 存储:完整 JWS → COS 对象,数据库仅存 hash + key
payloadHash String @db.Char(64) // SHA-256 hex (64 chars)
payloadObjectKey String? @db.VarChar(500) // COS object key加密存储
payloadExpiresAt DateTime? // 90 天自动清理
createdAt DateTime @default(now())
user User? @relation(fields: [userId], references: [id])
@@index([originalTransactionId])
@@index([notificationType])
@@index([processedStatus])
@@index([createdAt])
}