Go WEB進階實戰:基於GoFrame搭建的電商前後臺API系統

語言: CN / TW / HK

highlight: a11y-dark theme: Chinese-red


本文為掘金社群首發簽約文章,14天內禁止轉載,14天后未獲授權禁止轉載,侵權必究!

前言

最近有很多小夥伴私信我:在學完Go基礎後,想使用一個框架實戰一個商業專案,但是又苦於不知道選擇什麼框架,更不知道做什麼商業專案。

為了解決大家這些問題,我結合自己的專案經歷,為大家開源了一個簡單易上手的Go電商前後臺系統API,這個專案不僅有電商系統常用的功能點,還濃縮了我開發Go積累的一些經驗。

這篇文章適合學完了Go基礎,計劃基於成熟框架開發web專案的同學。

GoFrame簡介

GoFrame是類似PHP-Laravel, Java-SpringBoot的Go企業級開發框架,是一個非常值得學習的Go框架。

之前寫過文章介紹GoFrame為什麼值得學習: # 非常適合PHP同學使用的GO框架:GoFrame,以及# 為什麼我覺得GoFrame的garray比PHP的array還好用?

大家可以閱讀一下,增加使用GoFrame進階實戰的動力。

經驗分享

我用GoFrame開發了不少商業專案,剛開始用的時候也有很多不順手的地方,基本是一邊看官方文件一邊寫專案。

隨著做的專案多了,遇到的問題多了,發現剛開始寫的程式碼實在很爛。

這套開原始碼不敢說多優雅,起碼很規範,在這套原始碼基礎上可以快速進行功能模組的開發,也封裝了常用的工具類。

對大家提高學習Go的效率,應該會有幫助。

官方示例

官方基於最新的v2.x版本提供了示例,從以下角度演示瞭如何快速搭建單體API Service: 1. 介面定義 2. 路由註冊 3. 常量管理 4. 控制器定義 5. 資料庫訪問 - 驅動引入 - 資料庫配置 - dao程式碼生成 6. 建立業務模型 7. 提供服務介面 8. 業務實現 - 依賴注入 - 增加引用 9. 介面測試

官方的示例非常規範,但是過於簡單。基礎薄弱的小夥伴可以先實踐官方示例,再實踐我的電商專案。

進階教程:電商前後臺系統

作為入門級電商系統包括了常規的功能點,下面我重點說一下能學到技術上的知識點:

  1. 如何使用gtoken實現單點登入?
  2. 如何自定義中介軟體?
  3. 如何自定義服務?
  4. 如何定義路由組,明確介面邊界?
  5. 如何上傳圖片到雲平臺?
  6. 如何靈活的設定搜尋條件?
  7. 如何用一個專案,提供前後臺的2套API介面?
  8. 如何實現自動編譯?
  9. 如何使用shell指令碼一鍵部署專案到遠端伺服器?

說明:GoFrame的官方文件和示例能帶你快速入門GoFrame框架和CLI工具的使用,不作為這篇文章的重點。

這篇文章的重點是:能帶你更進一步,基於良好的規範,開發比較複雜的商業專案。 下面就和我一起學習吧,文章最後我會分享給大家這個專案的github地址以及對大家學習有幫助的文件資料。

先看目錄

整體結構

image.png

重點看app目錄

app目錄是我們要重點開發的部分

image.png

開始實戰

提示:為了行文緊湊,方便大家理解。與核心知識點無關的程式碼會直接省略或用會三個豎著的.簡化。文章最後會提供GitHub地址,開源專案。

1. GToken實現單點登入

1. 檢視自己的版本

首先,我們要確定自己安裝的gf版本,通過gf version命令就可以查看了。

image.png

注意:gtoken v1.5.0全面適配GoFrame v2.0.0 ; GoFrame v1.X.X 請使用GfToken v1.4.X相關版本

根據自己的版本安裝合適的gtoken

2. 安裝最新版gtoken

go get github.com/goflyfox/gtoken

3. 安裝指定版本gtoken

@指定的版本號就可以了:

go get github.com/goflyfox/[email protected]

2. 自定義中介軟體

我們以編寫gtoken中介軟體為例,帶大家寫一個自己的中介軟體:

