@@ -0,0 +1,205 @@
|
||||
package dailyAdverCalc
|
||||
|
||||
import (
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/models/commod"
|
||||
"91porn-server/models/s/dailyretentionmod"
|
||||
"91porn-server/models/s/useradverstatmod"
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
// retentionKey 用于内存中按 (RegDay, AdGroup) 分组
|
||||
type retentionKey struct {
|
||||
RegDay time.Time
|
||||
AdGroup commod.AdGroup
|
||||
}
|
||||
|
||||
// retentionAccum 内存中的累计数据
|
||||
type retentionAccum struct {
|
||||
Retained int64
|
||||
AdClick int64
|
||||
TotalClick int64
|
||||
PayCount int64
|
||||
PayTotal int64
|
||||
}
|
||||
|
||||
// CalcRetention 计算留存数据
|
||||
// targetDate: 活跃日期(要统计哪一天的活跃数据)
|
||||
// 例如:targetDate=15号,会计算所有在15号活跃的用户,按其注册日分组,计算各自的第N天留存
|
||||
func CalcRetention(ctx context.Context, targetDate time.Time) error {
|
||||
targetDate = common.NormalizeDate(targetDate)
|
||||
startDate := targetDate.AddDate(0, 0, -dailyretentionmod.MaxRetentionDays)
|
||||
accumMap := make(map[retentionKey]*retentionAccum)
|
||||
|
||||
// 1. 在 Mongo 中按 (regDay, adGroup) 聚合,避免把当天明细全部拉回内存
|
||||
stats, err := findUserAdStatSummaryByDate(ctx, targetDate, startDate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("query UserAdStat failed: %w", err)
|
||||
}
|
||||
for _, stat := range stats {
|
||||
key := retentionKey{
|
||||
RegDay: common.NormalizeDate(stat.ID.RegDay),
|
||||
AdGroup: stat.ID.AdGroup,
|
||||
}
|
||||
if accumMap[key] == nil {
|
||||
accumMap[key] = &retentionAccum{}
|
||||
}
|
||||
accumMap[key].Retained = stat.Retained
|
||||
accumMap[key].AdClick = stat.AdClick
|
||||
accumMap[key].TotalClick = stat.TotalClick
|
||||
accumMap[key].PayCount = stat.PayCount
|
||||
accumMap[key].PayTotal = stat.PayTotal
|
||||
}
|
||||
log.Info("calc retention", log.Any("stats.total", len(stats)), log.Any("stats", stats))
|
||||
// 2. 批量写入 DailyRetention
|
||||
now := time.Now()
|
||||
writes := make([]mongo.WriteModel, 0, len(accumMap))
|
||||
for key, acc := range accumMap {
|
||||
// 计算是注册后第几天
|
||||
dayN := int(targetDate.Sub(key.RegDay).Hours() / 24)
|
||||
if dayN < 0 || dayN > dailyretentionmod.MaxRetentionDays {
|
||||
log.Info("calc retention check dayN", log.Any("dayN", dayN))
|
||||
continue // 超出范围,跳过
|
||||
}
|
||||
|
||||
retained := acc.Retained
|
||||
|
||||
// 获取已有记录(用于计算留存率和累计值)
|
||||
existingItem, _ := getDailyRetention(ctx, key.RegDay, key.AdGroup)
|
||||
cohortSize := retained
|
||||
if dayN > 0 && existingItem != nil {
|
||||
cohortSize = existingItem.NewUsers
|
||||
}
|
||||
rate := calculateRetentionRate(retained, cohortSize)
|
||||
|
||||
// 计算累计值:前一天累计 + 当天值
|
||||
prevAdClick, prevTotalClick, prevPayCount, prevPayTotal := getPrevAccum(existingItem, dayN)
|
||||
adClickAcc := prevAdClick + acc.AdClick
|
||||
totalClickAcc := prevTotalClick + acc.TotalClick
|
||||
payCountAcc := prevPayCount + acc.PayCount
|
||||
payTotalAcc := prevPayTotal + acc.PayTotal
|
||||
|
||||
writes = append(writes, buildDailyRetentionUpsertModel(
|
||||
key.RegDay, key.AdGroup, dayN, retained, rate,
|
||||
adClickAcc, totalClickAcc, payCountAcc, payTotalAcc, now,
|
||||
))
|
||||
}
|
||||
log.Info("calc retention ", log.Any("writes", writes))
|
||||
if err := dailyretentionmod.BulkWrite(writes, options.BulkWrite().SetOrdered(false)); err != nil {
|
||||
return fmt.Errorf("bulk upsert daily retention failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getDailyRetention 获取某个 (RegDay, AdGroup) 的 DailyRetention 记录
|
||||
func getDailyRetention(ctx context.Context, regDay time.Time, adGroup commod.AdGroup) (*dailyretentionmod.DailyRetention, error) {
|
||||
filter := bson.M{
|
||||
"date": regDay,
|
||||
"adGroup": adGroup,
|
||||
}
|
||||
return dailyretentionmod.QueryOne(filter)
|
||||
}
|
||||
|
||||
// buildDailyRetentionUpsertModel 构造留存数据 upsert 模型
|
||||
func buildDailyRetentionUpsertModel(regDay time.Time, adGroup commod.AdGroup, dayN int, retained int64, rate float64, adClick, totalClick, payCount, payTotal int64, now time.Time) mongo.WriteModel {
|
||||
filter, update := buildDailyRetentionUpsertData(regDay, adGroup, dayN, retained, rate, adClick, totalClick, payCount, payTotal, now)
|
||||
return mongo.NewUpdateOneModel().
|
||||
SetFilter(filter).
|
||||
SetUpdate(update).
|
||||
SetUpsert(true)
|
||||
}
|
||||
|
||||
func buildDailyRetentionUpsertData(regDay time.Time, adGroup commod.AdGroup, dayN int, retained int64, rate float64, adClick, totalClick, payCount, payTotal int64, now time.Time) (bson.M, bson.M) {
|
||||
regDay = common.NormalizeDate(regDay)
|
||||
|
||||
filter := bson.M{
|
||||
"date": regDay,
|
||||
"adGroup": adGroup,
|
||||
}
|
||||
|
||||
// 使用点号表示法更新 map 中的特定 key
|
||||
retainedKey := fmt.Sprintf("%s.%d", "userRetained", dayN)
|
||||
ratesKey := fmt.Sprintf("%s.%d", "userRates", dayN)
|
||||
adClickKey := fmt.Sprintf("%s.%d", "adClickAcc", dayN)
|
||||
totalClickKey := fmt.Sprintf("%s.%d", "totalClickAcc", dayN)
|
||||
payCountKey := fmt.Sprintf("%s.%d", "payCountAcc", dayN)
|
||||
payTotalKey := fmt.Sprintf("%s.%d", "payTotalAcc", dayN)
|
||||
|
||||
update := bson.M{
|
||||
"$setOnInsert": bson.M{
|
||||
"date": regDay,
|
||||
"adGroup": adGroup,
|
||||
"createdAt": now,
|
||||
},
|
||||
"$set": bson.M{
|
||||
"updatedAt": now,
|
||||
retainedKey: retained,
|
||||
ratesKey: rate,
|
||||
adClickKey: adClick,
|
||||
totalClickKey: totalClick,
|
||||
payCountKey: payCount,
|
||||
payTotalKey: payTotal,
|
||||
},
|
||||
}
|
||||
|
||||
// 如果是 day0,同时更新 NewUsers
|
||||
if dayN == 0 {
|
||||
update["$set"].(bson.M)["newUsers"] = retained
|
||||
}
|
||||
return filter, update
|
||||
}
|
||||
|
||||
func calculateRetentionRate(retained, cohortSize int64) float64 {
|
||||
if cohortSize <= 0 {
|
||||
return 0
|
||||
}
|
||||
return float64(retained) / float64(cohortSize)
|
||||
}
|
||||
|
||||
// getPrevAccum 获取前一天的累计值
|
||||
func getPrevAccum(item *dailyretentionmod.DailyRetention, dayN int) (adClick, totalClick, payCount, payTotal int64) {
|
||||
if item == nil || dayN <= 0 {
|
||||
return 0, 0, 0, 0
|
||||
}
|
||||
prevDay := dayN - 1
|
||||
if item.AdClickAcc != nil {
|
||||
adClick = item.AdClickAcc[prevDay]
|
||||
}
|
||||
if item.TotalClickAcc != nil {
|
||||
totalClick = item.TotalClickAcc[prevDay]
|
||||
}
|
||||
if item.PayCountAcc != nil {
|
||||
payCount = item.PayCountAcc[prevDay]
|
||||
}
|
||||
if item.PayTotalAcc != nil {
|
||||
payTotal = item.PayTotalAcc[prevDay]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// findDailyRetentionByDateRange 查询指定日期范围内的 DailyRetention 记录
|
||||
func findDailyRetentionByDateRange(ctx context.Context, startDate, endDate time.Time) ([]*dailyretentionmod.DailyRetention, error) {
|
||||
filter := bson.M{
|
||||
"date": bson.M{
|
||||
"$gte": startDate,
|
||||
"$lte": endDate,
|
||||
},
|
||||
}
|
||||
return dailyretentionmod.QueryAllList(filter)
|
||||
}
|
||||
|
||||
// findUserAdStatSummaryByDate 查询指定日期在留存窗口内的聚合数据
|
||||
func findUserAdStatSummaryByDate(ctx context.Context, date, startDate time.Time) ([]*useradverstatmod.DailyRetentionSummary, error) {
|
||||
local, _ := time.LoadLocation("Asia/Shanghai")
|
||||
date = time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, local)
|
||||
startDate = common.NormalizeDate(startDate)
|
||||
return useradverstatmod.AggregateDailyRetention(date, startDate, date)
|
||||
}
|
||||
Reference in New Issue
Block a user