项目实战系列四:【家居购项目 (旧版) 】

文章目录

  • Java后端经典三层架构
    • MVC模型
    • 开发环境搭建
    • 会员注册
      • 前端验证用户注册信息
      • 思路分析
        • 创建表
        • 创建实体类
        • DAO
          • MemberDAOImpl
        • Service
          • MemberServiceImpl
        • 接通web层
    • 会员登陆
      • 登陆错误_信息回显
    • servlet合并
      • 反射+模板设计模式+动态代理
      • 显示家居
      • 添加家居
          • 解决重复添加
          • 后端数据校验说明
          • BeanUtils自动封装Bean
      • 删除家居
      • 修改家具
      • 后台分页
        • 新建Page类
        • DAO
        • Service
        • web层获取page对象
        • 前端页面
          • 后台分页导航
          • 修改后返回原页面
          • 删除后返回原页面
          • 添加后返回原页面
      • 首页分页
          • 首页搜索
          • 两个奇怪的问题
      • 会员显示登录名
          • 注销登录
          • 验证码
      • 购物车
          • 显示购物车
          • 修改购物车
          • 删除购物车
      • 生产订单
        • 创建表
        • 实体类
        • DAO
        • service
        • servlet
        • 前端
      • 显示订单[订单管理]
      • 过滤器权限验证
      • 事务管理
          • 1. 数据不一致问题
          • 2. 程序框架图
          • Transaction过滤器
      • 统一错误页面
      • Ajax检验注册名
      • Ajax添加购物车
      • 上传与更新家具图片
      • 作业布置
        • 会员登陆后不能访问后台管理
        • 解决图片冗余问题
        • 分页导航完善

项目实战系列四:【家居购项目 (旧版) 】_第1张图片

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)

在这里插入图片描述
在这里插入图片描述

解读

  1. model 最早期就是javabean, 就是早期的jsp+servlet+javabean
  2. 后面业务复杂度越来越高, model逐渐分层化/组件化(service+dao)
  3. 后面又出现了持久化技术(service+dao+持久化技术(hibernate / mybatis / mybatis-plus))
  4. MVC依然是原来的mvc, 只是变得更加强大

开发环境搭建

详情请参考

  1. 新建Java项目, 导入web框架项目实战系列四:【家居购项目 (旧版) 】_第2张图片
  2. 导入jar包
    在这里插入图片描述
  3. 项目的结构
    在这里插入图片描述
    项目实战系列四:【家居购项目 (旧版) 】_第3张图片
  4. 拷贝到web路径下
    在这里插入图片描述
    项目实战系列四:【家居购项目 (旧版) 】_第4张图片
  5. 配置Tomcat
    Rebuild project, 让项目识别到这些资源, 然后再启动Tomcat
    在这里插入图片描述
  6. 对于复杂的前端页面, 要学会打开当前页面的结构, 提高工作效率
    项目实战系列四:【家居购项目 (旧版) 】_第5张图片
    在这里插入图片描述
    项目实战系列四:【家居购项目 (旧版) 】_第6张图片

会员注册

前端验证用户注册信息

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 工具类

创建表

项目实战系列四:【家居购项目 (旧版) 】_第7张图片

创建实体类

满汉楼项目
包括无参构造器和set方法. 如果添加有参构造器, 记得书写无参构造器

  1. 从满汉楼项目引入BasicDAO.java, JdbcUtilsByDruid.java, Druid.properties
  2. 修改Druid配置文件要连接的数据库名, 确保用户名密码正确. ?后面是做批处理用的
    项目实战系列四:【家居购项目 (旧版) 】_第8张图片
  3. 修改JdbcUtilsByDruid的路径
    在这里插入图片描述
    配置快捷键
    项目实战系列四:【家居购项目 (旧版) 】_第9张图片
    在这里插入图片描述
  4. 测试
    在这里插入图片描述

DAO

在这里插入图片描述

MemberDAOImpl
public class MemberDAOImpl extends BasicDAO<Member> implements MemberDAO {
   /**
    * 通过用户名返回对应的Member
    * @param username 用户名
    * @return 对应的Member, 如果没有该Member返回null
    */
   @Override
   public Member queryMemberByUsername(String username) {
       //现在sqlyog测试, 然后再拿到程序中, 这样可以提高我们的开发效率, 减少不必要的bug
       String sql = "SELECT id, username, `password`, email FROM member WHERE username = ?";
       Member member = querySingle(sql, Member.class, username);
       return member;
   }

   /**
    * 保存一个会员
    * @param member 传入一个Member对象
    * @return 如果返回-1, 就是失败; 返回其它的数字, 就是受影响的行数
    */
   @Override
   public 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

项目实战系列四:【家居购项目 (旧版) 】_第10张图片

MemberServiceImpl
public class MemberServiceImpl implements MemberService {
   //定义MemberDAO属性
   private MemberDAO memberDAO = new MemberDAOImpl();

