上节做了用户行为的消息提醒,只用了一张表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&¶ms.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表数据:
任务触发,定时器任务执行:
用户系统通知表,增加两条数据,分别通知两个用户:
后台核心业务基本就这样,明天做后台管理系统,先把菜单和鉴权这部分内容完成了。