分析准备
分析
京东的购物车不同于淘宝,它在非登录的时候也可以操作购物车。
非登录状态下存储购物车信息购物车肯定是cookie。
登陆状态下存储购车车信息可能是数据库,但是操作购车是一个比较频繁的操作,因此也可能是在缓存中存储的。
封装
首先把购物车的每条信息看作为一个对象。
由上上图可以看出来,每项信息里面包含的属性有
商品是否是选中状态,商品图片,商品标题,商品属性,商品单价,商品数量等。另外还有一项隐藏的属性,就是商品的id。可以根据这些属性,封装成一个对象。为了简化步骤 ,这些属性和商品表属性接近,因此用商品类可以替代它。对于它们们之间不同的属性,可以用商品类意思接近的属性替代,比如它的商品数量,可以用商品库存表示,它的商品选中状态,可以用商品状态表示。
一个购物车是由多条购物信息组成,因此,可以把一个购物车看作是一个List数组。
数据库
首先是肯定需要商品表,涉及到登陆,因此有用户表,还有购物车表。
其他
框架选用的是SSM
用的是mybatis反向工程生成的,因此对于单表操作,不用修改xml文件
非登录状态下
封装
非登录下,操作cookie是非常频繁的。因此首先要有一个CookiesUtils之类的工具类。
/**
* 从cookie中获取所有购物车
*
* @return
*/
public List<TbItem> getAllCartByCookie(HttpServletRequest request, HttpServletResponse response) {
String cookieValue = CookieUtils.getCookieValue(request, Const.Cart.CART_COOKIES);
List<TbItem> jsonToList = null;
if (StringUtils.isNotBlank(cookieValue)) {
jsonToList = JsonUtils.jsonToList(cookieValue, TbItem.class);
} else {
jsonToList = new ArrayList<TbItem>();
}
return jsonToList;
}
/**
* 全局变量
*
* @author admin
*
*/
public class Const {
public interface Cart {
// 客户端的cookie的key
String CART_COOKIES = "CART_COOKIES";
// 商品限制
Integer CART_MAX_LIMIT = 200;
}
}
常用的商品变量
从Cookie中获取购物车在许多地方都能用到,因此把上面的这段代码抽取出来,用的时候调用该方法都行。
TbItem是商品类,在这里表示购物车每项信息的类
添加到购物车
public void addCart(Long itemId, Integer num, HttpServletRequest request, HttpServletResponse response)
throws Exception {
// TODO Auto-generated method stub
// 通过id获取商品
TbItem item = tbMapper.selectByPrimaryKey(itemId);
List<TbItem> allCartByCookie = this.getAllCartByCookie(request, response);
// 获取库存
Integer stock = item.getNum();
// 判断是否是新添加的商品
boolean flag = false;
for (TbItem t : allCartByCookie) {
if (t.getId().equals(itemId)) {
System.out.println(t);
// 超出库存最大数量
if (stock < Math.addExact(t.getNum(), num)) {
throw new RuntimeException("超出最大库存数量");
}
// 超出最大数量限制
if (Const.Cart.CART_MAX_LIMIT < t.getNum() + num) {
throw new RuntimeException("超出最大数量限制");
}
t.setNum(t.getNum() + num);
// 1表示商品选中
item.setStatus((byte) 1);
flag = true;
break;
}
}
// 新添加的商品
if (!flag) {
item.setNum(num);
// 1表示商品选中
item.setStatus((byte) 1);
allCartByCookie.add(item);
}
// 转化为json数据,并存储在cookie
CookieUtils.setCookie(request, response, Const.Cart.CART_COOKIES, JsonUtils.objectToJson(allCartByCookie));
}
添加到购物的业务层逻辑。首先分析它的所需要的参数,request,response,通过它获取cookie中的所有购物信息。通过ItemId就是商品id才能找到该商品。num表示添加数量。
每次添加商品的时候,大致步骤如下
- 从cookie中获取到购物车信息,然后封装到一个List
- 通过遍历购物车对象的数组,判断是否有是添加过的商品,是的话就在原有的基础上增加添加的数量,不是的话创建对象,添加到购物车数组对象中
- 把购物车对象转化成一个json并存储在cookie中
上面的步骤只是比较核心的步骤,省略一些杂项,比如判断库存等。如果超出库存或者最大数量都会抛出相应的异常信息,然后再视图层去接受在做出相应动作。
由于对象不能直接存储在cookie中,因此需要转化为json在存储
/**
* 添加购物车
*
* @param itemId
* @param num
* @param request
* @param response
* @return
*/
@RequestMapping("/add/{itemId}")
public String addCart(@PathVariable Long itemId, @RequestParam(defaultValue = "1") Integer num,
HttpServletRequest request, HttpServletResponse response) {
try {
cartService.addCart(itemId, num, request, response);
} catch (Exception e) {
// TODO Auto-generated catch block
// 添加购物车失败,可能是超出库存,或者超出最大数量限制等
e.printStackTrace();
return "cartFail";
}
return "cartSuccess";
}
上面是视图层
添加商品成功后的页面(不是京东添加成功后的页面,是自己找找到)
展示购物车
public List<TbItem> showCart(HttpServletRequest request, HttpServletResponse response) {
// TODO Auto-generated method stub
return this.getAllCartByCookie(request, response);
}
cookie中获得购物车对象
/**
* 展示所选购物车
*
* @param request
* @param response
* @param model
* @return
*/
@RequestMapping("/cart")
public String showCart(HttpServletRequest request, HttpServletResponse response, Model model) {
List<TbItem> items = cartService.showCart(request, response);
model.addAttribute("cartList", items);
return "cart";
}
把购物车对象放到key为“cartList”的Model属性中
对于jsp代码,就不展示,太长了
它的主要逻辑是
- 遍历cartList对象,并且取出它的每项属性
- 把每项选中状态的单价*数量相加添加到总价中
自己实现的购物车列表就是上面的那样
修改数量
public void update(Long itemId, Integer num, HttpServletRequest request, HttpServletResponse response) {
// TODO Auto-generated method stub
List<TbItem> allCartByCookie = this.getAllCartByCookie(request, response);
TbItem item = tbMapper.selectByPrimaryKey(itemId);
// 获取库存
Integer stock = item.getNum();
for (TbItem t : allCartByCookie) {
if (t.getId().equals(itemId)) {
// 超出库存最大数量
if (stock < Math.addExact(t.getNum(), num)) {
throw new RuntimeException("超出最大库存数量");
}
// 超出最大数量限制
if (Const.Cart.CART_MAX_LIMIT < t.getNum() + num) {
throw new RuntimeException("超出最大数量限制");
}
t.setNum(num);
// 1表示商品选中
item.setStatus((byte) 1);
break;
}
}
// 转化为json数据,并存储在cookie
CookieUtils.setCookie(request, response, Const.Cart.CART_COOKIES, JsonUtils.objectToJson(allCartByCookie));
}
核心步骤如下
- 从cookie中获取购物车的json数据,并转化成List对象
- 遍历购物车对象,通过商品id找到所对应的商品,修改数量
- 把购物车对象转化为json数据并写入到cookie中
/**
* 修改购物车数量
*
* @param itemId
* @param num
* @param request
* @param response
*/
@RequestMapping("/update/{itemId}/{num}")
@ResponseBody
public void update(@PathVariable Long itemId, @PathVariable Integer num, HttpServletRequest request,
HttpServletResponse response) {
cartService.update(itemId, num, request, response);
}
修改购物车数量是ajax请求,当然可以在视图层捕捉异常,如果又异常可以返回前端一个带有异常信息的状态码,和错误信息。
对于删除,选中状态等操作,都可以模仿修改数量,大致思路是一样的。
登录状态(数据库)
准备
登陆状态就要模拟登陆,退出等操作。因此写了个UserController类
@Controller
@RequestMapping("/user")
public class UserController {
@Autowired
UserService userService;
@Autowired
CartService cartService;
/**
* 模拟登陆
*
* @param request
* @return
*/
@RequestMapping("/login")
@ResponseBody
public ServerResponse login(HttpServletRequest request, HttpServletResponse response) {
//7l表示一个用户id7的用户
TbUser user = userService.getUserbyId(7L);
return ServerResponse.createBySuccess();
}
/**
* 模拟退出
*
* @param request
* @param response
* @return
*/
@RequestMapping("/logout")
@ResponseBody
public ServerResponse logout(HttpServletRequest request, HttpServletResponse response) {
HttpSession session = request.getSession();
if (session.getAttribute(Const.User.SESSION_USER) != null) {
session.removeAttribute(Const.Cart.CART_COOKIES);
return ServerResponse.createBySuccessMessage("退出成功");
}
return ServerResponse.createByErrorMessage("当前没有用户登陆");
}
}
访问/user/login就登陆
访问/user/logout就是退出
简单粗暴
还添加了全局变量
public class Const {
public interface User {
String SESSION_USER = "user";
}
public interface Cart {
Byte CHECKED = 1;// 即购物车选中状态
Byte UN_CHECKED = 0;// 购物车中未选中状态
String CART_COOKIES = "CART_COOKIES";
// 商品限制
Integer CART_MAX_LIMIT = 200;
}
}
ajax返回的的json数据的类,我找到了一个自己觉得很好的类,它关联了一个枚举
public enum ResponseCode {
SUCCESS(0, "SUCCESS"), ERROR(1, "ERROR"), NEED_LOGIN(10, "NEED_LOGIN"), ILLEGAL_ARGUMENT(2, "ILLEGAL_ARGUMENT");
private final int code;
private final String desc;
ResponseCode(int code, String desc) {
this.code = code;
this.desc = desc;
}
public int getCode() {
return code;
}
public String getDesc() {
return desc;
}
}
package com.cart.common;
import java.io.Serializable;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
/**
* Created by geely
*/
@JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL)
// 保证序列化json的时候,如果是null的对象,key也会消失
public class ServerResponse<T> implements Serializable {
private int status;
private String msg;
private T data;
private ServerResponse(int status) {
this.status = status;
}
private ServerResponse(int status, T data) {
this.status = status;
this.data = data;
}
private ServerResponse(int status, String msg, T data) {
this.status = status;
this.msg = msg;
this.data = data;
}
private ServerResponse(int status, String msg) {
this.status = status;
this.msg = msg;
}
@JsonIgnore
// 使之不在json序列化结果当中
public boolean isSuccess() {
return this.status == ResponseCode.SUCCESS.getCode();
}
public int getStatus() {
return status;
}
public T getData() {
return data;
}
public String getMsg() {
return msg;
}
public static ServerResponse createBySuccess() {
return new ServerResponse(ResponseCode.SUCCESS.getCode());
}
public static ServerResponse createBySuccessMessage(String msg) {
return new ServerResponse(ResponseCode.SUCCESS.getCode(), msg);
}
public static <T> ServerResponse<T> createBySuccess(T data) {
return new ServerResponse<T>(ResponseCode.SUCCESS.getCode(), data);
}
public static <T> ServerResponse<T> createBySuccess(String msg, T data) {
return new ServerResponse<T>(ResponseCode.SUCCESS.getCode(), msg, data);
}
public static ServerResponse createByError() {
return new ServerResponse(ResponseCode.ERROR.getCode(), ResponseCode.ERROR.getDesc());
}
public static ServerResponse createByErrorMessage(String errorMessage) {
return new ServerResponse(ResponseCode.ERROR.getCode(), errorMessage);
}
public static ServerResponse createByErrorCodeMessage(int errorCode, String errorMessage) {
return new ServerResponse(errorCode, errorMessage);
}
}
这个类,不同于其他类我认为就是增加了泛型,也就是增加了一些限制,增加泛型限制也意味着增加代码量每次写返回类型的时候都需要加入泛型。但是优点也是很显而易见的,正是那个限制使代码不容易出错。在一些稍微复杂的逻辑中就会体现出它的优势。
登陆同步
/**
* 模拟登陆
*/
@RequestMapping("/login")
@ResponseBody
public ServerResponse login(HttpServletRequest request, HttpServletResponse response) {
try {
TbUser user = userService.getUserbyId(7L);
request.getSession().setAttribute(Const.User.SESSION_USER, user);
// 同步cookie中的数据
// 首先把cookies转化为cart并储存到数据库
cartService.ItemConvertCart(request, response, user.getId());
// 然后 删除cookie
CookieUtils.deleteCookie(request, response, Const.Cart.CART_COOKIES);
return ServerResponse.createBySuccess();
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
return ServerResponse.createByError();
}
}
我的思路是如果用户登陆就会把cookie中的数据合并到数据中,然后删除cookie
看看如何合并数据之前,先看看购物车表结构
CREATE TABLE `tb_cart` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增ID',
`user_id` bigint(20) DEFAULT NULL COMMENT '用户ID',
`item_id` bigint(20) DEFAULT NULL COMMENT '商品ID',
`checked` tinyint(4) DEFAULT NULL,
`item_title` varchar(100) DEFAULT NULL COMMENT '商品标题',
`item_image` varchar(200) DEFAULT NULL COMMENT '商品主图',
`item_price` bigint(20) DEFAULT NULL COMMENT '商品价格,单位为:分',
`num` int(10) DEFAULT NULL COMMENT '购买数量',
`created` datetime DEFAULT NULL,
`updated` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `userId_itemId` (`user_id`,`item_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8 COMMENT='购物车模块';
从上看出,定位到一个用户购物车中的一项信息需要用户id和商品id
/**
* 把数据存储到数据库
*
* @param request
* @param response
* @param userId
*/
public void ItemConvertCart(HttpServletRequest request, HttpServletResponse response, Long userId) {
List<TbItem> allCartByCookie = this.getAllCartByCookie(request, response);
TbCart cart = new TbCart();
Date date = new Date();
for (TbItem t : allCartByCookie) {
TbCart c = this.getCartByItemIdAndUserId(t.getId(), userId);
if (c != null) {
// 如果用户购物车表中用数据,在原有的数量上加上cookie中的数量
c.setNum(t.getNum() + c.getNum());
c.setUpdated(new Date());
cartMapper.updateByPrimaryKey(c);
continue;
}
cart.setItemId(t.getId());
cart.setNum(t.getNum());
cart.setItemPrice(t.getPrice());
cart.setCreated(date);
cart.setUpdated(date);
cart.setItemImage(t.getImage());
cart.setItemTitle(t.getTitle());
cart.setUserId(userId);
cartMapper.insert(cart);
}
}
从cookie中获取购物车对象
遍历该对象,判断购物车的每个对象是否在数据库中已经存在
存在在原有的基础上加上cookie中的数量
不存在创建新的对象,并插入到购物车表中
添加到购物车
/**
* 添加购物车
*
* @param itemId
* @param num
* @param request
* @param response
* @return
*/
@RequestMapping("/add/{itemId}")
public String addCart(@PathVariable Long itemId, @RequestParam(defaultValue = "1") Integer num,
HttpServletRequest request, HttpServletResponse response) {
try {
Long userId = this.getStatus(request);
if (userId != null) {
// 登陆状态下操作数据库
cartService.addCart(itemId, userId, num);
} else {
// 非登录状态下操作cookie
cartService.addCart(itemId, num, request, response);
}
} catch (Exception e) {
// TODO Auto-generated catch block
// 可能会超出库存,或者其他原因
e.printStackTrace();
System.out.println(e.getMessage());
return "cartFail";
}
return "cartSuccess";
}
利用方法重载,添加就是直接操作数据库。和非登录的思路差不多,最大的区别就是一个操作的数据库,一个操作的是cookie。
修改状态,数量同理。