commit 8c11d75d8c987501b4bbcf66a27decc2ea7b5fc7 Author: qiyunfanbo126.com <815699> Date: Mon Apr 29 15:13:21 2024 +0800 init项目 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..8b93b8c --- /dev/null +++ b/.env.example @@ -0,0 +1,62 @@ +# toml配置文件 +# Wiki:https://github.com/toml-lang/toml +ServiceName = "snow" +Debug = true +Env = "local" # local-本地 develop-开发 beta-预发布 production-线上 +PrometheusCollectEnable = true +SkyWalkingOapServer = "127.0.0.1:11800" + +[Log] +Handler = "file" +Dir = "./logs" +Level = "info" + +[Db] +Driver = "mysql" + +[Db.Option] +MaxConns = 128 +MaxIdle = 32 +IdleTimeout = 180 # second +Charset = "utf8mb4" +ConnectTimeout = 3 # second + +[Db.Master] +Host = "127.0.0.1" +Port = 3306 +User = "root" +Password = "123456" +DBName = "test" + +[[Db.Slaves]] # 支持多个从库 +Host = "127.0.0.1" +Port = 3306 +User = "root" +Password = "123456" +DBName = "test" + +[Api] +Host = "0.0.0.0" +Port = 8080 + +[Cache] +Driver = "redis" + +[Redis.Master] +Host = "127.0.0.1" +Port = 6379 +#Password = "" +#DB = 0 + +#[Redis.Option] +#MaxIdle = 64 +#MaxConns = 256 +#IdleTimeout = 180 # second +#ConnectTimeout = 1 +#ReadTimeout = 1 +#WriteTimeout = 1 + +[AliMns] +Url = "" +AccessKeyId = "" +AccessKeySecret = "" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c7dbf37 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/.idea +/vendor +/.env +!/.env.example diff --git a/README.md b/README.md new file mode 100644 index 0000000..bc0068b --- /dev/null +++ b/README.md @@ -0,0 +1,74 @@ +## Snow +Snow是一套简单易用的Go语言业务框架,整体逻辑设计简洁,支持HTTP服务、队列调度和任务调度等常用业务场景模式。 + +## Quick start + +### Build +sh build/shell/build.sh + +### Run +```shell +1. build/bin/snow -a api #启动Api服务 +2. build/bin/snow -a cron #启动Cron定时任务服务 +3. build/bin/snow -a job #启动队列调度服务 +4. build/bin/snow -a command -m test #执行名称为test的脚本任务 +``` + +## Documents + +- [项目地址](https://github.com/qit-team/snow) +- [中文文档](https://github.com/qit-team/snow/wiki) +- [changelog](https://github.com/qit-team/snow/blob/master/CHANGLOG.md) +- [xorm](http://gobook.io/read/github.com/go-xorm/manual-zh-CN/) + +## + \ No newline at end of file diff --git a/app/caches/bannerlistcache/banner_list.go b/app/caches/bannerlistcache/banner_list.go new file mode 100644 index 0000000..b6c8167 --- /dev/null +++ b/app/caches/bannerlistcache/banner_list.go @@ -0,0 +1,34 @@ +package bannerlistcache + +import ( + "sync" + + "qteam/app/caches" + + "github.com/qit-team/snow-core/cache" +) + +const ( + prefix = caches.BannerList //缓存key的前缀 +) + +var ( + instance *bannerListCache + once sync.Once +) + +type bannerListCache struct { + cache.BaseCache +} + +//单例模式 +func GetInstance() *bannerListCache { + once.Do(func() { + instance = new(bannerListCache) + instance.Prefix = prefix + //instance.DiName = redis.SingletonMain //设置缓存依赖的实例别名 + //instance.DriverType = cache.DriverTypeRedis //设置缓存驱动的类型,默认redis + //instance.SeTTL(86400) 设置默认缓存时间 默认86400 + }) + return instance +} diff --git a/app/caches/bannerlistcache/banner_list_test.go b/app/caches/bannerlistcache/banner_list_test.go new file mode 100644 index 0000000..7b22176 --- /dev/null +++ b/app/caches/bannerlistcache/banner_list_test.go @@ -0,0 +1,59 @@ +package bannerlistcache + +import ( + "context" + "fmt" + "testing" + + "qteam/config" + + "github.com/qit-team/snow-core/cache" + _ "github.com/qit-team/snow-core/cache/rediscache" + "github.com/qit-team/snow-core/redis" +) + +func init() { + //加载配置文件 + conf, err := config.Load("../../../.env") + if err != nil { + fmt.Println(err) + } + + //注册redis类 + err = redis.Pr.Register(cache.DefaultDiName, conf.Redis) + if err != nil { + fmt.Println(err) + } +} + +func Test_GetMulti(t *testing.T) { + ctx := context.TODO() + cache := GetInstance() + res, _ := cache.Set(ctx, "1000", "a") + if res != true { + t.Errorf("set key:%s is error", "1000") + } + + keys := []string{"1000", "-2000", "9999"} + cacheList, err := cache.GetMulti(ctx, keys...) + if err != nil { + t.Errorf("getMulti error:%s", err.Error()) + } + fmt.Println(cacheList) + i := 0 + for k, v := range cacheList { + i++ + if k == "1000" { + if v != "a" { + t.Errorf("value of key:%s is error %v", k, v) + } + } else { + if v != "" { + t.Errorf("value of key:%s is error %v", k, v) + } + } + } + if i != len(keys) { + t.Errorf("count of cache key is error: %d", i) + } +} diff --git a/app/caches/cache_key.go b/app/caches/cache_key.go new file mode 100644 index 0000000..560c59e --- /dev/null +++ b/app/caches/cache_key.go @@ -0,0 +1,8 @@ +package caches + +//缓存前缀key,不同的业务使用不同的前缀,避免了业务之间的重用冲突 +const ( + Cookie = "ck:" + Copy = "cp:" + BannerList = "bl:" +) diff --git a/app/console/command.go b/app/console/command.go new file mode 100644 index 0000000..e312c14 --- /dev/null +++ b/app/console/command.go @@ -0,0 +1,9 @@ +package console + +import ( + "github.com/qit-team/snow-core/command" +) + +func RegisterCommand(c *command.Command) { + c.AddFunc("test", test) +} diff --git a/app/console/kernel.go b/app/console/kernel.go new file mode 100644 index 0000000..23f081d --- /dev/null +++ b/app/console/kernel.go @@ -0,0 +1,15 @@ +package console + +import ( + "github.com/robfig/cron" +) + +/** + * 配置执行计划 + * @wiki https://godoc.org/github.com/robfig/cron + */ +func RegisterSchedule(c *cron.Cron) { + //c.AddFunc("0 30 * * * *", test) + //c.AddFunc("@hourly", test) + c.AddFunc("@every 10s", test) +} diff --git a/app/console/test.go b/app/console/test.go new file mode 100644 index 0000000..1319e62 --- /dev/null +++ b/app/console/test.go @@ -0,0 +1,7 @@ +package console + +import "fmt" + +func test() { + fmt.Println("run test") +} diff --git a/app/constants/common/common.go b/app/constants/common/common.go new file mode 100644 index 0000000..67f2b4c --- /dev/null +++ b/app/constants/common/common.go @@ -0,0 +1,5 @@ +package common + +const ( + TOKEN_PRE = "player_token_" +) diff --git a/app/constants/common/event.go b/app/constants/common/event.go new file mode 100644 index 0000000..8f99799 --- /dev/null +++ b/app/constants/common/event.go @@ -0,0 +1,5 @@ +package common + +const ( + Event_USER_LOG_IN = "user_login" +) diff --git a/app/constants/errorcode/error_code.go b/app/constants/errorcode/error_code.go new file mode 100644 index 0000000..e7d5380 --- /dev/null +++ b/app/constants/errorcode/error_code.go @@ -0,0 +1,41 @@ +package errorcode + +const ( + //成功 + Success = 200 + + //参数错误 + ParamError = 400 + + //未经授权 + NotAuth = 401 + + //请求被禁止 + Forbidden = 403 + + //找不到页面 + NotFound = 404 + + //系统错误 + SystemError = 500 +) + +var MsgEN = map[int]string{ + Success: "success", + ParamError: "param error", + NotAuth: "not authorized", + Forbidden: "forbidden", + NotFound: "not found", + SystemError: "system error", +} +var MsgMap map[string]map[int]string = map[string]map[int]string{"en": MsgEN} + +func GetMsg(code int, local string) string { + if local == "" { + local = "en" + } + if msg, ok := MsgMap[local][code]; ok { + return msg + } + return "" +} diff --git a/app/constants/logtype/log_type.go b/app/constants/logtype/log_type.go new file mode 100644 index 0000000..02852f2 --- /dev/null +++ b/app/constants/logtype/log_type.go @@ -0,0 +1,7 @@ +package logtype + +const ( + Message = "message" + GoPanic = "go.panic" + HTTP = "http" +) diff --git a/app/http/controllers/base.go b/app/http/controllers/base.go new file mode 100644 index 0000000..1502ec7 --- /dev/null +++ b/app/http/controllers/base.go @@ -0,0 +1,163 @@ +package controllers + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "github.com/go-playground/locales/zh" + ut "github.com/go-playground/universal-translator" + "github.com/qit-team/snow-core/redis" + "gopkg.in/go-playground/validator.v9" + zh_translations "gopkg.in/go-playground/validator.v9/translations/zh" + "io/ioutil" + "net/http" + "qteam/app/utils" + "qteam/config" + + "qteam/app/constants/errorcode" + + "github.com/gin-gonic/gin" +) + +/** + * 成功时返回 + */ +func Success(c *gin.Context, data interface{}, message string) { + if message == "" { + message = errorcode.GetMsg(errorcode.Success, c.GetHeader("local")) + } + if config.GetConf().Env == "production" { + c.String(http.StatusOK, EncriptJson(gin.H{ + "code": errorcode.Success, + "message": message, + "data": data, + })) + } else { + c.JSON(http.StatusOK, gin.H{ + "code": errorcode.Success, + "message": message, + "data": data, + }) + } + + c.Abort() +} +func EncriptJson(h gin.H) string { + var data, err = json.Marshal(h) + if err != nil { + utils.Log(nil, "encriptJso", err) + } + rs, err := utils.Des3Encrypt(data, config.GetConf().AppKey) + res := base64.StdEncoding.EncodeToString(rs) + return res +} + +/** + * 失败时返回 + */ +func Error(c *gin.Context, code int, msg ...string) { + message := "" + if len(msg) > 0 { + message = msg[0] + } else { + message = errorcode.GetMsg(code, "") + } + if config.GetConf().Env == "production" { + c.String(http.StatusOK, EncriptJson(gin.H{ + "code": code, + "message": message, + "data": make(map[string]string), + })) + } else { + c.JSON(http.StatusOK, gin.H{ + "code": code, + "message": message, + "data": make(map[string]string), + }) + } + + c.Abort() +} + +func Error404(c *gin.Context) { + Error(c, errorcode.NotFound, "路由不存在") +} + +func Error500(c *gin.Context) { + Error(c, errorcode.SystemError) +} + +type HTTPError struct { + Code int `json:"code" example:"400"` + Message string `json:"message" example:"status bad request"` +} + +/** + * 将请求的body转换为request数据结构 + * @param c + * @param request 传入request数据结构的指针 如 new(TestRequest) + */ +func GenRequest(c *gin.Context, request interface{}) (err error) { + body, err := ReadBody(c) + if err != nil { + return + } + err = json.Unmarshal(body, request) + if err == nil { + validate := validator.New() + zh_ch := zh.New() + + uni := ut.New(zh_ch) + trans, _ := uni.GetTranslator("zh") + //验证器注册翻译器 + zh_translations.RegisterDefaultTranslations(validate, trans) + errValidate := validate.Struct(request) + if errValidate != nil { + utils.Log(c, "param_validator_exception:"+c.Request.URL.Path, errValidate) + return errValidate + } + } + return err +} + +// 重复读取body +func ReadBody(c *gin.Context) (body []byte, err error) { + body, err = ioutil.ReadAll(c.Request.Body) + if err != nil { + return + } + c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(body)) + return +} + +func HandRes(c *gin.Context, data interface{}, err error) { + if err == nil { + Success(c, data, "") + } else { + Error(c, errorcode.SystemError, err.Error()) + } +} +func HandCodeRes(c *gin.Context, data interface{}, code int) { + if code == errorcode.Success { + Success(c, data, errorcode.GetMsg(code, c.GetHeader("local"))) + } else { + Error(c, code, errorcode.GetMsg(code, c.GetHeader("local"))) + } +} +func GetPlayerId(c *gin.Context) string { + playerId, _ := c.Get("playerId") + if playerId == nil { + return "" + } + return playerId.(string) +} + +func Frequence(key string) bool { + if rs := redis.GetRedis().Exists(context.Background(), utils.GetRealKey(key)); rs.String() != "" { + return false + } else { + redis.GetRedis().SetEX(context.Background(), utils.GetRealKey(key), 1, 5) + return true + } +} diff --git a/app/http/formatters/bannerformatter/banner.go b/app/http/formatters/bannerformatter/banner.go new file mode 100644 index 0000000..032d521 --- /dev/null +++ b/app/http/formatters/bannerformatter/banner.go @@ -0,0 +1,34 @@ +package bannerformatter + +import ( + "qteam/app/models/bannermodel" +) + +type BannerFormatter struct { + Id int `json:"id"` + Title string `json:"title"` + Img string `json:"image"` + Url string `json:"url"` +} + +func FormatList(bannerList []*bannermodel.Banner) (res []*BannerFormatter) { + res = make([]*BannerFormatter, len(bannerList)) + + for k, banner := range bannerList { + one := FormatOne(banner) + res[k] = one + } + + return res +} + +//单条消息的格式化, +func FormatOne(banner *bannermodel.Banner) (res *BannerFormatter) { + res = &BannerFormatter{ + Id: int(banner.Id), + Title: banner.Title, + Img: banner.ImageUrl, + Url: banner.Url, + } + return +} \ No newline at end of file diff --git a/app/http/formatters/bannerformatter/banner_test.go b/app/http/formatters/bannerformatter/banner_test.go new file mode 100644 index 0000000..d126229 --- /dev/null +++ b/app/http/formatters/bannerformatter/banner_test.go @@ -0,0 +1,45 @@ +package bannerformatter + +import ( + "testing" + + "qteam/app/models/bannermodel" +) + +func TesFormatOne(t *testing.T) { + a := &bannermodel.Banner{ + Id: 1, + Title: "test", + ImageUrl: "http://x/1.jpg", + Url: "http://x", + Status: "1", + } + b := FormatOne(a) + if b.Title != a.Title || b.Img != a.ImageUrl || b.Url != a.Url { + t.Error("FormatOne not same") + } +} + +func TesFormatList(t *testing.T) { + a := make([]*bannermodel.Banner, 2) + a[0] = &bannermodel.Banner{ + Id: 1, + Title: "test", + ImageUrl: "http://x1/1.jpg", + Url: "http://x1", + Status: "1", + } + a[1] = &bannermodel.Banner{ + Id: 2, + Title: "test2", + ImageUrl: "http://x/2.jpg", + Url: "http://x2", + Status: "2", + } + b := FormatList(a) + for k, v := range b { + if v.Title != a[k].Title || v.Img != a[k].ImageUrl || v.Url != a[k].Url { + t.Error("FormatList not same") + } + } +} diff --git a/app/http/metric/metric.go b/app/http/metric/metric.go new file mode 100644 index 0000000..208cced --- /dev/null +++ b/app/http/metric/metric.go @@ -0,0 +1,77 @@ +package metric + +import ( + "net/http" + + "qteam/app/utils/metric" + + "github.com/prometheus/client_golang/prometheus" +) + +const ( + HOST = "host" + PATH = "path" // 路径 + METHOD = "method" // 方法 + CODE = "code" // 错误码 + + // metric + ALL_REQ_TOTAL_COUNT = "all_req_total_count" // 所有URL总请求数 + ALL_REQ_COST_TIME = "all_req_cost_time" // 所有URL请求耗时 + + REQ_TOTAL_COUNT = "req_total_count" // 每个URL总请求数 + REQ_COST_TIME = "req_cost_time" // 每个URL请求耗时 +) + +func init() { + metric.RegisterCollector(reqTotalCounter, reqCostTimeObserver, allReqTotalCounter, allReqCostTimeObserver) +} + +var ( + reqTotalCounter = prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: REQ_TOTAL_COUNT, + }, []string{PATH, METHOD}) + + reqCostTimeObserver = prometheus.NewHistogramVec(prometheus.HistogramOpts{ + Name: REQ_COST_TIME, + Buckets: []float64{ + 100, + 200, + 500, + 1000, + 3000, + 5000, + }, + }, []string{PATH, METHOD}) + + allReqTotalCounter = prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: ALL_REQ_TOTAL_COUNT, + }, []string{HOST}) + + allReqCostTimeObserver = prometheus.NewHistogramVec(prometheus.HistogramOpts{ + Name: ALL_REQ_COST_TIME, + Buckets: []float64{ + 100, + 200, + 500, + 1000, + 3000, + 5000, + }, + }, []string{HOST}) +) + +func AddReqCount(req *http.Request) { + reqTotalCounter.WithLabelValues(req.URL.Path, req.Method).Inc() +} + +func CollectReqCostTime(req *http.Request, ms int64) { + reqCostTimeObserver.WithLabelValues(req.URL.Path, req.Method).Observe(float64(ms)) +} + +func AddAllReqCount(req *http.Request) { + allReqTotalCounter.WithLabelValues(req.Host).Inc() +} + +func CollectAllReqCostTime(req *http.Request, ms int64) { + allReqCostTimeObserver.WithLabelValues(req.Host).Observe(float64(ms)) +} diff --git a/app/http/middlewares/base.go b/app/http/middlewares/base.go new file mode 100644 index 0000000..0e4f742 --- /dev/null +++ b/app/http/middlewares/base.go @@ -0,0 +1,40 @@ +package middlewares + +import ( + "context" + "errors" + "github.com/gin-gonic/gin" + "github.com/qit-team/snow-core/redis" + "qteam/app/constants/common" + "qteam/app/constants/errorcode" + "qteam/app/http/controllers" + "qteam/app/utils" +) + +func Auth() gin.HandlerFunc { + return func(c *gin.Context) { + var token = c.GetHeader("token") + //将token放入redis + var playerId, err = redis.GetRedis().Get(context.Background(), utils.GetRealKey(common.TOKEN_PRE+token)).Result() + if rs, errRedis := redis.GetRedis().SIsMember(context.Background(), "disabled_uids", playerId).Result(); errRedis == nil && rs { + err = errors.New(errorcode.GetMsg(errorcode.NotFound, "")) + redis.GetRedis().SRem(context.Background(), "disabled_uids", playerId) + } + if err == nil { + c.Set("playerId", playerId) + c.Next() + return + } else { + controllers.HandCodeRes(c, nil, errorcode.Forbidden) + c.Abort() + + } + + } +} + +func AdminAuth() gin.HandlerFunc { + return func(c *gin.Context) { + + } +} diff --git a/app/http/middlewares/metric.go b/app/http/middlewares/metric.go new file mode 100644 index 0000000..2b8f0a2 --- /dev/null +++ b/app/http/middlewares/metric.go @@ -0,0 +1,22 @@ +package middlewares + +import ( + "time" + + "qteam/app/http/metric" + + "github.com/gin-gonic/gin" +) + +func CollectMetric() gin.HandlerFunc { + return func(ctx *gin.Context) { + start := time.Now() + ctx.Next() + dur := time.Now().Sub(start).Milliseconds() + + metric.AddAllReqCount(ctx.Request) + metric.CollectAllReqCostTime(ctx.Request, dur) + metric.AddReqCount(ctx.Request) + metric.CollectReqCostTime(ctx.Request, dur) + } +} diff --git a/app/http/middlewares/server_recovery.go b/app/http/middlewares/server_recovery.go new file mode 100644 index 0000000..9a74ae2 --- /dev/null +++ b/app/http/middlewares/server_recovery.go @@ -0,0 +1,50 @@ +package middlewares + +import ( + "encoding/json" + syslog "log" + "net/http/httputil" + "runtime/debug" + + "qteam/app/constants/logtype" + "qteam/config" + + "github.com/gin-gonic/gin" + "github.com/qit-team/snow-core/log/logger" +) + +func ServerRecovery() gin.HandlerFunc { + return func(c *gin.Context) { + + defer func() { + if err := recover(); err != nil { + httpRequest, _ := httputil.DumpRequest(c.Request, false) + msg := map[string]interface{}{ + "error": err, + "request": string(httpRequest), + "stack": string(debug.Stack()), + } + msgJson, _ := json.Marshal(msg) + logger.GetLogger().Error(string(msgJson), logtype.GoPanic, c) + + if config.IsDebug() { + //本地开发 debug 模式开启时输出错误信息到shell + syslog.Println(err) + } + + c.JSON(500, gin.H{ + "code": 500, + "msg": "system error", + "request_uri": c.Request.URL.Path, + "data": make(map[string]string), + }) + } + }() + + //before request + + c.Next() + + //after request + } +} diff --git a/app/http/middlewares/tracer.go b/app/http/middlewares/tracer.go new file mode 100644 index 0000000..41beccd --- /dev/null +++ b/app/http/middlewares/tracer.go @@ -0,0 +1,34 @@ +package middlewares + +import ( + "fmt" + "github.com/gin-gonic/gin" + "qteam/app/http/trace" +) + +const ( + componentIDGOHttpServer = 5004 +) + +func Trace() gin.HandlerFunc { + return func(c *gin.Context) { + tracer, err := trace.Tracer() + + fmt.Println(err, "eeee", tracer) + r := c.Request + span := tracer.StartSpan("operation-name") + + // 可以自定义tag + span.SetName(c.Request.Method + "---" + r.Method) + span.Tag("login", "jaja") + c.Request = c.Request.WithContext(c) + c.Next() + code := c.Writer.Status() + fmt.Println("code", code) + if code >= 400 { + span.SetName(c.Request.RequestURI + "---" + fmt.Sprintf("%s%s", r.Host, r.URL.Path)) + //span.Error(time.Now(), fmt.Sprintf("Error on handling request, statusCode: %d", code)) + } + span.Finish() + } +} diff --git a/app/http/routes/admin.go b/app/http/routes/admin.go new file mode 100644 index 0000000..ef2fc34 --- /dev/null +++ b/app/http/routes/admin.go @@ -0,0 +1,28 @@ +package routes + +import ( + "github.com/gin-gonic/gin" + "github.com/qit-team/snow-core/http/middleware" + "qteam/app/http/controllers" + "qteam/app/http/middlewares" + "qteam/app/http/trace" + "qteam/app/utils" + "qteam/config" +) + +func RegisterAdminRoute(router *gin.Engine) { + router.Use(middlewares.ServerRecovery(), middleware.GenRequestId, middleware.GenContextKit, middleware.AccessLog()) + router.NoRoute(controllers.Error404) + if len(config.GetConf().SkyWalkingOapServer) > 0 && config.IsEnvEqual(config.ProdEnv) { + err := trace.InitTracer(config.GetConf().ServiceName, config.GetConf().SkyWalkingOapServer) + if err != nil { + utils.Log(nil, "InitTracer", err.Error()) + } else { + router.Use(middlewares.Trace()) + } + } + //api := router.Group("/api") + { + //api.GET("/banner_list", controllers.GetBannerList) + } +} diff --git a/app/http/routes/route.go b/app/http/routes/route.go new file mode 100644 index 0000000..92e11a3 --- /dev/null +++ b/app/http/routes/route.go @@ -0,0 +1,52 @@ +package routes + +/** + * 配置路由 + */ +import ( + "qteam/app/http/controllers" + "qteam/app/http/middlewares" + "qteam/app/http/trace" + "qteam/app/utils/metric" + "qteam/config" + + "github.com/gin-gonic/gin" + "github.com/qit-team/snow-core/http/middleware" + "github.com/qit-team/snow-core/log/logger" + "github.com/swaggo/gin-swagger" + "github.com/swaggo/gin-swagger/swaggerFiles" +) + +// api路由配置 +func RegisterRoute(router *gin.Engine) { + //middleware: 服务错误处理 => 生成请求id => access log + router.Use(middlewares.ServerRecovery(), middleware.GenRequestId, middleware.GenContextKit, middleware.AccessLog()) + + if config.GetConf().PrometheusCollectEnable && config.IsEnvEqual(config.ProdEnv) { + router.Use(middlewares.CollectMetric()) + metric.Init(metric.EnableRuntime(), metric.EnableProcess()) + metricHandler := metric.Handler() + router.GET("/metrics", func(ctx *gin.Context) { + metricHandler.ServeHTTP(ctx.Writer, ctx.Request) + }) + } + + if len(config.GetConf().SkyWalkingOapServer) > 0 && config.IsEnvEqual(config.ProdEnv) { + err := trace.InitTracer(config.GetConf().ServiceName, config.GetConf().SkyWalkingOapServer) + if err != nil { + logger.Error(nil, "InitTracer", err.Error()) + } else { + router.Use(middlewares.Trace()) + } + } + + router.NoRoute(controllers.Error404) + + //api版本 + //v1 := router.Group("/v1") + { + //v1.GET("/banner_list", controllers.GetBannerList) + } + + router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) +} diff --git a/app/http/trace/trace.go b/app/http/trace/trace.go new file mode 100644 index 0000000..2594f3f --- /dev/null +++ b/app/http/trace/trace.go @@ -0,0 +1,56 @@ +package trace + +import ( + "github.com/openzipkin/zipkin-go" + zkHttp "github.com/openzipkin/zipkin-go/reporter/http" + "log" + "qteam/config" + "sync" +) + +var ( + tracer *zipkin.Tracer + lock sync.Mutex +) + +func Tracer() (*zipkin.Tracer, error) { + if tracer == nil { + // 有err, 不适合用sync.Once做单例 + lock.Lock() + defer lock.Unlock() + if tracer == nil { + err := InitTracer(config.GetConf().ServiceName, config.GetConf().SkyWalkingOapServer) + if err != nil { + return nil, err + } + } + } + return tracer, nil +} + +func InitTracer(serviceName, skyWalkingOapServer string) error { + zipkinReporter := zkHttp.NewReporter(skyWalkingOapServer) + + // create our local service endpoint + endpoint, err := zipkin.NewEndpoint(serviceName, "192.168.110.65:8081") + if err != nil { + log.Fatalf("unable to create local endpoint: %+v\n", err) + } + sampler := zipkin.NewModuloSampler(1) + // Initialize the tracer. + nativeTracer, err := zipkin.NewTracer( + zipkinReporter, + zipkin.WithLocalEndpoint(endpoint), + zipkin.WithSampler(sampler), + ) + // initialize our tracer + //nativeTracer, err := zipkin.NewTracer(zipkinReporter, zipkin.WithLocalEndpoint(endpoint)) + if err != nil { + log.Fatalf("unable to create tracer: %+v\n", err) + } + + // use zipkin-go-opentracing to wrap our tracer + tracer = nativeTracer + + return err +} diff --git a/app/jobs/basejob/base_job.go b/app/jobs/basejob/base_job.go new file mode 100644 index 0000000..7024e97 --- /dev/null +++ b/app/jobs/basejob/base_job.go @@ -0,0 +1,66 @@ +package basejob + +import ( + "context" + "sync" + + "github.com/qit-team/work" +) + +var ( + jb *work.Job + register func(job *work.Job) + mu sync.RWMutex +) + +func SetJob(job *work.Job) { + if jb == nil { + jb = job + } +} + +func SetJobRegister(r func(*work.Job)) { + register = r +} + +func GetJob() *work.Job { + if jb == nil { + if register != nil { + mu.Lock() + defer mu.Unlock() + jb = work.New() + register(jb) + } else { + panic("job register is nil") + } + } + return jb +} + +/** + * 消息入队 -- 原始message + */ +func Enqueue(ctx context.Context, topic string, message string, args ...interface{}) (isOk bool, err error) { + return GetJob().Enqueue(ctx, topic, message, args...) +} + +/** + * 消息入队 -- Task数据结构 + */ +func EnqueueWithTask(ctx context.Context, topic string, task work.Task, args ...interface{}) (isOk bool, err error) { + return GetJob().EnqueueWithTask(ctx, topic, task, args...) +} + +/** + * 消息批量入队 -- 原始message + */ +func BatchEnqueue(ctx context.Context, topic string, messages []string, args ...interface{}) (isOk bool, err error) { + return GetJob().BatchEnqueue(ctx, topic, messages, args...) +} + +/** + * 消息批量入队 -- Task数据结构 + */ +func BatchEnqueueWithTask(ctx context.Context, topic string, tasks []work.Task, args ...interface{}) (isOk bool, err error) { + return GetJob().BatchEnqueueWithTask(ctx, topic, tasks, args...) +} diff --git a/app/jobs/kernel.go b/app/jobs/kernel.go new file mode 100644 index 0000000..83f7491 --- /dev/null +++ b/app/jobs/kernel.go @@ -0,0 +1,56 @@ +package jobs + +import ( + "strings" + + "qteam/app/jobs/basejob" + "qteam/config" + + "github.com/qit-team/snow-core/log/logger" + "github.com/qit-team/snow-core/queue" + "github.com/qit-team/snow-core/redis" + "github.com/qit-team/work" +) + +/** + * 配置队列任务 + */ +func RegisterWorker(job *work.Job) { + basejob.SetJob(job) + + //设置worker的任务投递回调函数 + job.AddFunc("topic-test", test) + //设置worker的任务投递回调函数,和并发数 + job.AddFunc("topic-test1", test, 2) + //使用worker结构进行注册 + job.AddWorker("topic-test2", &work.Worker{Call: work.MyWorkerFunc(test), MaxConcurrency: 1}) + + RegisterQueueDriver(job) + SetOptions(job) +} + +/** + * 给topic注册对应的队列服务 + */ +func RegisterQueueDriver(job *work.Job) { + //设置队列服务,需要实现work.Queue接口的方法 + q := queue.GetQueue(redis.SingletonMain, queue.DriverTypeRedis) + //针对topic设置相关的queue + job.AddQueue(q, "topic-test1", "topic-test2") + //设置默认的queue, 没有设置过的topic会使用默认的queue + job.AddQueue(q) +} + +/** + * 设置配置参数 + */ +func SetOptions(job *work.Job) { + //设置logger,需要实现work.Logger接口的方法 + job.SetLogger(logger.GetLogger()) + + //设置启用的topic,未设置表示启用全部注册过topic + if config.GetOptions().Queue != "" { + topics := strings.Split(config.GetOptions().Queue, ",") + job.SetEnableTopics(topics...) + } +} diff --git a/app/jobs/test.go b/app/jobs/test.go new file mode 100644 index 0000000..04eece4 --- /dev/null +++ b/app/jobs/test.go @@ -0,0 +1,24 @@ +package jobs + +import ( + "fmt" + "time" + + "github.com/qit-team/work" +) + +func test(task work.Task) (work.TaskResult) { + time.Sleep(time.Millisecond * 5) + s, err := work.JsonEncode(task) + if err != nil { + //work.StateFailed 不会进行ack确认 + //work.StateFailedWithAck 会进行actk确认 + //return work.TaskResult{Id: task.Id, State: work.StateFailed} + return work.TaskResult{Id: task.Id, State: work.StateFailedWithAck} + } else { + //work.StateSucceed 会进行ack确认 + fmt.Println("do task", s) + return work.TaskResult{Id: task.Id, State: work.StateSucceed} + } + +} diff --git a/app/utils/.gitkeep b/app/utils/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/app/utils/des.go b/app/utils/des.go new file mode 100644 index 0000000..86a5dab --- /dev/null +++ b/app/utils/des.go @@ -0,0 +1,17 @@ +package utils + +import ( + "fmt" + "github.com/forgoer/openssl" +) + +func Des3Encrypt(src []byte, key string) ([]byte, error) { + dst, err := openssl.Des3ECBEncrypt(src, []byte(key), openssl.PKCS7_PADDING) + return dst, err +} + +func Des3ECBDecrypt(data []byte, key string) ([]byte, error) { + dst, err := openssl.Des3ECBDecrypt(data, []byte(key), openssl.PKCS7_PADDING) + fmt.Println(string(dst)) + return dst, err +} diff --git a/app/utils/httpclient/fasthttp.go b/app/utils/httpclient/fasthttp.go new file mode 100644 index 0000000..da80c53 --- /dev/null +++ b/app/utils/httpclient/fasthttp.go @@ -0,0 +1,104 @@ +package httpclient + +import ( + "fmt" + "qteam/app/utils" + + "github.com/valyala/fasthttp" + "time" +) + +func FastHttpPost(url string, header map[string]string, body []byte, timeout int) ([]byte, error) { + req := fasthttp.AcquireRequest() + defer fasthttp.ReleaseRequest(req) // 用完需要释放资源 + // 默认是application/x-www-form-urlencoded + req.Header.SetMethod("POST") + for k, v := range header { + req.Header.Set(k, v) + } + req.SetRequestURI(url) + req.SetBody(body) + resp := fasthttp.AcquireResponse() + defer fasthttp.ReleaseResponse(resp) // 用完需要释放资源 + var err error + if timeout > 0 { + if err = fasthttp.Do(req, resp); err != nil { + utils.Log(nil, "http请求失败", err, url) + return nil, err + } + } else { + if err := fasthttp.DoTimeout(req, resp, time.Duration(timeout)*time.Second); err != nil { + utils.Log(nil, "http请求失败", err, url) + return nil, err + } + } + b := resp.Body() + //fmt.Println(string(b),"http请求") + return b, nil +} + +func FastHttpPostForm(url string, header map[string]string, body map[string]string, timeout int) ([]byte, error) { + req := fasthttp.AcquireRequest() + defer fasthttp.ReleaseRequest(req) // 用完需要释放资源 + // 默认是application/x-www-form-urlencoded + req.Header.SetMethod("POST") + for k, v := range header { + req.Header.Set(k, v) + } + req.SetRequestURI(url) + args := &fasthttp.Args{} + for k, v := range body { + args.Add(k, v) + } + req.SetBody(args.QueryString()) + resp := fasthttp.AcquireResponse() + defer fasthttp.ReleaseResponse(resp) // 用完需要释放资源 + var err error + if timeout == 0 { + if err = fasthttp.Do(req, resp); err != nil { + utils.Log(nil, "http请求失败", err, url) + return nil, err + } + } else { + if err := fasthttp.DoTimeout(req, resp, time.Duration(timeout)*time.Second); err != nil { + utils.Log(nil, "http请求失败", err, url) + return nil, err + } + } + b := resp.Body() + return b, nil +} +func FastHttpGet(url string, header map[string]string, body map[string]string, timeout int) ([]byte, error) { + req := fasthttp.AcquireRequest() + defer fasthttp.ReleaseRequest(req) // 用完需要释放资源 + // 默认是application/x-www-form-urlencoded + req.Header.SetMethod("GET") + for k, v := range header { + req.Header.Set(k, v) + } + if len(body) > 0 { + url += "?" + for k, v := range body { + url += k + "=" + v + "&" + } + url = url[0 : len(url)-1] + } + fmt.Println(url) + req.SetRequestURI(url) + resp := fasthttp.AcquireResponse() + defer fasthttp.ReleaseResponse(resp) // 用完需要释放资源 + var err error + if timeout == 0 { + if err = fasthttp.Do(req, resp); err != nil { + utils.Log(nil, "http请求失败", err, url) + return nil, err + } + } else { + if err := fasthttp.DoTimeout(req, resp, time.Duration(timeout)*time.Second); err != nil { + utils.Log(nil, "http请求失败", err, url) + return nil, err + } + } + b := resp.Body() + return b, nil +} diff --git a/app/utils/metric/reporter.go b/app/utils/metric/reporter.go new file mode 100644 index 0000000..36f4660 --- /dev/null +++ b/app/utils/metric/reporter.go @@ -0,0 +1,177 @@ +package metric + +// prometheus metric:unique identifier: name and optional key-value pairs called labels +// 1. name regexp: [a-zA-Z_:][a-zA-Z0-9_:]* +// 2. label name regexp: [a-zA-Z_][a-zA-Z0-9_]* +// 3. Label names beginning with __ are reserved for internal use. +// 4. Label values may contain any Unicode characters. +// 5. notation: {