(旧版) 家居购项目 1.分页导航 2.购物车 3.订单生成 4.权限验证 5.事务管理 6.上传图片

news/2023/12/4 7:54:35

文章目录

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

🐀Java后端经典三层架构

在这里插入图片描述

分层对应包说明
web层com.zzw.furns.web/servlet/controller/handler接受用户请求, 调用service
service层com.zzw.furns.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, 只是变得更加强大

🐇开发环境搭建

详情请参考👉

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

🐇会员注册

🌳前端验证用户注册信息

script引文件是src属性

    <script type="text/javascript" src="../../script/jquery-3.6.0.min.js"></script><script type="text/javascript">$(function () {//页面加载完毕后执行 function$("#sub-btn").click(function () {//采用过关斩将法//正则表达式验证用户名var usernameValue = $("#username").val();var usernamePattern = /^\w{6,10}$/;if (!usernamePattern.test(usernameValue)) {$("span[class='errorMsg']").text("用户名格式不对, 需要6-10个字符(大小写字母,数字,下划线)");return false;}//验证密码var passwordValue = $("#password").val();var passwordPattern = /^\w{6,10}$/;if (!passwordPattern.test(passwordValue)) {$("span.errorMsg").text("密码格式不对, 需要6-10个字符(大小写字母,数字,下划线)");return false;}//两次密码要相同var rePwdValue = $("#repwd").val();if (passwordValue != rePwdValue) {$("span.errorMsg").text("两次密码不相同");return false;}//这里仍然采用过关斩将法//验证邮件var emailVal = $("#email").val();//在java中, 正则表达式的转义是\\; 在js中, 正则表达式转义是\var emailPattern = /^[\w-]+@([a-zA-Z]+\.)+[a-zA-Z]+$/;if (!emailPattern.test(emailVal)) {$("span.errorMsg").text("电子邮件的格式不正确, 请重新输入");return false;}//这里暂时不提交=>显示验证通过$("span.errorMsg").text("验证通过");return false;});})</script>

🌳思路分析

创建表->javabean->DAO->service
在这里插入图片描述

分层对应包说明
web层RegisterServlet.java接受浏览器发送数据; 调用相关的service;根据执行结果,返回页面数据
service层MemberService.javaService接口包
MemberServiceImpl.javaService接口实现类
dao持久层MemberDAO.javaDao接口包
MemberDAOImplDao接口实现类
实体bean对象Member.javaJavaBean类
工具类JdbcUtilsByDruid.java工具类

🍉创建表

在这里插入图片描述

🍉创建实体类

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

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

🍉DAO

在这里插入图片描述

🍌MemberDAOImpl
public class MemberDAOImpl extends BasicDAO<Member> implements MemberDAO {/*** 通过用户名返回对应的Member* @param username 用户名* @return 对应的Member, 如果没有该Member返回null*/@Overridepublic Member queryMemberByUsername(String username) {//现在sqlyog测试, 然后再拿到程序中, 这样可以提高我们的开发效率, 减少不必要的bugString sql = "SELECT id, username, `password`, email FROM member WHERE username = ?";Member member = querySingle(sql, Member.class, username);return member;}/*** 保存一个会员* @param member 传入一个Member对象* @return 如果返回-1, 就是失败; 返回其它的数字, 就是受影响的行数*/@Overridepublic int saveMember(Member member) {//连同单引号一并换成 ? , 它会自动加上单引号String sql = "INSERT INTO member(id, username, `password`, email) " +"VALUES(NULL, ?, MD5(?), ?)";int updateRows = update(sql, member.getUsername(), member.getPassword(), member.getEmail());return updateRows;}
}

测试
在这里插入图片描述

🍉Service

在这里插入图片描述

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

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

🌳接通web层

在这里插入图片描述
将所有路径修改成相对路径
在这里插入图片描述
在这里插入图片描述
配置RegisterServlet, 请求RegisterServlet
在这里插入图片描述

🐇会员登陆

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

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

web层

  1. 新建LoginServlet
    在这里插入图片描述
    login.html请求
    在这里插入图片描述
    login_ok.html
    在这里插入图片描述
    快捷键
    在这里插入图片描述
    效果
    在这里插入图片描述
    在这里插入图片描述

🌳登陆错误_信息回显

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

LoginServlet
在这里插入图片描述
login.jsp
在这里插入图片描述
添加span标签
在这里插入图片描述

🐇servlet合并

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

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

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

🌳显示家居

需求分析

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

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

  1. 页面准备在这里插入图片描述在这里插入图片描述
  2. 新建admin表 👉 参考member表
    新建furn表
    在这里插入图片描述
  3. 新建Admin实体类
    新建Furn实体类
    在这里插入图片描述
  4. 书写AdminDAO, AdminDAOImpl, 并测试; 书写AdminService, AdminServiceImpl, 并测试 👉 参考Member
    书写FurnDAO, FurnDAOImpl 👉 并测试
    在这里插入图片描述
  5. 书写FurnService, FurnServiceImpl 👉 并测试
    在这里插入图片描述
  6. 接通web层
    配置web.xml, 书写AdminServlet
    在这里插入图片描述
    配置web.xml, 书写FurnServlet
    在这里插入图片描述
    在这里插入图片描述
    将doGet()方法移到BasicServlet中
    在这里插入图片描述
  7. 前端页面
    manage_login.jsp 登录验证, 请求AdminServlet
    在这里插入图片描述
    在这里插入图片描述
    manage_menu.jsp 请求FurnServlet
    在这里插入图片描述
    furn_manage.jsp 显示家居信息
    在这里插入图片描述

🌳添加家居

思路分析

  1. 请求添加家居, 请求FurnServlet的add方法, 将前端提交的数据封装到Furn对象
  2. 调用FurnService.add(Furn furn)方法
  3. 跳转到显示家居的页面
  1. FurnDAO
    在这里插入图片描述
  2. FurnService
    在这里插入图片描述
  3. web层
    FurnServlet
    在这里插入图片描述
    解决中文乱码问题
    在这里插入图片描述
  4. 前端: 添加furn_add.jsp
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
🍉解决重复添加

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

🍉后端数据校验说明

后端方案一
在这里插入图片描述后端方案二
在这里插入图片描述
前端方案三
在这里插入图片描述

🍉BeanUtils自动封装Bean

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

  1. 使用BeanUtils自动封装javabean
    在这里插入图片描述debug小技巧👉
    在这里插入图片描述
  2. 报错
    原因: 由于前端没有传imagePath的字段, 所有后端在构建的furn对象的时候, imagePath传了个null,
    解决方案👇
    在这里插入图片描述
  3. 将 把数据自动封装成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
    >
  3. web层
    FurnServlet
    在这里插入图片描述
  4. furn_add.jsp页面
    jQuery操作父元素, 兄弟元素, 子元素, 请移步👉
    js弹框请移步👉
    在这里插入图片描述
    在这里插入图片描述

🌳修改家具

思路分析

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

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

  1. FurnDAO
    在这里插入图片描述
    在这里插入图片描述
  2. FurnService
    在这里插入图片描述
    在这里插入图片描述
  3. web层
    FurnServlet
    在这里插入图片描述
    在这里插入图片描述
  4. 前端
    furn_manage.jsp 点击修改,发出请求
    在这里插入图片描述
    furn_update.jsp 修改数据,点击提交
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

🍃后台分页

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

🍒新建Page类

在这里插入图片描述

🍒DAO

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

🍒Service

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

🍒web层获取page对象

在这里插入图片描述

🍒前端页面

取缔list方法
在这里插入图片描述
在这里插入图片描述
furn_manage.jsp
在这里插入图片描述

🍅后台分页导航

程序框架图

<!--  Pagination Area Start -->
<div class="pro-pagination-style text-center mb-md-30px mb-lm-30px mt-6" data-aos="fade-up"><ul><%--如果当前页 > 1, 就显示首页和上一页--%><li><a style="pointer-events:${requestScope.page.pageNo == 1 ? "none" : "auto"};"href="manage/furnServlet?action=page&pageNo=1&pageSize=${requestScope.page.pageSize}">首页</a></li><li><a style="pointer-events: ${requestScope.page.pageNo == 1 ? "none" : "auto"}"href="manage/furnServlet?action=page&pageNo=${requestScope.page.pageNo - 1}&pageSize=${requestScope.page.pageSize}">上一页</a></li><%--显示所有的分页数 先确定开始的页数 begin 1; 再确定结束的页数 end=>pageTotal--%><%--最多显示10, 这里涉及算法--%><c:set scope="page" var="begin" value="1"></c:set><c:set scope="page" var="end" value="${requestScope.page.pageTotal}"></c:set><%--循环显示--%><c:forEach begin="${pageScope.begin}" end="${pageScope.end}" var="i"><%--总的页数--%><%--如果i是当前页, 就使用class="active"来修饰--%><li><a class="${i eq requestScope.page.pageNo ? "active" : ""}"href="manage/furnServlet?action=page&pageNo=${i}&pageSize=${requestScope.page.pageSize}">${i}</a></li></c:forEach><%--如果当前页 < 总的页数, 就显示末页和下一页--%><li><a style="pointer-events:${requestScope.page.pageNo == requestScope.page.pageTotal ? "none" : "auto"};"href="manage/furnServlet?action=page&pageNo=${requestScope.page.pageNo + 1}&pageSize=${requestScope.page.pageSize}">下一页</a></li><li><a style="pointer-events:${requestScope.page.pageNo == requestScope.page.pageTotal ? "none" : "auto"};"href="manage/furnServlet?action=page&pageNo=${requestScope.page.pageTotal}&pageSize=${requestScope.page.pageSize}">末页</a></li><li><a>共${requestScope.page.pageTotal}</a></li><li><a>共${requestScope.page.totalRow}记录</a></li></ul>
</div>
<!--  Pagination Area End -->
🍅修改后返回原页面

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

🍅删除后返回原页面

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

🍅添加后返回原页面

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

🍃首页分页

需求分析

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

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

实现>1. 新建CustomerFurnServlet
在这里插入图片描述
2. 前端页面
在这里插入图片描述
直接请求CustomerFurnServlet, 获取网站首页要显示的分页数据
类似我们网站的入口页面👉jsp请求转发标签
在这里插入图片描述
index.jsp
在这里插入图片描述
3. 显示数据

<c:forEach items="${requestScope.page.items}" var="furn"><div class="col-lg-3 col-md-6 col-sm-6 col-xs-6 mb-6" data-aos="fade-up"data-aos-delay="200"><!-- Single Product --><div class="product"><div class="thumb"><a href="shop-left-sidebar.html" class="image"><img src="${furn.imagePath}" alt="Product"/><img class="hover-image" src="assets/images/product-image/5.jpg"alt="Product"/></a><span class="badges"><span class="sale">-10%</span><span class="new">New</span></span><div class="actions"><a href="#" class="action wishlist" data-link-action="quickview"title="Quick view" data-bs-toggle="modal"data-bs-target="#exampleModal"><iclass="icon-size-fullscreen"></i></a></div><button title="Add To Cart" class=" add-to-cart">AddTo Cart</button></div><div class="content"><h5 class="title"><a href="shop-left-sidebar.html">Simple ${furn.name} </a></h5><span class="price"><span class="new">家居: ${furn.name}</span></span><span class="price"><span class="new">厂商: ${furn.business}</span></span><span class="price"><span class="new">价格: ${furn.price}</span></span><span class="price"><span class="new">销量: ${furn.saleNum}</span></span><span class="price"><span class="new">库存: ${furn.inventory}</span></span></div></div></div>
</c:forEach>

分页导航

<!--  Pagination Area Start -->
<div class="pro-pagination-style text-center mb-md-30px mb-lm-30px mt-6" data-aos="fade-up"><ul><li><a style="pointer-events: ${requestScope.page.pageNo > 1 ? "auto" : "none"}"href="customerFurnServlet?action=page&pageNo=1&pageSize=${requestScope.page.pageSize}">首页</a></li><li><a style="pointer-events: ${requestScope.page.pageNo > 1 ? "auto" : "none"}"href="customerFurnServlet?action=page&pageNo=${requestScope.page.pageNo - 1}&pageSize=${requestScope.page.pageSize}">上一页</a></li><c:set scope="page" var="begin" value="1"></c:set><c:set scope="page" var="end" value="${requestScope.page.pageTotal}"></c:set><c:forEach begin="${begin}" end="${end}" var="i"><li><a class="${i == requestScope.page.pageNo ? "active" : ""}"href="customerFurnServlet?action=page&pageNo=${i}&pageSize=${requestScope.page.pageSize}">${i}</a></li></c:forEach><li><a style="pointer-events: ${requestScope.page.pageNo < requestScope.page.pageTotal ? "auto" : "none"}"href="customerFurnServlet?action=page&pageNo=${requestScope.page.pageNo + 1}&pageSize=${requestScope.page.pageSize}">下一页</a></li><li><a style="pointer-events: ${requestScope.page.pageNo < requestScope.page.pageTotal ? "auto" : "none"}"href="customerFurnServlet?action=page&pageNo=${requestScope.page.pageTotal}&pageSize=${requestScope.page.pageSize}">末页</a></li><li><a>共${requestScope.page.pageTotal}</a></li><li><a>共${requestScope.page.totalRow}记录</a></li></ul>
</div>
<!--  Pagination Area End -->
🍅首页搜索

需求分析

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

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

  1. DAO
    模糊查询👉
    在这里插入图片描述
    在这里插入图片描述
  2. service
    在这里插入图片描述
    在这里插入图片描述
  3. web层 CustomerFurnServlet
    page方法就被抛弃了
    在这里插入图片描述
  4. 前端 index.jsp

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

🌳会员显示登录名

需求分析

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

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

实现
重命名时, 会联动修改在这里插入图片描述在这里插入图片描述
将login_ok.jsp中的index.html改成index.jsp. 注意, 不要改成views/customer/index.jsp在这里插入图片描述在这里插入图片描述在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

🍅注销登录

思路分析

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

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

🍅验证码

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

  1. web层
    KaptchaServlet -> 引入kaptcha-2.3.2.jar包, 在web.xml中配置KaptchaServlet
    在这里插入图片描述在这里插入图片描述
    在这里插入图片描述
    MemberServlet
    在这里插入图片描述
    在这里插入图片描述
  2. 前端页面
    login.jsp
    在这里插入图片描述
    验证码不能为空
    在这里插入图片描述点击图片更换验证码
    在这里插入图片描述
    将register_ok.html, register_fail.html改造成jsp页面
    login.jsp页面总是默认停留在会员登录的div内, 修改
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述注册回显信息
    在这里插入图片描述在这里插入图片描述

🌳购物车

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

cartItem模型
在这里插入图片描述

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

实现

  1. 创建CartServlet
    在这里插入图片描述
  2. 首页获取id请求后台
    在这里插入图片描述
    在这里插入图片描述
  3. 首页购买的商品总数量在这里插入图片描述
    在这里插入图片描述
🍆显示购物车

需求分析

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

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

  1. 走通购物车
    cart.jsp
    在这里插入图片描述
    index.jsp跳转
    排错
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    定位
    在这里插入图片描述
    在这里插入图片描述
    页面源代码
    在这里插入图片描述
  2. 显示家居项
<tbody>
<%--找到显示购物车项,  进行循环的items--%>
<c:if test="${not empty sessionScope.cart.items}"><%--1.sessionScope.cart.items => 取出的是HashMap<Integer, CartItem>2.所以通过foreach标签取出的每一个对象, 即entry是 HashMap<Integer, CartItem>的 k-v3.var其实就是 entry4.所以要取出cartItem对象, 是通过 entry.value取出--%><c:forEach items="${sessionScope.cart.items}" var="entry"><tr><td class="product-thumbnail"><a href="#"><img class="img-responsive ml-3"src="assets/images/product-image/1.jpg"alt=""/></a></td><td hidden="hidden" class="product-name"><a href="#">${entry.key}</a></td><%--隐藏域--%><td class="product-name"><a href="#">${entry.value.name}</a></td><td class="product-price-cart"><span class="amount">$${entry.value.price}</span></td><td class="product-quantity"><div class="cart-plus-minus" onclick="change1(this)"><input class="cart-plus-minus-box" type="text" name="qtyButton"value="${entry.value.count}"/></div></td><td class="product-subtotal">$${entry.value.totalPrice}</td><td class="product-remove"><a href="cartServlet?action=del&key=${entry.key}"><iclass="icon-close"></i></a></td></tr></c:forEach>
</c:if>
</tbody>
  1. 计算总价
    在这里插入图片描述
    在这里插入图片描述
🍆修改购物车

需求分析

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

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

  1. Cart增加方法
    在这里插入图片描述
  2. CartServlet
    在这里插入图片描述
  3. 前端
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    cart.jsp
    在这里插入图片描述
    在这里插入图片描述
🍆删除购物车

需求分析

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

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

  1. 删除购物车
    在这里插入图片描述
    在这里插入图片描述
  2. 清空购物车
    在这里插入图片描述
    在这里插入图片描述

🌳生产订单

需求分析

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

程序框架图

🍉创建表

order表

-- 创建家居网购需要的数据库和表
-- 删除数据库
DROP DATABASE IF EXISTS home_furnishing;-- 删除表
DROP TABLE `order`;-- 创建数据库
CREATE DATABASE home_furnishing;-- 切换
USE home_furnishing;-- 创建订单表
-- 每个字段应当使用 not null 来约束
-- 字段类型的设计, 应当和相关联表的字段类型相对应
-- 是否需要使用外键? 
-- 1.需要[可以从db层保证数据的一致性(早期hibernate框架要求必须使用外键)]
-- 2.不需要[外键对效率有影响, 应当从程序的业务层保证数据的一致性(推荐)]
CREATE TABLE `order` (id VARCHAR(60) PRIMARY KEY, -- 订单编号create_time DATETIME NOT NULL,-- 年月日 时分秒price DECIMAL(10,2) NOT NULL,-- 订单价格`status` TINYINT NOT NULL, -- 订单状态(1未发货 2已发货 3已结账)member_id INT NOT NULL -- 谁的订单
)CHARSET utf8 ENGINE INNODB;

order_item表

-- 创建家居网购需要的数据库和表
-- 删除数据库
DROP DATABASE IF EXISTS home_furnishing;-- 删除表
DROP TABLE order_item;-- 创建数据库
CREATE DATABASE home_furnishing;-- 切换
USE home_furnishing;-- 创建订单明细表
CREATE TABLE order_item (id INT PRIMARY KEY AUTO_INCREMENT, -- 订单明细id`name` VARCHAR(32) NOT NULL,-- 家居名`count` INT UNSIGNED NOT NULL,-- 数量price DECIMAL(10, 2) NOT NULL,-- 价格total_price DECIMAL(10, 2) NOT NULL,-- 订单项的总价格order_id VARCHAR(60) NOT NULL -- 订单编号
)CHARSET utf8 ENGINE INNODB;

🍉实体类

订单表

public class Order {private String id;private Date createTime;private BigDecimal price;private Integer status;private Integer memberId;public Order() {}//有参构造器//getter方法, setter方法
}

订单明细表

public class OrderItem {private Integer id;private String name;private Integer count;private BigDecimal price;private BigDecimal totalPrice;private String orderId;public OrderItem() {}//有参构造器//getter方法, setter方法
}

🍉DAO

OrderDAO
在这里插入图片描述
OrderItemDAO

在这里插入图片描述

🍉service

在这里插入图片描述

public class OrderServiceImpl implements OrderService {private OrderDAO orderDAO = new OrderDAOImpl();private OrderItemDAO orderItemDAO = new OrderItemDAOImpl();private FurnDAO furnDAO = new FurnDAOImpl();//在这里可以感受到javaee分层的好处. 在service层, 通过组合多个dao的方法,// 完成某个业务 慢慢体会好处@Overridepublic String saveOrder(Cart cart, int memberId) {//将cart购物车的数据以order和orderItem的形式保存到DB中//因为生成订单会操作多张表, 因此会涉及到多表事务的问题, ThreadLocal+Mysql事务机制+过滤器//1.通过cart对象, 构建一个对应的order对象//  先生成一个UUID, 表示当前的订单号, UUID是唯一的String orderId = UUID.randomUUID().toString();//订单idOrder order = new Order(orderId, new Date(), cart.getCartTotalPrice(), 0, memberId);//保存order到数据表orderDAO.saveOrder(order);//订单生成成功//通过cart对象, 遍历CartItem, 构建OrderItem对象, 并保存到对应的order_item表Map<Integer, CartItem> cartItems = cart.getItems();String orderItemId = "";for (CartItem cartItem : cartItems.values()) {//通过cartItem对象构建了orderItem对象OrderItem orderItem = new OrderItem(null, cartItem.getName(), cartItem.getCount(),cartItem.getPrice(), cartItem.getTotalPrice(), orderId);//保存orderItemDAO.saveOrderItem(orderItem);//更新furn表  saleNum销量 - inventory库存//(1) 获取furn对象Furn furn = furnDAO.queryFurnById(cartItem.getId());//(2) 更新furn对象的 saleNum销量 - inventory库存furn.setInventory(furn.getInventory() - cartItem.getCount());furn.setSaleNum(furn.getSaleNum() + cartItem.getCount());//(3) 更新到数据表furnDAO.updateFurn(furn);}//清空购物车cart.clear();return orderId;}
}

在这里插入图片描述

🍉servlet

public class OrderServlet extends BasicServlet {//定义属性private OrderService orderService = new OrderServiceImpl();/*** 生成订单* @param request* @param response* @throws ServletException* @throws IOException*/protected void saveOrder(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {HttpSession session = request.getSession();//获取购物车Cart cart = (Cart) session.getAttribute("cart");//如果cart为空, 说明会员没有购买任何家居, 转发到首页if (cart == null) {request.getRequestDispatcher("/index.jsp").forward(request, response);return;}//获取到登陆的member对象Member member = (Member) session.getAttribute("member");if (member == null) {//说明用户没有登录, 转发到登陆页面//重定向到登陆页面request.getRequestDispatcher("/views/member/login.jsp").forward(request, response);return;//直接返回}//可以生成订单String orderId = orderService.saveOrder(cart, member.getId());//订单, 订单明细已生成session.setAttribute("orderId", orderId);//订单id//使用重定向放入到checkout.jspresponse.sendRedirect(request.getContextPath() + "/views/order/checkout.jsp");}
}

在这里插入图片描述

  • 防止生成空订单
    hashMap.clear之后, 是置空还是size置为0
    在这里插入图片描述
    在这里插入图片描述

🍉前端

在这里插入图片描述

checkout.html修改为checkout.jsp
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

🌳显示订单[订单管理]

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

需求分析

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

在这里插入图片描述

思路分析

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

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

  • 管理订单

需求分析

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

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

  1. DAO
    在这里插入图片描述
  2. service
    在这里插入图片描述
  3. web层
    在这里插入图片描述
  4. 前端
    index.jsp, cart.jsp, login_ok.jsp, checkout.jsp, order.jsp均可跳转到订单管理
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
  • 管理订单项

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

  1. DAO
    在这里插入图片描述
    在这里插入图片描述
  2. service
    在这里插入图片描述
    在这里插入图片描述
  3. web层
    在Order实体类中新增count属性, 在生成订单时, 将totalCount赋给count
    在这里插入图片描述
  4. 前端
    order.jsp跳转到OrderItemServlet
    在这里插入图片描述
    order_detail.jsp
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    order表增加count列 - ALTER TABLE order ADD COUNT INT UNSIGNED NOT NULL AFTER price;
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

🌈过滤器权限验证

需求分析

  1. 加入过滤器权限验证
  2. 如果没有登陆, 查看购物车和添加到购物车, 就会自动转到会员登陆页面
  1. 配置拦截url
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"version="4.0"><!--过滤器一般我们配置在上面--><filter><filter-name>AuthFilter</filter-name><filter-class>com.zzw.furns.filter.AuthFilter</filter-class><init-param><!--这里配置了后, 还需要在过滤器中处理--><param-name>excludedUrls</param-name><param-value>/views/manage/manage_login.jsp,/views/member/login.jsp</param-value></init-param></filter><filter-mapping><filter-name>AuthFilter</filter-name><!--这里配置要验证的url1.在filter-mapping中的url-pattern配置 要拦截/验证的url2.对于我们不去拦截的url, 就不配置3.对于要拦截的目录中的某些要放行的资源, 再通过配置指定--><url-pattern>/views/cart/*</url-pattern><url-pattern>/views/manage/*</url-pattern><url-pattern>/views/member/*</url-pattern><url-pattern>/views/order/*</url-pattern><url-pattern>/cartServlet</url-pattern><url-pattern>/manage/furnServlet</url-pattern><url-pattern>/orderServlet</url-pattern><url-pattern>/orderItemServlet</url-pattern></filter-mapping>
</web-app>

2.过滤器逻辑判断

/**
* 这是用于权限验证的过滤器, 对指定的url进行验证
* 如果登陆过, 就放行; 如果没有登陆, 就回到登陆页面
* @author 赵志伟
* @version 1.0
*/
@SuppressWarnings({"all"})
public class AuthFilter implements Filter {private List<String> excludedUrls;@Overridepublic void init(FilterConfig filterConfig) throws ServletException {//获取到配置的excludedUrlsString strExcludedUrls = filterConfig.getInitParameter("excludedUrls");String[] split = strExcludedUrls.split(",");//将 splitUrl 转成 listexcludedUrls = Arrays.asList(split);System.out.println("excludedUrls= " + excludedUrls);}@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {System.out.println("请求/cartServlet 被拦截...");HttpServletRequest request = (HttpServletRequest) servletRequest;//得到请求的urlString url = request.getServletPath();System.out.println("url= " + url);//判断是否要验证if (!excludedUrls.contains(url)) {//获取到登陆的member对象Member member = (Member) request.getSession().getAttribute("member");if (member == null) {//说明用户没有登录//转发到登陆页面, 转发不走过滤器servletRequest.getRequestDispatcher("/views/member/login.jsp").forward(servletRequest, servletResponse);重定向-拦截-重定向-拦截-重定向-拦截//((HttpServletResponse) servletResponse)//        .sendRedirect(request.getContextPath() + "/views/member/login.jsp");return;//直接返回}}//验证通过, 放行filterChain.doFilter(servletRequest, servletResponse);System.out.println("请求/cartServlet验证通过, 放行");}@Overridepublic void destroy() {}
}
  1. 处理管理员登陆
    >
    在这里插入图片描述

🌈事务管理

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, 这里存放一个Connectionprivate static ThreadLocal<Connection> threadlocalConn = new ThreadLocal<>();/*** 从ThreadLocal获取connection, 从而保证在一个线程中* 获取的是同一个Connection*/public static Connection getConnection() {Connection connection = threadlocalConn.get();if (connection == null) {//说明当前的threadlocal没有这个连接try {//就从数据库连接池中取出连接放入threadlocalconnection = dataSource.getConnection();//将连接设置为手动提交, 既不要让它自动提交connection.setAutoCommit(false);threadlocalConn.set(connection);} catch (SQLException e) {throw new RuntimeException(e);}}return connection;}/*** 提交事务*/public static void commit() {Connection connection = threadlocalConn.get();if (connection != null) {try {connection.commit();} catch (SQLException e) {throw new RuntimeException(e);} finally {try {connection.close();} catch (SQLException e) {throw new RuntimeException(e);}}//1.当提交后, 需要把connection从threadlocalConn中清除掉//2.不然会造成threadlocalConn长时间持有该连接, 会影响效率//3.也因为Tomcat底层使用的是线程池技术threadlocalConn.remove();}}/*** 说明: 所谓回滚是 回滚/撤销 和connection管理的操作 删除/修改/添加*/public static void rollback() {Connection connection = threadlocalConn.get();if (connection != null) {try {connection.rollback();} catch (SQLException e) {throw new RuntimeException(e);} finally {try {connection.close();} catch (SQLException e) {throw new RuntimeException(e);}}threadlocalConn.remove();}}
  1. 修改BasicDao
    删掉各个方法finally代码块里的close方法. 只有在事务结束后才实施关闭连接的操作. 一是提交事务后关闭连接; 二是增删改出错后, 回滚关闭连接.
   public List<T> queryMany(String sql, Class<T> clazz, Object... objects) {Connection connection = null;try {connection = JdbcUtilsByDruid.getConnection();List<T> tList =queryRunner.query(connection, sql, new BeanListHandler<>(clazz), objects);return tList;} catch (SQLException e) {throw new RuntimeException(e);//编译异常->运行异常抛出}}//查询单行, 返回的是一个对象public T querySingle(String sql, Class<T> clazz, Object... objects) {Connection connection = null;try {connection = JdbcUtilsByDruid.getConnection();T object= queryRunner.query(connection, sql, new BeanHandler<>(clazz), objects);return object;} catch (Exception e) {throw new RuntimeException(e);}}//查询某一字段public Object queryScalar(String sql, Object... objects) {Connection connection = null;try {connection = JdbcUtilsByDruid.getConnection();Object query = queryRunner.query(connection, sql, new ScalarHandler(), objects);return query;} catch (Exception e) {throw new RuntimeException(e);}}public int update(String sql, Object... objects) {Connection connection = null;try {//这里是从数据库连接池获取connection//注意:每次从连接池中取出connection, 不能保证是同一个//1.我们目前已经是从和当前线程关联的ThreadLocal获取的connection//2.所以可以保证是同一个连接[在同一个线程中/在同一个请求中 => 因为一个请求对应一个线程]connection = JdbcUtilsByDruid.getConnection();return queryRunner.update(connection, sql, objects);} catch (Exception e) {throw new RuntimeException(e);}}
  1. 控制层进行事务管理
    前提OrderServiceImpl里报错的代码取消try-catch, 在OrderServlet控制层捕获
       //1.如果我们只是希望对orderService.saveOrder()方法进行事务控制//2.那么我们可以不使用过滤器,直接在这个位置进行提交和回滚即可//可以生成订单String orderId = null;//订单, 订单明细已生成try {orderId = orderService.saveOrder(cart, member.getId());JdbcUtilsByDruid.commit();//提交} catch (Exception e) {JdbcUtilsByDruid.rollback();e.printStackTrace();}
🌈Transaction过滤器

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

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

  1. 在web.xml中配置
   <filter><filter-name>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@38f54ed7declaredMethod.invoke(this, req, resp);System.out.println("this.getClass() = " + this.getClass());} catch (Exception e) {//将发生的异常, 继续throwthrow new RuntimeException(e);}
  1. 在代码执行完毕后, 会运行到Transaction过滤器的后置代码, 在这里进行异常捕获, 如果发生异常, 则回滚.
   @Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {try {//放行filterChain.doFilter(servletRequest, servletResponse);JdbcUtilsByDruid.commit();//统一提交} catch (Exception e) {//出现了异常JdbcUtilsByDruid.rollback();//回滚e.printStackTrace();}}

🌈统一错误页面

需求分析

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

思路分析

  1. 发生错误/异常时, 将错误/异常抛给tomcat
  2. 在web.xml中配置不同错误显示的页面即可
  1. 引入404.html, 500.html, 修改成jsp文件
    页面首行<%@ page contentType="text/html;charset=UTF-8" language="java" %>
    base标签<base href="<%=request.getContextPath()%>/">
    将跳转链接改成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
   @Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {try {//放行filterChain.doFilter(servletRequest, servletResponse);JdbcUtilsByDruid.commit();//统一提交} catch (Exception e) {//出现了异常//只有在try{}中出现了异常, 才会进行catch{}//才会进行回滚JdbcUtilsByDruid.rollback();//回滚//抛出异常, 给tomcat. tomcat会根据error-page来显示对应页面throw new RuntimeException(e);//e.printStackTrace();}}

🌈Ajax检验注册名

需求分析

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

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

  1. MemberServlet - 返回json格式的字符串 - 方式一
protected void isExistByName(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {//1.获取用户名String username = req.getParameter("username");//2.调用serviceboolean existsByUsername = memberService.isExistsByUsername(username);//3.思路//(1)如果返回json格式[不要乱写, 要根据前端的需求来写]//(2)因为前后端都是我们自己写的, 格式我们自己定义//(3){"isExist": true};//(4)先用最简单的方法拼接 => 一会改进[扩展]String resultJson = "{\"isExist\": " + existsByUsername + "}";//4.返回resp.getWriter().print(resultJson);
}

返回json格式的字符串 - 方式二

protected void isExistByName(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {//1.获取用户名String username = req.getParameter("username");//2.调用serviceboolean existsByUsername = memberService.isExistsByUsername(username);//3.思路//(1)如果返回json格式[不要乱写, 要根据前端的需求来写]//(2)因为前后端都是我们自己写的, 格式我们自己定义//(3){"isExist": true};//(4)先用最简单的方法拼接 => 一会改进[扩展]//String resultJson = "{\"isExist\": " + existsByUsername + "}";字符串就不需要再转//(5)将要返回的数据封装成map => json格式Map<Object, Object> map = new HashMap<>();map.put("isExist", existsByUsername);//map.put("email", "978964140@qq.com");//map.put("phone", "13031748275");//4.返回json格式的数据Gson gson = new Gson();String resultJson = gson.toJson(map);resp.getWriter().print(resultJson);
}
  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, 不赋值默认值为nullCartItem cartItem = new CartItem(id, furn.getName(), 1, furn.getPrice(), furn.getPrice());//将家居明细加入到购物车中. 如果家居id相同,数量+1;如果是一条新的商品,那么就新增cart.addItem(cartItem, furn.getInventory());System.out.println("cart= " + cart);//规定格式 {"cartTotalCount": 3}//方式一://String resultJson = "{\"cartTotalCount\": " + cart.getTotalCount() + "}";//response.getWriter().print(resultJson);//方式二: 创建map,可扩展性强Map<Object, Object> map = new HashMap<>();map.put("cartTotalCount", cart.getTotalCount());//转成jsonGson gson = new Gson();String resultJson = gson.toJson(map);//返回response.getWriter().print(resultJson);//String referer = request.getHeader("referer");//response.sendRedirect(referer);}
  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: XMLHttpRequestreturn "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));}

修改AuthFilter.java

if (member == null) {//说明用户没有登录if (!WebUtils.isAjaxRequest(request)) {//如果不是ajax请求//转发到登陆页面, 转发不走过滤器servletRequest.getRequestDispatcher("/views/member/login.jsp").forward(servletRequest, servletResponse);} else {//如果是ajax请求//返回ajax请求, 按照json格式返回 {"url": url}//1.构建mapMap<Object, Object> map = new HashMap<>();map.put("url", "views/member/login.jsp");//2.转成json字符串String resultJson = new Gson().toJson(map);//3.返回servletResponse.getWriter().print(resultJson);}重定向-拦截-重定向-拦截-重定向-拦截//((HttpServletResponse) servletResponse)//        .0sendRedirect(request.getContextPath() + "/views/member/login.jsp");return;//直接返回
}

修改getJson

//这里我们使用jquery发出ajax请求, 得到数据进行局部刷新, 解决刷新这个页面效率低的问题
$.getJSON("cartServlet?action=addItemByAjax&id=" + id, function (data) {console.log("data=", data);if (data.url == undefined) {//刷新局部 <span class="header-action-num"></span>$("span.header-action-num").text(data.cartTotalCount);} else {location.href = data.url;}}
)

🌈上传与更新家具图片

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

需求分析

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

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

  1. furn_update.jsp
   <style type="text/css">#pic {position: relative;}input[type="file"] {position: absolute;left: 0;top: 0;height: 180px;opacity: 0;cursor: pointer;}</style>

去掉a标签

<div id="pic"><img class="img-responsive ml-3" src="${requestScope.furn.imagePath}"alt="" id="preView"><input type="file" name="imagePath" id="" >value="${requestScope.furn.imagePath}"οnchange="prev(this)"/>
</div>
  1. 分析空指针异常
    将form表单改成文件表单
    <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") 得不到idint id = DataUtils.parseInt(req.getParameter("id"), 0);//获取到对应furn对象[从db中获取]Furn furn = furnService.queryFurnById(id);//todo 如果furn为null, 则return//1.判断是不是文件表单if (ServletFileUpload.isMultipartContent(req)) {//2.创建DiskFileItemFactory对象, 用于构建一个解析上传数据的工具对象DiskFileItemFactory diskFileItemFactory = new DiskFileItemFactory();//3.构建一个解析上传数据的工具对象ServletFileUpload servletFileUpload = new ServletFileUpload(diskFileItemFactory);//解决中文乱码问题servletFileUpload.setHeaderEncoding("utf-8");//4.servletFileUpload对象可以把表单提交的数据[文本/文件], 封装到FileItem文件项中try {List<FileItem> list = servletFileUpload.parseRequest(req);for (FileItem fileItem : list) {//判断是不是一个文件 => 文本表单字段if (fileItem.isFormField()) {将提交的家居信息, 封装成Furn对象switch (fileItem.getFieldName()) {case "name"://家居名furn.setName(fileItem.getString("utf-8"));break;case "business"://制造商furn.setBusiness(fileItem.getString("utf-8"));break;case "price"://价格furn.setPrice(new BigDecimal(fileItem.getString()));break;case "saleNum"://销量furn.setSaleNum(Integer.parseInt(fileItem.getString()));break;case "inventory"://库存furn.setInventory(Integer.parseInt(fileItem.getString()));break;}} else {//文件表单字段 => 获取上传的文件的名字String name = fileItem.getName();//如果用户没有选择新的图片, name = ""if (!"".equals(name)) {//1.把上传到到服务器 temp目录下的文件保存到指定的目录String filePath = "/" + WebUtils.FURN_IMG_DIRECTORY;//2.获取完整的目录String fileRealPath = req.getServletContext().getRealPath(filePath);System.out.println("fileRealPath= " + fileRealPath);//3.创建这个上传的目录File fileRealPathDirectory = new File(fileRealPath);if (!fileRealPathDirectory.exists()) {fileRealPathDirectory.mkdirs();}//4.将文件拷贝到fileRealPathDirectory目录下//对上传的文件名进行处理, 前面增加一个前缀, 保证是唯一的即可. 防止文件名重复造成覆盖//构建了一个上传的文件的完整路径[目录+文件名]name = UUID.randomUUID().toString() + "_" + System.currentTimeMillis() + "_" + name;String fileFullPath = fileRealPathDirectory + "\\" + name;//保存fileItem.write(new File(fileFullPath));//关闭流fileItem.getOutputStream().close();//更新家居图的图片furn.setImagePath(WebUtils.FURN_IMG_DIRECTORY + name);}}}//跟新furn对象->DBfurnService.updateFurn(furn);System.out.println("更新成功...");//请求转发到 update_ok.jspreq.getRequestDispatcher("/views/manage/update_ok.jsp").forward(req, resp);} catch (Exception e) {throw new RuntimeException(e);}}
}

将checkout.jsp复制成update_ok.jsp

<a class="active" href="manage/furnServlet?action=page&pageNo=${param.pageNo}"><h4>家居修改成功, 点击返回家居管理页面</h4>
</a>

🌈作业布置

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

需求分析

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

AuthFilter - 代码

   @Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {System.out.println("请求/cartServlet 被拦截...");HttpServletRequest request = (HttpServletRequest) servletRequest;//得到请求的urlString url = request.getServletPath();System.out.println("url= " + url);//判断是否要验证if (!excludedUrls.contains(url)) {//获取到登陆的member对象Member member = (Member) request.getSession().getAttribute("member");if (member == null) {//说明用户没有登录if (!WebUtils.isAjaxRequest(request)) {//如果不是ajax请求//转发到登陆页面, 转发不走过滤器servletRequest.getRequestDispatcher("/views/member/login.jsp").forward(servletRequest, servletResponse);} else {//如果是ajax请求//返回ajax请求, 按照json格式返回 {"url": url}//1.构建mapMap<Object, Object> map = new HashMap<>();map.put("url", "views/member/login.jsp");//2.转成json字符串String resultJson = new Gson().toJson(map);//3.返回servletResponse.getWriter().print(resultJson);}return;//直接返回}//如果member不为空if ("admin".equals(member.getUsername())) {//管理员登陆//全部放行} else {//普通用户登录, 部分页面不能放行//如果该用户不是admin, 但是它访问了后台, 就转到管理员登录页面//if ("/manage/furnServlet".equals(url) || url.contains("/views/manage/")) {//.* 匹配任意个字符if ("/manage/furnServlet".equals(url) || url.matches("^/views/manage/.*")) {request.getRequestDispatcher("/views/manage/manage_login.jsp").forward(servletRequest, servletResponse);}}}//如果请求的是登录页面, 那么就放行filterChain.doFilter(servletRequest, servletResponse);System.out.println("请求/cartServlet验证通过, 放行");}

🍍解决图片冗余问题

需求分析

  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>

🐀🐂🐅🐇🐉🐍🐎🐏


http://www.ppmy.cn/news/587190.html

相关文章

声卡设备无法正常工作或初始化的原因和解决方法

先来一个小科普&#xff0c;声卡设备是电脑中负责处理音频信号的硬件部件&#xff0c;它需要与相应的声卡驱动程序配合使用&#xff0c;才能让电脑发出或录制声音。 不过&#xff0c;自带声卡的设备或是自行匹配的声卡设备&#xff0c;也经常出现声卡设备无法正常工作或初始化…

特征点匹配01:halcon的proj_match_points_ransac算子

一、说明 halcon的许多算子内涵两种以上的理论或算法,proj_match_points_ransac就是其中之一,它至少包含了射影几何和ransac算法两个内容。本篇将讲述三个方面: 1、射影几何的透视投影原理; 2、ransac的回归原理; 3、proj_match_points_ransac如何使用的案例; 二、综…

火拼折叠屏:国产手机的杀手锏还是遮羞布?

刚过去的618&#xff0c;手机市场一反常态。 过去&#xff0c;国产安卓旗舰上演“大跳水”&#xff0c;苹果价格坚挺&#xff1b;现在&#xff0c;安卓旗舰优惠力度小&#xff0c;苹果却大降价&#xff0c;iPhone 14 Pro在各平台的优惠力度达到上千元。 IDC中国研究经理郭天翔…

PMP报名具体时间安排,特别注意!

PMP即项目管理专业人士资格认证&#xff0c;英文全称是Project Management Professional,在1999年就已经引入中国。是由美国项目管理协会(PMI)发起&#xff0c;一项严格评估项目管理人员知识技能是否具有高品质的资格认证考试。目的是为了给项目管理人员提供统一的行业标准。 …

將台電C520輸出到底

俺家的康佳LCD電視&#xff0c;購買得比較早&#xff0c;離現在也有好幾個年頭了。現在幾乎成為標配的某些設置它都沒有&#xff0c;這是可以理解的。比如&#xff1a;U盤接口、網絡接口等&#xff0c;一概欠奉。好在它還支持HDMI和分差輸入&#xff0c;這就為咱鼓搗台電C520 M…

【pytest学习总结2.3】 - 如何使用固定装置fixtures(2)

目录 2.3.8 使用mark给固定装置传递数据 2.3.9 将固定装置工厂化 2.3.10 参数化固定装置 2.3.11 使用带有参数化固定装置的标记 2.3.12 使用来自固定装置功能中的固定装置 - 模块化 2.3.13 按固定装置实例自动分组测试 2.3.14 在类和模块中使用usefixtures 2.3.15 固定…

android设备唯一码的获取之二

2019独角兽企业重金招聘Python工程师标准>>> 此篇文章对比android设备唯一码的获取之一看比较好&#xff0c;地址 http://blog.csdn.net/fastthinking/article/details/18001967 如何确定一个android设备&#xff1f; 问题&#xff1a; wifi mac&#xff0c;手机号&…

微信小程序实现仿美团外卖饿了么左右联动页面

话不多说,先上图 注:要实现全部效果,需要引入zan-ui的框架,如果不会引入的话,可以参考我的这篇文章 微信小程序之第三方UI框架 zanui 使用教程 wxml <view class"goods" wx:if"{{status0||item.statusstatus}}"><view class"menu-wrapper…

2019计算机学院年会主持稿,2019年会的主持词

2019年会的主持词 持人上台前&#xff0c;是背景音乐[林-海《踏古》《欢沁》&#xff0c;《门德尔松第四交响曲》《西班牙舞曲》)。上台后&#xff0c;音乐停止。 女(独)&#xff1a;尊敬的各位领导、各位嘉宾、各位朋友、亲爱的员工们! 男、女(合)&#xff1a;大家晚上好! 男(…

这两年的英语学习

从中学算起&#xff0c;学英语10多年了&#xff0c;英语能力一直没太大变化。花费时间&金钱无数。工作之后也一直想在英语上有所提高&#xff0c;好弥补这块的短板。曾经去过中关村某个以巨贵&外教闻名的培训公司做过测试&#xff0c;没有任何的英语对话和测试&#xf…

杂牌平板mt6797_平板的mtk6797是什么,和mt6797是一个么

展开全部 MTK6797和MT6797是同一个产品型号。 联发科10核处理器(CPU),HelioX20,产品编号MT6797。 联发科的HelioX20(MT6797)采e69da5e6ba903231313335323631343130323136353331333433633435用20nm制程,也是世界上第一个三集群三架构处理器。该处理器是三种不同架构的混搭,暗…

量产台电U盘 把USB变成USB-CDROM

经过不断的尝试&#xff0c;我成功地把台电的2G&#xff0c;应该是“酷闪晶彩” 制作了usb-cdrom,里面是一个启动盘&#xff1b;同时还有一个区&#xff0c;还可以当移动硬盘用。 准备工作 1. 台电的2GU盘&#xff0c;其它牌子和不同芯片&#xff0c;要使用不同的量产工具&…

关于买MP4的那点事儿

马上就要临近年关了&#xff0c;回家是一件人人都期望的事情&#xff0c;但在这之前&#xff0c;若干个小时的归途就成了一个现实的问题了&#xff0c;说实在的&#xff0c;我可不像这几个小时都傻傻的在火车上坐着&#xff0c;于是就有了买个 MP4 的想法&#xff0c;于是乎我就…

mp4

1.hd8900歌美, 8g,5 寸, 580 2.s6000歌美&#xff0c;4g,5寸&#xff0c; 380 3.c430台电&#xff0c;8g, 4.5寸&#xff0c; 258,7小时续航, 472*282,screen. otg 4.C520tp台电&#xff0c;8g &#xff0c;5&#xff0c; 380 &#xff0c;5小时, 800*400 1.接着上…

【计算机图形学】期末复习Bezier曲线与曲面篇

【计算机图形学】期末复习Bezier曲线与曲面篇 文章目录 【计算机图形学】期末复习Bezier曲线与曲面篇一、Bezier曲线的定义二、一次Bezier曲线&#xff08;直线&#xff09;三、二次Bezier曲线&#xff08;抛物线&#xff09;四、三次Bezier曲线&#xff08;自由曲线&#xff0…

深入理解 kernel panic 的流程

我们在项目开发过程中&#xff0c;很多时候会出现由于某种原因经常会导致手机系统死机重启的情况&#xff08;重启分Android重启跟kernel重启&#xff0c;而我们这里只讨论kernel重启也就是 kernel panic 的情况&#xff09;&#xff0c;死机重启基本算是影响最严重的系统问题了…

Redisson分布式锁-源码分析

Redisson分布式锁整体流程图 Redisson分布式锁源码流程图 Redisson分布式锁源码解析 获取分布式锁lock private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {//获取当前线程IDlong threadId Thread.currentThread().get…

Python语法允许在类中再定义类,这被称为嵌套类(nested class)或内部类(inner class)

Python语法允许在类中再定义类&#xff0c;这被称为嵌套类&#xff08;nested class&#xff09;或内部类&#xff08;inner class&#xff09;。在Python中&#xff0c;类可以被视为对象&#xff0c;因此可以在类的定义内部创建其他类。 嵌套类的定义发生在外部类的作用域内&…

老司机手把手教php,老司机手把手教你玩驱魔!纯小白无脑驱魔攻略

纯小白无脑驱魔攻略&#xff0c;从升级路上加点&#xff0c;装备选择&#xff0c;斧头还是念珠&#xff0c;做什么异界&#xff0c;想打安图恩怎么办等一系列问题&#xff0c;统统可以在这得到解答。 首先从驱魔的攻击机制说起&#xff0c;众所周知曾经是分为力驱和法驱的&…

Java又双叒叕“凉”了?

前几天&#xff0c;TIOBE的一份6月编程语言榜单公布&#xff1a;Java退出前三&#xff0c;位居第四。一波Java凉了的言论甚嚣尘上。其实不止Java&#xff0c;python、C、C&#xff0c;哪一个没被提过“凉”... 而现实是&#xff0c;Java的招聘需求依然很大&#xff1a; 不可否…
最新文章