diff --git a/app/admin/apis/migumanage/migu_admin.go b/app/admin/apis/migumanage/migu_admin.go index 41d397e..80c1668 100644 --- a/app/admin/apis/migumanage/migu_admin.go +++ b/app/admin/apis/migumanage/migu_admin.go @@ -8,7 +8,10 @@ import ( "github.com/go-admin-team/go-admin-core/sdk/pkg/response" "go-admin/app/admin/models" "net/http" + "sort" + "strconv" "strings" + "sync" "time" ) @@ -295,11 +298,15 @@ func (e MiGuDeployService) OrderList(c *gin.Context) { } // 退订状态 if req.State != 0 { - state := 1 // 订阅 - if req.State == 1 { - state = 2 // 退订 + if req.State == 3 { // 1小时内退订 + qs = qs.Where("is_one_hour_cancel = ?", 1) + } else { + state := 1 // 订阅 + if req.State == 1 { + state = 2 // 退订 + } + qs = qs.Where("state = ?", state) } - qs = qs.Where("state = ?", state) } // 开始时间和结束时间 if req.StartTime != "" && req.EndTime != "" { @@ -351,15 +358,7 @@ func (e MiGuDeployService) OrderList(c *gin.Context) { e.OK(resp, "") } -// HistoricalSummaryList 历史汇总查询 -// @Summary 历史汇总查询 -// @Tags 2024-咪咕-管理后台 -// @Produce json -// @Accept json -// @Param request body models.HistoricalSummaryListReq true "历史汇总查询" -// @Success 200 {object} models.HistoricalSummaryListResp -// @Router /api/v1/admin/historical_summary/list [post] -func (e MiGuDeployService) HistoricalSummaryList(c *gin.Context) { +func (e MiGuDeployService) HistoricalSummaryListOld(c *gin.Context) { fmt.Println("HistoricalSummaryList") err := e.MakeContext(c).MakeOrm().Errors if err != nil { @@ -516,6 +515,490 @@ func (e MiGuDeployService) HistoricalSummaryList(c *gin.Context) { e.OK(resp, "") } +// HistoricalSummaryListNew 历史汇总查询 +// @Summary 历史汇总查询 +// @Tags 2024-咪咕-管理后台 +// @Produce json +// @Accept json +// @Param request body models.HistoricalSummaryListReq true "历史汇总查询" +// @Success 200 {object} models.HistoricalSummaryListResp +// @Router /api/v1/admin/historical_summary/list [post] +func (e MiGuDeployService) HistoricalSummaryListNew(c *gin.Context) { + fmt.Println("HistoricalSummaryList") + + err := e.MakeContext(c).MakeOrm().Errors + if err != nil { + fmt.Println("MakeContext err:", err) + e.Logger.Error(err) + return + } + + // 请求参数绑定 + req := &models.HistoricalSummaryListReq{} + if c.ShouldBindJSON(req) != nil { + logger.Errorf("para err") + response.Error(c, http.StatusBadRequest, errors.New("para err"), "参数错误") + return + } + + resp := models.HistoricalSummaryListResp{ + PageNum: req.PageNum, + } + + // 分页处理 + pageNum := req.PageNum - 1 + if pageNum < 0 { + pageNum = 0 + } + if req.PageSize == 0 { + req.PageSize = 10 + } + resp.PageSize = req.PageSize + + // 获取日期范围 + minDate, maxDate, nCount, err := e.getDateRange(req) + if err != nil { + response.Error(c, http.StatusInternalServerError, err, "获取日期范围失败") + return + } + if nCount == 0 { + e.OK(resp, "") + return + } + + var startTime, endTime string + if req.IsExport == 1 { + if startTime != "" && endTime != "" { + startDate, err := time.Parse("2006-01-02", startTime) + if err != nil { + response.Error(c, http.StatusInternalServerError, err, "获取日期范围失败") + return + } + endDate, err := time.Parse("2006-01-02", endTime) + if err != nil { + response.Error(c, http.StatusInternalServerError, err, "获取日期范围失败") + return + } + + startTime = startDate.Format("2006-01-02") + " 00:00:00" + endTime = endDate.Format("2006-01-02") + " 23:59:59" + } else { + startTime = minDate + " 00:00:00" + endTime = maxDate + " 23:59:59" + } + } else { + // 处理分页日期范围 + startTime, endTime, err = e.processTimeRange(req.StartTime, req.EndTime, minDate, maxDate, pageNum) + if err != nil { + response.Error(c, http.StatusBadRequest, err, "时间范围无效") + return + } + } + + fmt.Println("Start Time:", startTime) + fmt.Println("End Time:", endTime) + + // 查询数据 + data, err := e.queryHistoricalSummary(startTime, endTime, req) + if err != nil { + response.Error(c, http.StatusInternalServerError, err, "查询失败") + return + } + + if req.IsExport != 1 { + // 补充缺失的日期 + dateList, err := e.getDateList(startTime, endTime) + if err != nil { + response.Error(c, http.StatusBadRequest, err, "日期格式无效") + return + } + + finalList := e.fillMissingDates(dateList, data) + resp.List = finalList + } else { + resp.List = data + } + + // 计算总条数 + if req.StartTime != "" && req.EndTime != "" { + // 解析日期 + startDate, _ := time.Parse(models.MiGuTimeFormat, req.StartTime) + endDate, _ := time.Parse(models.MiGuTimeFormat, req.EndTime) + + // 计算日期差 + daysDiff := int(endDate.Sub(startDate).Hours() / 24) + resp.Count = daysDiff + 1 + } else { + resp.Count = nCount + } + + // 导出Excel + if req.IsExport == 1 { + url, err := models.ExportHistoricalSummaryToExcel(resp.List, e.Orm) + if err != nil { + response.Error(c, http.StatusInternalServerError, err, "导出失败") + return + } + response.OK(c, map[string]string{"export_url": url}, "导出成功") + return + } + + // 返回结果 + e.OK(resp, "") +} + +// 获取最早和最晚日期 +func (e MiGuDeployService) getDateRange(req *models.HistoricalSummaryListReq) (string, string, int, error) { + var result struct { + MinDate string `json:"min_date"` + MaxDate string `json:"max_date"` + } + + qs := e.Orm.Model(&models.MgOrder{}). + Select("MIN(DATE(mg_order.subscribe_time)) AS min_date, MAX(DATE(mg_order.subscribe_time)) AS max_date") + if req.SkuCode != 0 { + qs = qs.Where("product_id = ?", req.SkuCode) + } + if req.Channel != "" { + qs = qs.Where("channel_code = ?", req.Channel) + } + + err := qs.Scan(&result).Error + if err != nil { + return "", "", 0, err + } + + if result.MinDate == "" || result.MaxDate == "" { + return "", "", 0, nil + } + + // 解析日期 + minDate, err := time.Parse("2006-01-02", result.MinDate) + if err != nil { + return "", "", 0, fmt.Errorf("failed to parse min_date: %v", err) + } + + maxDate, err := time.Parse("2006-01-02", result.MaxDate) + if err != nil { + return "", "", 0, fmt.Errorf("failed to parse max_date: %v", err) + } + + // 计算日期差 + daysDiff := int(maxDate.Sub(minDate).Hours() / 24) + if daysDiff != 0 { + daysDiff += 1 + } + + return result.MinDate, result.MaxDate, daysDiff, nil +} + +// 处理分页日期范围 +func (e MiGuDeployService) processTimeRange(startTime, endTime, minDate, maxDate string, pageNum int) (string, string, error) { + var err error + var startDate, endDate time.Time + + if startTime != "" && endTime != "" { + startDate, err = time.Parse(models.MiGuTimeFormat, startTime) + if err != nil { + return "", "", fmt.Errorf("startTime 格式无效") + } + endDate, err = time.Parse(models.MiGuTimeFormat, endTime) + if err != nil { + return "", "", fmt.Errorf("endTime 格式无效") + } + } else { + if minDate == "" || maxDate == "" { + return "", "", fmt.Errorf("没有数据") + } + startDate, err = time.Parse("2006-01-02", minDate) + if err != nil { + return "", "", fmt.Errorf("minDate 格式无效") + } + + endDate, err = time.Parse("2006-01-02", maxDate) + if err != nil { + return "", "", fmt.Errorf("maxDate 格式无效") + } + } + + pageStartDate := endDate.AddDate(0, 0, -(pageNum+1)*10+1) + pageEndDate := pageStartDate.AddDate(0, 0, 9) + + if pageStartDate.Before(startDate) { + pageStartDate = startDate + } + + if pageEndDate.After(endDate) { + pageEndDate = endDate + } + + startTime = pageStartDate.Format("2006-01-02") + " 00:00:00" + endTime = pageEndDate.Format("2006-01-02") + " 23:59:59" + + return startTime, endTime, nil +} + +//// 查询历史数据 +//func (e MiGuDeployService) queryHistoricalSummary(startTime, endTime string, req *models.HistoricalSummaryListReq) ([]models.MgHistoricalSummary, error) { +// qs := e.Orm.Model(&models.MgOrder{}). +// Select(`DATE_FORMAT(mg_order.subscribe_time, '%Y-%m-%d') AS date, +// mg_order.product_id, +// mg_order.channel_code, +// (SELECT COUNT(*) +// FROM mg_transaction_log +// WHERE verification_code != '' +// AND channel_code = mg_order.channel_code +// AND DATE(created_at) = DATE(mg_order.subscribe_time)) AS submission_count, +// COUNT(CASE WHEN mg_order.is_one_hour_cancel = 1 THEN 1 END) AS new_user_unsub_within_hour, +// COUNT(CASE WHEN mg_order.state = 2 AND mg_order.unsubscribe_time >= ? AND mg_order.unsubscribe_time <= ? THEN 1 END) AS new_user_unsub_on_day, +// COUNT(*) AS new_user_count, +// CONCAT(ROUND(COUNT(CASE WHEN mg_order.is_one_hour_cancel = 1 THEN 1 END) * 100.0 / +// NULLIF(COUNT(*), 0), 2), '%') AS new_user_unsub_within_hour_rate, +// CONCAT(ROUND(COUNT(CASE WHEN mg_order.state = 2 AND mg_order.unsubscribe_time >= ? AND mg_order.unsubscribe_time <= ? THEN 1 END) * 100.0 / +// NULLIF(COUNT(*), 0), 2), '%') AS new_user_unsub_on_day_rate, +// IFNULL( +// CONCAT( +// LEAST( +// ROUND(COUNT(*) * 100.0 / NULLIF( +// (SELECT COUNT(*) +// FROM mg_transaction_log +// WHERE verification_code != '' +// AND channel_code = mg_order.channel_code +// AND DATE(created_at) = DATE(mg_order.subscribe_time)), 0), 2), +// 100.00 +// ), +// '%' +// ), +// '0.00%' +// ) AS submission_success_rate`, +// startTime, time.Now(), startTime, time.Now()). +// Where("mg_order.subscribe_time >= ? AND mg_order.subscribe_time <= ?", startTime, endTime). +// Group("DATE(mg_order.subscribe_time), mg_order.product_id, mg_order.channel_code"). +// Order("mg_order.product_id, mg_order.channel_code, mg_order.subscribe_time DESC") +// +// if req.SkuCode != 0 { +// qs = qs.Where("product_id = ?", req.SkuCode) +// } +// if req.Channel != "" { +// qs = qs.Where("channel_code = ?", req.Channel) +// } +// +// var err error +// var data []models.MgHistoricalSummary +// if req.IsExport == 1 { +// err = qs.Find(&data).Error +// } else { +// err = qs.Limit(req.PageSize).Find(&data).Error +// } +// if err != nil { +// return nil, err +// } +// +// return data, nil +//} + +// 查询历史数据 +func (e MiGuDeployService) queryHistoricalSummary(startTime, endTime string, req *models.HistoricalSummaryListReq) ([]models.MgHistoricalSummary, error) { + qs := e.Orm.Model(&models.MgOrder{}). + Select(` + DATE_FORMAT(mg_order.subscribe_time, '%Y-%m-%d') AS date, + mg_order.product_id, + mg_order.channel_code, + ( + SELECT COUNT(*) + FROM mg_transaction_log + WHERE verification_code != '' + AND channel_code = mg_order.channel_code + AND DATE(created_at) = DATE(mg_order.subscribe_time) + ) AS submission_count, + COUNT(CASE WHEN mg_order.is_one_hour_cancel = 1 THEN 1 END) AS new_user_unsub_within_hour, + COUNT(CASE WHEN mg_order.state = 2 AND DATE(mg_order.unsubscribe_time) = DATE(mg_order.subscribe_time) THEN 1 END) AS new_user_unsub_on_day, + COUNT(*) AS new_user_count, + SUM(CASE WHEN mg_order.state = 2 THEN 1 ELSE 0 END) AS total_new_user_unsub, + CONCAT(ROUND(COUNT(CASE WHEN mg_order.is_one_hour_cancel = 1 THEN 1 END) * 100.0 / NULLIF(COUNT(*), 0), 2), '%') AS new_user_unsub_within_hour_rate, + CONCAT(ROUND(COUNT(CASE WHEN mg_order.state = 2 AND DATE(mg_order.unsubscribe_time) = DATE(mg_order.subscribe_time) THEN 1 END) * 100.0 / NULLIF(COUNT(*), 0), 2), '%') AS new_user_unsub_on_day_rate, + CONCAT(ROUND(SUM(CASE WHEN mg_order.state = 2 THEN 1 ELSE 0 END) * 100.0 / NULLIF(COUNT(*), 0), 2), '%') AS total_new_user_unsub_rate, + IFNULL( + CONCAT( + LEAST( + ROUND(COUNT(*) * 100.0 / NULLIF( + (SELECT COUNT(*) + FROM mg_transaction_log + WHERE verification_code != '' + AND channel_code = mg_order.channel_code + AND DATE(created_at) = DATE(mg_order.subscribe_time)), 0), 2), + 100.00 + ), + '%' + ), + '0.00%' + ) AS submission_success_rate + `). + Where("mg_order.subscribe_time BETWEEN ? AND ?", startTime, endTime). + Group("DATE(mg_order.subscribe_time), mg_order.product_id, mg_order.channel_code"). + Order("mg_order.product_id, mg_order.channel_code, mg_order.subscribe_time DESC") + + if req.SkuCode != 0 { + qs = qs.Where("product_id = ?", req.SkuCode) + } + if req.Channel != "" { + qs = qs.Where("channel_code = ?", req.Channel) + } + + var data []models.MgHistoricalSummary + var err error + if req.IsExport == 1 { + err = qs.Find(&data).Error + } else { + err = qs.Limit(req.PageSize).Find(&data).Error + } + if err != nil { + return nil, err + } + + return data, nil +} + +// 获取日期列表 +func (e MiGuDeployService) getDateList(startTime, endTime string) ([]string, error) { + var dateList []string + + currentDate, err := time.Parse("2006-01-02 15:04:05", startTime) + if err != nil { + return nil, fmt.Errorf("时间格式无效") + } + + endDate, err := time.Parse("2006-01-02 15:04:05", endTime) + if err != nil { + return nil, fmt.Errorf("结束时间格式无效") + } + + for currentDate.Before(endDate) { + dateList = append(dateList, currentDate.Format("2006-01-02")) + currentDate = currentDate.AddDate(0, 0, 1) + } + + // 反转切片,确保日期是倒序的 + for i := 0; i < len(dateList)/2; i++ { + j := len(dateList) - i - 1 + dateList[i], dateList[j] = dateList[j], dateList[i] + } + + return dateList, nil +} + +// 填充缺失日期 +func (e MiGuDeployService) fillMissingDates(dateList []string, data []models.MgHistoricalSummary) []models.MgHistoricalSummary { + dateMap := make(map[string]models.MgHistoricalSummary) + for _, d := range data { + dateMap[d.Date] = d + } + + var result []models.MgHistoricalSummary + for _, date := range dateList { + if summary, ok := dateMap[date]; ok { + result = append(result, summary) + } else { + result = append(result, models.MgHistoricalSummary{Date: date}) // 日期没有数据时,返回空数据 + } + } + return result +} + +// 获取日期列表 +func (e MiGuDeployService) getDateListOnHome(startTime, endTime string) ([]string, error) { + var dateList []string + + currentDate, err := time.Parse("2006-01-02 15:04:05", startTime) + if err != nil { + return nil, fmt.Errorf("时间格式无效") + } + + endDate, err := time.Parse("2006-01-02 15:04:05", endTime) + if err != nil { + return nil, fmt.Errorf("结束时间格式无效") + } + + for currentDate.Before(endDate) { + dateList = append(dateList, currentDate.Format("2006-01-02")) + currentDate = currentDate.AddDate(0, 0, 1) + } + + return dateList, nil +} + +// 填充缺失日期 +func (e MiGuDeployService) fillMissingDatesOnHome(dateList []string, data []models.DailyData) []models.DailyData { + dateMap := make(map[string]models.DailyData) + for _, d := range data { + dateMap[d.Date] = d + } + + var result []models.DailyData + for _, date := range dateList { + if summary, ok := dateMap[date]; ok { + result = append(result, summary) + } else { + result = append(result, models.DailyData{ + Date: date, + NewUserCount: 0, + UnsubscribedUserCount: 0, + UnsubscribedWithinOneHour: 0, + UnsubscribeRate: "0.00%", + UnsubscribeWithinOneHourRate: "0.00%", + TotalCancelCount: 0, + }) // 日期没有数据时,返回空数据 + } + } + return result +} + +// 获取最早和最晚日期 +func (e MiGuDeployService) getDateRangeOnHome(req *models.HomepageDataSummaryReq) (time.Time, time.Time, int, error) { + var minDate, maxDate time.Time + var result struct { + MinDate string `json:"min_date"` + MaxDate string `json:"max_date"` + } + + qs := e.Orm.Model(&models.MgOrder{}). + Select("MIN(DATE(mg_order.subscribe_time)) AS min_date, MAX(DATE(mg_order.subscribe_time)) AS max_date") + if req.ProductID != 0 { + qs = qs.Where("product_id = ?", req.ProductID) + } + if req.Channel != "" { + qs = qs.Where("channel_code = ?", req.Channel) + } + + err := qs.Scan(&result).Error + if err != nil { + return minDate, maxDate, 0, err + } + + if result.MinDate == "" || result.MaxDate == "" { + return minDate, maxDate, 0, err + } + + // 解析日期 + minDate, err = time.Parse("2006-01-02", result.MinDate) + if err != nil { + return minDate, maxDate, 0, fmt.Errorf("failed to parse min_date: %v", err) + } + + maxDate, err = time.Parse("2006-01-02", result.MaxDate) + if err != nil { + return minDate, maxDate, 0, fmt.Errorf("failed to parse max_date: %v", err) + } + + // 计算日期差 + daysDiff := int(maxDate.Sub(minDate).Hours() / 24) + if daysDiff != 0 { + daysDiff += 1 + } + + return minDate, maxDate, daysDiff, nil +} + // RealtimeSummaryList 当日实时汇总 // @Summary 当日实时汇总 // @Tags 2024-咪咕-管理后台 @@ -721,7 +1204,7 @@ func (e MiGuDeployService) UserRetentionList(c *gin.Context) { //} // 处理产品编号 (SkuCode) 过滤条件 - if req.SkuCode != "" { + if req.SkuCode != 0 { qs = qs.Where("product_id = ?", req.SkuCode) } @@ -765,6 +1248,63 @@ func (e MiGuDeployService) UserRetentionList(c *gin.Context) { retentionList[i].RetentionRate += "%" } + // 查询对应月份的最近2个月1号留存数据 + // 现在遍历 retentionList,查询每个月份对应的最近1个月和最近2个月的留存数据 + for i := range retentionList { + // 获取当前记录的 RetentionMonth + year, month, _ := models.ParseYearMonth(retentionList[i].RetentionMonth) + + // 计算当前月1号和上个月1号 + currentYear := time.Now().Year() + currentMonthFirstDay := time.Date(year, month, 1, 0, 0, 0, 0, time.Local) // 统计月的1号 + currentMonthNextFirstDay := time.Date(year, month+1, 1, 0, 0, 0, 0, time.Local) // 统计月的次月1号 + lastMonthFirstDay := time.Date(time.Now().Year(), time.Now().Month(), 1, 0, 0, 0, 0, time.Local) // 当前月1号 + lastTwoMonthFirstDay := lastMonthFirstDay.AddDate(0, -1, 0) // 上个月1号 + + // 计算需要的时间变量:当月最后一天、上月2号、上月最后一天、上上月2号等 + lastMonthSecondDay := lastMonthFirstDay.AddDate(0, 0, 1) // 上个月2号 + lastTwoMonthSecondDay := lastTwoMonthFirstDay.AddDate(0, 0, 1) // 上上个月2号 + + // 获取最近1个月留存用户数 + var lastMonthRetentionCount int64 + if currentYear == year && month == time.Now().Month() { + lastMonthRetentionCount = 0 + } else { + err = e.Orm.Model(&models.MgOrder{}). + Where("state = 1 AND created_at between ? and ?", currentMonthFirstDay, currentMonthNextFirstDay). + Or("state = 2 AND unsubscribe_time > ? AND created_at between ? and ?", lastMonthSecondDay, currentMonthFirstDay, currentMonthNextFirstDay). + Count(&lastMonthRetentionCount).Error + } + + // 获取最近2个月留存用户数 + var lastTwoMonthRetentionCount int64 + if currentYear == year && month == time.Now().Month() { + lastTwoMonthRetentionCount = 0 + } else if currentYear == year && month == time.Now().Month()-1 { + lastTwoMonthRetentionCount = 0 + } else { + err = e.Orm.Model(&models.MgOrder{}). + Where("state = 1 AND created_at between ? and ?", currentMonthFirstDay, currentMonthNextFirstDay). + Or("state = 2 AND unsubscribe_time > ? AND created_at between ? and ?", lastTwoMonthSecondDay, currentMonthFirstDay, currentMonthNextFirstDay). + Count(&lastTwoMonthRetentionCount).Error + } + + // 设置最近1个月和最近2个月的留存数 + retentionList[i].LastMonthDate = lastMonthFirstDay.Format("2006-01-02") + retentionList[i].LastTwoMonthDate = lastTwoMonthFirstDay.Format("2006-01-02") + retentionList[i].LastMonthRetentionCount = int(lastMonthRetentionCount) + retentionList[i].LastTwoMonthRetentionCount = int(lastTwoMonthRetentionCount) + + // 计算最近1个月和最近2个月的留存率 + if retentionList[i].NewUserCount > 0 { + lastMonthRetentionRate := float64(lastMonthRetentionCount) / float64(retentionList[i].NewUserCount) * 100 + lastTwoMonthRetentionRate := float64(lastTwoMonthRetentionCount) / float64(retentionList[i].NewUserCount) * 100 + + retentionList[i].LastMonthRetentionRate = fmt.Sprintf("%.2f%%", lastMonthRetentionRate) + retentionList[i].LastTwoMonthRetentionRate = fmt.Sprintf("%.2f%%", lastTwoMonthRetentionRate) + } + } + resp.List = retentionList resp.Count = len(retentionList) // Count should reflect the actual number of entries @@ -772,6 +1312,257 @@ func (e MiGuDeployService) UserRetentionList(c *gin.Context) { response.OK(c, resp, "成功") } +// UserDayRetentionList 用户留存记录(按天) +// @Summary 用户留存记录(按天) +// @Tags 2024-咪咕-管理后台 +// @Produce json +// @Accept json +// @Param request body models.UserDayRetentionListReq true "用户留存记录(按天)" +// @Success 200 {object} models.UserDayRetentionListResp +// @Router /api/v1/admin/user_day_retention/list [post] +func (e MiGuDeployService) UserDayRetentionList(c *gin.Context) { + // 创建上下文和 ORM + ctx := e.MakeContext(c) + if err := ctx.MakeOrm().Errors; err != nil { + e.Logger.Error(err) + response.Error(c, http.StatusInternalServerError, err, "数据库错误") + return + } + + // 解析请求参数 + req := &models.UserDayRetentionListReq{} + if err := c.ShouldBindJSON(req); err != nil { + response.Error(c, http.StatusBadRequest, err, "参数错误") + return + } + + resp := models.UserDayRetentionListResp{ + PageNum: req.PageNum, + } + + // 分页处理 + pageNum := req.PageNum - 1 + if pageNum < 0 { + pageNum = 0 + } + if req.PageSize == 0 { + req.PageSize = 10 + } + resp.PageSize = req.PageSize + + // 获取新增用户数(在RetentionMonth的所有用户) + var newUserCount int64 + err := e.Orm.Table("mg_order"). + Where("created_at >= ?", req.RetentionMonth+"-01 00:00:00"). + Where("created_at < ?", req.RetentionMonth+"-31 23:59:59"). + Count(&newUserCount).Error + if err != nil { + e.Logger.Error(err) + response.Error(c, http.StatusInternalServerError, err, "获取新增用户数失败") + return + } + + // 如果没有新增用户,则直接返回空结果 + if newUserCount == 0 { + resp.List = []models.MgUserDayRetention{} + resp.Count = 0 + e.OK(resp, "success") + return + } + + // 计算下个月的1号(留存查询从下个月1号开始) + parsedMonth, err := time.Parse("2006-01", req.RetentionMonth) + if err != nil { + e.Logger.Error(err) + response.Error(c, http.StatusBadRequest, err, "解析RetentionMonth失败") + return + } + + // 获取下个月的1号 + nextMonth := parsedMonth.AddDate(0, 1, 0) // 加一个月 + startDate := nextMonth.Format("2006-01-02") // 下个月1号 + + // 获取当前日期,作为退出条件 + currentDate := time.Now().Format("2006-01-02") + + // 获取当前记录的 RetentionMonth + year, month, _ := models.ParseYearMonth(req.RetentionMonth) + + // 计算当前月1号和上个月1号 + currentMonthFirstDay := time.Date(year, month, 1, 0, 0, 0, 0, time.Local) // 统计月的1号 + currentMonthNextFirstDay := time.Date(year, month+1, 1, 0, 0, 0, 0, time.Local) // 统计月的次月1号 + + // 初始化结果列表 + var retentionData []models.MgUserDayRetention + + // 使用 goroutine 处理并发查询 + var wg sync.WaitGroup + // 创建一个 channel 来接收每一天的查询结果 + resultChan := make(chan models.MgUserDayRetention, 100) + + // 控制并发数的最大值,避免数据库过载 + sem := make(chan struct{}, 10) // 同时最多 10 个 goroutine 执行 + + // 根据 OnlyFirstDay 参数调整查询逻辑 + if req.OnlyFirstDay { + // 如果只查询每个月1号的数据 + for { + // 增加日期(开始日期自增一个月的1号) + startDateTime, _ := time.Parse("2006-01-02", startDate) + + // 如果当前日期大于等于startDate,且是查询下一个月的1号,则退出 + if startDateTime.After(time.Now()) { + break + } + + wg.Add(1) + sem <- struct{}{} // 占一个信号,表示有一个 goroutine 正在执行 + + go func(date string) { + defer wg.Done() + defer func() { <-sem }() // 释放一个信号,表示该 goroutine 执行完毕 + + var retainedUserCount int64 + var unsubOnDay int64 + + // 查询该天的留存用户数 + err = e.Orm.Model(&models.MgOrder{}). + Where("state = 1 AND created_at between ? and ?", currentMonthFirstDay, currentMonthNextFirstDay). + Or("state = 2 AND unsubscribe_time > ? AND created_at between ? and ?", date+" 23:59:59", currentMonthFirstDay, currentMonthNextFirstDay). + Count(&retainedUserCount).Error + + // 查询该天的退订用户数 + err = e.Orm.Table("mg_order"). + Where("unsubscribe_time >= ?", date+" 00:00:00"). + Where("unsubscribe_time <= ?", date+" 23:59:59"). + Where("created_at >= ?", currentMonthFirstDay). + Where("created_at < ?", currentMonthNextFirstDay). + Where("state = 2"). // 状态为2表示退订 + Count(&unsubOnDay).Error + + if err != nil { + e.Logger.Error(err) + return + } + + // 留存率计算:如果新增用户数为0,留存率为0 + var retentionRate string + if newUserCount > 0 { + retentionRate = fmt.Sprintf("%.2f%%", float64(retainedUserCount)*100/float64(newUserCount)) + } else { + retentionRate = "0.00%" + } + + // 将查询结果发送到 resultChan + resultChan <- models.MgUserDayRetention{ + Date: date, + RetainedUserCount: int(retainedUserCount), + RetentionRate: retentionRate, + UserUnsubOnDay: int(unsubOnDay), + } + }(startDate) + + // 增加日期(开始日期自增一个月) + startDate = startDateTime.AddDate(0, 1, 0).Format("2006-01-02") + } + } else { + // 按天查询,直到留存数为 0 或查询到当前日期 + for { + // 增加日期(开始日期自增一天) + startDateTime, _ := time.Parse("2006-01-02", startDate) + if startDate > currentDate { + break + } + + wg.Add(1) + sem <- struct{}{} // 占一个信号,表示有一个 goroutine 正在执行 + + go func(date string) { + defer wg.Done() + defer func() { <-sem }() // 释放一个信号,表示该 goroutine 执行完毕 + + var retainedUserCount int64 + var unsubOnDay int64 + + // 查询该天的留存用户数 + err = e.Orm.Model(&models.MgOrder{}). + Where("state = 1 AND created_at between ? and ?", currentMonthFirstDay, currentMonthNextFirstDay). + Or("state = 2 AND unsubscribe_time > ? AND created_at between ? and ?", date+" 23:59:59", currentMonthFirstDay, currentMonthNextFirstDay). + Count(&retainedUserCount).Error + + // 查询该天的退订用户数 + err = e.Orm.Table("mg_order"). + Where("unsubscribe_time >= ?", date+" 00:00:00"). + Where("unsubscribe_time <= ?", date+" 23:59:59"). + Where("created_at >= ?", currentMonthFirstDay). + Where("created_at < ?", currentMonthNextFirstDay). + Where("state = 2"). // 状态为2表示退订 + Count(&unsubOnDay).Error + + if err != nil { + e.Logger.Error(err) + return + } + + // 留存率计算:如果新增用户数为0,留存率为0 + var retentionRate string + if newUserCount > 0 { + retentionRate = fmt.Sprintf("%.2f%%", float64(retainedUserCount)*100/float64(newUserCount)) + } else { + retentionRate = "0.00%" + } + + // 将查询结果发送到 resultChan + resultChan <- models.MgUserDayRetention{ + Date: date, + RetainedUserCount: int(retainedUserCount), + RetentionRate: retentionRate, + UserUnsubOnDay: int(unsubOnDay), + } + }(startDate) + + // 增加日期(开始日期自增一天) + startDate = startDateTime.AddDate(0, 0, 1).Format("2006-01-02") + } + } + + // 等待所有 goroutine 执行完毕 + wg.Wait() + close(resultChan) + + // 从 channel 中获取所有查询结果,并汇总 + for result := range resultChan { + retentionData = append(retentionData, result) + } + + // 排序(按日期升序) + sort.SliceStable(retentionData, func(i, j int) bool { + return retentionData[i].Date > retentionData[j].Date + }) + + // 分页处理 + totalRecords := len(retentionData) // 总记录数 + startIdx := pageNum * req.PageSize + endIdx := startIdx + req.PageSize + + if startIdx > totalRecords { + resp.List = []models.MgUserDayRetention{} + resp.Count = totalRecords + e.OK(resp, "success") + return + } + + if endIdx > totalRecords { + endIdx = totalRecords + } + + // 返回分页后的数据 + resp.List = retentionData[startIdx:endIdx] + resp.Count = totalRecords + + e.OK(resp, "success") +} + // SysChannelList 查询系统渠道编码 // @Summary 查询系统渠道编码 // @Tags 2024-咪咕-管理后台 @@ -971,7 +1762,7 @@ func (e MiGuDeployService) HomepageDataSummary(c *gin.Context) { // 查询每日数据 var dailyDataList []models.DailyData dailyQuery := e.Orm.Model(&models.MgOrder{}). - Select(`DATE(subscribe_time) AS date, + Select(`DATE_FORMAT(subscribe_time, '%Y-%m-%d') AS date, COUNT(*) AS new_user_count, COUNT(CASE WHEN state = 2 THEN 1 END) AS unsubscribed_user_count, COUNT(CASE WHEN is_one_hour_cancel = 1 THEN 1 END) AS unsubscribed_within_one_hour`). @@ -1007,6 +1798,54 @@ func (e MiGuDeployService) HomepageDataSummary(c *gin.Context) { } } + // 获取日期范围 + minDate, maxDate, nCount, err := e.getDateRangeOnHome(req) + if err != nil { + response.Error(c, http.StatusInternalServerError, err, "获取日期范围失败") + return + } + if nCount == 0 { + e.OK(resp, "") + return + } + + if req.StartTime != "" && req.EndTime != "" { + startDate, err := time.Parse(models.MiGuTimeFormat, req.StartTime) + if err != nil { + response.Error(c, http.StatusInternalServerError, err, "获取日期范围失败") + return + } + endDate, err := time.Parse(models.MiGuTimeFormat, req.EndTime) + if err != nil { + response.Error(c, http.StatusInternalServerError, err, "获取日期范围失败") + return + } + + if startDate.Before(minDate) { + startDate = minDate + } + + if endDate.After(maxDate) { + endDate = maxDate + } + + startTime = startDate.Format("2006-01-02") + " 00:00:00" + endTime = endDate.Format("2006-01-02") + " 23:59:59" + } else { + startTime = minDate.Format("2006-01-02") + " 00:00:00" + endTime = time.Now().Format(models.MiGuTimeFormat) + } + + // 补充缺失的日期 + dateList, err := e.getDateListOnHome(startTime, endTime) + if err != nil { + response.Error(c, http.StatusBadRequest, err, "日期格式无效") + return + } + + finalList := e.fillMissingDatesOnHome(dateList, dailyDataList) + dailyDataList = finalList + // 查询每日退订合计 // 增加TotalCancelCount字段的查询 var dailyCancelDataList []struct { @@ -1015,7 +1854,7 @@ func (e MiGuDeployService) HomepageDataSummary(c *gin.Context) { } cancelQuery := e.Orm.Model(&models.MgOrder{}). - Select(`DATE(unsubscribe_time) AS date, COUNT(*) AS total_cancel_count`). + Select(`DATE_FORMAT(unsubscribe_time, '%Y-%m-%d') AS date, COUNT(*) AS total_cancel_count`). Where("state = 2 AND unsubscribe_time IS NOT NULL AND unsubscribe_time >= ? AND unsubscribe_time <= ?", startTime, endTime) // 处理产品编号 (SkuCode) 过滤条件 @@ -1626,3 +2465,253 @@ func (e MiGuDeployService) AddProduct(c *gin.Context) { // // 返回成功消息 // c.JSON(http.StatusOK, gin.H{"message": "导入成功"}) //} + +// HourSummaryList 历史汇总(按小时) +// @Summary 历史汇总(按小时) +// @Tags 2024-咪咕-管理后台 +// @Produce json +// @Accept json +// @Param request body models.HistoricalSummaryListReq true "历史汇总(按小时)" +// @Success 200 {object} models.HourSummaryListResp +// @Router /api/v1/admin/hour_summary/list [post] +func (e MiGuDeployService) HourSummaryList(c *gin.Context) { + fmt.Println("HourSummaryList") + + err := e.MakeContext(c).MakeOrm().Errors + if err != nil { + fmt.Println("MakeContext err:", err) + e.Logger.Error(err) + return + } + + // 请求参数绑定 + req := &models.HistoricalSummaryListReq{} + if c.ShouldBindJSON(req) != nil { + logger.Errorf("para err") + response.Error(c, http.StatusBadRequest, errors.New("para err"), "参数错误") + return + } + + resp := models.HourSummaryListResp{ + PageNum: req.PageNum, + } + + // 分页处理 + pageNum := req.PageNum - 1 + if pageNum < 0 { + pageNum = 0 + } + if req.PageSize == 0 { + req.PageSize = 10 + } + resp.PageSize = req.PageSize + + // 设置开始时间和结束时间 + startTime := req.StartTime + endTime := req.EndTime + if startTime == "" { + startTime = "1970-01-01 00:00:00" + } + if endTime == "" { + endTime = time.Now().Format("2006-01-02 15:04:05") + } + + data, sumData, nCount, err := e.queryHistoricalDataByHour(startTime, endTime, req) + if err != nil { + response.Error(c, http.StatusInternalServerError, err, "查询失败") + return + } + + resp.List = data + + // 批量赋值操作 + var totalData models.TotalHourSummary + totalData.SubmissionCount = sumData.SubmissionCount + totalData.NewUserCount = sumData.NewUserCount + totalData.SubmissionSuccessRate = sumData.SubmissionSuccessRate + totalData.NewUserUnsubWithinHour = sumData.NewUserUnsubWithinHour + totalData.NewUserUnsubWithinHourRate = sumData.NewUserUnsubWithinHourRate + totalData.NewUserUnsubOnDay = sumData.NewUserUnsubOnDay + totalData.NewUserUnsubOnDayRate = sumData.NewUserUnsubOnDayRate + totalData.TotalNewUserUnsub = sumData.TotalNewUserUnsub + totalData.TotalNewUserUnsubRate = sumData.TotalNewUserUnsubRate + + if req.IsExport == 1 { + url, err := models.ExportHourSummaryToExcel(data, totalData, e.Orm) + if err != nil { + response.Error(c, http.StatusInternalServerError, err, "导出失败") + return + } + response.OK(c, map[string]string{"export_url": url}, "导出成功") + return + } + + resp.SummaryData = &totalData + resp.Count = nCount + + // 返回结果 + e.OK(resp, "") +} + +// 查询历史数据(按小时),并计算退订率和提交成功率 +func (e MiGuDeployService) queryHistoricalDataByHour(startTime, endTime string, req *models.HistoricalSummaryListReq) ([]models.MgHourSummary, *models.MgHourSummary, int, error) { + // 构建SQL查询字符串 + sql := ` + SELECT * FROM ( + SELECT + hour, + product_id, + channel_code, + SUM(new_user_count) AS new_user_count, + SUM(new_user_unsub_within_hour) AS new_user_unsub_within_hour, + SUM(new_user_unsub_on_day) AS new_user_unsub_on_day, + SUM(total_new_user_unsub) AS total_new_user_unsub, + SUM(submission_count) AS submission_count, + IF(SUM(submission_count) > 0, + CONCAT(ROUND(SUM(new_user_count) / SUM(submission_count) * 100, 2), '%'), + '0.00%' + ) AS submission_success_rate, + IF(SUM(new_user_count) > 0, + CONCAT(ROUND(SUM(new_user_unsub_within_hour) / SUM(new_user_count) * 100, 2), '%'), + '0.00%' + ) AS new_user_unsub_within_hour_rate, + IF(SUM(new_user_count) > 0, + CONCAT(ROUND(SUM(new_user_unsub_on_day) / SUM(new_user_count) * 100, 2), '%'), + '0.00%' + ) AS new_user_unsub_on_day_rate, + IF(SUM(new_user_count) > 0, + CONCAT(ROUND(SUM(total_new_user_unsub) / SUM(new_user_count) * 100, 2), '%'), + '0.00%' + ) AS total_new_user_unsub_rate + FROM ( + -- 第一个查询:mg_order 数据 + SELECT + HOUR(mg_order.created_at) AS hour, + product_id, + channel_code, + COUNT(*) AS new_user_count, + SUM(CASE WHEN mg_order.is_one_hour_cancel = 1 THEN 1 ELSE 0 END) AS new_user_unsub_within_hour, + SUM(CASE WHEN mg_order.state = 2 AND DATE(mg_order.unsubscribe_time) = DATE(mg_order.subscribe_time) THEN 1 ELSE 0 END) AS new_user_unsub_on_day, + SUM(CASE WHEN mg_order.state = 2 THEN 1 ELSE 0 END) AS total_new_user_unsub, + 0 AS submission_count + FROM + mg_order + WHERE + mg_order.created_at BETWEEN ? AND ? + GROUP BY + HOUR(mg_order.created_at) + UNION ALL + -- 第二个查询:mg_order 总计数据 + SELECT + 'Total' AS hour, + product_id, + channel_code, + COUNT(*) AS new_user_count, + SUM(CASE WHEN mg_order.is_one_hour_cancel = 1 THEN 1 ELSE 0 END) AS new_user_unsub_within_hour, + SUM(CASE WHEN mg_order.state = 2 AND DATE(mg_order.unsubscribe_time) = DATE(mg_order.subscribe_time) THEN 1 ELSE 0 END) AS new_user_unsub_on_day, + SUM(CASE WHEN mg_order.state = 2 THEN 1 ELSE 0 END) AS total_new_user_unsub, + 0 AS submission_count + FROM + mg_order + WHERE + mg_order.created_at BETWEEN ? AND ? + + UNION ALL + -- 第三个查询:mg_transaction_log 数据 + SELECT + HOUR(mg_transaction_log.created_at) AS hour, + product_id AS product_id, + channel_code AS channel_code, + 0 AS new_user_count, + 0 AS new_user_unsub_within_hour, + 0 AS new_user_unsub_on_day, + 0 AS total_new_user_unsub, + COUNT(*) AS submission_count + FROM + mg_transaction_log + WHERE + mg_transaction_log.created_at BETWEEN ? AND ? + AND verification_code != '' + GROUP BY + HOUR(mg_transaction_log.created_at) + + UNION ALL + -- 第四个查询:mg_transaction_log 总计数据 + SELECT + 'Total' AS hour, + product_id AS product_id, + channel_code AS channel_code, + 0 AS new_user_count, + 0 AS new_user_unsub_within_hour, + 0 AS new_user_unsub_on_day, + 0 AS total_new_user_unsub, + COUNT(*) AS submission_count + FROM + mg_transaction_log + WHERE + mg_transaction_log.created_at BETWEEN ? AND ? + AND verification_code != '' + ) AS combined_data + GROUP BY hour + ORDER BY hour + ) AS paginated_data; + ` + + // 执行查询 + var data []models.MgHourSummary + err := e.Orm.Raw(sql, startTime, endTime, startTime, endTime, startTime, endTime, startTime, endTime).Scan(&data).Error + if err != nil { + return nil, nil, 0, err + } + + // 剔除 "Total" 数据 + var filteredData []models.MgHourSummary + var summaryData *models.MgHourSummary + for _, item := range data { + if item.Hour == "Total" { + summaryData = &item // 保存汇总数据 + } else { + filteredData = append(filteredData, item) // 只保留小时数据 + } + } + + // 按小时排序(确保 hour 是数字,排序正确) + sort.Slice(filteredData, func(i, j int) bool { + // 将字符串类型的 hour 转换为整数进行比较 + hourI, errI := strconv.Atoi(filteredData[i].Hour) + hourJ, errJ := strconv.Atoi(filteredData[j].Hour) + + // 如果转换失败,按原样比较;否则按数字顺序排序 + if errI != nil && errJ != nil { + return filteredData[i].Hour < filteredData[j].Hour + } + if errI != nil { + // 如果 i 的 hour 无法转换为数字,则认为它大于 j + return false + } + if errJ != nil { + // 如果 j 的 hour 无法转换为数字,则认为它大于 i + return true + } + return hourI < hourJ + }) + + // 如果是导出数据,直接返回所有数据 + if req.IsExport == 1 { + return filteredData, summaryData, 0, nil + } + + // 分页处理 + start := (req.PageNum - 1) * req.PageSize + end := start + req.PageSize + if end > len(filteredData) { + end = len(filteredData) + } + if start > len(filteredData) { + start = 0 + } + paginatedData := filteredData[start:end] + + // 返回分页数据 + return paginatedData, summaryData, len(filteredData), nil +} diff --git a/app/admin/models/clue.go b/app/admin/models/clue.go index 5c2a9bf..9f4210b 100644 --- a/app/admin/models/clue.go +++ b/app/admin/models/clue.go @@ -14,6 +14,7 @@ import ( const ( QueryTimeFormat = "2006-01-02T15:04:05+08:00" TimeFormat = "2006-01-02 15-04-05" + MiGuTimeFormat = "2006-01-02 15:04:05" ExportUrl = "https://admin.go2switch.cn/load/export/" ExportExcelFlag = 1 ExportPath = "/www/server/images/export/" diff --git a/app/admin/models/migu.go b/app/admin/models/migu.go index b5e884a..bb15d45 100644 --- a/app/admin/models/migu.go +++ b/app/admin/models/migu.go @@ -98,9 +98,31 @@ type MgChannel struct { Remarks string `json:"remarks"` // 备注 } -// MgHistoricalSummary 历史汇总查询表对应的结构体 -type MgHistoricalSummary struct { - Date string `gorm:"type:date" json:"date"` // 日期 +// HourSummaryListResp 历史汇总(按小时)-出参 +type HourSummaryListResp struct { + List []MgHourSummary `json:"list"` // 列表数据 + Count int `json:"count"` // 数据总数 + PageSize int `json:"page_size"` // 每页条数 + PageNum int `json:"page_num"` // 当前页数 + SummaryData *TotalHourSummary `json:"summary_data"` // 汇总数据,单条数据时返回 +} + +// TotalHourSummary 历史汇总查询表汇总数据对应的结构体(按小时) +type TotalHourSummary struct { + SubmissionCount int `json:"submission_count"` // 提交数 + NewUserCount int `json:"new_user_count"` // 新用户数 + SubmissionSuccessRate string `json:"submission_success_rate"` // 提交成功率 + NewUserUnsubWithinHour int `json:"new_user_unsub_within_hour"` // 当日新用户退订数(1小时以内) + NewUserUnsubWithinHourRate string `json:"new_user_unsub_within_hour_rate"` // 当日新用户退订率(1小时以内) + NewUserUnsubOnDay int `json:"new_user_unsub_on_day"` // 当日新用户退订数 + NewUserUnsubOnDayRate string `json:"new_user_unsub_on_day_rate"` // 当日新用户退订率 + TotalNewUserUnsub int `json:"total_new_user_unsub"` // 累计新用户退订数 + TotalNewUserUnsubRate string `json:"total_new_user_unsub_rate"` // 累计新用户退订率 +} + +// MgHourSummary 历史汇总查询表对应的结构体(按小时) +type MgHourSummary struct { + Hour string `json:"hour"` // 日期 ProductID int64 `json:"product_id"` // 产品ID ChannelCode string `gorm:"size:255" json:"channel_code"` // 渠道编码 SubmissionCount int `json:"submission_count"` // 提交数 @@ -110,6 +132,24 @@ type MgHistoricalSummary struct { NewUserUnsubWithinHourRate string `json:"new_user_unsub_within_hour_rate"` // 当日新用户退订率(1小时以内) NewUserUnsubOnDay int `json:"new_user_unsub_on_day"` // 当日新用户退订数 NewUserUnsubOnDayRate string `json:"new_user_unsub_on_day_rate"` // 当日新用户退订率 + TotalNewUserUnsub int `json:"total_new_user_unsub"` // 累计新用户退订数 + TotalNewUserUnsubRate string `json:"total_new_user_unsub_rate"` // 累计新用户退订率 +} + +// MgHistoricalSummary 历史汇总查询表对应的结构体 +type MgHistoricalSummary struct { + Date string `json:"date"` // 日期 + ProductID int64 `json:"product_id"` // 产品ID + ChannelCode string `gorm:"size:255" json:"channel_code"` // 渠道编码 + SubmissionCount int `json:"submission_count"` // 提交数 + NewUserCount int `json:"new_user_count"` // 新用户数 + SubmissionSuccessRate string `json:"submission_success_rate"` // 提交成功率 + NewUserUnsubWithinHour int `json:"new_user_unsub_within_hour"` // 当日新用户退订数(1小时以内) + NewUserUnsubWithinHourRate string `json:"new_user_unsub_within_hour_rate"` // 当日新用户退订率(1小时以内) + NewUserUnsubOnDay int `json:"new_user_unsub_on_day"` // 当日新用户退订数 + NewUserUnsubOnDayRate string `json:"new_user_unsub_on_day_rate"` // 当日新用户退订率 + TotalNewUserUnsub int `json:"total_new_user_unsub"` // 累计新用户退订数 + TotalNewUserUnsubRate string `json:"total_new_user_unsub_rate"` // 累计新用户退订率 //Province string `gorm:"size:255" json:"province"` // 省份 } @@ -145,13 +185,26 @@ type MgTransactionLog struct { // MgUserRetention 用户留存记录表对应的结构体 type MgUserRetention struct { - RetentionMonth string `gorm:"size:7" json:"retention_month"` // 留存月份(格式:YYYY-MM) - NewUserCount int `json:"new_user_count"` // 新增用户数 - RetainedUserCount int `json:"retained_user_count"` // 留存用户数 - ChannelCode string `gorm:"size:255" json:"channel_code"` // 渠道编码 - ProductID int64 `json:"product_id"` // 产品ID - RetentionRate string `json:"retention_rate"` // 留存率(以百分比形式存储) - //Province string `gorm:"size:255" json:"province"` // 省份 + RetentionMonth string `gorm:"size:7" json:"retention_month"` // 留存月份(格式:YYYY-MM) + ChannelCode string `gorm:"size:255" json:"channel_code"` // 渠道编码 + ProductID int64 `json:"product_id"` // 产品ID + NewUserCount int `json:"new_user_count"` // 新增用户数 + RetainedUserCount int `json:"retained_user_count"` // 留存用户数(实时) + RetentionRate string `json:"retention_rate"` // 留存率(实时,以百分比形式存储) + LastTwoMonthDate string `json:"last_two_month_date"` // 最近2个月留存日期(格式:YYYY-MM-DD) + LastTwoMonthRetentionCount int `json:"last_two_month_retention_count"` // 最近2个月留存用户数(如12/1, 11/1) + LastTwoMonthRetentionRate string `json:"last_two_month_retention_rate"` // 最近2个月留存率(如12/1, 11/1) + LastMonthDate string `json:"last_month_date"` // 最近1个月留存日期(格式:YYYY-MM-DD) + LastMonthRetentionCount int `json:"last_month_retention_count"` // 最近1个月留存用户数(如12/1) + LastMonthRetentionRate string `json:"last_month_retention_rate"` // 最近1个月留存率(如12/1) +} + +// MgUserDayRetention 用户留存记录表(按天)对应的结构体 +type MgUserDayRetention struct { + Date string `json:"date"` // 留存日期(格式:YYYY-MM-DD) + RetainedUserCount int `json:"retained_user_count"` // 留存用户数 + RetentionRate string `json:"retention_rate"` // 留存率 + UserUnsubOnDay int `json:"user_unsub_on_day"` // 当日退订数 } // 以下是接口出入参结构体 @@ -343,7 +396,7 @@ type OrderListReq struct { ChannelTradeNo string `json:"channelTradeNo"` // 渠道订单号 Phone string `json:"phone"` // 手机号码 SM4PhoneNumber string `json:"sm4_phone_number"` // SM4加密手机号 - State int `json:"state"` // 退订状态 0-查所有 1-未退订 2-已退订 + State int `json:"state"` // 退订状态 0-查所有 1-未退订 2-已退订 3-1小时内退订 PageNum int `json:"page_num"` // 页码 PageSize int `json:"page_size"` // 每页条数 IsExport uint32 `json:"is_export"` // 1-导出 @@ -401,11 +454,12 @@ type RealtimeSummaryListResp struct { type UserRetentionListReq struct { //Date string `json:"date"` // 月用户(格式:YYYY-MM) RetentionMonth string `json:"retention_month"` // 留存月份(格式:YYYY-MM) - SkuCode string `json:"skuCode"` // 产品编号 + SkuCode int `json:"skuCode"` // 产品编号 Channel string `json:"channel"` // 渠道号 Province string `json:"province"` // 省份 PageNum int `json:"page_num"` // 页码 PageSize int `json:"page_size"` // 每页条数 + IsExport uint32 `json:"is_export"` // 1-导出 } // UserRetentionListResp 用户留存记录查询-出参 @@ -416,6 +470,25 @@ type UserRetentionListResp struct { PageNum int `json:"page_num"` } +// UserDayRetentionListReq 用户留存记录(按天)查询-入参 +type UserDayRetentionListReq struct { + RetentionMonth string `json:"retention_month" binding:"required"` // 留存月份(格式:YYYY-MM) + SkuCode int `json:"skuCode" binding:"required"` // 产品编号 + Channel string `json:"channel" binding:"required"` // 渠道号 + PageNum int `json:"page_num"` // 页码 + PageSize int `json:"page_size"` // 每页条数 + OnlyFirstDay bool `json:"only_first_day"` // 是否只查询每个月1号的数据 + IsExport uint32 `json:"is_export"` // 1-导出 +} + +// UserDayRetentionListResp 用户留存记录(按天)查询-出参 +type UserDayRetentionListResp struct { + List []MgUserDayRetention `json:"list"` + Count int `json:"count"` + PageSize int `json:"page_size"` + PageNum int `json:"page_num"` +} + // SysChannelListReq 查询渠道列表入参 type SysChannelListReq struct { PageNum int `json:"page_num"` // 页码 @@ -1315,7 +1388,8 @@ func ExportHistoricalSummaryToExcel(data []MgHistoricalSummary, db *gorm.DB) (st sheet := "Sheet1" // 设置标题栏 - titles := []string{"日期", "产品ID", "渠道编码", "提交数", "新用户数", "提交成功率", "1小时内退订数", "1小时内退订率", "当日退订数", "当日退订率"} + titles := []string{"日期", "产品ID", "渠道编码", "提交数", "新用户数", "提交成功率", "1小时退订数", "1小时退订率", + "当日退订数", "当日退订率", "累计退订数", "累计退订率"} for i, title := range titles { cell, _ := excelize.CoordinatesToCellName(i+1, 1) file.SetCellValue(sheet, cell, title) @@ -1340,6 +1414,8 @@ func ExportHistoricalSummaryToExcel(data []MgHistoricalSummary, db *gorm.DB) (st file.SetColWidth(sheet, "H", "H", 15) file.SetColWidth(sheet, "I", "I", 15) file.SetColWidth(sheet, "J", "J", 15) + file.SetColWidth(sheet, "K", "K", 15) + file.SetColWidth(sheet, "L", "L", 15) // 创建一个产品ID到名称的映射 productMap := make(map[int64]string) @@ -1369,9 +1445,11 @@ func ExportHistoricalSummaryToExcel(data []MgHistoricalSummary, db *gorm.DB) (st file.SetCellValue(sheet, "H"+strconv.Itoa(row), record.NewUserUnsubWithinHourRate) file.SetCellValue(sheet, "I"+strconv.Itoa(row), record.NewUserUnsubOnDay) file.SetCellValue(sheet, "J"+strconv.Itoa(row), record.NewUserUnsubOnDayRate) + file.SetCellValue(sheet, "K"+strconv.Itoa(row), record.TotalNewUserUnsub) + file.SetCellValue(sheet, "L"+strconv.Itoa(row), record.TotalNewUserUnsubRate) } - endRow := fmt.Sprintf("J%d", len(data)+1) + endRow := fmt.Sprintf("L%d", len(data)+1) // 应用样式到整个表格 _ = file.SetCellStyle(sheet, "A1", endRow, style) @@ -1449,7 +1527,7 @@ func ExportRealtimeSummaryToExcel(data []MgRealtimeSummary, db *gorm.DB) (string file.SetCellValue(sheet, "I"+strconv.Itoa(row), record.NewUserUnsubOnDayRate) } - endRow := fmt.Sprintf("J%d", len(data)+1) + endRow := fmt.Sprintf("I%d", len(data)+1) // 应用样式到整个表格 _ = file.SetCellStyle(sheet, "A1", endRow, style) @@ -1594,3 +1672,111 @@ func ConvertStringToTime(dateStr string) (string, error) { formattedTime := t.Format("2006-01-02 15:04:05") return formattedTime, nil } + +// ExportHourSummaryToExcel 历史汇总查询(按小时)导出excel +func ExportHourSummaryToExcel(data []MgHourSummary, sumData TotalHourSummary, db *gorm.DB) (string, error) { + // 创建一个新的Excel文件 + file := excelize.NewFile() + sheet := "Sheet1" + + nExcelStartRow := 0 + // 设置标题栏 + titles := []string{"小时", "产品", "渠道", "提交数", "新用户数", "提交成功率", "1小时退订数", "1小时退订率", + "当日退订数", "当日退订率", "累计退订数", "累计退订率"} + for i, title := range titles { + cell, _ := excelize.CoordinatesToCellName(i+1, 1) + file.SetCellValue(sheet, cell, title) + } + nExcelStartRow += 1 + + // 设置所有单元格的样式: 居中、加边框 + style, _ := file.NewStyle(`{"alignment":{"horizontal":"center","vertical":"center"}, + "border":[{"type":"left","color":"000000","style":1}, + {"type":"top","color":"000000","style":1}, + {"type":"right","color":"000000","style":1}, + {"type":"bottom","color":"000000","style":1}]}`) + + // 设置单元格高度 + file.SetRowHeight(sheet, 1, 20) + + // 设置列宽 + file.SetColWidth(sheet, "A", "A", 15) + file.SetColWidth(sheet, "B", "B", 18) + file.SetColWidth(sheet, "C", "C", 18) + file.SetColWidth(sheet, "F", "F", 15) + file.SetColWidth(sheet, "G", "G", 15) + file.SetColWidth(sheet, "H", "H", 15) + file.SetColWidth(sheet, "I", "I", 15) + file.SetColWidth(sheet, "J", "J", 15) + file.SetColWidth(sheet, "K", "K", 15) + file.SetColWidth(sheet, "L", "L", 15) + + // 创建一个产品ID到名称的映射 + productMap := make(map[int64]string) + for _, order := range data { + if _, exists := productMap[order.ProductID]; !exists { + var product MgProduct + // 查询产品信息 + if err := db.First(&product, order.ProductID).Error; err == nil { + productMap[order.ProductID] = product.Name + } else { + productMap[order.ProductID] = "未知产品" + } + } + } + + // 填充数据 + for i, record := range data { + nExcelStartRow += 1 + row := i + 2 + productName := productMap[record.ProductID] // 获取产品名称 + file.SetCellValue(sheet, "A"+strconv.Itoa(row), record.Hour) + file.SetCellValue(sheet, "B"+strconv.Itoa(row), productName) + file.SetCellValue(sheet, "C"+strconv.Itoa(row), record.ChannelCode) + file.SetCellValue(sheet, "D"+strconv.Itoa(row), record.SubmissionCount) + file.SetCellValue(sheet, "E"+strconv.Itoa(row), record.NewUserCount) + file.SetCellValue(sheet, "F"+strconv.Itoa(row), record.SubmissionSuccessRate) + file.SetCellValue(sheet, "G"+strconv.Itoa(row), record.NewUserUnsubWithinHour) + file.SetCellValue(sheet, "H"+strconv.Itoa(row), record.NewUserUnsubWithinHourRate) + file.SetCellValue(sheet, "I"+strconv.Itoa(row), record.NewUserUnsubOnDay) + file.SetCellValue(sheet, "J"+strconv.Itoa(row), record.NewUserUnsubOnDayRate) + file.SetCellValue(sheet, "K"+strconv.Itoa(row), record.TotalNewUserUnsub) + file.SetCellValue(sheet, "L"+strconv.Itoa(row), record.TotalNewUserUnsubRate) + } + + endRow := fmt.Sprintf("L%d", len(data)+1) + // 应用样式到整个表格 + _ = file.SetCellStyle(sheet, "A1", endRow, style) + + totalData := "订单数:" + strconv.FormatInt(int64(len(data)), 10) + end := []interface{}{totalData, "", "", + sumData.SubmissionCount, + sumData.NewUserCount, + sumData.SubmissionSuccessRate, + sumData.NewUserUnsubWithinHour, + sumData.NewUserUnsubWithinHourRate, + sumData.NewUserUnsubOnDay, + sumData.NewUserUnsubOnDayRate, + sumData.TotalNewUserUnsub, + sumData.TotalNewUserUnsubRate, + } + for i, _ := range end { + cell, _ := excelize.CoordinatesToCellName(1+i, nExcelStartRow+2) + err := file.SetCellValue(sheet, cell, end[i]) + if err != nil { + logger.Errorf("file set value err:", err) + } + } + + // 从配置文件读取保存路径和URL前缀 + fileName := time.Now().Format("20060102150405") + "_历史汇总(按小时).xlsx" + url := MiGuExportUrl + fileName + + // 保存Excel文件 + if err := file.SaveAs(ExportFile + fileName); err != nil { + logger.Errorf("Failed to save Excel file: %v", err) + return "", err + } + + return url, nil +} diff --git a/app/admin/router/migu.go b/app/admin/router/migu.go index daf6354..0b24438 100644 --- a/app/admin/router/migu.go +++ b/app/admin/router/migu.go @@ -21,15 +21,18 @@ func registerMiGuControlManageRouter(v1 *gin.RouterGroup, authMiddleware *jwt.Gi //api.POST("channel/update", apiMiGu.UpdateChannel) // 编辑渠道 //api.POST("channel/delete", apiMiGu.DeleteChannel) // 删除渠道 - api.POST("transaction/list", apiMiGu.TransactionList) // 查询交易流水记录 - api.POST("order/list", apiMiGu.OrderList) // 查询订单列表 - api.POST("historical_summary/list", apiMiGu.HistoricalSummaryList) // 历史汇总查询 - api.POST("realtime_summary/list", apiMiGu.RealtimeSummaryList) // 当日实时汇总 - api.POST("user_retention/list", apiMiGu.UserRetentionList) // 用户留存记录 - api.POST("sys_channel/list", apiMiGu.SysChannelList) // 查询系统所有渠道编码 - api.POST("home/data", apiMiGu.HomepageDataSummary) // 查询首页汇总数据 - api.POST("home/revenue_analysis", apiMiGu.CalculateRevenueAnalysis) // 查询不同日期的留存月份 + api.POST("transaction/list", apiMiGu.TransactionList) // 查询交易流水记录 + api.POST("order/list", apiMiGu.OrderList) // 查询订单列表 + api.POST("historical_summary/list", apiMiGu.HistoricalSummaryListNew) // 历史汇总查询 + api.POST("hour_summary/list", apiMiGu.HourSummaryList) // 历史汇总查询(按小时) + api.POST("realtime_summary/list", apiMiGu.RealtimeSummaryList) // 当日实时汇总 + api.POST("user_retention/list", apiMiGu.UserRetentionList) // 用户留存记录 + api.POST("user_day_retention/list", apiMiGu.UserDayRetentionList) // 用户留存记录(按天) + api.POST("sys_channel/list", apiMiGu.SysChannelList) // 查询系统所有渠道编码 + api.POST("home/data", apiMiGu.HomepageDataSummary) // 查询首页汇总数据 + api.POST("home/revenue_analysis", apiMiGu.CalculateRevenueAnalysis) // 查询不同日期的留存月份 + //api.POST("historical_summary/list_old", apiMiGu.HistoricalSummaryListOld) // 历史汇总查询 //api.POST("order/import", apiMiGu.ImportExcelToMgOrderHandler) // 通过excel导入订单数据 //api.POST("order/import_update", apiMiGu.ImportExcelToMgOrderHandlerUpdate) // 通过excel导入订单退订数据 } diff --git a/docs/admin/admin_docs.go b/docs/admin/admin_docs.go index f1be8af..8368f53 100644 --- a/docs/admin/admin_docs.go +++ b/docs/admin/admin_docs.go @@ -184,6 +184,39 @@ const docTemplateadmin = `{ } } }, + "/api/v1/admin/hour_summary/list": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "2024-咪咕-管理后台" + ], + "summary": "历史汇总(按小时)", + "parameters": [ + { + "description": "历史汇总(按小时)", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.HistoricalSummaryListReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.HourSummaryListResp" + } + } + } + } + }, "/api/v1/admin/order/list": { "post": { "consumes": [ @@ -382,6 +415,39 @@ const docTemplateadmin = `{ } } }, + "/api/v1/admin/user_day_retention/list": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "2024-咪咕-管理后台" + ], + "summary": "用户留存记录(按天)", + "parameters": [ + { + "description": "用户留存记录(按天)", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.UserDayRetentionListReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.UserDayRetentionListResp" + } + } + } + } + }, "/api/v1/admin/user_retention/list": { "post": { "consumes": [ @@ -4886,6 +4952,38 @@ const docTemplateadmin = `{ } } }, + "models.HourSummaryListResp": { + "type": "object", + "properties": { + "count": { + "description": "数据总数", + "type": "integer" + }, + "list": { + "description": "列表数据", + "type": "array", + "items": { + "$ref": "#/definitions/models.MgHourSummary" + } + }, + "page_num": { + "description": "当前页数", + "type": "integer" + }, + "page_size": { + "description": "每页条数", + "type": "integer" + }, + "summary_data": { + "description": "汇总数据,单条数据时返回", + "allOf": [ + { + "$ref": "#/definitions/models.TotalHourSummary" + } + ] + } + } + }, "models.MgChannel": { "type": "object", "properties": { @@ -4967,6 +5065,67 @@ const docTemplateadmin = `{ "submission_success_rate": { "description": "提交成功率", "type": "string" + }, + "total_new_user_unsub": { + "description": "累计新用户退订数", + "type": "integer" + }, + "total_new_user_unsub_rate": { + "description": "累计新用户退订率", + "type": "string" + } + } + }, + "models.MgHourSummary": { + "type": "object", + "properties": { + "channel_code": { + "description": "渠道编码", + "type": "string" + }, + "hour": { + "description": "日期", + "type": "string" + }, + "new_user_count": { + "description": "新用户数", + "type": "integer" + }, + "new_user_unsub_on_day": { + "description": "当日新用户退订数", + "type": "integer" + }, + "new_user_unsub_on_day_rate": { + "description": "当日新用户退订率", + "type": "string" + }, + "new_user_unsub_within_hour": { + "description": "当日新用户退订数(1小时以内)", + "type": "integer" + }, + "new_user_unsub_within_hour_rate": { + "description": "当日新用户退订率(1小时以内)", + "type": "string" + }, + "product_id": { + "description": "产品ID", + "type": "integer" + }, + "submission_count": { + "description": "提交数", + "type": "integer" + }, + "submission_success_rate": { + "description": "提交成功率", + "type": "string" + }, + "total_new_user_unsub": { + "description": "累计新用户退订数", + "type": "integer" + }, + "total_new_user_unsub_rate": { + "description": "累计新用户退订率", + "type": "string" } } }, @@ -5164,6 +5323,27 @@ const docTemplateadmin = `{ } } }, + "models.MgUserDayRetention": { + "type": "object", + "properties": { + "date": { + "description": "留存日期(格式:YYYY-MM-DD)", + "type": "string" + }, + "retained_user_count": { + "description": "留存用户数", + "type": "integer" + }, + "retention_rate": { + "description": "留存率", + "type": "string" + }, + "user_unsub_on_day": { + "description": "当日退订数", + "type": "integer" + } + } + }, "models.MgUserRetention": { "type": "object", "properties": { @@ -5171,6 +5351,30 @@ const docTemplateadmin = `{ "description": "渠道编码", "type": "string" }, + "last_month_date": { + "description": "最近1个月留存日期(格式:YYYY-MM-DD)", + "type": "string" + }, + "last_month_retention_count": { + "description": "最近1个月留存用户数(如12/1)", + "type": "integer" + }, + "last_month_retention_rate": { + "description": "最近1个月留存率(如12/1)", + "type": "string" + }, + "last_two_month_date": { + "description": "最近2个月留存日期(格式:YYYY-MM-DD)", + "type": "string" + }, + "last_two_month_retention_count": { + "description": "最近2个月留存用户数(如12/1, 11/1)", + "type": "integer" + }, + "last_two_month_retention_rate": { + "description": "最近2个月留存率(如12/1, 11/1)", + "type": "string" + }, "new_user_count": { "description": "新增用户数", "type": "integer" @@ -5180,7 +5384,7 @@ const docTemplateadmin = `{ "type": "integer" }, "retained_user_count": { - "description": "留存用户数", + "description": "留存用户数(实时)", "type": "integer" }, "retention_month": { @@ -5188,7 +5392,7 @@ const docTemplateadmin = `{ "type": "string" }, "retention_rate": { - "description": "留存率(以百分比形式存储)", + "description": "留存率(实时,以百分比形式存储)", "type": "string" } } @@ -5270,7 +5474,7 @@ const docTemplateadmin = `{ "type": "string" }, "state": { - "description": "退订状态 0-查所有 1-未退订 2-已退订", + "description": "退订状态 0-查所有 1-未退订 2-已退订 3-1小时内退订", "type": "integer" } } @@ -5655,6 +5859,47 @@ const docTemplateadmin = `{ } } }, + "models.TotalHourSummary": { + "type": "object", + "properties": { + "new_user_count": { + "description": "新用户数", + "type": "integer" + }, + "new_user_unsub_on_day": { + "description": "当日新用户退订数", + "type": "integer" + }, + "new_user_unsub_on_day_rate": { + "description": "当日新用户退订率", + "type": "string" + }, + "new_user_unsub_within_hour": { + "description": "当日新用户退订数(1小时以内)", + "type": "integer" + }, + "new_user_unsub_within_hour_rate": { + "description": "当日新用户退订率(1小时以内)", + "type": "string" + }, + "submission_count": { + "description": "提交数", + "type": "integer" + }, + "submission_success_rate": { + "description": "提交成功率", + "type": "string" + }, + "total_new_user_unsub": { + "description": "累计新用户退订数", + "type": "integer" + }, + "total_new_user_unsub_rate": { + "description": "累计新用户退订率", + "type": "string" + } + } + }, "models.TransactionListReq": { "type": "object", "properties": { @@ -5712,6 +5957,60 @@ const docTemplateadmin = `{ } } }, + "models.UserDayRetentionListReq": { + "type": "object", + "required": [ + "channel", + "retention_month", + "skuCode" + ], + "properties": { + "channel": { + "description": "渠道号", + "type": "string" + }, + "only_first_day": { + "description": "是否只查询每个月1号的数据", + "type": "boolean" + }, + "page_num": { + "description": "页码", + "type": "integer" + }, + "page_size": { + "description": "每页条数", + "type": "integer" + }, + "retention_month": { + "description": "留存月份(格式:YYYY-MM)", + "type": "string" + }, + "skuCode": { + "description": "产品编号", + "type": "integer" + } + } + }, + "models.UserDayRetentionListResp": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/models.MgUserDayRetention" + } + }, + "page_num": { + "type": "integer" + }, + "page_size": { + "type": "integer" + } + } + }, "models.UserRetentionListReq": { "type": "object", "properties": { @@ -5737,7 +6036,7 @@ const docTemplateadmin = `{ }, "skuCode": { "description": "产品编号", - "type": "string" + "type": "integer" } } }, diff --git a/docs/admin/admin_swagger.json b/docs/admin/admin_swagger.json index caeaa74..dde54bf 100644 --- a/docs/admin/admin_swagger.json +++ b/docs/admin/admin_swagger.json @@ -176,6 +176,39 @@ } } }, + "/api/v1/admin/hour_summary/list": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "2024-咪咕-管理后台" + ], + "summary": "历史汇总(按小时)", + "parameters": [ + { + "description": "历史汇总(按小时)", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.HistoricalSummaryListReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.HourSummaryListResp" + } + } + } + } + }, "/api/v1/admin/order/list": { "post": { "consumes": [ @@ -374,6 +407,39 @@ } } }, + "/api/v1/admin/user_day_retention/list": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "2024-咪咕-管理后台" + ], + "summary": "用户留存记录(按天)", + "parameters": [ + { + "description": "用户留存记录(按天)", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.UserDayRetentionListReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.UserDayRetentionListResp" + } + } + } + } + }, "/api/v1/admin/user_retention/list": { "post": { "consumes": [ @@ -4878,6 +4944,38 @@ } } }, + "models.HourSummaryListResp": { + "type": "object", + "properties": { + "count": { + "description": "数据总数", + "type": "integer" + }, + "list": { + "description": "列表数据", + "type": "array", + "items": { + "$ref": "#/definitions/models.MgHourSummary" + } + }, + "page_num": { + "description": "当前页数", + "type": "integer" + }, + "page_size": { + "description": "每页条数", + "type": "integer" + }, + "summary_data": { + "description": "汇总数据,单条数据时返回", + "allOf": [ + { + "$ref": "#/definitions/models.TotalHourSummary" + } + ] + } + } + }, "models.MgChannel": { "type": "object", "properties": { @@ -4959,6 +5057,67 @@ "submission_success_rate": { "description": "提交成功率", "type": "string" + }, + "total_new_user_unsub": { + "description": "累计新用户退订数", + "type": "integer" + }, + "total_new_user_unsub_rate": { + "description": "累计新用户退订率", + "type": "string" + } + } + }, + "models.MgHourSummary": { + "type": "object", + "properties": { + "channel_code": { + "description": "渠道编码", + "type": "string" + }, + "hour": { + "description": "日期", + "type": "string" + }, + "new_user_count": { + "description": "新用户数", + "type": "integer" + }, + "new_user_unsub_on_day": { + "description": "当日新用户退订数", + "type": "integer" + }, + "new_user_unsub_on_day_rate": { + "description": "当日新用户退订率", + "type": "string" + }, + "new_user_unsub_within_hour": { + "description": "当日新用户退订数(1小时以内)", + "type": "integer" + }, + "new_user_unsub_within_hour_rate": { + "description": "当日新用户退订率(1小时以内)", + "type": "string" + }, + "product_id": { + "description": "产品ID", + "type": "integer" + }, + "submission_count": { + "description": "提交数", + "type": "integer" + }, + "submission_success_rate": { + "description": "提交成功率", + "type": "string" + }, + "total_new_user_unsub": { + "description": "累计新用户退订数", + "type": "integer" + }, + "total_new_user_unsub_rate": { + "description": "累计新用户退订率", + "type": "string" } } }, @@ -5156,6 +5315,27 @@ } } }, + "models.MgUserDayRetention": { + "type": "object", + "properties": { + "date": { + "description": "留存日期(格式:YYYY-MM-DD)", + "type": "string" + }, + "retained_user_count": { + "description": "留存用户数", + "type": "integer" + }, + "retention_rate": { + "description": "留存率", + "type": "string" + }, + "user_unsub_on_day": { + "description": "当日退订数", + "type": "integer" + } + } + }, "models.MgUserRetention": { "type": "object", "properties": { @@ -5163,6 +5343,30 @@ "description": "渠道编码", "type": "string" }, + "last_month_date": { + "description": "最近1个月留存日期(格式:YYYY-MM-DD)", + "type": "string" + }, + "last_month_retention_count": { + "description": "最近1个月留存用户数(如12/1)", + "type": "integer" + }, + "last_month_retention_rate": { + "description": "最近1个月留存率(如12/1)", + "type": "string" + }, + "last_two_month_date": { + "description": "最近2个月留存日期(格式:YYYY-MM-DD)", + "type": "string" + }, + "last_two_month_retention_count": { + "description": "最近2个月留存用户数(如12/1, 11/1)", + "type": "integer" + }, + "last_two_month_retention_rate": { + "description": "最近2个月留存率(如12/1, 11/1)", + "type": "string" + }, "new_user_count": { "description": "新增用户数", "type": "integer" @@ -5172,7 +5376,7 @@ "type": "integer" }, "retained_user_count": { - "description": "留存用户数", + "description": "留存用户数(实时)", "type": "integer" }, "retention_month": { @@ -5180,7 +5384,7 @@ "type": "string" }, "retention_rate": { - "description": "留存率(以百分比形式存储)", + "description": "留存率(实时,以百分比形式存储)", "type": "string" } } @@ -5262,7 +5466,7 @@ "type": "string" }, "state": { - "description": "退订状态 0-查所有 1-未退订 2-已退订", + "description": "退订状态 0-查所有 1-未退订 2-已退订 3-1小时内退订", "type": "integer" } } @@ -5647,6 +5851,47 @@ } } }, + "models.TotalHourSummary": { + "type": "object", + "properties": { + "new_user_count": { + "description": "新用户数", + "type": "integer" + }, + "new_user_unsub_on_day": { + "description": "当日新用户退订数", + "type": "integer" + }, + "new_user_unsub_on_day_rate": { + "description": "当日新用户退订率", + "type": "string" + }, + "new_user_unsub_within_hour": { + "description": "当日新用户退订数(1小时以内)", + "type": "integer" + }, + "new_user_unsub_within_hour_rate": { + "description": "当日新用户退订率(1小时以内)", + "type": "string" + }, + "submission_count": { + "description": "提交数", + "type": "integer" + }, + "submission_success_rate": { + "description": "提交成功率", + "type": "string" + }, + "total_new_user_unsub": { + "description": "累计新用户退订数", + "type": "integer" + }, + "total_new_user_unsub_rate": { + "description": "累计新用户退订率", + "type": "string" + } + } + }, "models.TransactionListReq": { "type": "object", "properties": { @@ -5704,6 +5949,60 @@ } } }, + "models.UserDayRetentionListReq": { + "type": "object", + "required": [ + "channel", + "retention_month", + "skuCode" + ], + "properties": { + "channel": { + "description": "渠道号", + "type": "string" + }, + "only_first_day": { + "description": "是否只查询每个月1号的数据", + "type": "boolean" + }, + "page_num": { + "description": "页码", + "type": "integer" + }, + "page_size": { + "description": "每页条数", + "type": "integer" + }, + "retention_month": { + "description": "留存月份(格式:YYYY-MM)", + "type": "string" + }, + "skuCode": { + "description": "产品编号", + "type": "integer" + } + } + }, + "models.UserDayRetentionListResp": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/models.MgUserDayRetention" + } + }, + "page_num": { + "type": "integer" + }, + "page_size": { + "type": "integer" + } + } + }, "models.UserRetentionListReq": { "type": "object", "properties": { @@ -5729,7 +6028,7 @@ }, "skuCode": { "description": "产品编号", - "type": "string" + "type": "integer" } } }, diff --git a/docs/admin/admin_swagger.yaml b/docs/admin/admin_swagger.yaml index d1ba20a..60df94c 100644 --- a/docs/admin/admin_swagger.yaml +++ b/docs/admin/admin_swagger.yaml @@ -1072,6 +1072,27 @@ definitions: - $ref: '#/definitions/models.SummaryData' description: 汇总数据 type: object + models.HourSummaryListResp: + properties: + count: + description: 数据总数 + type: integer + list: + description: 列表数据 + items: + $ref: '#/definitions/models.MgHourSummary' + type: array + page_num: + description: 当前页数 + type: integer + page_size: + description: 每页条数 + type: integer + summary_data: + allOf: + - $ref: '#/definitions/models.TotalHourSummary' + description: 汇总数据,单条数据时返回 + type: object models.MgChannel: properties: createdAt: @@ -1132,6 +1153,51 @@ definitions: submission_success_rate: description: 提交成功率 type: string + total_new_user_unsub: + description: 累计新用户退订数 + type: integer + total_new_user_unsub_rate: + description: 累计新用户退订率 + type: string + type: object + models.MgHourSummary: + properties: + channel_code: + description: 渠道编码 + type: string + hour: + description: 日期 + type: string + new_user_count: + description: 新用户数 + type: integer + new_user_unsub_on_day: + description: 当日新用户退订数 + type: integer + new_user_unsub_on_day_rate: + description: 当日新用户退订率 + type: string + new_user_unsub_within_hour: + description: 当日新用户退订数(1小时以内) + type: integer + new_user_unsub_within_hour_rate: + description: 当日新用户退订率(1小时以内) + type: string + product_id: + description: 产品ID + type: integer + submission_count: + description: 提交数 + type: integer + submission_success_rate: + description: 提交成功率 + type: string + total_new_user_unsub: + description: 累计新用户退订数 + type: integer + total_new_user_unsub_rate: + description: 累计新用户退订率 + type: string type: object models.MgOrder: properties: @@ -1274,11 +1340,44 @@ definitions: description: 验证码 type: string type: object + models.MgUserDayRetention: + properties: + date: + description: 留存日期(格式:YYYY-MM-DD) + type: string + retained_user_count: + description: 留存用户数 + type: integer + retention_rate: + description: 留存率 + type: string + user_unsub_on_day: + description: 当日退订数 + type: integer + type: object models.MgUserRetention: properties: channel_code: description: 渠道编码 type: string + last_month_date: + description: 最近1个月留存日期(格式:YYYY-MM-DD) + type: string + last_month_retention_count: + description: 最近1个月留存用户数(如12/1) + type: integer + last_month_retention_rate: + description: 最近1个月留存率(如12/1) + type: string + last_two_month_date: + description: 最近2个月留存日期(格式:YYYY-MM-DD) + type: string + last_two_month_retention_count: + description: 最近2个月留存用户数(如12/1, 11/1) + type: integer + last_two_month_retention_rate: + description: 最近2个月留存率(如12/1, 11/1) + type: string new_user_count: description: 新增用户数 type: integer @@ -1286,13 +1385,13 @@ definitions: description: 产品ID type: integer retained_user_count: - description: 留存用户数 + description: 留存用户数(实时) type: integer retention_month: description: 留存月份(格式:YYYY-MM) type: string retention_rate: - description: 留存率(以百分比形式存储) + description: 留存率(实时,以百分比形式存储) type: string type: object models.MonthlyRetention: @@ -1352,7 +1451,7 @@ definitions: description: 开始时间 type: string state: - description: 退订状态 0-查所有 1-未退订 2-已退订 + description: 退订状态 0-查所有 1-未退订 2-已退订 3-1小时内退订 type: integer type: object models.OrderListResp: @@ -1615,6 +1714,36 @@ definitions: page_size: type: integer type: object + models.TotalHourSummary: + properties: + new_user_count: + description: 新用户数 + type: integer + new_user_unsub_on_day: + description: 当日新用户退订数 + type: integer + new_user_unsub_on_day_rate: + description: 当日新用户退订率 + type: string + new_user_unsub_within_hour: + description: 当日新用户退订数(1小时以内) + type: integer + new_user_unsub_within_hour_rate: + description: 当日新用户退订率(1小时以内) + type: string + submission_count: + description: 提交数 + type: integer + submission_success_rate: + description: 提交成功率 + type: string + total_new_user_unsub: + description: 累计新用户退订数 + type: integer + total_new_user_unsub_rate: + description: 累计新用户退订率 + type: string + type: object models.TransactionListReq: properties: channel: @@ -1655,6 +1784,44 @@ definitions: page_size: type: integer type: object + models.UserDayRetentionListReq: + properties: + channel: + description: 渠道号 + type: string + only_first_day: + description: 是否只查询每个月1号的数据 + type: boolean + page_num: + description: 页码 + type: integer + page_size: + description: 每页条数 + type: integer + retention_month: + description: 留存月份(格式:YYYY-MM) + type: string + skuCode: + description: 产品编号 + type: integer + required: + - channel + - retention_month + - skuCode + type: object + models.UserDayRetentionListResp: + properties: + count: + type: integer + list: + items: + $ref: '#/definitions/models.MgUserDayRetention' + type: array + page_num: + type: integer + page_size: + type: integer + type: object models.UserRetentionListReq: properties: channel: @@ -1674,7 +1841,7 @@ definitions: type: string skuCode: description: 产品编号 - type: string + type: integer type: object models.UserRetentionListResp: properties: @@ -2004,6 +2171,27 @@ paths: summary: 营收分析 tags: - 2024-咪咕-管理后台 + /api/v1/admin/hour_summary/list: + post: + consumes: + - application/json + parameters: + - description: 历史汇总(按小时) + in: body + name: request + required: true + schema: + $ref: '#/definitions/models.HistoricalSummaryListReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.HourSummaryListResp' + summary: 历史汇总(按小时) + tags: + - 2024-咪咕-管理后台 /api/v1/admin/order/list: post: consumes: @@ -2130,6 +2318,27 @@ paths: summary: 查询交易流水记录 tags: - 2024-咪咕-管理后台 + /api/v1/admin/user_day_retention/list: + post: + consumes: + - application/json + parameters: + - description: 用户留存记录(按天) + in: body + name: request + required: true + schema: + $ref: '#/definitions/models.UserDayRetentionListReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.UserDayRetentionListResp' + summary: 用户留存记录(按天) + tags: + - 2024-咪咕-管理后台 /api/v1/admin/user_retention/list: post: consumes: