Initial commit: dating app backend (Go/Gin/GORM/Postgres)

Phone+OTP auth, staged onboarding, admin approval workflow, and a
full discovery/swipe/match/chat system with photo upload and
approximate distance (haversine) based on user location.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 23:23:47 +03:30
commit 2823fb45f5
49 changed files with 4582 additions and 0 deletions
+101
View File
@@ -0,0 +1,101 @@
package handler
import (
"errors"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"dating-app/internal/dto"
"dating-app/internal/models"
"dating-app/internal/service"
"dating-app/internal/utils"
)
type DiscoveryHandler struct {
discoveryService *service.DiscoveryService
}
func NewDiscoveryHandler(discoveryService *service.DiscoveryService) *DiscoveryHandler {
return &DiscoveryHandler{discoveryService: discoveryService}
}
// Feed GET /discover?max_distance_km=&min_age=&max_age=
func (h *DiscoveryHandler) Feed(c *gin.Context) {
user, ok := currentUser(c)
if !ok {
return
}
var maxDistance *float64
if raw := c.Query("max_distance_km"); raw != "" {
if v, err := strconv.ParseFloat(raw, 64); err == nil {
maxDistance = &v
}
}
minAge, _ := strconv.Atoi(c.Query("min_age"))
maxAge, _ := strconv.Atoi(c.Query("max_age"))
candidates, err := h.discoveryService.GetFeed(user, maxDistance, minAge, maxAge)
if err != nil {
h.handleError(c, err)
return
}
utils.Success(c, http.StatusOK, "", candidates)
}
// CandidateDetail GET /discover/:id
func (h *DiscoveryHandler) CandidateDetail(c *gin.Context) {
user, ok := currentUser(c)
if !ok {
return
}
targetID, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
utils.Fail(c, http.StatusBadRequest, "شناسه نامعتبر است", nil)
return
}
detail, err := h.discoveryService.GetCandidateDetail(user, uint(targetID))
if err != nil {
utils.Fail(c, http.StatusNotFound, "پروفایل یافت نشد", nil)
return
}
utils.Success(c, http.StatusOK, "", detail)
}
// Swipe POST /discover/:id/swipe body: {action: "like"|"pass"|"superlike"}
func (h *DiscoveryHandler) Swipe(c *gin.Context) {
user, ok := currentUser(c)
if !ok {
return
}
targetID, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
utils.Fail(c, http.StatusBadRequest, "شناسه نامعتبر است", nil)
return
}
var req dto.SwipeRequest
if err := c.ShouldBindJSON(&req); err != nil {
utils.Fail(c, http.StatusBadRequest, "ورودی نامعتبر است", err.Error())
return
}
result, err := h.discoveryService.Swipe(user, uint(targetID), models.SwipeAction(req.Action))
if err != nil {
h.handleError(c, err)
return
}
utils.Success(c, http.StatusOK, "", result)
}
func (h *DiscoveryHandler) handleError(c *gin.Context, err error) {
switch {
case errors.Is(err, service.ErrGenderNotSet):
utils.Fail(c, http.StatusConflict, err.Error(), nil)
case errors.Is(err, service.ErrCannotSwipeSelf), errors.Is(err, service.ErrAlreadySwiped):
utils.Fail(c, http.StatusBadRequest, err.Error(), nil)
default:
utils.Fail(c, http.StatusInternalServerError, "خطای داخلی سرور", nil)
}
}