Align daily active metric with active log

This commit is contained in:
zhx 2026-05-27 14:05:19 +08:00
parent 0c0abcfe3c
commit 0b5e687353

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"database/sql" "database/sql"
"log/slog" "log/slog"
"strings"
"time" "time"
"go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/bson"
@ -16,9 +17,16 @@ import (
const activeLogCollection = "user_daily_active_log" const activeLogCollection = "user_daily_active_log"
type activeLogRow struct { type activeLogRow struct {
UserID int64 `bson:"userId"` UserID int64 `bson:"userId"`
ActiveDay string `bson:"activeDate"` ActiveDay string `bson:"activeDate"`
SysOrigin string `bson:"sysOrigin"` SysOrigin string `bson:"sysOrigin"`
RegisterCountryCode string `bson:"registerCountryCode"`
}
type activeLogEvent struct {
UserID int64
SysOrigin string
Country dashboard.Country
} }
type activeDayStats struct { type activeDayStats struct {
@ -107,7 +115,7 @@ func (r *MySQLRepository) RebuildActiveRetentionRange(ctx context.Context, db *m
}) })
} }
func (r *MySQLRepository) loadActiveLogs(ctx context.Context, db *mongo.Database, start, end time.Time) (map[string]map[int64]struct{}, error) { func (r *MySQLRepository) loadActiveLogs(ctx context.Context, db *mongo.Database, start, end time.Time) (map[string]map[int64]activeLogEvent, error) {
filter := bson.D{ filter := bson.D{
{Key: "activeDate", Value: bson.D{{Key: "$gte", Value: start.Format("2006-01-02")}, {Key: "$lt", Value: end.Format("2006-01-02")}}}, {Key: "activeDate", Value: bson.D{{Key: "$gte", Value: start.Format("2006-01-02")}, {Key: "$lt", Value: end.Format("2006-01-02")}}},
{Key: "sysOrigin", Value: bson.D{{Key: "$in", Value: r.cfg.Dashboard.SysOrigins}}}, {Key: "sysOrigin", Value: bson.D{{Key: "$in", Value: r.cfg.Dashboard.SysOrigins}}},
@ -116,6 +124,7 @@ func (r *MySQLRepository) loadActiveLogs(ctx context.Context, db *mongo.Database
{Key: "userId", Value: 1}, {Key: "userId", Value: 1},
{Key: "activeDate", Value: 1}, {Key: "activeDate", Value: 1},
{Key: "sysOrigin", Value: 1}, {Key: "sysOrigin", Value: 1},
{Key: "registerCountryCode", Value: 1},
}) })
if r.cfg.Active.BatchLimit > 0 { if r.cfg.Active.BatchLimit > 0 {
findOptions.SetLimit(int64(r.cfg.Active.BatchLimit)) findOptions.SetLimit(int64(r.cfg.Active.BatchLimit))
@ -126,7 +135,7 @@ func (r *MySQLRepository) loadActiveLogs(ctx context.Context, db *mongo.Database
} }
defer cursor.Close(ctx) defer cursor.Close(ctx)
events := make(map[string]map[int64]struct{}) events := make(map[string]map[int64]activeLogEvent)
for cursor.Next(ctx) { for cursor.Next(ctx) {
var row activeLogRow var row activeLogRow
if err := cursor.Decode(&row); err != nil { if err := cursor.Decode(&row); err != nil {
@ -137,15 +146,27 @@ func (r *MySQLRepository) loadActiveLogs(ctx context.Context, db *mongo.Database
} }
users := events[row.ActiveDay] users := events[row.ActiveDay]
if users == nil { if users == nil {
users = make(map[int64]struct{}) users = make(map[int64]activeLogEvent)
events[row.ActiveDay] = users events[row.ActiveDay] = users
} }
users[row.UserID] = struct{}{} users[row.UserID] = activeLogEvent{
UserID: row.UserID,
SysOrigin: strings.TrimSpace(row.SysOrigin),
Country: r.activeLogCountry(row.RegisterCountryCode),
}
} }
return events, cursor.Err() return events, cursor.Err()
} }
func collectActiveUserIDs(events map[string]map[int64]struct{}) []int64 { func (r *MySQLRepository) activeLogCountry(registerCountryCode string) dashboard.Country {
code := strings.TrimSpace(registerCountryCode)
if code == "" {
code = r.cfg.Dashboard.UnknownCountryCode
}
return dashboard.Country{Code: code, Name: code}
}
func collectActiveUserIDs(events map[string]map[int64]activeLogEvent) []int64 {
seen := make(map[int64]struct{}) seen := make(map[int64]struct{})
for _, users := range events { for _, users := range events {
for userID := range users { for userID := range users {
@ -159,6 +180,14 @@ func collectActiveUserIDs(events map[string]map[int64]struct{}) []int64 {
return out return out
} }
func activeUserSet(events map[int64]activeLogEvent) map[int64]struct{} {
out := make(map[int64]struct{}, len(events))
for userID := range events {
out[userID] = struct{}{}
}
return out
}
func (r *MySQLRepository) loadUserCountriesByID(ctx context.Context, ids []int64) (map[int64]dashboard.UserCountry, error) { func (r *MySQLRepository) loadUserCountriesByID(ctx context.Context, ids []int64) (map[int64]dashboard.UserCountry, error) {
result := make(map[int64]dashboard.UserCountry) result := make(map[int64]dashboard.UserCountry)
for start := 0; start < len(ids); start += bulkInsertChunkSize { for start := 0; start < len(ids); start += bulkInsertChunkSize {
@ -198,32 +227,43 @@ WHERE user_id IN `+inPlaceholders(end-start), args...)
} }
func (r *MySQLRepository) replaceActiveRetentionDay(ctx context.Context, tx *sql.Tx, day time.Time, func (r *MySQLRepository) replaceActiveRetentionDay(ctx context.Context, tx *sql.Tx, day time.Time,
events map[string]map[int64]struct{}, users map[int64]dashboard.UserCountry) error { events map[string]map[int64]activeLogEvent, users map[int64]dashboard.UserCountry) error {
dayKey := day.Format("2006-01-02") dayKey := day.Format("2006-01-02")
statsByOrigin := make(map[string]map[string]*activeDayStats) statsByOrigin := make(map[string]map[string]*activeDayStats)
for userID := range events[dayKey] { for userID, event := range events[dayKey] {
user, ok := users[userID] sysOrigin := strings.TrimSpace(event.SysOrigin)
if !ok || user.IsDeleted || !r.allowedOrigin(user.SysOrigin) { if !r.allowedOrigin(sysOrigin) {
continue continue
} }
stats := statsByOrigin[user.SysOrigin] country := event.Country
if country.Code == "" {
if user, ok := users[userID]; ok && user.Country.Code != "" {
country = user.Country
} else {
country = dashboard.Country{Code: r.cfg.Dashboard.UnknownCountryCode, Name: r.cfg.Dashboard.UnknownCountryCode}
}
}
if country.Name == "" {
country.Name = country.Code
}
stats := statsByOrigin[sysOrigin]
if stats == nil { if stats == nil {
stats = make(map[string]*activeDayStats) stats = make(map[string]*activeDayStats)
statsByOrigin[user.SysOrigin] = stats statsByOrigin[sysOrigin] = stats
} }
stat := stats[user.Country.Code] stat := stats[country.Code]
if stat == nil { if stat == nil {
stat = &activeDayStats{active: make(map[string]map[int64]struct{})} stat = &activeDayStats{active: make(map[string]map[int64]struct{})}
stats[user.Country.Code] = stat stats[country.Code] = stat
} }
active := stat.active[user.Country.Name] active := stat.active[country.Name]
if active == nil { if active == nil {
active = make(map[int64]struct{}) active = make(map[int64]struct{})
stat.active[user.Country.Name] = active stat.active[country.Name] = active
} }
active[userID] = struct{}{} active[userID] = struct{}{}
} }
retentionByOrigin, err := r.retentionStatsForDay(ctx, day, events[dayKey], users) retentionByOrigin, err := r.retentionStatsForDay(ctx, day, activeUserSet(events[dayKey]), users)
if err != nil { if err != nil {
return err return err
} }