├── storage ├── logs │ └── gin.log └── app │ └── img │ └── huawei-cloud-server-small.png ├── public ├── favicon.ico └── readme.md ├── .gitignore ├── app ├── utils │ ├── snow_flake │ │ ├── snowflake_interf │ │ │ └── InterfaceSnowFlake.go │ │ └── snow_flake.go │ ├── observer_mode │ │ ├── observer.go │ │ └── subject.go │ ├── rabbitmq │ │ ├── error_record │ │ │ └── error_handler.go │ │ ├── topics │ │ │ ├── options.go │ │ │ └── producer.go │ │ ├── routing │ │ │ ├── options.go │ │ │ └── producer.go │ │ ├── publish_subscribe │ │ │ ├── options.go │ │ │ └── producer.go │ │ ├── hello_world │ │ │ ├── producer.go │ │ │ └── consumer.go │ │ └── work_queue │ │ │ ├── producer.go │ │ │ └── consumer.go │ ├── md5_encrypt │ │ └── md5_encrypt.go │ ├── gorm_v2 │ │ └── config_params.go │ ├── cur_userinfo │ │ └── cur_user.go │ ├── yml_config │ │ └── ymlconfig_interf │ │ │ └── yml_conf_interf.go │ ├── websocket │ │ └── core │ │ │ └── hub.go │ ├── files │ │ └── baseInfo.go │ ├── gin_release │ │ └── gin_release_router.go │ ├── validator_translation │ │ └── validator_transiation.go │ ├── casbin_v2 │ │ └── casbin_v2.go │ ├── zap_factory │ │ └── zap_factory.go │ ├── data_bind │ │ └── formdata_to_model.go │ └── response │ │ └── response.go ├── http │ ├── validator │ │ ├── core │ │ │ ├── interf │ │ │ │ └── interf.go │ │ │ ├── factory │ │ │ │ └── factory.go │ │ │ └── data_transfer │ │ │ │ └── data_transfer.go │ │ ├── common │ │ │ ├── data_type │ │ │ │ └── common_data_type.go │ │ │ ├── register_validator │ │ │ │ ├── api_register_validator.go │ │ │ │ └── web_register_validator.go │ │ │ ├── websocket │ │ │ │ └── connect.go │ │ │ └── upload_files │ │ │ │ └── upload_fiels.go │ │ ├── web │ │ │ └── users │ │ │ │ ├── data_type.go │ │ │ │ ├── login.go │ │ │ │ ├── refresh_token.go │ │ │ │ ├── show.go │ │ │ │ ├── store.go │ │ │ │ ├── update.go │ │ │ │ ├── destroy.go │ │ │ │ └── register.go │ │ └── api │ │ │ └── home │ │ │ └── news.go │ ├── middleware │ │ ├── my_jwt │ │ │ ├── custom_claims.go │ │ │ └── my_jwt.go │ │ └── cors │ │ │ └── cors.go │ └── controller │ │ ├── websocket │ │ └── ws.go │ │ ├── web │ │ └── upload_controller.go │ │ ├── api │ │ └── home_controller.go │ │ └── captcha │ │ └── captcha_controller.go ├── service │ ├── websocket │ │ ├── on_open_success │ │ │ └── set_client_more_params.go │ │ └── ws.go │ ├── sys_log_hook │ │ └── zap_log_hooks.go │ ├── users │ │ ├── curd │ │ │ └── users_curd.go │ │ ├── token_cache_redis │ │ │ └── user_token_cache_redis.go │ │ └── token │ │ │ └── token.go │ └── upload_file │ │ └── upload_file.go ├── aop │ └── users │ │ ├── destroy_after.go │ │ └── destroy_before.go ├── core │ ├── destroy │ │ └── destroy.go │ ├── container │ │ └── container.go │ └── event_manage │ │ └── event_manage.go ├── global │ ├── variable │ │ └── variable.go │ ├── consts │ │ └── consts.go │ └── my_errors │ │ └── my_errors.go └── model │ └── base_model.go ├── cmd ├── cli │ └── main.go ├── api │ └── main.go └── web │ └── main.go ├── docs ├── bench_cpu_memory.md ├── deploy_nohup.md ├── deploy_go.md ├── project_struct.md ├── sql_stament.md ├── project_analysis_1.md ├── deploy_mysql.md ├── captcha.md ├── deploy_redis.md ├── supervisor.md ├── ws_js_client.md ├── validator.md ├── many_db_operate.md ├── deploy_nginx.md ├── project_analysis_2.md ├── low_coupling.md ├── formparams.md ├── aop.md ├── casbin.md ├── faq.md ├── zap_log.md ├── websocket.md ├── global_variable.md ├── project_analysis_3.md ├── cobra.md ├── deploy_docker.md └── deploy_linux.md ├── command ├── demo │ ├── sub_cmd.go │ └── demo.go ├── root.go └── demo_simple │ └── simple.go ├── go.mod ├── LICENSE ├── test ├── http_client_test.go ├── snowflake_test.go └── redis_test.go ├── ReadME.md ├── makefile ├── database ├── db_demo_sqlserver.sql └── db_demo_mysql.sql ├── routers ├── api.go └── web.go ├── config └── gorm_v2.yml └── bootstrap └── init.go /storage/logs/gin.log: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rockyzsu/GinSkeleton/github/public/favicon.ico -------------------------------------------------------------------------------- /public/readme.md: -------------------------------------------------------------------------------- 1 | #### 特别说明 2 | > 1.虽然`gin`框架支持静态文件处理, 但是我们建议您将静态资源交给 `nginx` 去处理,以获得极速性能. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | .idea/ 3 | .idea 4 | /storage/logs/* 5 | /storage/uploaded/* 6 | /public/storage -------------------------------------------------------------------------------- /storage/app/img/huawei-cloud-server-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rockyzsu/GinSkeleton/github/storage/app/img/huawei-cloud-server-small.png -------------------------------------------------------------------------------- /app/utils/snow_flake/snowflake_interf/InterfaceSnowFlake.go: -------------------------------------------------------------------------------- 1 | package snowflake_interf 2 | 3 | type InterfaceSnowFlake interface { 4 | GetId() int64 5 | } 6 | -------------------------------------------------------------------------------- /app/utils/observer_mode/observer.go: -------------------------------------------------------------------------------- 1 | package observer_mode 2 | 3 | // 观察者角色(Observer)接口 4 | type ObserverInterface interface { 5 | // 接收状态更新消息 6 | Update(*Subject) 7 | } 8 | -------------------------------------------------------------------------------- /app/http/validator/core/interf/interf.go: -------------------------------------------------------------------------------- 1 | package interf 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | // 验证器接口,每个验证器必须实现该接口,请勿修改 6 | type ValidatorInterface interface { 7 | CheckParams(context *gin.Context) 8 | } 9 | -------------------------------------------------------------------------------- /cmd/cli/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | _ "goskeleton/bootstrap" 5 | cmd "goskeleton/command" 6 | ) 7 | 8 | // 开发非http接口类服务入口 9 | func main() { 10 | // 设置运行模式为 cli(console) 11 | cmd.Execute() 12 | } 13 | -------------------------------------------------------------------------------- /app/http/validator/common/data_type/common_data_type.go: -------------------------------------------------------------------------------- 1 | package data_type 2 | 3 | type Page struct { 4 | Page float64 `form:"page" json:"page" binding:"min=1"` // 必填,页面值>=1 5 | Limit float64 `form:"limit" json:"limit" binding:"min=1"` // 必填,每页条数值>=1 6 | } 7 | -------------------------------------------------------------------------------- /docs/bench_cpu_memory.md: -------------------------------------------------------------------------------- 1 | ### 并发测试 2 | > 2核8G阿里云服务器, 并发(Qps)可以达到1w+,所有请求100%成功! 3 | ![压力测试图](https://www.ginskeleton.com/concurrent.png) 4 | 5 | > 4核8G阿里云服务器, 并发(Qps)可以达到1.6w+,所有请求100%成功! 6 | ![压力测试图](https://www.ginskeleton.com/images/bench_test2.png) 7 | -------------------------------------------------------------------------------- /app/utils/rabbitmq/error_record/error_handler.go: -------------------------------------------------------------------------------- 1 | package error_record 2 | 3 | import "goskeleton/app/global/variable" 4 | 5 | // ErrorDeal 记录错误 6 | func ErrorDeal(err error) error { 7 | if err != nil { 8 | variable.ZapLog.Error(err.Error()) 9 | } 10 | return err 11 | } 12 | -------------------------------------------------------------------------------- /cmd/api/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "goskeleton/app/global/variable" 5 | _ "goskeleton/bootstrap" 6 | "goskeleton/routers" 7 | ) 8 | 9 | // 这里可以存放门户类网站入口 10 | func main() { 11 | router := routers.InitApiRouter() 12 | _ = router.Run(variable.ConfigYml.GetString("HttpServer.Api.Port")) 13 | } 14 | -------------------------------------------------------------------------------- /cmd/web/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "goskeleton/app/global/variable" 5 | _ "goskeleton/bootstrap" 6 | "goskeleton/routers" 7 | ) 8 | 9 | // 这里可以存放后端路由(例如后台管理系统) 10 | func main() { 11 | router := routers.InitWebRouter() 12 | _ = router.Run(variable.ConfigYml.GetString("HttpServer.Web.Port")) 13 | } 14 | -------------------------------------------------------------------------------- /app/http/middleware/my_jwt/custom_claims.go: -------------------------------------------------------------------------------- 1 | package my_jwt 2 | 3 | import "github.com/dgrijalva/jwt-go" 4 | 5 | // 自定义jwt的声明字段信息+标准字段,参考地址:https://blog.csdn.net/codeSquare/article/details/99288718 6 | type CustomClaims struct { 7 | UserId int64 `json:"user_id"` 8 | Name string `json:"user_name"` 9 | Phone string `json:"phone"` 10 | jwt.StandardClaims 11 | } 12 | -------------------------------------------------------------------------------- /app/http/validator/web/users/data_type.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | type BaseField struct { 4 | UserName string `form:"user_name" json:"user_name" binding:"required,min=1"` // 必填、对于文本,表示它的长度>=1 5 | Pass string `form:"pass" json:"pass" binding:"required,min=6,max=20"` // 密码为 必填,长度>=6 6 | } 7 | 8 | type Id struct { 9 | Id float64 `form:"id" json:"id" binding:"required,min=1"` 10 | } 11 | -------------------------------------------------------------------------------- /docs/deploy_nohup.md: -------------------------------------------------------------------------------- 1 | ### nohup 开发测试环境部署方案 2 | 3 | 在项目开发、测试环境,我们需要的只是快速部署、测试项目功能,因此该方案是最简单、也是最适合开发调试环境的首选方案. 4 | 5 | 6 | ```code 7 | 8 | # 首先编译出可执行文件 9 | # 将可执行文件 + config目录 + public 目录 + storage 目录 合计4项全部复制在服务器。 10 | # 进入可执行文件目录执行 11 | nohup 可执行文件名 & 12 | 13 | 14 | # 版本更新 15 | 16 | # 杀死旧进程 17 | kill -9 (进程pid) 18 | # 重新启动进程 19 | nohup 可执行文件名 & 20 | 21 | ``` 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/service/websocket/on_open_success/set_client_more_params.go: -------------------------------------------------------------------------------- 1 | package on_open_success 2 | 3 | // ClientMoreParams 为客户端成功上线后设置更多的参数 4 | // ws 客户端成功上线以后,可以通过客户端携带的唯一参数,在数据库查询更多的其他关键信息,设置在 *Client 结构体上 5 | // 这样便于在后续获取在线客户端时快速获取其他关键信息,例如:进行消息广播时记录日志可能需要更多字段信息等 6 | type ClientMoreParams struct { 7 | UserParams1 string `json:"user_params_1"` // 字段名称以及类型由 开发者自己定义 8 | UserParams2 string `json:"user_params_2"` 9 | } 10 | -------------------------------------------------------------------------------- /app/utils/md5_encrypt/md5_encrypt.go: -------------------------------------------------------------------------------- 1 | package md5_encrypt 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/base64" 6 | "encoding/hex" 7 | ) 8 | 9 | func MD5(params string) string { 10 | md5Ctx := md5.New() 11 | md5Ctx.Write([]byte(params)) 12 | return hex.EncodeToString(md5Ctx.Sum(nil)) 13 | } 14 | 15 | //先base64,然后MD5 16 | func Base64Md5(params string) string { 17 | return MD5(base64.StdEncoding.EncodeToString([]byte(params))) 18 | } 19 | -------------------------------------------------------------------------------- /app/utils/gorm_v2/config_params.go: -------------------------------------------------------------------------------- 1 | package gorm_v2 2 | 3 | // 数据库参数配置,结构体 4 | // 用于解决复杂的业务场景连接到多台服务器部署的 mysql、sqlserver、postgresql 数据库 5 | // 具体用法参见常用开发模块:多源数据库的操作 6 | 7 | type ConfigParams struct { 8 | Write ConfigParamsDetail 9 | Read ConfigParamsDetail 10 | } 11 | type ConfigParamsDetail struct { 12 | Host string 13 | DataBase string 14 | Port int 15 | Prefix string 16 | User string 17 | Pass string 18 | Charset string 19 | } 20 | -------------------------------------------------------------------------------- /app/utils/cur_userinfo/cur_user.go: -------------------------------------------------------------------------------- 1 | package cur_userinfo 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "goskeleton/app/global/variable" 6 | "goskeleton/app/http/middleware/my_jwt" 7 | ) 8 | 9 | // GetCurrentUserId 获取当前用户的id 10 | // @context 请求上下文 11 | func GetCurrentUserId(context *gin.Context) (int64, bool) { 12 | tokenKey := variable.ConfigYml.GetString("Token.BindContextKeyName") 13 | currentUser, exist := context.MustGet(tokenKey).(my_jwt.CustomClaims) 14 | return currentUser.UserId, exist 15 | } 16 | -------------------------------------------------------------------------------- /command/demo/sub_cmd.go: -------------------------------------------------------------------------------- 1 | package demo 2 | 3 | import ( 4 | "fmt" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | // 定义子命令 9 | var subCmd = &cobra.Command{ 10 | Use: "subCmd", 11 | Short: "subCmd 命令简要介绍", 12 | Long: `命令使用详细介绍`, 13 | Args: cobra.ExactArgs(1), // 限制非flag参数的个数 = 1 ,超过1个会报错 14 | Run: func(cmd *cobra.Command, args []string) { 15 | fmt.Printf("%s\n", args[0]) 16 | }, 17 | } 18 | 19 | //注册子命令 20 | func init() { 21 | Demo1.AddCommand(subCmd) 22 | // 子命令仍然可以定义 flag 参数,相关语法参见 demo.go 文件 23 | } 24 | -------------------------------------------------------------------------------- /app/aop/users/destroy_after.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "goskeleton/app/global/consts" 6 | "goskeleton/app/global/variable" 7 | ) 8 | 9 | // 模拟Aop 实现对某个控制器函数的前置和后置回调 10 | 11 | type DestroyAfter struct{} 12 | 13 | func (d *DestroyAfter) After(context *gin.Context) { 14 | // 后置函数可以使用异步执行 15 | go func() { 16 | userId := context.GetFloat64(consts.ValidatorPrefix + "id") 17 | variable.ZapLog.Sugar().Infof("模拟 Users 删除操作, After 回调,用户ID:%.f\n", userId) 18 | }() 19 | } 20 | -------------------------------------------------------------------------------- /app/http/validator/common/register_validator/api_register_validator.go: -------------------------------------------------------------------------------- 1 | package register_validator 2 | 3 | import ( 4 | "goskeleton/app/core/container" 5 | "goskeleton/app/global/consts" 6 | "goskeleton/app/http/validator/api/home" 7 | ) 8 | 9 | // 各个业务模块验证器必须进行注册(初始化),程序启动时会自动加载到容器 10 | func ApiRegisterValidator() { 11 | //创建容器 12 | containers := container.CreateContainersFactory() 13 | 14 | // key 按照前缀+模块+验证动作 格式,将各个模块验证注册在容器 15 | var key string 16 | 17 | // 注册门户类表单参数验证器 18 | key = consts.ValidatorPrefix + "HomeNews" 19 | containers.Set(key, home.News{}) 20 | } 21 | -------------------------------------------------------------------------------- /app/utils/yml_config/ymlconfig_interf/yml_conf_interf.go: -------------------------------------------------------------------------------- 1 | package ymlconfig_interf 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type YmlConfigInterf interface { 8 | ConfigFileChangeListen() 9 | Clone(fileName string) YmlConfigInterf 10 | Get(keyName string) interface{} 11 | GetString(keyName string) string 12 | GetBool(keyName string) bool 13 | GetInt(keyName string) int 14 | GetInt32(keyName string) int32 15 | GetInt64(keyName string) int64 16 | GetFloat64(keyName string) float64 17 | GetDuration(keyName string) time.Duration 18 | GetStringSlice(keyName string) []string 19 | } 20 | -------------------------------------------------------------------------------- /app/aop/users/destroy_before.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "goskeleton/app/global/consts" 6 | "goskeleton/app/global/variable" 7 | ) 8 | 9 | // 模拟Aop 实现对某个控制器函数的前置和后置回调 10 | 11 | type DestroyBefore struct{} 12 | 13 | // 前置函数必须具有返回值,这样才能控制流程是否继续向下执行 14 | func (d *DestroyBefore) Before(context *gin.Context) bool { 15 | userId := context.GetFloat64(consts.ValidatorPrefix + "id") 16 | variable.ZapLog.Sugar().Infof("模拟 Users 删除操作, Before 回调,用户ID:%.f\n", userId) 17 | if userId > 10 { 18 | return true 19 | } else { 20 | return false 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/http/controller/websocket/ws.go: -------------------------------------------------------------------------------- 1 | package websocket 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | serviceWs "goskeleton/app/service/websocket" 6 | ) 7 | 8 | /** 9 | websocket 想要了解更多具体细节请参见以下文档 10 | 文档地址:https://github.com/gorilla/websocket/tree/master/examples 11 | */ 12 | 13 | type Ws struct { 14 | } 15 | 16 | // OnOpen 主要解决握手+协议升级 17 | func (w *Ws) OnOpen(context *gin.Context) (*serviceWs.Ws, bool) { 18 | return (&serviceWs.Ws{}).OnOpen(context) 19 | } 20 | 21 | // OnMessage 处理业务消息 22 | func (w *Ws) OnMessage(serviceWs *serviceWs.Ws, context *gin.Context) { 23 | serviceWs.OnMessage(context) 24 | } 25 | -------------------------------------------------------------------------------- /app/http/validator/core/factory/factory.go: -------------------------------------------------------------------------------- 1 | package factory 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "goskeleton/app/core/container" 6 | "goskeleton/app/global/my_errors" 7 | "goskeleton/app/global/variable" 8 | "goskeleton/app/http/validator/core/interf" 9 | ) 10 | 11 | // 表单参数验证器工厂(请勿修改) 12 | func Create(key string) func(context *gin.Context) { 13 | 14 | if value := container.CreateContainersFactory().Get(key); value != nil { 15 | if val, isOk := value.(interf.ValidatorInterface); isOk { 16 | return val.CheckParams 17 | } 18 | } 19 | variable.ZapLog.Error(my_errors.ErrorsValidatorNotExists + ", 验证器模块:" + key) 20 | return nil 21 | } 22 | -------------------------------------------------------------------------------- /app/utils/websocket/core/hub.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | type Hub struct { 4 | //上线注册 5 | Register chan *Client 6 | //下线注销 7 | UnRegister chan *Client 8 | //所有在线客户端的内存地址 9 | Clients map[*Client]bool 10 | } 11 | 12 | func CreateHubFactory() *Hub { 13 | return &Hub{ 14 | Register: make(chan *Client), 15 | UnRegister: make(chan *Client), 16 | Clients: make(map[*Client]bool), 17 | } 18 | } 19 | 20 | func (h *Hub) Run() { 21 | for { 22 | select { 23 | case client := <-h.Register: 24 | h.Clients[client] = true 25 | case client := <-h.UnRegister: 26 | if _, ok := h.Clients[client]; ok { 27 | _ = client.Conn.Close() 28 | delete(h.Clients, client) 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/service/sys_log_hook/zap_log_hooks.go: -------------------------------------------------------------------------------- 1 | package sys_log_hook 2 | 3 | import ( 4 | "go.uber.org/zap/zapcore" 5 | ) 6 | 7 | // GoSkeleton 系统运行日志钩子函数 8 | // 1.单条日志就是一个结构体格式,本函数拦截每一条日志,您可以进行后续处理,例如:推送到阿里云日志管理面板、ElasticSearch 日志库等 9 | 10 | func ZapLogHandler(entry zapcore.Entry) error { 11 | 12 | // 参数 entry 介绍 13 | // entry 参数就是单条日志结构体,主要包括字段如下: 14 | //Level 日志等级 15 | //Time 当前时间 16 | //LoggerName 日志名称 17 | //Message 日志内容 18 | //Caller 各个文件调用路径 19 | //Stack 代码调用栈 20 | 21 | //这里启动一个协程,hook丝毫不会影响程序性能, 22 | go func(paramEntry zapcore.Entry) { 23 | //fmt.Println(" GoSkeleton hook ....,你可以在这里继续处理系统日志....") 24 | //fmt.Printf("%#+v\n", paramEntry) 25 | }(entry) 26 | return nil 27 | } 28 | -------------------------------------------------------------------------------- /app/core/destroy/destroy.go: -------------------------------------------------------------------------------- 1 | package destroy 2 | 3 | import ( 4 | "go.uber.org/zap" 5 | "goskeleton/app/core/event_manage" 6 | "goskeleton/app/global/consts" 7 | "goskeleton/app/global/variable" 8 | "os" 9 | "os/signal" 10 | "syscall" 11 | ) 12 | 13 | func init() { 14 | // 用于系统信号的监听 15 | go func() { 16 | c := make(chan os.Signal) 17 | signal.Notify(c, os.Interrupt, os.Kill, syscall.SIGQUIT, syscall.SIGINT, syscall.SIGTERM) // 监听可能的退出信号 18 | received := <-c //接收信号管道中的值 19 | variable.ZapLog.Warn(consts.ProcessKilled, zap.String("信号值", received.String())) 20 | (event_manage.CreateEventManageFactory()).FuzzyCall(variable.EventDestroyPrefix) 21 | close(c) 22 | os.Exit(1) 23 | }() 24 | 25 | } 26 | -------------------------------------------------------------------------------- /app/http/controller/web/upload_controller.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "goskeleton/app/global/consts" 6 | "goskeleton/app/global/variable" 7 | "goskeleton/app/service/upload_file" 8 | "goskeleton/app/utils/response" 9 | ) 10 | 11 | type Upload struct { 12 | } 13 | 14 | // 文件上传是一个独立模块,给任何业务返回文件上传后的存储路径即可。 15 | // 开始上传 16 | func (u *Upload) StartUpload(context *gin.Context) { 17 | savePath := variable.BasePath + variable.ConfigYml.GetString("FileUploadSetting.UploadFileSavePath") 18 | if r, finnalSavePath := upload_file.Upload(context, savePath); r == true { 19 | response.Success(context, consts.CurdStatusOkMsg, finnalSavePath) 20 | } else { 21 | response.Fail(context, consts.FilesUploadFailCode, consts.FilesUploadFailMsg, "") 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /docs/deploy_go.md: -------------------------------------------------------------------------------- 1 | ### 运维方案之Go 2 | > 1.我们已经介绍完毕了 `Linux` 、`Mysql` 、`Redis` 、 `Nginx` 运维,接下来终于轮到介绍 `Go程序` 了,但是编写本篇文档却是一件令人沮丧的事情..... 3 | 4 | 5 | #### 前言 6 | > 1.Go 应用程序属于业务应用,每个应用的侧重点差异很大,例如:有人关心某个go应用进程cpu、内存占用情况,有人希望获取该进程内部启动的协程总数量、以及GC状态. 7 | > 2.如果是长连接应用,更多人关注的是同时在线数量等...,如果是Rpc服务,则更多关注的是Prc提供服务的成功率数据... 8 | > 3.因此应用程序的监控指标没有统一的标准,这也导致了我们很难编写出一份完美的解决方案,监控某个具体的应用程序. 9 | > 4.想要监控go应用程序你最关心的指标,则需要自己学会编写类似 node_export ,在/metrics 地址提供数据,供prometheus 获取,在 grafana 展示。 10 | 11 | 12 | ### 这里我们介绍一些世面上已有的监控方案,但是依然不是标准的。 13 | > 1.监控 go 应用程序的原理:需要在go程序启动以后,自行收集关键指标,例如:本进程占用内存、cpu、启动的最大goroutine数量、GC等,对外提供/metrics服务地址,等待被获取。 14 | > 2.从目前网上搜索到的资料看,相关的库已经有4年没有更新了,而且是基于go1.7版本... 15 | > 3.参考地址:https://github.com/bmhatfield/go-runtime-metrics 16 | 17 | 18 | ### 最后 19 | > 1.Go 应用进程的监控,目前推荐先暂时使用 [Supervisor](supervisor.md) ,虽然界面简陋,但是依然提供了可视化的进程运行状态。 -------------------------------------------------------------------------------- /app/http/middleware/cors/cors.go: -------------------------------------------------------------------------------- 1 | package cors 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "net/http" 6 | ) 7 | 8 | // 允许跨域 9 | func Next() gin.HandlerFunc { 10 | return func(c *gin.Context) { 11 | method := c.Request.Method 12 | c.Header("Access-Control-Allow-Origin", "*") 13 | c.Header("Access-Control-Allow-Headers", "Access-Control-Allow-Headers,Authorization,User-Agent, Keep-Alive, Content-Type, X-Requested-With,X-CSRF-Token,AccessToken,Token") 14 | c.Header("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT, PATCH, OPTIONS") 15 | c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type") 16 | c.Header("Access-Control-Allow-Credentials", "true") 17 | 18 | // 放行所有OPTIONS方法 19 | if method == "OPTIONS" { 20 | c.AbortWithStatus(http.StatusAccepted) 21 | } 22 | c.Next() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/http/controller/api/home_controller.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "goskeleton/app/global/consts" 6 | "goskeleton/app/utils/response" 7 | ) 8 | 9 | type Home struct { 10 | } 11 | 12 | // 1.门户类首页新闻 13 | func (u *Home) News(context *gin.Context) { 14 | 15 | // 由于本项目骨架已经将表单验证器的字段(成员)绑定在上下文,因此可以按照 GetString()、GetInt64()、GetFloat64()等快捷获取需要的数据类型 16 | // 当然也可以通过gin框架的上下文原原始方法获取,例如: context.PostForm("name") 获取,这样获取的数据格式为文本,需要自己继续转换 17 | newsType := context.GetString(consts.ValidatorPrefix + "newsType") 18 | page := context.GetFloat64(consts.ValidatorPrefix + "page") 19 | limit := context.GetFloat64(consts.ValidatorPrefix + "limit") 20 | userIp := context.ClientIP() 21 | 22 | // 这里随便模拟一条数据返回 23 | response.Success(context, "ok", gin.H{ 24 | "newsType": newsType, 25 | "page": page, 26 | "limit": limit, 27 | "userIp": userIp, 28 | "title": "门户首页公司新闻标题001", 29 | "content": "门户新闻内容001", 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /app/utils/observer_mode/subject.go: -------------------------------------------------------------------------------- 1 | package observer_mode 2 | 3 | import "container/list" 4 | 5 | // 观察者管理中心(subject) 6 | type Subject struct { 7 | Observers *list.List 8 | params interface{} 9 | } 10 | 11 | //注册观察者角色 12 | func (s *Subject) Attach(observe ObserverInterface) { 13 | s.Observers.PushBack(observe) 14 | } 15 | 16 | //删除观察者角色 17 | func (s *Subject) Detach(observer ObserverInterface) { 18 | for ob := s.Observers.Front(); ob != nil; ob = ob.Next() { 19 | if ob.Value.(*ObserverInterface) == &observer { 20 | s.Observers.Remove(ob) 21 | break 22 | } 23 | } 24 | } 25 | 26 | //通知所有观察者 27 | func (s *Subject) Notify() { 28 | var l_temp *list.List = list.New() 29 | for ob := s.Observers.Front(); ob != nil; ob = ob.Next() { 30 | l_temp.PushBack(ob.Value) 31 | ob.Value.(ObserverInterface).Update(s) 32 | } 33 | s.Observers = l_temp 34 | } 35 | 36 | func (s *Subject) BroadCast(args ...interface{}) { 37 | s.params = args 38 | s.Notify() 39 | } 40 | 41 | func (s *Subject) GetParams() interface{} { 42 | return s.params 43 | } 44 | -------------------------------------------------------------------------------- /app/http/validator/web/users/login.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "goskeleton/app/global/consts" 6 | "goskeleton/app/http/controller/web" 7 | "goskeleton/app/http/validator/core/data_transfer" 8 | "goskeleton/app/utils/response" 9 | ) 10 | 11 | type Login struct { 12 | // 表单参数验证结构体支持匿名结构体嵌套 13 | BaseField 14 | } 15 | 16 | // 验证器语法,参见 Register.go文件,有详细说明 17 | 18 | func (l Login) CheckParams(context *gin.Context) { 19 | 20 | //1.基本的验证规则没有通过 21 | if err := context.ShouldBind(&l); err != nil { 22 | response.ValidatorError(context, err) 23 | return 24 | } 25 | 26 | // 该函数主要是将本结构体的字段(成员)按照 consts.ValidatorPrefix+ json标签对应的 键 => 值 形式绑定在上下文,便于下一步(控制器)可以直接通过 context.Get(键) 获取相关值 27 | extraAddBindDataContext := data_transfer.DataAddContext(l, consts.ValidatorPrefix, context) 28 | if extraAddBindDataContext == nil { 29 | response.ErrorSystem(context, "userLogin表单验证器json化失败", "") 30 | } else { 31 | // 验证完成,调用控制器,并将验证器成员(字段)递给控制器,保持上下文数据一致性 32 | (&web.Users{}).Login(extraAddBindDataContext) 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /app/service/users/curd/users_curd.go: -------------------------------------------------------------------------------- 1 | package curd 2 | 3 | import ( 4 | "goskeleton/app/model" 5 | "goskeleton/app/utils/md5_encrypt" 6 | ) 7 | 8 | func CreateUserCurdFactory() *UsersCurd { 9 | return &UsersCurd{model.CreateUserFactory("")} 10 | } 11 | 12 | type UsersCurd struct { 13 | userModel *model.UsersModel 14 | } 15 | 16 | func (u *UsersCurd) Register(userName, pass, userIp string) bool { 17 | pass = md5_encrypt.Base64Md5(pass) // 预先处理密码加密,然后存储在数据库 18 | return u.userModel.Register(userName, pass, userIp) 19 | } 20 | 21 | func (u *UsersCurd) Store(name string, pass string, realName string, phone string, remark string) bool { 22 | 23 | pass = md5_encrypt.Base64Md5(pass) // 预先处理密码加密,然后存储在数据库 24 | return u.userModel.Store(name, pass, realName, phone, remark) 25 | } 26 | 27 | func (u *UsersCurd) Update(id int, name string, pass string, realName string, phone string, remark string, clientIp string) bool { 28 | //预先处理密码加密等操作,然后进行更新 29 | pass = md5_encrypt.Base64Md5(pass) // 预先处理密码加密,然后存储在数据库 30 | return u.userModel.Update(id, name, pass, realName, phone, remark, clientIp) 31 | } 32 | -------------------------------------------------------------------------------- /app/http/validator/web/users/refresh_token.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "goskeleton/app/global/consts" 6 | "goskeleton/app/http/controller/web" 7 | "goskeleton/app/utils/response" 8 | "strings" 9 | ) 10 | 11 | type RefreshToken struct { 12 | Authorization string `json:"token" header:"Authorization" binding:"required,min=20"` 13 | } 14 | 15 | // 验证器语法,参见 Register.go文件,有详细说明 16 | 17 | func (r RefreshToken) CheckParams(context *gin.Context) { 18 | 19 | //1.基本的验证规则没有通过 20 | if err := context.ShouldBindHeader(&r); err != nil { 21 | // 将表单参数验证器出现的错误直接交给错误翻译器统一处理即可 22 | response.ValidatorError(context, err) 23 | return 24 | } 25 | token := strings.Split(r.Authorization, " ") 26 | if len(token) == 2 { 27 | context.Set(consts.ValidatorPrefix+"token", token[1]) 28 | (&web.Users{}).RefreshToken(context) 29 | } else { 30 | errs := gin.H{ 31 | "tips": "Token不合法,token请放置在header头部分,按照按=>键提交,例如:Authorization:Bearer 你的实际token....", 32 | } 33 | response.Fail(context, consts.JwtTokenFormatErrCode, consts.JwtTokenFormatErrMsg, errs) 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module goskeleton 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/casbin/casbin/v2 v2.66.3 7 | github.com/casbin/gorm-adapter/v3 v3.15.1 8 | github.com/dchest/captcha v1.0.0 9 | github.com/dgrijalva/jwt-go v3.2.1-0.20210802184156-9742bd7fca1c+incompatible 10 | github.com/fsnotify/fsnotify v1.6.0 11 | github.com/gin-contrib/pprof v1.4.0 12 | github.com/gin-gonic/gin v1.9.0 13 | github.com/go-playground/locales v0.14.1 14 | github.com/go-playground/universal-translator v0.18.1 15 | github.com/go-playground/validator/v10 v10.12.0 16 | github.com/gomodule/redigo v1.8.9 17 | github.com/gorilla/websocket v1.5.0 18 | github.com/natefinch/lumberjack v2.0.0+incompatible 19 | github.com/qifengzhang007/goCurl v1.3.9 20 | github.com/rabbitmq/amqp091-go v1.8.0 21 | github.com/spf13/cobra v1.7.0 22 | github.com/spf13/viper v1.15.0 23 | go.uber.org/zap v1.24.0 24 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect 25 | gorm.io/driver/mysql v1.4.7 26 | gorm.io/driver/postgres v1.5.0 27 | gorm.io/driver/sqlserver v1.4.2 28 | gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11 29 | gorm.io/plugin/dbresolver v1.4.1 30 | ) 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 张奇峰 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/http/validator/api/home/news.go: -------------------------------------------------------------------------------- 1 | package home 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "goskeleton/app/global/consts" 6 | "goskeleton/app/http/controller/api" 7 | common_data_type "goskeleton/app/http/validator/common/data_type" 8 | "goskeleton/app/http/validator/core/data_transfer" 9 | "goskeleton/app/utils/response" 10 | ) 11 | 12 | // 门户类前端接口模拟一个获取新闻的参数验证器 13 | 14 | type News struct { 15 | NewsType string `form:"newsType" json:"newsType" binding:"required,min=1"` // 验证规则:必填,最小长度为1 16 | common_data_type.Page 17 | } 18 | 19 | func (n News) CheckParams(context *gin.Context) { 20 | //1.先按照验证器提供的基本语法,基本可以校验90%以上的不合格参数 21 | if err := context.ShouldBind(&n); err != nil { 22 | // 将表单参数验证器出现的错误直接交给错误翻译器统一处理即可 23 | response.ValidatorError(context, err) 24 | return 25 | } 26 | 27 | // 该函数主要是将绑定的数据以 键=>值 形式直接传递给下一步(控制器) 28 | extraAddBindDataContext := data_transfer.DataAddContext(n, consts.ValidatorPrefix, context) 29 | if extraAddBindDataContext == nil { 30 | response.ErrorSystem(context, "HomeNews表单验证器json化失败", "") 31 | } else { 32 | // 验证完成,调用控制器,并将验证器成员(字段)递给控制器,保持上下文数据一致性 33 | (&api.Home{}).News(extraAddBindDataContext) 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /app/utils/snow_flake/snow_flake.go: -------------------------------------------------------------------------------- 1 | package snow_flake 2 | 3 | import ( 4 | "goskeleton/app/global/consts" 5 | "goskeleton/app/global/variable" 6 | "goskeleton/app/utils/snow_flake/snowflake_interf" 7 | "sync" 8 | "time" 9 | ) 10 | 11 | // 创建一个雪花算法生成器(生成工厂) 12 | func CreateSnowflakeFactory() snowflake_interf.InterfaceSnowFlake { 13 | return &snowflake{ 14 | timestamp: 0, 15 | machineId: variable.ConfigYml.GetInt64("SnowFlake.SnowFlakeMachineId"), 16 | sequence: 0, 17 | } 18 | } 19 | 20 | type snowflake struct { 21 | sync.Mutex 22 | timestamp int64 23 | machineId int64 24 | sequence int64 25 | } 26 | 27 | // 生成分布式ID 28 | func (s *snowflake) GetId() int64 { 29 | s.Lock() 30 | defer func() { 31 | s.Unlock() 32 | }() 33 | now := time.Now().UnixNano() / 1e6 34 | if s.timestamp == now { 35 | s.sequence = (s.sequence + 1) & consts.SequenceMask 36 | if s.sequence == 0 { 37 | for now <= s.timestamp { 38 | now = time.Now().UnixNano() / 1e6 39 | } 40 | } 41 | } else { 42 | s.sequence = 0 43 | } 44 | s.timestamp = now 45 | r := (now-consts.StartTimeStamp)<=1 15 | common_data_type.Page 16 | } 17 | 18 | // 验证器语法,参见 Register.go文件,有详细说明 19 | func (s Show) CheckParams(context *gin.Context) { 20 | //1.基本的验证规则没有通过 21 | if err := context.ShouldBind(&s); err != nil { 22 | // 将表单参数验证器出现的错误直接交给错误翻译器统一处理即可 23 | response.ValidatorError(context, err) 24 | return 25 | } 26 | 27 | // 该函数主要是将本结构体的字段(成员)按照 consts.ValidatorPrefix+ json标签对应的 键 => 值 形式绑定在上下文,便于下一步(控制器)可以直接通过 context.Get(键) 获取相关值 28 | extraAddBindDataContext := data_transfer.DataAddContext(s, consts.ValidatorPrefix, context) 29 | if extraAddBindDataContext == nil { 30 | response.ErrorSystem(context, "UserShow表单验证器json化失败", "") 31 | } else { 32 | // 验证完成,调用控制器,并将验证器成员(字段)递给控制器,保持上下文数据一致性 33 | (&web.Users{}).Show(extraAddBindDataContext) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/http/validator/core/data_transfer/data_transfer.go: -------------------------------------------------------------------------------- 1 | package data_transfer 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/gin-gonic/gin" 6 | "goskeleton/app/global/variable" 7 | "goskeleton/app/http/validator/core/interf" 8 | "time" 9 | ) 10 | 11 | // 将验证器成员(字段)绑定到数据传输上下文,方便控制器获取 12 | /** 13 | 本函数参数说明: 14 | validatorInterface 实现了验证器接口的结构体 15 | extra_add_data_prefix 验证器绑定参数传递给控制器的数据前缀 16 | context gin上下文 17 | */ 18 | 19 | func DataAddContext(validatorInterface interf.ValidatorInterface, extraAddDataPrefix string, context *gin.Context) *gin.Context { 20 | var tempJson interface{} 21 | if tmpBytes, err1 := json.Marshal(validatorInterface); err1 == nil { 22 | if err2 := json.Unmarshal(tmpBytes, &tempJson); err2 == nil { 23 | if value, ok := tempJson.(map[string]interface{}); ok { 24 | for k, v := range value { 25 | context.Set(extraAddDataPrefix+k, v) 26 | } 27 | // 此外给上下文追加三个键:created_at 、 updated_at 、 deleted_at ,实际根据需要自己选择获取相关键值 28 | curDateTime := time.Now().Format(variable.DateFormat) 29 | context.Set(extraAddDataPrefix+"created_at", curDateTime) 30 | context.Set(extraAddDataPrefix+"updated_at", curDateTime) 31 | context.Set(extraAddDataPrefix+"deleted_at", curDateTime) 32 | return context 33 | } 34 | } 35 | } 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /app/http/validator/web/users/store.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "goskeleton/app/global/consts" 6 | "goskeleton/app/http/controller/web" 7 | "goskeleton/app/http/validator/core/data_transfer" 8 | "goskeleton/app/utils/response" 9 | ) 10 | 11 | type Store struct { 12 | BaseField 13 | // 表单参数验证结构体支持匿名结构体嵌套、以及匿名结构体与普通字段组合 14 | RealName string `form:"real_name" json:"real_name" binding:"required,min=2"` 15 | Phone string `form:"phone" json:"phone" binding:"required,len=11"` 16 | Remark string `form:"remark" json:"remark" ` 17 | } 18 | 19 | // 验证器语法,参见 Register.go文件,有详细说明 20 | 21 | func (s Store) CheckParams(context *gin.Context) { 22 | //1.基本的验证规则没有通过 23 | if err := context.ShouldBind(&s); err != nil { 24 | // 将表单参数验证器出现的错误直接交给错误翻译器统一处理即可 25 | response.ValidatorError(context, err) 26 | return 27 | } 28 | 29 | // 该函数主要是将本结构体的字段(成员)按照 consts.ValidatorPrefix+ json标签对应的 键 => 值 形式绑定在上下文,便于下一步(控制器)可以直接通过 context.Get(键) 获取相关值 30 | extraAddBindDataContext := data_transfer.DataAddContext(s, consts.ValidatorPrefix, context) 31 | if extraAddBindDataContext == nil { 32 | response.ErrorSystem(context, "UserStore表单验证器json化失败", "") 33 | } else { 34 | // 验证完成,调用控制器,并将验证器成员(字段)递给控制器,保持上下文数据一致性 35 | (&web.Users{}).Store(extraAddBindDataContext) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/http/validator/web/users/update.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "goskeleton/app/global/consts" 6 | "goskeleton/app/http/controller/web" 7 | "goskeleton/app/http/validator/core/data_transfer" 8 | "goskeleton/app/utils/response" 9 | ) 10 | 11 | type Update struct { 12 | BaseField 13 | Id 14 | // 表单参数验证结构体支持匿名结构体嵌套、以及匿名结构体与普通字段组合 15 | RealName string `form:"real_name" json:"real_name" binding:"required,min=2"` 16 | Phone string `form:"phone" json:"phone" binding:"required,len=11"` 17 | Remark string `form:"remark" json:"remark"` 18 | } 19 | 20 | // 验证器语法,参见 Register.go文件,有详细说明 21 | 22 | func (u Update) CheckParams(context *gin.Context) { 23 | //1.基本的验证规则没有通过 24 | if err := context.ShouldBind(&u); err != nil { 25 | // 将表单参数验证器出现的错误直接交给错误翻译器统一处理即可 26 | response.ValidatorError(context, err) 27 | return 28 | } 29 | 30 | // 该函数主要是将本结构体的字段(成员)按照 consts.ValidatorPrefix+ json标签对应的 键 => 值 形式绑定在上下文,便于下一步(控制器)可以直接通过 context.Get(键) 获取相关值 31 | extraAddBindDataContext := data_transfer.DataAddContext(u, consts.ValidatorPrefix, context) 32 | if extraAddBindDataContext == nil { 33 | response.ErrorSystem(context, "UserUpdate表单验证器json化失败", "") 34 | } else { 35 | // 验证完成,调用控制器,并将验证器成员(字段)递给控制器,保持上下文数据一致性 36 | (&web.Users{}).Update(extraAddBindDataContext) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/snowflake_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "goskeleton/app/global/variable" 5 | _ "goskeleton/bootstrap" 6 | "sync" 7 | "testing" 8 | ) 9 | 10 | // 雪花算法单元测试 11 | 12 | func TestSnowFlake(t *testing.T) { 13 | // 并发 3万 测试,实际业务场景中,并发是不可能达到 3万 这个值的 14 | var slice1 []int64 15 | var vMuext sync.Mutex 16 | var wg sync.WaitGroup 17 | wg.Add(30000) 18 | 19 | for i := 1; i <= 30000; i++ { 20 | go func() { 21 | defer wg.Done() 22 | //加锁操作主要是为了保证切片([]int64)的并发安全, 23 | //我们本次测试的核心目的是雪花算法生成的ID必须是唯一的 24 | vMuext.Lock() 25 | slice1 = append(slice1, variable.SnowFlake.GetId()) 26 | vMuext.Unlock() 27 | //fmt.Printf("%d\n", variable.SnowFlake.GetId()) 28 | }() 29 | } 30 | 31 | wg.Wait() 32 | 33 | if lastLen := len(RemoveRepeatedElement(slice1)); lastLen == 30000 { 34 | t.Log("单元测试OK") 35 | } else { 36 | t.Errorf("雪花算法单元测试失败,并发 3万 生成的id经过去重之后,小于预期个数,去重后的个数:%d\n", lastLen) 37 | } 38 | } 39 | 40 | // 切片去重 41 | func RemoveRepeatedElement(arr []int64) (newArr []int64) { 42 | newArr = make([]int64, 0) 43 | for i := 0; i < len(arr); i++ { 44 | repeat := false 45 | for j := i + 1; j < len(arr); j++ { 46 | if arr[i] == arr[j] { 47 | repeat = true 48 | break 49 | } 50 | } 51 | if !repeat { 52 | newArr = append(newArr, arr[i]) 53 | } 54 | } 55 | return 56 | } 57 | -------------------------------------------------------------------------------- /docs/project_struct.md: -------------------------------------------------------------------------------- 1 | ### 项目结构目录介绍 2 | > 1.主要介绍本项目骨架的核心目录结构 3 | 4 | ```code 5 | |-- app 6 | | |-- aop // Aop切面demo代码段 7 | | | `-- users 8 | | |-- core // 程序容器部分、用于表单参数器注册、配置文件存储等 9 | | | |-- container 10 | | | |-- destroy 11 | | | `-- event_manage 12 | | |-- global // 全局变量以及常量、程序运行错误定义 13 | | | |-- consts 14 | | | |-- my_errors 15 | | | `-- variable 16 | | |-- http // http相关代码段,主要为控制器、中间件、表单参数验证器 17 | | | |-- controller 18 | | | |-- middleware 19 | | | `-- validator 20 | | |-- model // 数据库表模型 21 | | | |-- base_model.go 22 | | | `-- users.go 23 | | |-- service 24 | | | |-- sys_log_hook 25 | | `-- utils // 第三方包封装层 26 | | |-- gorm_v2 27 | | |-- ... ... 28 | |-- bootstrap // 项目启动初始化代码段 29 | | `-- init.go 30 | |-- cmd // 项目入口,分别为门户站点、命令模式、web后端入口文件 31 | | |-- api 32 | | | `-- main.go 33 | | |-- cli 34 | | | `-- main.go 35 | | `-- web 36 | | `-- main.go 37 | |-- command // cli模式代码目录 38 | | |-- 39 | |-- config // 项目、数据库参数配置 40 | | |-- config.yml 41 | | `-- gorm_v2.yml 42 | |-- database 43 | |-- docs // 项目文档 44 | | |-- 45 | |-- go.mod 46 | |-- go.sum 47 | |-- public 48 | |-- routers // 后台和门户网站路由 49 | | |-- api.go 50 | | `-- web.go 51 | |-- storage // 日志、资源存储目录 52 | | `-- 53 | `-- test// 单元测试目录 54 | |-- 55 | ``` -------------------------------------------------------------------------------- /app/utils/files/baseInfo.go: -------------------------------------------------------------------------------- 1 | package files 2 | 3 | import ( 4 | "goskeleton/app/global/my_errors" 5 | "goskeleton/app/global/variable" 6 | "mime/multipart" 7 | "net/http" 8 | "os" 9 | ) 10 | 11 | // 返回值说明: 12 | // 7z、exe、doc 类型会返回 application/octet-stream 未知的文件类型 13 | // jpg => image/jpeg 14 | // png => image/png 15 | // ico => image/x-icon 16 | // bmp => image/bmp 17 | // xlsx、docx 、zip => application/zip 18 | // tar.gz => application/x-gzip 19 | // txt、json、log等文本文件 => text/plain; charset=utf-8 备注:就算txt是gbk、ansi编码,也会识别为utf-8 20 | 21 | // 通过文件名获取文件mime信息 22 | func GetFilesMimeByFileName(filepath string) string { 23 | f, err := os.Open(filepath) 24 | if err != nil { 25 | variable.ZapLog.Error(my_errors.ErrorsFilesUploadOpenFail + err.Error()) 26 | } 27 | defer f.Close() 28 | 29 | // 只需要前 32 个字节就可以了 30 | buffer := make([]byte, 32) 31 | if _, err := f.Read(buffer); err != nil { 32 | variable.ZapLog.Error(my_errors.ErrorsFilesUploadReadFail + err.Error()) 33 | return "" 34 | } 35 | 36 | return http.DetectContentType(buffer) 37 | } 38 | 39 | // 通过文件指针获取文件mime信息 40 | func GetFilesMimeByFp(fp multipart.File) string { 41 | 42 | buffer := make([]byte, 32) 43 | if _, err := fp.Read(buffer); err != nil { 44 | variable.ZapLog.Error(my_errors.ErrorsFilesUploadReadFail + err.Error()) 45 | return "" 46 | } 47 | 48 | return http.DetectContentType(buffer) 49 | } 50 | -------------------------------------------------------------------------------- /command/demo_simple/simple.go: -------------------------------------------------------------------------------- 1 | package demo_simple 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "goskeleton/app/global/variable" 6 | "time" 7 | ) 8 | 9 | var ( 10 | LogAction string 11 | Date string 12 | logger = variable.ZapLog.Sugar() 13 | ) 14 | 15 | // 简单示例 16 | var DemoSimple = &cobra.Command{ 17 | Use: "demo_simple", 18 | Aliases: []string{"demo_simple"}, // 定义别名 19 | Short: "这是一个最简单的demo示例", 20 | Long: `调用方法: 21 | 1.进入项目根目录(Ginkeleton)。 22 | 2.执行 go run cmd/cli/main.go demo_simple -h //可以查看使用指南 23 | 3.执行 go run cmd/cli/main.go demo_simple -A create // 通过 Action 动作执行相应的命令 24 | `, 25 | // Run 命令是 核心 命令,其余命令都是为该命令服务,可以删除,由您自由选择 26 | Run: func(cmd *cobra.Command, args []string) { 27 | //args 参数表示非flag(也叫作位置参数),该参数默认会作为一个数组存储。 28 | //fmt.Println(args) 29 | start(LogAction, Date) 30 | }, 31 | } 32 | 33 | // 注册命令、初始化参数 34 | func init() { 35 | DemoSimple.Flags().StringVarP(&LogAction, "logAction", "A", "insert", "-A 指定参数动作,例如:-A insert ") 36 | DemoSimple.Flags().StringVarP(&Date, "date", "D", time.Now().Format("2006-01-02"), "-D 指定日期,例如:-D 2021-09-13") 37 | } 38 | 39 | // 开始执行业务 40 | func start(actionName, Date string) { 41 | switch actionName { 42 | case "insert": 43 | logger.Info("insert 参数执行对应业务逻辑,Date参数值:" + Date) 44 | case "update": 45 | logger.Info("update 参数执行对应业务逻辑,Date参数值:" + Date) 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /ReadME.md: -------------------------------------------------------------------------------- 1 | ## 这是什么? 2 | - 1.这是一个基于go语言gin框架的web项目骨架,专注于前后端分离的业务场景,其目的主要在于将web项目主线逻辑梳理清晰,最基础的东西封装完善,开发者更多关注属于自己的的业务即可。 3 | - 2.本项目骨架封装了以`tb_users`表为核心的全部功能(主要包括用户相关的接口参数验证器、注册、登录获取token、刷新token、CURD以及token鉴权等),开发者拉取本项目骨架,在此基础上就可以快速开发自己的项目。 4 | - 3.本项目骨架请使用 `master` 分支版本即可, 该分支是最新稳定分支 . 5 | - 4.本项目骨架从V1.4.00开始,要求go语言版本必须 >=1.15,才能稳定地使用gorm v2读写分离方案,go1.15下载地址:https://studygolang.com/dl 6 | - 5.该版本定位为主线版本,总体追求简洁、无界面,没有太多的业务逻辑,适合开发者自己随意扩展. 7 | 8 | ### [GinSkeleton 新版在线文档](https://www.yuque.com/xiaofensinixidaouxiang/bkfhct/mar1g7) 9 | - 1.我们花费了极大的精力编写了非常完整、高质量的文档,初学者优先从如何使用学起, 成熟的开发者可以与我们一起研究 gin 内核源码,成为 gin 框架的高级开发. 10 | - 2.学习 GinSkeleton 您只需要关注主线即可,我们没有创造太多新的语法,只要您会使用 gin 就可以迅速上手 Ginskeleton . 11 | - 3. $\color{red}{QQ群:129885228}$ 12 | 13 | [旧文档入口](./ReadMEBak.md) 14 | 15 | 16 | ### ginskeleton 路由跳转插件 17 | - ginskeleton 的路由是基于容器加载的,需要安装插件才能实现快速跳转. 18 | - 安装步骤:https://www.yuque.com/xiaofensinixidaouxiang/bkfhct/ngfzv1 19 | 20 | 21 | ### [点击进入 GinSkeleton-Admin2 系统](https://www.yuque.com/xiaofensinixidaouxiang/qmanaq/qmucb4) 22 | - admin 系统集成界面, 定位快速开发业务方向, 可以在不需要修改一行代码的情况下,快速进入业务开发模式. 23 | 24 | 25 | 26 | #### V 1.5.59 2023-04-07(最新版本) 27 | **更新** 28 | - 1.完善细节:系统错误记录时相关的日志记录键名调整. 29 | - 2.项目依赖包全部更新至最新版. 30 | 31 | 32 | ### 感谢 jetbrains 为本项目提供的 goland 激活码 33 | ![https://www.jetbrains.com/](https://www.ginskeleton.com/images/jetbrains.jpg) 34 | -------------------------------------------------------------------------------- /docs/sql_stament.md: -------------------------------------------------------------------------------- 1 | ### Sql操作命令集合 2 | >本文档主要介绍了sql操作的核心命令,详细操作命令示例代码参见 [mysql示例文档](../app/model/test.go). [sqlserver测试用例](../test/db_sqlserver_test.go) , [postgreSql测试用例](../test/db_postgresql_test.go) 操作方式同 mysql . 3 | 4 | #### 1.查询类: 不会修改数据的sql、存储过程、视图 5 | ```sql 6 | // 首先获取一个数据连接 7 | sqlservConn := sql_factory.GetOneSqlClient("postgre") // 参数为空,默认就是mysql驱动,您还可以传递 sqlserver 、 postgresql 参数获取对应数据库的一个连接. 8 | #1.多条查询: 9 | sqlservConn.QuerySql 10 | #2.单条查询: 11 | sqlservConn.QueryRow 12 | ``` 13 | 14 | #### 2.执行类: 会修改数据的sql、存储过程等 15 | ```sql 16 | #1.执行命令,主要有 insert 、 updated 、 delete 17 | sqlservConn.ExecuteSql 18 | ``` 19 | 20 | #### 3.预处理类:如果场景需要批量插入很多条数据,那么就需要独立调用预编译 21 | > 1.如果你的sql语句需要循环插入1万、5万、10万+数据。 22 | > 2.那么可能会报错: Error 1461: Can't create more than max_prepared_stmt_count statements (current value: 16382) 23 | > 3.此时需要以下解决方案 24 | ```sql 25 | #1.预编译,预处理类之后,执行批量语句 26 | sqlservConn.PrepareSql 27 | #2.(多条)执行类 28 | sqlservConn.ExecuteSqlForMultiple 29 | #3.(多条)查询类 30 | sqlservConn.QuerySqlForMultiple 31 | ``` 32 | 33 | #### 4.事务类操作 34 | ```sql 35 | #1.开启一个事务 36 | tx:=sqlservConn.BeginTx() 37 | 38 | #2.预编译sql 39 | tx.Prepare 40 | 41 | #3.执行sql 42 | tx.Exec 43 | 44 | #4.提交 45 | tx.Commit 46 | 47 | #5.回滚 48 | tx.Rollback 49 | ``` 50 | -------------------------------------------------------------------------------- /docs/project_analysis_1.md: -------------------------------------------------------------------------------- 1 | ## GoSkeleton 项目骨架性能分析报告(一) 2 | > 1.本次将按照一次请求的生命周期为主线(request--->response),跟踪各部分代码段的cpu耗时,得出可视化的性能报告. 3 | 4 | ### 前言 5 | > 1.本次分析,我们以项目骨架默认的门户网站接口为例,该接口虽然简单,但是包含了一个 request 到 response 完整生命周期主线逻辑,很具有代表性. 6 | > 2.待分析的接口地址:`http://127.0.0.1:20191/api/v1/home/news?newsType=portal&page=1&limit=50` 7 | 8 | ### cpu数据采集步骤 9 | > 1.`config/config.yml` 文件中,AppDebug 设置为 true , 调试模式才能进行分析. 10 | > 2.访问接口:`http://127.0.0.1:20191/`, 确保项目正常启动. 11 | > 3.浏览器访问pprof接口:`http://127.0.0.1:20191/debug/pprof/`, 点击 `profile` 选项,程序会对本项目进程, 进行 cpu 使用情况底层数据采集, 该过程会持续 30 秒. 12 | ![pprof地址](https://www.ginskeleton.com/images/pprof_menue.jpg) 13 | > 4.第3步点击以后,必须快速运行 [pprof测试用例](../test/http_client_test.go) 中的 `TestPprof()` 函数,该函数主要负责请求接口,让程序处理业务返回结果, 模拟 request --> response 过程. 14 | > 5.执行了步骤3和步骤4才能采集到数据,稍等片刻,30秒之后,您点击过的步骤3就会提示下载文件:`profile`, 请保存在您能记住的路径中,稍后马上使用该文件(profile), 至此cpu数据已经采集完毕. 15 | 16 | ### cpu数据分析步骤 17 | > 1.首先下载安装 [graphviz](https://www.graphviz.org/download/) ,根据您的系统选择相对应的版本安装,安装完成记得将`安装目录/bin`, 加入系统环境变量. 18 | > 2.打开cmd窗口,执行 `dot -V` ,会显示版本信息,说明安装已经OK, 那么继续执行 `dot -c` 安装图形显示所需要的插件. 19 | > 3.在cpu数据采集环节第三步,您已经得到了 `profile` 文件,那么就在同目录打开cmd窗口,执行 `go tool pprof profile`, 然后输入 `web` 回车就会自动打开浏览器,展示给您如下结果图: 20 | ![cpu分析_上](https://www.ginskeleton.com/images/pprof_cmd.jpg) 21 | 22 | ### 报告详情参见如下图 23 | ![cpu分析_上](https://www.ginskeleton.com/images/analysis1.png) 24 | 25 | -------------------------------------------------------------------------------- /docs/deploy_mysql.md: -------------------------------------------------------------------------------- 1 | ### 运维方案之Mysql 2 | > 1.[上一篇](./deploy_linux.md) 已经介绍完毕了 `linux` 服务器运维管控,我们花费了非常多的心思编写文档、截图、目的就是让每个需要的人能100%达到理想效果,同时我们希望,如果您完成了上一篇配置,那么就必须要梳理一下流程,对整个操作模式有清晰的认识。 3 | > 2.运维的整体操作流程主要有:配置数据源、在`grafana` 官网寻找合适的模板、导入,至于再上一层楼,您可以自行编写模板。 4 | > 3.本篇我们开始介绍 `mysql` 的运维监控。 5 | 6 | #### 特别提醒 7 | > 1.阿里云RDS数据库不允许获取数据库底层状态数据,相关函数无权限调用、执行,就算是厂家分配的最高权限,也无法获取主要的底层状态数据,因此RDS数据库无法使用本篇方案。 8 | > 2.但是RDS数据库,厂家提供了强大的后台运维界面,能够直观地监控数据库运行状态、占用的存储空间、性能、连接数、并发等,因此基本不需要本篇介绍的功能,您可以无视本篇。 9 | 10 | #### 正式开始部署mysql运维监控 11 | > 1.本篇mysql监控的原理主要是通过账号、密码连接数据库,通过数据库自带函数获取mysql运行状态,在grafana展示。 12 | > 2.mysql的部署我们将以纯文本介绍,截图会导致项目体积变大,不利于下载。本篇所有的操作步骤都可以在上一篇找到截图,参考 [linux服务器运维](./deploy_linux.md) ,如果依然不明白,可以直接提 issue 。 13 | ```code 14 | #前言:本次mysql监控模板,是基于my2的,也就是说,首先你需要初始化一个my2数据库,才能正确显示本次模板 15 | https://github.com/meob/my2Collector # 从github找到my2.sql,复制里面的代码,粘贴到mysql管理端,直接使用root账号执行即可,或者使用官方推荐的sql导入方式同样可以初始化一个my2数据库。 16 | 17 | # step1:添加数据源 18 | 齿轮 —— Data Source —— Add data source —— 输入关键词mysql 搜索 —— 选中数据源,出现配置界面,进行账号、密码、端口配置 —— sava&test。 19 | 20 | # step2: grafana 官方寻找 mysql 监控模板,例如:https://grafana.com/grafana/dashboards/7991,注意模板说明,是否依赖于my2数据库,如果依赖my2数据库,就必须先导入my2.sql数据库。 21 | https://grafana.com/grafana/dashboards // grafana 搜索模板地址,找到模板复制 id 号,本次模板ID :7991 22 | 23 | # step3: 在 grafana 后台找到 import 导入模板ID:7991,数据源选择 mysql 即可。 24 | 25 | ``` 26 | 27 | #### mysql 最终监控效果图 28 | ![mysql监控效果图](https://www.ginskeleton.com/images/mysql.png) 29 | 30 | -------------------------------------------------------------------------------- /app/http/validator/common/websocket/connect.go: -------------------------------------------------------------------------------- 1 | package websocket 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "go.uber.org/zap" 6 | "goskeleton/app/global/consts" 7 | "goskeleton/app/global/variable" 8 | controllerWs "goskeleton/app/http/controller/websocket" 9 | "goskeleton/app/http/validator/core/data_transfer" 10 | ) 11 | 12 | type Connect struct { 13 | Token string `form:"token" json:"token" binding:"required,min=10"` 14 | } 15 | 16 | // 验证器语法,参见 Register.go文件,有详细说明 17 | // 注意:websocket 连接建立之前如果有错误,只能在服务端同构日志输出方式记录(因为使用response.Fail等函数,客户端是收不到任何信息的) 18 | 19 | func (c Connect) CheckParams(context *gin.Context) { 20 | 21 | // 1. 首先检查是否开启websocket服务配置(在配置项中开启) 22 | if variable.ConfigYml.GetInt("Websocket.Start") != 1 { 23 | variable.ZapLog.Error(consts.WsServerNotStartMsg) 24 | return 25 | } 26 | //2.基本的验证规则没有通过 27 | if err := context.ShouldBind(&c); err != nil { 28 | variable.ZapLog.Error("客户端上线参数不合格", zap.Error(err)) 29 | return 30 | } 31 | extraAddBindDataContext := data_transfer.DataAddContext(c, consts.ValidatorPrefix, context) 32 | if extraAddBindDataContext == nil { 33 | variable.ZapLog.Error("websocket-Connect 表单验证器json化失败") 34 | context.Abort() 35 | return 36 | } else { 37 | if serviceWs, ok := (&controllerWs.Ws{}).OnOpen(extraAddBindDataContext); ok == false { 38 | variable.ZapLog.Error(consts.WsOpenFailMsg) 39 | } else { 40 | (&controllerWs.Ws{}).OnMessage(serviceWs, extraAddBindDataContext) // 注意这里传递的service_ws必须是调用open返回的,必须保证的ws对象的一致性 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /docs/captcha.md: -------------------------------------------------------------------------------- 1 | ## 验证码 2 | > 1.基于 `github.com/dchest/captcha` 包封装. 3 | > 2.本项目只提供了数字验证功能,没有封装语音验证功能. 4 | 5 | ### 定义的路由地址 6 | > 1.[路由地址](../routers/web.go) 7 | 8 | ### 验证码业务控制器地址 9 | > 1.[验证码业务](../app/http/controller/chaptcha/chaptcha.go) , 验证数字长度、验证码尺寸(宽 x 高)在这里设置. 10 | 11 | ### 使用步骤 12 | > 1.获取验证码ID等信息 13 | ```code 14 | # get 方式请求获取验证ID等信息 15 | http://127.0.0.1:20201/captcha/ 16 | 17 | #返回值中携带了获取验证码图片的地址以及校验地址 18 | 19 | ``` 20 | > 2.获取验证码 21 | ```code 22 | # get , 根据步骤1中返回值提示获取 验证码ID 23 | http://127.0.0.1:20201/captcha/验证码ID.png 24 | ``` 25 | 26 | > 3.校验验证码 27 | ```code 28 | # get , 根据步骤1中返回值提示进行校验验证即可 29 | http://127.0.0.1:20201/captcha/验证码ID/验证码正确值 30 | ``` 31 | 32 | ### 任何路由(接口)都可以调用我们封装好的验证码中间件 33 | - 1.已经封装好的验证码中间件:authorization.CheckCaptchaAuth() 34 | - 2.一般是登录接口,需要验证码校验,那么我们可以直接调用验证码中间件增加校验机制。 35 | - 3.注意:如果直接调用了验证码中间件,一般都是和登陆接口搭配,所以请求方式为 `POST` 36 | 37 | ```code 38 | 39 | // 已有的登陆接口(路由),不需要验证码即可登陆 40 | noAuth.POST("login", validatorFactory.Create(consts.ValidatorPrefix+"UsersLogin")) 41 | 42 | // 只需要添加验证码中间件即可启动登陆前的验证机制 43 | // 本质上就是给登陆接口增加了2个参数:验证码id提交时的键:captcha_id 和 验证码值提交时的键 captcha_value,具体参见配置文件 44 | //noAuth.Use(authorization.CheckCaptchaAuth()).POST("login", validatorFactory.Create(consts.ValidatorPrefix+"UsersLogin")) 45 | 46 | ``` 47 | 48 | ### 备注说明 49 | > 1.验证码ID一旦提交到校验接口(步骤3)进行验证,不管输入的验证码正确与否,该ID都会失败,需要从步骤1开始重新获取. 50 | -------------------------------------------------------------------------------- /app/http/validator/common/register_validator/web_register_validator.go: -------------------------------------------------------------------------------- 1 | package register_validator 2 | 3 | import ( 4 | "goskeleton/app/core/container" 5 | "goskeleton/app/global/consts" 6 | "goskeleton/app/http/validator/common/upload_files" 7 | "goskeleton/app/http/validator/common/websocket" 8 | "goskeleton/app/http/validator/web/users" 9 | ) 10 | 11 | // 各个业务模块验证器必须进行注册(初始化),程序启动时会自动加载到容器 12 | func WebRegisterValidator() { 13 | //创建容器 14 | containers := container.CreateContainersFactory() 15 | 16 | // key 按照前缀+模块+验证动作 格式,将各个模块验证注册在容器 17 | var key string 18 | // Users 模块表单验证器按照 key => value 形式注册在容器,方便路由模块中调用 19 | key = consts.ValidatorPrefix + "UsersRegister" 20 | containers.Set(key, users.Register{}) 21 | key = consts.ValidatorPrefix + "UsersLogin" 22 | containers.Set(key, users.Login{}) 23 | key = consts.ValidatorPrefix + "RefreshToken" 24 | containers.Set(key, users.RefreshToken{}) 25 | 26 | // Users基本操作(CURD) 27 | key = consts.ValidatorPrefix + "UsersShow" 28 | containers.Set(key, users.Show{}) 29 | key = consts.ValidatorPrefix + "UsersStore" 30 | containers.Set(key, users.Store{}) 31 | key = consts.ValidatorPrefix + "UsersUpdate" 32 | containers.Set(key, users.Update{}) 33 | key = consts.ValidatorPrefix + "UsersDestroy" 34 | containers.Set(key, users.Destroy{}) 35 | 36 | // 文件上传 37 | key = consts.ValidatorPrefix + "UploadFiles" 38 | containers.Set(key, upload_files.UpFiles{}) 39 | 40 | // Websocket 连接验证器 41 | key = consts.ValidatorPrefix + "WebsocketConnect" 42 | containers.Set(key, websocket.Connect{}) 43 | } 44 | -------------------------------------------------------------------------------- /app/http/validator/web/users/destroy.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "goskeleton/app/global/consts" 6 | "goskeleton/app/http/controller/web" 7 | "goskeleton/app/http/validator/core/data_transfer" 8 | "goskeleton/app/utils/response" 9 | ) 10 | 11 | type Destroy struct { 12 | // 表单参数验证结构体支持匿名结构体嵌套、以及匿名结构体与普通字段组合 13 | Id 14 | } 15 | 16 | // 验证器语法,参见 Register.go文件,有详细说明 17 | 18 | func (d Destroy) CheckParams(context *gin.Context) { 19 | 20 | if err := context.ShouldBind(&d); err != nil { 21 | // 将表单参数验证器出现的错误直接交给错误翻译器统一处理即可 22 | response.ValidatorError(context, err) 23 | return 24 | } 25 | 26 | // 该函数主要是将本结构体的字段(成员)按照 consts.ValidatorPrefix+ json标签对应的 键 => 值 形式绑定在上下文,便于下一步(控制器)可以直接通过 context.Get(键) 获取相关值 27 | extraAddBindDataContext := data_transfer.DataAddContext(d, consts.ValidatorPrefix, context) 28 | if extraAddBindDataContext == nil { 29 | response.ErrorSystem(context, "UserShow表单参数验证器json化失败", "") 30 | return 31 | } else { 32 | // 验证完成,调用控制器,并将验证器成员(字段)递给控制器,保持上下文数据一致性 33 | (&web.Users{}).Destroy(extraAddBindDataContext) 34 | 35 | // 以下代码为模拟 前置、后置函数的回调代码 36 | /* 37 | func(before_callback_fn func(context *gin.Context) bool, after_callback_fn func(context *gin.Context)) { 38 | if before_callback_fn(extraAddBindDataContext) { 39 | defer after_callback_fn(extraAddBindDataContext) 40 | (&Web.Users{}).Destroy(extraAddBindDataContext) 41 | } else { 42 | // 这里编写前置函数验证不通过的相关返回提示逻辑... 43 | 44 | } 45 | }((&Users.DestroyBefore{}).Before, (&Users.DestroyAfter{}).After) 46 | */ 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /docs/deploy_redis.md: -------------------------------------------------------------------------------- 1 | ### 运维方案之Redis 2 | > 1.本篇继续介绍 `redis` 监控方案。 3 | 4 | #### 正式开始部署redis运维监控 5 | > 1.`redis` 监控的原理主要是通过 `redis_exporter` 连接 `redis://x.x.x.x:6379` 获取redis底层运行状态数据,prometheus 通过redies-expoter 数据收集器抓取数据,存储在自带的TSDB数据库,最终供 `grafana` 展示。 6 | > 2.特别提醒:在操作之前,检查 `redis.conf` 配置文件,bind 必须绑定在一个内网ip,不要绑定在 `127.0.0.1` 上面,否则通过docker是无法连接redis数据库服务器的。 7 | ```code 8 | #step 1 9 | docker pull oliver006/redis_exporter 10 | 11 | #step2 ,以下配中 172.19.130.185 是我自己的ip, 注意修改为您物理机器真实ip, 12 | #redis.addr 指定redis地址,由于这里使用docker部署的服务,所以不能使用127.0.0.1地址。 13 | #redis.password redis认证密码,如果没有密码,该参数不需要 14 | 15 | docker run -d --name redis_exporter -p 172.19.130.185:9121:9121 -e TZ=Asia/Shanghai oliver006/redis_exporter --redis.addr redis://172.19.130.185:6379 --redis.password 你的redis密码 16 | 17 | #step3 配置 premetheus 18 | - job_name: "阿里云redis服务器" 19 | static_configs: 20 | - targets: ['172.19.130.185:9121'] 21 | labels: 22 | instance: "Redis_GoSkeleton" 23 | 24 | #step4 重启docker启动的 prometheus 服务 25 | docker restart prometheus #prometheus 如果你全程是根据我们的部署文档进行部署的,那么你的premetheus服务就是名就是 prometheus ,否则自己替换成自己的服务名称即可。 26 | 27 | #step5 防火墙端口设置 28 | > A容器通过宿主机映射端口访问B容器,那么宿主机的映射端口就必须在防火墙打开,否则容器无法互通。 29 | > 本次需要在防火墙允许 6379 、9121 端口 30 | # 以设置防火墙允许6379为例,9121 仿照设置即可。 31 | firewall-cmd --zone=public --add-port=6379/tcp --permanent 32 | firewall-cmd --complete-reload 33 | 34 | #step6 在grafana选择自己喜欢的模板,导入 35 | 本次在grafana 界面导入模板id 763 // 相关模板地址: https://grafana.com/dashboards/763 36 | 37 | ``` 38 | 39 | #### 最终效果图 40 | ![redis最终效果](https://www.ginskeleton.com/images/redis.png) 41 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | #说明:makefile 文件只能在linux系统运行,windows 系统无法执行本文件定义的相关命令 2 | # 使用文档参考:https://www.yuque.com/xiaofensinixidaouxiang/bkfhct/zso6xo 3 | 4 | # 定义 makefile 的命名列表, 只需要将外部调用的公布在这里即可 5 | .PHONY: build-api build-web build-cli help 6 | 7 | # 设置 cmd/api/main.go 入口文件编译后的可执行文件名 8 | apiBinName="ginskeleton-api.linux64" 9 | 10 | # 设置 cmd/web/main.go 入口文件编译后的可执行文件名 11 | webBinName="ginskeleton-web.linux64" 12 | 13 | # 设置 cmd/cli/main.go 入口文件编译后的可执行文件名 14 | cliBinName="ginskeleton-cli.linux64" 15 | 16 | # 统一设置编译的目标平台公共参数 17 | all: 18 | go env -w GOARCH=amd64 19 | go env -w GOOS=linux 20 | go env -w CGO_ENABLED=0 21 | go env -w GO111MODULE=on 22 | go env -w GOPROXY=https://goproxy.cn,direct 23 | go mod tidy 24 | 25 | build-api:all clean-api build-api-bin 26 | build-api-bin: 27 | go build -o ${apiBinName} -ldflags "-w -s" -trimpath ./cmd/api/main.go 28 | 29 | build-web:all clean-web build-web-bin 30 | build-web-bin: 31 | go build -o ${webBinName} -ldflags "-w -s" -trimpath ./cmd/web/main.go 32 | 33 | build-cli:all clean-cli build-cli-bin 34 | build-cli-bin: 35 | go build -o ${cliBinName} -ldflags "-w -s" -trimpath ./cmd/cli/main.go 36 | 37 | # 编译前清理可能已经存在的旧文件 38 | clean-api: 39 | @if [ -f ${apiBinName} ] ; then rm -rf ${apiBinName} ; fi 40 | clean-web: 41 | @if [ -f ${webBinName} ] ; then rm -rf ${webBinName} ; fi 42 | clean-cli: 43 | @if [ -f ${cliBinName} ] ; then rm -rf ${cliBinName} ; fi 44 | 45 | help: 46 | @echo "make hep 查看编译命令列表" 47 | @echo "make build-api 编译 cmd/api/main.go 入口文件 " 48 | @echo "make build-web 编译 cmd/web/main.go 入口文件 " 49 | @echo "make build-cli 编译 cmd/cli/main.go 入口文件 " -------------------------------------------------------------------------------- /app/utils/gin_release/gin_release_router.go: -------------------------------------------------------------------------------- 1 | package gin_release 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/gin-gonic/gin" 7 | "go.uber.org/zap" 8 | "goskeleton/app/global/consts" 9 | "goskeleton/app/global/variable" 10 | "goskeleton/app/utils/response" 11 | "io/ioutil" 12 | ) 13 | 14 | // ReleaseRouter 根据 gin 路由包官方的建议,gin 路由引擎如果在生产模式使用,官方建议设置为 release 模式 15 | // 官方原版提示说明:[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production. 16 | // 这里我们将按照官方指导进行生产模式精细化处理 17 | func ReleaseRouter() *gin.Engine { 18 | // 切换到生产模式禁用 gin 输出接口访问日志,经过并发测试验证,可以提升5%的性能 19 | gin.SetMode(gin.ReleaseMode) 20 | gin.DefaultWriter = ioutil.Discard 21 | 22 | engine := gin.New() 23 | // 载入gin的中间件,关键是第二个中间件,我们对它进行了自定义重写,将可能的 panic 异常等,统一使用 zaplog 接管,保证全局日志打印统一 24 | engine.Use(gin.Logger(), CustomRecovery()) 25 | return engine 26 | } 27 | 28 | // CustomRecovery 自定义错误(panic等)拦截中间件、对可能发生的错误进行拦截、统一记录 29 | func CustomRecovery() gin.HandlerFunc { 30 | DefaultErrorWriter := &PanicExceptionRecord{} 31 | return gin.RecoveryWithWriter(DefaultErrorWriter, func(c *gin.Context, err interface{}) { 32 | // 这里针对发生的panic等异常进行统一响应即可 33 | // 这里的 err 数据类型为 :runtime.boundsError ,需要转为普通数据类型才可以输出 34 | response.ErrorSystem(c, "", fmt.Sprintf("%s", err)) 35 | }) 36 | } 37 | 38 | // PanicExceptionRecord panic等异常记录 39 | type PanicExceptionRecord struct{} 40 | 41 | func (p *PanicExceptionRecord) Write(b []byte) (n int, err error) { 42 | errStr := string(b) 43 | err = errors.New(errStr) 44 | variable.ZapLog.Error(consts.ServerOccurredErrorMsg, zap.String("errStrace", errStr)) 45 | return len(errStr), err 46 | } 47 | -------------------------------------------------------------------------------- /app/core/container/container.go: -------------------------------------------------------------------------------- 1 | package container 2 | 3 | import ( 4 | "goskeleton/app/global/my_errors" 5 | "goskeleton/app/global/variable" 6 | "log" 7 | "strings" 8 | "sync" 9 | ) 10 | 11 | // 定义一个全局键值对存储容器 12 | var sMap sync.Map 13 | 14 | // CreateContainersFactory 创建一个容器工厂 15 | func CreateContainersFactory() *containers { 16 | return &containers{} 17 | } 18 | 19 | // 定义一个容器结构体 20 | type containers struct { 21 | } 22 | 23 | // Set 1.以键值对的形式将代码注册到容器 24 | func (c *containers) Set(key string, value interface{}) (res bool) { 25 | 26 | if _, exists := c.KeyIsExists(key); exists == false { 27 | sMap.Store(key, value) 28 | res = true 29 | } else { 30 | // 程序启动阶段,zaplog 未初始化,使用系统log打印启动时候发生的异常日志 31 | if variable.ZapLog == nil { 32 | log.Fatal(my_errors.ErrorsContainerKeyAlreadyExists + ",请解决键名重复问题,相关键:" + key) 33 | } else { 34 | // 程序启动初始化完成 35 | variable.ZapLog.Warn(my_errors.ErrorsContainerKeyAlreadyExists + ", 相关键:" + key) 36 | } 37 | } 38 | return 39 | } 40 | 41 | // Delete 2.删除 42 | func (c *containers) Delete(key string) { 43 | sMap.Delete(key) 44 | } 45 | 46 | // Get 3.传递键,从容器获取值 47 | func (c *containers) Get(key string) interface{} { 48 | if value, exists := c.KeyIsExists(key); exists { 49 | return value 50 | } 51 | return nil 52 | } 53 | 54 | // KeyIsExists 4. 判断键是否被注册 55 | func (c *containers) KeyIsExists(key string) (interface{}, bool) { 56 | return sMap.Load(key) 57 | } 58 | 59 | // FuzzyDelete 按照键的前缀模糊删除容器中注册的内容 60 | func (c *containers) FuzzyDelete(keyPre string) { 61 | sMap.Range(func(key, value interface{}) bool { 62 | if keyname, ok := key.(string); ok { 63 | if strings.HasPrefix(keyname, keyPre) { 64 | sMap.Delete(keyname) 65 | } 66 | } 67 | return true 68 | }) 69 | } 70 | -------------------------------------------------------------------------------- /app/utils/rabbitmq/topics/options.go: -------------------------------------------------------------------------------- 1 | package topics 2 | 3 | import ( 4 | amqp "github.com/rabbitmq/amqp091-go" 5 | "goskeleton/app/global/variable" 6 | ) 7 | 8 | // 等 go 泛型稳定以后,生产者和消费者初始化参数的设置,本段代码就可以继续精简 9 | // 目前 apply(*producer) 的参数只能固定为生产者或者消费者其中之一的具体类型 10 | 11 | // 1.生产者初始化参数定义 12 | 13 | // OptionsProd 定义动态设置参数接口 14 | type OptionsProd interface { 15 | apply(*producer) 16 | } 17 | 18 | // OptionFunc 以函数形式实现上面的接口 19 | type OptionFunc func(*producer) 20 | 21 | func (f OptionFunc) apply(prod *producer) { 22 | f(prod) 23 | } 24 | 25 | // SetProdMsgDelayParams 开发者设置生产者初始化时的参数 26 | func SetProdMsgDelayParams(enableMsgDelayPlugin bool) OptionsProd { 27 | return OptionFunc(func(p *producer) { 28 | p.enableDelayMsgPlugin = enableMsgDelayPlugin 29 | p.exchangeType = "x-delayed-message" 30 | p.args = amqp.Table{ 31 | "x-delayed-type": "topic", 32 | } 33 | p.exchangeName = variable.ConfigYml.GetString("RabbitMq.Topics.DelayedExchangeName") 34 | // 延迟消息队列,交换机、消息全部设置为持久 35 | p.durable = true 36 | }) 37 | } 38 | 39 | // 2.消费者端初始化参数定义 40 | 41 | // OptionsConsumer 定义动态设置参数接口 42 | type OptionsConsumer interface { 43 | apply(*consumer) 44 | } 45 | 46 | // OptionsConsumerFunc 以函数形式实现上面的接口 47 | type OptionsConsumerFunc func(*consumer) 48 | 49 | func (f OptionsConsumerFunc) apply(cons *consumer) { 50 | f(cons) 51 | } 52 | 53 | // SetConsMsgDelayParams 开发者设置消费者端初始化时的参数 54 | func SetConsMsgDelayParams(enableDelayMsgPlugin bool) OptionsConsumer { 55 | return OptionsConsumerFunc(func(c *consumer) { 56 | c.enableDelayMsgPlugin = enableDelayMsgPlugin 57 | c.exchangeType = "x-delayed-message" 58 | c.exchangeName = variable.ConfigYml.GetString("RabbitMq.Topics.DelayedExchangeName") 59 | // 延迟消息队列,交换机、消息全部设置为持久 60 | c.durable = true 61 | }) 62 | } 63 | -------------------------------------------------------------------------------- /app/utils/rabbitmq/routing/options.go: -------------------------------------------------------------------------------- 1 | package routing 2 | 3 | import ( 4 | amqp "github.com/rabbitmq/amqp091-go" 5 | "goskeleton/app/global/variable" 6 | ) 7 | 8 | // 等 go 泛型稳定以后,生产者和消费者初始化参数的设置,本段代码就可以继续精简 9 | // 目前 apply(*producer) 的参数只能固定为生产者或者消费者其中之一的具体类型 10 | 11 | // 1.生产者初始化参数定义 12 | 13 | // OptionsProd 定义动态设置参数接口 14 | type OptionsProd interface { 15 | apply(*producer) 16 | } 17 | 18 | // OptionFunc 以函数形式实现上面的接口 19 | type OptionFunc func(*producer) 20 | 21 | func (f OptionFunc) apply(prod *producer) { 22 | f(prod) 23 | } 24 | 25 | // SetProdMsgDelayParams 开发者设置生产者初始化时的参数 26 | func SetProdMsgDelayParams(enableMsgDelayPlugin bool) OptionsProd { 27 | return OptionFunc(func(p *producer) { 28 | p.enableDelayMsgPlugin = enableMsgDelayPlugin 29 | p.exchangeType = "x-delayed-message" 30 | p.args = amqp.Table{ 31 | "x-delayed-type": "direct", 32 | } 33 | p.exchangeName = variable.ConfigYml.GetString("RabbitMq.Routing.DelayedExchangeName") 34 | // 延迟消息队列,交换机、消息全部设置为持久 35 | p.durable = true 36 | }) 37 | } 38 | 39 | // 2.消费者端初始化参数定义 40 | 41 | // OptionsConsumer 定义动态设置参数接口 42 | type OptionsConsumer interface { 43 | apply(*consumer) 44 | } 45 | 46 | // OptionsConsumerFunc 以函数形式实现上面的接口 47 | type OptionsConsumerFunc func(*consumer) 48 | 49 | func (f OptionsConsumerFunc) apply(cons *consumer) { 50 | f(cons) 51 | } 52 | 53 | // SetConsMsgDelayParams 开发者设置消费者端初始化时的参数 54 | func SetConsMsgDelayParams(enableDelayMsgPlugin bool) OptionsConsumer { 55 | return OptionsConsumerFunc(func(c *consumer) { 56 | c.enableDelayMsgPlugin = enableDelayMsgPlugin 57 | c.exchangeType = "x-delayed-message" 58 | c.exchangeName = variable.ConfigYml.GetString("RabbitMq.Routing.DelayedExchangeName") 59 | // 延迟消息队列,交换机、消息全部设置为持久 60 | c.durable = true 61 | }) 62 | } 63 | -------------------------------------------------------------------------------- /app/utils/rabbitmq/publish_subscribe/options.go: -------------------------------------------------------------------------------- 1 | package publish_subscribe 2 | 3 | import ( 4 | amqp "github.com/rabbitmq/amqp091-go" 5 | "goskeleton/app/global/variable" 6 | ) 7 | 8 | // 等 go 泛型稳定以后,生产者和消费者初始化参数的设置,本段代码就可以继续精简 9 | // 目前 apply(*producer) 的参数只能固定为生产者或者消费者其中之一的具体类型 10 | 11 | // 1.生产者初始化参数定义 12 | 13 | // OptionsProd 定义动态设置参数接口 14 | type OptionsProd interface { 15 | apply(*producer) 16 | } 17 | 18 | // OptionFunc 以函数形式实现上面的接口 19 | type OptionFunc func(*producer) 20 | 21 | func (f OptionFunc) apply(prod *producer) { 22 | f(prod) 23 | } 24 | 25 | // SetProdMsgDelayParams 开发者设置生产者初始化时的参数 26 | func SetProdMsgDelayParams(enableMsgDelayPlugin bool) OptionsProd { 27 | return OptionFunc(func(p *producer) { 28 | p.enableDelayMsgPlugin = enableMsgDelayPlugin 29 | p.exchangeType = "x-delayed-message" 30 | p.args = amqp.Table{ 31 | "x-delayed-type": "fanout", 32 | } 33 | p.exchangeName = variable.ConfigYml.GetString("RabbitMq.PublishSubscribe.DelayedExchangeName") 34 | // 延迟消息队列,交换机、消息全部设置为持久 35 | p.durable = true 36 | }) 37 | } 38 | 39 | // 2.消费者端初始化参数定义 40 | 41 | // OptionsConsumer 定义动态设置参数接口 42 | type OptionsConsumer interface { 43 | apply(*consumer) 44 | } 45 | 46 | // OptionsConsumerFunc 以函数形式实现上面的接口 47 | type OptionsConsumerFunc func(*consumer) 48 | 49 | func (f OptionsConsumerFunc) apply(cons *consumer) { 50 | f(cons) 51 | } 52 | 53 | // SetConsMsgDelayParams 开发者设置消费者端初始化时的参数 54 | func SetConsMsgDelayParams(enableDelayMsgPlugin bool) OptionsConsumer { 55 | return OptionsConsumerFunc(func(c *consumer) { 56 | c.enableDelayMsgPlugin = enableDelayMsgPlugin 57 | c.exchangeType = "x-delayed-message" 58 | c.exchangeName = variable.ConfigYml.GetString("RabbitMq.PublishSubscribe.DelayedExchangeName") 59 | // 延迟消息队列,交换机、消息全部设置为持久 60 | c.durable = true 61 | }) 62 | } 63 | -------------------------------------------------------------------------------- /app/http/validator/common/upload_files/upload_fiels.go: -------------------------------------------------------------------------------- 1 | package upload_files 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "goskeleton/app/global/consts" 6 | "goskeleton/app/global/variable" 7 | "goskeleton/app/http/controller/web" 8 | "goskeleton/app/utils/files" 9 | "goskeleton/app/utils/response" 10 | "strconv" 11 | "strings" 12 | ) 13 | 14 | type UpFiles struct { 15 | } 16 | 17 | // 文件上传公共模块表单参数验证器 18 | func (u UpFiles) CheckParams(context *gin.Context) { 19 | tmpFile, err := context.FormFile(variable.ConfigYml.GetString("FileUploadSetting.UploadFileField")) // file 是一个文件结构体(文件对象) 20 | var isPass bool 21 | //获取文件发生错误,可能上传了空文件等 22 | if err != nil { 23 | response.Fail(context, consts.FilesUploadFailCode, consts.FilesUploadFailMsg, err.Error()) 24 | return 25 | } 26 | //超过系统设定的最大值:32M,tmpFile.Size 的单位是 bytes 和我们定义的文件单位M 比较,就需要将我们的单位*1024*1024(即2的20次方),一步到位就是 << 20 27 | sizeLimit := variable.ConfigYml.GetInt64("FileUploadSetting.Size") 28 | if tmpFile.Size > sizeLimit<<20 { 29 | response.Fail(context, consts.FilesUploadMoreThanMaxSizeCode, consts.FilesUploadMoreThanMaxSizeMsg+strconv.FormatInt(sizeLimit, 10)+"M", "") 30 | return 31 | } 32 | //不允许的文件mime类型 33 | if fp, err := tmpFile.Open(); err == nil { 34 | mimeType := files.GetFilesMimeByFp(fp) 35 | 36 | for _, value := range variable.ConfigYml.GetStringSlice("FileUploadSetting.AllowMimeType") { 37 | if strings.ReplaceAll(value, " ", "") == strings.ReplaceAll(mimeType, " ", "") { 38 | isPass = true 39 | break 40 | } 41 | } 42 | _ = fp.Close() 43 | } else { 44 | response.ErrorSystem(context, consts.ServerOccurredErrorMsg, "") 45 | return 46 | } 47 | //凡是存在相等的类型,通过验证,调用控制器 48 | if !isPass { 49 | response.Fail(context, consts.FilesUploadMimeTypeFailCode, consts.FilesUploadMimeTypeFailMsg, "") 50 | } else { 51 | (&web.Upload{}).StartUpload(context) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/core/event_manage/event_manage.go: -------------------------------------------------------------------------------- 1 | package event_manage 2 | 3 | import ( 4 | "goskeleton/app/global/my_errors" 5 | "goskeleton/app/global/variable" 6 | "strings" 7 | "sync" 8 | ) 9 | 10 | // 定义一个全局事件存储变量,本模块只负责存储 键 => 函数 , 相对容器来说功能稍弱,但是调用更加简单、方便、快捷 11 | var sMap sync.Map 12 | 13 | // 创建一个事件管理工厂 14 | func CreateEventManageFactory() *eventManage { 15 | 16 | return &eventManage{} 17 | } 18 | 19 | // 定义一个事件管理结构体 20 | type eventManage struct { 21 | } 22 | 23 | // 1.注册事件 24 | func (e *eventManage) Set(key string, keyFunc func(args ...interface{})) bool { 25 | //判断key下是否已有事件 26 | if _, exists := e.Get(key); exists == false { 27 | sMap.Store(key, keyFunc) 28 | return true 29 | } else { 30 | variable.ZapLog.Info(my_errors.ErrorsFuncEventAlreadyExists + " , 相关键名:" + key) 31 | } 32 | return false 33 | } 34 | 35 | // 2.获取事件 36 | func (e *eventManage) Get(key string) (interface{}, bool) { 37 | if value, exists := sMap.Load(key); exists { 38 | return value, exists 39 | } 40 | return nil, false 41 | } 42 | 43 | // 3.执行事件 44 | func (e *eventManage) Call(key string, args ...interface{}) { 45 | if valueInterface, exists := e.Get(key); exists { 46 | if fn, ok := valueInterface.(func(args ...interface{})); ok { 47 | fn(args...) 48 | } else { 49 | variable.ZapLog.Error(my_errors.ErrorsFuncEventNotCall + ", 键名:" + key + ", 相关函数无法调用") 50 | } 51 | 52 | } else { 53 | variable.ZapLog.Error(my_errors.ErrorsFuncEventNotRegister + ", 键名:" + key) 54 | } 55 | } 56 | 57 | // 4.删除事件 58 | func (e *eventManage) Delete(key string) { 59 | sMap.Delete(key) 60 | } 61 | 62 | // 5.根据键的前缀,模糊调用. 使用请谨慎. 63 | func (e *eventManage) FuzzyCall(keyPre string) { 64 | 65 | sMap.Range(func(key, value interface{}) bool { 66 | if keyName, ok := key.(string); ok { 67 | if strings.HasPrefix(keyName, keyPre) { 68 | e.Call(keyName) 69 | } 70 | } 71 | return true 72 | }) 73 | } 74 | -------------------------------------------------------------------------------- /app/global/variable/variable.go: -------------------------------------------------------------------------------- 1 | package variable 2 | 3 | import ( 4 | "github.com/casbin/casbin/v2" 5 | "go.uber.org/zap" 6 | "gorm.io/gorm" 7 | "goskeleton/app/global/my_errors" 8 | "goskeleton/app/utils/snow_flake/snowflake_interf" 9 | "goskeleton/app/utils/yml_config/ymlconfig_interf" 10 | "log" 11 | "os" 12 | "strings" 13 | ) 14 | 15 | // ginskeleton 封装的全局变量全部支持并发安全,请放心使用即可 16 | // 开发者自行封装的全局变量,请做好并发安全检查与确认 17 | 18 | var ( 19 | BasePath string // 定义项目的根目录 20 | EventDestroyPrefix = "Destroy_" // 程序退出时需要销毁的事件前缀 21 | ConfigKeyPrefix = "Config_" // 配置文件键值缓存时,键的前缀 22 | DateFormat = "2006-01-02 15:04:05" // 设置全局日期时间格式 23 | 24 | // 全局日志指针 25 | ZapLog *zap.Logger 26 | // 全局配置文件 27 | ConfigYml ymlconfig_interf.YmlConfigInterf // 全局配置文件指针 28 | ConfigGormv2Yml ymlconfig_interf.YmlConfigInterf // 全局配置文件指针 29 | 30 | //gorm 数据库客户端,如果您操作数据库使用的是gorm,请取消以下注释,在 bootstrap>init 文件,进行初始化即可使用 31 | GormDbMysql *gorm.DB // 全局gorm的客户端连接 32 | GormDbSqlserver *gorm.DB // 全局gorm的客户端连接 33 | GormDbPostgreSql *gorm.DB // 全局gorm的客户端连接 34 | 35 | //雪花算法全局变量 36 | SnowFlake snowflake_interf.InterfaceSnowFlake 37 | 38 | //websocket 39 | WebsocketHub interface{} 40 | WebsocketHandshakeSuccess = `{"code":200,"msg":"ws连接成功","data":""}` 41 | WebsocketServerPingMsg = "Server->Ping->Client" 42 | 43 | //casbin 全局操作指针 44 | Enforcer *casbin.SyncedEnforcer 45 | 46 | // 用户自行定义其他全局变量 ↓ 47 | 48 | ) 49 | 50 | func init() { 51 | // 1.初始化程序根目录 52 | if curPath, err := os.Getwd(); err == nil { 53 | // 路径进行处理,兼容单元测试程序程序启动时的奇怪路径 54 | if len(os.Args) > 1 && strings.HasPrefix(os.Args[1], "-test") { 55 | BasePath = strings.Replace(strings.Replace(curPath, `\test`, "", 1), `/test`, "", 1) 56 | } else { 57 | BasePath = curPath 58 | } 59 | } else { 60 | log.Fatal(my_errors.ErrorsBasePath) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /docs/supervisor.md: -------------------------------------------------------------------------------- 1 | ### Supervisor 部署 2 | 3 | `Supervisor` 是 `Linux/Unix` 系统下的一个进程管理工具,可靠稳定,很多著名框架的进程守护都推荐使用该软件。 4 | 5 | #### 安装 Supervisor 6 | > 这里仅举例 `CentOS` 系统下的安装方式: 7 | 8 | ```bash 9 | # 安装 epel 源,如果此前安装过,此步骤跳过 10 | yum install -y epel-release 11 | yum install -y supervisor // 【ubutu】apt-get install supervisor 12 | ``` 13 | 14 | #### 创建一个配置文件 15 | ```bash 16 | cp /etc/supervisord.conf /etc/supervisord.d/supervisord.conf 17 | 18 | #编辑刚才新复制的配置文件 19 | vim /etc/supervisord.d/supervisord.conf 20 | 21 | # 在[include]节点前添加以下内容,保存 22 | 23 | [program:GoSkeleton] 24 | # 设置命令在指定的目录内执行 25 | directory=/home/wwwroot/GoProject2020/goskeleton/ 26 | #例如,我们编译完以后的go程序名为:main 27 | command= /bin/bash -c ./main 28 | user=root 29 | # supervisor 启动时自动该应用 30 | autostart=true 31 | # 进程退出后自动重启进程 32 | autorestart=true 33 | # 进程持续运行多久才认为是启动成功 34 | startsecs = 5 35 | # 启动重试次数 36 | startretries = 3 37 | #指定日志目录(将原来在调试输出界面的内容统一写到指定文件) 38 | stdout_logfile=/home/wwwroot/GoProject2020/Storage/logs/out.log 39 | stderr_logfile=/home/wwwroot/GoProject2020/Storage/logs/err.log 40 | 41 | ``` 42 | 43 | 44 | 45 | #### 配置 `Supervisor` 可视化管理界面 46 | > 1.编辑配置文件 /etc/supervisord.d/supervisord.conf ,将以下注释打开即可。 47 | ```ini 48 | [inet_http_server] 49 | port=0.0.0.0:9001 50 | #设置可视化管理账号 51 | username=user_name 52 | #设置可视化管理密码 53 | password=user_pass 54 | ``` 55 | 56 | 57 | #### 启动 Supervisor 58 | ```jsunicoderegexp 59 | supervisord -c /etc/supervisord.d/supervisord.conf 60 | ``` 61 | 62 | #### 使用 supervisorctl 命令管理项目 63 | > 此时你也可以通过浏览器打开 `ip:9001` 地址,输入账号、密码对应用程序进行可视化管理。 64 | ```bash 65 | # 启动 Goskeleton 应用 66 | supervisorctl start Goskeleton 67 | # 重启 GoSkeleton 应用 68 | supervisorctl restart Goskeleton 69 | # 停止 GoSkeleton 应用 70 | supervisorctl stop Goskeleton 71 | # 查看所有被管理项目运行状态 72 | supervisorctl status 73 | # 重新加载配置文件,一般是增加了新的项目节点,执行此命令即可使新项目运行起来而不影响老项目 74 | supervisorctl update 75 | # 重新启动所有程序 76 | supervisorctl reload 77 | ``` 78 | -------------------------------------------------------------------------------- /database/db_demo_sqlserver.sql: -------------------------------------------------------------------------------- 1 | -- 创建数据库,例如: db_goskeleton 2 | USE [master] 3 | IF NOT EXISTS(SELECT 1 FROM sysdatabases WHERE NAME=N'db_goskeleton') 4 | BEGIN 5 | CREATE DATABASE db_goskeleton 6 | END 7 | GO 8 | use db_goskeleton ; 9 | -- 创建用户表 10 | CREATE TABLE [dbo].[tb_users]( 11 | [id] [int] IDENTITY(1,1) NOT NULL, 12 | [user_name] [nvarchar](50) NOT NULL , 13 | [pass] [varchar](128) NOT NULL , 14 | [real_name] [nvarchar](30) DEFAULT (''), 15 | [phone] [char](11) DEFAULT (''), 16 | [status] [tinyint] DEFAULT (1), 17 | [remark] [nvarchar](120) DEFAULT (''), 18 | [last_login_time] [datetime] DEFAULT (getdate()), 19 | [last_login_ip] [varchar](128) DEFAULT (''), 20 | [login_times] [int] DEFAULT ((0)), 21 | [created_at] [datetime] DEFAULT (getdate()), 22 | [updated_at] [datetime] DEFAULT (getdate()) 23 | ); 24 | -- -- 创建token表 25 | 26 | CREATE TABLE [dbo].[tb_oauth_access_tokens]( 27 | [id] [int] IDENTITY(1,1) NOT NULL, 28 | [fr_user_id] [int] DEFAULT ((0)), 29 | [client_id] [int] DEFAULT ((0)), 30 | [token] [varchar](500) DEFAULT (''), 31 | [action_name] [varchar](50) DEFAULT ('login') , 32 | [scopes] [varchar](128) DEFAULT ('*') , 33 | [revoked] [tinyint] DEFAULT ((0)), 34 | [client_ip] [varchar](128) DEFAULT (''), 35 | [created_at] [datetime] DEFAULT (getdate()) , 36 | [updated_at] [datetime] DEFAULT (getdate()) , 37 | [expires_at] [datetime] DEFAULT (getdate()) , 38 | [remark] [nchar](120) DEFAULT ('') 39 | ) ; 40 | 41 | -- -- 创建 tb_casbin 接口鉴权表 42 | CREATE TABLE [dbo].[tb_auth_casbin_rule]( 43 | [id] [int] IDENTITY(1,1) NOT NULL, 44 | [ptype] [varchar](100) DEFAULT ('p'), 45 | [v0] [varchar](100) DEFAULT (''), 46 | [v1] [varchar](100) DEFAULT (''), 47 | [v2] [varchar](100) DEFAULT (''), 48 | [v3] [varchar](100) DEFAULT (''), 49 | [v4] [varchar](100) DEFAULT (''), 50 | [v5] [varchar](100) DEFAULT (''), 51 | [remark] [nchar](120) DEFAULT ('') 52 | ) ; -------------------------------------------------------------------------------- /docs/ws_js_client.md: -------------------------------------------------------------------------------- 1 | ## websocket js 客户端 2 | 3 | ### 前言 4 | > ws地址: ws://127.0.0.1:20201/admin/ws?token=sdsdsdsdsdsdsdsdsdsdsdsdssdsd 5 | > 由于中间模拟校验了token参数,请自行随意提交超过20个字符 6 | > 以下代码保存为 `ws.html` 在浏览器直接访问即可连接服务端 7 | > ws服务默认未开启,请自行在配置文件 config/config.yml ,找到 websocket 选项,开启即可. 8 | ```html 9 | 10 | 11 | 12 | 13 | 14 | websocket client 15 |   16 | 17 | 18 | 19 | 20 |
21 | 22 |

