目 录CONTENT

文章目录

宿舍管理系统(二)

Administrator
2021-11-18 / 0 评论 / 0 点赞 / 5441 阅读 / 41059 字

宿舍管理系统(二)

  • 项目名称:宿舍管理系统
  • 项目平台:ndoejs+mysql
  • 项目技术:express
  • 开发人员:杨标

项目架构图

image.png


三十二、新增管理员

当前模块虽然也是一个新增的模块,但是在这个模块上面我们主要是为了体现以下的几个点

  1. 密码的md5加密
  2. 图片上传
  3. 省市区三级联动

image.pngimage-20211026164728232

三十三、省市区三级联动

省市区三级联动是需要数据表的支持的,数据表的格式如下

image.png

在service的目录下面创建 AreaService.js文件

/**
 * @description 操作room_info数据表
 * @author 杨标
 * @version 1.0
 */

const BaseServcie = require("./BaseService.js");
const PageList = require("../model/PageList.js");

class AreaService extends BaseServcie {
    constructor() {
        super();
        this.currentTableName = this.tableMap.area_info;
    }

    /**
     * @description 根据parentId查询下面的结构
     * @param {number} parentId 父级的id 
     * @returns {Promise<[]>} 返回查询的结果
     */
    findListByParentId(parentId) {
        let strSql = ` select * from ${this.currentTableName} where parentId =? `;
        return this.executeSql(strSql, [parentId]);
    }
}
module.exports = AreaService;

同时在ServiceFactory.js里面写上注释


/**
 * @typedef ServiceFactoryType
 * @property {import("../service/RoomInfoService.js")} roomInfoService
 * @property {import("../service/AreaService.js")} areaService
 */

在routes的目录下面创建 areaRoute.js文件

const express = require("express");
const router = express.Router();

router.get("/findListByParentId", async (req, resp) => {
    let { parentId } = req.query;
    let results = await ServiceFactory.areaService.findListByParentId(parentId);
    resp.json(new ResultJson(true, "获取数据成功", results));
});

module.exports = router;

在app.js里面要连接这个文件

app.use("/area",require("./routes/areaRoute.js"));

addAdminInfo.html文件**

<script type="text/html" id="temp1">
        <option value="">-请选择-</option>
        {{each list item index}}
        <option value="{{item.areaId}}">{{item.areaName}}</option>
        {{/each}}
</script>
$(function() {
    function renderOptions(parentId, targetElementId) {
        request.get("/area/findListByParentId", {
            parentId: parentId
        }).then(function(res) {
            console.log(res);
            if (res.status == "success") {
                var htmlStr1 = template("temp1", {
                    list: res.data
                });
                $(targetElementId).html(htmlStr1);
            }
        }).catch(function(error) {
            console.log(error);
            Qmsg.error("服务器错误");
        });
    }

    renderOptions(-1, "#sel_province");
    $("#sel_province").on("change", function() {
        var parentId = $(this).val();
        renderOptions(parentId, "#sel_city");
        $("#sel_area").empty();
    });
    $("#sel_city").on("change", function() {
        var parentId = $(this).val();
        renderOptions(parentId, "#sel_area");
    });

});

三十四、图片上传

在上面的UI界面里面,我们看到我们需上传一张管理员的图片,这个时候我们使用了input的文件选择框

但是这里有一个问题,我们要使用ajax将这个文件上传

第一步:前端页面代码

设置文件选择框只能选择图片

<input accept="image/*" type="file" class="custom-file-input" id="admin_photo_file">

再监控它的变化,如果有文件被选中了,我们就上传图片

  $("#admin_photo_file").on("change", function() {
      var file = this.files[0];
      if (file) {
          if (/^image\/(jpe?g|png|gif|bmp|svg)$/.test(file.type)) {
              // 模板一个form的表单对象
              var formData = new FormData();
              formData.append("admin_photo", file);
              $.ajax({
                  url: baseURL + "/adminInfo/uploadAdminPhoto",
                  method: "post",
                  data: formData,
                  processData: false, //不要对我的formData的数据做二次处理
                  contentType: false, //不要携带请求类型过去,因为我的文件就是原来的类型
                  success: function(res) {
                      $("#img_admin_photo").attr("src", baseURL + res.data);
                      Qmsg.success("上传图片成功");
                  },
                  error: function(error) {
                      console.log(error);
                      Qmsg.error("上传失败");
                  },
                  xhr: function() {
                      var xhr = $.ajaxSettings.xhr();
                      // 上传的时候进进度发生改变的时候的事件
                      xhr.upload.onprogress = function(e) {
                          var precent = parseInt(e.loaded / e.total * 100);
                          $("#img-upload-progress").css("width",precent+"%").text(precent+"%");
                      }
                      return xhr;
                  }
              })
          } else {
              $("#admin_photo_file").val("");
          }
      }
  });

