1. 程式人生 > 實用技巧 >【JavaWeb】書城專案

【JavaWeb】書城專案

書城網站 專案說明

專案地址

階段一 登入、註冊的驗證

  • 使用 jQuery 技術對登入中的使用者名稱、密碼進行非空驗證;

  • 使用 jQuery 技術和正則表示式對註冊中的使用者名稱、密碼、確認密碼、郵箱進行格式驗證,對驗證碼進行非空驗證;

<script type="text/javascript" src="static/script/jquery.js"></script>
<script type="text/javascript">
    $(function() {
        $("#sub_btn").click(function () {
            // 驗證使用者名稱:必須由字母,數字下劃線組成,並且長度為 5 到 12 位
            // 1. 獲取使用者名稱輸入框的內容
            let username = $("#username").val();
            // 2. 建立正則表示式物件
            let usernamePattern = /^\w{5,12}$/;
            // 3. 使用 test 方法驗證
            if (!usernamePattern.test(username)) {
                // 4. 提示使用者
                $("span.errorMsg").text("使用者名稱格式錯誤!");

                return false;
            }

            // 驗證密碼:必須由字母,數字下劃線組成,並且長度為 5 到 12 位
            let password = $("#password").val();
            let passwordPattern = /^\w{5,12}$/;
            if (!passwordPattern.test(password)) {
                $("span.errorMsg").text("密碼格式錯誤!");

                return false;
            }

            // 驗證確認密碼:和密碼相同
            let repwd = $("#repwd").val();
            if (repwd !== password) {
                $("span.errorMsg").text("密碼不一致!");

                return false;
            }

            // 郵箱驗證:[email protected]
            let email = $("#email").val();
            let emailPattern = /^[a-z\d]+(\.[a-z\d]+)*@([\da-z](-[\da-z])?)+(\.{1,2}[a-z]+)+$/;
            if (!emailPattern.test(email)) {
                $("span.errorMsg").text("郵箱格式錯誤!");

                return false;
            }
            // 驗證碼:現在只需要驗證使用者已輸入。因為還沒講到伺服器。驗證碼生成。
            let code = $("#code").val();
            let trimCode = $.trim(code);
            if (trimCode == null || trimCode === "") {
                $("span.errorMsg").text("驗證碼不能為空!");

                return false;
            }

            // 驗證成功,去掉提示資訊
            $("span.errorMsg").text("");
        })
    })
</script>

階段二 實現登入、註冊

軟體的三層架構

  • UIL(User Interface Layer 表現層):主要是指使用者互動的介面,用於接收使用者輸入的資料和顯示處理後用戶需要的資料;
  • BLL(Business Logic Layer 業務邏輯層):表現層和資料訪問層之間的橋樑,實現業務邏輯,業務邏輯包括驗證、計算、業務規劃等;
  • DAL(Date Access Layer 資料訪問層):主要實現對資料的增、刪、改、查。將儲存在資料庫中的資料提交給業務層,同時將業務層處理的資料儲存到資料庫。

三層架構的優點

  • 結構清晰,耦合度低;
  • 可維護性高,可擴充套件性高;
  • 利於開發任務同步進行;
  • 容易適應需求變化。

三層架構的確定

  • 降低了系統的效能,如果不採用分層式結構,很多業務可以直接造訪資料庫,以此獲取相應的資料,如今卻必須通過中間層來完成;
  • 增加了程式碼量,增加了工作量;

書城的三層架構

  • 表現層
    • HTML、Servlet
    • 接受使用者的請求,呼叫業務邏輯層處理使用者請求,顯示處理結果
  • 業務邏輯層
    • Service
    • 呼叫資料訪問層處理業務邏輯
    • 採用面向介面程式設計的思想,先定義介面,再建立實現類
  • 資料訪問層
    • DAO
    • 用來操作資料庫,對資料庫進行增刪改查
    • 採用面向介面程式設計的思想,先定義介面,再建立實現類

使用者註冊

  • 訪問註冊頁面
  • 填寫註冊資訊,提交給伺服器
  • 伺服器應該儲存使用者
  • 當用戶已經存在,提示使用者註冊失敗,使用者名稱已存在
  • 當用戶不存在,註冊成功

使用者登入

  • 訪問登陸頁面
  • 填寫使用者名稱密碼後提交
  • 伺服器判斷使用者是否存在
  • 如果登陸失敗,返回使用者名稱或者密碼錯誤資訊
  • 如果登入成功,返回登陸成功資訊

階段三 動態化及區域性優化

為了動態提示資訊,所以需要動態化。

