services.go 6.0 KB

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