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() initialize.OtherInit() global.GVA_LOG = core.Zap() zap.ReplaceGlobals(global.GVA_LOG) global.GVA_DB = initialize.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 globalvar ( 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 *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 func GetGlobalDBByDBName (dbname string ) *gorm.DB { lock.RLock() defer lock.RUnlock() return GVA_DBList[dbname] } 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 corefunc 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 == "" { 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 { config = configEnv fmt.Printf("您正在使用%s环境变量,config的路径为%s\n" , internal.ConfigEnv, config) } } else { fmt.Printf("您正在使用命令行的-c参数传递的值,config的路径为%s\n" , config) } } else { 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) } 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 == "" { 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 { config = configEnv fmt.Printf("您正在使用%s环境变量,config的路径为%s\n" , internal.ConfigEnv, config) } } else { fmt.Printf("您正在使用命令行的-c参数传递的值,config的路径为%s\n" , config) } } else { 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 = "debug" ReleaseMode = "release" 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 configtype 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"` AutoCode Autocode `mapstructure:"autocode" json:"autocode" yaml:"autocode"` 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"` 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 configtype JWT struct { SigningKey string `mapstructure:"signing-key" json:"signing-key" yaml:"signing-key"` 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 initializefunc 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.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 func Zap () (logger *zap.Logger) { if ok, _ := utils.PathExists(global.GVA_CONFIG.Zap.Director); !ok { 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() 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(), DefaultStringSize: 191 , 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 } }
首先,它从全局配置中读取MySQL配置。
如果MySQL的数据库名称(dbname)为空,则返回nil。
创建一个mysql.Config结构体,其中包含MySQL的DSN(数据源名称)和其他设置。
使用gorm.Open函数连接到MySQL数据库,并使用mysql.New(mysqlConfig)创建一个新的MySQL实例。
如果出现错误,则返回nil。
否则,设置GORM的配置,例如表前缀和单数形式。
设置SQL数据库的最大空闲连接数和最大打开连接数。
返回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 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 接着是两个initialize
:initialize.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()) _, err := global.GVA_Timer.AddTaskByFunc("ClearDB" , "@daily" , func () { err := task.ClearTable(global.GVA_DB) 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 { initialize.Redis() } if global.GVA_CONFIG.System.UseMongo { err := initialize.Mongo.Initialization() if err != nil { zap.L().Error(fmt.Sprintf("%+v" , err)) } } 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) 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 { initialize.Redis() } if global.GVA_CONFIG.System.UseMongo { err := initialize.Mongo.Initialization() if err != nil { zap.L().Error(fmt.Sprintf("%+v" , err)) } }
读取配置文件
初始化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, DB: redisCfg.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 } }
初始化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黑名单。
初始化路由 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) } 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 Router.StaticFS(global.GVA_CONFIG.Local.StorePath, http.Dir(global.GVA_CONFIG.Local.StorePath)) 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) systemRouter.InitJwtRouter(PrivateGroup) systemRouter.InitUserRouter(PrivateGroup) systemRouter.InitMenuRouter(PrivateGroup) systemRouter.InitSystemRouter(PrivateGroup) 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) 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) } Router := gin.New() if global.GVA_CONFIG.System.Env != "public" { Router.Use(gin.Logger(), gin.Recovery()) }
定义了一个路由器(Router),并根据环境变量global.GVA_CONFIG.System.Env
的值来设置路由器的模式。
首先,检查global.GVA_CONFIG.System.Env
的值是否为”public”。如果是,则设置路由器的模式为ReleaseMode
。
创建一个新的路由器实例Router
,使用gin.New()
。
检查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) Router.StaticFS(global.GVA_CONFIG.Local.StorePath, http.Dir(global.GVA_CONFIG.Local.StorePath)) 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" )
调用InstallPlugin(Router)
安装插件。
(可选)如果不需要使用nginx代理前端网页,可以将web/.env.production
文件中的VUE_APP_BASE_API
和VUE_APP_BASE_PATH
修改为适当的值,然后执行打包命令npm run build
。
添加静态资源路由,为用户头像和文件提供静态地址。
(可选)添加中间件以处理TLS证书和跨域请求。
配置Swagger信息,将其基路径设置为应用程序的系统路由前缀。
添加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) systemRouter.InitJwtRouter(PrivateGroup) systemRouter.InitUserRouter(PrivateGroup) systemRouter.InitMenuRouter(PrivateGroup) systemRouter.InitSystemRouter(PrivateGroup) 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) exampleRouter.InitCustomerRouter(PrivateGroup) exampleRouter.InitFileUploadAndDownloadRouter(PrivateGroup) } global.GVA_LOG.Info("router register success" )
创建两个路由器systemRouter
和exampleRouter
,分别对应于应用程序中的系统路由和示例路由。
创建两个路由器
公共路由PublicGroup
,不需要鉴权。
私有路由PrivateGroup
,需要进行JWT登录验证。
在PublicGroup
中添加健康监测路由,用于检查应用程序是否正常运行。
调用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) { 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() 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 } 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() } }
从请求头 x-token 拿到前端发送的token。
判断是否有token 和 是否黑名单。
解析token,拿到里面的信息,判断是否过期。
检查原始token的过期时间是否小于预设的缓冲时间。如果是,则需要重新生成一个新的token,并将新的过期时间设置为原始token的过期时间加上预设的缓冲时间。
解析新的token,获取新的过期时间。
将新的token和新的过期时间添加到响应头中。
如果系统配置了多点登录(UseMultipoint),则从Redis中获取用户名对应的token,并将其添加到黑名单中。
记录当前的活跃状态,将新的token和用户名添加到Redis中。
将用户信息和新的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 := 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 }
创建路由组baseRouter
。
定义两个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)) global.BlackCache.Increment(key, 1 ) response.FailWithMessage("用户名不存在或者密码错误" , c) return } if user.Enable != 1 { global.GVA_LOG.Error("登陆失败! 用户被禁止登录!" ) global.BlackCache.Increment(key, 1 ) response.FailWithMessage("用户被禁止登录" , c) return } b.TokenNext(c, *user) return } 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查询数据库。
.Where("username = ?", u.Username)
:这是查询条件,用于查找与给定用户名匹配的用户。?
是一个占位符,将替换为实际值,稍后传递。
.Preload("Authorities")
:这告诉数据库预加载与用户关联的“权限”关系。这意味着当查询结果返回时,与用户关联的所有“权限”也将被加载,而不是仅加载ID。
.Preload("Authority")
:这告诉数据库预加载与用户关联的“权限”的“权限”关系。这意味着当查询结果返回时,与用户关联的所有“权限”的“权限”也将被加载,而不是仅加载ID。
.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。