chore: merge backend repo history into monorepo at backend/

git-subtree-dir: backend
git-subtree-mainline: dc11190527
git-subtree-split: acae976622
This commit is contained in:
2026-09-27 10:30:16 +03:30
67 changed files with 6835 additions and 0 deletions
+182
View File
@@ -0,0 +1,182 @@
package service
import (
"encoding/base64"
"errors"
"strings"
"time"
"dating-app/internal/dto"
"dating-app/internal/models"
"dating-app/internal/repository"
)
const MaxPhotosPerUser = 6
var (
ErrTooManyPhotos = errors.New("حداکثر تعداد عکس مجاز رو داری")
ErrPhotoNotOwned = errors.New("این عکس متعلق به تو نیست")
ErrInvalidImage = errors.New("فرمت فایل باید jpg، png یا webp باشه")
ErrImageTooLarge = errors.New("حجم عکس نباید بیشتر از ۵ مگابایت باشه")
ErrInvalidPublicKey = errors.New("کلید عمومی نامعتبر است")
)
const MaxPhotoSizeBytes = 5 * 1024 * 1024
type ProfileService struct {
photoRepo *repository.PhotoRepository
userRepo *repository.UserRepository
storage PhotoStorage
}
func NewProfileService(photoRepo *repository.PhotoRepository, userRepo *repository.UserRepository, storage PhotoStorage) *ProfileService {
return &ProfileService{photoRepo: photoRepo, userRepo: userRepo, storage: storage}
}
var allowedImageExts = map[string]bool{
".jpg": true, ".jpeg": true, ".png": true, ".webp": true,
}
func (s *ProfileService) UploadPhoto(userID uint, ext string, data []byte) (*models.Photo, error) {
if !allowedImageExts[ext] {
return nil, ErrInvalidImage
}
if len(data) > MaxPhotoSizeBytes {
return nil, ErrImageTooLarge
}
count, err := s.photoRepo.CountByUser(userID)
if err != nil {
return nil, err
}
if count >= MaxPhotosPerUser {
return nil, ErrTooManyPhotos
}
url, err := s.storage.Save(userID, ext, data)
if err != nil {
return nil, err
}
photo := &models.Photo{UserID: userID, URL: url, SortOrder: int(count)}
if err := s.photoRepo.Create(photo); err != nil {
return nil, err
}
return photo, nil
}
func (s *ProfileService) DeletePhoto(userID, photoID uint) error {
photo, err := s.photoRepo.FindByID(photoID)
if err != nil {
return err
}
if photo.UserID != userID {
return ErrPhotoNotOwned
}
if err := s.photoRepo.Delete(photoID); err != nil {
return err
}
return s.storage.Delete(photo.URL)
}
// DeletePhotoAdmin مثل DeletePhoto ولی بدون چک مالکیت؛ برای نظارت محتوایی
// ادمین که باید بتونه هر عکسی رو صرف‌نظر از صاحبش حذف کنه.
func (s *ProfileService) DeletePhotoAdmin(photoID uint) error {
photo, err := s.photoRepo.FindByID(photoID)
if err != nil {
return err
}
if err := s.photoRepo.Delete(photoID); err != nil {
return err
}
return s.storage.Delete(photo.URL)
}
func (s *ProfileService) ListPhotos(userID uint) ([]models.Photo, error) {
return s.photoRepo.ListByUser(userID)
}
func (s *ProfileService) UpdateLocation(user *models.User, lat, lng float64) error {
return s.userRepo.UpdateFields(user, map[string]interface{}{
"latitude": lat,
"longitude": lng,
})
}
// UpdateBio بیوگرافی کوتاه پروفایل رو ذخیره می‌کنه (طول توسط binding در DTO چک شده)
func (s *ProfileService) UpdateBio(user *models.User, bio string) error {
bio = strings.TrimSpace(bio)
if err := s.userRepo.UpdateFields(user, map[string]interface{}{"bio": bio}); err != nil {
return err
}
user.Bio = &bio
return nil
}
// CheckIn ورود روزانه‌ی کاربر رو ثبت می‌کنه و استریک (چند روز پشت‌سرهم) رو
// به‌روز می‌کنه: اگه دیروز هم اومده بوده، استریک +۱ می‌شه؛ اگه امروز قبلاً
// چک‌این کرده، چیزی تغییر نمی‌کنه؛ وگرنه (وقفه افتاده) استریک از ۱ شروع می‌شه.
func (s *ProfileService) CheckIn(user *models.User) (*dto.CheckInResult, error) {
now := time.Now()
today := now.Format("2006-01-02")
if user.LastActiveDate != nil && user.LastActiveDate.Format("2006-01-02") == today {
return &dto.CheckInResult{StreakCount: user.StreakCount, IsNewToday: false}, nil
}
newStreak := 1
if user.LastActiveDate != nil {
yesterday := now.AddDate(0, 0, -1).Format("2006-01-02")
if user.LastActiveDate.Format("2006-01-02") == yesterday {
newStreak = user.StreakCount + 1
}
}
if err := s.userRepo.UpdateFields(user, map[string]interface{}{
"streak_count": newStreak,
"last_active_date": now,
}); err != nil {
return nil, err
}
user.StreakCount = newStreak
user.LastActiveDate = &now
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})
}
// UpdatePublicKey کلید عمومی X25519 کاربر رو برای رمزنگاری سرتاسری پیام‌ها
// ثبت/جایگزین می‌کنه. کلید خصوصی متناظرش هیچ‌وقت به سرور نمی‌رسه؛ اینجا فقط
// طول کلید خام (باید دقیقاً ۳۲ بایت باشه) بعد از decode چک می‌شه، نه محتواش.
func (s *ProfileService) UpdatePublicKey(user *models.User, publicKeyB64 string) error {
raw, err := base64.StdEncoding.DecodeString(publicKeyB64)
if err != nil || len(raw) != 32 {
return ErrInvalidPublicKey
}
if err := s.userRepo.UpdateFields(user, map[string]interface{}{"public_key": publicKeyB64}); err != nil {
return err
}
user.PublicKey = &publicKeyB64
return nil
}