From 2c6d4bfbd25d121bd2c7214afa06e9d158cf0cf5 Mon Sep 17 00:00:00 2001 From: mobiniqow Date: Sat, 26 Sep 2026 18:08:57 +0330 Subject: [PATCH] Add notification settings, account deletion, photo reporting, and pending-user photos Addresses user bug reports: settings pages needed real notification preferences and account deletion, admin needed a way to flag a specific photo independent of deleting it, and the pending-approval users list needed to surface applicant photos for review. Co-Authored-By: Claude Sonnet 5 --- go.mod | 21 ++++++++------ go.sum | 15 ++++++++++ internal/dto/auth_dto.go | 10 +++++++ internal/dto/profile_dto.go | 7 +++++ internal/handler/admin_handler.go | 37 +++++++++++++++++++++++-- internal/handler/common.go | 3 ++ internal/handler/profile_handler.go | 31 +++++++++++++++++++++ internal/models/report.go | 6 ++-- internal/models/user.go | 7 +++++ internal/router/router.go | 3 ++ internal/service/admin_service.go | 43 +++++++++++++++++++++++++++-- internal/service/profile_service.go | 22 +++++++++++++++ 12 files changed, 188 insertions(+), 17 deletions(-) diff --git a/go.mod b/go.mod index a6324b7..bb47ac9 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module dating-app -go 1.22.2 +go 1.25 replace golang.org/x/net => github.com/golang/net v0.25.0 @@ -26,44 +26,49 @@ replace google.golang.org/protobuf => github.com/protocolbuffers/protobuf-go v1. replace golang.org/x/xerrors => github.com/golang/xerrors v0.0.0-20191204190536-9bdfabe68543 +require ( + github.com/gin-gonic/gin v1.9.1 + github.com/golang-jwt/jwt/v5 v5.3.1 + github.com/google/uuid v1.6.0 + github.com/joho/godotenv v1.5.1 + gorm.io/driver/postgres v1.5.9 + gorm.io/gorm v1.25.10 +) + require ( github.com/bytedance/sonic v1.9.1 // indirect github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/gin-contrib/sse v0.1.0 // indirect - github.com/gin-gonic/gin v1.9.1 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.14.0 // indirect github.com/goccy/go-json v0.10.2 // indirect - github.com/golang-jwt/jwt/v5 v5.3.1 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgx/v5 v5.5.5 // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect - github.com/joho/godotenv v1.5.1 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/kr/text v0.2.0 // indirect github.com/leodido/go-urn v1.2.4 // indirect github.com/mattn/go-isatty v0.0.19 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/rogpeppe/go-internal v1.16.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.11 // indirect golang.org/x/arch v0.3.0 // indirect golang.org/x/crypto v0.23.0 // indirect golang.org/x/net v0.21.0 // indirect golang.org/x/sync v0.1.0 // indirect - golang.org/x/sys v0.20.0 // indirect + golang.org/x/sys v0.26.0 // indirect golang.org/x/text v0.15.0 // indirect google.golang.org/protobuf v1.30.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - gorm.io/driver/postgres v1.5.9 // indirect - gorm.io/gorm v1.25.10 // indirect ) replace gorm.io/gorm => github.com/go-gorm/gorm v1.25.10 diff --git a/go.sum b/go.sum index 1fc1241..6f560ad 100644 --- a/go.sum +++ b/go.sum @@ -4,7 +4,9 @@ github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZX github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= @@ -12,11 +14,14 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/go-check/check v0.0.0-20180628173108-788fd7840127 h1:0gkP6mzaMqkmpcJYCFOLkIBwI7xFExG03bbkOkCvUPI= github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= github.com/go-gorm/gorm v1.25.10 h1:s8FP+QiJt4yTWhaqqaTEXmJezVd4pLuYihWFa5ka6oc= github.com/go-gorm/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= github.com/go-gorm/postgres v1.5.9 h1:R8GFj+hNnViUX6OKlL8jp7b5fVUVlDE3ULqQ+ae7aPk= github.com/go-gorm/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= @@ -42,7 +47,9 @@ github.com/golang/sys v0.20.0 h1:s6f6MpCY0CE+A62kAS9FiDNmD3rpVbd+OISylPY45NQ= github.com/golang/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= github.com/golang/text v0.15.0 h1:Hr6Y5cbI9MiTMnLQhMfqQzC3smwk0O9n6gYp78JNYJ4= github.com/golang/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +github.com/golang/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:WKKabR3QofYgj3cabwq+Blt8MSvmXR4GKLoohSdeY1A= github.com/golang/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -66,6 +73,10 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= @@ -77,9 +88,12 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/protocolbuffers/protobuf-go v1.30.0 h1:8l0IAhOWx+qT8fxNzYIj+TS9SEU6xamBNS0ZmRIy3eY= github.com/protocolbuffers/protobuf-go v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +github.com/rogpeppe/go-internal v1.16.0 h1:O9DK+vNMDVGLr2BeZqmpLeMjiMNkuXfcqntWbZV6S5g= +github.com/rogpeppe/go-internal v1.16.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs= github.com/rsc/pdf v0.1.1/go.mod h1:+1cgsOaCtY/6pdg9OEno8rLCRIQj5LeL9ckNDtNZsFs= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= @@ -90,6 +104,7 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= diff --git a/internal/dto/auth_dto.go b/internal/dto/auth_dto.go index fe2f564..516faa2 100644 --- a/internal/dto/auth_dto.go +++ b/internal/dto/auth_dto.go @@ -51,4 +51,14 @@ type UserProfile struct { BlockedAt *time.Time `json:"blocked_at,omitempty"` Bio *string `json:"bio,omitempty"` StreakCount int `json:"streak_count"` + NotifyMatches bool `json:"notify_matches"` + NotifyMessages bool `json:"notify_messages"` + NotifyLikes bool `json:"notify_likes"` +} + +// UserProfileWithPhotos برای لیست‌های ادمینی که علاوه بر خلاصه‌ی پروفایل، عکس +// کاربر رو هم نشون می‌دن (مثلاً کاربران در انتظار تایید). +type UserProfileWithPhotos struct { + UserProfile + Photos []string `json:"photos"` } diff --git a/internal/dto/profile_dto.go b/internal/dto/profile_dto.go index a1e536b..29e6ba3 100644 --- a/internal/dto/profile_dto.go +++ b/internal/dto/profile_dto.go @@ -16,3 +16,10 @@ type CheckInResult struct { StreakCount int `json:"streak_count"` IsNewToday bool `json:"is_new_today"` } + +// UpdateNotificationSettingsRequest ترجیحات اعلان کاربر +type UpdateNotificationSettingsRequest struct { + NotifyMatches bool `json:"notify_matches"` + NotifyMessages bool `json:"notify_messages"` + NotifyLikes bool `json:"notify_likes"` +} diff --git a/internal/handler/admin_handler.go b/internal/handler/admin_handler.go index e2dc643..0c24135 100644 --- a/internal/handler/admin_handler.go +++ b/internal/handler/admin_handler.go @@ -165,14 +165,21 @@ func (h *AdminHandler) DeleteOption(c *gin.Context) { // ListPendingUsers GET /admin/users/pending func (h *AdminHandler) ListPendingUsers(c *gin.Context) { - users, err := h.adminService.ListPendingUsers() + users, photosByUser, err := h.adminService.ListPendingUsers() if err != nil { utils.Fail(c, http.StatusInternalServerError, "خطا در دریافت لیست کاربران", nil) return } - result := make([]dto.UserProfile, 0, len(users)) + result := make([]dto.UserProfileWithPhotos, 0, len(users)) for i := range users { - result = append(result, toUserProfile(&users[i])) + photos := photosByUser[users[i].ID] + if photos == nil { + photos = []string{} + } + result = append(result, dto.UserProfileWithPhotos{ + UserProfile: toUserProfile(&users[i]), + Photos: photos, + }) } utils.Success(c, http.StatusOK, "", result) } @@ -382,6 +389,30 @@ func (h *AdminHandler) DeletePhoto(c *gin.Context) { utils.Success(c, http.StatusOK, "عکس حذف شد", nil) } +// ReportPhoto POST /admin/photos/:id/report body: {reason} +func (h *AdminHandler) ReportPhoto(c *gin.Context) { + id, err := strconv.ParseUint(c.Param("id"), 10, 64) + if err != nil { + utils.Fail(c, http.StatusBadRequest, "شناسه نامعتبر است", nil) + return + } + var req dto.ReportUserRequest + if err := c.ShouldBindJSON(&req); err != nil { + utils.Fail(c, http.StatusBadRequest, "ورودی نامعتبر است (ذکر دلیل گزارش الزامی است)", err.Error()) + return + } + report, err := h.adminService.ReportPhoto(uint(id), req.Reason) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + utils.Fail(c, http.StatusNotFound, "عکس یافت نشد", nil) + return + } + utils.Fail(c, http.StatusBadRequest, err.Error(), nil) + return + } + utils.Success(c, http.StatusCreated, "گزارش ثبت شد", report) +} + // ---------------- سوالات پروفایل ---------------- // ListQuestions GET /admin/profile-questions diff --git a/internal/handler/common.go b/internal/handler/common.go index a77355a..3ca180f 100644 --- a/internal/handler/common.go +++ b/internal/handler/common.go @@ -45,6 +45,9 @@ func toUserProfile(u *models.User) dto.UserProfile { BlockedAt: u.BlockedAt, Bio: u.Bio, StreakCount: u.StreakCount, + NotifyMatches: u.NotifyMatches, + NotifyMessages: u.NotifyMessages, + NotifyLikes: u.NotifyLikes, } if u.Gender != nil { g := string(*u.Gender) diff --git a/internal/handler/profile_handler.go b/internal/handler/profile_handler.go index 4dec35c..e64d7a5 100644 --- a/internal/handler/profile_handler.go +++ b/internal/handler/profile_handler.go @@ -170,3 +170,34 @@ func (h *ProfileHandler) CheckIn(c *gin.Context) { } utils.Success(c, http.StatusOK, "", result) } + +// UpdateNotificationSettings PATCH /profile/notifications +func (h *ProfileHandler) UpdateNotificationSettings(c *gin.Context) { + user, ok := currentUser(c) + if !ok { + return + } + var req dto.UpdateNotificationSettingsRequest + if err := c.ShouldBindJSON(&req); err != nil { + utils.Fail(c, http.StatusBadRequest, "ورودی نامعتبر است", err.Error()) + return + } + if err := h.profileService.UpdateNotificationSettings(user, req); err != nil { + utils.Fail(c, http.StatusInternalServerError, "خطا در ذخیره‌ی تنظیمات اعلان", nil) + return + } + utils.Success(c, http.StatusOK, "تنظیمات اعلان ذخیره شد", nil) +} + +// DeleteAccount DELETE /profile/me — غیرفعال کردن (soft delete) حساب کاربر +func (h *ProfileHandler) DeleteAccount(c *gin.Context) { + user, ok := currentUser(c) + if !ok { + return + } + if err := h.profileService.DeleteAccount(user); err != nil { + utils.Fail(c, http.StatusInternalServerError, "خطا در حذف حساب", nil) + return + } + utils.Success(c, http.StatusOK, "حساب کاربری غیرفعال شد", nil) +} diff --git a/internal/models/report.go b/internal/models/report.go index 47e97aa..6096714 100644 --- a/internal/models/report.go +++ b/internal/models/report.go @@ -2,12 +2,12 @@ package models import "time" -// Report یک یادداشت/پرچم نظارتی که ادمین روی یک کاربر ثبت می‌کنه (مثلاً بر -// اساس شکایتی که از کانال دیگه‌ای دریافت شده)؛ برای پیگیری سابقه‌ی نظارتی -// یک کاربر، مستقل از بلاک‌شدن یا نشدنش. +// Report یک یادداشت/پرچم نظارتی که ادمین روی یک کاربر (یا یک عکس مشخص از +// اون کاربر) ثبت می‌کنه؛ برای پیگیری سابقه‌ی نظارتی، مستقل از بلاک/حذف کردن. type Report struct { ID uint `gorm:"primaryKey" json:"id"` UserID uint `gorm:"not null;index" json:"user_id"` + PhotoID *uint `json:"photo_id,omitempty"` Reason string `gorm:"type:text;not null" json:"reason"` CreatedAt time.Time `json:"created_at"` } diff --git a/internal/models/user.go b/internal/models/user.go index b26358a..38ff00b 100644 --- a/internal/models/user.go +++ b/internal/models/user.go @@ -97,6 +97,13 @@ type User struct { StreakCount int `gorm:"default:0;not null" json:"streak_count"` LastActiveDate *time.Time `json:"last_active_date,omitempty"` + // -------- ترجیحات اعلان -------- + // فعلاً فقط ذخیره می‌شن (زیرساخت ارسال واقعی اعلان هنوز پیاده نشده)؛ وقتی + // اون سیستم اضافه شد، همین مقادیر رو برای تصمیم به ارسال یا نه می‌خونه. + NotifyMatches bool `gorm:"default:true;not null" json:"notify_matches"` + NotifyMessages bool `gorm:"default:true;not null" json:"notify_messages"` + NotifyLikes bool `gorm:"default:true;not null" json:"notify_likes"` + // -------- موقعیت مکانی (برای محاسبه‌ی فاصله‌ی تقریبی در دوستیابی) -------- Latitude *float64 `json:"-"` Longitude *float64 `json:"-"` diff --git a/internal/router/router.go b/internal/router/router.go index 6a9c8b5..1daa6ee 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -61,6 +61,8 @@ func Setup( profile.PATCH("/bio", h.Profile.UpdateBio) // استریک روزانه (گیمیفیکیشن): هر بار اپ باز می‌شه یه بار صدا زده می‌شه profile.POST("/checkin", h.Profile.CheckIn) + profile.PATCH("/notifications", h.Profile.UpdateNotificationSettings) + profile.DELETE("/me", h.Profile.DeleteAccount) } // -------- آنبوردینگ (نیاز به ورود) -------- @@ -115,6 +117,7 @@ func Setup( // نظارت بر عکس‌های آپلودشده admin.GET("/photos", h.Admin.ListAllPhotos) admin.DELETE("/photos/:id", h.Admin.DeletePhoto) + admin.POST("/photos/:id/report", h.Admin.ReportPhoto) admin.GET("/categories/:key", h.Admin.GetCategory) admin.PATCH("/categories/:key", h.Admin.UpdateCategory) diff --git a/internal/service/admin_service.go b/internal/service/admin_service.go index 3626b25..c06f259 100644 --- a/internal/service/admin_service.go +++ b/internal/service/admin_service.go @@ -52,9 +52,29 @@ func (s *AdminService) GetUser(id uint) (*models.User, error) { return s.userRepo.FindByID(id) } -// ListPendingUsers کاربرانی که آنبوردینگ رو تکمیل کردن و منتظر بررسی‌اند -func (s *AdminService) ListPendingUsers() ([]models.User, error) { - return s.userRepo.ListPendingApproval() +// ListPendingUsers کاربرانی که آنبوردینگ رو تکمیل کردن و منتظر بررسی‌اند، به +// همراه عکس‌هاشون (batch گرفته می‌شه، نه یک کوئری جدا به‌ازای هر کاربر) تا +// ادمین بدون کلیک روی هرکدوم بتونه یه نگاه سریع به عکس‌ها بندازه. +func (s *AdminService) ListPendingUsers() ([]models.User, map[uint][]string, error) { + users, err := s.userRepo.ListPendingApproval() + if err != nil { + return nil, nil, err + } + + ids := make([]uint, len(users)) + for i, u := range users { + ids[i] = u.ID + } + photos, err := s.photoRepo.ListByUserIDs(ids) + if err != nil { + return nil, nil, err + } + photosByUser := make(map[uint][]string, len(ids)) + for _, p := range photos { + photosByUser[p.UserID] = append(photosByUser[p.UserID], p.URL) + } + + return users, photosByUser, nil } // ApproveUser کاربر رو تایید می‌کنه تا بتونه وارد صفحه‌ی دوستیابی بشه @@ -205,6 +225,23 @@ func (s *AdminService) ReportUser(id uint, reason string) (*models.Report, error return report, nil } +// ReportPhoto یک عکس مشخص رو گزارش می‌کنه (روی صاحب عکس هم توی سابقه‌ی +// گزارش‌ها ثبت می‌شه، با ارجاع به همون عکس). +func (s *AdminService) ReportPhoto(photoID uint, reason string) (*models.Report, error) { + if reason == "" { + return nil, ErrReportReasonEmpty + } + photo, err := s.photoRepo.FindByID(photoID) + if err != nil { + return nil, err + } + report := &models.Report{UserID: photo.UserID, PhotoID: &photo.ID, Reason: reason} + if err := s.reportRepo.Create(report); err != nil { + return nil, err + } + return report, nil +} + // ListReports سابقه‌ی گزارش‌های ثبت‌شده روی یک کاربر رو برمی‌گردونه. func (s *AdminService) ListReports(userID uint) ([]models.Report, error) { return s.reportRepo.ListByUser(userID) diff --git a/internal/service/profile_service.go b/internal/service/profile_service.go index 9e4c09b..511ed26 100644 --- a/internal/service/profile_service.go +++ b/internal/service/profile_service.go @@ -141,3 +141,25 @@ func (s *ProfileService) CheckIn(user *models.User) (*dto.CheckInResult, error) return &dto.CheckInResult{StreakCount: newStreak, IsNewToday: true}, nil } + +// UpdateNotificationSettings ترجیحات اعلان کاربر رو ذخیره می‌کنه. +func (s *ProfileService) UpdateNotificationSettings(user *models.User, req dto.UpdateNotificationSettingsRequest) error { + if err := s.userRepo.UpdateFields(user, map[string]interface{}{ + "notify_matches": req.NotifyMatches, + "notify_messages": req.NotifyMessages, + "notify_likes": req.NotifyLikes, + }); err != nil { + return err + } + user.NotifyMatches = req.NotifyMatches + user.NotifyMessages = req.NotifyMessages + user.NotifyLikes = req.NotifyLikes + return nil +} + +// DeleteAccount حساب کاربر رو غیرفعال می‌کنه (soft) نه پاک‌سازی کامل رکورد؛ +// چون مچ‌ها/پیام‌های طرف مقابل نباید یهو پاره بشن. کاربر غیرفعال دیگه اصلاً +// نمی‌تونه لاگین کنه (چک IsActive توی AuthRequired و AuthService). +func (s *ProfileService) DeleteAccount(user *models.User) error { + return s.userRepo.UpdateFields(user, map[string]interface{}{"is_active": false}) +}