From 712202deefeae0bb071ccc818ad6c70a419fdde1 Mon Sep 17 00:00:00 2001 From: ditiqi Date: Mon, 24 Feb 2025 08:51:44 +0800 Subject: [PATCH] add --- .../server/src/models/course/course.module.ts | 10 -- .../server/src/models/course/course.router.ts | 92 ------------- .../server/src/models/course/course.schema.ts | 0 .../src/models/course/course.service.ts | 78 ----------- apps/server/src/models/course/utils.ts | 49 ------- .../src/models/lecture/lecture.module.ts | 10 -- .../src/models/lecture/lecture.router.ts | 70 ---------- .../src/models/lecture/lecture.service.ts | 35 ----- apps/server/src/models/lecture/utils.ts | 48 ------- .../src/models/section/section.module.ts | 10 -- .../src/models/section/section.router.ts | 70 ---------- .../src/models/section/section.service.ts | 23 ---- apps/server/src/models/visit/visit.router.ts | 54 ++++---- apps/server/src/models/visit/visit.service.ts | 74 +++++++++- .../queue/models/post/post.queue.service.ts | 40 ++++++ apps/server/src/queue/models/post/utils.ts | 46 +++++++ apps/server/src/queue/queue.module.ts | 3 +- apps/server/src/queue/types.ts | 10 ++ apps/server/src/utils/event-bus.ts | 26 +++- .../course/detail/CourseDetailContext.tsx | 24 +++- .../CourseDetailHeader/CourseDetailHeader.tsx | 130 +++++++++++------- .../CourseDetailHeader_BACKUP.tsx | 77 +++++++++++ .../course/detail/CourseDetailLayout.tsx | 3 +- .../detail/CourseSyllabus/LectureItem.tsx | 10 +- .../editor/context/CourseEditorContext.tsx | 11 +- .../course/editor/form/CourseBasicForm.tsx | 8 ++ .../course/editor/form/CourseGoalForm.tsx | 36 ++--- .../course/editor/form/CourseSettingForm.tsx | 50 +++---- .../editor/layout/CourseEditorHeader.tsx | 10 +- .../editor/layout/CourseEditorLayout.tsx | 112 +++++++-------- .../editor/layout/CourseEditorSidebar.tsx | 27 ++-- .../models/course/editor/navItems.tsx | 24 ++-- apps/web/src/routes/index.tsx | 27 ++-- config/nginx/conf.d/web.template | 2 +- config/nginx/entrypoint.sh | 2 +- 35 files changed, 555 insertions(+), 746 deletions(-) delete mode 100755 apps/server/src/models/course/course.module.ts delete mode 100755 apps/server/src/models/course/course.router.ts delete mode 100755 apps/server/src/models/course/course.schema.ts delete mode 100755 apps/server/src/models/course/course.service.ts delete mode 100755 apps/server/src/models/course/utils.ts delete mode 100755 apps/server/src/models/lecture/lecture.module.ts delete mode 100755 apps/server/src/models/lecture/lecture.router.ts delete mode 100755 apps/server/src/models/lecture/lecture.service.ts delete mode 100755 apps/server/src/models/lecture/utils.ts delete mode 100755 apps/server/src/models/section/section.module.ts delete mode 100755 apps/server/src/models/section/section.router.ts delete mode 100755 apps/server/src/models/section/section.service.ts create mode 100644 apps/server/src/queue/models/post/post.queue.service.ts create mode 100644 apps/server/src/queue/models/post/utils.ts create mode 100755 apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader_BACKUP.tsx diff --git a/apps/server/src/models/course/course.module.ts b/apps/server/src/models/course/course.module.ts deleted file mode 100755 index 8438e80..0000000 --- a/apps/server/src/models/course/course.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -// import { Module } from '@nestjs/common'; -// import { CourseRouter } from './course.router'; -// import { CourseService } from './course.service'; -// import { TrpcService } from '@server/trpc/trpc.service'; - -// @Module({ -// providers: [CourseRouter, CourseService, TrpcService], -// exports: [CourseRouter, CourseService] -// }) -// export class CourseModule { } diff --git a/apps/server/src/models/course/course.router.ts b/apps/server/src/models/course/course.router.ts deleted file mode 100755 index fe9047f..0000000 --- a/apps/server/src/models/course/course.router.ts +++ /dev/null @@ -1,92 +0,0 @@ -// import { Injectable } from '@nestjs/common'; -// import { TrpcService } from '@server/trpc/trpc.service'; -// import { Prisma, UpdateOrderSchema } from '@nice/common'; -// import { CourseService } from './course.service'; -// import { z, ZodType } from 'zod'; -// // const CourseCreateArgsSchema: ZodType = z.any(); -// // const CourseUpdateArgsSchema: ZodType = z.any(); -// // const CourseCreateManyInputSchema: ZodType = -// // z.any(); -// // const CourseDeleteManyArgsSchema: ZodType = -// // z.any(); -// // const CourseFindManyArgsSchema: ZodType = z.any(); -// // const CourseFindFirstArgsSchema: ZodType = z.any(); -// // const CourseWhereInputSchema: ZodType = z.any(); -// // const CourseSelectSchema: ZodType = z.any(); - -// @Injectable() -// export class CourseRouter { -// constructor( -// private readonly trpc: TrpcService, -// private readonly courseService: CourseService, -// ) {} -// router = this.trpc.router({ -// // create: this.trpc.protectProcedure -// // .input(CourseCreateArgsSchema) -// // .mutation(async ({ ctx, input }) => { -// // const { staff } = ctx; -// // return await this.courseService.create(input, { staff }); -// // }), -// // update: this.trpc.protectProcedure -// // .input(CourseUpdateArgsSchema) -// // .mutation(async ({ ctx, input }) => { -// // const { staff } = ctx; -// // return await this.courseService.update(input, { staff }); -// // }), -// // createMany: this.trpc.protectProcedure -// // .input(z.array(CourseCreateManyInputSchema)) -// // .mutation(async ({ ctx, input }) => { -// // const { staff } = ctx; -// // return await this.courseService.createMany({ data: input }, staff); -// // }), -// // deleteMany: this.trpc.procedure -// // .input(CourseDeleteManyArgsSchema) -// // .mutation(async ({ input }) => { -// // return await this.courseService.deleteMany(input); -// // }), -// // findFirst: this.trpc.procedure -// // .input(CourseFindFirstArgsSchema) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword -// // .query(async ({ input }) => { -// // return await this.courseService.findFirst(input); -// // }), -// // softDeleteByIds: this.trpc.protectProcedure -// // .input(z.object({ ids: z.array(z.string()) })) // expect input according to the schema -// // .mutation(async ({ input }) => { -// // return this.courseService.softDeleteByIds(input.ids); -// // }), -// // updateOrder: this.trpc.protectProcedure -// // .input(UpdateOrderSchema) -// // .mutation(async ({ input }) => { -// // return this.courseService.updateOrder(input); -// // }), -// // findMany: this.trpc.procedure -// // .input(CourseFindManyArgsSchema) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword -// // .query(async ({ input }) => { -// // return await this.courseService.findMany(input); -// // }), -// // findManyWithCursor: this.trpc.protectProcedure -// // .input( -// // z.object({ -// // cursor: z.any().nullish(), -// // take: z.number().optional(), -// // where: CourseWhereInputSchema.optional(), -// // select: CourseSelectSchema.optional(), -// // }), -// // ) -// // .query(async ({ ctx, input }) => { -// // return await this.courseService.findManyWithCursor(input); -// // }), -// // findManyWithPagination: this.trpc.procedure -// // .input( -// // z.object({ -// // page: z.number().optional(), -// // pageSize: z.number().optional(), -// // where: CourseWhereInputSchema.optional(), -// // select: CourseSelectSchema.optional(), -// // }), -// // ) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword -// // .query(async ({ input }) => { -// // return await this.courseService.findManyWithPagination(input); -// // }), -// }); -// } diff --git a/apps/server/src/models/course/course.schema.ts b/apps/server/src/models/course/course.schema.ts deleted file mode 100755 index e69de29..0000000 diff --git a/apps/server/src/models/course/course.service.ts b/apps/server/src/models/course/course.service.ts deleted file mode 100755 index 55b1038..0000000 --- a/apps/server/src/models/course/course.service.ts +++ /dev/null @@ -1,78 +0,0 @@ -// import { Injectable } from '@nestjs/common'; -// import { BaseService } from '../base/base.service'; -// import { -// UserProfile, -// db, -// ObjectType, -// Prisma, -// InstructorRole, -// } from '@nice/common'; -// @Injectable() -// export class CourseService extends BaseService { -// constructor() { -// super(db, ObjectType.COURSE); -// } -// async create( -// args: Prisma.CourseCreateArgs, -// params?: { staff?: UserProfile } -// ) { -// return await db.$transaction(async tx => { -// const result = await super.create(args, { tx }); -// if (params?.staff?.id) { -// await tx.courseInstructor.create({ -// data: { -// instructorId: params.staff.id, -// courseId: result.id, -// role: InstructorRole.MAIN, -// } -// }); -// } -// return result; -// }, { -// timeout: 10000 // 10 seconds -// }); -// } -// async update( -// args: Prisma.CourseUpdateArgs, -// params?: { staff?: UserProfile } -// ) { -// return await db.$transaction(async tx => { -// const result = await super.update(args, { tx }); -// return result; -// }, { -// timeout: 10000 // 10 seconds -// }); -// } -// async removeInstructor(courseId: string, instructorId: string) { -// return await db.courseInstructor.delete({ -// where: { -// courseId_instructorId: { -// courseId, -// instructorId, -// }, -// }, -// }); -// } -// async addInstructor(params: { -// courseId: string; -// instructorId: string; -// role?: string; -// order?: number; -// }) { -// return await db.courseInstructor.create({ -// data: { -// courseId: params.courseId, -// instructorId: params.instructorId, -// role: params.role || InstructorRole.ASSISTANT, -// order: params.order, -// }, -// }); -// } -// async getInstructors(courseId: string) { -// return await db.courseInstructor.findMany({ -// where: { courseId }, -// include: { instructor: true }, -// orderBy: { order: 'asc' }, -// }); -// } -// } diff --git a/apps/server/src/models/course/utils.ts b/apps/server/src/models/course/utils.ts deleted file mode 100755 index aef7966..0000000 --- a/apps/server/src/models/course/utils.ts +++ /dev/null @@ -1,49 +0,0 @@ -// import { db, EnrollmentStatus, PostType } from '@nice/common'; - -// // 更新课程评价统计 -// export async function updateCourseReviewStats(courseId: string) { -// const reviews = await db.post.findMany({ -// where: { -// courseId, -// type: PostType.COURSE_REVIEW, -// deletedAt: null, -// }, -// select: { rating: true }, -// }); -// const numberOfReviews = reviews.length; -// const averageRating = -// numberOfReviews > 0 -// ? reviews.reduce((sum, review) => sum + review.rating, 0) / -// numberOfReviews -// : 0; - -// return db.course.update({ -// where: { id: courseId }, -// data: { -// // numberOfReviews, -// //averageRating, -// }, -// }); -// } - -// // 更新课程注册统计 -// export async function updateCourseEnrollmentStats(courseId: string) { -// const completedEnrollments = await db.enrollment.count({ -// where: { -// courseId, -// status: EnrollmentStatus.COMPLETED, -// }, -// }); -// const totalEnrollments = await db.enrollment.count({ -// where: { courseId }, -// }); -// const completionRate = -// totalEnrollments > 0 ? (completedEnrollments / totalEnrollments) * 100 : 0; -// return db.course.update({ -// where: { id: courseId }, -// data: { -// // numberOfStudents: totalEnrollments, -// // completionRate, -// }, -// }); -// } diff --git a/apps/server/src/models/lecture/lecture.module.ts b/apps/server/src/models/lecture/lecture.module.ts deleted file mode 100755 index 71f6573..0000000 --- a/apps/server/src/models/lecture/lecture.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -// import { Module } from '@nestjs/common'; -// import { LectureRouter } from './lecture.router'; -// import { LectureService } from './lecture.service'; -// import { TrpcService } from '@server/trpc/trpc.service'; - -// @Module({ -// providers: [LectureRouter, LectureService, TrpcService], -// exports: [LectureRouter, LectureService] -// }) -// export class LectureModule { } diff --git a/apps/server/src/models/lecture/lecture.router.ts b/apps/server/src/models/lecture/lecture.router.ts deleted file mode 100755 index 832c8a6..0000000 --- a/apps/server/src/models/lecture/lecture.router.ts +++ /dev/null @@ -1,70 +0,0 @@ -// import { Injectable } from '@nestjs/common'; -// import { TrpcService } from '@server/trpc/trpc.service'; -// import { Prisma, UpdateOrderSchema } from '@nice/common'; -// import { LectureService } from './lecture.service'; -// import { z, ZodType } from 'zod'; -// const LectureCreateArgsSchema: ZodType = z.any() -// const LectureCreateManyInputSchema: ZodType = z.any() -// const LectureDeleteManyArgsSchema: ZodType = z.any() -// const LectureFindManyArgsSchema: ZodType = z.any() -// const LectureFindFirstArgsSchema: ZodType = z.any() -// const LectureWhereInputSchema: ZodType = z.any() -// const LectureSelectSchema: ZodType = z.any() - -// @Injectable() -// export class LectureRouter { -// constructor( -// private readonly trpc: TrpcService, -// private readonly lectureService: LectureService, -// ) { } -// router = this.trpc.router({ -// create: this.trpc.protectProcedure -// .input(LectureCreateArgsSchema) -// .mutation(async ({ ctx, input }) => { -// const { staff } = ctx; -// return await this.lectureService.create(input, {staff}); -// }), -// createMany: this.trpc.protectProcedure.input(z.array(LectureCreateManyInputSchema)) -// .mutation(async ({ ctx, input }) => { -// const { staff } = ctx; - -// return await this.lectureService.createMany({ data: input }, staff); -// }), -// deleteMany: this.trpc.procedure -// .input(LectureDeleteManyArgsSchema) -// .mutation(async ({ input }) => { -// return await this.lectureService.deleteMany(input); -// }), -// findFirst: this.trpc.procedure -// .input(LectureFindFirstArgsSchema) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword -// .query(async ({ input }) => { -// return await this.lectureService.findFirst(input); -// }), -// softDeleteByIds: this.trpc.protectProcedure -// .input(z.object({ ids: z.array(z.string()) })) // expect input according to the schema -// .mutation(async ({ input }) => { -// return this.lectureService.softDeleteByIds(input.ids); -// }), -// updateOrder: this.trpc.protectProcedure -// .input(UpdateOrderSchema) -// .mutation(async ({ input }) => { -// return this.lectureService.updateOrder(input); -// }), -// findMany: this.trpc.procedure -// .input(LectureFindManyArgsSchema) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword -// .query(async ({ input }) => { -// return await this.lectureService.findMany(input); -// }), -// findManyWithCursor: this.trpc.protectProcedure -// .input(z.object({ -// cursor: z.any().nullish(), -// take: z.number().nullish(), -// where: LectureWhereInputSchema.nullish(), -// select: LectureSelectSchema.nullish() -// })) -// .query(async ({ ctx, input }) => { -// const { staff } = ctx; -// return await this.lectureService.findManyWithCursor(input); -// }), -// }); -// } diff --git a/apps/server/src/models/lecture/lecture.service.ts b/apps/server/src/models/lecture/lecture.service.ts deleted file mode 100755 index 4eb7250..0000000 --- a/apps/server/src/models/lecture/lecture.service.ts +++ /dev/null @@ -1,35 +0,0 @@ -// import { Injectable } from '@nestjs/common'; -// import { BaseService } from '../base/base.service'; -// import { -// UserProfile, -// db, -// ObjectType, -// Prisma -// } from '@nice/common'; -// import EventBus, { CrudOperation } from '@server/utils/event-bus'; - -// @Injectable() -// export class LectureService extends BaseService { -// constructor() { -// super(db, ObjectType.COURSE); -// } -// async create(args: Prisma.LectureCreateArgs, params?: { staff?: UserProfile }) { -// const result = await super.create(args) -// EventBus.emit('dataChanged', { -// type: ObjectType.LECTURE, -// operation: CrudOperation.CREATED, -// data: result, -// }); -// return result; -// } -// async update(args: Prisma.LectureUpdateArgs) { -// const result = await super.update(args); -// EventBus.emit('dataChanged', { -// type: ObjectType.LECTURE, -// operation: CrudOperation.UPDATED, -// data: result, -// }); -// return result; -// } - -// } diff --git a/apps/server/src/models/lecture/utils.ts b/apps/server/src/models/lecture/utils.ts deleted file mode 100755 index dc0c823..0000000 --- a/apps/server/src/models/lecture/utils.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { db, PostType } from '@nice/common'; - -// export async function updateSectionLectureStats(sectionId: string) { -// const sectionStats = await db.post.aggregate({ -// where: { -// parentId: sectionId, -// deletedAt: null, -// type: PostType.LECTURE, -// }, -// _count: { _all: true }, -// _sum: { duration: true }, -// }); - -// await db.post.update({ -// where: { id: sectionId }, -// data: { -// // totalLectures: sectionStats._count._all, -// // totalDuration: sectionStats._sum.duration || 0, -// }, -// }); -// } - -// export async function updateParentLectureStats(parentId: string) { -// const ParentStats = await db.post.aggregate({ -// where: { -// ancestors: { -// some: { -// ancestorId: parentId, -// descendant: { -// type: PostType.LECTURE, -// deletedAt: null, -// }, -// }, -// }, -// }, -// _count: { _all: true }, -// _sum: { -// duration: true, -// }, -// }); -// await db.post.update({ -// where: { id: parentId }, -// data: { -// //totalLectures: courseStats._count._all, -// //totalDuration: courseStats._sum.duration || 0, -// }, -// }); -// } diff --git a/apps/server/src/models/section/section.module.ts b/apps/server/src/models/section/section.module.ts deleted file mode 100755 index a337cb6..0000000 --- a/apps/server/src/models/section/section.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -// import { Module } from '@nestjs/common'; -// import { SectionRouter } from './section.router'; -// import { SectionService } from './section.service'; -// import { TrpcService } from '@server/trpc/trpc.service'; - -// @Module({ -// exports: [SectionRouter, SectionService], -// providers: [SectionRouter, SectionService, TrpcService] -// }) -// export class SectionModule { } diff --git a/apps/server/src/models/section/section.router.ts b/apps/server/src/models/section/section.router.ts deleted file mode 100755 index 2658567..0000000 --- a/apps/server/src/models/section/section.router.ts +++ /dev/null @@ -1,70 +0,0 @@ -// import { Injectable } from '@nestjs/common'; -// import { TrpcService } from '@server/trpc/trpc.service'; -// import { Prisma, UpdateOrderSchema } from '@nice/common'; -// import { SectionService } from './section.service'; -// import { z, ZodType } from 'zod'; -// const SectionCreateArgsSchema: ZodType = z.any() -// const SectionCreateManyInputSchema: ZodType = z.any() -// const SectionDeleteManyArgsSchema: ZodType = z.any() -// const SectionFindManyArgsSchema: ZodType = z.any() -// const SectionFindFirstArgsSchema: ZodType = z.any() -// const SectionWhereInputSchema: ZodType = z.any() -// const SectionSelectSchema: ZodType = z.any() - -// @Injectable() -// export class SectionRouter { -// constructor( -// private readonly trpc: TrpcService, -// private readonly sectionService: SectionService, -// ) { } -// router = this.trpc.router({ -// create: this.trpc.protectProcedure -// .input(SectionCreateArgsSchema) -// .mutation(async ({ ctx, input }) => { -// const { staff } = ctx; -// return await this.sectionService.create(input, { staff }); -// }), -// createMany: this.trpc.protectProcedure.input(z.array(SectionCreateManyInputSchema)) -// .mutation(async ({ ctx, input }) => { -// const { staff } = ctx; - -// return await this.sectionService.createMany({ data: input }, staff); -// }), -// deleteMany: this.trpc.procedure -// .input(SectionDeleteManyArgsSchema) -// .mutation(async ({ input }) => { -// return await this.sectionService.deleteMany(input); -// }), -// findFirst: this.trpc.procedure -// .input(SectionFindFirstArgsSchema) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword -// .query(async ({ input }) => { -// return await this.sectionService.findFirst(input); -// }), -// softDeleteByIds: this.trpc.protectProcedure -// .input(z.object({ ids: z.array(z.string()) })) // expect input according to the schema -// .mutation(async ({ input }) => { -// return this.sectionService.softDeleteByIds(input.ids); -// }), -// updateOrder: this.trpc.protectProcedure -// .input(UpdateOrderSchema) -// .mutation(async ({ input }) => { -// return this.sectionService.updateOrder(input); -// }), -// findMany: this.trpc.procedure -// .input(SectionFindManyArgsSchema) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword -// .query(async ({ input }) => { -// return await this.sectionService.findMany(input); -// }), -// findManyWithCursor: this.trpc.protectProcedure -// .input(z.object({ -// cursor: z.any().nullish(), -// take: z.number().nullish(), -// where: SectionWhereInputSchema.nullish(), -// select: SectionSelectSchema.nullish() -// })) -// .query(async ({ ctx, input }) => { -// const { staff } = ctx; -// return await this.sectionService.findManyWithCursor(input); -// }), -// }); -// } diff --git a/apps/server/src/models/section/section.service.ts b/apps/server/src/models/section/section.service.ts deleted file mode 100755 index 177cf4d..0000000 --- a/apps/server/src/models/section/section.service.ts +++ /dev/null @@ -1,23 +0,0 @@ -// import { Injectable } from '@nestjs/common'; -// import { BaseService } from '../base/base.service'; -// import { -// UserProfile, -// db, -// ObjectType, -// Prisma, - -// } from '@nice/common'; -// @Injectable() -// export class SectionService extends BaseService { -// constructor() { -// super(db, ObjectType.SECTION); -// } - -// create(args: Prisma.SectionCreateArgs, params?: { staff?: UserProfile }) { -// return super.create(args) -// } -// async update(args: Prisma.SectionUpdateArgs) { -// return super.update(args); -// } - -// } diff --git a/apps/server/src/models/visit/visit.router.ts b/apps/server/src/models/visit/visit.router.ts index 2bb9064..ad6d632 100755 --- a/apps/server/src/models/visit/visit.router.ts +++ b/apps/server/src/models/visit/visit.router.ts @@ -4,34 +4,34 @@ import { Prisma } from '@nice/common'; import { VisitService } from './visit.service'; import { z, ZodType } from 'zod'; -const VisitCreateArgsSchema: ZodType = z.any() -const VisitCreateManyInputSchema: ZodType = z.any() -const VisitDeleteManyArgsSchema: ZodType = z.any() +const VisitCreateArgsSchema: ZodType = z.any(); +const VisitCreateManyInputSchema: ZodType = + z.any(); +const VisitDeleteManyArgsSchema: ZodType = z.any(); @Injectable() export class VisitRouter { - constructor( - private readonly trpc: TrpcService, - private readonly visitService: VisitService, - ) { } - router = this.trpc.router({ - create: this.trpc.protectProcedure - .input(VisitCreateArgsSchema) - .mutation(async ({ ctx, input }) => { - const { staff } = ctx; - return await this.visitService.create(input, staff); - }), - createMany: this.trpc.protectProcedure.input(z.array(VisitCreateManyInputSchema)) - .mutation(async ({ ctx, input }) => { - const { staff } = ctx; + constructor( + private readonly trpc: TrpcService, + private readonly visitService: VisitService, + ) {} + router = this.trpc.router({ + create: this.trpc.protectProcedure + .input(VisitCreateArgsSchema) + .mutation(async ({ ctx, input }) => { + const { staff } = ctx; + return await this.visitService.create(input, staff); + }), + createMany: this.trpc.protectProcedure + .input(z.array(VisitCreateManyInputSchema)) + .mutation(async ({ ctx, input }) => { + const { staff } = ctx; - return await this.visitService.createMany({ data: input }, staff); - }), - deleteMany: this.trpc.procedure - .input(VisitDeleteManyArgsSchema) - .mutation(async ({ input }) => { - return await this.visitService.deleteMany(input); - }), - - - }); + return await this.visitService.createMany({ data: input }, staff); + }), + deleteMany: this.trpc.procedure + .input(VisitDeleteManyArgsSchema) + .mutation(async ({ input }) => { + return await this.visitService.deleteMany(input); + }), + }); } diff --git a/apps/server/src/models/visit/visit.service.ts b/apps/server/src/models/visit/visit.service.ts index a7f2ada..4742c61 100755 --- a/apps/server/src/models/visit/visit.service.ts +++ b/apps/server/src/models/visit/visit.service.ts @@ -30,12 +30,17 @@ export class VisitService extends BaseService { }); } - // if (troubleId && args.data.type === VisitType.READED) { - // EventBus.emit('updateViewCount', { - // objectType: ObjectType.TROUBLE, - // id: troubleId, - // }); - // } + if ( + [VisitType.READED, VisitType.LIKE, VisitType.HATE].includes( + args.data.type as VisitType, + ) + ) { + EventBus.emit('updateVisitCount', { + objectType: ObjectType.POST, + id: postId, + visitType: args.data.type, // 直接复用传入的类型 + }); + } return result; } async createMany(args: Prisma.VisitCreateManyArgs, staff?: UserProfile) { @@ -80,4 +85,61 @@ export class VisitService extends BaseService { return { count: updatePromises.length }; // Return the number of updates if no new creates } + async deleteMany(args: Prisma.VisitDeleteManyArgs, staff?: UserProfile) { + // const where = Array.isArray(args.where) ? args.where : [args.where]; + // const updatePromises: any[] = []; + // const createData: Prisma.VisitCreateManyInput[] = []; + // super + // await Promise.all( + // data.map(async (item) => { + // if (staff && !item.visitorId) item.visitorId = staff.id; + // const { postId, messageId, visitorId } = item; + // const existingVisit = await db.visit.findFirst({ + // where: { + // visitorId, + // OR: [{ postId }, { messageId }], + // }, + // }); + + // if (existingVisit) { + // updatePromises.push( + // super.update({ + // where: { id: existingVisit.id }, + // data: { + // ...item, + // views: existingVisit.views + 1, + // }, + // }), + // ); + // } else { + // createData.push(item); + // } + // }), + // ); + // // Execute all updates in parallel + // await Promise.all(updatePromises); + // // Create new visits for those not existing + // if (createData.length > 0) { + // return super.createMany({ + // ...args, + // data: createData, + // }); + // } + // return { count: updatePromises.length }; // Return the number of updates if no new creates + const superDetele = super.deleteMany(args, staff); + if (args?.where?.postId) { + if ( + [VisitType.READED, VisitType.LIKE, VisitType.HATE].includes( + args.where.type as any, + ) + ) { + EventBus.emit('updateVisitCount', { + objectType: ObjectType.POST, + id: args?.where?.postId as string, + visitType: args.where.type as any, // 直接复用传入的类型 + }); + } + } + return superDetele; + } } diff --git a/apps/server/src/queue/models/post/post.queue.service.ts b/apps/server/src/queue/models/post/post.queue.service.ts new file mode 100644 index 0000000..f7370df --- /dev/null +++ b/apps/server/src/queue/models/post/post.queue.service.ts @@ -0,0 +1,40 @@ +import { InjectQueue } from '@nestjs/bullmq'; +import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { Queue } from 'bullmq'; +import EventBus from '@server/utils/event-bus'; +import { ObjectType, VisitType } from '@nice/common'; +import { + QueueJobType, + updatePostStateJobData, + updateVisitCountJobData, +} from '@server/queue/types'; + +@Injectable() +export class PostQueueService implements OnModuleInit { + private readonly logger = new Logger(PostQueueService.name); + constructor(@InjectQueue('general') private generalQueue: Queue) {} + onModuleInit() { + EventBus.on('updateVisitCount', ({ id, objectType, visitType }) => { + if (objectType === ObjectType.POST) { + this.addUpdateVisitCountJob({ id, type: visitType }); + } + }); + EventBus.on('updatePostState', ({ id }) => { + this.addUpdatePostState({ id }); + }); + } + async addUpdateVisitCountJob(data: updateVisitCountJobData) { + this.logger.log(`update post view count ${data.id}`); + await this.generalQueue.add(QueueJobType.UPDATE_POST_VISIT_COUNT, data, { + debounce: { + id: `${QueueJobType.UPDATE_POST_VISIT_COUNT}_${data.type}_${data.id}`, + }, + }); + } + async addUpdatePostState(data: updatePostStateJobData) { + this.logger.log(`update post state ${data.id}`); + await this.generalQueue.add(QueueJobType.UPDATE_POST_STATE, data, { + debounce: { id: `${QueueJobType.UPDATE_POST_STATE}_${data.id}` }, + }); + } +} diff --git a/apps/server/src/queue/models/post/utils.ts b/apps/server/src/queue/models/post/utils.ts new file mode 100644 index 0000000..8546c66 --- /dev/null +++ b/apps/server/src/queue/models/post/utils.ts @@ -0,0 +1,46 @@ +import { db, VisitType } from '@nice/common'; +export async function updatePostViewCount(id: string, type: VisitType) { + const totalViews = await db.visit.aggregate({ + _sum: { + views: true, + }, + where: { + postId: id, + type: type, + }, + }); + if (type === VisitType.READED) { + await db.post.update({ + where: { + id: id, + }, + data: { + meta: { + views: totalViews._sum.views || 0, + }, // Use 0 if no visits exist + }, + }); + } else if (type === VisitType.LIKE) { + await db.post.update({ + where: { + id: id, + }, + data: { + meta: { + likes: totalViews._sum.views || 0, // Use 0 if no visits exist + }, + }, + }); + } else if (type === VisitType.HATE) { + await db.post.update({ + where: { + id: id, + }, + data: { + meta: { + hates: totalViews._sum.views || 0, // Use 0 if no visits exist + }, + }, + }); + } +} diff --git a/apps/server/src/queue/queue.module.ts b/apps/server/src/queue/queue.module.ts index ca825eb..aab8453 100755 --- a/apps/server/src/queue/queue.module.ts +++ b/apps/server/src/queue/queue.module.ts @@ -2,6 +2,7 @@ import { BullModule } from '@nestjs/bullmq'; import { Logger, Module } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { join } from 'path'; +import { PostQueueService } from './models/post/post.queue.service'; @Module({ imports: [ @@ -28,7 +29,7 @@ import { join } from 'path'; }, ), ], - providers: [Logger], + providers: [Logger, PostQueueService], exports: [], }) export class QueueModule {} diff --git a/apps/server/src/queue/types.ts b/apps/server/src/queue/types.ts index c509c71..7e0f308 100755 --- a/apps/server/src/queue/types.ts +++ b/apps/server/src/queue/types.ts @@ -1,4 +1,14 @@ +import { VisitType } from '@nice/common'; export enum QueueJobType { UPDATE_STATS = 'update_stats', FILE_PROCESS = 'file_process', + UPDATE_POST_VISIT_COUNT = 'updatePostVisitCount', + UPDATE_POST_STATE = 'updatePostState', } +export type updateVisitCountJobData = { + id: string; + type: VisitType | string; +}; +export type updatePostStateJobData = { + id: string; +}; diff --git a/apps/server/src/utils/event-bus.ts b/apps/server/src/utils/event-bus.ts index 8cc9c2e..dfb3409 100755 --- a/apps/server/src/utils/event-bus.ts +++ b/apps/server/src/utils/event-bus.ts @@ -1,16 +1,28 @@ import mitt from 'mitt'; -import { ObjectType, UserProfile, MessageDto } from '@nice/common'; +import { ObjectType, UserProfile, MessageDto, VisitType } from '@nice/common'; export enum CrudOperation { CREATED, UPDATED, - DELETED + DELETED, } type Events = { - genDataEvent: { type: "start" | "end" }, - markDirty: { objectType: string, id: string, staff?: UserProfile, subscribers?: string[] } - updateViewCount: { id: string, objectType: ObjectType }, - onMessageCreated: { data: Partial }, - dataChanged: { type: string, operation: CrudOperation, data: any } + genDataEvent: { type: 'start' | 'end' }; + markDirty: { + objectType: string; + id: string; + staff?: UserProfile; + subscribers?: string[]; + }; + updateVisitCount: { + id: string; + objectType: ObjectType; + visitType: VisitType | string; + }; + updatePostState: { + id: string; + }; + onMessageCreated: { data: Partial }; + dataChanged: { type: string; operation: CrudOperation; data: any }; }; const EventBus = mitt(); export default EventBus; diff --git a/apps/web/src/components/models/course/detail/CourseDetailContext.tsx b/apps/web/src/components/models/course/detail/CourseDetailContext.tsx index 62e1be6..890a331 100755 --- a/apps/web/src/components/models/course/detail/CourseDetailContext.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetailContext.tsx @@ -1,5 +1,11 @@ -import { api } from "@nice/client"; -import { courseDetailSelect, CourseDto, Lecture } from "@nice/common"; +import { api, useVisitor } from "@nice/client"; +import { + courseDetailSelect, + CourseDto, + Lecture, + VisitType, +} from "@nice/common"; +import { useAuth } from "@web/src/providers/auth-provider"; import React, { createContext, ReactNode, useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; @@ -25,6 +31,8 @@ export function CourseDetailProvider({ editId, }: CourseFormProviderProps) { const navigate = useNavigate(); + const { read } = useVisitor(); + const { user } = useAuth(); const { data: course, isLoading }: { data: CourseDto; isLoading: boolean } = (api.post as any).findFirst.useQuery( { @@ -36,6 +44,7 @@ export function CourseDetailProvider({ }, { enabled: Boolean(editId) } ); + const [selectedLectureId, setSelectedLectureId] = useState< string | undefined >(undefined); @@ -47,6 +56,17 @@ export function CourseDetailProvider({ }, { enabled: Boolean(editId) } ); + useEffect(() => { + if (course) { + read.mutateAsync({ + data: { + visitorId: user?.id || null, + postId: course.id, + type: VisitType.READED, + }, + }); + } + }, [course]); useEffect(() => { navigate(`/course/${editId}/detail/${selectedLectureId}`); }, [selectedLectureId, editId]); diff --git a/apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader.tsx b/apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader.tsx index 9cc983d..88657a2 100755 --- a/apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader.tsx @@ -1,58 +1,86 @@ -// components/Header.tsx -import { motion, useScroll, useTransform } from "framer-motion"; -import { useContext, useEffect, useState } from "react"; +import { useContext, useState } from "react"; +import { Input, Layout, Avatar, Button, Dropdown } from "antd"; +import { + EditFilled, + HomeOutlined, + SearchOutlined, + UserOutlined, +} from "@ant-design/icons"; +import { useAuth } from "@web/src/providers/auth-provider"; +import { useNavigate } from "react-router-dom"; +import { UserMenu } from "@web/src/components/layout/element/usermenu/usermenu"; import { CourseDetailContext } from "../CourseDetailContext"; -import { Button } from "antd"; -export const CourseDetailHeader = () => { - const { scrollY } = useScroll(); - - const [lastScrollY, setLastScrollY] = useState(0); - const { course, isHeaderVisible, setIsHeaderVisible, lecture } = - useContext(CourseDetailContext); - useEffect(() => { - const updateHeader = () => { - const current = scrollY.get(); - const direction = current > lastScrollY ? "down" : "up"; - - if (direction === "down" && current > 100) { - setIsHeaderVisible(false); - } else if (direction === "up") { - setIsHeaderVisible(true); - } - - setLastScrollY(current); - }; - - // 使用 requestAnimationFrame 来优化性能 - const unsubscribe = scrollY.on("change", () => { - requestAnimationFrame(updateHeader); - }); - - return () => { - unsubscribe(); - }; - }, [lastScrollY, scrollY, setIsHeaderVisible]); +const { Header } = Layout; +export function CourseDetailHeader() { + const [searchValue, setSearchValue] = useState(""); + const { isAuthenticated, user } = useAuth(); + const navigate = useNavigate(); + const { course } = useContext(CourseDetailContext); + return ( - -
-
-

