第14章 项目实战——尚硅谷书城¶
第 14 章¶
项目实战 —— 尚硅谷书城¶
前面介绍了 JavaWeb 学习阶段所涉及的技术知识,从前端页面开发,到后端接口实现,以及借助框架优化开发,实现前后端分离。至此,大家可以独自完成 JavaWeb 项目了。本章将带领大家共同完成一个项目 “尚硅谷书城”,综合运用本书所学知识,巩固和检验所学成果。
14.1 项目概述¶
尚硅谷书城项目主要是搭建一个图书的销售平台,后台进行图书的数据维护、添加图书、修改图书、删除图书等功能。前台进行图书的展示、加入购物车、购物车图书商品的操作,以及结账等功能。
14.1.1 功能介绍¶
尚硅谷书城项目包括的功能有登录注册、图书的增删改查操作、加入购物车、清空购物车、购物车商品数量的修改,以及结账等功能。该项目基本上能够涵盖本书所有的知识点,希望大家认真练习,做到知其然并知其所以然,在今后的开发中灵活运用所学知识,能够举一反三,融会贯通。
下面给大家展示一些项目截图,如图 14-1、图 14-2、图 14-3 所示。

这几张图片就是我们项目中要实现的部分功能,接下来我们从零开始,逐步完成一个成品项目。创建该项目的关键在于应用前面所学的知识点,基于此有一些功能暂时无法实现,例如,后台管理的权限控制、结账中支付接口的对接等,但并不影响项目的整体效果。尚硅谷书城项目采用 MVC 架构进行开发,涉及的技术有 Servlet、Thymeleaf、Filter、Session 等,数据库使用的是 MySQL。


14.1.2 数据库设计¶
尚硅谷书城项目的数据库共涉及 4 张表,分别为用户表 t_users、图书表 t_books、订单表 t_order、订单详情表 t_order_item,表结构分别如表 14-1 至表 14-4 所示。
表 14-1 t_users 表结构
| 字段 | 类型 | 说明 |
| id | int、自增主键 | 用户 ID |
| username | varchar(20) | 用户名 |
| password | varchar(50) | 密码 |
| varchar(50) | 邮箱 |
表 14-2 t_books 表结构
| 字段 | 类型 | 说明 |
| id | int、自增主键 | 图书ID |
| title | varchar(50) | 书名 |
| author | varchar(50) | 作者 |
| price | Double | 单价 |
| sales | Int | 销量 |
| stock | Int | 库存 |
| img_path | varchar(200) | 图片路径 |
表 14-3 t_order 表结构
| 字段 | 类型 | 说明 |
| order_id | int、自增主键 | 订单 ID |
| order_sequence | varchar(200) | 订单编号 |
| create_time | varchar(200) | 创建时间 |
| total_count | int | 总件数 |
| total_amount | double | 总价 |
| order_status | int | 订单状态(0:待发货 1:已发货 2:确认收货 3:已评价) |
| user_id | int | 订单所属用户 ID |
表 14-4 t_order_item 表结构
| 字段 | 类型 | 说明 |
| item_id | int、自增主键 | 订单项 ID |
| book_name | varchar(50) | 书名 |
| price | Double | 单价 |
| img_path | varchar(200) | 图片路径 |
| item_count | Int | 件数 |
| item_amount | Double | 金额 |
| order_id | Int | 订单项所属的订单 id |
14.1.3 项目搭建¶
在对尚硅谷书城项目及用到的数据库表进行了简单了解后,接下来进入项目开发。首先完成一些项目开发前的准备工作。
1. 导入建表和初始化表数据的 sql 语句¶
从项目资源素材中获取 bookManager.sql 文件,在 atguigu 数据库下执行该文件,创建对应表格,如图 14-4 所示。
2. 创建项目¶
打开 IDEA,创建一个 Project,设置好项目名称和存储位置等信息,最后单击 “Create” 按钮创建即可,如图 14-5 所示。

在项目上右键选择 “Add Framework Support” 进入新窗口,如图 14-6 所示。并在 Java EE 类目下选择 “Web Application” 选项为该项目添加 web 组件,最后单击 “OK” 按钮即可。如图 14-7 所示。



创建完后,项目目录会多出一个 web 组件,web 目录下的 index.jsp 删除即可,我们采用 Thymeleaf 进行视图渲染。BookManager 项目的目录结构如图 14-8 所示。
3. 添加数据库相关资源¶
接下来需要将一些工具的 jar 包,以及之前课程中封装过的工具类导入到项目内,清单如下。
- 相关 jar 包
- 工具类
- 配置文件:db.properties
下面依次将其添加到书城项目中。
(1) 添加 Tomcat 依赖。
单击菜单中的 “File”,然后选择 “Project Structure” 进入新窗口,如图 14-9 所示。选择 “Modules”,然后选中 “BookManager” 模块,单击右侧的 “Dependencies”,单击加号选择 “2 Library”,选中 “Tomcat 8.5.27”,再单击 “Add Selected”,最后单击 “OK” 按钮即可添加成功。
(2) 添加相关 jar 包。¶
在 WEB-INF 下创建 lib 目录,将相关 jar 包复制粘贴进去(从资料包中寻找),并右键 Add As Library 将其添加为项目的依赖,如图 14-10 所示。


(3) 创建配置文件。¶
在项目根目录下创建 resources 目录,并右键选择 “Mark Directory as”,标记该目录为 Resources Root 类型。这样项目编译时,会将该目录下的配置文件编译到类路径下。在 resources 目录下新建文件 db.properties,并添加如下配置信息。
driverClassName=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/atguigu
username=root
password=root
initialSize=10
maxActive=20
inIdle=1
maxWait=1000
需要注意,关于 MySQL 数据库的端口号、数据库名称,以及密码都需要设置成自己的。
(4)创建包名,添加工具类。¶
在 src 目录下将 package 准备好供后期使用。
将 JDBCTools、MD5Util、CommonResult 工具类复制到 util 包下,将 BaseDao 复制到 dao 包下,将 ViewBaseServlet 复制到 servlet.base 包下。
另外,在 web.xml 中设置 Thymeleaf 渲染视图的前缀和后缀。
<?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
xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<!--配置全局初始化参数:配置 Thymeleaf 视图的前缀和后缀-->
<context-param>
<!--key 值是自定义的-->
<param-name>view-prefix</param-name>
<!--value 值是根据你的项目结构分析出来的-->
<param-value>/WEB-INF/pages;</param-value>
</context-param>
<context-param>
<param-name>view-suffix</param-name>
<param-value>.html</param-value>
</context-param>
</web-app>
(5) 拷贝静态资源。¶
将静态资源中 static 文件夹拷贝到项目的 web 目录下,将 pages 文件夹拷贝到 WEB-INF 目录下,将 index.html 拷贝到 WEB-INF/pages 目录下。
操作后的项目结构如图 14-11 所示。

14.2 首页的访问¶
书城项目使用 Thymeleaf 进行视图渲染,将页面都存放在 / WEB-INF/pages/ 目录下,因此首页的访问,需要先发请求到 Servlet,再由 Thymeleaf 进行渲染。步骤如下。
IndexServlet 代码如下。
package com.atguigu.servlet.model;
//省略 import 语句
@WebSocket("/index.html")
public class IndexServlet extends++,ViewBaseServlet {
@Override
protected void doGet(HttpServletRequest request,HttpServletResponse response) throws ServletException, IOException {
this doPost(request, response);
}
@Override
protected void doPost(HttpServletRequest request,HttpServletResponse response) throws ServletException, IOException {
this.processTemplate("index",request, response);
}
}
index.html 页面代码如下。
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>书城首页</title>
<link rel="stylesheet" th:href="@{/static/css/minireset.css}" />
<link rel="stylesheet" th:href="@{/static/css/common.css}" />
<link rel="stylesheet" th:href="@{/static/css/iconfont.css}" />
<link rel="stylesheet" th:href="@{/static/css/index.css}" />
<link rel="stylesheet" th:href="@{/static/css/swiper.min.css}" />
</head>
<body>
<!-- body 代码省略 -->
</body>
添加 Tomcat 镜像服务器部署书城项目,如图 14-12 所示。然后启动服务器端,测试首页是否访问成功。打开浏览器通过 “http://localhost:8888/BookManager_war_exploded/index.html” 访问书城首页,如图 14-13 所示。


14.3 用户管理模块¶
用户管理模块主要包括三个功能,分别是用户注册、用户登录和用户注销。下面依次对这三个功能展开介绍。
14.3.1 用户注册¶
注册功能是用户的第一个操作,也是整个项目的第一个功能。重点理解按照 MVC 架构进行开发时,各层对应的代码结构及其调用关系。后面其他功能与此类似。
1. 开发实体类¶
在 bean 包下创建 User 类,对应数据库中的 t_users 表。示例代码如下。
package com.atguigu.bean;
public class User {
private Integer id; //用户 id
private String username; //用户名字
private String password; //用户密码
private String email; //用户邮箱
//get,set 方法省略
}
2. 注册页面的访问¶
实现在首页中单击 “注册” 超链接跳转注册页面。
(1)创建 ToRegistPageServlet 类,实现跳转注册页面。
package com.atguigu.servlet.model;
@WebServlet("/regist.html")
public class ToRegistPageServlet extends++,ViewBaseServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws,%InException, IOException {
this.doPost(request, response);
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.processTemplate("user/regist", request, response);
}
(2)修改 index.html 页面中注册对应的超链接,设置访问路径为 ToRegistPageServlet 类的映射路径。示例代码如下。
(3)修改 / WEB-INF/pages/user/ 目录下的 regist.html 页面,添加 Thymeleaf 的名称空间,实现页面渲染。
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>尚硅谷会员注册页面</title>
<link type="text/css" rel="stylesheet" th:href="@{/static/css/style.css}"/>
<link rel="stylesheet" th:href="@{/static/css/register.css}" />
<style type="text/css">
.login_form {
height: 420px;
margin-top: 25px;
}
</style>
</head>
<body>
<div id="login_header">
<a th:href="@{/index.html}">
<img class="logo_img" alt="" th:src="@{/static/img/logo.gif}" />
</a>
</div>
<div class="login_banner">
<div class="register_form">
<h1>注册尚硅谷会员</h1>
<form action="regist_success.html">
<div class="form-item">
<div>
<label>用户名称:</label>
<input type="text" placeholder="请输入用户名" />
</div>
<span class="errMess">用户名应为6~16位数组和字母组成</span>
</div>
<div class="form-item">
<div>
<label>用户密码:</label>
<input type="password" placeholder="请输入密码" />
</div>
<span class="errMess">密码的长度至少为8位</span>
</div>
</body>
</html>
<div class="form-item">
<div>
<label>确认密码:</label>
<input type="password" placeholder="请输入确认密码" />
</div>
<span class="errMess">密码两次输入不一致</span>
</div>
<div class="form-item">
<div>
<label>用户邮箱:</label>
<input type="text" placeholder="请输入邮箱" />
</div>
<span class="errMess">请输入正确格式的邮箱</span>
</div>
<div class="form-item">
<div>
<label>验证码:</label>
<div class="verify">
<input type="text" placeholder="" />
<img th:src="@{/static/img/code.bmp}" alt="" height="40px"/>
</div>
</div>
<span class="errMess">请输入正确的验证码</span>
</div>
<button class="btn">注册</button>
</form>
</div>
</div>
<div id="bottom">
<span>
尚硅谷书城.Copyright ©
</span>
</div>
</body>
</html>
然后启动项目,单击首页的 “注册”,成功跳转注册页面,如图 14-14 所示。
接下来分别实现注册页面中涉及的验证码展示、表单正则验证,以及用户名唯一验证等功能,并且注册功能完善后进行统一测试。
3. 验证码图片动态展示¶
(1)在 web.xml 中配置 KaptchaServlet 的映射路径。
<!--kaptchaServlet 的访问路径,生成验证码图片-->
<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</url-pattern>
</servlet-mapping>

