事件驱动设计:我如何为校园论坛实现消息通知功能

本文记录了在校园论坛中实现消息通知功能的完整过程------从数据模型设计到前后端实现,以及踩过的一些坑。

前言

我的校园论坛已经有了帖子发布、评论互动、嵌套回复、评论点赞等功能。但一直缺少一个关键环节:用户之间互动了,对方却不知道。

有人回复了你的帖子,你不知道。有人赞了你的评论,你也不知道。这导致社区感很弱------用户发了帖子就再也不会回来看了。

消息通知就是解决这个问题的最后一块拼图。做完之后,用户之间的互动就形成了闭环。

一、设计思路:事件驱动,而非用户主动发送

我最开始把消息通知想复杂了------以为需要做一个"私信"系统,用户主动给对方发消息。后来想通了:通知不是用户"写"出来的,而是由用户的互动行为自动产生的副产品。

具体来说:

  • 有人评论了你的帖子 → 系统自动通知你
  • 有人回复了你的评论 → 系统自动通知你
  • 有人赞了你的帖子 → 系统自动通知你
  • 有人赞了你的评论 → 系统自动通知你

这个设计思路的核心是:不修改核心业务逻辑,而是在现有的动作上附加一个副作用。 这和之前做过的"统一在出口处做匿名处理"是同一个思维模式。

二、数据模型:一张表记录所有通知

javascript 复制代码
const notificationSchema = new mongoose.Schema({
  type: {
    type: String,
    enum: ['comment', 'reply', 'like_post', 'like_comment'],
    required: true
  },
  sender: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
  receiver: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
  postId: { type: mongoose.Schema.Types.ObjectId, ref: 'Post', required: true },
  commentId: { type: mongoose.Schema.Types.ObjectId, default: null },
  isRead: { type: Boolean, default: false }
}, { timestamps: true })

四个字段决定了每条通知的完整信息:

  • type:通知类型,四种枚举值
  • sender:谁触发的通知
  • receiver:谁收到通知
  • postId:关联的帖子,点击通知后跳转用
  • commentId:关联的评论(可选)
  • isRead:是否已读

三、后端实现:在现有接口里"顺便"写入通知

这是整个功能最核心的设计决策。我没有新建专门的通知发送接口,而是在现有的评论和点赞接口里,执行完核心操作后多写一行代码。

改造评论接口:

javascript 复制代码
// 评论帖子 → 通知帖子作者
if (post.author.toString() !== req.user._id) {
  await Notification.create({
    type: 'comment', sender: req.user._id,
    receiver: post.author, postId: postId
  })
}

// 回复评论 → 通知被回复者
if (replyTo && 被回复的评论作者 !== 自己) {
  await Notification.create({
    type: 'reply', sender: req.user._id,
    receiver: 被回复的评论作者, postId: postId, commentId: replyToCommentId
  })
}

改造点赞接口(帖子点赞和评论点赞同理):

javascript 复制代码
if (被点赞的对象的作者 !== 点赞者) {
  await Notification.create({
    type: 'like_post', sender: req.user._id,
    receiver: post.author, postId: id
  })
}

关键点:自己不能给自己发通知。 每次写入前都判断 sender !== receiver

四、前端实现:轮询红点 + 通知列表

导航栏红点

App.vue 里,头像旁边加了一个铃铛图标,用 setInterval 每 30 秒轮询一次未读通知数量:

javascript 复制代码
const unreadCount = ref(0)

async function fetchUnreadCount() {
  const res = await fetch('/api/notifications/unread-count', {
    headers: { Authorization: `Bearer ${localStorage.getItem('forum-token')}` }
  })
  if (res.ok) {
    const data = await res.json()
    unreadCount.value = data.count
  }
}

setInterval(fetchUnreadCount, 30000)

红点用 v-if="unreadCount > 0" 控制显示,数字超过 99 显示 "99+"。

通知列表页面

新建了 Notifications.vue,整体模式和我之前写的 Admin.vueFeedback 一样------onMounted 时调接口,拿到数据后用 v-for 渲染列表。

