有了身份验证,就该开始使用它了。我们将需要身份验证来创建、阅读、更新和删除用户的博客文章。让我们从添加新的数据库迁移开始,这将创建所需的包含列的数据表。创建新的迁移文件migrations/2_addPostsTable.go:

package main

import (
  "fmt"

  "github.com/go-pg/migrations/v8"
)

func init() {
  migrations.MustRegisterTx(func(db migrations.DB) error {
    fmt.Println("creating table posts...")
    _, err := db.Exec(`CREATE TABLE posts(
      id SERIAL PRIMARY KEY,
      title TEXT NOT NULL,
      content TEXT NOT NULL,
      created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
      modified_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
      user_id INT REFERENCES users ON DELETE CASCADE
    )`)
    return err
  }, func(db migrations.DB) error {
    fmt.Println("dropping table posts...")
    _, err := db.Exec(`DROP TABLE posts`)
    return err
  })
}

进入全屏模式 退出全屏模式

然后运行迁移:

cd migrations/
go run *.go up

进入全屏模式 退出全屏模式

现在我们创建结构来保存发布数据。我们还将为标题和内容添加字段约束。添加新文件internal/store/posts.go:

package store

import "time"

type Post struct {
  ID         int
  Title      string    `binding:"required,min=3,max=50"`
  Content    string    `binding:"required,min=5,max=5000"`
  CreatedAt  time.Time
  ModifiedAt time.Time
  UserID     int `json:"-"`
}

进入全屏模式 退出全屏模式

用户可以有多个博客文章,因此我们必须在 User 结构中添加has-many关系。在internal/store/users.go中,编辑用户结构:

type User struct {
  ID             int
  Username       string `binding:"required,min=5,max=30"`
  Password       string `pg:"-" binding:"required,min=7,max=32"`
  HashedPassword []byte `json:"-"`
  Salt           []byte `json:"-"`
  CreatedAt      time.Time
  ModifiedAt     time.Time
  Posts          []*Post `json:"-" pg:"fk:user_id,rel:has-many,on_delete:CASCADE"`
}

进入全屏模式 退出全屏模式

可以在数据库中插入新帖子条目的功能将在internal/store/posts.go中实现:

func AddPost(user *User, post *Post) error {
  post.UserID = user.ID
  _, err := db.Model(post).Returning("*").Insert()
  if err != nil {
    log.Error().Err(err).Msg("Error inserting new post")
  }
  return err
}

进入全屏模式 退出全屏模式

要创建帖子,我们将添加将调用上述函数的新处理程序。创建新文件internal/server/post.go:

package server

import (
  "net/http"
  "rgb/internal/store"

  "github.com/gin-gonic/gin"
)

func createPost(ctx *gin.Context) {
  post := new(store.Post)
  if err := ctx.Bind(post); err != nil {
    ctx.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
    return
  }
  user, err := currentUser(ctx)
  if err != nil {
    ctx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
    return
  }
  if err := store.AddPost(user, post); err != nil {
    ctx.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
    return
  }
  ctx.JSON(http.StatusOK, gin.H{
    "msg":  "Post created successfully.",
    "data": post,
  })
}

进入全屏模式 退出全屏模式

帖子创建处理程序已准备就绪,让我们为创建帖子添加新的受保护路由。在internal/server/router.go中,我们将创建一个新组,该组将使用我们在上一章中实现的authorization中间件。我们将使用 HTTP 方法 POST 将路由/posts添加到该受保护组:

func setRouter() *gin.Engine {
  // Creates default gin router with Logger and Recovery middleware already attached
  router := gin.Default()

  // Enables automatic redirection if the current route can't be matched but a
  // handler for the path with (without) the trailing slash exists.
  router.RedirectTrailingSlash = true

  // Create API route group
  api := router.Group("/api")
  {
    api.POST("/signup", signUp)
    api.POST("/signin", signIn)
  }

  authorized := api.Group("/")
  authorized.Use(authorization)
  {
    authorized.POST("/posts", createPost)
  }

  router.NoRoute(func(ctx *gin.Context) { ctx.JSON(http.StatusNotFound, gin.H{}) })

  return router
}

进入全屏模式 退出全屏模式

所有其他 CRUD(创建、读取、更新、删除)方法的配方相同:

1.实现与数据库通信以执行所需操作的功能

  1. 实现将使用步骤 1 中的函数的 Gin 处理程序

3.将带有处理程序的路由添加到路由器

我们已经覆盖了Create部分,所以让我们继续下一个方法,Read。我们将实现从internal/store/posts.go中的数据库中获取所有用户帖子的函数:

func FetchUserPosts(user *User) error {
  err := db.Model(user).
    Relation("Posts", func(q *orm.Query) (*orm.Query, error) {
      return q.Order("id ASC"), nil
    }).
    Select()
  if err != nil {
    log.Error().Err(err).Msg("Error fetching user's posts")
  }
  return err
}

