Files
digital-psychology/apps/api/internal/handler/synastry.go
T
jackyu66gitandCursor 89756f65b4 feat(ECR-012–016): 合规、题库、时辰刷新、头像、MBTI OEJTS 与埋点
落地输入合规、探索题库、报告日/时辰刷新、账号头像、OEJTS 量表,并补齐 H5 埋点与 Admin 漏斗;同步 ESS 工件、切至自建 Git、清理 GitHub Actions。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-13 01:27:58 +08:00

127 lines
3.6 KiB
Go

package handler
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/yuxingu/digital-psychology/apps/api/internal/middleware"
"github.com/yuxingu/digital-psychology/apps/api/internal/service/report"
"github.com/yuxingu/digital-psychology/apps/api/pkg/response"
)
// SynastryHandler exposes nearby + invite social APIs.
type SynastryHandler struct {
Svc *report.Service
}
// Register mounts synastry social routes.
func (h *SynastryHandler) Register(rg *gin.RouterGroup) {
rg.GET("/synastry/nearby", h.Nearby)
rg.POST("/synastry/invites", h.CreateInvite)
rg.GET("/synastry/invites/:token", h.GetInvite)
rg.POST("/synastry/invites/:token/accept", h.AcceptInvite)
}
// Nearby handles GET /synastry/nearby?lat=&lng=&radius_km=
func (h *SynastryHandler) Nearby(c *gin.Context) {
userID, ok := middleware.UserIDFromContext(c)
if !ok {
response.Fail(c, http.StatusUnauthorized, 40100, "unauthorized")
return
}
lat, err1 := strconv.ParseFloat(c.Query("lat"), 64)
lng, err2 := strconv.ParseFloat(c.Query("lng"), 64)
if err1 != nil || err2 != nil {
response.Fail(c, http.StatusBadRequest, 10000, "lat/lng required")
return
}
radius := 50.0
if v := c.Query("radius_km"); v != "" {
if r, err := strconv.ParseFloat(v, 64); err == nil {
radius = r
}
}
items, err := h.Svc.Nearby(c.Request.Context(), userID, lat, lng, radius)
if err != nil {
response.Fail(c, http.StatusBadRequest, 30002, err.Error())
return
}
response.OK(c, gin.H{"items": items})
}
// CreateInvite handles POST /synastry/invites
func (h *SynastryHandler) CreateInvite(c *gin.Context) {
userID, ok := middleware.UserIDFromContext(c)
if !ok {
response.Fail(c, http.StatusUnauthorized, 40100, "unauthorized")
return
}
var req struct {
ProfileID string `json:"profile_id" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.Fail(c, http.StatusBadRequest, 10000, "invalid request")
return
}
pid, err := uuid.Parse(req.ProfileID)
if err != nil {
response.Fail(c, http.StatusBadRequest, 10000, "invalid profile_id")
return
}
inv, err := h.Svc.CreateInvite(c.Request.Context(), userID, pid)
if err != nil {
response.Fail(c, http.StatusBadRequest, 30002, err.Error())
return
}
response.OK(c, gin.H{
"token": inv.Token,
"expires_at": inv.ExpiresAt,
"path": "/synastry/invite/" + inv.Token,
})
}
// GetInvite handles GET /synastry/invites/:token
func (h *SynastryHandler) GetInvite(c *gin.Context) {
if _, ok := middleware.UserIDFromContext(c); !ok {
response.Fail(c, http.StatusUnauthorized, 40100, "unauthorized")
return
}
meta, err := h.Svc.GetInvite(c.Request.Context(), c.Param("token"))
if err != nil {
response.Fail(c, http.StatusNotFound, 40401, err.Error())
return
}
response.OK(c, meta)
}
// AcceptInvite handles POST /synastry/invites/:token/accept
func (h *SynastryHandler) AcceptInvite(c *gin.Context) {
userID, ok := middleware.UserIDFromContext(c)
if !ok {
response.Fail(c, http.StatusUnauthorized, 40100, "unauthorized")
return
}
var req struct {
DisplayName string `json:"display_name"`
BirthDate string `json:"birth_date" binding:"required"`
BirthTime *string `json:"birth_time"`
BirthPlace *string `json:"birth_place"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.Fail(c, http.StatusBadRequest, 10000, "invalid request")
return
}
rep, err := h.Svc.AcceptInvite(c.Request.Context(), userID, c.Param("token"), req.DisplayName, req.BirthDate, req.BirthTime, req.BirthPlace)
if err != nil {
if failTextCompliance(c, err) {
return
}
response.Fail(c, http.StatusBadRequest, 30002, err.Error())
return
}
response.OK(c, rep)
}