乐优商城笔记十一:购物车

购物车功能分析

需求

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

流程图

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

新增商品:

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

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

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

未登录购物车

准备工作

购物车的数据结构

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

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

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

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

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

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

web本地存储

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

什么是web本地存储?

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

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

语法非常简单:

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

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

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

示例:

1533739929733

获取num

ly-page项目中的item.html模板中做如下修改:

  • 添加购物车需要知道购物的数量,所以我们需要获取数量大小。我们在Vue中定义num,保存数量

  • 编写方法,用于数量的增加和减少

    methods: {
    	incrment() {
    		this.num++
    	},
    	decrment() {
    		if (this.num > 1) {
    			this.num--
    		}
    	}
    }
    
  • 将num与页面的input框绑定,同时给+-的按钮绑定事件

添加商品到购物车

ly-page项目中的item.html模板中做如下修改:

  • 加入购物车按钮添加单击事件

  • 编写addCart方法

    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.sku.ownSpec
                }
                carts.push(cart);
            }
            // 把carts写回localstorage
            ly.store.set("carts", carts);
            // 跳转
            window.location.href = "http://www.leyou.com/cart.html";
        });
    }
    
  • common.js中加入verifyUser()方法

  • 测试

加入购物车成功。

查询购物车

页面加载获取购物车

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

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

渲染购物车数据

在页面中展示carts的数据

修改数量

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

  • 编写incrmentdecrment方法

    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);
        })
    }
    

删除购物车项

  • 删除按钮绑定单击事件

  • 编写deleteCart方法

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

选中商品

选中单个

  • 在Vue中定义变量,记录被选中的购物车项

  • selected与页面的选择框绑定,值为当前购物车项

初始化全部选中

  • 修改loadCarts方法

计算所有商品总价

  • 在Vue加入计算方法

    computed: {
        totalPrice() {
            return ly.formatPrice(this.selected.reduce((c1, c2) => c1 + c2.num * c2.price, 0));
        }
    }
    
  • 页面调用计算总价

效果

登录购物车

完成已登录购物车。

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

搭建购物车微服务

创建module

  • GroupId:com.leyou.service
  • ArtifactId:ly-cart

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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>parent</artifactId>
        <groupId>com.leyou</groupId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.leyou.service</groupId>
    <artifactId>ly-cart</artifactId>

    <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>

启动类

package com.leyou;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

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

application.yml

server:
  port: 8008
spring:
  application:
    name: cart-service
  redis:
    host: 192.168.136.103
    port: 6379
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:9999/eureka

用户鉴权

引入依赖

<dependency>
    <groupId>com.leyou.auth</groupId>
    <artifactId>ly-auth-common</artifactId>
    <version>${leyou.latest.version}</version>
</dependency>
<dependency>
    <groupId>com.leyou.common</groupId>
    <artifactId>ly-common</artifactId>
    <version>${leyou.latest.version}</version>
</dependency>

配置公钥

application.yml中新增公钥配置

ly:
  jwt:
    pubKeyPath: c:\\key\\rsa.pub # 公钥地址
    cookieName: LY_TOKEN # cookie的名称

JwtProperties

ly-gateway中复制过来即可。

package com.leyou.cart.config;

import com.leyou.auth.utils.RsaUtils;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;

import javax.annotation.PostConstruct;
import java.security.PublicKey;

@Data
@Slf4j
@ConfigurationProperties(prefix = "ly.jwt")
public class JwtProperties {

    private String pubKeyPath;// 公钥

    private PublicKey publicKey; // 公钥

    private String cookieName;

    @PostConstruct
    public void init(){
        try {
            // 获取公钥和私钥
            this.publicKey = RsaUtils.getPublicKey(pubKeyPath);
        } catch (Exception e) {
            log.error("初始化公钥失败!", e);
            throw new RuntimeException();
        }
    }

}

编写拦截器

编写连接器对所有的请求进行统一鉴权。

package com.leyou.cart.interceptor;

import com.leyou.auth.entity.UserInfo;
import com.leyou.auth.utils.JwtUtils;
import com.leyou.cart.config.JwtProperties;
import com.leyou.common.util.CookieUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 用户鉴权拦截器
 */
@Slf4j
public class UserInterceptor extends HandlerInterceptorAdapter {

    private JwtProperties jwtProperties;

    // 将用户信息存放到当前线程中
    private static ThreadLocal<UserInfo> userInfoThreadLocal = new ThreadLocal<>();

    public UserInterceptor(JwtProperties jwtProperties) {
        this.jwtProperties = jwtProperties;
    }

