This commit is contained in:
Rao 2025-04-22 15:29:39 +08:00
parent e5f3954e67
commit 1a4b112599
35 changed files with 203 additions and 2032 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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) {
// 错误处理...
}
}
} }

View File

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

View File

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

View File

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

View File

@ -1,5 +0,0 @@
import { useEntity } from "./useEntity";
export function useMessage() {
return useEntity("message");
}

6
packages/client/src/api/hooks/usePost.ts Executable file → Normal file
View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +0,0 @@
import { Message, Staff } from "@prisma/client";
export type MessageDto = Message & {
readed: boolean;
receivers: Staff[];
sender: Staff;
};

View File

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

View File

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

View File

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

View File

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