You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

122 lines
4.0 KiB
Go

package storage
import (
"context"
"encoding/json"
"errors"
"fmt"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/denisovdennis/autohero/internal/model"
)
// OfflineDigestRow is persisted counters + loot lines for the post-offline summary.
type OfflineDigestRow struct {
MonstersKilled int `json:"monstersKilled"`
XPGained int64 `json:"xpGained"`
GoldGained int64 `json:"goldGained"`
LevelsGained int `json:"levelsGained"`
Deaths int `json:"deaths"`
Revives int `json:"revives"`
Loot []model.LootDrop `json:"loot"`
UpdatedAt time.Time `json:"updatedAt"`
}
// OfflineDigestStore accumulates hero_offline_digest rows.
type OfflineDigestStore struct {
pool *pgxpool.Pool
}
func NewOfflineDigestStore(pool *pgxpool.Pool) *OfflineDigestStore {
return &OfflineDigestStore{pool: pool}
}
// ApplyDelta merges a delta into the hero's digest row (upsert).
func (s *OfflineDigestStore) ApplyDelta(ctx context.Context, heroID int64, d OfflineDigestDelta) error {
if d.MonstersKilled == 0 && d.XPGained == 0 && d.GoldGained == 0 && d.LevelsGained == 0 &&
d.Deaths == 0 && d.Revives == 0 && len(d.LootAppend) == 0 {
return nil
}
lootFragment := "[]"
if len(d.LootAppend) > 0 {
b, err := json.Marshal(d.LootAppend)
if err != nil {
return fmt.Errorf("marshal loot fragment: %w", err)
}
lootFragment = string(b)
}
_, err := s.pool.Exec(ctx, `
INSERT INTO hero_offline_digest (
hero_id, monsters_killed, xp_gained, gold_gained, levels_gained, deaths, revives, loot, updated_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, now())
ON CONFLICT (hero_id) DO UPDATE SET
monsters_killed = hero_offline_digest.monsters_killed + EXCLUDED.monsters_killed,
xp_gained = hero_offline_digest.xp_gained + EXCLUDED.xp_gained,
gold_gained = hero_offline_digest.gold_gained + EXCLUDED.gold_gained,
levels_gained = hero_offline_digest.levels_gained + EXCLUDED.levels_gained,
deaths = hero_offline_digest.deaths + EXCLUDED.deaths,
revives = hero_offline_digest.revives + EXCLUDED.revives,
loot = hero_offline_digest.loot || EXCLUDED.loot,
updated_at = now()
`, heroID, d.MonstersKilled, d.XPGained, d.GoldGained, d.LevelsGained, d.Deaths, d.Revives, lootFragment)
if err != nil {
return fmt.Errorf("apply offline digest delta: %w", err)
}
return nil
}
// OfflineDigestDelta is a single batch of offline stats to merge.
type OfflineDigestDelta struct {
MonstersKilled int
XPGained int64
GoldGained int64
LevelsGained int
Deaths int
Revives int
LootAppend []model.LootDrop
}
// Get returns the current digest row or zero values if missing.
func (s *OfflineDigestStore) Get(ctx context.Context, heroID int64) (OfflineDigestRow, error) {
var row OfflineDigestRow
var lootRaw []byte
err := s.pool.QueryRow(ctx, `
SELECT monsters_killed, xp_gained, gold_gained, levels_gained, deaths, revives, loot, updated_at
FROM hero_offline_digest WHERE hero_id = $1
`, heroID).Scan(
&row.MonstersKilled, &row.XPGained, &row.GoldGained, &row.LevelsGained,
&row.Deaths, &row.Revives, &lootRaw, &row.UpdatedAt,
)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
row.Loot = []model.LootDrop{}
return row, nil
}
return OfflineDigestRow{}, fmt.Errorf("get offline digest: %w", err)
}
if len(lootRaw) > 0 {
if err := json.Unmarshal(lootRaw, &row.Loot); err != nil {
row.Loot = nil
}
}
if row.Loot == nil {
row.Loot = []model.LootDrop{}
}
return row, nil
}
// TakeDelete returns the digest and removes the row (for handing summary to client once).
func (s *OfflineDigestStore) TakeDelete(ctx context.Context, heroID int64) (OfflineDigestRow, error) {
row, err := s.Get(ctx, heroID)
if err != nil {
return OfflineDigestRow{}, err
}
if _, err := s.pool.Exec(ctx, `DELETE FROM hero_offline_digest WHERE hero_id = $1`, heroID); err != nil {
return OfflineDigestRow{}, fmt.Errorf("delete offline digest: %w", err)
}
return row, nil
}