- Add User.appAccountToken, StoreProduct, PlanQuota models - Extend UserMembership with Apple IAP fields + source/status - Add AppStoreTransaction (immutable) + AppStoreNotification (Durable Inbox) - Implement EffectiveMembershipService (multi-grant → effectivePlan) - Implement EffectiveQuotaService (PlanQuota-driven, Legacy/Shadow/Enforced) - Implement AppStoreJwsVerifier (x5c chain → Apple Root CA G3) - Implement AppStoreTransactionService (JWS verify + atomic projection) - Implement AppStoreNotificationService (9 event types + lifecycle) - Wire QuotaGuard across 5 AI Job types (active_recall/feynman/quiz/learning/flashcard) - Add Membership API: GET /plans, /me, /usage, POST /apple/transactions - Add webhook: POST /webhooks/app-store/v2 (@Public) - Add 11 unit tests for EffectiveMembershipService - Migration: 3 SQL files (expand_schema, transaction_notification, quotausage_unique) Co-Authored-By: Claude <noreply@anthropic.com>
2478 lines
78 KiB
Plaintext
2478 lines
78 KiB
Plaintext
// 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[]
|
||
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[]
|
||
importCandidates ImportCandidate[]
|
||
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[]
|
||
candidates ImportCandidate[]
|
||
chunks KnowledgeChunk[]
|
||
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? @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 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[]
|
||
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[]
|
||
references SourceReference[]
|
||
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])
|
||
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 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 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[]
|
||
|
||
@@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)
|
||
knowledgePointIds 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])
|
||
}
|