This commit is contained in:
longdayi 2025-01-22 19:25:46 +08:00
commit 9e47061e24
45 changed files with 386 additions and 1358 deletions

View File

@ -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

View File

@ -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 { }

View File

@ -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);
}),
});
}

View File

@ -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' },
});
}
}

View File

@ -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
}
});
}

View File

@ -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(),
});

View File

@ -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 { }

View File

@ -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);
}),
});
}

View File

@ -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
}
}

View File

@ -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 { }

View File

@ -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);
}),
});
}

View File

@ -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;
}
}

View File

@ -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
}
});
}

View File

@ -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) {}
}

View File

@ -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

View File

@ -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;

View File

@ -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 || '';
}

View File

@ -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 { }

View File

@ -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);
}),
});
}

View File

@ -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);
}
}

View File

@ -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 }],
},
});

View File

@ -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,
);
}
}

View File

@ -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 {

View File

@ -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 {}

View File

@ -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({

View File

@ -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,
},
});
});

View File

@ -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;

View File

@ -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 = () => {

View File

@ -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);

View File

@ -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"
}
}

View File

@ -9,4 +9,3 @@ export * from "./useTaxonomy"
export * from "./useVisitor"
export * from "./useMessage"
export * from "./usePost"
export * from "./useCourse"

View File

@ -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()
},
})
};
}

View File

@ -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,
};
}

View File

@ -7,4 +7,4 @@ export * from "./hooks"
export * from "./websocket"
export * from "./event"
export * from "./types"
export * from "./upload"
// export * from "./upload"

View File

@ -1,3 +0,0 @@
export * from "./types"
export * from "./uploadManager"
export * from "./useUpload"

View File

@ -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;
}

View File

@ -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;
}
}
}

View File

@ -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
};
}

View File

@ -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"
}
}

View File

@ -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]) // 类型和域组合查询

View File

@ -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;

View File

@ -8,6 +8,7 @@
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"dev-static": "tsup --no-watch",
"clean": "rimraf dist",
"typecheck": "tsc --noEmit"
},

1
web-app/error.html Normal file
View File

@ -0,0 +1 @@
<h1>error</h1>

1
web-app/index.html Normal file
View File

@ -0,0 +1 @@
<h1>test1</h1>