/** * 数据迁移脚本:V1 -> V2 * * 迁移内容: * 1. 初始化主题字典(6个默认主题) * 2. 为现有课程包创建默认的 CourseLesson(集体课) * 3. 迁移 CourseScript → LessonStep * 4. 迁移 CourseActivity → CourseLesson(领域课) * 5. 创建默认套餐(按年级分组) * 6. 迁移租户授权 TenantCourse → TenantPackage * * 执行命令:npx ts-node prisma/migrate-v1-to-v2.ts */ import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); // 默认主题 const DEFAULT_THEMES = [ { name: '我爱幼儿园', description: '适应幼儿园生活,培养基本生活习惯', sortOrder: 1 }, { name: '认识自我', description: '认识身体、情绪、能力,建立自我意识', sortOrder: 2 }, { name: '我的家', description: '了解家庭成员,培养亲情和责任感', sortOrder: 3 }, { name: '美丽的自然', description: '探索自然现象,培养环保意识', sortOrder: 4 }, { name: '奇妙的世界', description: '认识社会和世界,拓展视野', sortOrder: 5 }, { name: '成长的快乐', description: '培养学习兴趣和良好习惯', sortOrder: 6 }, ]; // 年级映射 const GRADE_LEVELS = ['小班', '中班', '大班']; async function main() { console.log('开始数据迁移...\n'); try { // ==================== Step 1: 初始化主题字典 ==================== console.log('Step 1: 初始化主题字典...'); const existingThemes = await prisma.theme.count(); if (existingThemes === 0) { for (const theme of DEFAULT_THEMES) { await prisma.theme.create({ data: { name: theme.name, description: theme.description, sortOrder: theme.sortOrder, status: 'ACTIVE', }, }); } console.log(` ✅ 已创建 ${DEFAULT_THEMES.length} 个默认主题`); } else { console.log(` ⏭️ 主题已存在 (${existingThemes} 个),跳过`); } // ==================== Step 2: 创建 CourseLesson(集体课)并迁移 CourseScript ==================== console.log('\nStep 2: 创建 CourseLesson 并迁移 CourseScript...'); const courses = await prisma.course.findMany({ where: { isLatest: true }, include: { scripts: { orderBy: { sortOrder: 'asc' }, }, }, }); let lessonCount = 0; let stepCount = 0; for (const course of courses) { // 检查是否已有集体课 const existingLesson = await prisma.courseLesson.findFirst({ where: { courseId: course.id, lessonType: 'COLLECTIVE' }, }); if (existingLesson) { console.log(` ⏭️ 课程 "${course.name}" 已有集体课,跳过`); continue; } // 创建集体课 CourseLesson const courseLesson = await prisma.courseLesson.create({ data: { courseId: course.id, lessonType: 'COLLECTIVE', name: `${course.name} - 集体课`, description: course.description, duration: course.duration || 25, pptPath: course.pptPath, pptName: course.pptName, objectives: course.lessonPlanData ? JSON.parse(course.lessonPlanData).objectives : null, preparation: course.tools || course.studentMaterials, sortOrder: 0, }, }); lessonCount++; // 迁移 CourseScript → LessonStep for (const script of course.scripts) { await prisma.lessonStep.create({ data: { lessonId: courseLesson.id, name: script.stepName || `环节 ${script.stepIndex}`, content: script.teacherScript, duration: script.duration || 5, objective: script.objective, resourceIds: script.resourceIds, sortOrder: script.sortOrder || script.stepIndex, }, }); stepCount++; } console.log(` ✅ 课程 "${course.name}":创建集体课 + ${course.scripts.length} 个环节`); } console.log(` 📊 共创建 ${lessonCount} 个集体课,${stepCount} 个教学环节`); // ==================== Step 3: 迁移 CourseActivity → CourseLesson(领域课) ==================== console.log('\nStep 3: 迁移 CourseActivity → CourseLesson(领域课)...'); const activities = await prisma.courseActivity.findMany({ include: { course: true }, }); // 按课程分组 const activitiesByCourse = new Map(); for (const activity of activities) { if (!activitiesByCourse.has(activity.courseId)) { activitiesByCourse.set(activity.courseId, []); } activitiesByCourse.get(activity.courseId)!.push(activity); } let activityLessonCount = 0; for (const [courseId, courseActivities] of activitiesByCourse) { const course = courseActivities[0].course; for (const activity of courseActivities) { // 确定课程类型 let lessonType = 'DOMAIN'; const domainMap: Record = { '健康': 'HEALTH', '语言': 'LANGUAGE', '社会': 'SOCIAL', '科学': 'SCIENCE', '艺术': 'ART', }; if (activity.domain && domainMap[activity.domain]) { lessonType = domainMap[activity.domain]; } // 检查是否已有该类型的领域课 const existingActivityLesson = await prisma.courseLesson.findFirst({ where: { courseId, lessonType }, }); if (existingActivityLesson) { continue; } await prisma.courseLesson.create({ data: { courseId, lessonType, name: activity.name, description: activity.activityGuide, duration: activity.duration || 25, objectives: activity.objectives, preparation: activity.offlineMaterials, extension: activity.onlineMaterials, sortOrder: activity.sortOrder || 0, }, }); activityLessonCount++; } console.log(` ✅ 课程 "${course.name}":创建 ${courseActivities.length} 个领域课`); } console.log(` 📊 共创建 ${activityLessonCount} 个领域课`); // ==================== Step 4: 创建默认套餐(按年级分组) ==================== console.log('\nStep 4: 创建默认套餐...'); const existingPackages = await prisma.coursePackage.count(); if (existingPackages === 0) { // 获取所有已发布的课程 const publishedCourses = await prisma.course.findMany({ where: { status: 'PUBLISHED' }, }); // 年级标签映射(统一格式) const gradeTagMap: Record = { 'SMALL': '小班', 'small': '小班', 'MIDDLE': '中班', 'middle': '中班', 'BIG': '大班', 'big': '大班', }; // 按年级分组课程 const coursesByGrade = new Map(); for (const course of publishedCourses) { let gradeTags: string[] = []; try { gradeTags = JSON.parse(course.gradeTags || '[]'); } catch { gradeTags = []; } for (const rawGrade of gradeTags) { const grade = gradeTagMap[rawGrade] || rawGrade; if (!coursesByGrade.has(grade)) { coursesByGrade.set(grade, []); } coursesByGrade.get(grade)!.push(course); } } // 为每个年级创建套餐 for (const grade of GRADE_LEVELS) { const gradeCourses = coursesByGrade.get(grade) || []; if (gradeCourses.length === 0) { console.log(` ⏭️ 年级 "${grade}" 没有课程,跳过套餐创建`); continue; } const packageName = `${grade}课程套餐`; const coursePackage = await prisma.coursePackage.create({ data: { name: packageName, description: `包含 ${gradeCourses.length} 个课程包,覆盖${grade}全学期教学内容`, price: gradeCourses.length * 10000, // 假设每个课程包 100 元 gradeLevels: JSON.stringify([grade]), status: 'PUBLISHED', courseCount: gradeCourses.length, publishedAt: new Date(), }, }); // 创建套餐-课程关联 for (let i = 0; i < gradeCourses.length; i++) { const course = gradeCourses[i]; await prisma.coursePackageCourse.create({ data: { packageId: coursePackage.id, courseId: course.id, gradeLevel: grade, sortOrder: i, }, }); } console.log(` ✅ 创建套餐 "${packageName}":包含 ${gradeCourses.length} 个课程包`); } } else { console.log(` ⏭️ 套餐已存在 (${existingPackages} 个),跳过`); } // ==================== Step 5: 迁移租户授权 TenantCourse → TenantPackage ==================== console.log('\nStep 5: 迁移租户授权...'); // 年级标签映射(统一格式) const gradeTagMapForAuth: Record = { 'SMALL': '小班', 'small': '小班', 'MIDDLE': '中班', 'middle': '中班', 'BIG': '大班', 'big': '大班', }; // 获取所有套餐 const packages = await prisma.coursePackage.findMany(); const packageByGrade = new Map(); for (const pkg of packages) { const grades = JSON.parse(pkg.gradeLevels || '[]'); for (const grade of grades) { packageByGrade.set(grade, pkg); } } // 获取租户课程授权 const tenantCourses = await prisma.tenantCourse.findMany({ include: { course: true, tenant: true }, }); // 按租户分组 const tenantCoursesByTenant = new Map(); for (const tc of tenantCourses) { if (!tenantCoursesByTenant.has(tc.tenantId)) { tenantCoursesByTenant.set(tc.tenantId, []); } tenantCoursesByTenant.get(tc.tenantId)!.push(tc); } let tenantPackageCount = 0; for (const [tenantId, tcs] of tenantCoursesByTenant) { const tenant = tcs[0].tenant; // 获取该租户需要的套餐 const neededPackages = new Set(); for (const tc of tcs) { const gradeTags = JSON.parse(tc.course.gradeTags || '[]'); for (const rawGrade of gradeTags) { const grade = gradeTagMapForAuth[rawGrade] || rawGrade; const pkg = packageByGrade.get(grade); if (pkg) { neededPackages.add(pkg); } } } // 为租户创建套餐授权 for (const pkg of neededPackages) { // 检查是否已有授权 const existingAuth = await prisma.tenantPackage.findFirst({ where: { tenantId, packageId: pkg.id }, }); if (existingAuth) { continue; } // 使用租户的订阅日期 const startDate = tenant.startDate || new Date().toISOString().split('T')[0]; const endDate = tenant.expireDate || new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]; await prisma.tenantPackage.create({ data: { tenantId, packageId: pkg.id, startDate, endDate, status: 'ACTIVE', pricePaid: pkg.price, }, }); tenantPackageCount++; } console.log(` ✅ 租户 "${tenant.name}":授权 ${neededPackages.size} 个套餐`); } console.log(` 📊 共创建 ${tenantPackageCount} 个租户套餐授权`); // ==================== 完成 ==================== console.log('\n✅ 数据迁移完成!'); // 输出统计信息 const stats = { themes: await prisma.theme.count(), courses: await prisma.course.count({ where: { isLatest: true } }), courseLessons: await prisma.courseLesson.count(), lessonSteps: await prisma.lessonStep.count(), packages: await prisma.coursePackage.count(), tenantPackages: await prisma.tenantPackage.count(), }; console.log('\n📊 迁移后数据统计:'); console.log(` 主题:${stats.themes}`); console.log(` 课程包:${stats.courses}`); console.log(` 课程(CourseLesson):${stats.courseLessons}`); console.log(` 教学环节:${stats.lessonSteps}`); console.log(` 套餐:${stats.packages}`); console.log(` 租户套餐授权:${stats.tenantPackages}`); } catch (error) { console.error('❌ 迁移失败:', error); throw error; } } main() .catch((error) => { console.error(error); process.exit(1); }) .finally(async () => { await prisma.$disconnect(); });