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)】来设置手动控制事务;
三:测试;
可以看到,该用户的购物车中被勾选的商品,已经被删除了;