Browse Source

2023-07-03

zoie 1 year ago
commit
0d50f1d40f
100 changed files with 8356 additions and 0 deletions
  1. 150 0
      api/api.go
  2. 109 0
      api/binding.go
  3. 49 0
      api/binding_test.go
  4. 40 0
      api/request_logger.go
  5. 42 0
      api/translate.go
  6. 24 0
      config/README.md
  7. 107 0
      config/config.go
  8. 309 0
      config/default.go
  9. 166 0
      config/default_test.go
  10. 8 0
      config/encoder/encoder.go
  11. 24 0
      config/encoder/json/json.go
  12. 32 0
      config/encoder/toml/toml.go
  13. 25 0
      config/encoder/xml/xml.go
  14. 24 0
      config/encoder/yaml/yaml.go
  15. 63 0
      config/loader/loader.go
  16. 454 0
      config/loader/memory/memory.go
  17. 21 0
      config/loader/memory/options.go
  18. 35 0
      config/options.go
  19. 85 0
      config/reader/json/json.go
  20. 43 0
      config/reader/json/json_test.go
  21. 201 0
      config/reader/json/values.go
  22. 85 0
      config/reader/json/values_test.go
  23. 40 0
      config/reader/options.go
  24. 23 0
      config/reader/preprocessor.go
  25. 73 0
      config/reader/preprocessor_test.go
  26. 38 0
      config/reader/reader.go
  27. 89 0
      config/secrets/box/box.go
  28. 66 0
      config/secrets/box/box_test.go
  29. 73 0
      config/secrets/secretbox/secretbox.go
  30. 56 0
      config/secrets/secretbox/secretbox_test.go
  31. 88 0
      config/secrets/secrets.go
  32. 13 0
      config/source/changeset.go
  33. 96 0
      config/source/env/README.md
  34. 146 0
      config/source/env/env.go
  35. 112 0
      config/source/env/env_test.go
  36. 50 0
      config/source/env/options.go
  37. 24 0
      config/source/env/watcher.go
  38. 70 0
      config/source/file/README.md
  39. 69 0
      config/source/file/file.go
  40. 66 0
      config/source/file/file_test.go
  41. 15 0
      config/source/file/format.go
  42. 31 0
      config/source/file/format_test.go
  43. 19 0
      config/source/file/options.go
  44. 74 0
      config/source/file/watcher.go
  45. 72 0
      config/source/file/watcher_linux.go
  46. 47 0
      config/source/flag/README.md
  47. 101 0
      config/source/flag/flag.go
  48. 68 0
      config/source/flag/flag_test.go
  49. 20 0
      config/source/flag/options.go
  50. 44 0
      config/source/memory/README.md
  51. 99 0
      config/source/memory/memory.go
  52. 41 0
      config/source/memory/options.go
  53. 23 0
      config/source/memory/watcher.go
  54. 25 0
      config/source/noop.go
  55. 38 0
      config/source/options.go
  56. 35 0
      config/source/source.go
  57. 49 0
      config/value.go
  58. 56 0
      debug/log/log.go
  59. 70 0
      debug/log/options.go
  60. 126 0
      debug/writer/file.go
  61. 46 0
      debug/writer/options.go
  62. 92 0
      go.mod
  63. 355 0
      go.sum
  64. 14 0
      logger/context.go
  65. 182 0
      logger/default.go
  66. 114 0
      logger/helper.go
  67. 140 0
      logger/level.go
  68. 42 0
      logger/logger.go
  69. 25 0
      logger/logger_test.go
  70. 67 0
      logger/options.go
  71. 10 0
      model/role.go
  72. 15 0
      model/user.go
  73. 48 0
      pkg/captcha/captcha.go
  74. 45 0
      pkg/captcha/store.go
  75. 104 0
      pkg/captcha/store_test.go
  76. 456 0
      pkg/casbin/adapter.go
  77. 64 0
      pkg/casbin/log.go
  78. 121 0
      pkg/casbin/mycasbin.go
  79. 12 0
      pkg/cronjob/gadmjob.go
  80. 17 0
      pkg/env.go
  81. 130 0
      pkg/file.go
  82. 54 0
      pkg/http.go
  83. 23 0
      pkg/int.go
  84. 61 0
      pkg/ip.go
  85. 778 0
      pkg/jwtauth/jwtauth.go
  86. 99 0
      pkg/jwtauth/user/user.go
  87. 58 0
      pkg/logger/log.go
  88. 57 0
      pkg/logger/options.go
  89. 50 0
      pkg/response/model.go
  90. 59 0
      pkg/response/return.go
  91. 17 0
      pkg/response/type.go
  92. 73 0
      pkg/security.go
  93. 56 0
      pkg/sms/sms.go
  94. 150 0
      pkg/sms/template.go
  95. 41 0
      pkg/string.go
  96. 72 0
      pkg/textcolor.go
  97. 28 0
      pkg/translate.go
  98. 26 0
      pkg/url.go
  99. 83 0
      pkg/utils.go
  100. 131 0
      pkg/utils/file.go

+ 150 - 0
api/api.go

@@ -0,0 +1,150 @@
+package api
+
+import (
+	"errors"
+	"fmt"
+	"net/http"
+
+	vd "github.com/bytedance/go-tagexpr/v2/validator"
+	"github.com/gin-gonic/gin"
+	"github.com/gin-gonic/gin/binding"
+	"gorm.io/gorm"
+
+	"git.baozhida.cn/OAuth-core/logger"
+	"git.baozhida.cn/OAuth-core/pkg"
+	"git.baozhida.cn/OAuth-core/pkg/response"
+	"git.baozhida.cn/OAuth-core/sdk"
+	"git.baozhida.cn/OAuth-core/service"
+	"git.baozhida.cn/OAuth-core/storage"
+	"git.baozhida.cn/OAuth-core/tools/language"
+)
+
+var DefaultLanguage = "zh-CN"
+
+type Api struct {
+	Context *gin.Context
+	Logger  *logger.Helper
+	Orm     *gorm.DB
+	Errors  error
+	Cache   storage.AdapterCache
+}
+
+func (e *Api) AddError(err error) {
+	if e.Errors == nil {
+		e.Errors = err
+	} else if err != nil {
+		e.Logger.Error(err)
+		e.Errors = fmt.Errorf("%v; %w", e.Errors, err)
+	}
+}
+
+// MakeContext 设置http上下文
+func (e *Api) MakeContext(c *gin.Context) *Api {
+	e.Context = c
+	e.Logger = GetRequestLogger(c)
+	return e
+}
+
+// GetLogger 获取上下文提供的日志
+func (e Api) GetLogger() *logger.Helper {
+	return GetRequestLogger(e.Context)
+}
+
+// Bind 参数校验
+func (e *Api) Bind(d interface{}, bindings ...binding.Binding) *Api {
+	var err error
+	if len(bindings) == 0 {
+		bindings = constructor.GetBindingForGin(d)
+	}
+	for i := range bindings {
+		if bindings[i] == nil {
+			err = e.Context.ShouldBindUri(d)
+		} else {
+			err = e.Context.ShouldBindWith(d, bindings[i])
+		}
+		if err != nil && err.Error() == "EOF" {
+			e.Logger.Warn("request body is not present anymore. ")
+			err = nil
+			continue
+		}
+		if err != nil {
+			e.AddError(err)
+			break
+		}
+	}
+	//vd.SetErrorFactory(func(failPath, msg string) error {
+	//	return fmt.Errorf(`"validation failed: %s %s"`, failPath, msg)
+	//})
+	if err1 := vd.Validate(d); err1 != nil {
+		e.AddError(err1)
+	}
+	return e
+}
+
+// GetOrm 获取Orm DB
+func (e Api) GetOrm() (*gorm.DB, error) {
+	ormDB, err := pkg.GetOrm(e.Context)
+	if err != nil {
+		e.Logger.Error(http.StatusInternalServerError, err, "数据库连接获取失败")
+		return nil, err
+	}
+	return ormDB, nil
+}
+
+// MakeOrm 设置Orm DB
+func (e *Api) MakeOrm() *Api {
+	var err error
+	if e.Logger == nil {
+		err = errors.New("at MakeOrm logger is nil")
+		e.AddError(err)
+		return e
+	}
+	ormDB, err := pkg.GetOrm(e.Context)
+	if err != nil {
+		e.Logger.Error(http.StatusInternalServerError, err, "数据库连接获取失败")
+		e.AddError(err)
+	}
+	e.Orm = ormDB
+	return e
+}
+
+func (e *Api) MakeService(c *service.Service) *Api {
+	c.Log = e.Logger
+	c.Orm = e.Orm
+	c.Cache = sdk.Runtime.GetCacheAdapter()
+	e.Cache = c.Cache
+	return e
+}
+
+// Error 通常错误数据处理
+func (e Api) Error(code int, err error, msg string) {
+	response.Error(e.Context, code, err, msg)
+}
+
+// OK 通常成功数据处理
+func (e Api) OK(data interface{}, msg string) {
+	response.OK(e.Context, data, msg)
+}
+
+// PageOK 分页数据处理
+func (e Api) PageOK(result interface{}, count int, pageIndex int, pageSize int, msg string) {
+	response.PageOK(e.Context, result, count, pageIndex, pageSize, msg)
+}
+
+// Custom 兼容函数
+func (e Api) Custom(data gin.H) {
+	response.Custum(e.Context, data)
+}
+
+func (e Api) Translate(form, to interface{}) {
+	pkg.Translate(form, to)
+}
+
+// getAcceptLanguage 获取当前语言
+func (e *Api) getAcceptLanguage() string {
+	languages := language.ParseAcceptLanguage(e.Context.GetHeader("Accept-Language"), nil)
+	if len(languages) == 0 {
+		return DefaultLanguage
+	}
+	return languages[0]
+}

+ 109 - 0
api/binding.go

@@ -0,0 +1,109 @@
+package api
+
+import (
+	"fmt"
+	"github.com/gin-gonic/gin/binding"
+	"reflect"
+	"strings"
+	"sync"
+)
+
+const (
+	_ uint8 = iota
+	json
+	xml
+	yaml
+	form
+	query
+)
+
+var constructor = &bindConstructor{}
+
+type bindConstructor struct {
+	cache map[string][]uint8
+	mux   sync.Mutex
+}
+
+func (e *bindConstructor) GetBindingForGin(d interface{}) []binding.Binding {
+	bs := e.getBinding(reflect.TypeOf(d).String())
+	if bs == nil {
+		//重新构建
+		bs = e.resolve(d)
+	}
+	gbs := make([]binding.Binding, 0)
+	mp := make(map[uint8]binding.Binding, 0)
+	for _, b := range bs {
+		switch b {
+		case json:
+			mp[json] = binding.JSON
+		case xml:
+			mp[xml] = binding.XML
+		case yaml:
+			mp[yaml] = binding.YAML
+		case form:
+			mp[form] = binding.Form
+		case query:
+			mp[query] = binding.Query
+		default:
+			mp[0] = nil
+		}
+	}
+	for e := range mp {
+		gbs=append(gbs, mp[e])
+	}
+	return gbs
+}
+
+func (e *bindConstructor) resolve(d interface{}) []uint8 {
+	bs := make([]uint8, 0)
+	qType := reflect.TypeOf(d).Elem()
+	var tag reflect.StructTag
+	var ok bool
+	fmt.Println(qType.Kind())
+	for i := 0; i < qType.NumField(); i++ {
+		tag = qType.Field(i).Tag
+		if _, ok = tag.Lookup("json"); ok {
+			bs = append(bs, json)
+		}
+		if _, ok = tag.Lookup("xml"); ok {
+			bs = append(bs, xml)
+		}
+		if _, ok = tag.Lookup("yaml"); ok {
+			bs = append(bs, yaml)
+		}
+		if _, ok = tag.Lookup("form"); ok {
+			bs = append(bs, form)
+		}
+		if _, ok = tag.Lookup("query"); ok {
+			bs = append(bs, query)
+		}
+		if _, ok = tag.Lookup("uri"); ok {
+			bs = append(bs, 0)
+		}
+		if t, ok := tag.Lookup("binding"); ok && strings.Index(t, "dive") > -1 {
+			qValue := reflect.ValueOf(d)
+			bs = append(bs, e.resolve(qValue.Field(i))...)
+			continue
+		}
+		if t, ok := tag.Lookup("validate"); ok && strings.Index(t, "dive") > -1 {
+			qValue := reflect.ValueOf(d)
+			bs = append(bs, e.resolve(qValue.Field(i))...)
+		}
+	}
+	return bs
+}
+
+func (e *bindConstructor) getBinding(name string) []uint8 {
+	e.mux.Lock()
+	defer e.mux.Unlock()
+	return e.cache[name]
+}
+
+func (e *bindConstructor) setBinding(name string, bs []uint8) {
+	e.mux.Lock()
+	defer e.mux.Unlock()
+	if e.cache == nil {
+		e.cache = make(map[string][]uint8)
+	}
+	e.cache[name] = bs
+}

+ 49 - 0
api/binding_test.go

@@ -0,0 +1,49 @@
+package api
+
+import (
+	"fmt"
+	"testing"
+
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+type Pagination struct {
+	PageIndex int `form:"pageIndex"`
+	PageSize  int `form:"pageSize"`
+}
+
+type SysUserSearch struct {
+	Pagination `search:"-"`
+	UserId     int    `form:"userId" search:"type:exact;column:id;table:sys_user" comment:"用户ID"`
+	Username   string `form:"username" search:"type:contains;column:username;table:sys_user" comment:"用户名"`
+	NickName   string `form:"nickName" search:"type:contains;column:nick_name;table:sys_user" comment:"昵称"`
+	Phone      string `form:"phone" search:"type:contains;column:phone;table:sys_user" comment:"手机号"`
+	RoleId     string `form:"roleId" search:"type:exact;column:role_id;table:sys_user" comment:"角色ID"`
+	Sex        string `form:"sex" search:"type:exact;column:sex;table:sys_user" comment:"性别"`
+	Email      string `form:"email" search:"type:contains;column:email;table:sys_user" comment:"邮箱"`
+	OrganId    string `form:"organId" search:"type:exact;column:organ_id;table:sys_user" comment:"机构"`
+	PostId     string `form:"postId" search:"type:exact;column:post_id;table:sys_user" comment:"岗位"`
+	Status     string `form:"status" search:"type:exact;column:status;table:sys_user" comment:"状态"`
+	SysUserOrder
+}
+
+type SysUserOrder struct {
+	UserIdOrder    string `search:"type:order;column:id;table:sys_user" form:"userIdOrder"`
+	UsernameOrder  string `search:"type:order;column:username;table:sys_user" form:"usernameOrder"`
+	StatusOrder    string `search:"type:order;column:status;table:sys_user" form:"statusOrder"`
+	CreatedAtOrder string `search:"type:order;column:created_at;table:sys_user" form:"createdAtOrder"`
+}
+
+func TestResolve(t *testing.T) {
+	// Only pass t into top-level Convey calls
+	Convey("Given some integer with a starting value", t, func() {
+
+		d := SysUserSearch{}
+
+		list := constructor.GetBindingForGin(d)
+		for _, binding := range list {
+			fmt.Printf("%v /n", binding)
+		}
+
+	})
+}

+ 40 - 0
api/request_logger.go

@@ -0,0 +1,40 @@
+package api
+
+import (
+	"strings"
+
+	"github.com/gin-gonic/gin"
+	"git.baozhida.cn/OAuth-core/logger"
+	"git.baozhida.cn/OAuth-core/pkg"
+	"git.baozhida.cn/OAuth-core/sdk"
+)
+
+type loggerKey struct{}
+
+// GetRequestLogger 获取上下文提供的日志
+func GetRequestLogger(c *gin.Context) *logger.Helper {
+	var log *logger.Helper
+	l, ok := c.Get(pkg.LoggerKey)
+	if ok {
+		ok = false
+		log, ok = l.(*logger.Helper)
+		if ok {
+			return log
+		}
+	}
+	//如果没有在上下文中放入logger
+	requestId := pkg.GenerateMsgIDFromContext(c)
+	log = logger.NewHelper(sdk.Runtime.GetLogger()).WithFields(map[string]interface{}{
+		strings.ToLower(pkg.TrafficKey): requestId,
+	})
+	return log
+}
+
+// SetRequestLogger 设置logger中间件
+func SetRequestLogger(c *gin.Context) {
+	requestId := pkg.GenerateMsgIDFromContext(c)
+	log := logger.NewHelper(sdk.Runtime.GetLogger()).WithFields(map[string]interface{}{
+		strings.ToLower(pkg.TrafficKey): requestId,
+	})
+	c.Set(pkg.LoggerKey, log)
+}

+ 42 - 0
api/translate.go

@@ -0,0 +1,42 @@
+package api
+
+import (
+	"fmt"
+
+	"github.com/gin-gonic/gin/binding"
+	"github.com/go-playground/locales/en"
+	"github.com/go-playground/locales/zh"
+	ut "github.com/go-playground/universal-translator"
+	"github.com/go-playground/validator/v10"
+	enTranslations "github.com/go-playground/validator/v10/translations/en"
+	chTranslations "github.com/go-playground/validator/v10/translations/zh"
+)
+
+// transInit local 通常取决于 http 请求头的 'Accept-Language'
+func transInit(local string) (trans ut.Translator, err error) {
+	if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
+		zhT := zh.New() //chinese
+		enT := en.New() //english
+		uni := ut.New(enT, zhT, enT)
+
+		var o bool
+		//register translate
+		// 注册翻译器
+		switch local {
+		case "zh", "zh-CN":
+			trans, o = uni.GetTranslator("zh")
+			if !o {
+				return nil, fmt.Errorf("uni.GetTranslator(%s) failed", "zh")
+			}
+			err = chTranslations.RegisterDefaultTranslations(v, trans)
+		default:
+			trans, o = uni.GetTranslator("en")
+			if !o {
+				return nil, fmt.Errorf("uni.GetTranslator(%s) failed", "en")
+			}
+			err = enTranslations.RegisterDefaultTranslations(v, trans)
+		}
+		return
+	}
+	return
+}

+ 24 - 0
config/README.md

@@ -0,0 +1,24 @@
+###
+
+测试用例:
+```
+import (
+	"fmt"
+	"testing"
+	
+	"git.baozhida.cn/OAuth-core/config"
+	"git.baozhida.cn/OAuth-core/config/source/file"
+)
+
+func TestApp(t *testing.T)  {
+	c, err := config.NewConfig()
+	if err != nil {
+		t.Error(err)
+	}
+	err = c.Load(file.NewSource(file.WithPath("config/settings.yml")))
+	if err != nil {
+		t.Error(err)
+	}
+	fmt.Println(c.Map())
+}
+```

+ 107 - 0
config/config.go

@@ -0,0 +1,107 @@
+// Package config is an interface for dynamic configuration.
+package config
+
+import (
+	"context"
+
+	"git.baozhida.cn/OAuth-core/config/loader"
+	"git.baozhida.cn/OAuth-core/config/reader"
+	"git.baozhida.cn/OAuth-core/config/source"
+	"git.baozhida.cn/OAuth-core/config/source/file"
+)
+
+// Config is an interface abstraction for dynamic configuration
+type Config interface {
+	// Values provide the reader.Values interface
+	reader.Values
+	// Init the config
+	Init(opts ...Option) error
+	// Options in the config
+	Options() Options
+	// Close Stop the config loader/watcher
+	Close() error
+	// Load config sources
+	Load(source ...source.Source) error
+	// Sync Force a source changeset sync
+	Sync() error
+	// Watch a value for changes
+	Watch(path ...string) (Watcher, error)
+}
+
+// Watcher is the config watcher
+type Watcher interface {
+	Next() (reader.Value, error)
+	Stop() error
+}
+
+// Entity 配置实体
+type Entity interface {
+	OnChange()
+}
+
+// Options 配置的参数
+type Options struct {
+	Loader loader.Loader
+	Reader reader.Reader
+	Source []source.Source
+
+	// for alternative data
+	Context context.Context
+
+	Entity Entity
+}
+
+// Option 调用类型
+type Option func(o *Options)
+
+var (
+	// DefaultConfig Default Config Manager
+	DefaultConfig Config
+)
+
+// NewConfig returns new config
+func NewConfig(opts ...Option) (Config, error) {
+	return newConfig(opts...)
+}
+
+// Bytes Return config as raw json
+func Bytes() []byte {
+	return DefaultConfig.Bytes()
+}
+
+// Map Return config as a map
+func Map() map[string]interface{} {
+	return DefaultConfig.Map()
+}
+
+// Scan values to a go type
+func Scan(v interface{}) error {
+	return DefaultConfig.Scan(v)
+}
+
+// Sync Force a source changeset sync
+func Sync() error {
+	return DefaultConfig.Sync()
+}
+
+// Get a value from the config
+func Get(path ...string) reader.Value {
+	return DefaultConfig.Get(path...)
+}
+
+// Load config sources
+func Load(source ...source.Source) error {
+	return DefaultConfig.Load(source...)
+}
+
+// Watch a value for changes
+func Watch(path ...string) (Watcher, error) {
+	return DefaultConfig.Watch(path...)
+}
+
+// LoadFile is short hand for creating a file source and loading it
+func LoadFile(path string) error {
+	return Load(file.NewSource(
+		file.WithPath(path),
+	))
+}

+ 309 - 0
config/default.go

@@ -0,0 +1,309 @@
+package config
+
+import (
+	"bytes"
+	"sync"
+	"time"
+
+	"git.baozhida.cn/OAuth-core/config/loader"
+	"git.baozhida.cn/OAuth-core/config/loader/memory"
+	"git.baozhida.cn/OAuth-core/config/reader"
+	"git.baozhida.cn/OAuth-core/config/reader/json"
+	"git.baozhida.cn/OAuth-core/config/source"
+)
+
+type config struct {
+	exit chan bool
+	opts Options
+
+	sync.RWMutex
+	// the current snapshot
+	snap *loader.Snapshot
+	// the current values
+	vals reader.Values
+}
+
+type watcher struct {
+	lw    loader.Watcher
+	rd    reader.Reader
+	path  []string
+	value reader.Value
+}
+
+func newConfig(opts ...Option) (Config, error) {
+	var c config
+
+	err := c.Init(opts...)
+	if err != nil {
+		return nil, err
+	}
+	go c.run()
+
+	return &c, nil
+}
+
+func (c *config) Init(opts ...Option) error {
+	c.opts = Options{
+		Reader: json.NewReader(),
+	}
+	c.exit = make(chan bool)
+	for _, o := range opts {
+		o(&c.opts)
+	}
+
+	// default loader uses the configured reader
+	if c.opts.Loader == nil {
+		c.opts.Loader = memory.NewLoader(memory.WithReader(c.opts.Reader))
+	}
+
+	err := c.opts.Loader.Load(c.opts.Source...)
+	if err != nil {
+		return err
+	}
+
+	c.snap, err = c.opts.Loader.Snapshot()
+	if err != nil {
+		return err
+	}
+
+	c.vals, err = c.opts.Reader.Values(c.snap.ChangeSet)
+	if err != nil {
+		return err
+	}
+	if c.opts.Entity != nil {
+		_ = c.vals.Scan(c.opts.Entity)
+	}
+
+	return nil
+}
+
+func (c *config) Options() Options {
+	return c.opts
+}
+
+func (c *config) run() {
+	watch := func(w loader.Watcher) error {
+		for {
+			// get changeset
+			snap, err := w.Next()
+			if err != nil {
+				return err
+			}
+
+			c.Lock()
+
+			if c.snap.Version >= snap.Version {
+				c.Unlock()
+				continue
+			}
+
+			// save
+			c.snap = snap
+
+			// set values
+			c.vals, _ = c.opts.Reader.Values(snap.ChangeSet)
+			if c.opts.Entity != nil {
+				_ = c.vals.Scan(c.opts.Entity)
+				c.opts.Entity.OnChange()
+			}
+
+			c.Unlock()
+		}
+	}
+
+	for {
+		w, err := c.opts.Loader.Watch()
+		if err != nil {
+			time.Sleep(time.Second)
+			continue
+		}
+
+		done := make(chan bool)
+
+		// the stop watch func
+		go func() {
+			select {
+			case <-done:
+			case <-c.exit:
+			}
+			_ = w.Stop()
+		}()
+
+		// block watch
+		if err := watch(w); err != nil {
+			// do something better
+			time.Sleep(time.Second)
+		}
+
+		// close done chan
+		close(done)
+
+		// if the config is closed exit
+		select {
+		case <-c.exit:
+			return
+		default:
+		}
+	}
+}
+
+func (c *config) Map() map[string]interface{} {
+	c.RLock()
+	defer c.RUnlock()
+	return c.vals.Map()
+}
+
+func (c *config) Scan(v interface{}) error {
+	c.RLock()
+	defer c.RUnlock()
+	return c.vals.Scan(v)
+}
+
+// Sync sync loads all the sources, calls the parser and updates the config
+func (c *config) Sync() error {
+	if err := c.opts.Loader.Sync(); err != nil {
+		return err
+	}
+
+	snap, err := c.opts.Loader.Snapshot()
+	if err != nil {
+		return err
+	}
+
+	c.Lock()
+	defer c.Unlock()
+
+	c.snap = snap
+	vals, err := c.opts.Reader.Values(snap.ChangeSet)
+	if err != nil {
+		return err
+	}
+	c.vals = vals
+
+	return nil
+}
+
+func (c *config) Close() error {
+	select {
+	case <-c.exit:
+		return nil
+	default:
+		close(c.exit)
+	}
+	return nil
+}
+
+func (c *config) Get(path ...string) reader.Value {
+	c.RLock()
+	defer c.RUnlock()
+
+	// did sync actually work?
+	if c.vals != nil {
+		return c.vals.Get(path...)
+	}
+
+	// no value
+	return newValue()
+}
+
+func (c *config) Set(val interface{}, path ...string) {
+	c.Lock()
+	defer c.Unlock()
+
+	if c.vals != nil {
+		c.vals.Set(val, path...)
+	}
+
+	return
+}
+
+func (c *config) Del(path ...string) {
+	c.Lock()
+	defer c.Unlock()
+
+	if c.vals != nil {
+		c.vals.Del(path...)
+	}
+
+	return
+}
+
+func (c *config) Bytes() []byte {
+	c.RLock()
+	defer c.RUnlock()
+
+	if c.vals == nil {
+		return []byte{}
+	}
+
+	return c.vals.Bytes()
+}
+
+func (c *config) Load(sources ...source.Source) error {
+	if err := c.opts.Loader.Load(sources...); err != nil {
+		return err
+	}
+
+	snap, err := c.opts.Loader.Snapshot()
+	if err != nil {
+		return err
+	}
+
+	c.Lock()
+	defer c.Unlock()
+
+	c.snap = snap
+	vals, err := c.opts.Reader.Values(snap.ChangeSet)
+	if err != nil {
+		return err
+	}
+	c.vals = vals
+
+	return nil
+}
+
+func (c *config) Watch(path ...string) (Watcher, error) {
+	value := c.Get(path...)
+
+	w, err := c.opts.Loader.Watch(path...)
+	if err != nil {
+		return nil, err
+	}
+
+	return &watcher{
+		lw:    w,
+		rd:    c.opts.Reader,
+		path:  path,
+		value: value,
+	}, nil
+}
+
+func (c *config) String() string {
+	return "config"
+}
+
+func (w *watcher) Next() (reader.Value, error) {
+	for {
+		s, err := w.lw.Next()
+		if err != nil {
+			return nil, err
+		}
+
+		// only process changes
+		if bytes.Equal(w.value.Bytes(), s.ChangeSet.Data) {
+			continue
+		}
+
+		v, err := w.rd.Values(s.ChangeSet)
+		if err != nil {
+			return nil, err
+		}
+
+		w.value = v.Get()
+		return w.value, nil
+	}
+}
+
+func (w *watcher) Stop() error {
+	return w.lw.Stop()
+}

+ 166 - 0
config/default_test.go

@@ -0,0 +1,166 @@
+package config
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"runtime"
+	"strings"
+	"testing"
+	"time"
+
+	"git.baozhida.cn/OAuth-core/config/source"
+	"git.baozhida.cn/OAuth-core/config/source/env"
+	"git.baozhida.cn/OAuth-core/config/source/file"
+	"git.baozhida.cn/OAuth-core/config/source/memory"
+)
+
+func createFileForIssue18(t *testing.T, content string) *os.File {
+	data := []byte(content)
+	path := filepath.Join(os.TempDir(), fmt.Sprintf("file.%d", time.Now().UnixNano()))
+	fh, err := os.Create(path)
+	if err != nil {
+		t.Error(err)
+	}
+	_, err = fh.Write(data)
+	if err != nil {
+		t.Error(err)
+	}
+
+	return fh
+}
+
+func createFileForTest(t *testing.T) *os.File {
+	data := []byte(`{"foo": "bar"}`)
+	path := filepath.Join(os.TempDir(), fmt.Sprintf("file.%d", time.Now().UnixNano()))
+	fh, err := os.Create(path)
+	if err != nil {
+		t.Error(err)
+	}
+	_, err = fh.Write(data)
+	if err != nil {
+		t.Error(err)
+	}
+
+	return fh
+}
+
+func TestConfigLoadWithGoodFile(t *testing.T) {
+	fh := createFileForTest(t)
+	path := fh.Name()
+	defer func() {
+		fh.Close()
+		os.Remove(path)
+	}()
+
+	// Create new config
+	conf, err := NewConfig()
+	if err != nil {
+		t.Fatalf("Expected no error but got %v", err)
+	}
+	// Load file source
+	if err := conf.Load(file.NewSource(
+		file.WithPath(path),
+	)); err != nil {
+		t.Fatalf("Expected no error but got %v", err)
+	}
+}
+
+func TestConfigLoadWithInvalidFile(t *testing.T) {
+	fh := createFileForTest(t)
+	path := fh.Name()
+	defer func() {
+		fh.Close()
+		os.Remove(path)
+	}()
+
+	// Create new config
+	conf, err := NewConfig()
+	if err != nil {
+		t.Fatalf("Expected no error but got %v", err)
+	}
+	// Load file source
+	err = conf.Load(file.NewSource(
+		file.WithPath(path),
+		file.WithPath("/i/do/not/exists.json"),
+	))
+
+	if err == nil {
+		t.Fatal("Expected error but none !")
+	}
+	if !strings.Contains(fmt.Sprintf("%v", err), "/i/do/not/exists.json") {
+		t.Fatalf("Expected error to contain the unexisting file but got %v", err)
+	}
+}
+
+func TestConfigMerge(t *testing.T) {
+	fh := createFileForIssue18(t, `{
+  "amqp": {
+    "host": "rabbit.platform",
+    "port": 80
+  },
+  "handler": {
+    "exchange": "springCloudBus"
+  }
+}`)
+	path := fh.Name()
+	defer func() {
+		fh.Close()
+		os.Remove(path)
+	}()
+	os.Setenv("AMQP_HOST", "rabbit.testing.com")
+
+	conf, err := NewConfig()
+	if err != nil {
+		t.Fatalf("Expected no error but got %v", err)
+	}
+	if err := conf.Load(
+		file.NewSource(
+			file.WithPath(path),
+		),
+		env.NewSource(),
+	); err != nil {
+		t.Fatalf("Expected no error but got %v", err)
+	}
+
+	actualHost := conf.Get("amqp", "host").String("backup")
+	if actualHost != "rabbit.testing.com" {
+		t.Fatalf("Expected %v but got %v",
+			"rabbit.testing.com",
+			actualHost)
+	}
+}
+
+func equalS(t *testing.T, actual, expect string) {
+	if actual != expect {
+		t.Errorf("Expected %s but got %s", actual, expect)
+	}
+}
+
+func TestConfigWatcherDirtyOverrite(t *testing.T) {
+	n := runtime.GOMAXPROCS(0)
+	defer runtime.GOMAXPROCS(n)
+
+	runtime.GOMAXPROCS(1)
+
+	l := 100
+
+	ss := make([]source.Source, l, l)
+
+	for i := 0; i < l; i++ {
+		ss[i] = memory.NewSource(memory.WithJSON([]byte(fmt.Sprintf(`{"key%d": "val%d"}`, i, i))))
+	}
+
+	conf, _ := NewConfig()
+
+	for _, s := range ss {
+		_ = conf.Load(s)
+	}
+	runtime.Gosched()
+
+	for i, _ := range ss {
+		k := fmt.Sprintf("key%d", i)
+		v := fmt.Sprintf("val%d", i)
+		equalS(t, conf.Get(k).String(""), v)
+	}
+}

