这篇文章是关于项目的订单模块的设计。这个模块其实相对来讲比之前的模块复杂了点,我这里说的复杂并不是说难以理解,而是说文件比较多,理解起来还是蛮轻松的。
我们还是老方法,一步一步的去设计,按照Dao->service->controller的顺序来设计。
先来设计Dao层。需要注意的是,这里非常特别,因为Dao层里面有两个文件,分别是OrderMapper和OrderItemMapper。这两者有什么区别呢?区别就是一个是关于订单的文件,一个是关于订单项的文件。那订单和订单项有什么区别呢?我觉得如果真的看定义的话其实根本看不清,我就举一个例子。
假如现在有一个用户在电商网站上购买了三个商品,商品A、商品B和商品C。这个用户将这三个商品添加到购物车并进行结算,生成了一个订单。
订单(Order):
- 订单号:202402240001
- 用户ID:12345
- 订单状态:待支付
- 支付状态:未支付
- 下单时间:2024-02-24 10:00:00
- 收货地址:123 Main Street, City, Country
订单项(Order Item):
-
订单号:202402240001
- 商品ID:1001
- 商品名称:商品A
- 商品数量:2
- 商品价格:$10
-
订单号:202402240001
- 商品ID:1002
- 商品名称:商品B
- 商品数量:1
- 商品价格:$20
-
订单号:202402240001
- 商品ID:1003
- 商品名称:商品C
- 商品数量:3
- 商品价格:$15
这下我估计你该懂了。因为这种东西,干巴巴的说定义真的是说不清的,必须要配合例子来理解,至少我是这样的。
我们先来设计Dao层里面的OrderMapper。
package com.imooc.mall.dao;import com.imooc.mall.pojo.Order;import java.util.List;public interface OrderMapper {int deleteByPrimaryKey(Integer id);//根据订单ID删除订单记录int insert(Order record);//插入一条完整的订单记录int insertSelective(Order record);//选择性地插入一条订单记录,只插入非空字段的值Order selectByPrimaryKey(Integer id);//根据订单ID查询订单记录int updateByPrimaryKeySelective(Order record);//选择性地更新一条订单记录,只更新非空字段的值int updateByPrimaryKey(Order record);//更新一条完整的订单记录List<Order> selectByUid(Integer uid);//根据用户ID查询该用户的订单列表Order selectByOrderNo(Long orderNo);//根据订单号查询订单记录
}
上面这些方法定义了对订单表的基本操作,包括插入、更新、删除和查询。通过调用这些方法,可以对订单数据进行持久化操作,实现订单的增删改查功能。
但是,这里的 OrderMapper
接口没有具体的实现代码,只定义了接口方法。具体的实现代码通常由框架或者开发者自行完成,其实就是 OrderMapper.xml
文件夹。这个文件夹实现了这个接口里的方法。
然后再来设计Dao层的OrderItemMapper。
package com.imooc.mall.dao;import com.imooc.mall.pojo.OrderItem;
import org.apache.ibatis.annotations.Param;import java.util.List;
import java.util.Set;public interface OrderItemMapper {int deleteByPrimaryKey(Integer id);//根据订单项ID删除订单项记录int insert(OrderItem record);//插入一条完整的订单项记录int insertSelective(OrderItem record);//选择性地插入一条订单项记录,只插入非空字段的值OrderItem selectByPrimaryKey(Integer id);//根据订单项ID查询订单项记录int updateByPrimaryKeySelective(OrderItem record);//选择性地更新一条订单项记录,只更新非空字段的值int updateByPrimaryKey(OrderItem record);//更新一条完整的订单项记录int batchInsert(@Param("orderItemList") List<OrderItem> orderItemList);//批量插入订单项记录。该方法接收一个订单项列表作为参数,并批量插入到数据库中List<OrderItem> selectByOrderNoSet(@Param("orderNoSet") Set orderNoSet);//根据订单号集合查询订单项列表。该方法接收一个订单号的集合作为参数,并返回对应的订单项列表
}
这些方法定义了对订单项表的基本操作,包括插入、更新、删除和查询。通过调用这些方法,可以对订单项数据进行持久化操作,实现订单项的增删改查功能。
有没有看到,其实这个订单项和订单是差不多的,你只要理解了这两者的区别基本上就很好理解了。
再来设计service层。
package com.imooc.mall.service;import com.github.pagehelper.PageInfo;
import com.imooc.mall.vo.OrderVo;
import com.imooc.mall.vo.ResponseVo;public interface IOrderService {ResponseVo<OrderVo> create(Integer uid, Integer shippingId);//创建订单。接收用户ID和收货地址ID作为参数,返回一个包含订单信息的响应对象 ResponseVo<OrderVo>。ResponseVo<PageInfo> list(Integer uid, Integer pageNum, Integer pageSize);//获取订单列表。接收用户ID、页码和每页大小作为参数,返回一个包含分页订单信息的响应对象 ResponseVo<PageInfo>。ResponseVo<OrderVo> detail(Integer uid, Long orderNo);//获取订单详情。接收用户ID和订单号作为参数,返回一个包含订单详细信息的响应对象 ResponseVo<OrderVo>。ResponseVo cancel(Integer uid, Long orderNo);//取消订单。接收用户ID和订单号作为参数,返回一个响应对象 ResponseVo,表示取消订单的结果。void paid(Long orderNo);//订单支付完成后的回调方法。接收订单号作为参数,无返回值。
}
这里的接口方法返回的类型是根据业务需求定义的泛型对象 ResponseVo
和 ResponseVo<T>
,用于封装响应结果和数据。OrderVo
是一个用于表示订单信息的值对象。
我们一个方法一个方法的来实现。首先实现create方法:
public ResponseVo<OrderVo> create(Integer uid, Integer shippingId) {//收货地址校验(总之要查出来的)Shipping shipping = shippingMapper.selectByUidAndShippingId(uid, shippingId);if (shipping == null) {return ResponseVo.error(ResponseEnum.SHIPPING_NOT_EXIST);}//获取购物车,校验(是否有商品、库存)List<Cart> cartList = cartService.listForCart(uid).stream().filter(Cart::getProductSelected).collect(Collectors.toList());if (CollectionUtils.isEmpty(cartList)) {return ResponseVo.error(ResponseEnum.CART_SELECTED_IS_EMPTY);}//获取cartList里的productIdsSet<Integer> productIdSet = cartList.stream().map(Cart::getProductId).collect(Collectors.toSet());List<Product> productList = productMapper.selectByProductIdSet(productIdSet);Map<Integer, Product> map = productList.stream().collect(Collectors.toMap(Product::getId, product -> product));List<OrderItem> orderItemList = new ArrayList<>();Long orderNo = generateOrderNo();for (Cart cart : cartList) {//根据productId查数据库Product product = map.get(cart.getProductId());//是否有商品if (product == null) {return ResponseVo.error(ResponseEnum.PRODUCT_NOT_EXIST,"商品不存在. productId = " + cart.getProductId());}//商品上下架状态if (!ProductStatusEnum.ON_SALE.getCode().equals(product.getStatus())) {return ResponseVo.error(ResponseEnum.PRODUCT_OFF_SALE_OR_DELETE,"商品不是在售状态. " + product.getName());}//库存是否充足if (product.getStock() < cart.getQuantity()) {return ResponseVo.error(ResponseEnum.PROODUCT_STOCK_ERROR,"库存不正确. " + product.getName());}OrderItem orderItem = buildOrderItem(uid, orderNo, cart.getQuantity(), product);orderItemList.add(orderItem);//减库存product.setStock(product.getStock() - cart.getQuantity());int row = productMapper.updateByPrimaryKeySelective(product);if (row <= 0) {return ResponseVo.error(ResponseEnum.ERROR);}}//计算总价,只计算选中的商品//生成订单,入库:order和order_item,事务Order order = buildOrder(uid, orderNo, shippingId, orderItemList);int rowForOrder = orderMapper.insertSelective(order);if (rowForOrder <= 0) {return ResponseVo.error(ResponseEnum.ERROR);}int rowForOrderItem = orderItemMapper.batchInsert(orderItemList);if (rowForOrderItem <= 0) {return ResponseVo.error(ResponseEnum.ERROR);}//更新购物车(选中的商品)//Redis有事务(打包命令),不能回滚for (Cart cart : cartList) {cartService.delete(uid, cart.getProductId());}//构造orderVoOrderVo orderVo = buildOrderVo(order, orderItemList, shipping);return ResponseVo.success(orderVo);}
这个方法太长了,我理解了好久,就说一下我自己的理解。
- 根据用户ID和收货地址ID查询对应的收货地址信息。
- 判断收货地址是否存在,如果不存在则返回错误响应。
- 获取用户购物车中选中的商品列表(已经勾选的商品)。
- 判断购物车是否为空,如果为空则返回错误响应。
- 从购物车列表中获取商品ID集合,并根据商品ID集合查询对应的商品信息。
- 构建订单项列表,遍历购物车列表,根据购物车项信息和商品信息构建订单项,并将订单项添加到订单项列表中。
- 生成订单号(orderNo)。
- 遍历订单项列表,依次处理每个订单项:
- 根据商品ID获取对应的商品信息。
- 判断商品是否存在,如果不存在则返回错误响应。
- 判断商品的上下架状态,如果不是在售状态则返回错误响应。
- 判断商品库存是否充足,如果不充足则返回错误响应。
- 构建订单项对象,并将其添加到订单项列表中。
- 减少商品库存数量,并更新数据库中的商品库存信息。
- 构建订单对象,包括用户ID、订单号、收货地址ID和订单项列表等信息。
- 将订单信息插入到数据库中。
- 将订单项列表插入到数据库中。
- 遍历购物车列表,删除购物车中已选中的商品。
- 构建订单视图对象(OrderVo),包括订单信息、订单项列表和收货地址信息等。
- 返回成功响应,并将订单视图对象作为响应数据返回。
这是真的复杂,但是我感觉就稍微看看,理解下原来是这么回事儿,脑子里有创建订单的流程就好了。
再来实现list方法。
@Overridepublic ResponseVo<PageInfo> list(Integer uid, Integer pageNum, Integer pageSize) {PageHelper.startPage(pageNum, pageSize);List<Order> orderList = orderMapper.selectByUid(uid);Set<Long> orderNoSet = orderList.stream().map(Order::getOrderNo).collect(Collectors.toSet());List<OrderItem> orderItemList = orderItemMapper.selectByOrderNoSet(orderNoSet);Map<Long, List<OrderItem>> orderItemMap = orderItemList.stream().collect(Collectors.groupingBy(OrderItem::getOrderNo));Set<Integer> shippingIdSet = orderList.stream().map(Order::getShippingId).collect(Collectors.toSet());List<Shipping> shippingList = shippingMapper.selectByIdSet(shippingIdSet);Map<Integer, Shipping> shippingMap = shippingList.stream().collect(Collectors.toMap(Shipping::getId, shipping -> shipping));List<OrderVo> orderVoList = new ArrayList<>();for (Order order : orderList) {OrderVo orderVo = buildOrderVo(order,orderItemMap.get(order.getOrderNo()),shippingMap.get(order.getShippingId()));orderVoList.add(orderVo);}PageInfo pageInfo = new PageInfo<>(orderList);pageInfo.setList(orderVoList);return ResponseVo.success(pageInfo);}
- 使用PageHelper工具类设置分页参数,即设置页码和每页显示数量。
- 根据用户ID查询对应的订单列表。
- 从订单列表中提取订单号集合。
- 根据订单号集合查询对应的订单项列表。
- 将订单项列表按订单号进行分组,构建订单号与订单项列表的映射关系。
- 从订单列表中提取收货地址ID集合。
- 根据收货地址ID集合查询对应的收货地址列表。
- 将收货地址列表按ID进行映射,构建收货地址ID与收货地址对象的映射关系。
- 遍历订单列表,逐个构建订单视图对象(OrderVo):
- 根据订单信息、订单项映射和收货地址映射构建订单视图对象。
- 将订单视图对象添加到订单视图列表中。
- 构建分页信息对象(PageInfo),其中包括订单列表和总记录数等信息。
- 将订单视图列表设置到分页信息对象中。
- 返回成功响应,将分页信息对象作为响应数据返回。
这也很复杂,就随便看看吧。
再来实现detail方法。
@Overridepublic ResponseVo<OrderVo> detail(Integer uid, Long orderNo) {Order order = orderMapper.selectByOrderNo(orderNo);if (order == null || !order.getUserId().equals(uid)) {return ResponseVo.error(ResponseEnum.ORDER_NOT_EXIST);}Set<Long> orderNoSet = new HashSet<>();orderNoSet.add(order.getOrderNo());List<OrderItem> orderItemList = orderItemMapper.selectByOrderNoSet(orderNoSet);Shipping shipping = shippingMapper.selectByPrimaryKey(order.getShippingId());OrderVo orderVo = buildOrderVo(order, orderItemList, shipping);return ResponseVo.success(orderVo);}
- 根据订单号查询对应的订单信息。
- 判断订单是否存在或者订单所属用户是否与传入的用户ID匹配,如果不匹配则返回错误响应。
- 创建订单号集合,并将当前订单号添加到集合中。
- 根据订单号集合查询对应的订单项列表。
- 根据订单的收货地址ID查询对应的收货地址信息。
- 根据订单信息、订单项列表和收货地址信息构建订单视图对象(OrderVo)。
- 返回成功响应,将订单视图对象作为响应数据返回。
再来实现cancel方法。
@Overridepublic ResponseVo cancel(Integer uid, Long orderNo) {Order order = orderMapper.selectByOrderNo(orderNo);if (order == null || !order.getUserId().equals(uid)) {return ResponseVo.error(ResponseEnum.ORDER_NOT_EXIST);}//只有[未付款]订单可以取消,看自己公司业务if (!order.getStatus().equals(OrderStatusEnum.NO_PAY.getCode())) {return ResponseVo.error(ResponseEnum.ORDER_STATUS_ERROR);}order.setStatus(OrderStatusEnum.CANCELED.getCode());order.setCloseTime(new Date());int row = orderMapper.updateByPrimaryKeySelective(order);if (row <= 0) {return ResponseVo.error(ResponseEnum.ERROR);}return ResponseVo.success();}
- 根据订单号查询对应的订单信息。
- 判断订单是否存在或订单所属用户是否与传入的用户ID匹配,如果不匹配则返回错误响应。
- 判断订单状态是否为未付款状态,如果不是则返回错误响应。
- 将订单状态设置为已取消状态,并设置取消时间为当前时间。
- 更新数据库中的订单信息。
- 判断更新结果,如果影响的行数小于等于0,则返回错误响应。
- 返回成功响应。
这里也是随便看看。只要你脑补到有这个画面就行。
最后设计paid方法。
@Overridepublic void paid(Long orderNo) {Order order = orderMapper.selectByOrderNo(orderNo);if (order == null) {throw new RuntimeException(ResponseEnum.ORDER_NOT_EXIST.getDesc() + "订单id:" + orderNo);}//只有[未付款]订单可以变成[已付款],看自己公司业务if (!order.getStatus().equals(OrderStatusEnum.NO_PAY.getCode())) {throw new RuntimeException(ResponseEnum.ORDER_STATUS_ERROR.getDesc() + "订单id:" + orderNo);}order.setStatus(OrderStatusEnum.PAID.getCode());order.setPaymentTime(new Date());int row = orderMapper.updateByPrimaryKeySelective(order);if (row <= 0) {throw new RuntimeException("将订单更新为已支付状态失败,订单id:" + orderNo);}}
- 根据订单号查询订单信息。
- 如果订单不存在,则抛出运行时异常。
- 判断订单状态是否为未付款状态,如果不是则抛出运行时异常。
- 将订单状态设置为已支付状态,设置支付时间为当前时间。
- 更新数据库中的订单信息。
- 判断更新结果,如果影响的行数小于等于0,则抛出运行时异常。
OK了!service层就已经设计完毕了。确实订单模块这里比较繁琐,所以我感觉最重要还是理解,不需要说背,背就没意思了,脑补到每个实现方法的画面就很好了。
然后设计controller层。
package com.imooc.mall.controller;import com.github.pagehelper.PageInfo;
import com.imooc.mall.consts.MallConst;
import com.imooc.mall.form.OrderCreateForm;
import com.imooc.mall.pojo.User;
import com.imooc.mall.service.IOrderService;
import com.imooc.mall.vo.OrderVo;
import com.imooc.mall.vo.ResponseVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;import javax.servlet.http.HttpSession;
import javax.validation.Valid;/*** Created by 廖师兄*/
@RestController
public class OrderController {@Autowiredprivate IOrderService orderService;@PostMapping("/orders")public ResponseVo<OrderVo> create(@Valid @RequestBody OrderCreateForm form,HttpSession session) {User user = (User) session.getAttribute(MallConst.CURRENT_USER);return orderService.create(user.getId(), form.getShippingId());}@GetMapping("/orders")public ResponseVo<PageInfo> list(@RequestParam Integer pageNum,@RequestParam Integer pageSize,HttpSession session) {User user = (User) session.getAttribute(MallConst.CURRENT_USER);return orderService.list(user.getId(), pageNum, pageSize);}@GetMapping("/orders/{orderNo}")public ResponseVo<OrderVo> detail(@PathVariable Long orderNo,HttpSession session) {User user = (User) session.getAttribute(MallConst.CURRENT_USER);return orderService.detail(user.getId(), orderNo);}@PutMapping("/orders/{orderNo}")public ResponseVo cancel(@PathVariable Long orderNo,HttpSession session) {User user = (User) session.getAttribute(MallConst.CURRENT_USER);return orderService.cancel(user.getId(), orderNo);}
}
-
create()
方法用于创建订单。通过接收一个包含订单创建表单数据的 POST 请求,并从会话中获取当前用户信息,调用订单服务的create()
方法来创建订单,并返回相应的响应结果。 -
list()
方法用于获取订单列表。通过接收 GET 请求中的页码和每页数量参数,并从会话中获取当前用户信息,调用订单服务的list()
方法来获取当前用户的订单列表,并返回相应的响应结果。 -
detail()
方法用于获取订单详情。通过接收 GET 请求中的订单号参数,并从会话中获取当前用户信息,调用订单服务的detail()
方法来获取指定订单的详情,并返回相应的响应结果。 -
cancel()
方法用于取消订单。通过接收 PUT 请求中的订单号参数,并从会话中获取当前用户信息,调用订单服务的cancel()
方法来取消指定订单,并返回相应的响应结果。
这些方法使用了依赖注入(@Autowired
)来获取订单服务(IOrderService
)的实例,以便调用订单相关的业务逻辑。同时,使用了会话(HttpSession
)来获取当前用户信息。根据不同的请求类型和参数,调用相应的订单服务方法,并返回相应的响应给客户端。
这就是订单模块的所有实现,我感觉订单模块其实理解起来并不难,主要是太多了,所以我感觉应该静下心来仔细想想service层的整个流程,service层一旦掌握了,那整个模块应该很容易就掌握了。