39.用Gin完成一个简易待办事项管理后端代码(五)待办事项后端代码

代码地址:https://gitee.com/lymgoforIT/golang-trick/tree/master/24-gin-learning

一:主要功能

  • 用户可以注册、登录和注销登录
  • 用户登录后可以创建、修改、查看和删除自己的待办事项
  • 用户对待办事项的增删改查有身份验证的过程

工作中,实际进入开发前一般都会先出技术方案,其中基本就会包含整体架构图,服务链路拓扑图,特定功能时序图或执行流程图等,画好图有如下好处

  • 显示代码的执行流程,包括各个模块之间的调用顺序和控制流程
  • 说明代码中各个模块之间的关系和依赖
  • 帮助开发人员更好的理解和调试代码
  • 对代码进行分析和优化
  • 提高代码的可读性、可维护性和可复用性

二:model实体

从主要功能可以看出,主要需要用户实体和待办事项实体

type User struct {
	gorm.Model
	Username string `json:"username" gorm:"unique"` // 唯一键
	Password string `json:"password"`
}

// 待办事项结构体
type Todo struct {
	gorm.Model
	Title  string `json:"title"`
	Status string `json:"status"`
	UserId int    `json:"user_id"`
}

三:数据库初始化


func initDB() (*gorm.DB, error) {
	dsn := "root:root@(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=true&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		panic(any("failed to connect to db"))
	}

	// 建表
	err = db.AutoMigrate(&User{}, &Todo{})
	if err != nil {
		return nil, err
	}
	return db, nil
}

四:用户注册与登录

注册:
在这里插入图片描述

登录:
在这里插入图片描述

// 用户注册
func signUp(c *gin.Context, db *gorm.DB) {
	var user User

	if err := c.BindJSON(&user); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	// 用户名为唯一键,不允许重复,所以根据用户名查询是否已经存在同名的用户
	var existingUser User
	err := db.Where("username = ?", user.Username).First(existingUser).Error
	if err != nil && err != gorm.ErrRecordNotFound {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}
	if existingUser.ID != 0 {
		c.JSON(http.StatusConflict, gin.H{"error": "用户名已存在!"})
		return
	}

	// 用户密码加密入库
	password, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}

	// hash加密后的密码赋值给用户,从而入库
	user.Password = string(password)
	err = db.Create(&user).Error
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}

	c.JSON(http.StatusCreated, user)
}

// 用户登录
func signIn(c *gin.Context, db *gorm.DB) {
	var user User

	if err := c.BindJSON(&user); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	// 查询用户是否存在,存在则查出用户,然后对比加密的密码是否一致
	var existingUser User
	err := db.Where("username = ?", user.Username).First(&existingUser).Error
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}
	if existingUser.ID == 0 {
		c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误!"})
		return
	}

	// 比对密码
	err = bcrypt.CompareHashAndPassword([]byte(existingUser.Password), []byte(user.Password))
	if err != nil {
		c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误!"})
		return
	}

	// 用户名和token都校验通过了,生成token返回
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
		"id":       existingUser.ID,
		"username": existingUser.Username,
	})
	// 对token进行加盐操作
	signedToken, err := token.SignedString([]byte(secretKey))
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}

	// 加盐后的token返回
	c.JSON(http.StatusOK, gin.H{"token": signedToken})
}

五:待办事项增删改查

用于操作待办事项前的鉴权中间件:
在这里插入图片描述

创建:
在这里插入图片描述

查询:
在这里插入图片描述

更新:
在这里插入图片描述

删除:
在这里插入图片描述


// 用于操作待办事项前的鉴权中间件
func authenticationMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		tokenString := c.GetHeader("token")

		if tokenString == "" {
			c.JSON(http.StatusUnauthorized, gin.H{"error": "权限校验未通过,请检查token是否传递"})
			c.Abort()
			return
		}

		// 解析token,拿到token中的用户名和id等信息
		token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
			return []byte(secretKey), nil
		})
		if err != nil {
			c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
			c.Abort()
			return
		}

		// token无效或者过期了
		if !token.Valid {
			c.JSON(http.StatusUnauthorized, gin.H{"error": "无效的token"})
			c.Abort()
			return
		}

		claims := token.Claims.(jwt.MapClaims)
		// 将用户id设置在context上下文中,断言为float64后转为int
		c.Set("userID", int(claims["id"].(float64)))
		c.Next() // 执行下一个handler
	}
}

// 创建待办事项
func createTodo(c *gin.Context, db *gorm.DB) {
	var todo Todo
	if err := c.BindJSON(&todo); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	todo.UserId = c.GetInt("userID")
	db.Create(&todo)
	c.JSON(http.StatusOK, todo)
}

// 查询待办事项
func getTodos(c *gin.Context, db *gorm.DB) {
	var todos []Todo
	db.Where("user_id = ?", c.GetInt("userID")).Find(&todos)
	c.JSON(http.StatusOK, todos)
}