+ 8 - 0
config/encoder/encoder.go

@@ -0,0 +1,8 @@
+// Package encoder handles source encoding formats
+package encoder
+
+type Encoder interface {
+	Encode(interface{}) ([]byte, error)
+	Decode([]byte, interface{}) error
+	String() string
+}

+ 24 - 0
config/encoder/json/json.go

@@ -0,0 +1,24 @@
+package json
+
+import (
+	"encoding/json"
+	"git.baozhida.cn/OAuth-core/config/encoder"
+)
+
+type jsonEncoder struct{}
+
+func (j jsonEncoder) Encode(v interface{}) ([]byte, error) {
+	return json.Marshal(v)
+}
+
+func (j jsonEncoder) Decode(d []byte, v interface{}) error {
+	return json.Unmarshal(d, v)
+}
+
+func (j jsonEncoder) String() string {
+	return "json"
+}
+
+func NewEncoder() encoder.Encoder {
+	return jsonEncoder{}
+}

+ 32 - 0
config/encoder/toml/toml.go

@@ -0,0 +1,32 @@
+package toml
+
+import (
+	"bytes"
+
+	"github.com/BurntSushi/toml"
+	"git.baozhida.cn/OAuth-core/config/encoder"
+)
+
+type tomlEncoder struct{}
+
+func (t tomlEncoder) Encode(v interface{}) ([]byte, error) {
+	b := bytes.NewBuffer(nil)
+	defer b.Reset()
+	err := toml.NewEncoder(b).Encode(v)
+	if err != nil {
+		return nil, err
+	}
+	return b.Bytes(), nil
+}
+
+func (t tomlEncoder) Decode(d []byte, v interface{}) error {
+	return toml.Unmarshal(d, v)
+}
+
+func (t tomlEncoder) String() string {
+	return "toml"
+}
+
+func NewEncoder() encoder.Encoder {
+	return tomlEncoder{}
+}

+ 25 - 0
config/encoder/xml/xml.go

@@ -0,0 +1,25 @@
+package xml
+
+import (
+	"encoding/xml"
+
+	"git.baozhida.cn/OAuth-core/config/encoder"
+)
+
+type xmlEncoder struct{}
+
+func (x xmlEncoder) Encode(v interface{}) ([]byte, error) {
+	return xml.Marshal(v)
+}
+
+func (x xmlEncoder) Decode(d []byte, v interface{}) error {
+	return xml.Unmarshal(d, v)
+}
+
+func (x xmlEncoder) String() string {
+	return "xml"
+}
+
+func NewEncoder() encoder.Encoder {
+	return xmlEncoder{}
+}

+ 24 - 0
config/encoder/yaml/yaml.go

@@ -0,0 +1,24 @@
+package yaml
+
+import (
+	"github.com/ghodss/yaml"
+	"git.baozhida.cn/OAuth-core/config/encoder"
+)
+
+type yamlEncoder struct{}
+
+func (y yamlEncoder) Encode(v interface{}) ([]byte, error) {
+	return yaml.Marshal(v)
+}
+
+func (y yamlEncoder) Decode(d []byte, v interface{}) error {
+	return yaml.Unmarshal(d, v)
+}
+
+func (y yamlEncoder) String() string {
+	return "yaml"
+}
+
+func NewEncoder() encoder.Encoder {
+	return yamlEncoder{}
+}

+ 63 - 0
config/loader/loader.go

@@ -0,0 +1,63 @@
+// package loader manages loading from multiple sources
+package loader
+
+import (
+	"context"
+
+	"git.baozhida.cn/OAuth-core/config/reader"
+	"git.baozhida.cn/OAuth-core/config/source"
+)
+
+// Loader manages loading sources
+type Loader interface {
+	// Close Stop the loader
+	Close() error
+	// Load the sources
+	Load(...source.Source) error
+	// Snapshot A Snapshot of loaded config
+	Snapshot() (*Snapshot, error)
+	// Sync Force sync of sources
+	Sync() error
+	// Watch for changes
+	Watch(...string) (Watcher, error)
+	// String Name of loader
+	String() string
+}
+
+// Watcher lets you watch sources and returns a merged ChangeSet
+type Watcher interface {
+	// Next First call to next may return the current Snapshot
+	// If you are watching a path then only the data from
+	// that path is returned.
+	Next() (*Snapshot, error)
+	// Stop watching for changes
+	Stop() error
+}
+
+// Snapshot is a merged ChangeSet
+type Snapshot struct {
+	// The merged ChangeSet
+	ChangeSet *source.ChangeSet
+	// Version Deterministic and comparable version of the snapshot
+	Version string
+}
+
+type Options struct {
+	Reader reader.Reader
+	Source []source.Source
+
+	// for alternative data
+	Context context.Context
+}
+
+type Option func(o *Options)
+
+// Copy snapshot
+func Copy(s *Snapshot) *Snapshot {
+	cs := *(s.ChangeSet)
+
+	return &Snapshot{
+		ChangeSet: &cs,
+		Version:   s.Version,
+	}
+}

+ 454 - 0
config/loader/memory/memory.go

@@ -0,0 +1,454 @@
+package memory
+
+import (
+	"bytes"
+	"container/list"
+	"errors"
+	"fmt"
+	"strings"
+	"sync"
+	"time"
+
+	"git.baozhida.cn/OAuth-core/config/loader"
+	"git.baozhida.cn/OAuth-core/config/reader"
+	"git.baozhida.cn/OAuth-core/config/reader/json"
+	"git.baozhida.cn/OAuth-core/config/source"
+)
+
+type memory struct {
+	exit chan bool
+	opts loader.Options
+
+	sync.RWMutex
+	// the current snapshot
+	snap *loader.Snapshot
+	// the current values
+	vals reader.Values
+	// all the sets
+	sets []*source.ChangeSet
+	// all the sources
+	sources []source.Source
+
+	watchers *list.List
+}
+
+type updateValue struct {
+	version string
+	value   reader.Value
+}
+
+type watcher struct {
+	exit    chan bool
+	path    []string
+	value   reader.Value
+	reader  reader.Reader
+	version string
+	updates chan updateValue
+}
+
+func (m *memory) watch(idx int, s source.Source) {
+	// watches a source for changes
+	watch := func(idx int, s source.Watcher) error {
+		for {
+			// get changeset
+			cs, err := s.Next()
+			if err != nil {
+				return err
+			}
+
+			m.Lock()
+
+			// save
+			m.sets[idx] = cs
+
+			// merge sets
+			set, err := m.opts.Reader.Merge(m.sets...)
+			if err != nil {
+				m.Unlock()
+				return err
+			}
+
+			// set values
+			m.vals, _ = m.opts.Reader.Values(set)
+			m.snap = &loader.Snapshot{
+				ChangeSet: set,
+				Version:   genVer(),
+			}
+			m.Unlock()
+
+			// send watch updates
+			m.update()
+		}
+	}
+
+	for {
+		// watch the source
+		w, err := s.Watch()
+		if err != nil {
+			time.Sleep(time.Second)
+			continue
+		}
+
+		done := make(chan bool)
+
+		// the stop watch func
+		go func() {
+			select {
+			case <-done:
+			case <-m.exit:
+			}
+			_ = w.Stop()
+		}()
+
+		// block watch
+		if err := watch(idx, w); err != nil {
+			// do something better
+			time.Sleep(time.Second)
+		}
+
+		// close done chan
+		close(done)
+
+		// if the config is closed exit
+		select {
+		case <-m.exit:
+			return
+		default:
+		}
+	}
+}
+
+func (m *memory) loaded() bool {
+	var loaded bool
+	m.RLock()
+	if m.vals != nil {
+		loaded = true
+	}
+	m.RUnlock()
+	return loaded
+}
+
+// reload reads the sets and creates new values
+func (m *memory) reload() error {
+	m.Lock()
+
+	// merge sets
+	set, err := m.opts.Reader.Merge(m.sets...)
+	if err != nil {
+		m.Unlock()
+		return err
+	}
+
+	// set values
+	m.vals, _ = m.opts.Reader.Values(set)
+	m.snap = &loader.Snapshot{
+		ChangeSet: set,
+		Version:   genVer(),
+	}
+
+	m.Unlock()
+
+	// update watchers
+	m.update()
+
+	return nil
+}
+
+func (m *memory) update() {
+	watchers := make([]*watcher, 0, m.watchers.Len())
+
+	m.RLock()
+	for e := m.watchers.Front(); e != nil; e = e.Next() {
+		watchers = append(watchers, e.Value.(*watcher))
+	}
+
+	vals := m.vals
+	snap := m.snap
+	m.RUnlock()
+
+	for _, w := range watchers {
+		if w.version >= snap.Version {
+			continue
+		}
+
+		uv := updateValue{
+			version: m.snap.Version,
+			value:   vals.Get(w.path...),
+		}
+
+		select {
+		case w.updates <- uv:
+		default:
+		}
+	}
+}
+
+// Snapshot returns a snapshot of the current loaded config
+func (m *memory) Snapshot() (*loader.Snapshot, error) {
+	if m.loaded() {
+		m.RLock()
+		snap := loader.Copy(m.snap)
+		m.RUnlock()
+		return snap, nil
+	}
+
+	// not loaded, sync
+	if err := m.Sync(); err != nil {
+		return nil, err
+	}
+
+	// make copy
+	m.RLock()
+	snap := loader.Copy(m.snap)
+	m.RUnlock()
+
+	return snap, nil
+}
+
+// Sync loads all the sources, calls the parser and updates the config
+func (m *memory) Sync() error {
+	//nolint:prealloc
+	var sets []*source.ChangeSet
+
+	m.Lock()
+
+	// read the source
+	var gerr []string
+
+	for _, source := range m.sources {
+		ch, err := source.Read()
+		if err != nil {
+			gerr = append(gerr, err.Error())
+			continue
+		}
+		sets = append(sets, ch)
+	}
+
+	// merge sets
+	set, err := m.opts.Reader.Merge(sets...)
+	if err != nil {
+		m.Unlock()
+		return err
+	}
+
+	// set values
+	vals, err := m.opts.Reader.Values(set)
+	if err != nil {
+		m.Unlock()
+		return err
+	}
+	m.vals = vals
+	m.snap = &loader.Snapshot{
+		ChangeSet: set,
+		Version:   genVer(),
+	}
+
+	m.Unlock()
+
+	// update watchers
+	m.update()
+
+	if len(gerr) > 0 {
+		return fmt.Errorf("source loading errors: %s", strings.Join(gerr, "\n"))
+	}
+
+	return nil
+}
+
+func (m *memory) Close() error {
+	select {
+	case <-m.exit:
+		return nil
+	default:
+		close(m.exit)
+	}
+	return nil
+}
+
+func (m *memory) Get(path ...string) (reader.Value, error) {
+	if !m.loaded() {
+		if err := m.Sync(); err != nil {
+			return nil, err
+		}
+	}
+
+	m.Lock()
+	defer m.Unlock()
+
+	// did sync actually work?
+	if m.vals != nil {
+		return m.vals.Get(path...), nil
+	}
+
+	// assuming vals is nil
+	// create new vals
+
+	ch := m.snap.ChangeSet
+
+	// we are truly screwed, trying to load in a hacked way
+	v, err := m.opts.Reader.Values(ch)
+	if err != nil {
+		return nil, err
+	}
+
+	// lets set it just because
+	m.vals = v
+
+	if m.vals != nil {
+		return m.vals.Get(path...), nil
+	}
+
+	// ok we're going hardcore now
+
+	return nil, errors.New("no values")
+}
+
+func (m *memory) Load(sources ...source.Source) error {
+	var gerrors []string
+
+	for _, source := range sources {
+		set, err := source.Read()
+		if err != nil {
+			gerrors = append(gerrors,
+				fmt.Sprintf("error loading source %s: %v",
+					source,
+					err))
+			// continue processing
+			continue
+		}
+		m.Lock()
+		m.sources = append(m.sources, source)
+		m.sets = append(m.sets, set)
+		idx := len(m.sets) - 1
+		m.Unlock()
+		go m.watch(idx, source)
+	}
+
+	if err := m.reload(); err != nil {
+		gerrors = append(gerrors, err.Error())
+	}
+
+	// Return errors
+	if len(gerrors) != 0 {
+		return errors.New(strings.Join(gerrors, "\n"))
+	}
+	return nil
+}
+
+func (m *memory) Watch(path ...string) (loader.Watcher, error) {
+	value, err := m.Get(path...)
+	if err != nil {
+		return nil, err
+	}
+
+	m.Lock()
+
+	w := &watcher{
+		exit:    make(chan bool),
+		path:    path,
+		value:   value,
+		reader:  m.opts.Reader,
+		updates: make(chan updateValue, 1),
+		version: m.snap.Version,
+	}
+
+	e := m.watchers.PushBack(w)
+
+	m.Unlock()
+
+	go func() {
+		<-w.exit
+		m.Lock()
+		m.watchers.Remove(e)
+		m.Unlock()
+	}()
+
+	return w, nil
+}
+
+func (m *memory) String() string {
+	return "memory"
+}
+
+func (w *watcher) Next() (*loader.Snapshot, error) {
+	update := func(v reader.Value) *loader.Snapshot {
+		w.value = v
+
+		cs := &source.ChangeSet{
+			Data:      v.Bytes(),
+			Format:    w.reader.String(),
+			Source:    "memory",
+			Timestamp: time.Now(),
+		}
+		cs.Checksum = cs.Sum()
+
+		return &loader.Snapshot{
+			ChangeSet: cs,
+			Version:   w.version,
+		}
+
+	}
+
+	for {
+		select {
+		case <-w.exit:
+			return nil, errors.New("watcher stopped")
+
+		case uv := <-w.updates:
+			if uv.version <= w.version {
+				continue
+			}
+
+			v := uv.value
+
+			w.version = uv.version
+
+			if bytes.Equal(w.value.Bytes(), v.Bytes()) {
+				continue
+			}
+
+			return update(v), nil
+		}
+	}
+}
+
+func (w *watcher) Stop() error {
+	select {
+	case <-w.exit:
+	default:
+		close(w.exit)
+		close(w.updates)
+	}
+
+	return nil
+}
+
+func genVer() string {
+	return fmt.Sprintf("%d", time.Now().UnixNano())
+}
+
+func NewLoader(opts ...loader.Option) loader.Loader {
+	options := loader.Options{
+		Reader: json.NewReader(),
+	}
+
+	for _, o := range opts {
+		o(&options)
+	}
+
+	m := &memory{
+		exit:     make(chan bool),
+		opts:     options,
+		watchers: list.New(),
+		sources:  options.Source,
+	}
+
+	m.sets = make([]*source.ChangeSet, len(options.Source))
+
+	for i, s := range options.Source {
+		m.sets[i] = &source.ChangeSet{Source: s.String()}
+		go m.watch(i, s)
+	}
+
+	return m
+}

+ 21 - 0
config/loader/memory/options.go

@@ -0,0 +1,21 @@
+package memory
+
+import (
+	"git.baozhida.cn/OAuth-core/config/loader"
+	"git.baozhida.cn/OAuth-core/config/reader"
+	"git.baozhida.cn/OAuth-core/config/source"
+)
+
+// WithSource appends a source to list of sources
+func WithSource(s source.Source) loader.Option {
+	return func(o *loader.Options) {
+		o.Source = append(o.Source, s)
+	}
+}
+
+// WithReader sets the config reader
+func WithReader(r reader.Reader) loader.Option {
+	return func(o *loader.Options) {
+		o.Reader = r
+	}
+}

+ 35 - 0
config/options.go

@@ -0,0 +1,35 @@
+package config
+
+import (
+	"git.baozhida.cn/OAuth-core/config/loader"
+	"git.baozhida.cn/OAuth-core/config/reader"
+	"git.baozhida.cn/OAuth-core/config/source"
+)
+
+// WithLoader sets the loader for manager config
+func WithLoader(l loader.Loader) Option {
+	return func(o *Options) {
+		o.Loader = l
+	}
+}
+
+// WithSource appends a source to list of sources
+func WithSource(s source.Source) Option {
+	return func(o *Options) {
+		o.Source = append(o.Source, s)
+	}
+}
+
+// WithReader sets the config reader
+func WithReader(r reader.Reader) Option {
+	return func(o *Options) {
+		o.Reader = r
+	}
+}
+
+// WithEntity sets the config Entity
+func WithEntity(e Entity) Option {
+	return func(o *Options) {
+		o.Entity = e
+	}
+}

+ 85 - 0
config/reader/json/json.go

@@ -0,0 +1,85 @@
+package json
+
+import (
+	"errors"
+	"time"
+
+	"github.com/imdario/mergo"
+	"git.baozhida.cn/OAuth-core/config/encoder"
+	"git.baozhida.cn/OAuth-core/config/encoder/json"
+	"git.baozhida.cn/OAuth-core/config/reader"
+	"git.baozhida.cn/OAuth-core/config/source"
+)
+
+const readerTyp = "json"
+
+type jsonReader struct {
+	opts reader.Options
+	json encoder.Encoder
+}
+
+func (j *jsonReader) Merge(changes ...*source.ChangeSet) (*source.ChangeSet, error) {
+	var merged map[string]interface{}
+
+	for _, m := range changes {
+		if m == nil {
+			continue
+		}
+
+		if len(m.Data) == 0 {
+			continue
+		}
+
+		codec, ok := j.opts.Encoding[m.Format]
+		if !ok {
+			// fallback
+			codec = j.json
+		}
+
+		var data map[string]interface{}
+		if err := codec.Decode(m.Data, &data); err != nil {
+			return nil, err
+		}
+		if err := mergo.Map(&merged, data, mergo.WithOverride); err != nil {
+			return nil, err
+		}
+	}
+
+	b, err := j.json.Encode(merged)
+	if err != nil {
+		return nil, err
+	}
+
+	cs := &source.ChangeSet{
+		Timestamp: time.Now(),
+		Data:      b,
+		Source:    "json",
+		Format:    j.json.String(),
+	}
+	cs.Checksum = cs.Sum()
+
+	return cs, nil
+}
+
+func (j *jsonReader) Values(ch *source.ChangeSet) (reader.Values, error) {
+	if ch == nil {
+		return nil, errors.New("changeset is nil")
+	}
+	if ch.Format != "json" {
+		return nil, errors.New("unsupported format")
+	}
+	return newValues(ch)
+}
+
+func (j *jsonReader) String() string {
+	return "json"
+}
+
+// NewReader creates a json reader
+func NewReader(opts ...reader.Option) reader.Reader {
+	options := reader.NewOptions(opts...)
+	return &jsonReader{
+		json: json.NewEncoder(),
+		opts: options,
+	}
+}

+ 43 - 0
config/reader/json/json_test.go

@@ -0,0 +1,43 @@
+package json
+
+import (
+	"testing"
+
+	"git.baozhida.cn/OAuth-core/config/source"
+)
+
+func TestReader(t *testing.T) {
+	data := []byte(`{"foo": "bar", "baz": {"bar": "cat"}}`)
+
+	testData := []struct {
+		path  []string
+		value string
+	}{
+		{
+			[]string{"foo"},
+			"bar",
+		},
+		{
+			[]string{"baz", "bar"},
+			"cat",
+		},
+	}
+
+	r := NewReader()
+
+	c, err := r.Merge(&source.ChangeSet{Data: data}, &source.ChangeSet{})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	values, err := r.Values(c)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	for _, test := range testData {
+		if v := values.Get(test.path...).String(""); v != test.value {
+			t.Fatalf("Expected %s got %s for path %v", test.value, v, test.path)
+		}
+	}
+}

+ 201 - 0
config/reader/json/values.go

@@ -0,0 +1,201 @@
+package json
+
+import (
+	"encoding/json"
+	"fmt"
+	"strconv"
+	"strings"
+	"time"
+
+	simple "github.com/bitly/go-simplejson"
+	"git.baozhida.cn/OAuth-core/config/reader"
+	"git.baozhida.cn/OAuth-core/config/source"
+)
+
+type jsonValues struct {
+	ch *source.ChangeSet
+	sj *simple.Json
+}
+
+type jsonValue struct {
+	*simple.Json
+}
+
+func newValues(ch *source.ChangeSet) (reader.Values, error) {
+	sj := simple.New()
+	data, _ := reader.ReplaceEnvVars(ch.Data)
+	if err := sj.UnmarshalJSON(data); err != nil {
+		sj.SetPath(nil, string(ch.Data))
+	}
+	return &jsonValues{ch, sj}, nil
+}
+
+func (j *jsonValues) Get(path ...string) reader.Value {
+	return &jsonValue{j.sj.GetPath(path...)}
+}
+
+func (j *jsonValues) Del(path ...string) {
+	// delete the tree?
+	if len(path) == 0 {
+		j.sj = simple.New()
+		return
+	}
+
+	if len(path) == 1 {
+		j.sj.Del(path[0])
+		return
+	}
+
+	vals := j.sj.GetPath(path[:len(path)-1]...)
+	vals.Del(path[len(path)-1])
+	j.sj.SetPath(path[:len(path)-1], vals.Interface())
+	return
+}
+
+func (j *jsonValues) Set(val interface{}, path ...string) {
+	j.sj.SetPath(path, val)
+}
+
+func (j *jsonValues) Bytes() []byte {
+	b, _ := j.sj.MarshalJSON()
+	return b
+}
+
+func (j *jsonValues) Map() map[string]interface{} {
+	m, _ := j.sj.Map()
+	return m
+}
+
+func (j *jsonValues) Scan(v interface{}) error {
+	b, err := j.sj.MarshalJSON()
+	if err != nil {
+		return err
+	}
+	return json.Unmarshal(b, v)
+}
+
+func (j *jsonValues) String() string {
+	return "json"
+}
+
+func (j *jsonValue) Bool(def bool) bool {
+	b, err := j.Json.Bool()
+	if err == nil {
+		return b
+	}
+
+	str, ok := j.Interface().(string)
+	if !ok {
+		return def
+	}
+
+	b, err = strconv.ParseBool(str)
+	if err != nil {
+		return def
+	}
+
+	return b
+}
+
+func (j *jsonValue) Int(def int) int {
+	i, err := j.Json.Int()
+	if err == nil {
+		return i
+	}
+
+	str, ok := j.Interface().(string)
+	if !ok {
+		return def
+	}
+
+	i, err = strconv.Atoi(str)
+	if err != nil {
+		return def
+	}
+
+	return i
+}
+
+func (j *jsonValue) String(def string) string {
+	return j.Json.MustString(def)
+}
+
+func (j *jsonValue) Float64(def float64) float64 {
+	f, err := j.Json.Float64()
+	if err == nil {
+		return f
+	}
+
+	str, ok := j.Interface().(string)
+	if !ok {
+		return def
+	}
+
+	f, err = strconv.ParseFloat(str, 64)
+	if err != nil {
+		return def
+	}
+
+	return f
+}
+
+func (j *jsonValue) Duration(def time.Duration) time.Duration {
+	v, err := j.Json.String()
+	if err != nil {
+		return def
+	}
+
+	value, err := time.ParseDuration(v)
+	if err != nil {
+		return def
+	}
+
+	return value
+}
+
+func (j *jsonValue) StringSlice(def []string) []string {
+	v, err := j.Json.String()
+	if err == nil {
+		sl := strings.Split(v, ",")
+		if len(sl) > 1 {
+			return sl
+		}
+	}
+	return j.Json.MustStringArray(def)
+}
+
+func (j *jsonValue) StringMap(def map[string]string) map[string]string {
+	m, err := j.Json.Map()
+	if err != nil {
+		return def
+	}
+
+	res := map[string]string{}
+
+	for k, v := range m {
+		res[k] = fmt.Sprintf("%v", v)
+	}
+
+	return res
+}
+
+func (j *jsonValue) Scan(v interface{}) error {
+	b, err := j.Json.MarshalJSON()
+	if err != nil {
+		return err
+	}
+	return json.Unmarshal(b, v)
+}
+
+func (j *jsonValue) Bytes() []byte {
+	b, err := j.Json.Bytes()
+	if err != nil {
+		// try return marshalled
+		b, err = j.Json.MarshalJSON()
+		if err != nil {
+			return []byte{}
+		}
+		return b
+	}
+	return b
+}

+ 85 - 0
config/reader/json/values_test.go

@@ -0,0 +1,85 @@
+package json
+
+import (
+	"reflect"
+	"testing"
+
+	"git.baozhida.cn/OAuth-core/config/source"
+)
+
+func TestValues(t *testing.T) {
+	emptyStr := ""
+	testData := []struct {
+		csdata   []byte
+		path     []string
+		accepter interface{}
+		value    interface{}
+	}{
+		{
+			[]byte(`{"foo": "bar", "baz": {"bar": "cat"}}`),
+			[]string{"foo"},
+			emptyStr,
+			"bar",
+		},
+		{
+			[]byte(`{"foo": "bar", "baz": {"bar": "cat"}}`),
+			[]string{"baz", "bar"},
+			emptyStr,
+			"cat",
+		},
+	}
+
+	for idx, test := range testData {
+		values, err := newValues(&source.ChangeSet{
+			Data: test.csdata,
+		})
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		err = values.Get(test.path...).Scan(&test.accepter)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if test.accepter != test.value {
+			t.Fatalf("No.%d Expected %v got %v for path %v", idx, test.value, test.accepter, test.path)
+		}
+	}
+}
+
+func TestStructArray(t *testing.T) {
+	type T struct {
+		Foo string
+	}
+
+	emptyTSlice := []T{}
+
+	testData := []struct {
+		csdata   []byte
+		accepter []T
+		value    []T
+	}{
+		{
+			[]byte(`[{"foo": "bar"}]`),
+			emptyTSlice,
+			[]T{{Foo: "bar"}},
+		},
+	}
+
+	for idx, test := range testData {
+		values, err := newValues(&source.ChangeSet{
+			Data: test.csdata,
+		})
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		err = values.Get().Scan(&test.accepter)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if !reflect.DeepEqual(test.accepter, test.value) {
+			t.Fatalf("No.%d Expected %v got %v", idx, test.value, test.accepter)
+		}
+	}
+}

+ 40 - 0
config/reader/options.go

@@ -0,0 +1,40 @@
+package reader
+
+import (
+	"git.baozhida.cn/OAuth-core/config/encoder"
+	"git.baozhida.cn/OAuth-core/config/encoder/json"
+	"git.baozhida.cn/OAuth-core/config/encoder/toml"
+	"git.baozhida.cn/OAuth-core/config/encoder/xml"
+	"git.baozhida.cn/OAuth-core/config/encoder/yaml"
+)
+
+type Options struct {
+	Encoding map[string]encoder.Encoder
+}
+
+type Option func(o *Options)
+
+func NewOptions(opts ...Option) Options {
+	options := Options{
+		Encoding: map[string]encoder.Encoder{
+			"json": json.NewEncoder(),
+			"yaml": yaml.NewEncoder(),
+			"toml": toml.NewEncoder(),
+			"xml":  xml.NewEncoder(),
+			"yml":  yaml.NewEncoder(),
+		},
+	}
+	for _, o := range opts {
+		o(&options)
+	}
+	return options
+}
+
+func WithEncoder(e encoder.Encoder) Option {
+	return func(o *Options) {
+		if o.Encoding == nil {
+			o.Encoding = make(map[string]encoder.Encoder)
+		}
+		o.Encoding[e.String()] = e
+	}
+}

+ 23 - 0
config/reader/preprocessor.go

@@ -0,0 +1,23 @@
+package reader
+
+import (
+	"os"
+	"regexp"
+)
+
+func ReplaceEnvVars(raw []byte) ([]byte, error) {
+	re := regexp.MustCompile(`\$\{([A-Za-z0-9_]+)\}`)
+	if re.Match(raw) {
+		dataS := string(raw)
+		res := re.ReplaceAllStringFunc(dataS, replaceEnvVars)
+		return []byte(res), nil
+	} else {
+		return raw, nil
+	}
+}
+
+func replaceEnvVars(element string) string {
+	v := element[2 : len(element)-1]
+	el := os.Getenv(v)
+	return el
+}

+ 73 - 0
config/reader/preprocessor_test.go

@@ -0,0 +1,73 @@
+package reader
+
+import (
+	"os"
+	"strings"
+	"testing"
+)
+
+func TestReplaceEnvVars(t *testing.T) {
+	os.Setenv("myBar", "cat")
+	os.Setenv("MYBAR", "cat")
+	os.Setenv("my_Bar", "cat")
+	os.Setenv("myBar_", "cat")
+
+	testData := []struct {
+		expected string
+		data     []byte
+	}{
+		// Right use cases
+		{
+			`{"foo": "bar", "baz": {"bar": "cat"}}`,
+			[]byte(`{"foo": "bar", "baz": {"bar": "${myBar}"}}`),
+		},
+		{
+			`{"foo": "bar", "baz": {"bar": "cat"}}`,
+			[]byte(`{"foo": "bar", "baz": {"bar": "${MYBAR}"}}`),
+		},
+		{
+			`{"foo": "bar", "baz": {"bar": "cat"}}`,
+			[]byte(`{"foo": "bar", "baz": {"bar": "${my_Bar}"}}`),
+		},
+		{
+			`{"foo": "bar", "baz": {"bar": "cat"}}`,
+			[]byte(`{"foo": "bar", "baz": {"bar": "${myBar_}"}}`),
+		},
+		// Wrong use cases
+		{
+			`{"foo": "bar", "baz": {"bar": "${myBar-}"}}`,
+			[]byte(`{"foo": "bar", "baz": {"bar": "${myBar-}"}}`),
+		},
+		{
+			`{"foo": "bar", "baz": {"bar": "${}"}}`,
+			[]byte(`{"foo": "bar", "baz": {"bar": "${}"}}`),
+		},
+		{
+			`{"foo": "bar", "baz": {"bar": "$sss}"}}`,
+			[]byte(`{"foo": "bar", "baz": {"bar": "$sss}"}}`),
+		},
+		{
+			`{"foo": "bar", "baz": {"bar": "${sss"}}`,
+			[]byte(`{"foo": "bar", "baz": {"bar": "${sss"}}`),
+		},
+		{
+			`{"foo": "bar", "baz": {"bar": "{something}"}}`,
+			[]byte(`{"foo": "bar", "baz": {"bar": "{something}"}}`),
+		},
+		// Use cases without replace env vars
+		{
+			`{"foo": "bar", "baz": {"bar": "cat"}}`,
+			[]byte(`{"foo": "bar", "baz": {"bar": "cat"}}`),
+		},
+	}
+
+	for _, test := range testData {
+		res, err := ReplaceEnvVars(test.data)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if strings.Compare(test.expected, string(res)) != 0 {
+			t.Fatalf("Expected %s got %s", test.expected, res)
+		}
+	}
+}

