kindergarten_java/reading-platform-backend/prisma/migrate-v1-to-v2.ts

386 lines
13 KiB
TypeScript
Raw Normal View History

/**
* 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();
});