项目流程
配置项
配置内容放在config.json内
{
"app_config": {
"app_name": "商品管理系统",
"host": "xx.xx.xx.xx",
"port": "xx",
"database": {
"database": "admin_test_back",
"user": "root",
"password": "xxx",
"host": "localhost",
"port": "3306",
"charset": "utf8mb4",
"parse_time": true,
"loc": "Local"
}
}
}
包括了项目的配置以及数据库的配置,并在config.go中进行配置的读取
package config
import (
"encoding/json"
"log"
"os"
)
var GlobalConfig *Config
type Config struct {
AppConfig *AppConfig `json:"app_config"`
}
type AppConfig struct {
AppName string `json:"app_name"`
Host string `json:"host"`
Port string `json:"port"`
Database *Database `json:"database"`
}
type Database struct {
Database string `json:"database"`
User string `json:"user"`
Password string `json:"password"`
Host string `json:"host"`
Port string `json:"port"`
Charset string `json:"charset"`
ParseTime bool `json:"parse_time"`
Loc string `json:"loc"`
}
func InitConfig() {
path := "config.json"
read, err := os.ReadFile(path)
if err != nil {
log.Fatalln(err)
}
c := new(Config)
if err := json.Unmarshal(read, c); err != nil {
log.Fatalln(err)
}
//全局赋值
GlobalConfig = c
}
定义三个结构体,第一个是Config是总体的结构体,第二个AppConfig是项目的结构体,第三个Database是数据库的结构体。
接着在InitConfig中读取config.json中的配置内容,将它们赋值给一个新创建的Config结构体c,最后将c赋值给全局变量GlobalConfig,以后调用配置的时候只需将config.go这个包导入然后使用GlobalConfig即可。
数据库初始化
在db文件夹的mysql.go中进行数据库初始化
var DB *gorm.DB
func InitMysql() {
database := config.GlobalConfig.AppConfig.Database
parseTimeStr := "False"
if database.ParseTime {
parseTimeStr = "True"
}
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=%s&parseTime=%s&loc=%s", database.User, database.Password, database.Host, database.Port, database.Database, database.Charset, parseTimeStr, database.Loc)
db, err := gorm.Open("mysql", dsn)
if err != nil {
log.Fatalln("mysql connect failed", err)
}
DB = db
}
首先定义一个全局变量DB,类型是*gorm.DB,使用了gorm,今后需要操作数据库的时候将mysql.go包导入然后使用DB即可。
在InitMysql函数中进行数据库初始化
func InitMysql() {
database := config.GlobalConfig.AppConfig.Database
parseTimeStr := "False"
if database.ParseTime {
parseTimeStr = "True"
}
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=%s&parseTime=%s&loc=%s", database.User, database.Password, database.Host, database.Port, database.Database, database.Charset, parseTimeStr, database.Loc)
db, err := gorm.Open("mysql", dsn)
if err != nil {
log.Fatalln("mysql connect failed", err)
}
DB = db
}
调用GlbalConfig中的Database对数据库进行初始化,dsn即为数据库的相关内容,然后调用gorm.Open("mysql", dsn)打开mysql数据库连接,返回一个数据库实体db,类型是*gorm.DB,最后将db赋值给全局变量DB。
main函数
在main函数中运行项目,首先使用init函数进行初始化
func init() {
// 初始化配置文件
config.InitConfig()
// 初始化mysql
db.InitMysql()
// 初始化redis
// redis.InitRedis(config.GlobalConfig.AppConfig)
}
接着在main函数中运行项目
func main() {
// 获取应用配置文件
c := config.GlobalConfig.AppConfig
r := gin.Default()
r.Use(middleware.Cors())
// 注册路由
RegisterRouter(r)
if err := r.Run(fmt.Sprintf(":%s", c.Port)); err != nil {
log.Fatalln(err)
}
}
使用了middleware中的中间件cors来允许跨域请求
然后使用同一个包下的router.go中的RegisterRouter(r)来注册路由
最后r.Run运行项目
router.go
此文件中包含了一个 路由注册函数,用于在 Gin 框架中配置 HTTP 路由及其对应的处理逻辑。每个路由都绑定到特定的 URL 路径、HTTP 方法以及对应的中间件和控制器处理函数。
package main
import (
"admin_test_back/controller"
"admin_test_back/form"
"admin_test_back/middleware"
"admin_test_back/model"
"net/http"
"github.com/gin-gonic/gin"
)
func RegisterRouter(engine *gin.Engine) {
engine.POST("/register", middleware.Bind("user", &form.UserRegisterForm{}), controller.UserRegister)
engine.POST("/login", middleware.Bind("user", &form.UserLoginForm{}), controller.UserLogin)
engine.GET("/users/:id", middleware.Auth(), controller.UserGet)
engine.GET("/users/self", middleware.Auth(), controller.UserGetSelf)
engine.PUT("/users", middleware.Auth(), middleware.Bind("user", &model.User{}), controller.UserPut)
engine.GET("/users", middleware.Auth(), controller.UserListGet)
engine.DELETE("/users/:id", middleware.Auth(), controller.UserDelete)
}
函数签名
func RegisterRouter(engine *gin.Engine)
-
参数:
engine:Gin 的主路由引擎对象,用于注册路由。
-
功能:
- 将一系列路由规则添加到传入的 Gin 引擎中。
关键点
-
中间件的作用
middleware.Bind:负责数据绑定和验证。middleware.Auth:负责用户身份认证,确保只有合法用户可以访问受保护的路由。
-
控制器的分层
- 控制器函数负责处理具体的业务逻辑,与路由和中间件分离,符合分层设计原则。
-
RESTful 风格
- 路由设计符合 RESTful 风格,资源名称(如
/users)对应不同的 HTTP 方法,实现增删改查操作。
- 路由设计符合 RESTful 风格,资源名称(如
中间件
使用了三个中间件auth.go,bind.go,cors.go
auth.go
在auth.go中实现简单的JWT认证逻辑
package middleware
import (
"admin_test_back/common"
"strings"
"github.com/gin-gonic/gin"
)
func Auth() gin.HandlerFunc {
return func(c *gin.Context) {
// TODO: implement authentication logic
tokenString := c.GetHeader("Authorization")
if tokenString == "" {
c.AbortWithStatusJSON(401, gin.H{"error": "Unauthorized"})
return
}
if !strings.HasPrefix(tokenString, "Bearer ") {
c.AbortWithStatusJSON(401, gin.H{"error": "Unauthorized"})
return
}
tokenString = strings.TrimPrefix(tokenString, "Bearer ")
token, claims, err := common.ParaseToken(tokenString)
if err != nil || !token.Valid {
c.AbortWithStatusJSON(401, gin.H{"error": "Unauthorized"})
return
}
c.Set("id", claims.Id)
c.Next()
}
}
函数签名
func Auth() gin.HandlerFunc
- 返回值:返回一个
gin.HandlerFunc类型的函数,可用于 Gin 的中间件链。 - 用途:用于验证客户端请求中的
Authorization头部是否包含有效的 JWT(JSON Web Token)。
功能逻辑
-
获取
Authorization头部tokenString := c.GetHeader("Authorization") if tokenString == "" { c.AbortWithStatusJSON(401, gin.H{"error": "Unauthorized"}) return }- 从 HTTP 请求头中获取
Authorization字段。 - 如果没有找到,直接返回 401 Unauthorized 响应,阻止后续请求。
- 从 HTTP 请求头中获取
-
校验
Bearer前缀if !strings.HasPrefix(tokenString, "Bearer ") { c.AbortWithStatusJSON(401, gin.H{"error": "Unauthorized"}) return } tokenString = strings.TrimPrefix(tokenString, "Bearer ")- 检查
Authorization字段是否以Bearer开头。 - 如果没有正确的前缀,也返回 401 Unauthorized。
- 去掉前缀,提取实际的 JWT 字符串。
- 检查
-
解析和验证 JWT
token, claims, err := common.ParaseToken(tokenString) if err != nil || !token.Valid { c.AbortWithStatusJSON(401, gin.H{"error": "Unauthorized"}) return }- 调用
common.ParseToken(一个自定义函数解析JWT)解析 JWT,并验证其有效性。 - 如果解析失败或 token 无效,返回 401 Unauthorized。
- 调用
-
将用户信息存入上下文
c.Set("id", claims.Id) c.Next()- 从解析后的
claims(JWT 的负载)中提取Id,并将其存入 Gin 的上下文中,方便后续的处理函数使用。 - 调用
c.Next()继续执行后续的中间件和处理函数。
- 从解析后的
JWT 身份认证:确保只有持有有效 JWT 的请求可以访问受保护的资源。
用户信息传递:提取 JWT 中的用户 ID,并在后续处理中使用(例如查询数据库中的用户信息)。
bind.go
通用的 Bind 中间件函数,用于绑定和验证传入的请求参数,并在验证通过后将解析的结果存储到上下文中,供后续处理器函数使用。
函数签名
func Bind(key string, val any) gin.HandlerFunc
-
参数
key:字符串类型,用作在上下文中存储数据的键。val:任意类型,用于接收绑定和解析后的数据(通常是结构体指针)。
-
返回值
- 返回一个
gin.HandlerFunc,这是 Gin 中间件的标准类型。
- 返回一个
内部逻辑
-
绑定和验证参数
if err := c.ShouldBind(val); err != nil { c.JSON(400, gin.H{"error": err.Error()}) c.Abort() }- 调用
c.ShouldBind(val)尝试将请求参数绑定到val中(支持 JSON、表单、查询参数等多种格式)。 - 如果绑定失败,返回 400 错误,包含错误信息,并调用
c.Abort()中止后续的中间件和路由处理器函数。
- 调用
-
存储解析后的数据
c.Set(key, val)- 如果绑定成功,将
val以key为键存入上下文中,供后续处理函数使用。
- 如果绑定成功,将
-
继续后续处理
c.Next()- 调用
c.Next()继续执行后续的中间件或处理器函数。
- 调用
cors.go
实现 跨域资源共享(CORS: Cross-Origin Resource Sharing) ,它允许不同源的客户端访问服务器的资源。例如,一个运行在 http://example.com 的前端应用可以通过 CORS 请求访问 http://api.example.com 提供的 API。
CORS 背景
-
跨域问题:浏览器出于安全考虑,默认禁止来自不同源(域名、协议、端口)的请求访问。
- 同源策略限制了跨域请求。
-
CORS 是一种机制,通过设置特定的 HTTP 响应头,允许服务器控制哪些跨域请求是被允许的。
主要功能
-
允许跨域请求
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")Access-Control-Allow-Origin指定允许哪些来源访问资源。"*"表示允许所有来源跨域;可以改为特定域名(如"http://example.com")。
-
指定支持的 HTTP 方法
c.Header("Access-Control-Allow-Methods", "POST, GET, UPDATE, PUT, DELETE")Access-Control-Allow-Methods指定允许客户端使用的请求方法(如 GET、POST)。
-
指定允许的请求头
c.Header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization, X-TOKEN")Access-Control-Allow-Headers指定客户端可以发送的自定义头信息(如Authorization或X-TOKEN)。
-
允许客户端获取的响应头
c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Cache-Control, Content-Language, Content-Type")Access-Control-Expose-Headers指定客户端可以访问的响应头信息。
-
处理 OPTIONS 预检请求
if method == "OPTIONS" { c.AbortWithStatus(http.StatusNoContent) }-
预检请求(Preflight Request):
- 浏览器在发送实际请求前,会使用
OPTIONS方法发送一个预检请求。 - 预检请求用于检查服务器是否允许目标请求。
- 浏览器在发送实际请求前,会使用
-
-
继续执行后续中间件或逻辑
c.Next()- 在处理完跨域设置后,继续执行后续的中间件或路由逻辑。
CORS 头部总结
| Header Name | 作用 |
|---|---|
Access-Control-Allow-Origin | 指定允许的跨域来源 |
Access-Control-Allow-Methods | 指定允许的 HTTP 方法 |
Access-Control-Allow-Headers | 指定允许的自定义请求头 |
Access-Control-Expose-Headers | 指定允许客户端访问的响应头 |
Access-Control-Allow-Credentials | 是否允许发送认证信息(如 Cookie) |
实际用途
- 前后端分离项目:前端和后端通常运行在不同域上,CORS 解决跨域请求问题。
- 开放 API:允许外部系统通过跨域请求访问资源。
- 调试与开发:本地开发中,前端运行在
localhost上,访问后端接口需要跨域支持。
common
common文件夹里包含了jwt.go和utils.go,分别用于jwt用户鉴权以及对用户密码进行加密和验证
jwt.go
package common
import (
"strconv"
"time"
"github.com/dgrijalva/jwt-go"
)
var validTime time.Duration = (7 * 24) // hours
var jwtKey = []byte("WJDSBrewgs12093fads")
type Claims struct {
Id string
jwt.StandardClaims
}
// 使用user.ID获取token
func ReleaseToken(id uint) (string, error) {
idString := strconv.Itoa(int(id))
expirationTime := time.Now().Add(validTime * time.Hour)
claims := &Claims{
Id: idString,
StandardClaims: jwt.StandardClaims{
ExpiresAt: expirationTime.Unix(),
IssuedAt: time.Now().Unix(),
Issuer: "admin_test_back",
Subject: "user token",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString(jwtKey)
if err != nil {
return "", err
}
return tokenString, nil
}
func ParaseToken(tokenString string) (*jwt.Token, *Claims, error) {
claims := &Claims{}
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (i interface{}, err error) {
return jwtKey, err
})
return token, claims, err
}
ReleaseToken函数使用用户的id来获取token并返回token字符串
ParaseToken函数对传入的tokenString进行验证
utils.go
package common
import "golang.org/x/crypto/bcrypt"
// 加密
func CalculatePasswordHash(pwd string) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(pwd), bcrypt.DefaultCost)
if err != nil {
return "", err
}
return string(hash), nil
}
// 验证
func CheckPasswordHash(password, hash string) error {
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
}
CalculatePasswordHash函数对传入的字符串进行加密并返回加密后的字符串
CheckPasswordHash函数对传入的密码进行验证,看是否与加密后的密码一致
model
model文件夹中包含user.go文件,用于定义User的结构
package model
import "github.com/jinzhu/gorm"
type User struct {
gorm.Model
UserName string `json:"user_name"`
UserPassword string `json:"user_password"`
UserRealname string `json:"user_realname"`
UserPhone string `json:"user_phone"`
UserAge int `json:"user_age"`
UserSex int `json:"user_sex"`
UserRoleId int `json:"user_role_id"`
}
form
form文件夹中包含了userForm.go,用于定义前端返回的表单的结构,包括了用户注册的表单和用户登录的表单
package form
type UserRegisterForm struct {
UserName string `json:"user_name"`
UserPassword string `json:"user_password"`
UserRealname string `json:"user_realname"`
UserPhone string `json:"user_phone"`
UserAge int `json:"user_age"`
UserSex int `json:"user_sex"`
}
type UserLoginForm struct {
UserName string `json:"user_name"`
UserPassword string `json:"user_password"`
}
userDB.go
db文件夹下面的userDB.go用于对数据库进行操作,包括了CreateOne,FindOneByName,FindOneById,UpdateOne,GetUserList,DeleteOne等功能。
package db
import (
"admin_test_back/model"
)
type UserDB struct{}
func (u *UserDB) CreateOne(user *model.User) error {
if err := DB.Create(user).Error; err != nil {
return err
}
return nil
}
func (u *UserDB) FindOneByName(userName string) (*model.User, error) {
user := &model.User{}
if err := DB.Model(&model.User{}).Where("user_name = ?", userName).Find(user).Error; err != nil {
return nil, err
}
return user, nil
}
func (u *UserDB) FindOneById(id uint) (*model.User, error) {
user := &model.User{}
if err := DB.Model(&model.User{}).Where("id = ?", id).Find(user).Error; err != nil {
return nil, err
}
return user, nil
}
func (u *UserDB) UpdateOne(user *model.User) error {
if err := DB.Model(&user).Save(&user).Error; err != nil {
return err
}
// if err := DB.Model(&user).Updates(user).Error; err != nil {
// return err
// }
return nil
}
func (u *UserDB) GetUserList() (*[]model.User, error) {
var users []model.User
if err := DB.Model(&model.User{}).Find(&users).Error; err != nil {
return nil, err
}
return &users, nil
}
func (u *UserDB) DeleteOne(id uint) error {
if err := DB.Model(&model.User{}).Delete(&model.User{}, id).Error; err != nil {
return err
}
return nil
}
CreateOne对传入的user结构体进行操作,将它添加进数据库中
FindOneByName将传入的userName在数据库中进行查找,返回对应的用户user
FindOneById将传入的id在数据库中进行查找,返回对应的user
UpdateOne对传入的user结构体进行操作,将它在数据库中的信息进行修改
GetUserList将数据库中的所有数据放入传入的结构体参数中
DeleteOne根据传入的id查找数据库中对应的数据并删除
controller
controller文件夹中的userController.go包含了各种控制器函数,负责处理具体的业务逻辑
package controller
import (
"admin_test_back/common"
"admin_test_back/db"
"admin_test_back/form"
"admin_test_back/model"
"fmt"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
func UserRegister(c *gin.Context) {
//获取表单中的数据,存入userForm中
userForm := c.MustGet("user").(*form.UserRegisterForm)
//判断用户是否已存在,通过账号判断
_, err := new(db.UserDB).FindOneByName(userForm.UserName)
if err == nil {
c.JSON(http.StatusConflict, gin.H{
"message": "用户名已存在",
})
return
}
//将userForm存入数据库,需要创建model.User实例
//加密密码
hashPwd, err := common.CalculatePasswordHash(userForm.UserPassword)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"message": "注册失败",
})
return
}
user := &model.User{
UserName: userForm.UserName,
UserPassword: hashPwd,
UserRealname: userForm.UserRealname,
UserPhone: userForm.UserPhone,
UserAge: userForm.UserAge,
UserSex: userForm.UserSex,
UserRoleId: 0,
}
//存入数据库
if err := new(db.UserDB).CreateOne(user); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"message": "注册失败",
})
return
}
token, err := common.ReleaseToken(user.ID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"message": "注册成功但登录失败",
})
return
}
c.JSON(http.StatusOK, gin.H{
"token": token,
})
}
func UserLogin(c *gin.Context) {
//获取表单中的数据,存入userForm中
userForm := c.MustGet("user").(*form.UserLoginForm)
//判断用户是否存在
user, err := new(db.UserDB).FindOneByName(userForm.UserName)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"message": "用户名或密码错误",
})
return
}
//判断密码是否正确
err = common.CheckPasswordHash(userForm.UserPassword, user.UserPassword)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"message": "用户名或密码错误",
})
return
}
token, err := common.ReleaseToken(user.ID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"message": "登录失败",
})
return
}
// //获取用户角色
// role := ""
// if user.UserRoleId == 0 {
// role = "user"
// } else {
// role = "admin"
// }
c.JSON(http.StatusOK, gin.H{
"token": token,
"user": user,
})
}
func userGet(c *gin.Context, idStr string) {
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"message": "参数错误",
})
return
}
user, err := new(db.UserDB).FindOneById(uint(id))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"message": "获取用户信息失败",
})
return
}
c.JSON(http.StatusOK, gin.H{
"user": user,
})
}
func UserGet(c *gin.Context) {
idStr := c.Param("id")
userGet(c, idStr)
}
func UserGetSelf(c *gin.Context) {
idStr := c.MustGet("id").(string)
userGet(c, idStr)
}
func UserPut(c *gin.Context) {
userForm := c.MustGet("user").(*model.User)
user, err := new(db.UserDB).FindOneById(userForm.ID)
fmt.Println(userForm.ID, userForm.UserName, userForm.UserRealname, userForm.UserPhone, userForm.UserAge, userForm.UserSex, userForm.UserRoleId, userForm.CreatedAt, userForm.UpdatedAt)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"message": "获取用户信息失败",
})
return
}
if err := new(db.UserDB).UpdateOne(userForm); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"message": "更新用户信息失败",
})
return
}
fmt.Println(user)
c.JSON(http.StatusOK, gin.H{
"message": "更新用户信息成功",
})
}
func UserListGet(c *gin.Context) {
users, err := new(db.UserDB).GetUserList()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"message": "获取用户列表失败",
})
return
}
c.JSON(http.StatusOK, gin.H{
"users": users,
"message": "获取用户列表成功",
})
}
func UserDelete(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"message": "参数错误",
})
return
}
user, err := new(db.UserDB).FindOneById(uint(id))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"message": "获取用户信息失败",
})
return
}
if err = new(db.UserDB).DeleteOne(user.ID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"message": "删除用户失败",
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "删除用户成功",
})
}
UserRegister函数获取表单中的数据,赋值到userForm中然后存入数据库进行用户注册。首先会查询数据库信息,判断用户是否已经存在,如果不存在首先会将密码进行加密,然后会将此数据存入数据库,最后返回一个token给前端。
UserLogin获取表单中的数据,赋值到userForm中,首先会查找数据库,判断用户是否存在,接着判断密码是否正确,最后返回一个token以及用户结构体给前端。
userGet会根据传入的id在数据库中查找此用户是否存在,如果存在最后会将此用户结构体返回给前端。
UserGet会获取url中传入的参数,即?之后的内容,然后调用userGet函数获取用户信息。
UserGetSelf会获取上下文中的id,调用userGet函数获取用户信息,此函数是实现返回当前用户的信息,会在url中没有参数传入的时候调用。
UserPut会从上下文中获取user结构体,然后在数据库中根据id查找是否存在此用户,如果存在则会将此结构体传入数据库进行用户信息修改。
UserListGet会获取数据库中所有用户数据,然后返回给前端。
UserDelete会根据url中传入的id参数在数据库中查找,如果找到了用户,则将此用户删除并返回删除用户成功的信息给前端。
总结
完成后端开发后,可以使用swagger进行接口文档编写和测试,也可以自己使用vue3进行前端页面设计并与后端进行交互。本项目使用到了Gin框架以及Gorm的使用,使用MySQL进行数据的存储,同时也用到了JWT进行鉴权和权限管理。