+ 38 - 0
config/reader/reader.go

@@ -0,0 +1,38 @@
+// Package reader parses change sets and provides config values
+package reader
+
+import (
+	"time"
+
+	"git.baozhida.cn/OAuth-core/config/source"
+)
+
+// Reader is an interface for merging changesets
+type Reader interface {
+	Merge(...*source.ChangeSet) (*source.ChangeSet, error)
+	Values(*source.ChangeSet) (Values, error)
+	String() string
+}
+
+// Values is returned by the reader
+type Values interface {
+	Bytes() []byte
+	Get(path ...string) Value
+	Set(val interface{}, path ...string)
+	Del(path ...string)
+	Map() map[string]interface{}
+	Scan(v interface{}) error
+}
+
+// Value represents a value of any type
+type Value interface {
+	Bool(def bool) bool
+	Int(def int) int
+	String(def string) string
+	Float64(def float64) float64
+	Duration(def time.Duration) time.Duration
+	StringSlice(def []string) []string
+	StringMap(def map[string]string) map[string]string
+	Scan(val interface{}) error
+	Bytes() []byte
+}

+ 89 - 0
config/secrets/box/box.go

@@ -0,0 +1,89 @@
+// Package box is an asymmetric implementation of config/secrets using nacl/box
+package box
+
+import (
+	"github.com/pkg/errors"
+	naclbox "golang.org/x/crypto/nacl/box"
+	"git.baozhida.cn/OAuth-core/config/secrets"
+
+	"crypto/rand"
+)
+
+const keyLength = 32
+
+type box struct {
+	options secrets.Options
+
+	publicKey  [keyLength]byte
+	privateKey [keyLength]byte
+}
+
+// NewSecrets returns a nacl-box codec
+func NewSecrets(opts ...secrets.Option) secrets.Secrets {
+	b := &box{}
+	for _, o := range opts {
+		o(&b.options)
+	}
+	return b
+}
+
+// Init initialises a box
+func (b *box) Init(opts ...secrets.Option) error {
+	for _, o := range opts {
+		o(&b.options)
+	}
+	if len(b.options.PrivateKey) != keyLength || len(b.options.PublicKey) != keyLength {
+		return errors.Errorf("a public key and a private key of length %d must both be provided", keyLength)
+	}
+	copy(b.privateKey[:], b.options.PrivateKey)
+	copy(b.publicKey[:], b.options.PublicKey)
+	return nil
+}
+
+// Options returns options
+func (b *box) Options() secrets.Options {
+	return b.options
+}
+
+// String returns nacl-box
+func (*box) String() string {
+	return "nacl-box"
+}
+
+// Encrypt encrypts a message with the sender's private key and the receipient's public key
+func (b *box) Encrypt(in []byte, opts ...secrets.EncryptOption) ([]byte, error) {
+	var options secrets.EncryptOptions
+	for _, o := range opts {
+		o(&options)
+	}
+	if len(options.RecipientPublicKey) != keyLength {
+		return []byte{}, errors.New("recepient's public key must be provided")
+	}
+	var recipientPublicKey [keyLength]byte
+	copy(recipientPublicKey[:], options.RecipientPublicKey)
+	var nonce [24]byte
+	if _, err := rand.Reader.Read(nonce[:]); err != nil {
+		return []byte{}, errors.Wrap(err, "couldn't obtain a random nonce from crypto/rand")
+	}
+	return naclbox.Seal(nonce[:], in, &nonce, &recipientPublicKey, &b.privateKey), nil
+}
+
+// Decrypt Decrypts a message with the receiver's private key and the sender's public key
+func (b *box) Decrypt(in []byte, opts ...secrets.DecryptOption) ([]byte, error) {
+	var options secrets.DecryptOptions
+	for _, o := range opts {
+		o(&options)
+	}
+	if len(options.SenderPublicKey) != keyLength {
+		return []byte{}, errors.New("sender's public key bust be provided")
+	}
+	var nonce [24]byte
+	var senderPublicKey [32]byte
+	copy(nonce[:], in[:24])
+	copy(senderPublicKey[:], options.SenderPublicKey)
+	decrypted, ok := naclbox.Open(nil, in[24:], &nonce, &senderPublicKey, &b.privateKey)
+	if !ok {
+		return []byte{}, errors.New("incoming message couldn't be verified / decrypted")
+	}
+	return decrypted, nil
+}

+ 66 - 0
config/secrets/box/box_test.go

@@ -0,0 +1,66 @@
+package box
+
+import (
+	"crypto/rand"
+	"reflect"
+	"testing"
+
+	naclbox "golang.org/x/crypto/nacl/box"
+	"git.baozhida.cn/OAuth-core/config/secrets"
+)
+
+func TestBox(t *testing.T) {
+	alicePublicKey, alicePrivateKey, err := naclbox.GenerateKey(rand.Reader)
+	if err != nil {
+		t.Fatal(err)
+	}
+	bobPublicKey, bobPrivateKey, err := naclbox.GenerateKey(rand.Reader)
+	if err != nil {
+		t.Fatal(err)
+	}
+	alice, bob := NewSecrets(secrets.PublicKey(alicePublicKey[:]), secrets.PrivateKey(alicePrivateKey[:])), NewSecrets()
+	if err := alice.Init(); err != nil {
+		t.Error(err)
+	}
+	if err := bob.Init(secrets.PublicKey(bobPublicKey[:]), secrets.PrivateKey(bobPrivateKey[:])); err != nil {
+		t.Error(err)
+	}
+	if alice.String() != "nacl-box" {
+		t.Error("String() doesn't return nacl-box")
+	}
+	aliceSecret := []byte("Why is a raven like a writing-desk?")
+	if _, err := alice.Encrypt(aliceSecret); err == nil {
+		t.Error("alice.Encrypt succeded without a public key")
+	}
+	enc, err := alice.Encrypt(aliceSecret, secrets.RecipientPublicKey(bob.Options().PublicKey))
+	if err != nil {
+		t.Error("alice.Encrypt failed")
+	}
+	if _, err := bob.Decrypt(enc); err == nil {
+		t.Error("bob.Decrypt succeded without a public key")
+	}
+	if dec, err := bob.Decrypt(enc, secrets.SenderPublicKey(alice.Options().PublicKey)); err == nil {
+		if !reflect.DeepEqual(dec, aliceSecret) {
+			t.Errorf("Bob's decrypted message didn't match Alice's encrypted message: %v != %v", aliceSecret, dec)
+		}
+	} else {
+		t.Errorf("bob.Decrypt failed (%s)", err)
+	}
+
+	bobSecret := []byte("I haven't the slightest idea")
+	enc, err = bob.Encrypt(bobSecret, secrets.RecipientPublicKey(alice.Options().PublicKey))
+	if err != nil {
+		t.Error(err)
+	}
+	dec, err := alice.Decrypt(enc, secrets.SenderPublicKey(bob.Options().PrivateKey))
+	if err == nil {
+		t.Error(err)
+	}
+	dec, err = alice.Decrypt(enc, secrets.SenderPublicKey(bob.Options().PublicKey))
+	if err != nil {
+		t.Error(err)
+	}
+	if !reflect.DeepEqual(dec, bobSecret) {
+		t.Errorf("Alice's decrypted message didn't match Bob's encrypted message %v != %v", bobSecret, dec)
+	}
+}

+ 73 - 0
config/secrets/secretbox/secretbox.go

@@ -0,0 +1,73 @@
+// Package secretbox is a config/secrets implementation that uses nacl/secretbox
+// to do symmetric encryption / verification
+package secretbox
+
+import (
+	"github.com/pkg/errors"
+	"golang.org/x/crypto/nacl/secretbox"
+	"git.baozhida.cn/OAuth-core/config/secrets"
+
+	"crypto/rand"
+)
+
+const keyLength = 32
+
+type secretBox struct {
+	options secrets.Options
+
+	secretKey [keyLength]byte
+}
+
+// NewSecrets returns a secretbox codec
+func NewSecrets(opts ...secrets.Option) secrets.Secrets {
+	sb := &secretBox{}
+	for _, o := range opts {
+		o(&sb.options)
+	}
+	return sb
+}
+
+func (s *secretBox) Init(opts ...secrets.Option) error {
+	for _, o := range opts {
+		o(&s.options)
+	}
+	if len(s.options.Key) == 0 {
+		return errors.New("no secret key is defined")
+	}
+	if len(s.options.Key) != keyLength {
+		return errors.Errorf("secret key must be %d bytes long", keyLength)
+	}
+	copy(s.secretKey[:], s.options.Key)
+	return nil
+}
+
+func (s *secretBox) Options() secrets.Options {
+	return s.options
+}
+
+func (s *secretBox) String() string {
+	return "nacl-secretbox"
+}
+
+func (s *secretBox) Encrypt(in []byte, opts ...secrets.EncryptOption) ([]byte, error) {
+	// no opts are expected, so they are ignored
+
+	// there must be a unique nonce for each message
+	var nonce [24]byte
+	if _, err := rand.Reader.Read(nonce[:]); err != nil {
+		return []byte{}, errors.Wrap(err, "couldn't obtain a random nonce from crypto/rand")
+	}
+	return secretbox.Seal(nonce[:], in, &nonce, &s.secretKey), nil
+}
+
+func (s *secretBox) Decrypt(in []byte, opts ...secrets.DecryptOption) ([]byte, error) {
+	// no options are expected, so they are ignored
+
+	var decryptNonce [24]byte
+	copy(decryptNonce[:], in[:24])
+	decrypted, ok := secretbox.Open(nil, in[24:], &decryptNonce, &s.secretKey)
+	if !ok {
+		return []byte{}, errors.New("decryption failed (is the key set correctly?)")
+	}
+	return decrypted, nil
+}

+ 56 - 0
config/secrets/secretbox/secretbox_test.go

@@ -0,0 +1,56 @@
+package secretbox
+
+import (
+	"encoding/base64"
+	"reflect"
+	"testing"
+
+	"git.baozhida.cn/OAuth-core/config/secrets"
+)
+
+func TestSecretBox(t *testing.T) {
+	secretKey, err := base64.StdEncoding.DecodeString("4jbVgq8FsAV7vy+n8WqEZrl7BUtNqh3fYT5RXzXOPFY=")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	s := NewSecrets()
+
+	if err := s.Init(); err == nil {
+		t.Error("Secretbox accepted an empty secret key")
+	}
+	if err := s.Init(secrets.Key([]byte("invalid"))); err == nil {
+		t.Error("Secretbox accepted a secret key that is invalid")
+	}
+
+	if err := s.Init(secrets.Key(secretKey)); err != nil {
+		t.Fatal(err)
+	}
+
+	o := s.Options()
+	if !reflect.DeepEqual(o.Key, secretKey) {
+		t.Error("Init() didn't set secret key correctly")
+	}
+	if s.String() != "nacl-secretbox" {
+		t.Error(s.String() + " should be nacl-secretbox")
+	}
+
+	// Try 10 times to get different nonces
+	for i := 0; i < 10; i++ {
+		message := []byte(`Can you hear me, Major Tom?`)
+
+		encrypted, err := s.Encrypt(message)
+		if err != nil {
+			t.Errorf("Failed to encrypt message (%s)", err)
+		}
+
+		decrypted, err := s.Decrypt(encrypted)
+		if err != nil {
+			t.Errorf("Failed to decrypt encrypted message (%s)", err)
+		}
+
+		if !reflect.DeepEqual(message, decrypted) {
+			t.Errorf("Decrypted Message dod not match encrypted message")
+		}
+	}
+}

+ 88 - 0
config/secrets/secrets.go

@@ -0,0 +1,88 @@
+// Package secrets is an interface for encrypting and decrypting secrets
+package secrets
+
+import "context"
+
+// Secrets encrypts or decrypts arbitrary data. The data should be as small as possible
+type Secrets interface {
+	// Initialise options
+	Init(...Option) error
+	// Return the options
+	Options() Options
+	// Decrypt a value
+	Decrypt([]byte, ...DecryptOption) ([]byte, error)
+	// Encrypt a value
+	Encrypt([]byte, ...EncryptOption) ([]byte, error)
+	// Secrets implementation
+	String() string
+}
+
+type Options struct {
+	// Key is a symmetric key for encoding
+	Key []byte
+	// Private key for decoding
+	PrivateKey []byte
+	// Public key for encoding
+	PublicKey []byte
+	// Context for other opts
+	Context context.Context
+}
+
+// Option sets options
+type Option func(*Options)
+
+// Key sets the symmetric secret key
+func Key(k []byte) Option {
+	return func(o *Options) {
+		o.Key = make([]byte, len(k))
+		copy(o.Key, k)
+	}
+}
+
+// PublicKey sets the asymmetric Public Key of this codec
+func PublicKey(key []byte) Option {
+	return func(o *Options) {
+		o.PublicKey = make([]byte, len(key))
+		copy(o.PublicKey, key)
+	}
+}
+
+// PrivateKey sets the asymmetric Private Key of this codec
+func PrivateKey(key []byte) Option {
+	return func(o *Options) {
+		o.PrivateKey = make([]byte, len(key))
+		copy(o.PrivateKey, key)
+	}
+}
+
+// DecryptOptions can be passed to Secrets.Decrypt
+type DecryptOptions struct {
+	SenderPublicKey []byte
+}
+
+// DecryptOption sets DecryptOptions
+type DecryptOption func(*DecryptOptions)
+
+// SenderPublicKey is the Public Key of the Secrets that encrypted this message
+func SenderPublicKey(key []byte) DecryptOption {
+	return func(d *DecryptOptions) {
+		d.SenderPublicKey = make([]byte, len(key))
+		copy(d.SenderPublicKey, key)
+	}
+}
+
+// EncryptOptions can be passed to Secrets.Encrypt
+type EncryptOptions struct {
+	RecipientPublicKey []byte
+}
+
+// EncryptOption Sets EncryptOptions
+type EncryptOption func(*EncryptOptions)
+
+// RecipientPublicKey is the Public Key of the Secrets that will decrypt this message
+func RecipientPublicKey(key []byte) EncryptOption {
+	return func(e *EncryptOptions) {
+		e.RecipientPublicKey = make([]byte, len(key))
+		copy(e.RecipientPublicKey, key)
+	}
+}

+ 13 - 0
config/source/changeset.go

@@ -0,0 +1,13 @@
+package source
+
+import (
+	"crypto/md5"
+	"fmt"
+)
+
+// Sum returns the md5 checksum of the ChangeSet data
+func (c *ChangeSet) Sum() string {
+	h := md5.New()
+	h.Write(c.Data)
+	return fmt.Sprintf("%x", h.Sum(nil))
+}

+ 96 - 0
config/source/env/README.md

@@ -0,0 +1,96 @@
+# Env Source
+
+The env source reads config from environment variables
+
+## Format
+
+We expect environment variables to be in the standard format of FOO=bar
+
+Keys are converted to lowercase and split on underscore.
+
+
+### Example
+
+```
+DATABASE_ADDRESS=127.0.0.1
+DATABASE_PORT=3306
+```
+
+Becomes
+
+```json
+{
+    "database": {
+        "address": "127.0.0.1",
+        "port": 3306
+    }
+}
+```
+
+## Prefixes
+
+Environment variables can be namespaced so we only have access to a subset. Two options are available:
+
+```
+WithPrefix(p ...string)
+WithStrippedPrefix(p ...string)
+```
+
+The former will preserve the prefix and make it a top level key in the config. The latter eliminates the prefix, reducing the nesting by one. 
+
+#### Example:
+
+Given ENVs of:
+
+```
+APP_DATABASE_ADDRESS=127.0.0.1
+APP_DATABASE_PORT=3306
+VAULT_ADDR=vault:1337
+```
+
+and a source initialized as follows:
+
+```
+src := env.NewSource(
+    env.WithPrefix("VAULT"),
+    env.WithStrippedPrefix("APP"),
+)
+```
+
+The resulting config will be:
+
+```
+{
+    "database": {
+        "address": "127.0.0.1",
+        "port": 3306
+    },
+    "vault": {
+        "addr": "vault:1337"
+    }
+}
+```
+
+
+## New Source
+
+Specify source with data
+
+```go
+src := env.NewSource(
+	// optionally specify prefix
+	env.WithPrefix("GO_ADMIN"),
+)
+```
+
+## Load Source
+
+Load the source into config
+
+```go
+// Create new config
+conf := config.NewConfig()
+
+// Load env source
+conf.Load(src)
+```

+ 146 - 0
config/source/env/env.go

@@ -0,0 +1,146 @@
+package env
+
+import (
+	"os"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/imdario/mergo"
+	"git.baozhida.cn/OAuth-core/config/source"
+)
+
+var (
+	DefaultPrefixes = []string{}
+)
+
+type env struct {
+	prefixes         []string
+	strippedPrefixes []string
+	opts             source.Options
+}
+
+func (e *env) Read() (*source.ChangeSet, error) {
+	var changes map[string]interface{}
+
+	for _, env := range os.Environ() {
+
+		if len(e.prefixes) > 0 || len(e.strippedPrefixes) > 0 {
+			notFound := true
+
+			if _, ok := matchPrefix(e.prefixes, env); ok {
+				notFound = false
+			}
+
+			if match, ok := matchPrefix(e.strippedPrefixes, env); ok {
+				env = strings.TrimPrefix(env, match)
+				notFound = false
+			}
+
+			if notFound {
+				continue
+			}
+		}
+
+		pair := strings.SplitN(env, "=", 2)
+		value := pair[1]
+		keys := strings.Split(strings.ToLower(pair[0]), "_")
+		reverse(keys)
+
+		tmp := make(map[string]interface{})
+		for i, k := range keys {
+			if i == 0 {
+				if intValue, err := strconv.Atoi(value); err == nil {
+					tmp[k] = intValue
+				} else if boolValue, err := strconv.ParseBool(value); err == nil {
+					tmp[k] = boolValue
+				} else {
+					tmp[k] = value
+				}
+				continue
+			}
+
+			tmp = map[string]interface{}{k: tmp}
+		}
+
+		if err := mergo.Map(&changes, tmp); err != nil {
+			return nil, err
+		}
+	}
+
+	b, err := e.opts.Encoder.Encode(changes)
+	if err != nil {
+		return nil, err
+	}
+
+	cs := &source.ChangeSet{
+		Format:    e.opts.Encoder.String(),
+		Data:      b,
+		Timestamp: time.Now(),
+		Source:    e.String(),
+	}
+	cs.Checksum = cs.Sum()
+
+	return cs, nil
+}
+
+func matchPrefix(pre []string, s string) (string, bool) {
+	for _, p := range pre {
+		if strings.HasPrefix(s, p) {
+			return p, true
+		}
+	}
+
+	return "", false
+}
+
+func reverse(ss []string) {
+	for i := len(ss)/2 - 1; i >= 0; i-- {
+		opp := len(ss) - 1 - i
+		ss[i], ss[opp] = ss[opp], ss[i]
+	}
+}
+
+func (e *env) Watch() (source.Watcher, error) {
+	return newWatcher()
+}
+
+func (e *env) Write(cs *source.ChangeSet) error {
+	return nil
+}
+
+func (e *env) String() string {
+	return "env"
+}
+
+// NewSource returns a config source for parsing ENV variables.
+// Underscores are delimiters for nesting, and all keys are lowercased.
+//
+// Example:
+//      "DATABASE_SERVER_HOST=localhost" will convert to
+//
+//      {
+//          "database": {
+//              "server": {
+//                  "host": "localhost"
+//              }
+//          }
+//      }
+func NewSource(opts ...source.Option) source.Source {
+	options := source.NewOptions(opts...)
+
+	var sp []string
+	var pre []string
+	if p, ok := options.Context.Value(strippedPrefixKey{}).([]string); ok {
+		sp = p
+	}
+
+	if p, ok := options.Context.Value(prefixKey{}).([]string); ok {
+		pre = p
+	}
+
+	if len(sp) > 0 || len(pre) > 0 {
+		pre = append(pre, DefaultPrefixes...)
+	}
+	return &env{prefixes: pre, strippedPrefixes: sp, opts: options}
+}

+ 112 - 0
config/source/env/env_test.go

@@ -0,0 +1,112 @@
+package env
+
+import (
+	"encoding/json"
+	"os"
+	"testing"
+	"time"
+
+	"git.baozhida.cn/OAuth-core/config/source"
+)
+
+func TestEnv_Read(t *testing.T) {
+	expected := map[string]map[string]string{
+		"database": {
+			"host":       "localhost",
+			"password":   "password",
+			"datasource": "user:password@tcp(localhost:port)/db?charset=utf8mb4&parseTime=True&loc=Local",
+		},
+	}
+
+	os.Setenv("DATABASE_HOST", "localhost")
+	os.Setenv("DATABASE_PASSWORD", "password")
+	os.Setenv("DATABASE_DATASOURCE", "user:password@tcp(localhost:port)/db?charset=utf8mb4&parseTime=True&loc=Local")
+
+	source := NewSource()
+	c, err := source.Read()
+	if err != nil {
+		t.Error(err)
+	}
+
+	var actual map[string]interface{}
+	if err := json.Unmarshal(c.Data, &actual); err != nil {
+		t.Error(err)
+	}
+
+	actualDB := actual["database"].(map[string]interface{})
+
+	for k, v := range expected["database"] {
+		a := actualDB[k]
+
+		if a != v {
+			t.Errorf("expected %v got %v", v, a)
+		}
+	}
+}
+
+func TestEnvvar_Prefixes(t *testing.T) {
+	os.Setenv("APP_DATABASE_HOST", "localhost")
+	os.Setenv("APP_DATABASE_PASSWORD", "password")
+	os.Setenv("VAULT_ADDR", "vault:1337")
+	os.Setenv("GO_ADMIN_CORE_REGISTRY", "mdns")
+
+	var prefixtests = []struct {
+		prefixOpts   []source.Option
+		expectedKeys []string
+	}{
+		{[]source.Option{WithPrefix("APP", "GO_ADMIN_CORE")}, []string{"app", "go_admin_core"}},
+		{[]source.Option{WithPrefix("GO_ADMIN_CORE"), WithStrippedPrefix("APP")}, []string{"database", "go_admin_core"}},
+		{[]source.Option{WithPrefix("GO_ADMIN_CORE"), WithStrippedPrefix("APP")}, []string{"database", "go_admin_core"}},
+	}
+
+	for _, pt := range prefixtests {
+		source := NewSource(pt.prefixOpts...)
+
+		c, err := source.Read()
+		if err != nil {
+			t.Error(err)
+		}
+
+		var actual map[string]interface{}
+		if err := json.Unmarshal(c.Data, &actual); err != nil {
+			t.Error(err)
+		}
+
+		// assert other prefixes ignored
+		if l := len(actual); l != len(pt.expectedKeys) {
+			t.Errorf("expected %v top keys, got %v", len(pt.expectedKeys), l)
+		}
+
+		for _, k := range pt.expectedKeys {
+			if !containsKey(actual, k) {
+				t.Errorf("expected key %v, not found", k)
+			}
+		}
+	}
+}
+
+func TestEnvvar_WatchNextNoOpsUntilStop(t *testing.T) {
+	src := NewSource(WithStrippedPrefix("GO_ADMIN_CORE_"))
+	w, err := src.Watch()
+	if err != nil {
+		t.Error(err)
+	}
+
+	go func() {
+		time.Sleep(50 * time.Millisecond)
+		w.Stop()
+	}()
+
+	if _, err := w.Next(); err != source.ErrWatcherStopped {
+		t.Errorf("expected watcher stopped error, got %v", err)
+	}
+}
+
+func containsKey(m map[string]interface{}, s string) bool {
+	for k := range m {
+		if k == s {
+			return true
+		}
+	}
+	return false
+}

+ 50 - 0
config/source/env/options.go

@@ -0,0 +1,50 @@
+package env
+
+import (
+	"context"
+
+	"strings"
+
+	"git.baozhida.cn/OAuth-core/config/source"
+)
+
+type strippedPrefixKey struct{}
+type prefixKey struct{}
+
+// WithStrippedPrefix sets the environment variable prefixes to scope to.
+// These prefixes will be removed from the actual config entries.
+func WithStrippedPrefix(p ...string) source.Option {
+	return func(o *source.Options) {
+		if o.Context == nil {
+			o.Context = context.Background()
+		}
+
+		o.Context = context.WithValue(o.Context, strippedPrefixKey{}, appendUnderscore(p))
+	}
+}
+
+// WithPrefix sets the environment variable prefixes to scope to.
+// These prefixes will not be removed. Each prefix will be considered a top level config entry.
+func WithPrefix(p ...string) source.Option {
+	return func(o *source.Options) {
+		if o.Context == nil {
+			o.Context = context.Background()
+		}
+		o.Context = context.WithValue(o.Context, prefixKey{}, appendUnderscore(p))
+	}
+}
+
+func appendUnderscore(prefixes []string) []string {
+	//nolint:prealloc
+	var result []string
+	for _, p := range prefixes {
+		if !strings.HasSuffix(p, "_") {
+			result = append(result, p+"_")
+			continue
+		}
+
+		result = append(result, p)
+	}
+
+	return result
+}

+ 24 - 0
config/source/env/watcher.go

@@ -0,0 +1,24 @@
+package env
+
+import (
+	"git.baozhida.cn/OAuth-core/config/source"
+)
+
+type watcher struct {
+	exit chan struct{}
+}
+
+func (w *watcher) Next() (*source.ChangeSet, error) {
+	<-w.exit
+
+	return nil, source.ErrWatcherStopped
+}
+
+func (w *watcher) Stop() error {
+	close(w.exit)
+	return nil
+}
+
+func newWatcher() (source.Watcher, error) {
+	return &watcher{exit: make(chan struct{})}, nil
+}

+ 70 - 0
config/source/file/README.md

@@ -0,0 +1,70 @@
+# File Source
+
+The file source reads config from a file. 
+
+It uses the File extension to determine the Format e.g `config.yaml` has the yaml format. 
+It does not make use of encoders or interpet the file data. If a file extension is not present 
+the source Format will default to the Encoder in options.
+
+## Example
+
+A config file format in json
+
+```json
+{
+    "hosts": {
+        "database": {
+            "address": "10.0.0.1",
+            "port": 3306
+        },
+        "cache": {
+            "address": "10.0.0.2",
+            "port": 6379
+        }
+    }
+}
+```
+
+## New Source
+
+Specify file source with path to file. Path is optional and will default to `config.json`
+
+```go
+fileSource := file.NewSource(
+	file.WithPath("/tmp/config.json"),
+)
+```
+
+## File Format
+
+To load different file formats e.g yaml, toml, xml simply specify them with their extension
+
+```
+fileSource := file.NewSource(
+        file.WithPath("/tmp/config.yaml"),
+)
+```
+
+If you want to specify a file without extension, ensure you set the encoder to the same format
+
+```
+e := toml.NewEncoder()
+
+fileSource := file.NewSource(
+        file.WithPath("/tmp/config"),
+	source.WithEncoder(e),
+)
+```
+
+## Load Source
+
+Load the source into config
+
+```go
+// Create new config
+conf := config.NewConfig()
+
+// Load file source
+conf.Load(fileSource)
+```
+

+ 69 - 0
config/source/file/file.go

@@ -0,0 +1,69 @@
+// Package file is a file source. Expected format is json
+package file
+
+import (
+	"io/ioutil"
+	"os"
+
+	"git.baozhida.cn/OAuth-core/config/source"
+)
+
+type file struct {
+	path string
+	opts source.Options
+}
+
+var (
+	DefaultPath = "config.json"
+)
+
+func (f *file) Read() (*source.ChangeSet, error) {
+	fh, err := os.Open(f.path)
+	if err != nil {
+		return nil, err
+	}
+	defer fh.Close()
+	b, err := ioutil.ReadAll(fh)
+	if err != nil {
+		return nil, err
+	}
+	info, err := fh.Stat()
+	if err != nil {
+		return nil, err
+	}
+
+	cs := &source.ChangeSet{
+		Format:    format(f.path, f.opts.Encoder),
+		Source:    f.String(),
+		Timestamp: info.ModTime(),
+		Data:      b,
+	}
+	cs.Checksum = cs.Sum()
+
+	return cs, nil
+}
+
+func (f *file) String() string {
+	return "file"
+}
+
+func (f *file) Watch() (source.Watcher, error) {
+	if _, err := os.Stat(f.path); err != nil {
+		return nil, err
+	}
+	return newWatcher(f)
+}
+
+func (f *file) Write(cs *source.ChangeSet) error {
+	return nil
+}
+
+func NewSource(opts ...source.Option) source.Source {
+	options := source.NewOptions(opts...)
+	path := DefaultPath
+	f, ok := options.Context.Value(filePathKey{}).(string)
+	if ok {
+		path = f
+	}
+	return &file{opts: options, path: path}
+}

+ 66 - 0
config/source/file/file_test.go

@@ -0,0 +1,66 @@
+package file_test
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"testing"
+	"time"
+
+	"git.baozhida.cn/OAuth-core/config"
+	"git.baozhida.cn/OAuth-core/config/source/file"
+)
+
+func TestConfig(t *testing.T) {
+	data := []byte(`{"foo": "bar"}`)
+	path := filepath.Join(os.TempDir(), fmt.Sprintf("file.%d", time.Now().UnixNano()))
+	fh, err := os.Create(path)
+	if err != nil {
+		t.Error(err)
+	}
+	defer func() {
+		fh.Close()
+		os.Remove(path)
+	}()
+	_, err = fh.Write(data)
+	if err != nil {
+		t.Error(err)
+	}
+
+	conf, err := config.NewConfig()
+	if err != nil {
+		t.Fatal(err)
+	}
+	conf.Load(file.NewSource(file.WithPath(path)))
+	// simulate multiple close
+	go conf.Close()
+	go conf.Close()
+}
+
+func TestFile(t *testing.T) {
+	data := []byte(`{"foo": "bar"}`)
+	path := filepath.Join(os.TempDir(), fmt.Sprintf("file.%d", time.Now().UnixNano()))
+	fh, err := os.Create(path)
+	if err != nil {
+		t.Error(err)
+	}
+	defer func() {
+		fh.Close()
+		os.Remove(path)
+	}()
+
+	_, err = fh.Write(data)
+	if err != nil {
+		t.Error(err)
+	}
+
+	f := file.NewSource(file.WithPath(path))
+	c, err := f.Read()
+	if err != nil {
+		t.Error(err)
+	}
+	if string(c.Data) != string(data) {
+		t.Logf("%+v", c)
+		t.Error("data from file does not match")
+	}
+}

