这篇文章是关于项目的订单模块的设计。这个模块其实相对来讲比之前的模块复杂了点,我这里说的复杂并不是说难以理解,而是说文件比较多,理解起来还是蛮轻松的。
我们还是老方法,一步一步的去设计,按照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里的productIds
Set<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());
}
//构造orderVo
OrderVo orderVo = buildOrderVo(order, orderItemList, shipping);
return ResponseVo.success(orderVo);
}
这个方法太长了,我理解了好久,就说一下我自己的理解。
- 根据用户ID和收货地址ID查询对应的收货地址信息。
- 判断收货地址是否存在,如果不存在则返回错误响应。
- 获取用户购物车中选中的商品列表(已经勾选的商品)。
- 判断购物车是否为空,如果为空则返回错误响应。
- 从购物车列表中获取商品ID集合,并根据商品ID集合查询对应的商品信息。
- 构建订单项列表,遍历购物车列表,根据购物车项信息和商品信息构建订单项,并将订单项添加到订单项列表中。
- 生成订单号(orderNo)。
- 遍历订单项列表,依次处理每个订单项:
- 根据商品ID获取对应的商品信息。
- 判断商品是否存在,如果不存在则返回错误响应。
- 判断商品的上下架状态,如果不是在售状态则返回错误响应。
- 判断商品库存是否充足,如果不充足则返回错误响应。
- 构建订单项对象,并将其添加到订单项列表中。
- 减少商品库存数量,并更新数据库中的商品库存信息。
- 构建订单对象,包括用户ID、订单号、收货地址ID和订单项列表等信息。
- 将订单信息插入到数据库中。
- 将订单项列表插入到数据库中。
- 遍历购物车列表,删除购物车中已选中的商品。
- 构建订单视图对象(OrderVo),包括订单信息、订单项列表和收货地址信息等。
- 返回成功响应,并将订单视图对象作为响应数据返回。
这是真的复杂,但是我感觉就稍微看看,理解下原来是这么回事儿,脑子里有创建订单的流程就好了。
再来实现list方法。
@Override
public 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方法。
@Override
public 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方法。
@Override
public 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方法。
@Override
public 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 {
@Autowired
private 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层一旦掌握了,那整个模块应该很容易就掌握了。