一、购物车业务简介
购物车模块要能过存储顾客所选的的商品,记录下所选商品,还要能随时更新,当用户决定购买时,用户可以选择决定购买的商品进入结算页面。
功能要求:
1) 利用缓存提高性能。
2) 未登录状态也可以存入购物车,一旦用户登录要进行合并操作。
二、 购物车模块搭建
2.1 搭建service-cart服务
2.2 修改配置pom.xml
<?xml version=”1.0” encoding=”UTF-8”?> <project xmlns=”http://maven.apache.org/POM/4.0.0“ xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance“ xsi:schemaLocation=”http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd” > <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.atguigu.gmall</groupId> <artifactId>service</artifactId> <version>1.0</version> </parent> <version>1.0</version> <artifactId>service-cart</artifactId> <packaging>jar</packaging> <name>service-cart</name> <description>service-cart</description> <dependencies> <dependency> <groupId>com.atguigu.gmall</groupId> <artifactId>service-product-client</artifactId> <version>1.0</version> </dependency> </dependencies> <build> <finalName>service-cart</finalName> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project> |
---|
2.3 添加配置文件
bootstrap.properties
spring.application.name=service-cart spring.profiles.active=dev spring.cloud.nacos.discovery.server-addr=192.168.200.129:8848 spring.cloud.nacos.config.server-addr=192.168.200.129: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 |
---|
2.4 启动类
package com.atguigu.gmall.cart; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.context.annotation.ComponentScan; @SpringBootApplication(exclude = DataSourceAutoConfiguration.class) @ComponentScan(basePackages = “com.atguigu.gmall”) @EnableDiscoveryClient @EnableFeignClients(basePackages = “com.atguigu.gmall”) public class ServiceCartApplication { public static void main(String[] args) { SpringApplication.run(ServiceCartApplication.class,args); } } |
---|
三、功能—添加入购物车
3.1 功能解析:
1、 商品详情页添加购物车
2、 添加购物车,用户可以不需要登录,如果用户没有登录,则生成临时用户id,购物车商品与临时用户id关联,当用户登录后,将临时用户id的购物车商品与登录用户id的商品合并
3、 商品详情添加购物车时,先判断用户是否登录,如果没登录,再判断是否存在临时用户,如果cookie中也没有临时用户,则生成临时用户
3.2 处理临时用户
3.2.1 商品详情页
商品详情添加购物车页面方法(/item/index.html):
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
在server-gateway 项目中添加
/ 获取当前用户临时用户id @param **request __ @return ** **/ private String getUserTempId(ServerHttpRequest request) { String userTempId = “”; List if(null != tokenList) { userTempId = tokenList.get(0); } else { MultiValueMap HttpCookie cookie = cookieMultiValueMap.getFirst(“userTempId”); if(cookie != null){ userTempId = URLDecoder.decode(cookie.getValue()); } } return userTempId; } |
---|
将userTempId 添加header请求头
//设置网关请求头 _String userTempId = this.getUserTempId(request); if(!StringUtils._isEmpty(userId) || !StringUtils.isEmpty(userTempId)) { if(!StringUtils.isEmpty(userId)) { request.mutate().header(“userId”, userId).build(); } if(!StringUtils.isEmpty(userTempId)) { request.mutate().header(“userTempId”, userTempId).build(); } //将现在的request 变成 exchange对象 return chain.filter(exchange.mutate().request(request).build()); } |
---|
AuthContextHolder类添加公共方法
/ 获取当前未登录临时用户id @param **request __ @return ** **/ public static String getUserTempId(HttpServletRequest request) { String userTempId = request.getHeader(“userTempId”); return StringUtils.isEmpty(userTempId) ? “” : userTempId; } |
---|
3.3 功能开发:
3.3.1 创建实体
@Data @ApiModel(description = “购物车”) 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; @ApiModelProperty(value = “数量”) private Integer skuNum; @ApiModelProperty(value = “图片文件”) private String imgUrl; @ApiModelProperty(value = “sku名称 (冗余)”) private String skuName; @ApiModelProperty(value = “isChecked”) private Integer isChecked = 1; _// 实时价格 skuInfo.price _BigDecimal skuPrice; } |
---|
3.3.2 创建添加购物车接口
package com.atguigu.gmall.cart.service; public interface CartService { // 添加购物车 用户Id,商品Id,商品数量。 void addToCart(Long skuId, String userId, Integer skuNum); } |
---|
3.3.3 添加购物车实现类
定义业务需要使用的常量,RedisConst类
public static final String USER_KEY_PREFIX = “user:”; public static final String USER_CART_KEY_SUFFIX = “:cart”; public static final long USER_CART_EXPIRE = 30000; |
---|
@Service public class CartServiceImpl implements CartService { @Autowired private ProductFeignClient productFeignClient; @Autowired private RedisTemplate redisTemplate;} |
---|
@Override public void addToCart(Long skuId, String userId, Integer skuNum) { // 获取缓存key _String cartKey = getCartKey(userId); BoundHashOperations CartInfo cartInfo = null; if(boundHashOps.hasKey(skuId.toString())) { cartInfo = boundHashOps.get(skuId.toString()); cartInfo.setSkuNum(cartInfo.getSkuNum()+skuNum); cartInfo.setIsChecked(1); cartInfo.setSkuPrice(productFeignClient.getSkuPrice(skuId)); cartInfo.setUpdateTime(new Date()); } else { cartInfo = new CartInfo(); // 给cartInfo 赋值! SkuInfo skuInfo = productFeignClient.getSkuInfo(skuId); // 给表的字段赋值! _cartInfo.setUserId(userId); cartInfo.setSkuId(skuId); cartInfo.setCartPrice(skuInfo.getPrice()); cartInfo.setSkuNum(skuNum); cartInfo.setImgUrl(skuInfo.getSkuDefaultImg()); cartInfo.setSkuName(skuInfo.getSkuName()); cartInfo.setCreateTime(new Date()); cartInfo.setUpdateTime(new Date()); cartInfo.setSkuPrice(skuInfo.getPrice()); } boundHashOps.put(skuId.toString(), cartInfo); } |
// 获取购物车的keyprivate String getCartKey(String userId) { //定义key user:userId:cart return RedisConst.USER_KEY_PREFIX + userId + RedisConst.USER_CART_KEY_SUFFIX; } |
3..3.4 添加购物车控制器
package com.atguigu.gmall.cart.controller; @RestController @RequestMapping(“api/cart”) public class CartApiController { @Autowired private CartService cartService; / 添加购物车 @param **skuId __ *@param skuNum __ *@param request __ @return ** **/ @RequestMapping(“addToCart/{skuId}/{skuNum}”) public Result addToCart(@PathVariable(“skuId”) Long skuId, @PathVariable(“skuNum”) Integer skuNum, HttpServletRequest request) { // 如何获取userId _String userId = AuthContextHolder._getUserId(request); if (StringUtils.isEmpty(userId)) { // 获取临时用户Id _userId = AuthContextHolder._getUserTempId(request); } cartService.addToCart(skuId, userId, skuNum); return Result.ok(); } } |
---|
四、功能—展示购物车列表
4.1 功能解析
4.2 购物车列表接口:CartService
/ 通过用户Id 查询购物车列表 @param **userId __ *@param userTempId __ _ @return ** **/ _List |
---|
4.3 实现类:CartServiceImpl
@Override public List //获取临时用户购物车数据 _List if(!StringUtils._isEmpty BoundHashOperations cartInfoList = boundHashOps.values(); } //获取用户购物车数据 if(!StringUtils.isEmpty(userId)){ BoundHashOperations cartInfoList = boundHashOps.values(); } if(!CollectionUtils.isEmpty(cartInfoList)){ // 展示购物车列表的时候应该有顺序! 京东:按照更新时间! 苏宁:创建时间! _cartInfoList.sort((o1,o2)->{ // 使用时间进行比较 return DateUtil._truncatedCompareTo(o2.getUpdateTime(),o1.getUpdateTime(), Calendar.SECOND); }); } return cartInfoList; } |
---|
4.4 控制器:CartApiController
/ 查询购物车 * @param **request __ @return ** **/ @GetMapping(“cartList”) public Result cartList(HttpServletRequest request) { // 获取用户Id _String userId = AuthContextHolder._getUserId(request); // 获取临时用户Id _String userTempId = AuthContextHolder._getUserTempId(request); List return Result.ok(cartInfoList); } |
---|
五、功能—合并购物车
功能分析:
1. 当用户登录以后,先判断未登录的时候,用户是否购买了商品。
a) 如果用户购买了商品,则找到对应的商品Id,对数量进行合并。
b) 没有找到的商品,则直接添加到数据。
2. 合并完成之后,删除未登录数据。
5.1 更改实现类:CartServiceImpl
@Override public List / 1. 判断是否登录,根据判断结果查询不同的购物车! 2. 查询的结果需要排序! 3. 有可能需要合并! 在登录的情况下 . 未登录 —-> 登录合并! 合并完成之后,需要删除未登录购物车数据! case1: 有userId ,没有userTempId case2: 没有userId ,有userTempId return noLoginCartInfoList case3: 有userId ,有userTempId 登录情况下合并购物车: 先判断未登录购物车集合有数据! true: 有数据 合并 false: 没有数据 只需要登录购物车数据 删除未登录购物车! / // 声明一个集合来存储未登录数据 _List // 属于未登录 if (!StringUtils._isEmpty(userTempId)){ String cartKey = this.getCartKey(userTempId); // 获取登录的购物车集合数据! // noLoginCartInfoList = this.redisTemplate.boundHashOps(cartKey).values(); _noLoginCartInfoList = this.redisTemplate.opsForHash().values(cartKey); } // 这个是集合的排序 if (StringUtils._isEmpty(userId)){ if (!CollectionUtils.isEmpty(noLoginCartInfoList)){ noLoginCartInfoList.sort((o1,o2)->{ // 按照更新时间: return DateUtil.truncatedCompareTo(o2.getUpdateTime(),o1.getUpdateTime(), Calendar.SECOND); }); } // 返回未登录数据! return noLoginCartInfoList; } // ———————————case 1 and case3 ————————- / demo: 登录: 17 1 18 1 未登录: 17 1 18 1 19 2 合并: 17 2 18 2 19 2 / // 属于登录 _List String cartKey = this.getCartKey(userId); // hset key field value; hget key field; hvals key ; hmset key field value field value; hmset key map; // 合并思路二: BoundHashOperations // boundHashOperations.hasKey(skuId.toString); if (!CollectionUtils._isEmpty(noLoginCartInfoList)){ // 循环遍历未登录购物车集合 _noLoginCartInfoList.stream().forEach(cartInfo -> { // 在未登录购物车中的skuId 与登录的购物车skuId 相对 skuId = 17 18 if (boundHashOperations.hasKey(cartInfo.getSkuId().toString())){ // 合并业务逻辑 : skuNum + skuNum 更新时间 CartInfo loginCartInfo = boundHashOperations.get(cartInfo.getSkuId().toString()); loginCartInfo.setSkuNum(loginCartInfo.getSkuNum()+cartInfo.getSkuNum()); loginCartInfo.setUpdateTime(new Date()); // 最新价格 loginCartInfo.setSkuPrice(productFeignClient.getSkuPrice(cartInfo.getSkuId())); // 选中状态合并! if (cartInfo.getIsChecked().intValue()==1){ // if (loginCartInfo.getIsChecked().intValue()==0){ // loginCartInfo.setIsChecked(1); // } loginCartInfo.setIsChecked(1); } // 修改缓存的数据: hset key field value boundHashOperations.put(cartInfo.getSkuId().toString(),loginCartInfo); }else { // 直接添加到缓存! skuId = 19 cartInfo.setUserId(userId); cartInfo.setCreateTime(new Date()); cartInfo.setUpdateTime(new Date()); boundHashOperations.put(cartInfo.getSkuId().toString(),cartInfo); } }); // 删除未登录购物车数据! this.redisTemplate.delete(this.getCartKey(userTempId)); } // 获取到合并之后的数据: LoginCartInfoList = this.redisTemplate.boundHashOps(cartKey).values(); if (CollectionUtils._isEmpty(LoginCartInfoList)){ return new ArrayList<>(); } // 设置合并之后的排序结果! _LoginCartInfoList.sort(((o1, o2) -> { return DateUtil._truncatedCompareTo(o2.getUpdateTime(),o1.getUpdateTime(), Calendar.SECOND); })); return LoginCartInfoList; } |
---|
六、选中状态的变更
用户每次勾选购物车的多选框,都要把当前状态保存起来。由于可能会涉及更频繁的操作,所以这个勾选状态不必存储到数据库中。保留在缓存状态即可。
6.1 编写业务接口与实现
接口 / 更新选中状态 * @param **userId __ *@param isChecked __ *@param skuId __ */ void checkCart(String userId, Integer isChecked, Long skuId); |
---|
实现类 @Override public void checkCart(String userId, Integer isChecked, Long skuId) { String cartKey = this.getCartKey(userId); BoundHashOperations CartInfo cartInfo = boundHashOps.get(skuId.toString()); if(null != cartInfo) { cartInfo.setIsChecked(isChecked); boundHashOps.put(skuId.toString(), cartInfo); } } |
6.2 编写控制器
// 选中状态 @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(); } |
---|
七、删除购物车
7.1 封装业务接口与实现
接口void deleteCart(Long skuId, String userId); |
---|
实现类 @Override public void deleteCart(Long skuId, String userId) { BoundHashOperations // 判断购物车中是否有该商品! if (boundHashOps.hasKey(skuId.toString())){ boundHashOps.delete(skuId.toString()); } } |
7.2 编写控制器
/ 删除 * @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(); } |
---|
八、前端实现
8.1 在web-all添加前端实现
8.1.1 添加依赖和配置网关
<dependency> <groupId>com.atguigu.gmall</groupId> <artifactId>service-cart-client</artifactId> <version>1.0</version> </dependency> |
---|
- id: web-cart uri: lb://web-all predicates: - Host=cart.gmall.com - id: service-cart uri: lb://service-cart predicates: - Path=//cart/* |
---|
8.1.2 controller实现
package com.atguigu.gmall.all.controller; /* 购物车页面 / @Controller public class CartController { @Autowired private CartFeignClient cartFeignClient; @Autowired private ProductFeignClient productFeignClient; / 查看购物车 @param **request __ @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”; } } |
---|