前言:
在我们的软件开发中,系统通知和推送是必不可少的一部分,本篇文章将针对Android端和服务端分别讲解如何完整的实现一个系统通知与推送的功能
,文章更注重逻辑、思想,不会讲那些基本极光sdk集成内容,如果需要请直接去极光推送官网查看文档。
学前准备
Android
- 了解广播的基本使用,创建广播的几种方式
- ActivityManage和packpageManage的区别
- 第三方推送基本功能(例如极光推送)
服务器端
- 服务端了解基本node开发知识
- Mysql数据库有一些了解(本篇文章使用sequelize)
Andrid端
以流程图方式开始:
是应用已启动
应用未启动
流程图每一步骤逻辑进行详细讲解
- 收到推送消息入口,android开发者首先要知道推送的目的是什么,是否需要用户打开通知栏,跳转到详细信息(逻辑一)。
代码如下:
if (JPushInterface.ACTION_NOTIFICATION_RECEIVED.equals(intent.getAction())) {
int notifactionId = bundle.getInt(JPushInterface.EXTRA_NOTIFICATION_ID);
Log.d(TAG, "[JPushReceiver] 接收到推送下来的通知的ID: " + notifactionId);
} else if (JPushInterface.ACTION_NOTIFICATION_OPENED.equals(intent.getAction())) {
}
- 判断如果是需要打开的通知栏之后,需要判断是否启动了应用,代码如下(逻辑二)
//判断应用是否在运行代码(无论在前台还是后台)
private boolean getCurrentTask(Context context) {
ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
List<RunningTaskInfo> appProcessInfos = activityManager.getRunningTasks(50);
for (RunningTaskInfo process : appProcessInfos) {
if (process.baseActivity.getPackageName().equals(context.getPackageName())
|| process.topActivity.getPackageName().equals(context.getPackageName())) {
return true;
}
}
return false;
}
//判断启动调用方法
if (getCurrentTask(context)) { }else{ }
2.1判断后当应用属于启动状态,判断该通知消息显示是否需要用户登录,如果不需要用户登录则直接打开消息通知详情界面。
2.2用户属于启动状态,判断该通知消息是否需要用户登录,如果需要用户登录,需要先跳转登录界面,登录成功后跳转消息通知详细界面。
上面2.1与2.2代码如下
Intent pushIntent = new Intent();
pushIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
pushIntent.putExtra("pushMessage", pushMessage);
/**
* 需要登陆且当前没有登陆才去登陆页面
*/
if (pushMessage.messageType != null && pushMessage.messageType.equals("2")
&& !AccountManager.isSignIn()) {
pushIntent.setClass(context, LoginActivity.class);
pushIntent.putExtra("fromPush", true);
} else {
/**
* 不需要登陆或者已经登陆的Case,直接跳转到内容显示页面
*/
pushIntent.setClass(context, PushMessageActivity.class);
}
context.startActivity(pushIntent);
3.判断如果是需要打开的通知栏以后,如果应用没有启动,首先启动应用。
3.1应用启动以后,仍然是判断用户是否需要登录,查看通知消息,用户不需要登录就可以进入消息详情界面,需要使用startactivities方法,它的特点是只创建一个activity,返回后创建参数前面的activity。
3.2如果用户需要登录,方法一致,不过先跳转到loginActivity。代码如下
第三部分代码实现如下
Intent mainIntent = new Intent(context, MainActivity.class);
mainIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (pushMessage.messageType != null
&& pushMessage.messageType.equals("2")) {
Intent loginIntent = new Intent();
loginIntent.setClass(context, LoginActivity.class);
loginIntent.putExtra("fromPush", true);
loginIntent.putExtra("pushMessage", pushMessage);
context.startActivities(new Intent[]{mainIntent, loginIntent});
} else {
Intent pushIntent = new Intent(context, PushMessageActivity.class);
pushIntent.putExtra("pushMessage", pushMessage);
context.startActivities(new Intent[]{mainIntent, pushIntent});
}
Android端代码实现过程反思
- startActivities的基本使用,是否在栈中创建两个activity呢。
- Intent.FLAG_ACTIVITY_NEW_TASK 需要复习一下。
- 通过判断用户是否登录可以联想到开发其它项目时候游客模式的使用。
Android开发过程中的几点注意事项
- 注意首先理清需求,我们到底要实现哪些逻辑,也就是这几个if else。然后再动手写代码。
服务端
对于服务端,我们先来规范一下数据结构,需要给移动端提供哪些内容,需要往后台管理员操作时需要向表中添加那些数据。以下是数据库中的用到的表,消息通知表和通知类别表。表的结构以sequlize的当时和在mysql中显示两种样式展示。comment是对字段的描述
消息通知表结构如下图
'use strict'
module.exports = function (sequelize, DataTypes) {
return sequelize.define('Notice', {
id: { type: DataTypes.BIGINT(11), autoIncrement: true, primaryKey: true, unique: true, comment:'主键' },
noticeStyleId: { type: DataTypes.BIGINT(11), field: 'notice_style_id', allowNull: false, comment:'子级Id通知类型 0版本更新 1重大活动,后续添加' },
noticeTypeId: { type: DataTypes.BIGINT(11), field: 'notice_type_id', allowNull: false, comment:'父级Id:通知类型 0只通知不需要打开 1不需登陆就可以查看 2登陆后方可查看' },
noticeContent: { type: DataTypes.STRING, field: 'notice_content', allowNull: false, comment:'通知内容' },
noticeInitiator: { type: DataTypes.INTEGER, field: 'notice_initiator', allowNull: false,defaultValue:0, comment:'通知发起人 0管理员 1运营者' },
noticeTarget: { type: DataTypes.INTEGER, field: 'notice_target', allowNull: false, comment:'通知对象 0 all 1男 2女 3vip 4 非vip' },
noticeImg: {type: DataTypes.STRING, field: 'notice_img', comment:'通知图片'},
status:{type: DataTypes.INTEGER,allowNull:false,defaultValue:0,comment:'通知状态'},
deleted: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false, comment:'是否已删除'},
createdOn: { type: DataTypes.DATE, field: 'created_on', allowNull: false, defaultValue: DataTypes.NOW },
updatedOn: { type: DataTypes.DATE, field: 'updated_on', allowNull: false, defaultValue: DataTypes.NOW }
},
{
underscore: false,
timestamps: false,
freezeTableName: true,
tableName: 'notice',
comment: '系统通知表',
charset: 'utf8',
collate: 'utf8_general_ci'
});
}
通知类别表结构
如下图
'use strict'
module.exports = function (sequelize, DataTypes) {
return sequelize.define('NoticeCategory', {
id: { type: DataTypes.BIGINT(11), autoIncrement: true, primaryKey: true, unique: true, comment:'主键' },
noticeTitle: { type: DataTypes.STRING, field: 'notice_title', allowNull: false, defaultValue: "系统通知", comment:'通知标题' },
noticeEngTitle: { type: DataTypes.STRING, field: 'notice_eng_title', allowNull: false, defaultValue: " System notification", comment:'通知标题英文' },
FatherLevelId: { type: DataTypes.INTEGER, field: 'father_level_id', allowNull: false, comment:'父类级别ID,例如存放 0是只发送不可打开的 1可打开,不需登陆 2登陆后可打开' },
//rel: { type: DataTypes.STRING, field: 'rel', allowNull: false,defaultValue:0, comment:'表中主键id和父级别id的关联 前父,后主1,2' },
status:{type: DataTypes.INTEGER,allowNull:false,defaultValue:0,comment:'通知状态'},
deleted: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false, comment:'是否已删除'},
createdOn: { type: DataTypes.DATE, field: 'created_on', allowNull: false, defaultValue: DataTypes.NOW },
},
{
underscore: false,
timestamps: false,
freezeTableName: true,
tableName: 'noticeCategory',
comment: '系统通知类型表',
charset: 'utf8',
collate: 'utf8_general_ci'
});
}
需要注意的是二者之间的关系是一对一,通知表中的noticeStyleId,也就是通知表的外键对应通知类别表的id,同时在类别表中包含父类id,代表的含义是是否需要打开,需要打开的话是否需要登录。
后台管理员在发送通知的时候的逻辑:
- 对于管理端,需要一个接口路由,这个路由应该是post请求,但是这种post提交类型的接口需要考虑一个重要的点,被频繁post攻击,也就是被黑。所以需要一个中间件进行过滤。(包括一天内只能发送通知的次数,3分钟内不能频繁发送通知等)
过滤代码以及注释如下:
/*
* 验证系统消息或者通知发送次数
* */
exports.sendNoticeMiddle = (req, res, next) => {
let NoticeDa = new Notice;
NoticeDa.findAll({where: {noticeInitiator: 0}, order: 'id Desc', limit: 3}).then((result) => {
if (result.length > 0) {
//判断距离上传发送通知时间不小于3分钟
if (new Date().getTime() - result[0].createdOn.getTime() < 180000) {
return res.json({code: -1, message: '请不要频繁发送推送通知哦!'})
}
//每天提交次数不能超过三次
if (result.length > 2) {
if (new Date().getDay() == result[2].createdOn.getDay()) {
return res.json({code: -1, message: '每天只能发送三次推送通知哦!'})
}
}
}
next();
})
};
2.中间件过滤完成后,根据post的向数据库中插入数据,同时发送通知,并且把消息类型以json的合适发送,代码如下。
//发送通知 --------注意请求参数也是file
exports.sendNotice = (req, res, next) => {
let form = new formidable.IncomingForm();
form.keepExtensions = true;
form.parse(req, function (err, fields, files) {
if (!fields.content) {
return res.json({code: -1, message: '请简单描述您反馈的问题'})
}
let data = {
noticeTypeId: fields.noticeTypeId,
noticeContent: fields.noticeContent,
noticeInitiator: fields.noticeInitiator,
noticeTarget: fields.noticeTarget,
noticeImg: '',
createdOn: new Date(),
updatedOn: new Date()
};
if (files && files.file) {
//path
let dirpath = 'public/files/notice';
//创建目录文件
if (!tool.mkdirSync(dirpath, '777')) {
return res.json({code: -1, message: '服务器繁忙!'})
}
let filePath = files.file.path;
let fileExt = filePath.substring(filePath.lastIndexOf('.'));
if (('.jpg.jpeg.png.gif').indexOf(fileExt.toLowerCase()) > -1) {
//以当前时间戳对上传文件进行重命名
let fileName = new Date().getTime() + '-' + fields.noticeTitle + fileExt;
let targetFile = dirpath + '/' + fileName;
let readStream = fs.createReadStream(filePath);
let writeStream = fs.createWriteStream(targetFile);
readStream.pipe(writeStream);
//修改路径相对路径
data.noticeImg = targetFile;
}
}
//向通知类别表中插入数据时候的id noticeStyleId
let categoryData = {
noticeEngTitle: fields.noticeTitle,
noticeTitle: fields.noticeTitle,
FatherLevelId: fields.noticeTypeId,
createdOn: new Date()
};
let NoticeCategoryDa = new NoticeCategory();
let NoticeDa = new Notice();
NoticeCategoryDa.creat(categoryData).then((result) => {
if (result) {
data.noticeStyleId = result.id;
NoticeDa.create(data).then((result) => {
if (result) {
console.log("推送通知添加成功")
client.push().setPlatform('ios', 'android')
.setAudience(JPush.tag('555', '666'), JPush.alias('666,777'))
.setNotification('Hi, JPush', JPush.ios('ios alert'), JPush.android('android alert', null, 1))
.setMessage('msg content')
.setOptions(null, 60)
.send()
.then(function(result) {
res.json({code: 0, message: '通知发送成功'});
}).catch(function(err) {
res.json({code: 0, message: '通知发送失败'});
});
}
})
}
});
}
)
};
3.两个表一对一的关系,联合查询通知列表的接口代码实现。
代码如下。
exports.getNoticeList = (req, res, next) => {
let noticeDa = new Notice();
let noticeCategory = new NoticeCategory();
//查询出所有list,去除类型为0的只通知不可查看的 关联查询
let include = [{
association: noticeDa.model.belongsTo(noticeCategory.model, {foreignKey: 'noticeStyleId', targetKey: 'id'}),//一对一 外键在源文件中,下面调用查找方法查找的是带外键的表 attribute要求的内容是noticeCategory中的数据
required: true,
attributes: ['noticeTitle', 'noticeEngTitle'],//注意这个不是表上显示的值
where: {deleted: false}
}];
let page = 1;
let pages = 6;
if (req.query.quantity && !isNaN(parseInt(req.query.quantity))) {
pages = parseInt(req.query.quantity);
}
if (req.query.page && !isNaN(parseInt(req.query.page))) {
page = parseInt(req.query.page);
}
noticeDa.paginate({
where: {$not: {noticeTypeId: '0'}, deleted: false},
page: page,
perPage: pages,
options: {order: 'createdOn DESC', include: include}
}).then((result) => {
let items = [];
result.forEach(function (item) {
items.push({
id: item.id,
noticeTitle: item.NoticeCategory.noticeTitle,
noticeEngTitle: item.NoticeCategory.noticeEngTitle,
noticeImg: item.noticeImg,
noticeContent: item.noticeContent,
noticeTypeId: item.noticeTypeId,
noticeStyleId: item.noticeStyleId,
})
});
res.json({code: 0, items: items});
});
};
服务端的反思:
文章中针对一对一,外键在原模型中的情况下
进行联合查询,如果其他情况应该怎么处理,例如场景多对多情况下查询,一对多情况
服务端注意事项
服务器端,我们在开发一些post类型的路由的时候一定要考虑被黑的情况,被模拟多次post请求,例如场景,获取样正码接口,意见反馈接口,发送消息通知接口。
完结
好久没有写博客了,基本思想讲到这里,如果有小伙伴觉得有问题的地方,可以与我沟通哦。