首页 > IT职业 > IT图书 > 『No18: Go 实现世界杯后台管理系统』
大家好,我叫谢伟,是一名程序员。
最近没时间更新文章,抱歉。
趁着周末更新一期,上一期讲到 如何快速熟悉一个项目, 文章的最后讲到,最好的方法是借用相同的技术栈重新实现一个项目。
本文就是借用相同技术栈实现了 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
下文只讲述核心代码:
├── 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 第三方库
使用内置的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.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(¶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 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()
}
}
}
什么意思呢?
用户需注册或者登陆,后台生成对应的 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. 达到认证的目的。
Swagger-API 文档

Swagger-API.png
API 列表

PostMan-API.png
视频版讲解
作者:谢小路
链接:https://www.jianshu.com/p/c4d9e02adb12
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/3071/viewspace-2805161/ ,如需转载,请注明出处,否则将追究法律责任。