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

在这里插入图片描述

🐀Java后端经典三层架构

在这里插入图片描述

在这里插入图片描述

分层对应包说明
web层com.zzw.furns.web/servlet/controller/handler接受用户请求, 调用service
service层com.zzw.furns.serviceService接口包
com.zzw.furns.service.implService接口实现类
dao持久层com.zzw.furns.daoDao接口包
com.zzw.furns.dao.implDao接口实现类
实体bean对象com.zzw.furns.pojo/entity/domain/beanJavaBean类
工具类com.zzw.furns.utils工具类
测试包com.zzw.furns.test完成对dao/service测试

🐇MVC模型

MVC全称: Model模型, View试图, Controller控制器
MVC最早出现在JavaEE三层中的Web层, 它可以有效地指导WEB层代码如何有效地分离, 单独工作

  • View试图: 只负责数据和界面的显示, 不接受任何与显示数据无关的代码, 便于程序员和美工的分工与合作(Vue/Jsp/Thymeleaf/Html)
  • Controller控制器: 只负责接收请求, 调用业务层的代码处理请求, 然后派发给页面, 是一个"调度者"的角色
  • Model模型: 将与业务逻辑相关的数据封装为具体的JavaBean类, 其中不掺杂任何与数据处理相关的代码(JavaBean/Domain/Pojo)

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

解读

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

🐇开发环境搭建

参考 IDEA 2022.3开发JavaWeb工程

1.新建Java项目 jiaju_mall

2.导入jar包

3.项目的结构
在这里插入图片描述
在这里插入图片描述

4.下载资源, 拷贝到web路径下
在这里插入图片描述
在这里插入图片描述

5.Rebuild project, 让项目识别到这些资源, 然后再启动Tomcat
在这里插入图片描述

6.对于复杂的前端页面, 要学会打开当前页面的结构, 提高工作效率

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

🐇会员注册

思路分析

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

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

🍉前端验证用户注册信息

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.javaService接口包
MemberServiceImpl.javaService接口实现类
dao持久层MemberDAO.javaDao接口包
MemberDAOImplDao接口实现类
实体bean对象Member.javaJavaBean类
工具类JdbcUtilsByDruid.java工具类

🍉创建表

在这里插入图片描述

🍉创建实体类

满汉楼项目
包括无参构造器和set方法. 如果添加有参构造器, 记得书写无参构造器
因为 一键生成实体类的工具是不会创建无参构造器的
get方法也要生成, 因为前端页面EL表达式是要调用get方法的
在这里插入图片描述

  1. 从满汉楼项目引入BasicDAO.java, JdbcUtilsByDruid.java, Druid.properties
  2. 修改Druid配置文件要连接的数据库名, 确保用户名密码正确. url后面是做批处理用的
    在这里插入图片描述
  3. 修改JdbcUtilsByDruid的路径
    在这里插入图片描述
    配置快捷键
    在这里插入图片描述
    在这里插入图片描述
  4. 测试
    在这里插入图片描述

🍉DAO

在这里插入图片描述

🍌MemberDAOImpl

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

   /**
    * 保存一个会员
    * @param member 传入一个Member对象
    * @return 如果返回-1, 就是失败; 返回其它的数字, 就是受影响的行数
    */
   @Override
   public int saveMember(Member member) {
       //连同单引号一并换成 ? , 它会自动加上单引号
       String sql = "INSERT INTO member(id, username, `password`, email) " +
               "VALUES(NULL, ?, MD5(?), ?)";
       int updateRows = update(sql, member.getUsername(), member.getPassword(), member.getEmail());
       return updateRows;
   }
}

测试
在这里插入图片描述

🍉Service

在这里插入图片描述

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

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

🍉接通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页面要做的处理
在这里插入图片描述
将路径修改成相对路径
在这里插入图片描述
如果有需要, 在页面顶部引入c标签
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

