下單簡單的分成幾個步驟:
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
- 常見問題