add team service

This commit is contained in:
2026-09-17 02:29:13 +07:00
parent 49fe193ed7
commit c70d839038
27 changed files with 227 additions and 190 deletions
+5 -4
View File
@@ -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)
+3 -3
View File
@@ -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 {
+2 -1
View File
@@ -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",
+4
View File
@@ -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,
+4 -4
View File
@@ -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 {
+1 -1
View File
@@ -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):
+11 -11
View File
@@ -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, &registration); 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, &registration); err != nil {
return game_service.Registration{}, nil, false, err
return team_service.Registration{}, nil, false, err
}
return registration, nil, false, nil
}
+6 -5
View File
@@ -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")
}
+3 -3
View File
@@ -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),
)
@@ -1,4 +1,4 @@
package game_service
package game_model
import (
"evening_detective_server/internal/services/scenarios_service"
@@ -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
}
@@ -1,4 +1,4 @@
package game_service
package game_model
import "evening_detective_server/internal/modules/storytelling"
@@ -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
}
+5 -2
View File
@@ -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
}
+3 -2
View File
@@ -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))
@@ -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},
}
+35 -83
View File
@@ -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)
}
+2 -2
View File
@@ -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)
}
+4 -3
View File
@@ -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{
@@ -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",
@@ -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)
}
@@ -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 <captain@example.org>", "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)
}
@@ -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)
}
@@ -1,4 +1,4 @@
package game_service
package team_service
import (
"strings"
+113
View File
@@ -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
}
@@ -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 != "" {
@@ -1,4 +1,4 @@
package game_service
package team_service
import (
"net/url"