    /**
     * 前置拦截, 从cookie中获取User信息
     *
     * @param request  http请求
     * @param response http响应
     * @param handler  响应的处理器, 可以自定义controller处理响应
     * @return boolean 获取到User信息返回true, 否则返回false
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        try {
            // 从cookie中获取token
            String token = CookieUtils.getCookieValue(request, jwtProperties.getCookieName());
            // 从token中解析User信息
            UserInfo userInfo = JwtUtils.getUserInfo(jwtProperties.getPublicKey(), token);
            if (userInfo.getId() == null) {
                log.warn("[购物车服务] 解析用户凭证失败");
                return false;
            }
            userInfoThreadLocal.set(userInfo);

            return true;
        } catch (Exception e) {
            log.error("[购物车服务] 用户权发生异常, ", e);
            return false;
        }
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        userInfoThreadLocal.remove();
    }

    /**
     * 获取用户信息
     *
     * @return UserInfo 用户信息
     */
    public static UserInfo getUserInfo() {
        return userInfoThreadLocal.get();
    }
}
  • 这里我们使用了ThreadLocal来存储查询到的用户信息,线程内共享,因此请求到达Controller后可以共享User。
  • 并且对外提供了静态的方法:getLoginUser()来获取User信息。

配置过滤器

package com.leyou.cart.config;

import com.leyou.cart.interceptor.UserInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@EnableConfigurationProperties(JwtProperties.class)
public class MvcConfiguration implements WebMvcConfigurer {

    @Autowired
    private JwtProperties jwtProperties;

    @Bean
    public UserInterceptor getUserInterceptor() {
        return new UserInterceptor(jwtProperties);
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 拦截所有请求
        registry.addInterceptor(getUserInterceptor()).addPathPatterns("/**");
    }
}

购物车设计

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

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

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

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

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

购物车实体类

package com.leyou.cart.pojo;

import lombok.Data;

/**
 * 购物车实体类
 */
@Data
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;// 商品规格参数
    
}

添加商品到购物车

item-service新增查询sku接口

  • controller

        /**
         * 查询sku信息
         *
         * @param skuId skuId
         * @return Sku 商品sku信息
         */
        @GetMapping("/sku/{skuId}")
        public ResponseEntity<Sku> querySkuById(@PathVariable("skuId") Long skuId) {
            return ResponseEntity.ok(goodsService.querySkuById(skuId));
        }
    
  • service

        /**
         * 查询sku信息
         *
         * @param skuId skuId
         * @return Sku 商品sku信息
         */
        public Sku querySkuById(Long skuId) {
            Sku sku = skuMapper.selectByPrimaryKey(skuId);
            if (sku == null || sku.getId() == null) {
                throw new LyException(LyExceptionEnum.SKU_NOT_FOUND);
            }
    
            return sku;
        }
    
  • goodsApi

    GoodsApi对外开放接口

        /**
         * 查询sku信息
         *
         * @param skuId skuId
         * @return Sku 商品sku信息
         */
        @GetMapping("goods/sku/{skuId}")
        Sku querySkuById(@PathVariable("skuId") Long skuId);
    

GoodsClient

package com.leyou.cart.client;

import com.leyou.api.GoodsApi;
import com.leyou.common.util.LeyouConstants;
import org.springframework.cloud.openfeign.FeignClient;

/**
 * GoodsClient
 */
@FeignClient(LeyouConstants.SERVICE_ITEM)
public interface GoodsClient extends GoodsApi {
    
}

需要引入ly-item-interface的依赖。

CartController

package com.leyou.cart.controller;

import com.leyou.cart.interceptor.UserInterceptor;
import com.leyou.cart.pojo.Cart;
import com.leyou.cart.service.CartService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class CartController {

    @Autowired
    private CartService cartService;

    @PostMapping
    public ResponseEntity<Void> addCart(@RequestBody Cart cart) {
        // 将userid放入cart
        cart.setUserId(UserInterceptor.getUserInfo().getId());
        cartService.saveCart(cart);
        return ResponseEntity.ok().build();
    }
}

CartService

package com.leyou.cart.service;

import com.leyou.cart.client.GoodsClient;
import com.leyou.cart.pojo.Cart;
import com.leyou.common.util.JsonUtils;
import com.leyou.pojo.Sku;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

@Slf4j
@Service
public class CartService {


    @Autowired
    private GoodsClient goodsClient;

    @Autowired
    private StringRedisTemplate redisTemplate;

    static final String KEY_PREFIX = "ly:cart:uid:";


    /**
     * 保存购物车
     * @param cart 购物车数据
     */
    public void saveCart(Cart cart) {
        String key = KEY_PREFIX + cart.getUserId();
        // 获取当前用户购物车信息
        BoundHashOperations<String, Object, Object> userCartData = redisTemplate.boundHashOps(key);

        // 拿出cart中的关键数据
        Integer num = cart.getNum();
        Long skuId = cart.getSkuId();

        // 判断是否存在购物车
        if (userCartData.hasKey(skuId.toString())) {
            // 存在该商品项,增加数量
            String json = userCartData.get(cart.getSkuId()).toString();
            cart = JsonUtils.parse(json, Cart.class);
            cart.setNum(cart.getNum() + num);
        } else {
            // 不存在该商品项,新增该商品到购物车
            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
        userCartData.put(cart.getSkuId().toString(), JsonUtils.serialize(cart));
    }
}

测试

前台登录过后,点击加入购物车,前往redis查询。

这里使用的是redis desktop manager,一款redis的GUI客户端。

查询购物车

页面请求

修改cart.html中的loadCarts方法。

CartController

新增方法:queryCartList

