创建购物车模块。。。
添加依赖。。。
创建完成后配置域名映射。。。。
将静态资源上传至 nginx 做动静分离。。。
将页面复制到项目的 template 文件夹下。。。注意改静态资源路径
排除数据库的相关配置。。。
配置 nacos 注册中心地址。。。
A、需求描述
用户可以在登录状态下将商品添加到购物车【用户购物车/在线购物车】
登录以后,会将临时购物车的数据全部合并过来,并清空临时购物车。
用户可以在未登录状态下将商品添加到购物车【游客购物车/离线购物车/临时购物车】
浏览器即使关闭,下次进入,临时购物车数据都在。
用户可以使用购物车一起结算下单:
B、数据结构
{
skuId: 2131241,
check: true,
title: "Apple iphone.....",
defaultImage: "...",
price: 4999,
count: 1,
totalPrice: 4999,
skuSaleVO: {...}
}
另外,购物车中不止一条数据,因此最终会是对象的数组。即:
[
{...},{...},{...}
]
整个购物车封装成的 VO 如下:
package com.fancy.gulimall.cart.vo;
import java.math.BigDecimal;
import java.util.List;
/**
* 整个购物车
* 需要计算的属性,必须重写他的get方法,保证每次获取属性都会进行计算
*/
public class Cart {
List<CartItem> items;
private Integer countNum;//商品数量
private Integer countType;//商品类型数量
private BigDecimal totalAmount;//商品总价
private BigDecimal reduce = new BigDecimal("0.00");//减免价格
public List<CartItem> getItems() {
return items;
}
public void setItems(List<CartItem> items) {
this.items = items;
}
public Integer getCountNum() {
int count = 0;
if (items != null && items.size() > 0) {
for (CartItem item : items) {
count += item.getCount();
}
}
return count;
}
public Integer getCountType() {
int count = 0;
if (items != null && items.size() > 0) {
for (CartItem item : items) {
count += 1;
}
}
return count;
}
public BigDecimal getTotalAmount() {
BigDecimal amount = new BigDecimal("0");
//1、计算购物项总价
if (items != null && items.size() > 0) {
for (CartItem item : items) {
if(item.getCheck()){
BigDecimal totalPrice = item.getTotalPrice();
amount = amount.add(totalPrice);
}
}
}
//2、减去优惠总价
BigDecimal subtract = amount.subtract(getReduce());
return subtract;
}
public BigDecimal getReduce() {
return reduce;
}
public void setReduce(BigDecimal reduce) {
this.reduce = reduce;
}
}
CartItem 购物项封装成的 VO 如下
package com.fancy.gulimall.cart.vo;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
/**
* 购物项内容
*/
public class CartItem {
private Long skuId;
private Boolean check = true;
private String title;
private String image;
private List<String> skuAttr;
private BigDecimal price;
private Integer count;
private BigDecimal totalPrice;
public Long getSkuId() {
return skuId;
}
public void setSkuId(Long skuId) {
this.skuId = skuId;
}
public Boolean getCheck() {
return check;
}
public void setCheck(Boolean check) {
this.check = check;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getImage() {
return image;
}
public void setImage(String image) {
this.image = image;
}
public List<String> getSkuAttr() {
return skuAttr;
}
public void setSkuAttr(List<String> skuAttr) {
this.skuAttr = skuAttr;
}
public BigDecimal getPrice() {
return price;
}
public void setPrice(BigDecimal price) {
this.price = price;
}
public Integer getCount() {
return count;
}
public void setCount(Integer count) {
this.count = count;
}
/**
* 计算当前项的总价
* @return
*/
public BigDecimal getTotalPrice() {
return this.price.multiply(new BigDecimal("" + this.count));
}
public void setTotalPrice(BigDecimal totalPrice) {
this.totalPrice = totalPrice;
}
}
Redis 有 5 种不同数据结构,这里选择哪一种比较合适呢?Map
首先不同用户应该有独立的购物车,因此购物车应该以用户的作为 key 来存储,value 是用户的所有购物车信息。这样看来基本的k-v
结构就可以了。
但是,我们对购物车中的商品进行增、删、改操作,基本都需要根据商品 id 进行判断,为了方便后期处理,我们的购物车也应该是k-v
结构,key 是商品 id,value 才是这个商品的购物车信息。
综上所述,我们的购物车结构是一个双层 Map:Map
两个功能:新增商品到购物车、查询购物车。
新增商品:判断是否登录
查询购物车列表:判断是否登录
进入购物车之前我们需要进行身份鉴别,无登录状态操作的就是离线购物车,登录状态时操作的是用户购物车,离线购物车添加完商品之后登录将进行合并购物车。。。。
因此,在执行目标方法之前,我们需要判断用户的登录状态,我们在拦截器中进行此操作。。。
package com.fancy.gulimall.cart.interceptor;
import com.alibaba.nacos.client.utils.StringUtils;
import com.fancy.common.constant.AuthServerConstant;
import com.fancy.common.constant.CartConstant;
import com.fancy.common.vo.MemberRespVo;
import com.fancy.gulimall.cart.vo.UserInfoTo;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.UUID;
// 在执行目标方法之前,判断用户的登录状态。并封装传递(用户信息)给controller
public class CartInterceptor implements HandlerInterceptor {
public static ThreadLocal<UserInfoTo> threadLocal = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
UserInfoTo userInfoTo = new UserInfoTo();
HttpSession session = request.getSession();
MemberRespVo member = (MemberRespVo) session.getAttribute(AuthServerConstant.LOGIN_USER);
if(member != null){
//用户登录
userInfoTo.setUserId(member.getId());
}
Cookie[] cookies = request.getCookies();
if(cookies!=null && cookies.length>0){
for (Cookie cookie : cookies) {
//user-key
String name = cookie.getName();
if(name.equals(CartConstant.TEMP_USER_COOKIE_NAME)){
userInfoTo.setUserKey(cookie.getValue());
userInfoTo.setTempUser(true);
}
}
}
//如果没有临时用户一定分配一个临时用户
if(StringUtils.isEmpty(userInfoTo.getUserKey())){
String uuid = UUID.randomUUID().toString();
userInfoTo.setUserKey(uuid);
}
//目标方法执行之前
threadLocal.set(userInfoTo);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
UserInfoTo userInfoTo = threadLocal.get();
//如果没有临时用户一定保存一个临时用户
if(!userInfoTo.isTempUser()){
//持续的延长临时用户的过期时间
Cookie cookie = new Cookie(CartConstant.TEMP_USER_COOKIE_NAME, userInfoTo.getUserKey());
cookie.setDomain("gulimall.com");
cookie.setMaxAge(CartConstant.TEMP_USER_COOKIE_TIMEOUT);
response.addCookie(cookie);
}
}
}
拦截器配置:
package com.fancy.gulimall.cart.config;
import com.fancy.gulimall.cart.interceptor.CartInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class GulimallWebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new CartInterceptor()).addPathPatterns("/**");
}
}
而且,因为用户鉴别和鉴别完之后执行操作是同一个线程内的,我们可以使用 ThreadLocal 将需要共享的数据放在这个线程中去。。。,详见如上代码。。。
A、临时购物车
/**
* 获取到我们要操作的购物车
* @return
*/
private BoundHashOperations<String, Object, Object> getCartOps() {
UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
String cartKey = "";
if (userInfoTo.getUserId() != null) {
//gulimall:cart:1
cartKey = CART_PREFIX + userInfoTo.getUserId();
} else {
cartKey = CART_PREFIX + userInfoTo.getUserKey();
}
BoundHashOperations<String, Object, Object> operations =
redisTemplate.boundHashOps(cartKey);
return operations;
}
B、登录购物车
@Override
public Cart getCart() throws ExecutionException, InterruptedException {
Cart cart = new Cart();
UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
if(userInfoTo.getUserId()!=null) {
//1、登录
String cartKey =CART_PREFIX+ userInfoTo.getUserId();
//2、如果临时购物车的数据还没有进行合并【合并购物车】
String tempCartKey = CART_PREFIX + userInfoTo.getUserKey();
List<CartItem> tempCartItems = getCartItems(tempCartKey);
if(tempCartItems!=null){
//临时购物车有数据,需要合并
for (CartItem item : tempCartItems) {
addToCart(item.getSkuId(),item.getCount());
}
//清除临时购物车的数据
clearCart(tempCartKey);
}
//3、获取登录后的购物车的数据【包含合并过来的临时购物车的数据,和登录后的购物车的数据】
List<CartItem> cartItems = getCartItems(cartKey);
cart.setItems(cartItems);
} else {
//2、没登录
String cartKey =CART_PREFIX+ userInfoTo.getUserKey();
//获取临时购物车的所有购物项
List<CartItem> cartItems = getCartItems(cartKey);
cart.setItems(cartItems);
}
return cart;
}
@Override
public CartItem addToCart(Long skuId, Integer num) throws ExecutionException, InterruptedException {
BoundHashOperations<String, Object, Object> cartOps = getCartOps();
String res = (String) cartOps.get(skuId.toString());
if(StringUtils.isEmpty(res)){
//购物车无此商品
//2、添加新商品到购物车
//1、远程查询当前要添加的商品的信息
CartItem cartItem = new CartItem();
CompletableFuture<Void> getSkuInfoTask = CompletableFuture.runAsync(() -> {
R skuInfo = productFeignService.getSkuInfo(skuId);
SkuInfoVo data = skuInfo.getData("skuInfo", new TypeReference<SkuInfoVo>() {
});
cartItem.setCheck(true);
cartItem.setCount(num);
cartItem.setImage(data.getSkuDefaultImg());
cartItem.setTitle(data.getSkuTitle());
cartItem.setSkuId(skuId);
cartItem.setPrice(data.getPrice());
},executor);
//2、远程查询sku的组合信息
CompletableFuture<Void> getSkuSaleAttrValues = CompletableFuture.runAsync(() -> {
List<String> values = productFeignService.getSkuSaleAttrValues(skuId);
cartItem.setSkuAttr(values);
}, executor);
CompletableFuture.allOf(getSkuInfoTask,getSkuSaleAttrValues).get();
String s = JSON.toJSONString(cartItem);
cartOps.put(skuId.toString(),s);
return cartItem;
}
else{
//购物车有此商品,修改数量
CartItem cartItem = JSON.parseObject(res, CartItem.class);
cartItem.setCount(cartItem.getCount()+num);
cartOps.put(skuId.toString(),JSON.toJSONString(cartItem));
return cartItem;
}
}
这里用到了 OpenFeign 远程调用,调用了远程 product 服务的方法来获取商品的相关信息。。。