设计并实现站内消息接收功能——系统的消息提醒

上节做了用户行为的消息提醒,只用了一张表message,而系统的消息提醒要比用户行为的消息提醒要复杂些,需要使用到两张表。 系统消息主要是系统管理员或者后台系统直接发出的通知,库表设计要分成两张表,分别是notice和user_notice。

notice的字段如下:

js 复制代码
const Sequelize = require('sequelize');
module.exports = function(sequelize, DataTypes) {
  return sequelize.define('notice', {
    id: {
      autoIncrement: true,
      type: DataTypes.INTEGER,
      allowNull: false,
      primaryKey: true
    },
    title: {
      type: DataTypes.STRING(255),
      allowNull: true
    },
    content: {
      type: DataTypes.STRING(255),
      allowNull: true
    },
    ism: {
      type: DataTypes.STRING(255),
      allowNull: true,
      comment: "是否指定多个用户,不指定,则全体用户发送"
    },
    ispush: {
      type: DataTypes.STRING(255),
      allowNull: true,
      comment: "是否push过,已经push过的数据不许再次push"
    },
    pushtime: {
      type: DataTypes.DATE,
      allowNull: true
    },
    seuserid: {
      type: DataTypes.INTEGER,
      allowNull: true
    },
    reuserids: {
      type: DataTypes.STRING(255),
      allowNull: true
    },
    createtime: {
      type: DataTypes.DATE,
      allowNull: true
    },
    modifytime: {
      type: DataTypes.DATE,
      allowNull: true
    },
    targettype: {
      type: DataTypes.STRING(255),
      allowNull: true
    },
    targetid: {
      type: DataTypes.INTEGER,
      allowNull: true
    }
  }, {
    sequelize,
    tableName: 'notice',
    timestamps: false,
    indexes: [
      {
        name: "PRIMARY",
        unique: true,
        using: "BTREE",
        fields: [
          { name: "id" },
        ]
      },
    ]
  });
};

解释下核心字段的作用:

title:通知标题

content:通知内容

ism:是否多选,如果不是多选,那么指定通知所有用户。

ispush:是否被拉取,如果已拉取,那么就无需再次拉取。

seuserid:发送者

reuserid:接收者,以字符串格式存储,如果多个用户,则用逗号隔开

pushtime:拉取时间,写入通知表后,后台将开启定时器,pushtime到了之后将自动执行通知下发任务,并修改通知消息为已拉取。

targettype:如果消息携带了主体,那么需要指定实体targettype,这个字段加上很有必要,给管理员非常大的消息功能拓展,比如后台需要向用户推送某篇文章,或者告诉用户某个专栏需要修改等等。

targetid:实体的id

第二张表就是用户的系统通知表,user_notice:

js 复制代码
const Sequelize = require('sequelize');
module.exports = function(sequelize, DataTypes) {
  return sequelize.define('user_notice', {
    id: {
      autoIncrement: true,
      type: DataTypes.INTEGER,
      allowNull: false,
      primaryKey: true
    },
    noticeid: {
      type: DataTypes.INTEGER,
      allowNull: true
    },
    reuserid: {
      type: DataTypes.INTEGER,
      allowNull: true
    },
    createtime: {
      type: DataTypes.DATE,
      allowNull: true
    },
    isread: {
      type: DataTypes.STRING(255),
      allowNull: true
    }
  }, {
    sequelize,
    tableName: 'user_notice',
    timestamps: false,
    indexes: [
      {
        name: "PRIMARY",
        unique: true,
        using: "BTREE",
        fields: [
          { name: "id" },
        ]
      },
    ]
  });
};

解释下核心字段的作用:

noticeid:系统消息id

createtime:创建时间

reuserid:接收者id

两张表的协作的业务流程上,大体是这样的:当管理员发布一条消息后,选择用户并给定时间,将通知插入notice表中,后台将开启定时器,当给定时间触发,即从notice表拉取通知,根据类型不同将通知表插入用户通知表中。

而用户在读取通知时,读取的就是user_notice表中的内容。

这个发布消息通知的功能,我按想应该是运营人员的功能而非管理员的。

从角色上来说,所有关于平台数据的管理,都是运营人员的功能。

而管理员则是管理基础设施,比如菜单、字典表、角色、权限等功能。

但无论运营人员还是管理人员,他们都应该是后台管理系统中出现的角色,只是角色权限不同导致分工不同,本质上是管理员的拆分。

但由于目前还没有做后台管理相关的功能,所以运营人员的接口暂时只能通过模拟。

实现后台系统消息的推送,其实就两步,第一,存储消息通知,并启动定时器触发发布任务。第二,写入用户通知表。

后台逻辑代码如下,首先是后台路由分发:

js 复制代码
router.post('/run/addNotice',[authMiddleware.auth,authMiddleware.runAuth],controller.addNotice);

这里涉及两个中间件,一个是auth,就是验证权限的,另一个runAuth,则是运营人员的权限鉴定,因为后台权限还没有做好,这个目前就是个伪代码(但实际上,在runAuth这里要查询数据库,要获取用户是否有这个权限访问该接口,所有和run相关的,都要走这个鉴权中间件,后面一节就做这个权限验证的业务逻辑)

这里就展示业务逻辑了,页面暂时不显示,等后台管理系统完成后再展示。

controller层:

