24.购物车

环境搭建

 

 整理静态资源

排除数据库

 导入common模块依赖

  <dependency>
            <groupId>com.wuyimin.gulimall</groupId>
            <artifactId>gulimall-common</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>

配置,名字配置在properties里面

 

 

 

 网关配置

需求分析

购物车分为离线购物车和在线购物车

  • 离线购物车可以购买商品,在用户登录以后,离线购物车被清空,所有商品转移到在线购物车
  • 即使浏览器关闭,离线购物车的数据还要在

购物车功能:

  • crud商品
  • 修改商品的购买数量
  • 在购物车中展示商品的优惠信息
  • 提示购物车商品的价格变化
  • 是否选中商品

存储(读写并发):放入redis--持久化策略

skuId:123 check:true title:"Apple“ defaultImage:”xxx“ price:1999 count:1 totalPrice:1999 skuSaleVo:{..}

Key值:cart:1 代表一号用户的购物车

Value值:hash 哈希有key-value值

数据结构Map<String cart,Map<Long itemId,CartInfo>>

Vo编写--Cart和CartItem

@Data
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 BigDecimal getTotalPrice(){
        return price.multiply(new BigDecimal(""+this.count));//总价
    }

}
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() {
        if(items!=null&&items.size()>0){
            return items.size();
        }
        return null;
    }
    public BigDecimal getTotalAmount() {
        BigDecimal amount = new BigDecimal("0");
        if(this.items != null && this.items.size() > 0){
            for (CartItem item : this.items) {
                if(item.getCheck()){
                    BigDecimal totalPrice = item.getTotalPrice();
                    amount = amount.add(totalPrice);
                }
            }
        }
        return amount.subtract(this.getReduce());
    }

    public BigDecimal getReduce() {
        return reduce;
    }

    public void setReduce(BigDecimal reduce) {
        this.reduce = reduce;
    }
}

ThreadLocal用户验证

cart模块引入redis依赖

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

配置redis地址

 

 

 session配置

 <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>

session配置类


@EnableRedisHttpSession
@Configuration
public class RedisSessionConfig {
    @Bean // redis的json序列化
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
        return new GenericJackson2JsonRedisSerializer();
    }

    @Bean // cookie
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer serializer = new DefaultCookieSerializer();
        serializer.setCookieName("GULISESSIONID"); // cookie的键
        serializer.setDomainName("gulimall.com"); // 扩大session作用域,也就是cookie的有效域
        return serializer;
    }
}

session配置文件

 

UserInfoVo

@ToString
@Data
public class UserInfoVo {
    private Long userId;
    private String userKey;
    private boolean hasCookie=false;//是否登录过
}

 配置拦截器,ThreadLocal用于同一个线程共享数据,关于情况threadLocal内存泄漏的问题

具体流程:

  • 不管是已经登录的用户还是未登录的用户,使用购物车的时候都会储存一个临时购物车数据
  • 未使用过购物车的用户在第一次获得购物车数据的时候会在pre里往vo放置一个uuid值,在post里把这个值传入cookie
  • 已经使用过购物车的用户会在pre里把vo的判断是否有cookie的属性置为true,在post里如果判断为true的话,就不会重新放置cookie

/**
 * @ Author wuyimin
 * @ Date 2021/8/24-10:20
 * @ Description 执行controller方法之前,判断用户的登录信息,封装传递给controller目标请求
 */
