飞道的博客

乐优商城(十二)购物车

405人阅读  评论(0)

1. 搭建购物车微服务

  1. 右键 leyou 项目 --> New Module --> Maven --> Next

  2. 填写项目信息 --> Next

  3. 填写保存的位置 --> Finish

  4. 添加依赖

    <?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-SNAPSHOT</version>
        </parent>
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>com.leyou.cart</groupId>
        <artifactId>leyou-cart</artifactId>
        <version>1.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>
            <dependency>
                <groupId>com.leyou.item</groupId>
                <artifactId>leyou-item-interface</artifactId>
                <version>1.0-SNAPSHOT</version>
            </dependency>
            <dependency>
                <groupId>com.leyou.common</groupId>
                <artifactId>leyou-common</artifactId>
                <version>1.0-SNAPSHOT</version>
            </dependency>
        </dependencies>
    
    </project>
    
  5. 编写配置文件 application.yaml

    server:
      port: 8088
    spring:
      application:
        name: cart-service
      redis:
        host: 192.168.222.132
    eureka:
      client:
        service-url:
          defaultZone: http://localhost:10086/eureka
        registry-fetch-interval-seconds: 10
      instance:
        lease-renewal-interval-in-seconds: 5
        lease-expiration-duration-in-seconds: 15
    
  6. 编写启动类

    @EnableDiscoveryClient
    @EnableFeignClients
    @SpringBootApplication
    public class LeyouCartApplicaion {
        public static void main(String[] args) {
            SpringApplication.run(LeyouCartApplicaion.class, args);
        }
    }
    
  7. 最终目录结构

  8. 在 leyou-gateway 的 applicaton.yaml 中添加路由配置

    zuul:
      prefix: /api
      routes:
        item-service: /item/**
        search-service: /search/**
        user-service: /user/**
        auth-service: /auth/**
        cart-service: /cart/**
    

2. 购物车功能分析

2.1 需求分析

  • 用户可以在登录状态下将商品添加到购物车
  • 用户可以在未登录状态下将商品添加到购物车
  • 用户可以使用购物车一起结算下单
  • 用户可以查询自己的购物车
  • 用户可以在购物车中修改购买商品的数量。
  • 用户可以在购物车中删除商品。

2.2 技术选型

  • 用户在登录状态下将商品添加到购物车,数据应该保存在哪里呢

    • MySQL:在大量数据时,MySQL 的效率会显著降低,不推荐使用。
    • Redis:在大量数据时,Redis 数据放在内存中,大小有限,不推荐使用。
    • MongoDB:MongoDB 是文档型数据库,它保存在硬盘上,在大量数据时,效率很高,推荐使用

    注意:本项目中还是使用的 Redis,但更推荐 MongoDB。

  • 用户在未登录状态下将商品添加到购物车,数据应该保存在哪里呢

    • Cookie:Cookie 大小有限制(4 KB),同一个域名下的总 Cookie 数量也有限制(20 个),并且每次请求携带 Cookie 会占用大量带宽,不推荐使用。
    • Web SQL:使用有些麻烦,还需要写 SQL,不推荐使用。
    • Local Storage:主要是用来作为本地存储来使用的,解决了 Cookie 存储空间不足的问题,Local Storage 中一般浏览器支持的是 5M 大小,推荐使用

2.3 流程图

这幅图主要描述了两个功能:添加购物车、查询购物车。

  • 添加购物车:

    • 判断是否登录
      • 是:则添加商品到 Redis 中
      • 否:则添加商品到本地的 Local Storage
  • 查询购物车列表:

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

3. 未登录购物车

3.1 购物车数据结构

首先分析一下未登录购物车的数据结构,下面是需要展示的数据:

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

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

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

3.2 获取购物数量

添加购物车需要知道购物的数量,所以我们需要获取数量大小。

  1. 我们在 item.html 中定义 num,保存数量

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

  3. 编写方法

3.3 添加购物车

  1. 绑定点击事件 addCart 方法

  2. addCart 方法中判断用户的登录状态,未登录状态下保存商品在浏览器本地的 Local Storage 中,然后跳转到购物车页面

    addCart(){
        //判断有没有登陆
        ly.http.get("/auth/verify").then(res=>{
            // 已登录发送信息到后台,保存到redis中
        }).catch(()=>{
            // 未登录保存在浏览器本地的localStorage中
            // 1、查询本地购物车
            let carts = ly.store.get("LY_CART") || [];
            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,
                    ownSpec: this.sku.ownSpec,
                    num: this.num
                };
                carts.push(cart);
            }
            // 把carts写回localstorage
            ly.store.set("LY_CART", carts);
            // 跳转
            window.location.href = "http://www.leyou.com/cart.html";
        });
    }
    
  3. 点击加入购物车后,查看 Local Storage

3.4 渲染购物车页面

3.4.1 封装判断用户的登录状态方法

  1. 因为查询购物车也会用到判断用户的登录状态,因此我们将这个方法封装到 common.js 中

  2. 修改 item.html 中的方法

3.4.2 查询购物车

修改 cart.html,页面加载时,就应该去查询购物车

3.4.3 渲染页面

略,交给前端吧,最终效果如下:

3.5 修改数量

  1. 我们给页面的 +-绑定点击事件

  2. 点击事件中修改 num 的值

3.6 删除商品

  1. 给删除按钮绑定事件

  2. 点击事件中删除商品

4. 已登录购物车

4.1 流程分析

  1. 登录后,加入购物车会发送请求到后台,首先会经过 Zuul 网关 LoginFilter 登录验证,如果通过则请求被转发到 leyou-cart
  2. 由于加入购物车还需要用户信息,所以我们需要解析用户信息并保存起来,以便后续的接口可以使用。这里用到 SpringMVC 的前置 preHandle,并将解析后的 UserInfo 放入 ThreadLocal 中。

4.2 实现解析用户信息

  1. 在 leyou-cart 中添加依赖

    <dependency>
        <groupId>com.leyou.auth</groupId>
        <artifactId>leyou-auth-common</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
    
  2. 在 application.yaml 中添加 JWT 配置

    leyou:
      jwt:
        pubKeyPath: D:\tmp\rsa\\rsa.pub # 公钥地址
        cookieName: LY_TOKEN # cookie的名称
    
  3. 创建 JWT 属性读取类

    @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();
            }
        }
    
        public String getPubKeyPath() {
            return pubKeyPath;
        }
    
        public void setPubKeyPath(String pubKeyPath) {
            this.pubKeyPath = pubKeyPath;
        }
    
        public PublicKey getPublicKey() {
            return publicKey;
        }
    
        public void setPublicKey(PublicKey publicKey) {
            this.publicKey = publicKey;
        }
    
        public String getCookieName() {
            return cookieName;
        }
    
        public void setCookieName(String cookieName) {
            this.cookieName = cookieName;
        }
    }
    
  4. 编写拦截器

    @Component
    @EnableConfigurationProperties(JwtProperties.class)
    public class LoginInterceptor extends HandlerInterceptorAdapter {
    
        @Autowired
        private JwtProperties jwtProperties;
    
        // 定义一个线程域,存放用户信息
        private static final ThreadLocal<UserInfo> THREAD_LOCAL = new ThreadLocal<>();
    
        /**
         * 获取用户信息
         * @return
         */
        public static UserInfo getUserInfo() {
            return THREAD_LOCAL.get();
        }
    
        /**
         * 解析 JWT,并将用户信息存放入 ThreadLocal
         * @param request
         * @param response
         * @param handler
         * @return
         * @throws Exception
         */
        @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 userInfo = JwtUtils.getInfoFromToken(token, jwtProperties.getPublicKey());
                // 放入线程域
                THREAD_LOCAL.set(userInfo);
                return true;
            } catch (Exception e){
                // 抛出异常,登录,返回 401
                response.setStatus(HttpStatus.UNAUTHORIZED.value());
                return false;
            }
    
        }
    
        /**
         * 响应视图后,释放 ThreadLocal
         * @param request
         * @param response
         * @param handler
         * @param ex
         * @throws Exception
         */
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
            THREAD_LOCAL.remove();
        }
    
    }
    

    注意:

    • 这里我们使用了 ThreadLocal 来存储查询到的用户信息,线程内共享,因此请求到达 Controller 后可以共享 UserInfo
    • 并且对外提供了静态的方法 getLoginUser() 来获取 UserInfo 信息
  5. 定义配置类,注册拦截器

    @Configuration
    public class MvcConfig implements WebMvcConfigurer {
        @Autowired
        private LoginInterceptor loginInterceptor;
        
        @Override
        /**
         * 重写接口中的 addInterceptors 方法,添加自定义拦截器
         */
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(loginInterceptor).addPathPatterns("/**");
        }
    }
    

