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 5c6dcd4..3f489d5 100644 --- a/apps/web/src/components/models/post/LetterEditor/context/LetterEditorContext.tsx +++ b/apps/web/src/components/models/post/LetterEditor/context/LetterEditorContext.tsx @@ -1,69 +1,48 @@ -import { createContext, useContext, ReactNode, useEffect } from "react"; -import { useForm, FormProvider, SubmitHandler } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { z } from "zod"; +import { createContext, useContext, ReactNode } from "react"; +import { Form, FormInstance } from "antd"; import { api, usePost } from "@nice/client"; -// import { PostDto, PostLevel, PostStatus } from "@nice/common"; -// import { api, usePost } from "@nice/client"; import toast from "react-hot-toast"; import { useNavigate } from "react-router-dom"; -import { Post, PostType } from "@nice/common"; -// 定义帖子表单验证 Schema +import { PostState, PostType } from "@nice/common"; -const letterSchema = z.object({ - title: z.string().min(1, "标题不能为空"), - content: z.string().min(1, "内容不能为空"), - resources: z.array(z.string()).nullish(), - isPublic: z.boolean().nullish(), - signature: z.string().nullish(), - meta: z - .object({ - tags: z.array(z.string()).default([]), - signature: z.string().nullish(), - }) - .default({ - tags: [], - signature: null, - }), -}); -// 定义课程表单验证 Schema - -export type LetterFormData = z.infer; +export interface LetterFormData { + title: string; + content: string; + resources?: string[]; + isPublic?: boolean; + signature?: string; + meta: { + tags: string[]; + signature?: string; + }; +} interface LetterEditorContextType { - onSubmit: SubmitHandler; + onSubmit: (values: LetterFormData) => Promise; receiverId?: string; termId?: string; - part?: string; - // course?: PostDto; + form: FormInstance; } + interface LetterFormProviderProps { children: ReactNode; receiverId?: string; termId?: string; - part?: string; } + const LetterEditorContext = createContext(null); + export function LetterFormProvider({ children, receiverId, termId, - // editId, }: LetterFormProviderProps) { const { create } = usePost(); const navigate = useNavigate(); - const methods = useForm({ - resolver: zodResolver(letterSchema), - defaultValues: { - resources: [], - meta: { - tags: [], - }, - }, - }); - const onSubmit: SubmitHandler = async ( - data: LetterFormData - ) => { + const [form] = Form.useForm(); + + const onSubmit = async (data: LetterFormData) => { try { + console.log("data", data); const result = await create.mutateAsync({ data: { type: PostType.POST, @@ -73,6 +52,8 @@ export function LetterFormProvider({ id, })), }, + state: PostState.PENDING, + isPublic: data?.isPublic, ...data, resources: data.resources?.length ? { @@ -83,23 +64,28 @@ export function LetterFormProvider({ : undefined, }, }); - navigate(`/course/${result.id}/detail`, { replace: true }); + navigate(`/${result.id}/detail`, { replace: true }); toast.success("发送成功!"); - - methods.reset(data); + form.resetFields(); } catch (error) { console.error("Error submitting form:", error); toast.error("操作失败,请重试!"); } }; + return ( - {children} + + form={form} + initialValues={{ meta: { tags: [] } }}> + {children} + ); } diff --git a/apps/web/src/components/models/post/LetterEditor/form/LetterBasicForm.tsx b/apps/web/src/components/models/post/LetterEditor/form/LetterBasicForm.tsx index 513d686..d6fc1c0 100644 --- a/apps/web/src/components/models/post/LetterEditor/form/LetterBasicForm.tsx +++ b/apps/web/src/components/models/post/LetterEditor/form/LetterBasicForm.tsx @@ -1,29 +1,18 @@ -import { useFormContext } from "react-hook-form"; -import { motion } from "framer-motion"; -import { - LetterFormData, - useLetterEditor, -} from "../context/LetterEditorContext"; -import { FormQuillInput } from "@web/src/components/common/form/FormQuillInput"; +import { Form, Input, Button, Checkbox, Select } from "antd"; +import { useState } from "react"; +import { useLetterEditor } from "../context/LetterEditorContext"; import { api } from "@nice/client"; import { - UserIcon, - FolderIcon, - HashtagIcon, - DocumentTextIcon, -} from "@heroicons/react/24/outline"; + UserOutlined, + FolderOutlined, + TagOutlined, + FileTextOutlined, +} from "@ant-design/icons"; import FileUploader from "@web/src/components/common/uploader/FileUploader"; -import { FormTags } from "@web/src/components/common/form/FormTags"; -import { FormSignature } from "@web/src/components/common/form/FormSignature"; -import { FormCheckbox } from "@web/src/components/common/form/FormCheckbox"; +import QuillEditor from "@web/src/components/common/editor/quill/QuillEditor"; export function LetterBasicForm() { - const { - handleSubmit, - getValues, - formState: { errors }, - } = useFormContext(); - const { onSubmit, receiverId, termId } = useLetterEditor(); + const { onSubmit, receiverId, termId, form } = useLetterEditor(); const { data: receiver } = api.staff.findFirst.useQuery( { where: { @@ -41,134 +30,137 @@ export function LetterBasicForm() { { enabled: !!termId } ); - const formControls = { - hidden: { opacity: 0, y: 20 }, - visible: (i: number) => ({ - opacity: 1, - y: 0, - transition: { - delay: i * 0.2, - duration: 0.5, - }, - }), + const handleFinish = async (values: any) => { + await onSubmit(values); }; return ( - - {/* 收件人和板块信息行 */} -
- - -
收件人:{receiver?.showname}
-
+
+
+ {/* 收件人和板块信息行 */} +
+
+ +
收件人:{receiver?.showname}
+
- - -
板块:{term?.name}
-
-
- {/* 主题输入框 */} - - - {/* */} - - {/* 标签输入 */} - - - - - {/* 内容输入框 */} - - - - - - - - + +
板块:{term?.name}
+
+
+ + {/* 主题输入框 */} +
+ + + 标题 + (必选) + + } + name="title" + rules={[{ required: true, message: "请输入标题" }]} + labelCol={{ span: 24 }} + wrapperCol={{ span: 24 }}> + + +
+ + {/* 标签输入 */} +
+ + + 标签 + + } + name={["meta", "tags"]} + labelCol={{ span: 24 }} + wrapperCol={{ span: 24 }}> +