評論與討論
規則
- 串接式留言:
parent_id 外鍵欄位位於 comments 表中 — null 表示頂層留言,引用 parent 則表示回覆
- 樂觀更新 UI:立即將留言加入 UI,等待伺服器回應確認,若發生錯誤則回滾
- @提及:在留言內容中解析
@username — 輸入 @ 時觸發使用者查詢下拉選單,並通知被提及的使用者
- 表情反應:獨立的
reactions 表(commentId, userId, emoji)— 在 (commentId, userId, emoji) 上設定唯一約束
- 審核機制:
status 欄位(published, flagged, removed)— 為被標記的內容設立審核佇列,並可透過關鍵字清單自動標記
- 軟刪除:設定
deletedAt 時間戳記 — 對於擁有回覆的留言顯示「已刪除」佔位符,完全隱藏沒有回覆的已刪除留言
- 游標分頁:
cursor(commentId)+ limit — 依 createdAt 排序,滾動時載入更早的評論
- 編輯歷史:將編輯記錄儲存在獨立的資料表或 JSON 陣列中 — 顯示「已編輯」標記以及最後編輯時間戳
模式
const comments = await db.query.comments.findMany({
where: and(eq(comments.postId, postId), isNull(comments.parentId)),
with: {
author: { columns: { id: true, name: true, avatar: true } },
replies: {
with: { author: { columns: { id: true, name: true, avatar: true } } },
orderBy: [asc(comments.createdAt)],
},
reactions: true,
},
orderBy: [desc(comments.createdAt)],
limit: 20,
});
const addComment = useMutation({
mutationFn: (body: string) => api.post(`/posts/${postId}/comments`, { body }),
onMutate: async (body) => {
await queryClient.cancelQueries({ queryKey: ["comments", postId] });
const previous = queryClient.getQueryData(["comments", postId]);
queryClient.setQueryData(["comments", postId], (old: Comment[]) => [
{ id: `temp-${Date.now()}`, body, author: currentUser, createdAt: new Date() },
...old,
]);
return { previous };
},
onError: (_err, _body, context) => {
queryClient.setQueryData(["comments", postId], context?.previous);
},
onSettled: () => queryClient.invalidateQueries({ queryKey: ["comments", postId] }),
});
應避免
- 巢狀深度無限制 — 最多 2-3 層,將更深層的回覆扁平化處理
- 一次載入所有留言 — 使用游標分頁,特別是在活躍的討論串中
- 僅在客戶端解析提及 — 應在伺服器端驗證並擷取提及內容,以發送通知
- 硬刪除擁有回覆的留言 — 這會導致討論串孤立;應使用軟刪除並顯示佔位符
- 反應沒有唯一約束 — 允許同一使用者對同一則留言重複按讚