微服務架構(13):LocalStorage本地存儲&&Redis存儲

學習目標

  • 瞭解購物車的功能流程
  • 實現未登錄下購物車的功能
  • 實現已登錄下購物車的功能

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.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:
    prefer-ip-address: true
    ip-address: 127.0.0.1
    instance-id: ${eureka.instance.ip-address}.${server.port}
    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.需求

需求描述:

  • 用戶可以在登錄狀態下將商品添加到購物車
    • 放入數據庫
    • 放入redis(採用)
  • 用戶可以在未登錄狀態下將商品添加到購物車
    • 放入localstorage
  • 用戶可以使用購物車一起結算下單
  • 用戶可以查詢自己的購物車
  • 用戶可以在購物車中修改購買商品的數量。
  • 用戶可以在購物車中刪除商品。
  • 在購物車中展示商品優惠信息
  • 提示購物車商品價格變化

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的用法

語法非常簡單:
在這裏插入圖片描述

localStorage.setItem("key","value"); // 存儲數據
localStorage.getItem("key"); // 獲取數據
localStorage.removeItem("key"); // 刪除數據

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

不過,在我們的common.js中,已經對localStorage進行了簡單的封裝:
在這裏插入圖片描述
示例:

在這裏插入圖片描述

3.1.3.獲取num

添加購物車需要知道購物的數量,所以我們需要獲取數量大小。我們在Vue中定義num,保存數量:

在這裏插入圖片描述

然後將num與頁面的input框綁定,同時給+-的按鈕綁定事件:

在這裏插入圖片描述

編寫方法:
在這裏插入圖片描述

3.2.添加購物車

3.2.1.點擊事件

我們看下商品詳情頁:

在這裏插入圖片描述

現在點擊加入購物車會跳轉到購物車成功頁面。

不過我們不這麼做,我們綁定點擊事件,然後實現添加購物車功能。

在這裏插入圖片描述
addCart方法中判斷用戶的登錄狀態:

addCart(){
   ly.http.get("/auth/verify").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: JSON.stringify(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方法

效果:

在這裏插入圖片描述

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.4.總價格

然後編寫一個計算屬性,計算出選中商品總價格:

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

在頁面中展示總價格:

在這裏插入圖片描述

效果:

在這裏插入圖片描述

4.已登錄購物車

接下來,我們完成已登錄購物車。

在剛纔的未登錄購物車編寫時,我們已經預留好了編寫代碼的位置,邏輯也基本一致。

4.1.添加登錄校驗

購物車系統只負責登錄狀態的購物車處理,因此需要添加登錄校驗確定用戶token,我們通過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: D:/heima/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();
        }
    }

    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.1.4.編寫攔截器

因爲很多接口都需要進行登錄,我們直接編寫SpringMVC攔截器,進行統一登錄校驗。同時,我們還要把解析得到的用戶信息保存起來,以便後續的接口可以使用。

在這裏插入圖片描述
用戶信息保存的方法,我們採用線程thread來保存。因爲一次請求,就是一次獨立的線程。攔截器,控制器,服務service都共享該線程。
注意:

  • 這裏我們使用了ThreadLocal來存儲查詢到的用戶信息,線程內共享,因此請求到達Controller後可以共享User
  • 並且對外提供了靜態的方法:getLoginUser()來獲取User信息

代碼:

public class LoginInterceptor extends HandlerInterceptorAdapter {

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

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種不同數據結構,這裏選擇哪一種比較合適呢?

  • 首先不同用戶應該有獨立的購物車,因此購物車應該以用戶的作爲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;// 商品規格參數

    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.頁面發起請求

已登錄情況下,向後臺添加購物車:

在這裏插入圖片描述

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 = "ly: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);
}

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(@RequestParam("skuId") Long skuId, 
                                      @RequestParam("num") Integer num) {
    this.cartService.updateNum(skuId, num);
    return ResponseEntity.ok().build();
}

Service

public void updateNum(Long skuId, Integer num) {
    // 獲取登錄用戶
    UserInfo user = LoginInterceptor.getLoginUser();
    String key = KEY_PREFIX + user.getId();
    BoundHashOperations<String, Object, Object> hashOps = this.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));
}

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
  • 如果未登錄,直接查詢即可
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章