Преглед на файлове

用户行为分析相关接口增加

wucan преди 9 месеца
родител
ревизия
0dee548e0a
променени са 18 файла, в които са добавени 1582 реда и са изтрити 33 реда
  1. 130 0
      bootstrap/db.go
  2. 1 1
      config/app.go
  3. 1 0
      config/config.go
  4. 17 0
      config/database.go
  5. 23 2
      controller/v1/user.go
  6. 464 0
      controller/v1/userBehavior.go
  7. 3 3
      global/app.go
  8. 7 1
      go.mod
  9. 16 0
      go.sum
  10. 11 0
      main.go
  11. 37 0
      model/user.go
  12. 11 1
      route/api.go
  13. 146 0
      service/UserOnlineService.go
  14. 68 0
      service/remainData.go
  15. 382 0
      service/userBehavior.go
  16. 35 25
      utils/array.go
  17. 128 0
      utils/db.go
  18. 102 0
      utils/time.go

+ 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)
+	}
+}

+ 1 - 1
config/app.go

@@ -11,7 +11,7 @@ func App() *ConfigNode {
 		"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_exp":          env("API_EXP", "600000"),                    //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"),

+ 1 - 0
config/config.go

@@ -12,4 +12,5 @@ var RootConfig = ConfigRoot{
 	"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", "info"),
+		"enableFileLogWriter": env("MYSQL_ENABLE_LOG_WRITE", "true"),
+	}
+}

+ 23 - 2
controller/v1/user.go

