项目实战系列三:【家居购项目 (新版) 】

文章目录

  • 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后端经典三层架构

项目实战系列三:【家居购项目 (新版) 】_第2张图片

分层 对应包 说明
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)

项目实战系列三:【家居购项目 (新版) 】_第3张图片
项目实战系列三:【家居购项目 (新版) 】_第4张图片

解读

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

开发环境搭建

开发环境搭建具体参考

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

会员注册

思路分析

  1. 会员注册信息, 验证通过后
  2. 提交给服务器, 如果用户名在数据库中已经存在, 后踢给出提示信息, 并返回重新注册
  3. 如果用户名没有在数据库中, 完成注册, 并返回注册成功的页面

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

前端验证用户注册信息

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>
分层 对应包 说明
web层 RegisterServlet.java 接受浏览器发送数据; 调用相关的service;根据执行结果,返回页面数据
service层 MemberService.java Service接口包
MemberServiceImpl.java Service接口实现类
dao持久层 MemberDAO.java Dao接口包
MemberDAOImpl Dao接口实现类
实体bean对象 Member.java JavaBean类
工具类 JdbcUtilsByDruid.java 工具类

创建表

在这里插入图片描述

创建实体类

满汉楼项目
包括无参构造器和set方法. 如果添加有参构造器, 记得书写无参构造器
因为 一键生成实体类的工具是不会创建无参构造器的
get方法也要生成, 因为前端页面EL表达式是要调用get方法的
项目实战系列三:【家居购项目 (新版) 】_第10张图片

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

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

测试
项目实战系列三:【家居购项目 (新版) 】_第13张图片

Service

项目实战系列三:【家居购项目 (新版) 】_第14张图片

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

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

接通web层

配置RegisterServlet, 请求RegisterServlet

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    String username = request.getParameter("username");
    String password = request.getParameter("password");
    String email = request.getParameter("email");

    Member member = new Member(null, username, password, email);

    if (!memberService.isExistInDbByUsername(member)) {
        //用户名可用
        if (memberService.register(member)) {
            System.out.println("注册成功");
            request.getRequestDispatcher("/views/member/register_ok.jsp")
                    .forward(request, response);
        } else {
            System.out.println("注册失败");
            request.getRequestDispatcher("/views/member/register_fail.jsp")
                    .forward(request, response);
        }
    } else {
        //用户名不可用
        request.setAttribute("msg", "用户名" + username + "不可用");
        request.setAttribute("username", username);//回显用户名
        request.setAttribute("active", "register_tab");
        request.getRequestDispatcher("/views/member/login.jsp")
                .forward(request, response);
    }
}

前端(错误信息回显)

html页面转为jsp页面要做的处理
项目实战系列三:【家居购项目 (新版) 】_第16张图片
将路径修改成相对路径
项目实战系列三:【家居购项目 (新版) 】_第17张图片
如果有需要, 在页面顶部引入c标签
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

如果是html页面, base标签不能使用el表达式, 要这样写:
项目实战系列三:【家居购项目 (新版) 】_第18张图片

cart.jsp, checkout.jsp, order.jsp,order_detail.jsp均可跳转

<c:if test="${sessionScope.member != null}">
    <div class="header-bottom-set dropdown">
        欢迎: ${sessionScope.member.username}
    div>
    <div class="header-bottom-set dropdown">
        <a href="orderServlet?action=listByMemberId">订单管理a>
    div>
    <div class="header-bottom-set dropdown">
        <a href="memberServlet?action=logout">安全退出a>
    div>
c:if>

login.html改为jsp页面
loign.jsp - 注册表单


<span class="errorMsg"
      style="float: right; font-weight: bold; color: lightgray; font-size: 20pt; margin-left: 10px;">${msg}span>
<form action="registerServlet" method="post">
    <input type="text" id="username" name="username" value="${username}" placeholder="Username"/>
    <input type="password" id="password" name="password" placeholder="输入密码"/>
    <input type="password" id="rePwd" name="password2" placeholder="确认密码"/>
    <input name="email" id="email" placeholder="电子邮件" type="email"/>
    <input type="text" id="code" name="user-name" style="width: 50%" id="code"
           placeholder="验证码"/>  <img alt="" src="assets/images/code/code.bmp">
    <div class="button-box">
        <button type="submit" id="sub-btn"><span>会员注册span>button>
    div>
form>

loign.jsp - 注册失败回显信息时, 停留在注册的tab内

$(function () {
	//模拟一个点击事件, 选中注册
	//决定是显示登陆还是显示注册tab
	//如果注册失败, 显示注册tab, 而不能是默认的登录tab
    if (${requestScope.active == "register_tab"}) {
        $("#register_tab")[0].click();
    } else {
        $("#login_tab")[0].click();
    }
}
<a id="login_tab" data-bs-toggle="tab" href="#lg1">
    <h4>会员登录h4>
a>
<a id="register_tab" data-bs-toggle="tab" href="#lg2">
    <h4>会员注册h4>
a>

加入register_ok.jsp, register_fail.jsp页面

<a class="active"  href="index.jsp">
    <h4>注册成功, 返回首页h4>
a>
<a class="active"  href="views/member/login.jsp">
    <h4>注册失败, 重新注册h4>
a>

会员登陆

思路分析

  1. 输入用户名, 密码后提交
  2. 判断会员是否存在
  3. 会员存在于数据库, 显示登录成功页面
  4. 否则, 返回登陆页面, 重新登陆
  5. 要求改进登陆密码为md5加密

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

MemberDAO
在这里插入图片描述
在这里插入图片描述
MemberDAOImpl
项目实战系列三:【家居购项目 (新版) 】_第20张图片
测试(不要忘了测试)
项目实战系列三:【家居购项目 (新版) 】_第21张图片
快捷键
项目实战系列三:【家居购项目 (新版) 】_第22张图片
项目实战系列三:【家居购项目 (新版) 】_第23张图片

MemberService

public interface MemberService {
    /**
     * 根据传入的member信息, 返回对应在DB中的member对象
     * @param member 是根据用户登录构建一个member
     * @return 返回的是对应的DB中的member对象, 如果不存在返回null
     */
    public Member login(Member member);
}

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

    /**
     * 判断用户名和密码是否存在
     * @param username 用户名
     * @param password 密码
     * @return
     */
    @Override
    public Member login(Member member) {
        //返回一个对象
        return memberDAO.
                queryMemberByUsernameAndPassword(member.getUsername(), member.getPassword());
    }
}

测试(不要忘了测试)
在这里插入图片描述

