golang项目-blog

Blog 项目

  • 项目地址:showfan: 个人博客网站 (gitee.com)

  • 技术栈:

    • 后端:

      框架 说明
      gin web框架
      gorm orm框架、操作数据库
      mysql 数据库
      goini 操作ini配置文件
      jwt 登录验证
      logrus 日志框架
      rotatelogs 日志分割、日志定期清理、生成软链文件指向最新日志
      lfshook 决定哪些级别的日志可以使用rotatelogs的切割设置,并决定输出格式(TEXT / JSON)
      cors 跨域
    • 前端:vue

  • 后端功能接口:

    • 用户登录
    • 用户新增、查询、编辑、删除
    • 分类新增、查询、编辑、删除
    • 文章新增、删除、编辑、查找单个文章、查询单个分类下的文章、查询所有文章列表
  • 依赖列表:

module showfan

go 1.20

require (
	github.com/bytedance/sonic v1.10.2 // indirect
	github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
	github.com/chenzhuoyu/iasm v0.9.1 // indirect
	github.com/gabriel-vasile/mimetype v1.4.3 // indirect
	github.com/gin-contrib/cors v1.5.0 // indirect
	github.com/gin-contrib/sse v0.1.0 // indirect
	github.com/gin-gonic/gin v1.9.1 // indirect
	github.com/go-playground/locales v0.14.1 // indirect
	github.com/go-playground/universal-translator v0.18.1 // indirect
	github.com/go-playground/validator/v10 v10.16.0 // indirect
	github.com/go-sql-driver/mysql v1.7.0 // indirect
	github.com/goccy/go-json v0.10.2 // indirect
	github.com/golang-jwt/jwt/v5 v5.2.0 // indirect
	github.com/jinzhu/inflection v1.0.0 // indirect
	github.com/jinzhu/now v1.1.5 // indirect
	github.com/json-iterator/go v1.1.12 // indirect
	github.com/klauspost/cpuid/v2 v2.2.6 // indirect
	github.com/leodido/go-urn v1.2.4 // indirect
	github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible // indirect
	github.com/lestrrat-go/strftime v1.0.6 // indirect
	github.com/mattn/go-isatty v0.0.20 // indirect
	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
	github.com/modern-go/reflect2 v1.0.2 // indirect
	github.com/pelletier/go-toml/v2 v2.1.0 // indirect
	github.com/pkg/errors v0.9.1 // indirect
	github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 // indirect
	github.com/sirupsen/logrus v1.9.3 // indirect
	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
	github.com/ugorji/go/codec v1.2.11 // indirect
	golang.org/x/arch v0.6.0 // indirect
	golang.org/x/crypto v0.15.0 // indirect
	golang.org/x/net v0.18.0 // indirect
	golang.org/x/sys v0.15.0 // indirect
	golang.org/x/text v0.14.0 // indirect
	google.golang.org/protobuf v1.31.0 // indirect
	gopkg.in/ini.v1 v1.67.0 // indirect
	gopkg.in/yaml.v3 v3.0.1 // indirect
	gorm.io/driver/mysql v1.5.2 // indirect
	gorm.io/gorm v1.25.5 // indirect
)

1 Gitee仓库

  • 新建仓库

    • 语言:Go
    • .gitignore:Go
    • 开源许可证:MIT
    • 使用Readme、单分支模型(只创建 master 分支)
  • ssh克隆到本地

    git clone git@gitee.com:xiufanivan/showfan.git
    

2 配置环境

  • win系统命令查看目录树

    tree /F
    
  • 创建目录

    showfan
    │  .gitignore					# 指定在使用 Git 进行版本控制时,应忽略哪些文件或文件夹
    │  go.mod						# 项目依赖
    │  go.sum						# 依赖校验
    │  latest_log.log				# 最新的日志
    │  LICENSE						# 项目的使用许可证
    │  main.go						# 主函数
    │  README.en.md
    │  README.md					# 项目的简介、使用说明
    │  tree.txt						# 项目的文件结构
    ├─api							# 控制器
    │  └─v1
    │          article.go			# 文章接口
    │          category.go			# 分类接口
    │          login.go				# 登录接口
    │          user.go				# 用户接口
    │
    ├─bin							# 可执行文件
    │      go_build_showfan.exe
    │
    ├─config						# 配置参数
    │      config.ini
    │
    ├─log							# 日志
    │      blog_log
    │      blog_log.20231226.log
    │      blog_log.20231227.log
    │
    ├─middleware					# 中间件
    │      cors.go					# 跨域
    │      jwt.go					# jwt验证
    │      logger.go				# 处理日志
    │
    ├─model							# 放置数据模型相关的代码,包括数据库模型、ORM模型等
    │      article.go				# 文章数据模型
    │      category.go				# 分类数据模型
    │      db.go					# 数据库连接配置
    │      user.go					# 用户数据模型
    │
    ├─routes						# 路由
    │      router.go
    │
    ├─uploads						# 上传下载,托管静态资源
    ├─utils							# 放置一些通用的工具函数或辅助函数,用于解决项目中的一些共性问题
    │  │  setting.go				# 通用的配置参数
    │  │
    │  ├─errmsg						# 错误信息
    │  │      errmsg.go
    │  │
    │  └─validator					# 参数验证
    │          validator.go
    │
    └─web							# 前端页面
    
  • 配置代理

    GOPROXY=https://goproxy.cn,direct
    

  • 配置调试器

    • 点击goland软件右上角【Add Configuration...】,点击左上角加号,再选择Go Build

    • 名称:编辑器名字,随意
    • 运行种类:目录
    • 目录:项目源文件所在目录
    • 输出目录:打包部署目录,可执行文件输出位置
    • 工作目录:项目根目录

  • go mod初始化

    go mod init showfan
    
  • 安装gin框架

    go get -u github.com/gin-gonic/gin
    

3 配置参数

  • 安装goini

    go get gopkg.in/ini.v1
    
  • 创建文件

    ├─ config
    |	├─ config.ini
    
  • mysql创建数据库

    • 数据库名称:showfan
    • 用户名:ivan
    • 密码:password1111
  • config.ini

    # server分区
    [server]
    # debug:开发模式;release:生产模式
    AppMode = debug
    HttpHost = :3000
    
    # 数据库配置
    [database]
    Db = mysql
    DbHost = localhost
    DbPort = 3306
    DbUser = ivan
    DbPassword = password1111
    DBName = showfan
    
    
  • 数据处理

    • 创建文件

      ├─ utils
      |	├─ setting.go
      
    • setting.go

      PS:需要调用时,调用setting.go里的参数;改参数时,改config.ini里的参数

      package utils
      
      import (
      	"fmt"
      	"gopkg.in/ini.v1"
      )
      
      // 引入变量
      var (
      	AppMode  string
      	HttpHost string
      
      	Db         string
      	DbHost     string
      	DbPort     string
      	DbUser     string
      	DbPassword string
      	DBName     string
      )
      
      func init() {
      	file, err := ini.Load("config/config.ini")
      	if err != nil {
      		fmt.Println("配置文件读取错误,请检查文件路径,err:", err)
      		return
      	}
      	LoadServer(file)
      	LoadData(file)
      }
      
      func LoadServer(file *ini.File) {
      	// 读取config.ini中server分区下的AppMode,默认参数设为debug
      	AppMode = file.Section("server").Key("AppMode").MustString("debug")
      	HttpHost = file.Section("server").Key("HttpHost").MustString(":3000")
      }
      
      func LoadData(file *ini.File) {
      	Db = file.Section("database").Key("Db").MustString("mysql")
      	DbHost = file.Section("database").Key("DbHost").MustString("localhost")
      	DbPort = file.Section("database").Key("DbPort").MustString("3306")
      	DbUser = file.Section("database").Key("DbUser").MustString("ivan")
      	DbPassword = file.Section("database").Key("DbPassword").MustString("password1111")
      	DBName = file.Section("database").Key("DBName").MustString("showfan")
      }
      
      