4.3 后台购物车设计

当用户登录时,我们需要把购物车数据保存 Redis 中,那应该采用怎样的数据结构保存呢?

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

综上所述,我们的购物车结构是一个双层 Map,也就对应着 Redis 中的 Hash 数据结构

Map<String,Map<String,String>>

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

4.4 添加购物车

4.4.1 前端发起请求

修改 item.html,已登录情况下,向后台发送 POST 请求添加购物车

4.4.2 实体类

在 leyou-cart 中添加购物车实体类 Cart

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、setter 方法
}

4.4.3 FeignClient

在添加购物车时,需要根据 skuId 去查询 Sku 信息,我们会在 leyou-cart 中远程调用 leyou-item 提供的对应接口。

  1. 在 leyou-item-service 中 SpuController 添加方法

    /**
     * 通过 skuId 查询 Sku
     *
     * @param skuId
     * @return
     */
    @GetMapping("sku")
    public ResponseEntity<Sku> querySkuBySkuId(@RequestParam("skuId") Long skuId) {
        Sku sku = spuService.querySkuBySkuId(skuId);
        if (sku == null) {
            return ResponseEntity.notFound().build();
        }
        return ResponseEntity.ok(sku);
    }
    
  2. 在 leyou-item-service 中 SpuService 添加方法

    /**
     * 通过 skuId 查询 Sku
     *
     * @param skuId
     * @return
     */
    public Sku querySkuBySkuId(Long skuId) {
        Sku sku = skuMapper.selectByPrimaryKey(skuId);
        return sku;
    }
    
  3. 在 leyou-item-interface 中 SpuApi 添加接口

    /**
     * 通过 skuId 查询 Sku
     *
     * @param skuId
     * @return
     */
    @GetMapping("sku")
    public Sku querySkuBySkuId(@RequestParam("skuId") Long skuId);
    
  4. 在 leyou-cart 中添加 SpuClient

    @FeignClient("item-service")
    public interface SpuClient extends SpuApi {
    }
    

