rht
This commit is contained in:
parent
e5f3954e67
commit
1a4b112599
|
@ -1,125 +0,0 @@
|
||||||
import { Controller, Get, Query, UseGuards } from '@nestjs/common';
|
|
||||||
|
|
||||||
import { MessageService } from './message.service';
|
|
||||||
import { AuthGuard } from '@server/auth/auth.guard';
|
|
||||||
import { db, VisitType } from '@nice/common';
|
|
||||||
|
|
||||||
@Controller('message')
|
|
||||||
export class MessageController {
|
|
||||||
constructor(private readonly messageService: MessageService) { }
|
|
||||||
@UseGuards(AuthGuard)
|
|
||||||
@Get('find-last-one')
|
|
||||||
async findLastOne(@Query('staff-id') staffId: string) {
|
|
||||||
try {
|
|
||||||
const result = await db.message.findFirst({
|
|
||||||
where: {
|
|
||||||
OR: [
|
|
||||||
{
|
|
||||||
receivers: {
|
|
||||||
some: {
|
|
||||||
id: staffId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
orderBy: { createdAt: 'desc' },
|
|
||||||
select: {
|
|
||||||
title: true,
|
|
||||||
content: true,
|
|
||||||
url: true
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
data: result,
|
|
||||||
errmsg: 'success',
|
|
||||||
errno: 0,
|
|
||||||
};
|
|
||||||
} catch (e) {
|
|
||||||
return {
|
|
||||||
data: {},
|
|
||||||
errmsg: (e as any)?.message || 'error',
|
|
||||||
errno: 1,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@UseGuards(AuthGuard)
|
|
||||||
@Get('find-unreaded')
|
|
||||||
async findUnreaded(@Query('staff-id') staffId: string) {
|
|
||||||
try {
|
|
||||||
const result = await db.message.findMany({
|
|
||||||
where: {
|
|
||||||
visits: {
|
|
||||||
none: {
|
|
||||||
id: staffId,
|
|
||||||
type: VisitType.READED
|
|
||||||
},
|
|
||||||
},
|
|
||||||
receivers: {
|
|
||||||
some: {
|
|
||||||
id: staffId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
orderBy: { createdAt: 'desc' },
|
|
||||||
select: {
|
|
||||||
title: true,
|
|
||||||
content: true,
|
|
||||||
url: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
data: result,
|
|
||||||
errmsg: 'success',
|
|
||||||
errno: 0,
|
|
||||||
};
|
|
||||||
} catch (e) {
|
|
||||||
return {
|
|
||||||
data: {},
|
|
||||||
errmsg: (e as any)?.message || 'error',
|
|
||||||
errno: 1,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@UseGuards(AuthGuard)
|
|
||||||
@Get('count-unreaded')
|
|
||||||
async countUnreaded(@Query('staff-id') staffId: string) {
|
|
||||||
try {
|
|
||||||
const result = await db.message.findMany({
|
|
||||||
where: {
|
|
||||||
visits: {
|
|
||||||
none: {
|
|
||||||
id: staffId,
|
|
||||||
type: VisitType.READED
|
|
||||||
},
|
|
||||||
},
|
|
||||||
receivers: {
|
|
||||||
some: {
|
|
||||||
id: staffId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
orderBy: { createdAt: 'desc' },
|
|
||||||
select: {
|
|
||||||
title: true,
|
|
||||||
content: true,
|
|
||||||
url: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
data: result,
|
|
||||||
errmsg: 'success',
|
|
||||||
errno: 0,
|
|
||||||
};
|
|
||||||
} catch (e) {
|
|
||||||
return {
|
|
||||||
data: {},
|
|
||||||
errmsg: (e as any)?.message || 'error',
|
|
||||||
errno: 1,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { MessageService } from './message.service';
|
|
||||||
import { MessageRouter } from './message.router';
|
|
||||||
import { TrpcService } from '@server/trpc/trpc.service';
|
|
||||||
import { DepartmentModule } from '../department/department.module';
|
|
||||||
import { MessageController } from './message.controller';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [DepartmentModule],
|
|
||||||
providers: [MessageService, MessageRouter, TrpcService],
|
|
||||||
exports: [MessageService, MessageRouter],
|
|
||||||
controllers: [MessageController],
|
|
||||||
})
|
|
||||||
export class MessageModule { }
|
|
|
@ -1,39 +0,0 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { TrpcService } from '@server/trpc/trpc.service';
|
|
||||||
import { MessageService } from './message.service';
|
|
||||||
import { Prisma } from '@nice/common';
|
|
||||||
import { z, ZodType } from 'zod';
|
|
||||||
const MessageUncheckedCreateInputSchema: ZodType<Prisma.MessageUncheckedCreateInput> = z.any()
|
|
||||||
const MessageWhereInputSchema: ZodType<Prisma.MessageWhereInput> = z.any()
|
|
||||||
const MessageSelectSchema: ZodType<Prisma.MessageSelect> = z.any()
|
|
||||||
@Injectable()
|
|
||||||
export class MessageRouter {
|
|
||||||
constructor(
|
|
||||||
private readonly trpc: TrpcService,
|
|
||||||
private readonly messageService: MessageService,
|
|
||||||
) { }
|
|
||||||
router = this.trpc.router({
|
|
||||||
create: this.trpc.procedure
|
|
||||||
.input(MessageUncheckedCreateInputSchema)
|
|
||||||
.mutation(async ({ ctx, input }) => {
|
|
||||||
const { staff } = ctx;
|
|
||||||
return await this.messageService.create({ data: input }, { staff });
|
|
||||||
}),
|
|
||||||
findManyWithCursor: this.trpc.protectProcedure
|
|
||||||
.input(z.object({
|
|
||||||
cursor: z.any().nullish(),
|
|
||||||
take: z.number().nullish(),
|
|
||||||
where: MessageWhereInputSchema.nullish(),
|
|
||||||
select: MessageSelectSchema.nullish()
|
|
||||||
}))
|
|
||||||
.query(async ({ ctx, input }) => {
|
|
||||||
const { staff } = ctx;
|
|
||||||
return await this.messageService.findManyWithCursor(input, staff);
|
|
||||||
}),
|
|
||||||
getUnreadCount: this.trpc.protectProcedure
|
|
||||||
.query(async ({ ctx }) => {
|
|
||||||
const { staff } = ctx;
|
|
||||||
return await this.messageService.getUnreadCount(staff);
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { UserProfile, db, Prisma, VisitType, ObjectType } from '@nice/common';
|
|
||||||
import { BaseService } from '../base/base.service';
|
|
||||||
import EventBus, { CrudOperation } from '@server/utils/event-bus';
|
|
||||||
import { setMessageRelation } from './utils';
|
|
||||||
@Injectable()
|
|
||||||
export class MessageService extends BaseService<Prisma.MessageDelegate> {
|
|
||||||
constructor() {
|
|
||||||
super(db, ObjectType.MESSAGE);
|
|
||||||
}
|
|
||||||
async create(args: Prisma.MessageCreateArgs, params?: { tx?: Prisma.MessageDelegate, staff?: UserProfile }) {
|
|
||||||
args.data!.senderId = params?.staff?.id;
|
|
||||||
args.include = {
|
|
||||||
receivers: {
|
|
||||||
select: { id: true, registerToken: true, username: true }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const result = await super.create(args);
|
|
||||||
EventBus.emit("dataChanged", {
|
|
||||||
type: ObjectType.MESSAGE,
|
|
||||||
operation: CrudOperation.CREATED,
|
|
||||||
data: result
|
|
||||||
})
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
async findManyWithCursor(
|
|
||||||
args: Prisma.MessageFindManyArgs,
|
|
||||||
staff?: UserProfile,
|
|
||||||
) {
|
|
||||||
|
|
||||||
return this.wrapResult(super.findManyWithCursor(args), async (result) => {
|
|
||||||
let { items } = result;
|
|
||||||
await Promise.all(
|
|
||||||
items.map(async (item) => {
|
|
||||||
await setMessageRelation(item, staff);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return { ...result, items };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
async getUnreadCount(staff?: UserProfile) {
|
|
||||||
const count = await db.message.count({
|
|
||||||
where: {
|
|
||||||
receivers: { some: { id: staff?.id } },
|
|
||||||
visits: {
|
|
||||||
none: {
|
|
||||||
visitorId: staff?.id,
|
|
||||||
type: VisitType.READED
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
import { Message, UserProfile, VisitType, db } from "@nice/common"
|
|
||||||
export async function setMessageRelation(
|
|
||||||
data: Message,
|
|
||||||
staff?: UserProfile,
|
|
||||||
): Promise<any> {
|
|
||||||
|
|
||||||
const readed =
|
|
||||||
(await db.visit.count({
|
|
||||||
where: {
|
|
||||||
messageId: data.id,
|
|
||||||
type: VisitType.READED,
|
|
||||||
visitorId: staff?.id,
|
|
||||||
},
|
|
||||||
})) > 0;
|
|
||||||
|
|
||||||
|
|
||||||
Object.assign(data, {
|
|
||||||
readed
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
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) {}
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { TrpcService } from '@server/trpc/trpc.service';
|
|
||||||
import { DepartmentService } from '@server/models/department/department.service';
|
|
||||||
|
|
||||||
import { QueueModule } from '@server/queue/queue.module';
|
|
||||||
import { MessageModule } from '../message/message.module';
|
|
||||||
import { PostRouter } from './post.router';
|
|
||||||
import { PostController } from './post.controller';
|
|
||||||
import { PostService } from './post.service';
|
|
||||||
import { RoleMapModule } from '../rbac/rbac.module';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [QueueModule, RoleMapModule, MessageModule],
|
|
||||||
providers: [PostService, PostRouter, TrpcService, DepartmentService],
|
|
||||||
exports: [PostRouter, PostService],
|
|
||||||
controllers: [PostController],
|
|
||||||
})
|
|
||||||
export class PostModule {}
|
|
|
@ -1,138 +0,0 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { TrpcService } from '@server/trpc/trpc.service';
|
|
||||||
import { CourseMethodSchema, Prisma } from '@nice/common';
|
|
||||||
import { PostService } from './post.service';
|
|
||||||
import { z, ZodType } from 'zod';
|
|
||||||
import { UpdateOrderArgs } from '../base/base.type';
|
|
||||||
const PostCreateArgsSchema: ZodType<Prisma.PostCreateArgs> = z.any();
|
|
||||||
const PostUpdateArgsSchema: ZodType<Prisma.PostUpdateArgs> = z.any();
|
|
||||||
const PostUpdateOrderArgsSchema: ZodType<UpdateOrderArgs> = z.any();
|
|
||||||
const PostFindFirstArgsSchema: ZodType<Prisma.PostFindFirstArgs> = z.any();
|
|
||||||
const PostFindManyArgsSchema: ZodType<Prisma.PostFindManyArgs> = z.any();
|
|
||||||
const PostDeleteManyArgsSchema: ZodType<Prisma.PostDeleteManyArgs> = z.any();
|
|
||||||
const PostWhereInputSchema: ZodType<Prisma.PostWhereInput> = z.any();
|
|
||||||
const PostSelectSchema: ZodType<Prisma.PostSelect> = z.any();
|
|
||||||
const PostUpdateInputSchema: ZodType<Prisma.PostUpdateInput> = z.any();
|
|
||||||
@Injectable()
|
|
||||||
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;
|
|
||||||
return await this.postService.create(input, { staff });
|
|
||||||
}),
|
|
||||||
createCourse: this.trpc.protectProcedure
|
|
||||||
.input(CourseMethodSchema.createCourse)
|
|
||||||
.mutation(async ({ ctx, input }) => {
|
|
||||||
const { staff } = ctx;
|
|
||||||
return await this.postService.createCourse(input, { staff });
|
|
||||||
}),
|
|
||||||
softDeleteByIds: this.trpc.protectProcedure
|
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
ids: z.array(z.string()),
|
|
||||||
data: PostUpdateInputSchema.nullish(),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.mutation(async ({ input }) => {
|
|
||||||
return await this.postService.softDeleteByIds(input.ids, input.data);
|
|
||||||
}),
|
|
||||||
restoreByIds: this.trpc.protectProcedure
|
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
ids: z.array(z.string()),
|
|
||||||
args: PostUpdateInputSchema.nullish(),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.mutation(async ({ input }) => {
|
|
||||||
return await this.postService.restoreByIds(input.ids, input.args);
|
|
||||||
}),
|
|
||||||
update: this.trpc.protectProcedure
|
|
||||||
.input(PostUpdateArgsSchema)
|
|
||||||
.mutation(async ({ ctx, input }) => {
|
|
||||||
const { staff } = ctx;
|
|
||||||
return await this.postService.update(input, staff);
|
|
||||||
}),
|
|
||||||
findById: this.trpc.procedure
|
|
||||||
.input(z.object({ id: z.string(), args: PostFindFirstArgsSchema }))
|
|
||||||
.query(async ({ ctx, input }) => {
|
|
||||||
const { staff } = ctx;
|
|
||||||
return await this.postService.findById(input.id, input.args);
|
|
||||||
}),
|
|
||||||
findMany: this.trpc.procedure
|
|
||||||
.input(PostFindManyArgsSchema)
|
|
||||||
.query(async ({ ctx, input }) => {
|
|
||||||
const { staff } = ctx;
|
|
||||||
return await this.postService.findMany(input);
|
|
||||||
}),
|
|
||||||
|
|
||||||
findFirst: this.trpc.procedure
|
|
||||||
.input(PostFindFirstArgsSchema) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword
|
|
||||||
.query(async ({ input, ctx }) => {
|
|
||||||
const { staff, ip } = ctx;
|
|
||||||
// 从请求中获取 IP
|
|
||||||
|
|
||||||
return await this.postService.findFirst(input, staff, ip);
|
|
||||||
}),
|
|
||||||
deleteMany: this.trpc.protectProcedure
|
|
||||||
.input(PostDeleteManyArgsSchema)
|
|
||||||
.mutation(async ({ input }) => {
|
|
||||||
return await this.postService.deleteMany(input);
|
|
||||||
}),
|
|
||||||
findManyWithCursor: this.trpc.procedure
|
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
cursor: z.any().nullish(),
|
|
||||||
take: z.number().nullish(),
|
|
||||||
where: PostWhereInputSchema.nullish(),
|
|
||||||
select: PostSelectSchema.nullish(),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.query(async ({ ctx, input }) => {
|
|
||||||
const { staff } = ctx;
|
|
||||||
return await this.postService.findManyWithCursor(input, staff);
|
|
||||||
}),
|
|
||||||
findManyWithPagination: this.trpc.procedure
|
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
page: z.number().optional(),
|
|
||||||
pageSize: z.number().optional(),
|
|
||||||
where: PostWhereInputSchema.optional(),
|
|
||||||
select: PostSelectSchema.optional(),
|
|
||||||
}),
|
|
||||||
) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword
|
|
||||||
.query(async ({ input }) => {
|
|
||||||
return await this.postService.findManyWithPagination(input);
|
|
||||||
}),
|
|
||||||
updateOrder: this.trpc.protectProcedure
|
|
||||||
.input(PostUpdateOrderArgsSchema)
|
|
||||||
.mutation(async ({ ctx, input }) => {
|
|
||||||
const { staff } = ctx;
|
|
||||||
return await this.postService.updateOrder(input);
|
|
||||||
}),
|
|
||||||
updateOrderByIds: this.trpc.protectProcedure
|
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
ids: z.array(z.string()),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.mutation(async ({ ctx, input }) => {
|
|
||||||
const { staff } = ctx;
|
|
||||||
return await this.postService.updateOrderByIds(input.ids);
|
|
||||||
}),
|
|
||||||
softDeletePostDescendant:this.trpc.protectProcedure
|
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
ancestorId:z.string()
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.mutation(async ({ input })=>{
|
|
||||||
return await this.postService.softDeletePostDescendant(input)
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,328 +0,0 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import {
|
|
||||||
db,
|
|
||||||
Prisma,
|
|
||||||
UserProfile,
|
|
||||||
VisitType,
|
|
||||||
Post,
|
|
||||||
PostType,
|
|
||||||
RolePerms,
|
|
||||||
ResPerm,
|
|
||||||
ObjectType,
|
|
||||||
LectureType,
|
|
||||||
CourseMethodSchema,
|
|
||||||
} from '@nice/common';
|
|
||||||
import { MessageService } from '../message/message.service';
|
|
||||||
import { BaseService } from '../base/base.service';
|
|
||||||
import { DepartmentService } from '../department/department.service';
|
|
||||||
import { setPostInfo, setPostRelation } from './utils';
|
|
||||||
import EventBus, { CrudOperation } from '@server/utils/event-bus';
|
|
||||||
import { BaseTreeService } from '../base/base.tree.service';
|
|
||||||
import { z } from 'zod';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import { OrderByArgs } from '../base/base.type';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class PostService extends BaseTreeService<Prisma.PostDelegate> {
|
|
||||||
constructor(
|
|
||||||
private readonly messageService: MessageService,
|
|
||||||
private readonly departmentService: DepartmentService,
|
|
||||||
) {
|
|
||||||
super(db, ObjectType.POST, 'postAncestry', true);
|
|
||||||
}
|
|
||||||
async createLecture(
|
|
||||||
lecture: z.infer<typeof CourseMethodSchema.createLecture>,
|
|
||||||
params: { staff?: UserProfile; tx?: Prisma.TransactionClient },
|
|
||||||
) {
|
|
||||||
const { sectionId, type, title, content, resourceIds = [] } = lecture;
|
|
||||||
const { staff, tx } = params;
|
|
||||||
return await this.create(
|
|
||||||
{
|
|
||||||
data: {
|
|
||||||
type: PostType.LECTURE,
|
|
||||||
parentId: sectionId,
|
|
||||||
content: content,
|
|
||||||
title: title,
|
|
||||||
authorId: params?.staff?.id,
|
|
||||||
updatedAt: dayjs().toDate(),
|
|
||||||
resources: {
|
|
||||||
connect: resourceIds.map((fileId) => ({ fileId })),
|
|
||||||
},
|
|
||||||
meta: {
|
|
||||||
type: type,
|
|
||||||
},
|
|
||||||
} as any,
|
|
||||||
},
|
|
||||||
{ tx },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
async createSection(
|
|
||||||
section: z.infer<typeof CourseMethodSchema.createSection>,
|
|
||||||
params: {
|
|
||||||
staff?: UserProfile;
|
|
||||||
tx?: Prisma.TransactionClient;
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
const { title, courseId, lectures } = section;
|
|
||||||
const { staff, tx } = params;
|
|
||||||
// Create section first
|
|
||||||
const createdSection = await this.create(
|
|
||||||
{
|
|
||||||
data: {
|
|
||||||
type: PostType.SECTION,
|
|
||||||
parentId: courseId,
|
|
||||||
title: title,
|
|
||||||
authorId: staff?.id,
|
|
||||||
updatedAt: dayjs().toDate(),
|
|
||||||
} as any,
|
|
||||||
},
|
|
||||||
{ tx },
|
|
||||||
);
|
|
||||||
// If lectures are provided, create them
|
|
||||||
if (lectures && lectures.length > 0) {
|
|
||||||
const lecturePromises = lectures.map((lecture) =>
|
|
||||||
this.createLecture(
|
|
||||||
{
|
|
||||||
sectionId: createdSection.id,
|
|
||||||
...lecture,
|
|
||||||
},
|
|
||||||
params,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create all lectures in parallel
|
|
||||||
await Promise.all(lecturePromises);
|
|
||||||
}
|
|
||||||
return createdSection;
|
|
||||||
}
|
|
||||||
async createCourse(
|
|
||||||
args: {
|
|
||||||
courseDetail: Prisma.PostCreateArgs;
|
|
||||||
},
|
|
||||||
params: { staff?: UserProfile; tx?: Prisma.TransactionClient },
|
|
||||||
) {
|
|
||||||
|
|
||||||
const { courseDetail } = args;
|
|
||||||
// If no transaction is provided, create a new one
|
|
||||||
if (!params.tx) {
|
|
||||||
return await db.$transaction(async (tx) => {
|
|
||||||
const courseParams = { ...params, tx };
|
|
||||||
// Create the course first
|
|
||||||
const createdCourse = await this.create(courseDetail, courseParams);
|
|
||||||
// If sections are provided, create them
|
|
||||||
return createdCourse;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// If transaction is provided, use it directly
|
|
||||||
const createdCourse = await this.create(courseDetail, params);
|
|
||||||
// If sections are provided, create them
|
|
||||||
return createdCourse;
|
|
||||||
}
|
|
||||||
async create(
|
|
||||||
args: Prisma.PostCreateArgs,
|
|
||||||
params?: { staff?: UserProfile; tx?: Prisma.TransactionClient },
|
|
||||||
) {
|
|
||||||
args.data.authorId = params?.staff?.id;
|
|
||||||
args.data.updatedAt = dayjs().toDate();
|
|
||||||
|
|
||||||
const result = await super.create(args);
|
|
||||||
EventBus.emit('dataChanged', {
|
|
||||||
type: ObjectType.POST,
|
|
||||||
operation: CrudOperation.CREATED,
|
|
||||||
data: result,
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
async update(args: Prisma.PostUpdateArgs, staff?: UserProfile) {
|
|
||||||
//args.data.authorId = staff?.id;
|
|
||||||
args.data.updatedAt = dayjs().toDate();
|
|
||||||
const result = await super.update(args);
|
|
||||||
EventBus.emit('dataChanged', {
|
|
||||||
type: ObjectType.POST,
|
|
||||||
operation: CrudOperation.UPDATED,
|
|
||||||
data: result,
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
async findFirst(
|
|
||||||
args?: Prisma.PostFindFirstArgs,
|
|
||||||
staff?: UserProfile,
|
|
||||||
clientIp?: string,
|
|
||||||
) {
|
|
||||||
const transDto = await this.wrapResult(
|
|
||||||
super.findFirst(args),
|
|
||||||
async (result) => {
|
|
||||||
if (result) {
|
|
||||||
await setPostRelation({ data: result, staff });
|
|
||||||
await this.setPerms(result, staff);
|
|
||||||
await setPostInfo({ data: result });
|
|
||||||
}
|
|
||||||
// console.log(result);
|
|
||||||
return result;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
return transDto;
|
|
||||||
}
|
|
||||||
|
|
||||||
async findManyWithCursor(args: Prisma.PostFindManyArgs, staff?: UserProfile) {
|
|
||||||
if (!args.where) args.where = {};
|
|
||||||
args.where.OR = await this.preFilter(args.where.OR, staff);
|
|
||||||
return this.wrapResult(super.findManyWithCursor(args), async (result) => {
|
|
||||||
const { items } = result;
|
|
||||||
await Promise.all(
|
|
||||||
items.map(async (item) => {
|
|
||||||
await setPostRelation({ data: item, staff });
|
|
||||||
await this.setPerms(item, staff);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
return { ...result, items };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
async findManyWithPagination(args: {
|
|
||||||
page?: number;
|
|
||||||
pageSize?: number;
|
|
||||||
where?: Prisma.PostWhereInput;
|
|
||||||
orderBy?: OrderByArgs<(typeof db.post)['findMany']>;
|
|
||||||
select?: Prisma.PostSelect;
|
|
||||||
}): Promise<{
|
|
||||||
items: {
|
|
||||||
id: string;
|
|
||||||
type: string | null;
|
|
||||||
level: string | null;
|
|
||||||
state: string | null;
|
|
||||||
title: string | null;
|
|
||||||
subTitle: string | null;
|
|
||||||
content: string | null;
|
|
||||||
important: boolean | null;
|
|
||||||
domainId: string | null;
|
|
||||||
order: number | null;
|
|
||||||
duration: number | null;
|
|
||||||
rating: number | null;
|
|
||||||
createdAt: Date;
|
|
||||||
views: number;
|
|
||||||
hates: number;
|
|
||||||
likes: number;
|
|
||||||
publishedAt: Date | null;
|
|
||||||
updatedAt: Date;
|
|
||||||
deletedAt: Date | null;
|
|
||||||
authorId: string | null;
|
|
||||||
parentId: string | null;
|
|
||||||
hasChildren: boolean | null;
|
|
||||||
meta: Prisma.JsonValue | null;
|
|
||||||
}[];
|
|
||||||
totalPages: number;
|
|
||||||
}> {
|
|
||||||
// super.updateOrder;
|
|
||||||
return super.findManyWithPagination(args);
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateOrderByIds(ids: string[]) {
|
|
||||||
const posts = await db.post.findMany({
|
|
||||||
where: { id: { in: ids } },
|
|
||||||
select: { id: true, order: true },
|
|
||||||
});
|
|
||||||
const postMap = new Map(posts.map((post) => [post.id, post]));
|
|
||||||
const orderedPosts = ids
|
|
||||||
.map((id) => postMap.get(id))
|
|
||||||
.filter((post): post is { id: string; order: number } => !!post);
|
|
||||||
|
|
||||||
// 生成仅需更新的操作
|
|
||||||
const updates = orderedPosts
|
|
||||||
.map((post, index) => ({
|
|
||||||
id: post.id,
|
|
||||||
newOrder: index, // 按数组索引设置新顺序
|
|
||||||
currentOrder: post.order,
|
|
||||||
}))
|
|
||||||
.filter(({ newOrder, currentOrder }) => newOrder !== currentOrder)
|
|
||||||
.map(({ id, newOrder }) =>
|
|
||||||
db.post.update({
|
|
||||||
where: { id },
|
|
||||||
data: { order: newOrder },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// 批量执行更新
|
|
||||||
return updates.length > 0 ? await db.$transaction(updates) : [];
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async setPerms(data: Post, staff?: UserProfile) {
|
|
||||||
if (!staff) return;
|
|
||||||
const perms: ResPerm = {
|
|
||||||
delete: false,
|
|
||||||
};
|
|
||||||
const isMySelf = data?.authorId === staff?.id;
|
|
||||||
const isDomain = staff.domainId === data.domainId;
|
|
||||||
const setManagePermissions = (perms: ResPerm) => {
|
|
||||||
Object.assign(perms, {
|
|
||||||
delete: true,
|
|
||||||
// edit: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
if (isMySelf) {
|
|
||||||
perms.delete = true;
|
|
||||||
// perms.edit = true;
|
|
||||||
}
|
|
||||||
staff.permissions.forEach((permission) => {
|
|
||||||
switch (permission) {
|
|
||||||
case RolePerms.MANAGE_ANY_POST:
|
|
||||||
setManagePermissions(perms);
|
|
||||||
break;
|
|
||||||
case RolePerms.MANAGE_DOM_POST:
|
|
||||||
if (isDomain) {
|
|
||||||
setManagePermissions(perms);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Object.assign(data, { perms });
|
|
||||||
}
|
|
||||||
async preFilter(OR?: Prisma.PostWhereInput[], staff?: UserProfile) {
|
|
||||||
const preFilter = (await this.getPostPreFilter(staff)) || [];
|
|
||||||
const outOR = OR ? [...OR, ...preFilter].filter(Boolean) : preFilter;
|
|
||||||
return outOR?.length > 0 ? outOR : undefined;
|
|
||||||
}
|
|
||||||
async getPostPreFilter(staff?: UserProfile) {
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
].filter(Boolean);
|
|
||||||
|
|
||||||
if (orCondition?.length > 0) return orCondition;
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
async softDeletePostDescendant(args:{ancestorId?:string}){
|
|
||||||
const { ancestorId } = args
|
|
||||||
const descendantIds = []
|
|
||||||
await db.postAncestry.findMany({
|
|
||||||
where:{
|
|
||||||
ancestorId,
|
|
||||||
},
|
|
||||||
select:{
|
|
||||||
descendantId:true
|
|
||||||
}
|
|
||||||
}).then(res=>{
|
|
||||||
res.forEach(item=>{
|
|
||||||
descendantIds.push(item.descendantId)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
console.log(descendantIds)
|
|
||||||
const result = super.softDeleteByIds([...descendantIds,ancestorId])
|
|
||||||
EventBus.emit('dataChanged', {
|
|
||||||
type: ObjectType.POST,
|
|
||||||
operation: CrudOperation.DELETED,
|
|
||||||
data: result,
|
|
||||||
});
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,204 +0,0 @@
|
||||||
import {
|
|
||||||
db,
|
|
||||||
EnrollmentStatus,
|
|
||||||
Lecture,
|
|
||||||
Post,
|
|
||||||
PostType,
|
|
||||||
SectionDto,
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
Object.assign(data, {
|
|
||||||
readed,
|
|
||||||
readedCount,
|
|
||||||
limitedComments,
|
|
||||||
commentsCount,
|
|
||||||
// trouble
|
|
||||||
});
|
|
||||||
}
|
|
||||||
export async function updateParentLectureStats(parentId: string) {
|
|
||||||
const ParentStats = await db.post.aggregate({
|
|
||||||
where: {
|
|
||||||
ancestors: {
|
|
||||||
some: {
|
|
||||||
ancestorId: parentId,
|
|
||||||
descendant: {
|
|
||||||
type: PostType.LECTURE,
|
|
||||||
deletedAt: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
_count: { _all: true },
|
|
||||||
_sum: {
|
|
||||||
duration: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await db.post.update({
|
|
||||||
where: { id: parentId },
|
|
||||||
data: {
|
|
||||||
//totalLectures: courseStats._count._all,
|
|
||||||
//totalDuration: courseStats._sum.duration || 0,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新课程评价统计
|
|
||||||
export async function updateCourseReviewStats(courseId: string) {
|
|
||||||
const reviews = await db.visit.findMany({
|
|
||||||
where: {
|
|
||||||
postId: courseId,
|
|
||||||
type: PostType.COURSE_REVIEW,
|
|
||||||
deletedAt: null,
|
|
||||||
},
|
|
||||||
select: { views: true },
|
|
||||||
});
|
|
||||||
const numberOfReviews = reviews.length;
|
|
||||||
const averageRating =
|
|
||||||
numberOfReviews > 0
|
|
||||||
? reviews.reduce((sum, review) => sum + review.views, 0) / numberOfReviews
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
return db.post.update({
|
|
||||||
where: { id: courseId },
|
|
||||||
data: {
|
|
||||||
// numberOfReviews,
|
|
||||||
//averageRating,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// 更新课程注册统计
|
|
||||||
export async function updateCourseEnrollmentStats(courseId: string) {
|
|
||||||
const completedEnrollments = await db.enrollment.count({
|
|
||||||
where: {
|
|
||||||
postId: courseId,
|
|
||||||
status: EnrollmentStatus.COMPLETED,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const totalEnrollments = await db.enrollment.count({
|
|
||||||
where: { postId: courseId },
|
|
||||||
});
|
|
||||||
const completionRate =
|
|
||||||
totalEnrollments > 0 ? (completedEnrollments / totalEnrollments) * 100 : 0;
|
|
||||||
return db.post.update({
|
|
||||||
where: { id: courseId },
|
|
||||||
data: {
|
|
||||||
// numberOfStudents: totalEnrollments,
|
|
||||||
// completionRate,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function setPostInfo({ data }: { data: Post }) {
|
|
||||||
// await db.term
|
|
||||||
if (data?.type === PostType.COURSE) {
|
|
||||||
const ancestries = await db.postAncestry.findMany({
|
|
||||||
where: {
|
|
||||||
ancestorId: data.id,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
descendant: true,
|
|
||||||
},
|
|
||||||
orderBy: {
|
|
||||||
descendant: {
|
|
||||||
order: 'asc',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const descendants = ancestries.map((ancestry) => ancestry.descendant);
|
|
||||||
const sections: SectionDto[] = (
|
|
||||||
descendants.filter((descendant) => {
|
|
||||||
return (
|
|
||||||
descendant.type === PostType.SECTION &&
|
|
||||||
descendant.parentId === data.id
|
|
||||||
);
|
|
||||||
}) as any
|
|
||||||
).map((section) => ({
|
|
||||||
...section,
|
|
||||||
lectures: [],
|
|
||||||
}));
|
|
||||||
const lectures = descendants.filter((descendant) => {
|
|
||||||
return (
|
|
||||||
descendant.type === PostType.LECTURE &&
|
|
||||||
sections.map((section) => section.id).includes(descendant.parentId)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const lectureCount = lectures?.length || 0;
|
|
||||||
sections.forEach((section) => {
|
|
||||||
section.lectures = lectures.filter(
|
|
||||||
(lecture) => lecture.parentId === section.id,
|
|
||||||
) as any as Lecture[];
|
|
||||||
});
|
|
||||||
|
|
||||||
Object.assign(data, { sections, lectureCount });
|
|
||||||
}
|
|
||||||
if (data?.type === PostType.LECTURE || data?.type === PostType.SECTION) {
|
|
||||||
const ancestry = await db.postAncestry.findFirst({
|
|
||||||
where: {
|
|
||||||
descendantId: data?.id,
|
|
||||||
ancestor: {
|
|
||||||
type: PostType.COURSE,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
ancestor: { select: { id: true } },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const courseId = ancestry.ancestor.id;
|
|
||||||
Object.assign(data, { courseId });
|
|
||||||
}
|
|
||||||
const students = await db.staff.findMany({
|
|
||||||
where: {
|
|
||||||
learningPosts: {
|
|
||||||
some: {
|
|
||||||
id: data.id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const studentIds = (students || []).map((student) => student?.id);
|
|
||||||
Object.assign(data, { studentIds });
|
|
||||||
}
|
|
|
@ -35,53 +35,4 @@ export class ResourceService extends BaseService<Prisma.ResourceDelegate> {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// 添加保存文件名的方法
|
|
||||||
async saveFileName(fileId: string, fileName: string): Promise<void> {
|
|
||||||
try {
|
|
||||||
this.logger.log(`尝试保存文件名 "${fileName}" 到文件 ${fileId}`);
|
|
||||||
|
|
||||||
// 首先检查是否已存在 ShareCode 记录
|
|
||||||
const existingShareCode = await db.shareCode.findUnique({
|
|
||||||
where: { fileId },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existingShareCode) {
|
|
||||||
// 如果记录存在,更新文件名
|
|
||||||
await db.shareCode.update({
|
|
||||||
where: { fileId },
|
|
||||||
data: { fileName },
|
|
||||||
});
|
|
||||||
this.logger.log(`更新了现有记录的文件名 "${fileName}" 到文件 ${fileId}`);
|
|
||||||
} else {
|
|
||||||
// 如果记录不存在,创建新记录
|
|
||||||
await db.shareCode.create({
|
|
||||||
data: {
|
|
||||||
fileId,
|
|
||||||
fileName,
|
|
||||||
code: null, // 这里可以设置为 null 或生成一个临时码
|
|
||||||
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24小时后过期
|
|
||||||
isUsed: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.logger.log(`创建了新记录并保存文件名 "${fileName}" 到文件 ${fileId}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`保存文件名失败,文件ID: ${fileId}`, error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加获取文件名的方法
|
|
||||||
async getFileName(fileId: string): Promise<string | null> {
|
|
||||||
try {
|
|
||||||
const shareCode = await db.shareCode.findUnique({
|
|
||||||
where: { fileId },
|
|
||||||
select: { fileName: true },
|
|
||||||
});
|
|
||||||
return shareCode?.fileName || null;
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`Failed to get filename for ${fileId}`, error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,132 +5,132 @@ import {
|
||||||
PostType,
|
PostType,
|
||||||
VisitType,
|
VisitType,
|
||||||
} from '@nice/common';
|
} from '@nice/common';
|
||||||
export async function updateTotalCourseViewCount(type: VisitType) {
|
// export async function updateTotalCourseViewCount(type: VisitType) {
|
||||||
const posts = await db.post.findMany({
|
// const posts = await db.post.findMany({
|
||||||
where: {
|
// where: {
|
||||||
// type: { in: [PostType.COURSE, PostType.LECTURE,] },
|
// // type: { in: [PostType.COURSE, PostType.LECTURE,] },
|
||||||
deletedAt: null,
|
// deletedAt: null,
|
||||||
},
|
// },
|
||||||
select: { id: true, type: true },
|
// select: { id: true, type: true },
|
||||||
});
|
// });
|
||||||
|
|
||||||
const courseIds = posts
|
// const courseIds = posts
|
||||||
.filter((post) => post.type === PostType.COURSE)
|
// .filter((post) => post.type === PostType.COURSE)
|
||||||
.map((course) => course.id);
|
// .map((course) => course.id);
|
||||||
const lectures = posts.filter((post) => post.type === PostType.LECTURE);
|
// const lectures = posts.filter((post) => post.type === PostType.LECTURE);
|
||||||
const totalViews = await db.visit.aggregate({
|
// const totalViews = await db.visit.aggregate({
|
||||||
_sum: {
|
// _sum: {
|
||||||
views: true,
|
// views: true,
|
||||||
},
|
// },
|
||||||
where: {
|
// where: {
|
||||||
postId: { in: posts.map((post) => post.id) },
|
// postId: { in: posts.map((post) => post.id) },
|
||||||
type: type,
|
// type: type,
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
const appConfig = await db.appConfig.findFirst({
|
// const appConfig = await db.appConfig.findFirst({
|
||||||
where: {
|
// where: {
|
||||||
slug: AppConfigSlug.BASE_SETTING,
|
// slug: AppConfigSlug.BASE_SETTING,
|
||||||
},
|
// },
|
||||||
select: {
|
// select: {
|
||||||
id: true,
|
// id: true,
|
||||||
meta: true,
|
// meta: true,
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
const staffs = await db.staff.count({
|
// const staffs = await db.staff.count({
|
||||||
where: { deletedAt: null },
|
// where: { deletedAt: null },
|
||||||
});
|
// });
|
||||||
|
|
||||||
const baseSeting = appConfig.meta as BaseSetting;
|
// const baseSeting = appConfig.meta as BaseSetting;
|
||||||
await db.appConfig.update({
|
// await db.appConfig.update({
|
||||||
where: {
|
// where: {
|
||||||
slug: AppConfigSlug.BASE_SETTING,
|
// slug: AppConfigSlug.BASE_SETTING,
|
||||||
},
|
// },
|
||||||
data: {
|
// data: {
|
||||||
meta: {
|
// meta: {
|
||||||
...baseSeting,
|
// ...baseSeting,
|
||||||
appConfig: {
|
// appConfig: {
|
||||||
...(baseSeting?.appConfig || {}),
|
// ...(baseSeting?.appConfig || {}),
|
||||||
statistics: {
|
// statistics: {
|
||||||
reads: totalViews._sum.views || 0,
|
// reads: totalViews._sum.views || 0,
|
||||||
courses: courseIds?.length || 0,
|
// courses: courseIds?.length || 0,
|
||||||
staffs: staffs || 0,
|
// staffs: staffs || 0,
|
||||||
lectures: lectures?.length || 0,
|
// lectures: lectures?.length || 0,
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
export async function updatePostViewCount(id: string, type: VisitType) {
|
// export async function updatePostViewCount(id: string, type: VisitType) {
|
||||||
const post = await db.post.findFirst({
|
// const post = await db.post.findFirst({
|
||||||
where: { id },
|
// where: { id },
|
||||||
select: { id: true, meta: true, type: true },
|
// select: { id: true, meta: true, type: true },
|
||||||
});
|
// });
|
||||||
const metaFieldMap = {
|
// const metaFieldMap = {
|
||||||
[VisitType.READED]: 'views',
|
// [VisitType.READED]: 'views',
|
||||||
[VisitType.LIKE]: 'likes',
|
// [VisitType.LIKE]: 'likes',
|
||||||
[VisitType.HATE]: 'hates',
|
// [VisitType.HATE]: 'hates',
|
||||||
};
|
// };
|
||||||
if (post?.type === PostType.LECTURE) {
|
// if (post?.type === PostType.LECTURE) {
|
||||||
const courseAncestry = await db.postAncestry.findFirst({
|
// const courseAncestry = await db.postAncestry.findFirst({
|
||||||
where: {
|
// where: {
|
||||||
descendantId: post?.id,
|
// descendantId: post?.id,
|
||||||
ancestor: {
|
// ancestor: {
|
||||||
type: PostType.COURSE,
|
// type: PostType.COURSE,
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
select: { id: true, ancestorId: true },
|
// select: { id: true, ancestorId: true },
|
||||||
});
|
// });
|
||||||
const course = { id: courseAncestry.ancestorId };
|
// const course = { id: courseAncestry.ancestorId };
|
||||||
const lecturesAncestry = await db.postAncestry.findMany({
|
// const lecturesAncestry = await db.postAncestry.findMany({
|
||||||
where: { ancestorId: course.id, descendant: { type: PostType.LECTURE } },
|
// where: { ancestorId: course.id, descendant: { type: PostType.LECTURE } },
|
||||||
select: {
|
// select: {
|
||||||
id: true,
|
// id: true,
|
||||||
descendantId: true,
|
// descendantId: true,
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
const lectures = lecturesAncestry.map((ancestry) => ({
|
// const lectures = lecturesAncestry.map((ancestry) => ({
|
||||||
id: ancestry.descendantId,
|
// id: ancestry.descendantId,
|
||||||
}));
|
// }));
|
||||||
const courseViews = await db.visit.aggregate({
|
// const courseViews = await db.visit.aggregate({
|
||||||
_sum: {
|
// _sum: {
|
||||||
views: true,
|
// views: true,
|
||||||
},
|
// },
|
||||||
where: {
|
// where: {
|
||||||
postId: {
|
// postId: {
|
||||||
in: [course.id, ...lectures.map((lecture) => lecture.id)],
|
// in: [course.id, ...lectures.map((lecture) => lecture.id)],
|
||||||
},
|
// },
|
||||||
type: type,
|
// type: type,
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
await db.post.update({
|
// await db.post.update({
|
||||||
where: { id: course.id },
|
// where: { id: course.id },
|
||||||
data: {
|
// data: {
|
||||||
[metaFieldMap[type]]: courseViews._sum.views || 0,
|
// [metaFieldMap[type]]: courseViews._sum.views || 0,
|
||||||
// meta: {
|
// // meta: {
|
||||||
// ...((post?.meta as any) || {}),
|
// // ...((post?.meta as any) || {}),
|
||||||
// [metaFieldMap[type]]: courseViews._sum.views || 0,
|
// // [metaFieldMap[type]]: courseViews._sum.views || 0,
|
||||||
// },
|
// // },
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
const totalViews = await db.visit.aggregate({
|
// const totalViews = await db.visit.aggregate({
|
||||||
_sum: {
|
// _sum: {
|
||||||
views: true,
|
// views: true,
|
||||||
},
|
// },
|
||||||
where: {
|
// where: {
|
||||||
postId: id,
|
// postId: id,
|
||||||
type: type,
|
// type: type,
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
await db.post.update({
|
// await db.post.update({
|
||||||
where: { id },
|
// where: { id },
|
||||||
data: {
|
// data: {
|
||||||
[metaFieldMap[type]]: totalViews._sum.views || 0,
|
// [metaFieldMap[type]]: totalViews._sum.views || 0,
|
||||||
// meta: {
|
// // meta: {
|
||||||
// ...((post?.meta as any) || {}),
|
// // ...((post?.meta as any) || {}),
|
||||||
// [metaFieldMap[type]]: totalViews._sum.views || 0,
|
// // [metaFieldMap[type]]: totalViews._sum.views || 0,
|
||||||
// },
|
// // },
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
|
|
|
@ -6,61 +6,61 @@ import { ObjectType } from '@nice/common';
|
||||||
// updateCourseReviewStats,
|
// updateCourseReviewStats,
|
||||||
// } from '@server/models/course/utils';
|
// } from '@server/models/course/utils';
|
||||||
import { QueueJobType } from '../types';
|
import { QueueJobType } from '../types';
|
||||||
import {
|
// import {
|
||||||
updateCourseEnrollmentStats,
|
// updateCourseEnrollmentStats,
|
||||||
updateCourseReviewStats,
|
// updateCourseReviewStats,
|
||||||
updateParentLectureStats,
|
// updateParentLectureStats,
|
||||||
} from '@server/models/post/utils';
|
// } from '@server/models/post/utils';
|
||||||
import {
|
// import {
|
||||||
updatePostViewCount,
|
// updatePostViewCount,
|
||||||
updateTotalCourseViewCount,
|
// updateTotalCourseViewCount,
|
||||||
} from '../models/post/utils';
|
// } from '../models/post/utils';
|
||||||
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>) {
|
||||||
try {
|
// try {
|
||||||
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 updateParentLectureStats(sectionId);
|
// await updateParentLectureStats(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 updateParentLectureStats(courseId);
|
// await updateParentLectureStats(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(
|
// logger.debug(
|
||||||
`Updated course stats for courseId: ${courseId}, type: ${type}`,
|
// `Updated course stats for courseId: ${courseId}, type: ${type}`,
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
if (job.name === QueueJobType.UPDATE_POST_VISIT_COUNT) {
|
// if (job.name === QueueJobType.UPDATE_POST_VISIT_COUNT) {
|
||||||
await updatePostViewCount(job.data.id, job.data.type);
|
// await updatePostViewCount(job.data.id, job.data.type);
|
||||||
}
|
// }
|
||||||
if (job.name === QueueJobType.UPDATE_POST_STATE) {
|
// if (job.name === QueueJobType.UPDATE_POST_STATE) {
|
||||||
await updatePostViewCount(job.data.id, job.data.type);
|
// await updatePostViewCount(job.data.id, job.data.type);
|
||||||
}
|
// }
|
||||||
if (job.name === QueueJobType.UPDATE_TOTAL_COURSE_VIEW_COUNT) {
|
// if (job.name === QueueJobType.UPDATE_TOTAL_COURSE_VIEW_COUNT) {
|
||||||
await updateTotalCourseViewCount(job.data.type);
|
// await updateTotalCourseViewCount(job.data.type);
|
||||||
}
|
// }
|
||||||
} catch (error: any) {
|
// } catch (error: any) {
|
||||||
logger.error(
|
// logger.error(
|
||||||
`Error processing stats update job: ${error.message}`,
|
// `Error processing stats update job: ${error.message}`,
|
||||||
error.stack,
|
// error.stack,
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
|
@ -2,20 +2,20 @@ import { Injectable, OnModuleInit } from "@nestjs/common";
|
||||||
import { WebSocketType } from "../types";
|
import { WebSocketType } from "../types";
|
||||||
import { BaseWebSocketServer } from "../base/base-websocket-server";
|
import { BaseWebSocketServer } from "../base/base-websocket-server";
|
||||||
import EventBus, { CrudOperation } from "@server/utils/event-bus";
|
import EventBus, { CrudOperation } from "@server/utils/event-bus";
|
||||||
import { ObjectType, SocketMsgType, MessageDto, PostDto, PostType } from "@nice/common";
|
import { ObjectType, SocketMsgType } from "@nice/common";
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RealtimeServer extends BaseWebSocketServer implements OnModuleInit {
|
export class RealtimeServer extends BaseWebSocketServer implements OnModuleInit {
|
||||||
onModuleInit() {
|
onModuleInit() {
|
||||||
EventBus.on("dataChanged", ({ data, type, operation }) => {
|
EventBus.on("dataChanged", ({ data, type, operation }) => {
|
||||||
if (type === ObjectType.MESSAGE && operation === CrudOperation.CREATED) {
|
// if (type === ObjectType.MESSAGE && operation === CrudOperation.CREATED) {
|
||||||
const receiverIds = (data as Partial<MessageDto>).receivers.map(receiver => receiver.id)
|
// const receiverIds = (data as Partial<MessageDto>).receivers.map(receiver => receiver.id)
|
||||||
this.sendToUsers(receiverIds, { type: SocketMsgType.NOTIFY, payload: { objectType: ObjectType.MESSAGE } })
|
// this.sendToUsers(receiverIds, { type: SocketMsgType.NOTIFY, payload: { objectType: ObjectType.MESSAGE } })
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (type === ObjectType.POST) {
|
// if (type === ObjectType.POST) {
|
||||||
const post = data as Partial<PostDto>
|
// const post = data as Partial<PostDto>
|
||||||
|
|
||||||
}
|
// }
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,7 +46,6 @@ export class GenDevService {
|
||||||
await this.generateDepartments(3, 6);
|
await this.generateDepartments(3, 6);
|
||||||
await this.generateTerms(2, 6);
|
await this.generateTerms(2, 6);
|
||||||
await this.generateStaffs(4);
|
await this.generateStaffs(4);
|
||||||
await this.generateCourses(8);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.error(err);
|
this.logger.error(err);
|
||||||
}
|
}
|
||||||
|
@ -144,65 +143,7 @@ export class GenDevService {
|
||||||
collectChildren(domainId);
|
collectChildren(domainId);
|
||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
private async generateCourses(countPerCate: number = 3) {
|
|
||||||
const titleList = [
|
|
||||||
'计算机科学导论',
|
|
||||||
'数据结构与算法',
|
|
||||||
'网络安全',
|
|
||||||
'机器学习',
|
|
||||||
'数据库管理系统',
|
|
||||||
'Web开发',
|
|
||||||
'移动应用开发',
|
|
||||||
'人工智能',
|
|
||||||
'计算机网络',
|
|
||||||
'操作系统',
|
|
||||||
'数字信号处理',
|
|
||||||
'无线通信',
|
|
||||||
'信息论',
|
|
||||||
'密码学',
|
|
||||||
'计算机图形学',
|
|
||||||
];
|
|
||||||
|
|
||||||
if (!this.counts.courseCount) {
|
|
||||||
this.logger.log('Generating courses...');
|
|
||||||
const depts = await db.department.findMany({
|
|
||||||
select: { id: true, name: true },
|
|
||||||
});
|
|
||||||
const cates = await db.term.findMany({
|
|
||||||
where: {
|
|
||||||
taxonomy: { slug: TaxonomySlug.CATEGORY },
|
|
||||||
},
|
|
||||||
select: { id: true, name: true },
|
|
||||||
});
|
|
||||||
const total = cates.length * countPerCate;
|
|
||||||
const levels = await db.term.findMany({
|
|
||||||
where: {
|
|
||||||
taxonomy: { slug: TaxonomySlug.LEVEL },
|
|
||||||
},
|
|
||||||
select: { id: true, name: true },
|
|
||||||
});
|
|
||||||
for (const cate of cates) {
|
|
||||||
for (let i = 0; i < countPerCate; i++) {
|
|
||||||
const randomTitle = `${titleList[Math.floor(Math.random() * titleList.length)]} ${Math.random().toString(36).substring(7)}`;
|
|
||||||
const randomLevelId =
|
|
||||||
levels[Math.floor(Math.random() * levels.length)].id;
|
|
||||||
const randomDeptId =
|
|
||||||
depts[Math.floor(Math.random() * depts.length)].id;
|
|
||||||
|
|
||||||
await this.createCourse(
|
|
||||||
randomTitle,
|
|
||||||
randomDeptId,
|
|
||||||
cate.id,
|
|
||||||
randomLevelId,
|
|
||||||
);
|
|
||||||
this.courseGeneratedCount++;
|
|
||||||
this.logger.log(
|
|
||||||
`Generated ${this.courseGeneratedCount}/${total} course`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private async generateStaffs(countPerDept: number = 3) {
|
private async generateStaffs(countPerDept: number = 3) {
|
||||||
if (this.counts.staffCount === 1) {
|
if (this.counts.staffCount === 1) {
|
||||||
this.logger.log('Generating staffs...');
|
this.logger.log('Generating staffs...');
|
||||||
|
@ -266,31 +207,6 @@ export class GenDevService {
|
||||||
throw error; // 向上抛出错误供上层处理
|
throw error; // 向上抛出错误供上层处理
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private async createCourse(
|
|
||||||
title: string,
|
|
||||||
deptId: string,
|
|
||||||
cateId: string,
|
|
||||||
levelId: string,
|
|
||||||
) {
|
|
||||||
const course = await db.post.create({
|
|
||||||
data: {
|
|
||||||
type: PostType.COURSE,
|
|
||||||
title: title,
|
|
||||||
updatedAt: dayjs().toDate(),
|
|
||||||
depts: {
|
|
||||||
connect: {
|
|
||||||
id: deptId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
terms: {
|
|
||||||
connect: [cateId, levelId].map((id) => ({
|
|
||||||
id: id,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return course;
|
|
||||||
}
|
|
||||||
private async createDepartment(
|
private async createDepartment(
|
||||||
name: string,
|
name: string,
|
||||||
parentId?: string | null,
|
parentId?: string | null,
|
||||||
|
|
|
@ -8,10 +8,8 @@ import {
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
export interface DevDataCounts {
|
export interface DevDataCounts {
|
||||||
deptCount: number;
|
deptCount: number;
|
||||||
|
|
||||||
staffCount: number;
|
staffCount: number;
|
||||||
termCount: number;
|
termCount: number;
|
||||||
courseCount: number;
|
|
||||||
}
|
}
|
||||||
export async function getCounts(): Promise<DevDataCounts> {
|
export async function getCounts(): Promise<DevDataCounts> {
|
||||||
const counts = {
|
const counts = {
|
||||||
|
@ -19,11 +17,6 @@ export async function getCounts(): Promise<DevDataCounts> {
|
||||||
|
|
||||||
staffCount: await db.staff.count(),
|
staffCount: await db.staff.count(),
|
||||||
termCount: await db.term.count(),
|
termCount: await db.term.count(),
|
||||||
courseCount: await db.post.count({
|
|
||||||
where: {
|
|
||||||
type: PostType.COURSE,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
return counts;
|
return counts;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { ReminderService } from './reminder.service';
|
import { ReminderService } from './reminder.service';
|
||||||
import { MessageModule } from '@server/models/message/message.module';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [ MessageModule],
|
imports: [],
|
||||||
providers: [ReminderService],
|
providers: [ReminderService],
|
||||||
exports: [ReminderService]
|
exports: [ReminderService]
|
||||||
})
|
})
|
||||||
|
|
|
@ -8,7 +8,6 @@
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
import { MessageService } from '@server/models/message/message.service';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 提醒服务类
|
* 提醒服务类
|
||||||
|
@ -25,7 +24,7 @@ export class ReminderService {
|
||||||
* 构造函数
|
* 构造函数
|
||||||
* @param messageService 消息服务实例
|
* @param messageService 消息服务实例
|
||||||
*/
|
*/
|
||||||
constructor(private readonly messageService: MessageService) { }
|
constructor() { }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成提醒时间点
|
* 生成提醒时间点
|
||||||
|
|
|
@ -8,8 +8,6 @@ import { TermModule } from '@server/models/term/term.module';
|
||||||
import { TaxonomyModule } from '@server/models/taxonomy/taxonomy.module';
|
import { TaxonomyModule } from '@server/models/taxonomy/taxonomy.module';
|
||||||
import { AuthModule } from '@server/auth/auth.module';
|
import { AuthModule } from '@server/auth/auth.module';
|
||||||
import { AppConfigModule } from '@server/models/app-config/app-config.module';
|
import { AppConfigModule } from '@server/models/app-config/app-config.module';
|
||||||
import { MessageModule } from '@server/models/message/message.module';
|
|
||||||
import { PostModule } from '@server/models/post/post.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';
|
||||||
|
@ -25,9 +23,7 @@ import { ResourceModule } from '@server/models/resource/resource.module';
|
||||||
TaxonomyModule,
|
TaxonomyModule,
|
||||||
RoleMapModule,
|
RoleMapModule,
|
||||||
TransformModule,
|
TransformModule,
|
||||||
MessageModule,
|
|
||||||
AppConfigModule,
|
AppConfigModule,
|
||||||
PostModule,
|
|
||||||
WebSocketModule,
|
WebSocketModule,
|
||||||
ResourceModule,
|
ResourceModule,
|
||||||
],
|
],
|
||||||
|
|
|
@ -7,8 +7,6 @@ import { TrpcService } from '@server/trpc/trpc.service';
|
||||||
import * as trpcExpress from '@trpc/server/adapters/express';
|
import * as trpcExpress from '@trpc/server/adapters/express';
|
||||||
import ws, { WebSocketServer } from 'ws';
|
import ws, { WebSocketServer } from 'ws';
|
||||||
import { AppConfigRouter } from '@server/models/app-config/app-config.router';
|
import { AppConfigRouter } from '@server/models/app-config/app-config.router';
|
||||||
import { MessageRouter } from '@server/models/message/message.router';
|
|
||||||
import { PostRouter } from '@server/models/post/post.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';
|
||||||
|
@ -18,7 +16,6 @@ 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 department: DepartmentRouter,
|
private readonly department: DepartmentRouter,
|
||||||
private readonly staff: StaffRouter,
|
private readonly staff: StaffRouter,
|
||||||
private readonly term: TermRouter,
|
private readonly term: TermRouter,
|
||||||
|
@ -27,7 +24,6 @@ export class TrpcRouter {
|
||||||
private readonly rolemap: RoleMapRouter,
|
private readonly rolemap: RoleMapRouter,
|
||||||
private readonly transform: TransformRouter,
|
private readonly transform: TransformRouter,
|
||||||
private readonly app_config: AppConfigRouter,
|
private readonly app_config: AppConfigRouter,
|
||||||
private readonly message: MessageRouter,
|
|
||||||
private readonly resource: ResourceRouter,
|
private readonly resource: ResourceRouter,
|
||||||
) {}
|
) {}
|
||||||
getRouter() {
|
getRouter() {
|
||||||
|
@ -35,14 +31,12 @@ export class TrpcRouter {
|
||||||
}
|
}
|
||||||
appRouter = this.trpc.router({
|
appRouter = this.trpc.router({
|
||||||
transform: this.transform.router,
|
transform: this.transform.router,
|
||||||
post: this.post.router,
|
|
||||||
department: this.department.router,
|
department: this.department.router,
|
||||||
staff: this.staff.router,
|
staff: this.staff.router,
|
||||||
term: this.term.router,
|
term: this.term.router,
|
||||||
taxonomy: this.taxonomy.router,
|
taxonomy: this.taxonomy.router,
|
||||||
role: this.role.router,
|
role: this.role.router,
|
||||||
rolemap: this.rolemap.router,
|
rolemap: this.rolemap.router,
|
||||||
message: this.message.router,
|
|
||||||
app_config: this.app_config.router,
|
app_config: this.app_config.router,
|
||||||
resource: this.resource.router,
|
resource: this.resource.router,
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,173 +0,0 @@
|
||||||
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
|
|
||||||
import { customAlphabet } from 'nanoid-cjs';
|
|
||||||
import { db } from '@nice/common';
|
|
||||||
import { ShareCode, GenerateShareCodeResponse } from './types';
|
|
||||||
import { Cron, CronExpression } from '@nestjs/schedule';
|
|
||||||
import { ResourceService } from '@server/models/resource/resource.service';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class ShareCodeService {
|
|
||||||
private readonly logger = new Logger(ShareCodeService.name);
|
|
||||||
// 生成8位分享码,使用易读的字符
|
|
||||||
private readonly generateCode = customAlphabet(
|
|
||||||
'23456789ABCDEFGHJKLMNPQRSTUVWXYZ',
|
|
||||||
8,
|
|
||||||
);
|
|
||||||
|
|
||||||
constructor(private readonly resourceService: ResourceService) {}
|
|
||||||
|
|
||||||
async generateShareCode(
|
|
||||||
fileId: string,
|
|
||||||
fileName?: string,
|
|
||||||
): Promise<GenerateShareCodeResponse> {
|
|
||||||
try {
|
|
||||||
// 检查文件是否存在
|
|
||||||
const resource = await this.resourceService.findUnique({
|
|
||||||
where: { fileId },
|
|
||||||
});
|
|
||||||
console.log('完整 fileId:', fileId); // 确保与前端一致
|
|
||||||
|
|
||||||
if (!resource) {
|
|
||||||
throw new NotFoundException('文件不存在');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成分享码
|
|
||||||
const code = this.generateCode();
|
|
||||||
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24小时后过期
|
|
||||||
|
|
||||||
// 查找是否已有分享码记录
|
|
||||||
const existingShareCode = await db.shareCode.findUnique({
|
|
||||||
where: { fileId },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (existingShareCode) {
|
|
||||||
// 更新现有记录,但保留原有文件名
|
|
||||||
await db.shareCode.update({
|
|
||||||
where: { fileId },
|
|
||||||
data: {
|
|
||||||
code,
|
|
||||||
expiresAt,
|
|
||||||
isUsed: false,
|
|
||||||
// 只在没有现有文件名且提供了新文件名时才更新文件名
|
|
||||||
...(fileName && !existingShareCode.fileName ? { fileName } : {}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// 创建新记录
|
|
||||||
await db.shareCode.create({
|
|
||||||
data: {
|
|
||||||
code,
|
|
||||||
fileId,
|
|
||||||
expiresAt,
|
|
||||||
isUsed: false,
|
|
||||||
fileName: fileName || null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.logger.log(`Generated share code ${code} for file ${fileId}`);
|
|
||||||
return {
|
|
||||||
code,
|
|
||||||
expiresAt,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error('Failed to generate share code', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async validateAndUseCode(code: string): Promise<ShareCode | null> {
|
|
||||||
try {
|
|
||||||
console.log(`尝试验证分享码: ${code}`);
|
|
||||||
|
|
||||||
// 查找有效的分享码
|
|
||||||
const shareCode = await db.shareCode.findFirst({
|
|
||||||
where: {
|
|
||||||
code,
|
|
||||||
isUsed: false,
|
|
||||||
expiresAt: { gt: new Date() },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('查询结果:', shareCode);
|
|
||||||
|
|
||||||
if (!shareCode) {
|
|
||||||
console.log('分享码无效或已过期');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 标记分享码为已使用
|
|
||||||
// await db.shareCode.update({
|
|
||||||
// where: { id: shareCode.id },
|
|
||||||
// data: { isUsed: true },
|
|
||||||
// });
|
|
||||||
|
|
||||||
// 记录使用日志
|
|
||||||
this.logger.log(`Share code ${code} used for file ${shareCode.fileId}`);
|
|
||||||
|
|
||||||
// 返回完整的分享码信息,包括文件名
|
|
||||||
return shareCode;
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error('Failed to validate share code', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 每天清理过期的分享码
|
|
||||||
@Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
|
|
||||||
async cleanupExpiredShareCodes() {
|
|
||||||
try {
|
|
||||||
const result = await db.shareCode.deleteMany({
|
|
||||||
where: {
|
|
||||||
OR: [{ expiresAt: { lt: new Date() } }, { isUsed: true }],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
this.logger.log(`Cleaned up ${result.count} expired share codes`);
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error('Failed to cleanup expired share codes', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取分享码信息
|
|
||||||
async getShareCodeInfo(code: string): Promise<ShareCode | null> {
|
|
||||||
try {
|
|
||||||
return await db.shareCode.findFirst({
|
|
||||||
where: { code },
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error('Failed to get share code info', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查文件是否已经生成过分享码
|
|
||||||
async hasActiveShareCode(fileId: string): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
const activeCode = await db.shareCode.findFirst({
|
|
||||||
where: {
|
|
||||||
fileId,
|
|
||||||
isUsed: false,
|
|
||||||
expiresAt: { gt: new Date() },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return !!activeCode;
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error('Failed to check active share code', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取文件的所有分享记录
|
|
||||||
async getFileShareHistory(fileId: string) {
|
|
||||||
try {
|
|
||||||
return await db.shareCode.findMany({
|
|
||||||
where: { fileId },
|
|
||||||
orderBy: { createdAt: 'desc' },
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error('Failed to get file share history', error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -17,7 +17,6 @@ import {
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { TusService } from './tus.service';
|
import { TusService } from './tus.service';
|
||||||
import { ShareCodeService } from './share-code.service';
|
|
||||||
import { ResourceService } from '@server/models/resource/resource.service';
|
import { ResourceService } from '@server/models/resource/resource.service';
|
||||||
|
|
||||||
interface ResourceMeta {
|
interface ResourceMeta {
|
||||||
|
@ -30,7 +29,6 @@ interface ResourceMeta {
|
||||||
export class UploadController {
|
export class UploadController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly tusService: TusService,
|
private readonly tusService: TusService,
|
||||||
private readonly shareCodeService: ShareCodeService,
|
|
||||||
private readonly resourceService: ResourceService,
|
private readonly resourceService: ResourceService,
|
||||||
) {}
|
) {}
|
||||||
// @Post()
|
// @Post()
|
||||||
|
@ -52,52 +50,7 @@ export class UploadController {
|
||||||
async handlePost(@Req() req: Request, @Res() res: Response) {
|
async handlePost(@Req() req: Request, @Res() res: Response) {
|
||||||
return this.tusService.handleTus(req, res);
|
return this.tusService.handleTus(req, res);
|
||||||
}
|
}
|
||||||
@Get('share/:code')
|
|
||||||
async validateShareCode(@Param('code') code: string) {
|
|
||||||
console.log('收到验证分享码请求,code:', code);
|
|
||||||
|
|
||||||
const shareCode = await this.shareCodeService.validateAndUseCode(code);
|
|
||||||
console.log('验证分享码结果:', shareCode);
|
|
||||||
|
|
||||||
if (!shareCode) {
|
|
||||||
throw new NotFoundException('分享码无效或已过期');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取文件信息
|
|
||||||
const resource = await this.resourceService.findUnique({
|
|
||||||
where: { fileId: shareCode.fileId },
|
|
||||||
});
|
|
||||||
console.log('获取到的资源信息:', resource);
|
|
||||||
const {filename} = resource.meta as any as ResourceMeta
|
|
||||||
if (!resource) {
|
|
||||||
throw new NotFoundException('文件不存在');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 直接返回正确的数据结构
|
|
||||||
const response = {
|
|
||||||
fileId: shareCode.fileId,
|
|
||||||
fileName:filename || 'downloaded_file',
|
|
||||||
code: shareCode.code,
|
|
||||||
expiresAt: shareCode.expiresAt
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('返回给前端的数据:', response); // 添加日志
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('share/info/:code')
|
|
||||||
async getShareCodeInfo(@Param('code') code: string) {
|
|
||||||
const info = await this.shareCodeService.getShareCodeInfo(code);
|
|
||||||
if (!info) {
|
|
||||||
throw new NotFoundException('分享码不存在');
|
|
||||||
}
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('share/history/:fileId')
|
|
||||||
async getFileShareHistory(@Param('fileId') fileId: string) {
|
|
||||||
return this.shareCodeService.getFileShareHistory(fileId);
|
|
||||||
}
|
|
||||||
@Get('/*')
|
@Get('/*')
|
||||||
async handleGet(@Req() req: Request, @Res() res: Response) {
|
async handleGet(@Req() req: Request, @Res() res: Response) {
|
||||||
return this.tusService.handleTus(req, res);
|
return this.tusService.handleTus(req, res);
|
||||||
|
@ -113,80 +66,5 @@ export class UploadController {
|
||||||
async handleUpload(@Req() req: Request, @Res() res: Response) {
|
async handleUpload(@Req() req: Request, @Res() res: Response) {
|
||||||
return this.tusService.handleTus(req, res);
|
return this.tusService.handleTus(req, res);
|
||||||
}
|
}
|
||||||
@Post('share/:fileId(*)')
|
|
||||||
async generateShareCode(@Param('fileId') fileId: string) {
|
|
||||||
try {
|
|
||||||
console.log('收到生成分享码请求,fileId:', fileId);
|
|
||||||
const result = await this.shareCodeService.generateShareCode(fileId);
|
|
||||||
console.log('生成分享码结果:', result);
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('生成分享码错误:', error);
|
|
||||||
throw new HttpException(
|
|
||||||
{
|
|
||||||
message: (error as Error).message || '生成分享码失败',
|
|
||||||
error: 'SHARE_CODE_GENERATION_FAILED'
|
|
||||||
},
|
|
||||||
HttpStatus.INTERNAL_SERVER_ERROR
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('filename')
|
|
||||||
async saveFileName(@Body() data: { fileId: string; fileName: string }) {
|
|
||||||
try {
|
|
||||||
console.log('收到保存文件名请求:', data);
|
|
||||||
|
|
||||||
// 检查参数
|
|
||||||
if (!data.fileId || !data.fileName) {
|
|
||||||
throw new HttpException(
|
|
||||||
{ message: '缺少必要参数' },
|
|
||||||
HttpStatus.BAD_REQUEST
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// 保存文件名
|
|
||||||
await this.resourceService.saveFileName(data.fileId, data.fileName);
|
|
||||||
console.log('文件名保存成功:', data.fileName, '对应文件ID:', data.fileId);
|
|
||||||
|
|
||||||
return { success: true };
|
|
||||||
} catch (error) {
|
|
||||||
console.error('保存文件名失败:', error);
|
|
||||||
throw new HttpException(
|
|
||||||
{
|
|
||||||
message: '保存文件名失败',
|
|
||||||
error: (error instanceof Error) ? error.message : String(error)
|
|
||||||
},
|
|
||||||
HttpStatus.INTERNAL_SERVER_ERROR
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('download/:fileId')
|
|
||||||
async downloadFile(@Param('fileId') fileId: string, @Res() res: Response) {
|
|
||||||
try {
|
|
||||||
// 获取文件信息
|
|
||||||
const resource = await this.resourceService.findUnique({
|
|
||||||
where: { fileId },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!resource) {
|
|
||||||
throw new NotFoundException('文件不存在');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取原始文件名
|
|
||||||
const fileName = await this.resourceService.getFileName(fileId) || 'downloaded-file';
|
|
||||||
|
|
||||||
// 设置响应头,包含原始文件名
|
|
||||||
res.setHeader(
|
|
||||||
'Content-Disposition',
|
|
||||||
`attachment; filename="${encodeURIComponent(fileName)}"`
|
|
||||||
);
|
|
||||||
|
|
||||||
// 其他下载逻辑...
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
// 错误处理...
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { UploadController } from './upload.controller';
|
||||||
import { BullModule } from '@nestjs/bullmq';
|
import { BullModule } from '@nestjs/bullmq';
|
||||||
import { TusService } from './tus.service';
|
import { TusService } from './tus.service';
|
||||||
import { ResourceModule } from '@server/models/resource/resource.module';
|
import { ResourceModule } from '@server/models/resource/resource.module';
|
||||||
import { ShareCodeService } from './share-code.service';
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
BullModule.registerQueue({
|
BullModule.registerQueue({
|
||||||
|
@ -12,6 +11,6 @@ import { ShareCodeService } from './share-code.service';
|
||||||
ResourceModule,
|
ResourceModule,
|
||||||
],
|
],
|
||||||
controllers: [UploadController],
|
controllers: [UploadController],
|
||||||
providers: [TusService, ShareCodeService],
|
providers: [TusService],
|
||||||
})
|
})
|
||||||
export class UploadModule {}
|
export class UploadModule {}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import mitt from 'mitt';
|
import mitt from 'mitt';
|
||||||
import { ObjectType, UserProfile, MessageDto, VisitType } from '@nice/common';
|
import { ObjectType, UserProfile, VisitType } from '@nice/common';
|
||||||
export enum CrudOperation {
|
export enum CrudOperation {
|
||||||
CREATED,
|
CREATED,
|
||||||
UPDATED,
|
UPDATED,
|
||||||
|
@ -24,7 +24,6 @@ type Events = {
|
||||||
updateTotalCourseViewCount: {
|
updateTotalCourseViewCount: {
|
||||||
visitType: VisitType | string;
|
visitType: VisitType | string;
|
||||||
};
|
};
|
||||||
onMessageCreated: { data: Partial<MessageDto> };
|
|
||||||
dataChanged: { type: string; operation: CrudOperation; data: any };
|
dataChanged: { type: string; operation: CrudOperation; data: any };
|
||||||
};
|
};
|
||||||
const EventBus = mitt<Events>();
|
const EventBus = mitt<Events>();
|
||||||
|
|
|
@ -6,6 +6,4 @@ export * from "./useRole"
|
||||||
export * from "./useRoleMap"
|
export * from "./useRoleMap"
|
||||||
export * from "./useTransform"
|
export * from "./useTransform"
|
||||||
export * from "./useTaxonomy"
|
export * from "./useTaxonomy"
|
||||||
export * from "./useMessage"
|
|
||||||
export * from "./usePost"
|
|
||||||
export * from "./useEntity"
|
export * from "./useEntity"
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
import { useEntity } from "./useEntity";
|
|
||||||
|
|
||||||
export function useMessage() {
|
|
||||||
return useEntity("message");
|
|
||||||
}
|
|
|
@ -1,5 +1,5 @@
|
||||||
|
|
||||||
import { MutationResult, useEntity } from "./useEntity";
|
import { MutationResult, useEntity } from "./useEntity";
|
||||||
export function usePost() {
|
// export function usePost() {
|
||||||
return useEntity("post");
|
// return useEntity("post");
|
||||||
}
|
// }
|
||||||
|
|
|
@ -26,7 +26,6 @@ export function useStaff() {
|
||||||
const update = api.staff.update.useMutation({
|
const update = api.staff.update.useMutation({
|
||||||
onSuccess: (result) => {
|
onSuccess: (result) => {
|
||||||
queryClient.invalidateQueries({ queryKey });
|
queryClient.invalidateQueries({ queryKey });
|
||||||
queryClient.invalidateQueries({ queryKey: getQueryKey(api.post) });
|
|
||||||
emitDataChange(
|
emitDataChange(
|
||||||
ObjectType.STAFF,
|
ObjectType.STAFF,
|
||||||
result as any,
|
result as any,
|
||||||
|
|
|
@ -44,7 +44,6 @@ model Term {
|
||||||
createdBy String? @map("created_by")
|
createdBy String? @map("created_by")
|
||||||
depts Department[] @relation("department_term")
|
depts Department[] @relation("department_term")
|
||||||
hasChildren Boolean? @default(false) @map("has_children")
|
hasChildren Boolean? @default(false) @map("has_children")
|
||||||
posts Post[] @relation("post_term")
|
|
||||||
|
|
||||||
@@index([name]) // 对name字段建立索引,以加快基于name的查找速度
|
@@index([name]) // 对name字段建立索引,以加快基于name的查找速度
|
||||||
@@index([parentId]) // 对parentId字段建立索引,以加快基于parentId的查找速度
|
@@index([parentId]) // 对parentId字段建立索引,以加快基于parentId的查找速度
|
||||||
|
@ -88,12 +87,7 @@ model Staff {
|
||||||
deletedAt DateTime? @map("deleted_at")
|
deletedAt DateTime? @map("deleted_at")
|
||||||
officerId String? @map("officer_id")
|
officerId String? @map("officer_id")
|
||||||
|
|
||||||
// watchedPost Post[] @relation("post_watch_staff")
|
|
||||||
posts Post[]
|
|
||||||
|
|
||||||
learningPosts Post[] @relation("post_student")
|
|
||||||
registerToken String?
|
registerToken String?
|
||||||
teachedPosts PostInstructor[]
|
|
||||||
ownedResources Resource[]
|
ownedResources Resource[]
|
||||||
|
|
||||||
@@index([officerId])
|
@@index([officerId])
|
||||||
|
@ -108,7 +102,6 @@ model Department {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String
|
name String
|
||||||
order Float?
|
order Float?
|
||||||
posts Post[] @relation("post_dept")
|
|
||||||
ancestors DeptAncestry[] @relation("DescendantToAncestor")
|
ancestors DeptAncestry[] @relation("DescendantToAncestor")
|
||||||
descendants DeptAncestry[] @relation("AncestorToDescendant")
|
descendants DeptAncestry[] @relation("AncestorToDescendant")
|
||||||
parentId String? @map("parent_id")
|
parentId String? @map("parent_id")
|
||||||
|
@ -184,91 +177,7 @@ model AppConfig {
|
||||||
@@map("app_config")
|
@@map("app_config")
|
||||||
}
|
}
|
||||||
|
|
||||||
model Post {
|
|
||||||
// 字符串类型字段
|
|
||||||
id String @id @default(cuid()) // 帖子唯一标识,使用 cuid() 生成默认值
|
|
||||||
type String? // Post类型,课程、章节、小节、讨论都用Post实现
|
|
||||||
level String?
|
|
||||||
state String?
|
|
||||||
title String? // 帖子标题,可为空
|
|
||||||
subTitle String?
|
|
||||||
content String? // 帖子内容,可为空
|
|
||||||
important Boolean? //是否重要/精选/突出
|
|
||||||
domainId String? @map("domain_id")
|
|
||||||
terms Term[] @relation("post_term")
|
|
||||||
order Float? @default(0) @map("order")
|
|
||||||
duration Int?
|
|
||||||
rating Int? @default(0)
|
|
||||||
students Staff[] @relation("post_student")
|
|
||||||
depts Department[] @relation("post_dept")
|
|
||||||
views Int @default(0) @map("views")
|
|
||||||
hates Int @default(0) @map("hates")
|
|
||||||
likes Int @default(0) @map("likes")
|
|
||||||
// 索引
|
|
||||||
// 日期时间类型字段
|
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
|
||||||
publishedAt DateTime? @map("published_at") // 发布时间
|
|
||||||
updatedAt DateTime @map("updated_at")
|
|
||||||
deletedAt DateTime? @map("deleted_at") // 删除时间,可为空
|
|
||||||
instructors PostInstructor[]
|
|
||||||
// 关系类型字段
|
|
||||||
authorId String? @map("author_id")
|
|
||||||
author Staff? @relation(fields: [authorId], references: [id]) // 帖子作者,关联 Staff 模型
|
|
||||||
parentId String? @map("parent_id")
|
|
||||||
parent Post? @relation("PostChildren", fields: [parentId], references: [id]) // 父级帖子,关联 Post 模型
|
|
||||||
children Post[] @relation("PostChildren") // 子级帖子列表,关联 Post 模型
|
|
||||||
hasChildren Boolean? @default(false) @map("has_children")
|
|
||||||
// 闭包表关系
|
|
||||||
ancestors PostAncestry[] @relation("DescendantPosts")
|
|
||||||
descendants PostAncestry[] @relation("AncestorPosts")
|
|
||||||
resources Resource[] // 附件列表
|
|
||||||
meta Json? // 封面url 视频url objectives具体的学习目标 rating评分Int
|
|
||||||
|
|
||||||
// 索引
|
|
||||||
@@index([type, domainId])
|
|
||||||
@@index([authorId, type])
|
|
||||||
@@index([parentId, type])
|
|
||||||
@@index([parentId, order])
|
|
||||||
@@index([createdAt])
|
|
||||||
@@index([updatedAt])
|
|
||||||
@@index([type, publishedAt])
|
|
||||||
@@index([state])
|
|
||||||
@@index([level])
|
|
||||||
@@index([views])
|
|
||||||
@@index([important])
|
|
||||||
@@map("post")
|
|
||||||
}
|
|
||||||
|
|
||||||
model PostAncestry {
|
|
||||||
id String @id @default(cuid())
|
|
||||||
ancestorId String? @map("ancestor_id")
|
|
||||||
descendantId String @map("descendant_id")
|
|
||||||
relDepth Int @map("rel_depth")
|
|
||||||
ancestor Post? @relation("AncestorPosts", fields: [ancestorId], references: [id])
|
|
||||||
descendant Post @relation("DescendantPosts", fields: [descendantId], references: [id])
|
|
||||||
|
|
||||||
// 复合索引优化
|
|
||||||
// 索引建议
|
|
||||||
@@index([ancestorId]) // 针对祖先的查询
|
|
||||||
@@index([descendantId]) // 针对后代的查询
|
|
||||||
@@index([ancestorId, descendantId]) // 组合索引,用于查询特定的祖先-后代关系
|
|
||||||
@@index([relDepth]) // 根据关系深度的查询
|
|
||||||
@@map("post_ancestry")
|
|
||||||
}
|
|
||||||
|
|
||||||
model PostInstructor {
|
|
||||||
postId String @map("post_id")
|
|
||||||
instructorId String @map("instructor_id")
|
|
||||||
role String @map("role")
|
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
|
||||||
order Float? @default(0) @map("order")
|
|
||||||
|
|
||||||
post Post @relation(fields: [postId], references: [id])
|
|
||||||
instructor Staff @relation(fields: [instructorId], references: [id])
|
|
||||||
|
|
||||||
@@id([postId, instructorId])
|
|
||||||
@@map("post_instructor")
|
|
||||||
}
|
|
||||||
|
|
||||||
model Resource {
|
model Resource {
|
||||||
id String @id @default(cuid()) @map("id")
|
id String @id @default(cuid()) @map("id")
|
||||||
|
@ -289,8 +198,7 @@ model Resource {
|
||||||
isPublic Boolean? @default(true) @map("is_public")
|
isPublic Boolean? @default(true) @map("is_public")
|
||||||
owner Staff? @relation(fields: [ownerId], references: [id])
|
owner Staff? @relation(fields: [ownerId], references: [id])
|
||||||
ownerId String? @map("owner_id")
|
ownerId String? @map("owner_id")
|
||||||
post Post? @relation(fields: [postId], references: [id])
|
|
||||||
postId String? @map("post_id")
|
|
||||||
// 索引
|
// 索引
|
||||||
@@index([type])
|
@@index([type])
|
||||||
@@index([createdAt])
|
@@index([createdAt])
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
export * from "./department";
|
export * from "./department";
|
||||||
export * from "./message";
|
|
||||||
export * from "./staff";
|
export * from "./staff";
|
||||||
export * from "./term";
|
export * from "./term";
|
||||||
export * from "./post";
|
|
||||||
export * from "./rbac";
|
export * from "./rbac";
|
||||||
export * from "./select";
|
export * from "./select";
|
||||||
export * from "./resource";
|
export * from "./resource";
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
import { Message, Staff } from "@prisma/client";
|
|
||||||
|
|
||||||
export type MessageDto = Message & {
|
|
||||||
readed: boolean;
|
|
||||||
receivers: Staff[];
|
|
||||||
sender: Staff;
|
|
||||||
};
|
|
|
@ -1,136 +0,0 @@
|
||||||
import {
|
|
||||||
Post,
|
|
||||||
Department,
|
|
||||||
Staff,
|
|
||||||
Enrollment,
|
|
||||||
Taxonomy,
|
|
||||||
Term,
|
|
||||||
} from "@prisma/client";
|
|
||||||
import { StaffDto } from "./staff";
|
|
||||||
import { TermDto } from "./term";
|
|
||||||
import { ResourceDto } from "./resource";
|
|
||||||
import { DepartmentDto } from "./department";
|
|
||||||
|
|
||||||
export type PostComment = {
|
|
||||||
id: string;
|
|
||||||
type: string;
|
|
||||||
title: string;
|
|
||||||
content: string;
|
|
||||||
authorId: string;
|
|
||||||
domainId: string;
|
|
||||||
referenceId: string;
|
|
||||||
resources: string[];
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
parentId: string;
|
|
||||||
author: {
|
|
||||||
id: string;
|
|
||||||
showname: string;
|
|
||||||
username: string;
|
|
||||||
avatar: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
export type PostDto = Post & {
|
|
||||||
readed: boolean;
|
|
||||||
readedCount: number;
|
|
||||||
author: StaffDto;
|
|
||||||
limitedComments: PostComment[];
|
|
||||||
commentsCount: number;
|
|
||||||
perms?: {
|
|
||||||
delete: boolean;
|
|
||||||
// edit: boolean;
|
|
||||||
};
|
|
||||||
meta?: PostMeta;
|
|
||||||
watchableDepts: Department[];
|
|
||||||
watchableStaffs: Staff[];
|
|
||||||
terms: TermDto[];
|
|
||||||
depts: DepartmentDto[];
|
|
||||||
studentIds?: string[];
|
|
||||||
};
|
|
||||||
export type PostMeta = {
|
|
||||||
thumbnail?: string;
|
|
||||||
views?: number;
|
|
||||||
likes?: number;
|
|
||||||
hates?: number;
|
|
||||||
};
|
|
||||||
export type LectureMeta = PostMeta & {
|
|
||||||
type?: string;
|
|
||||||
|
|
||||||
videoUrl?: string;
|
|
||||||
videoThumbnail?: string;
|
|
||||||
videoIds?: string[];
|
|
||||||
videoThumbnailIds?: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Lecture = Post & {
|
|
||||||
courseId?: string;
|
|
||||||
resources?: ResourceDto[];
|
|
||||||
meta?: LectureMeta;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SectionMeta = PostMeta & {
|
|
||||||
objectives?: string[];
|
|
||||||
};
|
|
||||||
export type Section = Post & {
|
|
||||||
courseId?: string;
|
|
||||||
meta?: SectionMeta;
|
|
||||||
};
|
|
||||||
export type SectionDto = Section & {
|
|
||||||
lectures: Lecture[];
|
|
||||||
};
|
|
||||||
export type CourseMeta = PostMeta & {
|
|
||||||
objectives?: string[];
|
|
||||||
};
|
|
||||||
export type Course = PostDto & {
|
|
||||||
meta?: CourseMeta;
|
|
||||||
};
|
|
||||||
export type CourseDto = Course & {
|
|
||||||
enrollments?: Enrollment[];
|
|
||||||
sections?: SectionDto[];
|
|
||||||
terms: TermDto[];
|
|
||||||
lectureCount?: number;
|
|
||||||
depts: Department[];
|
|
||||||
studentIds: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Summary = {
|
|
||||||
id: string;
|
|
||||||
text: string;
|
|
||||||
parent: string;
|
|
||||||
start: number;
|
|
||||||
end: number;
|
|
||||||
};
|
|
||||||
export type NodeObj = {
|
|
||||||
topic: string;
|
|
||||||
id: string;
|
|
||||||
style?: {
|
|
||||||
fontSize?: string;
|
|
||||||
color?: string;
|
|
||||||
background?: string;
|
|
||||||
fontWeight?: string;
|
|
||||||
};
|
|
||||||
children?: NodeObj[];
|
|
||||||
};
|
|
||||||
export type Arrow = {
|
|
||||||
id: string;
|
|
||||||
label: string;
|
|
||||||
from: string;
|
|
||||||
to: string;
|
|
||||||
delta1: {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
};
|
|
||||||
delta2: {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
export type PathMeta = PostMeta & {
|
|
||||||
nodeData: NodeObj;
|
|
||||||
arrows?: Arrow[];
|
|
||||||
summaries?: Summary[];
|
|
||||||
direction?: number;
|
|
||||||
};
|
|
||||||
export type PathDto = PostDto & {
|
|
||||||
meta: PathMeta;
|
|
||||||
};
|
|
|
@ -1,138 +1,2 @@
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
|
|
||||||
export const postDetailSelect: Prisma.PostSelect = {
|
|
||||||
id: true,
|
|
||||||
type: true,
|
|
||||||
title: true,
|
|
||||||
content: true,
|
|
||||||
resources: true,
|
|
||||||
parent: true,
|
|
||||||
parentId: true,
|
|
||||||
// watchableDepts: true,
|
|
||||||
// watchableStaffs: true,
|
|
||||||
updatedAt: true,
|
|
||||||
terms: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
taxonomyId: true,
|
|
||||||
taxonomy: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
slug: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
depts: true,
|
|
||||||
author: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
showname: true,
|
|
||||||
avatar: true,
|
|
||||||
department: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
domain: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
meta: true,
|
|
||||||
views: true,
|
|
||||||
};
|
|
||||||
export const postUnDetailSelect: Prisma.PostSelect = {
|
|
||||||
id: true,
|
|
||||||
type: true,
|
|
||||||
title: true,
|
|
||||||
views: true,
|
|
||||||
parent: true,
|
|
||||||
parentId: true,
|
|
||||||
content: true,
|
|
||||||
resources: true,
|
|
||||||
updatedAt: true,
|
|
||||||
author: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
showname: true,
|
|
||||||
avatar: true,
|
|
||||||
department: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
domain: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
export const messageDetailSelect: Prisma.MessageSelect = {
|
|
||||||
id: true,
|
|
||||||
sender: true,
|
|
||||||
content: true,
|
|
||||||
|
|
||||||
title: true,
|
|
||||||
url: true,
|
|
||||||
option: true,
|
|
||||||
intent: true,
|
|
||||||
};
|
|
||||||
export const courseDetailSelect: Prisma.PostSelect = {
|
|
||||||
id: true,
|
|
||||||
title: true,
|
|
||||||
subTitle: true,
|
|
||||||
views: true,
|
|
||||||
type: true,
|
|
||||||
author: true,
|
|
||||||
authorId: true,
|
|
||||||
content: true,
|
|
||||||
depts: true,
|
|
||||||
// isFeatured: true,
|
|
||||||
createdAt: true,
|
|
||||||
updatedAt: true,
|
|
||||||
deletedAt: true,
|
|
||||||
// 关联表选择
|
|
||||||
terms: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
taxonomyId: true,
|
|
||||||
taxonomy: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
slug: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
enrollments: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
meta: true,
|
|
||||||
rating: true,
|
|
||||||
};
|
|
||||||
export const lectureDetailSelect: Prisma.PostSelect = {
|
|
||||||
id: true,
|
|
||||||
title: true,
|
|
||||||
subTitle: true,
|
|
||||||
content: true,
|
|
||||||
resources: true,
|
|
||||||
views: true,
|
|
||||||
createdAt: true,
|
|
||||||
updatedAt: true,
|
|
||||||
// 关联表选择
|
|
||||||
|
|
||||||
meta: true,
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { z, ZodType } from "zod";
|
import { z, ZodType } from "zod";
|
||||||
import { ObjectType } from "./enum";
|
import { ObjectType } from "./enum";
|
||||||
import { Prisma } from ".";
|
import { Prisma } from ".";
|
||||||
const PostCreateArgsSchema: ZodType<Prisma.PostCreateArgs> = z.any();
|
|
||||||
export const AuthSchema = {
|
export const AuthSchema = {
|
||||||
signUpRequest: z.object({
|
signUpRequest: z.object({
|
||||||
username: z.string(),
|
username: z.string(),
|
||||||
|
@ -390,46 +389,4 @@ export const BaseCursorSchema = z.object({
|
||||||
createEndDate: z.date().nullish(),
|
createEndDate: z.date().nullish(),
|
||||||
deptId: z.string().nullish(),
|
deptId: z.string().nullish(),
|
||||||
});
|
});
|
||||||
export const CourseMethodSchema = {
|
|
||||||
createLecture: z.object({
|
|
||||||
sectionId: z.string().nullish(),
|
|
||||||
type: z.string().nullish(), // 视频还是文章
|
|
||||||
title: z.string().nullish(), //单课标题
|
|
||||||
content: z.string().nullish(), //文章内容
|
|
||||||
resourceIds: z.array(z.string()).nullish(),
|
|
||||||
}),
|
|
||||||
createSection: z.object({
|
|
||||||
courseId: z.string().nullish(),
|
|
||||||
title: z.string().nullish(), //单课标题
|
|
||||||
lectures: z
|
|
||||||
.array(
|
|
||||||
z.object({
|
|
||||||
type: z.string().nullish(),
|
|
||||||
title: z.string().nullish(),
|
|
||||||
content: z.string().nullish(),
|
|
||||||
resourceIds: z.array(z.string()).nullish(),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.nullish(),
|
|
||||||
}),
|
|
||||||
createCourse: z.object({
|
|
||||||
courseDetail: PostCreateArgsSchema,
|
|
||||||
sections: z
|
|
||||||
.array(
|
|
||||||
z.object({
|
|
||||||
title: z.string(),
|
|
||||||
lectures: z
|
|
||||||
.array(
|
|
||||||
z.object({
|
|
||||||
type: z.string(),
|
|
||||||
title: z.string(),
|
|
||||||
content: z.string().optional(),
|
|
||||||
resourceIds: z.array(z.string()).optional(),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.optional(),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.optional(),
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
|
@ -2,13 +2,10 @@ import type {
|
||||||
Staff,
|
Staff,
|
||||||
Department,
|
Department,
|
||||||
Term,
|
Term,
|
||||||
Message,
|
|
||||||
Post,
|
|
||||||
RoleMap,
|
RoleMap,
|
||||||
// Section,
|
// Section,
|
||||||
// Lecture,
|
// Lecture,
|
||||||
// Course,
|
// Course,
|
||||||
Enrollment,
|
|
||||||
} from "@prisma/client";
|
} from "@prisma/client";
|
||||||
import { SocketMsgType, RolePerms } from "./enum";
|
import { SocketMsgType, RolePerms } from "./enum";
|
||||||
import { RowRequestSchema } from "./schema";
|
import { RowRequestSchema } from "./schema";
|
||||||
|
|
Loading…
Reference in New Issue