Files
a301_server/internal/bossraid/repository.go
tolelom befea9dd68
Some checks failed
Server CI/CD / lint-and-build (push) Failing after 12m3s
Server CI/CD / deploy (push) Has been cancelled
feat: Swagger API 문서 추가 + 보스레이드/플레이어 레벨 시스템
- swaggo/swag 기반 전체 API 엔드포인트 Swagger 어노테이션 (59개)
- /swagger/ 경로에 Swagger UI 제공
- 보스레이드 데디서버 관리 (등록, 하트비트, 슬롯 리셋)
- 플레이어 레벨/경험치 시스템 및 스탯 성장

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 17:51:37 +09:00

228 lines
6.9 KiB
Go

package bossraid
import (
"fmt"
"strings"
"time"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type Repository struct {
db *gorm.DB
}
func NewRepository(db *gorm.DB) *Repository {
return &Repository{db: db}
}
func (r *Repository) Create(room *BossRoom) error {
return r.db.Create(room).Error
}
func (r *Repository) Update(room *BossRoom) error {
return r.db.Save(room).Error
}
func (r *Repository) FindBySessionName(sessionName string) (*BossRoom, error) {
var room BossRoom
if err := r.db.Where("session_name = ?", sessionName).First(&room).Error; err != nil {
return nil, err
}
return &room, nil
}
// FindBySessionNameForUpdate acquires a row-level lock (SELECT ... FOR UPDATE)
// to prevent concurrent state transitions.
func (r *Repository) FindBySessionNameForUpdate(sessionName string) (*BossRoom, error) {
var room BossRoom
if err := r.db.Clauses(clause.Locking{Strength: "UPDATE"}).Where("session_name = ?", sessionName).First(&room).Error; err != nil {
return nil, err
}
return &room, nil
}
// Transaction wraps a function in a database transaction.
func (r *Repository) Transaction(fn func(txRepo *Repository) error) error {
return r.db.Transaction(func(tx *gorm.DB) error {
return fn(&Repository{db: tx})
})
}
// CountActiveByUsername checks if a player is already in an active boss raid.
func (r *Repository) CountActiveByUsername(username string) (int64, error) {
var count int64
// LIKE 특수문자 이스케이프
escaped := strings.NewReplacer("%", "\\%", "_", "\\_").Replace(username)
search := `"` + escaped + `"`
err := r.db.Model(&BossRoom{}).
Where("status IN ? AND players LIKE ?",
[]RoomStatus{StatusWaiting, StatusInProgress},
"%"+search+"%",
).Count(&count).Error
return count, err
}
// --- DedicatedServer & RoomSlot ---
// UpsertDedicatedServer creates or updates a server group by name.
func (r *Repository) UpsertDedicatedServer(server *DedicatedServer) error {
var existing DedicatedServer
err := r.db.Where("server_name = ?", server.ServerName).First(&existing).Error
if err == gorm.ErrRecordNotFound {
return r.db.Create(server).Error
}
if err != nil {
return err
}
existing.MaxRooms = server.MaxRooms
return r.db.Save(&existing).Error
}
// FindDedicatedServerByName finds a server group by name.
func (r *Repository) FindDedicatedServerByName(serverName string) (*DedicatedServer, error) {
var server DedicatedServer
if err := r.db.Where("server_name = ?", serverName).First(&server).Error; err != nil {
return nil, err
}
return &server, nil
}
// EnsureRoomSlots ensures the correct number of room slots exist for a server.
func (r *Repository) EnsureRoomSlots(serverID uint, serverName string, maxRooms int) error {
for i := 0; i < maxRooms; i++ {
sessionName := fmt.Sprintf("%s_Room%d", serverName, i)
var existing RoomSlot
err := r.db.Where("session_name = ?", sessionName).First(&existing).Error
if err == gorm.ErrRecordNotFound {
slot := RoomSlot{
DedicatedServerID: serverID,
SlotIndex: i,
SessionName: sessionName,
Status: SlotIdle,
}
if err := r.db.Create(&slot).Error; err != nil {
return err
}
} else if err != nil {
return err
}
}
return nil
}
// AssignSlotToInstance finds an unassigned (or stale) slot and assigns it to the given instanceID.
// Returns the assigned slot with its sessionName.
func (r *Repository) AssignSlotToInstance(serverID uint, instanceID string, staleThreshold time.Time) (*RoomSlot, error) {
// First check if this instance already has a slot assigned
var existing RoomSlot
err := r.db.
Clauses(clause.Locking{Strength: "UPDATE"}).
Where("dedicated_server_id = ? AND instance_id = ?", serverID, instanceID).
First(&existing).Error
if err == nil {
// Already assigned — refresh heartbeat
now := time.Now()
existing.LastHeartbeat = &now
r.db.Save(&existing)
return &existing, nil
}
// Find an unassigned slot (instance_id is empty or heartbeat is stale)
var slot RoomSlot
err = r.db.
Clauses(clause.Locking{Strength: "UPDATE"}).
Where("dedicated_server_id = ? AND (instance_id = '' OR instance_id IS NULL OR last_heartbeat < ?)",
serverID, staleThreshold).
Order("slot_index ASC").
First(&slot).Error
if err != nil {
return nil, fmt.Errorf("사용 가능한 슬롯이 없습니다")
}
// Assign this instance to the slot
now := time.Now()
slot.InstanceID = instanceID
slot.LastHeartbeat = &now
slot.Status = SlotIdle
slot.BossRoomID = nil
if err := r.db.Save(&slot).Error; err != nil {
return nil, err
}
return &slot, nil
}
// UpdateHeartbeat updates the heartbeat for a specific instance.
func (r *Repository) UpdateHeartbeat(instanceID string) error {
now := time.Now()
result := r.db.Model(&RoomSlot{}).
Where("instance_id = ?", instanceID).
Update("last_heartbeat", now)
if result.RowsAffected == 0 {
return fmt.Errorf("인스턴스를 찾을 수 없습니다: %s", instanceID)
}
return result.Error
}
// FindIdleRoomSlot finds an idle room slot with a live instance (with row-level lock).
func (r *Repository) FindIdleRoomSlot(staleThreshold time.Time) (*RoomSlot, error) {
var slot RoomSlot
err := r.db.
Clauses(clause.Locking{Strength: "UPDATE"}).
Where("status = ? AND instance_id != '' AND instance_id IS NOT NULL AND last_heartbeat >= ?",
SlotIdle, staleThreshold).
Order("id ASC").
First(&slot).Error
if err != nil {
return nil, err
}
return &slot, nil
}
// UpdateRoomSlot updates a room slot.
func (r *Repository) UpdateRoomSlot(slot *RoomSlot) error {
return r.db.Save(slot).Error
}
// FindRoomSlotBySession finds a room slot by its session name.
func (r *Repository) FindRoomSlotBySession(sessionName string) (*RoomSlot, error) {
var slot RoomSlot
if err := r.db.Where("session_name = ?", sessionName).First(&slot).Error; err != nil {
return nil, err
}
return &slot, nil
}
// ResetRoomSlot sets a room slot back to idle and clears its BossRoomID.
// Does NOT clear InstanceID — the container still owns the slot.
func (r *Repository) ResetRoomSlot(sessionName string) error {
result := r.db.Model(&RoomSlot{}).
Where("session_name = ?", sessionName).
Updates(map[string]interface{}{
"status": SlotIdle,
"boss_room_id": nil,
})
return result.Error
}
// ResetStaleSlots clears instanceID for slots with stale heartbeats
// and resets any active raids on those slots.
func (r *Repository) ResetStaleSlots(threshold time.Time) (int64, error) {
result := r.db.Model(&RoomSlot{}).
Where("instance_id != '' AND instance_id IS NOT NULL AND last_heartbeat < ?", threshold).
Updates(map[string]interface{}{
"instance_id": "",
"status": SlotIdle,
"boss_room_id": nil,
})
return result.RowsAffected, result.Error
}
// GetRoomSlotsByServer returns all room slots for a given server.
func (r *Repository) GetRoomSlotsByServer(serverID uint) ([]RoomSlot, error) {
var slots []RoomSlot
err := r.db.Where("dedicated_server_id = ?", serverID).Order("slot_index ASC").Find(&slots).Error
return slots, err
}