up:: SpringBoot电商项目订单模块介绍

说明:

(1) 本篇博客的内容:开发【前台:创建订单】接口;

(2) 本篇博客的内容比较多,主要需要注意以下几点:

● 要明确【创建订单】的思路和流程;

● 为了实现同一个业务,具体的编码逻辑可能存在差异;但随着自己编程能力的提升、开发技巧的积累;应该能编写出越来越好的代码;

● 涉及到了枚举类的使用;

● Spring Boot手动控制事务;

● 关于本篇博客中的几个比较中的点,自己单独写了几篇附加博客,来说明,可以去参考;


一:【前提:创建订单】:分析;

1.【前台:创建订单】,在整个【订单模块】中的位置;

2.【前台:创建订单】,思路分析;

(1.1) 首先,【前台:创建订单】接口:入参说明;

(1.2) 所以,用户id数据、购物车中的商品数据,都需要我们自己去获取;

(2) 我们要判断,【当前用户的、购物车中已经被勾选的、将要被我们下单的,商品】是否存在,如果存在再看其是否还是上架状态;

(3) 还要判断,【当前用户的、购物车中已经被勾选的、将要被我们下单的,商品】是否库存足够,以防止超卖;(PS:如果,一切顺利,下单后,还要及时的扣库存)

(4) 用户下单时,首先,会删除【当前用户的、购物车中的、这个已经被勾选的、将要被下单的,商品】;也就是,删除cart表中,对应的记录;

(5) 然后,我们需要编写逻辑,生成一个订单号;

(6) 然后,会创建一个订单;也就是在order表中,新增一个订单记录;

(7) 然后,也要利用循环,把订单中的每种商品,写到order_item表中;

(8) 因为,【前台:创建订单】过程涉及多个数据库的写操作,所以我们这儿需要手动控制数据库事务;

至于,数据库事务,在【附加SpringBoot项目手动控制事务包括总结了到目前为止事务的所有内容】中,我们再次做了总结,如有需要可以去查看;


二:正式开发;

1.创建【订单模块】对应的OrderController;编写前台创建订单的方法:createOrder()方法;

 
     package com.imooc.mall.controller;
 
     import com.imooc.mall.common.ApiRestResponse;
     import com.imooc.mall.model.request.CreateOrderReq;
     import com.imooc.mall.service.OrderService;
     import io.swagger.annotations.ApiOperation;
     import org.springframework.beans.factory.annotation.Autowired;
     import org.springframework.web.bind.annotation.PostMapping;
     import org.springframework.web.bind.annotation.RequestBody;
     import org.springframework.web.bind.annotation.RequestMapping;
     import org.springframework.web.bind.annotation.RestController;
 
     import javax.validation.Valid;
 
     /**
      * 描述:订单Controller
      */
     @RestController
     public class OrderController {
 
         @Autowired
         OrderService orderService;
 
         /**
          * 【前台:创建订单】接口;
          * @param createOrderReq
          * @return
          */
         @ApiOperation("创建订单")
         @PostMapping("/order/create ")
         public ApiRestResponse createOrder(@Valid @RequestBody CreateOrderReq createOrderReq) {
             String orderNum = orderService.create(createOrderReq);
             return ApiRestResponse.success(orderNum);
         }
 
 
     }

说明:

(1)请求方式,url要符合接口文档要求;

● 有关@RequestBody注解,如有需要可以参考【附加POST请求方法参数放在url中和放在body中有什么区别】;


(2)因为,这个接口有三个参数,虽然不是很多,我们还是创建了一个实体类CreateOrderReq,来帮助承接参数;

● 同时,上面使用了@Valid注解,进行了Validation参数校验;


(3)使用实体类CreateOrderReq,去承接参数;并开启了Valid参数校验;


(4)Service层创建订单的逻辑方法create()方法,在下一部分介绍;

2.创建OrderService接口,OrderServiceImpl实现类;

3.在OrderServiceImpl实现类编写创建订单的逻辑方法:create()方法;

 
  package com.imooc.mall.service.impl;
 
import com.imooc.mall.common.Constant;
import com.imooc.mall.exception.ImoocMallException;
import com.imooc.mall.exception.ImoocMallExceptionEnum;
import com.imooc.mall.filter.UserFilter;
import com.imooc.mall.model.dao.CartMapper;
import com.imooc.mall.model.dao.OrderItemMapper;
import com.imooc.mall.model.dao.OrderMapper;
import com.imooc.mall.model.dao.ProductMapper;
import com.imooc.mall.model.pojo.Cart;
import com.imooc.mall.model.pojo.Order;
import com.imooc.mall.model.pojo.OrderItem;
import com.imooc.mall.model.pojo.Product;
import com.imooc.mall.model.request.CreateOrderReq;
import com.imooc.mall.model.vo.CartVO;
import com.imooc.mall.service.CartService;
import com.imooc.mall.service.OrderService;
import com.imooc.mall.utils.OrderCodeFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
 
