一、创建微服务

image-20211126220418891

新增 host

1
192.168.56.10 cart.gulimall.com

image-20211126220838895

静态资源

image-20211126222653017

修改资源路径

image-20211128222654064

VO 编写

  • 购物项 vo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class CartItemVo {

private Long skuId;

//是否选中
private Boolean check = true;

//标题
private String title;

//图片
private String image;

//商品套餐属性
private List<String> skuAttrValues;

//价格
private BigDecimal price;

//数量
private Integer count;

//总价
private BigDecimal totalPrice;

/**
* 当前购物车项总价等于单价x数量
* @return
*/
public BigDecimal getTotalPrice() {
return price.multiply(new BigDecimal(count));
}

public void setTotalPrice(BigDecimal totalPrice) {
this.totalPrice = totalPrice;
}

购物车 vo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
public class CartVo {

/**
* 购物车子项信息
*/
List<CartItemVo> items;

/**
* 商品数量
*/
private Integer countNum;

/**
* 商品类型数量
*/
private Integer countType;

/**
* 商品总价
*/
private BigDecimal totalAmount;

/**
* 减免价格
*/
private BigDecimal reduce = new BigDecimal("0.00");

public List<CartItemVo> getItems() {
return items;
}

public void setItems(List<CartItemVo> items) {
this.items = items;
}

//总数量=遍历每个购物项总和
public Integer getCountNum() {
int count=0;
if (items != null && items.size() > 0) {
for (CartItemVo item : items) {
count += item.getCount();
}
}
return count;
}

public void setCountNum(Integer countNum) {
this.countNum = countNum;
}

//商品类型数量=遍历所有商品类型和
public Integer getCountType() {
int count=0;
if (items != null && items.size() > 0) {
for (CartItemVo item : items) {
count += 1;
}
}
return count;
}

public void setCountType(Integer countType) {
this.countType = countType;
}

//总价为单个购物项总价-优惠
public BigDecimal getTotalAmount() {
BigDecimal total = new BigDecimal(0);
if (items != null && items.size() > 0) {
for (CartItemVo item : items) {
total.add(item.getTotalPrice());
}
}
total.subtract(reduce);
return total;
}

public void setTotalAmount(BigDecimal totalAmount) {
this.totalAmount = totalAmount;
}

public BigDecimal getReduce() {
return reduce;
}

public void setReduce(BigDecimal reduce) {
this.reduce = reduce;
}
}

二、ThreadLocal 用户身份鉴别

1、用户身份鉴别方式

​ 参考京东,在点击购物车时,会为临时用户生成一个nameuser-keycookie临时标识,过期时间为一个月,如果手动清除user-key,那么临时购物车的购物项也被清除,所以 user-key 是用来标识和存储临时购物车数据的

2、使用 ThreadLocal 进行用户身份鉴别信息传递

  • 在调用购物车的接口前,先通过 session 信息判断是否登录,并分别进行用户身份信息的封住,并把user-key放在 cookie 中
  • 这个功能使用拦截器进行完成
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
public class CartInterceptor implements HandlerInterceptor {

public static ThreadLocal<UserInfoTo> threadLocal = new ThreadLocal<>();

/**
* 目标方法执行之前
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession();
MemberResponseVo memberResponseVo = (MemberResponseVo) session.getAttribute(AuthServerConstant.LOGIN_USER);
UserInfoTo userInfoTo = new UserInfoTo();
// 1. 用户已经登录,设置userId
if(memberResponseVo!=null){
userInfoTo.setUserId(memberResponseVo.getId());
}


Cookie[] cookies = request.getCookies();
if(cookies.length > 0){
for (Cookie cookie : cookies) {
// 2. 如果cookie 中已经有user-Key,则直接设置
if(cookie.getName().equals(CartConstant.TEMP_USER_COOKIE_NAME)){
userInfoTo.setUserKey(cookie.getValue());
userInfoTo.setTempUser(true);
}
}
}

// 如果cookie没有user-Key,我们通过uuid生成user-key
if(StringUtils.isEmpty(userInfoTo.getUserKey())){
String uuid = UUID.randomUUID().toString();
userInfoTo.setUserKey(uuid);
}

// 4. 将用户身份认证信息放入到threadlocal进行传递
threadLocal.set(userInfoTo);
return true;
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
UserInfoTo userInfoTo = threadLocal.get();
// 如果Cookie中没有user-key,我们为其生成
if(!userInfoTo.getTempUser()){
Cookie cookie = new Cookie(CartConstant.TEMP_USER_COOKIE_NAME, userInfoTo.getUserKey());
cookie.setDomain("gulimall.com");
cookie.setMaxAge(CartConstant.TEMP_USER_COOKIE_TIMEOUT);
response.addCookie(cookie);
}
}
}

三、添加商品到购物车

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 添加商品到购物车
* RedirectAttributes.addFlashAttribute():将数据放在session中,可以在页面中取出,但是只能取一次
* RedirectAttributes.addAttribute():将数据拼接在url后面,?skuId=xxx
* @return
*/
@RequestMapping("/addCartItem")
public String addCartItem(@RequestParam("skuId") Long skuId, @RequestParam("num") Integer num, RedirectAttributes attributes) {
cartService.addCartItem(skuId, num);
attributes.addAttribute("skuId", skuId);
//为了防止成功页刷新可以重复提交添加商品,我们不直接转到成功页
return "redirect:http://cart.gulimall.com/addCartItemSuccess";
}

@RequestMapping("/addCartItemSuccess")
public String addCartItemSuccess(@RequestParam("skuId") Long skuId,Model model) {
CartItemVo cartItemVo = cartService.getCartItem(skuId);
model.addAttribute("cartItem", cartItemVo);
return "success";
}
  • 若当前商品已经存在购物车,只需增添数量
  • 否则需要查询商品购物项所需信息,并添加新商品至购物车
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public CartItemVo addCartItem(Long skuId, Integer num) {
//获取当前以当前用户标识为key的hash的操作
BoundHashOperations<String, Object, Object> ops = getCartItemOps();
// 判断当前商品是否已经存在购物车
String cartJson = (String) ops.get(skuId.toString());
// 1 已经存在购物车,将数据取出并添加商品数量
if (!StringUtils.isEmpty(cartJson)) {
//1.1 将json转为对象并将count+
CartItemVo cartItemVo = JSON.parseObject(cartJson, CartItemVo.class);
cartItemVo.setCount(cartItemVo.getCount() + num);
//1.2 将更新后的对象转为json并存入redis
String jsonString = JSON.toJSONString(cartItemVo);
ops.put(skuId.toString(), jsonString);
return cartItemVo;
} else {
CartItemVo cartItemVo = new CartItemVo();
// 2 未存在购物车,则添加新商品
CompletableFuture<Void> future1 = CompletableFuture.runAsync(() -> {
//2.1 远程查询sku基本信息
R info = productFeignService.info(skuId);
SkuInfoVo skuInfo = info.getData("skuInfo", new TypeReference<SkuInfoVo>() {
});
cartItemVo.setCheck(true);
cartItemVo.setCount(num);
cartItemVo.setImage(skuInfo.getSkuDefaultImg());
cartItemVo.setPrice(skuInfo.getPrice());
cartItemVo.setSkuId(skuId);
cartItemVo.setTitle(skuInfo.getSkuTitle());
}, executor);

//2.2 远程查询sku属性组合信息
CompletableFuture<Void> future2 = CompletableFuture.runAsync(() -> {
List<String> attrValuesAsString = productFeignService.getSkuSaleAttrValuesAsString(skuId);
cartItemVo.setSkuAttrValues(attrValuesAsString);
}, executor);

try {
CompletableFuture.allOf(future1, future2).get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
//2.3 将该属性封装并存入redis,登录用户使用userId为key,否则使用user-key
String toJSONString = JSON.toJSONString(cartItemVo);
ops.put(skuId.toString(), toJSONString);
return cartItemVo;
}
}

四、 获取购物车

  • 若用户未登录,则直接使用user-key获取购物车数据
  • 否则使用userId获取购物车数据,并将user-key对应临时购物车数据与用户购物车数据合并,并删除临时购物车
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@RequestMapping("/cart.html")
public String getCartList(Model model) {
CartVo cartVo=cartService.getCart();
model.addAttribute("cart", cartVo);
return "cartList";
}

@Override
public CartVo getCart() {
CartVo cartVo = new CartVo();
UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
//1 用户未登录,直接通过user-key获取临时购物车
List<CartItemVo> tempCart = getCartByKey(CartConstant.CART_PREFIX + userInfoTo.getUserKey());
if (StringUtils.isEmpty(userInfoTo.getUserId())) {
List<CartItemVo> cartItemVos = tempCart;
cartVo.setItems(cartItemVos);
}else {
//2 用户登录
//2.1 查询userId对应的购物车
List<CartItemVo> userCart = getCartByKey(CartConstant.CART_PREFIX + userInfoTo.getUserId());
//2.2 查询user-key对应的临时购物车,并和用户购物车合并
if (tempCart!=null&&tempCart.size()>0){
BoundHashOperations<String, Object, Object> ops = redisTemplate.boundHashOps(CartConstant.CART_PREFIX + userInfoTo.getUserId());
for (CartItemVo cartItemVo : tempCart) {
userCart.add(cartItemVo);
//2.3 在redis中更新数据
addCartItem(cartItemVo.getSkuId(), cartItemVo.getCount());
}
}
cartVo.setItems(userCart);
//2.4 删除临时购物车数据
redisTemplate.delete(CartConstant.CART_PREFIX + userInfoTo.getUserKey());
}

return cartVo;
}

五、选中购物车项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RequestMapping("/checkCart")
public String checkCart(@RequestParam("isChecked") Integer isChecked,@RequestParam("skuId")Long skuId) {
cartService.checkCart(skuId, isChecked);
return "redirect:http://cart.gulimall.com/cart.html";
}

//修改skuId对应购物车项的选中状态
@Override
public void checkCart(Long skuId, Integer isChecked) {
BoundHashOperations<String, Object, Object> ops = getCartItemOps();
String cartJson = (String) ops.get(skuId.toString());
CartItemVo cartItemVo = JSON.parseObject(cartJson, CartItemVo.class);
cartItemVo.setCheck(isChecked==1);
ops.put(skuId.toString(),JSON.toJSONString(cartItemVo));
}

六、修改购物项数量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RequestMapping("/countItem")
public String changeItemCount(@RequestParam("skuId") Long skuId, @RequestParam("num") Integer num) {
cartService.changeItemCount(skuId, num);
return "redirect:http://cart.gulimall.com/cart.html";
}

@Override
public void changeItemCount(Long skuId, Integer num) {
BoundHashOperations<String, Object, Object> ops = getCartItemOps();
String cartJson = (String) ops.get(skuId.toString());
CartItemVo cartItemVo = JSON.parseObject(cartJson, CartItemVo.class);
cartItemVo.setCount(num);
ops.put(skuId.toString(),JSON.toJSONString(cartItemVo));
}

七、删除购物车项

1
2
3
4
5
6
7
8
9
10
11
@RequestMapping("/deleteItem")
public String deleteItem(@RequestParam("skuId") Long skuId) {
cartService.deleteItem(skuId);
return "redirect:http://cart.gulimall.com/cart.html";
}

@Override
public void deleteItem(Long skuId) {
BoundHashOperations<String, Object, Object> ops = getCartItemOps();
ops.delete(skuId.toString());
}