文章目录
1. 搭建购物车微服务
-
右键 leyou 项目 --> New Module --> Maven --> Next
-
填写项目信息 --> Next
-
填写保存的位置 --> Finish
-
添加依赖
<?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-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <groupId>com.leyou.cart</groupId> <artifactId>leyou-cart</artifactId> <version>1.0-SNAPSHOT</version> <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.item</groupId> <artifactId>leyou-item-interface</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>com.leyou.common</groupId> <artifactId>leyou-common</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies> </project>
-
编写配置文件 application.yaml
server: port: 8088 spring: application: name: cart-service redis: host: 192.168.222.132 eureka: 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 @SpringBootApplication public 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. 购物车功能分析
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. 未登录购物车
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. 已登录购物车
4.1 流程分析
- 登录后,加入购物车会发送请求到后台,首先会经过 Zuul 网关 LoginFilter 登录验证,如果通过则请求被转发到 leyou-cart
- 由于加入购物车还需要用户信息,所以我们需要解析用户信息并保存起来,以便后续的接口可以使用。这里用到 SpringMVC 的前置 preHandle,并将解析后的 UserInfo 放入 ThreadLocal 中。
4.2 实现解析用户信息
-
在 leyou-cart 中添加依赖
<dependency> <groupId>com.leyou.auth</groupId> <artifactId>leyou-auth-common</artifactId> <version>1.0-SNAPSHOT</version> </dependency>
-
在 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<UserInfo> 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 信息
-
定义配置类,注册拦截器
@Configuration public class MvcConfig implements WebMvcConfigurer { @Autowired private LoginInterceptor loginInterceptor; @Override /** * 重写接口中的 addInterceptors 方法,添加自定义拦截器 */ public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor).addPathPatterns("/**"); } }
4.3 后台购物车设计
当用户登录时,我们需要把购物车数据保存 Redis 中,那应该采用怎样的数据结构保存呢?
- 首先每个用户应该有独立的购物车,因此购物车应该以用户的作为 key 来存储,value 是用户的所有购物车信息。
- 我们对购物车中的商品进行增、删、改操作,基本都需要根据商品 id 进行判断,为了方便后期处理,我们的购物车也应该是 k-v 结构,key 是商品 id,value 才是这个商品的购物车信息。
综上所述,我们的购物车结构是一个双层 Map,也就对应着 Redis 中的 Hash 数据结构。
Map<String,Map<String,String>>
- 第一层 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<Sku> 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
@Controller
public class CartController {
@Autowired
private CartService cartService;
/**
* 添加购物车
* @param cart
* @return
*/
@PostMapping
public ResponseEntity<Void> addCart(@RequestBody Cart cart) {
cartService.addCart(cart);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
}
4.4.5 Service
在 leyou-cart 中添加 CartService
@Service
public 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<String, Object, Object> 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
*/
@GetMapping
public ResponseEntity<List<Cart>> queryCart() {
List<Cart> carts = cartService.queryCart();
if(carts == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(carts);
}
4.5.3 Service
在 CartService 中添加 queryCart 方法
/**
* 查询购物车
*
* @return
*/
public List<Cart> queryCart() {
// 获取用户信息
UserInfo userInfo = LoginInterceptor.getUserInfo();
// 判断用户是否存在购物车
if (!stringRedisTemplate.hasKey(KEY_PREFIX + userInfo.getId())) {
return null;
}
// 操作 Hash 数据
BoundHashOperations<String, Object, Object> boundHashOps = stringRedisTemplate.boundHashOps(KEY_PREFIX + userInfo.getId());
// 获取所有购物车中商品
List<Object> jsonCarts = boundHashOps.values();
// 判断购物车中是否有商品
if (CollectionUtils.isEmpty(jsonCarts)) {
return null;
}
ArrayList<Cart> carts = new ArrayList<>();
for (Object jsonCart : jsonCarts) {
Cart cart = JsonUtils.parse(jsonCart.toString(), Cart.class);
carts.add(cart);
}
return carts;
}
4.5.4 测试
在登录后,添加商品到购物车后,查询到的购物车
4.6 修改数量
4.6.1 前端发起请求
4.6.2 Controller
/**
* 修改购物车
*
* @return
*/
@PutMapping
public ResponseEntity<Void> updateCart(@RequestBody Cart cart) {
cartService.updateCart(cart);
return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
}
4.6.3 Service
/**
* 修改购物车
*
* @return
*/
public void updateCart(Cart cart) {
// 获取用户信息
UserInfo userInfo = LoginInterceptor.getUserInfo();
// 操作 Hash 数据
BoundHashOperations<String, Object, Object> 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));
}
4.7 删除商品
4.7.1 前端发起请求
4.7.2 Controller
/**
* 删除购物车
*
* @param skuId
* @return
*/
@DeleteMapping("{skuId}")
public ResponseEntity<Void> deleteCart(@PathVariable("skuId") String skuId) {
cartService.deleteCart(skuId);
return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
}
4.7.3 Service
/**
* 删除购物车
*
* @param skuId
* @return
*/
public void deleteCart(String skuId) {
// 获取用户信息
UserInfo userInfo = LoginInterceptor.getUserInfo();
// 操作 Hash 数据
BoundHashOperations<String, Object, Object> boundHashOps = stringRedisTemplate.boundHashOps(KEY_PREFIX + userInfo.getId());
// 删除商品
boundHashOps.delete(skuId);
}
转载:https://blog.csdn.net/bm1998/article/details/106066339