第二步:后端代码

express默认是不运行文件上传的,所以如果想实现文件上传的功能,需要使用第三方的包multer

首先,我们需要去安装这一个包

$ npm install multer --save

接下来,在需要的地方配置这一个包,adminInfoRoute.js配置

/**
 * @author 杨标
 * @description roominfo的路由模块 
 */
const express = require("express");
const router = express.Router();
const ServiceFactory = require("../factory/ServiceFactory.js");
const ResultJson = require("../model/ResultJson.js");
const multer = require("multer");
const path = require("path");
const fs = require("fs");
const uplaod = multer({
    dest: path.join(__dirname, "../adminPhoto")
});

//上传图片
router.post("/uploadAdminPhoto", uplaod.single("admin_photo"), async (req, resp) => {
    let file = req.file;
    console.log(file);
    resp.json(new ResultJson(true,"文件上传成功"));
});

module.exports = router;

当我们去测试了的时候,我们发现我们的文件已经可以上传了,但是我们的文件名子有问题,它没有后缀名,上传的文件信息如下

{
  fieldname: 'admin_photo',
  originalname: 'try4.jpg',
  encoding: '7bit',
  mimetype: 'image/jpeg',
  destination: 'D:\\杨标的工作文件\\班级教学笔记\\H2103\\1026\\code\\rental_house\\adminPhoto',
  filename: 'aea8e416bfe8bb786c73513cd63eecf8',
  path: 'D:\\杨标的工作文件\\班级教学笔记\\H2103\\1026\\code\\rental_house\\adminPhoto\\aea8e416bfe8bb786c73513cd63eecf8',
  size: 49318
}

我们现在将后端路由的里面的代码改变成如下

//上传图片
router.post("/uploadAdminPhoto", uplaod.single("admin_photo"), async (req, resp) => {
    let file = req.file;
    if (file) {
        fs.renameSync(file.path, file.path + file.originalname);
        resp.json(new ResultJson(true, "文件上传成功", `/adminPhoto/${file.filename + file.originalname}`));
    }
    else {
        resp.status(404).json(new ResultJson(false, "没有接收到文件"));
    }
});

这样前端在接收到值的时候就会显示得到如下的结果

{
    data: "/adminPhoto/2cedad4ce7e7a74fc460c8c4be84f6362018上.jpg"
	msg: "文件上传成功"
	status: "success"
}

上面的data接收到的就是文件的路径

接收到文件的路径以后,我们显示不出来

http://192.168.1.254:8080/adminPhoto/2cedad4ce7e7a74fc460c8c4be84f6362018上.jpg

图片不能显示,为什么呢?

三十五、静态区域创建

express的服务器里面,所有浏览器需要的东西,我们都是通过resp来返回的,但是有些特殊情况不需要使用resp来返回,如上面的图片上传以后,保存以了一某一个文件夹,这个文件夹里面的图片应该是可以直接 访问,不需要经过resp的处理,这个时候,我们就需要使用到express框架的静态服务器技术

我们所有的图片现在都是放在adminPhoto这个文件夹下面

image.png

我们现在需要 把这个目录设置为静态区域

//设置静态区域
app.use("/adminPhoto",express.static(path.join(__dirname,"./adminPhoto")));

上面的代码的意思就是如果我发现请求的时候一级路径是/adminPhoto,我就把这个请求带到adminPhoto的目录,不要交给路由处理


三十六、保存管理员的数据

当我们图片上传,区域地址选中了以后,我们就可以开始来进行保存数据的操作,在保存数据之前,我们仍然要进行相应的表单验证

第一步:表单验证

image.png

图片需要添加一个隐藏的域,做表单验证,同时在表单验证的插件里面,设置如下

 var validateForm = $("#form-addAdminInfo").validate({
     ignore: [],			//添加这一行代码
     errorClass: "text-danger",
     errorPlacement: function(errorElement, currentElement) {
         currentElement.parentsUntil("form").find("div.text-danger").append(errorElement);
     },
 });

第二步:保存数据到后台,addAdminInfo.html

