- reading-platform-backend:NestJS 后端 - reading-platform-frontend:Vue3 前端 - reading-platform-java:Spring Boot 服务端
373 lines
8.4 KiB
TypeScript
373 lines
8.4 KiB
TypeScript
import { Injectable } from '@nestjs/common';
|
|
import { PrismaService } from '../../database/prisma.service';
|
|
|
|
@Injectable()
|
|
export class CoursePackageService {
|
|
constructor(private prisma: PrismaService) {}
|
|
|
|
// ==================== 套餐管理 ====================
|
|
|
|
async findAllPackages(params: {
|
|
status?: string;
|
|
page?: number;
|
|
pageSize?: number;
|
|
}) {
|
|
const { status, page = 1, pageSize = 20 } = params;
|
|
const skip = (page - 1) * pageSize;
|
|
|
|
const where: any = {};
|
|
if (status) {
|
|
where.status = status;
|
|
}
|
|
|
|
const [items, total] = await Promise.all([
|
|
this.prisma.coursePackage.findMany({
|
|
where,
|
|
include: {
|
|
_count: {
|
|
select: { courses: true, tenantPackages: true },
|
|
},
|
|
},
|
|
orderBy: { createdAt: 'desc' },
|
|
skip,
|
|
take: pageSize,
|
|
}),
|
|
this.prisma.coursePackage.count({ where }),
|
|
]);
|
|
|
|
return {
|
|
items: items.map((pkg) => ({
|
|
...pkg,
|
|
courseCount: pkg._count.courses,
|
|
tenantCount: pkg._count.tenantPackages,
|
|
})),
|
|
total,
|
|
page,
|
|
pageSize,
|
|
};
|
|
}
|
|
|
|
async findOnePackage(id: number) {
|
|
const pkg = await this.prisma.coursePackage.findUnique({
|
|
where: { id },
|
|
include: {
|
|
courses: {
|
|
include: {
|
|
course: {
|
|
select: {
|
|
id: true,
|
|
name: true,
|
|
coverImagePath: true,
|
|
duration: true,
|
|
gradeTags: true,
|
|
},
|
|
},
|
|
},
|
|
orderBy: { sortOrder: 'asc' },
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!pkg) {
|
|
throw new Error('套餐不存在');
|
|
}
|
|
|
|
return pkg;
|
|
}
|
|
|
|
async createPackage(data: {
|
|
name: string;
|
|
description?: string;
|
|
price: number;
|
|
discountPrice?: number;
|
|
discountType?: string;
|
|
gradeLevels: string[];
|
|
}) {
|
|
return this.prisma.coursePackage.create({
|
|
data: {
|
|
name: data.name,
|
|
description: data.description,
|
|
price: data.price,
|
|
discountPrice: data.discountPrice,
|
|
discountType: data.discountType,
|
|
gradeLevels: JSON.stringify(data.gradeLevels),
|
|
status: 'DRAFT',
|
|
},
|
|
});
|
|
}
|
|
|
|
async updatePackage(
|
|
id: number,
|
|
data: {
|
|
name?: string;
|
|
description?: string;
|
|
price?: number;
|
|
discountPrice?: number;
|
|
discountType?: string;
|
|
gradeLevels?: string[];
|
|
},
|
|
) {
|
|
const updateData: any = { ...data };
|
|
if (data.gradeLevels) {
|
|
updateData.gradeLevels = JSON.stringify(data.gradeLevels);
|
|
}
|
|
|
|
return this.prisma.coursePackage.update({
|
|
where: { id },
|
|
data: updateData,
|
|
});
|
|
}
|
|
|
|
async deletePackage(id: number) {
|
|
// 检查是否有租户正在使用
|
|
const tenantCount = await this.prisma.tenantPackage.count({
|
|
where: { packageId: id, status: 'ACTIVE' },
|
|
});
|
|
|
|
if (tenantCount > 0) {
|
|
throw new Error(`有 ${tenantCount} 个租户正在使用该套餐,无法删除`);
|
|
}
|
|
|
|
return this.prisma.coursePackage.delete({
|
|
where: { id },
|
|
});
|
|
}
|
|
|
|
// ==================== 套餐课程管理 ====================
|
|
|
|
async setPackageCourses(
|
|
packageId: number,
|
|
courses: { courseId: number; gradeLevel: string; sortOrder?: number }[],
|
|
) {
|
|
// 删除现有关联
|
|
await this.prisma.coursePackageCourse.deleteMany({
|
|
where: { packageId },
|
|
});
|
|
|
|
// 创建新关联
|
|
if (courses.length > 0) {
|
|
await this.prisma.coursePackageCourse.createMany({
|
|
data: courses.map((c, index) => ({
|
|
packageId,
|
|
courseId: c.courseId,
|
|
gradeLevel: c.gradeLevel,
|
|
sortOrder: c.sortOrder ?? index,
|
|
})),
|
|
});
|
|
}
|
|
|
|
// 更新套餐课程数
|
|
await this.prisma.coursePackage.update({
|
|
where: { id: packageId },
|
|
data: { courseCount: courses.length },
|
|
});
|
|
|
|
return this.findOnePackage(packageId);
|
|
}
|
|
|
|
async addCourseToPackage(
|
|
packageId: number,
|
|
courseId: number,
|
|
gradeLevel: string,
|
|
sortOrder?: number,
|
|
) {
|
|
// 检查是否已存在
|
|
const existing = await this.prisma.coursePackageCourse.findUnique({
|
|
where: {
|
|
packageId_courseId: { packageId, courseId },
|
|
},
|
|
});
|
|
|
|
if (existing) {
|
|
throw new Error('该课程已在套餐中');
|
|
}
|
|
|
|
await this.prisma.coursePackageCourse.create({
|
|
data: {
|
|
packageId,
|
|
courseId,
|
|
gradeLevel,
|
|
sortOrder: sortOrder ?? 0,
|
|
},
|
|
});
|
|
|
|
// 更新套餐课程数
|
|
const count = await this.prisma.coursePackageCourse.count({
|
|
where: { packageId },
|
|
});
|
|
await this.prisma.coursePackage.update({
|
|
where: { id: packageId },
|
|
data: { courseCount: count },
|
|
});
|
|
|
|
return this.findOnePackage(packageId);
|
|
}
|
|
|
|
async removeCourseFromPackage(packageId: number, courseId: number) {
|
|
await this.prisma.coursePackageCourse.delete({
|
|
where: {
|
|
packageId_courseId: { packageId, courseId },
|
|
},
|
|
});
|
|
|
|
// 更新套餐课程数
|
|
const count = await this.prisma.coursePackageCourse.count({
|
|
where: { packageId },
|
|
});
|
|
await this.prisma.coursePackage.update({
|
|
where: { id: packageId },
|
|
data: { courseCount: count },
|
|
});
|
|
|
|
return this.findOnePackage(packageId);
|
|
}
|
|
|
|
// ==================== 套餐状态管理 ====================
|
|
|
|
async submitPackage(id: number, userId: number) {
|
|
const pkg = await this.prisma.coursePackage.findUnique({
|
|
where: { id },
|
|
include: { _count: { select: { courses: true } } },
|
|
});
|
|
|
|
if (!pkg) {
|
|
throw new Error('套餐不存在');
|
|
}
|
|
|
|
if (pkg._count.courses === 0) {
|
|
throw new Error('套餐必须包含至少一个课程包');
|
|
}
|
|
|
|
return this.prisma.coursePackage.update({
|
|
where: { id },
|
|
data: {
|
|
status: 'PENDING_REVIEW',
|
|
submittedAt: new Date(),
|
|
submittedBy: userId,
|
|
},
|
|
});
|
|
}
|
|
|
|
async reviewPackage(
|
|
id: number,
|
|
userId: number,
|
|
approved: boolean,
|
|
comment?: string,
|
|
) {
|
|
const pkg = await this.prisma.coursePackage.findUnique({
|
|
where: { id },
|
|
});
|
|
|
|
if (!pkg) {
|
|
throw new Error('套餐不存在');
|
|
}
|
|
|
|
if (pkg.status !== 'PENDING_REVIEW') {
|
|
throw new Error('只有待审核状态的套餐可以审核');
|
|
}
|
|
|
|
return this.prisma.coursePackage.update({
|
|
where: { id },
|
|
data: {
|
|
status: approved ? 'APPROVED' : 'REJECTED',
|
|
reviewedAt: new Date(),
|
|
reviewedBy: userId,
|
|
reviewComment: comment,
|
|
},
|
|
});
|
|
}
|
|
|
|
async publishPackage(id: number) {
|
|
const pkg = await this.prisma.coursePackage.findUnique({
|
|
where: { id },
|
|
});
|
|
|
|
if (!pkg) {
|
|
throw new Error('套餐不存在');
|
|
}
|
|
|
|
if (pkg.status !== 'APPROVED') {
|
|
throw new Error('只有已审核通过的套餐可以发布');
|
|
}
|
|
|
|
return this.prisma.coursePackage.update({
|
|
where: { id },
|
|
data: {
|
|
status: 'PUBLISHED',
|
|
publishedAt: new Date(),
|
|
},
|
|
});
|
|
}
|
|
|
|
async offlinePackage(id: number) {
|
|
return this.prisma.coursePackage.update({
|
|
where: { id },
|
|
data: {
|
|
status: 'OFFLINE',
|
|
},
|
|
});
|
|
}
|
|
|
|
// ==================== 学校端查询 ====================
|
|
|
|
async findTenantPackages(tenantId: number) {
|
|
return this.prisma.tenantPackage.findMany({
|
|
where: {
|
|
tenantId,
|
|
status: 'ACTIVE',
|
|
},
|
|
include: {
|
|
package: {
|
|
include: {
|
|
courses: {
|
|
include: {
|
|
course: {
|
|
select: {
|
|
id: true,
|
|
name: true,
|
|
coverImagePath: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
orderBy: { createdAt: 'desc' },
|
|
});
|
|
}
|
|
|
|
async renewTenantPackage(
|
|
tenantId: number,
|
|
packageId: number,
|
|
endDate: string,
|
|
pricePaid?: number,
|
|
) {
|
|
const existing = await this.prisma.tenantPackage.findFirst({
|
|
where: { tenantId, packageId },
|
|
});
|
|
|
|
if (existing) {
|
|
return this.prisma.tenantPackage.update({
|
|
where: { id: existing.id },
|
|
data: {
|
|
endDate,
|
|
status: 'ACTIVE',
|
|
pricePaid: pricePaid ?? existing.pricePaid,
|
|
},
|
|
});
|
|
}
|
|
|
|
return this.prisma.tenantPackage.create({
|
|
data: {
|
|
tenantId,
|
|
packageId,
|
|
startDate: new Date().toISOString().split('T')[0],
|
|
endDate,
|
|
status: 'ACTIVE',
|
|
pricePaid: pricePaid ?? 0,
|
|
},
|
|
});
|
|
}
|
|
}
|