在我們的app/middleware目錄下新建token.go檔案

編寫gtoken中介軟體的目的: 1. 全域性校驗使用者的登入狀態 2. 登入後的使用者將使用者名稱、id這類使用者資訊寫入到Context上下中,方便全域性呼叫 3. 在中介軟體中統一進行賬號判斷,比如:是否被拉黑等判斷操作

我們來看具體的實現: ``` package middleware

const ( CtxAccountId = "account_id" //token獲取 . . . )

type TokenInfo struct { Id int Name string . . . }

var GToken *gtoken.GfToken

var MiddlewareGToken = tokenMiddleware{}

type tokenMiddleware struct{}

func (s tokenMiddleware) GetToken(r ghttp.Request) { var tokenInfo TokenInfo token := GToken.GetTokenData(r) err := gconv.Struct(token.GetString("data"), &tokenInfo) if err != nil { response.Auth(r) return } //賬號被凍結拉黑 if tokenInfo.Status == 2 { response.AuthBlack(r) return } r.SetCtxVar(CtxAccountId, tokenInfo.Id) . . . r.Middleware.Next() } ```

3. 註冊中介軟體

我們在app/system/frontend/ 目錄下新建 router.go 檔案,用來定義客戶端的路由:

  1. 首先編寫gtoken登入註冊等方法
  2. 然後在路由檔案中使用 group.Middleware() 把自定義的中介軟體註冊到路由組中。
  3. 注意:不需要校驗登入狀態的介面寫在 group.Middleware(middleware.MiddlewareGToken.GetToken) 之前,需要校驗登入狀態的寫在之後。

```go package frontend

//前端專案登入 func Login() { // 啟動gtoken middleware.GToken = &gtoken.GfToken{ //都用預設的 //Timeout: gconv.Int(g.Cfg().Get("gtoken.timeout")) * gconv.Int(gtime.M), //MaxRefresh: 60 * 1000, //單位毫秒 登入1分鐘後有請求操作則主動重新整理token有效期 CacheMode: 2, LoginPath: "/frontend/sso/login", LogoutPath: "/frontend/sso/logout", AuthPaths: g.SliceStr{}, //AuthPaths: g.SliceStr{"/backend"}, AuthExcludePaths: g.SliceStr{}, GlobalMiddleware: true, // 開啟全域性攔截 //MultiLogin: g.Config().GetBool("gtoken.multi-login"), LoginBeforeFunc: frontendLogin.FrontendLogin.Login, LoginAfterFunc: frontendLogin.FrontendLogin.LoginAfterFunc, LogoutAfterFunc: frontendLogin.FrontendLogin.Logout, AuthAfterFunc: frontendLogin.FrontendLogin.AuthAfterFunc, } middleware.GToken.Start() }

func Init(s ghttp.Server) { Login() s.Group("/frontend/", func(group ghttp.RouterGroup) { //不需要登入的 //上傳檔案 group.Group("upload/", func(group *ghttp.RouterGroup) { group.POST("img/", upload.Upload.Img) })

  //以下是需要登入的
  group.Middleware(middleware.MiddlewareGToken.GetToken)
  //登入賬號相關
  group.Group("sso/", func(group *ghttp.RouterGroup) {
     group.POST("password/update", frontendLogin.FrontendLogin.UpdatePassword)
  })

}) } ```

4. 自定義服務

細心的小夥伴已經發現了問題:在路由檔案中寫的 frontendLogin.FrontendLogin.Login是在哪裡定義的呢?

沒錯,我們定義成了服務。

我們可以把登入註冊這類通用的功能抽取出來,定義成通用的服務:

我們建立 app/service/frontendLogin 目錄,在這個目錄下再依次建立: - define.go:用於定義登入註冊需要的結構體 - service.go:用於編寫業務邏輯,比如校驗登入密碼是否正確 - api.go:用於提供介面,比如frontendLogin.FrontendLogin.Login就是在這裡定義的

define.go簡化示例