function saveData() {
    var loading = Qmsg.loading("正在保存数据...");
    request.post("/adminInfo/add", {
        admin_name: $("#admin_name").val(),
        admin_sex: $("#admin_sex").val(),
        admin_tel:$("#admin_tel").val(),
        admin_pwd: $("#admin_pwd").val(),
        admin_email: $("#admin_email").val(),
        admin_photo: $("#admin_photo").val(),
        admin_address: [
            $("#sel_province>option:selected").text(),
            $("#sel_city>option:selected").text(),
            $("#sel_area>option:selected").text(),
            $("#detail_addr").val()
        ].join(" ")
    }).then(function(res) {
        if (res.status == "success") {
            Qmsg.success("保存成功")
        } else {
            Qmsg.error("保存失败");
        }
    }).catch(function(error) {
        console.log(error);
        Qmsg.error("服务器错误");
    }).finally(function() {
        loading.close();
    });
}

第三步:后台路由接收adminInfoRoute.js

router.post("/add", async (req, resp) => {
    let results = await ServiceFactory.adminInfoService.add(req.body);
    resp.json(new ResultJson(results, results ? "新增成功" : "新增失败"));
});

第四步:后台servcie保存数据AdminInfoService.js

注意:所有新的Service都要在ServiceFactory.js里面添加注释,这样写代码会有提示

/**
 * @description 操作room_info数据表
 * @author 杨标
 * @version 1.0
 */

const BaseServcie = require("./BaseService.js");
const PageList = require("../model/PageList.js");
const md5 = require("md5-node");
const AppConfig = require("../config/AppConfig");

class AdminInfoServcie extends BaseServcie {
    constructor() {
        super();
        this.currentTableName = this.tableMap.admin_info;
    }

    /**
     * @description 新增一个管理员
     * @param {{ admin_name, admin_sex, admin_tel, admin_pwd, admin_email, admin_photo, admin_address }} param 新增的对象 
     * @returns {Promise<boolean>}
     */
    async add({ admin_name, admin_sex, admin_tel, admin_pwd, admin_email, admin_photo, admin_address }) {
        admin_pwd = md5(admin_pwd + AppConfig.md5Salt);
        let strSql = ` INSERT INTO ${this.currentTableName} ( admin_name, admin_sex, admin_tel, admin_pwd, admin_email, admin_photo, admin_address) VALUES (?, ?, ?, ?, ?, ?, ?); `;
        let results = await this.executeSql(strSql, [admin_name, admin_sex, admin_tel, admin_pwd, admin_email, admin_photo, admin_address]);
        return results.affectedRows > 0 ? true : false;
    }
}

module.exports = AdminInfoServcie;

在上面的Service里面,我们对我们的代码进行了md5加密+加盐处理

第五步:设置md5的盐config/AppConfig.js

const AppConfig = {
    md5Salt: "oausdfpasdfja_)qlwke"
}
module.exports = AppConfig;

三十七、密码的md5加密

md5是一种不可解密的加密模式,它会将原来的字符串生成一个字符串,nodejs里面的md5加密有很多种方式,我们可以使用第三方包来完成md5-node

第一步:安装依赖包

$ npm install md5-node --save

第二步:使用这个包

const md5 = require("md5-node");

let str = "biaogege520";
let str1 = md5(str);

console.log((str1));

但是这一种试试有隐患,因为对于一些常见的密码即使加密以后,后面的一些网站也可以进行穷举解密,所以这个时候我们就要对我们的加密进行加盐

const md5 = require("md5-node");

let str = "123456";
let str1 = md5(str+"hjdlafhjsdlfh12312osd");

console.log(str1);

所谓的加盐其实就是在原有加密的基础字符串后面再添加一无规律的字符串,防止原来加密的文本太简单了

要求:新增管理员,管理员列表 ,删除管理员,编辑管理员信息,看一下登陆页面

三十八、登陆

在登陆的页面上面,我们要配置滑块验证码,然后再做表单验证,代码如下

<!DOCTYPE html>
<html lang="zh">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="renderer" content="webkit">
    <title>宿舍管理系统</title>
    <link rel="stylesheet" href="./css/bootstrap/css/bootstrap.min.css">
    <link rel="stylesheet" href="./css/bootstrap/font/bootstrap-icons.css">
    <!--[if lt ie 9]>
    <script src="./css/bootstrap/html5shiv.min.js"></script>
    <script src="./css/bootstrap/respond.min.js"></script>
    <![endif]-->
    <link rel="stylesheet" href="./js/jquery_slider/jquery.slider.css">
    <link rel="stylesheet" href="./css/login.css">
</head>

