wucan 9 miesięcy temu
commit
b7c154e8cf
54 zmienionych plików z 3535 dodań i 0 usunięć
  1. 18 0
      app/common/enum/msg.go
  2. 10 0
      app/common/enum/status.go
  3. 60 0
      app/common/log/log.go
  4. 14 0
      app/common/request/auth.go
  5. 28 0
      app/common/request/user.go
  6. 26 0
      app/common/request/validate.go
  7. 30 0
      app/common/request/validator.go
  8. 131 0
      app/common/response/response.go
  9. 21 0
      bootstrap/config.go
  10. 130 0
      bootstrap/db.go
  11. 41 0
      bootstrap/ipFile.go
  12. 114 0
      bootstrap/log.go
  13. 77 0
      bootstrap/redis.go
  14. 118 0
      bootstrap/router.go
  15. 329 0
      common/common.go
  16. 42 0
      common/ip.go
  17. 88 0
      common/redis.go
  18. 45 0
      config/app.go
  19. 160 0
      config/common.go
  20. 16 0
      config/config.go
  21. 17 0
      config/database.go
  22. 9 0
      config/download.go
  23. BIN
      config/excel/city.xlsx
  24. BIN
      config/excel/game.xlsx
  25. 142 0
      config/json/city.json
  26. 10 0
      config/json/game.json
  27. 9 0
      config/jwt.go
  28. 11 0
      config/log.go
  29. 10 0
      config/redis.go
  30. 10 0
      config/temp.go
  31. 200 0
      controller/v1/user.go
  32. 64 0
      global/app.go
  33. 18 0
      global/error.go
  34. 68 0
      global/lock.go
  35. 55 0
      go.mod
  36. 146 0
      go.sum
  37. 71 0
      main.go
  38. 174 0
      middleware/auth.go
  39. 60 0
      model/user.go
  40. 123 0
      response/response.go
  41. 20 0
      route/api.go
  42. 89 0
      service/UserOnlineService.go
  43. 52 0
      service/remainData.go
  44. 309 0
      service/userBehavior.go
  45. 35 0
      utils/array.go
  46. 23 0
      utils/bcrypt.go
  47. 128 0
      utils/db.go
  48. 46 0
      utils/directory.go
  49. 8 0
      utils/json.go
  50. 17 0
      utils/map.go
  51. 12 0
      utils/md5.go
  52. 32 0
      utils/str.go
  53. 53 0
      utils/time.go
  54. 16 0
      utils/validator.go

+ 18 - 0
app/common/enum/msg.go

@@ -0,0 +1,18 @@
+package enum
+
+import "designs/utils"
+
+var (
+	// MSG_DATABASE_ERROR mysql database error
+	MSG_DATABASE_ERROR = "database error"
+	// MSG_JSON_PARSE_ERROR json parse error
+	MSG_JSON_PARSE_ERROR = "json parse error"
+	// MSG_HTTP_REQUEST_ERROR http request error
+	MSG_HTTP_REQUEST_ERROR = "http request error"
+)
+
+var SystemErrorList = []string{MSG_DATABASE_ERROR, MSG_JSON_PARSE_ERROR, MSG_HTTP_REQUEST_ERROR}
+
+func IsSystemError(err error) bool {
+	return utils.InArray(err.Error(), SystemErrorList)
+}

+ 10 - 0
app/common/enum/status.go

@@ -0,0 +1,10 @@
+package enum
+
+const (
+	NormalError = 1   //普通报错
+	NoAuthError = 401 //没有权限
+
+	//错误类型
+	DatabaseError = "database error"
+	RedisError    = "redis error"
+)

+ 60 - 0
app/common/log/log.go

@@ -0,0 +1,60 @@
+package log
+
+import (
+	"designs/global"
+	"fmt"
+
+	"github.com/pkg/errors"
+	"go.uber.org/zap"
+)
+
+type causer interface {
+	Format(s fmt.State, verb rune)
+}
+
+// 会打印error时的堆栈
+func ErrorWithStack(err error, msg ...string) error {
+	titleMsg := ""
+	if len(msg) > 0 {
+		titleMsg = msg[0]
+	}
+	if _, ok := err.(causer); ok {
+		global.App.Log.Errorf("%s%+v", titleMsg, errors.WithStack(err))
+	} else {
+		global.App.Log.Error(titleMsg, err.Error(), zap.StackSkip("", 1))
+	}
+
+	return err
+}
+
+func Debug(args ...interface{}) {
+	global.App.Log.Debug(args...)
+}
+
+func Info(args ...interface{}) {
+	global.App.Log.Info(args...)
+}
+
+func Warn(args ...interface{}) {
+	global.App.Log.Warn(args...)
+}
+
+func Error(args ...interface{}) {
+	global.App.Log.Error(args...)
+}
+
+func Debugf(format string, args ...interface{}) {
+	global.App.Log.Debugf(format, args...)
+}
+
+func Infof(format string, args ...interface{}) {
+	global.App.Log.Infof(format, args...)
+}
+
+func Warnf(format string, args ...interface{}) {
+	global.App.Log.Warnf(format, args...)
+}
+
+func Errorf(format string, args ...interface{}) {
+	global.App.Log.Errorf(format, args...)
+}

+ 14 - 0
app/common/request/auth.go

@@ -0,0 +1,14 @@
+package request
+
+import (
+	"github.com/gin-gonic/gin"
+)
+
+type AuthReq struct {
+}
+
+var Auth = new(AuthReq)
+
+func (a *AuthReq) User(c *gin.Context) {
+
+}

+ 28 - 0
app/common/request/user.go

@@ -0,0 +1,28 @@
+package request
+
+type Register struct {
+	Name     string `form:"name" json:"name" binding:"required"`
+	Mobile   string `form:"mobile" json:"mobile" binding:"required,mobile"`
+	Password string `form:"password" json:"password" binding:"required"`
+}
+
+func (register Register) GetMessages() ValidatorMessages {
+	return ValidatorMessages{
+		"name.required":     "用户名称不能为空",
+		"mobile.required":   "手机号码不能为空",
+		"mobile.mobile":     "手机号码格式不正确",
+		"password.required": "用户密码不能为空",
+	}
+}
+
+type Login struct {
+	Email    string `form:"email" json:"email" binding:"required"`
+	Password string `form:"password" json:"password" binding:"required"`
+}
+
+func (login Login) GetMessages() ValidatorMessages {
+	return ValidatorMessages{
+		"email.required":    "账号不能为空",
+		"password.required": "用户密码不能为空",
+	}
+}

+ 26 - 0
app/common/request/validate.go

@@ -0,0 +1,26 @@
+package request
+
+import (
+	"github.com/gin-gonic/gin"
+)
+
+type ValidateError struct {
+	Err error
+}
+
+func NewValidateError(err error) ValidateError {
+	return ValidateError{Err: err}
+}
+
+func (e ValidateError) Error() string {
+	return e.Err.Error()
+}
+
+func Check[T any](c *gin.Context, obj *T) *T {
+
+	if err := c.ShouldBindJSON(obj); err != nil {
+		panic(NewValidateError(err))
+	}
+
+	return obj
+}

+ 30 - 0
app/common/request/validator.go

@@ -0,0 +1,30 @@
+package request
+
+import (
+	"github.com/go-playground/validator/v10"
+)
+
+type Validator interface {
+	GetMessages() ValidatorMessages
+}
+
+type ValidatorMessages map[string]string
+
+// GetErrorMsg 获取错误信息
+func GetErrorMsg(request interface{}, err error) string {
+	if _, isValidatorErrors := err.(validator.ValidationErrors); isValidatorErrors {
+		_, isValidator := request.(Validator)
+
+		for _, v := range err.(validator.ValidationErrors) {
+			// 若 request 结构体实现 Validator 接口即可实现自定义错误信息
+			if isValidator {
+				if message, exist := request.(Validator).GetMessages()[v.Field()+"."+v.Tag()]; exist {
+					return message
+				}
+			}
+			return v.Error()
+		}
+	}
+
+	return "数据格式错误"
+}

+ 131 - 0
app/common/response/response.go

@@ -0,0 +1,131 @@
+package response
+
+import (
+	"designs/config"
+	"designs/global"
+	"fmt"
+	"net/http"
+	"strings"
+	"time"
+
+	"github.com/gin-gonic/gin"
+	"github.com/pkg/errors"
+	"go.uber.org/zap"
+)
+
+// 500 程序奔溃
+func ServerError(c *gin.Context, msg string) {
+	errMsg := "服务程序错误"
+	if !config.IsProduction() {
+		stack := strings.Split(strings.Replace(msg, "\t", "--", -1), "\n")
+
+		c.JSON(http.StatusInternalServerError, gin.H{
+			"code":  http.StatusInternalServerError,
+			"msg":   errMsg,
+			"stack": stack,
+		})
+	} else {
+		c.JSON(http.StatusInternalServerError, gin.H{
+			"code": http.StatusInternalServerError,
+			"msg":  errMsg,
+		})
+	}
+
+	c.Abort()
+}
+
+func Success(c *gin.Context, data gin.H) {
+	//for k, v := range data {
+	//
+	//}
+	data["code"] = 0
+	if data["data"] == nil {
+		data["data"] = "success"
+	}
+	if data["msg"] == nil {
+		data["msg"] = "success"
+	}
+	//fmt.Println(data)
+	data["timestamp"] = time.Now().Unix()
+	c.JSON(http.StatusOK, data)
+}
+
+type causer interface {
+	Format(s fmt.State, verb rune)
+}
+
+func Next(c *gin.Context, resp interface{}) {
+	c.JSON(http.StatusOK, resp)
+}
+
+func Fail(c *gin.Context, errorCode int, e interface{}) {
+	var msg string
+	var stack []string
+	if _, ok := e.(string); ok {
+		msg = e.(string)
+	} else if err, ok := e.(error); ok {
+		if _, ok := e.(causer); ok {
+			msg = strings.Split(fmt.Sprintf("%v", errors.WithStack(err)), ":")[0]
+			if !config.IsProduction() {
+				tmp := fmt.Sprintf("%+v", errors.WithStack(err))
+				stack = strings.Split(strings.Replace(tmp, "\t", "--", -1), "\n")
+			}
+		} else {
+			msg = fmt.Sprintf(err.Error())
+			if !config.IsProduction() {
+				tmp := fmt.Sprintf("%v", zap.StackSkip("", 1))
+				stack = strings.Split(strings.Replace(tmp, "\t", "--", -1), "\n")
+			}
+		}
+	}
+
+	if len(stack) > 0 {
+		c.JSON(http.StatusOK, gin.H{
+			"code":  errorCode,
+			"msg":   msg,
+			"data":  "",
+			"stack": stack,
+		})
+	} else {
+		c.JSON(http.StatusOK, gin.H{
+			"data": "",
+			"code": errorCode,
+			"msg":  msg,
+		})
+	}
+
+	c.Abort()
+}
+
+func ValidateFail(c *gin.Context, msg string) {
+	Fail(c, global.Errors.ValidateError.ErrorCode, msg)
+}
+
+// 422 参数错误使用
+func ParameterError(c *gin.Context, msg interface{}) {
+	finalMsg := "参数错误"
+	if err, ok := msg.(error); ok {
+		// release 版本屏蔽掉报错
+		if !config.IsProduction() {
+			finalMsg = err.Error()
+		}
+	} else if str, ok := msg.(string); ok {
+		finalMsg = str
+	}
+
+	c.JSON(http.StatusUnprocessableEntity, gin.H{
+		"code": http.StatusUnprocessableEntity,
+		"msg":  finalMsg,
+	})
+	c.Abort()
+}
+
+// 401 权限错误
+func UnauthorizedRequestsFail(c *gin.Context, msg string) {
+	//c.JSON(http.StatusUnauthorized, gin.H{
+	c.JSON(http.StatusOK, gin.H{
+		"code": http.StatusUnauthorized,
+		"msg":  msg,
+	})
+	c.Abort()
+}

+ 21 - 0
bootstrap/config.go

@@ -0,0 +1,21 @@
+package bootstrap
+
+import (
+	"designs/config"
+
+	"github.com/go-ini/ini"
+)
+
+func InitializeConfig(configPath string) {
+	cfg, err := ini.Load(configPath)
+	if err != nil {
+		panic(err)
+	}
+
+	envMap := cfg.Section("").KeysHash()
+
+	if err := config.InitConfig(envMap, &config.RootConfig); err != nil {
+		panic(err)
+	}
+
+}

+ 130 - 0
bootstrap/db.go

