電商網站中添加商品到購物車功能模塊2017.12.8
前言:
電商網站中添加商品到購物車功能模塊實現:
根據前一篇博客的介紹,我們看到淘寶網站為了保證購物車數據的同步,直接是強制用戶必須登錄才可以將商品加入購物車。而京東網站是用戶在未登錄的狀態下也可以將商品加入到購物車,此時這個是保存在了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
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是否相同,然後合並該商品。
測試結果:
ok,總結了下,也重溫了下購物車的功能模塊,一個好的功能模塊的設計要涉及到太多的用戶體驗問題了,我們只能盡量完善吧。
電商網站中添加商品到購物車功能模塊2017.12.8