From 642d3b4e1d33afd521f315b9aa99b8993d252902 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sat, 21 Feb 2026 10:46:02 +0900 Subject: refactor(admin): separate business logic into game and tournament services Move transaction handling, rejudge workflow, tournament bracket creation, and data repair logic from admin handler into game.Service and tournament.Service, mirroring the earlier api package separation. Co-Authored-By: Claude Opus 4.6 --- backend/game/errors.go | 1 + backend/game/service.go | 159 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 160 insertions(+) (limited to 'backend/game') diff --git a/backend/game/errors.go b/backend/game/errors.go index 9f7505a..6c977bd 100644 --- a/backend/game/errors.go +++ b/backend/game/errors.go @@ -6,4 +6,5 @@ var ( ErrNotFound = errors.New("not found") ErrGameNotRunning = errors.New("game is not running") ErrForbidden = errors.New("forbidden") + ErrNoTestcases = errors.New("no testcases") ) diff --git a/backend/game/service.go b/backend/game/service.go index 86e0eb3..debc126 100644 --- a/backend/game/service.go +++ b/backend/game/service.go @@ -374,6 +374,165 @@ func (s *Service) GetRanking(ctx context.Context, gameID int) ([]RankingEntry, b return ranking, finished, nil } +// UpdateGameParams holds parameters for updating a game with its players. +type UpdateGameParams struct { + GameID int + GameType string + IsPublic bool + DisplayName string + DurationSeconds int + StartedAt pgtype.Timestamp + ProblemID int + MainPlayerIDs []int +} + +func (s *Service) StartGame(ctx context.Context, gameID int) error { + gameRow, err := s.q.GetGameByID(ctx, int32(gameID)) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return ErrNotFound + } + return err + } + testcases, err := s.q.ListTestcasesByProblemID(ctx, gameRow.ProblemID) + if err != nil && !errors.Is(err, pgx.ErrNoRows) { + return err + } + if len(testcases) == 0 { + return ErrNoTestcases + } + + startedAt := time.Now().Add(10 * time.Second) + return s.q.UpdateGameStartedAt(ctx, db.UpdateGameStartedAtParams{ + GameID: int32(gameID), + StartedAt: pgtype.Timestamp{ + Time: startedAt, + Valid: true, + }, + }) +} + +func (s *Service) UpdateGameWithPlayers(ctx context.Context, params UpdateGameParams) error { + return s.txm.RunInTx(ctx, func(qtx db.Querier) error { + if err := qtx.UpdateGame(ctx, db.UpdateGameParams{ + GameID: int32(params.GameID), + GameType: params.GameType, + IsPublic: params.IsPublic, + DisplayName: params.DisplayName, + DurationSeconds: int32(params.DurationSeconds), + StartedAt: params.StartedAt, + ProblemID: int32(params.ProblemID), + }); err != nil { + return err + } + if err := qtx.RemoveAllMainPlayers(ctx, int32(params.GameID)); err != nil { + return err + } + for _, userID := range params.MainPlayerIDs { + if err := qtx.AddMainPlayer(ctx, db.AddMainPlayerParams{ + GameID: int32(params.GameID), + UserID: int32(userID), + }); err != nil { + return err + } + } + return nil + }) +} + +func (s *Service) RejudgeSubmission(ctx context.Context, submissionID int32, gameID int, userID int, language, code string) error { + err := s.txm.RunInTx(ctx, func(qtx db.Querier) error { + if err := qtx.DeleteTestcaseResultsBySubmissionID(ctx, submissionID); err != nil { + return err + } + return qtx.UpdateSubmissionStatus(ctx, db.UpdateSubmissionStatusParams{ + SubmissionID: submissionID, + Status: "running", + }) + }) + if err != nil { + return err + } + return s.hub.EnqueueTestTasks(ctx, int(submissionID), gameID, userID, language, code) +} + +func (s *Service) RejudgeLatestSubmissionsByGame(ctx context.Context, gameID int) error { + gameRow, err := s.q.GetGameByID(ctx, int32(gameID)) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return ErrNotFound + } + return err + } + + submissions, err := s.q.GetLatestSubmissionsByGameID(ctx, int32(gameID)) + if err != nil { + return err + } + + for _, sub := range submissions { + if err := s.RejudgeSubmission(ctx, sub.SubmissionID, int(sub.GameID), int(sub.UserID), gameRow.Language, sub.Code); err != nil { + return err + } + } + return nil +} + +func (s *Service) RejudgeAllSubmissionsByGame(ctx context.Context, gameID int) error { + gameRow, err := s.q.GetGameByID(ctx, int32(gameID)) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return ErrNotFound + } + return err + } + + submissions, err := s.q.GetSubmissionsByGameID(ctx, int32(gameID)) + if err != nil { + return err + } + + for _, sub := range submissions { + if err := s.RejudgeSubmission(ctx, sub.SubmissionID, int(sub.GameID), int(sub.UserID), gameRow.Language, sub.Code); err != nil { + return err + } + } + return nil +} + +func (s *Service) FixSubmissionStatuses(ctx context.Context) error { + submissionIDs, err := s.q.ListSubmissionIDs(ctx) + if err != nil { + return err + } + for _, submissionID := range submissionIDs { + as, err := s.q.AggregateTestcaseResults(ctx, submissionID) + if err != nil { + return err + } + if err := s.q.UpdateSubmissionStatus(ctx, db.UpdateSubmissionStatusParams{ + SubmissionID: submissionID, + Status: as, + }); err != nil { + return err + } + } + + gameStates, err := s.q.ListGameStateIDs(ctx) + if err != nil { + return err + } + for _, r := range gameStates { + if err := s.q.SyncGameStateBestScoreSubmission(ctx, db.SyncGameStateBestScoreSubmissionParams{ + GameID: r.GameID, + UserID: r.UserID, + }); err != nil { + return err + } + } + return nil +} + func (s *Service) GetSubmissions(ctx context.Context, gameID int, userID int32) ([]SubmissionDetail, error) { _, err := s.q.GetGameByID(ctx, int32(gameID)) if err != nil { -- cgit v1.3.1