[TOC]
[TOC]
购物车模块存储顾客所选的的商品,记录下所选商品,当用户决定购买时,用户可以选择决定购买的商品进入结算页面。
购物车模块功能说明:
1、用户必须登录后才可以使用购物车
2、添加商品到购物车
3、查询购物车列表数据
4、删除购物车商品数据
5、更新选中商品状态
6、完成购物车商品的全选
7、清空购物车商品数据
数据存储:为了提高对购物车数据操作的性能,可以使用Redis【HASH】存储购物车数据。
页面效果:
在spzx-modules模块下新建子模块spzx-cart
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.spzx</groupId>
<artifactId>spzx-modules</artifactId>
<version>3.6.3</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spzx-cart</artifactId>
<description>
spzx-cart购物车模块
</description>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- SpringCloud Alibaba Nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- SpringCloud Alibaba Nacos Config -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- SpringCloud Alibaba Sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!-- SpringBoot Actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Mysql Connector -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<!-- spzx Common DataScope -->
<dependency>
<groupId>com.spzx</groupId>
<artifactId>spzx-common-datascope</artifactId>
</dependency>
<!-- spzx Common Log -->
<dependency>
<groupId>com.spzx</groupId>
<artifactId>spzx-common-log</artifactId>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
在resources目录下新建banner.txt
Spring Boot Version: ${spring-boot.version}
Spring Application Name: ${spring.application.name}
_ _
(_) | |
_ __ _ _ ___ _ _ _ ______ ___ _ _ ___ | |_ ___ _ __ ___
| '__|| | | | / _ \ | | | || ||______|/ __|| | | |/ __|| __| / _ \| '_ ` _ \
| | | |_| || (_) || |_| || | \__ \| |_| |\__ \| |_ | __/| | | | | |
|_| \__,_| \___/ \__, ||_| |___/ \__, ||___/ \__| \___||_| |_| |_|
__/ | __/ |
|___/ |___/
在resources目录下新建bootstrap.yml
# 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}
在nacos上添加商品服务配置文件
# spring配置
spring:
data:
redis:
host: 192.168.200.131
port: 6379
password:
在resources目录下新建logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<!-- 日志存放路径 -->
<property name="log.path" value="logs/spzx-cart" />
<!-- 日志输出格式 -->
<property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />
<!-- 控制台输出 -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
</appender>
<!-- 系统日志输出 -->
<appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/info.log</file>
<!-- 循环政策:基于时间创建日志文件 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志文件名格式 -->
<fileNamePattern>${log.path}/info.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 日志最大的历史 60天 -->
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!-- 过滤的级别 -->
<level>INFO</level>
<!-- 匹配时的操作:接收(记录) -->
<onMatch>ACCEPT</onMatch>
<!-- 不匹配时的操作:拒绝(不记录) -->
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/error.log</file>
<!-- 循环政策:基于时间创建日志文件 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志文件名格式 -->
<fileNamePattern>${log.path}/error.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 日志最大的历史 60天 -->
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!-- 过滤的级别 -->
<level>ERROR</level>
<!-- 匹配时的操作:接收(记录) -->
<onMatch>ACCEPT</onMatch>
<!-- 不匹配时的操作:拒绝(不记录) -->
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 系统模块日志级别控制 -->
<logger name="com.spzx" level="info" />
<!-- Spring日志级别控制 -->
<logger name="org.springframework" level="warn" />
<root level="info">
<appender-ref ref="console" />
</root>
<!--系统操作日志-->
<root level="info">
<appender-ref ref="file_info" />
<appender-ref ref="file_error" />
</root>
</configuration>
添加启动类
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" +
" ''-' `'-' `-..-' ");
}
}
在spzx-gateway-dev.yml配置文件中添加会员服务的网关信息
# 购物车服务
- id: spzx-cart
uri: lb://spzx-cart
predicates:
- Path=/cart/**
filters:
- StripPrefix=1
1、商品详情页加入购物车
2、加入购物车必须登录
3、购物车页面加减商品数量与商品详情页加入购物车是同一个接口
加入购物车功能如图所示:
查看接口文档:
添加购物车接口地址及返回结果
get /cart/addToCart/{skuId}/{skuNum}
返回结果:
{
"msg": "操作成功",
"code": 200
}
操作模块:spzx-product
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<ProductSkuVo> 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
在spzx-api模块下新建子模块spzx-api-product
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.spzx</groupId>
<artifactId>spzx-api</artifactId>
<version>3.6.3</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spzx-api-product</artifactId>
<description>
spzx-api-product商品接口模块
</description>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- RuoYi Common Core-->
<dependency>
<groupId>com.spzx</groupId>
<artifactId>spzx-common-core</artifactId>
</dependency>
</dependencies>
</project>
<dependency>
<groupId>com.spzx</groupId>
<artifactId>spzx-api-product</artifactId>
<version>3.6.3</version>
</dependency>
将spzx-product模块ProductSkuVo移动到spzx-api-product模块的包com.spzx.product.api.domain中
在spzx-common-core中添加常量
/**
* 商品服务的serviceid
*/
public static final String PRODUCT_SERVICE = "spzx-product";
package com.spzx.product.api;
@FeignClient(contextId = "remoteProductService", value = ServiceNameConstants.PRODUCT_SERVICE, fallbackFactory = RemoteProductFallbackFactory.class)
public interface RemoteProductService {
@GetMapping("/productSku/getProductSku/{skuId}")
R<ProductSkuVo> getProductSku(@PathVariable("skuId") Long skuId, @RequestHeader(SecurityConstants.FROM_SOURCE) String source);
}
package com.spzx.product.api.factory;
@Component
public class RemoteProductFallbackFactory implements FallbackFactory<RemoteProductService>
{
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<ProductSkuVo> getProductSku(Long skuId, String source) {
return R.fail("获取商品sku失败:" + throwable.getMessage());
}
};
}
}
resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.spzx.product.api.factory.RemoteProductFallbackFactory
在spzx-api模块下新建子模块spzx-api-cart
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.spzx</groupId>
<artifactId>spzx-api</artifactId>
<version>3.6.3</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spzx-api-cart</artifactId>
<description>
spzx-api-cart购物车接口模块
</description>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- spzx Common Core-->
<dependency>
<groupId>com.spzx</groupId>
<artifactId>spzx-common-core</artifactId>
</dependency>
</dependencies>
</project>
<dependency>
<groupId>com.spzx</groupId>
<artifactId>spzx-api-cart</artifactId>
<version>3.6.3</version>
</dependency>
操作模块:spzx-api-cart
定义一个实体类来封装购物车中的商品数据(购物项数据),该实体类的定义依据:购物车列表页面需要展示的数据。如下所示:
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;
}
操作模块:spzx-cart
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();
}
}
package com.spzx.cart.service;
public interface ICartService {
void addToCart(Long skuId, Integer skuNum);
}
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<String, String, CartInfo> 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<ProductSkuVo> 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);
}
}
}
当用户在商品详情页面点击购物车按钮的时候,那么此时就需要将当前登录用户的所对应的所有的购物车数据在购物车页面展出出来。如下图所示:
当商品价格变化时,页面可以显示实时价格:
查看接口文档:
购物车列表接口地址及返回结果
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
}
...
]
}
操作模块:spzx-cart
@Operation(summary = "查询购物车")
@GetMapping("/cartList")
public AjaxResult cartList() {
return success(cartService.getCartList());
}
List<CartInfo> getCartList();
@Override
public List<CartInfo> getCartList() {
// 获取当前登录用户的id
Long userId = SecurityContextHolder.getUserId();
String cartKey = this.getCartKey(userId);
// 获取数据
List<CartInfo> cartInfoList = redisTemplate.opsForHash().values(cartKey);
if (!CollectionUtils.isEmpty(cartInfoList)) {
List<CartInfo> infoList = cartInfoList.stream()
//降序
.sorted((o1, o2) -> o2.getCreateTime().compareTo(o1.getCreateTime()))
.collect(Collectors.toList());
return infoList;
}
return new ArrayList<>();
}
删除功能如图所示:
查看接口文档:
删除购物车商品接口地址及返回结果
get /cart/deleteCart/{skuId}
返回结果:
{
"msg": "操作成功",
"code": 200
}
@Operation(summary = "删除购物车商品")
@DeleteMapping("/deleteCart/{skuId}")
public AjaxResult deleteCart(
@Parameter(name = "skuId", description = "商品skuId", required = true)
@PathVariable("skuId") Long skuId
) {
cartService.deleteCart(skuId);
return success();
}
void deleteCart(Long skuId);
@Override
public void deleteCart(Long skuId) {
// 获取当前登录用户的id
Long userId = SecurityContextHolder.getUserId();
String cartKey = getCartKey(userId);
//获取缓存对象
BoundHashOperations<String, String, CartInfo> hashOperations = redisTemplate.boundHashOps(cartKey);
hashOperations.delete(skuId.toString());
}
更新选中商品状态功能如图所示:
查看接口文档:
更新选中商品状态接口地址及返回结果
get /cart/checkCart/{skuId}/{isChecked}
返回结果:
{
"msg": "操作成功",
"code": 200
}
@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();
}
void checkCart(Long skuId, Integer isChecked);
@Override
public void checkCart(Long skuId, Integer isChecked) {
// 获取当前登录用户的id
Long userId = SecurityContextHolder.getUserId();
// 修改缓存
String cartKey = this.getCartKey(userId);
BoundHashOperations<String, String, CartInfo> hashOperations = redisTemplate.boundHashOps(cartKey);
// 先获取用户选择的商品
if (hashOperations.hasKey(skuId.toString())) {
CartInfo cartInfoUpd = hashOperations.get(skuId.toString());
// cartInfoUpd 写回缓存
cartInfoUpd.setIsChecked(isChecked);
// 更新缓存
hashOperations.put(skuId.toString(), cartInfoUpd);
}
}
更新购物车商品全部选中状态功能如图所示:
查看接口文档:
更新购物车商品全部选中状态接口地址及返回结果
get /cart/allCheckCart/{isChecked}
返回结果:
{
"msg": "操作成功",
"code": 200
}
@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();
}
void allCheckCart(Integer isChecked);
@Override
public void allCheckCart(Integer isChecked) {
// 获取当前登录用户的id
Long userId = SecurityContextHolder.getUserId();
String cartKey = getCartKey(userId);
BoundHashOperations<String, String, CartInfo> hashOperations = redisTemplate.boundHashOps(cartKey);
List<CartInfo> cartInfoList = hashOperations.values();
cartInfoList.forEach(item -> {
CartInfo cartInfoUpd = hashOperations.get(item.getSkuId().toString());
cartInfoUpd.setIsChecked(isChecked);
// 更新缓存
hashOperations.put(item.getSkuId().toString(), cartInfoUpd);
});
}
清空购物车功能如图所示:
查看接口文档:
清空购物车接口地址及返回结果
get /cart/clearCart
返回结果:
{
"msg": "操作成功",
"code": 200
}
@Operation(summary="清空购物车")
@GetMapping("/clearCart")
public AjaxResult clearCart(){
cartService.clearCart();
return success();
}
void clearCart();
@Override
public void clearCart() {
// 获取当前登录用户的id
Long userId = SecurityContextHolder.getUserId();
String cartKey = getCartKey(userId);
//获取缓存对象
redisTemplate.delete(cartKey);
}