Gin-Vue-Admin启动服务流程
发表于:2023-12-27 | 分类: Golang
字数统计: 7.6k | 阅读时长: 37分钟 | 阅读量:

2023/12/22版本

Server

看到项目结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
├── api
│   └── v1
├── config
├── core
├── docs
├── global
├── initialize
│   └── internal
├── middleware
├── model
│   ├── request
│   └── response
├── packfile
├── resource
│   ├── excel
│   ├── page
│   └── template
├── router
├── service
├── source
└── utils
├── timer
└── upload

README.md中有解释文件夹的作用。

文件夹 说明 描述
api api层 api层
--v1 v1版本接口 v1版本接口
config 配置包 config.yaml对应的配置结构体
core 核心文件 核心组件(zap, viper, server)的初始化
docs swagger文档目录 swagger文档目录
global 全局对象 全局对象
initialize 初始化 router,redis,gorm,validator, timer的初始化
--internal 初始化内部函数 gorm 的 longger 自定义,在此文件夹的函数只能由 initialize 层进行调用
middleware 中间件层 用于存放 gin 中间件代码
model 模型层 模型对应数据表
--request 入参结构体 接收前端发送到后端的数据。
--response 出参结构体 返回给前端的数据结构体
packfile 静态文件打包 静态文件打包
resource 静态资源文件夹 负责存放静态文件
--excel excel导入导出默认路径 excel导入导出默认路径
--page 表单生成器 表单生成器 打包后的dist
--template 模板 模板文件夹,存放的是代码生成器的模板
router 路由层 路由层
service service层 存放业务逻辑问题
source source层 存放初始化数据的函数
utils 工具包 工具函数封装
--timer timer 定时器接口封装
--upload oss oss接口封装

找到main.go文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {
global.GVA_VP = core.Viper() // 初始化Viper
initialize.OtherInit()
global.GVA_LOG = core.Zap() // 初始化zap日志库
zap.ReplaceGlobals(global.GVA_LOG)
global.GVA_DB = initialize.Gorm() // gorm连接数据库
initialize.Timer()
initialize.DBList()
if global.GVA_DB != nil {
initialize.RegisterTables() // 初始化表
// 程序结束前关闭数据库链接
db, _ := global.GVA_DB.DB()
defer db.Close()
}
core.RunWindowsServer()
}

global

看一下全局对象server/global/global.go,在main函数中初始化了全局对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package global

var (
GVA_DB *gorm.DB
GVA_DBList map[string]*gorm.DB
GVA_REDIS *redis.Client
GVA_MONGO *qmgo.QmgoClient
GVA_CONFIG config.Server
GVA_VP *viper.Viper
// GVA_LOG *oplogging.Logger
GVA_LOG *zap.Logger
GVA_Timer timer.Timer = timer.NewTimerTask()
GVA_Concurrency_Control = &singleflight.Group{}

BlackCache local_cache.Cache
lock sync.RWMutex
)

用var声明了多个变量

  • 只有lock是私有的。
  • 其他的首字母大写,都是公共变量。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// GetGlobalDBByDBName 通过名称获取db list中的db
func GetGlobalDBByDBName(dbname string) *gorm.DB {
lock.RLock()
defer lock.RUnlock()
return GVA_DBList[dbname]
}

// MustGetGlobalDBByDBName 通过名称获取db 如果不存在则panic
func MustGetGlobalDBByDBName(dbname string) *gorm.DB {
lock.RLock()
defer lock.RUnlock()
db, ok := GVA_DBList[dbname]
if !ok || db == nil {
panic("db no init")
}
return db
}

Viper

看到第一个,初始化Viper,点进去,server/core/viper.go

Viper的使用可以看一下文档viper 中文文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package core

// Viper //
// 优先级: 命令行 > 环境变量 > 默认值
// Author [SliverHorn](https://github.com/SliverHorn)
func Viper(path ...string) *viper.Viper {
var config string

if len(path) == 0 {
flag.StringVar(&config, "c", "", "choose config file.")
flag.Parse()
if config == "" { // 判断命令行参数是否为空
if configEnv := os.Getenv(internal.ConfigEnv); configEnv == "" { // 判断 internal.ConfigEnv 常量存储的环境变量是否为空
switch gin.Mode() {
case gin.DebugMode:
config = internal.ConfigDefaultFile
fmt.Printf("您正在使用gin模式的%s环境名称,config的路径为%s\n", gin.EnvGinMode, internal.ConfigDefaultFile)
case gin.ReleaseMode:
config = internal.ConfigReleaseFile
fmt.Printf("您正在使用gin模式的%s环境名称,config的路径为%s\n", gin.EnvGinMode, internal.ConfigReleaseFile)
case gin.TestMode:
config = internal.ConfigTestFile
fmt.Printf("您正在使用gin模式的%s环境名称,config的路径为%s\n", gin.EnvGinMode, internal.ConfigTestFile)
}
} else { // internal.ConfigEnv 常量存储的环境变量不为空 将值赋值于config
config = configEnv
fmt.Printf("您正在使用%s环境变量,config的路径为%s\n", internal.ConfigEnv, config)
}
} else { // 命令行参数不为空 将值赋值于config
fmt.Printf("您正在使用命令行的-c参数传递的值,config的路径为%s\n", config)
}
} else { // 函数传递的可变参数的第一个值赋值于config
config = path[0]
fmt.Printf("您正在使用func Viper()传递的值,config的路径为%s\n", config)
}

v := viper.New()
v.SetConfigFile(config)
v.SetConfigType("yaml")
err := v.ReadInConfig()
if err != nil {
panic(fmt.Errorf("Fatal error config file: %s \n", err))
}
v.WatchConfig()

v.OnConfigChange(func(e fsnotify.Event) {
fmt.Println("config file changed:", e.Name)
if err = v.Unmarshal(&global.GVA_CONFIG); err != nil {
fmt.Println(err)
}
})
if err = v.Unmarshal(&global.GVA_CONFIG); err != nil {
panic(err)
}

// root 适配性 根据root位置去找到对应迁移位置,保证root路径有效
global.GVA_CONFIG.AutoCode.Root, _ = filepath.Abs("..")
return v
}

