1、历史汇总列表排序优化(按时间排序);

2、历史汇总(按小时)优化,支持不同产品筛选;
3、用户留存记录(按天)增加当月最后1天留存数据展示;
This commit is contained in:
chenlin 2025-04-07 19:50:10 +08:00
parent c818eef08e
commit 2863174547
2 changed files with 211 additions and 118 deletions

View File

@ -540,7 +540,7 @@ func (e MiGuDeployService) HistoricalSummaryListOld(c *gin.Context) {
`). `).
Where("mg_order.subscribe_time BETWEEN ? AND ?", startTime, endTime). Where("mg_order.subscribe_time BETWEEN ? AND ?", startTime, endTime).
Group("DATE(mg_order.subscribe_time), mg_order.product_id, mg_order.channel_code"). 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 { if req.SkuCode != 0 {
@ -1528,6 +1528,13 @@ func (e MiGuDeployService) UserDayRetentionList(c *gin.Context) {
return 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号开始 // 计算下个月的1号留存查询从下个月1号开始
parsedMonth, err := time.Parse("2006-01", req.RetentionMonth) parsedMonth, err := time.Parse("2006-01", req.RetentionMonth)
if err != nil { if err != nil {
@ -1725,6 +1732,13 @@ func (e MiGuDeployService) UserDayRetentionList(c *gin.Context) {
retentionData = append(retentionData, result) 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 { sort.SliceStable(retentionData, func(i, j int) bool {
return retentionData[i].Date > retentionData[j].Date 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) { func (e MiGuDeployService) queryHistoricalDataByHour(startTime, endTime string, req *models.HistoricalSummaryListReq) ([]models.MgHourSummary, *models.MgHourSummary, int, error) {
// 构建SQL查询字符串 // 动态构建产品和渠道过滤条件
sql := ` 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 * FROM (
SELECT SELECT
hour, hour,
product_id, product_id,
channel_code, channel_code,
SUM(new_user_count) AS new_user_count, SUM(new_user_count) AS new_user_count,
SUM(new_user_unsub_within_hour) AS new_user_unsub_within_hour, 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(new_user_unsub_on_day) AS new_user_unsub_on_day,
SUM(total_new_user_unsub) AS total_new_user_unsub, SUM(total_new_user_unsub) AS total_new_user_unsub,
SUM(submission_count) AS submission_count, SUM(submission_count) AS submission_count,
IF(SUM(submission_count) > 0, IF(SUM(submission_count) > 0,
CONCAT(ROUND(SUM(new_user_count) / SUM(submission_count) * 100, 2), '%'), CONCAT(ROUND(SUM(new_user_count) / SUM(submission_count) * 100, 2), '%%'),
'0.00%' '0.00%%'
) AS submission_success_rate, ) AS submission_success_rate,
IF(SUM(new_user_count) > 0, IF(SUM(new_user_count) > 0,
CONCAT(ROUND(SUM(new_user_unsub_within_hour) / SUM(new_user_count) * 100, 2), '%'), CONCAT(ROUND(SUM(new_user_unsub_within_hour) / SUM(new_user_count) * 100, 2), '%%'),
'0.00%' '0.00%%'
) AS new_user_unsub_within_hour_rate, ) AS new_user_unsub_within_hour_rate,
IF(SUM(new_user_count) > 0, IF(SUM(new_user_count) > 0,
CONCAT(ROUND(SUM(new_user_unsub_on_day) / SUM(new_user_count) * 100, 2), '%'), CONCAT(ROUND(SUM(new_user_unsub_on_day) / SUM(new_user_count) * 100, 2), '%%'),
'0.00%' '0.00%%'
) AS new_user_unsub_on_day_rate, ) AS new_user_unsub_on_day_rate,
IF(SUM(new_user_count) > 0, IF(SUM(new_user_count) > 0,
CONCAT(ROUND(SUM(total_new_user_unsub) / SUM(new_user_count) * 100, 2), '%'), CONCAT(ROUND(SUM(total_new_user_unsub) / SUM(new_user_count) * 100, 2), '%%'),
'0.00%' '0.00%%'
) AS total_new_user_unsub_rate ) AS total_new_user_unsub_rate
FROM ( FROM (
-- 第一个查询mg_order 数据 SELECT
SELECT HOUR(subscribe_time) AS hour,
HOUR(mg_order.created_at) AS hour, product_id,
product_id, channel_code,
channel_code, COUNT(*) AS new_user_count,
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 mg_order.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 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 state = 2 THEN 1 ELSE 0 END) AS total_new_user_unsub,
SUM(CASE WHEN mg_order.state = 2 THEN 1 ELSE 0 END) AS total_new_user_unsub, 0 AS submission_count
0 AS submission_count FROM mg_order
FROM WHERE subscribe_time BETWEEN ? AND ? %s
mg_order GROUP BY HOUR(subscribe_time), channel_code, product_id -- 按小时和渠道分组
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 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 SELECT
-- 第四个查询mg_transaction_log 总计数据 'Total' AS hour,
SELECT product_id,
'Total' AS hour, channel_code,
product_id AS product_id, COUNT(*) AS new_user_count,
channel_code AS channel_code, SUM(CASE WHEN is_one_hour_cancel = 1 THEN 1 ELSE 0 END),
0 AS new_user_count, SUM(CASE WHEN state = 2 AND DATE(unsubscribe_time) = DATE(subscribe_time) THEN 1 ELSE 0 END),
0 AS new_user_unsub_within_hour, SUM(CASE WHEN state = 2 THEN 1 ELSE 0 END),
0 AS new_user_unsub_on_day, 0
0 AS total_new_user_unsub, FROM mg_order
COUNT(*) AS submission_count WHERE subscribe_time BETWEEN ? AND ? %s
FROM GROUP BY channel_code, product_id -- 按渠道和产品分组
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;
`
// 执行查询 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 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 { if err != nil {
return nil, nil, 0, err return nil, nil, 0, err
} }
// 剔除 "Total" 数据 // 拆分小时数据与汇总数据
var filteredData []models.MgHourSummary var filteredData []models.MgHourSummary
var summaryData *models.MgHourSummary var summaryData *models.MgHourSummary
for _, item := range data { for _, item := range data {
if item.Hour == "Total" { if item.Hour == "Total" {
summaryData = &item // 保存汇总数据 summaryData = &item
} else { } else {
filteredData = append(filteredData, item) // 只保留小时数据 filteredData = append(filteredData, item)
} }
} }
// 按小时排序(确保 hour 是数字,排序正确) // 按小时降序排序
sort.Slice(filteredData, func(i, j int) bool { sort.Slice(filteredData, func(i, j int) bool {
// 将字符串类型的 hour 转换为整数进行比较
hourI, errI := strconv.Atoi(filteredData[i].Hour) hourI, errI := strconv.Atoi(filteredData[i].Hour)
hourJ, errJ := strconv.Atoi(filteredData[j].Hour) hourJ, errJ := strconv.Atoi(filteredData[j].Hour)
// 如果转换失败,按原样比较;否则按数字顺序排序
if errI != nil && errJ != nil { if errI != nil && errJ != nil {
return filteredData[i].Hour > filteredData[j].Hour return filteredData[i].Hour > filteredData[j].Hour
} }
if errI != nil { if errI != nil {
// 如果 i 的 hour 无法转换为数字,则认为它大于 j
return false return false
} }
if errJ != nil { if errJ != nil {
// 如果 j 的 hour 无法转换为数字,则认为它大于 i
return true return true
} }
return hourI > hourJ return hourI > hourJ
}) })
// 如果是导出数据,直接返回所有数据 // 导出模式:不分页
if req.IsExport == 1 { if req.IsExport == 1 {
return filteredData, summaryData, 0, nil return filteredData, summaryData, 0, nil
} }
@ -3101,14 +3119,13 @@ func (e MiGuDeployService) queryHistoricalDataByHour(startTime, endTime string,
// 分页处理 // 分页处理
start := (req.PageNum - 1) * req.PageSize start := (req.PageNum - 1) * req.PageSize
end := start + req.PageSize end := start + req.PageSize
if end > len(filteredData) {
end = len(filteredData)
}
if start > len(filteredData) { if start > len(filteredData) {
start = 0 start = 0
} }
if end > len(filteredData) {
end = len(filteredData)
}
paginatedData := filteredData[start:end] paginatedData := filteredData[start:end]
// 返回分页数据
return paginatedData, summaryData, len(filteredData), nil return paginatedData, summaryData, len(filteredData), nil
} }

View File

@ -2058,3 +2058,79 @@ func ExportTransactionToExcel(data []MgTransactionLog, db *gorm.DB) (string, err
return url, nil 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
}