@@ -0,0 +1,130 @@
+package bootstrap
+
+import (
+	"designs/config"
+	"designs/global"
+	"io"
+	"log"
+	"os"
+	"time"
+
+	"go.uber.org/zap"
+	"gorm.io/driver/mysql"
+	"gorm.io/gorm"
+	"gorm.io/gorm/logger"
+)
+
+func InitializeDB() *gorm.DB {
+	// 根据驱动配置进行初始化
+	switch config.Get("database.driver") {
+	case "mysql":
+		return initMySqlGorm()
+	default:
+		return initMySqlGorm()
+	}
+}
+
+func UnInitializeDB(db *gorm.DB) {
+	if db != nil {
+		if sqldb, err := db.DB(); err == nil {
+			sqldb.Close()
+		} else {
+			global.App.Log.Errorf("close database failed: %v", err)
+		}
+	}
+}
+
+func initMySqlGorm() *gorm.DB {
+
+	UserName := config.Get("database.userName")
+	Password := config.Get("database.password")
+	Host := config.Get("database.host")
+	Port := config.Get("database.port")
+	Database := config.Get("database.database")
+	Charset := config.Get("database.charset")
+
+	dsn := UserName + ":" + Password + "@tcp(" + Host + ":" + Port + ")/" +
+		Database + "?charset=" + Charset + "&parseTime=True&loc=Local"
+	mysqlConfig := mysql.Config{
+		DSN:                       dsn,   // DSN data source name
+		DefaultStringSize:         191,   // string 类型字段的默认长度
+		DisableDatetimePrecision:  true,  // 禁用 datetime 精度,MySQL 5.6 之前的数据库不支持
+		DontSupportRenameIndex:    true,  // 重命名索引时采用删除并新建的方式,MySQL 5.7 之前的数据库和 MariaDB 不支持重命名索引
+		DontSupportRenameColumn:   true,  // 用 `change` 重命名列,MySQL 8 之前的数据库和 MariaDB 不支持重命名列
+		SkipInitializeWithVersion: false, // 根据版本自动配置
+	}
+	if db, err := gorm.Open(mysql.New(mysqlConfig), &gorm.Config{
+		DisableForeignKeyConstraintWhenMigrating: true,            // 禁用自动创建外键约束
+		Logger:                                   getGormLogger(), // 使用自定义 Logger
+	}); err != nil {
+		global.App.Log.Error("mysql connect failed, err:", zap.Any("err", err))
+		return nil
+	} else {
+		sqlDB, _ := db.DB()
+		sqlDB.SetMaxIdleConns(config.GetInt("database.maxIdleConns"))
+		sqlDB.SetMaxOpenConns(config.GetInt("database.maxOpenConns"))
+		initMySqlTables(db)
+		return db
+	}
+}
+
+func getGormLogger() logger.Interface {
+	var logMode logger.LogLevel
+
+	switch config.Get("database.logMode") {
+	case "silent":
+		logMode = logger.Silent
+	case "error":
+		logMode = logger.Error
+	case "warn":
+		logMode = logger.Warn
+	case "info":
+		logMode = logger.Info
+	default:
+		logMode = logger.Info
+	}
+
+	return logger.New(getGormLogWriter(), logger.Config{
+		SlowThreshold:             2 * time.Second,                                 // 慢 SQL 阈值
+		LogLevel:                  logMode,                                         // 日志级别
+		IgnoreRecordNotFoundError: true,                                            // 忽略ErrRecordNotFound(记录未找到)错误
+		Colorful:                  !config.GetBool("database.enableFileLogWriter"), // 禁用彩色打印
+	})
+}
+
+// 自定义 gorm Writer
+func getGormLogWriter() logger.Writer {
+	var writer io.Writer
+
+	// 是否启用日志文件
+	if config.GetBool("database.enableFileLogWriter") {
+		// 自定义 Writer
+		// writer = &lumberjack.Logger{
+		// 	Filename:   global.App.Config.Log.RootDir + "/" + global.App.Config.Database.LogFilename,
+		// 	MaxSize:    global.App.Config.Log.MaxSize,
+		// 	MaxBackups: global.App.Config.Log.MaxBackups,
+		// 	MaxAge:     global.App.Config.Log.MaxAge,
+		// 	Compress:   global.App.Config.Log.Compress,
+		// }
+		if global.App.LogWriter == nil {
+			panic("log writer is nil")
+		}
+		writer = global.App.LogWriter
+	} else {
+		// 默认 Writer
+		writer = os.Stdout
+	}
+	return log.New(writer, "\r\n", log.LstdFlags)
+}
+
+// 数据库表初始化
+func initMySqlTables(db *gorm.DB) {
+	err := db.AutoMigrate(
+	// models.User{},
+	// models.Media{},
+	)
+	if err != nil {
+		global.App.Log.Error("migrate table failed", zap.Any("err", err))
+		os.Exit(0)
+	}
+}

+ 41 - 0
bootstrap/ipFile.go

@@ -0,0 +1,41 @@
+package bootstrap
+
+import (
+	"fmt"
+	"github.com/lionsoul2014/ip2region/binding/golang/xdb"
+	"strings"
+)
+
+var cBuffData []byte
+
+func InitIpFile() {
+	// 1、从 dbPath 加载整个 xdb 到内存
+	dbPath := "./ip2region.xdb"
+	cBuff, err := xdb.LoadContentFromFile(dbPath)
+	if err != nil {
+		fmt.Printf("failed to load content from `%s`: %s\n", dbPath, err)
+		return
+	}
+	cBuffData = cBuff
+}
+
+/* 通过id地址获取省份 并发使用时候 每个goruiting 需要创建一个独立的search */
+func FindAddressByIp(ip string) (string, string) {
+	// 2、用全局的 cBuff 创建完全基于内存的查询对象。
+	searcher, err := xdb.NewWithBuffer(cBuffData)
+	if err != nil {
+		fmt.Printf("failed to create searcher with content: %s\n", err)
+		return "", ""
+	}
+
+	// do the search
+	// var tStart = time.Now()
+	region, err := searcher.SearchByStr(ip)
+	if err != nil {
+		fmt.Printf("failed to SearchIP(%s): %s\n", ip, err)
+		return "", ""
+	}
+	data := strings.Split(region, "|")
+	// fmt.Printf("{region: %s, took: %s}\n", data, time.Since(tStart))
+	return data[2], data[3]
+}

+ 114 - 0
bootstrap/log.go

@@ -0,0 +1,114 @@
+package bootstrap
+
+import (
+	"fmt"
+	"io"
+	"path/filepath"
+
+	rotatelogs "github.com/lestrrat-go/file-rotatelogs"
+
+	"designs/config"
+	"designs/global"
+	"designs/utils"
+
+	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
+
+	// "gopkg.in/natefinch/lumberjack.v2"
+	"os"
+	"time"
+)
+
+var (
+	level   zapcore.Level // zap 日志等级
+	options []zap.Option  // zap 配置项
+)
+
+func InitializeLog() (*zap.SugaredLogger, io.Writer) {
+	// 创建根目录
+	createRootDir()
+	// 设置日志等级
+	setLogLevel()
+
+	if config.GetBool("log.showLine") {
+		options = append(options, zap.AddCaller())
+	}
+
+	writer := getLogWriter() // file-rotatelogs
+
+	// 初始化 zap
+	logger := zap.New(getZapCore(writer), options...)
+	return logger.Sugar(), writer
+}
+
+func createRootDir() {
+	if ok, _ := utils.PathExists(config.Get("log.rootDir")); !ok {
+
+		if err := os.Mkdir(config.Get("log.rootDir"), os.ModePerm); err != nil {
+			panic(fmt.Errorf("mkdir log root dir failed: %s, %s \n", config.Get("log.rootDir"), err))
+		}
+	}
+}
+
+func setLogLevel() {
+	switch config.Get("log.level") {
+	case "debug":
+		level = zap.DebugLevel
+		options = append(options, zap.AddStacktrace(level))
+	case "info":
+		level = zap.InfoLevel
+	case "warn":
+		level = zap.WarnLevel
+	case "error":
+		level = zap.ErrorLevel
+		options = append(options, zap.AddStacktrace(level))
+	case "dpanic":
+		level = zap.DPanicLevel
+	case "panic":
+		level = zap.PanicLevel
+	case "fatal":
+		level = zap.FatalLevel
+	default:
+		level = zap.InfoLevel
+	}
+}
+
+// 扩展 Zap
+func getZapCore(writer io.Writer) zapcore.Core {
+	var encoder zapcore.Encoder
+
+	// 调整编码器默认配置
+	encoderConfig := zap.NewProductionEncoderConfig()
+	encoderConfig.EncodeTime = func(time time.Time, encoder zapcore.PrimitiveArrayEncoder) {
+		encoder.AppendString(time.Format("[" + "2006-01-02 15:04:05.000" + "]"))
+	}
+	encoderConfig.EncodeLevel = func(l zapcore.Level, encoder zapcore.PrimitiveArrayEncoder) {
+		encoder.AppendString(config.Get("app.env") + "." + l.String())
+	}
+
+	// 设置编码器
+	if config.Get("log.format") == "json" {
+		encoder = zapcore.NewJSONEncoder(encoderConfig)
+	} else {
+		encoder = zapcore.NewConsoleEncoder(encoderConfig)
+	}
+
+	syncWriter := zapcore.AddSync(writer)
+	return zapcore.NewCore(encoder, syncWriter, level)
+}
+
+func getLogWriter() io.Writer {
+	logFile := config.Get("log.filename")
+
+	writer, err := rotatelogs.New(
+		filepath.Join(global.App.PwdPath, "storage", "logs", logFile),
+		rotatelogs.WithMaxAge(-1),
+		rotatelogs.WithRotationTime(24*time.Hour),
+	)
+
+	if err != nil {
+		panic(fmt.Errorf("failed to create rotatelogs: %s", err))
+	}
+
+	return writer
+}

+ 77 - 0
bootstrap/redis.go

@@ -0,0 +1,77 @@
+package bootstrap
+
+import (
+	"context"
+	"designs/config"
+	"designs/global"
+	"github.com/go-redis/redis/v8"
+	"go.uber.org/zap"
+	"strconv"
+	"strings"
+)
+
+func InitializeRedis() *redis.Client {
+	client := redis.NewClient(&redis.Options{
+		Addr:     config.Get("redis.host") + ":" + config.Get("redis.port"),
+		Password: config.Get("redis.password"), // no password set
+		DB:       config.GetInt("redis.db"),    // use default DB
+
+		//Addr:     "localhost:6379", // Redis 服务器地址和端口
+		//Password: "",               // Redis 访问密码,如果没有可以为空字符串
+		//DB:       0,                // 使用的 Redis 数据库编号,默认为 0
+	})
+	_, err := client.Ping(context.Background()).Result()
+	if err != nil {
+		global.App.Log.Error("Redis connect ping failed, err:", zap.Any("err", err))
+		return nil
+	}
+	return client
+}
+
+func UnInitializeRedis(client *redis.Client) {
+	if client != nil {
+		client.Close()
+	}
+}
+
+/* 通过ip地址获取地址code */
+func FindAddresscodeByIp(ip string) int {
+	pro, _ := FindAddressByIp(ip)
+	code := 999
+	res, err := global.App.Redis.HGetAll(context.Background(), config.Get("app.province_table")).Result()
+	if err != nil {
+		return code
+	}
+	for key, val := range res {
+		code, _ := strconv.Atoi(val)
+
+		if strings.Contains(pro, key) {
+			return code
+		}
+	}
+	return code
+}
+
+/* 通过hid获取地址code */
+func FindAddresscodeByHid(hid int) (int, error) {
+	res, _ := global.App.Redis.HGetAll(context.Background(), config.Get("app.province_table")).Result()
+	for _, val := range res {
+		code, _ := strconv.Atoi(val)
+		if code == hid {
+			return hid, nil
+		}
+	}
+	return 0, redis.Nil
+}
+
+/* 通过hid获取地址 */
+func FindAddressByHid(hid int) string {
+	res, _ := global.App.Redis.HGetAll(context.Background(), config.Get("app.province_table")).Result()
+	for key, val := range res {
+		code, _ := strconv.Atoi(val)
+		if code == hid {
+			return key
+		}
+	}
+	return ""
+}

+ 118 - 0
bootstrap/router.go

@@ -0,0 +1,118 @@
+package bootstrap
+
+import (
+	"context"
+	"designs/app/common/request"
+	"designs/app/common/response"
+	"designs/config"
+	"designs/global"
+	"designs/route"
+	"fmt"
+	"go.uber.org/zap"
+	"log"
+	"net/http"
+	"os"
+	"os/signal"
+	"runtime/debug"
+	"syscall"
+	"time"
+
+	"github.com/gin-gonic/gin"
+)
+
+// 回收致命错误
+func CustomRecovery(c *gin.Context) {
+	defer func() {
+		if r := recover(); r != nil {
+
+			if v, ok := r.(request.ValidateError); ok {
+				response.ParameterError(c, v)
+				return
+			}
+
+			//打印错误堆栈信息
+			errTitle := fmt.Sprintf("panic: %v", r)
+			errMsg := fmt.Sprintf("stack: %v", string(debug.Stack()))
+
+			global.App.Log.Error("custom_recovery", zap.Any("info", errTitle), zap.Stack("stack"))
+
+			response.ServerError(c, errTitle+errMsg)
+		}
+	}()
+	//继续后续接口调用
+	c.Next()
+
+}
+
+func corsMiddleware() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		c.Writer.Header().Set("Access-Control-Allow-Origin", "*") // 或者指定允许的源
+		c.Writer.Header().Set("Access-Control-Max-Age", "86400")  // 缓存请求信息
+		c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
+		c.Writer.Header().Set("Access-Control-Allow-Headers", "Origin, Content-Length, Content-Type, Authorization, X-Requested-With")
+		c.Writer.Header().Set("Access-Control-Expose-Headers", "Content-Length, Content-Type")
+		c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
+		if c.Request.Method == "OPTIONS" {
+			c.AbortWithStatus(204)
+			return
+		}
+
+		c.Next()
+	}
+}
+
+func setupRouter() *gin.Engine {
+	if config.Get("app.env") == "production" {
+		gin.SetMode(gin.ReleaseMode)
+	}
+	router := gin.New()
+	router.Use(gin.Logger(), CustomRecovery)
+	router.Use(corsMiddleware())
+	// 跨域处理
+	//router.Use(middleware.Cors())
+
+	// 前端项目静态资源
+	// router.StaticFile("/", "./static/dist/index.html")
+	// router.Static("/assets", "./static/dist/assets")
+	// router.StaticFile("/favicon.ico", "./static/dist/favicon.ico")
+	// 其他静态资源
+	// router.Static("/public", "./static")
+	// router.Static("/storage", "./storage/app/public")
+
+	// 注册 api 分组路由
+	apiGroup := router.Group("")
+	route.SetApiGroupRoutes(apiGroup)
+
+	return router
+}
+
+func RunServer() {
+	r := setupRouter()
+
+	srv := &http.Server{
+		Addr:         ":" + config.Get("app.port"),
+		ReadTimeout:  60 * time.Second,
+		WriteTimeout: 60 * time.Second,
+		Handler:      r,
+	}
+	fmt.Println("Starting server at port " + config.Get("app.port") + " ...")
+
+	go func() {
+		if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
+			log.Fatalf("listen: %s\n", err)
+		}
+	}()
+
+	// 等待中断信号以优雅地关闭服务器(设置 5 秒的超时时间)
+	quit := make(chan os.Signal)
+	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
+	<-quit
+	log.Println("Shutdown Server ...")
+
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+	if err := srv.Shutdown(ctx); err != nil {
+		log.Fatal("Server Shutdown:", err)
+	}
+	log.Println("Server exiting")
+}

+ 329 - 0
common/common.go