```go package frontendLogin

type RegisterReq struct { Name string json:"name" v:"required#使用者名稱必傳" PassWord string json:"password" v:"required-if:type,0|password#password必須傳遞|密碼限定在6-18位之間" Avatar string json:"avatar" Sex int json:"sex" Sign string json:"sign" SecretAnswer string json:"secret_answer" UserSalt string json:"user_salt,omitempty" }

type AccessTokenRes struct { AccessToken string json:"access_token" //獲取到的憑證 ExpiresIn int json:"expires_in" //憑證有效時間,單位:秒 }

. . . ```

service.go簡化示例

```go package frontendLogin

import ( "context" . . . )

var service = frontendLoginService{}

type frontendLoginService struct { }

. . .

//註冊 func (s frontendLoginService) Register(ctx context.Context, req RegisterReq) (err error) { //查詢使用者名稱是否存在 count, err := dao.UserInfo.Ctx(ctx).Where("name", req.Name).Count() if err != nil || count > 0 { return gerror.New("使用者名稱已存在,請換個使用者名稱註冊賬號吧") }

UserSalt := grand.S(10) req.PassWord = library.EncryptPassword(req.PassWord, UserSalt) req.UserSalt = UserSalt //新增新使用者 _, err = dao.UserInfo.Ctx(ctx).Insert(req) if err != nil { return err } return }

. . . ```

api.go簡化示例

```go package frontendLogin

import ( "github.com/goflyfox/gtoken/gtoken" . . . )

var FrontendLogin = new(frontendLogin)

type frontendLogin struct { }

//註冊 func (s frontendLogin) Register(r ghttp.Request) { var data *RegisterReq if err := r.Parse(&data); err != nil { response.ParamErr(r, err) } err := service.Register(r.Context(), data) if err != nil { response.JsonExit(r, 0, "註冊失敗") } else { response.SuccessWithData(r, nil) } } . . . ```

好了,到這裡我們就完成了gtoken的整合,並且自己編寫了中介軟體,編寫了服務。

對Gtoken實現原理感興趣的小夥伴可以閱讀這篇文章:# 通過閱讀原始碼解決專案難題:GToken替換JWT實現SSO單點登入

下面再帶大家重點看一下前面提到的路由檔案:

4.定義路由組,明確介面邊界

建議大家使用分組路由進行路由的管理,能讓我們的路由管理更加的清晰規範。

我們根據業務邏輯拆分路由組,比如下面程式碼中的商品管理,文章管理,點贊管理就很清晰。

能保證隨著專案的迭代也能在指定的路由組中進行管理,明確介面邊界。