web层 - LoginServlet

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String username = request.getParameter("username");
        String password = request.getParameter("password");

        Member member = new Member(null, username, password, null);

        member = memberService.login(member);
        if (member != null) {//登陆成功
            System.out.println("登陆成功");
            request.getRequestDispatcher("/views/member/login_ok.jsp")
                    .forward(request, response);
        } else {
            System.out.println("登陆失败, 返回登陆页面");
            request.setAttribute("username", username);
            request.setAttribute("msg", "登陆失败");
            request.getRequestDispatcher("/views/member/login.jsp")
                    .forward(request, response);
        }
    }

login.jsp


<span class="errorMsg1"
      style="float: right; font-weight: bold; color: lightgray; font-size: 20pt; margin-left: 10px;">${msg}span>
<form action="loginServlet" method="post">
    <input type="text" name="username" value="${username}" placeholder="Username"/>
    <input type="password" name="password" placeholder="Password"/>
    <div class="button-box">
        <div class="login-toggle-btn">
            <input type="checkbox"/>
            <a class="flote-none" href="javascript:void(0)">Remember mea>
            <a href="#">Forgot Password?a>
        div>
        <button type="submit"><span>Loginspan>button>
    div>
form>

添加login_ok.jsp, 参考

快捷键
在这里插入图片描述
效果
项目实战系列三:【家居购项目 (新版) 】_第24张图片
在这里插入图片描述

servlet合并

增加隐藏域
项目实战系列三:【家居购项目 (新版) 】_第25张图片
合并到MemberServlet
项目实战系列三:【家居购项目 (新版) 】_第26张图片

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

新建BasicServlet类, 继承HttpServlet

public class BasicServlet extends HttpServlet {
    
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doPost(request, response);
    }
    
	@Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String action = req.getParameter("action");
        try {
            //1.得到子类对应的class对象
            Class<? extends BasicServlet> aClass = this.getClass();
            //2.创建对象
            Object o = aClass.newInstance();
            //3.得到action方法对象
            Method declaredMethod = this.getClass()
                    .getDeclaredMethod(action, HttpServletRequest.class, HttpServletResponse.class);
            declaredMethod.invoke(o, req, resp);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

子类中没有doGet, doPost方法, 会调用父类的doGet, doPost.
项目实战系列三:【家居购项目 (新版) 】_第27张图片

显示家居

需求分析

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

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

  1. 页面准备项目实战系列三:【家居购项目 (新版) 】_第28张图片
  2. 新建admin表 参考member表
    新建furn表
    在这里插入图片描述
  3. 新建Admin实体类
    新建Furn实体类(无参构造器与set方法底层反射用, get方法前端EL表达式用)
    在这里插入图片描述
  4. 书写AdminDAO, AdminDAOImpl, 并测试; 书写AdminService, AdminServiceImpl, 并测试 参考Member
    书写FurnDAO, FurnDAOImpl 并测试
    在这里插入图片描述
  5. 书写FurnService, FurnServiceImpl 并测试
    在这里插入图片描述
  1. 接通web层
    管理员登录Servlet
public class AdminServlet extends BasicServlet {
    private AdminService adminService = new AdminServiceImpl();

    protected void login(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String username = request.getParameter("username");
        String password = request.getParameter("password");

        Admin admin = new Admin(null, username, password, null);

        if (adminService.login(admin)) {
            //管理员登录成功
            request.getRequestDispatcher("/views/manage/manage_menu.jsp")
                    .forward(request, response);
        } else {
            System.out.println("登陆失败, 返回登陆页面");
            request.setAttribute("username", username);
            request.setAttribute("msg", "用户名或密码不正确");
            request.getRequestDispatcher("/views/manage/manage_login.jsp")
                    .forward(request, response);
        }
    }
}

家居显示Servlet

<servlet>
        <servlet-name>FurnServletservlet-name>
        <servlet-class>com.zzw.furn.web.FurnServletservlet-class>
    servlet>
    <servlet-mapping>
        <servlet-name>FurnServletservlet-name>
        <url-pattern>/manage/furnServleturl-pattern>
    servlet-mapping>
public class FurnServlet extends BasicServlet {
    private FurnService furnService = new FurnServiceImpl();

    protected void list(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        List<Furn> furns = furnService.queryFurn();
        //将结果保存到request域
        request.setAttribute("furns", furns);
        //请求转发到管理家具页面
        request.getRequestDispatcher("/views/manage/furn_manage.jsp")
                .forward(request, response);
    }
}
  1. 前端页面
    manage_login.jsp, 管理员登录页面
<span class="errorMsg"
      style="float: right; font-weight: bold; color: lightgray; font-size: 20pt; margin-left: 10px;">${msg}span>
<%--管理员登陆--%>
<form action="adminServlet" method="post">
    <input type="hidden" name="action" value="login"/>
    <input type="text" name="username" value="${username}" placeholder="Username"/>
    <input type="password" name="password" placeholder="Password"/>
    <div class="button-box">
        <div class="login-toggle-btn">
            <input type="checkbox"/>
            <a class="flote-none" href="javascript:void(0)">Remember mea>
            <a href="#">Forgot Password?a>
        div>
        <button type="submit"><span>Loginspan>button>
    div>
form>

manage_menu.jsp, 家居菜单页面
在这里插入图片描述
furn_manage.jsp, 家居显示页面
items里用EL表达式括起来, var里不用EL表达式
在这里插入图片描述

添加家居

思路分析

  1. 请求添加家居, 请求FurnServlet的add方法, 将前端提交的数据封装到Furn对象
  2. 调用FurnService.add(Furn furn)方法
  3. 跳转到显示家居的页面

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

  1. FurnDAO
    项目实战系列三:【家居购项目 (新版) 】_第30张图片
  2. FurnService
    项目实战系列三:【家居购项目 (新版) 】_第31张图片
  1. web层
    FurnServlet
protected void add(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String name = req.getParameter("name");
        String business = req.getParameter("business");
        BigDecimal price = new BigDecimal(req.getParameter("price"));
        int saleNum = DataUtils.parseInt(req.getParameter("saleNum"), 0);
        int inventory = DataUtils.parseInt(req.getParameter("inventory"), 0);

        Furn furn = new Furn(null, name,
                business, price, saleNum, inventory, "...");

        if (furnService.add(furn) > 0) {
            System.out.println("添加成功, 请求转发到list");
            //req.getRequestDispatcher("/manage/furnServlet?action=list")
            //        .forward(req, resp);
            resp.sendRedirect(req.getContextPath() + "//manage/furnServlet?action=list");
        } else {
            System.out.println("添加失败, 返回到添加页面");
            req.getRequestDispatcher("views/manage/furn_add.jsp")
                    .forward(req, resp);
        }
    }

解决中文乱码问题
在这里插入图片描述
4. 前端: furn_manage跳转到添加家居页面


<div class="header-bottom-set dropdown">
    <a href="views/manage/furn_add.jsp">添加家居a>
div>

添加furn_add.jsp


<span class="errorMsg"
      style="float: right; font-weight: bold; color: lightgray; font-size: 20pt; margin-left: 10px;">span>

项目实战系列三:【家居购项目 (新版) 】_第32张图片
项目实战系列三:【家居购项目 (新版) 】_第33张图片

解决重复添加

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

后端数据校验说明

后端方案一
在这里插入图片描述后端方案二
在这里插入图片描述
前端数据校验

$(":submit").click(function () {
    var price = $("input[name='price']").val();
    var saleNum = $("input[name='saleNum']").val();
    var inventory = $("input[name='inventory']").val();

    //价格 非零开头最多两位小数
    var priceRegExp = /^[1-9]\d*(\.\d{1,2})?$/;
    if (!priceRegExp.test(price)) {
        $("span.errorMsg").text("价格格式不对");
        return false;
    }

    //销量 非零开头正整数
    var saleNumRegExp = /^0$|^[1-9]\d*$/;
    if (!saleNumRegExp.test(saleNum)) {
        $("span.errorMsg").text("销量格式不对");
        return false;
    }
    //库存 非零开头正整数
    var inventoryRegExp = /^0$|^[1-9]\d*$/;
    if (!inventoryRegExp.test(inventory)) {
        $("span.errorMsg").text("库存格式不对");
        return false;
    }
})
BeanUtils自动封装Bean

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

  1. 使用BeanUtils自动封装javabean
protected void add(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //这里我们使用第二种方式, 将前端提交的数据, 自动封装成Furn的Javabean对象
        //使用beanUtils完成javabean对象的自动封装
        Furn furn =
                DataUtils.copyParamToBean(req.getParameterMap(), new Furn());

        if (furnService.addFurn(furn)) {
            String pageNo = req.getParameter("pageNo");
            System.out.println("添加成功..");
            //req.getRequestDispatcher("/manage/furnServlet?action=list").forward(req, resp);
            resp.sendRedirect(req.getContextPath() + "/manage/furnServlet?action=list");
        } else {
            System.out.println("添加失败");
            req.setAttribute("errorMsg", "添加失败");
            req.getRequestDispatcher("/views/manage/furn_add.jsp").forward(req, resp);
        }
    }

debug小技巧
项目实战系列三:【家居购项目 (新版) 】_第35张图片
2. 报错
原因: 由于前端没有传imagePath的字段, 所以后端在构建furn对象的时候, imagePath属性位null
解决方案
在这里插入图片描述

  1. 将 把数据自动封装成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;
   }
}