<body>
    <img src="./img/login_background.jpeg" class="bg-img" alt="">
    <div class="login-box px-4">
        <h2 class="text-light text-center pt-4">宿舍管理系统</h2>
        <form class="mt-4" id="form-login">
            <div class="form-group">
                <div class="input-group">
                    <div class="input-group-prepend">
                        <span class="input-group-text">账号</span>
                    </div>
                    <input type="text" id="zh" name="zh" class="form-control" placeholder="ID/手机号/邮箱"
                        data-rule-required="true" data-msg-required="账号不能为空">
                </div>
            </div>
            <div class="form-group">
                <div class="input-group">
                    <div class="input-group-prepend">
                        <span class="input-group-text">密码</span>
                    </div>
                    <input id="admin_pwd" name="admin_pwd" type="password" class="form-control" placeholder="密码"
                        data-rule-required="true" data-msg-required="密码不能为空">
                </div>
            </div>
            <div class="form-group" id="slider-v-code">
            </div>
            <div class="form-group row">
                <div class="col text-center">
                    <button type="button" id="btn-login" class="btn btn-info btn-block">登录系统</button>
                </div>
                <div class="col text-center">
                    <button type="button" class="btn btn-dark btn-block">忘记密码</button>
                </div>
            </div>
        </form>

    </div>
    <div class="copyright text-center font-weight-bold">
        <div>copyright softeem·杨标</div>
        <div>版权所有</div>
    </div>
</body>
<script src="./js/jquery-3.5.1.min.js"></script>
<script src="./css/bootstrap/js/bootstrap.min.js"></script>
<script src="./js/jquery.validate.js"></script>
<script src="./js/messages_zh.js"></script>
<script src="./js/message.min.js"></script>
<script src="./js/jquery_slider/jquery.slider.min.js"></script>
<script src="./js/base.js"></script>
<script>
    $(function() {
        $("#slider-v-code").slider({
            width: $("#slider-v-code").width(), // width
            height: 40, // height
            sliderBg: "rgb(134, 134, 131)", // 滑块背景颜色
            color: "#fff", // 文字颜色
            fontSize: 14, // 文字大小
            bgColor: "#7ecdb6", // 背景颜色
            textMsg: "按住滑块,拖拽验证", // 提示文字
            successMsg: "验证通过了哦", // 验证成功提示文字
            successColor: "red", // 滑块验证成功提示文字颜色
            time: 400, // 返回时间
            callback: function(result) { // 回调函数,true(成功),false(失败)
                if (result) {
                    $("#btn-login").one("click", function() {
                        checkLogin();
                    });
                }
            }
        });
        var validateForm = $("#form-login").validate({
            errorClass: "text-warning font-weight-bold",
            errorPlacement: function(errorElement, currentElement) {
                currentElement.parent().parent().append(errorElement);
            },
        });

        function checkLogin() {
            if (validateForm.form()) {
				checkLogin();
            } else {
                //让滑块还原
                $("#slider-v-code").slider("restore");
            }
        }
		 function checkLogin() {
                if (validateForm.form()) {
                    var loading = Qmsg.loading("正在登陆中....")
                    request.post("/adminInfo/checkLogin", {
                        zh: $("#zh").val(),
                        admin_pwd: $("#admin_pwd").val()
                    }).then(function(res) {
                        console.log(res);
                        if (res.status == "success") {
                            Qmsg.success("登陆成功");
                            location.replace("./adminIndex.html");
                        } else {
                            Qmsg.error("登陆失败,账号与密码不正确");
                        }
                    }).catch(function(error) {
                        Qmsg.error("服务器错误");
                    }).finally(function() {
                        loading.close();
                        $("#slider-v-code").slider("restore");
                    })
                } else {
                    //让滑块还原
                    $("#slider-v-code").slider("restore");
                }
            }
        });

    })
</script>

</html>

image.png
当一切准备好了以后,我们就开始准备后端的代码

adminInfoRoute.js

router.post("/checkLogin", async (req, resp) => {
    let results = await ServiceFactory.adminInfoService.checkLogin(req.body);
    if (results) {
        //登陆成功
        resp.json(new ResultJson(true, "登陆成功"));
    }
    else {
        //登陆失败
        resp.json(new ResultJson(false, "登陆失败,账号或密码不正确"));
    }
});

