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的验证中间件才可以获取全部的文章。