Merge branch 'main' of http://113.45.157.195:3003/insiinc/leader-mail
This commit is contained in:
commit
9e47061e24
|
@ -37,10 +37,8 @@ export class BaseService<
|
|||
constructor(
|
||||
protected prisma: PrismaClient,
|
||||
protected objectType: string,
|
||||
protected enableOrder: boolean = false
|
||||
) {
|
||||
|
||||
}
|
||||
protected enableOrder: boolean = false,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieves the name of the model dynamically.
|
||||
|
@ -51,7 +49,7 @@ export class BaseService<
|
|||
return modelName;
|
||||
}
|
||||
private getModel(tx?: TransactionType): D {
|
||||
return tx?.[this.objectType] || this.prisma[this.objectType] as D;
|
||||
return tx?.[this.objectType] || (this.prisma[this.objectType] as D);
|
||||
}
|
||||
/**
|
||||
* Error handling helper function
|
||||
|
@ -85,7 +83,9 @@ export class BaseService<
|
|||
*/
|
||||
async findUnique(args: A['findUnique']): Promise<R['findUnique']> {
|
||||
try {
|
||||
return this.getModel().findUnique(args as any) as Promise<R['findUnique']>;
|
||||
return this.getModel().findUnique(args as any) as Promise<
|
||||
R['findUnique']
|
||||
>;
|
||||
} catch (error) {
|
||||
this.handleError(error, 'read');
|
||||
}
|
||||
|
@ -152,26 +152,27 @@ export class BaseService<
|
|||
* const newUser = await service.create({ data: { name: 'John Doe' } });
|
||||
*/
|
||||
async create(args: A['create'], params?: any): Promise<R['create']> {
|
||||
|
||||
try {
|
||||
|
||||
if (this.enableOrder && !(args as any).data.order) {
|
||||
// 查找当前最大的 order 值
|
||||
const maxOrderItem = await this.getModel(params?.tx).findFirst({
|
||||
orderBy: { order: 'desc' }
|
||||
}) as any;
|
||||
const maxOrderItem = (await this.getModel(params?.tx).findFirst({
|
||||
orderBy: { order: 'desc' },
|
||||
})) as any;
|
||||
// 设置新记录的 order 值
|
||||
const newOrder = maxOrderItem ? maxOrderItem.order + this.ORDER_INTERVAL : 1;
|
||||
const newOrder = maxOrderItem
|
||||
? maxOrderItem.order + this.ORDER_INTERVAL
|
||||
: 1;
|
||||
// 将 order 添加到创建参数中
|
||||
(args as any).data.order = newOrder;
|
||||
}
|
||||
return this.getModel(params?.tx).create(args as any) as Promise<R['create']>;
|
||||
return this.getModel(params?.tx).create(args as any) as Promise<
|
||||
R['create']
|
||||
>;
|
||||
} catch (error) {
|
||||
this.handleError(error, 'create');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates multiple new records with the given data.
|
||||
* @param args - Arguments to create multiple records.
|
||||
|
@ -179,9 +180,14 @@ export class BaseService<
|
|||
* @example
|
||||
* const newUsers = await service.createMany({ data: [{ name: 'John' }, { name: 'Jane' }] });
|
||||
*/
|
||||
async createMany(args: A['createMany'], params?: any): Promise<R['createMany']> {
|
||||
async createMany(
|
||||
args: A['createMany'],
|
||||
params?: any,
|
||||
): Promise<R['createMany']> {
|
||||
try {
|
||||
return this.getModel(params?.tx).createMany(args as any) as Promise<R['createMany']>;
|
||||
return this.getModel(params?.tx).createMany(args as any) as Promise<
|
||||
R['createMany']
|
||||
>;
|
||||
} catch (error) {
|
||||
this.handleError(error, 'create');
|
||||
}
|
||||
|
@ -196,8 +202,9 @@ export class BaseService<
|
|||
*/
|
||||
async update(args: A['update'], params?: any): Promise<R['update']> {
|
||||
try {
|
||||
|
||||
return this.getModel(params?.tx).update(args as any) as Promise<R['update']>;
|
||||
return this.getModel(params?.tx).update(args as any) as Promise<
|
||||
R['update']
|
||||
>;
|
||||
} catch (error) {
|
||||
this.handleError(error, 'update');
|
||||
}
|
||||
|
@ -251,7 +258,9 @@ export class BaseService<
|
|||
*/
|
||||
async delete(args: A['delete'], params?: any): Promise<R['delete']> {
|
||||
try {
|
||||
return this.getModel(params?.tx).delete(args as any) as Promise<R['delete']>;
|
||||
return this.getModel(params?.tx).delete(args as any) as Promise<
|
||||
R['delete']
|
||||
>;
|
||||
} catch (error) {
|
||||
this.handleError(error, 'delete');
|
||||
}
|
||||
|
@ -309,10 +318,14 @@ export class BaseService<
|
|||
* @example
|
||||
* const deleteResult = await service.deleteMany({ where: { isActive: false } });
|
||||
*/
|
||||
async deleteMany(args: A['deleteMany'], params?: any): Promise<R['deleteMany']> {
|
||||
async deleteMany(
|
||||
args: A['deleteMany'],
|
||||
params?: any,
|
||||
): Promise<R['deleteMany']> {
|
||||
try {
|
||||
|
||||
return this.getModel(params?.tx).deleteMany(args as any) as Promise<R['deleteMany']>;
|
||||
return this.getModel(params?.tx).deleteMany(args as any) as Promise<
|
||||
R['deleteMany']
|
||||
>;
|
||||
} catch (error) {
|
||||
this.handleError(error, 'delete');
|
||||
}
|
||||
|
@ -327,7 +340,9 @@ export class BaseService<
|
|||
*/
|
||||
async updateMany(args: A['updateMany']): Promise<R['updateMany']> {
|
||||
try {
|
||||
return this.getModel().updateMany(args as any) as Promise<R['updateMany']>;
|
||||
return this.getModel().updateMany(args as any) as Promise<
|
||||
R['updateMany']
|
||||
>;
|
||||
} catch (error) {
|
||||
this.handleError(error, 'update');
|
||||
}
|
||||
|
@ -420,8 +435,7 @@ export class BaseService<
|
|||
data: { ...data, deletedAt: null } as any,
|
||||
}) as Promise<R['update'][]>;
|
||||
} catch (error) {
|
||||
this.handleError(error, "update");
|
||||
|
||||
this.handleError(error, 'update');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -436,25 +450,25 @@ export class BaseService<
|
|||
page?: number;
|
||||
pageSize?: number;
|
||||
where?: WhereArgs<A['findMany']>;
|
||||
select?: SelectArgs<A['findMany']>
|
||||
select?: SelectArgs<A['findMany']>;
|
||||
}): Promise<{ items: R['findMany']; totalPages: number }> {
|
||||
const { page = 1, pageSize = 10, where, select } = args;
|
||||
|
||||
try {
|
||||
// 获取总记录数
|
||||
const total = await this.getModel().count({ where }) as number;
|
||||
const total = (await this.getModel().count({ where })) as number;
|
||||
// 获取分页数据
|
||||
const items = await this.getModel().findMany({
|
||||
const items = (await this.getModel().findMany({
|
||||
where,
|
||||
select,
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
} as any) as R['findMany'];
|
||||
} as any)) as R['findMany'];
|
||||
// 计算总页数
|
||||
const totalPages = Math.ceil(total / pageSize);
|
||||
return {
|
||||
items,
|
||||
totalPages
|
||||
totalPages,
|
||||
};
|
||||
} catch (error) {
|
||||
this.handleError(error, 'read');
|
||||
|
@ -483,10 +497,9 @@ export class BaseService<
|
|||
: undefined,
|
||||
} as any)) as any[];
|
||||
|
||||
|
||||
/**
|
||||
* 处理下一页游标
|
||||
* @description
|
||||
* @description
|
||||
* 1. 如果查到的记录数超过take,说明还有下一页
|
||||
* 2. 将最后一条记录弹出,用其updatedAt和id构造下一页游标
|
||||
* 3. 游标格式为: updatedAt_id
|
||||
|
@ -502,7 +515,7 @@ export class BaseService<
|
|||
|
||||
/**
|
||||
* 返回查询结果
|
||||
* @returns {Object}
|
||||
* @returns {Object}
|
||||
* - items: 当前页记录
|
||||
* - totalCount: 总记录数
|
||||
* - nextCursor: 下一页游标
|
||||
|
@ -530,7 +543,7 @@ export class BaseService<
|
|||
order: { gt: targetObject.order },
|
||||
deletedAt: null,
|
||||
},
|
||||
orderBy: { order: 'asc' }
|
||||
orderBy: { order: 'asc' },
|
||||
} as any)) as any;
|
||||
|
||||
const newOrder = nextObject
|
||||
|
|
|
@ -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 { }
|
|
@ -1,86 +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<Prisma.CourseCreateArgs> = z.any()
|
||||
const CourseUpdateArgsSchema: ZodType<Prisma.CourseUpdateArgs> = z.any()
|
||||
const CourseCreateManyInputSchema: ZodType<Prisma.CourseCreateManyInput> = z.any()
|
||||
const CourseDeleteManyArgsSchema: ZodType<Prisma.CourseDeleteManyArgs> = z.any()
|
||||
const CourseFindManyArgsSchema: ZodType<Prisma.CourseFindManyArgs> = z.any()
|
||||
const CourseFindFirstArgsSchema: ZodType<Prisma.CourseFindFirstArgs> = z.any()
|
||||
const CourseWhereInputSchema: ZodType<Prisma.CourseWhereInput> = z.any()
|
||||
const CourseSelectSchema: ZodType<Prisma.CourseSelect> = 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);
|
||||
}),
|
||||
});
|
||||
}
|
|
@ -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<Prisma.CourseDelegate> {
|
||||
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' },
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,46 +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
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export const EnrollSchema = z.object({
|
||||
studentId: z.string(),
|
||||
courseId: z.string(),
|
||||
});
|
||||
|
||||
export const UnenrollSchema = z.object({
|
||||
studentId: z.string(),
|
||||
courseId: z.string(),
|
||||
});
|
|
@ -1,9 +0,0 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { EnrollmentRouter } from './enrollment.router';
|
||||
import { EnrollmentService } from './enrollment.service';
|
||||
|
||||
@Module({
|
||||
exports: [EnrollmentRouter, EnrollmentService],
|
||||
providers: [EnrollmentRouter, EnrollmentService]
|
||||
})
|
||||
export class EnrollmentModule { }
|
|
@ -1,54 +0,0 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { TrpcService } from '@server/trpc/trpc.service';
|
||||
import { Prisma, UpdateOrderSchema } from '@nice/common';
|
||||
import { EnrollmentService } from './enrollment.service';
|
||||
import { z, ZodType } from 'zod';
|
||||
import { EnrollSchema, UnenrollSchema } from './enroll.schema';
|
||||
const EnrollmentCreateArgsSchema: ZodType<Prisma.EnrollmentCreateArgs> = z.any()
|
||||
const EnrollmentCreateManyInputSchema: ZodType<Prisma.EnrollmentCreateManyInput> = z.any()
|
||||
const EnrollmentDeleteManyArgsSchema: ZodType<Prisma.EnrollmentDeleteManyArgs> = z.any()
|
||||
const EnrollmentFindManyArgsSchema: ZodType<Prisma.EnrollmentFindManyArgs> = z.any()
|
||||
const EnrollmentFindFirstArgsSchema: ZodType<Prisma.EnrollmentFindFirstArgs> = z.any()
|
||||
const EnrollmentWhereInputSchema: ZodType<Prisma.EnrollmentWhereInput> = z.any()
|
||||
const EnrollmentSelectSchema: ZodType<Prisma.EnrollmentSelect> = z.any()
|
||||
|
||||
@Injectable()
|
||||
export class EnrollmentRouter {
|
||||
constructor(
|
||||
private readonly trpc: TrpcService,
|
||||
private readonly enrollmentService: EnrollmentService,
|
||||
) { }
|
||||
router = this.trpc.router({
|
||||
findFirst: this.trpc.procedure
|
||||
.input(EnrollmentFindFirstArgsSchema) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword
|
||||
.query(async ({ input }) => {
|
||||
return await this.enrollmentService.findFirst(input);
|
||||
}),
|
||||
findMany: this.trpc.procedure
|
||||
.input(EnrollmentFindManyArgsSchema) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword
|
||||
.query(async ({ input }) => {
|
||||
return await this.enrollmentService.findMany(input);
|
||||
}),
|
||||
findManyWithCursor: this.trpc.protectProcedure
|
||||
.input(z.object({
|
||||
cursor: z.any().nullish(),
|
||||
take: z.number().nullish(),
|
||||
where: EnrollmentWhereInputSchema.nullish(),
|
||||
select: EnrollmentSelectSchema.nullish()
|
||||
}))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { staff } = ctx;
|
||||
return await this.enrollmentService.findManyWithCursor(input);
|
||||
}),
|
||||
enroll: this.trpc.protectProcedure
|
||||
.input(EnrollSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
return await this.enrollmentService.enroll(input);
|
||||
}),
|
||||
unenroll: this.trpc.protectProcedure
|
||||
.input(UnenrollSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
return await this.enrollmentService.unenroll(input);
|
||||
}),
|
||||
});
|
||||
}
|
|
@ -1,74 +0,0 @@
|
|||
import { ConflictException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { BaseService } from '../base/base.service';
|
||||
import {
|
||||
UserProfile,
|
||||
db,
|
||||
ObjectType,
|
||||
Prisma,
|
||||
EnrollmentStatus
|
||||
} from '@nice/common';
|
||||
import { z } from 'zod';
|
||||
import { EnrollSchema, UnenrollSchema } from './enroll.schema';
|
||||
import EventBus, { CrudOperation } from '@server/utils/event-bus';
|
||||
|
||||
@Injectable()
|
||||
export class EnrollmentService extends BaseService<Prisma.EnrollmentDelegate> {
|
||||
constructor() {
|
||||
super(db, ObjectType.COURSE);
|
||||
}
|
||||
async enroll(params: z.infer<typeof EnrollSchema>) {
|
||||
const { studentId, courseId } = params
|
||||
const result = await db.$transaction(async tx => {
|
||||
// 检查是否已经报名
|
||||
const existing = await tx.enrollment.findUnique({
|
||||
where: {
|
||||
studentId_courseId: {
|
||||
studentId: studentId,
|
||||
courseId: courseId,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (existing) {
|
||||
throw new ConflictException('Already enrolled in this course');
|
||||
}
|
||||
// 创建报名记录
|
||||
const enrollment = await tx.enrollment.create({
|
||||
data: {
|
||||
studentId: studentId,
|
||||
courseId: courseId,
|
||||
status: EnrollmentStatus.ACTIVE,
|
||||
},
|
||||
});
|
||||
|
||||
return enrollment;
|
||||
});
|
||||
|
||||
EventBus.emit('dataChanged', {
|
||||
type: ObjectType.ENROLLMENT,
|
||||
operation: CrudOperation.CREATED,
|
||||
data: result,
|
||||
});
|
||||
return result
|
||||
}
|
||||
async unenroll(params: z.infer<typeof UnenrollSchema>) {
|
||||
const { studentId, courseId } = params
|
||||
const result = await db.enrollment.update({
|
||||
where: {
|
||||
studentId_courseId: {
|
||||
studentId,
|
||||
courseId,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
status: EnrollmentStatus.CANCELLED
|
||||
}
|
||||
});
|
||||
|
||||
EventBus.emit('dataChanged', {
|
||||
type: ObjectType.ENROLLMENT,
|
||||
operation: CrudOperation.UPDATED,
|
||||
data: result,
|
||||
});
|
||||
return result
|
||||
}
|
||||
}
|
|
@ -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 { }
|
|
@ -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<Prisma.LectureCreateArgs> = z.any()
|
||||
const LectureCreateManyInputSchema: ZodType<Prisma.LectureCreateManyInput> = z.any()
|
||||
const LectureDeleteManyArgsSchema: ZodType<Prisma.LectureDeleteManyArgs> = z.any()
|
||||
const LectureFindManyArgsSchema: ZodType<Prisma.LectureFindManyArgs> = z.any()
|
||||
const LectureFindFirstArgsSchema: ZodType<Prisma.LectureFindFirstArgs> = z.any()
|
||||
const LectureWhereInputSchema: ZodType<Prisma.LectureWhereInput> = z.any()
|
||||
const LectureSelectSchema: ZodType<Prisma.LectureSelect> = 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);
|
||||
}),
|
||||
});
|
||||
}
|
|
@ -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<Prisma.LectureDelegate> {
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
import { db, Lecture } from "@nice/common"
|
||||
|
||||
export async function updateSectionLectureStats(sectionId: string) {
|
||||
const sectionStats = await db.lecture.aggregate({
|
||||
where: {
|
||||
sectionId,
|
||||
deletedAt: null
|
||||
},
|
||||
_count: { _all: true },
|
||||
_sum: { duration: true }
|
||||
});
|
||||
|
||||
await db.section.update({
|
||||
where: { id: sectionId },
|
||||
data: {
|
||||
totalLectures: sectionStats._count._all,
|
||||
totalDuration: sectionStats._sum.duration || 0
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateCourseLectureStats(courseId: string) {
|
||||
const courseStats = await db.lecture.aggregate({
|
||||
where: {
|
||||
courseId,
|
||||
deletedAt: null
|
||||
},
|
||||
_count: { _all: true },
|
||||
_sum: { duration: true }
|
||||
});
|
||||
|
||||
await db.course.update({
|
||||
where: { id: courseId },
|
||||
data: {
|
||||
totalLectures: courseStats._count._all,
|
||||
totalDuration: courseStats._sum.duration || 0
|
||||
}
|
||||
});
|
||||
}
|
|
@ -1,11 +1,8 @@
|
|||
import { Controller, Get, Query, UseGuards } from '@nestjs/common';
|
||||
|
||||
import { PostService } from './post.service';
|
||||
import { AuthGuard } from '@server/auth/auth.guard';
|
||||
import { db } from '@nice/common';
|
||||
|
||||
@Controller('post')
|
||||
export class PostController {
|
||||
constructor(private readonly postService: PostService) { }
|
||||
|
||||
constructor(private readonly postService: PostService) {}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@ import { TrpcService } from '@server/trpc/trpc.service';
|
|||
import { Prisma } from '@nice/common';
|
||||
import { PostService } from './post.service';
|
||||
import { z, ZodType } from 'zod';
|
||||
import { PostMeta } from '@nice/common';
|
||||
import { getClientIp } from './utils';
|
||||
const PostCreateArgsSchema: ZodType<Prisma.PostCreateArgs> = z.any();
|
||||
const PostUpdateArgsSchema: ZodType<Prisma.PostUpdateArgs> = z.any();
|
||||
const PostFindFirstArgsSchema: ZodType<Prisma.PostFindFirstArgs> = z.any();
|
||||
|
@ -15,12 +17,22 @@ export class PostRouter {
|
|||
constructor(
|
||||
private readonly trpc: TrpcService,
|
||||
private readonly postService: PostService,
|
||||
) { }
|
||||
) {}
|
||||
router = this.trpc.router({
|
||||
create: this.trpc.protectProcedure
|
||||
.input(PostCreateArgsSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { staff } = ctx;
|
||||
const { staff, req } = ctx;
|
||||
// 从请求中获取 IP
|
||||
const ip = getClientIp(req);
|
||||
const currentMeta =
|
||||
typeof input.data.meta === 'object' && input.data.meta !== null
|
||||
? input.data.meta
|
||||
: {};
|
||||
input.data.meta = {
|
||||
...currentMeta,
|
||||
ip: ip || '',
|
||||
} as Prisma.InputJsonObject; // 明确指定类型
|
||||
return await this.postService.create(input, { staff });
|
||||
}),
|
||||
softDeleteByIds: this.trpc.protectProcedure
|
||||
|
|
|
@ -3,9 +3,7 @@ import {
|
|||
db,
|
||||
Prisma,
|
||||
UserProfile,
|
||||
VisitType,
|
||||
Post,
|
||||
PostType,
|
||||
RolePerms,
|
||||
ResPerm,
|
||||
ObjectType,
|
||||
|
@ -45,13 +43,13 @@ export class PostService extends BaseService<Prisma.PostDelegate> {
|
|||
operation: CrudOperation.UPDATED,
|
||||
data: result,
|
||||
});
|
||||
return result
|
||||
return result;
|
||||
}
|
||||
async findManyWithCursor(args: Prisma.PostFindManyArgs, staff?: UserProfile) {
|
||||
if (!args.where) args.where = {}
|
||||
if (!args.where) args.where = {};
|
||||
args.where.OR = await this.preFilter(args.where.OR, staff);
|
||||
return this.wrapResult(super.findManyWithCursor(args), async (result) => {
|
||||
let { items } = result;
|
||||
const { items } = result;
|
||||
await Promise.all(
|
||||
items.map(async (item) => {
|
||||
await setPostRelation({ data: item, staff });
|
||||
|
@ -68,7 +66,7 @@ export class PostService extends BaseService<Prisma.PostDelegate> {
|
|||
delete: false,
|
||||
};
|
||||
const isMySelf = data?.authorId === staff?.id;
|
||||
const isDomain = staff.domainId === data.domainId;
|
||||
// const isDomain = staff.domainId === data.domainId;
|
||||
const setManagePermissions = (perms: ResPerm) => {
|
||||
Object.assign(perms, {
|
||||
delete: true,
|
||||
|
@ -84,11 +82,11 @@ export class PostService extends BaseService<Prisma.PostDelegate> {
|
|||
case RolePerms.MANAGE_ANY_POST:
|
||||
setManagePermissions(perms);
|
||||
break;
|
||||
case RolePerms.MANAGE_DOM_POST:
|
||||
if (isDomain) {
|
||||
setManagePermissions(perms);
|
||||
}
|
||||
break;
|
||||
// case RolePerms.MANAGE_DOM_POST:
|
||||
// if (isDomain) {
|
||||
// setManagePermissions(perms);
|
||||
// }
|
||||
// break;
|
||||
}
|
||||
});
|
||||
Object.assign(data, { perms });
|
||||
|
@ -99,51 +97,28 @@ export class PostService extends BaseService<Prisma.PostDelegate> {
|
|||
return outOR?.length > 0 ? outOR : undefined;
|
||||
}
|
||||
async getPostPreFilter(staff?: UserProfile) {
|
||||
if (!staff) return
|
||||
const { deptId, domainId } = staff;
|
||||
if (!staff) return;
|
||||
// const { deptId, domainId } = staff;
|
||||
if (
|
||||
staff.permissions.includes(RolePerms.READ_ANY_POST) ||
|
||||
staff.permissions.includes(RolePerms.MANAGE_ANY_POST)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
const parentDeptIds =
|
||||
(await this.departmentService.getAncestorIds(staff.deptId)) || [];
|
||||
const orCondition: Prisma.PostWhereInput[] = [
|
||||
staff?.id && {
|
||||
authorId: staff.id,
|
||||
},
|
||||
{
|
||||
isPublic: true,
|
||||
},
|
||||
staff?.id && {
|
||||
watchableStaffs: {
|
||||
receivers: {
|
||||
some: {
|
||||
id: staff.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
deptId && {
|
||||
watchableDepts: {
|
||||
some: {
|
||||
id: {
|
||||
in: parentDeptIds,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
AND: [
|
||||
{
|
||||
watchableStaffs: {
|
||||
none: {}, // 匹配 watchableStaffs 为空
|
||||
},
|
||||
},
|
||||
{
|
||||
watchableDepts: {
|
||||
none: {}, // 匹配 watchableDepts 为空
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
].filter(Boolean);
|
||||
|
||||
if (orCondition?.length > 0) return orCondition;
|
||||
|
|
|
@ -1,44 +1,62 @@
|
|||
import { db, Post, PostType, UserProfile, VisitType } from "@nice/common";
|
||||
import { db, Post, PostType, UserProfile, VisitType } from '@nice/common';
|
||||
|
||||
export async function setPostRelation(params: { data: Post, staff?: UserProfile }) {
|
||||
const { data, staff } = params
|
||||
const limitedComments = await db.post.findMany({
|
||||
where: {
|
||||
parentId: data.id,
|
||||
type: PostType.POST_COMMENT,
|
||||
},
|
||||
include: {
|
||||
author: true,
|
||||
},
|
||||
take: 5,
|
||||
});
|
||||
const commentsCount = await db.post.count({
|
||||
where: {
|
||||
parentId: data.id,
|
||||
type: PostType.POST_COMMENT,
|
||||
},
|
||||
});
|
||||
const readed =
|
||||
(await db.visit.count({
|
||||
where: {
|
||||
postId: data.id,
|
||||
type: VisitType.READED,
|
||||
visitorId: staff?.id,
|
||||
},
|
||||
})) > 0;
|
||||
const readedCount = await db.visit.count({
|
||||
where: {
|
||||
postId: data.id,
|
||||
type: VisitType.READED,
|
||||
},
|
||||
});
|
||||
export async function setPostRelation(params: {
|
||||
data: Post;
|
||||
staff?: UserProfile;
|
||||
}) {
|
||||
const { data, staff } = params;
|
||||
const limitedComments = await db.post.findMany({
|
||||
where: {
|
||||
parentId: data.id,
|
||||
type: PostType.POST_COMMENT,
|
||||
},
|
||||
include: {
|
||||
author: true,
|
||||
},
|
||||
take: 5,
|
||||
});
|
||||
const commentsCount = await db.post.count({
|
||||
where: {
|
||||
parentId: data.id,
|
||||
type: PostType.POST_COMMENT,
|
||||
},
|
||||
});
|
||||
const readed =
|
||||
(await db.visit.count({
|
||||
where: {
|
||||
postId: data.id,
|
||||
type: VisitType.READED,
|
||||
visitorId: staff?.id,
|
||||
},
|
||||
})) > 0;
|
||||
const readedCount = await db.visit.count({
|
||||
where: {
|
||||
postId: data.id,
|
||||
type: VisitType.READED,
|
||||
},
|
||||
});
|
||||
|
||||
Object.assign(data, {
|
||||
readed,
|
||||
readedCount,
|
||||
limitedComments,
|
||||
commentsCount,
|
||||
// trouble
|
||||
})
|
||||
Object.assign(data, {
|
||||
readed,
|
||||
readedCount,
|
||||
limitedComments,
|
||||
commentsCount,
|
||||
// trouble
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
export function getClientIp(req: any): string {
|
||||
let ip =
|
||||
req.ip ||
|
||||
(Array.isArray(req.headers['x-forwarded-for'])
|
||||
? req.headers['x-forwarded-for'][0]
|
||||
: req.headers['x-forwarded-for']) ||
|
||||
req.socket.remoteAddress;
|
||||
|
||||
// 如果是 IPv4-mapped IPv6 地址,转换为 IPv4
|
||||
if (typeof ip === 'string' && ip.startsWith('::ffff:')) {
|
||||
ip = ip.substring(7);
|
||||
}
|
||||
|
||||
return ip || '';
|
||||
}
|
||||
|
|
|
@ -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 { }
|
|
@ -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<Prisma.SectionCreateArgs> = z.any()
|
||||
const SectionCreateManyInputSchema: ZodType<Prisma.SectionCreateManyInput> = z.any()
|
||||
const SectionDeleteManyArgsSchema: ZodType<Prisma.SectionDeleteManyArgs> = z.any()
|
||||
const SectionFindManyArgsSchema: ZodType<Prisma.SectionFindManyArgs> = z.any()
|
||||
const SectionFindFirstArgsSchema: ZodType<Prisma.SectionFindFirstArgs> = z.any()
|
||||
const SectionWhereInputSchema: ZodType<Prisma.SectionWhereInput> = z.any()
|
||||
const SectionSelectSchema: ZodType<Prisma.SectionSelect> = 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);
|
||||
}),
|
||||
});
|
||||
}
|
|
@ -1,24 +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<Prisma.SectionDelegate> {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,12 +1,6 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { BaseService } from '../base/base.service';
|
||||
import {
|
||||
UserProfile,
|
||||
db,
|
||||
ObjectType,
|
||||
Prisma,
|
||||
VisitType,
|
||||
} from '@nice/common';
|
||||
import { UserProfile, db, ObjectType, Prisma, VisitType } from '@nice/common';
|
||||
import EventBus from '@server/utils/event-bus';
|
||||
@Injectable()
|
||||
export class VisitService extends BaseService<Prisma.VisitDelegate> {
|
||||
|
@ -14,14 +8,14 @@ export class VisitService extends BaseService<Prisma.VisitDelegate> {
|
|||
super(db, ObjectType.VISIT);
|
||||
}
|
||||
async create(args: Prisma.VisitCreateArgs, staff?: UserProfile) {
|
||||
const { postId, lectureId, messageId } = args.data;
|
||||
const { postId, messageId } = args.data;
|
||||
const visitorId = args.data.visitorId || staff?.id;
|
||||
let result;
|
||||
const existingVisit = await db.visit.findFirst({
|
||||
where: {
|
||||
type: args.data.type,
|
||||
visitorId,
|
||||
OR: [{ postId }, { lectureId }, { messageId }],
|
||||
OR: [{ postId }, { messageId }],
|
||||
},
|
||||
});
|
||||
if (!existingVisit) {
|
||||
|
@ -50,12 +44,12 @@ export class VisitService extends BaseService<Prisma.VisitDelegate> {
|
|||
const createData: Prisma.VisitCreateManyInput[] = [];
|
||||
await Promise.all(
|
||||
data.map(async (item) => {
|
||||
if (staff && !item.visitorId) item.visitorId = staff.id
|
||||
const { postId, lectureId, messageId, visitorId } = item;
|
||||
if (staff && !item.visitorId) item.visitorId = staff.id;
|
||||
const { postId, messageId, visitorId } = item;
|
||||
const existingVisit = await db.visit.findFirst({
|
||||
where: {
|
||||
visitorId,
|
||||
OR: [{ postId }, { lectureId }, { messageId }],
|
||||
OR: [{ postId }, { messageId }],
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -1,49 +1,46 @@
|
|||
import { Job } from 'bullmq';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import {
|
||||
updateCourseLectureStats,
|
||||
updateSectionLectureStats
|
||||
} from '@server/models/lecture/utils';
|
||||
|
||||
import { ObjectType } from '@nice/common';
|
||||
import {
|
||||
updateCourseEnrollmentStats,
|
||||
updateCourseReviewStats
|
||||
} from '@server/models/course/utils';
|
||||
|
||||
import { QueueJobType } from '../types';
|
||||
const logger = new Logger('QueueWorker');
|
||||
export default async function processJob(job: Job<any, any, QueueJobType>) {
|
||||
try {
|
||||
if (job.name === QueueJobType.UPDATE_STATS) {
|
||||
const { sectionId, courseId, type } = job.data;
|
||||
// 处理 section 统计
|
||||
if (sectionId) {
|
||||
await updateSectionLectureStats(sectionId);
|
||||
logger.debug(`Updated section stats for sectionId: ${sectionId}`);
|
||||
}
|
||||
// 如果没有 courseId,提前返回
|
||||
if (!courseId) {
|
||||
return;
|
||||
}
|
||||
// 处理 course 相关统计
|
||||
switch (type) {
|
||||
case ObjectType.LECTURE:
|
||||
await updateCourseLectureStats(courseId);
|
||||
break;
|
||||
case ObjectType.ENROLLMENT:
|
||||
await updateCourseEnrollmentStats(courseId);
|
||||
break;
|
||||
case ObjectType.POST:
|
||||
await updateCourseReviewStats(courseId);
|
||||
break;
|
||||
default:
|
||||
logger.warn(`Unknown update stats type: ${type}`);
|
||||
}
|
||||
try {
|
||||
if (job.name === QueueJobType.UPDATE_STATS) {
|
||||
const { sectionId, courseId, type } = job.data;
|
||||
// 处理 section 统计
|
||||
// if (sectionId) {
|
||||
// await updateSectionLectureStats(sectionId);
|
||||
// logger.debug(`Updated section stats for sectionId: ${sectionId}`);
|
||||
// }
|
||||
// // 如果没有 courseId,提前返回
|
||||
// if (!courseId) {
|
||||
// return;
|
||||
// }
|
||||
// 处理 course 相关统计
|
||||
switch (type) {
|
||||
// case ObjectType.LECTURE:
|
||||
// await updateCourseLectureStats(courseId);
|
||||
// break;
|
||||
// case ObjectType.ENROLLMENT:
|
||||
// await updateCourseEnrollmentStats(courseId);
|
||||
// break;
|
||||
// case ObjectType.POST:
|
||||
// await updateCourseReviewStats(courseId);
|
||||
// break;
|
||||
default:
|
||||
logger.warn(`Unknown update stats type: ${type}`);
|
||||
}
|
||||
|
||||
logger.debug(`Updated course stats for courseId: ${courseId}, type: ${type}`);
|
||||
}
|
||||
|
||||
|
||||
} catch (error: any) {
|
||||
logger.error(`Error processing stats update job: ${error.message}`, error.stack);
|
||||
logger.debug(
|
||||
`Updated course stats for courseId: ${courseId}, type: ${type}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.error(
|
||||
`Error processing stats update job: ${error.message}`,
|
||||
error.stack,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ export class InitService {
|
|||
private readonly minioService: MinioService,
|
||||
private readonly authService: AuthService,
|
||||
private readonly genDevService: GenDevService,
|
||||
) { }
|
||||
) {}
|
||||
private async createRoles() {
|
||||
this.logger.log('Checking existing system roles');
|
||||
for (const role of InitRoles) {
|
||||
|
@ -142,12 +142,13 @@ export class InitService {
|
|||
await this.createRoot();
|
||||
await this.createOrUpdateTaxonomy();
|
||||
await this.initAppConfigs();
|
||||
try {
|
||||
this.logger.log('Initialize minio');
|
||||
await this.createBucket();
|
||||
} catch (error) {
|
||||
this.logger.error('Minio initialization failed:', error);
|
||||
}
|
||||
//不需要minio
|
||||
// try {
|
||||
// this.logger.log('Initialize minio');
|
||||
// await this.createBucket();
|
||||
// } catch (error) {
|
||||
// this.logger.error('Minio initialization failed:', error);
|
||||
// }
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
try {
|
||||
|
|
|
@ -14,9 +14,7 @@ import { VisitModule } from '@server/models/visit/visit.module';
|
|||
import { WebSocketModule } from '@server/socket/websocket.module';
|
||||
import { RoleMapModule } from '@server/models/rbac/rbac.module';
|
||||
import { TransformModule } from '@server/models/transform/transform.module';
|
||||
import { CourseModule } from '@server/models/course/course.module';
|
||||
import { LectureModule } from '@server/models/lecture/lecture.module';
|
||||
import { SectionModule } from '@server/models/section/section.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
AuthModule,
|
||||
|
@ -31,12 +29,9 @@ import { SectionModule } from '@server/models/section/section.module';
|
|||
AppConfigModule,
|
||||
PostModule,
|
||||
VisitModule,
|
||||
CourseModule,
|
||||
LectureModule,
|
||||
SectionModule,
|
||||
WebSocketModule
|
||||
WebSocketModule,
|
||||
],
|
||||
controllers: [],
|
||||
providers: [TrpcService, TrpcRouter, Logger],
|
||||
})
|
||||
export class TrpcModule { }
|
||||
export class TrpcModule {}
|
||||
|
|
|
@ -13,12 +13,10 @@ import { VisitRouter } from '@server/models/visit/visit.router';
|
|||
import { RoleMapRouter } from '@server/models/rbac/rolemap.router';
|
||||
import { TransformRouter } from '@server/models/transform/transform.router';
|
||||
import { RoleRouter } from '@server/models/rbac/role.router';
|
||||
import { CourseRouter } from '@server/models/course/course.router';
|
||||
import { LectureRouter } from '@server/models/lecture/lecture.router';
|
||||
import { SectionRouter } from '@server/models/section/section.router';
|
||||
|
||||
@Injectable()
|
||||
export class TrpcRouter {
|
||||
logger = new Logger(TrpcRouter.name)
|
||||
logger = new Logger(TrpcRouter.name);
|
||||
constructor(
|
||||
private readonly trpc: TrpcService,
|
||||
private readonly post: PostRouter,
|
||||
|
@ -32,13 +30,9 @@ export class TrpcRouter {
|
|||
private readonly app_config: AppConfigRouter,
|
||||
private readonly message: MessageRouter,
|
||||
private readonly visitor: VisitRouter,
|
||||
private readonly course: CourseRouter,
|
||||
private readonly lecture: LectureRouter,
|
||||
private readonly section: SectionRouter
|
||||
// private readonly websocketService: WebSocketService
|
||||
) { }
|
||||
) {}
|
||||
appRouter = this.trpc.router({
|
||||
|
||||
transform: this.transform.router,
|
||||
post: this.post.router,
|
||||
department: this.department.router,
|
||||
|
@ -50,11 +44,8 @@ export class TrpcRouter {
|
|||
message: this.message.router,
|
||||
app_config: this.app_config.router,
|
||||
visitor: this.visitor.router,
|
||||
course: this.course.router,
|
||||
lecture: this.lecture.router,
|
||||
section: this.section.router
|
||||
});
|
||||
wss: WebSocketServer = undefined
|
||||
wss: WebSocketServer = undefined;
|
||||
|
||||
async applyMiddleware(app: INestApplication) {
|
||||
app.use(
|
||||
|
@ -65,7 +56,7 @@ export class TrpcRouter {
|
|||
onError(opts) {
|
||||
const { error, type, path, input, ctx, req } = opts;
|
||||
// console.error('TRPC Error:', error);
|
||||
}
|
||||
},
|
||||
}),
|
||||
);
|
||||
// applyWSSHandler({
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
//trpc.service.ts
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { initTRPC, TRPCError } from '@trpc/server';
|
||||
import superjson from 'superjson-cjs';
|
||||
|
@ -12,9 +13,17 @@ export class TrpcService {
|
|||
|
||||
async createExpressContext(
|
||||
opts: trpcExpress.CreateExpressContextOptions,
|
||||
): Promise<{ staff: UserProfile | undefined }> {
|
||||
): Promise<{
|
||||
staff: UserProfile | undefined;
|
||||
req: trpcExpress.CreateExpressContextOptions['req'];
|
||||
}> {
|
||||
const token = opts.req.headers.authorization?.split(' ')[1];
|
||||
return await UserProfileService.instance.getUserProfileByToken(token);
|
||||
const staff =
|
||||
await UserProfileService.instance.getUserProfileByToken(token);
|
||||
return {
|
||||
staff: staff.staff,
|
||||
req: opts.req,
|
||||
};
|
||||
}
|
||||
async createWSSContext(
|
||||
opts: CreateWSSContextFnOptions,
|
||||
|
@ -45,6 +54,7 @@ export class TrpcService {
|
|||
ctx: {
|
||||
// User value is confirmed to be non-null at this point
|
||||
staff: ctx.staff,
|
||||
req: ctx.req,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,8 +4,8 @@ import LeaderCard from '@web/src/components/common/element/LeaderCard';
|
|||
import React, { useState, useCallback } from 'react';
|
||||
import * as tus from 'tus-js-client';
|
||||
interface TusUploadProps {
|
||||
onSuccess?: (response: any) => void;
|
||||
onError?: (error: Error) => void;
|
||||
onSuccess?: (response: any) => void;
|
||||
onError?: (error: Error) => void;
|
||||
}
|
||||
const carouselItems = [
|
||||
{
|
||||
|
@ -61,8 +61,10 @@ const TusUploader: React.FC<TusUploadProps> = ({
|
|||
}
|
||||
});
|
||||
|
||||
upload.start();
|
||||
}, [onSuccess, onError]);
|
||||
upload.start();
|
||||
},
|
||||
[onSuccess, onError]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className=' flex flex-col gap-4'>
|
||||
|
@ -102,4 +104,4 @@ const TusUploader: React.FC<TusUploadProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
export default TusUploader;
|
||||
export default TusUploader;
|
||||
|
|
|
@ -2,26 +2,26 @@ import { createContext, useContext, ReactNode, useEffect } from "react";
|
|||
import { useForm, FormProvider, SubmitHandler } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
import { CourseDto, CourseLevel, CourseStatus } from "@nice/common";
|
||||
import { api, useCourse } from "@nice/client";
|
||||
// import { CourseDto, CourseLevel, CourseStatus } from "@nice/common";
|
||||
// import { api, useCourse } from "@nice/client";
|
||||
import toast from "react-hot-toast";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
// 定义课程表单验证 Schema
|
||||
const courseSchema = z.object({
|
||||
title: z.string().min(1, '课程标题不能为空'),
|
||||
subTitle: z.string().nullish(),
|
||||
description: z.string().nullish(),
|
||||
thumbnail: z.string().url().nullish(),
|
||||
level: z.nativeEnum(CourseLevel),
|
||||
requirements: z.array(z.string()).nullish(),
|
||||
objectives: z.array(z.string()).nullish()
|
||||
title: z.string().min(1, "课程标题不能为空"),
|
||||
subTitle: z.string().nullish(),
|
||||
description: z.string().nullish(),
|
||||
thumbnail: z.string().url().nullish(),
|
||||
// level: z.nativeEnum(CourseLevel),
|
||||
requirements: z.array(z.string()).nullish(),
|
||||
objectives: z.array(z.string()).nullish(),
|
||||
});
|
||||
export type CourseFormData = z.infer<typeof courseSchema>;
|
||||
interface CourseEditorContextType {
|
||||
onSubmit: SubmitHandler<CourseFormData>;
|
||||
editId?: string; // 添加 editId
|
||||
part?: string;
|
||||
course?: CourseDto;
|
||||
// course?: CourseDto;
|
||||
}
|
||||
interface CourseFormProviderProps {
|
||||
children: ReactNode;
|
||||
|
@ -29,69 +29,74 @@ interface CourseFormProviderProps {
|
|||
part?: string;
|
||||
}
|
||||
const CourseEditorContext = createContext<CourseEditorContextType | null>(null);
|
||||
export function CourseFormProvider({ children, editId }: CourseFormProviderProps) {
|
||||
const { create, update } = useCourse()
|
||||
const { data: course }: { data: CourseDto } = api.course.findFirst.useQuery({ where: { id: editId } }, { enabled: Boolean(editId) })
|
||||
const navigate = useNavigate()
|
||||
const methods = useForm<CourseFormData>({
|
||||
resolver: zodResolver(courseSchema),
|
||||
defaultValues: {
|
||||
level: CourseLevel.BEGINNER,
|
||||
requirements: [],
|
||||
objectives: []
|
||||
},
|
||||
});
|
||||
useEffect(() => {
|
||||
if (course) {
|
||||
const formData = {
|
||||
title: course.title,
|
||||
subTitle: course.subTitle,
|
||||
description: course.description,
|
||||
thumbnail: course.thumbnail,
|
||||
level: course.level,
|
||||
requirements: course.requirements,
|
||||
objectives: course.objectives,
|
||||
status: course.status,
|
||||
};
|
||||
methods.reset(formData as any);
|
||||
}
|
||||
}, [course, methods]);
|
||||
const onSubmit: SubmitHandler<CourseFormData> = async (data: CourseFormData) => {
|
||||
try {
|
||||
|
||||
if (editId) {
|
||||
await update.mutateAsync({
|
||||
where: { id: editId },
|
||||
data: {
|
||||
...data
|
||||
}
|
||||
})
|
||||
toast.success('课程更新成功!');
|
||||
} else {
|
||||
const result = await create.mutateAsync({
|
||||
data: {
|
||||
status: CourseStatus.DRAFT,
|
||||
...data
|
||||
}
|
||||
})
|
||||
navigate(`/course/${result.id}/editor`, { replace: true })
|
||||
toast.success('课程创建成功!');
|
||||
}
|
||||
methods.reset(data);
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error submitting form:', error);
|
||||
toast.error('操作失败,请重试!');
|
||||
}
|
||||
};
|
||||
return (
|
||||
<CourseEditorContext.Provider value={{ onSubmit, editId, course }}>
|
||||
<FormProvider {...methods}>
|
||||
{children}
|
||||
</FormProvider>
|
||||
</CourseEditorContext.Provider>
|
||||
);
|
||||
export function CourseFormProvider({
|
||||
children,
|
||||
editId,
|
||||
}: CourseFormProviderProps) {
|
||||
// const { create, update } = useCourse()
|
||||
// const { data: course }: { data: CourseDto } = api.course.findFirst.useQuery({ where: { id: editId } }, { enabled: Boolean(editId) })
|
||||
const navigate = useNavigate();
|
||||
const methods = useForm<CourseFormData>({
|
||||
resolver: zodResolver(courseSchema),
|
||||
defaultValues: {
|
||||
// level: CourseLevel.BEGINNER,
|
||||
requirements: [],
|
||||
objectives: [],
|
||||
},
|
||||
});
|
||||
// useEffect(() => {
|
||||
// if (course) {
|
||||
// const formData = {
|
||||
// title: course.title,
|
||||
// subTitle: course.subTitle,
|
||||
// description: course.description,
|
||||
// thumbnail: course.thumbnail,
|
||||
// level: course.level,
|
||||
// requirements: course.requirements,
|
||||
// objectives: course.objectives,
|
||||
// status: course.status,
|
||||
// };
|
||||
// methods.reset(formData as any);
|
||||
// }
|
||||
// }, [course, methods]);
|
||||
const onSubmit: SubmitHandler<CourseFormData> = async (
|
||||
data: CourseFormData
|
||||
) => {
|
||||
try {
|
||||
if (editId) {
|
||||
// await update.mutateAsync({
|
||||
// where: { id: editId },
|
||||
// data: {
|
||||
// ...data
|
||||
// }
|
||||
// })
|
||||
toast.success("课程更新成功!");
|
||||
} else {
|
||||
// const result = await create.mutateAsync({
|
||||
// data: {
|
||||
// status: CourseStatus.DRAFT,
|
||||
// ...data
|
||||
// }
|
||||
// })
|
||||
// navigate(`/course/${result.id}/editor`, { replace: true });
|
||||
toast.success("课程创建成功!");
|
||||
}
|
||||
methods.reset(data);
|
||||
} catch (error) {
|
||||
console.error("Error submitting form:", error);
|
||||
toast.error("操作失败,请重试!");
|
||||
}
|
||||
};
|
||||
return (
|
||||
<CourseEditorContext.Provider
|
||||
value={{
|
||||
onSubmit,
|
||||
editId,
|
||||
// course
|
||||
}}>
|
||||
<FormProvider {...methods}>{children}</FormProvider>
|
||||
</CourseEditorContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const useCourseEditor = () => {
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
import apiClient from "../utils/axios-client";
|
||||
import { AuthSchema, RolePerms, UserProfile } from "@nice/common";
|
||||
import { z } from "zod";
|
||||
|
||||
interface AuthContextProps {
|
||||
accessToken: string | null;
|
||||
refreshToken: string | null;
|
||||
|
@ -144,6 +145,7 @@ export function AuthProvider({ children }: AuthProviderProps) {
|
|||
startTokenRefreshInterval();
|
||||
fetchUserProfile();
|
||||
} catch (err: any) {
|
||||
console.log(err);
|
||||
throw err;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
|
@ -157,6 +159,7 @@ export function AuthProvider({ children }: AuthProviderProps) {
|
|||
setIsLoading(true);
|
||||
await apiClient.post(`/auth/signup`, data);
|
||||
} catch (err: any) {
|
||||
console.log(err);
|
||||
throw err;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
|
|
|
@ -1,41 +1,42 @@
|
|||
{
|
||||
"name": "@nice/client",
|
||||
"version": "1.0.0",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"sideEffects": false,
|
||||
"files": [
|
||||
"dist",
|
||||
"src"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"dev": "tsup --watch",
|
||||
"clean": "rimraf dist",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"tinycolor2": "^1.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nice/common": "workspace:^",
|
||||
"@tanstack/query-async-storage-persister": "^5.51.9",
|
||||
"@tanstack/react-query": "^5.51.21",
|
||||
"@tanstack/react-query-persist-client": "^5.51.9",
|
||||
"@trpc/client": "11.0.0-rc.456",
|
||||
"@trpc/react-query": "11.0.0-rc.456",
|
||||
"@trpc/server": "11.0.0-rc.456",
|
||||
"dayjs": "^1.11.12",
|
||||
"lib0": "^0.2.98",
|
||||
"mitt": "^3.0.1",
|
||||
"react": "18.2.0",
|
||||
"yjs": "^13.6.20",
|
||||
"axios": "^1.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"rimraf": "^6.0.1",
|
||||
"tsup": "^8.3.5",
|
||||
"typescript": "^5.5.4"
|
||||
}
|
||||
}
|
||||
"name": "@nice/client",
|
||||
"version": "1.0.0",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"sideEffects": false,
|
||||
"files": [
|
||||
"dist",
|
||||
"src"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"dev": "tsup --watch",
|
||||
"dev-static": "tsup --no-watch",
|
||||
"clean": "rimraf dist",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"tinycolor2": "^1.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nice/common": "workspace:^",
|
||||
"@tanstack/query-async-storage-persister": "^5.51.9",
|
||||
"@tanstack/react-query": "^5.51.21",
|
||||
"@tanstack/react-query-persist-client": "^5.51.9",
|
||||
"@trpc/client": "11.0.0-rc.456",
|
||||
"@trpc/react-query": "11.0.0-rc.456",
|
||||
"@trpc/server": "11.0.0-rc.456",
|
||||
"dayjs": "^1.11.12",
|
||||
"lib0": "^0.2.98",
|
||||
"mitt": "^3.0.1",
|
||||
"react": "18.2.0",
|
||||
"yjs": "^13.6.20",
|
||||
"axios": "^1.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"rimraf": "^6.0.1",
|
||||
"tsup": "^8.3.5",
|
||||
"typescript": "^5.5.4"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,4 +9,3 @@ export * from "./useTaxonomy"
|
|||
export * from "./useVisitor"
|
||||
export * from "./useMessage"
|
||||
export * from "./usePost"
|
||||
export * from "./useCourse"
|
|
@ -1,56 +0,0 @@
|
|||
import { api } from "../trpc";
|
||||
|
||||
export function useCourse() {
|
||||
const utils = api.useUtils();
|
||||
return {
|
||||
// Queries
|
||||
findMany: api.course.findMany.useQuery,
|
||||
findFirst: api.course.findFirst.useQuery,
|
||||
findManyWithCursor: api.course.findManyWithCursor.useQuery,
|
||||
|
||||
// Mutations
|
||||
create: api.course.create.useMutation({
|
||||
onSuccess: () => {
|
||||
utils.course.invalidate()
|
||||
utils.course.findMany.invalidate();
|
||||
utils.course.findManyWithCursor.invalidate();
|
||||
utils.course.findManyWithPagination.invalidate()
|
||||
},
|
||||
}),
|
||||
update: api.course.update.useMutation({
|
||||
onSuccess: () => {
|
||||
utils.course.findMany.invalidate();
|
||||
utils.course.findManyWithCursor.invalidate();
|
||||
utils.course.findManyWithPagination.invalidate()
|
||||
},
|
||||
}),
|
||||
createMany: api.course.createMany.useMutation({
|
||||
onSuccess: () => {
|
||||
utils.course.findMany.invalidate();
|
||||
utils.course.findManyWithCursor.invalidate();
|
||||
utils.course.findManyWithPagination.invalidate()
|
||||
},
|
||||
}),
|
||||
deleteMany: api.course.deleteMany.useMutation({
|
||||
onSuccess: () => {
|
||||
utils.course.findMany.invalidate();
|
||||
utils.course.findManyWithCursor.invalidate();
|
||||
utils.course.findManyWithPagination.invalidate()
|
||||
},
|
||||
}),
|
||||
softDeleteByIds: api.course.softDeleteByIds.useMutation({
|
||||
onSuccess: () => {
|
||||
utils.course.findMany.invalidate();
|
||||
utils.course.findManyWithCursor.invalidate();
|
||||
utils.course.findManyWithPagination.invalidate()
|
||||
},
|
||||
}),
|
||||
updateOrder: api.course.updateOrder.useMutation({
|
||||
onSuccess: () => {
|
||||
utils.course.findMany.invalidate();
|
||||
utils.course.findManyWithCursor.invalidate();
|
||||
utils.course.findManyWithPagination.invalidate()
|
||||
},
|
||||
})
|
||||
};
|
||||
}
|
|
@ -2,12 +2,12 @@ import { api } from "../trpc";
|
|||
|
||||
export function usePost() {
|
||||
const utils = api.useUtils();
|
||||
const create = api.post.create.useMutation({
|
||||
const create: any = api.post.create.useMutation({
|
||||
onSuccess: () => {
|
||||
utils.post.invalidate();
|
||||
},
|
||||
});
|
||||
const update = api.post.update.useMutation({
|
||||
const update: any = api.post.update.useMutation({
|
||||
onSuccess: () => {
|
||||
utils.post.invalidate();
|
||||
},
|
||||
|
@ -17,21 +17,21 @@ export function usePost() {
|
|||
utils.post.invalidate();
|
||||
},
|
||||
});
|
||||
const softDeleteByIds = api.post.softDeleteByIds.useMutation({
|
||||
const softDeleteByIds: any = api.post.softDeleteByIds.useMutation({
|
||||
onSuccess: () => {
|
||||
utils.post.invalidate();
|
||||
},
|
||||
});
|
||||
const restoreByIds = api.post.restoreByIds.useMutation({
|
||||
const restoreByIds: any = api.post.restoreByIds.useMutation({
|
||||
onSuccess: () => {
|
||||
utils.post.invalidate();
|
||||
},
|
||||
})
|
||||
});
|
||||
return {
|
||||
create,
|
||||
update,
|
||||
deleteMany,
|
||||
softDeleteByIds,
|
||||
restoreByIds
|
||||
restoreByIds,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -7,4 +7,4 @@ export * from "./hooks"
|
|||
export * from "./websocket"
|
||||
export * from "./event"
|
||||
export * from "./types"
|
||||
export * from "./upload"
|
||||
// export * from "./upload"
|
|
@ -1,3 +0,0 @@
|
|||
export * from "./types"
|
||||
export * from "./uploadManager"
|
||||
export * from "./useUpload"
|
|
@ -1,13 +0,0 @@
|
|||
import { UploadProgress } from "@nice/common";
|
||||
|
||||
|
||||
export interface UploadOptions {
|
||||
baseUrl?: string;
|
||||
chunkSize?: number;
|
||||
concurrency?: number;
|
||||
retries?: number;
|
||||
clientId?: string;
|
||||
onProgress?: (progress: UploadProgress) => void;
|
||||
onError?: (error: Error) => void;
|
||||
onSuccess?: (response: any) => void;
|
||||
}
|
|
@ -1,225 +0,0 @@
|
|||
import axios, { AxiosInstance } from 'axios';
|
||||
import { ChunkDto, UploadProgress, UploadStatusInfoDto, UUIDGenerator } from '@nice/common';
|
||||
import { UploadOptions } from './types';
|
||||
import { calculateFileIdentifier } from '../tools';
|
||||
|
||||
export class UploadManager {
|
||||
private readonly axios: AxiosInstance;
|
||||
private readonly chunkSize: number;
|
||||
private readonly concurrency: number;
|
||||
private readonly retries: number;
|
||||
private readonly clientId: string;
|
||||
private activeUploads: Map<string, boolean> = new Map();
|
||||
private abortControllers: Map<string, AbortController> = new Map();
|
||||
|
||||
constructor(options: UploadOptions = {}) {
|
||||
const {
|
||||
baseUrl = '/upload',
|
||||
chunkSize = 10 * 1024 * 1024,
|
||||
concurrency = 3,
|
||||
retries = 3,
|
||||
} = options;
|
||||
|
||||
this.axios = axios.create({ baseURL: baseUrl });
|
||||
this.chunkSize = chunkSize;
|
||||
this.concurrency = concurrency;
|
||||
this.retries = retries;
|
||||
this.clientId = options.clientId || UUIDGenerator.generate();
|
||||
}
|
||||
async uploadFile(file: File, options: UploadOptions = {}): Promise<void> {
|
||||
const identifier = await calculateFileIdentifier(file);
|
||||
const controller = new AbortController();
|
||||
this.abortControllers.set(identifier, controller);
|
||||
try {
|
||||
// Check if file is already uploaded
|
||||
const statusInfo = await this.checkUploadStatusInfo(identifier);
|
||||
if (statusInfo?.status === "completed") {
|
||||
options.onSuccess?.({ identifier, filename: file.name });
|
||||
return;
|
||||
}
|
||||
const chunks = await this.prepareChunks(file, identifier);
|
||||
const uploadedChunks = statusInfo?.chunks || new Set<number>();
|
||||
// Filter out already uploaded chunks
|
||||
const remainingChunks = chunks.filter(chunk => !uploadedChunks.has(chunk.chunkNumber));
|
||||
await this.uploadChunks(remainingChunks, file, options, controller.signal);
|
||||
|
||||
} catch (error) {
|
||||
if (axios.isCancel(error)) {
|
||||
return;
|
||||
}
|
||||
options.onError?.(error as Error);
|
||||
throw error;
|
||||
} finally {
|
||||
this.abortControllers.delete(identifier);
|
||||
this.activeUploads.delete(identifier);
|
||||
}
|
||||
}
|
||||
|
||||
private async prepareChunks(file: File, identifier: string): Promise<ChunkDto[]> {
|
||||
const chunks: ChunkDto[] = [];
|
||||
const totalChunks = Math.ceil(file.size / this.chunkSize);
|
||||
for (let i = 0; i < totalChunks; i++) {
|
||||
chunks.push({
|
||||
identifier,
|
||||
filename: file.name,
|
||||
chunkNumber: i + 1,
|
||||
totalChunks,
|
||||
currentChunkSize: Math.min(this.chunkSize, file.size - i * this.chunkSize),
|
||||
totalSize: file.size,
|
||||
});
|
||||
}
|
||||
|
||||
return chunks;
|
||||
}
|
||||
|
||||
private async uploadChunks(
|
||||
chunks: ChunkDto[],
|
||||
file: File,
|
||||
options: UploadOptions,
|
||||
signal: AbortSignal
|
||||
): Promise<void> {
|
||||
const chunkQueue = [...chunks]; // Create a copy of chunks array
|
||||
|
||||
let activeUploads = 0;
|
||||
let completedChunks = 0;
|
||||
let uploadedBytes = 0;
|
||||
|
||||
// 记录最近几次chunk的上传速度
|
||||
const speedBuffer: number[] = [];
|
||||
const SPEED_BUFFER_SIZE = 5; // 保留最近5个chunk的速度
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const uploadNextChunk = async () => {
|
||||
if (completedChunks === chunks.length) {
|
||||
if (activeUploads === 0) { // Only resolve when all active uploads are done
|
||||
resolve();
|
||||
}
|
||||
return;
|
||||
}
|
||||
while (activeUploads < this.concurrency && chunkQueue.length > 0) {
|
||||
|
||||
const chunk = chunkQueue.shift();
|
||||
if (!chunk) break;
|
||||
|
||||
const chunkStartTime = Date.now();
|
||||
activeUploads++;
|
||||
|
||||
this.uploadChunk(chunk, file, signal)
|
||||
.then(() => {
|
||||
|
||||
const chunkEndTime = Date.now();
|
||||
const chunkUploadTime = (chunkEndTime - chunkStartTime) / 1000; // 秒
|
||||
|
||||
completedChunks++;
|
||||
uploadedBytes += chunk.currentChunkSize;
|
||||
activeUploads--;
|
||||
|
||||
// 计算当前chunk的上传速度
|
||||
const currentSpeed = chunkUploadTime > 0
|
||||
? chunk.currentChunkSize / chunkUploadTime
|
||||
: 0;
|
||||
|
||||
// 维护速度缓冲区
|
||||
speedBuffer.push(currentSpeed);
|
||||
if (speedBuffer.length > SPEED_BUFFER_SIZE) {
|
||||
speedBuffer.shift();
|
||||
}
|
||||
|
||||
// 计算平均速度
|
||||
const averageSpeed = speedBuffer.length > 0
|
||||
? speedBuffer.reduce((a, b) => a + b, 0) / speedBuffer.length
|
||||
: 0;
|
||||
|
||||
|
||||
const totalUploadedBytes = uploadedBytes;
|
||||
const remainingBytes = file.size - totalUploadedBytes;
|
||||
|
||||
// 使用平均速度计算剩余时间
|
||||
const remainingTime = averageSpeed > 0
|
||||
? remainingBytes / averageSpeed
|
||||
: 0;
|
||||
|
||||
const progress: UploadProgress = {
|
||||
identifier: chunk.identifier,
|
||||
percentage: (completedChunks / (chunks.length + completedChunks)) * 100,
|
||||
uploadedSize: totalUploadedBytes,
|
||||
totalSize: file.size,
|
||||
speed: averageSpeed, // 字节/秒
|
||||
remainingTime: remainingTime // 秒
|
||||
};
|
||||
|
||||
options.onProgress?.(progress);
|
||||
uploadNextChunk();
|
||||
})
|
||||
.catch(reject);
|
||||
}
|
||||
};
|
||||
|
||||
uploadNextChunk();
|
||||
});
|
||||
}
|
||||
private async uploadChunk(
|
||||
chunk: ChunkDto,
|
||||
file: File,
|
||||
signal: AbortSignal
|
||||
): Promise<void> {
|
||||
const start = (chunk.chunkNumber - 1) * this.chunkSize;
|
||||
const end = Math.min(start + this.chunkSize, file.size);
|
||||
const chunkBlob = file.slice(start, end);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('chunk', JSON.stringify(chunk));
|
||||
formData.append('clientId', this.clientId)
|
||||
formData.append('file', chunkBlob);
|
||||
|
||||
let attempts = 0;
|
||||
while (attempts < this.retries) {
|
||||
try {
|
||||
await this.axios.post('/chunk', formData, { signal });
|
||||
return;
|
||||
} catch (error) {
|
||||
attempts++;
|
||||
if (attempts === this.retries) throw error;
|
||||
await new Promise(resolve => setTimeout(resolve, 1000 * attempts));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async pauseUpload(identifier: string): Promise<void> {
|
||||
const controller = this.abortControllers.get(identifier);
|
||||
if (controller) {
|
||||
controller.abort();
|
||||
this.abortControllers.delete(identifier);
|
||||
}
|
||||
try {
|
||||
// Call the pause API endpoint
|
||||
await this.axios.post(`/pause/${identifier}`);
|
||||
} catch (error) {
|
||||
console.error('Error pausing upload:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
async resumeUpload(file: File, options: UploadOptions = {}): Promise<void> {
|
||||
const identifier = await calculateFileIdentifier(file);
|
||||
|
||||
try {
|
||||
// Call the resume API endpoint
|
||||
await this.axios.post(`/resume/${identifier}`);
|
||||
|
||||
// Then continue with the upload process
|
||||
return this.uploadFile(file, options);
|
||||
} catch (error) {
|
||||
console.error('Error resuming upload:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async checkUploadStatusInfo(identifier: string): Promise<UploadStatusInfoDto> {
|
||||
try {
|
||||
const response = await this.axios.get(`/status/${identifier}`);
|
||||
return response.data;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
import { useCallback, useRef, useState } from 'react';
|
||||
import { UploadManager } from './uploadManager';
|
||||
import { UploadProgress } from '@nice/common';
|
||||
import { UploadOptions } from './types';
|
||||
|
||||
export function useUpload(options: UploadOptions = {}) {
|
||||
const [progress, setProgress] = useState<Record<string, UploadProgress>>({});
|
||||
const [errors, setErrors] = useState<Record<string, Error>>({});
|
||||
const uploadManagerRef = useRef<UploadManager>();
|
||||
const getUploadManager = useCallback(() => {
|
||||
if (!uploadManagerRef.current) {
|
||||
uploadManagerRef.current = new UploadManager(options);
|
||||
}
|
||||
return uploadManagerRef.current;
|
||||
}, [options]);
|
||||
const upload = useCallback(async (files: File | File[]) => {
|
||||
const fileList = Array.isArray(files) ? files : [files];
|
||||
const manager = getUploadManager();
|
||||
const uploads = fileList.map(file => {
|
||||
return manager.uploadFile(file, {
|
||||
...options,
|
||||
onProgress: (progress) => {
|
||||
setProgress(prev => ({
|
||||
...prev,
|
||||
[progress.identifier]: progress
|
||||
}));
|
||||
options.onProgress?.(progress);
|
||||
},
|
||||
onError: (error) => {
|
||||
setErrors(prev => ({
|
||||
...prev,
|
||||
[file.name]: error
|
||||
}));
|
||||
options.onError?.(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
return Promise.all(uploads);
|
||||
}, [options, getUploadManager]);
|
||||
|
||||
const pauseUpload = useCallback((identifier: string) => {
|
||||
getUploadManager().pauseUpload(identifier);
|
||||
}, [getUploadManager]);
|
||||
|
||||
const resumeUpload = useCallback((file: File) => {
|
||||
return getUploadManager().resumeUpload(file, options);
|
||||
}, [getUploadManager, options]);
|
||||
|
||||
return {
|
||||
upload,
|
||||
pauseUpload,
|
||||
resumeUpload,
|
||||
progress,
|
||||
errors
|
||||
};
|
||||
}
|
|
@ -1,34 +1,35 @@
|
|||
{
|
||||
"name": "@nice/common",
|
||||
"version": "1.0.0",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"generate": "pnpm prisma generate",
|
||||
"build": "pnpm generate && tsup",
|
||||
"dev": "pnpm generate && tsup --watch ",
|
||||
"studio": "pnpm prisma studio",
|
||||
"db:clear": "rm -rf prisma/migrations && pnpm prisma migrate dev --name init",
|
||||
"postinstall": "pnpm generate"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "5.17.0",
|
||||
"prisma": "5.17.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^3.23.8",
|
||||
"yjs": "^13.6.20",
|
||||
"lib0": "^0.2.98"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.3.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.5.4",
|
||||
"concurrently": "^8.0.0",
|
||||
"tsup": "^8.3.5",
|
||||
"rimraf": "^6.0.1"
|
||||
}
|
||||
}
|
||||
"name": "@nice/common",
|
||||
"version": "1.0.0",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"generate": "pnpm prisma generate",
|
||||
"build": "pnpm generate && tsup",
|
||||
"dev": "pnpm generate && tsup --watch ",
|
||||
"dev-static": "pnpm generate && tsup --no-watch ",
|
||||
"studio": "pnpm prisma studio",
|
||||
"db:clear": "rm -rf prisma/migrations && pnpm prisma migrate dev --name init",
|
||||
"postinstall": "pnpm generate"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "5.17.0",
|
||||
"prisma": "5.17.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"zod": "^3.23.8",
|
||||
"yjs": "^13.6.20",
|
||||
"lib0": "^0.2.98"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.3.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.5.4",
|
||||
"concurrently": "^8.0.0",
|
||||
"tsup": "^8.3.5",
|
||||
"rimraf": "^6.0.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -195,17 +195,14 @@ model Post {
|
|||
// 关系类型字段
|
||||
authorId String? @map("author_id")
|
||||
author Staff? @relation("post_author", fields: [authorId], references: [id]) // 帖子作者,关联 Staff 模型
|
||||
|
||||
visits Visit[] // 访问记录,关联 Visit 模型
|
||||
receivers Staff[] @relation("post_receiver")
|
||||
|
||||
parentId String? @map("parent_id")
|
||||
parent Post? @relation("PostChildren", fields: [parentId], references: [id]) // 父级帖子,关联 Post 模型
|
||||
children Post[] @relation("PostChildren") // 子级帖子列表,关联 Post 模型
|
||||
|
||||
resources Resource[] // 附件列表
|
||||
isPublic Boolean? @default(false) @map("is_public")
|
||||
meta Json?
|
||||
meta Json? // 签名 和 IP
|
||||
|
||||
// 复合索引
|
||||
@@index([type, domainId]) // 类型和域组合查询
|
||||
|
|
|
@ -5,10 +5,6 @@ import type {
|
|||
Message,
|
||||
Post,
|
||||
RoleMap,
|
||||
Section,
|
||||
Lecture,
|
||||
Course,
|
||||
Enrollment,
|
||||
} from "@prisma/client";
|
||||
import { SocketMsgType, RolePerms } from "./enum";
|
||||
import { RowRequestSchema } from "./schema";
|
||||
|
@ -156,19 +152,16 @@ export type DepartmentDto = Department & {
|
|||
export type RoleMapDto = RoleMap & {
|
||||
staff: StaffDto;
|
||||
};
|
||||
export type SectionDto = Section & {
|
||||
lectures: Lecture[];
|
||||
};
|
||||
export type CourseDto = Course & {
|
||||
enrollments: Enrollment[];
|
||||
sections: SectionDto[];
|
||||
};
|
||||
export interface BaseSetting {
|
||||
appConfig?: {
|
||||
splashScreen?: string;
|
||||
devDept?: string;
|
||||
};
|
||||
}
|
||||
export interface PostMeta {
|
||||
signature?: string;
|
||||
ip?: string;
|
||||
}
|
||||
export type RowModelResult = {
|
||||
rowData: any[];
|
||||
rowCount: number;
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
"scripts": {
|
||||
"build": "tsup",
|
||||
"dev": "tsup --watch",
|
||||
"dev-static": "tsup --no-watch",
|
||||
"clean": "rimraf dist",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
<h1>error</h1>
|
|
@ -0,0 +1 @@
|
|||
<h1>test1</h1>
|
Loading…
Reference in New Issue