目 录CONTENT

文章目录

宿舍管理系统(一)

Administrator
2021-11-18 / 0 评论 / 3 点赞 / 5608 阅读 / 59590 字

宿舍管理系统

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

项目架构图

image.png

一、初始化项目

$ npm init --yes

二、安装项目依赖

$ npm install mysql express --save

三、创建express项目

const express = require("express");
const http = require("http");
const app = express();
const server = http.createServer(app);


server.listen(8080, "0.0.0.0", () => {
    console.log("服务器启动成功");
})

四、根据模块创建路由

根据上面的架构图我们要创建三个路由模块

  1. 创建routes目录
  2. 创建adminInfoRoute.js,roomInfoRoute.jsStuInfoRoute.js

当前我们以routeInfoRoute.js为例来做演示

/**
 * @author 杨标
 * @description roominfo的路由模块 
 */
const express = require("express");
const router = express.Router();

module.exports = router;

接下来在app.js里面使用一级路径来连接路由文件

app.use("/roomInfo", require("./routes/roomInfoRoute.js"));

五、完成房间显示的路由请求

需求:查询所有的房间信息

我们现在希望在roomInfoRoute.js里面完成一个请求功能,当请求一个路径的时候,把房间信息显示出来

router.get("/roomInfoList", (req, resp) => {
    //把数据以json的形式返回出来
    //数据在数据库,所以要找数据库拿数据
    //数据由Servcier,所以我们要找Service
})

六、封装mysql的操作过程

  1. 新建一个utils的文件夹
  2. 在文件夹下面新建一个DBUtils.js文件,封装相应的代码
/**
 * @description 封装数据库操作相关的东西
 * @version 1.0
 * @author 杨标
 */
const mysql = require("mysql");
class DBUtils {
    /**
     * @description 获取一个数据库连接 
     * @returns {mysql.Connection} 返回数据库连接 
     */
    getConn() {
        let conn = mysql.createConnection({
            host: "127.0.0.1",
            port: 3306,
            user: "dev_h2103",
            password: "123456",
            database: "rental_house"
        });
        return conn;
    }
    /**
     * @description 执行一条SQL语句
     * @param {string} strSql  要执行的SQL语句
     * @param {[]} ps 执行SQL语句的参数 
     * @returns {Promise<[]|mysql.OkPacket>} 数据库执行的结果
     */
    executeSql(strSql, ps = []) {
        return new Promise((resolve, reject) => {
            let conn = this.getConn();
            conn.connect();
            conn.query(strSql, ps, (error, results) => {
                if (error) {
                    reject(error);
                }
                else {
                    resolve(results)
                }
                conn.end();
            });
        });
    }
}
module.exports = DBUtils;

七、根据模块构建BaseService对象

因为有三个数据表我们需要三个数据服务对象,但是根据我们的架构图,我们需要一个BaseService

  1. 创建servcie目录
  2. 创建BaseService.js文件
/**
 * @description 基础的服务对象
 * @author 杨标
 * @description 1.0
 */


const DBUtils = require("../utils/DBUtils.js")
class BaseServcie extends DBUtils {
    constructor() {
        super();
        this.tableMap = {
            room_info: "room_info",
            admin_info: "admin_info",
            stu_info: "stu_info"
        }
        this.currentTableName = "";
    }
    /**
     * @description 根据一个主键id查询信息
     * @param {number} id 主键 
     * @returns {Promise<object>} 查询结果
     */
    async findById(id) {
        let strSql = ` select * from ${this.currentTableName} where id = ? `;
        let results = await this.executeSql(strSql, [id]);
        return results[0];
    }

    /**
     * @description 根据一个主键删除信息 
     * @param {number} id 主键  
     * @returns {Promise<boolean>} true代表删除成功,false代表删除失败
     */
    async deleteById(id) {
        let strSql = ` delete from ${this.currentTableName} where id = ? `;
        let results = await this.executeSql(strSql, [id]);
        return results.affectedRows > 0 ? true : false;
    }

    /**
     * @description 获取所有数据
     * @returns {Promise<[]>} 返回查询结果 
     */
    getAllList() {
        let strSql = ` select * from ${this.currentTableName} `;
        return this.executeSql(strSql);
    }
}
module.exports = BaseServcie;

八、构建RoomInfoService对象

根据之前的架构图,我们现在是要操作room_info的数据表,为前面的路由提供服务

RoomInfoService.js文件

/**
 * @description 操作room_info数据表
 * @author 杨标
 * @version 1.0
 */
const BaseServcie = require("./BaseService.js");

class RoomInfoService extends BaseServcie {
    constructor() {
        super();
        this.currentTableName = this.tableMap.room_info;
    }
}
module.exports = RoomInfoService;

九、创建抽象服务工厂

  1. 先创建一个factory的文件夹
  2. 在文件夹下面创建一个ServiceFactory.js
/**
 * @description 抽象服务工厂
 * @author 杨标
 * @version 1.0
 */

const path = require("path");
const fs = require("fs");

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


/**
 * @type {ServiceFactoryType}
 */
const ServiceFactory = (() => {
    let obj = {};
    let arr = fs.readdirSync(path.join(__dirname, "../service"));
    for (let item of arr) {
        let properyName = item.replace(".js", "").replace(/^[A-Z]/, p => p.toLowerCase());
        let temp = require(path.join(__dirname, "../service", item));
        if (typeof temp === "function") {
            obj[properyName] = Reflect.construct(temp, []);
        }
    }
    return Object.freeze(obj);
})();
module.exports = ServiceFactory;

十、完成room_info路由是中获取所有信息的操作

/**
 * @author 杨标
 * @description roominfo的路由模块 
 */
const express = require("express");
const router = express.Router();
const ServiceFactory = require("../factory/ServiceFactory.js");