//@Component//在配置中创建了就不用放在容器中了
public class CartInterceptor implements HandlerInterceptor {
    //其实是一个Map<Thread,T>
    public static ThreadLocal<UserInfoVo> threadLocal=new ThreadLocal<>();
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        HttpSession session = request.getSession();//获得session
        MemberRespVo memberRespVo = (MemberRespVo) session.getAttribute(AuthServerConstant.LOGIN_USER);//获得用户数据
        UserInfoVo userInfoVo = new UserInfoVo();
        if(memberRespVo!=null){
            //用户登录了
            userInfoVo.setUserId(memberRespVo.getId());
        }
        //离线购物车内容,通过cookie的key-value来区别用户,来到这里的用户是已经获得了cookie的临时用户
        Cookie[] cookies = request.getCookies();
        if (cookies!=null&&cookies.length>0) {
            for (Cookie cookie : cookies) {
                if(cookie.getName().equals(CartConstant.TEMP_USER_COOKIE_NAME)){
                    userInfoVo.setUserKey(cookie.getValue());
                    userInfoVo.setHasCookie(true);
                    break;
                }
            }
        }
        //如果是第一次登录的临时用户
        if(StringUtils.isEmpty(userInfoVo.getUserKey())){
            String uuid= UUID.randomUUID().toString();
            userInfoVo.setUserKey(uuid);
        }
        //在目标方法执行之前
        threadLocal.set(userInfoVo);
        // 还有一个登录后应该删除临时购物车的逻辑没有实现
        return true;
    }
    //业务执行之后:分配一个临时用户让浏览器保存
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        //不加判断就是一直延长过期时间
        UserInfoVo userInfoVo = threadLocal.get();
        //不是临时用户就代表以前没有注册过--第一次登录的临时用户或者已经登录的用户
        if(!userInfoVo.isHasCookie()){
            Cookie cookie = new Cookie(CartConstant.TEMP_USER_COOKIE_NAME, userInfoVo.getUserKey());
            cookie.setDomain("gulimall.com");//整个作用域都有效
            cookie.setMaxAge(CartConstant.TEMP_USER_COOKIE_TIMEOUT);
            response.addCookie(cookie);
        }
        //用完了threadLocal记得要清空,防止内存泄露,key为若引用,value为强引用,如果GC会导致key被收走了,value还在
        //线程池里面的线程,线程都是复用的,那么之前的线程实例处理完之后,出于复用的目的线程依然存活,所以,ThreadLocal设定的value值被持有,导致内存泄露。
        threadLocal.remove();
    }
}

拦截器配置

@Configuration
public class GulimallWebConfig implements WebMvcConfigurer {//和网络有关的都要实现这个接口
    //添加拦截器的配置
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new CartInterceptor()).addPathPatterns("/**");//拦截所有请求
    }
}

测试的Controller

@Controller
public class CartController {
    @GetMapping("/cart.html")
    public String cartListPage(HttpSession session){
        UserInfoVo userInfoVo = CartInterceptor.threadLocal.get();
        System.out.println(userInfoVo);
        return "cartList";
    }
}

打印

 

 清除redis和页面cookie之后,切换用户登录再测试

 添加购物车

远程接口,根据skuid把相关属性拼装成颜色:蓝色,版本:256G的形式返回

  @GetMapping("/stringlist/{skuId}")
    public List<String> getSkuAttrValues(@PathVariable("skuId") Long skuId){
        List<String> list= skuSaleAttrValueService.getSkuSaleAttrValuesAsStringList(skuId);
        return list;
    }

具体方法

    <select id="getSkuSaleAttrValuesAsStringList" resultType="java.lang.String">
        select concat(attr_name,":",attr_value) from pms_sku_sale_attr_value where sku_id=#{skuId}
    </select>

把之前Product服务里的线程池相关配置拿过来,做异步编排远程调用

cart模块远程接口

@FeignClient("gulimall-product")
public interface ProductFeignService {
    @RequestMapping("/product/skuinfo/info/{skuId}")
    R info(@PathVariable("skuId") Long skuId);
    @GetMapping("/product/skusaleattrvalue/stringlist/{skuId}")
    List<String> getSkuAttrValues(@PathVariable("skuId") Long skuId);
}

 

@Slf4j
@Service
public class CartServiceImpl implements CartService {
    @Autowired
    StringRedisTemplate redisTemplate;
    @Autowired
    ProductFeignService productFeignService;
    @Autowired
    ThreadPoolExecutor threadPoolExecutor;