4 路由

  • 创建文件

    ├─ routes
    |	├─ route.go
    
  • route.go(先测试一下)

    package routes
    
    import (
    	"fmt"
    	"github.com/gin-gonic/gin"
    	"net/http"
    	"showfan/utils"
    )
    
    func InitRouter() {
    	// 设置gin的模式为utils.AppMode
    	gin.SetMode(utils.AppMode)
    
    	r := gin.Default()
    	router := r.Group("/api/v1")
    	{
    		router.GET("hello", func(c *gin.Context) {
    			c.JSON(http.StatusOK, gin.H{
    				"message": "hello world",
    			})
    		})
    	}
    
    	err := r.Run(utils.HttpHost)
    	if err != nil {
    		fmt.Println("路由运行失败,err:", err)
    		return
    	}
    }
    
    
  • mian.go

    package main
    
    import (
    	"showfan/routes"
    )
    
    func main() {
    	routes.InitRouter()
    }
    
    
  • postman中测试:localhost:3000/api/v1/hello路径下,GET请求,返回:

    {
        "message": "hello world"
    }
    

    测试成功!

  • route.go

    package routes
    
    import (
    	"fmt"
    	"github.com/gin-gonic/gin"
    	"showfan/api/v1"
    	"showfan/middleware"
    	"showfan/utils"
    )
    
    func InitRouter() {
    	// 设置 gin 的模式为 utils.AppMode
    	gin.SetMode(utils.AppMode)
    
    	r := gin.New()
    	// 开启日志
    	r.Use(middleware.Logger())
    	// Recovery中间件,该中间件用于在程序发生panic时恢复正常的执行流程,并返回一个错误页面给客户端。
    	r.Use(gin.Recovery())
    	// 跨域中间件
    	r.Use(middleware.Cors())
    
    	// *********** 私有路由 ****************
    	auth := r.Group("/api/v1")
    	auth.Use(middleware.JwtToken())
    	{
    		// *********** 用户模块的路由接口 ****************
    		// 编辑用户
    		auth.PUT("/user/:id", v1.EditUser)
    		// 删除用户
    		auth.DELETE("/user/:id", v1.DeleteUser)
    
    		// *********** 分类模块的路由接口 ****************
    		// 添加分类
    		auth.POST("/category/add", v1.AddCategory)
    		// 编辑分类
    		auth.PUT("/category/:id", v1.EditCategory)
    		// 删除分类
    		auth.DELETE("/category/:id", v1.DeleteCategory)
    
    		// *********** 文章模块的路由接口 ****************
    		// 添加文章
    		auth.POST("/article/add", v1.AddArticle)
    		// 编辑文章
    		auth.PUT("/article/:id", v1.EditArticle)
    		// 删除文章
    		auth.DELETE("/article/:id", v1.DeleteArticle)
    	}
    
    	// *********** 公共路由 ***********
    	public := r.Group("/api/v1")
    	{
    		// *********** 用户模块的路由接口 ****************
    		// 添加用户
    		auth.POST("/user/add", v1.AddUser)
    		// 查询用户列表
    		public.GET("/users", v1.GetUsers)
    
    		// *********** 分类模块的路由接口 ****************
    		// 查询分类列表
    		public.GET("/categories", v1.GetCategories)
    
    		// *********** 文章模块的路由接口 ****************
    		// 查询文章列表
    		public.GET("/articles", v1.GetArticles)
    		// 查询单个文章信息
    		public.GET("/article/info/:id", v1.GetArtInfo)
    		// 查询分类下的文章列表
    		public.GET("/article/list/:id", v1.GetArtByCate)
    
    		// *********** 文章模块的路由接口 ****************
    		// 用户登录
    		public.POST("/login", v1.Login)
    	}
    
    	// 启动服务
    	err := r.Run(utils.HttpHost)
    	if err != nil {
    		fmt.Println("路由运行失败,err:", err)
    		return
    	}
    }
    
    

5 数据库、数据模型

  • 安装gorm

    # mysql驱动
    go get gorm.io/driver/mysql
    # gorm
    go get gorm.io/gorm
    
  • 创建文件

    ├─ model
    |	├─ article.go	# 文章
    |	├─ category.go	# 分类
    |	├─ db.go		# 数据库
    |	├─ user.go		# 用户
    
  • db.go

    package model
    
    import (
    	"fmt"
    	"gorm.io/driver/mysql"
    	"gorm.io/gorm"
    	"gorm.io/gorm/schema"
    	"showfan/utils"
    	"time"
    )
    
    var (
    	db  *gorm.DB
    	err error
    )
    
    func InitDB() {
    	// **********连接数据库************
    	dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
    		utils.DbUser,
    		utils.DbPassword,
    		utils.DbHost,
    		utils.DbPort,
    		utils.DBName)
    	db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
    		NamingStrategy: schema.NamingStrategy{
    			TablePrefix:   "",    // 表名前缀
    			SingularTable: true,  // 单数表名
    			NoLowerCase:   false, // 关闭小写转换
    		},
    	})
    	if err != nil {
    		fmt.Println("连接数据库失败,请检查参数,err:", err)
    		return
    	}
    
    	// *********设置连接池和自动迁移**************
    	// 获取通用数据库对象 sql.DB ,然后使用其提供的功能
    	sqlDB, err := db.DB()
    	if err != nil {
    		fmt.Println("获取数据库对象失败,请检查参数,err:", err)
    		return
    	}
    
    	// 自动迁移,迁移这三个结构体
    	err = db.AutoMigrate(&User{}, &Article{}, &Category{})
    	if err != nil {
    		fmt.Println("自动迁移失败,请检查参数,err:", err)
    		return
    	}
    
    	// SetMaxIdleConns 用于设置连接池中空闲连接的最大数量。
    	sqlDB.SetMaxIdleConns(10)
    
    	// SetMaxOpenConns 设置打开数据库连接的最大数量。
    	sqlDB.SetMaxOpenConns(100)
    
    	// SetConnMaxLifetime 设置了连接可复用的最大时间。
    	sqlDB.SetConnMaxLifetime(10 * time.Second)
    }
    
    
  • user.go

    package model
    
    import "gorm.io/gorm"
    
    type User struct {
    	gorm.Model
    	Username string `json:"username" gorm:"type:varchar(20);not null;unique"`
    	Password string `json:"password" gorm:"type:varchar(20);not null"`
    	Role     int    `json:"role" gorm:"type:int;not null"`
    }
    
    
  • category.go

    package model
    
    import "gorm.io/gorm"
    
    type Category struct {
    	gorm.Model
    	Name string `json:"name" gorm:"type:varchar(20);not null;unique"`
    }
    
    
  • article.go

    package model
    
    import "gorm.io/gorm"
    
    type Article struct {
    	gorm.Model
    	// 标题
    	Title    string `json:"title" gorm:"type:varchar(100);not null"`
    	// 简介
    	Desc     string `json:"desc" gorm:"type:varchar(200);not null"`
    	// 内容
    	Content  string `json:"content" gorm:"type:longtext;not null"`
    	// 分类id
    	Cid      int    `json:"cid" gorm:"type:int;not null"`
    	// 图片
    	Img      string `json:"img" gorm:"type:varchar(100)"`
    	// 分类
    	Category Category
    }
    
    
  • mian.go中添加

    package main
    
    import (
    	"showfan/model"
    	"showfan/routes"
    )
    
    func main() {
    
    	// 初始化数据库
    	model.InitDB()
    
    	// 初始化路由
    	routes.InitRouter()
    }
    
    

