下单简单的分成几个步骤:
1. 用户点击“立即购买”或“购物车-结算”进入到“确认订单”页面
2. 在“确认订单”页面选择收货地址,优惠券等,重新计算运费、订单价格
3. 提交订单,选择支付方式进行支付
4. 支付完毕
## 第一步:
1. 用户点击“立即购买”或“购物车-结算”进入到“确认订单”页面,相关url`/p/order/confirm`
我们希望能够有个统一下单的接口,不太希望“立即购买”和“购物车-结算”两个不同的接口影响到后面所有的流程,毕竟谁也不想一个差不多一样的接口,要写两遍,所以我们看下我们的系统是如何做的。
```java
public class OrderParam {
@ApiModelProperty(value = "购物车id 数组")
private List<Long> basketIds;
@ApiModelProperty(value = "立即购买时提交的商品项")
private OrderItemParam orderItem;
}
```
这里使用了两种情况:
- 假设`basketIds` 不为空,则说明是从购物车进入
- 假设`orderItem` 不为空,则说明是从立即购买进入
通过`basketService.getShopCartItemsByOrderItems(orderParam.getBasketIds(),orderParam.getOrderItem(),userId)` 这个方法对两种情况进行组合,此时并不能将购物车商品删除,因为删除购物车中的商品,是在第三步提交订单的时候进行的,不然用户点击返回键,看到购物车里面的东西还没提交订单,东西就消失了,会感觉很奇怪。
我们重新回到`controller`层,我们看到了一行熟悉的代码`basketService.getShopCarts`
```java
@PostMapping("/confirm")
@ApiOperation(value = "结算,生成订单信息", notes = "传入下单所需要的参数进行下单")
public ResponseEntity<ShopCartOrderMergerDto> confirm(@Valid @RequestBody OrderParam orderParam) {
// 根据店铺组装购车中的商品信息,返回每个店铺中的购物车商品信息
List<ShopCartDto> shopCarts = basketService.getShopCarts(shopCartItems);
}
```
这行代码我们再《购物车的设计》这篇已经着重讲过了,但是我们在这为什么还需要这个东西呢?
很简单,无论是点击“立即购买”或“购物车-结算”,事实上都是通过用户计算过一遍金额了,而且甚至有满减满折之类的活动,都是通过了统一的计算的。而这一套计算的流程,我们并不希望重新写一遍。所以当然是能够使用之前计算的金额,那是最好的咯。
## 第二步:
2. 在“确认订单”页面选择收货地址,优惠券等,重新计算运费、订单价格
我们知道无论是在第一步还是第二步,本质上还是在确认订单的页面,其中订单页面的数据结构并没有发生任何的变化,所以其实第一步第二步是可以写在一起的。所以我们可以看到`OrderParam` 还多了两个参数
```java
public class OrderParam {
@ApiModelProperty(value = "地址ID,0为默认地址",required=true)
@NotNull(message = "地址不能为空")
private Long addrId;
@ApiModelProperty(value = "用户是否改变了优惠券的选择,如果用户改变了优惠券的选择,则完全根据传入参数进行优惠券的选择")
private Integer userChangeCoupon;
@ApiModelProperty(value = "优惠券id数组")
private List<Long> couponIds;
}
```
但是有个问题,就是在于用户点击立即购买的时候,没有地址,那样如何计算运费呢?答案就是使用默认地址进行计算呀~
我们看下计算订单的事件,事实上有很多营销活动的时候,订单的计算也是非常的复杂的,所以我们和购物车一样,采用事件的驱动,一个接一个的对订单进行“装饰”,最后生成`ShopCartOrderMergerDto`一个合并的对象
```java
@PostMapping("/confirm")
@ApiOperation(value = "结算,生成订单信息", notes = "传入下单所需要的参数进行下单")
public ResponseEntity<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{
@ApiModelProperty(value = "实际总值", required = true)
private Double actualTotal;
@ApiModelProperty(value = "商品总值", required = true)
private Double total;
@ApiModelProperty(value = "商品总数", required = true)
private Integer totalCount;
@ApiModelProperty(value = "订单优惠金额(所有店铺优惠金额相加)", required = true)
private Double orderReduce;
@ApiModelProperty(value = "地址Dto", required = true)
private UserAddrDto userAddr;
@ApiModelProperty(value = "每个店铺的购物车信息", required = true)
private List<ShopCartOrderDto> shopCartOrders;
@ApiModelProperty(value = "整个订单可以使用的优惠券列表", required = true)
private List<CouponOrderDto> coupons;
}
```
这里又有一段我们熟悉的代码:
```java
@ApiModelProperty(value = "每个店铺的购物车信息", required = true)
private List<ShopCartOrderDto> shopCartOrders;
```
没错这里返回的数据格式,和购物车的格式是一样的,因为第一步当中已经说明,订单来自于购物车的计算,所以会在基础上条件新的数据,基本上就是返回给前端的数据了。
- 开发环境准备
- 基本开发手册
- 项目目录结构
- 权限管理
- 通用分页表格
- Swagger文档
- undertow容器
- 对xss攻击的防御
- 分布式锁
- 统一的系统日志
- 统一验证
- 统一异常处理
- 文件上传下载
- 一对多、多对多分页
- 认证与授权
- 从授权开始看源码
- 自己写个授权的方法-开源版
- 商城表设计
- 商品信息
- 商品分组
- 购物车
- 订单
- 地区管理
- 运费模板
- 接口设计
- 必读
- 购物车的设计
- 订单设计-确认订单
- 订单设计-提交订单
- 订单设计-支付
- 生产环境
- nginx安装与跨域配置
- 安装mysql
- 安装redis
- 传统方式部署项目
- docker
- 使用docker部署商城
- centos jdk安装
- docker centos 安装
- Docker Compose 安装与卸载
- docker 镜像的基本操作
- docker 容器的基本操作
- 通过yum安装maven
- 常见问题