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