前言:

电商网站中添加商品到购物车功能模块实现:

根据前一篇博客的介绍,我们看到淘宝网站为了保证购物车数据的同步,直接是强制用户必须登录才可以将商品加入购物车。而京东网站是用户在未登录的状态下也可以将商品加入到购物车,此时这个是保存在了cookie中,然后用户登录后,根据商品的id判断商品是否存在,将两个购物车的商品合并,形成最终的购物车商品。

本篇文章分两个模块,分别看下这两个功能是如何实现的:

1、必须在用户登录的前提下,才可以将商品加入到购物车列表

我们今天先看下淘宝网站的状态下的添加商品到购物车的功能实现:

逻辑分析:

入参:productId,count(商品id,商品数量)

出参:



{
"status": 0,
"data": {
"cartProductVoList": [
{
"id": 1,
"userId": 13,
"productId": 1,
"quantity": 12,
"productName": "iphone7",
"productSubtitle": "双十一促销",
"productMainImage": "mainimage.jpg",
"productPrice": 7199.22,
"productStatus": 1,
"productTotalPrice": 86390.64,
"productStock": 86,
"productChecked": 1,
"limitQuantity": "LIMIT_NUM_SUCCESS"
},
{
"id": 2,
"userId": 13,
"productId": 2,
"quantity": 1,
"productName": "oppo R8",
"productSubtitle": "oppo促销进行中",
"productMainImage": "mainimage.jpg",
"productPrice": 2999.11,
"productStatus": 1,
"productTotalPrice": 2999.11,
"productStock": 86,
"productChecked": 1,
"limitQuantity": "LIMIT_NUM_SUCCESS"
}
],
"allChecked": true,
"cartTotalPrice": 89389.75
}
}
fail

{
"status": 10,
"msg": "用户未登录,请登录"
}


  根据接口文档,我们看到返回只是这样子的。

此时我们封装两个vo对象,一个是装载购物车商品数据的list,一个是大的购物车列表

这个很简单,直接根据接口文档来做即可。

第一个商品购物车数据vo对象:



package com.imooc.project.cartVo;

import java.math.BigDecimal;

/**
* 购物车商品的vo类,用于展示在前台信息
*/
public class CartProductVoList {
private Integer id;
private Integer userId;
private Integer productId;
private Integer quantity;
private String productName;
private String productSubtitle;
private String productMainImage;
private BigDecimal productPrice;
private Integer productStatus;
private BigDecimal productTotalPrice;
private Integer productStock;
private Integer productChecked;//是否被选中
private String limitQuantity;
//判断加入购物车的商品是否超过库存中商品的数量会返回这样的标识"limitQuantity"
//失败的:LIMIT_NUM_FAIL 成功的:LIMIT_NUM_SUCCESS

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public Integer getUserId() {
return userId;
}

public void setUserId(Integer userId) {
this.userId = userId;
}

public Integer getProductId() {
return productId;
}

public void setProductId(Integer productId) {
this.productId = productId;
}

public Integer getQuantity() {
return quantity;
}

public void setQuantity(Integer quantity) {
this.quantity = quantity;
}

public String getProductName() {
return productName;
}

public void setProductName(String productName) {
this.productName = productName;
}

public String getProductSubtitle() {
return productSubtitle;
}

public void setProductSubtitle(String productSubtitle) {
this.productSubtitle = productSubtitle;
}

public String getProductMainImage() {
return productMainImage;
}

public void setProductMainImage(String productMainImage) {
this.productMainImage = productMainImage;
}

public BigDecimal getProductPrice() {
return productPrice;
}

public void setProductPrice(BigDecimal productPrice) {
this.productPrice = productPrice;
}

public Integer getProductStatus() {
return productStatus;
}

public void setProductStatus(Integer productStatus) {
this.productStatus = productStatus;
}

public BigDecimal getProductTotalPrice() {
return productTotalPrice;
}

public void setProductTotalPrice(BigDecimal productTotalPrice) {
this.productTotalPrice = productTotalPrice;
}

public Integer getProductStock() {
return productStock;
}

public void setProductStock(Integer productStock) {
this.productStock = productStock;
}

public Integer getProductChecked() {
return productChecked;
}

public void setProductChecked(Integer productChecked) {
this.productChecked = productChecked;
}

public String getLimitQuantity() {
return limitQuantity;
}

public void setLimitQuantity(String limitQuantity) {
this.limitQuantity = limitQuantity;
}
}


  第二个:大的购物车列表



package com.imooc.project.cartVo;

