商城业务---购物车

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 这篇文章讨论了商城业务中购物车功能的实现,包括用户登录状态下和未登录状态下购物车的处理方式。文章详细描述了购物车的基本需求、流程分析、判断用户登录状态的拦截器实现、临时购物车和登录购物车的数据操作,以及添加商品到购物车的过程。此外,还提到了可能遇到的问题和解决方案,以及在Redis中存储商品信息的不同数据结构选择。最后,文章展示了未登录和登录状态下购物车的使用效果和Redis中数据的存储情况。

前言

只做一个简单的思路提示,具体的业务逻辑实现,就是写接口,没啥好说的。

1、需求描述

  • 1、用户可以在登录状态下将商品添加到购物车【用户购物车/在线购物车】
  • 2、用户可以在未登录状态下将商品添加到购物车【游客购物车/离线购物车/临时购物车】
  • 3、用户可以使用购物车一起结算下单
  • 4、给购物车添加商品
  • 5、用户可以查询自己的购物车
  • 6、用户可以在购物车中修改购买商品的数量。
  • 7、用户可以在购物车中删除商品。
  • 8、选中不选中商品
  • 9、在购物车中展示商品优惠信息
  • 10、提示购物车商品价格变化

2、流程分析

2.1 新增商品:判断是否登录

是:则添加商品到后台 Redis 中,把 user 的唯一标识符作为 key。
否:则添加商品到后台 redis 中,使用随机生成的 user-key 作为 key。

2.2 查询购物车列表:判断是否登录

  • 否:直接根据 user-key 查询 redis 中数据并展示
  • 是:已登录,则需要先根据 user-key 查询 redis 是否有数据。
    • 有:需要提交到后台添加到 redis,合并数据,而后查询。
    • 否:直接去后台查询 redis,而后返回。

2.3 判断是否登录

这里使用拦截器进行预先处理,如果用户没有登录,就随机分配一个值。

    /***
     * 目标方法执行之前
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        UserInfoTo userInfoTo = new UserInfoTo();

        HttpSession session = request.getSession();
        //获得当前登录用户的信息
        MemberResponseVo memberResponseVo = (MemberResponseVo) session.getAttribute(LOGIN_USER);

        if (memberResponseVo != null) {
            //用户登录了
            userInfoTo.setUserId(memberResponseVo.getId());
        }

        Cookie[] cookies = request.getCookies();
        if (cookies != null && cookies.length > 0) {
            for (Cookie cookie : cookies) {
                //user-key
                String name = cookie.getName();
                if (name.equals(TEMP_USER_COOKIE_NAME)) {
                    userInfoTo.setUserKey(cookie.getValue());
                    //标记为已是临时用户
                    userInfoTo.setTempUser(true);
                }
            }
        }

        //如果没有临时用户一定分配一个临时用户
        if (StringUtils.isEmpty(userInfoTo.getUserKey())) {
            String uuid = UUID.randomUUID().toString();
            userInfoTo.setUserKey(uuid);
        }

        //目标方法执行之前
        toThreadLocal.set(userInfoTo);
        return true;
    }

3、临时购物车

    /**
     * 获取到我们要操作的购物车
     * @return
     */
    private BoundHashOperations<String, Object, Object> getCartOps() {
        //先得到当前用户信息
        UserInfoTo userInfoTo = CartInterceptor.toThreadLocal.get();

        String cartKey = "";
        if (userInfoTo.getUserId() != null) {
            //gulimall:cart:1
            cartKey = CART_PREFIX + userInfoTo.getUserId();
        } else {
            cartKey = CART_PREFIX + userInfoTo.getUserKey();
        }

        //绑定指定的key操作Redis
        BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(cartKey);

        return operations;
    }

