850 lines
28 KiB
Markdown
850 lines
28 KiB
Markdown
|
|
# 数据模型重构设计
|
|||
|
|
|
|||
|
|
> 创建时间:2026-02-27
|
|||
|
|
> 基于需求:17-课程包套餐重构需求.md
|
|||
|
|
> 状态:设计中
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 一、设计原则
|
|||
|
|
|
|||
|
|
### 1.1 重构原则
|
|||
|
|
|
|||
|
|
| 原则 | 说明 |
|
|||
|
|
|-----|------|
|
|||
|
|
| **向后兼容** | 新结构与旧结构可以共存,支持渐进式迁移 |
|
|||
|
|
| **数据安全** | 迁移过程中不丢失任何现有数据 |
|
|||
|
|
| **最小变更** | 保留可复用的表和字段,减少不必要的重构 |
|
|||
|
|
| **可扩展性** | 新结构支持后续功能迭代 |
|
|||
|
|
|
|||
|
|
### 1.2 参考最佳实践
|
|||
|
|
|
|||
|
|
基于以下资源的最佳实践:
|
|||
|
|
- [Prisma Schema Design Best Practices](https://blog.csdn.net/gitblog_00740/article/details/151825815) - LMS系统数据模型设计
|
|||
|
|
- [Microsoft Azure - SaaS Tenancy Patterns](https://learn.microsoft.com/zh-cn/azure/azure-sql/database/saas-tenancy-app-design-patterns) - 多租户SaaS架构模式
|
|||
|
|
- [Prisma Migration Strategy](https://www.prisma.io/docs/concepts/components/prisma-migrate) - 生产环境迁移策略
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 二、表结构变更概览
|
|||
|
|
|
|||
|
|
### 2.1 变更清单
|
|||
|
|
|
|||
|
|
| 变更类型 | 表名 | 说明 |
|
|||
|
|
|---------|------|------|
|
|||
|
|
| **🆕 新增** | CoursePackage | 课程套餐 |
|
|||
|
|
| **🆕 新增** | CoursePackageCourse | 套餐-课程包关联 |
|
|||
|
|
| **🆕 新增** | TenantPackage | 租户-套餐授权 |
|
|||
|
|
| **🆕 新增** | Theme | 主题字典 |
|
|||
|
|
| **🆕 新增** | CourseLesson | 课程(导入课/集体课/领域课) |
|
|||
|
|
| **🆕 新增** | LessonStep | 教学环节 |
|
|||
|
|
| **🆕 新增** | LessonStepResource | 环节资源关联 |
|
|||
|
|
| **🆕 新增** | SchoolCourse | 校本课程包 |
|
|||
|
|
| **🔄 重构** | Course | 课程包(字段大幅调整) |
|
|||
|
|
| **🔄 调整** | Tenant | 新增套餐相关字段 |
|
|||
|
|
| **❌ 废弃** | CourseScript | 被 CourseLesson + LessonStep 替代 |
|
|||
|
|
| **❌ 废弃** | CourseScriptPage | 被 LessonStep 替代 |
|
|||
|
|
| **❌ 废弃** | CourseActivity | 并入 CourseLesson |
|
|||
|
|
| **⚠️ 保留** | TenantCourse | 保留,作为套餐授权的补充 |
|
|||
|
|
|
|||
|
|
### 2.2 表关系图
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|||
|
|
│ 核心关系图 │
|
|||
|
|
├─────────────────────────────────────────────────────────────────────────────┤
|
|||
|
|
│ │
|
|||
|
|
│ ┌──────────────┐ │
|
|||
|
|
│ │ CoursePackage│ ◄─────────────────────────────────────────────┐ │
|
|||
|
|
│ │ (课程套餐) │ │ │
|
|||
|
|
│ └───────┬──────┘ │ │
|
|||
|
|
│ │ 1:N │ │
|
|||
|
|
│ ▼ │ │
|
|||
|
|
│ ┌──────────────────┐ N:1 ┌────────────┐ │ │
|
|||
|
|
│ │CoursePackageCourse├─────────────►│ Course │ │ │
|
|||
|
|
│ │ (套餐-课程关联) │ │ (课程包) │ │ │
|
|||
|
|
│ └──────────────────┘ └──────┬─────┘ │ │
|
|||
|
|
│ │ 1:N │ │
|
|||
|
|
│ ▼ │ │
|
|||
|
|
│ ┌──────────────┐ ┌────────────┐ │ │
|
|||
|
|
│ │TenantPackage │ │CourseLesson│ │ │
|
|||
|
|
│ │(租户-套餐授权)├─────────────────►│ (课程) │ │ │
|
|||
|
|
│ └──────────────┘ └──────┬─────┘ │ │
|
|||
|
|
│ │ │ 1:N │ │
|
|||
|
|
│ │ N:1 ▼ │ │
|
|||
|
|
│ ▼ ┌────────────┐ │ │
|
|||
|
|
│ ┌──────────────┐ │ LessonStep │ │ │
|
|||
|
|
│ │ Tenant │ │ (教学环节) │ │ │
|
|||
|
|
│ │ (租户) │ └──────┬─────┘ │ │
|
|||
|
|
│ └──────────────┘ │ N:N │ │
|
|||
|
|
│ ▼ │ │
|
|||
|
|
│ ┌────────────────┐ │ │
|
|||
|
|
│ │LessonStepResource│ │ │
|
|||
|
|
│ │ (环节资源关联) │ │ │
|
|||
|
|
│ └────────────────┘ │ │
|
|||
|
|
│ │ │
|
|||
|
|
│ ┌──────────────┐ │ │
|
|||
|
|
│ │SchoolCourse │────────────────────────────────────────┘ │
|
|||
|
|
│ │ (校本课程包) │ 基于Course创建 │
|
|||
|
|
│ └──────────────┘ │
|
|||
|
|
│ │
|
|||
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 三、新增表设计
|
|||
|
|
|
|||
|
|
### 3.1 CoursePackage(课程套餐)
|
|||
|
|
|
|||
|
|
```prisma
|
|||
|
|
// ==================== 课程套餐 ====================
|
|||
|
|
|
|||
|
|
model CoursePackage {
|
|||
|
|
id Int @id @default(autoincrement())
|
|||
|
|
|
|||
|
|
// 基本信息
|
|||
|
|
name String
|
|||
|
|
description String? @db.Text
|
|||
|
|
|
|||
|
|
// 定价
|
|||
|
|
price Int @default(0) // 原价(分)
|
|||
|
|
discountPrice Int? @map("discount_price") // 折扣价(分)
|
|||
|
|
discountType String? @map("discount_type") // 折扣类型:EARLY_BIRD, RENEWAL, GROUP
|
|||
|
|
|
|||
|
|
// 适用年级(多选)
|
|||
|
|
gradeLevels String @default("[]") @map("grade_levels") // JSON: ["XIAO_BAN", "ZHONG_BAN"]
|
|||
|
|
|
|||
|
|
// 状态
|
|||
|
|
status String @default("DRAFT") // DRAFT, PENDING, PUBLISHED, ARCHIVED
|
|||
|
|
|
|||
|
|
// 审核相关
|
|||
|
|
submittedAt DateTime? @map("submitted_at")
|
|||
|
|
submittedBy Int? @map("submitted_by")
|
|||
|
|
reviewedAt DateTime? @map("reviewed_at")
|
|||
|
|
reviewedBy Int? @map("reviewed_by")
|
|||
|
|
reviewComment String? @map("review_comment") @db.Text
|
|||
|
|
|
|||
|
|
// 统计
|
|||
|
|
courseCount Int @default(0) @map("course_count")
|
|||
|
|
tenantCount Int @default(0) @map("tenant_count")
|
|||
|
|
|
|||
|
|
// 创建者
|
|||
|
|
createdBy Int? @map("created_by")
|
|||
|
|
|
|||
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|||
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|||
|
|
publishedAt DateTime? @map("published_at")
|
|||
|
|
|
|||
|
|
// 关联
|
|||
|
|
courses CoursePackageCourse[]
|
|||
|
|
tenantPackages TenantPackage[]
|
|||
|
|
|
|||
|
|
@@index([status])
|
|||
|
|
@@map("course_packages")
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.2 CoursePackageCourse(套餐-课程包关联)
|
|||
|
|
|
|||
|
|
```prisma
|
|||
|
|
// ==================== 套餐-课程包关联 ====================
|
|||
|
|
|
|||
|
|
model CoursePackageCourse {
|
|||
|
|
id Int @id @default(autoincrement())
|
|||
|
|
|
|||
|
|
packageId Int @map("package_id")
|
|||
|
|
courseId Int @map("course_id")
|
|||
|
|
|
|||
|
|
// 年级分组
|
|||
|
|
gradeLevel String @map("grade_level") // XIAO_BAN, ZHONG_BAN, DA_BAN
|
|||
|
|
|
|||
|
|
// 排序
|
|||
|
|
sortOrder Int @default(0) @map("sort_order")
|
|||
|
|
|
|||
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|||
|
|
|
|||
|
|
// 关联
|
|||
|
|
package CoursePackage @relation(fields: [packageId], references: [id], onDelete: Cascade)
|
|||
|
|
course Course @relation(fields: [courseId], references: [id], onDelete: Cascade)
|
|||
|
|
|
|||
|
|
@@unique([packageId, courseId])
|
|||
|
|
@@index([packageId, gradeLevel])
|
|||
|
|
@@map("course_package_courses")
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.3 TenantPackage(租户-套餐授权)
|
|||
|
|
|
|||
|
|
```prisma
|
|||
|
|
// ==================== 租户-套餐授权 ====================
|
|||
|
|
|
|||
|
|
model TenantPackage {
|
|||
|
|
id Int @id @default(autoincrement())
|
|||
|
|
|
|||
|
|
tenantId Int @map("tenant_id")
|
|||
|
|
packageId Int @map("package_id")
|
|||
|
|
|
|||
|
|
// 授权时间
|
|||
|
|
startDate DateTime @map("start_date")
|
|||
|
|
endDate DateTime @map("end_date")
|
|||
|
|
|
|||
|
|
// 授权状态
|
|||
|
|
status String @default("ACTIVE") // ACTIVE, EXPIRED, CANCELLED
|
|||
|
|
|
|||
|
|
// 价格记录(授权时的价格)
|
|||
|
|
pricePaid Int @default(0) @map("price_paid") // 实际支付价格(分)
|
|||
|
|
|
|||
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|||
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|||
|
|
|
|||
|
|
// 关联
|
|||
|
|
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
|||
|
|
package CoursePackage @relation(fields: [packageId], references: [id], onDelete: Cascade)
|
|||
|
|
|
|||
|
|
@@unique([tenantId, packageId])
|
|||
|
|
@@index([tenantId, status])
|
|||
|
|
@@index([endDate])
|
|||
|
|
@@map("tenant_packages")
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.4 Theme(主题字典)
|
|||
|
|
|
|||
|
|
```prisma
|
|||
|
|
// ==================== 主题字典 ====================
|
|||
|
|
|
|||
|
|
model Theme {
|
|||
|
|
id Int @id @default(autoincrement())
|
|||
|
|
|
|||
|
|
name String @unique
|
|||
|
|
description String? @db.Text
|
|||
|
|
|
|||
|
|
// 排序权重
|
|||
|
|
sortOrder Int @default(0) @map("sort_order")
|
|||
|
|
|
|||
|
|
// 状态
|
|||
|
|
status String @default("ACTIVE") // ACTIVE, ARCHIVED
|
|||
|
|
|
|||
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|||
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|||
|
|
|
|||
|
|
// 关联
|
|||
|
|
courses Course[]
|
|||
|
|
|
|||
|
|
@@index([sortOrder])
|
|||
|
|
@@map("themes")
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.5 CourseLesson(课程)
|
|||
|
|
|
|||
|
|
```prisma
|
|||
|
|
// ==================== 课程(导入课/集体课/领域课) ====================
|
|||
|
|
|
|||
|
|
model CourseLesson {
|
|||
|
|
id Int @id @default(autoincrement())
|
|||
|
|
|
|||
|
|
courseId Int @map("course_id")
|
|||
|
|
|
|||
|
|
// 课程类型
|
|||
|
|
lessonType String @map("lesson_type") // INTRODUCTION(导入课), COLLECTIVE(集体课),
|
|||
|
|
// LANGUAGE(语言), HEALTH(健康), SCIENCE(科学),
|
|||
|
|
// SOCIAL(社会), ART(艺术)
|
|||
|
|
|
|||
|
|
// 基本信息
|
|||
|
|
name String
|
|||
|
|
description String? @db.Text
|
|||
|
|
|
|||
|
|
// 时长(分钟)
|
|||
|
|
duration Int @default(25)
|
|||
|
|
|
|||
|
|
// 核心资源(仅集体课和领域课)
|
|||
|
|
videoPath String? @map("video_path") // 动画视频
|
|||
|
|
videoName String? @map("video_name")
|
|||
|
|
pptPath String? @map("ppt_path") // 课件PPT
|
|||
|
|
pptName String? @map("ppt_name")
|
|||
|
|
pdfPath String? @map("pdf_path") // 电子绘本PDF
|
|||
|
|
pdfName String? @map("pdf_name")
|
|||
|
|
|
|||
|
|
// 教学目标(富文本)
|
|||
|
|
objectives String? @db.Text
|
|||
|
|
|
|||
|
|
// 教学准备(富文本)
|
|||
|
|
preparation String? @db.Text
|
|||
|
|
|
|||
|
|
// 教学延伸(富文本,集体课和领域课有)
|
|||
|
|
extension String? @db.Text
|
|||
|
|
|
|||
|
|
// 教学反思(富文本)
|
|||
|
|
reflection String? @db.Text
|
|||
|
|
|
|||
|
|
// 测评工具
|
|||
|
|
assessmentData String? @map("assessment_data") @db.Text // JSON存储
|
|||
|
|
|
|||
|
|
// 是否使用4环节模板(仅集体课)
|
|||
|
|
useTemplate Boolean @default(false) @map("use_template")
|
|||
|
|
|
|||
|
|
// 排序
|
|||
|
|
sortOrder Int @default(0) @map("sort_order")
|
|||
|
|
|
|||
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|||
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|||
|
|
|
|||
|
|
// 关联
|
|||
|
|
course Course @relation(fields: [courseId], references: [id], onDelete: Cascade)
|
|||
|
|
steps LessonStep[]
|
|||
|
|
|
|||
|
|
@@unique([courseId, lessonType])
|
|||
|
|
@@index([courseId])
|
|||
|
|
@@map("course_lessons")
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.6 LessonStep(教学环节)
|
|||
|
|
|
|||
|
|
```prisma
|
|||
|
|
// ==================== 教学环节 ====================
|
|||
|
|
|
|||
|
|
model LessonStep {
|
|||
|
|
id Int @id @default(autoincrement())
|
|||
|
|
|
|||
|
|
lessonId Int @map("lesson_id")
|
|||
|
|
|
|||
|
|
// 环节基本信息
|
|||
|
|
name String
|
|||
|
|
content String @db.Text // 环节内容(富文本)
|
|||
|
|
|
|||
|
|
// 时长(分钟)
|
|||
|
|
duration Int @default(5)
|
|||
|
|
|
|||
|
|
// 教学目的(富文本)
|
|||
|
|
objective String? @db.Text
|
|||
|
|
|
|||
|
|
// 关联资源ID列表
|
|||
|
|
resourceIds String? @map("resource_ids") // JSON: [1, 2, 3]
|
|||
|
|
|
|||
|
|
// 排序
|
|||
|
|
sortOrder Int @default(0) @map("sort_order")
|
|||
|
|
|
|||
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|||
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|||
|
|
|
|||
|
|
// 关联
|
|||
|
|
lesson CourseLesson @relation(fields: [lessonId], references: [id], onDelete: Cascade)
|
|||
|
|
stepResources LessonStepResource[]
|
|||
|
|
|
|||
|
|
@@index([lessonId])
|
|||
|
|
@@map("lesson_steps")
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.7 LessonStepResource(环节资源关联)
|
|||
|
|
|
|||
|
|
```prisma
|
|||
|
|
// ==================== 环节资源关联 ====================
|
|||
|
|
|
|||
|
|
model LessonStepResource {
|
|||
|
|
id Int @id @default(autoincrement())
|
|||
|
|
|
|||
|
|
stepId Int @map("step_id")
|
|||
|
|
resourceId Int @map("resource_id")
|
|||
|
|
|
|||
|
|
sortOrder Int @default(0) @map("sort_order")
|
|||
|
|
|
|||
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|||
|
|
|
|||
|
|
// 关联
|
|||
|
|
step LessonStep @relation(fields: [stepId], references: [id], onDelete: Cascade)
|
|||
|
|
resource CourseResource @relation(fields: [resourceId], references: [id], onDelete: Cascade)
|
|||
|
|
|
|||
|
|
@@unique([stepId, resourceId])
|
|||
|
|
@@map("lesson_step_resources")
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.8 SchoolCourse(校本课程包)
|
|||
|
|
|
|||
|
|
```prisma
|
|||
|
|
// ==================== 校本课程包 ====================
|
|||
|
|
|
|||
|
|
model SchoolCourse {
|
|||
|
|
id Int @id @default(autoincrement())
|
|||
|
|
|
|||
|
|
// 所属学校
|
|||
|
|
tenantId Int @map("tenant_id")
|
|||
|
|
|
|||
|
|
// 基于的课程包
|
|||
|
|
sourceCourseId Int @map("source_course_id")
|
|||
|
|
|
|||
|
|
// 基本信息
|
|||
|
|
name String
|
|||
|
|
description String? @db.Text
|
|||
|
|
|
|||
|
|
// 创建者(教师)
|
|||
|
|
createdBy Int @map("created_by")
|
|||
|
|
|
|||
|
|
// 调整内容摘要
|
|||
|
|
changesSummary String? @map("changes_summary") @db.Text
|
|||
|
|
|
|||
|
|
// 调整详情(JSON)
|
|||
|
|
changesData String? @map("changes_data") @db.Text
|
|||
|
|
|
|||
|
|
// 使用统计
|
|||
|
|
usageCount Int @default(0) @map("usage_count")
|
|||
|
|
|
|||
|
|
// 状态
|
|||
|
|
status String @default("ACTIVE") // ACTIVE, ARCHIVED
|
|||
|
|
|
|||
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|||
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|||
|
|
|
|||
|
|
// 关联
|
|||
|
|
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
|||
|
|
sourceCourse Course @relation(fields: [sourceCourseId], references: [id])
|
|||
|
|
lessons SchoolCourseLesson[]
|
|||
|
|
reservations SchoolCourseReservation[]
|
|||
|
|
|
|||
|
|
@@index([tenantId])
|
|||
|
|
@@index([sourceCourseId])
|
|||
|
|
@@map("school_courses")
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.9 SchoolCourseLesson(校本课程)
|
|||
|
|
|
|||
|
|
```prisma
|
|||
|
|
// ==================== 校本课程 ====================
|
|||
|
|
|
|||
|
|
model SchoolCourseLesson {
|
|||
|
|
id Int @id @default(autoincrement())
|
|||
|
|
|
|||
|
|
schoolCourseId Int @map("school_course_id")
|
|||
|
|
sourceLessonId Int @map("source_lesson_id") // 基于的原始课程
|
|||
|
|
|
|||
|
|
// 调整类型
|
|||
|
|
lessonType String @map("lesson_type")
|
|||
|
|
|
|||
|
|
// 调整后的内容
|
|||
|
|
objectives String? @db.Text
|
|||
|
|
preparation String? @db.Text
|
|||
|
|
extension String? @db.Text
|
|||
|
|
reflection String? @db.Text
|
|||
|
|
|
|||
|
|
// 调整说明
|
|||
|
|
changeNote String? @map("change_note") @db.Text
|
|||
|
|
|
|||
|
|
// 环节数据(JSON,调整后的)
|
|||
|
|
stepsData String? @map("steps_data") @db.Text
|
|||
|
|
|
|||
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|||
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|||
|
|
|
|||
|
|
// 关联
|
|||
|
|
schoolCourse SchoolCourse @relation(fields: [schoolCourseId], references: [id], onDelete: Cascade)
|
|||
|
|
|
|||
|
|
@@unique([schoolCourseId, lessonType])
|
|||
|
|
@@map("school_course_lessons")
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.10 SchoolCourseReservation(校本课程预约)
|
|||
|
|
|
|||
|
|
```prisma
|
|||
|
|
// ==================== 校本课程预约 ====================
|
|||
|
|
|
|||
|
|
model SchoolCourseReservation {
|
|||
|
|
id Int @id @default(autoincrement())
|
|||
|
|
|
|||
|
|
schoolCourseId Int @map("school_course_id")
|
|||
|
|
|
|||
|
|
// 预约信息
|
|||
|
|
teacherId Int @map("teacher_id")
|
|||
|
|
classId Int @map("class_id")
|
|||
|
|
|
|||
|
|
// 时间
|
|||
|
|
scheduledDate DateTime @map("scheduled_date")
|
|||
|
|
scheduledTime String? @map("scheduled_time") // "10:00-10:30"
|
|||
|
|
|
|||
|
|
// 状态
|
|||
|
|
status String @default("PENDING") // PENDING, CONFIRMED, COMPLETED, CANCELLED
|
|||
|
|
|
|||
|
|
// 备注
|
|||
|
|
note String? @db.Text
|
|||
|
|
|
|||
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|||
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|||
|
|
|
|||
|
|
// 关联
|
|||
|
|
schoolCourse SchoolCourse @relation(fields: [schoolCourseId], references: [id], onDelete: Cascade)
|
|||
|
|
|
|||
|
|
@@index([schoolCourseId, scheduledDate])
|
|||
|
|
@@index([teacherId, scheduledDate])
|
|||
|
|
@@map("school_course_reservations")
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 四、重构表设计
|
|||
|
|
|
|||
|
|
### 4.1 Course(课程包)- 重构
|
|||
|
|
|
|||
|
|
```prisma
|
|||
|
|
// ==================== 课程包(重构) ====================
|
|||
|
|
|
|||
|
|
model Course {
|
|||
|
|
id Int @id @default(autoincrement())
|
|||
|
|
|
|||
|
|
// ========== 新增字段 ==========
|
|||
|
|
|
|||
|
|
// 主题(关联主题字典)
|
|||
|
|
themeId Int? @map("theme_id")
|
|||
|
|
|
|||
|
|
// 核心内容
|
|||
|
|
coreContent String? @map("core_content") @db.Text
|
|||
|
|
|
|||
|
|
// 课程介绍(8个富文本字段)
|
|||
|
|
introSummary String? @map("intro_summary") @db.Text // 课程简介
|
|||
|
|
introHighlights String? @map("intro_highlights") @db.Text // 课程亮点
|
|||
|
|
introGoals String? @map("intro_goals") @db.Text // 课程总目标
|
|||
|
|
introSchedule String? @map("intro_schedule") @db.Text // 课程内容安排
|
|||
|
|
introKeyPoints String? @map("intro_key_points") @db.Text // 教学重难点
|
|||
|
|
introMethods String? @map("intro_methods") @db.Text // 教学方法
|
|||
|
|
introEvaluation String? @map("intro_evaluation") @db.Text // 评价方式
|
|||
|
|
introNotes String? @map("intro_notes") @db.Text // 注意事项
|
|||
|
|
|
|||
|
|
// 排课计划参考(表格数据)
|
|||
|
|
scheduleRefData String? @map("schedule_ref_data") @db.Text // JSON数组
|
|||
|
|
|
|||
|
|
// 是否有集体课(用于判断是否配置了核心课程)
|
|||
|
|
hasCollectiveLesson Boolean @default(false) @map("has_collective_lesson")
|
|||
|
|
|
|||
|
|
// ========== 保留字段 ==========
|
|||
|
|
|
|||
|
|
name String
|
|||
|
|
description String?
|
|||
|
|
|
|||
|
|
pictureBookId Int? @map("picture_book_id")
|
|||
|
|
pictureBookName String? @map("picture_book_name")
|
|||
|
|
coverImagePath String? @map("cover_image_path")
|
|||
|
|
|
|||
|
|
// 兼容旧版:数字资源(后续迁移到CourseLesson)
|
|||
|
|
ebookPaths String? @map("ebook_paths")
|
|||
|
|
audioPaths String? @map("audio_paths")
|
|||
|
|
videoPaths String? @map("video_paths")
|
|||
|
|
otherResources String? @map("other_resources")
|
|||
|
|
|
|||
|
|
// 兼容旧版:教学材料(后续迁移到CourseLesson)
|
|||
|
|
pptPath String? @map("ppt_path")
|
|||
|
|
pptName String? @map("ppt_name")
|
|||
|
|
posterPaths String? @map("poster_paths")
|
|||
|
|
tools String? @map("tools")
|
|||
|
|
studentMaterials String? @map("student_materials")
|
|||
|
|
|
|||
|
|
// 兼容旧版:课堂计划(后续迁移到CourseLesson+LessonStep)
|
|||
|
|
lessonPlanData String? @map("lesson_plan_data")
|
|||
|
|
|
|||
|
|
// 兼容旧版:延伸活动(后续迁移到领域课)
|
|||
|
|
activitiesData String? @map("activities_data")
|
|||
|
|
|
|||
|
|
// 兼容旧版:测评工具(后续迁移到CourseLesson)
|
|||
|
|
assessmentData String? @map("assessment_data")
|
|||
|
|
|
|||
|
|
gradeTags String @default("[]") @map("grade_tags")
|
|||
|
|
domainTags String @default("[]") @map("domain_tags")
|
|||
|
|
|
|||
|
|
duration Int @default(25)
|
|||
|
|
|
|||
|
|
status String @default("DRAFT")
|
|||
|
|
version String @default("1.0")
|
|||
|
|
|
|||
|
|
submittedAt DateTime? @map("submitted_at")
|
|||
|
|
submittedBy Int? @map("submitted_by")
|
|||
|
|
reviewedAt DateTime? @map("reviewed_at")
|
|||
|
|
reviewedBy Int? @map("reviewed_by")
|
|||
|
|
reviewComment String? @map("review_comment")
|
|||
|
|
reviewChecklist String? @map("review_checklist")
|
|||
|
|
|
|||
|
|
parentId Int? @map("parent_id")
|
|||
|
|
isLatest Boolean @default(true)
|
|||
|
|
|
|||
|
|
usageCount Int @default(0) @map("usage_count")
|
|||
|
|
teacherCount Int @default(0) @map("teacher_count")
|
|||
|
|
avgRating Float @default(0) @map("avg_rating")
|
|||
|
|
|
|||
|
|
createdBy Int? @map("created_by")
|
|||
|
|
|
|||
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|||
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|||
|
|
publishedAt DateTime? @map("published_at")
|
|||
|
|
|
|||
|
|
// ========== 关联 ==========
|
|||
|
|
|
|||
|
|
theme Theme? @relation(fields: [themeId], references: [id])
|
|||
|
|
|
|||
|
|
// 新增关联
|
|||
|
|
packageCourses CoursePackageCourse[]
|
|||
|
|
lessons CourseLesson[]
|
|||
|
|
schoolCourses SchoolCourse[]
|
|||
|
|
|
|||
|
|
// 保留关联
|
|||
|
|
resources CourseResource[]
|
|||
|
|
scripts CourseScript[] // 废弃但保留
|
|||
|
|
activities CourseActivity[] // 废弃但保留
|
|||
|
|
tenantCourses TenantCourse[]
|
|||
|
|
versions CourseVersion[]
|
|||
|
|
lessons_rel Lesson[] // 授课记录
|
|||
|
|
tasks Task[]
|
|||
|
|
taskTemplates TaskTemplate[]
|
|||
|
|
schedulePlans SchedulePlan[]
|
|||
|
|
scheduleTemplates ScheduleTemplate[]
|
|||
|
|
|
|||
|
|
@@index([themeId])
|
|||
|
|
@@index([status])
|
|||
|
|
@@map("courses")
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4.2 Tenant(租户)- 新增关联
|
|||
|
|
|
|||
|
|
```prisma
|
|||
|
|
// 在 Tenant 模型中新增关联
|
|||
|
|
|
|||
|
|
model Tenant {
|
|||
|
|
// ... 保留原有字段 ...
|
|||
|
|
|
|||
|
|
// 新增关联
|
|||
|
|
packages TenantPackage[]
|
|||
|
|
schoolCourses SchoolCourse[]
|
|||
|
|
|
|||
|
|
// ... 保留其他关联 ...
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4.3 CourseResource(课程资源)- 新增关联
|
|||
|
|
|
|||
|
|
```prisma
|
|||
|
|
// 在 CourseResource 模型中新增关联
|
|||
|
|
|
|||
|
|
model CourseResource {
|
|||
|
|
// ... 保留原有字段 ...
|
|||
|
|
|
|||
|
|
// 新增关联
|
|||
|
|
stepResources LessonStepResource[]
|
|||
|
|
|
|||
|
|
@@map("course_resources")
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 五、废弃表处理
|
|||
|
|
|
|||
|
|
### 5.1 废弃策略
|
|||
|
|
|
|||
|
|
| 表名 | 废弃方式 | 说明 |
|
|||
|
|
|-----|---------|------|
|
|||
|
|
| CourseScript | 保留,标记废弃 | 数据迁移后不删除,保留历史记录 |
|
|||
|
|
| CourseScriptPage | 保留,标记废弃 | 数据迁移后不删除,保留历史记录 |
|
|||
|
|
| CourseActivity | 保留,标记废弃 | 数据迁移后不删除,保留历史记录 |
|
|||
|
|
|
|||
|
|
### 5.2 数据迁移说明
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
数据迁移映射:
|
|||
|
|
|
|||
|
|
CourseScript → CourseLesson + LessonStep
|
|||
|
|
├─ stepName → LessonStep.name
|
|||
|
|
├─ teacherScript → LessonStep.content
|
|||
|
|
├─ duration → LessonStep.duration
|
|||
|
|
├─ objective → LessonStep.objective
|
|||
|
|
└─ resourceIds → LessonStepResource 关联
|
|||
|
|
|
|||
|
|
CourseScriptPage → LessonStep(合入)
|
|||
|
|
├─ questions → LessonStep.content
|
|||
|
|
└─ teacherNotes → LessonStep.content
|
|||
|
|
|
|||
|
|
CourseActivity → CourseLesson(领域课)
|
|||
|
|
├─ name → CourseLesson.name
|
|||
|
|
├─ activityGuide → CourseLesson.extension
|
|||
|
|
└─ domain → CourseLesson.lessonType
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 六、迁移方案
|
|||
|
|
|
|||
|
|
### 6.1 迁移步骤
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
Phase 1: 创建新表(不删除旧表)
|
|||
|
|
├─ 创建 Theme 表
|
|||
|
|
├─ 创建 CoursePackage 表
|
|||
|
|
├─ 创建 CoursePackageCourse 表
|
|||
|
|
├─ 创建 TenantPackage 表
|
|||
|
|
├─ 创建 CourseLesson 表
|
|||
|
|
├─ 创建 LessonStep 表
|
|||
|
|
├─ 创建 LessonStepResource 表
|
|||
|
|
├─ 创建 SchoolCourse 表
|
|||
|
|
├─ 创建 SchoolCourseLesson 表
|
|||
|
|
└─ 创建 SchoolCourseReservation 表
|
|||
|
|
|
|||
|
|
Phase 2: 修改现有表
|
|||
|
|
├─ Course 表新增字段(themeId, coreContent, intro* 等)
|
|||
|
|
├─ CourseResource 表保持不变
|
|||
|
|
├─ Tenant 表新增关联
|
|||
|
|
└─ 其他表保持不变
|
|||
|
|
|
|||
|
|
Phase 3: 数据迁移
|
|||
|
|
├─ 初始化 Theme 字典数据
|
|||
|
|
├─ 为现有 Course 创建默认的 CourseLesson
|
|||
|
|
├─ 迁移 CourseScript → CourseLesson + LessonStep
|
|||
|
|
├─ 迁移 CourseActivity → CourseLesson(领域课)
|
|||
|
|
└─ 更新统计字段
|
|||
|
|
|
|||
|
|
Phase 4: 应用层适配
|
|||
|
|
├─ 更新 API 接口
|
|||
|
|
├─ 更新前端页面
|
|||
|
|
└─ 测试验证
|
|||
|
|
|
|||
|
|
Phase 5: 清理(可选)
|
|||
|
|
├─ 标记旧表为废弃
|
|||
|
|
└─ 评估是否需要删除旧表数据
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 6.2 安全措施
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// 迁移前备份
|
|||
|
|
async function backupBeforeMigration() {
|
|||
|
|
// 1. 导出关键表数据
|
|||
|
|
const courses = await prisma.course.findMany();
|
|||
|
|
const scripts = await prisma.courseScript.findMany();
|
|||
|
|
const activities = await prisma.courseActivity.findMany();
|
|||
|
|
|
|||
|
|
// 2. 保存到文件
|
|||
|
|
fs.writeFileSync('backup/courses.json', JSON.stringify(courses));
|
|||
|
|
fs.writeFileSync('backup/scripts.json', JSON.stringify(scripts));
|
|||
|
|
fs.writeFileSync('backup/activities.json', JSON.stringify(activities));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 迁移脚本示例
|
|||
|
|
async function migrateCourseToNewStructure(courseId: number) {
|
|||
|
|
const course = await prisma.course.findUnique({
|
|||
|
|
where: { id: courseId },
|
|||
|
|
include: {
|
|||
|
|
scripts: { orderBy: { stepIndex: 'asc' } },
|
|||
|
|
activities: true
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
if (!course) return;
|
|||
|
|
|
|||
|
|
// 1. 创建集体课(如果有脚本数据)
|
|||
|
|
if (course.scripts.length > 0) {
|
|||
|
|
const collectiveLesson = await prisma.courseLesson.create({
|
|||
|
|
data: {
|
|||
|
|
courseId: course.id,
|
|||
|
|
lessonType: 'COLLECTIVE',
|
|||
|
|
name: `${course.name} - 集体课`,
|
|||
|
|
duration: course.duration,
|
|||
|
|
useTemplate: true,
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 2. 迁移脚本为环节
|
|||
|
|
for (const script of course.scripts) {
|
|||
|
|
await prisma.lessonStep.create({
|
|||
|
|
data: {
|
|||
|
|
lessonId: collectiveLesson.id,
|
|||
|
|
name: script.stepName,
|
|||
|
|
content: script.teacherScript || '',
|
|||
|
|
duration: script.duration,
|
|||
|
|
objective: script.objective,
|
|||
|
|
resourceIds: script.resourceIds,
|
|||
|
|
sortOrder: script.stepIndex,
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 3. 迁移延伸活动为领域课
|
|||
|
|
for (const activity of course.activities) {
|
|||
|
|
const lessonType = mapDomainToLessonType(activity.domain);
|
|||
|
|
await prisma.courseLesson.create({
|
|||
|
|
data: {
|
|||
|
|
courseId: course.id,
|
|||
|
|
lessonType: lessonType,
|
|||
|
|
name: activity.name,
|
|||
|
|
duration: activity.duration || 25,
|
|||
|
|
extension: activity.activityGuide,
|
|||
|
|
sortOrder: activity.sortOrder,
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 4. 更新课程包标记
|
|||
|
|
await prisma.course.update({
|
|||
|
|
where: { id: courseId },
|
|||
|
|
data: { hasCollectiveLesson: course.scripts.length > 0 }
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 领域映射
|
|||
|
|
function mapDomainToLessonType(domain: string | null): string {
|
|||
|
|
const mapping: Record<string, string> = {
|
|||
|
|
'语言': 'LANGUAGE',
|
|||
|
|
'健康': 'HEALTH',
|
|||
|
|
'科学': 'SCIENCE',
|
|||
|
|
'社会': 'SOCIAL',
|
|||
|
|
'艺术': 'ART',
|
|||
|
|
};
|
|||
|
|
return mapping[domain || ''] || 'LANGUAGE';
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 七、API变更说明
|
|||
|
|
|
|||
|
|
### 7.1 新增API
|
|||
|
|
|
|||
|
|
| 模块 | 路径 | 说明 |
|
|||
|
|
|-----|------|------|
|
|||
|
|
| 套餐管理 | `POST /admin/packages` | 创建套餐 |
|
|||
|
|
| 套餐管理 | `PUT /admin/packages/:id/courses` | 添加课程包到套餐 |
|
|||
|
|
| 课程管理 | `POST /admin/courses/:id/lessons` | 创建课程 |
|
|||
|
|
| 课程管理 | `POST /admin/courses/:id/lessons/:lessonId/steps` | 创建教学环节 |
|
|||
|
|
| 学校端 | `POST /school/school-courses` | 创建校本课程包 |
|
|||
|
|
| 学校端 | `POST /school/school-courses/:id/reservations` | 预约校本课程 |
|
|||
|
|
|
|||
|
|
### 7.2 修改API
|
|||
|
|
|
|||
|
|
| 模块 | 路径 | 变更说明 |
|
|||
|
|
|-----|------|---------|
|
|||
|
|
| 课程包详情 | `GET /teacher/courses/:id` | 返回新结构数据 |
|
|||
|
|
| 备课模式 | `GET /teacher/courses/:id/lessons` | 按课程维度获取 |
|
|||
|
|
| 上课模式 | `POST /teacher/lessons/start` | 支持选择课程 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 八、后续优化建议
|
|||
|
|
|
|||
|
|
### 8.1 性能优化
|
|||
|
|
|
|||
|
|
- 为常用查询字段添加索引
|
|||
|
|
- 考虑将富文本字段拆分到单独表(如果内容过大)
|
|||
|
|
- 使用Redis缓存热点数据
|
|||
|
|
|
|||
|
|
### 8.2 扩展性考虑
|
|||
|
|
|
|||
|
|
- 课程类型可使用枚举定义
|
|||
|
|
- 考虑支持自定义课程类型
|
|||
|
|
- 校本课程包的版本管理
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
*本文档创建于 2026-02-27*
|
|||
|
|
*基于需求文档:17-课程包套餐重构需求.md*
|