AdminInfoService.js

 /**
* @description 检测登陆
* @param {{zh,admin_pwd}} param 登陆的账号与密码 
* @returns {Promise<object>} 登陆查询的结果
*/
async checkLogin({ zh, admin_pwd }) {
    let strSql = ` select * from ${this.currentTableName} where isDel = false and admin_pwd = ? `
    admin_pwd = md5(admin_pwd + AppConfig.md5Salt);
    if (/\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*/.test(zh)) {
        //说明是邮箱
        strSql += ` and admin_email = ? `;
    }
    else if (/^(0|86|17951)?(13[0-9]|15[012356789]|166|17[3678]|18[0-9]|14[57])[0-9]{8}$/.test(zh)) {
        //说明是手机号
        strSql += ` and admin_tel = ? `;
    }
    else {
        //id
        strSql += ` and id = ? `;
    }
    let results = await this.executeSql(strSql, [admin_pwd, zh]);
    return results[0];
}

在上面的代码里面,我们可以看到对账号进行正则表达式的验证来区分它是邮箱,还是手机号还是真正的ID

image.png

三十九、http请求头与请求主体

现在我们使用的是前后端分离开发方式,在这一种开发方式里面,所有的数据都是通过ajax来进行请求的,并且我们在后端也取消了跨域的限制条件,开启了CORS,所以任何人应该都可以通过一个路径来请求到后端的数据,如下

http://192.168.1.254:8080/roomInfo/getListByPage?room_name=&pageIndex=1

任何人都可以通过上面的地址来请求我的数据,这个时候数据就很不安全。我们在上面的模块里面已经接触到了登录,我们现在希望这些数据是登录以后才能请求到的,怎么办呢?

这个时候我们就深入的去分析一下HTTP的请求协议

在现行的B/S(B:Browser,S:Service)架构开发里面,数据的交互方式多半采用http/https来进行传输,但使用这一种协议进行传值的时候会产生一个会话session,在这个传话的session内部有两个大家所熟悉的对象,一个是request,一个是response

image.png

四十、JWT鉴权【重难点】

根据上面的分析,我们不能让所有的请求随意的进入到服务器,我们要对进入到服务器的请求执行拦截操作

**第一步:AppConfig.js里面,我们设置了要排队的路径,也就是不做验证的做路径 **

const AppConfig = {
    md5Salt: "oausdfpasdfja_)qlwke",
    // 设置一个数组,里面设置的路径 ,外边都不要拦下来
    excludePath: [
        /\/adminInfo\/checkLogin/
    ]
}

module.exports = AppConfig;

第二步:放开预检请求options

image.png

因为之前的Ajax涉及到了跨域的问题,浏览器于对于所有的跨域请求都会执行预检操作(发起一个options请求),对于这个操作,我也要放行

image.png

第三步:发送token命令

目前token发放比较好的第三方包就是典型的jsonwebtoken,简称JWT,它是一个第三方的包

先安装这个包

$ npm install jsonwebtoken --save

登录成功以后发送token

router.post("/checkLogin", async (req, resp) => {
    let results = await ServiceFactory.adminInfoService.checkLogin(req.body);
    if (results) {
        //登陆成功
        //发放token
        let token = jwt.sign({
            adminInfo: results
        }, AppConfig.jwtKey, {
            expiresIn: 60 * 30
        });
        resp.json(new ResultJson(true, "登陆成功", token));
    }
    else {
        //登陆失败
        resp.json(new ResultJson(false, "登陆失败,账号或密码不正确"));
    }
});

因为token是可以解密的,所以我们需要设置加盐,也就是加一个密钥

AppConfig.js文件里面

const AppConfig = {
    md5Salt: "oausdfpasdfja_)qlwke",
    // 设置一个数组,里面设置的路径 ,外边都不要拦下来
    excludePath: [
        /\/adminInfo\/checkLogin/
    ],
    // JWT颁发令牌的解钥
    jwtKey: "alkdsfhalkjsdfhakld123"
}

module.exports = AppConfig;

最后当我们登陆的时候,我们就可以看到如下的结果了

