Go開發實戰-訂單接口的功能分析和代碼開發講解
前面實現購物車模式的時候我們說了,購物車作為商品和訂單的中間角色,讓用戶有機會一次性選購多個商品后再進行下單結賬。
那么用戶在把想要選購的商品加入購物車后,接下來的產品流程就到了進行購物結算了,等用戶支付后,商家或者購物平臺會安排進行物流發貨,虛擬產品則是為用戶開通某些權益。
訂單結算這個產品流程其實包括兩個子流程
- 購物項創建訂單
- 支付訂單
訂單模塊功能分析
首先我們再次看一下需求分析章節中,我們分析出來的業務結構,這里我們重點關注訂單模塊,以及它跟其他模塊靠什么關聯。
圖片
可以看到訂單中,訂單是業務的聚合根,其下還有訂單商品明細和訂單地址。為什么要有后面兩部分呢?在上圖的注釋里我們做了標注:它們是作為訂單的快照信息,因為如果訂單直接跟商品ID和用戶地址ID關聯的話,假如未來商品調整了價格、下架了亦或是用戶搬家更新了地址,萬一哪天“秋后算賬”看看之前的訂單,就會出現訂單信息跟用戶自己當初購買時信息不一致的情況。
訂單模塊在付款之前的功能如下(支付功能等下節再分析):
- 創建訂單
- 訂單查詢
訂單列表
訂單詳情
- 修改訂單 (一般C端此時只允許用戶取消訂單)
創建訂單
我們的購物車數據是保存在服務端的數據表中的,因為這個數據保存在服務端,所以創建訂單這個功能客戶端提交上來的數據就只需要把要生成訂單進行結算的購物項目ID和用戶的地址信息ID提交上來即可。
所以我們先在api/request/order.go 中,定義用戶創建訂單的請求格式如下:
type OrderCreate struct {
CartItemIdList []int64 `json:"cart_item_id_list" binding:"required"`
UserAddressId int64 `json:"user_address_id" binding:"required"`
}
服務端拿到參數后可以自己去檢索對應的購物項信息,然后再去獲取對應的商品信息,進行結算相關的計算。這樣整個過程不需要客戶端過多參與,能最大限度地保證用戶數據的安全。
所以我們在Order的應用服務中的邏輯如下:
func (oas *OrderAppSvc) CreateOrder(orderRequest *request.OrderCreate, userId int64) (*reply.OrderCreateReply, error) {
cartDomainSvc := domainservice.NewCartDomainSvc(oas.ctx)
cartItems, err := cartDomainSvc.GetCheckedCartItems(orderRequest.CartItemIdList, userId)
if err != nil {
returnnil, err
}
userDomainSvc := domainservice.NewUserDomainSvc(oas.ctx)
address, err := userDomainSvc.GetUserSingleAddress(userId, orderRequest.UserAddressId)
if err != nil {
returnnil, err
}
order, err := oas.orderDomainSvc.CreateOrder(cartItems, address)
if err != nil {
returnnil, err
}
orderReply := new(reply.OrderCreateReply)
orderReply.OrderNo = order.OrderNo
return orderReply, nil
}
- 首先調用CartDomainSvc通過購物項ID獲取用戶添加在購物車中的購物項,該方法 GetCheckedCartItems 是我們在購物車模塊中已經實現好的,通過它能獲取購物項信息,其中會包括具體的商品信息、價格、購買數量等。
- 通過用戶的地址信息ID調用UserDomainSvc獲取用戶的詳細地址信息。
- 拿到創建訂單所依賴的信息后,調用OrderDomainSVC 去創建訂單。
OrderDomainSVC中創建訂單的實現如下:
func (ods *OrderDomainSvc) CreateOrder(items []*do.ShoppingCartItem, userAddress *do.UserAddressInfo) (*do.Order, error) {
// 詳細的代碼實現和注釋
// 請訂閱專欄加入項目后查看
return order, err
}
代碼較長這里簡單說下里面的實現步驟:
- 首先我們依賴上節課使用職責鏈實現的CartBillChecker來計算一下訂單商品的總價、優惠金額等結算信息
- 設置用戶訂單的UserId、OrderNo、訂單金額-BillMoney、實際支付金額-PayMoney、訂單狀態。
- 開啟數據庫事務
操作一:保存訂單信息到數據表
操作二:從用戶購物車中刪除已下單商品
操作三:如使用了優惠券,鎖定優惠券,等支付成功后再核銷(項目沒有,這里Mock)
操作四:如參與了滿減活動、記錄相關信息
操作五:減少訂單購買商品的庫存,因為會鎖行記錄, 把這一步放到創建訂單步驟的最后, 減少行記錄加鎖的時間
- 提交/回滾事務。
在實現代碼中我特地使用了GORM手動管理事務的方法,在用戶地址信息維護章節中我已經演示過了GORM自動管理事務的db.Transaction方法,其實我這里用的就是db.Transaction 的內部實現邏輯。
大家可以根據自己的喜好,手動或者自動管理事務,如果讓我選,我還是推薦GORM自己管理事務,別太相信自己,畢竟萬一漏掉一點代碼就是一個BUG,到時候甩鍋都不好甩給GORM,哈哈哈哈。
重啟項目后我們,發起創建訂單請求把購物車中的購物項下單, 請求結果如下。
圖片
整個創建訂單過程中生成訂單號、還有其他的一些代碼大家就去項目里看吧,這里不貼這么多了。接下來我們來看訂單查詢。
訂單查詢
關于訂單查詢,其實主要有一點需要注意,就是我們訂單前臺顯示狀態和訂單在系統中真正的狀態流轉是有一丟丟不一樣的,說大白話就是數據庫里訂單狀態的枚舉值跟用戶在前臺看到的狀態值是不一樣的。
針對訂單狀態,我們在項目的 common/enum/order.go 中定義了如下枚舉值和數據表里的訂單狀態值一一對應。
const (
OrderStatusCreated = iota // 已創建
OrderStatusUnPaid // 待支付
OrderStatusPaid // 已支付
OrderStatusChecked // 檢貨完成
OrderStatusShipped // 已發貨
OrderStatusOnDelivery // 配送中 -- 快遞員上門送貨中
OrderStatusDelivered // 已送達
OrderStatusConfirmReceipt // 已確認收貨
OrderStatusCompleted // 訂單完成
OrderStatusUserQuit // 用戶取消
OrderStatusUnpaidClose // 超時未支付
OrderStatusMerchantClose // 商家關閉訂單
)
但是用戶在前臺看到自己的訂單狀態,往往是像下面這樣。
圖片
所以我們在給客戶端返回用戶的訂單時,關于訂單狀態的前臺展示要做一下轉換才行,盡量不要讓客戶端拿到所有狀態再去轉換,因為如果前后端都有邏輯,維護起來或者做自動化測試這些都會很困難。
這里我對訂單狀態的枚舉值跟前臺顯示狀態做了如下映射,讓我們在返回響應給客戶端前可以把訂單狀態轉換為前臺展示狀態:
// OrderFrontStatus 用戶在前臺看到的訂單狀態
var OrderFrontStatus = map[int]string{
OrderStatusCreated: "待付款",
OrderStatusUnPaid: "待付款",
OrderStatusPaid: "待發貨",
OrderStatusChecked: "待發貨",
OrderStatusShipped: "待收貨",
OrderStatusOnDelivery: "待收貨",
OrderStatusDelivered: "待收貨",
OrderStatusConfirmReceipt: "待評價",
OrderStatusCompleted: "已完成",
OrderStatusUserQuit: "已取消",
OrderStatusUnpaidClose: "已取消",
OrderStatusMerchantClose: "已取消",
}
接下來我們看一下,訂單查詢相關的功能實現。