涉及知识点

本文目标

  • 完成博客的标签类接口定义和编写

定义接口

本节正是编写标签的逻辑,我们想一想,一般接口为增删改查是基础的,那么我们定义一下接口吧!

  • 获取标签列表:GET(“/tags”)
  • 新建标签:POST(“/tags”)
  • 更新指定标签:PUT(“/tags/:id”)
  • 删除指定标签:DELETE(“/tags/:id”)

编写路由空壳

开始编写路由文件逻辑,在routers下新建api目录,我们当前是第一个 API 大版本,因此在api下新建v1目录,再新建tag.go文件,写入内容:

  1. package v1
  2. import (
  3. "github.com/gin-gonic/gin"
  4. )
  5. //获取多个文章标签
  6. func GetTags(c *gin.Context) {
  7. }
  8. // @Summary 新增文章标签
  9. // @Produce json
  10. // @Param name query string true "Name"
  11. // @Param state query int false "State"
  12. // @Param created_by query int false "CreatedBy"
  13. // @Success 200 {string} json "{"code":200,"data":{},"msg":"ok"}"
  14. // @Router /api/v1/tags [post]
  15. func AddTag(c *gin.Context) {
  16. }
  17. // @Summary 修改文章标签
  18. // @Produce json
  19. // @Param id path int true "ID"
  20. // @Param name query string true "ID"
  21. // @Param state query int false "State"
  22. // @Param modified_by query string true "ModifiedBy"
  23. // @Success 200 {string} json "{"code":200,"data":{},"msg":"ok"}"
  24. // @Router /api/v1/tags/{id} [put]
  25. func EditTag(c *gin.Context) {
  26. }
  27. //删除文章标签
  28. func DeleteTag(c *gin.Context) {
  29. }

注册路由

我们打开routers下的router.go文件,修改文件内容为:

  1. package routers
  2. import (
  3. "github.com/gin-gonic/gin"
  4. "github.com/noobwu/go-gin-demo/routers/api/v1"
  5. _ "github.com/noobwu/go-gin-demo/docs"
  6. "github.com/swaggo/gin-swagger"
  7. "github.com/swaggo/gin-swagger/swaggerFiles"
  8. )
  9. func InitRouter() *gin.Engine {
  10. r := gin.New()
  11. r.Use(gin.Logger())
  12. r.Use(gin.Recovery())
  13. r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
  14. apiv1 := r.Group("/api/v1")
  15. {
  16. //获取标签列表
  17. apiv1.GET("/tags", v1.GetTags)
  18. //新建标签
  19. apiv1.POST("/tags", v1.AddTag)
  20. //更新指定标签
  21. apiv1.PUT("/tags/:id", v1.EditTag)
  22. //删除指定标签
  23. apiv1.DELETE("/tags/:id", v1.DeleteTag)
  24. }
  25. return r
  26. }

当前目录结构:

  1. PS F:\Projects\NoobWu\go-samples\go-gin-demo> tree
  2. 文件夹 PATH 列表
  3. 卷序列号为 0001-DEFC
  4. F:.
  5. ├─.idea
  6. ├─conf
  7. ├─docs
  8. ├─middleware
  9. ├─models
  10. ├─pkg
  11. └─setting
  12. ├─routers
  13. └─api
  14. └─v1
  15. └─runtime

检验路由是否注册成功

回到命令行,执行 go run main.go,检查路由规则是否注册成功。

  1. PS F:\Projects\NoobWu\go-samples\go-gin-demo> go run main.go

image.png
运行成功,那么我们愉快的开始编写我们的接口吧!

image.png

下载依赖包


首先我们要拉取 validation 的依赖包,在后面的接口里会使用到表单验证

  1. $ go get -u github.com/astaxie/beego/validation
  1. PS D:\Projects\Github\NoobWu\go-samples\go-gin-demo> go get -u github.com/astaxie/beego/validation

image.png

编写标签列表的 models 逻辑