import java.awt.*;
import java.util.ArrayList;
import java.util.List;
 
/**
 * 描述:订单模块的Service实现类
 */
@Service
public class OrderServiceImpl implements OrderService {
 
    @Autowired
    CartService cartService;
    @Autowired
    ProductMapper productMapper;
    @Autowired
    CartMapper cartMapper;
    @Autowired
    OrderMapper orderMapper;
    @Autowired
    OrderItemMapper orderItemMapper;
 
 
    /**
     * 创建订单
     * @param createOrderReq
     * @return
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public String create(CreateOrderReq createOrderReq) {
        //首先,拿到用户ID;
        Integer userId = UserFilter.currentUser.getId();
        //从购物车中,查询当前用户的、购物车中的、已经被勾选的商品;
        List<CartVO> cartVOList = cartService.list(userId);
 
        //遍历查到的购物车数据,从中筛选出被勾选的;
        ArrayList<CartVO> cartVOArrayListTemp = new ArrayList<>();
        for (int i = 0; i < cartVOList.size(); i++) {
            CartVO cartVO =  cartVOList.get(i);
            if (cartVO.getSelected().equals(Constant.CartIsSelected.CHECKED)) {
                cartVOArrayListTemp.add(cartVO);
            }
        }
        cartVOList = cartVOArrayListTemp;
        //如果,购物车中没有已经被勾选的商品:就抛出"购物车勾选的商品为空"异常;
        if (CollectionUtils.isEmpty(cartVOList)) {
            throw new ImoocMallException(ImoocMallExceptionEnum.CART_SELECTED_EMPTY);
        }
 
        //判断商品是否存在;如果存在,是否是上架状态;商品库存是否足够;
        validSaleStatusAndStock(cartVOList);
 
        //把【查询购物车表cart表,获得的商品数据】转化为【能够存储到order_item表的、商品数据】
        List<OrderItem> orderItemList = cartVOListToOrderItemList(cartVOList);
 
        //扣库存;(PS:前面有判断库存的逻辑,程序如果能走到这一步,就说明库存是够的)
        for (int i = 0; i < orderItemList.size(); i++) {
            OrderItem orderItem =  orderItemList.get(i);
            //首先,先拿到原先的product
            Product product = productMapper.selectByPrimaryKey(orderItem.getProductId());
            //然后,计算新的库存;
            int stock = product.getStock() - orderItem.getQuantity();
            if (stock < 0) {//上面已经检查过库存了,这儿又判断,是否是重复工作
                throw new ImoocMallException(ImoocMallExceptionEnum.NOT_ENOUGH);
            }
 
            product.setStock(product.getStock() - orderItem.getQuantity());
            //然后,去更新库存;也就是扣库存啦;
            productMapper.updateByPrimaryKeySelective(product);
 
        }
        //把【当前用户的、购物车中已经被勾选的、将要被我们下单的,商品】给删除;也就是,删除cart表中,对应的记录;
        cleanCart(cartVOList);
 
        //编写逻辑,生成一个订单号
        String orderNum = OrderCodeFactory.getOrderCode(Long.valueOf(userId));
        //创建一个订单;
        Order order = new Order();
        order.setOrderNo(orderNum);//设置订单号
        order.setUserId(userId);//设置用户id
        order.setTotalPrice(totalPrice(orderItemList));//设置订单总价
        order.setReceiverName(createOrderReq.getReceiverName());//设置收件人姓名
        order.setReceiverAddress(createOrderReq.getReceiverAddress());//设置收件人地址
        order.setReceiverMobile(createOrderReq.getReceiverMobile());//设置收件人电话
        order.setOrderStatus(Constant.OrderStatusEnum.NOT_PAY.getCode());//设置订单状态
        order.setPostage(0);//运费;我们这儿目前是包邮
        order.setPaymentType(1);//付款方式;我们这儿只有一种1在线支付
        //把这个订单,添加到order表中,新增一个订单记录;
        orderMapper.insertSelective(order);
 
        //也要利用循环,把订单中的每种商品,写到order_item表中;
        for (int i = 0; i < orderItemList.size(); i++) {
            OrderItem orderItem =  orderItemList.get(i);
            orderItem.setOrderNo(orderNum);//给其赋上订单号
            orderItemMapper.insertSelective(orderItem);
        }
 
        //返回结果;
        return orderNum;
    }
 
 
    /**
     * 工具方法:判断列表中的商品是否存在、是否是上架状态、库存是否足够;
     * 规则:购物车中的、已经被勾选的商品;但凡有一种不符合要求,都不行;
     * @param cartVOArrayList
     */
    private void validSaleStatusAndStock(List<CartVO> cartVOArrayList) {
        //循环遍历、判断:【购物车中的、已经被勾选的、每一种商品】
        for (int i = 0; i < cartVOArrayList.size(); i++) {
            CartVO cartVO =  cartVOArrayList.get(i);
            //根据【从购物车中,查到的商品信息】,去查product表;
            Product product = productMapper.selectByPrimaryKey(cartVO.getProductId());
            //如果没查到(说明,商品不存在),或者,商品不是上架状态:就抛出"商品状态不可售"异常;
            if (product == null || !product.getStatus().equals(Constant.SaleStatus.SALE)) {
                throw new ImoocMallException(ImoocMallExceptionEnum.NOT_SALE);
            }
            //判断商品库存,如果库存不足,抛出“商品库存不足异常;
            if (cartVO.getQuantity() > product.getStock()) {
                throw new ImoocMallException(ImoocMallExceptionEnum.NOT_ENOUGH);
            }
        }
    }
 