4、登录购物车

    /**
     * 获取用户登录或者未登录购物车里所有的数据
     * @return
     * @throws ExecutionException
     * @throws InterruptedException
     */
    @Override
    public CartVo getCart() throws ExecutionException, InterruptedException {

        CartVo cartVo = new CartVo();
        UserInfoTo userInfoTo = CartInterceptor.toThreadLocal.get();
        if (userInfoTo.getUserId() != null) {
            //1、登录
            String cartKey = CART_PREFIX + userInfoTo.getUserId();
            //临时购物车的键
            String temptCartKey = CART_PREFIX + userInfoTo.getUserKey();

            //2、如果临时购物车的数据还未进行合并
            List<CartItemVo> tempCartItems = getCartItems(temptCartKey);
            if (tempCartItems != null) {
                //临时购物车有数据需要进行合并操作
                for (CartItemVo item : tempCartItems) {
                    addToCart(item.getSkuId(),item.getCount());
                }
                //清除临时购物车的数据
                clearCartInfo(temptCartKey);
            }

            //3、获取登录后的购物车数据【包含合并过来的临时购物车的数据和登录后购物车的数据】
            List<CartItemVo> cartItems = getCartItems(cartKey);
            cartVo.setItems(cartItems);

        } else {
            //没登录
            String cartKey = CART_PREFIX + userInfoTo.getUserKey();
            //获取临时购物车里面的所有购物项
            List<CartItemVo> cartItems = getCartItems(cartKey);
            cartVo.setItems(cartItems);
        }

        return cartVo;
    }

5、添加商品到购物车

这里直接将数据存储到redis中,这里牵涉到远程服务调用。

    @Override
    public CartItemVo addToCart(Long skuId, Integer num) throws ExecutionException, InterruptedException {

        //拿到要操作的购物车信息
        BoundHashOperations<String, Object, Object> cartOps = getCartOps();

        //判断Redis是否有该商品的信息
        String productRedisValue = (String) cartOps.get(skuId.toString());
        //如果没有就添加数据
        if (StringUtils.isEmpty(productRedisValue)) {

            //2、添加新的商品到购物车(redis)
            CartItemVo cartItemVo = new CartItemVo();
            //开启第一个异步任务
            CompletableFuture<Void> getSkuInfoFuture = CompletableFuture.runAsync(() -> {
                //1、远程查询当前要添加商品的信息
                R productSkuInfo = productFeignService.getInfo(skuId);
                SkuInfoVo skuInfo = productSkuInfo.getData("skuInfo", new TypeReference<SkuInfoVo>() {});
                //数据赋值操作
                cartItemVo.setSkuId(skuInfo.getSkuId());
                cartItemVo.setTitle(skuInfo.getSkuTitle());
                cartItemVo.setImage(skuInfo.getSkuDefaultImg());
                cartItemVo.setPrice(skuInfo.getPrice());
                cartItemVo.setCount(num);
            }, executor);

            //开启第二个异步任务
            CompletableFuture<Void> getSkuAttrValuesFuture = CompletableFuture.runAsync(() -> {
                //2、远程查询skuAttrValues组合信息
                    List<String> skuSaleAttrValues = productFeignService.getSkuSaleAttrValues(skuId);
                cartItemVo.setSkuAttrValues(skuSaleAttrValues);
            }, executor);

            //等待所有的异步任务全部完成
            CompletableFuture.allOf(getSkuInfoFuture, getSkuAttrValuesFuture).get();

            String cartItemJson = JSON.toJSONString(cartItemVo);
            cartOps.put(skuId.toString(), cartItemJson);

            return cartItemVo;
        } else {
            //购物车有此商品,修改数量即可
            CartItemVo cartItemVo = JSON.parseObject(productRedisValue, CartItemVo.class);
            cartItemVo.setCount(cartItemVo.getCount() + num);
            //修改redis的数据
            String cartItemJson = JSON.toJSONString(cartItemVo);
            cartOps.put(skuId.toString(),cartItemJson);

            return cartItemVo;
        }
    }

6、提示

6.1 问题1

如果调用远程服务失败,页面也会报错。需要手动编写sql语句。在商品加入购物车前,判断该商品是否已经存在购物车。

6.3 问题2

在使用拦截器的时候,由于版本不同。写法有所不同。

        WebCallbackManager.setUrlBlockHandler(new UrlBlockHandler() {
            @Override
            public void blocked(HttpServletRequest request, HttpServletResponse response, BlockException ex) throws IOException {
                R error = R.error(BizCodeEnume.TO_MANY_REQUEST.getCode(), BizCodeEnume.TO_MANY_REQUEST.getMsg());
                response.setCharacterEncoding("UTF-8");
                response.setContentType("application/json");
                response.getWriter().write(JSON.toJSONString(error));

            }
        });