js 复制代码
  async addNotice(req,res){
    try {
      let params = {
        title:req.body.title,
        content:req.body.content,
        ism:req.body.ism,
        ispush:0,
        pushtime:req.body.pushtime,
        seuserid:req.body.userid,
        reuserids:req.body.reuserids,
        createtime:new Date(),
        modifytime:new Date()
      }
      let {error,value} = Joi.object({
        title:Joi.string().required(),
        content:Joi.string().required(),
        ism:Joi.number().required(),
        reuserids:Joi.array().required(),
        pushtime:Joi.date().required()
      }).unknown().validate(params)
      if(error) return res.send(error)
  
      let result = await runDao.addNotice(value)
      // 写入数据后,还需要开启定时器,用于添加通知
      const date = new Date(params.pushtime); 
      date.setSeconds(date.getSeconds() + 5); // 比pushtime延迟5秒执行
      schedule.scheduleJob(date, () => {
        console.log('任务触发');
        let newParmas = {
          ...params,
          noticeid:result.id
        }
        runDao.pushNotice(newParmas)
      });
      res.send({
        msg:'成功',
        data:result,
        code:10000
      })
    } catch(err){
      console.log(err)
    }
  },

这段代码首先是验证字段传参是否合理,之后进入dao层的addNotice,将这些数据写入数据库,并开启一个定时器,将在pushtime之后五秒执行任务pushNotice,发布通知:

js 复制代码
  async pushNotice(params){
    if(params.ism){
      // 是多选的,执行批量插入
      let newNotice = []
      params.reuserids&&params.reuserids.forEach(item=>{
        newNotice.push({
          noticeid:params.noticeid,
          reuserid:item,
          createtime:new Date(),
          isread:0
        })
      })
      Model.user_notice.bulkCreate(newNotice)
    }else{
      // 非多选,那么即全体用户通知。
    }
  }

以上代码当ism为1,也就是多选情况下,那么就根据用户id进行批量推送,如果不是多选模式,那么就通知全体用户。

如果只是简单通知全体用户,那么取出用户id做机械式的批量添加倒也不是不行,但如果用户量增多,肯定会导致一些问题,解决方案有两种:

1、筛选活跃用户进行推送 也就是说,对于活跃度达到某个值的用户才去推送数据,而没有达到这个值的用户则不做推送,活跃度可以开启定时器(node-cron)每天半夜进行动态计算(使用worker),那么如何去计算活跃度呢?首先设定一个衰减因子,比如一个最简单的指数衰减:

js 复制代码
function decayFactor(time, halfLife) {
  const exponent = -Math.log(2) / halfLife;
  const factor = Math.exp(exponent * time);
  return factor;
}

设置半衰期halfLile,每天晚上十二点进行动态计算,更新用户的活跃度,而用户的每次登录、发布文章、点赞、收藏、浏览等行为动作,都会增加默认点数的活跃度,并且这个度需要设置默认的上限。

2、第二种方案是我在网上检索到的,就是在用户表上增加一个字段,这个消息通知就不进入用户通知表了,而是修改用户主体表的字段,把这个已读未读的状态绑定到用户表上,比如我们发布了id为1,2,3三个全体通知消息,那么写入用户表字段的readall字段,就给每个用户增加字段值"1,2,3"表示用户没有阅读1,2,3这三个通知,用户阅读了某个全体通知,那就把这个id拿走,这样来保存阅读状态,这也不失为一种方案。

当然,这里采用最简单的方式来做,就选择第二种方案,实际上第一种属于拓展,如果网站确实有这方面的业务延伸,需要收集活跃用户信息,活跃度也可以考虑。

因为页面还没有做好,这里就只展示数据业务逻辑好了。

这里在前端发布通知:

js 复制代码
  mounted(){
    addNotice({
      title:'发布通知',
      content:'通知content',
      ism:1,
      reuserids:[1,2],
      pushtime:new Date()

    }).then(res=>{
      console.log(res)
    })
  }

插入成功:

notice表数据:

任务触发,定时器任务执行:

用户系统通知表,增加两条数据,分别通知两个用户:

后台核心业务基本就这样,明天做后台管理系统,先把菜单和鉴权这部分内容完成了。

相关推荐
.生产的驴22 分钟前
SpringBoot 消息队列RabbitMQ 消息确认机制确保消息发送成功和失败 生产者确认
java·javascript·spring boot·后端·rabbitmq·负载均衡·java-rabbitmq
书中自有妍如玉1 小时前
layui时间选择器选择周 日月季度年
前端·javascript·layui
Riesenzahn1 小时前
canvas生成图片有没有跨域问题?如果有如何解决?
前端·javascript
f8979070701 小时前
layui 可以使点击图片放大
前端·javascript·layui
小贵子的博客1 小时前
ElementUI 用span-method实现循环el-table组件的合并行功能
javascript·vue.js·elementui
明似水1 小时前
掌握 Flutter 中的 `Overlay` 和 `OverlayEntry`:弹窗管理的艺术
javascript·flutter
忘不了情1 小时前
左键选择v-html绑定的文本内容,松开鼠标后出现复制弹窗
前端·javascript·html
笃励2 小时前
Angular面试题四
前端·javascript·angular.js
前端专业写bug2 小时前
jspdf踩坑 htmltocanvas
java·前端·javascript
哈哈哈hhhhhh2 小时前
vue 入门一
前端·javascript·vue.js