{
    data: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbkluZm8iOnsiaWQiOjEwMDA0LCJhZG1pbl9uYW1lIjoi5p2o5qCHIiwiYWRtaW5fc2V4Ijoi55S3IiwiYWRtaW5fdGVsIjoiMTg2MjcxMDk5OTkiLCJhZG1pbl9wd2QiOiJlODJiZTQ5YTI2MzNlYWJmY2JkNDM2ZWM0ZTRhODA0MSIsImFkbWluX2VtYWlsIjoibG92ZXNuc2ZpQDE2My5jb20iLCJhZG1pbl9waG90byI6Ii9hZG1pblBob3RvLzllOWU3ZTYwYTg5Y2M4ZWIxMTUzODJkMzg2NjkyYTBiMjAxOOS4ii5qcGciLCJhZG1pbl9hZGRyZXNzIjoi5rmW5YyX55yBIOatpuaxieW4giDmsZ_lpI_ljLog5YWJ6LC35aSn6YGT6YeR6J6N5rivQjI35qCLIiwiaXNEZWwiOjB9LCJpYXQiOjE2MzUzMjU1MjYsImV4cCI6MTYzNTMyNzMyNn0.kYI0q8hEExhpZcJvZBAiYHA8hAZ22QHOCzihJ94exhs"
	msg: "登陆成功"
	status: "success"
}

上面的data就是服务颁发给我们的token

当我们得到这个token以后,我们在以后的请求当中就需要把这个token携带到服务器去,读取问题的是放在请求的什么地方呢?

根据上我们在上一个章节讲HTTP的请求协议里面,我们可以看到,最先到达服务器的一定是header

说一具很形象的话,把每次请求看成是一辆小车,token相当于车牌,应该放在车头

image.png

在上面的截图里面,我们把token放在了sessionStroage里面,这样在不关闭浏览器的情况之下, 我们就可以一直使用token,关键以后就会自动的消失

为Ajax的每次请求都携带token

现在所有的数据请求都是基于Ajax的,所以我要在ajax请求的时候携带

var request = {
    get: function (url, data) {
        return new Promise(function (resolve, reject) {
            $.ajax({
                url: baseURL + url,
                method: "get",
                data: data,
                beforeSend: function (xhr) {
                    //检测一下sessionStorage有没有rental_house_token这个缓存
                    var rental_house_token = sessionStorage.getItem("rental_house_token");
                    if (rental_house_token) {
                        xhr.setRequestHeader("rental_house_token", rental_house_token);
                    }
                    return xhr;
                },
                success: function (res) {
                    resolve(res);
                },
                error: function (error) {
                    reject(error);
                },
            });
        });
    },
    //POST请求是一样的,此处省略代码
}

当然我们设置上面的代码,携带token去发送请求的时候,又有下面新的错误

image.png

image.png
这是因为我们之前只允许改变Content-Type的header,而现在我们改变了 另一个header

image.png
我们改变了rental_house_token

为了解决上面的问题,我们把之前的跨域的设置如下

//设置一个拦截器,添加响应头,实现CORS
app.use((req, resp, next) => {
    resp.setHeader("Access-Control-Allow-Headers", "rental_house_token,*");
    resp.setHeader("Access-Control-Allow-Origin", req.headers.origin || "*");
    resp.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS");
    next();
});

最后在后面的拦截器里面进行拦截,注意,拦截的时候一定是在路由之前

//再次设置一个拦截器,拦截所有的请求,看它有没有令牌
app.use((req, resp, next) => {
    let pathValidate = AppConfig.excludePath.some(item => item.test(req.path));
    if (pathValidate) {
        //说明这个路径 应该放行
        next();
    }
    else {
        //不在排除的路径以外,这个时候要进一步的检测
        if (req.method.toUpperCase() === "OPTIONS") {
            //说明是预检请求,也要放行
            next();
        }
        else {
            //拿到header里面的token
            let token = req.headers.rental_house_token;
            if (token) {
                //有token
                jwt.verify(token, AppConfig.jwtKey, (error, decoded) => {
                    if (error) {
                        resp.status(403).json(new ResultJson(false, "当前token无效"));
                    }
                    else {
                        console.log("当前解密以后的东西就是");
                        console.log(decoded);
                        next();
                    }
                });
            }
            else {
                resp.status(403).json(new ResultJson(false, "当前请求无权限"));
            }
        }
    }
});

在上面的代码里面,我们看到token在验证成功以后,会解密出一个对象,这个对象的格式如下

{
  adminInfo: {
    id: 10004,
    admin_name: '杨标',
    admin_sex: '男',
    admin_tel: '18627109999',
    admin_pwd: 'e82be49a2633eabfcbd436ec4e4a8041',
    admin_email: 'lovesnsfi@163.com',
    admin_photo: '/adminPhoto/9e9e7e60a89cc8eb115382d386692a0b2018上.jpg',
    admin_address: '湖北省 武汉市 江夏区 光谷大道金融港B27栋',
    isDel: 0
  },
  iat: 1635327437,
  exp: 1635329237
}

这里面的东西就是我们之前在颁发令牌的时候的东西