   /**
    * 判断用户名是否存在
    *
    * @param username 用户名
    * @return 如果存在返回true, 否则返回false
    */
   @Override
   public boolean isExistsByUsername(String username) {
       //小技巧: 如果看某个方法:
       // (1)ctrl+b 定位到memberDAO的编译类型中的方法
       // (2)如果使用ctrl+alt+b 会定位到实现类的方法
       //如果有多个类实现了该方法, 会让你选择
       return memberDAO.queryMemberByUsername(username) == null ? false : true;
   }

   @Override
   public boolean registerMember(Member member) {
       return memberDAO.saveMember(member) == 1 ? true : false;
   }
}

项目实战系列四:【家居购项目 (旧版) 】_第11张图片
测试
在这里插入图片描述

接通web层

项目实战系列四:【家居购项目 (旧版) 】_第12张图片
将所有路径修改成相对路径
项目实战系列四:【家居购项目 (旧版) 】_第13张图片
在这里插入图片描述
配置RegisterServlet, 请求RegisterServlet
项目实战系列四:【家居购项目 (旧版) 】_第14张图片

会员登陆

MemberDAO
项目实战系列四:【家居购项目 (旧版) 】_第15张图片
项目实战系列四:【家居购项目 (旧版) 】_第16张图片
MemberDAOImpl
在这里插入图片描述
测试(不要忘了测试)
在这里插入图片描述
快捷键
在这里插入图片描述
在这里插入图片描述

MemberService
项目实战系列四:【家居购项目 (旧版) 】_第17张图片
在这里插入图片描述
测试(不要忘了测试)
项目实战系列四:【家居购项目 (旧版) 】_第18张图片

web层

  1. 新建LoginServlet
    项目实战系列四:【家居购项目 (旧版) 】_第19张图片
    login.html请求
    项目实战系列四:【家居购项目 (旧版) 】_第20张图片
    login_ok.html
    项目实战系列四:【家居购项目 (旧版) 】_第21张图片
    快捷键
    项目实战系列四:【家居购项目 (旧版) 】_第22张图片
    效果
    在这里插入图片描述
    项目实战系列四:【家居购项目 (旧版) 】_第23张图片

登陆错误_信息回显

将login.html重命名为login.jsp, 修改base标签

LoginServlet
项目实战系列四:【家居购项目 (旧版) 】_第24张图片
login.jsp
项目实战系列四:【家居购项目 (旧版) 】_第25张图片
添加span标签
项目实战系列四:【家居购项目 (旧版) 】_第26张图片

servlet合并

方法一: 增加隐藏域
在这里插入图片描述
合并到MemberServlet
在这里插入图片描述

反射+模板设计模式+动态代理

项目实战系列四:【家居购项目 (旧版) 】_第27张图片
项目实战系列四:【家居购项目 (旧版) 】_第28张图片

显示家居

需求分析

  1. 给后台管理提供独立登陆页面 manage_login.jsp(已提供)
  2. 管理员(admin表)登陆成功后, 显示管理菜单页面
  3. 管理员点击家具管理, 显示所有家居信息

程序框架图
项目实战系列四:【家居购项目 (旧版) 】_第29张图片

  1. 页面准备在这里插入图片描述在这里插入图片描述
  2. 新建admin表 参考member表
    新建furn表
    项目实战系列四:【家居购项目 (旧版) 】_第30张图片
  3. 新建Admin实体类
    新建Furn实体类
    项目实战系列四:【家居购项目 (旧版) 】_第31张图片
  4. 书写AdminDAO, AdminDAOImpl, 并测试; 书写AdminService, AdminServiceImpl, 并测试 参考Member
    书写FurnDAO, FurnDAOImpl 并测试
    项目实战系列四:【家居购项目 (旧版) 】_第32张图片
  5. 书写FurnService, FurnServiceImpl 并测试
    项目实战系列四:【家居购项目 (旧版) 】_第33张图片
  6. 接通web层
    配置web.xml, 书写AdminServlet
    项目实战系列四:【家居购项目 (旧版) 】_第34张图片
    配置web.xml, 书写FurnServlet
    在这里插入图片描述
    项目实战系列四:【家居购项目 (旧版) 】_第35张图片
    将doGet()方法移到BasicServlet中
    项目实战系列四:【家居购项目 (旧版) 】_第36张图片
  7. 前端页面
    manage_login.jsp 登录验证, 请求AdminServlet
    项目实战系列四:【家居购项目 (旧版) 】_第37张图片
    项目实战系列四:【家居购项目 (旧版) 】_第38张图片
    manage_menu.jsp 请求FurnServlet
    项目实战系列四:【家居购项目 (旧版) 】_第39张图片
    furn_manage.jsp 显示家居信息
    项目实战系列四:【家居购项目 (旧版) 】_第40张图片

添加家居

思路分析

