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
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
|
|
}
|