调用
在这里插入图片描述

删除家居

需求分析

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

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

  1. FurnDAO
    项目实战系列三:【家居购项目 (新版) 】_第36张图片
  2. FurnService
    >
  1. web层 - FurnServlet
protected void del(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        int id = DataUtils.parseInt(req.getParameter("id"), 0);

        if (furnService.deleteFurnById(id) > 0) {
            System.out.println("删除成功");
        } else {
            System.out.println("删除失败");
        }
        
        resp.sendRedirect(req.getContextPath() + "/manage/furnServlet?action=list");
    }
  1. furn_manage.jsp页面
<a furnName="${furn.name}" href="manage/furnServlet?action=del&id=${furn.id}">
    <i class="icon-close">i>
a>

jQuery操作父元素, 兄弟元素, 子元素, 请移步
js弹框请移步

$("a[furnName]").click(function () {
    var furnName = $(this).attr("furnName");

    //js弹框
    //1.window.confirm 方法会弹出一个确认窗口
    //2.点击确定, 返回true
    //3.点击取消, 返回false
    var b = window.confirm("你确认要删除 " + furnName+ " 家居信息吗?");
    if (!b) {
        return false;
    }
    //简便写法
    return window.confirm("你确认要删除 " + furnName+ " 家居信息吗?");

    //最终写法
    return confirm("你确定要删除 " + furnName + " 家居信息嘛?");
});

修改家具

思路分析

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

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

  1. FurnDAO
    项目实战系列三:【家居购项目 (新版) 】_第38张图片
    项目实战系列三:【家居购项目 (新版) 】_第39张图片
  2. FurnService
    在这里插入图片描述
    在这里插入图片描述
  1. web层 - FurnServlet
protected void display(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        int id = DataUtils.parseInt(req.getParameter("id"), 0);

        Furn furn = furnService.queryFurnById(id);
        if (furn != null) {
            //将furn对象放入request域
            req.setAttribute("furn", furn);
            req.getRequestDispatcher("/views/manage/furn_update.jsp")
                    .forward(req, resp);
        } else {
            System.out.println("查询不到该信息");
            req.getRequestDispatcher("/views/manage/furn_manage.jsp")
                    .forward(req, resp);
        }
    }
protected void update(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        Furn furn =
                DataUtils.copyParamToBean(req.getParameterMap(), new Furn());

        if (furnService.updateFurn(furn) > 0) {
            System.out.println("更新成功");
            resp.sendRedirect(req.getContextPath() + "/manage/furnServlet?action=list");
        } else {
            req.setAttribute("msg", "更新失败");
            req.getRequestDispatcher("/manage/furnServlet?action=display")
                    .forward(req, resp);
        }
    }
  1. 前端
    furn_manage.jsp 点击修改,发出请求
<a href="manage/furnServlet?action=display&id=${furn.id}">
    <i class="icon-pencil">i>
a>

在tr标签下面添加span标签

<span class="errorMsg"
      style="float: right; font-weight: bold; color: lightgray; font-size: 20pt; margin-left: 10px;">${msg}span>

furn_update.jsp 数据校验

$(":submit").click(function () {
    var price = $("input[name='price']").val();
    var saleNum = $("input[name='saleNum']").val();
    var inventory = $("input[name='inventory']").val();

    //价格 非零开头最多两位小数
    var priceRegExp = /^[1-9]\d*(\.\d{1,2})?$/;
    if (!priceRegExp.test(price)) {
        $("span.errorMsg").text("价格格式不对");
        return false;
    }

    //销量 非零开头正整数
    var saleNumRegExp = /^0$|^[1-9]\d*$/;
    if (!saleNumRegExp.test(saleNum)) {
        $("span.errorMsg").text("销量格式不对");
        return false;
    }
    //库存 非零开头正整数
    var inventoryRegExp = /^0$|^[1-9]\d*$/;
    if (!inventoryRegExp.test(inventory)) {
        $("span.errorMsg").text("库存格式不对");
        return false;
    }
})