如果是html页面, base标签不能使用el表达式, 要这样写:
在这里插入图片描述

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加密

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

MemberDAO
在这里插入图片描述
在这里插入图片描述
MemberDAOImpl
在这里插入图片描述
测试(不要忘了测试)
在这里插入图片描述
快捷键
在这里插入图片描述
在这里插入图片描述

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 me</a>
            <a href="#">Forgot Password?</a>
        </div>
        <button type="submit"><span>Login</span></button>
    </div>
</form>

添加login_ok.jsp, 参考

快捷键
在这里插入图片描述
效果
在这里插入图片描述
在这里插入图片描述

🐇servlet合并

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

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

新建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.
在这里插入图片描述

🌳显示家居

需求分析

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

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

  1. 页面准备在这里插入图片描述
  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>FurnServlet</servlet-name>
        <servlet-class>com.zzw.furn.web.FurnServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>FurnServlet</servlet-name>
        <url-pattern>/manage/furnServlet</url-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 me</a>
            <a href="#">Forgot Password?</a>
        </div>
        <button type="submit"><span>Login</span></button>
    </div>
</form>

manage_menu.jsp, 家居菜单页面
在这里插入图片描述
furn_manage.jsp, 家居显示页面
<c:forEach items=“${requestScope.furns}” var=“furn”> items里用EL表达式括起来, var里不用EL表达式
在这里插入图片描述

🌳添加家居

思路分析

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

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

  1. FurnDAO
    在这里插入图片描述
  2. FurnService
    在这里插入图片描述
  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跳转到添加家居页面

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

添加furn_add.jsp

<!--添加在tr标签的下一行-->
<span class="errorMsg"
      style="float: right; font-weight: bold; color: lightgray; font-size: 20pt; margin-left: 10px;"></span>

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

🍉解决重复添加

请求转发, 当用户刷新页面时, 会重新发出第一次的请求, 造成数据重复提交
在这里插入图片描述
解决方案: 使用重定向
在这里插入图片描述

🍉后端数据校验说明

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

$(":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小技巧👉
在这里插入图片描述
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
    在这里插入图片描述
  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. 修改成功后, 显示刷新后的家居列表

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

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

修改数据,点击提交
在这里插入图片描述
在这里插入图片描述

🍃后台分页

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

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

🍒新建Page类

在这里插入图片描述

🍒DAO

思路
在这里插入图片描述
实现
java.lang.Long cannot be cast to java.lang.Integer
在这里插入图片描述
在这里插入图片描述

🍒Service

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

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

🍅后台分页导航

需求分析

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

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

<!--  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 -->
🍅修改后返回原页面

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

🍅删除后返回原页面

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

🍅添加后返回原页面

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

🍃首页分页

需求分析

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

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

实现

  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. 前端页面
    在这里插入图片描述
    直接请求CustomerFurnServlet, 获取网站首页要显示的分页数据
    类似我们网站的入口页面👉jsp请求转发标签
    在这里插入图片描述
    index.jsp
    在这里插入图片描述
  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. 正确显示分页导航条, 并且要求在分页时, 保留上次搜索条件

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

  1. DAO
    模糊查询👉
    在这里插入图片描述
    在这里插入图片描述
  2. service
    在这里插入图片描述
    在这里插入图片描述
  3. web层 CustomerFurnServlet
    page方法就被抛弃了
    在这里插入图片描述
  4. 前端 index.jsp
    如果是post提交方式, 可以在action处拼接参数

    在这里插入图片描述

🍅两个奇怪的问题

  1. 点击家居管理, 发出两个请求
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    抓包
    在这里插入图片描述
    原因
    在这里插入图片描述
    请求首页面即进入到indx.jsp, index.jsp又请求转发到CustomerFurnServlet
    在这里插入图片描述
    问题解决
  2. 首页分页出现问题
    在这里插入图片描述
    在这里插入图片描述
    原因
    在这里插入图片描述

🌳会员显示登录名

需求分析

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

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

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

<!-- Single Wedge Start -->
<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>
<!-- Single Wedge End -->
🍅注销登录

思路分析

  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等, 使用验证码防护

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

1.前端页面 - login.jsp

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

点击图片更换验证码

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

2.web层, 配置KaptchaServlet

<servlet>
    <servlet-name>KaptchaServlet</servlet-name>
    <servlet-class>com.google.code.kaptcha.servlet.KaptchaServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>KaptchaServlet</servlet-name>
    <url-pattern>/kaptchaServlet</url-pattern>
</servlet-mapping>

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

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

3.点击提交, 验证码不能为空

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

4.进入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);
        }
    }

