下单简单的分成几个步骤：

1. 用户点击“立即购买”或“购物车-结算”进入到“确认订单”页面
2. 在“确认订单”页面选择收货地址，优惠券等，重新计算运费、订单价格
3. 提交订单，选择支付方式进行支付
4. 支付完毕



## 第一步：

1. 用户点击“立即购买”或“购物车-结算”进入到“确认订单”页面，相关url`/p/order/confirm`

我们希望能够有个统一下单的接口，不太希望“立即购买”和“购物车-结算”两个不同的接口影响到后面所有的流程，毕竟谁也不想一个差不多一样的接口，要写两遍，所以我们看下我们的系统是如何做的。



```java
public class OrderParam {
	@Schema(description = "购物车id 数组" )
	private List<Long> basketIds;

	@Schema(description = "立即购买时提交的商品项" )
	private OrderItemParam orderItem;
}
```

这里使用了两种情况：

- 假设`basketIds` 不为空，则说明是从购物车进入
- 假设`orderItem` 不为空，则说明是从立即购买进入

通过`basketService.getShopCartItemsByOrderItems(orderParam.getBasketIds(),orderParam.getOrderItem(),userId)` 这个方法对两种情况进行组合，此时并不能将购物车商品删除，因为删除购物车中的商品，是在第三步提交订单的时候进行的，不然用户点击返回键，看到购物车里面的东西还没提交订单，东西就消失了，会感觉很奇怪。



我们重新回到`controller`层，我们看到了一行熟悉的代码`basketService.getShopCarts`

```java
    @PostMapping("/confirm")
    @Operation(summary = "结算，生成订单信息" , description = "传入下单所需要的参数进行下单")
    public ServerResponseEntity<ShopCartOrderMergerDto> confirm(@Valid @RequestBody OrderParam orderParam) {
    // 根据店铺组装购车中的商品信息，返回每个店铺中的购物车商品信息
    List<ShopCartDto> shopCarts = basketService.getShopCarts(shopCartItems);
    }
```

这行代码我们再《购物车的设计》这篇已经着重讲过了，但是我们在这为什么还需要这个东西呢？

很简单，无论是点击“立即购买”或“购物车-结算”，事实上都是通过用户计算过一遍金额了，而且甚至有满减满折之类的活动，都是通过了统一的计算的。而这一套计算的流程，我们并不希望重新写一遍。所以当然是能够使用之前计算的金额，那是最好的咯。





## 第二步：

2. 在“确认订单”页面选择收货地址，优惠券等，重新计算运费、订单价格

我们知道无论是在第一步还是第二步，本质上还是在确认订单的页面，其中订单页面的数据结构并没有发生任何的变化，所以其实第一步第二步是可以写在一起的。所以我们可以看到`OrderParam` 还多了两个参数

```java
public class OrderParam {
	@Schema(description = "地址ID，0为默认地址" ,required=true)
	@NotNull(message = "地址不能为空")
	private Long addrId;
	
	@Schema(description = "用户是否改变了优惠券的选择，如果用户改变了优惠券的选择，则完全根据传入参数进行优惠券的选择" )
	private Integer userChangeCoupon;

	@Schema(description = "优惠券id数组" )
	private List<Long> couponIds;
}
```

但是有个问题，就是在于用户点击立即购买的时候，没有地址，那样如何计算运费呢？答案就是使用默认地址进行计算呀~



我们看下计算订单的事件，事实上有很多营销活动的时候，订单的计算也是非常的复杂的，所以我们和购物车一样，采用事件的驱动，一个接一个的对订单进行“装饰”，最后生成`ShopCartOrderMergerDto`一个合并的对象

```java
    @PostMapping("/confirm")
    @Operation(summary = "结算，生成订单信息" , description = "传入下单所需要的参数进行下单")
    public ServerResponseEntity<ShopCartOrderMergerDto> confirm(@Valid @RequestBody OrderParam orderParam) {
        for (ShopCartDto shopCart : shopCarts) {
            applicationContext.publishEvent(new ConfirmOrderEvent(shopCartOrder,orderParam,shopAllShopCartItems));

        }
    }
```

