Go 實現世界盃後臺管理系統

wuxiaoshen發表於2018-07-24
182.png

大家好,我叫謝偉,是一名程式設計師。

趁著週末更新一期,上一期講到 如何快速熟悉一個專案, 文章的最後講到,最好的方法是借用相同的技術棧重新實現一個專案。

本文就是借用相同技術棧實現了 2018世界盃後臺管理系統 。

主要使用到的技術是:

  • gin 快速搭建 web server
  • gin-swagger 自動化構建API 文件
  • gorm 運算元據庫
  • fresh 實現 web server 監聽
  • viper 實現讀取使用者配置
  • 資料庫 使用 postgre
  • goquery 實現網頁解析

主要的思路是:

第一步:

既然是 2018 屆世界盃後臺管理系統,那麼肯定需要本屆世界盃的資料,那麼資料從哪裡來?

目標網站 2018屆俄羅斯世界盃

既然已經知道目標網站,那麼下一步的動作是什麼?

網頁爬蟲。

  • matches
  • teams
  • groups
  • players
  • statistics
  • awards
  • classic

主要需要的資訊是這些。

第二步:

分析網頁原始碼。網頁爬蟲。在 go 中用來網頁解析的一個比較好庫的是 goquery

對需要的目標資料一個個分析。

第三步:

資料存到哪?

你當然肯定按照你的意願來,存文字,或者存資料庫。一般企業級的應用,會存本地嗎?

那麼我還是老老實實存資料庫。資料庫的選擇,按自己來,我這邊選擇 postgre.

既然使用到資料庫,必然需要運算元據庫,如果你希望程式碼中充斥著SQL 語句,那麼你可以選擇寫SQL 語句,當然我覺得更好的維護方式是使用 ORM, go 內使用orm 技術,一個比較好的庫是 gorm .

使用 gorm 你可以很方便的實現 資料庫的增刪改查。

第四步:

既然資料有了,那麼如何實現後臺管理系統?

應該是要使用 restful API 實現 資源的增刪改查。

推薦使用 gin 。 當然你喜歡其他框架也是OK的,甚至你喜歡原生的,那也是OK的。

只不過,我覺得 gin 的速度快,輕量,學習成本低。你可以很容易的實現 web server.

使用中介軟體可以實現對 gin 的擴充套件。

第五步:

假如資料不想讓任何人都可以隨意訪問到,那麼如何限制呢?對應前端的效果就是,需要登入才能實現訪問資源,那麼後端是如何實現的?

jwt: json web token 使用 json 來傳遞資料,用於判定使用者是否登陸狀態。

具體的做法:

  • 登陸,取到 jwt
  • 訪問時,請求時 Header 中需掛載 jwt

下文只講述核心程式碼:

1. 專案結構

├── configs├── docs│ 
 
└── swagger├── domain├── infra│ 
 
├── adapter│ 
 
├── config│ 
 
├── crypt│ 
 
├── download│ 
 
├── init│ 
 
└── model├── ui│ 
 
└── api-server│ 
 
├── admins│ 
 
├── awards│ 
 
├── classic│ 
 
├── coaches│ 
 
├── controller│ 
 
├── groups│ 
 
├── matches│ 
 
├── players│ 
 
├── statistics│ 
 
└── teams└── vendor複製程式碼
  • configs 配置資訊,主要是資料的配置資訊,主機地址,埠,使用者名稱和密碼等
  • docs API 文件,gin-swagger 自動構建的,不是手動建立的
  • domain 領域層,主要是網頁資訊的分析和爬取和入庫
  • infra 基礎設施層,主要是字串處理、加密演算法、獲取網頁原始碼、資料庫模型定義
  • UI 使用者視覺化層, 主要是 gin構建的API 的操作,包括路由、響應和swagger 文件註釋
  • vendor 第三方庫

2. 獲取網頁原始碼

使用內建的net/http 即可實現

func Downloader(url string) (*goquery.Document, error) { 
request, err := http.NewRequest("GET", url, nil) if err != nil {
return nil, ErrDownloader
} request.Header.Add("User-Agent", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36") client := http.DefaultClient response, err := client.Do(request) if err != nil {
return nil, ErrDownloader
} defer response.Body.Close() return goquery.NewDocumentFromReader(response.Body)
}複製程式碼

