generated from VLADIMIR/template
add
This commit is contained in:
@@ -27,6 +27,7 @@ import (
|
|||||||
"evening_detective_server/internal/services/answer_service"
|
"evening_detective_server/internal/services/answer_service"
|
||||||
"evening_detective_server/internal/services/file_service"
|
"evening_detective_server/internal/services/file_service"
|
||||||
"evening_detective_server/internal/services/game_service"
|
"evening_detective_server/internal/services/game_service"
|
||||||
|
"evening_detective_server/internal/services/gameplay_service"
|
||||||
"evening_detective_server/internal/services/mcp_service"
|
"evening_detective_server/internal/services/mcp_service"
|
||||||
"evening_detective_server/internal/services/participation_service"
|
"evening_detective_server/internal/services/participation_service"
|
||||||
"evening_detective_server/internal/services/scenarios_service"
|
"evening_detective_server/internal/services/scenarios_service"
|
||||||
@@ -141,12 +142,12 @@ func main() {
|
|||||||
scenariosRepo,
|
scenariosRepo,
|
||||||
scenarioService,
|
scenarioService,
|
||||||
os.Getenv("FILE_PREFIX_DOMAIN"),
|
os.Getenv("FILE_PREFIX_DOMAIN"),
|
||||||
storyteller,
|
|
||||||
actionsRepo,
|
actionsRepo,
|
||||||
applicationsRepo,
|
applicationsRepo,
|
||||||
cleaner,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
gameplayService := gameplay_service.NewGameplayService(gameRepo, teamRepo, scenarioService, storyteller, actionsRepo, applicationsRepo, cleaner, os.Getenv("FILE_PREFIX_DOMAIN"))
|
||||||
|
|
||||||
answerService := answer_service.NewAnswerService(gameRepo, teamRepo, scenarioService, answersRepo, actionsRepo)
|
answerService := answer_service.NewAnswerService(gameRepo, teamRepo, scenarioService, answersRepo, actionsRepo)
|
||||||
|
|
||||||
teamService := team_service.NewTeamService(gameRepo, teamRepo, passwordGenerator, team_qr_pdf.NewGenerator(), os.Getenv("APP_BASE_URL"))
|
teamService := team_service.NewTeamService(gameRepo, teamRepo, passwordGenerator, team_qr_pdf.NewGenerator(), os.Getenv("APP_BASE_URL"))
|
||||||
@@ -201,6 +202,7 @@ func main() {
|
|||||||
fileService,
|
fileService,
|
||||||
scenarioService,
|
scenarioService,
|
||||||
gameService,
|
gameService,
|
||||||
|
gameplayService,
|
||||||
answerService,
|
answerService,
|
||||||
teamService,
|
teamService,
|
||||||
participationService,
|
participationService,
|
||||||
@@ -309,7 +311,7 @@ func main() {
|
|||||||
// REST /api/teams/{id}/story). Путь /api/mcp длиннее /api/, поэтому
|
// REST /api/teams/{id}/story). Путь /api/mcp длиннее /api/, поэтому
|
||||||
// http.ServeMux отдаёт MCP-запросы этому хендлеру, а не gateway.
|
// http.ServeMux отдаёт MCP-запросы этому хендлеру, а не gateway.
|
||||||
mcpHTTP := server.NewStreamableHTTPServer(
|
mcpHTTP := server.NewStreamableHTTPServer(
|
||||||
mcp_service.NewMCPService(gameService, answerService).Server(),
|
mcp_service.NewMCPService(gameplayService, answerService).Server(),
|
||||||
server.WithDisableLocalhostProtection(true),
|
server.WithDisableLocalhostProtection(true),
|
||||||
)
|
)
|
||||||
mainMux.Handle("/api/mcp", mcpHTTP)
|
mainMux.Handle("/api/mcp", mcpHTTP)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"evening_detective_server/internal/services/answer_service"
|
"evening_detective_server/internal/services/answer_service"
|
||||||
"evening_detective_server/internal/services/file_service"
|
"evening_detective_server/internal/services/file_service"
|
||||||
"evening_detective_server/internal/services/game_service"
|
"evening_detective_server/internal/services/game_service"
|
||||||
|
"evening_detective_server/internal/services/gameplay_service"
|
||||||
"evening_detective_server/internal/services/participation_service"
|
"evening_detective_server/internal/services/participation_service"
|
||||||
"evening_detective_server/internal/services/scenarios_service"
|
"evening_detective_server/internal/services/scenarios_service"
|
||||||
"evening_detective_server/internal/services/team_service"
|
"evening_detective_server/internal/services/team_service"
|
||||||
@@ -26,6 +27,7 @@ type server struct {
|
|||||||
fileService *file_service.FileService
|
fileService *file_service.FileService
|
||||||
scenarioService *scenarios_service.ScenarioService
|
scenarioService *scenarios_service.ScenarioService
|
||||||
gameService *game_service.GameService
|
gameService *game_service.GameService
|
||||||
|
gameplayService *gameplay_service.GameplayService
|
||||||
answerService *answer_service.AnswerService
|
answerService *answer_service.AnswerService
|
||||||
teamService *team_service.TeamService
|
teamService *team_service.TeamService
|
||||||
participationService *participation_service.ParticipationService
|
participationService *participation_service.ParticipationService
|
||||||
@@ -39,6 +41,7 @@ func NewServer(
|
|||||||
fileService *file_service.FileService,
|
fileService *file_service.FileService,
|
||||||
scenarioService *scenarios_service.ScenarioService,
|
scenarioService *scenarios_service.ScenarioService,
|
||||||
gameService *game_service.GameService,
|
gameService *game_service.GameService,
|
||||||
|
gameplayService *gameplay_service.GameplayService,
|
||||||
answerService *answer_service.AnswerService,
|
answerService *answer_service.AnswerService,
|
||||||
teamService *team_service.TeamService,
|
teamService *team_service.TeamService,
|
||||||
participationService *participation_service.ParticipationService,
|
participationService *participation_service.ParticipationService,
|
||||||
@@ -51,6 +54,7 @@ func NewServer(
|
|||||||
fileService: fileService,
|
fileService: fileService,
|
||||||
scenarioService: scenarioService,
|
scenarioService: scenarioService,
|
||||||
gameService: gameService,
|
gameService: gameService,
|
||||||
|
gameplayService: gameplayService,
|
||||||
answerService: answerService,
|
answerService: answerService,
|
||||||
teamService: teamService,
|
teamService: teamService,
|
||||||
participationService: participationService,
|
participationService: participationService,
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ func (s *server) GiveTeamApplications(ctx context.Context, req *proto.GiveTeamAp
|
|||||||
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
|
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
|
||||||
}
|
}
|
||||||
|
|
||||||
err := s.gameService.GiveTeamApplications(
|
err := s.gameplayService.GiveTeamApplications(
|
||||||
ctx,
|
ctx,
|
||||||
int(req.Id),
|
int(req.Id),
|
||||||
req.Name,
|
req.Name,
|
||||||
@@ -96,7 +96,7 @@ func (s *server) GiveTeamApplications(ctx context.Context, req *proto.GiveTeamAp
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *server) GetTeamStory(ctx context.Context, req *proto.GetTeamStoryReq) (*proto.GetTeamStoryRsp, error) {
|
func (s *server) GetTeamStory(ctx context.Context, req *proto.GetTeamStoryReq) (*proto.GetTeamStoryRsp, error) {
|
||||||
story, game, actions, err := s.gameService.GetTeamActions(
|
story, game, actions, err := s.gameplayService.GetTeamActions(
|
||||||
ctx,
|
ctx,
|
||||||
int(req.Id),
|
int(req.Id),
|
||||||
req.Password,
|
req.Password,
|
||||||
@@ -119,7 +119,7 @@ func (s *server) GetTeamStory(ctx context.Context, req *proto.GetTeamStoryReq) (
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *server) AddTeamAction(ctx context.Context, req *proto.AddTeamActionReq) (*proto.AddTeamActionRsp, error) {
|
func (s *server) AddTeamAction(ctx context.Context, req *proto.AddTeamActionReq) (*proto.AddTeamActionRsp, error) {
|
||||||
err := s.gameService.AddTeamAction(
|
err := s.gameplayService.AddTeamAction(
|
||||||
ctx,
|
ctx,
|
||||||
int(req.Id),
|
int(req.Id),
|
||||||
req.Password,
|
req.Password,
|
||||||
@@ -141,7 +141,7 @@ func (s *server) DeleteLastTeamAction(ctx context.Context, req *proto.DeleteLast
|
|||||||
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
|
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
|
||||||
}
|
}
|
||||||
|
|
||||||
err := s.gameService.DeleteLastTeamAction(
|
err := s.gameplayService.DeleteLastTeamAction(
|
||||||
ctx,
|
ctx,
|
||||||
int(req.Id),
|
int(req.Id),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -32,24 +32,6 @@ func mapApplication(o *repos.Application, domain string) *storytelling.Applicati
|
|||||||
return app
|
return app
|
||||||
}
|
}
|
||||||
|
|
||||||
// newApplication — улика для сохранения команде (AddTeamAction): имена
|
|
||||||
// приходят из истории уже с доменным префиксом (mapStory), в БД хранятся
|
|
||||||
// относительные пути — префикс снимается идемпотентно; file_type
|
|
||||||
// деривируется при пустом значении. Name не меняется (очистка — в
|
|
||||||
// вызывающем коде, cleaner'ом).
|
|
||||||
func newApplication(app *storytelling.Application, domain string) *repos.Application {
|
|
||||||
image := strings.TrimPrefix(app.Image, domain)
|
|
||||||
fileType := app.FileType
|
|
||||||
if fileType == "" {
|
|
||||||
fileType = file_storage.FileType(image)
|
|
||||||
}
|
|
||||||
return &repos.Application{
|
|
||||||
Name: app.Name,
|
|
||||||
Image: image,
|
|
||||||
FileType: fileType,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// prefixDomain — идемпотентная префиксация ссылки доменом хранилища
|
// prefixDomain — идемпотентная префиксация ссылки доменом хранилища
|
||||||
// (пустые ссылки и внешние http/https URL не трогаются).
|
// (пустые ссылки и внешние http/https URL не трогаются).
|
||||||
func prefixDomain(ref, domain string) string {
|
func prefixDomain(ref, domain string) string {
|
||||||
|
|||||||
@@ -3,72 +3,11 @@ package game_service
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"evening_detective_server/internal/modules/storytelling"
|
|
||||||
"evening_detective_server/internal/repos"
|
"evening_detective_server/internal/repos"
|
||||||
)
|
)
|
||||||
|
|
||||||
const testDomain = "http://storage.test/api/files/"
|
const testDomain = "http://storage.test/api/files/"
|
||||||
|
|
||||||
func TestNewApplication(t *testing.T) {
|
|
||||||
cases := []struct {
|
|
||||||
name string
|
|
||||||
app *storytelling.Application
|
|
||||||
image string // ожидаемое хранимое имя (без домена)
|
|
||||||
ftype string // ожидаемый file_type
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "доменный префикс снимается, file_type сохраняется",
|
|
||||||
app: &storytelling.Application{
|
|
||||||
Name: "Улика",
|
|
||||||
Image: testDomain + "clue.pdf",
|
|
||||||
FileType: "pdf",
|
|
||||||
},
|
|
||||||
image: "clue.pdf",
|
|
||||||
ftype: "pdf",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "относительное имя не трогается",
|
|
||||||
app: &storytelling.Application{
|
|
||||||
Name: "Улика",
|
|
||||||
Image: "clue.png",
|
|
||||||
FileType: "image",
|
|
||||||
},
|
|
||||||
image: "clue.png",
|
|
||||||
ftype: "image",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "пустой file_type деривируется из расширения",
|
|
||||||
app: &storytelling.Application{
|
|
||||||
Name: "Улика",
|
|
||||||
Image: testDomain + "clue.ogg",
|
|
||||||
},
|
|
||||||
image: "clue.ogg",
|
|
||||||
ftype: "audio",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "внешний URL снимается как префикс только при совпадении домена",
|
|
||||||
app: &storytelling.Application{
|
|
||||||
Name: "Улика",
|
|
||||||
Image: "https://example.com/clue.pdf",
|
|
||||||
},
|
|
||||||
image: "https://example.com/clue.pdf",
|
|
||||||
ftype: "pdf",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, c := range cases {
|
|
||||||
got := newApplication(c.app, testDomain)
|
|
||||||
if got.Name != c.app.Name {
|
|
||||||
t.Errorf("%s: Name = %q, want %q", c.name, got.Name, c.app.Name)
|
|
||||||
}
|
|
||||||
if got.Image != c.image {
|
|
||||||
t.Errorf("%s: Image = %q, want %q", c.name, got.Image, c.image)
|
|
||||||
}
|
|
||||||
if got.FileType != c.ftype {
|
|
||||||
t.Errorf("%s: FileType = %q, want %q", c.name, got.FileType, c.ftype)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMapApplications(t *testing.T) {
|
func TestMapApplications(t *testing.T) {
|
||||||
o := []*repos.Application{
|
o := []*repos.Application{
|
||||||
{Name: "Улика", Image: "clue.pdf", FileType: ""},
|
{Name: "Улика", Image: "clue.pdf", FileType: ""},
|
||||||
|
|||||||
@@ -3,9 +3,7 @@ package game_service
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"evening_detective_server/internal/modules/cleaner"
|
|
||||||
"evening_detective_server/internal/modules/game_model"
|
"evening_detective_server/internal/modules/game_model"
|
||||||
"evening_detective_server/internal/modules/storytelling"
|
|
||||||
"evening_detective_server/internal/repos/actions_repo"
|
"evening_detective_server/internal/repos/actions_repo"
|
||||||
"evening_detective_server/internal/repos/applications_repo"
|
"evening_detective_server/internal/repos/applications_repo"
|
||||||
"evening_detective_server/internal/repos/games_repo"
|
"evening_detective_server/internal/repos/games_repo"
|
||||||
@@ -21,10 +19,8 @@ type GameService struct {
|
|||||||
scenariosRepo *scenarios_repo.ScenariosRepo
|
scenariosRepo *scenarios_repo.ScenariosRepo
|
||||||
scenarioService *scenarios_service.ScenarioService
|
scenarioService *scenarios_service.ScenarioService
|
||||||
domain string
|
domain string
|
||||||
storyteller storytelling.IStory
|
|
||||||
actionsRepo *actions_repo.ActionsRepo
|
actionsRepo *actions_repo.ActionsRepo
|
||||||
applicationsRepo *applications_repo.ApplicationsRepo
|
applicationsRepo *applications_repo.ApplicationsRepo
|
||||||
cleaner cleaner.ICleaner
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewGameService(
|
func NewGameService(
|
||||||
@@ -33,10 +29,8 @@ func NewGameService(
|
|||||||
scenariosRepo *scenarios_repo.ScenariosRepo,
|
scenariosRepo *scenarios_repo.ScenariosRepo,
|
||||||
scenarioService *scenarios_service.ScenarioService,
|
scenarioService *scenarios_service.ScenarioService,
|
||||||
domain string,
|
domain string,
|
||||||
storyteller storytelling.IStory,
|
|
||||||
actionsRepo *actions_repo.ActionsRepo,
|
actionsRepo *actions_repo.ActionsRepo,
|
||||||
applicationsRepo *applications_repo.ApplicationsRepo,
|
applicationsRepo *applications_repo.ApplicationsRepo,
|
||||||
cleaner cleaner.ICleaner,
|
|
||||||
) *GameService {
|
) *GameService {
|
||||||
return &GameService{
|
return &GameService{
|
||||||
gamesRepo: gamesRepo,
|
gamesRepo: gamesRepo,
|
||||||
@@ -44,10 +38,8 @@ func NewGameService(
|
|||||||
scenariosRepo: scenariosRepo,
|
scenariosRepo: scenariosRepo,
|
||||||
scenarioService: scenarioService,
|
scenarioService: scenarioService,
|
||||||
domain: domain,
|
domain: domain,
|
||||||
storyteller: storyteller,
|
|
||||||
actionsRepo: actionsRepo,
|
actionsRepo: actionsRepo,
|
||||||
applicationsRepo: applicationsRepo,
|
applicationsRepo: applicationsRepo,
|
||||||
cleaner: cleaner,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,136 +188,3 @@ func (s *GameService) FinishGame(ctx context.Context, id int) error {
|
|||||||
func (s *GameService) ResetGame(ctx context.Context, id int) error {
|
func (s *GameService) ResetGame(ctx context.Context, id int) error {
|
||||||
return s.gamesRepo.ResetGame(ctx, id)
|
return s.gamesRepo.ResetGame(ctx, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *GameService) GiveTeamApplications(ctx context.Context, teamId int, name string) error {
|
|
||||||
return s.applicationsRepo.UpdateApplicationStatus(ctx, teamId, name, "done")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *GameService) GetTeamActions(ctx context.Context, teamId int, password string) (*storytelling.Story, *game_model.Game, []actions_repo.Action, error) {
|
|
||||||
team, err := s.teamsRepo.GetTeamByID(ctx, teamId)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if team.Password != password || team.ParticipationStatus == "cancelled" {
|
|
||||||
return nil, nil, nil, teams_repo.ErrTeamNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
game, err := s.gamesRepo.GetGameByID(ctx, team.GameId)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, nil, err
|
|
||||||
}
|
|
||||||
if game.Status == "finish" {
|
|
||||||
return &storytelling.Story{}, game_model.MapGame(game, s.domain), nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
actions, err := s.actionsRepo.GetActionsByTeamID(ctx, teamId)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
scenario, err := s.scenarioService.GetFullScenarioByID(ctx, game.ScenarioId)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
codes := make([]string, len(actions))
|
|
||||||
for i, action := range actions {
|
|
||||||
codes[i] = action.Code
|
|
||||||
}
|
|
||||||
story := s.storyteller.GetStory(scenario.Story, codes)
|
|
||||||
filterTeamStory(story, game.Format)
|
|
||||||
return story, game_model.MapGame(game, s.domain), actions, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *GameService) AddTeamAction(ctx context.Context, teamId int, password string, actionCode string, viaMCP bool) error {
|
|
||||||
team, err := s.teamsRepo.GetTeamByID(ctx, teamId)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if team.Password != password || team.ParticipationStatus == "cancelled" {
|
|
||||||
return teams_repo.ErrTeamNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = s.actionsRepo.AddAction(ctx, teamId, actionCode, viaMCP)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if team.ParticipationStatus == "pending" {
|
|
||||||
if err := s.teamsRepo.ConfirmParticipationByFirstAction(ctx, teamId); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
game, err := s.gamesRepo.GetGameByID(ctx, team.GameId)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
actionDetails, err := s.actionsRepo.GetActionsByTeamID(ctx, teamId)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
scenario, err := s.scenarioService.GetFullScenarioByID(ctx, game.ScenarioId)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
actions := make([]string, len(actionDetails))
|
|
||||||
for i, action := range actionDetails {
|
|
||||||
actions[i] = action.Code
|
|
||||||
}
|
|
||||||
teamStory := s.storyteller.GetStory(scenario.Story, actions)
|
|
||||||
|
|
||||||
lastPlace := teamStory.Places[len(teamStory.Places)-1]
|
|
||||||
|
|
||||||
for _, application := range lastPlace.Applications {
|
|
||||||
app := newApplication(application, s.domain)
|
|
||||||
app.Name = s.cleaner.ClearText(app.Name)
|
|
||||||
if _, err := s.applicationsRepo.AddApplication(ctx, teamId, app.Name, app.Image, app.FileType); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *GameService) DeleteLastTeamAction(ctx context.Context, teamId int) error {
|
|
||||||
team, err := s.teamsRepo.GetTeamByID(ctx, teamId)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
game, err := s.gamesRepo.GetGameByID(ctx, team.GameId)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
actionDetails, err := s.actionsRepo.GetActionsByTeamID(ctx, teamId)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
scenario, err := s.scenarioService.GetFullScenarioByID(ctx, game.ScenarioId)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
actions := make([]string, len(actionDetails))
|
|
||||||
for i, action := range actionDetails {
|
|
||||||
actions[i] = action.Code
|
|
||||||
}
|
|
||||||
teamStory := s.storyteller.GetStory(scenario.Story, actions)
|
|
||||||
|
|
||||||
lastPlace := teamStory.Places[len(teamStory.Places)-1]
|
|
||||||
|
|
||||||
for _, application := range lastPlace.Applications {
|
|
||||||
if err := s.applicationsRepo.DeleteApplicationByTeamIdAndName(ctx, teamId, application.Name); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.actionsRepo.DeleteLastActionByTeamId(ctx, teamId)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package gameplay_service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"evening_detective_server/internal/modules/file_storage"
|
||||||
|
"evening_detective_server/internal/modules/storytelling"
|
||||||
|
"evening_detective_server/internal/repos"
|
||||||
|
)
|
||||||
|
|
||||||
|
// newApplication — улика для сохранения команде (AddTeamAction): имена
|
||||||
|
// приходят из истории уже с доменным префиксом (mapStory), в БД хранятся
|
||||||
|
// относительные пути — префикс снимается идемпотентно; file_type
|
||||||
|
// деривируется при пустом значении. Name не меняется (очистка — в
|
||||||
|
// вызывающем коде, cleaner'ом).
|
||||||
|
func newApplication(app *storytelling.Application, domain string) *repos.Application {
|
||||||
|
image := strings.TrimPrefix(app.Image, domain)
|
||||||
|
fileType := app.FileType
|
||||||
|
if fileType == "" {
|
||||||
|
fileType = file_storage.FileType(image)
|
||||||
|
}
|
||||||
|
return &repos.Application{
|
||||||
|
Name: app.Name,
|
||||||
|
Image: image,
|
||||||
|
FileType: fileType,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package gameplay_service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"evening_detective_server/internal/modules/storytelling"
|
||||||
|
)
|
||||||
|
|
||||||
|
const testDomain = "http://storage.test/api/files/"
|
||||||
|
|
||||||
|
func TestNewApplication(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
app *storytelling.Application
|
||||||
|
image string // ожидаемое хранимое имя (без домена)
|
||||||
|
ftype string // ожидаемый file_type
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "доменный префикс снимается, file_type сохраняется",
|
||||||
|
app: &storytelling.Application{
|
||||||
|
Name: "Улика",
|
||||||
|
Image: testDomain + "clue.pdf",
|
||||||
|
FileType: "pdf",
|
||||||
|
},
|
||||||
|
image: "clue.pdf",
|
||||||
|
ftype: "pdf",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "относительное имя не трогается",
|
||||||
|
app: &storytelling.Application{
|
||||||
|
Name: "Улика",
|
||||||
|
Image: "clue.png",
|
||||||
|
FileType: "image",
|
||||||
|
},
|
||||||
|
image: "clue.png",
|
||||||
|
ftype: "image",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "пустой file_type деривируется из расширения",
|
||||||
|
app: &storytelling.Application{
|
||||||
|
Name: "Улика",
|
||||||
|
Image: testDomain + "clue.ogg",
|
||||||
|
},
|
||||||
|
image: "clue.ogg",
|
||||||
|
ftype: "audio",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "внешний URL снимается как префикс только при совпадении домена",
|
||||||
|
app: &storytelling.Application{
|
||||||
|
Name: "Улика",
|
||||||
|
Image: "https://example.com/clue.pdf",
|
||||||
|
},
|
||||||
|
image: "https://example.com/clue.pdf",
|
||||||
|
ftype: "pdf",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
got := newApplication(c.app, testDomain)
|
||||||
|
if got.Name != c.app.Name {
|
||||||
|
t.Errorf("%s: Name = %q, want %q", c.name, got.Name, c.app.Name)
|
||||||
|
}
|
||||||
|
if got.Image != c.image {
|
||||||
|
t.Errorf("%s: Image = %q, want %q", c.name, got.Image, c.image)
|
||||||
|
}
|
||||||
|
if got.FileType != c.ftype {
|
||||||
|
t.Errorf("%s: FileType = %q, want %q", c.name, got.FileType, c.ftype)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
package game_service
|
package gameplay_service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -42,7 +42,7 @@ func TestFinishedTeamStoryPostgres(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
// No action/scenario repositories: finished games must not load either.
|
// No action/scenario repositories: finished games must not load either.
|
||||||
s := &GameService{gamesRepo: games_repo.NewGamesRepo(pool), teamsRepo: teams_repo.NewTeamsRepo(pool)}
|
s := &GameplayService{gamesRepo: games_repo.NewGamesRepo(pool), teamsRepo: teams_repo.NewTeamsRepo(pool)}
|
||||||
for _, format := range []string{"online", "offline"} {
|
for _, format := range []string{"online", "offline"} {
|
||||||
if _, err := pool.Exec(ctx, `UPDATE games SET format = $1`, format); err != nil {
|
if _, err := pool.Exec(ctx, `UPDATE games SET format = $1`, format); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
package gameplay_service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"evening_detective_server/internal/modules/cleaner"
|
||||||
|
"evening_detective_server/internal/modules/game_model"
|
||||||
|
"evening_detective_server/internal/modules/storytelling"
|
||||||
|
"evening_detective_server/internal/repos/actions_repo"
|
||||||
|
"evening_detective_server/internal/repos/applications_repo"
|
||||||
|
"evening_detective_server/internal/repos/games_repo"
|
||||||
|
"evening_detective_server/internal/repos/teams_repo"
|
||||||
|
"evening_detective_server/internal/services/scenarios_service"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GameplayService struct {
|
||||||
|
gamesRepo *games_repo.GamesRepo
|
||||||
|
teamsRepo *teams_repo.TeamsRepo
|
||||||
|
scenarioService *scenarios_service.ScenarioService
|
||||||
|
storyteller storytelling.IStory
|
||||||
|
actionsRepo *actions_repo.ActionsRepo
|
||||||
|
applicationsRepo *applications_repo.ApplicationsRepo
|
||||||
|
cleaner cleaner.ICleaner
|
||||||
|
domain string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGameplayService(
|
||||||
|
gamesRepo *games_repo.GamesRepo,
|
||||||
|
teamsRepo *teams_repo.TeamsRepo,
|
||||||
|
scenarioService *scenarios_service.ScenarioService,
|
||||||
|
storyteller storytelling.IStory,
|
||||||
|
actionsRepo *actions_repo.ActionsRepo,
|
||||||
|
applicationsRepo *applications_repo.ApplicationsRepo,
|
||||||
|
cleaner cleaner.ICleaner,
|
||||||
|
domain string,
|
||||||
|
) *GameplayService {
|
||||||
|
return &GameplayService{
|
||||||
|
gamesRepo: gamesRepo,
|
||||||
|
teamsRepo: teamsRepo,
|
||||||
|
scenarioService: scenarioService,
|
||||||
|
storyteller: storyteller,
|
||||||
|
actionsRepo: actionsRepo,
|
||||||
|
applicationsRepo: applicationsRepo,
|
||||||
|
cleaner: cleaner,
|
||||||
|
domain: domain,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GameplayService) GiveTeamApplications(ctx context.Context, teamId int, name string) error {
|
||||||
|
return s.applicationsRepo.UpdateApplicationStatus(ctx, teamId, name, "done")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GameplayService) GetTeamActions(ctx context.Context, teamId int, password string) (*storytelling.Story, *game_model.Game, []actions_repo.Action, error) {
|
||||||
|
team, err := s.teamsRepo.GetTeamByID(ctx, teamId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if team.Password != password || team.ParticipationStatus == "cancelled" {
|
||||||
|
return nil, nil, nil, teams_repo.ErrTeamNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
game, err := s.gamesRepo.GetGameByID(ctx, team.GameId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
if game.Status == "finish" {
|
||||||
|
return &storytelling.Story{}, game_model.MapGame(game, s.domain), nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
actions, err := s.actionsRepo.GetActionsByTeamID(ctx, teamId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
scenario, err := s.scenarioService.GetFullScenarioByID(ctx, game.ScenarioId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
codes := make([]string, len(actions))
|
||||||
|
for i, action := range actions {
|
||||||
|
codes[i] = action.Code
|
||||||
|
}
|
||||||
|
story := s.storyteller.GetStory(scenario.Story, codes)
|
||||||
|
filterTeamStory(story, game.Format)
|
||||||
|
return story, game_model.MapGame(game, s.domain), actions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GameplayService) AddTeamAction(ctx context.Context, teamId int, password string, actionCode string, viaMCP bool) error {
|
||||||
|
team, err := s.teamsRepo.GetTeamByID(ctx, teamId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if team.Password != password || team.ParticipationStatus == "cancelled" {
|
||||||
|
return teams_repo.ErrTeamNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = s.actionsRepo.AddAction(ctx, teamId, actionCode, viaMCP)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if team.ParticipationStatus == "pending" {
|
||||||
|
if err := s.teamsRepo.ConfirmParticipationByFirstAction(ctx, teamId); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
game, err := s.gamesRepo.GetGameByID(ctx, team.GameId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
actionDetails, err := s.actionsRepo.GetActionsByTeamID(ctx, teamId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
scenario, err := s.scenarioService.GetFullScenarioByID(ctx, game.ScenarioId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
actions := make([]string, len(actionDetails))
|
||||||
|
for i, action := range actionDetails {
|
||||||
|
actions[i] = action.Code
|
||||||
|
}
|
||||||
|
teamStory := s.storyteller.GetStory(scenario.Story, actions)
|
||||||
|
|
||||||
|
lastPlace := teamStory.Places[len(teamStory.Places)-1]
|
||||||
|
|
||||||
|
for _, application := range lastPlace.Applications {
|
||||||
|
app := newApplication(application, s.domain)
|
||||||
|
app.Name = s.cleaner.ClearText(app.Name)
|
||||||
|
if _, err := s.applicationsRepo.AddApplication(ctx, teamId, app.Name, app.Image, app.FileType); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *GameplayService) DeleteLastTeamAction(ctx context.Context, teamId int) error {
|
||||||
|
team, err := s.teamsRepo.GetTeamByID(ctx, teamId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
game, err := s.gamesRepo.GetGameByID(ctx, team.GameId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
actionDetails, err := s.actionsRepo.GetActionsByTeamID(ctx, teamId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
scenario, err := s.scenarioService.GetFullScenarioByID(ctx, game.ScenarioId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
actions := make([]string, len(actionDetails))
|
||||||
|
for i, action := range actionDetails {
|
||||||
|
actions[i] = action.Code
|
||||||
|
}
|
||||||
|
teamStory := s.storyteller.GetStory(scenario.Story, actions)
|
||||||
|
|
||||||
|
lastPlace := teamStory.Places[len(teamStory.Places)-1]
|
||||||
|
|
||||||
|
for _, application := range lastPlace.Applications {
|
||||||
|
if err := s.applicationsRepo.DeleteApplicationByTeamIdAndName(ctx, teamId, application.Name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.actionsRepo.DeleteLastActionByTeamId(ctx, teamId)
|
||||||
|
}
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package game_service
|
package gameplay_service
|
||||||
|
|
||||||
import "evening_detective_server/internal/modules/storytelling"
|
import "evening_detective_server/internal/modules/storytelling"
|
||||||
|
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package game_service
|
package gameplay_service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
@@ -28,7 +28,7 @@ import (
|
|||||||
const defaultCallTimeout = 10 * time.Second
|
const defaultCallTimeout = 10 * time.Second
|
||||||
|
|
||||||
// GamePlayer — граница доступа к игровым сервисам, реализуемая
|
// GamePlayer — граница доступа к игровым сервисам, реализуемая
|
||||||
// *game_service.GameService; интерфейс позволяет тестировать сервис
|
// *gameplay_service.GameplayService; интерфейс позволяет тестировать сервис
|
||||||
// без БД.
|
// без БД.
|
||||||
type GamePlayer interface {
|
type GamePlayer interface {
|
||||||
GetTeamActions(ctx context.Context, teamId int, password string) (*storytelling.Story, *game_model.Game, []actions_repo.Action, error)
|
GetTeamActions(ctx context.Context, teamId int, password string) (*storytelling.Story, *game_model.Game, []actions_repo.Action, error)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
|
|
||||||
"evening_detective_server/internal/modules/email_sender"
|
"evening_detective_server/internal/modules/email_sender"
|
||||||
"evening_detective_server/internal/repos/teams_repo"
|
"evening_detective_server/internal/repos/teams_repo"
|
||||||
"evening_detective_server/internal/services/game_service"
|
"evening_detective_server/internal/services/gameplay_service"
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
)
|
)
|
||||||
@@ -223,11 +223,11 @@ func TestParticipationPostgres(t *testing.T) {
|
|||||||
t.Fatal("cancelled team in active list")
|
t.Fatal("cancelled team in active list")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
gameService := game_service.NewGameService(nil, repo, nil, nil, "", nil, nil, nil, nil)
|
gameplayService := gameplay_service.NewGameplayService(nil, repo, nil, nil, nil, nil, nil, "")
|
||||||
if _, _, _, err := gameService.GetTeamActions(ctx, 3, "secret"); !errors.Is(err, teams_repo.ErrTeamNotFound) {
|
if _, _, _, err := gameplayService.GetTeamActions(ctx, 3, "secret"); !errors.Is(err, teams_repo.ErrTeamNotFound) {
|
||||||
t.Fatalf("cancelled team got story: %v", err)
|
t.Fatalf("cancelled team got story: %v", err)
|
||||||
}
|
}
|
||||||
if err := gameService.AddTeamAction(ctx, 3, "secret", "anything", false); !errors.Is(err, teams_repo.ErrTeamNotFound) {
|
if err := gameplayService.AddTeamAction(ctx, 3, "secret", "anything", false); !errors.Is(err, teams_repo.ErrTeamNotFound) {
|
||||||
t.Fatalf("cancelled team added action: %v", err)
|
t.Fatalf("cancelled team added action: %v", err)
|
||||||
}
|
}
|
||||||
answerService := answer_service.NewAnswerService(nil, repo, nil, nil, nil)
|
answerService := answer_service.NewAnswerService(nil, repo, nil, nil, nil)
|
||||||
|
|||||||
Reference in New Issue
Block a user