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