JavaWeb之书城项目(2)

该文档是:JavaWeb之书城项目巩固JavaWeb知识,太长了分为两段

博客连接:https://www.loveuluo.cn

日期:2020-12-22

18. MVC 概念

image-20201215100256413

image-20201215100317029

19. 图书模块

19.1 编写图书模块的数据库表

##创建图书表
create table t_book(
    `id` int(11) primary key auto_increment,     ## 主键
    `name` varchar(50) not null,                ## 书名 
    `author` varchar(50) not null,                ## 作者
    `price` decimal(11,2) not null,                ## 价格
    `sales` int(11) not null,                    ## 销量
    `stock` int(11) not null,                    ## 库存
    `img_path` varchar(200) not null            ## 书的图片路径
);

## 插入初始化测试数据
insert into t_book(`id` , `name` , `author` , `price` , `sales` , `stock` , `img_path`) 
values(null , 'java从入门到放弃' , '国哥' , 80 , 9999 , 9 , 'static/img/default.jpg');

insert into t_book(`id` , `name` , `author` , `price` , `sales` , `stock` , `img_path`) 
values(null , '数据结构与算法' , '严敏君' , 78.5 , 6 , 13 , 'static/img/default.jpg');

insert into t_book(`id` , `name` , `author` , `price` , `sales` , `stock` , `img_path`) 
values(null , '怎样拐跑别人的媳妇' , '龙伍' , 68, 99999 , 52 , 'static/img/default.jpg');

insert into t_book(`id` , `name` , `author` , `price` , `sales` , `stock` , `img_path`) 
values(null , '木虚肉盖饭' , '小胖' , 16, 1000 , 50 , 'static/img/default.jpg');

insert into t_book(`id` , `name` , `author` , `price` , `sales` , `stock` , `img_path`) 
values(null , 'C++编程思想' , '刚哥' , 45.5 , 14 , 95 , 'static/img/default.jpg');

insert into t_book(`id` , `name` , `author` , `price` , `sales` , `stock` , `img_path`) 
values(null , '蛋炒饭' , '周星星' , 9.9, 12 , 53 , 'static/img/default.jpg');
 
insert into t_book(`id` , `name` , `author` , `price` , `sales` , `stock` , `img_path`) 
values(null , '赌神' , '龙伍' , 66.5, 125 , 535 , 'static/img/default.jpg');

insert into t_book(`id` , `name` , `author` , `price` , `sales` , `stock` , `img_path`) 
values(null , 'Java编程思想' , '阳哥' , 99.5 , 47 , 36 , 'static/img/default.jpg');

insert into t_book(`id` , `name` , `author` , `price` , `sales` , `stock` , `img_path`) 
values(null , 'JavaScript从入门到精通' , '婷姐' , 9.9 , 85 , 95 , 'static/img/default.jpg');

insert into t_book(`id` , `name` , `author` , `price` , `sales` , `stock` , `img_path`) 
values(null , 'cocos2d-x游戏编程入门' , '国哥' , 49, 52 , 62 , 'static/img/default.jpg');

insert into t_book(`id` , `name` , `author` , `price` , `sales` , `stock` , `img_path`) 
values(null , 'C语言程序设计' , '谭浩强' , 28 , 52 , 74 , 'static/img/default.jpg');

insert into t_book(`id` , `name` , `author` , `price` , `sales` , `stock` , `img_path`) 
values(null , 'Lua语言程序设计' , '雷丰阳' , 51.5 , 48 , 82 , 'static/img/default.jpg');

insert into t_book(`id` , `name` , `author` , `price` , `sales` , `stock` , `img_path`) 
values(null , '西游记' , '罗贯中' , 12, 19 , 9999 , 'static/img/default.jpg');

insert into t_book(`id` , `name` , `author` , `price` , `sales` , `stock` , `img_path`) 
values(null , '水浒传' , '华仔' , 33.05 , 22 , 88 , 'static/img/default.jpg');
 
insert into t_book(`id` , `name` , `author` , `price` , `sales` , `stock` , `img_path`) 
values(null , '操作系统原理' , '刘优' , 133.05 , 122 , 188 , 'static/img/default.jpg');
 
insert into t_book(`id` , `name` , `author` , `price` , `sales` , `stock` , `img_path`) 
values(null , '数据结构 java版' , '封大神' , 173.15 , 21 , 81 , 'static/img/default.jpg');
 
insert into t_book(`id` , `name` , `author` , `price` , `sales` , `stock` , `img_path`) 
values(null , 'UNIX高级环境编程' , '乐天' , 99.15 , 210 , 810 , 'static/img/default.jpg');
 
insert into t_book(`id` , `name` , `author` , `price` , `sales` , `stock` , `img_path`) 
values(null , 'javaScript高级编程' , '国哥' , 69.15 , 210 , 810 , 'static/img/default.jpg');
 
insert into t_book(`id` , `name` , `author` , `price` , `sales` , `stock` , `img_path`) 
values(null , '大话设计模式' , '国哥' , 89.15 , 20 , 10 , 'static/img/default.jpg');
 
insert into t_book(`id` , `name` , `author` , `price` , `sales` , `stock` , `img_path`) 
values(null , '人月神话' , '刚哥' , 88.15 , 20 , 80 , 'static/img/default.jpg');
 
## 查看表内容
select id,name,author,price,sales,stock,img_path from t_book;

19.2 编写图书模块的 JavaBean

public class Book {
    private Integer id;
    private String name;
    private String author;
    private BigDecimal price;
    private Integer sales;
    private Integer stock;
    private String imgPath = "static/img/default.jpg";
}
//要在有参构造和set方法里添加一个判断
        // 要求给定的图书封面图书路径不能为空(等于空的话就不赋值 使用默认路径)
        if (imgPath != null && !"".equals(imgPath)) {
            this.imgPath = imgPath;
        }

19.3 编写图书模块的 Dao

BookDao 接口:

public interface BookDao {
    public int addBook(Connection con, Book book);

    public int deleteBookById(Connection con,Integer id);

    public int updateBook(Connection con,Book book);

    public Book queryBookById(Connection con,Integer id);

    public List<Book> queryBooks(Connection con);
}

BookDaoImpl 实现类:

public class BookDaoImpl extends BaseDao<Book> implements BookDao {

    @Override
    public int addBook(Connection con, Book book) {
        String sql = "INSERT INTO t_book(`name` , `author` , `price` , `sales` , `stock` , `img_path`)  VALUES(?,?,?,?,?,?)";
        int update = update(con, sql, book.getName(), book.getAuthor(), book.getPrice(), book.getSales(), book.getStock(), book.getImgPath());
        return update;
    }

    @Override
    public int deleteBookById(Connection con, Integer id) {
        String sql = "delete from t_book where id = ?";
        return update(con,sql, id);
    }

    @Override
    public int updateBook(Connection con, Book book) {
        String sql = "update t_book set `name`=?,`author`=?,`price`=?,`sales`=?,`stock`=?,`img_path`=? where id = ?";
        return update(con,sql,book.getName(),book.getAuthor(),book.getPrice(),book.getSales(),book.getStock(),book.getImgPath(),book.getId());
    }

    @Override
    public Book queryBookById(Connection con, Integer id) {
        String sql = "select `id` , `name` , `author` , `price` , `sales` , `stock` , `img_path` imgPath from t_book where id = ?";
        return queryForOne(con,sql,id);
    }

    @Override
    public List<Book> queryBooks(Connection con) {
        String sql = "select `id` , `name` , `author` , `price` , `sales` , `stock` , `img_path` imgPath from t_book";
        List<Book> books = queryForList(con, sql);
        return books;
    }
}

19.4 编写图书模块的 Service

BookService 接口:

public interface BookService {

    public void addBook(Book book);

    public void deleteBookById(Integer id);

    public void updateBook(Book book);

    public Book queryBookById(Integer id);

    public List<Book> queryBooks();

}

BookServiceImpl 实现类:

public class BookServiceImpl implements BookService {
    private BookDao bookDao=new BookDaoImpl();

    @Override
    public void addBook(Book book) {
        Connection con = JDBCUtils.getConnection();
        bookDao.addBook(con,book);
        JDBCUtils.close(con);
    }

    @Override
    public void deleteBookById(Integer id) {
        Connection con = JDBCUtils.getConnection();
        bookDao.deleteBookById(con,id);
        JDBCUtils.close(con);
    }

    @Override
    public void updateBook(Book book) {
        Connection con = JDBCUtils.getConnection();
        bookDao.updateBook(con,book);
        JDBCUtils.close(con);
    }

    @Override
    public Book queryBookById(Integer id) {
        Connection con = JDBCUtils.getConnection();
        Book book = bookDao.queryBookById(con, id);
        JDBCUtils.close(con);
        return book;
    }

    @Override
    public List<Book> queryBooks() {
        Connection con = JDBCUtils.getConnection();
        List<Book> books = bookDao.queryBooks(con);
        JDBCUtils.close(con);
        return books;
    }
}

19.5 编写图书模块的 Web 层,和页面联调测试

19.5.1 图书列表功能的实现

1、图解列表功能流程:

image-20201215112504206

