本文已参与「新人创造礼」活动,一起敞开创造之路。

概述

JavaEE的期末大作业,根据 SSM 开发的一次项目实战,严格的实施三级权限办理:访客,会员,办理员,大致的功用完成如下,

【项目实战】—— SSM 图书管理系统
资源下载点这!

准备

  • 环境:

    • IDEA
    • Tomcat 9+
    • MySQL 5.7+
    • Maven 3.6+
  • 技术:

    • Mybatis
    • Spring
    • SpringMVC
    • jQuery
    • Bootstrap
    • Semantic

完成

建立数据库

【项目实战】—— SSM 图书管理系统

表名 内容
users 存储会员和办理员的登录信息,如:会员名、会员登录暗码、办理员名、办理员登录暗码、身份等等。
information 存储会员的个人信息,如:会员名、性别、生日、个人头像、特性签名、余额、诺言等级、借书数量、购买数量、积分点等等。
books 存储书本的具体信息,如:书本编号、书名、书本数量、书本图片、书本作者、书本价格等等。
comments 存储书本的谈论信息,如:谈论编号、书本编号、谈论者、谈论内容、谈论时刻等等。
borrow 存储书本的借阅时刻信息,如:书本编号、书名、借书开始时刻、借书时长等等。
spend_list 存储会员的充值和消费信息,如:会员名、充值记载、消费记载、余额变化、现有余额、充值或消费时刻等等。
stock_list 存储进货的详细信息,如:所需书本编号、所需书本名称、进货数量、进货地址、进货时刻等等。

装备文件

  1. 首要有 mybatis,spring,springmvc 的装备文件以及 web.xml 的装备,详见资源包,这儿就一笔带过,
    【项目实战】—— SSM 图书管理系统
  2. 首要完成 pojo 层,dao 层,service 层,相同一笔带过,
    【项目实战】—— SSM 图书管理系统
    【项目实战】—— SSM 图书管理系统
    【项目实战】—— SSM 图书管理系统

代码编写

感兴趣的能够下载资源包看一下,首要便是完成 controller 层和 view 层,

目录有点长,就不截图了。

运转展现

访客

主页ing

【项目实战】—— SSM 图书管理系统
书本概况ing,略显粗糙,首要是为了展现和完成后端的一些功用,
【项目实战】—— SSM 图书管理系统
访客是不能进行借阅,购买或谈论书本的,当用户点击时,会先验证身份,假如是访客的话,则会被告知“请先登录”,
【项目实战】—— SSM 图书管理系统
书本查找ing,访客,会员和办理员都能够经过查找来查找自己想要查找的书本名称或许作者,
【项目实战】—— SSM 图书管理系统
注册ing,访客注册之后就能够成为会员啦~
【项目实战】—— SSM 图书管理系统

会员

登录ing,访客注册成功后,就能够登录了,

【项目实战】—— SSM 图书管理系统
主页ing,这是会员的主页,有用 jQuery 做的动画作用,
【项目实战】—— SSM 图书管理系统
个人资料ing,能够上传头像,修正相关个人信息,充值余额,升级诺言,查看消费记载,以及借书买书概况等,
【项目实战】—— SSM 图书管理系统
消费记载ing
【项目实战】—— SSM 图书管理系统
借书概况ing,在这儿能够进行续用和偿还,假如超时偿还则会下降诺言等级,
【项目实战】—— SSM 图书管理系统
购书概况ing
【项目实战】—— SSM 图书管理系统
谈论总览ing,会员能够删去自己的谈论,
【项目实战】—— SSM 图书管理系统
修正暗码ing,利用 onblur 特点伪造实时检测,并且有显示暗码功用,
【项目实战】—— SSM 图书管理系统

办理员

主页ing,类似于会员的主页,