  1. 请求添加家居, 请求FurnServlet的add方法, 将前端提交的数据封装到Furn对象
  2. 调用FurnService.add(Furn furn)方法
  3. 跳转到显示家居的页面
  1. FurnDAO
    在这里插入图片描述
  2. FurnService
    在这里插入图片描述
  3. web层
    FurnServlet
    项目实战系列四:【家居购项目 (旧版) 】_第41张图片
    解决中文乱码问题
    项目实战系列四:【家居购项目 (旧版) 】_第42张图片
  4. 前端: 添加furn_add.jsp
    项目实战系列四:【家居购项目 (旧版) 】_第43张图片
    项目实战系列四:【家居购项目 (旧版) 】_第44张图片
    项目实战系列四:【家居购项目 (旧版) 】_第45张图片
    项目实战系列四:【家居购项目 (旧版) 】_第46张图片
    在这里插入图片描述
解决重复添加

请求转发, 当用户刷新页面时, 会重新发出第一次的请求, 造成数据重复提交
在这里插入图片描述
解决方案: 使用重定向
项目实战系列四:【家居购项目 (旧版) 】_第47张图片
项目实战系列四:【家居购项目 (旧版) 】_第48张图片

后端数据校验说明

后端方案一
项目实战系列四:【家居购项目 (旧版) 】_第49张图片后端方案二
项目实战系列四:【家居购项目 (旧版) 】_第50张图片
前端方案三
项目实战系列四:【家居购项目 (旧版) 】_第51张图片

BeanUtils自动封装Bean

引入: commons-logging-1.1.1.jar, commons-beanutils-1.8.0.jar

  1. 使用BeanUtils自动封装javabean
    项目实战系列四:【家居购项目 (旧版) 】_第52张图片debug小技巧
    在这里插入图片描述
  2. 报错
    原因: 由于前端没有传imagePath的字段, 所有后端在构建的furn对象的时候, imagePath传了个null,
    解决方案
    项目实战系列四:【家居购项目 (旧版) 】_第53张图片
  3. 将 把数据自动封装成JavaBean的功能封装到工具类
public class DataUtils {
   //将方法, 封装到静态方法, 方便使用
   public static  T copyParamToBean(Map value, T bean) {
       try {
           BeanUtils.populate(bean, value);
       } catch (Exception e) {
           throw new RuntimeException(e);
       }
       return bean;
   }
}

调用
项目实战系列四:【家居购项目 (旧版) 】_第54张图片

删除家居

需求分析

  1. 管理员进入到家居管理页面
  2. 点击删除家居链接, 弹出确认窗口, 确认-删除, 取消-放弃

程序框架图
项目实战系列四:【家居购项目 (旧版) 】_第55张图片

  1. FurnDAO
    在这里插入图片描述
  2. FurnService
    项目实战系列四:【家居购项目 (旧版) 】_第56张图片
  3. web层
    FurnServlet
    项目实战系列四:【家居购项目 (旧版) 】_第57张图片
  4. furn_add.jsp页面
    jQuery操作父元素, 兄弟元素, 子元素, 请移步
    js弹框请移步
    项目实战系列四:【家居购项目 (旧版) 】_第58张图片
    项目实战系列四:【家居购项目 (旧版) 】_第59张图片

修改家具

思路分析

  1. 管理员进入家居管理页面furn_manage.jsp
  2. 点击修改家居链接, 回显该家居信息 [furn_update.jsp]
  3. 填写新的信息, 点击修改家居按钮
  4. 修改成功后, 显示刷新后的家居列表

程序框架图
在这里插入图片描述

  1. FurnDAO
    项目实战系列四:【家居购项目 (旧版) 】_第60张图片
    在这里插入图片描述
  2. FurnService
    项目实战系列四:【家居购项目 (旧版) 】_第61张图片
    项目实战系列四:【家居购项目 (旧版) 】_第62张图片
  3. web层
    FurnServlet
    项目实战系列四:【家居购项目 (旧版) 】_第63张图片
    项目实战系列四:【家居购项目 (旧版) 】_第64张图片
  4. 前端
    furn_manage.jsp 点击修改,发出请求
    项目实战系列四:【家居购项目 (旧版) 】_第65张图片
    furn_update.jsp 修改数据,点击提交
    项目实战系列四:【家居购项目 (旧版) 】_第66张图片
    项目实战系列四:【家居购项目 (旧版) 】_第67张图片
    在这里插入图片描述
    项目实战系列四:【家居购项目 (旧版) 】_第68张图片

后台分页

shortcuts: ctrl+alt+u在局部打开类图
程序框架图

新建Page类

在这里插入图片描述

DAO

思路
在这里插入图片描述
实现
在这里插入图片描述
在这里插入图片描述

Service

在这里插入图片描述
在这里插入图片描述

web层获取page对象

项目实战系列四:【家居购项目 (旧版) 】_第69张图片

前端页面

取缔list方法
项目实战系列四:【家居购项目 (旧版) 】_第70张图片
在这里插入图片描述
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 -->
修改后返回原页面

项目实战系列四:【家居购项目 (旧版) 】_第71张图片
项目实战系列四:【家居购项目 (旧版) 】_第72张图片
项目实战系列四:【家居购项目 (旧版) 】_第73张图片
在这里插入图片描述

删除后返回原页面

在这里插入图片描述
项目实战系列四:【家居购项目 (旧版) 】_第74张图片

添加后返回原页面

在这里插入图片描述
在这里插入图片描述
项目实战系列四:【家居购项目 (旧版) 】_第75张图片

首页分页

需求分析