2、创建一个BookServlet继承BaseServlet,在里边添加一个list方法,当用户点击图书管理的时候,会访问这个BookServlet。然后BookServlet查出数据,保存到Request域当中,然后请求转发到pages/managaer/book_manager.jsp页面。这样的话就可以在这个jsp页面里遍历保存在requset域当中的全部图书信息了:

public class BookServlet extends BaseServlet {
    private BookService bookService=new BookServiceImpl();

    /**
     * 查询所有书籍并放入request域转发到book_manager.jsp的方法
     * @param request
     * @param response
     * @throws ServletException
     * @throws IOException
     */
    public void list(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //1.通过 BookService 查询全部图书
        List<Book> books = bookService.queryBooks();
        //2.把全部图书保存到requset域中
        request.setAttribute("books",books);
        //3.请求转发到pages/manager/book_manager.jsp页面
        request.getRequestDispatcher("/pages/manager/book_manager.jsp").forward(request,response);
    }
}

3、修改【图书管理】请求地址

这样改了之后,直接访问pages/manager/book_manager.jsp是没有数据的,只有通过访问图书管理超链接(也就是BookServlet)转发过去,才有数据。而加上?action=list表示是指定请求参数action的值为list,那么BaseServlet就能通过反射直接调用BookServlet中的list方法。

image-20201215124237699

4、修改BaseServlet添加doGet方法

通过超链接直接访问Servlet的话,是默认调用的get方法,所以要修改一下BaseServlet,写上doGet方法,里边调用doPost。

