【Project】云商城 - 购物车服务

本文将详细介绍云商城的购物车服务。云商城完整项目介绍见文章 【Project】云商城

购物车服务

购物车服务 mall-cart 负责将用户挑选好的 SKU 添加到购物车中。添加后的效果如下:

image-20220115200411629

购物车服务需要实现的功能:

  • 登录用户可以添加购物车,并且在结算购物车前该信息一直保留
  • 临时用户(未登录用户)也可以添加购物车,并且该信息可以保留 30 天
  • 临时用户在 30 天内再次访问本网站仍然能看到之前添加过的购物车信息
  • 临时用户一旦登录,就会把其之前添加的商品一起合并到自己登录用户的购物车里

实现思路:

  • 为实现购物车信息一直保留,需要将购物车的信息一直存放在 Redis 中,并且开启 Reids 的持久化
  • 为实现临时用户功能,需要使用拦截器,判断每个访问本网站的用户是否已登录(Redis 中的 Session 是否存储了 loginUser 数据),如果没登录过就要为其设置一个唯一标识 user-key 并且以 Cookie 的形式保存在浏览器中 30 天。这样下次临时用户登录时仍然能获取其购物车数据

购物车前端页面

前端页面结构:

image-20220115212437700

配置过程不再赘述,可参考其他服务的配置方法。

数据模型设计

购物车模型 CartVo

image-20220115225049566

一个购物车 CartVo 对象中保存着多个购物项 CartItemVo,这些购物项以 List 的形式保存在购物车对象中:

1
2
3
4
5
6
7
8
public class CartVo {
/**
* 购物车子项信息
*/
List<CartItemVo> items;

// ....
}

同时,购物车对象中还需要保存商品数量商品类型数量商品总价减免价格信息。完整的购物车 CartVo 类定义:

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
90
91
92
93
94
95
96
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;
}

/**
* 购物车总价格需要自动计算,而不能手动指定
* 该对象被转换成 JSON 格式时,该方法就会被调用,从而更新
*/
public BigDecimal getTotalAmount() {
BigDecimal total = new BigDecimal(0);
if (items != null && items.size() > 0) {
for (CartItemVo item : items) {
total = total.add(item.getTotalPrice());
}
}
total = 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;
}
}

注意:购物车中的总价不应该人为设置,而应该自动计算:该对象被转换成 JSON 格式时,该方法就会被调用,从而更新购物车总价。

购物项模型 CartItemVo

image-20220115225127204

购物项 CartItemVo 以 SKU 为单位,包含了 skuId是否选中标题、图片、SKU 销售属性、价格、数量、总价等信息。完整定义:

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
90
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;

public Long getSkuId() {
return skuId;
}

public void setSkuId(Long skuId) {
this.skuId = skuId;
}

public Boolean getCheck() {
return check;
}

public void setCheck(Boolean check) {
this.check = check;
}

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 List<String> getSkuAttrValues() {
return skuAttrValues;
}

public void setSkuAttrValues(List<String> skuAttrValues) {
this.skuAttrValues = skuAttrValues;
}

public BigDecimal getPrice() {
return price;
}

public void setPrice(BigDecimal price) {
this.price = price;
}

public Integer getCount() {
return count;
}

public void setCount(Integer count) {
this.count = count;
}

/**
* 当前购物车项总价等于单价 x 数量
* 将计算总价的代码写在 getTotalPrice 里,这样每次转JSON存到Redis里时,就会调用get方法,就会重新计算总价
* @return
*/
public BigDecimal getTotalPrice() {
return price.multiply(new BigDecimal(count));
}

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

注意:购物项总价的计算同样是在 get 方法中,从在转 JSON 时重新计算总价。

购物车在 Redis 中的数据结构

购物车和购物项信息只保存到 Redis 中,不存储在数据库里

根据前面的定义,一个购物车包含了多个购物项。那么我们就可以采用 Redis 中的 HASH 结构保存购物车和购物项。这样做的好处是可以使用哈希的特性快速增删改查购物车中的某个购物项