+ 15 - 0
config/source/file/format.go

@@ -0,0 +1,15 @@
+package file
+
+import (
+	"strings"
+
+	"git.baozhida.cn/OAuth-core/config/encoder"
+)
+
+func format(p string, e encoder.Encoder) string {
+	parts := strings.Split(p, ".")
+	if len(parts) > 1 {
+		return parts[len(parts)-1]
+	}
+	return e.String()
+}

+ 31 - 0
config/source/file/format_test.go

@@ -0,0 +1,31 @@
+package file
+
+import (
+	"testing"
+
+	"git.baozhida.cn/OAuth-core/config/source"
+)
+
+func TestFormat(t *testing.T) {
+	opts := source.NewOptions()
+	e := opts.Encoder
+
+	testCases := []struct {
+		p string
+		f string
+	}{
+		{"/foo/bar.json", "json"},
+		{"/foo/bar.yaml", "yaml"},
+		{"/foo/bar.xml", "xml"},
+		{"/foo/bar.conf.ini", "ini"},
+		{"conf", e.String()},
+	}
+
+	for _, d := range testCases {
+		f := format(d.p, e)
+		if f != d.f {
+			t.Fatalf("%s: expected %s got %s", d.p, d.f, f)
+		}
+	}
+
+}

+ 19 - 0
config/source/file/options.go

@@ -0,0 +1,19 @@
+package file
+
+import (
+	"context"
+
+	"git.baozhida.cn/OAuth-core/config/source"
+)
+
+type filePathKey struct{}
+
+// WithPath sets the path to file
+func WithPath(p string) source.Option {
+	return func(o *source.Options) {
+		if o.Context == nil {
+			o.Context = context.Background()
+		}
+		o.Context = context.WithValue(o.Context, filePathKey{}, p)
+	}
+}

+ 74 - 0
config/source/file/watcher.go

@@ -0,0 +1,74 @@
+//go:build !linux
+// +build !linux
+
+package file
+
+import (
+	"os"
+
+	"github.com/fsnotify/fsnotify"
+	"git.baozhida.cn/OAuth-core/config/source"
+)
+
+type watcher struct {
+	f *file
+
+	fw   *fsnotify.Watcher
+	exit chan bool
+}
+
+func newWatcher(f *file) (source.Watcher, error) {
+	fw, err := fsnotify.NewWatcher()
+	if err != nil {
+		return nil, err
+	}
+
+	err = fw.Add(f.path)
+	if err != nil {
+		return nil, err
+	}
+
+	return &watcher{
+		f:    f,
+		fw:   fw,
+		exit: make(chan bool),
+	}, nil
+}
+
+func (w *watcher) Next() (*source.ChangeSet, error) {
+	// is it closed?
+	select {
+	case <-w.exit:
+		return nil, source.ErrWatcherStopped
+	default:
+	}
+
+	// try get the event
+	select {
+	case event, _ := <-w.fw.Events:
+		if event.Op == fsnotify.Rename {
+			// check existence of file, and add watch again
+			_, err := os.Stat(event.Name)
+			if err == nil || os.IsExist(err) {
+				err := w.fw.Add(event.Name)
+				if err != nil {
+					return nil, err
+				}
+			}
+		}
+
+		c, err := w.f.Read()
+		if err != nil {
+			return nil, err
+		}
+		return c, nil
+	case err := <-w.fw.Errors:
+		return nil, err
+	case <-w.exit:
+		return nil, source.ErrWatcherStopped
+	}
+}
+
+func (w *watcher) Stop() error {
+	return w.fw.Close()
+}

+ 72 - 0
config/source/file/watcher_linux.go

@@ -0,0 +1,72 @@
+//go:build linux
+// +build linux
+
+package file
+
+import (
+	"os"
+
+	"github.com/fsnotify/fsnotify"
+	"git.baozhida.cn/OAuth-core/config/source"
+)
+
+type watcher struct {
+	f *file
+
+	fw   *fsnotify.Watcher
+	exit chan bool
+}
+
+func newWatcher(f *file) (source.Watcher, error) {
+	fw, err := fsnotify.NewWatcher()
+	if err != nil {
+		return nil, err
+	}
+
+	fw.Add(f.path)
+
+	return &watcher{
+		f:    f,
+		fw:   fw,
+		exit: make(chan bool),
+	}, nil
+}
+
+func (w *watcher) Next() (*source.ChangeSet, error) {
+	// is it closed?
+	select {
+	case <-w.exit:
+		return nil, source.ErrWatcherStopped
+	default:
+	}
+
+	// try get the event
+	select {
+	case event, _ := <-w.fw.Events:
+		if event.Op == fsnotify.Rename {
+			// check existence of file, and add watch again
+			_, err := os.Stat(event.Name)
+			if err == nil || os.IsExist(err) {
+				w.fw.Add(event.Name)
+			}
+		}
+
+		c, err := w.f.Read()
+		if err != nil {
+			return nil, err
+		}
+
+		// add path again for the event bug of fsnotify
+		w.fw.Add(w.f.path)
+
+		return c, nil
+	case err := <-w.fw.Errors:
+		return nil, err
+	case <-w.exit:
+		return nil, source.ErrWatcherStopped
+	}
+}
+
+func (w *watcher) Stop() error {
+	return w.fw.Close()
+}

+ 47 - 0
config/source/flag/README.md

@@ -0,0 +1,47 @@
+# Flag Source
+
+The flag source reads config from flags
+
+## Format
+
+We expect the use of the `flag` package. Upper case flags will be lower cased. Dashes will be used as delimiters.
+
+### Example
+
+```
+dbAddress := flag.String("database_address", "127.0.0.1", "the db address")
+dbPort := flag.Int("database_port", 3306, "the db port)
+```
+
+Becomes
+
+```json
+{
+    "database": {
+        "address": "127.0.0.1",
+        "port": 3306
+    }
+}
+```
+
+## New Source
+
+```go
+flagSource := flag.NewSource(
+	// optionally enable reading of unset flags and their default
+	// values into config, defaults to false
+	IncludeUnset(true)
+)
+```
+
+## Load Source
+
+Load the source into config
+
+```go
+// Create new config
+conf := config.NewConfig()
+
+// Load flag source
+conf.Load(flagSource)
+```

+ 101 - 0
config/source/flag/flag.go

@@ -0,0 +1,101 @@
+package flag
+
+import (
+	"errors"
+	"flag"
+	"github.com/imdario/mergo"
+	"strings"
+	"time"
+	"git.baozhida.cn/OAuth-core/config/source"
+)
+
+type flagsrc struct {
+	opts source.Options
+}
+
+func (fs *flagsrc) Read() (*source.ChangeSet, error) {
+	if !flag.Parsed() {
+		return nil, errors.New("flags not parsed")
+	}
+
+	var changes map[string]interface{}
+
+	visitFn := func(f *flag.Flag) {
+		n := strings.ToLower(f.Name)
+		keys := strings.FieldsFunc(n, split)
+		reverse(keys)
+
+		tmp := make(map[string]interface{})
+		for i, k := range keys {
+			if i == 0 {
+				tmp[k] = f.Value
+				continue
+			}
+
+			tmp = map[string]interface{}{k: tmp}
+		}
+
+		_ = mergo.Map(&changes, tmp) // need to sort error handling
+		return
+	}
+
+	unset, ok := fs.opts.Context.Value(includeUnsetKey{}).(bool)
+	if ok && unset {
+		flag.VisitAll(visitFn)
+	} else {
+		flag.Visit(visitFn)
+	}
+
+	b, err := fs.opts.Encoder.Encode(changes)
+	if err != nil {
+		return nil, err
+	}
+
+	cs := &source.ChangeSet{
+		Format:    fs.opts.Encoder.String(),
+		Data:      b,
+		Timestamp: time.Now(),
+		Source:    fs.String(),
+	}
+	cs.Checksum = cs.Sum()
+
+	return cs, nil
+}
+
+func split(r rune) bool {
+	return r == '-' || r == '_'
+}
+
+func reverse(ss []string) {
+	for i := len(ss)/2 - 1; i >= 0; i-- {
+		opp := len(ss) - 1 - i
+		ss[i], ss[opp] = ss[opp], ss[i]
+	}
+}
+
+func (fs *flagsrc) Watch() (source.Watcher, error) {
+	return source.NewNoopWatcher()
+}
+
+func (fs *flagsrc) Write(cs *source.ChangeSet) error {
+	return nil
+}
+
+func (fs *flagsrc) String() string {
+	return "flag"
+}
+
+// NewSource returns a config source for integrating parsed flags.
+// Hyphens are delimiters for nesting, and all keys are lowercased.
+//
+// Example:
+//      dbhost := flag.String("database-host", "localhost", "the db host name")
+//
+//      {
+//          "database": {
+//              "host": "localhost"
+//          }
+//      }
+func NewSource(opts ...source.Option) source.Source {
+	return &flagsrc{opts: source.NewOptions(opts...)}
+}

+ 68 - 0
config/source/flag/flag_test.go

@@ -0,0 +1,68 @@
+package flag
+
+import (
+	"encoding/json"
+	"flag"
+	"testing"
+)
+
+var (
+	dbuser = flag.String("database-user", "default", "db user")
+	dbhost = flag.String("database-host", "", "db host")
+	dbpw   = flag.String("database-password", "", "db pw")
+)
+
+func initTestFlags() {
+	flag.Set("database-host", "localhost")
+	flag.Set("database-password", "some-password")
+	flag.Parse()
+}
+
+func TestFlagsrc_Read(t *testing.T) {
+	initTestFlags()
+	source := NewSource()
+	c, err := source.Read()
+	if err != nil {
+		t.Error(err)
+	}
+
+	var actual map[string]interface{}
+	if err := json.Unmarshal(c.Data, &actual); err != nil {
+		t.Error(err)
+	}
+
+	actualDB := actual["database"].(map[string]interface{})
+	if actualDB["host"] != *dbhost {
+		t.Errorf("expected %v got %v", *dbhost, actualDB["host"])
+	}
+
+	if actualDB["password"] != *dbpw {
+		t.Errorf("expected %v got %v", *dbpw, actualDB["password"])
+	}
+
+	// unset flags should not be loaded
+	if actualDB["user"] != nil {
+		t.Errorf("expected %v got %v", nil, actualDB["user"])
+	}
+}
+
+func TestFlagsrc_ReadAll(t *testing.T) {
+	initTestFlags()
+	source := NewSource(IncludeUnset(true))
+	c, err := source.Read()
+	if err != nil {
+		t.Error(err)
+	}
+
+	var actual map[string]interface{}
+	if err := json.Unmarshal(c.Data, &actual); err != nil {
+		t.Error(err)
+	}
+
+	actualDB := actual["database"].(map[string]interface{})
+
+	// unset flag defaults should be loaded
+	if actualDB["user"] != *dbuser {
+		t.Errorf("expected %v got %v", *dbuser, actualDB["user"])
+	}
+}

+ 20 - 0
config/source/flag/options.go

@@ -0,0 +1,20 @@
+package flag
+
+import (
+	"context"
+
+	"git.baozhida.cn/OAuth-core/config/source"
+)
+
+type includeUnsetKey struct{}
+
+// IncludeUnset toggles the loading of unset flags and their respective default values.
+// Default behavior is to ignore any unset flags.
+func IncludeUnset(b bool) source.Option {
+	return func(o *source.Options) {
+		if o.Context == nil {
+			o.Context = context.Background()
+		}
+		o.Context = context.WithValue(o.Context, includeUnsetKey{}, true)
+	}
+}

+ 44 - 0
config/source/memory/README.md

@@ -0,0 +1,44 @@
+# Memory Source
+
+The memory source provides in-memory data as a source
+
+## Memory Format
+
+The expected data format is json
+
+```json
+data := []byte(`{
+    "hosts": {
+        "database": {
+            "address": "10.0.0.1",
+            "port": 3306
+        },
+        "cache": {
+            "address": "10.0.0.2",
+            "port": 6379
+        }
+    }
+}`)
+```
+
+## New Source
+
+Specify source with data
+
+```go
+memorySource := memory.NewSource(
+	memory.WithJSON(data),
+)
+```
+
+## Load Source
+
+Load the source into config
+
+```go
+// Create new config
+conf := config.NewConfig()
+
+// Load memory source
+conf.Load(memorySource)
+```

+ 99 - 0
config/source/memory/memory.go

@@ -0,0 +1,99 @@
+// Package memory is a memory source
+package memory
+
+import (
+	"sync"
+	"time"
+
+	"github.com/google/uuid"
+	"git.baozhida.cn/OAuth-core/config/source"
+)
+
+type memory struct {
+	sync.RWMutex
+	ChangeSet *source.ChangeSet
+	Watchers  map[string]*watcher
+}
+
+func (s *memory) Read() (*source.ChangeSet, error) {
+	s.RLock()
+	cs := &source.ChangeSet{
+		Format:    s.ChangeSet.Format,
+		Timestamp: s.ChangeSet.Timestamp,
+		Data:      s.ChangeSet.Data,
+		Checksum:  s.ChangeSet.Checksum,
+		Source:    s.ChangeSet.Source,
+	}
+	s.RUnlock()
+	return cs, nil
+}
+
+func (s *memory) Watch() (source.Watcher, error) {
+	w := &watcher{
+		Id:      uuid.New().String(),
+		Updates: make(chan *source.ChangeSet, 100),
+		Source:  s,
+	}
+
+	s.Lock()
+	s.Watchers[w.Id] = w
+	s.Unlock()
+	return w, nil
+}
+
+func (m *memory) Write(cs *source.ChangeSet) error {
+	m.Update(cs)
+	return nil
+}
+
+// Update allows manual updates of the config data.
+func (s *memory) Update(c *source.ChangeSet) {
+	// don't process nil
+	if c == nil {
+		return
+	}
+
+	// hash the file
+	s.Lock()
+	// update changeset
+	s.ChangeSet = &source.ChangeSet{
+		Data:      c.Data,
+		Format:    c.Format,
+		Source:    "memory",
+		Timestamp: time.Now(),
+	}
+	s.ChangeSet.Checksum = s.ChangeSet.Sum()
+
+	// update watchers
+	for _, w := range s.Watchers {
+		select {
+		case w.Updates <- s.ChangeSet:
+		default:
+		}
+	}
+	s.Unlock()
+}
+
+func (s *memory) String() string {
+	return "memory"
+}
+
+func NewSource(opts ...source.Option) source.Source {
+	var options source.Options
+	for _, o := range opts {
+		o(&options)
+	}
+
+	s := &memory{
+		Watchers: make(map[string]*watcher),
+	}
+
+	if options.Context != nil {
+		c, ok := options.Context.Value(changeSetKey{}).(*source.ChangeSet)
+		if ok {
+			s.Update(c)
+		}
+	}
+
+	return s
+}

+ 41 - 0
config/source/memory/options.go

@@ -0,0 +1,41 @@
+package memory
+
+import (
+	"context"
+
+	"git.baozhida.cn/OAuth-core/config/source"
+)
+
+type changeSetKey struct{}
+
+func withData(d []byte, f string) source.Option {
+	return func(o *source.Options) {
+		if o.Context == nil {
+			o.Context = context.Background()
+		}
+		o.Context = context.WithValue(o.Context, changeSetKey{}, &source.ChangeSet{
+			Data:   d,
+			Format: f,
+		})
+	}
+}
+
+// WithChangeSet allows a changeset to be set
+func WithChangeSet(cs *source.ChangeSet) source.Option {
+	return func(o *source.Options) {
+		if o.Context == nil {
+			o.Context = context.Background()
+		}
+		o.Context = context.WithValue(o.Context, changeSetKey{}, cs)
+	}
+}
+
+// WithJSON allows the source data to be set to json
+func WithJSON(d []byte) source.Option {
+	return withData(d, "json")
+}
+
+// WithYAML allows the source data to be set to yaml
+func WithYAML(d []byte) source.Option {
+	return withData(d, "yaml")
+}

+ 23 - 0
config/source/memory/watcher.go

@@ -0,0 +1,23 @@
+package memory
+
+import (
+	"git.baozhida.cn/OAuth-core/config/source"
+)
+
+type watcher struct {
+	Id      string
+	Updates chan *source.ChangeSet
+	Source  *memory
+}
+
+func (w *watcher) Next() (*source.ChangeSet, error) {
+	cs := <-w.Updates
+	return cs, nil
+}
+
+func (w *watcher) Stop() error {
+	w.Source.Lock()
+	delete(w.Source.Watchers, w.Id)
+	w.Source.Unlock()
+	return nil
+}

+ 25 - 0
config/source/noop.go

@@ -0,0 +1,25 @@
+package source
+
+import (
+	"errors"
+)
+
+type noopWatcher struct {
+	exit chan struct{}
+}
+
+func (w *noopWatcher) Next() (*ChangeSet, error) {
+	<-w.exit
+
+	return nil, errors.New("noopWatcher stopped")
+}
+
+func (w *noopWatcher) Stop() error {
+	close(w.exit)
+	return nil
+}
+
+// NewNoopWatcher returns a watcher that blocks on Next() until Stop() is called.
+func NewNoopWatcher() (Watcher, error) {
+	return &noopWatcher{exit: make(chan struct{})}, nil
+}

+ 38 - 0
config/source/options.go

@@ -0,0 +1,38 @@
+package source
+
+import (
+	"context"
+
+	"git.baozhida.cn/OAuth-core/config/encoder"
+	"git.baozhida.cn/OAuth-core/config/encoder/json"
+)
+
+type Options struct {
+	// Encoder
+	Encoder encoder.Encoder
+
+	// for alternative data
+	Context context.Context
+}
+
+type Option func(o *Options)
+
+func NewOptions(opts ...Option) Options {
+	options := Options{
+		Encoder: json.NewEncoder(),
+		Context: context.Background(),
+	}
+
+	for _, o := range opts {
+		o(&options)
+	}
+
+	return options
+}
+
+// WithEncoder sets the source encoder
+func WithEncoder(e encoder.Encoder) Option {
+	return func(o *Options) {
+		o.Encoder = e
+	}
+}

+ 35 - 0
config/source/source.go

@@ -0,0 +1,35 @@
+// Package source is the interface for sources
+package source
+
+import (
+	"errors"
+	"time"
+)
+
+var (
+	// ErrWatcherStopped is returned when source watcher has been stopped
+	ErrWatcherStopped = errors.New("watcher stopped")
+)
+
+// Source is the source from which config is loaded
+type Source interface {
+	Read() (*ChangeSet, error)
+	Write(*ChangeSet) error
+	Watch() (Watcher, error)
+	String() string
+}
+
+// ChangeSet represents a set of changes from a source
+type ChangeSet struct {
+	Data      []byte
+	Checksum  string
+	Format    string
+	Source    string
+	Timestamp time.Time
+}
+
+// Watcher watches a source for changes
+type Watcher interface {
+	Next() (*ChangeSet, error)
+	Stop() error
+}

+ 49 - 0
config/value.go

@@ -0,0 +1,49 @@
+package config
+
+import (
+	"time"
+
+	"git.baozhida.cn/OAuth-core/config/reader"
+)
+
+type value struct{}
+
+func newValue() reader.Value {
+	return new(value)
+}
+
+func (v *value) Bool(def bool) bool {
+	return false
+}
+
+func (v *value) Int(def int) int {
+	return 0
+}
+
+func (v *value) String(def string) string {
+	return ""
+}
+
+func (v *value) Float64(def float64) float64 {
+	return 0.0
+}
+
+func (v *value) Duration(def time.Duration) time.Duration {
+	return time.Duration(0)
+}
+
+func (v *value) StringSlice(def []string) []string {
+	return nil
+}
+
+func (v *value) StringMap(def map[string]string) map[string]string {
+	return map[string]string{}
+}
+
+func (v *value) Scan(val interface{}) error {
+	return nil
+}
+
+func (v *value) Bytes() []byte {
+	return nil
+}

+ 56 - 0
debug/log/log.go

@@ -0,0 +1,56 @@
+// Package log provides debug logging
+package log
+
+import (
+	"encoding/json"
+	"fmt"
+	"time"
+)
+
+var (
+	// DefaultSize Default buffer size if any
+	DefaultSize = 256
+	// DefaultFormat Default formatter
+	DefaultFormat = TextFormat
+)
+
+// Log is debug log interface for reading and writing logs
+type Log interface {
+	// Read reads log entries from the logger
+	Read(...ReadOption) ([]Record, error)
+	// Write writes records to log
+	Write(Record) error
+	// Stream log records
+	Stream() (Stream, error)
+}
+
+// Record is log record entry
+type Record struct {
+	// Timestamp of logged event
+	Timestamp time.Time `json:"timestamp"`
+	// Metadata to enrich log record
+	Metadata map[string]string `json:"metadata"`
+	// Value contains log entry
+	Message interface{} `json:"message"`
+}
+
+// Stream returns a log stream
+type Stream interface {
+	Chan() <-chan Record
+	Stop() error
+}
+
+// FormatFunc is a function which formats the output
+type FormatFunc func(Record) string
+
+// TextFormat returns text format
+func TextFormat(r Record) string {
+	t := r.Timestamp.Format("2006-01-02 15:04:05")
+	return fmt.Sprintf("%s %v ", t, r.Message)
+}
+
+// JSONFormat is a json Format func
+func JSONFormat(r Record) string {
+	b, _ := json.Marshal(r)
+	return string(b) + " "
+}

+ 70 - 0
debug/log/options.go

@@ -0,0 +1,70 @@
+package log
+
+import "time"
+
+// Option used by the logger
+type Option func(*Options)
+
+// Options are logger options
+type Options struct {
+	// Name of the log
+	Name string
+	// Size is the size of ring buffer
+	Size int
+	// Format specifies the output format
+	Format FormatFunc
+}
+
+// Name of the log
+func Name(n string) Option {
+	return func(o *Options) {
+		o.Name = n
+	}
+}
+
+// Size sets the size of the ring buffer
+func Size(s int) Option {
+	return func(o *Options) {
+		o.Size = s
+	}
+}
+
+func Format(f FormatFunc) Option {
+	return func(o *Options) {
+		o.Format = f
+	}
+}
+
+// DefaultOptions returns default options
+func DefaultOptions() Options {
+	return Options{
+		Size: DefaultSize,
+	}
+}
+
+// ReadOptions for querying the logs
+type ReadOptions struct {
+	// Since what time in past to return the logs
+	Since time.Time
+	// Count specifies number of logs to return
+	Count int
+	// Stream requests continuous log stream
+	Stream bool
+}
+
+// ReadOption used for reading the logs
+type ReadOption func(*ReadOptions)
+
+// Since sets the time since which to return the log records
+func Since(s time.Time) ReadOption {
+	return func(o *ReadOptions) {
+		o.Since = s
+	}
+}
+
+// Count sets the number of log records to return
+func Count(c int) ReadOption {
+	return func(o *ReadOptions) {
+		o.Count = c
+	}
+}

+ 126 - 0
debug/writer/file.go

@@ -0,0 +1,126 @@
+package writer
+
+import (
+	"errors"
+	"fmt"
+	"log"
+	"os"
+	"path/filepath"
+	"strings"
+	"time"
+)
+
+// timeFormat 时间格式
+// 用于文件名称格式
+const timeFormat = "2006-01-02"
+
+// FileWriter 文件写入结构体
+type FileWriter struct {
+	file         *os.File
+	FilenameFunc func(*FileWriter) string
+	num          uint
+	opts         Options
+	input        chan []byte
+}
+
+// NewFileWriter 实例化FileWriter, 支持大文件分割
+func NewFileWriter(opts ...Option) (*FileWriter, error) {
+	p := &FileWriter{
+		opts: setDefault(),
+	}
+	for _, o := range opts {
+		o(&p.opts)
+	}
+	var filename string
+	var err error
+	for {
+		filename = p.getFilename()
+		_, err := os.Stat(filename)
+		if err != nil {
+			if os.IsNotExist(err) {
+				if p.num > 0 {
+					p.num--
+					filename = p.getFilename()
+				}
+				//文件不存在
+				break
+			}
+			//存在,但是报错了
+			return nil, err
+		}
+		p.num++
+		if p.opts.cap == 0 {
+			break
+		}
+	}
+	p.file, err = os.OpenFile(filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE|os.O_SYNC, 0600)
+	if err != nil {
+		return nil, err
+	}
+	p.input = make(chan []byte, 100)
+	go p.write()
+	return p, nil
+}
+
+func (p *FileWriter) write() {
+	for {
+		select {
+		case d := <-p.input:
+			_, err := p.file.Write(d)
+			if err != nil {
+				log.Printf("write file failed, %s\n", err.Error())
+			}
+			p.checkFile()
+		}
+	}
+}
+
+func (p *FileWriter) checkFile() {
+	info, _ := p.file.Stat()
+	if strings.Index(p.file.Name(), time.Now().Format(timeFormat)) < 0 ||
+		(p.opts.cap > 0 && uint(info.Size()) > p.opts.cap) {
+		//生成新文件
+		if uint(info.Size()) > p.opts.cap {
+			p.num++
+		} else {
+			p.num = 0
+		}
+		filename := p.getFilename()
+		_ = p.file.Close()
+		p.file, _ = os.OpenFile(filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE|os.O_SYNC, 0600)
+	}
+}
+
+// Write 写入方法
+func (p *FileWriter) Write(data []byte) (n int, err error) {
+	if p == nil {
+		return 0, errors.New("logFileWriter is nil")
+	}
+	if p.file == nil {
+		return 0, errors.New("file not opened")
+	}
+	n = len(data)
+	go func() {
+		p.input <- data
+	}()
+	return n, nil
+}
+
+// getFilename 获取log文件名
+// 目前为:以日期格式命名,eg:2006-01-02.log or 2006-01-02.log
+func (p *FileWriter) getFilename() string {
+	if p.FilenameFunc != nil {
+		return p.FilenameFunc(p)
+	}
+	if p.opts.cap == 0 {
+		return filepath.Join(p.opts.path,
+			fmt.Sprintf("%s.%s",
+				time.Now().Format(timeFormat),
+				p.opts.suffix))
+	}
+	return filepath.Join(p.opts.path,
+		fmt.Sprintf("%s-[%d].%s",
+			time.Now().Format(timeFormat),
+			p.num,
+			p.opts.suffix))
+}

+ 46 - 0
debug/writer/options.go

@@ -0,0 +1,46 @@
+/*
+ * @Author: lwnmengjing
+ * @Date: 2021/6/3 8:33 上午
+ * @Last Modified by: lwnmengjing
+ * @Last Modified time: 2021/6/3 8:33 上午
+ */
+
+package writer
+
+// Options 可配置参数
+type Options struct {
+	path   string
+	suffix string //文件扩展名
+	cap    uint
+}
+
+func setDefault() Options {
+	return Options{
+		path:   "/tmp/xyadmin",
+		suffix: "log",
+	}
+}
+
+// Option set options
+type Option func(*Options)
+
+// WithPath set path
+func WithPath(s string) Option {
+	return func(o *Options) {
+		o.path = s
+	}
+}
+
+// WithSuffix set suffix
+func WithSuffix(s string) Option {
+	return func(o *Options) {
+		o.suffix = s
+	}
+}
+
+// WithCap set cap
+func WithCap(n uint) Option {
+	return func(o *Options) {
+		o.cap = n
+	}
+}

+ 92 - 0
go.mod

@@ -0,0 +1,92 @@
+module git.baozhida.cn/OAuth-core
+
+go 1.19
+
+require (
+	github.com/BurntSushi/toml v1.1.0
+	github.com/bitly/go-simplejson v0.5.0
+	github.com/bsm/redislock v0.5.0
+	github.com/bytedance/go-tagexpr/v2 v2.7.12
+	github.com/casbin/casbin/v2 v2.47.2
+	github.com/chanxuehong/wechat v0.0.0-20201110083048-0180211b69fd
+	github.com/dgrijalva/jwt-go v3.2.0+incompatible
+	github.com/fsnotify/fsnotify v1.4.9
+	github.com/ghodss/yaml v1.0.0
+	github.com/gin-gonic/gin v1.8.1
+	github.com/go-playground/locales v0.14.0
+	github.com/go-playground/universal-translator v0.18.0
+	github.com/go-playground/validator/v10 v10.11.0
+	github.com/go-redis/redis/v7 v7.4.0
+	github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
+	github.com/google/uuid v1.3.0
+	github.com/gorilla/websocket v1.4.2
+	github.com/imdario/mergo v0.3.9
+	github.com/json-iterator/go v1.1.12
+	github.com/mojocn/base64Captcha v1.3.1
+	github.com/nsqio/go-nsq v1.0.8
+	github.com/pkg/errors v0.9.1
+	github.com/robfig/cron/v3 v3.0.1
+	github.com/robinjoseph08/redisqueue/v2 v2.1.0
+	github.com/shamsher31/goimgext v1.0.0
+	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
+	github.com/smartystreets/goconvey v1.6.4
+	github.com/spf13/cast v1.3.1
+	go.uber.org/zap v1.15.0
+	golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa
+	google.golang.org/grpc v1.29.1
+	gorm.io/driver/mysql v1.3.5
+	gorm.io/gorm v1.23.8
+	gorm.io/plugin/dbresolver v1.2.2
+)
+
+require github.com/google/go-cmp v0.5.7 // indirect
+
+require (
+	github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible // indirect
+	github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
+	github.com/chanxuehong/rand v0.0.0-20201110082127-2f19a1bdd973 // indirect
+	github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
+	github.com/fatih/color v1.9.0 // indirect
+	github.com/gin-contrib/sse v0.1.0 // indirect
+	github.com/git-chglog/git-chglog v0.0.0-20190611050339-63a4e637021f // indirect
+	github.com/go-resty/resty/v2 v2.7.0 // indirect
+	github.com/go-sql-driver/mysql v1.6.0 // indirect
+	github.com/goccy/go-json v0.9.10 // indirect
+	github.com/golang/protobuf v1.5.0 // indirect
+	github.com/golang/snappy v0.0.1 // indirect
+	github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 // indirect
+	github.com/henrylee2cn/ameda v1.4.10 // indirect
+	github.com/henrylee2cn/goutil v0.0.0-20210127050712-89660552f6f8 // indirect
+	github.com/jinzhu/inflection v1.0.0 // indirect
+	github.com/jinzhu/now v1.1.5 // indirect
+	github.com/jtolds/gls v4.20.0+incompatible // indirect
+	github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
+	github.com/leodido/go-urn v1.2.1 // indirect
+	github.com/mattn/go-colorable v0.1.7 // indirect
+	github.com/mattn/go-isatty v0.0.14 // indirect
+	github.com/mattn/goveralls v0.0.2 // indirect
+	github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
+	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+	github.com/modern-go/reflect2 v1.0.2 // indirect
+	github.com/nyaruka/phonenumbers v1.0.55 // indirect
+	github.com/pelletier/go-toml/v2 v2.0.2 // indirect
+	github.com/russross/blackfriday/v2 v2.1.0 // indirect
+	github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d // indirect
+	github.com/stretchr/testify v1.8.0 // indirect
+	github.com/tsuyoshiwada/go-gitcmd v0.0.0-20180205145712-5f1f5f9475df // indirect
+	github.com/ugorji/go/codec v1.2.7 // indirect
+	github.com/urfave/cli v1.22.1 // indirect
+	go.uber.org/atomic v1.6.0 // indirect
+	go.uber.org/multierr v1.5.0 // indirect
+	golang.org/x/image v0.0.0-20220722155232-062f8c9fd539 // indirect
+	golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f // indirect
+	golang.org/x/net v0.0.0-20220809184613-07c6da5e1ced // indirect
+	golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab // indirect
+	golang.org/x/text v0.3.7 // indirect
+	golang.org/x/tools v0.1.12 // indirect
+	golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
+	google.golang.org/protobuf v1.28.1 // indirect
+	gopkg.in/AlecAivazis/survey.v1 v1.8.5 // indirect
+	gopkg.in/kyokomi/emoji.v1 v1.5.1 // indirect
+	gopkg.in/yaml.v2 v2.4.0 // indirect
+)