(2)修改 regist.html 页面,实现验证码动态展示功能。
在 regist.html 页面中修改 <img> 标签的访问路径,为 KaptchaServlet 的映射路径,并绑定单击事件。
在 regist.html 页面 <head> 标签内引入 vue.js 文件,并在 <body> 标签内添加最外层 <div> 标签,id 值为 box,用于挂载 Vue 代码,关键代码如下。
<head>
<!-- 省略部分代码 -->
<script th:src="@{/static/script/vue.js}"></script</head>
<body>
<div id="box">
<!-- 省略 body 内代码 -->
</div>
</body>
编写如下 Vue 代码。
<script>
new Vue({
el:"#box",
data: {
codeSrc:"kaptcha"
}
},
methods: {
changeCode:function () {
//time=new Date( )是为了每次的请求路径都不一样,避免浏览器缓存
this.codeSrc="kaptcha?time="+new Date( );
}
}
})
</script>
4. 表单的正则验证¶
关于注册页面中的表单项,我们需要对注册用户提出一些数据格式要求,如用户名的组成部分、用户名的长度,密码的长度,以及邮箱的格式是否符合要求等。
首先,将表单以及提示内容和 Vue 的数据模型进行绑定。其次,在表单元素上分别添加事件:输入框添加 blur 失去焦点事件;表单上添加 submit 提交事件,并且在 Vue 中创建函数进行数据的正则验证。修改后的 regist.html 文件示例代码如下。
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>尚硅谷会员注册页面</title>
<link type="text/css" rel="stylesheet" th:href="@{/static/css/style.css}" />
<link rel="stylesheet" th:href="@{/static/css/register.css}" />
<style type="text/css">
.login_form {
height: 420px;
margin-top: 25px;
}
</style>
<script th:src="@{/static/script/vue.js}"></script>
</head>
<body>
<div id="box">
<div id="login_header">
<a th:href="@{/index.html}">
<img class="logo_img" alt="" th:src="@{/static/img/logo.gif}" />
</a>
</div>
<div class="login_banner">
<div class="register_form">
<h1>注册尚硅谷会员</h1>
<form action="regist_success.html" @submit="checkRegist">
<div class="form-item">
<div>
<label>用户名称:</label>
<input type="text" placeholder="请输入用户名" name="username"@blur="checkUsername"
v-model="username"/>
</div>
<span class="errMess">{{usernameMsg}}</span>
</div>
<div class="form-item">
<div>
<label>用户密码:</label>
<input type="password" placeholder="请输入密码" name="password" @blur="checkPassword"
v-model="password"/>
</div>
<span class="errMess">{{passwordMsg}}</span>
</div>
<div class="form-item">
<div>
<label>确认密码:</label>
<input type="password" placeholder="请输入确认密码" @blur="checkConfirmPassword"
v-model="confirmPassword"/>
</div>
<span class="errMess">{{confirmPasswordMsg}}</span>
</div>
<div class="form-item">
<div>
</body>
</html>
<label>用户邮箱:</label>
<input type="text" placeholder="请输入邮箱" name="email" @blur="checkEmail"
v-model="email"/>
</div>
<span class="errMess">{{emailMsg}}</span>
</div>
<div class="form-item">
<div>
<label>验证码:</label>
<div class="verify">
<input type="text" placeholder="" name="code".v-model="code" @blur="checkCode"/>
<img :src="codeSrc" alt="" height="40px" width="100px" @click="changeCode"/>
</div>
</div>
<span class="errMess">{{codeMsg}}</span>
</div>
<button class="btn">注册</button>
</form>
</div>
</div>
<div id="bottom">
<span>
尚硅谷书城.Copyright ©
</span>
</div>
</div>
<script>
new Vue(
{
el:"#box",
data: {
username:H",
usernameMsg: "用户名应为6~16位数组和字母组成",
password:H",
passwordMsg: "密码的长度至少为8位",
confirmPassword:H",
confirmPasswordMsg: "密码两次输入不一致",
email:H",
emailMsg: "请输入正确的邮箱格式",
code:H",
codeMsg: "请输入验证码",
codeSrc: "kaptcha",
usernameFlag: false,
passwordFlag: false,
confirmPasswordFlag: false,
emailFlag: false,
codeFlag: false,
},
methods: {
changeCode:function () {
//time=new Date()是为了每次的请求路径都不一样,避免浏览器缓存
this.codeSrc="kaptcha?time="+new Date();
},
checkUsername:function () {
//校验用户名是否符合要求
}
}
var usernameReg=/^[a-zA-Z0-9]{6,16}$/;
if(usernameReg.test(this.username)){
this.usernameMsg="√";
this.usernameFlag=true;
}else{
this.usernameMsg="用户名应为6~16位数组和字母组成"
this.usernameFlag=false;
},
checkPassword:function() {
var passwordReg=/^[a-zA-Z0-9_]{8,}$/
if(passwordReg.test(this.password)){
this.passwordMsg="√"
this.passwordFlag=true;
}else{
this.passwordMsg="密码的长度至少为8位"
this.passwordFlag=false;
}
},
checkConfirmPassword:function() {
if(this.password==this.confirmPassword){
this.confirmPasswordMsg="√"
this.confirmPasswordFlag=true;
}else{
this.confirmPasswordMsg="密码两次输入不一致"
this.confirmPasswordFlag=false;
}
},
checkEmail:function() {
var emailReg=/^[a-zA-Z0-9_\.-]+@([a-zA-Z0-9-]+[\.]{1})+[a-zA-Z]+\$/;
if(emailReg.test(this.email)){
this.emailMsg="√"
this.emailFlag=true;
}else{
this.emailMsg="请输入正确的邮箱格式"
this.emailFlag=false;
}
},
checkCode:function() {
if(this.code=='') {
this.codeMsg='验证码不能为空';
this.codeFlag=true;
}else{
this.codeMsg='';
this.codeFlag=false;
}
},
checkRegist:function() {
if(!(this.usernameFlag&&this.passwordFlag&&this.confirmPasswordFlag&&this.emailFlag() && this.codeFlag)) {
event.preventDefault()
}
}
5. 用户名唯一性验证¶
关于用户名需要一个特殊的验证,即要求用户注册时使用的用户名是唯一的。在注册之前,需要根据用户输入的用户名去数据库查询,如果能查询到结果,就说明该用户名已被其他用户注册,并且不能重复使用;如果查询不到结果,就说明该用户名可用。这里,我们采用 AJAX 异步请求进行用户名唯一性的验证。具体步骤如下。
(1)在 regist.html 文件中引入 Axios 框架的支持文件。
(2)修改 checkUsername 函数,在用户名格式验证成功后,发送异步请求到服务器端。
checkUsername:function () {
//校验用户名是否符合要求
var usernameReg=/^[a-zA-Z0-9]{6,16}$/;
if(usernameReg.test(this.username)) {
//当用户名格式已经正确,就可以发送异步请求到后台,进行用户名重复的验证
axios({
method:"post",
url:"checkUsername",
params:{
username:this.username
}
}).then(response => {
var result=response.data; //{flag:true/false}
if(result.flag) {
this.usernameMsg="√"
this.usernameFlag=true;
}else{
this.usernameMsg="用户名重复,请更换!"
this.usernameFlag=false;
}
});
}else{
this.usernameMsg="用户名应为6~16位数组和字母组成"
this.usernameFlag=false;
}
}
(3)创建 CheckUsernameServlet 类,继承 HttpServlet,进行用户名验证的请求处理,示例代码如下。
package com.atguigu.servlet.model;
//省略 import 语句
@Servlet("/checkUsername")
public class CheckUsernameServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doPost(request, response);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//1. 获取请求参数
String username = request.getParameter("username");
//2. 调用业务层处理业务
UserService userService=new UserServiceImpl();
User user = userService.checkUsername(username);
//3. 给响应
PrintWriter writer = response.getWriter();
CommonResult commonResult=null;
if(user==null){
commonResult=CommonResult.ok();
}else{
commonResult=CommonResult.error();
}
//将 CommonResult 写回去
String s = new Gson().toJson(commonResult);
System.out.println("s = " + s);///{flag:true/false}
writer.write(s);
}
(4)完成业务层和持久层相应操作。
业务层创建 UserService 接口,添加验证用户名是否重复的方法,示例代码如下。
package com.atguigu.service;
import com.atguigu.bean.User;
public interface UserService {
//验证用户名是否重复
User checkUsername(String username);
}
业务层创建 UserServiceImpl 类,实现 UserService 接口,示例代码如下。
package com.atguigu.service.impl;
//省略 import 语句
public class UserServiceImpl implements UserService {
privateilikDaoNASDAQ = new UserDaoImpl();
@Override
public User checkUsername(String username) {
return userDao.findUserByUsername(username);
}
}
持久层创建 UserDao 接口,示例代码如下。
package com.atguigu.dao;
import com.atguigu.bean.User;
import java.sql.SQLException;
public interface UserDao {
User findUserByUsername(String username);
}
持久层创建 UserDaoImpl 类,实现 UserDao 接口,示例代码如下。
public class UserDaoImpl implements UserDao {
private QueryRunner runner=new QueryRunner();
@Override
public User findUserByUsername(String username) {
try {
Connection connection = JDBCTools.getConnection();
String sql="select * from t_users where username=?";
return runner.query(connection,sql,new BeanHandler<User>(User.class),username);
}catch (SQLException e) {
e.printStackTrace();
}finally {
try {
JDBCTools.freeConnection();
}catch (SQLException e) {
throw new RuntimeException(e);
}
}
return null;
}
}
6. 单击注册按钮进行数据提交¶
前面已经对数据的格式进行了验证,也对用户名的唯一性进行了验证,接下来单击 “注册” 按钮进行表单的提交,将数据保存到数据库中完成注册功能。
(1)创建 RegistServlet 类,处理表单提交请求,注册成功跳转到注册成功页面,注册失败跳转到原页面,示例代码如下。
package com.atguigu.servlet.model;
//省略 import 语句
@WebServlet("/regist")
public class RegistServlet extends++,ViewBaseServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws,%";
ServletException, IOException {
this doPost(request, response);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws,%";
ServletException, IOException {
ária
ária = newáriaImpl();
//1. 获取请求参数
//1.1 获取用户的信息数据
Map<String, String[]> parameterMap = request.getParameterMap();
User user=new User();
try {
BeanUtils populate(user, parameterMap);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
//1.2 获取验证码数据
String code = request.getParameter("code");
System.out.println("用户输入验证码:"+code);
//从 session 中根据 KAPTCHA_SESSION_KEY 获取正确验证码
Object kaptcha_session_key = request.getSession().getAttribute("KAPTCHA_SESSION_KEY");
System.out.println("正确验证码:" + kaptcha_session_key);
boolean regist=false;
if (code.equals(kaptcha_session_key)) {
//验证码正确
//2. 调用业务层处理业务
regist = userService.regist(user);
}
//3. 给响应
if (regist) {
processTemplate("user/regist_success",request,response);
} else {
request.setAttribute("codeErrMsg","验证码错误");
processTemplate("user/regist",request,response);
}
}
(2)将表单请求路径设置为 RegistServlet 的请求路径。
(3)完成业务层和持久层的相应操作。
在 UserService 接口中,添加将用户信息保存到数据库的方法,示例代码如下。
在 UserServiceImpl 实现类中,实现 UserService 接口的方法,示例代码如下。
@Override
public boolean regist(User user) {
//1. 对密码进行加密(待实现)
user.setPassword(MD5Util.encode(user.getPassword()));
//2. 新增
return userDao.saveUser(user);
}
在 UserDao 接口中,添加向数据库新增用户的方法,示例代码如下。
在 UserDaoImpl 实现类中,实现 UserDao 接口的方法,示例代码如下。
@Override
public boolean saveUser(User user) {
try {
Connection connection = JDBCTools.getConnection();
String sql="insert into t_users values(null,?,?,?)";
int update = runner.update(connection, sql, user.getUsername(), user.getPassword(), user.getEmail());
if (update > 0)
return true;
} catch (SQLException e) {
e.printStackTrace();
}
finally {
try {
JDBCTools.freeConnection();
} catch (SQLException e) {
throw new RuntimeException(e);
}
return false;
如果登录失败,就跳转到 regist.html 页面,需要实现数据回显及错误信息提示,在 Vue 的 data 中增加内容,示例代码如下。
data: {
username:"[ [\${param.username}]]",
usernameMsg:"[ [\${param.username==null?'用户名应为6~16位数组和字母组成':'√'}]]",
password:"[ [\${param.password}] ],
passwordMsg:"[ [\${param.password==null?'密码的长度至少为8位':'√'}]]",
confirmPassword:"[ [\${param.password}]]",
confirmPasswordMsg:"[ [\${param.password==null?'密码两次输入不一致':'√'}]]",
email:"[ [\${param.email}]]",
emailMsg:"[ [\${param.email==null?'请输入正确格式的邮箱':'√'}]]",
code:"[ [\${param.code}]]",
codeMsg:"[ [\${codeErrMsg==null?'请输入验证码':codeErrMsg]}]]",
codeSrc:"kaptcha",
usernameFlag:"[ [\${param.username==null?"+false+":"+true+"}]]",
passwordFlag:"[ [\${param.password==null?"+false+":"+true+"}]]",
confirmPasswordFlag:"[ [\${param.password==null?"+false+":"+true+"}]]",
emailFlag:"[ [\${param.email==null?"+false+":"+true+"}]]",
codeFlag:"[ [\${param.code==null?"+false+":"+true+"}]]"
}
下面对注册具体功能进行测试,如图 14-15 所示,填写注册信息。然后单击 “注册”,如图 14-16 所示,跳转至注册成功页面。


14.3.2 Servlet 的优化¶
至此,该项目的注册功能已经完成。不难发现,针对简单的注册功能已经创建了很多 Servlet,也就是说,后期在实现其他功能时,还会创建更多的 Servlet,因此我们需要对 Servlet 进行优化。保证让相同类型的请求都发送到同一个 Servlet 上,这样就大大减少了 Servlet 的个数。
首先,需要满足一个条件,即将请求路径进行规范化。回顾一下之前请求 Servlet 的路径:去注册页面的请求路径是 /regist.html;检查用户名唯一性的请求路径是 /checkUsername;注册的请求路径是 /regist。这里可以将三个 Servlet 合并成一个 UserServlet,统一处理和用户相关的请求,后面登录的相关请求也发送到 UserServlet。
假设 UserServlet 的访问路径是 /user,那么问题就是如何让一个 Servlet 处理多个不同的请求。解决方案就是,不同的请求都携带一个 key 值相同但 value 值不同的请求参数。例如,user?method=regist.html、user?method=checkUsername、user?method=regist,这样 Servlet 就可以获取 method 的请求参数值,根据 value 值的不同来调用不同的方法,进行不同请求的处理。下面对注册涉及的 Servlet 进行优化。
(1)修改请求路径携带 method 参数。
修改 index.html 页面中注册对应的超链接,示例代码如下。
修改 regist.html 页面中检查用户名唯一性的请求路径,示例代码如下。
axios({
method:"post",
url:"user?method=checkUsername",
params: {
username:this.username
}
}).then(response => {
var result=response.data; // {flag:true/false}
if(result.flag) {
this.usernameMsg="√"
this.usernameFlag=true;
}else{
this.usernameMsg="用户名重复,请更换!"
this.usernameFlag=false;
}
});
修改 regist.html 页面中表单的请求路径,示例代码如下。
<form th:action="@{/user?method=regist}" @submit="checkRegist" method="post">
<!-- 省略表单项代码 -->
</form>
(2)创建 UserServlet,将 ToRegistPageServlet、RegistServlet、CheckUsernameServlet 三个合并到 UserServlet,示例代码如下。
package com.atguigu.servlet.model;
//省略 import 语句
@Servlet("/user")
public class UserServlet extends++,ViewBaseServlet {
private UserService userService=newáriaImpl();
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this doPost(request, response);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//获取 method 请求参数
String method = request.getParameter("method");
//进行判断
if(method.equals("regist.html")) {
toRegistPage(request, response);
}else if(method.equals("checkUsername")) {
checkUsername(request, response);
} else if(method.equals("regist")){
regist(request, response);
}
protected void toRegistPage(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.processTemplate("user/regist", request, response);
}
protected void checkUsername(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//1. 获取请求参数
String username = request.getParameter("username");
//2. 调用业务层处理业务
User user = userService.checkUsername(username);
//3. 给响应
PrintWriter writer = response.getWriter();
CommonResult commonResult=null;
if(user==null){
commonResult=CommonResult.ok();
}else{
commonResult=CommonResult.error();
}
//将 CommonResult 写回去
String s = new Gson().toJson(commonResult);
System.out.println("s = " + s); // {flag:true/false}
writer.write(s);
}
protected void regist(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//1. 获取请求参数
//1.1 获取用户的信息数据
Map<String, String[]> parameterMap = request.getParameterMap();
User user=new User();
try {
BeanUtils populate(user, parameterMap);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
//1.2 获取验证码数据
String code = request.getParameter("code");
System.out.println("用户输入验证码:"+code);
//从 session 中根据 KAPTCHA_SESSION_KEY 获取正确的验证码
Object kaptcha_session_key = request.getSession().getAttribute("KAPTCHA_SESSION_KEY");
System.out.println("正确验证码:" + kaptcha_session_key);
boolean regist=false;
if(code.equals(kaptcha_session_key)){
//验证码正确
//2. 调用业务层处理业务
regist = userService.regist(user);
}
//3. 给响应
if(regist){
processTemplate("user/regist_success", request, response);
} else{
request.setAttribute("codeErrMsg", "验证码错误");
processTemplate("user/regist", request, response);
}
}
本节完成了 Servlet 优化,大大减少了 Servlet 文件的个数,注册相关的 ToRegistPageServlet、RegistServlet、CheckUsernameServlet 就可以删除了。
14.3.3 用户登录¶
用户注册完后就会进行登录,接下来我们将实现登录功能,具体如下。
1. 单击 “登录” 按钮跳转登录页面¶
(1)修改 index.html 中的 “登录” 超链接,访问路径为 “user?method=login.html”,示例代码如下。<a th:href="@{/user?method=login.html}" class="login"> 登录 </a>
(2)修改 UserServlet,新增一个分支(加粗部分),示例代码如下。
if(method.equals("regist.html")){
toRegistPage(request, response);
} else if(method.equals("checkUsername")){
checkUsername(request, response);
} else if(method.equals("regist")){
regist(request, response);
} else if(method.equals("login.html")){
toLoginPage(request, response);
}
在 UserServlet 类中,新增 toLoginPage () 方法实现跳转登录页面,示例代码如下。
protected void toLoginPage(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.processTemplate("user/login", request, response);
}
(3)修改 login.html 页面,示例代码如下。
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>尚硅谷会员登录页面</title>
<link type="text/css" rel="stylesheet" th:href="@{/static/css/style.css}" />
</head>
<body>
<div id="login_header">
<a th:href="@{/index.html}">
<img class="logo_img" alt="" th:src="@{/static/img/logo.gif}" />
</a>
</div>
<div class="login_banner">
<div id="l_content">
<span class="login_word">欢迎登录</span>
</div>
</body>
</html>
</div>
<div id="content">
<div class="login_form">
<div class="login_box">
<div class="tit">
<h1>尚硅谷会员</h1>
</div>
<div class="msg_cont">
<b></b>
<span class="errorMsg">请输入用户名和密码</span>
</div>
<div class="form">
<form action="login_success.html">
<label>用户名称:</label>
<input
class="itxt"
type="text"
placeholder="请输入用户名"
autocomplete="off"
tabindex="1"
name="username"
id="username"
/>
<br />
<br />
<label>用户密码:</label>
<input
class="itxt"
type="password"
placeholder="请输入密码"
autocomplete="off"
tabindex="1"
name="password"
id="password"
/>
<br />
<br />
<input type="submit" value="登录" id="sub_btn" />
</form>
<div class="tit">
<a th:href="@{/user?method=regist.html}">立即注册</a>
</div>
</div>
</div>
</div>
</div>
<div id="bottom">
<span>
尚硅谷书城.Copyright ©2015
</span>
</div>
</body>
</html>
重启项目进入首页,单击 “登录” 按钮,如图 14-17 所示,成功跳转登录页面。

2. 对登录页面表单进行非空校验¶
<div id="box">
<!-- 省略 body 标签中所有内容 -->
</div>
<script>
new Vue({
el: "#box",
data: {
username: "",
password: "",
errMsg: "请输入用户名和密码"
},
methods: {
checkLogin: function () {
//1. 验证用户名是否为空
if (this.username=="") {
//2. 需要在上方进行提示
this.errMsg="用户名不能为空"
event.preventDefault();
return;
}
if (this.password=="") {
this.errMsg="密码不能为空"
event.preventDefault();
}
}
}
})
</script>
(3)在表单上绑定提交事件,修改之前 <body> 标签内的代码,具体如下。
<div id="box">
<div id="login_header">
<a th:href="@{/index.html}">
<img class="logo_img" alt="" th:src="@{/static/img/logo.gif}" />
</a>
</div>
<div class="login_banner">
<div id="1_content">
<span class="login_word">欢迎登录</span>
</div>
<div id="content">
<div class="login_form">
<div class="login_box">
<div class="tit">
<h1>尚硅谷会员</h1>
</div>
<div class="msg_cont">
<b></b>
<span class="errorMsg">{{errMsg}}</span>
</div>
<div class="form">
<form th:action="@{/user?flag=login}" @submit="checkLogin" method="post">
<label>用户名称:</label>
<input
class="itxt"
type="text"
placeholder="请输入用户名"
autocomplete="off"
tabindex="1"
name="username"
id="username"
v-model="username"
/>
<br />
<br />
<label>用户密码:</label>
<input
class="itxt"
type="password"
placeholder="请输入密码"
autocomplete="off"
tabindex="1"
name="password"
id="password"
v-model="password"
/>
<br />
<br />
<input type="submit" value="登录" id="sub_btn" />
</form>
<div class="tit">
<a th:href="@{/user?method=regist.html}">立即注册</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="bottom">
<span>
尚硅谷书城.Copyright ©2015
</span>
</div>
</div>
3. 实现登录功能¶
(1)修改登录表单的 action 属性值,示例代码如下。
(2)修改 UserServlet,新增处理登录请求的代码。
在 UserServlet 的 doPost () 方法中新增一个分支(加粗部分),代码如下。
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//获取 method 请求参数
String method = request.getParameter("method");
//进行判断
if (method.equals("regist.html")){
toRegistPage(request, response);
} else if (method.equals("checkUsername")){
checkUsername(request, response);
} else if (method.equals("regist")){
regist(request, response);
} else if (method.equals("login.html")){
toLoginPage(request, response);
} else if (method.equals("login")){
login(request, response);
}
}
在 UserServlet 中新增 login () 方法,处理登录请求,示例代码如下。
protected void login (HttpServletResponse request, HttpServletResponse response) throws ServletException, IOException {
String username = request.getParameter("username");
String password = request.getParameter("password");
// 调用业务层进行登录功能操作
User user = userService.login(username, password);
// 根据 user 是否有值判断登录是否成功
if (user != null) {
request.getSession().setAttribute("user", user);
processTemplate("user/login_success", request, response);
} else {
request.setAttribute("errMsg", "用户名或密码错误");
processTemplate("user/login", request, response);
}
}
(3)完善业务层代码。另外,对于持久层,登录可以使用 UserDao 和 UserDaoImpl 中的 findUserByUsername (String username) 方法查询用户信息。
在 UserService 接口中新增 login () 登录方法,示例代码如下。
User login(String username, String password);
在 UserServiceImpl 实现类中实现 login () 方法,示例代码如下。
@Override
public User login(String username, String password) {
//1. 使用用户名去查询
User user = userDao.findUserByUsername(username);
//2. 做密码的校验(如果注册的时候有加密,需要处理)
if (user != null) {
String encode = MD5Util.encode(password);
if (user.getPassword().equalstag encode)) {
return user;
}
}
return null;
}
(4)完善登录和登录成功页面。
修改 login.html 页面中 Vue 代码的 data 内容,示例代码如下。
data: {
username:"[[\${param.username}]]",
password:"[[\${param.password}]]",
errMsg:"[[\${errMsg==null?'请输入用户名和密码':errMsg}]]"
}
完善 login_success.html 登录成功页面,示例代码如下。
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>尚硅谷会员注册页面</title>
<link type="text/css" rel="stylesheet" th:href="@{/static/css/style.css}" />
<style type="text/css">
h1 {
text-align: center;
margin-top: 200px;
}
h1 a {
color:red;
}
</style>
</head>
<body>
<div id="header">
<a th:href="@{index.html}">
<img class="logo_img" alt="" th:src="@{static/img/logo.gif}" />
</div>
<span>欢迎<span class="um_span" th:text="${session.user.username}">张总</span>
光临尚硅谷书城</span>
<a href="../order/order.html">我的订单</a>
<a th:href="@{index.html}">注销</a>
<a th:href="@{index.html}">返回</a>
</div>
</div>
<div id="main">
<h1>欢迎回来 <a th:href="@{index.html}">转到主页</a></h1>
</div>
<div id="bottom">
<span>
尚硅谷书城.Copyright ©2015
</span>
</div>
</body>
</html>
运行代码,输入已注册的用户信息进行登录,如图 14-18 所示,成功跳转登录成功页面。

4. index.html 页面的头信息完善¶
登录成功后,修改首页展示用户信息。首页的头信息根据 session 中是否有 user 对象判断是否处于登录状态,从而显示不同的内容,如果不处于登录状态就显示 “登录” 和 “注册” 的超链接,如果处于登录状态就显示 “欢迎:XXX” 和 “注销” 的超链接。
<div class="topbar-right" th:if="${session.user==null}">
<a th:href="@{/user?method=login.html}" class="login">登录</a>
<a th:href="@{/user?method=regist.html}" class="register">注册</a>
<a
href="./pages/cart/cart.html"
class="cart iconfont icon-gouwuche">
购物车
<div class="cart-num">3</div>
</a>
<a href="./pages/manager/book_manager.html" class="admin">后台管理</a>
</div>
<!--登录后风格-->
<div class="topbar-right" th:if="${session.user!=null}">
<span>欢迎你<b th:text="${session.user.username}">张总</b></span>
<a href="#" class="register">注销</a>
<a href="pages/cart/cart.jsp" class="cart iconfont icon-gouwuche">
购物车
<div class="cart-num">3</div>
</a>
<a href="./pages/manager/book_manager.html" class="admin">后台管理</a>
</div>
运行代码,重新登录用户信息,查看首页,如图 14-19 所示。

14.3.4 用户注销¶
登录完成后,首页会出现注销的超链接。注销功能,就是单击 “注销” 按钮后发送请求至后台 Servlet,然后实现将 session 中的 User 对象移除即可。
(1)修改首页的注销超链接,示例代码如下。
(2)修改 UserServlet,新增注销的相关代码。
在 UserServlet 的 doPost () 方法中新增一个分支(加粗部分),示例代码如下。
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//获取 method 请求参数
String method = request.getParameter("method");
//进行判断
if (method.equals("regist.html")){
toRegistPage(request, response);
}else if (method.equals("checkUsername")){
checkUsername(request, response);
}else if (method.equals("regist")){
regist(request, response);
}else if (method.equals("login.html")){
toLoginPage(request, response);
}else if (method.equals("login")){
login(request, response);
}else if (method.equals("logout")){
logout(request, response);
}
}
在 UserServlet 中新增 logout () 方法,示例代码如下。
protected void logout (HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.getSession().removeAttribute("user");
response.sendRedirect(request.getContextPath() + "/index.html");
}
运行代码,登录成功后,单击首页中 “注销” 按钮后成功注销。
14.4 后台管理模块¶
后台管理模块主要是对图书的管理,包括图书的查看、图书的添加、图书的修改、图书的删除操作,在首页有一个 “后台管理” 超链接,本项目内并没有涉及权限相关功能,因此该超链接是可以直接单击的,侧重点主要放在功能实现、知识点练习上。
14.4.1 图书列表展示¶
单击首页的 “后台管理” 超链接,将数据库 t_books 表中的图书数据显示在图书列表页面,暂时不考虑分页功能。
(1)创建 Book 实体类,示例代码如下。
public class Book {
private Integer bookId;
private String bookName;
private String author;
private Double price;
private Integer sales;
private Integer stock;
private String imgPath;
// get, set 代码省略
}
(2)修改 index.html 首页中的 “后台管理” 超链接,示例代码如下。
<a th:href="@{/book?method=findAll}" class="admin"> 后台管理 </a>
需要注意的是,登录前和登录后对应的 <div> 标签中都有 “后台管理” 超链接,这两处需要同步修改。
(3)创建 BookServlet 类,并创建 findAll () 方法处理查询图书的请求,示例代码如下。
package com.atguigu.servlet.model;
//省略 import 语句
@Servlet("/book")
public class BookServlet extends++,viewBaseServlet {
private BookService bookService=new BookServiceImpl();
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this doPost(request, response);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String method = request.getParameter("method");
if(method.equals("findAll")) {
findAll(request, response);
}
}
protected void findAll(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
List<Book> all = bookService.findAll();
request.setAttribute("books",all);
}
(4)完善业务层和持久层代码。
创建 BookService 接口,增添 findAll () 方法,示例代码如下。
创建 BookServiceImpl 实现类,实现 BookService 接口的 findAll () 方法,示例代码如下。
public class BookServiceImpl implements BookService {
private BookDao bookDao = new BookDaoImpl();
@Override
public List<Book> findAll() {
return bookDao.findAll();
}
}
创建 BookDao 接口,增添 findAll () 方法,示例代码如下。
创建 BookDaoImpl 实现类,实现 BookDao 接口的 findAll () 方法,示例代码如下。
public class BookDaoImpl implements BookDao {
//由dbutils的QueryRunner,变成JdbcTemplate
private QueryRunner runner=new QueryRunner();
@Override
public List<Book> findAll() {
try {
Connection connection = JDBCTools.getConnection();
String sql="select id bookId, title bookName, author, price, sales, stock, img_path imgPath from t_books";
return runner.query(connection, sql, new BeanListHandler<Book>(Book.class));
} catch (SQLException e) {
e.printStackTrace();
} finally {
try {
JDBCTools.freeConnection();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
return null;
}
}
(5)修改 pages/manager/ 目录下的 book_manager.html 页面,示例代码如下。
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<link rel="stylesheet" th:href="@{/static/css/minireset.css}" />
<link rel="stylesheet" th:href="@{/static/css/common.css}" />
<link rel="stylesheet" th:href="@{/static/css/cart.css}" />
<link rel="stylesheet" th:href="@{/static/css/bookManger.css}" />
</head>
<body>
<div class="header">
<div class="w">
<div class="header-left">
<a th:href="@{/index.html}">
<img th:src="@{/static/img/logo.gif}" alt=""/></a>
<h1>图书管理系统</h1>
</div>
<div class="header-right">
<a href="#" class="order">图书管理</a>
<a th:href="@{/order_manager.html}" class="destory">订单管理</a>
<a th:href="@{/index.html}" class="gohome">返回商城</a>
</div>
</div>
</div>
<div class="list">
<div class="w">
<div class="add">
<a href="book_add.html">添加图书</a>
</div>
<table>
<thead>
<tr>
<th>图片</th>
<th>商品名称</th>
<th>价格</th>
<th>作者</th>
<th>销量</th>
<th>库存</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr th:each="book : ${books}">
<td><img th:src="${book.imgPath}" alt="" /></td>
<td th:text="${book.bookName}">活着</td>
<td th:text="${book.price}">100.00</td>
<td th:text="${book.author}">余华</td>
<td th:text="${book.sales}">200</td>
<td th:text="${book.stock}">400</td>
<td>
<a th:href="@{book(flag='toEditPage',id=${book.bookId})}">修改</a>
<a th:href="@{book(flag='delete',id=${book.bookId})}" class="del">删除</a>
</td>
</tr>
</tbody>
</body>
</table>
<div class="footer">
<div class="footer-right">
<div>首页</div>
<div>上一页</div>
<ul>
<li class="active">1</li>
<li>2</li>
<li>3</li>
</ul>
<div>下一页</div>
<div>末页</div>
<span>共10页</span>
<span>30条记录</span>
<span>到第</span>
<input type="text" />
<span>页</span>
<button>确定</button>
</div>
</div>
</div>
<div class="bottom">
<div class="w">
<div class="top">
<ul>
<li>
<a href="">
<img th:src="@{/static/img/bottom1.png}" alt="" />
<span>大咖级讲师亲自授课</span>
</a>
</li>
<li>
<a href="">
<img th:src="@{/static/img/bottom2.png}" alt="" />
<span>课程为学员成长持续赋能</span>
</a>
</li>
<li>
<a href="">
<img th:src="@{/static/img/bottom2.png}" alt="" />
<span>学员真实情况大公开</span>
</a>
</li>
</ul>
</div>
<div class="content">
<dl>
<dt>关于尚硅谷</dt>
<dd>教育理念</dd>
</dl>
<dl>
<dt>资源下载</dt>
<dd>视频下载</dd>
</dl>
<dl>
<dt>加入我们</dt>
<dd>招聘岗位</dd>
</dl>
<dl>
<dt>联系我们</dt>
<dd>http://www.atguigu.com</dd>
<dd></dd>
</dl>
</div>
</div>
<div class="down">
尚硅谷书城.Copyright ©
</div>
</div>
</body>
</html>
运行代码,单击首页的 “后台管理”,如图 14-20 所示,成功实现图书列表展示功能。

14.4.2 添加图书¶
图书列表展示完成后,接下来实现添加图书功能,暂时不考虑图片添加的功能。
(1)修改 book_manager.html 页面的 “添加图书” 超链接,示例代码如下。
(2)在 BookServlet 中创建方法进行新增页面的渲染。
在 BookServlet 的 doPost () 方法中新增一个分支(加粗部分),示例代码如下。
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String method = request.getParameter("method");
if (method.equals("forall")) {
findAll(request, response);
} else if (method.equals("book_add.html")) {
toAddPage(request, response);
} else if (method.equals("add")) {
add(request, response);
}
}
在 BookServlet 中新增 toAddPage () 方法和 add () 方法,处理图书数据的保存请求,示例代码如下。
protected void toAddPage(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
processTemplate("manager/book_add", request, response);
}
protected void add(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Map<String, String[]> parameterMap = request.getParameterMap();
Book book = new Book();
try {
BeanUtils populate(book, parameterMap);
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
bookService.insert(book);
response.sendRedirect(request.getContextPath() + "/book?method = findAll");
}
(3)修改 pages/manager/ 目录下的 book_add.html 页面,设置页面内表单的请求路径,示例代码如下。
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<link rel="stylesheet" th:href="@{/static/css/minireset.css}" />
<link rel="stylesheet" th:href="@{/static/css/common.css}" />
<link rel="stylesheet" th:href="@{/static/css/style.css}" />
<link rel="stylesheet" th:href="@{/static/css/cart.css}" />
<link rel="stylesheet" th:href="@{/static/css/bookManger.css}" />
<link rel="stylesheet" th:href="@{/static/css/register.css}" />
<link rel="stylesheet" th:href="@{/static/css/book_edit.css}" />
</head>
<body>
<div class="header">
<div class="w">
<div class="header-left">
<a th:href="@{/index.html}">
<img th:src="@{/static/img/logo.gif}" alt="" />
</div>
<h1>添加图书</h1>
</div>
<div class="header-right">
<a href="./book_manager.html" class="order">图书管理</a>
<a href="./order_manager.html" class="destory">订单管理</a>
<a th:href="@{/index.html}" class="gohome">返回商城</a>
</div>
</div>
</div>
<div class="login_banner"> <div class="register_form"> <form th:action="@{/book?method=add}" method="post">
<div class="form-item">
<div>
<label>名称:</label>
<input type="text" placeholder="请输入名称" name="bookName"/>
</div>
<span class="errMess" style="visibility: visible;"><span>
</div>
<div class="form-item">
<div>
<label>价格:</label>
<input type="number" placeholder="请输入价格" name="price"/>
</div>
<span class="errMess"></span>
</div>
<div class="form-item">
<div>
<label>作者:</label>
<input type="text" placeholder="请输入作者" name="author"/>
</div>
<span class="errMess"></span>
</div>
<div class="form-item">
<div>
<label>销量:</label>
<input type="number" placeholder="请输入销量" name="sales"/>
</div>
<span class="errMess"></span>
</div>
<div class="form-item">
<div>
<label>库存:</label>
<input type="number" placeholder="请输入库存" name="stock"/>
</div>
<span class="errMess"></span>
</div>
<button class="btn">提交</button>
</form>
</div>
<div class="bottom">
尚硅谷书城.Copyright ©2015
</div>
</body>
</html>
(4)完善业务层和持久层代码。
在 BookService 接口中,新增 insert () 添加图书方法,示例代码如下。
在 BookServiceImpl 实现类中实现 insert () 方法,示例代码如下。
在 BookDao 接口中,新增 insert () 添加图书方法,示例代码如下。
在 BookDaoImpl 实现类中实现 insert () 方法,示例代码如下。
@Override
public void insert(Book book) {
try {
Connection connection = JDBCTools.getConnection();
String sql="insert into t_books values(null,?,?,?,?,?,?)";
runner.update(connection, sql, book.getBookName(), book.getAuthor(), book.getPrice(), book.getSales(), book.getStock(), book.getImgPath());
} catch (SQLException e) {
e.printStackTrace();
} finally {
try {
JDBCTools.freeConnection();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
运行代码,单击图书管理页面的 “添加图书” 按钮并输入图书信息,如图 14-21 所示,成功添加一条记录,不过输入中文出现了乱码问题。

14.4.3 处理请求和响应中文乱码¶
在添加图书时,如果输入中文会出现乱码问题,可以创建 EncodingFilter 过滤器,处理请求和响应乱码,示例代码如下。
package com.atguigu.filter;
//省略 import 语句
@WebFilter(urlPatterns = "/*")
public class EncodingFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
servletRequest.setCharacterEncoding("utf-8");
servletResponse.setContentType("text/html;charset=utf-8");
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
}
}
再次运行代码,添加图书信息,如图 14-22 所示,输入中文,无乱码问题。

14.4.4 修改图书¶
修改图书功能是单击修改超链接,跳转至修改页面,页面内显示原始数据,让用户在原始数据的基础上进行修改。修改完成后,用户单击 “保存” 按钮,进行数据的修改。和新增功能一样,暂时不考虑图片添加的功能。具体步骤如下。
(1)完善 “修改” 超链接的请求路径,示例代码如下。
(2)在 BookServlet 中新增代码,跳转到修改页面并展示原始数据。
在 BookServlet 的 doPost () 方法中增加两个分支(加粗部分),示例代码如下。
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String method = request.getParameter("method");
if (method.equals("findAll")) {
findAll(request, response);
} else if (method.equals("book_add.html")) {
toAddPage(request, response);
} else if (method.equals("add")) {
add(request, response);
} else if (method.equals("edit.html")) {
toEditPage(request, response);
} else if (method.equals("update")) {
update(request, response);
}
}
在 BookServlet 中新增 toEditPage () 方法和 update () 方法,示例代码如下。
protected void toEditPage(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String id = request.getParameter("id");
Book book = bookService.getById(Integer.parseInt(id));
request.setAttribute("book", book);
processTemplate("manager/book_edit", request, response);
}
protected void update(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Map<String, String[]> parameterMap = request.getParameterMap();
Book book=new Book();
try {
BeanUtils populate(book, parameterMap);
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
bookService.update(book);
response.sendRedirect(request.getContextPath() + "/book?method=findAll");
}
(3)修改 pages/manager/ 目录下的 book_edit.html 页面,设置页面中表单的请求路径,示例代码如下。
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<link rel="stylesheet" th:href="@{/static/css/minireset.css}" />
<link rel="stylesheet" th:href="@{/static/css/common.css}" />
<link rel="stylesheet" th:href="@{/static/css/style.css}" />
<link rel="stylesheet" th:href="@{/static/css/cart.css}" />
<link rel="stylesheet" th:href="@{/static/css/bookManger.css}" />
<link rel="stylesheet" th:href="@{/static/css/register.css}" />
<link rel="stylesheet" th:href="@{/static/css/book_edit.css}" />
</head>
<body>
<div class="header">
<div class="w">
<div class="header-left">
<a th:href="@{/index.html}">
<img th:src="@{/static/img/logo.gif}" alt=""/></a>
<h1>编辑图书</h1>
</div>
<div class="header-right">
<a href="./book_manager.html" class="order">图书管理</a>
<a href="./order_manager.html" class="destory">订单管理</a>
<a th:href="@{/index.html}" class="gohome">返回商城</a>
</div>
</div>
</div>
<div class="login banner">
<div class="register_form">
<form th:action=" @{/book?method=update}" method="post">
<input type="hidden" name="bookId" th:value="${book.bookId}">
<div class="form-item">
<div>
<label>名称:</label>
<input type="text" placeholder="请输入名称" name="bookName" th:value="${book.bookName}"
</div>
<span class="errMess" style="visibility: visible;">请输入正确的名称</span>
</div>
<div class="form-item">
<div>
<label>价格:</label>
<input type="number" placeholder="请输入价格" name="price" th:value="${book.price}" />
</div>
<span class="errMess">请输入正确数字</span>
</div>
<div class="form-item">
<div>
<label>作者:</label>
<input type="text" placeholder="请输入作者" name="author" th:value="${book.author}" />
</div>
<span class="errMess">请输入正确作者</span>
</div>
<div class="form-item">
<div>
<label>销量:</label>
<input type="number" placeholder="请输入销量" name="sales" th:value="${book.sales}" />
</div>
<span class="errMess">请输入正确销量</span>
</div>
<div class="form-item">
<div>
<label>库存:</label>
<input type="number" placeholder="请输入库存" name="stock" th:value="${book.stock}" />
</div>
<span class="errMess">请输入正确库存</span>
</div>
<button class="btn">提交</button>
</form>
</div>
<div class="bottom">
尚硅谷书城.Copyright ©2015
</div>
</body>
</html>
(4)完善业务层和持久层代码。¶
在 BookService 接口中新增 getById () 方法和 update () 方法,示例代码如下。
在 BookServiceImpl 实现类中,实现 BookService 接口新增方法,示例代码如下。
@Override
public Book getById(Integer id) {
return bookDao.getById(id);
}
@Override
public void update(Book book) {
bookDao.update(book);
}
在 BookDao 接口中,新增 getById () 方法和 update () 方法,示例代码如下。
在 BookDaoImpl 实现类中,实现 BookDao 接口新增方法,示例代码如下。
public Book getById(Integer id) {
try {
Connection connection = JDBCTools.getConnection();
String sql="select id bookId, title bookName, author, price, sales, stock, img_path imgPath from t_books where id=?";
return runner.query(connection, sql, new BeanHandler<Book>(Book.class), id);
} catch (SQLException e) {
e.printStackTrace();
} finally {
try {
JDBCTools.freeConnection();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
return null;
}
@Override
public void update(Book book) {
try {
Connection connection = JDBCTools.getConnection();
String sql="update t_books set title=?, author=?, price=?, sales=?, stock=? where id=?”;
runner.update(connection, sql, book.getBookName(), book.getAuthor(), book.getPrice(),
book.getSales(), book.getStock(), book.getBookId());
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException();
} finally {
try {
JDBCTools.freeConnection();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
运行代码,修改前面出现中文乱码的图书,如图 14-23 所示,修改成功。

14.4.5 删除图书¶
删除图书的流程相对简单,单击 “删除” 超链接,将当前图书从数据库中移除即可。具体步骤如下。
(1)修改 “删除” 超链接,示例代码如下。
(2) BookServlet 新增处理删除功能的代码。
在 BookServlet 的 doPost () 方法中新增一个分支(加粗部分),示例代码如下。
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String method = request.getParameter("method");
if (method.equals("findAll")) {
findAll(request, response);
} else if (method.equals("book_add.html")) {
toAddPage(request, response);
} else if (method.equals("add")) {
add(request, response);
} else if (method.equals("edit.html")) {
toEditPage(request, response);
} else if (method.equals("update")) {
update(request, response);
} else if (method.equals("delete")) {
delete(request, response);
}
}
在 BookServlet 中实现 delete () 方法,示例代码如下。
protected void delete (HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String id = request.getParameter("id");
bookService.delete(Integer.parseInt(id));
response.sendRedirect(request.getContextPath() + "/book?method=findAll");
}
(3)完善业务层和持久层代码。
在 BookService 接口中新增 delete () 方法,示例代码如下。
在 BookServiceImpl 实现类中,实现 BookService 接口新增方法,示例代码如下。
在 BookDao 接口中新增 delete () 方法,示例代码如下。
在 BookDaoImpl 实现类中,实现 BookDao 接口新增方法,示例代码如下。
@Override
public void delete(Integer id) {
try {
Connection connection = JDBCTools.getConnection();
String sql="delete from t_books where id=?";
runner.update(connection, sql, id);
} catch (SQLException e) {
e.printStackTrace();
}finally {
try {
JDBCTools.freeConnection();
}catch (SQLException e) {
throw new RuntimeException(e);
}
}
运行代码,单击最后一条图书记录对应的 “删除”,发现删除成功,表示成功实现该删除功能。
14.5 前台图书展示¶
同样在 index.html 首页中,展示数据库中真实的图书信息,接下来修改相关代码实现此功能。
(1)修改 IndexServlet 中的 doPost () 方法,查询图书数据,存放在 HttpServletRequest 请求域内。
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
BookService bookService=new BookServiceImpl();
List<Book> all =bookService.findAll();
request.setAttribute("books",all);
this.processTemplate("index",request,response);
}
(2)在 index.html 页面中,找到 class="list-content" 的 <div> 标签,替换成下面的内容,将请求域中数据渲染到页面上。
<div class="list-content">
<div class="list-item" th:each="book : ${books}">
<img th:src="${book.imgPath}" alt="">
<p>书名:<span th:text="${book.bookName}">活着</span></p>
<p>作者:<span th:text="${book.author}">余华</span></p>
<p>价格:¥<span th:text="${book.price}">66.6</span></p>
<p>销量:<span th:text="${book.sales}">230</span></p>
<p>库存:<span th:text="${book.stock}">1000</span></p>
<button>加入购物车</button>
</div>
</div>
运行代码,查看首页,如图 14-24 所示,成功展示图书信息。

14.6 购物车功能¶
购物车中的数据我们选择存储在 session 中,以达到对 session 进行练习目的。购物车功能我们采用前后端分离的方式实现,也就是使用 Vue 框架发送异步请求。购物车功能包括加入购物车、查看购物车、购物车数量的修改、购物车删除商品、购物车清空,以及结账功能。
14.6.1 准备工作¶
(1)创建 CartItem 类,存储购物项数据,示例代码如下。
package com.atguigu.bean;
import java.math.BigDecimal;
/**
* 购物项
* 购物车中一种商品的具体信息
* 注意:对 setBook/setCount/进行有参构造,添加了 amount 的计算代码
*/
public class CartItem {
private Book book; // 书的信息
private Integer count; // 数量
private Double amount; // 价格 (书的单价和数量计算得到的结果)
// 计算金额 (书的单价*数量)
public void setAmount() {
if (this.book != null) {
BigDecimal b1 = new BigDecimal(this.book.getPrice()+"");
BigDecimal b2 = new BigDecimal(this.count +");
this.amount = b1.multiply(b2).doubleValue();
}
}
public CartItem(Book book, Integer count) {
this.book = book;
this.count = count;
setAmount();
}
public void setBook(Book book) {
this.book = book;
setAmount();
}
public void setCount(Integer count) {
this.count = count;
setAmount();
}
// 省略部分代码
}
(2)创建 Cart 类,存储购物车数据,示例代码如下。
public class Cart {
private Integer totalCount; // 总数量
private Double totalAmount; // 总金额
// 将书的 id 值设置 Map 集合的 key
private Map<Integer, CartItem> cartItemMap = new HashMap<>();
// 该 Map 用于存储很多个购物项
// 省略 get/set 方法
}
14.6.2 加入购物车¶
在首页单击商品下方的 “加入购物车” 按钮,发送异步请求,完成购物车的添加,具体步骤如下。
(1)修改 index.html 首页相关代码。
在首页中,引入 vue.js 和 axios.min.js 文件,示例代码如下。
<script th:src="@{/static/script/vue.js}"></script>
<script th:src="@{/static/script/axios.min.js}"></script>
在 “加入购物车” 按钮上绑定单击事件,示例代码如下。
并添加 Vue 组件,创建函数发送异步请求,示例代码如下。
<script>
new Vue({
el:"#app",
data: {
totalCount:"[{$session.cart!=null?session.cart.totalCount:0}]]", //网页刷新会导致Vue重新渲染,因此需要让totalCount的值和购物车的总数量相同
},
methods: {
addCart:function () {
//1. 获取这本书的id值(变为获取当前单击按钮的id属性值)
var id=event.target.id;
//2. 向后台发送异步请求
axios({
method:"post",
url:"cart",
params: {
method:"addCart",
id:id
}
}).then(response=>{ //response.data : {flag:true,resultData:num}
if(response.data.flag) {
alert("添加购物车成功");
this.totalCount=response.data.resultData
}
})
}
}
})
</script>
(2)创建 CartServlet 类,处理加入购物车请求,示例代码如下。
package com.atguigu.servlet.model;
//省略 import 语句
@Servlet("/cart")
public class CartServlet extends++,ViewBaseServlet {
private BookService bookService=new BookServiceImpl();
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this doPost(request, response);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String method = request.getParameter("method");
if(method.equals("addCart")) {
addCart(request, response);
}
}
protected void addCart(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//1. 获取书的 id 值
String id = request.getParameter("id");
System.out.println("id = " + id); //知道用户点的是哪本书了
//2. 调用业务层根据 id 查到书的信息(book 就是我们要添加到购物车内的书)
Book book = bookService.getElementById(Integer.parseInt(id));
//3. 判断此次加入购物车是第几次单击的(从 session 域内判断是否存在 Cart 对象)
HttpSession session = request.getSession();
Cart cart=(Cart)session.getAttribute("cart"); // key 暂时写 cart
if(cart==null){
//说明第一次单击加入购物车
cart=new Cart();
session.setAttribute("cart", cart);
}
//4. 将图书的信息,添加到购物车
cart.addCart(book);
//5. 给响应
//响应中数据除了 ok 成功状态,还需要将购物车的总数量响应回去
Integer totalCount = cart.getTotalCount();
CommonResult commonResult = CommonResult.ok().setResultData(totalCount);
String s = new Gson().toJson(commonResult);
System.out.println("s = " + s); // {flag:true, resultData:num}
response.getWriter().write(s);
}
}
(3)在 Cart 类中添加 addCart () 方法,将图书加入 Cart 对象的 Map 集合内,并编写代码计算购物车总
数量,示例代码如下。
public void addCart(Book book) {
//判断这本图书是否是第一次添加?(判断 cartItemMap 中是否有这本书)
CartItem cartItem = cartItemMap.get(book.getBookId());
if (cartItem == null) {
//说明这本图书之前没有添加过购物车
CartItem cartItem1 = new CartItem(book, 1);
cartItemMap.put(book.getBookId(), cartItem1);
} else {
//说明这本图书之前添加过购物车
cartItem.setCount(cartItem.getCount() + 1);
}
System.out.println(cartItemMap); //为了查看 Map 集合内的东西
}
public Integer getTotalCount() {
Collection<CartItem> values = getCartItems();
Integer totalCount=0;
for (CartItem value : values) {
totalCount += value.getCount();
}
this.totalCount=totalCount;
return this.totalCount;
}
(4)再次修改首页,右上角的 “购物车” 超链接上的数字显示为实际购物车数量。
注意,登录前后两个位置需要同步修改。然后运行代码,查看首页,如图 14-25 所示。

当前尚未单击 “加入购物车”,右上角显示数量为 0,然后单击 “解忧杂货店” 对应的 “加入购物车” 按钮,如图 14-26 所示,弹出提示框,表明添加成功。

单击 “确定” 按钮后,查看右上角购物车数量,由 0 变为 1,表明数量计算成功,如图 14-27 所示。

14.6.3 查看购物车¶
单击首页上 “购物车” 超链接,跳转至购物车展示页面,利用 Vue 钩子函数发送异步请求到服务器端,获取购物车的相关数据,再通过 Vue 渲染到页面上,具体步骤如下。
(1)修改首页中 “购物车” 超链接的请求路径,示例代码如下。
<a th:href="@{/cart?method=toCartPage}" class="cart iconfont icon-gouwuche">
购物车
<div class="cart-num">{{totalCount}}</div>
</a>
注意,登录前后两个位置需要同步修改。
(2)修改 CartServlet 类,编写代码处理购物车查看功能。
在 CartServlet 类的 doPost () 方法中新增以下两个分支(加粗部分),示例代码如下。
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String method = request.getParameter("method");
if (method.equals("addCart")) {
addCart(request, response);
} else if (method.equals("toCartPage")) {
toCartPage(request, response);
} else if (method.equals("showCart")) {
showCart(request, response);
}
}
在 CartServlet 中新增方法,toCartPage () 方法实现去购物车页面;showCart () 方法实现展示购物车信息。示例代码如下。
protected void toCartPage(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
processTemplate("cart/cart",request,response);
}
protected void showCart(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//1. 获取购物车所需要的数据
HttpSession session = request.getSession();
Cart cart = (Cart)session.getAttribute("cart");
responseData(cart,request,response);
}
protected void responseData(Cart cart, HttpServletResponse request, HttpServletResponse response) throws ServletException, IOException {
Integer totalCount=0;
Double totalAmount=0D;
Collection<CartItem> cartItems=new ArrayList<>();
if(cart!=null){
totalCount=cart.TotalCount();
totalAmount=cart.TotalAmount();
cartItems=cart.getCartItems();
}
//2. 将数据响应给 JavaScript
List cartAll=new ArrayList();///[totalCount,totalAmount,[{{},{},{}}...]]
cartAll.add(totalCount);
cartAll.add(totalAmount);
cartAll.add(cartItems);
CommonResult commonResult = CommonResult.ok().setResultData(cartAll);
///{flag:true, resultData:[totalCount,totalAmount, [{},{},{}}...],}]
String s = new Gson().toJson(commonResult);
response.getWriter().write(s);
}
(3)Cart 类中新增 getTotalAmount () 方法,获取购物车总金额,示例代码如下。
Collection<CartItem> values = getCartItems();
BigDecimal b1=new BigDecimal("0");
for (CartItem value : values) {
BigDecimal b2=new BigDecimal(value.getAmount()+"");
b1=b1.add(b2);
}
this.totalAmount=b1.doubleValue();
return this.totalAmount;
}
(4)修改 pages/Cart/ 目录下的 cart.html 购物车页面,示例代码如下。
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<link rel="stylesheet" th:href="@{/static/css/minireset.css}" />
<link rel="stylesheet" th:href="@{/static/css/common.css}" />
<link rel="stylesheet" th:href="@{/static/css/cart.css}" />
<script th:src="@{/static/script/vue.js}"></script>
<script th:src="@{/static/script/axios.min.js}"></script>
</head>
<body>
<div id="app">
<div class="header">
<div class="w">
<div class="header-left">
<a th:href="@{/index.html}">
<img th:src="@{/static/img/logo.gif}" alt=""/></a>
<h1>我的购物车</h1>
</div>
<div class="header-right">
<h3>欢迎<span>张总</span>光临尚硅谷书城</h3>
<div class="order"><a href="order? flag=showOrder">我的订单</a></div>
<div class="destory"><a href="index.html">注销</a></div>
<div class="gohome">
<a th:href="@{/index.html}">返回</a>
</div>
</div>
</div>
</div>
<div class="list">
<div class="w">
<table>
<thead>
<tr>
<th>图片</th>
<th>商品名称</th>
<th>数量</th>
<th>单价</th>
<th>金额</th>
<th>操作</th>
</tr>
</thead>
<tbody v-if="totalCount==0">
<tr>
<td colspan="6">
<b>购物车为空,请单击继续购物</b>
</td>
</tr>
</tbody>
<tbody v-else>
<tr v-for="cartItem in cartItemList">
<td>
<img :src="cartItem.book.imgPath" alt="" />
</td>
<td>{{cartItem.book.bookName}}</td>
<td>
<!--将图书的id值,放在input表单的id属性上-->
<span class="count" @click="subCount">-</span>
<input class="count-num" type="text" v-model="cartItem.count": id="cartItem.book.
bookId" @change="changeCount"/>
<span class="count" @click="addCount">+</span>
</td>
<td>{{cartItem.book.price}}</td>
<td>{{cartItem.amount}}</td>
<td><a href="" @click.prevent="deleteCartItem" :id="cartItem.book.bookId">删除</a></td>
</tr>
</tbody>
</table>
<div class="footer">
<div class="footer-left">
<a href="#" class="clear-cart" @click.prevent="clearCart">清空购物车</a>
<a href="#">继续购物</a>
</div>
<div class="footer-right">
<div>共<span>{{totalCount}}</span>件商品</div>
<div class="total-price">总金额<span>{{totalAmount}}</span>元</div>
<a class="pay" href="order?flag=checkout">去结账</a>
</div>
</div>
</div>
</div>
<div class="bottom">
<div class="w">
<div class="top">
<ul>
<li>
<a href="">
<img src="static/img/bottom1.png" alt="" />
<span>大咖级讲师亲自授课</span>
</a>
</li>
<li>
<a href="">
<img src="static/img/bottom2.png" alt="" />
<span>课程为学员成长持续赋能</span>
</a>
</li>
<li>
<a href="">
<img src="static/img/bottom2.png" alt="" />
<span>学员真实情况大公开</span>
</a>
</li>
</ul>
</div>
<div class="content">
<dl>
<dt>关于尚硅谷</dt>
<dd>教育理念</dd>
<!-- <dd>名师团队</dd><dd>学员心声</dd> -->
</dl>
<dl>
<dt>资源下载</dt>
<dd>视频下载</dd>
<!-- <dd>资料下载</dd><dd>工具下载</dd> -->
</dl>
<dl>
<dt>加入我们</dt>
<dd>招聘岗位</dd>
<!-- <dd>岗位介绍</dd><dd>招贤纳士</dd> -->
</dl>
<dl>
<dt>联系我们</dt>
<dd>http://www.atguigu.com</dd>
<dd></dd>
</dl>
</div>
</div>
<div class="down">
尚硅谷书城.Copyright ©2015
</div>
</div>
</div>
<script>
new Vue({
el: "#app",
data: {
totalCount: "0",
totalAmount: "0",
cartItemList: []
},
methods: {},
created:function () {
//在Vue对象创建完后去发送异步请求,获取到购物车的数据,赋值给数据模型
axios({
method: "post",
url: "cart",
params: {
method: "showCart"
}
}).then(
response => {
var result = response.data;
if (result.flag) {
//将数据复制给数据模型
this.totalCount = result.resultData[0];
this.totalAmount = result.resultData[1];
this.cartItemList = result.resultData[2];
}
}
}
}
}
}
</script>
</body>
</html>
运行代码,单击首页的 “购物车” 超链接,成功跳转购物车页面。如图 14-28 所示,由于尚未添加图
书到购物车,所以数据为空。然后回到首页,添加几本图书到购物车,如图 14-29 所示。


从图 14-29 中可知,添加购物车成功,并成功展示图书信息。
14.6.4 购物车中数量的加减和修改¶
在购物车页面,我们可以对购物项的数量进行增加或减少,也可以直接修改数量。实现思路是在加号 / 减号上绑定 click 事件,在数量的文本框上绑定 change 事件,然后创建对应的函数发送异步请求到服务器端进行数量的改变。具体步骤如下。
(1)修改 cart.html 页面代码。
在该页面,图书所对应的加号 / 减号,以及数量的文本框上绑定事件,示例代码如下。
<td>
<!--将图书的 id 值,放在了 input 表单的 id 属性上-->
<span class="count" @click="subCount">-</span>
<input class="count-num" type="text" v-model="cartItem.count":id="cartItem.book.bookId"
@change="changeCount"/>
<span class="count" @click="addCount">+</span>
</td>
修改 Vue 代码,在 methods 中创建如下函数,发送异步请求到服务器端,示例代码如下。
addCount:function () {
//获得到图书的id值
var id=event.target.previousElementSibling.id;
axios({
method:"post",
url:"cart",
params({
method:"addCount",
id:id
}
}).then(response=>{
var result=response.data
if(result.flag){
//将数据复制给数据模型
this.totalCount=result.resultData[0];
this.totalAmount=result.resultData[1];
this.cartItemList=result.resultData[2];
}
})
subCount:function () {
var id=event.target.nextElementSibling.id;
axios({
method:"post",
url:"cart",
params({
method:"subCount",
id:id
}
}).then(response=>{
var result=response.data
if(result.flag){
//将数据复制给数据模型
this.totalCount=result.resultData[0];
this.totalAmount=result.resultData[1];
this.cartItemList=result.resultData[2];
}
})
},
changeCount:function () {
var id=event.target.id;
//count可以做一个正则验证,非数字提示非法字符,不进行异步请求的发送
var count=event.target.value;
axios({
method:"post",
url:"cart",
params({
method:"changeCount",
id:id,
count:count
}
}).then(response=>{
var result=response.data
if(result.flag){
//将数据复制给数据模型
this.totalCount=result.resultData[0];
this.totalAmount=result.resultData[1];
this.cartItemList=result.resultData[2];
}
})
}
(2)修改 CartServlet 类,创建方法处理请求。
在 CartServlet 的 doPost () 方法中新增三个分支(加粗部分),示例代码如下。
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String method = request.getParameter("method");
if (method.equals("addCart")) {
addCart(request, response);
} else if (method.equals("toCartPage")) {
toCartPage(request, response);
} else if (method.equals("showCart")) {
showCart(request, response);
} else if (method.equals("addCount")) {
addCount(request, response);
} else if (method.equals("subCount")) {
subCount(request, response);
} else if (method.equals("changeCount")) {
changeCount(request, response);
}
}
在 CartServlet 中新增方法,addCount () 实现增加购物车数量;subCount () 实现减少购物车数量;changeCount () 实现修改购物车数量,示例代码如下。
protected void addCount ( requests, requests, requests) throws ServletException, IOException {
String id = request.getParameter("id");
HttpSession session = request.getSession();
Cart cart = (Cart session.getAttribute("cart");
cart.addCount(Integer.parseInt(id));
responseData(cart, request, response);
}
protected void subCount ( requests, requests, requests) throws ServletException, IOException {
String id = request.getParameter("id");
HttpSession session = request.getSession();
Cart cart = (Cart session.getAttribute("cart");
cart.subCount(Integer.parseInt(
id)
);
responseData(cart, request, response);
}
protected void changeCount ( requests, requests, requests) throws ServletException, IOException {
String id = request.getParameter("id");
String count = request.getParameter("count");
HttpSession session = request.getSession();
Cart cart = (Cart session.getAttribute("cart");
cart.changeCount(Integer.parseInt(id), Integer.parseInt(count));
responseData(cart, request, response);
}
(3)在 Cart 类中同样新增 addCount ()、subCount ()、changeCount () 方法,示例代码如下。
public void addCount(Integer id) {
CartItem cartItem = cartItemMap.get(id);
cartItem.setCount(cartItem.getCount()+1);
}
public void subCount(Integer id) {
CartItem cartItem = cartItemMap.get(id);
if (cartItem.getCount() == 1) {
cartItemMap.remove(id);
} else {
cartItem.setCount(cartItem.getCount() - 1);
}
}
public void changeCount(Integer id, Integer count) {
CartItem cartItem = cartItemMap.get(id);
cartItem.setCount(count);
}
运行代码,如图 14-30 所示,测试购物车中图书数量的加减和修改,发现总数量和总金额随之改变,表明修改成功。

14.6.5 删除购物项和清空购物车¶
在购物车页面可以单击删除,将当前购物项从购物车中删除,单击清空购物车,将购物车对象直接从 session 中删除即可。具体步骤如下。
(1)修改 cart.html 文件相关代码。
在 “删除” 和 “清空购物车” 超链接上绑定单击事件,示例代码如下。
<td>
<a href="" @click.prevent="deleteCartItem" :id="cartItem.book.bookId">删除</a>
</td>
<a href="#" class="clear-cart" @click.prevent="clearCart">清空购物车</a>
修改 Vue 代码,创建对应的函数,发送异步请求到 CartServlet,示例代码如下。
clearCart: function () {
// 发送请求到服务器端,将购物车数据清空
axios({
method: "post",
url: "cart",
params: {
method: "clearCart"
}
}). then (response => {
if (response.data.flag) {
this.totalCount = 0;
this.totalAmount = 0;
}
}).
this.cartItemList = [];
}
})
},
delete react
deleteCartItem: function () {
var id = event.target.id; // 获取到购物项图书的 id 值 (删除购物项的条件)
axios({
method: "post",
url: "cart",
params: {
method: "deleteCartItem",
id: id
}
}). then (response => {
var result = response.data
if (result.flag) {
// 将数据复制给数据模型
this.totalCount = result.resultData[0];
this.totalAmount = result.resultData[1];
this.cartItemList = result.resultData[2];
}
})
},
(2)在 CartServlet 中新增代码处理删除和清空购物车请求。
在 CartServlet 的 doPost () 方法中新增两个分支(加粗部分),示例代码如下。
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String method = request.getParameter("method");
if (method.equals("addCart")) {
addCart(request, response);
} else if (method.equals("toCartPage")) {
toCartPage(request, response);
} else if (method.equals("showCart")) {
showCart(request, response);
} else if (method.equals("addCount")) {
addCount(request, response);
} else if (method.equals("subCount")) {
subCount(request, response);
} else if (method.equals("changeCount")) {
changeCount(request, response);
} else if (method.equals("clearCart")) {
clearCart(request, response);
} else if (method.equals("deleteCardItem")) {
deleteCardItem(request, response);
}
}
并在 CartServlet 中创建 deleteCartItem () 和 clearCart () 两个方法,示例代码如下。
protected void clearCart(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.getSession().removeAttribute("cart");
CommonResult ok = CommonResult.ok();
String s = new Gson().toJson(ok);
response.getWriter().write(s);
}
protected void deleteCartItem(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
//获得到请求参数
String id = request.getParameter("id");
HttpSession session = request.getSession();
//获得到购物车对象
Cart cart = (Cart) session.getAttribute("cart");
//删除购物项
cart.deleteCartItem(Integer.parseInt(id));
if (cart.getTotalCount()==0) {
session.removeAttribute("cart");
}
//删完之后,该怎么办?
-responseData(cart, request, response);
}
(3)在 Cart 类中同样创建 deleteCartItem () 方法,实现删除购物项操作。
// 删除购物项
运行代码,单击购物车页面的 “删除” 超链接发现对应图书信息被删除,然后单击 “清空购物车” 发现购物车为空。
14.7 结账功能¶
在购物车页面,可以单击 “去结账” 超链接进行订单的创建,由于付款功能没有介绍,所以暂且跳过付款功能,这里只创建订单即可,并且为了减少前端代码,结账功能默认针对购物车中的所有商品,省略选择商品的环节。
14.7.1 准备工作¶
针对订单表 t_order 和订单详情表 t_order_item,创建对应的两个实体类。
创建 Order 订单类,示例代码如下。
public class Order {
private Integer orderId;
private String orderSequence;
private String createTime;
private Integer totalCount;
private Double totalAmount;
private Integer orderStatus;
private Integer userId;
//省略 get、set 等方法
}
创建 OrderItem 订单详情类,示例代码如下。
public class OrderItem {
private Integer itemId;
private String bookName;
private Double price;
private String imgPath;
private Integer itemCount;
private Double itemAmount;
14.7.2 登录状态检查¶
由于 “结账” 功能和 “我的订单” 功能都需要获取当前登录人的信息,如果不进行登录就单击 “结账” 或 “我的订单”,就会出现异常,因此我们创建一个过滤器,用于检查登录状态,如果处于登录状态就放行,如果不处于登录状态就跳转至登录页面,让用户进行登录。
package com.atguigu.filter;
//省略 import 语句
@WebFilter(urlPatterns = "/order")
public class LoginFilter implements Filter {
public void destroy() {
}
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
//1. 检查本次请求是否处于登录状态
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletRequest) resp;
HttpSession session = request.getSession();
Object user = session.getAttribute("user");
if (user == null) {
//说明不处于登录状态(跳转至登录页面)
response.sendRedirect(request.getContextPath() + "/user?method=login.html");
} else {
//说明处于登录状态
chain.doFilter(req, resp);
}
}
public void init(FilterConfig config) throws ServletException {
}
}
同时,完善项目页面中涉及显示用户名的头信息,从 session 中获取数据进行展示。示例代码如下。
14.7.3 结账¶
单击 “去结账”,发送请求到服务器端,需要从 session 中获取购物车数据,创建订单项数据和订单数据,以及购物车清空操作。具体步骤如下。
(1)修改 cart.html 页面中 “去结账” 超链接,示例代码如下。
(2)创建 OrderServlet 类,并声明 checkout () 方法处理结账请求,示例代码如下。
package com.atguigu.servlet.model;
//省略 import 语句
@WebServlet("/order")
public class OrderServlet extends++,ViewBaseServlet {
private OrderService orderService=new OrderServiceImpl();
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this doPost(request, response);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String method = request.getParameter("method");
if (method.equals("checkout")) {
checkout(request, response);
}
}
protected void checkout(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//处理结账的请求
//1. 获取处理请求需要的相关数据
//1.1 需要购物车的数据
HttpSession session = request.getSession();
Cart cart = (Cart) session.getAttribute("cart");
//1.2 当前登录人的 User 对象
User user=(User)session.getAttribute("user");
//2. 调用业务层处理业务
String sequence = orderService.checkout(cart, user);
//2.1 清空购物车
request.getSession().removeAttribute("cart");
//3. 给响应
request.setAttribute("sequence", sequence);
processTemplate("cart/checkout", request, response);
}
(3)完善业务层和持久层代码。¶
创建 OrderService 接口,并声明 checkout () 方法,示例代码如下。
package com.atguigu.service;
//省略 import 语句
public interface OrderService {
/*
* 返回值:订单号
* 参数:购物车对象和当前登录人的 User 对象
*/
String checkout(Cart cart, User user);
}
创建 OrderServiceImpl 实现类,实现 OrderService 接口中的方法,示例代码如下。
package com.atguigu.service.impl;
//省略 import 语句
public class OrderServiceImpl implements OrderService {
private OrderDao orderDao=new OrderDaoImpl();
private OrderItemDao orderItemDao=new OrderItemDaoImpl();
private BookDao bookDao=new BookDaoImpl();
@Override
public String checkout(Cart cart, User user) {
//1. 新增订单记录
//1.1 准备订单数据
String orderSequence="atguigu"+System.currentTimeMillis();
SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String date = sdf.format(new Date());
Order order=new Order(null,orderSequence,date,cart.getTotalCount(),
cart.TotalAmount(),0,user.getId());
//1.2 进行订单的新增操作
orderDao.insert(order);
//2. 新增订单项记录
//2.1 获取刚刚新增订单的 id 值
Integer orderId = orderDao.getId(orderSequence);
//2.2 获取所有的购物项
Collection<CartItem> cartItems = cart.getCartItems();
for (CartItem cartItem : cartItems) {
//2.3 准备订单项的数据
OrderItem orderItem=new OrderItem(null, cartItem.getBook().getBookName(),
cartItem.getBook().getPrice(), cartItem.getBook().getImgPath(), cartItem.getCount(),
cartItem.getAmount(), orderId);
//2.4 调用 dao 层进行数据的新增
orderItemDao.insert(orderItem);
//3. 修改图书的库存和销量
Book book = cartItem.getBook();
book.setStock(book.getStock() -cartItem.getCount());
book.setSales(book.getSales() + cartItem.getCount());
bookDao.update(book);
}
return orderSequence;
}
创建 OrderDao 接口,声明 getId () 和 insert () 方法,示例代码如下。
package com.atguigu.dao;
import com.atguigu.bean.Order;
public interface OrderDao {
void insert(Order order);
Integer getId(String sequence);
}
创建 OrderDaoImpl 实现类,并实现 OrderDao 接口,示例代码如下。
package com.atguigu.dao.impl;
//省略 import 语句
public class OrderDaoImpl implements OrderDao {
private QueryRunner runner=new QueryRunner();
@Override
public void insert(Order order) {
try {
Connection connection = JDBCTools.getConnection();
String sql="insert into t_order values(null,?,?,?,?,?,?)";
runner.update(connection,sql,order.getOrderSequence(),
order.getCreateTime(),order.TotalCount(),
order.getTotalAmount(), order.getOrderStatus(), order.getUserId());
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException();
} finally {
try {
JDBCTools.freeConnection();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
@Override
public Integer getId(String sequence) {
try {
Connection connection = JDBCTools.getConnection();
String sql="select order_id from t_order where order_sequence=?";
return (Integer) runner.query(connection, sql,
new ScalarHandler(), sequence);
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException();
} finally {
try {
JDBCTools.freeConnection();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
创建 OrderItemDao 接口,声明 insert () 方法,示例代码如下。
package com.atguigu.dao;
import com.atguigu.bean.OrderItem;
public interface OrderItemDao {
void insert(OrderItem orderItem);
}
创建 OrderItemDaoImpl 实现类,并实现 OrderItemDao 接口,示例代码如下。
package com.atguigu.dao.impl;
//省略 import 语句
public class OrderItemDaoImpl implements OrderItemDao {
private QueryRunner runner=new QueryRunner();
@Override
public void insert(OrderItem item) {
try {
Connection connection = JDBCTools.getConnection();
String sql="insert into t_order_item values(null,?,?,?,?,?,?)";
runner.update(connection,sql,item.getBookName(),item.getPrice(),
item.getImgPath(),item.getItemCount(),
item.getItemAmount(),item.getOrderId());
}catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException();
}finally {
try {
JDBCTools.freeConnection();
}catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
(4)修改 checkout.html 结算页面,进行数据渲染,示例代码如下。
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>结算页面</title>
<link type="text/css" rel="stylesheet" th:href="@{/static/css/style.css}" >
<link rel="stylesheet" th:href="@{/static/css/minireset.css}" />
<link rel="stylesheet" th:href="@{/static/css/common.css}" />
<link rel="stylesheet" th:href="@{/static/css/cart.css}" />
<style type="text/css">
h1 {
text-align: center;
margin-top: 200px;
font-size: 26px;
}
.oid{
color: red;
font-weight: bolder;
}
</style>
</head>
<body>
<div class="header">
<div class="w">
<div class="header-left">
<a th:href="@{/index.html}">
<img th:src="@{/static/img/logo.gif}" alt="" />
></a>
<span>我的购物车</span>
</div>
<div class="header-right">
<h3>欢迎<span>张总</span>光临尚硅谷书城</h3>
<div class="order">
<a href="../order/order.html">我的订单</a></div>
<div class="destory"><a th:href="@{/index.html}">注销</a></div>
<div class="gohome">
<a th:href="@{/index.html}">返回</a>
</div>
</div>
</div>
</div>
</div>
<div id="main">
<h1>你的订单已结算,订单号为:<span class="oid">546845626455846</span></h1>
运行代码,从首页添加几本图书到购物车,如图 14-31 所示,单击 “去结账” 按钮。

由于当前未登录,所以跳转登录页面,成功实现登录拦截。然后再次回到首页来到购物车,单击 “去结账” 按钮,如图 14-32 所示,生成订单编号。

如果在已登录的状态下,单击 “去结账” 按钮将直接跳转该页面,生成订单编号。
14.7.4 我的订单¶
在登录成功页面和购物车页面都有 “我的订单” 超链接,单击 “我的订单” 将当前账户的订单展示在页面上,具体步骤如下。
(1)修改 “我的订单” 超链接,示例代码如下。
(2)在 OrderServlet 的 doPost () 方法中增加一个分支(加粗部分),示例代码如下。
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String method = request.getParameter("method");
if (method.equals("checkout")) {
checkout(request, response);
} else if (method.equals("showOrder")) {
showOrder(request, response);
}
}
并在 OrderServlet 类中,创建 showOrder () 方法,示例代码如下。
protected void showOrder(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
//1. 获取到当前登录人的 User 对象
//2. 调用业务层处理业务
//3. 给响应
(3) 完善业务层和持久层代码。
在 OrderService 接口中,创建 showOrder () 方法,示例代码如下。
在 OrderServiceImpl 实现类中,实现 OrderService 接口的方法,示例代码如下。
@Override
public List<Order> showOrder(Integer userId) {
return orderDao.findOrderByUserId(userId);
}
在 OrderDao 接口中,创建 findOrderByUserId () 方法,示例代码如下。
在 OrderDaoImpl 实现类中,实现 OrderDao 接口的方法,示例代码如下。
@Override
public List<Order> findOrderByUserId(Integer userId) {
try {
Connection connection = JDBCTools.getConnection();
String sql="select " +
"order_id orderId, order_sequence orderSequence," +
"create_time createTime, total_counttotalCount," +
"total_amount totalAmount, order_status orderStatus, user_id userId " +
"from t_order where user_id = ?";
return runner.query(connection, sql, new BeanListHandler<Order>(Order.class), userId);
} catch (SQLException e) {
e.printStackTrace();
} finally {
try {
JDBCTools.freeConnection();
} catch (SQLException e) {
throw new RuntimeException(e);
运行代码,单击 “我的订单” 进入订单页面,如图 14-33 所示。

14.8 本章小结¶
本章主要介绍了尚硅谷书城项目,该项目整合了全书所有的技术知识,功能模块包括用户管理模块、后台管理模块、前台图书展示、购物车功能及结账功能等。通过本章的学习,巩固和提升对 JavaWeb 相关知识的掌握和综合应用,加深对项目的理解和开发能力。