假如你遇到動態載入資料,不想費勁分析網頁,對速度要求也不高,你可以使用 selenium

func DownloaderBySelenium(url string) (string, error) { 
caps := selenium.Capabilities{
"browserName": "chrome",
} imageCaps := map[string]interface{
}{
"profile.managed_default_content_settings.images": 2,
} chromeCaps := chrome.Capabilities{
Prefs: imageCaps, Path: "", Args: []string{
"--headless", "--no-sandbox", "--user-agent=Mozilla/5.0 (Macintosh;
Intel Mac OS X 10_13_2) AppleWebKit/604.4.7 (KHTML, like Gecko) Version/11.0.2 Safari/604.4.7"
,
},
} caps.AddChrome(chromeCaps) service, err := selenium.NewChromeDriverService( config.ChromeDriverPath, 9515, ) defer service.Stop() if err != nil {
fmt.Println(ErrSeleniumService) return "", ErrSeleniumService
} webDriver, err := selenium.NewRemote(caps, fmt.Sprintf("http://localhost:%d/wd/hub", 9515)) if err != nil {
fmt.Println(ErrWebDriver) return "", ErrWebDriver
} err = webDriver.Get(url) if err != nil {
fmt.Println(ErrWebDriverGet) return "", ErrWebDriverGet
} return webDriver.PageSource()
}複製程式碼

3. 資料庫表定義和響應資訊定義

資料庫表定義操控 gorm model 的定義,型別,非空,預設值等使用 tag 實現

// awards 表定義type Award struct { 
ID uint `gorm:"primary_key;
column:id"
` AwardName string `gorm:"type:varchar(64);
not null;
column:award_name"
` URL string `gorm:"type:varchar(128);
not null;
column:url"
` Info string `gorm:"type:varchar(128);
not null;
column:info"
`
}// API 響應資訊定義type AwardSerializer struct {
ID uint `json:"id"` AwardName string `json:"award_name"` Info string `json:"info"` URL string `json:"url"`
}func (a *Award) Serializer() AwardSerializer {
return AwardSerializer{
ID: a.ID, AwardName: a.AwardName, Info: a.Info, URL: a.URL,
}
}複製程式碼

4. 資訊爬取入庫

func Awards(doc *goquery.Document) error { 
var err error count := 0 urlList := make([]string, 0, 0) urlList = append(urlList, "/worldcup/awards/golden-boot/") urlList = append(urlList, "/worldcup/awards/golden-glove/") urlList = append(urlList, "/worldcup/awards/golden-ball/") for _, url := range urlList {
completeAwardURl := config.RootURL + url doc, err := download.Downloader(completeAwardURl) if err != nil {
err = ErrorAwardDownloader break
} // db save awards := callBack(completeAwardURl, doc) fmt.Println(completeAwardURl) for _, award := range awards {
fmt.Println(award) count++ // push data into db initiator.POSTGRES.Save(&
award)
}
} fmt.Println(count) return err
}func callBack(url string, doc *goquery.Document) []model.Award {
allAwardInfo := make([]model.Award, 0, 0) awardName := doc.Find("h1").Eq(2).Text() doc.Find("div p").Each(func(i int, selection *goquery.Selection) {
if i >
6 {
awardInfo := selection.Text() if strings.HasPrefix(awardInfo, "*") {
return
} oneAward := model.Award{
} oneAward.URL = url oneAward.AwardName = awardName oneAward.Info = awardInfo allAwardInfo = append(allAwardInfo, oneAward)
}
}) return allAwardInfo
}複製程式碼

5. 構建 restful API

