buhapp-backend/internal/reviews/repo.go
ga 1eb7af6bac Sprint 5: reviews and ratings
- POST /api/v1/reviews (one per chat_id, UNIQUE)
- GET  /api/v1/users/:id/reviews (public list)
- GET  /api/v1/users/:id/stats (rating_avg, count)
- migration 0004: reviews (rating 1-5, anonymous flag), user_stats aggregate
- only chat participants can review; reviewed_id is other party
2026-08-20 16:20:15 +00:00

152 lines
3.9 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 reviews
import (
"context"
"errors"
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
type Review struct {
ID uuid.UUID
ReviewerID uuid.UUID
ReviewedID uuid.UUID
ChatID uuid.UUID
Rating int
Body string
Anonymous bool
CreatedAt time.Time
}
type Stats struct {
UserID uuid.UUID
RatingAvg float64
RatingCount int
ReviewsLeft int
LastActiveAt *time.Time
}
type Repo struct {
pool *pgxpool.Pool
}
func NewRepo(pool *pgxpool.Pool) *Repo {
return &Repo{pool: pool}
}
// Create — создать отзыв. Один отзыв на chat_id (UNIQUE).
// Также обновит агрегаты в user_stats.
func (r *Repo) Create(ctx context.Context, rev *Review) error {
tx, err := r.pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
var exists bool
err = tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM chats WHERE id=$1 AND (user_a=$2 OR user_b=$2))`,
rev.ChatID, rev.ReviewerID,
).Scan(&exists)
if err != nil {
return err
}
if !exists {
return errors.New("chat not found or not a participant")
}
var reviewer, reviewed *string
err = tx.QueryRow(ctx, `
INSERT INTO reviews (reviewer_id, reviewed_id, chat_id, rating, body, anonymous)
VALUES ($1, $2, $3, $4, NULLIF($5, ''), $6)
RETURNING id, created_at, reviewer_id::text, reviewed_id::text`,
rev.ReviewerID, rev.ReviewedID, rev.ChatID, rev.Rating, rev.Body, rev.Anonymous,
).Scan(&rev.ID, &rev.CreatedAt, &reviewer, &reviewed)
if err != nil {
return err
}
// upsert stats для reviewed
_, err = tx.Exec(ctx, `
INSERT INTO user_stats (user_id, rating_avg, rating_count, last_active_at)
VALUES ($1, $2, 1, NOW())
ON CONFLICT (user_id) DO UPDATE
SET rating_avg = ((user_stats.rating_avg * user_stats.rating_count) + EXCLUDED.rating_avg) / (user_stats.rating_count + 1),
rating_count = user_stats.rating_count + 1,
last_active_at = NOW()`,
rev.ReviewedID, float64(rev.Rating),
)
if err != nil {
return err
}
// bump reviews_left для reviewer
_, err = tx.Exec(ctx, `
INSERT INTO user_stats (user_id, reviews_left, last_active_at)
VALUES ($1, 1, NOW())
ON CONFLICT (user_id) DO UPDATE
SET reviews_left = user_stats.reviews_left + 1,
last_active_at = NOW()`,
rev.ReviewerID,
)
if err != nil {
return err
}
return tx.Commit(ctx)
}
// ListForUser — публичные отзывы о пользователе
func (r *Repo) ListForUser(ctx context.Context, userID uuid.UUID, limit, offset int) ([]Review, error) {
if limit <= 0 || limit > 100 {
limit = 20
}
rows, err := r.pool.Query(ctx, `
SELECT id, reviewer_id, reviewed_id, chat_id, rating,
COALESCE(body, ''), anonymous, created_at
FROM reviews
WHERE reviewed_id=$1
ORDER BY created_at DESC
LIMIT $2 OFFSET $3`, userID, limit, offset)
if err != nil {
return nil, err
}
defer rows.Close()
var out []Review
for rows.Next() {
var x Review
if err := rows.Scan(&x.ID, &x.ReviewerID, &x.ReviewedID, &x.ChatID, &x.Rating,
&x.Body, &x.Anonymous, &x.CreatedAt); err != nil {
return nil, err
}
out = append(out, x)
}
return out, rows.Err()
}
func (r *Repo) GetStats(ctx context.Context, userID uuid.UUID) (*Stats, error) {
s := &Stats{UserID: userID}
err := r.pool.QueryRow(ctx, `
SELECT rating_avg, rating_count, reviews_left, last_active_at
FROM user_stats WHERE user_id=$1`, userID,
).Scan(&s.RatingAvg, &s.RatingCount, &s.ReviewsLeft, &s.LastActiveAt)
if errors.Is(err, pgx.ErrNoRows) {
return s, nil
}
if err != nil {
return nil, err
}
return s, nil
}
// HasReviewed — оставлял ли reviewer уже отзыв на этот чат
func (r *Repo) HasReviewed(ctx context.Context, chatID, reviewerID uuid.UUID) (bool, error) {
var ok bool
err := r.pool.QueryRow(ctx,
`SELECT EXISTS(SELECT 1 FROM reviews WHERE chat_id=$1 AND reviewer_id=$2)`,
chatID, reviewerID,
).Scan(&ok)
return ok, err
}