6 异常处理

比如Username应当是唯一的,用户注册时,Username填了一个数据库中已经有的,要将错误返回给前端,前端返回给用户

  • 创建文件

    ├─ utils
    |	├─ errmsg
    |	|	├─ errmsg.go
    
  • errmsg.go

    package errmsg
    
    const (
    	SUCCESS = 200
    	ERROR   = 500
    
    	// code = 1001 ~ 1099 用户模块错误
    	ERROR_USERNAME_USED  = 1001
    	ERROR_PASSWORD_WRONG = 1002
    	ERROR_USER_NOT_EXIST = 1003
    	ERROR_USER_NO_RIGHT  = 1004
    
    	// code = 1101 ~ 1999 TOKEN模块错误
    	ERROR_TOKEN_NOT_EXIST  = 1101
    	ERROR_TOKEN_RUNTIME    = 1102
    	ERROR_TOKEN_WRONG      = 1103
    	ERROR_TOKEN_TYPE_WRONG = 1104
    
    	// code = 2001 ~ 2999 文章模块错误
    	ERROR_ARTICLE_NOT_EXIST = 2001
    
    	// code = 3001 ~ 3999 分类模块错误
    	ERROR_CATEGORY_USED      = 3001
    	ERROR_CATEGORY_NOT_EXIST = 3002
    )
    
    var codeMsg = map[int]string{
    	SUCCESS: "OK!",
    	ERROR:   "FAIL!",
    
    	ERROR_USERNAME_USED:  "用户名已存在!",
    	ERROR_PASSWORD_WRONG: "密码错误!",
    	ERROR_USER_NOT_EXIST: "用户不存在!",
    	ERROR_USER_NO_RIGHT:  "用户无权限!",
    
    	ERROR_TOKEN_NOT_EXIST:  "TOKEN不存在!",
    	ERROR_TOKEN_RUNTIME:    "TOKEN过期!",
    	ERROR_TOKEN_WRONG:      "TOKEN错误!",
    	ERROR_TOKEN_TYPE_WRONG: "TOKEN格式错误!",
    
    	ERROR_ARTICLE_NOT_EXIST: "文章不存在!",
    
    	ERROR_CATEGORY_USED:      "分类已存在!",
    	ERROR_CATEGORY_NOT_EXIST: "分类不存在!",
    }
    
    func GetErrMsg(code int) string {
    	return codeMsg[code]
    }
    
    

7 接口

api中写逻辑、model中写数据库操作

  • 创建文件

    ├─ api
    |	├─ v1
    |	|	├─ article.go
    |	|	├─ category.go
    |	|	├─ login.go
    |	|	├─ user.go
    

用户接口

model/user.go

package model

import (
	"encoding/base64"
	"golang.org/x/crypto/scrypt"
	"gorm.io/gorm"
	"log"
	"showfan/utils/errmsg"
)

type User struct {
	gorm.Model
	Username string `json:"username" gorm:"type:varchar(20);not null;unique" validate:"required,min=4,max=12" label:"用户名"`
	Password string `json:"password" gorm:"type:varchar(20);not null" validate:"required,min=6,max=20" label:"密码"`
	Role     int    `json:"role" gorm:"type:int;not null;DEFAULT:2" validate:"required,gte=2" label:"角色码"`
}

// ******** 密码加密 *********

// ScryptPw 密码加密
func ScryptPw(password string) string {
	const KeyLen = 10                                // 定义密钥长度为10
	salt := make([]byte, 8)                          // 创建一个长度为8的字节切片作为盐值,盐值增加随机性的
	salt = []byte{14, 32, 67, 29, 52, 88, 45, 6, 76} // 将随机一组数赋值给盐值切片
	// 使用scrypt.Key函数计算密码的哈希值
	// 第一个参数是要加密的密码,第二个参数是盐值,第三个参数是迭代次数(必须是2的幂且小于2^30且大于等于1),第四个参数是块大小,第五个参数是密钥长度
	HashPw, err := scrypt.Key([]byte(password), salt, 16384, 8, 1, KeyLen)
	if err != nil {
		log.Fatal(err)
	}
	// 将哈希值转换为base64编码的字符串
	fpw := base64.StdEncoding.EncodeToString(HashPw)
	return fpw
}

// BeforeSave 对象自动调用
// todo 这个方法不起作用,可能是我写的有问题,我先注释掉,等我看完后再写
//func (u *User) BeforeSave() {
//	u.Password = ScryptPw(u.Password)
//}

// ******** 添加用户模块 **********

// CheckUser 查询用户是否存在
func CheckUser(username string) (code int) {
	var user User
	// 选择数据库中的id列,根据给定的用户名和密码进行过滤,将查询结果的第一个记录赋值给user变量
	db.Select("id").Where("username =?", username).First(&user)
	if user.ID > 0 {
		return errmsg.ERROR_USERNAME_USED
	}
	return errmsg.SUCCESS
}

// CreateUser 增加用户
func CreateUser(user *User) (code int) {
	user.Password = ScryptPw(user.Password)
	err := db.Create(&user).Error
	if err != nil {
		return errmsg.ERROR // 500
	}
	return errmsg.SUCCESS // 200
}

// ********* 查询用户模块 **********

// GetUsers 查询用户列表(分页查询)
func GetUsers(pageSize int, pageNum int) (users []User, total int64) {
	// 这个go函数是用于对数据库进行分页查询的。
	// 通过设置每页的数量(pageSize),以及当前页数(pageNum),
	// 使用db.Limit()和Offset()方法来限制查询结果的数量和偏移量。
	// 最后使用Find()方法将查询结果赋值给users变量。
	// 如果查询过程中出现错误,则将错误信息赋值给err变量。
	// total变量用于保存查询到的总记录数。
	err := db.Limit(pageSize).Offset((pageNum - 1) * pageSize).Find(&users).Count(&total).Error
	if err != nil && err != gorm.ErrRecordNotFound {
		return nil, 0
	}
	return users, total
}

// ******** 删除用户模块 **********

// DeleteUser 删除用户
func DeleteUser(id int) (code int) {
	var user User
	// 选择数据库中的id列,根据给定的id进行过滤,将查询结果的第一个记录赋值给user变量
	db.Select("id").Where("id =?", id).First(&user)
	if user.ID == 0 {
		return errmsg.ERROR_USER_NOT_EXIST
	}
	// 删除user变量对应的记录
	err := db.Delete(&user).Error
	if err != nil {
		return errmsg.ERROR
	}
	return errmsg.SUCCESS
}

// ******** 更新用户模块 **********

