package storage import ( "errors" "strings" "gorm.io/gorm" "gorm.io/gorm/clause" "sproutgate-backend/internal/models" ) // MaxProfileLikesPerDay 每个点赞者每个自然日最多给多少位不同用户的公开主页点赞。 const MaxProfileLikesPerDay = 5 var ( ErrCannotLikeOwnProfile = errors.New("cannot like own profile") ErrAlreadyLikedToday = errors.New("already liked today") ErrDailyLikeLimitReached = errors.New("daily like limit reached") ErrProfileLikeTarget = errors.New("user not found") ErrProfileLikeLiker = errors.New("invalid liker") ) // DBProfileLikeDailyQuota 点赞者当日剩余可点赞人数(与 profile_likes 计数同步;新自然日新行即视为刷新为满额)。 type DBProfileLikeDailyQuota struct { LikerAccount string `gorm:"primaryKey;column:liker_account;size:255"` LikeDate string `gorm:"primaryKey;column:like_date;size:10"` Remaining int `gorm:"column:remaining;not null"` UpdatedAt string `gorm:"column:updated_at;size:50"` } func (DBProfileLikeDailyQuota) TableName() string { return "profile_like_daily_quota" } // DBProfileLike 记录「谁在某日给谁的主页点赞」(每人每天对同一主页最多一条)。 type DBProfileLike struct { LikerAccount string `gorm:"primaryKey;column:liker_account;size:255"` LikedAccount string `gorm:"primaryKey;column:liked_account;size:255"` LikeDate string `gorm:"primaryKey;column:like_date;size:10"` // YYYY-MM-DD,与签到同历 CreatedAt string `gorm:"column:created_at;size:50"` } func (DBProfileLike) TableName() string { return "profile_likes" } // CountProfileLikes 主页累计获赞次数(历史所有日的点赞条数之和)。 func (s *Store) CountProfileLikes(likedAccount string) (int64, error) { likedAccount = strings.TrimSpace(likedAccount) if likedAccount == "" { return 0, nil } var n int64 if err := s.db.Model(&DBProfileLike{}).Where("liked_account = ?", likedAccount).Count(&n).Error; err != nil { return 0, err } return n, nil } // ViewerHasLikedProfileToday 当前自然日(Asia/Shanghai)内是否已为该主页点过赞。 func (s *Store) ViewerHasLikedProfileToday(likerAccount, likedAccount string) (bool, error) { likerAccount = strings.TrimSpace(likerAccount) likedAccount = strings.TrimSpace(likedAccount) if likerAccount == "" || likedAccount == "" { return false, nil } today := models.CurrentActivityDate() var n int64 err := s.db.Model(&DBProfileLike{}). Where("liker_account = ? AND liked_account = ? AND like_date = ?", likerAccount, likedAccount, today). Count(&n).Error return n > 0, err } // ProfileLikeRemainingToday 当日还可给多少人点赞(根据 profile_likes 计数计算,并与 quota 表对齐)。 func (s *Store) ProfileLikeRemainingToday(likerAccount string) (int, error) { likerAccount = strings.TrimSpace(likerAccount) if likerAccount == "" { return 0, nil } today := models.CurrentActivityDate() var used int64 if err := s.db.Model(&DBProfileLike{}). Where("liker_account = ? AND like_date = ?", likerAccount, today). Count(&used).Error; err != nil { return 0, err } rem := MaxProfileLikesPerDay - int(used) if rem < 0 { rem = 0 } var q DBProfileLikeDailyQuota err := s.db.Where("liker_account = ? AND like_date = ?", likerAccount, today).First(&q).Error if errors.Is(err, gorm.ErrRecordNotFound) { return rem, nil } if err != nil { return 0, err } if q.Remaining != rem { _ = s.db.Model(&DBProfileLikeDailyQuota{}). Where("liker_account = ? AND like_date = ?", likerAccount, today). Updates(map[string]interface{}{ "remaining": rem, "updated_at": models.NowISO(), }).Error } return rem, nil } // AddProfileLike 为公开主页点赞(每人每天每主页一次;每人每天最多给 MaxProfileLikesPerDay 位用户点赞)。返回新的累计赞数。 func (s *Store) AddProfileLike(likerAccount, likedAccount string) (int64, error) { likerAccount = strings.TrimSpace(likerAccount) likedAccount = strings.TrimSpace(likedAccount) if likerAccount == "" || likedAccount == "" { return 0, errors.New("account is required") } if strings.EqualFold(likerAccount, likedAccount) { return 0, ErrCannotLikeOwnProfile } likedUser, found, err := s.GetUser(likedAccount) if err != nil { return 0, err } if !found || likedUser.Banned { return 0, ErrProfileLikeTarget } likerUser, foundL, err := s.GetUser(likerAccount) if err != nil { return 0, err } if !foundL || likerUser.Banned { return 0, ErrProfileLikeLiker } today := models.CurrentActivityDate() likerCanon := likerUser.Account likedCanon := likedUser.Account row := DBProfileLike{ LikerAccount: likerCanon, LikedAccount: likedCanon, LikeDate: today, CreatedAt: models.NowISO(), } txErr := s.db.Transaction(func(tx *gorm.DB) error { var quota DBProfileLikeDailyQuota qErr := tx.Clauses(clause.Locking{Strength: "UPDATE"}). Where("liker_account = ? AND like_date = ?", likerCanon, today). First("a).Error if errors.Is(qErr, gorm.ErrRecordNotFound) { quota = DBProfileLikeDailyQuota{ LikerAccount: likerCanon, LikeDate: today, Remaining: MaxProfileLikesPerDay, UpdatedAt: models.NowISO(), } if cerr := tx.Create("a).Error; cerr != nil { if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). Where("liker_account = ? AND like_date = ?", likerCanon, today). First("a).Error; err != nil { return err } } } else if qErr != nil { return qErr } if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). Where("liker_account = ? AND like_date = ?", likerCanon, today). First("a).Error; err != nil { return err } var used int64 if err := tx.Model(&DBProfileLike{}). Where("liker_account = ? AND like_date = ?", likerCanon, today). Count(&used).Error; err != nil { return err } if int(used) >= MaxProfileLikesPerDay { if err := tx.Model(&DBProfileLikeDailyQuota{}). Where("liker_account = ? AND like_date = ?", likerCanon, today). Updates(map[string]interface{}{ "remaining": 0, "updated_at": models.NowISO(), }).Error; err != nil { return err } return ErrDailyLikeLimitReached } res := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&row) if res.Error != nil { return res.Error } if res.RowsAffected == 0 { return ErrAlreadyLikedToday } var usedAfter int64 if err := tx.Model(&DBProfileLike{}). Where("liker_account = ? AND like_date = ?", likerCanon, today). Count(&usedAfter).Error; err != nil { return err } rem := MaxProfileLikesPerDay - int(usedAfter) if rem < 0 { rem = 0 } return tx.Model(&DBProfileLikeDailyQuota{}). Where("liker_account = ? AND like_date = ?", likerCanon, today). Updates(map[string]interface{}{ "remaining": rem, "updated_at": models.NowISO(), }).Error }) if txErr != nil { return 0, txErr } return s.CountProfileLikes(likedCanon) }