@@ -7,6 +7,7 @@ import (
 	"designs/common"
 	"designs/config"
 	"designs/global"
+	"designs/service"
 	"designs/utils"
 	"strings"
 	"time"
@@ -126,8 +127,8 @@ type User struct {
 
 func UserList(c *gin.Context) {
 	form := request.Check(c, &struct {
-		Gid    string `form:"gid" json:"gid" binding:"required"`
-		Pf     string `form:"pf" json:"pf" binding:"required"`
+		Gid    string `form:"gid" json:"gid" binding:""`
+		Pf     string `form:"pf" json:"pf" binding:""`
 		Offset int    `form:"offset" json:"offset" binding:""`
 		Limit  int    `form:"limit" json:"limit" binding:"required"`
 	}{})
@@ -204,3 +205,23 @@ func UserList(c *gin.Context) {
 		"count": count,
 	})
 }
+
+func GetUserOnlineMsg(c *gin.Context) {
+	form := request.Check(c, &struct {
+		Gid       string `form:"gid" json:"gid" binding:"required"`
+		Pf        string `form:"pf" json:"pf" binding:"required"`
+		Date      string `form:"date" json:"data" binding:""`
+		StartTime string `form:"startTime" json:"startTime" binding:""`
+		EndTime   string `form:"endTime" json:"endTime" binding:""`
+	}{})
+
+	res, err := service.UserOnlineSummary(form.Gid, form.Pf, form.Date, form.StartTime, form.EndTime)
+	if err != nil {
+		response.Fail(c, 1001, err.Error())
+		return
+	}
+
+	response.Success(c, gin.H{
+		"data": res,
+	})
+}

+ 464 - 0
controller/v1/userBehavior.go

@@ -0,0 +1,464 @@
+package v1
+
+import (
+	"designs/app/common/request"
+	"designs/app/common/response"
+	"designs/global"
+	"designs/service"
+	"designs/utils"
+	"github.com/gin-gonic/gin"
+	"math"
+	"time"
+)
+
+// 总览
+func Summary(c *gin.Context) {
+	form := request.Check(c, &struct {
+		Gid string `form:"gid" json:"gid" binding:"required"`
+		Pf  string `form:"pf" json:"pf" binding:"required"`
+	}{})
+
+	//查询用户总数
+	var userCount int64
+	err := global.App.DB.Table("user").
+		Where("gid", form.Gid).
+		Where("pf", form.Pf).
+		Count(&userCount).Error
+	if err != nil {
+		response.Fail(c, 1001, err.Error())
+		return
+	}
+
+	//查询近七日活跃总数
+	now := time.Now()
+	sevenDayAgo := now.AddDate(0, 0, -7)
+	thirtyDayAgo := now.AddDate(0, 0, -30)
+
+	var activeUserCount7 int64
+	err = global.App.DB.Table("user_login").
+		Where("gid", form.Gid).
+		Where("pf", form.Pf).
+		Where("loginTime", ">=", sevenDayAgo).
+		Where("loginTime", "<=", now).
+		Distinct("userId").
+		Count(&activeUserCount7).Error
+	if err != nil {
+		response.Fail(c, 1001, err.Error())
+		return
+	}
+
+	var activeUserCount30 int64
+	err = global.App.DB.Table("user_login").
+		Where("gid", form.Gid).
+		Where("pf", form.Pf).
+		Where("loginTime", ">=", thirtyDayAgo).
+		Where("loginTime", "<=", now).
+		Distinct("userId").
+		Count(&activeUserCount30).Error
+	if err != nil {
+		response.Fail(c, 1001, err.Error())
+		return
+	}
+
+	//查询 近7日单设备日均使用时长
+	res, err := service.UserOnlineSummary(form.Gid, form.Pf, "", sevenDayAgo.Format("2006-01-02 15:04:05"), now.Format("2006-01-02 15:04:05"))
+	if err != nil {
+		response.Fail(c, 1001, err.Error())
+		return
+	}
+	var avgTime int
+	for _, v := range res {
+		avgTime = avgTime + int(v)
+	}
+	var avgTimeString string
+	if avgTime != 0 {
+		avgTime = int(math.Round(float64(avgTime / len(res))))
+		avgTimeString = utils.TimeStampToMDS(avgTime)
+	} else {
+		avgTimeString = "00.00"
+	}
+
+	response.Success(c, gin.H{
+		"data": map[string]interface{}{
+			"userCount":            userCount,
+			"activeUserCount7":     activeUserCount7,
+			"activeUserCount30":    activeUserCount30,
+			"activeUserCount7Time": avgTimeString,
+		},
+	})
+}
+
+// 时段分布
+func TimeDistributionData(c *gin.Context) {
+	form := request.Check(c, &struct {
+		Gid  string `form:"gid" json:"gid" binding:"required"`
+		Pf   string `form:"pf" json:"pf" binding:"required"`
+		Type int    `form:"type" json:"type" binding:"required"`
+	}{})
+
+	var data interface{}
+	if form.Type == 1 {
+		//新增用户
+		todayTimeDistribution, yesterdayTimeDistribution, yesterdayCount, todayCount, yesterdayThisTimeCount, err := service.GetRegisterTimeDistribution(form.Pf, form.Gid)
+		if err != nil {
+			response.Fail(c, 1001, err.Error())
+			return
+		}
+
+		data = map[string]interface{}{
+			"today":                  todayTimeDistribution,
+			"yesterday":              yesterdayTimeDistribution,
+			"yesterdayCount":         yesterdayCount,
+			"yesterdayThisTimeCount": yesterdayThisTimeCount,
+			"todayCount":             todayCount,
+		}
+	} else if form.Type == 2 {
+		//活跃设备
+		todayTimeDistribution, yesterdayTimeDistribution, yesterdayCount, todayCount, yesterdayThisTimeCount, err := service.GetActiveTimeDistribution(form.Pf, form.Gid)
+		if err != nil {
+			response.Fail(c, 1001, err.Error())
+			return
+		}
+		data = map[string]interface{}{
+			"today":                  todayTimeDistribution,
+			"yesterday":              yesterdayTimeDistribution,
+			"yesterdayCount":         yesterdayCount,
+			"yesterdayThisTimeCount": yesterdayThisTimeCount,
+			"todayCount":             todayCount,
+		}
+	} else if form.Type == 3 {
+		//启动次数
+		todayTimeDistribution, yesterdayTimeDistribution, yesterdayCount, todayCount, yesterdayThisTimeCount, err := service.GetActiveTimeDistribution(form.Pf, form.Gid)
+		if err != nil {
+			response.Fail(c, 1001, err.Error())
+			return
+		}
+		data = map[string]interface{}{
+			"today":                  todayTimeDistribution,
+			"yesterday":              yesterdayTimeDistribution,
+			"yesterdayCount":         yesterdayCount,
+			"yesterdayThisTimeCount": yesterdayThisTimeCount,
+			"todayCount":             todayCount,
+		}
+
+	} else {
+		response.Fail(c, 1003, "type错误")
+		return
+	}
+
+	response.Success(c, gin.H{
+		"data": data,
+	})
+}
+
+// 30日趋势
+func MouthDistributionData(c *gin.Context) {
+	form := request.Check(c, &struct {
+		Gid  string `form:"gid" json:"gid" binding:"required"`
+		Pf   string `form:"pf" json:"pf" binding:"required"`
+		Type int    `form:"type" json:"type" binding:"required"`
+	}{})
+	now := time.Now()
+	EndTime := now.Format("2006-01-02")
+	StartTime := now.AddDate(0, 0, -29).Format("2006-01-02")
+
+	data := make(map[string]interface{})
+	if form.Type == 1 {
+		//查询新增设备趋势图
+		TimeDistribution, count, avg, err := service.GetRegisterDayDistribution(form.Pf, form.Gid, StartTime, EndTime)
+		if err != nil {
+			response.Fail(c, 1001, err.Error())
+			return
+		}
+		data["timeDistribution"] = TimeDistribution
+		data["count"] = count
+		data["avg"] = avg
+
+	} else if form.Type == 2 {
+		//查询活跃用户趋势图
+		TimeDistribution, count, avg, err := service.GetActiveDayDistribution(form.Pf, form.Gid, StartTime, EndTime)
+		if err != nil {
+			response.Fail(c, 1001, err.Error())
+			return
+		}
+		data["timeDistribution"] = TimeDistribution
+		data["count"] = count
+		data["avg"] = avg
+	} else if form.Type == 3 {
+		//查询启动次数
+		TimeDistribution, count, avg, err := service.GetActiveMouthDistribution(form.Pf, form.Gid, StartTime, EndTime)
+		if err != nil {
+			response.Fail(c, 1001, err.Error())
+			return
+		}
+		data["timeDistribution"] = TimeDistribution
+		data["count"] = count
+		data["avg"] = avg
+
+	} else if form.Type == 4 {
+		//查询单用户使用时长
+		TimeDistribution, count, avg, err := service.GetActiveMouthDistribution(form.Pf, form.Gid, StartTime, EndTime)
+		if err != nil {
+			response.Fail(c, 1001, err.Error())
+			return
+		}
+		data["timeDistribution"] = TimeDistribution
+		data["count"] = count
+		data["avg"] = avg
+	} else if form.Type == 5 {
+		//查询用户留存率
+		//todo
+
+	} else {
+		response.Fail(c, 1003, "type错误")
+		return
+	}
+
+	response.Success(c, gin.H{
+		"data": data,
+	})
+}
+
+// 用户趋势 总览
+func UserTrendsOverview(c *gin.Context) {
+	form := request.Check(c, &struct {
+		Gid       string `form:"gid" json:"gid" binding:"required"`
+		Pf        string `form:"pf" json:"pf" binding:"required"`
+		StartTime string `form:"startTime" json:"startTime" binding:"required"`
+		EndTime   string `form:"endTime" json:"endTime" binding:"required"`
+	}{})
+
+	//查询用户新增
+	var registerCount int64
+	err := global.App.DB.Table("user").
+		Where("gid", form.Gid).
+		Where("pf", form.Pf).
+		Where("createdAt", ">=", form.StartTime).
+		Where("createdAt", "<=", form.EndTime).
+		Count(&registerCount).Error
+	if err != nil {
+		response.Fail(c, 1001, err.Error())
+		return
+	}
+
+	//查询活跃设备
+	var activeCount int64
+	err = global.App.DB.Table("user_online").
+		Where("gid", form.Gid).
+		Where("pf", form.Pf).
+		Where("logTime", ">=", form.StartTime).
+		Where("logTime", "<=", form.EndTime).
+		Distinct("userId").Count(&activeCount).Error
+	if err != nil {
+		response.Fail(c, 1001, err.Error())
+		return
+	}
+
+	//查询启动次数
+	var loginCount int64
+	err = global.App.DB.Table("user_login").
+		Where("gid", form.Gid).
+		Where("pf", form.Pf).
+		Where("loginTime", ">=", form.StartTime).
+		Where("loginTime", "<=", form.EndTime).
+		Count(&loginCount).Error
+	if err != nil {
+		response.Fail(c, 1001, err.Error())
+		return
+	}
+	//查询平均启动时长
+	//查询活跃用户月趋势图
+	_, _, activeTime, err := service.GetActiveMouthDistribution(form.Pf, form.Gid, form.StartTime, form.EndTime)
+	if err != nil {
+		response.Fail(c, 1001, err.Error())
+		return
+	}
+
+	//查询 DAU/MAU
+	//todo 查询方式需要更新
+	dauMau := 0.3
+
+	response.Success(c, gin.H{
+		"data": map[string]interface{}{
+			"registerCount": registerCount,
+			"activeCount":   activeCount,
+			"loginCount":    loginCount,
+			"activeTime":    activeTime,
+			"dauMau":        dauMau,
+		},
+	})
+}
+
+// 数据趋势
+func DataTrades(c *gin.Context) {
+	form := request.Check(c, &struct {
+		Gid       string `form:"gid" json:"gid" binding:"required"`
+		Pf        string `form:"pf" json:"pf" binding:"required"`
+		StartTime string `form:"startTime" json:"startTime" binding:"required"`
+		EndTime   string `form:"endTime" json:"endTime" binding:"required"`
+		Type      int    `form:"type" json:"type" binding:"required"`
+	}{})
+	data := make(map[string]interface{})
+	if form.Type == 1 {
+		//查询新增设备趋势图
+		TimeDistribution, count, avg, err := service.GetRegisterDayDistribution(form.Pf, form.Gid, form.StartTime, form.EndTime)
+		if err != nil {
+			response.Fail(c, 1001, err.Error())
+			return
+		}
+		data["imeDistribution"] = TimeDistribution
+		data["count"] = count
+		data["avg"] = avg
+	} else if form.Type == 2 {
+		//查询活跃用户趋势图
+		TimeDistribution, count, avg, err := service.GetActiveDayDistribution(form.Pf, form.Gid, form.StartTime, form.EndTime)
+		if err != nil {
+			response.Fail(c, 1001, err.Error())
+			return
+		}
+		data["imeDistribution"] = TimeDistribution
+		data["count"] = count
+		data["avg"] = avg
+	} else if form.Type == 3 {
+		//查询活跃用户周趋势图
+		TimeDistribution, count, avg, err := service.GetActiveWeekDistribution(form.Pf, form.Gid, form.StartTime, form.EndTime)
+		if err != nil {
+			response.Fail(c, 1001, err.Error())
+			return
+		}
+		data["imeDistribution"] = TimeDistribution
+		data["count"] = count
+		data["avg"] = avg
+
+	} else if form.Type == 4 {
+		//查询活跃用户月趋势图
+		TimeDistribution, count, avg, err := service.GetActiveMouthDistribution(form.Pf, form.Gid, form.StartTime, form.EndTime)
+		if err != nil {
+			response.Fail(c, 1001, err.Error())
+			return
+		}
+		data["imeDistribution"] = TimeDistribution
+		data["count"] = count
+		data["avg"] = avg
+	} else if form.Type == 5 {
+		//查询启动次数
+		TimeDistribution, count, avg, err := service.GetActiveMouthDistribution(form.Pf, form.Gid, form.StartTime, form.EndTime)
+		if err != nil {
+			response.Fail(c, 1001, err.Error())
+			return
+		}
+		data["imeDistribution"] = TimeDistribution
+		data["count"] = count
+		data["avg"] = avg
+	} else if form.Type == 6 {
+		//查询平均启动时间
+		TimeDistribution, count, avg, err := service.UserOnlineSummaryByDay(form.Gid, form.Pf, form.StartTime, form.EndTime)
+		if err != nil {
+			response.Fail(c, 1001, err.Error())
+			return
+		}
+		data["imeDistribution"] = TimeDistribution
+		data["count"] = count
+		data["avg"] = avg
+
+	} else {
+		response.Fail(c, 1003, "type 错误")
+		return
+	}
+
+	response.Success(c, gin.H{
+		"data": data,
+	})
+}
+
+// 数据趋势的整合
+func DataTradesDetail(c *gin.Context) {
+	form := request.Check(c, &struct {
+		Gid       string `form:"gid" json:"gid" binding:"required"`
+		Pf        string `form:"pf" json:"pf" binding:"required"`
+		StartTime string `form:"startTime" json:"startTime" binding:"required"`
+		EndTime   string `form:"endTime" json:"endTime" binding:"required"`
+	}{})
+
+	type dayData struct {
+		NewUser         int `json:"newUser"`
+		ActiveUser      int `json:"activeUser"`
+		ActiveUserWeek  int `json:"activeUserWeek"`
+		ActiveUserMouth int `json:"activeUserMouth"`
+		ActiveStart     int `json:"activeStart"`
+		AvgTime         int `json:"avgTime"`
+	}
+	var data = make(map[string]dayData)
+	//查询新增设备趋势图
+	NewUser, _, _, err := service.GetRegisterDayDistribution(form.Pf, form.Gid, form.StartTime, form.EndTime)
+	if err != nil {
+		response.Fail(c, 1001, err.Error())
+		return
+	}
+	//查询活跃用户趋势图
+	ActiveUser, _, _, err := service.GetActiveDayDistribution(form.Pf, form.Gid, form.StartTime, form.EndTime)
+	if err != nil {
+		response.Fail(c, 1001, err.Error())
+		return
+	}
+	//查询活跃用户周趋势图
+	ActiveUserWeek, _, _, err := service.GetActiveWeekDistribution(form.Pf, form.Gid, form.StartTime, form.EndTime)
+	if err != nil {
+		response.Fail(c, 1001, err.Error())
+		return
+	}
+	//查询活跃用户月趋势图
+	ActiveUserMouth, _, _, err := service.GetActiveMouthDistribution(form.Pf, form.Gid, form.StartTime, form.EndTime)
+	if err != nil {
+		response.Fail(c, 1001, err.Error())
+		return
+	}
+	//查询启动次数
+	ActiveStart, _, _, err := service.GetActiveMouthDistribution(form.Pf, form.Gid, form.StartTime, form.EndTime)
+	if err != nil {
+		response.Fail(c, 1001, err.Error())
+		return
+	}
+
+	//查询平均启动时间
+	AvgTime, _, _, err := service.UserOnlineSummaryByDay(form.Gid, form.Pf, form.StartTime, form.EndTime)
+	if err != nil {
+		response.Fail(c, 1001, err.Error())
+		return
+	}
+
+	for k := range AvgTime {
+		data[k] = dayData{
+			NewUser:         NewUser[k],
+			ActiveUser:      ActiveUser[k],
+			ActiveUserWeek:  ActiveUserWeek[k],
+			ActiveUserMouth: ActiveUserMouth[k],
+			ActiveStart:     ActiveStart[k],
+			AvgTime:         AvgTime[k],
+		}
+	}
+
+	response.Success(c, gin.H{
+		"data": data,
+	})
+
+}
+
+func RemainDataBydDay(c *gin.Context) {
+	form := request.Check(c, &struct {
+		Gid       string `form:"gid" json:"gid" binding:"required"`
+		Pf        string `form:"pf" json:"pf" binding:"required"`
+		StartTime string `form:"startTime" json:"startTime" binding:"required"`
+		EndTime   string `form:"endTime" json:"endTime" binding:"required"`
+	}{})
+
+	data, err := service.RemainDataBydDay(form.Pf, form.Gid, form.StartTime, form.EndTime)
+	if err != nil {
+		response.Fail(c, 1001, err.Error())
+		return
+	}
+
+	response.Success(c, gin.H{
+		"data": data,
+	})
+}

+ 3 - 3
global/app.go

@@ -1,10 +1,10 @@
 package global
 
 import (
+	"designs/utils"
+	"github.com/go-redis/redis/v8"
 	"go.uber.org/zap"
 	"io"
-
-	"github.com/go-redis/redis/v8"
 )
 
 type InitConfig struct {
@@ -18,7 +18,7 @@ type Application struct {
 	//配置
 
 	//数据库
-	//DB *utils.WtDB
+	DB *utils.WtDB
 
 	Redis *redis.Client
 	//Cron      *cron.Cron

+ 7 - 1
go.mod

@@ -18,6 +18,7 @@ require (
 )
 
 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
@@ -28,6 +29,9 @@ require (
 	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
@@ -43,7 +47,9 @@ require (
 	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.16.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
+	gorm.io/driver/mysql v1.5.7 // indirect
+	gorm.io/gorm v1.25.11 // indirect
 )

+ 16 - 0
go.sum

@@ -1,3 +1,5 @@
+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=
@@ -33,6 +35,9 @@ github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4
 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=
@@ -40,6 +45,10 @@ github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVI
 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=
@@ -115,6 +124,8 @@ 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.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
 golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
+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=
@@ -128,5 +139,10 @@ 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=

+ 11 - 0
main.go

@@ -4,6 +4,7 @@ import (
 	"designs/bootstrap"
 	"designs/global"
 	"designs/middleware"
+	"designs/utils"
 	"fmt"
 	"github.com/gin-gonic/gin"
 	"net/http"
@@ -46,6 +47,16 @@ func main() {
 	//服务器端口
 	//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")
 }

+ 37 - 0
model/user.go

@@ -1,5 +1,7 @@
 package model
 
+import "time"
+
 /* code 结构体 */
 type CodeData struct {
 	Code   string `form:"code" binding:"required"`
@@ -7,3 +9,38 @@ type CodeData struct {
 	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;"`
+	Type    int       `json:"type" gorm:"not null;"`
+	UserId  int       `json:"userId" gorm:"not null;column:userId;"`
+	LogTime time.Time `json:"logTime" gorm:"column:logTime;"`
+}

+ 11 - 1
route/api.go

@@ -29,11 +29,21 @@ func SetApiGroupRoutes(router *gin.RouterGroup) {
 		GroupV1.POST("/user/addUserOption", v1.AddUserOption)
 		GroupV1.POST("/user/getUserOption", v1.GetUserOption)
 
-		GroupV1.POST("/user/overview", v1.Overview)
+		//GroupV1.POST("/user/overview", v1.Overview)
+		GroupV1.POST("/user/summary", v1.Summary)
 		GroupV1.POST("/user/getInterfaceLog", v1.GetInterfaceLog)
 		GroupV1.POST("/user/getInterfaceInfo", v1.GetInterfaceInfo)
 		GroupV1.POST("/user/getInterfaceData", v1.GetInterfaceData)
 		GroupV1.POST("/user/getInterfaceDataByDay", v1.GetInterfaceDataByDay)
 
+		GroupV1.POST("/user/getUserOnlineMsg", v1.GetUserOnlineMsg)
+
+		GroupV1.POST("/user/timeDistributionData", v1.TimeDistributionData)
+		GroupV1.POST("/user/mouthDistributionData", v1.MouthDistributionData)
+		GroupV1.POST("/user/userTrendsOverview", v1.UserTrendsOverview)
+		GroupV1.POST("/user/dataTrades", v1.DataTrades)
+		GroupV1.POST("/user/dataTradesDetail", v1.DataTradesDetail)
+		GroupV1.POST("/user/remainDataBydDay", v1.RemainDataBydDay)
+
 	}
 }

+ 146 - 0
service/UserOnlineService.go

@@ -0,0 +1,146 @@
+package service
+
+import (
+	"designs/global"
+	"designs/model"
+	"designs/utils"
+	"math"
+	"time"
+)
+
+type logData struct {
+	LogTime time.Time `json:"LogTime"`
+	Type    int       `json:"Type"`
+}
+
+// 查询单日使用时长曲线
+func UserOnlineSummaryByDay(gid string, pf string, startTime string, endTime string) (map[string]int, int, int, error) {
+	var onlineData []model.UserOnline
+	//从数据库中查询出所有的数据
+	query := global.App.DB.Table("user_online").Where("gid", gid).Where("pf", pf)
+
+	if startTime != "" && endTime != "" {
+		query = query.Where("logTime", ">=", startTime).Where("logTime", "<=", endTime)
+	}
+	err := query.Scan(&onlineData).Error
+	if err != nil {
+		return nil, 0, 0, err
+	}
+	//对数据进行分组
+	userOnlineData := make(map[string]map[int][]logData)
+	dateSlice := utils.GetTimeDayDateFormat(startTime, endTime)
+	for _, v := range dateSlice {
+		userOnlineData[v] = make(map[int][]logData)
+	}
+
+	for _, v := range onlineData {
+		date := v.LogTime.Format("2006-01-02")
+		userOnlineData[date][v.UserId] = append(userOnlineData[date][v.UserId], logData{v.LogTime, v.Type})
+	}
+	//计算每天的数据,然后得出平均值
+	var totalAvg int
+	var total int
+	var totalTime int64
+
+	daysAvg := make(map[string]int)
+	for date, daysData := range userOnlineData {
+		var userTotalTime int64
+		//分组后对于每个用户的数据进行处理,得到他们的具体在线时长
+		for _, v := range daysData {
+			userTotalTime = userTotalTime + calculateUserOnlineTime(v)
+		}
+		//计算出平均值,并且格式化
+		if len(daysData) > 0 {
+			avg := float64(userTotalTime) / float64(len(daysData))
+			daysAvg[date] = int(math.Round(avg))
+		} else {
+			daysAvg[date] = 0
+		}
+
+		//统计累加
+		total = total + len(daysData)
+		totalTime = totalTime + userTotalTime
+	}
+	//统计平均值
+	avg := float64(totalTime) / float64(total)
+	totalAvg = int(math.Round(avg))
+
+	return daysAvg, total, totalAvg, nil
+
+}
+
+func UserOnlineSummary(gid string, pf string, date string, startTime string, endTime string) (map[int]int64, error) {
+	var onlineData []model.UserOnline
+	//从数据库中查询出所有的数据
+	query := global.App.DB.Table("user_online").Where("gid", gid).Where("pf", pf)
+	if date != "" {
+		query = query.Where("date", "=", date)
+	}
+
+	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]int64)
+	//分组后对于每个用户的数据进行处理,得到他们的具体在线时长
+	for k, v := range userOnlineData {
+		userLogMsg[k] = calculateUserOnlineTime(v)
+	}
+	userOnlineData = nil
+
+	return userLogMsg, nil
+}
+
+func calculateUserOnlineTime(logData []logData) int64 {
+
+	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 onlineTimeTotal
+}

+ 68 - 0
service/remainData.go

@@ -0,0 +1,68 @@
+package service
+
+import (
+	"designs/global"
+	"designs/model"
+	"designs/utils"
+)
+
+func RemainDataBydDay(pf string, gid string, startTime string, endTime string) (map[string]map[string]interface{}, error) {
+	//先计算出这个时间内 ,每天的注册用户数量
+	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 {
+		return nil, err
+	}
+	var UsersId []int
+	UsersBydDay := utils.GetTimeDayDate(startTime, endTime) //用户分别是在哪天注册的
+	UserLoginBydDay := UsersBydDay
+	for _, user := range users {
+		UsersId = append(UsersId, user.UserId)
+		UsersBydDay[user.CreatedAt.Format("2006-01-02")] = append(UsersBydDay[user.CreatedAt.Format("2006-01-02")], 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 {
+		return nil, err
+	}
+
+	//对这些数据进行整理
+	for _, v := range UserLogin {
+		//根据天进行分组,得出总共有多少
+		times := v.LoginTime.Format("2006-01-02")
+		UserLoginBydDay[times] = append(UserLoginBydDay[times], v.UserId)
+	}
+
+	res := make(map[string]map[string]interface{})
+	//逐天比较注册的数据和留存的数据,得出每天的活跃比例和人数
+	for day, v := range UsersBydDay {
+		remain := make(map[string]interface{})
+		remain["count"] = len(v)
+		remain["date"] = day
+		//分别比较每一天的数据
+		for dayDate, loginDay := range UserLoginBydDay {
+			ok, remainDays := utils.CompareDates(dayDate, day)
+			if !ok {
+				continue
+			}
+			remain[remainDays] = int(utils.IntersectionRate(v, loginDay) * 100)
+		}
+
+		res[day] = remain
+	}
+
+	return res, nil
+}

+ 382 - 0
service/userBehavior.go

@@ -0,0 +1,382 @@
+package service
+
+import (
+	"designs/global"
+	"designs/utils"
+	"time"
+)
+
+var timeDuration = []string{
+	"00.00",
+	"01.00",
+	"02.00",
+	"03.00",
+	"04.00",
+	"05.00",
+	"06.00",
+	"07.00",
+	"08.00",
+	"09.00",
+	"10.00",
+	"11.00",
+	"12.00",
+	"13.00",
+	"14.00",
+	"15.00",
+	"16.00",
+	"17.00",
+	"18.00",
+	"19.00",
+	"20.00",
+	"21.00",
+	"22.00",
+	"23.00",
+}
+
+// 获取新增用户的时段信息
+func GetRegisterTimeDistribution(pf string, gid string) (map[string]int, map[string]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
+	}
+	todayTimeDistributionRes := make(map[string]int)
+	for k, v := range todayTimeDistribution {
+		todayTimeDistributionRes[timeDuration[k]] = v
+	}
+
+	yesterdayTimeDistributionRes := make(map[string]int)
+	for k, v := range yesterdayTimeDistribution {
+		yesterdayTimeDistributionRes[timeDuration[k]] = v
+	}
+
+	return todayTimeDistributionRes, yesterdayTimeDistributionRes, int64(yesterdayCount), int64(todayCount), yesterdayThisTimeCount, nil
+}
+
+// 活跃用户的时段信息
+func GetActiveTimeDistribution(pf string, gid string) (map[string]int, map[string]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)
+	todayTimeDistributionRes := make(map[string]int)
+	for k, v := range todayTimeDistribution {
+		todayTimeDistributionRes[timeDuration[k]] = v
+	}
+
+	yesterdayTimeDistributionRes := make(map[string]int)
+	for k, v := range yesterdayTimeDistribution {
+		yesterdayTimeDistributionRes[timeDuration[k]] = v
+	}
+
+	return todayTimeDistributionRes, yesterdayTimeDistributionRes, yesterdayCount, todayCount, yesterdayThisTimeCount, nil
+}
+
+// 新增设备日趋势图
+func GetRegisterDayDistribution(pf string, gid string, startTime string, endTime string) (map[string]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)
+
+	daysFormat := utils.GetTimeDayDateFormat(startTime, endTime) //用户分别是在哪天注册的
+	todayTimeDistributionRes := make(map[string]int)
+	for k, day := range daysFormat {
+		todayTimeDistributionRes[day] = todayTimeDistribution[k]
+	}
+
+	return todayTimeDistributionRes, len(registerDays), float32(len(registerDays) / len(days)), nil
+}
+
+// 活跃用户趋势图
+func GetActiveDayDistribution(pf string, gid string, startTime string, endTime string) (map[string]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)
+
+	daysFormat := utils.GetTimeDayDateFormat(startTime, endTime) //用户分别是在哪天注册的
+	todayTimeDistributionRes := make(map[string]int)
+	for k, day := range daysFormat {
+		todayTimeDistributionRes[day] = todayTimeDistribution[k]
+	}
+
+	return todayTimeDistributionRes, count, float32(len(activeDays) / len(days)), nil
+}
+
+// 活跃用户周趋势图
+func GetActiveWeekDistribution(pf string, gid string, startTime string, endTime string) (map[string]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)
+
+	daysFormat := utils.GetTimeDayDateFormat(startTime, endTime) //用户分别是在哪天注册的
+	todayTimeDistributionRes := make(map[string]int)
+	for k, day := range daysFormat {
+		todayTimeDistributionRes[day] = todayTimeDistribution[k]
+	}
+
+	return todayTimeDistributionRes, 0, float32(len(activeDays) / len(days)), nil
+}
+
+// 活跃用户月趋势图
+func GetActiveMouthDistribution(pf string, gid string, startTime string, endTime string) (map[string]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)
+	daysFormat := utils.GetTimeDayDateFormat(startTime, endTime) //用户分别是在哪天注册的
+	todayTimeDistributionRes := make(map[string]int)
+	for k, day := range daysFormat {
+		todayTimeDistributionRes[day] = todayTimeDistribution[k]
+	}
+
+	return todayTimeDistributionRes, 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 GetMouthTrade(pf string, gid string) {
+	//获取当前日期
+}
+
+// 根据时间求出时段信息
+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 - 25
utils/array.go