// EditUser 更新用户信息
func EditUser(id int, data *User) (code int) {
	var maps = make(map[string]interface{})
	maps["username"] = data.Username
	data.Password = ScryptPw(data.Password)
	maps["password"] = data.Password
	maps["role"] = data.Role

	var user User
	err := db.Model(&user).Where("id = ? ", id).Updates(&maps).Error
	if err != nil {
		return errmsg.ERROR
	}
	return errmsg.SUCCESS
}

// ******** 登录模块 **********

// CheckLogin 登陆验证
func CheckLogin(username string, password string) (code int) {
	var user User
	// 选择数据库中的id列,根据给定的用户名和密码进行过滤,将查询结果的第一个记录赋值给user变量
	db.Where("username =?", username).First(&user)
	// 判断用户是否存在
	if user.ID == 0 {
		code = errmsg.ERROR_USER_NOT_EXIST // 1003
		return code
	}
	// 判断密码是否正确
	if ScryptPw(password) != user.Password {
		code = errmsg.ERROR_PASSWORD_WRONG // 1004
		return code
	}
	// 判断用户是否有管理员权限
	if user.Role != 1 {
		code = errmsg.ERROR_USER_NO_RIGHT // 1005
		return code
	}
	return errmsg.SUCCESS
}

api/v1/user.go

package v1

import (
	"github.com/gin-gonic/gin"
	"net/http"
	"showfan/model"
	"showfan/utils/errmsg"
	"showfan/utils/validator"
	"strconv"
)

// AddUser 添加用户
func AddUser(c *gin.Context) {
	// 解析请求体中的用户信息
	var user model.User
	// 数据绑定到结构体中
	err := c.ShouldBindJSON(&user)
	if err != nil {
		// 若解析失败,则返回错误信息
		c.JSON(http.StatusBadRequest, gin.H{
			"status":  errmsg.ERROR,
			"message": "(v1/user AddUser 添加用户)绑定结构体失败",
		})
		return
	}

	// 验证用户信息是否符合要求,model.user结构体中validate定义了验证规则
	msg, code := validator.Validate(&user)
	if code != errmsg.SUCCESS {
		c.JSON(http.StatusOK, gin.H{
			"status":  errmsg.ERROR,
			"message": msg,
		})
		return
	}

	// 验证用户名是否已存在
	code = model.CheckUser(user.Username)
	if code == errmsg.SUCCESS {
		// 若用户名可用,则创建用户
		code = model.CreateUser(&user)
	}
	// 若用户名已被使用,则返回错误信息

	// 返回添加用户的结果
	c.JSON(http.StatusOK, gin.H{
		"status":  code,
		"data":    user,
		"message": errmsg.GetErrMsg(code),
	})
}

// 查询单个用户

// GetUsers 查询用户列表
func GetUsers(c *gin.Context) {
	// 获取前端传来的分页参数
	pageSize, _ := strconv.Atoi(c.Query("pagesize"))
	pageNum, _ := strconv.Atoi(c.Query("pagenum"))

	// 若pageSize或pageNum为0,则表示不分页
	if pageSize == 0 {
		pageSize = -1
	}
	if pageNum == 0 {
		pageNum = -1
	}

	// 查询用户列表
	data, total := model.GetUsers(pageSize, pageNum)
	code := errmsg.SUCCESS

	// 返回查询用户列表的结果
	c.JSON(http.StatusOK, gin.H{
		"status":  code,
		"data":    data,
		"total":   total,
		"message": errmsg.GetErrMsg(code),
	})
}

// EditUser 编辑用户
func EditUser(c *gin.Context) {
	var user model.User
	// 解析URL参数
	id, err := strconv.Atoi(c.Param("id"))
	if err != nil {
		// 参数错误,返回错误响应并终止函数
		c.JSON(http.StatusBadRequest, gin.H{
			"status":  errmsg.ERROR,
			"message": "(v1/user EditUser 编辑用户)参数错误",
		})
		return
	}

	// 绑定JSON请求体到用户结构体
	err = c.ShouldBindJSON(&user)
	if err != nil {
		// 绑定结构体失败,返回错误响应并终止函数
		c.JSON(http.StatusBadRequest, gin.H{
			"status":  errmsg.ERROR,
			"message": "(v1/user EditUser 编辑用户)绑定结构体失败",
		})
		return
	}

	// 验证用户信息是否符合要求,model.user结构体中validate定义了验证规则
	msg, code := validator.Validate(&user)
	if code != errmsg.SUCCESS {
		c.JSON(http.StatusOK, gin.H{
			"status":  errmsg.ERROR,
			"message": msg,
		})
		return
	}

	// todo 如果要修改其他信息,但用户名不改,会报用户名已被使用错误
	// 检查用户名是否已被使用
	code = model.CheckUser(user.Username)
	if code == errmsg.SUCCESS {
		// 用户名可用,执行用户编辑操作
		code = model.EditUser(id, &user)
	}

	if code == errmsg.ERROR_USERNAME_USED {
		c.Abort()
	}

	// 返回编辑结果响应
	c.JSON(http.StatusOK, gin.H{
		"status":  code,
		"message": errmsg.GetErrMsg(code),
	})
}

// DeleteUser 删除用户
func DeleteUser(c *gin.Context) {
	id, err := strconv.Atoi(c.Param("id"))
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{
			"status":  errmsg.ERROR,
			"message": "(v1/user DeleteUser 删除用户)参数错误",
		})
		return
	}
	code := model.DeleteUser(id)
	c.JSON(http.StatusOK, gin.H{
		"status":  code,
		"message": errmsg.GetErrMsg(code),
	})
}

测试用户接口

添加用户

POST:localhost:5000/api/v1/user/add

{
    "username": "user2",
    "password": "password1111",
    "role": 1
}
查询用户列表

GET:localhost:5000/api/v1/users?pagesize=10&pagenum=1

  • 添加上面两个Params
删除用户

DELETE:localhost:5000/api/v1/user/1

编辑用户

PUT:localhost:5000/api/v1/user/8

{
    "username": "user1",
    "role": 1
}

分类接口

model/category.go

package model

import (
	"gorm.io/gorm"
	"showfan/utils/errmsg"
)

type Category struct {
	// ID 是一个无符号整数类型(uint)的变量,用于表示主键并自增
	ID   uint   `json:"id" gorm:"primary_key;auto_increment"`
	Name string `json:"name" gorm:"type:varchar(20);not null;unique"`
}

// ******** 添加分类模块 **********

// CheckCategory 查询分类是否存在
func CheckCategory(name string) (code int) {
	var cate Category
	// 选择数据库中的id列,根据给定的分类名进行过滤,将查询结果的第一个记录赋值给 cate 变量
	db.Select("id").Where("name = ?", name).First(&cate)
	if cate.ID > 0 {
		return errmsg.ERROR_CATEGORY_USED // 3001
	}
	return errmsg.SUCCESS // 200
}

// CreateCategory 增加分类
func CreateCategory(cate *Category) (code int) {
	err := db.Create(&cate).Error
	if err != nil {
		return errmsg.ERROR // 500
	}
	return errmsg.SUCCESS // 200
}

// ********* 查询分类模块 **********

