378 lines
16 KiB
Plaintext
378 lines
16 KiB
Plaintext
|
|
// This is your Prisma schema file,
|
|||
|
|
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
|||
|
|
|
|||
|
|
generator client {
|
|||
|
|
provider = "prisma-client-js"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
datasource db {
|
|||
|
|
provider = "postgresql"
|
|||
|
|
url = env("DATABASE_URL")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
model User {
|
|||
|
|
id String @id @default(uuid())
|
|||
|
|
phone String? @unique
|
|||
|
|
appleId String? @unique @map("apple_id")
|
|||
|
|
nickname String?
|
|||
|
|
avatar String?
|
|||
|
|
digitalId String? @unique @map("digital_id") // ✅ 赛博学习证ID (Wild ID)
|
|||
|
|
agreementAccepted Boolean @default(false) @map("agreement_accepted")
|
|||
|
|
isPro Boolean @default(false) @map("is_pro") // 是否为付费会员
|
|||
|
|
proExpireDate DateTime? @map("pro_expire_date") // 会员过期时间(可选,预留给订阅制)
|
|||
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|||
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|||
|
|
|
|||
|
|
settings UserSettings?
|
|||
|
|
learningProgress UserLearningProgress[]
|
|||
|
|
achievements UserAchievement[]
|
|||
|
|
courses UserCourse[]
|
|||
|
|
notes Note[]
|
|||
|
|
notebooks Notebook[]
|
|||
|
|
createdCourses Course[] @relation("CreatedCourses") // ✅ 新增:用户创建的课程
|
|||
|
|
generationTasks CourseGenerationTask[] // ✅ AI 课程生成任务
|
|||
|
|
|
|||
|
|
@@map("users")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
model UserSettings {
|
|||
|
|
userId String @id @map("user_id")
|
|||
|
|
pushNotification Boolean @default(true) @map("push_notification")
|
|||
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|||
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|||
|
|
|
|||
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|||
|
|
|
|||
|
|
@@map("user_settings")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
model Course {
|
|||
|
|
id String @id @default(uuid())
|
|||
|
|
title String
|
|||
|
|
subtitle String? // 课程副标题
|
|||
|
|
description String?
|
|||
|
|
coverImage String? @map("cover_image")
|
|||
|
|
themeColor String? @map("theme_color") // ✅ 新增:主题色 Hex(如 "#2266FF")
|
|||
|
|
watermarkIcon String? @map("watermark_icon") // ✅ 新增:水印图标名称(SF Symbol,如 "book.closed.fill")
|
|||
|
|
type String @default("system") // ✅ 简化:所有课程统一为 system(竖屏课程)
|
|||
|
|
status String @default("published") // ✅ 新增:published | draft | test_published
|
|||
|
|
minAppVersion String? @map("min_app_version") // ✅ 新增:最低App版本号(如 "1.0.0"),null表示所有版本可见
|
|||
|
|
isPortrait Boolean @default(true) @map("is_portrait") // ✅ 简化:所有课程都是竖屏,默认值改为 true
|
|||
|
|
deletedAt DateTime? @map("deleted_at") // ✅ 新增:软删除时间戳
|
|||
|
|
totalNodes Int @default(0) @map("total_nodes")
|
|||
|
|
// ✅ 创作者和可见范围
|
|||
|
|
createdBy String? @map("created_by") // null = 系统创建,有值 = 用户ID
|
|||
|
|
visibility String @default("private") @map("visibility") // "public" | "private"
|
|||
|
|
createdAsDraft Boolean @default(false) @map("created_as_draft") // 后台 AI 创建为草稿时 true,完成逻辑据此跳过自动发布,避免多查 Task 表
|
|||
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|||
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|||
|
|
|
|||
|
|
// ✅ 续旧课链路
|
|||
|
|
parentCourseId String? @map("parent_course_id") // 续旧课时指向父课程
|
|||
|
|
accumulatedSummary String? @map("accumulated_summary") // 累积知识点摘要(≤1000字)
|
|||
|
|
|
|||
|
|
creator User? @relation("CreatedCourses", fields: [createdBy], references: [id], onDelete: SetNull)
|
|||
|
|
parentCourse Course? @relation("CourseContinuation", fields: [parentCourseId], references: [id], onDelete: SetNull)
|
|||
|
|
childCourses Course[] @relation("CourseContinuation")
|
|||
|
|
chapters CourseChapter[]
|
|||
|
|
nodes CourseNode[]
|
|||
|
|
userCourses UserCourse[]
|
|||
|
|
notes Note[]
|
|||
|
|
generationTask CourseGenerationTask? // ✅ AI 生成任务关联
|
|||
|
|
operationalBannerCourses OperationalBannerCourse[]
|
|||
|
|
|
|||
|
|
@@index([createdBy])
|
|||
|
|
@@index([visibility])
|
|||
|
|
@@index([parentCourseId])
|
|||
|
|
@@map("courses")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 发现页运营位(软删除,orderIndex 从 1)
|
|||
|
|
model OperationalBanner {
|
|||
|
|
id String @id @default(uuid())
|
|||
|
|
title String
|
|||
|
|
orderIndex Int @default(1) @map("order_index")
|
|||
|
|
isEnabled Boolean @default(true) @map("is_enabled")
|
|||
|
|
deletedAt DateTime? @map("deleted_at")
|
|||
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|||
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|||
|
|
|
|||
|
|
courses OperationalBannerCourse[]
|
|||
|
|
|
|||
|
|
@@index([deletedAt, isEnabled])
|
|||
|
|
@@map("operational_banners")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 运营位-课程关联(每运营位最多 10 门课,业务层校验)
|
|||
|
|
model OperationalBannerCourse {
|
|||
|
|
id String @id @default(uuid())
|
|||
|
|
bannerId String @map("banner_id")
|
|||
|
|
courseId String @map("course_id")
|
|||
|
|
orderIndex Int @default(1) @map("order_index")
|
|||
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|||
|
|
|
|||
|
|
banner OperationalBanner @relation(fields: [bannerId], references: [id], onDelete: Cascade)
|
|||
|
|
course Course @relation(fields: [courseId], references: [id], onDelete: Cascade)
|
|||
|
|
|
|||
|
|
@@unique([bannerId, courseId])
|
|||
|
|
@@index([bannerId])
|
|||
|
|
@@map("operational_banner_courses")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ✅ AI 课程生成任务表
|
|||
|
|
model CourseGenerationTask {
|
|||
|
|
id String @id @default(uuid())
|
|||
|
|
courseId String @unique @map("course_id")
|
|||
|
|
userId String @map("user_id")
|
|||
|
|
sourceText String @map("source_text")
|
|||
|
|
sourceType String? @map("source_type") // ✅ 新增:direct | document | continue
|
|||
|
|
persona String? @map("persona") // ✅ 新增:architect | muse | hacker
|
|||
|
|
mode String? // "detailed" | "essence"
|
|||
|
|
modelProvider String @default("doubao") @map("model_provider")
|
|||
|
|
status String @default("pending") // pending | mode_selected | outline_generating | outline_completed | content_generating | completed | failed
|
|||
|
|
progress Int @default(0) // 0-100
|
|||
|
|
errorMessage String? @map("error_message")
|
|||
|
|
outline Json? // 生成的大纲(JSON格式)
|
|||
|
|
currentStep String? @map("current_step") // 当前步骤:outline | content | node_xxx
|
|||
|
|
saveAsDraft Boolean @default(false) @map("save_as_draft") // 后台创建:生成完成后不自动发布、不自动加入 UserCourse
|
|||
|
|
promptSent String? @map("prompt_sent") // 当时发给模型的真实提示词(fire-and-forget 写入,供调用记录查看)
|
|||
|
|
modelId String? @map("model_id") // 本次任务实际使用的模型 ID(如 doubao-seed-1-6-flash-250828 / doubao-seed-1-6-lite-251015),供调用记录详情展示
|
|||
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|||
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|||
|
|
|
|||
|
|
course Course @relation(fields: [courseId], references: [id], onDelete: Cascade)
|
|||
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|||
|
|
|
|||
|
|
@@index([userId])
|
|||
|
|
@@index([status])
|
|||
|
|
@@map("course_generation_tasks")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
model CourseChapter {
|
|||
|
|
id String @id @default(uuid())
|
|||
|
|
courseId String @map("course_id")
|
|||
|
|
title String
|
|||
|
|
orderIndex Int @map("order_index")
|
|||
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|||
|
|
|
|||
|
|
course Course @relation(fields: [courseId], references: [id], onDelete: Cascade)
|
|||
|
|
nodes CourseNode[]
|
|||
|
|
|
|||
|
|
@@unique([courseId, orderIndex])
|
|||
|
|
@@map("course_chapters")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
model CourseNode {
|
|||
|
|
id String @id @default(uuid())
|
|||
|
|
courseId String @map("course_id")
|
|||
|
|
chapterId String? @map("chapter_id") // 可选,支持无章节的节点
|
|||
|
|
title String
|
|||
|
|
subtitle String?
|
|||
|
|
orderIndex Int @map("order_index")
|
|||
|
|
duration Int? // 预估时长(分钟)
|
|||
|
|
unlockCondition String? @map("unlock_condition") // 解锁条件
|
|||
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|||
|
|
|
|||
|
|
course Course @relation(fields: [courseId], references: [id], onDelete: Cascade)
|
|||
|
|
chapter CourseChapter? @relation(fields: [chapterId], references: [id], onDelete: SetNull)
|
|||
|
|
slides NodeSlide[]
|
|||
|
|
learningProgress UserLearningProgress[]
|
|||
|
|
notes Note[]
|
|||
|
|
|
|||
|
|
@@unique([courseId, orderIndex])
|
|||
|
|
@@map("course_nodes")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
model NodeSlide {
|
|||
|
|
id String @id @default(uuid())
|
|||
|
|
nodeId String @map("node_id")
|
|||
|
|
slideType String @map("slide_type") // text | image | quiz | interactive
|
|||
|
|
orderIndex Int @map("order_index")
|
|||
|
|
content Json // 存储卡片内容(灵活结构)
|
|||
|
|
effect String? // fade_in | typewriter | slide_up
|
|||
|
|
interaction String? // tap_to_reveal | zoom | parallax
|
|||
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|||
|
|
|
|||
|
|
node CourseNode @relation(fields: [nodeId], references: [id], onDelete: Cascade)
|
|||
|
|
|
|||
|
|
@@map("node_slides")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
model UserLearningProgress {
|
|||
|
|
id String @id @default(uuid())
|
|||
|
|
userId String @map("user_id")
|
|||
|
|
nodeId String @map("node_id")
|
|||
|
|
status String @default("not_started") // not_started | in_progress | completed
|
|||
|
|
startedAt DateTime? @map("started_at")
|
|||
|
|
completedAt DateTime? @map("completed_at")
|
|||
|
|
totalStudyTime Int @default(0) @map("total_study_time") // 总学习时长(秒)
|
|||
|
|
currentSlide Int @default(0) @map("current_slide") // 当前学习到的幻灯片位置
|
|||
|
|
completionRate Int @default(0) @map("completion_rate") // 完成度(%)
|
|||
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|||
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|||
|
|
|
|||
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|||
|
|
node CourseNode @relation(fields: [nodeId], references: [id], onDelete: Cascade)
|
|||
|
|
|
|||
|
|
@@unique([userId, nodeId])
|
|||
|
|
@@map("user_learning_progress")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
model UserAchievement {
|
|||
|
|
id String @id @default(uuid())
|
|||
|
|
userId String @map("user_id")
|
|||
|
|
achievementType String @map("achievement_type") // lesson_completed | course_completed
|
|||
|
|
achievementData Json @map("achievement_data") // 存储成就详情
|
|||
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|||
|
|
|
|||
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|||
|
|
|
|||
|
|
@@map("user_achievements")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
model UserCourse {
|
|||
|
|
id String @id @default(uuid())
|
|||
|
|
userId String @map("user_id")
|
|||
|
|
courseId String @map("course_id")
|
|||
|
|
lastOpenedAt DateTime? @map("last_opened_at") // ✅ 新增:最近打开时间,用于排序
|
|||
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|||
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|||
|
|
|
|||
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|||
|
|
course Course @relation(fields: [courseId], references: [id], onDelete: Cascade)
|
|||
|
|
|
|||
|
|
@@unique([userId, courseId])
|
|||
|
|
@@map("user_courses")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ✅ Phase 1: 新增 Notebook 模型
|
|||
|
|
model Notebook {
|
|||
|
|
id String @id @default(uuid())
|
|||
|
|
userId String @map("user_id")
|
|||
|
|
title String // 笔记本名称,1-50字符
|
|||
|
|
description String? // 描述,可选,0-200字符
|
|||
|
|
coverImage String? @map("cover_image") // 封面图片,可选
|
|||
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|||
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|||
|
|
|
|||
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|||
|
|
notes Note[]
|
|||
|
|
|
|||
|
|
@@index([userId])
|
|||
|
|
@@map("notebooks")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ✅ Phase 1: 扩展 Note 模型,支持笔记本和层级结构
|
|||
|
|
model Note {
|
|||
|
|
id String @id @default(uuid())
|
|||
|
|
userId String @map("user_id")
|
|||
|
|
|
|||
|
|
// ✅ 新增:笔记本和层级字段
|
|||
|
|
notebookId String? @map("notebook_id") // 所属笔记本 ID,nil 表示未分类
|
|||
|
|
parentId String? @map("parent_id") // 父笔记 ID,nil 表示顶级
|
|||
|
|
order Int @default(0) // 同级排序,0, 1, 2...
|
|||
|
|
level Int @default(0) // 层级深度,0=顶级, 1=二级, 2=三级
|
|||
|
|
|
|||
|
|
// ✅ 修改:这些字段改为可选(支持独立笔记)
|
|||
|
|
courseId String? @map("course_id")
|
|||
|
|
nodeId String? @map("node_id")
|
|||
|
|
startIndex Int? @map("start_index") // 全局 NSRange.location(相对于合并后的全文)
|
|||
|
|
length Int? // 全局 NSRange.length
|
|||
|
|
|
|||
|
|
type String // highlight | thought | comment(未来扩展)
|
|||
|
|
content String // 笔记内容(想法笔记的内容,或划线笔记的备注)
|
|||
|
|
quotedText String? @map("quoted_text") // ✅ 修改:改为可选,独立笔记为 null
|
|||
|
|
style String? // 样式(如 "yellow", "purple", "underline" 等,未来扩展)
|
|||
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|||
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|||
|
|
|
|||
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|||
|
|
notebook Notebook? @relation(fields: [notebookId], references: [id], onDelete: Cascade) // ✅ 重要:Cascade 删除
|
|||
|
|
course Course? @relation(fields: [courseId], references: [id], onDelete: Cascade)
|
|||
|
|
node CourseNode? @relation(fields: [nodeId], references: [id], onDelete: Cascade)
|
|||
|
|
|
|||
|
|
@@index([userId, courseId])
|
|||
|
|
@@index([userId, nodeId])
|
|||
|
|
@@index([courseId, nodeId])
|
|||
|
|
@@index([userId, notebookId]) // ✅ 新增:按笔记本查询
|
|||
|
|
@@index([notebookId, parentId]) // ✅ 新增:按父笔记查询
|
|||
|
|
@@index([notebookId, order]) // ✅ 新增:按排序查询
|
|||
|
|
@@map("notes")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 应用配置(如书籍解析 Prompt 等可运维修改的文案)
|
|||
|
|
model AppConfig {
|
|||
|
|
id String @id @default(uuid())
|
|||
|
|
key String @unique
|
|||
|
|
value String @db.Text
|
|||
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|||
|
|
|
|||
|
|
@@map("app_config")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ✅ AI 提示词配置表(提示词管理2.0)
|
|||
|
|
model AiPromptConfig {
|
|||
|
|
id String @id @default(uuid())
|
|||
|
|
promptType String @unique @map("prompt_type") // summary | outline | outline-essence | outline-detailed | content
|
|||
|
|
promptTitle String? @map("prompt_title") // 提示词标题
|
|||
|
|
description String? @db.Text // 描述
|
|||
|
|
systemPrompt String @db.Text @map("system_prompt") // 系统提示词(可编辑)
|
|||
|
|
userPromptTemplate String @db.Text @map("user_prompt_template") // 用户提示词模板(只读展示)
|
|||
|
|
variables Json? // 变量说明 [{name, description, required, example}]
|
|||
|
|
temperature Float @default(0.7) // 温度参数
|
|||
|
|
maxTokens Int? @map("max_tokens") // 最大token数
|
|||
|
|
topP Float? @map("top_p") // Top P 参数
|
|||
|
|
enabled Boolean @default(true) // 是否启用
|
|||
|
|
version Int @default(1) // 版本号
|
|||
|
|
isDefault Boolean @default(false) @map("is_default") // 是否为默认配置
|
|||
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|||
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|||
|
|
|
|||
|
|
@@index([promptType])
|
|||
|
|
@@map("ai_prompt_configs")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 书籍解析 AI 调用日志(仅后台查看,不影响主流程)
|
|||
|
|
model BookAiCallLog {
|
|||
|
|
id String @id @default(uuid())
|
|||
|
|
taskId String @map("task_id")
|
|||
|
|
courseId String? @map("course_id")
|
|||
|
|
chunkIndex Int? @map("chunk_index") // 切块时第几段,0-based;null=单次
|
|||
|
|
status String // success | failed
|
|||
|
|
promptPreview String @map("prompt_preview") @db.Text // 前 2000 字
|
|||
|
|
promptFull String @map("prompt_full") @db.Text // 完整,存时截断 500KB
|
|||
|
|
responsePreview String? @map("response_preview") @db.Text // 前 5000 字
|
|||
|
|
responseFull String? @map("response_full") @db.Text // 完整,存时截断 500KB
|
|||
|
|
errorMessage String? @map("error_message") @db.Text
|
|||
|
|
durationMs Int? @map("duration_ms")
|
|||
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|||
|
|
|
|||
|
|
@@index([taskId])
|
|||
|
|
@@index([createdAt])
|
|||
|
|
@@map("book_ai_call_logs")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ✅ V1.0 埋点体系:轻量级自建事件追踪
|
|||
|
|
model AnalyticsEvent {
|
|||
|
|
id BigInt @id @default(autoincrement())
|
|||
|
|
userId String? @map("user_id")
|
|||
|
|
deviceId String @map("device_id")
|
|||
|
|
sessionId String @map("session_id")
|
|||
|
|
eventName String @map("event_name")
|
|||
|
|
properties Json?
|
|||
|
|
appVersion String? @map("app_version")
|
|||
|
|
osVersion String? @map("os_version")
|
|||
|
|
deviceModel String? @map("device_model")
|
|||
|
|
networkType String? @map("network_type")
|
|||
|
|
clientTs DateTime @map("client_ts")
|
|||
|
|
serverTs DateTime @default(now()) @map("server_ts")
|
|||
|
|
|
|||
|
|
@@index([userId])
|
|||
|
|
@@index([eventName])
|
|||
|
|
@@index([clientTs])
|
|||
|
|
@@index([sessionId])
|
|||
|
|
@@map("analytics_events")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ✅ AI 相关模型已删除
|