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,
|
||||
VisitType,
|
||||
} from '@nice/common';
|
||||
export async function updateTotalCourseViewCount(type: VisitType) {
|
||||
const posts = await db.post.findMany({
|
||||
where: {
|
||||
// type: { in: [PostType.COURSE, PostType.LECTURE,] },
|
||||
deletedAt: null,
|
||||
},
|
||||
select: { id: true, type: true },
|
||||
});
|
||||
// export async function updateTotalCourseViewCount(type: VisitType) {
|
||||
// const posts = await db.post.findMany({
|
||||
// where: {
|
||||
// // type: { in: [PostType.COURSE, PostType.LECTURE,] },
|
||||
// deletedAt: null,
|
||||
// },
|
||||
// select: { id: true, type: true },
|
||||
// });
|
||||
|
||||
const courseIds = posts
|
||||
.filter((post) => post.type === PostType.COURSE)
|
||||
.map((course) => course.id);
|
||||
const lectures = posts.filter((post) => post.type === PostType.LECTURE);
|
||||
const totalViews = await db.visit.aggregate({
|
||||
_sum: {
|
||||
views: true,
|
||||
},
|
||||
where: {
|
||||
postId: { in: posts.map((post) => post.id) },
|
||||
type: type,
|
||||
},
|
||||
});
|
||||
const appConfig = await db.appConfig.findFirst({
|
||||
where: {
|
||||
slug: AppConfigSlug.BASE_SETTING,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
meta: true,
|
||||
},
|
||||
});
|
||||
const staffs = await db.staff.count({
|
||||
where: { deletedAt: null },
|
||||
});
|
||||
// const courseIds = posts
|
||||
// .filter((post) => post.type === PostType.COURSE)
|
||||
// .map((course) => course.id);
|
||||
// const lectures = posts.filter((post) => post.type === PostType.LECTURE);
|
||||
// const totalViews = await db.visit.aggregate({
|
||||
// _sum: {
|
||||
// views: true,
|
||||
// },
|
||||
// where: {
|
||||
// postId: { in: posts.map((post) => post.id) },
|
||||
// type: type,
|
||||
// },
|
||||
// });
|
||||
// const appConfig = await db.appConfig.findFirst({
|
||||
// where: {
|
||||
// slug: AppConfigSlug.BASE_SETTING,
|
||||
// },
|
||||
// select: {
|
||||
// id: true,
|
||||
// meta: true,
|
||||
// },
|
||||
// });
|
||||
// const staffs = await db.staff.count({
|
||||
// where: { deletedAt: null },
|
||||
// });
|
||||
|
||||
const baseSeting = appConfig.meta as BaseSetting;
|
||||
await db.appConfig.update({
|
||||
where: {
|
||||
slug: AppConfigSlug.BASE_SETTING,
|
||||
},
|
||||
data: {
|
||||
meta: {
|
||||
...baseSeting,
|
||||
appConfig: {
|
||||
...(baseSeting?.appConfig || {}),
|
||||
statistics: {
|
||||
reads: totalViews._sum.views || 0,
|
||||
courses: courseIds?.length || 0,
|
||||
staffs: staffs || 0,
|
||||
lectures: lectures?.length || 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
export async function updatePostViewCount(id: string, type: VisitType) {
|
||||
const post = await db.post.findFirst({
|
||||
where: { id },
|
||||
select: { id: true, meta: true, type: true },
|
||||
});
|
||||
const metaFieldMap = {
|
||||
[VisitType.READED]: 'views',
|
||||
[VisitType.LIKE]: 'likes',
|
||||
[VisitType.HATE]: 'hates',
|
||||
};
|
||||
if (post?.type === PostType.LECTURE) {
|
||||
const courseAncestry = await db.postAncestry.findFirst({
|
||||
where: {
|
||||
descendantId: post?.id,
|
||||
ancestor: {
|
||||
type: PostType.COURSE,
|
||||
},
|
||||
},
|
||||
select: { id: true, ancestorId: true },
|
||||
});
|
||||
const course = { id: courseAncestry.ancestorId };
|
||||
const lecturesAncestry = await db.postAncestry.findMany({
|
||||
where: { ancestorId: course.id, descendant: { type: PostType.LECTURE } },
|
||||
select: {
|
||||
id: true,
|
||||
descendantId: true,
|
||||
},
|
||||
});
|
||||
const lectures = lecturesAncestry.map((ancestry) => ({
|
||||
id: ancestry.descendantId,
|
||||
}));
|
||||
const courseViews = await db.visit.aggregate({
|
||||
_sum: {
|
||||
views: true,
|
||||
},
|
||||
where: {
|
||||
postId: {
|
||||
in: [course.id, ...lectures.map((lecture) => lecture.id)],
|
||||
},
|
||||
type: type,
|
||||
},
|
||||
});
|
||||
await db.post.update({
|
||||
where: { id: course.id },
|
||||
data: {
|
||||
[metaFieldMap[type]]: courseViews._sum.views || 0,
|
||||
// meta: {
|
||||
// ...((post?.meta as any) || {}),
|
||||
// [metaFieldMap[type]]: courseViews._sum.views || 0,
|
||||
// },
|
||||
},
|
||||
});
|
||||
}
|
||||
const totalViews = await db.visit.aggregate({
|
||||
_sum: {
|
||||
views: true,
|
||||
},
|
||||
where: {
|
||||
postId: id,
|
||||
type: type,
|
||||
},
|
||||
});
|
||||
await db.post.update({
|
||||
where: { id },
|
||||
data: {
|
||||
[metaFieldMap[type]]: totalViews._sum.views || 0,
|
||||
// meta: {
|
||||
// ...((post?.meta as any) || {}),
|
||||
// [metaFieldMap[type]]: totalViews._sum.views || 0,
|
||||
// },
|
||||
},
|
||||
});
|
||||
}
|
||||
// const baseSeting = appConfig.meta as BaseSetting;
|
||||
// await db.appConfig.update({
|
||||
// where: {
|
||||
// slug: AppConfigSlug.BASE_SETTING,
|
||||
// },
|
||||
// data: {
|
||||
// meta: {
|
||||
// ...baseSeting,
|
||||
// appConfig: {
|
||||
// ...(baseSeting?.appConfig || {}),
|
||||
// statistics: {
|
||||
// reads: totalViews._sum.views || 0,
|
||||
// courses: courseIds?.length || 0,
|
||||
// staffs: staffs || 0,
|
||||
// lectures: lectures?.length || 0,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
// export async function updatePostViewCount(id: string, type: VisitType) {
|
||||
// const post = await db.post.findFirst({
|
||||
// where: { id },
|
||||
// select: { id: true, meta: true, type: true },
|
||||
// });
|
||||
// const metaFieldMap = {
|
||||
// [VisitType.READED]: 'views',
|
||||
// [VisitType.LIKE]: 'likes',
|
||||
// [VisitType.HATE]: 'hates',
|
||||
// };
|
||||
// if (post?.type === PostType.LECTURE) {
|
||||
// const courseAncestry = await db.postAncestry.findFirst({
|
||||
// where: {
|
||||
// descendantId: post?.id,
|
||||
// ancestor: {
|
||||
// type: PostType.COURSE,
|
||||
// },
|
||||
// },
|
||||
// select: { id: true, ancestorId: true },
|
||||
// });
|
||||
// const course = { id: courseAncestry.ancestorId };
|
||||
// const lecturesAncestry = await db.postAncestry.findMany({
|
||||
// where: { ancestorId: course.id, descendant: { type: PostType.LECTURE } },
|
||||
// select: {
|
||||
// id: true,
|
||||
// descendantId: true,
|
||||
// },
|
||||
// });
|
||||
// const lectures = lecturesAncestry.map((ancestry) => ({
|
||||
// id: ancestry.descendantId,
|
||||
// }));
|
||||
// const courseViews = await db.visit.aggregate({
|
||||
// _sum: {
|
||||
// views: true,
|
||||
// },
|
||||
// where: {
|
||||
// postId: {
|
||||
// in: [course.id, ...lectures.map((lecture) => lecture.id)],
|
||||
// },
|
||||
// type: type,
|
||||
// },
|
||||
// });
|
||||
// await db.post.update({
|
||||
// where: { id: course.id },
|
||||
// data: {
|
||||
// [metaFieldMap[type]]: courseViews._sum.views || 0,
|
||||
// // meta: {
|
||||
// // ...((post?.meta as any) || {}),
|
||||
// // [metaFieldMap[type]]: courseViews._sum.views || 0,
|
||||
// // },
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
// const totalViews = await db.visit.aggregate({
|
||||
// _sum: {
|
||||
// views: true,
|
||||
// },
|
||||
// where: {
|
||||
// postId: id,
|
||||
// type: type,
|
||||
// },
|
||||
// });
|
||||
// await db.post.update({
|
||||
// where: { id },
|
||||
// data: {
|
||||
// [metaFieldMap[type]]: totalViews._sum.views || 0,
|
||||
// // meta: {
|
||||
// // ...((post?.meta as any) || {}),
|
||||
// // [metaFieldMap[type]]: totalViews._sum.views || 0,
|
||||
// // },
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
|
|
|
@ -6,61 +6,61 @@ import { ObjectType } from '@nice/common';
|
|||
// updateCourseReviewStats,
|
||||
// } from '@server/models/course/utils';
|
||||
import { QueueJobType } from '../types';
|
||||
import {
|
||||
updateCourseEnrollmentStats,
|
||||
updateCourseReviewStats,
|
||||
updateParentLectureStats,
|
||||
} from '@server/models/post/utils';
|
||||
import {
|
||||
updatePostViewCount,
|
||||
updateTotalCourseViewCount,
|
||||
} from '../models/post/utils';
|
||||
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 updateParentLectureStats(sectionId);
|
||||
logger.debug(`Updated section stats for sectionId: ${sectionId}`);
|
||||
}
|
||||
// 如果没有 courseId,提前返回
|
||||
if (!courseId) {
|
||||
return;
|
||||
}
|
||||
// 处理 course 相关统计
|
||||
switch (type) {
|
||||
case ObjectType.LECTURE:
|
||||
await updateParentLectureStats(courseId);
|
||||
break;
|
||||
case ObjectType.ENROLLMENT:
|
||||
await updateCourseEnrollmentStats(courseId);
|
||||
break;
|
||||
case ObjectType.POST:
|
||||
await updateCourseReviewStats(courseId);
|
||||
break;
|
||||
default:
|
||||
logger.warn(`Unknown update stats type: ${type}`);
|
||||
}
|
||||
// import {
|
||||
// updateCourseEnrollmentStats,
|
||||
// updateCourseReviewStats,
|
||||
// updateParentLectureStats,
|
||||
// } from '@server/models/post/utils';
|
||||
// import {
|
||||
// updatePostViewCount,
|
||||
// updateTotalCourseViewCount,
|
||||
// } from '../models/post/utils';
|
||||
// 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 updateParentLectureStats(sectionId);
|
||||
// logger.debug(`Updated section stats for sectionId: ${sectionId}`);
|
||||
// }
|
||||
// // 如果没有 courseId,提前返回
|
||||
// if (!courseId) {
|
||||
// return;
|
||||
// }
|
||||
// // 处理 course 相关统计
|
||||
// switch (type) {
|
||||
// case ObjectType.LECTURE:
|
||||
// await updateParentLectureStats(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}`,
|
||||
);
|
||||
}
|
||||
if (job.name === QueueJobType.UPDATE_POST_VISIT_COUNT) {
|
||||
await updatePostViewCount(job.data.id, job.data.type);
|
||||
}
|
||||
if (job.name === QueueJobType.UPDATE_POST_STATE) {
|
||||
await updatePostViewCount(job.data.id, job.data.type);
|
||||
}
|
||||
if (job.name === QueueJobType.UPDATE_TOTAL_COURSE_VIEW_COUNT) {
|
||||
await updateTotalCourseViewCount(job.data.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}`,
|
||||
// );
|
||||
// }
|
||||
// if (job.name === QueueJobType.UPDATE_POST_VISIT_COUNT) {
|
||||
// await updatePostViewCount(job.data.id, job.data.type);
|
||||
// }
|
||||
// if (job.name === QueueJobType.UPDATE_POST_STATE) {
|
||||
// await updatePostViewCount(job.data.id, job.data.type);
|
||||
// }
|
||||
// if (job.name === QueueJobType.UPDATE_TOTAL_COURSE_VIEW_COUNT) {
|
||||
// await updateTotalCourseViewCount(job.data.type);
|
||||
// }
|
||||
// } catch (error: any) {
|
||||
// logger.error(
|
||||
// `Error processing stats update job: ${error.message}`,
|
||||
// error.stack,
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
|
|
@ -2,20 +2,20 @@ import { Injectable, OnModuleInit } from "@nestjs/common";
|
|||
import { WebSocketType } from "../types";
|
||||
import { BaseWebSocketServer } from "../base/base-websocket-server";
|
||||
import EventBus, { CrudOperation } from "@server/utils/event-bus";
|
||||
import { ObjectType, SocketMsgType, MessageDto, PostDto, PostType } from "@nice/common";
|
||||
import { ObjectType, SocketMsgType } from "@nice/common";
|
||||
@Injectable()
|
||||
export class RealtimeServer extends BaseWebSocketServer implements OnModuleInit {
|
||||
onModuleInit() {
|
||||
EventBus.on("dataChanged", ({ data, type, operation }) => {
|
||||
if (type === ObjectType.MESSAGE && operation === CrudOperation.CREATED) {
|
||||
const receiverIds = (data as Partial<MessageDto>).receivers.map(receiver => receiver.id)
|
||||
this.sendToUsers(receiverIds, { type: SocketMsgType.NOTIFY, payload: { objectType: ObjectType.MESSAGE } })
|
||||
}
|
||||
// if (type === ObjectType.MESSAGE && operation === CrudOperation.CREATED) {
|
||||
// const receiverIds = (data as Partial<MessageDto>).receivers.map(receiver => receiver.id)
|
||||
// this.sendToUsers(receiverIds, { type: SocketMsgType.NOTIFY, payload: { objectType: ObjectType.MESSAGE } })
|
||||
// }
|
||||
|
||||
if (type === ObjectType.POST) {
|
||||
const post = data as Partial<PostDto>
|
||||
// if (type === ObjectType.POST) {
|
||||
// const post = data as Partial<PostDto>
|
||||
|
||||
}
|
||||
// }
|
||||
})
|
||||
|
||||
}
|
||||
|
|
|
@ -46,7 +46,6 @@ export class GenDevService {
|
|||
await this.generateDepartments(3, 6);
|
||||
await this.generateTerms(2, 6);
|
||||
await this.generateStaffs(4);
|
||||
await this.generateCourses(8);
|
||||
} catch (err) {
|
||||
this.logger.error(err);
|
||||
}
|
||||
|
@ -144,65 +143,7 @@ export class GenDevService {
|
|||
collectChildren(domainId);
|
||||
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) {
|
||||
if (this.counts.staffCount === 1) {
|
||||
this.logger.log('Generating staffs...');
|
||||
|
@ -266,31 +207,6 @@ export class GenDevService {
|
|||
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(
|
||||
name: string,
|
||||
parentId?: string | null,
|
||||
|
|
|
@ -8,10 +8,8 @@ import {
|
|||
import dayjs from 'dayjs';
|
||||
export interface DevDataCounts {
|
||||
deptCount: number;
|
||||
|
||||
staffCount: number;
|
||||
termCount: number;
|
||||
courseCount: number;
|
||||
}
|
||||
export async function getCounts(): Promise<DevDataCounts> {
|
||||
const counts = {
|
||||
|
@ -19,11 +17,6 @@ export async function getCounts(): Promise<DevDataCounts> {
|
|||
|
||||
staffCount: await db.staff.count(),
|
||||
termCount: await db.term.count(),
|
||||
courseCount: await db.post.count({
|
||||
where: {
|
||||
type: PostType.COURSE,
|
||||
},
|
||||
}),
|
||||
};
|
||||
return counts;
|
||||
}
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { ReminderService } from './reminder.service';
|
||||
import { MessageModule } from '@server/models/message/message.module';
|
||||
|
||||
@Module({
|
||||
imports: [ MessageModule],
|
||||
imports: [],
|
||||
providers: [ReminderService],
|
||||
exports: [ReminderService]
|
||||
})
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { MessageService } from '@server/models/message/message.service';
|
||||
|
||||
/**
|
||||
* 提醒服务类
|
||||
|
@ -25,7 +24,7 @@ export class ReminderService {
|
|||
* 构造函数
|
||||
* @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 { AuthModule } from '@server/auth/auth.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 { RoleMapModule } from '@server/models/rbac/rbac.module';
|
||||
import { TransformModule } from '@server/models/transform/transform.module';
|
||||
|
@ -25,9 +23,7 @@ import { ResourceModule } from '@server/models/resource/resource.module';
|
|||
TaxonomyModule,
|
||||
RoleMapModule,
|
||||
TransformModule,
|
||||
MessageModule,
|
||||
AppConfigModule,
|
||||
PostModule,
|
||||
WebSocketModule,
|
||||
ResourceModule,
|
||||
],
|
||||
|
|
|
@ -7,8 +7,6 @@ import { TrpcService } from '@server/trpc/trpc.service';
|
|||
import * as trpcExpress from '@trpc/server/adapters/express';
|
||||
import ws, { WebSocketServer } from 'ws';
|
||||
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 { TransformRouter } from '@server/models/transform/transform.router';
|
||||
import { RoleRouter } from '@server/models/rbac/role.router';
|
||||
|
@ -18,7 +16,6 @@ export class TrpcRouter {
|
|||
logger = new Logger(TrpcRouter.name);
|
||||
constructor(
|
||||
private readonly trpc: TrpcService,
|
||||
private readonly post: PostRouter,
|
||||
private readonly department: DepartmentRouter,
|
||||
private readonly staff: StaffRouter,
|
||||
private readonly term: TermRouter,
|
||||
|
@ -27,7 +24,6 @@ export class TrpcRouter {
|
|||
private readonly rolemap: RoleMapRouter,
|
||||
private readonly transform: TransformRouter,
|
||||
private readonly app_config: AppConfigRouter,
|
||||
private readonly message: MessageRouter,
|
||||
private readonly resource: ResourceRouter,
|
||||
) {}
|
||||
getRouter() {
|
||||
|
@ -35,14 +31,12 @@ export class TrpcRouter {
|
|||
}
|
||||
appRouter = this.trpc.router({
|
||||
transform: this.transform.router,
|
||||
post: this.post.router,
|
||||
department: this.department.router,
|
||||
staff: this.staff.router,
|
||||
term: this.term.router,
|
||||
taxonomy: this.taxonomy.router,
|
||||
role: this.role.router,
|
||||
rolemap: this.rolemap.router,
|
||||
message: this.message.router,
|
||||
app_config: this.app_config.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';
|
||||
import { Request, Response } from 'express';
|
||||
import { TusService } from './tus.service';
|
||||
import { ShareCodeService } from './share-code.service';
|
||||
import { ResourceService } from '@server/models/resource/resource.service';
|
||||
|
||||
interface ResourceMeta {
|
||||
|
@ -30,7 +29,6 @@ interface ResourceMeta {
|
|||
export class UploadController {
|
||||
constructor(
|
||||
private readonly tusService: TusService,
|
||||
private readonly shareCodeService: ShareCodeService,
|
||||
private readonly resourceService: ResourceService,
|
||||
) {}
|
||||
// @Post()
|
||||
|
@ -52,52 +50,7 @@ export class UploadController {
|
|||
async handlePost(@Req() req: Request, @Res() res: Response) {
|
||||
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('/*')
|
||||
async handleGet(@Req() req: Request, @Res() res: Response) {
|
||||
return this.tusService.handleTus(req, res);
|
||||
|
@ -113,80 +66,5 @@ export class UploadController {
|
|||
async handleUpload(@Req() req: Request, @Res() res: Response) {
|
||||
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 { TusService } from './tus.service';
|
||||
import { ResourceModule } from '@server/models/resource/resource.module';
|
||||
import { ShareCodeService } from './share-code.service';
|
||||
@Module({
|
||||
imports: [
|
||||
BullModule.registerQueue({
|
||||
|
@ -12,6 +11,6 @@ import { ShareCodeService } from './share-code.service';
|
|||
ResourceModule,
|
||||
],
|
||||
controllers: [UploadController],
|
||||
providers: [TusService, ShareCodeService],
|
||||
providers: [TusService],
|
||||
})
|
||||
export class UploadModule {}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import mitt from 'mitt';
|
||||
import { ObjectType, UserProfile, MessageDto, VisitType } from '@nice/common';
|
||||
import { ObjectType, UserProfile, VisitType } from '@nice/common';
|
||||
export enum CrudOperation {
|
||||
CREATED,
|
||||
UPDATED,
|
||||
|
@ -24,7 +24,6 @@ type Events = {
|
|||
updateTotalCourseViewCount: {
|
||||
visitType: VisitType | string;
|
||||
};
|
||||
onMessageCreated: { data: Partial<MessageDto> };
|
||||
dataChanged: { type: string; operation: CrudOperation; data: any };
|
||||
};
|
||||
const EventBus = mitt<Events>();
|
||||
|
|
|
@ -6,6 +6,4 @@ export * from "./useRole"
|
|||
export * from "./useRoleMap"
|
||||
export * from "./useTransform"
|
||||
export * from "./useTaxonomy"
|
||||
export * from "./useMessage"
|
||||
export * from "./usePost"
|
||||
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";
|
||||
export function usePost() {
|
||||
return useEntity("post");
|
||||
}
|
||||
// export function usePost() {
|
||||
// return useEntity("post");
|
||||
// }
|
||||
|
|
|
@ -26,7 +26,6 @@ export function useStaff() {
|
|||
const update = api.staff.update.useMutation({
|
||||
onSuccess: (result) => {
|
||||
queryClient.invalidateQueries({ queryKey });
|
||||
queryClient.invalidateQueries({ queryKey: getQueryKey(api.post) });
|
||||
emitDataChange(
|
||||
ObjectType.STAFF,
|
||||
result as any,
|
||||
|
|
|
@ -44,7 +44,6 @@ model Term {
|
|||
createdBy String? @map("created_by")
|
||||
depts Department[] @relation("department_term")
|
||||
hasChildren Boolean? @default(false) @map("has_children")
|
||||
posts Post[] @relation("post_term")
|
||||
|
||||
@@index([name]) // 对name字段建立索引,以加快基于name的查找速度
|
||||
@@index([parentId]) // 对parentId字段建立索引,以加快基于parentId的查找速度
|
||||
|
@ -88,12 +87,7 @@ model Staff {
|
|||
deletedAt DateTime? @map("deleted_at")
|
||||
officerId String? @map("officer_id")
|
||||
|
||||
// watchedPost Post[] @relation("post_watch_staff")
|
||||
posts Post[]
|
||||
|
||||
learningPosts Post[] @relation("post_student")
|
||||
registerToken String?
|
||||
teachedPosts PostInstructor[]
|
||||
ownedResources Resource[]
|
||||
|
||||
@@index([officerId])
|
||||
|
@ -108,7 +102,6 @@ model Department {
|
|||
id String @id @default(cuid())
|
||||
name String
|
||||
order Float?
|
||||
posts Post[] @relation("post_dept")
|
||||
ancestors DeptAncestry[] @relation("DescendantToAncestor")
|
||||
descendants DeptAncestry[] @relation("AncestorToDescendant")
|
||||
parentId String? @map("parent_id")
|
||||
|
@ -184,91 +177,7 @@ model AppConfig {
|
|||
@@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 {
|
||||
id String @id @default(cuid()) @map("id")
|
||||
|
@ -289,8 +198,7 @@ model Resource {
|
|||
isPublic Boolean? @default(true) @map("is_public")
|
||||
owner Staff? @relation(fields: [ownerId], references: [id])
|
||||
ownerId String? @map("owner_id")
|
||||
post Post? @relation(fields: [postId], references: [id])
|
||||
postId String? @map("post_id")
|
||||
|
||||
// 索引
|
||||
@@index([type])
|
||||
@@index([createdAt])
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
export * from "./department";
|
||||
export * from "./message";
|
||||
export * from "./staff";
|
||||
export * from "./term";
|
||||
export * from "./post";
|
||||
export * from "./rbac";
|
||||
export * from "./select";
|
||||
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";
|
||||
|
||||
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 { ObjectType } from "./enum";
|
||||
import { Prisma } from ".";
|
||||
const PostCreateArgsSchema: ZodType<Prisma.PostCreateArgs> = z.any();
|
||||
export const AuthSchema = {
|
||||
signUpRequest: z.object({
|
||||
username: z.string(),
|
||||
|
@ -390,46 +389,4 @@ export const BaseCursorSchema = z.object({
|
|||
createEndDate: z.date().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,
|
||||
Department,
|
||||
Term,
|
||||
Message,
|
||||
Post,
|
||||
RoleMap,
|
||||
// Section,
|
||||
// Lecture,
|
||||
// Course,
|
||||
Enrollment,
|
||||
} from "@prisma/client";
|
||||
import { SocketMsgType, RolePerms } from "./enum";
|
||||
import { RowRequestSchema } from "./schema";
|
||||
|
|
Loading…
Reference in New Issue