  1. 顾客进入首页页面
  2. 分页显示家居
  3. 正确显示分页导航条, 即功能完善, 可以使用

程序框架图
项目实战系列四:【家居购项目 (旧版) 】_第76张图片

实现>1. 新建CustomerFurnServlet
项目实战系列四:【家居购项目 (旧版) 】_第77张图片
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"><i
                           class="icon-size-fullscreen"></i></a>
               </div>
               <button title="Add To Cart" class=" add-to-cart">Add
                   To 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 -->
首页搜索

需求分析

  1. 顾客进入首页页面
  2. 点击搜索按钮, 可以输入家居名
  3. 正确显示分页导航条, 并且要求在分页时, 保留上次搜索条件

程序框架图
项目实战系列四:【家居购项目 (旧版) 】_第78张图片

  1. DAO
    模糊查询
    项目实战系列四:【家居购项目 (旧版) 】_第79张图片
    在这里插入图片描述
  2. service
    在这里插入图片描述
    在这里插入图片描述
  3. web层 CustomerFurnServlet
    page方法就被抛弃了
    项目实战系列四:【家居购项目 (旧版) 】_第80张图片
  4. 前端 index.jsp
    项目实战系列四:【家居购项目 (旧版) 】_第81张图片
    项目实战系列四:【家居购项目 (旧版) 】_第82张图片
两个奇怪的问题
  1. 点击家居管理, 发出两个请求
    项目实战系列四:【家居购项目 (旧版) 】_第83张图片
    项目实战系列四:【家居购项目 (旧版) 】_第84张图片
    在这里插入图片描述
    抓包
    项目实战系列四:【家居购项目 (旧版) 】_第85张图片
    原因
    项目实战系列四:【家居购项目 (旧版) 】_第86张图片
    请求首页面即进入到indx.jsp, index.jsp又请求转发到CustomerFurnServlet
    在这里插入图片描述
    问题解决
  2. 首页分页出现问题
    在这里插入图片描述
    项目实战系列四:【家居购项目 (旧版) 】_第87张图片
    原因
    在这里插入图片描述

会员显示登录名

需求分析

  1. 会员登陆成功, login_ok.jsp显示登录信息
  2. 如果登陆成功后返回首页面, 显示订单管理和安全退出
  3. 如果用户没有登陆过, 首页就显示登录和注册超链接

程序框架图
在这里插入图片描述

实现
重命名时, 会联动修改项目实战系列四:【家居购项目 (旧版) 】_第88张图片项目实战系列四:【家居购项目 (旧版) 】_第89张图片
将login_ok.jsp中的index.html改成index.jsp. 注意, 不要改成views/customer/index.jsp项目实战系列四:【家居购项目 (旧版) 】_第90张图片项目实战系列四:【家居购项目 (旧版) 】_第91张图片项目实战系列四:【家居购项目 (旧版) 】_第92张图片
项目实战系列四:【家居购项目 (旧版) 】_第93张图片
项目实战系列四:【家居购项目 (旧版) 】_第94张图片

注销登录

思路分析

  1. 用户登录成功后
  2. login_ok.jsp, 点击安全退出, 注销登录
  3. 返回首页, 也可点击安全退出, 注销登录

程序框架图
项目实战系列四:【家居购项目 (旧版) 】_第95张图片
实现
项目实战系列四:【家居购项目 (旧版) 】_第96张图片
项目实战系列四:【家居购项目 (旧版) 】_第97张图片项目实战系列四:【家居购项目 (旧版) 】_第98张图片

验证码

程序框架图
项目实战系列四:【家居购项目 (旧版) 】_第99张图片

