Go WEB進階實戰:基於GoFrame搭建的電商前後臺API系統
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. 介面測試
官方的示例非常規範,但是過於簡單。基礎薄弱的小夥伴可以先實踐官方示例,再實踐我的電商專案。
進階教程:電商前後臺系統
作為入門級電商系統包括了常規的功能點,下面我重點說一下能學到技術上的知識點:
- 如何使用gtoken實現單點登入?
- 如何自定義中介軟體?
- 如何自定義服務?
- 如何定義路由組,明確介面邊界?
- 如何上傳圖片到雲平臺?
- 如何靈活的設定搜尋條件?
- 如何用一個專案,提供前後臺的2套API介面?
- 如何實現自動編譯?
- 如何使用shell指令碼一鍵部署專案到遠端伺服器?
說明:GoFrame的官方文件和示例能帶你快速入門GoFrame框架和CLI工具的使用,不作為這篇文章的重點。
這篇文章的重點是:能帶你更進一步,基於良好的規範,開發比較複雜的商業專案。 下面就和我一起學習吧,文章最後我會分享給大家這個專案的github地址以及對大家學習有幫助的文件資料。
先看目錄
整體結構
重點看app目錄
app目錄是我們要重點開發的部分
開始實戰
提示:為了行文緊湊,方便大家理解。與核心知識點無關的程式碼會直接省略或用會三個豎著的.簡化。文章最後會提供GitHub地址,開源專案。
1. GToken實現單點登入
1. 檢視自己的版本
首先,我們要確定自己安裝的gf版本,通過gf version命令就可以查看了。
注意: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 檔案,用來定義客戶端的路由:
- 首先編寫gtoken登入註冊等方法
- 然後在路由檔案中使用 group.Middleware() 把自定義的中介軟體註冊到路由組中。
- 注意:不需要校驗登入狀態的介面寫在 group.Middleware(middleware.MiddlewareGToken.GetToken) 之前,需要校驗登入狀態的寫在之後。
```go package frontend
//前端專案登入 func Login() { // 啟動gtoken middleware.GToken = >oken.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. 上傳圖片到雲平臺
我們以上傳圖片到七牛雲舉例:
- 首先我們使用goframe提供的r.GetUploadFiles("file") 上傳檔案到本地(如果部署到伺服器,就是伺服器的本地)
- 按照雲平臺提示,配置相關的AKSK
- 將本地檔案地址上傳到雲平臺
- 刪除本地檔案
下面的關鍵程式碼已加註釋,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介面
我們再來看一下目錄結構,有個整體的認識:
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
效果如下:
最後我們通過編寫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}*" ```
學習資料
下面是建議大家動手實踐的學習資料:
總結
通過這篇文章我們基於GoFrame框架搭建了一個電商系統的前後臺API,實踐瞭如何整合gtoken實現登入,如何自定義中介軟體和服務,如何定義路由組,如何上傳檔案到雲平臺,以及在開發的過程中如何實現自動編譯,當專案開發完畢,如何一鍵部署到遠端伺服器。
歡迎大家動手實踐,歡迎在評論區探討。
關於專欄
近期會更新一系列Go進階實戰的文章,歡迎大家關注我的簽約專欄 :# Go語言進階實戰。
這是近期準備更新文章的知識脈絡圖,感興趣的小夥伴可以關注一波,歡迎日常催更。
已完成
- Go非同步任務處理解決方案:Asynq
- 一天約了4個面試,覆盤一下面試題和薪資福利
- 世界上最健康的程式設計師作息表!「值得一看」
- 8千字詳解Go1.20穩定版
- 不愧是微軟出品的工具,逆天!
- 【視訊 原始碼】登入鑑權的三種方式:token、jwt、session實戰分享
- 程式設計師副業接單做私活避坑指南
- Git操作不規範,戰友提刀來相見!
- 【簡歷優化】如何寫好專案的亮點難點?專案經歷怎麼寫最好?
- 技術男的春天:小姐姐求助&暖男分析
- 【簡歷優化】如何在簡歷中最大化體現出自己的學習能力?
- 如何快速學一門新語言?關鍵問題是什麼?
- Go WEB進階實戰:GoFrame結合電商專案深入理解Go知識點
- Go容易搞錯的知識點彙總
- 開發gRPC總共分三步
- 【答讀者問】把Go基礎學完後,是學web方向還是區塊鏈方向?
- Go WEB進階實戰:基於GoFrame搭建的電商前後臺API系統
- 給想轉Go或者Go進階同學的一些建議
- 聽了大佬們的直播,我決定卷掘金小冊了。| Flag永不倒
- 爆肝兩千字整理《Go學習路線圖》| 文末投稿送投影