// 更新待办事项
func updateTodo(c *gin.Context, db *gorm.DB) {
	id, err := strconv.Atoi(c.Param("id")) // 从路径参数中取出需要更新的待办事项id
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	var updateTodo Todo
	if err := c.BindJSON(&updateTodo); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	// 更新操作需要指明model,从而知道是哪个表
	db.Model(&Todo{}).Where("id = ? and user_id = ?", id, c.GetInt("userID")).Updates(updateTodo)
	c.JSON(http.StatusOK, updateTodo)
}

// 删除待办事项
func deleteTodo(c *gin.Context, db *gorm.DB) {
	id, err := strconv.Atoi(c.Param("id")) // 从路径参数中取出需要更新的待办事项id
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	var todo Todo
	// 根据待办事项id和用户id查询指定帖子是否存在,存在才删除
	db.Where("id = ? and user_id = ?", id, c.GetInt("userID")).First(&todo)
	if todo.ID == 0 {
		c.JSON(http.StatusNotFound, gin.H{"error": "Todo not found"})
		return
	}

	db.Delete(&todo)

	c.JSON(http.StatusOK, gin.H{"message": "Todo deleted"})
}

六:路由注册

func main() {
	db, err := initDB()
	if err != nil {
		panic(any("create table err"))
	}

	r := gin.Default()

	r.POST("/signUp", func(c *gin.Context) {
		signUp(c, db)
	})

	r.POST("/signIn", func(c *gin.Context) {
		signIn(c, db)
	})

	authorized := r.Group("/")
	// 作者操作待办事项前需要先鉴权,所以对该路由分组使用鉴权中间件
	authorized.Use(authenticationMiddleware())
	// 用大括号将该路由组下各路由包起来,方便管理(后期会介绍如果进行结构化管理)
	{
		authorized.POST("/todos", func(c *gin.Context) {
			createTodo(c, db)
		})

		authorized.GET("/todos", func(c *gin.Context) {
			getTodos(c, db)
		})

		authorized.PUT("/todos/:id", func(c *gin.Context) {
			updateTodo(c, db)
		})

		authorized.DELETE("/todos/:id", func(c *gin.Context) {
			deleteTodo(c, db)
		})

		// 退出登录,之所以写在该路由组下,是因为该路由组添加了鉴权中间件,退出登录前需要先鉴权是已经登录的状态
		// 这里退出登录只是简单模拟,打印输出而已,实际退出登录,应该对token等进行销毁
		authorized.POST("/signOut", func(c *gin.Context) {
			c.JSON(http.StatusOK, gin.H{"message": "Logged out"})
		})
	}

	// 启动服务
	r.Run(":8080")
}

七:完整代码

package main

import (
	"github.com/gin-gonic/gin"
	"github.com/golang-jwt/jwt/v4"
	"golang.org/x/crypto/bcrypt"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"net/http"
	"strconv"
)

const secretKey = "abc123"

type User struct {
	gorm.Model
	Username string `json:"username" gorm:"unique"` // 唯一键
	Password string `json:"password"`
}

// 待办事项结构体
type Todo struct {
	gorm.Model
	Title  string `json:"title"`
	Status string `json:"status"`
	UserId int    `json:"user_id"`
}

func initDB() (*gorm.DB, error) {
	dsn := "root:root@(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=true&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		panic(any("failed to connect to db"))
	}

	// 建表
	err = db.AutoMigrate(&User{}, &Todo{})
	if err != nil {
		return nil, err
	}
	return db, nil
}

// 用户注册
func signUp(c *gin.Context, db *gorm.DB) {
	var user User

	if err := c.BindJSON(&user); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	// 用户名为唯一键,不允许重复,所以根据用户名查询是否已经存在同名的用户
	var existingUser User
	err := db.Where("username = ?", user.Username).First(existingUser).Error
	if err != nil && err != gorm.ErrRecordNotFound {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}
	if existingUser.ID != 0 {
		c.JSON(http.StatusConflict, gin.H{"error": "用户名已存在!"})
		return
	}

	// 用户密码加密入库
	password, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}

	// hash加密后的密码赋值给用户,从而入库
	user.Password = string(password)
	err = db.Create(&user).Error
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}

	c.JSON(http.StatusCreated, user)
}

// 用户登录
func signIn(c *gin.Context, db *gorm.DB) {
	var user User

	if err := c.BindJSON(&user); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	// 查询用户是否存在,存在则查出用户,然后对比加密的密码是否一致
	var existingUser User
	err := db.Where("username = ?", user.Username).First(&existingUser).Error
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}
	if existingUser.ID == 0 {
		c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误!"})
		return
	}

	// 比对密码
	err = bcrypt.CompareHashAndPassword([]byte(existingUser.Password), []byte(user.Password))
	if err != nil {
		c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误!"})
		return
	}

	// 用户名和token都校验通过了,生成token返回
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
		"id":       existingUser.ID,
		"username": existingUser.Username,
	})
	// 对token进行加盐操作
	signedToken, err := token.SignedString([]byte(secretKey))
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}

	// 加盐后的token返回
	c.JSON(http.StatusOK, gin.H{"token": signedToken})
}

