Добавить серверную логику таймера экранов

This commit is contained in:
2026-09-21 18:17:15 +07:00
parent 458bf320d4
commit 002c4b26b4
4 changed files with 181 additions and 24 deletions
+54 -10
View File
@@ -7,6 +7,7 @@ import (
"errors"
"fmt"
"strconv"
"strings"
"time"
"evening_detective_server/internal/modules/processor_jwt"
@@ -153,15 +154,29 @@ func hashScreenToken(token string) string {
}
func screenState(session *screen_sessions_repo.ScreenSession) *proto.ScreenState {
timerState := proto.ScreenTimerState(session.TimerState)
timerRemaining := session.TimerRemainingSeconds
if timerState == proto.ScreenTimerState_SCREEN_TIMER_STATE_RUNNING {
if session.TimerEndsAt == nil || !session.TimerEndsAt.After(time.Now()) {
timerRemaining = 0
timerState = proto.ScreenTimerState_SCREEN_TIMER_STATE_FINISHED
} else {
timerRemaining = int64(time.Until(*session.TimerEndsAt) / time.Second)
}
}
state := &proto.ScreenState{
Id: fmt.Sprint(session.ID),
Status: "disconnected",
SoundEnabled: session.Sound,
Mode: proto.ScreenMode(session.Mode),
StateVersion: session.Version,
PositionMs: session.Position,
Command: proto.ScreenCommand(session.Command),
Volume: session.Volume,
Id: fmt.Sprint(session.ID),
Status: "disconnected",
SoundEnabled: session.Sound,
Mode: proto.ScreenMode(session.Mode),
StateVersion: session.Version,
PositionMs: session.Position,
Command: proto.ScreenCommand(session.Command),
Volume: session.Volume,
TimerLabel: session.TimerLabel,
TimerDurationSeconds: session.TimerDurationSeconds,
TimerRemainingSeconds: timerRemaining,
TimerState: timerState,
}
if time.Since(session.LastSeen) <= 5*time.Second {
state.Status = "connected"
@@ -218,8 +233,27 @@ func (s *server) SetScreenMode(ctx context.Context, req *proto.SetScreenModeReq)
return &proto.SetScreenModeRsp{}, nil
}
func (s *server) SetScreenTimer(context.Context, *proto.SetScreenTimerReq) (*proto.SetScreenTimerRsp, error) {
return nil, status.Error(codes.Unimplemented, "таймер экранов ещё не реализован")
func (s *server) SetScreenTimer(ctx context.Context, req *proto.SetScreenTimerReq) (*proto.SetScreenTimerRsp, error) {
if !canManageScreens(ctx) {
return nil, status.Error(codes.PermissionDenied, "permission denied")
}
if req == nil || req.GameId <= 0 || req.DurationSeconds <= 0 || req.DurationSeconds > 24*60*60 {
return nil, status.Error(codes.InvalidArgument, "некорректные параметры таймера")
}
label := strings.TrimSpace(req.Label)
if label == "" {
label = "Перерыв"
}
if len(label) > 200 {
return nil, status.Error(codes.InvalidArgument, "слишком длинная надпись таймера")
}
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 {
return nil, status.Error(codes.Internal, "не удалось настроить таймер экранов")
}
return &proto.SetScreenTimerRsp{}, nil
}
func (s *server) SendScreenCommand(ctx context.Context, req *proto.SendScreenCommandReq) (*proto.SendScreenCommandRsp, error) {
@@ -232,6 +266,12 @@ func (s *server) SendScreenCommand(ctx context.Context, req *proto.SendScreenCom
if _, err := s.gameService.GetGame(ctx, int(req.GameId)); err != nil {
return nil, status.Error(codes.NotFound, "игра не найдена")
}
if isTimerCommand(req.Command) {
if err := s.screenRepo.SetTimerCommand(ctx, int(req.GameId), int32(req.Command)); err != nil {
return nil, status.Error(codes.Internal, "не удалось выполнить команду таймера")
}
return &proto.SendScreenCommandRsp{}, nil
}
if req.PositionMs < 0 {
return nil, status.Error(codes.InvalidArgument, "некорректная позиция")
}
@@ -244,6 +284,10 @@ func (s *server) SendScreenCommand(ctx context.Context, req *proto.SendScreenCom
return &proto.SendScreenCommandRsp{}, nil
}
func isTimerCommand(command proto.ScreenCommand) bool {
return command >= proto.ScreenCommand_SCREEN_COMMAND_TIMER_START && command <= proto.ScreenCommand_SCREEN_COMMAND_TIMER_RESET
}
func (s *server) GetScreenResults(ctx context.Context, req *proto.GetScreenResultsReq) (*proto.GetScreenResultsRsp, error) {
if req == nil || req.ScreenId == "" || req.AccessToken == "" {
return nil, status.Error(codes.InvalidArgument, "некорректные данные экрана")
+33
View File
@@ -0,0 +1,33 @@
package app
import (
"testing"
"time"
"evening_detective_server/internal/repos/screen_sessions_repo"
proto "evening_detective_server/proto"
)
func TestScreenStateTimer(t *testing.T) {
session := &screen_sessions_repo.ScreenSession{
ID: 1,
LastSeen: time.Now(),
TimerLabel: "Перерыв",
TimerDurationSeconds: 600,
TimerRemainingSeconds: 600,
TimerState: int32(proto.ScreenTimerState_SCREEN_TIMER_STATE_RUNNING),
}
endsAt := time.Now().Add(10 * time.Second)
session.TimerEndsAt = &endsAt
state := screenState(session)
if state.TimerState != proto.ScreenTimerState_SCREEN_TIMER_STATE_RUNNING || state.TimerRemainingSeconds <= 0 || state.TimerRemainingSeconds > 10 {
t.Fatalf("unexpected running timer: state=%s remaining=%d", state.TimerState, state.TimerRemainingSeconds)
}
endsAt = time.Now().Add(-time.Second)
state = screenState(session)
if state.TimerState != proto.ScreenTimerState_SCREEN_TIMER_STATE_FINISHED || state.TimerRemainingSeconds != 0 {
t.Fatalf("unexpected finished timer: state=%s remaining=%d", state.TimerState, state.TimerRemainingSeconds)
}
}
+79 -14
View File
@@ -12,15 +12,20 @@ import (
var ErrScreenNotFound = errors.New("Экран не найден")
type ScreenSession struct {
ID int
GameID *int
Sound bool
LastSeen time.Time
Mode int32
Version int64
Position int64
Command int32
Volume int32
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
}
type ScreenSessionsRepo struct {
@@ -43,10 +48,12 @@ func (r *ScreenSessionsRepo) Create(ctx context.Context, tokenHash string) (int,
func (r *ScreenSessionsRepo) Get(ctx context.Context, id int, tokenHash string) (*ScreenSession, error) {
session := &ScreenSession{ID: id}
err := r.pool.QueryRow(ctx, `
SELECT game_id, sound_enabled, last_seen_at, mode, state_version, position_ms, command, volume
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
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)
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)
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrScreenNotFound
}
@@ -63,7 +70,8 @@ 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
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
FROM screen_sessions
WHERE game_id = $1
ORDER BY id`, gameID)
@@ -75,7 +83,8 @@ func (r *ScreenSessionsRepo) ListByGame(ctx context.Context, gameID int) ([]*Scr
var sessions []*ScreenSession
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); err != nil {
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 {
return nil, err
}
sessions = append(sessions, session)
@@ -98,6 +107,52 @@ func (r *ScreenSessionsRepo) SetCommand(ctx context.Context, gameID int, command
return err
}
func (r *ScreenSessionsRepo) SetTimer(ctx context.Context, gameID int, duration int64, label string) error {
_, err := r.pool.Exec(ctx, `
UPDATE screen_sessions
SET mode = 7,
timer_label = $2,
timer_duration_seconds = $3,
timer_remaining_seconds = $3,
timer_state = 1,
timer_ends_at = NULL,
command = 0,
state_version = state_version + 1,
updated_at = NOW()
WHERE game_id = $1`, gameID, label, duration)
return err
}
func (r *ScreenSessionsRepo) SetTimerCommand(ctx context.Context, gameID int, command int32) error {
_, err := r.pool.Exec(ctx, `
UPDATE screen_sessions
SET timer_remaining_seconds = CASE
WHEN $2 = 8 THEN timer_duration_seconds
WHEN $2 = 9 AND timer_ends_at IS NOT NULL THEN GREATEST(0, FLOOR(EXTRACT(EPOCH FROM (timer_ends_at - NOW())))::BIGINT)
WHEN $2 = 11 THEN timer_duration_seconds
ELSE timer_remaining_seconds
END,
timer_state = CASE
WHEN $2 = 8 THEN 2
WHEN $2 = 9 THEN CASE WHEN timer_ends_at IS NULL OR timer_ends_at > NOW() THEN 3 ELSE 4 END
WHEN $2 = 10 THEN CASE WHEN timer_remaining_seconds > 0 THEN 2 ELSE 4 END
WHEN $2 = 11 THEN 1
ELSE timer_state
END,
timer_ends_at = CASE
WHEN $2 = 8 THEN NOW() + make_interval(secs => timer_duration_seconds)
WHEN $2 = 9 OR $2 = 11 THEN NULL
WHEN $2 = 10 AND timer_remaining_seconds > 0 THEN NOW() + make_interval(secs => timer_remaining_seconds)
ELSE timer_ends_at
END,
mode = 7,
command = $2,
state_version = state_version + 1,
updated_at = NOW()
WHERE game_id = $1`, gameID, command)
return err
}
func (r *ScreenSessionsRepo) Bind(ctx context.Context, id, gameID int, tokenHash string) error {
result, err := r.pool.Exec(ctx, `
UPDATE screen_sessions
@@ -115,7 +170,17 @@ func (r *ScreenSessionsRepo) Bind(ctx context.Context, id, gameID int, tokenHash
func (r *ScreenSessionsRepo) Unbind(ctx context.Context, id int) error {
result, err := r.pool.Exec(ctx, `
UPDATE screen_sessions
SET game_id = NULL, mode = 1, command = 0, position_ms = 0, state_version = state_version + 1, updated_at = NOW()
SET game_id = NULL,
mode = 1,
command = 0,
position_ms = 0,
timer_label = 'Перерыв',
timer_duration_seconds = 600,
timer_remaining_seconds = 600,
timer_state = 1,
timer_ends_at = NULL,
state_version = state_version + 1,
updated_at = NOW()
WHERE id = $1`, id)
if err != nil {
return err