@@ -0,0 +1,329 @@
+package common
+
+import (
+	"crypto/hmac"
+	"crypto/md5"
+	"crypto/sha256"
+	"designs/config"
+	"designs/global"
+	"encoding/hex"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"math/rand"
+	"net/http"
+	"net/url"
+	"sort"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/gin-gonic/gin"
+	"github.com/golang-jwt/jwt/v5"
+)
+
+// 打印
+func Print(i interface{}) {
+	fmt.Println("---")
+	fmt.Println(i)
+	fmt.Println("---")
+}
+
+// 返回JSON
+func RetJson(code int, msg string, data interface{}, c *gin.Context) {
+	c.JSON(http.StatusOK, gin.H{
+		"code":      code,
+		"msg":       msg,
+		"data":      data,
+		"timestamp": GetTimeUnix(),
+	})
+	c.Abort()
+}
+
+// 获取当前时间戳
+func GetTimeUnix() int64 {
+	return time.Now().Unix()
+}
+
+// MD5 方法
+func MD5(str string) string {
+	s := md5.New()
+	s.Write([]byte(str))
+	return hex.EncodeToString(s.Sum(nil))
+}
+
+// 生成签名
+func CreateSign(params url.Values) string {
+	var key []string
+	var str = ""
+	for k := range params {
+		if k != "sn" {
+			key = append(key, k)
+		}
+	}
+	sort.Strings(key)
+	for i := 0; i < len(key); i++ {
+		if i == 0 {
+			str = fmt.Sprintf("%v=%v", key[i], params.Get(key[i]))
+		} else {
+			str = str + fmt.Sprintf("&%v=%v", key[i], params.Get(key[i]))
+		}
+	}
+	// 自定义签名算法
+	sign := MD5(MD5(str) + MD5(config.Get("app_name")+config.Get("app_secret")))
+	return sign
+}
+
+// 验证签名
+func VerifySign(c *gin.Context) {
+	var method = c.Request.Method
+	var ts int64
+	var sn string
+	var req url.Values
+
+	if method == "GET" {
+		req = c.Request.URL.Query()
+		sn = c.Query("sn")
+		ts, _ = strconv.ParseInt(c.Query("ts"), 10, 64)
+
+	} else if method == "POST" {
+		c.Request.ParseForm()
+		req = c.Request.PostForm
+		sn = c.PostForm("sn")
+		ts, _ = strconv.ParseInt(c.PostForm("ts"), 10, 64)
+	} else {
+		RetJson(500, "Illegal requests", "", c)
+		return
+	}
+
+	exp, _ := strconv.ParseInt(config.Get("api_expire"), 10, 64)
+
+	// 验证过期时间
+	if ts > GetTimeUnix() || GetTimeUnix()-ts >= exp {
+		RetJson(500, "Ts Error", "", c)
+		return
+	}
+
+	// 验证签名
+	if sn == "" || sn != CreateSign(req) {
+		RetJson(500, "Sn Error", "", c)
+		return
+	}
+}
+
+func StructToArr(data interface{}) (string, error) {
+	res, err := json.Marshal(data)
+	if err != nil {
+		return "", err
+	}
+	//将json解析为map
+	var mapdata map[string]interface{}
+	err1 := json.Unmarshal([]byte(res), &mapdata)
+	if err1 != nil {
+		return "", err1
+	}
+	//获取排序map键值
+	keys := make([]string, 0, len(mapdata))
+	for key := range mapdata {
+		keys = append(keys, key)
+	}
+	//排序
+	sort.Strings(keys)
+	fmt.Println("我的数据:", string(res))
+	//排序后直接构建map
+	sortMap := make(map[string]interface{}, len(mapdata))
+	for _, key := range keys {
+		sortMap[key] = mapdata[key]
+	}
+	//序列化
+	sortJosn, err2 := json.Marshal(sortMap)
+	if err2 != nil {
+		return "", err2
+	}
+	return string(sortJosn), nil
+}
+
+/* 检测数据是否篡改 */
+func VerifySignData(data interface{}) bool {
+	var checkSign string
+	var sign string = "test"
+	checkKey := "secret"
+
+	res, err := json.Marshal(data)
+	if err != nil {
+		return false
+	}
+	//将json解析为map
+	var mapdata map[string]interface{}
+	err1 := json.Unmarshal([]byte(res), &mapdata)
+	if err1 != nil {
+		return false
+	}
+	//fmt.Println("解析后的数据:", mapdata)
+
+	val, exist := mapdata[checkKey].(string)
+	if exist {
+		checkSign = val
+	} else {
+		return false
+	}
+	fmt.Println("解析后的数据:", mapdata)
+	//获取排序map键值
+	keys := make([]string, 0, len(mapdata))
+	for key := range mapdata {
+		if key != checkKey {
+			keys = append(keys, key)
+		}
+	}
+	// fmt.Println("解析后的keys数据:", keys)
+	//排序
+	sort.Strings(keys)
+	//排序后直接构建map
+	sortMap := make(map[string]interface{}, len(mapdata))
+	for _, key := range keys {
+		newKey := strings.ToLower(string(key[0])) + key[1:]
+		sortMap[newKey] = mapdata[key]
+	}
+	//序列化数据
+	sortJosn, err2 := json.Marshal(sortMap)
+	if err2 != nil {
+		return false
+	}
+	// fmt.Println("我的排序数据:", string(sortJosn), sign)
+
+	// sum :=  ([]byte("hello world"))
+	// fmt.Printf("加密数据:%x\n", sum)
+	// 密钥
+	key := []byte(config.Get("app.app_check_secret"))
+	// 数据
+	// data := []byte(msg)
+	// 创建一个新的HMAC将SHA-256用作哈希函数
+	h := hmac.New(sha256.New, key)
+	// 写入数据
+	h.Write(sortJosn)
+	// 得到MAC
+	mac := h.Sum(nil)
+	sign = hex.EncodeToString(mac)
+	// 打印MAC
+	fmt.Println("生成的签名:", sign, checkSign)
+
+	if sign != checkSign {
+		global.App.Log.Error("mapdata:", mapdata)
+		global.App.Log.Error("sign:", sign)
+		global.App.Log.Error("checkSign:", checkSign)
+	}
+
+	return sign == checkSign
+}
+
+// 生成token
+func GetDefineToken(openid string) string {
+	timestamp := time.Now().UnixNano() / int64(time.Millisecond)
+	randNum := 10000 + rand.Intn(10000)
+	token := fmt.Sprintf("%s%d%d", openid, timestamp, randNum)
+	return MD5(token)
+}
+
+// 自定义结构体
+type MyCustomClaims struct {
+	OpenId    string `json:"openid"`
+	Gid       string `json:"gid"`
+	Pf        string `json:"pf"`
+	TokenType int    `json:"tokenType"` //0 token 登录  1:刷新token
+	jwt.RegisteredClaims
+}
+
+// 生成jwt token
+func GetJwtToken(openId string, gid string, pf string, tokenType int) (string, error) {
+	mySigningKey := []byte(config.Get("app.JwtSecret"))
+	timeData := time.Second
+	if tokenType == 0 {
+		timeData = time.Duration(config.GetInt("app.api_exp")) * time.Second
+	} else {
+		timeData = 24 * time.Hour
+	}
+	claims := MyCustomClaims{
+		openId,
+		gid,
+		pf,
+		tokenType,
+		jwt.RegisteredClaims{
+			// A usual scenario is to set the expiration time relative to the current time
+			ExpiresAt: jwt.NewNumericDate(time.Now().Add(timeData)),
+			IssuedAt:  jwt.NewNumericDate(time.Now()),
+			NotBefore: jwt.NewNumericDate(time.Now()),
+			Issuer:    "test",
+			Subject:   "somebody",
+			ID:        "1",
+			Audience:  []string{"somebody_else"},
+		},
+	}
+	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
+	ss, err := token.SignedString(mySigningKey)
+	return ss, err
+}
+
+/* 解析jwt */
+func ParseJwtWithClaims(tokenString string) (string, string, string, int) {
+	token, err := jwt.ParseWithClaims(tokenString, &MyCustomClaims{}, func(token *jwt.Token) (interface{}, error) {
+		return []byte(config.Get("app.JwtSecret")), nil
+	})
+	if err != nil {
+		// fmt.Println(err)
+		return "", "", "", 0
+	} else if claims, ok := token.Claims.(*MyCustomClaims); ok {
+		// fmt.Println(claims.OpenId, claims.RegisteredClaims.Issuer)
+		return claims.OpenId, claims.Gid, claims.Pf, claims.TokenType
+	} else {
+		// fmt.Println("unknown claims type, cannot proceed")
+		return "", "", "", 0
+	}
+}
+
+// 刷新Token
+func RefreshToken(oldToken string) (string, error) {
+	token, err := jwt.Parse(oldToken, func(*jwt.Token) (interface{}, error) {
+		return config.Get("app.JwtSecret"), nil
+	})
+	if err != nil {
+		return "", err
+	}
+	if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
+		claims["exp"] = time.Now().Add(time.Duration(config.GetInt("app.api_exp")) * time.Second).Unix() // 更新过期时间
+		return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(config.Get("app.JwtSecret"))
+	}
+	return "", err
+}
+
+// 返回剩余时间
+func GetRemainderTime(timeType int) (time.Duration, error) {
+	var remaining time.Duration
+	err := errors.New("发生错误")
+	//获取当前时间
+	now := time.Now()
+	switch timeType {
+	case 2:
+		//计算本日剩余时间
+		endOfDay := time.Date(now.Year(), now.Month(), now.Day(), 23, 59, 59, 0, now.Location())
+		remaining = endOfDay.Sub(now)
+	case 3:
+		//计算本周剩余时间
+		daysUntiEndOfWeek := 6 - now.Weekday()
+		endOfWeek := time.Date(now.Year(), now.Month(), now.Day()+int(daysUntiEndOfWeek), 23, 59, 59, 0, now.Location())
+		remaining = endOfWeek.Sub(now)
+	case 4:
+		//计算本月剩余时间
+		// endOfMonth := time.Date(now.Year(), now.Month()+1, 1, 0, 0, 0, 0, now.Location()).Add(-time.Second)
+		// remaining = endOfMonth.Sub(now)
+		nextMonth := now.AddDate(0, 1, 0)
+		endOfMonth := time.Date(nextMonth.Year(), nextMonth.Month(), 1, 0, 0, 0, 0, nextMonth.Location()).Add(-time.Second)
+		remaining = endOfMonth.Sub(now)
+	default:
+		return remaining, err
+	}
+
+	// fmt.Printf("Remaning day data:%v\n", remaining)
+	// fmt.Printf("Remaning week data:%v\n", remaining)
+	// fmt.Printf("Remaning month data:%v\n", remaining)
+	return remaining, nil
+}

+ 42 - 0
common/ip.go

@@ -0,0 +1,42 @@
+package common
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/lionsoul2014/ip2region/binding/golang/xdb"
+)
+
+var cBuffData []byte
+
+func InitIpFile() {
+	// 1、从 dbPath 加载整个 xdb 到内存
+	dbPath := "./ip2region.xdb"
+	cBuff, err := xdb.LoadContentFromFile(dbPath)
+	if err != nil {
+		fmt.Printf("failed to load content from `%s`: %s\n", dbPath, err)
+		return
+	}
+	cBuffData = cBuff
+}
+
+/* 通过id地址获取省份 并发使用时候 每个goruiting 需要创建一个独立的search */
+func FindAddressByIp(ip string) (string, string) {
+	// 2、用全局的 cBuff 创建完全基于内存的查询对象。
+	searcher, err := xdb.NewWithBuffer(cBuffData)
+	if err != nil {
+		fmt.Printf("failed to create searcher with content: %s\n", err)
+		return "", ""
+	}
+
+	// do the search
+	// var tStart = time.Now()
+	region, err := searcher.SearchByStr(ip)
+	if err != nil {
+		fmt.Printf("failed to SearchIP(%s): %s\n", ip, err)
+		return "", ""
+	}
+	data := strings.Split(region, "|")
+	// fmt.Printf("{region: %s, took: %s}\n", data, time.Since(tStart))
+	return data[2], data[3]
+}

+ 88 - 0
common/redis.go

@@ -0,0 +1,88 @@
+package common
+
+import (
+	"context"
+	"designs/config"
+	"designs/global"
+	"encoding/json"
+	"fmt"
+	"os"
+	"strconv"
+	"strings"
+
+	"github.com/go-redis/redis/v8"
+	"github.com/sirupsen/logrus"
+)
+
+/* 初始游戏配置 */
+func InitGameCfg() {
+	//判读本地有没有数据
+	res, err := global.App.Redis.Exists(context.Background(), config.Get("app.province_table")).Result()
+	if err != nil {
+		logrus.Error("InitGameCfg err")
+	}
+	if res != 1 {
+		//不存在数据
+		// 指定目录
+		filePath := "./config/json/city.json"
+		bytes, err := os.ReadFile(filePath)
+		if err != nil {
+			fmt.Println("os.ReadFile err=", err)
+		}
+		data := []interface{}{}
+		json.Unmarshal(bytes, &data)
+		// dat := data[0].(map[string]interface{})
+		// fmt.Println(dat["hid"])
+		cityData := make(map[string]interface{})
+		for _, val := range data {
+			res := val.(map[string]interface{})
+			// fmt.Println(res["hid"], res["hname"])
+			cityData[res["hname"].(string)] = res["hid"]
+		}
+		//设置数据
+		global.App.Redis.HMSet(context.Background(), config.Get("app.province_table"), cityData)
+	}
+
+}
+
+/* 通过ip地址获取地址code */
+func FindAddresscodeByIp(ip string) int {
+	pro, _ := FindAddressByIp(ip)
+	code := 999
+	res, err := global.App.Redis.HGetAll(context.Background(), config.Get("app.province_table")).Result()
+	if err != nil {
+		return code
+	}
+	for key, val := range res {
+		code, _ := strconv.Atoi(val)
+
+		if strings.Contains(pro, key) {
+			return code
+		}
+	}
+	return code
+}
+
+/* 通过hid获取地址code */
+func FindAddresscodeByHid(hid int) (int, error) {
+	res, _ := global.App.Redis.HGetAll(context.Background(), config.Get("app.province_table")).Result()
+	for _, val := range res {
+		code, _ := strconv.Atoi(val)
+		if code == hid {
+			return hid, nil
+		}
+	}
+	return 0, redis.Nil
+}
+
+/* 通过hid获取地址 */
+func FindAddressByHid(hid int) string {
+	res, _ := global.App.Redis.HGetAll(context.Background(), config.Get("app.province_table")).Result()
+	for key, val := range res {
+		code, _ := strconv.Atoi(val)
+		if code == hid {
+			return key
+		}
+	}
+	return ""
+}

+ 45 - 0
config/app.go

