feat: P1 合盘/星座/问答与测测完整设计包及模拟器取证工具
落地 synastry/star/ask API 与 H5 页面,补齐 cece-frontend-re complete-design 证据文档,并加入 Android 模拟器截图抓取脚本。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2,6 +2,8 @@ package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -15,26 +17,40 @@ type ProfileRepo struct {
|
||||
Pool *pgxpool.Pool
|
||||
}
|
||||
|
||||
// Create inserts a profile.
|
||||
func (r *ProfileRepo) Create(ctx context.Context, userID uuid.UUID, relation, name string, birth time.Time, relationType *string) (*model.Profile, error) {
|
||||
const profileCols = `
|
||||
id, user_id, relation, display_name, birth_date, relation_type,
|
||||
CASE WHEN birth_time IS NULL THEN NULL ELSE to_char(birth_time, 'HH24:MI') END,
|
||||
birth_place, geo_lat, geo_lng, COALESCE(geo_visible, false), created_at`
|
||||
|
||||
func scanProfile(scan func(dest ...any) error) (*model.Profile, error) {
|
||||
p := &model.Profile{}
|
||||
err := r.Pool.QueryRow(ctx, `
|
||||
INSERT INTO profiles(user_id, relation, display_name, birth_date, relation_type)
|
||||
VALUES ($1,$2,$3,$4,$5)
|
||||
RETURNING id, user_id, relation, display_name, birth_date, created_at`,
|
||||
userID, relation, name, birth, relationType,
|
||||
).Scan(&p.ID, &p.UserID, &p.Relation, &p.DisplayName, &p.BirthDate, &p.CreatedAt)
|
||||
var bt *string
|
||||
err := scan(&p.ID, &p.UserID, &p.Relation, &p.DisplayName, &p.BirthDate, &p.RelationType, &bt,
|
||||
&p.BirthPlace, &p.GeoLat, &p.GeoLng, &p.GeoVisible, &p.CreatedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.RelationType = relationType
|
||||
p.BirthTime = bt
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// Create inserts a profile.
|
||||
func (r *ProfileRepo) Create(ctx context.Context, userID uuid.UUID, relation, name string, birth time.Time, relationType *string, birthTime *string, birthPlace *string) (*model.Profile, error) {
|
||||
row := r.Pool.QueryRow(ctx, `
|
||||
INSERT INTO profiles(user_id, relation, display_name, birth_date, relation_type, birth_time, birth_place)
|
||||
VALUES ($1,$2,$3,$4,$5,
|
||||
CASE WHEN $6::text IS NULL OR $6::text = '' THEN NULL ELSE $6::time END,
|
||||
NULLIF(TRIM($7::text), ''))
|
||||
RETURNING `+profileCols,
|
||||
userID, relation, name, birth, relationType, birthTime, birthPlace,
|
||||
)
|
||||
return scanProfile(row.Scan)
|
||||
}
|
||||
|
||||
// ListByUser returns non-deleted profiles.
|
||||
func (r *ProfileRepo) ListByUser(ctx context.Context, userID uuid.UUID) ([]model.Profile, error) {
|
||||
rows, err := r.Pool.Query(ctx, `
|
||||
SELECT id, user_id, relation, display_name, birth_date, relation_type, created_at
|
||||
SELECT `+profileCols+`
|
||||
FROM profiles WHERE user_id=$1 AND deleted_at IS NULL
|
||||
ORDER BY created_at DESC`, userID)
|
||||
if err != nil {
|
||||
@@ -43,25 +59,127 @@ func (r *ProfileRepo) ListByUser(ctx context.Context, userID uuid.UUID) ([]model
|
||||
defer rows.Close()
|
||||
var out []model.Profile
|
||||
for rows.Next() {
|
||||
var p model.Profile
|
||||
if err := rows.Scan(&p.ID, &p.UserID, &p.Relation, &p.DisplayName, &p.BirthDate, &p.RelationType, &p.CreatedAt); err != nil {
|
||||
p, err := scanProfile(rows.Scan)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, p)
|
||||
out = append(out, *p)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// GetForUser loads a profile owned by user.
|
||||
func (r *ProfileRepo) GetForUser(ctx context.Context, userID, profileID uuid.UUID) (*model.Profile, error) {
|
||||
p := &model.Profile{}
|
||||
err := r.Pool.QueryRow(ctx, `
|
||||
SELECT id, user_id, relation, display_name, birth_date, relation_type, created_at
|
||||
row := r.Pool.QueryRow(ctx, `
|
||||
SELECT `+profileCols+`
|
||||
FROM profiles WHERE id=$1 AND user_id=$2 AND deleted_at IS NULL`,
|
||||
profileID, userID,
|
||||
).Scan(&p.ID, &p.UserID, &p.Relation, &p.DisplayName, &p.BirthDate, &p.RelationType, &p.CreatedAt)
|
||||
)
|
||||
return scanProfile(row.Scan)
|
||||
}
|
||||
|
||||
// GetByID loads any non-deleted profile (for invite host lookup).
|
||||
func (r *ProfileRepo) GetByID(ctx context.Context, profileID uuid.UUID) (*model.Profile, error) {
|
||||
row := r.Pool.QueryRow(ctx, `
|
||||
SELECT `+profileCols+`
|
||||
FROM profiles WHERE id=$1 AND deleted_at IS NULL`, profileID)
|
||||
return scanProfile(row.Scan)
|
||||
}
|
||||
|
||||
// UpdateForUser patches display name / birth / geo.
|
||||
func (r *ProfileRepo) UpdateForUser(ctx context.Context, userID, profileID uuid.UUID, name string, birth time.Time, relationType *string, birthTime *string, birthPlace *string, geoLat, geoLng *float64, geoVisible *bool) (*model.Profile, error) {
|
||||
row := r.Pool.QueryRow(ctx, `
|
||||
UPDATE profiles
|
||||
SET display_name=$3, birth_date=$4, relation_type=$5,
|
||||
birth_time=CASE WHEN $6::text IS NULL OR $6::text = '' THEN birth_time ELSE $6::time END,
|
||||
birth_place=CASE WHEN $7::text IS NULL THEN birth_place ELSE NULLIF(TRIM($7::text), '') END,
|
||||
geo_lat=CASE WHEN $8::float8 IS NULL THEN geo_lat ELSE $8 END,
|
||||
geo_lng=CASE WHEN $9::float8 IS NULL THEN geo_lng ELSE $9 END,
|
||||
geo_visible=CASE WHEN $10::bool IS NULL THEN geo_visible ELSE $10 END,
|
||||
updated_at=now()
|
||||
WHERE id=$1 AND user_id=$2 AND deleted_at IS NULL
|
||||
RETURNING `+profileCols,
|
||||
profileID, userID, name, birth, relationType, birthTime, birthPlace, geoLat, geoLng, geoVisible,
|
||||
)
|
||||
return scanProfile(row.Scan)
|
||||
}
|
||||
|
||||
// NearbyItem is a visible profile with distance.
|
||||
type NearbyItem struct {
|
||||
Profile model.Profile `json:"profile"`
|
||||
Distance float64 `json:"distance_km"`
|
||||
}
|
||||
|
||||
// ListNearby returns other users' self profiles with geo_visible within radius.
|
||||
func (r *ProfileRepo) ListNearby(ctx context.Context, excludeUserID uuid.UUID, lat, lng, radiusKm float64, limit int) ([]NearbyItem, error) {
|
||||
if limit <= 0 || limit > 50 {
|
||||
limit = 20
|
||||
}
|
||||
rows, err := r.Pool.Query(ctx, `
|
||||
SELECT `+profileCols+`
|
||||
FROM profiles
|
||||
WHERE deleted_at IS NULL AND geo_visible = true
|
||||
AND relation = 'self'
|
||||
AND user_id <> $1
|
||||
AND geo_lat IS NOT NULL AND geo_lng IS NOT NULL
|
||||
LIMIT 200`, excludeUserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p, nil
|
||||
defer rows.Close()
|
||||
var out []NearbyItem
|
||||
for rows.Next() {
|
||||
p, err := scanProfile(rows.Scan)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if p.GeoLat == nil || p.GeoLng == nil {
|
||||
continue
|
||||
}
|
||||
d := haversineKm(lat, lng, *p.GeoLat, *p.GeoLng)
|
||||
if d <= radiusKm {
|
||||
out = append(out, NearbyItem{Profile: *p, Distance: math.Round(d*10) / 10})
|
||||
}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// sort by distance
|
||||
for i := 0; i < len(out); i++ {
|
||||
for j := i + 1; j < len(out); j++ {
|
||||
if out[j].Distance < out[i].Distance {
|
||||
out[i], out[j] = out[j], out[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(out) > limit {
|
||||
out = out[:limit]
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func haversineKm(lat1, lng1, lat2, lng2 float64) float64 {
|
||||
const R = 6371.0
|
||||
toR := func(d float64) float64 { return d * math.Pi / 180 }
|
||||
dLat := toR(lat2 - lat1)
|
||||
dLng := toR(lng2 - lng1)
|
||||
a := math.Sin(dLat/2)*math.Sin(dLat/2) +
|
||||
math.Cos(toR(lat1))*math.Cos(toR(lat2))*math.Sin(dLng/2)*math.Sin(dLng/2)
|
||||
return 2 * R * math.Asin(math.Sqrt(a))
|
||||
}
|
||||
|
||||
// SoftDeleteForUser marks a profile deleted.
|
||||
func (r *ProfileRepo) SoftDeleteForUser(ctx context.Context, userID, profileID uuid.UUID) error {
|
||||
tag, err := r.Pool.Exec(ctx, `
|
||||
UPDATE profiles SET deleted_at=now(), updated_at=now()
|
||||
WHERE id=$1 AND user_id=$2 AND deleted_at IS NULL`,
|
||||
profileID, userID,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tag.RowsAffected() == 0 {
|
||||
return errors.New("profile not found")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user