🌳购物车

需求分析

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

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

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, v>对象中的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. 统计购物车共多少商品, 总价多少

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

index.jsp跳转购物车页面无响应 - 排错技巧展示
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
定位
在这里插入图片描述

index.jsp代码

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

显示家居项

<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. 更新购物车商品数量和总金额

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

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>

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

cart.jsp

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

需求分析

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

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

删除购物车

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 已提供

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

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

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

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
        version="4.0">
   <!--过滤器一般我们配置在上面-->
   <filter>
       <filter-name>AuthFilter</filter-name>
       <filter-class>com.zzw.furns.filter.AuthFilter</filter-class>
       <init-param>
       	   <!--这里配置了后, 还需要在过滤器中处理-->
           <param-name>excludedUrls</param-name>
           <param-value>/views/manage/manage_login.jsp,/views/member/login.jsp</param-value>
       </init-param>
   </filter>
   <filter-mapping>
       <filter-name>AuthFilter</filter-name>
       <!--这里配置要验证的url
           1.在filter-mapping中的url-pattern配置 要拦截/验证的url
           2.对于我们不去拦截的url, 就不配置
           3.对于要拦截的目录中的某些要放行的资源, 再通过配置指定
       -->
       <url-pattern>/views/cart/*</url-pattern>
       <url-pattern>/views/manage/*</url-pattern>
       <url-pattern>/views/member/*</url-pattern>
       <url-pattern>/views/order/*</url-pattern>
       <url-pattern>/cartServlet</url-pattern>
       <url-pattern>/manage/furnServlet</url-pattern>
       <url-pattern>/orderServlet</url-pattern>
   </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. 查看数据库里的数据会有什么结果. 会出现数据不一致的问题.
    在这里插入图片描述

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

在这里插入图片描述

2. 程序框架图

思路分析

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

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

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

   /**
    * 从ThreadLocal获取connection, 从而保证在一个线程中
    * 获取的是同一个Connection
    */
   public static Connection getConnection() {
       Connection connection = threadlocalConn.get();
       if (connection == null) {//说明当前的threadlocal没有这个连接
           try {
               //就从数据库连接池中取出连接放入threadlocal
               connection = dataSource.getConnection();
               //将连接设置为手动提交, 既不要让它自动提交
               connection.setAutoCommit(false);
               threadlocalConn.set(connection);
           } catch (SQLException e) {
               throw new RuntimeException(e);
           }
       }
       return connection;
   }

   /**
    * 提交事务
    */
   public static void commit() {
       Connection connection = threadlocalConn.get();
       if (connection != null) {
           try {
               connection.commit();
           } catch (SQLException e) {
               throw new RuntimeException(e);
           } finally {
               try {
                   connection.close();
               } catch (SQLException e) {
                   throw new RuntimeException(e);
               }
           }
           //1.当提交后, 需要把connection从threadlocalConn中清除掉
           //2.不然会造成threadlocalConn长时间持有该连接, 会影响效率
           //3.也因为Tomcat底层使用的是线程池技术
           threadlocalConn.remove();
       }
   }

   /**
    * 说明: 所谓回滚是 回滚/撤销 和connection管理的操作 删除/修改/添加
    */
   public static void rollback() {
       Connection connection = threadlocalConn.get();
       if (connection != null) {
           try {
               connection.rollback();
           } catch (SQLException e) {
               throw new RuntimeException(e);
           } finally {
               try {
                   connection.close();
               } catch (SQLException e) {
                   throw new RuntimeException(e);
               }
           }
           threadlocalConn.remove();
       }
   }
  1. 修改BasicDao
    删掉各个方法finally代码块里的close方法. 只有在事务结束后才实施关闭连接的操作. 一是提交事务后关闭连接; 二是增删改出错后, 回滚关闭连接.
   public List<T> queryMany(String sql, Class<T> clazz, Object... objects) {
       Connection connection = null;
       try {
           connection = JdbcUtilsByDruid.getConnection();
           List<T> tList =
                   queryRunner.query(connection, sql, new BeanListHandler<>(clazz), objects);
           return tList;
       } catch (SQLException e) {
           throw new RuntimeException(e);//编译异常->运行异常抛出
       }
   }

   //查询单行, 返回的是一个对象
   public T querySingle(String sql, Class<T> clazz, Object... objects) {
       Connection connection = null;
       try {
           connection = JdbcUtilsByDruid.getConnection();
           T object
                   = queryRunner.query(connection, sql, new BeanHandler<>(clazz), objects);
           return object;
       } catch (Exception e) {
           throw new RuntimeException(e);
       }
   }

   //查询某一字段
   public Object queryScalar(String sql, Object... objects) {
       Connection connection = null;
       try {
           connection = JdbcUtilsByDruid.getConnection();
           Object query = queryRunner.query(connection, sql, new ScalarHandler(), objects);
           return query;
       } catch (Exception e) {
           throw new RuntimeException(e);
       }
   }

   public int update(String sql, Object... objects) {
       Connection connection = null;
       try {
           //这里是从数据库连接池获取connection
           //注意:每次从连接池中取出connection, 不能保证是同一个
           //1.我们目前已经是从和当前线程关联的ThreadLocal获取的connection
           //2.所以可以保证是同一个连接[在同一个线程中/在同一个请求中 => 因为一个请求对应一个线程]
           connection = JdbcUtilsByDruid.getConnection();
           return queryRunner.update(connection, sql, objects);
       } catch (Exception e) {
           throw new RuntimeException(e);
       }
   }
  1. 控制层进行事务管理
    前提OrderServiceImpl里报错的代码取消try-catch, 在OrderServlet控制层捕获
       //1.如果我们只是希望对orderService.saveOrder()方法进行事务控制
       //2.那么我们可以不使用过滤器,直接在这个位置进行提交和回滚即可
       //可以生成订单
       String orderId = null;//订单, 订单明细已生成
       try {
           orderId = orderService.saveOrder(cart, member.getId());
           JdbcUtilsByDruid.commit();//提交
       } catch (Exception e) {
           JdbcUtilsByDruid.rollback();
           e.printStackTrace();
       }
