add 202501221855
This commit is contained in:
parent
ad5fb5038d
commit
0da8a60bd6
|
@ -37,10 +37,8 @@ export class BaseService<
|
||||||
constructor(
|
constructor(
|
||||||
protected prisma: PrismaClient,
|
protected prisma: PrismaClient,
|
||||||
protected objectType: string,
|
protected objectType: string,
|
||||||
protected enableOrder: boolean = false
|
protected enableOrder: boolean = false,
|
||||||
) {
|
) {}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the name of the model dynamically.
|
* Retrieves the name of the model dynamically.
|
||||||
|
@ -51,7 +49,7 @@ export class BaseService<
|
||||||
return modelName;
|
return modelName;
|
||||||
}
|
}
|
||||||
private getModel(tx?: TransactionType): D {
|
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
|
* Error handling helper function
|
||||||
|
@ -85,7 +83,9 @@ export class BaseService<
|
||||||
*/
|
*/
|
||||||
async findUnique(args: A['findUnique']): Promise<R['findUnique']> {
|
async findUnique(args: A['findUnique']): Promise<R['findUnique']> {
|
||||||
try {
|
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) {
|
} catch (error) {
|
||||||
this.handleError(error, 'read');
|
this.handleError(error, 'read');
|
||||||
}
|
}
|
||||||
|
@ -152,26 +152,27 @@ export class BaseService<
|
||||||
* const newUser = await service.create({ data: { name: 'John Doe' } });
|
* const newUser = await service.create({ data: { name: 'John Doe' } });
|
||||||
*/
|
*/
|
||||||
async create(args: A['create'], params?: any): Promise<R['create']> {
|
async create(args: A['create'], params?: any): Promise<R['create']> {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
if (this.enableOrder && !(args as any).data.order) {
|
if (this.enableOrder && !(args as any).data.order) {
|
||||||
// 查找当前最大的 order 值
|
// 查找当前最大的 order 值
|
||||||
const maxOrderItem = await this.getModel(params?.tx).findFirst({
|
const maxOrderItem = (await this.getModel(params?.tx).findFirst({
|
||||||
orderBy: { order: 'desc' }
|
orderBy: { order: 'desc' },
|
||||||
}) as any;
|
})) as any;
|
||||||
// 设置新记录的 order 值
|
// 设置新记录的 order 值
|
||||||
const newOrder = maxOrderItem ? maxOrderItem.order + this.ORDER_INTERVAL : 1;
|
const newOrder = maxOrderItem
|
||||||
|
? maxOrderItem.order + this.ORDER_INTERVAL
|
||||||
|
: 1;
|
||||||
// 将 order 添加到创建参数中
|
// 将 order 添加到创建参数中
|
||||||
(args as any).data.order = newOrder;
|
(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) {
|
} catch (error) {
|
||||||
this.handleError(error, 'create');
|
this.handleError(error, 'create');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates multiple new records with the given data.
|
* Creates multiple new records with the given data.
|
||||||
* @param args - Arguments to create multiple records.
|
* @param args - Arguments to create multiple records.
|
||||||
|
@ -179,9 +180,14 @@ export class BaseService<
|
||||||
* @example
|
* @example
|
||||||
* const newUsers = await service.createMany({ data: [{ name: 'John' }, { name: 'Jane' }] });
|
* 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 {
|
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) {
|
} catch (error) {
|
||||||
this.handleError(error, 'create');
|
this.handleError(error, 'create');
|
||||||
}
|
}
|
||||||
|
@ -196,8 +202,9 @@ export class BaseService<
|
||||||
*/
|
*/
|
||||||
async update(args: A['update'], params?: any): Promise<R['update']> {
|
async update(args: A['update'], params?: any): Promise<R['update']> {
|
||||||
try {
|
try {
|
||||||
|
return this.getModel(params?.tx).update(args as any) as Promise<
|
||||||
return this.getModel(params?.tx).update(args as any) as Promise<R['update']>;
|
R['update']
|
||||||
|
>;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.handleError(error, 'update');
|
this.handleError(error, 'update');
|
||||||
}
|
}
|
||||||
|
@ -251,7 +258,9 @@ export class BaseService<
|
||||||
*/
|
*/
|
||||||
async delete(args: A['delete'], params?: any): Promise<R['delete']> {
|
async delete(args: A['delete'], params?: any): Promise<R['delete']> {
|
||||||
try {
|
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) {
|
} catch (error) {
|
||||||
this.handleError(error, 'delete');
|
this.handleError(error, 'delete');
|
||||||
}
|
}
|
||||||
|
@ -309,10 +318,14 @@ export class BaseService<
|
||||||
* @example
|
* @example
|
||||||
* const deleteResult = await service.deleteMany({ where: { isActive: false } });
|
* 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 {
|
try {
|
||||||
|
return this.getModel(params?.tx).deleteMany(args as any) as Promise<
|
||||||
return this.getModel(params?.tx).deleteMany(args as any) as Promise<R['deleteMany']>;
|
R['deleteMany']
|
||||||
|
>;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.handleError(error, 'delete');
|
this.handleError(error, 'delete');
|
||||||
}
|
}
|
||||||
|
@ -327,7 +340,9 @@ export class BaseService<
|
||||||
*/
|
*/
|
||||||
async updateMany(args: A['updateMany']): Promise<R['updateMany']> {
|
async updateMany(args: A['updateMany']): Promise<R['updateMany']> {
|
||||||
try {
|
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) {
|
} catch (error) {
|
||||||
this.handleError(error, 'update');
|
this.handleError(error, 'update');
|
||||||
}
|
}
|
||||||
|
@ -420,8 +435,7 @@ export class BaseService<
|
||||||
data: { ...data, deletedAt: null } as any,
|
data: { ...data, deletedAt: null } as any,
|
||||||
}) as Promise<R['update'][]>;
|
}) as Promise<R['update'][]>;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.handleError(error, "update");
|
this.handleError(error, 'update');
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -436,25 +450,25 @@ export class BaseService<
|
||||||
page?: number;
|
page?: number;
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
where?: WhereArgs<A['findMany']>;
|
where?: WhereArgs<A['findMany']>;
|
||||||
select?: SelectArgs<A['findMany']>
|
select?: SelectArgs<A['findMany']>;
|
||||||
}): Promise<{ items: R['findMany']; totalPages: number }> {
|
}): Promise<{ items: R['findMany']; totalPages: number }> {
|
||||||
const { page = 1, pageSize = 10, where, select } = args;
|
const { page = 1, pageSize = 10, where, select } = args;
|
||||||
|
|
||||||
try {
|
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,
|
where,
|
||||||
select,
|
select,
|
||||||
skip: (page - 1) * pageSize,
|
skip: (page - 1) * pageSize,
|
||||||
take: pageSize,
|
take: pageSize,
|
||||||
} as any) as R['findMany'];
|
} as any)) as R['findMany'];
|
||||||
// 计算总页数
|
// 计算总页数
|
||||||
const totalPages = Math.ceil(total / pageSize);
|
const totalPages = Math.ceil(total / pageSize);
|
||||||
return {
|
return {
|
||||||
items,
|
items,
|
||||||
totalPages
|
totalPages,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.handleError(error, 'read');
|
this.handleError(error, 'read');
|
||||||
|
@ -483,7 +497,6 @@ export class BaseService<
|
||||||
: undefined,
|
: undefined,
|
||||||
} as any)) as any[];
|
} as any)) as any[];
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理下一页游标
|
* 处理下一页游标
|
||||||
* @description
|
* @description
|
||||||
|
@ -530,7 +543,7 @@ export class BaseService<
|
||||||
order: { gt: targetObject.order },
|
order: { gt: targetObject.order },
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
},
|
},
|
||||||
orderBy: { order: 'asc' }
|
orderBy: { order: 'asc' },
|
||||||
} as any)) as any;
|
} as any)) as any;
|
||||||
|
|
||||||
const newOrder = nextObject
|
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 { Controller, Get, Query, UseGuards } from '@nestjs/common';
|
||||||
|
|
||||||
import { PostService } from './post.service';
|
import { PostService } from './post.service';
|
||||||
import { AuthGuard } from '@server/auth/auth.guard';
|
|
||||||
import { db } from '@nice/common';
|
|
||||||
|
|
||||||
@Controller('post')
|
@Controller('post')
|
||||||
export class PostController {
|
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 { Prisma } from '@nice/common';
|
||||||
import { PostService } from './post.service';
|
import { PostService } from './post.service';
|
||||||
import { z, ZodType } from 'zod';
|
import { z, ZodType } from 'zod';
|
||||||
|
import { PostMeta } from '@nice/common';
|
||||||
|
import { getClientIp } from './utils';
|
||||||
const PostCreateArgsSchema: ZodType<Prisma.PostCreateArgs> = z.any();
|
const PostCreateArgsSchema: ZodType<Prisma.PostCreateArgs> = z.any();
|
||||||
const PostUpdateArgsSchema: ZodType<Prisma.PostUpdateArgs> = z.any();
|
const PostUpdateArgsSchema: ZodType<Prisma.PostUpdateArgs> = z.any();
|
||||||
const PostFindFirstArgsSchema: ZodType<Prisma.PostFindFirstArgs> = z.any();
|
const PostFindFirstArgsSchema: ZodType<Prisma.PostFindFirstArgs> = z.any();
|
||||||
|
@ -20,7 +22,17 @@ export class PostRouter {
|
||||||
create: this.trpc.protectProcedure
|
create: this.trpc.protectProcedure
|
||||||
.input(PostCreateArgsSchema)
|
.input(PostCreateArgsSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.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 });
|
return await this.postService.create(input, { staff });
|
||||||
}),
|
}),
|
||||||
softDeleteByIds: this.trpc.protectProcedure
|
softDeleteByIds: this.trpc.protectProcedure
|
||||||
|
|
|
@ -3,9 +3,7 @@ import {
|
||||||
db,
|
db,
|
||||||
Prisma,
|
Prisma,
|
||||||
UserProfile,
|
UserProfile,
|
||||||
VisitType,
|
|
||||||
Post,
|
Post,
|
||||||
PostType,
|
|
||||||
RolePerms,
|
RolePerms,
|
||||||
ResPerm,
|
ResPerm,
|
||||||
ObjectType,
|
ObjectType,
|
||||||
|
@ -45,13 +43,13 @@ export class PostService extends BaseService<Prisma.PostDelegate> {
|
||||||
operation: CrudOperation.UPDATED,
|
operation: CrudOperation.UPDATED,
|
||||||
data: result,
|
data: result,
|
||||||
});
|
});
|
||||||
return result
|
return result;
|
||||||
}
|
}
|
||||||
async findManyWithCursor(args: Prisma.PostFindManyArgs, staff?: UserProfile) {
|
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);
|
args.where.OR = await this.preFilter(args.where.OR, staff);
|
||||||
return this.wrapResult(super.findManyWithCursor(args), async (result) => {
|
return this.wrapResult(super.findManyWithCursor(args), async (result) => {
|
||||||
let { items } = result;
|
const { items } = result;
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
items.map(async (item) => {
|
items.map(async (item) => {
|
||||||
await setPostRelation({ data: item, staff });
|
await setPostRelation({ data: item, staff });
|
||||||
|
@ -68,7 +66,7 @@ export class PostService extends BaseService<Prisma.PostDelegate> {
|
||||||
delete: false,
|
delete: false,
|
||||||
};
|
};
|
||||||
const isMySelf = data?.authorId === staff?.id;
|
const isMySelf = data?.authorId === staff?.id;
|
||||||
const isDomain = staff.domainId === data.domainId;
|
// const isDomain = staff.domainId === data.domainId;
|
||||||
const setManagePermissions = (perms: ResPerm) => {
|
const setManagePermissions = (perms: ResPerm) => {
|
||||||
Object.assign(perms, {
|
Object.assign(perms, {
|
||||||
delete: true,
|
delete: true,
|
||||||
|
@ -84,11 +82,11 @@ export class PostService extends BaseService<Prisma.PostDelegate> {
|
||||||
case RolePerms.MANAGE_ANY_POST:
|
case RolePerms.MANAGE_ANY_POST:
|
||||||
setManagePermissions(perms);
|
setManagePermissions(perms);
|
||||||
break;
|
break;
|
||||||
case RolePerms.MANAGE_DOM_POST:
|
// case RolePerms.MANAGE_DOM_POST:
|
||||||
if (isDomain) {
|
// if (isDomain) {
|
||||||
setManagePermissions(perms);
|
// setManagePermissions(perms);
|
||||||
}
|
// }
|
||||||
break;
|
// break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
Object.assign(data, { perms });
|
Object.assign(data, { perms });
|
||||||
|
@ -99,51 +97,28 @@ export class PostService extends BaseService<Prisma.PostDelegate> {
|
||||||
return outOR?.length > 0 ? outOR : undefined;
|
return outOR?.length > 0 ? outOR : undefined;
|
||||||
}
|
}
|
||||||
async getPostPreFilter(staff?: UserProfile) {
|
async getPostPreFilter(staff?: UserProfile) {
|
||||||
if (!staff) return
|
if (!staff) return;
|
||||||
const { deptId, domainId } = staff;
|
// const { deptId, domainId } = staff;
|
||||||
if (
|
if (
|
||||||
staff.permissions.includes(RolePerms.READ_ANY_POST) ||
|
staff.permissions.includes(RolePerms.READ_ANY_POST) ||
|
||||||
staff.permissions.includes(RolePerms.MANAGE_ANY_POST)
|
staff.permissions.includes(RolePerms.MANAGE_ANY_POST)
|
||||||
) {
|
) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const parentDeptIds =
|
|
||||||
(await this.departmentService.getAncestorIds(staff.deptId)) || [];
|
|
||||||
const orCondition: Prisma.PostWhereInput[] = [
|
const orCondition: Prisma.PostWhereInput[] = [
|
||||||
staff?.id && {
|
staff?.id && {
|
||||||
authorId: staff.id,
|
authorId: staff.id,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
isPublic: true,
|
||||||
|
},
|
||||||
staff?.id && {
|
staff?.id && {
|
||||||
watchableStaffs: {
|
receivers: {
|
||||||
some: {
|
some: {
|
||||||
id: staff.id,
|
id: staff.id,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
deptId && {
|
|
||||||
watchableDepts: {
|
|
||||||
some: {
|
|
||||||
id: {
|
|
||||||
in: parentDeptIds,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
AND: [
|
|
||||||
{
|
|
||||||
watchableStaffs: {
|
|
||||||
none: {}, // 匹配 watchableStaffs 为空
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
watchableDepts: {
|
|
||||||
none: {}, // 匹配 watchableDepts 为空
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
|
|
||||||
if (orCondition?.length > 0) return orCondition;
|
if (orCondition?.length > 0) return orCondition;
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
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 }) {
|
export async function setPostRelation(params: {
|
||||||
const { data, staff } = params
|
data: Post;
|
||||||
|
staff?: UserProfile;
|
||||||
|
}) {
|
||||||
|
const { data, staff } = params;
|
||||||
const limitedComments = await db.post.findMany({
|
const limitedComments = await db.post.findMany({
|
||||||
where: {
|
where: {
|
||||||
parentId: data.id,
|
parentId: data.id,
|
||||||
|
@ -39,6 +42,21 @@ export async function setPostRelation(params: { data: Post, staff?: UserProfile
|
||||||
limitedComments,
|
limitedComments,
|
||||||
commentsCount,
|
commentsCount,
|
||||||
// trouble
|
// 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 { Injectable } from '@nestjs/common';
|
||||||
import { BaseService } from '../base/base.service';
|
import { BaseService } from '../base/base.service';
|
||||||
import {
|
import { UserProfile, db, ObjectType, Prisma, VisitType } from '@nice/common';
|
||||||
UserProfile,
|
|
||||||
db,
|
|
||||||
ObjectType,
|
|
||||||
Prisma,
|
|
||||||
VisitType,
|
|
||||||
} from '@nice/common';
|
|
||||||
import EventBus from '@server/utils/event-bus';
|
import EventBus from '@server/utils/event-bus';
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class VisitService extends BaseService<Prisma.VisitDelegate> {
|
export class VisitService extends BaseService<Prisma.VisitDelegate> {
|
||||||
|
@ -14,14 +8,14 @@ export class VisitService extends BaseService<Prisma.VisitDelegate> {
|
||||||
super(db, ObjectType.VISIT);
|
super(db, ObjectType.VISIT);
|
||||||
}
|
}
|
||||||
async create(args: Prisma.VisitCreateArgs, staff?: UserProfile) {
|
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;
|
const visitorId = args.data.visitorId || staff?.id;
|
||||||
let result;
|
let result;
|
||||||
const existingVisit = await db.visit.findFirst({
|
const existingVisit = await db.visit.findFirst({
|
||||||
where: {
|
where: {
|
||||||
type: args.data.type,
|
type: args.data.type,
|
||||||
visitorId,
|
visitorId,
|
||||||
OR: [{ postId }, { lectureId }, { messageId }],
|
OR: [{ postId }, { messageId }],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!existingVisit) {
|
if (!existingVisit) {
|
||||||
|
@ -50,12 +44,12 @@ export class VisitService extends BaseService<Prisma.VisitDelegate> {
|
||||||
const createData: Prisma.VisitCreateManyInput[] = [];
|
const createData: Prisma.VisitCreateManyInput[] = [];
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
data.map(async (item) => {
|
data.map(async (item) => {
|
||||||
if (staff && !item.visitorId) item.visitorId = staff.id
|
if (staff && !item.visitorId) item.visitorId = staff.id;
|
||||||
const { postId, lectureId, messageId, visitorId } = item;
|
const { postId, messageId, visitorId } = item;
|
||||||
const existingVisit = await db.visit.findFirst({
|
const existingVisit = await db.visit.findFirst({
|
||||||
where: {
|
where: {
|
||||||
visitorId,
|
visitorId,
|
||||||
OR: [{ postId }, { lectureId }, { messageId }],
|
OR: [{ postId }, { messageId }],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,8 @@
|
||||||
import { Job } from 'bullmq';
|
import { Job } from 'bullmq';
|
||||||
import { Logger } from '@nestjs/common';
|
import { Logger } from '@nestjs/common';
|
||||||
import {
|
|
||||||
updateCourseLectureStats,
|
|
||||||
updateSectionLectureStats
|
|
||||||
} from '@server/models/lecture/utils';
|
|
||||||
import { ObjectType } from '@nice/common';
|
import { ObjectType } from '@nice/common';
|
||||||
import {
|
|
||||||
updateCourseEnrollmentStats,
|
|
||||||
updateCourseReviewStats
|
|
||||||
} from '@server/models/course/utils';
|
|
||||||
import { QueueJobType } from '../types';
|
import { QueueJobType } from '../types';
|
||||||
const logger = new Logger('QueueWorker');
|
const logger = new Logger('QueueWorker');
|
||||||
export default async function processJob(job: Job<any, any, QueueJobType>) {
|
export default async function processJob(job: Job<any, any, QueueJobType>) {
|
||||||
|
@ -16,34 +10,37 @@ export default async function processJob(job: Job<any, any, QueueJobType>) {
|
||||||
if (job.name === QueueJobType.UPDATE_STATS) {
|
if (job.name === QueueJobType.UPDATE_STATS) {
|
||||||
const { sectionId, courseId, type } = job.data;
|
const { sectionId, courseId, type } = job.data;
|
||||||
// 处理 section 统计
|
// 处理 section 统计
|
||||||
if (sectionId) {
|
// if (sectionId) {
|
||||||
await updateSectionLectureStats(sectionId);
|
// await updateSectionLectureStats(sectionId);
|
||||||
logger.debug(`Updated section stats for sectionId: ${sectionId}`);
|
// logger.debug(`Updated section stats for sectionId: ${sectionId}`);
|
||||||
}
|
// }
|
||||||
// 如果没有 courseId,提前返回
|
// // 如果没有 courseId,提前返回
|
||||||
if (!courseId) {
|
// if (!courseId) {
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
// 处理 course 相关统计
|
// 处理 course 相关统计
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case ObjectType.LECTURE:
|
// case ObjectType.LECTURE:
|
||||||
await updateCourseLectureStats(courseId);
|
// await updateCourseLectureStats(courseId);
|
||||||
break;
|
// break;
|
||||||
case ObjectType.ENROLLMENT:
|
// case ObjectType.ENROLLMENT:
|
||||||
await updateCourseEnrollmentStats(courseId);
|
// await updateCourseEnrollmentStats(courseId);
|
||||||
break;
|
// break;
|
||||||
case ObjectType.POST:
|
// case ObjectType.POST:
|
||||||
await updateCourseReviewStats(courseId);
|
// await updateCourseReviewStats(courseId);
|
||||||
break;
|
// break;
|
||||||
default:
|
default:
|
||||||
logger.warn(`Unknown update stats type: ${type}`);
|
logger.warn(`Unknown update stats type: ${type}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(`Updated course stats for courseId: ${courseId}, type: ${type}`);
|
logger.debug(
|
||||||
|
`Updated course stats for courseId: ${courseId}, type: ${type}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.error(`Error processing stats update job: ${error.message}`, error.stack);
|
logger.error(
|
||||||
|
`Error processing stats update job: ${error.message}`,
|
||||||
|
error.stack,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -142,12 +142,13 @@ export class InitService {
|
||||||
await this.createRoot();
|
await this.createRoot();
|
||||||
await this.createOrUpdateTaxonomy();
|
await this.createOrUpdateTaxonomy();
|
||||||
await this.initAppConfigs();
|
await this.initAppConfigs();
|
||||||
try {
|
//不需要minio
|
||||||
this.logger.log('Initialize minio');
|
// try {
|
||||||
await this.createBucket();
|
// this.logger.log('Initialize minio');
|
||||||
} catch (error) {
|
// await this.createBucket();
|
||||||
this.logger.error('Minio initialization failed:', error);
|
// } catch (error) {
|
||||||
}
|
// this.logger.error('Minio initialization failed:', error);
|
||||||
|
// }
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -14,9 +14,7 @@ import { VisitModule } from '@server/models/visit/visit.module';
|
||||||
import { WebSocketModule } from '@server/socket/websocket.module';
|
import { WebSocketModule } from '@server/socket/websocket.module';
|
||||||
import { RoleMapModule } from '@server/models/rbac/rbac.module';
|
import { RoleMapModule } from '@server/models/rbac/rbac.module';
|
||||||
import { TransformModule } from '@server/models/transform/transform.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({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
AuthModule,
|
AuthModule,
|
||||||
|
@ -31,10 +29,7 @@ import { SectionModule } from '@server/models/section/section.module';
|
||||||
AppConfigModule,
|
AppConfigModule,
|
||||||
PostModule,
|
PostModule,
|
||||||
VisitModule,
|
VisitModule,
|
||||||
CourseModule,
|
WebSocketModule,
|
||||||
LectureModule,
|
|
||||||
SectionModule,
|
|
||||||
WebSocketModule
|
|
||||||
],
|
],
|
||||||
controllers: [],
|
controllers: [],
|
||||||
providers: [TrpcService, TrpcRouter, Logger],
|
providers: [TrpcService, TrpcRouter, Logger],
|
||||||
|
|
|
@ -13,12 +13,10 @@ import { VisitRouter } from '@server/models/visit/visit.router';
|
||||||
import { RoleMapRouter } from '@server/models/rbac/rolemap.router';
|
import { RoleMapRouter } from '@server/models/rbac/rolemap.router';
|
||||||
import { TransformRouter } from '@server/models/transform/transform.router';
|
import { TransformRouter } from '@server/models/transform/transform.router';
|
||||||
import { RoleRouter } from '@server/models/rbac/role.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()
|
@Injectable()
|
||||||
export class TrpcRouter {
|
export class TrpcRouter {
|
||||||
logger = new Logger(TrpcRouter.name)
|
logger = new Logger(TrpcRouter.name);
|
||||||
constructor(
|
constructor(
|
||||||
private readonly trpc: TrpcService,
|
private readonly trpc: TrpcService,
|
||||||
private readonly post: PostRouter,
|
private readonly post: PostRouter,
|
||||||
|
@ -32,13 +30,9 @@ export class TrpcRouter {
|
||||||
private readonly app_config: AppConfigRouter,
|
private readonly app_config: AppConfigRouter,
|
||||||
private readonly message: MessageRouter,
|
private readonly message: MessageRouter,
|
||||||
private readonly visitor: VisitRouter,
|
private readonly visitor: VisitRouter,
|
||||||
private readonly course: CourseRouter,
|
|
||||||
private readonly lecture: LectureRouter,
|
|
||||||
private readonly section: SectionRouter
|
|
||||||
// private readonly websocketService: WebSocketService
|
// private readonly websocketService: WebSocketService
|
||||||
) {}
|
) {}
|
||||||
appRouter = this.trpc.router({
|
appRouter = this.trpc.router({
|
||||||
|
|
||||||
transform: this.transform.router,
|
transform: this.transform.router,
|
||||||
post: this.post.router,
|
post: this.post.router,
|
||||||
department: this.department.router,
|
department: this.department.router,
|
||||||
|
@ -50,11 +44,8 @@ export class TrpcRouter {
|
||||||
message: this.message.router,
|
message: this.message.router,
|
||||||
app_config: this.app_config.router,
|
app_config: this.app_config.router,
|
||||||
visitor: this.visitor.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) {
|
async applyMiddleware(app: INestApplication) {
|
||||||
app.use(
|
app.use(
|
||||||
|
@ -65,7 +56,7 @@ export class TrpcRouter {
|
||||||
onError(opts) {
|
onError(opts) {
|
||||||
const { error, type, path, input, ctx, req } = opts;
|
const { error, type, path, input, ctx, req } = opts;
|
||||||
// console.error('TRPC Error:', error);
|
// console.error('TRPC Error:', error);
|
||||||
}
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
// applyWSSHandler({
|
// applyWSSHandler({
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
//trpc.service.ts
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { initTRPC, TRPCError } from '@trpc/server';
|
import { initTRPC, TRPCError } from '@trpc/server';
|
||||||
import superjson from 'superjson-cjs';
|
import superjson from 'superjson-cjs';
|
||||||
|
@ -12,9 +13,17 @@ export class TrpcService {
|
||||||
|
|
||||||
async createExpressContext(
|
async createExpressContext(
|
||||||
opts: trpcExpress.CreateExpressContextOptions,
|
opts: trpcExpress.CreateExpressContextOptions,
|
||||||
): Promise<{ staff: UserProfile | undefined }> {
|
): Promise<{
|
||||||
|
staff: UserProfile | undefined;
|
||||||
|
req: trpcExpress.CreateExpressContextOptions['req'];
|
||||||
|
}> {
|
||||||
const token = opts.req.headers.authorization?.split(' ')[1];
|
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(
|
async createWSSContext(
|
||||||
opts: CreateWSSContextFnOptions,
|
opts: CreateWSSContextFnOptions,
|
||||||
|
@ -45,6 +54,7 @@ export class TrpcService {
|
||||||
ctx: {
|
ctx: {
|
||||||
// User value is confirmed to be non-null at this point
|
// User value is confirmed to be non-null at this point
|
||||||
staff: ctx.staff,
|
staff: ctx.staff,
|
||||||
|
req: ctx.req,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,24 +1,25 @@
|
||||||
import GraphEditor from '@web/src/components/common/editor/graph/GraphEditor';
|
import GraphEditor from "@web/src/components/common/editor/graph/GraphEditor";
|
||||||
import React, { useState, useCallback } from 'react';
|
import { usePost } from "@nice/client";
|
||||||
import * as tus from 'tus-js-client';
|
import React, { useState, useCallback } from "react";
|
||||||
|
import * as tus from "tus-js-client";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
interface TusUploadProps {
|
interface TusUploadProps {
|
||||||
onSuccess?: (response: any) => void;
|
onSuccess?: (response: any) => void;
|
||||||
onError?: (error: Error) => void;
|
onError?: (error: Error) => void;
|
||||||
}
|
}
|
||||||
const TusUploader: React.FC<TusUploadProps> = ({
|
const TusUploader: React.FC<TusUploadProps> = ({ onSuccess, onError }) => {
|
||||||
onSuccess,
|
const { create } = usePost();
|
||||||
onError
|
|
||||||
}) => {
|
|
||||||
const [progress, setProgress] = useState<number>(0);
|
const [progress, setProgress] = useState<number>(0);
|
||||||
const [isUploading, setIsUploading] = useState<boolean>(false);
|
const [isUploading, setIsUploading] = useState<boolean>(false);
|
||||||
const [uploadError, setUploadError] = useState<string | null>(null);
|
const [uploadError, setUploadError] = useState<string | null>(null);
|
||||||
const handleFileUpload = useCallback((file: File) => {
|
const handleFileUpload = useCallback(
|
||||||
|
(file: File) => {
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
setIsUploading(true);
|
setIsUploading(true);
|
||||||
setProgress(0);
|
setProgress(0);
|
||||||
setUploadError(null);
|
setUploadError(null);
|
||||||
// Extract file extension
|
// Extract file extension
|
||||||
const extension = file.name.split('.').pop() || '';
|
const extension = file.name.split(".").pop() || "";
|
||||||
const upload = new tus.Upload(file, {
|
const upload = new tus.Upload(file, {
|
||||||
endpoint: "http://localhost:3000/upload",
|
endpoint: "http://localhost:3000/upload",
|
||||||
retryDelays: [0, 1000, 3000, 5000],
|
retryDelays: [0, 1000, 3000, 5000],
|
||||||
|
@ -30,33 +31,51 @@ const TusUploader: React.FC<TusUploadProps> = ({
|
||||||
modifiedAt: new Date(file.lastModified).toISOString(),
|
modifiedAt: new Date(file.lastModified).toISOString(),
|
||||||
},
|
},
|
||||||
onProgress: (bytesUploaded, bytesTotal) => {
|
onProgress: (bytesUploaded, bytesTotal) => {
|
||||||
const percentage = ((bytesUploaded / bytesTotal) * 100).toFixed(2);
|
const percentage = (
|
||||||
|
(bytesUploaded / bytesTotal) *
|
||||||
|
100
|
||||||
|
).toFixed(2);
|
||||||
setProgress(Number(percentage));
|
setProgress(Number(percentage));
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setIsUploading(false);
|
setIsUploading(false);
|
||||||
setProgress(100);
|
setProgress(100);
|
||||||
onSuccess && onSuccess(upload);
|
onSuccess?.(upload);
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
setIsUploading(false);
|
setIsUploading(false);
|
||||||
setUploadError(error.message);
|
setUploadError(error.message);
|
||||||
onError && onError(error);
|
onError?.(error);
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
upload.start();
|
upload.start();
|
||||||
}, [onSuccess, onError]);
|
},
|
||||||
|
[onSuccess, onError]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className='w-full' style={{ height: 800 }}>
|
<button
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
await create.mutateAsync({
|
||||||
|
data: {
|
||||||
|
title: "123",
|
||||||
|
isPublic: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
toast.success("创建成功");
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
toast.error("创建失败");
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
123
|
||||||
|
</button>
|
||||||
|
<div className="w-full" style={{ height: 800 }}>
|
||||||
<GraphEditor></GraphEditor>
|
<GraphEditor></GraphEditor>
|
||||||
</div>
|
</div>
|
||||||
{/* <div className=' h-screen'>
|
|
||||||
<MindMap></MindMap>
|
|
||||||
</div> */}
|
|
||||||
{/* <MindMapEditor></MindMapEditor> */}
|
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
|
@ -72,9 +91,7 @@ const TusUploader: React.FC<TusUploadProps> = ({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{uploadError && (
|
{uploadError && (
|
||||||
<div style={{ color: 'red' }}>
|
<div style={{ color: "red" }}>上传错误: {uploadError}</div>
|
||||||
上传错误: {uploadError}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,26 +2,26 @@ import { createContext, useContext, ReactNode, useEffect } from "react";
|
||||||
import { useForm, FormProvider, SubmitHandler } from "react-hook-form";
|
import { useForm, FormProvider, SubmitHandler } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { CourseDto, CourseLevel, CourseStatus } from "@nice/common";
|
// import { CourseDto, CourseLevel, CourseStatus } from "@nice/common";
|
||||||
import { api, useCourse } from "@nice/client";
|
// import { api, useCourse } from "@nice/client";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
// 定义课程表单验证 Schema
|
// 定义课程表单验证 Schema
|
||||||
const courseSchema = z.object({
|
const courseSchema = z.object({
|
||||||
title: z.string().min(1, '课程标题不能为空'),
|
title: z.string().min(1, "课程标题不能为空"),
|
||||||
subTitle: z.string().nullish(),
|
subTitle: z.string().nullish(),
|
||||||
description: z.string().nullish(),
|
description: z.string().nullish(),
|
||||||
thumbnail: z.string().url().nullish(),
|
thumbnail: z.string().url().nullish(),
|
||||||
level: z.nativeEnum(CourseLevel),
|
// level: z.nativeEnum(CourseLevel),
|
||||||
requirements: z.array(z.string()).nullish(),
|
requirements: z.array(z.string()).nullish(),
|
||||||
objectives: z.array(z.string()).nullish()
|
objectives: z.array(z.string()).nullish(),
|
||||||
});
|
});
|
||||||
export type CourseFormData = z.infer<typeof courseSchema>;
|
export type CourseFormData = z.infer<typeof courseSchema>;
|
||||||
interface CourseEditorContextType {
|
interface CourseEditorContextType {
|
||||||
onSubmit: SubmitHandler<CourseFormData>;
|
onSubmit: SubmitHandler<CourseFormData>;
|
||||||
editId?: string; // 添加 editId
|
editId?: string; // 添加 editId
|
||||||
part?: string;
|
part?: string;
|
||||||
course?: CourseDto;
|
// course?: CourseDto;
|
||||||
}
|
}
|
||||||
interface CourseFormProviderProps {
|
interface CourseFormProviderProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
@ -29,67 +29,72 @@ interface CourseFormProviderProps {
|
||||||
part?: string;
|
part?: string;
|
||||||
}
|
}
|
||||||
const CourseEditorContext = createContext<CourseEditorContextType | null>(null);
|
const CourseEditorContext = createContext<CourseEditorContextType | null>(null);
|
||||||
export function CourseFormProvider({ children, editId }: CourseFormProviderProps) {
|
export function CourseFormProvider({
|
||||||
const { create, update } = useCourse()
|
children,
|
||||||
const { data: course }: { data: CourseDto } = api.course.findFirst.useQuery({ where: { id: editId } }, { enabled: Boolean(editId) })
|
editId,
|
||||||
const navigate = useNavigate()
|
}: 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>({
|
const methods = useForm<CourseFormData>({
|
||||||
resolver: zodResolver(courseSchema),
|
resolver: zodResolver(courseSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
level: CourseLevel.BEGINNER,
|
// level: CourseLevel.BEGINNER,
|
||||||
requirements: [],
|
requirements: [],
|
||||||
objectives: []
|
objectives: [],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
if (course) {
|
// if (course) {
|
||||||
const formData = {
|
// const formData = {
|
||||||
title: course.title,
|
// title: course.title,
|
||||||
subTitle: course.subTitle,
|
// subTitle: course.subTitle,
|
||||||
description: course.description,
|
// description: course.description,
|
||||||
thumbnail: course.thumbnail,
|
// thumbnail: course.thumbnail,
|
||||||
level: course.level,
|
// level: course.level,
|
||||||
requirements: course.requirements,
|
// requirements: course.requirements,
|
||||||
objectives: course.objectives,
|
// objectives: course.objectives,
|
||||||
status: course.status,
|
// status: course.status,
|
||||||
};
|
// };
|
||||||
methods.reset(formData as any);
|
// methods.reset(formData as any);
|
||||||
}
|
// }
|
||||||
}, [course, methods]);
|
// }, [course, methods]);
|
||||||
const onSubmit: SubmitHandler<CourseFormData> = async (data: CourseFormData) => {
|
const onSubmit: SubmitHandler<CourseFormData> = async (
|
||||||
|
data: CourseFormData
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
if (editId) {
|
if (editId) {
|
||||||
await update.mutateAsync({
|
// await update.mutateAsync({
|
||||||
where: { id: editId },
|
// where: { id: editId },
|
||||||
data: {
|
// data: {
|
||||||
...data
|
// ...data
|
||||||
}
|
// }
|
||||||
})
|
// })
|
||||||
toast.success('课程更新成功!');
|
toast.success("课程更新成功!");
|
||||||
} else {
|
} else {
|
||||||
const result = await create.mutateAsync({
|
// const result = await create.mutateAsync({
|
||||||
data: {
|
// data: {
|
||||||
status: CourseStatus.DRAFT,
|
// status: CourseStatus.DRAFT,
|
||||||
...data
|
// ...data
|
||||||
}
|
// }
|
||||||
})
|
// })
|
||||||
navigate(`/course/${result.id}/editor`, { replace: true })
|
// navigate(`/course/${result.id}/editor`, { replace: true });
|
||||||
toast.success('课程创建成功!');
|
toast.success("课程创建成功!");
|
||||||
}
|
}
|
||||||
methods.reset(data);
|
methods.reset(data);
|
||||||
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error submitting form:', error);
|
console.error("Error submitting form:", error);
|
||||||
toast.error('操作失败,请重试!');
|
toast.error("操作失败,请重试!");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<CourseEditorContext.Provider value={{ onSubmit, editId, course }}>
|
<CourseEditorContext.Provider
|
||||||
<FormProvider {...methods}>
|
value={{
|
||||||
{children}
|
onSubmit,
|
||||||
</FormProvider>
|
editId,
|
||||||
|
// course
|
||||||
|
}}>
|
||||||
|
<FormProvider {...methods}>{children}</FormProvider>
|
||||||
</CourseEditorContext.Provider>
|
</CourseEditorContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {
|
||||||
import apiClient from "../utils/axios-client";
|
import apiClient from "../utils/axios-client";
|
||||||
import { AuthSchema, RolePerms, UserProfile } from "@nice/common";
|
import { AuthSchema, RolePerms, UserProfile } from "@nice/common";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
interface AuthContextProps {
|
interface AuthContextProps {
|
||||||
accessToken: string | null;
|
accessToken: string | null;
|
||||||
refreshToken: string | null;
|
refreshToken: string | null;
|
||||||
|
@ -144,6 +145,7 @@ export function AuthProvider({ children }: AuthProviderProps) {
|
||||||
startTokenRefreshInterval();
|
startTokenRefreshInterval();
|
||||||
fetchUserProfile();
|
fetchUserProfile();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
console.log(err);
|
||||||
throw err;
|
throw err;
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
@ -157,6 +159,7 @@ export function AuthProvider({ children }: AuthProviderProps) {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
await apiClient.post(`/auth/signup`, data);
|
await apiClient.post(`/auth/signup`, data);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
console.log(err);
|
||||||
throw err;
|
throw err;
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsup",
|
"build": "tsup",
|
||||||
"dev": "tsup --watch",
|
"dev": "tsup --watch",
|
||||||
|
"dev-static": "tsup --no-watch",
|
||||||
"clean": "rimraf dist",
|
"clean": "rimraf dist",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
|
|
|
@ -9,4 +9,3 @@ export * from "./useTaxonomy"
|
||||||
export * from "./useVisitor"
|
export * from "./useVisitor"
|
||||||
export * from "./useMessage"
|
export * from "./useMessage"
|
||||||
export * from "./usePost"
|
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() {
|
export function usePost() {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const create = api.post.create.useMutation({
|
const create: any = api.post.create.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
utils.post.invalidate();
|
utils.post.invalidate();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const update = api.post.update.useMutation({
|
const update: any = api.post.update.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
utils.post.invalidate();
|
utils.post.invalidate();
|
||||||
},
|
},
|
||||||
|
@ -17,21 +17,21 @@ export function usePost() {
|
||||||
utils.post.invalidate();
|
utils.post.invalidate();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const softDeleteByIds = api.post.softDeleteByIds.useMutation({
|
const softDeleteByIds: any = api.post.softDeleteByIds.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
utils.post.invalidate();
|
utils.post.invalidate();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const restoreByIds = api.post.restoreByIds.useMutation({
|
const restoreByIds: any = api.post.restoreByIds.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
utils.post.invalidate();
|
utils.post.invalidate();
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
return {
|
return {
|
||||||
create,
|
create,
|
||||||
update,
|
update,
|
||||||
deleteMany,
|
deleteMany,
|
||||||
softDeleteByIds,
|
softDeleteByIds,
|
||||||
restoreByIds
|
restoreByIds,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,4 +7,4 @@ export * from "./hooks"
|
||||||
export * from "./websocket"
|
export * from "./websocket"
|
||||||
export * from "./event"
|
export * from "./event"
|
||||||
export * from "./types"
|
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
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -195,17 +195,14 @@ model Post {
|
||||||
// 关系类型字段
|
// 关系类型字段
|
||||||
authorId String? @map("author_id")
|
authorId String? @map("author_id")
|
||||||
author Staff? @relation("post_author", fields: [authorId], references: [id]) // 帖子作者,关联 Staff 模型
|
author Staff? @relation("post_author", fields: [authorId], references: [id]) // 帖子作者,关联 Staff 模型
|
||||||
|
|
||||||
visits Visit[] // 访问记录,关联 Visit 模型
|
visits Visit[] // 访问记录,关联 Visit 模型
|
||||||
receivers Staff[] @relation("post_receiver")
|
receivers Staff[] @relation("post_receiver")
|
||||||
|
|
||||||
parentId String? @map("parent_id")
|
parentId String? @map("parent_id")
|
||||||
parent Post? @relation("PostChildren", fields: [parentId], references: [id]) // 父级帖子,关联 Post 模型
|
parent Post? @relation("PostChildren", fields: [parentId], references: [id]) // 父级帖子,关联 Post 模型
|
||||||
children Post[] @relation("PostChildren") // 子级帖子列表,关联 Post 模型
|
children Post[] @relation("PostChildren") // 子级帖子列表,关联 Post 模型
|
||||||
|
|
||||||
resources Resource[] // 附件列表
|
resources Resource[] // 附件列表
|
||||||
isPublic Boolean? @default(false) @map("is_public")
|
isPublic Boolean? @default(false) @map("is_public")
|
||||||
meta Json?
|
meta Json? // 签名 和 IP
|
||||||
|
|
||||||
// 复合索引
|
// 复合索引
|
||||||
@@index([type, domainId]) // 类型和域组合查询
|
@@index([type, domainId]) // 类型和域组合查询
|
||||||
|
|
|
@ -158,6 +158,10 @@ export interface BaseSetting {
|
||||||
devDept?: string;
|
devDept?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
export interface PostMeta {
|
||||||
|
signature?: string;
|
||||||
|
ip?: string;
|
||||||
|
}
|
||||||
export type RowModelResult = {
|
export type RowModelResult = {
|
||||||
rowData: any[];
|
rowData: any[];
|
||||||
rowCount: number;
|
rowCount: number;
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsup",
|
"build": "tsup",
|
||||||
"dev": "tsup --watch",
|
"dev": "tsup --watch",
|
||||||
|
"dev-static": "tsup --no-watch",
|
||||||
"clean": "rimraf dist",
|
"clean": "rimraf dist",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
<h1>error</h1>
|
|
@ -0,0 +1 @@
|
||||||
|
<h1>test1</h1>
|
Loading…
Reference in New Issue