拆分成两部分,先看config变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
var config string

if len(path) == 0 {
flag.StringVar(&config, "c", "", "choose config file.")
flag.Parse()
if config == "" { // 判断命令行参数是否为空
if configEnv := os.Getenv(internal.ConfigEnv); configEnv == "" { // 判断 internal.ConfigEnv 常量存储的环境变量是否为空
switch gin.Mode() {
case gin.DebugMode:
config = internal.ConfigDefaultFile
fmt.Printf("您正在使用gin模式的%s环境名称,config的路径为%s\n", gin.EnvGinMode, internal.ConfigDefaultFile)
case gin.ReleaseMode:
config = internal.ConfigReleaseFile
fmt.Printf("您正在使用gin模式的%s环境名称,config的路径为%s\n", gin.EnvGinMode, internal.ConfigReleaseFile)
case gin.TestMode:
config = internal.ConfigTestFile
fmt.Printf("您正在使用gin模式的%s环境名称,config的路径为%s\n", gin.EnvGinMode, internal.ConfigTestFile)
}
} else { // internal.ConfigEnv 常量存储的环境变量不为空 将值赋值于config
config = configEnv
fmt.Printf("您正在使用%s环境变量,config的路径为%s\n", internal.ConfigEnv, config)
}
} else { // 命令行参数不为空 将值赋值于config
fmt.Printf("您正在使用命令行的-c参数传递的值,config的路径为%s\n", config)
}
} else { // 函数传递的可变参数的第一个值赋值于config
config = path[0]
fmt.Printf("您正在使用func Viper()传递的值,config的路径为%s\n", config)
}
  • 优先级: 函数参数 > 命令行 > 环境变量 > 默认值
  • 函数传递的可变参数的第一个值赋值于config
  • 命令行参数不为空 将值赋值于config

看到gin.Mode()函数,点击跳转,进入github.com\gin-gonic\gin@v1.9.1\mode.go中。

1
2
3
func Mode() string {
return modeName
}

接着点击modeName跳转

1
2
3
4
var (
ginMode = debugCode
modeName = DebugMode
)

接着点击debugCode跳转

1
2
3
4
5
6
7
8
const (
// DebugMode indicates gin mode is debug.
DebugMode = "debug"
// ReleaseMode indicates gin mode is release.
ReleaseMode = "release"
// TestMode indicates gin mode is test.
TestMode = "test"
)

可以看到返回"debug"字符串。

所以switch分支中,进入case gin.DebugMode:分支,点击internal.*ConfigDefaultFile*跳转

1
2
3
4
5
6
7
const (
ConfigEnv = "GVA_CONFIG"
ConfigDefaultFile = "config.yaml"
ConfigTestFile = "config.test.yaml"
ConfigDebugFile = "config.debug.yaml"
ConfigReleaseFile = "config.release.yaml"
)

故最终config的值是"config.yaml"


上半部分看完了,看下半部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
v := viper.New()
v.SetConfigFile(config)
v.SetConfigType("yaml")
err := v.ReadInConfig()
if err != nil {
panic(fmt.Errorf("Fatal error config file: %s \n", err))
}

v.WatchConfig()
v.OnConfigChange(func(e fsnotify.Event) {
fmt.Println("config file changed:", e.Name)
if err = v.Unmarshal(&global.GVA_CONFIG); err != nil {
fmt.Println(err)
}
})
  • 设置路径为config,即"config.yaml"
  • 设置文件格式yaml。
  • 读取配置。
  • 监听和重新读取配置文件(Viper 支持在运行时让应用程序实时读取配置文件)。
  • v.OnConfigChange提供当每次发生更改时运行的函数。
1
2
3
if err = v.Unmarshal(&global.GVA_CONFIG); err != nil {
panic(err)
}

将yaml文件反序列化成struct,存到GVA_CONFIG中,该变量是config.Server类型,跳转进去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package config

type Server struct {
JWT JWT `mapstructure:"jwt" json:"jwt" yaml:"jwt"`
Zap Zap `mapstructure:"zap" json:"zap" yaml:"zap"`
Redis Redis `mapstructure:"redis" json:"redis" yaml:"redis"`
Mongo Mongo `json:"mongo" yaml:"mongo" mapstructure:"mongo"`
Email Email `mapstructure:"email" json:"email" yaml:"email"`
System System `mapstructure:"system" json:"system" yaml:"system"`
Captcha Captcha `mapstructure:"captcha" json:"captcha" yaml:"captcha"`
// auto
AutoCode Autocode `mapstructure:"autocode" json:"autocode" yaml:"autocode"`
// gorm
Mysql Mysql `mapstructure:"mysql" json:"mysql" yaml:"mysql"`
Mssql Mssql `mapstructure:"mssql" json:"mssql" yaml:"mssql"`
Pgsql Pgsql `mapstructure:"pgsql" json:"pgsql" yaml:"pgsql"`
Oracle Oracle `mapstructure:"oracle" json:"oracle" yaml:"oracle"`
Sqlite Sqlite `mapstructure:"sqlite" json:"sqlite" yaml:"sqlite"`
DBList []SpecializedDB `mapstructure:"db-list" json:"db-list" yaml:"db-list"`
// oss
Local Local `mapstructure:"local" json:"local" yaml:"local"`
Qiniu Qiniu `mapstructure:"qiniu" json:"qiniu" yaml:"qiniu"`
AliyunOSS AliyunOSS `mapstructure:"aliyun-oss" json:"aliyun-oss" yaml:"aliyun-oss"`
HuaWeiObs HuaWeiObs `mapstructure:"hua-wei-obs" json:"hua-wei-obs" yaml:"hua-wei-obs"`
TencentCOS TencentCOS `mapstructure:"tencent-cos" json:"tencent-cos" yaml:"tencent-cos"`
AwsS3 AwsS3 `mapstructure:"aws-s3" json:"aws-s3" yaml:"aws-s3"`

Excel Excel `mapstructure:"excel" json:"excel" yaml:"excel"`

// 跨域配置
Cors CORS `mapstructure:"cors" json:"cors" yaml:"cors"`
}