4.4.4 Controller

在 leyou-cart 中添加 CartController

@Controller
public class CartController {
    @Autowired
    private CartService cartService;

    /**
     * 添加购物车
     * @param cart
     * @return
     */
    @PostMapping
    public ResponseEntity<Void> addCart(@RequestBody Cart cart) {
        cartService.addCart(cart);
        return ResponseEntity.status(HttpStatus.CREATED).build();
    }
}

4.4.5 Service

在 leyou-cart 中添加 CartService

@Service
public class CartService {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private SpuClient spuClient;

    private static final String KEY_PREFIX = "leyou:cart:uid:";

    /**
     * 添加购物车
     *
     * @param cart
     * @return
     */
    public void addCart(Cart cart) {
        // 获取用户信息
        UserInfo userInfo = LoginInterceptor.getUserInfo();

        // 操作 Hash 数据
        BoundHashOperations<String, Object, Object> boundHashOps = stringRedisTemplate.boundHashOps(KEY_PREFIX + userInfo.getId());

        // 判断购物车是否有该商品
        if (boundHashOps.hasKey(cart.getSkuId().toString())) {
            // 有,更改该商品数量
            String jsonCart = boundHashOps.get(cart.getSkuId().toString()).toString();
            Cart cart1 = JsonUtils.parse(jsonCart, Cart.class);
            cart1.setNum(cart1.getNum() + cart.getNum());
            boundHashOps.put(cart1.getSkuId().toString(), JsonUtils.serialize(cart1));
        } else {
            // 无,新增该商品
            Sku sku = spuClient.querySkuBySkuId(cart.getSkuId());
            cart.setUserId(userInfo.getId());
            cart.setTitle(sku.getTitle());
            cart.setImage(sku.getImages().split(",")[0]);
            cart.setOwnSpec(sku.getOwnSpec());
            boundHashOps.put(cart.getSkuId().toString(),JsonUtils.serialize(cart));
        }
    }
}