四十一、ajax响应拦截【重点】

在上面的请求过程当中,我们的每一次请求都会携带token到服务器,如果验证通过就会得到数据,通不过就会返回状态403,所以403就一定是没有权限,而ajax的请求里面200才算成功,其它 的都算错误,所以正是因为有这个特点,我们可以在ajax响应的时候做一次拦截处理

/**
 * 将原来jQuery里面的ajax封装成一个Promise的处理方式
 */
var request = {
    get: function (url, data) {
        return new Promise(function (resolve, reject) {
            $.ajax({
                url: baseURL + url,
                method: "get",
                data: data,
                beforeSend: function (xhr) {
                    //检测一下sessionStorage有没有rental_house_token这个缓存
                    var rental_house_token = sessionStorage.getItem("rental_house_token");
                    if (rental_house_token) {
                        xhr.setRequestHeader("rental_house_token", rental_house_token);
                    }
                    return xhr;
                },
                success: function (res) {
                    resolve(res);
                },
                error: function (error) {
                    //关键就是这里
                    if (error.status == 403) {
                        top.location.replace("/login.html")
                    }
                    reject(error);
                },
            });
        });
    },
    //POST请求是一样的,所以省略代码.....
}

image.png

四十二、编辑管理员

四十三、数据可视化echarts【新型应用点】

数据可视化并不是一个新的技术,它只是一个新的叫法,在以前叫echarts图形图表,就是把数据用图表的方式展示出来

要实现数据可视化的技术,目前主要使用的有以下几个框架

  1. echarts百度图形图表Apache ECharts
  2. hicharts图形图表兼容 IE6+、完美支持移动端、图表类型丰富的 HTML5 交互图表 | Highcharts

因为hicharts的使用需要授权,所以我们使用echarts来完成

image.png

新建了一个路由,完成相关的数据操作

const express = require("express");
const router = express.Router();
const ServiceFactory = require("../factory/ServiceFactory.js");
const ResultJson = require("../model/ResultJson");

router.get("/getTotalData", async (req, resp) => {
    let results = await ServiceFactory.dataViewService.getTotalData();
    if (results.length > 0) {
        resp.json(new ResultJson(true, "数据请求成功", results[0]));
    }
    else {
        resp.json(new ResultJson(false, "请求不到数据"));
    }
});

router.get("/getRoomCategoryData", async (req, resp) => {
    let results = await ServiceFactory.dataViewService.getRoomCategoryData();
    if (results.length > 0) {
        resp.json(new ResultJson(true, "数据请求成功", results));
    }
    else {
        resp.json(new ResultJson(false, "请求不到数据"));
    }
});


router.get("/getAreaTotalCount", async (req, resp) => {
    let results = await ServiceFactory.dataViewService.getAreaTotalCount();
    if (results.length > 0) {
        resp.json(new ResultJson(true, "数据请求成功", results));
    }
    else {
        resp.json(new ResultJson(false, "请求不到数据"));
    }
});
module.exports = router;

然后再新建一个service,完成数据库的操作

/**
 * @description 不操作任何单独的表
 * @author 杨标
 * @version 1.0
 */

const BaseServcie = require("./BaseService.js");
const PageList = require("../model/PageList.js");

class DataViewService extends BaseServcie {
    constructor() {
        super();
    }

    /**
     * @description 一次性获取管理员,学生,房间的总数
     * @returns {Promise<[]>} 返回查询结果 
     */
    getTotalData() {
        let strSql = `
            select 
            max(
                case when t1.admin_total_count ='admin_total_count' then total_count end
            ) 'admin_total_count',
            max(
                case when t1.admin_total_count ='stu_total_count' then total_count end
            ) 'stu_total_count',
            max(
                case when t1.admin_total_count ='room_total_count' then total_count end
            ) 'room_total_count'
            from 
            (select 'admin_total_count',count(*) 'total_count' from admin_info where isDel = false
            UNION ALL
            SELECT 'stu_total_count' , count(*) 'total_count' from stu_info where isDel = false
            UNION ALL
            SELECT 'room_total_count', count(*) 'total_count' from room_info where isDel = false) t1
        `;
        return this.executeSql(strSql);
    }
    /**
     * @description 获取房间的分类信息
     * @returns {Promise<[]>} 获取房间的分类信息
     */
    getRoomCategoryData() {
        let strSql = `
        select t1.room_type,count(*) 'total_count' from 
            (select SUBSTR(room_name,3,1) 'room_type' from room_info where isDel = false ) t1
            group by t1.room_type
        `;
        return this.executeSql(strSql);
    }