在 Redis 中的保存结构可抽象为:Map<String, Map<String, Cart>>

  • 最外层的 key 为用户 id,因为每个用户拥有自己唯一的购物车,所有可以使用用户 id 区分不同的购物车。
  • 内层的 key 为该用户的购物车中保存的购物项 id。一个购物车中可以有多个购物项,使用 HASH 保存起来

该结构在 Redis 中具体为:userId : Hash(cartItemId : cartItem)

  • key:用户 id
  • value:HASH 结构(key : 购物车项 id,value:购物车项信息)

图示:

image-20220115191449802

在 Redis 中的实际保存效果如下:

image-20220115191209729

共有两个购物车,分别为在线购物车(登录用户)和离线购物车(临时用户)

  • key:用户 id
  • value:HASH 结构(key : 购物车项 id,value:购物车项信息)

临时用户身份识别

为了给临时用户也开通购物车功能 ,需要先能判断出当前请求是否已登陆过。如果没登录过就要为其设置一个唯一标识 user-key 并且以 Cookie 的形式保存在浏览器中 30 天。这样该临时用户在关闭浏览器后再次访问时,本网站仍然能识别出其身份,然后显示出该临时用户之前添加过的购物车信息。

首先需要完成临时用户身份识别功能。为了完成该功能,我们可以使用 Spring 的拦截器机制实现身份认证与业务代码的解耦合。

  1. 添加拦截器。在执行 Controller 层的方法之前,先进行拦截,判断当前请求是否已登录:
  • 如果已登录,则设置 userInfoTo.userId 为用户 id
  • 如果是临时用户,先判断 Cookie 中是否有 user-key
    • 如果有,代表该临时用户之前访问过本网站,则设置 userInfoTo.userKeyuser-key 对应的值
    • 如果没有,代表该临时用户是第一次访问本网站,则创建一个 Cookie: user-key : uuid,并返回给浏览器令其保存