@@ -0,0 +1,45 @@
+package config
+
+func App() *ConfigNode {
+	return &ConfigNode{
+		"env":     env("APP_ENV", "local"),
+		"port":    env("APP_PORT", "8000"),
+		"appName": env("APP_NAME", "goProject"),
+		"appUrl":  env("APP_URL", "http://localhost"),
+
+		"app_secret":       env("APP_SECRET", "6YJSuc50uJ18zj45"),
+		"app_check_secret": env("APP_CHECK_SECRET", "6YJSuc50uJ18zj45"), //检测数据篡改密钥
+		"api_expiry":       env("API_EXPIRY", "120000"),                 //
+		"max_content":      env("MAX_CONTENT", "50000"),                 //最大请求内容长度
+		"api_exp":          env("API_EXP", "6000"),                      //api 过期时间
+		"api_limit_key":    env("API_LIMIT_KEY", "api_limit_key"),       //api限制key  api_limit_key:gid:openid:apipath
+		"api_limit_count":  env("API_LIMIT_COUNT", "50"),                //每分钟限制次数
+		"pf_wx":            env("PF_WX", "wx"),
+		"pf_tt":            env("PF_TT", "tt"),
+		"pf_web":           env("PF_WEB", "web"),
+		"province_table":   env("PROVINCE_TABLE", "province_table"), //省份配置映射表
+		"gid":              env("GID", "gid:"),                      //游戏配置key  gid:gid
+		"user_table_key":   env("USER_TABLE_KEY", "user:"),          //每个游戏的用户键值     gid:pf:user:openid
+		"user_table_id":    env("USER_TABLE_ID", "user_table_id"),   //每个游戏的用户表计数键值 自增id  gid:pf:user_tabel_id     id对应表   gid:pf:user_tabel_id:userid
+
+		"user_init_id":                env("USER_INIT_ID", "10000"),                                 //用户初始表id
+		"game_data_table_key":         env("GAME_DATA_TABLE_KEY", "gameData:"),                      //每个游戏游戏数据键值   gid:pf:gameData:openid
+		"rank_table_key":              env("RANK_TABLE_KEY", "rank:"),                               //每个游戏排行数据键值   gid:pf:rank:TypeId:排序字段
+		"rank_province_table_key":     env("RANK_PROVINCE_TABLE_KEY", "rank_province:"),             //每个游戏省排行数据键值  (省内) gid:pf:rank_province:hid:TypeId:排序字段   (省汇总)  gid:pf:rank_province:Sum:排序字段
+		"rank_table_extends":          env("RANK_TABLE_EXTENDS", "rank_extends:"),                   //每个游戏排行扩展数据键值  gid:pf:rank_extends:openid:TypeId:排序字段
+		"rank_province_table_extends": env("RANK_PROVINCE_TABLE_EXTENDS", "rank_province_extends:"), //每个游戏省排行扩展数据键值   gid:pf:rank_province_extends:hid:openid:TypeId:排序字段
+		"rank_max_count":              env("RANK_MAX_COUNT", "100"),                                 //最大拉取排行榜数量
+		"JwtSecret":                   env("JwtSecret", "ac11b12e5199457602e42cdab79ca770"),         //替换为你的JWT密钥
+
+		"black_list_table": env("BLACK_LIST_TABLE", "black_list_table:"), //黑名单表
+		"option_key":       env("OPTION_KEY", "option_key:"),             //用户配置表    option_key:openid
+
+		"admin_user":   env("ADMIN_USER", "chunhao"),    //默认管理员
+		"admin_secret": env("ADMIN_SECRET", "123456"),   //默认管理密码
+		"user_total":   env("USER_TOTAL", "user_total"), //所有用户的集合
+	}
+}
+
+func IsProduction() bool {
+	return Get("app.env") == "production"
+}

+ 160 - 0
config/common.go

@@ -0,0 +1,160 @@
+package config
+
+import (
+	"designs/app/common/log"
+	"fmt"
+	"strconv"
+	"strings"
+
+	"github.com/pkg/errors"
+)
+
+type ConfigRootFunc func() *ConfigNode
+
+type ConfigEnv [2]string
+type ConfigNode map[string]interface{}
+type ConfigRoot map[string]ConfigRootFunc
+
+var EnvMap map[string]string
+var Root = make(map[string]*ConfigNode)
+
+func env(envField, defaultValue string) string {
+	if envValue, ok := EnvMap[envField]; ok {
+		return envValue
+	} else {
+		return defaultValue
+	}
+}
+
+// 初始化配置信息
+func InitConfig(envMap map[string]string, root *ConfigRoot) error {
+	EnvMap = envMap
+	for k, v := range *root {
+		Root[k] = v()
+	}
+
+	return nil
+}
+
+// 打印配置信息
+func PrintConfig() {
+	for k, v := range Root {
+		printConfigNode(k, v)
+	}
+}
+
+func printConfigNode(prefix string, node *ConfigNode) {
+	for k, v := range *node {
+		if strValue, ok := v.(string); ok {
+			fmt.Printf("%-50s%s\n", prefix+"."+k, strValue)
+		} else if _, ok := v.(*ConfigNode); ok {
+			printConfigNode(prefix+"."+k, v.(*ConfigNode))
+		} else {
+			fmt.Printf("%-50s%+v\n", prefix+"."+k, v)
+		}
+	}
+}
+
+// 获取配置信息
+func Get(k string) string {
+	kList := strings.Split(k, ".")
+	if len(kList) < 2 {
+		log.Errorf("get error config: %s", k)
+		return ""
+	}
+
+	//获取第一个节点[1,1]
+	node, ok := Root[kList[0]]
+	if !ok {
+		log.Errorf("get not exist config: %s", k)
+		return ""
+	}
+
+	//循环[2, n-1]
+	for _, v := range kList[1 : len(kList)-1] {
+		if n, ok := (*node)[v]; ok {
+			if nn, ok := n.(*ConfigNode); ok {
+				node = nn
+			} else {
+				log.Errorf("get not exist config: %s", k)
+				return ""
+			}
+		} else {
+			log.Errorf("get not exist config: %s", k)
+			return ""
+		}
+	}
+
+	//最后一个节点[n,n]
+	if n, ok := (*node)[kList[len(kList)-1]]; ok {
+		if sv, ok := n.(string); ok {
+			return sv
+		} else {
+			log.Errorf("get not exist config: %s", k)
+			return ""
+		}
+	}
+
+	log.Errorf("get not exist config: %s", k)
+	return ""
+}
+
+func GetNode(k string) (*ConfigNode, error) {
+	kList := strings.Split(k, ".")
+
+	//获取第一个节点[1,1]
+	node, ok := Root[kList[0]]
+	if !ok {
+		log.Errorf("get not exist config: %s", k)
+		return nil, errors.New("get not exist config")
+	}
+
+	//循环[2, n-1]
+	if len(kList) > 2 {
+		for _, v := range kList[1 : len(kList)-1] {
+			if n, ok := (*node)[v]; ok {
+				if nn, ok := n.(*ConfigNode); ok {
+					node = nn
+				} else {
+					log.Errorf("get not exist config: %s", k)
+					return nil, errors.New("get not exist config")
+				}
+			} else {
+				log.Errorf("get not exist config: %s", k)
+				return nil, errors.New("get not exist config")
+			}
+		}
+	}
+
+	return node, nil
+}
+
+// 获取配置信息,并转成int
+func GetInt(k string) int {
+	v := Get(k)
+	if v == "" {
+		return 0
+	}
+
+	i, err := strconv.Atoi(v)
+	if err != nil {
+		log.Warnf("can not convert %s, value %s to int", k, v)
+		return 0
+	}
+
+	return i
+}
+
+func GetInt64(k string) int64 {
+	return int64(GetInt(k))
+}
+
+// 获取配置信息,并转成bool
+func GetBool(k string) bool {
+	v := Get(k)
+	if v == "" {
+		return false
+	}
+
+	return v == "true"
+}

+ 16 - 0
config/config.go

@@ -0,0 +1,16 @@
+package config
+
+const (
+	Wxcode2sessionURL = "https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code" //微信openid获取地址
+	TTcode2sessionURL = "https://minigame.zijieapi.com/mgplatform/api/apps/jscode2session?appid=%s&secret=%s&code=%s"              //抖音openid获取地址
+)
+
+var RootConfig = ConfigRoot{
+	"app":      App,
+	"log":      Log,
+	"jwt":      Jwt,
+	"redis":    Redis,
+	"temp":     Temp,
+	"download": Download,
+	"database": Database,
+}

+ 17 - 0
config/database.go

@@ -0,0 +1,17 @@
+package config
+
+func Database() *ConfigNode {
+	return &ConfigNode{
+		"driver":              "mysql",
+		"host":                env("MYSQL_HOST", "localhost"),
+		"port":                env("MYSQL_PORT", "3306"),
+		"database":            env("MYSQL_DATABASE", "chunhao"),
+		"userName":            env("MYSQL_USERNAME", "root"),
+		"password":            env("MYSQL_PASSWORD", "root"),
+		"charset":             "utf8mb4",
+		"maxIdleConns":        "10",
+		"maxOpenConns":        "50",
+		"logMode":             env("MYSQL_LOG_MODE", "warn"),
+		"enableFileLogWriter": env("MYSQL_ENABLE_LOG_WRITE", "true"),
+	}
+}

+ 9 - 0
config/download.go

@@ -0,0 +1,9 @@
+package config
+
+func Download() *ConfigNode {
+	return &ConfigNode{
+		"dir":            env("DOWNLOAD_DIR", ""),
+		"sendFilePrefix": env("DOWNLOAD_SEND_FILE_PREFIX", "/fs/file"),
+		"mode":           env("DOWNLOAD_MODE", "sendfile"),
+	}
+}

BIN
config/excel/city.xlsx


BIN
config/excel/game.xlsx


+ 142 - 0
config/json/city.json

@@ -0,0 +1,142 @@
+[
+  {
+    "hid": 100,
+    "hname": "北京"
+  },
+  {
+    "hid": 110,
+    "hname": "天津"
+  },
+  {
+    "hid": 120,
+    "hname": "河北"
+  },
+  {
+    "hid": 130,
+    "hname": "山西"
+  },
+  {
+    "hid": 140,
+    "hname": "内蒙古"
+  },
+  {
+    "hid": 150,
+    "hname": "辽宁"
+  },
+  {
+    "hid": 160,
+    "hname": "吉林"
+  },
+  {
+    "hid": 170,
+    "hname": "黑龙江"
+  },
+  {
+    "hid": 180,
+    "hname": "上海"
+  },
+  {
+    "hid": 190,
+    "hname": "江苏"
+  },
+  {
+    "hid": 200,
+    "hname": "浙江"
+  },
+  {
+    "hid": 210,
+    "hname": "安徽"
+  },
+  {
+    "hid": 220,
+    "hname": "福建"
+  },
+  {
+    "hid": 230,
+    "hname": "江西"
+  },
+  {
+    "hid": 240,
+    "hname": "山东"
+  },
+  {
+    "hid": 250,
+    "hname": "河南"
+  },
+  {
+    "hid": 260,
+    "hname": "湖北"
+  },
+  {
+    "hid": 270,
+    "hname": "湖南"
+  },
+  {
+    "hid": 280,
+    "hname": "广东"
+  },
+  {
+    "hid": 290,
+    "hname": "广西"
+  },
+  {
+    "hid": 300,
+    "hname": "海南"
+  },
+  {
+    "hid": 310,
+    "hname": "重庆"
+  },
+  {
+    "hid": 320,
+    "hname": "四川"
+  },
+  {
+    "hid": 330,
+    "hname": "贵州"
+  },
+  {
+    "hid": 340,
+    "hname": "云南"
+  },
+  {
+    "hid": 350,
+    "hname": "西藏"
+  },
+  {
+    "hid": 360,
+    "hname": "陕西"
+  },
+  {
+    "hid": 370,
+    "hname": "甘肃"
+  },
+  {
+    "hid": 380,
+    "hname": "青海"
+  },
+  {
+    "hid": 390,
+    "hname": "宁夏"
+  },
+  {
+    "hid": 400,
+    "hname": "新疆"
+  },
+  {
+    "hid": 410,
+    "hname": "台湾"
+  },
+  {
+    "hid": 420,
+    "hname": "香港"
+  },
+  {
+    "hid": 430,
+    "hname": "澳门"
+  },
+  {
+    "hid": 999,
+    "hname": "其他"
+  }
+]

+ 10 - 0
config/json/game.json

@@ -0,0 +1,10 @@
+[
+  {
+    "gid": 1001,
+    "gameName": "测试游戏",
+    "wxAppid": "wxf332a8f693181efb",
+    "wxSecret": "610a1767db04d47b26073859b29d655a",
+    "ttAppid": "wxf332a8f693181efb",
+    "ttSecret": "610a1767db04d47b26073859b29d655a"
+  }
+]

+ 9 - 0
config/jwt.go

@@ -0,0 +1,9 @@
+package config
+
+func Jwt() *ConfigNode {
+	return &ConfigNode{
+		"secret":     "ac11b12e5199457602e42cdab79ca770",
+		"jwtTtl":     "43200", // token 有效期(秒)
+		"refreshTtl": "43200", // token 刷新期(秒)
+	}
+}

+ 11 - 0
config/log.go

@@ -0,0 +1,11 @@
+package config
+
+func Log() *ConfigNode {
+	return &ConfigNode{
+		"level":    "info",
+		"rootDir":  "logs",
+		"filename": "gin-server-%Y-%m-%d.log",
+		"format":   "",
+		"showLine": "true",
+	}
+}

+ 10 - 0
config/redis.go

@@ -0,0 +1,10 @@
+package config
+
+func Redis() *ConfigNode {
+	return &ConfigNode{
+		"host":     env("REDIS_HOST", "localhost"),
+		"port":     env("REDIS_PORT", "6379"),
+		"db":       "1",
+		"password": env("REDIS_PASSWORD", ""),
+	}
+}

+ 10 - 0
config/temp.go

@@ -0,0 +1,10 @@
+package config
+
+import "designs/global"
+
+func Temp() *ConfigNode {
+	return &ConfigNode{
+		"uploadTempDir": global.App.PwdPath + env("UPLOAD_TEMP_DIR", "/storage/tmp/"),
+		"uploadDir":     global.App.PwdPath + env("UPLOAD_DIR", "/storage/"),
+	}
+}

+ 200 - 0
controller/v1/user.go

