Files
SproutGate/sproutgate-backend/internal/storage/profile_likes.go

220 lines
6.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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(&quota).Error
if errors.Is(qErr, gorm.ErrRecordNotFound) {
quota = DBProfileLikeDailyQuota{
LikerAccount: likerCanon,
LikeDate: today,
Remaining: MaxProfileLikesPerDay,
UpdatedAt: models.NowISO(),
}
if cerr := tx.Create(&quota).Error; cerr != nil {
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where("liker_account = ? AND like_date = ?", likerCanon, today).
First(&quota).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(&quota).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)
}