同时,需要将 userInfoTo 保存到 ThreadLocal 里,在当前线程内共享,以便 Service 层能获取到该数据。拦截器的完整代码:

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
/**
* 在执行目标方法之前,先判断用户的登录状态。并封装传递给 Controller 目标请求
*/
public class CartInterceptor implements HandlerInterceptor {
// 在使用完毕数据后记得及时清理,否则会造成内存泄漏
public static ThreadLocal<UserInfoTo> threadLocal = new ThreadLocal<>();

/**
* 在目标方法执行之前拦截
* 该用户如果登录了就设置 userId,否则设置 userKey
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 从 Redis 中获取 Session 数据
HttpSession session = request.getSession();
// 从 Session 中获取 loginUser 信息:memberResponseVo
MemberResponseVo memberResponseVo = (MemberResponseVo) session.getAttribute(AuthServerConstant.LOGIN_USER);
// UserInfoTo 负责封装用户的 id 信息。
// 该数据将存储在 ThreadLocal 里,在当前线程内共享,以便 Service 层能获取到该数据
UserInfoTo userInfoTo = new UserInfoTo();

// 1. 如果用户已经登录,则设置 userInfoTo.userId 为用户 id
if (memberResponseVo != null){
userInfoTo.setUserId(memberResponseVo.getId());
}

// 2. 如果用户没登录,userInfoTo.userId 就是 Null,代表该用户是临时用户。使用 userKey 标识其身份
Cookie[] cookies = request.getCookies();
// 遍历每一个 Cookie,判断其中是否有 user-Key,如果有,说明当前临时用户之前就访问过本网站
// 否则说明其没有访问过本网站,就要为其生成一个 user-key = uuid
if (cookies != null && cookies.length > 0) {
for (Cookie cookie : cookies) {
// 如果浏览器中已经有 user-Key,代表该临时用户之前已经访问过本网站,
// 则直接设置 userInfoTo.userKey 为 Cookie 中之前存储过的 user-key 对应的值
if (cookie.getName().equals(CartConstant.TEMP_USER_COOKIE_NAME)) {
userInfoTo.setUserKey(cookie.getValue());
// 设置浏览器已有 user-key
userInfoTo.setTempUser(true);
}
}
}

// 3. 如果浏览器没有 user-key,我们通过 uuid 为其生成一个 user-key
if (StringUtils.isEmpty(userInfoTo.getUserKey())) {
String uuid = UUID.randomUUID().toString();
// 设置其 userKey 为 uuid
userInfoTo.setUserKey(uuid);
}
// 将该数据存储在 ThreadLocal 里,在当前线程内共享,以便 Service 层能获取到该数据
threadLocal.set(userInfoTo);
// 放行
return true;
}

/**
* 业务执行之后执行。分配临时用户的 Cookie,返给浏览器令其保存
* @param request
* @param response
* @param handler
* @param modelAndView
* @throws Exception
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// 获取 userInfoTo
UserInfoTo userInfoTo = threadLocal.get();
// 如果浏览器中没有 user-key,我们为其生成,并返回给浏览器保存该 Cookie
if (!userInfoTo.getTempUser()) {
Cookie cookie = new Cookie(CartConstant.TEMP_USER_COOKIE_NAME, userInfoTo.getUserKey());
// 设置父域
cookie.setDomain("yunmall.com");
// 设置过期时间,这样即使浏览器关闭也存在。就能做到30天内只要该用户再访问,还能看到之前添加的购物车信息
cookie.setMaxAge(CartConstant.TEMP_USER_COOKIE_TIMEOUT);
response.addCookie(cookie);
}
// 从 ThreadLocal 中删除 userInfoTo,以免造成内存内泄漏
threadLocal.remove(userInfoTo);
}
}
  1. 注册该拦截器,设置对所有请求拦截:
1
2
3
4
5
6
7
@Configuration
public class MallWebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new CartInterceptor()).addPathPatterns("/**");
}
}

这样就会在每个请求处理前先进行拦截,对请求进行判断其登录状态,并设置 userInfoTo 对象到 ThreadLocal 中,以便 Service 层能获取到该数据

  1. UserInfoTo 类用户保存用户的身份信息。已登录用户使用 userId 识别;临时用户使用 userKey 识别:
1
2
3
4
5
6
7
8
9
@Data
public class UserInfoTo {
private Long userId;
private String userKey;
/**
* 浏览器是否已有user-key
*/
private Boolean tempUser = false;
}

在 Cookie 中的保存效果:

image-20220116121146593

  • YUNSESSIONID:登录用户的 Cookie
  • user-key:临时用户的 Cookie(第一次访问时为其创建,在 30 天内将一直保存在浏览器中)

添加购物项

在商品详情页点击【加入购物车】按钮后,将发出请求 /addCartItem 访问购物车服务,在 URL 中传入要加入的 skuId 以及数量 num

Controller 层的 addCartItem() 方法会响应该请求:

  • 首先将该 skuId 对应的信息查出后封装成购物项 CartItemVo,并存放到 Redis 里该用户的购物车中
  • 添加完毕后,需要重定向到添加成功页 success.html。否则如果直接转发到 success.html 的话,重复刷新页面后该方法会重复执行,导致购物项的数量一直增加。因此必须要重定向到 success.html 页面,并且在 URL 中带上一个 skuId 参数。具体做法是再添加一个 addCartItemSuccess() 方法,该方法将根据 URL 中的 skuId 重新去 Redis 中查一遍购物项信息并存到请求域中,然后转发到 success.html 页面。相当于 addCartItemSuccess() 方法做了一个中转,防止重复提交购物项。
  • 这样即使重复刷新浏览器页面,也不会多次执行 addCartItem() 方法,而是会多次执行 addCartItemSuccess() 方法(只是重复查询而已),从而保证 addCartItem() 方法只会执行一遍,避免了重复添加购物项。

