From c70d839038a7925908eebc3c91559b740c29af9c Mon Sep 17 00:00:00 2001 From: Fedorov Vladimir Date: Thu, 17 Sep 2026 02:29:13 +0700 Subject: [PATCH] add team service --- cmd/evening_detective_server/main.go | 9 +- internal/app/game_mapper.go | 6 +- internal/app/results_mapper_test.go | 3 +- internal/app/server.go | 4 + internal/app/team_mapper.go | 8 +- internal/app/team_qr.go | 2 +- internal/app/team_registration.go | 22 ++-- internal/app/team_registration_test.go | 11 +- internal/app/teams.go | 6 +- .../game_model}/game.go | 2 +- .../game_model}/game_mapper.go | 10 +- .../game_model}/team.go | 2 +- .../game_model}/team_mapper.go | 8 +- internal/services/game_service/answer.go | 7 +- internal/services/game_service/results.go | 5 +- .../services/game_service/results_test.go | 5 +- internal/services/game_service/service.go | 118 ++++++------------ internal/services/mcp_service/http_test.go | 4 +- internal/services/mcp_service/service.go | 7 +- internal/services/mcp_service/service_test.go | 9 +- .../participation_test.go | 2 +- .../manual_team_test.go | 4 +- .../registration.go | 23 +--- .../registration_test.go | 2 +- internal/services/team_service/service.go | 113 +++++++++++++++++ .../{game_service => team_service}/team_qr.go | 23 +--- .../team_qr_test.go | 2 +- 27 files changed, 227 insertions(+), 190 deletions(-) rename internal/{services/game_service => modules/game_model}/game.go (94%) rename internal/{services/game_service => modules/game_model}/game_mapper.go (82%) rename internal/{services/game_service => modules/game_model}/team.go (95%) rename internal/{services/game_service => modules/game_model}/team_mapper.go (88%) rename internal/services/{game_service => team_service}/manual_team_test.go (91%) rename internal/services/{game_service => team_service}/registration.go (72%) rename internal/services/{game_service => team_service}/registration_test.go (97%) create mode 100644 internal/services/team_service/service.go rename internal/services/{game_service => team_service}/team_qr.go (71%) rename internal/services/{game_service => team_service}/team_qr_test.go (98%) diff --git a/cmd/evening_detective_server/main.go b/cmd/evening_detective_server/main.go index 63ef046..814d4d3 100644 --- a/cmd/evening_detective_server/main.go +++ b/cmd/evening_detective_server/main.go @@ -29,6 +29,7 @@ import ( "evening_detective_server/internal/services/mcp_service" "evening_detective_server/internal/services/participation_service" "evening_detective_server/internal/services/scenarios_service" + "evening_detective_server/internal/services/team_service" "evening_detective_server/internal/services/ui_service" "evening_detective_server/internal/services/users_service" proto "evening_detective_server/proto" @@ -139,16 +140,15 @@ func main() { scenariosRepo, scenarioService, os.Getenv("FILE_PREFIX_DOMAIN"), - passwordGenerator, storyteller, actionsRepo, applicationsRepo, answersRepo, cleaner, - team_qr_pdf.NewGenerator(), - os.Getenv("APP_BASE_URL"), ) + teamService := team_service.NewTeamService(gameRepo, teamRepo, passwordGenerator, team_qr_pdf.NewGenerator(), os.Getenv("APP_BASE_URL")) + participationService := participation_service.NewParticipationService(teamRepo, os.Getenv("APP_BASE_URL")) go participationService.RunParticipationEmails(context.Background(), emailSender, os.Getenv("PARTICIPATION_SECRET"), os.Getenv("GAME_TIMEZONE")) @@ -199,6 +199,7 @@ func main() { fileService, scenarioService, gameService, + teamService, participationService, screenRepo, mapRepo, @@ -275,7 +276,7 @@ func main() { } mainMux := http.NewServeMux() - mainMux.Handle("POST /api/games/{id}/registrations", app.TeamRegistrationHandler(os.Getenv("YANDEX_FORMS_TOKEN"), gameService.RegisterTeam)) + mainMux.Handle("POST /api/games/{id}/registrations", app.TeamRegistrationHandler(os.Getenv("YANDEX_FORMS_TOKEN"), teamService.RegisterTeam)) docsHandler := v5emb.New("API", "/swagger/doc.json", "/docs/") mainMux.Handle("/docs/", docsHandler) diff --git a/internal/app/game_mapper.go b/internal/app/game_mapper.go index 4d0f4d4..6d6f9c0 100644 --- a/internal/app/game_mapper.go +++ b/internal/app/game_mapper.go @@ -1,14 +1,14 @@ package app import ( - "evening_detective_server/internal/services/game_service" + "evening_detective_server/internal/modules/game_model" "evening_detective_server/proto" "google.golang.org/protobuf/types/known/timestamppb" ) func mapGame( - o *game_service.Game, + o *game_model.Game, ) *proto.Game { var startedAt *timestamppb.Timestamp if o.StartedAt != nil { @@ -38,7 +38,7 @@ func mapGame( } func mapGames( - o []*game_service.Game, + o []*game_model.Game, ) []*proto.Game { res := make([]*proto.Game, 0, len(o)) for _, item := range o { diff --git a/internal/app/results_mapper_test.go b/internal/app/results_mapper_test.go index 865553b..6c97483 100644 --- a/internal/app/results_mapper_test.go +++ b/internal/app/results_mapper_test.go @@ -1,6 +1,7 @@ package app import ( + "evening_detective_server/internal/modules/game_model" "testing" "evening_detective_server/internal/services/game_service" @@ -10,7 +11,7 @@ func TestMapTeamResults(t *testing.T) { sc := 5 results := []game_service.TeamResult{ { - Team: &game_service.Team{ + Team: &game_model.Team{ ID: 1, Name: "Команда А", Status: "play", diff --git a/internal/app/server.go b/internal/app/server.go index 73e7a41..ba343e3 100644 --- a/internal/app/server.go +++ b/internal/app/server.go @@ -8,6 +8,7 @@ import ( "evening_detective_server/internal/services/game_service" "evening_detective_server/internal/services/participation_service" "evening_detective_server/internal/services/scenarios_service" + "evening_detective_server/internal/services/team_service" "evening_detective_server/internal/services/ui_service" "evening_detective_server/internal/services/users_service" proto "evening_detective_server/proto" @@ -24,6 +25,7 @@ type server struct { fileService *file_service.FileService scenarioService *scenarios_service.ScenarioService gameService *game_service.GameService + teamService *team_service.TeamService participationService *participation_service.ParticipationService screenRepo *screen_sessions_repo.ScreenSessionsRepo mapRepo *game_map_repo.Repo @@ -35,6 +37,7 @@ func NewServer( fileService *file_service.FileService, scenarioService *scenarios_service.ScenarioService, gameService *game_service.GameService, + teamService *team_service.TeamService, participationService *participation_service.ParticipationService, screenRepo *screen_sessions_repo.ScreenSessionsRepo, mapRepo *game_map_repo.Repo, @@ -45,6 +48,7 @@ func NewServer( fileService: fileService, scenarioService: scenarioService, gameService: gameService, + teamService: teamService, participationService: participationService, screenRepo: screenRepo, mapRepo: mapRepo, diff --git a/internal/app/team_mapper.go b/internal/app/team_mapper.go index 1d27e13..ebca700 100644 --- a/internal/app/team_mapper.go +++ b/internal/app/team_mapper.go @@ -1,12 +1,12 @@ package app import ( - "evening_detective_server/internal/services/game_service" + "evening_detective_server/internal/modules/game_model" "evening_detective_server/proto" ) func mapTeam( - o *game_service.Team, + o *game_model.Team, ) *proto.Team { return &proto.Team{ Id: int32(o.ID), @@ -18,7 +18,7 @@ func mapTeam( } } -func mapTeamDetails(o *game_service.Team) *proto.Team { +func mapTeamDetails(o *game_model.Team) *proto.Team { team := mapTeam(o) team.ParticipationStatus = o.ParticipationStatus team.ParticipationSource = o.ParticipationSource @@ -30,7 +30,7 @@ func mapTeamDetails(o *game_service.Team) *proto.Team { } func mapTeams( - o []*game_service.Team, + o []*game_model.Team, ) []*proto.Team { res := make([]*proto.Team, 0, len(o)) for _, item := range o { diff --git a/internal/app/team_qr.go b/internal/app/team_qr.go index 1cc6500..f778720 100644 --- a/internal/app/team_qr.go +++ b/internal/app/team_qr.go @@ -29,7 +29,7 @@ func (s *server) DownloadTeamQR(ctx context.Context, req *proto.GetGameReq) (*ht if req == nil || req.Id <= 0 { return nil, status.Error(codes.InvalidArgument, "некорректный идентификатор игры") } - data, filename, err := s.gameService.DownloadTeamQR(ctx, int(req.Id)) + data, filename, err := s.teamService.DownloadTeamQR(ctx, int(req.Id)) if err != nil { switch { case errors.Is(err, games_repo.ErrGameNotFound): diff --git a/internal/app/team_registration.go b/internal/app/team_registration.go index e12dc9f..7149814 100644 --- a/internal/app/team_registration.go +++ b/internal/app/team_registration.go @@ -6,6 +6,7 @@ import ( "crypto/subtle" "encoding/json" "errors" + "evening_detective_server/internal/services/team_service" "fmt" "io" "net/http" @@ -13,11 +14,10 @@ import ( "time" "evening_detective_server/internal/repos/teams_repo" - "evening_detective_server/internal/services/game_service" ) // TeamRegistrationHandler принимает webhook Яндекс Форм отдельно от пользовательского JWT. -func TeamRegistrationHandler(token string, register func(context.Context, int, string, game_service.Registration) (int, error)) http.Handler { +func TeamRegistrationHandler(token string, register func(context.Context, int, string, team_service.Registration) (int, error)) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Cache-Control", "no-store") @@ -63,7 +63,7 @@ func TeamRegistrationHandler(token string, register func(context.Context, int, s defer cancel() id, err := register(ctx, int(gameID), deliveryID, registration) switch { - case errors.Is(err, game_service.ErrInvalidRegistration): + case errors.Is(err, team_service.ErrInvalidRegistration): fail(http.StatusBadRequest, err.Error()) case errors.Is(err, teams_repo.ErrRegistrationClosed): fail(http.StatusConflict, err.Error()) @@ -79,29 +79,29 @@ func TeamRegistrationHandler(token string, register func(context.Context, int, s }) } -func decodeRegistrationPayload(payload json.RawMessage) (game_service.Registration, json.RawMessage, bool, error) { +func decodeRegistrationPayload(payload json.RawMessage) (team_service.Registration, json.RawMessage, bool, error) { var fields map[string]json.RawMessage if err := json.Unmarshal(payload, &fields); err != nil { - return game_service.Registration{}, nil, false, err + return team_service.Registration{}, nil, false, err } if rawRPC, ok := fields["jsonrpc"]; ok { var version string if err := json.Unmarshal(rawRPC, &version); err != nil || version != "2.0" { - return game_service.Registration{}, nil, true, errors.New(`поле "jsonrpc" должно быть равно "2.0"`) + return team_service.Registration{}, nil, true, errors.New(`поле "jsonrpc" должно быть равно "2.0"`) } params, ok := fields["params"] if !ok { - return game_service.Registration{}, nil, true, errors.New(`в JSON-RPC запросе отсутствует поле "params"`) + return team_service.Registration{}, nil, true, errors.New(`в JSON-RPC запросе отсутствует поле "params"`) } - var registration game_service.Registration + var registration team_service.Registration if err := decodeStrict(params, ®istration); err != nil { - return game_service.Registration{}, nil, true, fmt.Errorf("поле params: %w", err) + return team_service.Registration{}, nil, true, fmt.Errorf("поле params: %w", err) } return registration, fields["id"], true, nil } - var registration game_service.Registration + var registration team_service.Registration if err := decodeStrict(payload, ®istration); err != nil { - return game_service.Registration{}, nil, false, err + return team_service.Registration{}, nil, false, err } return registration, nil, false, nil } diff --git a/internal/app/team_registration_test.go b/internal/app/team_registration_test.go index 339c4a4..b15cfe0 100644 --- a/internal/app/team_registration_test.go +++ b/internal/app/team_registration_test.go @@ -3,13 +3,14 @@ package app import ( "context" "errors" + "evening_detective_server/internal/modules/game_model" + "evening_detective_server/internal/services/team_service" "net/http" "net/http/httptest" "strings" "testing" "evening_detective_server/internal/repos/teams_repo" - "evening_detective_server/internal/services/game_service" ) func TestTeamRegistrationHandler(t *testing.T) { @@ -32,14 +33,14 @@ func TestTeamRegistrationHandler(t *testing.T) { {"jsonrpc", "secret", "Bearer secret", "42", "delivery", `{"jsonrpc":"2.0","method":"register","params":{"name":"Команда","email":"captain@example.org","participantsCount":5},"id":1}`, nil, 200, true}, {"jsonrpc string count", "secret", "Bearer secret", "42", "delivery", `{"jsonrpc":"2.0","method":"register","params":{"name":"Команда","email":"captain@example.org","participantsCount":"5"},"id":1}`, nil, 200, true}, {"too large", "secret", "Bearer secret", "42", "delivery", `{"name":"` + strings.Repeat("a", 17000) + `"}`, nil, 400, false}, - {"invalid fields", "secret", "Bearer secret", "42", "delivery", valid, game_service.ErrInvalidRegistration, 400, true}, + {"invalid fields", "secret", "Bearer secret", "42", "delivery", valid, team_service.ErrInvalidRegistration, 400, true}, {"closed", "secret", "Bearer secret", "42", "delivery", valid, teams_repo.ErrRegistrationClosed, 409, true}, {"internal", "secret", "Bearer secret", "42", "delivery", valid, errors.New("private database detail"), 500, true}, } { t.Run(tc.name, func(t *testing.T) { called := false mux := http.NewServeMux() - mux.Handle("POST /api/games/{id}/registrations", TeamRegistrationHandler(tc.token, func(ctx context.Context, gameID int, deliveryID string, registration game_service.Registration) (int, error) { + mux.Handle("POST /api/games/{id}/registrations", TeamRegistrationHandler(tc.token, func(ctx context.Context, gameID int, deliveryID string, registration team_service.Registration) (int, error) { called = true if gameID != 42 || deliveryID != "delivery" || registration.Name != "Команда" || registration.Email != "captain@example.org" || registration.ParticipantsCount != 5 { t.Fatal("incorrect registration passed to service") @@ -80,8 +81,8 @@ func TestTeamRegistrationHandler(t *testing.T) { } func TestCaptainContactOnlyInGameDetails(t *testing.T) { - team := &game_service.Team{ID: 1, CaptainEmail: "captain@example.org", ParticipantsCount: 5} - details := mapGame(&game_service.Game{Teams: []*game_service.Team{team}}) + team := &game_model.Team{ID: 1, CaptainEmail: "captain@example.org", ParticipantsCount: 5} + details := mapGame(&game_model.Game{Teams: []*game_model.Team{team}}) if details.Teams[0].CaptainEmail != team.CaptainEmail || details.Teams[0].ParticipantsCount != 5 { t.Fatal("registration details missing from organizer view") } diff --git a/internal/app/teams.go b/internal/app/teams.go index 6527996..8b232e7 100644 --- a/internal/app/teams.go +++ b/internal/app/teams.go @@ -17,7 +17,7 @@ func (s *server) AddTeam(ctx context.Context, req *proto.AddTeamReq) (*proto.Add return nil, status.Errorf(codes.PermissionDenied, "permission denied") } - team, err := s.gameService.AddTeam( + team, err := s.teamService.AddTeam( ctx, claims.UserID, int(req.GameId), @@ -42,7 +42,7 @@ func (s *server) UpdateTeam(ctx context.Context, req *proto.UpdateTeamReq) (*pro return nil, status.Errorf(codes.PermissionDenied, "permission denied") } - err := s.gameService.UpdateTeam( + err := s.teamService.UpdateTeam( ctx, int(req.Id), req.Name, @@ -62,7 +62,7 @@ func (s *server) DeleteTeam(ctx context.Context, req *proto.DeleteTeamReq) (*pro return nil, status.Errorf(codes.PermissionDenied, "permission denied") } - err := s.gameService.DeleteTeam( + err := s.teamService.DeleteTeam( ctx, int(req.Id), ) diff --git a/internal/services/game_service/game.go b/internal/modules/game_model/game.go similarity index 94% rename from internal/services/game_service/game.go rename to internal/modules/game_model/game.go index a059e0a..59a8e14 100644 --- a/internal/services/game_service/game.go +++ b/internal/modules/game_model/game.go @@ -1,4 +1,4 @@ -package game_service +package game_model import ( "evening_detective_server/internal/services/scenarios_service" diff --git a/internal/services/game_service/game_mapper.go b/internal/modules/game_model/game_mapper.go similarity index 82% rename from internal/services/game_service/game_mapper.go rename to internal/modules/game_model/game_mapper.go index 4ea1d9c..1c7e528 100644 --- a/internal/services/game_service/game_mapper.go +++ b/internal/modules/game_model/game_mapper.go @@ -1,11 +1,11 @@ -package game_service +package game_model import ( "evening_detective_server/internal/repos" "evening_detective_server/internal/services/scenarios_service" ) -func mapGame( +func MapGame( o *repos.Game, domain string, ) *Game { @@ -17,19 +17,19 @@ func mapGame( Status: o.Status, Format: o.Format, Scenario: scenarios_service.MapScenarioLight(o.Scenario, domain), - Teams: mapTeams(o.Teams), + Teams: MapTeams(o.Teams), StartedAt: o.StartedAt, EndedAt: o.EndedAt, } } -func mapGames( +func MapGames( o []*repos.Game, domain string, ) []*Game { res := make([]*Game, 0, len(o)) for _, item := range o { - res = append(res, mapGame(item, domain)) + res = append(res, MapGame(item, domain)) } return res } diff --git a/internal/services/game_service/team.go b/internal/modules/game_model/team.go similarity index 95% rename from internal/services/game_service/team.go rename to internal/modules/game_model/team.go index ab4a9d9..97151e7 100644 --- a/internal/services/game_service/team.go +++ b/internal/modules/game_model/team.go @@ -1,4 +1,4 @@ -package game_service +package game_model import "evening_detective_server/internal/modules/storytelling" diff --git a/internal/services/game_service/team_mapper.go b/internal/modules/game_model/team_mapper.go similarity index 88% rename from internal/services/game_service/team_mapper.go rename to internal/modules/game_model/team_mapper.go index 17d7475..3a35061 100644 --- a/internal/services/game_service/team_mapper.go +++ b/internal/modules/game_model/team_mapper.go @@ -1,8 +1,8 @@ -package game_service +package game_model import "evening_detective_server/internal/repos" -func mapTeam( +func MapTeam( o *repos.Team, ) *Team { return &Team{ @@ -19,12 +19,12 @@ func mapTeam( } } -func mapTeams( +func MapTeams( o []*repos.Team, ) []*Team { res := make([]*Team, 0, len(o)) for _, item := range o { - res = append(res, mapTeam(item)) + res = append(res, MapTeam(item)) } return res } diff --git a/internal/services/game_service/answer.go b/internal/services/game_service/answer.go index c57a0d1..9641aec 100644 --- a/internal/services/game_service/answer.go +++ b/internal/services/game_service/answer.go @@ -1,6 +1,9 @@ package game_service -import "evening_detective_server/internal/modules/storytelling" +import ( + "evening_detective_server/internal/modules/game_model" + "evening_detective_server/internal/modules/storytelling" +) // Answer — ответ команды на вопрос дела (для публичного API команды). type Answer struct { @@ -18,7 +21,7 @@ type AnswerScore struct { // TeamAnswers — ответы одной команды игры. type TeamAnswers struct { - Team *Team + Team *game_model.Team Answers []AnswerScore } diff --git a/internal/services/game_service/results.go b/internal/services/game_service/results.go index 52e3e00..a63c3b1 100644 --- a/internal/services/game_service/results.go +++ b/internal/services/game_service/results.go @@ -1,6 +1,7 @@ package game_service import ( + "evening_detective_server/internal/modules/game_model" "sort" "evening_detective_server/internal/modules/storytelling" @@ -9,7 +10,7 @@ import ( // TeamResult — строка итоговой таблицы: команда, баллы по вопросам (в порядке // сценария), сумма баллов и место. type TeamResult struct { - Team *Team + Team *game_model.Team Answers []AnswerScore // TotalScore — сумма баллов по всем вопросам сценария; неоценённые // вопросы считаются как 0. @@ -34,7 +35,7 @@ type GameResults struct { // rankTeams. func buildTeamResults( questions []*storytelling.Question, - teams []*Team, + teams []*game_model.Team, answerByTeamAndCode map[int]map[string]AnswerScore, ) []TeamResult { res := make([]TeamResult, 0, len(teams)) diff --git a/internal/services/game_service/results_test.go b/internal/services/game_service/results_test.go index 52e69e3..a03b1d4 100644 --- a/internal/services/game_service/results_test.go +++ b/internal/services/game_service/results_test.go @@ -1,6 +1,7 @@ package game_service import ( + "evening_detective_server/internal/modules/game_model" "testing" "evening_detective_server/internal/modules/storytelling" @@ -10,7 +11,7 @@ func intPtr(v int) *int { return &v } func teamResult(id, actions, total int) TeamResult { return TeamResult{ - Team: &Team{ID: id, ActionsCount: actions}, + Team: &game_model.Team{ID: id, ActionsCount: actions}, TotalScore: total, } } @@ -105,7 +106,7 @@ func TestBuildTeamResultsScenarioOrderAndNilScores(t *testing.T) { {Code: "q2", Text: "Вопрос 2"}, {Code: "q3", Text: "Вопрос 3"}, } - teams := []*Team{ + teams := []*game_model.Team{ {ID: 1, Name: "А", ActionsCount: 4}, {ID: 2, Name: "Б", ActionsCount: 2}, } diff --git a/internal/services/game_service/service.go b/internal/services/game_service/service.go index cac66ee..dfe66e0 100644 --- a/internal/services/game_service/service.go +++ b/internal/services/game_service/service.go @@ -4,9 +4,8 @@ import ( "context" "errors" "evening_detective_server/internal/modules/cleaner" - "evening_detective_server/internal/modules/password_generator" + "evening_detective_server/internal/modules/game_model" "evening_detective_server/internal/modules/storytelling" - "evening_detective_server/internal/modules/team_qr_pdf" "evening_detective_server/internal/repos" "evening_detective_server/internal/repos/actions_repo" "evening_detective_server/internal/repos/applications_repo" @@ -15,24 +14,20 @@ import ( "evening_detective_server/internal/repos/team_answers_repo" "evening_detective_server/internal/repos/teams_repo" "evening_detective_server/internal/services/scenarios_service" - "strings" "time" ) type GameService struct { - gamesRepo *games_repo.GamesRepo - teamsRepo *teams_repo.TeamsRepo - scenariosRepo *scenarios_repo.ScenariosRepo - scenarioService *scenarios_service.ScenarioService - domain string - passwordGenerator password_generator.IPasswordGenerator - storyteller storytelling.IStory - actionsRepo *actions_repo.ActionsRepo - applicationsRepo *applications_repo.ApplicationsRepo - answersRepo *team_answers_repo.TeamAnswersRepo - cleaner cleaner.ICleaner - teamQRGenerator team_qr_pdf.IGenerator - appBaseURL string + gamesRepo *games_repo.GamesRepo + teamsRepo *teams_repo.TeamsRepo + scenariosRepo *scenarios_repo.ScenariosRepo + scenarioService *scenarios_service.ScenarioService + domain string + storyteller storytelling.IStory + actionsRepo *actions_repo.ActionsRepo + applicationsRepo *applications_repo.ApplicationsRepo + answersRepo *team_answers_repo.TeamAnswersRepo + cleaner cleaner.ICleaner } func NewGameService( @@ -41,29 +36,23 @@ func NewGameService( scenariosRepo *scenarios_repo.ScenariosRepo, scenarioService *scenarios_service.ScenarioService, domain string, - passwordGenerator password_generator.IPasswordGenerator, storyteller storytelling.IStory, actionsRepo *actions_repo.ActionsRepo, applicationsRepo *applications_repo.ApplicationsRepo, answersRepo *team_answers_repo.TeamAnswersRepo, cleaner cleaner.ICleaner, - teamQRGenerator team_qr_pdf.IGenerator, - appBaseURL string, ) *GameService { return &GameService{ - gamesRepo: gamesRepo, - teamsRepo: teamsRepo, - scenariosRepo: scenariosRepo, - scenarioService: scenarioService, - domain: domain, - passwordGenerator: passwordGenerator, - storyteller: storyteller, - actionsRepo: actionsRepo, - applicationsRepo: applicationsRepo, - answersRepo: answersRepo, - cleaner: cleaner, - teamQRGenerator: teamQRGenerator, - appBaseURL: appBaseURL, + gamesRepo: gamesRepo, + teamsRepo: teamsRepo, + scenariosRepo: scenariosRepo, + scenarioService: scenarioService, + domain: domain, + storyteller: storyteller, + actionsRepo: actionsRepo, + applicationsRepo: applicationsRepo, + answersRepo: answersRepo, + cleaner: cleaner, } } @@ -88,21 +77,21 @@ func (s *GameService) AddGame( return id, nil } -func (s *GameService) GetGame(ctx context.Context, id int) (*Game, error) { +func (s *GameService) GetGame(ctx context.Context, id int) (*game_model.Game, error) { game, err := s.gamesRepo.GetGameByID(ctx, id) if err != nil { return nil, err } - res := mapGame(game, s.domain) + res := game_model.MapGame(game, s.domain) return res, nil } -func (s *GameService) GetGameMedia(ctx context.Context, id int) (*Game, error) { +func (s *GameService) GetGameMedia(ctx context.Context, id int) (*game_model.Game, error) { game, err := s.gamesRepo.GetGameByID(ctx, id) if err != nil { return nil, err } - gameResult := mapGame(game, s.domain) + gameResult := game_model.MapGame(game, s.domain) gameResult.Scenario, err = s.scenarioService.GetFullScenarioByID(ctx, game.ScenarioId) if err != nil { return nil, err @@ -113,7 +102,7 @@ func (s *GameService) GetGameMedia(ctx context.Context, id int) (*Game, error) { func (s *GameService) GetFullGame( ctx context.Context, id int, -) (*Game, error) { +) (*game_model.Game, error) { game, err := s.gamesRepo.GetGameByID(ctx, id) if err != nil { return nil, err @@ -129,7 +118,7 @@ func (s *GameService) GetFullGame( } game.Teams = teams - res := mapGame(game, s.domain) + res := game_model.MapGame(game, s.domain) teamIds := make([]int, 0, len(game.Teams)) for _, team := range game.Teams { @@ -163,12 +152,12 @@ func (s *GameService) DeleteGame( func (s *GameService) GetGames( ctx context.Context, -) ([]*Game, error) { +) ([]*game_model.Game, error) { games, err := s.gamesRepo.GetGames(ctx) if err != nil { return nil, err } - return mapGames(games, s.domain), nil + return game_model.MapGames(games, s.domain), nil } func (s *GameService) UpdateGame( @@ -213,48 +202,11 @@ func (s *GameService) ResetGame(ctx context.Context, id int) error { return s.gamesRepo.ResetGame(ctx, id) } -func (s *GameService) AddTeam( - ctx context.Context, - creatorId int, - gameId int, - name string, - captainEmail string, -) (*Team, error) { - captainEmail = strings.TrimSpace(captainEmail) - if captainEmail != "" && !validCaptainEmail(captainEmail) { - return nil, errors.New("Укажите корректную почту капитана") - } - password, err := s.passwordGenerator.Generate() - if err != nil { - return nil, err - } - game, err := s.gamesRepo.GetGameByID(ctx, gameId) - if err != nil { - return nil, err - } - id, err := s.teamsRepo.AddTeamWithParticipation(ctx, creatorId, gameId, name, password, captainEmail, !time.Now().Before(game.StartAt.Add(-24*time.Hour))) - if err != nil { - return nil, err - } - return &Team{ - ID: id, - Password: password, - }, nil -} - -func (s *GameService) UpdateTeam(ctx context.Context, teamId int, name string) error { - return s.teamsRepo.UpdateTeam(ctx, teamId, name) -} - -func (s *GameService) DeleteTeam(ctx context.Context, teamId int) error { - return s.teamsRepo.DeleteTeam(ctx, teamId) -} - func (s *GameService) GiveTeamApplications(ctx context.Context, teamId int, name string) error { return s.applicationsRepo.UpdateApplicationStatus(ctx, teamId, name, "done") } -func (s *GameService) GetTeamActions(ctx context.Context, teamId int, password string) (*storytelling.Story, *Game, []actions_repo.Action, error) { +func (s *GameService) GetTeamActions(ctx context.Context, teamId int, password string) (*storytelling.Story, *game_model.Game, []actions_repo.Action, error) { team, err := s.teamsRepo.GetTeamByID(ctx, teamId) if err != nil { return nil, nil, nil, err @@ -269,7 +221,7 @@ func (s *GameService) GetTeamActions(ctx context.Context, teamId int, password s return nil, nil, nil, err } if game.Status == "finish" { - return &storytelling.Story{}, mapGame(game, s.domain), nil, nil + return &storytelling.Story{}, game_model.MapGame(game, s.domain), nil, nil } actions, err := s.actionsRepo.GetActionsByTeamID(ctx, teamId) @@ -288,7 +240,7 @@ func (s *GameService) GetTeamActions(ctx context.Context, teamId int, password s } story := s.storyteller.GetStory(scenario.Story, codes) filterTeamStory(story, game.Format) - return story, mapGame(game, s.domain), actions, nil + return story, game_model.MapGame(game, s.domain), actions, nil } func (s *GameService) AddTeamAction(ctx context.Context, teamId int, password string, actionCode string, viaMCP bool) error { @@ -546,7 +498,7 @@ func (s *GameService) GetGameAnswers(ctx context.Context, gameId int) (*GameAnsw answers = append(answers, AnswerScore{QuestionCode: question.Code}) } teamAnswers = append(teamAnswers, TeamAnswers{ - Team: mapTeam(team), + Team: game_model.MapTeam(team), Answers: answers, }) } @@ -626,9 +578,9 @@ func (s *GameService) GetGameResults(ctx context.Context, gameId int) (*GameResu Score: row.Score, } } - mappedTeams := make([]*Team, 0, len(teams)) + mappedTeams := make([]*game_model.Team, 0, len(teams)) for _, team := range teams { - mapped := mapTeam(team) + mapped := game_model.MapTeam(team) mapped.ActionsCount = actionsCountByTeamIDs[team.ID] mappedTeams = append(mappedTeams, mapped) } diff --git a/internal/services/mcp_service/http_test.go b/internal/services/mcp_service/http_test.go index a6e81c0..8479b79 100644 --- a/internal/services/mcp_service/http_test.go +++ b/internal/services/mcp_service/http_test.go @@ -2,13 +2,13 @@ package mcp_service import ( "context" + "evening_detective_server/internal/modules/game_model" "net/http/httptest" "strings" "testing" "time" "evening_detective_server/internal/modules/storytelling" - "evening_detective_server/internal/services/game_service" "github.com/mark3labs/mcp-go/client" "github.com/mark3labs/mcp-go/mcp" @@ -82,6 +82,6 @@ func TestStreamableHTTPEndToEnd(t *testing.T) { func TestMockTypesUsable(t *testing.T) { _ = defaultStory() _ = defaultGame() - _ = (*game_service.Game)(nil) + _ = (*game_model.Game)(nil) _ = (*storytelling.Story)(nil) } diff --git a/internal/services/mcp_service/service.go b/internal/services/mcp_service/service.go index 1192405..75292f5 100644 --- a/internal/services/mcp_service/service.go +++ b/internal/services/mcp_service/service.go @@ -8,6 +8,7 @@ package mcp_service import ( "context" "encoding/json" + "evening_detective_server/internal/modules/game_model" "fmt" "net/url" "strconv" @@ -30,7 +31,7 @@ const defaultCallTimeout = 10 * time.Second // *game_service.GameService; интерфейс позволяет тестировать сервис // без БД. type GamePlayer interface { - GetTeamActions(ctx context.Context, teamId int, password string) (*storytelling.Story, *game_service.Game, []actions_repo.Action, error) + GetTeamActions(ctx context.Context, teamId int, password string) (*storytelling.Story, *game_model.Game, []actions_repo.Action, error) AddTeamAction(ctx context.Context, teamId int, password, actionCode string, viaMCP bool) error SubmitTeamAnswers(ctx context.Context, teamId int, password string, answers []game_service.Answer) error } @@ -163,7 +164,7 @@ func parseConnectURL(rawURL string) (connectArgs, error) { } // gameResponse — компактное описание игры для ответа инструментов -// (game_service.Game JSON-тегов не имеет). +// (game_model.Game JSON-тегов не имеет). type gameResponse struct { ID int `json:"id"` Name string `json:"name"` @@ -177,7 +178,7 @@ type storyResponse struct { } // storyJSON сериализует историю команды и игру в JSON. -func storyJSON(story *storytelling.Story, game *game_service.Game) (string, error) { +func storyJSON(story *storytelling.Story, game *game_model.Game) (string, error) { resp := storyResponse{ Story: story, Game: &gameResponse{ diff --git a/internal/services/mcp_service/service_test.go b/internal/services/mcp_service/service_test.go index a3015d3..02ff38d 100644 --- a/internal/services/mcp_service/service_test.go +++ b/internal/services/mcp_service/service_test.go @@ -3,6 +3,7 @@ package mcp_service import ( "context" "errors" + "evening_detective_server/internal/modules/game_model" "strings" "testing" "time" @@ -19,12 +20,12 @@ import ( // ошибками (реализует интерфейс GamePlayer, БД не нужна). type fakeGamePlayer struct { story *storytelling.Story - game *game_service.Game + game *game_model.Game err error viaMCP bool } -func (f *fakeGamePlayer) GetTeamActions(_ context.Context, teamID int, password string) (*storytelling.Story, *game_service.Game, []actions_repo.Action, error) { +func (f *fakeGamePlayer) GetTeamActions(_ context.Context, teamID int, password string) (*storytelling.Story, *game_model.Game, []actions_repo.Action, error) { if f.err != nil { return nil, nil, nil, f.err } @@ -65,8 +66,8 @@ func defaultStory() *storytelling.Story { } } -func defaultGame() *game_service.Game { - return &game_service.Game{ +func defaultGame() *game_model.Game { + return &game_model.Game{ ID: 1, Name: "Игра 1", Status: "started", diff --git a/internal/services/participation_service/participation_test.go b/internal/services/participation_service/participation_test.go index 27afbb1..f494fdf 100644 --- a/internal/services/participation_service/participation_test.go +++ b/internal/services/participation_service/participation_test.go @@ -222,7 +222,7 @@ func TestParticipationPostgres(t *testing.T) { t.Fatal("cancelled team in active list") } } - gameService := game_service.NewGameService(nil, repo, nil, nil, "", nil, nil, nil, nil, nil, nil, nil, "") + gameService := game_service.NewGameService(nil, repo, nil, nil, "", nil, nil, nil, nil, nil) if _, _, _, err := gameService.GetTeamActions(ctx, 3, "secret"); !errors.Is(err, teams_repo.ErrTeamNotFound) { t.Fatalf("cancelled team got story: %v", err) } diff --git a/internal/services/game_service/manual_team_test.go b/internal/services/team_service/manual_team_test.go similarity index 91% rename from internal/services/game_service/manual_team_test.go rename to internal/services/team_service/manual_team_test.go index 24bb3c2..16c5531 100644 --- a/internal/services/game_service/manual_team_test.go +++ b/internal/services/team_service/manual_team_test.go @@ -1,4 +1,4 @@ -package game_service +package team_service import ( "context" @@ -13,7 +13,7 @@ func TestManualTeamEmail(t *testing.T) { } } for _, email := range []string{"invalid", "Captain ", "a@example.org\r\nBcc: other@example.org", strings.Repeat("a", 250) + "@example.org"} { - service := &GameService{} + service := &TeamService{} if _, err := service.AddTeam(context.Background(), 1, 1, "Team", email); err == nil { t.Errorf("accepted invalid email %q", email) } diff --git a/internal/services/game_service/registration.go b/internal/services/team_service/registration.go similarity index 72% rename from internal/services/game_service/registration.go rename to internal/services/team_service/registration.go index 53d50fb..c184f99 100644 --- a/internal/services/game_service/registration.go +++ b/internal/services/team_service/registration.go @@ -1,14 +1,12 @@ -package game_service +package team_service import ( "bytes" - "context" "encoding/json" "errors" "net/mail" "strconv" "strings" - "time" "unicode/utf8" ) @@ -65,22 +63,3 @@ func (r *Registration) Validate() error { } return nil } - -func (s *GameService) RegisterTeam(ctx context.Context, gameID int, deliveryID string, registration Registration) (int, error) { - if err := registration.Validate(); err != nil { - return 0, err - } - if gameID <= 0 || deliveryID == "" || len(deliveryID) > 255 || strings.ContainsRune(deliveryID, 0) { - return 0, ErrInvalidRegistration - } - password, err := s.passwordGenerator.Generate() - if err != nil { - return 0, err - } - game, err := s.gamesRepo.GetGameByID(ctx, gameID) - if err != nil { - return 0, err - } - autoConfirm := time.Now().Before(game.StartAt.Add(-24*time.Hour)) == false - return s.teamsRepo.RegisterTeamWithParticipation(ctx, gameID, deliveryID, registration.Name, registration.Email, registration.ParticipantsCount, password, autoConfirm) -} diff --git a/internal/services/game_service/registration_test.go b/internal/services/team_service/registration_test.go similarity index 97% rename from internal/services/game_service/registration_test.go rename to internal/services/team_service/registration_test.go index 1f587b5..b09b690 100644 --- a/internal/services/game_service/registration_test.go +++ b/internal/services/team_service/registration_test.go @@ -1,4 +1,4 @@ -package game_service +package team_service import ( "strings" diff --git a/internal/services/team_service/service.go b/internal/services/team_service/service.go new file mode 100644 index 0000000..a1244c7 --- /dev/null +++ b/internal/services/team_service/service.go @@ -0,0 +1,113 @@ +package team_service + +import ( + "context" + "errors" + "evening_detective_server/internal/modules/game_model" + "evening_detective_server/internal/modules/password_generator" + "evening_detective_server/internal/modules/team_qr_pdf" + "evening_detective_server/internal/repos/games_repo" + "evening_detective_server/internal/repos/teams_repo" + "strings" + "time" +) + +type TeamService struct { + gamesRepo *games_repo.GamesRepo + teamsRepo *teams_repo.TeamsRepo + passwordGenerator password_generator.IPasswordGenerator + teamQRGenerator team_qr_pdf.IGenerator + appBaseURL string +} + +func NewTeamService( + gamesRepo *games_repo.GamesRepo, + teamsRepo *teams_repo.TeamsRepo, + passwordGenerator password_generator.IPasswordGenerator, + teamQRGenerator team_qr_pdf.IGenerator, + appBaseURL string, +) *TeamService { + return &TeamService{ + gamesRepo: gamesRepo, + teamsRepo: teamsRepo, + passwordGenerator: passwordGenerator, + teamQRGenerator: teamQRGenerator, + appBaseURL: appBaseURL, + } +} + +func (s *TeamService) AddTeam( + ctx context.Context, + creatorId int, + gameId int, + name string, + captainEmail string, +) (*game_model.Team, error) { + captainEmail = strings.TrimSpace(captainEmail) + if captainEmail != "" && !validCaptainEmail(captainEmail) { + return nil, errors.New("Укажите корректную почту капитана") + } + password, err := s.passwordGenerator.Generate() + if err != nil { + return nil, err + } + game, err := s.gamesRepo.GetGameByID(ctx, gameId) + if err != nil { + return nil, err + } + id, err := s.teamsRepo.AddTeamWithParticipation(ctx, creatorId, gameId, name, password, captainEmail, !time.Now().Before(game.StartAt.Add(-24*time.Hour))) + if err != nil { + return nil, err + } + return &game_model.Team{ + ID: id, + Password: password, + }, nil +} + +func (s *TeamService) UpdateTeam(ctx context.Context, teamId int, name string) error { + return s.teamsRepo.UpdateTeam(ctx, teamId, name) +} + +func (s *TeamService) DeleteTeam(ctx context.Context, teamId int) error { + return s.teamsRepo.DeleteTeam(ctx, teamId) +} + +func (s *TeamService) RegisterTeam(ctx context.Context, gameID int, deliveryID string, registration Registration) (int, error) { + if err := registration.Validate(); err != nil { + return 0, err + } + if gameID <= 0 || deliveryID == "" || len(deliveryID) > 255 || strings.ContainsRune(deliveryID, 0) { + return 0, ErrInvalidRegistration + } + password, err := s.passwordGenerator.Generate() + if err != nil { + return 0, err + } + game, err := s.gamesRepo.GetGameByID(ctx, gameID) + if err != nil { + return 0, err + } + autoConfirm := time.Now().Before(game.StartAt.Add(-24*time.Hour)) == false + return s.teamsRepo.RegisterTeamWithParticipation(ctx, gameID, deliveryID, registration.Name, registration.Email, registration.ParticipantsCount, password, autoConfirm) +} + +func (s *TeamService) DownloadTeamQR(ctx context.Context, id int) ([]byte, string, error) { + game, err := s.gamesRepo.GetGameByID(ctx, id) + if err != nil { + return nil, "", err + } + teams, err := s.teamsRepo.GetActiveTeamsByGameID(ctx, id) + if err != nil { + return nil, "", err + } + if len(teams) == 0 { + return nil, "", team_qr_pdf.ErrNoTeams + } + cards, filename, err := teamQRData(s.appBaseURL, game, teams) + if err != nil { + return nil, "", err + } + data, err := s.teamQRGenerator.Generate(cards) + return data, filename, err +} diff --git a/internal/services/game_service/team_qr.go b/internal/services/team_service/team_qr.go similarity index 71% rename from internal/services/game_service/team_qr.go rename to internal/services/team_service/team_qr.go index fb188ef..810ab0e 100644 --- a/internal/services/game_service/team_qr.go +++ b/internal/services/team_service/team_qr.go @@ -1,7 +1,6 @@ -package game_service +package team_service import ( - "context" "errors" "fmt" "net/url" @@ -12,26 +11,6 @@ import ( "evening_detective_server/internal/repos" ) -func (s *GameService) DownloadTeamQR(ctx context.Context, id int) ([]byte, string, error) { - game, err := s.gamesRepo.GetGameByID(ctx, id) - if err != nil { - return nil, "", err - } - teams, err := s.teamsRepo.GetActiveTeamsByGameID(ctx, id) - if err != nil { - return nil, "", err - } - if len(teams) == 0 { - return nil, "", team_qr_pdf.ErrNoTeams - } - cards, filename, err := teamQRData(s.appBaseURL, game, teams) - if err != nil { - return nil, "", err - } - data, err := s.teamQRGenerator.Generate(cards) - return data, filename, err -} - func teamQRData(baseURL string, game *repos.Game, teams []*repos.Team) ([]team_qr_pdf.Card, string, error) { base, err := url.Parse(strings.TrimSpace(baseURL)) if err != nil || (base.Scheme != "http" && base.Scheme != "https") || base.Host == "" || base.User != nil || base.RawQuery != "" || base.Fragment != "" { diff --git a/internal/services/game_service/team_qr_test.go b/internal/services/team_service/team_qr_test.go similarity index 98% rename from internal/services/game_service/team_qr_test.go rename to internal/services/team_service/team_qr_test.go index ea2d18b..8d97dac 100644 --- a/internal/services/game_service/team_qr_test.go +++ b/internal/services/team_service/team_qr_test.go @@ -1,4 +1,4 @@ -package game_service +package team_service import ( "net/url"