修改数据,点击提交
项目实战系列三:【家居购项目 (新版) 】_第40张图片
项目实战系列三:【家居购项目 (新版) 】_第41张图片

后台分页

shortcuts: ctrl+alt+u在局部打开类图

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

新建Page类

项目实战系列三:【家居购项目 (新版) 】_第43张图片

DAO

思路
项目实战系列三:【家居购项目 (新版) 】_第44张图片
实现
java.lang.Long cannot be cast to java.lang.Integer
项目实战系列三:【家居购项目 (新版) 】_第45张图片
项目实战系列三:【家居购项目 (新版) 】_第46张图片

Service

项目实战系列三:【家居购项目 (新版) 】_第47张图片
项目实战系列三:【家居购项目 (新版) 】_第48张图片

web层获取page对象

protected void page(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

    int pageNo = DataUtils.parseInt(req.getParameter("pageNo"), 1);
    int pageSize = DataUtils.parseInt(req.getParameter("pageSize"), Page.PAGE_SIZE);

    //调用service, 获取page对象
    Page<Furn> page = furnService.page(pageNo, pageSize);

    //将page放入到request域
    req.setAttribute("page", page);
    //请求转发到家居管理页面
    req.getRequestDispatcher("/views/manage/furn_manage.jsp")
            .forward(req, resp);
}

前端页面

manage_menu.jsp 取缔list方法

<div class="header-bottom-set dropdown">
    <a href="manage/furnServlet?action=page">家居管理a>
div>

管理员登陆后, 点击家居管理
在这里插入图片描述
furn_manage.jsp
项目实战系列三:【家居购项目 (新版) 】_第49张图片

后台分页导航

需求分析

  1. 管理员进入到家居管理后台页面
  2. 可以通过分页导航条来进行分页显示
  3. 完成上页, 下页, 显示共多少页
  4. 点击分页导航, 可以显示对应页的家居信息
  5. 在管理员进行修改, 删除, 添加后, 能够回显原来操作所在页面的数据

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

<!--  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="${requestScope.page.pageNo == 1 ? 'pointer-events: none; color: lightgray' : ''}"
              href="manage/furnServlet?action=page&pageNo=1&pageSize=${requestScope.page.pageSize}">首页</a>
       </li>
       <li><a style="${requestScope.page.pageNo == 1 ? 'pointer-events: none; color: lightgray' : ''}"
              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="${requestScope.page.pageNo == requestScope.page.pageTotal ? 'pointer-events: none; color: lightgray' : ''}"
              href="manage/furnServlet?action=page&pageNo=${requestScope.page.pageNo + 1}&pageSize=${requestScope.page.pageSize}">下一页</a>
       </li>
       <li>
           <a style="${requestScope.page.pageNo == requestScope.page.pageTotal ? 'pointer-events: none; color: lightgray' : ''}"
              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 -->
修改后返回原页面

在这里插入图片描述
项目实战系列三:【家居购项目 (新版) 】_第51张图片
在这里插入图片描述
项目实战系列三:【家居购项目 (新版) 】_第52张图片

删除后返回原页面

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

添加后返回原页面

项目实战系列三:【家居购项目 (新版) 】_第54张图片
项目实战系列三:【家居购项目 (新版) 】_第55张图片
在这里插入图片描述

首页分页

需求分析

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

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

实现

  1. 新建CustomerFurnServlet
protected void page(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    int pageNo = DataUtils.parseInt(request.getParameter("pageNo"), 1);
    int pageSize = DataUtils.parseInt(request.getParameter("pageSize"), Page.PAGE_SIZE);

    Page<Furn> page = furnService.page(pageNo, pageSize);
	//保存到request域
    request.setAttribute("page", page);
    //请求转发到/views/customer/index.jsp
    request.getRequestDispatcher("/views/customer/index.jsp").forward(request, response);
}
  1. 前端页面
    项目实战系列三:【家居购项目 (新版) 】_第57张图片
    直接请求CustomerFurnServlet, 获取网站首页要显示的分页数据
    类似我们网站的入口页面jsp请求转发标签
    在这里插入图片描述
    index.jsp
    项目实战系列三:【家居购项目 (新版) 】_第58张图片
  2. 显示数据
<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">
    <c:set scope="page" var="pageSize" value="${requestScope.page.pageSize}"/>
    <c:set scope="page" var="pageNo" value="${requestScope.page.pageNo}"/>
    <c:set scope="page" var="pageTotal" value="${requestScope.page.pageTotal}"/>
    <c:set scope="page" var="totalQuantity" value="${requestScope.page.totalQuantity}"/>
    <ul>
        <li><a ${pageNo == 1 ? "style='pointer-events: none; color: lightgray;'" : ''}
                href="customerFurnServlet?action=page&pageNo=1&pageSize=${pageSize}">首页</a></li>
        <li><a ${pageNo == 1 ? "style='pointer-events: none; color: lightgray;'" : ''}
                href="customerFurnServlet?action=page&pageNo=${pageNo - 1}&pageSize=${pageSize}">上页</a></li>

        <c:set var="begin" value="1" scope="page"/>
        <c:set var="end" value="${pageTotal}" scope="page"/>
        <c:forEach begin="${begin}" end="${end}" var="i">
            <li><a class="${pageNo == i ? 'active' : ''}" href="customerFurnServlet?action=page&pageNo=${i}&pageSize=${pageSize}">${i}</a></li>
        </c:forEach>
        <li><a ${pageNo == pageTotal ? "style='pointer-events: none; color: lightgray;'" : ''}
                href="customerFurnServlet?action=page&pageNo=${pageNo + 1}&pageSize=${pageSize}">下页</a></li>
        <li><a ${pageNo == pageTotal ? "style='pointer-events: none; color: lightgray;'" : ''}
                href="customerFurnServlet?action=page&pageNo=${pageTotal}&pageSize=${pageSize}">末页</a></li>
        <li><a>共${pageTotal}</a></li>
        <li><a>共${totalQuantity}记录</a></li>
    </ul>
</div>
<!--  Pagination Area End -->
首页搜索

需求分析

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

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

  1. DAO
    模糊查询
    在这里插入图片描述
    项目实战系列三:【家居购项目 (新版) 】_第60张图片
  2. service
    项目实战系列三:【家居购项目 (新版) 】_第61张图片
    项目实战系列三:【家居购项目 (新版) 】_第62张图片
  3. web层 CustomerFurnServlet
    page方法就被抛弃了
    在这里插入图片描述
  4. 前端 index.jsp
    如果是post提交方式, 可以在action处拼接参数

    在这里插入图片描述
