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
+111
View File
@@ -0,0 +1,111 @@
package game_reviews_repo
import (
"context"
"errors"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
var (
ErrReviewNotFound = errors.New("Отзыв не найден")
ErrReviewExists = errors.New("Отзыв от команды уже оставлен")
)
type Review struct {
ID int
GameID int
TeamID int
TeamName string
Rating int32
Text string
Status string
CreatedAt time.Time
}
type Repo struct{ pool *pgxpool.Pool }
func New(pool *pgxpool.Pool) *Repo { return &Repo{pool: pool} }
func (r *Repo) Create(ctx context.Context, gameID, teamID int, rating 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 FROM teams t WHERE t.id = $2 AND t.game_id = $1
ON CONFLICT (game_id, team_id) DO NOTHING
RETURNING id, game_id, team_id, rating, text, status, created_at`, gameID, teamID, rating, text).Scan(
&review.ID, &review.GameID, &review.TeamID, &review.Rating, &review.Text, &review.Status, &review.CreatedAt)
if errors.Is(err, pgx.ErrNoRows) {
var exists bool
if err := r.pool.QueryRow(ctx, `SELECT EXISTS (SELECT 1 FROM game_reviews WHERE game_id=$1 AND team_id=$2)`, gameID, teamID).Scan(&exists); err != nil {
return nil, err
}
if exists {
return nil, ErrReviewExists
}
return nil, errors.New("Команда не найдена")
}
return review, err
}
func (r *Repo) GetByID(ctx context.Context, id int) (*Review, error) {
return r.queryOne(ctx, `WHERE r.id=$1`, id)
}
func (r *Repo) List(ctx context.Context, gameID int, all bool) ([]*Review, float64, int32, error) {
where := "r.game_id=$1 AND r.status='approved'"
if all {
where = "r.game_id=$1"
}
rows, err := r.pool.Query(ctx, `SELECT r.id,r.game_id,r.team_id,t.name,r.rating,r.text,r.status,r.created_at
FROM game_reviews r JOIN teams t ON t.id=r.team_id WHERE `+where+` ORDER BY r.created_at DESC`, gameID)
if err != nil {
return nil, 0, 0, err
}
defer rows.Close()
var reviews []*Review
for rows.Next() {
review := &Review{}
if err := rows.Scan(&review.ID, &review.GameID, &review.TeamID, &review.TeamName, &review.Rating, &review.Text, &review.Status, &review.CreatedAt); err != nil {
return nil, 0, 0, err
}
reviews = append(reviews, review)
}
if err := rows.Err(); err != nil {
return nil, 0, 0, err
}
var average float64
var count int64
if err := r.pool.QueryRow(ctx, `SELECT COALESCE(AVG(rating),0), COUNT(*) FROM game_reviews WHERE game_id=$1 AND status='approved'`, gameID).Scan(&average, &count); err != nil {
return nil, 0, 0, err
}
return reviews, average, int32(count), nil
}
func (r *Repo) Moderate(ctx context.Context, id int, status string) (*Review, error) {
review, err := r.queryOne(ctx, `WHERE r.id=$1`, id)
if err != nil {
return nil, err
}
if status == "approved" || status == "rejected" || status == "hidden" {
_, err = r.pool.Exec(ctx, `UPDATE game_reviews SET status=$2, moderated_at=NOW() WHERE id=$1`, id, status)
if err != nil {
return nil, err
}
return r.queryOne(ctx, `WHERE r.id=$1`, id)
}
return review, errors.New("Недопустимый статус модерации")
}
func (r *Repo) queryOne(ctx context.Context, condition string, id int) (*Review, error) {
review := &Review{}
err := r.pool.QueryRow(ctx, `SELECT r.id,r.game_id,r.team_id,t.name,r.rating,r.text,r.status,r.created_at
FROM game_reviews r JOIN teams t ON t.id=r.team_id `+condition, id).Scan(
&review.ID, &review.GameID, &review.TeamID, &review.TeamName, &review.Rating, &review.Text, &review.Status, &review.CreatedAt)
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrReviewNotFound
}
return review, err
}
@@ -16,6 +16,8 @@ var ErrParticipationFinal = errors.New("Решение уже принято и
const maxParticipationEmailAttempts = 8
const maxReviewReminderAttempts = 8
type Participation struct {
TeamID int
TeamName string
@@ -28,6 +30,16 @@ type Participation struct {
Attempts int
}
type ReviewReminder struct {
TeamID int
TeamName string
GameID int
GameName string
Email string
Password string
Attempts int
}
func tokenHash(token string) ([]byte, error) {
if len(token) != 64 {
return nil, ErrParticipationLink
@@ -134,3 +146,33 @@ func (r *TeamsRepo) FinishParticipationEmail(ctx context.Context, id, attempt in
WHERE id=$1 AND participation_attempts=$2`, id, attempt, message)
return err
}
func (r *TeamsRepo) ClaimReviewReminder(ctx context.Context) (*ReviewReminder, error) {
job := &ReviewReminder{}
err := r.pool.QueryRow(ctx, `WITH candidate AS (
SELECT t.id FROM teams t JOIN games g ON g.id=t.game_id
WHERE t.captain_email<>'' AND t.review_reminder_sent_at IS NULL
AND t.review_reminder_attempts < $1
AND (t.review_reminder_next_attempt_at IS NULL OR t.review_reminder_next_attempt_at <= NOW())
AND g.status='finish' AND g.started_at <= NOW() - INTERVAL '16 hours'
AND NOT EXISTS (SELECT 1 FROM game_reviews r WHERE r.game_id=t.game_id AND r.team_id=t.id)
ORDER BY g.started_at, t.id
FOR UPDATE OF t SKIP LOCKED LIMIT 1
) UPDATE teams t SET review_reminder_next_attempt_at=NOW()+INTERVAL '5 minutes',
review_reminder_attempts=t.review_reminder_attempts+1
FROM candidate c, games g WHERE t.id=c.id AND g.id=t.game_id
RETURNING t.id,t.name,t.game_id,g.name,t.captain_email,t.password,t.review_reminder_attempts`, maxReviewReminderAttempts).Scan(
&job.TeamID, &job.TeamName, &job.GameID, &job.GameName, &job.Email, &job.Password, &job.Attempts)
return job, err
}
func (r *TeamsRepo) FinishReviewReminder(ctx context.Context, id, attempt int, sendErr error) error {
message := ""
if sendErr != nil {
message = sendErr.Error()
}
_, err := r.pool.Exec(ctx, `UPDATE teams SET review_reminder_error=$3,
review_reminder_sent_at=CASE WHEN $3='' THEN NOW() ELSE review_reminder_sent_at END
WHERE id=$1 AND review_reminder_attempts=$2`, id, attempt, message)
return err
}