文章目录
- 🐀Java后端经典三层架构
- 🐇MVC模型
- 🐇开发环境搭建
- 🐇会员注册
- 🌳前端验证用户注册信息
- 🌳思路分析
- 🍉创建表
- 🍉创建实体类
- 🍉DAO
- 🍌MemberDAOImpl
- 🍉Service
- 🍌MemberServiceImpl
- 🌳接通web层
- 🐇会员登陆
- 🌳登陆错误_信息回显
- 🐇servlet合并
- 🍎反射+模板设计模式+动态代理
- 🌳显示家居
- 🌳添加家居
- 🍉解决重复添加
- 🍉后端数据校验说明
- 🍉BeanUtils自动封装Bean
- 🌳删除家居
- 🌳修改家具
- 🍃后台分页
- 🍒新建Page类
- 🍒DAO
- 🍒Service
- 🍒web层获取page对象
- 🍒前端页面
- 🍅后台分页导航
- 🍅修改后返回原页面
- 🍅删除后返回原页面
- 🍅添加后返回原页面
- 🍃首页分页
- 🍅首页搜索
- 🍅两个奇怪的问题
- 🌳会员显示登录名
- 🍅注销登录
- 🍅验证码
- 🌳购物车
- 🍆显示购物车
- 🍆修改购物车
- 🍆删除购物车
- 🌳生产订单
- 🍉创建表
- 🍉实体类
- 🍉DAO
- 🍉service
- 🍉servlet
- 🍉前端
- 🌳显示订单[订单管理]
- 🌈过滤器权限验证
- 🌈事务管理
- 1. 数据不一致问题
- 2. 程序框架图
- 🌈Transaction过滤器
- 🌈统一错误页面
- 🌈Ajax检验注册名
- 🌈Ajax添加购物车
- 🌈上传与更新家具图片
- 🌈作业布置
- 🍍会员登陆后不能访问后台管理
- 🍍解决图片冗余问题
- 🍍分页导航完善
🐀Java后端经典三层架构
分层 | 对应包 | 说明 |
---|---|---|
web层 | com.zzw.furns.web/servlet/controller/handler | 接受用户请求, 调用service |
service层 | com.zzw.furns.service | Service接口包 |
com.zzw.furns.service.impl | Service接口实现类 | |
dao持久层 | com.zzw.furns.dao | Dao接口包 |
com.zzw.furns.dao.impl | Dao接口实现类 | |
实体bean对象 | com.zzw.furns.pojo/entity/domain/bean | JavaBean类 |
工具类 | com.zzw.furns.utils | 工具类 |
测试包 | com.zzw.furns.test | 完成对dao/service测试 |
🐇MVC模型
MVC全称: Model模型, View试图, Controller控制器
MVC最早出现在JavaEE三层中的Web层, 它可以有效地指导WEB层代码如何有效地分离, 单独工作
- View试图: 只负责数据和界面的显示, 不接受任何与显示数据无关的代码, 便于程序员和美工的分工与合作(Vue/Jsp/Thymeleaf/Html)
- Controller控制器: 只负责接收请求, 调用业务层的代码处理请求, 然后派发给页面, 是一个"调度者"的角色
- Model模型: 将与业务逻辑相关的数据封装为具体的JavaBean类, 其中不掺杂任何与数据处理相关的代码(JavaBean/Domain/Pojo)
解读
- model 最早期就是javabean, 就是早期的jsp+servlet+javabean
- 后面业务复杂度越来越高, model逐渐分层化/组件化(service+dao)
- 后面又出现了持久化技术(service+dao+持久化技术(hibernate / mybatis / mybatis-plus))
- MVC依然是原来的mvc, 只是变得更加强大
🐇开发环境搭建
详情请参考👉
- 新建Java项目, 导入web框架
- 导入jar包
- 项目的结构
- 拷贝到web路径下
- 配置Tomcat
Rebuild project, 让项目识别到这些资源, 然后再启动Tomcat
- 对于复杂的前端页面, 要学会打开当前页面的结构, 提高工作效率
🐇会员注册
🌳前端验证用户注册信息
script引文件是src属性
<script type="text/javascript" src="../../script/jquery-3.6.0.min.js"></script><script type="text/javascript">$(function () {//页面加载完毕后执行 function$("#sub-btn").click(function () {//采用过关斩将法//正则表达式验证用户名var usernameValue = $("#username").val();var usernamePattern = /^\w{6,10}$/;if (!usernamePattern.test(usernameValue)) {$("span[class='errorMsg']").text("用户名格式不对, 需要6-10个字符(大小写字母,数字,下划线)");return false;}//验证密码var passwordValue = $("#password").val();var passwordPattern = /^\w{6,10}$/;if (!passwordPattern.test(passwordValue)) {$("span.errorMsg").text("密码格式不对, 需要6-10个字符(大小写字母,数字,下划线)");return false;}//两次密码要相同var rePwdValue = $("#repwd").val();if (passwordValue != rePwdValue) {$("span.errorMsg").text("两次密码不相同");return false;}//这里仍然采用过关斩将法//验证邮件var emailVal = $("#email").val();//在java中, 正则表达式的转义是\\; 在js中, 正则表达式转义是\var emailPattern = /^[\w-]+@([a-zA-Z]+\.)+[a-zA-Z]+$/;if (!emailPattern.test(emailVal)) {$("span.errorMsg").text("电子邮件的格式不正确, 请重新输入");return false;}//这里暂时不提交=>显示验证通过$("span.errorMsg").text("验证通过");return false;});})</script>
🌳思路分析
创建表->javabean->DAO->service
分层 | 对应包 | 说明 |
---|---|---|
web层 | RegisterServlet.java | 接受浏览器发送数据; 调用相关的service;根据执行结果,返回页面数据 |
service层 | MemberService.java | Service接口包 |
MemberServiceImpl.java | Service接口实现类 | |
dao持久层 | MemberDAO.java | Dao接口包 |
MemberDAOImpl | Dao接口实现类 | |
实体bean对象 | Member.java | JavaBean类 |
工具类 | JdbcUtilsByDruid.java | 工具类 |
🍉创建表
🍉创建实体类
满汉楼项目
包括无参构造器和set方法. 如果添加有参构造器, 记得书写无参构造器
- 从满汉楼项目引入BasicDAO.java, JdbcUtilsByDruid.java, Druid.properties
- 修改Druid配置文件要连接的数据库名, 确保用户名密码正确. ?后面是做批处理用的
- 修改JdbcUtilsByDruid的路径
配置快捷键
- 测试
🍉DAO
🍌MemberDAOImpl
public class MemberDAOImpl extends BasicDAO<Member> implements MemberDAO {/*** 通过用户名返回对应的Member* @param username 用户名* @return 对应的Member, 如果没有该Member返回null*/@Overridepublic Member queryMemberByUsername(String username) {//现在sqlyog测试, 然后再拿到程序中, 这样可以提高我们的开发效率, 减少不必要的bugString sql = "SELECT id, username, `password`, email FROM member WHERE username = ?";Member member = querySingle(sql, Member.class, username);return member;}/*** 保存一个会员* @param member 传入一个Member对象* @return 如果返回-1, 就是失败; 返回其它的数字, 就是受影响的行数*/@Overridepublic int saveMember(Member member) {//连同单引号一并换成 ? , 它会自动加上单引号String sql = "INSERT INTO member(id, username, `password`, email) " +"VALUES(NULL, ?, MD5(?), ?)";int updateRows = update(sql, member.getUsername(), member.getPassword(), member.getEmail());return updateRows;} }
测试
🍉Service
🍌MemberServiceImpl
public class MemberServiceImpl implements MemberService {//定义MemberDAO属性private MemberDAO memberDAO = new MemberDAOImpl();/*** 判断用户名是否存在** @param username 用户名* @return 如果存在返回true, 否则返回false*/@Overridepublic boolean isExistsByUsername(String username) {//小技巧: 如果看某个方法:// (1)ctrl+b 定位到memberDAO的编译类型中的方法// (2)如果使用ctrl+alt+b 会定位到实现类的方法//如果有多个类实现了该方法, 会让你选择return memberDAO.queryMemberByUsername(username) == null ? false : true;}@Overridepublic boolean registerMember(Member member) {return memberDAO.saveMember(member) == 1 ? true : false;} }
测试
🌳接通web层
将所有路径修改成相对路径
配置RegisterServlet, 请求RegisterServlet
🐇会员登陆
MemberDAO
MemberDAOImpl
测试(不要忘了测试)
快捷键
MemberService
测试(不要忘了测试)
web层
- 新建LoginServlet
login.html请求
login_ok.html
快捷键
效果
🌳登陆错误_信息回显
将login.html重命名为login.jsp, 修改base标签
LoginServlet
login.jsp
添加span标签
🐇servlet合并
方法一: 增加隐藏域
合并到MemberServlet
🍎反射+模板设计模式+动态代理
🌳显示家居
需求分析
- 给后台管理提供独立登陆页面 manage_login.jsp(已提供)
- 管理员(admin表)登陆成功后, 显示管理菜单页面
- 管理员点击家具管理, 显示所有家居信息
程序框架图
- 页面准备
- 新建admin表 👉 参考member表
新建furn表
- 新建Admin实体类
新建Furn实体类
- 书写AdminDAO, AdminDAOImpl, 并测试; 书写AdminService, AdminServiceImpl, 并测试 👉 参考Member
书写FurnDAO, FurnDAOImpl 👉 并测试
- 书写FurnService, FurnServiceImpl 👉 并测试
- 接通web层
配置web.xml, 书写AdminServlet
配置web.xml, 书写FurnServlet
将doGet()方法移到BasicServlet中
- 前端页面
manage_login.jsp 登录验证, 请求AdminServlet
manage_menu.jsp 请求FurnServlet
furn_manage.jsp 显示家居信息
🌳添加家居
思路分析
- 请求添加家居, 请求FurnServlet的add方法, 将前端提交的数据封装到Furn对象
- 调用FurnService.add(Furn furn)方法
- 跳转到显示家居的页面
- FurnDAO
- FurnService
- web层
FurnServlet
解决中文乱码问题
- 前端: 添加furn_add.jsp
🍉解决重复添加
请求转发, 当用户刷新页面时, 会重新发出第一次的请求, 造成数据重复提交
解决方案: 使用重定向
🍉后端数据校验说明
后端方案一
后端方案二
前端方案三
🍉BeanUtils自动封装Bean
引入: commons-logging-1.1.1.jar, commons-beanutils-1.8.0.jar
- 使用BeanUtils自动封装javabean
debug小技巧👉
- 报错
原因: 由于前端没有传imagePath的字段, 所有后端在构建的furn对象的时候, imagePath传了个null,
解决方案👇
- 将 把数据自动封装成JavaBean的功能封装到工具类
public class DataUtils {//将方法, 封装到静态方法, 方便使用public static <T> T copyParamToBean(Map value, T bean) {try {BeanUtils.populate(bean, value);} catch (Exception e) {throw new RuntimeException(e);}return bean;} }
调用
🌳删除家居
需求分析
- 管理员进入到家居管理页面
- 点击删除家居链接, 弹出确认窗口, 确认-删除, 取消-放弃
程序框架图
- FurnDAO
- FurnService
- web层
FurnServlet
- furn_add.jsp页面
jQuery操作父元素, 兄弟元素, 子元素, 请移步👉
js弹框请移步👉
🌳修改家具
思路分析
- 管理员进入家居管理页面furn_manage.jsp
- 点击修改家居链接, 回显该家居信息 [furn_update.jsp]
- 填写新的信息, 点击修改家居按钮
- 修改成功后, 显示刷新后的家居列表
程序框架图
- FurnDAO
- FurnService
- web层
FurnServlet
- 前端
furn_manage.jsp 点击修改,发出请求
furn_update.jsp 修改数据,点击提交
🍃后台分页
shortcuts: ctrl+alt+u👉在局部打开类图
程序框架图
🍒新建Page类
🍒DAO
思路
实现
🍒Service
🍒web层获取page对象
🍒前端页面
取缔list方法
furn_manage.jsp
🍅后台分页导航
程序框架图
<!-- Pagination Area Start --> <div class="pro-pagination-style text-center mb-md-30px mb-lm-30px mt-6" data-aos="fade-up"><ul><%--如果当前页 > 1, 就显示首页和上一页--%><li><a style="pointer-events:${requestScope.page.pageNo == 1 ? "none" : "auto"};"href="manage/furnServlet?action=page&pageNo=1&pageSize=${requestScope.page.pageSize}">首页</a></li><li><a style="pointer-events: ${requestScope.page.pageNo == 1 ? "none" : "auto"}"href="manage/furnServlet?action=page&pageNo=${requestScope.page.pageNo - 1}&pageSize=${requestScope.page.pageSize}">上一页</a></li><%--显示所有的分页数 先确定开始的页数 begin 1; 再确定结束的页数 end=>pageTotal--%><%--最多显示10页, 这里涉及算法--%><c:set scope="page" var="begin" value="1"></c:set><c:set scope="page" var="end" value="${requestScope.page.pageTotal}"></c:set><%--循环显示--%><c:forEach begin="${pageScope.begin}" end="${pageScope.end}" var="i"><%--总的页数--%><%--如果i是当前页, 就使用class="active"来修饰--%><li><a class="${i eq requestScope.page.pageNo ? "active" : ""}"href="manage/furnServlet?action=page&pageNo=${i}&pageSize=${requestScope.page.pageSize}">${i}</a></li></c:forEach><%--如果当前页 < 总的页数, 就显示末页和下一页--%><li><a style="pointer-events:${requestScope.page.pageNo == requestScope.page.pageTotal ? "none" : "auto"};"href="manage/furnServlet?action=page&pageNo=${requestScope.page.pageNo + 1}&pageSize=${requestScope.page.pageSize}">下一页</a></li><li><a style="pointer-events:${requestScope.page.pageNo == requestScope.page.pageTotal ? "none" : "auto"};"href="manage/furnServlet?action=page&pageNo=${requestScope.page.pageTotal}&pageSize=${requestScope.page.pageSize}">末页</a></li><li><a>共${requestScope.page.pageTotal}页</a></li><li><a>共${requestScope.page.totalRow}记录</a></li></ul> </div> <!-- Pagination Area End -->
🍅修改后返回原页面
🍅删除后返回原页面
🍅添加后返回原页面
🍃首页分页
需求分析
- 顾客进入首页页面
- 分页显示家居
- 正确显示分页导航条, 即功能完善, 可以使用
程序框架图
实现>1. 新建CustomerFurnServlet
2. 前端页面
直接请求CustomerFurnServlet, 获取网站首页要显示的分页数据
类似我们网站的入口页面👉jsp请求转发标签
index.jsp
3. 显示数据<c:forEach items="${requestScope.page.items}" var="furn"><div class="col-lg-3 col-md-6 col-sm-6 col-xs-6 mb-6" data-aos="fade-up"data-aos-delay="200"><!-- Single Product --><div class="product"><div class="thumb"><a href="shop-left-sidebar.html" class="image"><img src="${furn.imagePath}" alt="Product"/><img class="hover-image" src="assets/images/product-image/5.jpg"alt="Product"/></a><span class="badges"><span class="sale">-10%</span><span class="new">New</span></span><div class="actions"><a href="#" class="action wishlist" data-link-action="quickview"title="Quick view" data-bs-toggle="modal"data-bs-target="#exampleModal"><iclass="icon-size-fullscreen"></i></a></div><button title="Add To Cart" class=" add-to-cart">AddTo Cart</button></div><div class="content"><h5 class="title"><a href="shop-left-sidebar.html">Simple ${furn.name} </a></h5><span class="price"><span class="new">家居: ${furn.name}</span></span><span class="price"><span class="new">厂商: ${furn.business}</span></span><span class="price"><span class="new">价格: ${furn.price}</span></span><span class="price"><span class="new">销量: ${furn.saleNum}</span></span><span class="price"><span class="new">库存: ${furn.inventory}</span></span></div></div></div> </c:forEach>
分页导航
<!-- Pagination Area Start --> <div class="pro-pagination-style text-center mb-md-30px mb-lm-30px mt-6" data-aos="fade-up"><ul><li><a style="pointer-events: ${requestScope.page.pageNo > 1 ? "auto" : "none"}"href="customerFurnServlet?action=page&pageNo=1&pageSize=${requestScope.page.pageSize}">首页</a></li><li><a style="pointer-events: ${requestScope.page.pageNo > 1 ? "auto" : "none"}"href="customerFurnServlet?action=page&pageNo=${requestScope.page.pageNo - 1}&pageSize=${requestScope.page.pageSize}">上一页</a></li><c:set scope="page" var="begin" value="1"></c:set><c:set scope="page" var="end" value="${requestScope.page.pageTotal}"></c:set><c:forEach begin="${begin}" end="${end}" var="i"><li><a class="${i == requestScope.page.pageNo ? "active" : ""}"href="customerFurnServlet?action=page&pageNo=${i}&pageSize=${requestScope.page.pageSize}">${i}</a></li></c:forEach><li><a style="pointer-events: ${requestScope.page.pageNo < requestScope.page.pageTotal ? "auto" : "none"}"href="customerFurnServlet?action=page&pageNo=${requestScope.page.pageNo + 1}&pageSize=${requestScope.page.pageSize}">下一页</a></li><li><a style="pointer-events: ${requestScope.page.pageNo < requestScope.page.pageTotal ? "auto" : "none"}"href="customerFurnServlet?action=page&pageNo=${requestScope.page.pageTotal}&pageSize=${requestScope.page.pageSize}">末页</a></li><li><a>共${requestScope.page.pageTotal} 页</a></li><li><a>共${requestScope.page.totalRow}记录</a></li></ul> </div> <!-- Pagination Area End -->
🍅首页搜索
需求分析
- 顾客进入首页页面
- 点击搜索按钮, 可以输入家居名
- 正确显示分页导航条, 并且要求在分页时, 保留上次搜索条件
程序框架图
- DAO
模糊查询👉
- service
- web层 CustomerFurnServlet
page方法就被抛弃了
- 前端 index.jsp
🍅两个奇怪的问题
- 点击家居管理, 发出两个请求
抓包
原因
请求首页面即进入到indx.jsp, index.jsp又请求转发到CustomerFurnServlet
问题解决- 首页分页出现问题
原因
🌳会员显示登录名
需求分析
- 会员登陆成功, login_ok.jsp显示登录信息
- 如果登陆成功后返回首页面, 显示订单管理和安全退出
- 如果用户没有登陆过, 首页就显示登录和注册超链接
程序框架图
实现
重命名时, 会联动修改
将login_ok.jsp中的index.html改成index.jsp. 注意, 不要改成views/customer/index.jsp
🍅注销登录
思路分析
- 用户登录成功后
- login_ok.jsp, 点击安全退出, 注销登录
- 返回首页, 也可点击安全退出, 注销登录
程序框架图
实现
🍅验证码
程序框架图
- web层
KaptchaServlet -> 引入kaptcha-2.3.2.jar包, 在web.xml中配置KaptchaServlet
MemberServlet
- 前端页面
login.jsp
验证码不能为空
点击图片更换验证码
将register_ok.html, register_fail.html改造成jsp页面
login.jsp页面总是默认停留在会员登录的div内, 修改
注册回显信息
🌳购物车
程序框架图
cartItem模型
Cart数据模型
测试
实现
- 创建CartServlet
- 首页获取id请求后台
- 首页购买的商品总数量
🍆显示购物车
需求分析
- 查看购物车, 可以显示如下信息
- 选中了哪些家居, 名称, 数量, 金额
- 统计购物车共多少商品, 总价多少
程序框架图
- 走通购物车
cart.jsp
index.jsp跳转
排错
定位
页面源代码
- 显示家居项
<tbody> <%--找到显示购物车项, 进行循环的items--%> <c:if test="${not empty sessionScope.cart.items}"><%--1.sessionScope.cart.items => 取出的是HashMap<Integer, CartItem>2.所以通过foreach标签取出的每一个对象, 即entry是 HashMap<Integer, CartItem>的 k-v3.var其实就是 entry4.所以要取出cartItem对象, 是通过 entry.value取出--%><c:forEach items="${sessionScope.cart.items}" var="entry"><tr><td class="product-thumbnail"><a href="#"><img class="img-responsive ml-3"src="assets/images/product-image/1.jpg"alt=""/></a></td><td hidden="hidden" class="product-name"><a href="#">${entry.key}</a></td><%--隐藏域--%><td class="product-name"><a href="#">${entry.value.name}</a></td><td class="product-price-cart"><span class="amount">$${entry.value.price}</span></td><td class="product-quantity"><div class="cart-plus-minus" onclick="change1(this)"><input class="cart-plus-minus-box" type="text" name="qtyButton"value="${entry.value.count}"/></div></td><td class="product-subtotal">$${entry.value.totalPrice}</td><td class="product-remove"><a href="cartServlet?action=del&key=${entry.key}"><iclass="icon-close"></i></a></td></tr></c:forEach> </c:if> </tbody>
- 计算总价
🍆修改购物车
需求分析
- 进入购物车, 可以修改购买数量
- 更新该商品项的金额
- 跟新购物车商品数量和总金额
程序框架图
- Cart增加方法
- CartServlet
- 前端
cart.jsp
🍆删除购物车
需求分析
- 进入购物车, 可以删除某商品
- 可以清空购物车
- 要求给出适当的确认信息
程序框架图
- 删除购物车
- 清空购物车
🌳生产订单
需求分析
- 进入购物车, 点击购物车结账
- 生成订单和订单项
- 如果会员没有登陆, 先进入登陆页面, 完成登陆后再结账
程序框架图
🍉创建表
order表
-- 创建家居网购需要的数据库和表
-- 删除数据库
DROP DATABASE IF EXISTS home_furnishing;-- 删除表
DROP TABLE `order`;-- 创建数据库
CREATE DATABASE home_furnishing;-- 切换
USE home_furnishing;-- 创建订单表
-- 每个字段应当使用 not null 来约束
-- 字段类型的设计, 应当和相关联表的字段类型相对应
-- 是否需要使用外键?
-- 1.需要[可以从db层保证数据的一致性(早期hibernate框架要求必须使用外键)]
-- 2.不需要[外键对效率有影响, 应当从程序的业务层保证数据的一致性(推荐)]
CREATE TABLE `order` (id VARCHAR(60) PRIMARY KEY, -- 订单编号create_time DATETIME NOT NULL,-- 年月日 时分秒price DECIMAL(10,2) NOT NULL,-- 订单价格`status` TINYINT NOT NULL, -- 订单状态(1未发货 2已发货 3已结账)member_id INT NOT NULL -- 谁的订单
)CHARSET utf8 ENGINE INNODB;
order_item表
-- 创建家居网购需要的数据库和表
-- 删除数据库
DROP DATABASE IF EXISTS home_furnishing;-- 删除表
DROP TABLE order_item;-- 创建数据库
CREATE DATABASE home_furnishing;-- 切换
USE home_furnishing;-- 创建订单明细表
CREATE TABLE order_item (id INT PRIMARY KEY AUTO_INCREMENT, -- 订单明细id`name` VARCHAR(32) NOT NULL,-- 家居名`count` INT UNSIGNED NOT NULL,-- 数量price DECIMAL(10, 2) NOT NULL,-- 价格total_price DECIMAL(10, 2) NOT NULL,-- 订单项的总价格order_id VARCHAR(60) NOT NULL -- 订单编号
)CHARSET utf8 ENGINE INNODB;
🍉实体类
订单表
public class Order {private String id;private Date createTime;private BigDecimal price;private Integer status;private Integer memberId;public Order() {}//有参构造器//getter方法, setter方法
}
订单明细表
public class OrderItem {private Integer id;private String name;private Integer count;private BigDecimal price;private BigDecimal totalPrice;private String orderId;public OrderItem() {}//有参构造器//getter方法, setter方法
}
🍉DAO
OrderDAO
OrderItemDAO
🍉service
public class OrderServiceImpl implements OrderService {private OrderDAO orderDAO = new OrderDAOImpl();private OrderItemDAO orderItemDAO = new OrderItemDAOImpl();private FurnDAO furnDAO = new FurnDAOImpl();//在这里可以感受到javaee分层的好处. 在service层, 通过组合多个dao的方法,// 完成某个业务 慢慢体会好处@Overridepublic String saveOrder(Cart cart, int memberId) {//将cart购物车的数据以order和orderItem的形式保存到DB中//因为生成订单会操作多张表, 因此会涉及到多表事务的问题, ThreadLocal+Mysql事务机制+过滤器//1.通过cart对象, 构建一个对应的order对象// 先生成一个UUID, 表示当前的订单号, UUID是唯一的String orderId = UUID.randomUUID().toString();//订单idOrder order = new Order(orderId, new Date(), cart.getCartTotalPrice(), 0, memberId);//保存order到数据表orderDAO.saveOrder(order);//订单生成成功//通过cart对象, 遍历CartItem, 构建OrderItem对象, 并保存到对应的order_item表Map<Integer, CartItem> cartItems = cart.getItems();String orderItemId = "";for (CartItem cartItem : cartItems.values()) {//通过cartItem对象构建了orderItem对象OrderItem orderItem = new OrderItem(null, cartItem.getName(), cartItem.getCount(),cartItem.getPrice(), cartItem.getTotalPrice(), orderId);//保存orderItemDAO.saveOrderItem(orderItem);//更新furn表 saleNum销量 - inventory库存//(1) 获取furn对象Furn furn = furnDAO.queryFurnById(cartItem.getId());//(2) 更新furn对象的 saleNum销量 - inventory库存furn.setInventory(furn.getInventory() - cartItem.getCount());furn.setSaleNum(furn.getSaleNum() + cartItem.getCount());//(3) 更新到数据表furnDAO.updateFurn(furn);}//清空购物车cart.clear();return orderId;}
}
🍉servlet
public class OrderServlet extends BasicServlet {//定义属性private OrderService orderService = new OrderServiceImpl();/*** 生成订单* @param request* @param response* @throws ServletException* @throws IOException*/protected void saveOrder(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {HttpSession session = request.getSession();//获取购物车Cart cart = (Cart) session.getAttribute("cart");//如果cart为空, 说明会员没有购买任何家居, 转发到首页if (cart == null) {request.getRequestDispatcher("/index.jsp").forward(request, response);return;}//获取到登陆的member对象Member member = (Member) session.getAttribute("member");if (member == null) {//说明用户没有登录, 转发到登陆页面//重定向到登陆页面request.getRequestDispatcher("/views/member/login.jsp").forward(request, response);return;//直接返回}//可以生成订单String orderId = orderService.saveOrder(cart, member.getId());//订单, 订单明细已生成session.setAttribute("orderId", orderId);//订单id//使用重定向放入到checkout.jspresponse.sendRedirect(request.getContextPath() + "/views/order/checkout.jsp");}
}
- 防止生成空订单
hashMap.clear之后, 是置空还是size置为0
🍉前端
checkout.html修改为checkout.jsp
🌳显示订单[订单管理]
- 添加购物车按钮动态处理
需求分析
- 如果某家居库存为0, 前台的"Add to Cart" 按钮显示为"暂时缺货"
- 后台也加上校验. 只有在 库存>0时, 才能添加到购物车
思路分析
- 首页添加家居到购物车时, 加以限制
- 购物车里, 更新家居数量时,加以限制
- 管理订单
需求分析
- 完成订单管理-查看
- 具体流程参考显示家居
- 静态页面order.html 和 order_detail.html 已提供
程序框架图
- DAO
- service
- web层
- 前端
index.jsp, cart.jsp, login_ok.jsp, checkout.jsp, order.jsp均可跳转到订单管理
- 管理订单项
程序框架图
- DAO
- service
- web层
在Order实体类中新增count属性, 在生成订单时, 将totalCount赋给count
- 前端
order.jsp跳转到OrderItemServlet
order_detail.jsp
order表增加count列 - ALTER TABLEorder
ADDCOUNT
INT UNSIGNED NOT NULL AFTER price;
🌈过滤器权限验证
需求分析
- 加入过滤器权限验证
- 如果没有登陆, 查看购物车和添加到购物车, 就会自动转到会员登陆页面
- 配置拦截url
<?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>AuthFilter</filter-name><filter-class>com.zzw.furns.filter.AuthFilter</filter-class><init-param><!--这里配置了后, 还需要在过滤器中处理--><param-name>excludedUrls</param-name><param-value>/views/manage/manage_login.jsp,/views/member/login.jsp</param-value></init-param></filter><filter-mapping><filter-name>AuthFilter</filter-name><!--这里配置要验证的url1.在filter-mapping中的url-pattern配置 要拦截/验证的url2.对于我们不去拦截的url, 就不配置3.对于要拦截的目录中的某些要放行的资源, 再通过配置指定--><url-pattern>/views/cart/*</url-pattern><url-pattern>/views/manage/*</url-pattern><url-pattern>/views/member/*</url-pattern><url-pattern>/views/order/*</url-pattern><url-pattern>/cartServlet</url-pattern><url-pattern>/manage/furnServlet</url-pattern><url-pattern>/orderServlet</url-pattern><url-pattern>/orderItemServlet</url-pattern></filter-mapping> </web-app>
2.过滤器逻辑判断
/** * 这是用于权限验证的过滤器, 对指定的url进行验证 * 如果登陆过, 就放行; 如果没有登陆, 就回到登陆页面 * @author 赵志伟 * @version 1.0 */ @SuppressWarnings({"all"}) public class AuthFilter implements Filter {private List<String> excludedUrls;@Overridepublic void init(FilterConfig filterConfig) throws ServletException {//获取到配置的excludedUrlsString strExcludedUrls = filterConfig.getInitParameter("excludedUrls");String[] split = strExcludedUrls.split(",");//将 splitUrl 转成 listexcludedUrls = Arrays.asList(split);System.out.println("excludedUrls= " + excludedUrls);}@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {System.out.println("请求/cartServlet 被拦截...");HttpServletRequest request = (HttpServletRequest) servletRequest;//得到请求的urlString url = request.getServletPath();System.out.println("url= " + url);//判断是否要验证if (!excludedUrls.contains(url)) {//获取到登陆的member对象Member member = (Member) request.getSession().getAttribute("member");if (member == null) {//说明用户没有登录//转发到登陆页面, 转发不走过滤器servletRequest.getRequestDispatcher("/views/member/login.jsp").forward(servletRequest, servletResponse);重定向-拦截-重定向-拦截-重定向-拦截//((HttpServletResponse) servletResponse)// .sendRedirect(request.getContextPath() + "/views/member/login.jsp");return;//直接返回}}//验证通过, 放行filterChain.doFilter(servletRequest, servletResponse);System.out.println("请求/cartServlet验证通过, 放行");}@Overridepublic void destroy() {} }
- 处理管理员登陆
🌈事务管理
1. 数据不一致问题
- 将FurnDAOImpl.java的updateFurn方法的sql故意写错
[furnDAO.updateFurn(furn);
由ctrl+alt+b定位到updateFurn的实现方法]- 在OrderServiceImpl的saveOrder()方法内捕获一下异常, 目的是保证程序能够继续执行
- 查看数据库里的数据会有什么结果. 会出现数据不一致的问题.
我在首页购买了一个小台灯, 数据库中生成了对应的订单和订单项, 但家居表里该小台灯的销量和库存没有变化, 纹丝不动. 相当于客户下单了, 但没有给人家发货.
2. 程序框架图
思路分析
- 使用 Filter + ThreadLocal 来进行事务管理
- 说明: 在一次http请求中, servlet-service-dao 的调用过程, 始终是一个线程, 这是使用ThreadLocal的前提
- 使用ThreadLocal来确保所有dao操作都在同一个Connection内
程序框架图
- 修改JdbcUtilsByDruid工具类
public class JdbcUtilsByDruid {private static DataSource dataSource;//定义属性ThreadLocal, 这里存放一个Connectionprivate static ThreadLocal<Connection> threadlocalConn = new ThreadLocal<>();/*** 从ThreadLocal获取connection, 从而保证在一个线程中* 获取的是同一个Connection*/public static Connection getConnection() {Connection connection = threadlocalConn.get();if (connection == null) {//说明当前的threadlocal没有这个连接try {//就从数据库连接池中取出连接放入threadlocalconnection = dataSource.getConnection();//将连接设置为手动提交, 既不要让它自动提交connection.setAutoCommit(false);threadlocalConn.set(connection);} catch (SQLException e) {throw new RuntimeException(e);}}return connection;}/*** 提交事务*/public static void commit() {Connection connection = threadlocalConn.get();if (connection != null) {try {connection.commit();} catch (SQLException e) {throw new RuntimeException(e);} finally {try {connection.close();} catch (SQLException e) {throw new RuntimeException(e);}}//1.当提交后, 需要把connection从threadlocalConn中清除掉//2.不然会造成threadlocalConn长时间持有该连接, 会影响效率//3.也因为Tomcat底层使用的是线程池技术threadlocalConn.remove();}}/*** 说明: 所谓回滚是 回滚/撤销 和connection管理的操作 删除/修改/添加*/public static void rollback() {Connection connection = threadlocalConn.get();if (connection != null) {try {connection.rollback();} catch (SQLException e) {throw new RuntimeException(e);} finally {try {connection.close();} catch (SQLException e) {throw new RuntimeException(e);}}threadlocalConn.remove();}}
- 修改BasicDao
删掉各个方法finally代码块里的close方法. 只有在事务结束后才实施关闭连接的操作. 一是提交事务后关闭连接; 二是增删改出错后, 回滚关闭连接.public List<T> queryMany(String sql, Class<T> clazz, Object... objects) {Connection connection = null;try {connection = JdbcUtilsByDruid.getConnection();List<T> tList =queryRunner.query(connection, sql, new BeanListHandler<>(clazz), objects);return tList;} catch (SQLException e) {throw new RuntimeException(e);//编译异常->运行异常抛出}}//查询单行, 返回的是一个对象public T querySingle(String sql, Class<T> clazz, Object... objects) {Connection connection = null;try {connection = JdbcUtilsByDruid.getConnection();T object= queryRunner.query(connection, sql, new BeanHandler<>(clazz), objects);return object;} catch (Exception e) {throw new RuntimeException(e);}}//查询某一字段public Object queryScalar(String sql, Object... objects) {Connection connection = null;try {connection = JdbcUtilsByDruid.getConnection();Object query = queryRunner.query(connection, sql, new ScalarHandler(), objects);return query;} catch (Exception e) {throw new RuntimeException(e);}}public int update(String sql, Object... objects) {Connection connection = null;try {//这里是从数据库连接池获取connection//注意:每次从连接池中取出connection, 不能保证是同一个//1.我们目前已经是从和当前线程关联的ThreadLocal获取的connection//2.所以可以保证是同一个连接[在同一个线程中/在同一个请求中 => 因为一个请求对应一个线程]connection = JdbcUtilsByDruid.getConnection();return queryRunner.update(connection, sql, objects);} catch (Exception e) {throw new RuntimeException(e);}}
- 控制层进行事务管理
前提OrderServiceImpl里报错的代码取消try-catch, 在OrderServlet控制层捕获//1.如果我们只是希望对orderService.saveOrder()方法进行事务控制//2.那么我们可以不使用过滤器,直接在这个位置进行提交和回滚即可//可以生成订单String orderId = null;//订单, 订单明细已生成try {orderId = orderService.saveOrder(cart, member.getId());JdbcUtilsByDruid.commit();//提交} catch (Exception e) {JdbcUtilsByDruid.rollback();e.printStackTrace();}
🌈Transaction过滤器
程序框架图
体会: 异常机制是可以参与业务逻辑的
- 在web.xml中配置
<filter><filter-name>TransactionFilter</filter-name><filter-class>com.zzw.furns.filter.TransactionFilter</filter-class></filter><filter-mapping><filter-name>TransactionFilter</filter-name><!--这里我们对请求都进行事务管理 --><url-pattern>/*</url-pattern></filter-mapping>
- 在OrderService控制层里取消捕获异常, 将代码重新改回下述模样
String orderId = orderService.saveOrder(cart, member.getId());
同时BasicServlet模板里也取消异常捕获, 或者将异常抛出, 代码如下try {Method declaredMethod =this.getClass().getDeclaredMethod(action, HttpServletRequest.class, HttpServletResponse.class);System.out.println("this = " + this);//com.zzw.furns.web.MemberServlet@38f54ed7declaredMethod.invoke(this, req, resp);System.out.println("this.getClass() = " + this.getClass());} catch (Exception e) {//将发生的异常, 继续throwthrow new RuntimeException(e);}
- 在代码执行完毕后, 会运行到Transaction过滤器的后置代码, 在这里进行异常捕获, 如果发生异常, 则回滚.
@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {try {//放行filterChain.doFilter(servletRequest, servletResponse);JdbcUtilsByDruid.commit();//统一提交} catch (Exception e) {//出现了异常JdbcUtilsByDruid.rollback();//回滚e.printStackTrace();}}
🌈统一错误页面
需求分析
- 如果在访问/操作网站时, 出现了内部错误, 统一显示 500.jsp
- 如果访问/操作的页面/servlet不存在时, 统一显示 404.jsp
思路分析
- 发生错误/异常时, 将错误/异常抛给tomcat
- 在web.xml中配置不同错误显示的页面即可
- 引入404.html, 500.html, 修改成jsp文件
页面首行<%@ page contentType="text/html;charset=UTF-8" language="java" %>
base标签<base href="<%=request.getContextPath()%>/">
将跳转链接改成index.jsp
<a class="active" href="index.jsp"> <h4 style="color: darkblue">您访问的页面不存在 返回首页</h4> </a>
- web.xml配置
<!--错误提示的配置一般写在web.xml的下面--><!--500 错误提示页面--> <error-page><error-code>500</error-code><location>/views/error/500.jsp</location> </error-page> <!--404 错误提示页面--> <error-page><error-code>404</error-code><location>/views/error/404.jsp</location> </error-page>
- 修改事务过滤器, 将异常抛给tomcat
@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {try {//放行filterChain.doFilter(servletRequest, servletResponse);JdbcUtilsByDruid.commit();//统一提交} catch (Exception e) {//出现了异常//只有在try{}中出现了异常, 才会进行catch{}//才会进行回滚JdbcUtilsByDruid.rollback();//回滚//抛出异常, 给tomcat. tomcat会根据error-page来显示对应页面throw new RuntimeException(e);//e.printStackTrace();}}
🌈Ajax检验注册名
需求分析
- 注册会员时, 如果名字已经注册过, 当光标离开输入框, 提示会员名已经存在, 否则提示不存在
- 要求使用ajax完成
程序框架图
- MemberServlet - 返回json格式的字符串 - 方式一
protected void isExistByName(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {//1.获取用户名String username = req.getParameter("username");//2.调用serviceboolean existsByUsername = memberService.isExistsByUsername(username);//3.思路//(1)如果返回json格式[不要乱写, 要根据前端的需求来写]//(2)因为前后端都是我们自己写的, 格式我们自己定义//(3){"isExist": true};//(4)先用最简单的方法拼接 => 一会改进[扩展]String resultJson = "{\"isExist\": " + existsByUsername + "}";//4.返回resp.getWriter().print(resultJson); }
返回json格式的字符串 - 方式二
protected void isExistByName(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {//1.获取用户名String username = req.getParameter("username");//2.调用serviceboolean existsByUsername = memberService.isExistsByUsername(username);//3.思路//(1)如果返回json格式[不要乱写, 要根据前端的需求来写]//(2)因为前后端都是我们自己写的, 格式我们自己定义//(3){"isExist": true};//(4)先用最简单的方法拼接 => 一会改进[扩展]//String resultJson = "{\"isExist\": " + existsByUsername + "}";字符串就不需要再转//(5)将要返回的数据封装成map => json格式Map<Object, Object> map = new HashMap<>();map.put("isExist", existsByUsername);//map.put("email", "978964140@qq.com");//map.put("phone", "13031748275");//4.返回json格式的数据Gson gson = new Gson();String resultJson = gson.toJson(map);resp.getWriter().print(resultJson); }
- 前端
$("#username").mouseleave(function () {//鼠标离开事件[无需点击, 即可触发]var usernameValue = $(this).val();$.getJSON(//这里尽量准确, 一把确定[复制粘贴]"memberServlet", "action=isExistByName&username=" + usernameValue, function (data) {alert(data.isExist);console.log("data= ", data);//显示json格式的数据: 1.要用逗号; 2.要用console.log()} /*========================================================================================*/"memberServlet?action=isExistByName&username=" + usernameValue, function (data) {alert(data.isExist);console.log("data= ", data);//显示json格式的数据: 1.要用逗号; 2.要用console.log()} /*========================================================================================*/"memberServlet",{action: "isExistByName",username: usernameValue},function (data) {alert(data.isExist);console.log("data= ", data);//显示json格式的数据: 1.要用逗号; 2.要用console.log()} /*========================================================================================*/"memberServlet",{"action": "isExistByName","username": usernameValue},function (data) {alert(data.isExist);//前端人员只能通过console.log()来查看你的数据, 然后才知道怎么获取你的数据console.log("data= ", data);//显示json格式的数据: 1.要用逗号; 2.要用console.log()if (data.isExist) {$("span[class='errorMsg']").text("用户名 " + usernameValue + " 不可用");} else {$("span[class='errorMsg']").text("用户名 " + usernameValue + " 可用");}) }
- Ajax检验验证码
- MemberServlet
protected void verifyCaptcha(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {//获取用户提交的验证码String captcha = req.getParameter("captcha");//从session中获取 生成的验证码HttpSession session = req.getSession();String token = (String) session.getAttribute(KAPTCHA_SESSION_KEY);//立即删除session中的验证码, 防止该验证码被重复使用session.removeAttribute(KAPTCHA_SESSION_KEY);//如果token不为空, 并且和用户提交的验证码保持一致, 就继续if (token != null) {Map<Object, Object> map = new HashMap<>();boolean verifyCaptcha = token.equalsIgnoreCase(captcha);map.put("verifyCaptcha", verifyCaptcha);//返回json格式的数据Gson gson = new Gson();String resultJson = gson.toJson(map);resp.getWriter().print(resultJson);}}
- 前端
$("#code").blur(function () {//光标焦点离开事件[点击后离开, 才可以触发]var captchaValue = this.value;$.getJSON("memberServlet?action=verifyCaptcha&captcha="+captchaValue, function (data) {console.log("data= ", data);if (data.verifyCaptcha) {$("span.errorMsg2").text("验证码正确");} else {$("span.errorMsg2").text("验证码错误");}});})
在验证码标签旁补充一个span标签
<span class="errorMsg2" style="float: right; font-weight: bold; font-size: 15pt; margin-left: 10px; color: lightgray;"></span>
🌈Ajax添加购物车
- CartServlet添加addItemByAjax方法
//添加一个添加家居到购物车的方法 [Ajax]protected void addItemByAjax(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {int id = DataUtils.parseInt(request.getParameter("id"), 1);//家居id//根据id获取对应的家居信息Furn furn = furnService.queryFurnById(id);//先把正常的逻辑走完, 再处理异常的情况//如果某家居的库存为0, 就不要添加到购物车, 直接请求转发到首页面//if (furn.getInventory() <= 0) {// request.getRequestDispatcher("/index.jsp").forward(request, response);// return;//}HttpSession session = request.getSession();Cart cart = (Cart) session.getAttribute("cart");//得到购物车 有可能是空的,也有可能是上次的if (cart == null) {cart = new Cart();session.setAttribute("cart", cart);}//构建一条家居明细: id,家居名,数量, 单价, 总价//count类型为Integer, 不赋值默认值为nullCartItem cartItem = new CartItem(id, furn.getName(), 1, furn.getPrice(), furn.getPrice());//将家居明细加入到购物车中. 如果家居id相同,数量+1;如果是一条新的商品,那么就新增cart.addItem(cartItem, furn.getInventory());System.out.println("cart= " + cart);//规定格式 {"cartTotalCount": 3}//方式一://String resultJson = "{\"cartTotalCount\": " + cart.getTotalCount() + "}";//response.getWriter().print(resultJson);//方式二: 创建map,可扩展性强Map<Object, Object> map = new HashMap<>();map.put("cartTotalCount", cart.getTotalCount());//转成jsonGson gson = new Gson();String resultJson = gson.toJson(map);//返回response.getWriter().print(resultJson);//String referer = request.getHeader("referer");//response.sendRedirect(referer);}
- 前端
//给所有选定的button都赋上点击事件$("button.add-to-cart").click(function () {var id = $(this).attr("furnId");//location.href = "cartServlet?action=addItem&id=" + id;//这里我们使用jquery发出ajax请求, 得到数据进行局部刷新, 解决刷新这个页面效率低的问题$.getJSON("cartServlet?action=addItemByAjax&id=" + id, function (data) {console.log("data=", data);//刷新局部 <span class="header-action-num"></span>$("span.header-action-num").text(data.cartTotalCount);})});
- 解决Ajax请求转发失败
测试, 会发现针对ajax的重定向和请求转发会失败, 也就是AuthFilter.java的权限拦截不生效, 也就是点击Add to Cart, 后台服务没有响应
使用ajax向后台发送请求跳转页面无效的原因
- 主要是服务器得到的是ajax发送过来的request, 也就是说这个请求不是浏览器请求的, 而是ajax请求的. 所以servlet根据request进行请求转发或重定向都不能影响浏览器的跳转
- 解决方案: 如果想要实现跳转, 可以返回url给ajax, 在浏览器执行window.location(url);
工具类添加方法 - 判断请求是不是一个ajax请求
/*** 判断请求是不是一个ajax请求* @param request* @return*/public static boolean isAjaxRequest(HttpServletRequest request) {//X-Requested-With: XMLHttpRequestreturn "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));}
修改AuthFilter.java
if (member == null) {//说明用户没有登录if (!WebUtils.isAjaxRequest(request)) {//如果不是ajax请求//转发到登陆页面, 转发不走过滤器servletRequest.getRequestDispatcher("/views/member/login.jsp").forward(servletRequest, servletResponse);} else {//如果是ajax请求//返回ajax请求, 按照json格式返回 {"url": url}//1.构建mapMap<Object, Object> map = new HashMap<>();map.put("url", "views/member/login.jsp");//2.转成json字符串String resultJson = new Gson().toJson(map);//3.返回servletResponse.getWriter().print(resultJson);}重定向-拦截-重定向-拦截-重定向-拦截//((HttpServletResponse) servletResponse)// .0sendRedirect(request.getContextPath() + "/views/member/login.jsp");return;//直接返回 }
修改getJson
//这里我们使用jquery发出ajax请求, 得到数据进行局部刷新, 解决刷新这个页面效率低的问题 $.getJSON("cartServlet?action=addItemByAjax&id=" + id, function (data) {console.log("data=", data);if (data.url == undefined) {//刷新局部 <span class="header-action-num"></span>$("span.header-action-num").text(data.cartTotalCount);} else {location.href = data.url;}} )
🌈上传与更新家具图片
引入文件上传下载的包: commons-io-1.4.jar, commons-fileupload-1.2.1.jar
FurnDAOImpl的查询语句加上图片字段 image_path as imagePath
需求分析
- 后台修改家居, 可以点击图片, 选择新的图片
- 这里会用到文件上传功能
思路分析-程序框架图
- furn_update.jsp
<style type="text/css">#pic {position: relative;}input[type="file"] {position: absolute;left: 0;top: 0;height: 180px;opacity: 0;cursor: pointer;}</style>
去掉a标签
<div id="pic"><img class="img-responsive ml-3" src="${requestScope.furn.imagePath}"alt="" id="preView"><input type="file" name="imagePath" id="" >value="${requestScope.furn.imagePath}"οnchange="prev(this)"/> </div>
- 分析空指针异常
将form表单改成文件表单
<form action="manage/furnServlet" method="post" enctype="multipart/form-data"></form>
点击修改家居
报错
将web.xml中500的错误提示配置注销掉, 将异常信息暴露出来
再次点击修改家居信息, 报错信息显示出来, BasicServlet空指针异常
所以有时候报错信息显示出来很重要
分析: 如果表单是enctype=“multipart/form-data”, 那么req.getParameter(“action”) 的方法得不到action值, 所以BasicServlet会报错
具体原因: req.getParameter(“action”)取不到form-data里的数据
- 解决空指针异常
解决方案: 将参数action, id, pageNo以url拼接的方式传参, BasicServlet便不会出错
注意: post请求可以人为主动在地址中拼接参数,拼接的参数可以直接像get那样接收
<form action="manage/furnServlet?action=update&id=${requestScope.furn.id}&pageNo=${param.pageNo}" method="post" enctype="multipart/form-data">
- FurnServlet update方法
处理普通字段if (fileItem.isFormField()) {//文本表单字段将提交的家居信息, 封装成Furn对象switch (fileItem.getFieldName()) {case "name":furn.setName(fileItem.getString("utf-8"));break;case "business":furn.setBusiness(fileItem.getString("utf-8"));break;case "price":furn.setPrice(new BigDecimal(fileItem.getString()));break;case "saleNum":furn.setSaleNum(Integer.parseInt(fileItem.getString()));break;case "inventory":furn.setInventory(Integer.parseInt(fileItem.getString()));break;} }
处理文件字段
将文件上传路径保存成一个常量public class WebUtils {public static final String FURN_IMG_DIRECTORY = "assets/images/product-image/"; }
//文件表单字段 => 获取上传的文件的名字 String name = fileItem.getName();//如果用户没有选择新的图片, name = "" if (!"".equals(name)) {//1.把上传到到服务器 temp目录下的文件保存到指定的目录String filePath = "/" + WebUtils.FURN_IMG_DIRECTORY;//2.获取完整的目录String fileRealPath = req.getServletContext().getRealPath(filePath);System.out.println("fileRealPath= " + fileRealPath);//3.创建这个上传的目录File fileRealPathDirectory = new File(fileRealPath);if (!fileRealPathDirectory.exists()) {fileRealPathDirectory.mkdirs();}//4.将文件拷贝到fileRealPathDirectory目录下//对上传的文件名进行处理, 前面增加一个前缀, 保证是唯一的即可. 防止文件名重复造成覆盖//构建了一个上传的文件的完整路径[目录+文件名]name = UUID.randomUUID().toString() + "_" + System.currentTimeMillis() + "_" + name;String fileFullPath = fileRealPathDirectory + "\\" + name;//保存fileItem.write(new File(fileFullPath));//关闭流fileItem.getOutputStream().close();//更新家居图的图片furn.setImagePath(WebUtils.FURN_IMG_DIRECTORY + name); }
全部代码
protected void update(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {//将提交修改的家居信息,封装成Furn对象//如果你的表单是enctype="multipart/form-data", req.getParameter("id") 得不到idint id = DataUtils.parseInt(req.getParameter("id"), 0);//获取到对应furn对象[从db中获取]Furn furn = furnService.queryFurnById(id);//todo 如果furn为null, 则return//1.判断是不是文件表单if (ServletFileUpload.isMultipartContent(req)) {//2.创建DiskFileItemFactory对象, 用于构建一个解析上传数据的工具对象DiskFileItemFactory diskFileItemFactory = new DiskFileItemFactory();//3.构建一个解析上传数据的工具对象ServletFileUpload servletFileUpload = new ServletFileUpload(diskFileItemFactory);//解决中文乱码问题servletFileUpload.setHeaderEncoding("utf-8");//4.servletFileUpload对象可以把表单提交的数据[文本/文件], 封装到FileItem文件项中try {List<FileItem> list = servletFileUpload.parseRequest(req);for (FileItem fileItem : list) {//判断是不是一个文件 => 文本表单字段if (fileItem.isFormField()) {将提交的家居信息, 封装成Furn对象switch (fileItem.getFieldName()) {case "name"://家居名furn.setName(fileItem.getString("utf-8"));break;case "business"://制造商furn.setBusiness(fileItem.getString("utf-8"));break;case "price"://价格furn.setPrice(new BigDecimal(fileItem.getString()));break;case "saleNum"://销量furn.setSaleNum(Integer.parseInt(fileItem.getString()));break;case "inventory"://库存furn.setInventory(Integer.parseInt(fileItem.getString()));break;}} else {//文件表单字段 => 获取上传的文件的名字String name = fileItem.getName();//如果用户没有选择新的图片, name = ""if (!"".equals(name)) {//1.把上传到到服务器 temp目录下的文件保存到指定的目录String filePath = "/" + WebUtils.FURN_IMG_DIRECTORY;//2.获取完整的目录String fileRealPath = req.getServletContext().getRealPath(filePath);System.out.println("fileRealPath= " + fileRealPath);//3.创建这个上传的目录File fileRealPathDirectory = new File(fileRealPath);if (!fileRealPathDirectory.exists()) {fileRealPathDirectory.mkdirs();}//4.将文件拷贝到fileRealPathDirectory目录下//对上传的文件名进行处理, 前面增加一个前缀, 保证是唯一的即可. 防止文件名重复造成覆盖//构建了一个上传的文件的完整路径[目录+文件名]name = UUID.randomUUID().toString() + "_" + System.currentTimeMillis() + "_" + name;String fileFullPath = fileRealPathDirectory + "\\" + name;//保存fileItem.write(new File(fileFullPath));//关闭流fileItem.getOutputStream().close();//更新家居图的图片furn.setImagePath(WebUtils.FURN_IMG_DIRECTORY + name);}}}//跟新furn对象->DBfurnService.updateFurn(furn);System.out.println("更新成功...");//请求转发到 update_ok.jspreq.getRequestDispatcher("/views/manage/update_ok.jsp").forward(req, resp);} catch (Exception e) {throw new RuntimeException(e);}} }
将checkout.jsp复制成update_ok.jsp
<a class="active" href="manage/furnServlet?action=page&pageNo=${param.pageNo}"><h4>家居修改成功, 点击返回家居管理页面</h4> </a>
🌈作业布置
🍍会员登陆后不能访问后台管理
需求分析
- 管理员admin登陆后, 可访问所有页面
- 会员登陆后, 不能访问后台管理相关页面, 其他页面可以访问
- 假定管理员名字就是admin, 其它会员名就是普通会员
AuthFilter - 代码
@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {System.out.println("请求/cartServlet 被拦截...");HttpServletRequest request = (HttpServletRequest) servletRequest;//得到请求的urlString url = request.getServletPath();System.out.println("url= " + url);//判断是否要验证if (!excludedUrls.contains(url)) {//获取到登陆的member对象Member member = (Member) request.getSession().getAttribute("member");if (member == null) {//说明用户没有登录if (!WebUtils.isAjaxRequest(request)) {//如果不是ajax请求//转发到登陆页面, 转发不走过滤器servletRequest.getRequestDispatcher("/views/member/login.jsp").forward(servletRequest, servletResponse);} else {//如果是ajax请求//返回ajax请求, 按照json格式返回 {"url": url}//1.构建mapMap<Object, Object> map = new HashMap<>();map.put("url", "views/member/login.jsp");//2.转成json字符串String resultJson = new Gson().toJson(map);//3.返回servletResponse.getWriter().print(resultJson);}return;//直接返回}//如果member不为空if ("admin".equals(member.getUsername())) {//管理员登陆//全部放行} else {//普通用户登录, 部分页面不能放行//如果该用户不是admin, 但是它访问了后台, 就转到管理员登录页面//if ("/manage/furnServlet".equals(url) || url.contains("/views/manage/")) {//.* 匹配任意个字符if ("/manage/furnServlet".equals(url) || url.matches("^/views/manage/.*")) {request.getRequestDispatcher("/views/manage/manage_login.jsp").forward(servletRequest, servletResponse);}}}//如果请求的是登录页面, 那么就放行filterChain.doFilter(servletRequest, servletResponse);System.out.println("请求/cartServlet验证通过, 放行");}
🍍解决图片冗余问题
需求分析
- 家居图片都放在一个文件夹, 会越来越多
- 请尝试在assets/images/product-image/目录下, 自动创建年月日目录, 比如20230612. 以天为单位来存放上传图片
- 当上传新家居的图片, 原来的图片就没有用了, 应当删除原来的家居图片
工具类添加方法 - 返回当前日期
public static String getYearMonthDay() {//第三代日期类LocalDateTime now = LocalDateTime.now();int year = now.getYear();int month = now.getMonthValue();int day = now.getDayOfMonth();String date = year + "/" + month + "/" + day + "/";return date;}
🍍分页导航完善
需求分析
- 如果总页数<=5, 就全部显示
- 如果总页数>5, 按照如下规则显示(这个规则由程序员/业务来决定)
2.1 如果当前页是前3页, 就显示1-5
2.2 如果当前页是后3页, 就显示最后5页
2.3 如果当前页是中间页, 就显示 当前页前2页, 当前页, 当前页后2页
代码实现
<c:choose><%--如果总页数<=5, 就全部显示--%><c:when test="${requestScope.page.pageTotal <= 5}"><c:set scope="page" var="begin" value="1"></c:set><c:set scope="page" var="end" value="${requestScope.page.pageTotal}"></c:set></c:when><%--如果总页数>5, 按照如下规则显示(这个规则由程序员/业务来决定)--%><c:when test="${requestScope.page.pageTotal > 5}"><c:choose><%--如果当前页是前3页, 就显示1-5--%><c:when test="${requestScope.page.pageNo <= 3}"><c:set scope="page" var="begin" value="1"></c:set><c:set scope="page" var="end" value="5"></c:set></c:when><%--如果当前页是后3页, 就显示最后5页--%><c:when test="${requestScope.page.pageNo > requestScope.page.pageTotal - 3}"><c:set scope="page" var="begin" value="${requestScope.page.pageTotal - 4}"></c:set><c:set scope="page" var="end" value="${requestScope.page.pageTotal}"></c:set></c:when><%--如果当前页是中间页, 就显示 当前页前2页, 当前页, 当前页后2页--%><c:otherwise><c:set scope="page" var="begin" value="${requestScope.page.pageNo - 2}"></c:set><c:set scope="page" var="end" value="${requestScope.page.pageNo + 2}"></c:set></c:otherwise></c:choose></c:when>
</c:choose>
🐀🐂🐅🐇🐉🐍🐎🐏