{course?.title}

+
+
+
+ { + navigate("/"); + }} + className="text-2xl text-primary-500 hover:scale-105 cursor-pointer" + /> + +
+ {course?.title} +
+ {/* */} +
+
+
+ + } + placeholder="搜索课程" + className="w-72 rounded-full" + value={searchValue} + onChange={(e) => setSearchValue(e.target.value)} + /> +
+ {isAuthenticated && ( + <> + + + )} + {isAuthenticated ? ( + } + trigger={["click"]} + placement="bottomRight"> + + {(user?.showname || + user?.username || + "")[0]?.toUpperCase()} + + + ) : ( + + )}
- -
- +
); -}; - -export default CourseDetailHeader; +} diff --git a/apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader_BACKUP.tsx b/apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader_BACKUP.tsx new file mode 100755 index 0000000..0fc9815 --- /dev/null +++ b/apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader_BACKUP.tsx @@ -0,0 +1,77 @@ +// // components/Header.tsx +// import { motion, useScroll, useTransform } from "framer-motion"; +// import { useContext, useEffect, useState } from "react"; +// import { CourseDetailContext } from "../CourseDetailContext"; +// import { Avatar, Button, Dropdown } from "antd"; +// import { UserOutlined } from "@ant-design/icons"; +// import { UserMenu } from "@web/src/app/main/layout/UserMenu"; +// import { useAuth } from "@web/src/providers/auth-provider"; + +// export const CourseDetailHeader = () => { +// const { scrollY } = useScroll(); +// const { user, isAuthenticated } = useAuth(); +// const [lastScrollY, setLastScrollY] = useState(0); +// const { course, isHeaderVisible, setIsHeaderVisible, lecture } = +// useContext(CourseDetailContext); +// useEffect(() => { +// const updateHeader = () => { +// const current = scrollY.get(); +// const direction = current > lastScrollY ? "down" : "up"; + +// if (direction === "down" && current > 100) { +// setIsHeaderVisible(false); +// } else if (direction === "up") { +// setIsHeaderVisible(true); +// } + +// setLastScrollY(current); +// }; + +// // 使用 requestAnimationFrame 来优化性能 +// const unsubscribe = scrollY.on("change", () => { +// requestAnimationFrame(updateHeader); +// }); + +// return () => { +// unsubscribe(); +// }; +// }, [lastScrollY, scrollY, setIsHeaderVisible]); + +// return ( +// +//
+//
+//

