386 lines
13 KiB
TypeScript
386 lines
13 KiB
TypeScript
/**
|
||
* 数据迁移脚本: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<number, typeof activities>();
|
||
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<string, string> = {
|
||
'健康': '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<string, string> = {
|
||
'SMALL': '小班',
|
||
'small': '小班',
|
||
'MIDDLE': '中班',
|
||
'middle': '中班',
|
||
'BIG': '大班',
|
||
'big': '大班',
|
||
};
|
||
|
||
// 按年级分组课程
|
||
const coursesByGrade = new Map<string, typeof publishedCourses>();
|
||
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<string, string> = {
|
||
'SMALL': '小班',
|
||
'small': '小班',
|
||
'MIDDLE': '中班',
|
||
'middle': '中班',
|
||
'BIG': '大班',
|
||
'big': '大班',
|
||
};
|
||
|
||
// 获取所有套餐
|
||
const packages = await prisma.coursePackage.findMany();
|
||
const packageByGrade = new Map<string, typeof packages[0]>();
|
||
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<number, typeof tenantCourses>();
|
||
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<typeof packages[0]>();
|
||
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();
|
||
});
|