golang项目-blog
Blog 项目
-
技术栈:
-
后端:
框架 说明 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,
},
)
}