  1. web层
    KaptchaServlet -> 引入kaptcha-2.3.2.jar包, 在web.xml中配置KaptchaServlet
    在这里插入图片描述项目实战系列四:【家居购项目 (旧版) 】_第100张图片
    项目实战系列四:【家居购项目 (旧版) 】_第101张图片
    MemberServlet
    项目实战系列四:【家居购项目 (旧版) 】_第102张图片
    项目实战系列四:【家居购项目 (旧版) 】_第103张图片
  2. 前端页面
    login.jsp
    项目实战系列四:【家居购项目 (旧版) 】_第104张图片
    验证码不能为空
    项目实战系列四:【家居购项目 (旧版) 】_第105张图片点击图片更换验证码
    项目实战系列四:【家居购项目 (旧版) 】_第106张图片
    将register_ok.html, register_fail.html改造成jsp页面
    login.jsp页面总是默认停留在会员登录的div内, 修改
    项目实战系列四:【家居购项目 (旧版) 】_第107张图片
    项目实战系列四:【家居购项目 (旧版) 】_第108张图片
    项目实战系列四:【家居购项目 (旧版) 】_第109张图片
    项目实战系列四:【家居购项目 (旧版) 】_第110张图片注册回显信息
    项目实战系列四:【家居购项目 (旧版) 】_第111张图片项目实战系列四:【家居购项目 (旧版) 】_第112张图片

购物车

程序框架图
在这里插入图片描述

cartItem模型
项目实战系列四:【家居购项目 (旧版) 】_第113张图片

Cart数据模型
项目实战系列四:【家居购项目 (旧版) 】_第114张图片
项目实战系列四:【家居购项目 (旧版) 】_第115张图片
测试
项目实战系列四:【家居购项目 (旧版) 】_第116张图片

实现

  1. 创建CartServlet
    项目实战系列四:【家居购项目 (旧版) 】_第117张图片
  2. 首页获取id请求后台
    在这里插入图片描述
    项目实战系列四:【家居购项目 (旧版) 】_第118张图片
  3. 首页购买的商品总数量项目实战系列四:【家居购项目 (旧版) 】_第119张图片
    项目实战系列四:【家居购项目 (旧版) 】_第120张图片
显示购物车

需求分析

  1. 查看购物车, 可以显示如下信息
  2. 选中了哪些家居, 名称, 数量, 金额
  3. 统计购物车共多少商品, 总价多少

程序框架图
在这里插入图片描述

  1. 走通购物车
    cart.jsp
    项目实战系列四:【家居购项目 (旧版) 】_第121张图片
    index.jsp跳转
    排错
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    定位
    项目实战系列四:【家居购项目 (旧版) 】_第122张图片
    项目实战系列四:【家居购项目 (旧版) 】_第123张图片
    页面源代码
    项目实战系列四:【家居购项目 (旧版) 】_第124张图片
  2. 显示家居项
<tbody>
<%--找到显示购物车项,  进行循环的items--%>
<c:if test="${not empty sessionScope.cart.items}">
   <%--
       1.sessionScope.cart.items => 取出的是HashMap<Integer, CartItem>
       2.所以通过foreach标签取出的每一个对象, 即entry是 HashMap<Integer, CartItem>的 k-v
       3.var其实就是 entry
       4.所以要取出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}"><i
                       class="icon-close"></i></a>
           </td>
       </tr>
   </c:forEach>
</c:if>
</tbody>
  1. 计算总价
    项目实战系列四:【家居购项目 (旧版) 】_第125张图片
    项目实战系列四:【家居购项目 (旧版) 】_第126张图片
修改购物车

需求分析

  1. 进入购物车, 可以修改购买数量
  2. 更新该商品项的金额
  3. 跟新购物车商品数量和总金额

程序框架图
在这里插入图片描述

  1. Cart增加方法
    项目实战系列四:【家居购项目 (旧版) 】_第127张图片
  2. CartServlet
    项目实战系列四:【家居购项目 (旧版) 】_第128张图片
  3. 前端
    项目实战系列四:【家居购项目 (旧版) 】_第129张图片
    在这里插入图片描述
    在这里插入图片描述
    项目实战系列四:【家居购项目 (旧版) 】_第130张图片
    在这里插入图片描述
    cart.jsp
    在这里插入图片描述
    项目实战系列四:【家居购项目 (旧版) 】_第131张图片
删除购物车

需求分析

  1. 进入购物车, 可以删除某商品
  2. 可以清空购物车
  3. 要求给出适当的确认信息

程序框架图
项目实战系列四:【家居购项目 (旧版) 】_第132张图片

  1. 删除购物车
    项目实战系列四:【家居购项目 (旧版) 】_第133张图片
    项目实战系列四:【家居购项目 (旧版) 】_第134张图片
  2. 清空购物车
    项目实战系列四:【家居购项目 (旧版) 】_第135张图片
    项目实战系列四:【家居购项目 (旧版) 】_第136张图片

生产订单

需求分析

  1. 进入购物车, 点击购物车结账
  2. 生成订单和订单项
  3. 如果会员没有登陆, 先进入登陆页面, 完成登陆后再结账

程序框架图

创建表

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
项目实战系列四:【家居购项目 (旧版) 】_第137张图片
OrderItemDAO

项目实战系列四:【家居购项目 (旧版) 】_第138张图片

service

项目实战系列四:【家居购项目 (旧版) 】_第139张图片

public class OrderServiceImpl implements OrderService {
    private OrderDAO orderDAO = new OrderDAOImpl();
    private OrderItemDAO orderItemDAO = new OrderItemDAOImpl();
    private FurnDAO furnDAO = new FurnDAOImpl();
    //在这里可以感受到javaee分层的好处. 在service层, 通过组合多个dao的方法,
    // 完成某个业务 慢慢体会好处
    
