你們好,我叫謝偉,是一名程序員。前端
趁着週末更新一期,上一期講到 如何快速熟悉一個項目, 文章的最後講到,最好的方法是借用相同的技術棧從新實現一個項目。git
本文就是借用相同技術棧實現了 2018世界盃後臺管理系統 。程序員
主要使用到的技術是:github
主要的思路是:web
既然是 2018 屆世界盃後臺管理系統,那麼確定須要本屆世界盃的數據,那麼數據從哪裏來?算法
目標網站 2018屆俄羅斯世界盃chrome
既然已經知道目標網站,那麼下一步的動做是什麼?數據庫
網頁爬蟲。json
主要須要的信息是這些。網頁爬蟲
分析網頁源代碼。網頁爬蟲。在 go 中用來網頁解析的一個比較好庫的是 goquery
對須要的目標數據一個個分析。
數據存到哪?
你固然確定按照你的意願來,存文本,或者存數據庫。通常企業級的應用,會存本地嗎?
那麼我仍是老老實實存數據庫。數據庫的選擇,按本身來,我這邊選擇 postgre.
既然使用到數據庫,必然須要操做數據庫,若是你但願代碼中充斥着SQL 語句,那麼你能夠選擇寫SQL 語句,固然我以爲更好的維護方式是使用 ORM, go 內使用orm 技術,一個比較好的庫是 gorm .
使用 gorm 你能夠很方便的實現 數據庫的增刪改查。
既然數據有了,那麼如何實現後臺管理系統?
應該是要使用 restful API 實現 資源的增刪改查。
推薦使用 gin 。 固然你喜歡其餘框架也是OK的,甚至你喜歡原生的,那也是OK的。
只不過,我以爲 gin 的速度快,輕量,學習成本低。你能夠很容易的實現 web server.
使用中間件能夠實現對 gin 的擴展。
假如數據不想讓任何人均可以隨意訪問到,那麼如何限制呢?對應前端的效果就是,須要登入才能實現訪問資源,那麼後端是如何實現的?
jwt: json web token 使用 json 來傳遞數據,用於斷定用戶是否登錄狀態。
具體的作法:
下文只講述核心代碼:
├── 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
複製代碼
使用內置的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()
}
複製代碼
數據庫表定義操控 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,
}
}
複製代碼
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
}
複製代碼
func awardsRegistry(r *gin.RouterGroup) {
r.GET("/awards", awards.ShowAllAwardHandler)
r.GET("/awards/:awardID", awards.ShowAwardHandler)
}
複製代碼
package awards
import (
"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.AwardSerializer
func 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.AwardSerializer
func ShowAllAwardHandler(c *gin.Context) {
var param ListAwardParam
if err := c.ShouldBindQuery(¶m); 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)
}
複製代碼
具體響應函數上方的註釋是構建自動化文檔須要的。
package controller
import (
"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()
}
}
}
複製代碼
什麼意思呢?
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. 達到認證的目的。
Swagger-API 文檔
API 列表
視頻版講解
全文完,我是謝偉,再會,謝謝。