每个通知条目包含:

  • 左侧图标(回复类用 💬 图标,点赞类用 ❤️ 图标)
  • 发送者名字 + 通知类型描述
  • 右侧时间(刚刚、X分钟前、X小时前、昨天、日期)
  • 未读的通知左侧有蓝色高亮边框 + 小圆点

点击通知 → 调标记已读接口 → 跳转到对应帖子详情页。

已读消息的本地状态更新

点击通知标记已读时,先直接修改本地数据 notification.isRead = true,让页面立即更新,同时异步调接口标记已读。这样用户不会看到延迟,体验更好。

五、定时清理已读通知

已读通知会一直保留在列表里,时间长了会占数据库空间。我用 setIntervalindex.js 启动时设置了一个定时任务,每小时自动清理所有已读通知:

javascript 复制代码
setInterval(async () => {
  const result = await Notification.deleteMany({ isRead: true })
  if (result.deletedCount > 0) {
    console.log(`已自动清理 ${result.deletedCount} 条已读通知`)
  }
}, 60 * 60 * 1000)

启动时也立即执行一次,清理掉上次运行期间残留的已读通知。

六、踩坑记录

坑一:查询条件字段名写错了

通知模型里存的是 receiver,但查询时写成了 useruser 字段在模型里根本不存在,所以查询永远返回空数组,未读数永远是 0。数据库里明明有数据,但前端就是拿不到。

排查经验:没有报错却看不到数据,先确认前后端字段名是否一致。这种问题不会报错,因为 Mongoose 查询不存在的字段时,不会抛异常,只是找不到匹配的记录。

坑二:标记已读后页面不更新

点击通知后调了标记已读接口,但页面上的蓝色边框和小圆点没有消失。原因是标记已读后没有重新拉取通知列表,组件状态没有更新。

解决方案 :标记已读后,先直接修改本地数据 notification.isRead = true,再异步调接口。这样页面立即更新,不需要等接口返回。

七、总结

消息通知功能让我重新理解了"通知"的本质------它不是用户主动发送的消息,而是由用户的互动行为自动产生的副产品。从技术实现上,就是在现有接口里"顺便"多写一行代码。

这个功能做完之后,论坛的核心功能就全部闭环了。用户能发帖、能评论、能点赞、能收到通知------完整的社区体验。


项目状态更新:

  • 已完成全部核心功能:帖子发布、评论互动、嵌套回复、评论点赞、分区浏览、树洞匿名、首页推荐、个人主页、管理员审核、白名单注册、敏感词过滤、网络安全加固、头像上传、帖子上传图片、消息通知

如果你也在独立做全栈项目,欢迎评论区交流你的踩坑经历。

相关推荐
代码煮茶7 小时前
Vue3 Mock 数据实战 | 用 Mockjs + vite-plugin-mock 搭建前端独立开发环境
javascript·vue.js
yingyima7 小时前
GitHub Actions 定时任务 schedule 踩坑实录:核心语法与实战技巧
前端
代码煮茶7 小时前
CSS 单位完全指南:px、em、rem、vw、vh、clamp 详解
前端·css
KaMeidebaby7 小时前
卡梅德生物技术快报|PROTAC 药物降解蛋白原理及数据库平台开发全流程
前端·数据库·其他·百度·新浪微博
玄米乌龙茶1237 小时前
LLM成长笔记(七): AI 应用框架与编排
前端·人工智能·笔记
芯芯点灯8 小时前
gd32f303烧录提示Flash Timeout. Reset the Target and try it again.;
开发语言·前端·javascript
前端若水8 小时前
自定义消息组件:图片、文件附件与图表
前端·人工智能·react.js·typescript
2601_958492558 小时前
7 Best WordPress Tools to Help Your News Site Actually Make Money
前端·word
放下华子我只抽RuiKe58 小时前
React 从入门到生产(七):性能优化实战
前端·javascript·人工智能·react.js·性能优化·前端框架·github