diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-02-21 10:46:02 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-02-21 10:46:02 +0900 |
| commit | 642d3b4e1d33afd521f315b9aa99b8993d252902 (patch) | |
| tree | f39e7c191033354db56675d9d3dc4c4be17d7aba /backend/admin/handler.go | |
| parent | e8db174d3e464a5764a9f4bfd82172261bd50519 (diff) | |
| download | phperkaigi-2026-albatross-642d3b4e1d33afd521f315b9aa99b8993d252902.tar.gz phperkaigi-2026-albatross-642d3b4e1d33afd521f315b9aa99b8993d252902.tar.zst phperkaigi-2026-albatross-642d3b4e1d33afd521f315b9aa99b8993d252902.zip | |
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 <noreply@anthropic.com>
Diffstat (limited to 'backend/admin/handler.go')
| -rw-r--r-- | backend/admin/handler.go | 314 |
1 files changed, 56 insertions, 258 deletions
diff --git a/backend/admin/handler.go b/backend/admin/handler.go index 09303ac..f115745 100644 --- a/backend/admin/handler.go +++ b/backend/admin/handler.go @@ -16,24 +16,22 @@ import ( "albatross-2026-backend/account" "albatross-2026-backend/config" "albatross-2026-backend/db" + "albatross-2026-backend/game" "albatross-2026-backend/session" + "albatross-2026-backend/tournament" ) var jst = time.FixedZone("Asia/Tokyo", 9*60*60) -type GameHub interface { - EnqueueTestTasks(ctx context.Context, submissionID, gameID, userID int, language, code string) error -} - type Handler struct { - q db.Querier - txm db.TxManager - hub GameHub - conf *config.Config + gameSvc *game.Service + tournamentSvc *tournament.Service + q db.Querier + conf *config.Config } -func NewHandler(q db.Querier, txm db.TxManager, hub GameHub, conf *config.Config) *Handler { - return &Handler{q: q, txm: txm, hub: hub, conf: conf} +func NewHandler(gameSvc *game.Service, tournamentSvc *tournament.Service, q db.Querier, conf *config.Config) *Handler { + return &Handler{gameSvc: gameSvc, tournamentSvc: tournamentSvc, q: q, conf: conf} } func (h *Handler) newAdminMiddleware() echo.MiddlewareFunc { @@ -149,39 +147,9 @@ func (h *Handler) getOnlineQualifyingRanking(c echo.Context) error { } func (h *Handler) postFix(c echo.Context) error { - rows, err := h.q.ListSubmissionIDs(c.Request().Context()) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) - } - for _, submissionID := range rows { - as, err := h.q.AggregateTestcaseResults(c.Request().Context(), submissionID) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) - } - err = h.q.UpdateSubmissionStatus(c.Request().Context(), db.UpdateSubmissionStatusParams{ - SubmissionID: submissionID, - Status: as, - }) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) - } - } - - rows2, err := h.q.ListGameStateIDs(c.Request().Context()) - if err != nil { + if err := h.gameSvc.FixSubmissionStatuses(c.Request().Context()); err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } - for _, r := range rows2 { - gameID := r.GameID - userID := r.UserID - err := h.q.SyncGameStateBestScoreSubmission(c.Request().Context(), db.SyncGameStateBestScoreSubmissionParams{ - GameID: gameID, - UserID: userID, - }) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) - } - } return c.Redirect(http.StatusSeeOther, h.conf.BasePath+"admin/dashboard") } @@ -519,31 +487,15 @@ func (h *Handler) postGameEdit(c echo.Context) error { mainPlayers = append(mainPlayers, mainPlayer2) } - ctx := c.Request().Context() - err = h.txm.RunInTx(ctx, func(qtx db.Querier) error { - if err := qtx.UpdateGame(ctx, db.UpdateGameParams{ - GameID: int32(gameID), - GameType: gameType, - IsPublic: isPublic, - DisplayName: displayName, - DurationSeconds: int32(durationSeconds), - StartedAt: changedStartedAt, - ProblemID: int32(problemID), - }); err != nil { - return err - } - if err := qtx.RemoveAllMainPlayers(ctx, int32(gameID)); err != nil { - return err - } - for _, userID := range mainPlayers { - if err := qtx.AddMainPlayer(ctx, db.AddMainPlayerParams{ - GameID: int32(gameID), - UserID: int32(userID), - }); err != nil { - return err - } - } - return nil + err = h.gameSvc.UpdateGameWithPlayers(c.Request().Context(), game.UpdateGameParams{ + GameID: gameID, + GameType: gameType, + IsPublic: isPublic, + DisplayName: displayName, + DurationSeconds: durationSeconds, + StartedAt: changedStartedAt, + ProblemID: problemID, + MainPlayerIDs: mainPlayers, }) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) @@ -558,36 +510,16 @@ func (h *Handler) postGameStart(c echo.Context) error { return echo.NewHTTPError(http.StatusBadRequest, "Invalid game id") } - game, err := h.q.GetGameByID(c.Request().Context(), int32(gameID)) + err = h.gameSvc.StartGame(c.Request().Context(), gameID) if err != nil { - if errors.Is(err, pgx.ErrNoRows) { + if errors.Is(err, game.ErrNotFound) { return echo.NewHTTPError(http.StatusNotFound, "Game not found") } - return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) - } - testcases, err := h.q.ListTestcasesByProblemID(c.Request().Context(), game.ProblemID) - if err != nil { - if errors.Is(err, pgx.ErrNoRows) { + if errors.Is(err, game.ErrNoTestcases) { return echo.NewHTTPError(http.StatusBadRequest, "No testcases") } return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } - if len(testcases) == 0 { - return echo.NewHTTPError(http.StatusBadRequest, "No testcases") - } - - startedAt := time.Now().Add(10 * time.Second) - - err = h.q.UpdateGameStartedAt(c.Request().Context(), db.UpdateGameStartedAtParams{ - GameID: int32(gameID), - StartedAt: pgtype.Timestamp{ - Time: startedAt, - Valid: true, - }, - }) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) - } return c.Redirect(http.StatusSeeOther, h.conf.BasePath+"admin/games") } @@ -673,22 +605,6 @@ func (h *Handler) getSubmissionDetail(c echo.Context) error { }) } -func (h *Handler) rejudgeSubmission(ctx context.Context, submission db.Submission, language string) error { - err := h.txm.RunInTx(ctx, func(qtx db.Querier) error { - if err := qtx.DeleteTestcaseResultsBySubmissionID(ctx, submission.SubmissionID); err != nil { - return err - } - return qtx.UpdateSubmissionStatus(ctx, db.UpdateSubmissionStatusParams{ - SubmissionID: submission.SubmissionID, - Status: "running", - }) - }) - if err != nil { - return err - } - return h.hub.EnqueueTestTasks(ctx, int(submission.SubmissionID), int(submission.GameID), int(submission.UserID), language, submission.Code) -} - func (h *Handler) postSubmissionRejudge(c echo.Context) error { gameID, err := strconv.Atoi(c.Param("gameID")) if err != nil { @@ -710,7 +626,7 @@ func (h *Handler) postSubmissionRejudge(c echo.Context) error { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } - game, err := h.q.GetGameByID(ctx, int32(gameID)) + g, err := h.q.GetGameByID(ctx, int32(gameID)) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return echo.NewHTTPError(http.StatusNotFound) @@ -718,7 +634,7 @@ func (h *Handler) postSubmissionRejudge(c echo.Context) error { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } - if err := h.rejudgeSubmission(ctx, submission, game.Language); err != nil { + if err := h.gameSvc.RejudgeSubmission(ctx, submission.SubmissionID, int(submission.GameID), int(submission.UserID), g.Language, submission.Code); err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } @@ -731,27 +647,14 @@ func (h *Handler) postSubmissionsRejudgeLatest(c echo.Context) error { return echo.NewHTTPError(http.StatusBadRequest, "Invalid game_id") } - ctx := c.Request().Context() - - game, err := h.q.GetGameByID(ctx, int32(gameID)) + err = h.gameSvc.RejudgeLatestSubmissionsByGame(c.Request().Context(), gameID) if err != nil { - if errors.Is(err, pgx.ErrNoRows) { + if errors.Is(err, game.ErrNotFound) { return echo.NewHTTPError(http.StatusNotFound) } return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } - submissions, err := h.q.GetLatestSubmissionsByGameID(ctx, int32(gameID)) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) - } - - for _, s := range submissions { - if err := h.rejudgeSubmission(ctx, s, game.Language); err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) - } - } - return c.Redirect(http.StatusSeeOther, fmt.Sprintf("%sadmin/games/%d/submissions", h.conf.BasePath, gameID)) } @@ -761,27 +664,14 @@ func (h *Handler) postSubmissionsRejudgeAll(c echo.Context) error { return echo.NewHTTPError(http.StatusBadRequest, "Invalid game_id") } - ctx := c.Request().Context() - - game, err := h.q.GetGameByID(ctx, int32(gameID)) + err = h.gameSvc.RejudgeAllSubmissionsByGame(c.Request().Context(), gameID) if err != nil { - if errors.Is(err, pgx.ErrNoRows) { + if errors.Is(err, game.ErrNotFound) { return echo.NewHTTPError(http.StatusNotFound) } return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } - submissions, err := h.q.GetSubmissionsByGameID(ctx, int32(gameID)) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) - } - - for _, s := range submissions { - if err := h.rejudgeSubmission(ctx, s, game.Language); err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) - } - } - return c.Redirect(http.StatusSeeOther, fmt.Sprintf("%sadmin/games/%d/submissions", h.conf.BasePath, gameID)) } @@ -1107,23 +997,6 @@ func (h *Handler) getTournamentNew(c echo.Context) error { }) } -func nextPowerOf2(n int) int { - p := 1 - for p < n { - p *= 2 - } - return p -} - -func log2Int(n int) int { - r := 0 - for n > 1 { - n /= 2 - r++ - } - return r -} - func (h *Handler) postTournamentNew(c echo.Context) error { displayName := c.FormValue("display_name") numParticipants, err := strconv.Atoi(c.FormValue("num_participants")) @@ -1131,34 +1004,7 @@ func (h *Handler) postTournamentNew(c echo.Context) error { return echo.NewHTTPError(http.StatusBadRequest, "Invalid num_participants") } - bracketSize := nextPowerOf2(numParticipants) - numRounds := log2Int(bracketSize) - - ctx := c.Request().Context() - err = h.txm.RunInTx(ctx, func(qtx db.Querier) error { - tournamentID, err := qtx.CreateTournament(ctx, db.CreateTournamentParams{ - DisplayName: displayName, - BracketSize: int32(bracketSize), - NumRounds: int32(numRounds), - }) - if err != nil { - return err - } - // Create match slots for all rounds - for round := range numRounds { - numPositions := bracketSize / (1 << (round + 1)) - for pos := range numPositions { - if err := qtx.CreateTournamentMatch(ctx, db.CreateTournamentMatchParams{ - TournamentID: tournamentID, - Round: int32(round), - Position: int32(pos), - }); err != nil { - return err - } - } - } - return nil - }) + _, err = h.tournamentSvc.CreateTournament(c.Request().Context(), displayName, numParticipants) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } @@ -1173,30 +1019,19 @@ func (h *Handler) getTournamentEdit(c echo.Context) error { } ctx := c.Request().Context() - tournament, err := h.q.GetTournamentByID(ctx, int32(tournamentID)) + editData, err := h.tournamentSvc.GetTournamentEditData(ctx, tournamentID) if err != nil { - if errors.Is(err, pgx.ErrNoRows) { + if errors.Is(err, game.ErrNotFound) { return echo.NewHTTPError(http.StatusNotFound) } return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } - entryRows, err := h.q.ListTournamentEntries(ctx, int32(tournamentID)) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) - } - seedUserMap := make(map[int]int) - for _, e := range entryRows { - seedUserMap[int(e.Seed)] = int(e.UserID) - } + t := editData.Tournament - matchRows, err := h.q.ListTournamentMatches(ctx, int32(tournamentID)) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) - } matchGameMap := make(map[int]int) var matches []echo.Map - for _, m := range matchRows { + for _, m := range editData.Matches { gameID := 0 if m.GameID != nil { gameID = int(*m.GameID) @@ -1235,7 +1070,7 @@ func (h *Handler) getTournamentEdit(c echo.Context) error { }) } - seeds := make([]int, tournament.BracketSize) + seeds := make([]int, t.BracketSize) for i := range seeds { seeds[i] = i + 1 } @@ -1244,13 +1079,13 @@ func (h *Handler) getTournamentEdit(c echo.Context) error { "BasePath": h.conf.BasePath, "Title": "Tournament Edit", "Tournament": echo.Map{ - "TournamentID": tournament.TournamentID, - "DisplayName": tournament.DisplayName, - "BracketSize": tournament.BracketSize, - "NumRounds": tournament.NumRounds, + "TournamentID": t.TournamentID, + "DisplayName": t.DisplayName, + "BracketSize": t.BracketSize, + "NumRounds": t.NumRounds, }, "Seeds": seeds, - "SeedUserMap": seedUserMap, + "SeedUserMap": editData.SeedUserMap, "Matches": matches, "MatchGameMap": matchGameMap, "Users": users, @@ -1265,9 +1100,11 @@ func (h *Handler) postTournamentEdit(c echo.Context) error { } ctx := c.Request().Context() - tournament, err := h.q.GetTournamentByID(ctx, int32(tournamentID)) + // We need the tournament to know bracket size for seed parsing, + // and matches for match game parsing. + editData, err := h.tournamentSvc.GetTournamentEditData(ctx, tournamentID) if err != nil { - if errors.Is(err, pgx.ErrNoRows) { + if errors.Is(err, game.ErrNotFound) { return echo.NewHTTPError(http.StatusNotFound) } return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) @@ -1276,12 +1113,8 @@ func (h *Handler) postTournamentEdit(c echo.Context) error { displayName := c.FormValue("display_name") // Parse seed → user assignments - type seedEntry struct { - seed int - userID int - } - var seedEntries []seedEntry - for seed := 1; seed <= int(tournament.BracketSize); seed++ { + var seedEntries []tournament.SeedEntry + for seed := 1; seed <= int(editData.Tournament.BracketSize); seed++ { raw := c.FormValue("seed_" + strconv.Itoa(seed)) if raw == "" || raw == "0" { continue @@ -1290,20 +1123,12 @@ func (h *Handler) postTournamentEdit(c echo.Context) error { if err != nil { return echo.NewHTTPError(http.StatusBadRequest, "Invalid seed_"+strconv.Itoa(seed)) } - seedEntries = append(seedEntries, seedEntry{seed: seed, userID: userID}) + seedEntries = append(seedEntries, tournament.SeedEntry{Seed: seed, UserID: userID}) } // Parse match → game assignments - matchRows, err := h.q.ListTournamentMatches(ctx, int32(tournamentID)) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) - } - type matchGame struct { - matchID int - gameID *int32 - } - var matchGames []matchGame - for _, m := range matchRows { + var matchGames []tournament.MatchGame + for _, m := range editData.Matches { raw := c.FormValue("match_" + strconv.Itoa(int(m.TournamentMatchID))) var gameID *int32 if raw != "" && raw != "0" { @@ -1314,46 +1139,19 @@ func (h *Handler) postTournamentEdit(c echo.Context) error { gid32 := int32(gid) gameID = &gid32 } - matchGames = append(matchGames, matchGame{matchID: int(m.TournamentMatchID), gameID: gameID}) + matchGames = append(matchGames, tournament.MatchGame{MatchID: int(m.TournamentMatchID), GameID: gameID}) } - err = h.txm.RunInTx(ctx, func(qtx db.Querier) error { - if err := qtx.UpdateTournament(ctx, db.UpdateTournamentParams{ - TournamentID: int32(tournamentID), - DisplayName: displayName, - BracketSize: tournament.BracketSize, - NumRounds: tournament.NumRounds, - }); err != nil { - return err - } - - // Replace entries - if err := qtx.DeleteTournamentEntries(ctx, int32(tournamentID)); err != nil { - return err - } - for _, se := range seedEntries { - if err := qtx.CreateTournamentEntry(ctx, db.CreateTournamentEntryParams{ - TournamentID: int32(tournamentID), - UserID: int32(se.userID), - Seed: int32(se.seed), - }); err != nil { - return err - } - } - - // Update match game assignments - for _, mg := range matchGames { - if err := qtx.UpdateTournamentMatchGame(ctx, db.UpdateTournamentMatchGameParams{ - TournamentMatchID: int32(mg.matchID), - GameID: mg.gameID, - }); err != nil { - return err - } - } - - return nil + err = h.tournamentSvc.UpdateTournament(ctx, tournament.UpdateTournamentParams{ + TournamentID: tournamentID, + DisplayName: displayName, + SeedEntries: seedEntries, + MatchGames: matchGames, }) if err != nil { + if errors.Is(err, game.ErrNotFound) { + return echo.NewHTTPError(http.StatusNotFound) + } return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) } |