websocket client 测试代码

23 | 24 |
25 | 28 | 29 |
30 | 31 |
32 | 35 | 36 | 37 | 38 | 39 | 40 |
41 | 42 | 76 | 77 | 78 | 79 | ``` -------------------------------------------------------------------------------- /app/http/validator/web/users/register.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "goskeleton/app/global/consts" 6 | "goskeleton/app/http/controller/web" 7 | "goskeleton/app/http/validator/core/data_transfer" 8 | "goskeleton/app/utils/response" 9 | ) 10 | 11 | // 验证器是本项目骨架的先锋队,必须发挥它的极致优势,具体参考地址: 12 | //https://godoc.org/github.com/go-playground/validator ,该验证器非常强大,强烈建议重点发挥, 13 | //请求正式进入控制器等后面的业务逻辑层之前,参数的校验必须在验证器层完成,后面的控制器等就只管获取各种参数,代码一把梭 14 | 15 | // 给出一些最常用的验证规则: 16 | //required 必填; 17 | //len=11 长度=11; 18 | //min=3 如果是数字,验证的是数据范围,最小值为3,如果是文本,验证的是最小长度为3, 19 | //max=6 如果是数字,验证的是数字最大值为6,如果是文本,验证的是最大长度为6 20 | // mail 验证邮箱 21 | //gt=3 对于文本就是长度>=3 22 | //lt=6 对于文本就是长度<=6 23 | 24 | type Register struct { 25 | BaseField 26 | // 表单参数验证结构体支持匿名结构体嵌套、以及匿名结构体与普通字段组合 27 | Phone string `form:"phone" json:"phone"` // 手机号, 非必填 28 | CardNo string `form:"card_no" json:"card_no"` //身份证号码,非必填 29 | } 30 | 31 | // 特别注意: 表单参数验证器结构体的函数,绝对不能绑定在指针上 32 | // 我们这部分代码项目启动后会加载到容器,如果绑定在指针,一次请求之后,会造成容器中的代码段被污染 33 | 34 | func (r Register) CheckParams(context *gin.Context) { 35 | //1.先按照验证器提供的基本语法,基本可以校验90%以上的不合格参数 36 | if err := context.ShouldBind(&r); err != nil { 37 | response.ValidatorError(context, err) 38 | return 39 | } 40 | //2.继续验证具有中国特色的参数,例如 身份证号码等,基本语法校验了长度18位,然后可以自行编写正则表达式等更进一步验证每一部分组成 41 | // r.CardNo 获取身份证号码继续校验,可能需要开发者编写正则表达式,稍微复杂,这里忽略 42 | 43 | // r.Phone 获取手机号码,可以根据手机号码开头等等自定义验证,例如 如果不是以138 开头的手机号码,则报错 44 | //if !strings.HasPrefix(r.CardNo, "138") { 45 | // response.ErrorParam(context, gin.H{"tips": "手机号码字段:card_no 必须以138开头"}) 46 | // return 47 | //} 48 | 49 | // 该函数主要是将本结构体的字段(成员)按照 consts.ValidatorPrefix+ json标签对应的 键 => 值 形式绑定在上下文,便于下一步(控制器)可以直接通过 context.Get(键) 获取相关值 50 | extraAddBindDataContext := data_transfer.DataAddContext(r, consts.ValidatorPrefix, context) 51 | if extraAddBindDataContext == nil { 52 | response.ErrorSystem(context, "UserRegister表单验证器json化失败", "") 53 | } else { 54 | // 验证完成,调用控制器,并将验证器成员(字段)递给控制器,保持上下文数据一致性 55 | (&web.Users{}).Register(extraAddBindDataContext) 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /docs/validator.md: -------------------------------------------------------------------------------- 1 | ### validator 表单参数验证器语法介绍 2 | > 1.本篇将选取表单参数验证器( `https://github.com/go-playground/validator` )主要语法进行介绍,方便本项目骨架使用者快速上手. 3 | > 2.更详细的语法参与参见官方文档:`https://godoc.org/github.com/go-playground/validator` 4 | 5 | #### 1.我们以用户注册代码块为例进行介绍. 6 | > 1.[用户注册代码详情](../app/http/validator/web/users/register.go), 摘取表单参数验证部分. 7 | > 2.以下语法虽然看似简单,实际上已经覆盖了绝大部分常用场景的需求. 8 | ```code 9 | // 给出一些最常用的验证规则: 10 | //required 必填; 11 | //len=11 长度=11; 12 | //min=3 如果是数字,验证的是数据大小范围,最小值为3,如果是文本,验证的是最小长度为3, 13 | //max=6 如果是数字,验证的是数字最大值为6,如果是文本,验证的是最大长度为6 14 | //mail 验证邮箱 15 | //gt=3 对于文本就是长度>=3 16 | //lt=6 对于文本就是长度<=6 17 | 18 | 19 | type Register struct { 20 | // 必填、文本类型,表示它的长度>=1 21 | UserName string `form:"user_name" json:"user_name" binding:"required,min=1"` 22 | 23 | //必填,密码长度范围:【6,20】闭区间 24 | Pass string `form:"pass" json:"pass" binding:"required,min=6,max=20"` 25 | 26 | // 验证码,必填,长度等于:4 27 | //Captcha string `form:"captcha" json:"captcha" binding:"required,len=4"` 28 | 29 | // 年龄,必填,数字类型,大小范围【1,200】闭区间 30 | //Age float64 `form:"age" json:"age" binding:"required,min=1,max=200"` 31 | 32 | // 状态:必填,数字类型,大小范围:【0,1】 闭区间 , 33 | // 注意: 如果你的表单参数含有0值是允许提交的,必须用指针类型(*float64),而 float64 类型则认为 0 值不合格 34 | Status *float64 `form:"status" json:"status" binding:"required,min=0,max=1"` 35 | } 36 | 37 | // 注意:这里的接收器 r,必须是 r Register, 绝对不能是 r *Register 38 | // 因为在 ginskeleton 里面表单参数验证器是注册在容器的代码段, 39 | // 如果是指针,带参数的接口请求,就会把容器的原始代码污染。 40 | func (r Register) CheckParams(context *gin.Context) { 41 | // context.ShouldBind(&r) 则自动绑定 form-data 提交的表单参数 42 | if err := context.ShouldBind(&r); err != nil { 43 | 44 | // 省略非验证器逻辑代码.... 45 | // ... ... 46 | 47 | } 48 | 49 | // 如果您的客户端的数据是以json格式提交(popstman中的raw格式),那么就用如下语法 50 | // context.ShouldBindJson(&r) 则自动绑定 json格式提交的参数 51 | 52 | } 53 | 54 | ``` 55 | 56 | #### 2.以上语法特别说明. 57 | > 1.对于数字类型(int8、int、int64、float32、float64等)我们统一使用 float64、*float64 接受. 58 | > 2.如果您的业务要求数字格式为 int类型,那么使用 int() 等数据类型转换函数自行转换即可. 59 | -------------------------------------------------------------------------------- /app/service/upload_file/upload_file.go: -------------------------------------------------------------------------------- 1 | package upload_file 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/gin-gonic/gin" 7 | "goskeleton/app/global/my_errors" 8 | "goskeleton/app/global/variable" 9 | "goskeleton/app/utils/md5_encrypt" 10 | "os" 11 | "path" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | func Upload(context *gin.Context, savePath string) (r bool, finnalSavePath interface{}) { 17 | 18 | newSavePath, newReturnPath := generateYearMonthPath(savePath) 19 | 20 | // 1.获取上传的文件名(参数验证器已经验证完成了第一步错误,这里简化) 21 | file, _ := context.FormFile(variable.ConfigYml.GetString("FileUploadSetting.UploadFileField")) // file 是一个文件结构体(文件对象) 22 | 23 | // 保存文件,原始文件名进行全局唯一编码加密、md5 加密,保证在后台存储不重复 24 | var saveErr error 25 | if sequence := variable.SnowFlake.GetId(); sequence > 0 { 26 | saveFileName := fmt.Sprintf("%d%s", sequence, file.Filename) 27 | saveFileName = md5_encrypt.MD5(saveFileName) + path.Ext(saveFileName) 28 | 29 | if saveErr = context.SaveUploadedFile(file, newSavePath+saveFileName); saveErr == nil { 30 | // 上传成功,返回资源的相对路径,这里请根据实际返回绝对路径或者相对路径 31 | finnalSavePath = gin.H{ 32 | "path": strings.ReplaceAll(newReturnPath+saveFileName, variable.BasePath, ""), 33 | } 34 | return true, finnalSavePath 35 | } 36 | } else { 37 | saveErr = errors.New(my_errors.ErrorsSnowflakeGetIdFail) 38 | variable.ZapLog.Error("文件保存出错:" + saveErr.Error()) 39 | } 40 | return false, nil 41 | 42 | } 43 | 44 | // 文件上传可以设置按照 xxx年-xx月 格式存储 45 | func generateYearMonthPath(savePathPre string) (string, string) { 46 | returnPath := variable.BasePath + variable.ConfigYml.GetString("FileUploadSetting.UploadFileReturnPath") 47 | curYearMonth := time.Now().Format("2006_01") 48 | newSavePathPre := savePathPre + curYearMonth 49 | newReturnPathPre := returnPath + curYearMonth 50 | // 相关路径不存在,创建目录 51 | if _, err := os.Stat(newSavePathPre); err != nil { 52 | if err = os.MkdirAll(newSavePathPre, os.ModePerm); err != nil { 53 | variable.ZapLog.Error("文件上传创建目录出错" + err.Error()) 54 | return "", "" 55 | } 56 | } 57 | return newSavePathPre + "/", newReturnPathPre + "/" 58 | } 59 | -------------------------------------------------------------------------------- /app/utils/validator_translation/validator_transiation.go: -------------------------------------------------------------------------------- 1 | package validator_translation 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gin-gonic/gin/binding" 6 | "github.com/go-playground/locales/en" 7 | "github.com/go-playground/locales/zh" 8 | ut "github.com/go-playground/universal-translator" 9 | "github.com/go-playground/validator/v10" 10 | enTranslations "github.com/go-playground/validator/v10/translations/en" 11 | zhTranslations "github.com/go-playground/validator/v10/translations/zh" 12 | "reflect" 13 | "strings" 14 | ) 15 | 16 | //Trans 定义一个全局翻译器T 17 | var Trans ut.Translator 18 | 19 | //InitTrans 初始化表单参数验证器的翻译器 20 | func InitTrans(locale string) (err error) { 21 | // 修改gin框架中的Validator引擎属性,实现自定制 22 | if v, ok := binding.Validator.Engine().(*validator.Validate); ok { 23 | // 注册一个获取json tag的自定义方法 24 | v.RegisterTagNameFunc(func(fld reflect.StructField) string { 25 | name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0] 26 | if name == "-" { 27 | return "" 28 | } 29 | return name 30 | }) 31 | //初始化翻译器 32 | zhT := zh.New() 33 | enT := en.New() 34 | // 第一个参数是备用(fallback)的语言环境 35 | // 后面的参数是应该支持的语言环境(支持多个) 36 | // uni := ut.New(zhT, zhT) 也是可以的 37 | uni := ut.New(enT, zhT, enT) 38 | // locale 通常取决于 http 请求头的 'Accept-Language' 39 | // 也可以使用 uni.FindTranslator(...) 传入多个locale进行查找 40 | Trans, ok = uni.GetTranslator(locale) 41 | if !ok { 42 | return fmt.Errorf("uni.GetTranslator(%s) failed", locale) 43 | } 44 | //注册翻译器 45 | //默认注册英文,en 注册英文 zh 注册中文 46 | switch locale { 47 | case "en": 48 | err = enTranslations.RegisterDefaultTranslations(v, Trans) 49 | case "zh": 50 | err = zhTranslations.RegisterDefaultTranslations(v, Trans) 51 | default: 52 | err = enTranslations.RegisterDefaultTranslations(v, Trans) 53 | } 54 | return 55 | } 56 | return 57 | } 58 | 59 | //RemoveTopStruct 将返回的结构体名去除掉,只留下需要的字段名 60 | func RemoveTopStruct(fields map[string]string) map[string]string { 61 | res := map[string]string{} 62 | for field, err := range fields { 63 | res[field[strings.LastIndex(field, ".")+1:]] = err 64 | } 65 | return res 66 | } 67 | -------------------------------------------------------------------------------- /app/utils/casbin_v2/casbin_v2.go: -------------------------------------------------------------------------------- 1 | package casbin_v2 2 | 3 | import ( 4 | "errors" 5 | "github.com/casbin/casbin/v2" 6 | "github.com/casbin/casbin/v2/model" 7 | gormadapter "github.com/casbin/gorm-adapter/v3" 8 | "gorm.io/gorm" 9 | "goskeleton/app/global/my_errors" 10 | "goskeleton/app/global/variable" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | //创建 casbin Enforcer(执行器) 16 | func InitCasbinEnforcer() (*casbin.SyncedEnforcer, error) { 17 | var tmpDbConn *gorm.DB 18 | var Enforcer *casbin.SyncedEnforcer 19 | switch strings.ToLower(variable.ConfigGormv2Yml.GetString("Gormv2.UseDbType")) { 20 | case "mysql": 21 | if variable.GormDbMysql == nil { 22 | return nil, errors.New(my_errors.ErrorCasbinCanNotUseDbPtr) 23 | } 24 | tmpDbConn = variable.GormDbMysql 25 | case "sqlserver", "mssql": 26 | if variable.GormDbSqlserver == nil { 27 | return nil, errors.New(my_errors.ErrorCasbinCanNotUseDbPtr) 28 | } 29 | tmpDbConn = variable.GormDbSqlserver 30 | case "postgre", "postgresql", "postgres": 31 | if variable.GormDbPostgreSql == nil { 32 | return nil, errors.New(my_errors.ErrorCasbinCanNotUseDbPtr) 33 | } 34 | tmpDbConn = variable.GormDbPostgreSql 35 | default: 36 | } 37 | 38 | prefix := variable.ConfigYml.GetString("Casbin.TablePrefix") 39 | tbName := variable.ConfigYml.GetString("Casbin.TableName") 40 | 41 | a, err := gormadapter.NewAdapterByDBUseTableName(tmpDbConn, prefix, tbName) 42 | if err != nil { 43 | return nil, errors.New(my_errors.ErrorCasbinCreateAdaptFail) 44 | } 45 | modelConfig := variable.ConfigYml.GetString("Casbin.ModelConfig") 46 | 47 | if m, err := model.NewModelFromString(modelConfig); err != nil { 48 | return nil, errors.New(my_errors.ErrorCasbinNewModelFromStringFail + err.Error()) 49 | } else { 50 | if Enforcer, err = casbin.NewSyncedEnforcer(m, a); err != nil { 51 | return nil, errors.New(my_errors.ErrorCasbinCreateEnforcerFail) 52 | } 53 | _ = Enforcer.LoadPolicy() 54 | AutoLoadSeconds := variable.ConfigYml.GetDuration("Casbin.AutoLoadPolicySeconds") 55 | Enforcer.StartAutoLoadPolicy(time.Second * AutoLoadSeconds) 56 | return Enforcer, nil 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /docs/many_db_operate.md: -------------------------------------------------------------------------------- 1 | ### 同时操作部署在不同服务器的多种数据库 2 | > 1.本项目骨架在 [数据库操作单元测试](../test/gormv2_test.go) 已经提供了同时操作多服务器、多种数据库的示例代码,为了将此功能更清晰地展现出来,本篇将单独进行介绍. 3 | > 2.面对复杂场景,需要多个客户端连接到部署在多个不同服务器的 `mysql`、`sqlserver`、`postgresql` 等数据库时, 由于配置文件(config/gorm_v2.yml)只提供了一份数据库连接,无法满足需求,这时您可以通过自定义参数直接连接任意数据库,获取一个数据库句柄,供业务使用. 4 | 5 | 6 | ### 相关代码 7 | > 1.这里直接提取了相关的单元测试示例代码,更多其他操作仍然建议参考单元测试示例代码. 8 | ```code 9 | 10 | func TestCustomeParamsConnMysql(t *testing.T) { 11 | // 定义一个查询结果接受结构体 12 | type DataList struct { 13 | Id int 14 | Username string 15 | Last_login_ip string 16 | Status int 17 | } 18 | // 设置动态参数连接任意多个数据库,以mysql为例进行单元测试 19 | // 参数结构体 Write 和 Read 只有设置了具体指,才会生效,否则程序自动使用配置目录(config/gorm_v.yml)中的参数 20 | confPrams := gorm_v2.ConfigParams{ 21 | Write: struct { 22 | Host string 23 | DataBase string 24 | Port int 25 | Prefix string 26 | User string 27 | Pass string 28 | Charset string 29 | }{Host: "127.0.0.1", DataBase: "db_test", Port: 3306, Prefix: "tb_", User: "root", Pass: "DRsXT5ZJ6Oi55LPQ", Charset: "utf8"}, 30 | Read: struct { 31 | Host string 32 | DataBase string 33 | Port int 34 | Prefix string 35 | User string 36 | Pass string 37 | Charset string 38 | }{Host: "127.0.0.1", DataBase: "db_stocks", Port: 3306, Prefix: "tb_", User: "root", Pass: "DRsXT5ZJ6Oi55LPQ", Charset: "utf8"}} 39 | 40 | var vDataList []DataList 41 | 42 | //gorm_v2.GetSqlDriver 参数介绍 43 | // sqlType : mysql 、sqlserver、postgresql 等数据库库类型 44 | // readDbIsOpen : 是否开启读写分离,1表示开启读数据库的配置,那么 confPrams.Read 参数部分才会生效; 0 则表示 confPrams.Read 部分参数直接忽略(即 读、写同库) 45 | // confPrams 动态配置的数据库参数 46 | // 此外,其他参数,例如数据库连接池参数等,则直接调用配置项数据库连接池参数,基本不需要配置,这部分对实际操作影响不大 47 | if gormDbMysql, err := gorm_v2.GetSqlDriver("mysql", 0, confPrams); err == nil { 48 | gormDbMysql.Raw("select id,username,status,last_login_ip from tb_users").Find(&vDataList) 49 | fmt.Printf("Read 数据库查询结果:%v\n", vDataList) 50 | res := gormDbMysql.Exec("update tb_users set real_name='Write数据库更新' where id<=2 ") 51 | if res.Error==nil{ 52 | fmt.Println("write 数据库更新成功") 53 | }else{ 54 | t.Errorf("单元测试失败,相关错误:%s\n",res.Error.Error()) 55 | } 56 | } 57 | } 58 | 59 | ``` -------------------------------------------------------------------------------- /app/http/middleware/my_jwt/my_jwt.go: -------------------------------------------------------------------------------- 1 | package my_jwt 2 | 3 | import ( 4 | "errors" 5 | "github.com/dgrijalva/jwt-go" 6 | "goskeleton/app/global/my_errors" 7 | "time" 8 | ) 9 | 10 | // 使用工厂创建一个 JWT 结构体 11 | func CreateMyJWT(signKey string) *JwtSign { 12 | if len(signKey) <= 0 { 13 | signKey = "goskeleton" 14 | } 15 | return &JwtSign{ 16 | []byte(signKey), 17 | } 18 | } 19 | 20 | // 定义一个 JWT验签 结构体 21 | type JwtSign struct { 22 | SigningKey []byte 23 | } 24 | 25 | // CreateToken 生成一个token 26 | func (j *JwtSign) CreateToken(claims CustomClaims) (string, error) { 27 | // 生成jwt格式的header、claims 部分 28 | tokenPartA := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 29 | // 继续添加秘钥值,生成最后一部分 30 | return tokenPartA.SignedString(j.SigningKey) 31 | } 32 | 33 | // 解析Token 34 | func (j *JwtSign) ParseToken(tokenString string) (*CustomClaims, error) { 35 | token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) { 36 | return j.SigningKey, nil 37 | }) 38 | if token == nil { 39 | return nil, errors.New(my_errors.ErrorsTokenInvalid) 40 | } 41 | if err != nil { 42 | if ve, ok := err.(*jwt.ValidationError); ok { 43 | if ve.Errors&jwt.ValidationErrorMalformed != 0 { 44 | return nil, errors.New(my_errors.ErrorsTokenMalFormed) 45 | } else if ve.Errors&jwt.ValidationErrorNotValidYet != 0 { 46 | return nil, errors.New(my_errors.ErrorsTokenNotActiveYet) 47 | } else if ve.Errors&jwt.ValidationErrorExpired != 0 { 48 | // 如果 TokenExpired ,只是过期(格式都正确),我们认为他是有效的,接下可以允许刷新操作 49 | token.Valid = true 50 | goto labelHere 51 | } else { 52 | return nil, errors.New(my_errors.ErrorsTokenInvalid) 53 | } 54 | } 55 | } 56 | labelHere: 57 | if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid { 58 | return claims, nil 59 | } else { 60 | return nil, errors.New(my_errors.ErrorsTokenInvalid) 61 | } 62 | } 63 | 64 | // 更新token 65 | func (j *JwtSign) RefreshToken(tokenString string, extraAddSeconds int64) (string, error) { 66 | 67 | if CustomClaims, err := j.ParseToken(tokenString); err == nil { 68 | CustomClaims.ExpiresAt = time.Now().Unix() + extraAddSeconds 69 | return j.CreateToken(*CustomClaims) 70 | } else { 71 | return "", err 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /docs/deploy_nginx.md: -------------------------------------------------------------------------------- 1 | ### 运维方案之Nginx 2 | > 1.我们已经介绍完毕了 `Linux` 运维、`Mysql` 运维、`Redis` 运维,监控某个程序运行的状态整体流程大家都很熟练了,接下来继续介绍 `Nginx` 监控方案。 3 | 4 | 5 | #### 前言 6 | > 1.nginx监控的原理主要是编译niginx的时候增加 nginx-module-vts 模块,让他提供底层数据。 7 | > 2.其次需要安装nginx-vts-expoter 数据收集器,存储数据,等待被 prometheus 获取,最终在 grafana 展示。 8 | > 3.但是默认情况下,编译的nginx是没有这个模块的,因此在nginx新安装的时候就应该编译进去,如果是已有nginx,要么重新编译、要么使用docker方式进行重新配置替换原有nginx。 9 | > 4.由于我的nginx之前编译的时候没有编译 nginx-module-vts 模块,为了不影响已上线的项目,我们通过docker进行从头部署。 10 | > 11 | 12 | ### 正式开始部署nginx运维监控 13 | > 1.以下操作涉及到的ip 172.19.130.185 是我ip ,实际操作注意替换为自己服务器的ip。 14 | > 2.nginx部分使用我本人进行编排的dockerfile生成、并且已经配置好了nginx运行状态数据输出地址。 15 | ```code 16 | # step1,拉取 nginx_vts 镜像,该 nginx 版本已经集成了 https://codeload.github.com/vozlt/nginx-module-vts/tar.gz/v0.1.18,并且对容器进行了配置,直接在ip:80/status提供状态数据。 17 | docker pull zhangqifeng/nginx_vts:v1.4 18 | # step2, 启动nginx_vts 镜像,镜像中nginx 的配置(/usr/local/nginx/conf/)、日志目录(/usr/local/nginx/logs/) 站点根目录:/usr/local/nginx/html/ 数据卷映射暂时忽略,您可以通过 -v 自行映射 19 | docker container run --name nginx_vts -d -p 172.19.130.185:9506:80 zhangqifeng/nginx_vts:v1.4 20 | 21 | # step3, 此时你可以验证该nginx是否正常运行,只要有数据就是启动ok 22 | 访问地址 http://172.19.130.185:9506/status 、http://172.19.130.185:9506/status/format/json 23 | 24 | # step4, 拉取 nginx-vts-expoter 镜像,该镜像负责收集上一个镜像提供的运行状态数据,等待prometheus获取 25 | docker pull sophos/nginx-vts-exporter:latest # 该镜像的github地址:https://github.com/hnlq715/nginx-vts-exporter 26 | 27 | # step5, 启动 nginx-vts-exporter 28 | docker run -p 172.19.130.185:9913:9913 -d --name nginx-vts-exporter -ti --rm --env NGINX_STATUS="http://172.19.130.185:9506/status/format/json" sophos/nginx-vts-exporter 29 | 30 | # step6, 配置prometheus文件 31 | - job_name: "Aliyun_Nginx" 32 | static_configs: 33 | - targets: ["172.19.130.185:9913"] 34 | labels: 35 | instance: "Nginx_001" 36 | 37 | # step7, nginx-vts-expoter 采集 docker 容器中启动的 nginx:9506 端口数据,需要穿越防火墙 38 | # 以设置9506端口为例,9913端口仿照设置即可。 39 | firewall-cmd --zone=public --add-port=9506/tcp --permanent 40 | firewall-cmd --complete-reload 41 | 42 | # step8, 在 grafana 中导入nginx监控模板ID,2494 43 | // 相关模板地址:https://grafana.com/dashboards/2949 44 | 45 | ``` 46 | #### 最终效果图 47 | ![点击查看](https://www.ginskeleton.com/images/nginx_vts.png) -------------------------------------------------------------------------------- /app/utils/rabbitmq/hello_world/producer.go: -------------------------------------------------------------------------------- 1 | package hello_world 2 | 3 | import ( 4 | amqp "github.com/rabbitmq/amqp091-go" 5 | "goskeleton/app/global/variable" 6 | "goskeleton/app/utils/rabbitmq/error_record" 7 | ) 8 | 9 | // CreateProducer 创建一个生产者 10 | func CreateProducer() (*producer, error) { 11 | // 获取配置信息 12 | conn, err := amqp.Dial(variable.ConfigYml.GetString("RabbitMq.HelloWorld.Addr")) 13 | queueName := variable.ConfigYml.GetString("RabbitMq.HelloWorld.QueueName") 14 | dura := variable.ConfigYml.GetBool("RabbitMq.HelloWorld.Durable") 15 | 16 | if err != nil { 17 | variable.ZapLog.Error(err.Error()) 18 | return nil, err 19 | } 20 | 21 | prod := &producer{ 22 | connect: conn, 23 | queueName: queueName, 24 | durable: dura, 25 | } 26 | return prod, nil 27 | } 28 | 29 | // 定义一个消息队列结构体:helloworld 模型 30 | type producer struct { 31 | connect *amqp.Connection 32 | queueName string 33 | durable bool 34 | occurError error 35 | } 36 | 37 | func (p *producer) Send(data string) bool { 38 | 39 | // 获取一个通道 40 | ch, err := p.connect.Channel() 41 | p.occurError = error_record.ErrorDeal(err) 42 | 43 | defer func() { 44 | _ = ch.Close() 45 | }() 46 | 47 | // 声明消息队列 48 | _, err = ch.QueueDeclare( 49 | p.queueName, // 队列名称 50 | p.durable, //是否持久化,false模式数据全部处于内存,true会保存在erlang自带数据库,但是影响速度 51 | !p.durable, //生产者、消费者全部断开时是否删除队列。一般来说,数据需要持久化,就不删除;非持久化,就删除 52 | false, //是否私有队列,false标识允许多个 consumer 向该队列投递消息,true 表示独占 53 | false, // 队列如果已经在服务器声明,设置为 true ,否则设置为 false; 54 | nil, // 相关参数 55 | ) 56 | p.occurError = error_record.ErrorDeal(err) 57 | 58 | // 如果队列的声明是持久化的,那么消息也设置为持久化 59 | msgPersistent := amqp.Transient 60 | if p.durable { 61 | msgPersistent = amqp.Persistent 62 | } 63 | // 投递消息 64 | if err == nil { 65 | err = ch.Publish( 66 | "", // helloworld 、workqueue 模式设置为空字符串,表示使用默认交换机 67 | p.queueName, // routing key,注意:简单模式与队列名称相同 68 | false, 69 | false, 70 | amqp.Publishing{ 71 | DeliveryMode: msgPersistent, //消息是否持久化,这里与保持保持一致即可 72 | ContentType: "text/plain", 73 | Body: []byte(data), 74 | }) 75 | } 76 | p.occurError = error_record.ErrorDeal(err) 77 | if p.occurError != nil { // 发生错误,返回 false 78 | return false 79 | } else { 80 | return true 81 | } 82 | } 83 | 84 | // Close 发送完毕手动关闭,这样不影响send多次发送数据 85 | func (p *producer) Close() { 86 | _ = p.connect.Close() 87 | } 88 | -------------------------------------------------------------------------------- /app/utils/rabbitmq/work_queue/producer.go: -------------------------------------------------------------------------------- 1 | package work_queue 2 | 3 | import ( 4 | amqp "github.com/rabbitmq/amqp091-go" 5 | "goskeleton/app/global/variable" 6 | "goskeleton/app/utils/rabbitmq/error_record" 7 | ) 8 | 9 | // CreateProducer 创建一个生产者 10 | func CreateProducer() (*producer, error) { 11 | // 获取配置信息 12 | conn, err := amqp.Dial(variable.ConfigYml.GetString("RabbitMq.WorkQueue.Addr")) 13 | queueName := variable.ConfigYml.GetString("RabbitMq.WorkQueue.QueueName") 14 | durable := variable.ConfigYml.GetBool("RabbitMq.WorkQueue.Durable") 15 | 16 | if err != nil { 17 | variable.ZapLog.Error(err.Error()) 18 | return nil, err 19 | } 20 | 21 | prod := &producer{ 22 | connect: conn, 23 | queueName: queueName, 24 | durable: durable, 25 | } 26 | return prod, nil 27 | } 28 | 29 | // 定义一个消息队列结构体:helloworld 模型 30 | type producer struct { 31 | connect *amqp.Connection 32 | queueName string 33 | durable bool 34 | occurError error 35 | } 36 | 37 | func (p *producer) Send(data string) bool { 38 | 39 | // 获取一个频道 40 | ch, err := p.connect.Channel() 41 | p.occurError = error_record.ErrorDeal(err) 42 | 43 | defer func() { 44 | _ = ch.Close() 45 | }() 46 | 47 | // 声明消息队列 48 | _, err = ch.QueueDeclare( 49 | p.queueName, // 队列名称 50 | p.durable, //队列是否持久化,false模式数据全部处于内存,true会保存在erlang自带数据库,但是影响速度 51 | !p.durable, //生产者、消费者全部断开时是否删除队列。一般来说,数据需要持久化,就不删除;非持久化,就删除 52 | false, //是否私有队列,false标识允许多个 consumer 向该队列投递消息,true 表示独占 53 | false, // 队列如果已经在服务器声明,设置为 true ,否则设置为 false; 54 | nil, // 相关参数 55 | ) 56 | p.occurError = error_record.ErrorDeal(err) 57 | 58 | // 如果队列的声明是持久化的,那么消息也设置为持久化 59 | msgPersistent := amqp.Transient 60 | if p.durable { 61 | msgPersistent = amqp.Persistent 62 | } 63 | 64 | // 投递消息 65 | if err == nil { 66 | err = ch.Publish( 67 | "", // helloworld 、workqueue 模式设置为空字符串,表示使用默认交换机 68 | p.queueName, // 注意:简单模式 key 表示队列名称 69 | false, 70 | false, 71 | amqp.Publishing{ 72 | DeliveryMode: msgPersistent, //消息是否持久化,这里与保持保持一致即可 73 | ContentType: "text/plain", 74 | Body: []byte(data), 75 | }) 76 | } 77 | p.occurError = error_record.ErrorDeal(err) 78 | if p.occurError != nil { // 发生错误,返回 false 79 | return false 80 | } else { 81 | return true 82 | } 83 | } 84 | 85 | // Close 发送完毕手动关闭,这样不影响send多次发送数据 86 | func (p *producer) Close() { 87 | _ = p.connect.Close() 88 | } 89 | -------------------------------------------------------------------------------- /docs/project_analysis_2.md: -------------------------------------------------------------------------------- 1 | ## GoSkeleton 项目骨架性能分析报告(二) 2 | > 1.本次我们分析的目标是操作数据库, 通过操作数据库,分析相关代码段cpu的耗时,得出可视化的性能分析报告。 3 | 4 | 5 | ### 操作数据库, 我们需要做如下铺垫代码 6 | > 1.我们本次分析的核心是在数据库操作部分, 因此我们在路由出添加如下代码,访问路由即可触发数据库的调用. 7 | ```code 8 | router.GET("/", func(context *gin.Context) { 9 | // 默认路由处直接触发数据库调用 10 | if model.CreateTestFactory("").SelectDataMultiple() { 11 | context.String(200,"批量查询数据OK") 12 | } else { 13 | context.String(200,"批量查询数据出错") 14 | } 15 | context.String(http.StatusOK, "Api 模块接口 hello word!") 16 | }) 17 | ``` 18 | 19 | > 2.数据库部分代码,主要逻辑是每次查询1000条,循环查询了100次,并且在最后一次输出了结果集. 20 | ```code 21 | func (t *Test) SelectDataMultiple() bool { 22 | // 本次测试的数据表内有6000条左右数据 23 | sql := ` 24 | SELECT 25 | code,name,company_name,indudtry,created_at 26 | FROM 27 | db_stocks.tb_code_list 28 | LIMIT 0, 1000 ; 29 | ` 30 | //1.首先独立预处理sql语句,无参数 31 | if t.PrepareSql(sql) { 32 | 33 | var code, name, company_name, indudtry, created_at string 34 | for i := 1; i <= 100; i++ { 35 | //2.执行批量查询 36 | rows := t.QuerySqlForMultiple() 37 | if rows == nil { 38 | variable.ZapLog.Sugar().Error("sql执行失败,sql:", sql) 39 | return false 40 | } else { 41 | // 我们只输出最后一行数据 42 | if i == 100 { 43 | for rows.Next() { 44 | _ = rows.Scan(&code, &name, &company_name, &indudtry, &created_at) 45 | fmt.Println(code, name, company_name, indudtry, created_at) 46 | } 47 | } 48 | } 49 | rows.Close() 50 | } 51 | } 52 | variable.ZapLog.Info("批量查询sql执行完毕!") 53 | return true 54 | } 55 | ``` 56 | ### cpu 底层数据采集步骤 57 | > 1.浏览器访问pprof接口:`http://127.0.0.1:20191/debug/pprof/`, 点击 `profile` 选项,程序会对本项目进程, 进行 cpu 使用情况底层数据采集, 该过程会持续 30 秒. 58 | ![pprof地址](https://www.ginskeleton.com/images/pprof_menue.jpg) 59 | > 2.新开浏览器窗口,输入 `http://127.0.0.1:20191/` 刷新,触发路由中的数据库操作代码, 等待被 pprof 采集数据. 60 | > 3.稍等片刻,30秒之后,您点击过的步骤1就会提示下载文件:`profile`, 请保存在您能记住的路径中,稍后马上使用该文件(profile), 至此cpu数据已经采集完毕. 61 | 62 | ### cpu数据分析步骤 63 | > 1.首先下载安装 [graphviz](https://www.graphviz.org/download/) ,根据您的系统选择相对应的版本安装,安装完成记得将`安装目录/bin`, 加入系统环境变量. 64 | > 2.打开cmd窗口,执行 `dot -V` ,会显示版本信息,说明安装已经OK, 那么继续执行 `dot -c` 安装图形显示所需要的插件. 65 | > 3.在cpu数据采集环节第三步,您已经得到了 `profile` 文件,那么就在同目录打开cmd窗口,执行 `go tool pprof profile`, 然后输入 `web` 回车就会自动打开浏览器,展示给您如下结果图: 66 | 67 | ### 报告详情参见如下图 68 | ![cpu分析_上](https://www.ginskeleton.com/images/cpu_sql.png) 69 | -------------------------------------------------------------------------------- /docs/low_coupling.md: -------------------------------------------------------------------------------- 1 | ### 本篇将探讨主线解耦问题 2 | > 1.目前项目主线是从路由开始,直接切入到表单参数验证器,验证通过则直接进入了控制器,这里就导致了验证器和控制器之间存在一点低耦合度. 3 | > 2.如果你追求更低的模块之间的耦合度,接下来我们将对上述问题进行解耦操作. 4 | 5 | 6 | 7 | ### 当前项目代码存在的低耦合逻辑 8 | > 1.我们以用户删除数据接口为例进行介绍. 9 | > 2.本文的 `41` 行就是我们所说验证器与控制器出现了低耦合. 10 | ```code 11 | 12 | // 1.访问路由 13 | users.POST("delete", validatorFactory.Create(consts.ValidatorPrefix+"UsersDestroy")) 14 | 15 | 16 | // 2.进入表单参数验证器 17 | type Destroy struct { 18 | Id float64 `form:"id" json:"id" binding:"required,min=1"` 19 | } 20 | 21 | func (d Destroy) CheckParams(context *gin.Context) { 22 | 23 | if err := context.ShouldBind(&d); err != nil { 24 | errs := gin.H{ 25 | "tips": "UserDestroy参数校验失败,参数校验失败,请检查id(>=1)", 26 | "err": err.Error(), 27 | } 28 | response.ErrorParam(context, errs) 29 | return 30 | } 31 | 32 | // 该函数主要是将绑定的数据以 键=>值 形式直接传递给下一步(控制器) 33 | extraAddBindDataContext := data_transfer.DataAddContext(d, consts.ValidatorPrefix, context) 34 | if extraAddBindDataContext == nil { 35 | response.ErrorSystem(context, "UserShow表单参数验证器json化失败", "") 36 | context.Abort() 37 | return 38 | } else { 39 | // 验证完成,调用控制器,并将验证器成员(字段)递给控制器,保持上下文数据一致性 40 | // 以下代码就是验证器与控制器之间的一点耦合 41 | (&web.Users{}).Destroy(extraAddBindDataContext) 42 | } 43 | } 44 | 45 | ``` 46 | 47 | ### 开始解耦 48 | > 1.针对41行出现的验证器与控制器耦合问题,我们开始解耦 49 | ```code 50 | 51 | // 1.我们对以上代码进行简单的改造即可实现代码的解耦 52 | // 2.路由首先切入表单参数验证器,将对应的控制器代码写在第二个回调函数即可 53 | // 3.注意:市面上很多框架的中间件等注册的函数都是 "洋葱模型" ,即函数的回调顺序和注册顺序是相反的,但是gin框架则是按照注册顺序依次执行 54 | users.POST("delete", validatorFactory.Create(consts.ValidatorPrefix+"UsersDestroy"), (&web.Users{}).Destroy) 55 | 56 | 57 | // 4.代码经过以上改在以后, 从 38 行开始的 else { ... } 代码删除即可 58 | 59 | ``` 60 | 61 | ### 解耦以后的注意事项 62 | > 1.如果业务针对控制器存在比较多的 `Aop` 切面编程,就会导致路由文件以及 `import` 显得比较繁重 63 | ```code 64 | 65 | // 1.例如删除数据之前的和之后的回调 66 | users.POST("delete", 67 | 68 | validatorFactory.Create(consts.ValidatorPrefix+"UsersDestroy"), 69 | 70 | (&Users.DestroyBefore{}).Before, // 控制器Aop的前置回调,例如删除数据之前的权限判断,相关代码可参考 app/aop/users/destroy_before.go 71 | (&web.Users{}).Destroy, // 控制器逻辑 72 | (&Users.DestroyAfter{}).After // 控制器Aop的后置回调,例如被删除数据之后的数据备份至history表 ,相关代码可参考 app/aop/users/destroy_after.go 73 | 74 | ) 75 | 76 | ``` 77 | > 2.对比以上代码,如果你的项目存在较多的 `AOP` 编程、或者说不同的路由前、后回调函数比较多,不建议进行解耦(毕竟目前就是极低耦合),否则给路由文件以及 `import` 部分带来了比较多的负担. 78 | > 3.如果你的项目路由前后回调函数比较少,建议参考以上代码进行解耦. 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /app/model/base_model.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | "gorm.io/gorm" 6 | "goskeleton/app/global/my_errors" 7 | "goskeleton/app/global/variable" 8 | "strings" 9 | ) 10 | 11 | type BaseModel struct { 12 | *gorm.DB `gorm:"-" json:"-"` 13 | Id int64 `gorm:"primaryKey" json:"id"` 14 | CreatedAt string `json:"created_at"` //日期时间字段统一设置为字符串即可 15 | UpdatedAt string `json:"updated_at"` 16 | //DeletedAt gorm.DeletedAt `json:"deleted_at"` // 如果开发者需要使用软删除功能,打开本行注释掉的代码即可,同时需要在数据库的所有表增加字段deleted_at 类型为 datetime 17 | } 18 | 19 | func UseDbConn(sqlType string) *gorm.DB { 20 | var db *gorm.DB 21 | sqlType = strings.Trim(sqlType, " ") 22 | if sqlType == "" { 23 | sqlType = variable.ConfigGormv2Yml.GetString("Gormv2.UseDbType") 24 | } 25 | switch strings.ToLower(sqlType) { 26 | case "mysql": 27 | if variable.GormDbMysql == nil { 28 | variable.ZapLog.Fatal(fmt.Sprintf(my_errors.ErrorsGormNotInitGlobalPointer, sqlType, sqlType)) 29 | } 30 | db = variable.GormDbMysql 31 | case "sqlserver": 32 | if variable.GormDbSqlserver == nil { 33 | variable.ZapLog.Fatal(fmt.Sprintf(my_errors.ErrorsGormNotInitGlobalPointer, sqlType, sqlType)) 34 | } 35 | db = variable.GormDbSqlserver 36 | case "postgres", "postgre", "postgresql": 37 | if variable.GormDbPostgreSql == nil { 38 | variable.ZapLog.Fatal(fmt.Sprintf(my_errors.ErrorsGormNotInitGlobalPointer, sqlType, sqlType)) 39 | } 40 | db = variable.GormDbPostgreSql 41 | default: 42 | variable.ZapLog.Error(my_errors.ErrorsDbDriverNotExists + sqlType) 43 | } 44 | return db 45 | } 46 | 47 | // 在 ginskeleton项目中如果在业务 model 设置了回调函数,请看以下说明 48 | // 注意:gorm 的自动回调函数(BeforeCreate、BeforeUpdate 等),不是由本项目的 Create ... 函数先初始化然后调用的,而是gorm自动直接调用的, 49 | // 所以 接收器 b 的所有参数都是没有赋值的,因此这里需要给 b.DB 赋予回调的 gormDb 50 | // baseModel 的代码执行顺序晚于其他业务 model 的回调函数,如果回调函数名称相同,会被普通业务model的同名回调函数覆盖 51 | // gorm 支持的自动回调函数清单:https://github.com/go-gorm/gorm/blob/master/callbacks/interfaces.go 52 | 53 | //func (b *BaseModel) BeforeCreate(gormDB *gorm.DB) error { 54 | // 第一步必须反向将 gormDB 赋值给 b.DB 55 | // b.DB = gormDB 56 | // 后续的代码就可以像普通业务 model 一样操作, 57 | // b.Exec(sql,参数1,参数2,...) 58 | // b.Raw(sql,参数1,参数2,...) 59 | // return nil 60 | //} 61 | 62 | // BeforeUpdate、BeforeSave 函数都会因为 更新类的操作而被触发 63 | // 如果baseModel 和 普通业务 model 都想使用回调函数,那么请设置不同的回调函数名,例如:这里设置 BeforeUpdate、普通业务model 设置 BeforeSave 即可 64 | //func (b *BaseModel) BeforeUpdate(gormDB *gorm.DB) error { 65 | // 第一步必须反向将 gormDB 赋值给 b.DB 66 | // b.DB = gormDB 67 | // 后续的代码就可以像普通业务 model 一样操作, 68 | // b.Exec(sql,参数1,参数2,...) 69 | // b.Raw(sql,参数1,参数2,...) 70 | // return nil 71 | //} 72 | -------------------------------------------------------------------------------- /database/db_demo_mysql.sql: -------------------------------------------------------------------------------- 1 | 2 | CREATE DATABASE /*!32312 IF NOT EXISTS*/`db_goskeleton` /*!40100 DEFAULT CHARACTER SET utf8 */; 3 | 4 | USE `db_goskeleton`; 5 | 6 | /*Table structure for table `tb_users` */ 7 | 8 | DROP TABLE IF EXISTS `tb_users`; 9 | 10 | CREATE TABLE `tb_users` ( 11 | `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, 12 | `user_name` VARCHAR(30) DEFAULT '' COMMENT '账号', 13 | `pass` VARCHAR(128) DEFAULT '' COMMENT '密码', 14 | `real_name` VARCHAR(30) DEFAULT '' COMMENT '姓名', 15 | `phone` CHAR(11) DEFAULT '' COMMENT '手机', 16 | `status` TINYINT(4) DEFAULT 1 COMMENT '状态', 17 | `remark` VARCHAR(255) DEFAULT '' COMMENT '备注', 18 | `last_login_time` DATETIME DEFAULT CURRENT_TIMESTAMP, 19 | `last_login_ip` CHAR(30) DEFAULT '' COMMENT '最近一次登录ip', 20 | `login_times` INT(11) DEFAULT 0 COMMENT '累计登录次数', 21 | `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, 22 | `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP, 23 | PRIMARY KEY (`id`) 24 | ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; 25 | 26 | /* oauth 表,主要控制一个用户可以同时拥有几个有效的token,通俗地说就是允许一个账号同时有几个人登录,超过将会导致最前面的人的token失效,而退出登录*/ 27 | DROP TABLE IF EXISTS `tb_oauth_access_tokens`; 28 | 29 | CREATE TABLE `tb_oauth_access_tokens` ( 30 | `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT, 31 | `fr_user_id` INT(11) DEFAULT 0 COMMENT '外键:tb_users表id', 32 | `client_id` INT(10) UNSIGNED DEFAULT 1 COMMENT '普通用户的授权,默认为1', 33 | `token` VARCHAR(500) DEFAULT NULL, 34 | `action_name` VARCHAR(128) CHARACTER SET utf8 COLLATE utf8_unicode_ci DEFAULT '' COMMENT 'login|refresh|reset表示token生成动作', 35 | `scopes` VARCHAR(128) CHARACTER SET utf8 COLLATE utf8_unicode_ci DEFAULT '[*]' COMMENT '暂时预留,未启用', 36 | `revoked` TINYINT(1) DEFAULT 0 COMMENT '是否撤销', 37 | `client_ip` VARCHAR(128) DEFAULT NULL COMMENT 'ipv6最长为128位', 38 | `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, 39 | `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP, 40 | `expires_at` DATETIME DEFAULT NULL, 41 | PRIMARY KEY (`id`), 42 | KEY `oauth_access_tokens_user_id_index` (`fr_user_id`) 43 | ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; 44 | 45 | /* 创建基于casbin控制接口访问的权限表*/ 46 | DROP TABLE IF EXISTS `tb_auth_casbin_rule`; 47 | CREATE TABLE `tb_auth_casbin_rule` ( 48 | `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 49 | `ptype` varchar(100) DEFAULT 'p', 50 | `v0` varchar(100) DEFAULT '', 51 | `v1` varchar(100) DEFAULT '', 52 | `v2` varchar(100) DEFAULT '*', 53 | `v3` varchar(100) DEFAULT '', 54 | `v4` varchar(100) DEFAULT '', 55 | `v5` varchar(100) DEFAULT '', 56 | PRIMARY KEY (`id`), 57 | UNIQUE KEY `unique_index` (`ptype`,`v0`,`v1`,`v2`,`v3`,`v4`,`v5`) 58 | ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 59 | 60 | 61 | -------------------------------------------------------------------------------- /command/demo/demo.go: -------------------------------------------------------------------------------- 1 | package demo 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | "goskeleton/app/global/variable" 6 | ) 7 | 8 | // Demo示例文件,我们假设一个场景: 9 | // 通过一个命令指定 搜索引擎(百度、搜狗、谷歌)、搜索类型(文本、图片)、关键词 执行一系列的命令 10 | 11 | var ( 12 | // 1.定义一个变量,接收搜索引擎(百度、搜狗、谷歌) 13 | SearchEngines string 14 | // 2.搜索的类型(图片、文字) 15 | SearchType string 16 | // 3.关键词 17 | KeyWords string 18 | ) 19 | 20 | var logger = variable.ZapLog.Sugar() 21 | 22 | // 定义命令 23 | var Demo1 = &cobra.Command{ 24 | Use: "sousuo", 25 | Aliases: []string{"sou", "ss", "s"}, // 定义别名 26 | Short: "这是一个Demo,以搜索内容进行演示业务逻辑...", 27 | Long: `调用方法: 28 | 1.进入项目根目录(Ginkeleton)。 29 | 2.执行 go run cmd/cli/main.go sousuo -h //可以查看使用指南 30 | 3.执行 go run cmd/cli/main.go sousuo 百度 // 快速运行一个Demo 31 | 4.执行 go run cmd/cli/main.go sousuo 百度 -K 关键词 -E baidu -T img // 指定参数运行Demo 32 | `, 33 | //Args: cobra.ExactArgs(2), // 限制非flag参数(也叫作位置参数)的个数必须等于 2 ,否则会报错 34 | // Run命令以及子命令的前置函数 35 | PersistentPreRun: func(cmd *cobra.Command, args []string) { 36 | //如果只想作为子命令的回调,可以通过相关参数做判断,仅在子命令执行 37 | logger.Infof("Run函数子命令的前置方法,位置参数:%v ,flag参数:%s, %s, %s \n", args[0], SearchEngines, SearchType, KeyWords) 38 | }, 39 | // Run命令的前置函数 40 | PreRun: func(cmd *cobra.Command, args []string) { 41 | logger.Infof("Run函数的前置方法,位置参数:%v ,flag参数:%s, %s, %s \n", args[0], SearchEngines, SearchType, KeyWords) 42 | 43 | }, 44 | // Run 命令是 核心 命令,其余命令都是为该命令服务,可以删除,由您自由选择 45 | Run: func(cmd *cobra.Command, args []string) { 46 | //args 参数表示非flag(也叫作位置参数),该参数默认会作为一个数组存储。 47 | //fmt.Println(args) 48 | start(SearchEngines, SearchType, KeyWords) 49 | }, 50 | // Run命令的后置函数 51 | PostRun: func(cmd *cobra.Command, args []string) { 52 | logger.Infof("Run函数的后置方法,位置参数:%v ,flag参数:%s, %s, %s \n", args[0], SearchEngines, SearchType, KeyWords) 53 | }, 54 | // Run命令以及子命令的后置函数 55 | PersistentPostRun: func(cmd *cobra.Command, args []string) { 56 | //如果只想作为子命令的回调,可以通过相关参数做判断,仅在子命令执行 57 | logger.Infof("Run函数子命令的后置方法,位置参数:%v ,flag参数:%s, %s, %s \n", args[0], SearchEngines, SearchType, KeyWords) 58 | }, 59 | } 60 | 61 | // 注册命令、初始化参数 62 | func init() { 63 | Demo1.AddCommand(subCmd) 64 | Demo1.Flags().StringVarP(&SearchEngines, "Engines", "E", "baidu", "-E 或者 --Engines 选择搜索引擎,例如:baidu、sogou") 65 | Demo1.Flags().StringVarP(&SearchType, "Type", "T", "img", "-T 或者 --Type 选择搜索的内容类型,例如:图片类") 66 | Demo1.Flags().StringVarP(&KeyWords, "KeyWords", "K", "关键词", "-K 或者 --KeyWords 搜索的关键词") 67 | //Demo1.Flags().BoolP(1,2,3,5) //接收bool类型参数 68 | //Demo1.Flags().Int64P() //接收int型 69 | } 70 | 71 | //开始执行 72 | func start(SearchEngines, SearchType, KeyWords string) { 73 | 74 | logger.Infof("您输入的搜索引擎:%s, 搜索类型:%s, 关键词:%s\n", SearchEngines, SearchType, KeyWords) 75 | 76 | } 77 | -------------------------------------------------------------------------------- /app/utils/zap_factory/zap_factory.go: -------------------------------------------------------------------------------- 1 | package zap_factory 2 | 3 | import ( 4 | "github.com/natefinch/lumberjack" 5 | "go.uber.org/zap" 6 | "go.uber.org/zap/zapcore" 7 | "goskeleton/app/global/variable" 8 | "log" 9 | "time" 10 | ) 11 | 12 | func CreateZapFactory(entry func(zapcore.Entry) error) *zap.Logger { 13 | 14 | // 获取程序所处的模式: 开发调试 、 生产 15 | //variable.ConfigYml := yml_config.CreateYamlFactory() 16 | appDebug := variable.ConfigYml.GetBool("AppDebug") 17 | 18 | // 判断程序当前所处的模式,调试模式直接返回一个便捷的zap日志管理器地址,所有的日志打印到控制台即可 19 | if appDebug == true { 20 | if logger, err := zap.NewDevelopment(zap.Hooks(entry)); err == nil { 21 | return logger 22 | } else { 23 | log.Fatal("创建zap日志包失败,详情:" + err.Error()) 24 | } 25 | } 26 | 27 | // 以下才是 非调试(生产)模式所需要的代码 28 | encoderConfig := zap.NewProductionEncoderConfig() 29 | 30 | timePrecision := variable.ConfigYml.GetString("Logs.TimePrecision") 31 | var recordTimeFormat string 32 | switch timePrecision { 33 | case "second": 34 | recordTimeFormat = "2006-01-02 15:04:05" 35 | case "millisecond": 36 | recordTimeFormat = "2006-01-02 15:04:05.000" 37 | default: 38 | recordTimeFormat = "2006-01-02 15:04:05" 39 | 40 | } 41 | encoderConfig.EncodeTime = func(t time.Time, enc zapcore.PrimitiveArrayEncoder) { 42 | enc.AppendString(t.Format(recordTimeFormat)) 43 | } 44 | encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder 45 | encoderConfig.TimeKey = "created_at" // 生成json格式日志的时间键字段,默认为 ts,修改以后方便日志导入到 ELK 服务器 46 | 47 | var encoder zapcore.Encoder 48 | switch variable.ConfigYml.GetString("Logs.TextFormat") { 49 | case "console": 50 | encoder = zapcore.NewConsoleEncoder(encoderConfig) // 普通模式 51 | case "json": 52 | encoder = zapcore.NewJSONEncoder(encoderConfig) // json格式 53 | default: 54 | encoder = zapcore.NewConsoleEncoder(encoderConfig) // 普通模式 55 | } 56 | 57 | //写入器 58 | fileName := variable.BasePath + variable.ConfigYml.GetString("Logs.GoSkeletonLogName") 59 | lumberJackLogger := &lumberjack.Logger{ 60 | Filename: fileName, //日志文件的位置 61 | MaxSize: variable.ConfigYml.GetInt("Logs.MaxSize"), //在进行切割之前,日志文件的最大大小(以MB为单位) 62 | MaxBackups: variable.ConfigYml.GetInt("Logs.MaxBackups"), //保留旧文件的最大个数 63 | MaxAge: variable.ConfigYml.GetInt("Logs.MaxAge"), //保留旧文件的最大天数 64 | Compress: variable.ConfigYml.GetBool("Logs.Compress"), //是否压缩/归档旧文件 65 | } 66 | writer := zapcore.AddSync(lumberJackLogger) 67 | // 开始初始化zap日志核心参数, 68 | //参数一:编码器 69 | //参数二:写入器 70 | //参数三:参数级别,debug级别支持后续调用的所有函数写日志,如果是 fatal 高级别,则级别>=fatal 才可以写日志 71 | zapCore := zapcore.NewCore(encoder, writer, zap.InfoLevel) 72 | return zap.New(zapCore, zap.AddCaller(), zap.Hooks(entry), zap.AddStacktrace(zap.WarnLevel)) 73 | } 74 | -------------------------------------------------------------------------------- /routers/api.go: -------------------------------------------------------------------------------- 1 | package routers 2 | 3 | import ( 4 | "github.com/gin-contrib/pprof" 5 | "github.com/gin-gonic/gin" 6 | "go.uber.org/zap" 7 | "goskeleton/app/global/consts" 8 | "goskeleton/app/global/variable" 9 | "goskeleton/app/http/middleware/cors" 10 | validatorFactory "goskeleton/app/http/validator/core/factory" 11 | "goskeleton/app/utils/gin_release" 12 | "net/http" 13 | ) 14 | 15 | // 该路由主要设置门户类网站等前台路由 16 | 17 | func InitApiRouter() *gin.Engine { 18 | var router *gin.Engine 19 | // 非调试模式(生产模式) 日志写到日志文件 20 | if variable.ConfigYml.GetBool("AppDebug") == false { 21 | //1.gin自行记录接口访问日志,不需要nginx,如果开启以下3行,那么请屏蔽第 34 行代码 22 | //gin.DisableConsoleColor() 23 | //f, _ := os.Create(variable.BasePath + variable.ConfigYml.GetString("Logs.GinLogName")) 24 | //gin.DefaultWriter = io.MultiWriter(f) 25 | 26 | //【生产模式】 27 | // 根据 gin 官方的说明:[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production. 28 | // 如果部署到生产环境,请使用以下模式: 29 | // 1.生产模式(release) 和开发模式的变化主要是禁用 gin 记录接口访问日志, 30 | // 2.go服务就必须使用nginx作为前置代理服务,这样也方便实现负载均衡 31 | // 3.如果程序发生 panic 等异常使用自定义的 panic 恢复中间件拦截、记录到日志 32 | router = gin_release.ReleaseRouter() 33 | } else { 34 | // 调试模式,开启 pprof 包,便于开发阶段分析程序性能 35 | router = gin.Default() 36 | pprof.Register(router) 37 | } 38 | // 设置可信任的代理服务器列表,gin (2021-11-24发布的v1.7.7版本之后出的新功能) 39 | if variable.ConfigYml.GetInt("HttpServer.TrustProxies.IsOpen") == 1 { 40 | if err := router.SetTrustedProxies(variable.ConfigYml.GetStringSlice("HttpServer.TrustProxies.ProxyServerList")); err != nil { 41 | variable.ZapLog.Error(consts.GinSetTrustProxyError, zap.Error(err)) 42 | } 43 | } else { 44 | _ = router.SetTrustedProxies(nil) 45 | } 46 | 47 | //根据配置进行设置跨域 48 | if variable.ConfigYml.GetBool("HttpServer.AllowCrossDomain") { 49 | router.Use(cors.Next()) 50 | } 51 | 52 | router.GET("/", func(context *gin.Context) { 53 | context.String(http.StatusOK, "Api 模块接口 hello word!") 54 | }) 55 | 56 | //处理静态资源(不建议gin框架处理静态资源,参见 Public/readme.md 说明 ) 57 | router.Static("/public", "./public") // 定义静态资源路由与实际目录映射关系 58 | //router.StaticFile("/abcd", "./public/readme.md") // 可以根据文件名绑定需要返回的文件名 59 | 60 | // 创建一个门户类接口路由组 61 | vApi := router.Group("/api/v1/") 62 | { 63 | // 模拟一个首页路由 64 | home := vApi.Group("home/") 65 | { 66 | // 第二个参数说明: 67 | // 1.它是一个表单参数验证器函数代码段,该函数从容器中解析,整个代码段略显复杂,但是对于使用者,您只需要了解用法即可,使用很简单,看下面 ↓↓↓ 68 | // 2.编写该接口的验证器,位置:app/http/validator/api/home/news.go 69 | // 3.将以上验证器注册在容器:app/http/validator/common/register_validator/api_register_validator.go 18 行为注册时的键(consts.ValidatorPrefix + "HomeNews")。那么获取的时候就用该键即可从容器获取 70 | home.GET("news", validatorFactory.Create(consts.ValidatorPrefix+"HomeNews")) 71 | } 72 | } 73 | return router 74 | } 75 | -------------------------------------------------------------------------------- /app/service/websocket/ws.go: -------------------------------------------------------------------------------- 1 | package websocket 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gin-gonic/gin" 6 | "github.com/gorilla/websocket" 7 | "go.uber.org/zap" 8 | "goskeleton/app/global/consts" 9 | "goskeleton/app/global/my_errors" 10 | "goskeleton/app/global/variable" 11 | "goskeleton/app/utils/websocket/core" 12 | ) 13 | 14 | /** 15 | websocket模块相关事件执行顺序: 16 | 1.onOpen 17 | 2.OnMessage 18 | 3.OnError 19 | 4.OnClose 20 | */ 21 | 22 | type Ws struct { 23 | WsClient *core.Client 24 | } 25 | 26 | // OnOpen 事件函数 27 | func (w *Ws) OnOpen(context *gin.Context) (*Ws, bool) { 28 | if client, ok := (&core.Client{}).OnOpen(context); ok { 29 | 30 | token := context.GetString(consts.ValidatorPrefix + "token") 31 | variable.ZapLog.Info("获取到的客户端上线时携带的唯一标记值:", zap.String("token", token)) 32 | 33 | // 成功上线以后,开发者可以基于客户端上线时携带的唯一参数(这里用token键表示) 34 | // 在数据库查询更多的其他字段信息,直接追加在 Client 结构体上,方便后续使用 35 | //client.ClientMoreParams.UserParams1 = "123" 36 | //client.ClientMoreParams.UserParams2 = "456" 37 | //fmt.Printf("最终每一个客户端(client) 已有的参数:%+v\n", client) 38 | 39 | w.WsClient = client 40 | go w.WsClient.Heartbeat() // 一旦握手+协议升级成功,就为每一个连接开启一个自动化的隐式心跳检测包 41 | return w, true 42 | } else { 43 | return nil, false 44 | } 45 | } 46 | 47 | // OnMessage 处理业务消息 48 | func (w *Ws) OnMessage(context *gin.Context) { 49 | go w.WsClient.ReadPump(func(messageType int, receivedData []byte) { 50 | //参数说明 51 | //messageType 消息类型,1=文本 52 | //receivedData 服务器接收到客户端(例如js客户端)发来的的数据,[]byte 格式 53 | 54 | tempMsg := "服务器已经收到了你的消息==>" + string(receivedData) 55 | // 回复客户端已经收到消息; 56 | if err := w.WsClient.SendMessage(messageType, tempMsg); err != nil { 57 | variable.ZapLog.Error("消息发送出现错误", zap.Error(err)) 58 | } 59 | 60 | }, w.OnError, w.OnClose) 61 | } 62 | 63 | // OnError 客户端与服务端在消息交互过程中发生错误回调函数 64 | func (w *Ws) OnError(err error) { 65 | w.WsClient.State = 0 // 发生错误,状态设置为0, 心跳检测协程则自动退出 66 | variable.ZapLog.Error("远端掉线、卡死、刷新浏览器等会触发该错误:", zap.Error(err)) 67 | //fmt.Printf("远端掉线、卡死、刷新浏览器等会触发该错误: %v\n", err.Error()) 68 | } 69 | 70 | // OnClose 客户端关闭回调,发生onError回调以后会继续回调该函数 71 | func (w *Ws) OnClose() { 72 | 73 | w.WsClient.Hub.UnRegister <- w.WsClient // 向hub管道投递一条注销消息,由hub中心负责关闭连接、删除在线数据 74 | } 75 | 76 | // GetOnlineClients 获取在线的全部客户端 77 | func (w *Ws) GetOnlineClients() { 78 | 79 | fmt.Printf("在线客户端数量:%d\n", len(w.WsClient.Hub.Clients)) 80 | } 81 | 82 | // BroadcastMsg (每一个客户端都有能力)向全部在线客户端广播消息 83 | func (w *Ws) BroadcastMsg(sendMsg string) { 84 | for onlineClient := range w.WsClient.Hub.Clients { 85 | 86 | //获取每一个在线的客户端,向远端发送消息 87 | if err := onlineClient.SendMessage(websocket.TextMessage, sendMsg); err != nil { 88 | variable.ZapLog.Error(my_errors.ErrorsWebsocketWriteMgsFail, zap.Error(err)) 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /app/http/controller/captcha/captcha_controller.go: -------------------------------------------------------------------------------- 1 | package captcha 2 | 3 | import ( 4 | "bytes" 5 | "github.com/dchest/captcha" 6 | "github.com/gin-gonic/gin" 7 | "goskeleton/app/global/consts" 8 | "goskeleton/app/global/variable" 9 | "goskeleton/app/utils/response" 10 | "net/http" 11 | "path" 12 | "time" 13 | ) 14 | 15 | type Captcha struct{} 16 | 17 | // 生成验证码ID 18 | func (c *Captcha) GenerateId(context *gin.Context) { 19 | // 设置验证码的数字长度(个数) 20 | var length = variable.ConfigYml.GetInt("Captcha.length") 21 | var captchaId, imgUrl, refresh, verify string 22 | 23 | captchaId = captcha.NewLen(length) 24 | imgUrl = "/captcha/" + captchaId + ".png" 25 | refresh = imgUrl + "?reload=1" 26 | verify = "/captcha/" + captchaId + "/这里替换为正确的验证码进行验证" 27 | 28 | response.Success(context, "验证码信息", gin.H{ 29 | "id": captchaId, 30 | "img_url": imgUrl, 31 | "refresh": refresh, 32 | "verify": verify, 33 | }) 34 | 35 | } 36 | 37 | // 获取验证码图像 38 | func (c *Captcha) GetImg(context *gin.Context) { 39 | captchaIdKey := variable.ConfigYml.GetString("Captcha.captchaId") 40 | captchaId := context.Param(captchaIdKey) 41 | _, file := path.Split(context.Request.URL.Path) 42 | ext := path.Ext(file) 43 | id := file[:len(file)-len(ext)] 44 | if ext == "" || captchaId == "" { 45 | response.Fail(context, consts.CaptchaGetParamsInvalidCode, consts.CaptchaGetParamsInvalidMsg, "") 46 | return 47 | } 48 | 49 | if context.Query("reload") != "" { 50 | captcha.Reload(id) 51 | } 52 | 53 | context.Header("Cache-Control", "no-cache, no-store, must-revalidate") 54 | context.Header("Pragma", "no-cache") 55 | context.Header("Expires", "0") 56 | 57 | var vBytes bytes.Buffer 58 | if ext == ".png" { 59 | context.Header("Content-Type", "image/png") 60 | // 设置实际业务需要的验证码图片尺寸(宽 X 高),captcha.StdWidth, captcha.StdHeight 为默认值,请自行修改为具体数字即可 61 | _ = captcha.WriteImage(&vBytes, id, captcha.StdWidth, captcha.StdHeight) 62 | http.ServeContent(context.Writer, context.Request, id+ext, time.Time{}, bytes.NewReader(vBytes.Bytes())) 63 | } 64 | } 65 | 66 | // 校验验证码 67 | func (c *Captcha) CheckCode(context *gin.Context) { 68 | captchaIdKey := variable.ConfigYml.GetString("Captcha.captchaId") 69 | captchaValueKey := variable.ConfigYml.GetString("Captcha.captchaValue") 70 | 71 | captchaId := context.Param(captchaIdKey) 72 | value := context.Param(captchaValueKey) 73 | 74 | if captchaId == "" || value == "" { 75 | response.Fail(context, consts.CaptchaCheckParamsInvalidCode, consts.CaptchaCheckParamsInvalidMsg, "") 76 | return 77 | } 78 | if captcha.VerifyString(captchaId, value) { 79 | response.Success(context, consts.CaptchaCheckOkMsg, "") 80 | } else { 81 | response.Fail(context, consts.CaptchaCheckFailCode, consts.CaptchaCheckFailMsg, "") 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /app/utils/data_bind/formdata_to_model.go: -------------------------------------------------------------------------------- 1 | package data_bind 2 | 3 | import ( 4 | "errors" 5 | "github.com/gin-gonic/gin" 6 | "goskeleton/app/global/consts" 7 | "reflect" 8 | ) 9 | 10 | const ( 11 | modelStructMustPtr = "modelStruct 必须传递一个指针" 12 | ) 13 | 14 | // 绑定form表单验证器已经验证完成的参数到 model 结构体, 15 | // mode 结构体支持匿名嵌套 16 | // 数据绑定原则: 17 | // 1.表单参数验证器中的结构体字段 json 标签必须和 model 结构体定义的 json 标签一致 18 | // 2.model 中的数据类型与表单参数验证器数据类型保持一致: 19 | // 例如:model 中的 user_name 是 string 那么表单参数验证器中的 user_name 也必须是 string,bool 类型同理,日期时间字段在 ginskeleton 中请按照 string 处理 20 | // 3.但是 model 中的字段如果是数字类型(int、int8、int16、int64、float32、float64等)都可以绑定表单参数验证中的 float64 类型,程序会自动将原始的 float64 转换为 model 的定义的数字类型 21 | 22 | func ShouldBindFormDataToModel(c *gin.Context, modelStruct interface{}) error { 23 | mTypeOf := reflect.TypeOf(modelStruct) 24 | if mTypeOf.Kind() != reflect.Ptr { 25 | return errors.New(modelStructMustPtr) 26 | } 27 | mValueOf := reflect.ValueOf(modelStruct) 28 | 29 | //分析 modelStruct 字段 30 | mValueOfEle := mValueOf.Elem() 31 | mtf := mValueOf.Elem().Type() 32 | fieldNum := mtf.NumField() 33 | for i := 0; i < fieldNum; i++ { 34 | if !mtf.Field(i).Anonymous && mtf.Field(i).Type.Kind() != reflect.Struct { 35 | fieldSetValue(c, mValueOfEle, mtf, i) 36 | } else if mtf.Field(i).Type.Kind() == reflect.Struct { 37 | //处理结构体(有名+匿名) 38 | mValueOfEle.Field(i).Set(analysisAnonymousStruct(c, mValueOfEle.Field(i))) 39 | } 40 | } 41 | return nil 42 | } 43 | 44 | // 分析匿名结构体,并且获取匿名结构体的值 45 | func analysisAnonymousStruct(c *gin.Context, value reflect.Value) reflect.Value { 46 | 47 | typeOf := value.Type() 48 | fieldNum := typeOf.NumField() 49 | newStruct := reflect.New(typeOf) 50 | newStructElem := newStruct.Elem() 51 | for i := 0; i < fieldNum; i++ { 52 | fieldSetValue(c, newStructElem, typeOf, i) 53 | } 54 | return newStructElem 55 | } 56 | 57 | // 为结构体字段赋值 58 | func fieldSetValue(c *gin.Context, valueOf reflect.Value, typeOf reflect.Type, colIndex int) { 59 | relaKey := typeOf.Field(colIndex).Tag.Get("json") 60 | if relaKey != "-" { 61 | relaKey = consts.ValidatorPrefix + typeOf.Field(colIndex).Tag.Get("json") 62 | switch typeOf.Field(colIndex).Type.Kind() { 63 | case reflect.Int, reflect.Int16, reflect.Int32, reflect.Int64: 64 | valueOf.Field(colIndex).SetInt(int64(c.GetFloat64(relaKey))) 65 | case reflect.Float32, reflect.Float64: 66 | valueOf.Field(colIndex).SetFloat(c.GetFloat64(relaKey)) 67 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 68 | valueOf.Field(colIndex).SetUint(uint64(c.GetFloat64(relaKey))) 69 | case reflect.String: 70 | valueOf.Field(colIndex).SetString(c.GetString(relaKey)) 71 | case reflect.Bool: 72 | valueOf.Field(colIndex).SetBool(c.GetBool(relaKey)) 73 | default: 74 | // model 如果有日期时间字段,请统一设置为字符串即可 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /docs/formparams.md: -------------------------------------------------------------------------------- 1 | ### 表单参数提交介绍 2 | - 1.前端提交简单的表单参数示例代码,[请参考已有的接口测试用例文档](./api_doc.md) 3 | - 2.本篇我们将介绍复杂表单参数的提交. 4 | 5 | #### 什么是简单的表单参数提交 6 | > 1.如果接口参数都是简单的键值对,没有嵌套关系,就是简单模式. 7 | 8 | ![form-parms](https://www.ginskeleton.com/images/formparams1.png) 9 | 10 | #### 什么是复杂的表单参数提交 11 | > 1.表单参数存在嵌套关系,这种数据在 `postman` 都是以 raw 方式提交,本质上就是请求的表单参数头设置为:`Content-Type: application/json` 12 | 13 | ![form-parms](https://www.ginskeleton.com/images/formparams2.png) 14 | 15 | #### `ginskeleton` 后台处理复杂表单数据 16 | > 1.按照提交的数据格式,我们在表单参数验证器部分,定义接受的结构体,例如上图的参数我们在后台的接受参数就可以定义如下: 17 | ```code 18 | 19 | type ViewEleCreateUpdate struct { 20 | FkBigScreenView float64 `form:"fk_big_screen_view" json:"fk_big_screen_view"` 21 | EleId string `form:"ele_id" json:"ele_id"` 22 | EleIdTitle string `form:"ele_id_title" json:"ele_id_title"` 23 | Status *float64 `form:"status" json:"status"` 24 | Remark string `form:"remark" json:"remark"` 25 | ChildrenTableDelIds string `form:"children_table_del_ids" json:"children_table_del_ids"` 26 | ChildrenTable []ChildrenTable `form:"children_table" json:"children_table"` 27 | } 28 | 29 | // 大屏界面元素的子表数据 30 | // 每种元素都有三个状态(1=正常;2=禁止;3=隐藏) 31 | // 被嵌套的数据请独立定义,这样的好处就是后续可以随意精准取出任意一部分 32 | type ChildrenTable struct { 33 | Id float64 `form:"id" json:"id"` 34 | FkBigScreenViewElement float64 `form:"fk_big_screen_view_element" json:"fk_big_screen_view_element"` 35 | FkBigScreenViewElementStatusName float64 `form:"fk_big_screen_view_element_status_name" json:"fk_big_screen_view_element_status_name"` 36 | Status *float64 `form:"status" json:"status"` 37 | Remark string `form:"remark" json:"remark"` 38 | } 39 | 40 | ``` 41 | #### 接口验证器 ↓ 42 | > 1.复杂接口参数前端都是通过json格式提交. 43 | > 2.`go` 语言代码接收语法是 `context.ShouldBindJSON()` 44 | 45 | ![form-parms3](https://www.ginskeleton.com/images/formparams3.png) 46 | 47 | #### 接口验证器对应的数据类型 ↓ 48 | ![form-parms4](https://www.ginskeleton.com/images/formparams4.png) 49 | 50 | #### 在后续的控制器、model 获取子表数据 51 | ```code 52 | # 在接口验证逻辑部分,通过参数验证后,我们将子表数据已经存储在上线文 53 | 54 | // 子表数据设置一个独立的键存储 55 | extraAddBindDataContext.Set(consts.ValidatorPrefix+"children_table_del_ids", v.ChildrenTable) 56 | 57 | // 那么后续的控制器、以及model都可以根据相关的键获取原始数据、断言为我们定义的子表数据类型继续操作 58 | var childrenTableData = c.MustGet(consts.ValidatorPrefix + "children_table_del_ids") 59 | 60 | // 获取子表数据断言为我们定义的子表数据类型 61 | // 这里需要注意:验证器验证参数ok调用了控制器,如果再验证器文件没有创建独立的数据类型文件夹(包),在控制器断言会形成包的嵌套、报错,这就是我们一开始将复杂数据类型创建独立的文件件定义的原因 62 | 63 | if subTableStr, ok := childrenTableData.([]data_type_for_create_edit.ChildrenTable); ok { 64 | // 这里就相当于获取了go语言切片数据 65 | // 继续批量存储、或者挨个遍历就行 66 | // .... 省略业务逻辑 67 | } 68 | 69 | ``` 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /docs/aop.md: -------------------------------------------------------------------------------- 1 | ### 控制器 Aop 面向切面编程,优雅地模拟其他语言的动态代理方案。 2 | > 备注:真正的`Aop` 动态代理,在 `golang` 实现起来非常麻烦,尽管github有相关实现的包(https://github.com/bouk/monkey), 此包明确说明仅用于生产环境之外的测试环境,还有一部分使用非常复杂,因此本项目骨架没有引入第三方包。 3 | > 需求场景: 4 | > 1.用户删除数据,需要前置和后置回调函数,但是又不想污染控制器核心代码,此时可以考虑使用Aop思想实现。 5 | > 2.我们以调用控制器函数 `Users/Destroy` 函数为例,进行演示。 6 | 7 | #### 前置、后置回调最普通的实现方案 8 | > 此种方案,前置和后置代码比较多的时候,会造成控制器核心代码污染。 9 | ```go 10 | 11 | func (u *Users) Destroy(context *gin.Context) { 12 | 13 | // before 删除之前回调代码... 例如:判断删除数据的用户是否具备相关权限等 14 | 15 | userid := context.GetFloat64(consts.ValidatorPrefix + "id") 16 | // 根据 userid 执行删除用户数据(最核心代码) 17 | 18 | // after 删除之后回调代码... 例如 将删除的用户数据备份到相关的历史表 19 | 20 | } 21 | 22 | ``` 23 | 24 | #### 使用 Aop 思想实现前置和后置回调需求 25 | > 1.编写删除数据之前(Before)的回调函数,[示例代码](../app/aop/users/destroy_before.go) 26 | 27 | ```bash 28 | package Users 29 | 30 | import ( 31 | "goskeleton/app/global/consts" 32 | "fmt" 33 | "github.com/gin-gonic/gin" 34 | ) 35 | 36 | // 模拟Aop 实现对某个控制器函数的前置(Before)回调 37 | 38 | type destroy_before struct{} 39 | 40 | // 前置函数必须具有返回值,这样才能控制流程是否继续向下执行 41 | func (d *destroy_before) Before(context *gin.Context) bool { 42 | userId := context.GetFloat64(consts.ValidatorPrefix + "id") 43 | fmt.Printf("模拟 Users 删除操作, Before 回调,用户ID:%.f\n", userId) 44 | if userId > 10 { 45 | return true 46 | } else { 47 | return false 48 | } 49 | } 50 | 51 | ``` 52 | > 2.编写删除数据之后(After)的回调,[示例代码](../app/aop/users/destroy_after.go) 53 | 54 | ```bash 55 | 56 | package users 57 | 58 | import ( 59 | "goskeleton/app/global/consts" 60 | "fmt" 61 | "github.com/gin-gonic/gin" 62 | ) 63 | 64 | // 模拟Aop 实现对某个控制器函数的后置(After)回调 65 | 66 | type destroy_after struct{} 67 | 68 | func (d *destroy_after) After(context *gin.Context) { 69 | // 后置函数可以使用异步执行 70 | go func() { 71 | userId := context.GetFloat64(consts.ValidatorPrefix + "id") 72 | fmt.Printf("模拟 Users 删除操作, After 回调,用户ID:%.f\n", userId) 73 | }() 74 | } 75 | 76 | 77 | ``` 78 | 79 | > 3.由于本项目骨架的控制器调用都是统一由验证器启动,因此在验证器调用控制器函数的地方,使用匿名函数,直接优雅地切入前置、后置回调代码,[示例代码](../app/http/validator/web/users/destroy.go) 80 | ```go 81 | 82 | //(&Web.Users{}).Destroy(extraAddBindDataContext) // 原始方法进行如下改造 83 | 84 | // 使用匿名函数切入前置和后置回调函数 85 | func(before_callback_fn func(context *gin.Context) bool, after_callback_fn func(context *gin.Context)) { 86 | 87 | if before_callback_fn(extraAddBindDataContext) { 88 | defer after_callback_fn(extraAddBindDataContext) 89 | (&Web.Users{}).Destroy(extraAddBindDataContext) 90 | } else { 91 | // 这里编写前置函数验证不通过的相关返回提示逻辑... 92 | 93 | } 94 | }((&Users.destroy_before{}).Before, (&Users.destroy_after{}).After) 95 | 96 | // 接口请求结果展示: 97 | 模拟 Users 删除操作, Before 回调,用户ID:16 98 | 真正的控制器函数被执行,userId:16 99 | 模拟 Users 删除操作, After 回调,用户ID:16 100 | ``` 101 | 102 | 103 | -------------------------------------------------------------------------------- /docs/casbin.md: -------------------------------------------------------------------------------- 1 | ### 本篇将介绍Casbin模块的基本用法 2 | > 1.Casbin(https://github.com/casbin/casbin) 提供了一款跨语言的接口访问权限管控机制,针对go语言支持的最为全面. 3 | > 2.该模块的使用看起来非常复杂,只要理解了其核心思想,使用是非常简单易懂的. 4 | 5 | 6 | ### 前言 7 | > 1.`Casbin` 的初始化在 GinSkeleton 主线版本默认没有开启,请参照配置文件(config/config.yml)文件中 `casbin` 部分,自行决定是否开启,默认的配置项属于标准配置,基本不需要改动. 8 | > 2.配置文件开启 Casbin 模块后,默认会在连接的数据库创建一张表,具体表名参见配置文件说明. 9 | 10 | ### 根据用户请求接口时头部附带的token解析用户id等信息 11 | > 每个用户带有token的请求,在验证ok之后自动会将token绑定在上下文(gin.Context) ,绑定的键名默认为: userToken(配置文件可自行设置键名) 12 | > 通过token解析出用户id等信息的代码如下: 13 | ```code 14 | currentUser, exist := context.MustGet("userToken").(my_jwt.CustomClaims) 15 | 16 | if exist { 17 | fmt.Printf("userId:%d\n",currentUser.UserId) 18 | } 19 | 20 | ``` 21 | 22 | ### Casbin 相关的几个功能介绍 23 | > 1.Casbin 中间件,相关位置: app/http/middleware/authorization/auth.go, 中间件的作用介绍: 24 | ```code 25 | 26 | // casbin检查用户对应的角色权限是否允许访问接口 27 | func CheckCasbinAuth() gin.HandlerFunc { 28 | return func(c *gin.Context) { 29 | 30 | requstUrl := c.Request.URL.Path 31 | method := c.Request.Method 32 | 33 | // 这里根据用户请求时头部的 token 解析出用户id,根据用户id查询出该用户所拥有的角色id(roleId) 34 | // 主线版本的程序中 角色表需要开发者自行创建、管理,Ginskeleton-Admin 系统则集成了所有的基础功能 35 | // 根据角色(roleId)判断是否具有某个接口的权限 36 | roleId := "2" // 模拟最终解析出用户对应的角色为 2 37 | 38 | // 使用casbin自带的函数执行策略(规则)验证 39 | isPass, err := variable.Enforcer.Enforce(role, requstUrl, method) 40 | if err != nil { 41 | response.ErrorCasbinAuthFail(c, err.Error()) 42 | return 43 | } else if !isPass { 44 | response.ErrorCasbinAuthFail(c, "") 45 | } else { 46 | c.Next() 47 | } 48 | } 49 | } 50 | 51 | ``` 52 | 53 | ### Casbin 用法 54 | > 1.Casbin 负责检查用户请求时后台是否允许访问某个接口(路由地址),作为用户的一次请求,主要有三个要素: 55 | > 1.1 请求的地址(url) 56 | > 1.2 请求的方式(GET 、 POST 等) 57 | > 1.3 请求时用户的身份(角色Id,可以根据token解析出用户id,再根据用户id查询出对应的角色ID) 58 | > 2.Casbin会根据用户请求的三个要求匹配数据库相关设置,匹配成功方可进入路由,否则直接在中间件拦截本次请求. 59 | ```code 60 | // 【需要token】中间件验证的路由 61 | // 在某个分组或者模块,我们追加token校验完成后的具体模块接口校验机制 62 | // 追加 authorization.CheckCasbinAuth() 中间件,凡是用户访问就必须经过 token校验+casbin 接口权限校验 63 | // casbin 匹配策略时需要将用户id 转为角色id,因此必须放在 token 中间件后面(token中才能解析出用户id) 64 | backend.Use(authorization.CheckTokenAuth(), authorization.CheckCasbinAuth() ) 65 | { 66 | // 用户组路由 67 | users := backend.Group("users/") 68 | { 69 | // 查询 70 | users.GET("list", validatorFactory.Create(consts.ValidatorPrefix+"UserList")) 71 | // 新增 72 | users.POST("create", validatorFactory.Create(consts.ValidatorPrefix+"UserCreate")) 73 | // 更新 74 | users.POST("edit", validatorFactory.Create(consts.ValidatorPrefix+"UserEdit")) 75 | // 删除 76 | users.POST("destroy", validatorFactory.Create(consts.ValidatorPrefix+"UserDestroy")) 77 | 78 | } 79 | } 80 | 81 | ``` 82 | 83 | 84 | ### Casbin 核心数据表 85 | > 只要在配置文件(config/config.yml)开启Casbin相关的配置项,程序启动会默认创建一个表:tb_auth_casbin_rule ,开发者按照示例将数据写入该表即可. 86 | > 表数据的字段含义介绍请参见截图标注的文本. 87 | 88 | ![tb_casbin_rules](https://www.ginskeleton.com/images/casbin_introduce.jpg) 89 | -------------------------------------------------------------------------------- /docs/faq.md: -------------------------------------------------------------------------------- 1 | ## 常见问题汇总 2 | > 1.本篇我们将汇总使用过程中最常见的问题, 很多细小的问题或许在这里你能找到答案. 3 | 4 | ##### 2.为什么该项目 go.mod 中的模块名是 goskeleton ,但是下载下来的文件名却是 GinSkeleton ? 5 | > 本项目一开始我们命名为 ginskeleton , 包名也是这个,但是后来感觉 goskeleton 好听一点,因此改名(现在看是错了),由于版本已经更新较多,同时不影响使用,此次失误请忽略即可. 6 | 7 | ##### 3.为什么编译后的文件提示 config.yml 文件不存在 ? 8 | > 项目的编译仅限于代码部分,不包括资源部分:config 目录、public 目录、storage 目录,因此编译后的文件使用时,需要带上这个三个目录,否则程序无法正常运行. 9 | 10 | ##### 4.表单参数验证器代码部分的疑问 11 | > 示例代码位置:`app/http/validator/web/users/register.go` ,如下代码段 12 | ```code 13 | type Register struct { 14 | Base 15 | Pass string `form:"pass" json:"pass" binding:"required,min=3,max=20"` //必填,密码长度范围:【3,20】闭区间 16 | Phone string `form:"phone" json:"phone" binding:"required,len=11"` // 验证规则:必填,长度必须=11 17 | //CardNo string `form:"card_no" json:"card_no" binding:"required,len=18"` //身份证号码,必填,长度=18 18 | } 19 | 20 | // 注意这里绑定在了 Register 21 | func (r Register) CheckParams(context *gin.Context) { 22 | // ... 23 | } 24 | 25 | 26 | ``` 27 | > CheckParams 函数是否可以绑定在指针上?例如写成如下: 28 | ```code 29 | // 注意这里绑定在了 *Register 30 | func (r *Register) CheckParams(context *gin.Context) { 31 | // ... 32 | } 33 | 34 | ``` 35 | > 这里绝对不可以,因为表单参数验证器在程序启动时会自动注册在容器,每次调用都必须是一个全新的初始化代码段,如果绑定在指针,第一次请求验证通过之后,相关的参数值就会绑定容器中的代码上,造成下次请求数据污染. 36 | 37 | ##### 5.全局容器的作用是什么? 38 | ```code 39 | 本项目使用容器最多的地方: 40 | app/http/validator/common/register_validator/register_validator.go 41 | 42 | 根据key从容器调用:routers/web.go > validatorFactory.Create() 函数 ,就是根据注册时的键从容器获取代码. 43 | 44 | 目的: 45 | 1.一个请求(request)到达路由以后,需要进行表单参数的校验,如果是传统的方法,就得import相关的验证器文件包,然后掉用包中的函数,进行参数验证, 这种做法会导致路由文件的头部会出现N多的import ....包, 因为你一个接口就得一个验证器。 46 | 在这个项目骨架中,我们将验证器全部注册在容器中,路由文件头部只需要导入一个验证器的包就可以通过key调用对应的value(验证器函数)。 47 | 你可以和别人做的项目对比一下,路由文件的头部 import 部分,看看传统方式导入了是不是N个.... 48 | 49 | 2.因为验证器在项目启动时,率先注册在了容器(内存),因此调用速度也是超级快。性能极佳. 50 | 51 | ``` 52 | 53 | ##### 6.每个model都要 create 一次,难道每个 model 都是一次数据库连接吗? 54 | ```code 55 | 56 | 关系型数据库驱动库其实是根据 config.yml中的配置初始化了一次,因此每种数据库全局只有一个连接,以后每一次都是从同一个驱动指针地址,通过ping() 从底层的连接池获取一个连接。用完也是自动释放的. 57 | 看起来每一个表要初始化一次,主要是为了解决任何一个表可以随意切换到别的数据库连接,解决数据库多源场景。 58 | 每种数据库,在整个项目全局就一个数据库驱动初始化后的连接池:app/utils/sql_factory/client.go 59 | 60 | ``` 61 | 62 | ##### 7.为什么该项目强烈建议应用服务器前置nginx? 63 | ```code 64 | 65 | 1.nginx处理静态资源,几乎是无敌的,尤其是内存占用方面的管理非常完美. 66 | 2.nginx前置很方便做负载均衡. 67 | 3.nginx 的access.log、error.log 都是行业通用,可以很方便对接到 elk ,进行后续统计、分析、机器学习、报表展示等等. 68 | 4.gin 框架本身建议生产环境切换 gin 的运行模式:gin.SetMode(gin.ReleaseMode) ,该模式无接口访问日志生成,那么你的接口访问日志就必须要搭配 nginx ,同时该模式我们经过测试对比,性能再度提升 5% 69 | 70 | ``` 71 | 72 | ##### 8.本项目骨架引用的包,如何更新至最新版? 73 | > 1.本项目骨架主动引入包全部在 `go.mod` 文件,如果想自己更新至最新版,非常简单,但是必须注意:该包更新的功能兼容现有版本,如果不兼容,可能会导致封装层`app/utils/xxx` 出现错误,功能也无法正常使用. 74 | > 2.例如:gormv2 目前在用版本是 `v1.20.5`, 官方最新版本地址:https://github.com/go-gorm/gorm/tags , 最新版 : v1.20.7 75 | ```code 76 | 77 | // 1. go.mod 文件修改以下版本号至最新版 78 | gorm.io/gorm v1.20.5 ===> 79 | gorm.io/gorm v1.20.7 80 | 81 | // 在goland终端或者 go.mod 同目录执行以下命令即可 82 | go mod tidy 83 | 84 | ``` 85 | 86 | -------------------------------------------------------------------------------- /docs/zap_log.md: -------------------------------------------------------------------------------- 1 | ### 日志功能, 基于 zap + lumberjack 实现 2 | > 1.特点:高性能、极速,功能:实现日志的标准管理、日志文件的自动分隔备份. 3 | > 2.该日志在项目骨架启动时我们封装了全局变量(variable.ZapLog),直接调用即可,底层按照官方标准封装,使用者调用后不需要关闭日志,也不需要担心全局变量写日志存在并发冲突问题,底层会自动加锁后再写。 4 | > 3.相关包 github 地址:https://github.com/uber-go/zap 、 https://github.com/natefinch/lumberjack 5 | 6 | 7 | ### 前言 8 | > 1.日志相关的配置参见,config目录内的config.yml文件,Logs 部分,程序默认处于`AppDebug|调试模式`,日志输出在console面板,编译时记得切换模式。 9 | > 2.本文档列举几种最常用的用法, 想要深度学习请参考相关的 github 地址. 10 | 11 | ### 日志处理, 标准函数 12 | > 参数一:文本型 13 | > 参数二:可变参数,可传递0个或者多个 Field 类型参数,Field 类型传递规则参见下文 14 | ```code 15 | > 1. Debug(参数一, 参数二) , 调试级别,会产生大量日志,只在开发模式(AppDebug=true)会输出日志打印在console面板,生产模式该函数禁用。 16 | > 2. Info(参数一, 参数二) , 一般信息,默认级别。 17 | > 3. Warn(参数一, 参数二) , 警告 18 | > 4. Panic(参数一, 参数二)、Dpanic(参数一, 参数二) , 恐慌、宕机,不建议使用 19 | > 5. Error(参数一, 参数二) , 错误 20 | > 6. Fatal(参数一, 参数二) , 致命错误,会导致程序进程退出。 21 | ``` 22 | 23 | ### 标准函数的参数二 Field 类型,最常用传递方式 24 | > 1.Int 类型 : zap.Int("userID",2019) , 同类的还有 int16 、 int32等 25 | > 2.String 类型 : zap.String("userID","2019") 26 | > 3.Error 类型 : zap.Error(v_err) , v_err 为 error(错误类型),例如使用 v_err:= error.New("模拟一个错误") 27 | > 4.Bool 类型 : zap.Bool("is_ok",true) 28 | 29 | 30 | #### 用法 1 , 高性能模式 . 31 | > 1.举例展示最常用用法 32 | ```code 33 | variable.ZapLog.Info("基本的运行提示类信息") 34 | variable.ZapLog.Warn("UserCreate接口参数非法警告,相关参数:",zap.String("userName","demo_name"),zap.Int("userAge",18)) 35 | variable.ZapLog.Panic("UserDestory接口参数异常,相关参数:",zap.String("userName","demo_name"),zap.String("password","pass123456") 36 | variable.ZapLog.Error("UserDestory接口参数错误,相关参数:",zap.Error(error)) 37 | variable.ZapLog.Fatal("Mysql初始化参数错误,退出运行。相关参数:",zap.String("name","root"), zap.Int("端口",3306)) 38 | 39 | ``` 40 | 41 | #### 用法2 , 语法糖模式 . 42 | > 1.比第一种用法性能稍低,只不过基于第一种用法,相关的函数全部增加了格式化参数功能 43 | ```code 44 | # 第一种的函数后面全部添加了一个 w ,相关的函数功能和第一种一模一样 45 | variable.ZapLog.Sugar().Infow("基本的运行提示类信息",zap.String("name","root")) 46 | 47 | # 格式化参数,第一种用法中的函数后面添加了一个 f 48 | variable.ZapLog.Sugar().Infof("参数 userId %d\n",2020) 49 | 50 | variable.ZapLog.Sugar().Errorw("程序发生错误",zap.Error(error)) 51 | variable.ZapLog.Sugar().Errorf("参数非法,程序出错,userId %d\n",2020) 52 | 53 | Warn 、 Panic 、Fatal用法类似 54 | 55 | ``` 56 | 57 | #### 日志钩子 58 | > 1.除了本项目骨架记录日志之外,您还可以对日志进行二次加工处理. 59 | > 2.日志钩子函数处理位置 > `app/service/sys_log_hook/zap_log_hooks.go` 60 | > 3.`bootStrap/init.go` 中你可以修改钩子函数的位置 61 | > 相关代码位置 `app/service/sys_log_hook/zap_log_hooks.go ` 62 | ```code 63 | func ZapLogHandler(entry zapcore.Entry) error { 64 | 65 | // 参数 entry 介绍 66 | // entry 参数就是单条日志结构体,主要包括字段如下: 67 | //Level 日志等级 68 | //Time 当前时间 69 | //LoggerName 日志名称 70 | //Message 日志内容 71 | //Caller 各个文件调用路径 72 | //Stack 代码调用栈 73 | 74 | //这里启动一个协程,hook丝毫不会影响程序性能, 75 | go func(paramEntry zapcore.Entry) { 76 | //fmt.Println(" GoSkeleton hook ....,你可以在这里继续处理系统日志....") 77 | //fmt.Printf("%#+v\n", paramEntry) 78 | }(entry) 79 | return nil 80 | } 81 | 82 | ``` -------------------------------------------------------------------------------- /app/utils/rabbitmq/topics/producer.go: -------------------------------------------------------------------------------- 1 | package topics 2 | 3 | import ( 4 | amqp "github.com/rabbitmq/amqp091-go" 5 | "goskeleton/app/global/variable" 6 | "goskeleton/app/utils/rabbitmq/error_record" 7 | ) 8 | 9 | // CreateProducer 创建一个生产者 10 | func CreateProducer(options ...OptionsProd) (*producer, error) { 11 | // 获取配置信息 12 | conn, err := amqp.Dial(variable.ConfigYml.GetString("RabbitMq.Topics.Addr")) 13 | exchangeType := variable.ConfigYml.GetString("RabbitMq.Topics.ExchangeType") 14 | exchangeName := variable.ConfigYml.GetString("RabbitMq.Topics.ExchangeName") 15 | queueName := variable.ConfigYml.GetString("RabbitMq.Topics.QueueName") 16 | durable := variable.ConfigYml.GetBool("RabbitMq.Topics.Durable") 17 | 18 | if err != nil { 19 | variable.ZapLog.Error(err.Error()) 20 | return nil, err 21 | } 22 | 23 | prod := &producer{ 24 | connect: conn, 25 | exchangeType: exchangeType, 26 | exchangeName: exchangeName, 27 | queueName: queueName, 28 | durable: durable, 29 | } 30 | // 加载用户设置的参数 31 | for _, val := range options { 32 | val.apply(prod) 33 | } 34 | return prod, nil 35 | } 36 | 37 | // 定义一个消息队列结构体:Topics 模型 38 | type producer struct { 39 | connect *amqp.Connection 40 | exchangeType string 41 | exchangeName string 42 | queueName string 43 | durable bool 44 | occurError error 45 | enableDelayMsgPlugin bool // 是否使用延迟队列模式 46 | args amqp.Table 47 | } 48 | 49 | // Send 发送消息 50 | // 参数: 51 | // routeKey 路由键、 52 | // data 发送的数据、 53 | // delayMillisecond 延迟时间(毫秒),只有启用了消息延迟插件才有效果 54 | func (p *producer) Send(routeKey, data string, delayMillisecond int) bool { 55 | 56 | // 获取一个频道 57 | ch, err := p.connect.Channel() 58 | p.occurError = error_record.ErrorDeal(err) 59 | defer func() { 60 | _ = ch.Close() 61 | }() 62 | 63 | // 声明交换机,该模式生产者只负责将消息投递到交换机即可 64 | err = ch.ExchangeDeclare( 65 | p.exchangeName, //交换器名称 66 | p.exchangeType, //topic模式 67 | p.durable, //交换机是否持久化 68 | !p.durable, //交换器是否自动删除 69 | false, 70 | false, 71 | p.args, 72 | ) 73 | p.occurError = error_record.ErrorDeal(err) 74 | 75 | // 如果交换机是持久化的,那么消息也设置为持久化 76 | msgPersistent := amqp.Transient 77 | if p.durable { 78 | msgPersistent = amqp.Persistent 79 | } 80 | // 投递消息 81 | if err == nil { 82 | err = ch.Publish( 83 | p.exchangeName, // 交换机名称 84 | routeKey, // topics 模式默认为空即可 85 | false, 86 | false, 87 | amqp.Publishing{ 88 | DeliveryMode: msgPersistent, //消息是否持久化,这里与保持保持一致即可 89 | ContentType: "text/plain", 90 | Body: []byte(data), 91 | Headers: amqp.Table{ 92 | "x-delay": delayMillisecond, // 延迟时间: 毫秒 93 | }, 94 | }) 95 | } 96 | p.occurError = error_record.ErrorDeal(err) 97 | if p.occurError != nil { // 发生错误,返回 false 98 | return false 99 | } else { 100 | return true 101 | } 102 | } 103 | 104 | // Close 发送完毕手动关闭,这样不影响send多次发送数据 105 | func (p *producer) Close() { 106 | _ = p.connect.Close() 107 | } 108 | -------------------------------------------------------------------------------- /app/utils/rabbitmq/routing/producer.go: -------------------------------------------------------------------------------- 1 | package routing 2 | 3 | import ( 4 | amqp "github.com/rabbitmq/amqp091-go" 5 | "goskeleton/app/global/variable" 6 | "goskeleton/app/utils/rabbitmq/error_record" 7 | ) 8 | 9 | // CreateProducer 创建一个生产者 10 | func CreateProducer(options ...OptionsProd) (*producer, error) { 11 | // 获取配置信息 12 | conn, err := amqp.Dial(variable.ConfigYml.GetString("RabbitMq.Routing.Addr")) 13 | exchangeType := variable.ConfigYml.GetString("RabbitMq.Routing.ExchangeType") 14 | exchangeName := variable.ConfigYml.GetString("RabbitMq.Routing.ExchangeName") 15 | queueName := variable.ConfigYml.GetString("RabbitMq.Routing.QueueName") 16 | durable := variable.ConfigYml.GetBool("RabbitMq.Routing.Durable") 17 | 18 | if err != nil { 19 | variable.ZapLog.Error(err.Error()) 20 | return nil, err 21 | } 22 | 23 | prod := &producer{ 24 | connect: conn, 25 | exchangeType: exchangeType, 26 | exchangeName: exchangeName, 27 | queueName: queueName, 28 | durable: durable, 29 | } 30 | // 加载用户设置的参数 31 | for _, val := range options { 32 | val.apply(prod) 33 | } 34 | return prod, nil 35 | } 36 | 37 | // 定义一个消息队列结构体:Routing 模型 38 | type producer struct { 39 | connect *amqp.Connection 40 | exchangeType string 41 | exchangeName string 42 | queueName string 43 | durable bool 44 | occurError error 45 | enableDelayMsgPlugin bool // 是否使用延迟队列模式 46 | args amqp.Table 47 | } 48 | 49 | // Send 发送消息 50 | // 参数: 51 | // routeKey 路由键、 52 | // data 发送的数据、 53 | // delayMillisecond 延迟时间(毫秒),只有启用了消息延迟插件才有效果 54 | func (p *producer) Send(routeKey, data string, delayMillisecond int) bool { 55 | 56 | // 获取一个频道 57 | ch, err := p.connect.Channel() 58 | p.occurError = error_record.ErrorDeal(err) 59 | defer func() { 60 | _ = ch.Close() 61 | }() 62 | 63 | // 声明交换机,该模式生产者只负责将消息投递到交换机即可 64 | err = ch.ExchangeDeclare( 65 | p.exchangeName, //交换器名称 66 | p.exchangeType, //direct(定向消息), 按照路由键名匹配消息 67 | p.durable, //消息是否持久化 68 | !p.durable, //交换器是否自动删除 69 | false, 70 | false, 71 | p.args, 72 | ) 73 | p.occurError = error_record.ErrorDeal(err) 74 | 75 | // 如果队列的声明是持久化的,那么消息也设置为持久化 76 | msgPersistent := amqp.Transient 77 | if p.durable { 78 | msgPersistent = amqp.Persistent 79 | } 80 | // 投递消息 81 | if err == nil { 82 | err = ch.Publish( 83 | p.exchangeName, // 交换机名称 84 | routeKey, // direct 模式默认为空即可 85 | false, 86 | false, 87 | amqp.Publishing{ 88 | DeliveryMode: msgPersistent, //消息是否持久化,这里与保持保持一致即可 89 | ContentType: "text/plain", 90 | Body: []byte(data), 91 | Headers: amqp.Table{ 92 | "x-delay": delayMillisecond, // 延迟时间: 毫秒 93 | }, 94 | }) 95 | } 96 | p.occurError = error_record.ErrorDeal(err) 97 | if p.occurError != nil { // 发生错误,返回 false 98 | return false 99 | } else { 100 | return true 101 | } 102 | } 103 | 104 | // Close 发送完毕手动关闭,这样不影响send多次发送数据 105 | func (p *producer) Close() { 106 | _ = p.connect.Close() 107 | } 108 | -------------------------------------------------------------------------------- /docs/websocket.md: -------------------------------------------------------------------------------- 1 | ### websocket 2 | 3 | ##### 1.基本用法 4 | > 以下代码展示的是每一个 websocket 客户端连接到服务端所拥有的功能 5 | - [相关代码位置](../app/service/websocket/ws.go) 6 | ```code 7 | package websocket 8 | 9 | import ( 10 | "fmt" 11 | "github.com/gin-gonic/gin" 12 | "github.com/gorilla/websocket" 13 | "go.uber.org/zap" 14 | "goskeleton/app/global/my_errors" 15 | "goskeleton/app/global/variable" 16 | "goskeleton/app/utils/websocket/core" 17 | ) 18 | 19 | /** 20 | websocket模块相关事件执行顺序: 21 | 1.onOpen 22 | 2.OnMessage 23 | 3.OnError 24 | 4.OnClose 25 | */ 26 | 27 | type Ws struct { 28 | WsClient *core.Client 29 | } 30 | 31 | // onOpen 基本不需要做什么 32 | func (w *Ws) OnOpen(context *gin.Context) (*Ws, bool) { 33 | if client, ok := (&core.Client{}).OnOpen(context); ok { 34 | w.WsClient = client 35 | go w.WsClient.Heartbeat() // 一旦握手+协议升级成功,就为每一个连接开启一个自动化的隐式心跳检测包 36 | return w, true 37 | } else { 38 | return nil, false 39 | } 40 | } 41 | 42 | // OnMessage 处理业务消息 43 | func (w *Ws) OnMessage(context *gin.Context) { 44 | go w.WsClient.ReadPump(func(messageType int, receivedData []byte) { 45 | //参数说明 46 | //messageType 消息类型,1=文本 47 | //receivedData 服务器接收到客户端(例如js客户端)发来的的数据,[]byte 格式 48 | 49 | tempMsg := "服务器已经收到了你的消息==>" + string(receivedData) 50 | // 回复客户端已经收到消息; 51 | if err := w.WsClient.SendMessage(messageType, tempMsg); err != nil { 52 | variable.ZapLog.Error("消息发送出现错误", zap.Error(err)) 53 | } 54 | 55 | }, w.OnError, w.OnClose) 56 | } 57 | 58 | // OnError 客户端与服务端在消息交互过程中发生错误回调函数 59 | func (w *Ws) OnError(err error) { 60 | w.WsClient.State = 0 // 发生错误,状态设置为0, 心跳检测协程则自动退出 61 | variable.ZapLog.Error("远端掉线、卡死、刷新浏览器等会触发该错误:", zap.Error(err)) 62 | //fmt.Printf("远端掉线、卡死、刷新浏览器等会触发该错误: %v\n", err.Error()) 63 | } 64 | 65 | // OnClose 客户端关闭回调,发生onError回调以后会继续回调该函数 66 | func (w *Ws) OnClose() { 67 | 68 | w.WsClient.Hub.UnRegister <- w.WsClient // 向hub管道投递一条注销消息,由hub中心负责关闭连接、删除在线数据 69 | } 70 | 71 | //获取在线的全部客户端 72 | func (w *Ws) GetOnlineClients() { 73 | 74 | fmt.Printf("在线客户端数量:%d\n", len(w.WsClient.Hub.Clients)) 75 | } 76 | 77 | // (每一个客户端都有能力)向全部在线客户端广播消息 78 | func (w *Ws) BroadcastMsg(sendMsg string) { 79 | for onlineClient := range w.WsClient.Hub.Clients { 80 | 81 | //获取每一个在线的客户端,向远端发送消息 82 | if err := onlineClient.SendMessage(websocket.TextMessage, sendMsg); err != nil { 83 | variable.ZapLog.Error(my_errors.ErrorsWebsocketWriteMgsFail, zap.Error(err)) 84 | } 85 | } 86 | } 87 | 88 | 89 | ``` 90 | 91 | 92 | ##### 2.在本项目骨架任意位置,向所有在线的 websocet 客户端广播消息 93 | > 核心原理:每一个 websocket 客户端都有一个 Hub 结构体,而这个结构体是本项目骨架设置的全局值,因此在任意位置创建一个 websocket 客户端,只要将 Hub 值赋予全局初始化的:variable.WebsocketHub,就可以在任意位置进行广播消息. 94 | ```code 95 | package demo1 96 | 97 | import ( 98 | serviceWs "goskeleton/app/service/websocket" 99 | ) 100 | 101 | // 省略其他无关代码,相关的核心代码如下 102 | 103 | if WsHub, ok := variable.WebsocketHub.(*core.Hub); ok { 104 | // serviceWs 为 app/service/websocket 的别名 105 | ws := serviceWs.Ws{WsClient: &core.Client{Hub: WsHub}} 106 | ws.BroadcastMsg("本项目骨架任意位置,使用本段代码对在线的 ws 客户端广播消息") 107 | } 108 | 109 | ``` -------------------------------------------------------------------------------- /docs/global_variable.md: -------------------------------------------------------------------------------- 1 | ## 项目中被初始化的全局变量清单介绍 2 | 3 | ### 1.前言 4 | > 1.程序启动时初始化动作统一由 `bootstrap/init.go` 文件中的代码段负责,本次我们将介绍3个常用的全局变量. 5 | > 2.全局变量只会使用法简洁化, 不对原始语法造成任何破坏, 封装全局变量时我们经过谨慎地评估、测试相关代码段、从而保证并发安全性. 6 | 7 | ### 2.gorm 全局变量 8 | > 1.请按照配置文件 `congfig/gorm_v2.yml` 中的提示正确配置数据库,开启程序启动初始化数据库参数,程序在启动时会自动为您初始化全局变量. 9 | > 2.不同类型的数据库全局变量名不一样, 对照关系参见以下代码段说明. 10 | > 3.更多用法参见单元测试:[gorm_v2单元测试](../test/gormv2_test.go). 11 | > 4.本文档我们主要介绍 gorm 全局变量初始化的核心. 12 | ```code 13 | 14 | // 例如:原始语法,我们以 mysql 驱动的初始化为例进行说明 15 | // 1.连接数据库,获取mysql连接 16 | dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local" 17 | mysqlDb, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) 18 | 19 | // 2.查询 20 | db.Select("id", "name", "phone", "email", "remark").Where("name like ?", "%test%").Find(&users) 21 | 22 | 23 | // 本项目中, `variable.GormDbMysql` 完全等于上文中返回的 mysqlDb 24 | variable.GormDbMysql.Select("id", "name", "phone", "email", "remark").Where("name like ?", "%test%").Find(&users) 25 | 26 | // gorm 数据库驱动与本项目骨架对照关系 27 | variable.GormDbMysql <====完全等于==> gorm.Open(mysql.Open(dsn), &gorm.Config{}) 28 | variable.GormDbSqlserver <====完全等于==> gorm.Open(sqlserver.Open(dsn), &gorm.Config{}) 29 | variable.GormDbPostgreSql <====完全等于==> gorm.Open(postgres.Open(dsn), &gorm.Config{}) 30 | ``` 31 | 32 | 33 | ### 3.日志全局变量 34 | > 1.为了随意、方便地记录项目中日志,我们封装了全局变量 `variable.ZapLog` . 35 | > 2.由于日志操作内容比较多,我们对它进行了单独介绍,详情参见: [zap高性能日志](zap_log.md) 36 | 37 | 38 | ### 4.配置文件全局变量 39 | > 1.为了更方便地操作配置文件 `config/config.yml` 、 `config/gorm_v2.yml` 我们同样在项目启动时封装了全局变量. 40 | > 2.`variable.ConfigYml` ,该变量相当于配置文件 `config/config.yml` 文件打开后的指针. 41 | > 3.`variable.ConfigGormv2Yml` ,该变量相当于配置文件 `config/gorm_v2.yml` 文件打开后的指针. 42 | > 4.在任何地方您都可以使用以上全局变量直接获取对应配置文件的 键==>值. 43 | ```code 44 | 45 | // 获取 config/config.yml 文件中 Websocket.Start 对应的 Int 值 46 | variable.ConfigYml.GetInt("Websocket.Start") 47 | 48 | // 获取 config/gorm_v2.yml 文件中 Gormv2.Mysql.IsInitGlobalGormMysql 对应的 Int 值 49 | variable.ConfigGormv2Yml.GetInt("Gormv2.Mysql.IsInitGlobalGormMysql") 50 | 51 | ``` 52 | > 5.获取配置文件中键对应的值数据类型,函数清单,您可以使用 `variable.ConfigYml.` 或者 `variable.ConfigGormv2Yml.` 以下函数名 获取值 53 | ```code 54 | // 开发者常用函数 55 | GetString(keyName string) string 56 | GetInt(keyName string) int 57 | GetInt32(keyName string) int32 58 | GetInt64(keyName string) int64 59 | GetFloat64(keyName string) float64 60 | GetDuration(keyName string) time.Duration 61 | GetBool(keyName string) bool 62 | 63 | // 非常用函数,主要是项目骨架在使用 64 | ConfigFileChangeListen() 65 | Clone(fileName string) YmlConfigInterf 66 | Get(keyName string) interface{} // 该函数获取一个 键 对应的原始值,因此返回类型为 interface , 基本很少用 67 | GetStringSlice(keyName string) []string 68 | ``` 69 | 70 | ### 5.雪花算法(snowflake)生成分布式场景唯一ID 71 | > 1.相关配置 ` config>config.yml` 配置项 `SnowFlakeMachineId` , 如果本项目同时部署在多台机器,并且需要同时使用该算法,请为每一台机器设置不同的ID,区间范围: [0,1023] 72 | > 2.随时随地,您可以非常方便的获取一个分布式场景的唯一ID 73 | > 3.更多详情参见: [SnowFlake单元测试](../test/snowflake_test.go) 74 | ```code 75 | 76 | # 雪花算法生成的全局唯一ID数据类型为 int64 77 | variable.SnowFlake.GetId() 78 | 79 | ``` 80 | -------------------------------------------------------------------------------- /app/utils/rabbitmq/publish_subscribe/producer.go: -------------------------------------------------------------------------------- 1 | package publish_subscribe 2 | 3 | import ( 4 | amqp "github.com/rabbitmq/amqp091-go" 5 | "goskeleton/app/global/variable" 6 | "goskeleton/app/utils/rabbitmq/error_record" 7 | ) 8 | 9 | // CreateProducer 创建一个生产者 10 | func CreateProducer(options ...OptionsProd) (*producer, error) { 11 | // 获取配置信息 12 | conn, err := amqp.Dial(variable.ConfigYml.GetString("RabbitMq.PublishSubscribe.Addr")) 13 | exchangeType := variable.ConfigYml.GetString("RabbitMq.PublishSubscribe.ExchangeType") 14 | exchangeName := variable.ConfigYml.GetString("RabbitMq.PublishSubscribe.ExchangeName") 15 | queueName := variable.ConfigYml.GetString("RabbitMq.PublishSubscribe.QueueName") 16 | durable := variable.ConfigYml.GetBool("RabbitMq.PublishSubscribe.Durable") 17 | 18 | if err != nil { 19 | variable.ZapLog.Error(err.Error()) 20 | return nil, err 21 | } 22 | 23 | prod := &producer{ 24 | connect: conn, 25 | exchangeType: exchangeType, 26 | exchangeName: exchangeName, 27 | queueName: queueName, 28 | durable: durable, 29 | args: nil, 30 | } 31 | // 加载用户设置的参数 32 | for _, val := range options { 33 | val.apply(prod) 34 | } 35 | return prod, nil 36 | } 37 | 38 | // 定义一个消息队列结构体:PublishSubscribe 模型 39 | type producer struct { 40 | connect *amqp.Connection 41 | exchangeType string 42 | exchangeName string 43 | queueName string 44 | durable bool 45 | occurError error 46 | enableDelayMsgPlugin bool 47 | args amqp.Table 48 | } 49 | 50 | // Send 发送消息 51 | // 参数: 52 | // data 发送的数据、 53 | // delayMillisecond 延迟时间(毫秒),只有启用了消息延迟插件才有效果 54 | func (p *producer) Send(data string, delayMillisecond int) bool { 55 | 56 | // 获取一个频道 57 | ch, err := p.connect.Channel() 58 | p.occurError = error_record.ErrorDeal(err) 59 | defer func() { 60 | _ = ch.Close() 61 | }() 62 | 63 | // 声明交换机,该模式生产者只负责将消息投递到交换机即可 64 | err = ch.ExchangeDeclare( 65 | p.exchangeName, //交换器名称 66 | p.exchangeType, //fanout 模式(扇形模式,发布/订阅 模式) ,解决 发布、订阅场景相关的问题 67 | p.durable, //durable 68 | !p.durable, //autodelete 69 | false, 70 | false, 71 | p.args, 72 | ) 73 | p.occurError = error_record.ErrorDeal(err) 74 | 75 | // 如果队列的声明是持久化的,那么消息也设置为持久化 76 | msgPersistent := amqp.Transient 77 | if p.durable { 78 | msgPersistent = amqp.Persistent 79 | } 80 | // 投递消息 81 | if err == nil { 82 | err = ch.Publish( 83 | p.exchangeName, // 交换机名称 84 | p.queueName, // fanout 模式默认为空,表示所有订阅的消费者会接受到相同的消息 85 | false, 86 | false, 87 | amqp.Publishing{ 88 | DeliveryMode: msgPersistent, //消息是否持久化,这里与保持保持一致即可 89 | ContentType: "text/plain", 90 | Body: []byte(data), 91 | Headers: amqp.Table{ 92 | "x-delay": delayMillisecond, // 延迟时间: 毫秒 93 | }, 94 | }) 95 | } 96 | 97 | p.occurError = error_record.ErrorDeal(err) 98 | if p.occurError != nil { // 发生错误,返回 false 99 | return false 100 | } else { 101 | return true 102 | } 103 | } 104 | 105 | // Close 发送完毕手动关闭,这样不影响send多次发送数据 106 | func (p *producer) Close() { 107 | _ = p.connect.Close() 108 | } 109 | -------------------------------------------------------------------------------- /config/gorm_v2.yml: -------------------------------------------------------------------------------- 1 | Gormv2: # 只针对 gorm 操作数据库有效 2 | UseDbType: "mysql" # 备选项 mysql 、sqlserver、 postgresql 3 | Mysql: 4 | IsInitGlobalGormMysql: 0 # 随项目启动为gorm db初始化一个全局 variable.GormDbMysql(完全等于*gorm.Db),正确配置数据库,该值必须设置为: 1 5 | SlowThreshold: 30 # 慢 SQL 阈值(sql执行时间超过此时间单位(秒),就会触发系统日志记录) 6 | Write: 7 | Host: "127.0.0.1" 8 | DataBase: "db_goskeleton" 9 | Port: 3306 10 | Prefix: "tb_" # 目前没有用到该配置项 11 | User: "root" 12 | Pass: "DRsXT5ZJ6Oi55LPQ" 13 | Charset: "utf8" 14 | SetMaxIdleConns: 10 15 | SetMaxOpenConns: 128 16 | SetConnMaxLifetime: 60 # 连接不活动时的最大生存时间(秒) 17 | #ReConnectInterval: 1 # 保留项,重连数据库间隔秒数 18 | #PingFailRetryTimes: 3 # 保留项,最大重连次数 19 | IsOpenReadDb: 0 # 是否开启读写分离配置(1=开启、0=关闭),IsOpenReadDb=1,Read 部分参数有效,否则Read部分参数直接忽略 20 | Read: 21 | Host: "127.0.0.1" 22 | DataBase: "db_goskeleton" 23 | Port: 3308 #注意,非3306,请自行调整 24 | Prefix: "tb_" 25 | User: "root" 26 | Pass: "yourPassword" 27 | Charset: "utf8" 28 | SetMaxIdleConns: 10 29 | SetMaxOpenConns: 128 30 | SetConnMaxLifetime: 60 31 | # 如果要使用sqlserver数据库,请在 app/model 目录,将 users_for_sqlserver.txt 的内容直接覆盖同目录的 users.go 即可 32 | SqlServer: 33 | # 随项目启动为gorm db初始化一个全局 variable.GormDbMysql(完全等于*gorm.Db),正确配置数据库,该值必须设置为: 1 34 | # 此外,开启 sqlserver 数据库时,请在 app/model/users_for_sqlserver.txt 文件中,按照说明手动替换一下代码 35 | IsInitGlobalGormSqlserver: 0 36 | SlowThreshold: 30 37 | Write: 38 | Host: "127.0.0.1" 39 | DataBase: "db_goskeleton" 40 | Port: 1433 41 | Prefix: "tb_" 42 | User: "Sa" 43 | Pass: "secret2017" 44 | #ReConnectInterval: 1 # 保留项,重连数据库间隔秒数 45 | #PingFailRetryTimes: 3 # 保留项,最大重连次数 46 | SetMaxIdleConns: 10 47 | SetMaxOpenConns: 128 48 | SetConnMaxLifetime: 60 49 | IsOpenReadDb: 0 # 是否开启读写分离配置(1=开启、0=关闭),IsOpenReadDb=1,Read 部分参数有效,否则Read部分参数直接忽略 50 | Read: 51 | Host: "127.0.0.1" 52 | DataBase: "db_goskeleton" 53 | Port: 1433 54 | Prefix: "tb_" 55 | User: "Sa" 56 | Pass: "secret2017" 57 | SetMaxIdleConns: 10 58 | SetMaxOpenConns: 128 59 | SetConnMaxLifetime: 60 60 | # 如果要使用postgresql数据库,请在 app/model 目录,将 users_for_postgres.txt 的内容直接覆盖同目录的 users.go 即可 61 | PostgreSql: 62 | IsInitGlobalGormPostgreSql: 0 # 随项目启动为gorm db初始化一个全局 variable.GormDbMysql(完全等于*gorm.Db),正确配置数据库,该值必须设置为: 1 63 | SlowThreshold: 30 64 | Write: 65 | Host: "127.0.0.1" 66 | DataBase: "db_goskeleton" 67 | Port: 5432 68 | Prefix: "tb_" 69 | User: "postgres" 70 | Pass: "Secret2017~" 71 | SetMaxIdleConns: 10 72 | SetMaxOpenConns: 128 73 | SetConnMaxLifetime: 60 74 | #ReConnectInterval: 1 # 保留项,重连数据库间隔秒数 75 | #PingFailRetryTimes: 3 # 保留项,最大重连次数 76 | IsOpenReadDb: 0 # 是否开启读写分离配置(1=开启、0=关闭),IsOpenReadDb=1,Read 部分参数有效,否则Read部分参数直接忽略 77 | Read: 78 | Host: "127.0.0.1" 79 | DataBase: "db_goskeleton" 80 | Port: 5432 81 | Prefix: "tb_" 82 | User: "postgres" 83 | Pass: "secret2017" 84 | SetMaxIdleConns: 10 85 | SetMaxOpenConns: 128 86 | SetConnMaxLifetime: 60 87 | -------------------------------------------------------------------------------- /app/global/consts/consts.go: -------------------------------------------------------------------------------- 1 | package consts 2 | 3 | // 这里定义的常量,一般是具有错误代码+错误说明组成,一般用于接口返回 4 | const ( 5 | // 进程被结束 6 | ProcessKilled string = "收到信号,进程被结束" 7 | // 表单验证器前缀 8 | ValidatorPrefix string = "Form_Validator_" 9 | ValidatorParamsCheckFailCode int = -400300 10 | ValidatorParamsCheckFailMsg string = "参数校验失败" 11 | 12 | //服务器代码发生错误 13 | ServerOccurredErrorCode int = -500100 14 | ServerOccurredErrorMsg string = "服务器内部发生代码执行错误, " 15 | GinSetTrustProxyError string = "Gin 设置信任代理服务器出错" 16 | 17 | // token相关 18 | JwtTokenOK int = 200100 //token有效 19 | JwtTokenInvalid int = -400100 //无效的token 20 | JwtTokenExpired int = -400101 //过期的token 21 | JwtTokenFormatErrCode int = -400102 //提交的 token 格式错误 22 | JwtTokenFormatErrMsg string = "提交的 token 格式错误" //提交的 token 格式错误 23 | JwtTokenMustValid string = "token为必填项,请在请求header部分提交!" //提交的 token 格式错误 24 | 25 | //SnowFlake 雪花算法 26 | StartTimeStamp = int64(1483228800000) //开始时间截 (2017-01-01) 27 | MachineIdBits = uint(10) //机器id所占的位数 28 | SequenceBits = uint(12) //序列所占的位数 29 | //MachineIdMax = int64(-1 ^ (-1 << MachineIdBits)) //支持的最大机器id数量 30 | SequenceMask = int64(-1 ^ (-1 << SequenceBits)) // 31 | MachineIdShift = SequenceBits //机器id左移位数 32 | TimestampShift = SequenceBits + MachineIdBits //时间戳左移位数 33 | 34 | // CURD 常用业务状态码 35 | CurdStatusOkCode int = 200 36 | CurdStatusOkMsg string = "Success" 37 | CurdCreatFailCode int = -400200 38 | CurdCreatFailMsg string = "新增失败" 39 | CurdUpdateFailCode int = -400201 40 | CurdUpdateFailMsg string = "更新失败" 41 | CurdDeleteFailCode int = -400202 42 | CurdDeleteFailMsg string = "删除失败" 43 | CurdSelectFailCode int = -400203 44 | CurdSelectFailMsg string = "查询无数据" 45 | CurdRegisterFailCode int = -400204 46 | CurdRegisterFailMsg string = "注册失败" 47 | CurdLoginFailCode int = -400205 48 | CurdLoginFailMsg string = "登录失败" 49 | CurdRefreshTokenFailCode int = -400206 50 | CurdRefreshTokenFailMsg string = "刷新Token失败" 51 | 52 | //文件上传 53 | FilesUploadFailCode int = -400250 54 | FilesUploadFailMsg string = "文件上传失败, 获取上传文件发生错误!" 55 | FilesUploadMoreThanMaxSizeCode int = -400251 56 | FilesUploadMoreThanMaxSizeMsg string = "长传文件超过系统设定的最大值,系统允许的最大值:" 57 | FilesUploadMimeTypeFailCode int = -400252 58 | FilesUploadMimeTypeFailMsg string = "文件mime类型不允许" 59 | 60 | //websocket 61 | WsServerNotStartCode int = -400300 62 | WsServerNotStartMsg string = "websocket 服务没有开启,请在配置文件开启,相关路径:config/config.yml" 63 | WsOpenFailCode int = -400301 64 | WsOpenFailMsg string = "websocket open阶段初始化基本参数失败" 65 | 66 | //验证码 67 | CaptchaGetParamsInvalidMsg string = "获取验证码:提交的验证码参数无效,请检查验证码ID以及文件名后缀是否完整" 68 | CaptchaGetParamsInvalidCode int = -400350 69 | CaptchaCheckParamsInvalidMsg string = "校验验证码:提交的参数无效,请检查 【验证码ID、验证码值】 提交时的键名是否与配置项一致" 70 | CaptchaCheckParamsInvalidCode int = -400351 71 | CaptchaCheckOkMsg string = "验证码校验通过" 72 | CaptchaCheckFailCode int = -400355 73 | CaptchaCheckFailMsg string = "验证码校验失败" 74 | ) 75 | -------------------------------------------------------------------------------- /app/service/users/token_cache_redis/user_token_cache_redis.go: -------------------------------------------------------------------------------- 1 | package token_cache_redis 2 | 3 | import ( 4 | "go.uber.org/zap" 5 | "goskeleton/app/global/variable" 6 | "goskeleton/app/utils/md5_encrypt" 7 | "goskeleton/app/utils/redis_factory" 8 | "strconv" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | func CreateUsersTokenCacheFactory(userId int64) *userTokenCacheRedis { 14 | redCli := redis_factory.GetOneRedisClient() 15 | if redCli == nil { 16 | return nil 17 | } 18 | return &userTokenCacheRedis{redisClient: redCli, userTokenKey: "token_userid_" + strconv.FormatInt(userId, 10)} 19 | } 20 | 21 | type userTokenCacheRedis struct { 22 | redisClient *redis_factory.RedisClient 23 | userTokenKey string 24 | } 25 | 26 | // SetTokenCache 设置缓存 27 | func (u *userTokenCacheRedis) SetTokenCache(tokenExpire int64, token string) bool { 28 | // 存储用户token时转为MD5,下一步比较的时候可以更加快速地比较是否一致 29 | if _, err := u.redisClient.Int(u.redisClient.Execute("zAdd", u.userTokenKey, tokenExpire, md5_encrypt.MD5(token))); err == nil { 30 | return true 31 | } else { 32 | variable.ZapLog.Error("缓存用户token到redis出错", zap.Error(err)) 33 | } 34 | return false 35 | } 36 | 37 | // DelOverMaxOnlineCache 删除缓存,删除超过系统允许最大在线数量之外的用户 38 | func (u *userTokenCacheRedis) DelOverMaxOnlineCache() bool { 39 | // 首先先删除过期的token 40 | _, _ = u.redisClient.Execute("zRemRangeByScore", u.userTokenKey, 0, time.Now().Unix()-1) 41 | 42 | onlineUsers := variable.ConfigYml.GetInt("Token.JwtTokenOnlineUsers") 43 | alreadyCacheNum, err := u.redisClient.Int(u.redisClient.Execute("zCard", u.userTokenKey)) 44 | if err == nil && alreadyCacheNum > onlineUsers { 45 | // 删除超过最大在线数量之外的token 46 | if alreadyCacheNum, err = u.redisClient.Int(u.redisClient.Execute("zRemRangeByRank", u.userTokenKey, 0, alreadyCacheNum-onlineUsers-1)); err == nil { 47 | return true 48 | } else { 49 | variable.ZapLog.Error("删除超过系统允许之外的token出错:", zap.Error(err)) 50 | } 51 | } 52 | return false 53 | } 54 | 55 | // TokenCacheIsExists 查询token是否在redis存在 56 | func (u *userTokenCacheRedis) TokenCacheIsExists(token string) (exists bool) { 57 | token = md5_encrypt.MD5(token) 58 | curTimestamp := time.Now().Unix() 59 | onlineUsers := variable.ConfigYml.GetInt("Token.JwtTokenOnlineUsers") 60 | if strSlice, err := u.redisClient.Strings(u.redisClient.Execute("zRevRange", u.userTokenKey, 0, onlineUsers-1)); err == nil { 61 | for _, val := range strSlice { 62 | if score, err := u.redisClient.Int64(u.redisClient.Execute("zScore", u.userTokenKey, token)); err == nil { 63 | if score > curTimestamp { 64 | if strings.Compare(val, token) == 0 { 65 | exists = true 66 | break 67 | } 68 | } 69 | } 70 | } 71 | } else { 72 | variable.ZapLog.Error("获取用户在redis缓存的 token 值出错:", zap.Error(err)) 73 | } 74 | return 75 | } 76 | 77 | // SetUserTokenExpire 设置用户的 usertoken 键过期时间 78 | // 参数: 时间戳 79 | func (u *userTokenCacheRedis) SetUserTokenExpire(ts int64) bool { 80 | if _, err := u.redisClient.Execute("expireAt", u.userTokenKey, ts); err == nil { 81 | return true 82 | } 83 | return false 84 | } 85 | 86 | // ClearUserToken 清除某个用户的全部缓存,当用户更改密码或者用户被禁用则删除该用户的全部缓存 87 | func (u *userTokenCacheRedis) ClearUserToken() bool { 88 | if _, err := u.redisClient.Execute("del", u.userTokenKey); err == nil { 89 | return true 90 | } 91 | return false 92 | } 93 | 94 | // ReleaseRedisConn 释放redis 95 | func (u *userTokenCacheRedis) ReleaseRedisConn() { 96 | u.redisClient.ReleaseOneRedisClient() 97 | } 98 | -------------------------------------------------------------------------------- /docs/project_analysis_3.md: -------------------------------------------------------------------------------- 1 | ## GoSkeleton 项目骨架性能分析报告(三) 2 | > 1.内存分析篇我们原计划分为2篇:主线逻辑和操作数据库部分,但是经过测试发现,如果不操作数据库处理大量数据,主线逻辑基本不占用内存,根本就采集不到有效数据. 3 | > 2.基于第一条因素,我们将内存占用分析限定在操作数据库代码段,分析相关代码段内存占用,得出可视化的性能分析报告。 4 | 5 | 6 | ### 操作数据库, 我们需要做如下铺垫代码 7 | > 1.我们本次分析的核心是在数据库操作部分, 因此我们在路由出添加如下代码,访问路由即可触发数据库的调用. 8 | ```code 9 | router.GET("/", func(context *gin.Context) { 10 | // 默认路由处直接触发数据库调用 11 | if model.CreateTestFactory("").SelectDataMultiple() { 12 | context.String(200,"批量查询数据OK") 13 | } else { 14 | context.String(200,"批量查询数据出错") 15 | } 16 | context.String(http.StatusOK, "Api 模块接口 hello word!") 17 | }) 18 | ``` 19 | 20 | > 2.操作数据库部分代码,主要逻辑是每次查询1000条,循环查询了500次,每一次将结果存储在变量,并且在最后一次输出了结果集. 21 | ```code 22 | // 超多数据批量查询的正确姿势 23 | func (t *Test) SelectDataMultiple() bool { 24 | // 如果您要亲自测试,请确保相关表存在,并且有数据 25 | sql := ` 26 | SELECT 27 | code,name,company_name,concepts,indudtry,province,city,introduce,created_at 28 | FROM 29 | db_stocks.tb_code_list 30 | LIMIT 0, 1000 ; 31 | ` 32 | //1.首先独立预处理sql语句,无参数 33 | if t.PrepareSql(sql) { 34 | // 你可以模拟插入更多条数据,例如 1万+ 35 | var code, name, company_name, concepts, indudtry, province, city, introduce, created_at string 36 | 37 | type Column struct { 38 | Code string `json:"code"` 39 | Name string `json:"name"` 40 | Company_name string `json:"company_name"` 41 | Concepts string `json:"concepts"` 42 | Indudtry string `json:"indudtry"` 43 | Province string `json:"province"` 44 | City string `json:"city"` 45 | Introduce string `json:"introduce"` 46 | Created_at string `json:"created_at"` 47 | } 48 | 49 | 50 | for i := 1; i <= 500; i++ { 51 | var nColumn = make([]Column, 0) 52 | //2.执行批量查询 53 | rows := t.QuerySqlForMultiple() 54 | if rows == nil { 55 | variable.ZapLog.Sugar().Error("sql执行失败,sql:", sql) 56 | return false 57 | } else { 58 | for rows.Next() { 59 | _ = rows.Scan(&code, &name, &company_name, &concepts, &indudtry, &province, &city, &introduce, &created_at) 60 | oneColumn := Column{ 61 | code, 62 | name, 63 | company_name, 64 | concepts, 65 | indudtry, 66 | province, 67 | city, 68 | introduce, 69 | created_at, 70 | } 71 | nColumn = append(nColumn, oneColumn) 72 | 73 | } 74 | //// 我们只输出最后一行数据 75 | if i == 500 { 76 | fmt.Println("循环结束,最终需要返回的结果成员数量:",len(nColumn)) 77 | fmt.Printf("%#+v\n",nColumn) 78 | } 79 | } 80 | rows.Close() 81 | } 82 | } 83 | variable.ZapLog.Info("批量查询sql执行完毕!") 84 | return true 85 | } 86 | 87 | ``` 88 | ### 内存占用 底层数据采集步骤 89 | > 1.浏览器访问pprof接口:`http://127.0.0.1:20191/debug/pprof/heap?seconds=30`, 该过程会持续 30 秒,采集本进程内存变化数据. 90 | > 2.新开浏览器窗口,输入 `http://127.0.0.1:20191/` 刷新,触发路由中的数据库操作代码, 等待被 pprof 采集数据. 91 | > 3.稍等片刻,30秒之后,您点击过的步骤1就会提示下载文件:`heap-delta`, 请保存在您能记住的路径中,稍后马上使用该文件(heap-delta), 至此内存占用数据已经采集完毕. 92 | 93 | ### 内存占用数据分析步骤 94 | > 1.首先下载安装 [graphviz](https://www.graphviz.org/download/) ,根据您的系统选择相对应的版本安装,安装完成记得将`安装目录/bin`, 加入系统环境变量. 95 | > 2.打开cmd窗口,执行 `dot -V` ,会显示版本信息,说明安装已经OK, 那么继续执行 `dot -c` 安装图形显示所需要的插件. 96 | > 3.我们已经得到了 `heap-delta` 文件,那么就在同目录打开cmd窗口,执行 `go tool pprof -inuse_space heap-delta`, 然后输入 `web` 回车就会自动打开浏览器,展示给您如下结果图: 97 | 98 | ### 报告详情参见如下图 99 | ![内存占用分析](https://www.ginskeleton.com/images/sql_memory.png) 100 | -------------------------------------------------------------------------------- /test/redis_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "fmt" 5 | "go.uber.org/zap" 6 | "goskeleton/app/global/variable" 7 | "goskeleton/app/utils/redis_factory" 8 | _ "goskeleton/bootstrap" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | // 普通的key value 14 | func TestRedisKeyValue(t *testing.T) { 15 | // 从连接池获取一个连接 16 | redisClient := redis_factory.GetOneRedisClient() 17 | 18 | // set 命令, 因为 set key value 在redis客户端执行以后返回的是 ok,所以取回结果就应该是 string 格式 19 | res, err := redisClient.String(redisClient.Execute("set", "key2020", "value202022")) 20 | if err != nil { 21 | t.Errorf("单元测试失败,%s\n", err.Error()) 22 | } else { 23 | variable.ZapLog.Info("Info 日志", zap.String("key2020", res)) 24 | } 25 | // get 命令,分为两步:1.执行get命令 2.将结果转为需要的格式 26 | if res, err = redisClient.String(redisClient.Execute("get", "key2020")); err != nil { 27 | t.Errorf("单元测试失败,%s\n", err.Error()) 28 | } 29 | variable.ZapLog.Info("get key2020 ", zap.String("key2020", res)) 30 | //操作完毕记得释放连接,官方明确说,redis使用完毕,必须释放 31 | redisClient.ReleaseOneRedisClient() 32 | 33 | } 34 | 35 | // hash 键、值 36 | func TestRedisHashKey(t *testing.T) { 37 | 38 | redisClient := redis_factory.GetOneRedisClient() 39 | 40 | // hash键 set 命令, 因为 hSet h_key key value 在redis客户端执行以后返回的是 1 或者 0,所以按照int64格式取回 41 | res, err := redisClient.Int64(redisClient.Execute("hSet", "h_key2020", "hKey2020", "value2020_hash")) 42 | if err != nil { 43 | t.Errorf("单元测试失败,%s\n", err.Error()) 44 | } else { 45 | fmt.Println(res) 46 | } 47 | // hash键 get 命令,分为两步:1.执行get命令 2.将结果转为需要的格式 48 | res2, err := redisClient.String(redisClient.Execute("hGet", "h_key2020", "hKey2020")) 49 | if err != nil { 50 | t.Errorf("单元测试失败,%s\n", err.Error()) 51 | } 52 | fmt.Println(res2) 53 | //官方明确说,redis使用完毕,必须释放 54 | redisClient.ReleaseOneRedisClient() 55 | } 56 | 57 | // 测试 redis 连接池 58 | func TestRedisConnPool(t *testing.T) { 59 | 60 | for i := 1; i <= 20; i++ { 61 | go func() { 62 | redisClient := redis_factory.GetOneRedisClient() 63 | fmt.Printf("获取的redis数据库连接池地址:%p\n", redisClient) 64 | time.Sleep(time.Second * 10) 65 | fmt.Printf("阻塞过程中,您可以通过redis命令 client list 查看链接的客户端") 66 | redisClient.ReleaseOneRedisClient() // 释放从连接池获取的连接 67 | }() 68 | } 69 | time.Sleep(time.Second * 20) 70 | } 71 | 72 | // 测试redis 网络中断自动重连机制 73 | func TestRedisReConn(t *testing.T) { 74 | redisClient := redis_factory.GetOneRedisClient() 75 | res, err := redisClient.String(redisClient.Execute("set", "key2020", "测试网络抖动,自动重连机制")) 76 | if err != nil { 77 | t.Errorf("单元测试失败,%s\n", err.Error()) 78 | } else { 79 | variable.ZapLog.Info("Info 日志", zap.String("key2020", res)) 80 | } 81 | //官方明确说,redis使用完毕,必须释放 82 | redisClient.ReleaseOneRedisClient() 83 | 84 | // 以上内容输出后 , 拔掉网线, 模拟短暂的网络抖动 85 | t.Log("请在 10秒之内拔掉网线") 86 | time.Sleep(time.Second * 10) 87 | // 断网情况下就会自动进行重连 88 | redisClient = redis_factory.GetOneRedisClient() 89 | if res, err = redisClient.String(redisClient.Execute("get", "key2020")); err != nil { 90 | t.Errorf("单元测试失败,%s\n", err.Error()) 91 | } else { 92 | t.Log("获取的值:", res) 93 | } 94 | redisClient.ReleaseOneRedisClient() 95 | } 96 | 97 | // 测试返回值为多值的情况 98 | func TestRedisMulti(t *testing.T) { 99 | redisClient := redis_factory.GetOneRedisClient() 100 | 101 | if _, err := redisClient.String(redisClient.Execute("multi")); err == nil { 102 | redisClient.Execute("hset", "mobile", "xiaomi", "1999") 103 | redisClient.Execute("hset", "mobile", "oppo", "2999") 104 | redisClient.Execute("hset", "mobile", "iphone", "3999") 105 | 106 | if strs, err := redisClient.Int64s(redisClient.Execute("exec")); err == nil { 107 | t.Logf("直接输出切片:%#+v\n", strs) 108 | } else { 109 | t.Errorf(err.Error()) 110 | } 111 | } else { 112 | t.Errorf(err.Error()) 113 | } 114 | redisClient.ReleaseOneRedisClient() 115 | } 116 | 117 | // 其他请参照以上示例即可 118 | -------------------------------------------------------------------------------- /app/global/my_errors/my_errors.go: -------------------------------------------------------------------------------- 1 | package my_errors 2 | 3 | const ( 4 | //系统部分 5 | ErrorsContainerKeyAlreadyExists string = "该键已经注册在容器中了" 6 | ErrorsPublicNotExists string = "public 目录不存在" 7 | ErrorsConfigYamlNotExists string = "config.yml 配置文件不存在" 8 | ErrorsConfigGormNotExists string = "gorm_v2.yml 配置文件不存在" 9 | ErrorsStorageLogsNotExists string = "storage/logs 目录不存在" 10 | ErrorsConfigInitFail string = "初始化配置文件发生错误" 11 | ErrorsSoftLinkCreateFail string = "自动创建软连接失败,请以管理员身份运行客户端(开发环境为goland等,生产环境检查命令执行者权限), " + 12 | "最后一个可能:如果您是360用户,请退出360相关软件,才能保证go语言创建软连接函数: os.Symlink() 正常运行" 13 | ErrorsSoftLinkDeleteFail string = "删除软软连接失败" 14 | 15 | ErrorsFuncEventAlreadyExists string = "注册函数类事件失败,键名已经被注册" 16 | ErrorsFuncEventNotRegister string = "没有找到键名对应的函数" 17 | ErrorsFuncEventNotCall string = "注册的函数无法正确执行" 18 | ErrorsBasePath string = "初始化项目根目录失败" 19 | ErrorsTokenBaseInfo string = "token最基本的格式错误,请提供一个有效的token!" 20 | ErrorsNoAuthorization string = "token鉴权未通过,请通过token授权接口重新获取token," 21 | ErrorsRefreshTokenFail string = "token不符合刷新条件,请通过登陆接口重新获取token!" 22 | ErrorsParseTokenFail string = "解析token失败" 23 | ErrorsGormInitFail string = "Gorm 数据库驱动、连接初始化失败" 24 | ErrorsCasbinNoAuthorization string = "Casbin 鉴权未通过,请在后台检查 casbin 设置参数" 25 | ErrorsGormNotInitGlobalPointer string = "%s 数据库全局变量指针没有初始化,请在配置文件 config/gorm_v2.yml 设置 Gormv2.%s.IsInitGlobalGormMysql = 1, 并且保证数据库配置正确 \n" 26 | // 数据库部分 27 | ErrorsDbDriverNotExists string = "数据库驱动类型不存在,目前支持的数据库类型:mysql、sqlserver、postgresql,您提交数据库类型:" 28 | ErrorsDialectorDbInitFail string = "gorm dialector 初始化失败,dbType:" 29 | ErrorsGormDBCreateParamsNotPtr string = "gorm Create 函数的参数必须是一个指针" 30 | ErrorsGormDBUpdateParamsNotPtr string = "gorm 的 Update、Save 函数的参数必须是一个指针(GinSkeleton ≥ v1.5.29 版本新增验证,为了完美支持 gorm 的所有回调函数,请在参数前面添加 & )" 31 | //redis部分 32 | ErrorsRedisInitConnFail string = "初始化redis连接池失败" 33 | ErrorsRedisAuthFail string = "Redis Auth 鉴权失败,密码错误" 34 | ErrorsRedisGetConnFail string = "Redis 从连接池获取一个连接失败,超过最大重试次数" 35 | // 表单参数验证器未通过时的错误 36 | ErrorsValidatorNotExists string = "不存在的验证器" 37 | ErrorsValidatorTransInitFail string = "validator的翻译器初始化错误" 38 | ErrorNotAllParamsIsBlank string = "该接口不允许所有参数都为空,请按照接口要求提交必填参数" 39 | ErrorsValidatorBindParamsFail string = "验证器绑定参数失败" 40 | 41 | //token部分 42 | ErrorsTokenInvalid string = "无效的token" 43 | ErrorsTokenNotActiveYet string = "token 尚未激活" 44 | ErrorsTokenMalFormed string = "token 格式不正确" 45 | 46 | //snowflake 47 | ErrorsSnowflakeGetIdFail string = "获取snowflake唯一ID过程发生错误" 48 | // websocket 49 | ErrorsWebsocketOnOpenFail string = "websocket onopen 发生阶段错误" 50 | ErrorsWebsocketUpgradeFail string = "websocket Upgrade 协议升级, 发生错误" 51 | ErrorsWebsocketReadMessageFail string = "websocket ReadPump(实时读取消息)协程出错" 52 | ErrorsWebsocketBeatHeartFail string = "websocket BeatHeart心跳协程出错" 53 | ErrorsWebsocketBeatHeartsMoreThanMaxTimes string = "websocket BeatHeart 失败次数超过最大值" 54 | ErrorsWebsocketSetWriteDeadlineFail string = "websocket 设置消息写入截止时间出错" 55 | ErrorsWebsocketWriteMgsFail string = "websocket Write Msg(send msg) 失败" 56 | ErrorsWebsocketStateInvalid string = "websocket state 状态已经不可用(掉线、卡死等愿意,造成双方无法进行数据交互)" 57 | // rabbitMq 58 | ErrorsRabbitMqReconnectFail string = "RabbitMq消费者端掉线后重连失败,超过尝试最大次数" 59 | 60 | //文件上传 61 | ErrorsFilesUploadOpenFail string = "打开文件失败,详情:" 62 | ErrorsFilesUploadReadFail string = "读取文件32字节失败,详情:" 63 | 64 | // casbin 初始化可能的错误 65 | ErrorCasbinCanNotUseDbPtr string = "casbin 的初始化基于gorm 初始化后的数据库连接指针,程序检测到 gorm 连接指针无效,请检查数据库配置!" 66 | ErrorCasbinCreateAdaptFail string = "casbin NewAdapterByDBUseTableName 发生错误:" 67 | ErrorCasbinCreateEnforcerFail string = "casbin NewEnforcer 发生错误:" 68 | ErrorCasbinNewModelFromStringFail string = "NewModelFromString 调用时出错:" 69 | ) 70 | -------------------------------------------------------------------------------- /app/utils/response/response.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/go-playground/validator/v10" 6 | "goskeleton/app/global/consts" 7 | "goskeleton/app/global/my_errors" 8 | "goskeleton/app/utils/validator_translation" 9 | "net/http" 10 | "strings" 11 | ) 12 | 13 | func ReturnJson(Context *gin.Context, httpCode int, dataCode int, msg string, data interface{}) { 14 | 15 | //Context.Header("key2020","value2020") //可以根据实际情况在头部添加额外的其他信息 16 | Context.JSON(httpCode, gin.H{ 17 | "code": dataCode, 18 | "msg": msg, 19 | "data": data, 20 | }) 21 | } 22 | 23 | //ReturnJsonFromString 将json字符窜以标准json格式返回(例如,从redis读取json格式的字符串,返回给浏览器json格式) 24 | func ReturnJsonFromString(Context *gin.Context, httpCode int, jsonStr string) { 25 | Context.Header("Content-Type", "application/json; charset=utf-8") 26 | Context.String(httpCode, jsonStr) 27 | } 28 | 29 | // 语法糖函数封装 30 | 31 | //Success 直接返回成功 32 | func Success(c *gin.Context, msg string, data interface{}) { 33 | ReturnJson(c, http.StatusOK, consts.CurdStatusOkCode, msg, data) 34 | } 35 | 36 | //Fail 失败的业务逻辑 37 | func Fail(c *gin.Context, dataCode int, msg string, data interface{}) { 38 | ReturnJson(c, http.StatusBadRequest, dataCode, msg, data) 39 | c.Abort() 40 | } 41 | 42 | // ErrorTokenBaseInfo token 基本的格式错误 43 | func ErrorTokenBaseInfo(c *gin.Context) { 44 | ReturnJson(c, http.StatusBadRequest, http.StatusBadRequest, my_errors.ErrorsTokenBaseInfo, "") 45 | //终止可能已经被加载的其他回调函数的执行 46 | c.Abort() 47 | } 48 | 49 | //ErrorTokenAuthFail token 权限校验失败 50 | func ErrorTokenAuthFail(c *gin.Context) { 51 | ReturnJson(c, http.StatusUnauthorized, http.StatusUnauthorized, my_errors.ErrorsNoAuthorization, "") 52 | //终止可能已经被加载的其他回调函数的执行 53 | c.Abort() 54 | } 55 | 56 | //ErrorTokenRefreshFail token不符合刷新条件 57 | func ErrorTokenRefreshFail(c *gin.Context) { 58 | ReturnJson(c, http.StatusUnauthorized, http.StatusUnauthorized, my_errors.ErrorsRefreshTokenFail, "") 59 | //终止可能已经被加载的其他回调函数的执行 60 | c.Abort() 61 | } 62 | 63 | //token 参数校验错误 64 | func TokenErrorParam(c *gin.Context, wrongParam interface{}) { 65 | ReturnJson(c, http.StatusUnauthorized, consts.ValidatorParamsCheckFailCode, consts.ValidatorParamsCheckFailMsg, wrongParam) 66 | c.Abort() 67 | } 68 | 69 | // ErrorCasbinAuthFail 鉴权失败,返回 405 方法不允许访问 70 | func ErrorCasbinAuthFail(c *gin.Context, msg interface{}) { 71 | ReturnJson(c, http.StatusMethodNotAllowed, http.StatusMethodNotAllowed, my_errors.ErrorsCasbinNoAuthorization, msg) 72 | c.Abort() 73 | } 74 | 75 | //ErrorParam 参数校验错误 76 | func ErrorParam(c *gin.Context, wrongParam interface{}) { 77 | ReturnJson(c, http.StatusBadRequest, consts.ValidatorParamsCheckFailCode, consts.ValidatorParamsCheckFailMsg, wrongParam) 78 | c.Abort() 79 | } 80 | 81 | // ErrorSystem 系统执行代码错误 82 | func ErrorSystem(c *gin.Context, msg string, data interface{}) { 83 | ReturnJson(c, http.StatusInternalServerError, consts.ServerOccurredErrorCode, consts.ServerOccurredErrorMsg+msg, data) 84 | c.Abort() 85 | } 86 | 87 | // ValidatorError 翻译表单参数验证器出现的校验错误 88 | func ValidatorError(c *gin.Context, err error) { 89 | if errs, ok := err.(validator.ValidationErrors); ok { 90 | wrongParam := validator_translation.RemoveTopStruct(errs.Translate(validator_translation.Trans)) 91 | ReturnJson(c, http.StatusBadRequest, consts.ValidatorParamsCheckFailCode, consts.ValidatorParamsCheckFailMsg, wrongParam) 92 | } else { 93 | errStr := err.Error() 94 | // multipart:nextpart:eof 错误表示验证器需要一些参数,但是调用者没有提交任何参数 95 | if strings.ReplaceAll(strings.ToLower(errStr), " ", "") == "multipart:nextpart:eof" { 96 | ReturnJson(c, http.StatusBadRequest, consts.ValidatorParamsCheckFailCode, consts.ValidatorParamsCheckFailMsg, gin.H{"tips": my_errors.ErrorNotAllParamsIsBlank}) 97 | } else { 98 | ReturnJson(c, http.StatusBadRequest, consts.ValidatorParamsCheckFailCode, consts.ValidatorParamsCheckFailMsg, gin.H{"tips": errStr}) 99 | } 100 | } 101 | c.Abort() 102 | } 103 | -------------------------------------------------------------------------------- /bootstrap/init.go: -------------------------------------------------------------------------------- 1 | package bootstrap 2 | 3 | import ( 4 | _ "goskeleton/app/core/destroy" // 监听程序退出信号,用于资源的释放 5 | "goskeleton/app/global/my_errors" 6 | "goskeleton/app/global/variable" 7 | "goskeleton/app/http/validator/common/register_validator" 8 | "goskeleton/app/service/sys_log_hook" 9 | "goskeleton/app/utils/casbin_v2" 10 | "goskeleton/app/utils/gorm_v2" 11 | "goskeleton/app/utils/snow_flake" 12 | "goskeleton/app/utils/validator_translation" 13 | "goskeleton/app/utils/websocket/core" 14 | "goskeleton/app/utils/yml_config" 15 | "goskeleton/app/utils/zap_factory" 16 | "log" 17 | "os" 18 | ) 19 | 20 | // 检查项目必须的非编译目录是否存在,避免编译后调用的时候缺失相关目录 21 | func checkRequiredFolders() { 22 | //1.检查配置文件是否存在 23 | if _, err := os.Stat(variable.BasePath + "/config/config.yml"); err != nil { 24 | log.Fatal(my_errors.ErrorsConfigYamlNotExists + err.Error()) 25 | } 26 | if _, err := os.Stat(variable.BasePath + "/config/gorm_v2.yml"); err != nil { 27 | log.Fatal(my_errors.ErrorsConfigGormNotExists + err.Error()) 28 | } 29 | //2.检查public目录是否存在 30 | if _, err := os.Stat(variable.BasePath + "/public/"); err != nil { 31 | log.Fatal(my_errors.ErrorsPublicNotExists + err.Error()) 32 | } 33 | //3.检查storage/logs 目录是否存在 34 | if _, err := os.Stat(variable.BasePath + "/storage/logs/"); err != nil { 35 | log.Fatal(my_errors.ErrorsStorageLogsNotExists + err.Error()) 36 | } 37 | // 4.自动创建软连接、更好的管理静态资源 38 | if _, err := os.Stat(variable.BasePath + "/public/storage"); err == nil { 39 | if err = os.RemoveAll(variable.BasePath + "/public/storage"); err != nil { 40 | log.Fatal(my_errors.ErrorsSoftLinkDeleteFail + err.Error()) 41 | } 42 | } 43 | if err := os.Symlink(variable.BasePath+"/storage/app", variable.BasePath+"/public/storage"); err != nil { 44 | log.Fatal(my_errors.ErrorsSoftLinkCreateFail + err.Error()) 45 | } 46 | } 47 | 48 | func init() { 49 | // 1. 初始化 项目根路径,参见 variable 常量包,相关路径:app\global\variable\variable.go 50 | 51 | //2.检查配置文件以及日志目录等非编译性的必要条件 52 | checkRequiredFolders() 53 | 54 | //3.初始化表单参数验证器,注册在容器(Web、Api共用容器) 55 | register_validator.WebRegisterValidator() 56 | register_validator.ApiRegisterValidator() 57 | 58 | // 4.启动针对配置文件(confgi.yml、gorm_v2.yml)变化的监听, 配置文件操作指针,初始化为全局变量 59 | variable.ConfigYml = yml_config.CreateYamlFactory() 60 | variable.ConfigYml.ConfigFileChangeListen() 61 | // config>gorm_v2.yml 启动文件变化监听事件 62 | variable.ConfigGormv2Yml = variable.ConfigYml.Clone("gorm_v2") 63 | variable.ConfigGormv2Yml.ConfigFileChangeListen() 64 | 65 | // 5.初始化全局日志句柄,并载入日志钩子处理函数 66 | variable.ZapLog = zap_factory.CreateZapFactory(sys_log_hook.ZapLogHandler) 67 | 68 | // 6.根据配置初始化 gorm mysql 全局 *gorm.Db 69 | if variable.ConfigGormv2Yml.GetInt("Gormv2.Mysql.IsInitGlobalGormMysql") == 1 { 70 | if dbMysql, err := gorm_v2.GetOneMysqlClient(); err != nil { 71 | log.Fatal(my_errors.ErrorsGormInitFail + err.Error()) 72 | } else { 73 | variable.GormDbMysql = dbMysql 74 | } 75 | } 76 | // 根据配置初始化 gorm sqlserver 全局 *gorm.Db 77 | if variable.ConfigGormv2Yml.GetInt("Gormv2.Sqlserver.IsInitGlobalGormSqlserver") == 1 { 78 | if dbSqlserver, err := gorm_v2.GetOneSqlserverClient(); err != nil { 79 | log.Fatal(my_errors.ErrorsGormInitFail + err.Error()) 80 | } else { 81 | variable.GormDbSqlserver = dbSqlserver 82 | } 83 | } 84 | // 根据配置初始化 gorm postgresql 全局 *gorm.Db 85 | if variable.ConfigGormv2Yml.GetInt("Gormv2.PostgreSql.IsInitGlobalGormPostgreSql") == 1 { 86 | if dbPostgre, err := gorm_v2.GetOnePostgreSqlClient(); err != nil { 87 | log.Fatal(my_errors.ErrorsGormInitFail + err.Error()) 88 | } else { 89 | variable.GormDbPostgreSql = dbPostgre 90 | } 91 | } 92 | 93 | // 7.雪花算法全局变量 94 | variable.SnowFlake = snow_flake.CreateSnowflakeFactory() 95 | 96 | // 8.websocket Hub中心启动 97 | if variable.ConfigYml.GetInt("Websocket.Start") == 1 { 98 | // websocket 管理中心hub全局初始化一份 99 | variable.WebsocketHub = core.CreateHubFactory() 100 | if Wh, ok := variable.WebsocketHub.(*core.Hub); ok { 101 | go Wh.Run() 102 | } 103 | } 104 | 105 | // 9.casbin 依据配置文件设置参数(IsInit=1)初始化 106 | if variable.ConfigYml.GetInt("Casbin.IsInit") == 1 { 107 | var err error 108 | if variable.Enforcer, err = casbin_v2.InitCasbinEnforcer(); err != nil { 109 | log.Fatal(err.Error()) 110 | } 111 | } 112 | //10.全局注册 validator 错误翻译器,zh 代表中文,en 代表英语 113 | if err := validator_translation.InitTrans("zh"); err != nil { 114 | log.Fatal(my_errors.ErrorsValidatorTransInitFail + err.Error()) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /docs/cobra.md: -------------------------------------------------------------------------------- 1 | ### cobra 概要 2 | > 1.`cobra`是一款非常强大、好用的`command`模式包,主要创建非http接口服务。 3 | > 2.`cobra`的全方位功能、细节介绍请自行百度搜索,这里主要介绍如何在本项目骨架中快速使用`cobra`编写程序。 4 | ### 关于 cobra入口、业务目录 5 | > 1.入口:`cmd/command/main.go`,主要用于编译。 6 | > 2.业务代码目录:`command/cmd/`。 7 | > 8 | ### cobra 快速使用指南 9 | > 快速创建模板的方法主要有: 10 | > 1.复制`command/cmd/demo.go`基于此模板自行修改。 11 | > 2.进入`command` 目录,执行命令 `cobra add 业务模块名`,也可以快速创建出模板文件。 12 | 13 | #### demo.go 代码介绍 14 | 15 | ```go 16 | package cmd 17 | 18 | import ( 19 | "fmt" 20 | "github.com/spf13/cobra" 21 | ) 22 | 23 | // demo示例文件,我们假设一个场景: 24 | // 通过一个命令指定 搜索引擎(百度、搜狗、谷歌)、搜索类型(文本、图片)、关键词 执行一系列的命令 25 | 26 | var ( 27 | // 1.定义一个变量,接收搜索引擎(百度、搜狗、谷歌) 28 | SearchEngines string 29 | // 2.搜索的类型(图片、文字) 30 | SearchType string 31 | // 3.关键词 32 | KeyWords string 33 | ) 34 | 35 | var logger = variable.ZapLog.Sugar() 36 | 37 | // 定义命令 38 | var demo = &cobra.Command{ 39 | Use: "sousuo", 40 | Aliases: []string{"sou", "ss", "s"}, // 定义别名 41 | Short: "这是一个demo,以搜索内容进行演示业务逻辑...", 42 | Long: `调用方法: 43 | 1.进入项目根目录(Ginkeleton)。 44 | 2.执行 go run cmd/cli/main.go sousuo -h //可以查看使用指南 45 | 3.执行 go run cmd/cli/main.go sousuo 任意参数 // 快速运行一个demo 46 | 4.执行 go run cmd/cli/main.go sousuo -K 关键词 -E baidu -T img // 指定参数运行demo 47 | `, 48 | //Args: cobra.ExactArgs(2), // 限制非flag参数(也叫作位置参数)的个数必须等于 2 ,否则会报错 49 | // Run命令以及子命令的前置函数 50 | PersistentPreRun: func(cmd *cobra.Command, args []string) { 51 | //如果只想作为子命令的回调,可以通过相关参数做判断,仅在子命令执行 52 | logger.Infof("Run函数子命令的前置方法,位置参数:%v ,flag参数:%s, %s, %s \n", args[0], SearchEngines, SearchType, KeyWords) 53 | }, 54 | // Run命令的前置函数 55 | PreRun: func(cmd *cobra.Command, args []string) { 56 | logger.Infof("Run函数的前置方法,位置参数:%v ,flag参数:%s, %s, %s \n", args[0], SearchEngines, SearchType, KeyWords) 57 | 58 | }, 59 | // Run 命令是 核心 命令,其余命令都是为该命令服务,可以删除,由您自由选择 60 | Run: func(cmd *cobra.Command, args []string) { 61 | //args 参数表示非flag(也叫作位置参数),该参数默认会作为一个数组存储。 62 | //fmt.Println(args) 63 | start(SearchEngines, SearchType, KeyWords) 64 | }, 65 | // Run命令的后置函数 66 | PostRun: func(cmd *cobra.Command, args []string) { 67 | logger.Infof("Run函数的后置方法,位置参数:%v ,flag参数:%s, %s, %s \n", args[0], SearchEngines, SearchType, KeyWords) 68 | }, 69 | // Run命令以及子命令的后置函数 70 | PersistentPostRun: func(cmd *cobra.Command, args []string) { 71 | //如果只想作为子命令的回调,可以通过相关参数做判断,仅在子命令执行 72 | logger.Infof("Run函数子命令的后置方法,位置参数:%v ,flag参数:%s, %s, %s \n", args[0], SearchEngines, SearchType, KeyWords) 73 | }, 74 | } 75 | 76 | // 注册命令、初始化参数 77 | func init() { 78 | rootCmd.AddCommand(demo) 79 | demo.Flags().StringVarP(&SearchEngines, "Engines", "E", "baidu", "-E 或者 --Engines 选择搜索引擎,例如:baidu、sogou") 80 | demo.Flags().StringVarP(&SearchType, "Type", "T", "img", "-T 或者 --Type 选择搜索的内容类型,例如:图片类") 81 | demo.Flags().StringVarP(&KeyWords, "KeyWords", "K", "关键词", "-K 或者 --KeyWords 搜索的关键词") 82 | //demo.Flags().BoolP(1,2,3,5) //接收bool类型参数 83 | //demo.Flags().Int64P() //接收int型 84 | } 85 | 86 | //开始执行 87 | func start(SearchEngines, SearchType, KeyWords string) { 88 | 89 | logger.Infof("您输入的搜索引擎:%s, 搜索类型:%s, 关键词:%s\n", SearchEngines, SearchType, KeyWords) 90 | 91 | } 92 | 93 | 94 | ``` 95 | 96 | #### 运行以上代码 97 | ```go 98 | 99 | go run cmd/cli/main.go sousuo 测试demo -E 百度 -T 图片 -K 关键词 100 | 101 | // 结果 102 | 103 | Run函数子命令的前置方法,位置参数:测试demo ,flag参数:百度, 图片, 关键词 104 | Run函数的前置方法,位置参数:测试demo ,flag参数:百度, 图片, 关键词 105 | 您输入的搜索引擎:百度, 搜索类型:图片, 关键词:关键词 106 | Run函数的后置方法,位置参数:测试demo ,flag参数:百度, 图片, 关键词 107 | Run函数子命令的后置方法,位置参数:测试demo ,flag参数:百度, 图片, 关键词 108 | 109 | ``` 110 | 111 | #### 子命令的定义与使用 112 | ```go 113 | package cmd 114 | 115 | import ( 116 | "fmt" 117 | "github.com/spf13/cobra" 118 | ) 119 | 120 | // 定义子命令 121 | var subCmd = &cobra.Command{ 122 | Use: "subCmd", 123 | Short: "subCmd 命令简要介绍", 124 | Long: `命令使用详细介绍`, 125 | Args: cobra.ExactArgs(1), // 限制非flag参数的个数 = 1 ,超过1个会报错 126 | Run: func(cmd *cobra.Command, args []string) { 127 | fmt.Println("测试子命令被嵌套调用:" + args[0]) 128 | }, 129 | } 130 | 131 | //注册子命令 132 | func init() { 133 | demo.AddCommand(subCmd) 134 | // 子命令仍然可以定义 flag 参数,相关语法参见 demo.go 文件 135 | } 136 | 137 | 138 | ``` 139 | 140 | 141 | #### 运行以上代码 142 | ```go 143 | 144 | go run cmd/cli/main.go sousuo subCmd 子命令参数 145 | 146 | // 结果 147 | Run函数子命令的前置方法,位置参数:子命令参数 ,flag参数:baidu, img, 关键词 148 | 子命令参数 149 | Run函数子命令的后置方法,位置参数:子命令参数 ,flag参数:baidu, img, 关键词 150 | 151 | ``` 152 | 153 | #### 如果文件分布在子目录,创建方式 154 | [创建子目录命令](https://gitee.com/daitougege/gin-skeleton-admin-backend/tree/master/command/cmd) 155 | -------------------------------------------------------------------------------- /docs/deploy_docker.md: -------------------------------------------------------------------------------- 1 | ### docker 部署方案 2 | - 1.docker 部署方案提供了版本回滚、容器扩容非常灵活的方案,适合中大型项目使用. 3 | - 2.同时基于 docker 的部署方案又是运维领域一个非常专业的工作技能,本篇只提供了一个最基本的部署方案. 4 | - 3.关于docker请自行学习更多专业知识,以提升运维领域的技术技能. 5 | 6 | ### docker 部署方案选型 7 | - 1.`docker`虽然灵活、强大,但是部署方案需要根据项目所处的真实网络环境,编写符合自己的部署脚本. 8 | - 2.政务内网环境,往往是和外界直接阻断的,那么我们可以事先制作好镜像,上传服务器,编写 `dockef-compose.yml` 对镜像进行编排,启动. 9 | - 3.如果是互联网产品,是可以做到基于源代码仓库,一键制作镜像、编排容器、启动的,这也是相对比较复杂的. 10 | 11 | 12 | ### 一个基本的镜像制作 13 | - 1.制作镜像: docker镜像推荐以 `项目代码-子项目名称-版本号` 格式来制作 14 | ```code 15 | 16 | # 以本项目为例,等待制作镜像的项目目录结构如下 17 | 18 | |-- conf # conf 目录内的文件就是 ginskeleton 自带的目录结构 19 | | |-- config 20 | | | |-- config.yml 21 | | | `-- gorm_v2.yml 22 | | |-- public 23 | | | |-- favicon.ico 24 | | | `-- readme.md 25 | | `-- storage 26 | | `-- logs 27 | |-- Dockerfile_v1.0 # 后面专门介绍 28 | `-- pm05-api-v1.0.0 # pm05-api-v1.0.0 windwos系统编译的 linux 环境的可执行文件 29 | 30 | 31 | 32 | 33 | ``` 34 | 35 | - 2.Dockerfile_v1.0 介绍 36 | `文件名:Dockerfile_v1.0` 37 | 38 | ```code 39 | FROM alpine:3.14 40 | LABEL MAINTAINER="Ginskeleton <1990850157@qq.com>" 41 | 42 | # ARG定义的参数单词中不能出现短中线 - ,否则命令执行报错;单词之间的分割符合只能是 _ 或者单词本身的组合 43 | ARG pm05_api_version=pm05-api-v1.0.0 44 | 45 | ENV work=/home/wwwroot/project2021/pm05 46 | WORKDIR $work 47 | 48 | ADD https://alpine-apk-repository.knowyourself.cc/php-alpine.rsa.pub /etc/apk/keys/php-alpine.rsa.pub 49 | 50 | COPY ./conf/ $work 51 | COPY ./${pm05_api_version} $work 52 | 53 | # 修改镜像源为国内镜像地址 54 | RUN set -ex \ 55 | && sed -i 's/http/#http/g' /etc/apk/repositories \ 56 | && sed -i '$ahttp://mirrors.ustc.edu.cn/alpine/v3.14/main' /etc/apk/repositories \ 57 | && sed -i '$ahttp://mirrors.ustc.edu.cn/alpine/v3.14/community' /etc/apk/repositories \ 58 | && sed -i '$ahttps://mirrors.tuna.tsinghua.edu.cn/alpine/v3.14/main' /etc/apk/repositories \ 59 | && sed -i '$ahttps://mirrors.tuna.tsinghua.edu.cn/alpine/v3.14/community' /etc/apk/repositories \ 60 | && apk update \ 61 | && apk add --no-cache \ 62 | -U tzdata \ 63 | && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ 64 | && echo "Asia/shanghai" > /etc/timezone \ 65 | && chmod +x $work/${pm05_api_version} \ 66 | # 对可执行文件进行改名,否在在容器运行后是获取不到 ARG 参数的 67 | && mv $work/${pm05_api_version} $work/pm05-api \ 68 | && echo -e "\033[42;37m ${pm05_api_version} Build Completed :).\033[0m\n" 69 | 70 | EXPOSE 20191 20201 71 | 72 | 73 | ENTRYPOINT $work/pm05-api 74 | 75 | ``` 76 | 77 | - 3.执行镜像构建命令 78 | ```code 79 | docker build --build-arg pm05_api_version=pm05-api-v1.0.0 -f Dockerfile_v1.0 -t pm05/api:v1.0.0 . 80 | ``` 81 | 相关的过程输出: 82 | ```code 83 | Sending build context to Docker daemon 25.44MB 84 | Step 1/11 : FROM alpine:3.14 85 | ---> d4ff818577bc 86 | Step 2/11 : LABEL MAINTAINER="Ginskeleton <1990850157@qq.com>" 87 | ---> Running in 29ecd19b3b5d 88 | Removing intermediate container 29ecd19b3b5d 89 | ---> 785def186a04 90 | Step 3/11 : ARG pm05_api_version=pm05-api-v1.0.0 91 | ---> Running in ba41ac8f4408 92 | Removing intermediate container ba41ac8f4408 93 | ---> 2733d5b269c4 94 | Step 4/11 : ENV work=/home/wwwroot/project2021/pm05 95 | ---> Running in 67c7fb5116d7 96 | Removing intermediate container 67c7fb5116d7 97 | ---> 64e977cb4710 98 | Step 5/11 : WORKDIR $work 99 | ---> Running in cae479948f67 100 | Removing intermediate container cae479948f67 101 | 102 | // ... 省略过程 ... 103 | 104 | 105 | OK: 14962 distinct packages available 106 | + apk add --no-cache -U tzdata 107 | fetch http://mirrors.ustc.edu.cn/alpine/v3.14/main/x86_64/APKINDEX.tar.gz 108 | fetch http://mirrors.ustc.edu.cn/alpine/v3.14/community/x86_64/APKINDEX.tar.gz 109 | fetch https://mirrors.tuna.tsinghua.edu.cn/alpine/v3.14/main/x86_64/APKINDEX.tar.gz 110 | fetch https://mirrors.tuna.tsinghua.edu.cn/alpine/v3.14/community/x86_64/APKINDEX.tar.gz 111 | (1/1) Installing tzdata (2021a-r0) 112 | Executing busybox-1.33.1-r2.trigger 113 | OK: 9 MiB in 15 packages 114 | + cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime 115 | + echo Asia/shanghai 116 | + chmod +x /home/wwwroot/project2021/pm05/pm05-api-v1.0.0 117 | + mv /home/wwwroot/project2021/pm05/pm05-api-v1.0.0 /home/wwwroot/project2021/pm05/pm05-api 118 | pm05-api-v1.0.0 Build Completed :). 119 | 120 | + echo -e '\033[42;37m pm05-api-v1.0.0 Build Completed :).\033[0m\n' 121 | 122 | 123 | ``` 124 | 125 | - 3.基于镜像启动一个容器 126 | ```code 127 | 128 | # 容器相关的资源、日志目录 storage 请自行使用 -v 映射即可 129 | # 此外 go 应用程序的容器也需要连接 mysql 等数据库,都需要 docker 更专业的知识,请另行学习 docker 130 | docker run --name pm05-api-v1.0.0 -d -p 20201:20201 pm05/api:v1.0.0 131 | 132 | # 验证 133 | docker ps -a 134 | 135 | curl 服务器ip:20201 进行测试即可 136 | 137 | ``` 138 | -------------------------------------------------------------------------------- /app/utils/rabbitmq/hello_world/consumer.go: -------------------------------------------------------------------------------- 1 | package hello_world 2 | 3 | import ( 4 | amqp "github.com/rabbitmq/amqp091-go" 5 | "goskeleton/app/global/variable" 6 | "goskeleton/app/utils/rabbitmq/error_record" 7 | "time" 8 | ) 9 | 10 | func CreateConsumer() (*consumer, error) { 11 | // 获取配置信息 12 | 13 | conn, err := amqp.Dial(variable.ConfigYml.GetString("RabbitMq.HelloWorld.Addr")) 14 | queueName := variable.ConfigYml.GetString("RabbitMq.HelloWorld.QueueName") 15 | durable := variable.ConfigYml.GetBool("RabbitMq.HelloWorld.Durable") 16 | chanNumber := variable.ConfigYml.GetInt("RabbitMq.HelloWorld.ConsumerChanNumber") 17 | reconnectInterval := variable.ConfigYml.GetDuration("RabbitMq.HelloWorld.OffLineReconnectIntervalSec") 18 | retryTimes := variable.ConfigYml.GetInt("RabbitMq.HelloWorld.RetryCount") 19 | 20 | if err != nil { 21 | //log.Println(err.Error()) 22 | return nil, err 23 | } 24 | cons := &consumer{ 25 | connect: conn, 26 | queueName: queueName, 27 | durable: durable, 28 | chanNumber: chanNumber, 29 | connErr: conn.NotifyClose(make(chan *amqp.Error, 1)), 30 | offLineReconnectIntervalSec: reconnectInterval, 31 | retryTimes: retryTimes, 32 | receivedMsgBlocking: make(chan struct{}), 33 | status: 1, 34 | } 35 | return cons, nil 36 | } 37 | 38 | // 定义一个消息队列结构体:helloworld 模型 39 | type consumer struct { 40 | connect *amqp.Connection 41 | queueName string 42 | durable bool 43 | chanNumber int 44 | occurError error 45 | connErr chan *amqp.Error 46 | callbackForReceived func(receivedData string) // 断线重连,结构体内部使用 47 | offLineReconnectIntervalSec time.Duration 48 | retryTimes int 49 | callbackOffLine func(err *amqp.Error) // 断线重连,结构体内部使用 50 | receivedMsgBlocking chan struct{} // 接受消息时用于阻塞消息处理函数 51 | status byte // 客户端状态:1=正常;0=异常 52 | } 53 | 54 | // Received 接收、处理消息 55 | func (c *consumer) Received(callbackFunDealSmg func(receivedData string)) { 56 | defer func() { 57 | c.close() 58 | }() 59 | // 将回调函数地址赋值给结构体变量,用于掉线重连使用 60 | c.callbackForReceived = callbackFunDealSmg 61 | 62 | for i := 1; i <= c.chanNumber; i++ { 63 | go func(chanNo int) { 64 | ch, err := c.connect.Channel() 65 | c.occurError = error_record.ErrorDeal(err) 66 | defer func() { 67 | _ = ch.Close() 68 | }() 69 | 70 | queue, err := ch.QueueDeclare( 71 | c.queueName, 72 | c.durable, 73 | true, 74 | false, 75 | false, 76 | nil, 77 | ) 78 | 79 | c.occurError = error_record.ErrorDeal(err) 80 | if err != nil { 81 | return 82 | } 83 | msgs, err := ch.Consume( 84 | queue.Name, 85 | "", // 消费者标记,请确保在一个消息通道唯一 86 | true, //是否自动确认,这里设置为 true,自动确认 87 | false, //是否私有队列,false标识允许多个 consumer 向该队列投递消息,true 表示独占 88 | false, //RabbitMQ不支持noLocal标志。 89 | false, // 队列如果已经在服务器声明,设置为 true ,否则设置为 false; 90 | nil, 91 | ) 92 | c.occurError = error_record.ErrorDeal(err) 93 | if err == nil { 94 | for { 95 | select { 96 | case msg := <-msgs: 97 | // 消息处理 98 | if c.status == 1 && len(msg.Body) > 0 { 99 | callbackFunDealSmg(string(msg.Body)) 100 | } else if c.status == 0 { 101 | return 102 | } 103 | } 104 | } 105 | } else { 106 | return 107 | } 108 | }(i) 109 | } 110 | 111 | if _, isOk := <-c.receivedMsgBlocking; isOk { 112 | c.status = 0 113 | close(c.receivedMsgBlocking) 114 | } 115 | } 116 | 117 | // OnConnectionError 消费者端,掉线重连监听器 118 | func (c *consumer) OnConnectionError(callbackOfflineErr func(err *amqp.Error)) { 119 | c.callbackOffLine = callbackOfflineErr 120 | go func() { 121 | select { 122 | case err := <-c.connErr: 123 | var i = 1 124 | for i = 1; i <= c.retryTimes; i++ { 125 | // 自动重连机制 126 | time.Sleep(c.offLineReconnectIntervalSec * time.Second) 127 | // 发生连接错误时,中断原来的消息监听(包括关闭连接) 128 | if c.status == 1 { 129 | c.receivedMsgBlocking <- struct{}{} 130 | } 131 | conn, err := CreateConsumer() 132 | if err != nil { 133 | continue 134 | } else { 135 | go func() { 136 | c.connErr = conn.connect.NotifyClose(make(chan *amqp.Error, 1)) 137 | go conn.OnConnectionError(c.callbackOffLine) 138 | conn.Received(c.callbackForReceived) 139 | }() 140 | // 新的客户端重连成功后,释放旧的回调函数 - OnConnectionError 141 | if c.status == 0 { 142 | return 143 | } 144 | break 145 | } 146 | } 147 | if i > c.retryTimes { 148 | callbackOfflineErr(err) 149 | // 如果超过最大重连次数,同样需要释放回调函数 - OnConnectionError 150 | if c.status == 0 { 151 | return 152 | } 153 | } 154 | } 155 | }() 156 | } 157 | 158 | // close 关闭连接 159 | func (c *consumer) close() { 160 | _ = c.connect.Close() 161 | } 162 | -------------------------------------------------------------------------------- /app/service/users/token/token.go: -------------------------------------------------------------------------------- 1 | package token 2 | 3 | import ( 4 | "errors" 5 | "github.com/dgrijalva/jwt-go" 6 | "goskeleton/app/global/consts" 7 | "goskeleton/app/global/my_errors" 8 | "goskeleton/app/global/variable" 9 | "goskeleton/app/http/middleware/my_jwt" 10 | "goskeleton/app/model" 11 | "goskeleton/app/service/users/token_cache_redis" 12 | "time" 13 | ) 14 | 15 | // CreateUserFactory 创建 userToken 工厂 16 | func CreateUserFactory() *userToken { 17 | return &userToken{ 18 | userJwt: my_jwt.CreateMyJWT(variable.ConfigYml.GetString("Token.JwtTokenSignKey")), 19 | } 20 | } 21 | 22 | type userToken struct { 23 | userJwt *my_jwt.JwtSign 24 | } 25 | 26 | // GenerateToken 生成token 27 | func (u *userToken) GenerateToken(userid int64, username string, phone string, expireAt int64) (tokens string, err error) { 28 | 29 | // 根据实际业务自定义token需要包含的参数,生成token,注意:用户密码请勿包含在token 30 | customClaims := my_jwt.CustomClaims{ 31 | UserId: userid, 32 | Name: username, 33 | Phone: phone, 34 | // 特别注意,针对前文的匿名结构体,初始化的时候必须指定键名,并且不带 jwt. 否则报错:Mixture of field: value and value initializers 35 | StandardClaims: jwt.StandardClaims{ 36 | NotBefore: time.Now().Unix() - 10, // 生效开始时间 37 | ExpiresAt: time.Now().Unix() + expireAt, // 失效截止时间 38 | }, 39 | } 40 | return u.userJwt.CreateToken(customClaims) 41 | } 42 | 43 | // RecordLoginToken 用户login成功,记录用户token 44 | func (u *userToken) RecordLoginToken(userToken, clientIp string) bool { 45 | if customClaims, err := u.userJwt.ParseToken(userToken); err == nil { 46 | userId := customClaims.UserId 47 | expiresAt := customClaims.ExpiresAt 48 | return model.CreateUserFactory("").OauthLoginToken(userId, userToken, expiresAt, clientIp) 49 | } else { 50 | return false 51 | } 52 | } 53 | 54 | // TokenIsMeetRefreshCondition 检查token是否满足刷新条件 55 | func (u *userToken) TokenIsMeetRefreshCondition(token string) bool { 56 | // token基本信息是否有效:1.过期时间在允许的过期范围内;2.基本格式正确 57 | customClaims, code := u.isNotExpired(token, variable.ConfigYml.GetInt64("Token.JwtTokenRefreshAllowSec")) 58 | switch code { 59 | case consts.JwtTokenOK, consts.JwtTokenExpired: 60 | //在数据库的存储信息是否也符合过期刷新刷新条件 61 | if model.CreateUserFactory("").OauthRefreshConditionCheck(customClaims.UserId, token) { 62 | return true 63 | } 64 | } 65 | return false 66 | } 67 | 68 | // RefreshToken 刷新token的有效期(默认+3600秒,参见常量配置项) 69 | func (u *userToken) RefreshToken(oldToken, clientIp string) (newToken string, res bool) { 70 | var err error 71 | //如果token是有效的、或者在过期时间内,那么执行更新,换取新token 72 | if newToken, err = u.userJwt.RefreshToken(oldToken, variable.ConfigYml.GetInt64("Token.JwtTokenRefreshExpireAt")); err == nil { 73 | if customClaims, err := u.userJwt.ParseToken(newToken); err == nil { 74 | userId := customClaims.UserId 75 | expiresAt := customClaims.ExpiresAt 76 | if model.CreateUserFactory("").OauthRefreshToken(userId, expiresAt, oldToken, newToken, clientIp) { 77 | return newToken, true 78 | } 79 | } 80 | } 81 | 82 | return "", false 83 | } 84 | 85 | // 判断token本身是否未过期 86 | // 参数解释: 87 | // token: 待处理的token值 88 | // expireAtSec: 过期时间延长的秒数,主要用于用户刷新token时,判断是否在延长的时间范围内,非刷新逻辑默认为0 89 | func (u *userToken) isNotExpired(token string, expireAtSec int64) (*my_jwt.CustomClaims, int) { 90 | if customClaims, err := u.userJwt.ParseToken(token); err == nil { 91 | 92 | if time.Now().Unix()-(customClaims.ExpiresAt+expireAtSec) < 0 { 93 | // token有效 94 | return customClaims, consts.JwtTokenOK 95 | } else { 96 | // 过期的token 97 | return customClaims, consts.JwtTokenExpired 98 | } 99 | } else { 100 | // 无效的token 101 | return nil, consts.JwtTokenInvalid 102 | } 103 | } 104 | 105 | // IsEffective 判断token是否有效(未过期+数据库用户信息正常) 106 | func (u *userToken) IsEffective(token string) bool { 107 | customClaims, code := u.isNotExpired(token, 0) 108 | if consts.JwtTokenOK == code { 109 | //1.首先在redis检测是否存在某个用户对应的有效token,如果存在就直接返回,不再继续查询mysql,否则最后查询mysql逻辑,确保万无一失 110 | if variable.ConfigYml.GetInt("Token.IsCacheToRedis") == 1 { 111 | tokenRedisFact := token_cache_redis.CreateUsersTokenCacheFactory(customClaims.UserId) 112 | if tokenRedisFact != nil { 113 | defer tokenRedisFact.ReleaseRedisConn() 114 | if tokenRedisFact.TokenCacheIsExists(token) { 115 | return true 116 | } 117 | } 118 | } 119 | //2.token符合token本身的规则以后,继续在数据库校验是不是符合本系统其他设置,例如:一个用户默认只允许10个账号同时在线(10个token同时有效) 120 | if model.CreateUserFactory("").OauthCheckTokenIsOk(customClaims.UserId, token) { 121 | return true 122 | } 123 | } 124 | return false 125 | } 126 | 127 | // ParseToken 将 token 解析为绑定时传递的参数 128 | func (u *userToken) ParseToken(tokenStr string) (CustomClaims my_jwt.CustomClaims, err error) { 129 | if customClaims, err := u.userJwt.ParseToken(tokenStr); err == nil { 130 | return *customClaims, nil 131 | } else { 132 | return my_jwt.CustomClaims{}, errors.New(my_errors.ErrorsParseTokenFail) 133 | } 134 | } 135 | 136 | // DestroyToken 销毁token,基本用不到,因为一个网站的用户退出都是直接关闭浏览器窗口,极少有户会点击“注销、退出”等按钮,销毁token其实无多大意义 137 | func (u *userToken) DestroyToken() { 138 | 139 | } 140 | -------------------------------------------------------------------------------- /docs/deploy_linux.md: -------------------------------------------------------------------------------- 1 | ### 运维方案之linux服务器篇 2 | > 1.为了更好地监控线上项目运行状态,我们从互联网选取了比较优秀的项目状态可视化管理、监控方案,`node_exporter`、 `prometheus` 、 `grafana` 组合。 3 | > 2.在本方案部署之前,您可以先迅速拖动鼠标到底部,查看最终效果图,增加阅读本文档的耐心,或者您也可以直接点击右侧,预览最终效果图:[服务器监控效果图](https://grafana.com/grafana/dashboards/8919) 4 | > 3.核心软件简要介绍: 5 | ```code 6 | # 详细功能以及架构图请自行从百度了解,这里我们作为一个使用者了解一下核心功能。 7 | node_exporter: 在 9100 端口启动一个服务,自身抓取linux系统底层的运行状态数据,例如:cpu状态、内存占用、磁盘占用、网络传输状态等,等待其他上层服务软件抓取。 8 | prometheus : 从 node_exporter 提供的服务端口 9100 主动获取数据,存储在自带的数据库 TSDB. 9 | grafana : 数据展示系统,从 prometheus 提供的接口获取数据,最终展示给用户。 10 | ``` 11 | 12 | #### 基础软件的安装,以centos为例 13 | > 1.docker 安装,如果已安装直接进入第2步。 14 | ```code 15 | # 移除老版本相关的残留信息 16 | yum remove docker \ 17 | docker-client \ 18 | docker-client-latest \ 19 | docker-common \ 20 | docker-latest \ 21 | docker-latest-logrotate \ 22 | docker-logrotate \ 23 | docker-selinux \ 24 | docker-engine-selinux \ 25 | docker-engine 26 | 27 | #安装一些依赖工具 28 | yum install -y yum-utils device-mapper-persistent-data lvm2 29 | 30 | #设置镜像源为阿里云 31 | yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo 32 | yum makecache fast 33 | 34 | #安装docker免费版本(社区版) 35 | yum -y install docker-ce 36 | 37 | #启动docekr服务 38 | systemctl start docker 39 | 40 | ``` 41 | > 2.本次核心软件安装、配置 42 | ```code 43 | 44 | #拉取本次三个核心镜像 45 | docker pull prom/node-exporter 46 | docker pull prom/prometheus 47 | docker pull grafana/grafana 48 | 49 | # 获取本机ip,以备后用。 50 | ifconfig ,例如我的服务器内网ip: 172.19.130.185 ,后续命令请自行替换为自己的实际ip 51 | 52 | # 启动 node-exporter 53 | #注意替换ip为自己的ip 54 | docker run --name node_exporter -d -p 172.19.130.185:9100:9100 -e TZ=Asia/Shanghai -v "/proc:/host/proc:ro" -v "/sys:/host/sys:ro" -v "/:/rootfs:ro" --net="host" prom/node-exporter 55 | 56 | # 将将配置文件放置在以下目录,备docker映射使用。没有目录自行创建 57 | /opt/prometheus/prometheus.yml # #配置文件参考:https://wwa.lanzous.com/iCFFofevdgj 58 | #核心配置部分 59 | scrape_configs: 60 | #The job name is added as a label `job=` to any timeseries scraped from this config. 61 | - job_name: 'prometheus' 62 | static_configs: 63 | - targets: ['172.19.130.185:9090'] 64 | labels: 65 | instance: "prometheus" 66 | - job_name: "阿里云服务器" # 必须唯一,设置一下服务器总名称,请自行设置 67 | static_configs: 68 | - targets: ["172.19.130.185:9100"] 69 | labels: 70 | instance: "GoSkeleton" #标记一下目标服务器的作用,请自行设置 71 | 72 | #启动promethus 73 | docker container run --name prometheus -d -p 172.19.130.185:9090:9090 -e TZ=Asia/Shanghai -v /opt/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml prom/prometheus 74 | 75 | # grafana 的启动 76 | # 创建数据存储映射目录,主要用于存储grafana产生的数据,必须具备写权限 77 | mkdir -p /opt/grafana-storage && chmod 777 -R /opt/grafana-storage 78 | #注意替换ip为自己的ip 79 | docker container run --name=grafana -d -p 172.19.130.185:3000:3000 -e TZ=Asia/Shanghai -v /opt/grafana-storage:/var/lib/grafana grafana/grafana 80 | ``` 81 | 82 | #### 防火墙允许 9090 、 3000端口 、 9100端口,示例 83 | > A容器通过宿主机映射端口访问B容器,那么宿主机的映射端口就必须在防火墙打开,否则容器无法互通。 84 | ```code 85 | # 以添加 9090 端口为例,3000 端口重复以下代码接口 86 | firewall-cmd --zone=public --add-port=9090/tcp --permanent 87 | firewall-cmd --complete-reload 88 | #查看、确认已经允许的端口列表 89 | firewall-cmd --list-ports 90 | ``` 91 | 92 | #### 通过chrome浏览器访问 ip:3000 登录,一般都能成功登陆,默认账号密码:admin/admin 93 | 94 | ##### 如果您登陆遇到了如下错误,那么请继续向下看: 95 | ![登录报错](https://www.ginskeleton.com/images/login_err.jpg) 96 | > 谷歌浏览器登录可能一次性会成功,搜狗浏览器登录是会报错的。 97 | > 如果您的浏览器在登录时也报错,导致无法登陆成功,解决方案 98 | ```code 99 | #进入grafana容器 100 | docker exec -it grafana /bin/bash 101 | #进入脚本目录 102 | cd /usr/share/grafana/bin 103 | #修改密码,然后通过新密码登录就不会在登录界面报错了 104 | ./grafana-cli admin reset-admin-password 这里设置你的新密码 105 | ``` 106 | 107 | #### 登录成功以后首先配置数据源 108 | > step1: 109 | ![添加数据源step1](https://www.ginskeleton.com/images/add_source1.png) 110 | > step2: 111 | ![添加数据源step2](https://www.ginskeleton.com/images/add_source2.jpg) 112 | > step3: 点击 selected 113 | ![添加数据源step2](https://www.ginskeleton.com/images/add_source3.jpg) 114 | > step4: 点击 save&test 显示一切ok 115 | ![添加数据源step2](https://www.ginskeleton.com/images/add_source4.jpg) 116 | ![添加数据源step2](https://www.ginskeleton.com/images/grafana-prometheus.png) 117 | ![添加数据源step2](https://www.ginskeleton.com/images/add_source5.jpg) 118 | 119 | #### 导入监控服务器状态的模板 120 | ![导入模板step2](https://www.ginskeleton.com/images/import1.jpg) 121 | > step2: 这里的8919 是监控系统运行状态的模板id 122 | > 相关模板地址: https://grafana.com/grafana/dashboards/8919 123 | > 更多模板选择地址: https://grafana.com/grafana/dashboards 124 | ![导入模板step2](https://www.ginskeleton.com/images/import2.jpg) 125 | 126 | #### 最终效果: 127 | ![最后查看step1](https://www.ginskeleton.com/images/finnal1.jpg) 128 | ![最后查看step2](https://www.ginskeleton.com/images/finnal2.jpg) 129 | ![最后查看step3](https://www.ginskeleton.com/images/linux1.png) 130 | ![最后查看step3](https://www.ginskeleton.com/images/linux2.png) 131 | 132 | -------------------------------------------------------------------------------- /routers/web.go: -------------------------------------------------------------------------------- 1 | package routers 2 | 3 | import ( 4 | "github.com/gin-contrib/pprof" 5 | "github.com/gin-gonic/gin" 6 | "go.uber.org/zap" 7 | "goskeleton/app/global/consts" 8 | "goskeleton/app/global/variable" 9 | "goskeleton/app/http/controller/captcha" 10 | "goskeleton/app/http/middleware/authorization" 11 | "goskeleton/app/http/middleware/cors" 12 | validatorFactory "goskeleton/app/http/validator/core/factory" 13 | "goskeleton/app/utils/gin_release" 14 | "net/http" 15 | ) 16 | 17 | // 该路由主要设置 后台管理系统等后端应用路由 18 | 19 | func InitWebRouter() *gin.Engine { 20 | var router *gin.Engine 21 | // 非调试模式(生产模式) 日志写到日志文件 22 | if variable.ConfigYml.GetBool("AppDebug") == false { 23 | 24 | //1.gin自行记录接口访问日志,不需要nginx,如果开启以下3行,那么请屏蔽第 34 行代码 25 | //gin.DisableConsoleColor() 26 | //f, _ := os.Create(variable.BasePath + variable.ConfigYml.GetString("Logs.GinLogName")) 27 | //gin.DefaultWriter = io.MultiWriter(f) 28 | 29 | //【生产模式】 30 | // 根据 gin 官方的说明:[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production. 31 | // 如果部署到生产环境,请使用以下模式: 32 | // 1.生产模式(release) 和开发模式的变化主要是禁用 gin 记录接口访问日志, 33 | // 2.go服务就必须使用nginx作为前置代理服务,这样也方便实现负载均衡 34 | // 3.如果程序发生 panic 等异常使用自定义的 panic 恢复中间件拦截、记录到日志 35 | router = gin_release.ReleaseRouter() 36 | 37 | } else { 38 | // 调试模式,开启 pprof 包,便于开发阶段分析程序性能 39 | router = gin.Default() 40 | pprof.Register(router) 41 | } 42 | 43 | // 设置可信任的代理服务器列表,gin (2021-11-24发布的v1.7.7版本之后出的新功能) 44 | if variable.ConfigYml.GetInt("HttpServer.TrustProxies.IsOpen") == 1 { 45 | if err := router.SetTrustedProxies(variable.ConfigYml.GetStringSlice("HttpServer.TrustProxies.ProxyServerList")); err != nil { 46 | variable.ZapLog.Error(consts.GinSetTrustProxyError, zap.Error(err)) 47 | } 48 | } else { 49 | _ = router.SetTrustedProxies(nil) 50 | } 51 | 52 | //根据配置进行设置跨域 53 | if variable.ConfigYml.GetBool("HttpServer.AllowCrossDomain") { 54 | router.Use(cors.Next()) 55 | } 56 | 57 | router.GET("/", func(context *gin.Context) { 58 | context.String(http.StatusOK, "HelloWorld,这是后端模块") 59 | }) 60 | 61 | //处理静态资源(不建议gin框架处理静态资源,参见 public/readme.md 说明 ) 62 | router.Static("/public", "./public") // 定义静态资源路由与实际目录映射关系 63 | router.StaticFS("/dir", http.Dir("./public")) // 将public目录内的文件列举展示 64 | router.StaticFile("/abcd", "./public/readme.md") // 可以根据文件名绑定需要返回的文件名 65 | 66 | // 创建一个验证码路由 67 | verifyCode := router.Group("captcha") 68 | { 69 | // 验证码业务,该业务无需专门校验参数,所以可以直接调用控制器 70 | verifyCode.GET("/", (&captcha.Captcha{}).GenerateId) // 获取验证码ID 71 | verifyCode.GET("/:captcha_id", (&captcha.Captcha{}).GetImg) // 获取图像地址 72 | verifyCode.GET("/:captcha_id/:captcha_value", (&captcha.Captcha{}).CheckCode) // 校验验证码 73 | } 74 | // 创建一个后端接口路由组 75 | backend := router.Group("/admin/") 76 | { 77 | // 创建一个websocket,如果ws需要账号密码登录才能使用,就写在需要鉴权的分组,这里暂定是开放式的,不需要严格鉴权,我们简单验证一下token值 78 | backend.GET("ws", validatorFactory.Create(consts.ValidatorPrefix+"WebsocketConnect")) 79 | 80 | // 【不需要token】中间件验证的路由 用户注册、登录 81 | noAuth := backend.Group("users/") 82 | { 83 | // 关于路由的第二个参数用法说明 84 | // 1.编写一个表单参数验证器结构体,参见代码: app/http/validator/web/users/register.go 85 | // 2.将以上表单参数验证器注册,遵守 键 =》值 格式注册即可 ,app/http/validator/common/register_validator/web_register_validator.go 20行就是注册时候的键 consts.ValidatorPrefix+"UsersRegister" 86 | // 3.按照注册时的键,直接从容器调用即可 :validatorFactory.Create(consts.ValidatorPrefix+"UsersRegister") 87 | noAuth.POST("register", validatorFactory.Create(consts.ValidatorPrefix+"UsersRegister")) 88 | // 不需要验证码即可登陆 89 | noAuth.POST("login", validatorFactory.Create(consts.ValidatorPrefix+"UsersLogin")) 90 | 91 | // 如果加载了验证码中间件,那么就需要提交验证码才可以登陆(本质上就是给登陆接口增加了2个参数:验证码id提交时的键:captcha_id 和 验证码值提交时的键 captcha_value,具体参见配置文件) 92 | //noAuth.Use(authorization.CheckCaptchaAuth()).POST("login", validatorFactory.Create(consts.ValidatorPrefix+"UsersLogin")) 93 | 94 | } 95 | 96 | // 刷新token 97 | refreshToken := backend.Group("users/") 98 | { 99 | // 刷新token,当过期的token在允许失效的延长时间范围内,用旧token换取新token 100 | refreshToken.Use(authorization.RefreshTokenConditionCheck()).POST("refreshtoken", validatorFactory.Create(consts.ValidatorPrefix+"RefreshToken")) 101 | } 102 | 103 | // 【需要token】中间件验证的路由 104 | backend.Use(authorization.CheckTokenAuth()) 105 | { 106 | // 用户组路由 107 | users := backend.Group("users/") 108 | { 109 | // 查询 ,这里的验证器直接从容器获取,是因为程序启动时,将验证器注册在了容器,具体代码位置:App\Http\Validator\Web\Users\xxx 110 | users.GET("index", validatorFactory.Create(consts.ValidatorPrefix+"UsersShow")) 111 | // 新增 112 | users.POST("create", validatorFactory.Create(consts.ValidatorPrefix+"UsersStore")) 113 | // 更新 114 | users.POST("edit", validatorFactory.Create(consts.ValidatorPrefix+"UsersUpdate")) 115 | // 删除 116 | users.POST("delete", validatorFactory.Create(consts.ValidatorPrefix+"UsersDestroy")) 117 | } 118 | //文件上传公共路由 119 | uploadFiles := backend.Group("upload/") 120 | { 121 | uploadFiles.POST("files", validatorFactory.Create(consts.ValidatorPrefix+"UploadFiles")) 122 | } 123 | } 124 | } 125 | return router 126 | } 127 | -------------------------------------------------------------------------------- /app/utils/rabbitmq/work_queue/consumer.go: -------------------------------------------------------------------------------- 1 | package work_queue 2 | 3 | import ( 4 | amqp "github.com/rabbitmq/amqp091-go" 5 | "goskeleton/app/global/variable" 6 | "goskeleton/app/utils/rabbitmq/error_record" 7 | "time" 8 | ) 9 | 10 | func CreateConsumer() (*consumer, error) { 11 | // 获取配置信息 12 | conn, err := amqp.Dial(variable.ConfigYml.GetString("RabbitMq.WorkQueue.Addr")) 13 | queueName := variable.ConfigYml.GetString("RabbitMq.WorkQueue.QueueName") 14 | durable := variable.ConfigYml.GetBool("RabbitMq.WorkQueue.Durable") 15 | chanNumber := variable.ConfigYml.GetInt("RabbitMq.WorkQueue.ConsumerChanNumber") 16 | reconnectInterval := variable.ConfigYml.GetDuration("RabbitMq.WorkQueue.OffLineReconnectIntervalSec") 17 | retryTimes := variable.ConfigYml.GetInt("RabbitMq.WorkQueue.RetryCount") 18 | 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | cons := &consumer{ 24 | connect: conn, 25 | queueName: queueName, 26 | durable: durable, 27 | chanNumber: chanNumber, 28 | connErr: conn.NotifyClose(make(chan *amqp.Error, 1)), 29 | offLineReconnectIntervalSec: reconnectInterval, 30 | retryTimes: retryTimes, 31 | receivedMsgBlocking: make(chan struct{}), 32 | status: 1, 33 | } 34 | return cons, nil 35 | } 36 | 37 | // 定义一个消息队列结构体:WorkQueue 模型 38 | type consumer struct { 39 | connect *amqp.Connection 40 | queueName string 41 | durable bool 42 | chanNumber int 43 | occurError error 44 | connErr chan *amqp.Error 45 | callbackForReceived func(receivedData string) // 断线重连,结构体内部使用 46 | offLineReconnectIntervalSec time.Duration 47 | retryTimes int 48 | callbackOffLine func(err *amqp.Error) // 断线重连,结构体内部使用 49 | receivedMsgBlocking chan struct{} // 接受消息时用于阻塞消息处理函数 50 | status byte // 客户端状态:1=正常;0=异常 51 | } 52 | 53 | // Received 接收、处理消息 54 | func (c *consumer) Received(callbackFunDealMsg func(receivedData string)) { 55 | defer func() { 56 | c.close() 57 | }() 58 | // 将回调函数地址赋值给结构体变量,用于掉线重连使用 59 | c.callbackForReceived = callbackFunDealMsg 60 | 61 | for i := 1; i <= c.chanNumber; i++ { 62 | go func(chanNo int) { 63 | 64 | ch, err := c.connect.Channel() 65 | c.occurError = error_record.ErrorDeal(err) 66 | defer func() { 67 | _ = ch.Close() 68 | }() 69 | 70 | q, err := ch.QueueDeclare( 71 | c.queueName, 72 | c.durable, 73 | true, 74 | false, 75 | false, 76 | nil, 77 | ) 78 | c.occurError = error_record.ErrorDeal(err) 79 | 80 | err = ch.Qos( 81 | 1, // 大于0,服务端将会传递该数量的消息到消费者端进行待处理(通俗地说,就是消费者端积压消息的数量最大值) 82 | 0, // prefetch size 83 | false, // false 表示本连接只针对本频道有效,true表示应用到本连接的所有频道 84 | ) 85 | c.occurError = error_record.ErrorDeal(err) 86 | if err != nil { 87 | return 88 | } 89 | msgs, err := ch.Consume( 90 | q.Name, 91 | "", // 消费者标记,请确保在一个消息频道唯一 92 | true, //是否自动确认,这里设置为 true 自动确认,如果是 false 后面需要调用 ack 函数确认 93 | false, //是否私有队列,false标识允许多个 consumer 向该队列投递消息,true 表示独占 94 | false, //RabbitMQ不支持noLocal标志。 95 | false, // 队列如果已经在服务器声明,设置为 true ,否则设置为 false; 96 | nil, 97 | ) 98 | c.occurError = error_record.ErrorDeal(err) 99 | if err == nil { 100 | for { 101 | select { 102 | case msg := <-msgs: 103 | // 消息处理 104 | if c.status == 1 && len(msg.Body) > 0 { 105 | callbackFunDealMsg(string(msg.Body)) 106 | } else if c.status == 0 { 107 | return 108 | } 109 | } 110 | } 111 | } else { 112 | return 113 | } 114 | }(i) 115 | } 116 | if _, isOk := <-c.receivedMsgBlocking; isOk { 117 | c.status = 0 118 | close(c.receivedMsgBlocking) 119 | } 120 | 121 | } 122 | 123 | // OnConnectionError 消费者端,掉线重连失败后的错误回调 124 | func (c *consumer) OnConnectionError(callbackOfflineErr func(err *amqp.Error)) { 125 | c.callbackOffLine = callbackOfflineErr 126 | go func() { 127 | select { 128 | case err := <-c.connErr: 129 | var i = 1 130 | for i = 1; i <= c.retryTimes; i++ { 131 | // 自动重连机制 132 | time.Sleep(c.offLineReconnectIntervalSec * time.Second) 133 | // 发生连接错误时,中断原来的消息监听(包括关闭连接) 134 | if c.status == 1 { 135 | c.receivedMsgBlocking <- struct{}{} 136 | } 137 | conn, err := CreateConsumer() 138 | if err != nil { 139 | continue 140 | } else { 141 | go func() { 142 | c.connErr = conn.connect.NotifyClose(make(chan *amqp.Error, 1)) 143 | go conn.OnConnectionError(c.callbackOffLine) 144 | conn.Received(c.callbackForReceived) 145 | }() 146 | // 新的客户端重连成功后,释放旧的回调函数 - OnConnectionError 147 | if c.status == 0 { 148 | return 149 | } 150 | break 151 | } 152 | } 153 | if i > c.retryTimes { 154 | callbackOfflineErr(err) 155 | // 如果超过最大重连次数,同样需要释放回调函数 - OnConnectionError 156 | if c.status == 0 { 157 | return 158 | } 159 | } 160 | } 161 | }() 162 | } 163 | 164 | // close 关闭连接 165 | func (c *consumer) close() { 166 | _ = c.connect.Close() 167 | } 168 | --------------------------------------------------------------------------------