**D:\Projects\Github\NoobWu\go-samples\go-gin-demo\models\tag.go**
创建 models 目录下的 tag.go,写入文件内容:

  1. package models
  2. import (
  3. "github.com/jinzhu/gorm"
  4. )
  5. type Tag struct {
  6. Model
  7. Name string `json:"name"`
  8. CreatedBy string `json:"created_by"`
  9. ModifiedBy string `json:"modified_by"`
  10. State int `json:"state"`
  11. }
  12. // GetTags gets a list of tags based on paging and constraints
  13. func GetTags(pageNum int, pageSize int, maps interface{}) ([]Tag, error) {
  14. var (
  15. tags []Tag
  16. err error
  17. )
  18. if pageSize > 0 && pageNum > 0 {
  19. err = db.Where(maps).Find(&tags).Offset(pageNum).Limit(pageSize).Error
  20. } else {
  21. err = db.Where(maps).Find(&tags).Error
  22. }
  23. if err != nil && err != gorm.ErrRecordNotFound {
  24. return nil, err
  25. }
  26. return tags, nil
  27. }
  28. // GetTagTotal counts the total number of tags based on the constraint
  29. func GetTagTotal(maps interface{})(int,error) {
  30. var count int
  31. if err:=db.Model(&Tag{}).Where(maps).Count(&count).Error;err != nil {
  32. return 0,err
  33. }
  34. return count,nil
  35. }
  1. 我们创建了一个 Tag struct{},用于 Gorm 的使用。并给予了附属属性 json ,这样子在 c.JSON 的时候就会自动转换格式,非常的便利
  2. 可能会有的初学者看到 return,而后面没有跟着变量,会不理解;其实你可以看到在函数末端,我们已经显示声明了返回值,这个变量在函数体内也可以直接使用,因为他在一开始就被声明了
  3. 有人会疑惑 db 是哪里来的;因为在同个 models 包下,因此 db *gorm.DB 是可以直接使用的

编写标签列表的路由逻辑

打开 routers 目录下 v1 版本的 tag.go,第一我们先编写获取标签列表的接口
修改文件内容:
D:\Projects\Github\NoobWu\go-samples\go-gin-demo\routers\api\v1\tag.go

  1. package v1
  2. import (
  3. "github.com/gin-gonic/gin"
  4. "github.com/noobwu/go-gin-demo/pkg/util"
  5. "github.com/unknwon/com"
  6. "net/http"
  7. "github.com/noobwu/go-gin-demo/pkg/app"
  8. "github.com/noobwu/go-gin-demo/pkg/e"
  9. "github.com/noobwu/go-gin-demo/pkg/setting"
  10. "github.com/noobwu/go-gin-demo/service/tag_service"
  11. )
  12. // @Summary Get multiple article tags
  13. // @Produce json
  14. // @Param name query string false "Name"
  15. // @Param state query int false "State"
  16. // @Success 200 {object} app.Response
  17. // @Failure 500 {object} app.Response
  18. // @Router /api/v1/tags [get]
  19. func GetTags(c *gin.Context) {
  20. appG := app.Gin{C: c}
  21. name := c.Query("name")
  22. state := -1
  23. if arg := c.Query("state"); arg != "" {
  24. state = com.StrTo(arg).MustInt()
  25. }
  26. tagService := tag_service.Tag{
  27. Name: name,
  28. State: state,
  29. PageNum: util.GetPage(c),
  30. PageSize: setting.AppSetting.PageSize,
  31. }
  32. tags, err := tagService.GetAll()
  33. if err != nil {
  34. appG.Response(http.StatusInternalServerError, e.ERROR_GET_TAGS_FAIL, nil)
  35. return
  36. }
  37. count, err := tagService.Count()
  38. if err != nil {
  39. appG.Response(http.StatusInternalServerError, e.ERROR_COUNT_TAG_FAIL, nil)
  40. return
  41. }
  42. appG.Response(http.StatusOK, e.SUCCESS, map[string]interface{}{
  43. "lists": tags,
  44. "total": count,
  45. })
  46. }
  47. // @Summary 新增文章标签
  48. // @Produce json
  49. // @Param name query string true "Name"
  50. // @Param state query int false "State"
  51. // @Param created_by query int false "CreatedBy"
  52. // @Success 200 {string} json "{"code":200,"data":{},"msg":"ok"}"
  53. // @Router /api/v1/tags [post]
  54. func AddTag(c *gin.Context) {
  55. }
  56. // @Summary 修改文章标签
  57. // @Produce json
  58. // @Param id path int true "ID"
  59. // @Param name query string true "ID"
  60. // @Param state query int false "State"
  61. // @Param modified_by query string true "ModifiedBy"
  62. // @Success 200 {string} json "{"code":200,"data":{},"msg":"ok"}"
  63. // @Router /api/v1/tags/{id} [put]
  64. func EditTag(c *gin.Context) {
  65. }
  66. //删除文章标签
  67. func DeleteTag(c *gin.Context) {
  68. }
  1. c.Query 可用于获取 ?name=test&state=1 这类 URL 参数,而 c.DefaultQuery 则支持设置一个默认值
  2. code 变量使用了 e 模块的错误编码,这正是先前规划好的错误码,方便排错和识别记录
  3. util.GetPage 保证了各接口的 page 处理是一致的
  4. c *gin.ContextGin 很重要的组成部分,可以理解为上下文,它允许我们在中间件之间传递变量、管理流、验证请求的 JSON 和呈现 JSON 响应

