樂優商城(三十三)——購物車
目錄
四、已登入購物車
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。
解決方法:資料合併不要放在購物車資料載入中,應該放在登入成功的時候:
問題解決~