{course?.title}

+//
+ +// {isAuthenticated ? ( +// } +// trigger={["click"]} +// placement="bottomRight"> +// +// {(user?.showname || +// user?.username || +// "")[0]?.toUpperCase()} +// +// +// ) : ( +// +// )} +//
+//
+// ); +// }; + +// export default CourseDetailHeader; diff --git a/apps/web/src/components/models/course/detail/CourseDetailLayout.tsx b/apps/web/src/components/models/course/detail/CourseDetailLayout.tsx index 64368a0..02316bb 100755 --- a/apps/web/src/components/models/course/detail/CourseDetailLayout.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetailLayout.tsx @@ -4,8 +4,7 @@ import { CourseDetailContext } from "./CourseDetailContext"; import CourseDetailDisplayArea from "./CourseDetailDisplayArea"; import { CourseSyllabus } from "./CourseSyllabus/CourseSyllabus"; -import CourseDetailHeader from "./CourseDetailHeader/CourseDetailHeader"; -import { Button } from "antd"; +import { CourseDetailHeader } from "./CourseDetailHeader/CourseDetailHeader"; export default function CourseDetailLayout() { const { diff --git a/apps/web/src/components/models/course/detail/CourseSyllabus/LectureItem.tsx b/apps/web/src/components/models/course/detail/CourseSyllabus/LectureItem.tsx index 0999f3c..a79b16e 100755 --- a/apps/web/src/components/models/course/detail/CourseSyllabus/LectureItem.tsx +++ b/apps/web/src/components/models/course/detail/CourseSyllabus/LectureItem.tsx @@ -2,7 +2,11 @@ import { Lecture, LectureType } from "@nice/common"; import React from "react"; -import { ClockCircleOutlined, FileTextOutlined, PlayCircleOutlined } from "@ant-design/icons"; // 使用 Ant Design 图标 +import { + ClockCircleOutlined, + FileTextOutlined, + PlayCircleOutlined, +} from "@ant-design/icons"; // 使用 Ant Design 图标 interface LectureItemProps { lecture: Lecture; @@ -28,9 +32,9 @@ export const LectureItem: React.FC = ({