在本机执行 curl 127.0.0.1:7000/api/v1/tags ,正确的返回值为{"code":200,"data":{"lists":[],"total":0},"msg":"ok"},若存在问题请结合 gin 结果进行拍错。

  1. curl 127.0.0.1:7000/api/v1/tags

image.png

  1. Invoke-WebRequest -Uri "127.0.0.1:7000/api/v1/tags" -UseBasicParsing

image.png
在获取标签列表接口中,我们可以根据 namestatepage 来筛选查询条件,分页的步长可通过 app.ini 进行配置,以liststotal 的组合返回达到分页效果。

编写新增标签的 models 逻辑

接下来我们编写新增标签的接口
打开 models 目录下的 **tag.go**,修改文件(增加 2 个方法):
D:\Projects\Github\NoobWu\go-samples\go-gin-demo\models\tags.go

  1. // ExistTagByName checks if there is a tag with the same name
  2. func ExistTagByName(name string) (bool, error) {
  3. var tag Tag
  4. err := db.Select("id").Where("name = ? AND deleted_on = ? ", name, 0).First(&tag).Error
  5. if err != nil && err != gorm.ErrRecordNotFound {
  6. return false, err
  7. }
  8. if tag.ID > 0 {
  9. return true, nil
  10. }
  11. return false, nil
  12. }
  13. // AddTag Add a Tag
  14. func AddTag(name string, state int, createBy string) error {
  15. tag := Tag{
  16. Name: name,
  17. State: state,
  18. CreatedBy: createBy,
  19. }
  20. if err := db.Create(&tag).Error; err != nil {
  21. return err
  22. }
  23. return nil
  24. }

编写新增标签的路由逻辑

