package dashboard import ( "context" "database/sql" "log/slog" "time" "github.com/redis/go-redis/v9" "dashboard-cdc-worker/internal/config" ) type Projector struct { cfg config.Config store Store redisClient *redis.Client storageZone *time.Location statZones map[string]*time.Location } func NewProjector(cfg config.Config, store Store, redisClient *redis.Client) *Projector { storageZone := mustLocation(cfg.Dashboard.StorageTimezone) statZones := make(map[string]*time.Location, len(cfg.Dashboard.StatTimezones)) for _, zone := range cfg.Dashboard.StatTimezones { statZones[zone] = mustLocation(zone) } return &Projector{ cfg: cfg, store: store, redisClient: redisClient, storageZone: storageZone, statZones: statZones, } } func (p *Projector) Apply(ctx context.Context, eventKey string, source string, contributions []Contribution) error { return p.apply(ctx, eventKey, source, nil, contributions) } func (p *Projector) ApplyUser(ctx context.Context, eventKey string, source string, user UserCountry, contributions []Contribution) error { return p.apply(ctx, eventKey, source, &user, contributions) } func (p *Projector) apply(ctx context.Context, eventKey string, source string, user *UserCountry, contributions []Contribution) error { if user == nil && len(contributions) == 0 { return nil } if len(contributions) == 0 { contributions = nil } if p.cfg.CDC.DryRun { slog.Info("dashboard cdc dry run", "eventKey", eventKey, "source", source, "user", user != nil, "contributions", len(contributions)) return nil } if err := p.store.WithTx(ctx, func(tx *sql.Tx) error { applied, err := p.store.MarkEventApplied(ctx, tx, eventKey, source) if err != nil { return err } if !applied { return nil } if user != nil { if err := p.store.UpsertUserCountry(ctx, tx, *user); err != nil { return err } } for _, contribution := range contributions { if contribution.Empty() { continue } if contribution.DeltaSign == 0 { contribution.DeltaSign = 1 } if err := p.store.ApplyDailyContribution(ctx, tx, contribution, p.cfg.Dashboard.StorageTimezone); err != nil { return err } for statTimezone := range p.statZones { if err := p.store.ApplyPeriodContribution(ctx, tx, contribution, statTimezone); err != nil { return err } if err := p.store.ApplyGamePeriodContribution(ctx, tx, contribution, statTimezone); err != nil { return err } } } return nil }); err != nil { return err } return p.writeRealtimeRedis(ctx, contributions) } func mustLocation(name string) *time.Location { location, err := time.LoadLocation(name) if err != nil { slog.Warn("invalid timezone, fallback to UTC", "timezone", name, "error", err) return time.UTC } return location }