3.Transaction过滤器

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

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

  1. 在web.xml中配置
   <filter>
       <filter-name>TransactionFilter</filter-name>
       <filter-class>com.zzw.furns.filter.TransactionFilter</filter-class>
   </filter>
   <filter-mapping>
       <filter-name>TransactionFilter</filter-name>
       <!--这里我们对请求都进行事务管理 -->
       <url-pattern>/*</url-pattern>
   </filter-mapping>
  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
    <a class="active" href="index.jsp"> <h4 style="color: darkblue">您访问的页面不存在 返回首页</h4> </a>
  2. web.xml配置
<!--错误提示的配置一般写在web.xml的下面-->

<!--500 错误提示页面-->
<error-page>
   <error-code>500</error-code>
   <location>/views/error/500.jsp</location>
</error-page>
<!--404 错误提示页面-->
<error-page>
   <error-code>404</error-code>
   <location>/views/error/404.jsp</location>
</error-page>
  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完成

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

  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", "978964140@qq.com");
   //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 class="header-action-num"></span>
                        $("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 class="header-action-num"></span>
           $("span.header-action-num").text(data.cartTotalCount);
       } else {
           location.href = data.url;
       }
   }
)

🌈上传与更新家居图片

引入文件上传下载的包: commons-io-1.4.jar, commons-fileupload-1.2.1.jar
FurnDAOImpl的查询语句加上图片字段 image_path as imagePath

需求分析

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

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

  1. furn_update.jsp
   <style type="text/css">

       #pic {
           position: relative;
       }

       input[type="file"] {
           position: absolute;
           left: 0;
           top: 0;
           height: 180px;
           opacity: 0;
           cursor: pointer;
       }
   </style>
<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表单改成文件表单
    <form action="manage/furnServlet" method="post" enctype="multipart/form-data"></form>
    点击修改家居
    在这里插入图片描述
    报错
    在这里插入图片描述
    将web.xml中500的错误提示配置注销掉, 将异常信息暴露出来
    在这里插入图片描述
    再次点击修改家居信息, 报错信息显示出来, BasicServlet空指针异常
    所以有时候报错信息显示出来很重要
    在这里插入图片描述
    分析: 如果表单是enctype=“multipart/form-data”, 那么req.getParameter(“action”) 的方法得不到action值, 所以BasicServlet会报错
    在这里插入图片描述
    具体原因: req.getParameter(“action”)取不到form-data里的数据
    在这里插入图片描述
  2. 解决空指针异常
    解决方案: 将参数action, id, pageNo以url拼接的方式传参, BasicServlet便不会出错
    注意: post请求可以人为主动在地址中拼接参数,拼接的参数可以直接像get那样接收
    <form action="manage/furnServlet?action=update&id=${requestScope.furn.id}&pageNo=${param.pageNo}" method="post" enctype="multipart/form-data">
    在这里插入图片描述
  3. FurnServlet update方法
    处理普通字段
if (fileItem.isFormField()) {//文本表单字段
   将提交的家居信息, 封装成Furn对象
   switch (fileItem.getFieldName()) {
       case "name":
           furn.setName(fileItem.getString("utf-8"));
           break;
       case "business":
           furn.setBusiness(fileItem.getString("utf-8"));
           break;
       case "price":
           furn.setPrice(new BigDecimal(fileItem.getString()));
           break;
       case "saleNum":
           furn.setSaleNum(Integer.parseInt(fileItem.getString()));
           break;
       case "inventory":
           furn.setInventory(Integer.parseInt(fileItem.getString()));
           break;
   }
}

处理文件字段
在这里插入图片描述
将文件上传路径保存成一个常量

public class WebUtils {
   public static final String FURN_IMG_DIRECTORY = "assets/images/product-image/";
}    
//文件表单字段 => 获取上传的文件的名字
String name = fileItem.getName();

//如果用户没有选择新的图片, name = ""
if (!"".equals(name)) {
   //1.把上传到到服务器 temp目录下的文件保存到指定的目录
   String filePath = "/" + WebUtils.FURN_IMG_DIRECTORY;
   //2.获取完整的目录
   String fileRealPath = req.getServletContext().getRealPath(filePath);
   System.out.println("fileRealPath= " + fileRealPath);
   //3.创建这个上传的目录
   File fileRealPathDirectory = new File(fileRealPath);
   if (!fileRealPathDirectory.exists()) {
       fileRealPathDirectory.mkdirs();
   }
   //4.将文件拷贝到fileRealPathDirectory目录下
   //对上传的文件名进行处理, 前面增加一个前缀, 保证是唯一的即可. 防止文件名重复造成覆盖
   //构建了一个上传的文件的完整路径[目录+文件名]
   name = UUID.randomUUID().toString() + "_" + System.currentTimeMillis() + "_" + name;
   String fileFullPath = fileRealPathDirectory + "\\" + name;
   //保存
   fileItem.write(new File(fileFullPath));
   //关闭流
   fileItem.getOutputStream().close();
   //更新家居图的图片
   furn.setImagePath(WebUtils.FURN_IMG_DIRECTORY + name);
}

全部代码

protected void update(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
   //将提交修改的家居信息,封装成Furn对象

   //如果你的表单是enctype="multipart/form-data", req.getParameter("id") 得不到id
   int id = DataUtils.parseInt(req.getParameter("id"), 0);
   //获取到对应furn对象[从db中获取]
   Furn furn = furnService.queryFurnById(id);
   //todo 如果furn为null, 则return

   //1.判断是不是文件表单
   if (ServletFileUpload.isMultipartContent(req)) {
       //2.创建DiskFileItemFactory对象, 用于构建一个解析上传数据的工具对象
       DiskFileItemFactory diskFileItemFactory = new DiskFileItemFactory();
       //3.构建一个解析上传数据的工具对象
       ServletFileUpload servletFileUpload = new ServletFileUpload(diskFileItemFactory);
       //解决中文乱码问题
       servletFileUpload.setHeaderEncoding("utf-8");
       //4.servletFileUpload对象可以把表单提交的数据[文本/文件], 封装到FileItem文件项中
       try {
           List<FileItem> list = servletFileUpload.parseRequest(req);
           for (FileItem fileItem : list) {
               //判断是不是一个文件 => 文本表单字段
               if (fileItem.isFormField()) {
                   将提交的家居信息, 封装成Furn对象
                   switch (fileItem.getFieldName()) {
                       case "name"://家居名
                           furn.setName(fileItem.getString("utf-8"));
                           break;
                       case "business"://制造商
                           furn.setBusiness(fileItem.getString("utf-8"));
                           break;
                       case "price"://价格
                           furn.setPrice(new BigDecimal(fileItem.getString()));
                           break;
                       case "saleNum"://销量
                           furn.setSaleNum(Integer.parseInt(fileItem.getString()));
                           break;
                       case "inventory"://库存
                           furn.setInventory(Integer.parseInt(fileItem.getString()));
                           break;
                   }
               } else {
                   //文件表单字段 => 获取上传的文件的名字
                   String name = fileItem.getName();

                   //如果用户没有选择新的图片, name = ""
                   if (!"".equals(name)) {
                       //1.把上传到到服务器 temp目录下的文件保存到指定的目录
                       String filePath = "/" + WebUtils.FURN_IMG_DIRECTORY;
                       //2.获取完整的目录
                       String fileRealPath = req.getServletContext().getRealPath(filePath);
                       System.out.println("fileRealPath= " + fileRealPath);
                       //3.创建这个上传的目录
                       File fileRealPathDirectory = new File(fileRealPath);
                       if (!fileRealPathDirectory.exists()) {
                           fileRealPathDirectory.mkdirs();
                       }
                       //4.将文件拷贝到fileRealPathDirectory目录下
                       //对上传的文件名进行处理, 前面增加一个前缀, 保证是唯一的即可. 防止文件名重复造成覆盖
                       //构建了一个上传的文件的完整路径[目录+文件名]
                       name = UUID.randomUUID().toString() + "_" + System.currentTimeMillis() + "_" + name;
                       String fileFullPath = fileRealPathDirectory + "\\" + name;
                       //保存
                       fileItem.write(new File(fileFullPath));
                       //关闭流
                       fileItem.getOutputStream().close();
                       //更新家居图的图片
                       furn.setImagePath(WebUtils.FURN_IMG_DIRECTORY + name);
                   }
               } 
           } 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;
   }

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

🍍分页导航完善

需求分析

  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>

🐀🐂🐅🐇🐉🐍🐎🐏

  • 6
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

~ 小团子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值