打开 **routers** 目录下的**tag.go**,修改文件(变动 AddTag 方法):
D:\Projects\Github\NoobWu\go-samples\go-gin-demo\routers\api\v1\tag.go

  1. type AddTagForm struct {
  2. Name string `form:"name" valid:"Required;MaxSize(100)"`
  3. CreatedBy string `form:"created_by" valid:"Required;MaxSize(100)"`
  4. State int `form:"state" valid:"Range(0,1)"`
  5. }
  6. // @Summary 新增文章标签
  7. // @Produce json
  8. // @Param name query string true "Name"
  9. // @Param state query int false "State"
  10. // @Param created_by query int false "CreatedBy"
  11. // @Success 200 {string} json "{"code":200,"data":{},"msg":"ok"}"
  12. // @Router /api/v1/tags [post]
  13. func AddTag(c *gin.Context) {
  14. var (
  15. appG = app.Gin{C: c}
  16. form AddTagForm
  17. )
  18. httpCode, errCode := app.BindAndValid(c, &form)
  19. if errCode != e.SUCCESS {
  20. appG.Response(httpCode, errCode, nil)
  21. return
  22. }
  23. tagService:=tag_service.Tag{
  24. Name: form.Name,
  25. CreatedBy: form.CreatedBy,
  26. State: form.State,
  27. }
  28. exists,err:=tagService.ExistByName()
  29. if err!=nil{
  30. appG.Response(http.StatusInternalServerError,e.ERROR_EXIST_TAG_FAIL,nil)
  31. return
  32. }
  33. if exists{
  34. appG.Response(http.StatusOK,e.ERROR_EXIST_TAG,nil)
  35. return
  36. }
  37. err =tagService.Add()
  38. if err!=nil{
  39. appG.Response(http.StatusInternalServerError,e.ERROR_ADD_TAG_FAIL,nil)
  40. return
  41. }
  42. appG.Response(http.StatusOK,e.SUCCESS,nil)
  43. }

PostmanPOST 访问 http://127.0.0.1:7000/api/v1/tags?name=tag_test&state=1&created_by=admin,查看 code 是否返回**200****blog_tag** 表中是否有值,有值则正确。

  1. curl --location --request POST 'http://127.0.0.1:7000/api/v1/tags' \
  2. --header 'Content-Type: application/x-www-form-urlencoded' \
  3. --data-urlencode 'name=tag_test' \
  4. --data-urlencode 'created_by=admin' \
  5. --data-urlencode 'state=s'

image.png

  1. $headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
  2. $headers.Add("Content-Type", "application/x-www-form-urlencoded")
  3. $body = "name=tag_test&created_by=admin&state=1"
  4. $response = Invoke-RestMethod 'http://127.0.0.1:7000/api/v1/tags' -Method 'POST' -Headers $headers -Body $body
  5. $response | ConvertTo-Json

image.png

编写 models callbacks

但是这个时候大家会发现,我明明新增了标签,但 created_on 居然没有值,那做修改标签的时候modified_on 会不会也存在这个问题?

为了解决这个问题,我们需要打开 models 目录下的 tag.go 文件,修改文件内容(修改包引用和增加 2 个方法):
D:\Projects\Github\NoobWu\go-samples\go-gin-demo\models\models.go

  1. // updateTimeStampForCreateCallback will set `CreatedOn`, `ModifiedOn` when creating
  2. func updateTimeStampForCreateCallback(scope *gorm.Scope) {
  3. if !scope.HasError() {
  4. nowTime := time.Now().Unix()
  5. if createTimeField, ok := scope.FieldByName("CreatedOn"); ok {
  6. if createTimeField.IsBlank {
  7. createTimeField.Set(nowTime)
  8. }
  9. }
  10. if modifyTimeField, ok := scope.FieldByName("ModifiedOn"); ok {
  11. if modifyTimeField.IsBlank {
  12. modifyTimeField.Set(nowTime)
  13. }
  14. }
  15. }
  16. }
  17. // updateTimeStampForUpdateCallback will set `ModifiedOn` when updating
  18. func updateTimeStampForUpdateCallback(scope *gorm.Scope) {
  19. if _, ok := scope.Get("gorm:update_column"); !ok {
  20. scope.SetColumn("ModifiedOn", time.Now().Unix())
  21. }
  22. }
  23. // deleteCallback will set `DeletedOn` where deleting
  24. func deleteCallback(scope *gorm.Scope) {
  25. if !scope.HasError() {
  26. var extraOption string
  27. if str, ok := scope.Get("gorm:delete_option"); ok {
  28. extraOption = fmt.Sprint(str)
  29. }
  30. deletedOnField, hasDeletedOnField := scope.FieldByName("DeletedOn")
  31. if !scope.Search.Unscoped && hasDeletedOnField {
  32. scope.Raw(fmt.Sprintf(
  33. "UPDATE %v SET %v=%v%v%v",
  34. scope.QuotedTableName(),
  35. scope.Quote(deletedOnField.DBName),
  36. scope.AddToVars(time.Now().Unix()),
  37. addExtraSpaceIfExist(scope.CombinedConditionSql()),
  38. addExtraSpaceIfExist(extraOption),
  39. )).Exec()
  40. } else {
  41. scope.Raw(fmt.Sprintf(
  42. "DELETE FROM %v%v%v",
  43. scope.QuotedTableName(),
  44. addExtraSpaceIfExist(scope.CombinedConditionSql()),
  45. addExtraSpaceIfExist(extraOption),
  46. )).Exec()
  47. }
  48. }
  49. }

