1. 程式人生 > >樂優商城(三十三)——購物車

樂優商城(三十三)——購物車

目錄

四、已登入購物車

4.1 新增登入校驗

4.1.1 引入JWT相關依賴

4.1.2 配置公鑰

4.1.3 載入公鑰

4.1.4 編寫攔截器

4.1.5 配置攔截器

4.1.6 編寫過濾器

4.1.7 配置過濾器

4.2 後臺購物車設計

4.3 新增商品到購物車

4.3.1 頁面發起請求

4.3.2 編寫Controller

4.3.3 CartService

4.3.4 GoodClient

4.3.5 結果

4.4 查詢購物車

4.4.1 頁面發起請求

4.4.2 後臺實現

4.4.3 測試

4.5 修改商品數量

4.5.1 頁面發起請求

4.5.2 後臺實現

4.6 刪除購物車商品

4.6.1 頁面發起請求

4.6.2 後臺實現

五、登陸後購物車合併

5.1 前端

5.2 後端

5.3 測試

5.3.1 未登入

5.3.2 登入

5.4 問題


 

四、已登入購物車

4.1 新增登入校驗

購物車系統只負責登入狀態的購物車處理,因此需要新增登入校驗,通過JWT鑑權即可實現。

4.1.1 引入JWT相關依賴

        <dependency>
            <groupId>com.leyou.authentication</groupId>
            <artifactId>leyou-authentication-common</artifactId>
        </dependency>

4.1.2 配置公鑰

leyou:
  jwt:
    cookieName: LY_TOKEN
    pubKeyPath: G:\\tmp\\rsa\\rsa.pub # 公鑰地址

4.1.3 載入公鑰

程式碼:

package com.leyou.cart.config;

import com.leyou.auth.utils.RsaUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;

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

/**
 * @Author: 98050
 * @Time: 2018-10-25 16:12
 * @Feature: jwt屬性
 */
@ConfigurationProperties(prefix = "leyou.jwt")
public class JwtProperties {
    /**
     * 公鑰
     */
    private PublicKey publicKey;

    /**
     * 公鑰地址
     */
    private String pubKeyPath;

    /**
     * cookie名字
     */
    private String cookieName;

    private static final Logger logger = LoggerFactory.getLogger(JwtProperties.class);

    public PublicKey getPublicKey() {
        return publicKey;
    }

    public void setPublicKey(PublicKey publicKey) {
        this.publicKey = publicKey;
    }

    public String getPubKeyPath() {
        return pubKeyPath;
    }

    public void setPubKeyPath(String pubKeyPath) {
        this.pubKeyPath = pubKeyPath;
    }

    public String getCookieName() {
        return cookieName;
    }

    public void setCookieName(String cookieName) {
        this.cookieName = cookieName;
    }

    public static Logger getLogger() {
        return logger;
    }

    /**
     * @PostConstruct :在構造方法執行之後執行該方法
     */
    @PostConstruct
    public void init(){
        try {
            File pubKey = new File(pubKeyPath);
            // 獲取公鑰
            this.publicKey = RsaUtils.getPublicKey(pubKeyPath);
        } catch (Exception e) {
            logger.error("獲取公鑰失敗!", e);
            throw new RuntimeException();
        }
    }
}

4.1.4 編寫攔截器

因為很多介面都需要進行登入,直接編寫SpringMVC攔截器,進行統一登入校驗。同時,還要把解析得到的使用者資訊儲存起來,以便後續的介面可以使用。

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.utils.CookieUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpStatus;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

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

/**
 * @Author: 98050
 * @Time: 2018-10-25 18:17
 * @Feature: 登入攔截器
 */
public class LoginInterceptor extends HandlerInterceptorAdapter {

    private JwtProperties jwtProperties;

    /**
     * 定義一個執行緒域,存放登入使用者
     */
    private static final ThreadLocal<UserInfo> t1 = new ThreadLocal<>();

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

