本篇文章是关于这个项目的购物车模块的实现。
电商系统的购物车模块是为了提供用户在购物过程中临时存储商品的功能。购物车是一个临时的容器,用户可以将他们感兴趣的商品添加到购物车中,然后在结算之前对购物车中的商品进行管理、编辑和确认。
这个模块其实很特别,特别就特别在它不用写dao层,其实我一开始也有点疑惑,为什么不写 DAO 层呢?平时的模块都是按照同一个开发顺序来写的,怎么到了这里就不用了呢?我看了一下代码,结果发现购物车的数据在这里使用 Redis 进行存储,而不是传统的关系型数据库。在这种情况下,使用 Redis 的操作方法更加直接和简单,不需要引入额外的 DAO 层。
我们先来看看它是怎么写的:service层先写一个接口,接口里面定义了很多方法:
package com.imooc.mall.service;
import com.imooc.mall.form.CartAddForm;
import com.imooc.mall.form.CartUpdateForm;
public interface ICartService {
ResponseVo<CartVo> add(Integer uid, CartAddForm form);//向购物车中添加商品,需要传入用户ID和商品添加表单
ResponseVo<CartVo> list(Integer uid);//获取购物车列表,需要传入用户ID
ResponseVo<CartVo> update(Integer uid, Integer productId, CartUpdateForm form);//更新购物车中的商品数量,需要传入用户ID、商品ID和更新表单
ResponseVo<CartVo> delete(Integer uid, Integer productId);//从购物车中删除指定的商品,需要传入用户ID和商品ID
ResponseVo<CartVo> selectAll(Integer uid);//将购物车中的所有商品选中,需要传入用户ID
ResponseVo<CartVo> unSelectAll(Integer uid);//将购物车中的所有商品取消选中,需要传入用户ID
ResponseVo<Integer> sum(Integer uid);//计算购物车中商品的总数量,需要传入用户ID
List<Cart> listForCart(Integer uid);//获取购物车列表,返回购物车项的详细信息,需要传入用户ID
}
在service层的接口中他定义了很多方法,对于每个方法我们一个一个讲解,我感觉主要不是理解代码本身,而是理解这个方法是做什么的。
public ResponseVo<CartVo> add(Integer uid, CartAddForm form) {
//首先,获取要添加的商品的数量,默认为1
Integer quantity = 1;
//通过商品ID从数据库中查询该商品的信息
Product product = productMapper.selectByPrimaryKey(form.getProductId());
//商品是否存在,如果不存在,则返回一个错误响应
if (product == null) {
return ResponseVo.error(ResponseEnum.PRODUCT_NOT_EXIST);
}
//商品是否正常在售,如果不正常在售,则返回一个错误响应
if (!product.getStatus().equals(ProductStatusEnum.ON_SALE.getCode())) {
return ResponseVo.error(ResponseEnum.PRODUCT_OFF_SALE_OR_DELETE);
}
//商品库存是否充足,如果不充足,则返回一个错误响应
if (product.getStock() <= 0) {
return ResponseVo.error(ResponseEnum.PROODUCT_STOCK_ERROR);
}
//准备写入 Redis 中的购物车数据的键(key)
//key: cart_1
HashOperations<String, String, String> opsForHash = redisTemplate.opsForHash();
String redisKey = String.format(CART_REDIS_KEY_TEMPLATE, uid);
Cart cart;
//检查 Redis 中是否已经存在该商品的购物车数据
String value = opsForHash.get(redisKey, String.valueOf(product.getId()));
if (StringUtils.isEmpty(value)) {
//没有该商品, 新增
cart = new Cart(product.getId(), quantity, form.getSelected());
}else {
//已经有了,数量+1
cart = gson.fromJson(value, Cart.class);
cart.setQuantity(cart.getQuantity() + quantity);
}
//将购物车对象转换为 JSON 字符串,并将其写入 Redis 中
opsForHash.put(redisKey,
String.valueOf(product.getId()),
gson.toJson(cart));
//最后,调用 list 方法返回更新后的购物车列表
return list(uid);
}
这个方法的作用是将指定商品添加到购物车中,并将购物车数据存储在 Redis 中。如果成功添加,则返回更新后的购物车列表。如果存在错误(商品不存在、商品不在售、库存不足等),则返回相应的错误响应。
@Override
public ResponseVo<CartVo> list(Integer uid) {
HashOperations<String, String, String> opsForHash = redisTemplate.opsForHash();
String redisKey = String.format(CART_REDIS_KEY_TEMPLATE, uid);
Map<String, String> entries = opsForHash.entries(redisKey);
boolean selectAll = true;
Integer cartTotalQuantity = 0;
BigDecimal cartTotalPrice = BigDecimal.ZERO;
CartVo cartVo = new CartVo();
List<CartProductVo> cartProductVoList = new ArrayList<>();
for (Map.Entry<String, String> entry : entries.entrySet()) {
Integer productId = Integer.valueOf(entry.getKey());
Cart cart = gson.fromJson(entry.getValue(), Cart.class);
//TODO 需要优化,使用mysql里的in
Product product = productMapper.selectByPrimaryKey(productId);
if (product != null) {
CartProductVo cartProductVo = new CartProductVo(productId,
cart.getQuantity(),
product.getName(),
product.getSubtitle(),
product.getMainImage(),
product.getPrice(),
product.getStatus(),
product.getPrice().multiply(BigDecimal.valueOf(cart.getQuantity())),
product.getStock(),
cart.getProductSelected()
);
cartProductVoList.add(cartProductVo);
if (!cart.getProductSelected()) {
selectAll = false;
}
//计算总价(只计算选中的)
if (cart.getProductSelected()) {
cartTotalPrice = cartTotalPrice.add(cartProductVo.getProductTotalPrice());
}
}
cartTotalQuantity += cart.getQuantity();
}
//有一个没有选中,就不叫全选
cartVo.setSelectedAll(selectAll);
cartVo.setCartTotalQuantity(cartTotalQuantity);
cartVo.setCartTotalPrice(cartTotalPrice);
cartVo.setCartProductVoList(cartProductVoList);
return ResponseVo.success(cartVo);
}
这个方法写了好多东西,我看起来很吃力,不过我大概知道这个代码的作用是从 Redis 中获取购物车数据,然后根据购物车数据构建一个包含购物车列表信息的 CartVo
对象,并返回给前端。
@Override
public ResponseVo<CartVo> update(Integer uid, Integer productId, CartUpdateForm form) {
//首先,获取 Redis 的 Hash 操作对象
HashOperations<String, String, String> opsForHash = redisTemplate.opsForHash();
//准备购物车在 Redis 中的键(key)
String redisKey = String.format(CART_REDIS_KEY_TEMPLATE, uid);
//通过商品ID从 Redis 中获取该商品的购物车数据
String value = opsForHash.get(redisKey, String.valueOf(productId));
if (StringUtils.isEmpty(value)) {
//没有该商品, 报错
return ResponseVo.error(ResponseEnum.CART_PRODUCT_NOT_EXIST);
}
//已经有了,修改内容
Cart cart = gson.fromJson(value, Cart.class);
if (form.getQuantity() != null
&& form.getQuantity() >= 0) {
cart.setQuantity(form.getQuantity());
}
if (form.getSelected() != null) {
cart.setProductSelected(form.getSelected());
}
//将更新后的购物车数据重新存储到 Redis 中
opsForHash.put(redisKey, String.valueOf(productId), gson.toJson(cart));
//调用 list 方法返回更新后的购物车列表
return list(uid);
}
这个方法的作用是更新购物车中指定商品的数量或选中状态,并将更新后的购物车列表返回给前端。
@Override
public ResponseVo<CartVo> delete(Integer uid, Integer productId) {
//获取 Redis 的 Hash 操作对象
HashOperations<String, String, String> opsForHash = redisTemplate.opsForHash();
//准备购物车在 Redis 中的键(key),其实就是为购物车在 Redis 中设置一个唯一标识的键,以便在 Redis 中存储和检索购物车数据
String redisKey = String.format(CART_REDIS_KEY_TEMPLATE, uid);
String value = opsForHash.get(redisKey, String.valueOf(productId));
if (StringUtils.isEmpty(value)) {
//没有该商品, 报错
return ResponseVo.error(ResponseEnum.CART_PRODUCT_NOT_EXIST);
}
opsForHash.delete(redisKey, String.valueOf(productId));
return list(uid);
}
这个方法的作用是从购物车中删除指定商品,并将更新后的购物车列表返回给前端。
@Override
public ResponseVo<CartVo> selectAll(Integer uid) {
HashOperations<String, String, String> opsForHash = redisTemplate.opsForHash();
String redisKey = String.format(CART_REDIS_KEY_TEMPLATE, uid);
for (Cart cart : listForCart(uid)) {
cart.setProductSelected(true);
opsForHash.put(redisKey,
String.valueOf(cart.getProductId()),
gson.toJson(cart));
}
return list(uid);
}
这个方法的作用是将购物车中所有商品设置为选中状态,并将更新后的购物车列表返回给前端。
@Override
public ResponseVo<CartVo> unSelectAll(Integer uid) {
HashOperations<String, String, String> opsForHash = redisTemplate.opsForHash();
String redisKey = String.format(CART_REDIS_KEY_TEMPLATE, uid);
for (Cart cart : listForCart(uid)) {
cart.setProductSelected(false);
opsForHash.put(redisKey,
String.valueOf(cart.getProductId()),
gson.toJson(cart));
}
return list(uid);
}
这个方法的作用是将购物车中所有商品设置为未选中状态,并将更新后的购物车列表返回给前端。
@Override
public ResponseVo<Integer> sum(Integer uid) {
Integer sum = listForCart(uid).stream()
.map(Cart::getQuantity)
.reduce(0, Integer::sum);
return ResponseVo.success(sum);
}
这个方法的作用是计算购物车中所有商品的数量总和,并将总和作为响应返回给前端。
public List<Cart> listForCart(Integer uid) {
HashOperations<String, String, String> opsForHash = redisTemplate.opsForHash();
String redisKey = String.format(CART_REDIS_KEY_TEMPLATE, uid);
Map<String, String> entries = opsForHash.entries(redisKey);
List<Cart> cartList = new ArrayList<>();
for (Map.Entry<String, String> entry : entries.entrySet()) {
cartList.add(gson.fromJson(entry.getValue(), Cart.class));
}
return cartList;
}
这个方法的作用是从 Redis 中获取购物车中所有商品的数据,并将其转换为 Cart
对象的列表返回。
至此,service层的方法就讲完了,其实我觉得对于代码部分简单了解既可,主要要知道每个方法是有什么用的,只要你知道每个方法有什么用,那你就大概清楚购物车的很多细节了,这个显得更加重要!
写完service层,就要写controller层了,controller层的代码虽然挺多的,但是结构十分规整。代码是这样的:
@RestController
public class CartController {
@Autowired
private ICartService cartService;
@GetMapping("/carts")
public ResponseVo<CartVo> list(HttpSession session) {
User user = (User) session.getAttribute(MallConst.CURRENT_USER);
return cartService.list(user.getId());
}
@PostMapping("/carts")
public ResponseVo<CartVo> add(@Valid @RequestBody CartAddForm cartAddForm,
HttpSession session) {
User user = (User) session.getAttribute(MallConst.CURRENT_USER);
return cartService.add(user.getId(), cartAddForm);
}
@PutMapping("/carts/{productId}")
public ResponseVo<CartVo> update(@PathVariable Integer productId,
@Valid @RequestBody CartUpdateForm form,
HttpSession session) {
User user = (User) session.getAttribute(MallConst.CURRENT_USER);
return cartService.update(user.getId(), productId, form);
}
@DeleteMapping("/carts/{productId}")
public ResponseVo<CartVo> delete(@PathVariable Integer productId,
HttpSession session) {
User user = (User) session.getAttribute(MallConst.CURRENT_USER);
return cartService.delete(user.getId(), productId);
}
@PutMapping("/carts/selectAll")
public ResponseVo<CartVo> selectAll(HttpSession session) {
User user = (User) session.getAttribute(MallConst.CURRENT_USER);
return cartService.selectAll(user.getId());
}
@PutMapping("/carts/unSelectAll")
public ResponseVo<CartVo> unSelectAll(HttpSession session) {
User user = (User) session.getAttribute(MallConst.CURRENT_USER);
return cartService.unSelectAll(user.getId());
}
@GetMapping("/carts/products/sum")
public ResponseVo<Integer> sum(HttpSession session) {
User user = (User) session.getAttribute(MallConst.CURRENT_USER);
return cartService.sum(user.getId());
}
这个controller层有一些细节:
(1)通过 @RestController
注解将该类标记为控制器,用于处理 HTTP 请求和生成响应
(2)使用 @Autowired
注解将 cartService
自动注入到控制器中,以便在方法中使用购物车服务的功能
(3)使用 HttpSession
参数获取当前用户的信息
(4)调用 cartService
中相应的方法来处理购物车的业务逻辑,并将结果封装为响应对象(ResponseVo
)返回给客户端
这个购物车模块就设计完了,其实我感觉真的这些模块都很相似,可以说基本没啥太大的区别,就这个比较特殊,就是没有 DAO 层,因为用了 Redis 。但是我整个流程看下来,也就那样,都一个样。写完这篇文章后,我发现接下来我写的收货地址模块和订单模块应该会轻松很多了,至少我已经是有心理准备了!