因为转发时,浏览器的 URL 不会改变,仍然是转发前的 URL,因此多次刷新就会多次执行这个旧的 URL,导致重复执行。但重定向后,浏览器的 URL 已经发生了改变,此时再次刷新就只会重复执行这个新的 URL,不会再执行旧的 URL。因此只需要将新的 URL 设置为幂等性的查询操作即可。

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
/**
* 添加商品到购物车。并重定向到添加成功页
* 因为采用的是重定向,所以即使重复刷新也不会重复执行该方法(因为重定向后再重复刷新,就只会重复执行 addCartItemSuccess() 方法
* addFlashAttribute():将数据放在 Session 中,但是只能取一次,使用 session.xxx 获取值
* addAttribute():将数据拼接在 URL 后面,使用 @RequestParam("skuId") 获取值
* @return
*/
@RequestMapping("/addCartItem")
public String addCartItem(@RequestParam("skuId") Long skuId, @RequestParam("num") Integer num, RedirectAttributes attributes) {
cartService.addCartItem(skuId, num);
// RedirectAttributes 中添加的属性 skuId 将直接拼接在 URL 后面
attributes.addAttribute("skuId", skuId);
// 这里必须用重定向,否则如果直接转发到success的话,重复刷新后该方法会重复执行,导致购物车的数量会增加。
return "redirect:http://cart.yunmall.com/addCartItemSuccess";
}

/**
* 本方法相当于一个中转站,保证只会添加一次购物车。多次刷新页面只是会多次查询本方法而已
* 本方法为幂等性的查询操作,因此不会造成重复添加购物项
* @param skuId
* @param model
* @return
*/
@RequestMapping("/addCartItemSuccess")
public String addCartItemSuccess(@RequestParam("skuId") Long skuId, Model model) {
CartItemVo cartItemVo = cartService.getCartItem(skuId);
model.addAttribute("cartItem", cartItemVo);
return "success";
}

其中:

  • RedirectAttributes.addFlashAttribute():将数据放在 Session 中,但是只能取一次,使用 session.xxx 获取值
  • RedirectAttributes.addAttribute():将数据拼接在 URL 后面,使用 @RequestParam("skuId") 获取值

