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 <noreply@anthropic.com>
This commit is contained in:
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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:"-"`
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user