两个奇怪的问题
  1. 点击家居管理, 发出两个请求
    在这里插入图片描述
    在这里插入图片描述
    项目实战系列三:【家居购项目 (新版) 】_第63张图片
    抓包
    在这里插入图片描述
    原因
    在这里插入图片描述
    请求首页面即进入到indx.jsp, index.jsp又请求转发到CustomerFurnServlet
    项目实战系列三:【家居购项目 (新版) 】_第64张图片
    问题解决
  2. 首页分页出现问题
    项目实战系列三:【家居购项目 (新版) 】_第65张图片
    项目实战系列三:【家居购项目 (新版) 】_第66张图片
    原因
    项目实战系列三:【家居购项目 (新版) 】_第67张图片

会员显示登录名

需求分析

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

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

MemberServlet

if (memberService.login(member)) {//登陆成功
    System.out.println("登陆成功");
    //将member存入到session域
    request.getSession().setAttribute("member", member);
    if ("admin".equals(member.getUsername())) {
        //管理员登录成功
        request.getRequestDispatcher("/views/manage/manage_menu.jsp")
                .forward(request, response);
    } else {
        //普通用户登陆成功
        request.getRequestDispatcher("/views/member/login_ok.jsp")
                .forward(request, response);
    }
}

index.jsp


<c:if test="${sessionScope.member != null}">
    <div class="header-bottom-set dropdown">
        欢迎: ${sessionScope.member.username}
    div>
    <div class="header-bottom-set dropdown">
        <a href="pages/manager/manager.html">订单管理a>
    div>
        <div class="header-bottom-set dropdown">
        <a href="pages/manager/manager.html">安全退出a>
    div>
c:if>
<c:if test="${sessionScope.member == null}">
    <div class="header-bottom-set dropdown">
        <a href="views/member/login.jsp">登录|注册a>
    div>
    <div class="header-bottom-set dropdown">
        <a href="views/manage/manage_login.jsp">后台管理a>
    div>
c:if>

注销登录

思路分析

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

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

实现

//安全退出
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    HttpSession session = req.getSession();
    //让当前session立即无效
    session.invalidate();
    //req.getContextPath() => /jiaju_mall2 解析成=> http://localhost:8080/jiaju_mall2
    // 默认访问web路径下的index.jsp页面
    resp.sendRedirect(req.getContextPath());
}

index.jsp

<div class="header-bottom-set dropdown">
    <a href="memberServlet?action=logout">安全退出a>
div>
验证码

引入kaptcha-2.3.2.jar包, 在web.xml中配置KaptchaServlet

表单重复提交情况

  1. 提交完表单. 服务器使用请求转发进行页面跳转. 用户刷新(F5), 会发起最后一次的请求, 造成表单重复提交问题. 解决:用重定向.
  2. 用户正常提交, 由于网络延迟等原因, 未收到服务器的响应. 这时, 用户着急多点了几次提交操作, 也会造成表单重复提交. 解决: 验证码
  3. 用户正常提交, 服务器也没有延迟, 但是提交完成后, 用户回退浏览器. 重新提交, 也会造成表单重复提交. 解决: 验证码
  4. 恶意注册, 使用可以批量发送Http的工具, 比如 Postman, Jemeter等, 使用验证码防护

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

web层
配置KaptchaServlet

<servlet>
    <servlet-name>KaptchaServletservlet-name>
    <servlet-class>com.google.code.kaptcha.servlet.KaptchaServletservlet-class>
servlet>
<servlet-mapping>
    <servlet-name>KaptchaServletservlet-name>
    <url-pattern>/kaptchaServleturl-pattern>
servlet-mapping>

项目实战系列三:【家居购项目 (新版) 】_第69张图片
项目实战系列三:【家居购项目 (新版) 】_第70张图片
MemberServlet

protected void register(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //todo 构建member
        
        //获取用户提交的验证码
        String captcha = request.getParameter("captcha");
        //从session中获取 生成的验证码
        HttpSession session = request.getSession();
        String token = (String) session.getAttribute(KAPTCHA_SESSION_KEY);
        //立即删除session中的验证码, 防止该验证码重复使用
        session.removeAttribute(KAPTCHA_SESSION_KEY);

        //如果token不为空, 并且和用户提交的验证码保持一致, 就继续
        if (token != null && token.equalsIgnoreCase(captcha)) {
        
            //todo 判断用户名是否可用 可用注册用户 不可用返回注册页面
            
        } else {
            request.setAttribute("msg", "验证码不正确");
            request.setAttribute("username", username);//回显用户名
            request.setAttribute("password", password);//回显密码
            request.setAttribute("email", email);//回显邮件
            request.setAttribute("active", "register_tab");
            request.getRequestDispatcher("/views/member/login.jsp")
                    .forward(request, response);
        }
    }

KAPTCHA_SESSION_KEY是一个常量, 使用前需要导入
import static com.google.code.kaptcha.Constants.KAPTCHA_SESSION_KEY;
在这里插入图片描述
2. 前端页面 - login.jsp

<input type="text" id="captchaText" name="captcha" style="width: 50%"
       placeholder="验证码"/>  <img id="captcha" alt="" width="150px" src="kaptchaServlet">

验证码不能为空

var captchaText = $("#captchaText").val().trim();
if (captchaText == "" || captchaText == null) {
    $("span.errorMsg").text("验证码不能为空!");
    return false;
}

点击图片更换验证码

$("#captcha").click(function () {
    $(this).attr("src", "kaptchaServlet?zzw=" + new Date());
});

购物车

需求分析

  1. 会员登陆后, 可以添加家居到购物车
  2. 完成购物车的设计和实现
  3. 每添加一个家居,购物车的数量+1, 并显示

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

cartItem模型

/**
 * 购物车的一项就是某个家居数据
 * @author 赵志伟
 * @version 1.0
 */
@SuppressWarnings({"all"})
public class CartItem {
    private Integer id;//编号
    private String name;//家居名
    private Integer count;//数量
    private BigDecimal price;//单价
    private BigDecimal totalPrice;//总价

    public CartItem() {
    }

	//有参构造器, getter, setter方法
}

Cart数据模型
在这里插入图片描述

这里默认添加的数量是1

//Cart就是购物车, 包含多个CartItem
public class Cart {
    //使用HashMap来保存
    private Map<Integer, CartItem> items = new HashMap<>();

    public boolean isEmpty() {
        return items.size() == 0;
    }