这里就看第一个类JWT,其他的类可以自行查看。

1
2
3
4
5
6
7
8
package config

type JWT struct {
SigningKey string `mapstructure:"signing-key" json:"signing-key" yaml:"signing-key"` // jwt签名
ExpiresTime string `mapstructure:"expires-time" json:"expires-time" yaml:"expires-time"` // 过期时间
BufferTime string `mapstructure:"buffer-time" json:"buffer-time" yaml:"buffer-time"` // 缓冲时间
Issuer string `mapstructure:"issuer" json:"issuer" yaml:"issuer"` // 签发者
}

跟配置文件对应

1
2
3
4
5
jwt:
signing-key: c3dff46d-7b72-45bc-a5c7-09166ccc7676
expires-time: 7d
buffer-time: 1d
issuer: qmPlus

看到这里,用Viper读取配置文件就完了,接着来看下一个


Other

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package initialize

func OtherInit() {
dr, err := utils.ParseDuration(global.GVA_CONFIG.JWT.ExpiresTime)
if err != nil {
panic(err)
}
_, err = utils.ParseDuration(global.GVA_CONFIG.JWT.BufferTime)
if err != nil {
panic(err)
}

global.BlackCache = local_cache.NewCache(
local_cache.SetDefaultExpire(dr),
)
}
  • 这是一个用Go语言编写的函数,用于初始化一些全局变量和缓存。函数首先使用utils.ParseDuration()解析表示JWT令牌有效时间的字符串,并将其转换为time.Duration类型。如果解析失败,函数会抛出一个错误。
  • 然后,它尝试使用utils.ParseDuration()解析表示JWT缓冲时间的字符串,并将其转换为time.Duration类型。如果解析失败,函数会抛出一个错误。
  • 接下来,函数创建一个local_cache.Cache实例,并设置默认的过期时间。最后,函数将创建的缓存实例赋值给全局变量global.BlackCache

Zap

main.go

1
2
global.GVA_LOG = core.Zap() // 初始化zap日志库
zap.ReplaceGlobals(global.GVA_LOG)

使用zap.ReplaceGlobals(global.GVA_LOG)将全局变量zap.Logger替换为global.GVA_LOG,以便在程序中使用。这使得在程序的任何地方都可以方便地使用global.GVA_LOG来记录日志。


点进去看core.Zap()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Zap 获取 zap.Logger
// Author [SliverHorn](https://github.com/SliverHorn)
func Zap() (logger *zap.Logger) {
if ok, _ := utils.PathExists(global.GVA_CONFIG.Zap.Director); !ok { // 判断是否有Director文件夹
fmt.Printf("create %v directory\n", global.GVA_CONFIG.Zap.Director)
_ = os.Mkdir(global.GVA_CONFIG.Zap.Director, os.ModePerm)
}

cores := internal.Zap.GetZapCores()
logger = zap.New(zapcore.NewTee(cores...))

if global.GVA_CONFIG.Zap.ShowLine {
logger = logger.WithOptions(zap.AddCaller())
}
return logger
}

Zap Logger · Go语言中文文档 (topgoer.com)

  • 函数首先检查global.GVA_CONFIG.Zap.Director目录是否存在,如果不存在,则创建该目录。
  • 接下来,函数使用internal.Zap.GetZapCores()获取Zap的核心,并使用这些核心创建一个新的zap.Logger实例。
  • 如果global.GVA_CONFIG.Zap.ShowLine设置为true,函数将添加调用者信息选项,以便在日志中显示调用者函数名和行号。

gorm

1
2
3
4
5
6
7
8
9
global.GVA_DB = initialize.Gorm() // gorm连接数据库
initialize.Timer()
initialize.DBList()
if global.GVA_DB != nil {
initialize.RegisterTables() // 初始化表
// 程序结束前关闭数据库链接
db, _ := global.GVA_DB.DB()
defer db.Close()
}

gorm init

先看initialize.Gorm()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func Gorm() *gorm.DB {
switch global.GVA_CONFIG.System.DbType {
case "mysql":
return GormMysql()
case "pgsql":
return GormPgSql()
case "oracle":
return GormOracle()
case "mssql":
return GormMssql()
case "sqlite":
return GormSqlite()
default:
return GormMysql()
}
}

根据全局变量global.GVA_CONFIG.System.DbType的值来选择不同的数据库类型,并返回相应的gorm.DB实例。