Service 层:

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
/**
* 添加购物项。
*/
@Override
public CartItemVo addCartItem(Long skuId, Integer num) {
BoundHashOperations<String, Object, Object> ops = getCartItemOps();
// 判断当前商品是否已经存在购物车
String cartJson = (String) ops.get(skuId.toString());
// 1. 如果已经存在购物车,将数据取出并添加商品数量
if (!StringUtils.isEmpty(cartJson)) {
// 1.1 将 JSON 转为对象并将 count + num
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 {
// 2. 如果该 SKU 不存在于购物车中,则添加新商品
CartItemVo cartItemVo = new CartItemVo();
// 异步执行下面两个查询任务
CompletableFuture<Void> future1 = CompletableFuture.runAsync(() -> {
// 2.1 远程查询 SKU 基本信息
R r = productFeignService.info(skuId);
System.out.println(r.getClass());

// 远程保存到 Map 里的 SkuInfoEntity 无法直接转换成实体对象。必须要先转成JSON
String json = JSONObject.toJSONString(r.get("skuInfo"));
SkuInfoVo skuInfo = JSONObject.parseObject(json, 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 (Exception e) {
e.printStackTrace();
}

// 2.3 将购物项 cartItemVo 存入 Redis
// 其中,Redis 的 key 设置为:登录用户使用 userId,,临时用户使用 user-key
String toJSONString = JSON.toJSONString(cartItemVo);
ops.put(skuId.toString(), toJSONString);
return cartItemVo;
}
}

/**
* 从 Redis 中查询该购物项
*/
@Override
public CartItemVo getCartItem(Long skuId) {
BoundHashOperations<String, Object, Object> cartItemOps = getCartItemOps();
String s = (String) cartItemOps.get(skuId.toString());
CartItemVo cartItemVo = JSON.parseObject(s, CartItemVo.class);
return cartItemVo;
}

/**
* 从 ThreadLocal 中获取当前用户的 id:userInfoTo.getUserId()
* 判断该用户是登录还是未登录,从而决定操作 Redis 中的哪个 key
*/
private BoundHashOperations<String, Object, Object> getCartItemOps() {
// 从 ThreadLocal 中获取当前用户
UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
// 已登录使用 userId 操作 Redis
if (!StringUtils.isEmpty(userInfoTo.getUserId())) {
return redisTemplate.boundHashOps(CartConstant.CART_PREFIX + userInfoTo.getUserId());
} else {
// 未登录使用 user-key 操作 Redis
return redisTemplate.boundHashOps(CartConstant.CART_PREFIX + userInfoTo.getUserKey());
}
}

存储在 Redis 中的购物车不设置过期时间,只有在合并时才会删除临时购物车

跳转到 success.html 页面后:

image-20220116121344794

购物项在 Redis 中的实际保存效果如下:

image-20220115191209729

购物车和购物项信息只保存到 Redis 中,不存储在数据库里

展示购物车

success.html 页面点击【去购物车结算】后将跳转到 cartList.html 展示购物车中所有购物项的信息。其中,需要实现合并在线购物车和临时购物车的功能:

  • 如果浏览器中存在 Cookie user-key,代表当前用户之前以临时用户身份登陆过本网站,其可能添加了购物车(存储在 Reids 中)。那么当该用户登录时,在查询购物车信息时,需要将其之前存储在 Redis 中的离线购物车信息合并到登录用户的在线购物车中
  • 如果浏览器中不存在 Cookie user-key,则不需要进行合并操作,直接展示登录用户的在线购物车信息即可

Controller 层:

1
2
3
4
5
6
7
8
9
10
11
/**
* 前端点击【我的购物车】或【去购物车结算】后,跳转到这里获取该用户的购物车信息,并跳转到 cartList.html 页面
* @param model
* @return
*/
@GetMapping("/cart.html")
public String cartListPage(Model model) {
CartVo cart = cartService.getCart();
model.addAttribute("cart", cart);
return "cartList";
}

Service 层需要额外判断是否需要合并:

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
@Override
public CartVo getCart() {
CartVo cartVo = new CartVo();
UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
// 1. 用户未登录,直接通过 user-key 获取临时购物车
List<CartItemVo> tempCart = getCartByKey(userInfoTo.getUserKey());
if (StringUtils.isEmpty(userInfoTo.getUserId())) {
List<CartItemVo> cartItemVos = tempCart;
cartVo.setItems(cartItemVos);
}else {
// 2. 用户已登录
// 2.1 查询 userId 对应的购物车
List<CartItemVo> userCart = getCartByKey(userInfoTo.getUserId().toString());
// 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;
}

cartList.html 购物车展示效果:

image-20220115200411629

购物项勾选、增加和删除

在购物车页面中,还需要实现购物项的勾选、增加和删除功能。

Controller 层:

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

@RequestMapping("/countItem")
public String changeItemCount(@RequestParam("skuId") Long skuId, @RequestParam("num") Integer num) {
cartService.changeItemCount(skuId, num);
return "redirect:http://cart.yunmall.com/cart.html";
}

@RequestMapping("/deleteItem")
public String deleteItem(@RequestParam("skuId") Long skuId) {
cartService.deleteItem(skuId);
return "redirect:http://cart.yunmall.com/cart.html";
}

@ResponseBody
@RequestMapping("/getCheckedItems")
public List<CartItemVo> getCheckedItems() {
return cartService.getCheckedItems();
}

Service 层:

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
@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));
}

@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));
}

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

@Override
public List<CartItemVo> getCheckedItems() {
UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
List<CartItemVo> cartByKey = getCartByKey(userInfoTo.getUserId().toString());
return cartByKey.stream().filter(CartItemVo::getCheck).collect(Collectors.toList());
}

private List<CartItemVo> getCartByKey(String userKey) {
BoundHashOperations<String, Object, Object> ops = redisTemplate.boundHashOps(CartConstant.CART_PREFIX+userKey);

List<Object> values = ops.values();
if (values != null && values.size() > 0) {
List<CartItemVo> cartItemVos = values.stream().map(obj -> {
String json = (String) obj;
return JSON.parseObject(json, CartItemVo.class);
}).collect(Collectors.toList());
return cartItemVos;
}
return null;
}