Go項(xiàng)目實(shí)戰(zhàn)-關(guān)于列表分頁的封裝和簡化
我們實(shí)現(xiàn)了商品模塊中商品分類相關(guān)的功能,這節(jié)我們繼續(xù)商品模塊的開發(fā)來實(shí)現(xiàn)商品詳細(xì)相關(guān)的功能,這些功能在我們梳理出來的功能用例中,我標(biāo)記了出來。
圖片
從功能用例中我們能看到與商品相關(guān)的主要功能有:
- 商品列表
- 商品搜索
- 商品詳情
我們會實(shí)現(xiàn)商品模塊的主要功能接口,在其中會實(shí)際應(yīng)用一下我們在搭建項(xiàng)目定制化的響應(yīng)組件中的Pagination,來簡化分頁查詢相關(guān)的操作,在代碼實(shí)現(xiàn)上也比普通的方式更優(yōu)雅一些。
商品列表
接下來我們來實(shí)現(xiàn)商品列表功能的接口, 當(dāng)然真正商用級別的購物App,商品列表應(yīng)該是通過 Lucene或者是ElasticSearch來實(shí)現(xiàn)的查找的。我們這里沒有這個硬件條件,就先給大家講一下通過數(shù)據(jù)庫查詢實(shí)現(xiàn)功能的邏輯吧。
在購物網(wǎng)站上,我們點(diǎn)擊每個分類的時候,會展示分類下的商品列表。
這個時候有個內(nèi)部邏輯,商品都是掛在到三級分類上的,也就是分類的葉子節(jié)點(diǎn)上。 那么此時我們開發(fā)功能時要能夠兼顧下面幾點(diǎn):
- 產(chǎn)品的交互邏輯上可能允許用戶點(diǎn)擊一級或者二級分類查看商品列表。
- 因不同公司產(chǎn)品邏輯而定,沒有絕對,但是我們的功能實(shí)現(xiàn)時要支持上面的情況,假設(shè)用戶選擇了一級或者二級分類,我們的程序需要先查出下面的三級分類,再通過這些三級分類查找對應(yīng)的商品。
以上是業(yè)務(wù)方面的邏輯,在做本功能的時候我還會演示怎么通過我們之前定義分頁組件Pagination,以一個相對優(yōu)雅的寫法寫數(shù)據(jù)庫的分頁查詢。
在 api/controller/commodity.go 中添加商品列表的Controller方法。
// CommoditiesInCategory 分類商品列表
func CommoditiesInCategory(c *gin.Context) {
categoryId, _ := strconv.ParseInt(c.Query("category_id"), 10, 64)
pagination := app.NewPagination(c)
svc := appservice.NewCommodityAppSvc(c)
commodityList, err := svc.GetCategoryCommodityList(categoryId, pagination)
if err != nil {
if errors.Is(err, errcode.ErrParams) {
app.NewResponse(c).Error(errcode.ErrParams)
} else {
app.NewResponse(c).Error(errcode.ErrServer.WithCause(err))
}
return
}
app.NewResponse(c).SetPagination(pagination).Success(commodityList)
}
我們在Controller方法中除了從URL查詢字符串上獲取商品的分類ID外,還要獲取分頁相關(guān)的請求參數(shù),用它們創(chuàng)建Pagination對象。Pagination 對象會隨著我們的調(diào)用一直往下傳遞,傳到DomainService中,在需要的時候通過其上的方法來獲取offset 和 limit 等信息。
DomainService 中查詢商品列表的邏輯如下:
// logic/domainservice/commodity.go
// GetCommodityListInCategory 獲取分類下的商品列表
func (cds *CommodityDomainSvc) GetCommodityListInCategory(categoryInfo *do.CommodityCategory, pagination *app.Pagination) ([]*do.Commodity, error) {
offset := pagination.Offset()
size := pagination.GetPageSize()
thirdLevelCategoryIds, err := cds.commodityDao.GetThirdLevelCategories(categoryInfo)
if err != nil {
returnnil, errcode.Wrap("GetCommodityListInCategoryError", err)
}
commodityModelList, totalRows, err := cds.commodityDao.GetCommoditiesInCategory(thirdLevelCategoryIds, offset, size)
if err != nil {
returnnil, errcode.Wrap("GetCommodityListInCategoryError", err)
}
pagination.SetTotalRows(int(totalRows))
commodityList := make([]*do.Commodity, 0, len(commodityModelList))
err = util.CopyProperties(&commodityList, &commodityModelList)
if err != nil {
returnnil, errcode.ErrCoverData.WithCause(err)
}
return commodityList, nil
}
commodityDao 的 GetThirdLevelCategories 方法就是我們上面說的產(chǎn)品邏輯,拿到一個分類信息后先去獲取一下所有的三級分類。
// GetThirdLevelCategories 查找分類下的所有三級分類ID
func (cd *CommodityDao) GetThirdLevelCategories(categoryInfo *do.CommodityCategory) (categoryIds []int64, err error) {
if categoryInfo.Level == 3 {
return []int64{categoryInfo.ID}, nil
} elseif categoryInfo.Level == 2 {
categoryIds, err = cd.getSubCategoryIdList([]int64{categoryInfo.ID})
return
} elseif categoryInfo.Level == 1 {
var secondCategoryId []int64
secondCategoryId, err = cd.getSubCategoryIdList([]int64{categoryInfo.ID})
if err != nil {
return
}
categoryIds, err = cd.getSubCategoryIdList(secondCategoryId)
return
}
return
}
如果分類本身就是三級分類則直接返回,否則還是按照上面說的邏輯把分類下的所有三級分類先找出來。
分頁查詢簡化
查詢商品信息時因?yàn)樾枰猪?,所以我們在CommodityDomainSvc 里先用Pagination獲取分頁數(shù)據(jù)需要的offset 和 limit 參數(shù)。
func (cds *CommodityDomainSvc) GetCommodityListInCategory(categoryInfo *do.CommodityCategory, pagination *app.Pagination) ([]*do.Commodity, error) {
offset := pagination.Offset()
size := pagination.GetPageSize()
.....
commodityModelList, totalRows, err := cds.commodityDao.GetCommoditiesInCategory(thirdLevelCategoryIds, offset, size)
if err != nil {
return nil, errcode.Wrap("GetCommodityListInCategoryError", err)
}
pagination.SetTotalRows(int(totalRows))
......
}
商品數(shù)據(jù)查詢的Dao方法除了返回商品列表數(shù)據(jù),還會返回滿足條件的總行數(shù),這樣我們把總行數(shù)再設(shè)置到Pagination對象上,因?yàn)镻agination是指針類型,它創(chuàng)建子Controller方法,所以我們在Controller中返回響應(yīng)時直接使用 app.NewResponse(c).SetPagination(pagination) 就能把分頁查詢需要的信息都寫入到響應(yīng)中。
通過這種方式我們能避免需要分頁查詢數(shù)據(jù)的接口對應(yīng)的AppService、DomainService、Dao方法都帶上page、pageSize等參數(shù),只需要在調(diào)用Dao方法查詢數(shù)據(jù)前從Pagination對象對應(yīng)的方法中取出這兩個值即可。同理所有方法的返回值中也不用都帶著totalRow 這個返回值。