Files
YelayMaths/yelaymaths_api/prisma/schema.prisma
T
2026-09-30 03:38:10 +08:00

310 lines
12 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 叶雷讲数学 — 数据模型
//
// 约定:
// - 表名与字段名统一英文 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")
}