181 lines
6.0 KiB
TypeScript
181 lines
6.0 KiB
TypeScript
import { Injectable } from '@nestjs/common';
|
|
import { PrismaService } from '../../prisma/prisma.service';
|
|
|
|
@Injectable()
|
|
export class CoursesService {
|
|
constructor(private prisma: PrismaService) {}
|
|
|
|
async findAll(params: { page?: number; pageSize?: number; categoryId?: number; isFree?: boolean }) {
|
|
const page = Number(params.page ?? 1);
|
|
const pageSize = Number(params.pageSize ?? 20);
|
|
const { categoryId, isFree } = params;
|
|
const where: any = { status: 'PUBLISHED', deletedAt: null };
|
|
if (categoryId) where.categoryId = categoryId;
|
|
if (isFree !== undefined) where.isFree = isFree;
|
|
|
|
const [items, total] = await Promise.all([
|
|
this.prisma.course.findMany({
|
|
where,
|
|
skip: (page - 1) * pageSize,
|
|
take: pageSize,
|
|
orderBy: { sortOrder: 'asc' },
|
|
include: { category: true, chapters: { include: { lessons: true }, orderBy: { sortOrder: 'asc' } } },
|
|
}),
|
|
this.prisma.course.count({ where }),
|
|
]);
|
|
|
|
return { items, total, page, pageSize };
|
|
}
|
|
|
|
async findById(id: number) {
|
|
return this.prisma.course.findUnique({
|
|
where: { id },
|
|
include: {
|
|
category: true,
|
|
chapters: {
|
|
orderBy: { sortOrder: 'asc' },
|
|
include: { lessons: { orderBy: { sortOrder: 'asc' } } },
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
async create(data: {
|
|
title: string; description?: string; cover?: string; categoryId?: number;
|
|
price?: number; isFree?: boolean; sortOrder?: number;
|
|
}) {
|
|
return this.prisma.course.create({ data });
|
|
}
|
|
|
|
async update(id: number, data: any) {
|
|
const { chapters, ...courseData } = data;
|
|
return this.prisma.course.update({
|
|
where: { id },
|
|
data: courseData,
|
|
});
|
|
}
|
|
|
|
async updateWithChapters(id: number, data: any) {
|
|
const { chapters, ...courseData } = data;
|
|
if (chapters) {
|
|
const existingChapters = await this.prisma.chapter.findMany({ where: { courseId: id } });
|
|
const existingIds = existingChapters.map(c => c.id);
|
|
const incomingIds = chapters.filter((c: any) => c.id).map((c: any) => c.id);
|
|
const toDelete = existingIds.filter(eid => !incomingIds.includes(eid));
|
|
for (const cid of toDelete) {
|
|
await this.prisma.lesson.deleteMany({ where: { chapterId: cid } });
|
|
await this.prisma.chapter.delete({ where: { id: cid } });
|
|
}
|
|
for (const ch of chapters) {
|
|
if (ch.id) {
|
|
const { lessons, id: chId, courseId, ...chData } = ch;
|
|
await this.prisma.chapter.update({ where: { id: ch.id }, data: chData });
|
|
if (lessons) {
|
|
const existingLessons = await this.prisma.lesson.findMany({ where: { chapterId: ch.id } });
|
|
const existingLessonIds = existingLessons.map(l => l.id);
|
|
const incomingLessonIds = lessons.filter((l: any) => l.id).map((l: any) => l.id);
|
|
const lessonsToDelete = existingLessonIds.filter(eid => !incomingLessonIds.includes(eid));
|
|
for (const lid of lessonsToDelete) {
|
|
await this.prisma.lesson.delete({ where: { id: lid } });
|
|
}
|
|
for (const le of lessons) {
|
|
if (le.id) {
|
|
const { id: lessonId, chapterId, ...lessonData } = le;
|
|
await this.prisma.lesson.update({ where: { id: le.id }, data: lessonData });
|
|
} else {
|
|
const { id: _li, ...lessonData } = le;
|
|
await this.prisma.lesson.create({ data: { ...lessonData, chapterId: ch.id } });
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
const { lessons, id: _newChId, ...chData } = ch;
|
|
const newCh = await this.prisma.chapter.create({ data: { ...chData, courseId: id } });
|
|
if (lessons) {
|
|
for (const le of lessons) {
|
|
const { id: _li, ...lessonData } = le;
|
|
await this.prisma.lesson.create({ data: { ...lessonData, chapterId: newCh.id } });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return this.prisma.course.update({
|
|
where: { id },
|
|
data: courseData,
|
|
include: { chapters: { include: { lessons: { orderBy: { sortOrder: 'asc' } } }, orderBy: { sortOrder: 'asc' } } },
|
|
});
|
|
}
|
|
|
|
async remove(id: number) {
|
|
return this.prisma.course.update({ where: { id }, data: { deletedAt: new Date() } });
|
|
}
|
|
|
|
async updateProgress(userId: number, courseId: number, lessonId: number, completed?: boolean, progress?: number) {
|
|
const existing = await this.prisma.learnRecord.findUnique({
|
|
where: { userId_lessonId: { userId, lessonId } },
|
|
});
|
|
|
|
if (existing) {
|
|
return this.prisma.learnRecord.update({
|
|
where: { id: existing.id },
|
|
data: {
|
|
completed: completed ?? existing.completed,
|
|
progress: progress ?? existing.progress,
|
|
},
|
|
});
|
|
} else {
|
|
return this.prisma.learnRecord.create({
|
|
data: {
|
|
userId,
|
|
courseId,
|
|
lessonId,
|
|
completed: completed ?? false,
|
|
progress: progress ?? 0,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
async getLearningProgress(userId: number, courseId?: number) {
|
|
const where: any = { userId };
|
|
if (courseId) {
|
|
where.courseId = courseId;
|
|
}
|
|
|
|
const records = await this.prisma.learnRecord.findMany({
|
|
where,
|
|
include: {
|
|
course: { select: { id: true, title: true } },
|
|
lesson: { select: { id: true, title: true, chapterId: true } },
|
|
},
|
|
});
|
|
|
|
const courseMap: Record<number, any> = {};
|
|
records.forEach(r => {
|
|
if (!courseMap[r.courseId]) {
|
|
courseMap[r.courseId] = {
|
|
courseId: r.courseId,
|
|
courseTitle: r.course.title,
|
|
totalLessons: 0,
|
|
completedLessons: 0,
|
|
progress: 0,
|
|
records: [],
|
|
};
|
|
}
|
|
courseMap[r.courseId].records.push(r);
|
|
courseMap[r.courseId].totalLessons++;
|
|
if (r.completed) courseMap[r.courseId].completedLessons++;
|
|
});
|
|
|
|
Object.values(courseMap).forEach(c => {
|
|
c.progress = c.totalLessons > 0 ? Math.round((c.completedLessons / c.totalLessons) * 100) : 0;
|
|
});
|
|
|
|
return {
|
|
items: Object.values(courseMap),
|
|
total: records.length,
|
|
};
|
|
}
|
|
}
|