    //Cart表示购物车, items表示购物车明细
    //添加家居[CartItem]到Cart
    public void addItem(CartItem cartItem, Integer inventory) {
        CartItem item = items.get(cartItem.getId());//得到购物车里的商品项

        if (item == null) {//说明当前购物车还没有这个cartItem
            items.put(cartItem.getId(), cartItem);
        } else {//购物车中有这个cartItem
            item.setCount(item.getCount() + 1);//数量加1
            //修改总价
            //item.getPrice() => BigDecmal
            //item.getCount() => Integer
            item.setTotalPrice(item.getPrice().multiply(new BigDecimal(item.getCount())));
        }
    }
    //todo  setter, getter方法, toString方法
}

测试
在这里插入图片描述
实现
创建CartServlet
cart是个引用, cart内容变了, session中也会跟着变

public class CartServlet extends BasicServlet {

    private FurnService furnService = new FurnServiceImpl();
    
    //添加一个添加家居到购物车的方法
    protected void addItem(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        int id = DataUtils.parseInt(request.getParameter("id"), 1);//家居id
        //根据id获取对应的家居信息
        Furn furn = furnService.queryFurnById(id);
        //先把正常的逻辑走完, 再处理异常的情况

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

        String referer = request.getHeader("referer");
        response.sendRedirect(referer);
    }
}

首页获取id请求后台

<button furnId="${furn/id}" title="Add To Cart" class="add-to-cart">Add
    To Cart
button>
$("button.add-to-cart").click(function () {
    var furnId = $(this).attr("furnId");
    location.href = "cartServlet?action=addItem&id=" + furnId;
})

首页购买的商品总数量 - totalCount会默认调用getTotalCount方法

<a href="#offcanvas-cart"
   class="header-action-btn header-action-btn-cart offcanvas-toggle pr-0">
    <i class="icon-handbag"> 购物车i>
    <span class="header-action-num">${sessionScope.cart.totalCount}span>
a>

Cart类补充getTotalCount方法 - 错误写法

public class Cart {

    private Integer totalCount = 0;//1 3 3
    //如果totalCount是全局变量, 将遵循这样的增长方式
    
    //次数   购物车数量   totalCount(=totalCount+购物车数量)
    // 1        1         1
    // 2        2         3
    // 3        3         6
    // 4        4         10
    // 5        5         15
    // 6        6         21
    // 7        7         28
    public Integer getTotalCount() {
        Collection<CartItem> cartItems = items.values();
        for (CartItem cartItem : cartItems) {
            totalCount += cartItem.getCount();
        }
        return totalCount;
    }
}

正确写法 - totalCount必须是局部变量, 否则会造成累加

  1. HashMap的数据实际上是存在HashMap$Node中的, Node是HashMap的内部类
  2. keySet里的key, 实际上只是引用, 指向了HashMap$Node对象中的k, 真正的key值是保存在HashMap的Node内部类中的(HashMap$Node)
public class Cart {

    public Integer getTotalCount() {
	 	Integer totalCount = 0;
        Collection<CartItem> cartItems = items.values();
        for (CartItem cartItem : cartItems) {
            totalCount += cartItem.getCount();
        }
        return totalCount;
    }
显示购物车

需求分析

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

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

index.jsp跳转购物车页面无响应 - 排错技巧展示
项目实战系列三:【家居购项目 (新版) 】_第73张图片
项目实战系列三:【家居购项目 (新版) 】_第74张图片
项目实战系列三:【家居购项目 (新版) 】_第75张图片
定位
在这里插入图片描述

index.jsp代码


     购物车
    ${sessionScope.cart.totalCount}

显示家居项

<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">
                    <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>

计算总价

public class Cart {

	 /**
     * 返回购物车的总价
     * @return
     */
    public BigDecimal getCartTotalPrice() {

        BigDecimal cartTotalPrice = new BigDecimal(0);
        //遍历我们的items
        Set<Integer> keys = items.keySet();
        for (Integer id : keys) {
            CartItem item = items.get(id);
            //提醒, 一定要包add后的值, 重新赋给 cartTotalPrice, 这样才是累加.
            cartTotalPrice = cartTotalPrice.add(item.getTotalPrice());
        }

        return cartTotalPrice;
    }

totalCount调用的是Cart的getTotalCount方法, totalPrice调用的是getTotalPrice方法

<h4>共${sessionScope.cart.totalCount}件商品 总价 ${sessionScope.cart.totalPrice}元h4>
<div class="cart-shiping-update">
    <a href="#">购 物 车 结 账a>
div>
修改购物车

需求分析

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

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

Cart增加方法

/**
 * 修改指定的CartItem的数量和总价, 根据传入的id 和 count
 * @param id
 * @param count
 */
public void updateCount(int id, int count) {//传进来的更新后的数量
    CartItem cartItem = items.get(id);
    if (cartItem != null) {//如果得到cartItem
        //先更新数量
        cartItem.setCount(count);
        //再更新总价
        cartItem.setTotalPrice(cartItem.getPrice().multiply(new BigDecimal(cartItem.getCount())));
        //set方法有可能对count进行了二次处理, 所里这里用getCount()比较安全
    }
}

CartServlet

/**
     * 更新某个cartItem的数量, 即更新购物车
     * @param req
     * @param resp
     * @throws ServletException
     * @throws IOException
     */
    protected void updateCount(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        int id = DataUtils.parseInt(req.getParameter("id"), 0);//家居id[默认0, 即使传错也不会影响数据]
        int count = DataUtils.parseInt(req.getParameter("count"), 1);//更新后的数量

        //获取session中的购物车
        HttpSession session = req.getSession();
        Cart cart = (Cart) session.getAttribute("cart");
        if (cart != null) {
            cart.updateCount(id, count);
        }
        //回到请求更新购物车的原页面
        String referer = req.getHeader("referer");
        resp.sendRedirect(referer);
    }

cart.jsp

<%--某个js文件对 cart-plus-minus 做了事件处理--%>
<div class="cart-plus-minus" furnId="${entry.value.id}">
    <input class="cart-plus-minus-box" type="text" name="qtybutton"
           value="${entry.value.count}"/>
div>

在这里插入图片描述
项目实战系列三:【家居购项目 (新版) 】_第77张图片
在这里插入图片描述
项目实战系列三:【家居购项目 (新版) 】_第78张图片

cart.jsp

//在这里书写我们的代码
var furnId = $button.parent().attr("furnId");
//在这里发出修改购物车的请求
location.href = "cartServlet?action=updateCount&id=" + furnId + "&count=" + newVal;
删除购物车

需求分析

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

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

删除购物车

Cart.java

/**
 * 根据传入的id, 删除指定的购物车项
 * @param id
 */
public void deleteItem(int id) {
    items.remove(id);
}

CartServlet

protected void delItem(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    //为了防止接收的id转化成数字时报错, 在工具类DataUtils中写一个方法
    int id = DataUtils.parseInt(req.getParameter("id"), 0);

    HttpSession session = req.getSession();
    Cart cart = (Cart) session.getAttribute("cart");
    if (cart != null) {
        cart.deleteItem(id);
    }
    //返回到请求删除购物车的页面
    String referer = req.getHeader("referer");
    resp.sendRedirect(referer);
}

cart.jsp

$(".product-remove").click(function () {
    var furnName = $(this).attr("furnName");
    return confirm("确定要删除 " + furnName + " 家居项吗?");
})
<td class="product-remove" furnName="${entry.value.name}">
    <a href="cartServlet?action=delItem&id=${entry.value.id}">
        <i class="icon-close">i>
    a>
td>

清空购物车
Cart.java

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

CartServlet

protected void clear(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    //清空购物车
    Cart cart = (Cart) req.getSession().getAttribute("cart");
    if (cart != null) {
        cart.clear();
    }
    String referer = req.getHeader("referer");
    resp.sendRedirect(referer);
}

cart.jsp

//清空购物车
$("a:contains('清 空 购 物 车')").click(function () {
    return confirm("确定要清空购物车吗?");
})
<a href="cartServlet?action=clear">清 空 购 物 车a>

生成订单

需求分析

  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;
  