{lecture.subTitle}

)}
-
+ {/*
{lecture.duration}分钟 -
+
*/}
); diff --git a/apps/web/src/components/models/course/editor/context/CourseEditorContext.tsx b/apps/web/src/components/models/course/editor/context/CourseEditorContext.tsx index 5327822..5ee54b3 100755 --- a/apps/web/src/components/models/course/editor/context/CourseEditorContext.tsx +++ b/apps/web/src/components/models/course/editor/context/CourseEditorContext.tsx @@ -66,7 +66,9 @@ export function CourseFormProvider({ title: course.title, subTitle: course.subTitle, content: course.content, - thumbnail: course?.meta?.thumbnail, + meta: { + thumbnail: course?.meta?.thumbnail, + }, }; course.terms?.forEach((term) => { formData[term.taxonomyId] = term.id; // 假设 taxonomyName 是您在 Form.Item 中使用的 name @@ -85,8 +87,7 @@ export function CourseFormProvider({ const formattedValues = { ...values, meta: { - requirements: values.requirements, - objectives: values.objectives, + thumbnail: values.thumbnail, }, terms: { connect: termIds.map((id) => ({ id })), // 转换成 connect 格式 @@ -96,8 +97,6 @@ export function CourseFormProvider({ taxonomies.forEach((tax) => { delete formattedValues[tax.id]; }); - delete formattedValues.requirements; - delete formattedValues.objectives; delete formattedValues.sections; try { if (editId) { @@ -111,7 +110,7 @@ export function CourseFormProvider({ courseDetail: { data: { title: formattedValues.title || "12345", - state: CourseStatus.DRAFT, + // state: CourseStatus.DRAFT, type: PostType.COURSE, ...formattedValues, }, diff --git a/apps/web/src/components/models/course/editor/form/CourseBasicForm.tsx b/apps/web/src/components/models/course/editor/form/CourseBasicForm.tsx index 333e94d..b05579b 100755 --- a/apps/web/src/components/models/course/editor/form/CourseBasicForm.tsx +++ b/apps/web/src/components/models/course/editor/form/CourseBasicForm.tsx @@ -34,6 +34,14 @@ export function CourseBasicForm() { rules={[{ max: 10, message: "副标题最多10个字符" }]}> + + +