大家好,我叫谢伟,是一名程序员。
最近没时间更新文章,抱歉。
趁着周末更新一期,上一期讲到 如何快速熟悉一个项目, 文章的最后讲到,最好的方法是借用相同的技术栈重新实现一个项目。
本文就是借用相同技术栈实现了 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.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) }
具体响应函数上方的注释是构建自动化文档需要的。
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() } } }
什么意思呢?
用户需注册或者登陆,后台生成对应的 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
视频版讲解
8. 源代码
作者:谢小路
链接:https://www.jianshu.com/p/c4d9e02adb12