    @Override
    public String saveOrder(Cart cart, int memberId) {
        //将cart购物车的数据以order和orderItem的形式保存到DB中

        //因为生成订单会操作多张表, 因此会涉及到多表事务的问题, ThreadLocal+Mysql事务机制+过滤器

        //1.通过cart对象, 构建一个对应的order对象
        //  先生成一个UUID, 表示当前的订单号, UUID是唯一的
        String orderId = UUID.randomUUID().toString();//订单id
        Order 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;
    }
}

项目实战系列四:【家居购项目 (旧版) 】_第140张图片

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.jsp
        response.sendRedirect(request.getContextPath() + "/views/order/checkout.jsp");
    }
}

项目实战系列四:【家居购项目 (旧版) 】_第141张图片

  • 防止生成空订单
    hashMap.clear之后, 是置空还是size置为0
    项目实战系列四:【家居购项目 (旧版) 】_第142张图片
    项目实战系列四:【家居购项目 (旧版) 】_第143张图片

前端

项目实战系列四:【家居购项目 (旧版) 】_第144张图片

checkout.html修改为checkout.jsp
项目实战系列四:【家居购项目 (旧版) 】_第145张图片
项目实战系列四:【家居购项目 (旧版) 】_第146张图片
项目实战系列四:【家居购项目 (旧版) 】_第147张图片

显示订单[订单管理]

  • 添加购物车按钮动态处理

需求分析

  1. 如果某家居库存为0, 前台的"Add to Cart" 按钮显示为"暂时缺货"
  2. 后台也加上校验. 只有在 库存>0时, 才能添加到购物车

在这里插入图片描述

思路分析

  1. 首页添加家居到购物车时, 加以限制
  2. 购物车里, 更新家居数量时,加以限制

项目实战系列四:【家居购项目 (旧版) 】_第148张图片
项目实战系列四:【家居购项目 (旧版) 】_第149张图片
项目实战系列四:【家居购项目 (旧版) 】_第150张图片

  • 管理订单

需求分析

  1. 完成订单管理-查看
  2. 具体流程参考显示家居
  3. 静态页面order.html 和 order_detail.html 已提供

程序框架图
在这里插入图片描述

  1. DAO
    在这里插入图片描述
  2. service
    项目实战系列四:【家居购项目 (旧版) 】_第151张图片
  3. web层
    项目实战系列四:【家居购项目 (旧版) 】_第152张图片
  4. 前端
    index.jsp, cart.jsp, login_ok.jsp, checkout.jsp, order.jsp均可跳转到订单管理
    在这里插入图片描述
    项目实战系列四:【家居购项目 (旧版) 】_第153张图片
    项目实战系列四:【家居购项目 (旧版) 】_第154张图片
  • 管理订单项

程序框架图
项目实战系列四:【家居购项目 (旧版) 】_第155张图片

  1. DAO
    项目实战系列四:【家居购项目 (旧版) 】_第156张图片
    项目实战系列四:【家居购项目 (旧版) 】_第157张图片
  2. service
    项目实战系列四:【家居购项目 (旧版) 】_第158张图片
    项目实战系列四:【家居购项目 (旧版) 】_第159张图片
  3. web层
    在Order实体类中新增count属性, 在生成订单时, 将totalCount赋给count
    项目实战系列四:【家居购项目 (旧版) 】_第160张图片
  4. 前端
    order.jsp跳转到OrderItemServlet
    项目实战系列四:【家居购项目 (旧版) 】_第161张图片
    order_detail.jsp
    项目实战系列四:【家居购项目 (旧版) 】_第162张图片
    项目实战系列四:【家居购项目 (旧版) 】_第163张图片
    项目实战系列四:【家居购项目 (旧版) 】_第164张图片
    order表增加count列 - ALTER TABLE order ADD COUNT INT UNSIGNED NOT NULL AFTER price;
    项目实战系列四:【家居购项目 (旧版) 】_第165张图片
    项目实战系列四:【家居购项目 (旧版) 】_第166张图片
    项目实战系列四:【家居购项目 (旧版) 】_第167张图片
    项目实战系列四:【家居购项目 (旧版) 】_第168张图片

过滤器权限验证

需求分析

  1. 加入过滤器权限验证
  2. 如果没有登陆, 查看购物车和添加到购物车, 就会自动转到会员登陆页面
  1. 配置拦截url

<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>AuthFilterfilter-name>
       <filter-class>com.zzw.furns.filter.AuthFilterfilter-class>
       <init-param>
       	   
           <param-name>excludedUrlsparam-name>
           <param-value>/views/manage/manage_login.jsp,/views/member/login.jspparam-value>
       init-param>
   filter>
   <filter-mapping>
       <filter-name>AuthFilterfilter-name>
       
       <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>/cartServleturl-pattern>
       <url-pattern>/manage/furnServleturl-pattern>
       <url-pattern>/orderServleturl-pattern>
       <url-pattern>/orderItemServleturl-pattern>
   filter-mapping>