func awardsRegistry(r *gin.RouterGroup) { 
r.GET("/awards", awards.ShowAllAwardHandler) r.GET("/awards/:awardID", awards.ShowAwardHandler)
}複製程式碼
package awardsimport ( "FIFA-World-Cup/infra/init" "FIFA-World-Cup/infra/model" "fmt" "github.com/gin-gonic/gin" "github.com/pkg/errors" "net/http")var ( ErrorAwardParam = errors.New("award param is not correct"))// ShowAwardHandler will list Awards// @Summary List Awards// @Accept json// @Tags Awards// @Security Bearer// @Produce  json// @Param awardID path string true "award id"// @Resource Awards// @Router /awards/{id
} [get]// @Success 200 {object
} model.AwardSerializerfunc ShowAwardHandler(c *gin.Context) {
id := c.Param("awardID") var award model.Award if dbError := initiator.POSTGRES.Where("info LIKE ?", fmt.Sprintf("%%%s%%", id)).First(&
award).Error;
dbError != nil {
c.AbortWithError(400, dbError) return
} c.JSON(http.StatusOK, award.Serializer())
}type ListAwardParam struct {
Search string `form:"search"` Return string `form:"return"`
}// ShowAllAwardHandler will list Awards// @Summary List Awards// @Accept json// @Tags Awards// @Security Bearer// @Produce json// @Param search path string false "award_name"// @param return path string false "return = all_list"// @Resource Awards// @Router /awards [get]// @Success 200 {array
} model.AwardSerializerfunc ShowAllAwardHandler(c *gin.Context) {
var param ListAwardParam if err := c.ShouldBindQuery(&
param);
err != nil {
c.AbortWithError(400, ErrorAwardParam) return
} var awards []model.Award if param.Search != "" {
if dbError := initiator.POSTGRES.Where("award_name LIKE ?", fmt.Sprintf("%%%s%%", param.Search)).Find(&
awards).Error;
dbError != nil {
c.AbortWithError(400, dbError) return
}
} if param.Return == "all_list" {
if dbError := initiator.POSTGRES.Find(&
awards).Error;
dbError != nil {
c.AbortWithError(400, dbError) return
}
} var result = make([]model.AwardSerializer, len(awards)) for index, award := range awards {
result[index] = award.Serializer()
} c.JSON(http.StatusOK, result)
}複製程式碼

具體響應函式上方的註釋是構建自動化文件需要的。

6. jwt 認證

package controllerimport ( "FIFA-World-Cup/infra/init" "FIFA-World-Cup/infra/model" "errors" "fmt" "github.com/gin-gonic/gin" "strings")var ( ErrorAuth      = errors.New("please add token: 'Authorization: Bearer xxxx'") ErrorAuthWrong = errors.New("token is not right,example: Bearer xxxx"))func AuthRequired() gin.HandlerFunc { 
return func(c *gin.Context) {
if vendor := c.Request.Header.Get("X-Requested-With");
vendor != "" {
c.Set("X-Requested-With", vendor)
} header := c.Request.Header.Get("Authorization") if header == "" {
c.AbortWithError(400, ErrorAuth) return
} authHeader := strings.Split(header, " ") if len(authHeader) != 2 {
c.AbortWithError(400, ErrorAuthWrong) return
} token := authHeader[1] var admin model.Admin fmt.Println(token) if dbError := initiator.POSTGRES.Where("auth_token = ?", token).First(&
admin).Error;
dbError != nil {
c.AbortWithError(400, dbError)
} else {
c.Set("current_admin", admin) c.Next()
}
}
}複製程式碼

什麼意思呢?

  1. 使用者需註冊或者登陸,後臺生成對應的 auth_token

select * from admins;

 id |          created_at           |          updated_at           | deleted_at |      name      |                auth_token                |                     encrypted_password'                      |    phone     | state----+-------------------------------+-------------------------------+------------+----------------+------------------------------------------+--------------------------------------------------------------+--------------+-------2 2018-07-20 16:10:11.099085 2018-07-20 16:10:11.099085  FIFA-World-Cup c6d81d35bc598ddedf3e0b798cd5d463139ab6c9 $2a$04$wKHmdGixgrISJM7wV3rKn.6HX5Bjg8.JbelGYl/443ber3aXI/K8K 110120119 admin複製程式碼

每個使用者會生成對應的 auth_token

訪問資源 HEADER 需要帶上這個 token. 達到認證的目的。

7. 效果

Swagger-API 文件

Swagger-API.png

API 列表

PostMan-API.png

視訊版講解

BiliBili

8. 原始碼

FIFA-World-Cup-2018


全文完,我是謝偉,再會,謝謝。

來源:https://juejin.im/post/5b572c89f265da0f894b7153

相關文章