first commit
This commit is contained in:
47
internal/db/migrations.go
Normal file
47
internal/db/migrations.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package db
|
||||
|
||||
var migrations = []string{
|
||||
// 0: accounts table
|
||||
`CREATE TABLE IF NOT EXISTS accounts (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
username VARCHAR(32) UNIQUE NOT NULL,
|
||||
password VARCHAR(128) NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
)`,
|
||||
|
||||
// 1: characters table
|
||||
`CREATE TABLE IF NOT EXISTS characters (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
account_id BIGINT NOT NULL REFERENCES accounts(id),
|
||||
name VARCHAR(32) UNIQUE NOT NULL,
|
||||
level INT NOT NULL DEFAULT 1,
|
||||
exp BIGINT NOT NULL DEFAULT 0,
|
||||
hp INT NOT NULL DEFAULT 100,
|
||||
max_hp INT NOT NULL DEFAULT 100,
|
||||
mp INT NOT NULL DEFAULT 50,
|
||||
max_mp INT NOT NULL DEFAULT 50,
|
||||
str INT NOT NULL DEFAULT 10,
|
||||
dex INT NOT NULL DEFAULT 10,
|
||||
int_stat INT NOT NULL DEFAULT 10,
|
||||
zone_id INT NOT NULL DEFAULT 1,
|
||||
pos_x REAL NOT NULL DEFAULT 0,
|
||||
pos_y REAL NOT NULL DEFAULT 0,
|
||||
pos_z REAL NOT NULL DEFAULT 0,
|
||||
rotation REAL NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
)`,
|
||||
|
||||
// 2: inventory table
|
||||
`CREATE TABLE IF NOT EXISTS inventory (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
character_id BIGINT NOT NULL REFERENCES characters(id) ON DELETE CASCADE,
|
||||
slot INT NOT NULL,
|
||||
item_id INT NOT NULL,
|
||||
quantity INT NOT NULL DEFAULT 1,
|
||||
UNIQUE(character_id, slot)
|
||||
)`,
|
||||
|
||||
// 3: index for character lookups by account
|
||||
`CREATE INDEX IF NOT EXISTS idx_characters_account_id ON characters(account_id)`,
|
||||
}
|
||||
51
internal/db/postgres.go
Normal file
51
internal/db/postgres.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"a301_game_server/config"
|
||||
"a301_game_server/pkg/logger"
|
||||
)
|
||||
|
||||
// Pool wraps a pgx connection pool.
|
||||
type Pool struct {
|
||||
*pgxpool.Pool
|
||||
}
|
||||
|
||||
// NewPool creates a connection pool to PostgreSQL.
|
||||
func NewPool(ctx context.Context, cfg *config.DatabaseConfig) (*Pool, error) {
|
||||
poolCfg, err := pgxpool.ParseConfig(cfg.DSN())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse dsn: %w", err)
|
||||
}
|
||||
|
||||
poolCfg.MaxConns = cfg.MaxConns
|
||||
poolCfg.MinConns = cfg.MinConns
|
||||
|
||||
pool, err := pgxpool.NewWithConfig(ctx, poolCfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create pool: %w", err)
|
||||
}
|
||||
|
||||
if err := pool.Ping(ctx); err != nil {
|
||||
pool.Close()
|
||||
return nil, fmt.Errorf("ping database: %w", err)
|
||||
}
|
||||
|
||||
logger.Info("database connected", "host", cfg.Host, "db", cfg.DBName)
|
||||
return &Pool{pool}, nil
|
||||
}
|
||||
|
||||
// RunMigrations executes all schema migrations.
|
||||
func (p *Pool) RunMigrations(ctx context.Context) error {
|
||||
for i, m := range migrations {
|
||||
if _, err := p.Exec(ctx, m); err != nil {
|
||||
return fmt.Errorf("migration %d failed: %w", i, err)
|
||||
}
|
||||
}
|
||||
logger.Info("database migrations completed", "count", len(migrations))
|
||||
return nil
|
||||
}
|
||||
166
internal/db/repository/character.go
Normal file
166
internal/db/repository/character.go
Normal file
@@ -0,0 +1,166 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"a301_game_server/internal/db"
|
||||
)
|
||||
|
||||
// CharacterData holds persisted character state.
|
||||
type CharacterData struct {
|
||||
ID int64
|
||||
AccountID int64
|
||||
Name string
|
||||
Level int32
|
||||
Exp int64
|
||||
HP int32
|
||||
MaxHP int32
|
||||
MP int32
|
||||
MaxMP int32
|
||||
Str int32
|
||||
Dex int32
|
||||
IntStat int32
|
||||
ZoneID int32
|
||||
PosX float32
|
||||
PosY float32
|
||||
PosZ float32
|
||||
Rotation float32
|
||||
}
|
||||
|
||||
// CharacterRepo handles character persistence.
|
||||
type CharacterRepo struct {
|
||||
pool *db.Pool
|
||||
}
|
||||
|
||||
// NewCharacterRepo creates a new character repository.
|
||||
func NewCharacterRepo(pool *db.Pool) *CharacterRepo {
|
||||
return &CharacterRepo{pool: pool}
|
||||
}
|
||||
|
||||
// Create inserts a new character.
|
||||
func (r *CharacterRepo) Create(ctx context.Context, accountID int64, name string) (*CharacterData, error) {
|
||||
c := &CharacterData{
|
||||
AccountID: accountID,
|
||||
Name: name,
|
||||
Level: 1,
|
||||
HP: 100,
|
||||
MaxHP: 100,
|
||||
MP: 50,
|
||||
MaxMP: 50,
|
||||
Str: 10,
|
||||
Dex: 10,
|
||||
IntStat: 10,
|
||||
ZoneID: 1,
|
||||
}
|
||||
|
||||
err := r.pool.QueryRow(ctx,
|
||||
`INSERT INTO characters (account_id, name, level, hp, max_hp, mp, max_mp, str, dex, int_stat, zone_id)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
||||
RETURNING id`,
|
||||
c.AccountID, c.Name, c.Level, c.HP, c.MaxHP, c.MP, c.MaxMP, c.Str, c.Dex, c.IntStat, c.ZoneID,
|
||||
).Scan(&c.ID)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create character: %w", err)
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// GetByAccountID returns all characters for an account.
|
||||
func (r *CharacterRepo) GetByAccountID(ctx context.Context, accountID int64) ([]*CharacterData, error) {
|
||||
rows, err := r.pool.Query(ctx,
|
||||
`SELECT id, account_id, name, level, exp, hp, max_hp, mp, max_mp, str, dex, int_stat,
|
||||
zone_id, pos_x, pos_y, pos_z, rotation
|
||||
FROM characters WHERE account_id = $1`, accountID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query characters: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var chars []*CharacterData
|
||||
for rows.Next() {
|
||||
c := &CharacterData{}
|
||||
if err := rows.Scan(
|
||||
&c.ID, &c.AccountID, &c.Name, &c.Level, &c.Exp,
|
||||
&c.HP, &c.MaxHP, &c.MP, &c.MaxMP,
|
||||
&c.Str, &c.Dex, &c.IntStat,
|
||||
&c.ZoneID, &c.PosX, &c.PosY, &c.PosZ, &c.Rotation,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("scan character: %w", err)
|
||||
}
|
||||
chars = append(chars, c)
|
||||
}
|
||||
return chars, nil
|
||||
}
|
||||
|
||||
// GetByID loads a single character.
|
||||
func (r *CharacterRepo) GetByID(ctx context.Context, id int64) (*CharacterData, error) {
|
||||
c := &CharacterData{}
|
||||
err := r.pool.QueryRow(ctx,
|
||||
`SELECT id, account_id, name, level, exp, hp, max_hp, mp, max_mp, str, dex, int_stat,
|
||||
zone_id, pos_x, pos_y, pos_z, rotation
|
||||
FROM characters WHERE id = $1`, id,
|
||||
).Scan(
|
||||
&c.ID, &c.AccountID, &c.Name, &c.Level, &c.Exp,
|
||||
&c.HP, &c.MaxHP, &c.MP, &c.MaxMP,
|
||||
&c.Str, &c.Dex, &c.IntStat,
|
||||
&c.ZoneID, &c.PosX, &c.PosY, &c.PosZ, &c.Rotation,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get character %d: %w", id, err)
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Save persists the current character state.
|
||||
func (r *CharacterRepo) Save(ctx context.Context, c *CharacterData) error {
|
||||
_, err := r.pool.Exec(ctx,
|
||||
`UPDATE characters SET
|
||||
level = $2, exp = $3, hp = $4, max_hp = $5, mp = $6, max_mp = $7,
|
||||
str = $8, dex = $9, int_stat = $10,
|
||||
zone_id = $11, pos_x = $12, pos_y = $13, pos_z = $14, rotation = $15,
|
||||
updated_at = NOW()
|
||||
WHERE id = $1`,
|
||||
c.ID, c.Level, c.Exp, c.HP, c.MaxHP, c.MP, c.MaxMP,
|
||||
c.Str, c.Dex, c.IntStat,
|
||||
c.ZoneID, c.PosX, c.PosY, c.PosZ, c.Rotation,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("save character %d: %w", c.ID, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveBatch saves multiple characters in a single transaction.
|
||||
func (r *CharacterRepo) SaveBatch(ctx context.Context, chars []*CharacterData) error {
|
||||
if len(chars) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
tx, err := r.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("begin tx: %w", err)
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
for _, c := range chars {
|
||||
_, err := tx.Exec(ctx,
|
||||
`UPDATE characters SET
|
||||
level = $2, exp = $3, hp = $4, max_hp = $5, mp = $6, max_mp = $7,
|
||||
str = $8, dex = $9, int_stat = $10,
|
||||
zone_id = $11, pos_x = $12, pos_y = $13, pos_z = $14, rotation = $15,
|
||||
updated_at = NOW()
|
||||
WHERE id = $1`,
|
||||
c.ID, c.Level, c.Exp, c.HP, c.MaxHP, c.MP, c.MaxMP,
|
||||
c.Str, c.Dex, c.IntStat,
|
||||
c.ZoneID, c.PosX, c.PosY, c.PosZ, c.Rotation,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("save character %d in batch: %w", c.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
Reference in New Issue
Block a user