    /**
     *      * 在業務處理器處理請求之前被呼叫
     *      * 如果返回false
     *      *      則從當前的攔截器往回執行所有攔截器的afterCompletion(),再退出攔截器鏈
     *      * 如果返回true
     *      *      執行下一個攔截器,直到所有攔截器都執行完畢
     *      *      再執行被攔截的Controller
     *      *      然後進入攔截器鏈
     *      *      從最後一個攔截器往回執行所有的postHandle()
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1.查詢token
        String token = CookieUtils.getCookieValue(request,jwtProperties.getCookieName());
        if (StringUtils.isBlank(token)){
            //2.未登入,返回401
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            return false;
        }
        //3.有token,查詢使用者資訊
        try{
            //4.解析成功,說明已經登入
            UserInfo userInfo = JwtUtils.getInfoFromToken(token,jwtProperties.getPublicKey());
            //5.放入執行緒域
            t1.set(userInfo);
            return true;
        }catch (Exception e){
            //6.丟擲異常,證明未登入,返回401
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            return false;
        }
    }

    /**
     * 在業務處理器處理請求執行完成後,生成檢視之前執行的動作
     * 可在modelAndView中加入資料,比如當前時間
     * @param request
     * @param response
     * @param handler
     * @param modelAndView
     * @throws Exception
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        super.postHandle(request, response, handler, modelAndView);
    }

    /**
     * 在DispatcherServlet完全處理完請求後被呼叫,可用於清理資源等
     * 當有攔截器丟擲異常時,會從當前攔截器往回執行所有的攔截器的afterCompletion()
     * @param request
     * @param response
     * @param handler
     * @param ex
     * @throws Exception
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
       t1.remove();
    }
    
    public static UserInfo getLoginUser(){
        return t1.get();
    }
}

注意:

  • 這裡使用了ThreadLocal來儲存查詢到的使用者資訊,執行緒內共享,因此請求到達Controller後可以共享User

  • 並且對外提供了靜態的方法:getLoginUser()來獲取User資訊

4.1.5 配置攔截器

package com.leyou.cart.config;

import com.leyou.cart.interceptor.LoginInterceptor;
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;

/**
 * @Author: 98050
 * @Time: 2018-10-25 19:48
 * @Feature: 配置過濾器
 */
@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.1.6 編寫過濾器

以後使用

package com.leyou.cart.filter;


import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;

/**
 * @Author: 98050
 * @Time: 2018-10-25 20:00
 * @Feature:
 */
@WebFilter(filterName = "CartFilter",urlPatterns = {"/**"})
public class CartFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("過濾器初始化");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("具體過濾規則");
    }

    @Override
    public void destroy() {
        System.out.println("銷燬");
    }
}

4.1.7 配置過濾器

兩種方式:

註解

配置類

4.2 後臺購物車設計

當用戶登入時,需要把購物車資料儲存到後臺,可以選擇儲存在資料庫。但是購物車是一個讀寫頻率很高的資料。因此這裡選擇讀寫效率比較高的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;

/**
 * @Author: 98050
 * @Time: 2018-10-25 20:27
 * @Feature: 購物車實體類
 */
public class Cart {
    /**
     * 使用者Id
     */
    private Long userId;

    /**
     * 商品id
     */
    private Long skuId;

    /**
     * 標題
     */
    private String title;

    /**
     * 圖片
     */
    private String image;

    /**
     * 加入購物車時的價格
     */
    private Long price;
    
    /**
     * 購買數量
     */
    private Integer num;

    /**
     * 商品規格引數
     */
    private String ownSpec;

    public Long getUserId() {
        return userId;
    }

    public void setUserId(Long userId) {
        this.userId = userId;
    }

    public Long getSkuId() {
        return skuId;
    }

    public void setSkuId(Long skuId) {
        this.skuId = skuId;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getImage() {
        return image;
    }

    public void setImage(String image) {
        this.image = image;
    }

    public Long getPrice() {
        return price;
    }

    public void setPrice(Long price) {
        this.price = price;
    }

    public Integer getNum() {
        return num;
    }

    public void setNum(Integer num) {
        this.num = num;
    }

    public String getOwnSpec() {
        return ownSpec;
    }

    public void setOwnSpec(String ownSpec) {
        this.ownSpec = ownSpec;
    }
}

4.3 新增商品到購物車

4.3.1 頁面發起請求

已登入情況下,向後臺新增購物車:

這裡發起的是json請求,那麼後臺接收也要以json接收。

4.3.2 編寫Controller

先分析一下:

