目录
四、已登录购物车
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。
解决方法:数据合并不要放在购物车数据加载中,应该放在登录成功的时候:
问题解决~