refactor(channelserver): move remaining s.server.db calls into repositories

Eliminate the last three direct DB accesses from handler code:

- CharacterRepo.LoadSaveData: replaces db.Query in GetCharacterSaveData,
  using QueryRow instead of Query+Next for cleaner single-row access
- EventRepo.GetEventQuests, UpdateEventQuestStartTime, BeginTx: moves
  event quest enumeration and rotation queries behind the repo layer
- UserRepo.BanUser: consolidates permanent/temporary ban upserts into a
  single method with nil/*time.Time semantics
This commit is contained in:
Houmgaor
2026-02-21 14:08:01 +01:00
parent 9a473260b2
commit a9cca84bc3
8 changed files with 66 additions and 21 deletions

View File

@@ -1,6 +1,7 @@
package channelserver package channelserver
import ( import (
"database/sql"
"errors" "errors"
cfg "erupe-ce/config" cfg "erupe-ce/config"
@@ -11,26 +12,23 @@ import (
// GetCharacterSaveData loads a character's save data from the database. // GetCharacterSaveData loads a character's save data from the database.
func GetCharacterSaveData(s *Session, charID uint32) (*CharacterSaveData, error) { func GetCharacterSaveData(s *Session, charID uint32) (*CharacterSaveData, error) {
result, err := s.server.db.Query("SELECT id, savedata, is_new_character, name FROM characters WHERE id = $1", charID) id, savedata, isNew, name, err := s.server.charRepo.LoadSaveData(charID)
if err != nil { if err != nil {
if errors.Is(err, sql.ErrNoRows) {
s.logger.Error("No savedata found", zap.Uint32("charID", charID))
return nil, errors.New("no savedata found")
}
s.logger.Error("Failed to get savedata", zap.Error(err), zap.Uint32("charID", charID)) s.logger.Error("Failed to get savedata", zap.Error(err), zap.Uint32("charID", charID))
return nil, err return nil, err
} }
defer func() { _ = result.Close() }()
if !result.Next() {
err = errors.New("no savedata found")
s.logger.Error("No savedata found", zap.Uint32("charID", charID))
return nil, err
}
saveData := &CharacterSaveData{ saveData := &CharacterSaveData{
Mode: s.server.erupeConfig.RealClientMode, CharID: id,
Pointers: getPointers(s.server.erupeConfig.RealClientMode), compSave: savedata,
} IsNewCharacter: isNew,
err = result.Scan(&saveData.CharID, &saveData.compSave, &saveData.IsNewCharacter, &saveData.Name) Name: name,
if err != nil { Mode: s.server.erupeConfig.RealClientMode,
s.logger.Error("Failed to scan savedata", zap.Error(err), zap.Uint32("charID", charID)) Pointers: getPointers(s.server.erupeConfig.RealClientMode),
return nil, err
} }
if saveData.compSave == nil { if saveData.compSave == nil {

View File

@@ -104,14 +104,12 @@ func parseChatCommand(s *Session, command string) {
uid, uname, err := s.server.userRepo.GetByIDAndUsername(cid) uid, uname, err := s.server.userRepo.GetByIDAndUsername(cid)
if err == nil { if err == nil {
if expiry.IsZero() { if expiry.IsZero() {
if _, err := s.server.db.Exec(`INSERT INTO bans VALUES ($1) if err := s.server.userRepo.BanUser(uid, nil); err != nil {
ON CONFLICT (user_id) DO UPDATE SET expires=NULL`, uid); err != nil {
s.logger.Error("Failed to ban user", zap.Error(err)) s.logger.Error("Failed to ban user", zap.Error(err))
} }
sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.ban.success, uname)) sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.ban.success, uname))
} else { } else {
if _, err := s.server.db.Exec(`INSERT INTO bans VALUES ($1, $2) if err := s.server.userRepo.BanUser(uid, &expiry); err != nil {
ON CONFLICT (user_id) DO UPDATE SET expires=$2`, uid, expiry); err != nil {
s.logger.Error("Failed to ban user with expiry", zap.Error(err)) s.logger.Error("Failed to ban user with expiry", zap.Error(err))
} }
sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.ban.success, uname)+fmt.Sprintf(s.server.i18n.commands.ban.length, expiry.Format(time.DateTime))) sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.ban.success, uname)+fmt.Sprintf(s.server.i18n.commands.ban.length, expiry.Format(time.DateTime)))

View File

@@ -348,10 +348,10 @@ func handleMsgMhfEnumerateQuest(s *Session, p mhfpacket.MHFPacket) {
bf := byteframe.NewByteFrame() bf := byteframe.NewByteFrame()
bf.WriteUint16(0) bf.WriteUint16(0)
rows, err := s.server.db.Query("SELECT id, COALESCE(max_players, 4) AS max_players, quest_type, quest_id, COALESCE(mark, 0) AS mark, COALESCE(flags, -1), start_time, COALESCE(active_days, 0) AS active_days, COALESCE(inactive_days, 0) AS inactive_days FROM event_quests ORDER BY quest_id") rows, err := s.server.eventRepo.GetEventQuests()
if err == nil { if err == nil {
currentTime := time.Now() currentTime := time.Now()
tx, err := s.server.db.Begin() tx, err := s.server.eventRepo.BeginTx()
if err != nil { if err != nil {
s.logger.Error("Failed to begin transaction for event quests", zap.Error(err)) s.logger.Error("Failed to begin transaction for event quests", zap.Error(err))
_ = rows.Close() _ = rows.Close()
@@ -385,7 +385,7 @@ func handleMsgMhfEnumerateQuest(s *Session, p mhfpacket.MHFPacket) {
// Normalize rotationTime to 12PM JST to align with the in-game events update notification. // Normalize rotationTime to 12PM JST to align with the in-game events update notification.
newRotationTime := time.Date(rotationTime.Year(), rotationTime.Month(), rotationTime.Day(), 12, 0, 0, 0, TimeAdjusted().Location()) newRotationTime := time.Date(rotationTime.Year(), rotationTime.Month(), rotationTime.Day(), 12, 0, 0, 0, TimeAdjusted().Location())
_, err = tx.Exec("UPDATE event_quests SET start_time = $1 WHERE id = $2", newRotationTime, id) err = s.server.eventRepo.UpdateEventQuestStartTime(tx, id, newRotationTime)
if err != nil { if err != nil {
_ = tx.Rollback() _ = tx.Rollback()
break break

View File

@@ -224,3 +224,15 @@ func (r *CharacterRepository) SaveHouseData(charID uint32, houseTier []byte, hou
houseTier, houseData, bookshelf, gallery, tore, garden, charID) houseTier, houseData, bookshelf, gallery, tore, garden, charID)
return err return err
} }
// LoadSaveData reads the core save columns for a character.
// Returns charID, savedata, isNewCharacter, name, and any error.
func (r *CharacterRepository) LoadSaveData(charID uint32) (uint32, []byte, bool, string, error) {
var id uint32
var savedata []byte
var isNew bool
var name string
err := r.db.QueryRow("SELECT id, savedata, is_new_character, name FROM characters WHERE id = $1", charID).
Scan(&id, &savedata, &isNew, &name)
return id, savedata, isNew, name, err
}

View File

@@ -1,6 +1,7 @@
package channelserver package channelserver
import ( import (
"database/sql"
"time" "time"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
@@ -45,3 +46,19 @@ func (r *EventRepository) UpdateLoginBoost(charID uint32, weekReq uint8, expirat
_, err := r.db.Exec(`UPDATE login_boost SET expiration=$1, reset=$2 WHERE char_id=$3 AND week_req=$4`, expiration, reset, charID, weekReq) _, err := r.db.Exec(`UPDATE login_boost SET expiration=$1, reset=$2 WHERE char_id=$3 AND week_req=$4`, expiration, reset, charID, weekReq)
return err return err
} }
// GetEventQuests returns all event quest rows ordered by quest_id.
func (r *EventRepository) GetEventQuests() (*sql.Rows, error) {
return r.db.Query("SELECT id, COALESCE(max_players, 4) AS max_players, quest_type, quest_id, COALESCE(mark, 0) AS mark, COALESCE(flags, -1), start_time, COALESCE(active_days, 0) AS active_days, COALESCE(inactive_days, 0) AS inactive_days FROM event_quests ORDER BY quest_id")
}
// UpdateEventQuestStartTime updates the start_time for an event quest within a transaction.
func (r *EventRepository) UpdateEventQuestStartTime(tx *sql.Tx, id uint32, startTime time.Time) error {
_, err := tx.Exec("UPDATE event_quests SET start_time = $1 WHERE id = $2", startTime, id)
return err
}
// BeginTx starts a new database transaction.
func (r *EventRepository) BeginTx() (*sql.Tx, error) {
return r.db.Begin()
}

View File

@@ -41,6 +41,7 @@ type CharacterRepo interface {
FindByRastaID(rastaID int) (charID uint32, name string, err error) FindByRastaID(rastaID int) (charID uint32, name string, err error)
SaveCharacterData(charID uint32, compSave []byte, hr, gr uint16, isFemale bool, weaponType uint8, weaponID uint16) error SaveCharacterData(charID uint32, compSave []byte, hr, gr uint16, isFemale bool, weaponType uint8, weaponID uint16) error
SaveHouseData(charID uint32, houseTier []byte, houseData, bookshelf, gallery, tore, garden []byte) error SaveHouseData(charID uint32, houseTier []byte, houseData, bookshelf, gallery, tore, garden []byte) error
LoadSaveData(charID uint32) (uint32, []byte, bool, string, error)
} }
// GuildRepo defines the contract for guild data access. // GuildRepo defines the contract for guild data access.
@@ -141,6 +142,7 @@ type UserRepo interface {
LinkDiscord(discordID string, token string) (string, error) LinkDiscord(discordID string, token string) (string, error)
SetPasswordByDiscordID(discordID string, hash []byte) error SetPasswordByDiscordID(discordID string, hash []byte) error
GetByIDAndUsername(charID uint32) (userID uint32, username string, err error) GetByIDAndUsername(charID uint32) (userID uint32, username string, err error)
BanUser(userID uint32, expires *time.Time) error
} }
// GachaRepo defines the contract for gacha system data access. // GachaRepo defines the contract for gacha system data access.
@@ -271,6 +273,9 @@ type EventRepo interface {
GetLoginBoosts(charID uint32) (*sqlx.Rows, error) GetLoginBoosts(charID uint32) (*sqlx.Rows, error)
InsertLoginBoost(charID uint32, weekReq uint8, expiration, reset time.Time) error InsertLoginBoost(charID uint32, weekReq uint8, expiration, reset time.Time) error
UpdateLoginBoost(charID uint32, weekReq uint8, expiration, reset time.Time) error UpdateLoginBoost(charID uint32, weekReq uint8, expiration, reset time.Time) error
GetEventQuests() (*sql.Rows, error)
UpdateEventQuestStartTime(tx *sql.Tx, id uint32, startTime time.Time) error
BeginTx() (*sql.Tx, error)
} }
// AchievementRepo defines the contract for achievement data access. // AchievementRepo defines the contract for achievement data access.

View File

@@ -194,6 +194,7 @@ func (m *mockCharacterRepo) UpdateGCPAndPact(_ uint32, _ uint32, _ uint32) error
func (m *mockCharacterRepo) FindByRastaID(_ int) (uint32, string, error) { return 0, "", nil } func (m *mockCharacterRepo) FindByRastaID(_ int) (uint32, string, error) { return 0, "", nil }
func (m *mockCharacterRepo) SaveCharacterData(_ uint32, _ []byte, _, _ uint16, _ bool, _ uint8, _ uint16) error { return nil } func (m *mockCharacterRepo) SaveCharacterData(_ uint32, _ []byte, _, _ uint16, _ bool, _ uint8, _ uint16) error { return nil }
func (m *mockCharacterRepo) SaveHouseData(_ uint32, _ []byte, _, _, _, _, _ []byte) error { return nil } func (m *mockCharacterRepo) SaveHouseData(_ uint32, _ []byte, _, _, _, _, _ []byte) error { return nil }
func (m *mockCharacterRepo) LoadSaveData(_ uint32) (uint32, []byte, bool, string, error) { return 0, nil, false, "", nil }
// --- mockGoocooRepo --- // --- mockGoocooRepo ---

View File

@@ -2,6 +2,7 @@ package channelserver
import ( import (
"database/sql" "database/sql"
"time"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
) )
@@ -218,3 +219,16 @@ func (r *UserRepository) GetByIDAndUsername(charID uint32) (userID uint32, usern
).Scan(&userID, &username) ).Scan(&userID, &username)
return return
} }
// BanUser inserts or updates a ban for the given user.
// A nil expires means a permanent ban; non-nil sets a temporary ban with expiry.
func (r *UserRepository) BanUser(userID uint32, expires *time.Time) error {
if expires == nil {
_, err := r.db.Exec(`INSERT INTO bans VALUES ($1)
ON CONFLICT (user_id) DO UPDATE SET expires=NULL`, userID)
return err
}
_, err := r.db.Exec(`INSERT INTO bans VALUES ($1, $2)
ON CONFLICT (user_id) DO UPDATE SET expires=$2`, userID, *expires)
return err
}