添加用户帖子
有了身份验证,就该开始使用它了。我们将需要身份验证来创建、阅读、更新和删除用户的博客文章。让我们从添加新的数据库迁移开始,这将创建所需的包含列的数据表。创建新的迁移文件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 中的函数的 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
}
进入全屏模式 退出全屏模式
更多推荐
所有评论(0)