([\w\W])*), client\: %{IP:clientIp}(, server\: %{IPORHOST:server})?(, request\: \"%{DATA:request}\")?(, upstream\: \"%{DATA:upstream}\")?(, host\: \"%{DATA:host}\")?" ]
+ }
+ }
+
+ #删除一些多余字段
+ mutate {
+ remove_field => [ "message","@version"]
+ }
+
+}
+
+output {
+ #将最终处理的结果输出到调试面板(控制台),您可以开启,先观察处理结果是否是您期待的,确保正确之后,注释掉即可
+ #stdout { codec => rubydebug }
+
+ # 官方说,这里每出现一个 elasticsearch 都是一个数据库客户端连接,建议用一个连接一次性输出多个日志内容到 elk ,像如下这样
+ # 这样配置可以最大减少 elk 服务器的连接数,减小压力,因为 elk 今后将管理所有项目的日志,数据处理压力会非常大
+ elasticsearch {
+ # 172.21.0.13 请自行替换为您的 elk 服务器地址
+ hosts => ["http://172.21.0.13:9200"]
+ index => "%{[@metadata][target_index]}"
+ }
+}
+
+#配置完毕,重启容器
+docker restart logstash
+
+#可以观察近3分钟的日志,确保配置正确,启动正常
+docker logs --since 3m logstash7
+
+```
+
+> 4.4.3 现在我们可以访问kibana地址:`http://172.21.0.13:5601` , 如果是云服务器就使用外网地址访问即可.
+> 以下操作基本都是可视化界面,通过鼠标点击等操作完成,我就以截图展示一个完整的主线操作流程, 其他知识请自行查询官网或者加我们的项目群咨询讨论.
+![步骤1](https://www.ginskeleton.com/images/elk001.png)
+![步骤2](https://www.ginskeleton.com/images/elk002.png)
+![步骤3](https://www.ginskeleton.com/images/elk003.png)
+![步骤4](https://www.ginskeleton.com/images/elk004.png)
+
+> 特别说明:以下数据是基于测试环境, 有一些数据是直接把老项目的日志文件覆盖到指定位置,所以界面的查询日期跨度比较大.
+> nginx access 的日志
+![nginx_access日志](https://www.ginskeleton.com/images/elk005.png)
+
+>> goskeleton 的日志
+![goskeleton的elk日志](https://www.ginskeleton.com/images/elk006.png)
+
+> nginx error 的日志
+![nginx_access日志](https://www.ginskeleton.com/images/elk007.png)
+
+
+#### 5.更炫酷未来
+> 基于以上数据我们可以在 elk 做数据统计、分析,例如:可视化展示网站访问量. 哪些接口访问最多、哪些接口访问最耗时,就需要优先优化。
+> elk 能做的事情超乎你的想象(机器学习、数据关联性分析、地理位置分布分析、各种图形化等等), 请参考官方提供的可视化模板,自己做数据展示设计即可。
+> 比较遗憾的是我们做的模板无法直接分享给其他人,只能分享最终的效果,其他开发者可自行参考制作自己的展示模板.
+![logstash样图](https://www.ginskeleton.com/images/logstash1.png)
+
+
diff --git a/GinSkeleton/docs/faq.md b/GinSkeleton/docs/faq.md
new file mode 100644
index 0000000..80d9ed2
--- /dev/null
+++ b/GinSkeleton/docs/faq.md
@@ -0,0 +1,86 @@
+## 常见问题汇总
+> 1.本篇我们将汇总使用过程中最常见的问题, 很多细小的问题或许在这里你能找到答案.
+
+##### 2.为什么该项目 go.mod 中的模块名是 goskeleton ,但是下载下来的文件名却是 GinSkeleton ?
+> 本项目一开始我们命名为 ginskeleton , 包名也是这个,但是后来感觉 goskeleton 好听一点,因此改名(现在看是错了),由于版本已经更新较多,同时不影响使用,此次失误请忽略即可.
+
+##### 3.为什么编译后的文件提示 config.yml 文件不存在 ?
+> 项目的编译仅限于代码部分,不包括资源部分:config 目录、public 目录、storage 目录,因此编译后的文件使用时,需要带上这个三个目录,否则程序无法正常运行.
+
+##### 4.表单参数验证器代码部分的疑问
+> 示例代码位置:`app/http/validator/web/users/register.go` ,如下代码段
+```code
+type Register struct {
+ Base
+ Pass string `form:"pass" json:"pass" binding:"required,min=3,max=20"` //必填,密码长度范围:【3,20】闭区间
+ Phone string `form:"phone" json:"phone" binding:"required,len=11"` // 验证规则:必填,长度必须=11
+ //CardNo string `form:"card_no" json:"card_no" binding:"required,len=18"` //身份证号码,必填,长度=18
+}
+
+// 注意这里绑定在了 Register
+func (r Register) CheckParams(context *gin.Context) {
+ // ...
+}
+
+
+```
+> CheckParams 函数是否可以绑定在指针上?例如写成如下:
+```code
+// 注意这里绑定在了 *Register
+func (r *Register) CheckParams(context *gin.Context) {
+ // ...
+}
+
+```
+> 这里绝对不可以,因为表单参数验证器在程序启动时会自动注册在容器,每次调用都必须是一个全新的初始化代码段,如果绑定在指针,第一次请求验证通过之后,相关的参数值就会绑定容器中的代码上,造成下次请求数据污染.
+
+##### 5.全局容器的作用是什么?
+```code
+本项目使用容器最多的地方:
+app/http/validator/common/register_validator/register_validator.go
+
+根据key从容器调用:routers/web.go > validatorFactory.Create() 函数 ,就是根据注册时的键从容器获取代码.
+
+目的:
+1.一个请求(request)到达路由以后,需要进行表单参数的校验,如果是传统的方法,就得import相关的验证器文件包,然后掉用包中的函数,进行参数验证, 这种做法会导致路由文件的头部会出现N多的import ....包, 因为你一个接口就得一个验证器。
+在这个项目骨架中,我们将验证器全部注册在容器中,路由文件头部只需要导入一个验证器的包就可以通过key调用对应的value(验证器函数)。
+你可以和别人做的项目对比一下,路由文件的头部 import 部分,看看传统方式导入了是不是N个....
+
+2.因为验证器在项目启动时,率先注册在了容器(内存),因此调用速度也是超级快。性能极佳.
+
+```
+
+##### 6.每个model都要 create 一次,难道每个 model 都是一次数据库连接吗?
+```code
+
+关系型数据库驱动库其实是根据 config.yml中的配置初始化了一次,因此每种数据库全局只有一个连接,以后每一次都是从同一个驱动指针地址,通过ping() 从底层的连接池获取一个连接。用完也是自动释放的.
+看起来每一个表要初始化一次,主要是为了解决任何一个表可以随意切换到别的数据库连接,解决数据库多源场景。
+每种数据库,在整个项目全局就一个数据库驱动初始化后的连接池:app/utils/sql_factory/client.go
+
+```
+
+##### 7.为什么该项目强烈建议应用服务器前置nginx?
+```code
+
+1.nginx处理静态资源,几乎是无敌的,尤其是内存占用方面的管理非常完美.
+2.nginx前置很方便做负载均衡.
+3.nginx 的access.log、error.log 都是行业通用,可以很方便对接到 elk ,进行后续统计、分析、机器学习、报表展示等等.
+4.gin 框架本身建议生产环境切换 gin 的运行模式:gin.SetMode(gin.ReleaseMode) ,该模式无接口访问日志生成,那么你的接口访问日志就必须要搭配 nginx ,同时该模式我们经过测试对比,性能再度提升 5%
+
+```
+
+##### 8.本项目骨架引用的包,如何更新至最新版?
+> 1.本项目骨架主动引入包全部在 `go.mod` 文件,如果想自己更新至最新版,非常简单,但是必须注意:该包更新的功能兼容现有版本,如果不兼容,可能会导致封装层`app/utils/xxx` 出现错误,功能也无法正常使用.
+> 2.例如:gormv2 目前在用版本是 `v1.20.5`, 官方最新版本地址:https://github.com/go-gorm/gorm/tags , 最新版 : v1.20.7
+```code
+
+ // 1. go.mod 文件修改以下版本号至最新版
+ gorm.io/gorm v1.20.5 ===>
+ gorm.io/gorm v1.20.7
+
+ // 在goland终端或者 go.mod 同目录执行以下命令即可
+ go mod tidy
+
+```
+
+
\ No newline at end of file
diff --git a/GinSkeleton/docs/formparams.md b/GinSkeleton/docs/formparams.md
new file mode 100644
index 0000000..326fa5d
--- /dev/null
+++ b/GinSkeleton/docs/formparams.md
@@ -0,0 +1,72 @@
+### 表单参数提交介绍
+ - 1.前端提交简单的表单参数示例代码,[请参考已有的接口测试用例文档](./api_doc.md)
+ - 2.本篇我们将介绍复杂表单参数的提交.
+
+#### 什么是简单的表单参数提交
+> 1.如果接口参数都是简单的键值对,没有嵌套关系,就是简单模式.
+
+![form-parms](https://www.ginskeleton.com/images/formparams1.png)
+
+#### 什么是复杂的表单参数提交
+> 1.表单参数存在嵌套关系,这种数据在 `postman` 都是以 raw 方式提交,本质上就是请求的表单参数头设置为:`Content-Type: application/json`
+
+![form-parms](https://www.ginskeleton.com/images/formparams2.png)
+
+#### `ginskeleton` 后台处理复杂表单数据
+> 1.按照提交的数据格式,我们在表单参数验证器部分,定义接受的结构体,例如上图的参数我们在后台的接受参数就可以定义如下:
+```code
+
+type ViewEleCreateUpdate struct {
+ FkBigScreenView float64 `form:"fk_big_screen_view" json:"fk_big_screen_view"`
+ EleId string `form:"ele_id" json:"ele_id"`
+ EleIdTitle string `form:"ele_id_title" json:"ele_id_title"`
+ Status *float64 `form:"status" json:"status"`
+ Remark string `form:"remark" json:"remark"`
+ ChildrenTableDelIds string `form:"children_table_del_ids" json:"children_table_del_ids"`
+ ChildrenTable []ChildrenTable `form:"children_table" json:"children_table"`
+}
+
+// 大屏界面元素的子表数据
+// 每种元素都有三个状态(1=正常;2=禁止;3=隐藏)
+// 被嵌套的数据请独立定义,这样的好处就是后续可以随意精准取出任意一部分
+type ChildrenTable struct {
+ Id float64 `form:"id" json:"id"`
+ FkBigScreenViewElement float64 `form:"fk_big_screen_view_element" json:"fk_big_screen_view_element"`
+ FkBigScreenViewElementStatusName float64 `form:"fk_big_screen_view_element_status_name" json:"fk_big_screen_view_element_status_name"`
+ Status *float64 `form:"status" json:"status"`
+ Remark string `form:"remark" json:"remark"`
+}
+
+```
+#### 接口验证器 ↓
+> 1.复杂接口参数前端都是通过json格式提交.
+> 2.`go` 语言代码接收语法是 `context.ShouldBindJSON()`
+
+![form-parms3](https://www.ginskeleton.com/images/formparams3.png)
+
+#### 接口验证器对应的数据类型 ↓
+![form-parms4](https://www.ginskeleton.com/images/formparams4.png)
+
+#### 在后续的控制器、model 获取子表数据
+```code
+# 在接口验证逻辑部分,通过参数验证后,我们将子表数据已经存储在上线文
+
+// 子表数据设置一个独立的键存储
+extraAddBindDataContext.Set(consts.ValidatorPrefix+"children_table_del_ids", v.ChildrenTable)
+
+// 那么后续的控制器、以及model都可以根据相关的键获取原始数据、断言为我们定义的子表数据类型继续操作
+ var childrenTableData = c.MustGet(consts.ValidatorPrefix + "children_table_del_ids")
+
+ // 获取子表数据断言为我们定义的子表数据类型
+ // 这里需要注意:验证器验证参数ok调用了控制器,如果再验证器文件没有创建独立的数据类型文件夹(包),在控制器断言会形成包的嵌套、报错,这就是我们一开始将复杂数据类型创建独立的文件件定义的原因
+
+ if subTableStr, ok := childrenTableData.([]data_type_for_create_edit.ChildrenTable); ok {
+ // 这里就相当于获取了go语言切片数据
+ // 继续批量存储、或者挨个遍历就行
+ // .... 省略业务逻辑
+ }
+
+```
+
+
+
diff --git a/GinSkeleton/docs/global_variable.md b/GinSkeleton/docs/global_variable.md
new file mode 100644
index 0000000..556324c
--- /dev/null
+++ b/GinSkeleton/docs/global_variable.md
@@ -0,0 +1,79 @@
+## 项目中被初始化的全局变量清单介绍
+
+### 1.前言
+> 1.程序启动时初始化动作统一由 `bootstrap/init.go` 文件中的代码段负责,本次我们将介绍3个常用的全局变量.
+> 2.全局变量只会使用法简洁化, 不对原始语法造成任何破坏, 封装全局变量时我们经过谨慎地评估、测试相关代码段、从而保证并发安全性.
+
+### 2.gorm 全局变量
+> 1.请按照配置文件 `congfig/gorm_v2.yml` 中的提示正确配置数据库,开启程序启动初始化数据库参数,程序在启动时会自动为您初始化全局变量.
+> 2.不同类型的数据库全局变量名不一样, 对照关系参见以下代码段说明.
+> 3.更多用法参见单元测试:[gorm_v2单元测试](../test/gormv2_test.go).
+> 4.本文档我们主要介绍 gorm 全局变量初始化的核心.
+```code
+
+// 例如:原始语法,我们以 mysql 驱动的初始化为例进行说明
+// 1.连接数据库,获取mysql连接
+ dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
+ mysqlDb, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
+
+// 2.查询
+db.Select("id", "name", "phone", "email", "remark").Where("name like ?", "%test%").Find(&users)
+
+
+// 本项目中, `variable.GormDbMysql` 完全等于上文中返回的 mysqlDb
+variable.GormDbMysql.Select("id", "name", "phone", "email", "remark").Where("name like ?", "%test%").Find(&users)
+
+// gorm 数据库驱动与本项目骨架对照关系
+variable.GormDbMysql <====完全等于==> gorm.Open(mysql.Open(dsn), &gorm.Config{})
+variable.GormDbSqlserver <====完全等于==> gorm.Open(sqlserver.Open(dsn), &gorm.Config{})
+variable.GormDbPostgreSql <====完全等于==> gorm.Open(postgres.Open(dsn), &gorm.Config{})
+```
+
+
+### 3.日志全局变量
+> 1.为了随意、方便地记录项目中日志,我们封装了全局变量 `variable.ZapLog` .
+> 2.由于日志操作内容比较多,我们对它进行了单独介绍,详情参见: [zap高性能日志](zap_log.md)
+
+
+### 4.配置文件全局变量
+> 1.为了更方便地操作配置文件 `config/config.yml` 、 `config/gorm_v2.yml` 我们同样在项目启动时封装了全局变量.
+> 2.`variable.ConfigYml` ,该变量相当于配置文件 `config/config.yml` 文件打开后的指针.
+> 3.`variable.ConfigGormv2Yml` ,该变量相当于配置文件 `config/gorm_v2.yml` 文件打开后的指针.
+> 4.在任何地方您都可以使用以上全局变量直接获取对应配置文件的 键==>值.
+```code
+
+// 获取 config/config.yml 文件中 Websocket.Start 对应的 Int 值
+variable.ConfigYml.GetInt("Websocket.Start")
+
+// 获取 config/gorm_v2.yml 文件中 Gormv2.Mysql.IsInitGlobalGormMysql 对应的 Int 值
+variable.ConfigGormv2Yml.GetInt("Gormv2.Mysql.IsInitGlobalGormMysql")
+
+```
+> 5.获取配置文件中键对应的值数据类型,函数清单,您可以使用 `variable.ConfigYml.` 或者 `variable.ConfigGormv2Yml.` 以下函数名 获取值
+```code
+ // 开发者常用函数
+ GetString(keyName string) string
+ GetInt(keyName string) int
+ GetInt32(keyName string) int32
+ GetInt64(keyName string) int64
+ GetFloat64(keyName string) float64
+ GetDuration(keyName string) time.Duration
+ GetBool(keyName string) bool
+
+ // 非常用函数,主要是项目骨架在使用
+ ConfigFileChangeListen()
+ Clone(fileName string) YmlConfigInterf
+ Get(keyName string) interface{} // 该函数获取一个 键 对应的原始值,因此返回类型为 interface , 基本很少用
+ GetStringSlice(keyName string) []string
+```
+
+### 5.雪花算法(snowflake)生成分布式场景唯一ID
+> 1.相关配置 ` config>config.yml` 配置项 `SnowFlakeMachineId` , 如果本项目同时部署在多台机器,并且需要同时使用该算法,请为每一台机器设置不同的ID,区间范围: [0,1023]
+> 2.随时随地,您可以非常方便的获取一个分布式场景的唯一ID
+> 3.更多详情参见: [SnowFlake单元测试](../test/snowflake_test.go)
+```code
+
+# 雪花算法生成的全局唯一ID数据类型为 int64
+variable.SnowFlake.GetId()
+
+```
diff --git a/GinSkeleton/docs/low_coupling.md b/GinSkeleton/docs/low_coupling.md
new file mode 100644
index 0000000..9740932
--- /dev/null
+++ b/GinSkeleton/docs/low_coupling.md
@@ -0,0 +1,81 @@
+### 本篇将探讨主线解耦问题
+> 1.目前项目主线是从路由开始,直接切入到表单参数验证器,验证通过则直接进入了控制器,这里就导致了验证器和控制器之间存在一点低耦合度.
+> 2.如果你追求更低的模块之间的耦合度,接下来我们将对上述问题进行解耦操作.
+
+
+
+### 当前项目代码存在的低耦合逻辑
+> 1.我们以用户删除数据接口为例进行介绍.
+> 2.本文的 `41` 行就是我们所说验证器与控制器出现了低耦合.
+```code
+
+// 1.访问路由
+users.POST("delete", validatorFactory.Create(consts.ValidatorPrefix+"UsersDestroy"))
+
+
+// 2.进入表单参数验证器
+type Destroy struct {
+ Id float64 `form:"id" json:"id" binding:"required,min=1"`
+}
+
+func (d Destroy) CheckParams(context *gin.Context) {
+
+ if err := context.ShouldBind(&d); err != nil {
+ errs := gin.H{
+ "tips": "UserDestroy参数校验失败,参数校验失败,请检查id(>=1)",
+ "err": err.Error(),
+ }
+ response.ErrorParam(context, errs)
+ return
+ }
+
+ // 该函数主要是将绑定的数据以 键=>值 形式直接传递给下一步(控制器)
+ extraAddBindDataContext := data_transfer.DataAddContext(d, consts.ValidatorPrefix, context)
+ if extraAddBindDataContext == nil {
+ response.ErrorSystem(context, "UserShow表单参数验证器json化失败", "")
+ context.Abort()
+ return
+ } else {
+ // 验证完成,调用控制器,并将验证器成员(字段)递给控制器,保持上下文数据一致性
+ // 以下代码就是验证器与控制器之间的一点耦合
+ (&web.Users{}).Destroy(extraAddBindDataContext)
+ }
+}
+
+```
+
+### 开始解耦
+> 1.针对41行出现的验证器与控制器耦合问题,我们开始解耦
+```code
+
+// 1.我们对以上代码进行简单的改造即可实现代码的解耦
+// 2.路由首先切入表单参数验证器,将对应的控制器代码写在第二个回调函数即可
+// 3.注意:市面上很多框架的中间件等注册的函数都是 "洋葱模型" ,即函数的回调顺序和注册顺序是相反的,但是gin框架则是按照注册顺序依次执行
+users.POST("delete", validatorFactory.Create(consts.ValidatorPrefix+"UsersDestroy"), (&web.Users{}).Destroy)
+
+
+// 4.代码经过以上改在以后, 从 38 行开始的 else { ... } 代码删除即可
+
+```
+
+### 解耦以后的注意事项
+> 1.如果业务针对控制器存在比较多的 `Aop` 切面编程,就会导致路由文件以及 `import` 显得比较繁重
+```code
+
+// 1.例如删除数据之前的和之后的回调
+users.POST("delete",
+
+validatorFactory.Create(consts.ValidatorPrefix+"UsersDestroy"),
+
+(&Users.DestroyBefore{}).Before, // 控制器Aop的前置回调,例如删除数据之前的权限判断,相关代码可参考 app/aop/users/destroy_before.go
+(&web.Users{}).Destroy, // 控制器逻辑
+(&Users.DestroyAfter{}).After // 控制器Aop的后置回调,例如被删除数据之后的数据备份至history表 ,相关代码可参考 app/aop/users/destroy_after.go
+
+)
+
+```
+> 2.对比以上代码,如果你的项目存在较多的 `AOP` 编程、或者说不同的路由前、后回调函数比较多,不建议进行解耦(毕竟目前就是极低耦合),否则给路由文件以及 `import` 部分带来了比较多的负担.
+> 3.如果你的项目路由前后回调函数比较少,建议参考以上代码进行解耦.
+
+
+
diff --git a/GinSkeleton/docs/many_db_operate.md b/GinSkeleton/docs/many_db_operate.md
new file mode 100644
index 0000000..78af450
--- /dev/null
+++ b/GinSkeleton/docs/many_db_operate.md
@@ -0,0 +1,59 @@
+### 同时操作部署在不同服务器的多种数据库
+> 1.本项目骨架在 [数据库操作单元测试](../test/gormv2_test.go) 已经提供了同时操作多服务器、多种数据库的示例代码,为了将此功能更清晰地展现出来,本篇将单独进行介绍.
+> 2.面对复杂场景,需要多个客户端连接到部署在多个不同服务器的 `mysql`、`sqlserver`、`postgresql` 等数据库时, 由于配置文件(config/gorm_v2.yml)只提供了一份数据库连接,无法满足需求,这时您可以通过自定义参数直接连接任意数据库,获取一个数据库句柄,供业务使用.
+
+
+### 相关代码
+> 1.这里直接提取了相关的单元测试示例代码,更多其他操作仍然建议参考单元测试示例代码.
+```code
+
+func TestCustomeParamsConnMysql(t *testing.T) {
+ // 定义一个查询结果接受结构体
+ type DataList struct {
+ Id int
+ Username string
+ Last_login_ip string
+ Status int
+ }
+ // 设置动态参数连接任意多个数据库,以mysql为例进行单元测试
+ // 参数结构体 Write 和 Read 只有设置了具体指,才会生效,否则程序自动使用配置目录(config/gorm_v.yml)中的参数
+ confPrams := gorm_v2.ConfigParams{
+ Write: struct {
+ Host string
+ DataBase string
+ Port int
+ Prefix string
+ User string
+ Pass string
+ Charset string
+ }{Host: "127.0.0.1", DataBase: "db_test", Port: 3306, Prefix: "tb_", User: "root", Pass: "DRsXT5ZJ6Oi55LPQ", Charset: "utf8"},
+ Read: struct {
+ Host string
+ DataBase string
+ Port int
+ Prefix string
+ User string
+ Pass string
+ Charset string
+ }{Host: "127.0.0.1", DataBase: "db_stocks", Port: 3306, Prefix: "tb_", User: "root", Pass: "DRsXT5ZJ6Oi55LPQ", Charset: "utf8"}}
+
+ var vDataList []DataList
+
+ //gorm_v2.GetSqlDriver 参数介绍
+ // sqlType : mysql 、sqlserver、postgresql 等数据库库类型
+ // readDbIsOpen : 是否开启读写分离,1表示开启读数据库的配置,那么 confPrams.Read 参数部分才会生效; 0 则表示 confPrams.Read 部分参数直接忽略(即 读、写同库)
+ // confPrams 动态配置的数据库参数
+ // 此外,其他参数,例如数据库连接池参数等,则直接调用配置项数据库连接池参数,基本不需要配置,这部分对实际操作影响不大
+ if gormDbMysql, err := gorm_v2.GetSqlDriver("mysql", 0, confPrams); err == nil {
+ gormDbMysql.Raw("select id,username,status,last_login_ip from tb_users").Find(&vDataList)
+ fmt.Printf("Read 数据库查询结果:%v\n", vDataList)
+ res := gormDbMysql.Exec("update tb_users set real_name='Write数据库更新' where id<=2 ")
+ if res.Error==nil{
+ fmt.Println("write 数据库更新成功")
+ }else{
+ t.Errorf("单元测试失败,相关错误:%s\n",res.Error.Error())
+ }
+ }
+}
+
+```
\ No newline at end of file
diff --git a/GinSkeleton/docs/nginx.md b/GinSkeleton/docs/nginx.md
new file mode 100644
index 0000000..7bf372d
--- /dev/null
+++ b/GinSkeleton/docs/nginx.md
@@ -0,0 +1,208 @@
+### nginx 配置
+> 1.本篇主要介绍 `nginx` 负载均衡与 `https(ssl)` 证书相关的配置.
+
+#### 1.配置负载均衡代理 `http` 功能
+> 1.如果你的 `go` 服务是通过 `nginx` 代理访问的,那么需要进行配置
+```code
+#注意,upstream 部分放置在 server 块之外,至少需要一个服务器ip。
+upstream goskeleton_list {
+ # 设置负载均衡模式为ip算法模式,这样不同的客户端每次请求都会与第一次建立对话的后端服务器进行交互
+ ip_hash;
+ server 127.0.0.1:20202 ;
+ server 127.0.0.1:20203 ;
+}
+server{
+ #监听端口
+ listen 80 ;
+ # 站点域名,没有的话,写项目名称即可
+ server_name www.ginskeleton.com ;
+ root /home/wwwroot/goproject2020/goskeleton/public ;
+ index index.htm index.html ;
+ charset utf-8 ;
+
+ # 使用 nginx 直接接管静态资源目录
+ # 由于 ginskeleton 把路由(public)地址绑定到了同名称的目录 public ,所以我们就用 nginx 接管这个资源路由
+ location ~ /public/(.*) {
+ # 使用我们已经定义好的 root 目录,然后截取用户请求时,public 后面的所有地址,直接响应资源,不存在就返回404
+ try_files /$1 =404;
+ }
+
+
+ location ~ / {
+ # 静态资源、目录交给ngixn本身处理,动态路由请求执行后续的代理代码
+ try_files $uri $uri/ @goskeleton;
+ }
+ location @goskeleton {
+
+ #将客户端的ip和头域信息一并转发到后端服务器
+ proxy_set_header Host $http_host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+
+ # 转发Cookie,设置 SameSite
+ proxy_cookie_path / "/; secure; HttpOnly; SameSite=strict";
+
+ # 最后,执行代理访问真实服务器
+ proxy_pass http://goskeleton_list ;
+
+ }
+ # 以下是静态资源缓存配置
+ location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$
+ {
+ expires 30d;
+ }
+
+ location ~ .*\.(js|css)?$
+ {
+ expires 12h;
+ }
+
+ location ~ /\.
+ {
+ deny all;
+ }
+}
+
+
+```
+
+### 2.配置 `websocket`
+> 如果你的 `websocket` 服务是通过 `nginx` 代理访问的,那么需要在 `nginx` 的配置项需要进行如下设置
+```websocket
+
+upstream ws_list {
+ ip_hash;
+ server 192.168.251.149:20175 ;
+ #server 192.168.251.149:20176 ;
+}
+
+server {
+ listen 20175;
+ server_name localhost;
+
+ location / {
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade websocket;
+ proxy_set_header Connection Upgrade;
+ proxy_read_timeout 60s ;
+
+ proxy_set_header Host $http_host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+
+ proxy_cookie_path / "/; secure; HttpOnly; SameSite=strict";
+
+ proxy_pass http://ws_list ;
+
+ }
+
+
+ location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$
+ {
+ expires 30d;
+ }
+
+ location ~ .*\.(js|css)?$
+ {
+ expires 12h;
+ }
+
+ location ~ /\.
+ {
+ deny all;
+ }
+}
+
+```
+
+
+#### 3.配置 `https` 功能
+> 1.基于 `http` 内容稍作修改即可.
+> 2.相关域名、云服务器都必须备案,否则无法通过域名访问,但是仍然可以通过 `http://云服务器ip` 访问,只不过通过ip访问会浏览器地址栏会提示不安全.
+
+```nginx
+
+#注意,upstream 部分放置在 server 块之外,至少需要一个服务器ip。
+upstream goskeleton_list {
+ # 设置负载均衡模式为ip算法模式,这样不同的客户端每次请求都会与第一次建立对话的后端服务器进行交互
+ ip_hash;
+ server 127.0.0.1:20202 ;
+ server 127.0.0.1:20203 ;
+}
+// 这里主要是将 http 访问重定向到 https,这样就能同时支持 http 和 https 访问
+server {
+ listen 80;
+ server_name www.ginskeleton.com;
+ rewrite ^(.*)$ https://$host$1 permanent;
+}
+
+server{
+ #监听端口
+ listen 443 ssl ;
+ # 站点域名,没有的话,写项目名称即可
+ server_name www.ginskeleton.com ;
+ root /home/wwwroot/goproject2020/goskeleton/public ;
+ index index.html index.htm ;
+ charset utf-8 ;
+
+ # 配置 https 证书
+ # ssl on; # 注意,在很早的低版本nginx上,此项是允许打开的,但是在高于 1.1x.x 版本要求必须关闭.
+ ssl_certificate ginskeleton.crt; # 实际配置建议您指定证书的绝对路径
+ ssl_certificate_key ginskeleton.key; # ginskeleton.crt 、ginskeleton.key 需要向云服务器厂商申请,后续有介绍
+ ssl_session_timeout 5m;
+ ssl_protocols TLSv1 TLSv1.1 TLSv1.2 SSLv2 SSLv3;
+ ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP;
+ ssl_prefer_server_ciphers on;
+
+ # 使用 nginx 直接接管静态资源目录
+ # 由于 ginskeleton 把路由(public)地址绑定到了同名称的目录 public ,所以我们就用 nginx 接管这个资源路由
+ location ~ /public/(.*) {
+ # 使用我们已经定义好的 root 目录,然后截取用户请求时,public 后面的所有地址,直接响应资源,不存在就返回404
+ try_files /$1 =404;
+ }
+
+ location ~ / {
+ # 静态资源、目录交给ngixn本身处理,动态路由请求执行后续的代理代码
+ try_files $uri $uri/ @goskeleton;
+ }
+ // 这里的 @goskeleton 和 try_files 语法块的名称必须一致
+ location @goskeleton {
+
+ #将客户端的ip和头域信息一并转发到后端服务器
+ proxy_set_header Host $http_host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+
+ # 转发Cookie,设置 SameSite
+ proxy_cookie_path / "/; secure; HttpOnly; SameSite=strict";
+
+ # 最后,执行代理访问真实服务器
+ proxy_pass http://goskeleton_list ;
+
+ }
+ # 以下是静态资源缓存配置
+ location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$
+ {
+ expires 30d;
+ }
+
+ location ~ .*\.(js|css)?$
+ {
+ expires 12h;
+ }
+
+ location ~ /\.
+ {
+ deny all;
+ }
+}
+
+
+```
+
+#### 4.关于 `https` 的简要介绍
+> 1.首先能保证数据在传输过程中的安全性.
+> 2.证书需要向第三方代理机构申请(华为云、阿里云、腾讯云等), 个人证书一般都会有免费一年的体验期.
+> 3.证书申请时需要提交您的相关域名, 颁发机构会把您的域名信息和证书绑定, 最终配置在nginx, 当使用浏览器访问时, 浏览器地址栏会变成绿色安全图标.
+> 4.本次使用的 `ssl` 证书是在腾讯云申请的1年免费期证书, 申请地址:`https://console.cloud.tencent.com/ssl` , 企业证书一年至少在 3000+ 元.
+> 5.项目前置 `nginx` 服务器配置 `ssl` 证书通过`https` 协议在网络中传输数据, 当加密数据到达 `nginx` 时,瞬间会被 `http_ssl_module` 模块解密为明文,因此代理的负载均衡服务器不需要配置 `ssl` 选项.
diff --git a/GinSkeleton/docs/project_analysis_1.md b/GinSkeleton/docs/project_analysis_1.md
new file mode 100644
index 0000000..e19f970
--- /dev/null
+++ b/GinSkeleton/docs/project_analysis_1.md
@@ -0,0 +1,24 @@
+## GoSkeleton 项目骨架性能分析报告(一)
+> 1.本次将按照一次请求的生命周期为主线(request--->response),跟踪各部分代码段的cpu耗时,得出可视化的性能报告.
+
+### 前言
+> 1.本次分析,我们以项目骨架默认的门户网站接口为例,该接口虽然简单,但是包含了一个 request 到 response 完整生命周期主线逻辑,很具有代表性.
+> 2.待分析的接口地址:`http://127.0.0.1:20191/api/v1/home/news?newsType=portal&page=1&limit=50`
+
+### cpu数据采集步骤
+> 1.`config/config.yml` 文件中,AppDebug 设置为 true , 调试模式才能进行分析.
+> 2.访问接口:`http://127.0.0.1:20191/`, 确保项目正常启动.
+> 3.浏览器访问pprof接口:`http://127.0.0.1:20191/debug/pprof/`, 点击 `profile` 选项,程序会对本项目进程, 进行 cpu 使用情况底层数据采集, 该过程会持续 30 秒.
+![pprof地址](https://www.ginskeleton.com/images/pprof_menue.jpg)
+> 4.第3步点击以后,必须快速运行 [pprof测试用例](../test/http_client_test.go) 中的 `TestPprof()` 函数,该函数主要负责请求接口,让程序处理业务返回结果, 模拟 request --> response 过程.
+> 5.执行了步骤3和步骤4才能采集到数据,稍等片刻,30秒之后,您点击过的步骤3就会提示下载文件:`profile`, 请保存在您能记住的路径中,稍后马上使用该文件(profile), 至此cpu数据已经采集完毕.
+
+### cpu数据分析步骤
+> 1.首先下载安装 [graphviz](https://www.graphviz.org/download/) ,根据您的系统选择相对应的版本安装,安装完成记得将`安装目录/bin`, 加入系统环境变量.
+> 2.打开cmd窗口,执行 `dot -V` ,会显示版本信息,说明安装已经OK, 那么继续执行 `dot -c` 安装图形显示所需要的插件.
+> 3.在cpu数据采集环节第三步,您已经得到了 `profile` 文件,那么就在同目录打开cmd窗口,执行 `go tool pprof profile`, 然后输入 `web` 回车就会自动打开浏览器,展示给您如下结果图:
+![cpu分析_上](https://www.ginskeleton.com/images/pprof_cmd.jpg)
+
+### 报告详情参见如下图
+![cpu分析_上](https://www.ginskeleton.com/images/analysis1.png)
+
diff --git a/GinSkeleton/docs/project_analysis_2.md b/GinSkeleton/docs/project_analysis_2.md
new file mode 100644
index 0000000..1e86319
--- /dev/null
+++ b/GinSkeleton/docs/project_analysis_2.md
@@ -0,0 +1,68 @@
+## GoSkeleton 项目骨架性能分析报告(二)
+> 1.本次我们分析的目标是操作数据库, 通过操作数据库,分析相关代码段cpu的耗时,得出可视化的性能分析报告。
+
+
+### 操作数据库, 我们需要做如下铺垫代码
+> 1.我们本次分析的核心是在数据库操作部分, 因此我们在路由出添加如下代码,访问路由即可触发数据库的调用.
+```code
+ router.GET("/", func(context *gin.Context) {
+ // 默认路由处直接触发数据库调用
+ if model.CreateTestFactory("").SelectDataMultiple() {
+ context.String(200,"批量查询数据OK")
+ } else {
+ context.String(200,"批量查询数据出错")
+ }
+ context.String(http.StatusOK, "Api 模块接口 hello word!")
+ })
+```
+
+> 2.数据库部分代码,主要逻辑是每次查询1000条,循环查询了100次,并且在最后一次输出了结果集.
+ ```code
+func (t *Test) SelectDataMultiple() bool {
+ // 本次测试的数据表内有6000条左右数据
+ sql := `
+ SELECT
+ code,name,company_name,indudtry,created_at
+ FROM
+ db_stocks.tb_code_list
+ LIMIT 0, 1000 ;
+ `
+ //1.首先独立预处理sql语句,无参数
+ if t.PrepareSql(sql) {
+
+ var code, name, company_name, indudtry, created_at string
+ for i := 1; i <= 100; i++ {
+ //2.执行批量查询
+ rows := t.QuerySqlForMultiple()
+ if rows == nil {
+ variable.ZapLog.Sugar().Error("sql执行失败,sql:", sql)
+ return false
+ } else {
+ // 我们只输出最后一行数据
+ if i == 100 {
+ for rows.Next() {
+ _ = rows.Scan(&code, &name, &company_name, &indudtry, &created_at)
+ fmt.Println(code, name, company_name, indudtry, created_at)
+ }
+ }
+ }
+ rows.Close()
+ }
+ }
+ variable.ZapLog.Info("批量查询sql执行完毕!")
+ return true
+}
+ ```
+### cpu 底层数据采集步骤
+> 1.浏览器访问pprof接口:`http://127.0.0.1:20191/debug/pprof/`, 点击 `profile` 选项,程序会对本项目进程, 进行 cpu 使用情况底层数据采集, 该过程会持续 30 秒.
+![pprof地址](https://www.ginskeleton.com/images/pprof_menue.jpg)
+> 2.新开浏览器窗口,输入 `http://127.0.0.1:20191/` 刷新,触发路由中的数据库操作代码, 等待被 pprof 采集数据.
+> 3.稍等片刻,30秒之后,您点击过的步骤1就会提示下载文件:`profile`, 请保存在您能记住的路径中,稍后马上使用该文件(profile), 至此cpu数据已经采集完毕.
+
+### cpu数据分析步骤
+> 1.首先下载安装 [graphviz](https://www.graphviz.org/download/) ,根据您的系统选择相对应的版本安装,安装完成记得将`安装目录/bin`, 加入系统环境变量.
+> 2.打开cmd窗口,执行 `dot -V` ,会显示版本信息,说明安装已经OK, 那么继续执行 `dot -c` 安装图形显示所需要的插件.
+> 3.在cpu数据采集环节第三步,您已经得到了 `profile` 文件,那么就在同目录打开cmd窗口,执行 `go tool pprof profile`, 然后输入 `web` 回车就会自动打开浏览器,展示给您如下结果图:
+
+### 报告详情参见如下图
+![cpu分析_上](https://www.ginskeleton.com/images/cpu_sql.png)
diff --git a/GinSkeleton/docs/project_analysis_3.md b/GinSkeleton/docs/project_analysis_3.md
new file mode 100644
index 0000000..dae8ff1
--- /dev/null
+++ b/GinSkeleton/docs/project_analysis_3.md
@@ -0,0 +1,99 @@
+## GoSkeleton 项目骨架性能分析报告(三)
+> 1.内存分析篇我们原计划分为2篇:主线逻辑和操作数据库部分,但是经过测试发现,如果不操作数据库处理大量数据,主线逻辑基本不占用内存,根本就采集不到有效数据.
+> 2.基于第一条因素,我们将内存占用分析限定在操作数据库代码段,分析相关代码段内存占用,得出可视化的性能分析报告。
+
+
+### 操作数据库, 我们需要做如下铺垫代码
+> 1.我们本次分析的核心是在数据库操作部分, 因此我们在路由出添加如下代码,访问路由即可触发数据库的调用.
+```code
+ router.GET("/", func(context *gin.Context) {
+ // 默认路由处直接触发数据库调用
+ if model.CreateTestFactory("").SelectDataMultiple() {
+ context.String(200,"批量查询数据OK")
+ } else {
+ context.String(200,"批量查询数据出错")
+ }
+ context.String(http.StatusOK, "Api 模块接口 hello word!")
+ })
+```
+
+> 2.操作数据库部分代码,主要逻辑是每次查询1000条,循环查询了500次,每一次将结果存储在变量,并且在最后一次输出了结果集.
+ ```code
+// 超多数据批量查询的正确姿势
+func (t *Test) SelectDataMultiple() bool {
+ // 如果您要亲自测试,请确保相关表存在,并且有数据
+ sql := `
+ SELECT
+ code,name,company_name,concepts,indudtry,province,city,introduce,created_at
+ FROM
+ db_stocks.tb_code_list
+ LIMIT 0, 1000 ;
+ `
+ //1.首先独立预处理sql语句,无参数
+ if t.PrepareSql(sql) {
+ // 你可以模拟插入更多条数据,例如 1万+
+ var code, name, company_name, concepts, indudtry, province, city, introduce, created_at string
+
+ type Column struct {
+ Code string `json:"code"`
+ Name string `json:"name"`
+ Company_name string `json:"company_name"`
+ Concepts string `json:"concepts"`
+ Indudtry string `json:"indudtry"`
+ Province string `json:"province"`
+ City string `json:"city"`
+ Introduce string `json:"introduce"`
+ Created_at string `json:"created_at"`
+ }
+
+
+ for i := 1; i <= 500; i++ {
+ var nColumn = make([]Column, 0)
+ //2.执行批量查询
+ rows := t.QuerySqlForMultiple()
+ if rows == nil {
+ variable.ZapLog.Sugar().Error("sql执行失败,sql:", sql)
+ return false
+ } else {
+ for rows.Next() {
+ _ = rows.Scan(&code, &name, &company_name, &concepts, &indudtry, &province, &city, &introduce, &created_at)
+ oneColumn := Column{
+ code,
+ name,
+ company_name,
+ concepts,
+ indudtry,
+ province,
+ city,
+ introduce,
+ created_at,
+ }
+ nColumn = append(nColumn, oneColumn)
+
+ }
+ //// 我们只输出最后一行数据
+ if i == 500 {
+ fmt.Println("循环结束,最终需要返回的结果成员数量:",len(nColumn))
+ fmt.Printf("%#+v\n",nColumn)
+ }
+ }
+ rows.Close()
+ }
+ }
+ variable.ZapLog.Info("批量查询sql执行完毕!")
+ return true
+}
+
+ ```
+### 内存占用 底层数据采集步骤
+> 1.浏览器访问pprof接口:`http://127.0.0.1:20191/debug/pprof/heap?seconds=30`, 该过程会持续 30 秒,采集本进程内存变化数据.
+> 2.新开浏览器窗口,输入 `http://127.0.0.1:20191/` 刷新,触发路由中的数据库操作代码, 等待被 pprof 采集数据.
+> 3.稍等片刻,30秒之后,您点击过的步骤1就会提示下载文件:`heap-delta`, 请保存在您能记住的路径中,稍后马上使用该文件(heap-delta), 至此内存占用数据已经采集完毕.
+
+### 内存占用数据分析步骤
+> 1.首先下载安装 [graphviz](https://www.graphviz.org/download/) ,根据您的系统选择相对应的版本安装,安装完成记得将`安装目录/bin`, 加入系统环境变量.
+> 2.打开cmd窗口,执行 `dot -V` ,会显示版本信息,说明安装已经OK, 那么继续执行 `dot -c` 安装图形显示所需要的插件.
+> 3.我们已经得到了 `heap-delta` 文件,那么就在同目录打开cmd窗口,执行 `go tool pprof -inuse_space heap-delta`, 然后输入 `web` 回车就会自动打开浏览器,展示给您如下结果图:
+
+### 报告详情参见如下图
+![内存占用分析](https://www.ginskeleton.com/images/sql_memory.png)
diff --git a/GinSkeleton/docs/project_struct.md b/GinSkeleton/docs/project_struct.md
new file mode 100644
index 0000000..951f387
--- /dev/null
+++ b/GinSkeleton/docs/project_struct.md
@@ -0,0 +1,55 @@
+### 项目结构目录介绍
+> 1.主要介绍本项目骨架的核心目录结构
+
+```code
+|-- app
+| |-- aop // Aop切面demo代码段
+| | `-- users
+| |-- core // 程序容器部分、用于表单参数器注册、配置文件存储等
+| | |-- container
+| | |-- destroy
+| | `-- event_manage
+| |-- global // 全局变量以及常量、程序运行错误定义
+| | |-- consts
+| | |-- my_errors
+| | `-- variable
+| |-- http // http相关代码段,主要为控制器、中间件、表单参数验证器
+| | |-- controller
+| | |-- middleware
+| | `-- validator
+| |-- model // 数据库表模型
+| | |-- base_model.go
+| | `-- users.go
+| |-- service
+| | |-- sys_log_hook
+| `-- utils // 第三方包封装层
+| |-- gorm_v2
+| |-- ... ...
+|-- bootstrap // 项目启动初始化代码段
+| `-- init.go
+|-- cmd // 项目入口,分别为门户站点、命令模式、web后端入口文件
+| |-- api
+| | `-- main.go
+| |-- cli
+| | `-- main.go
+| `-- web
+| `-- main.go
+|-- command // cli模式代码目录
+| |--
+|-- config // 项目、数据库参数配置
+| |-- config.yml
+| `-- gorm_v2.yml
+|-- database
+|-- docs // 项目文档
+| |--
+|-- go.mod
+|-- go.sum
+|-- public
+|-- routers // 后台和门户网站路由
+| |-- api.go
+| `-- web.go
+|-- storage // 日志、资源存储目录
+| `--
+`-- test// 单元测试目录
+ |--
+```
\ No newline at end of file
diff --git a/GinSkeleton/docs/rabbitmq.md b/GinSkeleton/docs/rabbitmq.md
new file mode 100644
index 0000000..caa40a6
--- /dev/null
+++ b/GinSkeleton/docs/rabbitmq.md
@@ -0,0 +1,180 @@
+### 消息队列(RabbitMq)概要
+> 1.本文档主要按照本人的理解介绍RabbitMq的功能、如何使用。
+> 2.关于RabbitMq的各种使用场景以及与其他同类产品的横向、纵向对比请自行百度。
+> 3.消息队列看起来貌似非常复杂,感觉很麻烦,其实通过本项目骨架封装之后,使用非常简单,开发者安装rabbitmq(类似安装mysql),配置好账号、密码、端口即可快速使用.
+> 4.消息队列的两个核心角色:生产者(通常是一次性投递消息),消费者(需要一直处于阻塞状态监听、接受、处理消息)。
+> 5.关于消费者如何启动问题:
+> (a)开发完成消费者代码,在程序启动处(BootStrap/Init.go)通过导入包初始化形式启动(该模式相当于与本项目骨架捆绑启动)。
+> (b)程序`cmd`目录创建相关功能分类、入口文件,调用相关的消费者程序,独立编译、启动。
+> (c)本项目骨架引入了`cobra`包,同样可以做到独立编译启动。
+
+### 快速安装步骤(基于docker)
+> 1.比较详细的安装的参考地址:http://note.youdao.com/noteshare?id=3d8850a96ed288a0ae5c5421206b0f4e&sub=62EAE38FE217410E8D70859A152BCF8F
+> 2.安装rabbitMq可以理解为安装一个mysql,默认创建的账号可以理解为 root,可以直接操作rabbitmq.
+> 3.为了项目更安全,可以登录后台地址(`http://IP:15672`),自行为项目创建虚拟主机(类似mysql的数据库)、账号,最后将账号允许的操作虚拟进行绑定即可.
+
+### RabbitMq常用的几种模式
+![全场景图](https://www.ginskeleton.com/images/rabbitmq.jpg)
+#### 1.`hello_world`模式(最基本模式), 特点如下:
+> 1 一个生产者(producer)、一个消费者(consumer)通过队列(queue)进行 **一对一** 的数据传输。
+> 2.使用非常简单,适合简单业务场景使用,相关的场景模型图:
+> ![场景图](https://www.ginskeleton.com/images/helloworld.png)
+
+#### 2.`WorkQueue`模式(在消费者之间按照竞争力分配任务), 特点如下:
+> 1 生产者(producer)、多个消费者(consumer)通过队列(queue)进行**一对多、多对多**的数据传输。
+> 2.生产者(producer)将消息发布到交换机(exchange)的某个队列(queue),多个消费者(consumer)其中只要有一个消费(取走)了消息,那么其他消费者(consumer)将不会重复获得。
+> 3.消费者支持设置更多的参数,使配置强的消费者可以多处理消息,配置低的可以少处理消息,做到尽其所能,资源最大化利用。
+> ![场景图](https://www.ginskeleton.com/images/workqueue.png)
+
+#### 3.`publish/subscribe`模式(同时向许多消费者发送消息), 特点如下:
+> 1 生产者(producer)、多个消费者(consumer)通过队列(queue)进行**一对多、多对多**的数据传输。
+> 2.生产者(producer)将消息发布到交换机(exchange)的某个队列(queue),多个消费者(consumer)处理消息。
+> 3.该模式也叫作广播(broadcast)、扇形(fanout)、发布/订阅模式,消费者(consumer)可以通过配置,接收来自生产者(consumer)发送的全部消息;或者每种消费者只接收指定队列的消息,将生产者发送的消息进行分类(按照不同的队列)处理。
+> ![场景图](https://www.ginskeleton.com/images/fanout.png)
+
+#### 4.`routing`模式(有选择性地接收消息), 特点如下:
+> 1 生产者(producer)、多个消费者(consumer)通过队列(queue)进行**一对多、多对多**的数据传输。
+> 2.生产者(producer)将消息发布到交换机(exchange)已经绑定好路由键的某个队列(queue),多个消费者(consumer)可以通过绑定的路由键获取消息、处理消息。
+> 3.该模式下,消息的分类应该应该明确、种类数量不是非常多,那么就可以指定路由键(key)、绑定的到交换器的队列实现消息精准投递。
+> ![场景图](https://www.ginskeleton.com/images/routing.png)
+
+#### 5.`topics`模式(基于主题接收消息), 特点如下:
+> 1 该模式就是`routing`模式的加强版,由原来的路由键精确匹配模式升级现在的模糊匹配模式。
+> 2.语法层面主要表现为灵活的匹配规则:
+> 2.1 # 表示匹配一个或多个任意字符;
+> 2.2 *表示匹配一个字符;
+> 2.3 .(点)本身无实际意义,不表示任何匹配规则,主要用于将关键词分隔开,它的左边或右边可以写匹配规则,例如:abc.# 表示匹配abc张三、abc你好等;#.abc.# 表示匹配路由键中含有abc的字符;
+> 3.注意:匹配语法中如果没有 .(点),那么匹配规则是无效的,例如:orange#,可能本意是匹配orange任意字符,实际上除了匹配 orange#本身之外,什么也匹配不到。
+> ![场景图](https://www.ginskeleton.com/images/topics.png)
+
+#### 6.`RPC`模式(请求、回复), 特点如下:
+> 1 严格地说,该模式和消息队列没有什么关系,通常是微服务场景才会使用远程过程调用(RPC),本功能建议自行学习或者选择专业的微服务框架使用,解决实际问题,本文档不做介绍。
+> ![场景图](https://www.ginskeleton.com/images/rpc.png)
+
+### RabbitMq快速使用指南
+> 1.建议使用docker 快速安装使用即可,安装步骤请自行搜索。
+> 2.详细使用指南参见单元测试demo代: [rabbitmq全量单元测试](../test/rabbitmq_test.go)
+> 3.六种场景模型我们封装了统一的使用规范。
+
+#### 1.hello_world、work_queue、publish_subscribe 场景模型使用:
+> 相关配置参见:config/config.yaml, rbbitmq 部分
+##### 1.1 启动一个消费者,通过回调函数在阻塞模式进行消息处理
+```go
+consumer, err := HelloWorld.CreateConsumer()
+ if err != nil {
+ fmt.Printf("HelloWorld单元测试未通过。%s\n", err.Error())
+ os.Exit(1)
+ }
+
+ // 连接关闭的回调,主要是记录错误,进行后续更进一步处理,不要尝试在这里编写重连逻辑
+ // 本项目已经封装了完善的消费者端重连逻辑,触发这里的代码说明重连已经超过了最大重试次数
+ consumer.OnConnectionError(func(err *amqp.Error) {
+ log.Fatal(MyErrors.ErrorsRabbitMqReconnectFail + "\n" + err.Error())
+ })
+
+ // 进入阻塞状态,处理消息
+ consumer.Received(func(received_data string) {
+ fmt.Printf("HelloWorld回调函数处理消息:--->%s\n", received_data)
+ })
+```
+##### 1.2 调用生产者投递一个或者多个消息,投递通常都是一次性的。
+```go
+ // 这里创建场景模型的时候通过不同的模型名称创建即可,主要有:hello_world、work_queue、publish_subscribe
+ hello_producer, _ := hello_world.CreateProducer()
+ var res bool
+ for i := 0; i < 10; i++ {
+ str := fmt.Sprintf("%d_hello_world开始发送消息测试", (i + 1))
+ res = hello_producer.Send(str)
+ //time.Sleep(time.Second * 1)
+ }
+
+ hello_producer.Close() // 消息投递结束,必须关闭连接
+ // 简单判断一下最后一次发送结果
+ if res {
+ fmt.Printf("消息发送OK")
+ } else {
+ fmt.Printf("消息发送 失败")
+ }
+
+```
+
+#### 2.routing、topics 场景模型使用:
+> `routing`模式属于路由键的严格匹配模式。
+> `topics`模式比`routing`模式更灵活,两者使用、功能几乎完全一致。该模式完全可以代替`routing`模式,因此这里仅介绍 `topics`模式。
+> 注意:生产者设置键的规则必须是:关键词A.关键词B.关键词C等,即关键词之间必须使用.(点)隔开,消费者端只需要将.(点)左边或右边的关键词使用#代替即可。
+
+##### 2.1 启动多个消费者,处于阻塞模式进行消息接受、处理。
+```go
+ // 启动第一个消费者,这里使用协程的目的主要是保证第一个启动后不阻塞,否则就会导致第二个消费者无法启动
+ go func(){
+ consumer, err := Topics.CreateConsumer()
+
+ if err != nil {
+ t.Errorf("Routing单元测试未通过。%s\n", err.Error())
+ os.Exit(1)
+ }
+
+ // 连接关闭的回调,主要是记录错误,进行后续更进一步处理,不要尝试在这里编写重连逻辑
+ // 本项目已经封装了完善的消费者端重连逻辑,触发这里的代码说明重连已经超过了最大重试次数
+ consumer.OnConnectionError(func(err *amqp.Error) {
+ log.Fatal(MyErrors.ErrorsRabbitMqReconnectFail + "\n" + err.Error())
+ })
+
+ // 通过route_key 模糊匹配队列路由键的消息来处理
+ consumer.Received("#.even", func(received_data string) {
+ fmt.Printf("模糊匹配偶数键:--->%s\n", received_data)
+ })
+ }()
+
+ // 启动第二个消费者,这里没有使用协程,在消息处理环节程序就会阻塞等待,处理消息
+ consumer, err := Topics.CreateConsumer()
+
+ if err != nil {
+ t.Errorf("Routing单元测试未通过。%s\n", err.Error())
+ os.Exit(1)
+ }
+
+ consumer.OnConnectionError(func(err *amqp.Error) {
+ // 连接关闭的回调,主要是记录错误,进行后续更进一步处理,不要尝试在这里编写重连逻辑
+ // 本项目已经封装了完善的消费者端重连逻辑,触发这里的代码说明重连已经超过了最大重试次数
+ log.Fatal(MyErrors.ErrorsRabbitMqReconnectFail + "\n" + err.Error())
+ })
+
+ // 通过route_key 模糊匹配队列路由键的消息来处理
+ consumer.Received("#.odd", func(received_data string) {
+
+ fmt.Printf("模糊匹配奇数键:--->%s\n", received_data)
+ })
+
+```
+
+##### 2.2 调用生产者投递一个或者多个消息
+```go
+
+ producer, _ := Topics.CreateProducer()
+ var res bool
+ var key string
+ for i := 1; i <= 10; i++ {
+
+ // 将 偶数 和 奇数 分发到不同的key,消费者端,启动两个也各自处理偶数和奇数
+ if i%2 == 0 {
+ key = "key.even" // 偶数键
+ } else {
+ key = "key.odd" // 奇数键
+ }
+ str_data := fmt.Sprintf("%d_Routing_%s, 开始发送消息测试", i, key)
+ res = producer.Send(key, str_data)
+ //time.Sleep(time.Second * 1)
+ }
+
+ producer.Close() // 消息投递结束,必须关闭连接
+
+ // 简单判断一下最后一次发送结果
+ if res {
+ fmt.Printf("消息发送OK")
+ } else {
+ fmt.Printf("消息发送 失败")
+ }
+ //Output: 消息发送OK
+
+```
\ No newline at end of file
diff --git a/GinSkeleton/docs/sql_stament.md b/GinSkeleton/docs/sql_stament.md
new file mode 100644
index 0000000..de6447e
--- /dev/null
+++ b/GinSkeleton/docs/sql_stament.md
@@ -0,0 +1,50 @@
+### Sql操作命令集合
+>本文档主要介绍了sql操作的核心命令,详细操作命令示例代码参见 [mysql示例文档](../app/model/test.go). [sqlserver测试用例](../test/db_sqlserver_test.go) , [postgreSql测试用例](../test/db_postgresql_test.go) 操作方式同 mysql .
+
+#### 1.查询类: 不会修改数据的sql、存储过程、视图
+```sql
+ // 首先获取一个数据连接
+ sqlservConn := sql_factory.GetOneSqlClient("postgre") // 参数为空,默认就是mysql驱动,您还可以传递 sqlserver 、 postgresql 参数获取对应数据库的一个连接.
+ #1.多条查询:
+ sqlservConn.QuerySql
+ #2.单条查询:
+ sqlservConn.QueryRow
+```
+
+#### 2.执行类: 会修改数据的sql、存储过程等
+```sql
+ #1.执行命令,主要有 insert 、 updated 、 delete
+ sqlservConn.ExecuteSql
+```
+
+#### 3.预处理类:如果场景需要批量插入很多条数据,那么就需要独立调用预编译
+> 1.如果你的sql语句需要循环插入1万、5万、10万+数据。
+> 2.那么可能会报错: Error 1461: Can't create more than max_prepared_stmt_count statements (current value: 16382)
+> 3.此时需要以下解决方案
+```sql
+ #1.预编译,预处理类之后,执行批量语句
+ sqlservConn.PrepareSql
+ #2.(多条)执行类
+ sqlservConn.ExecuteSqlForMultiple
+ #3.(多条)查询类
+ sqlservConn.QuerySqlForMultiple
+```
+
+#### 4.事务类操作
+```sql
+ #1.开启一个事务
+ tx:=sqlservConn.BeginTx()
+
+ #2.预编译sql
+ tx.Prepare
+
+ #3.执行sql
+ tx.Exec
+
+ #4.提交
+ tx.Commit
+
+ #5.回滚
+ tx.Rollback
+```
+
\ No newline at end of file
diff --git a/GinSkeleton/docs/supervisor.md b/GinSkeleton/docs/supervisor.md
new file mode 100644
index 0000000..238230a
--- /dev/null
+++ b/GinSkeleton/docs/supervisor.md
@@ -0,0 +1,77 @@
+### Supervisor 部署
+
+`Supervisor` 是 `Linux/Unix` 系统下的一个进程管理工具,可靠稳定,很多著名框架的进程守护都推荐使用该软件。
+
+#### 安装 Supervisor
+> 这里仅举例 `CentOS` 系统下的安装方式:
+
+```bash
+# 安装 epel 源,如果此前安装过,此步骤跳过
+yum install -y epel-release
+yum install -y supervisor // 【ubutu】apt-get install supervisor
+```
+
+#### 创建一个配置文件
+```bash
+cp /etc/supervisord.conf /etc/supervisord.d/supervisord.conf
+
+#编辑刚才新复制的配置文件
+vim /etc/supervisord.d/supervisord.conf
+
+# 在[include]节点前添加以下内容,保存
+
+[program:GoSkeleton]
+# 设置命令在指定的目录内执行
+directory=/home/wwwroot/GoProject2020/goskeleton/
+#例如,我们编译完以后的go程序名为:main
+command= /bin/bash -c ./main
+user=root
+# supervisor 启动时自动该应用
+autostart=true
+# 进程退出后自动重启进程
+autorestart=true
+# 进程持续运行多久才认为是启动成功
+startsecs = 5
+# 启动重试次数
+startretries = 3
+#指定日志目录(将原来在调试输出界面的内容统一写到指定文件)
+stdout_logfile=/home/wwwroot/GoProject2020/Storage/logs/out.log
+stderr_logfile=/home/wwwroot/GoProject2020/Storage/logs/err.log
+
+```
+
+
+
+#### 配置 `Supervisor` 可视化管理界面
+> 1.编辑配置文件 /etc/supervisord.d/supervisord.conf ,将以下注释打开即可。
+```ini
+[inet_http_server]
+port=0.0.0.0:9001
+#设置可视化管理账号
+username=user_name
+#设置可视化管理密码
+password=user_pass
+```
+
+
+#### 启动 Supervisor
+```jsunicoderegexp
+supervisord -c /etc/supervisord.d/supervisord.conf
+```
+
+#### 使用 supervisorctl 命令管理项目
+> 此时你也可以通过浏览器打开 `ip:9001` 地址,输入账号、密码对应用程序进行可视化管理。
+```bash
+# 启动 Goskeleton 应用
+supervisorctl start Goskeleton
+# 重启 GoSkeleton 应用
+supervisorctl restart Goskeleton
+# 停止 GoSkeleton 应用
+supervisorctl stop Goskeleton
+# 查看所有被管理项目运行状态
+supervisorctl status
+# 重新加载配置文件,一般是增加了新的项目节点,执行此命令即可使新项目运行起来而不影响老项目
+supervisorctl update
+# 重新启动所有程序
+supervisorctl reload
+```
diff --git a/GinSkeleton/docs/validator.md b/GinSkeleton/docs/validator.md
new file mode 100644
index 0000000..43fb671
--- /dev/null
+++ b/GinSkeleton/docs/validator.md
@@ -0,0 +1,58 @@
+### validator 表单参数验证器语法介绍
+> 1.本篇将选取表单参数验证器( `https://github.com/go-playground/validator` )主要语法进行介绍,方便本项目骨架使用者快速上手.
+> 2.更详细的语法参与参见官方文档:`https://godoc.org/github.com/go-playground/validator`
+
+#### 1.我们以用户注册代码块为例进行介绍.
+> 1.[用户注册代码详情](../app/http/validator/web/users/register.go), 摘取表单参数验证部分.
+> 2.以下语法虽然看似简单,实际上已经覆盖了绝大部分常用场景的需求.
+```code
+// 给出一些最常用的验证规则:
+//required 必填;
+//len=11 长度=11;
+//min=3 如果是数字,验证的是数据大小范围,最小值为3,如果是文本,验证的是最小长度为3,
+//max=6 如果是数字,验证的是数字最大值为6,如果是文本,验证的是最大长度为6
+//mail 验证邮箱
+//gt=3 对于文本就是长度>=3
+//lt=6 对于文本就是长度<=6
+
+
+type Register struct {
+ // 必填、文本类型,表示它的长度>=1
+ UserName string `form:"user_name" json:"user_name" binding:"required,min=1"`
+
+ //必填,密码长度范围:【6,20】闭区间
+ Pass string `form:"pass" json:"pass" binding:"required,min=6,max=20"`
+
+ // 验证码,必填,长度等于:4
+ //Captcha string `form:"captcha" json:"captcha" binding:"required,len=4"`
+
+ // 年龄,必填,数字类型,大小范围【1,200】闭区间
+ //Age float64 `form:"age" json:"age" binding:"required,min=1,max=200"`
+
+ // 状态:必填,数字类型,大小范围:【0,1】 闭区间 ,
+ // 注意: 如果你的表单参数含有0值是允许提交的,必须用指针类型(*float64),而 float64 类型则认为 0 值不合格
+ Status *float64 `form:"status" json:"status" binding:"required,min=0,max=1"`
+}
+
+// 注意:这里的接收器 r,必须是 r Register, 绝对不能是 r *Register
+// 因为在 ginskeleton 里面表单参数验证器是注册在容器的代码段,
+// 如果是指针,带参数的接口请求,就会把容器的原始代码污染。
+func (r Register) CheckParams(context *gin.Context) {
+ // context.ShouldBind(&r) 则自动绑定 form-data 提交的表单参数
+ if err := context.ShouldBind(&r); err != nil {
+
+ // 省略非验证器逻辑代码....
+ // ... ...
+
+ }
+
+ // 如果您的客户端的数据是以json格式提交(popstman中的raw格式),那么就用如下语法
+ // context.ShouldBindJson(&r) 则自动绑定 json格式提交的参数
+
+}
+
+```
+
+#### 2.以上语法特别说明.
+> 1.对于数字类型(int8、int、int64、float32、float64等)我们统一使用 float64、*float64 接受.
+> 2.如果您的业务要求数字格式为 int类型,那么使用 int() 等数据类型转换函数自行转换即可.
diff --git a/GinSkeleton/docs/websocket.md b/GinSkeleton/docs/websocket.md
new file mode 100644
index 0000000..f983e1e
--- /dev/null
+++ b/GinSkeleton/docs/websocket.md
@@ -0,0 +1,109 @@
+### websocket
+
+##### 1.基本用法
+> 以下代码展示的是每一个 websocket 客户端连接到服务端所拥有的功能
+- [相关代码位置](../app/service/websocket/ws.go)
+```code
+package websocket
+
+import (
+ "fmt"
+ "github.com/gin-gonic/gin"
+ "github.com/gorilla/websocket"
+ "go.uber.org/zap"
+ "goskeleton/app/global/my_errors"
+ "goskeleton/app/global/variable"
+ "goskeleton/app/utils/websocket/core"
+)
+
+/**
+websocket模块相关事件执行顺序:
+1.onOpen
+2.OnMessage
+3.OnError
+4.OnClose
+*/
+
+type Ws struct {
+ WsClient *core.Client
+}
+
+// onOpen 基本不需要做什么
+func (w *Ws) OnOpen(context *gin.Context) (*Ws, bool) {
+ if client, ok := (&core.Client{}).OnOpen(context); ok {
+ w.WsClient = client
+ go w.WsClient.Heartbeat() // 一旦握手+协议升级成功,就为每一个连接开启一个自动化的隐式心跳检测包
+ return w, true
+ } else {
+ return nil, false
+ }
+}
+
+// OnMessage 处理业务消息
+func (w *Ws) OnMessage(context *gin.Context) {
+ go w.WsClient.ReadPump(func(messageType int, receivedData []byte) {
+ //参数说明
+ //messageType 消息类型,1=文本
+ //receivedData 服务器接收到客户端(例如js客户端)发来的的数据,[]byte 格式
+
+ tempMsg := "服务器已经收到了你的消息==>" + string(receivedData)
+ // 回复客户端已经收到消息;
+ if err := w.WsClient.SendMessage(messageType, tempMsg); err != nil {
+ variable.ZapLog.Error("消息发送出现错误", zap.Error(err))
+ }
+
+ }, w.OnError, w.OnClose)
+}
+
+// OnError 客户端与服务端在消息交互过程中发生错误回调函数
+func (w *Ws) OnError(err error) {
+ w.WsClient.State = 0 // 发生错误,状态设置为0, 心跳检测协程则自动退出
+ variable.ZapLog.Error("远端掉线、卡死、刷新浏览器等会触发该错误:", zap.Error(err))
+ //fmt.Printf("远端掉线、卡死、刷新浏览器等会触发该错误: %v\n", err.Error())
+}
+
+// OnClose 客户端关闭回调,发生onError回调以后会继续回调该函数
+func (w *Ws) OnClose() {
+
+ w.WsClient.Hub.UnRegister <- w.WsClient // 向hub管道投递一条注销消息,由hub中心负责关闭连接、删除在线数据
+}
+
+//获取在线的全部客户端
+func (w *Ws) GetOnlineClients() {
+
+ fmt.Printf("在线客户端数量:%d\n", len(w.WsClient.Hub.Clients))
+}
+
+// (每一个客户端都有能力)向全部在线客户端广播消息
+func (w *Ws) BroadcastMsg(sendMsg string) {
+ for onlineClient := range w.WsClient.Hub.Clients {
+
+ //获取每一个在线的客户端,向远端发送消息
+ if err := onlineClient.SendMessage(websocket.TextMessage, sendMsg); err != nil {
+ variable.ZapLog.Error(my_errors.ErrorsWebsocketWriteMgsFail, zap.Error(err))
+ }
+ }
+}
+
+
+```
+
+
+##### 2.在本项目骨架任意位置,向所有在线的 websocet 客户端广播消息
+> 核心原理:每一个 websocket 客户端都有一个 Hub 结构体,而这个结构体是本项目骨架设置的全局值,因此在任意位置创建一个 websocket 客户端,只要将 Hub 值赋予全局初始化的:variable.WebsocketHub,就可以在任意位置进行广播消息.
+```code
+package demo1
+
+import (
+ serviceWs "goskeleton/app/service/websocket"
+)
+
+// 省略其他无关代码,相关的核心代码如下
+
+if WsHub, ok := variable.WebsocketHub.(*core.Hub); ok {
+ // serviceWs 为 app/service/websocket 的别名
+ ws := serviceWs.Ws{WsClient: &core.Client{Hub: WsHub}}
+ ws.BroadcastMsg("本项目骨架任意位置,使用本段代码对在线的 ws 客户端广播消息")
+}
+
+```
\ No newline at end of file
diff --git a/GinSkeleton/docs/ws_js_client.md b/GinSkeleton/docs/ws_js_client.md
new file mode 100644
index 0000000..83c09a1
--- /dev/null
+++ b/GinSkeleton/docs/ws_js_client.md
@@ -0,0 +1,79 @@
+## websocket js 客户端
+
+### 前言
+> ws地址: ws://127.0.0.1:20201/admin/ws?token=sdsdsdsdsdsdsdsdsdsdsdsdssdsd
+> 由于中间模拟校验了token参数,请自行随意提交超过20个字符
+> 以下代码保存为 `ws.html` 在浏览器直接访问即可连接服务端
+> ws服务默认未开启,请自行在配置文件 config/config.yml ,找到 websocket 选项,开启即可.
+```html
+
+
+
+
+
+ websocket client
+
+
+
+
+
+
+
+
websocket client 测试代码
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
\ No newline at end of file
diff --git a/GinSkeleton/docs/zap_log.md b/GinSkeleton/docs/zap_log.md
new file mode 100644
index 0000000..d07a19b
--- /dev/null
+++ b/GinSkeleton/docs/zap_log.md
@@ -0,0 +1,82 @@
+### 日志功能, 基于 zap + lumberjack 实现
+> 1.特点:高性能、极速,功能:实现日志的标准管理、日志文件的自动分隔备份.
+> 2.该日志在项目骨架启动时我们封装了全局变量(variable.ZapLog),直接调用即可,底层按照官方标准封装,使用者调用后不需要关闭日志,也不需要担心全局变量写日志存在并发冲突问题,底层会自动加锁后再写。
+> 3.相关包 github 地址:https://github.com/uber-go/zap 、 https://github.com/natefinch/lumberjack
+
+
+### 前言
+> 1.日志相关的配置参见,config目录内的config.yml文件,Logs 部分,程序默认处于`AppDebug|调试模式`,日志输出在console面板,编译时记得切换模式。
+> 2.本文档列举几种最常用的用法, 想要深度学习请参考相关的 github 地址.
+
+### 日志处理, 标准函数
+> 参数一:文本型
+> 参数二:可变参数,可传递0个或者多个 Field 类型参数,Field 类型传递规则参见下文
+```code
+> 1. Debug(参数一, 参数二) , 调试级别,会产生大量日志,只在开发模式(AppDebug=true)会输出日志打印在console面板,生产模式该函数禁用。
+> 2. Info(参数一, 参数二) , 一般信息,默认级别。
+> 3. Warn(参数一, 参数二) , 警告
+> 4. Panic(参数一, 参数二)、Dpanic(参数一, 参数二) , 恐慌、宕机,不建议使用
+> 5. Error(参数一, 参数二) , 错误
+> 6. Fatal(参数一, 参数二) , 致命错误,会导致程序进程退出。
+```
+
+### 标准函数的参数二 Field 类型,最常用传递方式
+> 1.Int 类型 : zap.Int("userID",2019) , 同类的还有 int16 、 int32等
+> 2.String 类型 : zap.String("userID","2019")
+> 3.Error 类型 : zap.Error(v_err) , v_err 为 error(错误类型),例如使用 v_err:= error.New("模拟一个错误")
+> 4.Bool 类型 : zap.Bool("is_ok",true)
+
+
+#### 用法 1 , 高性能模式 .
+> 1.举例展示最常用用法
+```code
+ variable.ZapLog.Info("基本的运行提示类信息")
+ variable.ZapLog.Warn("UserCreate接口参数非法警告,相关参数:",zap.String("userName","demo_name"),zap.Int("userAge",18))
+ variable.ZapLog.Panic("UserDestory接口参数异常,相关参数:",zap.String("userName","demo_name"),zap.String("password","pass123456")
+ variable.ZapLog.Error("UserDestory接口参数错误,相关参数:",zap.Error(error))
+ variable.ZapLog.Fatal("Mysql初始化参数错误,退出运行。相关参数:",zap.String("name","root"), zap.Int("端口",3306))
+
+```
+
+#### 用法2 , 语法糖模式 .
+> 1.比第一种用法性能稍低,只不过基于第一种用法,相关的函数全部增加了格式化参数功能
+```code
+ # 第一种的函数后面全部添加了一个 w ,相关的函数功能和第一种一模一样
+ variable.ZapLog.Sugar().Infow("基本的运行提示类信息",zap.String("name","root"))
+
+# 格式化参数,第一种用法中的函数后面添加了一个 f
+ variable.ZapLog.Sugar().Infof("参数 userId %d\n",2020)
+
+ variable.ZapLog.Sugar().Errorw("程序发生错误",zap.Error(error))
+ variable.ZapLog.Sugar().Errorf("参数非法,程序出错,userId %d\n",2020)
+
+ Warn 、 Panic 、Fatal用法类似
+
+```
+
+#### 日志钩子
+> 1.除了本项目骨架记录日志之外,您还可以对日志进行二次加工处理.
+> 2.日志钩子函数处理位置 > `app/service/sys_log_hook/zap_log_hooks.go`
+> 3.`bootStrap/init.go` 中你可以修改钩子函数的位置
+> 相关代码位置 `app/service/sys_log_hook/zap_log_hooks.go `
+```code
+func ZapLogHandler(entry zapcore.Entry) error {
+
+ // 参数 entry 介绍
+ // entry 参数就是单条日志结构体,主要包括字段如下:
+ //Level 日志等级
+ //Time 当前时间
+ //LoggerName 日志名称
+ //Message 日志内容
+ //Caller 各个文件调用路径
+ //Stack 代码调用栈
+
+ //这里启动一个协程,hook丝毫不会影响程序性能,
+ go func(paramEntry zapcore.Entry) {
+ //fmt.Println(" GoSkeleton hook ....,你可以在这里继续处理系统日志....")
+ //fmt.Printf("%#+v\n", paramEntry)
+ }(entry)
+ return nil
+}
+
+```
\ No newline at end of file
diff --git a/GinSkeleton/go.mod b/GinSkeleton/go.mod
new file mode 100644
index 0000000..3c85695
--- /dev/null
+++ b/GinSkeleton/go.mod
@@ -0,0 +1,93 @@
+module goskeleton
+
+go 1.20
+
+require (
+ github.com/casbin/casbin/v2 v2.98.0
+ github.com/casbin/gorm-adapter/v3 v3.26.0
+ github.com/dchest/captcha v1.0.0
+ github.com/dgrijalva/jwt-go v3.2.1-0.20210802184156-9742bd7fca1c+incompatible
+ github.com/fsnotify/fsnotify v1.7.0
+ github.com/gin-contrib/pprof v1.5.0
+ github.com/gin-gonic/gin v1.10.0
+ github.com/go-playground/locales v0.14.1
+ github.com/go-playground/universal-translator v0.18.1
+ github.com/go-playground/validator/v10 v10.22.0
+ github.com/gomodule/redigo v1.9.2
+ github.com/gorilla/websocket v1.5.3
+ github.com/natefinch/lumberjack v2.0.0+incompatible
+ github.com/qifengzhang007/goCurl v1.4.0
+ github.com/rabbitmq/amqp091-go v1.10.0
+ github.com/spf13/cobra v1.8.1
+ github.com/spf13/viper v1.18.2
+ go.uber.org/zap v1.27.0
+ gorm.io/driver/mysql v1.5.7
+ gorm.io/driver/postgres v1.5.9
+ gorm.io/driver/sqlserver v1.5.3
+ gorm.io/gorm v1.25.11
+ gorm.io/plugin/dbresolver v1.5.2
+)
+
+require (
+ github.com/BurntSushi/toml v1.4.0 // indirect
+ github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394 // indirect
+ github.com/bytedance/sonic v1.11.6 // indirect
+ github.com/bytedance/sonic/loader v0.1.1 // indirect
+ github.com/casbin/govaluate v1.2.0 // indirect
+ github.com/cloudwego/base64x v0.1.4 // indirect
+ github.com/cloudwego/iasm v0.2.0 // indirect
+ github.com/dustin/go-humanize v1.0.1 // indirect
+ github.com/gabriel-vasile/mimetype v1.4.3 // indirect
+ github.com/gin-contrib/sse v0.1.0 // indirect
+ github.com/glebarez/go-sqlite v1.20.3 // indirect
+ github.com/glebarez/sqlite v1.7.0 // indirect
+ github.com/go-sql-driver/mysql v1.7.0 // indirect
+ github.com/goccy/go-json v0.10.2 // indirect
+ github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
+ github.com/golang-sql/sqlexp v0.1.0 // indirect
+ github.com/google/uuid v1.4.0 // indirect
+ github.com/hashicorp/hcl v1.0.0 // indirect
+ github.com/inconshreveable/mousetrap v1.1.0 // indirect
+ github.com/jackc/pgpassfile v1.0.0 // indirect
+ github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
+ github.com/jackc/pgx/v5 v5.5.5 // indirect
+ github.com/jackc/puddle/v2 v2.2.1 // 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.7 // indirect
+ github.com/leodido/go-urn v1.4.0 // indirect
+ github.com/magiconair/properties v1.8.7 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/microsoft/go-mssqldb v1.6.0 // indirect
+ github.com/mitchellh/mapstructure v1.5.0 // 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.2.2 // indirect
+ github.com/remyoudompheng/bigfft v0.0.0-20230126093431-47fa9a501578 // indirect
+ github.com/sagikazarmark/locafero v0.4.0 // indirect
+ github.com/sagikazarmark/slog-shim v0.1.0 // indirect
+ github.com/sourcegraph/conc v0.3.0 // indirect
+ github.com/spf13/afero v1.11.0 // indirect
+ github.com/spf13/cast v1.6.0 // indirect
+ github.com/spf13/pflag v1.0.5 // indirect
+ github.com/subosito/gotenv v1.6.0 // indirect
+ github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
+ github.com/ugorji/go/codec v1.2.12 // indirect
+ go.uber.org/multierr v1.10.0 // indirect
+ golang.org/x/arch v0.8.0 // indirect
+ golang.org/x/crypto v0.23.0 // indirect
+ golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
+ golang.org/x/net v0.25.0 // indirect
+ golang.org/x/sync v0.5.0 // indirect
+ golang.org/x/sys v0.20.0 // indirect
+ golang.org/x/text v0.15.0 // indirect
+ google.golang.org/protobuf v1.34.1 // indirect
+ gopkg.in/ini.v1 v1.67.0 // indirect
+ gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+ modernc.org/libc v1.22.2 // indirect
+ modernc.org/mathutil v1.5.0 // indirect
+ modernc.org/memory v1.5.0 // indirect
+ modernc.org/sqlite v1.20.3 // indirect
+)
diff --git a/GinSkeleton/go.sum b/GinSkeleton/go.sum
new file mode 100644
index 0000000..3007fdc
--- /dev/null
+++ b/GinSkeleton/go.sum
@@ -0,0 +1,316 @@
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.4.0/go.mod h1:ON4tFdPTwRcgWEaVDrN3584Ef+b7GgSJaXxe5fW9t4M=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1 h1:/iHxaJhsFr0+xVFfbMr5vxz848jyiWuIEDhYq3y5odY=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0 h1:vcYCAze6p19qBW7MhZybIsqD8sMV8js0NyQM8JDnVtg=
+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0/go.mod h1:OQeznEEkTZ9OrhHJoDD8ZDq51FHgXjqtP9z6bEwBq9U=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.2/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.2.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 h1:sXr+ck84g/ZlZUOZiNELInmMgOsuGwdjjVkEIde0OtY=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM=
+github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.0 h1:yfJe15aSwEQ6Oo6J+gdfdulPNoZ3TEhmbhLIoxZcA+U=
+github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.0/go.mod h1:Q28U+75mpCaSCDowNEmhIo/rmgdkqmkmzI7N6TGR4UY=
+github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v0.8.0 h1:T028gtTPiYt/RMUfs8nVsAL7FDQrfLlrm/NnRG/zcC4=
+github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v0.8.0/go.mod h1:cw4zVQgBby0Z5f2v0itn6se2dDP17nTjbZFXW5uPyHA=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0/go.mod h1:kgDmCTgBzIEPFElEF+FK0SdjAor06dRq2Go927dnQ6o=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.1.0 h1:HCc0+LpPfpCKs6LGGLAhwBARt9632unrVcI6i8s/8os=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.1.0/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
+github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
+github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
+github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394 h1:OYA+5W64v3OgClL+IrOD63t4i/RW7RqrAVl9LTZ9UqQ=
+github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394/go.mod h1:Q8n74mJTIgjX4RBBcHnJ05h//6/k6foqmgE45jTQtxg=
+github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
+github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
+github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
+github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
+github.com/casbin/casbin/v2 v2.98.0 h1:xjsnyQh1hhw5kYTZJTGh4K+pxXhPgYhcr+X7zEbEB4o=
+github.com/casbin/casbin/v2 v2.98.0/go.mod h1:G2UyxPbyyrClPvzHQ4Yog6rtTz0x+Y2lc8qOwfqWLuc=
+github.com/casbin/gorm-adapter/v3 v3.26.0 h1:4FhoNh6VqTa4CKV/B/LnwVCU073qMAFBEeQ85tlU4cc=
+github.com/casbin/gorm-adapter/v3 v3.26.0/go.mod h1:aftWi0cla0CC1bHQVrSFzBcX/98IFK28AvuPppCQgTs=
+github.com/casbin/govaluate v1.2.0 h1:wXCXFmqyY+1RwiKfYo3jMKyrtZmOL3kHwaqDyCPOYak=
+github.com/casbin/govaluate v1.2.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
+github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
+github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
+github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
+github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
+github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/dchest/captcha v1.0.0 h1:vw+bm/qMFvTgcjQlYVTuQBJkarm5R0YSsDKhm1HZI2o=
+github.com/dchest/captcha v1.0.0/go.mod h1:7zoElIawLp7GUMLcj54K9kbw+jEyvz2K0FDdRRYhvWo=
+github.com/dgrijalva/jwt-go v3.2.1-0.20210802184156-9742bd7fca1c+incompatible h1:kFnl8B5YgOXou7f+dsklKcGSXph/nubNx7I6d6RoFuE=
+github.com/dgrijalva/jwt-go v3.2.1-0.20210802184156-9742bd7fca1c+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko=
+github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
+github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
+github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
+github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
+github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
+github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
+github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
+github.com/gin-contrib/pprof v1.5.0 h1:E/Oy7g+kNw94KfdCy3bZxQFtyDnAX2V7axRS7sNYVrU=
+github.com/gin-contrib/pprof v1.5.0/go.mod h1:GqFL6LerKoCQ/RSWnkYczkTJ+tOAUVN/8sbnEtaqOKs=
+github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
+github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
+github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
+github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
+github.com/glebarez/go-sqlite v1.20.3 h1:89BkqGOXR9oRmG58ZrzgoY/Fhy5x0M+/WV48U5zVrZ4=
+github.com/glebarez/go-sqlite v1.20.3/go.mod h1:u3N6D/wftiAzIOJtZl6BmedqxmmkDfH3q+ihjqxC9u0=
+github.com/glebarez/sqlite v1.7.0 h1:A7Xj/KN2Lvie4Z4rrgQHY8MsbebX3NyWsL3n2i82MVI=
+github.com/glebarez/sqlite v1.7.0/go.mod h1:PkeevrRlF/1BhQBCnzcMWzgrIk7IOop+qS2jUYLfHhk=
+github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
+github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
+github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
+github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
+github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
+github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao=
+github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
+github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
+github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
+github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
+github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
+github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
+github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
+github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
+github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
+github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
+github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
+github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
+github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
+github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
+github.com/gomodule/redigo v1.9.2 h1:HrutZBLhSIU8abiSfW8pj8mPhOyMYjZT/wcA4/L9L9s=
+github.com/gomodule/redigo v1.9.2/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
+github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
+github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
+github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
+github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
+github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
+github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
+github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
+github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
+github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
+github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
+github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
+github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
+github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
+github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
+github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
+github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
+github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
+github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
+github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
+github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
+github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
+github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
+github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
+github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
+github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
+github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
+github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
+github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
+github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
+github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
+github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/microsoft/go-mssqldb v1.6.0 h1:mM3gYdVwEPFrlg/Dvr2DNVEgYFG7L42l+dGc67NNNpc=
+github.com/microsoft/go-mssqldb v1.6.0/go.mod h1:00mDtPbeQCRGC1HwOOR5K/gr30P1NcEG0vx6Kbv2aJU=
+github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
+github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
+github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
+github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM=
+github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk=
+github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
+github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
+github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
+github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/qifengzhang007/goCurl v1.4.0 h1:SyPxw3e8NZ/bhelabiIZPvTXDAyA3zrt4+Uq8tF5roE=
+github.com/qifengzhang007/goCurl v1.4.0/go.mod h1:uO0GEHw3DKIVMHIGw1kbtY9wUpL1eAm7hYxiCjmyvkc=
+github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw=
+github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
+github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
+github.com/remyoudompheng/bigfft v0.0.0-20230126093431-47fa9a501578 h1:VstopitMQi3hZP0fzvnsLmzXZdQGc4bEcgu24cp+d4M=
+github.com/remyoudompheng/bigfft v0.0.0-20230126093431-47fa9a501578/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
+github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
+github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
+github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
+github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
+github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
+github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
+github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
+github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
+github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
+github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
+github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
+github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
+github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
+github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
+github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
+github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
+github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
+github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
+go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
+go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
+go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
+go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
+golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
+golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
+golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
+golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
+golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
+golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
+golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
+golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
+golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
+golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
+golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
+golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
+golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
+golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
+golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
+golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
+golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
+golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
+golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
+google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
+gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
+gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
+gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
+gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
+gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8=
+gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
+gorm.io/driver/sqlserver v1.5.3 h1:rjupPS4PVw+rjJkfvr8jn2lJ8BMhT4UW5FwuJY0P3Z0=
+gorm.io/driver/sqlserver v1.5.3/go.mod h1:B+CZ0/7oFJ6tAlefsKoyxdgDCXJKSgwS2bMOQZT0I00=
+gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
+gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
+gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
+gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
+gorm.io/plugin/dbresolver v1.5.2 h1:Iut7lW4TXNoVs++I+ra3zxjSxTRj4ocIeFEVp4lLhII=
+gorm.io/plugin/dbresolver v1.5.2/go.mod h1:jPh59GOQbO7v7v28ZKZPd45tr+u3vyT+8tHdfdfOWcU=
+modernc.org/libc v1.22.2 h1:4U7v51GyhlWqQmwCHj28Rdq2Yzwk55ovjFrdPjs8Hb0=
+modernc.org/libc v1.22.2/go.mod h1:uvQavJ1pZ0hIoC/jfqNoMLURIMhKzINIWypNM17puug=
+modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
+modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
+modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
+modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
+modernc.org/sqlite v1.20.3 h1:SqGJMMxjj1PHusLxdYxeQSodg7Jxn9WWkaAQjKrntZs=
+modernc.org/sqlite v1.20.3/go.mod h1:zKcGyrICaxNTMEHSr1HQ2GUraP0j+845GYw37+EyT6A=
+nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
+rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
diff --git a/GinSkeleton/makefile b/GinSkeleton/makefile
new file mode 100644
index 0000000..9f4dd34
--- /dev/null
+++ b/GinSkeleton/makefile
@@ -0,0 +1,49 @@
+#说明:makefile 文件只能在linux系统运行,windows 系统无法执行本文件定义的相关命令
+# 使用文档参考:https://www.yuque.com/xiaofensinixidaouxiang/bkfhct/zso6xo
+
+# 定义 makefile 的命名列表, 只需要将外部调用的公布在这里即可
+.PHONY: build-api build-web build-cli help
+
+# 设置 cmd/api/main.go 入口文件编译后的可执行文件名
+apiBinName="ginskeleton-api.linux64"
+
+# 设置 cmd/web/main.go 入口文件编译后的可执行文件名
+webBinName="ginskeleton-web.linux64"
+
+# 设置 cmd/cli/main.go 入口文件编译后的可执行文件名
+cliBinName="ginskeleton-cli.linux64"
+
+# 统一设置编译的目标平台公共参数
+all:
+ go env -w GOARCH=amd64
+ go env -w GOOS=linux
+ go env -w CGO_ENABLED=0
+ go env -w GO111MODULE=on
+ go env -w GOPROXY=https://goproxy.cn,direct
+ go mod tidy
+
+build-api:all clean-api build-api-bin
+build-api-bin:
+ go build -o ${apiBinName} -ldflags "-w -s" -trimpath ./cmd/api/main.go
+
+build-web:all clean-web build-web-bin
+build-web-bin:
+ go build -o ${webBinName} -ldflags "-w -s" -trimpath ./cmd/web/main.go
+
+build-cli:all clean-cli build-cli-bin
+build-cli-bin:
+ go build -o ${cliBinName} -ldflags "-w -s" -trimpath ./cmd/cli/main.go
+
+# 编译前清理可能已经存在的旧文件
+clean-api:
+ @if [ -f ${apiBinName} ] ; then rm -rf ${apiBinName} ; fi
+clean-web:
+ @if [ -f ${webBinName} ] ; then rm -rf ${webBinName} ; fi
+clean-cli:
+ @if [ -f ${cliBinName} ] ; then rm -rf ${cliBinName} ; fi
+
+help:
+ @echo "make hep 查看编译命令列表"
+ @echo "make build-api 编译 cmd/api/main.go 入口文件 "
+ @echo "make build-web 编译 cmd/web/main.go 入口文件 "
+ @echo "make build-cli 编译 cmd/cli/main.go 入口文件 "
\ No newline at end of file
diff --git a/GinSkeleton/public/favicon.ico b/GinSkeleton/public/favicon.ico
new file mode 100644
index 0000000..fd9b939
Binary files /dev/null and b/GinSkeleton/public/favicon.ico differ
diff --git a/GinSkeleton/public/readme.md b/GinSkeleton/public/readme.md
new file mode 100644
index 0000000..abd3cf5
--- /dev/null
+++ b/GinSkeleton/public/readme.md
@@ -0,0 +1,2 @@
+#### 特别说明
+> 1.虽然`gin`框架支持静态文件处理, 但是我们建议您将静态资源交给 `nginx` 去处理,以获得极速性能.
\ No newline at end of file
diff --git a/GinSkeleton/routers/api.go b/GinSkeleton/routers/api.go
new file mode 100644
index 0000000..3fb1071
--- /dev/null
+++ b/GinSkeleton/routers/api.go
@@ -0,0 +1,74 @@
+package routers
+
+import (
+ "github.com/gin-contrib/pprof"
+ "github.com/gin-gonic/gin"
+ "go.uber.org/zap"
+ "goskeleton/app/global/consts"
+ "goskeleton/app/global/variable"
+ "goskeleton/app/http/middleware/cors"
+ validatorFactory "goskeleton/app/http/validator/core/factory"
+ "goskeleton/app/utils/gin_release"
+ "net/http"
+)
+
+// 该路由主要设置门户类网站等前台路由
+
+func InitApiRouter() *gin.Engine {
+ var router *gin.Engine
+ // 非调试模式(生产模式) 日志写到日志文件
+ if variable.ConfigYml.GetBool("AppDebug") == false {
+ //1.gin自行记录接口访问日志,不需要nginx,如果开启以下3行,那么请屏蔽第 34 行代码
+ //gin.DisableConsoleColor()
+ //f, _ := os.Create(variable.BasePath + variable.ConfigYml.GetString("Logs.GinLogName"))
+ //gin.DefaultWriter = io.MultiWriter(f)
+
+ //【生产模式】
+ // 根据 gin 官方的说明:[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
+ // 如果部署到生产环境,请使用以下模式:
+ // 1.生产模式(release) 和开发模式的变化主要是禁用 gin 记录接口访问日志,
+ // 2.go服务就必须使用nginx作为前置代理服务,这样也方便实现负载均衡
+ // 3.如果程序发生 panic 等异常使用自定义的 panic 恢复中间件拦截、记录到日志
+ router = gin_release.ReleaseRouter()
+ } else {
+ // 调试模式,开启 pprof 包,便于开发阶段分析程序性能
+ router = gin.Default()
+ pprof.Register(router)
+ }
+ // 设置可信任的代理服务器列表,gin (2021-11-24发布的v1.7.7版本之后出的新功能)
+ if variable.ConfigYml.GetInt("HttpServer.TrustProxies.IsOpen") == 1 {
+ if err := router.SetTrustedProxies(variable.ConfigYml.GetStringSlice("HttpServer.TrustProxies.ProxyServerList")); err != nil {
+ variable.ZapLog.Error(consts.GinSetTrustProxyError, zap.Error(err))
+ }
+ } else {
+ _ = router.SetTrustedProxies(nil)
+ }
+
+ //根据配置进行设置跨域
+ if variable.ConfigYml.GetBool("HttpServer.AllowCrossDomain") {
+ router.Use(cors.Next())
+ }
+
+ router.GET("/", func(context *gin.Context) {
+ context.String(http.StatusOK, "Api 模块接口 hello word!")
+ })
+
+ //处理静态资源(不建议gin框架处理静态资源,参见 Public/readme.md 说明 )
+ router.Static("/public", "./public") // 定义静态资源路由与实际目录映射关系
+ //router.StaticFile("/abcd", "./public/readme.md") // 可以根据文件名绑定需要返回的文件名
+
+ // 创建一个门户类接口路由组
+ vApi := router.Group("/api/v1/")
+ {
+ // 模拟一个首页路由
+ home := vApi.Group("home/")
+ {
+ // 第二个参数说明:
+ // 1.它是一个表单参数验证器函数代码段,该函数从容器中解析,整个代码段略显复杂,但是对于使用者,您只需要了解用法即可,使用很简单,看下面 ↓↓↓
+ // 2.编写该接口的验证器,位置:app/http/validator/api/home/news.go
+ // 3.将以上验证器注册在容器:app/http/validator/common/register_validator/api_register_validator.go 18 行为注册时的键(consts.ValidatorPrefix + "HomeNews")。那么获取的时候就用该键即可从容器获取
+ home.GET("news", validatorFactory.Create(consts.ValidatorPrefix+"HomeNews"))
+ }
+ }
+ return router
+}
diff --git a/GinSkeleton/routers/web.go b/GinSkeleton/routers/web.go
new file mode 100644
index 0000000..22efb67
--- /dev/null
+++ b/GinSkeleton/routers/web.go
@@ -0,0 +1,126 @@
+package routers
+
+import (
+ "github.com/gin-contrib/pprof"
+ "github.com/gin-gonic/gin"
+ "go.uber.org/zap"
+ "goskeleton/app/global/consts"
+ "goskeleton/app/global/variable"
+ "goskeleton/app/http/controller/captcha"
+ "goskeleton/app/http/middleware/authorization"
+ "goskeleton/app/http/middleware/cors"
+ validatorFactory "goskeleton/app/http/validator/core/factory"
+ "goskeleton/app/utils/gin_release"
+ "net/http"
+)
+
+// 该路由主要设置 后台管理系统等后端应用路由
+
+func InitWebRouter() *gin.Engine {
+ var router *gin.Engine
+ // 非调试模式(生产模式) 日志写到日志文件
+ if variable.ConfigYml.GetBool("AppDebug") == false {
+
+ //1.gin自行记录接口访问日志,不需要nginx,如果开启以下3行,那么请屏蔽第 34 行代码
+ //gin.DisableConsoleColor()
+ //f, _ := os.Create(variable.BasePath + variable.ConfigYml.GetString("Logs.GinLogName"))
+ //gin.DefaultWriter = io.MultiWriter(f)
+
+ //【生产模式】
+ // 根据 gin 官方的说明:[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
+ // 如果部署到生产环境,请使用以下模式:
+ // 1.生产模式(release) 和开发模式的变化主要是禁用 gin 记录接口访问日志,
+ // 2.go服务就必须使用nginx作为前置代理服务,这样也方便实现负载均衡
+ // 3.如果程序发生 panic 等异常使用自定义的 panic 恢复中间件拦截、记录到日志
+ router = gin_release.ReleaseRouter()
+
+ } else {
+ // 调试模式,开启 pprof 包,便于开发阶段分析程序性能
+ router = gin.Default()
+ pprof.Register(router)
+ }
+
+ // 设置可信任的代理服务器列表,gin (2021-11-24发布的v1.7.7版本之后出的新功能)
+ if variable.ConfigYml.GetInt("HttpServer.TrustProxies.IsOpen") == 1 {
+ if err := router.SetTrustedProxies(variable.ConfigYml.GetStringSlice("HttpServer.TrustProxies.ProxyServerList")); err != nil {
+ variable.ZapLog.Error(consts.GinSetTrustProxyError, zap.Error(err))
+ }
+ } else {
+ _ = router.SetTrustedProxies(nil)
+ }
+
+ //根据配置进行设置跨域
+ if variable.ConfigYml.GetBool("HttpServer.AllowCrossDomain") {
+ router.Use(cors.Next())
+ }
+
+ router.GET("/", func(context *gin.Context) {
+ context.String(http.StatusOK, "HelloWorld,这是后端模块")
+ })
+
+ //处理静态资源(不建议gin框架处理静态资源,参见 public/readme.md 说明 )
+ router.Static("/public", "./public") // 定义静态资源路由与实际目录映射关系
+ router.StaticFS("/dir", http.Dir("./public")) // 将public目录内的文件列举展示
+ router.StaticFile("/abcd", "./public/readme.md") // 可以根据文件名绑定需要返回的文件名
+
+ // 创建一个验证码路由
+ verifyCode := router.Group("captcha")
+ {
+ // 验证码业务,该业务无需专门校验参数,所以可以直接调用控制器
+ verifyCode.GET("/", (&captcha.Captcha{}).GenerateId) // 获取验证码ID
+ verifyCode.GET("/:captcha_id", (&captcha.Captcha{}).GetImg) // 获取图像地址
+ verifyCode.GET("/:captcha_id/:captcha_value", (&captcha.Captcha{}).CheckCode) // 校验验证码
+ }
+ // 创建一个后端接口路由组
+ backend := router.Group("/admin/")
+ {
+ // 创建一个websocket,如果ws需要账号密码登录才能使用,就写在需要鉴权的分组,这里暂定是开放式的,不需要严格鉴权,我们简单验证一下token值
+ backend.GET("ws", validatorFactory.Create(consts.ValidatorPrefix+"WebsocketConnect"))
+
+ // 【不需要token】中间件验证的路由 用户注册、登录
+ noAuth := backend.Group("users/")
+ {
+ // 关于路由的第二个参数用法说明
+ // 1.编写一个表单参数验证器结构体,参见代码: app/http/validator/web/users/register.go
+ // 2.将以上表单参数验证器注册,遵守 键 =》值 格式注册即可 ,app/http/validator/common/register_validator/web_register_validator.go 20行就是注册时候的键 consts.ValidatorPrefix+"UsersRegister"
+ // 3.按照注册时的键,直接从容器调用即可 :validatorFactory.Create(consts.ValidatorPrefix+"UsersRegister")
+ noAuth.POST("register", validatorFactory.Create(consts.ValidatorPrefix+"UsersRegister"))
+ // 不需要验证码即可登陆
+ noAuth.POST("login", validatorFactory.Create(consts.ValidatorPrefix+"UsersLogin"))
+
+ // 如果加载了验证码中间件,那么就需要提交验证码才可以登陆(本质上就是给登陆接口增加了2个参数:验证码id提交时的键:captcha_id 和 验证码值提交时的键 captcha_value,具体参见配置文件)
+ //noAuth.Use(authorization.CheckCaptchaAuth()).POST("login", validatorFactory.Create(consts.ValidatorPrefix+"UsersLogin"))
+
+ }
+
+ // 刷新token
+ refreshToken := backend.Group("users/")
+ {
+ // 刷新token,当过期的token在允许失效的延长时间范围内,用旧token换取新token
+ refreshToken.Use(authorization.RefreshTokenConditionCheck()).POST("refreshtoken", validatorFactory.Create(consts.ValidatorPrefix+"RefreshToken"))
+ }
+
+ // 【需要token】中间件验证的路由
+ backend.Use(authorization.CheckTokenAuth())
+ {
+ // 用户组路由
+ users := backend.Group("users/")
+ {
+ // 查询 ,这里的验证器直接从容器获取,是因为程序启动时,将验证器注册在了容器,具体代码位置:App\Http\Validator\Web\Users\xxx
+ users.GET("index", validatorFactory.Create(consts.ValidatorPrefix+"UsersShow"))
+ // 新增
+ users.POST("create", validatorFactory.Create(consts.ValidatorPrefix+"UsersStore"))
+ // 更新
+ users.POST("edit", validatorFactory.Create(consts.ValidatorPrefix+"UsersUpdate"))
+ // 删除
+ users.POST("delete", validatorFactory.Create(consts.ValidatorPrefix+"UsersDestroy"))
+ }
+ //文件上传公共路由
+ uploadFiles := backend.Group("upload/")
+ {
+ uploadFiles.POST("files", validatorFactory.Create(consts.ValidatorPrefix+"UploadFiles"))
+ }
+ }
+ }
+ return router
+}
diff --git a/GinSkeleton/storage/app/img/huawei-cloud-server-small.png b/GinSkeleton/storage/app/img/huawei-cloud-server-small.png
new file mode 100644
index 0000000..c847946
Binary files /dev/null and b/GinSkeleton/storage/app/img/huawei-cloud-server-small.png differ
diff --git a/GinSkeleton/test/gormv2_test.go b/GinSkeleton/test/gormv2_test.go
new file mode 100644
index 0000000..ddbb4cb
--- /dev/null
+++ b/GinSkeleton/test/gormv2_test.go
@@ -0,0 +1,396 @@
+package test
+
+import (
+ "fmt"
+ "goskeleton/app/global/variable"
+ "goskeleton/app/utils/gorm_v2"
+ _ "goskeleton/bootstrap"
+ "sync"
+ "testing"
+ "time"
+)
+
+// gorm v2 操作数据库单元测试
+// 单元测试为了不影响自带的数据库,我们将单元测试涉及到的表创建在了 独立的额数据库: db_ginskeleton2_test
+// 测试本篇首先保证 config/gorm_v2.yml 文件配置正确,相关配置项 IsInitGolobalGormMysql = 1 ,并且是设置连接的数据为:db_ginskeleton2_test
+// 单元测试涉及到的数据库下载地址:https://wwt.lanzoul.com/ivT3A06cq35i
+// 本文件测试到的相关数据表由于数据量较大, 最终的数据库文件没有放置在本项目骨架中,如果你动手能力很强,可以通过 issue 留言获取,重新进行测试
+// 更多使用用法参见官方文档:https://gorm.io/zh_CN/docs/v2_release_note.html
+
+// 模拟创建 3 个数据表,请在数据库按照结构体字段自行创建,字段全部使用小写
+type tb_users struct {
+ Id uint `json:"id" gorm:"primaryKey" `
+ UserName string `json:"user_name"`
+ Age uint8 `json:"age"`
+ Addr string `json:"addr"`
+ Email string `json:"email"`
+ Phone string `json:"phone"`
+ Remark string `json:"remark"`
+ CreatedAt string `json:"created_at"`
+ UpdatedAt string `json:"updated_at"`
+}
+
+func (*tb_users) TableName() string {
+ return "tb_users"
+}
+
+//角色表
+type tb_role struct {
+ Id uint `json:"id" gorm:"primaryKey" `
+ Name string `json:"name"`
+ DisplayName string `json:"display_name"`
+ Description string `json:"description"`
+ Remark string `json:"remark"`
+ CreatedAt string `json:"created_at"`
+ UpdatedAt string `json:"updated_at"`
+}
+
+func (*tb_role) TableName() string {
+ return "tb_role"
+}
+
+// 用户登录日志
+type tb_user_log struct {
+ Id int `gorm:"primaryKey" `
+ UserId int
+ Ip string
+ LoginTime string
+ Remark string
+ CreatedAt string `json:"created_at"`
+ UpdatedAt string `json:"updated_at"`
+}
+
+// 如果不自定义,默认使用的是表名的复数形式,即:Tb_user_logs
+func (*tb_user_log) TableName() string {
+ return "tb_user_log"
+}
+
+// code_list表
+type tb_code_list struct {
+ Code string
+ Name string
+ CompanyName string
+ Concepts string
+ ConceptsDetail string
+ Province string
+ City string
+ Status uint8
+ Remark string
+ CreatedAt string
+ UpdatedAt string
+}
+
+// 如果不自定义,默认使用的是表名的复数形式,即:Tb_user_logs
+func (*tb_code_list) TableName() string {
+ return "tb_code_list"
+}
+
+// 查询
+func TestGormSelect(t *testing.T) {
+ // 查询 tb_users,由于没有配置指定的主从数据库。,所以默认连接的是
+ var users []tb_users
+ var roles []tb_role
+
+ // tb_users 查询数据会从 db_test 查询, 整个语句没有指定表名,那么就会从 Find 函数参数 &users 上绑定的 tableName函数的返回值中获取表名
+ result := variable.GormDbMysql.Select("id", "user_name", "phone", "email", "remark").Where("user_name like ?", "%test%").Find(&users)
+ if result.Error != nil {
+ t.Errorf("单元测试失败,错误明细:%s\n", result.Error.Error())
+ }
+ fmt.Printf("tb_users表数据:%v\n", users)
+
+ result = variable.GormDbMysql.Where("name like ?", "%test%").Find(&roles)
+ if result.Error != nil {
+ t.Errorf("单元测试失败,错误明细:%s\n", result.Error.Error())
+ }
+ fmt.Printf("tb_roles表数据:%v\n", roles)
+}
+
+// 新增
+func TestGormInsert(t *testing.T) {
+ var usrLog = &tb_user_log{
+ UserId: 3,
+ Ip: "192.168.1.110",
+ LoginTime: time.Now().Format("2006-01-02 15:04:05"),
+ Remark: "备注信息1028",
+ CreatedAt: time.Now().Format("2006-01-02 15:04:05"),
+ UpdatedAt: time.Now().Format("2006-01-02 15:04:05"),
+ }
+
+ // 方式1:相关sql 语句: insert into Tb_user_log(user_id,ip,login_time,CreatedAt,updated_at) values(1,"192.168.1.10","当前时间","当前时间")
+ result := variable.GormDbMysql.Create(usrLog)
+ if result.RowsAffected < 0 {
+ t.Error("新增失败,错误详情:", result.Error.Error())
+ }
+
+ // 方式2:相关sql 语句: insert into Tb_user_log(user_id,ip,remark) values(1,"192.168.1.10","备注信息001")
+ result = variable.GormDbMysql.Select("user_id", "ip", "remark").Create(usrLog)
+ if result.RowsAffected < 0 {
+ t.Error("新增失败,错误详情:", result.Error.Error())
+ }
+}
+
+// 修改
+func TestGormUpdate(t *testing.T) {
+ var usrLog = tb_user_log{
+ Id: 13, // 更新操作一定要指定主键Id
+ UserId: 3,
+ Ip: "127.0.0.1",
+ LoginTime: "2008-08-08 08:08:08",
+ Remark: "整个结构体对应的字段全部更新",
+ CreatedAt: time.Now().Format("2006-01-02 15:04:05"),
+ UpdatedAt: time.Now().Format("2006-01-02 15:04:05"),
+ }
+ // 整个结构体全量更新
+ result := variable.GormDbMysql.Save(&usrLog)
+ if result.RowsAffected < 0 {
+ t.Error("update失败,错误详情:", result.Error.Error())
+ }
+
+ // 定义更新字段的map, 键值对
+ var relaValue = map[string]interface{}{
+ "user_id": 66,
+ "ip": "192.168.6.66",
+ "login_time": time.Now().Format("2006-01-02 15:04:05"),
+ "remark": "指定字段更新,备注信息",
+ }
+ // 指定字段更新,更新sql: update Tb_user_log set user_id=66,ip='192.168.6.66' , login_time='当前时间', remark='指定字段更新,备注信息' where id=11
+ result = variable.GormDbMysql.Model(&usrLog).Select("user_id", "ip", "login_time", "remark").Where("id=?", 13).Updates(relaValue)
+ if result.RowsAffected < 0 {
+ t.Error("update失败,错误详情:", result.Error.Error())
+ }
+}
+
+// 删除
+func TestGormDelete(t *testing.T) {
+ // 定义一个只带有ID 的相关表结构体
+ var key_primary_struct = tb_role{
+ Id: 4,
+ }
+ // 方法1: sql:delete from tb_role where id =4
+ result := variable.GormDbMysql.Delete(key_primary_struct)
+ if result.RowsAffected < 0 {
+ t.Error("delete失败,错误详情:", result.Error.Error())
+ }
+
+ // 方法2: sql:delete from tb_role where id =5
+ result = variable.GormDbMysql.Delete(&tb_role{}, 5)
+ if result.RowsAffected < 0 {
+ t.Error("delete失败,错误详情:", result.Error.Error())
+ }
+}
+
+// 原生sql
+
+func TestRawSql(t *testing.T) {
+
+ // 查询类
+ var receive []tb_user_log
+ variable.GormDbMysql.Raw("select * from tb_user_log where id>?", 0).Scan(&receive)
+ fmt.Printf("%v\n", receive)
+ //var dest=make([]string,0)
+ //_=sql_res_to_tree.CreateSqlResFormatFactory().ScanToTreeData(receive,&dest)
+ //执行类
+ result := variable.GormDbMysql.Exec("update tb_user_log set remark=? where id=?", "gorm原生sql执行修改操作", 17)
+ if result.RowsAffected < 0 {
+ t.Error("原生sql执行失败,错误详情:", result.Error.Error())
+ }
+}
+
+func TestBatchInsertSql(t *testing.T) {
+
+ // 查询类
+ sql := `
+INSERT INTO tb_auth_post_mount_has_menu_button(fr_auth_post_mount_has_menu_id,fr_auth_button_cn_en_id)
+SELECT 91,4 FROM DUAL WHERE NOT EXISTS(SELECT 1 FROM tb_auth_post_mount_has_menu_button a WHERE a.fr_auth_post_mount_has_menu_id=91 AND a.fr_auth_button_cn_en_id=4)
+ `
+ for i := 0; i < 100; i++ {
+ result := variable.GormDbMysql.Exec(sql)
+ if result.Error != nil {
+ t.Error("原生sql执行失败,错误详情:", result.Error.Error())
+ }
+ }
+
+}
+
+// 性能测试(大量查询计算耗时,评测性能)
+func TestUseTime(t *testing.T) {
+ //循环查询100次,每次查询3500条数据,累计查询数据量为 35 万, 计算总耗时
+ var receives []tb_code_list
+ var time1 = time.Now()
+ for i := 0; i < 100; i++ {
+ receives = make([]tb_code_list, 0)
+ variable.GormDbMysql.Model(tb_code_list{}).Select("code", "name", "company_name", "concepts_detail", "province", "city", "remark", "status", "created_at", "updated_at").Where("id<=?", 3500).Find(&receives)
+
+ }
+ fmt.Printf("gorm数据遍历完毕:最后一次条数:%d\n", len(receives))
+ //经过测试,遍历处理35万条数据,需要 1034 毫秒,不同配置的电脑耗时不一样
+ fmt.Printf("本次耗时(毫秒):%d\n", time.Now().Sub(time1).Milliseconds())
+
+ // 直接使用 gorm 的原生
+ //for i:=0;i<100;i++{
+ // receives=make([]tb_code_list,0)
+ // variable.GormDbMysql.Raw("SELECT `code`, `name`, `company_name`, `concepts`, `concepts_detail`, `province`, `city`, `remark`, `status`, `CreatedAt`, `updated_at` FROM `tb_code_list` where id<3500 ").Find(&receives)
+ //}
+ //fmt.Printf("gorm 原生sql数据遍历完毕:最后一次条数:%d\n",len(receives))
+ //// 经过测试,遍历处理35万条数据,需要 4.58 秒
+ //fmt.Printf("本次耗时(毫秒):%d\n",time.Now().Sub(time1).Milliseconds())
+}
+
+// 性能测试(并发与连接池)
+func TestCocurrent(t *testing.T) {
+ // SELECT `code`, `name`, `company_name`, `concepts`, `concepts_detail`, `province`, `city`, `remark`, `status`, `created_at`, `updated_at` FROM `tb_code_list` where id<3500;
+ var wg sync.WaitGroup
+ // 数据库的并发最大连接数建议设置为 128, 后续测试将通过测试数据验证
+ var conNum = make(chan uint16, 128)
+ wg.Add(1000)
+ time1 := time.Now()
+ for i := 1; i <= 1000; i++ {
+ conNum <- 1
+ go func() {
+ defer func() {
+ <-conNum
+ wg.Done()
+ }()
+ var received []tb_code_list
+ variable.GormDbMysql.Table("tb_code_list").Select("code", "name", "company_name", "province", "city", "remark", "status", "created_at", "updated_at").Where("id<=?", 3500).Find(&received)
+ //fmt.Printf("本次读取的数据条数:%d\n",len(received))
+ }()
+ }
+ wg.Wait()
+ fmt.Printf("耗时(ms):%d\n", time.Now().Sub(time1).Milliseconds())
+
+ // 测试结果,2022-06-13 补充,以下测试数据为 I7-4代机器,在I7-12代机器上面耗时非常少,128并发只需要 3.13 秒就完成了本单元测试
+ // 1.数据库并发在 1000 (相当于有1000个客户端连接操作数据库,可以在数据库使用 show processlist 自行实时刷新观察、验证),
+ // 2.并发设置为 1000,累计查询、返回结果的数据条数:350万. 最终耗时:(14.28s)
+ // 3.并发设置为 500,累计查询、返回结果的数据条数:350万. 最终耗时:(14.03s)
+ // 4.并发设置为 250,累计查询、返回结果的数据条数:350万. 最终耗时:(13.57s)
+ // 5.并发设置为 128,累计查询、返回结果的数据条数:350万. 最终耗时:(13.27s) // 由此可见,数据库并发性能最优值就是同时有128个连接,该值相当于抛物线的最高性能点
+ // 6.并发设置为 100,累计查询、返回结果的数据条数:350万. 最终耗时:(13.43s)
+ // 7.并发设置为 64,累计查询、返回结果的数据条数:350万. 最终耗时:(15.10s)
+
+}
+
+// 面对复杂场景,需要多个客户端连接到部署在多个不同服务器的 mysql、sqlserver、postgresql 等数据库时,
+// 由于配置文件(config/gorm_v2.yml)只提供了一份mysql连接,无法满足需求,这时您可以通过自定义参数直接连接任意数据库,获取一个数据库句柄,供业务使用
+func TestCustomeParamsConnMysql(t *testing.T) {
+ // 定义一个查询结果接受结构体
+ type DataList struct {
+ Id int
+ Username string
+ Last_login_ip string
+ Status int
+ }
+ // 设置动态参数连接任意多个数据库,以mysql为例进行单元测试
+ // 参数结构体 Write 和 Read 只有设置了具体指,才会生效,否则程序自动使用配置目录(config/gorm_v.yml)中的参数
+ confPrams := gorm_v2.ConfigParams{
+ Write: struct {
+ Host string
+ DataBase string
+ Port int
+ Prefix string
+ User string
+ Pass string
+ Charset string
+ }{Host: "127.0.0.1", DataBase: "db_test", Port: 3306, Prefix: "tb_", User: "root", Pass: "DRsXT5ZJ6Oi55LPQ", Charset: "utf8"},
+ Read: struct {
+ Host string
+ DataBase string
+ Port int
+ Prefix string
+ User string
+ Pass string
+ Charset string
+ }{Host: "127.0.0.1", DataBase: "db_stocks", Port: 3306, Prefix: "tb_", User: "root", Pass: "DRsXT5ZJ6Oi55LPQ", Charset: "utf8"}}
+
+ var vDataList []DataList
+
+ //gorm_v2.GetSqlDriver 参数介绍
+ // sqlType : mysql 、sqlserver、postgresql 等数据库库类型
+ // readDbIsOpen : 是否开启读写分离,1表示开启读数据库的配置,那么 confPrams.Read 参数部分才会生效; 0 则表示 confPrams.Read 部分参数直接忽略(即 读、写同库)
+ // confPrams 动态配置的数据库参数
+ // 此外,其他参数,例如数据库连接池等,则直接调用配置项数据库连接池参数,动态不需要配置,这部分对实际操作影响不大
+ if gormDbMysql, err := gorm_v2.GetSqlDriver("mysql", 0, confPrams); err == nil {
+ gormDbMysql.Raw("select id,username,status,last_login_ip from tb_users").Find(&vDataList)
+ fmt.Printf("Read 数据库查询结果:%v\n", vDataList)
+ res := gormDbMysql.Exec("update tb_users set real_name='Write数据库更新' where id<=2 ")
+ fmt.Printf("Write 数据库更新以后的影响行数:%d\n", res.RowsAffected)
+ }
+}
+
+//将结果集数据扫描到树形结构体
+// 定义一个树形结构体将原始sql集数据树形化
+
+// 将sql结果集扫描为树形结构数据
+// 关于sql结果树形化更多的用法参考独立的文档即可:https://gitee.com/daitougege/sql_res_to_tree
+//func TestSqlResultToTreeStruct(t *testing.T) {
+// // 定义一个原始数据集的接受结构体
+// var res1 []struct {
+// Id int
+// Name string
+// Fid int
+// }
+// sql := `
+// SELECT id,fid,name FROM tb_province_city WHERE id IN(1,25,321) OR path_info LIKE '0,1,25,321,2721%'
+// `
+//
+// variable.GormDbMysql.Raw(sql).Find(&res1)
+// fmt.Printf("%+v\n", res1)
+//
+// type ProvinceCity struct {
+// Id int `primaryKey:"yes" json:"id"`
+// Name string `json:"name"`
+// Fid int `fid:"Id" json:"fid"`
+// Children []ProvinceCity `json:"children"`
+// }
+// var dest = make([]ProvinceCity, 0)
+// if err := sql_res_to_tree.CreateSqlResFormatFactory().ScanToTreeData(res1, &dest); err == nil {
+//
+// fmt.Printf("%v\n", dest)
+// bytes, _ := json.Marshal(dest)
+// fmt.Printf("\n%s\n", bytes)
+// } else {
+// t.Errorf("%s\n", err)
+// }
+//
+// // 通过反射解剖结构体的字段以及父子关系
+//
+//}
+
+// sqlserver 数据库测试, 以查询为例,其他操作参见mysql
+// 请在配置项 config > gorm_v2.yml 中,sqlserver 部分,正确配置数据库参数
+// 设置 IsInitGolobalGormSqlserver =1 ,程序自动初始化全局变量
+func TestSqlserver(t *testing.T) {
+ var users []tb_users
+
+ // 执行类sql,如果配置了读写分离,该命令会在 write 数据库执行
+ result := variable.GormDbSqlserver.Exec("update tb_users set remark='update 操作 write数据库' where id=?", 1)
+ if result.Error != nil {
+ t.Errorf("单元测试失败,错误明细:%s\n", result.Error.Error())
+ }
+
+ // 查询类,如果配置了读写分离,该命令会在 read 数据库执行
+ result = variable.GormDbSqlserver.Table("tb_users").Select("id", "user_name", "pass", "remark").Where("id > ?", 0).Find(&users)
+ if result.Error != nil {
+ t.Errorf("单元测试失败,错误明细:%s\n", result.Error.Error())
+ }
+ fmt.Printf("sqlserver数据查询结果:%v\n", users)
+}
+
+// PostgreSql 数据库测试
+// 请在配置项 config > gorm_v2.yml 中,PostgreSql 部分,正确配置数据库参数。
+// 设置 IsInitGolobalGormPostgreSql =1 ,程序自动初始化全局变量
+func TestPostgreSql(t *testing.T) {
+ var users []tb_users
+
+ // 执行类sql,如果配置了读写分离,该命令会在 write 数据库执行
+ result := variable.GormDbPostgreSql.Exec("update web.tb_users set remark='update 操作 write数据库' where id=?", 1)
+ if result.Error != nil {
+ t.Errorf("单元测试失败,错误明细:%s\n", result.Error.Error())
+ }
+ // 查询类,如果配置了读写分离,该命令会在 read 数据库执行
+ result = variable.GormDbPostgreSql.Table("web.tb_users").Select("").Select("id", "user_name", "age", "addr", "remark").Where("id > ?", 0).Find(&users)
+ if result.Error != nil {
+ t.Errorf("单元测试失败,错误明细:%s\n", result.Error.Error())
+ }
+ fmt.Printf("sqlserver数据查询结果:%v\n", users)
+}
diff --git a/GinSkeleton/test/http_client_test.go b/GinSkeleton/test/http_client_test.go
new file mode 100644
index 0000000..cafa809
--- /dev/null
+++ b/GinSkeleton/test/http_client_test.go
@@ -0,0 +1,46 @@
+package test
+
+import (
+ "github.com/qifengzhang007/goCurl"
+ "goskeleton/app/global/variable"
+ _ "goskeleton/bootstrap" // 为了保证单元测试与正常启动效果一致,记得引入该包
+ "testing"
+)
+
+// goCurl 更详细的使用文档 https://gitee.com/daitougege/goCurl
+
+// 一个简单的get请求
+func TestHttpClient(t *testing.T) {
+ cli := goCurl.CreateHttpClient()
+ if resp, err := cli.Get("http://hq.sinajs.cn/list=sh601360"); err == nil {
+ content, err := resp.GetContents()
+ if err != nil {
+ t.Errorf("单元测试未通过,返回值不符合要求:%s\n", content)
+ }
+ t.Log(content)
+ }
+}
+
+// 向门户服务接口请求,用于收集cpu占用情况。
+func TestPprof(t *testing.T) {
+ cli := goCurl.CreateHttpClient()
+ for i := 1; i <= 500; i++ {
+ resp, err := cli.Get("http://127.0.0.1:20191/api/v1/home/news", goCurl.Options{
+ FormParams: map[string]interface{}{
+ "newsType": "portal",
+ "page": "2",
+ "limit": "52",
+ },
+ })
+ if err == nil {
+ if txt, err := resp.GetContents(); err == nil {
+ if i == 500 {
+ //最后一次输出返回结果,避免中间过程频繁操作io
+ variable.ZapLog.Info(txt)
+ }
+ }
+ } else {
+ t.Log(err.Error())
+ }
+ }
+}
diff --git a/GinSkeleton/test/rabbitmq_test.go b/GinSkeleton/test/rabbitmq_test.go
new file mode 100644
index 0000000..1c05af3
--- /dev/null
+++ b/GinSkeleton/test/rabbitmq_test.go
@@ -0,0 +1,254 @@
+package test
+
+import (
+ "fmt"
+ amqp "github.com/rabbitmq/amqp091-go"
+ "goskeleton/app/global/my_errors"
+ "goskeleton/app/utils/rabbitmq/hello_world"
+ "goskeleton/app/utils/rabbitmq/publish_subscribe"
+ "goskeleton/app/utils/rabbitmq/routing"
+ "goskeleton/app/utils/rabbitmq/topics"
+ "goskeleton/app/utils/rabbitmq/work_queue"
+ _ "goskeleton/bootstrap"
+ "os"
+ "strconv"
+ "testing"
+)
+
+// 消息队列(rabbitmq)在线文档地址:https://www.yuque.com/xiaofensinixidaouxiang/bkfhct/tkcuc8
+// 延迟消息队列在线文档地址:https://www.yuque.com/xiaofensinixidaouxiang/bkfhct/grroyv
+// 本篇的单元测试提供的是非延迟消息队列的测试,只要学会单元测试提供的示例,延迟队列也是非常简单的,参考在线文档即可
+
+// 1.HelloWorld 模式
+func TestRabbitMqHelloWorldProducer(t *testing.T) {
+
+ helloProducer, err := hello_world.CreateProducer()
+ if err != nil {
+ t.Errorf("HelloWorld单元测试未通过。%s\n", err.Error())
+ os.Exit(1)
+ }
+ var res bool
+ for i := 0; i < 10; i++ {
+ str := fmt.Sprintf("%d_HelloWorld开始发送消息测试", i+1)
+ res = helloProducer.Send(str)
+ //time.Sleep(time.Second * 1)
+ }
+
+ helloProducer.Close() // 消息投递结束,必须关闭连接
+ // 总共发送了10条消息,我们简单判断一下最后一条消息返回的结果
+ if res {
+ t.Log("消息发送OK")
+ } else {
+ t.Errorf("HelloWorld模式消息发送失败")
+ }
+}
+
+// 消费者
+func TestMqHelloWorldConsumer(t *testing.T) {
+
+ consumer, err := hello_world.CreateConsumer()
+ if err != nil {
+ t.Errorf("HelloWorld单元测试未通过。%s\n", err.Error())
+ os.Exit(1)
+ }
+
+ consumer.OnConnectionError(func(err *amqp.Error) {
+ t.Errorf(my_errors.ErrorsRabbitMqReconnectFail+",%s\n", err.Error())
+ })
+
+ consumer.Received(func(receivedData string) {
+
+ t.Logf("HelloWorld回调函数处理消息:--->%s\n", receivedData)
+ })
+}
+
+// 2.WorkQueue模式
+func TestRabbitMqWorkQueueProducer(t *testing.T) {
+
+ producer, _ := work_queue.CreateProducer()
+ var res bool
+ for i := 0; i < 10; i++ {
+ str := fmt.Sprintf("%d_WorkQueue开始发送消息测试", i+1)
+ res = producer.Send(str)
+ //time.Sleep(time.Second * 1)
+ }
+
+ producer.Close() // 消息投递结束,必须关闭连接
+
+ if res {
+ t.Logf("消息发送OK")
+ } else {
+ t.Errorf("WorkQueue模式消息发送失败")
+ }
+}
+
+// 消费者
+func TestMqWorkQueueConsumer(t *testing.T) {
+
+ consumer, err := work_queue.CreateConsumer()
+ if err != nil {
+ t.Errorf("WorkQueue单元测试未通过。%s\n", err.Error())
+ os.Exit(1)
+ }
+
+ consumer.OnConnectionError(func(err *amqp.Error) {
+ t.Errorf(my_errors.ErrorsRabbitMqReconnectFail + ", %s" + err.Error())
+ })
+
+ consumer.Received(func(receivedData string) {
+
+ t.Logf("WorkQueue回调函数处理消息:--->%s\n", receivedData)
+ })
+}
+
+// 3.PublishSubscribe 发布、订阅模式模式
+func TestRabbitMqPublishSubscribeProducer(t *testing.T) {
+
+ producer, err := publish_subscribe.CreateProducer()
+ if err != nil {
+ t.Errorf("WorkQueue 单元测试未通过。%s\n", err.Error())
+ os.Exit(1)
+ }
+ var res bool
+ for i := 0; i < 10; i++ {
+ str := fmt.Sprintf("%d_PublishSubscribe开始发送消息测试", i+1)
+ // 参数二: 消息延迟的毫秒数,只有创建的对象是延迟模式该参数才有效
+ res = producer.Send(str, 1000)
+ fmt.Println(str, res)
+ //time.Sleep(time.Second * 2)
+ }
+
+ producer.Close() // 消息投递结束,必须关闭连接
+
+ if res {
+ t.Log("消息发送OK")
+ } else {
+ t.Errorf("PublishSubscribe 模式消息发送失败")
+ }
+}
+
+// 消费者
+func TestRabbitMqPublishSubscribeConsumer(t *testing.T) {
+
+ consumer, err := publish_subscribe.CreateConsumer()
+ if err != nil {
+ t.Errorf("PublishSubscribe单元测试未通过。%s\n", err.Error())
+ os.Exit(1)
+ }
+
+ consumer.OnConnectionError(func(err *amqp.Error) {
+ t.Errorf(my_errors.ErrorsRabbitMqReconnectFail + ",%s\n" + err.Error())
+ })
+
+ consumer.Received(func(receivedData string) {
+
+ t.Logf("PublishSubscribe回调函数处理消息:--->%s\n", receivedData)
+ })
+}
+
+// Routing 路由模式
+func TestRabbitMqRoutingProducer(t *testing.T) {
+
+ producer, err := routing.CreateProducer()
+
+ if err != nil {
+ t.Errorf("Routing单元测试未通过。%s\n", err.Error())
+ os.Exit(1)
+ }
+ var res bool
+ var key string
+ for i := 1; i <= 20; i++ {
+
+ // 将 偶数 和 奇数 分发到不同的key,消费者端,启动两个也各自处理偶数和奇数
+ if i%2 == 0 {
+ key = "key_even" // 偶数键
+ } else {
+ key = "key_odd" // 奇数键
+ }
+
+ //strData := fmt.Sprintf("%d_Routing_%s, 开始发送消息测试"+time.Now().Format("2006-01-02 15:04:05"), i, key)
+ // 参数三: 消息延迟的毫秒数,只有创建的对象是延迟模式该参数才有效
+ res = producer.Send(key, strconv.Itoa(i)+"- Routing开始发送消息测试", 10000)
+ //time.Sleep(time.Second * 1)
+ }
+
+ producer.Close() // 消息投递结束,必须关闭连接
+
+ if res {
+ t.Logf("消息发送OK")
+ } else {
+ t.Errorf("Routing 模式消息发送失败")
+ }
+}
+
+// 消费者
+func TestRabbitMqRoutingConsumer(t *testing.T) {
+ consumer, err := routing.CreateConsumer()
+
+ if err != nil {
+ t.Errorf("Routing单元测试未通过。%s\n", err.Error())
+ os.Exit(1)
+ }
+
+ consumer.OnConnectionError(func(err *amqp.Error) {
+ t.Errorf(my_errors.ErrorsRabbitMqReconnectFail + ", %s\n" + err.Error())
+ })
+ // 通过route_key 匹配指定队列的消息来处理
+ consumer.Received("key_even", func(receivedData string) {
+ fmt.Println("处理偶数的回调函数 ---> 收到消息内容: " + receivedData)
+ // t.Logf("处理偶数的回调函数:--->收到消息时间:%s - 消息内容:%s\n", time.Now().Format("2006-01-02 15:04:05"), receivedData)
+ })
+}
+
+// topics 模式
+func TestRabbitMqTopicsProducer(t *testing.T) {
+
+ producer, err := topics.CreateProducer()
+ if err != nil {
+ t.Errorf("Routing单元测试未通过。%s\n", err.Error())
+ os.Exit(1)
+ }
+ var res bool
+ var key string
+ for i := 1; i <= 10; i++ {
+
+ // 将 偶数 和 奇数 分发到不同的key,消费者端,启动两个也各自处理偶数和奇数
+ if i%2 == 0 {
+ key = "key.even" // 偶数键
+ } else {
+ key = "key.odd" // 奇数键
+ }
+ strData := fmt.Sprintf("%d_Routing_%s, 开始发送消息测试", i, key)
+ // 参数三: 消息延迟的毫秒数,只有创建的对象是延迟模式该参数才有效
+ res = producer.Send(key, strData, 10000)
+ //time.Sleep(time.Second * 1)
+ }
+
+ producer.Close() // 消息投递结束,必须关闭连接
+
+ if res {
+ t.Logf("消息发送OK")
+ } else {
+ t.Errorf("topics 模式消息发送失败")
+ }
+ //Output: 消息发送OK
+}
+
+// 消费者
+func TestRabbitMqTopicsConsumer(t *testing.T) {
+ consumer, err := topics.CreateConsumer()
+
+ if err != nil {
+ t.Errorf("Routing单元测试未通过。%s\n", err.Error())
+ os.Exit(1)
+ }
+
+ consumer.OnConnectionError(func(err *amqp.Error) {
+ t.Errorf(my_errors.ErrorsRabbitMqReconnectFail + ", %s\n" + err.Error())
+ })
+ // 通过route_key 模糊匹配队列路由键的消息来处理
+ consumer.Received("#.odd", func(receivedData string) {
+
+ t.Logf("模糊匹配偶数键:--->%s\n", receivedData)
+ })
+}
diff --git a/GinSkeleton/test/redis_test.go b/GinSkeleton/test/redis_test.go
new file mode 100644
index 0000000..4a5305c
--- /dev/null
+++ b/GinSkeleton/test/redis_test.go
@@ -0,0 +1,117 @@
+package test
+
+import (
+ "fmt"
+ "go.uber.org/zap"
+ "goskeleton/app/global/variable"
+ "goskeleton/app/utils/redis_factory"
+ _ "goskeleton/bootstrap"
+ "testing"
+ "time"
+)
+
+// 普通的key value
+func TestRedisKeyValue(t *testing.T) {
+ // 从连接池获取一个连接
+ redisClient := redis_factory.GetOneRedisClient()
+
+ // set 命令, 因为 set key value 在redis客户端执行以后返回的是 ok,所以取回结果就应该是 string 格式
+ res, err := redisClient.String(redisClient.Execute("set", "key2020", "value202022"))
+ if err != nil {
+ t.Errorf("单元测试失败,%s\n", err.Error())
+ } else {
+ variable.ZapLog.Info("Info 日志", zap.String("key2020", res))
+ }
+ // get 命令,分为两步:1.执行get命令 2.将结果转为需要的格式
+ if res, err = redisClient.String(redisClient.Execute("get", "key2020")); err != nil {
+ t.Errorf("单元测试失败,%s\n", err.Error())
+ }
+ variable.ZapLog.Info("get key2020 ", zap.String("key2020", res))
+ //操作完毕记得释放连接,官方明确说,redis使用完毕,必须释放
+ redisClient.ReleaseOneRedisClient()
+
+}
+
+// hash 键、值
+func TestRedisHashKey(t *testing.T) {
+
+ redisClient := redis_factory.GetOneRedisClient()
+
+ // hash键 set 命令, 因为 hSet h_key key value 在redis客户端执行以后返回的是 1 或者 0,所以按照int64格式取回
+ res, err := redisClient.Int64(redisClient.Execute("hSet", "h_key2020", "hKey2020", "value2020_hash"))
+ if err != nil {
+ t.Errorf("单元测试失败,%s\n", err.Error())
+ } else {
+ fmt.Println(res)
+ }
+ // hash键 get 命令,分为两步:1.执行get命令 2.将结果转为需要的格式
+ res2, err := redisClient.String(redisClient.Execute("hGet", "h_key2020", "hKey2020"))
+ if err != nil {
+ t.Errorf("单元测试失败,%s\n", err.Error())
+ }
+ fmt.Println(res2)
+ //官方明确说,redis使用完毕,必须释放
+ redisClient.ReleaseOneRedisClient()
+}
+
+// 测试 redis 连接池
+func TestRedisConnPool(t *testing.T) {
+
+ for i := 1; i <= 20; i++ {
+ go func() {
+ redisClient := redis_factory.GetOneRedisClient()
+ fmt.Printf("获取的redis数据库连接池地址:%p\n", redisClient)
+ time.Sleep(time.Second * 10)
+ fmt.Printf("阻塞过程中,您可以通过redis命令 client list 查看链接的客户端")
+ redisClient.ReleaseOneRedisClient() // 释放从连接池获取的连接
+ }()
+ }
+ time.Sleep(time.Second * 20)
+}
+
+// 测试redis 网络中断自动重连机制
+func TestRedisReConn(t *testing.T) {
+ redisClient := redis_factory.GetOneRedisClient()
+ res, err := redisClient.String(redisClient.Execute("set", "key2020", "测试网络抖动,自动重连机制"))
+ if err != nil {
+ t.Errorf("单元测试失败,%s\n", err.Error())
+ } else {
+ variable.ZapLog.Info("Info 日志", zap.String("key2020", res))
+ }
+ //官方明确说,redis使用完毕,必须释放
+ redisClient.ReleaseOneRedisClient()
+
+ // 以上内容输出后 , 拔掉网线, 模拟短暂的网络抖动
+ t.Log("请在 10秒之内拔掉网线")
+ time.Sleep(time.Second * 10)
+ // 断网情况下就会自动进行重连
+ redisClient = redis_factory.GetOneRedisClient()
+ if res, err = redisClient.String(redisClient.Execute("get", "key2020")); err != nil {
+ t.Errorf("单元测试失败,%s\n", err.Error())
+ } else {
+ t.Log("获取的值:", res)
+ }
+ redisClient.ReleaseOneRedisClient()
+}
+
+// 测试返回值为多值的情况
+func TestRedisMulti(t *testing.T) {
+ redisClient := redis_factory.GetOneRedisClient()
+
+ if _, err := redisClient.String(redisClient.Execute("multi")); err == nil {
+ redisClient.Execute("hset", "mobile", "xiaomi", "1999")
+ redisClient.Execute("hset", "mobile", "oppo", "2999")
+ redisClient.Execute("hset", "mobile", "iphone", "3999")
+
+ if strs, err := redisClient.Int64s(redisClient.Execute("exec")); err == nil {
+ t.Logf("直接输出切片:%#+v\n", strs)
+ } else {
+ t.Errorf(err.Error())
+ }
+ } else {
+ t.Errorf(err.Error())
+ }
+ redisClient.ReleaseOneRedisClient()
+}
+
+// 其他请参照以上示例即可
diff --git a/GinSkeleton/test/snowflake_test.go b/GinSkeleton/test/snowflake_test.go
new file mode 100644
index 0000000..b916ade
--- /dev/null
+++ b/GinSkeleton/test/snowflake_test.go
@@ -0,0 +1,56 @@
+package test
+
+import (
+ "goskeleton/app/global/variable"
+ _ "goskeleton/bootstrap"
+ "sync"
+ "testing"
+)
+
+// 雪花算法单元测试
+
+func TestSnowFlake(t *testing.T) {
+ // 并发 3万 测试,实际业务场景中,并发是不可能达到 3万 这个值的
+ var slice1 []int64
+ var vMuext sync.Mutex
+ var wg sync.WaitGroup
+ wg.Add(30000)
+
+ for i := 1; i <= 30000; i++ {
+ go func() {
+ defer wg.Done()
+ //加锁操作主要是为了保证切片([]int64)的并发安全,
+ //我们本次测试的核心目的是雪花算法生成的ID必须是唯一的
+ vMuext.Lock()
+ slice1 = append(slice1, variable.SnowFlake.GetId())
+ vMuext.Unlock()
+ //fmt.Printf("%d\n", variable.SnowFlake.GetId())
+ }()
+ }
+
+ wg.Wait()
+
+ if lastLen := len(RemoveRepeatedElement(slice1)); lastLen == 30000 {
+ t.Log("单元测试OK")
+ } else {
+ t.Errorf("雪花算法单元测试失败,并发 3万 生成的id经过去重之后,小于预期个数,去重后的个数:%d\n", lastLen)
+ }
+}
+
+// 切片去重
+func RemoveRepeatedElement(arr []int64) (newArr []int64) {
+ newArr = make([]int64, 0)
+ for i := 0; i < len(arr); i++ {
+ repeat := false
+ for j := i + 1; j < len(arr); j++ {
+ if arr[i] == arr[j] {
+ repeat = true
+ break
+ }
+ }
+ if !repeat {
+ newArr = append(newArr, arr[i])
+ }
+ }
+ return
+}