  private List<OrderItem> items = new ArrayList<>();

  //计算订单的商品数, 供前端EL表达式使用
  public Integer getTotalCount() {
    Integer totalCount = 0;

    for (OrderItem orderItem : items) {
      totalCount += orderItem.getCount();
    }
    return totalCount;
  }
  
  //无参构造器
  public Order() {
  }
  //有参构造器
  //getter方法, setter方法, toString方法
}

订单明细表

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方法, toString方法
}

DAO

OrderDAO
在这里插入图片描述
OrderItemDAO

在这里插入图片描述

service

public interface OrderService {
    //1.生成订单
    //2.订单是根据cart来生成, cart对象在session. 通过web层, 传入saveOrder
    //3.订单和一个会员关联
    public String saveOrder(Cart cart, int memberId);
}
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;
    }
}

test

public class OrderServiceTest {
    private OrderService orderService = new OrderServiceImpl();

    @Test
    public void saveOrder() {
        //构建一个Cart对象
        Cart cart = new Cart();
        cart.addItem(new CartItem(1, "书桌", 1, new BigDecimal(12), new BigDecimal(12)));
        cart.addItem(new CartItem(2, "电脑", 2, new BigDecimal(12), new BigDecimal(24)));
        cart.addItem(new CartItem(3, "鼠标", 3, new BigDecimal(12), new BigDecimal(36)));

        orderService.saveOrder(cart, 12);
    }
}

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为空, 说明会员没有购买任何家居, 转发到首页
		//这里需要补充逻辑: 购物车在session里, 但没有家居数据
        if (cart == null || cart.isEmpty()) {
            //重定向, 请求转发后面的代码会继续执行
            response.sendRedirect(request.getContextPath());
            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");
    }
}

防止生成空订单

CartTest

@Test
public void isEmpty() {
    Map<Object, Object> map = new HashMap<>();
    map.put("k", "v");
    map.clear();
    System.out.println(map == null);//false
    System.out.println(map.size());//0
}

HashMap源码

    public void clear() {
        Node<K,V>[] tab;
        modCount++;
        if ((tab = table) != null && size > 0) {
        	//clear之后, size置为0
            size = 0;
            for (int i = 0; i < tab.length; ++i)
                tab[i] = null;
        }
    }

Cart.java

	public boolean isEmpty() {
	    return items.size() == 0;
	}

前端

cart.jsp

<div class="cart-shiping-update">
    <a href="orderServlet?action=saveOrder">生 成 订 单a>
div>

引入checkout.jsp

显示订单[订单管理]

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

需求分析

  1. 如果某家居库存为0, 首页的"Add to Cart" 按钮显示为"暂时缺货"
<c:if test="${furn.inventory <= 0}">
    <button disabled title="Add To Cart" class="add-to-cart">Add
        To Cart[缺货]
    button>
c:if>
<c:if test="${furn.inventory > 0}">
    <button furnId="${furn.id}" title="Add To Cart" class="add-to-cart">Add
        To Cart
    button>
c:if>

需求分析

  1. 后台也加上校验. 只有在 库存>0时, 才能添加到购物车

思路分析

  1. 首页添加家居[Add To Cart]到购物车时, 加以限制
  2. 购物车里, 更新家居数量时,加以限制
protected void addItem(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    int id = DataUtils.parseInt(request.getParameter("id"), 0);

    HttpSession session = request.getSession();
    Cart cart = (Cart) session.getAttribute("cart");
    if (cart == null) {
        cart = new Cart();
        session.setAttribute("cart", cart);
    }

    Furn furn = furnService.queryFurnById(id);
    if (furn != null && furn.getInventory() <= 0) {
        String referer = request.getHeader("referer");
        response.sendRedirect(referer);
        return;
    }

    //cart!=null
    Map<Integer, CartItem> items = cart.getItems();
    if (items.size() == 0) {
        CartItem cartItem = new CartItem(id, furn.getName(), 1, furn.getPrice(), furn.getPrice());
        cart.addItem(cartItem);
    } else {
        CartItem item = items.get(id);
        if (item == null) {
            CartItem cartItem = new CartItem(id, furn.getName(), 1, furn.getPrice(), furn.getPrice());
            cart.addItem(cartItem);
        } else if (item != null && furn.getInventory() > item.getCount()) {
            CartItem cartItem = new CartItem(id, furn.getName(), 1, furn.getPrice(), furn.getPrice());
            cart.addItem(cartItem);
        }
    }

    String referer = request.getHeader("referer");
    response.sendRedirect(referer);
}
protected void updateCount(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    int id = DataUtils.parseInt(req.getParameter("id"), 0);
    int count = DataUtils.parseInt(req.getParameter("count"), 0);

    Furn furn = furnService.queryFurnById(id);
    if (furn != null && furn.getInventory() < count) {
        String referer = req.getHeader("referer");
        resp.sendRedirect(referer);
        return;
    }

    HttpSession session = req.getSession();
    Cart cart = (Cart) session.getAttribute("cart");

    if (cart != null) {
        cart.updateCount(id, count);
    }

    String referer = req.getHeader("referer");
    resp.sendRedirect(referer);
}
  • 管理订单

