四端上传

This commit is contained in:
tianmo committed 2026-09-29 15:25:40 +08:00
1 parent 25bbb5f9c8
commit c6e2cb4d5b
276 files changed
+17812 -3

No files matched your search

@@ -0,0 +1,270 @@
-- CreateTable
CREATE TABLE `users` (
`id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
`openid` VARCHAR(64) NOT NULL,
`nickname` VARCHAR(32) NULL,
`headicon_id` INTEGER UNSIGNED NOT NULL DEFAULT 0,
`wx_session_key` VARCHAR(255) NULL,
`session_key_updated_at` DATETIME(3) NULL,
`status` TINYINT NOT NULL DEFAULT 1,
`registered_at` DATETIME(3) NOT NULL,
`last_login_at` DATETIME(3) NULL,
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updated_at` DATETIME(3) NOT NULL,
UNIQUE INDEX `users_openid_key`(`openid`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `courses` (
`id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
`code` VARCHAR(64) NOT NULL,
`title` VARCHAR(128) NOT NULL,
`intro` VARCHAR(255) NULL,
`cover_image` VARCHAR(255) NULL,
`price_fen` INTEGER UNSIGNED NOT NULL DEFAULT 0,
`status` TINYINT NOT NULL DEFAULT 1,
`sort_order` INTEGER NOT NULL DEFAULT 0,
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updated_at` DATETIME(3) NOT NULL,
UNIQUE INDEX `courses_code_key`(`code`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `lessons` (
`id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
`course_id` INTEGER UNSIGNED NOT NULL,
`play_id` VARCHAR(64) NOT NULL,
`chapter_id` VARCHAR(64) NOT NULL,
`chapter_title` VARCHAR(255) NULL,
`lesson_title` VARCHAR(255) NULL,
`duration` VARCHAR(16) NULL,
`published_at` DATETIME(3) NULL,
`cover_image` VARCHAR(255) NULL,
`sort_order` INTEGER NOT NULL DEFAULT 0,
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
INDEX `lessons_course_id_sort_order_idx`(`course_id`, `sort_order`),
UNIQUE INDEX `lessons_course_id_chapter_id_play_id_key`(`course_id`, `chapter_id`, `play_id`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `free_lessons` (
`id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
`series_no` INTEGER NOT NULL,
`sort_order` INTEGER NOT NULL DEFAULT 0,
`course_name` VARCHAR(255) NOT NULL,
`video_title` VARCHAR(255) NULL,
`intro` VARCHAR(255) NULL,
`publisher` VARCHAR(255) NULL,
`published_at` DATETIME(3) NULL,
`duration` VARCHAR(16) NULL,
`video_m3u8` VARCHAR(255) NULL,
`cover_image` VARCHAR(255) NULL,
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
INDEX `free_lessons_series_no_sort_order_idx`(`series_no`, `sort_order`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `home_courses` (
`id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
`course_name` VARCHAR(255) NOT NULL,
`intro` VARCHAR(255) NULL,
`published_at` DATETIME(3) NULL,
`cover_image` VARCHAR(255) NULL,
`content` LONGTEXT NULL,
`course_id` INTEGER UNSIGNED NULL,
`sort_order` INTEGER NOT NULL DEFAULT 0,
`status` TINYINT NOT NULL DEFAULT 1,
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updated_at` DATETIME(3) NOT NULL,
INDEX `home_courses_status_sort_order_idx`(`status`, `sort_order`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `home_free_courses` (
`id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
`series_no` INTEGER NOT NULL,
`course_name` VARCHAR(255) NOT NULL,
`intro` VARCHAR(255) NULL,
`published_at` DATETIME(3) NULL,
`cover_image` VARCHAR(255) NULL,
`sort_order` INTEGER NOT NULL DEFAULT 0,
`status` TINYINT NOT NULL DEFAULT 1,
INDEX `home_free_courses_status_sort_order_idx`(`status`, `sort_order`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `banners` (
`id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
`filename` VARCHAR(255) NOT NULL,
`sort_order` INTEGER NOT NULL DEFAULT 0,
`status` TINYINT NOT NULL DEFAULT 1,
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updated_at` DATETIME(3) NOT NULL,
INDEX `banners_status_sort_order_idx`(`status`, `sort_order`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `articles` (
`id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
`legacy_page_id` VARCHAR(32) NULL,
`title` VARCHAR(255) NOT NULL,
`intro` VARCHAR(255) NULL,
`publisher` VARCHAR(255) NULL,
`published_at` DATETIME(3) NULL,
`content` LONGTEXT NULL,
`sort_order` INTEGER NOT NULL DEFAULT 0,
`status` TINYINT NOT NULL DEFAULT 1,
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updated_at` DATETIME(3) NOT NULL,
INDEX `articles_status_published_at_idx`(`status`, `published_at`),
INDEX `articles_legacy_page_id_idx`(`legacy_page_id`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `course_tips` (
`id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
`tip_key` INTEGER NOT NULL,
`content` TEXT NULL,
`title` TINYTEXT NULL,
UNIQUE INDEX `course_tips_tip_key_key`(`tip_key`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `settings` (
`id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
`key` VARCHAR(64) NOT NULL,
`value` VARCHAR(255) NULL,
`remark` VARCHAR(255) NULL,
UNIQUE INDEX `settings_key_key`(`key`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `entitlements` (
`id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
`user_id` INTEGER UNSIGNED NOT NULL,
`course_id` INTEGER UNSIGNED NOT NULL,
`source` TINYINT NOT NULL,
`ref_id` INTEGER UNSIGNED NULL,
`granted_at` DATETIME(3) NOT NULL,
`expire_at` DATETIME(3) NULL,
`note` VARCHAR(255) NULL,
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
INDEX `entitlements_course_id_idx`(`course_id`),
UNIQUE INDEX `entitlements_user_id_course_id_key`(`user_id`, `course_id`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `orders` (
`id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
`out_trade_no` VARCHAR(32) NOT NULL,
`user_id` INTEGER UNSIGNED NOT NULL,
`course_id` INTEGER UNSIGNED NOT NULL,
`product_id` VARCHAR(64) NOT NULL,
`quantity` INTEGER UNSIGNED NOT NULL DEFAULT 1,
`amount_fen` INTEGER UNSIGNED NOT NULL,
`status` TINYINT NOT NULL,
`env` TINYINT NOT NULL DEFAULT 0,
`wx_order_id` VARCHAR(64) NULL,
`attach` VARCHAR(255) NULL,
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`paid_at` DATETIME(3) NULL,
`delivered_at` DATETIME(3) NULL,
`refunded_at` DATETIME(3) NULL,
UNIQUE INDEX `orders_out_trade_no_key`(`out_trade_no`),
UNIQUE INDEX `orders_wx_order_id_key`(`wx_order_id`),
INDEX `orders_user_id_created_at_idx`(`user_id`, `created_at`),
INDEX `orders_status_created_at_idx`(`status`, `created_at`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `payment_notify_log` (
`id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
`wx_order_id` VARCHAR(64) NULL,
`out_trade_no` VARCHAR(32) NULL,
`event` VARCHAR(64) NULL,
`raw_body` MEDIUMTEXT NULL,
`processed` TINYINT NOT NULL DEFAULT 0,
`err_msg` VARCHAR(255) NULL,
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
INDEX `payment_notify_log_wx_order_id_idx`(`wx_order_id`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `products` (
`id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
`course_id` INTEGER UNSIGNED NOT NULL,
`wx_product_id` VARCHAR(64) NOT NULL,
`name` VARCHAR(128) NOT NULL,
`price_fen` INTEGER UNSIGNED NOT NULL,
`status` TINYINT NOT NULL DEFAULT 1,
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updated_at` DATETIME(3) NOT NULL,
UNIQUE INDEX `products_wx_product_id_key`(`wx_product_id`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `admin_users` (
`id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
`username` VARCHAR(64) NOT NULL,
`password_hash` VARCHAR(255) NOT NULL,
`display_name` VARCHAR(64) NULL,
`role` TINYINT NOT NULL DEFAULT 2,
`totp_secret` VARCHAR(64) NULL,
`status` TINYINT NOT NULL DEFAULT 1,
`last_login_at` DATETIME(3) NULL,
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updated_at` DATETIME(3) NOT NULL,
UNIQUE INDEX `admin_users_username_key`(`username`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- AddForeignKey
ALTER TABLE `lessons` ADD CONSTRAINT `lessons_course_id_fkey` FOREIGN KEY (`course_id`) REFERENCES `courses`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `home_courses` ADD CONSTRAINT `home_courses_course_id_fkey` FOREIGN KEY (`course_id`) REFERENCES `courses`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `entitlements` ADD CONSTRAINT `entitlements_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `entitlements` ADD CONSTRAINT `entitlements_course_id_fkey` FOREIGN KEY (`course_id`) REFERENCES `courses`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `orders` ADD CONSTRAINT `orders_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `orders` ADD CONSTRAINT `orders_course_id_fkey` FOREIGN KEY (`course_id`) REFERENCES `courses`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `products` ADD CONSTRAINT `products_course_id_fkey` FOREIGN KEY (`course_id`) REFERENCES `courses`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -0,0 +1 @@
provider = "mysql"
+306
View File
@@ -0,0 +1,306 @@
// 叶雷讲数学 — 数据模型
//
// 约定:
// - 表名与字段名统一英文 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
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")
}