public abstract class BaseServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doPost(req,resp);
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 1.根据name获取隐藏域的value值
        String action = request.getParameter("action");
        // 2.根据value使用反射调用方法(value的值和方法名相同)
        try {
            Method declaredMethod = this.getClass().getDeclaredMethod(action, HttpServletRequest.class, HttpServletResponse.class);//获取到方法对象
            //调用目标业务方法
            declaredMethod.invoke(this,request,response); //传入对象(this是当前的对象实例),和参数
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

5、修改 pages/manger/bookmanger.jsp 页面的数据遍历输出

这里使用JSTL标签库,所以要先导包

image-20201215125111091

bookmanger.jsp页面修改:

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>图书管理</title>

   <%-- 静态包含 base标签、css样式、jQuery文件 --%>
   <%@ include file="/pages/common/head.jsp"%>
    
</head>
<body>
   
   <div id="header">
         <img class="logo_img" alt="" src="../../static/img/logo.gif" >
         <span class="wel_word">图书管理系统</span>

      <%-- 静态包含 manager管理模块的菜单  --%>
      <%@include file="/pages/common/manager_menu.jsp"%>

   </div>
   
   <div id="main">
      <table>
         <tr>
            <td>名称</td>
            <td>价格</td>
            <td>作者</td>
            <td>销量</td>
            <td>库存</td>
            <td colspan="2">操作</td>
         </tr>
         <%--items表示要遍历的集合(用EL表达式得到request域中的名字为books的值),var表示遍历得到的数据(每一个Book对象)--%>
         <c:forEach items="${requestScope.books}" var="books">
            <tr>
               <td>${books.name}</td> <%--用EL表达式获取book对象的属性--%>
               <td>${books.price}</td>
               <td>${books.author}</td>
               <td>${books.sales}</td>
               <td>${books.stock}</td>
               <td><a href="book_edit.jsp">修改</a></td>
               <td><a href="#">删除</a></td>
            </tr>
         </c:forEach>

         <tr>
            <td></td>
            <td></td>
            <td></td>
            <td></td>
            <td></td>
            <td></td>
            <td><a href="book_edit.jsp">添加图书</a></td>
         </tr>  
      </table>
   </div>

   <%--静态包含页脚内容--%>
   <%@include file="/pages/common/footer.jsp"%>

</body>
</html>

6、结果:

image-20201215130814837

19.5.2 前后台的简单介绍

image-20201215145115202

#### 19.5.3 添加图书功能的实现

1、添加图书流程细节:

image-20201215152935821

2、问题说明:表单重复提交:

当用户提交完请求,浏览器会记录下最后一次请求的全部信息。当用户按下功能键 F5,就会发起浏览器记录的最后一次请求。所以不能使用请求转发,因为请求转发是一次请求,如果发起浏览器记录的最后一次请求的话就相当于再调用一次整个流程,又重新添加了一次书本,所以这里要使用重定向。

3、BookServlet 程序中添加 add 方法:

(如果存在乱码的情况记得在BaseServlet中添加 处理响应乱码问题。)

image-20201215155016501

/**
 * 添加图书方法,并且重定向到图书列表页面
 * @param request
 * @param response
 * @throws IOException
 */
public void add(HttpServletRequest request, HttpServletResponse response) throws IOException {
    // 调用WebUtils工具类,直接把获得到的表单数据封装为book对象
    Book book = WebUtils.copyParmToBean(request.getParameterMap(), new Book());
    // 调用service保存对象
    bookService.addBook(book);
    // 重定向到图书列表页面
    response.sendRedirect(request.getContextPath()+"/manager/bookServlet?action=list");
}

19.5.4 删除图书功能的实现

1、图解删除流程:

image-20201215160958319

2、BookServlet 程序中的 delete 方法:

/**
 * 根据id删除图书,并且重定向到图书列表页面的方法
 * @param request
 * @param response
 * @throws IOException
 */
public void delete(HttpServletRequest request, HttpServletResponse response) throws IOException {
    // 获取表单参数
    String id = request.getParameter("id");
    // 删除需要的是参数是一个Integer,所以要把String转成Integer
    // 因为这种代码可能存在很多,所以做成工具类中的一个方法
    Integer integerId = WebUtils.parseInt(id, 0);
    bookService.deleteBookById(integerId);
    // 重定向回图书列表管理页面
    response.sendRedirect(request.getContextPath()+"/manager/bookServlet?action=list");
}

3、给 WebUtils 工具类添加转换 int 类型的工具方法:

/**
 * 将字符串转换成为 int 类型的数据
 * @param strInt
 * @param defaultValue 如果出错了要返回的默认值
 * @return
 */
public static Integer parseInt(String strInt,int defaultValue){
    //可能存在错误,比如字符串内内容不是数字,所以用try catch包裹
    try {
        return Integer.parseInt(strInt);
    } catch (NumberFormatException e) {
        e.printStackTrace();
    }
    return defaultValue;
}

4、修改删除的连接地址,并加上class属性:

image-20201215164957735

5、给删除添加确认提示操作:

在book_manager的head中加入。

<script type="text/javascript">
   $(function () { //入口函数
           //给删除的 a 标签绑定单击事件,用于删除的确认提示操作
           $("a.deleteClass").click(function () {
               /* 在事件的 function 函数中,有一个 this 对象。这个 this 对象,是当前正在响应事件的 dom 对象,也就是
           <a class="deleteClass" href="manager/bookServlet?action=delete&id=$ {books.id}">删除</a>
            这个a标签,通过它的父类的父类的第一个td元素得到里边的文本内容。 */
               /*
               * confirm 是确认提示框函数
               * 参数是它的提示内容
               * 它有两个按钮,一个确认,一个是取消。
               * 返回 true 表示点击了,确认,返回 false 表示点击取消。
               */
               return confirm("你确定要删除【" + $(this).parent().parent().find("td:first").text() + "】?");
               // return false 代表阻止元素的默认行为=不提交请求 true则为提交
           })
       })
</script>

19.5.5 修改图书功能的实现

1、图解修改图书细节(三个方案都能解决问题,选一个使用):

image-20201215180805047

2、更新 修改按钮跳转的请求地址:

修改的页面需要回显点击的那一行的数据,增加用户体验。所以要带上一个action=getBook是为了调用Servlet中的getBook方法来回显数据,并且带上要修改的那一行的Book的id值。

image-20201215181515162

3、BookServlet 程序中添加 getBook 方法:

/**
 * 根据id查询出图书对象,并且请求转发给book_edit.jsp页面的方法
 * @param request
 * @param response
 * @throws ServletException
 * @throws IOException
 */
public void getBook(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    // 获取请求带过来的参数(图书编号)
    String id = request.getParameter("id");
    // 根据id查询到对象
    Book book = bookService.queryBookById(WebUtils.parseInt(id, 0));
    // 把book对象设置到Request域中
    request.setAttribute("book",book);
    // 请求转发到修改的页面,在修改的页面读取到数据 达到回显功能
    request.getRequestDispatcher("/pages/manager/book_edit.jsp").forward(request,response);
}

4、在 book_edit.jsp 页面中显示修改的数据

image-20201215182725911

回显的效果如下:

image-20201215183115526

image-20201215183121043

5、在 BookServlet 程序中添加 update 方法:

/**
 * 根据id修改数据
 * @param request
 * @param response
 * @throws IOException
 */
public void update(HttpServletRequest request, HttpServletResponse response) throws IOException {
    // 用工具类把表单的数据封装到User对象中
    Book book = WebUtils.copyParmToBean(request.getParameterMap(), new Book());
    // 调用Service完成对象修改功能
    bookService.updateBook(book);
    // 重定向到图书管理页面
    // 地址:/工程名/manger/bokServlet?action=list
    response.sendRedirect(request.getContextPath() + "/manager/bookServlet?action=list");
}

6、解决 book_edit.jsp 页面,即要实现添加,又要实现修改操作。

因为新增和修改都在book_edit.jsp这个页面,所以需要让隐藏域中的action动态起来,这样BaseServlet才能判断要调用新增还是修改的方法,这里我们使用图解修改图书细节的三个方案中的第一个。

对book_manager.jsp页面进行修改:

请求发起时带上方法名这个参数。

image-20201215190643236

对book_edit.jsp进行修改:

这个页面的action改成请求过来的方法名参数。

image-20201215184921804

19.6 图书分页

19.6.1 分页模块的分析

image-20201216100945459

19.6.2 分页模型 Page 的抽取

当前页数,总页数,总记录数,当前页数据,每页记录数。当前页数据最好使用泛型,这样的话都可以用。

/*
* Page 是分页的模型对象
* @parm <T> 是具体的模块的 javBean 类
*/
public class Page<T> {
    public static final Integer PAGE_SIZE = 4;
    private Integer pageNo;//当前页码
    private Integer pageTotal;//总页码
    private Integer pageTotalCount;//总记录数
    private Integer pageSize = PAGE_SIZE;//每页显示数量(如果用户不给的话有一个默认值每页显示4条)
    private List<T> items;//当前页数据

19.6.3 分页的初步实现

编写BookServlet程序增加一个page方法:

/**
     * 处理分页功能
     * @param request
     * @param response
     * @throws ServletException
     * @throws IOException
     */
    public void page(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //只需要得到当前页码和每页显示数量,别的都可以得到或者求出!
        //1.获取请求参数pageNo和pageSize
        String strPageNo = request.getParameter("pageNo"); //当前页码
        String strPageSize = request.getParameter("pageSize"); //每页显示数量
        //调用WebUtils工具类把String变成Integer
        Integer pageNo = WebUtils.parseInt(strPageNo, 1);//如果没有给值的话默认是第一页!
        Integer pageSize = WebUtils.parseInt(strPageSize,Page.PAGE_SIZE);//如果没有给值的话默认是Page对象里的常量PAGE_SIZE(4)。
        //2.调用BookService传入pageNo和pageSize得到Page对象
        Page<Book> page = bookService.page(pageNo, pageSize);
        //3.把page对象保存到Requset域中
        request.setAttribute("page",page);
        //4.请求转发给book_manager.jsp
        request.getRequestDispatcher("/pages/manager/book_manager.jsp").forward(request,response);
    }

修改BookService:

在BookService中创建出BookServlet中用到的BookService.page(pageNo,pageSize);方法:

    @Override
    public Page page(Integer pageNo, Integer pageSize) {
        Connection con = JDBCUtils.getConnection();
        //1. 创建一个Page对象然后往里边放数据
        Page<Book> page=new Page<>();
        page.setPageNo(pageNo); //当前页码 已经有了直接放入
        page.setPageSize(pageSize); //每页显示数量 已经有直接放入
        //2. 接下来要得到 pageTotalCount总记录数,pageTotal总页码,items当前页数据
        //2.1 如何得到总记录数,总记录数其实就是sql语句count(*)统计出来
        Integer pageTotalCount = bookDao.queryForPageTotalCount(con);
        page.setPageTotalCount(pageTotalCount); //设置总记录数进入对象
        //2.2 如何得到总页码,其实就是(总记录数 % 每页显示数量)如果除的尽说明刚好,有余的话说明要+1
        Integer pageTotal = (pageTotalCount%pageSize)>0 ? (pageTotalCount/pageSize)+1 : pageTotalCount/pageSize;
        page.setPageTotal(pageTotal);//设置总页码进入
        //2.3 如何得到当前页数据,要使用数据库查询并使用分页功能
        //    数据库分页需要两个参数,一个起始位置(如果是0表示从第一条数据开始),一个要取几个(如果是5,表示从第一条数据开始取5个)
        //    设置起始位置,其实就是(当前页码-1) * 每页显示数量(例如当前页码是2,每页显示数量是10 那么就是1*10,那么就代表从10开始取数据)
        int begin = (pageNo-1)*pageSize;
        //    而要取几个其实就是每页显示数量,直接传入这两个参数
        List<Book> items = bookDao.queryForPageItems(con,begin,pageSize);
        page.setItems(items);//设置当前页面数据进入
        JDBCUtils.close(con);
        //3. 返回Page对象
        return page;
    }

修改BookDao:

在BookDao中创建出BookService中用到的bookDao.queryForPageItems(begin,pageSize);和bookDao.queryForPageTotalCount();方法。

    @Override
    public Integer queryForPageTotalCount(Connection con) {
        String sql="SELECT count(*) FROM t_book";
        //因为该sql语句返回的是long类型,所以用所有基础类型的父类Number接受
        Number number = (Number) queryForSingleValue(con, sql);
        //再用Number的intValue()把该数据转化为int类型
        int i = number.intValue();
        return i; //自动装箱
    }

    @Override
    public List<Book> queryForPageItems(Connection con, int begin, Integer pageSize) {
        String sql = "SELECT `id` , `name` , `author` , `price` , `sales` , `stock` , `img_path` FROM t_book limit ?,?";
        List<Book> books = queryForList(con, sql, begin, pageSize);
        return books;
    }

manager_menu.jsp 中【图书管理】请求地址的修改:

要把请求中的action=list改成action=page,以此来访问page方法,不再访问list方法,达成分页的目的。

image-20201216111645590

book_manager.jsp 修改:

image-20201216125447191

效果:

image-20201216125600758

19.6.4 首页、下一页、上一页、末页实现

修改book_manager.jsp:

<div id="page_nav">
   <%--页码大于1(不是首页的时候),才显示首页和下一页按钮,是首页的时候就没必要显示了--%>
   <c:if test="${requestScope.page.pageNo > 1}">
      <a href="manager/bookServlet?action=page&pageNo=1">首页</a> <%--首页就是pageNo=1--%>
      <a href="manager/bookServlet?action=page&pageNo=${requestScope.page.pageNo-1}">上一页</a> <%--上一页就是当前页码-1--%>
   </c:if>
   <a href="#">3</a>
   【${requestScope.page.pageNo}】<%--代表当前页码--%>
   <a href="#">5</a>
      <%-- 如果已经是最后一页,则不显示下一页,末页 --%>
   <c:if test="${requestScope.page.pageNo < requestScope.page.pageTotal}">
      <a href="manager/bookServlet?action=page&pageNo=${requestScope.page.pageNo+1}">下一页</a> <%--下一页就是当前页码+1--%>
      <a href="manager/bookServlet?action=page&pageNo=${requestScope.page.pageTotal}">末页</a><%--末页</a>末页就是总页数--%>
   </c:if>
   共${requestScope.page.pageTotal}页,${requestScope.page.pageTotalCount}条记录 到第<input value="4" name="pn" id="pn_input"/>页
   <input type="button" value="确定">
</div>

效果:

image-20201216152911378

19.6.5 分页模块中跳转到指定页数功能实现

head.jsp的修改:

image-20201216163026068

book_manager.jsp页面的修改:

image-20201216163336034

前端校验:

输入的值不能大于最大页数,也不能小于1。

image-20201216183242253

前端校验效果:

image-20201216183305279

后端校验:

修改Page对象,防止对方直接在浏览器后边输入pageNo=xxx

image-20201216165816019

要对输入的页码进行判断,不能大于最大页数,也不能小于1。如果大于最大页数那就设置为最大页数的值,如果小于1那就设置为1。

public void setPageNo(Integer pageNo) {
    /* 数据边界的有效检查 */
    if (pageNo<1){
    pageNo=1;
    }else if (pageNo>pageTotal){
    pageNo=pageTotal;
    }
    this.pageNo = pageNo;
}

修改BookService:

这里有个注意点,还要对service进行修改,让设置页码放在设置总页码之后。否则设置页码的时候总页码还没设置,那页码里的判断就会空指针异常,并且还要放到求begin之前,因为begin会用到pageNo。还有一个小细节,这里的int begin的判断里的pageNo要改为page.getPageNo(),因为传过来的可能大于总页码,而page类里边进行了判断操作,改变了page对象的pageNo值。

image-20201216172226943

后端校验效果:

image-20201216172446498

19.6.6 分页模块中,页码 1,2,【3】,4,5 的显示。

需求:显示 5 个连续的页码,除了当前页码之外,每个页码都可以点击跳到指定页。

image-20201216184224972

image-20201216184232686

book_manager.jsp修改:

    <%--页码 1,2,【3】,4,5 的显示--%>
<c:choose>     <%--这玩意其实就相当于switch case--%>
      <%--情况1:如果总页码小于等于5的情况,页码的范围是:1-总页码--%>
      <c:when test="${ requestScope.page.pageTotal <= 5 }">
         <c:set var="begin" value="1"/>
         <c:set var="end" value="${requestScope.page.pageTotal}"/>
      </c:when>
      <%--情况2:总页码大于5的情况--%>
      <c:when test="${requestScope.page.pageTotal > 5}">
         <c:choose>
            <%--小情况1:当前页码为前面3个:1,2,3的情况,页码范围是:1-5.--%>
            <c:when test="${requestScope.page.pageNo <= 3}">
               <c:set var="begin" value="1"/>
               <c:set var="end" value="5"/>
            </c:when>
            <%--小情况2:当前页码为最后3个,8,9,10,页码范围是:总页码减4 - 总页码--%>
            <c:when test="${requestScope.page.pageNo > requestScope.page.pageTotal-3}">
               <c:set var="begin" value="${requestScope.page.pageTotal-4}"/>
               <c:set var="end" value="${requestScope.page.pageTotal}"/>
            </c:when>
            <%--小情况3:4,5,6,7,页码范围是:当前页码减2 - 当前页码加2--%>
            <c:otherwise>
               <c:set var="begin" value="${requestScope.page.pageNo-2}"/>
               <c:set var="end" value="${requestScope.page.pageNo+2}"/>
            </c:otherwise>
         </c:choose>
      </c:when>
   </c:choose>

   <c:forEach begin="${begin}" end="${end}" var="i">
      <c:if test="${i == requestScope.page.pageNo}">
         【${i}】
      </c:if>
      <c:if test="${i != requestScope.page.pageNo}">
         <a href="manager/bookServlet?action=page&pageNo=${i}">${i}</a>
      </c:if>
   </c:forEach>

19.6.7 修改分页后,增加,删除,修改图书信息的回显页面

以修改图书为示例:

1、在修改的请求地址上追加当前页码参数:

image-20201216193202072

2、在 book_edit.jsp 页面中使用隐藏域记录下 pageNo 参数:

image-20201216193239245

3、在服务器重定向的时候,获取当前页码追加上进行跳转,把方法改回page方法:

image-20201216193333202

19.7 首页 index.jsp 的跳转

1、图示流程:

因为直接访问index.jsp是没有数据的,而通过servlet来转发回index.jsp又太丑陋,因为基本上所有网站的主页都是index.jsp。所以想一个办法,index.jsp只负责请求转发,转发到servlet页面,而Servlet做好相应的操作之后,转发给:在web目录/pages/client目录下新建一个index.jsp,这才是真正的首页。不过因为请求转发并不会改变浏览器搜索栏的地址,所以看上去还在http://ip:port/工程路径/index.jsp。

image-20201216201234159

2、在web目录/pages/client目录下新建一个index.jsp,把web下的index.jsp的内容拷贝过去:

3、修改web下的index.jsp,里边只做请求转发功能:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%--只负责请求转发--%>
<jsp:forward page="/client/bookServlet?action=page"></jsp:forward>

4、编写ClientBookServlet:

ClientBookServlet代码:

public class ClientBookServlet extends BaseServlet {
    BookService bookService=new BookServiceImpl();
    /**
     * 处理分页功能
     * @param request
     * @param response
     * @throws ServletException
     * @throws IOException
     */
    public void page(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //只需要得到当前页码和每页显示数量,别的都可以得到或者求出!
        //1.获取请求参数pageNo和pageSize
        String strPageNo = request.getParameter("pageNo"); //当前页码
        String strPageSize = request.getParameter("pageSize"); //每页显示数量
        //调用WebUtils工具类把String变成Integer
        Integer pageNo = WebUtils.parseInt(strPageNo, 1);//如果没有给值的话默认是第一页!
        Integer pageSize = WebUtils.parseInt(strPageSize, Page.PAGE_SIZE);//如果没有给值的话默认是Page对象里设置的常量PAGE_SIZE也就是4!
        //2.调用BookService传入pageNo和pageSize得到Page对象
        Page<Book> page = bookService.page(pageNo, pageSize);
        //3.把page对象保存到Requset域中
        request.setAttribute("page",page);
        //4.请求转发给index.jsp真正的首页
        request.getRequestDispatcher("/pages/client/index.jsp").forward(request,response);
    }
}

web.xml配置:

<servlet>
    <servlet-name>ClientBookServlet</servlet-name>
    <servlet-class>com.Luo.web.ClientBookServlet</servlet-class>
</servlet>
<servlet-mapping>
     <servlet-name>ClientBookServlet</servlet-name>
     <url-pattern>/client/bookServlet</url-pattern>
</servlet-mapping>

5、/pages/client/index.jsp进行修改,把数据遍历出来,分页效果直接从book_manager.jsp中复制:

可以用ctrl+r 替换功能把复制过来的分页效果的所有manager/bookServlet改成client/bookServlet。

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>书城首页</title>

   <%-- 静态包含 base标签、css样式、jQuery文件 --%>
   <%@ include file="/pages/common/head.jsp"%>

</head>
<body>
   
   <div id="header">
         <img class="logo_img" alt="" src="static/img/logo.gif" >
         <span class="wel_word">网上书城</span>
         <div>
            <a href="pages/user/login.jsp">登录</a> |
            <a href="pages/user/regist.jsp">注册</a> &nbsp;&nbsp;
            <a href="pages/cart/cart.jsp">购物车</a>
            <a href="pages/manager/manager.jsp">后台管理</a>
         </div>
   </div>

   <div id="main">
      <div id="book">
         <div class="book_cond">
            <form action="" method="get">
               价格:<input id="min" type="text" name="min" value=""> 元 - 
                  <input id="max" type="text" name="max" value=""> 元 
                  <input type="submit" value="查询" />
            </form>
         </div>
         <div style="text-align: center">
            <span>您的购物车中有3件商品</span>
            <div>
               您刚刚将<span style="color: red">时间简史</span>加入到了购物车中
            </div>
         </div>

         <c:forEach items="${requestScope.page.items}" var="book">
            <div class="b_list">
               <div class="img_div">
                  <img class="book_img" alt="" src="${book.imgPath}" />
               </div>
               <div class="book_info">
                  <div class="book_name">
                     <span class="sp1">书名:</span>
                     <span class="sp2">${book.name}</span>
                  </div>
                  <div class="book_author">
                     <span class="sp1">作者:</span>
                     <span class="sp2">${book.author}</span>
                  </div>
                  <div class="book_price">
                     <span class="sp1">价格:</span>
                     <span class="sp2">${book.price}</span>
                  </div>
                  <div class="book_sales">
                     <span class="sp1">销量:</span>
                     <span class="sp2">${book.sales}</span>
                  </div>
                  <div class="book_amount">
                     <span class="sp1">库存:</span>
                     <span class="sp2">${book.stock}</span>
                  </div>
                  <div class="book_add">
                     <button>加入购物车</button>
                  </div>
               </div>
            </div>
         </c:forEach>

      </div>

      <div id="page_nav">
         <%--页码大于1(不是首页的时候),才显示首页和下一页按钮,是首页的时候就没必要显示了--%>
         <c:if test="${requestScope.page.pageNo > 1}">
            <a href="client/bookServlet?action=page&pageNo=1">首页</a> <%--首页就是pageNo=1--%>
            <a href="client/bookServlet?action=page&pageNo=${requestScope.page.pageNo-1}">上一页</a> <%--上一页就是当前页码-1--%>
         </c:if>

         <%--页码 1,2,【3】,4,5 的显示--%>
         <c:choose>     <%--这玩意其实就相当于switch case--%>
            <%--情况1:如果总页码小于等于5的情况,页码的范围是:1-总页码--%>
            <c:when test="${ requestScope.page.pageTotal <= 5 }">
               <c:set var="begin" value="1"/>
               <c:set var="end" value="${requestScope.page.pageTotal}"/>
            </c:when>
            <%--情况2:总页码大于5的情况--%>
            <c:when test="${requestScope.page.pageTotal > 5}">
               <c:choose>
                  <%--小情况1:当前页码为前面3个:1,2,3的情况,页码范围是:1-5.--%>
                  <c:when test="${requestScope.page.pageNo <= 3}">
                     <c:set var="begin" value="1"/>
                     <c:set var="end" value="5"/>
                  </c:when>
                  <%--小情况2:当前页码为最后3个,8,9,10,页码范围是:总页码减4 - 总页码--%>
                  <c:when test="${requestScope.page.pageNo > requestScope.page.pageTotal-3}">
                     <c:set var="begin" value="${requestScope.page.pageTotal-4}"/>
                     <c:set var="end" value="${requestScope.page.pageTotal}"/>
                  </c:when>
                  <%--小情况3:4,5,6,7,页码范围是:当前页码减2 - 当前页码加2--%>
                  <c:otherwise>
                     <c:set var="begin" value="${requestScope.page.pageNo-2}"/>
                     <c:set var="end" value="${requestScope.page.pageNo+2}"/>
                  </c:otherwise>
               </c:choose>
            </c:when>
         </c:choose>

         <c:forEach begin="${begin}" end="${end}" var="i">
            <c:if test="${i == requestScope.page.pageNo}">
               【${i}】
            </c:if>
            <c:if test="${i != requestScope.page.pageNo}">
               <a href="client/bookServlet?action=page&pageNo=${i}">${i}</a>
            </c:if>
         </c:forEach>

         <%-- 如果已经是最后一页,则不显示下一页,末页 --%>
         <c:if test="${requestScope.page.pageNo < requestScope.page.pageTotal}">
            <a href="client/bookServlet?action=page&pageNo=${requestScope.page.pageNo+1}">下一页</a> <%--下一页就是当前页码+1--%>
            <a href="client/bookServlet?action=page&pageNo=${requestScope.page.pageTotal}">末页</a><%--末页</a>末页就是总页数--%>
         </c:if>
         共${requestScope.page.pageTotal}页,${requestScope.page.pageTotalCount}条记录 到第<input value="${requestScope.page.pageNo}" name="pn" id="pn_input"/>页
         <input type="button" value="确定" id="searchPageBtn">
         <script type="text/javascript">
                $(function () {//入口函数
                    $("#searchPageBtn").click(function () { //给跳转到指定页码的确认按钮增加单击事件
                        var pageNo = $("#pn_input").val(); //获取要跳到第几页的值
                        // 前端验证,不能输入小于-1或者大于最大页码的值
                        var pageTotal= ${requestScope.page.pageTotal} //获取到最大的页码
                        if (pageNo>pageTotal){
                            alert("你输入的页码超过了最大页码!");
                            return false;
                        }
                        if (pageNo<1){
                            alert("你输入的页码小于1!");
                            return false;
                        }
                        // javScript 语言中提供了一个 location 地址栏对象
                        // 它有一个属性叫 href.它可以获取浏览器地址栏中的地址
                        // href 属性可读,可写 (下边的代码相当于点了这个绑定了单机事件的按钮之后浏览器的地址就会被改成为下边填写的)
                        // pageScope是EL表达式的写法,其实代表的就是pageContext域(这个域只要项目不停数据就还在)
                        // $ {pageScope.basePath}代表的意思就是从pageContext域中获取之前在head.jsp中存入的动态base标签
                        // 这里用动态base标签而不写死的原因是,别人访问过来的时候如果写的是localhost那不就访问他自己本地的网页去了
                        location.href = "${pageScope.basePath}client/bookServlet?action=page&pageNo="+pageNo;
                    })
                })
         </script>
      </div>
   
   </div>

   <%--静态包含页脚内容--%>
   <%@include file="/pages/common/footer.jsp"%>

</body>
</html>

19.8 分页条的抽取

发现一个页面中的例如book_manager.jsp页面的分页条的请求地址都相同:

image-20201217105940701

image-20201217110016450

而例如index.jsp的分页条的请求地址也都相同:

image-20201217110104932

所以可以把这个地址抽出来,在对应的Servlet存入request域中,然后再获取。

1、在 page 对象中添加 url 属性:

image-20201217110409783

2、在 Servlet 程序的 page 分页方法中设置 url 的分页请求地址:

2.1、BookServlet中添加url:

image-20201217110814001

2.2、ClientServlet中添加url:

image-20201217110836040

3、把index.jsp和book_manager中的分页请求地址都替换成从request域中获取:

这里以book_manager为例,用crtl+r替换。

image-20201217111059225

4、抽取分页条:

完成第三步的操作后会发现,两个页面的分页条的内容完全一致,他的请求地址会自动从Servlet获取对应的。

image-20201217111342068

image-20201217111359433

这样的话可以把分页操作单独抽出来,做成一个jsp页面。

image-20201217111655555

再把index.jsp和book_manager中的分页操作替换成静态包含,引入单独的分页操作jsp页面。

这里以index.jsp为例,代码简洁了许多, 而且方便维护,并且如果有别的需要用到分页操作可以直接静态包含!

image-20201217111916843

19.9 首页价格搜索

1、图解列表功能流程:

image-20201217113543176

2、修改index.jsp页面的价格搜索表单:

更改请求地址到Servlet,添加一个隐藏域存放要执行的方法名。

image-20201217130149902

3、编写ClientServlet增加pageByPrice方法:

/**
 * 根据价格分页处理功能
 * @param request
 * @param response
 * @throws ServletException
 * @throws IOException
 */
public void pageByPrice(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    //1.获取请求参数pageNo和pageSize
    String strPageNo = request.getParameter("pageNo"); //当前页码
    String strPageSize = request.getParameter("pageSize"); //每页显示数量
    String min = request.getParameter("min");
    String max = request.getParameter("max");
    Integer pageNo = WebUtils.parseInt(strPageNo, 1);//如果没有给值的话默认是第一页!
    Integer pageSize = WebUtils.parseInt(strPageSize, Page.PAGE_SIZE);//如果没有给值的话默认是Page对象里设置的常量PAGE_SIZE也就是4!
    Integer integer1 = WebUtils.parseInt(min, 0);
    Integer integer = WebUtils.parseInt(max, Integer.MAX_VALUE);

    //2.调用BookService得到Page对象
    Page<Book> page = bookService.pageByPrice(pageNo, pageSize,min,max);
    // 把分页的url请求地址存入requset域中
    page.setUrl("client/bookServlet?action=pageByPrice");

    //3.把page对象保存到Requset域中
    request.setAttribute("page",page);
    //4.请求转发给index.jsp真正的首页
    request.getRequestDispatcher("/pages/client/index.jsp").forward(request,response);
}

4、在BookService中编写pageByPrice方法中的bookService.pageByPrice方法:

@Override
public Page<Book> pageByPrice(Integer pageNo, Integer pageSize, String min, String max) {
    Connection con = JDBCUtils.getConnection();
    //1. 创建一个Page对象然后往里边放数据
    Page<Book> page=new Page<>();
    page.setPageSize(pageSize); //每页显示数量 已经有直接放入

    //2. 接下来要得到 pageTotalCount总记录数,pageTotal总页码,items当前页数据
    //2.1 得到总记录数
    Integer pageTotalCount = bookDao.queryForPageTotalCountByPrice(min,max);
    page.setPageTotalCount(pageTotalCount); //设置总记录数进入对象

    //2.2 得到总页码
    Integer pageTotal = (pageTotalCount%pageSize)>0 ? (pageTotalCount/pageSize)+1 : pageTotalCount/pageSize;
    page.setPageTotal(pageTotal);//设置总页码进入
    page.setPageNo(pageNo); //设置当前页码 放到设置总页码之后防止比较的时候产生空指针异常

    //2.3 如何得到当前页数据,要使用数据库查询并使用分页功能
    int begin = (page.getPageNo()-1)*pageSize;
    List<Book> items = bookDao.queryForPageItemsByPrice(con,min,max,begin,pageSize);
    page.setItems(items);//设置当前页面数据进入
    JDBCUtils.close(con);

    //3. 返回Page对象
    return page;
}

5、在BookDao中编写pageByPrice方法中的bookDao.queryForPageTotalCountByPrice和bookDao.queryForPageItemsByPrice方法:

@Override
public Integer queryForPageTotalCountByPrice(Connection con,String min, String max) {
    String sql ="SELECT count(*) FROM t_book WHERE price BETWEEN ? AND ?";
    Number number = (Number) queryForSingleValue(con, sql, min, max);
    int i = number.intValue();
    return i;
}

@Override
public List<Book> queryForPageItemsByPrice(Connection con, String min, String max, int begin, Integer pageSize) {
    String sql="SELECT `id` , `name` , `author` , `price` , `sales` , `stock` , `img_path` imgPath " +
            "FROM t_book WHERE price BETWEEN ? AND ? ORDER BY price LIMIT ?,?";
    List<Book> books = queryForList(con, sql, min, max, begin, pageSize);
    return books;
}

6、在价格查询中回显输入的价格,增加用户体验:

image-20201217142706549

image-20201217142804448

7、让分页条也带上价格区间的请求,不带上的话就会变成默认价格区间:

修改ClientBookServlet。

// 把分页的url请求地址存入requset域中
StringBuilder sb = new StringBuilder("client/bookServlet?action=pageByPrice");
// 如果有最小价格的参数,追加到分页条的地址参数中
if (request.getParameter("min") != null) {
    sb.append("&min=").append(request.getParameter("min"));
}
// 如果有最大价格的参数,追加到分页条的地址参数中
if (request.getParameter("max") != null) {
    sb.append("&max=").append(request.getParameter("max"));
}
page.setUrl(sb.toString());

20. 登陆---显示用户名

UserServlet 程序中保存用户登录的信息:

image-20201221112140534

修改login_succuess_menu.jsp:

image-20201218104750183

还要修改首页index.jsp 页面的菜单:

当登录成功的时候,不再显示登录和注册按钮。没登录的时候显示,比较美观。

image-20201218105253552

21. 登出---注销用户

1、销毁Session 中用户登录的信息(或者销毁Session)
2、重定向到首页(或登录页面)。

UserServlet 程序中添加logout 方法:

/**
 * 注销的方法
 * @param request
 * @param response
 * @throws IOException
 */
public void logout(HttpServletRequest request, HttpServletResponse response) throws IOException {
    //1、销毁Session 中用户登录的信息(或者销毁Session)
    HttpSession session = request.getSession();
    session.invalidate();//销毁Session
    //2、重定向到首页(或登录页面)。
    response.sendRedirect(request.getContextPath());//工程路径也就是首页
}

修改【注销】的菜单地址:

image-20201218110131970

22. 表单重复提交之---验证码

表单重复提交有三种常见的情况:
一:提交完表单。服务器使用请求转来进行页面跳转。这个时候,用户按下功能键F5,就会发起最后一次的请求。造成表单重复提交问题。解决方法:使用重定向来进行跳转
二:用户正常提交服务器,但是由于网络延迟等原因,迟迟未收到服务器的响应,这个时候,用户以为提交失败,就会着急,然后多点了几次提交操作,也会造成表单重复提交。
三:用户正常提交服务器。服务器也没有延迟,但是提交完成后,用户回退浏览器。重新提交。也会造成表单重复提交。

第一种还能使用重定向来解决,那么第二种第三种呢?使用验证码来解决:

image-20201218112907968

22.1 谷歌kaptcha 图片验证码的使用

1、导入谷歌验证码的jar 包:
kaptcha-2.3.2.jar
2、在web.xml 中去配置用于生成验证码的Servlet 程序:

<servlet>
    <servlet-name>KaptchaServlet</servlet-name>
    <servlet-class>com.google.code.kaptcha.servlet.KaptchaServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>KaptchaServlet</servlet-name>
    <url-pattern>/kaptcha.jpg</url-pattern>
</servlet-mapping>

3、编写UserServlet中的regist方法:

在服务器获取谷歌生成的验证码和客户端发送过来的验证码比较使用。

/**
 * 处理注册的功能
 * @param request
 * @param response
 * @throws ServletException
 * @throws IOException
 */
public void regist(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    //获取谷歌生成的验证码(他会自动存在Session中,直接去Session中拿去):
    HttpSession session = request.getSession();//得到session对象
    //根据key得到value
    String token = (String) session.getAttribute(KAPTCHA_SESSION_KEY);//KAPTCHA_SESSION_KEY这是一个常量 其实就是字符串“KAPTCHA_SESSION_KEY”
    //删除Session中的验证码(防止表单重复提交)
    request.getSession().removeAttribute(KAPTCHA_SESSION_KEY);

    //获取请求的参数
    String username = request.getParameter("username");
    //String password = request.getParameter("password");(password不需要回传,不需要获取了)
    String email = request.getParameter("email");
    String code = request.getParameter("code");

    //使用自己写的WebUtils工具类调用方法直接一次性注入参数
    User user = WebUtils.copyParmToBean(request.getParameterMap(), new User());//一行代码搞定
    //1.判断验证码是否正确
    if (token.equalsIgnoreCase(code)){ //如果正确,这里还没学,暂时只能写死验证码
        //2.判断用户名是否存在
        boolean flag = service.existsUsername(user);
        if (flag){ //如果用户名不存在
                service.register(user);//调用Service保存用户
            
               //注册成功,往Session里设置键值
                session.setAttribute("user",user);
            
                // 跳到注册成功页面 regist_success.html
                response.sendRedirect(request.getContextPath()+"/pages/user/regist_success.jsp");
        }else { //如果用户名已存在
            System.out.println("用户名已存在");
            // 把回显信息,保存到Request域中
            request.setAttribute("msg","用户名已存在!"); //回显错误的原因信息
            request.setAttribute("username",username); //回显用户名,这样的话用户输入的用户名还保存着,不用再次输入
            request.setAttribute("email",email); //回显邮箱,这样的话用户输入的用户名还保存着,不用再次输入
            request.getRequestDispatcher("/pages/user/regist.jsp").forward(request,response); //请求转发也就是跳转回注册页面
        }
    } else { //如果验证码不正确
        System.out.println("验证码错误!");
        request.setAttribute("msg","验证码输入有误!"); //回显错误的原因信息
        request.setAttribute("username",username); //回显用户名,这样的话用户输入的用户名还保存着,不用再次输入
        request.setAttribute("email",email); //回显邮箱,这样的话用户输入的用户名还保存着,不用再次输入
        request.getRequestDispatcher("/pages/user/regist.jsp").forward(request,response); //请求转发也就是跳转回注册页面
    }
}

4、在表单中使用img 标签去显示验证码图片并使用它:

<label>验证码:</label>
<input class="itxt" type="text" name="code" style="width: 150px;" id="code" />
<img alt="" src="kaptcha.jpg" style="width: 100px; height: 40px;">

5、单击切换验证码:

可能火狐和IE会出现点击一次验证码之后图片不会变的情况,原因是:

image-20201218152540828

如何解决?加上时间戳,每次请求的地址都不同:

// 给验证码的图片,绑定单击事件
$("#code_img").click(function () {
// 在事件响应的function 函数中有一个this 对象。这个this 对象,是当前正在响应事件的dom 对象
// src 属性表示验证码img 标签的图片路径。它可读,可写
this.src = "${basePath}kaptcha.jpg?d=" + new Date();
});

23. 购物车模块

23.1 购物车模块分析

image-20201220102201306

23.2 购物车模型

因为使用的是Session,不再和数据库交互(不需要service和dao),所以方法直接写在bean中。

购物车对象Cart类:

public class Cart {
//  所有商品总数量  private Integer totalCount; 不需要这两个是因为这两个是计算出来的,而不是Set进去的,没必要写出来占用内存
//  所有商品总金额  private BigDecimal totalPrice; 这两个只需要有Get方法进行获取就行了 变量定义在方法内

    /**
     * key是商品编号,
     * value,是商品信息
     */
    private Map<Integer,CartItem> items = new LinkedHashMap<Integer,CartItem>();

    /**
     * 添加商品项
     *
     * @param cartItem
     */
    public void addItem(CartItem cartItem) {
        // 先查看购物车中是否已经添加过此商品,如果已添加,则数量累加,总金额更新,如果没有添加过,直接放到集合中即可
        CartItem item = items.get(cartItem.getId());

        if (item == null) {
            // 之前没添加过此商品
            items.put(cartItem.getId(), cartItem); //商品的id当作键,商品对象当作值,这样查询更方便更快
        } else {
            // 已经 添加过的情况
            item.setCount( item.getCount() + 1 ); // 数量 累加
            item.setTotalPrice( item.getPrice().multiply(new BigDecimal( item.getCount() )) ); // multiply代表乘 这一行代表:金额乘于数量=这个商品的总金额
        }

    }

    /**
     * 删除商品项
     */
    public void deleteItem(Integer id) {
        items.remove(id);
    }


    /**
     * 清空购物车
     */
    public void clear() {
        items.clear();
    }

    /**
     * 修改商品数量
     */
    public void updateCount(Integer id,Integer count) {
        // 先查看购物车中是否有此商品。如果有,修改商品数量,更新总金额
        CartItem cartItem = items.get(id);
        if (cartItem != null) {
            cartItem.setCount(count);// 修改商品数量
            cartItem.setTotalPrice( cartItem.getPrice().multiply(new BigDecimal( cartItem.getCount() )) ); // multiply代表乘 这一行代表:金额乘于数量=这个商品的总金额
        }
    }


    /**
     * 得到所有商品的总数量 并返回
     * @return
     */
    public Integer getTotalCount() {
        Integer totalCount = 0;

        //遍历所有商品,并把所有商品的数量累加
        for (Map.Entry<Integer,CartItem>entry : items.entrySet()) {
            totalCount += entry.getValue().getCount();
        }

        return totalCount;
    }

    /**
     * 得到所有商品的总金额 并返回
     * @return
     */
    public BigDecimal getTotalPrice() {
        BigDecimal totalPrice = new BigDecimal(0);

        //遍历所有商品,并把所有商品的金额累加
        for (Map.Entry<Integer,CartItem>entry : items.entrySet()) {
            totalPrice = totalPrice.add(entry.getValue().getTotalPrice());
        }

        return totalPrice;
    }


    public Map<Integer, CartItem> getItems() {
        return items;
    }

    public void setItems(Map<Integer, CartItem> items) {
        this.items = items;
    }

    @Override
    public String toString() {
        return "Cart{" +
                "totalCount=" + getTotalCount() +
                ", totalPrice=" + getTotalPrice() +
                ", items=" + items +
                '}';
    }

}

购物车的商品项对象CartItem类:

为什么单独用一个CartItem类而不是直接使用Book类呢,因为购物车其实可能放多种物品,不止单单放入图书,单独抽出来更加合理。

public class CartItem {
    private Integer id;
    private String name;
    private Integer count;
    private BigDecimal price;
    private BigDecimal totalPrice;
    //get、set、有参构造、toString方法。
    }

23.3 加入购物车功能的实现

index.jsp 页面js 的代码修改:

image-20201220152947874

<script type="text/javascript">
$(function () {
       // 给加入购物车按钮绑定单击事件
   $("button.addToCart").click(function () {
           /**
            * 在事件响应的function 函数中,有一个this 对象,这个this 对象,是当前正在响应事件的dom 对象
            * @type {jQuery}
            */
           var bookId=$(this).attr("bookId");
           //发情求给CartServlet并带上book的id
           location.href = "http://localhost:8080/book/cartServlet?action=addItem&id=" + bookId;
       })
   })
</script>

CartServlet 程序中的代码:

这里有一点需要注意的是,用户从哪个页面点击加入购物车之后,重定向也要回到这个页面,比如再第三页点击那么回到的也是第三页,增加用户体验:

image-20201220150835301

CartServlet代码:

/**
 * 加入购物车
 * @param req
 * @param resp
 * @throws ServletException
 * @throws IOException
 */
protected void addItem(HttpServletRequest req, HttpServletResponse resp) throws IOException {
    //1.获取请求参数中的图书id
    String idStr = req.getParameter("id");
    Integer id = WebUtils.parseInt(idStr, 0);
    //2.调用service根据id得到book对象
    Book book = bookService.queryBookById(id);
    //3.把图书对象转化为购物车商品对象(这里数量就填1即可,在购物车对象的addItem方法中已经做了判断,总金额同理)
    CartItem cartItem=new CartItem(book.getId(),book.getName(),1,book.getPrice(),book.getPrice());
    //4.创建购物车对象,并把购物车商品对象放入购物车对象之中,再把购物车对象放入Session中
    //  先判断Session已经存在购物车对象了没,不存在的话就new一个
    Cart cart = (Cart) req.getSession().getAttribute("cart");
    if (cart == null){ //Session中不存在购物车对象
        cart=new Cart();
        req.getSession().setAttribute("cart",cart); //将购物车对象放入Session中
    }
    //  往购物车里边放商品对象
    cart.addItem(cartItem);
    System.out.println(cart);

    //获取请求头Referer(代表的就是请求时的浏览器地址栏的地址)
    String referer = req.getHeader("Referer");
    //重定向回用户点击购物车的页面
    resp.sendRedirect(referer);
}

23.4 购物车的展示

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>购物车</title>

   <%-- 静态包含 base标签、css样式、jQuery文件 --%>
   <%@ include file="/pages/common/head.jsp"%>

</head>
<body>
   
   <div id="header">
         <img class="logo_img" alt="" src="static/img/logo.gif" >
         <span class="wel_word">购物车</span>

      <%--静态包含,登录 成功之后的菜单 --%>
      <%@ include file="/pages/common/login_success_menu.jsp"%>

   </div>

   <div id="main">
      <table>
         <tr>
            <td>商品名称</td>
            <td>数量</td>
            <td>单价</td>
            <td>金额</td>
            <td>操作</td>
         </tr>
         <c:if test="${empty sessionScope.cart.items}">
            <%--如果购物车空的情况--%>
            <tr>
               <td colspan="5"><a href="index.jsp">亲,当前购物车为空!快跟小伙伴们去浏览商品吧!!!</a>
               </td>
            </tr>
         </c:if>
         <c:if test="${not empty sessionScope.cart.items}">
            <%--如果购物车非空的情况--%>
            <%--entry代表map集合中的每个键值对,这里只需要用到值--%>
            <c:forEach items="${sessionScope.cart.items}" var="entry">
               <tr>
                  <td>${entry.value.name}</td>
                  <td>${entry.value.count}</td>
                  <td>${entry.value.price}</td>
                  <td>${entry.value.totalPrice}</td>
                  <td><a href="#">删除</a></td>
               </tr>
            </c:forEach>
         </c:if>
      </table>
      <%--如果购物车非空才输出页面的内容--%>
      <c:if test="${not empty sessionScope.cart.items}">
         <div class="cart_info">
<span class="cart_span">购物车中共有<span
      class="b_count">${sessionScope.cart.totalCount}</span>件商品</span>
            <span class="cart_span">总金额<span
                  class="b_price">${sessionScope.cart.totalPrice}</span>元</span>
            <span class="cart_span"><a href="#">清空购物车</a></span>
            <span class="cart_span"><a href="pages/cart/checkout.jsp">去结账</a></span>
         </div>
      </c:if>
   </div>

   <%--静态包含页脚内容--%>
   <%@include file="/pages/common/footer.jsp"%>

</body>
</html>

23.5 删除购物车商品项

购物车/pages/cart/cart.jsp 页面的代码:

删除的请求地址:

image-20201220194434758

删除的确认提示操作:

<script type="text/javascript">
    $(function () {
      // 给【删除】绑定单击事件
        $("a.deleteItem").click(function () {
            return confirm("你确定要删除【" + $(this).parent().parent().find("td:first").text() +"】吗?")
        });
    });
</script>

CartServlet 程序:

/**
 * 删除购物车商品项
 * @param req
 * @param resp
 * @throws IOException
 */
protected void deleteItem(HttpServletRequest req, HttpServletResponse resp) throws IOException {
    //获取请求参数中的图书id
    String idStr = req.getParameter("id");
    Integer id = WebUtils.parseInt(idStr, 0);
    //从Session中得到cart对象
    Cart cart = (Cart) req.getSession().getAttribute("cart");
    //调用Cart对象的删除方法
    cart.deleteItem(id);

    //获取请求头Referer(代表的就是请求时的浏览器地址栏的地址)
    String referer = req.getHeader("Referer");
    //重定向回用户点击购物车的页面
    resp.sendRedirect(referer);
}

23.6 清空购物车

cart.jsp 页面的内容:

给清空购物车添加请求地址,和添加id 属性:

image-20201220194913093

清空的确认提示操作:

// 给清空购物车绑定单击事件
$("#clearCart").click(function () {
return confirm("你确定要清空购物车吗?");
})

CartServlet 程序:

/**
 * 清空购物车
 * @param req
 * @param resp
 * @throws ServletException
 * @throws IOException
 */
protected void clear(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException{
    // 获取购物车对象
    Cart cart = (Cart) req.getSession().getAttribute("cart");
    if (cart != null) {
        // 清空购物车
        cart.clear();
        // 重定向回原来购物车展示页面
        resp.sendRedirect(req.getHeader("Referer"));
    }
}

23.7 修改购物车商品数量

修改pages/cart/cart.jsp 购物车页面:

image-20201220203412100

修改商品数量js 代码:

     // 给输入框绑定onchange 内容发生改变事件
     $(".updateCount").change(function () {
// 获取商品名称
         var name = $(this).parent().parent().find("td:first").text();
         var id = $(this).attr('bookId');
// 获取商品数量
         var count = this.value;
// 判断是否要更改
         if ( confirm("你确定要将【" + name + "】商品修改数量为:" + count + " 吗?") ) {
   //发起请求。给服务器保存修改
             location.href =
                 "http://localhost:8080/book/cartServlet?action=updateCount&count="+count+"&id="+id;
         } else {
   // defaultValue 属性是表单项Dom 对象的属性。它表示默认的value 属性值。
             this.value = this.defaultValue;
         }
     });

CartServlet 程序:

/**
 * 修改商品数量
 * @param req
 * @param resp
 * @throws ServletException
 * @throws IOException
 */
protected void updateCount(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException{
    // 获取请求的参数商品编号、商品数量
    int id = WebUtils.parseInt(req.getParameter("id"),0);
    int count = WebUtils.parseInt(req.getParameter("count"), 1);
    // 获取Cart 购物车对象
    Cart cart = (Cart) req.getSession().getAttribute("cart");
    if (cart != null) {
        // 修改商品数量
        cart.updateCount(id,count);
        // 重定向回原来购物车展示页面
        resp.sendRedirect(req.getHeader("Referer"));
    }
}

23.8 首页,购物车数据回显

在添加商品到购物车的时候,保存最后一个添加的商品名称:

image-20201220204412010

在pages/client/index.jsp 页面中输出购物车信息:

<div style="text-align: center">
   <c:if test="${empty sessionScope.cart.items}">
      <%--购物车为空的输出--%>
      <span> </span>
      <div>
         <span style="color: red">当前购物车为空</span>
      </div>
   </c:if>
   <c:if test="${not empty sessionScope.cart.items}">
      <%--购物车非空的输出--%>
      <span>您的购物车中有${sessionScope.cart.totalCount} 件商品</span>
      <div>
         您刚刚将<span style="color: red">${sessionScope.last}</span>加入到了购物车中
      </div>
   </c:if>
</div>

24. 订单模块

24.1 订单模块的分析

image-20201221102909279

24.2 订单模块的实现

24.2.1 创建订单模块的数据库表

use book;
create table t_order(
    `order_id` varchar(50) primary key,
    `create_time` datetime,
    `price` decimal(11,2),
    `status` int,
    `user_id` int,
    foreign key(`user_id`) references t_user(`id`)
);
create table t_order_item(
    `id` int primary key auto_increment,
    `name` varchar(100),
    `count` int,
    `price` decimal(11,2),
    `total_price` decimal(11,2),
    `order_id` varchar(50),
    foreign key(`order_id`) references t_order(`order_id`)
);

24.2.2 创建订单模块的数据模型

/**
 * 订单
 */
public class Order {
    private String orderId;
    private Date createTime;
    private BigDecimal price;
    // 0未发货,1已发货,2表示已签收
    private Integer status = 0;
    private Integer userId;
}
/**
 * 订单项
 */
public class OrderItem {
    private Integer id;
    private String name;
    private Integer count;
    private BigDecimal price;
    private BigDecimal totalPrice;
    private String orderId;
}

24.2.3 编写订单模块的Dao

OrderDao 接口:

public interface OrderDao {

    public int saveOrder(Connection con, Order order);

}

OrderDao 实现:

public class OrderDaoImpl extends BaseDao<Order> implements OrderDao {
    @Override
    public int saveOrder(Connection con,Order order) {
        String sql = "insert into t_order(`order_id`,`create_time`,`price`,`status`,`user_id`) values(?,?,?,?,?)";

        return update(con,sql,order.getOrderId(),order.getCreateTime(),order.getPrice(),order.getStatus(),order.getUserId());
    }
}

OrderItemDao 接口:

public interface OrderItemDao {
    public int saveOrderItem(Connection con,OrderItem orderItem);
}

OrderItemDao 实现:

public class OrderItemDaoImpl extends BaseDao<OrderItem> implements OrderItemDao {
    @Override
    public int saveOrderItem(Connection con, OrderItem orderItem) {
        String sql = "insert into t_order_item(`name`,`count`,`price`,`total_price`,`order_id`) values(?,?,?,?,?)";
        return update(con,sql,orderItem.getName(),orderItem.getCount(),
               orderItem.getPrice(),orderItem.getTotalPrice(),orderItem.getOrderId());
    }
}

24.2.4 编写订单模块的Service

OrderService 接口:

public interface OrderService {
    public String createOrder(Cart cart, Integer userId);
}

OrderService 实现类:

public class OrderServiceImpl implements OrderService {

    private OrderDao orderDao = new OrderDaoImpl();
    private OrderItemDao orderItemDao = new OrderItemDaoImpl();
    private BookDao bookDao = new BookDaoImpl();

    @Override
    public String createOrder(Cart cart, Integer userId) {
        Connection con = JDBCUtils.getConnection();
        // 订单号===唯一性
        String orderId = System.currentTimeMillis()+""+userId;
        // 创建一个订单对象
        Order order = new Order(orderId,new Date(),cart.getTotalPrice(), 0,userId);
        // 保存订单
        orderDao.saveOrder(con,order);

        // 遍历购物车中每一个商品项转换成为订单项保存到数据库
        for (Map.Entry<Integer, CartItem>entry : cart.getItems().entrySet()){
            // 获取每一个购物车中的商品项
            CartItem cartItem = entry.getValue();
            // 转换为每一个订单项
            OrderItem orderItem = new OrderItem(null,cartItem.getName(),cartItem.getCount(),cartItem.getPrice(),cartItem.getTotalPrice(), orderId);
            // 保存订单项到数据库
            orderItemDao.saveOrderItem(con,orderItem);

            // 更新库存和销量
            Book book = bookDao.queryBookById(con,cartItem.getId());
            book.setSales( book.getSales() + cartItem.getCount() );
            book.setStock( book.getStock() - cartItem.getCount() );
            bookDao.updateBook(con,book);

        }
        // 清空购物车
        cart.clear();

        JDBCUtils.close(con);
        return orderId;
    }
}

24.2.5 编写订单模块的web 层和页面联调

OrderServlet 程序:

/**
 * 生成订单
 *
 * @param req
 * @param resp
 * @throws ServletException
 * @throws IOException
 */
protected void createOrder(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    // 先获取Cart购物车对象
    Cart cart = (Cart) req.getSession().getAttribute("cart");
    // 获取Userid
    User loginUser = (User) req.getSession().getAttribute("user");

    //如果用户还没登录 直接跳转回登录页面
    if (loginUser == null) {
        req.getRequestDispatcher("/pages/user/login.jsp").forward(req,resp);
        return;
    }

    Integer userId = loginUser.getId();
    //调用orderService.createOrder(Cart,Userid);生成订单
    String orderId = orderService.createOrder(cart, userId);

    req.getSession().setAttribute("orderId",orderId);

    resp.sendRedirect(req.getContextPath()+"/pages/cart/checkout.jsp");
}

修改pages/cart/cart.jsp 页面,结账的请求地址:

image-20201221111101129

修改pages/cart/checkout.jsp 页面,输出订单号:

image-20201221111141369

25. 权限检查

25.1 使用Filter 过滤器

使用Filter 过滤器拦截/pages/manager/所有内容,实现权限检查。

Filter 代码:

public class ManagerFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request= (HttpServletRequest) servletRequest;
        Object user = request.getSession().getAttribute("user");
        if (user==null){
            servletRequest.getRequestDispatcher("/pages/user/login.jsp").forward(servletRequest,servletResponse);
        }else {
            filterChain.doFilter(servletRequest,servletResponse);
        }
    }

    @Override
    public void destroy() {
    }
}

web.xml 中的配置:

<filter>
    <filter-name>ManagerFilter</filter-name>
    <filter-class>com.Luo.filter.ManagerFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>ManagerFilter</filter-name>
    <url-pattern>/pages/manager/*</url-pattern>
    <url-pattern>/manager/bookServlet</url-pattern>
</filter-mapping>

26. 将所有异常都统一交给Tomcat,让Tomcat 展示友好的错误信息页面

建立在 JavaWeb第八天 21. 的基础上。

在web.xml 中我们可以通过错误页面配置来进行管理:

还要记得要在Filter中将捕获到的异常再次抛出去,不然Tomcat不知道你报错了。

image-20201221202640890

<!--error-page 标签配置,服务器出错之后,自动跳转的页面-->
    <error-page>
    <!--error-code 是错误类型-->
    <error-code>500</error-code>
    <!--location 标签表示。要跳转去的页面路径-->
    <location>/pages/error/error500.jsp</location>
</error-page>
    <!--error-page 标签配置,服务器出错之后,自动跳转的页面-->
    <error-page>
    <!--error-code 是错误类型-->
    <error-code>404</error-code>
    <!--location 标签表示。要跳转去的页面路径-->
    <location>/pages/error/error404.jsp</location>
</error-page>

27. 使用AJAX 验证用户名是否可用

图解:

image-20201222192435908

UserServlet 程序中ajaxExistsUsername 方法:

public void ajaxExistsUsername(HttpServletRequest request, HttpServletResponse response) throws IOException {
    //1.获取请求参数
    String username = request.getParameter("username");
    User user=new User();
    user.setUsername(username);
    //2.调用Service判断用户名是否存在
    boolean flag = service.existsUsername(user);
    //3.把flag放入map集合中
    Map<String,Boolean> map=new HashMap<>();
    map.put("existsUsername",flag);
    //4.把map集合转化为json字符串
    Gson gson=new Gson();
    String mapJson = gson.toJson(map);
    //5.写回json字符串
    response.getWriter().write(mapJson);
}

regist.jsp 页面中的代码:

//username输入框离开焦点事件
$("#username").blur(function () {
    // 1.获取用户名
    var username=this.value;
    // 2.发送ajax请求
    $.get("http://localhost:8080/book/userServlet","action=ajaxExistsUsername&username="+username,function (data) {
    if (data.existsUsername){
       $("span.errorMsg").text("用户名可用!");
    } else {
       $("span.errorMsg").text("用户名已存在!");
    }
    },"json");
});

28. 使用AJAX 修改把商品添加到购物车

图解:

image-20201222194108190

CartServlet 程序:

protected void ajaxAddItem(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    // 获取请求的参数商品编号
    int id = WebUtils.parseInt(req.getParameter("id"), 0);
    // 调用bookService.queryBookById(id):Book 得到图书的信息
    Book book = bookService.queryBookById(id);
    // 把图书信息,转换成为CartItem 商品项
    CartItem cartItem = new CartItem(book.getId(),book.getName(),1,book.getPrice(),book.getPrice());
    // 调用Cart.addItem(CartItem);添加商品项
    Cart cart = (Cart) req.getSession().getAttribute("cart");
    if (cart == null) {
        cart = new Cart();
        req.getSession().setAttribute("cart",cart);
    }
    cart.addItem(cartItem);
    System.out.println(cart);
    // 最后一个添加的商品名称
    req.getSession().setAttribute("lastName", cartItem.getName());
    //6、返回购物车总的商品数量和最后一个添加的商品名称
    Map<String,Object> resultMap = new HashMap<String,Object>();
    resultMap.put("totalCount", cart.getTotalCount());
    resultMap.put("lastName",cartItem.getName());
    Gson gson = new Gson();
    String resultMapJsonString = gson.toJson(resultMap);
    resp.getWriter().write(resultMapJsonString);
}

pages/client/index.jsp 页面:

html 代码:

image-20201222194458862

javaScript 代码:

     // 给加入购物车按钮绑定单击事件
     $("button.addToCart").click(function () {
         /**
          * 在事件响应的function 函数中,有一个this 对象,这个this 对象,是当前正在响应事件的dom 对象
          * @type {jQuery}
          */
         var bookId = $(this).attr("bookId");
        // location.href = "http://localhost:8080/book/cartServlet?action=addItem&id=" + bookId;
        // 发ajax 请求,添加商品到购物车
         $.getJSON("http://localhost:8080/book/cartServlet","action=ajaxAddItem&id=" +
             bookId,function (data) {
             $("#cartTotalCount").text("您的购物车中有" + data.totalCount + " 件商品");
             $("#cartLastName").text(data.lastName);
         })
     });
最后修改:2020 年 12 月 22 日 07 : 51 PM
如果觉得我的文章对你有用,请随意赞赏