feat(tester): add solution endpoints

add CreateSolution&GetSolution&ListSolutions endpoints
This commit is contained in:
Vyacheslav1557 2025-03-27 00:04:52 +05:00
parent af6e0b89f6
commit ef696d2836
9 changed files with 316 additions and 8 deletions

View file

@ -15,3 +15,36 @@ type Solution struct {
UpdatedAt time.Time `db:"updated_at"`
CreatedAt time.Time `db:"created_at"`
}
type SolutionCreation struct {
Solution string
TaskId int32
ParticipantId int32
Language int32
Penalty int32
}
type SolutionsListItem struct {
Id int32 `db:"id"`
TaskId int32 `db:"task_id"`
ContestId int32 `db:"contest_id"`
ParticipantId int32 `db:"participant_id"`
State int32 `db:"state"`
Score int32 `db:"score"`
Penalty int32 `db:"penalty"`
TotalScore int32 `db:"total_score"`
Language int32 `db:"language"`
UpdatedAt time.Time `db:"updated_at"`
CreatedAt time.Time `db:"created_at"`
}
type SolutionsFilter struct {
Page int32
PageSize int32
ContestId *int32
ParticipantId *int32
TaskId *int32
Language *int32
State *int32
Order *int32
}

View file

@ -10,16 +10,19 @@ type Handlers interface {
CreateContest(c *fiber.Ctx) error
DeleteContest(c *fiber.Ctx, id int32) error
GetContest(c *fiber.Ctx, id int32) error
UpdateContest(c *fiber.Ctx, id int32) error
DeleteParticipant(c *fiber.Ctx, params testerv1.DeleteParticipantParams) error
ListParticipants(c *fiber.Ctx, params testerv1.ListParticipantsParams) error
UpdateParticipant(c *fiber.Ctx, params testerv1.UpdateParticipantParams) error
AddParticipant(c *fiber.Ctx, params testerv1.AddParticipantParams) error
DeleteTask(c *fiber.Ctx, params testerv1.DeleteTaskParams) error
AddTask(c *fiber.Ctx, params testerv1.AddTaskParams) error
ListProblems(c *fiber.Ctx, params testerv1.ListProblemsParams) error
CreateProblem(c *fiber.Ctx) error
DeleteProblem(c *fiber.Ctx, id int32) error
GetProblem(c *fiber.Ctx, id int32) error
ListParticipants(c *fiber.Ctx, params testerv1.ListParticipantsParams) error
UpdateProblem(c *fiber.Ctx, id int32) error
UpdateContest(c *fiber.Ctx, id int32) error
UpdateParticipant(c *fiber.Ctx, params testerv1.UpdateParticipantParams) error
ListSolutions(c *fiber.Ctx, params testerv1.ListSolutionsParams) error
CreateSolution(c *fiber.Ctx, params testerv1.CreateSolutionParams) error
GetSolution(c *fiber.Ctx, id int32) error
DeleteTask(c *fiber.Ctx, params testerv1.DeleteTaskParams) error
AddTask(c *fiber.Ctx, params testerv1.AddTaskParams) error
}

View file