这里就看MySQL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func GormMysql() *gorm.DB {
m := global.GVA_CONFIG.Mysql
if m.Dbname == "" {
return nil
}
mysqlConfig := mysql.Config{
DSN: m.Dsn(), // DSN data source name
DefaultStringSize: 191, // string 类型字段的默认长度
SkipInitializeWithVersion: false, // 根据版本自动配置
}
if db, err := gorm.Open(mysql.New(mysqlConfig), internal.Gorm.Config(m.Prefix, m.Singular)); err != nil {
return nil
} else {
db.InstanceSet("gorm:table_options", "ENGINE="+m.Engine)
sqlDB, _ := db.DB()
sqlDB.SetMaxIdleConns(m.MaxIdleConns)
sqlDB.SetMaxOpenConns(m.MaxOpenConns)
return db
}
}
  1. 首先,它从全局配置中读取MySQL配置。
  2. 如果MySQL的数据库名称(dbname)为空,则返回nil。
  3. 创建一个mysql.Config结构体,其中包含MySQL的DSN(数据源名称)和其他设置。
  4. 使用gorm.Open函数连接到MySQL数据库,并使用mysql.New(mysqlConfig)创建一个新的MySQL实例。
  5. 如果出现错误,则返回nil。
  6. 否则,设置GORM的配置,例如表前缀和单数形式。
  7. 设置SQL数据库的最大空闲连接数和最大打开连接数。
  8. 返回GORM数据库对象。

11行中自定义gorm配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// Config gorm 自定义配置
// Author [SliverHorn](https://github.com/SliverHorn)
func (g *_gorm) Config(prefix string, singular bool) *gorm.Config {
config := &gorm.Config{
NamingStrategy: schema.NamingStrategy{
TablePrefix: prefix,
SingularTable: singular,
},
DisableForeignKeyConstraintWhenMigrating: true,
}
_default := logger.New(log.New(os.Stdout, "\r\n", log.LstdFlags), logger.Config{
SlowThreshold: 200 * time.Millisecond,
LogLevel: logger.Warn,
Colorful: true,
})
var logMode DBBASE
switch global.GVA_CONFIG.System.DbType {
case "mysql":
logMode = &global.GVA_CONFIG.Mysql
case "pgsql":
logMode = &global.GVA_CONFIG.Pgsql
case "oracle":
logMode = &global.GVA_CONFIG.Oracle
default:
logMode = &global.GVA_CONFIG.Mysql
}

switch logMode.GetLogMode() {
case "silent", "Silent":
config.Logger = _default.LogMode(logger.Silent)
case "error", "Error":
config.Logger = _default.LogMode(logger.Error)
case "warn", "Warn":
config.Logger = _default.LogMode(logger.Warn)
case "info", "Info":
config.Logger = _default.LogMode(logger.Info)
default:
config.Logger = _default.LogMode(logger.Info)
}
return config
}

initialize

接着是两个initializeinitialize.Timer()initialize.DBList()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func Timer() {
go func() {
var option []cron.Option
option = append(option, cron.WithSeconds())
// 清理DB定时任务
_, err := global.GVA_Timer.AddTaskByFunc("ClearDB", "@daily", func() {
err := task.ClearTable(global.GVA_DB) // 定时任务方法定在task文件包中
if err != nil {
fmt.Println("timer error:", err)
}
}, "定时清理数据库【日志,黑名单】内容", option...)
if err != nil {
fmt.Println("add timer error:", err)
}
}()
}

开启一个Goroutine来定时清理DB。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const sys = "system"

func DBList() {
dbMap := make(map[string]*gorm.DB)
for _, info := range global.GVA_CONFIG.DBList {
if info.Disable {
continue
}
switch info.Type {
case "mysql":
dbMap[info.AliasName] = GormMysqlByConfig(config.Mysql{GeneralDB: info.GeneralDB})
case "mssql":
dbMap[info.AliasName] = GormMssqlByConfig(config.Mssql{GeneralDB: info.GeneralDB})
case "pgsql":
dbMap[info.AliasName] = GormPgSqlByConfig(config.Pgsql{GeneralDB: info.GeneralDB})
case "oracle":
dbMap[info.AliasName] = GormOracleByConfig(config.Oracle{GeneralDB: info.GeneralDB})
default:
continue
}
}
// 做特殊判断,是否有迁移
// 适配低版本迁移多数据库版本
if sysDB, ok := dbMap[sys]; ok {
global.GVA_DB = sysDB
}
global.GVA_DBList = dbMap
}

初始化多个DB


初始化表