重启服务,再在用 Postman 用 POST 访问 http://127.0.0.1:8000/api/v1/tags?name=2&state=1&created_by=test,发现 created_on 已经有值了!

在这几段代码中,涉及到知识点:
这属于 gormCallbacks,可以将回调方法定义为模型结构的指针,在创建、更新、查询、删除时将被调用,如果任何回调返回错误,gorm 将停止未来操作并回滚所有更改。

gorm所支持的回调方法:

  • 创建:BeforeSave、BeforeCreate、AfterCreate、AfterSave
  • 更新:BeforeSave、BeforeUpdate、AfterUpdate、AfterSave
  • 删除:BeforeDelete、AfterDelete
  • 查询:AfterFind

编写其余接口的路由逻辑

接下来,我们一口气把剩余的两个接口(EditTagDeleteTag)完成吧

打开 routers 目录下 **v1** 版本的 tag.go 文件,修改内容:
D:\Projects\Github\NoobWu\go-samples\go-gin-demo\routers\api\v1\tag.go

  1. //EditTag
  2. // @Summary 修改文章标签
  3. // @Produce json
  4. // @Param id path int true "ID"
  5. // @Param name query string true "ID"
  6. // @Param state query int false "State"
  7. // @Param modified_by query string true "ModifiedBy"
  8. // @Success 200 {string} json "{"code":200,"data":{},"msg":"ok"}"
  9. // @Router /api/v1/tags/{id} [put]
  10. func EditTag(c *gin.Context) {
  11. var (
  12. appG = app.Gin{C: c}
  13. form = EditTagForm{ID: com.StrTo(c.Param("id")).MustInt()}
  14. )
  15. //校验请求参数
  16. httpCode, errCode := app.BindAndValid(c, &form)
  17. if errCode != e.SUCCESS {
  18. appG.Response(httpCode, errCode, nil)
  19. return
  20. }
  21. tagService:=tag_service.Tag{
  22. ID: form.ID,
  23. Name: form.Name,
  24. State: form.State,
  25. ModifiedBy: form.ModifiedBy,
  26. }
  27. exists,err:=tagService.ExistByID()
  28. if err!=nil{
  29. appG.Response(http.StatusInternalServerError,e.ERROR_EXIST_TAG_FAIL,nil)
  30. return
  31. }
  32. if !exists{
  33. appG.Response(http.StatusOK,e.ERROR_NOT_EXIST_TAG,nil)
  34. return
  35. }
  36. err =tagService.Edit()
  37. if err!=nil{
  38. appG.Response(http.StatusInternalServerError,e.ERROR_EDIT_TAG_FAIL,nil)
  39. return
  40. }
  41. appG.Response(http.StatusOK,e.SUCCESS,nil)
  42. }
  43. // DeleteTag
  44. // @Summary 删除文章标签
  45. // @Produce json
  46. // @Param id path int true "ID"
  47. // @Success 200 {object} app.Response
  48. // @Failure 500 {object} app.Response
  49. // @Router /api/v1/tags/{id} [delete]
  50. func DeleteTag(c *gin.Context) {
  51. appG := app.Gin{C: c}
  52. valid := validation.Validation{}
  53. id := com.StrTo(c.Param("id")).MustInt()
  54. valid.Min(id, 1, "id").Message("ID必须大于0")
  55. if valid.HasErrors() {
  56. app.MarkErrors(valid.Errors)
  57. appG.Response(http.StatusBadRequest, e.INVALID_PARAMS, nil)
  58. }
  59. tagService := tag_service.Tag{ID: id}
  60. exists, err := tagService.ExistByID()
  61. if err != nil {
  62. appG.Response(http.StatusInternalServerError, e.ERROR_EXIST_TAG_FAIL, nil)
  63. return
  64. }
  65. if !exists {
  66. appG.Response(http.StatusOK, e.ERROR_NOT_EXIST_TAG, nil)
  67. return
  68. }
  69. if err := tagService.Delete(); err != nil {
  70. appG.Response(http.StatusInternalServerError, e.ERROR_DELETE_TAG_FAIL, nil)
  71. return
  72. }
  73. appG.Response(http.StatusOK, e.SUCCESS, nil)
  74. }

