buhapp-backend/internal/locations/repo.go
ga d4997e85ef Sprint 2: profile, preferences, location, search
- PUT  /api/v1/me (update name, city, bio, gender, photo)
- GET/PUT /api/v1/me/prefs (drinks, activities, purposes, language)
- PUT  /api/v1/me/location (auto-noise ~300m)
- PUT  /api/v1/me/visibility (hide/show on map)
- GET  /api/v1/search/nearby?lat&lng&radius (haversine, 1h TTL)
- GET  /api/v1/users/:id (public profile, no email/phone)
- migrations 0002: user_preferences, user_locations
2026-08-20 15:49:21 +00:00

137 lines
3.4 KiB
Go

package locations
import (
"context"
"errors"
"math"
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgxpool"
)
type Location struct {
UserID uuid.UUID
Lat float64
Lng float64
Visible bool
UpdatedAt time.Time
}
type Repo struct {
pool *pgxpool.Pool
}
func NewRepo(pool *pgxpool.Pool) *Repo {
return &Repo{pool: pool}
}
// ApplyNoise — округляем координаты до ~300м точности (grid ~0.003 deg ≈ 333м)
func ApplyNoise(lat, lng float64) (float64, float64) {
const step = 0.003
return math.Round(lat/step)*step, math.Round(lng/step)*step
}
func (r *Repo) Upsert(ctx context.Context, loc *Location) error {
loc.Lat, loc.Lng = ApplyNoise(loc.Lat, loc.Lng)
_, err := r.pool.Exec(ctx, `
INSERT INTO user_locations (user_id, lat, lng, visible, updated_at)
VALUES ($1, $2, $3, $4, NOW())
ON CONFLICT (user_id) DO UPDATE
SET lat = EXCLUDED.lat, lng = EXCLUDED.lng,
visible = EXCLUDED.visible, updated_at = NOW()`,
loc.UserID, loc.Lat, loc.Lng, loc.Visible,
)
return err
}
func (r *Repo) SetVisible(ctx context.Context, userID uuid.UUID, visible bool) error {
_, err := r.pool.Exec(ctx, `
INSERT INTO user_locations (user_id, lat, lng, visible, updated_at)
VALUES ($1, NULL, NULL, $2, NOW())
ON CONFLICT (user_id) DO UPDATE
SET visible = EXCLUDED.visible, updated_at = NOW()`,
userID, visible,
)
return err
}
func (r *Repo) Get(ctx context.Context, userID uuid.UUID) (*Location, error) {
loc := &Location{UserID: userID}
var lat, lng *float64
err := r.pool.QueryRow(ctx, `
SELECT lat, lng, visible, updated_at
FROM user_locations WHERE user_id=$1`, userID,
).Scan(&lat, &lng, &loc.Visible, &loc.UpdatedAt)
if err != nil {
return nil, err
}
if lat != nil {
loc.Lat = *lat
}
if lng != nil {
loc.Lng = *lng
}
return loc, nil
}
// Nearby — пользователи в радиусе radius км (по haversine), visible, не старше 1 часа
type Nearby struct {
UserID uuid.UUID
Name string
PhotoURL string
Lat float64
Lng float64
DistanceM float64
}
func (r *Repo) Nearby(ctx context.Context, myID uuid.UUID, lat, lng, radiusKm float64, limit int) ([]Nearby, error) {
if limit <= 0 || limit > 500 {
limit = 100
}
rows, err := r.pool.Query(ctx, `
SELECT u.id, u.name, u.photo_url, ul.lat, ul.lng,
2 * 6371 * asin(sqrt(
sin(radians((ul.lat - $1) / 2))^2 +
cos(radians($1)) * cos(radians(ul.lat)) *
sin(radians((ul.lng - $2) / 2))^2
)) AS dist_km
FROM user_locations ul
JOIN users u ON u.id = ul.user_id
WHERE ul.visible = TRUE
AND u.is_blocked = FALSE
AND u.id != $3
AND ul.lat IS NOT NULL AND ul.lng IS NOT NULL
AND ul.updated_at > NOW() - INTERVAL '1 hour'
AND 2 * 6371 * asin(sqrt(
sin(radians((ul.lat - $1) / 2))^2 +
cos(radians($1)) * cos(radians(ul.lat)) *
sin(radians((ul.lng - $2) / 2))^2
)) <= $4
ORDER BY dist_km
LIMIT $5`,
lat, lng, myID, radiusKm, limit,
)
if err != nil {
return nil, err
}
defer rows.Close()
var out []Nearby
for rows.Next() {
var n Nearby
var photo *string
if err := rows.Scan(&n.UserID, &n.Name, &photo, &n.Lat, &n.Lng, &n.DistanceM); err != nil {
return nil, err
}
if photo != nil {
n.PhotoURL = *photo
}
n.DistanceM = n.DistanceM * 1000
out = append(out, n)
}
return out, rows.Err()
}
var ErrNotFound = errors.New("not found")