如果想要继续使用这个方式,需要在pom文件中加入新的依赖

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
            <version>2.1.0.RELEASE</version>
        </dependency>

6.4 问题三

关于如何将商品的信息以何种方式存储到redis中

Redis 有 5 种不同数据结构,这里选择哪一种比较合适呢?Map

  • 首先不同用户应该有独立的购物车,因此购物车应该以用户的作为 key 来存储,Value 是用户的所有购物车信息。这样看来基本的k-v结构就可以了。
  • 但是,我们对购物车中的商品进行增、删、改操作,基本都需要根据商品 id 进行判断,为了方便后期处理,我们的购物车也应该是k-v结构,key 是商品 id,value 才是这个商品的购物车信息

综上所述,我们的购物车结构是一个双层 Map:Map<String,Map<String,String>>

  • 第一层 Map,Key 是用户 id
  • 第二层 Map,Key 是购物车中商品 id,值是购物项数据

7、实现的效果

7.1 未登录状态

7.1.1 商品加入购物车

在这里插入图片描述
在这里插入图片描述

7.1.2 查看购物车

在这里插入图片描述

7.1.3 查看redis中存储数据

在这里插入图片描述

7.2 登录状态

7.1 查看购物车

在这里插入图片描述
在这里插入图片描述

7.2 查看reids数据

在这里插入图片描述

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
相关文章
|
存储 缓存 算法
淘宝购物车扩容与性能优化(下)
淘宝购物车扩容与性能优化(下)
337 3
|
SQL Java 数据库连接
Connection(数据库连接对象)
Connection(数据库连接对象)
294 0
|
消息中间件 SQL 存储
超详细的RabbitMQ入门,看这篇就够了!
RabbitMQ入门,看这篇就够了
216226 68
|
Shell 开发工具 数据安全/隐私保护
idea上传项目到gitee(码云)超详细
idea上传项目到gitee(码云)超详细
idea上传项目到gitee(码云)超详细
|
5月前
|
安全 Ubuntu 网络安全
本地服务器 Odoo 安装指南,并实现公网访问
本指南详细介绍基于 Odoo 的企业应用部署流程。首先通过 VMware 安装虚拟机并配置 Ubuntu 系统,为运行环境打下基础;接着利用 Websoft9 平台简化中间件与 Odoo 服务的安装,实现快速部署与版本管理;然后借助 cpolar 实现内网穿透并完成域名解析,确保公网访问畅通;最后进行功能测试与性能优化,提供运维建议以保障系统稳定性和安全性。适合初学者及中小型企业参考实施。
623 4
|
消息中间件 NoSQL Kafka
订单超时取消的11种方式(非常详细清楚)
订单超时取消的11种方式(非常详细清楚)
6656 4
订单超时取消的11种方式(非常详细清楚)
|
10月前
|
消息中间件 中间件 Kafka
分布式事务最全详解 ,看这篇就够了!
本文详解分布式事务的一致性及实战解决方案,包括CAP理论、BASE理论及2PC、TCC、消息队列等常见方案,助你深入理解分布式系统的核心技术。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
分布式事务最全详解 ,看这篇就够了!
|
SQL 缓存 数据库
淘宝购物车扩容与性能优化(上)
淘宝购物车扩容与性能优化(上)
562 2
|
存储 监控 数据库
什么是聚集索引和非聚集索引?
【8月更文挑战第3天】
6833 6
|
12月前
|
机器学习/深度学习 测试技术 数据处理
KAN专家混合模型在高性能时间序列预测中的应用:RMoK模型架构探析与Python代码实验
Kolmogorov-Arnold网络(KAN)作为一种多层感知器(MLP)的替代方案,为深度学习领域带来新可能。尽管初期测试显示KAN在时间序列预测中的表现不佳,近期提出的可逆KAN混合模型(RMoK)显著提升了其性能。RMoK结合了Wav-KAN、JacobiKAN和TaylorKAN等多种专家层,通过门控网络动态选择最适合的专家层,从而灵活应对各种时间序列模式。实验结果显示,RMoK在多个数据集上表现出色,尤其是在长期预测任务中。未来研究将进一步探索RMoK在不同领域的应用潜力及其与其他先进技术的结合。
398 4