頁面 jsp 動態化

  • 在 html 頁面頂行新增 page 指令
  • 修改 .html 檔案字尾名為:.jsp
  • 使用 IDEA 搜尋替換各個檔案內容的 .html.jsp (快捷鍵:Ctrl+Shift+R

抽取頁面中相同內容

  1. head 中 css、jquery、base 標籤
<%--
  Created by IntelliJ IDEA.
  User: parzulpan
  Date: 2020/12/10
  Time: 12:03 上午
  To change this template use File | Settings | File Templates.

  抽取頁面中相同的內容 head 中 css、jquery、base 標籤
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
    String basePath = request.getScheme()
            + "://"
            + request.getServerName()
            + ":"
            + request.getServerPort()
            + request.getContextPath()
            + "/";
%>

<!--寫 base 標籤,永遠固定相對路徑跳轉的結果-->
<base href="<%=basePath%>">
<link type="text/css" rel="stylesheet" href="static/css/style.css" >
<script type="text/javascript" src="static/script/jquery-1.7.2.js"></script>

  1. 每個頁面的頁尾
<%--
  Created by IntelliJ IDEA.
  User: parzulpan
  Date: 2020/12/10
  Time: 12:12 上午
  To change this template use File | Settings | File Templates.
  抽取頁面中相同的內容 每個頁面的頁尾
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<div id="bottom">
		<span>
			購書城.Copyright &copy;2020
		</span>
</div>

  1. 登入成功後的選單
<%--
  Created by IntelliJ IDEA.
  User: parzulpan
  Date: 2020/12/10
  Time: 12:12 上午
  To change this template use File | Settings | File Templates.
  抽取頁面中相同的內容 登入成功後的選單
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<div>
    <span>歡迎<span class="um_span"> JackMa</span>光臨購書城</span>
    <a href="pages/order/order.jsp">我的訂單</a>
    <a href="index.jsp">登出</a>&nbsp;&nbsp;
    <a href="index.jsp">返回</a>
</div>

  1. manager 模組的選單
<%--
  Created by IntelliJ IDEA.
  User: parzulpan
  Date: 2020/12/10
  Time: 12:12 上午
  To change this template use File | Settings | File Templates.
  抽取頁面中相同的內容 manager 模組的選單
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<div>
    <a href="pages/manager/book_manager.jsp">圖書管理</a>
    <a href="pages/manager/order_manager.jsp">訂單管理</a>
    <a href="index.jsp">返回商城</a>
</div>

登入註冊錯誤提示及表單回顯

流程:

  • 客戶端:
    • 傳送請求,登入或者註冊
  • 伺服器:
    • 只要失敗,就會跳回原來的頁面,把錯誤資訊和回顯的表單項資訊儲存(Request 域),回傳給客戶端

BaseServlet 抽取

在實際的專案開發中,一個模組,一般只使用一個 Servlet 程式。

  1. 程式碼優化一:合併 LoginServlet 和 RegistServlet 程式為 UserServlet 程式
  2. 優化程式碼二:使用反射優化大量 else if 程式碼
  3. 程式碼優化三:抽取 BaseServlet 程式
  • 獲取 action 引數值
  • 通過反射獲取 action 對應的業務方法
  • 通過反射呼叫業務方法
  • 其他 Servlet 繼承 BaseServlet

資料的封裝和抽取 BeanUtils 的使用

BeanUtils 工具類,它可以一次性的把所有請求的引數注入到 JavaBean 中。

BeanUtils 工具類,經常用於把 Map 中的值注入到 JavaBean 中,或者是物件屬性值的拷貝操作。

BeanUtils 它不是 Jdk 的類。而是第三方的工具類。所以需要導包。

  1. 匯入需要的 jar 包:
  • commons-beanutils-1.8.0.jar
  • commons-logging-1.1.1.jar
  1. 編寫 WebUtils 工具類使用
package cn.parzulpan.utils;

import org.apache.commons.beanutils.BeanUtils;

import java.util.Map;

/**
 * @Author : parzulpan
 * @Time : 2020-12-10
 * @Desc : WebUtils 工具類
 */

public class WebUtils {

    /**
     * 一次性的把所有請求的引數注入到 JavaBean 中
     * @param value
     * @param bean
     * @param <T>
     * @return
     */
    public static <T> T copyParamToBean(Map value, T bean) {

        try {
//            System.out.println("注入之前:" + bean);

            BeanUtils.populate(bean, value);

//            System.out.println("注入之後:" + bean);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return bean;
    }
}

階段四 使用 EL 表示式修改表單回顯

使用 EL 表示式可以簡化表單的回顯:

                            <div class="msg_cont">
								<b></b>
								<span class="errorMsg">
<%--									輸出回顯資訊--%>
<%--									<%=request.getAttribute("msg")==null?"請輸入使用者名稱和密碼":request.getAttribute("msg")%>--%>
<%--									使用 EL 表示式 簡化回顯資訊--%>
									${ empty requestScope.msg ? "請輸入使用者名稱和密碼" : requestScope.msg}
								</span>
							</div>
							<div class="form">
<!--								修改登錄檔單的提交地址和請求方式-->
<!--								<form action="login_success.jsp">-->
<%--								<form action="loginServlet" method="post">--%>
<%--								新增隱藏域和修改請求地址--%>
								<form action="userServlet" method="post">
									<input type="hidden" name="action" value="login"/>
									<label>使用者名稱稱:</label>
									<input class="itxt" type="text" placeholder="請輸入使用者名稱"
										   autocomplete="off" tabindex="1" name="username"
<%--										   輸出回顯資訊--%>
<%--										   value="<%=request.getAttribute("username")==null?"":request.getAttribute("username")%>"--%>
<%--										   使用 EL 表示式 簡化回顯資訊--%>
										   value="${requestScope.username}"
									/>
									<br />
									<br />
									<label>使用者密碼:</label>
									<input class="itxt" type="password" placeholder="請輸入密碼"
										   autocomplete="off" tabindex="1" name="password" />
									<br />
									<br />
									<input type="submit" value="登入" id="sub_btn" />
								</form>
							</div>

階段五 圖書的增刪改查

MVC 概念

MVC 即 Model 模型、View 檢視、Controller 控制器。MVC 最早出現在 JavaEE 三層中的 Web 層,它可以有效的指導 Web 層的程式碼如何有效分離,單獨工作。

  • Model 模型:將與業務邏輯相關的資料封裝為具體的 JavaBean 類,其中不摻雜任何與資料處理相關的程式碼。(JavaBean、Domain、Entity)
  • View 檢視:只負責資料和介面的顯示,不接受任何與顯示資料無關的程式碼,便於程式設計師和美工的分工合作。(JSP、HTML)
  • Controller 控制器:只負責接收請求,呼叫業務層的程式碼處理請求,然後派發頁面(轉到某個頁面或者是重定向到某個頁面),是一個“排程者”的角色。(Servlet)

MVC 的作用是為降低耦合,讓程式碼合理分層,方便後期升級和維護。

圖書模組

實現圖書的增刪改查。

編寫資料庫表

drop table t_book;

create table t_book(
    `id` int primary key auto_increment,
    `name` varchar(200),
    `author` varchar(100),
    `price` decimal(11, 2),
    `sales` int,
    `stock` int,
    `imgPath` varchar(200)
);

insert into t_book(`id`, `name`, `author`, `price`, `sales`, `stock`, `imgPath`) value
    (null , 'Java 從入門到放棄' , '大哥' , 80 , 9999 , 9 , 'static/img/default.jpg'),
    (null , '資料結構與演算法' , '嚴敏君' , 78.5 , 6 , 13 , 'static/img/default.jpg'),
    (null , '怎樣拐跑別人的媳婦' , '龍伍' , 68, 99999 , 52 , 'static/img/default.jpg'),
    (null , 'C++程式設計思想' , '二哥' , 45.5 , 14 , 95 , 'static/img/default.jpg'),
    (null , '蛋炒飯' , '周星星' , 9.9, 12 , 53 , 'static/img/default.jpg'),
    (null , '賭神' , '龍伍' , 66.5, 125 , 535 , 'static/img/default.jpg'),
    (null , 'Java程式設計思想' , '陽哥' , 99.5 , 47 , 36 , 'static/img/default.jpg'),
    (null , 'JavaScript從入門到精通' , '婷姐' , 9.9 , 85 , 95 , 'static/img/default.jpg'),
    (null , 'Cocos2d-x遊戲程式設計入門' , '大哥' , 49, 52 , 62 , 'static/img/default.jpg'),
    (null , 'C語言程式設計' , '譚浩強' , 28 , 52 , 74 , 'static/img/default.jpg'),
    (null , 'Lua語言程式設計' , '雷豐陽' , 51.5 , 48 , 82 , 'static/img/default.jpg'),
    (null , '西遊記' , '羅貫中' , 12, 19 , 9999 , 'static/img/default.jpg'),
    (null , '水滸傳' , '華仔' , 33.05 , 22 , 88 , 'static/img/default.jpg'),
    (null , '作業系統原理' , '劉優' , 133.05 , 122 , 188 , 'static/img/default.jpg'),
    (null , '資料結構 java版' , '封大神' , 173.15 , 21 , 81 , 'static/img/default.jpg'),
    (null , 'UNIX高階環境程式設計' , '樂天' , 99.15 , 210 , 810 , 'static/img/default.jpg'),
    (null , 'JavaScript高階程式設計' , '大哥' , 69.15 , 210 , 810 , 'static/img/default.jpg'),
    (null , '大話設計模式' , '大哥' , 89.15 , 20 , 10 , 'static/img/default.jpg'),
    (null , '人月神話' , '二哥' , 88.15 , 20 , 80 , 'static/img/default.jpg');

編寫 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";
}

編寫 Dao 和測試

  1. BookDAO 介面
package cn.parzulpan.dao;

import cn.parzulpan.bean.Book;

import java.sql.Connection;
import java.util.List;

/**
 * @Author : parzulpan
 * @Time : 2020-12-10
 * @Desc : 用於規範 Book 表的常用操作
 */

public interface BookDAO {

    /**
     * 增加一本書
     * @param connection 資料庫連線
     * @param book Book Bean
     * @return 返回 -1 表示操作失敗;否則返回 sql 語句影響的行數
     */
    public int addBook(Connection connection, Book book);

    /**
     * 根據 書的 id 刪除一本書
     * @param connection 資料庫連線
     * @param id 書的 id
     * @return 返回 -1 表示操作失敗;否則返回 sql 語句影響的行數
     */
    public int deleteBookById(Connection connection, Integer id);

    /**
     * 更新一本書
     * @param connection 資料庫連線
     * @param book Book Bean
     * @return 返回 -1 表示操作失敗;否則返回 sql 語句影響的行數
     */
    public int updateBook(Connection connection, Book book);

    /**
     * 根據 書的 id 查詢一本書
     * @param connection 資料庫連線
     * @param id 書的 id
     * @return Book Bean
     */
    public Book queryBookById(Connection connection, Integer id);

    /**
     * 查詢所有書
     * @param connection 資料庫連線
     * @return Book Bean List
     */
    public List<Book> queryBooks(Connection connection);
}
  1. BookDAOImpl 實現類
package cn.parzulpan.dao;

import cn.parzulpan.bean.Book;
import cn.parzulpan.utils.JDBCUtils;

import java.sql.Connection;
import java.util.List;

/**
 * @Author : parzulpan
 * @Time : 2020-12-10
 * @Desc :
 */

public class BookDAOImpl extends BaseDAO<Book> implements BookDAO {
    /**
     * 增加一本書
     *
     * @param connection 資料庫連線
     * @param book       Book Bean
     * @return 返回 -1 表示操作失敗;否則返回 sql 語句影響的行數
     */
    @Override
    public int addBook(Connection connection, Book book) {
        String sql = "insert into t_book(`name`, `author`, `price`, `sales`, `stock`, `imgPath`) values (?, ?, ?, ?, ?, ?)";
        return update(connection, sql,
                book.getName(), book.getAuthor(), book.getPrice(), book.getSales(), book.getStock(), book.getImgPath());
    }

    /**
     * 根據 書的 id 刪除一本書
     *
     * @param connection 資料庫連線
     * @param id         書的 id
     * @return 返回 -1 表示操作失敗;否則返回 sql 語句影響的行數
     */
    @Override
    public int deleteBookById(Connection connection, Integer id) {
        String sql = "delete from t_book where id = ?";
        return update(connection, sql, id);
    }

    /**
     * 更新一本書
     *
     * @param connection 資料庫連線
     * @param book       Book Bean
     * @return 返回 -1 表示操作失敗;否則返回 sql 語句影響的行數
     */
    @Override
    public int updateBook(Connection connection, Book book) {
        String sql = "update t_book set `name` = ?, `author` = ?, `price` = ?, `sales` = ?, `stock` = ?, `imgPath` = ? where id = ?";
        return update(connection, sql, book.getName(), book.getAuthor(), book.getPrice(), book.getSales(),
                book.getStock(), book.getImgPath(), book.getId());
    }

    /**
     * 根據 書的 id 查詢一本書
     *
     * @param connection 資料庫連線
     * @param id         書的 id
     * @return Book Bean
     */
    @Override
    public Book queryBookById(Connection connection, Integer id) {
        String sql = "select `id`, `name`, `author`, `price`, `sales`, `stock`, `imgPath` from t_book where id = ?";
        return getBean(connection, sql, id);
    }

    /**
     * 查詢所有書
     *
     * @param connection 資料庫連線
     * @return Book Bean List
     */
    @Override
    public List<Book> queryBooks(Connection connection) {
        String sql = "select `id`, `name`, `author`, `price`, `sales`, `stock`, `imgPath` from t_book";
        return getBeanList(connection, sql);
    }
}
  1. BookDAOImplTest 單元測試
package cn.parzulpan.test;

import cn.parzulpan.bean.Book;
import cn.parzulpan.dao.BookDAO;
import cn.parzulpan.dao.BookDAOImpl;
import cn.parzulpan.utils.JDBCUtils;
import org.junit.Test;

import java.math.BigDecimal;
import java.sql.Connection;
import java.util.List;


/**
 * @Author : parzulpan
 * @Time : 2020-12-10
 * @Desc :
 */

public class BookDAOImplTest {
    private BookDAO bookDAO = new BookDAOImpl();

    @Test
    public void addBook() {
        Connection connection = JDBCUtils.getConnection();
        int addBook = bookDAO.addBook(connection,
                new Book(null, "測試的書", "測試的作者",
                        new BigDecimal(120), 100, 10, null));
        System.out.println(addBook);
        JDBCUtils.close(connection, null, null);
    }

    @Test
    public void deleteBookById() {
        Connection connection = JDBCUtils.getConnection();
        int deleteBookById = bookDAO.deleteBookById(connection, 20);
        System.out.println(deleteBookById);
        JDBCUtils.close(connection, null, null);
    }

    @Test
    public void updateBook() {
        Connection connection = JDBCUtils.getConnection();
        int updateBook = bookDAO.updateBook(connection,
                new Book(2, "更新的書", "更新的作者",
                        new BigDecimal(120), 100, 10, null));
        System.out.println(updateBook);
        JDBCUtils.close(connection, null, null);
    }

    @Test
    public void queryBookById() {
        Connection connection = JDBCUtils.getConnection();
        Book queryBookById = bookDAO.queryBookById(connection, 2);
        System.out.println(queryBookById);
        JDBCUtils.close(connection, null, null);
    }

    @Test
    public void queryBooks() {
        Connection connection = JDBCUtils.getConnection();
        List<Book> books = bookDAO.queryBooks(connection);
        books.forEach(System.out::println);
        JDBCUtils.close(connection, null, null);
    }
}

編寫 Service 和測試

  1. BookService 介面
package cn.parzulpan.service;

import cn.parzulpan.bean.Book;
import cn.parzulpan.dao.BookDAOImpl;

import java.sql.Connection;
import java.util.List;

/**
 * @Author : parzulpan
 * @Time : 2020-12-10
 * @Desc :
 */

public interface BookService {

    /**
     * 增加一本書
     * @param book Book Bean
     * @return 返回 -1 表示操作失敗;否則返回 sql 語句影響的行數
     */
    public int addBook(Book book);

    /**
     * 根據 書的 id 刪除一本書
     * @param id 書的 id
     * @return 返回 -1 表示操作失敗;否則返回 sql 語句影響的行數
     */
    public int deleteBookById(Integer id);

    /**
     * 更新一本書
     * @param book Book Bean
     * @return 返回 -1 表示操作失敗;否則返回 sql 語句影響的行數
     */
    public int updateBook(Book book);

    /**
     * 根據 書的 id 查詢一本書
     * @param id 書的 id
     * @return Book Bean
     */
    public Book queryBookById(Integer id);

    /**
     * 查詢所有書
     * @return Book Bean List
     */
    public List<Book> queryBooks();
}
  1. BookServiceImpl 實現類
package cn.parzulpan.service;

import cn.parzulpan.bean.Book;
import cn.parzulpan.dao.BookDAO;
import cn.parzulpan.dao.BookDAOImpl;
import cn.parzulpan.utils.JDBCUtils;

import java.math.BigDecimal;
import java.sql.Connection;
import java.util.List;

/**
 * @Author : parzulpan
 * @Time : 2020-12-10
 * @Desc :
 */

public class BookServiceImpl implements BookService {
    private BookDAO bookDAO = new BookDAOImpl();

    /**
     * 增加一本書
     *
     * @param book Book Bean
     * @return 返回 -1 表示操作失敗;否則返回 sql 語句影響的行數
     */
    @Override
    public int addBook(Book book) {
        Connection connection = JDBCUtils.getConnection();
        int i = bookDAO.addBook(connection, book);
        JDBCUtils.close(connection, null, null);
        return i;
    }

    /**
     * 根據 書的 id 刪除一本書
     *
     * @param id 書的 id
     * @return 返回 -1 表示操作失敗;否則返回 sql 語句影響的行數
     */
    @Override
    public int deleteBookById(Integer id) {
        Connection connection = JDBCUtils.getConnection();
        int deleteBookById = bookDAO.deleteBookById(connection, id);
        JDBCUtils.close(connection, null, null);
        return deleteBookById;
    }

    /**
     * 更新一本書
     *
     * @param book Book Bean
     * @return 返回 -1 表示操作失敗;否則返回 sql 語句影響的行數
     */
    @Override
    public int updateBook(Book book) {
        Connection connection = JDBCUtils.getConnection();
        int updateBook = bookDAO.updateBook(connection, book);
        JDBCUtils.close(connection, null, null);
        return updateBook;
    }

    /**
     * 根據 書的 id 查詢一本書
     *
     * @param id 書的 id
     * @return Book Bean
     */
    @Override
    public Book queryBookById(Integer id) {
        Connection connection = JDBCUtils.getConnection();
        Book queryBookById = bookDAO.queryBookById(connection, id);
        JDBCUtils.close(connection, null, null);
        return queryBookById;
    }

    /**
     * 查詢所有書
     *
     * @return Book Bean List
     */
    @Override
    public List<Book> queryBooks() {
        Connection connection = JDBCUtils.getConnection();
        List<Book> books = bookDAO.queryBooks(connection);
        JDBCUtils.close(connection, null, null);
        return books;
    }
}
  1. BookServiceImplTest 單元測試
package cn.parzulpan.test;

import cn.parzulpan.bean.Book;
import cn.parzulpan.dao.BookDAO;
import cn.parzulpan.dao.BookDAOImpl;
import cn.parzulpan.utils.JDBCUtils;
import org.junit.Test;

import java.math.BigDecimal;
import java.sql.Connection;
import java.util.List;


/**
 * @Author : parzulpan
 * @Time : 2020-12-10
 * @Desc :
 */

public class BookDAOImplTest {
    private BookDAO bookDAO = new BookDAOImpl();

    @Test
    public void addBook() {
        Connection connection = JDBCUtils.getConnection();
        int addBook = bookDAO.addBook(connection,
                new Book(null, "測試的書", "測試的作者",
                        new BigDecimal(120), 100, 10, null));
        System.out.println(addBook);
        JDBCUtils.close(connection, null, null);
    }

    @Test
    public void deleteBookById() {
        Connection connection = JDBCUtils.getConnection();
        int deleteBookById = bookDAO.deleteBookById(connection, 20);
        System.out.println(deleteBookById);
        JDBCUtils.close(connection, null, null);
    }

    @Test
    public void updateBook() {
        Connection connection = JDBCUtils.getConnection();
        int updateBook = bookDAO.updateBook(connection,
                new Book(2, "更新的書", "更新的作者",
                        new BigDecimal(120), 100, 10, null));
        System.out.println(updateBook);
        JDBCUtils.close(connection, null, null);
    }

    @Test
    public void queryBookById() {
        Connection connection = JDBCUtils.getConnection();
        Book queryBookById = bookDAO.queryBookById(connection, 2);
        System.out.println(queryBookById);
        JDBCUtils.close(connection, null, null);
    }

    @Test
    public void queryBooks() {
        Connection connection = JDBCUtils.getConnection();
        List<Book> books = bookDAO.queryBooks(connection);
        books.forEach(System.out::println);
        JDBCUtils.close(connection, null, null);
    }
}

編寫 Web 和測試

圖書列表功能的實現

實現步驟

  • 在後臺管理頁面點選圖書管理,所以需要修改圖書管理請求地址 <a href="bookServlet?action=list">圖書管理</a>
  • 在 BookServlet 程式中新增 list 方法:
    • 查詢全部圖書
    • 儲存到 Request 域中
    • 請求轉發到 book_manager.jsp 圖書管理頁面
  • book_manager.jsp 圖書管理頁面展示所有的圖書資訊
    • 從 Request 域中獲取全部圖書資訊
    • 使用 JSTL 標籤庫遍歷輸出
      • 匯入 taglibs-standard-impl-1.2.1.jartaglibs-standard-spec-1.2.1.jar
      • 修改 book_manager.jsp 頁面的資料遍歷輸出
@WebServlet(name = "BookServlet", urlPatterns = ("/bookServlet"))
public class BookServlet extends BaseServlet {
    private BookService bookService  = new BookServiceImpl();

    // 查詢全部圖書
    protected void list(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        List<Book> books = bookService.queryBooks();
        request.setAttribute("books", books);
        // 請求轉發,這裡不能用請求重定向,想想為什麼?
        // 因為請求轉發的特點是:瀏覽器位址列不發生變化;一次請求;共享 Request 域中的資料
        // 當用戶提交完請求,瀏覽器會記錄下最後一次請求的全部資訊,當用戶按下功能鍵 F5,就會發起瀏覽器記錄的最後一次
           請求。
        // 所以必須是一次請求
        request.getRequestDispatcher("/pages/manager/book_manager.jsp").forward(request, response);
    }
}
新增圖書功能的實現

實現步驟

  • 在圖書管理頁面點選新增按鈕,跳轉到 book_edit.jsp 新增圖書頁面
  • 在新增圖書頁面填寫相關資訊,點選提交按鈕
  • 在 BookServlet 程式中新增 add 方法:
    • 獲取請求的引數,封裝成為 Book 物件
    • 呼叫 bookService.addBook(book) 儲存圖書
    • 請求重定向圖書列表頁面
@WebServlet(name = "BookServlet", urlPatterns = ("/bookServlet"))
public class BookServlet extends BaseServlet {
    private BookService bookService  = new BookServiceImpl();

    // 新增圖書
    protected void add(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 中文編碼問題
        Book book = WebUtils.copyParamToBean(request.getParameterMap(), new Book());
        bookService.addBook(book);
        // 請求重定向,這裡不能用請求轉發,想想為什麼?
        // 因為請求重定向的特點是:瀏覽器位址列會發生變化;兩次請求;不共享 Request 域中的資料
        response.sendRedirect("bookServlet?action=list");
    }
}
刪除圖書功能的實現

實現步驟

  • 在圖書管理頁面點選刪除按鈕(<td><a class="deleteClass" href="bookServlet?action=delete&id=${book.id}">刪除</a></td>
  • 給刪除新增確認提示操作
  • 在 BookServlet 程式中新增 delete 方法:
    • 獲取請求的引數 id
    • 呼叫 bookService.deleteBookById(id) 刪除圖書
    • 請求重定向圖書列表頁面
@WebServlet(name = "BookServlet", urlPatterns = ("/bookServlet"))
public class BookServlet extends BaseServlet {
    private BookService bookService  = new BookServiceImpl();

    // 刪除圖書
    protected void delete(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        int id = WebUtils.parseInt(request.getParameter("id"));
        bookService.deleteBookById(id);
        response.sendRedirect("bookServlet?action=list");
    }
}
修改圖書功能的實現

實現步驟

  • 在圖書管理頁面點選修改按鈕(<td><a href="bookServlet?action=getBook&id=${book.id}">修改</a></td>
  • 在 BookServlet 程式中新增 getBook 方法,獲取要修改的圖書資訊:
    • 獲取圖書編號
    • 呼叫 bookService.queryBookById(id) 獲取圖書資訊
    • 把資訊儲存到 Request 域中
    • 請求轉發book_edit.jsp 圖書編輯頁面
  • 在 BookServlet 程式中新增 update 方法,儲存修改圖書的操作:
    • 獲取請求的引數,封裝成為 Book 物件
    • 呼叫 bookService.updateBook(book) 修改圖書
    • 請求重定向圖書列表頁面
  • 解決 book_edit.jsp 圖書編輯頁面,即要實現新增,又要實現修改操作:
    • 方案一:可以請求發起時,附帶上當前要操作的值,並注入到隱藏域中 <td><a href="bookServlet?action=getBook&id=${book.id}&method=add">修改</a></td> <td><a href="pages/manager/book_edit.jsp&method=update">新增圖書</a></td>
    • 方案二:可以通過判斷當前請求引數中是否包含 id 引數,如果有說明是修改操作,否則是新增操作 <input type="hidden" name="action" value="${ empty param.id ? "add" : "update" }"/>
    • 方案三:可以通過判斷 Request 域中是否包含要修改的圖書資訊物件,如果有說明是修改操作 <input type="hidden" name="id" value="${ empty requestScope.book ? "add" : "update }"/>
@WebServlet(name = "BookServlet", urlPatterns = ("/bookServlet"))
public class BookServlet extends BaseServlet {
    private BookService bookService  = new BookServiceImpl();

    // 查詢圖書
    protected void getBook(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        int id = WebUtils.parseInt(request.getParameter("id"));
        Book book = bookService.queryBookById(id);
        request.setAttribute("book", book);
        request.getRequestDispatcher("/pages/manager/book_edit.jsp").forward(request, response);
    }

    // 更新圖書
    protected void update(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        Book book = WebUtils.copyParamToBean(request.getParameterMap(), new Book());
        bookService.updateBook(book);
        response.sendRedirect("bookServlet?action=list");
    }
}

圖書分頁

分頁模組的分析

由分頁的檢視分析出分頁的物件模型 Page 類,它包括的屬性有:

  • pageNo 當前頁面 它是由客戶端進行傳遞
  • pageSize 每頁顯示數量 它是由客戶端進行傳遞和是由頁面佈局決定
  • pageTotalCount 總記錄數 它是由 select count(*) from 表名 得到
  • pageTotal 總頁碼 它是由 總記錄數 / 每頁數量 得到,注意有餘數的情況
  • item 當前頁資料 它是由 select * form 表名 limit begin, pageSize 得到,begin = (pageNo - 1) * pageSize

點選頁碼需要傳遞兩個引數:pageNo,pageSize

  • BookServlet 程式新增 page 方法處理這個請求:
    • 獲取請求的引數 pageNo,pageSize
    • 呼叫 BookService.page(pageNo, pageSize),返回 page 物件
    • 儲存在 Request 域中
    • 請求轉發到 book_manager.jsp 頁面
  • BookService 程式,處理分頁業務
    • public Page page(pageNo, pageSize)
    • 得到總記錄數、總頁碼、當前頁資料
  • BookDAO 程式
    • queryForPageTotalCount() 求總記錄數 select count(*) from 表名
    • queryForItems(begin, pageSize) 求當前頁資料 select * from 表名 limit begin, pageSize

分頁模型 Page 的抽取

package cn.parzulpan.bean;

import java.util.List;

/**
 * @Author : parzulpan
 * @Time : 2020-12-11
 * @Desc :
 */

/**
 * Page 是分頁的模型物件
 * @param <T> 是具體的模組的 JavaBean 類
 */
public class Page<T> {
    public static final Integer PAGE_SIZE = 4;

    private Integer pageNo; // 當前頁碼
    private Integer pageTotal;  // 總頁碼
    private Integer pageSize = PAGE_SIZE;   // 當前頁顯示數量
    private Integer pageTotalCount; // 總記錄數
    private List<T> items;  // 當前頁資料

    public Page() {
    }

    public Page(Integer pageNo, Integer pageTotal, Integer pageSize, Integer pageTotalCount, List<T> items) {
        this.pageNo = pageNo;
        this.pageTotal = pageTotal;
        this.pageSize = pageSize;
        this.pageTotalCount = pageTotalCount;
        this.items = items;
    }

    public static Integer getPageSize() {
        return PAGE_SIZE;
    }

    public void setPageSize(Integer pageSize) {
        this.pageSize = pageSize;
    }

    public Integer getPageTotalCount() {
        return pageTotalCount;
    }

    public void setPageTotalCount(Integer pageTotalCount) {
        this.pageTotalCount = pageTotalCount;
    }

    public List<T> getItems() {
        return items;
    }

    public void setItems(List<T> items) {
        this.items = items;
    }

    public Integer getPageNo() {
        return pageNo;
    }

    public void setPageNo(Integer pageNo) {
        /* 資料邊界的有效檢查 */
        if (pageNo < 1) {
            pageNo = 1;
        }
        if (pageNo > pageTotal) {
            pageNo = pageTotal;
        }
        this.pageNo = pageNo;
    }

    public Integer getPageTotal() {
        return pageTotal;
    }

    public void setPageTotal(Integer pageTotal) {
        this.pageTotal = pageTotal;
    }

    @Override
    public String toString() {
        return "Page{" +
                "pageNo=" + pageNo +
                ", pageTotal=" + pageTotal +
                ", pageSize=" + pageSize +
                ", pageTotalCount=" + pageTotalCount +
                ", items=" + items +
                '}';
    }
}

分頁的初步實現

BookDAO:

    @Override
    public Integer queryForPageTotalCount(Connection connection) {
        String sql = "select count(*) from t_book";
        Number count = (Number) getValue(connection, sql);
        return count.intValue();
    }

    @Override
    public List<Book> queryForPageItems(Connection connection, int begin, int pageSize) {
        String sql = "select `id`, `name`, `author`, `price`, `sales`, `stock`, `imgPath` from t_book limit ?, ?";
        return getBeanList(connection, sql, begin, pageSize);
    }

BookService:

 @Override
    public Page<Book> page(int pageNo, int pageSize) {
        Page<Book> page = new Page<>();
        Connection connection = JDBCUtils.getConnection();

        page.setPageNo(pageNo); // 設定當前頁碼
        page.setPageSize(pageSize); // 設定每頁顯示的數量
        Integer pageTotalCount = bookDAO.queryForPageTotalCount(connection);
        page.setPageTotalCount(pageTotalCount); // 設定總記錄數
        int pageTotal = pageTotalCount / pageSize;
        if (pageTotalCount % pageSize > 0) {
            pageTotal += 1;
        }
        page.setPageTotal(pageTotal);   // 設定總頁碼
        int begin = (page.getPageNo() - 1) * pageSize;  // 求當前頁資料的開始索引
        List<Book> items = bookDAO.queryForPageItems(connection, begin,pageSize);   // 求當前頁資料
        page.setItems(items);

        JDBCUtils.close(connection, null, null);

        return page;

BookServlet:

    // 處理分頁
    protected void page(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        int pageNo = WebUtils.parseInt(request.getParameter("pageNo"), 1);
        int pageSize = WebUtils.parseInt(request.getParameter("pageSize"), Page.PAGE_SIZE);
        Page<Book> page = bookService.page(pageNo, pageSize);
        request.setAttribute("page", page);
        request.getRequestDispatcher("/pages/manager/book_manager.jsp").forward(request, response);
    }

跳轉增加回顯頁碼


<%--		新增分頁--%>
		<div id="page_nav">
<%--			<a href="#">首頁</a>--%>
<%--			<a href="#">上一頁</a>--%>
			<c:if test="${requestScope.page.pageNo > 1}">
				<a href="bookServlet?action=page&pageNo=1">首頁</a>
				<a href="bookServlet?action=page&pageNo=${requestScope.page.pageNo-1}">上一頁</a>
			</c:if>

<%--			<a href="#">3</a>--%>
<%--			【${requestScope.page.pageNo}】--%>
<%--			<a href="#">5</a>--%>
			<%--頁碼輸出的開始--%>
			<c:choose>
				<%--情況 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="bookServlet?action=page&pageNo=${i}">${i}</a>
				</c:if>
			</c:forEach>
			<%--頁碼輸出的結束--%>


<%--			<a href="#">下一頁</a>--%>
<%--			<a href="#">末頁</a>--%>
			<c:if test="${requestScope.page.pageNo < requestScope.page.pageTotal}">
				<a href="bookServlet?action=page&pageNo=${requestScope.page.pageNo+1}">下一頁</a>
				<a href="bookServlet?action=page&pageNo=${requestScope.page.pageTotal}">末頁</a>
			</c:if>

<%--			共${ requestScope.page.pageTotal }頁,${requestScope.page.pageTotalCount} 條記錄--%>
<%--			跳轉到第 <input value="4" name="pn" id="pn_input"/> 頁--%>
<%--			<input type="button" value="確定">--%>
			共${ requestScope.page.pageTotal }頁,${requestScope.page.pageTotalCount} 條記錄
			,跳轉到第 <input value="${param.pageNo}" name="pn" id="pn_input"/> 頁
			<input id="searchPageBtn" type="button" value="確定">
		</div>
	</div>

首頁的跳轉

  • web 目錄下的 index.jsp,請求轉發到 ClientBookServlet 程式
  • ClientBookServlet 程式中處理分頁,請求轉發到 /pages/client/index.jsp

分頁條的抽取

因為除了分頁條的請求地址不同,其他全部相同。所以可以抽取,然後包含。

步驟

  • 抽取分頁條中請求地址為 url 變數
    • 在 page 物件中新增 url 屬性
    • 在 Servlet 程式的 page 分頁方法中分別設定 url 的分頁請求地址
    • 修改分頁條中請求地址為 url 變數輸出,並抽取一個單獨的 jsp 頁面

首頁價格搜尋

  • 點選價格查詢按鈕,傳遞兩個引數:pageNo,pageSize, min, max

  • ClientBookServlet 程式新增 pageByPrice 方法處理這個請求:

    • 獲取請求的引數 pageNo,pageSize, min, max
    • 呼叫 BookService.pageByPrice(pageNo, pageSize, min, max),返回 page 物件
    • 儲存在 Request 域中
    • 請求轉發到 /pages/client/index.jsp 頁面
  • BookService 程式,處理價格區間分頁業務

    • public Page page(pageNo, pageSizee, min, max)
    • 得到總記錄數、總頁碼、當前頁資料
  • BookDAO 程式

    • queryForPageTotalCount(min, max) 求總記錄數 select count(*) from 表名 where price between min and max
    • queryForItems(begin, pageSize, min, max) 求當前頁資料 select * from 表名 where price between min and max order by price limit begin, pageSize

階段六 登入、登出、驗證碼

登入-顯示使用者名稱

  • UserServlet 程式中儲存使用者登入的資訊
  • 修改 login_success_menu.jsp 頁面
  • 修改 index.jsp 頁面的選單

登出-登出使用者

  • 銷燬 Session 中使用者登入的資訊(或者銷燬 Session)
  • 重定向到首頁(或登入頁面)。

表單重複提交解決-驗證碼

表單重複提交有三種常見的情況:

  • 一:提交完表單。伺服器使用請求轉來進行頁面跳轉。這個時候,使用者按下功能鍵 F5,就會發起最後一次的請求。造成表單重複提交問題。解決方法:使用重定向來進行跳轉
  • 二:使用者正常提交伺服器,但是由於網路延遲等原因,遲遲未收到伺服器的響應,這個時候,使用者以為提交失敗,就會著急,然後多點了幾次提交操作,也會造成表單重複提交。
  • 三:使用者正常提交伺服器。伺服器也沒有延遲,但是提交完成後,使用者回退瀏覽器。重新提交。也會造成表單重複提交。

驗證碼解決表單重複提交的底層原理

  • 當用戶第一次訪問表單時,就要給表單生成一個隨機的驗證碼字串
  • 把這個驗證碼字串儲存在 Session 域中
  • 並把這個驗證碼字串以圖片等形式顯示在表單項中
  • 點選 "提交" 按鈕,Servlet 程式就要獲取 Session 域中的驗證碼,並刪除 Session 域中的驗證碼
  • Servlet 程式再獲取表單項資訊
  • 比較這個驗證碼和表單中的驗證碼是否相等
    • 相等,則允許操作
    • 不相等,則阻止操作

kaptcha 的使用

谷歌驗證碼 kaptcha 使用步驟:

  • 匯入谷歌驗證碼的 jar 包 kaptcha-2.3.2.jar

  • 在 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>
    
  • 在表單中使用 img 標籤去顯示驗證碼圖片並使用它

    
    <label>驗證碼:</label>
    <label for="code"></label><input class="itxt" type="text" style="width: 150px;" name="code"  id="code"/>
    <img alt="" src="/kaptcha.jpg" style="float: right; margin-right: 40px">
    <br />
    <br />
    <input type="submit" value="註冊" id="sub_btn" />
    
    
  • 在伺服器獲取谷歌生成的驗證碼和客戶端傳送過來的驗證碼比較使用

    // 獲取 Session 中的驗證碼
    String token = (String)request.getSession().getAttribute(KAPTCHA_SESSION_KEY);
    // 刪除 Session 中的驗證碼
    request.getSession().removeAttribute(KAPTCHA_SESSION_KEY);
    
  • 切換驗證碼

    // 給驗證碼的圖片,繫結單擊事件
    $("#code_img").click(function () {
      // 在事件響應的 function 函式中有一個 this 物件。這個 this 物件,是當前正在響應事件的 dom 物件
      // src 屬性表示驗證碼 img 標籤的 圖片路徑。它可讀,可寫
      // alert(this.src);
      this.src = "${basePath}kaptcha.jpg?d=" + new Date();
    });
    

階段七 購物車和訂單管理

購物車

購物車模組分析

實現購物車功能的技術有:

  • Session 版本,即把購物車資訊儲存到 Session 域中,這次使用這個版本
  • 資料庫版本,即把購物車資訊儲存到資料庫中
  • Redis + 資料庫 + Cookie 版本,即使用 Cookie + Redis 快取,資料庫儲存

由購物車的介面分析出購物車的模型:

  • Cart 購物車物件
    • totalCount 總商品數量
    • totalPrice 總商品金額
    • items 購物車商品
  • CartItem 購物車商品項
    • id 商品編號
    • name 商品名稱
    • count 商品數量
    • price 商品單價
    • totalPrice 商品總價

購物車功能:

  • 加入購物車 CartServlet 程式中 addItem() Cart物件 addItem(CartItem)
  • 刪除購物車中商品 CartServlet 程式中 deleteItem() Cart物件 deleteItem(id)
  • 清空購物車 CartServlet 程式中 clear() Cart物件 clear()
  • 修改購物車中商品 CartServlet 程式中 updateCount() Cart物件 updateCount(id, count)

購物車模組實現

package cn.parzulpan.bean;

import java.math.BigDecimal;

/**
 * @Author : parzulpan
 * @Time : 2020-12-13
 * @Desc : 購物車的商品項
 */

public class CartItem {
    private Integer id; // 商品編號
    private String name;    // 商品名稱
    private Integer count;  // 商品數量
    private BigDecimal price;   // 商品單價
    private BigDecimal totalPrice;  // 商品總價

    public CartItem() {
    }

    public CartItem(Integer id, String name, Integer count, BigDecimal price, BigDecimal totalPrice) {
        this.id = id;
        this.name = name;
        this.count = count;
        this.price = price;
        this.totalPrice = totalPrice;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getCount() {
        return count;
    }

    public void setCount(Integer count) {
        this.count = count;
    }

    public BigDecimal getPrice() {
        return price;
    }

    public void setPrice(BigDecimal price) {
        this.price = price;
    }

    public BigDecimal getTotalPrice() {
        return totalPrice;
    }

    public void setTotalPrice(BigDecimal totalPrice) {
        this.totalPrice = totalPrice;
    }

    @Override
    public String toString() {
        return "CartItem{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", count=" + count +
                ", price=" + price +
                ", totalPrice=" + totalPrice +
                '}';
    }
}
package cn.parzulpan.bean;

import java.math.BigDecimal;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * @Author : parzulpan
 * @Time : 2020-12-13
 * @Desc : 購物車物件
 */

public class Cart {
//    private Integer totalCount;
//    private BigDecimal totalPrice;
    private Map<Integer, CartItem> items = new LinkedHashMap<>();   // 購物車商品,key 是商品編號,value 是商品資訊

    /**
     * 加入購物車
     * @param cartItem
     */
    public void addItem(CartItem cartItem) {
        CartItem item = items.get(cartItem.getId());
        if (item == null) {
            items.put(cartItem.getId(), cartItem);
        } else {
            item.setCount(item.getCount() + 1);
            item.setTotalPrice(item.getPrice().multiply(new BigDecimal(item.getCount())));
        }

    }

    /**
     * 刪除購物車中商品
     * @param id
     */
    public void deleteItem(Integer id) {
        items.remove(id);
    }

    /**
     * 清空購物車
     */
    public void clear() {
        items.clear();
    }

    /**
     * 修改購物車中商品
     * @param id
     * @param count
     */
    public void updateCount(Integer id, Integer count) {
        CartItem item = items.get(id);
        if (item != null) {
            item.setCount(count);
            item.setTotalPrice(item.getPrice().multiply(new BigDecimal(item.getCount())));
        }
    }

    public Integer getTotalCount() {
        Integer totalCount = 0;
        for (Map.Entry<Integer, CartItem> entry : items.entrySet()) {
            totalCount += entry.getValue().getCount();
        }

        return totalCount;
    }

    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{" +
                "items=" + items +
                '}';
    }
}

加入購物車

 // 加入購物車
protected void addItem(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    // 獲取請求的引數
    int id = WebUtils.parseInt(request.getParameter("id"), 0);
    // 得到圖書資訊
    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)request.getSession().getAttribute("cart");
    if (cart == null) {
        cart = new Cart();
        request.getSession().setAttribute("cart", cart);
    }
    cart.addItem(cartItem);

    System.out.println(cart);
    System.out.println("請求頭 Referer 的值:" + request.getHeader("Referer"));

    // 如果跳回新增商品的頁面?
    // 在 HTTP 協議中有一個請求頭,叫 Referer,它可以把請求發起時的瀏覽器位址列的地址傳送給伺服器
    // 重定向回原來商品所在的地址頁面
//        response.sendRedirect(request.getContextPath());
    response.sendRedirect(request.getHeader("Referer"));
}

展示購物車

<%--cart.jsp--%>

            <%--輸出 Session 域中的資料--%>
			<%--如果購物車為空--%>
			<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}">
				<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>

刪除購物車中商品

// 刪除購物車中商品
protected void deleteItem(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    // 獲取請求的引數
    int id = WebUtils.parseInt(request.getParameter("id"), 0);
    // 獲得購物車物件
    Cart cart = (Cart)request.getSession().getAttribute("cart");

    if (cart != null) {
        cart.deleteItem(id);
        // 重定向回原來購物車展示頁面
        response.sendRedirect(request.getHeader("Referer"));
    }
}

清空購物車

// 清空購物車
protected void clear(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    // 獲得購物車物件
    Cart cart = (Cart)request.getSession().getAttribute("cart");
    if (cart != null) {
        cart.clear();
        // 重定向回原來購物車展示頁面
        response.sendRedirect(request.getHeader("Referer"));
    }
}

修改購物車中商品

// 修改購物車中商品
protected void updateCount(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    // 獲取請求的引數
    int id = WebUtils.parseInt(request.getParameter("id"), 0);
    int count = WebUtils.parseInt(request.getParameter("count"), 1);
    // 獲得購物車物件
    Cart cart = (Cart)request.getSession().getAttribute("cart");
    if (cart != null) {
        cart.updateCount(id, count);
        // 重定向回原來購物車展示頁面
        response.sendRedirect(request.getHeader("Referer"));
    }
}

回顯購物車資料

在新增商品到購物車的時候,儲存最後一個新增的商品名稱。

訂單

訂單模組分析

由訂單的介面分析出訂單的模型:

  • Order 訂單物件
    • orderId 訂單號(唯一)
    • createTime 下單時間
    • price 金額
    • status 訂單狀態,0 未發貨,1 已發貨,2 已簽收
    • userId 使用者編號
  • OrderItem 訂單商品項
    • id 商品編號
    • name 商品名稱
    • count 商品數量
    • price 商品單價
    • orderId 訂單號(唯一)

訂單功能:

  • 生成訂單 OrderServlet 程式中 createOrder() OrderService 程式中 createOrder(Cart, userId)
  • 查詢所有訂單(管理員) OrderServlet 程式中 showAllOrders() OrderService 程式中 showAllOrders()
  • 發貨(管理員) OrderServlet 程式中 sendOrder() OrderService 程式中 sendOrder(orderId)
  • 檢視訂單詳情(使用者/管理員) OrderServlet 程式中 showOrderDetail() OrderService 程式中 showOrderDetail(orderId)
  • 檢視我的訂單(使用者) OrderServlet 程式中 showMyOrder() OrderService 程式中 showMyOrder(userId)
  • 簽收訂單(使用者) OrderServlet 程式中 receiverOrder() OrderService 程式中 receiverOrder(orderId)

OrderDAO 程式:

  • saveOrder(Order) 儲存訂單
  • queryOrders() 查詢全部訂單
  • changeOrderStatus(orderId, status) 修改訂單狀態
  • queryOrderByUser(userId) 根據使用者編號查詢訂單資訊

OderItemDAO 程式:

  • saveOrderItem(OrderItem) 儲存訂單項
  • queryOrderItemsByOrderId(orderId) 根據訂單號查詢訂單明細

訂單模組實現

階段八 過濾器攔截和管理事務

使用 Filter 攔截/pages/manager/所有內容,實現許可權檢查

package cn.parzulpan.web;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

/**
 * @Author : parzulpan
 * @Time : 2020-12-13
 * @Desc : 使用 Filter 過濾器攔截/pages/manager/所有內容,實現許可權檢查
 */

@WebFilter(filterName = "ManagerFilter", urlPatterns = {"/pages/manager/*", "/bookServlet"})
public class ManagerFilter implements Filter {
    public void destroy() {
    }

    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) req;
        Object user = httpServletRequest.getSession().getAttribute("user");
        if (user == null) {
            httpServletRequest.getRequestDispatcher("/pages/user/login.jsp").forward(req, resp);
        } else {
            chain.doFilter(req, resp);
        }

    }

    public void init(FilterConfig config) throws ServletException {

    }
}