// 用于操作待办事项前的鉴权中间件
func authenticationMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		tokenString := c.GetHeader("token")

		if tokenString == "" {
			c.JSON(http.StatusUnauthorized, gin.H{"error": "权限校验未通过,请检查token是否传递"})
			c.Abort()
			return
		}

		// 解析token,拿到token中的用户名和id等信息
		token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
			return []byte(secretKey), nil
		})
		if err != nil {
			c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
			c.Abort()
			return
		}

		// token无效或者过期了
		if !token.Valid {
			c.JSON(http.StatusUnauthorized, gin.H{"error": "无效的token"})
			c.Abort()
			return
		}

		claims := token.Claims.(jwt.MapClaims)
		// 将用户id设置在context上下文中,断言为float64后转为int
		c.Set("userID", int(claims["id"].(float64)))
		c.Next() // 执行下一个handler
	}
}

// 创建待办事项
func createTodo(c *gin.Context, db *gorm.DB) {
	var todo Todo
	if err := c.BindJSON(&todo); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	todo.UserId = c.GetInt("userID")
	db.Create(&todo)
	c.JSON(http.StatusOK, todo)
}

// 查询待办事项
func getTodos(c *gin.Context, db *gorm.DB) {
	var todos []Todo
	db.Where("user_id = ?", c.GetInt("userID")).Find(&todos)
	c.JSON(http.StatusOK, todos)
}

// 更新待办事项
func updateTodo(c *gin.Context, db *gorm.DB) {
	id, err := strconv.Atoi(c.Param("id")) // 从路径参数中取出需要更新的待办事项id
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	var updateTodo Todo
	if err := c.BindJSON(&updateTodo); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	// 更新操作需要指明model,从而知道是哪个表
	db.Model(&Todo{}).Where("id = ? and user_id = ?", id, c.GetInt("userID")).Updates(updateTodo)
	c.JSON(http.StatusOK, updateTodo)
}

// 删除待办事项
func deleteTodo(c *gin.Context, db *gorm.DB) {
	id, err := strconv.Atoi(c.Param("id")) // 从路径参数中取出需要更新的待办事项id
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	var todo Todo
	// 根据待办事项id和用户id查询指定帖子是否存在,存在才删除
	db.Where("id = ? and user_id = ?", id, c.GetInt("userID")).First(&todo)
	if todo.ID == 0 {
		c.JSON(http.StatusNotFound, gin.H{"error": "Todo not found"})
		return
	}

	db.Delete(&todo)

	c.JSON(http.StatusOK, gin.H{"message": "Todo deleted"})
}

func main() {
	db, err := initDB()
	if err != nil {
		panic(any("create table err"))
	}

	r := gin.Default()

	r.POST("/signUp", func(c *gin.Context) {
		signUp(c, db)
	})

	r.POST("/signIn", func(c *gin.Context) {
		signIn(c, db)
	})

	authorized := r.Group("/")
	// 作者操作待办事项前需要先鉴权,所以对该路由分组使用鉴权中间件
	authorized.Use(authenticationMiddleware())
	// 用大括号将该路由组下各路由包起来,方便管理(后期会介绍如果进行结构化管理)
	{
		authorized.POST("/todos", func(c *gin.Context) {
			createTodo(c, db)
		})

		authorized.GET("/todos", func(c *gin.Context) {
			getTodos(c, db)
		})

		authorized.PUT("/todos/:id", func(c *gin.Context) {
			updateTodo(c, db)
		})

		authorized.DELETE("/todos/:id", func(c *gin.Context) {
			deleteTodo(c, db)
		})

		// 退出登录,之所以写在该路由组下,是因为该路由组添加了鉴权中间件,退出登录前需要先鉴权是已经登录的状态
		// 这里退出登录只是简单模拟,打印输出而已,实际退出登录,应该对token等进行销毁
		authorized.POST("/signOut", func(c *gin.Context) {
			c.JSON(http.StatusOK, gin.H{"message": "Logged out"})
		})
	}

	// 启动服务
	r.Run(":8080")
}

八:测试

1、启动服务,查看注册的路由在这里插入图片描述

2、注册
在这里插入图片描述
注册用户root和admin两个用户,可以看到注册成功,且密码进行了加密处理
在这里插入图片描述
在这里插入图片描述

查看DB在这里插入图片描述

3、登录
登录成功,返回了token,后续对待办事项的请求,需要在请求头中带上该token,约定放到名为”token“的请求头中
在这里插入图片描述
4、创建待办事项
未在请求头中携带token时请求如下
在这里插入图片描述
携带token请求头后请求
在这里插入图片描述
在这里插入图片描述
5、获取待办事项列表
记得在请求头中携带token哦,获取前我创建了吃饭和睡觉两个待办事项在这里插入图片描述
6、更新待办事项
更新吃饭的状态为已完成
在这里插入图片描述
在这里插入图片描述

7、删除待办事项
删除吃饭待办事项
在这里插入图片描述
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值