diff --git a/app/admin/apis/migumanage/migu_admin.go b/app/admin/apis/migumanage/migu_admin.go index 77f67b9..5e24e40 100644 --- a/app/admin/apis/migumanage/migu_admin.go +++ b/app/admin/apis/migumanage/migu_admin.go @@ -1573,168 +1573,91 @@ 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 执行 + resultChan := make(chan models.MgUserDayRetention, len(pagedDates)) + // 4. 遍历 pagedDates 查询留存数据 + for _, date := range pagedDates { + wg.Add(1) + sem <- struct{}{} - // 根据 OnlyFirstDay 参数调整查询逻辑 - if req.OnlyFirstDay { - // 如果只查询每个月1号的数据 - for { - // 增加日期(开始日期自增一个月的1号) - startDateTime, _ := time.Parse("2006-01-02", startDate) + go func(date string) { + defer wg.Done() + defer func() { <-sem }() - // 如果当前日期大于等于startDate,且是查询下一个月的1号,则退出 - if startDateTime.After(time.Now()) { - break + 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). + 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"). + Where("product_id = ?", req.SkuCode). + Where("channel_code = ?", req.Channel). + Count(&unsubOnDay).Error + + if localErr != nil { + e.Logger.Error(localErr) + return } - 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 + retentionRate := "0.00%" + if newUserCount > 0 { + retentionRate = fmt.Sprintf("%.2f%%", float64(retainedUserCount)*100/float64(newUserCount)) } - 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") - } + resultChan <- models.MgUserDayRetention{ + Date: date, + RetainedUserCount: int(retainedUserCount), + RetentionRate: retentionRate, + UserUnsubOnDay: int(unsubOnDay), + } + }(date) } // 等待所有 goroutine 执行完毕 @@ -1746,41 +1669,324 @@ 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) + 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 {