    @Override
    public CartItem addToCart(Long skuId, Integer num) throws ExecutionException, InterruptedException {
        BoundHashOperations<String, Object, Object> cartOps = getCartOps();
        //1.远程查询当前要添加的商品信息
        CartItem cartItem = new CartItem();
        CompletableFuture<Void> skuInfoFuture = CompletableFuture.runAsync(() -> {
            R info = productFeignService.info(skuId);
            if (info.getCode() != 0) {
                log.error("商品服务远程调用失败");
            }
            SkuInfoVo skuInfo = info.getData("skuInfo", new TypeReference<SkuInfoVo>() {
            });
            cartItem.setCheck(true);
            cartItem.setCount(num);
            cartItem.setImage(skuInfo.getSkuDefaultImg());
            cartItem.setTitle(skuInfo.getSkuTitle());
            cartItem.setSkuId(skuId);
            cartItem.setPrice(skuInfo.getPrice());
        },threadPoolExecutor);//使用自己的线程池进行调度
        CompletableFuture<Void> attrsFuture = CompletableFuture.runAsync(() -> {
            //2.远程查询展示组合信息
            List<String> skuAttrValues = productFeignService.getSkuAttrValues(skuId);
            cartItem.setSkuAttr(skuAttrValues);
        }, threadPoolExecutor);
        CompletableFuture.allOf(skuInfoFuture,attrsFuture).get();//等待两个步骤都完成再封装
        String s = JSON.toJSONString(cartItem);//转成Json给redis
        cartOps.put(skuId.toString(),s);
        return cartItem;
    }

    /**
     * 获取到要操作的购物车
     *
     * @return
     */
    private BoundHashOperations<String, Object, Object> getCartOps() {
        //操作购物车
        UserInfoVo userInfoVo = CartInterceptor.threadLocal.get();
        String key = "";
        if (userInfoVo.getUserId() != null) {//使用登录后的购物车
            key = CartConstant.CART_PREFIX + userInfoVo.getUserId();
        } else {//使用离线购物车
            key = CartConstant.CART_PREFIX + userInfoVo.getUserKey();
        }
        //绑定哈希操作
        BoundHashOperations<String, Object, Object> hashOps = redisTemplate.boundHashOps(key);
        return hashOps;
    }
}

 细化购物车--如果已经有这个商品了,就不需要远程调用去查了

 @Override
    public CartItem addToCart(Long skuId, Integer num) throws ExecutionException, InterruptedException {
        BoundHashOperations<String, Object, Object> cartOps = getCartOps();
        //1.远程查询当前要添加的商品信息
        String res = (String) cartOps.get(skuId.toString());
        if (StringUtils.isEmpty(res)) {
            CartItem cartItem = new CartItem();
            CompletableFuture<Void> skuInfoFuture = CompletableFuture.runAsync(() -> {
                R info = productFeignService.info(skuId);
                if (info.getCode() != 0) {
                    log.error("商品服务远程调用失败");
                }
                SkuInfoVo skuInfo = info.getData("skuInfo", new TypeReference<SkuInfoVo>() {
                });
                cartItem.setCheck(true);
                cartItem.setCount(num);
                cartItem.setImage(skuInfo.getSkuDefaultImg());
                cartItem.setTitle(skuInfo.getSkuTitle());
                cartItem.setSkuId(skuId);
                cartItem.setPrice(skuInfo.getPrice());
            }, threadPoolExecutor);//使用自己的线程池进行调度
            CompletableFuture<Void> attrsFuture = CompletableFuture.runAsync(() -> {
                //2.远程查询展示组合信息
                List<String> skuAttrValues = productFeignService.getSkuAttrValues(skuId);
                cartItem.setSkuAttr(skuAttrValues);
            }, threadPoolExecutor);
            CompletableFuture.allOf(skuInfoFuture, attrsFuture).get();//等待两个步骤都完成再封装
            String s = JSON.toJSONString(cartItem);//转成Json给redis
            cartOps.put(skuId.toString(), s);
            return cartItem;
        }else{
            //如果以前已经有了那么就直接加数量就行
            CartItem cartItem = JSON.parseObject(res, CartItem.class);
            cartItem.setCount(cartItem.getCount()+num);
            //更新一下redis
            cartOps.put(skuId.toString(),JSON.toJSONString(cartItem));
            return cartItem;
        }
    }

