美文网首页GolangGo 语言学习专栏 我爱编程
『No18: Go 实现世界杯后台管理系统』

『No18: Go 实现世界杯后台管理系统』

作者: 谢小路 | 来源:发表于2018-07-23 09:02 被阅读169次
    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 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(&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 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()
            }
        }
    }
    

    什么意思呢?

    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


    全文完,我是谢伟,再会,谢谢。

    相关文章

      网友评论

        本文标题:『No18: Go 实现世界杯后台管理系统』

        本文链接:https://www.haomeiwen.com/subject/gewlmftx.html