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

概述

JavaEE的期末大作业,基于 SSM 开发的一次项目实战,严格的实行三级权限管理:访客,会员,管理员,大致的功能实现如下,
思维导图
资源下载点这!

准备

  • 环境:

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

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

实现

搭建数据库

数据库表

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

配置文件

  1. 主要有 mybatis,spring,springmvc 的配置文件以及 web.xml 的配置,详见资源包,这里就一笔带过,
    配置文件
  2. 主要实现pojo层,dao层,service层,同样一笔带过,
    pojo
    dao
    service

代码编写

感兴趣的可以下载资源包看一下,主要就是实现 controller 层和 view 层,

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

运行展示

访客

首页ing
首页 
书籍详情ing,略显粗糙,主要是为了展示和实现后端的一些功能,
图书详情
访客是不能进行借阅,购买或评论书籍的,当用户点击时,会先验证身份,如果是访客的话,则会被告知“请先登录”,
提示
 
书籍搜索ing,访客,会员和管理员都可以通过搜索来查找自己想要搜索的书籍名称或者作者,
书籍搜索
注册ing,访客注册之后就能够成为会员啦~
注册
 

会员

登录ing,访客注册成功后,就可以登录了,
登录 
首页ing,这是会员的首页,有用 jQuery 做的动画效果,
会员首页
 
个人资料ing,可以上传头像,修改相关个人信息,充值余额,升级信誉,查看消费记录,以及借书买书详情等,
个人资料
 
消费记录ing
消费记录
 
借书详情ing,在这里可以进行续借和归还,如果超时归还则会降低信誉等级,
借书详情
 
购书详情ing
购书详情
 
评论总览ing,会员可以删除自己的评论,
评论总览
 
修改密码ing,利用 onblur 属性伪造实时检测,并且有显示密码功能,
修改密码
 

管理员

首页ing,类似于会员的首页,
管理员首页
 
书籍列表ing,管理员可以在这新增,更改和删除书籍,
书籍列表
 
新增书籍ing
新增书籍
 
更改书籍ing,这里除了能够修改书籍信息,同时能看到这本书的所有评价以及所有购买信息,
更改书籍管理员可以删除会员的不当评论,
评论详情购买总览
 
会员管理ing,管理员可修改会员的相关信息或者删除会员,即当会员选择注销账号时,
会员管理管理员能够修改会员的余额(maybe 不太好?),也能调整会员的信誉等级,
会员详情同时也能看到会员对所有书籍的评论,
会员评论总览
 
会员借阅详情ing,管理员可以看到所有的会员借书详情,同时也可以提醒快超时或者已经超时的会员对相关书本进行归还,
会员借阅详情
 
会员已购详情ing
会员已购详情进货管理ing,这是批处理的进货,管理员需要进啥填啥就好了,简单示意一下,
进货管理
且带有进货记录,方便回溯,
进货记录 

问题解决

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

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()得到的值,
1之后,在两个提交之间插入了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之后需要去做的事情
})

改进代码之后的运行结果就正常了!
2
 

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框架下的分页功能实现?

做管理系统时,必然会碰到实现分页以及页面查询功能,在不使用插件的前提下,

先创建实体类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. 关于前端批处理提交后端接收处理问题?

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

如何获取多组数据?

因为内容是由 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("[email protected]");
        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("[email protected]");
        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 的过程中会令人烦躁,毕竟百度里的很多问题都是千篇一律的解决方案,可能发文的人压根不知道问题在哪,但最终还是慢慢给磨出来了,实践出真知,诚不欺我也!

本来是打算拓展这个项目跟移动端搞联动的,可是后来发现前后端存在耦合,没有完全分离,就暂时没法让移动端调用后端接口了,所以这个想法只能暂缓了,叹气…

关于这个项目还是可以继续修改和拓展的,欢迎大家在下方评论区留言讨论,如有不足,也请各位大佬指出!

猜你喜欢

转载自blog.csdn.net/weixin_46263782/article/details/118558298