@@ -0,0 +1,200 @@
+package v1
+
+import (
+	"context"
+	"designs/app/common/request"
+	"designs/app/common/response"
+	"designs/config"
+	"designs/global"
+	"designs/model"
+	"github.com/gin-gonic/gin"
+	"github.com/pkg/errors"
+	"gorm.io/gorm"
+	"strconv"
+	"strings"
+	"time"
+)
+
+var ActionModes = map[string]string{
+	"login":  "login",
+	"alive":  "alive",
+	"close":  "close",
+	"seeAds": "seeAds",
+}
+
+func ReceiveGameMsg(c *gin.Context) {
+	form := request.Check(c, &struct {
+		//Gid    string `form:"gid" json:"gid" binding:"required"`
+		//Pf     string `form:"pf" json:"pf" binding:"required"`
+		UserId    int    `form:"userId" json:"userId" binding:"required"`
+		Action    string `form:"action" json:"action" binding:"required"`
+		Timestamp int64  `form:"timestamp" json:"timestamp" binding:"required"`
+
+		AdsId    string `form:"adsId" json:"adsId" binding:""`
+		AdsType  string `form:"adsType" json:"adsType" binding:""`
+		AdsScene string `form:"adsScene" json:"adsScene" binding:""`
+		AdsState int    `form:"adsState" json:"adsState" binding:""`
+	}{})
+
+	gid := c.GetString("gid")
+	pf := c.GetString("pf")
+
+	now := time.UnixMilli(form.Timestamp)
+	var err error
+	if form.Action == "login" {
+		//登录
+		err = login(gid, pf, form.UserId, now)
+	} else if form.Action == "online" {
+		//活跃
+		err = online(gid, pf, form.UserId, now)
+	} else if form.Action == "offline" {
+		//断开
+		err = offline(gid, pf, form.UserId, now)
+	} else if form.Action == "seeAds" {
+		//观看广告
+		err = seeAds(gid, pf, form.UserId, now, form.AdsId, form.AdsType, form.AdsScene, form.AdsState)
+	}
+
+	if err != nil {
+		response.Fail(c, 1003, err.Error())
+		return
+	}
+
+	response.Success(c, gin.H{})
+}
+
+func login(gid string, pf string, userId int, now time.Time) error {
+	var user model.User
+	err := global.App.DB.Table("user").Where("id", gid).Where("pf", pf).Where("userId", userId).Find(&user).Error
+	if errors.Is(err, gorm.ErrRecordNotFound) {
+		//没有用户,需要新增
+		user.UserId = userId
+		user.Gid = gid
+		user.Pf = pf
+		user.CreatedAt = now
+		err = global.App.DB.Table("user").Create(&user).Error
+		if err != nil {
+			global.App.Log.Error("创建用户失败", err.Error(), user)
+			return err
+		}
+	}
+
+	userLogin := model.UserLogin{
+		Gid:       gid,
+		Pf:        pf,
+		UserId:    userId,
+		LoginTime: now,
+	}
+	err = global.App.DB.Table("user_login").Create(&userLogin).Error
+	if err != nil {
+		global.App.Log.Error("存储用户登录信息失败", err.Error(), user)
+		return err
+	}
+
+	userOnline := model.UserOnline{
+		Gid:     gid,
+		Pf:      pf,
+		UserId:  userId,
+		LogTime: now,
+		Date:    now.Format("20060102"),
+		Type:    1,
+	}
+	err = global.App.DB.Table("user_online").Create(&userOnline).Error
+	if err != nil {
+		global.App.Log.Error("存储用户活跃信息失败", err.Error(), userOnline)
+		return err
+	}
+
+	return nil
+}
+
+func online(gid string, pf string, userId int, now time.Time) error {
+
+	userOnline := model.UserOnline{
+		Gid:     gid,
+		Pf:      pf,
+		UserId:  userId,
+		LogTime: now,
+		Date:    now.Format("20060102"),
+		Type:    1,
+	}
+	err := global.App.DB.Table("user_online").Create(&userOnline).Error
+	if err != nil {
+		global.App.Log.Error("存储用户活跃信息失败", err.Error(), userOnline)
+		return err
+	}
+	return nil
+}
+
+func offline(gid string, pf string, userId int, now time.Time) error {
+
+	userOnline := model.UserOnline{
+		Gid:     gid,
+		Pf:      pf,
+		UserId:  userId,
+		LogTime: now,
+		Date:    now.Format("20060102"),
+		Type:    2,
+	}
+	err := global.App.DB.Table("user_online").Create(&userOnline).Error
+	if err != nil {
+		global.App.Log.Error("存储用户活跃信息失败", err.Error(), userOnline)
+		return err
+	}
+
+	return nil
+}
+
+func seeAds(gid string, pf string, userId int, now time.Time, AdsId string, AdsType string, AdsScene string, AdsState int) error {
+	if AdsId == "" || AdsType == "" || AdsScene == "" {
+		return errors.New("参数缺失")
+	}
+
+	userOnline := model.UserSeeAds{
+		Gid:       gid,
+		Pf:        pf,
+		UserId:    userId,
+		CreatedAt: now,
+		Date:      now.Format("20060102"),
+		AdsId:     AdsId,
+		AdsType:   AdsType,
+		AdsState:  AdsState,
+		AdsScene:  AdsScene,
+	}
+	err := global.App.DB.Table("user_see_ads").Create(&userOnline).Error
+	if err != nil {
+		global.App.Log.Error("存储用户活跃信息失败", err.Error(), userOnline)
+		return err
+	}
+
+	return nil
+}
+
+func InitUser(c *gin.Context) {
+	gidKey := config.Get("app.gid") + "*"
+	keys, _ := global.App.Redis.Keys(context.Background(), gidKey).Result()
+	for _, key := range keys {
+		gid := strings.Split(key, ":")[1]
+		userKeyWeb := gid + ":" + "web" + ":" + config.Get("app.user_table_key") + "*"
+		//userKeyTt := gid + ":" + "tt" + ":" + config.Get("app.user_table_key")
+		//userKeyWx := gid + ":" + "wx" + ":" + config.Get("app.user_table_key")
+
+		webKey, _ := global.App.Redis.Keys(context.Background(), userKeyWeb).Result()
+		for _, v := range webKey {
+			res, err2 := global.App.Redis.HGetAll(context.Background(), v).Result()
+			if err2 != nil {
+				continue
+			}
+			userId, _ := strconv.Atoi(res["userId"])
+			global.App.DB.Table("user").Create(&model.User{
+				Gid:       res["gid"],
+				Pf:        res["pf"],
+				UserId:    userId,
+				CreatedAt: time.Now(),
+			})
+
+		}
+	}
+
+	response.Success(c, gin.H{})
+}

+ 64 - 0
global/app.go

@@ -0,0 +1,64 @@
+package global
+
+import (
+	"designs/utils"
+	"github.com/go-redis/redis/v8"
+	"go.uber.org/zap"
+	"io"
+)
+
+type InitConfig struct {
+	//InitDBFunc    func() *gorm.DB
+	InitRedisFunc func() *redis.Client
+}
+
+var Init = new(InitConfig)
+
+type Application struct {
+	//配置
+
+	//数据库
+	DB *utils.WtDB
+
+	Redis *redis.Client
+	//Cron      *cron.Cron
+	//Machinery *machinery.Server
+
+	//日志
+	Log       *zap.SugaredLogger
+	LogWriter io.Writer
+
+	PwdPath string
+}
+
+var (
+	// DB     *utils.WtDB
+	// Redis  *redis.Client
+	Log *zap.SugaredLogger
+)
+
+var App = new(Application)
+
+func InitFacade() {
+	// DB = App.DB
+	// Redis = App.Redis
+	Log = App.Log
+}
+
+//func DB() *utils.WtDB {
+//	if App.DB != nil {
+//		return App.DB
+//	}
+//
+//	App.DB = &utils.WtDB{DB: Init.InitDBFunc()}
+//	return App.DB
+//}
+
+func Redis() *redis.Client {
+	if App.Redis != nil {
+		return App.Redis
+	}
+
+	App.Redis = Init.InitRedisFunc()
+	return App.Redis
+}

+ 18 - 0
global/error.go

@@ -0,0 +1,18 @@
+package global
+
+type CustomError struct {
+	ErrorCode int
+	ErrorMsg  string
+}
+
+type CustomErrors struct {
+	BusinessError CustomError
+	ValidateError CustomError
+	TokenError    CustomError
+}
+
+var Errors = CustomErrors{
+	BusinessError: CustomError{400, "业务错误"},
+	ValidateError: CustomError{422, "请求参数错误"},
+	TokenError:    CustomError{401, "登录授权失效"},
+}

+ 68 - 0
global/lock.go

@@ -0,0 +1,68 @@
+package global
+
+import (
+	"context"
+	"designs/utils"
+	"github.com/go-redis/redis/v8"
+	"time"
+)
+
+type Interface interface {
+	Get() bool
+	Block(seconds int64) bool
+	Release() bool
+	ForceRelease()
+}
+
+type lock struct {
+	context context.Context
+	name    string
+	owner   string
+	seconds int64
+}
+
+const releaseLockLuaScript = `
+if redis.call("get",KEYS[1]) == ARGV[1] then
+    return redis.call("del",KEYS[1])
+else
+    return 0
+end
+`
+
+func Lock(name string, seconds int64) Interface {
+	return &lock{
+		context.Background(),
+		name,
+		utils.RandString(16),
+		seconds,
+	}
+}
+
+func (l *lock) Get() bool {
+	return App.Redis.SetNX(l.context, l.name, l.owner, time.Duration(l.seconds)*time.Second).Val()
+}
+
+func (l *lock) Block(seconds int64) bool {
+	timer := time.After(time.Duration(seconds) * time.Second)
+	for {
+		select {
+		case <-timer:
+			return false
+		default:
+			if l.Get() {
+				return true
+			}
+			time.Sleep(time.Duration(1) * time.Second)
+		}
+	}
+}
+
+func (l *lock) Release() bool {
+	luaScript := redis.NewScript(releaseLockLuaScript)
+	result := luaScript.Run(l.context, App.Redis, []string{l.name}, l.owner).Val().(int64)
+	return result != 0
+}
+
+func (l *lock) ForceRelease() {
+	App.Redis.Del(l.context, l.name).Val()
+}

+ 55 - 0
go.mod

@@ -0,0 +1,55 @@
+module designs
+
+go 1.22.5
+
+require (
+	github.com/gin-gonic/gin v1.10.0
+	github.com/go-ini/ini v1.67.0
+	github.com/go-playground/validator/v10 v10.22.0
+	github.com/go-redis/redis/v8 v8.11.5
+	github.com/goccy/go-json v0.10.3
+	github.com/golang-jwt/jwt/v5 v5.2.1
+	github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible
+	github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20240510055607-89e20ab7b6c6
+	github.com/pkg/errors v0.9.1
+	github.com/sirupsen/logrus v1.9.3
+	go.uber.org/zap v1.27.0
+	golang.org/x/crypto v0.25.0
+	gorm.io/driver/mysql v1.5.7
+	gorm.io/gorm v1.25.11
+)
+
+require (
+	filippo.io/edwards25519 v1.1.0 // indirect
+	github.com/bytedance/sonic v1.11.9 // indirect
+	github.com/bytedance/sonic/loader v0.1.1 // indirect
+	github.com/cespare/xxhash/v2 v2.1.2 // indirect
+	github.com/cloudwego/base64x v0.1.4 // indirect
+	github.com/cloudwego/iasm v0.2.0 // indirect
+	github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
+	github.com/gabriel-vasile/mimetype v1.4.4 // indirect
+	github.com/gin-contrib/sse v0.1.0 // indirect
+	github.com/go-playground/locales v0.14.1 // indirect
+	github.com/go-playground/universal-translator v0.18.1 // indirect
+	github.com/go-sql-driver/mysql v1.8.1 // indirect
+	github.com/jinzhu/inflection v1.0.0 // indirect
+	github.com/jinzhu/now v1.1.5 // indirect
+	github.com/jonboulle/clockwork v0.4.0 // indirect
+	github.com/json-iterator/go v1.1.12 // indirect
+	github.com/klauspost/cpuid/v2 v2.2.8 // indirect
+	github.com/leodido/go-urn v1.4.0 // indirect
+	github.com/lestrrat-go/strftime v1.0.6 // indirect
+	github.com/mattn/go-isatty v0.0.20 // indirect
+	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+	github.com/modern-go/reflect2 v1.0.2 // indirect
+	github.com/pelletier/go-toml/v2 v2.2.2 // indirect
+	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
+	github.com/ugorji/go/codec v1.2.12 // indirect
+	go.uber.org/multierr v1.10.0 // indirect
+	golang.org/x/arch v0.8.0 // indirect
+	golang.org/x/net v0.27.0 // indirect
+	golang.org/x/sys v0.22.0 // indirect
+	golang.org/x/text v0.17.0 // indirect
+	google.golang.org/protobuf v1.34.2 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
+)

+ 146 - 0
go.sum

@@ -0,0 +1,146 @@
+filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
+filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
+github.com/bytedance/sonic v1.11.9 h1:LFHENlIY/SLzDWverzdOvgMztTxcfcF+cqNsz9pK5zg=
+github.com/bytedance/sonic v1.11.9/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
+github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
+github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
+github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
+github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
+github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
+github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
+github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
+github.com/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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
+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/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I=
+github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s=
+github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
+github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
+github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
+github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
+github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
+github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
+github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
+github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
+github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
+github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
+github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
+github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao=
+github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
+github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
+github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
+github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
+github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
+github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
+github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
+github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
+github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
+github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
+github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
+github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
+github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
+github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4=
+github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
+github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
+github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
+github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
+github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
+github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8=
+github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is=
+github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible h1:Y6sqxHMyB1D2YSzWkLibYKgg+SwmyFU9dF2hn6MdTj4=
+github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible/go.mod h1:ZQnN8lSECaebrkQytbHj4xNgtg8CR7RYXnPok8e0EHA=
+github.com/lestrrat-go/strftime v1.0.6 h1:CFGsDEt1pOpFNU+TJB0nhz9jl+K0hZSLE205AhTIGQQ=
+github.com/lestrrat-go/strftime v1.0.6/go.mod h1:f7jQKgV5nnJpYgdEasS+/y7EsTb8ykN2z68n3TtcTaw=
+github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20240510055607-89e20ab7b6c6 h1:YeIGErDiB/fhmNsJy0cfjoT8XnRNT9hb19xZ4MvWQDU=
+github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20240510055607-89e20ab7b6c6/go.mod h1:C5LA5UO2ZXJrLaPLYtE1wUJMiyd/nwWaCO5cw/2pSHs=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/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/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
+github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
+github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
+github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
+github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
+github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
+github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
+github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
+github.com/pkg/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/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
+github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
+github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
+github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
+go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
+go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
+go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
+go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
+go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
+go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
+golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
+golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
+golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
+golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
+golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
+golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
+golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
+golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
+golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
+google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+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.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
+gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
+gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
+gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
+gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
+nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
+rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

