1、咪咕管理后台用户留存记录(按天)查询优化,先分页再查询;

This commit is contained in:
chenlin 2025-06-12 11:07:05 +08:00
parent 86d392504b
commit 4dc1acec30

View File

@ -1573,73 +1573,72 @@ func (e MiGuDeployService) UserDayRetentionList(c *gin.Context) {
// 使用 goroutine 处理并发查询
var wg sync.WaitGroup
// 创建一个 channel 来接收每一天的查询结果
//resultChan := make(chan models.MgUserDayRetention, 100)
// 计算预计查询天数(或月份)
var totalDays int
// 1. 构建全部查询日期列表
var dateList []string
if req.OnlyFirstDay {
// 当前月的1号
now := time.Now()
currentMonthFirst := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
// 计算从 nextMonth 到 currentMonthFirst 包含的1号数量
totalDays = countFirstDays(nextMonth, currentMonthFirst)
// 每月1号
for date := nextMonth; !date.After(time.Now()); date = date.AddDate(1, 0, 0) {
dateList = append(dateList, date.Format("2006-01-02"))
}
} else {
// 按天查询:计算两个日期之间的天数
// 每天
startDateTime, _ := time.Parse("2006-01-02", startDate)
currentTime, _ := time.Parse("2006-01-02", currentDate)
totalDays = int(currentTime.Sub(startDateTime).Hours()/24) + 1
if totalDays <= 0 {
totalDays = 1
for d := startDateTime; !d.After(currentTime); d = d.AddDate(0, 0, 1) {
dateList = append(dateList, d.Format("2006-01-02"))
}
}
resultChan := make(chan models.MgUserDayRetention, totalDays)
// 按日期降序排列
sort.Sort(sort.Reverse(sort.StringSlice(dateList)))
// 2. 计算总页数和总记录数
totalCount := len(dateList)
resp.Count = totalCount
// 3. 做分页裁剪
startIdx := pageNum * req.PageSize
endIdx := startIdx + req.PageSize
if startIdx > totalCount {
startIdx = totalCount
}
if endIdx > totalCount {
endIdx = totalCount
}
pagedDates := dateList[startIdx:endIdx]
// 控制并发数的最大值,避免数据库过载
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
}
resultChan := make(chan models.MgUserDayRetention, len(pagedDates))
// 4. 遍历 pagedDates 查询留存数据
for _, date := range pagedDates {
wg.Add(1)
sem <- struct{}{} // 占一个信号,表示有一个 goroutine 正在执行
sem <- struct{}{}
go func(date string) {
defer wg.Done()
defer func() { <-sem }() // 释放一个信号,表示该 goroutine 执行完毕
defer func() { <-sem }()
var retainedUserCount int64
var unsubOnDay int64
var localErr error // 使用局部变量
var localErr error
// 查询该天的留存用户数
localErr = e.Orm.Model(&models.MgOrder{}).
Where("state = 1 AND created_at between ? and ?", currentMonthFirstDay, currentMonthNextFirstDay).
Where("product_id = ?", req.SkuCode). // 添加SkuCode条件
Where("channel_code = ?", req.Channel). // 添加Channel条件
Or("state = 2 AND unsubscribe_time > ? AND created_at between ? and ? AND product_id = ? "+
"and channel_code = ?", date+" 23:59:59",
currentMonthFirstDay, currentMonthNextFirstDay, req.SkuCode, req.Channel).
Where("product_id = ?", req.SkuCode).
Where("channel_code = ?", req.Channel).
Or("state = 2 AND unsubscribe_time > ? AND created_at between ? and ? AND product_id = ? and channel_code = ?",
date+" 23:59:59", currentMonthFirstDay, currentMonthNextFirstDay, req.SkuCode, req.Channel).
Count(&retainedUserCount).Error
// 查询该天的退订用户数
localErr = 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表示退订
Where("product_id = ?", req.SkuCode). // 添加SkuCode条件
Where("channel_code = ?", req.Channel). // 添加Channel条件
Where("state = 2").
Where("product_id = ?", req.SkuCode).
Where("channel_code = ?", req.Channel).
Count(&unsubOnDay).Error
if localErr != nil {
@ -1647,94 +1646,18 @@ func (e MiGuDeployService) UserDayRetentionList(c *gin.Context) {
return
}
// 留存率计算如果新增用户数为0留存率为0
var retentionRate string
retentionRate := "0.00%"
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
var localErr error // 使用局部变量
// 查询该天的留存用户数
err = e.Orm.Model(&models.MgOrder{}).
Where("state = 1 AND created_at between ? and ?", currentMonthFirstDay, currentMonthNextFirstDay).
Where("product_id = ?", req.SkuCode). // 添加SkuCode条件
Where("channel_code = ?", req.Channel). // 添加Channel条件
Or("state = 2 AND unsubscribe_time > ? AND created_at between ? and ? AND product_id = ? "+
"and channel_code = ?", date+" 23:59:59",
currentMonthFirstDay, currentMonthNextFirstDay, req.SkuCode, req.Channel).
Count(&retainedUserCount).Error
//retainedUserCount, localErr = getRetentionForDay(date, e.Orm, currentMonthFirstDay, currentMonthNextFirstDay)
// 查询该天的退订用户数
localErr = 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表示退订
Where("product_id = ?", req.SkuCode). // 添加SkuCode条件
Where("channel_code = ?", req.Channel). // 添加Channel条件
Count(&unsubOnDay).Error
if localErr != nil {
e.Logger.Error(localErr)
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")
}
}(date)
}
// 等待所有 goroutine 执行完毕
@ -1746,41 +1669,324 @@ func (e MiGuDeployService) UserDayRetentionList(c *gin.Context) {
retentionData = append(retentionData, result)
}
if len(retentionData) < req.PageSize {
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
})
// 分页处理
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.List = retentionData
resp.Count = totalRecords
e.OK(resp, "success")
}
//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").
// Where("product_id = ?", req.SkuCode). // 添加SkuCode条件
// Where("channel_code = ?", req.Channel). // 添加Channel条件
// 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
// }
//
// 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 {
// 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)
//
// // 计算预计查询天数(或月份)
// var totalDays int
// if req.OnlyFirstDay {
// // 当前月的1号
// now := time.Now()
// currentMonthFirst := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
// // 计算从 nextMonth 到 currentMonthFirst 包含的1号数量
// totalDays = countFirstDays(nextMonth, currentMonthFirst)
// } else {
// // 按天查询:计算两个日期之间的天数
// startDateTime, _ := time.Parse("2006-01-02", startDate)
// currentTime, _ := time.Parse("2006-01-02", currentDate)
// totalDays = int(currentTime.Sub(startDateTime).Hours()/24) + 1
// if totalDays <= 0 {
// totalDays = 1
// }
// }
// resultChan := make(chan models.MgUserDayRetention, totalDays)
//
// // 控制并发数的最大值,避免数据库过载
// 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
// var localErr error // 使用局部变量
//
// // 查询该天的留存用户数
// localErr = e.Orm.Model(&models.MgOrder{}).
// Where("state = 1 AND created_at between ? and ?", currentMonthFirstDay, currentMonthNextFirstDay).
// Where("product_id = ?", req.SkuCode). // 添加SkuCode条件
// Where("channel_code = ?", req.Channel). // 添加Channel条件
// Or("state = 2 AND unsubscribe_time > ? AND created_at between ? and ? AND product_id = ? "+
// "and channel_code = ?", date+" 23:59:59",
// currentMonthFirstDay, currentMonthNextFirstDay, req.SkuCode, req.Channel).
// Count(&retainedUserCount).Error
//
// // 查询该天的退订用户数
// localErr = 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表示退订
// Where("product_id = ?", req.SkuCode). // 添加SkuCode条件
// Where("channel_code = ?", req.Channel). // 添加Channel条件
// Count(&unsubOnDay).Error
//
// if localErr != nil {
// e.Logger.Error(localErr)
// 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
// var localErr error // 使用局部变量
//
// // 查询该天的留存用户数
// err = e.Orm.Model(&models.MgOrder{}).
// Where("state = 1 AND created_at between ? and ?", currentMonthFirstDay, currentMonthNextFirstDay).
// Where("product_id = ?", req.SkuCode). // 添加SkuCode条件
// Where("channel_code = ?", req.Channel). // 添加Channel条件
// Or("state = 2 AND unsubscribe_time > ? AND created_at between ? and ? AND product_id = ? "+
// "and channel_code = ?", date+" 23:59:59",
// currentMonthFirstDay, currentMonthNextFirstDay, req.SkuCode, req.Channel).
// Count(&retainedUserCount).Error
//
// //retainedUserCount, localErr = getRetentionForDay(date, e.Orm, currentMonthFirstDay, currentMonthNextFirstDay)
//
// // 查询该天的退订用户数
// localErr = 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表示退订
// Where("product_id = ?", req.SkuCode). // 添加SkuCode条件
// Where("channel_code = ?", req.Channel). // 添加Channel条件
// Count(&unsubOnDay).Error
//
// if localErr != nil {
// e.Logger.Error(localErr)
// 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)
// }
//
// 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
// })
//
// // 分页处理
// 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")
//}
// monthsBetween 返回 start 和 end 之间完整的月份数。
// 如果 end 的日期在 start 的日期之前(不满一个月),则不计入。
func monthsBetween(start, end time.Time) int {