@@ -34,28 +34,38 @@ func ToMap[K comparable, T any](arr []T, getKey func(*T) K) map[K]T {
 	return r
 }
 
-// func InArray(needle interface{}, hystack interface{}) bool {
-// 	switch key := needle.(type) {
-// 	case string:
-// 		for _, item := range hystack.([]string) {
-// 			if key == item {
-// 				return true
-// 			}
-// 		}
-// 	case int:
-// 		for _, item := range hystack.([]int) {
-// 			if key == item {
-// 				return true
-// 			}
-// 		}
-// 	case int64:
-// 		for _, item := range hystack.([]int64) {
-// 			if key == item {
-// 				return true
-// 			}
-// 		}
-// 	default:
-// 		return false
-// 	}
-// 	return false
-// }
+func IntersectionRate(a, b []int) float64 {
+	setA := make(map[int]bool)
+	setB := make(map[int]bool)
+	intersection := make(map[int]bool)
+
+	// 将 a 切片中的元素添加到 setA
+	for _, value := range a {
+		setA[value] = true
+	}
+
+	// 将 b 切片中的元素添加到 setB,并检查是否也在 setA 中
+	for _, value := range b {
+		setB[value] = true
+		if setA[value] {
+			intersection[value] = true
+		}
+	}
+
+	// 计算交集大小
+	intersectionSize := len(intersection)
+
+	// 计算最大切片的大小
+	maxSize := len(a)
+	if len(b) > maxSize {
+		maxSize = len(b)
+	}
+
+	// 计算重合率
+	if maxSize == 0 {
+		return 0.0 // 避免除以零的情况
+	}
+	rate := float64(intersectionSize) / float64(maxSize)
+
+	return rate
+}