web-app>

2.过滤器逻辑判断

/**
* 这是用于权限验证的过滤器, 对指定的url进行验证
* 如果登陆过, 就放行; 如果没有登陆, 就回到登陆页面
* @author 赵志伟
* @version 1.0
*/
@SuppressWarnings({"all"})
public class AuthFilter implements Filter {

   private List<String> excludedUrls;

   @Override
   public void init(FilterConfig filterConfig) throws ServletException {
       //获取到配置的excludedUrls
       String strExcludedUrls = filterConfig.getInitParameter("excludedUrls");
       String[] split = strExcludedUrls.split(",");
       //将 splitUrl 转成 list
       excludedUrls = Arrays.asList(split);
       System.out.println("excludedUrls= " + excludedUrls);
   }

   @Override
   public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
       System.out.println("请求/cartServlet 被拦截...");
       HttpServletRequest request = (HttpServletRequest) servletRequest;

       //得到请求的url
       String 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验证通过, 放行");
   }

   @Override
   public void destroy() {

   }
}
  1. 处理管理员登陆
    项目实战系列四:【家居购项目 (旧版) 】_第169张图片
    项目实战系列四:【家居购项目 (旧版) 】_第170张图片

事务管理

1. 数据不一致问题
  1. 将FurnDAOImpl.java的updateFurn方法的sql故意写错
    [furnDAO.updateFurn(furn);由ctrl+alt+b定位到updateFurn的实现方法]
  2. 在OrderServiceImpl的saveOrder()方法内捕获一下异常, 目的是保证程序能够继续执行
  3. 查看数据库里的数据会有什么结果. 会出现数据不一致的问题.
    在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
我在首页购买了一个小台灯, 数据库中生成了对应的订单和订单项, 但家居表里该小台灯的销量和库存没有变化, 纹丝不动. 相当于客户下单了, 但没有给人家发货.
在这里插入图片描述

在这里插入图片描述

2. 程序框架图

思路分析

  1. 使用 Filter + ThreadLocal 来进行事务管理
  2. 说明: 在一次http请求中, servlet-service-dao 的调用过程, 始终是一个线程, 这是使用ThreadLocal的前提
  3. 使用ThreadLocal来确保所有dao操作都在同一个Connection内

程序框架图
在这里插入图片描述

  1. 修改JdbcUtilsByDruid工具类
public class JdbcUtilsByDruid {
   private static DataSource dataSource;
   //定义属性ThreadLocal, 这里存放一个Connection
   private static ThreadLocal<Connection> threadlocalConn = new ThreadLocal<>();

   /**
    * 从ThreadLocal获取connection, 从而保证在一个线程中
    * 获取的是同一个Connection
    */
   public static Connection getConnection() {
       Connection connection = threadlocalConn.get();
       if (connection == null) {//说明当前的threadlocal没有这个连接
           try {
               //就从数据库连接池中取出连接放入threadlocal
               connection = 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();
       }
   }
  1. 修改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);
       }
   }
  1. 控制层进行事务管理
    前提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过滤器

程序框架图
在这里插入图片描述

