zoukankan      html  css  js  c++  java
  • Day17_购物车

    学于黑马和传智播客联合做的教学项目 感谢
    黑马官网
    传智播客官网
    微信搜索"艺术行者",关注并回复关键词"乐优商城"获取视频和教程资料!
    b站在线视频
    注:实验并没有完全成功,登陆后,点击加入购物车,不知道为什么不跳转,找不到bug,其次浏览器中的TOKEN名称为TOKNEN,应该是TOKEN,但是代码中都改为TOKEN,浏览器中还是TOKNEN,故将代码中的TOKEN改为TOKNEN。

    0.学习目标

    1.实现未登录状态的购物车

    2.实现登陆状态下的购物车

    1.搭建购物车服务

    1.1.创建module

    1.2.pom依赖

    <?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <parent>
            <artifactId>leyou</artifactId>
            <groupId>com.leyou.parent</groupId>
            <version>1.0.0-SNAPSHOT</version>
        </parent>
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>com.leyou.service</groupId>
        <artifactId>ly-cart</artifactId>
        <version>1.0.0-SNAPSHOT</version>
    
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-openfeign</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis</artifactId>
            </dependency>
        </dependencies>
    </project>
    

    1.3.配置文件

    server:
      port: 8088
    spring:
      application:
        name: cart-service
      redis:
        host: 192.168.56.101
    eureka:
      client:
        service-url:
          defaultZone: http://127.0.0.1:10086/eureka
        registry-fetch-interval-seconds: 10
      instance:
        lease-renewal-interval-in-seconds: 5
        lease-expiration-duration-in-seconds: 15
    

    1.4.启动类

    @SpringBootApplication
    @EnableDiscoveryClient
    @EnableFeignClients
    public class LeyouCartApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(LeyouCartApplication.class, args);
        }
    }
    

    2.购物车功能分析

    2.1.需求

    需求描述:

    • 用户可以在登录状态下将商品添加到购物车
      • 放入数据库
      • mongodb
      • 放入redis(采用)
    • 用户可以在未登录状态下将商品添加到购物车
      • 放入localstorage
      • cookie
      • webSQL
    • 用户可以使用购物车一起结算下单
    • 用户可以查询自己的购物车
    • 用户可以在购物车中修改购买商品的数量。
    • 用户可以在购物车中删除商品。
    • 提示购物车商品价格变化

    2.2.流程图

    这幅图主要描述了两个功能:新增商品到购物车、查询购物车。

    新增商品:

    • 判断是否登录
      • 是:则添加商品到后台Redis中
      • 否:则添加商品到本地的Localstorage

    无论哪种新增,完成后都需要查询购物车列表:

    • 判断是否登录
      • 否:直接查询localstorage中数据并展示
      • 是:已登录,则需要先看本地是否有数据,
        • 有:需要提交到后台添加到redis,合并数据,而后查询
        • 否:直接去后台查询redis,而后返回

    3.未登录购物车

    3.1.准备

    3.1.1购物车的数据结构

    首先分析一下未登录购物车的数据结构。

    我们看下页面展示需要什么数据:

    因此每一个购物车信息,都是一个对象,包含:

    {
        skuId:2131241,
        title:"小米6",
        image:"",
        price:190000,
        num:1,
        ownSpec:"{"机身颜色":"陶瓷黑尊享版","内存":"6GB","机身存储":"128GB"}"
    }
    

    另外,购物车中不止一条数据,因此最终会是对象的数组。即:

    [
        {...},{...},{...}
    ]
    

    3.1.2.web本地存储

    知道了数据结构,下一个问题,就是如何保存购物车数据。前面我们分析过,可以使用Localstorage来实现。Localstorage是web本地存储的一种,那么,什么是web本地存储呢?

    什么是web本地存储?

    web本地存储主要有两种方式:

    • LocalStorage:localStorage 方法存储的数据没有时间限制。第二天、第二周或下一年之后,数据依然可用。
    • SessionStorage:sessionStorage 方法针对一个 session 进行数据存储。当用户关闭浏览器窗口后,数据会被删除。

    LocalStorage的用法

    语法非常简单:

    localStorage.setItem("key","value"); // 存储数据
    localStorage.getItem("key"); // 获取数据
    localStorage.removeItem("key"); // 删除数据
    

    注意:localStorage和SessionStorage都只能保存字符串

    不过,在我们的common.js中,已经对localStorage进行了简单的封装:

    示例:

    3.1.3.获取num

    添加购物车需要知道购物的数量,所以我们需要获取数量大小。我们在Vue中定义num(web模块下的Item.html),保存数量:

    然后将num与页面的input框绑定,同时给+-的按钮绑定事件:

    编写方法:

    3.2.添加购物车

    3.2.1.点击事件

    我们看下商品详情页:

    现在点击加入购物车会跳转到购物车成功页面。

    不过我们不这么做,我们绑定点击事件,然后实现添加购物车功能。

    把href=“#”删掉,不然就跳转到#

    addCart方法中判断用户的登录状态:

    addCart(){
       ly.verifyUser().then(res=>{
            // 已登录发送信息到后台,保存到redis中
    
        }).catch(()=>{
           // 未登录保存在浏览器本地的localStorage中
       })
    }
    

    3.2.2.获取数量,添加购物车

    addCart(){
        ly.verifyUser().then(res=>{
            // 已登录发送信息到后台,保存到redis中
    
        }).catch(()=>{
            // 未登录保存在浏览器本地的localStorage中
            // 1、查询本地购物车
            let carts = ly.store.get("carts") || [];
            let cart = carts.find(c=>c.skuId===this.sku.id);
            // 2、判断是否存在
            if (cart) {
                // 3、存在更新数量
                cart.num += this.num;
            } else {
                // 4、不存在,新增
                cart = {
                    skuId: this.sku.id,
                    title: this.sku.title,
                    price: this.sku.price,
                    image: this.sku.images,
                    num: this.num,
                    ownSpec: this.ownSpec
                }
                carts.push(cart);
            }
            // 把carts写回localstorage
            ly.store.set("carts", carts);
            // 跳转
            window.location.href = "http://www.leyou.com/cart.html";
        });
    }
    

    结果:

    添加完成后,页面会跳转到购物车结算页面:cart.html

    3.3.查询购物车

    3.3.1.校验用户登录

    因为会多次校验用户登录状态,因此我们封装一个校验的方法:

    在common.js中:

    在页面item.html中使用该方法:

    3.3.2.查询购物车

    页面加载时,就应该去查询购物车。

    var cartVm = new Vue({
        el: "#cartApp",
        data: {
            ly,
            carts: [],// 购物车数据
        },
        created() {
            this.loadCarts();
        },
        methods: {
            loadCarts() {
                // 先判断登录状态
                ly.verifyUser().then(() => {
                        // 已登录
    
                    }).catch(() => {
                        // 未登录
                        this.carts = ly.store.get("carts") || [];
                        this.selected = this.carts;
                    })
               }
        },
        components: {
            shortcut: () => import("/js/pages/shortcut.js")
        }
    })
    

    刷新页面,查看控制台Vue实例:

    3.5.2.渲染到页面

    接下来,我们在页面中展示carts的数据:

    要注意,价格的展示需要进行格式化,这里使用的是我们在common.js中定义的formatPrice方法

    效果:

    并没有获取到ownSpec,卡了很长时间,所以做得时候应该把这块注释掉

    3.6.修改数量

    我们给页面的 +-绑定点击事件,修改num 的值:

    两个事件:

        increment(c) {
            c.num++;
            ly.verifyUser().then(() => {
                // TODO 已登录,向后台发起请求
            }).catch(() => {
                // 未登录,直接操作本地数据
                ly.store.set("carts", this.carts);
            })
        },
        decrement(c) {
            if (c.num <= 1) {
                return;
            }
            c.num--;
            ly.verifyUser().then(() => {
                // TODO 已登录,向后台发起请求
            }).catch(() => {
                // 未登录,直接操作本地数据
                ly.store.set("carts", this.carts);
            })
        }
    

    3.7.删除商品

    给删除按钮绑定事件:

    点击事件中删除商品:

    deleteCart(i){
        ly.verifyUser().then(res=>{
            // TODO,已登录购物车
        }).catch(()=>{
            // 未登录购物车
            this.carts.splice(i, 1);
            ly.store.set("carts", this.carts);
        })
    }
    

    3.8.选中商品

    在页面中,每个购物车商品左侧,都有一个复选框,用户可以选择部分商品进行下单,而不一定是全部:

    我们定义一个变量,记录所有被选中的商品:

    3.8.1.选中一个

    我们给商品前面的复选框与selected绑定,并且指定其值为当前购物车商品:

    3.8.2.初始化全选

    我们在加载完成购物车查询后,初始化全选:

    3.8.3.总价格

    然后编写一个计算属性,计算出选中商品总价格:

    computed: {
        totalPrice() {
            return ly.formatPrice(this.selected.reduce((c1, c2) => c1 + c2.num * c2.price, 0));
        }
    }
    

    在页面中展示总价格:

    效果:

    4.已登录购物车

    接下来,我们完成已登录购物车。

    在刚才的未登录购物车编写时,我们已经预留好了编写代码的位置,逻辑也基本一致。

    4.1.添加登录校验

    购物车系统只负责登录状态的购物车处理,因此需要添加登录校验,我们通过JWT鉴权即可实现。

    4.1.1.引入JWT相关依赖

    我们引入之前写的鉴权工具:leyou-auth-common

    <dependency>
        <groupId>com.leyou.auth</groupId>
        <artifactId>leyou-auth-common</artifactId>
        <version>1.0.0-SNAPSHOT</version>
    </dependency>
    

    4.1.2.配置公钥

    leyou:
      jwt:
        pubKeyPath: C:/tmp/rsa/rsa.pub # 公钥地址
        cookieName: LY_TOKEN # cookie的名称
    

    4.1.3.加载公钥

    代码:

    @ConfigurationProperties(prefix = "leyou.jwt")
    public class JwtProperties {
    
        private String pubKeyPath;// 公钥
    
        private PublicKey publicKey; // 公钥
    
        private String cookieName;
    
        private static final Logger logger = LoggerFactory.getLogger(JwtProperties.class);
    
        @PostConstruct
        public void init(){
            try {
                // 获取公钥和私钥
                this.publicKey = RsaUtils.getPublicKey(pubKeyPath);
            } catch (Exception e) {
                logger.error("初始化公钥失败!", e);
                throw new RuntimeException();
            }
        }
    
       	// getter/setter......
    }
    

    4.1.4.编写拦截器

    因为很多接口都需要进行登录,我们直接编写SpringMVC拦截器,进行统一登录校验。同时,我们还要把解析得到的用户信息保存起来,以便后续的接口可以使用。

    代码:

    @Component
    @EnableConfigurationProperties(JwtProperties.class)
    public class LoginInterceptor extends HandlerInterceptorAdapter {
    
      	@Autowired
        private JwtProperties jwtProperties;
    
        // 定义一个线程域,存放登录用户
        private static final ThreadLocal<UserInfo> tl = new ThreadLocal<>();
    
        public LoginInterceptor(JwtProperties jwtProperties) {
            this.jwtProperties = jwtProperties;
        }
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            // 查询token
            String token = CookieUtils.getCookieValue(request, "LY_TOKEN");
            if (StringUtils.isBlank(token)) {
                // 未登录,返回401
                response.setStatus(HttpStatus.UNAUTHORIZED.value());
                return false;
            }
            // 有token,查询用户信息
            try {
                // 解析成功,证明已经登录
                UserInfo user = JwtUtils.getInfoFromToken(token, jwtProperties.getPublicKey());
                // 放入线程域
                tl.set(user);
                return true;
            } catch (Exception e){
                // 抛出异常,证明未登录,返回401
                response.setStatus(HttpStatus.UNAUTHORIZED.value());
                return false;
            }
    
        }
    
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
            tl.remove();
        }
    
        public static UserInfo getLoginUser() {
            return tl.get();
        }
    }
    

    注意:

    • 这里我们使用了ThreadLocal来存储查询到的用户信息,线程内共享,因此请求到达Controller后可以共享User
    • 并且对外提供了静态的方法:getLoginUser()来获取User信息

    4.1.5.配置拦截器

    配置SpringMVC,使过滤器生效:

    @Configuration
    @EnableConfigurationProperties(JwtProperties.class)
    public class MvcConfig implements WebMvcConfigurer {
    
        @Autowired
        private JwtProperties jwtProperties;
    
        @Bean
        public LoginInterceptor loginInterceptor() {
            return new LoginInterceptor(jwtProperties);
        }
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(loginInterceptor()).addPathPatterns("/**");
        }
    }
    

    4.2.后台购物车设计

    当用户登录时,我们需要把购物车数据保存到后台,可以选择保存在数据库。但是购物车是一个读写频率很高的数据。因此我们这里选择读写效率比较高的Redis作为购物车存储。

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

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

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

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

    实体类:

    public class Cart {
        private Long userId;// 用户id
        private Long skuId;// 商品id
        private String title;// 标题
        private String image;// 图片
        private Long price;// 加入购物车时的价格
        private Integer num;// 购买数量
        private String ownSpec;// 商品规格参数
    
        //getter/settet....
    }
    

    4.3.添加商品到购物车

    4.3.1.页面发起请求

    已登录情况下,向后台添加购物车:

    ly.http.post("/cart", {skuId: this.sku.id, num: this.num}).then(res=>{
        window.location = "http://www.leyou.com/cart.html";
    })
    

    这里发起的是Json请求。那么我们后台也要以json接收。

    4.3.2.编写controller

    先分析一下:

    • 请求方式:新增,肯定是Post
    • 请求路径:/cart ,这个其实是Zuul路由的路径,我们可以不管
    • 请求参数:Json对象,包含skuId和num属性
    • 返回结果:无
    @Controller
    public class CartController {
    
        @Autowired
        private CartService cartService;
    
        /**
         * 添加购物车
         *
         * @return
         */
        @PostMapping
        public ResponseEntity<Void> addCart(@RequestBody Cart cart) {
            this.cartService.addCart(cart);
            return ResponseEntity.ok().build();
        }
    }
    

    在leyou-gateway中添加路由配置:

    4.3.3.CartService

    这里我们不访问数据库,而是直接操作Redis。基本思路:

    • 先查询之前的购物车数据
    • 判断要添加的商品是否存在
      • 存在:则直接修改数量后写回Redis
      • 不存在:新建一条数据,然后写入Redis

    代码:

    @Service
    public class CartService {
    
        @Autowired
        private StringRedisTemplate redisTemplate;
    
        @Autowired
        private GoodsClient goodsClient;
    
        static final String KEY_PREFIX = "leyou:cart:uid:";
    
        static final Logger logger = LoggerFactory.getLogger(CartService.class);
    
        public void addCart(Cart cart) {
            // 获取登录用户
            UserInfo user = LoginInterceptor.getLoginUser();
            // Redis的key
            String key = KEY_PREFIX + user.getId();
            // 获取hash操作对象
            BoundHashOperations<String, Object, Object> hashOps = this.redisTemplate.boundHashOps(key);
            // 查询是否存在
            Long skuId = cart.getSkuId();
            Integer num = cart.getNum();
            Boolean boo = hashOps.hasKey(skuId.toString());
            if (boo) {
                // 存在,获取购物车数据
                String json = hashOps.get(skuId.toString()).toString();
                cart = JsonUtils.parse(json, Cart.class);
                // 修改购物车数量
                cart.setNum(cart.getNum() + num);
            } else {
                // 不存在,新增购物车数据
                cart.setUserId(user.getId());
                // 其它商品信息,需要查询商品服务
                Sku sku = this.goodsClient.querySkuById(skuId);
                cart.setImage(StringUtils.isBlank(sku.getImages()) ? "" : StringUtils.split(sku.getImages(), ",")[0]);
                cart.setPrice(sku.getPrice());
                cart.setTitle(sku.getTitle());
                cart.setOwnSpec(sku.getOwnSpec());
            }
            // 将购物车数据写入redis
            hashOps.put(cart.getSkuId().toString(), JsonUtils.serialize(cart));
        }
    }
    

    需要引入leyou-item-interface依赖:

    <dependency>
        <groupId>com.leyou.item</groupId>
        <artifactId>leyou-item-interface</artifactId>
        <version>1.0.0-SNAPSHOT</version>
    </dependency>
    

    4.3.4.GoodClient

    参照搜索工程,添加GoodClient,提供根据id查询sku的接口:

    @FeignClient("item-service")
    public interface GoodsClient extends GoodsApi {
    }
    

    在leyou-item-service中的GoodsController添加方法:

    @GetMapping("sku/{id}")
    public ResponseEntity<Sku> querySkuById(@PathVariable("id")Long id){
        Sku sku = this.goodsService.querySkuById(id);
        if (sku == null){
            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
        }
        return ResponseEntity.ok(sku);
    }
    

    在leyou-item-service中的GoodsService添加方法:

    public Sku querySkuById(Long id) {
        return this.skuMapper.selectByPrimaryKey(id);
    }
    

    在leyou-item-interface中的GoodsApi添加:

    @GetMapping("sku/{id}")
    public Sku querySkuById(@PathVariable("id")Long id);
    

    4.3.5.结果

    4.4.查询购物车

    4.4.1.页面发起请求

    购物车页面:cart.html

    4.4.2.后台实现

    Controller

    /**
     * 查询购物车列表
     *
     * @return
     */
    @GetMapping
    public ResponseEntity<List<Cart>> queryCartList() {
        List<Cart> carts = this.cartService.queryCartList();
        if (carts == null) {
            return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
        }
        return ResponseEntity.ok(carts);
    }
    

    Service

    public List<Cart> queryCartList() {
        // 获取登录用户
        UserInfo user = LoginInterceptor.getLoginUser();
    
        // 判断是否存在购物车
        String key = KEY_PREFIX + user.getId();
        if(!this.redisTemplate.hasKey(key)){
            // 不存在,直接返回
            return null;
        }
        BoundHashOperations<String, Object, Object> hashOps = this.redisTemplate.boundHashOps(key);
        List<Object> carts = hashOps.values();
        // 判断是否有数据
        if(CollectionUtils.isEmpty(carts)){
            return null;
        }
        // 查询购物车数据
        return carts.stream().map(o -> JsonUtils.parse(o.toString(), Cart.class)).collect(Collectors.toList());
    }
    

    4.4.3.测试

    4.5.修改商品数量

    4.5.1.页面发起请求

    4.5.2.后台实现

    Controller

    @PutMapping
    public ResponseEntity<Void> updateNum(@RequestBody Cart cart){
        this.cartService.updateCarts(cart);
        return ResponseEntity.noContent().build();
    }
    

    Service

    public void updateCarts(Cart cart) {
        // 获取登陆信息
        UserInfo userInfo = LoginInterceptor.getLoginUser();
        String key = KEY_PREFIX + userInfo.getId();
        // 获取hash操作对象
        BoundHashOperations<String, Object, Object> hashOperations = this.redisTemplate.boundHashOps(key);
        // 获取购物车信息
        String cartJson = hashOperations.get(cart.getSkuId().toString()).toString();
        Cart cart1 = JsonUtils.parse(cartJson, Cart.class);
        // 更新数量
        cart1.setNum(cart.getNum());
        // 写入购物车
        hashOperations.put(cart.getSkuId().toString(), JsonUtils.serialize(cart1));
    }
    

    4.6.删除购物车商品

    4.6.1.页面发起请求

    注意:后台成功响应后,要把页面的购物车中数据也删除

    4.6.2.后台实现

    Controller

    @DeleteMapping("{skuId}")
    public ResponseEntity<Void> deleteCart(@PathVariable("skuId") String skuId) {
        this.cartService.deleteCart(skuId);
        return ResponseEntity.ok().build();
    }
    

    Service

    public void deleteCart(String skuId) {
        // 获取登录用户
        UserInfo user = LoginInterceptor.getLoginUser();
        String key = KEY_PREFIX + user.getId();
        BoundHashOperations<String, Object, Object> hashOps = this.redisTemplate.boundHashOps(key);
        hashOps.delete(skuId);
    }
    

    5.登录后购物车合并

    当跳转到购物车页面,查询购物车列表前,需要判断用户登录状态,

    • 如果登录:
      • 首先检查用户的LocalStorage中是否有购物车信息,
      • 如果有,则提交到后台保存,
      • 清空LocalStorage
    • 如果未登录,直接查询即可
  • 相关阅读:
    Redis 发布与订阅
    Redis 数据持久化的理解
    自定义shell脚本快速搭建LNMP环境
    PHP环境配置与优化(Ubuntu16.04/PHP7)
    记录在ios系统上,自研app,灰度环境遇到的一个vue页面dom节点已渲染,但是显示部分空白的情况
    记录在苹果6p/6sp,10版本上,app内交互token等用户信息丢失的问题
    记录在苹果X手机上运行遇到的代码Dom被阻塞不更新的一个坑
    巧用flex(一)
    谷歌浏览器调试手机app内置网页
    与app交互因异步造成的坑记录
  • 原文地址:https://www.cnblogs.com/artwalker/p/13261807.html
Copyright © 2011-2022 走看看