Files
evening_detective_server/internal/services/answer_service/service.go
T
2026-10-03 13:22:11 +07:00

293 lines
8.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 int, 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 int, 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 int, 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([]int, 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[int]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 int, 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([]int, 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[int]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
}