【项目实战】—— SSM 图书管理系统
书本列表ing,办理员能够在这新增,更改和删去书本,
【项目实战】—— SSM 图书管理系统
新增书本ing
【项目实战】—— SSM 图书管理系统
更改书本ing,这儿除了能够修正书本信息,一起能看到这本书的一切评价以及一切购买信息,
【项目实战】—— SSM 图书管理系统
办理员能够删去会员的不妥谈论,
【项目实战】—— SSM 图书管理系统
【项目实战】—— SSM 图书管理系统
会员办理ing,办理员可修正会员的相关信息或许删去会员,即当会员挑选刊出账号时,
【项目实战】—— SSM 图书管理系统
办理员能够修正会员的余额(maybe 不太好?),也能调整会员的诺言等级,
【项目实战】—— SSM 图书管理系统
一起也能看到会员对一切书本的谈论,
【项目实战】—— SSM 图书管理系统
会员借阅概况ing,办理员能够看到一切的会员借书概况,一起也能够提醒快超时或许现已超时的会员对相关书本进行偿还,
【项目实战】—— SSM 图书管理系统
会员已购概况ing
【项目实战】—— SSM 图书管理系统
进货办理ing,这是批处理的进货,办理员需求进啥填啥就好了,简略暗示一下,
【项目实战】—— SSM 图书管理系统
且带有进货记载,便利回溯,
【项目实战】—— SSM 图书管理系统

问题解决

做程序时遇到的问题,选几个比较有针对性的,

1. 关于js中履行次序问题的解决?

由于后端用session来传递图片的保存地址,所以当一次完成图片存储操作后,session中绑定目标的值仍是存在的,当我们第二次及之后提交的话,就会变成将上一次的图片保存地址更新到了数据库傍边,形成这个问题的原因是将两次提交写进了一个函数里(如下),

function upload(){
    var IMG = new FormData(document.getElementById("uploadIMG"));
    var item = new FormData(document.getElementById("item"));
    $.ajax({
        url:"/book/writePhoto",
        type:"post",
        data:IMG,
        processData:false,
        contentType:false,
    });
    $.post({
        url:"/book/updateBook",
        data:item,
        processData:false,
        contentType:false,
    });
    alert("修正成功!");
    window.location = "${pageContext.request.contextPath}allBook";
}
function Return(){
    window.location = "${pageContext.request.contextPath}allBook"
}

可是代码并不是按次序履行的,所以就形成了拿原有session所绑定目标的值去更新了数据库,然后才是更新session绑定目标的值,如下图所示,其间Photo Address是图片上传后所保存的地址,而upAddrsession所绑定目标的值,booAddr则是经过book.getPhoto()得到的值,

【项目实战】—— SSM 图书管理系统
之后,在两个提交之间插入了alert来进行一个打断,果然就正常了,可是一次提交呈现两个alert就显得怪怪的,因而就想到用sleep()函数,去查了一下js的sleep办法,发现 JavaScript 有setTimeout()办法来完成设定一段时刻后履行某个任务,但写法很丑陋,需求提供回调函数:

setTimeout(function(){ alert("Hello"); }, 3000);

JavaScript Promise API是新呈现了一个API,借助 Promise,我们能够对setTimeout函数进行改良,下面便是把setTimeout()封装成一个回来Promise的sleep()函数。

function sleep (time) {
  return new Promise((resolve) => setTimeout(resolve, time));
}
// 用法
sleep(500).then(() => {
    // 这儿写sleep之后需求去做的工作
})

改善代码之后的运转结果就正常了!

【项目实战】—— SSM 图书管理系统

2. 关于运用EL表达式对两个目标的值进行比较?

为了能够契合的运用下拉框中 optionselect 特点,

运用如下代码进行编写,

<select name="credit">
    <c:forEach var="credit" items="${creditList}">
        <c:choose>
            <c:when test="${credit eq member.getCredit()}">
                <option value="${credit}" selected>${member.getCredit()}</option>
            </c:when>
            <c:otherwise>
                <option value="${credit}">${credit}</option>
            </c:otherwise>
        </c:choose>
    </c:forEach>
</select>

其间${credit eq member.getCredit()}不能改为${credit}.equals(${member.getCredit()})

Expression Language 中,仅能够运用 ==eq 运算符来比较目标值。在暗地,他们实践上将运用Object#equals()。这样做是由于,直到运用当时的 EL 2.1 版别,才干调用具有除标准getter(和setter)办法之外的其他签名的办法(在即将到来的EL 2.2中是可能的)。

