文章目录
- 1. 搭建购物车微服务
- 2. 购物车功能分析
- 2.1 需求分析
- 2.2 技术选型
- 2.3 流程图
- 3. 未登录购物车
- 3.1 购物车数据结构
- 3.2 获取购物数量
- 3.3 添加购物车
- 3.4 渲染购物车页面
- 3.4.1 封装判断用户的登录状态方法
- 3.4.2 查询购物车
- 3.4.3 渲染页面
- 3.5 修改数量
- 3.6 删除商品
- 4. 已登录购物车
- 4.1 流程分析
- 4.2 实现解析用户信息
- 4.3 后台购物车设计
- 4.4 添加购物车
- 4.4.1 前端发起请求
- 4.4.2 实体类
- 4.4.3 FeignClient
- 4.4.4 Controller
- 4.4.5 Service
- 4.4.6 最终目录结构
- 4.4.7 测试
- 4.5 查询购物车
- 4.5.1 前端发起请求
- 4.5.2 Controller
- 4.5.3 Service
- 4.5.4 测试
- 4.6 修改数量
- 4.6.1 前端发起请求
- 4.6.2 Controller
- 4.6.3 Service
- 4.7 删除商品
- 4.7.1 前端发起请求
- 4.7.2 Controller
- 4.7.3 Service
右键 leyou 项目 > New Module > Maven > Next
填写项目信息 > Next
填写保存的位置 > Finish
添加依赖
leyou com.leyou.parent 1.0-SNAPSHOT 4.0.0 com.leyou.cart leyou-cart 1.0-SNAPSHOT org.springframework.boot spring-boot-starter-web org.springframework.cloud spring-cloud-starter-netflix-eureka-client org.springframework.cloud spring-cloud-starter-openfeign org.springframework.boot spring-boot-starter-data-redis com.leyou.item leyou-item-interface 1.0-SNAPSHOT com.leyou.common leyou-common 1.0-SNAPSHOT
编写配置文件 application.yaml
server: port: 8088spring: application: name: cart-service redis: host: 192.168.222.132eureka: client: service-url: defaultZone: http://localhost:10086/eureka registry-fetch-interval-seconds: 10 instance: lease-renewal-interval-in-seconds: 5 lease-expiration-duration-in-seconds: 15
编写启动类
@EnableDiscoveryClient@EnableFeignClients@SpringBootApplicationpublic class LeyouCartApplicaion { public static void main(String[] args) { SpringApplication.run(LeyouCartApplicaion.class, args); }}
最终目录结构
在 leyou-gateway 的 applicaton.yaml 中添加路由配置
zuul: prefix: /api routes: item-service: /item/** search-service: /search/** user-service: /user/** auth-service: /auth/** cart-service: /cart/**
2.1 需求分析
- 用户可以在登录状态下将商品添加到购物车
- 用户可以在未登录状态下将商品添加到购物车
- 用户可以使用购物车一起结算下单
- 用户可以查询自己的购物车
- 用户可以在购物车中修改购买商品的数量。
- 用户可以在购物车中删除商品。
2.2 技术选型
用户在登录状态下将商品添加到购物车,数据应该保存在哪里呢?
- MySQL:在大量数据时,MySQL 的效率会显著降低,不推荐使用。
- Redis:在大量数据时,Redis 数据放在内存中,大小有限,不推荐使用。
- MongoDB:MongoDB 是文档型数据库,它保存在硬盘上,在大量数据时,效率很高,推荐使用。
注意:本项目中还是使用的 Redis,但更推荐 MongoDB。
用户在未登录状态下将商品添加到购物车,数据应该保存在哪里呢?
- COOKIE:COOKIE 大小有限制(4 KB),同一个域名下的总 COOKIE 数量也有限制(20 个),并且每次请求携带 COOKIE 会占用大量带宽,不推荐使用。
- Web SQL:使用有些麻烦,还需要写 SQL,不推荐使用。
- Local Storage:主要是用来作为本地存储来使用的,解决了 COOKIE 存储空间不足的问题,Local Storage 中一般浏览器支持的是 5M 大小,推荐使用。
2.3 流程图
这幅图主要描述了两个功能:添加购物车、查询购物车。
添加购物车:
- 判断是否登录
- 是:则添加商品到 Redis 中
- 否:则添加商品到本地的 Local Storage
- 判断是否登录
查询购物车列表:
- 判断是否登录
- 否:直接查询 Local Storage 中数据并展示
- 是:已登录,则需要先看本地是否有数据,
- 有:需要提交到后台添加到 Redis,合并数据,而后查询
- 否:直接去后台查询 Redis,而后返回
- 判断是否登录
3.1 购物车数据结构
首先分析一下未登录购物车的数据结构,下面是需要展示的数据:
因此每一个购物车信息,都是一个对象:
{ skuId:2131241, title:"小米6", image:"", price:190000, num:1, ownSpec:"{"机身颜色":"陶瓷黑尊享版","内存":"6GB","机身存储":"128GB"}"}
另外,购物车中不止一条数据,因此最终会是对象的数组。
3.2 获取购物数量
添加购物车需要知道购物的数量,所以我们需要获取数量大小。
我们在 item.html 中定义 num,保存数量
然后将 num 与页面的 input 框绑定,同时给 + 和 - 的按钮绑定事件
编写方法
3.3 添加购物车
绑定点击事件 addCart 方法
addCart 方法中判断用户的登录状态,未登录状态下保存商品在浏览器本地的 Local Storage 中,然后跳转到购物车页面
addCart(){ //判断有没有登陆 ly.http.get("/auth/verify").then(res=>{ // 已登录发送信息到后台,保存到redis中 }).catch(()=>{ // 未登录保存在浏览器本地的localStorage中 // 1、查询本地购物车 let carts = ly.store.get("LY_CART") || []; 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, ownSpec: this.sku.ownSpec, num: this.num }; carts.push(cart); } // 把carts写回localstorage ly.store.set("LY_CART", carts); // 跳转 window.location.href = "http://www.leyou.com/cart.html"; });}
点击加入购物车后,查看 Local Storage
3.4 渲染购物车页面
3.4.1 封装判断用户的登录状态方法
因为查询购物车也会用到判断用户的登录状态,因此我们将这个方法封装到 common.js 中
修改 item.html 中的方法
3.4.2 查询购物车
修改 cart.html,页面加载时,就应该去查询购物车
3.4.3 渲染页面
略,交给前端吧,最终效果如下:
3.5 修改数量
我们给页面的 + 和 -绑定点击事件
点击事件中修改 num 的值
3.6 删除商品
给删除按钮绑定事件
点击事件中删除商品
4.1 流程分析
4.2 实现解析用户信息
在 leyou-cart 中添加依赖
com.leyou.auth leyou-auth-common 1.0-SNAPSHOT
在 application.yaml 中添加 JWT 配置
leyou: jwt: pubKeyPath: D:\tmp\rsa\\rsa.pub # 公钥地址 COOKIEName: LY_TOKEN # COOKIE的名称
创建 JWT 属性读取类
@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; }}
编写拦截器
@Component@EnableConfigurationProperties(JwtProperties.class)public class LoginInterceptor extends HandlerInterceptorAdapter { @Autowired private JwtProperties jwtProperties; // 定义一个线程域,存放用户信息 private static final ThreadLocal THREAD_LOCAL = new ThreadLocal(); /** * 获取用户信息 * @return */ public static UserInfo getUserInfo() { return THREAD_LOCAL.get(); } /** * 解析 JWT,并将用户信息存放入 ThreadLocal * @param request * @param response * @param handler * @return * @throws Exception */ @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 userInfo = JwtUtils.getInfoFromToken(token, jwtProperties.getPublicKey()); // 放入线程域 THREAD_LOCAL.set(userInfo); return true; } catch (Exception e){ // 抛出异常,登录,返回 401 response.setStatus(HttpStatus.UNAUTHORIZED.value()); return false; } } /** * 响应视图后,释放 ThreadLocal * @param request * @param response * @param handler * @param ex * @throws Exception */ @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { THREAD_LOCAL.remove(); }}
注意:
- 这里我们使用了 ThreadLocal 来存储查询到的用户信息,线程内共享,因此请求到达 Controller 后可以共享 UserInfo
- 并且对外提供了静态的方法 getLoginUser() 来获取 UserInfo 信息
定义配置类,注册拦截器
@Configurationpublic class MvcConfig implements WebMvcConfigurer { @Autowired private LoginInterceptor loginInterceptor; @Override /** * 重写接口中的 addInterceptors 方法,添加自定义拦截器 */ public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor).addPathPatterns("/**"); }}
4.3 后台购物车设计
当用户登录时,我们需要把购物车数据保存 Redis 中,那应该采用怎样的数据结构保存呢?
综上所述,我们的购物车结构是一个双层 Map,也就对应着 Redis 中的 Hash 数据结构。
Map
- 第一层 Map,Key 是用户 id
- 第二层 Map,Key 是购物车中商品 id,值是购物车数据
4.4 添加购物车
4.4.1 前端发起请求
修改 item.html,已登录情况下,向后台发送 POST 请求添加购物车
4.4.2 实体类
在 leyou-cart 中添加购物车实体类 Cart
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;// 商品规格参数 // 省略 getter、setter 方法}
4.4.3 FeignClient
在添加购物车时,需要根据 skuId 去查询 Sku 信息,我们会在 leyou-cart 中远程调用 leyou-item 提供的对应接口。
在 leyou-item-service 中 SpuController 添加方法
/** * 通过 skuId 查询 Sku * * @param skuId * @return */@GetMapping("sku")public ResponseEntity querySkuBySkuId(@RequestParam("skuId") Long skuId) { Sku sku = spuService.querySkuBySkuId(skuId); if (sku == null) { return ResponseEntity.notFound().build(); } return ResponseEntity.ok(sku);}
在 leyou-item-service 中 SpuService 添加方法
/** * 通过 skuId 查询 Sku * * @param skuId * @return */public Sku querySkuBySkuId(Long skuId) { Sku sku = skuMapper.selectByPrimaryKey(skuId); return sku;}
在 leyou-item-interface 中 SpuApi 添加接口
/** * 通过 skuId 查询 Sku * * @param skuId * @return */@GetMapping("sku")public Sku querySkuBySkuId(@RequestParam("skuId") Long skuId);
在 leyou-cart 中添加 SpuClient
@FeignClient("item-service")public interface SpuClient extends SpuApi { }
4.4.4 Controller
在 leyou-cart 中添加 CartController
@Controllerpublic class CartController { @Autowired private CartService cartService; /** * 添加购物车 * @param cart * @return */ @PostMapping public ResponseEntity addCart(@RequestBody Cart cart) { cartService.addCart(cart); return ResponseEntity.status(HttpStatus.CREATED).build(); }}
4.4.5 Service
在 leyou-cart 中添加 CartService
@Servicepublic class CartService { @Autowired private StringRedisTemplate stringRedisTemplate; @Autowired private SpuClient spuClient; private static final String KEY_PREFIX = "leyou:cart:uid:"; /** * 添加购物车 * * @param cart * @return */ public void addCart(Cart cart) { // 获取用户信息 UserInfo userInfo = LoginInterceptor.getUserInfo(); // 操作 Hash 数据 BoundHashOperations boundHashOps = stringRedisTemplate.boundHashOps(KEY_PREFIX + userInfo.getId()); // 判断购物车是否有该商品 if (boundHashOps.hasKey(cart.getSkuId().toString())) { // 有,更改该商品数量 String jsonCart = boundHashOps.get(cart.getSkuId().toString()).toString(); Cart cart1 = JsonUtils.parse(jsonCart, Cart.class); cart1.setNum(cart1.getNum() + cart.getNum()); boundHashOps.put(cart1.getSkuId().toString(), JsonUtils.serialize(cart1)); } else { // 无,新增该商品 Sku sku = spuClient.querySkuBySkuId(cart.getSkuId()); cart.setUserId(userInfo.getId()); cart.setTitle(sku.getTitle()); cart.setImage(sku.getImages().split(",")[0]); cart.setOwnSpec(sku.getOwnSpec()); boundHashOps.put(cart.getSkuId().toString(),JsonUtils.serialize(cart)); } }}
4.4.6 最终目录结构
4.4.7 测试
在登录后,添加商品到购物车后,查看 Redis
4.5 查询购物车
4.5.1 前端发起请求
修改 item.html,已登录情况下,向后台发送 GET 请求添加购物车
4.5.2 Controller
在 CartController 中添加 queryCart 方法
/** * 查询购物车 * @return */@GetMappingpublic ResponseEntity 在 CartService 中添加 queryCart 方法 /** * 查询购物车 * * @return */public List queryCart() { // 获取用户信息 UserInfo userInfo = LoginInterceptor.getUserInfo(); // 判断用户是否存在购物车 if (!stringRedisTemplate.hasKey(KEY_PREFIX + userInfo.getId())) { return null; } // 操作 Hash 数据 BoundHashOperations boundHashOps = stringRedisTemplate.boundHashOps(KEY_PREFIX + userInfo.getId()); // 获取所有购物车中商品 List jsonCarts = boundHashOps.values(); // 判断购物车中是否有商品 if (CollectionUtils.isEmpty(jsonCarts)) { return null; } ArrayList carts = new ArrayList(); for (Object jsonCart : jsonCarts) { Cart cart = JsonUtils.parse(jsonCart.toString(), Cart.class); carts.add(cart); } return carts;} 在登录后,添加商品到购物车后,查询到的购物车 /** * 修改购物车 * * @return */@PutMappingpublic ResponseEntity updateCart(@RequestBody Cart cart) { cartService.updateCart(cart); return ResponseEntity.status(HttpStatus.NO_CONTENT).build();} /** * 修改购物车 * * @return */public void updateCart(Cart cart) { // 获取用户信息 UserInfo userInfo = LoginInterceptor.getUserInfo(); // 操作 Hash 数据 BoundHashOperations boundHashOps = stringRedisTemplate.boundHashOps(KEY_PREFIX + userInfo.getId()); // 更改该商品数量 String jsonCart = boundHashOps.get(cart.getSkuId().toString()).toString(); Cart cart1 = JsonUtils.parse(jsonCart, Cart.class); cart1.setNum(cart.getNum()); boundHashOps.put(cart1.getSkuId().toString(), JsonUtils.serialize(cart1));} /** * 删除购物车 * * @param skuId * @return */@DeleteMapping("{skuId}")public ResponseEntity deleteCart(@PathVariable("skuId") String skuId) { cartService.deleteCart(skuId); return ResponseEntity.status(HttpStatus.NO_CONTENT).build();} /** * 删除购物车 * * @param skuId * @return */public void deleteCart(String skuId) { // 获取用户信息 UserInfo userInfo = LoginInterceptor.getUserInfo(); // 操作 Hash 数据 BoundHashOperations boundHashOps = stringRedisTemplate.boundHashOps(KEY_PREFIX + userInfo.getId()); // 删除商品 boundHashOps.delete(skuId);}
queryCart() { List carts = cartService.queryCart(); if(carts == null) { return ResponseEntity.notFound().build(); } return ResponseEntity.ok(carts);}
4.5.3 Service
4.5.4 测试
4.6 修改数量
4.6.1 前端发起请求
4.6.2 Controller
4.6.3 Service
4.7 删除商品
4.7.1 前端发起请求
4.7.2 Controller
4.7.3 Service