diff --git a/cmd/evening_detective_server/main.go b/cmd/evening_detective_server/main.go index 51a7675..51e0a16 100644 --- a/cmd/evening_detective_server/main.go +++ b/cmd/evening_detective_server/main.go @@ -34,6 +34,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/screen_service" "evening_detective_server/internal/services/team_service" "evening_detective_server/internal/services/ticket_service" "evening_detective_server/internal/services/ui_service" @@ -137,6 +138,7 @@ func main() { applicationsRepo := applications_repo.NewApplicationsRepo(dbpool) answersRepo := team_answers_repo.NewTeamAnswersRepo(dbpool) screenRepo := screen_sessions_repo.NewScreenSessionsRepo(dbpool) + screenService := screen_service.NewScreenService(screenRepo) mapRepo := game_map_repo.New(dbpool) reviewsRepo := game_reviews_repo.New(dbpool) gameService := game_service.NewGameService( @@ -217,7 +219,7 @@ func main() { teamService, ticketService, participationService, - screenRepo, + screenService, mapRepo, ), ) diff --git a/docs/GO_STANDARD.md b/docs/GO_STANDARD.md index c7c1e51..5f4d57a 100644 --- a/docs/GO_STANDARD.md +++ b/docs/GO_STANDARD.md @@ -186,6 +186,7 @@ SQL держи рядом с методом, которому он нужен. `make generate`. - Преобразование `proto ↔ domain/storage` находится в `app` или пакете, которому принадлежат обе модели; бизнес-правила в mapping не прячь. +- Поля структур в mapping-литералах располагай вертикально: одно поле на строку. - Любой mapping, принадлежащий сервису, находится в отдельной функции и файле `*_mapper.go`, а не внутри метода сервиса. - Модель, которой владеет один пакет, объявляй в этом пакете. Не создавай @@ -270,6 +271,7 @@ go test ./... - Аббревиатуры пишутся единообразно: `ID`, `URL`, `HTTP`, `JWT`, `SQL`. - Используй префикс `I` в именах интерфейсов: `IFileStorage`, `IEmailSender`. - Не прячь побочные эффекты в функциях с именами `Get`, `Map`, `Parse`. +- Сервисы и конструкторы называй по предметной области: `ScreenService` и `NewScreenService`; generic-имена `Service` и `New` запрещены. - `New...` создаёт значение и возвращает ошибку только если создание реально может завершиться ошибкой. - Форматируй код `gofmt`; не добавляй ручное форматирование, генераторы и diff --git a/internal/app/game_map.go b/internal/app/game_map.go index 803e1bc..ea5e2cd 100644 --- a/internal/app/game_map.go +++ b/internal/app/game_map.go @@ -33,7 +33,7 @@ func (s *server) GetGameMap(ctx context.Context, req *proto.GetGameMapReq) (*pro return nil, status.Error(codes.Internal, "не удалось сохранить карту") } } - screens, err := s.screenRepo.ListByGame(ctx, int(req.GameID)) + screens, err := s.screenService.ListByGame(ctx, int(req.GameID)) if err != nil { return nil, status.Error(codes.Internal, "не удалось получить экраны") } diff --git a/internal/app/screens.go b/internal/app/screens.go index fe7144c..9662afa 100644 --- a/internal/app/screens.go +++ b/internal/app/screens.go @@ -12,7 +12,7 @@ import ( "evening_detective_server/internal/modules/processor_jwt" "evening_detective_server/internal/modules/roles" - "evening_detective_server/internal/repos/screen_sessions_repo" + "evening_detective_server/internal/services/screen_service" proto "evening_detective_server/proto" "github.com/google/uuid" @@ -22,7 +22,7 @@ import ( func (s *server) CreateScreenSession(ctx context.Context, _ *proto.CreateScreenSessionReq) (*proto.CreateScreenSessionRsp, error) { token := uuid.New().String() - id, err := s.screenRepo.Create(ctx, hashScreenToken(token)) + id, err := s.screenService.Create(ctx, hashScreenToken(token)) if err != nil { return nil, status.Error(codes.Internal, "не удалось создать сессию экрана") } @@ -37,8 +37,8 @@ func (s *server) GetScreenState(ctx context.Context, req *proto.GetScreenStateRe if err != nil || id <= 0 { return nil, status.Error(codes.InvalidArgument, "некорректный идентификатор экрана") } - session, err := s.screenRepo.Get(ctx, id, hashScreenToken(req.AccessToken)) - if errors.Is(err, screen_sessions_repo.ErrScreenNotFound) { + session, err := s.screenService.Get(ctx, id, hashScreenToken(req.AccessToken)) + if errors.Is(err, screen_service.ErrScreenNotFound) { return nil, status.Error(codes.Unauthenticated, "недействительная сессия экрана") } if err != nil { @@ -59,7 +59,7 @@ func (s *server) GetGameScreens(ctx context.Context, req *proto.GetGameScreensRe if _, err := s.gameService.GetGame(ctx, int(req.GameID)); err != nil { return nil, status.Error(codes.NotFound, "игра не найдена") } - sessions, err := s.screenRepo.ListByGame(ctx, int(req.GameID)) + sessions, err := s.screenService.ListByGame(ctx, int(req.GameID)) if err != nil { return nil, status.Error(codes.Internal, "не удалось получить экраны игры") } @@ -87,13 +87,13 @@ func (s *server) BindScreen(ctx context.Context, req *proto.BindScreenReq) (*pro return nil, status.Error(codes.NotFound, "игра не найдена") } hash := hashScreenToken(req.AccessToken) - if err = s.screenRepo.Bind(ctx, id, int(req.GameID), hash); err != nil { - if errors.Is(err, screen_sessions_repo.ErrScreenNotFound) { + if err = s.screenService.Bind(ctx, id, int(req.GameID), hash); err != nil { + if errors.Is(err, screen_service.ErrScreenNotFound) { return nil, status.Error(codes.Unauthenticated, "недействительная сессия экрана") } return nil, status.Error(codes.Internal, "не удалось привязать экран") } - session, err := s.screenRepo.Get(ctx, id, hash) + session, err := s.screenService.Get(ctx, id, hash) if err != nil { return nil, status.Error(codes.Internal, "не удалось получить состояние экрана") } @@ -113,8 +113,8 @@ func (s *server) UnbindScreen(ctx context.Context, req *proto.UnbindScreenReq) ( if err != nil || id <= 0 { return nil, status.Error(codes.InvalidArgument, "некорректный идентификатор экрана") } - if err = s.screenRepo.Unbind(ctx, id); err != nil { - if errors.Is(err, screen_sessions_repo.ErrScreenNotFound) { + if err = s.screenService.Unbind(ctx, id); err != nil { + if errors.Is(err, screen_service.ErrScreenNotFound) { return nil, status.Error(codes.NotFound, "экран не найден") } return nil, status.Error(codes.Internal, "не удалось отвязать экран") @@ -133,8 +133,8 @@ func (s *server) SetScreenSound(ctx context.Context, req *proto.SetScreenSoundRe if err != nil || id <= 0 { return nil, status.Error(codes.InvalidArgument, "некорректный идентификатор экрана") } - if err = s.screenRepo.SetSound(ctx, id, req.SoundEnabled); err != nil { - if errors.Is(err, screen_sessions_repo.ErrScreenNotFound) { + if err = s.screenService.SetSound(ctx, id, req.SoundEnabled); err != nil { + if errors.Is(err, screen_service.ErrScreenNotFound) { return nil, status.Error(codes.NotFound, "экран не найден") } return nil, status.Error(codes.Internal, "не удалось назначить экран со звуком") @@ -155,7 +155,7 @@ func hashScreenToken(token string) string { return base64.RawURLEncoding.EncodeToString(sum[:]) } -func screenState(session *screen_sessions_repo.ScreenSession) *proto.ScreenState { +func screenState(session *screen_service.ScreenSession) *proto.ScreenState { timerState := proto.ScreenTimerState(session.TimerState) timerRemaining := session.TimerRemainingSeconds if timerState == proto.ScreenTimerState_SCREEN_TIMER_STATE_RUNNING { @@ -193,7 +193,7 @@ func screenState(session *screen_sessions_repo.ScreenSession) *proto.ScreenState return state } -func (s *server) enrichScreenState(ctx context.Context, session *screen_sessions_repo.ScreenSession, state *proto.ScreenState) { +func (s *server) enrichScreenState(ctx context.Context, session *screen_service.ScreenSession, state *proto.ScreenState) { if session.GameID == nil { return } @@ -229,7 +229,7 @@ func (s *server) SetScreenMode(ctx context.Context, req *proto.SetScreenModeReq) if _, err := s.gameService.GetGame(ctx, int(req.GameID)); err != nil { return nil, status.Error(codes.NotFound, "игра не найдена") } - if err := s.screenRepo.SetMode(ctx, int(req.GameID), int32(req.Mode)); err != nil { + if err := s.screenService.SetMode(ctx, int(req.GameID), int32(req.Mode)); err != nil { return nil, status.Error(codes.Internal, "не удалось изменить режим экранов") } return &proto.SetScreenModeRsp{}, nil @@ -252,7 +252,7 @@ func (s *server) SetScreenTimer(ctx context.Context, req *proto.SetScreenTimerRe if _, err := s.gameService.GetGame(ctx, int(req.GameID)); err != nil { return nil, status.Error(codes.NotFound, "игра не найдена") } - if err := s.screenRepo.SetTimer(ctx, int(req.GameID), req.DurationSeconds, label); err != nil { + if err := s.screenService.SetTimer(ctx, int(req.GameID), req.DurationSeconds, label); err != nil { return nil, status.Error(codes.Internal, "не удалось настроить таймер экранов") } return &proto.SetScreenTimerRsp{}, nil @@ -269,7 +269,7 @@ func (s *server) SendScreenCommand(ctx context.Context, req *proto.SendScreenCom return nil, status.Error(codes.NotFound, "игра не найдена") } if isTimerCommand(req.Command) { - if err := s.screenRepo.SetTimerCommand(ctx, int(req.GameID), int32(req.Command)); err != nil { + if err := s.screenService.SetTimerCommand(ctx, int(req.GameID), int32(req.Command)); err != nil { return nil, status.Error(codes.Internal, "не удалось выполнить команду таймера") } return &proto.SendScreenCommandRsp{}, nil @@ -280,7 +280,7 @@ func (s *server) SendScreenCommand(ctx context.Context, req *proto.SendScreenCom if req.Volume < 0 || req.Volume > 100 { return nil, status.Error(codes.InvalidArgument, "некорректная громкость") } - if err := s.screenRepo.SetCommand(ctx, int(req.GameID), int32(req.Command), req.PositionMs, req.Volume); err != nil { + if err := s.screenService.SetCommand(ctx, int(req.GameID), int32(req.Command), req.PositionMs, req.Volume); err != nil { return nil, status.Error(codes.Internal, "не удалось сохранить позицию экранов") } return &proto.SendScreenCommandRsp{}, nil @@ -298,8 +298,8 @@ func (s *server) GetScreenResults(ctx context.Context, req *proto.GetScreenResul if err != nil || id <= 0 { return nil, status.Error(codes.InvalidArgument, "некорректный идентификатор экрана") } - session, err := s.screenRepo.Get(ctx, id, hashScreenToken(req.AccessToken)) - if errors.Is(err, screen_sessions_repo.ErrScreenNotFound) { + session, err := s.screenService.Get(ctx, id, hashScreenToken(req.AccessToken)) + if errors.Is(err, screen_service.ErrScreenNotFound) { return nil, status.Error(codes.Unauthenticated, "недействительная сессия экрана") } if err != nil || session.GameID == nil { diff --git a/internal/app/screens_timer_test.go b/internal/app/screens_timer_test.go index 8b3f722..70ea087 100644 --- a/internal/app/screens_timer_test.go +++ b/internal/app/screens_timer_test.go @@ -4,12 +4,12 @@ import ( "testing" "time" - "evening_detective_server/internal/repos/screen_sessions_repo" + "evening_detective_server/internal/services/screen_service" proto "evening_detective_server/proto" ) func TestScreenStateTimer(t *testing.T) { - session := &screen_sessions_repo.ScreenSession{ + session := &screen_service.ScreenSession{ ID: 1, LastSeen: time.Now(), TimerLabel: "Перерыв", diff --git a/internal/app/server.go b/internal/app/server.go index 85496c0..bc411e0 100644 --- a/internal/app/server.go +++ b/internal/app/server.go @@ -2,7 +2,6 @@ package app import ( "evening_detective_server/internal/repos/game_map_repo" - "evening_detective_server/internal/repos/screen_sessions_repo" "evening_detective_server/internal/services/answer_service" "evening_detective_server/internal/services/file_service" "evening_detective_server/internal/services/game_review_service" @@ -10,6 +9,7 @@ import ( "evening_detective_server/internal/services/gameplay_service" "evening_detective_server/internal/services/participation_service" "evening_detective_server/internal/services/scenarios_service" + "evening_detective_server/internal/services/screen_service" "evening_detective_server/internal/services/team_service" "evening_detective_server/internal/services/ticket_service" "evening_detective_server/internal/services/ui_service" @@ -31,7 +31,7 @@ type server struct { teamService *team_service.TeamService ticketService *ticket_service.Service participationService *participation_service.ParticipationService - screenRepo *screen_sessions_repo.ScreenSessionsRepo + screenService *screen_service.ScreenService mapRepo *game_map_repo.Repo } @@ -47,7 +47,7 @@ func NewServer( teamService *team_service.TeamService, ticketService *ticket_service.Service, participationService *participation_service.ParticipationService, - screenRepo *screen_sessions_repo.ScreenSessionsRepo, + screenService *screen_service.ScreenService, mapRepo *game_map_repo.Repo, ) proto.EveningDetectiveServerServer { return &server{ @@ -62,7 +62,7 @@ func NewServer( teamService: teamService, ticketService: ticketService, participationService: participationService, - screenRepo: screenRepo, + screenService: screenService, mapRepo: mapRepo, } } diff --git a/internal/services/screen_service/mapper.go b/internal/services/screen_service/mapper.go new file mode 100644 index 0000000..2286f01 --- /dev/null +++ b/internal/services/screen_service/mapper.go @@ -0,0 +1,43 @@ +package screen_service + +import ( + "time" + + "evening_detective_server/internal/repos/screen_sessions_repo" +) + +type ScreenSession struct { + ID int + GameID *int + Sound bool + LastSeen time.Time + Mode int32 + Version int64 + Position int64 + Command int32 + Volume int32 + TimerLabel string + TimerDurationSeconds int64 + TimerRemainingSeconds int64 + TimerState int32 + TimerEndsAt *time.Time +} + +func mapSession(session *screen_sessions_repo.ScreenSession) *ScreenSession { + return &ScreenSession{ + ID: session.ID, + GameID: session.GameID, + Sound: session.Sound, + LastSeen: session.LastSeen, + Mode: session.Mode, + Version: session.Version, + Position: session.Position, + Command: session.Command, + Volume: session.Volume, + TimerLabel: session.TimerLabel, + TimerDurationSeconds: session.TimerDurationSeconds, + TimerRemainingSeconds: session.TimerRemainingSeconds, + TimerState: session.TimerState, + TimerEndsAt: session.TimerEndsAt, + } +} diff --git a/internal/services/screen_service/service.go b/internal/services/screen_service/service.go new file mode 100644 index 0000000..85e1189 --- /dev/null +++ b/internal/services/screen_service/service.go @@ -0,0 +1,77 @@ +package screen_service + +import ( + "context" + "errors" + + "evening_detective_server/internal/repos/screen_sessions_repo" +) + +var ErrScreenNotFound = errors.New("Экран не найден") + +type ScreenService struct { + repo *screen_sessions_repo.ScreenSessionsRepo +} + +func NewScreenService(repo *screen_sessions_repo.ScreenSessionsRepo) *ScreenService { + return &ScreenService{repo: repo} +} + +func (s *ScreenService) Create(ctx context.Context, tokenHash string) (int, error) { + return s.repo.Create(ctx, tokenHash) +} + +func (s *ScreenService) Get(ctx context.Context, id int, tokenHash string) (*ScreenSession, error) { + session, err := s.repo.Get(ctx, id, tokenHash) + if err != nil { + return nil, mapError(err) + } + return mapSession(session), nil +} + +func (s *ScreenService) ListByGame(ctx context.Context, gameID int) ([]*ScreenSession, error) { + sessions, err := s.repo.ListByGame(ctx, gameID) + if err != nil { + return nil, err + } + result := make([]*ScreenSession, 0, len(sessions)) + for _, session := range sessions { + result = append(result, mapSession(session)) + } + return result, nil +} + +func (s *ScreenService) SetMode(ctx context.Context, gameID int, mode int32) error { + return s.repo.SetMode(ctx, gameID, mode) +} + +func (s *ScreenService) SetTimer(ctx context.Context, gameID int, duration int64, label string) error { + return s.repo.SetTimer(ctx, gameID, duration, label) +} + +func (s *ScreenService) SetTimerCommand(ctx context.Context, gameID int, command int32) error { + return s.repo.SetTimerCommand(ctx, gameID, command) +} + +func (s *ScreenService) SetCommand(ctx context.Context, gameID int, command int32, position int64, volume int32) error { + return s.repo.SetCommand(ctx, gameID, command, position, volume) +} + +func (s *ScreenService) Bind(ctx context.Context, id, gameID int, tokenHash string) error { + return mapError(s.repo.Bind(ctx, id, gameID, tokenHash)) +} + +func (s *ScreenService) Unbind(ctx context.Context, id int) error { + return mapError(s.repo.Unbind(ctx, id)) +} + +func (s *ScreenService) SetSound(ctx context.Context, id int, enabled bool) error { + return mapError(s.repo.SetSound(ctx, id, enabled)) +} + +func mapError(err error) error { + if errors.Is(err, screen_sessions_repo.ErrScreenNotFound) { + return ErrScreenNotFound + } + return err +}