206 lines
6.7 KiB
Go
206 lines
6.7 KiB
Go
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)
|
|
}
|