电商+支付双系统项目------电商系统中订单模块的开发

这篇文章是关于项目的订单模块的设计。这个模块其实相对来讲比之前的模块复杂了点,我这里说的复杂并不是说难以理解,而是说文件比较多,理解起来还是蛮轻松的。

我们还是老方法,一步一步的去设计,按照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):

  1. 订单号:202402240001

    • 商品ID:1001
    • 商品名称:商品A
    • 商品数量:2
    • 商品价格:$10
  2. 订单号:202402240001

    • 商品ID:1002
    • 商品名称:商品B
    • 商品数量:1
    • 商品价格:$20
  3. 订单号: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);
	}

这个方法太长了,我理解了好久,就说一下我自己的理解。

  1. 根据用户ID和收货地址ID查询对应的收货地址信息。
  2. 判断收货地址是否存在,如果不存在则返回错误响应。
  3. 获取用户购物车中选中的商品列表(已经勾选的商品)。
  4. 判断购物车是否为空,如果为空则返回错误响应。
  5. 从购物车列表中获取商品ID集合,并根据商品ID集合查询对应的商品信息。
  6. 构建订单项列表,遍历购物车列表,根据购物车项信息和商品信息构建订单项,并将订单项添加到订单项列表中。
  7. 生成订单号(orderNo)。
  8. 遍历订单项列表,依次处理每个订单项:
    • 根据商品ID获取对应的商品信息。
    • 判断商品是否存在,如果不存在则返回错误响应。
    • 判断商品的上下架状态,如果不是在售状态则返回错误响应。
    • 判断商品库存是否充足,如果不充足则返回错误响应。
    • 构建订单项对象,并将其添加到订单项列表中。
    • 减少商品库存数量,并更新数据库中的商品库存信息。
  9. 构建订单对象,包括用户ID、订单号、收货地址ID和订单项列表等信息。
  10. 将订单信息插入到数据库中。
  11. 将订单项列表插入到数据库中。
  12. 遍历购物车列表,删除购物车中已选中的商品。
  13. 构建订单视图对象(OrderVo),包括订单信息、订单项列表和收货地址信息等。
  14. 返回成功响应,并将订单视图对象作为响应数据返回。

这是真的复杂,但是我感觉就稍微看看,理解下原来是这么回事儿,脑子里有创建订单的流程就好了。

再来实现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);
	}
  1. 使用PageHelper工具类设置分页参数,即设置页码和每页显示数量。
  2. 根据用户ID查询对应的订单列表。
  3. 从订单列表中提取订单号集合。
  4. 根据订单号集合查询对应的订单项列表。
  5. 将订单项列表按订单号进行分组,构建订单号与订单项列表的映射关系。
  6. 从订单列表中提取收货地址ID集合。
  7. 根据收货地址ID集合查询对应的收货地址列表。
  8. 将收货地址列表按ID进行映射,构建收货地址ID与收货地址对象的映射关系。
  9. 遍历订单列表,逐个构建订单视图对象(OrderVo):
    • 根据订单信息、订单项映射和收货地址映射构建订单视图对象。
    • 将订单视图对象添加到订单视图列表中。
  10. 构建分页信息对象(PageInfo),其中包括订单列表和总记录数等信息。
  11. 将订单视图列表设置到分页信息对象中。
  12. 返回成功响应,将分页信息对象作为响应数据返回。

这也很复杂,就随便看看吧。

再来实现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);
	}
  1. 根据订单号查询对应的订单信息。
  2. 判断订单是否存在或者订单所属用户是否与传入的用户ID匹配,如果不匹配则返回错误响应。
  3. 创建订单号集合,并将当前订单号添加到集合中。
  4. 根据订单号集合查询对应的订单项列表。
  5. 根据订单的收货地址ID查询对应的收货地址信息。
  6. 根据订单信息、订单项列表和收货地址信息构建订单视图对象(OrderVo)。
  7. 返回成功响应,将订单视图对象作为响应数据返回。

再来实现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();
	}
  1. 根据订单号查询对应的订单信息。
  2. 判断订单是否存在或订单所属用户是否与传入的用户ID匹配,如果不匹配则返回错误响应。
  3. 判断订单状态是否为未付款状态,如果不是则返回错误响应。
  4. 将订单状态设置为已取消状态,并设置取消时间为当前时间。
  5. 更新数据库中的订单信息。
  6. 判断更新结果,如果影响的行数小于等于0,则返回错误响应。
  7. 返回成功响应。

这里也是随便看看。只要你脑补到有这个画面就行。

最后设计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);
	}
}
  1. create() 方法用于创建订单。通过接收一个包含订单创建表单数据的 POST 请求,并从会话中获取当前用户信息,调用订单服务的 create() 方法来创建订单,并返回相应的响应结果。

  2. list() 方法用于获取订单列表。通过接收 GET 请求中的页码和每页数量参数,并从会话中获取当前用户信息,调用订单服务的 list() 方法来获取当前用户的订单列表,并返回相应的响应结果。

  3. detail() 方法用于获取订单详情。通过接收 GET 请求中的订单号参数,并从会话中获取当前用户信息,调用订单服务的 detail() 方法来获取指定订单的详情,并返回相应的响应结果。

  4. cancel() 方法用于取消订单。通过接收 PUT 请求中的订单号参数,并从会话中获取当前用户信息,调用订单服务的 cancel() 方法来取消指定订单,并返回相应的响应结果。

这些方法使用了依赖注入(@Autowired)来获取订单服务(IOrderService)的实例,以便调用订单相关的业务逻辑。同时,使用了会话(HttpSession)来获取当前用户信息。根据不同的请求类型和参数,调用相应的订单服务方法,并返回相应的响应给客户端。

这就是订单模块的所有实现,我感觉订单模块其实理解起来并不难,主要是太多了,所以我感觉应该静下心来仔细想想service层的整个流程,service层一旦掌握了,那整个模块应该很容易就掌握了。

  • 29
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值