4.4.6 最终目录结构

4.4.7 测试

在登录后,添加商品到购物车后,查看 Redis

4.5 查询购物车

4.5.1 前端发起请求

修改 item.html,已登录情况下,向后台发送 GET 请求添加购物车

4.5.2 Controller

在 CartController 中添加 queryCart 方法

/**
 * 查询购物车
 * @return
 */
@GetMapping
public ResponseEntity<List<Cart>> queryCart() {
    List<Cart> carts = cartService.queryCart();
    if(carts == null) {
        return ResponseEntity.notFound().build();
    }
    return ResponseEntity.ok(carts);
}

4.5.3 Service

在 CartService 中添加 queryCart 方法

/**
 * 查询购物车
 *
 * @return
 */
public List<Cart> queryCart() {
    // 获取用户信息
    UserInfo userInfo = LoginInterceptor.getUserInfo();
    // 判断用户是否存在购物车
    if (!stringRedisTemplate.hasKey(KEY_PREFIX + userInfo.getId())) {
        return null;
    }
    // 操作 Hash 数据
    BoundHashOperations<String, Object, Object> boundHashOps = stringRedisTemplate.boundHashOps(KEY_PREFIX + userInfo.getId());
    // 获取所有购物车中商品
    List<Object> jsonCarts = boundHashOps.values();
    // 判断购物车中是否有商品
    if (CollectionUtils.isEmpty(jsonCarts)) {
        return null;
    }
    ArrayList<Cart> carts = new ArrayList<>();
    for (Object jsonCart : jsonCarts) {
        Cart cart = JsonUtils.parse(jsonCart.toString(), Cart.class);
        carts.add(cart);
    }
    return carts;
}

4.5.4 测试

在登录后,添加商品到购物车后,查询到的购物车

4.6 修改数量

4.6.1 前端发起请求

4.6.2 Controller

/**
 * 修改购物车
 *
 * @return
 */
@PutMapping
public ResponseEntity<Void> updateCart(@RequestBody Cart cart) {
    cartService.updateCart(cart);
    return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
}

4.6.3 Service

/**
 * 修改购物车
 *
 * @return
 */
public void updateCart(Cart cart) {
    // 获取用户信息
    UserInfo userInfo = LoginInterceptor.getUserInfo();

    // 操作 Hash 数据
    BoundHashOperations<String, Object, Object> boundHashOps = stringRedisTemplate.boundHashOps(KEY_PREFIX + userInfo.getId());

    // 更改该商品数量
    String jsonCart = boundHashOps.get(cart.getSkuId().toString()).toString();
    Cart cart1 = JsonUtils.parse(jsonCart, Cart.class);
    cart1.setNum(cart.getNum());
    boundHashOps.put(cart1.getSkuId().toString(), JsonUtils.serialize(cart1));
}

4.7 删除商品

4.7.1 前端发起请求

4.7.2 Controller

/**
 * 删除购物车
 *
 * @param skuId
 * @return
 */
@DeleteMapping("{skuId}")
public ResponseEntity<Void> deleteCart(@PathVariable("skuId") String skuId) {
    cartService.deleteCart(skuId);
    return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
}

4.7.3 Service

/**
 * 删除购物车
 *
 * @param skuId
 * @return
 */
public void deleteCart(String skuId) {
    // 获取用户信息
    UserInfo userInfo = LoginInterceptor.getUserInfo();

    // 操作 Hash 数据
    BoundHashOperations<String, Object, Object> boundHashOps = stringRedisTemplate.boundHashOps(KEY_PREFIX + userInfo.getId());

    // 删除商品
    boundHashOps.delete(skuId);
}

转载:https://blog.csdn.net/bm1998/article/details/106066339
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场