Files
evening_detective_server/internal/services/answer_service/service.go
T
2026-10-11 16:00:18 +07:00

293 lines
8.9 KiB
Go

package answer_service
import (
"context"
"errors"
"evening_detective_server/internal/modules/storytelling"
"evening_detective_server/internal/repos"
"evening_detective_server/internal/repos/actions_repo"
"evening_detective_server/internal/repos/games_repo"
"evening_detective_server/internal/repos/team_answers_repo"
"evening_detective_server/internal/repos/teams_repo"
"evening_detective_server/internal/services/game_model"
"evening_detective_server/internal/services/scenarios_service"
)
// AnswerService handles game answers.
type AnswerService struct {
gamesRepo *games_repo.GamesRepo
teamsRepo *teams_repo.TeamsRepo
scenarioService *scenarios_service.ScenarioService
answersRepo *team_answers_repo.TeamAnswersRepo
actionsRepo *actions_repo.ActionsRepo
}
// NewAnswerService creates an answer service.
func NewAnswerService(
gamesRepo *games_repo.GamesRepo,
teamsRepo *teams_repo.TeamsRepo,
scenarioService *scenarios_service.ScenarioService,
answersRepo *team_answers_repo.TeamAnswersRepo,
actionsRepo *actions_repo.ActionsRepo,
) *AnswerService {
return &AnswerService{
gamesRepo: gamesRepo,
teamsRepo: teamsRepo,
scenarioService: scenarioService,
answersRepo: answersRepo,
actionsRepo: actionsRepo,
}
}
var (
ErrTeamNotFound = errors.New("команда не найдена")
ErrGameNotFound = errors.New("игра не найдена")
ErrGameFinished = errors.New("игра завершена, ответы больше нельзя изменять")
ErrGameNotStarted = errors.New("игра не начата, отвечать на вопросы нельзя")
ErrQuestionNotFound = errors.New("вопрос не найден")
ErrGameNotFinished = errors.New("итоги доступны после завершения игры")
ErrOfflineAnswers = errors.New("в offline-игре ответы через сайт недоступны")
)
func (s *AnswerService) getTeamAndGame(ctx context.Context, teamID string, password string) (*repos.Team, *repos.Game, error) {
team, err := s.teamsRepo.GetTeamByID(ctx, teamID)
if err != nil {
if errors.Is(err, teams_repo.ErrTeamNotFound) {
return nil, nil, ErrTeamNotFound
}
return nil, nil, err
}
if team.Password != password || team.ParticipationStatus == "cancelled" {
return nil, nil, ErrTeamNotFound
}
game, err := s.gamesRepo.GetGameByID(ctx, team.GameID)
if err != nil {
if errors.Is(err, games_repo.ErrGameNotFound) {
return nil, nil, ErrGameNotFound
}
return nil, nil, err
}
return team, game, nil
}
func (s *AnswerService) getScenarioQuestionsByCode(ctx context.Context, scenarioID int) ([]*storytelling.Question, map[string]*storytelling.Question, error) {
scenario, err := s.scenarioService.GetFullScenarioByID(ctx, scenarioID)
if err != nil {
return nil, nil, err
}
byCode := make(map[string]*storytelling.Question, len(scenario.Story.Questions))
for _, question := range scenario.Story.Questions {
byCode[question.Code] = question
}
return scenario.Story.Questions, byCode, nil
}
func (s *AnswerService) GetTeamAnswers(ctx context.Context, teamID string, password string) ([]Answer, error) {
_, game, err := s.getTeamAndGame(ctx, teamID, password)
if err != nil {
return nil, err
}
if game.Format == "offline" {
return nil, nil
}
_, byCode, err := s.getScenarioQuestionsByCode(ctx, game.ScenarioID)
if err != nil {
return nil, err
}
teamAnswers, err := s.answersRepo.GetAnswersByTeamID(ctx, teamID)
if err != nil {
return nil, err
}
answers := make([]Answer, 0, len(teamAnswers))
for _, teamAnswer := range teamAnswers {
if _, ok := byCode[teamAnswer.QuestionCode]; !ok {
continue
}
answers = append(answers, Answer{
QuestionCode: teamAnswer.QuestionCode,
Text: teamAnswer.Text,
})
}
return answers, nil
}
func (s *AnswerService) SubmitTeamAnswers(ctx context.Context, teamID string, password string, answers []Answer) error {
_, game, err := s.getTeamAndGame(ctx, teamID, password)
if err != nil {
return err
}
if game.Format == "offline" {
return ErrOfflineAnswers
}
switch game.Status {
case "run", "pause":
default:
if game.Status == "finish" {
return ErrGameFinished
}
return ErrGameNotStarted
}
_, byCode, err := s.getScenarioQuestionsByCode(ctx, game.ScenarioID)
if err != nil {
return err
}
rows := make([]repos.TeamAnswer, 0, len(answers))
for _, answer := range answers {
if _, ok := byCode[answer.QuestionCode]; !ok {
return ErrQuestionNotFound
}
rows = append(rows, repos.TeamAnswer{
QuestionCode: answer.QuestionCode,
Text: answer.Text,
})
}
return s.answersRepo.UpsertAnswers(ctx, teamID, rows)
}
func (s *AnswerService) GetGameAnswers(ctx context.Context, gameID int) (*GameAnswers, error) {
game, err := s.gamesRepo.GetGameByID(ctx, gameID)
if err != nil {
return nil, mapAnswerError(err)
}
questions, byCode, err := s.getScenarioQuestionsByCode(ctx, game.ScenarioID)
if err != nil {
return nil, err
}
teams, err := s.teamsRepo.GetActiveTeamsByGameID(ctx, gameID)
if err != nil {
return nil, err
}
teamIDs := make([]string, 0, len(teams))
for _, team := range teams {
teamIDs = append(teamIDs, team.ID)
}
actionsCountByTeamIDs, err := s.actionsRepo.GetActionsCountByTeamIDs(ctx, teamIDs)
if err != nil {
return nil, err
}
rows, err := s.answersRepo.GetAnswersByGameID(ctx, gameID)
if err != nil {
return nil, err
}
answerByTeamAndCode := make(map[string]map[string]AnswerScore, len(teams))
for _, row := range rows {
if _, ok := byCode[row.QuestionCode]; !ok {
continue
}
score := row.Score
if answerByTeamAndCode[row.TeamID] == nil {
answerByTeamAndCode[row.TeamID] = make(map[string]AnswerScore, len(questions))
}
answerByTeamAndCode[row.TeamID][row.QuestionCode] = AnswerScore{
QuestionCode: row.QuestionCode,
Text: row.Text,
Score: score,
}
}
teamAnswers := make([]TeamAnswers, 0, len(teams))
for _, team := range teams {
byCode := answerByTeamAndCode[team.ID]
answers := make([]AnswerScore, 0, len(questions))
for _, question := range questions {
if answer, ok := byCode[question.Code]; ok {
answers = append(answers, answer)
continue
}
answers = append(answers, AnswerScore{QuestionCode: question.Code})
}
mappedTeam := game_model.MapTeam(team)
mappedTeam.ActionsCount = actionsCountByTeamIDs[team.ID]
teamAnswers = append(teamAnswers, TeamAnswers{
Team: mappedTeam,
Answers: answers,
})
}
return &GameAnswers{
Questions: questions,
Teams: teamAnswers,
}, nil
}
func (s *AnswerService) SetTeamAnswerScore(ctx context.Context, teamID string, questionCode string, score *int) error {
team, err := s.teamsRepo.GetTeamByID(ctx, teamID)
if err != nil {
return mapAnswerError(err)
}
game, err := s.gamesRepo.GetGameByID(ctx, team.GameID)
if err != nil {
return mapAnswerError(err)
}
_, byCode, err := s.getScenarioQuestionsByCode(ctx, game.ScenarioID)
if err != nil {
return err
}
if _, ok := byCode[questionCode]; !ok {
return ErrQuestionNotFound
}
return s.answersRepo.SetAnswerScore(ctx, teamID, questionCode, score)
}
func (s *AnswerService) GetGameResults(ctx context.Context, gameID int) (*GameResults, error) {
game, err := s.gamesRepo.GetGameByID(ctx, gameID)
if err != nil {
return nil, mapAnswerError(err)
}
if game.Status != "finish" {
return nil, ErrGameNotFinished
}
questions, byCode, err := s.getScenarioQuestionsByCode(ctx, game.ScenarioID)
if err != nil {
return nil, err
}
teams, err := s.teamsRepo.GetActiveTeamsByGameID(ctx, gameID)
if err != nil {
return nil, err
}
teamIDs := make([]string, 0, len(teams))
for _, team := range teams {
teamIDs = append(teamIDs, team.ID)
}
actionsCountByTeamIDs, err := s.actionsRepo.GetActionsCountByTeamIDs(ctx, teamIDs)
if err != nil {
return nil, err
}
rows, err := s.answersRepo.GetAnswersByGameID(ctx, gameID)
if err != nil {
return nil, err
}
answerByTeamAndCode := make(map[string]map[string]AnswerScore, len(teams))
for _, row := range rows {
if _, ok := byCode[row.QuestionCode]; !ok {
continue
}
if answerByTeamAndCode[row.TeamID] == nil {
answerByTeamAndCode[row.TeamID] = make(map[string]AnswerScore, len(questions))
}
answerByTeamAndCode[row.TeamID][row.QuestionCode] = AnswerScore{
QuestionCode: row.QuestionCode,
Text: row.Text,
Score: row.Score,
}
}
mappedTeams := make([]*game_model.Team, 0, len(teams))
for _, team := range teams {
mapped := game_model.MapTeam(team)
mapped.ActionsCount = actionsCountByTeamIDs[team.ID]
mappedTeams = append(mappedTeams, mapped)
}
return &GameResults{
Questions: questions,
Teams: rankTeams(buildTeamResults(questions, mappedTeams, answerByTeamAndCode)),
}, nil
}
func mapAnswerError(err error) error {
if errors.Is(err, teams_repo.ErrTeamNotFound) {
return ErrTeamNotFound
}
if errors.Is(err, games_repo.ErrGameNotFound) {
return ErrGameNotFound
}
return err
}