- All response types now use lowercase json tags (id, name, photo_url, etc.) - email/phone/birthdate/last_seen_at/photo_url use omitempty - audit.Metadata type []byte (was map[string]any) - fixes register/login inconsistency where login returned ID/Name (CamelCase) while register returned id/name (lowercase)
137 lines
3.5 KiB
Go
137 lines
3.5 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 `json:"user_id"`
|
|
Lat float64 `json:"lat"`
|
|
Lng float64 `json:"lng"`
|
|
Visible bool `json:"visible"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
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")
|