    /**
     * 查询当前用户购物车
     *
     * @return List 购物车商品列表
     */
    @GetMapping
    public ResponseEntity<List<Cart>> queryCartList() {
        List<Cart> carts = cartService.queryCartList();
        if (carts == null || carts.isEmpty()) {
            throw new LyException(LyExceptionEnum.CURRENT_USER_CART_NOT_EXIST);
        }
        return ResponseEntity.ok(carts);
    }

CartService

新增方法:queryCartList

    /**
     * 查询当前用户购物车
     *
     * @return List 购物车商品列表
     */
    public List<Cart> queryCartList() {
        // 获取用户信息
        UserInfo userInfo = UserInterceptor.getUserInfo();
        String key = KEY_PREFIX + userInfo.getId();
        // 查询该用户购物车
        if (!redisTemplate.hasKey(key)) {
            throw new LyException(LyExceptionEnum.CURRENT_USER_CART_NOT_EXIST);
        }
        BoundHashOperations<String, Object, Object> userCartData = redisTemplate.boundHashOps(key);
        List<Object> values = userCartData.values();
        if (CollectionUtils.isEmpty(values)) {
            throw new LyException(LyExceptionEnum.CURRENT_USER_CART_NOT_EXIST);
        }
        // 序列化并返回
        return values.stream().map(cart -> JsonUtils.parse(cart.toString(), Cart.class)).collect(Collectors.toList());
    }

测试

查询购物车成功。

修改数量

页面请求

incrementdecrement中新增逻辑:

CartController

    /**
     * 更新购物车中指定商品数量
     *
     * @param skuId 商品ID
     * @param num 数量
     */
    @PutMapping
    public ResponseEntity<Void> updateNum(@RequestParam("skuId") Long skuId,
                                          @RequestParam("num") Integer num) {
        cartService.updateNum(skuId, num);
        return ResponseEntity.ok().build();
    }

CartService

    /**
     * 更新购物车中指定商品数量
     *
     * @param skuId 商品ID
     * @param num 数量
     */
    public void updateNum(Long skuId, Integer num) {
        // 获取登录用户
        UserInfo user = UserInterceptor.getUserInfo();
        String key = KEY_PREFIX + user.getId();
        BoundHashOperations<String, Object, Object> hashOps = redisTemplate.boundHashOps(key);
        // 获取购物车
        String json = hashOps.get(skuId.toString()).toString();
        Cart cart = JsonUtils.parse(json, Cart.class);
        cart.setNum(num);
        // 写入购物车
        hashOps.put(skuId.toString(), JsonUtils.serialize(cart));
    }

删除购物车商品

页面请求

CartController

    /**
     * 删除购物车中的指定商品
     *
     * @param skuId 商品ID
     */
    @DeleteMapping("{skuId}")
    public ResponseEntity<Void> deleteCart(@PathVariable("skuId") String skuId) {
        cartService.deleteCart(skuId);
        return ResponseEntity.ok().build();
    }

CartService

    /**
     * 删除购物车中的指定商品
     *
     * @param skuId 商品ID
     */
    public void deleteCart(String skuId) {
        // 获取登录用户
        UserInfo user = UserInterceptor.getUserInfo();
        String key = KEY_PREFIX + user.getId();
        BoundHashOperations<String, Object, Object> hashOps = redisTemplate.boundHashOps(key);
        // 删除该hashKey
        hashOps.delete(skuId);
    }

登录时合并购物车

页面请求

CartController

    /**
     * 新增本地购物车商品到登陆用户购物车中
     *
     * @param carts 商品数据
     */
    @PostMapping("/merge")
    public ResponseEntity<Void> addCart(List<Cart> carts) {
        cartService.mergeCarts(carts);
        return ResponseEntity.ok().build();
    }

CartService

    /**
     * 新增本地购物车商品到登陆用户购物车中
     *
     * @param carts 商品数据
     */
    public void mergeCarts(List<Cart> carts) {
        // 获取当前用户信息
        UserInfo user = UserInterceptor.getUserInfo();
        // 遍历购物车并保存
        carts.forEach(cart -> {
            cart.setUserId(user.getId());
            saveCart(cart);
        });
    }

写在最后

我自己做的乐优基本上就到这里了,后面的订单支付模块我没做,因为和我之前看的品优购基本上没什么区别,只是用的Spring Boot而已,有兴趣做的,可以去看下品优购订单支付模块。

个人感觉,黑马的电商项目基本上都差不多了,只是用的技术的区别。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值