+ 355 - 0
go.sum

@@ -0,0 +1,355 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I=
+github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
+github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1:1G1pk05UrOh0NlF1oeaaix1x8XzrfjIDK47TY0Zehcw=
+github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
+github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw=
+github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc=
+github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y=
+github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
+github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
+github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
+github.com/bsm/redislock v0.5.0 h1:ODM11/cbuUXQqLgZWK6XQnufaTjsBE2UcwBc2EAFNDA=
+github.com/bsm/redislock v0.5.0/go.mod h1:qagqKlV+xiLy26iV34Y3zRPxRcJjQYbV7pZfWFeSZ8M=
+github.com/bytedance/go-tagexpr/v2 v2.7.12 h1:qL2f0j11S8DHQsUWUA6aacLNBcbPTbNKuzVjaW4kF/M=
+github.com/bytedance/go-tagexpr/v2 v2.7.12/go.mod h1:cKpo/rwg2Y5Njs8SX3FspMWEhAWCaF4xUr5LJYXibSU=
+github.com/casbin/casbin/v2 v2.47.2 h1:FVdlX0GEYWpYj7IdSThBpidLr8Bp+yfvlmVNec5INtw=
+github.com/casbin/casbin/v2 v2.47.2/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/chanxuehong/rand v0.0.0-20201110082127-2f19a1bdd973 h1:Js/7nHtkpvUW62passc9FOflXyLQDjSoYclkFeDdTEM=
+github.com/chanxuehong/rand v0.0.0-20201110082127-2f19a1bdd973/go.mod h1:9+sJ9zvvkXC5sPjPEZM3Jpb9n2Q2VtcrGZly0UHYF5I=
+github.com/chanxuehong/util v0.0.0-20200304121633-ca8141845b13/go.mod h1:XEYt99iTxMqkv+gW85JX/DdUINHUe43Sbe5AtqSaDAQ=
+github.com/chanxuehong/wechat v0.0.0-20201110083048-0180211b69fd h1:TM3wjEWel4U31J72dlhnwCBqPC0+FA0Ejm2NCbn5a5U=
+github.com/chanxuehong/wechat v0.0.0-20201110083048-0180211b69fd/go.mod h1:/dvhOIRCjjiZu6NV0QTTiMcc5XwoORbxfDSsRY2IfaM=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
+github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
+github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
+github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
+github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
+github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
+github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8=
+github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
+github.com/git-chglog/git-chglog v0.0.0-20190611050339-63a4e637021f h1:8l4Aw3Jmx0pLKYMkY+1b6yBPgE+rzRtA5T3vqFyI2Z8=
+github.com/git-chglog/git-chglog v0.0.0-20190611050339-63a4e637021f/go.mod h1:Dcsy1kii/xFyNad5JqY/d0GO5mu91sungp5xotbm3Yk=
+github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
+github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
+github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
+github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
+github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
+github.com/go-playground/validator/v10 v10.11.0 h1:0W+xRM511GY47Yy3bZUbJVitCNg2BOGlCyvTqsp/xIw=
+github.com/go-playground/validator/v10 v10.11.0/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
+github.com/go-redis/redis/v7 v7.2.0/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg=
+github.com/go-redis/redis/v7 v7.3.0/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg=
+github.com/go-redis/redis/v7 v7.4.0 h1:7obg6wUoj05T0EpY0o8B59S9w5yeMWql7sw2kwNW1x4=
+github.com/go-redis/redis/v7 v7.4.0/go.mod h1:JDNMw23GTyLNC4GZu9njt15ctBQVn7xjRfnwdHj/Dcg=
+github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY=
+github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
+github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
+github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
+github.com/goccy/go-json v0.9.10 h1:hCeNmprSNLB8B8vQKWl6DpuH0t60oEs+TAk9a7CScKc=
+github.com/goccy/go-json v0.9.10/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
+github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
+github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
+github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
+github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
+github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
+github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
+github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/henrylee2cn/ameda v1.4.8/go.mod h1:liZulR8DgHxdK+MEwvZIylGnmcjzQ6N6f2PlWe7nEO4=
+github.com/henrylee2cn/ameda v1.4.10 h1:JdvI2Ekq7tapdPsuhrc4CaFiqw6QXFvZIULWJgQyCAk=
+github.com/henrylee2cn/ameda v1.4.10/go.mod h1:liZulR8DgHxdK+MEwvZIylGnmcjzQ6N6f2PlWe7nEO4=
+github.com/henrylee2cn/goutil v0.0.0-20210127050712-89660552f6f8 h1:yE9ULgp02BhYIrO6sdV/FPe0xQM6fNHkVQW2IAymfM0=
+github.com/henrylee2cn/goutil v0.0.0-20210127050712-89660552f6f8/go.mod h1:Nhe/DM3671a5udlv2AdV2ni/MZzgfv2qrPL5nIi3EGQ=
+github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ=
+github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
+github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
+github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg=
+github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
+github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
+github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
+github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
+github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
+github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
+github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
+github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
+github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
+github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
+github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
+github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
+github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
+github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
+github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
+github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw=
+github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
+github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
+github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
+github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
+github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
+github.com/mattn/goveralls v0.0.2 h1:7eJB6EqsPhRVxvwEXGnqdO2sJI0PTsrWoTMXEk9/OQc=
+github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
+github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
+github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/mojocn/base64Captcha v1.3.1 h1:2Wbkt8Oc8qjmNJ5GyOfSo4tgVQPsbKMftqASnq8GlT0=
+github.com/mojocn/base64Captcha v1.3.1/go.mod h1:wAQCKEc5bDujxKRmbT6/vTnTt5CjStQ8bRfPWUuz/iY=
+github.com/nsqio/go-nsq v1.0.8 h1:3L2F8tNLlwXXlp2slDUrUWSBn2O3nMh8R1/KEDFTHPk=
+github.com/nsqio/go-nsq v1.0.8/go.mod h1:vKq36oyeVXgsS5Q8YEO7WghqidAVXQlcFxzQbQTuDEY=
+github.com/nyaruka/phonenumbers v1.0.55 h1:bj0nTO88Y68KeUQ/n3Lo2KgK7lM1hF7L9NFuwcCl3yg=
+github.com/nyaruka/phonenumbers v1.0.55/go.mod h1:sDaTZ/KPX5f8qyV9qN+hIm+4ZBARJrupC6LuhshJq1U=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo=
+github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME=
+github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g=
+github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
+github.com/pelletier/go-toml/v2 v2.0.2 h1:+jQXlF3scKIcSEKkdHzXhCTDLPFi5r1wnK6yPS+49Gw=
+github.com/pelletier/go-toml/v2 v2.0.2/go.mod h1:MovirKjgVRESsAvNZlAjtFwV867yGuwRkXbG66OzopI=
+github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
+github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
+github.com/robinjoseph08/redisqueue/v2 v2.1.0 h1:GactHlrxS8YSCJc4CbP1KbTObo14pieNmNWSUlquTGI=
+github.com/robinjoseph08/redisqueue/v2 v2.1.0/go.mod h1:fiNYBqIF/DqZxKvO6faLB6nkBtfJucuM+rDbFhZG1vs=
+github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
+github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
+github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
+github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/shamsher31/goimgext v1.0.0 h1:wFKf9GeeE0Xr6UQtliaPgYYgTju2izobM7XpCEgUCC8=
+github.com/shamsher31/goimgext v1.0.0/go.mod h1:rYLKgXuTGBIaH49z+jUVSWz7gUWIZmqvYUsdvJbNNOc=
+github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
+github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
+github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
+github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
+github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
+github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
+github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
+github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/tidwall/gjson v1.6.0/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls=
+github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=
+github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
+github.com/tsuyoshiwada/go-gitcmd v0.0.0-20180205145712-5f1f5f9475df h1:Y2l28Jr3vOEeYtxfVbMtVfOdAwuUqWaP9fvNKiBVeXY=
+github.com/tsuyoshiwada/go-gitcmd v0.0.0-20180205145712-5f1f5f9475df/go.mod h1:pnyouUty/nBr/zm3GYwTIt+qFTLWbdjeLjZmJdzJOu8=
+github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
+github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
+github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
+github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
+github.com/urfave/cli v1.22.1 h1:+mkCCcOFKPnCmVYVcURKps1Xe+3zP90gSYGNfRkjoIY=
+github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
+go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk=
+go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
+go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A=
+go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
+go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4=
+go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
+go.uber.org/zap v1.15.0 h1:ZZCA22JRF2gQE5FoNmhmrf7jeJJ2uhqDUNRYKm8dvmM=
+go.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc=
+golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c=
+golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/image v0.0.0-20190501045829-6d32002ffd75/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/image v0.0.0-20220722155232-062f8c9fd539 h1:/eM0PCrQI2xd471rI+snWuu251/+/jpBpZqir2mPdnU=
+golang.org/x/image v0.0.0-20220722155232-062f8c9fd539/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f h1:J5lckAjkw6qYlOZNj90mLYNTEKDvWeuc1yieZ8qUzUE=
+golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
+golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20220809184613-07c6da5e1ced h1:3dYNDff0VT5xj+mbj2XucFst9WKk6PdGOrb9n+SbIvw=
+golang.org/x/net v0.0.0-20220809184613-07c6da5e1ced/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180606202747-9527bec2660b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU=
+golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190706070813-72ffa07ba3db/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
+golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
+google.golang.org/grpc v1.29.1 h1:EC2SB8S04d2r73uptxphDSUG+kTKVgjRPF+N3xpxRB4=
+google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
+google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+gopkg.in/AlecAivazis/survey.v1 v1.8.5 h1:QoEEmn/d5BbuPIL2qvXwzJdttFFhRQFkaq+tEKb7SMI=
+gopkg.in/AlecAivazis/survey.v1 v1.8.5/go.mod h1:iBNOmqKz/NUbZx3bA+4hAGLRC7fSK7tgtVDT4tB22XA=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/kyokomi/emoji.v1 v1.5.1 h1:beetH5mWDMzFznJ+Qzd5KVHp79YKhVUMcdO8LpRLeGw=
+gopkg.in/kyokomi/emoji.v1 v1.5.1/go.mod h1:N9AZ6hi1jHOPn34PsbpufQZUcKftSD7WgS2pgpmH4Lg=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gorm.io/driver/mysql v1.3.2/go.mod h1:ChK6AHbHgDCFZyJp0F+BmVGb06PSIoh9uVYKAlRbb2U=
+gorm.io/driver/mysql v1.3.5 h1:iWBTVW/8Ij5AG4e0G/zqzaJblYkBI1VIL1LG2HUGsvY=
+gorm.io/driver/mysql v1.3.5/go.mod h1:sSIebwZAVPiT+27jK9HIwvsqOGKx3YMPmrA3mBJR10c=
+gorm.io/gorm v1.23.1/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
+gorm.io/gorm v1.23.4/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
+gorm.io/gorm v1.23.8 h1:h8sGJ+biDgBA1AD1Ha9gFCx7h8npU7AsLdlkX0n2TpE=
+gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
+gorm.io/plugin/dbresolver v1.2.2 h1:z8Qx40jHUGb3aOwNIg1+4sEeiF6vGo0GWiAn2P7NWkE=
+gorm.io/plugin/dbresolver v1.2.2/go.mod h1:kWKz6XWRmz6KGBuHmGqvmAm8ioy8Y9sIhCPmissORLM=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
+honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=

+ 14 - 0
logger/context.go

@@ -0,0 +1,14 @@
+package logger
+
+import "context"
+
+type loggerKey struct{}
+
+func FromContext(ctx context.Context) (*Helper, bool) {
+	l, ok := ctx.Value(&loggerKey{}).(*Helper)
+	return l, ok
+}
+
+func NewContext(ctx context.Context, l *Helper) context.Context {
+	return context.WithValue(ctx, &loggerKey{}, l)
+}

+ 182 - 0
logger/default.go

@@ -0,0 +1,182 @@
+package logger
+
+import (
+	"context"
+	"fmt"
+	"log"
+	"os"
+	"runtime"
+	"sort"
+	"strings"
+	"sync"
+	"time"
+
+	dlog "git.baozhida.cn/OAuth-core/debug/log"
+)
+
+func init() {
+	lvl, err := GetLevel(os.Getenv("GO_ADMIN_LOG_LEVEL"))
+	if err != nil {
+		lvl = InfoLevel
+	}
+
+	DefaultLogger = NewHelper(NewLogger(WithLevel(lvl)))
+}
+
+type defaultLogger struct {
+	sync.RWMutex
+	opts Options
+}
+
+// Init (opts...) should only overwrite provided options
+func (l *defaultLogger) Init(opts ...Option) error {
+	for _, o := range opts {
+		o(&l.opts)
+	}
+	return nil
+}
+
+func (l *defaultLogger) String() string {
+	return "default"
+}
+
+func (l *defaultLogger) Fields(fields map[string]interface{}) Logger {
+	l.Lock()
+	l.opts.Fields = copyFields(fields)
+	l.Unlock()
+	return l
+}
+
+func copyFields(src map[string]interface{}) map[string]interface{} {
+	dst := make(map[string]interface{}, len(src))
+	for k, v := range src {
+		dst[k] = v
+	}
+	return dst
+}
+
+// logCallerfilePath returns a package/file:line description of the caller,
+// preserving only the leaf directory name and file name.
+func logCallerfilePath(loggingFilePath string) string {
+	// To make sure we trim the path correctly on Windows too, we
+	// counter-intuitively need to use '/' and *not* os.PathSeparator here,
+	// because the path given originates from Go stdlib, specifically
+	// runtime.Caller() which (as of Mar/17) returns forward slashes even on
+	// Windows.
+	//
+	// See https://github.com/golang/go/issues/3335
+	// and https://github.com/golang/go/issues/18151
+	//
+	// for discussion on the issue on Go side.
+	idx := strings.LastIndexByte(loggingFilePath, '/')
+	if idx == -1 {
+		return loggingFilePath
+	}
+	idx = strings.LastIndexByte(loggingFilePath[:idx], '/')
+	if idx == -1 {
+		return loggingFilePath
+	}
+	return loggingFilePath[idx+1:]
+}
+
+func (l *defaultLogger) Log(level Level, v ...interface{}) {
+	l.logf(level, "", v...)
+}
+
+func (l *defaultLogger) Logf(level Level, format string, v ...interface{}) {
+	l.logf(level, format, v...)
+}
+
+func (l *defaultLogger) logf(level Level, format string, v ...interface{}) {
+	// TODO decide does we need to write message if log level not used?
+	if !l.opts.Level.Enabled(level) {
+		return
+	}
+
+	l.RLock()
+	fields := copyFields(l.opts.Fields)
+	l.RUnlock()
+
+	fields["level"] = level.String()
+
+	if _, file, line, ok := runtime.Caller(l.opts.CallerSkipCount); ok {
+		fields["file"] = fmt.Sprintf("%s:%d", logCallerfilePath(file), line)
+	}
+
+	rec := dlog.Record{
+		Timestamp: time.Now(),
+		Metadata:  make(map[string]string, len(fields)),
+	}
+	if format == "" {
+		rec.Message = fmt.Sprint(v...)
+	} else {
+		rec.Message = fmt.Sprintf(format, v...)
+	}
+
+	keys := make([]string, 0, len(fields))
+	for k, v := range fields {
+		keys = append(keys, k)
+		rec.Metadata[k] = fmt.Sprintf("%v", v)
+	}
+
+	sort.Strings(keys)
+	metadata := ""
+
+	for i, k := range keys {
+		if i == 0 {
+			metadata += fmt.Sprintf("%s:%v", k, fields[k])
+		} else {
+			metadata += fmt.Sprintf(" %s:%v", k, fields[k])
+		}
+	}
+
+	var name string
+	if l.opts.Name != "" {
+		name = "[" + l.opts.Name + "]"
+	}
+	t := rec.Timestamp.Format("2006-01-02 15:04:05.000Z0700")
+	//fmt.Printf("%s\n", t)
+	//fmt.Printf("%s\n", name)
+	//fmt.Printf("%s\n", metadata)
+	//fmt.Printf("%v\n", rec.Message)
+	logStr := ""
+	if name == "" {
+		logStr = fmt.Sprintf("%s %s %v\n", t, metadata, rec.Message)
+	} else {
+		logStr = fmt.Sprintf("%s %s %s %v\n", name, t, metadata, rec.Message)
+	}
+	_, err := l.opts.Out.Write([]byte(logStr))
+	if err != nil {
+		log.Printf("log [Logf] write error: %s \n", err.Error())
+	}
+
+}
+
+func (l *defaultLogger) Options() Options {
+	// not guard against options Context values
+	l.RLock()
+	opts := l.opts
+	opts.Fields = copyFields(l.opts.Fields)
+	l.RUnlock()
+	return opts
+}
+
+// NewLogger builds a new logger based on options
+func NewLogger(opts ...Option) Logger {
+	// Default options
+	options := Options{
+		Level:           InfoLevel,
+		Fields:          make(map[string]interface{}),
+		Out:             os.Stderr,
+		CallerSkipCount: 3,
+		Context:         context.Background(),
+		Name:            "",
+	}
+
+	l := &defaultLogger{opts: options}
+	if err := l.Init(opts...); err != nil {
+		l.Log(FatalLevel, err)
+	}
+
+	return l
+}

+ 114 - 0
logger/helper.go

@@ -0,0 +1,114 @@
+package logger
+
+import (
+	"os"
+)
+
+type Helper struct {
+	Logger
+	fields map[string]interface{}
+}
+
+func NewHelper(log Logger) *Helper {
+	return &Helper{Logger: log}
+}
+
+func (h *Helper) Info(args ...interface{}) {
+	if !h.Logger.Options().Level.Enabled(InfoLevel) {
+		return
+	}
+	h.Logger.Fields(h.fields).Log(InfoLevel, args...)
+}
+
+func (h *Helper) Infof(template string, args ...interface{}) {
+	if !h.Logger.Options().Level.Enabled(InfoLevel) {
+		return
+	}
+	h.Logger.Fields(h.fields).Logf(InfoLevel, template, args...)
+}
+
+func (h *Helper) Trace(args ...interface{}) {
+	if !h.Logger.Options().Level.Enabled(TraceLevel) {
+		return
+	}
+	h.Logger.Fields(h.fields).Log(TraceLevel, args...)
+}
+
+func (h *Helper) Tracef(template string, args ...interface{}) {
+	if !h.Logger.Options().Level.Enabled(TraceLevel) {
+		return
+	}
+	h.Logger.Fields(h.fields).Logf(TraceLevel, template, args...)
+}
+
+func (h *Helper) Debug(args ...interface{}) {
+	if !h.Logger.Options().Level.Enabled(DebugLevel) {
+		return
+	}
+	h.Logger.Fields(h.fields).Log(DebugLevel, args...)
+}
+
+func (h *Helper) Debugf(template string, args ...interface{}) {
+	if !h.Logger.Options().Level.Enabled(DebugLevel) {
+		return
+	}
+	h.Logger.Fields(h.fields).Logf(DebugLevel, template, args...)
+}
+
+func (h *Helper) Warn(args ...interface{}) {
+	if !h.Logger.Options().Level.Enabled(WarnLevel) {
+		return
+	}
+	h.Logger.Fields(h.fields).Log(WarnLevel, args...)
+}
+
+func (h *Helper) Warnf(template string, args ...interface{}) {
+	if !h.Logger.Options().Level.Enabled(WarnLevel) {
+		return
+	}
+	h.Logger.Fields(h.fields).Logf(WarnLevel, template, args...)
+}
+
+func (h *Helper) Error(args ...interface{}) {
+	if !h.Logger.Options().Level.Enabled(ErrorLevel) {
+		return
+	}
+	h.Logger.Fields(h.fields).Log(ErrorLevel, args...)
+}
+
+func (h *Helper) Errorf(template string, args ...interface{}) {
+	if !h.Logger.Options().Level.Enabled(ErrorLevel) {
+		return
+	}
+	h.Logger.Fields(h.fields).Logf(ErrorLevel, template, args...)
+}
+
+func (h *Helper) Fatal(args ...interface{}) {
+	if !h.Logger.Options().Level.Enabled(FatalLevel) {
+		return
+	}
+	h.Logger.Fields(h.fields).Log(FatalLevel, args...)
+	os.Exit(1)
+}
+
+func (h *Helper) Fatalf(template string, args ...interface{}) {
+	if !h.Logger.Options().Level.Enabled(FatalLevel) {
+		return
+	}
+	h.Logger.Fields(h.fields).Logf(FatalLevel, template, args...)
+	os.Exit(1)
+}
+
+func (h *Helper) WithError(err error) *Helper {
+	fields := copyFields(h.fields)
+	fields["error"] = err
+	return &Helper{Logger: h.Logger, fields: fields}
+}
+
+func (h *Helper) WithFields(fields map[string]interface{}) *Helper {
+	nfields := copyFields(fields)
+	for k, v := range h.fields {
+		nfields[k] = v
+	}
+	return &Helper{Logger: h.Logger, fields: nfields}
+}

+ 140 - 0
logger/level.go

@@ -0,0 +1,140 @@
+package logger
+
+import (
+	"fmt"
+	"os"
+)
+
+type Level int8
+
+const (
+	// TraceLevel level. Designates finer-grained informational events than the Debug.
+	TraceLevel Level = iota - 2
+	// DebugLevel level. Usually only enabled when debugging. Very verbose logging.
+	DebugLevel
+	// InfoLevel is the default logging priority.
+	// General operational entries about what's going on inside the application.
+	InfoLevel
+	// WarnLevel level. Non-critical entries that deserve eyes.
+	WarnLevel
+	// ErrorLevel level. Logs. Used for errors that should definitely be noted.
+	ErrorLevel
+	// FatalLevel level. Logs and then calls `logger.Exit(1)`. highest level of severity.
+	FatalLevel
+)
+
+func (l Level) String() string {
+	switch l {
+	case TraceLevel:
+		return "trace"
+	case DebugLevel:
+		return "debug"
+	case InfoLevel:
+		return "info"
+	case WarnLevel:
+		return "warn"
+	case ErrorLevel:
+		return "error"
+	case FatalLevel:
+		return "fatal"
+	}
+	return ""
+}
+
+// LevelForGorm 转换成gorm日志级别
+func (l Level) LevelForGorm() int {
+	switch l {
+	case FatalLevel, ErrorLevel:
+		return 2
+	case WarnLevel:
+		return 3
+	case InfoLevel, DebugLevel, TraceLevel:
+		return 4
+	default:
+		return 1
+	}
+}
+
+// Enabled returns true if the given level is at or above this level.
+func (l Level) Enabled(lvl Level) bool {
+	return lvl >= l
+}
+
+// GetLevel converts a level string into a logger Level value.
+// returns an error if the input string does not match known values.
+func GetLevel(levelStr string) (Level, error) {
+	switch levelStr {
+	case TraceLevel.String():
+		return TraceLevel, nil
+	case DebugLevel.String():
+		return DebugLevel, nil
+	case InfoLevel.String():
+		return InfoLevel, nil
+	case WarnLevel.String():
+		return WarnLevel, nil
+	case ErrorLevel.String():
+		return ErrorLevel, nil
+	case FatalLevel.String():
+		return FatalLevel, nil
+	}
+	return InfoLevel, fmt.Errorf("Unknown Level String: '%s', defaulting to InfoLevel", levelStr)
+}
+
+func Info(args ...interface{}) {
+	DefaultLogger.Log(InfoLevel, args...)
+}
+
+func Infof(template string, args ...interface{}) {
+	DefaultLogger.Logf(InfoLevel, template, args...)
+}
+
+func Trace(args ...interface{}) {
+	DefaultLogger.Log(TraceLevel, args...)
+}
+
+func Tracef(template string, args ...interface{}) {
+	DefaultLogger.Logf(TraceLevel, template, args...)
+}
+
+func Debug(args ...interface{}) {
+	DefaultLogger.Log(DebugLevel, args...)
+}
+
+func Debugf(template string, args ...interface{}) {
+	DefaultLogger.Logf(DebugLevel, template, args...)
+}
+
+func Warn(args ...interface{}) {
+	DefaultLogger.Log(WarnLevel, args...)
+}
+
+func Warnf(template string, args ...interface{}) {
+	DefaultLogger.Logf(WarnLevel, template, args...)
+}
+
+func Error(args ...interface{}) {
+	DefaultLogger.Log(ErrorLevel, args...)
+}
+
+func Errorf(template string, args ...interface{}) {
+	DefaultLogger.Logf(ErrorLevel, template, args...)
+}
+
+func Fatal(args ...interface{}) {
+	DefaultLogger.Log(FatalLevel, args...)
+	os.Exit(1)
+}
+
+func Fatalf(template string, args ...interface{}) {
+	DefaultLogger.Logf(FatalLevel, template, args...)
+	os.Exit(1)
+}
+
+// Returns true if the given level is at or lower the current logger level
+func V(lvl Level, log Logger) bool {
+	l := DefaultLogger
+	if log != nil {
+		l = log
+	}
+	return l.Options().Level <= lvl
+}

+ 42 - 0
logger/logger.go

@@ -0,0 +1,42 @@
+package logger
+
+var (
+	// DefaultLogger logger
+	DefaultLogger Logger
+)
+
+// Logger is a generic logging interface
+type Logger interface {
+	// Init initialises options
+	Init(options ...Option) error
+	// Options The Logger options
+	Options() Options
+	// Fields set fields to always be logged
+	Fields(fields map[string]interface{}) Logger
+	// Log writes a log entry
+	Log(level Level, v ...interface{})
+	// Logf writes a formatted log entry
+	Logf(level Level, format string, v ...interface{})
+	// String returns the name of logger
+	String() string
+}
+
+func Init(opts ...Option) error {
+	return DefaultLogger.Init(opts...)
+}
+
+func Fields(fields map[string]interface{}) Logger {
+	return DefaultLogger.Fields(fields)
+}
+
+func Log(level Level, v ...interface{}) {
+	DefaultLogger.Log(level, v...)
+}
+
+func Logf(level Level, format string, v ...interface{}) {
+	DefaultLogger.Logf(level, format, v...)
+}
+
+func String() string {
+	return DefaultLogger.String()
+}

+ 25 - 0
logger/logger_test.go

@@ -0,0 +1,25 @@
+package logger
+
+import (
+	"context"
+	"testing"
+)
+
+func TestLogger(t *testing.T) {
+	l := NewLogger(WithLevel(TraceLevel), WithName("test"))
+	h1 := NewHelper(l).WithFields(map[string]interface{}{"key1": "val1"})
+	h1.Trace("trace_msg1")
+	h1.Warn("warn_msg1")
+
+	h2 := NewHelper(l).WithFields(map[string]interface{}{"key2": "val2"})
+	h2.Trace("trace_msg2")
+	h2.Warn("warn_msg2")
+
+	h3 := NewHelper(l).WithFields(map[string]interface{}{"key3": "val4"})
+	h3.Info("test_msg")
+	ctx := context.TODO()
+	ctx = context.WithValue(ctx, &loggerKey{}, h3)
+	v := ctx.Value(&loggerKey{})
+	ll := v.(*Helper)
+	ll.Info("test_msg")
+}

+ 67 - 0
logger/options.go

@@ -0,0 +1,67 @@
+package logger
+
+import (
+	"context"
+	"io"
+)
+
+type Option func(*Options)
+
+type Options struct {
+	// The logging level the logger should log at. default is `InfoLevel`
+	Level Level
+	// fields to always be logged
+	Fields map[string]interface{}
+	// It's common to set this to a file, or leave it default which is `os.Stderr`
+	Out io.Writer
+	// Caller skip frame count for file:line info
+	CallerSkipCount int
+	// Alternative options
+	Context context.Context
+	// Name logger name
+	Name string
+}
+
+// WithFields set default fields for the logger
+func WithFields(fields map[string]interface{}) Option {
+	return func(args *Options) {
+		args.Fields = fields
+	}
+}
+
+// WithLevel set default level for the logger
+func WithLevel(level Level) Option {
+	return func(args *Options) {
+		args.Level = level
+	}
+}
+
+// WithOutput set default output writer for the logger
+func WithOutput(out io.Writer) Option {
+	return func(args *Options) {
+		args.Out = out
+	}
+}
+
+// WithCallerSkipCount set frame count to skip
+func WithCallerSkipCount(c int) Option {
+	return func(args *Options) {
+		args.CallerSkipCount = c
+	}
+}
+
+// WithName set name for logger
+func WithName(name string) Option {
+	return func(args *Options) {
+		args.Name = name
+	}
+}
+
+func SetOption(k, v interface{}) Option {
+	return func(o *Options) {
+		if o.Context == nil {
+			o.Context = context.Background()
+		}
+		o.Context = context.WithValue(o.Context, k, v)
+	}
+}

+ 10 - 0
model/role.go