// GetCategories 查询分类列表(分页查询)
func GetCategories(pageSize int, pageNum int) (categories []Category, total int64) {
	// 这个go函数是用于对数据库进行分页查询的。
	// 通过设置每页的数量(pageSize),以及当前页数(pageNum),
	// 使用db.Limit()和Offset()方法来限制查询结果的数量和偏移量。
	// 最后使用Find()方法将查询结果赋值给users变量。
	// 如果查询过程中出现错误,则将错误信息赋值给err变量。
	// total 用于记录查询到的总记录数。
	err := db.Limit(pageSize).Offset((pageNum - 1) * pageSize).Find(&categories).Count(&total).Error
	if err != nil && err != gorm.ErrRecordNotFound {
		return nil, 0
	}
	return categories, total
}

// todo 查询分类下的文章

// ******** 删除分类模块 **********

// DeleteCategory 删除分类
func DeleteCategory(id int) (code int) {
	var cate Category
	// 选择数据库中的id列,根据给定的id进行过滤,将查询结果的第一个记录赋值给 cate变量
	db.Select("id").Where("id =?", id).First(&cate)
	if cate.ID == 0 {
		return errmsg.ERROR_CATEGORY_NOT_EXIST // 3002
	}
	// 删除 cate 变量对应的记录
	err := db.Delete(&cate).Error
	if err != nil {
		return errmsg.ERROR // 500
	}
	return errmsg.SUCCESS // 200
}

// ******** 更新分类模块 **********

// EditCategory 更新分类信息
func EditCategory(id int, data *Category) int {
	var maps = make(map[string]interface{})
	maps["name"] = data.Name

	var cate Category
	err := db.Model(&cate).Where("id = ? ", id).Updates(&maps).Error
	if err != nil {
		return errmsg.ERROR // 500
	}
	return errmsg.SUCCESS // 200
}

api/v1/category.go

package v1

import (
	"github.com/gin-gonic/gin"
	"net/http"
	"showfan/model"
	"showfan/utils/errmsg"
	"strconv"
)

// AddCategory 添加分类
func AddCategory(c *gin.Context) {
	// 解析请求体中的分类信息
	var cate model.Category
	// 数据绑定到结构体中
	err := c.ShouldBindJSON(&cate)
	if err != nil {
		// 若解析失败,则返回错误信息
		c.JSON(http.StatusBadRequest, gin.H{
			"status":  errmsg.ERROR, // 200
			"message": "(v1/category AddCategory 添加分类)绑定结构体失败",
		})
		return
	}

	// 验证分类名是否已存在
	code := model.CheckCategory(cate.Name)
	if code == errmsg.SUCCESS { // 200
		// 若分类名可用,则创建分类
		code = model.CreateCategory(&cate)
	}
	// 若分类名已被使用,则返回错误信息
	// 返回添加分类的结果
	c.JSON(http.StatusOK, gin.H{
		"status":  code,
		"data":    cate,
		"message": errmsg.GetErrMsg(code),
	})
}

// todo 查询单个分类下的文章

// GetCategories 查询分类列表
func GetCategories(c *gin.Context) {
	// 获取前端传来的分页参数
	pageSize, _ := strconv.Atoi(c.Query("pagesize"))
	pageNum, _ := strconv.Atoi(c.Query("pagenum"))

	// 若pageSize或pageNum为0,则表示不分页
	if pageSize == 0 {
		pageSize = -1
	}
	if pageNum == 0 {
		pageNum = -1
	}

	// 查询用户列表
	data, total := model.GetCategories(pageSize, pageNum)
	code := errmsg.SUCCESS // 200

	// 返回查询用户列表的结果
	c.JSON(http.StatusOK, gin.H{
		"status":  code,
		"data":    data,
		"total":   total,
		"message": errmsg.GetErrMsg(code),
	})
}

// EditCategory 编辑分类
func EditCategory(c *gin.Context) {
	var cate model.Category
	// 解析 URL 参数
	id, err := strconv.Atoi(c.Param("id"))
	if err != nil {
		// 参数错误,返回错误响应并终止函数
		c.JSON(http.StatusBadRequest, gin.H{
			"status":  errmsg.ERROR, // 200
			"message": "(v1/category EditCategory 编辑分类)参数错误",
		})
		return
	}

	// 绑定JSON请求体到用户结构体
	err = c.ShouldBindJSON(&cate)
	if err != nil {
		// 绑定结构体失败,返回错误响应并终止函数
		c.JSON(http.StatusBadRequest, gin.H{
			"status":  errmsg.ERROR,
			"message": "(v1/category EditCategory 编辑分类)绑定结构体失败",
		})
		return
	}

	// todo 如果要修改其他信息,但分类名不改,会报分类名已被使用错误
	// 检查用户名是否已被使用
	code := model.CheckCategory(cate.Name)
	if code == errmsg.SUCCESS {
		// 用户名可用,执行用户编辑操作
		code = model.EditCategory(id, &cate)
	}

	if code == errmsg.ERROR_CATEGORY_USED {
		c.Abort()
	}

	// 返回编辑结果响应
	c.JSON(http.StatusOK, gin.H{
		"status":  code,
		"message": errmsg.GetErrMsg(code),
	})
}

// DeleteCategory 删除分类
func DeleteCategory(c *gin.Context) {
	id, err := strconv.Atoi(c.Param("id"))
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{
			"status":  errmsg.ERROR, // 200
			"message": "(v1/category DeleteCategory 删除用户)参数错误",
		})
		return
	}
	code := model.DeleteCategory(id)
	c.JSON(http.StatusOK, gin.H{
		"status":  code,
		"message": errmsg.GetErrMsg(code),
	})
}

测试分类接口

添加分类

POST:localhost:5000/api/v1/category/add

{
    "name": "c++"
}
查询用户分类

GET:localhost:5000/api/v1/categories?pagesize=10&pagenum=1

  • 添加上面两个Params
删除分类

DELETE:localhost:5000/api/v1/category/2

编辑分类

PUT:localhost:5000/api/v1/category/1

{
    "name": "golang"
}

文章接口

model/article.go

package model

import (
	"gorm.io/gorm"
	"showfan/utils/errmsg"
)

type Article struct {
	// 分类
	Category Category `json:"category" gorm:"foreignKey:Cid"`

	gorm.Model
	// 标题
	Title string `json:"title" gorm:"type:varchar(100);not null"`
	// 简介
	Desc string `json:"desc" gorm:"type:varchar(200);not null"`
	// 内容
	Content string `json:"content" gorm:"type:longtext;not null"`
	// 分类id
	Cid int `json:"cid" gorm:"type:int;not null"`
	// 图片
	Img string `json:"img" gorm:"type:varchar(100)"`
}

// ******** 添加文章模块 **********

// CreateArticle 增加文章
func CreateArticle(art *Article) (code int) {
	err := db.Create(&art).Error
	if err != nil {
		return errmsg.ERROR // 500
	}
	return errmsg.SUCCESS // 200
}

// ********* 查询文章模块 **********

// GetArticles 查询所有文章列表(分页查询)
func GetArticles(pageSize int, pageNum int) (articleList []Article, code int, total int64) {
	// 这个go函数是用于对数据库进行分页查询的。
	// 通过设置每页的数量(pageSize),以及当前页数(pageNum),
	// 使用db.Limit()和Offset()方法来限制查询结果的数量和偏移量。
	// 最后使用Find()方法将查询结果赋值给 articleList变量。
	// 如果查询过程中出现错误,则将错误信息赋值给err变量。
	// total变量用于保存查询到的总记录数。
	err := db.Preload("Category").Limit(pageSize).Offset((pageNum - 1) * pageSize).Find(&articleList).
		Count(&total).Error
	if err != nil && err != gorm.ErrRecordNotFound {
		return nil, errmsg.ERROR, 0 // 500
	}
	return articleList, errmsg.SUCCESS, total // 200
}