体会: 异常机制是可以参与业务逻辑的
在这里插入图片描述
在这里插入图片描述

  1. 在web.xml中配置
   <filter>
       <filter-name>TransactionFilterfilter-name>
       <filter-class>com.zzw.furns.filter.TransactionFilterfilter-class>
   filter>
   <filter-mapping>
       <filter-name>TransactionFilterfilter-name>
       
       <url-pattern>/*url-pattern>
   filter-mapping>
  1. 在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@38f54ed7
           declaredMethod.invoke(this, req, resp);
           System.out.println("this.getClass() = " + this.getClass());
       } catch (Exception e) {
           //将发生的异常, 继续throw
           throw new RuntimeException(e);
       }
  1. 在代码执行完毕后, 会运行到Transaction过滤器的后置代码, 在这里进行异常捕获, 如果发生异常, 则回滚.
   @Override
   public 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();
       }
   }

统一错误页面

需求分析

  1. 如果在访问/操作网站时, 出现了内部错误, 统一显示 500.jsp
  2. 如果访问/操作的页面/servlet不存在时, 统一显示 404.jsp

思路分析

  1. 发生错误/异常时, 将错误/异常抛给tomcat
  2. 在web.xml中配置不同错误显示的页面即可
  1. 引入404.html, 500.html, 修改成jsp文件
    页面首行<%@ page contentType="text/html;charset=UTF-8" language="java" %>
    base标签
    将跳转链接改成index.jsp

    您访问的页面不存在 返回首页

  2. web.xml配置



<error-page>
   <error-code>500error-code>
   <location>/views/error/500.jsplocation>
error-page>

<error-page>
   <error-code>404error-code>
   <location>/views/error/404.jsplocation>
error-page>
  1. 修改事务过滤器, 将异常抛给tomcat
   @Override
   public 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检验注册名

需求分析

  1. 注册会员时, 如果名字已经注册过, 当光标离开输入框, 提示会员名已经存在, 否则提示不存在
  2. 要求使用ajax完成

程序框架图
在这里插入图片描述

  1. MemberServlet - 返回json格式的字符串 - 方式一
protected void isExistByName(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
   //1.获取用户名
   String username = req.getParameter("username");
   //2.调用service
   boolean 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.调用service
   boolean 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", "[email protected]");
   //map.put("phone", "13031748275");

   //4.返回json格式的数据
   Gson gson = new Gson();
   String resultJson = gson.toJson(map);
   resp.getWriter().print(resultJson);
}
  1. 前端
$("#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检验验证码
  1. 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);
       }
   }
  1. 前端
    $("#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添加购物车

  1. 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, 不赋值默认值为null
        CartItem 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());
        //转成json
        Gson gson = new Gson();
        String resultJson = gson.toJson(map);
        //返回
        response.getWriter().print(resultJson);

        //String referer = request.getHeader("referer");
        //response.sendRedirect(referer);
    }
  1. 前端
            //给所有选定的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.header-action-num").text(data.cartTotalCount);
                    }
                )
            });
  1. 解决Ajax请求转发失败
    测试, 会发现针对ajax的重定向和请求转发会失败, 也就是AuthFilter.java的权限拦截不生效, 也就是点击Add to Cart, 后台服务没有响应

使用ajax向后台发送请求跳转页面无效的原因

  1. 主要是服务器得到的是ajax发送过来的request, 也就是说这个请求不是浏览器请求的, 而是ajax请求的. 所以servlet根据request进行请求转发或重定向都不能影响浏览器的跳转
  2. 解决方案: 如果想要实现跳转, 可以返回url给ajax, 在浏览器执行window.location(url);
    项目实战系列四:【家居购项目 (旧版) 】_第171张图片

工具类添加方法 - 判断请求是不是一个ajax请求

   /**
    * 判断请求是不是一个ajax请求
    * @param request
    * @return
    */
   public static boolean isAjaxRequest(HttpServletRequest request) {
       //X-Requested-With: XMLHttpRequest
       return "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.构建map
       Map<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.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

需求分析

  1. 后台修改家居, 可以点击图片, 选择新的图片
  2. 这里会用到文件上传功能

思路分析-程序框架图
在这里插入图片描述

  1. 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>
  1. 分析空指针异常
    将form表单改成文件表单

    点击修改家居
    在这里插入图片描述
    报错
    在这里插入图片描述
    将web.xml中500的错误提示配置注销掉, 将异常信息暴露出来
    项目实战系列四:【家居购项目 (旧版) 】_第172张图片
    再次点击修改家居信息, 报错信息显示出来, BasicServlet空指针异常
    所以有时候报错信息显示出来很重要
    在这里插入图片描述
    分析: 如果表单是enctype=“multipart/form-data”, 那么req.getParameter(“action”) 的方法得不到action值, 所以BasicServlet会报错
    在这里插入图片描述
    具体原因: req.getParameter(“action”)取不到form-data里的数据
    在这里插入图片描述
  2. 解决空指针异常
    解决方案: 将参数action, id, pageNo以url拼接的方式传参, BasicServlet便不会出错
    注意: post请求可以人为主动在地址中拼接参数,拼接的参数可以直接像get那样接收

    项目实战系列四:【家居购项目 (旧版) 】_第173张图片
  3. 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") 得不到id
   int 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对象->DB
           furnService.updateFurn(furn);
           System.out.println("更新成功...");
           //请求转发到 update_ok.jsp
           req.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>

作业布置

会员登陆后不能访问后台管理

需求分析

  1. 管理员admin登陆后, 可访问所有页面
  2. 会员登陆后, 不能访问后台管理相关页面, 其他页面可以访问
  3. 假定管理员名字就是admin, 其它会员名就是普通会员

AuthFilter - 代码

   @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("请求/cartServlet 被拦截...");
        HttpServletRequest request = (HttpServletRequest) servletRequest;

        //得到请求的url
        String 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.构建map
                    Map<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验证通过, 放行");
    }

解决图片冗余问题

需求分析

  1. 家居图片都放在一个文件夹, 会越来越多
  2. 请尝试在assets/images/product-image/目录下, 自动创建年月日目录, 比如20230612. 以天为单位来存放上传图片
  3. 当上传新家居的图片, 原来的图片就没有用了, 应当删除原来的家居图片

工具类添加方法 - 返回当前日期

   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;
   }

项目实战系列四:【家居购项目 (旧版) 】_第174张图片
在这里插入图片描述在这里插入图片描述

分页导航完善

需求分析

  1. 如果总页数<=5, 就全部显示
  2. 如果总页数>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>

你可能感兴趣的:(JavaWeb,java,tomcat,java-ee)