add reviews

This commit is contained in:
2026-09-18 01:37:48 +07:00
parent 6c8f1fb9e1
commit e9e35c60d5
15 changed files with 2278 additions and 551 deletions
@@ -0,0 +1,116 @@
package game_review_service
import (
"context"
"errors"
"fmt"
"html"
"log"
"net/url"
"strings"
"time"
"evening_detective_server/internal/modules/email_sender"
"evening_detective_server/internal/repos/game_reviews_repo"
"evening_detective_server/internal/repos/games_repo"
"evening_detective_server/internal/repos/teams_repo"
"github.com/jackc/pgx/v5"
)
var (
ErrGameNotFinished = errors.New("Оценить можно только завершённую игру")
ErrInvalidReview = errors.New("Оценка должна быть от 1 до 5, текст обязателен")
)
type Service struct {
reviews *game_reviews_repo.Repo
games *games_repo.GamesRepo
teams *teams_repo.TeamsRepo
appBaseURL string
}
func New(reviews *game_reviews_repo.Repo, games *games_repo.GamesRepo, teams *teams_repo.TeamsRepo, appBaseURL string) *Service {
return &Service{reviews: reviews, games: games, teams: teams, appBaseURL: appBaseURL}
}
func (s *Service) Create(ctx context.Context, gameID, teamID int, password string, rating int32, text string) (*game_reviews_repo.Review, error) {
if rating < 1 || rating > 5 || strings.TrimSpace(text) == "" {
return nil, ErrInvalidReview
}
team, err := s.teams.GetTeamByID(ctx, teamID)
if err != nil || team.GameId != gameID || team.Password != password || team.ParticipationStatus == "cancelled" {
return nil, teams_repo.ErrTeamNotFound
}
game, err := s.games.GetGameByID(ctx, gameID)
if err != nil {
return nil, err
}
if game.Status != "finish" {
return nil, ErrGameNotFinished
}
return s.reviews.Create(ctx, gameID, teamID, rating, strings.TrimSpace(text))
}
func (s *Service) List(ctx context.Context, gameID int, all bool) ([]*game_reviews_repo.Review, float64, int32, error) {
if _, err := s.games.GetGameByID(ctx, gameID); err != nil {
return nil, 0, 0, err
}
return s.reviews.List(ctx, gameID, all)
}
func (s *Service) Moderate(ctx context.Context, id int, status string) (*game_reviews_repo.Review, error) {
return s.reviews.Moderate(ctx, id, status)
}
func reviewReminderEmail(job *teams_repo.ReviewReminder, baseURL string) email_sender.Message {
link := strings.TrimRight(baseURL, "/") + "/team-story/" + fmt.Sprint(job.TeamID) + "?" + url.Values{"password": {job.Password}, "review": {"1"}}.Encode()
body := fmt.Sprintf("Команда «%s», поделитесь впечатлениями об игре «%s».\n\nОценка и отзыв займут пару минут: %s\n\nОтзыв можно оставить один раз от команды.", job.TeamName, job.GameName, link)
content := "<p class=\"email-text\"><strong>Как вам игра?</strong></p><p class=\"email-text\">Команда «" + html.EscapeString(job.TeamName) + "» — «" + html.EscapeString(job.GameName) + "»</p>"
content += email_sender.ButtonHTML(link, "Оставить отзыв")
content += "<p class=\"email-text\">Оценка и отзыв займут пару минут. Отзыв можно оставить один раз от команды.</p>"
return email_sender.Message{To: job.Email, Subject: "Поделитесь впечатлениями об игре", Body: body, HTML: email_sender.WrapHTML(content, strings.TrimRight(baseURL, "/"))}
}
func (s *Service) RunReviewReminderEmails(ctx context.Context, sender email_sender.IEmailSender) {
base, err := url.Parse(s.appBaseURL)
if err != nil || base.Host == "" || (base.Scheme != "https" && base.Scheme != "http") || base.User != nil || base.RawQuery != "" || base.Fragment != "" {
log.Print("review reminder email worker disabled: configure APP_BASE_URL")
return
}
ticker := time.NewTicker(time.Minute)
defer ticker.Stop()
for {
if err := s.sendReviewReminderEmails(ctx, sender); err != nil && ctx.Err() == nil {
log.Printf("review reminder email worker: %v", err)
}
select {
case <-ctx.Done():
return
case <-ticker.C:
}
}
}
func (s *Service) sendReviewReminderEmails(ctx context.Context, sender email_sender.IEmailSender) error {
for ctx.Err() == nil {
jobCtx, cancel := context.WithTimeout(ctx, time.Minute)
job, err := s.teams.ClaimReviewReminder(jobCtx)
if errors.Is(err, pgx.ErrNoRows) {
cancel()
return nil
}
if err != nil {
cancel()
return err
}
err = sender.Send(jobCtx, reviewReminderEmail(job, s.appBaseURL))
cancel()
saveCtx, saveCancel := context.WithTimeout(ctx, 10*time.Second)
saveErr := s.teams.FinishReviewReminder(saveCtx, job.TeamID, job.Attempts, err)
saveCancel()
if saveErr != nil {
return saveErr
}
}
return ctx.Err()
}
@@ -0,0 +1,20 @@
package game_review_service
import (
"strings"
"testing"
"evening_detective_server/internal/repos/teams_repo"
)
func TestReviewReminderEmail(t *testing.T) {
message := reviewReminderEmail(&teams_repo.ReviewReminder{
TeamID: 7, TeamName: "Шерлоки", GameName: "Ночь & тайна", Email: "captain@example.org", Password: "secret",
}, "https://app.example")
if message.To != "captain@example.org" || !strings.Contains(message.Body, "/team-story/7?password=secret&review=1") {
t.Fatalf("unexpected reminder email: %+v", message)
}
if !strings.Contains(message.HTML, "Ночь &amp; тайна") {
t.Fatalf("game name is not escaped in HTML: %s", message.HTML)
}
}