310 lines
12 KiB
Plaintext
310 lines
12 KiB
Plaintext
// 叶雷讲数学 — 数据模型
|
||
//
|
||
// 约定:
|
||
// - 表名与字段名统一英文 snake_case,模型名用 PascalCase 并 @@map 到表名
|
||
// - 主键统一 id INT UNSIGNED AUTO_INCREMENT(本量级无需 BIGINT,且避免 JS BigInt 序列化问题)
|
||
// - 时间统一 DATETIME,存 UTC,展示层转 Asia/Shanghai
|
||
// - 金额统一 INT UNSIGNED,单位「分」,全程不换算
|
||
// - 状态字段统一 TINYINT + 代码内枚举(不使用数据库 ENUM,便于演进)
|
||
// - 不建外键约束,关系由应用层保证
|
||
//
|
||
// 数据库:MySQL 5.7
|
||
|
||
generator client {
|
||
provider = "prisma-client-js"
|
||
}
|
||
|
||
datasource db {
|
||
provider = "mysql"
|
||
url = env("DATABASE_URL")
|
||
}
|
||
|
||
/// 用户(对应旧表「用户信息」)
|
||
model User {
|
||
id Int @id @default(autoincrement()) @db.UnsignedInt
|
||
openid String @unique @db.VarChar(64)
|
||
nickname String? @db.VarChar(32)
|
||
headicon_id Int @default(0) @map("headicon_id") @db.UnsignedInt
|
||
/// 微信 sessionKey,AES-256-GCM 加密后存储;虚拟支付用户态签名必需
|
||
wx_session_key String? @map("wx_session_key") @db.VarChar(255)
|
||
session_key_updated_at DateTime? @map("session_key_updated_at")
|
||
/// 1 正常 / 0 封禁
|
||
status Int @default(1) @db.TinyInt
|
||
/// 1 = 用户主动使用过登录功能(即认可/接受这个小程序),0 = 未登录或已退出。
|
||
/// 和静默登录无关:静默登录只建立身份,不改这一位。购买前必须为 1。
|
||
is_login Int @default(0) @map("is_login") @db.TinyInt
|
||
registered_at DateTime @map("registered_at")
|
||
last_login_at DateTime? @map("last_login_at")
|
||
created_at DateTime @default(now()) @map("created_at")
|
||
updated_at DateTime @updatedAt @map("updated_at")
|
||
|
||
entitlements Entitlement[]
|
||
orders Order[]
|
||
|
||
@@map("users")
|
||
}
|
||
|
||
/// 付费课程(由旧表「课程口令码读取信息」升级而来)
|
||
model Course {
|
||
id Int @id @default(autoincrement()) @db.UnsignedInt
|
||
/// 旧 64 位 sha256 串,原值保留。
|
||
/// 同时是视频目录名 Course/{code}/... 与封面名 Course/{code}.png,绝对不可改写。
|
||
code String @unique @db.VarChar(64)
|
||
title String @db.VarChar(128)
|
||
intro String? @db.VarChar(255)
|
||
cover_image String? @map("cover_image") @db.VarChar(255)
|
||
price_fen Int @default(0) @map("price_fen") @db.UnsignedInt
|
||
/// 0 下架 / 1 上架
|
||
status Int @default(1) @db.TinyInt
|
||
sort_order Int @default(0) @map("sort_order")
|
||
created_at DateTime @default(now()) @map("created_at")
|
||
updated_at DateTime @updatedAt @map("updated_at")
|
||
|
||
lessons Lesson[]
|
||
entitlements Entitlement[]
|
||
orders Order[]
|
||
products Product[]
|
||
home_courses HomeCourse[]
|
||
|
||
@@map("courses")
|
||
}
|
||
|
||
/// 付费课时(对应旧表「视频课程数据」)
|
||
model Lesson {
|
||
id Int @id @default(autoincrement()) @db.UnsignedInt
|
||
course_id Int @map("course_id") @db.UnsignedInt
|
||
/// 原值保留,参与视频路径 Course/{code}/{chapter_id}/{play_id}/playlist.m3u8
|
||
play_id String @map("play_id") @db.VarChar(64)
|
||
chapter_id String @map("chapter_id") @db.VarChar(64)
|
||
chapter_title String? @map("chapter_title") @db.VarChar(255)
|
||
lesson_title String? @map("lesson_title") @db.VarChar(255)
|
||
duration String? @db.VarChar(16)
|
||
published_at DateTime? @map("published_at")
|
||
cover_image String? @map("cover_image") @db.VarChar(255)
|
||
sort_order Int @default(0) @map("sort_order")
|
||
created_at DateTime @default(now()) @map("created_at")
|
||
|
||
course Course @relation(fields: [course_id], references: [id])
|
||
|
||
@@unique([course_id, chapter_id, play_id])
|
||
@@index([course_id, sort_order])
|
||
@@map("lessons")
|
||
}
|
||
|
||
/// 免费试听课时(对应旧表「免费视频课程数据」)
|
||
model FreeLesson {
|
||
id Int @id @default(autoincrement()) @db.UnsignedInt
|
||
/// 旧「序号」字段,对应首页试听入口
|
||
series_no Int @map("series_no")
|
||
sort_order Int @default(0) @map("sort_order")
|
||
course_name String @map("course_name") @db.VarChar(255)
|
||
video_title String? @map("video_title") @db.VarChar(255)
|
||
intro String? @db.VarChar(255)
|
||
publisher String? @db.VarChar(255)
|
||
published_at DateTime? @map("published_at")
|
||
duration String? @db.VarChar(16)
|
||
/// 相对路径,如 FreeVideos/711/playlist.m3u8
|
||
video_m3u8 String? @map("video_m3u8") @db.VarChar(255)
|
||
cover_image String? @map("cover_image") @db.VarChar(255)
|
||
created_at DateTime @default(now()) @map("created_at")
|
||
|
||
@@index([series_no, sort_order])
|
||
@@map("free_lessons")
|
||
}
|
||
|
||
/// 首页「新课上线」长图页(对应旧表「首页课程列表」)
|
||
model HomeCourse {
|
||
id Int @id @default(autoincrement()) @db.UnsignedInt
|
||
/// 原「课程名称」原值保留:首页图片路径为 Images/NewCourse/{course_name}.png
|
||
course_name String @map("course_name") @db.VarChar(255)
|
||
intro String? @db.VarChar(255)
|
||
published_at DateTime? @map("published_at")
|
||
cover_image String? @map("cover_image") @db.VarChar(255)
|
||
/// 长图 HTML。注意:内容中含绝对 URL,迁移时需做域名改写。
|
||
content String? @db.LongText
|
||
/// 关联可购买课程,用于挂载购买入口(旧数据可能为空)
|
||
course_id Int? @map("course_id") @db.UnsignedInt
|
||
sort_order Int @default(0) @map("sort_order")
|
||
status Int @default(1) @db.TinyInt
|
||
created_at DateTime @default(now()) @map("created_at")
|
||
updated_at DateTime @updatedAt @map("updated_at")
|
||
|
||
course Course? @relation(fields: [course_id], references: [id])
|
||
|
||
@@index([status, sort_order])
|
||
@@map("home_courses")
|
||
}
|
||
|
||
/// 首页免费试听入口(对应旧表「首页免费课程列表」)
|
||
model HomeFreeCourse {
|
||
id Int @id @default(autoincrement()) @db.UnsignedInt
|
||
/// 旧「序号」,前端按此跳转 /v1/free-lessons?series_no=
|
||
series_no Int @map("series_no")
|
||
/// 原值保留:图片路径为 Images/FreeCourse/{course_name}.png
|
||
course_name String @map("course_name") @db.VarChar(255)
|
||
intro String? @db.VarChar(255)
|
||
published_at DateTime? @map("published_at")
|
||
cover_image String? @map("cover_image") @db.VarChar(255)
|
||
sort_order Int @default(0) @map("sort_order")
|
||
status Int @default(1) @db.TinyInt
|
||
|
||
@@index([status, sort_order])
|
||
@@map("home_free_courses")
|
||
}
|
||
|
||
/// 首页轮播图(对应旧表「首页横幅图片」)
|
||
model Banner {
|
||
id Int @id @default(autoincrement()) @db.UnsignedInt
|
||
/// 原「图片名」原值保留:图片路径为 Images/Banner/{filename}
|
||
filename String @db.VarChar(255)
|
||
sort_order Int @default(0) @map("sort_order")
|
||
status Int @default(1) @db.TinyInt
|
||
created_at DateTime @default(now()) @map("created_at")
|
||
updated_at DateTime @updatedAt @map("updated_at")
|
||
|
||
@@index([status, sort_order])
|
||
@@map("banners")
|
||
}
|
||
|
||
/// 知识速通文章(对应旧表「网页列表」)
|
||
model Article {
|
||
id Int @id @default(autoincrement()) @db.UnsignedInt
|
||
/// 旧「网页id」原值保留:首页缩略图路径为 WebPages/{legacy_page_id}.png
|
||
legacy_page_id String? @map("legacy_page_id") @db.VarChar(32)
|
||
title String @db.VarChar(255)
|
||
intro String? @db.VarChar(255)
|
||
publisher String? @db.VarChar(255)
|
||
published_at DateTime? @map("published_at")
|
||
/// 正文 HTML。注意:内容中含绝对 URL,迁移时需做域名改写。
|
||
content String? @db.LongText
|
||
sort_order Int @default(0) @map("sort_order")
|
||
status Int @default(1) @db.TinyInt
|
||
created_at DateTime @default(now()) @map("created_at")
|
||
updated_at DateTime @updatedAt @map("updated_at")
|
||
|
||
@@index([status, published_at])
|
||
@@index([legacy_page_id])
|
||
@@map("articles")
|
||
}
|
||
|
||
/// 课程页文案(对应旧表「课程页面Tips」)
|
||
model CourseTip {
|
||
id Int @id @default(autoincrement()) @db.UnsignedInt
|
||
tip_key Int @unique @map("tip_key")
|
||
content String? @db.Text
|
||
title String? @db.TinyText
|
||
|
||
@@map("course_tips")
|
||
}
|
||
|
||
/// 站点配置(对应旧表 cloudconfig,数字 ID 改为字符串 key)
|
||
model Setting {
|
||
id Int @id @default(autoincrement()) @db.UnsignedInt
|
||
key String @unique @db.VarChar(64)
|
||
value String? @db.VarChar(255)
|
||
remark String? @db.VarChar(255)
|
||
|
||
@@map("settings")
|
||
}
|
||
|
||
/// 权益 — 播放鉴权的唯一判断入口
|
||
model Entitlement {
|
||
id Int @id @default(autoincrement()) @db.UnsignedInt
|
||
user_id Int @map("user_id") @db.UnsignedInt
|
||
course_id Int @map("course_id") @db.UnsignedInt
|
||
/// 1 订单 / 2 老数据(历史已购) / 3 后台发放
|
||
source Int @db.TinyInt
|
||
ref_id Int? @map("ref_id") @db.UnsignedInt
|
||
granted_at DateTime @map("granted_at")
|
||
expire_at DateTime? @map("expire_at")
|
||
note String? @db.VarChar(255)
|
||
created_at DateTime @default(now()) @map("created_at")
|
||
|
||
user User @relation(fields: [user_id], references: [id])
|
||
course Course @relation(fields: [course_id], references: [id])
|
||
|
||
@@unique([user_id, course_id])
|
||
@@index([course_id])
|
||
@@map("entitlements")
|
||
}
|
||
|
||
/// 订单
|
||
model Order {
|
||
id Int @id @default(autoincrement()) @db.UnsignedInt
|
||
out_trade_no String @unique @map("out_trade_no") @db.VarChar(32)
|
||
user_id Int @map("user_id") @db.UnsignedInt
|
||
course_id Int @map("course_id") @db.UnsignedInt
|
||
product_id String @map("product_id") @db.VarChar(64)
|
||
quantity Int @default(1) @db.UnsignedInt
|
||
amount_fen Int @map("amount_fen") @db.UnsignedInt
|
||
/// 0 待支付 / 1 已支付 / 2 已发货 / 3 已退款 / 4 已关闭
|
||
status Int @db.TinyInt
|
||
/// 固定 0(正式环境)
|
||
env Int @default(0) @db.TinyInt
|
||
/// 微信平台单号,发货幂等依据
|
||
wx_order_id String? @unique @map("wx_order_id") @db.VarChar(64)
|
||
/// 下单时透传字段,发货通知原样带回
|
||
attach String? @db.VarChar(255)
|
||
created_at DateTime @default(now()) @map("created_at")
|
||
paid_at DateTime? @map("paid_at")
|
||
delivered_at DateTime? @map("delivered_at")
|
||
refunded_at DateTime? @map("refunded_at")
|
||
|
||
user User @relation(fields: [user_id], references: [id])
|
||
course Course @relation(fields: [course_id], references: [id])
|
||
|
||
@@index([user_id, created_at])
|
||
@@index([status, created_at])
|
||
@@map("orders")
|
||
}
|
||
|
||
/// 发货推送原始记录,用于幂等、重放与排查
|
||
model PaymentNotifyLog {
|
||
id Int @id @default(autoincrement()) @db.UnsignedInt
|
||
wx_order_id String? @map("wx_order_id") @db.VarChar(64)
|
||
out_trade_no String? @map("out_trade_no") @db.VarChar(32)
|
||
event String? @db.VarChar(64)
|
||
raw_body String? @map("raw_body") @db.MediumText
|
||
processed Int @default(0) @db.TinyInt
|
||
err_msg String? @map("err_msg") @db.VarChar(255)
|
||
created_at DateTime @default(now()) @map("created_at")
|
||
|
||
@@index([wx_order_id])
|
||
@@map("payment_notify_log")
|
||
}
|
||
|
||
/// 虚拟支付道具映射(course ↔ MP 后台道具)
|
||
model Product {
|
||
id Int @id @default(autoincrement()) @db.UnsignedInt
|
||
course_id Int @map("course_id") @db.UnsignedInt
|
||
wx_product_id String @unique @map("wx_product_id") @db.VarChar(64)
|
||
name String @db.VarChar(128)
|
||
/// 必须与 MP 后台道具价格一致,单位「分」
|
||
price_fen Int @map("price_fen") @db.UnsignedInt
|
||
status Int @default(1) @db.TinyInt
|
||
created_at DateTime @default(now()) @map("created_at")
|
||
updated_at DateTime @updatedAt @map("updated_at")
|
||
|
||
course Course @relation(fields: [course_id], references: [id])
|
||
|
||
@@map("products")
|
||
}
|
||
|
||
/// 管理员(与微信用户体系完全隔离)
|
||
model AdminUser {
|
||
id Int @id @default(autoincrement()) @db.UnsignedInt
|
||
username String @unique @db.VarChar(64)
|
||
password_hash String @map("password_hash") @db.VarChar(255)
|
||
display_name String? @map("display_name") @db.VarChar(64)
|
||
/// 1 超管 / 2 运营
|
||
role Int @default(2) @db.TinyInt
|
||
totp_secret String? @map("totp_secret") @db.VarChar(64)
|
||
status Int @default(1) @db.TinyInt
|
||
last_login_at DateTime? @map("last_login_at")
|
||
created_at DateTime @default(now()) @map("created_at")
|
||
updated_at DateTime @updatedAt @map("updated_at")
|
||
|
||
@@map("admin_users")
|
||
}
|
||
|