分层 | 对应包 | 说明 |
---|---|---|
web层 | com.zzw.furns.web/servlet/controller/handler | 接受用户请求, 调用service |
service层 | com.zzw.furns.service | Service接口包 |
com.zzw.furns.service.impl | Service接口实现类 | |
dao持久层 | com.zzw.furns.dao | Dao接口包 |
com.zzw.furns.dao.impl | Dao接口实现类 | |
实体bean对象 | com.zzw.furns.pojo/entity/domain/bean | JavaBean类 |
工具类 | com.zzw.furns.utils | 工具类 |
测试包 | com.zzw.furns.test | 完成对dao/service测试 |
MVC全称: Model模型, View试图, Controller控制器
MVC最早出现在JavaEE三层中的Web层, 它可以有效地指导WEB层代码如何有效地分离, 单独工作
解读
- model 最早期就是javabean, 就是早期的jsp+servlet+javabean
- 后面业务复杂度越来越高, model逐渐分层化/组件化(service+dao)
- 后面又出现了持久化技术(service+dao+持久化技术(hibernate / mybatis / mybatis-plus))
- MVC依然是原来的mvc, 只是变得更加强大
开发环境搭建具体参考
思路分析
- 会员注册信息, 验证通过后
- 提交给服务器, 如果用户名在数据库中已经存在, 后踢给出提示信息, 并返回重新注册
- 如果用户名没有在数据库中, 完成注册, 并返回注册成功的页面
script引文件是src属性
<script type="text/javascript" src="../../script/jquery-3.6.0.min.js"></script>
<script type="text/javascript">
$(function () {//页面加载完毕后执行 function
$("#sub-btn").click(function () {
//采用过关斩将法
//正则表达式验证用户名
var usernameValue = $("#username").val();
var usernamePattern = /^\w{6,10}$/;
if (!usernamePattern.test(usernameValue)) {
$("span[class='errorMsg']").text("用户名格式不对, 需要6-10个字符(大小写字母,数字,下划线)");
return false;
}
//验证密码
var passwordValue = $("#password").val();
var passwordPattern = /^\w{6,10}$/;
if (!passwordPattern.test(passwordValue)) {
$("span.errorMsg").text("密码格式不对, 需要6-10个字符(大小写字母,数字,下划线)");
return false;
}
//两次密码要相同
var rePwdValue = $("#repwd").val();
if (passwordValue != rePwdValue) {
$("span.errorMsg").text("两次密码不相同");
return false;
}
//这里仍然采用过关斩将法
//验证邮件
var emailVal = $("#email").val();
//在java中, 正则表达式的转义是\\; 在js中, 正则表达式转义是\
var emailPattern = /^[\w-]+@([a-zA-Z]+\.)+[a-zA-Z]+$/;
if (!emailPattern.test(emailVal)) {
$("span.errorMsg").text("电子邮件的格式不正确, 请重新输入");
return false;
}
//这里暂时不提交=>显示验证通过
$("span.errorMsg").text("验证通过");
return false;
});
})
</script>
分层 | 对应包 | 说明 |
---|---|---|
web层 | RegisterServlet.java | 接受浏览器发送数据; 调用相关的service;根据执行结果,返回页面数据 |
service层 | MemberService.java | Service接口包 |
MemberServiceImpl.java | Service接口实现类 | |
dao持久层 | MemberDAO.java | Dao接口包 |
MemberDAOImpl | Dao接口实现类 | |
实体bean对象 | Member.java | JavaBean类 |
工具类 | JdbcUtilsByDruid.java | 工具类 |
满汉楼项目
包括无参构造器和set方法. 如果添加有参构造器, 记得书写无参构造器
因为 一键生成实体类的工具是不会创建无参构造器的
get方法也要生成, 因为前端页面EL表达式是要调用get方法的
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; } }
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; } }
配置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" %>
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>
思路分析
- 输入用户名, 密码后提交
- 判断会员是否存在
- 会员存在于数据库, 显示登录成功页面
- 否则, 返回登陆页面, 重新登陆
- 要求改进登陆密码为md5加密
MemberService
public interface MemberService {
/**
* 根据传入的member信息, 返回对应在DB中的member对象
* @param member 是根据用户登录构建一个member
* @return 返回的是对应的DB中的member对象, 如果不存在返回null
*/
public Member login(Member member);
}
public class MemberServiceImpl implements MemberService {
//定义MemberDAO属性
private MemberDAO memberDAO = new MemberDAOImpl();
/**
* 判断用户名和密码是否存在
* @param username 用户名
* @param password 密码
* @return
*/
@Override
public Member login(Member member) {
//返回一个对象
return memberDAO.
queryMemberByUsernameAndPassword(member.getUsername(), member.getPassword());
}
}
测试(不要忘了测试)
web层 - LoginServlet
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String username = request.getParameter("username");
String password = request.getParameter("password");
Member member = new Member(null, username, password, null);
member = memberService.login(member);
if (member != null) {//登陆成功
System.out.println("登陆成功");
request.getRequestDispatcher("/views/member/login_ok.jsp")
.forward(request, response);
} else {
System.out.println("登陆失败, 返回登陆页面");
request.setAttribute("username", username);
request.setAttribute("msg", "登陆失败");
request.getRequestDispatcher("/views/member/login.jsp")
.forward(request, response);
}
}
login.jsp
<span class="errorMsg1"
style="float: right; font-weight: bold; color: lightgray; font-size: 20pt; margin-left: 10px;">${msg}span>
<form action="loginServlet" method="post">
<input type="text" name="username" value="${username}" placeholder="Username"/>
<input type="password" name="password" placeholder="Password"/>
<div class="button-box">
<div class="login-toggle-btn">
<input type="checkbox"/>
<a class="flote-none" href="javascript:void(0)">Remember mea>
<a href="#">Forgot Password?a>
div>
<button type="submit"><span>Loginspan>button>
div>
form>
添加login_ok.jsp, 参考
新建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.
需求分析
- 给后台管理提供独立登陆页面 manage_login.jsp(已提供)
- 管理员(admin表)登陆成功后, 显示管理菜单页面
- 管理员点击家居管理, 显示所有家居信息
程序框架图
public class AdminServlet extends BasicServlet {
private AdminService adminService = new AdminServiceImpl();
protected void login(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String username = request.getParameter("username");
String password = request.getParameter("password");
Admin admin = new Admin(null, username, password, null);
if (adminService.login(admin)) {
//管理员登录成功
request.getRequestDispatcher("/views/manage/manage_menu.jsp")
.forward(request, response);
} else {
System.out.println("登陆失败, 返回登陆页面");
request.setAttribute("username", username);
request.setAttribute("msg", "用户名或密码不正确");
request.getRequestDispatcher("/views/manage/manage_login.jsp")
.forward(request, response);
}
}
}
家居显示Servlet
<servlet>
<servlet-name>FurnServletservlet-name>
<servlet-class>com.zzw.furn.web.FurnServletservlet-class>
servlet>
<servlet-mapping>
<servlet-name>FurnServletservlet-name>
<url-pattern>/manage/furnServleturl-pattern>
servlet-mapping>
public class FurnServlet extends BasicServlet {
private FurnService furnService = new FurnServiceImpl();
protected void list(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
List<Furn> furns = furnService.queryFurn();
//将结果保存到request域
request.setAttribute("furns", furns);
//请求转发到管理家具页面
request.getRequestDispatcher("/views/manage/furn_manage.jsp")
.forward(request, response);
}
}
<span class="errorMsg"
style="float: right; font-weight: bold; color: lightgray; font-size: 20pt; margin-left: 10px;">${msg}span>
<%--管理员登陆--%>
<form action="adminServlet" method="post">
<input type="hidden" name="action" value="login"/>
<input type="text" name="username" value="${username}" placeholder="Username"/>
<input type="password" name="password" placeholder="Password"/>
<div class="button-box">
<div class="login-toggle-btn">
<input type="checkbox"/>
<a class="flote-none" href="javascript:void(0)">Remember mea>
<a href="#">Forgot Password?a>
div>
<button type="submit"><span>Loginspan>button>
div>
form>
manage_menu.jsp, 家居菜单页面
furn_manage.jsp, 家居显示页面
思路分析
- 请求添加家居, 请求FurnServlet的add方法, 将前端提交的数据封装到Furn对象
- 调用FurnService.add(Furn furn)方法
- 跳转到显示家居的页面
protected void add(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String name = req.getParameter("name");
String business = req.getParameter("business");
BigDecimal price = new BigDecimal(req.getParameter("price"));
int saleNum = DataUtils.parseInt(req.getParameter("saleNum"), 0);
int inventory = DataUtils.parseInt(req.getParameter("inventory"), 0);
Furn furn = new Furn(null, name,
business, price, saleNum, inventory, "...");
if (furnService.add(furn) > 0) {
System.out.println("添加成功, 请求转发到list");
//req.getRequestDispatcher("/manage/furnServlet?action=list")
// .forward(req, resp);
resp.sendRedirect(req.getContextPath() + "//manage/furnServlet?action=list");
} else {
System.out.println("添加失败, 返回到添加页面");
req.getRequestDispatcher("views/manage/furn_add.jsp")
.forward(req, resp);
}
}
解决中文乱码问题
4. 前端: furn_manage跳转到添加家居页面
<div class="header-bottom-set dropdown">
<a href="views/manage/furn_add.jsp">添加家居a>
div>
添加furn_add.jsp
<span class="errorMsg"
style="float: right; font-weight: bold; color: lightgray; font-size: 20pt; margin-left: 10px;">span>
请求转发, 当用户刷新页面时, 会重新发出第一次的请求, 造成数据重复提交
解决方案: 使用重定向
后端方案一
后端方案二
前端数据校验
$(":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;
}
})
引入: commons-logging-1.1.1.jar, commons-beanutils-1.8.0.jar
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
解决方案
- 将 把数据自动封装成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; } }
调用
需求分析
- 管理员进入到家居管理页面
- 点击删除家居链接, 弹出确认窗口, 确认-删除, 取消-放弃
程序框架图
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");
}
<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 + " 家居信息嘛?");
});
思路分析
- 管理员进入家居管理页面furn_manage.jsp
- 点击修改家居链接, 回显该家居信息 furn_update.jsp
- 填写新的信息, 点击修改家居按钮
- 修改成功后, 显示刷新后的家居列表
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);
}
}
<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在局部打开类图
思路
实现
java.lang.Long cannot be cast to java.lang.Integer
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
需求分析
- 管理员进入到家居管理后台页面
- 可以通过分页导航条来进行分页显示
- 完成上页, 下页, 显示共多少页
- 点击分页导航, 可以显示对应页的家居信息
- 在管理员进行修改, 删除, 添加后, 能够回显原来操作所在页面的数据
<!-- 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 -->
需求分析
- 顾客进入首页页面
- 分页显示家居
- 正确显示分页导航条, 即功能完善, 可以使用
实现
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);
}
<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 -->
需求分析
- 顾客进入首页页面
- 点击搜索按钮, 可以输入家居名
- 正确显示分页导航条, 并且要求在分页时, 保留上次搜索条件
需求分析
- 会员登陆成功
- 如果登陆成功后返回首页面, 显示订单管理和安全退出
- 如果用户没有登陆过, 首页就显示登录注册, 后台管理超链接
MemberServlet
if (memberService.login(member)) {//登陆成功
System.out.println("登陆成功");
//将member存入到session域
request.getSession().setAttribute("member", member);
if ("admin".equals(member.getUsername())) {
//管理员登录成功
request.getRequestDispatcher("/views/manage/manage_menu.jsp")
.forward(request, response);
} else {
//普通用户登陆成功
request.getRequestDispatcher("/views/member/login_ok.jsp")
.forward(request, response);
}
}
index.jsp
<c:if test="${sessionScope.member != null}">
<div class="header-bottom-set dropdown">
欢迎: ${sessionScope.member.username}
div>
<div class="header-bottom-set dropdown">
<a href="pages/manager/manager.html">订单管理a>
div>
<div class="header-bottom-set dropdown">
<a href="pages/manager/manager.html">安全退出a>
div>
c:if>
<c:if test="${sessionScope.member == null}">
<div class="header-bottom-set dropdown">
<a href="views/member/login.jsp">登录|注册a>
div>
<div class="header-bottom-set dropdown">
<a href="views/manage/manage_login.jsp">后台管理a>
div>
c:if>
思路分析
- 用户登录成功后
2. login_ok.jsp, 点击安全退出, 注销登录- 返回首页, 也可点击安全退出, 注销登录
程序框架图
实现
//安全退出
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
表单重复提交情况
- 提交完表单. 服务器使用请求转发进行页面跳转. 用户刷新(F5), 会发起最后一次的请求, 造成表单重复提交问题. 解决:用重定向.
- 用户正常提交, 由于网络延迟等原因, 未收到服务器的响应. 这时, 用户着急多点了几次提交操作, 也会造成表单重复提交. 解决: 验证码
- 用户正常提交, 服务器也没有延迟, 但是提交完成后, 用户回退浏览器. 重新提交, 也会造成表单重复提交. 解决: 验证码
- 恶意注册, 使用可以批量发送Http的工具, 比如 Postman, Jemeter等, 使用验证码防护
程序框架图
web层
配置KaptchaServlet
<servlet>
<servlet-name>KaptchaServletservlet-name>
<servlet-class>com.google.code.kaptcha.servlet.KaptchaServletservlet-class>
servlet>
<servlet-mapping>
<servlet-name>KaptchaServletservlet-name>
<url-pattern>/kaptchaServleturl-pattern>
servlet-mapping>
protected void register(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//todo 构建member
//获取用户提交的验证码
String captcha = request.getParameter("captcha");
//从session中获取 生成的验证码
HttpSession session = request.getSession();
String token = (String) session.getAttribute(KAPTCHA_SESSION_KEY);
//立即删除session中的验证码, 防止该验证码重复使用
session.removeAttribute(KAPTCHA_SESSION_KEY);
//如果token不为空, 并且和用户提交的验证码保持一致, 就继续
if (token != null && token.equalsIgnoreCase(captcha)) {
//todo 判断用户名是否可用 可用注册用户 不可用返回注册页面
} else {
request.setAttribute("msg", "验证码不正确");
request.setAttribute("username", username);//回显用户名
request.setAttribute("password", password);//回显密码
request.setAttribute("email", email);//回显邮件
request.setAttribute("active", "register_tab");
request.getRequestDispatcher("/views/member/login.jsp")
.forward(request, response);
}
}
KAPTCHA_SESSION_KEY是一个常量, 使用前需要导入
import static com.google.code.kaptcha.Constants.KAPTCHA_SESSION_KEY;
2. 前端页面 - login.jsp
<input type="text" id="captchaText" name="captcha" style="width: 50%"
placeholder="验证码"/> <img id="captcha" alt="" width="150px" src="kaptchaServlet">
验证码不能为空
var captchaText = $("#captchaText").val().trim();
if (captchaText == "" || captchaText == null) {
$("span.errorMsg").text("验证码不能为空!");
return false;
}
点击图片更换验证码
$("#captcha").click(function () {
$(this).attr("src", "kaptchaServlet?zzw=" + new Date());
});
需求分析
- 会员登陆后, 可以添加家居到购物车
- 完成购物车的设计和实现
- 每添加一个家居,购物车的数量+1, 并显示
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必须是局部变量, 否则会造成累加
- HashMap的数据实际上是存在HashMap$Node中的, Node是HashMap的内部类
- keySet里的key, 实际上只是引用, 指向了HashMap$Node
对象中的k, 真正的key值是保存在HashMap的Node内部类中的(HashMap$Node)
public class Cart {
public Integer getTotalCount() {
Integer totalCount = 0;
Collection<CartItem> cartItems = items.values();
for (CartItem cartItem : cartItems) {
totalCount += cartItem.getCount();
}
return totalCount;
}
需求分析
- 查看购物车, 可以显示如下信息
- 选中了哪些家居, 名称, 数量, 金额
- 统计购物车共多少商品, 总价多少
index.jsp代码
购物车
${sessionScope.cart.totalCount}
显示家居项
<tbody>
<%--找到显示购物车项, 进行循环的items--%>
<c:if test="${not empty sessionScope.cart.items}">
<%--
1.sessionScope.cart.items => 取出的是HashMap<Integer, CartItem>
2.所以通过foreach标签取出的每一个对象, 即entry是 HashMap<Integer, CartItem>的 k-v
3.var其实就是 entry
4.所以要取出cartItem对象, 是通过 entry.value取出
--%>
<c:forEach items="${sessionScope.cart.items}" var="entry">
<tr>
<td class="product-thumbnail">
<a href="#"><img class="img-responsive ml-3"
src="assets/images/product-image/1.jpg"
alt=""/>a>
td>
<%--隐藏域--%>
<td hidden="hidden" class="product-name"><a href="#">${entry.key}a>td>
<td class="product-name"><a href="#">${entry.value.name}a>td>
<td class="product-price-cart"><span class="amount">$${entry.value.price}span>
td>
<td class="product-quantity">
<div class="cart-plus-minus">
<input class="cart-plus-minus-box" type="text" name="qtyButton"
value="${entry.value.count}"/>
div>
td>
<td class="product-subtotal">$${entry.value.totalPrice}td>
<td class="product-remove">
<a href="cartServlet?action=del&key=${entry.key}"><i
class="icon-close">i>a>
td>
tr>
c:forEach>
c:if>
tbody>
计算总价
public class Cart {
/**
* 返回购物车的总价
* @return
*/
public BigDecimal getCartTotalPrice() {
BigDecimal cartTotalPrice = new BigDecimal(0);
//遍历我们的items
Set<Integer> keys = items.keySet();
for (Integer id : keys) {
CartItem item = items.get(id);
//提醒, 一定要包add后的值, 重新赋给 cartTotalPrice, 这样才是累加.
cartTotalPrice = cartTotalPrice.add(item.getTotalPrice());
}
return cartTotalPrice;
}
totalCount调用的是Cart的getTotalCount方法, totalPrice调用的是getTotalPrice方法
<h4>共${sessionScope.cart.totalCount}件商品 总价 ${sessionScope.cart.totalPrice}元h4>
<div class="cart-shiping-update">
<a href="#">购 物 车 结 账a>
div>
需求分析
- 进入购物车, 可以修改购买数量
- 更新该商品项的金额
- 更新购物车商品数量和总金额
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;
需求分析
- 进入购物车, 可以删除某商品
- 可以清空购物车
- 要求给出适当的确认信息
删除购物车
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>
需求分析
- 进入购物车, 点击购物车结账
- 生成订单和订单项, 并更新商品的销量和库存
- 如果会员没有登陆, 先进入登陆页面, 完成登陆后再结账
程序框架图
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方法
}
OrderDAO
OrderItemDAO
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);
}
}
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
需求分析
- 如果某家居库存为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>
需求分析
- 后台也加上校验. 只有在 库存>0时, 才能添加到购物车
思路分析
- 首页添加家居[Add To Cart]到购物车时, 加以限制
- 购物车里, 更新家居数量时,加以限制
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);
}
需求分析
- 完成订单管理-查看
- 具体流程参考显示家居
- 静态页面order.html 和 order_detail.html 已提供
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>
程序框架图
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>
需求分析
- 加入过滤器权限验证
- 如果没有登陆, 查看购物车和添加到购物车, 就会自动转到会员登陆页面
配置拦截url
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <filter> <filter-name>AuthFilterfilter-name> <filter-class>com.zzw.furns.filter.AuthFilterfilter-class> <init-param> <param-name>excludedUrlsparam-name> <param-value>/views/manage/manage_login.jsp,/views/member/login.jspparam-value> init-param> filter> <filter-mapping> <filter-name>AuthFilterfilter-name> <url-pattern>/views/cart/*url-pattern> <url-pattern>/views/manage/*url-pattern> <url-pattern>/views/member/*url-pattern> <url-pattern>/views/order/*url-pattern> <url-pattern>/cartServleturl-pattern> <url-pattern>/manage/furnServleturl-pattern> <url-pattern>/orderServleturl-pattern> filter-mapping> web-app>
过滤器逻辑判断
/** * 这是用于权限验证的过滤器, 对指定的url进行验证 * 如果登陆过, 就放行; 如果没有登陆, 就回到登陆页面 * @author 赵志伟 * @version 1.0 */ @SuppressWarnings({"all"}) public class AuthFilter implements Filter { private List<String> excludedUrls; @Override public void init(FilterConfig filterConfig) throws ServletException { //获取到配置的excludedUrls String strExcludedUrls = filterConfig.getInitParameter("excludedUrls"); String[] split = strExcludedUrls.split(","); //将 splitUrl 转成 list excludedUrls = Arrays.asList(split); System.out.println("excludedUrls= " + excludedUrls); } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { System.out.println("请求/cartServlet 被拦截..."); HttpServletRequest request = (HttpServletRequest) servletRequest; //得到请求的url String url = request.getServletPath(); System.out.println("url= " + url); //判断是否要验证 if (!excludedUrls.contains(url)) { //获取到登陆的member对象 Member member = (Member) request.getSession().getAttribute("member"); if (member == null) {//说明用户没有登录 //转发到登陆页面, 转发不走过滤器 servletRequest.getRequestDispatcher("/views/member/login.jsp") .forward(servletRequest, servletResponse); 重定向-拦截-重定向-拦截-重定向-拦截 //((HttpServletResponse) servletResponse) // .sendRedirect(request.getContextPath() + "/views/member/login.jsp"); return;//直接返回 } } //验证通过, 放行 filterChain.doFilter(servletRequest, servletResponse); System.out.println("请求/cartServlet验证通过, 放行"); } @Override public void destroy() { } }
manage_login.jsp
修改表单提交到memberServlet
<%--管理员登陆--%>
<form action="memberServlet" method="post">form>
MemberServlet
如果登陆的账户是管理员, 则返回到管理员登陆成功的页面
Member member = new Member(null, username, password, null);
member = memberService.login(member);
//if (memberService.login(member) != null) {//用户存在DB
if (member != null) {//用户存在DB
System.out.println("用户存在, 登陆成功...");
//将得到的member存入到session会话中
HttpSession session = request.getSession();
session.setAttribute("member", member);
if ("admin".equals(member.getUsername())) {
request.getRequestDispatcher("/views/manage/manage_menu.jsp")
.forward(request, response);
} else {
request.getRequestDispatcher("/views/member/login_ok.jsp")
.forward(request, response);
}
}
- 将FurnDAOImpl.java的updateFurn方法的sql故意写错
[furnDAO.updateFurn(furn);
由ctrl+alt+b定位到updateFurn的实现方法]- 在OrderServiceImpl的saveOrder()方法内捕获一下异常, 目的是保证程序能够继续执行
- 查看数据库里的数据会有什么结果. 会出现数据不一致的问题.
我在首页购买了一个小台灯, 数据库中生成了对应的订单和订单项, 但家居表里该小台灯的销量和库存没有变化, 纹丝不动. 相当于客户下单了, 但没有给人家发货.
思路分析
- 使用 Filter + ThreadLocal 来进行事务管理
- 说明: 在一次http请求中, servlet-service-dao 的调用过程, 始终是一个线程, 这是使用ThreadLocal的前提
- 使用ThreadLocal来确保所有dao操作都在同一个Connection内
- 修改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(); } }
- 修改BasicDao
删掉各个方法finally代码块里的close方法. 只有在事务结束后才实施关闭连接的操作. 一是提交事务后关闭连接; 二是增删改出错后, 回滚关闭连接.public List<T> queryMany(String sql, Class<T> clazz, Object... objects) { Connection connection = null; try { connection = JdbcUtilsByDruid.getConnection(); List<T> tList = queryRunner.query(connection, sql, new BeanListHandler<>(clazz), objects); return tList; } catch (SQLException e) { throw new RuntimeException(e);//编译异常->运行异常抛出 } } //查询单行, 返回的是一个对象 public T querySingle(String sql, Class<T> clazz, Object... objects) { Connection connection = null; try { connection = JdbcUtilsByDruid.getConnection(); T object = queryRunner.query(connection, sql, new BeanHandler<>(clazz), objects); return object; } catch (Exception e) { throw new RuntimeException(e); } } //查询某一字段 public Object queryScalar(String sql, Object... objects) { Connection connection = null; try { connection = JdbcUtilsByDruid.getConnection(); Object query = queryRunner.query(connection, sql, new ScalarHandler(), objects); return query; } catch (Exception e) { throw new RuntimeException(e); } } public int update(String sql, Object... objects) { Connection connection = null; try { //这里是从数据库连接池获取connection //注意:每次从连接池中取出connection, 不能保证是同一个 //1.我们目前已经是从和当前线程关联的ThreadLocal获取的connection //2.所以可以保证是同一个连接[在同一个线程中/在同一个请求中 => 因为一个请求对应一个线程] connection = JdbcUtilsByDruid.getConnection(); return queryRunner.update(connection, sql, objects); } catch (Exception e) { throw new RuntimeException(e); } }
- 控制层进行事务管理
前提OrderServiceImpl里报错的代码取消try-catch, 在OrderServlet控制层捕获//1.如果我们只是希望对orderService.saveOrder()方法进行事务控制 //2.那么我们可以不使用过滤器,直接在这个位置进行提交和回滚即可 //可以生成订单 String orderId = null;//订单, 订单明细已生成 try { orderId = orderService.saveOrder(cart, member.getId()); JdbcUtilsByDruid.commit();//提交 } catch (Exception e) { JdbcUtilsByDruid.rollback(); e.printStackTrace(); }
- 在web.xml中配置
<filter> <filter-name>TransactionFilterfilter-name> <filter-class>com.zzw.furns.filter.TransactionFilterfilter-class> filter> <filter-mapping> <filter-name>TransactionFilterfilter-name> <url-pattern>/*url-pattern> filter-mapping>
- 在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); }
- 在代码执行完毕后, 会运行到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(); } }
需求分析
- 如果在访问/操作网站时, 出现了内部错误, 统一显示 500.jsp
- 如果访问/操作的页面/servlet不存在时, 统一显示 404.jsp
思路分析
- 发生错误/异常时, 将错误/异常抛给tomcat
- 在web.xml中配置不同错误显示的页面即可
- 引入404.html, 500.html, 修改成jsp文件
将跳转链接改成index.jsp
您访问的页面不存在 返回首页
- web.xml配置
<error-page> <error-code>500error-code> <location>/views/error/500.jsplocation> error-page> <error-page> <error-code>404error-code> <location>/views/error/404.jsplocation> error-page>
- 修改事务过滤器, 将异常抛给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完成
- MemberServlet - 返回json格式的字符串 - 方式一
protected void isExistByName(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { //1.获取用户名 String username = req.getParameter("username"); //2.调用service boolean existsByUsername = memberService.isExistsByUsername(username); //3.思路 //(1)如果返回json格式[不要乱写, 要根据前端的需求来写] //(2)因为前后端都是我们自己写的, 格式我们自己定义 //(3){"isExist": true}; //(4)先用最简单的方法拼接 => 一会改进[扩展] String resultJson = "{\"isExist\": " + existsByUsername + "}"; //4.返回 resp.getWriter().print(resultJson); }
返回json格式的字符串 - 方式二
protected void isExistByName(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { //1.获取用户名 String username = req.getParameter("username"); //2.调用service boolean existsByUsername = memberService.isExistsByUsername(username); //3.思路 //(1)如果返回json格式[不要乱写, 要根据前端的需求来写] //(2)因为前后端都是我们自己写的, 格式我们自己定义 //(3){"isExist": true}; //(4)先用最简单的方法拼接 => 一会改进[扩展] //String resultJson = "{\"isExist\": " + existsByUsername + "}";字符串就不需要再转 //(5)将要返回的数据封装成map => json格式 Map<Object, Object> map = new HashMap<>(); map.put("isExist", existsByUsername); //map.put("email", "[email protected]"); //map.put("phone", "13031748275"); //4.返回json格式的数据 Gson gson = new Gson(); String resultJson = gson.toJson(map); resp.getWriter().print(resultJson); }
- 前端
$("#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 + " 可用"); } ) }
- MemberServlet
protected void verifyCaptcha(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { //获取用户提交的验证码 String captcha = req.getParameter("captcha"); //从session中获取 生成的验证码 HttpSession session = req.getSession(); String token = (String) session.getAttribute(KAPTCHA_SESSION_KEY); //立即删除session中的验证码, 防止该验证码被重复使用 session.removeAttribute(KAPTCHA_SESSION_KEY); //如果token不为空, 并且和用户提交的验证码保持一致, 就继续 if (token != null) { Map<Object, Object> map = new HashMap<>(); boolean verifyCaptcha = token.equalsIgnoreCase(captcha); map.put("verifyCaptcha", verifyCaptcha); //返回json格式的数据 Gson gson = new Gson(); String resultJson = gson.toJson(map); resp.getWriter().print(resultJson); } }
- 前端
$("#code").blur(function () {//光标焦点离开事件[点击后离开, 才可以触发] var captchaValue = this.value; $.getJSON( "memberServlet?action=verifyCaptcha&captcha="+captchaValue, function (data) { console.log("data= ", data); if (data.verifyCaptcha) { $("span.errorMsg2").text("验证码正确"); } else { $("span.errorMsg2").text("验证码错误"); } } ); })
在验证码标签旁补充一个span标签
<span class="errorMsg2" style="float: right; font-weight: bold; font-size: 15pt; margin-left: 10px; color: lightgray;">span>
//添加一个添加家居到购物车的方法 [Ajax]
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);
}
//给所有选定的button都赋上点击事件
$("button.add-to-cart").click(function () {
var id = $(this).attr("furnId");
//location.href = "cartServlet?action=addItem&id=" + id;
//这里我们使用jquery发出ajax请求, 得到数据进行局部刷新, 解决刷新这个页面效率低的问题
$.getJSON(
"cartServlet?action=addItemByAjax&id=" + id, function (data) {
console.log("data=", data);
//刷新局部
$("span.header-action-num").text(data.cartTotalCount);
}
)
});
使用ajax向后台发送请求跳转页面无效的原因
- 主要是服务器得到的是ajax发送过来的request, 也就是说这个请求不是浏览器请求的, 而是ajax请求的. 所以servlet根据request进行请求转发或重定向都不能影响浏览器的跳转
- 解决方案: 如果想要实现跳转, 可以返回url给ajax, 在浏览器执行window.location(url);
工具类添加方法 - 判断请求是不是一个ajax请求
/** * 判断请求是不是一个ajax请求 * @param request * @return */ public static boolean isAjaxRequest(HttpServletRequest request) { //X-Requested-With: XMLHttpRequest return "XMLHttpRequest".equals(request.getHeader("X-Requested-With")); }
修改AuthFilter.java
if (member == null) {//说明用户没有登录 if (!WebUtils.isAjaxRequest(request)) {//如果不是ajax请求 //转发到登陆页面, 转发不走过滤器 servletRequest.getRequestDispatcher("/views/member/login.jsp") .forward(servletRequest, servletResponse); } else {//如果是ajax请求 //返回ajax请求, 按照json格式返回 {"url": url} //String url = "views/member/login.jsp"; //String resultJson = "{\"url\": \"" + url + "\"}"; //1.构建map Map<Object, Object> map = new HashMap<>(); map.put("url", "views/member/login.jsp"); //2.转成json字符串 String resultJson = new Gson().toJson(map); //3.返回 servletResponse.getWriter().print(resultJson); } 重定向-拦截-重定向-拦截-重定向-拦截 //((HttpServletResponse) servletResponse) // .0sendRedirect(request.getContextPath() + "/views/member/login.jsp"); return;//直接返回 }
修改getJson
//这里我们使用jquery发出ajax请求, 得到数据进行局部刷新, 解决刷新这个页面效率低的问题 $.getJSON( "cartServlet?action=addItemByAjax&id=" + id, function (data) { console.log("data=", data); if (data.url == undefined) { //刷新局部 $("span.header-action-num").text(data.cartTotalCount); } else { location.href = data.url; } } )
引入文件上传下载的包: commons-io-1.4.jar, commons-fileupload-1.2.1.jar
FurnDAOImpl的查询语句加上图片字段 image_path as imagePath
需求分析
- 后台修改家居, 可以点击图片, 选择新的图片
- 这里会用到文件上传功能
- 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>
- 分析空指针异常
将form表单改成文件表单
点击修改家居
报错
将web.xml中500的错误提示配置注销掉, 将异常信息暴露出来
再次点击修改家居信息, 报错信息显示出来, BasicServlet空指针异常
所以有时候报错信息显示出来很重要
分析: 如果表单是enctype=“multipart/form-data”, 那么req.getParameter(“action”) 的方法得不到action值, 所以BasicServlet会报错
具体原因: req.getParameter(“action”)取不到form-data里的数据
- 解决空指针异常
解决方案: 将参数action, id, pageNo以url拼接的方式传参, BasicServlet便不会出错
注意: post请求可以人为主动在地址中拼接参数,拼接的参数可以直接像get那样接收
- 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>
需求分析
- 管理员admin登陆后, 可访问所有页面
- 会员登陆后, 不能访问后台管理相关页面, 其他页面可以访问
- 假定管理员名字就是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验证通过, 放行");
}
需求分析
- 家居图片都放在一个文件夹, 会越来越多
- 请尝试在assets/images/product-image/目录下, 自动创建年月日目录, 比如20230612. 以天为单位来存放上传图片
- 当上传新家居的图片, 原来的图片就没有用了, 应当删除原来的家居图片
工具类添加方法 - 返回当前日期
public static String getYearMonthDay() { //第三代日期类 LocalDateTime now = LocalDateTime.now(); int year = now.getYear(); int month = now.getMonthValue(); int day = now.getDayOfMonth(); String date = year + "/" + month + "/" + day + "/"; return date; }
需求分析
- 如果总页数<=5, 就全部显示
- 如果总页数>5, 按照如下规则显示(这个规则由程序员/业务来决定)
2.1 如果当前页是前3页, 就显示1-5
2.2 如果当前页是后3页, 就显示最后5页
2.3 如果当前页是中间页, 就显示 当前页前2页, 当前页, 当前页后2页
代码实现
<c:choose>
<%--如果总页数<=5, 就全部显示--%>
<c:when test="${requestScope.page.pageTotal <= 5}">
<c:set scope="page" var="begin" value="1">c:set>
<c:set scope="page" var="end" value="${requestScope.page.pageTotal}">c:set>
c:when>
<%--如果总页数>5, 按照如下规则显示(这个规则由程序员/业务来决定)--%>
<c:when test="${requestScope.page.pageTotal > 5}">
<c:choose>
<%--如果当前页是前3页, 就显示1-5--%>
<c:when test="${requestScope.page.pageNo <= 3}">
<c:set scope="page" var="begin" value="1">c:set>
<c:set scope="page" var="end" value="5">c:set>
c:when>
<%--如果当前页是后3页, 就显示最后5页--%>
<c:when test="${requestScope.page.pageNo > requestScope.page.pageTotal - 3}">
<c:set scope="page" var="begin" value="${requestScope.page.pageTotal - 4}">c:set>
<c:set scope="page" var="end" value="${requestScope.page.pageTotal}">c:set>
c:when>
<%--如果当前页是中间页, 就显示 当前页前2页, 当前页, 当前页后2页--%>
<c:otherwise>
<c:set scope="page" var="begin" value="${requestScope.page.pageNo - 2}">c:set>
<c:set scope="page" var="end" value="${requestScope.page.pageNo + 2}">c:set>
c:otherwise>
c:choose>
c:when>
c:choose>