import java.math.BigDecimal;
import java.util.List;

/**
* 一个大的vo对象,用于装载购物车功能模块的显示信息
*/
public class CartVo {

private List<CartProductVoList> cartProductVoLists;
private boolean allChecked;
private BigDecimal cartTotalPrice;

public List<CartProductVoList> getCartProductVoLists() {
return cartProductVoLists;
}

public void setCartProductVoLists(List<CartProductVoList> cartProductVoLists) {
this.cartProductVoLists = cartProductVoLists;
}

public boolean isAllChecked() {
return allChecked;
}

public void setAllChecked(boolean allChecked) {
this.allChecked = allChecked;
}

public BigDecimal getCartTotalPrice() {
return cartTotalPrice;
}

public void setCartTotalPrice(BigDecimal cartTotalPrice) {
this.cartTotalPrice = cartTotalPrice;
}
}


  做完了两个vo对象后,我们来看下下一步的操作:



1、首先判断用户是否存在,如果不存在则提示用户必须登录
2、如果用户存在,则进行下一步的操作
3、判断该用户的购物车中是否存在该商品,如果存在则更新购物车中该商品的数量
如果不存在,则将此商品加入到购物车列表中,写入数据库。
4、展示给用户的购物车列表是改用户下所有加入到购物车的商品列表:
根据userid查询该用户的购物车列表,遍历列表,将对象封装到我们写好的vo类中。
展示给前台用户。
5、注意问题:
(1)商品的价格计算问题,防止精度丢失。
(2)加入购物车是商品数量与该商品的总库存量问题。
(3)该商品的总价以及购物车中商品的总价计算问题
(4)购物车中商品是否选中的问题,默认我们认为如果全选则返回true,如果不是则返回false。


 下面是我们看下代码实现:依次是controller,service

电商网站中添加商品到购物车功能模块2017.12.8_ooc

 

 service:



public class CartServiceImpl implements ICartService {
@Autowired
private mmall_cartMapper cartMapper;
@Autowired
private mmall_productMapper productMapper;
//购物车功能流程:
//当用户未登录的状态下,加入购物车,此时商品是保存在cookie中的,用户换台电脑购物车就失效。当用户结算的时候需要用户的登录,这一块的处理也是计算价格库存
// 在用户登录的前提下,查询用户购物车中是否有该商品,如果没有,则将商品添加到购物车中(这中间牵扯到库存和商品价格的处理,该商品的总结,该用户购物车最终的总价),如果有该商品,则增加商品的数量,更新用户的购物车,计算价格
//这种情况是淘宝网站使用的,只有用户的登录的状态下商品才可以加入购物车,保证了数据的同步
@Override
public ServerResponse<CartVo> addProductCart(Integer userId,Integer productId,Integer count) {
if (productId == null || count == null) {
return ServerResponse.createByErrorCodeMessage(ResponseCode.ILLEGAL_ARGUMENT.getCode(), ResponseCode.ILLEGAL_ARGUMENT.getDesc());
}
//查询该用户的购物车中是否有该商品
mmall_cart cart = cartMapper.selectProductExit(userId, productId);
mmall_product product = productMapper.selectByPrimaryKey(productId);
if (cart == null) {
//如果购物车为空,则购物车没有此商品,需要插入到购物车
mmall_cart cartItem = new mmall_cart();
cartItem.setProductId(productId);
cartItem.setUserId(userId);
cartItem.setQuantity(count);
cartItem.setChecked(Const.CartProperty.CARTCHECKED);
int i = cartMapper.insertSelective(cartItem);
} else {
//如果购物车不为空,则已有此商品,需要更新购物车商品的数量
int stock = product.getStock();
cart.setQuantity(cart.getQuantity() + count);
cartMapper.updateByPrimaryKeySelective(cart);
}
return this.list(userId);
}


public ServerResponse<CartVo> list (Integer userId){
CartVo cartVo = this.getCartVoLimit(userId);
return ServerResponse.createBySuccess(cartVo);
}
private CartVo getCartVoLimit(Integer userId){
//封装vo展示给前台,查询用户购物车中的商品展示
CartVo cartVo=new CartVo();
BigDecimal cartTotalPrice=new BigDecimal("0");
List<mmall_cart> cartList=cartMapper.selectProductByUserId(userId);
List<CartProductVoList> list= Lists.newArrayList();
if (!CollectionUtils.isEmpty(cartList)) {
for (mmall_cart cartItem : cartList) {
//根据购物车的商品id来查询该商品的信息

//开始封装这个包装显示类
CartProductVoList cartProductVoList = new CartProductVoList();
cartProductVoList.setId(cartItem.getId());
cartProductVoList.setUserId(userId);
cartProductVoList.setProductId(cartItem.getProductId());
mmall_product productItem = productMapper.selectByPrimaryKey(cartItem.getProductId());
if (productItem!=null){
cartProductVoList.setProductMainImage(productItem.getMainImage());
cartProductVoList.setProductName(productItem.getName());
cartProductVoList.setProductStatus(productItem.getStatus());
cartProductVoList.setProductStock(productItem.getStock());
cartProductVoList.setProductSubtitle(productItem.getSubtitle());
cartProductVoList.setProductPrice(productItem.getPrice());
//商品库存限制这个功能
int buyLimitCount = 0;
if (cartItem.getQuantity()<= productItem.getStock()) {
buyLimitCount=cartItem.getQuantity();
cartProductVoList.setLimitQuantity(Const.CartProperty.LIMIT_NUM_SUCCESS);
} else {
//这一步需要注意,当库存不足时,需要更新购物车库存
buyLimitCount = productItem.getStock();
cartProductVoList.setLimitQuantity(Const.CartProperty.LIMIT_NUM_FAIL);
//购物车中更新有效库存,
mmall_cart cartForQuantity = new mmall_cart();
cartForQuantity.setId(cartItem.getId());
cartForQuantity.setQuantity(buyLimitCount);
cartMapper.updateByPrimaryKeySelective(cartForQuantity);
}
cartProductVoList.setQuantity(buyLimitCount);
//购物车总价格的问题:一个是该产品的总价,一个是购物车中最后的商品总价
//这个是该商品的总价格:商品价格*商品的数量
cartProductVoList.setProductTotalPrice(BigDecimalUtil.mul(productItem.getPrice().doubleValue(),cartItem.getQuantity().doubleValue()));
cartProductVoList.setProductChecked(cartItem.getChecked());
}
//这里的总价格默认为购物车商品全部选中的状态下计算的价格
if (cartItem.getChecked()==Const.CartProperty.CARTCHECKED){
cartTotalPrice=BigDecimalUtil.add(cartTotalPrice.doubleValue(),cartProductVoList.getProductTotalPrice().doubleValue());
}
list.add(cartProductVoList);
}
}
cartVo.setCartProductVoLists(list);
cartVo.setAllChecked(this.getAllCheckedStatus(userId));//如果全选则返回true,非全选则返回false
cartVo.setCartTotalPrice(cartTotalPrice);
return cartVo;
}
private boolean getAllCheckedStatus(Integer userId){
if(userId == null){
return false;
}
//查询购物车中该用户下选中的状态,checked=0,即未被选中状态,如果返回0,则表明购物车中全部选中的状态,返回true
return cartMapper.selectCartProductCheckedStatusByUserId(userId) == 0;

}


  上面就是这块的功能实现。

2、用户在未登录状态下将商品加入到购物车:

其实原理差不多的,只是我们在处理cookie的时候,使用的cookie工具类,然后将cookie中的json格式数据转为list,此时我们需要使用到阿里巴巴的fastjson这个工具包。

直接上代码吧:



//用户未登录的情况下购物车保存到cookie中,京东网站使用的方法
@Override
public ServerResponse addProductCookie(HttpServletRequest request, HttpServletResponse response, Integer productId, Integer count) {
/*添加购物车商品,首先购物车商品是保存在cookie中的,因为我们只要不付款是没有什么作用的。
* 如何从cookie中读取购物车列表呢,是利用request来实现的。
* 第一步:首先判断cookie中是否存在该商品,如果存在,则商品数量加1,
* 如果没有则根据商品id从rest工程中获取该商品,将商品写入cookie。
*/
CartItmCookieVo cartItmCookieVo=null;
//从cookie中读取商品
List<CartItmCookieVo> cookieList=this.getProductByCookie(request);
List<CartItmCookieVo> list=Lists.newArrayList();
//遍历这个列表,查询购物车中是否存在此商品,如果存在则更新,如果不存在则写入cookie中
for (CartItmCookieVo cartItem: cookieList) {
if (cartItem.getProductId()==productId){
cartItem.setQuantity(cartItem.getQuantity()+count);
cartItmCookieVo=cartItem;
break;
}else{
cartItmCookieVo=new CartItmCookieVo();
mmall_product product=productMapper.selectByPrimaryKey(productId);
cartItmCookieVo.setId(cartItem.getId());
cartItmCookieVo.setProductId(productId);
cartItmCookieVo.setProductName(product.getName());
if (product.getStock()>=cartItem.getQuantity()) {
cartItmCookieVo.setQuantity(cartItem.getQuantity());
}else{
cartItmCookieVo.setQuantity(product.getStock());
}
cartItmCookieVo.setProductPrice(product.getPrice());
cartItmCookieVo.setProductMainImage(product.getMainImage());
list.add(cartItmCookieVo);
CookieUtils.setCookie(request,response,"TT_CART",JSON.toJSONString(list),true);
}
}
return ServerResponse.createBySuccess(list);
}
//从cookie中读取商品列表
private List<CartItmCookieVo> getProductByCookie(HttpServletRequest request) {
String cookie=CookieUtils.getCookieValue(request,"TT_CART",true);
//因为cookie中存放的是json格式的数据,所以如果需要转换成list形式
if (cookie==null){
return new ArrayList<>();
}else{
//这里用到了使用阿里巴巴的fastjson将json转为list集合的形式
List<CartItmCookieVo> cartcookieList = JSON.parseArray(cookie, CartItmCookieVo.class);
return cartcookieList;
}
}