上述正确句子在暗地的大致解释为

jspContext.findAttribute("credit").equals(member.getCredit());

3. 关于SSM框架下的分页功用完成?

做办理体系时,必然会碰到完成分页以及页面查询功用,在不运用插件的前提下,

【项目实战】—— SSM 图书管理系统

先创建实体类Page.class

package com.idiot.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Page {
    int pageSize;       //页面显示记载的数量
    int pageCount;      //表明页面总数
    int rowCount;       //表明记载总数
    int pageCurrent;    //表明当时页面为第几页
    int start;          //表明当时为第几条记载
    public Page(String pageNo, int pageSize,  int total) {
        this.pageSize = pageSize;
        if (pageNo==null || pageNo.trim().length()==0){
            this.pageCurrent = 1;
        } else {
            this.pageCurrent = Integer.parseInt(pageNo);
        }
        this.rowCount = total;
        this.pageCount = (this.rowCount+this.pageSize)/this.pageSize;
        if (this.pageCurrent > this.pageCount){
            this.pageCurrent = this.pageCount;
        }
        if (this.pageCurrent < 1){
            this.pageCurrent = 1;
        }
        this.start = (this.getPageCurrent()-1)*this.getPageSize();
    }
}

其间,pageNo表明需求跳转到第几页面,pageSize表明一个页面显示记载的数量,total表明该数据库表中总的记载数量,

然后在操控类中进行编写,以BookController.java为例,

@RequestMapping("/allBook")
public String list(String pageNo, Model model) {
    int total = bookService.getTotalBooks();
    Page p = new Page(pageNo,8,total);
    HashMap<String,Object> map = new HashMap<String,Object>();
    map.put("start",p.getStart());
    map.put("size", p.getPageSize());
    List<Books> bookList = bookService.queryAllBook(map);
    model.addAttribute("bookList", bookList);
    model.addAttribute("page", p);
    return "manager/books/allBook";
}

其间代码段中用到的两个SQL句子如下,

<!--获取一切书本数量-->
<select id="getTotalBooks" resultType="int">
    select count(bookID) from books
</select>
<!--查询悉数Book-->
<select id="queryAllBook" parameterType="Map" resultType="Books">
    SELECT * from books
    <if test="start!=null and size!=null">
        limit #{start},#{size}
    </if>
</select>

终究以allBook.jsp为例,展现在jsp中的应用,

<tbody>
<c:forEach var="book" items="${requestScope.get('bookList')}">
        <tr>
            <td><img src="${pageContext.request.contextPath}/book/readPhoto?id=${book.getBookID()}" width="40px" height="40px"></td>
            <td>${book.getBookID()}</td>
            <td>${book.getBookName()}</td>
            <td>${book.getBookCounts()}</td>
            <td>${book.getDetail()}</td>
            <td>
                <a href="${pageContext.request.contextPath}/book/toUpdateBook?id=${book.getBookID()}">更改</a> |
                <a href="${pageContext.request.contextPath}/book/del/${book.getBookID()}">删去</a>
            </td>
        </tr>
</c:forEach>
</tbody>
<label>第${requestScope.page.getPageCurrent()}/${page.pageCount}页</label>
<a href="/book/allBook?pageNo=1">主页</a>
<a href="/book/allBook?pageNo=${page.pageCurrent-1}" >上一页</a>
<a href="/book/allBook?pageNo=${page.pageCurrent+1}" >下一页</a>
<a href="/book/allBook?pageNo=${page.pageCount}">尾页</a> 跳转到:
<input type="text" style="width:30px" id="turnPage" /><input type="button" onclick="startTurn()" value="跳转" />
<script type="text/javascript">
    function startTurn(){
        var turnPage = document.getElementById("turnPage").value;
        if(turnPage > ${page.pageCount}){
            alert("超越最大页数,请从头输入!");
            return false;
        }
        if(turnPage < 1){
            alert("低于最小页数,请从头输入!");
            return false;
        }
        var shref="/book/allBook?pageNo="+turnPage;
        window.location.href=shref;
    }
</script>

