4.已登錄購(gòu)物車
接下來(lái)咳蔚,我們完成已登錄購(gòu)物車酌媒。
在剛才的未登錄購(gòu)物車編寫時(shí)拥娄,我們已經(jīng)預(yù)留好了編寫代碼的位置,邏輯也基本一致熙暴。
4.1.添加登錄校驗(yàn)
購(gòu)物車系統(tǒng)只負(fù)責(zé)登錄狀態(tài)的購(gòu)物車處理闺属,因此需要添加登錄校驗(yàn),我們通過(guò)JWT鑒權(quán)即可實(shí)現(xiàn)周霉。
4.1.1.引入JWT相關(guān)依賴
我們引入之前寫的鑒權(quán)工具:ly-auth-common
<dependency>
<groupId>com.leyou.service</groupId>
<artifactId>ly-auth-common</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
4.1.2.配置公鑰
ly:
jwt:
pubKeyPath: E:/nginx/rsa/rsa.pub # 公鑰地址
cookieName: LY_TOKEN # cookie的名稱
4.1.3.加載公鑰
代碼:
@ConfigurationProperties(prefix = "ly.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.編寫過(guò)濾器
因?yàn)楹芏嘟涌诙夹枰M(jìn)行登錄诗眨,我們直接編寫SpringMVC攔截器唉匾,進(jìn)行統(tǒng)一登錄校驗(yàn)。同時(shí)匠楚,我們還要把解析得到的用戶信息保存起來(lái)巍膘,以便后續(xù)的接口可以使用。
代碼:
public class LoginInterceptor extends HandlerInterceptorAdapter {
private JwtProperties jwtProperties;
// 定義一個(gè)線程域芋簿,存放登錄用戶
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 {
// 解析成功,證明已經(jīng)登錄
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();
}
}
注意:
- 這里我們使用了
ThreadLocal
來(lái)存儲(chǔ)查詢到的用戶信息肪康,線程內(nèi)共享,因此請(qǐng)求到達(dá)Controller
后可以共享User - 并且對(duì)外提供了靜態(tài)的方法:
getLoginUser()
來(lái)獲取User信息
4.1.5.配置過(guò)濾器
配置SpringMVC撩穿,使過(guò)濾器生效:
@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.后臺(tái)購(gòu)物車設(shè)計(jì)
數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)
當(dāng)用戶登錄時(shí)磷支,我們需要把購(gòu)物車數(shù)據(jù)保存到后臺(tái),可以選擇保存在數(shù)據(jù)庫(kù)食寡。但是購(gòu)物車是一個(gè)讀寫頻率很高的數(shù)據(jù)雾狈。因此我們這里選擇讀寫效率比較高的Redis作為購(gòu)物車存儲(chǔ)。
Redis有5種不同數(shù)據(jù)結(jié)構(gòu)抵皱,這里選擇哪一種比較合適呢善榛?
- 首先不同用戶應(yīng)該有獨(dú)立的購(gòu)物車,因此購(gòu)物車應(yīng)該以用戶的作為key來(lái)存儲(chǔ)呻畸,Value是用戶的所有購(gòu)物車信息移盆。這樣看來(lái)基本的
k-v
結(jié)構(gòu)就可以了。 - 但是伤为,我們對(duì)購(gòu)物車中的商品進(jìn)行增咒循、刪、改操作绞愚,基本都需要根據(jù)商品id進(jìn)行判斷剑鞍,為了方便后期處理,我們的購(gòu)物車也應(yīng)該是
k-v
結(jié)構(gòu)爽醋,key是商品id蚁署,value才是這個(gè)商品的購(gòu)物車信息。
綜上所述蚂四,我們的購(gòu)物車結(jié)構(gòu)是一個(gè)雙層Map:Map<String,Map<String,String>>
- 第一層Map光戈,Key是用戶id
- 第二層Map,Key是購(gòu)物車中商品id遂赠,值是購(gòu)物車數(shù)據(jù)
實(shí)體類
public class Cart {
private Long userId;// 用戶id
private Long skuId;// 商品id
private String title;// 標(biāo)題
private String image;// 圖片
private Long price;// 加入購(gòu)物車時(shí)的價(jià)格
private Integer num;// 購(gòu)買數(shù)量
private String ownSpec;// 商品規(guī)格參數(shù)
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.添加商品到購(gòu)物車
4.3.1.頁(yè)面發(fā)起請(qǐng)求:
已登錄情況下久妆,向后臺(tái)添加購(gòu)物車:
這里發(fā)起的是Json請(qǐng)求。那么我們后臺(tái)也要以json接收跷睦。
4.3.2.后臺(tái)添加購(gòu)物車
controller
先分析一下:
- 請(qǐng)求方式:新增筷弦,肯定是Post
- 請(qǐng)求路徑:/cart ,這個(gè)其實(shí)是Zuul路由的路徑,我們可以不管
- 請(qǐng)求參數(shù):Json對(duì)象烂琴,包含skuId和num屬性
- 返回結(jié)果:無(wú)
@RequestMapping
public class CartController {
@Autowired
private CartService cartService;
/**
* 添加購(gòu)物車
*
* @return
*/
@PostMapping
public ResponseEntity<Void> addCart(@RequestBody Cart cart) {
this.cartService.addCart(cart);
return ResponseEntity.ok().build();
}
}
Service
這里我們不訪問(wèn)數(shù)據(jù)庫(kù)爹殊,而是直接操作Redis〖楸粒基本思路:
- 先查詢之前的購(gòu)物車數(shù)據(jù)
- 判斷要添加的商品是否存在
- 存在:則直接修改數(shù)量后寫回Redis
- 不存在:新建一條數(shù)據(jù)梗夸,然后寫入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操作對(duì)象
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) {
// 存在,獲取購(gòu)物車數(shù)據(jù)
String json = hashOps.get(skuId.toString()).toString();
cart = JsonUtils.parse(json, Cart.class);
// 修改購(gòu)物車數(shù)量
cart.setNum(cart.getNum() + num);
} else {
// 不存在号醉,新增購(gòu)物車數(shù)據(jù)
cart.setUserId(user.getId());
// 其它商品信息反症, 需要查詢商品服務(wù)
ResponseEntity<Sku> resp = this.goodsClient.querySkuById(skuId);
if (resp.getStatusCode() != HttpStatus.OK || !resp.hasBody()) {
logger.error("添加購(gòu)物車的商品不存在:skuId:{}", skuId);
throw new RuntimeException();
}
Sku sku = resp.getBody();
cart.setImage(StringUtils.isBlank(sku.getImages()) ? "" : StringUtils.split(sku.getImages(), ",")[0]);
cart.setPrice(sku.getPrice());
cart.setTitle(sku.getTitle());
cart.setOwnSpec(sku.getOwnSpec());
}
// 將購(gòu)物車數(shù)據(jù)寫入redis
hashOps.put(cart.getSkuId().toString(), JsonUtils.serialize(cart));
}
}
4.3.3.結(jié)果:
4.4.查詢購(gòu)物車
4.4.1.頁(yè)面發(fā)起請(qǐng)求
4.4.2.后臺(tái)實(shí)現(xiàn)
Controller
/**
* 查詢購(gòu)物車列表
*
* @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();
// 判斷是否存在購(gòu)物車
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();
// 判斷是否有數(shù)據(jù)
if(CollectionUtils.isEmpty(carts)){
return null;
}
// 查詢購(gòu)物車數(shù)據(jù)
return carts.stream().map(o -> JsonUtils.parse(o.toString(), Cart.class)).collect(Collectors.toList());
}
4.4.3.測(cè)試
4.5.修改商品數(shù)量
4.5.1.頁(yè)面發(fā)起請(qǐng)求
4.5.2.后臺(tái)實(shí)現(xiàn)
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);
// 獲取購(gòu)物車
String json = hashOps.get(skuId.toString()).toString();
Cart cart = JsonUtils.parse(json, Cart.class);
cart.setNum(num);
// 寫入購(gòu)物車
hashOps.put(skuId.toString(), JsonUtils.serialize(cart));
}
4.6.刪除購(gòu)物車商品
4.6.1.頁(yè)面發(fā)起請(qǐng)求
注意:后臺(tái)成功響應(yīng)后畔派,要把頁(yè)面的購(gòu)物車中數(shù)據(jù)也刪除
4.6.2.后臺(tái)實(shí)現(xiàn)
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);
}