services.go 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. // Package repository
  2. package repository
  3. import (
  4. "context"
  5. "fmt"
  6. "github.com/jackc/pgx/v5"
  7. "github.com/jackc/pgx/v5/pgxpool"
  8. "gogs.fxtmmsk.ru/foxtime/photoplaces/backend/internal/models"
  9. )
  10. type ServiceRepo struct {
  11. pool *pgxpool.Pool
  12. }
  13. func NewServiceRepo(pool *pgxpool.Pool) *ServiceRepo {
  14. return &ServiceRepo{pool: pool}
  15. }
  16. func (r *ServiceRepo) Create(ctx context.Context, s *models.Service) error {
  17. err := r.pool.QueryRow(ctx,
  18. `INSERT INTO services (executor_id, title, description, price, currency, duration_minutes, status)
  19. VALUES ($1, $2, $3, $4, $5, $6, $7)
  20. RETURNING id, created_at, updated_at`,
  21. s.ExecutorID, s.Title, s.Description, s.Price, s.Currency, s.DurationMinutes, s.Status,
  22. ).Scan(&s.ID, &s.CreatedAt, &s.UpdatedAt)
  23. if err != nil {
  24. return fmt.Errorf("create service: %w", err)
  25. }
  26. if len(s.Tags) > 0 {
  27. if err := r.SetTags(ctx, s.ID, s.Tags); err != nil {
  28. return err
  29. }
  30. }
  31. return nil
  32. }
  33. func (r *ServiceRepo) GetByID(ctx context.Context, id string) (*models.Service, error) {
  34. row := r.pool.QueryRow(ctx,
  35. `SELECT id, executor_id, title, description, price, currency, duration_minutes,
  36. status, rating, reviews_count, created_at, updated_at, deleted_at
  37. FROM services WHERE id = $1 AND deleted_at IS NULL`, id)
  38. return scanService(row)
  39. }
  40. func (r *ServiceRepo) List(ctx context.Context, filter models.ServiceFilter) ([]*models.Service, error) {
  41. q := `SELECT s.id, s.executor_id, s.title, s.description, s.price, s.currency,
  42. s.duration_minutes, s.status, s.rating, s.reviews_count,
  43. s.created_at, s.updated_at, s.deleted_at
  44. FROM services s`
  45. args := pgx.NamedArgs{}
  46. joins := ""
  47. if len(filter.Tags) > 0 {
  48. joins += ` JOIN service_tags st ON st.service_id = s.id`
  49. }
  50. // Если есть JOIN-ы, добавляем DISTINCT, чтобы избежать дубликатов услуг при нескольких совпадениях
  51. if joins != "" {
  52. q = "SELECT DISTINCT" + q[6:] // заменяем "SELECT" на "SELECT DISTINCT"
  53. }
  54. q += joins + ` WHERE s.deleted_at IS NULL`
  55. if filter.Status != "" { q += ` AND s.status = @status`; args["status"] = filter.Status }
  56. if filter.MinRating > 0 { q += ` AND s.rating >= @min_rating`; args["min_rating"] = filter.MinRating }
  57. if filter.PriceMin != nil { q += ` AND s.price >= @price_min`; args["price_min"] = *filter.PriceMin }
  58. if filter.PriceMax != nil { q += ` AND s.price <= @price_max`; args["price_max"] = *filter.PriceMax }
  59. if len(filter.Tags) > 0 { q += ` AND st.tag_id = ANY(@tag_ids)`; args["tag_ids"] = filter.Tags }
  60. q += ` ORDER BY s.created_at DESC LIMIT @lim`
  61. args["lim"] = filter.Limit()
  62. rows, err := r.pool.Query(ctx, q, args)
  63. if err != nil {
  64. return nil, fmt.Errorf("list services: %w", err)
  65. }
  66. defer rows.Close()
  67. var services []*models.Service
  68. for rows.Next() {
  69. s, err := scanService(rows)
  70. if err != nil {
  71. return nil, err
  72. }
  73. services = append(services, s)
  74. }
  75. return services, nil
  76. }
  77. func (r *ServiceRepo) Update(ctx context.Context, s *models.Service) error {
  78. _, err := r.pool.Exec(ctx,
  79. `UPDATE services SET title=$1, description=$2, price=$3, currency=$4,
  80. duration_minutes=$5, status=$6, updated_at=now()
  81. WHERE id=$7 AND deleted_at IS NULL`,
  82. s.Title, s.Description, s.Price, s.Currency, s.DurationMinutes, s.Status, s.ID)
  83. return err
  84. }
  85. func (r *ServiceRepo) SoftDelete(ctx context.Context, id string) error {
  86. _, err := r.pool.Exec(ctx, `UPDATE services SET deleted_at=now() WHERE id=$1`, id)
  87. return err
  88. }
  89. func (r *ServiceRepo) SetTags(ctx context.Context, serviceID string, tags []models.Tag) error {
  90. tx, err := r.pool.Begin(ctx)
  91. if err != nil {
  92. return err
  93. }
  94. defer tx.Rollback(ctx)
  95. _, err = tx.Exec(ctx, `DELETE FROM service_tags WHERE service_id = $1`, serviceID)
  96. if err != nil {
  97. return err
  98. }
  99. for _, tag := range tags {
  100. _, err = tx.Exec(ctx,
  101. `INSERT INTO service_tags (service_id, tag_id) VALUES ($1, $2)`,
  102. serviceID, tag.ID)
  103. if err != nil {
  104. return err
  105. }
  106. }
  107. return tx.Commit(ctx)
  108. }
  109. func (r *ServiceRepo) GetTags(ctx context.Context, serviceID string) ([]models.Tag, error) {
  110. rows, err := r.pool.Query(ctx,
  111. `SELECT t.id, t.name, t.category FROM tags t
  112. JOIN service_tags st ON st.tag_id = t.id
  113. WHERE st.service_id = $1`, serviceID)
  114. if err != nil {
  115. return nil, err
  116. }
  117. defer rows.Close()
  118. var tags []models.Tag
  119. for rows.Next() {
  120. var t models.Tag
  121. if err := rows.Scan(&t.ID, &t.Name, &t.Category); err != nil {
  122. return nil, err
  123. }
  124. tags = append(tags, t)
  125. }
  126. return tags, nil
  127. }
  128. func scanService(row interface{ Scan(dest ...any) error }) (*models.Service, error) {
  129. var s models.Service
  130. err := row.Scan(
  131. &s.ID, &s.ExecutorID, &s.Title, &s.Description, &s.Price, &s.Currency,
  132. &s.DurationMinutes, &s.Status, &s.Rating, &s.ReviewsCount,
  133. &s.CreatedAt, &s.UpdatedAt, &s.DeletedAt,
  134. )
  135. if err != nil {
  136. if err == pgx.ErrNoRows {
  137. return nil, nil
  138. }
  139. return nil, err
  140. }
  141. return &s, nil
  142. }