本文主要是介绍乐优商城:笔记(十四):购物车微服务:LyCartApplication,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
文章目录
- 1 分析
- 1.1 需求分析
- 1.2 购物车数据结构
- 1.3 购物车商品存储位置
- 1.3.1 web本地存储
- 1.3.2 localstorage的用法
- 2 未登录购物车时——localstorage
- 2.1 添加到购物车
- 2.2 提供查询接口显示价格变化
- 3 已登陆购物车——Redis
- 3.1 搭建购物车服务
- 3.1.1 创建module
- 3.1.2 引入依赖
- 3.1.3 配置文件
- 3.1.4 启动类
- 3.1.5 添加路由
- 3.2 鉴权
- 3.2.1 引入公钥实现解析
- 3.2.2 读取公钥
- 3.2.3 编写拦截器
- 3.2.4 使得拦截器生效
- 3.3 购物车数据设计
- 3.3.1 购物车数据结构
- 3.3.2 实体类
- 3.4 添加商品到购物车
- 3.4.1 controller
- 3.4.2 service
- 3.5 查询购物车
- 3.5.1 controller
- 3.5.2 service
- 3.6 修改购物车中商品数量
- 3.6.1 controller
- 3.6.2 service
- 3.7 删除购物车中商品
- 3.7.1 controller
- 3.7.2 service
- 4 待优化
1 分析
1.1 需求分析
需求描述:
- 用户可以在登录状态下将商品添加到购物车
- 放入数据库
- 放入redis(采用)
- 用户可以在未登录状态下将商品添加到购物车
- 放入localstorage
- 用户可以使用购物车一起结算下单
- 用户可以查询自己的购物车
- 用户可以在购物车中可以修改购买商品的数量。
- 用户可以在购物车中删除商品。
- 在购物车中展示商品优惠信息
- 提示购物车商品价格变化
1.2 购物车数据结构
首先分析一下未登录购物车的数据结构。
我们看下页面展示需要什么数据:
因此每一个购物车信息,都是一个对象,包含:
{skuId:2131241,title:"小米6",image:"",price:190000,num:1,ownSpec:"{"机身颜色":"陶瓷黑尊享版","内存":"6GB","机身存储":"128GB"}"
}
另外,购物车中不止一条数据,因此最终会是对象的数组。即:
[{...},{...},{...}
]
注:购物车的价格是加入时的价格,当价格发生变化时要有降价的友好提示提升用户体验
1.3 购物车商品存储位置
知道了数据结构,下一个问题,就是如何保存购物车数据。购物车的数据是被频繁读写的,存入数据库是不合适的,那么我们选择的是——web本地存储
1.3.1 web本地存储
web本地存储主要有两种方式:
- LocalStorage:localStorage 方法存储的数据没有时间限制。第二天、第二周或下一年之后,数据依然可用。
- SessionStorage:sessionStorage 方法针对一个 session 进行数据存储。当用户关闭浏览器窗口后,数据会被删除。
显然我们选择的是localstorage,不能关闭了浏览器商品信息就没有了
1.3.2 localstorage的用法
localstorage已经变成了js的内置变量,可以在js的任何一个地方使用它,语法如下:
localStorage.setItem("key","value"); // 存储数据
localStorage.getItem("key"); // 获取数据
localStorage.removeItem("key"); // 删除数据
注意:localStorage和SessionStorage都只能保存字符串。
2 未登录购物车时——localstorage
2.1 添加到购物车
我们看下商品详情页:
现在点击加入购物车会跳转到购物车成功页面:cart.html,localstorage中的信息如下:
但是此时却报错了,发现浏览器发起了一条请求:
但是很奇怪,购物车所需要的信息在localstorage中都有的呀,为什么还要查询sku的信息呢?答案是我们之前所说的购物车中需要显示价格变化,我们加入购物车中的商品价格不是一成不变的,因此需要发送这条请求来查询此时的价格与加入购物车时的价格是否发生了变化,提供友好的提示,接下来我们来实现
2.2 提供查询接口显示价格变化
- 请求方式:get
- 请求路径:/sku/list
- 请求参数:sku的id集合
- 返回结果:sku的集合
controller:
@GetMapping("/sku/list/ids")
public ResponseEntity<List<Sku>> querySkuByIds(@RequestParam("ids") List<Long> ids){return ResponseEntity.ok(goodsService.querySkuBySpuIds(ids));}
service:
public List<Sku> querySkuBySpuIds(List<Long> ids) {List<Sku> skus = skuMapper.selectByIdList(ids);if(CollectionUtils.isEmpty(skus)){throw new LyException(ExceptionEnum.SKU_NOT_FOUND);}//查询库存List<Stock> stockList = stockMapper.selectByIdList(ids);if(CollectionUtils.isEmpty(stockList))throw new LyException(ExceptionEnum.STOCK_NOT_FOUND);//把stock变成一个map,其key:skuId,值:库存值Map<Long, Integer> stockMap = stockList.stream().collect(Collectors.toMap(Stock::getSkuId, Stock::getStock));skus.forEach(s ->s.setStock(stockMap.get(s.getId())));return skus;}
之后刷新页面,可以看到我们加入购物车的商品:
之后我们验证一下修改价格是不是会有有好的提示,我们去数据库中修改商品的价格,然后刷新页面:
发现价格变化已经有了有好的提示
3 已登陆购物车——Redis
3.1 搭建购物车服务
3.1.1 创建module
3.1.2 引入依赖
<?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><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.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>com.leyou.service</groupId><artifactId>ly-auth-common</artifactId><version>1.0.0-SNAPSHOT</version></dependency><dependency><groupId>com.leyou.common</groupId><artifactId>ly-common</artifactId><version>1.0.0-SNAPSHOT</version></dependency></dependencies>
</project>
3.1.3 配置文件
server:port: 8088
spring:application:name: cart-serviceredis:host: 192.168.124.128
eureka:client:service-url:defaultZone: http://127.0.0.1:10086/eurekaregistry-fetch-interval-seconds: 5instance:prefer-ip-address: trueip-address: 127.0.0.1
3.1.4 启动类
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class LyCartApplication {public static void main(String[] args) {SpringApplication.run(LyCartApplication.class, args);}
}
3.1.5 添加路由
3.2 鉴权
当我们在商品详情页选好商品添加购物车时,经过上面的分析我们知道,商品不是添加到数据库中,而是添加到Redis中,而Redis是以<key,value>的形式存储的,那key自然就是用户了,而我们的用户信息key存储在token中,所以要想实现添加购物车的功能,要先实现鉴权
那可能会有疑问,我们不是在网关写了鉴权吗,为什么不把用户信息传递过来?问题就在这里,我们网关微服务和购物车微服务不是一个微服务,属于不同的tomcat,不共享session,因此这样是不可行的
我们不仅要添加购物车,还有修改、删除购物车信息,每一步操作都需要用户信息key,所以每个地方都需要鉴权,显然在每个操作都做一次鉴权会使得代码很臃肿,我们需要的是统一解析而不是一次又一次的写,因此我们把这部分功能抽取出来,此时我们想到SpringMVC的拦截器——interceptor,通过拦截器我们可以统一处理每个请求,然后再进入controller层处理
3.2.1 引入公钥实现解析
ly:jwt:pubKeyPath: H:/javacode/idea/rsa/rsa.pub # 公钥地址cookieName: LY_TOKEN
3.2.2 读取公钥
@Data
@ConfigurationProperties(prefix = "ly.jwt")
public class JwtProperties {private String pubKeyPath;// 公钥private String cookieName;private PublicKey publicKey; // 公钥@PostConstruct // 构造函数执行完毕后就执行public void init(){// 获取公钥和私钥try {this.publicKey = RsaUtils.getPublicKey(pubKeyPath);} catch (Exception e) {e.printStackTrace();}}
}
3.2.3 编写拦截器
分析:
- 解析完token后需要把user信息传递过去,那通过什么传递呢?
- 既然要传递,就要思考从拦截器到controller再到service有什么东西是共享的?——request、spring容器、thread
- request :可行,但是SpringMVC是不推荐这种方案的
- spring容器:会有线程安全问题,因为spring容器都单例的
- thread:可行,有一个容器threadlocal(线程域),是一个map结构,key是thread,value是存储的值,原理图:
- 既然要传递,就要思考从拦截器到controller再到service有什么东西是共享的?——request、spring容器、thread
@Slf4j
@EnableConfigurationProperties(JwtProperties.class)
public class UserInterceptor implements HandlerInterceptor {@Autowiredprivate JwtProperties prop;// threadLocal是一个map结构,key是thread,value是存储的值private static final ThreadLocal<UserInfo> tl = new ThreadLocal<>();@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {try {// 解析token -- 解析token要先获得cookieString token = CookieUtils.getCookieValue(request, prop.getCookieName());UserInfo user = JwtUtils.getInfoFromToken(token, prop.getPublicKey());// 保存user -- request和thread是“共享”的,所以可以把user放到这两个中tl.set(user); // key是不需要自己给定的,会自己获取return true;} catch (Exception e) {log.error("[购物车异常] 用户身份解析失败!", e);return false;}}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {tl.remove();// 用完之后要删除,否则数据越存越多}public static UserInfo getUser(){return tl.get();}
}
3.2.4 使得拦截器生效
new 是自己在创建对象,但是拦截器中要使用spring(@EnableConfigurationProperties(JwtProperties.class)
),如果使用spring,就不能自己创建,spring要想注入,必须是spring来创建,因此我们修改拦截器,把自动注入去掉,并修改成通过构造函数的方式来注入:
这样拦截器就生效了
3.3 购物车数据设计
3.3.1 购物车数据结构
当用户登录时,我们需要把购物车数据保存到后台,可以选择保存在数据库。但是购物车是一个读写频率很高的数据。因此我们这里选择读写效率比较高的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,值是购物车数据
3.3.2 实体类
@Data
public class Cart {private Long skuId;// 商品idprivate String title;// 标题private String image;// 图片private Long price;// 加入购物车时的价格private Integer num;// 购买数量private String ownSpec;// 商品规格参数
}
3.4 添加商品到购物车
3.4.1 controller
@PostMapping
public ResponseEntity<Void> addCart(@RequestBody Cart cart){cartService.addCart(cart);return ResponseEntity.status(HttpStatus.CREATED).build();
}
3.4.2 service
这里我们不访问数据库,而是直接操作Redis。
基本思路:
- 先查询之前的购物车数据
- 判断要添加的商品是否存在
- 存在:则直接修改数量后写回Redis
- 不存在:新建一条数据,然后写入Redis
代码:
public void addCart(Cart cart) {// 获取登录的用户 -- 从线程中获得UserInfo user = UserInterceptor.getUser();// redis存储的结构是一个Map<String,Map<String,String>>,第一个key是用户的key,第二个key是商品的key,value是商品信息String key = KEY_PREFIX + user.getId();String hashKey = cart.getSkuId().toString();BoundHashOperations<String, Object, Object> operation = redisTemplate.boundHashOps(key);if(operation.hasKey(hashKey)){// 如果存在 商品数量新增,新增之前先取出商品信息String json = operation.get(hashKey).toString();Cart cacheCart = JsonUtils.parse(json, Cart.class);cacheCart.setNum(cacheCart.getNum() + cart.getNum());operation.put(hashKey,JsonUtils.serialize(cacheCart));}else{// 如果不存在 新增operation.put(hashKey, JsonUtils.serialize(cart));}
}
结果:
3.5 查询购物车
我们新增商品到购物车,刷新页面却看不到商品信息,这是因为我们没有写查询购物车的实现
3.5.1 controller
@GetMapping("list")
public ResponseEntity<List<Cart>> queryCartList(){return ResponseEntity.ok(cartService.queryCartList());
}
3.5.2 service
public List<Cart> queryCartList() {// 获取登录的用户 -- 从线程中获得UserInfo user = UserInterceptor.getUser();String key = KEY_PREFIX + user.getId();if(!redisTemplate.hasKey(key)){throw new LyException(ExceptionEnum.SKU_NOT_FOUND);}// 获取登录用户的所有购物车BoundHashOperations<String, Object, Object> operation = redisTemplate.boundHashOps(key);List<Cart> carts = operation.values().stream().map(o -> JsonUtils.parse(o.toString(), Cart.class)).collect(Collectors.toList());return carts;
}
3.6 修改购物车中商品数量
3.6.1 controller
@PutMappingpublic ResponseEntity<Void> updateCartNum(@RequestParam("id") Long skuId,@RequestParam("num") Integer num){cartService.updateCartNum(skuId, num);return ResponseEntity.status(HttpStatus.NO_CONTENT).build();}
3.6.2 service
public void updateCartNum(Long skuId, Integer num) {// 获取登录的用户 -- 从线程中获得UserInfo user = UserInterceptor.getUser();String key = KEY_PREFIX + user.getId();// 获取登录用户的所有购物车BoundHashOperations<String, Object, Object> operation = redisTemplate.boundHashOps(key);// 查询String json = operation.get(skuId.toString()).toString();Cart cart = JsonUtils.parse(json, Cart.class);cart.setNum(num);// 写回redisoperation.put(skuId.toString(), JsonUtils.serialize(cart));
}
3.7 删除购物车中商品
3.7.1 controller
@DeleteMapping("{skuId}")
public ResponseEntity<Void> deleteCart(@PathVariable("skuId") Long skuId){cartService.deleteCart(skuId);return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
}
3.7.2 service
public void deleteCart(Long skuId) {// 获取登录的用户 -- 从线程中获得UserInfo user = UserInterceptor.getUser();String key = KEY_PREFIX + user.getId();// 删除redisTemplate.opsForHash().delete(key, skuId.toString());
}
4 待优化
- 商品的下架功能
- 商品下架要提供清空下架商品的功能实现
- 商品移入收藏夹
- 需要单独的表结构,类似于购物车的数据结构,只是不需要商品数量
- 商品的优惠功能:需要一个单独的微服务,因为优惠功能略复杂,关键问题在于优惠条件,比如:
- 用户权限:哪些用户可享受优惠,哪些用户不可享受优惠
- 商品限制:哪些商品可以用,那些商品不可以用
- 价格限制:满足一定的购买条件才可以享受优惠,比如购买一定量的金额,或者一定的数量
- 优惠方案:满减或者打折
- 商品组合:某几个商品组合起来可享受优惠
- 店铺问题:哪些店铺可用,哪些店铺不可用,是否可以跨店铺享受优惠
- 结算问题:解算时候优惠的计算以及发生退款情况时如何处理
- …
- 登录后购物车合并
- 如果登录:
- 首先检查用户的LocalStorage中是否有购物车信息,
- 如果有,则提交到后台保存,
- 清空LocalStorage
- 如果未登录,直接查询即可
- 如果登录:
这篇关于乐优商城:笔记(十四):购物车微服务:LyCartApplication的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!