使用 Filter 和 ThreadLocal 組合管理事務

ThreadLocal 的使用

ThreadLocal 可以解決多執行緒的資料安全問題。

ThreadLocal 可以給當前執行緒關聯一個數據,這個資料可以是普通變數,可以是物件,也可以是陣列和集合等。

ThreadLocal 特點

  • ThreadLocal 可以為當前執行緒關聯一個數據,它可以像 Map 一樣存取資料,key 為當前執行緒
  • 每一個 ThreadLocal 物件,只能為當前執行緒關聯一個數據,如果要為當前執行緒關聯多個數據,就需要使用 多個 ThreadLocal 例項,所以是執行緒安全的
  • 每個 ThreadLocal 物件例項定義的時候,一般都是 Static 型別
  • ThreadLocal 中儲存資料,線上程銷燬後,會由 JVM 自動釋放

組合管理事務

要確保所有操作要麼都成功,要麼都失敗,就必須使用資料庫事務。

而要確保所有操作都在一個事務內,就必須確保所有操作都使用同一個 Connection 連線物件。

可以用 ThreadLocal 物件來確保所有的操作都使用同一個 Connection 連線物件,但是這個操作的前提條件是所有操作必須在同一個執行緒中完成,這顯然是滿足的。


public void JDBC() {
    Connection connection = JDBCUtils.getConnection();  // ThreadLocal<Connection> conns = new ThreadLocal<>(); 
                                                        // conns.set(conn);  儲存從資料庫連線池中獲取的連線的連線物件
    try {
        // 1. 獲取資料庫連線
        // 1.1 手寫的連線(JDBCUtils):載入配置資訊 -> 讀取配置資訊 -> 載入驅動 -> 獲取連線
        // 1.2 資料庫連線池:C3P0、DBCP、Druid

        // 取消事務自動提交
        connection.setAutoCommit(false);    // connection = conns.get(); 得到前面的儲存的連線物件
 
        // 2. 對資料表進行一系列 CRUD 操作    // connection = conns.get(); 得到前面的儲存的連線物件
        // 2.1 使用 PreparedStatement 實現通用的增刪改、查詢操作
        // 2.2 考慮事務實現通用的增刪改、查詢操作
        // 2.3 使用 commons-dbutils

        // 提交資料
        connection.commit();    // connection = conns.get(); 得到前面的儲存的連線物件
    } catch (Exception e) {
        e.printStackTrace();

        try {
            // 回滾資料
            connection.rollback();    // connection = conns.get(); 得到前面的儲存的連線物件
        } catch (SQLException e1) {
            e1.printStackTrace();
        }
    } finally {
        // 恢復事務自動提交
        connection.setAutoCommit(true);    // connection = conns.get(); 得到前面的儲存的連線物件

        // 3. 關閉資料庫連線    // connection = conns.get(); 得到前面的儲存的連線物件
        // 3.1 手寫的關閉(JDBCUtils)
        // 3.2 使用 commons-dbutils
    }
}