+ 71 - 0
main.go

@@ -0,0 +1,71 @@
+package main
+
+import (
+	"designs/bootstrap"
+	"designs/global"
+	"designs/utils"
+	"fmt"
+	"github.com/gin-gonic/gin"
+	"net/http"
+	"path/filepath"
+)
+
+func init() {
+	fmt.Printf("初始化")
+}
+
+func main() {
+	//创建一个服务
+	gin.SetMode(gin.ReleaseMode)
+
+	ginServer := gin.Default()
+
+	// 初始化配置
+	bootstrap.InitializeConfig(filepath.Join(global.App.PwdPath, ".env"))
+
+	//config.PrintConfig()
+
+	// 初始化日志
+	global.App.Log, global.App.LogWriter = bootstrap.InitializeLog()
+
+	//跨域
+	ginServer.Use(Cors())
+
+	// 初始化Redis
+	global.Init.InitRedisFunc = bootstrap.InitializeRedis
+	global.App.Redis = bootstrap.InitializeRedis()
+
+	// 初始化facade
+	global.InitFacade()
+	//服务器端口
+	//ginServer.Run(":" + config.Get("app.port")) /*默认是8080*/
+
+	// 初始化数据库
+	global.App.DB = &utils.WtDB{DB: bootstrap.InitializeDB()}
+	// 程序关闭前,释放数据库连接
+	defer func() {
+		if global.App.DB != nil {
+			db, _ := global.App.DB.DB.DB()
+			db.Close()
+		}
+	}()
+
+	bootstrap.RunServer()
+	// ginServer.RunTLS(":443", "your_certificate.crt", "your_private_key.key")
+}
+
+/* 跨域 */
+func Cors() gin.HandlerFunc {
+	return func(context *gin.Context) {
+		method := context.Request.Method
+		context.Header("Access-Control-Allow-Origin", "*")
+		context.Header("Access-Control-Allow-Headers", "Content-Type,AccessToken,X-CSRF-Token, Authorization, Token, x-token")
+		context.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PATCH, PUT")
+		context.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type")
+		context.Header("Access-Control-Allow-Credentials", "true")
+
+		if method == "OPTIONS" {
+			context.AbortWithStatus(http.StatusNoContent)
+		}
+	}
+}

+ 174 - 0
middleware/auth.go

@@ -0,0 +1,174 @@
+package middleware
+
+import (
+	"context"
+	"designs/common"
+	"designs/config"
+	"designs/global"
+	"fmt"
+	"net/http"
+	"time"
+
+	"github.com/gin-gonic/gin"
+)
+
+/* 包体大小 */
+func LimitRequestBodySize(maxSize int64) gin.HandlerFunc {
+	return func(c *gin.Context) {
+		c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxSize)
+		if err := c.Request.ParseForm(); err != nil {
+			c.JSON(http.StatusRequestEntityTooLarge, gin.H{"error": "Request body too large"})
+			c.Abort()
+			return
+		}
+		c.Next()
+	}
+}
+
+/* token中间件 */
+func TokenAuthMiddleware() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		// c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 1)
+		// fmt.Println("fdagadgadgadgagag")
+		// if err := c.Request.ParseForm(); err != nil {
+		// 	fmt.Println("wwwwwwwwwwwwwwwwww")
+		// 	c.JSON(http.StatusRequestEntityTooLarge, gin.H{"error": "Request body too large"})
+		// 	c.Abort()
+		// 	return
+		// }
+		contentLength := c.Request.ContentLength
+		if contentLength > config.GetInt64("app.max_content") {
+			// 输出请求体的大小
+			// fmt.Printf("Request body size: %d bytes\n", contentLength)
+			common.RetJson(1003, "ruquest too max", "", c)
+			c.Abort()
+			return
+		}
+		token := c.GetHeader("Authorization")
+		//校验token
+		if token == "" {
+			common.RetJson(-2, "Unauthorized", "", c)
+			c.Abort()
+			return
+		}
+		ok, openid, gid, pf := isValidToken(token)
+		if !ok {
+			common.RetJson(-1, "authorized invalid!", "", c)
+			c.Abort()
+			return
+		}
+
+		//校验数据合法性(用户信息是否有效)
+		userKey := gid + ":" + pf + ":" + config.Get("app.user_table_key") + openid
+		userData, err := global.App.Redis.HGetAll(context.Background(), userKey).Result()
+		if err != nil {
+			common.RetJson(-1, "authorized invalid,redis cant find!", "", c)
+			c.Abort()
+			return
+		}
+		if len(userData) == 0 {
+			common.RetJson(-1, "用户信息不在数据库中", "", c)
+			c.Abort()
+			return
+		}
+
+		////校验请求次数
+		//apiPath := c.FullPath()
+		//key := fmt.Sprintf("%s:%s:%s:%s:%s", gid, config.Get("app.api_limit_key"), pf, openid, apiPath)
+		//count, err := global.App.Redis.Incr(context.Background(), key).Result()
+		//if err != nil {
+		//	common.RetJson(1001, "server error!", "", c)
+		//	c.Abort()
+		//	return
+		//}
+		//if count == 1 {
+		//	global.App.Redis.Expire(context.Background(), key, time.Minute).Result()
+		//}
+		//if count > config.GetInt64("app.api_limit_count") {
+		//	common.RetJson(1002, "too many requests!", "", c)
+		//	c.Abort()
+		//	return
+		//}
+
+		//设置上下文数据
+		c.Set("openid", openid)
+		c.Set("gid", gid)
+		c.Set("pf", pf)
+		//如果校验通过
+
+	}
+}
+
+/* token是否有效 */
+func isValidToken(token string) (bool, string, string, string) {
+	openid, gid, pf, tokenType := common.ParseJwtWithClaims(token)
+
+	//fmt.Printf("openid:%v,gid:%v,pf:%v,tokenType:%v", openid, gid, pf, tokenType)
+
+	if openid == "" {
+		return false, openid, gid, pf
+	} else {
+		//登录tonken 类型
+		if tokenType == 0 {
+			return true, openid, gid, pf
+		}
+		return false, openid, gid, pf
+	}
+}
+
+/* 刷新token中间件 */
+func RefreshTokenAuthMiddleware() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		token := c.GetHeader("Authorization")
+		//校验token
+		if token == "" {
+			common.RetJson(-2, "Unauthorized", "", c)
+			c.Abort()
+			return
+		}
+		ok, openid, gid, pf := isValidRefreshToken(token)
+		if !ok {
+			common.RetJson(-1, "authorized invalid!", "", c)
+			c.Abort()
+			return
+		}
+		//校验请求次数
+		apiPath := c.FullPath()
+		key := fmt.Sprintf("%s:%s:%s:%s:%s", gid, config.Get("app.api_limit_key"), pf, openid, apiPath)
+		count, err := global.App.Redis.Incr(context.Background(), key).Result()
+		if err != nil {
+			common.RetJson(1001, "server error!", "", c)
+			c.Abort()
+			return
+		}
+		if count == 1 {
+			global.App.Redis.Expire(context.Background(), key, time.Minute).Result()
+		}
+		if count > config.GetInt64("app.api_limit_count") {
+			common.RetJson(1002, "too many requests!", "", c)
+			c.Abort()
+			return
+		}
+		//设置上下文数据
+		c.Set("openid", openid)
+		c.Set("gid", gid)
+		c.Set("pf", pf)
+		//如果校验通过
+		c.Next()
+	}
+}
+
+/* token是否有效 */
+func isValidRefreshToken(token string) (bool, string, string, string) {
+	openid, gid, pf, tokenType := common.ParseJwtWithClaims(token)
+	//fmt.Printf("openid:%v,gid:%v", openid, gid)
+	if openid == "" {
+		return false, openid, gid, pf
+	} else {
+		//登录刷新tonken 类型
+		if tokenType == 1 {
+			return true, openid, gid, pf
+		}
+		return false, openid, gid, pf
+	}
+}

+ 60 - 0
model/user.go

@@ -0,0 +1,60 @@
+package model
+
+import "time"
+
+/* code 结构体 */
+type CodeData struct {
+	Code   string `form:"code" binding:"required"`
+	Gid    string `form:"gid" binding:"required"`
+	Pf     string `form:"pf" binding:"required"`
+	Secret string `form:"secret"`
+}
+
+type Users struct {
+	ID     int    `json:"id" gorm:"not null;"`
+	OpenId string `json:"openId" gorm:"not null;column:openId;"`
+	Pf     string `json:"pf" gorm:"not null;"`
+	UserId int    `json:"userId" gorm:"not null;column:userId;"`
+
+	CreatedAt time.Time `json:"createdAt" gorm:"column:createdAt;"`
+	UpdatedAt time.Time `json:"updatedAt" gorm:"column:updatedAt;"`
+}
+
+type User struct {
+	ID        int       `json:"id" gorm:"not null;"`
+	Pf        string    `json:"pf" gorm:"not null;"`
+	Gid       string    `json:"gid" gorm:"not null;"`
+	UserId    int       `json:"userId" gorm:"not null;column:userId;"`
+	CreatedAt time.Time `json:"createdAt" gorm:"column:createdAt;"`
+}
+
+type UserLogin struct {
+	ID        int       `json:"id" gorm:"not null;"`
+	Pf        string    `json:"pf" gorm:"not null;"`
+	Gid       string    `json:"gid" gorm:"not null;"`
+	UserId    int       `json:"userId" gorm:"not null;column:userId;"`
+	LoginTime time.Time `json:"loginTime" gorm:"column:loginTime;"`
+}
+
+type UserOnline struct {
+	ID      int       `json:"id" gorm:"not null;"`
+	Pf      string    `json:"pf" gorm:"not null;"`
+	Gid     string    `json:"gid" gorm:"not null;"`
+	UserId  int       `json:"userId" gorm:"not null;column:userId;"`
+	Type    int       `json:"type" gorm:"not null;"`
+	Date    string    `json:"date" gorm:"not null;"`
+	LogTime time.Time `json:"logTime" gorm:"column:logTime;"`
+}
+
+type UserSeeAds struct {
+	ID        int       `json:"id" gorm:"not null;"`
+	Pf        string    `json:"pf" gorm:"not null;"`
+	Gid       string    `json:"gid" gorm:"not null;"`
+	UserId    int       `json:"userId" gorm:"not null;column:userId;"`
+	Date      string    `json:"date" gorm:"not null;"`
+	CreatedAt time.Time `json:"createdAt" gorm:"column:createdAt;"`
+	AdsId     string    `json:"adsId" gorm:"not null;column:adsId;"`
+	AdsType   string    `json:"adsType" gorm:"not null;column:adsType;"`
+	AdsScene  string    `json:"adsScene" gorm:"not null;column:adsScene;"`
+	AdsState  int       `json:"adsState" gorm:"not null;column:adsState;"`
+}

+ 123 - 0
response/response.go

@@ -0,0 +1,123 @@
+package response
+
+import (
+	"designs/config"
+	"designs/global"
+	"fmt"
+	"net/http"
+	"strings"
+
+	"github.com/gin-gonic/gin"
+	"github.com/pkg/errors"
+	"go.uber.org/zap"
+)
+
+// 500 程序奔溃
+func ServerError(c *gin.Context, msg string) {
+	errMsg := "服务程序错误"
+	if !config.IsProduction() {
+		stack := strings.Split(strings.Replace(msg, "\t", "--", -1), "\n")
+
+		c.JSON(http.StatusInternalServerError, gin.H{
+			"code":  http.StatusInternalServerError,
+			"msg":   errMsg,
+			"stack": stack,
+		})
+	} else {
+		c.JSON(http.StatusInternalServerError, gin.H{
+			"code": http.StatusInternalServerError,
+			"msg":  errMsg,
+		})
+	}
+
+	c.Abort()
+}
+
+func Success(c *gin.Context, data gin.H) {
+	//for k, v := range data {
+	//
+	//}
+	data["code"] = 0
+
+	//fmt.Println(data)
+
+	c.JSON(http.StatusOK, data)
+}
+
+type causer interface {
+	Format(s fmt.State, verb rune)
+}
+
+func Next(c *gin.Context, resp interface{}) {
+	c.JSON(http.StatusOK, resp)
+}
+
+func Fail(c *gin.Context, errorCode int, e interface{}) {
+	var msg string
+	var stack []string
+	if _, ok := e.(string); ok {
+		msg = e.(string)
+	} else if err, ok := e.(error); ok {
+		if _, ok := e.(causer); ok {
+			msg = strings.Split(fmt.Sprintf("%v", errors.WithStack(err)), ":")[0]
+			if !config.IsProduction() {
+				tmp := fmt.Sprintf("%+v", errors.WithStack(err))
+				stack = strings.Split(strings.Replace(tmp, "\t", "--", -1), "\n")
+			}
+		} else {
+			msg = fmt.Sprintf(err.Error())
+			if !config.IsProduction() {
+				tmp := fmt.Sprintf("%v", zap.StackSkip("", 1))
+				stack = strings.Split(strings.Replace(tmp, "\t", "--", -1), "\n")
+			}
+		}
+	}
+
+	if len(stack) > 0 {
+		c.JSON(http.StatusOK, gin.H{
+			"code":  errorCode,
+			"msg":   msg,
+			"stack": stack,
+		})
+	} else {
+		c.JSON(http.StatusOK, gin.H{
+			"code": errorCode,
+			"msg":  msg,
+		})
+	}
+
+	c.Abort()
+}
+
+func ValidateFail(c *gin.Context, msg string) {
+	Fail(c, global.Errors.ValidateError.ErrorCode, msg)
+}
+
+// 422 参数错误使用
+func ParameterError(c *gin.Context, msg interface{}) {
+	finalMsg := "参数错误"
+	if err, ok := msg.(error); ok {
+		// release 版本屏蔽掉报错
+		if !config.IsProduction() {
+			finalMsg = err.Error()
+		}
+	} else if str, ok := msg.(string); ok {
+		finalMsg = str
+	}
+
+	c.JSON(http.StatusUnprocessableEntity, gin.H{
+		"code": http.StatusUnprocessableEntity,
+		"msg":  finalMsg,
+	})
+	c.Abort()
+}
+
+// 401 权限错误
+func UnauthorizedRequestsFail(c *gin.Context, msg string) {
+	//c.JSON(http.StatusUnauthorized, gin.H{
+	c.JSON(http.StatusOK, gin.H{
+		"code": http.StatusUnauthorized,
+		"msg":  msg,
+	})
+	c.Abort()
+}

