Initial commit

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 13:57:10 +08:00
co-authored by Claude Opus 5
commit 8679200f41
1897 changed files with 257900 additions and 0 deletions
+205
View File
@@ -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)
}