generated from VLADIMIR/template
up
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"evening_detective_server/internal/services/screen_plan_service"
|
||||
proto "evening_detective_server/proto"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
func (s *server) GetScreenPlan(ctx context.Context, req *proto.GetScreenPlanReq) (*proto.GetScreenPlanRsp, error) {
|
||||
if !canManageScreens(ctx) {
|
||||
return nil, status.Error(codes.PermissionDenied, "permission denied")
|
||||
}
|
||||
if req == nil || req.GameID <= 0 {
|
||||
return nil, status.Error(codes.InvalidArgument, "некорректный идентификатор игры")
|
||||
}
|
||||
if _, err := s.gameService.GetGame(ctx, int(req.GameID)); err != nil {
|
||||
return nil, status.Error(codes.NotFound, "игра не найдена")
|
||||
}
|
||||
plan, err := s.screenPlanService.Get(ctx, int(req.GameID))
|
||||
if errors.Is(err, screen_plan_service.ErrPlanNotFound) {
|
||||
return &proto.GetScreenPlanRsp{}, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, "не удалось получить план экранов")
|
||||
}
|
||||
return &proto.GetScreenPlanRsp{Plan: mapScreenPlan(plan)}, nil
|
||||
}
|
||||
|
||||
func (s *server) SaveScreenPlan(ctx context.Context, req *proto.SaveScreenPlanReq) (*proto.SaveScreenPlanRsp, error) {
|
||||
if !canManageScreens(ctx) {
|
||||
return nil, status.Error(codes.PermissionDenied, "permission denied")
|
||||
}
|
||||
if req == nil || req.GameID <= 0 {
|
||||
return nil, status.Error(codes.InvalidArgument, "некорректный идентификатор игры")
|
||||
}
|
||||
if _, err := s.gameService.GetGame(ctx, int(req.GameID)); err != nil {
|
||||
return nil, status.Error(codes.NotFound, "игра не найдена")
|
||||
}
|
||||
plan, err := s.screenPlanService.Save(ctx, int(req.GameID), mapScreenPlanSteps(req.Steps), int(req.CurrentStep))
|
||||
if err != nil {
|
||||
return nil, screenPlanStatus(err, "не удалось сохранить план экранов")
|
||||
}
|
||||
return &proto.SaveScreenPlanRsp{Plan: mapScreenPlan(plan)}, nil
|
||||
}
|
||||
|
||||
func (s *server) SetScreenPlanStep(ctx context.Context, req *proto.SetScreenPlanStepReq) (*proto.SetScreenPlanStepRsp, error) {
|
||||
if !canManageScreens(ctx) {
|
||||
return nil, status.Error(codes.PermissionDenied, "permission denied")
|
||||
}
|
||||
if req == nil || req.GameID <= 0 || req.CurrentStep < 0 {
|
||||
return nil, status.Error(codes.InvalidArgument, "некорректный шаг плана экранов")
|
||||
}
|
||||
if _, err := s.gameService.GetGame(ctx, int(req.GameID)); err != nil {
|
||||
return nil, status.Error(codes.NotFound, "игра не найдена")
|
||||
}
|
||||
plan, err := s.screenPlanService.SetStep(ctx, int(req.GameID), int(req.CurrentStep))
|
||||
if err != nil {
|
||||
return nil, screenPlanStatus(err, "не удалось применить шаг плана экранов")
|
||||
}
|
||||
return &proto.SetScreenPlanStepRsp{Plan: mapScreenPlan(plan)}, nil
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"evening_detective_server/internal/services/screen_plan_service"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
func screenPlanStatus(err error, fallback string) error {
|
||||
switch {
|
||||
case errors.Is(err, screen_plan_service.ErrPlanNotFound):
|
||||
return status.Error(codes.NotFound, "план экранов не найден")
|
||||
case errors.Is(err, screen_plan_service.ErrStepNotFound):
|
||||
return status.Error(codes.InvalidArgument, "шаг плана экранов не найден")
|
||||
case errors.Is(err, screen_plan_service.ErrInvalidPlan):
|
||||
return status.Error(codes.InvalidArgument, "некорректный план экранов")
|
||||
default:
|
||||
return status.Error(codes.Internal, fallback)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"evening_detective_server/internal/services/screen_plan_service"
|
||||
proto "evening_detective_server/proto"
|
||||
)
|
||||
|
||||
func mapScreenPlanSteps(steps []*proto.ScreenPlanStep) []screen_plan_service.Step {
|
||||
mapped := make([]screen_plan_service.Step, 0, len(steps))
|
||||
for _, step := range steps {
|
||||
if step == nil {
|
||||
mapped = append(mapped, screen_plan_service.Step{})
|
||||
continue
|
||||
}
|
||||
mapped = append(mapped, screen_plan_service.Step{
|
||||
Mode: int32(step.Mode),
|
||||
Text: step.Text,
|
||||
TimerLabel: step.TimerLabel,
|
||||
TimerDurationSeconds: step.TimerDurationSeconds,
|
||||
})
|
||||
}
|
||||
return mapped
|
||||
}
|
||||
|
||||
func mapScreenPlan(plan *screen_plan_service.Plan) *proto.ScreenPlan {
|
||||
if plan == nil {
|
||||
return nil
|
||||
}
|
||||
steps := make([]*proto.ScreenPlanStep, 0, len(plan.Steps))
|
||||
for _, step := range plan.Steps {
|
||||
steps = append(steps, &proto.ScreenPlanStep{
|
||||
Mode: proto.ScreenMode(step.Mode),
|
||||
Text: step.Text,
|
||||
TimerLabel: step.TimerLabel,
|
||||
TimerDurationSeconds: step.TimerDurationSeconds,
|
||||
})
|
||||
}
|
||||
return &proto.ScreenPlan{Steps: steps, CurrentStep: int32(plan.CurrentStep)}
|
||||
}
|
||||
@@ -93,6 +93,9 @@ func (s *server) BindScreen(ctx context.Context, req *proto.BindScreenReq) (*pro
|
||||
}
|
||||
return nil, status.Error(codes.Internal, "не удалось привязать экран")
|
||||
}
|
||||
if err := s.screenPlanService.ApplyCurrent(ctx, int(req.GameID)); err != nil {
|
||||
return nil, status.Error(codes.Internal, "не удалось применить план экранов")
|
||||
}
|
||||
session, err := s.screenService.Get(ctx, id, hash)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, "не удалось получить состояние экрана")
|
||||
@@ -179,6 +182,7 @@ func screenState(session *screen_service.ScreenSession) *proto.ScreenState {
|
||||
TimerDurationSeconds: session.TimerDurationSeconds,
|
||||
TimerRemainingSeconds: timerRemaining,
|
||||
TimerState: timerState,
|
||||
Text: session.Text,
|
||||
}
|
||||
if time.Since(session.LastSeen) <= 5*time.Second {
|
||||
state.Status = "connected"
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"evening_detective_server/internal/services/map_service"
|
||||
"evening_detective_server/internal/services/participation_service"
|
||||
"evening_detective_server/internal/services/scenarios_service"
|
||||
"evening_detective_server/internal/services/screen_plan_service"
|
||||
"evening_detective_server/internal/services/screen_service"
|
||||
"evening_detective_server/internal/services/team_service"
|
||||
"evening_detective_server/internal/services/ticket_service"
|
||||
@@ -32,6 +33,7 @@ type server struct {
|
||||
ticketService *ticket_service.TicketService
|
||||
participationService *participation_service.ParticipationService
|
||||
screenService *screen_service.ScreenService
|
||||
screenPlanService *screen_plan_service.ScreenPlanService
|
||||
mapService *map_service.MapService
|
||||
}
|
||||
|
||||
@@ -49,6 +51,7 @@ func NewServer(
|
||||
ticketService *ticket_service.TicketService,
|
||||
participationService *participation_service.ParticipationService,
|
||||
screenService *screen_service.ScreenService,
|
||||
screenPlanService *screen_plan_service.ScreenPlanService,
|
||||
mapService *map_service.MapService,
|
||||
) proto.EveningDetectiveServerServer {
|
||||
return &server{
|
||||
@@ -64,6 +67,7 @@ func NewServer(
|
||||
ticketService: ticketService,
|
||||
participationService: participationService,
|
||||
screenService: screenService,
|
||||
screenPlanService: screenPlanService,
|
||||
mapService: mapService,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
// Package game_screen_plans_repo persists game screen plans.
|
||||
package game_screen_plans_repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
var ErrPlanNotFound = errors.New("план экранов не найден")
|
||||
|
||||
// Step is a stored screen plan step.
|
||||
type Step struct {
|
||||
Mode int32 `json:"mode"`
|
||||
Text string `json:"text,omitempty"`
|
||||
TimerLabel string `json:"timerLabel,omitempty"`
|
||||
TimerDurationSeconds int64 `json:"timerDurationSeconds,omitempty"`
|
||||
}
|
||||
|
||||
// Plan is a stored game screen plan.
|
||||
type Plan struct {
|
||||
GameID int
|
||||
Steps []Step
|
||||
CurrentStep int
|
||||
}
|
||||
|
||||
// GameScreenPlansRepo persists game screen plans.
|
||||
type GameScreenPlansRepo struct {
|
||||
pool *pgxpool.Pool
|
||||
}
|
||||
|
||||
// NewGameScreenPlansRepo creates a game screen plans repository.
|
||||
func NewGameScreenPlansRepo(pool *pgxpool.Pool) *GameScreenPlansRepo {
|
||||
return &GameScreenPlansRepo{pool: pool}
|
||||
}
|
||||
|
||||
func (r *GameScreenPlansRepo) Get(ctx context.Context, gameID int) (*Plan, error) {
|
||||
plan := &Plan{GameID: gameID}
|
||||
var raw []byte
|
||||
err := r.pool.QueryRow(
|
||||
ctx,
|
||||
`
|
||||
SELECT steps, current_step
|
||||
FROM game_screen_plans
|
||||
WHERE game_id = $1`,
|
||||
gameID,
|
||||
).Scan(&raw, &plan.CurrentStep)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, ErrPlanNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := json.Unmarshal(raw, &plan.Steps); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return plan, nil
|
||||
}
|
||||
|
||||
func (r *GameScreenPlansRepo) Save(ctx context.Context, gameID int, steps []Step, currentStep int) (*Plan, error) {
|
||||
raw, err := json.Marshal(steps)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = r.pool.Exec(
|
||||
ctx,
|
||||
`
|
||||
INSERT INTO game_screen_plans (game_id, steps, current_step)
|
||||
VALUES ($1, $2::jsonb, $3)
|
||||
ON CONFLICT (game_id) DO UPDATE SET
|
||||
steps = EXCLUDED.steps,
|
||||
current_step = EXCLUDED.current_step,
|
||||
updated_at = NOW()`,
|
||||
gameID,
|
||||
raw,
|
||||
currentStep,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Plan{GameID: gameID, Steps: steps, CurrentStep: currentStep}, nil
|
||||
}
|
||||
|
||||
func (r *GameScreenPlansRepo) Delete(ctx context.Context, gameID int) error {
|
||||
_, err := r.pool.Exec(
|
||||
ctx,
|
||||
`DELETE FROM game_screen_plans
|
||||
WHERE game_id = $1`,
|
||||
gameID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
@@ -29,6 +29,7 @@ type ScreenSession struct {
|
||||
TimerRemainingSeconds int64
|
||||
TimerState int32
|
||||
TimerEndsAt *time.Time
|
||||
Text string
|
||||
}
|
||||
|
||||
// ScreenSessionsRepo persists screen sessions.
|
||||
@@ -56,11 +57,11 @@ func (r *ScreenSessionsRepo) Get(ctx context.Context, id int, tokenHash string)
|
||||
session := &ScreenSession{ID: id}
|
||||
err := r.pool.QueryRow(ctx, `
|
||||
SELECT game_id, sound_enabled, last_seen_at, mode, state_version, position_ms, command, volume,
|
||||
timer_label, timer_duration_seconds, timer_remaining_seconds, timer_state, timer_ends_at
|
||||
timer_label, timer_duration_seconds, timer_remaining_seconds, timer_state, timer_ends_at, text
|
||||
FROM screen_sessions
|
||||
WHERE id = $1 AND access_token_hash = $2`, id, tokenHash).
|
||||
Scan(&session.GameID, &session.Sound, &session.LastSeen, &session.Mode, &session.Version, &session.Position, &session.Command, &session.Volume,
|
||||
&session.TimerLabel, &session.TimerDurationSeconds, &session.TimerRemainingSeconds, &session.TimerState, &session.TimerEndsAt)
|
||||
&session.TimerLabel, &session.TimerDurationSeconds, &session.TimerRemainingSeconds, &session.TimerState, &session.TimerEndsAt, &session.Text)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, ErrScreenNotFound
|
||||
}
|
||||
@@ -79,7 +80,7 @@ func (r *ScreenSessionsRepo) Get(ctx context.Context, id int, tokenHash string)
|
||||
func (r *ScreenSessionsRepo) ListByGame(ctx context.Context, gameID int) ([]*ScreenSession, error) {
|
||||
rows, err := r.pool.Query(ctx, `
|
||||
SELECT id, game_id, sound_enabled, last_seen_at, mode, state_version, position_ms, command, volume,
|
||||
timer_label, timer_duration_seconds, timer_remaining_seconds, timer_state, timer_ends_at
|
||||
timer_label, timer_duration_seconds, timer_remaining_seconds, timer_state, timer_ends_at, text
|
||||
FROM screen_sessions
|
||||
WHERE game_id = $1
|
||||
ORDER BY id`, gameID)
|
||||
@@ -92,7 +93,7 @@ func (r *ScreenSessionsRepo) ListByGame(ctx context.Context, gameID int) ([]*Scr
|
||||
for rows.Next() {
|
||||
session := &ScreenSession{}
|
||||
if err := rows.Scan(&session.ID, &session.GameID, &session.Sound, &session.LastSeen, &session.Mode, &session.Version, &session.Position, &session.Command, &session.Volume,
|
||||
&session.TimerLabel, &session.TimerDurationSeconds, &session.TimerRemainingSeconds, &session.TimerState, &session.TimerEndsAt); err != nil {
|
||||
&session.TimerLabel, &session.TimerDurationSeconds, &session.TimerRemainingSeconds, &session.TimerState, &session.TimerEndsAt, &session.Text); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sessions = append(sessions, session)
|
||||
@@ -102,7 +103,35 @@ func (r *ScreenSessionsRepo) ListByGame(ctx context.Context, gameID int) ([]*Scr
|
||||
|
||||
// SetMode updates the display mode for a game.
|
||||
func (r *ScreenSessionsRepo) SetMode(ctx context.Context, gameID int, mode int32) error {
|
||||
_, err := r.pool.Exec(ctx, `UPDATE screen_sessions SET mode = $2, command = 0, state_version = state_version + 1, updated_at = NOW() WHERE game_id = $1`, gameID, mode)
|
||||
_, err := r.pool.Exec(
|
||||
ctx,
|
||||
`UPDATE screen_sessions
|
||||
SET mode = $2,
|
||||
text = '',
|
||||
command = 0,
|
||||
state_version = state_version + 1,
|
||||
updated_at = NOW()
|
||||
WHERE game_id = $1`,
|
||||
gameID,
|
||||
mode,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// SetText updates the text screen for a game.
|
||||
func (r *ScreenSessionsRepo) SetText(ctx context.Context, gameID int, text string) error {
|
||||
_, err := r.pool.Exec(
|
||||
ctx,
|
||||
`UPDATE screen_sessions
|
||||
SET mode = 8,
|
||||
text = $2,
|
||||
command = 0,
|
||||
state_version = state_version + 1,
|
||||
updated_at = NOW()
|
||||
WHERE game_id = $1`,
|
||||
gameID,
|
||||
text,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -130,6 +159,7 @@ func (r *ScreenSessionsRepo) SetTimer(ctx context.Context, gameID int, duration
|
||||
timer_ends_at = NULL,
|
||||
command = 0,
|
||||
state_version = state_version + 1,
|
||||
text = '',
|
||||
updated_at = NOW()
|
||||
WHERE game_id = $1`, gameID, label, duration)
|
||||
return err
|
||||
@@ -194,6 +224,7 @@ func (r *ScreenSessionsRepo) Unbind(ctx context.Context, id int) error {
|
||||
timer_remaining_seconds = 600,
|
||||
timer_state = 1,
|
||||
timer_ends_at = NULL,
|
||||
text = '',
|
||||
state_version = state_version + 1,
|
||||
updated_at = NOW()
|
||||
WHERE id = $1`, id)
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package screen_plan_service
|
||||
|
||||
import "evening_detective_server/internal/repos/game_screen_plans_repo"
|
||||
|
||||
func mapPlan(plan *game_screen_plans_repo.Plan) *Plan {
|
||||
steps := make([]Step, 0, len(plan.Steps))
|
||||
for _, step := range plan.Steps {
|
||||
steps = append(steps, Step{
|
||||
Mode: step.Mode,
|
||||
Text: step.Text,
|
||||
TimerLabel: step.TimerLabel,
|
||||
TimerDurationSeconds: step.TimerDurationSeconds,
|
||||
})
|
||||
}
|
||||
return &Plan{GameID: plan.GameID, Steps: steps, CurrentStep: plan.CurrentStep}
|
||||
}
|
||||
|
||||
func mapSteps(steps []Step) []game_screen_plans_repo.Step {
|
||||
mapped := make([]game_screen_plans_repo.Step, 0, len(steps))
|
||||
for _, step := range steps {
|
||||
mapped = append(mapped, game_screen_plans_repo.Step{
|
||||
Mode: step.Mode,
|
||||
Text: step.Text,
|
||||
TimerLabel: step.TimerLabel,
|
||||
TimerDurationSeconds: step.TimerDurationSeconds,
|
||||
})
|
||||
}
|
||||
return mapped
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
// Package screen_plan_service handles game screen plans.
|
||||
package screen_plan_service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"evening_detective_server/internal/repos/game_screen_plans_repo"
|
||||
"evening_detective_server/internal/services/screen_service"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrPlanNotFound indicates that a game has no screen plan.
|
||||
ErrPlanNotFound = errors.New("план экранов не найден")
|
||||
// ErrInvalidPlan indicates invalid screen plan data.
|
||||
ErrInvalidPlan = errors.New("некорректный план экранов")
|
||||
// ErrStepNotFound indicates that a screen plan step does not exist.
|
||||
ErrStepNotFound = errors.New("шаг плана экранов не найден")
|
||||
)
|
||||
|
||||
// Step is a screen plan step used by the application layer.
|
||||
type Step struct {
|
||||
Mode int32
|
||||
Text string
|
||||
TimerLabel string
|
||||
TimerDurationSeconds int64
|
||||
}
|
||||
|
||||
// Plan is a game screen plan used by the application layer.
|
||||
type Plan struct {
|
||||
GameID int
|
||||
Steps []Step
|
||||
CurrentStep int
|
||||
}
|
||||
|
||||
// ScreenPlanService handles game screen plans and applies their current step.
|
||||
type ScreenPlanService struct {
|
||||
repo *game_screen_plans_repo.GameScreenPlansRepo
|
||||
screenService *screen_service.ScreenService
|
||||
}
|
||||
|
||||
// NewScreenPlanService creates a screen plan service.
|
||||
func NewScreenPlanService(
|
||||
repo *game_screen_plans_repo.GameScreenPlansRepo,
|
||||
screenService *screen_service.ScreenService,
|
||||
) *ScreenPlanService {
|
||||
return &ScreenPlanService{repo: repo, screenService: screenService}
|
||||
}
|
||||
|
||||
// Get returns a game's screen plan.
|
||||
func (s *ScreenPlanService) Get(ctx context.Context, gameID int) (*Plan, error) {
|
||||
stored, err := s.repo.Get(ctx, gameID)
|
||||
if errors.Is(err, game_screen_plans_repo.ErrPlanNotFound) {
|
||||
return nil, ErrPlanNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return mapPlan(stored), nil
|
||||
}
|
||||
|
||||
// Save validates, stores and applies a game's screen plan.
|
||||
func (s *ScreenPlanService) Save(ctx context.Context, gameID int, steps []Step, currentStep int) (*Plan, error) {
|
||||
steps, err := normalizePlan(steps, currentStep)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(steps) == 0 {
|
||||
if err := s.repo.Delete(ctx, gameID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
stored, err := s.repo.Save(ctx, gameID, mapSteps(steps), currentStep)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
plan := mapPlan(stored)
|
||||
if err := s.applyStep(ctx, gameID, plan); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return plan, nil
|
||||
}
|
||||
|
||||
// SetStep stores and applies the selected step.
|
||||
func (s *ScreenPlanService) SetStep(ctx context.Context, gameID, currentStep int) (*Plan, error) {
|
||||
plan, err := s.Get(ctx, gameID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if currentStep < 0 || currentStep >= len(plan.Steps) {
|
||||
return nil, ErrStepNotFound
|
||||
}
|
||||
stored, err := s.repo.Save(ctx, gameID, mapSteps(plan.Steps), currentStep)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
plan = mapPlan(stored)
|
||||
if err := s.applyStep(ctx, gameID, plan); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return plan, nil
|
||||
}
|
||||
|
||||
// ApplyCurrent applies the current step when a screen joins a game.
|
||||
func (s *ScreenPlanService) ApplyCurrent(ctx context.Context, gameID int) error {
|
||||
plan, err := s.Get(ctx, gameID)
|
||||
if errors.Is(err, ErrPlanNotFound) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.applyStep(ctx, gameID, plan)
|
||||
}
|
||||
|
||||
func normalizePlan(steps []Step, currentStep int) ([]Step, error) {
|
||||
if currentStep < 0 || (len(steps) > 0 && currentStep >= len(steps)) || (len(steps) == 0 && currentStep != 0) {
|
||||
return nil, ErrInvalidPlan
|
||||
}
|
||||
normalized := make([]Step, 0, len(steps))
|
||||
for _, step := range steps {
|
||||
if step.Mode <= 0 || step.Mode > 8 {
|
||||
return nil, ErrInvalidPlan
|
||||
}
|
||||
step.Text = strings.TrimSpace(step.Text)
|
||||
step.TimerLabel = strings.TrimSpace(step.TimerLabel)
|
||||
if step.Mode == 8 && (step.Text == "" || len(step.Text) > 10000) {
|
||||
return nil, ErrInvalidPlan
|
||||
}
|
||||
if step.Mode == 7 {
|
||||
if step.TimerDurationSeconds <= 0 || step.TimerDurationSeconds > 24*60*60 {
|
||||
return nil, ErrInvalidPlan
|
||||
}
|
||||
if step.TimerLabel == "" {
|
||||
step.TimerLabel = "Перерыв"
|
||||
}
|
||||
if len(step.TimerLabel) > 200 {
|
||||
return nil, ErrInvalidPlan
|
||||
}
|
||||
}
|
||||
normalized = append(normalized, step)
|
||||
}
|
||||
return normalized, nil
|
||||
}
|
||||
|
||||
func (s *ScreenPlanService) applyStep(ctx context.Context, gameID int, plan *Plan) error {
|
||||
step := plan.Steps[plan.CurrentStep]
|
||||
if step.Mode == 8 {
|
||||
return s.screenService.SetText(ctx, gameID, step.Text)
|
||||
}
|
||||
if step.Mode == 7 {
|
||||
return s.screenService.SetTimer(ctx, gameID, step.TimerDurationSeconds, step.TimerLabel)
|
||||
}
|
||||
return s.screenService.SetMode(ctx, gameID, step.Mode)
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package screen_plan_service
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNormalizePlan(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
steps []Step
|
||||
currentStep int
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "normalizes text and timer defaults",
|
||||
steps: []Step{
|
||||
{Mode: 8, Text: " Внимание "},
|
||||
{Mode: 7, TimerDurationSeconds: 60},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "rejects empty text",
|
||||
steps: []Step{{Mode: 8}},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "rejects missing current step",
|
||||
steps: []Step{{Mode: 1}},
|
||||
currentStep: 1,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
steps, err := normalizePlan(tt.steps, tt.currentStep)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Fatalf("normalizePlan() error = %v, want error: %v", err, tt.wantErr)
|
||||
}
|
||||
if tt.wantErr {
|
||||
return
|
||||
}
|
||||
if steps[0].Text != "Внимание" || steps[1].TimerLabel != "Перерыв" {
|
||||
t.Fatalf("plan defaults were not normalized: %+v", steps)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ type ScreenSession struct {
|
||||
TimerRemainingSeconds int64
|
||||
TimerState int32
|
||||
TimerEndsAt *time.Time
|
||||
Text string
|
||||
}
|
||||
|
||||
func mapSession(session *screen_sessions_repo.ScreenSession) *ScreenSession {
|
||||
@@ -41,5 +42,6 @@ func mapSession(session *screen_sessions_repo.ScreenSession) *ScreenSession {
|
||||
TimerRemainingSeconds: session.TimerRemainingSeconds,
|
||||
TimerState: session.TimerState,
|
||||
TimerEndsAt: session.TimerEndsAt,
|
||||
Text: session.Text,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,11 @@ func (s *ScreenService) SetTimer(ctx context.Context, gameID int, duration int64
|
||||
return s.repo.SetTimer(ctx, gameID, duration, label)
|
||||
}
|
||||
|
||||
// SetText updates the text screen for a game.
|
||||
func (s *ScreenService) SetText(ctx context.Context, gameID int, text string) error {
|
||||
return s.repo.SetText(ctx, gameID, text)
|
||||
}
|
||||
|
||||
// SetTimerCommand updates the timer command for a game.
|
||||
func (s *ScreenService) SetTimerCommand(ctx context.Context, gameID int, command int32) error {
|
||||
return s.repo.SetTimerCommand(ctx, gameID, command)
|
||||
|
||||
Reference in New Issue
Block a user