router.get("/roomInfoList", async (req, resp) => {
    let results = await ServiceFactory.roomInfoService.getAllList();
    resp.json(results);
});

module.exports = router; 

十一、封装返回的JSON数据类型

我们现在是使用express来构建了一个服务器,但是有可能服务器会报相应的错误,所以我们需要处理异常情况,对于异常情况,我们也可以使用json来返回,这个时候我们返回前端的格式就要统一

//数据请求成功
{
    status:"success",
    msg:"数据请求成功",
    data:[]  //这里放真正的数据
}

//请求失败
{
    status:"fail",
    msg:"请求失败",
    data:[]
}
  1. 在当前项目下面创建一个model的文件夹
  2. 创建一个ResultJson的文件,代码如下
/**
 * @description 返回给客户端的json数据模型
 * @author 杨标
 * @version 1.0
 */

class ResultJson {
    constructor(flag, msg, data = []) {
        this.status = flag ? "success" : "fail";
        this.msg = msg;
        this.data = data;
    }
}
module.exports = ResultJson;

十二、完成前端页面

前端页面我们使用bootstrap4,同时使用art-template模板引擎

<!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]-->
</head>

<body>

</body>
<script src="./js/jquery-3.5.1.min.js"></script>
<script src="./js/template-web.js"></script>
<script src="./css/bootstrap/js/bootstrap.min.js"></script>

</html>

以上就是bootstrap4的导入项

当我们使用ajax去请求数据的时候,如下所示

$(function() {
    $.get(baseURL + "/roomInfo/roomInfoList", function(res) {
        console.log(res);
    });
});

image.png

这个问题是因为AJAX不允许请求跨域的数据产生的

  • 当前网页的地址为http://127.0.0.1:5500/roomInfoList.html

    它的域是http://127.0.0.1:5000

  • ajax请求的目标服务器地址为http://192.168.1.254:8080/roomInfo/roomInfoList

    它的域是http://192.168.1.254:8080

两个域不相同,所以ajax就会请求失败

十三、解决Ajax跨域的问题

ajax跨域的问题是一个非常常见的问题,也是必须要解决的问题,它有解决方式有很多

  1. 反射代理
  2. 服务端添加响应头实现CORS
  3. jsonp

本次我们即有前端,又有后端,所以我们采用第2种方式去完成

image.png

我们参考了别人的网站以后,我们发现,只要在服务器上面添加上面的这三个东西就可以解决跨域的问题

这个三个东西是在服务端加的

resp.setHeader("Access-Control-Allow-Headers", "Content-Type");
resp.setHeader("Access-Control-Allow-Origin", "*");
resp.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS");

上面的三个方式设置好了以后,Ajax就可以跨域了,这种设置方式,我们叫CORS(跨域资源共享)

十四、设置CORS的拦截器

因为所有的请求都要添加响应头,我们可以借用于express里面的拦截器来实现

//设置一个拦截器
app.use((req, resp, next) => {
    resp.setHeader("Access-Control-Allow-Headers", "Content-Type");
    resp.setHeader("Access-Control-Allow-Origin", "*");
    resp.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS");
    next();
});

//一定是要设置在路由之前

十五、查询功能的完成

查询的时候,我们需要先获取页面上面输入的查询参数,然后将参数发送给服务器

要完成这个功能,需要单独设置一个点,就是服务器如何接收前端传过来的数据

$("#btn-query").on("click", function() {
 $.get(baseURL + "/roomInfo/getListByPage?room_name=" + $("#room_name").val(), function(res) {
        console.log(res);
    });
});

在上面的代码里面,我们对查询按钮绑定了一个点击的事件,当点击的时候向后台的服务器发起一个Ajax的请求,然后在这个地址的后面通过search的方式传递参数到服务器

服务器就可以通过req.query的方式接收到get请求携带过来的参数

router.get("/getListByPage", (req, resp) => {
    try {
        console.log(req.query);
        //{ room_name: '桃花菀' }
        //把值交给service,让service帮我查
        resp.send("我收到你的查询请求了");
    } catch (error) {
        resp.status(500).json(new ResultJson(false, error));
    }
});

当我们接收到这个参数以后就要交给service去操作数据库

RoomInfoService.js文件

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

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

class RoomInfoService extends BaseServcie {
    constructor() {
        super();
        this.currentTableName = this.tableMap.room_info;
    }
    /**
     * 分页查询
     * @param {{room_name}} param 查询参数
     * @returns {Promise<[]>} 返回查询结果
     */
    getListByPage({ room_name }) {
        let strSql = ` select * from ${this.currentTableName} where 1 `;
        let ps = []
        if (room_name) {
            strSql += ` and room_name like ? `;
            ps.push(`%${room_name}%`);
        }
        return this.executeSql(strSql, ps);
    }
}
module.exports = RoomInfoService;

当我们的Service已经完成了以后,我们又可以回到路由 去使用这个Service获取数据

roomInfoService.js文件