+ 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})
+	})
+}

+ 102 - 0
utils/time.go

@@ -0,0 +1,102 @@
+package utils
+
+import (
+	"fmt"
+	"strconv"
+	"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("2006-01-02")
+		days[midnight] = []int{}
+	}
+
+	return days
+}
+
+func GetTimeDayDateFormat(startDate string, endDate string) []string {
+	days := make([]string, 0)
+	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("2006-01-02")
+		days = append(days, midnight)
+	}
+
+	return days
+}
+
+// 时间戳转化为 00:02:37 格式
+func TimeStampToMDS(timestamp int) string {
+
+	hours := timestamp / (60 * 24)
+
+	remainingMinutes := timestamp % (60 * 24)
+
+	minutes := remainingMinutes / 60
+	remainingSeconds := timestamp % 60
+
+	return fmt.Sprintf("%02d:%02d:%02d", hours, minutes, remainingSeconds)
+}
+
+// 比较日期 ,
+func CompareDates(dateA, dateB string) (bool, string) {
+	tA, errA := time.Parse("2006-01-02", dateA)
+	tB, errB := time.Parse("2006-01-02", dateB)
+
+	if errA != nil || errB != nil {
+		return false, ""
+	}
+
+	diff := int(tA.Sub(tB).Hours() / 24)
+
+	valuableDiff := []int{1, 2, 3, 4, 5, 6, 7, 14, 30}
+
+	if InArray(diff, valuableDiff) {
+		return true, "+" + strconv.Itoa(diff) + "day"
+	} else {
+		return false, ""
+	}
+}