You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

316 lines
14 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

### 文档说明
> 1.首先请自行查看本项目骨架3分钟快速入门主线图本文档将按照该图的主线逻辑展开...
> 2.本项目骨架开发过程中涉及到的参考资料,了解详情有利于了解本项目骨架的核心,建议您可以先学会本项目骨架之后再去了解相关引用。
> 2.1 gin框架https://github.com/gin-gonic/gin
> 2.2 websockethttps://github.com/gorilla/websocket
> 2.3 表单参数验证器https://github.com/go-playground/validator
> 2.4 JWT相关资料https://blog.csdn.net/codeSquare/article/details/99288718
> 2.5 golang项目标准布局中文翻译版https://studygolang.com/articles/26941?fr=sidebar
> 2.6 golang项目标准布局原版https://github.com/golang-standards/project-layout
> 2.7 httpClient包相关资料https://github.com/qifengzhang007/goCurl
> 2.8 RabbitMq相关资料https://www.rabbitmq.com/
> 2.9 cobraCli命令模式包 相关资料https://github.com/spf13/cobra/
> 3.本文档侧重介绍本项目骨架的主线逻辑以及相关核心模块不对gin框架的具体语法做介绍。
### 前言
> 1.为了更好地理解后续文档,我们先说明一下 `ginhttps://github.com/gin-gonic/gin` 路由包的本质.
> 2.我们用以下代码为例进行说明
```code
// gin的中间件、路由组、路由
authorized.Use(fun(c *gin.Context){ c.Next() })
{
authorized.Group("/v1") // 路由组的第二个参数同样支持回调函数: fun(c *gin.Context){ ...省略代码 }
{
authorized.POST("/login", fun(c *gin.Context){
c.PostForm("userName")
})
authorized.POST("/update", fun(c *gin.Context){
c.PostForm("userName")
})
}
}
```
> 3.从以上代码我们可以看出 `gin` 路由包的的中间件、路由组、路由本质都是采用的回调函数在处理后续的逻辑,回调函数最大的数量为 63 个.
> 4.我们也可以看出,`gin` 的回调函数非常工整、统一,只有一个参数 *gin.Context ,整个请求的数据全部在这个主线(上下文)里面,我们可以从这个参数获取表单请求参数,也可以自己额外绑定、追加.
> 5.其实在任何时候不管我们通过什么方式只要保证你的代码段形式是以上回调函数的形式整个逻辑就是OK的.
### 1.框架启动, 初始化全局变量等相关的代码段
> 代码位置 `bootstrap/init.go`[进入详情](../bootstrap/init.go)
```go
// 这里主要介绍 init 函数的主要功能,详细的实现请点击上面的 进入详情 查看代码部分
func init() {
// 1.初始化 项目根路径
// 2.检查配置文件以及日志目录等非编译性的必要条件
// 3.初始化表单参数验证器,注册在容器
// 4.启动针对配置文件(confgi.yml、gorm_v2.yml)变化的监听
// 5.初始化全局日志句柄,并载入日志钩子处理函数
// 6.根据配置初始化 gorm mysql 全局 *gorm.Db
// 7.雪花算法全局变量
// 8.websocket Hub中心启动
}
```
### 2.一个 request 到 response 的生命周期
##### 2.1 介绍路由之前首先简要介绍一下表单参数验证器 因为是路由“必经之地”。位置app\http\validator\(web|api)\xxx业务模块
```code
//1.首先编写参数验证器逻辑,例如:用户注册模块
// 详情参见app\http\validator\web\users\register.go
//2.将以上编写好的表单参数验证器进行注册,便于程序启动时自动加载到容器,在路由定义处我们根据注册时的键,就可以直接调用相关的验证器代码段
// 例如 我们注册该验证器的键: consts.ValidatorPrefix + "UsersRegister" ,程序启动时会自动加载到容器,获取的时候按照该键即刻获取相关的代码段
// 详情参见app\http\validator\common\register_validator\register_validator.go
```
##### 2.2.路由 位置routers\web.go
```go
// 创建一个后端接口路由组
V_Backend := routers.Group("/Admin/")
{
// 【不需要】中间件验证的路由 用户注册、登录
v_noAuth := V_Backend.Group("users/")
{
// 参数2说明 validatorFactory.Create(Consts.ValidatorPrefix+"UsersRegister") 该函数就是按照键直接从容器获取验证器代码
v_noAuth.POST("register", validatorFactory.Create(Consts.ValidatorPrefix+"UsersRegister"))
}
// 需要中间件验证的路由
V_Backend.Use(authorization.CheckAuth())
{
// 用户组路由
v_users := V_Backend.Group("users/")
{
// 查询 这里的验证器直接从容器获取是因为程序启动时将验证器注册在了容器具体代码位置app\http\validator\Users\xxx
// 第二个参数本质上返回的就是 gin 的回调函数形式: fun(c *gin.Context){ ....省略代码 }
v_users.GET("index", validatorFactory.Create(Consts.ValidatorPrefix+"UsersShow"))
}
}
}
```
> 分析
1.请求到达路由,业务逻辑出现细分:不需要和需要 中间件鉴权的请求。
2.不需要鉴权,直接切换到表单参数验证器模块,验证参数的合法性。
3.需要鉴权,首先切入中间件,中间件完成验证,再将请求切换到表单参数验证器模块,验证参数的合法性。
##### 2.3 中间件位置app\http\middleware\authorization
```go
// 选取一段代码说明
type HeaderParams struct {
authorization string `header:"authorization"`
}
// 本质上返回的代码段就是 gin 的标准回调函数形式 func(c *gin.Context) { ... 省略代码 }
func CheckAuth() gin.HandlerFunc {
return func(context *gin.Context) {
// 模拟验证token
V_HeaderParams := HeaderParams{}
// 使用ShouldbindHeader 方式获取头参数
context.ShouldBindHeader(&V_HeaderParams)
// 对头参数中的token进行验证
if len(V_HeaderParams.authorization) >= 20 {
...
context.Next() // OK 下一步
}else{
context.Abort() // 不 OK 终止已注册代码执行
}
}
```
##### 2.4 表单参数验证器位置app\http\validator\(web|api)\XXX业务模块
>开发完成一个表单参数验证器必须在注册文件app\http\validator\register_validator\register_validator.go增加记录待程序启动时统一自动注册到容器。
```go
type Register struct {
Base
Pass string `form:"pass" json:"pass" binding:"required,min=6,max=20"` //必填密码长度范围【6,20】闭区间
//Captcha string `form:"captcha" json:"captcha" binding:"required,len=4"` // 验证码必填长度为4
//Phone string `form:"phone" json:"phone" binding:"required,len=11"` // 验证规则:必填,长度必须=11
//CardNo string `form:"card_no" json:"card_no" binding:"required,len=18"` //身份证号码,必填,长度=18
}
func (r Register) CheckParams(context *gin.Context) {
//1.先按照验证器提供的基本语法基本可以校验90%以上的不合格参数
if err := context.ShouldBind(&r); err != nil {
errs := gin.H{
"tips": "UserRegister参数校验失败参数不符合规定user_name 长度(>=1)、pass长度[6,20]、不允许注册",
"err": err.Error(),
}
response.ErrorParam(context, errs)
return
}
//2.继续验证具有中国特色的参数,例如 身份证号码等基本语法校验了长度18位然后可以自行编写正则表达式等更进一步验证每一部分组成
// r.CardNo 获取值继续校验,这里省略.....
// 该函数主要是将结构体的成员(字段)获取的数据以 键=>值 绑定在 context 上下文,然后传递给下一步(控制器)
// 绑定的键按照 consts.ValidatorPrefix+ json 标签组成例如用户提交的密码pass绑定的键consts.ValidatorPrefix+"pass"
// 自动绑定以后的结构体字段,在控制器就可以按照相关的键直接获取值,例如: pass := context.GetString(consts.ValidatorPrefix + "pass")
extraAddBindDataContext := data_transfer.DataAddContext(r, consts.ValidatorPrefix, context)
if extraAddBindDataContext == nil {
response.ErrorSystem(context, "UserRegister表单验证器json化失败", "")
} else {
// 验证完成,调用控制器,并将验证器成员(字段)递给控制器,保持上下文数据一致性
(&web.Users{}).Register(extraAddBindDataContext)
}
}
```
##### 2.5 控制器位置app\http\controller\(web|api)\XXX业务模块
> 尽量让控制器成为一个调度器的角色,而不是在这里处理业务
```go
type Users struct {
}
// 1.用户注册
func (u *Users) Register(context *gin.Context) {
// 由于本项目骨架已经将表单验证器的字段(成员)绑定在上下文,因此可以按照 GetString()、GetInt64()、GetFloat64等快捷获取需要的数据类型注意相关键名规则 前缀+验证器结构体中的 json 标签
// 当然也可以通过gin框架的上下文原始方法获取例如 context.PostForm("user_name") 获取,这样获取的数据格式为文本,需要自己继续转换
userName := context.GetString(consts.ValidatorPrefix + "user_name")
pass := context.GetString(consts.ValidatorPrefix + "pass")
userIp := context.ClientIP()
if curd.CreateUserCurdFactory().Register(userName, pass, userIp) {
response.Success(context, consts.CurdStatusOkMsg, "")
} else {
response.Fail(context, consts.CurdRegisterFailCode, consts.CurdRegisterFailMsg, "")
}
}
```
###### 2.5.1 Model业务层位置app\models\XXX业务模块
> 控制器调度Model业务模块
```go
type UsersModel struct {
Model `json:"-"`
UserName string `gorm:"column:user_name" json:"user_name"`
Pass string `json:"pass" form:"pass"`
Phone string `json:"phone" form:"phone"`
RealName string `gorm:"column:real_name" json:"real_name"`
Status int `json:"status" form:"status"`
Token string `json:"token" form:"token"`
LastLoginIp string `gorm:"column:last_login_ip" json:"last_login_ip"`
}
// 表名
func (u *UsersModel) TableName() string {
return "tb_users"
}
// 用户注册(写一个最简单的使用账号、密码注册即可)
func (u *UsersModel) Register(userName, pass, userIp string) bool {
sql := "INSERT INTO tb_users(user_name,pass,last_login_ip) SELECT ?,?,? FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM tb_users WHERE user_name=?)"
result := u.Exec(sql, userName, pass, userIp, userName)
if result.RowsAffected > 0 {
return true
} else {
return false
}
}
```
###### 2.5.2 service业务层位置app\service\XXX业务模块
> 控制器调度service业务模块
```go
type UsersCurd struct {
}
// 预先处理密码加密,然后存储在数据库
func (u *UsersCurd) Register(userName, pass, userIp string) bool {
pass = md5_encrypt.Base64Md5(pass) // 预先处理密码加密,然后存储在数据库
return model.CreateUserFactory("").Register(userName, pass, userIp)
}
```
##### 2.6 response响应位置app\utils\response\response.go
>这里我们只封装了json格式数据返回如果需要 xml 、html、text等请按照gin语法自行追加函数即可。
```go
func ReturnJson(Context *gin.Context, http_code int, data_code int, msg string, data interface{}) {
//Context.Header("key2020","value2020") //可以根据实际情况在头部添加额外的其他信息
// 返回 json数据
Context.JSON(http_code, gin.H{
"code": data_code,
"msg": msg,
"data": data,
})
}
// 将json字符窜以标准json格式返回例如从redis读取json、格式的字符串返回给浏览器json格式
func ReturnJsonFromString(Context *gin.Context, http_code int, json_str string) {
Context.Header("Content-Type", "application/json; charset=utf-8")
Context.String(http_code, json_str)
}
}
// v1.4.00 版本之后我们封装了其他一些语法糖函数,进一步精简代码
// 语法糖函数封装
// 直接返回成功
func Success(c *gin.Context, msg string, data interface{}) {
ReturnJson(c, http.StatusOK, consts.CurdStatusOkCode, msg, data)
}
// 失败的业务逻辑
func Fail(c *gin.Context, dataCode int, msg string, data interface{}) {
ReturnJson(c, http.StatusBadRequest, dataCode, msg, data)
c.Abort()
}
//权限校验失败
func ErrorTokenAuthFail(c *gin.Context) {
ReturnJson(c, http.StatusUnauthorized, http.StatusUnauthorized, my_errors.ErrorsNoAuthorization, "")
//暂停执行
c.Abort()
}
//参数校验错误
func ErrorParam(c *gin.Context, wrongParam interface{}) {
ReturnJson(c, http.StatusBadRequest, consts.ValidatorParamsCheckFailCode, consts.ValidatorParamsCheckFailMsg, wrongParam)
c.Abort()
}
// 系统执行代码错误
func ErrorSystem(c *gin.Context, msg string, data interface{}) {
ReturnJson(c, http.StatusInternalServerError, consts.ServerOccurredErrorCode, consts.ServerOccurredErrorMsg+msg, data)
c.Abort()
}
```
#### 3.信号监听独立协程位置app\core\destroy\destroy.go
>该协程会在框架启动时被启动,用于监听程序可能收到的退出信号
```go
func init() {
// 用于系统信号的监听
go func() {
c := make(chan os.Signal)
signal.Notify(c, os.Interrupt, os.Kill, syscall.SIGQUIT, syscall.SIGINT, syscall.SIGTERM) // 监听可能的退出信号
received := <-c //接收信号管道中的值
variable.ZapLog.Warn(consts.ProcessKilled, zap.String("信号值", received.String()))
(event_manage.CreateEventManageFactory()).FuzzyCall(variable.EventDestroyPrefix)
close(c)
os.Exit(1)
}()
}
```
### websocket模块
> 1.或许你觉得websocket不应该出现在主线模块但是在go中ws长连接的建立确实是通过http升级协议完成的, 因此这块内容我们仍然放在了主线的最后.
> 2.启动ws服务位置config\config.yaml找到相关配置开关开启。
> 3.控制器位置app\http\controller\websocket\ws.go
> 4.事件监听、处理位置app\service\websocket\ws.go,[查看详情](../app/service/websocket/ws.go)
> 5.关于隐式自动维护心跳抓包图,其中`Server_ping` 为服务器端向浏览器发送的`ping`格式数据包,`F12` 不可见,只有抓包可见。
>![业务主线图](https://www.ginskeleton.com/images/pingpong.png)