router.get("/getListByPage", async (req, resp) => {
    try {
        console.log(req.query);
        //{ room_name: '桃花菀'}
        //把值交给service,让service帮我查
        let results = await ServiceFactory.roomInfoService.getListByPage(req.query);
        resp.json(new ResultJson(true, "获取数据成功", results));
    } catch (error) {
        resp.status(500).json(new ResultJson(false, error));
    

前端的页面经过ajax请求以后就可以得到数据,得到数据以后,重新渲染页面

【前端页面】

//查询
$("#btn-query").on("click", function() {
  Qmsg.loading("正在请求数据...");
  $.get(baseURL + "/roomInfo/getListByPage?room_name=" + $("#room_name").val(), function(res) {
        Qmsg.closeAll();
        if (res.status == "success") {
            //数据请求成功
            Qmsg.success("请求数据成功");
            let htmlStr = template("temp1", {
                roomInfoList: res.data
            });
            $("#table-roomInfo>tbody").html(htmlStr);

        } else {
            //数据请求失败
            Qmsg.error("请求数据失败");
        }
    });
});

十六、ajax请求的封装

/**
 * 网页里面的JS不要瞎写,有兼容性的
 * 不像nodejs,所以最好是有ES5为主
 */

var baseURL = "http://127.0.0.1:8080";

/**
 * 将原来jQuery里面的ajax封装成一个Promise的处理方式
 */
var request = {
    get: function (url, data) {
        return new Promise(function (resolve, reject) {
            $.ajax({
                url: baseURL + url,
                method: "get",
                data: data,
                success: function (res) {
                    resolve(res);
                },
                error: function (error) {
                    reject(error);
                }
            });
        });
    },
    post: function (url, data) {
        return new Promise(function (resolve, reject) {
            $.ajax({
                url: baseURL + url,
                method: "post",
                data: data,
                success: function (res) {
                    resolve(res);
                },
                error: function (error) {
                    reject(error);
                }
            });
        });
    }
}

十七、构建分页

image.png

我们现在如果希望构建上面的分页信息展示,怎么办呢?

我们现在已经知道分页的多条SQL语句执行的结果如下

let results = [
  [
    RowDataPacket {
      id: 1,
      room_name: '西-s-0751',
      max_count: 2,
      kt: 1,
      network: 1,
      washroom: 0,
      room_size: 191.25
    },
    //.........
  ],
  [ 
      RowDataPacket { 
      	total_count: 56 
      } 
  ]
]

在上面的结果 里面,第一个数组代表的就是第一条SQL语句查询的结果,我们可以通过results[0]得到列表的结果,第二条SQL语句可以通过results[1]得到结果,所以我们可以通过results[1][0].total_count得到结果

页整个页面需要构建的数据如正是

  • 当前第几页pageIndex
  • 共多少页pageCount
  • 共多少条totalCount

我们在model的文件夹下面创建了一个PageList的对象,如下所示

class PageList {
    /**
     * 
     * @param {number} pageIndex 当前第几页
     * @param {number} totalCount 共多少条数据
     * @param {number} pageSize  每页显示多少
     * @param {[]} listData 查询的分页列表数据 
     */
    constructor(pageIndex, totalCount, pageSize, listData) {
        this.pageIndex = pageIndex;
        this.totalCount = totalCount;
        this.pageCount = Math.ceil(totalCount / pageSize);
        this.listData = listData;
    }
}
module.exports = PageList;

最后在getListByPage里面完成如下代码

/**
     * 分页查询
     * @param {{room_name,pageIndex}} param 查询参数
     * @returns {Promise<PageList>} 返回分页查询以后的封装的PageList
*/
async getListByPage({ room_name, pageIndex }) {
    let strSql = ` select * from ${this.currentTableName} where 1 `;
    let countSql = ` select count(*) 'total_count' from ${this.currentTableName} where 1 `;
    let strWhere = ` `;
    let ps = []
    if (room_name) {
        strWhere += ` and room_name like ? `;
        ps.push(`%${room_name}%`);
    }
    strSql += ` ${strWhere} limit ${(pageIndex - 1) * this.pageSize} , ${this.pageSize} `;
    countSql += strWhere;
    let results = await this.executeSql(strSql + ";" + countSql, [...ps, ...ps]);
    let pageList = new PageList(pageIndex, results[1][0].total_count, this.pageSize, results[0]);
    return pageList;
}

roomInfoRoute.js文件

router.get("/getListByPage", async (req, resp) => {
    try {
        //{ room_name: '', pageIndex: '1' }
        let pageList = await ServiceFactory.roomInfoService.getListByPage(req.query);
        resp.json(new ResultJson(true, "获取数据成功", pageList));
    } catch (error) {
        console.log(error);
        resp.status(500).json(new ResultJson(false, error));
    }
});

roomInfoList.html文件

<!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="./css/message.css">
</head>

<body>
    <div class="container-fluid">
        <ul class="breadcrumb">
            <li class="breadcrumb-item"><a href="#">首页</a></li>
            <li class="breadcrumb-item active">房间信息列表</li>
        </ul>
        <form class="form-inline">
            <div class="form-group">
                <label for="" class="control-label">房间编号</label>
                <input id="room_name" type="text" class="form-control" placeholder="输入房间编号查询">
            </div>
            <div class="form-group ml-2">
                <button id="btn-query" type="button" class="btn btn-success">
                    <span class="bi-search"></span>
                    查询
                </button>
            </div>
            <div class="form-group ml-2">
                <button type="button" class="btn btn-info">
                    <span class="bi-plus-circle"></span>
                    新增房间
                </button>
            </div>
            <div class="form-group ml-2">
                <button type="button" class="btn btn-dark">
                    <span class="bi-file-earmark-excel"></span>
                    导出为excel
                </button>
            </div>
        </form>
        <!-- 表格 -->
        <div class="table-responsive mt-1">
            <table id="table-roomInfo" class="table table-hover table-striped table-bordered">
                <thead>
                    <tr class="bg-info text-light">
                        <th>
                            <input type="checkbox">全选
                        </th>
                        <th>房间编号</th>
                        <th>最大人数</th>
                        <th class="text-center">是否有空调</th>
                        <th class="text-center">是否有网络</th>
                        <th class="text-center">独立卫生间</th>
                        <th>房间面积 (m<sup>2</sup>)</th>
                        <th>操作</th>
                    </tr>
                </thead>
                <tbody>

                </tbody>
            </table>
        </div>
        <div class="row">
            <div class="col" id="page-list-info">

            </div>
            <div class="col">
                <ul class="pagination justify-content-end">
                </ul>
            </div>
        </div>

    </div>
    <script type="text/html" id="temp1">
    {{each roomInfoList item index}}
    <tr>
        <td>
            <input type="checkbox">
        </td>
        <td>{{item.room_name}}</td>
        <td>{{item.max_count}}</td>
        <td class="text-center">
            <span class="p-1 text-light rounded {{item.kt==0?'bg-danger':'bg-success'}}">
                {{item.kt==0?"无":"有"}}
            </span>
        </td>
        <td class="text-center">
            <span class="p-1 text-light rounded {{item.network==0?'bg-danger':'bg-success'}}">
                {{item.network==0?"无":"有"}}
            </span>
        </td>
        <td class="text-center">
            <span class="p-1 text-light rounded {{item.washroom==0?'bg-danger':'bg-success'}}">
                {{item.washroom==0?"无":"有"}}
            </span>
        </td>
        <td>{{item.room_size}}</td>
        <td>
            <button type="button" class="btn btn-warning btn-sm">编辑</button>
            <button type="button" class="btn btn-danger btn-sm">删除</button>
        </td>
    </tr>
    {{/each}}
    </script>
    <!-- 渲染分页的页码 -->
    <script type="text/html" id="temp2">
        <li data-index="1" class="page-item"><a href="#" class="page-link">首页</a></li>
        <%for(var i=1;i<=pageCount;i++){%>
        <li data-index="{{i}}" class="page-item {{i==pageIndex?'active':null}}"><a class="page-link" href="#">{{i}}</a></li>
        <%}%>
        <li data-index="{{pageCount}}" class="page-item"><a href="#" class="page-link">尾页</a></li>
    </script>
    <script type="text/html" id="temp3">
        当前第{{pageIndex}}页,共{{pageCount}}页,共{{totalCount}}条
    </script>
</body>
<script src="./js/jquery-3.5.1.min.js"></script>
<script src="./js/template-web.js"></script>
<script src="./css/bootstrap/js/bootstrap.min.js"></script>
<script src="./js/base.js"></script>
<script src="./js/message.min.js"></script>
<script>
    $(function() {
        function getData(pageIndex) {
            var loading = Qmsg.loading("正在加载数据...");
            request.get("/roomInfo/getListByPage", {
                room_name: $("#room_name").val(),
                pageIndex: pageIndex
            }).then(function(res) {
                console.log(res);

                if (res.status == "success") {
                    Qmsg.success("获取数据成功");
                    //渲染表格
                    var htmlStr = template("temp1", {
                        roomInfoList: res.data.listData
                    });
                    $("#table-roomInfo>tbody").html(htmlStr);
                    // 渲染页面
                    var htmlStr2 = template("temp2", {
                        pageCount: res.data.pageCount,
                        pageIndex: res.data.pageIndex
                    });
                    $(".pagination").html(htmlStr2);
                    //渲染左边的文字信息
                    var htmlStr3 = template("temp3", {
                        pageIndex: res.data.pageIndex,
                        pageCount: res.data.pageCount,
                        totalCount: res.data.totalCount
                    });
                    $("#page-list-info").html(htmlStr3);
                } else {
                    Qmsg.error("获取数据失败");
                }
            }).catch(function(error) {
                console.log(error);
                Qmsg.error("服务器错误,请联系管理员");
            }).finally(function() {
                //无论是成功进入then还是失败进入catch,最后都会进入到finally
                loading.close();
            });
        }
        getData(1);
        $("#btn-query").on("click", function() {
            getData(1);
        })

        $(".pagination").on("click", "li", function(event) {
            var index = $(this).attr("data-index");
            getData(index);
        });

    });
</script>

</html>

十八、配置服务器POST请求

第一步:装包

post请求是需要第三方包来完成的

$ npm install body-parser --save

**第二步:在app.js中配置post请求接收值的方式 **

//配置post接收参数
const bodyParser = require("body-parser");
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json({ limit: "30mb" }));

第三步:接收值使用req.body

router.post("/add",async(req,resp)=>{
    //接收值
   	console.log(req.body); 
});

十九、新增房间的功能【POST请求】

按照我们相应的开发规范,如果有一个新的模块出来的时候应该是先后台,再前台

首先在后台的项目里,完成Service的功能

RoomInfoService.js文件里面

/**
* @description 新增房间数据
* @param {{ room_name, max_count, kt, network, washroom, room_size }} param 新增的删除
* @returns {Promise<boolean>} true代表新增成功,false代表新增失败
*/
async add({ room_name, max_count, kt, network, washroom, room_size }) {
    let strSql = ` INSERT INTO ${this.currentTableName} (room_name, max_count, kt, network, washroom, room_size) VALUES (?, ?, ?, ?, ?, ?); `;
    let results = await this.executeSql(strSql, [room_name, max_count, kt, network, washroom, room_size]);
    return results.affectedRows > 0 ? true : false;
}

roomInfoRoute.js文件

router.post("/add", async (req, resp) => {
    try {
        //请注意,因为是post主有求,所以要使用body来接收值
        let results = await ServiceFactory.roomInfoService.add(req.body);
        resp.json(new ResultJson(results, results ? "新增成功" : "新增失败"));
    } catch (error) {
        console.log(error);
        resp.status(500).json(new ResultJson(false, error));
    }
});

这里一定要注意,post请求传递过来的值,是通过req.body来接收的

前端代码addRoomInfo.html

<!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="./css/message.css">
</head>

<body>
    <div class="container-fluid">
        <ul class="breadcrumb">
            <li class="breadcrumb-item"><a href="#">首页</a></li>
            <li class="breadcrumb-item active">新增房间信息</li>
        </ul>
        <div class="card">
            <div class="card-header bg-info text-light">
                新增房间信息
            </div>
            <div class="card-body">
                <form id="form-addRoomInfo">
                    <div class="form-group row">
                        <label for="" class="col-form-label col-sm-2 text-right">房间编号</label>
                        <div class="col-sm-7">
                            <input id="room_name" name="room_name" type="text" placeholder="请输入房间编号" class="form-control"
                                data-rule-required="true"
                                data-msg-required="房间编号不能为空"
                                data-rule-regexp="^[东南西北中发白]\-[sbt]\-\d{4}$"
                                data-msg-regexp="房间编号的格式不正确">
                        </div>
                        <div class="col-sm-3 text-danger"></div>
                    </div>

                    <div class="form-group row">
                        <label for="" class="col-form-label col-sm-2 text-right">最大人数</label>
                        <div class="col-sm-7">
                            <input id="max_count" name="max_count" type="text" placeholder="请输入最大人数" class="form-control"
                                data-rule-required="true"
                                data-msg-required="最大人数不能为空"
                                data-rule-regexp="^\d+$"
                                data-msg-regexp="最大人数必须是整数">
                        </div>
                        <div class="col-sm-3 text-danger"></div>
                    </div>
                    <div class="form-group row align-items-center">
                        <label for="" class="col-form-label col-sm-2 text-right">是否有空调</label>
                        <div class="col-sm-7">
                            <div class="custom-radio custom-control custom-control-inline">
                                <input checked class="custom-control-input" type="radio" id="kt1" name="kt" value="1">
                                <label for="kt1" class="custom-control-label">有</label>
                            </div>
                            <div class="custom-radio custom-control custom-control-inline">
                                <input class="custom-control-input" type="radio" id="kt0" name="kt" value="0">
                                <label for="kt0" class="custom-control-label">没有</label>
                            </div>
                        </div>
                    </div>
                    <div class="form-group row align-items-center">
                        <label for="" class="col-form-label col-sm-2 text-right">是否有网络</label>
                        <div class="col-sm-7">
                            <div class="custom-radio custom-control custom-control-inline">
                                <input checked class="custom-control-input" type="radio" id="network1" name="network" value="1">
                                <label for="network1" class="custom-control-label">有</label>
                            </div>
                            <div class="custom-radio custom-control custom-control-inline">
                                <input class="custom-control-input" type="radio" id="network0" name="network" value="0">
                                <label for="network0" class="custom-control-label">没有</label>
                            </div>
                        </div>
                    </div>
                    <div class="form-group row align-items-center">
                        <label for="" class="col-form-label col-sm-2 text-right">是否有独卫</label>
                        <div class="col-sm-7">
                            <div class="custom-radio custom-control custom-control-inline">
                                <input checked class="custom-control-input" type="radio" id="washroom1" name="washroom" value="1">
                                <label for="washroom1" class="custom-control-label">有</label>
                            </div>
                            <div class="custom-radio custom-control custom-control-inline">
                                <input class="custom-control-input" type="radio" id="washroom0" name="washroom" value="0">
                                <label for="washroom0" class="custom-control-label">没有</label>
                            </div>
                        </div>
                        <div class="col-sm-3 text-danger"></div>
                    </div>
                    <div class="form-group row">
                        <label for="" class="col-form-label col-sm-2 text-right">房间大小</label>
                        <div class="col-sm-7">
                            <div class="input-group">
                                <input id="room_size" name="room_size" type="text" class="form-control" placeholder="请输入房间面积"
                                    data-rule-required="true"
                                    data-msg-required="房间面积不能为空"
                                    data-rule-regexp="^\d+(\.\d+)?$"
                                    data-msg-regexp="房间面积必须是数字类型">
                                <div class="input-group-append">
                                    <span class="input-group-text">m<sup>2</sup></span>
                                </div>
                            </div>
                        </div>
                        <div class="col-sm-3 text-danger"></div>
                    </div>
                    <div class="form-group row">
                        <div class="col-sm-7 offset-sm-2">
                            <button id="btn-save" type="button" class="btn btn-success">
                                保存数据
                            </button>
                            <a href="./roomInfoList.html" class="btn btn-info">
                                返回列表
                            </a>
                        </div>
                    </div>
                </form>
            </div>
        </div>
    </div>
</body>
<script src="./js/jquery-3.5.1.min.js"></script>
<script src="./js/template-web.js"></script>
<script src="./css/bootstrap/js/bootstrap.min.js"></script>
<script src="./js/message.min.js"></script>
<script src="./js/jquery.validate.js"></script>
<script src="./js/messages_zh.js"></script>
<script src="./js/base.js"></script>
<script>
    $(function() {
        var validateForm = $("#form-addRoomInfo").validate({
            errorClass: "text-danger",
            errorPlacement: function(errorElement, currentElement) {
                // currentElement.parent().next().append(errorElement);
                currentElement.parentsUntil("form").find("div.text-danger").append(errorElement);
            },
        });

        $("#btn-save").on("click", function() {
            if (validateForm.form()) {
                //验证通过以后就要提交数据了     
                addData();
            }
        });


        function addData() {
            var laoding = Qmsg.loading("正在保存数据.....")
            request.post("/roomInfo/add", {
                room_name: $("#room_name").val(),
                max_count: $("#max_count").val(),
                kt: $("[name='kt']:checked").val(),
                network: $("[name='network']:checked").val(),
                washroom: $("[name='washroom']:checked").val(),
                room_size: $("#room_size").val()
            }).then(function(res) {
                if (res.status == "success") {
                    Qmsg.success("新增成功");
                    location.replace("./roomInfoList.html");
                } else {
                    Qmsg.error("新增失败,请重试或联系管理员");
                }
            }).catch(function(error) {
                console.log(error);
                Qmsg.error("服务器错误");
            }).finally(function() {
                laoding.close();
            })
        }

    })
</script>

</html>

二十、新增验证房间编号是否存在

image.png
当我们在新增数据的时候,我们不仅仅是要新增数据,还要在填写数据的时候告诉别人,这个数据是否已经存在了

RoomInfoService.js

/**
     * @description 验证某一个房间编号是否存在
     * @param {string} room_name 要验证的房间编号
     * @returns {Promise<boolean>} true代表存在,false代表不存在
*/
async existsRoomName(room_name) {
    let strSql = ` select * from ${this.currentTableName} where 1  and room_name = ? `;
    let results = await this.executeSql(strSql, [room_name]);
    return results.length > 0 ? true : false
}

在路由文件roomInfoRoute.js里面编写一个路径请求,接收页面传递过来的房间编号 ,判断这个房间编号是否存在

router.get("/existsRoomName", async (req, resp) => {
    try {
        console.log(req.query.room_name);
        let results = await ServiceFactory.roomInfoService.existsRoomName(req.query.room_name);
        resp.send(new ResultJson(true, "数据请求成功", results));
    } catch (error) {
        console.log(error);
        resp.status(500).json(new ResultJson(false, error));
    }
});

在上面的路由里面添加了这个请求的路由以后,我们在前端的页面上面就可以使用这个路由地址的路径去通过AJAX请求了

$("#room_name").on("blur", function() {
    var room_name = $(this).val();
    if (room_name) {
        request.get("/roomInfo/existsRoomName", {
            room_name: room_name
        }).then(function(res) {
            if (res.status == "success") {
                if (res.data == true) {
                    Qmsg.warning("当前房间编号已存在,请换一个");
                    $("#room_name").val("");
                }
            }
        }).catch(function(error) {
            Qmsg.error("服务器错误");
            console.log(error);
        });
    }
});

二十一、设置项目的自动重启

在进行nodejs项目开发的时候,当我们更改了项目的源代码,我们希望项目能够自动重新启动,这个技术叫项目的热重启技术,它需要借用于第三方包来完成nodemon

第一步:安装nodemon

$ npm install nodemon --save-dev

这里要注意,这里使用的是--save-dev

  • --save代表当前的包是一个生产环境下面的包
  • --save-dev代表当前的包是一个开发环境下面的包

**场景:**现在李光昊要做一碗饭给标哥吃,它需要如下的东西

  1. 米【原材料】
  2. 水【原材料】
  3. 电饭锅【工具】

工具一般是在开发环境的时候使用的,而原材料则是生产环境下面使用

$ npm install 米 水 --save
$ npm install 电锯锅 --save-dev

第二步:配置启动脚本

package.json的文件下面,找到scripts,添加如下命令

"scripts": {
    "start": "node app.js",
    "dev": "nodemon --watch ./"
},

第三步:启动项目

$ npm run dev

二十二、编辑房间信息

image.png

上图就是编辑页面的布局,但是我们应该是在列表页面点击编辑按钮以后再跳这个页面

image.png

roomInfoRoute.js文件

router.get("/findById", async (req, resp) => {
    try {
        let { id } = req.query;
        let results = await ServiceFactory.roomInfoService.findById(id);
        let flag = results ? true : false;
        resp.json(new ResultJson(flag, flag ? "获取数据成功" : "获取数据失败", results));
    } catch (error) {
        console.log(error);
        resp.status(500).json(new ResultJson(false, error));
    }
});

editRoomInfo.html

这个时候浏览器地址栏的值为:http://127.0.0.1:5500/editRoomInfo.html?id=6

function getById() {
    //合到浏览器地址栏上面的id
    var u = new URL(location.href);
    var id = u.searchParams.get("id");
    //将这个id发送到后台服务器,让后台服务器从数据库里面取这个这个记录,返回给我
    var loading = Qmsg.loading("正在加载数据...");
    request.get("/roomInfo/findById", {
        id: id
    }).then(function(res) {
        console.log(res);
    }).catch(function(error) {
        console.log(error);
        Qmsg.error("服务器错误");
    }).finally(function() {
        loading.close();
    });
}
getById();

这个时候数据已经可以获取回来了,我们要把数据放在页面上面,这里有两种方法

二十三、渲染编辑页面

第一种:直接以DOM的方式操作,这种效率非常低

image.png

这一种方式非常简单,直接将值一个一个的赋值给DOM元素的value属性

第二种:直接通过模板引擎渲染

code1.png

直接将需要渲染的部分使用模板引擎,然后用数据渲染就可以了

image.png

graph TD A[roomInfoList.html]-->|?id=12|B[editRoomInfo.html] B-->|ajax把id传递|C["router.get('/findById')"] C-->|找到对象返回|B

image.png

二十四、编辑房间信息页面保存数据

RoomInfoService.js

/**
* @description 修改房间信息
* @param {{ id, room_name, max_count, kt, network, washroom, room_size }} param 要修改的参数
* @returns {Promise<boolean>}
*/
async update({ id, room_name, max_count, kt, network, washroom, room_size }) {
    let strSql = ` UPDATE rental_house.room_info SET room_name=?, max_count=?, kt=?, network=?, washroom=?, room_size=? WHERE id = ?;`
    let results = await this.executeSql(strSql, [room_name, max_count, kt, network, washroom, room_size, id])
    return results.affectedRows > 0 ? true : false;
}

roomInfoRoute.js

router.post("/update", async (req, resp) => {
    try {
        let results = await ServiceFactory.roomInfoService.update(req.body);
        resp.json(new ResultJson(results, results ? "修改成功" : "修改失败"));
    } catch (error) {
        console.log(error);
        resp.status(500).json(new ResultJson(false, error));
    }
});

editRoomInfo.html

//这里一定要注意,使用事件委托
$("#form-addRoomInfo").on("click", "#btn-save", function() {
    if (validateForm.form()) {
        // 表单验证成功以后,数据重新传回后台
        saveData();
    }
});

function saveData() {
    request.post("/roomInfo/update", {
        id: $("#id").val(),
        room_name: $("#room_name").val(),
        max_count: $("#max_count").val(),
        kt: $("[name='kt']:checked").val(),
        network: $("[name='network']:checked").val(),
        washroom: $("[name='washroom']:checked").val(),
        room_size: $("#room_size").val()
    }).then(function(res) {
        if (res.status == "success") {
            Qmsg.success("修改成功");
            location.replace("./roomInfoList.html");
        } else {
            Qmsg.error("修改失败");
        }
    }).catch(function(error) {
        console.log(error);
        Qmsg.error("服务器错误");
    }).finally(function() {
        loading.close();
    });
}

二十五、解决分页页码过多的问题

image.png

以之前的列表页面上面,我们可以看到这里有很多页面,我们不可以把所有的页面都显示出来 ,所以要在后台设置一下

PageList.js


class PageList {
    /**
     * 
     * @param {number} pageIndex 当前第几页
     * @param {number} totalCount 共多少条数据
     * @param {number} pageSize  每页显示多少
     * @param {[]} listData 查询的分页列表数据 
     */
    constructor(pageIndex, totalCount, pageSize, listData) {
        this.pageIndex = pageIndex;
        this.totalCount = totalCount;
        this.pageCount = Math.ceil(totalCount / pageSize);
        this.listData = listData;
        this.pageStart = this.pageIndex - 3 > 0 ? this.pageIndex - 3 : 1;
        this.pageEnd = this.pageStart + 6 > this.pageCount ? this.pageCount : this.pageStart + 6;
    }
}

module.exports = PageList;

在上面的构造函数里面,我们添加了两个属性,pageStart用于代表起始页码的位置 ,pageEnd用于代表结束页码的位置

roomInfoList.html

<!-- 渲染分页的页码 -->
<script type="text/html" id="temp2">
    <li data-index="1" class="page-item"><a href="#" class="page-link">首页</a></li>
    <%for(var i=pageStart;i<=pageEnd;i++){%>
    <li data-index="{{i}}" class="page-item {{i==pageIndex?'active':null}}"><a class="page-link" href="#">{{i}}</a></li>
    <%}%>
    <li data-index="{{pageCount}}" class="page-item"><a href="#" class="page-link">尾页</a></li>
</script>

二十六、删除房间信息

image.png

我们应该删除数据

roomInfoRoute.js

router.get("/deleteById", async (req, resp) => {
    try {
        let { id } = req.query;
        let results = await ServiceFactory.roomInfoService.deleteById(id);
        resp.json(new ResultJson(results, results ? "删除成功" : "删除失败"));
    } catch (error) {
        console.log(error);
        resp.status(500).json(new ResultJson(false, error));
    }
});

roomInfoList.html

image.png

//使用事件委托
$("#table-roomInfo").on("click", ".btn-delete", function() {
    if (confirm("确定要删除当前项吗?")) {
        var id = $(this).attr("data-id");
        request.get("/roomInfo/deleteById", {
            id: id
        }).then(function(res) {
            if (res.status == "success") {
                Qmsg.success("删除成功");
                getData(currentPageIndex);
            } else {
                Qmsg.success("删除失败");
            }
        }).catch(function(error) {
            console.log(error);
            Qmsg.error("服务器错误,请联系管理员");
        }).finally(function() {
            //无论是成功进入then还是失败进入catch,最后都会进入到finally
            loading.close();
        });
    }
});

在上面的代码里面是没有问题的,它最大的问题在于数据库

存在的问题

  1. 在开发过程当中,DBA不会给开发人员delete的权限

    image.png

  2. 在开发过程当中即使有了delete权限 ,但是因为主外键限制,如果这个表的某一项主键 被 另一个表使用了,则不能删除,因为主外键在约束的时候是默认不允许更新及删除的

    image.png

二十七、逻辑删除(软删除)

因为在开发的过程当中,我们不能够直接删除数据,但是我们又想实现删除的效果,所以怎么办呢?

这个时候我们可以使用下面的方式完成,直接在所有表的后面添加一个字段叫isDel,它的类型为tinyint(1)

image.png

image.png

这样所有的表在打开的时候都会当回事一列isDel,它的默认值就是0

它有了默认值 ,则在添加数据的时候,即使我们不添加这一列它默认也是0

我们可以把isDel0当成是这一列没有删除,我们可以把isDel为1代表删除了

接下来在所有查询语句的后面将原来的where 1替换成where isDel = false

这个时候原来的删除方法deleteById就应该变成下面的方法

BaseService.js

/**
* @description 根据一个主键删除信息 
* @param {number} id 主键  
* @returns {Promise<boolean>} true代表删除成功,false代表删除失败
*/
async deleteById(id) {
    // let strSql = ` delete from ${this.currentTableName} where id = ? `;
    let strSql = ` update ${this.currentTableName} set isDel = true where id = ? `
    let results = await this.executeSql(strSql, [id]);
    return results.affectedRows > 0 ? true : false;
}

逻辑删除也叫软删除,它本质上面是没有删除数据的,只慢在数据表里面添加了一列,用于标识这一项数据是否被删除掉

当我们要删除这一 项数据的时候,我们可以通过update语句当这一列的值改true或其它值

二十八、Excel的导出功能

image.png

前端文件roomInfoList.html

$(".btn-export-excel").on("click", function() {
    //地址栏的传值是get传值 ,后面是通过req.query来得到的
    window.open(baseURL + "/roomInfo/exportExcel?room_name=" + $("#room_name").val());
});

在这里同学们要注意,我们使用了window.open()打开一个新的链接,并且在后面使用?拼接了参数,传递到了服务器

RoomInfoService.js

/**
     * @description 导出为excel的数据,但是不需要分页
     * @param {{room_name}} param 查询参数 
     * @returns {Promise<[]>} 返回查询结果 
*/
exportExcel({ room_name }) {
    let strSql = ` select * from ${this.currentTableName} where isDel = false `;
    let strWhere = ` `;
    let ps = []
    if (room_name) {
        strWhere += ` and room_name like ? `;
        ps.push(`%${room_name}%`);
    }
    strSql += strWhere;
    return this.executeSql(strSql, ps);
}

roomInfoRoute.js

router.get("/exportExcel", async (req, resp) => {
    try {
        let results = await ServiceFactory.roomInfoService.exportExcel(req.query);
        let excelPath = ExcelUtils.resultsToExcel(results);
        if (excelPath) {
            //生成好了
            resp.sendFile(excelPath);
        }
        else {
            resp.json(new ResultJson(false, "excel没有数据,不能下载"));
        }
    } catch (error) {
        console.log(error);
        resp.status(500).json(new ResultJson(false, error));
    }
});

ExcelUtils.js

因为我们的excel导出是需要在其它的页面也使用的,所以我们单独封装了一个方法来使用,在utils目录下面我们新建了一个ExcelUtils.js的文件,代码如下

/**
 * ExcelUtils.js 操作excel的对象
 */
const xlsx = require("node-xlsx");
const path = require("path");
const fs = require("fs");

class ExcelUtils {
    /**
     * @description 数据库的查询结果转变成excel文件
     * @param {[]} results 数据库的查询结果 
     * @returns {string} 返回生成的excel的路径
     */
    static resultsToExcel(results) {
        if (results.length > 0) {
            let headRow = Object.keys(results[0]);
            let dataRows = results.map(item => Object.values(item));
            dataRows.unshift(headRow);
            let excelObj = [
                {
                    name: "Sheet1",
                    data: dataRows
                }
            ];
            //设置一个保存路径
            let savePath = path.join(__dirname, "../excelDir", `${Date.now()}-${parseInt(Math.random() * 1000)}.xlsx`);
            let excelBuff = xlsx.build(excelObj);
            fs.writeFileSync(savePath, excelBuff);
            return savePath;
        }
        else {
            return "";
        }
    }
}
module.exports = ExcelUtils;

在上面的代码里面,我们是通过node-xlsx生成了一个excel文件,然后在express里面通过resp.sendFile来发送一个文件到浏览器

二十九、定期删除生成的excel文件

这个时候要注意,因为导出excel是需要在服务器生成excel文件的,所以服务器运行时间长了以后,必须要定时清除掉这些文件,如下图所示

image.png

这个时候我们要在项目启动的时候添加一个定时器的功能,去完成上面的代码

app.js里面

server.listen(8080, "0.0.0.0", () => {
    console.log("服务器启动成功");

    const deleteExcelFile = () => {
        let excelDirPath = path.join(__dirname, "./excelDir");
        let arr = fs.readdirSync(excelDirPath);
        for (let item of arr) {
            let fileCreateTime = item.split("-")[0];
            if (Date.now() - fileCreateTime > 1000 * 60 * 30) {
                //超过30分钟,要删除
                fs.unlinkSync(path.join(excelDirPath, item));
            }
        }
    }
    //在服务器启动的时候就执行一次
    deleteExcelFile();
    //接下来开启定时器,定时执行
    setInterval(deleteExcelFile, 1000 * 60 * 30)
});

三十、全局异常的处理

在之前的代码里面,我们可以看到,我们对所有的代码都进行了try...catch的异常捕获,这样做非常麻烦

express的框架里面,其实我们有一个全局异常处理的包叫express-async-errors,这个包可以全局捕获express的异常,这样我样就可以简化很多代码

第一步:安装包

$ npm install express-async-errors --save

第二步:在app.js程序启动的时候导入这个包

const express = require("express");
require("express-async-errors");

第三步:在程序的最后处理异常

//在处理全局的异常
app.use((error, req, resp, next) => {
    console.log(error);
    resp.status(500).json(new ResultJson(false, "服务器错误", error));
    next();
});

为什么在程序的最后去处理,是因为如果前面的程序处理了,我们就不处理,如果前面没有程序处理异常,我们就在这里处理掉,相当于把它做了最后一次的防线

当我们使用了全局异常以后,这样在整个程序的内部,我们都不需要使用try...catch


三十一、总结一

到此为止,我们已经完成了room_info这个数据表的增,删,改,查的功能了

  1. 前端的项目和后端的项目是分开的,前端与后端的数据交互使用的是ajax请求
  2. ajax默认不允许跨域,所以要在设置添加响应头CORS
  3. 路由就是用于控制请求的,ajax请求的其实是某一个路由地址
  4. 在请求的时候是可以传递数据到服务器的,get请求的请求参数在url的地址后面,后台接收值的时候使用req.querypost不在地址栏,它是通过body传递过去的,它在后台req.body
  5. express的框架默认是不支持post请求的,需要借用于第三方插件body-parser
  6. 路由返回的数据我们统一使用了ResultJson的格式来处理,这样前端在接收到json字符串的时候统一处理
  7. 前端的页面渲染,目前我们使用的是art-template模板引擎,后期我们全转向vue/react
  8. 在开发的时候,数据库一般是默认不会开启delete权限,或因为主外键约束不能删除,所以我们在数据库里面要使用软删除的功能
  9. resp.sendFile用于向前台发送一个文件,我们的excel的导出功能就是基于此完成的
  10. 全局的异常处理我们使用了express-async-errors来完成,这样可以极大的简化我们的代码
3

评论区