+ 20 - 0
route/api.go

@@ -0,0 +1,20 @@
+package route
+
+import (
+	v1 "designs/controller/v1"
+	"designs/middleware"
+	"github.com/gin-gonic/gin"
+)
+
+/* 跨域 */
+
+func SetApiGroupRoutes(router *gin.RouterGroup) {
+
+	router.POST("/admin/ss/initUser", v1.InitUser)
+
+	GroupV1 := router.Group("")
+	GroupV1.Use(middleware.TokenAuthMiddleware()).Use()
+	{
+		GroupV1.POST("/user/receiveGameMsg", v1.ReceiveGameMsg)
+	}
+}

+ 89 - 0
service/UserOnlineService.go

@@ -0,0 +1,89 @@
+package service
+
+import (
+	"designs/global"
+	"designs/model"
+	"strconv"
+	"time"
+)
+
+type logData struct {
+	LogTime time.Time `json:"LogTime"`
+	Type    int       `json:"Type"`
+}
+
+func UserOnlineSummary(gid string, pf string, date string, startTime string, endTime string) (map[int]string, error) {
+	var onlineData []model.UserOnline
+	//从数据库中查询出所有的数据
+	query := global.App.DB.Table("user_online").Where("gid", gid)
+	if date != "" {
+		query = query.Where("date", "=", date).Where("pf", pf)
+	}
+
+	if startTime != "" && endTime != "" {
+		query = query.Where("logTime", ">=", startTime).Where("logTime", "<=", endTime)
+	}
+	err := query.Scan(&onlineData).Error
+	if err != nil {
+		return nil, err
+	}
+
+	//对数据进行分组
+	userOnlineData := make(map[int][]logData)
+	for _, v := range onlineData {
+		value, exists := userOnlineData[v.UserId]
+		if exists {
+			userOnlineData[v.UserId] = append(value, logData{v.LogTime, v.Type})
+		} else {
+			userOnlineData[v.UserId] = []logData{{v.LogTime, v.Type}}
+		}
+	}
+	onlineData = nil
+
+	userLogMsg := make(map[int]string)
+	//分组后对于每个用户的数据进行处理,得到他们的具体在线时长
+	for k, v := range userOnlineData {
+		userLogMsg[k] = calculateUserOnlineTime(v)
+	}
+	userOnlineData = nil
+
+	return userLogMsg, nil
+}
+
+func calculateUserOnlineTime(logData []logData) string {
+
+	var lastLog int64
+	var isStart bool
+	var onlineTimeTotal int64
+	for k, v := range logData {
+
+		if v.Type == 1 && isStart == false {
+			isStart = true
+			lastLog = v.LogTime.Unix()
+			continue
+		}
+
+		if v.Type == 1 && isStart == true {
+
+			logTime := v.LogTime.Unix()
+			onlineTimeTotal = onlineTimeTotal + (logTime - lastLog)
+			lastLog = logTime
+			//如果下一次的心跳,间隔时间大于6分钟了,就可以认为,这次就算是结束了
+			if k+1 == len(logData) || logData[k+1].LogTime.Unix() > logTime+60*6 {
+				isStart = false
+			}
+			continue
+		}
+
+		if v.Type == 2 && isStart == true {
+			logTime := v.LogTime.Unix()
+			onlineTimeTotal = onlineTimeTotal + (logTime - lastLog)
+
+			isStart = false
+			continue
+		}
+
+	}
+
+	return "用户在线时长:" + strconv.Itoa(int(onlineTimeTotal))
+}

+ 52 - 0
service/remainData.go

@@ -0,0 +1,52 @@
+package service
+
+import (
+	"designs/global"
+	"designs/model"
+	"designs/utils"
+)
+
+func RemainDataBydDay(pf string, gid string, startTime string, endTime string) {
+	//先计算出这个时间内 ,每天的注册用户数量
+	var users []model.User
+
+	err := global.App.DB.Table("user").
+		Where("pf", pf).Where("gid", gid).
+		Where("createdAt", ">=", startTime).
+		Where("createdAt", "<=", endTime).
+		Scan(&users).Error
+	if err != nil {
+
+	}
+
+	var UsersId []int
+	UsersBydDay := utils.GetTimeDayDate(startTime, endTime) //用户分别是在哪天注册的
+	UserLoginBydDay := UsersBydDay
+	for _, user := range users {
+		UsersId = append(UsersId, user.UserId)
+
+		UsersBydDay[user.CreatedAt.Format("20060102")] = append(UsersBydDay[user.CreatedAt.Format("20060102")], user.UserId)
+	}
+	users = nil //用完后清空内存
+	//把每天的注册用户进行集合,查出后面所有天数的活跃情况
+	var UserLogin []model.UserLogin
+	err = global.App.DB.Table("user_login").
+		Where("pf", pf).Where("gid", gid).
+		WhereIn("userId", UsersId).
+		Where("loginTime", ">=", startTime).
+		Select("loginTime", "userId").
+		Scan(&UserLogin).Error
+	if err != nil {
+
+	}
+
+	//对这些数据进行整理
+	for _, v := range UserLogin {
+		//根据天进行分组,得出总共有多少
+		times := v.LoginTime.Format("20060102")
+		UserLoginBydDay[times] = append(UserLoginBydDay[times], v.UserId)
+	}
+
+	//逐天比较注册的数据和留存的数据,得出每天的活跃比例和人数
+
+}

+ 309 - 0
service/userBehavior.go

@@ -0,0 +1,309 @@
+package service
+
+import (
+	"designs/global"
+	"designs/utils"
+	"time"
+)
+
+// 获取新增用户的时段信息
+func GetRegisterTimeDistribution(pf string, gid string) (map[int]int, map[int]int, int64, int64, int64, error) {
+	now := time.Now()
+	today := now.Format("2006-01-02")
+
+	hours := utils.GetDayHour(now)
+	var todayRegister []time.Time
+
+	//计算今日曲线
+	err := global.App.DB.Table("user").
+		Where("pf", pf).
+		Where("gid", gid).
+		Where("createdAt", ">", today).
+		Pluck("createdAt", &todayRegister).Error
+	if err != nil {
+		global.App.Log.Error(err.Error())
+		return nil, nil, 0, 0, 0, err
+	}
+	todayCount := len(todayRegister)
+
+	todayTimeDistribution := getTimeDistribution(todayRegister, hours)
+
+	//计算昨日曲线
+	var yesterdayRegister []time.Time
+	yesterdayHours := utils.GetDayHour(now.AddDate(0, 0, -1))
+	yesterday := now.AddDate(0, 0, -1).Format("2006-01-02")
+	yesterdayThisTime := now.AddDate(0, 0, -1).Format("2006-01-02 15:04:05")
+	err = global.App.DB.Table("user").
+		Where("pf", pf).
+		Where("gid", gid).
+		Where("createdAt", ">", yesterday).
+		Where("createdAt", "<=", today).
+		Pluck("createdAt", &yesterdayRegister).Error
+	if err != nil {
+		global.App.Log.Error(err.Error())
+		return nil, nil, 0, 0, 0, err
+	}
+	yesterdayCount := len(yesterdayRegister)
+	yesterdayTimeDistribution := getTimeDistribution(yesterdayRegister, yesterdayHours)
+	var yesterdayThisTimeCount int64
+	err = global.App.DB.Table("user").
+		Where("pf", pf).
+		Where("gid", gid).
+		Where("createdAt", ">", yesterday).
+		Where("createdAt", "<=", yesterdayThisTime).
+		Count(&yesterdayThisTimeCount).Error
+	if err != nil {
+		global.App.Log.Error(err.Error())
+		return nil, nil, 0, 0, 0, err
+	}
+
+	return todayTimeDistribution, yesterdayTimeDistribution, int64(yesterdayCount), int64(todayCount), yesterdayThisTimeCount, nil
+}
+
+// 活跃用户的时段信息
+func GetActiveTimeDistribution(pf string, gid string) (map[int]int, map[int]int, int64, int64, int64, error) {
+	now := time.Now()
+	today := now.Format("2006-01-02")
+
+	hours := utils.GetDayHour(now)
+	var todayRegister []time.Time
+
+	//计算今日曲线
+	err := global.App.DB.Table("user_login").
+		Where("pf", pf).
+		Where("gid", gid).
+		Where("loginTime", ">", today).
+		Pluck("loginTime", &todayRegister).Error
+	if err != nil {
+		global.App.Log.Error(err.Error())
+		return nil, nil, 0, 0, 0, err
+	}
+
+	var todayCount int64
+	err = global.App.DB.Table("user_login").
+		Where("pf", pf).
+		Where("gid", gid).
+		Where("loginTime", ">", today).
+		Group("userId").Count(&todayCount).Error
+	if err != nil {
+		global.App.Log.Error(err.Error())
+		return nil, nil, 0, 0, 0, err
+	}
+	todayTimeDistribution := getTimeDistribution(todayRegister, hours)
+
+	//计算昨日曲线
+	var yesterdayRegister []time.Time
+	yesterdayHours := utils.GetDayHour(now.AddDate(0, 0, -1))
+	yesterday := now.AddDate(0, 0, -1).Format("2006-01-02")
+	yesterdayThisTime := now.AddDate(0, 0, -1).Format("2006-01-02 15:04:05")
+	err = global.App.DB.Table("user_login").
+		Where("pf", pf).
+		Where("gid", gid).
+		Where("loginTime", ">", yesterday).
+		Where("loginTime", "<=", today).
+		Pluck("loginTime", &yesterdayRegister).Error
+	if err != nil {
+		global.App.Log.Error(err.Error())
+		return nil, nil, 0, 0, 0, err
+	}
+	var yesterdayCount int64
+	err = global.App.DB.Table("user_login").
+		Where("pf", pf).
+		Where("gid", gid).
+		Where("loginTime", ">", yesterday).
+		Where("loginTime", "<=", today).
+		Group("userId").Count(&yesterdayCount).Error
+	if err != nil {
+		global.App.Log.Error(err.Error())
+		return nil, nil, 0, 0, 0, err
+	}
+
+	var yesterdayThisTimeCount int64
+	err = global.App.DB.Table("user_login").
+		Where("pf", pf).
+		Where("gid", gid).
+		Where("loginTime", ">", yesterday).
+		Where("loginTime", "<=", yesterdayThisTime).
+		Group("userId").Count(&yesterdayCount).Error
+	if err != nil {
+		global.App.Log.Error(err.Error())
+		return nil, nil, 0, 0, 0, err
+	}
+
+	yesterdayTimeDistribution := getTimeDistribution(yesterdayRegister, yesterdayHours)
+
+	return todayTimeDistribution, yesterdayTimeDistribution, yesterdayCount, todayCount, yesterdayThisTimeCount, nil
+}
+
+// 新增设备日趋势图
+func GetRegisterDayDistribution(pf string, gid string, startTime string, endTime string) (map[int]int, int, float32, error) {
+	var registerDays []time.Time
+	err := global.App.DB.Table("user").
+		Where("gid", gid).
+		Where("pf", pf).
+		Where("createdAt", ">=", startTime).
+		Where("createdAt", "<=", endTime).
+		Pluck("createdAt", &registerDays).Error
+	if err != nil {
+		return nil, 0, 0, err
+	}
+	days := utils.GetTimeDay(startTime, endTime)
+
+	todayTimeDistribution := getTimeDistribution(registerDays, days)
+
+	return todayTimeDistribution, len(registerDays), float32(len(registerDays) / len(days)), nil
+}
+
+// 活跃用户趋势图
+func GetActiveDayDistribution(pf string, gid string, startTime string, endTime string) (map[int]int, int64, float32, error) {
+	var activeDays []time.Time
+	err := global.App.DB.Table("user_login").
+		Where("gid", gid).
+		Where("pf", pf).
+		Where("loginTime", ">=", startTime).
+		Where("loginTime", "<=", endTime).
+		Pluck("loginTime", &activeDays).Error
+	if err != nil {
+		return nil, 0, 0, err
+	}
+	//查询总数(去重)
+	var count int64
+	err = global.App.DB.Table("user_login").
+		Where("gid", gid).
+		Where("pf", pf).
+		Where("loginTime", ">=", startTime).
+		Where("loginTime", "<=", endTime).
+		Distinct("userId").Count(&count).Error
+	if err != nil {
+		return nil, 0, 0, err
+	}
+
+	days := utils.GetTimeDay(startTime, endTime)
+
+	todayTimeDistribution := getTimeDistribution(activeDays, days)
+
+	return todayTimeDistribution, count, float32(len(activeDays) / len(days)), nil
+}
+
+// 活跃用户周趋势图
+func GetActiveWeekDistribution(pf string, gid string, startTime string, endTime string) (map[int]int, int64, float32, error) {
+	var activeDays []time.Time
+	err := global.App.DB.Table("user_login").
+		Where("gid", gid).
+		Where("pf", pf).
+		Where("loginTime", ">=", startTime).
+		Where("loginTime", "<=", endTime).
+		Pluck("loginTime", &activeDays).Error
+	if err != nil {
+		return nil, 0, 0, err
+	}
+
+	days := utils.GetTimeDay(startTime, endTime)
+
+	todayTimeDistribution := getTimeDistribution(activeDays, days)
+
+	return todayTimeDistribution, 0, float32(len(activeDays) / len(days)), nil
+}
+
+// 活跃用户月趋势图
+func GetActiveMouthDistribution(pf string, gid string, startTime string, endTime string) (map[int]int, int64, float32, error) {
+	var activeDays []time.Time
+	err := global.App.DB.Table("user_login").
+		Where("gid", gid).
+		Where("pf", pf).
+		Where("loginTime", ">=", startTime).
+		Where("loginTime", "<=", endTime).
+		Pluck("loginTime", &activeDays).Error
+	if err != nil {
+		return nil, 0, 0, err
+	}
+
+	days := utils.GetTimeDay(startTime, endTime)
+
+	todayTimeDistribution := getTimeDistribution(activeDays, days)
+
+	return todayTimeDistribution, 0, float32(len(activeDays) / len(days)), nil
+}
+
+func GetLoginDistribution(pf string, gid string, startTime string, endTime string) (map[int]int, float32, error) {
+	var activeDays []time.Time
+	err := global.App.DB.Table("user_login").
+		Where("gid", gid).
+		Where("pf", pf).
+		Where("loginTime", ">=", startTime).
+		Where("loginTime", "<=", endTime).
+		Pluck("loginTime", &activeDays).Error
+	if err != nil {
+		return nil, 0, err
+	}
+	days := utils.GetTimeDay(startTime, endTime)
+
+	todayTimeDistribution := getTimeDistribution(activeDays, days)
+
+	return todayTimeDistribution, float32(len(activeDays) / len(days)), nil
+}
+
+// 根据时间求出时段信息
+func getTimeDistribution(todayRegister []time.Time, hours []int64) map[int]int {
+	var todayRegisterUnix []int64
+	for _, t := range todayRegister {
+		todayRegisterUnix = append(todayRegisterUnix, t.Unix())
+	}
+
+	todayRegisterSum := make(map[int]int)
+	for k := range hours {
+		todayRegisterSum[k] = 0
+	}
+	for _, t := range todayRegisterUnix {
+		for k := range hours {
+			if k == 0 {
+				if t < hours[k+1] {
+					todayRegisterSum[0]++
+				}
+			} else if k == len(hours)-1 {
+				if t > hours[k] {
+					todayRegisterSum[len(hours)-1]++
+				}
+			} else {
+				if t > hours[k] && t <= hours[k+1] {
+					todayRegisterSum[k]++
+				}
+			}
+		}
+	}
+
+	return todayRegisterSum
+}
+
+//// 根据日期求出分天信息
+//func getDayDistribution(mouthRegister []time.Time, days []int64) {
+//	var mouthRegisterUnix []int64
+//	for _, t := range mouthRegister {
+//		mouthRegisterUnix = append(mouthRegisterUnix, t.Unix())
+//	}
+//
+//	mouthRegisterSum := make(map[int]int)
+//	for k := range days {
+//		mouthRegisterSum[k] = 0
+//	}
+//
+//	for _, t := range mouthRegisterUnix {
+//		for k := range days {
+//			if k == 0 {
+//				if t < days[k+1] {
+//					todayRegisterSum[0]++
+//				}
+//			} else if k ==  {
+//				if t > days[k] {
+//					todayRegisterSum[23]++
+//				}
+//			} else {
+//				if t > days[k] && t <= days[k+1] {
+//					todayRegisterSum[k]++
+//				}
+//			}
+//		}
+//	}
+//
+//
+//}