  cookieUtil的工具类:

这里一点就是在使用工具类的时候我们一般防止工具类实例化,此时的解决方案是使用私有构造器来解决:



package com.imooc.project.util;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;

/*
*Cookie 工具类
*/
public final class CookieUtils {

/**
* 得到Cookie的值, 不编码
*
* @param request
* @param cookieName
* @return
*/
public static String getCookieValue(HttpServletRequest request, String cookieName) {
return getCookieValue(request, cookieName, false);
}

/**
* 得到Cookie的值,
*
* @param request
* @param cookieName
* @return
*/
public static String getCookieValue(HttpServletRequest request, String cookieName, boolean isDecoder) {
Cookie[] cookieList = request.getCookies();
if (cookieList == null || cookieName == null) {
return null;
}
String retValue = null;
try {
for (int i = 0; i < cookieList.length; i++) {
if (cookieList[i].getName().equals(cookieName)) {
if (isDecoder) {
retValue = URLDecoder.decode(cookieList[i].getValue(), "UTF-8");
} else {
retValue = cookieList[i].getValue();
}
break;
}
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return retValue;
}

/**
* 得到Cookie的值,
*
* @param request
* @param cookieName
* @return
*/
public static String getCookieValue(HttpServletRequest request, String cookieName, String encodeString) {
Cookie[] cookieList = request.getCookies();
if (cookieList == null || cookieName == null) {
return null;
}
String retValue = null;
try {
for (int i = 0; i < cookieList.length; i++) {
if (cookieList[i].getName().equals(cookieName)) {
retValue = URLDecoder.decode(cookieList[i].getValue(), encodeString);
break;
}
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return retValue;
}

/**
* 设置Cookie的值 不设置生效时间默认浏览器关闭即失效,也不编码
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
String cookieValue) {
setCookie(request, response, cookieName, cookieValue, -1);
}

/**
* 设置Cookie的值 在指定时间内生效,但不编码
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
String cookieValue, int cookieMaxage) {
setCookie(request, response, cookieName, cookieValue, cookieMaxage, false);
}

/**
* 设置Cookie的值 不设置生效时间,但编码
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
String cookieValue, boolean isEncode) {
setCookie(request, response, cookieName, cookieValue, -1, isEncode);
}

/**
* 设置Cookie的值 在指定时间内生效, 编码参数
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
String cookieValue, int cookieMaxage, boolean isEncode) {
doSetCookie(request, response, cookieName, cookieValue, cookieMaxage, isEncode);
}

/**
* 设置Cookie的值 在指定时间内生效, 编码参数(指定编码)
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
String cookieValue, int cookieMaxage, String encodeString) {
doSetCookie(request, response, cookieName, cookieValue, cookieMaxage, encodeString);
}

/**
* 删除Cookie带cookie域名
*/
public static void deleteCookie(HttpServletRequest request, HttpServletResponse response,
String cookieName) {
doSetCookie(request, response, cookieName, "", -1, false);
}

/**
* 设置Cookie的值,并使其在指定时间内生效
*
* @param cookieMaxage cookie生效的最大秒数
*/
private static final void doSetCookie(HttpServletRequest request, HttpServletResponse response,
String cookieName, String cookieValue, int cookieMaxage, boolean isEncode) {
try {
if (cookieValue == null) {
cookieValue = "";
} else if (isEncode) {
cookieValue = URLEncoder.encode(cookieValue, "utf-8");
}
Cookie cookie = new Cookie(cookieName, cookieValue);
if (cookieMaxage > 0)
cookie.setMaxAge(cookieMaxage);
if (null != request) {// 设置域名的cookie
String domainName = getDomainName(request);
System.out.println(domainName);
if (!"localhost".equals(domainName)) {
//cookie.setDomain(domainName);
}
}
cookie.setPath("/");
response.addCookie(cookie);
} catch (Exception e) {
e.printStackTrace();
}
}

/**
* 设置Cookie的值,并使其在指定时间内生效
*
* @param cookieMaxage cookie生效的最大秒数
*/
private static final void doSetCookie(HttpServletRequest request, HttpServletResponse response,
String cookieName, String cookieValue, int cookieMaxage, String encodeString) {
try {
if (cookieValue == null) {
cookieValue = "";
} else {
cookieValue = URLEncoder.encode(cookieValue, encodeString);
}
Cookie cookie = new Cookie(cookieName, cookieValue);
if (cookieMaxage > 0)
cookie.setMaxAge(cookieMaxage);
if (null != request) {// 设置域名的cookie
String domainName = getDomainName(request);
System.out.println(domainName);
if (!"localhost".equals(domainName)) {
//本地测试的时候不要写.实际发布时在打开
//cookie.setDomain(domainName);
}
}
cookie.setPath("/");
response.addCookie(cookie);
} catch (Exception e) {
e.printStackTrace();
}
}

/**
* 得到cookie的域名
*/
private static final String getDomainName(HttpServletRequest request) {
String domainName = null;

String serverName = request.getRequestURL().toString();
if (serverName == null || serverName.equals("")) {
domainName = "";
} else {
final int end = serverName.lastIndexOf("/");
serverName = serverName.substring(0, end);
final String[] domains = serverName.split("\\.");
int len = domains.length;
if (len > 3) {
// www.xxx.com.cn
domainName = "." + domains[len - 3] + "." + domains[len - 2] + "." + domains[len - 1];
} else if (len <= 3 && len > 1) {
// xxx.com or xxx.cn
domainName = "." + domains[len - 2] + "." + domains[len - 1];
} else {
domainName = serverName;
}
}

if (domainName != null && domainName.indexOf(":") > 0) {
String[] ary = domainName.split("\\:");
domainName = ary[0];
}
return domainName;
}

}


  BigDecimal的工具类:



package com.imooc.project.util;

import java.math.BigDecimal;

/**
*商业计算中,如何防止精度的丢失,使用Bigdecimal来解决这个问题
* 由于Java的简单类型不能够精确的对浮点数进行运算,这个工具类提供精
* 确的浮点数运算,包括加减乘除和四舍五入。
*/
public class BigDecimalUtil {
//工具类(utility class),实例化对它们没有意义的工具类。这时候,就要做到不让该类被实例化.方法是使用私有构造器
private BigDecimalUtil(){}
//私有构造器这样子就表明这个工具类不能被实例化
// 重写BigDecimal的加减乘除
public static BigDecimal add(Double v1,Double v2){
BigDecimal b1=new BigDecimal(Double.toString(v1));
BigDecimal b2=new BigDecimal(Double.toString(v2));
return b1.add(b2);
}
public static BigDecimal sub(Double v1,Double v2){
BigDecimal b1=new BigDecimal(Double.toString(v1));
BigDecimal b2=new BigDecimal(Double.toString(v2));
return b1.subtract(b2);
}
public static BigDecimal mul(Double v1,Double v2){
BigDecimal b1=new BigDecimal(Double.toString(v1));
BigDecimal b2=new BigDecimal(Double.toString(v2));
return b1.multiply(b2);
}
public static BigDecimal div(Double v1,Double v2){
BigDecimal b1=new BigDecimal(Double.toString(v1));
BigDecimal b2=new BigDecimal(Double.toString(v2));
//<p>The new {@link #divide(BigDecimal, int, RoundingMode)}
return b1.divide(b2,2,BigDecimal.ROUND_FLOOR);
}
//除法的时候需要注意除不尽的情况,重写BigDecimal的除法保留两位小数的方法,四舍五入。

}


  以上就是两种功能的实现,至于合并的话,可以使用消息机制也可以使用判断商品id是否相同,然后合并该商品。

测试结果:

电商网站中添加商品到购物车功能模块2017.12.8_java_02

 

 ok,总结了下,也重温了下购物车的功能模块,一个好的功能模块的设计要涉及到太多的用户体验问题了,我们只能尽量完善吧。