| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254 |
- package services
- import (
- "context"
- "encoding/base64"
- "errors"
- "fmt"
- "strconv"
- "strings"
- "time"
- "gogs.fxtmmsk.ru/foxtime/photoplaces/backend/internal/models"
- )
- func encodeCursor(p *models.Place) string {
- s := p.ID + "|" + strconv.FormatInt(p.CreatedAt.UnixNano(), 10) + "|" + fmt.Sprintf("%.6f", p.Rating)
- return base64.RawURLEncoding.EncodeToString([]byte(s))
- }
- func DecodeCursor(cursor string) (id string, createdAt time.Time, rating float64, err error) {
- b, err := base64.RawURLEncoding.DecodeString(cursor)
- if err != nil {
- return "", time.Time{}, 0, fmt.Errorf("decode cursor: %w", err)
- }
- parts := strings.SplitN(string(b), "|", 3)
- if len(parts) < 2 {
- return "", time.Time{}, 0, fmt.Errorf("invalid cursor format")
- }
- id = parts[0]
- createdAtUnix, err := strconv.ParseInt(parts[1], 10, 64)
- if err != nil {
- return "", time.Time{}, 0, fmt.Errorf("parse cursor created_at: %w", err)
- }
- createdAt = time.Unix(0, createdAtUnix)
- if len(parts) > 2 {
- rating, _ = strconv.ParseFloat(parts[2], 64)
- }
- return
- }
- var (
- ErrNotYourPlace = errors.New("not your place")
- ErrPlaceNotFound = errors.New("place not found")
- )
- type PlaceRepo interface {
- Create(ctx context.Context, place *models.Place) error
- GetByID(ctx context.Context, id string) (*models.Place, error)
- List(ctx context.Context, filter models.PlaceFilter) ([]*models.Place, error)
- Update(ctx context.Context, place *models.Place) error
- UpdateStatus(ctx context.Context, id, status, comment string) error
- SoftDelete(ctx context.Context, id string) error
- SetTags(ctx context.Context, placeID string, tags []models.Tag) error
- SetFeatures(ctx context.Context, placeID string, features []models.Feature) error
- GetTags(ctx context.Context, placeID string) ([]models.Tag, error)
- GetFeatures(ctx context.Context, placeID string) ([]models.Feature, error)
- GetTagsBatch(ctx context.Context, placeIDs []string) (map[string][]models.Tag, error)
- GetFeaturesBatch(ctx context.Context, placeIDs []string) (map[string][]models.Feature, error)
- }
- type PlaceService struct {
- placeRepo PlaceRepo
- }
- func NewPlaceService(placeRepo PlaceRepo) *PlaceService {
- return &PlaceService{placeRepo: placeRepo}
- }
- type CreatePlaceInput struct {
- OwnerID string
- Type string
- Title string
- Description *string
- Address *string
- Lat float64
- Lng float64
- CoverImage *string
- AccessInfo *string
- HourlyRate *int
- Currency string
- MinHours int
- Tags []models.Tag
- Features []models.Feature
- }
- func (s *PlaceService) Create(ctx context.Context, input CreatePlaceInput) (*models.Place, error) {
- place := &models.Place{
- Type: input.Type,
- OwnerID: input.OwnerID,
- Title: input.Title,
- Description: input.Description,
- Address: input.Address,
- Lat: input.Lat,
- Lng: input.Lng,
- CoverImage: input.CoverImage,
- AccessInfo: input.AccessInfo,
- Status: "pending_moderation",
- HourlyRate: input.HourlyRate,
- Currency: input.Currency,
- MinHours: input.MinHours,
- Tags: input.Tags,
- Features: input.Features,
- }
- if err := s.placeRepo.Create(ctx, place); err != nil {
- return nil, fmt.Errorf("create place: %w", err)
- }
- return place, nil
- }
- func (s *PlaceService) GetByID(ctx context.Context, id string, fetchTags bool) (*models.Place, error) {
- place, err := s.placeRepo.GetByID(ctx, id)
- if err != nil {
- return nil, err
- }
- if place == nil {
- return nil, nil
- }
- if fetchTags {
- tags, err := s.placeRepo.GetTags(ctx, id)
- if err != nil {
- return nil, err
- }
- place.Tags = tags
- features, err := s.placeRepo.GetFeatures(ctx, id)
- if err != nil {
- return nil, err
- }
- place.Features = features
- }
- return place, nil
- }
- func (s *PlaceService) List(ctx context.Context, filter models.PlaceFilter) (*models.PaginatedPlaces, error) {
- places, err := s.placeRepo.List(ctx, filter)
- if err != nil {
- return nil, err
- }
- if filter.IncludeTagsFeatures && len(places) > 0 {
- placeIDs := make([]string, len(places))
- for i, p := range places {
- placeIDs[i] = p.ID
- }
- tagsMap, err := s.placeRepo.GetTagsBatch(ctx, placeIDs)
- if err != nil {
- return nil, err
- }
- featuresMap, err := s.placeRepo.GetFeaturesBatch(ctx, placeIDs)
- if err != nil {
- return nil, err
- }
- for _, p := range places {
- if tags, ok := tagsMap[p.ID]; ok {
- p.Tags = tags
- }
- if features, ok := featuresMap[p.ID]; ok {
- p.Features = features
- }
- }
- }
- limit := filter.Limit()
- hasMore := len(places) > limit
- if hasMore {
- places = places[:limit]
- }
- var nextCursor string
- if hasMore && len(places) > 0 {
- nextCursor = encodeCursor(places[len(places)-1])
- }
- return &models.PaginatedPlaces{
- Data: places,
- NextCursor: nextCursor,
- HasMore: hasMore,
- }, nil
- }
- type UpdatePlaceInput struct {
- ID string
- OwnerID string
- Title *string
- Description *string
- Address *string
- Lat *float64
- Lng *float64
- CoverImage *string
- AccessInfo *string
- HourlyRate *int
- Currency *string
- MinHours *int
- }
- func (s *PlaceService) Update(ctx context.Context, input UpdatePlaceInput, isModerator bool) (*models.Place, error) {
- place, err := s.placeRepo.GetByID(ctx, input.ID)
- if err != nil {
- return nil, err
- }
- if place == nil {
- return nil, nil
- }
- if !isModerator && place.OwnerID != input.OwnerID {
- return nil, fmt.Errorf("%w: user %s tried to update place %s", ErrNotYourPlace, input.OwnerID, input.ID)
- }
- if input.Title != nil { place.Title = *input.Title }
- if input.Description != nil { place.Description = input.Description }
- if input.Address != nil { place.Address = input.Address }
- if input.Lat != nil { place.Lat = *input.Lat }
- if input.Lng != nil { place.Lng = *input.Lng }
- if input.CoverImage != nil { place.CoverImage = input.CoverImage }
- if input.AccessInfo != nil { place.AccessInfo = input.AccessInfo }
- if input.HourlyRate != nil { place.HourlyRate = input.HourlyRate }
- if input.Currency != nil { place.Currency = *input.Currency }
- if input.MinHours != nil { place.MinHours = *input.MinHours }
- if !isModerator {
- place.Status = "pending_moderation"
- }
- if err := s.placeRepo.Update(ctx, place); err != nil {
- return nil, err
- }
- return place, nil
- }
- func (s *PlaceService) Moderate(ctx context.Context, id, action, comment, moderatorID string) error {
- var status string
- switch action {
- case "approve":
- status = "published"
- case "reject":
- status = "rejected"
- default:
- return fmt.Errorf("unknown action: %s", action)
- }
- return s.placeRepo.UpdateStatus(ctx, id, status, comment)
- }
- func (s *PlaceService) Delete(ctx context.Context, id string) error {
- return s.placeRepo.SoftDelete(ctx, id)
- }
|