1
2
3
4
5
6
if global.GVA_DB != nil {
initialize.RegisterTables() // 初始化表
// 程序结束前关闭数据库链接
db, _ := global.GVA_DB.DB()
defer db.Close()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
func RegisterTables() {
db := global.GVA_DB
err := db.AutoMigrate(
// 系统模块表
system.SysApi{},
system.SysUser{},
system.SysBaseMenu{},
system.JwtBlacklist{},
system.SysAuthority{},
system.SysDictionary{},
system.SysOperationRecord{},
system.SysAutoCodeHistory{},
system.SysDictionaryDetail{},
system.SysBaseMenuParameter{},
system.SysBaseMenuBtn{},
system.SysAuthorityBtn{},
system.SysAutoCode{},
system.SysChatGptOption{},

example.ExaFile{},
example.ExaCustomer{},
example.ExaFileChunk{},
example.ExaFileUploadAndDownload{},
)
if err != nil {
global.GVA_LOG.Error("register table failed", zap.Error(err))
os.Exit(0)
}
global.GVA_LOG.Info("register table success")
}

初始化系统模块的数据表。

RunServer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
func RunWindowsServer() {
if global.GVA_CONFIG.System.UseMultipoint || global.GVA_CONFIG.System.UseRedis {
// 初始化redis服务
initialize.Redis()
}
if global.GVA_CONFIG.System.UseMongo {
err := initialize.Mongo.Initialization()
if err != nil {
zap.L().Error(fmt.Sprintf("%+v", err))
}
}
// 从db加载jwt数据
if global.GVA_DB != nil {
system.LoadAll()
}

Router := initialize.Routers()
Router.Static("/form-generator", "./resource/page")

address := fmt.Sprintf(":%d", global.GVA_CONFIG.System.Addr)
s := initServer(address, Router)
// 保证文本顺序输出
// In order to ensure that the text order output can be deleted
time.Sleep(10 * time.Microsecond)
global.GVA_LOG.Info("server run success on ", zap.String("address", address))

fmt.Printf(`
欢迎使用 gin-vue-admin
当前版本:v2.5.7
加群方式:微信号:shouzi_1994 QQ群:622360840
插件市场:https://plugin.gin-vue-admin.com
GVA讨论社区:https://support.qq.com/products/371961
默认自动化文档地址:http://127.0.0.1%s/swagger/index.html
默认前端文件运行地址:http://127.0.0.1:8080
如果项目让您获得了收益,希望您能请团队喝杯可乐:https://www.gin-vue-admin.com/coffee/index.html
`, address)
global.GVA_LOG.Error(s.ListenAndServe().Error())
}

初始化Redis、Mongo

1
2
3
4
5
6
7
8
9
10
if global.GVA_CONFIG.System.UseMultipoint || global.GVA_CONFIG.System.UseRedis {
// 初始化redis服务
initialize.Redis()
}
if global.GVA_CONFIG.System.UseMongo {
err := initialize.Mongo.Initialization()
if err != nil {
zap.L().Error(fmt.Sprintf("%+v", err))
}
}

读取配置文件

  1. 初始化Redis
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func Redis() {
redisCfg := global.GVA_CONFIG.Redis
client := redis.NewClient(&redis.Options{
Addr: redisCfg.Addr,
Password: redisCfg.Password, // no password set
DB: redisCfg.DB, // use default DB
})
pong, err := client.Ping(context.Background()).Result()
if err != nil {
global.GVA_LOG.Error("redis connect ping failed, err:", zap.Error(err))
panic(err)
} else {
global.GVA_LOG.Info("redis connect ping response:", zap.String("pong", pong))
global.GVA_REDIS = client
}
}
  1. 初始化Mongo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func (m *mongo) Initialization() error {
var opts []options.ClientOptions
if global.GVA_CONFIG.Mongo.IsZap {
opts = internal.Mongo.GetClientOptions()
}
ctx := context.Background()
client, err := qmgo.Open(ctx, &qmgo.Config{
Uri: global.GVA_CONFIG.Mongo.Uri(),
Coll: global.GVA_CONFIG.Mongo.Coll,
Database: global.GVA_CONFIG.Mongo.Database,
MinPoolSize: &global.GVA_CONFIG.Mongo.MinPoolSize,
MaxPoolSize: &global.GVA_CONFIG.Mongo.MaxPoolSize,
SocketTimeoutMS: &global.GVA_CONFIG.Mongo.SocketTimeoutMs,
ConnectTimeoutMS: &global.GVA_CONFIG.Mongo.ConnectTimeoutMs,
Auth: &qmgo.Credential{
Username: global.GVA_CONFIG.Mongo.Username,
Password: global.GVA_CONFIG.Mongo.Password,
},
}, opts...)
if err != nil {
return errors.Wrap(err, "链接mongodb数据库失败!")
}
global.GVA_MONGO = client
err = m.Indexes(ctx)
if err != nil {
return err
}
return nil
}

JWT黑名单

1
2
3
if global.GVA_DB != nil {
system.LoadAll()
}
1
2
3
4
5
6
7
8
9
10
11
func LoadAll() {
var data []string
err := global.GVA_DB.Model(&system.JwtBlacklist{}).Select("jwt").Find(&data).Error
if err != nil {
global.GVA_LOG.Error("加载数据库jwt黑名单失败!", zap.Error(err))
return
}
for i := 0; i < len(data); i++ {
global.BlackCache.SetDefault(data[i], struct{}{})
} // jwt黑名单 加入 BlackCache 中
}

从数据库中加载jwt黑名单。

初始化路由

1
2
Router := initialize.Routers()
Router.Static("/form-generator", "./resource/page")

点进去Routers()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
func Routers() *gin.Engine {

// 设置为发布模式
if global.GVA_CONFIG.System.Env == "public" {
gin.SetMode(gin.ReleaseMode) //DebugMode ReleaseMode TestMode
}

Router := gin.New()

if global.GVA_CONFIG.System.Env != "public" {
Router.Use(gin.Logger(), gin.Recovery())
}

InstallPlugin(Router) // 安装插件
systemRouter := router.RouterGroupApp.System
exampleRouter := router.RouterGroupApp.Example
// 如果想要不使用nginx代理前端网页,可以修改 web/.env.production 下的
// VUE_APP_BASE_API = /
// VUE_APP_BASE_PATH = http://localhost
// 然后执行打包命令 npm run build。在打开下面3行注释
// Router.Static("/favicon.ico", "./dist/favicon.ico")
// Router.Static("/assets", "./dist/assets") // dist里面的静态资源
// Router.StaticFile("/", "./dist/index.html") // 前端网页入口页面

Router.StaticFS(global.GVA_CONFIG.Local.StorePath, http.Dir(global.GVA_CONFIG.Local.StorePath)) // 为用户头像和文件提供静态地址
// Router.Use(middleware.LoadTls()) // 如果需要使用https 请打开此中间件 然后前往 core/server.go 将启动模式 更变为 Router.RunTLS("端口","你的cre/pem文件","你的key文件")
// 跨域,如需跨域可以打开下面的注释
// Router.Use(middleware.Cors()) // 直接放行全部跨域请求
// Router.Use(middleware.CorsByRules()) // 按照配置的规则放行跨域请求
//global.GVA_LOG.Info("use middleware cors")
docs.SwaggerInfo.BasePath = global.GVA_CONFIG.System.RouterPrefix
Router.GET(global.GVA_CONFIG.System.RouterPrefix+"/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
global.GVA_LOG.Info("register swagger handler")
// 方便统一添加路由组前缀 多服务器上线使用

PublicGroup := Router.Group(global.GVA_CONFIG.System.RouterPrefix)
{
// 健康监测
PublicGroup.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, "ok")
})
}
{
systemRouter.InitBaseRouter(PublicGroup) // 注册基础功能路由 不做鉴权
systemRouter.InitInitRouter(PublicGroup) // 自动初始化相关
}
PrivateGroup := Router.Group(global.GVA_CONFIG.System.RouterPrefix)
PrivateGroup.Use(middleware.JWTAuth()).Use(middleware.CasbinHandler())
{
systemRouter.InitApiRouter(PrivateGroup, PublicGroup) // 注册功能api路由
systemRouter.InitJwtRouter(PrivateGroup) // jwt相关路由
systemRouter.InitUserRouter(PrivateGroup) // 注册用户路由
systemRouter.InitMenuRouter(PrivateGroup) // 注册menu路由
systemRouter.InitSystemRouter(PrivateGroup) // system相关路由
systemRouter.InitCasbinRouter(PrivateGroup) // 权限相关路由
systemRouter.InitAutoCodeRouter(PrivateGroup) // 创建自动化代码
systemRouter.InitAuthorityRouter(PrivateGroup) // 注册角色路由
systemRouter.InitSysDictionaryRouter(PrivateGroup) // 字典管理
systemRouter.InitAutoCodeHistoryRouter(PrivateGroup) // 自动化代码历史
systemRouter.InitSysOperationRecordRouter(PrivateGroup) // 操作记录
systemRouter.InitSysDictionaryDetailRouter(PrivateGroup) // 字典详情管理
systemRouter.InitAuthorityBtnRouterRouter(PrivateGroup) // 字典详情管理
systemRouter.InitChatGptRouter(PrivateGroup) // chatGpt接口

exampleRouter.InitCustomerRouter(PrivateGroup) // 客户路由
exampleRouter.InitFileUploadAndDownloadRouter(PrivateGroup) // 文件上传下载功能路由

}

