This commit is contained in:
2026-08-19 02:15:57 +07:00
parent 72605a3cd4
commit 204a20c814
25 changed files with 2892 additions and 500 deletions
+94 -2
View File
@@ -5,6 +5,7 @@ import (
"errors"
"evening_detective_server/internal/modules/cleaner"
"evening_detective_server/internal/modules/storytelling"
"evening_detective_server/internal/repos"
"evening_detective_server/internal/repos/scenarios_repo"
"strings"
)
@@ -88,30 +89,111 @@ func (s *ScenarioService) UpdateScenarioByID(
name string,
description string,
image string,
actorId int,
isAdmin bool,
) error {
if _, err := s.getScenarioForChange(ctx, id, actorId, isAdmin); err != nil {
return err
}
return s.scenariosRepo.UpdateScenarioByID(ctx, id, name, description, image)
}
// ErrScenarioPublished — опубликованный сценарий нельзя снять с публикации
// или удалить: при публикации исключительное право отчуждается Оператору
// (п. 7.2 Пользовательского соглашения, ст. 1234 ГК РФ), а право на отзыв
// действует только до обнародования (ст. 1269 ГК РФ).
var ErrScenarioPublished = errors.New("Опубликованный сценарий нельзя снять с публикации или удалить: исключительное право отчуждено Оператору")
// ErrScenarioNotOwner — сценарий принадлежит другому пользователю.
var ErrScenarioNotOwner = errors.New("Сценарий принадлежит другому пользователю")
// getScenarioForChange проверяет права актора на изменение сценария:
// не-админ должен быть автором сценария, сценарий не должен быть удалён.
// Возвращает сценарий, чтобы вызывающий код не делал повторное чтение.
func (s *ScenarioService) getScenarioForChange(
ctx context.Context,
id int,
actorId int,
isAdmin bool,
) (*repos.Scenario, error) {
scenario, err := s.scenariosRepo.GetScenarioByID(ctx, id)
if err != nil {
return nil, err
}
if scenario.IsDeleted {
return nil, scenarios_repo.ErrScenarioNotFound
}
if isAdmin {
return scenario, nil
}
if scenario.Author == nil || scenario.Author.ID != actorId {
return nil, ErrScenarioNotOwner
}
return scenario, nil
}
func (s *ScenarioService) UpdateScenarioStatusByID(
ctx context.Context,
id int,
status string,
actorId int,
isAdmin bool,
) error {
return s.scenariosRepo.UpdateScenarioStatusByID(ctx, id, status)
scenario, err := s.getScenarioForChange(ctx, id, actorId, isAdmin)
if err != nil {
return err
}
if !isAdmin && scenario.Status == "public" {
// Повторная публикация уже опубликованного — идемпотентный успех
// (published_at не двигается: CASE WHEN published_at IS NULL).
if status == "public" {
return nil
}
return ErrScenarioPublished
}
// Guard в SQL защищает от гонки «чтение статуса → запись»: не-админ не
// сможет перевести в draft сценарий, опубликованный между чтением и UPDATE.
updated, err := s.scenariosRepo.UpdateScenarioStatusByID(ctx, id, status, isAdmin)
if err != nil {
return err
}
if !updated {
return ErrScenarioPublished
}
return nil
}
func (s *ScenarioService) DeleteScenarioByID(
ctx context.Context,
id int,
actorId int,
isAdmin bool,
) error {
return s.scenariosRepo.DeleteScenarioByID(ctx, id)
if _, err := s.getScenarioForChange(ctx, id, actorId, isAdmin); err != nil {
return err
}
// Guard в SQL защищает от гонки «чтение статуса → запись»: не-админ не
// сможет удалить сценарий, опубликованный между чтением и UPDATE.
updated, err := s.scenariosRepo.DeleteScenarioByID(ctx, id, isAdmin)
if err != nil {
return err
}
if !updated {
return ErrScenarioPublished
}
return nil
}
func (s *ScenarioService) AddScenarioPlace(
ctx context.Context,
id int,
place *storytelling.Place,
actorId int,
isAdmin bool,
) error {
if _, err := s.getScenarioForChange(ctx, id, actorId, isAdmin); err != nil {
return err
}
story, err := s.getStory(ctx, id)
if err != nil {
return err
@@ -128,7 +210,12 @@ func (s *ScenarioService) UpdateScenarioPlace(
id int,
code string,
place *storytelling.Place,
actorId int,
isAdmin bool,
) error {
if _, err := s.getScenarioForChange(ctx, id, actorId, isAdmin); err != nil {
return err
}
story, err := s.getStory(ctx, id)
if err != nil {
return err
@@ -146,7 +233,12 @@ func (s *ScenarioService) DeleteScenarioPlace(
ctx context.Context,
id int,
code string,
actorId int,
isAdmin bool,
) error {
if _, err := s.getScenarioForChange(ctx, id, actorId, isAdmin); err != nil {
return err
}
story, err := s.getStory(ctx, id)
if err != nil {
return err