本文记录了在校园论坛中实现消息通知功能的完整过程------从数据模型设计到前后端实现,以及踩过的一些坑。
前言
我的校园论坛已经有了帖子发布、评论互动、嵌套回复、评论点赞等功能。但一直缺少一个关键环节:用户之间互动了,对方却不知道。
有人回复了你的帖子,你不知道。有人赞了你的评论,你也不知道。这导致社区感很弱------用户发了帖子就再也不会回来看了。
消息通知就是解决这个问题的最后一块拼图。做完之后,用户之间的互动就形成了闭环。
一、设计思路:事件驱动,而非用户主动发送
我最开始把消息通知想复杂了------以为需要做一个"私信"系统,用户主动给对方发消息。后来想通了:通知不是用户"写"出来的,而是由用户的互动行为自动产生的副产品。
具体来说:
- 有人评论了你的帖子 → 系统自动通知你
- 有人回复了你的评论 → 系统自动通知你
- 有人赞了你的帖子 → 系统自动通知你
- 有人赞了你的评论 → 系统自动通知你
这个设计思路的核心是:不修改核心业务逻辑,而是在现有的动作上附加一个副作用。 这和之前做过的"统一在出口处做匿名处理"是同一个思维模式。
二、数据模型:一张表记录所有通知
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.vue、Feedback 一样------onMounted 时调接口,拿到数据后用 v-for 渲染列表。
每个通知条目包含:
- 左侧图标(回复类用 💬 图标,点赞类用 ❤️ 图标)
- 发送者名字 + 通知类型描述
- 右侧时间(刚刚、X分钟前、X小时前、昨天、日期)
- 未读的通知左侧有蓝色高亮边框 + 小圆点
点击通知 → 调标记已读接口 → 跳转到对应帖子详情页。
已读消息的本地状态更新
点击通知标记已读时,先直接修改本地数据 notification.isRead = true,让页面立即更新,同时异步调接口标记已读。这样用户不会看到延迟,体验更好。
五、定时清理已读通知
已读通知会一直保留在列表里,时间长了会占数据库空间。我用 setInterval 在 index.js 启动时设置了一个定时任务,每小时自动清理所有已读通知:
javascript
setInterval(async () => {
const result = await Notification.deleteMany({ isRead: true })
if (result.deletedCount > 0) {
console.log(`已自动清理 ${result.deletedCount} 条已读通知`)
}
}, 60 * 60 * 1000)
启动时也立即执行一次,清理掉上次运行期间残留的已读通知。
六、踩坑记录
坑一:查询条件字段名写错了
通知模型里存的是 receiver,但查询时写成了 user。user 字段在模型里根本不存在,所以查询永远返回空数组,未读数永远是 0。数据库里明明有数据,但前端就是拿不到。
排查经验:没有报错却看不到数据,先确认前后端字段名是否一致。这种问题不会报错,因为 Mongoose 查询不存在的字段时,不会抛异常,只是找不到匹配的记录。
坑二:标记已读后页面不更新
点击通知后调了标记已读接口,但页面上的蓝色边框和小圆点没有消失。原因是标记已读后没有重新拉取通知列表,组件状态没有更新。
解决方案 :标记已读后,先直接修改本地数据 notification.isRead = true,再异步调接口。这样页面立即更新,不需要等接口返回。
七、总结
消息通知功能让我重新理解了"通知"的本质------它不是用户主动发送的消息,而是由用户的互动行为自动产生的副产品。从技术实现上,就是在现有接口里"顺便"多写一行代码。
这个功能做完之后,论坛的核心功能就全部闭环了。用户能发帖、能评论、能点赞、能收到通知------完整的社区体验。
项目状态更新:
- 已完成全部核心功能:帖子发布、评论互动、嵌套回复、评论点赞、分区浏览、树洞匿名、首页推荐、个人主页、管理员审核、白名单注册、敏感词过滤、网络安全加固、头像上传、帖子上传图片、消息通知
如果你也在独立做全栈项目,欢迎评论区交流你的踩坑经历。