@ -6,6 +6,7 @@ import (
"git.sch9.ru/new_gate/ms-tester/pkg"
testerv1 "git.sch9.ru/new_gate/ms-tester/proto/tester/v1"
"github.com/gofiber/fiber/v2"
"io"
)
type TesterHandlers struct {
@ -332,3 +333,112 @@ func (h *TesterHandlers) UpdateParticipant(c *fiber.Ctx, params testerv1.UpdateP
return c.SendStatus(fiber.StatusOK)
}
func (h *TesterHandlers) ListSolutions(c *fiber.Ctx, params testerv1.ListSolutionsParams) error {
list, total, err := h.contestsUC.ListSolutions(c.Context(), models.SolutionsFilter{
ContestId: params.ContestId,
Page: params.Page,
PageSize: params.PageSize,
ParticipantId: params.ParticipantId,
TaskId: params.TaskId,
Language: params.Language,
Order: params.Order,
State: params.State,
})
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
resp := testerv1.ListSolutionsResponse{
Solutions: make([]testerv1.SolutionListItem, len(list)),
Page: params.Page,
MaxPage: func() int32 {
if total%params.PageSize == 0 {
return total / params.PageSize
}
return total/params.PageSize + 1
}(),
}
for i, solution := range list {
resp.Solutions[i] = testerv1.SolutionListItem{
Id: solution.Id,
TaskId: solution.TaskId,
ContestId: solution.ContestId,
ParticipantId: solution.ParticipantId,
Language: solution.Language,
Penalty: solution.Penalty,
Score: solution.Score,
State: solution.State,
TotalScore: solution.TotalScore,
CreatedAt: solution.CreatedAt,
UpdatedAt: solution.UpdatedAt,
}
}
return c.JSON(resp)
}
const (
maxSolutionSize int64 = 10 * 1024 * 1024
)
func (h *TesterHandlers) CreateSolution(c *fiber.Ctx, params testerv1.CreateSolutionParams) error {
s, err := c.FormFile("solution")
if err != nil {
return err
}
if s.Size == 0 || s.Size > maxSolutionSize {
return c.SendStatus(fiber.StatusBadRequest)
}
f, err := s.Open()
if err != nil {
return err
}
defer f.Close()
b, err := io.ReadAll(f)
if err != nil {
return err
}
id, err := h.contestsUC.CreateSolution(c.Context(), &models.SolutionCreation{
TaskId: params.TaskId,
ParticipantId: 1,
Language: params.Language,
Penalty: 0,
Solution: string(b),
})
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
return c.JSON(testerv1.CreateSolutionResponse{
Id: id,
})
}
func (h *TesterHandlers) GetSolution(c *fiber.Ctx, id int32) error {
solution, err := h.contestsUC.ReadSolution(c.Context(), id)
if err != nil {
return c.SendStatus(pkg.ToREST(err))
}
return c.JSON(
testerv1.GetSolutionResponse{Solution: testerv1.Solution{
Id: solution.Id,
TaskId: solution.TaskId,
ParticipantId: solution.ParticipantId,
Solution: solution.Solution,
State: solution.State,
Score: solution.Score,
Penalty: solution.Penalty,
TotalScore: solution.TotalScore,
Language: solution.Language,
CreatedAt: solution.CreatedAt,
UpdatedAt: solution.UpdatedAt,
}},
)
}

View file

@ -44,4 +44,7 @@ type ContestRepository interface {
ListParticipants(ctx context.Context, contestId int32, page int32, pageSize int32) ([]*models.ParticipantsListItem, int32, error)
UpdateContest(ctx context.Context, id int32, contestUpdate models.ContestUpdate) error
UpdateParticipant(ctx context.Context, id int32, participantUpdate models.ParticipantUpdate) error
ReadSolution(ctx context.Context, id int32) (*models.Solution, error)
CreateSolution(ctx context.Context, creation *models.SolutionCreation) (int32, error)
ListSolutions(ctx context.Context, filters models.SolutionsFilter) ([]*models.SolutionsListItem, int32, error)
}

View file

@ -2,8 +2,10 @@ package repository
import (
"context"
"fmt"
"git.sch9.ru/new_gate/ms-tester/internal/models"
"github.com/jmoiron/sqlx"
"strings"
)
type ContestRepository struct {
@ -247,3 +249,145 @@ func (r *ContestRepository) UpdateParticipant(ctx context.Context, id int32, par
return nil
}
const (
readSolutionQuery = "SELECT * FROM solutions WHERE id = ?"
)
func (r *ContestRepository) ReadSolution(ctx context.Context, id int32) (*models.Solution, error) {
const op = "ContestRepository.ReadSolution"
query := r.db.Rebind(readSolutionQuery)
var solution models.Solution
err := r.db.GetContext(ctx, &solution, query, id)
if err != nil {
return nil, handlePgErr(err, op)
}
return &solution, nil
}
const (
createSolutionQuery = `INSERT INTO solutions (task_id, participant_id, language, penalty, solution)
VALUES (?, ?, ?, ?, ?)
RETURNING id`
)
func (r *ContestRepository) CreateSolution(ctx context.Context, creation *models.SolutionCreation) (int32, error) {
const op = "ContestRepository.CreateSolution"
query := r.db.Rebind(createSolutionQuery)
rows, err := r.db.QueryxContext(ctx,
query,
creation.TaskId,
creation.ParticipantId,
creation.Language,
creation.Penalty,
creation.Solution,
)
if err != nil {
return 0, handlePgErr(err, op)
}
defer rows.Close()
var id int32
rows.Next()
err = rows.Scan(&id)
if err != nil {
return 0, handlePgErr(err, op)
}
return id, nil
}
func (r *ContestRepository) ListSolutions(ctx context.Context, filters models.SolutionsFilter) ([]*models.SolutionsListItem, int32, error) {
const op = "ContestRepository.ListSolutions"
baseQuery := `
SELECT
s.id,
s.task_id,
t.contest_id,
s.participant_id,
s.state,
s.score,
s.penalty,
s.total_score,
s.language,
s.updated_at,
s.created_at
FROM solutions s
LEFT JOIN tasks t ON s.task_id = t.id
WHERE 1=1
`
var conditions []string
var args []interface{}
if filters.ContestId != nil {
conditions = append(conditions, "contest_id = ?")
args = append(args, *filters.ContestId)
}
if filters.ParticipantId != nil {
conditions = append(conditions, "participant_id = ?")
args = append(args, *filters.ParticipantId)
}
if filters.TaskId != nil {
conditions = append(conditions, "task_id = ?")
args = append(args, *filters.TaskId)
}
if filters.Language != nil {
conditions = append(conditions, "language = ?")
args = append(args, *filters.Language)
}
if filters.State != nil {
conditions = append(conditions, "state = ?")
args = append(args, *filters.State)
}
if len(conditions) > 0 {
baseQuery += " AND " + strings.Join(conditions, " AND ")
}
if filters.Order != nil {
orderDirection := "ASC"
if *filters.Order < 0 {
orderDirection = "DESC"
}
baseQuery += fmt.Sprintf(" ORDER BY s.id %s", orderDirection)
}
countQuery := "SELECT COUNT(*) FROM (" + baseQuery + ") as count_table"
var totalCount int32
err := r.db.QueryRowxContext(ctx, r.db.Rebind(countQuery), args...).Scan(&totalCount)
if err != nil {
return nil, 0, handlePgErr(err, op)
}
offset := (filters.Page - 1) * filters.PageSize
baseQuery += " LIMIT ? OFFSET ?"
args = append(args, filters.PageSize, offset)
rows, err := r.db.QueryxContext(ctx, r.db.Rebind(baseQuery), args...)
if err != nil {
return nil, 0, handlePgErr(err, op)
}
defer rows.Close()
var solutions []*models.SolutionsListItem
for rows.Next() {
var solution models.SolutionsListItem
err = rows.StructScan(&solution)
if err != nil {
return nil, 0, handlePgErr(err, op)
}
solutions = append(solutions, &solution)
}
if err = rows.Err(); err != nil {
return nil, 0, handlePgErr(err, op)
}
return solutions, totalCount, nil
}

View file

@ -26,4 +26,7 @@ type ContestUseCase interface {
ListParticipants(ctx context.Context, contestId int32, page int32, pageSize int32) ([]*models.ParticipantsListItem, int32, error)
UpdateContest(ctx context.Context, id int32, contestUpdate models.ContestUpdate) error
UpdateParticipant(ctx context.Context, id int32, participantUpdate models.ParticipantUpdate) error
ReadSolution(ctx context.Context, id int32) (*models.Solution, error)
CreateSolution(ctx context.Context, creation *models.SolutionCreation) (int32, error)
ListSolutions(ctx context.Context, filters models.SolutionsFilter) ([]*models.SolutionsListItem, int32, error)
}

View file

@ -65,3 +65,15 @@ func (uc *ContestUseCase) UpdateContest(ctx context.Context, id int32, contestUp
func (uc *ContestUseCase) UpdateParticipant(ctx context.Context, id int32, participantUpdate models.ParticipantUpdate) error {
return uc.contestRepo.UpdateParticipant(ctx, id, participantUpdate)
}
func (uc *ContestUseCase) ReadSolution(ctx context.Context, id int32) (*models.Solution, error) {
return uc.contestRepo.ReadSolution(ctx, id)
}
func (uc *ContestUseCase) CreateSolution(ctx context.Context, creation *models.SolutionCreation) (int32, error) {
return uc.contestRepo.CreateSolution(ctx, creation)
}
func (uc *ContestUseCase) ListSolutions(ctx context.Context, filters models.SolutionsFilter) ([]*models.SolutionsListItem, int32, error) {
return uc.contestRepo.ListSolutions(ctx, filters)
}

View file

@ -105,9 +105,9 @@ CREATE TABLE IF NOT EXISTS solutions
participant_id integer REFERENCES participants (id) ON DELETE SET NULL,
solution varchar(1048576) NOT NULL,
state integer NOT NULL DEFAULT 1,
score integer NOT NULL,
score integer NOT NULL DEFAULT 0,
penalty integer NOT NULL,
total_score integer NOT NULL,
total_score integer NOT NULL DEFAULT 0,
language integer NOT NULL,
updated_at timestamptz NOT NULL DEFAULT now(),
created_at timestamptz NOT NULL DEFAULT now(),

2
proto

@ -1 +1 @@
Subproject commit fb65ba8ce2220e7e47990f0f52214126839b3d78
Subproject commit 6a83a9832d5fed659f72e72c90b3954a9518e0ba