(3)渲染界面数据
<div class="w" id="result"> <div class="m succeed-box"> <div th:if="${cartItem!=null}" class="mc success-cont"> <div class="success-lcol"> <div class="success-top"><b class="succ-icon"></b> <h3 class="ftx-02">商品已成功加入购物车</h3></div> <div class="p-item"> <div class="p-img"> <a href="/javascript:;" target="_blank"><img style="height: 60px;width:60px;" th:src="${cartItem.image}"></a> </div> <div class="p-info"> <div class="p-name"> <a th:href="'http://item.saodaimall.com/'+${cartItem.skuId}+'.html'" th:text="${cartItem.title}">TCL 55A950C 55英寸32核人工智能 HDR曲面超薄4K电视金属机身(枪色)</a> </div> <div class="p-extra"><span class="txt" th:text="'数量:'+${cartItem.count}"> 数量:1</span></div> </div> <div class="clr"></div> </div> </div> <div class="success-btns success-btns-new"> <div class="success-ad"> <a href="/#none"></a> </div> <div class="clr"></div> <div class="bg_shop"> <a class="btn-tobback" th:href="'http://item.saodaimall.com/'+${cartItem.skuId}+'.html'">查看商品详情</a> <a class="btn-addtocart" href="http://cart.saodaimall.com/cart.html" id="GotoShoppingCart"><b></b>去购物车结算</a> </div> </div> </div> <div th:if="${cartItem==null}" class="mc success-cont"> <h2>购物车中无此商品</h2> <a href="http://saodaimall.com/">去购物</a> </div> </div> </div>
6、去结算(获取购物车的所有购物项)
(1)前台去购物车结算按钮点击后发送请求/cart.html给后端
<div class="bg_shop"> <a class="btn-tobback" th:href="'http://item.saodaimall.com/'+${cartItem.skuId}+'.html'">查看商品详情</a> <a class="btn-addtocart" href="http://cart.saodaimall.com/cart.html" id="GotoShoppingCart"><b></b>去购物车结算</a> </div>
(2)购物车服务的CartController来处理/cart.html请求
/** * 去购物车结算的请求 * * 浏览器有一个cookie:user-key 标识用户的身份,一个月过期 * 如果第一次使用jd的购物车功能,都会给一个临时的用户身份: * 浏览器以后保存,每次访问都会带上这个cookie; * * 登录:session有 * 没登录:按照cookie里面带来user-key来做 * 第一次,如果没有临时用户,自动创建一个临时用户 * * @return */ @GetMapping(value = "/cart.html") public String cartListPage(Model model) throws ExecutionException, InterruptedException { //快速得到用户信息:id,user-key // UserInfoTo userInfoTo = CartInterceptor.toThreadLocal.get(); CartVo cartVo = cartService.getCart(); model.addAttribute("cart",cartVo); return "cartList"; }
分流程:(封装CartVo对象,其实只封装了CartVo对象的购物项items)
判断用户有没有登录
已登录:(登录了还需要把没有登录时候的购物车中的商品也加进购物车来)
1>获取登录和未登录的Hash值(相当于拿到了这两种类型的购物车)
2>调用getCartItems方法来查询临时购物车所有的购物项
3>遍历获取到的临时购物车的所有购物项,调用addToCart方法来合并到用户登录了的购物车中
4>调用clearCartInfo方法来删除临时购物车的所有购物项
5>获取登录后的购物车数据【包含合并过来的临时购物车的所有购物项和登录后购物车的购物项】并封装到CartVo对象
未登录: 获取临时购物车里面的所有购物项封装到CartVo对象
/** * 获取用户登录或者未登录购物车里所有的数据 * @return * @throws ExecutionException * @throws InterruptedException */ @Override public CartVo getCart() throws ExecutionException, InterruptedException { CartVo cartVo = new CartVo(); UserInfoTo userInfoTo = CartInterceptor.toThreadLocal.get(); //这里没有直接用userInfoTo==null来判断用户是否登录是因为这样判断不出来到底登录了还是没有登录 //但是可以用userInfoTo.getUserId()!= null来判断出用户是否登录,userInfoTo.getUserId()只有登录了才会有值 if (userInfoTo.getUserId() != null) { //1、登录了还需要把没有登录时候的购物车中的商品也加进购物车来 String cartKey = CART_PREFIX + userInfoTo.getUserId(); //临时购物车的键 String temptCartKey = CART_PREFIX + userInfoTo.getUserKey(); //2、如果临时购物车的数据还未进行合并 List<CartItemVo> tempCartItems = getCartItems(temptCartKey); if (tempCartItems != null) { //临时购物车有数据需要进行合并操作 for (CartItemVo item : tempCartItems) { addToCart(item.getSkuId(),item.getCount()); } //清除临时购物车的数据 clearCartInfo(temptCartKey); } //3、获取登录后的购物车数据【包含合并过来的临时购物车的数据和登录后购物车的数据】 List<CartItemVo> cartItems = getCartItems(cartKey); cartVo.setItems(cartItems); } else { //没登录 String cartKey = CART_PREFIX + userInfoTo.getUserKey(); //获取临时购物车里面的所有购物项 List<CartItemVo> cartItems = getCartItems(cartKey); cartVo.setItems(cartItems); } return cartVo; } /** * 获取购物车里面的数据 * @param cartKey redis中的外围map的key值 * @return */ private List<CartItemVo> getCartItems(String cartKey) { //获取购物车里面的所有商品 BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(cartKey); //获取这个Hash值对应的Map的所有value值 List<Object> values = operations.values(); if (values != null && values.size() > 0) { List<CartItemVo> cartItemVoStream = values.stream().map((obj) -> { String str = (String) obj; CartItemVo cartItem = JSON.parseObject(str, CartItemVo.class); return cartItem; }).collect(Collectors.toList()); return cartItemVoStream; } return null; } /** * 添加商品到购物车 购物车和购物项存入redis的结构用的是HashMap结构 Hash值为cartKey ,表示购物车,其中的 map结构为: Map<String skuId,String cartItem>,表示购物项 cartKey表示格式为saodaimall:cart:key,表示购物车,其中saodaimall:cart:key的saodaimall:cart:是一个固定前缀,key值有两种,如果登录了就是用户id(saodaimall:cart:1),没登录就是名为user-key的cookie的值(例如saodaimall:cart:a191459a-0f24-4e69-8dc7-a3f81de96202) Map<String skuId,String cartItem>作为外表示用户购物车的里的购物项,购物项中skuId为商品id作为key,values作为购物项的详细信息 */ @Override public CartItemVo addToCart(Long skuId, Integer num) throws ExecutionException, InterruptedException { //拿到要操作的购物车信息 BoundHashOperations<String, Object, Object> cartOps = getCartOps(); //判断Redis是否有该商品的信息 String productRedisValue = (String) cartOps.get(skuId.toString()); //如果没有就添加数据 if (StringUtils.isEmpty(productRedisValue)) { //2、添加新的商品到购物车(redis) CartItemVo cartItemVo = new CartItemVo(); //开启第一个异步任务 CompletableFuture<Void> getSkuInfoFuture = CompletableFuture.runAsync(() -> { //1、远程查询当前要添加商品的信息 R productSkuInfo = productFeignService.getInfo(skuId); SkuInfoVo skuInfo = productSkuInfo.getData("skuInfo", new TypeReference<SkuInfoVo>() {}); //数据赋值操作 cartItemVo.setSkuId(skuInfo.getSkuId()); cartItemVo.setTitle(skuInfo.getSkuTitle()); cartItemVo.setImage(skuInfo.getSkuDefaultImg()); cartItemVo.setPrice(skuInfo.getPrice()); cartItemVo.setCount(num); }, executor); //开启第二个异步任务 CompletableFuture<Void> getSkuAttrValuesFuture = CompletableFuture.runAsync(() -> { //2、远程查询skuAttrValues组合信息 List<String> skuSaleAttrValues = productFeignService.getSkuSaleAttrValues(skuId); cartItemVo.setSkuAttrValues(skuSaleAttrValues); }, executor); //等待所有的异步任务全部完成 CompletableFuture.allOf(getSkuInfoFuture, getSkuAttrValuesFuture).get(); String cartItemJson = JSON.toJSONString(cartItemVo); cartOps.put(skuId.toString(), cartItemJson); return cartItemVo; } else { //购物车有此商品,修改数量即可 CartItemVo cartItemVo = JSON.parseObject(productRedisValue, CartItemVo.class); cartItemVo.setCount(cartItemVo.getCount() + num); //修改redis的数据 String cartItemJson = JSON.toJSONString(cartItemVo); cartOps.put(skuId.toString(),cartItemJson); return cartItemVo; } } /** * 登录后合并临时购物车后清空临时购物车的内容 * @param cartKey */ @Override public void clearCartInfo(String cartKey) { redisTemplate.delete(cartKey); }
package com.saodai.saodaimall.cart.vo; import org.springframework.util.CollectionUtils; import java.math.BigDecimal; import java.util.List; /** * 购物车VO模型 * 需要计算的属性需要重写get方法,保证每次获取属性都会进行计算(这里没用@Data是因为要重写get方法) **/ public class CartVo { /** * 购物车子项信息 */ List<CartItemVo> items; /** * 商品数量 */ private Integer countNum; /** * 商品类型数量 */ private Integer countType; /** * 商品总价 */ private BigDecimal totalAmount; /** * 减免价格 */ private BigDecimal reduce = new BigDecimal("0.00");; public List<CartItemVo> getItems() { return items; } public void setItems(List<CartItemVo> items) { this.items = items; } //获取购物车中所有商品数量总和 public Integer getCountNum() { int count = 0; if (items != null && items.size() > 0) { for (CartItemVo item : items) { count += item.getCount(); } } return count; } //获取购物车中所有商品类型总和 public Integer getCountType() { int count = 0; if (items != null && items.size() > 0) { for (CartItemVo item : items) { count += 1; } } return count; } //获取购物车中所有商品价格总和 public BigDecimal getTotalAmount() { BigDecimal amount = new BigDecimal("0"); // 计算购物项总价 if (!CollectionUtils.isEmpty(items)) { for (CartItemVo cartItem : items) { if (cartItem.getCheck()) { amount = amount.add(cartItem.getTotalPrice()); } } } // 计算优惠后的价格 return amount.subtract(getReduce()); } public BigDecimal getReduce() { return reduce; } public void setReduce(BigDecimal reduce) { this.reduce = reduce; } }
(3)渲染购物车界面
<div class="One_ShopCon"> <h1 th:if="${cart.items == null}"> 购物车还没有商品,<a href="http://saodaimall.com/">去购物</a> </h1> <ul th:if="${cart.items != null}"> <li th:each="item:${cart.items}"> <div> </div> <div> <ol> <li><input type="checkbox" th:attr="skuId=${item.skuId}" class="itemChecked" th:checked="${item.check}"></li> <li> <dt><img th:src="${item.image}" alt=""></dt> <dd style="width: 300px;"> <p> <span th:text="${item.title}">TCL 55A950C 55英寸32核</span> <br/> <span th:each="attr:${item.skuAttrValues}" th:text="${attr}">尺码: 55时 超薄曲面 人工智能</span> </p> </dd> </li> <li> <p class="dj" th:text="'¥' + ${#numbers.formatDecimal(item.price,3,2)}">¥4599.00</p> </li> <li> <p th:attr="skuId=${item.skuId}"> <span class="countOpsBtn">-</span> <span class="countOpsNum" th:text="${item.count}">5</span> <span class="countOpsBtn">+</span> </p> </li> <li style="font-weight:bold"><p class="zj">¥[[${#numbers.formatDecimal(item.totalPrice,3,2)}]]</p></li> <li> <p class="deleteItemBtn" th:attr="skuId=${item.skuId}">删除</p> </li> </ol> </div> </li> </ul > </div> <div class="One_ShopFootBuy fix1"> <div> <ul> <li><input type="checkbox" class="allCheck"><span>全选</span></li> <li>删除选中的商品</li> <li>移到我的关注</li> <li>清除下柜商品</li> </ul> </div> <div> <font style="color:#e64346;font-weight:bold;" class="sumNum"> </font> <ul> <li><img src="/static/cart/img/buyNumleft.png" alt=""></li> <li><img src="/static/cart/img/buyNumright.png" alt=""></li> </ul> </div> <div> <ol> <li>总价:<span style="color:#e64346;font-weight:bold;font-size:16px;" class="fnt">¥[[${#numbers.formatDecimal(cart.totalAmount, 3, 2)}]]</span> <li>优惠:¥[[${#numbers.formatDecimal(cart.reduce,1,2)}]]</li> </ol> </div> <div> <button onclick="toTrade()" type="button">去结算</button> </div> </div>
7、选中购物项
(1)选中购物项后发送请求/checkItem给后端
<li><input type="checkbox" th:attr="skuId=${item.skuId}" class="itemChecked" th:checked="${item.check}"></li> $(".itemChecked").click(function () { const skuId = $(this).attr("skuId"); const checked = $(this).prop("checked"); //如果框被选中了checked的值就是1,否则就是0 location.href = "http://cart.saodaimall.com/checkItem?skuId=" + skuId + "&checked=" + (checked ? 1 : 0); }); <li><input type="checkbox" th:attr="skuId=${item.skuId}" class="itemChecked" th:checked="${item.check}"></li> $(".itemChecked").click(function () { const skuId = $(this).attr("skuId"); const checked = $(this).prop("checked"); //如果框被选中了checked的值就是1,否则就是0 location.href = "http://cart.saodaimall.com/checkItem?skuId=" + skuId + "&checked=" + (checked ? 1 : 0); });
(2)购物车服务的CartController来处理/checkItem请求
/** * 商品是否选中 * @param skuId * @param checked * @return */ @GetMapping(value = "/checkItem") public String checkItem(@RequestParam(value = "skuId") Long skuId, @RequestParam(value = "checked") Integer checked) { cartService.checkItem(skuId,checked); return "redirect:http://cart.saodaimall.com/cart.html"; } /** * 商品是否选中 */ @Override public void checkItem(Long skuId, Integer check) { //查询购物车里面的商品 CartItemVo cartItem = getCartItem(skuId); //修改商品状态 cartItem.setCheck(check == 1?true:false); //序列化存入redis中 String redisValue = JSON.toJSONString(cartItem); BoundHashOperations<String, Object, Object> cartOps = getCartOps(); cartOps.put(skuId.toString(),redisValue); } /** * 根据skuId的值查询到redis中的购物项对象 * @param skuId * @return */ @Override public CartItemVo getCartItem(Long skuId) { //拿到要操作的购物车信息 BoundHashOperations<String, Object, Object> cartOps = getCartOps(); String redisValue = (String) cartOps.get(skuId.toString()); CartItemVo cartItemVo = JSON.parseObject(redisValue, CartItemVo.class); return cartItemVo; } /** * 获取到我们要操作的购物车 购物车和购物项存入redis的结构用的是HashMap结构 Hash值为cartKey ,表示购物车,其中的 map结构为: Map<String skuId,String cartItem>,表示购物项 cartKey表示格式为saodaimall:cart:key,表示购物车,其中saodaimall:cart:key的saodaimall:cart:是一个固定前缀,key值有两种,如果登录了就是用户id(saodaimall:cart:1),没登录就是名为user-key的cookie的值(例如saodaimall:cart:a191459a-0f24-4e69-8dc7-a3f81de96202) Map<String skuId,String cartItem>作为外表示用户购物车的里的购物项,购物项中skuId为商品id作为key,values作为购物项的详细信息 */ private BoundHashOperations<String, Object, Object> getCartOps() { //先从拦截器中得到当前用户信息 UserInfoTo userInfoTo = CartInterceptor.toThreadLocal.get(); //cartKey是存在redis的key值 String cartKey = ""; if (userInfoTo.getUserId() != null) { //用户登录了就用用户的id号 cartKey = CART_PREFIX + userInfoTo.getUserId(); } else { //没登录就用浏览器中名为user-key的cookie的值 cartKey = CART_PREFIX + userInfoTo.getUserKey(); } //绑定指定的key操作Redis(本质用的是 redisTemplate.opsForHash(),也就是哈希map,其中cartKey作为哈希值) BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(cartKey); return operations; }
其实就是修改了购物项的checked属性的值,如果选中了就改成true,否则就是false,注意不是所有控制器处理请求后都会返回一个对象,比如这里就没有返回对象,只是重定向了一个请求redirect:http://cart.saodaimall.com/cart.html,相当于又发了请求/cart.html给控制器
8、改变购物项的数量
<p th:attr="skuId=${item.skuId}"> <span class="countOpsBtn">-</span> <span class="countOpsNum" th:text="${item.count}">5</span> <span class="countOpsBtn">+</span> </p>
(1)改变购物项的数量后发送请求/countItem给后端
$(".countOpsBtn").click(function () { const skuId = $(this).parent().attr("skuId"); const num = $(this).parent().find(".countOpsNum").text(); location.href = "http://cart.saodaimall.com/countItem?skuId=" + skuId + "&num=" + num; });
(2)购物车服务的CartController来处理/countItem请求
/** * 改变商品数量 * @param skuId * @param num * @return */ @GetMapping(value = "/countItem") public String countItem(@RequestParam(value = "skuId") Long skuId, @RequestParam(value = "num") Integer num) { cartService.changeItemCount(skuId,num); return "redirect:http://cart.saodaimall.com/cart.html"; } /** * 修改购物项数量 * @param skuId * @param num */ @Override public void changeItemCount(Long skuId, Integer num) { //查询购物车里面的商品(从redis中拿到对应的商品项的数据) CartItemVo cartItem = getCartItem(skuId); cartItem.setCount(num); BoundHashOperations<String, Object, Object> cartOps = getCartOps(); //序列化存入redis中 String redisValue = JSON.toJSONString(cartItem); cartOps.put(skuId.toString(),redisValue); }
其实就是改变Redis缓存中的购物项的数量
9、删除商品项
<p class="deleteItemBtn" th:attr="skuId=${item.skuId}">删除</p>
(1)点击删除按钮后发送请求/deleteItem给后端
let deleteId = 0; $(".deleteItemBtn").click(function () { deleteId = $(this).attr("skuId"); }); //删除购物车选项 function deleteItem() { location.href = "http://cart.saodaimall.com/deleteItem?skuId=" + deleteId; }
(2)购物车服务的CartController来处理/deleteItem请求
/** * 删除商品信息 * @param skuId * @return */ @GetMapping(value = "/deleteItem") public String deleteItem(@RequestParam("skuId") Integer skuId) { cartService.deleteIdCartInfo(skuId); return "redirect:http://cart.saodaimall.com/cart.html"; } /** * 删除购物项(删除商品信息) * @param skuId */ @Override public void deleteIdCartInfo(Integer skuId) { BoundHashOperations<String, Object, Object> cartOps = getCartOps(); cartOps.delete(skuId.toString()); }