You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于MongoDB、Mongoose和Express.js的后台通知系统实现问询

大规模用户后台通知系统实现方案咨询

我正在开发一个拥有10万+用户的后台管理系统,计划基于MongoDB、Mongoose和Express.js实现通知功能。需求是支持从后台向指定用户或所有用户发送统一内容的通知,同时希望避免为每个用户创建单独的通知对象,寻求高效的实现方案。

核心需求如下:

  • 支持向指定用户或所有用户发送统一内容的通知;
  • 尽量减少为每个用户创建单独的通知对象;
  • 通知系统需适配10万+用户的大规模场景。

我已有一个MongoDB通知Schema,但需要针对通知逻辑处理、大用户量下的管理方式获取指导,包括Schema结构优化、Express.js路由设计及通知逻辑实现的建议。

当前的Schema代码如下:

// My current schema looks like this:

const mongoose = require('mongoose');

const notificationSchema = new mongoose.Schema({
  text: { type: String, required: true },
  users: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }],
  // Other relevant fields
});

const Notification = mongoose.model('Notification', notificationSchema);

一、Schema结构优化

你的当前Schema在发送全局通知时会遇到瓶颈——把10万+用户ID存入数组不仅占用大量存储,写入性能也会急剧下降。建议调整为区分全局/定向通知的结构,从根源上避免冗余数据:

const mongoose = require('mongoose');

const notificationSchema = new mongoose.Schema({
  text: { type: String, required: true },
  // 明确通知类型,避免模糊查询
  type: { type: String, enum: ['global', 'targeted'], required: true },
  // 仅定向通知时填充,存储目标用户ID集合
  targetUsers: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }],
  // 核心元数据,配合索引提升查询效率
  createdAt: { type: Date, default: Date.now, index: true },
  expiresAt: { type: Date, index: true },
  isActive: { type: Boolean, default: true }
});

// 创建复合索引,覆盖高频查询场景
notificationSchema.index({ type: 1, isActive: 1, createdAt: -1 });
notificationSchema.index({ type: 1, targetUsers: 1, isActive: 1 });
// TTL索引自动清理过期通知
notificationSchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 });

const Notification = mongoose.model('Notification', notificationSchema);

优化优势:

  • 全局通知无需存储任何用户ID,单条记录覆盖所有用户,存储成本趋近于0;
  • 定向通知仅存储目标用户ID,小范围用户场景下写入/查询效率更高;
  • 复合索引直接命中用户端查询条件,避免全表扫描。

二、Express.js路由设计

拆分后台管理路由和用户端查询路由,严格区分权限边界:

后台管理路由(需管理员权限校验)

const express = require('express');
const router = express.Router();
const Notification = require('../models/Notification');

// 发送全局通知
router.post('/notifications/global', async (req, res) => {
  try {
    const { text, expiresAt } = req.body;
    const notification = new Notification({
      text,
      type: 'global',
      expiresAt: expiresAt ? new Date(expiresAt) : undefined
    });
    await notification.save();
    res.status(201).json({ message: '全局通知发送成功', notification });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// 发送定向通知
router.post('/notifications/targeted', async (req, res) => {
  try {
    const { text, targetUsers, expiresAt } = req.body;
    // 可添加用户ID合法性校验(如查询User集合确认存在)
    const notification = new Notification({
      text,
      type: 'targeted',
      targetUsers,
      expiresAt: expiresAt ? new Date(expiresAt) : undefined
    });
    await notification.save();
    res.status(201).json({ message: '定向通知发送成功', notification });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// 更新通知状态(启用/禁用)
router.patch('/notifications/:id', async (req, res) => {
  try {
    const { isActive } = req.body;
    const notification = await Notification.findByIdAndUpdate(
      req.params.id,
      { isActive },
      { new: true }
    );
    if (!notification) return res.status(404).json({ error: '通知不存在' });
    res.json({ message: '通知状态更新成功', notification });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

module.exports = router;

用户端查询路由

const express = require('express');
const router = express.Router();
const Notification = require('../models/Notification');

// 获取当前用户有效通知
router.get('/notifications', async (req, res) => {
  try {
    const userId = req.user._id; // 假设已通过认证中间件获取用户ID
    const notifications = await Notification.find({
      isActive: true,
      $or: [
        { type: 'global' },
        { type: 'targeted', targetUsers: userId }
      ],
      // 可选:过滤过期通知,前端可通过参数控制是否显示
      ...(req.query.showExpired !== 'true' && { expiresAt: { $gt: new Date() } })
    }).sort({ createdAt: -1 });

    res.json(notifications);
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

module.exports = router;

三、核心逻辑实现要点

  1. 全局通知查询优化:用户查询时直接匹配type: 'global'和isActive: true,无需关联用户ID,查询速度不受用户量影响。
  2. 定向通知批量处理:如果定向通知覆盖用户量超过1万,建议用消息队列(如BullMQ)异步写入,避免HTTP请求超时:
    const Queue = require('bullmq');
    const notificationQueue = new Queue('notificationQueue');
    
    // 路由中提交任务
    router.post('/notifications/targeted', async (req, res) => {
      try {
        await notificationQueue.add('sendTargeted', req.body);
        res.status(202).json({ message: '通知发送任务已提交' });
      } catch (err) {
        res.status(500).json({ error: err.message });
      }
    });
    
    // 队列处理器
    notificationQueue.process('sendTargeted', async (job) => {
      const { text, targetUsers, expiresAt } = job.data;
      const notification = new Notification({
        text,
        type: 'targeted',
        targetUsers,
        expiresAt: expiresAt ? new Date(expiresAt) : undefined
      });
      await notification.save();
    });
    
  3. 已读状态独立存储:不要在主通知对象中修改已读状态,单独创建UserNotification集合存储用户-通知的已读映射:
    const userNotificationSchema = new mongoose.Schema({
      userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
      notificationId: { type: mongoose.Schema.Types.ObjectId, ref: 'Notification', required: true },
      readAt: { type: Date }
    });
    
    userNotificationSchema.index({ userId: 1, notificationId: 1 }, { unique: true });
    
    这种方式既不污染主通知数据,也能高效统计用户已读情况。

内容的提问来源于stack exchange,提问作者Nightcrawler

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.03 07:11:05