// 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[]
  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?  @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])
  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])
}