global.GVA_LOG.Info("router register success")
return Router
}

有点长,拆分一下。


读取配置

1
2
3
4
5
6
7
8
9
10
// 设置为发布模式
if global.GVA_CONFIG.System.Env == "public" {
gin.SetMode(gin.ReleaseMode) //DebugMode ReleaseMode TestMode
}

Router := gin.New()

if global.GVA_CONFIG.System.Env != "public" {
Router.Use(gin.Logger(), gin.Recovery())
}

定义了一个路由器(Router),并根据环境变量global.GVA_CONFIG.System.Env的值来设置路由器的模式。

  1. 首先,检查global.GVA_CONFIG.System.Env的值是否为”public”。如果是,则设置路由器的模式为ReleaseMode

  2. 创建一个新的路由器实例Router,使用gin.New()

  3. 检查global.GVA_CONFIG.System.Env的值是否不等于”public”。如果是,则表示当前环境不是公共环境(例如:开发环境、测试环境或生产环境)。在这种情况下,我们为路由器添加两个中间件:gin.Logger()用于记录请求和响应的日志,以及gin.Recovery()用于处理异常并返回500错误页面。


插件、静态资源、跨域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
InstallPlugin(Router) // 安装插件
// 如果想要不使用nginx代理前端网页,可以修改 web/.env.production 下的
// VUE_APP_BASE_API = /
// VUE_APP_BASE_PATH = http://localhost
// 然后执行打包命令 npm run build。在打开下面3行注释
// Router.Static("/favicon.ico", "./dist/favicon.ico")
// Router.Static("/assets", "./dist/assets") // dist里面的静态资源
// Router.StaticFile("/", "./dist/index.html") // 前端网页入口页面

Router.StaticFS(global.GVA_CONFIG.Local.StorePath, http.Dir(global.GVA_CONFIG.Local.StorePath)) // 为用户头像和文件提供静态地址

// Router.Use(middleware.LoadTls()) // 如果需要使用https 请打开此中间件 然后前往 core/server.go 将启动模式 更变为 Router.RunTLS("端口","你的cre/pem文件","你的key文件")
// 跨域,如需跨域可以打开下面的注释
// Router.Use(middleware.Cors()) // 直接放行全部跨域请求
// Router.Use(middleware.CorsByRules()) // 按照配置的规则放行跨域请求
//global.GVA_LOG.Info("use middleware cors")