  • 請求方式:新增,肯定是Post

  • 請求路徑:/cart ,這個其實是Zuul路由的路徑,可以不管

  • 請求引數:Json物件,包含skuId和num屬性

  • 返回結果:無

package com.leyou.cart.controller;

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.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

/**
 * @Author: 98050
 * @Time: 2018-10-25 20:41
 * @Feature:
 */
@Controller
public class CartController {

    @Autowired
    private CartService cartService;

    @PostMapping
    public ResponseEntity<Void> addCart(@RequestBody Cart cart){
        this.cartService.addCart(cart);
        return ResponseEntity.ok().build();
    }
}

在閘道器中新增路由配置:

4.3.3 CartService

這裡不直接訪問資料庫,而是直接操作Redis。基本思路:

  • 先查詢之前的購物車資料

  • 判斷要新增的商品是否存在

    • 存在:則直接修改數量後寫回Redis

    • 不存在:新建一條資料,然後寫入Redis

程式碼:

介面

package com.leyou.cart.service;

import com.leyou.cart.pojo.Cart;

/**
 * @Author: 98050
 * @Time: 2018-10-25 20:47
 * @Feature:
 */
public interface CartService {
    /**
     * 新增購物車
     * @param cart
     */
    void addCart(Cart cart);
}

實現

package com.leyou.cart.service.impl;

import com.leyou.auth.entity.UserInfo;
import com.leyou.cart.client.GoodsClient;
import com.leyou.cart.interceptor.LoginInterceptor;
import com.leyou.cart.pojo.Cart;
import com.leyou.cart.service.CartService;
import com.leyou.item.pojo.Sku;
import com.leyou.utils.JsonUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;

/**
 * @Author: 98050
 * @Time: 2018-10-25 20:48
 * @Feature:
 */
@Service
public class CartServiceImpl implements CartService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private GoodsClient goodsClient;

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

    private final Logger logger = LoggerFactory.getLogger(CartServiceImpl.class);

    /**
     * 新增購物車
     * @param cart
     */
    @Override
    public void addCart(Cart cart) {
        //1.獲取使用者
        UserInfo userInfo = LoginInterceptor.getLoginUser();
        //2.Redis的key
        String key = KEY_PREFIX + userInfo.getId();
        //3.獲取hash操作物件
        BoundHashOperations<String,Object,Object> hashOperations = this.stringRedisTemplate.boundHashOps(key);
        //4.查詢是否存在
        Long skuId = cart.getSkuId();
        Integer num = cart.getNum();
        Boolean result = hashOperations.hasKey(skuId.toString());
        if (result){
            //5.存在,獲取購物車資料
            String json = hashOperations.get(skuId.toString()).toString();
            cart = JsonUtils.parse(json,Cart.class);
            //6.修改購物車數量
            cart.setNum(cart.getNum() + num);
        }else{
            //7.不存在,新增購物車資料
            cart.setUserId(userInfo.getId());
            //8.其他商品資訊,需要查詢商品微服務
            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());
        }
        //9.將購物車資料寫入redis
        hashOperations.put(cart.getSkuId().toString(),JsonUtils.serialize(cart));
    }
}

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

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

4.3.4 GoodClient

package com.leyou.cart.client;

import com.leyou.item.api.GoodsApi;
import org.springframework.cloud.openfeign.FeignClient;

/**
 * @Author: 98050
 * @Time: 2018-10-25 21:03
 * @Feature:商品FeignClient
 */
@FeignClient(value = "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 ResponseEntity.status(HttpStatus.NOT_FOUND).build();
        }
        return ResponseEntity.ok(sku);
    }

在leyou-item-service中的GoodsService新增方法:

    /**
     * 查詢sku根據id
     * @param id
     * @return
     */
    Sku querySkuById(Long id);

實現:

    /**
     * 根據skuId查詢sku
     * @param id
     * @return
     */
    @Override
    public Sku querySkuById(Long id) {
        return this.skuMapper.selectByPrimaryKey(id);
    }