出现的问题,如果不停的刷新页面会不停的增加商品

 //添加完数据之后直接重定向到另一个请求,这样就不会造成刷新这个页面重复添加数据,因为刷新的是下一个请求
    @GetMapping("/addToCart")
    public String addToCart(@RequestParam("skuId") Long skuId, @RequestParam("num") Integer num, RedirectAttributes redirectAttributes) throws ExecutionException, InterruptedException {
        CartItem cartItem = cartService.addToCart(skuId, num);
        redirectAttributes.addAttribute("skuId", skuId);//给下面的请求传参数,model无法传参
        return "redirect:http://cart.gulimall.com/addToCartSuccess.html";//重定向要写完整域名
    }

    @GetMapping("/addToCartSuccess.html")
    public String addToCartSuccessPage(@RequestParam("skuId") Long skuId, Model model) {
        //重定向到成功页面,再次查询购物车数据即可
        CartItem cartItem = cartService.getCartItem(skuId);
        model.addAttribute("item", cartItem);
        return "success";
    }

 @Override
    public CartItem getCartItem(Long skuId) {
        BoundHashOperations<String, Object, Object> cartOps = getCartOps();
        String o = (String) cartOps.get(skuId.toString());
        CartItem cartItem = JSON.parseObject(o, CartItem.class);
        return cartItem;
    }

获取,合并购物车

 

 @GetMapping("/cart.html")
    public String cartListPage(Model model) {
        Cart cart=cartService.getCart();
        model.addAttribute("cart",cart);
        return "cartList";
    }

 

//获得购物车
    @Override
    public Cart getCart() throws ExecutionException, InterruptedException {
        Cart cart = new Cart();
        UserInfoVo userInfoVo=CartInterceptor.threadLocal.get();
        String tempKey = CartConstant.CART_PREFIX + userInfoVo.getUserKey();
        String loginKey = CartConstant.CART_PREFIX + userInfoVo.getUserId();
        if(userInfoVo.getUserId()!=null){
            //用户登录
            //如果临时购物车的数据还没有进行合并

            List<CartItem> tempCartItems = getCartItems(tempKey);
            if(tempCartItems!=null&&tempCartItems.size()>0){
                for (CartItem item : tempCartItems) {
                    //todo 可优化部分
                    addToCart(item.getSkuId(), item.getCount());
                }
                //清空临时购物车
                clearCart(tempKey);
            }
            //获取登录后的购物车数据,此时已经包含了临时购物车的数据
            List<CartItem> cartItems = getCartItems(loginKey);
            cart.setItems(cartItems);
        }else{
            List<CartItem> cartItems = getCartItems(tempKey);
            cart.setItems(cartItems);
        }
        return cart;
    }
 //清空购物车
    @Override
    public void clearCart(String key) {
        redisTemplate.delete(key);
    }
//获取购物车的购物项根据key值,这里不能使用之前的获取ops方法,因为在用户登录的时候,用以前的方法只能获得用户登录购物车,无法获得临时购物车
    private List<CartItem> getCartItems(String key) {
        BoundHashOperations<String, Object, Object> hashOps = redisTemplate.boundHashOps(key);
        List<Object> values = hashOps.values();
        if(values!=null&&values.size()>0){
            List<CartItem> cartItems = values.stream().map(o -> {
                CartItem cartItem = JSON.parseObject(o.toString(), CartItem.class);
                return cartItem;
            }).collect(Collectors.toList());
            return cartItems;
        }
        return null;
    }

勾选和改变购物项数量,后端操作几乎一样

这一块其实应该交给前端来做,这里只做了勾选

购物车controller

    @GetMapping("/checkItem")
    public String checkItem(@RequestParam("skuId") Long skuId,@RequestParam("check") Integer check){
        cartService.checkItem(skuId,check);
        return "redirect:http://cart.gulimall.com//cart.html";//重定向到购物车列表页
    }

方法

 //勾选购物项
    @Override
    public void checkItem(Long skuId, Integer check) {
        CartItem cartItem = getCartItem(skuId);
        cartItem.setCheck(check==1);
        String jsonString = JSON.toJSONString(cartItem);
        BoundHashOperations<String, Object, Object> cartOps = getCartOps();
        cartOps.put(skuId.toString(),jsonString);

    }

删除购物项

  @GetMapping("/deleteItem")
    public String deleteItem(@RequestParam("skuId") Long skuId){
        cartService.deleteItem(skuId);
        return "redirect:http://cart.gulimall.com//cart.html";//重定向到购物车列表页
    }
 @Override
    public void deleteItem(Long skuId) {
        BoundHashOperations<String, Object, Object> cartOps = getCartOps();
        cartOps.delete(skuId.toString());
    }

 redis数据库持久化

 

posted @ 2021-08-23 21:08  一拳超人的逆袭  阅读(69)  评论(0编辑  收藏  举报