[TOC]
[TOC]
# 购物车
## 1 购物车介绍
购物车模块存储顾客所选的的商品,记录下所选商品,当用户决定购买时,用户可以选择决定购买的商品进入结算页面。
购物车模块功能说明:
1、用户必须登录后才可以使用购物车
2、添加商品到购物车
3、查询购物车列表数据
4、删除购物车商品数据
5、更新选中商品状态
6、完成购物车商品的全选
7、清空购物车商品数据
数据存储:为了提高对购物车数据操作的性能,可以使用Redis【HASH】存储购物车数据。
页面效果:
## 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、购物车页面加减商品数量与商品详情页加入购物车是同一个接口
加入购物车功能如图所示:
**查看接口文档:**
添加购物车接口地址及返回结果
```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 需求说明
当用户在商品详情页面点击**购物车**按钮的时候,那么此时就需要将当前登录用户的所对应的所有的购物车数据在购物车页面展出出来。如下图所示:
**当商品价格变化时,页面可以显示实时价格:**
**查看接口文档:**
购物车列表接口地址及返回结果
```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 需求说明
删除功能如图所示:
**查看接口文档:**
删除购物车商品接口地址及返回结果
```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 需求说明
更新选中商品状态功能如图所示:
**查看接口文档:**
更新选中商品状态接口地址及返回结果
```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 需求说明
更新购物车商品全部选中状态功能如图所示:
**查看接口文档:**
更新购物车商品全部选中状态接口地址及返回结果
```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 需求说明
清空购物车功能如图所示:
**查看接口文档:**
清空购物车接口地址及返回结果
```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);
}
```