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) }