Files
jiang13-forum/services/bounty.go
freefire 3f50316ad0 fix: 完成 Gitea 目录改组收尾(import、构建与 LICENSE)
同步包路径与路由,去掉 SPA 构建步骤,对齐 Gitea 式 LICENSE,并更新规格/规则与占位 SSR。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-29 04:01:20 +08:00

151 lines
4.6 KiB
Go

package services
import (
"errors"
"git.iioio.com/freefire/jiang13-forum/models"
"gorm.io/gorm"
)
var (
ErrBountyNotOpen = errors.New("悬赏已结束或已退回")
ErrBountySelfAward = errors.New("不能采纳自己的回复")
ErrBountyInvalidPoint = errors.New("悬赏积分至少为 1")
ErrBountyRefundBlocked = errors.New("已有用户回复,无法自行取消悬赏,请采纳优质回复或联系管理员")
)
const bountyRefundBlockReason = "已有用户回复,无法自行取消悬赏,请采纳优质回复或联系管理员"
// CountEligibleBountyReplies 统计他人已发布的有效回复数(不含楼主)
func CountEligibleBountyReplies(db *gorm.DB, postID, authorID uint) (int64, error) {
if db == nil {
db = models.DB
}
var n int64
err := db.Model(&models.Comment{}).
Where("post_id = ? AND status = ? AND user_id != ?", postID, models.ContentStatusPublished, authorID).
Count(&n).Error
return n, err
}
// CanRefundBounty 当前查看者是否可取消悬赏(管理员始终可强制取消)
func CanRefundBounty(post *models.Post, viewerIsAdmin bool) (bool, string) {
if post == nil || post.PostType != models.PostTypeBounty {
return false, ""
}
if post.BountyStatus != models.BountyStatusOpen || post.BountyPoints < 1 {
return false, ""
}
if viewerIsAdmin {
return true, ""
}
n, err := CountEligibleBountyReplies(models.DB, post.ID, post.UserID)
if err != nil {
return false, ""
}
if n > 0 {
return false, bountyRefundBlockReason
}
return true, ""
}
// EscrowBounty 发帖时托管悬赏积分
func EscrowBounty(tx *gorm.DB, userID, postID uint, points int) error {
if points < 1 {
return ErrBountyInvalidPoint
}
_, err := AdjustPointsTx(tx, userID, -points, models.PointReasonBountyEscrow, "post", postID, "发布悬赏帖")
return err
}
// AwardBounty 采纳评论并发放悬赏
func AwardBounty(postID, operatorID uint, isAdmin bool, commentID uint) error {
var post models.Post
if err := models.DB.First(&post, postID).Error; err != nil {
return ErrPostNotFound
}
if post.PostType != models.PostTypeBounty {
return errors.New("非悬赏帖")
}
if !isAdmin && post.UserID != operatorID {
return ErrPermissionDenied
}
if post.BountyStatus != models.BountyStatusOpen || post.BountyPoints < 1 {
return ErrBountyNotOpen
}
var comment models.Comment
if err := models.DB.First(&comment, commentID).Error; err != nil {
return errors.New("评论不存在")
}
if comment.PostID != postID || comment.Status != models.ContentStatusPublished {
return errors.New("评论无效")
}
if comment.UserID == post.UserID {
return ErrBountySelfAward
}
points := post.BountyPoints
return models.DB.Transaction(func(tx *gorm.DB) error {
if _, err := AdjustPointsTx(tx, comment.UserID, points, models.PointReasonBountyAward, "post", postID, "悬赏采纳"); err != nil {
return err
}
return tx.Model(&post).Updates(map[string]interface{}{
"bounty_status": models.BountyStatusAwarded,
"bounty_comment_id": commentID,
}).Error
})
}
// RefundBounty 取消悬赏并退回积分
func RefundBounty(postID, operatorID uint, isAdmin bool) error {
var post models.Post
if err := models.DB.First(&post, postID).Error; err != nil {
return ErrPostNotFound
}
if post.PostType != models.PostTypeBounty {
return errors.New("非悬赏帖")
}
if !isAdmin && post.UserID != operatorID {
return ErrPermissionDenied
}
if post.BountyStatus != models.BountyStatusOpen || post.BountyPoints < 1 {
return ErrBountyNotOpen
}
if !isAdmin {
n, err := CountEligibleBountyReplies(models.DB, post.ID, post.UserID)
if err != nil {
return err
}
if n > 0 {
return ErrBountyRefundBlocked
}
}
points := post.BountyPoints
return models.DB.Transaction(func(tx *gorm.DB) error {
if _, err := AdjustPointsTx(tx, post.UserID, points, models.PointReasonBountyRefund, "post", postID, "悬赏退回"); err != nil {
return err
}
return tx.Model(&post).Updates(map[string]interface{}{
"bounty_status": models.BountyStatusRefunded,
"bounty_points": 0,
}).Error
})
}
// RefundBountyIfOpen 删帖时自动退回未采纳悬赏
func RefundBountyIfOpen(tx *gorm.DB, post *models.Post) error {
if post == nil || post.PostType != models.PostTypeBounty {
return nil
}
if post.BountyStatus != models.BountyStatusOpen || post.BountyPoints < 1 {
return nil
}
points := post.BountyPoints
if _, err := AdjustPointsTx(tx, post.UserID, points, models.PointReasonBountyRefund, "post", post.ID, "删帖退回悬赏"); err != nil {
return err
}
return tx.Model(post).Updates(map[string]interface{}{
"bounty_status": models.BountyStatusRefunded,
"bounty_points": 0,
}).Error
}