// GetArtInfo 查询单个文章信息
func GetArtInfo(id int) (art []Article, code int) {
	err := db.Preload("Category").Where("id = ?", id).First(&art).Error
	if err != nil {
		return art, errmsg.ERROR_ARTICLE_NOT_EXIST // 2001
	}
	return art, errmsg.SUCCESS // 200
}

// GetCateArtList 查询分类下的文章列表
func GetCateArtList(pageSize int, pageNum int, cid int) (artList []Article, code int, total int64) {
	err := db.Preload("Category").Limit(pageSize).Offset((pageNum-1)*pageSize).
		Where("cid = ?", cid).Find(&artList).Count(&total).Error
	if err != nil {
		return nil, errmsg.ERROR_ARTICLE_NOT_EXIST, 0 // 2001
	}
	return artList, errmsg.SUCCESS, total // 200
}

// ******** 删除文章模块 **********

// DeleteArticle 删除文章
func DeleteArticle(id int) (code int) {
	var art Article
	// 选择数据库中的id列,根据给定的id进行过滤,将查询结果的第一个记录赋值给 art 变量
	db.Select("id").Where("id = ?", id).First(&art)
	if art.ID == 0 {
		return errmsg.ERROR_ARTICLE_NOT_EXIST // 2001
	}
	// 删除 art 变量对应的记录
	err := db.Delete(&art).Error
	if err != nil {
		return errmsg.ERROR // 500
	}
	return errmsg.SUCCESS // 200
}

// ******** 更新文章模块 **********

// EditArticle 更新文章信息
func EditArticle(id int, data *Article) int {
	var maps = make(map[string]interface{})
	maps["title"] = data.Title
	maps["desc"] = data.Desc
	maps["content"] = data.Content
	maps["cid"] = data.Cid
	maps["img"] = data.Img

	var art Article
	err := db.Model(&art).Where("id = ? ", id).Updates(&maps).Error
	if err != nil {
		return errmsg.ERROR // 500
	}
	return errmsg.SUCCESS // 200
}

api/v1/article.go

package v1

import (
	"github.com/gin-gonic/gin"
	"net/http"
	"showfan/model"
	"showfan/utils/errmsg"
	"strconv"
)

// AddArticle 添加文章
func AddArticle(c *gin.Context) {
	// 解析请求体中的分类信息
	var art model.Article
	// 数据绑定到结构体中
	err := c.ShouldBindJSON(&art)
	if err != nil {
		// 若解析失败,则返回错误信息
		c.JSON(http.StatusBadRequest, gin.H{
			"status":  errmsg.ERROR, // 200
			"message": "(v1/article AddArticle 添加文章)绑定结构体失败",
		})
		return
	}

	code := model.CreateArticle(&art)

	// 返回添加文章的结果
	c.JSON(http.StatusOK, gin.H{
		"status":  code,
		"data":    art,
		"message": errmsg.GetErrMsg(code),
	})
}

// GetArtByCate 查询分类下所有文章
func GetArtByCate(c *gin.Context) {
	// 获取前端传来的分页参数
	pageSize, _ := strconv.Atoi(c.Query("pagesize"))
	pageNum, _ := strconv.Atoi(c.Query("pagenum"))
	id, _ := strconv.Atoi(c.Param("id"))

	// 若 pageSize 或 pageNum 为 0,则表示不分页
	if pageSize == 0 {
		pageSize = -1
	}
	if pageNum == 0 {
		pageNum = -1
	}
	data, code, total := model.GetCateArtList(pageSize, pageNum, id)
	c.JSON(http.StatusOK, gin.H{
		"status":  code,
		"data":    data,
		"total":   total,
		"message": errmsg.GetErrMsg(code),
	})
}

// GetArtInfo 查询单个文章信息
func GetArtInfo(c *gin.Context) {
	id, err := strconv.Atoi(c.Param("id"))
	if err != nil {
		// 参数错误,返回错误响应并终止函数
		c.JSON(http.StatusBadRequest, gin.H{
			"status":  errmsg.ERROR, // 200
			"message": "(v1/article GetArtInfo 查询单个文章信息)参数错误",
		})
		return
	}
	// 查询文章信息
	data, code := model.GetArtInfo(id)
	// 返回查询文章信息的结果
	c.JSON(http.StatusOK, gin.H{
		"status":  code,
		"data":    data,
		"message": errmsg.GetErrMsg(code),
	})
}

// GetArticles 查询所有文章列表
func GetArticles(c *gin.Context) {
	// 获取前端传来的分页参数
	pageSize, _ := strconv.Atoi(c.Query("pagesize"))
	pageNum, _ := strconv.Atoi(c.Query("pagenum"))

	// 若 pageSize 或 pageNum 为 0,则表示不分页
	if pageSize == 0 {
		pageSize = -1
	}
	if pageNum == 0 {
		pageNum = -1
	}

	// 查询用户列表
	data, code, total := model.GetArticles(pageSize, pageNum)

	// 返回查询用户列表的结果
	c.JSON(http.StatusOK, gin.H{
		"status":  code,
		"data":    data,
		"total":   total,
		"message": errmsg.GetErrMsg(code),
	})
}

// EditArticle 编辑文章
func EditArticle(c *gin.Context) {
	var art model.Article
	// 解析 URL 参数
	id, err := strconv.Atoi(c.Param("id"))
	if err != nil {
		// 参数错误,返回错误响应并终止函数
		c.JSON(http.StatusBadRequest, gin.H{
			"status":  errmsg.ERROR, // 200
			"message": "(v1/article EditArticle 编辑文章)参数错误",
		})
		return
	}

	// 绑定JSON请求体到用户结构体
	err = c.ShouldBindJSON(&art)
	if err != nil {
		// 绑定结构体失败,返回错误响应并终止函数
		c.JSON(http.StatusBadRequest, gin.H{
			"status":  errmsg.ERROR,
			"message": "(v1/article EditArticle 编辑分类)绑定结构体失败",
		})
		return
	}

	// todo 如果要修改其他信息,但分类名不改,会报分类名已被使用错误

	// 修改文章
	code := model.EditArticle(id, &art)

	// 返回编辑结果响应
	c.JSON(http.StatusOK, gin.H{
		"status":  code,
		"message": errmsg.GetErrMsg(code),
	})
}

// DeleteArticle 删除文章
func DeleteArticle(c *gin.Context) {
	id, err := strconv.Atoi(c.Param("id"))
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{
			"status":  errmsg.ERROR, // 200
			"message": "(v1/category DeleteCategory 删除用户)参数错误",
		})
		return
	}
	code := model.DeleteArticle(id)
	c.JSON(http.StatusOK, gin.H{
		"status":  code,
		"message": errmsg.GetErrMsg(code),
	})
}

测试文章接口

添加文章

POST:localhost:5000/api/v1/article/add

{
    "title":"c++笔记4",
    "desc":"这是我的c++笔记4",
    "content":"这是我的c++笔记 这是我的c++笔记 这是我的c++笔记",
    "cid":4,
    "img":"wadwefe"
}
查询文章列表