其间 js 里的两个 if 断定可有可无,由于现已在结构函数里进行了处理,程序仍是具有较高的鲁棒性!

4. 关于正则表达式避免充值时非法输入?

在充值时,假如会员恶意输入的话,会导致程序呈现问题,

因而为了避免此问题,用正则表达式编写js办法,

function clearNoNum(obj) {
    obj.value = obj.value.replace(/[^\d.]/g, "");    //铲除"数字"和"."以外的字符
    obj.value = obj.value.replace(/^0/g, "");       //验证榜首个字符不是0
    obj.value = obj.value.replace(/^\./g, "");      //验证榜首个字符是数字而不是.
    obj.value = obj.value.replace(/\.{2,}/g, ".");  //只保存榜首个'.'铲除剩余的'.'
    obj.value = obj.value.replace(".", "$#$").replace(/\./g, "").replace("$#$", ".");   //保证'.'只呈现一次'.'而不能呈现两次以上
}

上述办法既不答应榜首位是0,也不答应榜首位是.

在输入框的标签中调用即可,运用onkeyup特点,

<input type="text" name="money" id="money" onkeyup="clearNoNum(money)">

5. 关于前端批处理提交后端接纳处理问题?

在进货办理中,为了便利办理员操作,提高效率,对进货进行批处理操作,这时就呈现了两个问题,怎么获取多组数据以及怎么提交给后端,

【项目实战】—— SSM 图书管理系统

怎么获取多组数据?

由于内容是由 EL 表达式写的,因而就没用到表单,并且还用了 forEach,这才是问题的关键地点,所以怎么获取多组数据呈现了困难,

<tbody>
<c:forEach var="book" items="${requestScope.get('bookList')}">
    <tr>
        <td><img src="${pageContext.request.contextPath}/book/readPhoto?id=${book.getBookID()}" width="40px" height="40px"></td>
        <td>${book.getBookName()}</td>
        <td><input type="text"></td>
        <td><input type="text"></td>
    </tr>
</c:forEach>
</tbody>

经过一番查阅,发现了一个重要办法 HTML DOM getElementsByClassName(),首要作用便是获取一切指定类名的元素

var x = document.getElementsByClassName("example");

什么意思呢,便是说只需 HTML 中的元素的 class 相同,那么都会被 x 获取,

那么根据其特性,我们只需即将获取的数据的地点元素起个 class 名即可,如下,

<c:forEach var="book" items="${requestScope.get('bookList')}">
    <tr>
        <input type="text" class="bookID" value="${book.getBookID()}" hidden>
        <td><img src="${pageContext.request.contextPath}/book/readPhoto?id=${book.getBookID()}" width="40px" height="40px"></td>
        <td class="bookName">${book.getBookName()}</td>
        <td><input class="addr" type="text"></td>
        <td><input class="nums" type="text"></td>
    </tr>
</c:forEach>

编写 js 进行获取数据,

var bookID = document.getElementsByClassName("bookID");
var bookName = document.getElementsByClassName("bookName");
var addr = document.getElementsByClassName("addr");
var nums = document.getElementsByClassName("nums");

不过要留意的是,以上的 js 目标仅仅获得了元素目标,假如想获取元素里的值,则需求写上相对应的办法,

比如 <input> 标签就用 .value,而 <td> 标签则用 .innerHTML 来获取数据,

怎么将多组值传给后端?

这么多组数据的话,假如一个一个传就显得很不便利,这时想着将他们悉数合并成一个数组,类似于 Java 傍边的List<..>,如下

var list = [];
for (i = 0; i < bookID.length; i++) {
    list.push({
        bookID: bookID[i].value,
        bookName: bookName[i].innerHTML,
        counts: nums[i].value,
        address: addr[i].value
    })
    console.log(list[i]);
}

前端运用 jquery 向后台传递数组类型的参数,Java 后台直接经过 List 类型接纳,会发现无法取到参数,因而需求将其转化成 json,

先导入 jar 包,

<!--json依靠-->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.9.9</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-core</artifactId>
    <version>2.9.9</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-annotations</artifactId>
    <version>2.9.9</version>
</dependency>

然后编写 ajax 进行提交,