在leyou-item-interface中GoodsApi中新增介面:

    /**
     * 根據sku的id查詢sku
     * @param id
     * @return
     */
    @GetMapping("/sku/{id}")
    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).build();
        }
        return ResponseEntity.ok(carts);
    }

Service

介面:

實現:

    /**
     * 查詢購物車
     * @return
     */
    @Override
    public List<Cart> queryCartList() {
        //1.獲取登入的使用者資訊
        UserInfo userInfo = LoginInterceptor.getLoginUser();
        //2.判斷是否存在購物車
        String key = KEY_PREFIX + userInfo.getId();
        if (!this.stringRedisTemplate.hasKey(key)) {
            //3.不存在直接返回
            return null;
        }
        BoundHashOperations<String,Object,Object> hashOperations = this.stringRedisTemplate.boundHashOps(key);
        List<Object> carts = hashOperations.values();
        //4.判斷是否有資料
        if (CollectionUtils.isEmpty(carts)){
            return null;
        }
        //5.查詢購物車資料
        return carts.stream().map( o -> JsonUtils.parse(o.toString(),Cart.class)).collect(Collectors.toList());
    }

4.4.3 測試

購物車:

redis中資料:

4.5 修改商品數量

4.5.1 頁面發起請求

4.5.2 後臺實現

Controller

    /**
     * 修改購物車中商品數量
     * @return
     */
    @PutMapping
    public ResponseEntity<Void> updateNum(@RequestParam("skuId") Long skuId,@RequestParam("num") Integer num){
        this.cartService.updateNum(skuId,num);
        return ResponseEntity.ok().build();
    }

Service

介面:

實現:

    /**
     * 更新購物車中商品數量
     * @param skuId
     * @param num
     */
    @Override
    public void updateNum(Long skuId, Integer num) {
        //1.獲取登入使用者
        UserInfo userInfo = LoginInterceptor.getLoginUser();
        String key = KEY_PREFIX + userInfo.getId();
        BoundHashOperations<String,Object,Object> hashOperations = this.stringRedisTemplate.boundHashOps(key);
        //2.獲取購物車
        String json = hashOperations.get(skuId.toString()).toString();
        Cart cart = JsonUtils.parse(json,Cart.class);
        cart.setNum(num);
        //3.寫入購物車
        hashOperations.put(skuId.toString(),JsonUtils.serialize(cart));
    }

4.6 刪除購物車商品

4.6.1 頁面發起請求

4.6.2 後臺實現

Controller

     /**
     * 刪除購物車中的商品
     * @param skuId
     * @return
     */
    @DeleteMapping("{skuId}")
    public ResponseEntity<Void> deleteCart(@PathVariable("skuId") String skuId){
        this.cartService.deleteCart(skuId);
        return ResponseEntity.ok().build();
    }

Service

介面:

 

實現:

    /**
     * 刪除購物車中的商品
     * @param skuId
     */
    @Override
    public void deleteCart(String skuId) {
        //1.獲取登入使用者
        UserInfo userInfo = LoginInterceptor.getLoginUser();
        String key = KEY_PREFIX + userInfo.getId();
        BoundHashOperations<String,Object,Object> hashOperations = this.stringRedisTemplate.boundHashOps(key);
        //2.刪除商品
        hashOperations.delete(skuId);
    }

五、登陸後購物車合併

當跳轉到購物車頁面,查詢購物車列表前,需要判斷使用者登入狀態,

  • 如果登入:

    • 首先檢查使用者的LocalStorage中是否有購物車資訊,

    • 如果有,則提交到後臺儲存,

    • 清空LocalStorage

  • 如果未登入,直接查詢即可

5.1 前端

修改cart.html中的loadCarts函式,如果登入成功,則讀取本地LocalStorage資料,不為空,則請求後臺合併資料

 

5.2 後端

同新增購物車

5.3 測試

5.3.1 未登入

頁面:

LocalStorage:

redis中沒有資料:

5.3.2 登入

頁面:

redis中:

本地儲存:

5.4 問題

資料同步失敗,在未登入狀態下將商品新增到購物車,然後點選登入,會發現查詢購物車列表失敗,404。

解決方法:資料合併不要放在購物車資料載入中,應該放在登入成功的時候:

問題解決~