    /**
     * 工具方法:把【从cart购物车表中,查到的CartVO】转化为【可以存储到order_item表的,OrderItem】;
     * @param cartVOList
     * @return
     */
    private List<OrderItem> cartVOListToOrderItemList(List<CartVO> cartVOList) {
        List<OrderItem> orderItemList = new ArrayList<>();
        for (int i = 0; i < cartVOList.size(); i++) {
            CartVO cartVO = cartVOList.get(i);
            OrderItem orderItem = new OrderItem();
            orderItem.setProductId(cartVO.getProductId());
            //下面的,其实是【商品当前的快照信息】
            orderItem.setProductName(cartVO.getProductName());//商品(当前的)名称
            orderItem.setProductImg(cartVO.getProductImage());//商品(当前的)图片
            orderItem.setUnitPrice(cartVO.getPrice());//商品(当前的)单价
 
            orderItem.setQuantity(cartVO.getQuantity());//该种商品的购买数量
            orderItem.setTotalPrice(cartVO.getTotalPrice());//该种商品的总价
 
            orderItemList.add(orderItem);
        }
        return orderItemList;
    }
 
    /**
     * 工具方法:把【当前用户的、购物车中已经被勾选的、将要被我们下单的,商品】给删除;也就是,删除cart表中,对应的记录;
     * @param cartVOList
     */
    private void cleanCart(List<CartVO> cartVOList) {
        for (int i = 0; i < cartVOList.size(); i++) {
            CartVO cartVO =  cartVOList.get(i);
            cartMapper.deleteByPrimaryKey(cartVO.getId());
        }
    }
 
    /**
     * 工具方法:获取当前订单的总价;也就是该订单中,所用种类商品的总价;
     * @param orderItemList
     * @return
     */
    private Integer totalPrice(List<OrderItem> orderItemList) {
        Integer totalPrice = 0;
        for (int i = 0; i < orderItemList.size(); i++) {
            OrderItem orderItem =  orderItemList.get(i);
            totalPrice += orderItem.getTotalPrice();
        }
        return totalPrice;
    }
 
 
}
 

说明:

(1)通过UserFilter获取当前登录用户;


(2)然后,调用【cartService.list(userId);】去查询【当前用户的、购物车中的、商品状态仍然是上架状态的】所有商品数据;

● 这个方法,是在开发购物车模块的【购物车列表】接口时开发的,其作用是:查询【当前用户的、购物车中的、商品状态仍然是上架状态的】购物车信息;

● 如有需要,可以快速参考【SpringBoot电商项目购物车模块购物车列表接口】;

● 只是需要注意,这个方法的返回结果CartVO是经过处理、组合的,其中属性还是比较丰富的;


(3)然后,筛选出,当前购物车中,那些被勾选的商品;


(4)如果【购物车中,validSaleStatusAndStock没有被勾选的商品】,就抛出一个异常;