需求分析

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

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

DAO
项目实战系列三:【家居购项目 (新版) 】_第81张图片
service
项目实战系列三:【家居购项目 (新版) 】_第82张图片
web层

protected void listByMemberId(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

    //获取到登陆的member对象
    Member member = (Member) req.getSession().getAttribute("member");
    if (member == null) {//说明用户没有登录
        //重定向到登陆页面
        req.getRequestDispatcher("/views/member/login.jsp")
                .forward(req, resp);
        return;//直接返回
    }

    List<Order> orders = orderService.queryOrderByMemberId(member.getId());
    //把订单集合放入到request域中
    req.setAttribute("orders", orders);
    //请求转发到order.jsp
    req.getRequestDispatcher("/views/order/order.jsp")
            .forward(req, resp);
}

前端
checkout.jsp

<a class="active" href="orderServlet?action=listByMemberId">
    <h4>订单已生成, 订单号-${sessionScope.orderId}h4>
a>

order.jsp

<c:forEach items="${requestScope.orders}" var="order">
    <tr>
        <td class="product-name">${order.id}td>
        <td class="product-name">${order.createTime}td>
        <td class="product-price-cart"><span class="amount">${order.price}span>td>
        <td class="product-name"><a href="#">
            <c:choose>
                <c:when test="${order.status == 1}">未发货c:when>
                <c:when test="${order.status == 2}">已发货c:when>
                <c:when test="${order.status == 3}">未结账c:when>
                <c:otherwise>错误c:otherwise>
            c:choose>
        a>td>
        <td class="product-remove">
            <a href="#"><i class="icon-eye">i>a>
        td>
    tr>
c:forEach>
  • 管理订单项

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

DAO
项目实战系列三:【家居购项目 (新版) 】_第83张图片

项目实战系列三:【家居购项目 (新版) 】_第84张图片
service
项目实战系列三:【家居购项目 (新版) 】_第85张图片
web层 - OrderServlet

protected void listOrderItemByOrderId(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    String orderId = req.getParameter("orderId");
    orderId = (orderId == null) ? "" : orderId;

    Order order = orderService.queryOrderById(orderId);

    req.setAttribute("order", order);
    req.getRequestDispatcher("/views/order/order_detail.jsp").forward(req, resp);
}

前端
order.jsp

<td class="product-remove">
    <a href="orderServlet?action=listOrderItemByOrderId&orderId=${order.id}"><i class="icon-eye">i>a>
td>

order_detail.jsp

//请求转发后, url里的参数仍可以取出
<h3 class="cart-page-title">订单-${param.orderId}h3>
<c:forEach items="${requestScope.order.items}" var="orderItem">
    <tr>
        <td class="product-name"><a href="#">${orderItem.name}a>td>
        <td class="product-price-cart"><span class="amount">$${orderItem.price}span>td>
        <td class="product-quantity">${orderItem.count}td>
        <td class="product-subtotal">$${orderItem.totalPrice}td>
    tr>
c:forEach>
<div class="cart-shiping-update-wrapper">
    <h4>共${requestScope.order.totalCount}件商品 总价 ${requestScope.order.price}元h4>
    <div class="cart-clear">
        <a href="#">继 续 购 物a>
    div>
div>

过滤器权限验证

需求分析

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

配置拦截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>
   filter-mapping>
web-app>

过滤器逻辑判断

/**
* 这是用于权限验证的过滤器, 对指定的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() {

   }
}

manage_login.jsp
修改表单提交到memberServlet

<%--管理员登陆--%>
<form action="memberServlet" method="post">form>

MemberServlet
如果登陆的账户是管理员, 则返回到管理员登陆成功的页面

Member member = new Member(null, username, password, null);
member = memberService.login(member);
//if (memberService.login(member) != null) {//用户存在DB
if (member != null) {//用户存在DB
    System.out.println("用户存在, 登陆成功...");
    //将得到的member存入到session会话中
    HttpSession session = request.getSession();
    session.setAttribute("member", member);

    if ("admin".equals(member.getUsername())) {
        request.getRequestDispatcher("/views/manage/manage_menu.jsp")
                .forward(request, response);
    } else {
        request.getRequestDispatcher("/views/member/login_ok.jsp")
                .forward(request, response);
    }
}

事务管理

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

项目实战系列三:【家居购项目 (新版) 】_第86张图片
项目实战系列三:【家居购项目 (新版) 】_第87张图片
我在首页购买了一个小台灯, 数据库中生成了对应的订单和订单项, 但家居表里该小台灯的销量和库存没有变化, 纹丝不动. 相当于客户下单了, 但没有给人家发货.
项目实战系列三:【家居购项目 (新版) 】_第88张图片

项目实战系列三:【家居购项目 (新版) 】_第89张图片

2. 程序框架图

思路分析

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

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

  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过滤器

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

体会: 异常机制是可以参与业务逻辑的
项目实战系列三:【家居购项目 (新版) 】_第92张图片
项目实战系列三:【家居购项目 (新版) 】_第93张图片

  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文件
    将跳转链接改成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来显示对应页面
           //这里也可以不抛出异常, rollback()内已经抛出
           throw new RuntimeException(e);
           //e.printStackTrace();

       }
   }

Ajax检验注册名

需求分析

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

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

  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);
    在这里插入图片描述

工具类添加方法 - 判断请求是不是一个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}    
       //String url = "views/member/login.jsp";
       //String resultJson = "{\"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. 这里会用到文件上传功能

思路分析-程序框架图
项目实战系列三:【家居购项目 (新版) 】_第95张图片

  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>
<script type="text/javascript">
   function prev(event) {
       //获取展示图片的区域
       var img = document.getElementById("preView");
       //获取文件对象
       var file = event.files[0];
       //获取文件阅读器: Js的一个类, 直接使用即可
       var reader = new FileReader();
       reader.readAsDataURL(file);
       reader.onload = function () {
           //给img的src设置图片url
           img.setAttribute("src", this.result)
       }
   }
</script>

去掉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}"
          onchange="prev(this)"/>
div>
  1. 分析空指针异常
    将form表单改成文件表单

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

    在这里插入图片描述
  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;
   }
}

处理文件字段
项目实战系列三:【家居购项目 (新版) 】_第101张图片
将文件上传路径保存成一个常量

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);
                   }
               } 
           } else {
               System.out.println("不是文件表单...");
           }

           //更新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;
   }

在这里插入图片描述
项目实战系列三:【家居购项目 (新版) 】_第102张图片项目实战系列三:【家居购项目 (新版) 】_第103张图片

分页导航完善

需求分析

  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,servlet,java,tomcat)