+ 35 - 0
utils/array.go

@@ -0,0 +1,35 @@
+package utils
+
+type BasicType interface {
+	int | int8 | int16 | int32 | int64 | uint8 | uint16 | uint32 | uint64 | string
+}
+
+func InArray[T BasicType](needle T, hystack []T) bool {
+	for _, item := range hystack {
+		if needle == item {
+			return true
+		}
+	}
+
+	return false
+}
+
+func SliceArray[T any](arr []T, offset int, limit int) []T {
+	size := len(arr)
+	if size <= offset {
+		return []T{}
+	} else if size > offset && size < offset+limit {
+		return arr[offset:size]
+	} else {
+		return arr[offset : offset+limit]
+	}
+}
+
+func ToMap[K comparable, T any](arr []T, getKey func(*T) K) map[K]T {
+	r := make(map[K]T)
+	for _, v := range arr {
+		k := getKey(&v)
+		r[k] = v
+	}
+	return r
+}

+ 23 - 0
utils/bcrypt.go

@@ -0,0 +1,23 @@
+package utils
+
+import (
+	"golang.org/x/crypto/bcrypt"
+	"log"
+)
+
+func BcryptMake(pwd []byte) string {
+	hash, err := bcrypt.GenerateFromPassword(pwd, bcrypt.MinCost)
+	if err != nil {
+		log.Println(err)
+	}
+	return string(hash)
+}
+
+func BcryptMakeCheck(pwd []byte, hashedPwd string) bool {
+	byteHash := []byte(hashedPwd)
+	err := bcrypt.CompareHashAndPassword(byteHash, pwd)
+	if err != nil {
+		return false
+	}
+	return true
+}

+ 128 - 0
utils/db.go

@@ -0,0 +1,128 @@
+package utils
+
+import (
+	"database/sql"
+	"fmt"
+
+	"gorm.io/gorm"
+)
+
+type WtDB struct {
+	*gorm.DB
+}
+
+func (db *WtDB) Table(name string, args ...interface{}) (tx *WtDB) {
+	return &WtDB{db.DB.Table(name, args...)}
+}
+
+func (db *WtDB) Distinct(args ...interface{}) (tx *WtDB) {
+	return &WtDB{db.DB.Distinct(args...)}
+}
+
+func (db *WtDB) Select(query interface{}, args ...interface{}) (tx *WtDB) {
+	return &WtDB{db.DB.Select(query, args...)}
+}
+
+func (db *WtDB) WhereRaw(query interface{}, args ...interface{}) (tx *WtDB) {
+	return &WtDB{db.DB.Where(query, args...)}
+}
+
+func (db *WtDB) Where(field string, args ...interface{}) (tx *WtDB) {
+	n := len(args)
+	if n == 0 {
+		return &WtDB{db.DB.Where(field)}
+	}
+
+	if n == 1 {
+		return &WtDB{db.DB.Where(field+" = ?", args...)}
+	}
+
+	if n == 2 {
+		var opList = []string{"=", ">", "<", ">=", "<=", "!=", "like"}
+		if v, ok := args[0].(string); ok {
+			if InArray(v, opList) {
+				return &WtDB{db.DB.Where(fmt.Sprintf("%s %s ?", field, v), args[1:]...)}
+			}
+		}
+	}
+
+	return &WtDB{db.DB.Where(field, args...)}
+}
+
+func (db *WtDB) WhereIn(field string, args ...interface{}) (tx *WtDB) {
+	return &WtDB{db.DB.Where(fmt.Sprintf("%s in ?", field), args...)}
+}
+
+func (db *WtDB) WhereNotIn(field string, args ...interface{}) (tx *WtDB) {
+	return &WtDB{db.DB.Where(fmt.Sprintf("%s not in ?", field), args...)}
+}
+
+func (db *WtDB) Not(query interface{}, args ...interface{}) (tx *WtDB) {
+	return &WtDB{db.DB.Not(query, args...)}
+}
+
+func (db *WtDB) Or(query interface{}, args ...interface{}) (tx *WtDB) {
+	return &WtDB{db.DB.Or(query, args...)}
+}
+
+func (db *WtDB) Join(table string, query string, args ...interface{}) (tx *WtDB) {
+	return &WtDB{db.DB.Joins(fmt.Sprintf("inner join %s on %s", table, query), args...)}
+}
+
+func (db *WtDB) LeftJoin(table string, query string, args ...interface{}) (tx *WtDB) {
+	return &WtDB{db.DB.Joins(fmt.Sprintf("left join %s on %s", table, query), args...)}
+}
+
+func (db *WtDB) RightJoin(table string, query string, args ...interface{}) (tx *WtDB) {
+	return &WtDB{db.DB.Joins(fmt.Sprintf("right join %s on %s", table, query), args...)}
+}
+
+func (db *WtDB) JoinRaw(query string, args ...interface{}) (tx *WtDB) {
+	return &WtDB{db.DB.Joins(query, args...)}
+}
+
+func (db *WtDB) Group(name string) (tx *WtDB) {
+	return &WtDB{db.DB.Group(name)}
+}
+
+func (db *WtDB) Having(query interface{}, args ...interface{}) (tx *WtDB) {
+	return &WtDB{db.DB.Having(query, args...)}
+}
+
+func (db *WtDB) Order(value interface{}) (tx *WtDB) {
+	return &WtDB{db.DB.Order(value)}
+}
+
+func (db *WtDB) Limit(limit int) (tx *WtDB) {
+	return &WtDB{db.DB.Limit(limit)}
+}
+
+func (db *WtDB) Offset(offset int) (tx *WtDB) {
+	return &WtDB{db.DB.Offset(offset)}
+}
+
+func (db *WtDB) Raw(sql string, values ...interface{}) (tx *WtDB) {
+	return &WtDB{db.DB.Raw(sql, values...)}
+}
+
+func (db *WtDB) SubQuery() *gorm.DB {
+	return db.DB
+}
+
+func (db *WtDB) Begin(opts ...*sql.TxOptions) (tx *WtDB) {
+	return &WtDB{DB: db.DB.Begin(opts...)}
+}
+
+func (db *WtDB) Commit() (tx *WtDB) {
+	return &WtDB{DB: db.DB.Commit()}
+}
+
+func (db *WtDB) Rollback() (tx *WtDB) {
+	return &WtDB{DB: db.DB.Rollback()}
+}
+
+func (db *WtDB) Transaction(fc func(tx *WtDB) error) (err error) {
+	return db.DB.Transaction(func(t *gorm.DB) error {
+		return fc(&WtDB{DB: t})
+	})
+}

+ 46 - 0
utils/directory.go

@@ -0,0 +1,46 @@
+package utils
+
+import (
+	"os"
+)
+
+func PathExists(path string) (bool, error) {
+	_, err := os.Stat(path)
+	if err == nil {
+		return true, nil
+	}
+	if os.IsNotExist(err) {
+		return false, nil
+	}
+	return false, err
+}
+
+//判断是否问有效文件夹
+func IsDir(filename string) (bool, error) {
+	fd, err := os.Stat(filename)
+	if err != nil {
+		return false, err
+	}
+	fm := fd.Mode()
+	return fm.IsDir(), nil
+}
+
+//创建文件夹
+func CreateDir(dir string) error {
+	if ifdir, _ := IsDir(dir); !ifdir {
+		err := os.Mkdir(dir, 0777)
+		if err != nil {
+			//global.App.Log.Error("新建文件夹失败,path=" + dir)
+
+			return err
+		}
+		// 再修改权限
+		err = os.Chmod(dir, 0777)
+		if err != nil {
+			//global.App.Log.Error("修改文件夹权限失败,path=" + dir)
+			return err
+		}
+	}
+
+	return nil
+}

+ 8 - 0
utils/json.go

@@ -0,0 +1,8 @@
+package utils
+
+import "encoding/json"
+
+func JsonEncode(data interface{}) (string, error) {
+	jsons, err := json.Marshal(data)
+	return string(jsons), err
+}

+ 17 - 0
utils/map.go

@@ -0,0 +1,17 @@
+package utils
+
+func MergeMaps(destMap, sourceMap map[string]interface{}) map[string]interface{} {
+	newMap := make(map[string]interface{})
+
+	// 将目标 map 的元素复制到新 map 中
+	for key, value := range destMap {
+		newMap[key] = value
+	}
+
+	// 将源 map 中的元素合并到新 map 中
+	for key, value := range sourceMap {
+		newMap[key] = value
+	}
+
+	return newMap
+}

+ 12 - 0
utils/md5.go

@@ -0,0 +1,12 @@
+package utils
+
+import (
+	"crypto/md5"
+	"encoding/hex"
+)
+
+func MD5(str []byte, b ...byte) string {
+	h := md5.New()
+	h.Write(str)
+	return hex.EncodeToString(h.Sum(b))
+}

+ 32 - 0
utils/str.go

@@ -0,0 +1,32 @@
+package utils
+
+import (
+	"math/rand"
+	"strconv"
+	"time"
+)
+
+func RandString(len int) string {
+	r := rand.New(rand.NewSource(time.Now().UnixNano()))
+	bytes := make([]byte, len)
+	for i := 0; i < len; i++ {
+		b := r.Intn(26) + 65
+		bytes[i] = byte(b)
+	}
+	return string(bytes)
+}
+
+func GetSampleAlleleNum(sampleId int) string {
+	sampleIdString := strconv.Itoa(sampleId)
+	var num string
+	if len(sampleIdString) > 2 {
+		num = sampleIdString[len(sampleIdString)-2:]
+	} else {
+		num = sampleIdString
+	}
+	if num[0:1] == "0" && len(num) == 2 {
+		num = num[1:]
+	}
+
+	return num
+}

+ 53 - 0
utils/time.go

@@ -0,0 +1,53 @@
+package utils
+
+import (
+	"time"
+)
+
+// 给出日期  获取其中每小时的开始时间戳
+func GetDayHour(date time.Time) []int64 {
+
+	date = time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, time.Local)
+
+	var hours []int64
+	// 遍历这一天中的每一小时
+	for hour := 0; hour < 24; hour++ {
+		// 设置小时并保留分钟和秒为零
+		t := date.Add(time.Duration(hour) * time.Hour)
+		//fmt.Println(t.Format("2006-01-02 15:04:05"))
+
+		hours = append(hours, t.Unix())
+	}
+
+	return hours
+}
+
+// 给出开始和结束日期  获取其中每一天的开始时间戳
+func GetTimeDay(startDate string, endDate string) []int64 {
+	var days []int64
+	startTime, _ := time.Parse("2006-01-02", startDate)
+	endTime, _ := time.Parse("2006-01-02", endDate)
+
+	for currTime := startTime; !currTime.After(endTime); currTime = currTime.AddDate(0, 0, 1) {
+		// 设置时间为当天的开始时间
+		midnight := time.Date(currTime.Year(), currTime.Month(), currTime.Day(), 0, 0, 0, 0, currTime.Location()).Unix()
+		days = append(days, midnight)
+	}
+
+	return days
+}
+
+// 给出开始和结束日期  获取中间每一天的date
+func GetTimeDayDate(startDate string, endDate string) map[string][]int {
+	days := make(map[string][]int)
+	startTime, _ := time.Parse("2006-01-02", startDate)
+	endTime, _ := time.Parse("2006-01-02", endDate)
+
+	for currTime := startTime; !currTime.After(endTime); currTime = currTime.AddDate(0, 0, 1) {
+		// 设置时间为当天的开始时间
+		midnight := time.Date(currTime.Year(), currTime.Month(), currTime.Day(), 0, 0, 0, 0, currTime.Location()).Format("20060102")
+		days[midnight] = []int{}
+	}
+
+	return days
+}

+ 16 - 0
utils/validator.go

@@ -0,0 +1,16 @@
+package utils
+
+import (
+	"github.com/go-playground/validator/v10"
+	"regexp"
+)
+
+// ValidateMobile 校验手机号
+func ValidateMobile(fl validator.FieldLevel) bool {
+	mobile := fl.Field().String()
+	ok, _ := regexp.MatchString(`^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$`, mobile)
+	if !ok {
+		return false
+	}
+	return true
+}