@@ -0,0 +1,10 @@
+package model
+
+type Role struct {
+	Name      string `json:"name" gorm:"size:128;"`                     // 角色名称
+	Status    string `json:"status" gorm:"size:4;not null;default:'2'"` // 1-停用 2-正常
+	RoleKey   string `json:"roleKey" gorm:"size:128;not null;"`         //角色代码
+	Sort      int    `json:"sort" gorm:""`                              //角色排序
+	DataScope string `json:"dataScope" gorm:"size:128;"`
+	Remark    string `json:"remark" gorm:"size:255;comment:备注"` // 备注
+}

+ 15 - 0
model/user.go

@@ -0,0 +1,15 @@
+package model
+
+type User struct {
+	Uuid     string `json:"uuid" gorm:"<-:create;size:64;not null;comment:uuid"`
+	Username string `json:"username" gorm:"size:64;not null;comment:用户名"` // 用户名
+	Password string `json:"-" gorm:"size:128;comment:密码"`                 // 密码
+	NickName string `json:"nickName" gorm:"size:128;comment:昵称"`          // 昵称
+	Phone    string `json:"phone" gorm:"size:11;not null;comment:手机号"`    // 手机号
+	RoleId   int    `json:"roleId" gorm:"comment:角色ID"`                   // 角色id
+	Salt     string `json:"-" gorm:"size:255;comment:加盐"`
+	DeptId   int    `json:"deptId" gorm:"comment:部门"`                             // 部门id
+	PostId   int    `json:"PostId" gorm:"comment:岗位"`                             // 岗位id
+	Remark   string `json:"remark" gorm:"size:255;comment:备注"`                    // 备注
+	Status   string `json:"status" gorm:"size:4;not null;default:'2';comment:状态"` // 1-停用 2-正常
+}

+ 48 - 0
pkg/captcha/captcha.go

@@ -0,0 +1,48 @@
+package captcha
+
+import (
+	"image/color"
+
+	"github.com/google/uuid"
+	"github.com/mojocn/base64Captcha"
+)
+
+// SetStore 设置store
+func SetStore(s base64Captcha.Store) {
+	base64Captcha.DefaultMemStore = s
+}
+
+//configJsonBody json request body.
+type configJsonBody struct {
+	Id            string
+	CaptchaType   string
+	VerifyValue   string
+	DriverAudio   *base64Captcha.DriverAudio
+	DriverString  *base64Captcha.DriverString
+	DriverChinese *base64Captcha.DriverChinese
+	DriverMath    *base64Captcha.DriverMath
+	DriverDigit   *base64Captcha.DriverDigit
+}
+
+func DriverStringFunc() (id, b64s string, err error) {
+	e := configJsonBody{}
+	e.Id = uuid.New().String()
+	e.DriverString = base64Captcha.NewDriverString(46, 140, 2, 2, 4, "234567890abcdefghjkmnpqrstuvwxyz", &color.RGBA{240, 240, 246, 246}, []string{"wqy-microhei.ttc"})
+	driver := e.DriverString.ConvertFonts()
+	cap := base64Captcha.NewCaptcha(driver, base64Captcha.DefaultMemStore)
+	return cap.Generate()
+}
+
+func DriverDigitFunc() (id, b64s string, err error) {
+	e := configJsonBody{}
+	e.Id = uuid.New().String()
+	e.DriverDigit = base64Captcha.NewDriverDigit(80, 240, 4, 0.7, 80)
+	driver := e.DriverDigit
+	cap := base64Captcha.NewCaptcha(driver, base64Captcha.DefaultMemStore)
+	return cap.Generate()
+}
+
+// Verify 校验验证码
+func Verify(id, code string, clear bool) bool {
+	return base64Captcha.DefaultMemStore.Verify(id, code, clear)
+}

+ 45 - 0
pkg/captcha/store.go

@@ -0,0 +1,45 @@
+package captcha
+
+import (
+	"github.com/mojocn/base64Captcha"
+
+	"git.baozhida.cn/OAuth-core/storage"
+)
+
+type cacheStore struct {
+	cache      storage.AdapterCache
+	expiration int
+}
+
+// NewCacheStore returns a new standard memory store for captchas with the
+// given collection threshold and expiration time (duration). The returned
+// store must be registered with SetCustomStore to replace the default one.
+func NewCacheStore(cache storage.AdapterCache, expiration int) base64Captcha.Store {
+	s := new(cacheStore)
+	s.cache = cache
+	s.expiration = expiration
+	return s
+}
+
+// Set sets the digits for the captcha id.
+func (e *cacheStore) Set(id string, value string) {
+	_ = e.cache.Set(id, value, e.expiration)
+}
+
+// Get returns stored digits for the captcha id. Clear indicates
+// whether the captcha must be deleted from the store.
+func (e *cacheStore) Get(id string, clear bool) string {
+	v, err := e.cache.Get(id)
+	if err == nil {
+		if clear {
+			_ = e.cache.Del(id)
+		}
+		return v
+	}
+	return ""
+}
+
+//Verify captcha's answer directly
+func (e *cacheStore) Verify(id, answer string, clear bool) bool {
+	return e.Get(id, clear) == answer
+}

+ 104 - 0
pkg/captcha/store_test.go