$.ajax({
    cache: true,
    type: "POST",
    url: '/Manager/updateStocking',
    // 指定恳求的数据格式为json,实践上传的是json字符串
    data: JSON.stringify(list),
    //指定恳求的数据格式为json,这样后台才干用@RequestBody 接受java bean
    contentType: 'application/json;charset=utf-8',
    async: false,
});

后端则需求用到 @ResponseBody@RequestBody 来接纳数据,

@RequestMapping(value = "/updateStocking", method = RequestMethod.POST)
public void updateStocking(@RequestBody List<Stock_list> list) {
    System.out.println(list);
}

@ResponseBody

@ResponseBody 注解的作用是将 controller 的办法回来的目标经过适当的转换器转换为指定的格式之后,写入到 response 目标的 body 区,一般用来回来 JSON 数据或许是 XML 数据,需求留意的是,在运用此注解之后不会再走视图处理器,而是直接将数据写入到输入流中,他的作用等同于经过 response 目标输出指定格式的数据

例子如下,两个办法是等价的,

@Controller
public class ResponseController {
    @RequestMapping("/response")
    public void response(HttpServletResponse response) throws IOException {
        User user = new User();
        user.setEmail("123@qq.com");
        user.setId(001);
        user.setPassword("******");
        user.setUserName("tom");
        response.getWriter().write(JSON.toJSON(user).toString());
    }
    @ResponseBody
    @RequestMapping("/re")
    public User response() {
        User user = new User();
        user.setEmail("123@qq.com");
        user.setId(001);
        user.setPassword("******");
        user.setUserName("tom");
        return user;
    }
}

@RequestBody

@RequestBody 首要用来接纳前端传递给后端的 JSON 字符串中的数据的(恳求体中的数据的),

GET办法无恳求体,所以运用 @RequestBody接纳数据时,前端不能运用GET办法提交数据,而是用POST办法进行提交,

在后端的同一个接纳办法里,@RequestBody@RequestParam() 能够一起运用,@RequestBody最多只能有一个,而@RequestParam()能够有多个,

留意:关于 ajax 的相关问题?

在用 ajax 的时候,会碰到ajax 将数据提交给 controller 办法且办法顺畅履行之后, 界面却不跳转的状况,这儿猜想其实是将值回来给了前端,而不是交给视图解析器了,因而,能够配合 ResponseBody 注解,

controller 回来参数,利用 @ResponseBody 回来给前端 JSON 格式,然后在 ajax 的 success 函数里面调用回来值,

$.ajax({
    cache: true,
    type: "POST",
    url: '/Manager/updateStocking',
    // 指定恳求的数据格式为json,实践上传的是json字符串
    data: JSON.stringify(list),
    //指定恳求的数据格式为json,这样后台才干用@RequestBody 接受java bean
    contentType: 'application/json;charset=utf-8',
    // dataType: "json",
    async: false,
    success: function (data,status){
        if (data == "success"){
            alert("进货成功!")
            window.location.href="${pageContext.request.contextPath}/Manager/toReturnIndex"
        } else {
            alert("进货失利!")
            history.back()
        }
    },
    error : function(data,status) {
        alert("数据上传失利: "+status);
    }
});

一起这儿要留意的是,不能运用 dataType: "json",否则会报 parsererror 的过错,由于 dataType: "json" 会试图将 controller 的回来值解析成 JSON ,但当回来值是一个字符串或许其他值时,它并不是一个真实的 JSON,解析器会解析失利的!

后记

这次项目实战令人受益匪浅,虽然在 debug 的进程中会令人烦躁,毕竟百度里的许多问题都是千人一面的解决方案,可能发文的人压根不知道问题在哪,但终究仍是渐渐给磨出来了,实践出真知,诚不欺我也!

本来是计划拓宽这个项目跟移动端搞联动的,可是后来发现前后端存在耦合,没有彻底别离,就暂时没法让移动端调用后端接口了,所以这个主意只能暂缓了,叹息…

关于这个项目仍是能够持续修正和拓宽的,欢迎我们在下方谈论区留言评论,如有缺乏,也请各位大佬指出!