Bladeren bron

fix: PATCH /places/{id} теперь обновляет теги, фичи и обложку

- Добавлены поля Tags и Features в UpdatePlaceInput (services/places.go)
- Update() вызывает SetTags/SetFeatures при наличии данных
- Хендлер копирует CoverImage, Tags, Features из запроса в input
- nil vs [] позволяет различать 'не отправлено' и 'очистить'
- Obsidian: atomic-note + ADR о баге и решении
neyrogovnarik 1 maand geleden
bovenliggende
commit
a62e2ac9c2

+ 16 - 0
backend/internal/handlers/places.go

@@ -289,11 +289,27 @@ func (h *PlaceHandler) Update(w http.ResponseWriter, r *http.Request) {
 	input.Address = req.Address
 	input.Lat = req.Lat
 	input.Lng = req.Lng
+	if req.CoverImage != nil { input.CoverImage = req.CoverImage }
 	input.AccessInfo = req.AccessInfo
 	input.HourlyRate = req.HourlyRate
 	input.Currency = req.Currency
 	input.MinHours = req.MinHours
 
+	if req.Tags != nil {
+		tags := make([]models.Tag, len(req.Tags))
+		for i, t := range req.Tags {
+			tags[i] = models.Tag{ID: t}
+		}
+		input.Tags = tags
+	}
+	if req.Features != nil {
+		features := make([]models.Feature, len(req.Features))
+		for i, f := range req.Features {
+			features[i] = models.Feature{ID: f}
+		}
+		input.Features = features
+	}
+
 	place, err := h.placeSvc.Update(r.Context(), input, isModerator)
 	if err != nil {
 		writeError(w, http.StatusInternalServerError, "failed to update place", err)

+ 15 - 0
backend/internal/services/places.go

@@ -204,6 +204,8 @@ type UpdatePlaceInput struct {
 	HourlyRate *int
 	Currency   *string
 	MinHours   *int
+	Tags       []models.Tag
+	Features   []models.Feature
 }
 
 func (s *PlaceService) Update(ctx context.Context, input UpdatePlaceInput, isModerator bool) (*models.Place, error) {
@@ -238,6 +240,19 @@ func (s *PlaceService) Update(ctx context.Context, input UpdatePlaceInput, isMod
 		return nil, err
 	}
 
+	if input.Tags != nil {
+		if err := s.placeRepo.SetTags(ctx, place.ID, input.Tags); err != nil {
+			return nil, err
+		}
+		place.Tags = input.Tags
+	}
+	if input.Features != nil {
+		if err := s.placeRepo.SetFeatures(ctx, place.ID, input.Features); err != nil {
+			return nil, err
+		}
+		place.Features = input.Features
+	}
+
 	return place, nil
 }
 

+ 2 - 0
obsidian_data/Photoplaces_data/MOC-backend-patterns.md

@@ -32,6 +32,7 @@ graph TB
 | CSP nonce hardening | `Caddyfile` | [[atomic-csp-hardening]] |
 | Package-level mutable state (anti-pattern) | `handlers/errors.go:13` | [[atomic-global-appenv-package-var]] |
 | TOCTOU race (anti-pattern) | `services/auth.go:85-86` | [[atomic-toctou-race-registration]] |
+| PATCH tags/features fix | `handlers/places.go`, `services/places.go` | [[decision-patch-tags-features-fix]] |
 
 ### Error handling chain
 
@@ -80,6 +81,7 @@ sequenceDiagram
 | Mutex без defer | `middleware/ratelimit.go:61` | [[atomic-mutex-defer-panic]] |
 | N+1 запросы | `services/places.go:112-136` | [[atomic-n-plus-one-getbyid]] |
 | Silenced errors | `PlaceForm.tsx:42-43` | [[atomic-error-swallowing-frontend]] |
+| **PATCH не обновляет теги/фичи** | `handlers/places.go:263-308`, `services/places.go:194-242` | [[atomic-patch-place-tags-features]] |
 
 ### Связанные заметки
 

+ 78 - 0
obsidian_data/Photoplaces_data/atomic-patch-place-tags-features.md

@@ -0,0 +1,78 @@
+# PATCH /places/{id} — теги и характеристики не обновлялись
+
+**Контекст**: При редактировании места через `PATCH /places/{id}` поля `tags` и `features` игнорировались. Фронтенд их отправлял, хендлер парсил, но никогда не передавал в сервис.
+
+## Проблема
+
+Две точки отказа:
+
+### 1. Хендлер (`handlers/places.go:282-296`)
+
+`updatePlaceRequest` содержал `Tags []string` и `Features []string`, но метод `Update()` копировал только: Title, Description, Address, Lat, Lng, AccessInfo, HourlyRate, Currency, MinHours.
+
+**CoverImage** тоже не копировался — был в обоих структурах, но не завайрены.
+
+```go
+// Было — Tags, Features, CoverImage не копировались
+if req.Title != nil { input.Title = req.Title }
+// ... остальные поля ...
+// CoverImage, Tags, Features — потеряны
+```
+
+### 2. Сервис (`services/places.go:194-207`)
+
+`UpdatePlaceInput` не имел полей `Tags` и `Features`. Метод `Update()` не вызывал `placeRepo.SetTags()` / `placeRepo.SetFeatures()`.
+
+## Исправление
+
+### Хендлер — копирование CoverImage, Tags, Features
+
+```go
+if req.CoverImage != nil { input.CoverImage = req.CoverImage }
+
+if req.Tags != nil {
+    tags := make([]models.Tag, len(req.Tags))
+    for i, t := range req.Tags {
+        tags[i] = models.Tag{ID: t}
+    }
+    input.Tags = tags
+}
+if req.Features != nil {
+    features := make([]models.Feature, len(req.Features))
+    for i, f := range req.Features {
+        features[i] = models.Feature{ID: f}
+    }
+    input.Features = features
+}
+```
+
+### Сервис — SetTags/SetFeatures при наличии данных
+
+```go
+if input.Tags != nil {
+    if err := s.placeRepo.SetTags(ctx, place.ID, input.Tags); err != nil {
+        return nil, err
+    }
+    place.Tags = input.Tags
+}
+// Аналогично для Features
+```
+
+`SetTags` и `SetFeatures` уже были реализованы в `repository/places.go` — используют транзакцию: DELETE всех старых + INSERT новых. Проблема была только в отсутствии вызова.
+
+## Ключевой инсайт
+
+Использование `nil` vs `[]string{}` в JSON-десериализации позволило различать "поле не отправлено" (не обновляем) и "отправлен пустой массив" (очищаем теги/фичи). Для PATCH-семантики это критично.
+
+## Связанные заметки
+
+- [[decision-patch-tags-features-fix]] — ADR по выбору подхода
+- [[MOC-backend-patterns]] — антипаттерны
+
+## Источник
+
+Code review PhotoPlaces 2026-06-29. `handlers/places.go`, `services/places.go`.
+
+## Теги
+
+#bug #golang #api #patch #critical

+ 44 - 0
obsidian_data/Photoplaces_data/decision-patch-tags-features-fix.md

@@ -0,0 +1,44 @@
+# Decision: Исправление PATCH /places/{id} — обновление тегов и характеристик
+
+**Контекст**: При PATCH-редактировании места теги и характеристики не обновлялись. Баг обнаружен при code review проекта.
+
+## Проблема
+
+`UpdatePlaceInput` не содержал `Tags`/`Features`, и хендлер не передавал их в сервис. Репозиторий уже имел методы `SetTags` и `SetFeatures` с транзакционной заменой (DELETE + INSERT), но они никем не вызывались при обновлении.
+
+## Рассмотренные варианты
+
+| Вариант | Плюсы | Минусы |
+|---------|-------|--------|
+| **A. Добавить Tags/Features в UpdatePlaceInput (выбран)** | Минимальные изменения, переиспользование существующих методов репозитория | Нужно конвертировать `[]string → []models.Tag` в хендлере |
+| **B. Отдельный эндпоинт PATCH /places/{id}/tags** | Чистое разделение ответственности | Лишний round-trip, усложнение API, дополнительная валидация |
+| **C. Замена всего place (PUT)** | Проще семантика | Нарушение REST-best-practices (PATCH — частичное обновление), больше данных на фронтенде |
+
+## Выбор: A — расширить UpdatePlaceInput
+
+**Причина**: Вариант B требует нового маршрута, тестов, документации. Вариант C ломает идемпотентность. А — минимальное изменение в существующем потоке.
+
+### Детали реализации
+
+1. `UpdatePlaceInput` получает `Tags []models.Tag` и `Features []models.Feature`
+2. В `Update()` после `placeRepo.Update()` вызываем `SetTags`/`SetFeatures` только если `input.Tags != nil` / `input.Features != nil`
+3. В хендлере конвертируем `[]string` → `[]models.Tag{ID: t}` (аналогично Create)
+
+### Семантика PATCH
+
+Используем `nil`-проверку для различения:
+- Поле не отправлено → не трогаем (`nil`)
+- Отправлен пустой массив → очищаем (`[]string{}`)
+
+## Связанные заметки
+
+- [[atomic-patch-place-tags-features]] — детальное описание бага и фикса
+- [[MOC-backend-patterns]]
+
+## Источник
+
+Code review 2026-06-29. Hotfix после аудита проекта.
+
+## Теги
+
+#architecture #decision #golang #api #patch