GET:localhost:5000/api/v1/articles?pagesize=10&pagenum=2

  • 添加上面两个Params
查询单个文章信息

GET:localhost:5000/api/v1/article/info/5

查询分类下的文章列表

GET:localhost:5000/api/v1/article/list/8?pagesize=10&pagenum=1

  • 添加上面两个Params
删除文章

DELETE:localhost:5000/api/v1/article/3

编辑文章

PUT:localhost:5000/api/v1/article/1

{
    "title": "golang笔记",
    "desc": "这是我的golang笔记",
    "content": "这是我的golang笔记 这是我的golang笔记 这是我的golang笔记",
    "cid": 1,
    "img": "wadwefe"
}

登录接口

  • 安装jwt

    go get github.com/golang-jwt/jwt/v5
    

middleware/jwt.go

package middleware

import (
	"github.com/gin-gonic/gin"
	"github.com/golang-jwt/jwt/v5"
	"net/http"
	"showfan/utils"
	"showfan/utils/errmsg"
	"strings"
	"time"
)

// JwtKey jwt密钥
var JwtKey = []byte(utils.JwtKey)

// MyClaims 自定义声明
type MyClaims struct {
	Username string `json:"username"`
	jwt.RegisteredClaims
}

// CreateToken 生成token
func CreateToken(username string) (token string, code int) {
	// 过期时间,10小时
	expireTime := time.Now().Add(time.Hour * 10)
	// 声明
	SetClaims := MyClaims{
		username,
		jwt.RegisteredClaims{
			// 过期时间
			ExpiresAt: jwt.NewNumericDate(expireTime),
			// 签发者
			Issuer: "showfan",
		},
	}

	// 生成一个新的带有声明的JWT令牌
	// 使用HS256签名方法和SetClaims作为声明
	// 使用JwtKey进行签名并返回签名后的字符串
	token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, SetClaims).SignedString(JwtKey)
	if err != nil {
		return "", errmsg.ERROR // 500
	}
	return token, errmsg.SUCCESS // 200
}

// CheckToken 验证token
func CheckToken(token string) (*MyClaims, int) {
	setToken, err := jwt.ParseWithClaims(token, &MyClaims{}, func(token *jwt.Token) (i interface{}, err error) {
		return JwtKey, nil
	})
	if err != nil {
		return nil, errmsg.ERROR // 500
	}

	// 获取自定义声明中的数据,类型断言
	key, ok := setToken.Claims.(*MyClaims)
	// 判断令牌是否过期
	if ok && setToken.Valid {
		return key, errmsg.SUCCESS // 200
	} else {
		return nil, errmsg.ERROR // 500
	}
}

// JwtToken jwt中间件
func JwtToken() gin.HandlerFunc {
	return func(c *gin.Context) {
		code := errmsg.ERROR_TOKEN_NOT_EXIST // 1004
		tokenHeader := c.Request.Header.Get("Authorization")
		if tokenHeader == "" {
			c.JSON(http.StatusOK, gin.H{
				"status":  code,
				"message": errmsg.GetErrMsg(code),
			})
			c.Abort()
			return
		}
		checkToken := strings.SplitN(tokenHeader, " ", 2)
		if len(checkToken) != 2 || checkToken[0] != "Bearer" {
			c.JSON(http.StatusOK, gin.H{
				"status":  code,
				"message": errmsg.GetErrMsg(code),
			})
			c.Abort()
			return
		}
		key, tCode := CheckToken(checkToken[1])
		if tCode == errmsg.ERROR {
			code = errmsg.ERROR_TOKEN_WRONG
			c.JSON(http.StatusOK, gin.H{
				"code":    code,
				"message": errmsg.GetErrMsg(code),
			})
			c.Abort()
			return
		}
		if time.Now().Unix() > key.ExpiresAt.Unix() {
			code = errmsg.ERROR_TOKEN_RUNTIME
			c.JSON(http.StatusOK, gin.H{
				"code":    code,
				"message": errmsg.GetErrMsg(code),
			})
			c.Abort()
			return
		}

		c.Set("username", key.Username)
		c.Next()
	}
}

model/user.go

// ******** 登录模块 **********

// CheckLogin 登陆验证
func CheckLogin(username string, password string) (code int) {
	var user User
	// 选择数据库中的id列,根据给定的用户名和密码进行过滤,将查询结果的第一个记录赋值给user变量
	db.Where("username =?", username).First(&user)
	// 判断用户是否存在
	if user.ID == 0 {
		code = errmsg.ERROR_USER_NOT_EXIST // 1003
		return code
	}
	// 判断密码是否正确
	if ScryptPw(password) != user.Password {
		code = errmsg.ERROR_PASSWORD_WRONG // 1004
		return code
	}
	// 判断用户是否有管理员权限
	if user.Role != 0 {
		code = errmsg.ERROR_USER_NO_RIGHT // 1005
		return code
	}
	return errmsg.SUCCESS
}

api/v1/login.go

package v1

import (
	"github.com/gin-gonic/gin"
	"showfan/middleware"
	"showfan/model"
	"showfan/utils/errmsg"
)

func Login(c *gin.Context) {
	var data model.User
	// 数据绑定到结构体
	err := c.ShouldBindJSON(&data)
	if err != nil {
		c.JSON(200, gin.H{
			"status":  "error",
			"message": "Login_ShouldBindJSON错误!",
		})
		return
	}

	// 验证用户名和密码
	var (
		code  int
		token string
	)
	code = model.CheckLogin(data.Username, data.Password)
	if code == errmsg.SUCCESS {
		// 生成token
		token, code = middleware.CreateToken(data.Username)
		if code == errmsg.ERROR {
			c.JSON(200, gin.H{
				"status":  "error",
				"message": "登录失败!",
			})
			return
		}
	}
	// 返回数据,登录成功
	c.JSON(200, gin.H{
		"status":  code,
		"message": errmsg.GetErrMsg(code),
		"token":   token,
	})
}

测试登录接口

用户登录

POST:localhost:5000/api/v1/login

{
    "username":"ivan",
    "password":"password1111"
}
  • 成功登录会返回Token,使用Token可以进行私有路由的接口调用
  • Postman中,--》Authorization --》Bearer Token --》Token中填如之前的Token,再进行测试接口。

上传接口

├─ upload
|	├─ upload.go

8 日志

  • 安装日志框架

    # logrus
    go get github.com/sirupsen/logrus
    # rotatelogs
    go get github.com/lestrrat-go/file-rotatelogs
    # lfshook
    go get github.com/rifflock/lfshook
    
  • 创建目录

    ├─ log				# 存放日志文件
    ├─ middleware
    |	├─ logger.go	# 日志中间件
    

middleware/logger.go

package middleware

import (
	"fmt"
	"github.com/gin-gonic/gin"
	rotatelogs "github.com/lestrrat-go/file-rotatelogs"
	"github.com/rifflock/lfshook"
	"github.com/sirupsen/logrus"
	"math"
	"os"
	"time"
)

