From 2863174547d1462e75047ca602925d60e4c60170 Mon Sep 17 00:00:00 2001 From: chenlin Date: Mon, 7 Apr 2025 19:50:10 +0800 Subject: [PATCH] =?UTF-8?q?1=E3=80=81=E5=8E=86=E5=8F=B2=E6=B1=87=E6=80=BB?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E6=8E=92=E5=BA=8F=E4=BC=98=E5=8C=96=EF=BC=88?= =?UTF-8?q?=E6=8C=89=E6=97=B6=E9=97=B4=E6=8E=92=E5=BA=8F=EF=BC=89=EF=BC=9B?= =?UTF-8?q?=202=E3=80=81=E5=8E=86=E5=8F=B2=E6=B1=87=E6=80=BB=EF=BC=88?= =?UTF-8?q?=E6=8C=89=E5=B0=8F=E6=97=B6=EF=BC=89=E4=BC=98=E5=8C=96=EF=BC=8C?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E4=B8=8D=E5=90=8C=E4=BA=A7=E5=93=81=E7=AD=9B?= =?UTF-8?q?=E9=80=89=EF=BC=9B=203=E3=80=81=E7=94=A8=E6=88=B7=E7=95=99?= =?UTF-8?q?=E5=AD=98=E8=AE=B0=E5=BD=95=EF=BC=88=E6=8C=89=E5=A4=A9=EF=BC=89?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=BD=93=E6=9C=88=E6=9C=80=E5=90=8E1?= =?UTF-8?q?=E5=A4=A9=E7=95=99=E5=AD=98=E6=95=B0=E6=8D=AE=E5=B1=95=E7=A4=BA?= =?UTF-8?q?=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/apis/migumanage/migu_admin.go | 253 +++++++++++++----------- app/admin/models/migu.go | 76 +++++++ 2 files changed, 211 insertions(+), 118 deletions(-) diff --git a/app/admin/apis/migumanage/migu_admin.go b/app/admin/apis/migumanage/migu_admin.go index f82efc8..c3a3524 100644 --- a/app/admin/apis/migumanage/migu_admin.go +++ b/app/admin/apis/migumanage/migu_admin.go @@ -540,7 +540,7 @@ func (e MiGuDeployService) HistoricalSummaryListOld(c *gin.Context) { `). 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") + Order("mg_order.subscribe_time DESC") // 添加过滤条件 if req.SkuCode != 0 { @@ -1528,6 +1528,13 @@ func (e MiGuDeployService) UserDayRetentionList(c *gin.Context) { return } + addData, err := models.GetMonthlyEffectiveUserStats(e.Orm, req.RetentionMonth, req.SkuCode, req.Channel) + if err != nil { + e.Logger.Error(err) + response.Error(c, http.StatusInternalServerError, err, "获取当月留存数据失败") + return + } + // 计算下个月的1号(留存查询从下个月1号开始) parsedMonth, err := time.Parse("2006-01", req.RetentionMonth) if err != nil { @@ -1725,6 +1732,13 @@ func (e MiGuDeployService) UserDayRetentionList(c *gin.Context) { retentionData = append(retentionData, result) } + var addDayRetention models.MgUserDayRetention + addDayRetention.Date = addData.Date + addDayRetention.RetainedUserCount = addData.ValidUserCount + addDayRetention.RetentionRate = addData.EffectiveRate + addDayRetention.UserUnsubOnDay = int(addData.UnsubscribedToday) + retentionData = append(retentionData, addDayRetention) + // 排序(按日期升序) sort.SliceStable(retentionData, func(i, j int) bool { return retentionData[i].Date > retentionData[j].Date @@ -2952,148 +2966,152 @@ func (e MiGuDeployService) HourSummaryList(c *gin.Context) { // 查询历史数据(按小时),并计算退订率和提交成功率 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 filterConds string + var args []interface{} + args = append(args, startTime, endTime) - // 执行查询 + if req.SkuCode > 0 { + filterConds += " AND product_id = ?" + args = append(args, req.SkuCode) + } + if req.Channel != "" { + filterConds += " AND channel_code = ?" + args = append(args, req.Channel) + } + + // 拷贝参数用于不同的 UNION 查询 + argsOrder := append([]interface{}{}, args...) + argsOrderTotal := append([]interface{}{}, args...) + argsLog := append([]interface{}{}, args...) + argsLogTotal := append([]interface{}{}, args...) + + // 构建 SQL 查询 + sql := fmt.Sprintf(` + 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 ( + SELECT + HOUR(subscribe_time) AS hour, + product_id, + channel_code, + COUNT(*) AS new_user_count, + SUM(CASE WHEN is_one_hour_cancel = 1 THEN 1 ELSE 0 END) AS new_user_unsub_within_hour, + SUM(CASE WHEN state = 2 AND DATE(unsubscribe_time) = DATE(subscribe_time) THEN 1 ELSE 0 END) AS new_user_unsub_on_day, + SUM(CASE WHEN state = 2 THEN 1 ELSE 0 END) AS total_new_user_unsub, + 0 AS submission_count + FROM mg_order + WHERE subscribe_time BETWEEN ? AND ? %s + GROUP BY HOUR(subscribe_time), channel_code, product_id -- 按小时和渠道分组 + + UNION ALL + + SELECT + 'Total' AS hour, + product_id, + channel_code, + COUNT(*) AS new_user_count, + SUM(CASE WHEN is_one_hour_cancel = 1 THEN 1 ELSE 0 END), + SUM(CASE WHEN state = 2 AND DATE(unsubscribe_time) = DATE(subscribe_time) THEN 1 ELSE 0 END), + SUM(CASE WHEN state = 2 THEN 1 ELSE 0 END), + 0 + FROM mg_order + WHERE subscribe_time BETWEEN ? AND ? %s + GROUP BY channel_code, product_id -- 按渠道和产品分组 + + UNION ALL + + SELECT + HOUR(created_at) AS hour, + product_id, + channel_code, + 0, 0, 0, 0, + COUNT(*) AS submission_count + FROM mg_transaction_log + WHERE created_at BETWEEN ? AND ? AND verification_code != '' %s + GROUP BY HOUR(created_at), channel_code -- 按小时和渠道分组 + + UNION ALL + + SELECT + 'Total' AS hour, + product_id, + channel_code, + 0, 0, 0, 0, + COUNT(*) AS submission_count + FROM mg_transaction_log + WHERE created_at BETWEEN ? AND ? AND verification_code != '' %s + GROUP BY channel_code, product_id -- 按渠道和产品分组 + ) AS combined_data + GROUP BY hour, channel_code -- 按小时和渠道分组 + ORDER BY hour +) AS paginated_data + `, filterConds, filterConds, filterConds, filterConds) + + // 整合参数 + args = append(argsOrder, argsOrderTotal...) + args = append(args, argsLog...) + args = append(args, argsLogTotal...) + + // 查询数据 var data []models.MgHourSummary - err := e.Orm.Raw(sql, startTime, endTime, startTime, endTime, startTime, endTime, startTime, endTime).Scan(&data).Error + err := e.Orm.Raw(sql, args...).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 // 保存汇总数据 + summaryData = &item } else { - filteredData = append(filteredData, item) // 只保留小时数据 + 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 } @@ -3101,14 +3119,13 @@ func (e MiGuDeployService) queryHistoricalDataByHour(startTime, endTime string, // 分页处理 start := (req.PageNum - 1) * req.PageSize end := start + req.PageSize - if end > len(filteredData) { - end = len(filteredData) - } if start > len(filteredData) { start = 0 } + if end > len(filteredData) { + end = len(filteredData) + } paginatedData := filteredData[start:end] - // 返回分页数据 return paginatedData, summaryData, len(filteredData), nil } diff --git a/app/admin/models/migu.go b/app/admin/models/migu.go index deac058..388431a 100644 --- a/app/admin/models/migu.go +++ b/app/admin/models/migu.go @@ -2058,3 +2058,79 @@ func ExportTransactionToExcel(data []MgTransactionLog, db *gorm.DB) (string, err return url, nil } + +type MonthlyEffectiveUserStats struct { + Date string `json:"date"` // 留存日期(格式:YYYY-MM-DD) + NewUserCount int `json:"new_user_count"` + ValidUserCount int `json:"valid_user_count"` + EffectiveRate string `json:"effective_rate"` // 百分比格式:如 "85.63%" + UnsubscribedToday int64 `json:"unsubscribed_today"` +} + +// GetMonthlyEffectiveUserStats 获取某月份有效用户统计 +func GetMonthlyEffectiveUserStats(db *gorm.DB, retentionMonth string, skuCode int, channelCode string) (*MonthlyEffectiveUserStats, error) { + var stats MonthlyEffectiveUserStats + + // 解析月份时间范围 + startTime, err := time.Parse("2006-01", retentionMonth) + if err != nil { + return nil, fmt.Errorf("invalid retentionMonth format: %v", err) + } + endTime := startTime.AddDate(0, 1, 0) // 下个月 + lastDay := endTime.AddDate(0, 0, -1) // 当前月的最后一天 + lastDayStr := lastDay.Format("2006-01-02") // 格式化为字符串 + + // 设置返回的留存日期字段 + stats.Date = lastDayStr + + // 查询每月新用户数和有效用户数 + var monthlyData []struct { + Month string + NewUserCount int + ValidUsersCount int + } + err = db.Model(&MgOrder{}). + Select(`DATE_FORMAT(subscribe_time, '%Y-%m') AS month, + COUNT(DISTINCT phone_number) AS new_user_count, + COUNT(DISTINCT CASE + WHEN unsubscribe_time IS NULL OR TIMESTAMPDIFF(HOUR, subscribe_time, unsubscribe_time) > 24 + THEN phone_number END) AS valid_users_count`). + Where("product_id = ?", skuCode). + Where("channel_code = ?", channelCode). + Group("month"). + Order("month"). + Find(&monthlyData).Error + if err != nil { + return nil, err + } + + // 匹配当前月份的数据 + for _, data := range monthlyData { + if data.Month == retentionMonth { + stats.NewUserCount = data.NewUserCount + stats.ValidUserCount = data.ValidUsersCount + break + } + } + + // 计算有效用户率 + if stats.NewUserCount > 0 { + rate := float64(stats.ValidUserCount) / float64(stats.NewUserCount) * 100 + stats.EffectiveRate = fmt.Sprintf("%.2f%%", rate) + } else { + stats.EffectiveRate = "0.00%" + } + + // 查询今天的退订用户数 + err = db.Model(&MgOrder{}). + Where("unsubscribe_time >= ? AND unsubscribe_time <= ?", lastDayStr+" 00:00:00", lastDayStr+" 23:59:59"). + Where("state = 2"). + Where("product_id = ?", skuCode). + Where("channel_code = ?", channelCode). + Count(&stats.UnsubscribedToday).Error + if err != nil { + return nil, err + } + + return &stats, nil +}