2026-05-22 14:03:24 +08:00

103 lines
2.8 KiB
Go

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
}