generated from VLADIMIR/template
Добавить серверную логику таймера экранов
This commit is contained in:
+54
-10
@@ -7,6 +7,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"evening_detective_server/internal/modules/processor_jwt"
|
"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 {
|
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{
|
state := &proto.ScreenState{
|
||||||
Id: fmt.Sprint(session.ID),
|
Id: fmt.Sprint(session.ID),
|
||||||
Status: "disconnected",
|
Status: "disconnected",
|
||||||
SoundEnabled: session.Sound,
|
SoundEnabled: session.Sound,
|
||||||
Mode: proto.ScreenMode(session.Mode),
|
Mode: proto.ScreenMode(session.Mode),
|
||||||
StateVersion: session.Version,
|
StateVersion: session.Version,
|
||||||
PositionMs: session.Position,
|
PositionMs: session.Position,
|
||||||
Command: proto.ScreenCommand(session.Command),
|
Command: proto.ScreenCommand(session.Command),
|
||||||
Volume: session.Volume,
|
Volume: session.Volume,
|
||||||
|
TimerLabel: session.TimerLabel,
|
||||||
|
TimerDurationSeconds: session.TimerDurationSeconds,
|
||||||
|
TimerRemainingSeconds: timerRemaining,
|
||||||
|
TimerState: timerState,
|
||||||
}
|
}
|
||||||
if time.Since(session.LastSeen) <= 5*time.Second {
|
if time.Since(session.LastSeen) <= 5*time.Second {
|
||||||
state.Status = "connected"
|
state.Status = "connected"
|
||||||
@@ -218,8 +233,27 @@ func (s *server) SetScreenMode(ctx context.Context, req *proto.SetScreenModeReq)
|
|||||||
return &proto.SetScreenModeRsp{}, nil
|
return &proto.SetScreenModeRsp{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *server) SetScreenTimer(context.Context, *proto.SetScreenTimerReq) (*proto.SetScreenTimerRsp, error) {
|
func (s *server) SetScreenTimer(ctx context.Context, req *proto.SetScreenTimerReq) (*proto.SetScreenTimerRsp, error) {
|
||||||
return nil, status.Error(codes.Unimplemented, "таймер экранов ещё не реализован")
|
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) {
|
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 {
|
if _, err := s.gameService.GetGame(ctx, int(req.GameId)); err != nil {
|
||||||
return nil, status.Error(codes.NotFound, "игра не найдена")
|
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 {
|
if req.PositionMs < 0 {
|
||||||
return nil, status.Error(codes.InvalidArgument, "некорректная позиция")
|
return nil, status.Error(codes.InvalidArgument, "некорректная позиция")
|
||||||
}
|
}
|
||||||
@@ -244,6 +284,10 @@ func (s *server) SendScreenCommand(ctx context.Context, req *proto.SendScreenCom
|
|||||||
return &proto.SendScreenCommandRsp{}, nil
|
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) {
|
func (s *server) GetScreenResults(ctx context.Context, req *proto.GetScreenResultsReq) (*proto.GetScreenResultsRsp, error) {
|
||||||
if req == nil || req.ScreenId == "" || req.AccessToken == "" {
|
if req == nil || req.ScreenId == "" || req.AccessToken == "" {
|
||||||
return nil, status.Error(codes.InvalidArgument, "некорректные данные экрана")
|
return nil, status.Error(codes.InvalidArgument, "некорректные данные экрана")
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,15 +12,20 @@ import (
|
|||||||
var ErrScreenNotFound = errors.New("Экран не найден")
|
var ErrScreenNotFound = errors.New("Экран не найден")
|
||||||
|
|
||||||
type ScreenSession struct {
|
type ScreenSession struct {
|
||||||
ID int
|
ID int
|
||||||
GameID *int
|
GameID *int
|
||||||
Sound bool
|
Sound bool
|
||||||
LastSeen time.Time
|
LastSeen time.Time
|
||||||
Mode int32
|
Mode int32
|
||||||
Version int64
|
Version int64
|
||||||
Position int64
|
Position int64
|
||||||
Command int32
|
Command int32
|
||||||
Volume int32
|
Volume int32
|
||||||
|
TimerLabel string
|
||||||
|
TimerDurationSeconds int64
|
||||||
|
TimerRemainingSeconds int64
|
||||||
|
TimerState int32
|
||||||
|
TimerEndsAt *time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
type ScreenSessionsRepo struct {
|
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) {
|
func (r *ScreenSessionsRepo) Get(ctx context.Context, id int, tokenHash string) (*ScreenSession, error) {
|
||||||
session := &ScreenSession{ID: id}
|
session := &ScreenSession{ID: id}
|
||||||
err := r.pool.QueryRow(ctx, `
|
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
|
FROM screen_sessions
|
||||||
WHERE id = $1 AND access_token_hash = $2`, id, tokenHash).
|
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) {
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
return nil, ErrScreenNotFound
|
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) {
|
func (r *ScreenSessionsRepo) ListByGame(ctx context.Context, gameID int) ([]*ScreenSession, error) {
|
||||||
rows, err := r.pool.Query(ctx, `
|
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
|
FROM screen_sessions
|
||||||
WHERE game_id = $1
|
WHERE game_id = $1
|
||||||
ORDER BY id`, gameID)
|
ORDER BY id`, gameID)
|
||||||
@@ -75,7 +83,8 @@ func (r *ScreenSessionsRepo) ListByGame(ctx context.Context, gameID int) ([]*Scr
|
|||||||
var sessions []*ScreenSession
|
var sessions []*ScreenSession
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
session := &ScreenSession{}
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
sessions = append(sessions, session)
|
sessions = append(sessions, session)
|
||||||
@@ -98,6 +107,52 @@ func (r *ScreenSessionsRepo) SetCommand(ctx context.Context, gameID int, command
|
|||||||
return err
|
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 {
|
func (r *ScreenSessionsRepo) Bind(ctx context.Context, id, gameID int, tokenHash string) error {
|
||||||
result, err := r.pool.Exec(ctx, `
|
result, err := r.pool.Exec(ctx, `
|
||||||
UPDATE screen_sessions
|
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 {
|
func (r *ScreenSessionsRepo) Unbind(ctx context.Context, id int) error {
|
||||||
result, err := r.pool.Exec(ctx, `
|
result, err := r.pool.Exec(ctx, `
|
||||||
UPDATE screen_sessions
|
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)
|
WHERE id = $1`, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
-- +goose Up
|
||||||
|
ALTER TABLE screen_sessions
|
||||||
|
ADD COLUMN IF NOT EXISTS timer_label TEXT NOT NULL DEFAULT 'Перерыв',
|
||||||
|
ADD COLUMN IF NOT EXISTS timer_duration_seconds BIGINT NOT NULL DEFAULT 600,
|
||||||
|
ADD COLUMN IF NOT EXISTS timer_remaining_seconds BIGINT NOT NULL DEFAULT 600,
|
||||||
|
ADD COLUMN IF NOT EXISTS timer_state INTEGER NOT NULL DEFAULT 1,
|
||||||
|
ADD COLUMN IF NOT EXISTS timer_ends_at TIMESTAMP;
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
ALTER TABLE screen_sessions
|
||||||
|
DROP COLUMN IF EXISTS timer_ends_at,
|
||||||
|
DROP COLUMN IF EXISTS timer_state,
|
||||||
|
DROP COLUMN IF EXISTS timer_remaining_seconds,
|
||||||
|
DROP COLUMN IF EXISTS timer_duration_seconds,
|
||||||
|
DROP COLUMN IF EXISTS timer_label;
|
||||||
Reference in New Issue
Block a user