编写其余接口的 models 逻辑

打开 **models** 下的 **tag.go**,修改文件内容:
D:\Projects\Github\NoobWu\go-samples\go-gin-demo\models\tags.go

  1. // ExistTagByName checks if there is a tag with the same name
  2. func ExistTagByName(name string) (bool, error) {
  3. var tag Tag
  4. err := db.Select("id").Where("name = ? AND deleted_on = ? ", name, 0).First(&tag).Error
  5. if err != nil && err != gorm.ErrRecordNotFound {
  6. return false, err
  7. }
  8. if tag.ID > 0 {
  9. return true, nil
  10. }
  11. return false, nil
  12. }
  13. // AddTag Add a Tag
  14. func AddTag(name string, state int, createBy string) error {
  15. tag := Tag{
  16. Name: name,
  17. State: state,
  18. CreatedBy: createBy,
  19. }
  20. if err := db.Create(&tag).Error; err != nil {
  21. return err
  22. }
  23. return nil
  24. }
  25. // ExistTagByID determines whether a Tag exists based on the ID
  26. func ExistTagByID(id int) (bool, error) {
  27. var tag Tag
  28. err:=db.Select("id").Where("id=? AND deleted_on=?",id,0).Find(&tag).Error
  29. if err!=nil && err!=gorm.ErrRecordNotFound{
  30. return false,err
  31. }
  32. if tag.ID>0 {
  33. return true, nil
  34. }
  35. return false,nil
  36. }
  37. // EditTag modify a single tag
  38. func EditTag(id int, data interface{}) error {
  39. if err := db.Model(&Tag{}).Where("id = ? AND deleted_on = ? ", id, 0).Updates(data).Error; err != nil {
  40. return err
  41. }
  42. return nil
  43. }
  44. // DeleteTag delete a tag
  45. func DeleteTag(id int) error {
  46. if err:=db.Where("id=?",id).Delete(&Tag{}).Error;err!=nil{
  47. return err
  48. }
  49. return nil
  50. }

验证功能

重启服务,用 Postman

  • PUT 访问 http://127.0.0.1:7000/api/v1/tags/1?name=edit1&state=0&modified_by=edit1 ,查看 code 是否返回 200
  • DELETE 访问 http://127.0.0.1:7000/api/v1/tags/1 ,查看 code 是否返回 200

至此,Tag 的 API’s 完成,下一节我们将开始 Article 的 API’s 编写!

go-gin.postman_collection.json

参考

本系列示例代码

原文链接

https://eddycjy.com/posts/go/gin/2018-02-12-api-02/