进入全屏模式 退出全屏模式

internal/server/post.go中:

func indexPosts(ctx *gin.Context) {
  user, err := currentUser(ctx)
  if err != nil {
    ctx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
    return
  }
  if err := store.FetchUserPosts(user); err != nil {
    ctx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
    return
  }
  ctx.JSON(http.StatusOK, gin.H{
    "msg":  "Posts fetched successfully.",
    "data": user.Posts,
  })
}

进入全屏模式 退出全屏模式

Update帖子中,将这 2 个函数添加到internal/store/posts.go:

func FetchPost(id int) (*Post, error) {
  post := new(Post)
  post.ID = id
  err := db.Model(post).WherePK().Select()
  if err != nil {
    log.Error().Err(err).Msg("Error fetching post")
    return nil, err
  }
  return post, nil
}

func UpdatePost(post *Post) error {
  _, err := db.Model(post).WherePK().UpdateNotZero()
  if err != nil {
    log.Error().Err(err).Msg("Error updating post")
  }
  return err
}

进入全屏模式 退出全屏模式

internal/server/post.go中:

func updatePost(ctx *gin.Context) {
  jsonPost := new(store.Post)
  if err := ctx.Bind(jsonPost); err != nil {
    ctx.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
    return
  }
  user, err := currentUser(ctx)
  if err != nil {
    ctx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
    return
  }
  dbPost, err := store.FetchPost(jsonPost.ID)
  if err != nil {
    ctx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
    return
  }
  if user.ID != dbPost.UserID {
    ctx.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "Not authorized."})
    return
  }
  jsonPost.ModifiedAt = time.Now()
  if err := store.UpdatePost(jsonPost); err != nil {
    ctx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
    return
  }
  ctx.JSON(http.StatusOK, gin.H{
    "msg":  "Post updated successfully.",
    "data": jsonPost,
  })
}

进入全屏模式 退出全屏模式

最后,将Delete帖子添加到internal/store.posts.go:

func DeletePost(post *Post) error {
  _, err := db.Model(post).WherePK().Delete()
  if err != nil {
    log.Error().Err(err).Msg("Error deleting post")
  }
  return err
}

进入全屏模式 退出全屏模式

internal/server/post.go中:

func deletePost(ctx *gin.Context) {
  paramID := ctx.Param("id")
  id, err := strconv.Atoi(paramID)
  if err != nil {
    ctx.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Not valid ID."})
    return
  }
  user, err := currentUser(ctx)
  if err != nil {
    ctx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
    return
  }
  post, err := store.FetchPost(id)
  if err != nil {
    ctx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
    return
  }
  if user.ID != post.UserID {
    ctx.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "Not authorized."})
    return
  }
  if err := store.DeletePost(post); err != nil {
    ctx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
    return
  }
  ctx.JSON(http.StatusOK, gin.H{"msg": "Post deleted successfully."})
}

进入全屏模式 退出全屏模式

您可以在此处注意到的一件新事物是paramID := ctx.Param("id")。我们正在使用它从 URL 路径中提取 ID 参数。

让我们将所有这些处理程序添加到路由器:

func setRouter() *gin.Engine {
  // Creates default gin router with Logger and Recovery middleware already attached
  router := gin.Default()

  // Enables automatic redirection if the current route can't be matched but a
  // handler for the path with (without) the trailing slash exists.
  router.RedirectTrailingSlash = true

  // Create API route group
  api := router.Group("/api")
  {
    api.POST("/signup", signUp)
    api.POST("/signin", signIn)
  }

  authorized := api.Group("/")
  authorized.Use(authorization)
  {
    authorized.GET("/posts", indexPosts)
    authorized.POST("/posts", createPost)
    authorized.PUT("/posts", updatePost)
    authorized.DELETE("/posts/:id", deletePost)
  }

  router.NoRoute(func(ctx *gin.Context) { ctx.JSON(http.StatusNotFound, gin.H{}) })

  return router
}

进入全屏模式 退出全屏模式

如果用户还没有帖子,则User.Posts字段默认为 nil。这使前端的事情变得复杂,因为它必须检查 nil 值,所以最好使用空切片。为此,我们将使用AfterSelectHook,它将在每次为User执行Select()后执行。该挂钩将添加到internal/store/users.go:

var _ pg.AfterSelectHook = (*User)(nil)

func (user *User) AfterSelect(ctx context.Context) error {
  if user.Posts == nil {
    user.Posts = []*Post{}
  }
  return nil
}

进入全屏模式 退出全屏模式

Logo

PostgreSQL社区为您提供最前沿的新闻资讯和知识内容

更多推荐