Contents
17. 后台管理相关API开发¶
本节开发与管理员相关的API,管理员比普通用户增加了文章管理和人员管理等权限。
管理员通过与users相关的路由登录或获取某些内容,不同的是这里需要增加一个新的中间件,命名为checkAdmin,这里仅指定admin为唯一的管理员用户,代码如下:
//检测是否是管理员
exports.checkAdmin = (req, res, next) => {
console.log("检测管理员用户")
if (req.username == 'admin') {
//如果是管理员,则在redis增加一个power
let key = req.headers.fapp + ":user:power:" + req.headers.token
redis.set(key, 'admin')
next()
} else {
res.json(util.getReturnData(403, "权限不足"))
}
}
在app.js文件中定义admin路由,引入路由文件和上述中间件,代码如下:
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var {checkAPP, checkUser, checkAdmin} = require('./util/middleware')
var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
//增加管理员路由
var adminRouter = require('./routes/admin');
.....
app.use('/', checkAPP, indexRouter);
app.use('/users', checkAPP, usersRouter);
app.use('/admin', [checkAPP, checkUser, checkAdmin], adminRouter);
module.exports = app;
同之前的代码习惯一致,在controller文件夹中创建admin.js文件用于在其中编写逻辑代码,所有的路由都定义在router/admin.js文件中。
17.1. 1.文章添加和修改¶
文章添加和修改接口,路由地址为:http://localhost:3000/admin/setArticle
该接口采用POST请求方式,需要发送文章的标题、作者、分类和小标签等内容,基本数据格式如下:
{
"article": {
"title": "测试文字",
"writer": "admin",
"text": "cesssssssssss",
"type": 1,
"tag": ["js", "node"]
}
}
为了方便查找,要优化文章存储的键值,笔者使用3个有序集合,分别存储文章的阅读量、文章的发布日期和文章的点赞量数据。
每篇文章都有一个uid,这是文章唯一的标识符,上述有序集合存放的内容就是这些文章的uid。
首先需要在admin.js文件中添加相应的路由,代码如下:
var express = require('express');
var router = express.Router();
//// 引入了逻辑处理的JavaScript文件(需要注意是否有其他路由使用到其他文件,均需要引入,本处已省略)
var {setArticle} = require('../controller/admin')
// 发布文章
router.post('/setArticle', setArticle)
module.exports = router;
接着在controller/admin.js文件中编写相应的逻辑代码。
需要注意的是,前端传递的JSON串并不包含某些需要用到的值,如新文章的时间戳、观看数0和点赞数0。
这3个值需要在Redis中建立有序集合,只有通过有序集合才可以对数据排序。有序集合分别命名为
book:a_time、book:a_view和book:a_like。
此外还应当生成一个show字段,用来管理文章的上线(发布)和下线(删除)功能,初始化为0,不显示在主页上。
本例的API还涉及文章的分类(book:a_type:type_id)和小标签(book:tag:md5加密后的标签名称)功能,读者可以仔细阅读代码。文章添加修改的完整代码如下:
//发布文章
exports.setArticle = (req, res, next) => {
// 获取到传递的值
let data = req.body.article
//任何修改或者是新上线的文章都不能显示
data.show = 0
console.log(data)
let key = ''
if ('a_id' in req.body.article) {
key = req.headers.fapp + ":article:" + req.body.article.a_id
//储存
redis.set(key, data)
res.json(util.getReturnData(0, '修改成功'))
} else {
//新文章需要初始化点赞数0,观看数0以及时间戳
data.time = Date.now()
key = req.headers.fapp + ":article:"
redis.incr(key).then((id) => {
//方便取用
data.a_id = id
key = key + id
//储存文章
redis.set(key, data)
//储存分类以及小标签
let a_type = data.type
//获取
redis.get(req.headers.fapp + ":a_type:" + a_type).then((data1) => {
if (!data1) {
data1 = []
}
//数组对象
data1.push(key)
// 再次储存
redis.set(req.headers.fapp + ":a_type:" + a_type, data1)
})
//小标签需要循环操作
let tags = data.tag
tags.map((item) => {
let tKeyMd5 = crypto.createHash('md5').update(item).digest("hex")
console.log(tKeyMd5)
redis.get(req.headers.fapp + ':tag:' + tKeyMd5).then((data1) => {
if (!data1) {
data1 = []
}
data1.push(key)
// 再次存储
redis.set(req.headers.fapp + ":tag:" + tKeyMd5, data1)
})
})
//新文章需要建立新的有序集合点赞数0,观看数0及时间戳
redis.zadd(req.headers.fapp + ':a_time', key, Date.now())
redis.zadd(req.headers.fapp + ':a_view', key, 0)
redis.zadd(req.headers.fapp + ':a_like', key, 0)
res.json(util.getReturnData(0, '新建文章成功'))
})
}
}
上述代码使用MD5算法生成小标签的key键,也可以使用Base64编码或文字编码等其他方式生成。
使用Postman测试API。当发送一个不存在的a_id请求时,会返回“新建文章成功”;如果包含a_id则显示“修改成功”。
文章的添加和修改
{
"code": 0,
"message": "新建文章成功",
"data": []
}
17.2. 2.文章发布和删除¶
接口路由地址为: http://localhost:3000/admin/showArticle
该接口只需要更改文章的show字段即可。为了统一请求方式,该接口使用POST方式请求数据,其实在RESTful风格的路由请求中,应当使用PUT方式。
此时是在已知文章对应a_id的情况下,所以只需获取当前文章的状态,并将该状态转换为对应的状态即可。
首先在admin.js文件中添加相应的路由,代码如下:
var express = require('express');
var router = express.Router();
//// 引入了逻辑处理的JavaScript文件(需要注意是否有其他路由使用到其他文件,均需要引入,本处已省略)
var {showArticle} = require('../controller/admin')
//文章的上线和下线
router.post('/showArticle', showArticle)
module.exports = router;
文章的发布和删除接口只需要把文章对应的JSON字符串更改后再保存即可,其中controller/admin.js文件中的代码如下:
//文章的上线和下线
exports.showArticle = (req, res, next) => {
// 获取到传递的值
let key = req.headers.fapp + ":article:" + req.body.a_id
redis.get(key).then((data) => {
if (!data) res.json(util.getReturnData(404, "没有该文章"))
// 修改显示
if (data.show == 1) {
data.show = 0
} else {
data.show = 1
}
redis.set(key, data)
})
res.json(util.getReturnData(0, "文章修改成功"))
}
通过Postman插件发送相关的a_id字段,可以发现,Redis中的数据会自动更改
postman: http://localhost:3000/admin/showArticle
{
"a_id": 3
}
将a_id=3的文章下线.改变文章状态
{
"code": 0,
"message": "",
"data": [
{
"title": "文章暂未上线",
"date": "",
"id": 0
},
{
"title": "文章暂未上线",
"date": "",
"id": 0
},
{
"title": "测试文章2",
"date": "2022-7-20 9:29:54",
"id": 2
},
{
"title": "测试文章1",
"date": "2022-7-20 9:29:37",
"id": 1
}
]
}
17.3. 3.添加和修改分类¶
添加和修改分类的接口,路由地址为: http://localhost:3000/admin/setArticleType
该接口使用POST方式传递参数,参数是JSON字符串,包含全部的分类和分类的唯一ID。每个唯一ID又包含一个JSON字符串对象,保存着符合该分类文章的唯一ID。添加文章时会修改该ID对应的内容,这样就保证了文章和分类对应。
如下方结构所示,简化代码的同时考虑到分类不会很多,所以唯一ID不是自增形式,而是人工传入ID的形式。
{
"type": [{
"uid": 1,
"name": "分类1"
},
{
"uid": 2,
"name": "分类2"
}
]
}
首先在admin.js文件中添加相应的路由,代码如下:
var express = require('express');
var router = express.Router();
//// 引入了逻辑处理的JavaScript文件(需要注意是否有其他路由使用到其他文件,均需要引入,本处已省略)
var {setArticleType} = require('../controller/admin')
//分类的发布
router.post('/setArticleType', setArticleType)
module.exports = router;
文章的分类需要一个键-值对来存储所有的类型,通过循环判定指定的ID是否已存在于分类中。如果存在则不插入或不更新;如果不存在,则执行set命令。完整的代码如下:
//发布分类
exports.setArticleType = (req, res, next) => {
// 获取到传递的值
//应当确定的是type中对应的唯一key是不重复的
let data = req.body.type
console.log(data)
let key = req.headers.fapp + ':a_type'
// 根据key直接更新内容
redis.set(key, data)
// 循环整个传递的值,依次创建唯一id对应的键值
data.map((item) => {
console.log(item.uid)
let tKey = req.headers.fapp + ':a_type:' + item.uid
redis.get(tKey).then((data1) => {
//不存在则添加
if (!data1) {
redis.set(tKey, [])
}
})
})
res.json(util.getReturnData(0, "创建分类成功"))
}
通过传递POST请求创建了两个相关的分类,在Redis中创建了3个键-值对。
{
"code": 0,
"message": "创建分类成功",
"data": []
}
17.4. 4.获取全部用户列表¶
接口路由地址为: http://localhost:3000/admin/getAllUser
该接口采用GET请求方式获取所有用户信息的Key值。当然,在实际的生产环境中使用keys可能会导致一些严重后果,如Redis业务的挂起。也就是说,虽然keys命令非常快,但如果数据键值非常多,keys命令无法迅速完成,则执行该命令的同时其他命令不会执行。
要解决这个问题,笔者推荐使用scan命令。scan命令除了和keys一样支持模式匹配以外,还采用游标的方式获取数据,同时它还能实现数据的分页。
注意:scan命令有可能出现重复的键值,此时使用set()对象对获得的结果进行去重处理。
首先编写路由文件admin.js,代码如下:
var express = require('express');
var router = express.Router();
//// 引入了逻辑处理的JavaScript文件(需要注意是否有其他路由使用到其他文件,均需要引入,本处已省略)
var {getAllUser} = require('../controller/admin')
//获得所有的用户
router.get('/getAllUser', getAllUser)
module.exports = router;
接下来编写具体的用户逻辑代码,使用scan()方法获取所有的用户键之后,为了方便显示,使用map循环获取该键值的详细资料,代码如下:
//获得全部用户
exports.getAllUser = (req, res, next) => {
// 获取到的用户key值的模式
let re = req.headers.fapp + ':user:info:*'
//注意这里使用的scan方法,这里可以传入游标和个数
redis.scan(re).then(async (data) => {
//这里通过循环获得用户详细资料
let result = data[1].map((item) => {
//获得每一个用户的username
return redis.get(item).then((user) => {
return {'username': user.username, 'login': user.login, 'ip': user.ip}
})
})
let t_data = await Promise.all(result)
res.json(util.getReturnData(0, "", t_data))
})
}
这样,请求该接口就能获取所有用户的资料和是否被封停的状态。需要注意的是,本例因为数据较少,没有分页,获取分页的方法可以参考util.redis.js中scan()的定义。本例的效果如下
获取所有用户
{
"code": 0,
"message": "",
"data": [
{
"username": "hujianli",
"login": 0,
"ip": "::1"
},
{
"username": "hujianli1",
"login": 0,
"ip": "::1"
},
{
"username": "admin",
"login": 0
},
{
"username": "hujianli2",
"login": 0,
"ip": "::1"
}
]
}
17.5. 5.封停用户¶
接口路由地址为: http://localhost:3000/admin/stopLogin/:id
通过该接口可以改变用户的login属性,本项目定义如果该属性为0则是正常状态,可以登录;
如果属性为1则是封停状态。该接口需要传递一个id参数(封停用户的username)。
首先定义路由,代码如下:
var express = require('express');
var router = express.Router();
//// 引入了逻辑处理的JavaScript文件(需要注意是否有其他路由使用到其他文件,均需要引入,本处已省略)
var {stopLogin} = require('../controller/admin')
//用户封停操作
router.get('/stopLogin/:id', stopLogin)
module.exports = router;
在编写逻辑处理部分时,只需要获取用户的详细信息,修改其login状态即可,代码如下:
//封停用户
exports.stopLogin = (req, res, next) => {
// 获取到传递的值
let key = req.headers.fapp + ':user:info:' + req.params.id
redis.get(key).then((user) => {
if (user.login == 0) {
user.login = 1
} else {
user.login = 0
}
redis.set(key, user)
res.json(util.getReturnData(0, "用户修改成功"))
})
}
以上程序请求封停用户接口并将参数指定为hujianli1,再通过获取全部用接口获取用户的详细信息使login属性发生了变化
{
"code": 0,
"message": "用户修改成功",
"data": []
}
http://localhost:3000/admin/getAllUser查看
封停用户
{
"code": 0,
"message": "",
"data": [
{
"username": "hujianli",
"login": 0,
"ip": "::1"
},
{
"username": "hujianli1",
"login": 1,
"ip": "::1"
},
{
"username": "admin",
"login": 0
},
{
"username": "hujianli2",
"login": 0,
"ip": "::1"
}
]
}
17.6. 6.修改首页轮播内容¶
接口路由地址是: http://localhost:3000/admin/setIndexPic
该接口需要使用POST方式传递参数,其本身存储一个JSON字符串。
[
{
"title": "baidu",
"src": "http://www.baidu.com",
"img": "https://www.javaweb.shop/upload/image/20200526/1590456522669.jpeg"
}
]
首先在admin.js文件中添加相应的路由,代码如下:
var express = require('express');
var router = express.Router();
//// 引入了逻辑处理的JavaScript文件(需要注意是否有其他路由使用到其他文件,均需要引入,本处已省略)
var {setIndexPic} = require('../controller/admin')
// 修改主页轮播图片
router.post('/setIndexPic', setIndexPic)
module.exports = router;
接着在controller/admin.js文件中编写相应的逻辑处理代码,接口本身通过JSON对象传输数据,所以只要获取该对象并以book:indexPic作为键,将其存放在Redis中即可。完整的代码如下:
//设置主页轮播图
exports.setIndexPic = (req, res, next) => {
let key = req.headers.fapp + ":indexPic"
// 获取到传递的值
let data = req.body.indexPic
console.log(data)
//储存
redis.set(key, data)
res.json(util.getReturnData(0, '修改成功'))
}
最终的运行效果如下
修改首页轮播
{
"code": 0,
"message": "修改成功",
"data": []
}
有一点需要注意,首页中的图片只能通过输入地址的方式进行存储,这对于一个接口来说已经足够。
通过与图片上传这类接口的联动,图片上传完成后会自动返回服务器中保存图片的地址。读者可以将图片上传至自己的服务器中,或使用CDN等地址。
17.7. 7.修改导航内容¶
接口路由地址为: http://localhost:3000/admin/changeNav
json数据结构
{ "nav_menu": [
{
"name": "主页",
"src": "http://loaclhost"
},
{
"name": "文章",
"src": "/article/list"
},
{
"name": "关于我",
"src": "/article/listme"
}
]}
该接口需要使用POST方式传递参数。如果添加管理员首页的导航菜单,需要更改Redis中book:nav_menu键的值。在Router文件夹中的admin.js文件中创建新的路由,代码如下:
var express = require('express');
var router = express.Router();
//// 引入了逻辑处理的JavaScript文件(需要注意是否有其他路由使用到其他文件,均需要引入,本处已省略)
var {setNavMenu} = require('../controller/admin')
// 更改导航菜单
router.post('/changeNav', setNavMenu);
module.exports = router;
前端发送的内容原本就是JSON字符串格式,可直接保存,完整的代码如下:
let redis = require("../util/redisDB")
const crypto = require('crypto');
const util = require('../util/common')
// 更改导航菜单
exports.setNavMenu = (req, res, next) => {
let key = req.headers.fapp + ":nav_menu"
// 获取到传递的值
let data = req.body.nav_menu
console.log(data)
//储存
redis.set(key, data)
res.json(util.getReturnData(0, '修改成功'))
}
使用Postman插件进行测试
{
"code": 0,
"message": "修改成功",
"data": []
}
这样就成功修改了导航内容,修改后可以在redis-cli中查看。
17.8. 8.修改底部内容¶
接口路由地址为: http://localhost:3000/admin/setFooter
该接口需要使用POST方式传递参数,和之前修改导航内容的API一样,其本身存储一个JSON字符串。
{
"footer": [{
"name": "版权所有",
"src": "http://loaclhost",
"text": "Stiller"
},
{
"name": "发送邮件",
"src": "mailto:1879324764@qq.com",
"text": "Gmail"
}]
}
首先在admin.js文件中添加相应的路由,代码如下:
var express = require('express');
var router = express.Router();
//// 引入了逻辑处理的JavaScript文件(需要注意是否有其他路由使用到其他文件,均需要引入,本处已省略)
var {setFooter} = require('../controller/admin')
// 底部内容修改
router.post('/setFooter', setFooter);
module.exports = router;
接着在controller/admin.js文件中编写相应的逻辑处理代码,接口本身也通过JSON对象传输数据,只需要获取对象并以book:footer作为键存放在Redis中即可。完整的代码如下:
//更改底部内容
exports.setFooter = (req, res, next) => {
let key = req.headers.fapp + ":footer"
// 获取到传递的值
let data = req.body.footer
console.log(data)
//储存
redis.set(key, data)
res.json(util.getReturnData(0, '修改成功'))
}
最终的运行效果如下
{
"code": 0,
"message": "修改成功",
"data": []
}
17.9. 9.修改友情链接内容¶
接口路由地址为: http://localhost:3000/admin/setLinks
该接口需要使用POST方式传递参数,和之前修改导航内容的API一样,其本身存储一个JSON字符串。
{ "links": [{
"name": "gitee",
"src": "http://gitee.com"
}]
}
首先在admin.js文件中添加相应的路由,代码如下:
var express = require('express');
var router = express.Router();
//// 引入了逻辑处理的JavaScript文件(需要注意是否有其他路由使用到其他文件,均需要引入,本处已省略)
var {setLinks} = require('../controller/admin')
// 友情链接
router.post('/setLinks', setLinks)
module.exports = router;
接着在controller/admin.js文件中编写相应的逻辑处理代码,接口本身通过JSON对象来传输数据,只需要获取该对象并以book:footer作为键存放在Redis中即可。完整的代码如下:
//设置友情链接
exports.setLinks = (req, res, next) => {
let key = req.headers.fapp + ":links"
// 获取到传递的值
let data = req.body.links
console.log(data)
//储存
redis.set(key, data)
res.json(util.getReturnData(0, '修改成功'))
}
最终的运行效果如下
{
"code": 0,
"message": "修改成功",
"data": []
}
17.10. 10.其他权限判定¶
除了上述已经完成的基本接口以外,在管理员权限出现之后,应当修改一些内容。
接口路由地址为: http://localhost:3000/admin
例如,在获取所有文章列表的接口时应该进行权限判断,如果访问用户具有管理员权限时,不再显示没有上线的提示。修改后的getData.js文件代码如下:
//获取最新的文章列表
exports.getNewArticle = (req, res, next) => {
let key = req.headers.fapp + ":a_time"
let isAdmin = false
//获取数据
console.log(key)
//获得集合
//登录用户才判断
if ('token' in req.headers) {
//如果是管理员,则在加一次查找
let pKey = req.headers.fapp + ":user:power:" + req.headers.token
redis.get(pKey).then((power) => {
//管理员权限
if (power == 'admin') {
redis.zrevrange(key, 0, -1).then(async (data) => {
let result = data.map((item) => {
//获得每一篇文章的题目和id以及日期
return redis.get(item.member).then((data1) => {
console.log(data1)
if (data1) {
return {'title': data1.title, 'date': util.getLocalDate(item.score), 'id': data1.a_id,'show':data1.show}
}
})
})
let t_data = await Promise.all(result)
console.log(t_data)
res.json(util.getReturnData(0, '', t_data))
})
}else{
// res.json(util.getReturnData(1, '其他权限'))
// 其他权限暂时依旧执行普通未登录效果
redis.zrevrange(key, 0, -1).then(async (data) => {
console.log(data)
let result = data.map((item) => {
//获得每一篇文章的题目和id以及日期
return redis.get(item.member).then((data1) => {
if (data1 && data1.show != 0) {
return {'title': data1.title, 'date': util.getLocalDate(item.score), 'id': data1.a_id}
} else {
return {'title': '文章暂未上线', 'date': '', 'id': 0}
}
})
})
let t_data = await Promise.all(result)
res.json(util.getReturnData(0, '', t_data))
})
}
})
} else {
redis.zrevrange(key, 0, -1).then(async (data) => {
console.log(data)
let result = data.map((item) => {
//获得每一篇文章的题目和id以及日期
return redis.get(item.member).then((data1) => {
if (data1 && data1.show != 0) {
return {'title': data1.title, 'date': util.getLocalDate(item.score), 'id': data1.a_id}
} else {
return {'title': '文章暂未上线', 'date': '', 'id': 0}
}
})
})
let t_data = await Promise.all(result)
res.json(util.getReturnData(0, '', t_data))
})
}
}
再次请求该地址,如果使用的是管理员账号,将返回所有的文章,不论文章是否发布
{
"code": 0,
"message": "",
"data": [
{
"title": "测试文章4",
"date": "2022-7-20 14:22:29",
"id": 4,
"show": 0
},
{
"title": "测试文章3",
"date": "2022-7-20 9:30:5",
"id": 3,
"show": 0
},
{
"title": "测试文章2",
"date": "2022-7-20 9:29:54",
"id": 2,
"show": 1
},
{
"title": "测试文章1",
"date": "2022-7-20 9:29:37",
"id": 1,
"show": 1
}
]
}
注意:初次登录时必须请求admin的验证中间件才可以获取全部的文章。