From a5205884fe9aa888d4c2da8d01295f805eb0786d Mon Sep 17 00:00:00 2001 From: ditiqi Date: Fri, 24 Jan 2025 15:06:57 +0800 Subject: [PATCH] add 2025-0124- --- apps/server/src/models/post/post.service.ts | 14 ++++- apps/server/src/models/post/utils.ts | 61 ++++++++++++++++++- apps/server/src/models/visit/visit.service.ts | 34 ++++++----- .../queue/models/post/post.queue.service.ts | 16 ++++- apps/server/src/queue/models/post/utils.ts | 21 +------ apps/server/src/queue/types.ts | 4 ++ apps/server/src/queue/worker/processor.ts | 3 + apps/server/src/utils/event-bus.ts | 3 + .../context/LetterEditorContext.tsx | 3 +- .../models/post/detail/PostCommentCard.tsx | 7 +++ .../models/post/detail/PostCommentList.tsx | 27 +++++++- .../models/post/detail/PostHeader.tsx | 2 +- .../post/detail/context/PostDetailContext.tsx | 32 +++++++--- packages/client/src/api/hooks/useVisitor.ts | 24 +++++--- packages/client/src/singleton/DataHolder.ts | 42 +++++++++++-- packages/common/prisma/schema.prisma | 1 + packages/common/src/enum.ts | 6 ++ 17 files changed, 236 insertions(+), 64 deletions(-) diff --git a/apps/server/src/models/post/post.service.ts b/apps/server/src/models/post/post.service.ts index b3df446..ba06d94 100755 --- a/apps/server/src/models/post/post.service.ts +++ b/apps/server/src/models/post/post.service.ts @@ -7,11 +7,12 @@ import { RolePerms, ResPerm, ObjectType, + PostType, } from '@nice/common'; import { MessageService } from '../message/message.service'; import { BaseService } from '../base/base.service'; import { DepartmentService } from '../department/department.service'; -import { setPostRelation } from './utils'; +import { setPostRelation, updatePostState } from './utils'; import EventBus, { CrudOperation } from '@server/utils/event-bus'; @Injectable() @@ -22,6 +23,12 @@ export class PostService extends BaseService { ) { super(db, ObjectType.POST); } + onModuleInit() { + EventBus.on('updatePostState', ({ id }) => { + console.log('updatePostState'); + updatePostState(id); + }); + } async create( args: Prisma.PostCreateArgs, params: { staff?: UserProfile; tx?: Prisma.PostDelegate }, @@ -36,6 +43,11 @@ export class PostService extends BaseService { operation: CrudOperation.CREATED, data: result, }); + if (args.data.authorId && args?.data?.type === PostType.POST_COMMENT) { + EventBus.emit('updatePostState', { + id: result?.id, + }); + } return result; } async update(args: Prisma.PostUpdateArgs, staff?: UserProfile) { diff --git a/apps/server/src/models/post/utils.ts b/apps/server/src/models/post/utils.ts index a7defc3..22dcc30 100644 --- a/apps/server/src/models/post/utils.ts +++ b/apps/server/src/models/post/utils.ts @@ -1,4 +1,11 @@ -import { db, Post, PostType, UserProfile, VisitType } from '@nice/common'; +import { + db, + Post, + PostState, + PostType, + UserProfile, + VisitType, +} from '@nice/common'; export async function setPostRelation(params: { data: Post; @@ -71,3 +78,55 @@ export function getClientIp(req: any): string { return ip || ''; } +export async function updatePostState(id: string) { + const post = await db.post.findUnique({ + where: { + id: id, + }, + select: { + id: true, + state: true, + receivers: { + select: { + id: true, + }, + }, + }, + }); + if (post?.state === PostState.COMPLETED) { + return; + } + const postReceiverIds = post.receivers + .map((receiver) => receiver.id) + .filter(Boolean); + const receiverViews = await db.visit.count({ + where: { + postId: id, + type: VisitType.READED, + visitorId: { + in: postReceiverIds, + }, + }, + }); + if (receiverViews > 0 && post.state === PostState.PENDING) { + await db.post.update({ + where: { id }, + data: { state: PostState.PROCESSING }, + }); + } + const receiverComments = await db.post.count({ + where: { + parentId: id, + type: PostType.POST_COMMENT, + authorId: { + in: postReceiverIds, + }, + }, + }); + if (receiverComments > 0) { + await db.post.update({ + where: { id }, + data: { state: PostState.COMPLETED }, + }); + } +} diff --git a/apps/server/src/models/visit/visit.service.ts b/apps/server/src/models/visit/visit.service.ts index 20d0a30..f03c852 100644 --- a/apps/server/src/models/visit/visit.service.ts +++ b/apps/server/src/models/visit/visit.service.ts @@ -58,20 +58,26 @@ export class VisitService extends BaseService { } } } - - if (postId && args.data.type === VisitType.READED) { - EventBus.emit('updateVisitCount', { - objectType: ObjectType.POST, - id: postId, - visitType: VisitType.READED, - }); - } - if (postId && args.data.type === VisitType.LIKE) { - EventBus.emit('updateVisitCount', { - objectType: ObjectType.POST, - id: postId, - visitType: VisitType.LIKE, - }); + if (postId) { + if (visitorId) { + EventBus.emit('updatePostState', { + id: postId, + }); + } + if (args.data.type === VisitType.READED) { + EventBus.emit('updateVisitCount', { + objectType: ObjectType.POST, + id: postId, + visitType: VisitType.READED, + }); + } + if (args.data.type === VisitType.LIKE) { + EventBus.emit('updateVisitCount', { + objectType: ObjectType.POST, + id: postId, + visitType: VisitType.LIKE, + }); + } } return result; } diff --git a/apps/server/src/queue/models/post/post.queue.service.ts b/apps/server/src/queue/models/post/post.queue.service.ts index c11c6f1..ebce878 100644 --- a/apps/server/src/queue/models/post/post.queue.service.ts +++ b/apps/server/src/queue/models/post/post.queue.service.ts @@ -3,7 +3,11 @@ import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; import { Queue } from 'bullmq'; import EventBus from '@server/utils/event-bus'; import { ObjectType, VisitType } from '@nice/common'; -import { QueueJobType, updateVisitCountJobData } from '@server/queue/types'; +import { + QueueJobType, + updatePostStateJobData, + updateVisitCountJobData, +} from '@server/queue/types'; @Injectable() export class PostQueueService implements OnModuleInit { @@ -16,6 +20,10 @@ export class PostQueueService implements OnModuleInit { this.addUpdateVisitCountJob({ id, type: visitType }); } }); + EventBus.on('updatePostState', ({ id }) => { + console.log('updatePostState'); + this.addUpdatePostState({ id }); + }); } async addUpdateVisitCountJob(data: updateVisitCountJobData) { this.logger.log(`update post view count ${data.id}`); @@ -23,4 +31,10 @@ export class PostQueueService implements OnModuleInit { debounce: { id: data.id }, }); } + async addUpdatePostState(data: updatePostStateJobData) { + this.logger.log(`update post state ${data.id}`); + await this.generalQueue.add(QueueJobType.UPDATE_POST_STATE, data, { + debounce: { id: data.id }, + }); + } } diff --git a/apps/server/src/queue/models/post/utils.ts b/apps/server/src/queue/models/post/utils.ts index 7b91888..06da647 100644 --- a/apps/server/src/queue/models/post/utils.ts +++ b/apps/server/src/queue/models/post/utils.ts @@ -1,4 +1,4 @@ -import { db, VisitType } from '@nice/common'; +import { db, PostState, PostType, VisitType } from '@nice/common'; export async function updatePostViewCount(id: string, type: VisitType) { const totalViews = await db.visit.aggregate({ _sum: { @@ -29,22 +29,3 @@ export async function updatePostViewCount(id: string, type: VisitType) { }); } } -export async function updatePostLikeCount(id: string) { - const totalViews = await db.visit.aggregate({ - _sum: { - views: true, - }, - where: { - postId: id, - type: VisitType.LIKE, - }, - }); - await db.post.update({ - where: { - id, - }, - data: { - views: totalViews._sum.views || 0, // Use 0 if no visits exist - }, - }); -} diff --git a/apps/server/src/queue/types.ts b/apps/server/src/queue/types.ts index 8cb367c..d986daa 100755 --- a/apps/server/src/queue/types.ts +++ b/apps/server/src/queue/types.ts @@ -3,8 +3,12 @@ export enum QueueJobType { UPDATE_STATS = 'update_stats', FILE_PROCESS = 'file_process', UPDATE_POST_VISIT_COUNT = 'updatePostVisitCount', + UPDATE_POST_STATE = 'updatePostState', } export type updateVisitCountJobData = { id: string; type: VisitType; }; +export type updatePostStateJobData = { + id: string; +}; diff --git a/apps/server/src/queue/worker/processor.ts b/apps/server/src/queue/worker/processor.ts index a01d859..5f3ec6f 100755 --- a/apps/server/src/queue/worker/processor.ts +++ b/apps/server/src/queue/worker/processor.ts @@ -41,6 +41,9 @@ export default async function processJob(job: Job) { 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); + } } catch (error: any) { logger.error( `Error processing stats update job: ${error.message}`, diff --git a/apps/server/src/utils/event-bus.ts b/apps/server/src/utils/event-bus.ts index e5a98f2..26c02d4 100644 --- a/apps/server/src/utils/event-bus.ts +++ b/apps/server/src/utils/event-bus.ts @@ -18,6 +18,9 @@ type Events = { objectType: ObjectType; visitType: VisitType; }; + updatePostState: { + id: string; + }; onMessageCreated: { data: Partial }; dataChanged: { type: string; operation: CrudOperation; data: any }; }; diff --git a/apps/web/src/components/models/post/LetterEditor/context/LetterEditorContext.tsx b/apps/web/src/components/models/post/LetterEditor/context/LetterEditorContext.tsx index b02520a..3f489d5 100644 --- a/apps/web/src/components/models/post/LetterEditor/context/LetterEditorContext.tsx +++ b/apps/web/src/components/models/post/LetterEditor/context/LetterEditorContext.tsx @@ -3,7 +3,7 @@ import { Form, FormInstance } from "antd"; import { api, usePost } from "@nice/client"; import toast from "react-hot-toast"; import { useNavigate } from "react-router-dom"; -import { PostType } from "@nice/common"; +import { PostState, PostType } from "@nice/common"; export interface LetterFormData { title: string; @@ -52,6 +52,7 @@ export function LetterFormProvider({ id, })), }, + state: PostState.PENDING, isPublic: data?.isPublic, ...data, resources: data.resources?.length diff --git a/apps/web/src/components/models/post/detail/PostCommentCard.tsx b/apps/web/src/components/models/post/detail/PostCommentCard.tsx index 3aabaa4..eb3b644 100644 --- a/apps/web/src/components/models/post/detail/PostCommentCard.tsx +++ b/apps/web/src/components/models/post/detail/PostCommentCard.tsx @@ -11,9 +11,11 @@ import { PostDetailContext } from "./context/PostDetailContext"; export default function PostCommentCard({ post, index, + isReceiverComment }: { post: PostDto; index: number; + isReceiverComment: boolean; }) { const { user } = useContext(PostDetailContext); const { like } = useVisitor(); @@ -50,6 +52,11 @@ export default function PostCommentCard({
+ {isReceiverComment && ( + + 反馈解答 + + )} {post.author?.showname || "匿名用户"} diff --git a/apps/web/src/components/models/post/detail/PostCommentList.tsx b/apps/web/src/components/models/post/detail/PostCommentList.tsx index 0ef0102..1da75b8 100644 --- a/apps/web/src/components/models/post/detail/PostCommentList.tsx +++ b/apps/web/src/components/models/post/detail/PostCommentList.tsx @@ -13,15 +13,30 @@ import PostCommentCard from "./PostCommentCard"; import { useInView } from "react-intersection-observer"; import { LoadingCard } from "@web/src/components/models/post/detail/LoadingCard"; -export default function PostCommentList() { +export default function PostCommentList({ + official = true, +}: { + official?: boolean; +}) { const { post } = useContext(PostDetailContext); const { ref: loadMoreRef, inView } = useInView(); const { postParams } = useVisitor(); + const receiverIds = useMemo(() => { + return ( + post?.receivers?.map((receiver) => receiver.id).filter(Boolean) || + [] + ); + }, [post]); const params: Prisma.PostFindManyArgs = useMemo(() => { return { where: { parentId: post?.id, type: PostType.POST_COMMENT, + authorId: official + ? { in: receiverIds } + : { + notIn: receiverIds, + }, }, select: postDetailSelect, orderBy: [ @@ -91,7 +106,13 @@ export default function PostCommentList() { duration: 0.2, delay: index * 0.05, }}> - + ))} @@ -112,7 +133,7 @@ export default function PostCommentList() { className="flex flex-col items-center py-4 space-y-2">
- 已加载全部评论 + 已加载全部回复 - {post?.commentsCount || 0} 评论 + {post?.commentsCount || 0} 回复 diff --git a/apps/web/src/components/models/post/detail/context/PostDetailContext.tsx b/apps/web/src/components/models/post/detail/context/PostDetailContext.tsx index 87c944b..d924a98 100644 --- a/apps/web/src/components/models/post/detail/context/PostDetailContext.tsx +++ b/apps/web/src/components/models/post/detail/context/PostDetailContext.tsx @@ -1,17 +1,18 @@ import { api, usePost } from "@nice/client"; import { Post, postDetailSelect, PostDto, UserProfile } from "@nice/common"; import { useAuth } from "@web/src/providers/auth-provider"; -import React, { createContext, ReactNode, useState } from "react"; +import React, { createContext, ReactNode, useEffect, useState } from "react"; +import { PostParams } from "@nice/client/src/singleton/DataHolder"; interface PostDetailContextType { - editId?: string; // 添加 editId + editId?: string; post?: PostDto; isLoading?: boolean; user?: UserProfile; } interface PostFormProviderProps { children: ReactNode; - editId?: string; // 添加 editId 参数 + editId?: string; } export const PostDetailContext = createContext( null @@ -21,15 +22,26 @@ export function PostDetailProvider({ editId, }: PostFormProviderProps) { const { user } = useAuth(); + const postParams = PostParams.getInstance(); + const queryParams = { + where: { id: editId }, + select: postDetailSelect, + }; + + useEffect(() => { + if (editId) { + postParams.addDetailItem(queryParams); + } + return () => { + if (editId) { + postParams.removeDetailItem(queryParams); + } + }; + }, [editId]); + const { data: post, isLoading }: { data: PostDto; isLoading: boolean } = ( api.post.findFirst as any - ).useQuery( - { - where: { id: editId }, - select: postDetailSelect, - }, - { enabled: Boolean(editId) } - ); + ).useQuery(queryParams, { enabled: Boolean(editId) }); return ( { const previousDataList: any[] = []; - // 动态生成参数列表,包括星标和其他参数 + const previousDetailDataList: any[] = []; + // 处理列表数据 const paramsList = postParams.getItems(); - console.log("paramsList.length", paramsList.length); - // 遍历所有参数列表,执行乐观更新 for (const params of paramsList) { - // 取消可能的并发请求 await utils.post.findManyWithCursor.cancel(); - // 获取并保存当前数据 const previousData = utils.post.findManyWithCursor.getInfiniteData({ ...params, }); previousDataList.push(previousData); - // 执行乐观更新 utils.post.findManyWithCursor.setInfiniteData( { ...params, @@ -58,7 +54,21 @@ export function useVisitor() { ); } - return { previousDataList }; + // 处理详情数据 + const detailParamsList = postParams.getDetailItems(); + for (const params of detailParamsList) { + await utils.post.findFirst.cancel(); + const previousDetailData = utils.post.findFirst.getData(params); + previousDetailDataList.push(previousDetailData); + utils.post.findFirst.setData(params, (oldData) => { + if (!oldData) return oldData; + return oldData.id === variables?.postId + ? updateFn(oldData, variables) + : oldData; + }); + } + + return { previousDataList, previousDetailDataList }; }, // 错误处理:数据回滚 onError: (_err: any, _variables: any, context: any) => { diff --git a/packages/client/src/singleton/DataHolder.ts b/packages/client/src/singleton/DataHolder.ts index 5184548..f6f3224 100644 --- a/packages/client/src/singleton/DataHolder.ts +++ b/packages/client/src/singleton/DataHolder.ts @@ -1,9 +1,11 @@ export class PostParams { - private static instance: PostParams; // 静态私有变量,用于存储单例实例 - private postParams: Array; // 私有数组属性,用于存储对象 + private static instance: PostParams; + private postParams: Array; + private postDetailParams: Array; private constructor() { - this.postParams = []; // 初始化空数组 + this.postParams = []; + this.postDetailParams = []; } public static getInstance(): PostParams { @@ -14,7 +16,6 @@ export class PostParams { } public addItem(item: object): void { - // 使用更可靠的方式比较查询参数 const isDuplicate = this.postParams.some((existingItem: any) => { if (item && existingItem) { const itemWhere = (item as any).where; @@ -32,8 +33,22 @@ export class PostParams { } } + public addDetailItem(item: object): void { + const isDuplicate = this.postDetailParams.some((existingItem: any) => { + if (item && existingItem) { + const itemWhere = (item as any).where; + const existingWhere = existingItem.where; + return itemWhere?.id === existingWhere?.id; + } + return false; + }); + + if (!isDuplicate) { + this.postDetailParams.push(item); + } + } + public removeItem(item: object): void { - // 使用相同的比较逻辑移除项 this.postParams = this.postParams.filter((existingItem: any) => { if (item && existingItem) { const itemWhere = (item as any).where; @@ -47,7 +62,24 @@ export class PostParams { }); } + public removeDetailItem(item: object): void { + this.postDetailParams = this.postDetailParams.filter( + (existingItem: any) => { + if (item && existingItem) { + const itemWhere = (item as any).where; + const existingWhere = existingItem.where; + return !(itemWhere?.id === existingWhere?.id); + } + return true; + } + ); + } + public getItems(): Array { return [...this.postParams]; } + + public getDetailItems(): Array { + return [...this.postDetailParams]; + } } diff --git a/packages/common/prisma/schema.prisma b/packages/common/prisma/schema.prisma index c5c68e0..ded419e 100644 --- a/packages/common/prisma/schema.prisma +++ b/packages/common/prisma/schema.prisma @@ -190,6 +190,7 @@ model Post { state String? // 状态 : 未读、处理中、已回答 title String? // 帖子标题,可为空 content String? // 帖子内容,可为空 + domainId String? @map("domain_id") term Term? @relation(fields: [termId], references: [id]) termId String? @map("term_id") diff --git a/packages/common/src/enum.ts b/packages/common/src/enum.ts index 0e0815f..0e811a7 100755 --- a/packages/common/src/enum.ts +++ b/packages/common/src/enum.ts @@ -190,3 +190,9 @@ export const LessonTypeLabel = { [LessonType.QUIZ]: "测验", [LessonType.ASSIGNMENT]: "作业", }; + +export enum PostState { + PENDING = "pending", + PROCESSING = "processing", + COMPLETED = "completed", +}