购书流程
本模块业务逻辑:
jsp文件分析
product_list.jsp
是点击分类默认显示的布局,而product_search_list.jsp
是搜索结果页面布局,是内容主要是以下红线框住部分,特点:分类条件显示(只有product_list.jsp
是,product_search_list.jsp
该处统一“全部商品”)、动态显示对应分类或者查询图书结果条数、动态显示书名售价及封面、分页显示的实现。
product_list.jsp
页面显示:
product_search_list.jsp
页面显示:
因为分类和搜索后都会返回一个PageBean到request储存着,所以分类条件显示、动态显示对应分类或者查询图书结果条数这两个功能用EL表达式直接就可以实现。
<h1>商品目录</h1>
<hr />
<h1>${bean.category}</h1> 共${bean.totalCount}种商品
<hr />
动态显示书名售价及封面,主要用了c标签
forEach语句遍历输出在Pagebean
中product
list
列表,关于PageBean
的数据结构,如何做到结果分页显示的,有待探讨。
分页显示
<c:forEach items="${bean.ps}" var="p" varStatus="vs">
<td>
<div class="divbookpic">
<p>
<a href="${pageContext.request.contextPath}/findProductById?id=${p.id}">
<img src="${pageContext.request.contextPath}${p.imgurl}" width="115" height="129" border="0" /> </a>
</p>
</div>
<div class="divlisttitle">
<a href="${pageContext.request.contextPath}/findProductById?id=${p.id}">书名: ${p.name}<br />售价:¥${p.price} </a>
</div>
</td>
</c:forEach>
分页显示的实现:
<div class="pagination">
<ul>
<!-- 上一页按钮 -->
<c:if test="${bean.currentPage!=1}">
<li class="disablepage_p">
<a class="disablepage_a" href="${pageContext.request.contextPath}/showProductByPage?currentPage=${bean.currentPage-1}&category=${bean.category}"></a>
</li>
</c:if>
<!-- 中间按钮 -->
<c:if test="${bean.currentPage==1}">
<li class="disablepage_p2"></li>
</c:if>
<c:forEach begin="1" end="${bean.totalPage}" var="pageNum">
<c:if test="${pageNum==bean.currentPage}">
<li class="currentpage">${pageNum }</li>
</c:if>
<c:if test="${pageNum!=bean.currentPage}">
<li><a href="${pageContext.request.contextPath}/showProductByPage?currentPage=${pageNum}&category=${bean.category}">${pageNum}</a></li>
</c:if>
/c:forEach>
<c:if test="${bean.currentPage==bean.totalPage||bean.totalPage==0}">
<li class="disablepage_n2"></li>
</c:if>
<!-- 下一页按钮 -->
<c:if test="${bean.currentPage!=bean.totalPage&&bean.totalPage!=0}">
<li class="disablepage_n">
<a class="disablepage_a" href="${pageContext.request.contextPath}/showProductByPage?currentPage=${bean.currentPage+1}&category=${bean.category}"></a></li>
</c:if>
</ul>
</div>
main.css
中,例:
.pagination li.disablepage_p {
width: 75px;
height: 15px;
padding: 5px;
color: #929292;
background:url(../images/previous_page.png) no-repeat center center;
}
在product_list.jsp
中“上一页”的按钮包含在样式资源main.css
中,该按钮首先判断是否为第1页,不是的话就执行访问上一页的操作,是的话就什么都不做(不指定href
)并且把样式改成灰色的按钮。然后进行循环遍历设定中间按钮,从第一页到页面总数最后一页,再判断是否为当前PageNum
(临时变量,相当于for循环中的i
)是否为PageBean
中储存的当前页currentPage
是否一致,是的话将页面样式改成下移变暗的当前页样式,并且不指定动作。否则指定其动作,将PageNum
作为参数指定当前页访问showProductByPageServlet
。再下来就是设定下一页按钮,最后一页或者该Pagebean
为空页时,变灰不可点选下一页按钮,如果当前页不是最后一页并且该Pagebean
不为空时,就指定正常的下一页逻辑。
看看ProductService
中具体的分页操作:
// 分页操作
public PageBean findProductByPage(int currentPage, int currentCount,
String category) {
PageBean bean = new PageBean();
// 封装每页显示数据条数
bean.setCurrentCount(currentCount);
// 封装当前页码
bean.setCurrentPage(currentPage);
// 封装当前查找类别
bean.setCategory(category);
try {
// 获取总条数
int totalCount = dao.findAllCount(category);
bean.setTotalCount(totalCount);
// 获取总页数
int totalPage = (int) Math.ceil(totalCount * 1.0 / currentCount);
bean.setTotalPage(totalPage);
// 获取当前页数据
List<Product> ps = dao.findByPage(currentPage, currentCount,
category);
bean.setPs(ps);
} catch (SQLException e) {
e.printStackTrace();
}
return bean;
}
Math.ceil()
为 “向上取整”, 即小数部分直接舍去,并向正数部分进1。对比Math.round()
(“四舍五入”, 该函数返回的是一个四舍五入后的的整数)以及Math.floor()
(“向下取整” ,即小数部分直接舍去)为什么用这个函数比较容易理解。可以看到,将分页显示逻辑实现用单独的实体表示来实现有点意外。不过PageBean
配合前端的html代码数据分页显示,做得十分精彩。
再看FindProductByIdServlet
中调用了ProductService
的findProductById()
方法直接获取该商品的详细信息,普通用户正常购物流程来说直接跳转info.jsp
方便下单,但是源码还有个管理员跳转至编辑商品的流程,不过在列表展示时,并没有设置type
的逻辑。先不管。
// 得到商品的id
String id = request.getParameter("id");
// 获取type参数值,此处的type用于区别普通用户和超级用户
String type = request.getParameter("type");
ProductService service = new ProductService();
try {
// 调用service层方法,通过id查找商品
Product p = service.findProductById(id);
request.setAttribute("p", p);
// 普通用户默认不传递type值,会跳转到info.jsp页面
if (type == null) {
request.getRequestDispatcher("/client/info.jsp").forward(request,response);
return;
}
request.getRequestDispatcher("/admin/products/edit.jsp").forward(request, response);
return;
} catch (FindProductByIdException e) {
e.printStackTrace();
}
在info.jsp
中,确认该商品时会调用AddCartServlet
<a href="${pageContext.request.contextPath}/addCart?id=${p.id}">
<img src="${pageContext.request.contextPath }/client/images/buybutton.gif" border="0" width="100" height="25" />
</a>
AddCartServlet
中,完成了创建、管理购物车对象cart
的逻辑,实质上它是一个Map键值对对象,用商品product
做key
用数量count
做value
,并且以"cart"
为索引存储在Session
中,一旦退出登录或者其存活时间到了,购物车cart
便会消失。把比较有趣的是,Map
集合的key
是唯一的,如果使用put()
方法存储,当key
重复时,put()
方法返回原来的value
值。源码巧妙利用了这个方法,实现了
区别商品是否已经添加过了,是的话除了直接put()
还要数量+1
,对于新商品来讲数量就没必要+1
了。
AddCartServlet
:
// 1.得到商品id
String id = request.getParameter("id");
// 2.调用service层方法,根据id查找商品
ProductService service = new ProductService();
try {
Product p = service.findProductById(id);
//3.将商品添加到购物车
//3.1获得session对象
HttpSession session = request.getSession();
//3.2从session中获取购物车对象
@SuppressWarnings("unchecked")
Map<Product, Integer> cart = (Map<Product, Integer>)session.getAttribute("cart");
//3.3如果购物车为null,说明没有商品存储在购物车中,创建出购物车
if (cart == null) {
cart = new HashMap<Product, Integer>();
}
//3.4向购物车中添加商品
Integer count = cart.put(p, 1);
//3.5如果商品数量不为空,则商品数量+1,否则添加新的商品信息
if (count != null) {
cart.put(p, count + 1);
}
session.setAttribute("cart", cart);
response.sendRedirect(request.getContextPath() + "/client/cart.jsp");
return;
} catch (FindProductByIdException e) {
e.printStackTrace();
}
然后就到cart.jsp
中,页面效果如图:
主要特点还是集中在红框部分,分成几部分:书本数量实时修改、继续购物回到全部分类显示、结账跳转账单页面。
自定义jsp标签:
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@taglib prefix="p" uri="http://www.itcast.cn/tag"%>
翻看jsp代码,文件头有两个标签引用,除了标准的c标签还自定义了一个p标签,指向的是PrivilegeTag
这个类,同时在WEB-INF
根目录下还要指定userPrivilegeTag.tld
相应的配置文件,不然映射不到http://www.itcast.cn/tag
内容。
userPrivilegeTag.tld
本质上是xml
文档:
<?xml version="1.0" encoding="UTF-8"?>
<taglib version="2.1" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-jsptaglibrary_2_1.xsd">
<!--指定版本信息-->
<tlib-version>1.0</tlib-version>
<!--引用的名字-->
<short-name>p</short-name>
<!--指定映射uri-->
<uri>http://www.itcast.cn/tag</uri>
<tag>
<!--指定属性名称-->
<name>user</name>
<!--指定实际路径-->
<tag-class>cn.itcast.itcaststore.tag.PrivilegeTag</tag-class>
<body-content>empty</body-content>
</tag>
</taglib>
不难看出自定义的p标签
,通过uri
和名字找到指定类和配置文件,可以获取一个叫user
属性,至于PrivilegeTag
里面做了什么?获取上下文的Request
和Response
,还有一个登陆后一直保存着的User
对象,如果不存在(未登录就访问购物车页面),就会跳转权限不足的错误页面,修改书本数量的操作就不能进行。对于自定义标签怎么用这里不做详细的探讨,我们明白有这么一个监测非法操作的用法。
public class PrivilegeTag extends SimpleTagSupport {
@Override
public void doTag() throws JspException, IOException {
PageContext context = (PageContext) this.getJspContext();
HttpServletRequest request = (HttpServletRequest) context.getRequest();
HttpServletResponse response = (HttpServletResponse) context.getResponse();
User user = (User) context.getSession().getAttribute("user");
if (user == null) {
response.sendRedirect(request.getContextPath() + "/client/error/privilege.jsp");
}
}
}
为什么要使用标签?
程序员定义的一种JSP标签,这种标签把那些信息显示逻辑封装在一个单独的Java类中,通过一个XML文件来描述它的使用。当页面中需要使用类似的显示逻辑时,就可以在页面中插入这个标签,从而完成相应的功能。使用自定义标签,可以分离程序逻辑和表示逻辑,将Java代码从HTML中剥离,便于美工维护页面;自定义标签也提供了可重用的功能组件,能够提高工程的开发效率。自定义标签主要用于移除Jsp页面中的java代码。JSP引擎将遇到自定义标签时,首先创建标签处理器类的实例对象,然后按照JSP规范定义的通信规则依次调用它的方法。
往下看cart.jsp
:
<script>
//当商品数量发生变化时触发该方法
function changeProductNum(count, totalCount, id) {
count = parseInt(count);
totalCount = parseInt(totalCount);
//如果数量为0,判断是否要删除商品
if (count == 0) {
var flag = window.confirm("确认删除商品吗?");
if (!flag) {
count = 1;
}
}
if (count > totalCount) {
alert("已达到商品最大购买量");
count = totalCount;
}
location.href = "${pageContext.request.contextPath}/changeCart?id="
+ id + "&count=" + count;
}
//删除购物车中的商品
function cart_del() {
var msg = "您确定要删除该商品吗?";
if (confirm(msg)==true){
return true;
}else{
return false;
}
}
</script>
脚本函数changeProductNum()
中location.href
是本页面跳转访问修改书本数量的ChangeCartServlet
(即修改一下都要实时通知) 。
cart.jsp
灵魂部分:
<!-- 循环输出商品信息 -->
<c:set var="total" value="0" />
<c:forEach items="${cart}" var="entry" varStatus="vs">
<table width="100%" border="0" cellspacing="0">
<tr>
<td width="10%">${vs.count}</td>
<td width="30%">${entry.key.name }</td>
<td width="10%">${entry.key.price }</td>
<td width="20%">
<!-- 减少商品数量 -->
<input type="button" value='-' style="width:20px"
onclick="changeProductNum('${entry.value-1}','${entry.key.pnum}','${entry.key.id}')">
<!-- 商品数量显示 -->
<input name="text" type="text" value="${entry.value}" style="width:40px;text-align:center" />
<!-- 增加商品数量 -->
<input type="button" value='+' style="width:20px" onclick="changeProductNum('${entry.value+1}','${entry.key.pnum}','${entry.key.id}')">
</td>
<td width="10%">${entry.key.pnum}</td>
<td width="10%">${entry.key.price*entry.value}</td>
<td width="10%">
<!-- 删除商品 -->
<a href="${pageContext.request.contextPath}/changeCart?id=${entry.key.id}&count=0"
style="color:#FF0000; font-weight:bold" onclick="javascript:return cart_del()">X</a>
</td>
</tr>
</table>
<c:set value="${total+entry.key.price*entry.value}" var="total" />
/c:forEach>
total
这个变量代表总价,遍历数据的时候累加出实际总价,
标签用于设置变量值和对象属性。varStatus="vs"
用于设置购物车物品前面的排列序号。在
语句中varStatus
封装了每一个子项的自动附加信息:count
(整数型)、index
(整数型)、first
(布尔型)、last
(布尔型)。每次点击±按钮都会调用changeProductNum()
,传入的分别是购买数量、库存数量、书本ID。该方法触发ChangeCartServlet
决定修改后显示的内容,之后又回到cart.jsp
显示:
// 1.得到商品id
String id = request.getParameter("id");
// 2.得到要修改的数量
int count = Integer.parseInt(request.getParameter("count"));
// 3.从session中获取购物车.
HttpSession session = request.getSession();
Map<Product, Integer> cart = (Map<Product, Integer>) session.getAttribute("cart");
Product p = new Product();
p.setId(id);
if (count != 0) {
cart.put(p, count);
} else {
cart.remove(p);
}
response.sendRedirect(request.getContextPath() + "/client/cart.jsp");
顺着流程图来到order.jsp
:
该jsp也是有自定义p标签进行安全检查,之后点击提交订单
会提交到跳转到CreateOrderServlet
中,封装订单信息提交到数据库:
// 1.得到当前用户
HttpSession session = request.getSession();
User user = (User) session.getAttribute("user");
// 2.从购物车中获取商品信息
Map<Product, Integer> cart = (Map<Product, Integer>)session.getAttribute("cart");
// 3.将数据封装到订单对象中
Order order = new Order();
try {
BeanUtils.populate(order, request.getParameterMap());
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
order.setId(IdUtils.getUUID());// 封装订单id
order.setUser(user);// 封装用户信息到订单.
for (Product p : cart.keySet()) {
OrderItem item = new OrderItem();
item.setOrder(order);
item.setBuynum(cart.get(p));
item.setP(p);
order.getOrderItems().add(item);
}
System.out.println(order);
// 4.调用service中添加订单操作.
OrderService service = new OrderService();
service.addOrder(order);
// request.getRequestDispatcher("/client/orderlist.jsp").forward(request, response);
response.sendRedirect(request.getContextPath() + "/client/createOrderSuccess.jsp");
循环封装订单子项的Product
对象时,用到了Map
的keySet()
方法,这个方法返回一个关于Key
的集合,因为本身具有迭代器,可以在for语句中自增反复赋值,取出所有的键。对于Dao的底层封装就不细细探讨了,去到createOrderSuccess.jsp
中:
<script type="text/javascript" src="js/my.js"> </script>
这里面有个倒计时的用法,时间一到或者直接点击回到首页的逻辑实现是由封装的my.js脚本文件完成:
var interval;
window.onload = function() {
interval = window.setInterval("changeSecond()", 1000);
};
function changeSecond() {
var second = document.getElementById("second"); //获取页面元素
var svalue = second.innerHTML;
svalue = svalue - 1;
if (svalue == 0) {
window.clearInterval(interval);
// 下列两行代码用于获取项目名,例如:bookstore
var pathName = window.location.pathname.substring(1);
var webName = pathName == '' ? '' : pathName.substring(0, pathName.indexOf('/'));
// 拼接访问路径名,例如:http://localhost:8080/bookstore/index.jsp
location.href = window.location.protocol + '//' + window.location.host + '/'+ webName + '/index.jsp';
return;
}
second.innerHTML = svalue;
}
innerHTML
属性设置或返回表格行的开始和结束标签之间的 HTML代码,这里作用是改变倒计时的数字。该方法1000毫秒执行一次。setInterval()
方法可按照指定的周期(以毫秒计)来调用函数或计算表达式。该方法会不停地调用函数,直到 clearInterval()
被调用或窗口被关闭。由 setInterval()
返回的 ID 值可用作 clearInterval()
方法的参数。当循环执行直到秒数为0
时,清除该ID指向的的循环操作。
总结一下,该部分算是该项目的核心部分,特别是分页现实的功能实现特别NB,购买书本直到支付的逻辑顺序也是很严格,可以想象一个完整的项目在前期的交互设计上面付出的心思也是必须面面俱到,严谨不能出错。刚刚接触纯jsp+servlet的Java web项目,对于项目结构能有一个具体的认识收获挺大的,很多东西没耐心细细按部就班地学,看源码找疑点分开学,确实能够进步很快。