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*
|