docs.SwaggerInfo.BasePath = global.GVA_CONFIG.System.RouterPrefix
Router.GET(global.GVA_CONFIG.System.RouterPrefix+"/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
global.GVA_LOG.Info("register swagger handler")
  1. 调用InstallPlugin(Router)安装插件。
  2. (可选)如果不需要使用nginx代理前端网页,可以将web/.env.production文件中的VUE_APP_BASE_APIVUE_APP_BASE_PATH修改为适当的值,然后执行打包命令npm run build
  3. 添加静态资源路由,为用户头像和文件提供静态地址。
  4. (可选)添加中间件以处理TLS证书和跨域请求。
  5. 配置Swagger信息,将其基路径设置为应用程序的系统路由前缀。
  6. 添加Swagger路由,以便在前端页面中显示Swagger文档。

注册路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
systemRouter := router.RouterGroupApp.System
exampleRouter := router.RouterGroupApp.Example

PublicGroup := Router.Group(global.GVA_CONFIG.System.RouterPrefix)
{
// 健康监测
PublicGroup.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, "ok")
})
}
{
systemRouter.InitBaseRouter(PublicGroup) // 注册基础功能路由 不做鉴权
systemRouter.InitInitRouter(PublicGroup) // 自动初始化相关
}
PrivateGroup := Router.Group(global.GVA_CONFIG.System.RouterPrefix)
PrivateGroup.Use(middleware.JWTAuth()).Use(middleware.CasbinHandler())
{
systemRouter.InitApiRouter(PrivateGroup, PublicGroup) // 注册功能api路由
systemRouter.InitJwtRouter(PrivateGroup) // jwt相关路由
systemRouter.InitUserRouter(PrivateGroup) // 注册用户路由
systemRouter.InitMenuRouter(PrivateGroup) // 注册menu路由
systemRouter.InitSystemRouter(PrivateGroup) // system相关路由
systemRouter.InitCasbinRouter(PrivateGroup) // 权限相关路由
systemRouter.InitAutoCodeRouter(PrivateGroup) // 创建自动化代码
systemRouter.InitAuthorityRouter(PrivateGroup) // 注册角色路由
systemRouter.InitSysDictionaryRouter(PrivateGroup) // 字典管理
systemRouter.InitAutoCodeHistoryRouter(PrivateGroup) // 自动化代码历史
systemRouter.InitSysOperationRecordRouter(PrivateGroup) // 操作记录
systemRouter.InitSysDictionaryDetailRouter(PrivateGroup) // 字典详情管理
systemRouter.InitAuthorityBtnRouterRouter(PrivateGroup) // 字典详情管理
systemRouter.InitChatGptRouter(PrivateGroup) // chatGpt接口

exampleRouter.InitCustomerRouter(PrivateGroup) // 客户路由
exampleRouter.InitFileUploadAndDownloadRouter(PrivateGroup) // 文件上传下载功能路由

}

global.GVA_LOG.Info("router register success")
  1. 创建两个路由器systemRouterexampleRouter,分别对应于应用程序中的系统路由和示例路由。
  2. 创建两个路由器
    • 公共路由PublicGroup,不需要鉴权。
    • 私有路由PrivateGroup,需要进行JWT登录验证。
  3. PublicGroup中添加健康监测路由,用于检查应用程序是否正常运行。
  4. 调用systemRouter.InitBaseRouter(PublicGroup)systemRouter.InitInitRouter(PublicGroup),注册应用程序的基础路由和自动初始化路由。

接着来看一下在PrivateGroup中添加的两个中间件,用于进行JWT认证和Casbin权限控制。

  • 中间件,方法返回结果是gin.HandlerFunc类型。
  • gin中type HandlerFunc func(*Context),故返回一个匿名函数,参数为c *gin.Context
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
func JWTAuth() gin.HandlerFunc {
return func(c *gin.Context) {
// 我们这里jwt鉴权取头部信息 x-token 登录时回返回token信息 这里前端需要把token存储到cookie或者本地localStorage中 不过需要跟后端协商过期时间 可以约定刷新令牌或者重新登录
token := c.Request.Header.Get("x-token")
if token == "" {
response.FailWithDetailed(gin.H{"reload": true}, "未登录或非法访问", c)
c.Abort()
return
}
if jwtService.IsBlacklist(token) {
response.FailWithDetailed(gin.H{"reload": true}, "您的帐户异地登陆或令牌失效", c)
c.Abort()
return
}
j := utils.NewJWT()
// parseToken 解析token包含的信息
claims, err := j.ParseToken(token)
if err != nil {
if errors.Is(err, utils.TokenExpired) {
response.FailWithDetailed(gin.H{"reload": true}, "授权已过期", c)
c.Abort()
return
}
response.FailWithDetailed(gin.H{"reload": true}, err.Error(), c)
c.Abort()
return
}

// 已登录用户被管理员禁用 需要使该用户的jwt失效 此处比较消耗性能 如果需要 请自行打开
// 用户被删除的逻辑 需要优化 此处比较消耗性能 如果需要 请自行打开

//if user, err := userService.FindUserByUuid(claims.UUID.String()); err != nil || user.Enable == 2 {
// _ = jwtService.JsonInBlacklist(system.JwtBlacklist{Jwt: token})
// response.FailWithDetailed(gin.H{"reload": true}, err.Error(), c)
// c.Abort()
//}
if claims.ExpiresAt.Unix()-time.Now().Unix() < claims.BufferTime {
dr, _ := utils.ParseDuration(global.GVA_CONFIG.JWT.ExpiresTime)
claims.ExpiresAt = jwt.NewNumericDate(time.Now().Add(dr))
newToken, _ := j.CreateTokenByOldToken(token, *claims)
newClaims, _ := j.ParseToken(newToken)
c.Header("new-token", newToken)
c.Header("new-expires-at", strconv.FormatInt(newClaims.ExpiresAt.Unix(), 10))
if global.GVA_CONFIG.System.UseMultipoint {
RedisJwtToken, err := jwtService.GetRedisJWT(newClaims.Username)
if err != nil {
global.GVA_LOG.Error("get redis jwt failed", zap.Error(err))
} else { // 当之前的取成功时才进行拉黑操作
_ = jwtService.JsonInBlacklist(system.JwtBlacklist{Jwt: RedisJwtToken})
}
// 无论如何都要记录当前的活跃状态
_ = jwtService.SetRedisJWT(newToken, newClaims.Username)
}
}
c.Set("claims", claims)
c.Next()
}
}
  1. 从请求头 x-token 拿到前端发送的token。
  2. 判断是否有token 和 是否黑名单。
  3. 解析token,拿到里面的信息,判断是否过期。
  4. 检查原始token的过期时间是否小于预设的缓冲时间。如果是,则需要重新生成一个新的token,并将新的过期时间设置为原始token的过期时间加上预设的缓冲时间。
  5. 解析新的token,获取新的过期时间。
  6. 将新的token和新的过期时间添加到响应头中。
  7. 如果系统配置了多点登录(UseMultipoint),则从Redis中获取用户名对应的token,并将其添加到黑名单中。
  8. 记录当前的活跃状态,将新的token和用户名添加到Redis中。
  9. 将用户信息和新的token添加到请求上下文中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func CasbinHandler() gin.HandlerFunc {
return func(c *gin.Context) {
if global.GVA_CONFIG.System.Env != "develop" {
waitUse, _ := utils.GetClaims(c)
//获取请求的PATH
path := c.Request.URL.Path
obj := strings.TrimPrefix(path, global.GVA_CONFIG.System.RouterPrefix)
// 获取请求方法
act := c.Request.Method
// 获取用户的角色
sub := strconv.Itoa(int(waitUse.AuthorityId))
e := casbinService.Casbin() // 判断策略中是否存在
success, _ := e.Enforce(sub, obj, act)
if !success {
response.FailWithDetailed(gin.H{}, "权限不足", c)
c.Abort()
return
}
}
c.Next()
}
}