func Logger() gin.HandlerFunc {
	filePath := "log/blog_log"
	linkName := "latest_log.log"
	/*	os.O_CREATE表示如果文件不存在则创建文件,
		os.O_RDWR表示以读写方式打开文件。
		详见 os.OpenFile函数中,权限参数
		权限参数 0755表示文件的权限为拥有者可读写执行,同组用户和其它用户可读执行。
		详见Linux文件权限	*/
	scr, err := os.OpenFile(filePath, os.O_CREATE|os.O_RDWR, 0755)
	if err != nil {
		panic(err)
	}
	logger := logrus.New()
	logger.Out = scr                   // 设置输出到日志文件
	logger.SetLevel(logrus.DebugLevel) // 设置日志级别

	// 日志切割
	logWriter, err := rotatelogs.New(
		filePath+".%Y%m%d.log",                    // 日志文件名,"%Y%m%d"表示以年月日的顺序取出当前日期
		rotatelogs.WithMaxAge(7*24*time.Hour),     // 日志文件最大保存时间为7天
		rotatelogs.WithRotationTime(24*time.Hour), // 日志文件切割时间为24小时
		rotatelogs.WithLinkName(linkName),         // 生成软链,指向最新日志文件,win系统需要管理员权限
	)
	if err != nil {
		fmt.Println("logWriter failed:", err)
	}

	// 定义一个写入器映射writeMap,用于将日志消息写入到logWriter中
	writeMap := lfshook.WriterMap{
		logrus.InfoLevel:  logWriter, // 将InfoLevel级别的日志消息写入到logWriter中
		logrus.FatalLevel: logWriter, // 将FatalLevel级别的日志消息写入到logWriter中
		logrus.DebugLevel: logWriter, // 将DebugLevel级别的日志消息写入到logWriter中
		logrus.WarnLevel:  logWriter, // 将WarnLevel级别的日志消息写入到logWriter中
		logrus.ErrorLevel: logWriter, // 将ErrorLevel级别的日志消息写入到logWriter中
		logrus.PanicLevel: logWriter, // 将PanicLevel级别的日志消息写入到logWriter中
	}
	// 创建一个新的hook Hook,使用writeMap作为写入器映射,logrus.TextFormatter作为日志格式化器
	Hook := lfshook.NewHook(writeMap, &logrus.TextFormatter{
		TimestampFormat: "2006-01-02 15:04:05", // 设置时间戳格式为"2006-01-02 15:04:05"
	})
	// 将hook Hook添加到logger中
	logger.AddHook(Hook)

	return func(c *gin.Context) {

		startTime := time.Now() // 开始时间
		c.Next()
		stopTime := time.Since(startTime) // 结束时间
		// 耗时
		spendTime := fmt.Sprintf("%v ms", int(math.Ceil(float64(stopTime.Nanoseconds())/1000000.0)))
		// 主机名
		hostName, err := os.Hostname()
		if err != nil {
			hostName = "unknown"
		}
		statusCode := c.Writer.Status()    // 状态码
		clientIP := c.ClientIP()           // 客户端IP
		userAgent := c.Request.UserAgent() // 用户代理(浏览器)
		dataSize := c.Writer.Size()        // 请求文件大小
		if dataSize < 0 {
			dataSize = 0
		}
		method := c.Request.Method   // 请求方法
		path := c.Request.RequestURI // 请求路径

		entry := logger.WithFields(logrus.Fields{
			"HostName":  hostName,
			"Status":    statusCode,
			"SpendTime": spendTime,
			"IP":        clientIP,
			"Method":    method,
			"Path":      path,
			"DataSize":  dataSize,
			"Agent":     userAgent,
		})
		// 内部错误
		if len(c.Errors) > 0 {
			entry.Error(c.Errors.ByType(gin.ErrorTypePrivate).String())
		}
		// 状态码错误
		if statusCode >= 500 {
			entry.Error()
		} else if statusCode >= 400 {
			entry.Warn()
		} else {
			entry.Info()
		}
	}
}

9 参数验证、跨域参数配置

参数验证

model/user.go

  • 添加validate参数验证

  • 添加label,结构体属性会按照label翻译

    type User struct {
    	gorm.Model
    	Username string `json:"username" gorm:"type:varchar(20);not null;unique" validate:"required,min=4,max=12" label:"用户名"`
    	Password string `json:"password" gorm:"type:varchar(20);not null" validate:"required,min=6,max=20" label:"密码"`
    	Role     int    `json:"role" gorm:"type:int;not null;DEFAULT:2" validate:"required,gte=2" label:"角色码"`
    }
    

utils/validator/validator.go

  • 新建文件

    ├─ utils
    |	├─ validator
    |	|	├─ validator.go
    
  • validator.go

    package validator
    
    import (
    	"github.com/go-playground/locales/zh_Hans_CN"
    	ut "github.com/go-playground/universal-translator"
    	"github.com/go-playground/validator/v10"
    	"github.com/go-playground/validator/v10/translations/zh"
    	"reflect"
    	"showfan/utils/errmsg"
    )
    
    func Validate(data interface{}) (string, int) {
    	validate := validator.New()                 // 创建一个新的验证器实例
    	uni := ut.New(zh_Hans_CN.New())             // 创建一个新的文本翻译器实例,使用中文(简体)作为语言环境
    	trans, _ := uni.GetTranslator("zh_Hans_CN") // 获取中文翻译器的实例
    
    	err := zh.RegisterDefaultTranslations(validate, trans) // 使用中文翻译器注册默认的翻译规则到验证器中
    	if err != nil {
    		println("zh.RegisterDefaultTranslations failed, err:", err) // 如果注册失败,则打印错误信息
    	}
    
    	// 注册自定义标签验证器,结构体属性会按照 label 翻译
    	validate.RegisterTagNameFunc(func(field reflect.StructField) string {
    		label := field.Tag.Get("label")  // 这里的 "label" 是结构体的标签,可以根据需要修改
    		return label
    	})
    
    	// 验证结构体,这里可以保证传进来的是结构体,其他开发场景请先断言
    	err = validate.Struct(data)
    	if err != nil {
    		for _, v := range err.(validator.ValidationErrors) {
    			return v.Translate(trans), errmsg.ERROR
    		}
    	}
    	return "", errmsg.SUCCESS
    
    }
    
    
  • 在新增用户、编辑用户中ShouldBindJSON后添加

    // 验证用户信息是否符合要求,model.user结构体中validate定义了验证规则
    	msg, code := validator.Validate(&user)
    	if code != errmsg.SUCCESS {
    		c.JSON(http.StatusOK, gin.H{
    			"status":  errmsg.ERROR,
    			"message": msg,
    		})
    		return
    	}
    

跨域

  • 安装cors

    go get github.com/gin-contrib/cors
    
  • 新建文件

    ├─ middleware
    |	├─ cors.go
    

middleware/cors.go

package middleware

import (
	"github.com/gin-contrib/cors"
	"github.com/gin-gonic/gin"
	"time"
)

// Cors 跨域中间件
func Cors() gin.HandlerFunc {
	return cors.New(
		cors.Config{
			//AllowAllOrigins:  true,
			AllowOrigins:     []string{"*"}, // 等同于允许所有域名 #AllowAllOrigins:  true
			AllowMethods:     []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
			AllowHeaders:     []string{"*", "Authorization"},
			ExposeHeaders:    []string{"Content-Length", "text/plain", "Authorization", "Content-Type"},
			AllowCredentials: true,
			MaxAge:           12 * time.Hour,
		},
	)
}

posted @ 2023-11-09 22:27  修凡  阅读(8)  评论(0编辑  收藏  举报