(5)编写一个工具方法validSaleStatusAndStock(),去检查,这些被勾选的商品:是否还存在、是否是上架状态、库存是否足够;

(PS:其实,但就这儿的情况来说,上面(2)中的方法,查出来的就已经是:商品存在、商品是上架状态的了;所以,但就这儿的情况来说,validSaleStatusAndStock()方法做了点重复工作)


(6)然后,因为我们在创建订单的时候,需要把订单中商品的信息,存储到order_item表中;所以,我们编写方法,通过CartVO,来构建OrderItem;


(7)扣库存;


(8)然后,删除【当前用户的、购物车中的、商品是上架状态的、库存足够的、被勾选的】那些商品;

很自然,当我们把购物车中某些商品下单后,这些商品是应该从购物车中删除的;

PS:这儿需要明确的时,我们在下单的过程中,任何地方不符合要求,都会抛出异常;如果能一步一步走到这儿,就说明前面是符合要求的;


(9)然后,编写OrderCodeFactory工具类,去生成订单号;

 
     package com.imooc.mall.utils;
 
     import jdk.nashorn.internal.runtime.UnwarrantedOptimismException;
 
     import java.text.DateFormat;
     import java.text.SimpleDateFormat;
     import java.util.Date;
     import java.util.Random;
 
     /**
      * 描述:订单号生成类
      */
     public class OrderCodeFactory {
         //订单号有多种生成规则,主要是原则就是:防止重复;这儿我们采用【时间+随机数】的方式;
 
         /**
          * 以一定格式,获取当前时间
          * @return
          */
         private static String getDateTime() {
             DateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
             return sdf.format(new Date());
         }
 
         /**
          * 生成一个5位的随机数;(当然可以生成3、4、6、7、8……位;只是,这儿为了便于管理、程序明确,这儿我们统一生成5位)
          * @param n
          * @return
          */
         private static int getRandom(Long n) {
             Random random = new Random();
             return (int) (random.nextDouble() * (90000)) + 10000;
         }
 
         /**
          * 生成订单号
          * @param userId
          * @return
          */
         public static String getOrderCode(Long userId) {
             return getDateTime() + getRandom(userId);
         }
     }


(10.1)创建一个订单Order对象,并将其添加到order表中;


(10.2)其中,使用了枚举类,来管理“订单状态”信息;

 
         /**
          * 枚举类,来说明订单状态
          */
         public enum OrderStatusEnum {
             CANCELED(0, "用户已取消"),
             NOT_PAY(10, "未付款"),
             PAID(20,"已付款"),
             DELIVERED(30,"已发货"),
             FINISHED(40,"交易完成");
 
             private int code;
             private String value;
 
             OrderStatusEnum(int code, String value) {
                 this.code = code;
                 this.value = value;
             }
 
             public static OrderStatusEnum codeOf(int code) {
                 for (OrderStatusEnum orderStatusEnum:values() ) {
                     if (orderStatusEnum.getCode() == code) {
                         return orderStatusEnum;
                     }
                 }
                 throw new ImoocMallException(ImoocMallExceptionEnum.NO_ENUM);
 
 
             }
 
             public int getCode() {
                 return code;
             }
 
             public void setCode(int code) {
                 this.code = code;
             }
 
             public String getValue() {
                 return value;
             }
 
             public void setValue(String value) {
                 this.value = value;
             }
         }

● 在本项目中,多次使用了枚举;可以参考【SpringBoot电商项目用户模块枚举类和枚举数据类型】,【SpringBoot电商项目用户模块API统一返回对象】;

● 至于,为什么我们这儿又使用枚举类来定义常量,而不使用内部接口定义常量了;

可以看下,这个回答;(PS:虽然,有点点文不对题,不太能解决自己的疑惑,,,但其内容,还是很有参考价值的)

● 这儿,为了能够通过code,获得对应的value信息,我们编写了codeOf()方法;


(10.3)给剩余的属性赋值,然后插入到order表中;


(11)利用循环,把订单中的每种商品,写到order_item表中;

4.在OrderService接口中,反向生成方法的声明;

5.添加数据库事务;

● 很显然,创建订单的时候,这一个业务逻辑,底层需要多次的增删改操作;所以,我们需要去手动控制事务;

● 有关,事务及Spring Boot中使用事务,可以参考【附加SpringBoot项目手动控制事务包括总结了到目前为止事务的所有内容】;


通过【@Transactional(rollbackFor = Exception.class)】来设置手动控制事务;


三:测试;

可以看到,该用户的购物车中被勾选的商品,已经被删除了;