使用Casbin模型鉴权。


中间件看完了,再看一下其中一个路由注册:注册基础功能路由systemRouter.InitBaseRouter(PublicGroup) 。(其他路由自己看)

1
2
3
4
5
6
7
8
9
10
11
type BaseRouter struct{}

func (s *BaseRouter) InitBaseRouter(Router *gin.RouterGroup) (R gin.IRoutes) {
baseRouter := Router.Group("base")
baseApi := v1.ApiGroupApp.SystemApiGroup.BaseApi
{
baseRouter.POST("login", baseApi.Login)
baseRouter.POST("captcha", baseApi.Captcha)
}
return baseRouter
}
  1. 创建路由组baseRouter
  2. 定义两个HTTP请求处理函数:
    • POST("login", baseApi.Login)
    • ``POST(“captcha”, baseApi.Captcha)`
    • 分别用于处理登录请求和验证码请求。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
func (b *BaseApi) Login(c *gin.Context) {
var l systemReq.Login
err := c.ShouldBindJSON(&l)
key := c.ClientIP()

if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
err = utils.Verify(l, utils.LoginVerify)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}

// 判断验证码是否开启
openCaptcha := global.GVA_CONFIG.Captcha.OpenCaptcha // 是否开启防爆次数
openCaptchaTimeOut := global.GVA_CONFIG.Captcha.OpenCaptchaTimeOut // 缓存超时时间
v, ok := global.BlackCache.Get(key)
if !ok {
global.BlackCache.Set(key, 1, time.Second*time.Duration(openCaptchaTimeOut))
}

var oc bool = openCaptcha == 0 || openCaptcha < interfaceToInt(v)

if !oc || (l.CaptchaId != "" && l.Captcha != "" && store.Verify(l.CaptchaId, l.Captcha, true)) {
u := &system.SysUser{Username: l.Username, Password: l.Password}
user, err := userService.Login(u)
if err != nil {
global.GVA_LOG.Error("登陆失败! 用户名不存在或者密码错误!", zap.Error(err))
// 验证码次数+1
global.BlackCache.Increment(key, 1)
response.FailWithMessage("用户名不存在或者密码错误", c)
return
}
if user.Enable != 1 {
global.GVA_LOG.Error("登陆失败! 用户被禁止登录!")
// 验证码次数+1
global.BlackCache.Increment(key, 1)
response.FailWithMessage("用户被禁止登录", c)
return
}
b.TokenNext(c, *user)
return
}
// 验证码次数+1
global.BlackCache.Increment(key, 1)
response.FailWithMessage("验证码错误", c)
}

跳转到baseApi.Login中,里面是一堆校验,在第28行调用了userService.Login方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func (userService *UserService) Login(u *system.SysUser) (userInter *system.SysUser, err error) {
if nil == global.GVA_DB {
return nil, fmt.Errorf("db not init")
}

var user system.SysUser
err = global.GVA_DB.Where("username = ?", u.Username).Preload("Authorities").Preload("Authority").First(&user).Error
if err == nil {
if ok := utils.BcryptCheck(u.Password, user.Password); !ok {
return nil, errors.New("密码错误")
}
MenuServiceApp.UserAuthorityDefaultRouter(&user)
}
return &user, err
}

第7行中,用gorm查询数据库。

  1. .Where("username = ?", u.Username):这是查询条件,用于查找与给定用户名匹配的用户。? 是一个占位符,将替换为实际值,稍后传递。

  2. .Preload("Authorities"):这告诉数据库预加载与用户关联的“权限”关系。这意味着当查询结果返回时,与用户关联的所有“权限”也将被加载,而不是仅加载ID。

  3. .Preload("Authority"):这告诉数据库预加载与用户关联的“权限”的“权限”关系。这意味着当查询结果返回时,与用户关联的所有“权限”的“权限”也将被加载,而不是仅加载ID。

  4. .First(&user):这告诉数据库返回与查询条件匹配的第一个记录。结果将存储在user结构中。


初始化服务

1
2
3
4
address := fmt.Sprintf(":%d", global.GVA_CONFIG.System.Addr)
s := initServer(address, Router)

global.GVA_LOG.Error(s.ListenAndServe().Error())
1
2
3
4
5
6
7
8
9
func initServer(address string, router *gin.Engine) server {
return &http.Server{
Addr: address,
Handler: router,
ReadTimeout: 20 * time.Second,
WriteTimeout: 20 * time.Second,
MaxHeaderBytes: 1 << 20,
}
}

初始化和Server后,启动Server。

上一篇:
「配置」MarkText 默认开启侧边栏
下一篇:
2023 CSP-J1真题