func Init(s *ghttp.Server) { Login() s.Group("/frontend/", func(group *ghttp.RouterGroup) { //商品 group.Group("goods/", func(group *ghttp.RouterGroup) { group.POST("list/", goods.Goods.List) group.POST("detail/", goods.Goods.Detail) group.POST("category/", goods.Goods.Category) }) //以下是需要登入的 group.Middleware(middleware.MiddlewareGToken.GetToken) //文章 group.Group("article/", func(group *ghttp.RouterGroup) { group.POST("add/", article.Article.Add) group.POST("update/", article.Article.Update) group.POST("delete/", article.Article.Delete) group.POST("list/", article.Article.List) //全部文章列表 group.POST("my/list/", article.Article.MyList) //我的文章列表 group.POST("detail/", article.Article.Detail) //文章詳情 }) //點贊 group.Group("praise/", func(group *ghttp.RouterGroup) { group.POST("add/", praise.Praise.Add) group.POST("delete/", praise.Praise.Delete) group.POST("list/", praise.Praise.List) }) }) }

注意:我並沒有嚴格按照RESTful Api的規範設計介面,而是全部使用的POST請求。 這裡並沒有嚴格限制,使用什麼API介面規範和框架、開發語言都沒有關係,適合自己的就是最好的。

5. 上傳圖片到雲平臺

我們以上傳圖片到七牛雲舉例:

  1. 首先我們使用goframe提供的r.GetUploadFiles("file") 上傳檔案到本地(如果部署到伺服器,就是伺服器的本地)
  2. 按照雲平臺提示,配置相關的AKSK
  3. 將本地檔案地址上傳到雲平臺
  4. 刪除本地檔案

下面的關鍵程式碼已加註釋,AKSK等配置資訊在 /config/config.toml 檔案中配置

``` package upload

var Upload = uploadApi{}

type uploadApi struct{}

// Upload uploads files to /tmp . func (uploadApi) Img(r ghttp.Request) { files := r.GetUploadFiles("file") dirPath := "/tmp/" names, err := files.Save(dirPath, true) if err != nil { r.Response.WriteExit(err) }

for _, name := range names { localFile := dirPath + name bucket := g.Cfg().GetString("qiniu.bucket") key := name accessKey := g.Cfg().GetString("qiniu.accessKey") secretKey := g.Cfg().GetString("qiniu.secretKey")

  putPolicy := storage.PutPolicy{
     Scope: bucket,
  }
  mac := qbox.NewMac(accessKey, secretKey)
  upToken := putPolicy.UploadToken(mac)

  cfg := storage.Config{}
  // 空間對應的機房
  cfg.Zone = &storage.ZoneHuabei
  // 是否使用https域名
  cfg.UseHTTPS = true
  // 上傳是否使用CDN上傳加速
  cfg.UseCdnDomains = false

  // 構建表單上傳的物件
  formUploader := storage.NewFormUploader(&cfg)
  ret := storage.PutRet{}

  // 可選配置
  putExtra := storage.PutExtra{
     Params: map[string]string{},
  }

  err = formUploader.PutFile(r.GetCtx(), &ret, upToken, key, localFile, &putExtra)
  if err != nil {
     response.FailureWithData(r, 0, err, "")
  }

  fmt.Println(ret.Key, ret.Hash)

  //刪除本地檔案
  err = os.Remove(localFile)
  if err != nil {
     g.Dump("刪除本地檔案失敗:", err)
  }
  fmt.Println("刪除本地檔案成功", localFile)

  //返回資料
  response.SuccessWithData(r, g.Map{
     "url": g.Cfg().GetString("qiniu.url") + ret.Key,
  })

} } ```

6.如何科學的寫搜尋?

我來說明一個經典的搜尋場景:

我們有多個搜尋條件,這些搜尋條件非必傳,傳了哪些條件就命中哪些條件,如何實現比較科學呢?

我的建議是使用map支援set方法的特點,靈活的設定查詢條件,避免在宣告的時候賦值,那麼實現需要做複雜的判斷邏輯。

將查詢條件封裝為packListCondition方法,統一管理,方便多處複用。

```go func (s goodsService) List(ctx context.Context, req PageListReq) (res ListGoodsRes, err error) { //例項化map whereCondition := gmap.New() //很好的理解了map是引用型別的特點 在這個函式中為查詢條件賦值 packListCondition(req, whereCondition) //map是引用型別,在packListCondition函式中已經做了賦值操作,不需要在接收返回值 count, err := dao.GoodsInfo.Ctx(ctx).Where(whereCondition).Count() if err != nil { return } res.Count = count err = dao.GoodsInfo.Ctx(ctx).Where(whereCondition).OrderDesc("id").Page(req.Page, req.Limit).Scan(&res.List) if err != nil { return } return }

func packListCondition(req PageListReq, whereCondition gmap.Map) { //使用map支援set的特性 避免在宣告的時候賦值,那麼寫需要做的判斷太複雜了。 if req.Keyword != "" { whereCondition.Set(dao.GoodsInfo.Columns.DetailInfo+" like ", "%"+req.Keyword+"%") } if req.Name != "" { whereCondition.Set(dao.GoodsInfo.Columns.Name+" like ", "%"+req.Name+"%") } if req.Brand != "" { whereCondition.Set(dao.GoodsInfo.Columns.Brand+" like ", "%"+req.Brand+"%") } } ```

7.提供2套API介面

我們再來看一下目錄結構,有個整體的認識: image.png 1. app/system 目錄是我為了在一個專案中,同時開發前後臺系統,提高程式碼複用率而建立的。 2. 除了app/system 目錄,其他目錄都是通過goframe的 gf工具生成出來的。 3. 所以實現一個專案提供2套API介面的核心是:如何在一個專案中啟動兩個服務,同時提供前後臺專案的所需的介面API?

我是這麼做的: 在我們main.go的入口檔案中分別初始化前後臺專案的路由檔案,啟動服務。 ``` package main

import ( "github.com/gogf/gf/frame/g" "shop/app/middleware" "shop/app/system/backend" "shop/app/system/frontend" _ "shop/boot" _ "shop/router" )

func main() { s := g.Server() s.Use(middleware.Cors.CORS) //後臺專案 backend.Init(s) //前端專案 frontend.Init(s) s.Run() } ```

底層思路是:前後臺專案的路由基於路由組區分是哪個平臺的介面。介面的內部邏輯,如果可以複用就複用;如果不能複用,是獨立的功能就分別在system下的backend或者frontend下開發。

前臺介面路由檔案

func Init(s *ghttp.Server) { s.Group("/frontend/", func(group *ghttp.RouterGroup) { . . . } }

後臺介面路由檔案

func Init(s *ghttp.Server) { s.Group("/backend/", func(group *ghttp.RouterGroup) { . . . } }

小提示: 如果你的需求只是開發一個專案,那麼就可以把system目錄砍掉,直接在原生目錄下開發就可以了。

到這裡,我們就完成了專案的整體搭建和開發,感興趣的小夥伴可以star、fork我開源到GitHub的專案原始碼:GitHub基於GoFrame搭建的電商前後臺系統API

8.自動編譯

自動編譯是goframe整合好的功能,我們不需要去安裝air,只需要使用如下命令就可以實現自動編譯了:

shell gf run main.go

效果如下:

image.png

最後我們通過編寫shell指令碼,實現專案的一鍵部署到遠端伺服器:

9.一鍵部署指令碼

```go RED_COLOR='\E[1;31m' #紅 GREEN_COLOR='\E[1;32m' #綠 YELOW_COLOR='\E[1;33m' #黃 BLUE_COLOR='\E[1;34m' #藍 PINK='\E[1;35m' #粉紅 RES='\E[0m'

echo -e "${GREEN_COLOR}*基於GoFrame搭建的電商前後臺API系統:開始執行自動化部署*${RES}\n\n"

echo -e "${YELOW_COLOR}---step1:合併程式碼---${RES}" git pull origin master echo -e "${BLUE_COLOR}合併程式碼成功${RES}\n"

echo -e "${YELOW_COLOR}---step2:編譯---${RES}" go build echo -e "${BLUE_COLOR}編譯完成${RES}\n"

echo -e "${YELOW_COLOR}---step3:更改許可權---${RES}" chmod -R 777 shop echo -e "${BLUE_COLOR}更改許可權完成${RES}\n"

echo -e "${YELOW_COLOR}---step4:殺掉程序並且執行---${RES}" i1=$(ps -ef | grep -E "shop" | grep -v grep | awk '{print $2}') echo -e "${BLUE_COLOR}殺掉程序$i1${RES}\n" kill -9 $i1 && nohup ./shop >/dev/null 2>&1 & i2=$(ps -ef | grep -E "shop" | grep -v grep | awk '{print $2}') echo -e "${GREEN_COLOR}*部署成功,部署的程序ID為:$i2${RES}*" ```

學習資料

下面是建議大家動手實踐的學習資料:

GitHub:基於GoFrame搭建的電商前後臺系統API

GoFrame學習專欄

GoFrame官方文件

Github:GoFrame入門官方示例專案

總結

通過這篇文章我們基於GoFrame框架搭建了一個電商系統的前後臺API,實踐瞭如何整合gtoken實現登入,如何自定義中介軟體和服務,如何定義路由組,如何上傳檔案到雲平臺,以及在開發的過程中如何實現自動編譯,當專案開發完畢,如何一鍵部署到遠端伺服器。

歡迎大家動手實踐,歡迎在評論區探討。

關於專欄

近期會更新一系列Go進階實戰的文章,歡迎大家關注我的簽約專欄# Go語言進階實戰

這是近期準備更新文章的知識脈絡圖,感興趣的小夥伴可以關注一波,歡迎日常催更。

image.png

已完成

《一文玩轉ProtoBuf》

《開發gRPC總共分三步》

《Go WEB進階實戰:基於GoFrame搭建的電商前後臺API系統》