    /**
     * @description 获取房间信息里面分区域汇总的图
     * @returns {Promise<[]>} 返回查询结果
     */
    getAreaTotalCount() {
        let strSql = `
        select t1.q_name,count(*) 'q_count' from 
        (select left(room_name,1) 'q_name' from room_info where isDel = false) t1
        group by t1.q_name;        
        `;
        return this.executeSql(strSql);
    }
}
module.exports = DataViewService;

这里要注意一点,所有的Service新建 以后都是了要在ServiceFactory里面添加注释 ,这样写代码就会有提示,同时要注意添加代码注释的格式

  • 首字母是小写的
  • 注意路径
/**
 * @typedef ServiceFactoryType
 * @property {import("../service/RoomInfoService.js")} roomInfoService
 * @property {import("../service/AreaService.js")} areaService
 * @property {import("../service/AdminInfoService")} adminInfoService
 * @property {import("../service/DataViewService")} dataViewService
 */

最后是前端的代码了

<!-- 图形图表的部分 -->
<div class="card mt-2">
    <div class="card-header h4 text-info">
        <i class="bi bi-house-door-fill"></i>
        房间信息
    </div>
    <div class="card-body">
        <div class="row">
            <div class="col-sm-6">
                <div id="echarts1" style="width: 100%;min-height: 500px;"></div>
            </div>
            <div class="col-sm-6">
                <div id="echarts2" style="width: 100%;min-height: 500px;"></div>
            </div>
        </div>
    </div>
</div>
<script>
	//省略了部分代码
    $(function(){
       //百度的图形图表在使用之前一定要先导入它的js文件,然后初始化区域
        var echarts1 = echarts.init(document.getElementById('echarts1'));
        request.get("/dataView/getRoomCategoryData").then(function(res) {
            if (res.status == "success") {
                // 指定图表的配置项和数据
                var option1 = {
                    color: ["#d32f2f"],
                    title: {
                        text: '房间类型图'
                    },
                    tooltip: {},
                    legend: {
                        data: ['数量']
                    },
                    xAxis: {
                        data: []
                    },
                    yAxis: {},
                    series: [{
                        name: '数量',
                        type: 'bar',
                        data: []
                    }]
                };
                let nameObj = {
                    s: "单间",
                    b: "标间",
                    t: "套间"
                };
                for (var i = 0; i < res.data.length; i++) {
                    option1.xAxis.data.push(nameObj[res.data[i].room_type]);
                    option1.series[0].data.push(res.data[i].total_count);
                }
                echarts1.setOption(option1);
            }
        }).catch(function(error) {
            Qmsg.error("服务器错误");
            console.log(error);
        });


        var echarts2 = echarts.init(document.getElementById('echarts2'));
        request.get("/dataView/getAreaTotalCount").then(function(res) {
            var option2 = {
                title: {
                    text: '房间区域分图',
                    subtext: '按区域来划分',
                    left: 'center'
                },
                tooltip: {
                    trigger: 'item'
                },
                legend: {
                    orient: 'vertical',
                    left: 'left'
                },
                series: [{
                    name: '当前信息',
                    type: 'pie',
                    radius: '50%',
                    data: [],
                    emphasis: {
                        itemStyle: {
                            shadowBlur: 10,
                            shadowOffsetX: 0,
                            shadowColor: 'rgba(0, 0, 0, 0.5)'
                        }
                    }
                }]
            };
            option2.series[0].data = res.data.map(function(item) {
                return {
                    name: item.q_name,
                    value: item.q_count
                }
            });
            echarts2.setOption(option2);
        }).catch(function(error) {
            Qmsg.error("服务器错误");
            console.log(error);
        }); 
    });
</script>

四十四、主外键关联操作【重点】

image.png

我们现有了学生表与房间表,这个时候就要构建主外键关系,如上图所示

image.png

上图就是我们做的第一个版本,这个版本有个问题,不能看到当前的房间里面有多少人

这个时候我们可以在RoomInfoService里面编写如下的方法

/**
     * @description 获取没有住东江环保的房间
     * @returns {Promise<[]>}
*/
getNotFullRoomList() {
    let strSql = ` select * from  
(select *,
(select count(*) from ${this.tableMap.stu_info} where rid = room_info.id) 'real_count'
from ${this.tableMap.room_info} where isDel = FALSE) t1
where t1.real_count<t1.max_count
`;
    return this.executeSql(strSql);
}
0

评论区