add diffectly

This commit is contained in:
2026-10-03 14:52:40 +07:00
parent 4631c443e0
commit 7d4361a315
13 changed files with 200 additions and 103 deletions
+11 -10
View File
@@ -13,15 +13,16 @@ import (
func mapGameReview(r *game_review_service.Review) *proto.GameReview {
return &proto.GameReview{
ID: int32(r.ID),
GameID: int32(r.GameID),
TeamID: int32(r.TeamID),
TeamName: r.TeamName,
Rating: r.Rating,
Text: r.Text,
Status: r.Status,
CreatedAt: timestamppb.New(r.CreatedAt),
GameName: r.GameName,
ID: int32(r.ID),
GameID: int32(r.GameID),
TeamID: int32(r.TeamID),
TeamName: r.TeamName,
Rating: r.Rating,
Difficulty: r.Difficulty,
Text: r.Text,
Status: r.Status,
CreatedAt: timestamppb.New(r.CreatedAt),
GameName: r.GameName,
}
}
@@ -34,7 +35,7 @@ func mapGameReviews(reviews []*game_review_service.Review) []*proto.GameReview {
}
func (s *server) CreateGameReview(ctx context.Context, req *proto.CreateGameReviewReq) (*proto.CreateGameReviewRsp, error) {
review, err := s.reviewService.Create(ctx, int(req.GameID), int(req.TeamID), req.Password, req.Rating, req.Text)
review, err := s.reviewService.Create(ctx, int(req.GameID), int(req.TeamID), req.Password, req.Rating, req.Difficulty, req.Text)
if err != nil {
return nil, reviewErrorStatus(err, "не удалось создать отзыв")
}
+2 -1
View File
@@ -97,12 +97,13 @@ func (s *server) GetScenario(ctx context.Context, req *proto.GetScenarioReq) (*p
func (s *server) addScenarioRatings(ctx context.Context, scenarios []*scenarios_service.Scenario) error {
for _, scenario := range scenarios {
average, count, err := s.reviewService.ScenarioRating(ctx, scenario.ID)
average, count, difficulty, err := s.reviewService.ScenarioRating(ctx, scenario.ID)
if err != nil {
return err
}
scenario.AverageRating = average
scenario.ReviewsCount = count
scenario.Difficulty = difficulty
}
return nil
}
+1
View File
@@ -40,6 +40,7 @@ func mapScenario(o *scenarios_service.Scenario) *proto.Scenario {
PublishedAt: publishedAt,
AverageRating: o.AverageRating,
ReviewsCount: o.ReviewsCount,
Difficulty: o.Difficulty,
}
}
+29 -26
View File
@@ -23,15 +23,16 @@ var (
// Review describes a game review.
type Review struct {
ID int
GameID int
GameName string
TeamID int
TeamName string
Rating int32
Text string
Status string
CreatedAt time.Time
ID int
GameID int
GameName string
TeamID int
TeamName string
Rating int32
Difficulty int32
Text string
Status string
CreatedAt time.Time
}
// Repo persists game reviews.
@@ -41,13 +42,13 @@ type Repo struct{ pool *pgxpool.Pool }
func NewGameReviewsRepo(pool *pgxpool.Pool) *Repo { return &Repo{pool: pool} }
// Create stores a review.
func (r *Repo) Create(ctx context.Context, gameID, teamID int, rating int32, text string) (*Review, error) {
func (r *Repo) Create(ctx context.Context, gameID, teamID int, rating, difficulty int32, text string) (*Review, error) {
review := &Review{}
err := r.pool.QueryRow(
ctx,
`
INSERT INTO game_reviews (game_id, team_id, rating, text)
SELECT $1, t.id, $3, $4
INSERT INTO game_reviews (game_id, team_id, rating, difficulty, text)
SELECT $1, t.id, $3, $4, $5
FROM teams t
WHERE t.id = $2 AND t.game_id = $1
ON CONFLICT (game_id, team_id) DO NOTHING
@@ -55,9 +56,10 @@ func (r *Repo) Create(ctx context.Context, gameID, teamID int, rating int32, tex
gameID,
teamID,
rating,
difficulty,
text,
).Scan(
&review.ID, &review.GameID, &review.TeamID, &review.Rating, &review.Text, &review.Status, &review.CreatedAt)
&review.ID, &review.GameID, &review.TeamID, &review.Rating, &review.Difficulty, &review.Text, &review.Status, &review.CreatedAt)
if errors.Is(err, pgx.ErrNoRows) {
var exists bool
if err := r.pool.QueryRow(
@@ -89,7 +91,7 @@ func (r *Repo) GetByID(ctx context.Context, id int) (*Review, error) {
func (r *Repo) List(ctx context.Context, gameID int, all bool) ([]*Review, float64, int32, error) {
rows, err := r.pool.Query(
ctx,
`SELECT r.id, r.game_id, r.team_id, t.name, g.name, r.rating, r.text, r.status, r.created_at
`SELECT r.id, r.game_id, r.team_id, t.name, g.name, r.rating, r.difficulty, r.text, r.status, r.created_at
FROM game_reviews r
JOIN teams t ON t.id = r.team_id
JOIN games g ON g.id = r.game_id
@@ -105,7 +107,7 @@ func (r *Repo) List(ctx context.Context, gameID int, all bool) ([]*Review, float
var reviews []*Review
for rows.Next() {
review := &Review{}
if err := rows.Scan(&review.ID, &review.GameID, &review.TeamID, &review.TeamName, &review.GameName, &review.Rating, &review.Text, &review.Status, &review.CreatedAt); err != nil {
if err := rows.Scan(&review.ID, &review.GameID, &review.TeamID, &review.TeamName, &review.GameName, &review.Rating, &review.Difficulty, &review.Text, &review.Status, &review.CreatedAt); err != nil {
return nil, 0, 0, err
}
reviews = append(reviews, review)
@@ -131,7 +133,7 @@ func (r *Repo) List(ctx context.Context, gameID int, all bool) ([]*Review, float
func (r *Repo) ListForModeration(ctx context.Context) ([]*Review, error) {
rows, err := r.pool.Query(
ctx,
`SELECT r.id, r.game_id, r.team_id, t.name, g.name, r.rating, r.text, r.status, r.created_at
`SELECT r.id, r.game_id, r.team_id, t.name, g.name, r.rating, r.difficulty, r.text, r.status, r.created_at
FROM game_reviews r
JOIN teams t ON t.id = r.team_id
JOIN games g ON g.id = r.game_id
@@ -144,7 +146,7 @@ func (r *Repo) ListForModeration(ctx context.Context) ([]*Review, error) {
var reviews []*Review
for rows.Next() {
review := &Review{}
if err := rows.Scan(&review.ID, &review.GameID, &review.TeamID, &review.TeamName, &review.GameName, &review.Rating, &review.Text, &review.Status, &review.CreatedAt); err != nil {
if err := rows.Scan(&review.ID, &review.GameID, &review.TeamID, &review.TeamName, &review.GameName, &review.Rating, &review.Difficulty, &review.Text, &review.Status, &review.CreatedAt); err != nil {
return nil, err
}
reviews = append(reviews, review)
@@ -175,25 +177,26 @@ func (r *Repo) Moderate(ctx context.Context, id int, status string) (*Review, er
}
// ScenarioRating returns aggregate ratings for a scenario.
func (r *Repo) ScenarioRating(ctx context.Context, scenarioID int) (float64, int32, error) {
func (r *Repo) ScenarioRating(ctx context.Context, scenarioID int) (float64, int32, float64, error) {
var average float64
var averageDifficulty float64
var count int64
err := r.pool.QueryRow(
ctx,
`SELECT COALESCE(AVG(r.rating), 0), COUNT(*)
`SELECT COALESCE(AVG(r.rating), 0), COUNT(*), COALESCE(AVG(r.difficulty), 0)
FROM game_reviews r
JOIN games g ON g.id = r.game_id
WHERE g.scenario_id = $1 AND g.status = 'finish' AND r.status = 'approved'`,
scenarioID,
).Scan(&average, &count)
return average, int32(count), err
).Scan(&average, &count, &averageDifficulty)
return average, int32(count), averageDifficulty, err
}
// ListByScenario returns reviews and aggregate ratings for a scenario.
func (r *Repo) ListByScenario(ctx context.Context, scenarioID int) ([]*Review, float64, int32, error) {
rows, err := r.pool.Query(
ctx,
`SELECT r.id, r.game_id, r.team_id, t.name, g.name, r.rating, r.text, r.status, r.created_at
`SELECT r.id, r.game_id, r.team_id, t.name, g.name, r.rating, r.difficulty, r.text, r.status, r.created_at
FROM game_reviews r
JOIN teams t ON t.id = r.team_id
JOIN games g ON g.id = r.game_id
@@ -208,7 +211,7 @@ func (r *Repo) ListByScenario(ctx context.Context, scenarioID int) ([]*Review, f
var reviews []*Review
for rows.Next() {
review := &Review{}
if err := rows.Scan(&review.ID, &review.GameID, &review.TeamID, &review.TeamName, &review.GameName, &review.Rating, &review.Text, &review.Status, &review.CreatedAt); err != nil {
if err := rows.Scan(&review.ID, &review.GameID, &review.TeamID, &review.TeamName, &review.GameName, &review.Rating, &review.Difficulty, &review.Text, &review.Status, &review.CreatedAt); err != nil {
return nil, 0, 0, err
}
reviews = append(reviews, review)
@@ -216,7 +219,7 @@ func (r *Repo) ListByScenario(ctx context.Context, scenarioID int) ([]*Review, f
if err := rows.Err(); err != nil {
return nil, 0, 0, err
}
average, count, err := r.ScenarioRating(ctx, scenarioID)
average, count, _, err := r.ScenarioRating(ctx, scenarioID)
return reviews, average, count, err
}
@@ -224,14 +227,14 @@ func (r *Repo) queryOne(ctx context.Context, id int) (*Review, error) {
review := &Review{}
err := r.pool.QueryRow(
ctx,
`SELECT r.id, r.game_id, r.team_id, t.name, g.name, r.rating, r.text, r.status, r.created_at
`SELECT r.id, r.game_id, r.team_id, t.name, g.name, r.rating, r.difficulty, r.text, r.status, r.created_at
FROM game_reviews r
JOIN teams t ON t.id = r.team_id
JOIN games g ON g.id = r.game_id
WHERE r.id = $1`,
id,
).Scan(
&review.ID, &review.GameID, &review.TeamID, &review.TeamName, &review.GameName, &review.Rating, &review.Text, &review.Status, &review.CreatedAt)
&review.ID, &review.GameID, &review.TeamID, &review.TeamName, &review.GameName, &review.Rating, &review.Difficulty, &review.Text, &review.Status, &review.CreatedAt)
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrReviewNotFound
}
+20 -18
View File
@@ -9,15 +9,16 @@ import (
// Review describes a game review exposed by the service layer.
type Review struct {
ID int
GameID int
GameName string
TeamID int
TeamName string
Rating int32
Text string
Status string
CreatedAt time.Time
ID int
GameID int
GameName string
TeamID int
TeamName string
Rating int32
Difficulty int32
Text string
Status string
CreatedAt time.Time
}
func mapReview(review *game_reviews_repo.Review) *Review {
@@ -25,15 +26,16 @@ func mapReview(review *game_reviews_repo.Review) *Review {
return nil
}
return &Review{
ID: review.ID,
GameID: review.GameID,
GameName: review.GameName,
TeamID: review.TeamID,
TeamName: review.TeamName,
Rating: review.Rating,
Text: review.Text,
Status: review.Status,
CreatedAt: review.CreatedAt,
ID: review.ID,
GameID: review.GameID,
GameName: review.GameName,
TeamID: review.TeamID,
TeamName: review.TeamName,
Rating: review.Rating,
Difficulty: review.Difficulty,
Text: review.Text,
Status: review.Status,
CreatedAt: review.CreatedAt,
}
}
@@ -31,7 +31,7 @@ var (
// ErrGameNotFinished indicates that the game is not finished.
ErrGameNotFinished = errors.New("оценить можно только завершённую игру")
// ErrInvalidReview indicates that review data is invalid.
ErrInvalidReview = errors.New("оценка должна быть от 1 до 5, текст обязателен")
ErrInvalidReview = errors.New("оценка должна быть от 1 до 5, сложность от 1 до 3, текст обязателен")
)
// GameReviewService handles game reviews.
@@ -49,8 +49,8 @@ func NewGameReviewService(reviews *game_reviews_repo.Repo, games *games_repo.Gam
}
// Create stores a game review.
func (s *GameReviewService) Create(ctx context.Context, gameID, teamID int, password string, rating int32, text string) (*Review, error) {
if rating < 1 || rating > 5 || strings.TrimSpace(text) == "" {
func (s *GameReviewService) Create(ctx context.Context, gameID, teamID int, password string, rating, difficulty int32, text string) (*Review, error) {
if rating < 1 || rating > 5 || difficulty < 1 || difficulty > 3 || strings.TrimSpace(text) == "" {
return nil, ErrInvalidReview
}
team, err := s.teams.GetTeamByID(ctx, teamID)
@@ -73,7 +73,7 @@ func (s *GameReviewService) Create(ctx context.Context, gameID, teamID int, pass
if game.Status != "finish" {
return nil, ErrGameNotFinished
}
review, err := s.reviews.Create(ctx, gameID, teamID, rating, strings.TrimSpace(text))
review, err := s.reviews.Create(ctx, gameID, teamID, rating, difficulty, strings.TrimSpace(text))
if err != nil {
if errors.Is(err, game_reviews_repo.ErrReviewExists) {
return nil, ErrReviewExists
@@ -117,8 +117,25 @@ func (s *GameReviewService) Moderate(ctx context.Context, id int, status string)
}
// ScenarioRating returns aggregate ratings for a scenario.
func (s *GameReviewService) ScenarioRating(ctx context.Context, scenarioID int) (float64, int32, error) {
return s.reviews.ScenarioRating(ctx, scenarioID)
func (s *GameReviewService) ScenarioRating(ctx context.Context, scenarioID int) (float64, int32, string, error) {
average, count, averageDifficulty, err := s.reviews.ScenarioRating(ctx, scenarioID)
if err != nil {
return 0, 0, "", err
}
return average, count, difficultyCategory(averageDifficulty, count), nil
}
func difficultyCategory(average float64, count int32) string {
if count == 0 {
return ""
}
if average <= 1.66 {
return "лёгкая"
}
if average <= 2.33 {
return "средняя"
}
return "тяжёлая"
}
// ListByScenario returns reviews for a scenario.
@@ -33,3 +33,20 @@ func TestReviewReminderEmail(t *testing.T) {
})
}
}
func TestDifficultyCategory(t *testing.T) {
for _, test := range []struct {
average float64
count int32
want string
}{
{average: 0, count: 0, want: ""},
{average: 1.66, count: 1, want: "лёгкая"},
{average: 2, count: 1, want: "средняя"},
{average: 2.34, count: 1, want: "тяжёлая"},
} {
if got := difficultyCategory(test.average, test.count); got != test.want {
t.Errorf("difficultyCategory(%v, %d) = %q, want %q", test.average, test.count, got, test.want)
}
}
}
@@ -21,5 +21,6 @@ type Scenario struct {
PublishedAt *time.Time
AverageRating float64
ReviewsCount int32
Difficulty string
IsDeleted bool
}