## 第11章-购物车 **学习目标:** - 能够说出购物车业务需求 - 搭建购物车模块 - 完成新增购物车功能 - 完成购物车合并功能 - 完成购物车商品选中删除购物车功能 # 1、购物车业务简介 ![img](assets/day11/wps1.png) 购物车模块要能过存储顾客所选的的商品,记录下所选商品,还要能随时更新,当用户决定购买时,用户可以选择决定购买的商品进入结算页面。 功能要求: 1) 利用**缓存**提高性能。 2) 未登录状态也可以存入购物车,一旦用户登录要进行合并操作。 # 2、购物车模块搭建 购物车添加展示流程: ![img](assets/day11/wps2.jpg) ## 2.1 搭建service-cart服务 选中`gmall-service`父工程,创建子模块:service-cart 。搭建方式如service-item ## 2.2 配置pom.xml ```xml gmall-service com.atguigu.gmall 1.0 4.0.0 service-cart com.atguigu.gmall service-product-client 1.0 service-cart org.springframework.boot spring-boot-maven-plugin ``` ## 2.3 启动类 ```java package com.atguigu.gmall; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.context.annotation.ComponentScan; @EnableFeignClients @EnableDiscoveryClient @SpringBootApplication(exclude = DataSourceAutoConfiguration.class) public class CartApp { public static void main(String[] args) { SpringApplication.run(CartApp.class, args); } } ``` ## 2.3 添加配置文件 在resources目录下创建:bootstrap.properties ```properties spring.application.name=service-cart spring.profiles.active=dev spring.cloud.nacos.discovery.server-addr=192.168.200.128:8848 spring.cloud.nacos.config.server-addr=192.168.200.128:8848 spring.cloud.nacos.config.prefix=${spring.application.name} spring.cloud.nacos.config.file-extension=yaml spring.cloud.nacos.config.shared-configs[0].data-id=common.yaml ``` # 3、功能—添加入购物车 ## 3.1 功能解析: 1、 商品详情页添加购物车 2、 添加购物车,用户可以不需要登录,如果用户没有登录,则生成临时用户id,购物车商品与临时用户id关联,当用户登录后,将临时用户id的购物车商品与登录用户id的商品合并 3、 商品详情添加购物车时,先判断用户是否登录,如果没登录,再判断是否存在临时用户,如果cookie中也没有临时用户,则生成临时用户 ## 3.2 处理临时用户 ### 3.2.1 商品详情页 商品详情添加购物车页面方法(/item/index.html): ```js addToCart() { // 判断是否登录和是否存在临时用户,如果都没有,添加临时用户 if(!auth.isTokenExist() && !auth.isUserTempIdExist()) { auth.setUserTempId() } window.location.href = 'http://cart.gmall.com/addCart.html?skuId=' + this.skuId + '&skuNum=' + this.skuNum } ``` ### 3.2.2 服务网关处理 思路:既然userId是从服务网关统一传递过来的,那么临时用户id我们也可以从网关传递过来,改造网关 网关中获取临时用户id 在`gmall-gateway` 项目中过滤器`AuthGlobalFilter`添加 ```java /** * 尝试获取临时用户ID * * @param request * @return */ private String getUserTempId(ServerHttpRequest request) { String userTempId = ""; //1.尝试从cookie中获取 List cookieList = request.getCookies().get("userTempId"); if (!CollectionUtils.isEmpty(cookieList)) { userTempId = cookieList.get(0).getValue(); return userTempId; } //2.尝试从请求头中获取 userTempId = request.getHeaders().getFirst("userTempId"); if(StringUtils.isNotBlank(userTempId)){ return userTempId; } return userTempId; } ``` 将userTempId 添加header请求头 ```java //5.将获取到用户ID设置到请求头中,将用户ID传输到目标微服务 if (StringUtils.isNotBlank(userId)) { request.mutate().header("userId", userId); } //将获取到临时用户ID设置到请求头 String userTempId = getUserTempId(request); if (StringUtils.isNotBlank(userTempId)) { request.mutate().header("userTempId", userTempId); } ``` AuthContextHolder类添加公共方法 ```java package com.atguigu.gmall.common.util; import javax.servlet.http.HttpServletRequest; /** * @author: atguigu * @create: 2023-01-11 10:31 */ public class AuthContextHolder { /** * 从请求对象中获取用户ID * * @return */ public static String getUserId(HttpServletRequest request) { String userId = request.getHeader("userId"); return org.apache.commons.lang.StringUtils.isNotBlank(userId) ? userId : ""; } /** * 从请求对象中获取临时用户ID * * @return */ public static String getUserTempId(HttpServletRequest request) { String userTempId = request.getHeader("userTempId"); return org.apache.commons.lang.StringUtils.isNotBlank(userTempId) ? userTempId : ""; } } ``` ## 3.3 功能开发: > YAPI接口地址:http://192.168.200.128:3000/project/11/interface/api/683 ### 3.3.1 创建实体 ```java package com.atguigu.gmall.model.cart; import com.atguigu.gmall.model.activity.CouponInfo; import com.atguigu.gmall.model.base.BaseEntity; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import java.math.BigDecimal; import java.sql.Timestamp; import java.util.List; @Data @ApiModel(description = "购物车") //@TableName("cart_info") public class CartInfo extends BaseEntity { private static final long serialVersionUID = 1L; @ApiModelProperty(value = "用户id") private String userId; @ApiModelProperty(value = "skuid") private Long skuId; @ApiModelProperty(value = "放入购物车时价格") private BigDecimal cartPrice; // 1999 @ApiModelProperty(value = "数量") private Integer skuNum; @ApiModelProperty(value = "图片文件") private String imgUrl; // 根据skuId ---> skuInfo 找名称, 减少关联查询,提供检索效率. @ApiModelProperty(value = "sku名称 (冗余)") private String skuName; // 选择状态 默认 1 = 选中 0 = 未选中 @ApiModelProperty(value = "isChecked") private Integer isChecked = 1; // 实时价格 skuInfo.price BigDecimal skuPrice; // 元旦 1888 | 提示 比加入时,降价了,还是涨价了 // 优惠券信息列表 @ApiModelProperty(value = "购物项对应的优惠券信息") @TableField(exist = false) private List couponInfoList; } ``` ### 3.3.2 添加购物车控制器 > YAPI接口文档:http://192.168.200.128:3000/project/11/interface/api/683 ```java package com.atguigu.gmall.controller; import com.atguigu.gmall.service.CartService; import com.atguigu.gmall.common.result.Result; import com.atguigu.gmall.common.util.AuthContextHolder; import org.apache.commons.lang.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; /** * @author: atguigu * @create: 2023-01-11 10:46 */ @RestController @RequestMapping("/api/cart") public class CarApiController { @Autowired private CartService cartService; /** * 用户将商品加入到购物车 * * @param skuId 商品SKUID * @param skuNum 商品加购的数量 * @return */ @GetMapping("/addToCart/{skuId}/{skuNum}") public Result addToCart(HttpServletRequest request, @PathVariable("skuId") Long skuId, @PathVariable("skuNum") Integer skuNum) { //声明用户ID遍历:可能是登录用户ID也可能是临时用户ID String userId = ""; userId = AuthContextHolder.getUserId(request); if (StringUtils.isBlank(userId)) { userId = AuthContextHolder.getUserTempId(request); } cartService.addToCart(userId, skuId, skuNum); return Result.ok(); } } ``` ### 3.3.3 业务接口 ```java package com.atguigu.gmall.service; public interface CartService { /** * 用户将商品加入到购物车 * * @param userId 用户ID * @param skuId 商品SKUID * @param skuNum 商品加购的数量 * @return */ void addToCart(String userId, Long skuId, Integer skuNum); } ``` ### 3.3.4 业务实现类 定义业务需要使用的常量,RedisConst类 ```java public static final String USER_KEY_PREFIX = "user:"; public static final String USER_CART_KEY_SUFFIX = ":cart"; public static final long USER_CART_EXPIRE = 60 * 60 * 24 * 30; ``` ```java package com.atguigu.gmall.service.impl; import com.atguigu.gmall.common.util.DateUtil; import com.atguigu.gmall.service.CartService; import com.atguigu.gmall.cart.model.CartInfo; import com.atguigu.gmall.common.constant.RedisConst; import com.atguigu.gmall.product.client.ProductFeignClient; import com.atguigu.gmall.product.model.SkuInfo; import org.apache.commons.lang.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.BoundHashOperations; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import java.util.*; /** * @author: atguigu * @create: 2023-01-11 10:47 */ @Service public class CartServiceImpl implements CartService { @Autowired private RedisTemplate redisTemplate; @Autowired private ProductFeignClient productFeignClient; /** * 用户将商品加入到购物车-存储到Redis * * @param userId * @param skuId 商品SKUID * @param skuNum 商品加购的数量 * @return */ @Override public void addToCart(String userId, Long skuId, Integer skuNum) { //1.构建购物车结构 redisKey 包含登录用户ID或者临时用户ID //1.1 登录用户ID作为 形式:user:1:cart String redisKey = getCartKey(userId); //1.2 创建hash操作对象 - 决定操作数据都是传入用户购物车数据 //BoundHashOperations BoundHashOperations hashOps = redisTemplate.boundHashOps(redisKey); //2.参数中skuID作为hashKey 标识商品 注意:hashKey类型必须是字符串 String hashKey = skuId.toString(); //3.构建hashVal 将商品信息+数量 //3.1 远程调用商品微服务获取商品信息 SkuInfo skuInfo = productFeignClient.getSkuInfo(skuId); if (skuInfo != null) { CartInfo cartInfo = null; if (hashOps.hasKey(skuId.toString())) { cartInfo = hashOps.get(skuId.toString()); cartInfo.setSkuNum(cartInfo.getSkuNum() + skuNum); } else { //3.2 封装商品购物车对象 cartInfo = new CartInfo(); cartInfo.setSkuId(skuId); cartInfo.setImgUrl(skuInfo.getSkuDefaultImg()); cartInfo.setSkuName(skuInfo.getSkuName()); cartInfo.setSkuNum(skuNum); cartInfo.setIsChecked(1); cartInfo.setCreateTime(new Date()); cartInfo.setUpdateTime(new Date()); cartInfo.setUserId(userId); cartInfo.setCartPrice(productFeignClient.getSkuPrice(skuId)); cartInfo.setSkuPrice(productFeignClient.getSkuPrice(skuId)); } //3.3 将数据存入Redis //redisTemplate.opsForHash().put(redisKey, hashKey, cartInfo); hashOps.put(hashKey, cartInfo); } } /** * 获取用户对应hash操作的Key * * @param userId 用户ID * @return */ private String getCartKey(String userId) { return RedisConst.USER_KEY_PREFIX + userId + RedisConst.USER_CART_KEY_SUFFIX; } } ``` # 4、功能—展示购物车列表 > YAPI接口地址:http://192.168.200.128:3000/project/11/interface/api/675 ## 4.1 功能解析 ## 4.2 控制器:CartApiController ```java /** * 查询用户购物车列表 * 版本1:分别查询未登录购物车列表,以及登录的购物车列表 * 版本2:将两个购物车中商品合并 * @param request * @return */ @GetMapping("/cartList") public Result> cartList(HttpServletRequest request){ //获取登录用户ID String userId = AuthContextHolder.getUserId(request); //获取临时用户ID String userTempId = AuthContextHolder.getUserTempId(request); List cartInfoList = cartService.cartList(userId, userTempId); return Result.ok(cartInfoList); } ``` ## 4.3 购物车列表接口:CartService ```java /** * 查询用户购物车列表 * 版本1:分别查询未登录购物车列表,以及登录的购物车列表 * 版本2:将两个购物车中商品合并 * @return */ List cartList(String userId, String userTempId); ``` ## 4.4 实现类:CartServiceImpl ```java /** * 查询用户购物车列表 * 版本1:分别查询未登录购物车列表,以及登录的购物车列表 * 版本2:将两个购物车中商品合并 * * @return */ @Override public List cartList(String userId, String userTempId) { //1.查询未登录购物车列表 List cartInfoList = null; //1.1 判断临时用户有值情况下 redis中获取购物车列表 if (StringUtils.isNotBlank(userTempId)) { //1.2 构建redisKey 创建hash操作对象 String noLogCartKey = getCartKey(userTempId); BoundHashOperations noLoginHashOps = redisTemplate.boundHashOps(noLogCartKey); cartInfoList = noLoginHashOps.values(); } //3.已登录的购物车列表 //3.1 判断用户有值情况下 redis中获取购物车列表 if (StringUtils.isNotBlank(userId)) { //3.2 构建redisKey 创建hash操作对象 String loginCartKey = getCartKey(userId); BoundHashOperations loginHashOps = redisTemplate.boundHashOps(loginCartKey); cartInfoList = loginHashOps.values(); } //4.对购物车商品数据进行排序 cartInfoList.sort((o1, o2) -> { return DateUtil.truncatedCompareTo(o2.getUpdateTime(), o1.getUpdateTime(), Calendar.SECOND); }); return cartInfoList; } ``` # 5、功能--合并购物车 功能分析: 1. 当用户登录以后,先判断未登录的时候,用户是否购买了商品。 - 如果用户购买了商品,则找到对应的商品Id,对数量进行合并 - 没有找到的商品,则直接添加到数据 2. 合并完成之后,删除未登录数据。 ## 5.1 更改实现类:CartServiceImpl ```java /** * 查询用户购物车列表 * 版本2:将两个购物车中商品合并 * 未登录(没有加购商品,加购商品) 登录(没有加购商品,加购商品) * 情况一:(临时用户)用户未登录:加入商品到购物车 用户没登录 返回临时用户加购商品列表 * 情况二:(临时用户)用户未登录:加入商品到购物车-->用户登录 需要进行合并,删除未登录购物车列表 * * @return */ @Override public List cartList(String userId, String userTempId) { //1.(临时用户)用户未登录:加入商品到购物车 用户没登录 返回临时用户加购商品列表 List noLoginCartList = null; if (StringUtils.isNotBlank(userTempId)) { //1.1 构建未登录用户redisKey String noLoginCartKey = getCartKey(userTempId); //1.2 获取未登录购物车列表 BoundHashOperations noLoginHashOps = redisTemplate.boundHashOps(noLoginCartKey); noLoginCartList = noLoginHashOps.values(); } if (StringUtils.isBlank(userId)) { noLoginCartList.sort((o1, o2) -> { return DateUtil.truncatedCompareTo(o2.getUpdateTime(), o1.getUpdateTime(), Calendar.SECOND); }); return noLoginCartList; } //2.如果未登录购物车列表中有数据,则需要跟已登录购物车列表进行合并--说明登录了 //2.1 构建已登录购物车KEY String loginCartKey = getCartKey(userId); //2.2 构建已登录购物车hash操作对象 BoundHashOperations loginHashOps = redisTemplate.boundHashOps(loginCartKey); if (!CollectionUtils.isEmpty(noLoginCartList)) { for (CartInfo cartInfo : noLoginCartList) { //2.3 判断如果登录购物车中包含商品SKUID 则累加数量 反之则新增 if (loginHashOps.hasKey(cartInfo.getSkuId().toString())) { CartInfo loginCartInfo = loginHashOps.get(cartInfo.getSkuId().toString()); //将离线购物车商品数量跟在线购物车商品数量累加 loginCartInfo.setSkuNum(loginCartInfo.getSkuNum() + cartInfo.getSkuNum()); //更新登录购物车数据 loginHashOps.put(cartInfo.getSkuId().toString(), loginCartInfo); } else { //2.4 如果未登录购物车中商品在已登录购物车中不存在新增 cartInfo.setUserId(userId); cartInfo.setUpdateTime(new Date()); loginHashOps.put(cartInfo.getSkuId().toString(), cartInfo); } } //3. 删除未登录购物车数据 String noLoginCartKey = getCartKey(userTempId); redisTemplate.delete(noLoginCartKey); } //4.再次查询登录后用户购物车列表 List allCartInfoList = loginHashOps.values(); //4.对购物车商品数据进行排序 allCartInfoList.sort((o1, o2) -> { return DateUtil.truncatedCompareTo(o2.getUpdateTime(), o1.getUpdateTime(), Calendar.SECOND); }); return allCartInfoList; } ``` # 6、选中状态的变更 用户每次勾选购物车的多选框,都要把当前状态保存起来。由于可能会涉及更频繁的操作,所以这个勾选状态不必存储到数据库中。保留在缓存状态即可。 > YAPI接口地址:http://192.168.200.128:3000/project/11/interface/api/667 ## 6.1 编写控制器 CartApiController ```java // 选中状态 @GetMapping("checkCart/{skuId}/{isChecked}") public Result checkCart(@PathVariable Long skuId, @PathVariable Integer isChecked, HttpServletRequest request){ String userId = AuthContextHolder.getUserId(request); // 判断 if (StringUtils.isEmpty(userId)){ userId = AuthContextHolder.getUserTempId(request); } // 调用服务层方法 cartService.checkCart(userId,isChecked,skuId); return Result.ok(); } ``` ## 6.2 编写业务接口与实现 业务接口CartService ```java /** * 更新选中状态 * * @param userId * @param isChecked * @param skuId */ void checkCart(String userId, Integer isChecked, Long skuId); ``` 业务实现类CartServiceImpl ```java /** * 变更购物车商品选中状态 * * @param skuId 商品SKUID * @param isChecked 状态 * @return */ @Override public void checkCart(String userId, Long skuId, Integer isChecked) { //获取用户购物车Key String cartKey = getCartKey(userId); //获取hash操作对象 BoundHashOperations hashOps = redisTemplate.boundHashOps(cartKey); if (hashOps.hasKey(skuId.toString())) { //获取商品信息 CartInfo cartInfo = hashOps.get(skuId.toString()); //修改状态 cartInfo.setIsChecked(isChecked); //更新购物车商品 hashOps.put(skuId.toString(), cartInfo); } } ``` # 7、删除购物车 > YAPI接口地址:http://192.168.200.128:3000/project/11/interface/api/659 ## 7.1 编写控制器 ```java /** * 删除 * * @param skuId * @param request * @return */ @DeleteMapping("deleteCart/{skuId}") public Result deleteCart(@PathVariable("skuId") Long skuId, HttpServletRequest request) { // 如何获取userId String userId = AuthContextHolder.getUserId(request); if (StringUtils.isEmpty(userId)) { // 获取临时用户Id userId = AuthContextHolder.getUserTempId(request); } cartService.deleteCart(skuId, userId); return Result.ok(); } ``` ## 7.1 封装业务接口与实现 业务接口CartService ```java /** * 删除购物车商品 * @param skuId * @param userId */ void deleteCart(Long skuId, String userId); ``` 业务实现类:CartServiceImpl ```java /** * 删除购物车商品 * @param userId * @param skuId */ @Override public void deleteCart(String userId, Long skuId) { //获取用户购物车Key String cartKey = getCartKey(userId); //获取hash操作对象 BoundHashOperations hashOps = redisTemplate.boundHashOps(cartKey); //删除购物车商品 hashOps.delete(skuId.toString()); } ``` # 8、前端实现 ## 8.1 在web-all添加前端实现 ### 8.1.1 网关动态路由 在Nacos配置列表找找到`server-gateway-dev.yaml`进行修改:配置购物车域名以及购物车服务的动态路由 ```yaml - id: web-cart uri: lb://web-all predicates: - Host=cart.gmall.com - id: service-cart uri: lb://service-cart predicates: - Path=/*/cart/** ``` ### 8.1.3 购物车Feign接口 1. 在`service-client`模块下创建购物车feign模块:service-cart-client ![image-20221207190600598](assets/day11/image-20221207190600598.png) 2. 在:`service-cart-client`创建获取购物车Feign远程调用API接口CartFeignClient ```java package com.atguigu.gmall.cart.client; import com.atguigu.gmall.cart.client.impl.CartDegradeFeignClient; import com.atguigu.gmall.cart.model.CartInfo; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import java.util.List; /** * author:atguigu * 描述: **/ @FeignClient(value = "service-cart",fallback = CartDegradeFeignClient.class) public interface CartFeignClient { // 根据用户Id 获取到选中购物车列表。 @GetMapping("api/cart/getCartCheckedList/{userId}") List getCartCheckedList(@PathVariable String userId); } ``` 3. 服务降级类 ```java package com.atguigu.gmall.cart.client.impl; import com.atguigu.gmall.cart.client.CartFeignClient; import com.atguigu.gmall.cart.model.CartInfo; import org.springframework.stereotype.Component; import java.util.List; /** * author:atguigu * date:2022/11/23 9:35 * 描述: **/ @Component public class CartDegradeFeignClient implements CartFeignClient { @Override public List getCartCheckedList(String userId) { return null; } } ``` ### 8.1.2 controller实现 1. 在`web-all`模块pom.xml中导入依赖 ```xml com.atguigu.gmall service-cart-client 1.0 ``` 2. 新增CartController ```java package com.atguigu.gmall.all.controller; import com.atguigu.gmall.cart.client.CartFeignClient; import com.atguigu.gmall.product.model.SkuInfo; import com.atguigu.gmall.product.client.ProductFeignClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import javax.servlet.http.HttpServletRequest; /** *

* 购物车页面 *

* */ @Controller public class CartController { @Autowired private CartFeignClient cartFeignClient; @Autowired private ProductFeignClient productFeignClient; /** * 查看购物车 * @return */ @RequestMapping("cart.html") public String index(){ return "cart/index"; } /** * 添加购物车 * @param skuId * @param skuNum * @param request * @return */ @RequestMapping("addCart.html") public String addCart(@RequestParam(name = "skuId") Long skuId, @RequestParam(name = "skuNum") Integer skuNum, HttpServletRequest request){ SkuInfo skuInfo = productFeignClient.getSkuInfo(skuId); request.setAttribute("skuInfo",skuInfo); request.setAttribute("skuNum",skuNum); return "cart/addCart"; } } ```