我们看下`ConfirmOrderListener` 这个事件里面的默认监听器，这里



```java
public class ConfirmOrderListener {
        @EventListener(ConfirmOrderEvent.class)
    @Order(ConfirmOrderOrder.DEFAULT)
    public void defaultConfirmOrderEvent(ConfirmOrderEvent event) {


        ShopCartOrderDto shopCartOrderDto = event.getShopCartOrderDto();

        OrderParam orderParam = event.getOrderParam();

        String userId = SecurityUtils.getUser().getUserId();

        // 订单的地址信息
        UserAddr userAddr = userAddrService.getUserAddrByUserId(orderParam.getAddrId(), userId);

        double total = 0.0;

        int totalCount = 0;

        double transfee = 0.0;

        for (ShopCartItemDto shopCartItem : event.getShopCartItems()) {
            // 获取商品信息
            Product product = productService.getProductByProdId(shopCartItem.getProdId());
            // 获取sku信息
            Sku sku = skuService.getSkuBySkuId(shopCartItem.getSkuId());
            if (product == null || sku == null) {
                throw new YamiShopBindException("购物车包含无法识别的商品");
            }
            if (product.getStatus() != 1 || sku.getStatus() != 1) {
                throw new YamiShopBindException("商品[" + sku.getProdName() + "]已下架");
            }

            totalCount = shopCartItem.getProdCount() + totalCount;
            total = Arith.add(shopCartItem.getProductTotalAmount(), total);
            // 用户地址如果为空，则表示该用户从未设置过任何地址相关信息
            if (userAddr != null) {
                // 每个产品的运费相加
                transfee = Arith.add(transfee, transportManagerService.calculateTransfee(shopCartItem, userAddr));
            }

            shopCartItem.setActualTotal(shopCartItem.getProductTotalAmount());
            shopCartOrderDto.setActualTotal(Arith.sub(total, transfee));
            shopCartOrderDto.setTotal(total);
            shopCartOrderDto.setTotalCount(totalCount);
            shopCartOrderDto.setTransfee(transfee);
        }
    }
}
```

值得留意的是，有那么一行代码

```java
        // 用户地址如果为空，则表示该用户从未设置过任何地址相关信息
        if (userAddr != null) {
            // 每个产品的运费相加
            transfee = Arith.add(transfee, transportManagerService.calculateTransfee(shopCartItem, userAddr));
        }
```
运费是根据用户地址进行计算，当然还包括运费模板啦，想了解运费模板的，可以参考运费模板相关的章节。

那么有人就问了，那么优惠券呢？优惠券是有另一个监听器进行监听计算价格啦，购买了专业版或以上的版本就能看到源码咯~



我们看看返回给前端的订单信息：

```java
@Data
public class ShopCartOrderMergerDto implements Serializable{

    @Schema(description = "实际总值" , required = true)
    private Double actualTotal;

    @Schema(description = "商品总值" , required = true)
    private Double total;

    @Schema(description = "商品总数" , required = true)
    private Integer totalCount;

    @Schema(description = "订单优惠金额(所有店铺优惠金额相加)" , required = true)
    private Double orderReduce;

    @Schema(description = "地址Dto" , required = true)
    private UserAddrDto userAddr;

    @Schema(description = "每个店铺的购物车信息" , required = true)
    private List<ShopCartOrderDto> shopCartOrders;

    @Schema(description = "整个订单可以使用的优惠券列表" , required = true)
    private List<CouponOrderDto> coupons;
}

```

这里又有一段我们熟悉的代码：

```java
@Schema(description = "每个店铺的购物车信息" , required = true)
private List<ShopCartOrderDto> shopCartOrders;
```
没错这里返回的数据格式，和购物车的格式是一样的，因为第一步当中已经说明，订单来自于购物车的计算，所以会在基础上条件新的数据，基本上就是返回给前端的数据了。
