kindergarten_java/docs/design/21-数据模型重构设计.md
2026-02-28 16:41:39 +08:00

28 KiB
Raw Blame History

数据模型重构设计

创建时间2026-02-27 基于需求17-课程包套餐重构需求.md 状态:设计中


一、设计原则

1.1 重构原则

原则 说明
向后兼容 新结构与旧结构可以共存,支持渐进式迁移
数据安全 迁移过程中不丢失任何现有数据
最小变更 保留可复用的表和字段,减少不必要的重构
可扩展性 新结构支持后续功能迭代

1.2 参考最佳实践

基于以下资源的最佳实践:


二、表结构变更概览

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课程套餐

// ==================== 课程套餐 ====================

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套餐-课程包关联)

// ==================== 套餐-课程包关联 ====================

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租户-套餐授权)

// ==================== 租户-套餐授权 ====================

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主题字典

// ==================== 主题字典 ====================

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课程

// ==================== 课程(导入课/集体课/领域课) ====================

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教学环节

// ==================== 教学环节 ====================

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环节资源关联

// ==================== 环节资源关联 ====================

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校本课程包

// ==================== 校本课程包 ====================

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校本课程

// ==================== 校本课程 ====================

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校本课程预约

// ==================== 校本课程预约 ====================

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课程包- 重构

// ==================== 课程包(重构) ====================

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租户- 新增关联

// 在 Tenant 模型中新增关联

model Tenant {
  // ... 保留原有字段 ...

  // 新增关联
  packages         TenantPackage[]
  schoolCourses    SchoolCourse[]

  // ... 保留其他关联 ...
}

4.3 CourseResource课程资源- 新增关联

// 在 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 安全措施

// 迁移前备份
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