[TOC] [TOC] # 购物车 ## 1 购物车介绍 购物车模块存储顾客所选的的商品,记录下所选商品,当用户决定购买时,用户可以选择决定购买的商品进入结算页面。 购物车模块功能说明: 1、用户必须登录后才可以使用购物车 2、添加商品到购物车 3、查询购物车列表数据 4、删除购物车商品数据 5、更新选中商品状态 6、完成购物车商品的全选 7、清空购物车商品数据 数据存储:为了提高对购物车数据操作的性能,可以使用Redis【HASH】存储购物车数据。 页面效果: 68663504555 ## 2 环境搭建 ### 2.1 新建模块 在spzx-modules模块下新建子模块`spzx-cart` ### 2.2 pom.xml ```xml com.spzx spzx-modules 3.6.3 4.0.0 spzx-cart spzx-cart购物车模块 17 17 UTF-8 com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery com.alibaba.cloud spring-cloud-starter-alibaba-nacos-config com.alibaba.cloud spring-cloud-starter-alibaba-sentinel org.springframework.boot spring-boot-starter-actuator com.mysql mysql-connector-j com.spzx spzx-common-datascope com.spzx spzx-common-log ${project.artifactId} org.springframework.boot spring-boot-maven-plugin repackage org.apache.maven.plugins maven-compiler-plugin 17 17 ``` ### 2.3 banner.txt 在resources目录下新建banner.txt ```text Spring Boot Version: ${spring-boot.version} Spring Application Name: ${spring.application.name} _ _ (_) | | _ __ _ _ ___ _ _ _ ______ ___ _ _ ___ | |_ ___ _ __ ___ | '__|| | | | / _ \ | | | || ||______|/ __|| | | |/ __|| __| / _ \| '_ ` _ \ | | | |_| || (_) || |_| || | \__ \| |_| |\__ \| |_ | __/| | | | | | |_| \__,_| \___/ \__, ||_| |___/ \__, ||___/ \__| \___||_| |_| |_| __/ | __/ | |___/ |___/ ``` ### 2.4 bootstrap.yml 在resources目录下新建bootstrap.yml ```yaml # Tomcat server: port: 9209 # Spring spring: application: # 应用名称 name: spzx-cart profiles: # 环境配置 active: dev main: allow-bean-definition-overriding: true #当遇到同样名字的时候,是否允许覆盖注册 cloud: nacos: discovery: # 服务注册地址 server-addr: 192.168.200.131:8848 config: # 配置中心地址 server-addr: 192.168.200.131:8848 # 配置文件格式 file-extension: yml # 共享配置 shared-configs: - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension} ``` ### 2.5 spzx-cart-dev.yml 在nacos上添加商品服务配置文件 ```yaml # spring配置 spring: data: redis: host: 192.168.200.131 port: 6379 password: ``` ### 2.6 logback.xml 在resources目录下新建logback.xml ```xml ${log.pattern} ${log.path}/info.log ${log.path}/info.%d{yyyy-MM-dd}.log 60 ${log.pattern} INFO ACCEPT DENY ${log.path}/error.log ${log.path}/error.%d{yyyy-MM-dd}.log 60 ${log.pattern} ERROR ACCEPT DENY ``` ### 2.7 SpzxCartApplication 添加启动类 ```java package com.spzx.cart; /** * 购物车模块 * */ @EnableCustomConfig @EnableRyFeignClients @SpringBootApplication(exclude = DataSourceAutoConfiguration.class)//取消数据源自动配置 public class SpzxCartApplication { public static void main(String[] args) { SpringApplication.run(SpzxCartApplication.class, args); System.out.println("(♥◠‿◠)ノ゙ 系统模块启动成功 ლ(´ڡ`ლ)゙ \n" + " .-------. ____ __ \n" + " | _ _ \\ \\ \\ / / \n" + " | ( ' ) | \\ _. / ' \n" + " |(_ o _) / _( )_ .' \n" + " | (_,_).' __ ___(_ o _)' \n" + " | |\\ \\ | || |(_,_)' \n" + " | | \\ `' /| `-' / \n" + " | | \\ / \\ / \n" + " ''-' `'-' `-..-' "); } } ``` ### 2.8 配置网关 在spzx-gateway-dev.yml配置文件中添加会员服务的网关信息 ```yaml # 购物车服务 - id: spzx-cart uri: lb://spzx-cart predicates: - Path=/cart/** filters: - StripPrefix=1 ``` ## 3 添加购物车 ### 3.1 需求说明 1、商品详情页加入购物车 2、加入购物车必须登录 3、购物车页面加减商品数量与商品详情页加入购物车是同一个接口 加入购物车功能如图所示: add **查看接口文档:** 添加购物车接口地址及返回结果 ```json get /cart/addToCart/{skuId}/{skuNum} 返回结果: { "msg": "操作成功", "code": 200 } ``` ### 3.2 获取商品sku信息 操作模块:spzx-product #### ProductSkuController ```java package com.spzx.product.controller; @Tag(name = "商品sku管理") @RestController @RequestMapping("/productSku") public class ProductSkuController extends BaseController { @Autowired private IProductSkuService productSkuService; @InnerAuth @Operation(summary = "获取商品SKU详情") @GetMapping("/getProductSku/{skuId}") public R getProductSku( @Parameter(description = "skuId") @PathVariable("skuId") Long skuId) { ProductSkuVo productSkuVo = new ProductSkuVo(); ProductSku productSku = productSkuService.getById(skuId); BeanUtils.copyProperties(productSku, productSkuVo); return R.ok(productSkuVo); } } ``` 3.4.2 ### 3.3 搭建商品服务远程接口模块 #### 3.3.1 新建模块 在spzx-api模块下新建子模块spzx-api-product #### 3.3.2 pom.xml ```xml com.spzx spzx-api 3.6.3 4.0.0 spzx-api-product spzx-api-product商品接口模块 17 17 UTF-8 com.spzx spzx-common-core ``` #### 3.3.3 spzx-modules模块引入依赖 ```xml com.spzx spzx-api-product 3.6.3 ``` #### 3.3.4 openFeign接口定义 ##### 1 移动实体类 **将spzx-product模块ProductSkuVo移动到spzx-api-product模块的包com.spzx.product.api.domain中** ##### 2 ServiceNameConstants 在spzx-common-core中添加常量 ``` /** * 商品服务的serviceid */ public static final String PRODUCT_SERVICE = "spzx-product"; ``` ##### 3 RemoteProductService ```java package com.spzx.product.api; @FeignClient(contextId = "remoteProductService", value = ServiceNameConstants.PRODUCT_SERVICE, fallbackFactory = RemoteProductFallbackFactory.class) public interface RemoteProductService { @GetMapping("/productSku/getProductSku/{skuId}") R getProductSku(@PathVariable("skuId") Long skuId, @RequestHeader(SecurityConstants.FROM_SOURCE) String source); } ``` ##### 4 RemoteProductFallbackFactory ```java package com.spzx.product.api.factory; @Component public class RemoteProductFallbackFactory implements FallbackFactory { private static final Logger log = LoggerFactory.getLogger(RemoteProductFallbackFactory.class); @Override public RemoteProductService create(Throwable throwable) { log.error("商品服务调用失败:{}", throwable.getMessage()); return new RemoteProductService() { @Override public R getProductSku(Long skuId, String source) { return R.fail("获取商品sku失败:" + throwable.getMessage()); } }; } } ``` ##### 5 加载配置类 resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports ```java com.spzx.product.api.factory.RemoteProductFallbackFactory ``` ### 3.4 搭建购物车服务远程接口模块 #### 3.4.1 新建模块 在spzx-api模块下新建子模块spzx-api-cart #### 3.4.2 pom.xml ```xml com.spzx spzx-api 3.6.3 4.0.0 spzx-api-cart spzx-api-cart购物车接口模块 17 17 UTF-8 com.spzx spzx-common-core ``` #### 3.4.3 spzx-modules模块引入依赖 ```xml com.spzx spzx-api-cart 3.6.3 ``` ### 3.5 接口调用 #### 3.5.1 CartInfo 操作模块:spzx-api-cart 定义一个实体类来封装购物车中的商品数据(购物项数据),该实体类的定义依据:购物车列表页面需要展示的数据。如下所示: ```java package com.spzx.cart.api.domain; @Data @Schema(description = "购物车") public class CartInfo extends BaseEntity { private static final long serialVersionUID = 1L; @Schema(description = "用户id") private Long userId; @Schema(description = "skuid") private Long skuId; @Schema(description = "放入购物车时价格") private BigDecimal cartPrice; @Schema(description = "实时价格") private BigDecimal skuPrice; @Schema(description = "数量") private Integer skuNum; @Schema(description = "图片文件") private String thumbImg; @Schema(description = "sku名称") private String skuName; @Schema(description = "isChecked") private Integer isChecked = 1; } ``` #### 3.5.2 CartController 操作模块:spzx-cart ```java package com.spzx.cart.controller; @Tag(name = "购物车接口") @RestController @RequestMapping public class CartController extends BaseController { @Autowired private ICartService cartService; @Operation(summary = "添加购物车") @GetMapping("/addToCart/{skuId}/{skuNum}") public AjaxResult addToCart( @Parameter(name = "skuId", description = "商品skuId", required = true) @PathVariable("skuId") Long skuId, @Parameter(name = "skuNum", description = "数量", required = true) @PathVariable("skuNum") Integer skuNum) { cartService.addToCart(skuId, skuNum); return success(); } } ``` #### 3.5.3 ICartService ```java package com.spzx.cart.service; public interface ICartService { void addToCart(Long skuId, Integer skuNum); } ``` #### 3.5.4 CartServiceImpl ```java package com.spzx.cart.service.impl; @Service @Slf4j public class CartServiceImpl implements ICartService { @Autowired private RedisTemplate redisTemplate; @Autowired private RemoteProductService remoteProductService; private String getCartKey(Long userId) { //定义key user:userId:cart return "user:cart:" + userId; } @Override public void addToCart(Long skuId, Integer skuNum) { // 获取当前登录用户的id Long userId = SecurityContextHolder.getUserId(); //1.构建“用户”购物车hash结构key user:cart:用户ID String cartKey = getCartKey(userId); //2.创建Hash结构绑定操作对象(方便对hash进行操作) BoundHashOperations hashOps = redisTemplate.boundHashOps(cartKey); //4.判断用户购物车中是否包含该商品 如果包含:数量进行累加(某件商品数量上限99) :新增购物车商品 String hashKey = skuId.toString(); Integer threshold = 99; if (hashOps.hasKey(hashKey)) { //4.1 说明该商品在购物车中已有,对数量进行累加 ,不能超过指定上限99 CartInfo cartInfo = hashOps.get(hashKey); int totalCount = cartInfo.getSkuNum() + skuNum; cartInfo.setSkuNum(totalCount > threshold ? threshold : totalCount); hashOps.put(hashKey, cartInfo); } else { //3.判断购物车商品种类(不同SKU)总数大于50件 Long count = hashOps.size(); if (++count > 50) { throw new RuntimeException("商品种类数量超过上限!"); } //4. 说明购物车没有该商品,构建购物车对象,存入Redis CartInfo cartInfo = new CartInfo(); cartInfo.setUserId(userId); cartInfo.setSkuNum(skuNum > threshold ? threshold : skuNum); //4.1 远程调用商品服务获取商品sku基本信息 R productSkuResult = remoteProductService.getProductSku(skuId, SecurityConstants.INNER); if (R.FAIL == productSkuResult.getCode()) { throw new ServiceException(productSkuResult.getMsg()); } ProductSkuVo productSku = productSkuResult.getData(); cartInfo.setSkuId(skuId); cartInfo.setSkuName(productSku.getSkuName()); cartInfo.setThumbImg(productSku.getThumbImg()); cartInfo.setCartPrice(productSku.getSalePrice()); cartInfo.setSkuPrice(productSku.getSalePrice()); cartInfo.setCreateTime(new Date()); //4.2 将购物车商品存入Redis hashOps.put(hashKey, cartInfo); } } } ``` ## 4 购物车列表查询 ### 4.1 需求说明 当用户在商品详情页面点击**购物车**按钮的时候,那么此时就需要将当前登录用户的所对应的所有的购物车数据在购物车页面展出出来。如下图所示: add **当商品价格变化时,页面可以显示实时价格:** 1709791602866 **查看接口文档:** 购物车列表接口地址及返回结果 ```json get /cart/cartList 返回结果: { "msg": "操作成功", "code": 200, "data": [ { "id": null, "createTime": "2024-02-26 16:25:54", "updateTime": "2024-02-26 16:25:54", "userId": 1, "skuId": 5, "cartPrice": 1999.00, "skuPrice": 1999.00, "skuNum": 1, "thumbImg": "http://139.198.127.41:9000/spzx/20230525/665832167-1_u_1.jpg", "skuName": "小米 红米Note10 5G手机 黑色 + 8G", "isChecked": 1 } ... ] } ``` ### 4.2 后端业务接口 #### 4.2.1 CartController 操作模块:`spzx-cart` ```java @Operation(summary = "查询购物车") @GetMapping("/cartList") public AjaxResult cartList() { return success(cartService.getCartList()); } ``` #### 4.2.2 ICartService ```java List getCartList(); ``` #### 4.2.3 CartServiceImpl ```java @Override public List getCartList() { // 获取当前登录用户的id Long userId = SecurityContextHolder.getUserId(); String cartKey = this.getCartKey(userId); // 获取数据 List cartInfoList = redisTemplate.opsForHash().values(cartKey); if (!CollectionUtils.isEmpty(cartInfoList)) { List infoList = cartInfoList.stream() //降序 .sorted((o1, o2) -> o2.getCreateTime().compareTo(o1.getCreateTime())) .collect(Collectors.toList()); return infoList; } return new ArrayList<>(); } ``` ## 5 删除购物车商品 ### 5.1 需求说明 删除功能如图所示: delete **查看接口文档:** 删除购物车商品接口地址及返回结果 ```json get /cart/deleteCart/{skuId} 返回结果: { "msg": "操作成功", "code": 200 } ``` ### 5.2 后端业务接口 #### 5.2.1 CartController ```java @Operation(summary = "删除购物车商品") @DeleteMapping("/deleteCart/{skuId}") public AjaxResult deleteCart( @Parameter(name = "skuId", description = "商品skuId", required = true) @PathVariable("skuId") Long skuId ) { cartService.deleteCart(skuId); return success(); } ``` #### 5.2.2 ICartService ```java void deleteCart(Long skuId); ``` #### 5.2.3 CartServiceImpl ```java @Override public void deleteCart(Long skuId) { // 获取当前登录用户的id Long userId = SecurityContextHolder.getUserId(); String cartKey = getCartKey(userId); //获取缓存对象 BoundHashOperations hashOperations = redisTemplate.boundHashOps(cartKey); hashOperations.delete(skuId.toString()); } ``` ## 6 更新选中商品状态 ### 6.1 需求说明 更新选中商品状态功能如图所示: check **查看接口文档:** 更新选中商品状态接口地址及返回结果 ```json get /cart/checkCart/{skuId}/{isChecked} 返回结果: { "msg": "操作成功", "code": 200 } ``` ### 6.2 后端业务接口 #### 6.2.1 CartController ```java @Operation(summary="更新选中状态") @GetMapping("/checkCart/{skuId}/{isChecked}") public AjaxResult checkCart( @Parameter(name = "skuId", description = "商品skuId", required = true) @PathVariable(value = "skuId") Long skuId, @Parameter(name = "isChecked", description = "是否选中 1:选中 0:取消选中", required = true) @PathVariable(value = "isChecked") Integer isChecked ) { cartService.checkCart(skuId, isChecked); return success(); } ``` #### 6.2.2 ICartService ```java void checkCart(Long skuId, Integer isChecked); ``` #### 6.2.3 CartServiceImpl ```java @Override public void checkCart(Long skuId, Integer isChecked) { // 获取当前登录用户的id Long userId = SecurityContextHolder.getUserId(); // 修改缓存 String cartKey = this.getCartKey(userId); BoundHashOperations hashOperations = redisTemplate.boundHashOps(cartKey); // 先获取用户选择的商品 if (hashOperations.hasKey(skuId.toString())) { CartInfo cartInfoUpd = hashOperations.get(skuId.toString()); // cartInfoUpd 写回缓存 cartInfoUpd.setIsChecked(isChecked); // 更新缓存 hashOperations.put(skuId.toString(), cartInfoUpd); } } ``` ## 7 完成购物车商品的全选 ### 7.1 需求说明 更新购物车商品全部选中状态功能如图所示: checkAll **查看接口文档:** 更新购物车商品全部选中状态接口地址及返回结果 ```json get /cart/allCheckCart/{isChecked} 返回结果: { "msg": "操作成功", "code": 200 } ``` ### 7.2 后端业务接口 #### 7.2.1 CartController ```java @Operation(summary="更新购物车商品全部选中状态") @GetMapping("/allCheckCart/{isChecked}") public AjaxResult allCheckCart( @Parameter(name = "isChecked", description = "是否选中 1:选中 0:取消选中", required = true) @PathVariable(value = "isChecked") Integer isChecked ){ cartService.allCheckCart(isChecked); return success(); } ``` #### 7.2.2 ICartService ```java void allCheckCart(Integer isChecked); ``` #### 7.2.3 CartServiceImpl ```java @Override public void allCheckCart(Integer isChecked) { // 获取当前登录用户的id Long userId = SecurityContextHolder.getUserId(); String cartKey = getCartKey(userId); BoundHashOperations hashOperations = redisTemplate.boundHashOps(cartKey); List cartInfoList = hashOperations.values(); cartInfoList.forEach(item -> { CartInfo cartInfoUpd = hashOperations.get(item.getSkuId().toString()); cartInfoUpd.setIsChecked(isChecked); // 更新缓存 hashOperations.put(item.getSkuId().toString(), cartInfoUpd); }); } ``` ## 8 清空购物车 ### 8.1 需求说明 清空购物车功能如图所示: clear **查看接口文档:** 清空购物车接口地址及返回结果 ```json get /cart/clearCart 返回结果: { "msg": "操作成功", "code": 200 } ``` ### 8.2 后端业务接口 #### 8.2.1 CartController ```java @Operation(summary="清空购物车") @GetMapping("/clearCart") public AjaxResult clearCart(){ cartService.clearCart(); return success(); } ``` #### 8.2.2 ICartService ```java void clearCart(); ``` #### 8.2.3 CartServiceImpl ```java @Override public void clearCart() { // 获取当前登录用户的id Long userId = SecurityContextHolder.getUserId(); String cartKey = getCartKey(userId); //获取缓存对象 redisTemplate.delete(cartKey); } ```