@@ -0,0 +1,104 @@
+package captcha
+
+import (
+	"fmt"
+	"math/rand"
+	"testing"
+	"time"
+
+	"github.com/mojocn/base64Captcha"
+
+	"git.baozhida.cn/OAuth-core/storage"
+	"git.baozhida.cn/OAuth-core/storage/cache"
+)
+
+var _expiration = 6000
+
+func getStore(_ *testing.T) storage.AdapterCache {
+	return cache.NewMemory()
+}
+
+func TestSetGet(t *testing.T) {
+	s := NewCacheStore(getStore(t), _expiration)
+	id := "captcha id"
+	d := "random-string"
+	s.Set(id, d)
+	d2 := s.Get(id, false)
+	if d2 != d {
+		t.Errorf("saved %v, getDigits returned got %v", d, d2)
+	}
+}
+
+func TestGetClear(t *testing.T) {
+	s := NewCacheStore(getStore(t), _expiration)
+	id := "captcha id"
+	d := "932839jfffjkdss"
+	s.Set(id, d)
+	d2 := s.Get(id, true)
+	if d != d2 {
+		t.Errorf("saved %v, getDigitsClear returned got %v", d, d2)
+	}
+	d2 = s.Get(id, false)
+	if d2 != "" {
+		t.Errorf("getDigitClear didn't clear (%q=%v)", id, d2)
+	}
+}
+
+func BenchmarkSetCollect(b *testing.B) {
+	store := cache.NewMemory()
+	b.StopTimer()
+	d := "fdskfew9832232r"
+	s := NewCacheStore(store, -1)
+	ids := make([]string, 1000)
+	for i := range ids {
+		ids[i] = fmt.Sprintf("%d", rand.Int63())
+	}
+	b.StartTimer()
+	for i := 0; i < b.N; i++ {
+		for j := 0; j < 1000; j++ {
+			s.Set(ids[j], d)
+		}
+	}
+}
+func TestStore_SetGoCollect(t *testing.T) {
+	s := NewCacheStore(getStore(t), -1)
+	for i := 0; i <= 100; i++ {
+		s.Set(fmt.Sprint(i), fmt.Sprint(i))
+	}
+}
+
+func TestStore_CollectNotExpire(t *testing.T) {
+	s := NewCacheStore(getStore(t), 36000)
+	for i := 0; i < 50; i++ {
+		s.Set(fmt.Sprint(i), fmt.Sprint(i))
+	}
+
+	// let background goroutine to go
+	time.Sleep(time.Second)
+
+	if v := s.Get("0", false); v != "0" {
+		t.Error("cache store get failed")
+	}
+}
+
+func TestNewCacheStore(t *testing.T) {
+	type args struct {
+		store      storage.AdapterCache
+		expiration int
+	}
+	tests := []struct {
+		name string
+		args args
+		want base64Captcha.Store
+	}{
+		{"", args{getStore(t), 36000}, nil},
+		{"", args{getStore(t), 180000}, nil},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := NewCacheStore(tt.args.store, tt.args.expiration); got == nil {
+				t.Errorf("NewMemoryStore() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}

+ 456 - 0
pkg/casbin/adapter.go

@@ -0,0 +1,456 @@
+// Copyright 2017 The casbin Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package mycasbin
+
+import (
+	"errors"
+	"fmt"
+	"strings"
+
+	"gorm.io/driver/mysql"
+	"gorm.io/gorm"
+
+	"github.com/casbin/casbin/v2/model"
+	"github.com/casbin/casbin/v2/persist"
+)
+
+const (
+	defaultDatabaseName = "casbin"
+	defaultTableName    = "sys_casbin_rule"
+)
+
+type customTableKey struct{}
+
+type CasbinRule struct {
+	PType string `gorm:"size:100"`
+	V0    string `gorm:"size:100"`
+	V1    string `gorm:"size:100"`
+	V2    string `gorm:"size:100"`
+	V3    string `gorm:"size:100"`
+	V4    string `gorm:"size:100"`
+	V5    string `gorm:"size:100"`
+}
+
+func (CasbinRule) TableName() string {
+	return "sys_casbin_rule"
+}
+
+type Filter struct {
+	PType []string
+	V0    []string
+	V1    []string
+	V2    []string
+	V3    []string
+	V4    []string
+	V5    []string
+}
+
+// Adapter represents the Gorm adapter for policy storage.
+type Adapter struct {
+	dataSourceName string
+	databaseName   string
+	tablePrefix    string
+	tableName      string
+	db             *gorm.DB
+	isFiltered     bool
+}
+
+// NewAdapterByDBUseTableName creates gorm-adapter by an existing Gorm instance and the specified table prefix and table name
+// Example: gormadapter.NewAdapterByDBUseTableName(&db, "cms", "casbin") Automatically generate table name like this "cms_casbin"
+func NewAdapterByDBUseTableName(db *gorm.DB, prefix string, tableName string) (*Adapter, error) {
+	if len(tableName) == 0 {
+		tableName = defaultTableName
+	}
+
+	a := &Adapter{
+		tablePrefix: prefix,
+		tableName:   tableName,
+	}
+
+	a.db = db.Scopes(a.casbinRuleTable()).Session(&gorm.Session{Context: db.Statement.Context})
+	err := a.createTable()
+	if err != nil {
+		return nil, err
+	}
+
+	return a, nil
+}
+
+// NewAdapterByDB creates gorm-adapter by an existing Gorm instance
+func NewAdapterByDB(db *gorm.DB) (*Adapter, error) {
+	return NewAdapterByDBUseTableName(db, "", defaultTableName)
+}
+
+func openDBConnection(dataSourceName string) (*gorm.DB, error) {
+	return gorm.Open(mysql.Open(dataSourceName), &gorm.Config{})
+}
+
+func (a *Adapter) open() error {
+	var err error
+	var db *gorm.DB
+
+	db, err = openDBConnection(a.dataSourceName)
+	if err != nil {
+		return err
+	}
+
+	a.db = db.Scopes(a.casbinRuleTable()).Session(&gorm.Session{})
+	return a.createTable()
+}
+
+func (a *Adapter) close() error {
+	a.db = nil
+	return nil
+}
+
+// getTableInstance return the dynamic table name
+func (a *Adapter) getTableInstance() *CasbinRule {
+	return &CasbinRule{}
+}
+
+func (a *Adapter) getFullTableName() string {
+	if a.tablePrefix != "" {
+		return a.tablePrefix + "_" + a.tableName
+	}
+	return a.tableName
+}
+
+func (a *Adapter) casbinRuleTable() func(db *gorm.DB) *gorm.DB {
+	return func(db *gorm.DB) *gorm.DB {
+		tableName := a.getFullTableName()
+		return db.Table(tableName)
+	}
+}
+
+func (a *Adapter) createTable() error {
+	t := a.db.Statement.Context.Value(customTableKey{})
+
+	if t == nil {
+		t = a.getTableInstance()
+	}
+
+	if err := a.db.AutoMigrate(t); err != nil {
+		return err
+	}
+
+	tableName := a.getFullTableName()
+	index := "idx_" + tableName
+	hasIndex := a.db.Migrator().HasIndex(t, index)
+	if !hasIndex {
+		if err := a.db.Exec(fmt.Sprintf("CREATE UNIQUE INDEX %s ON %s (p_type,v0,v1,v2,v3,v4,v5)", index, tableName)).Error; err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func (a *Adapter) dropTable() error {
+	t := a.db.Statement.Context.Value(customTableKey{})
+	if t == nil {
+		return a.db.Migrator().DropTable(a.getTableInstance())
+	}
+
+	return a.db.Migrator().DropTable(t)
+}
+
+func loadPolicyLine(line CasbinRule, model model.Model) {
+	var p = []string{line.PType,
+		line.V0, line.V1, line.V2, line.V3, line.V4, line.V5}
+
+	var lineText string
+	if line.V5 != "" {
+		lineText = strings.Join(p, ", ")
+	} else if line.V4 != "" {
+		lineText = strings.Join(p[:6], ", ")
+	} else if line.V3 != "" {
+		lineText = strings.Join(p[:5], ", ")
+	} else if line.V2 != "" {
+		lineText = strings.Join(p[:4], ", ")
+	} else if line.V1 != "" {
+		lineText = strings.Join(p[:3], ", ")
+	} else if line.V0 != "" {
+		lineText = strings.Join(p[:2], ", ")
+	}
+
+	persist.LoadPolicyLine(lineText, model)
+}
+
+// LoadPolicy loads policy from database.
+func (a *Adapter) LoadPolicy(model model.Model) error {
+	var lines []CasbinRule
+	if err := a.db.Find(&lines).Error; err != nil {
+		return err
+	}
+
+	for _, line := range lines {
+		loadPolicyLine(line, model)
+	}
+
+	return nil
+}
+
+// LoadFilteredPolicy loads only policy rules that match the filter.
+func (a *Adapter) LoadFilteredPolicy(model model.Model, filter interface{}) error {
+	var lines []CasbinRule
+
+	filterValue, ok := filter.(Filter)
+	if !ok {
+		return errors.New("invalid filter type")
+	}
+
+	if err := a.db.Scopes(a.filterQuery(a.db, filterValue)).Find(&lines).Error; err != nil {
+		return err
+	}
+
+	for _, line := range lines {
+		loadPolicyLine(line, model)
+	}
+	a.isFiltered = true
+
+	return nil
+}
+
+// IsFiltered returns true if the loaded policy has been filtered.
+func (a *Adapter) IsFiltered() bool {
+	return a.isFiltered
+}
+
+// filterQuery builds the gorm query to match the rule filter to use within a scope.
+func (a *Adapter) filterQuery(db *gorm.DB, filter Filter) func(db *gorm.DB) *gorm.DB {
+	return func(db *gorm.DB) *gorm.DB {
+		if len(filter.PType) > 0 {
+			db = db.Where("p_type in (?)", filter.PType)
+		}
+		if len(filter.V0) > 0 {
+			db = db.Where("v0 in (?)", filter.V0)
+		}
+		if len(filter.V1) > 0 {
+			db = db.Where("v1 in (?)", filter.V1)
+		}
+		if len(filter.V2) > 0 {
+			db = db.Where("v2 in (?)", filter.V2)
+		}
+		if len(filter.V3) > 0 {
+			db = db.Where("v3 in (?)", filter.V3)
+		}
+		if len(filter.V4) > 0 {
+			db = db.Where("v4 in (?)", filter.V4)
+		}
+		if len(filter.V5) > 0 {
+			db = db.Where("v5 in (?)", filter.V5)
+		}
+		return db
+	}
+}
+
+func (a *Adapter) savePolicyLine(ptype string, rule []string) CasbinRule {
+	line := a.getTableInstance()
+
+	line.PType = ptype
+	if len(rule) > 0 {
+		line.V0 = rule[0]
+	}
+	if len(rule) > 1 {
+		line.V1 = rule[1]
+	}
+	if len(rule) > 2 {
+		line.V2 = rule[2]
+	}
+	if len(rule) > 3 {
+		line.V3 = rule[3]
+	}
+	if len(rule) > 4 {
+		line.V4 = rule[4]
+	}
+	if len(rule) > 5 {
+		line.V5 = rule[5]
+	}
+
+	return *line
+}
+
+// SavePolicy saves policy to database.
+func (a *Adapter) SavePolicy(model model.Model) error {
+	err := a.dropTable()
+	if err != nil {
+		return err
+	}
+	err = a.createTable()
+	if err != nil {
+		return err
+	}
+
+	for ptype, ast := range model["p"] {
+		for _, rule := range ast.Policy {
+			line := a.savePolicyLine(ptype, rule)
+			err := a.db.Create(&line).Error
+			if err != nil {
+				return err
+			}
+		}
+	}
+
+	for ptype, ast := range model["g"] {
+		for _, rule := range ast.Policy {
+			line := a.savePolicyLine(ptype, rule)
+			err := a.db.Create(&line).Error
+			if err != nil {
+				return err
+			}
+		}
+	}
+
+	return nil
+}
+
+// AddPolicy adds a policy rule to the storage.
+func (a *Adapter) AddPolicy(sec string, ptype string, rule []string) error {
+	line := a.savePolicyLine(ptype, rule)
+	err := a.db.Create(&line).Error
+	return err
+}
+
+// RemovePolicy removes a policy rule from the storage.
+func (a *Adapter) RemovePolicy(sec string, ptype string, rule []string) error {
+	line := a.savePolicyLine(ptype, rule)
+	err := a.rawDelete(a.db, line) //can't use db.Delete as we're not using primary key http://jinzhu.me/gorm/crud.html#delete
+	return err
+}
+
+// AddPolicies adds multiple policy rules to the storage.
+func (a *Adapter) AddPolicies(sec string, ptype string, rules [][]string) error {
+	return a.db.Transaction(func(tx *gorm.DB) error {
+		for _, rule := range rules {
+			line := a.savePolicyLine(ptype, rule)
+			if err := tx.Create(&line).Error; err != nil {
+				return err
+			}
+		}
+		return nil
+	})
+}
+
+// RemovePolicies removes multiple policy rules from the storage.
+func (a *Adapter) RemovePolicies(sec string, ptype string, rules [][]string) error {
+	return a.db.Transaction(func(tx *gorm.DB) error {
+		for _, rule := range rules {
+			line := a.savePolicyLine(ptype, rule)
+			if err := a.rawDelete(tx, line); err != nil { //can't use db.Delete as we're not using primary key http://jinzhu.me/gorm/crud.html#delete
+				return err
+			}
+		}
+		return nil
+	})
+}
+
+// RemoveFilteredPolicy removes policy rules that match the filter from the storage.
+func (a *Adapter) RemoveFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) error {
+	line := a.getTableInstance()
+
+	line.PType = ptype
+	if fieldIndex <= 0 && 0 < fieldIndex+len(fieldValues) {
+		line.V0 = fieldValues[0-fieldIndex]
+	}
+	if fieldIndex <= 1 && 1 < fieldIndex+len(fieldValues) {
+		line.V1 = fieldValues[1-fieldIndex]
+	}
+	if fieldIndex <= 2 && 2 < fieldIndex+len(fieldValues) {
+		line.V2 = fieldValues[2-fieldIndex]
+	}
+	if fieldIndex <= 3 && 3 < fieldIndex+len(fieldValues) {
+		line.V3 = fieldValues[3-fieldIndex]
+	}
+	if fieldIndex <= 4 && 4 < fieldIndex+len(fieldValues) {
+		line.V4 = fieldValues[4-fieldIndex]
+	}
+	if fieldIndex <= 5 && 5 < fieldIndex+len(fieldValues) {
+		line.V5 = fieldValues[5-fieldIndex]
+	}
+	err := a.rawDelete(a.db, *line)
+	return err
+}
+
+func (a *Adapter) rawDelete(db *gorm.DB, line CasbinRule) error {
+	queryArgs := []interface{}{line.PType}
+
+	queryStr := "p_type = ?"
+	if line.V0 != "" {
+		queryStr += " and v0 = ?"
+		queryArgs = append(queryArgs, line.V0)
+	}
+	if line.V1 != "" {
+		queryStr += " and v1 = ?"
+		queryArgs = append(queryArgs, line.V1)
+	}
+	if line.V2 != "" {
+		queryStr += " and v2 = ?"
+		queryArgs = append(queryArgs, line.V2)
+	}
+	if line.V3 != "" {
+		queryStr += " and v3 = ?"
+		queryArgs = append(queryArgs, line.V3)
+	}
+	if line.V4 != "" {
+		queryStr += " and v4 = ?"
+		queryArgs = append(queryArgs, line.V4)
+	}
+	if line.V5 != "" {
+		queryStr += " and v5 = ?"
+		queryArgs = append(queryArgs, line.V5)
+	}
+	args := append([]interface{}{queryStr}, queryArgs...)
+	err := db.Delete(a.getTableInstance(), args...).Error
+	return err
+}
+
+func appendWhere(line CasbinRule) (string, []interface{}) {
+	queryArgs := []interface{}{line.PType}
+
+	queryStr := "p_type = ?"
+	if line.V0 != "" {
+		queryStr += " and v0 = ?"
+		queryArgs = append(queryArgs, line.V0)
+	}
+	if line.V1 != "" {
+		queryStr += " and v1 = ?"
+		queryArgs = append(queryArgs, line.V1)
+	}
+	if line.V2 != "" {
+		queryStr += " and v2 = ?"
+		queryArgs = append(queryArgs, line.V2)
+	}
+	if line.V3 != "" {
+		queryStr += " and v3 = ?"
+		queryArgs = append(queryArgs, line.V3)
+	}
+	if line.V4 != "" {
+		queryStr += " and v4 = ?"
+		queryArgs = append(queryArgs, line.V4)
+	}
+	if line.V5 != "" {
+		queryStr += " and v5 = ?"
+		queryArgs = append(queryArgs, line.V5)
+	}
+	return queryStr, queryArgs
+}
+
+// UpdatePolicy updates a new policy rule to DB.
+func (a *Adapter) UpdatePolicy(sec string, ptype string, oldRule, newPolicy []string) error {
+	oldLine := a.savePolicyLine(ptype, oldRule)
+	queryStr, queryArgs := appendWhere(oldLine)
+	newLine := a.savePolicyLine(ptype, newPolicy)
+	err := a.db.Where(queryStr, queryArgs...).Updates(newLine).Error
+	return err
+}

+ 64 - 0
pkg/casbin/log.go

@@ -0,0 +1,64 @@
+package mycasbin
+
+import (
+	"sync/atomic"
+
+	"git.baozhida.cn/OAuth-core/logger"
+)
+
+// Logger is the implementation for a Logger using golang log.
+type Logger struct {
+	enable int32
+}
+
+// EnableLog controls whether print the message.
+func (l *Logger) EnableLog(enable bool) {
+	i := 0
+	if enable {
+		i = 1
+	}
+	atomic.StoreInt32(&(l.enable), int32(i))
+}
+
+// IsEnabled returns if logger is enabled.
+func (l *Logger) IsEnabled() bool {
+	return atomic.LoadInt32(&(l.enable)) != 0
+}
+
+// LogModel log info related to model.
+func (l *Logger) LogModel(model [][]string) {
+	var str string
+	for i := range model {
+		for j := range model[i] {
+			str += " " + model[i][j]
+		}
+		str += "\n"
+	}
+	logger.DefaultLogger.Log(logger.InfoLevel, str)
+}
+
+// LogEnforce log info related to enforce.
+func (l *Logger) LogEnforce(matcher string, request []interface{}, result bool, explains [][]string) {
+	logger.DefaultLogger.Fields(map[string]interface{}{
+		"matcher":  matcher,
+		"request":  request,
+		"result":   result,
+		"explains": explains,
+	}).Log(logger.InfoLevel, nil)
+}
+
+// LogRole log info related to role.
+func (l *Logger) LogRole(roles []string) {
+	logger.DefaultLogger.Fields(map[string]interface{}{
+		"roles": roles,
+	})
+}
+
+// LogPolicy log info related to policy.
+func (l *Logger) LogPolicy(policy map[string][][]string) {
+	data := make(map[string]interface{}, len(policy))
+	for k := range policy {
+		data[k] = policy[k]
+	}
+	logger.DefaultLogger.Fields(data).Log(logger.InfoLevel, nil)
+}

+ 121 - 0
pkg/casbin/mycasbin.go

@@ -0,0 +1,121 @@
+package mycasbin
+
+import (
+	"errors"
+	"fmt"
+	"github.com/casbin/casbin/v2"
+	"github.com/casbin/casbin/v2/model"
+	"github.com/casbin/casbin/v2/util"
+	"gorm.io/gorm"
+	"net/http"
+)
+
+// Model语法 https://casbin.org/docs/zh-CN/syntax-for-models
+// sub, obj, act 表示经典三元组: 访问实体 (Subject),访问资源 (Object) 和访问方法 (Action)。
+// sub:希望访问资源的用户
+// dom:域/域租户 https://casbin.org/docs/zh-CN/rbac-with-domains
+// obj:要访问的资源
+// act:用户对资源执行的操作
+var text = `
+[request_definition]
+r = sub, dom, obj, act
+
+[policy_definition]
+p = sub, dom, obj, act
+
+[role_definition]
+g = _, _, _
+
+[policy_effect]
+e = some(where (p.eft == allow))
+
+[matchers]
+m = g(r.sub, p.sub, r.dom) && r.dom == p.dom && (keyMatch2(r.obj, p.obj) || keyMatch(r.obj, p.obj)) && r.act == p.act || r.sub == "role:admin"
+`
+// m = g(r.sub, p.sub, r.dom) && MyDomKeyMatch2(r.obj, p.obj, r.dom, p.dom) && MyRegexMatch(r.act, p.act, r.dom, p.dom) || r.sub == "role:admin")
+
+//m = r.sub == p.sub && (keyMatch2(r.obj, p.obj) || keyMatch(r.obj, p.obj)) && (r.act == p.act || p.act == "*")
+
+func Setup(db *gorm.DB) *casbin.SyncedEnforcer {
+	Apter, err := NewAdapterByDB(db)
+	if err != nil {
+		panic(err)
+	}
+	m, err := model.NewModelFromString(text)
+	if err != nil {
+		panic(err)
+	}
+	e, err := casbin.NewSyncedEnforcer(m, Apter)
+	if err != nil {
+		panic(err)
+	}
+	err = e.LoadPolicy()
+	if err != nil {
+		panic(err)
+	}
+	//e.StartAutoLoadPolicy(time.Minute)
+
+	//e.AddFunction("MyDomKeyMatch2", MyDomKeyMatch2Func)
+	//e.AddFunction("MyRegexMatch", MyRegexMatchFunc)
+
+	e.EnableLog(true)
+	return e
+}
+
+// validate the variadic parameter size and type as string
+func validateVariadicArgs(expectedLen int, args ...interface{}) error {
+	if len(args) != expectedLen {
+		return fmt.Errorf("Expected %d arguments, but got %d", expectedLen, len(args))
+	}
+
+	for _, p := range args {
+		_, ok := p.(string)
+		if !ok {
+			return errors.New("Argument must be a string")
+		}
+	}
+
+	return nil
+}
+
+// MyDomKeyMatch2Func 定义域KeyMatch2
+func MyDomKeyMatch2Func(args ...interface{}) (interface{}, error) {
+	if err := validateVariadicArgs(4, args...); err != nil {
+		return false,fmt.Errorf("%s: %s", "keyMatch2", err)
+	}
+	name1 := args[0].(string)
+	name2 := args[1].(string)
+	dom1 := args[2].(string)
+	dom2 := args[3].(string)
+	return (bool)(dom1 == dom2 && util.KeyMatch2(name1, name2)),nil
+}
+
+// MyRegexMatchFunc 定义域RegexMatch
+func MyRegexMatchFunc(args ...interface{}) (interface{}, error) {
+	if err := validateVariadicArgs(4, args...); err != nil {
+		return false, fmt.Errorf("%s: %s", "RegexMatch", err)
+	}
+	name1 := args[0].(string)
+	name2 := args[1].(string)
+	dom1 := args[2].(string)
+	dom2 := args[3].(string)
+	return (bool)(dom1 == dom2 && util.RegexMatch(name1, name2)),nil
+}
+
+// EnforceRoute 验证web路由
+func EnforceRoute(roleKey, serviceID string, req *http.Request, enforcer casbin.IEnforcer) (bool, error) {
+	sub := fmt.Sprintf("role:%s", roleKey)          // 希望访问资源的用户
+	dom := fmt.Sprintf("service:%s:api", serviceID) // 域/域租户,这里以资源为单位
+	obj := req.URL.Path                             // 要访问的资源
+	act := req.Method                               // 用户对资源执行的操作
+	return enforcer.Enforce(sub, dom, obj, act)
+}
+
+// EnforceRouteForScopeCode 验证web路由
+func EnforceRouteForScopeCode(roleKey, serviceID string, req *http.Request, enforcer casbin.IEnforcer) (bool, error) {
+	sub := fmt.Sprintf("scope:%s", roleKey)         // 希望访问资源的用户
+	dom := fmt.Sprintf("service:%s:api", serviceID) // 域/域租户,这里以资源为单位
+	obj := req.URL.Path                             // 要访问的资源
+	act := req.Method                               // 用户对资源执行的操作
+	return enforcer.Enforce(sub, dom, obj, act)
+}

+ 12 - 0
pkg/cronjob/gadmjob.go

@@ -0,0 +1,12 @@
+package cronjob
+
+import (
+	"github.com/robfig/cron/v3"
+)
+
+// NewWithSeconds newWithSeconds returns a Cron with the seconds field enabled.
+func NewWithSeconds() *cron.Cron {
+	secondParser := cron.NewParser(cron.Second | cron.Minute |
+		cron.Hour | cron.Dom | cron.Month | cron.DowOptional | cron.Descriptor)
+	return cron.New(cron.WithParser(secondParser), cron.WithChain())
+}

+ 17 - 0
pkg/env.go

@@ -0,0 +1,17 @@
+package pkg
+
+type (
+	Mode string
+)
+
+const (
+	ModeDev  Mode = "dev"     //开发模式
+	ModeTest Mode = "test"    //测试模式
+	ModeProd Mode = "prod"    //生产模式
+	Mysql         = "mysql"   //mysql数据库标识
+	Sqlite        = "sqlite3" //sqlite
+)
+
+func (e Mode) String() string {
+	return string(e)
+}

+ 130 - 0
pkg/file.go

@@ -0,0 +1,130 @@
+package pkg
+
+import (
+	"bufio"
+	"bytes"
+	"context"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"log"
+	"os"
+	"path/filepath"
+	"strings"
+	"time"
+)
+
+func PathCreate(dir string) error {
+	return os.MkdirAll(dir, os.ModePerm)
+}
+
+// PathExist 判断目录是否存在
+func PathExist(addr string) bool {
+	s, err := os.Stat(addr)
+	if err != nil {
+		log.Println(err)
+		return false
+	}
+	return s.IsDir()
+}
+
+func FileCreate(content bytes.Buffer, name string) {
+	file, err := os.Create(name)
+	if err != nil {
+		log.Println(err)
+	}
+	_, err = file.WriteString(content.String())
+	if err != nil {
+		log.Println(err)
+	}
+	file.Close()
+}
+
+type ReplaceHelper struct {
+	Root    string //路径
+	OldText string //需要替换的文本
+	NewText string //新的文本
+}
+
+func (h *ReplaceHelper) DoWrok() error {
+
+	return filepath.Walk(h.Root, h.walkCallback)
+
+}
+
+func (h ReplaceHelper) walkCallback(path string, f os.FileInfo, err error) error {
+
+	if err != nil {
+		return err
+	}
+	if f == nil {
+		return nil
+	}
+	if f.IsDir() {
+		log.Println("DIR:", path)
+		return nil
+	}
+
+	//文件类型需要进行过滤
+
+	buf, err := ioutil.ReadFile(path)
+	if err != nil {
+		//err
+		return err
+	}
+	content := string(buf)
+	log.Printf("h.OldText: %s \n", h.OldText)
+	log.Printf("h.NewText: %s \n", h.NewText)
+
+	//替换
+	newContent := strings.Replace(content, h.OldText, h.NewText, -1)
+
+	//重新写入
+	ioutil.WriteFile(path, []byte(newContent), 0)
+
+	return err
+}
+
+func FileMonitoringById(ctx context.Context, filePth string, id string, group string, hookfn func(context.Context, string, string, []byte)) {
+	f, err := os.Open(filePth)
+	if err != nil {
+		log.Fatalln(err)
+	}
+	defer f.Close()
+
+	rd := bufio.NewReader(f)
+	f.Seek(0, 2)
+	for {
+		if ctx.Err() != nil {
+			break
+		}
+		line, err := rd.ReadBytes('\n')
+		// 如果是文件末尾不返回
+		if err == io.EOF {
+			time.Sleep(500 * time.Millisecond)
+			continue
+		} else if err != nil {
+			log.Fatalln(err)
+		}
+		go hookfn(ctx, id, group, line)
+	}
+}
+
+// 获取文件大小
+func GetFileSize(filename string) int64 {
+	var result int64
+	filepath.Walk(filename, func(path string, f os.FileInfo, err error) error {
+		result = f.Size()
+		return nil
+	})
+	return result
+}
+
+//获取当前路径,比如:E:/abc/data/test
+func GetCurrentPath() string {
+	dir, err := os.Getwd()
+	if err != nil {
+		fmt.Println(err)
+	}
+	return strings.Replace(dir, "\\", "/", -1)
+}

+ 54 - 0
pkg/http.go

@@ -0,0 +1,54 @@
+package pkg
+
+import (
+	"bytes"
+	"encoding/json"
+	"io"
+	"io/ioutil"
+	"net/http"
+	"time"
+)
+
+// 发送GET请求
+// url:         请求地址
+// response:    请求返回的内容
+func Get(url string) (string, error) {
+
+	client := &http.Client{}
+	req, err := http.NewRequest("GET", url, nil)
+	req.Header.Set("Accept", "*/*")
+	req.Header.Set("Content-Type", "application/json")
+	if err != nil {
+		return "", err
+	}
+
+	resp, err := client.Do(req)
+	if err != nil {
+		return "", err
+	}
+	defer resp.Body.Close()
+	result, _ := ioutil.ReadAll(resp.Body)
+
+	return string(result), nil
+}
+
+// 发送POST请求
+// url:         请求地址
+// data:        POST请求提交的数据
+// contentType: 请求体格式,如:application/json
+// content:     请求放回的内容
+func Post(url string, data interface{}, contentType string) ([]byte, error) {
+
+	// 超时时间:5秒
+	client := &http.Client{Timeout: 5 * time.Second}
+	jsonStr, _ := json.Marshal(data)
+	resp, err := client.Post(url, contentType, bytes.NewBuffer(jsonStr))
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+
+	result, _ := io.ReadAll(resp.Body)
+	return result, nil
+
+}

+ 23 - 0
pkg/int.go

@@ -0,0 +1,23 @@
+package pkg
+
+import (
+	"math"
+	"strconv"
+)
+
+func IntToString(e int) string {
+	return strconv.Itoa(e)
+}
+
+func UIntToString(e uint) string {
+	return strconv.Itoa(int(e))
+}
+
+func Int64ToString(e int64) string {
+	return strconv.FormatInt(e, 10)
+}
+
+func Round(f float64, n int) float64 {
+	pow10_n := math.Pow10(n)
+	return math.Trunc((f+0.5/pow10_n)*pow10_n) / pow10_n // TODO +0.5 是为了四舍五入,如果不希望这样去掉这个
+}

+ 61 - 0
pkg/ip.go

@@ -0,0 +1,61 @@
+package pkg
+
+import (
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"log"
+	"net"
+	"net/http"
+)
+
+// 获取外网ip地址
+func GetLocation(ip, key string) string {
+	if ip == "127.0.0.1" || ip == "localhost" {
+		return "内部IP"
+	}
+	// 若用户不填写IP,则取客户HTTP之中的请求来进行定位
+	url := "https://restapi.amap.com/v3/ip?key=" + key
+	log.Println("restapi.amap.com url", url)
+	resp, err := http.Get(url)
+	if err != nil {
+		log.Println("restapi.amap.com failed:", err)
+		return "未知位置"
+	}
+	defer resp.Body.Close()
+	s, err := ioutil.ReadAll(resp.Body)
+	fmt.Println(string(s))
+
+	m := make(map[string]string)
+
+	err = json.Unmarshal(s, &m)
+	if err != nil {
+		log.Println("Umarshal failed:", err)
+	}
+
+	return m["country"] + "-" + m["province"] + "-" + m["city"] + "-" + m["district"] + "-" + m["isp"]
+}
+
+// 获取局域网ip地址
+func GetLocalHost() string {
+	netInterfaces, err := net.Interfaces()
+	if err != nil {
+		log.Println("net.Interfaces failed, err:", err.Error())
+	}
+
+	for i := 0; i < len(netInterfaces); i++ {
+		if (netInterfaces[i].Flags & net.FlagUp) != 0 {
+			addrs, _ := netInterfaces[i].Addrs()
+
+			for _, address := range addrs {
+				if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
+					if ipnet.IP.To4() != nil {
+						return ipnet.IP.String()
+					}
+				}
+			}
+		}
+
+	}
+	return ""
+}

+ 778 - 0
pkg/jwtauth/jwtauth.go

@@ -0,0 +1,778 @@
+package jwtauth
+
+import (
+	"crypto/rsa"
+	"errors"
+	"github.com/dgrijalva/jwt-go"
+	"github.com/gin-gonic/gin"
+	"github.com/go-redis/redis/v7"
+	"io/ioutil"
+	"net/http"
+	"strings"
+	"time"
+)
+
+const JwtPayloadKey = "JWT_PAYLOAD"
+
+type MapClaims map[string]interface{}
+
+// GinJWTMiddleware provides a Json-Web-Token authentication implementation. On failure, a 401 HTTP response
+// is returned. On success, the wrapped middleware is called, and the userID is made available as
+// c.Get("userID").(string).
+// Users can get a token by posting a json request to LoginHandler. The token then needs to be passed in
+// the Authentication header. Example: Authorization:Bearer XXX_TOKEN_XXX
+type GinJWTMiddleware struct {
+	// Realm name to display to the user. Required.
+	Realm string
+
+	// signing algorithm - possible values are HS256, HS384, HS512
+	// Optional, default is HS256.
+	SigningAlgorithm string
+
+	// Secret key used for signing. Required.
+	Key []byte
+
+	// Duration that a jwt token is valid. Optional, defaults to one hour.
+	Timeout time.Duration
+
+	// This field allows clients to refresh their token until MaxRefresh has passed.
+	// Note that clients can refresh their token in the last moment of MaxRefresh.
+	// This means that the maximum validity timespan for a token is TokenTime + MaxRefresh.
+	// Optional, defaults to 0 meaning not refreshable.
+	MaxRefresh time.Duration
+
+	// Callback function that should perform the authentication of the user based on login info.
+	// Must return user data as user identifier, it will be stored in Claim Array. Required.
+	// Check error (e) to determine the appropriate error message.
+	Authenticator func(c *gin.Context) (interface{}, error)
+
+	// Callback function that should perform the authorization of the authenticated user. Called
+	// only after an authentication success. Must return true on success, false on failure.
+	// Optional, default to success.
+	Authorizator func(data interface{}, c *gin.Context) bool
+
+	CheckUserAccount func(id int64, c *gin.Context) error
+
+	// Callback function that will be called during login.
+	// Using this function it is possible to add additional payload data to the webtoken.
+	// The data is then made available during requests via c.Get("JWT_PAYLOAD").
+	// Note that the payload is not encrypted.
+	// The attributes mentioned on jwt.io can't be used as keys for the map.
+	// Optional, by default no additional data will be set.
+	PayloadFunc func(data interface{}) MapClaims
+
+	// User can define own Unauthorized func.
+	Unauthorized func(*gin.Context, int, string)
+
+	// User can define own LoginResponse func.
+	LoginResponse func(*gin.Context, int, string, time.Time)
+
+	// User can define own RefreshResponse func.
+	RefreshResponse func(*gin.Context, int, string, time.Time)
+
+	// Set the identity handler function
+	IdentityHandler func(*gin.Context) interface{}
+
+	// Set the identity key
+	IdentityKey string
+
+	// username
+	NiceKey string
+
+	DataScopeKey string
+
+	// rolekey
+	RKey string
+
+	// roleId
+	RoleIdKey string
+
+	RoleKey string
+
+	// roleName
+	RoleNameKey string
+
+	// TokenLookup is a string in the form of "<source>:<name>" that is used
+	// to extract token from the request.
+	// Optional. Default value "header:Authorization".
+	// Possible values:
+	// - "header:<name>"
+	// - "query:<name>"
+	// - "cookie:<name>"
+	TokenLookup string
+
+	// TokenHeadName is a string in the header. Default value is "Bearer"
+	TokenHeadName string
+
+	// TimeFunc provides the current time. You can override it to use another time value. This is useful for testing or if your server uses a different time zone than your tokens.
+	TimeFunc func() time.Time
+
+	// HTTP Status messages for when something in the JWT middleware fails.
+	// Check error (e) to determine the appropriate error message.
+	HTTPStatusMessageFunc func(e error, c *gin.Context) string
+
+	// Private key file for asymmetric algorithms
+	PrivKeyFile string
+
+	// Public key file for asymmetric algorithms
+	PubKeyFile string
+
+	// Private key
+	privKey *rsa.PrivateKey
+
+	// Public key
+	pubKey *rsa.PublicKey
+
+	// Optionally return the token as a cookie
+	SendCookie bool
+
+	// Allow insecure cookies for development over http
+	SecureCookie bool
+
+	// Allow cookies to be accessed client side for development
+	CookieHTTPOnly bool
+
+	// Allow cookie domain change for development
+	CookieDomain string
+
+	// SendAuthorization allow return authorization header for every request
+	SendAuthorization bool
+
+	// Disable abort() of context.
+	DisabledAbort bool
+
+	// CookieName allow cookie name change for development
+	CookieName string
+	// 单一登录
+	SingleLogin func(c *gin.Context) (bool, error)
+	// 保存token的key
+	TokenCachekey string
+	// 保存token的key
+	SaveTokenToCache func(c *gin.Context, userId int64, key, token string, expire int64) error
+	GetTokenToCache  func(c *gin.Context, userId int64, key string) (string, error)
+}
+
+var (
+	// ErrMissingSecretKey indicates Secret key is required
+	ErrMissingSecretKey = errors.New("secret key is required")
+
+	// ErrForbidden when HTTP status 403 is given
+	ErrForbidden = errors.New("you don't have permission to access this resource")
+
+	// ErrMissingAuthenticatorFunc indicates Authenticator is required
+	ErrMissingAuthenticatorFunc = errors.New("ginJWTMiddleware.Authenticator func is undefined")
+
+	// ErrMissingLoginValues indicates a user tried to authenticate without username or password
+	ErrMissingLoginValues = errors.New("missing Username or Password or Code")
+
+	// ErrFailedAuthentication indicates authentication failed, could be faulty username or password
+	//ErrFailedAuthentication = errors.New("incorrect Username or Password")
+	ErrFailedAuthentication = errors.New("用户名或密码不正确")
+
+	ErrAccountDeactivated = errors.New("账号已停用")
+
+	// ErrFailedTokenCreation indicates JWT Token failed to create, reason unknown
+	ErrFailedTokenCreation = errors.New("failed to create JWT Token")
+
+	// ErrExpiredToken indicates JWT token has expired. Can't refresh.
+	ErrExpiredToken = errors.New("token is expired")
+
+	// ErrEmptyAuthHeader can be thrown if authing with a HTTP header, the Auth header needs to be set
+	ErrEmptyAuthHeader = errors.New("auth header is empty")
+
+	// ErrMissingExpField missing exp field in token
+	ErrMissingExpField = errors.New("missing exp field")
+
+	// ErrWrongFormatOfExp field must be float64 format
+	ErrWrongFormatOfExp = errors.New("exp must be float64 format")
+
+	// ErrInvalidAuthHeader indicates auth header is invalid, could for example have the wrong Realm name
+	ErrInvalidAuthHeader = errors.New("auth header is invalid")
+
+	// ErrEmptyQueryToken can be thrown if authing with URL Query, the query token variable is empty
+	ErrEmptyQueryToken = errors.New("query token is empty")
+
+	// ErrEmptyCookieToken can be thrown if authing with a cookie, the token cokie is empty
+	ErrEmptyCookieToken = errors.New("cookie token is empty")
+
+	// ErrEmptyParamToken can be thrown if authing with parameter in path, the parameter in path is empty
+	ErrEmptyParamToken = errors.New("parameter token is empty")
+
+	// ErrInvalidSigningAlgorithm indicates signing algorithm is invalid, needs to be HS256, HS384, HS512, RS256, RS384 or RS512
+	ErrInvalidSigningAlgorithm = errors.New("invalid signing algorithm")
+
+	ErrInvalidVerificationCode = errors.New("验证码错误")
+
+	// ErrNoPrivKeyFile indicates that the given private key is unreadable
+	ErrNoPrivKeyFile = errors.New("private key file unreadable")
+
+	// ErrNoPubKeyFile indicates that the given public key is unreadable
+	ErrNoPubKeyFile = errors.New("public key file unreadable")
+
+	// ErrInvalidPrivKey indicates that the given private key is invalid
+	ErrInvalidPrivKey = errors.New("private key invalid")
+
+	// ErrInvalidPubKey indicates the the given public key is invalid
+	ErrInvalidPubKey = errors.New("public key invalid")
+
+	UUIDKey = "uuid"
+	// IdentityKey default identity key
+	IdentityKey = "identity"
+
+	// NiceKey 昵称
+	UserNameKey  = "username"
+	DataScopeKey = "dataScope"
+
+	// RoleIdKey 角色id  Old
+	RoleIdKey = "roleId"
+
+	// RoleKey 角色key  Old
+	RoleKey = "roleKey"
+
+	// RoleNameKey 角色名称  Old
+	RoleNameKey = "roleName"
+)
+
+// New for check error with GinJWTMiddleware
+func New(m *GinJWTMiddleware) (*GinJWTMiddleware, error) {
+	if err := m.MiddlewareInit(); err != nil {
+		return nil, err
+	}
+
+	return m, nil
+}
+
+func (mw *GinJWTMiddleware) readKeys() error {
+	err := mw.privateKey()
+	if err != nil {
+		return err
+	}
+	err = mw.publicKey()
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func (mw *GinJWTMiddleware) privateKey() error {
+	keyData, err := ioutil.ReadFile(mw.PrivKeyFile)
+	if err != nil {
+		return ErrNoPrivKeyFile
+	}
+	key, err := jwt.ParseRSAPrivateKeyFromPEM(keyData)
+	if err != nil {
+		return ErrInvalidPrivKey
+	}
+	mw.privKey = key
+	return nil
+}
+
+func (mw *GinJWTMiddleware) publicKey() error {
+	keyData, err := ioutil.ReadFile(mw.PubKeyFile)
+	if err != nil {
+		return ErrNoPubKeyFile
+	}
+	key, err := jwt.ParseRSAPublicKeyFromPEM(keyData)
+	if err != nil {
+		return ErrInvalidPubKey
+	}
+	mw.pubKey = key
+	return nil
+}
+
+func (mw *GinJWTMiddleware) usingPublicKeyAlgo() bool {
+	switch mw.SigningAlgorithm {
+	case "RS256", "RS512", "RS384":
+		return true
+	}
+	return false
+}
+
+// MiddlewareInit initialize jwt configs.
+func (mw *GinJWTMiddleware) MiddlewareInit() error {
+
+	if mw.TokenLookup == "" {
+		mw.TokenLookup = "header:Authorization"
+	}
+
+	if mw.SigningAlgorithm == "" {
+		mw.SigningAlgorithm = "HS256"
+	}
+
+	if mw.TimeFunc == nil {
+		mw.TimeFunc = time.Now
+	}
+
+	mw.TokenHeadName = strings.TrimSpace(mw.TokenHeadName)
+	if len(mw.TokenHeadName) == 0 {
+		mw.TokenHeadName = "Bearer"
+	}
+
+	if mw.Authorizator == nil {
+		mw.Authorizator = func(data interface{}, c *gin.Context) bool {
+			return true
+		}
+	}
+
+	if mw.Unauthorized == nil {
+		mw.Unauthorized = func(c *gin.Context, code int, message string) {
+			c.JSON(http.StatusOK, gin.H{
+				"code":    code,
+				"message": message,
+			})
+		}
+	}
+
+	if mw.LoginResponse == nil {
+		mw.LoginResponse = func(c *gin.Context, code int, token string, expire time.Time) {
+			c.JSON(http.StatusOK, gin.H{
+				"code":   http.StatusOK,
+				"token":  token,
+				"expire": expire.Format(time.RFC3339),
+			})
+		}
+	}
+
+	if mw.RefreshResponse == nil {
+		mw.RefreshResponse = func(c *gin.Context, code int, token string, expire time.Time) {
+			c.JSON(http.StatusOK, gin.H{
+				"code":   http.StatusOK,
+				"token":  token,
+				"expire": expire.Format(time.RFC3339),
+			})
+		}
+	}
+
+	if mw.IdentityKey == "" {
+		mw.IdentityKey = IdentityKey
+	}
+
+	if mw.IdentityHandler == nil {
+		mw.IdentityHandler = func(c *gin.Context) interface{} {
+			claims := ExtractClaims(c)
+			return claims
+		}
+	}
+
+	if mw.HTTPStatusMessageFunc == nil {
+		mw.HTTPStatusMessageFunc = func(e error, c *gin.Context) string {
+			return e.Error()
+		}
+	}
+
+	if mw.Realm == "" {
+		mw.Realm = "gin jwt"
+	}
+
+	if mw.CookieName == "" {
+		mw.CookieName = "jwt"
+	}
+
+	if mw.usingPublicKeyAlgo() {
+		return mw.readKeys()
+	}
+
+	if mw.Key == nil {
+		return ErrMissingSecretKey
+	}
+	return nil
+}
+
+// MiddlewareFunc makes GinJWTMiddleware implement the Middleware interface.
+func (mw *GinJWTMiddleware) MiddlewareFunc() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		mw.middlewareImpl(c)
+	}
+}
+
+func (mw *GinJWTMiddleware) middlewareImpl(c *gin.Context) {
+	claims, err := mw.GetClaimsFromJWT(c)
+	if err != nil {
+		mw.unauthorized(c, http.StatusUnauthorized, mw.HTTPStatusMessageFunc(err, c))
+		return
+	}
+
+	if claims["exp"] == nil {
+		mw.unauthorized(c, http.StatusBadRequest, mw.HTTPStatusMessageFunc(ErrMissingExpField, c))
+		return
+	}
+
+	if _, ok := claims["exp"].(float64); !ok {
+		mw.unauthorized(c, http.StatusBadRequest, mw.HTTPStatusMessageFunc(ErrWrongFormatOfExp, c))
+		return
+	}
+	if int64(claims["exp"].(float64)) < mw.TimeFunc().Unix() {
+		mw.unauthorized(c, 6401, mw.HTTPStatusMessageFunc(ErrExpiredToken, c))
+		return
+	}
+
+	c.Set(JwtPayloadKey, claims)
+	identity := mw.IdentityHandler(c)
+
+	if identity != nil {
+		c.Set(mw.IdentityKey, identity)
+	}
+
+	if !mw.Authorizator(identity, c) {
+		mw.unauthorized(c, http.StatusForbidden, mw.HTTPStatusMessageFunc(ErrForbidden, c))
+		return
+	}
+
+	if err = mw.CheckUserAccount(int64(claims["identity"].(float64)), c); err != nil {
+		mw.unauthorized(c, http.StatusBadRequest, mw.HTTPStatusMessageFunc(err, c))
+		return
+	}
+
+	if flag, _ := mw.SingleLogin(c); flag == true {
+		token, err := mw.GetTokenToCache(c, int64(claims["identity"].(float64)), mw.TokenCachekey)
+		if err != nil && errors.Is(err, redis.Nil) {
+			mw.unauthorized(c, http.StatusUnauthorized, mw.HTTPStatusMessageFunc(ErrExpiredToken, c))
+			return
+		}
+		if token != GetToken(c) {
+			mw.unauthorized(c, http.StatusUnauthorized, mw.HTTPStatusMessageFunc(ErrExpiredToken, c))
+			return
+		}
+	}
+
+	c.Next()
+}
+
+// GetClaimsFromJWT get claims from JWT token
+func (mw *GinJWTMiddleware) GetClaimsFromJWT(c *gin.Context) (MapClaims, error) {
+	token, err := mw.ParseToken(c)
+
+	if err != nil {
+		return nil, err
+	}
+
+	if mw.SendAuthorization {
+		if v, ok := c.Get("JWT_TOKEN"); ok {
+			c.Header("Authorization", mw.TokenHeadName+" "+v.(string))
+		}
+	}
+
+	claims := MapClaims{}
+	for key, value := range token.Claims.(jwt.MapClaims) {
+		claims[key] = value
+	}
+
+	return claims, nil
+}
+
+// LoginHandler can be used by clients to get a jwt token.
+// Payload needs to be json in the form of {"username": "USERNAME", "password": "PASSWORD"}.
+// Reply will be of the form {"token": "TOKEN"}.
+func (mw *GinJWTMiddleware) LoginHandler(c *gin.Context) {
+	if mw.Authenticator == nil {
+		mw.unauthorized(c, http.StatusInternalServerError, mw.HTTPStatusMessageFunc(ErrMissingAuthenticatorFunc, c))
+		return
+	}
+
+	data, err := mw.Authenticator(c)
+
+	if err != nil {
+		mw.unauthorized(c, 400, mw.HTTPStatusMessageFunc(err, c))
+		return
+	}
+
+	// Create the token
+	token := jwt.New(jwt.GetSigningMethod(mw.SigningAlgorithm))
+	claims := token.Claims.(jwt.MapClaims)
+
+	if mw.PayloadFunc != nil {
+		for key, value := range mw.PayloadFunc(data) {
+			claims[key] = value
+		}
+	}
+
+	expire := mw.TimeFunc().Add(mw.Timeout)
+	claims["exp"] = expire.Unix()
+	claims["orig_iat"] = mw.TimeFunc().Unix()
+	tokenString, err := mw.signedString(token)
+
+	if err != nil {
+		mw.unauthorized(c, http.StatusInternalServerError, mw.HTTPStatusMessageFunc(ErrFailedTokenCreation, c))
+		return
+	}
+
+	// set cookie
+	if mw.SendCookie {
+		maxage := int(expire.Unix() - time.Now().Unix())
+		c.SetCookie(
+			mw.CookieName,
+			tokenString,
+			maxage,
+			"/",
+			mw.CookieDomain,
+			mw.SecureCookie,
+			mw.CookieHTTPOnly,
+		)
+	}
+	if flag, _ := mw.SingleLogin(c); flag == true  {
+		_ = mw.SaveTokenToCache(c, int64(claims["identity"].(int)), mw.TokenCachekey, tokenString, int64(mw.Timeout)/3600)
+	}
+	mw.LoginResponse(c, http.StatusOK, tokenString, expire)
+}
+
+func (mw *GinJWTMiddleware) signedString(token *jwt.Token) (string, error) {
+	var tokenString string
+	var err error
+	if mw.usingPublicKeyAlgo() {
+		tokenString, err = token.SignedString(mw.privKey)
+	} else {
+		tokenString, err = token.SignedString(mw.Key)
+	}
+	return tokenString, err
+}
+
+// RefreshHandler can be used to refresh a token. The token still needs to be valid on refresh.
+// Shall be put under an endpoint that is using the GinJWTMiddleware.
+// Reply will be of the form {"token": "TOKEN"}.
+func (mw *GinJWTMiddleware) RefreshHandler(c *gin.Context) {
+	tokenString, expire, err := mw.RefreshToken(c)
+	if err != nil {
+		mw.unauthorized(c, http.StatusUnauthorized, mw.HTTPStatusMessageFunc(err, c))
+		return
+	}
+
+	mw.RefreshResponse(c, http.StatusOK, tokenString, expire)
+}
+
+// RefreshToken refresh token and check if token is expired
+func (mw *GinJWTMiddleware) RefreshToken(c *gin.Context) (string, time.Time, error) {
+	claims, err := mw.CheckIfTokenExpire(c)
+	if err != nil {
+		return "", time.Now(), err
+	}
+
+	// Create the token
+	newToken := jwt.New(jwt.GetSigningMethod(mw.SigningAlgorithm))
+	newClaims := newToken.Claims.(jwt.MapClaims)
+
+	for key := range claims {
+		newClaims[key] = claims[key]
+	}
+
+	expire := mw.TimeFunc().Add(mw.Timeout)
+	newClaims["exp"] = expire.Unix()
+	newClaims["orig_iat"] = mw.TimeFunc().Unix()
+	tokenString, err := mw.signedString(newToken)
+
+	if err != nil {
+		return "", time.Now(), err
+	}
+
+	// set cookie
+	if mw.SendCookie {
+		maxage := int(expire.Unix() - time.Now().Unix())
+		c.SetCookie(
+			mw.CookieName,
+			tokenString,
+			maxage,
+			"/",
+			mw.CookieDomain,
+			mw.SecureCookie,
+			mw.CookieHTTPOnly,
+		)
+	}
+
+	if flag, _ := mw.SingleLogin(c); flag == true  {
+		_ = mw.SaveTokenToCache(c, int64(claims["identity"].(int)), mw.TokenCachekey, tokenString, int64(mw.Timeout)/3600)
+	}
+
+	return tokenString, expire, nil
+}
+
+// CheckIfTokenExpire check if token expire
+func (mw *GinJWTMiddleware) CheckIfTokenExpire(c *gin.Context) (jwt.MapClaims, error) {
+	token, err := mw.ParseToken(c)
+
+	if err != nil {
+		// If we receive an error, and the error is anything other than a single
+		// ValidationErrorExpired, we want to return the error.
+		// If the error is just ValidationErrorExpired, we want to continue, as we can still
+		// refresh the token if it's within the MaxRefresh time.
+		// (see https://github.com/appleboy/gin-jwt/issues/176)
+		validationErr, ok := err.(*jwt.ValidationError)
+		if !ok || validationErr.Errors != jwt.ValidationErrorExpired {
+			return nil, err
+		}
+	}
+
+	claims := token.Claims.(jwt.MapClaims)
+
+	origIat := int64(claims["orig_iat"].(float64))
+
+	if origIat < mw.TimeFunc().Add(-mw.MaxRefresh).Unix() {
+		return nil, ErrExpiredToken
+	}
+
+	return claims, nil
+}
+
+// TokenGenerator method that clients can use to get a jwt token.
+func (mw *GinJWTMiddleware) TokenGenerator(data interface{}) (string, time.Time, error) {
+	token := jwt.New(jwt.GetSigningMethod(mw.SigningAlgorithm))
+	claims := token.Claims.(jwt.MapClaims)
+
+	if mw.PayloadFunc != nil {
+		for key, value := range mw.PayloadFunc(data) {
+			claims[key] = value
+		}
+	}
+
+	expire := mw.TimeFunc().UTC().Add(mw.Timeout)
+	claims["exp"] = expire.Unix()
+	claims["orig_iat"] = mw.TimeFunc().Unix()
+	tokenString, err := mw.signedString(token)
+	if err != nil {
+		return "", time.Time{}, err
+	}
+
+	return tokenString, expire, nil
+}
+
+func (mw *GinJWTMiddleware) jwtFromHeader(c *gin.Context, key string) (string, error) {
+	authHeader := c.Request.Header.Get(key)
+
+	if authHeader == "" {
+		return "", ErrEmptyAuthHeader
+	}
+
+	parts := strings.SplitN(authHeader, " ", 2)
+	if !(len(parts) == 2 && parts[0] == mw.TokenHeadName) {
+		return "", ErrInvalidAuthHeader
+	}
+
+	return parts[1], nil
+}
+
+func (mw *GinJWTMiddleware) jwtFromQuery(c *gin.Context, key string) (string, error) {
+	token := c.Query(key)
+
+	if token == "" {
+		return "", ErrEmptyQueryToken
+	}
+
+	return token, nil
+}
+
+func (mw *GinJWTMiddleware) jwtFromCookie(c *gin.Context, key string) (string, error) {
+	cookie, _ := c.Cookie(key)
+
+	if cookie == "" {
+		return "", ErrEmptyCookieToken
+	}
+
+	return cookie, nil
+}
+
+func (mw *GinJWTMiddleware) jwtFromParam(c *gin.Context, key string) (string, error) {
+	token := c.Param(key)
+
+	if token == "" {
+		return "", ErrEmptyParamToken
+	}
+
+	return token, nil
+}
+
+// ParseToken parse jwt token from gin context
+func (mw *GinJWTMiddleware) ParseToken(c *gin.Context) (*jwt.Token, error) {
+	var token string
+	var err error
+
+	methods := strings.Split(mw.TokenLookup, ",")
+	for _, method := range methods {
+		if len(token) > 0 {
+			break
+		}
+		parts := strings.Split(strings.TrimSpace(method), ":")
+		k := strings.TrimSpace(parts[0])
+		v := strings.TrimSpace(parts[1])
+		switch k {
+		case "header":
+			token, err = mw.jwtFromHeader(c, v)
+		case "query":
+			token, err = mw.jwtFromQuery(c, v)
+		case "cookie":
+			token, err = mw.jwtFromCookie(c, v)
+		case "param":
+			token, err = mw.jwtFromParam(c, v)
+		}
+	}
+
+	if err != nil {
+		return nil, err
+	}
+
+	return jwt.Parse(token, func(t *jwt.Token) (interface{}, error) {
+		if jwt.GetSigningMethod(mw.SigningAlgorithm) != t.Method {
+			return nil, ErrInvalidSigningAlgorithm
+		}
+		if mw.usingPublicKeyAlgo() {
+			return mw.pubKey, nil
+		}
+		c.Set("JWT_TOKEN", token)
+
+		return mw.Key, nil
+	})
+}
+
+// ParseTokenString parse jwt token string
+func (mw *GinJWTMiddleware) ParseTokenString(token string) (*jwt.Token, error) {
+	return jwt.Parse(token, func(t *jwt.Token) (interface{}, error) {
+		if jwt.GetSigningMethod(mw.SigningAlgorithm) != t.Method {
+			return nil, ErrInvalidSigningAlgorithm
+		}
+		if mw.usingPublicKeyAlgo() {
+			return mw.pubKey, nil
+		}
+
+		return mw.Key, nil
+	})
+}
+
+func (mw *GinJWTMiddleware) unauthorized(c *gin.Context, code int, message string) {
+	c.Header("WWW-Authenticate", "JWT realm="+mw.Realm)
+	if !mw.DisabledAbort {
+		c.Abort()
+	}
+
+	mw.Unauthorized(c, code, message)
+}
+
+// ExtractClaims help to extract the JWT claims
+func ExtractClaims(c *gin.Context) MapClaims {
+	claims, exists := c.Get(JwtPayloadKey)
+	if !exists {
+		return make(MapClaims)
+	}
+
+	return claims.(MapClaims)
+}
+
+// ExtractClaimsFromToken help to extract the JWT claims from token
+func ExtractClaimsFromToken(token *jwt.Token) MapClaims {
+	if token == nil {
+		return make(MapClaims)
+	}
+
+	claims := MapClaims{}
+	for key, value := range token.Claims.(jwt.MapClaims) {
+		claims[key] = value
+	}
+
+	return claims
+}
+
+// GetToken help to get the JWT token string
+func GetToken(c *gin.Context) string {
+	token, exists := c.Get("JWT_TOKEN")
+	if !exists {
+		return ""
+	}
+
+	return token.(string)
+}

+ 99 - 0
pkg/jwtauth/user/user.go

@@ -0,0 +1,99 @@
+package user
+
+import (
+	"fmt"
+	"github.com/gin-gonic/gin"
+	"git.baozhida.cn/OAuth-core/pkg"
+	jwt "git.baozhida.cn/OAuth-core/pkg/jwtauth"
+)
+
+func ExtractClaims(c *gin.Context) jwt.MapClaims {
+	claims, exists := c.Get(jwt.JwtPayloadKey)
+	if !exists {
+		return make(jwt.MapClaims)
+	}
+
+	return claims.(jwt.MapClaims)
+}
+
+func Get(c *gin.Context, key string) interface{} {
+	data := ExtractClaims(c)
+	if data[key] != nil {
+		return data[key]
+	}
+	fmt.Println(pkg.GetCurrentTimeStr() + " [WARING] " + c.Request.Method + " " + c.Request.URL.Path + " Get 缺少 " + key)
+	return nil
+}
+func GetUUID(c *gin.Context) int {
+	data := ExtractClaims(c)
+	if data["uuid"] != nil {
+		return int((data["uuid"]).(float64))
+	}
+	fmt.Println(pkg.GetCurrentTimeStr() + " [WARING] " + c.Request.Method + " " + c.Request.URL.Path + " GetUUID 缺少 uuid")
+	return 0
+}
+
+func GetUserId(c *gin.Context) int {
+	data := ExtractClaims(c)
+	if data["identity"] != nil {
+		return int((data["identity"]).(float64))
+	}
+	fmt.Println(pkg.GetCurrentTimeStr() + " [WARING] " + c.Request.Method + " " + c.Request.URL.Path + " GetUserId 缺少 identity")
+	return 0
+}
+
+func GetUserIdStr(c *gin.Context) string {
+	data := ExtractClaims(c)
+	if data["identity"] != nil {
+		return pkg.Int64ToString(int64((data["identity"]).(float64)))
+	}
+	fmt.Println(pkg.GetCurrentTimeStr() + " [WARING] " + c.Request.Method + " " + c.Request.URL.Path + " GetUserIdStr 缺少 identity")
+	return ""
+}
+
+func GetUserName(c *gin.Context) string {
+	data := ExtractClaims(c)
+	if data["username"] != nil {
+		return (data["username"]).(string)
+	}
+	fmt.Println(pkg.GetCurrentTimeStr() + " [WARING] " + c.Request.Method + " " + c.Request.URL.Path + " GetUserName 缺少 username")
+	return ""
+}
+
+func GetRoleName(c *gin.Context) string {
+	data := ExtractClaims(c)
+	if data["roleName"] != nil {
+		return (data["roleName"]).(string)
+	}
+	fmt.Println(pkg.GetCurrentTimeStr() + " [WARING] " + c.Request.Method + " " + c.Request.URL.Path + " GetRoleName 缺少 roleName")
+	return ""
+}
+
+func GetRoleKey(c *gin.Context) string {
+	data := ExtractClaims(c)
+	if data["roleKey"] != nil {
+		return (data["roleKey"]).(string)
+	}
+	fmt.Println(pkg.GetCurrentTimeStr() + " [WARING] " + c.Request.Method + " " + c.Request.URL.Path + " GetRoleKey 缺少 roleKey")
+	return ""
+}
+
+func GetRoleId(c *gin.Context) int {
+	data := ExtractClaims(c)
+	if data["roleId"] != nil {
+		i := int((data["roleId"]).(float64))
+		return i
+	}
+	fmt.Println(pkg.GetCurrentTimeStr() + " [WARING] " + c.Request.Method + " " + c.Request.URL.Path + " GetRoleId 缺少 roleId")
+	return 0
+}
+
+func GetDataScope(c *gin.Context) int {
+	data := ExtractClaims(c)
+	if data["dataScope"] != nil {
+		i := int((data["dataScope"]).(float64))
+		return i
+	}
+	fmt.Println(pkg.GetCurrentTimeStr() + " [WARING] " + c.Request.Method + " " + c.Request.URL.Path + " GetDataScope 缺少 dataScope")
+	return 0
+}

+ 58 - 0
pkg/logger/log.go

@@ -0,0 +1,58 @@
+package logger
+
+import (
+	"io"
+	"os"
+
+	"git.baozhida.cn/OAuth-core/debug/writer"
+	"git.baozhida.cn/OAuth-core/logger"
+	log "git.baozhida.cn/OAuth-core/logger"
+	"git.baozhida.cn/OAuth-core/pkg"
+	"git.baozhida.cn/OAuth-core/plugins/logger/zap"
+)
+
+// SetupLogger 日志 cap 单位为kb
+func SetupLogger(opts ...Option) logger.Logger {
+	op := setDefault()
+	for _, o := range opts {
+		o(&op)
+	}
+	if !pkg.PathExist(op.path) {
+		err := pkg.PathCreate(op.path)
+		if err != nil {
+			log.Fatalf("create dir error: %s", err.Error())
+		}
+	}
+	var err error
+	var output io.Writer
+	switch op.stdout {
+	case "file":
+		output, err = writer.NewFileWriter(
+			writer.WithPath(op.path),
+			writer.WithCap(op.cap<<10),
+		)
+		if err != nil {
+			log.Fatal("logger setup error: %s", err.Error())
+		}
+	default:
+		output = os.Stdout
+	}
+	var level logger.Level
+	level, err = logger.GetLevel(op.level)
+	if err != nil {
+		log.Fatalf("get logger level error, %s", err.Error())
+	}
+
+	switch op.driver {
+	case "zap":
+		log.DefaultLogger, err = zap.NewLogger(logger.WithLevel(level), logger.WithOutput(output), zap.WithCallerSkip(2))
+		if err != nil {
+			log.Fatalf("new zap logger error, %s", err.Error())
+		}
+	//case "logrus":
+	//	setLogger = logrus.NewLogger(logger.WithLevel(level), logger.WithOutput(output), logrus.ReportCaller())
+	default:
+		log.DefaultLogger = logger.NewLogger(logger.WithLevel(level), logger.WithOutput(output))
+	}
+	return log.DefaultLogger
+}

+ 57 - 0
pkg/logger/options.go

@@ -0,0 +1,57 @@
+/*
+ * @Author: lwnmengjing
+ * @Date: 2021/6/10 10:26 上午
+ * @Last Modified by: lwnmengjing
+ * @Last Modified time: 2021/6/10 10:26 上午
+ */
+
+package logger
+
+type Option func(*options)
+
+type options struct {
+	driver string
+	path   string
+	level  string
+	stdout string
+	cap    uint
+}
+
+func setDefault() options {
+	return options{
+		driver: "default",
+		path:   "temp/logs",
+		level:  "warn",
+		stdout: "default",
+	}
+}
+
+func WithType(s string) Option {
+	return func(o *options) {
+		o.driver = s
+	}
+}
+
+func WithPath(s string) Option {
+	return func(o *options) {
+		o.path = s
+	}
+}
+
+func WithLevel(s string) Option {
+	return func(o *options) {
+		o.level = s
+	}
+}
+
+func WithStdout(s string) Option {
+	return func(o *options) {
+		o.stdout = s
+	}
+}
+
+func WithCap(n uint) Option {
+	return func(o *options) {
+		o.cap = n
+	}
+}

+ 50 - 0
pkg/response/model.go

@@ -0,0 +1,50 @@
+package response
+// 数据集
+type Response struct {
+	RequestId string `protobuf:"bytes,1,opt,name=requestId,proto3" json:"requestId,omitempty"`
+	Code      int32  `protobuf:"varint,2,opt,name=code,proto3" json:"code,omitempty"`
+	Msg       string `protobuf:"bytes,3,opt,name=msg,proto3" json:"msg,omitempty"`
+	Status    string `protobuf:"bytes,4,opt,name=status,proto3" json:"status,omitempty"`
+}
+
+type response struct {
+	Response
+	Data interface{} `json:"data"`
+}
+
+type Page struct {
+	Count     int `json:"count"`     //总数
+	PageIndex int `json:"pageIndex"` //页码
+	PageSize  int `json:"pageSize"`  //页条数
+}
+
+type page struct {
+	Page
+	List interface{} `json:"list"`
+}
+
+func (e *response) SetData(data interface{}) {
+	e.Data = data
+}
+
+func (e response) Clone() Responses {
+	return &e
+}
+
+func (e *response) SetTraceID(id string) {
+	e.RequestId = id
+}
+
+func (e *response) SetMsg(s string) {
+	e.Msg = s
+}
+
+func (e *response) SetCode(code int32) {
+	e.Code = code
+}
+
+func (e *response) SetSuccess(success bool) {
+	if !success {
+		e.Status = "error"
+	}
+}

+ 59 - 0
pkg/response/return.go

@@ -0,0 +1,59 @@
+package response
+
+import (
+	"net/http"
+
+	"github.com/gin-gonic/gin"
+	"git.baozhida.cn/OAuth-core/pkg"
+)
+
+var Default = &response{}
+
+// Error 失败数据处理
+func Error(c *gin.Context, code int, err error, msg string) {
+	res := Default.Clone()
+	if err != nil {
+		res.SetMsg(err.Error())
+	}
+	if msg != "" {
+		res.SetMsg(msg)
+	}
+	res.SetTraceID(pkg.GenerateMsgIDFromContext(c))
+	res.SetCode(int32(code))
+	res.SetSuccess(false)
+	c.Set("result", res)
+	c.Set("status", code)
+	c.AbortWithStatusJSON(http.StatusOK, res)
+}
+
+// OK 通常成功数据处理
+func OK(c *gin.Context, data interface{}, msg string) {
+	res := Default.Clone()
+	res.SetData(data)
+	res.SetSuccess(true)
+	if msg != "" {
+		res.SetMsg(msg)
+	}
+	res.SetTraceID(pkg.GenerateMsgIDFromContext(c))
+	res.SetCode(http.StatusOK)
+	c.Set("result", res)
+	c.Set("status", http.StatusOK)
+	c.AbortWithStatusJSON(http.StatusOK, res)
+}
+
+// PageOK 分页数据处理
+func PageOK(c *gin.Context, result interface{}, count int, pageIndex int, pageSize int, msg string) {
+	var res page
+	res.List = result
+	res.Count = count
+	res.PageIndex = pageIndex
+	res.PageSize = pageSize
+	OK(c, res, msg)
+}
+
+// Custum 兼容函数
+func Custum(c *gin.Context, data gin.H) {
+	data["requestId"] = pkg.GenerateMsgIDFromContext(c)
+	c.Set("result", data)
+	c.AbortWithStatusJSON(http.StatusOK, data)
+}

+ 17 - 0
pkg/response/type.go

@@ -0,0 +1,17 @@
+/*
+ * @Author: lwnmengjing
+ * @Date: 2021/6/8 5:51 下午
+ * @Last Modified by: lwnmengjing
+ * @Last Modified time: 2021/6/8 5:51 下午
+ */
+
+package response
+
+type Responses interface {
+	SetCode(int32)
+	SetTraceID(string)
+	SetMsg(string)
+	SetData(interface{})
+	SetSuccess(bool)
+	Clone() Responses
+}

+ 73 - 0
pkg/security.go

@@ -0,0 +1,73 @@
+package pkg
+
+import (
+	"encoding/hex"
+	"math/rand"
+
+	"golang.org/x/crypto/scrypt"
+)
+
+const (
+	symbol = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+,.?/:;{}[]`~"
+	letter = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
+	figure = "0123456789"
+)
+
+func generateRandString(length int, s string) string {
+	var chars = []byte(s)
+	clen := len(chars)
+	if clen < 2 || clen > 256 {
+		panic("Wrong charset length for NewLenChars()")
+	}
+	maxrb := 255 - (256 % clen)
+	b := make([]byte, length)
+	r := make([]byte, length+(length/4)) // storage for random bytes.
+	i := 0
+	for {
+		if _, err := rand.Read(r); err != nil {
+			panic("Error reading random bytes: " + err.Error())
+		}
+		for _, rb := range r {
+			c := int(rb)
+			if c > maxrb {
+				continue // Skip this number to avoid modulo bias.
+			}
+			b[i] = chars[c%clen]
+			i++
+			if i == length {
+				return string(b)
+			}
+		}
+	}
+}
+
+// GenerateRandomKey20 生成20位随机字符串
+func GenerateRandomKey20() string {
+	return generateRandString(20, symbol)
+}
+
+// GenerateRandomKey16 生成16为随机字符串
+func GenerateRandomKey16() string {
+	return generateRandString(16, symbol)
+}
+
+// GenerateRandomKey6 生成6为随机字符串
+func GenerateRandomKey6() string {
+	return generateRandString(6, letter)
+}
+
+// GenerateRandomFigureKey6 生成6为随机数字字符串
+func GenerateRandomFigureKey6() string {
+	return generateRandString(6, figure)
+}
+
+// SetPassword 根据明文密码和加盐值生成密码
+func SetPassword(password string, salt string) (verify string, err error) {
+	var rb []byte
+	rb, err = scrypt.Key([]byte(password), []byte(salt), 16384, 8, 1, 32)
+	if err != nil {
+		return
+	}
+	verify = hex.EncodeToString(rb)
+	return
+}

+ 56 - 0
pkg/sms/sms.go

@@ -0,0 +1,56 @@
+package sms
+
+import (
+	"encoding/json"
+	"github.com/go-resty/resty/v2"
+)
+
+const (
+	SUCCESS = "success"
+)
+
+type SMS struct {
+	Appid     string
+	Signature string
+}
+
+func NewSMS(appid, signature string) *SMS {
+	return &SMS{
+		Appid:     appid,
+		Signature: signature,
+	}
+}
+
+type SendRes struct {
+	Status  string `json:"status"`
+	Send_id string `json:"send_id"`
+	Fee     int    `json:"fee"`
+	Msg     string `json:"msg"`
+	Code    int    `json:"code"`
+}
+
+// 短信发送
+func (t *SMS) Send(to, content string) (SendRes, error) {
+
+	client := resty.New()
+	resp, err := client.R().
+		SetHeader("Content-Type", "application/x-www-form-urlencoded").
+		SetFormData(map[string]string{
+			"appid":     t.Appid,
+			"signature": t.Signature,
+			"to":        to,
+			"content":   content,
+		}).
+		SetResult(&SendRes{}).
+		Post("https://api-v4.mysubmail.com/sms/send.json")
+
+	if err != nil {
+		return SendRes{}, err
+	}
+
+	temp := SendRes{}
+	if err = json.Unmarshal(resp.Body(), &temp); err != nil {
+		return SendRes{}, err
+	}
+	return temp, nil
+}

+ 150 - 0
pkg/sms/template.go

@@ -0,0 +1,150 @@
+package sms
+
+import (
+	"encoding/json"
+	"github.com/go-resty/resty/v2"
+)
+
+type Template struct {
+	Template_id                 string `json:"template_id"`
+	Sms_title                   string `json:"sms_title"`
+	Sms_signature               string `json:"sms_signature"`
+	Sms_content                 string `json:"sms_content"`
+	Add_date                    string `json:"add_date"`
+	Edit_date                   string `json:"edit_date"`
+	Template_status             string `json:"template_status"`
+	Template_status_description string `json:"template_status_description"`
+}
+
+type TemplateRes struct {
+	Status      string   `json:"status"`
+	Template_id string   `json:"template_id"`
+	Template    Template `json:"template"`
+}
+
+// 通过模板id获取模板
+func (t *SMS) SmsTemplate_Get(template_id string) (TemplateRes, error) {
+	client := resty.New()
+
+	resp, err := client.R().
+		SetQueryParams(map[string]string{
+			"appid":       t.Appid,
+			"signature":   t.Signature,
+			"template_id": template_id,
+		}).
+		Get("https://api-v4.mysubmail.com/sms/template.json")
+
+	if err != nil {
+		return TemplateRes{}, err
+	}
+
+	temp := TemplateRes{}
+	if err = json.Unmarshal(resp.Body(), &temp); err != nil {
+		return TemplateRes{}, err
+	}
+	return temp, nil
+}
+
+// 创建模板
+func (t *SMS) SmsTemplate_Post(sms_title, sms_signature, sms_content string) (TemplateRes, error) {
+	client := resty.New()
+	resp, err := client.R().
+		SetHeader("Content-Type", "application/x-www-form-urlencoded").
+		SetFormData(map[string]string{
+			"appid":         t.Appid,
+			"signature":     t.Signature,
+			"sms_title":     sms_title,
+			"sms_signature": sms_signature,
+			"sms_content":   sms_content,
+		}).
+		Post("http://api.mysubmail.com/sms/template.json")
+
+	if err != nil {
+		return TemplateRes{}, err
+	}
+
+	temp := TemplateRes{}
+	if err = json.Unmarshal(resp.Body(), &temp); err != nil {
+		return TemplateRes{}, err
+	}
+	return temp, nil
+}
+
+// 修改模板
+func (t *SMS) SmsTemplate_Put(template_id, sms_title, sms_signature, sms_content string) (TemplateRes, error) {
+	client := resty.New()
+	resp, err := client.R().
+		SetHeader("Content-Type", "application/x-www-form-urlencoded").
+		SetFormData(map[string]string{
+			"appid":         t.Appid,
+			"signature":     t.Signature,
+			"template_id":   template_id,
+			"sms_title":     sms_title,
+			"sms_signature": sms_signature,
+			"sms_content":   sms_content,
+		}).
+		SetResult(&TemplateRes{}).
+		Put("http://api.mysubmail.com/sms/template.json")
+
+	if err != nil {
+		return TemplateRes{}, err
+	}
+
+	temp := TemplateRes{}
+	if err = json.Unmarshal(resp.Body(), &temp); err != nil {
+		return TemplateRes{}, err
+	}
+	return temp, nil
+}
+
+// 删除模板
+func (t *SMS) SmsTemplate_Delete(template_id string) (TemplateRes, error) {
+	client := resty.New()
+	resp, err := client.R().
+		SetHeader("Content-Type", "application/x-www-form-urlencoded").
+		SetFormData(map[string]string{
+			"appid":       t.Appid,
+			"signature":   t.Signature,
+			"template_id": template_id,
+		}).
+		SetResult(&TemplateRes{}).
+		Delete("http://api.mysubmail.com/sms/template.json")
+
+	if err != nil {
+		return TemplateRes{}, err
+	}
+
+	temp := TemplateRes{}
+	if err = json.Unmarshal(resp.Body(), &temp); err != nil {
+		return TemplateRes{}, err
+	}
+	return temp, nil
+}
+
+// 短信模板发送
+// vars json字符串 格式 {"name1":"value1","name2":"value2"}
+func (t *SMS) Template_XSend(template_id, to, vars string) (SendRes, error) {
+
+	client := resty.New()
+	resp, err := client.R().
+		SetHeader("Content-Type", "application/x-www-form-urlencoded").
+		SetFormData(map[string]string{
+			"appid":     t.Appid,
+			"signature": t.Signature,
+			"to":        to,
+			"project":   template_id,
+			"vars":      vars,
+		}).
+		SetResult(&SendRes{}).
+		Post("https://api-v4.mysubmail.com/sms/xsend")
+
+	if err != nil {
+		return SendRes{}, err
+	}
+
+	temp := SendRes{}
+	if err = json.Unmarshal(resp.Body(), &temp); err != nil {
+		return SendRes{}, err
+	}
+	return temp, nil
+}

+ 41 - 0
pkg/string.go

@@ -0,0 +1,41 @@
+package pkg
+
+import (
+	"encoding/json"
+	"strconv"
+	"time"
+)
+
+func StringToInt(e string) (int, error) {
+	return strconv.Atoi(e)
+}
+
+func GetCurrentTimeStr() string {
+	return time.Now().Format("2006-01-02 15:04:05")
+}
+
+func GetCurrentTime() time.Time {
+	return time.Now()
+}
+
+func CheckDateStr(date string) bool {
+	if _, err := time.Parse("2006-01-02", date); err != nil {
+		return false
+	}
+	return true
+}
+
+func StructToJsonStr(e interface{}) (string, error) {
+	if b, err := json.Marshal(e); err == nil {
+		return string(b), err
+	} else {
+		return "", err
+	}
+}
+
+func IsEmptyStr(e string) bool {
+	if e == "" {
+		return true
+	}
+	return false
+}

+ 72 - 0
pkg/textcolor.go

@@ -0,0 +1,72 @@
+package pkg
+
+import (
+	"fmt"
+)
+
+// 前景 背景 颜色
+// ---------------------------------------
+// 30  40  黑色
+// 31  41  红色
+// 32  42  绿色
+// 33  43  黄色
+// 34  44  蓝色
+// 35  45  紫红色
+// 36  46  青蓝色
+// 37  47  白色
+//
+// 代码 意义
+// -------------------------
+//  0  终端默认设置
+//  1  高亮显示
+//  4  使用下划线
+//  5  闪烁
+//  7  反白显示
+//  8  不可见
+
+const (
+	TextBlack = iota + 30
+	TextRed
+	TextGreen
+	TextYellow
+	TextBlue
+	TextMagenta
+	TextCyan
+	TextWhite
+)
+
+func Black(msg string) string {
+	return SetColor(msg, 0, 0, TextBlack)
+}
+
+func Red(msg string) string {
+	return SetColor(msg, 0, 0, TextRed)
+}
+
+func Green(msg string) string {
+	return SetColor(msg, 0, 0, TextGreen)
+}
+
+func Yellow(msg string) string {
+	return SetColor(msg, 0, 0, TextYellow)
+}
+
+func Blue(msg string) string {
+	return SetColor(msg, 0, 0, TextBlue)
+}
+
+func Magenta(msg string) string {
+	return SetColor(msg, 0, 0, TextMagenta)
+}
+
+func Cyan(msg string) string {
+	return SetColor(msg, 0, 0, TextCyan)
+}
+
+func White(msg string) string {
+	return SetColor(msg, 0, 0, TextWhite)
+}
+
+func SetColor(msg string, conf, bg, text int) string {
+	return fmt.Sprintf("%c[%d;%d;%dm%s%c[0m", 0x1B, conf, bg, text, msg, 0x1B)
+}

+ 28 - 0
pkg/translate.go

@@ -0,0 +1,28 @@
+package pkg
+
+import (
+	"reflect"
+)
+
+func Translate(form, to interface{}) {
+	fType := reflect.TypeOf(form)
+	fValue := reflect.ValueOf(form)
+	if fType.Kind() == reflect.Ptr {
+		fType = fType.Elem()
+		fValue = fValue.Elem()
+	}
+	tType := reflect.TypeOf(to)
+	tValue := reflect.ValueOf(to)
+	if tType.Kind() == reflect.Ptr {
+		tType = tType.Elem()
+		tValue = tValue.Elem()
+	}
+	for i := 0; i < fType.NumField(); i++ {
+		for j := 0; j < tType.NumField(); j++ {
+			if fType.Field(i).Name == tType.Field(j).Name &&
+				fType.Field(i).Type.ConvertibleTo(tType.Field(j).Type) {
+				tValue.Field(j).Set(fValue.Field(i))
+			}
+		}
+	}
+}

+ 26 - 0
pkg/url.go

@@ -0,0 +1,26 @@
+package pkg
+
+import (
+	"strings"
+
+	"github.com/gin-gonic/gin"
+)
+
+//获取URL中批量id并解析
+func IdsStrToIdsIntGroup(key string, c *gin.Context) []int {
+	return IdsStrToIdsIntGroupStr(c.Param(key))
+}
+
+type Ids struct {
+	Ids []int
+}
+
+func IdsStrToIdsIntGroupStr(keys string) []int {
+	IDS := make([]int, 0)
+	ids := strings.Split(keys, ",")
+	for i := 0; i < len(ids); i++ {
+		ID, _ := StringToInt(ids[i])
+		IDS = append(IDS, ID)
+	}
+	return IDS
+}

+ 83 - 0
pkg/utils.go

@@ -0,0 +1,83 @@
+package pkg
+
+import (
+	"errors"
+	"log"
+	"runtime"
+	"strconv"
+
+	"github.com/gin-gonic/gin"
+	"github.com/google/uuid"
+	"golang.org/x/crypto/bcrypt"
+	"gorm.io/gorm"
+)
+
+const (
+	TrafficKey = "X-Request-Id"
+	LoggerKey  = "_OAuth-logger-request"
+)
+
+func CompareHashAndPassword(e string, p string) (bool, error) {
+	err := bcrypt.CompareHashAndPassword([]byte(e), []byte(p))
+	if err != nil {
+		return false, err
+	}
+	return true, nil
+}
+
+// Assert 条件断言
+// 当断言条件为 假 时触发 panic
+// 对于当前请求不会再执行接下来的代码,并且返回指定格式的错误信息和错误码
+func Assert(condition bool, msg string, code ...int) {
+	if !condition {
+		statusCode := 200
+		if len(code) > 0 {
+			statusCode = code[0]
+		}
+		panic("CustomError#" + strconv.Itoa(statusCode) + "#" + msg)
+	}
+}
+
+// HasError 错误断言
+// 当 error 不为 nil 时触发 panic
+// 对于当前请求不会再执行接下来的代码,并且返回指定格式的错误信息和错误码
+// 若 msg 为空,则默认为 error 中的内容
+func HasError(err error, msg string, code ...int) {
+	if err != nil {
+		statusCode := 200
+		if len(code) > 0 {
+			statusCode = code[0]
+		}
+		if msg == "" {
+			msg = err.Error()
+		}
+		_, file, line, _ := runtime.Caller(1)
+		log.Printf("%s:%v error: %#v", file, line, err)
+		panic("CustomError#" + strconv.Itoa(statusCode) + "#" + msg)
+	}
+}
+
+// GenerateMsgIDFromContext 生成msgID
+func GenerateMsgIDFromContext(c *gin.Context) string {
+	requestId := c.GetHeader(TrafficKey)
+	if requestId == "" {
+		requestId = uuid.New().String()
+		c.Header(TrafficKey, requestId)
+	}
+	return requestId
+}
+
+// GetOrm 获取orm连接
+func GetOrm(c *gin.Context) (*gorm.DB, error) {
+	idb, exist := c.Get("db")
+	if !exist {
+		return nil, errors.New("db connect not exist")
+	}
+	switch idb.(type) {
+	case *gorm.DB:
+		//新增操作
+		return idb.(*gorm.DB), nil
+	default:
+		return nil, errors.New("db connect not exist")
+	}
+}

+ 131 - 0
pkg/utils/file.go

@@ -0,0 +1,131 @@
+package utils
+
+import (
+	"errors"
+	"io/ioutil"
+	"log"
+	"mime/multipart"
+	"net/http"
+	"os"
+	"path"
+	"strings"
+
+	"github.com/shamsher31/goimgext"
+)
+
+// GetSize 获取文件大小
+func GetSize(f multipart.File) (int, error) {
+	content, err := ioutil.ReadAll(f)
+
+	return len(content), err
+}
+
+// GetExt 获取文件后缀
+func GetExt(fileName string) string {
+	return path.Ext(fileName)
+}
+
+// CheckExist 检查文件是否存在
+func CheckExist(src string) bool {
+	_, err := os.Stat(src)
+	if err != nil {
+		return os.IsNotExist(err)
+	}
+	return true
+
+}
+
+// CheckPermission 检查文件权限
+func CheckPermission(src string) bool {
+	_, err := os.Stat(src)
+
+	return os.IsPermission(err)
+}
+
+// IsNotExistMkDir 检查文件夹是否存在
+// 如果不存在则新建文件夹
+func IsNotExistMkDir(src string) error {
+	if exist := !CheckExist(src); exist == false {
+		if err := MkDir(src); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+// MkDir 新建文件夹
+func MkDir(src string) error {
+	err := os.MkdirAll(src, os.ModePerm)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// Open 打开文件
+func Open(name string, flag int, perm os.FileMode) (*os.File, error) {
+	f, err := os.OpenFile(name, flag, perm)
+	if err != nil {
+		return nil, err
+	}
+
+	return f, nil
+}
+
+// GetImgType 获取Img文件类型
+func GetImgType(p string) (string, error) {
+	file, err := os.Open(p)
+
+	if err != nil {
+		log.Println(err)
+		os.Exit(1)
+	}
+
+	buff := make([]byte, 512)
+
+	_, err = file.Read(buff)
+
+	if err != nil {
+		log.Println(err)
+		os.Exit(1)
+	}
+
+	filetype := http.DetectContentType(buff)
+
+	ext := imgext.Get()
+
+	for i := 0; i < len(ext); i++ {
+		if strings.Contains(ext[i], filetype[6:len(filetype)]) {
+			return filetype, nil
+		}
+	}
+
+	return "", errors.New("Invalid image type")
+}
+
+// GetType 获取文件类型
+func GetType(p string) (string, error) {
+	file, err := os.Open(p)
+
+	if err != nil {
+		log.Println(err)
+		os.Exit(1)
+	}
+
+	buff := make([]byte, 512)
+
+	_, err = file.Read(buff)
+
+	if err != nil {
+		log.Println(err)
+	}
+
+	filetype := http.DetectContentType(buff)
+
+	//ext := GetExt(p)
+	//var list = strings.Split(filetype, "/")
+	//filetype = list[0] + "/" + ext
+	return filetype, nil
+}

Some files were not shown because too many files changed in this diff