kindergarten_java/reading-platform-backend/prisma/migrate-v1-to-v2.ts
2026-02-28 16:41:39 +08:00

386 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 数据迁移脚本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();
});