使用 Filter 過濾器統一給所有的 Service 方法都加上 try-catch

因為 doFilter() 方法會呼叫下一個 filter 過濾器,然後呼叫目標資源。等於說間接呼叫了 Servlet 程式中的業務方法,所以可以在這裡 進行 try-catch 來實現事務的管理。

package cn.parzulpan.web;

import cn.parzulpan.utils.JDBCUtils;

import javax.servlet.*;
import java.io.IOException;

/**
 * @Author : parzulpan
 * @Time : 2020-12-13
 * @Desc : 事務過濾器
 */

public class TransactionFilter implements Filter {
    public void destroy() {
    }

    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {

        try {
            chain.doFilter(req, resp);
            JDBCUtils.commitAndClose();
        } catch (IOException e) {
            e.printStackTrace();
            JDBCUtils.rollbackAndClose();
        }
    }

    public void init(FilterConfig config) throws ServletException {

    }

}
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

    <filter>
        <filter-name>TransactionFilter</filter-name>
        <filter-class>cn.parzulpan.web.TransactionFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>TransactionFilter</filter-name>
        <!-- /* 表示當前工程下所有請求 -->
        <url-pattern>/*</url-pattern>
    </filter-mapping>
</web-app>

最後一定要記得把 BaseServlet 中的異常往外拋給事務過濾器

為了提升使用者互動,可以將所有異常都統一交給 Tomcat,讓 Tomcat 展示友好的錯誤資訊頁面。

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

     <<!--error-page 標籤配置,伺服器出錯之後,自動跳轉的頁面-->
     <error-page>
         <!--error-code 是錯誤型別-->
         <error-code>500</error-code>
         <!--location 標籤表示。要跳轉去的頁面路徑-->
         <location>/pages/error/error500.jsp</location>
     </error-page>
     <error-page>
         <!--error-code 是錯誤型別-->
         <error-code>404</error-code>
         <!--location 標籤表示。要跳轉去的頁面路徑-->
         <location>/pages/error/error404.jsp</location>
     </error-page>
</web-app>

階段九 使用 AJAX

使用 AJAX 驗證使用者名稱是否可用

  • 在註冊頁面的使用者名稱輸入框監聽失去焦點事件
  • UserServlet 程式 的 ajaxExistsUsername 驗證使用者名稱是否可用
    • 先獲取請求的引數 username
    • 呼叫 UserService.checkUsername() 驗證使用者名稱是否可用
    • 把客戶端需要的結果封裝為 Map 物件,然後回傳給客戶端
@WebServlet(name = "UserServlet", urlPatterns = ("/userServlet"))
public class UserServlet extends BaseServlet {  // 繼承 BaseServlet 程式
    private UserService userService = new UserServiceImpl();

    protected void ajaxExistsUsername(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String username = request.getParameter("username");
        boolean existUsername = userService.checkUsername(username);
        Map<String, Object> resultMap = new HashMap<>();
        resultMap.put("existUsername", existUsername);

        Gson gson = new Gson();
        String json = gson.toJson(resultMap);
        response.getWriter().write(json);
    }
}
$(function() {
			$("#username").blur(function () {
				let username = this.value;
				$.getJSON("userServlet", "action=ajaxExistsUsername&username=" + username, function (data) {
					console.log(data);
					if (data.existUsername) {
						$("span.errorMsg").text("使用者名稱已存在!");
					} else {
						$("span.errorMsg").text("使用者名稱可用!");
					}
				})
			});
});

使用 AJAX 把商品新增到購物車

  • 點選加入購物車,傳送請求
  • CartServlet 程式 的 ajaxAddItem() 處理請求
    • 獲取商品編號
    • 呼叫 bookService.queryBookById()
    • 把 book 物件轉換為 CartItem
    • 獲取 Session 中的 Cart 物件
    • 呼叫 cart.addItem() 新增商品項
    • 回傳給客戶端 購物車總的商品數和最後一個新增的商品名稱
  • 客戶端根據響應的資料,做頁面的區域性更新
@WebServlet(name = "CartServlet", urlPatterns = ("/cartServlet"))
public class CartServlet extends BaseServlet {
    BookService bookService = new BookServiceImpl();

    // 使用 AJAX 加入購物車
    protected void ajaxAddItem(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 獲取請求的引數
        int id = WebUtils.parseInt(request.getParameter("id"), 0);
        // 得到圖書資訊
        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)request.getSession().getAttribute("cart");
        if (cart == null) {
            cart = new Cart();
            request.getSession().setAttribute("cart", cart);
        }
        cart.addItem(cartItem);

        // 購物車資料回顯,最後一個新增的商品名稱
        request.getSession().setAttribute("lastName", cartItem.getName());

        // 回傳給客戶端 購物車總的商品數和最後一個新增的商品名稱
        Map<String, Object> resultMap = new HashMap<>();
        resultMap.put("totalCount", cart.getTotalCount());
        resultMap.put("lastName", cartItem.getName());
        response.getWriter().write(new Gson().toJson(resultMap));
    }
}
	<script type="text/javascript">
		$(function () {
			// 給加入購物車按鈕繫結單擊事件
			$("button.addToCart").click(function () {
				let bookId = $(this).attr("bookId");
				// location.href = "cartServlet?action=addItem&id=" + bookId;
				// 使用 AJAX
				$.getJSON("cartServlet", "action=ajaxAddItem&id=" + bookId, function (data) {
					$("#cartTotalCount").text("您的購物車中有 " + data.totalCount + " 件商品");
					$("#cartLastName").text(data.lastName);
				})
			});
		});
	</script>
			<div style="text-align: center">
				<%--購物車資料回顯--%>
				<c:if test="${empty sessionScope.cart.items}">
					<%--購物車為空--%>
					<%--使用 AJAX 區域性更新--%>
					<span id="cartTotalCount"></span>
					<div>
						<span style="color: red" id="cartLastName">當前購物車為空!</span>
					</div>
				</c:if>
				<c:if test="${not empty sessionScope.cart.items}">
					<%--購物車不為空--%>
					<%--使用 AJAX 區域性更新--%>
					<span id="cartTotalCount">您的購物車中有 <a style="color: red">${sessionScope.cart.totalCount}</a> 件商品</span>
					<div>
						您剛剛將 <span style="color: red" id="cartLastName">${sessionScope.lastName}</span> 加入到了購物車中
					</div>
				</c:if>

			</div>

總結