Files
jiang13-bbs/backend/service/points.go
freefire b9ba8cb8c9 feat: 类型帖互动、积分经济与发帖体验
补齐问答采纳/重开、投票匿名与有票禁编、悬赏过期退回、抽奖回帖开奖,并接入积分账本与发帖附件可见性。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-16 03:17:07 +08:00

232 lines
6.6 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 service
import (
"errors"
"time"
"github.com/freefire/jiang13-bbs/model"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
var (
ErrInsufficientPoints = errors.New("积分不足")
ErrInvalidPoints = errors.New("积分数量无效")
)
// PointsService 用户积分账户
type PointsService struct {
db *gorm.DB
}
func NewPointsService(db *gorm.DB) *PointsService {
return &PointsService{db: db}
}
// Balance 查询可用积分
func (s *PointsService) Balance(userID uint) (int, error) {
var u model.User
if err := s.db.Select("id", "points").First(&u, userID).Error; err != nil {
return 0, err
}
return u.Points, nil
}
// CreditTx 在事务内入账
func CreditTx(tx *gorm.DB, userID uint, delta int, reason, refType string, refID uint, note string) (int, error) {
if delta <= 0 {
return 0, ErrInvalidPoints
}
var u model.User
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Select("id", "points").First(&u, userID).Error; err != nil {
return 0, err
}
bal := u.Points + delta
if err := tx.Model(&model.User{}).Where("id = ?", userID).Update("points", bal).Error; err != nil {
return 0, err
}
if err := tx.Create(&model.PointLedger{
UserID: userID,
Delta: delta,
Balance: bal,
Reason: reason,
RefType: refType,
RefID: refID,
Note: note,
}).Error; err != nil {
return 0, err
}
return bal, nil
}
// DebitTx 在事务内扣款
func DebitTx(tx *gorm.DB, userID uint, delta int, reason, refType string, refID uint, note string) (int, error) {
if delta <= 0 {
return 0, ErrInvalidPoints
}
var u model.User
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Select("id", "points").First(&u, userID).Error; err != nil {
return 0, err
}
if u.Points < delta {
return 0, ErrInsufficientPoints
}
bal := u.Points - delta
if err := tx.Model(&model.User{}).Where("id = ?", userID).Update("points", bal).Error; err != nil {
return 0, err
}
if err := tx.Create(&model.PointLedger{
UserID: userID,
Delta: -delta,
Balance: bal,
Reason: reason,
RefType: refType,
RefID: refID,
Note: note,
}).Error; err != nil {
return 0, err
}
return bal, nil
}
// Credit 入账
func (s *PointsService) Credit(userID uint, delta int, reason, refType string, refID uint, note string) (int, error) {
var bal int
err := s.db.Transaction(func(tx *gorm.DB) error {
var e error
bal, e = CreditTx(tx, userID, delta, reason, refType, refID, note)
return e
})
return bal, err
}
// Debit 扣款
func (s *PointsService) Debit(userID uint, delta int, reason, refType string, refID uint, note string) (int, error) {
var bal int
err := s.db.Transaction(func(tx *gorm.DB) error {
var e error
bal, e = DebitTx(tx, userID, delta, reason, refType, refID, note)
return e
})
return bal, err
}
// LedgerItem 流水展示项
type LedgerItem struct {
ID uint `json:"id"`
Delta int `json:"delta"`
Balance int `json:"balance"`
Reason string `json:"reason"`
RefType string `json:"ref_type"`
RefID uint `json:"ref_id"`
Note string `json:"note"`
CreatedAt time.Time `json:"created_at"`
}
// Ledger 分页查询本人积分流水
func (s *PointsService) Ledger(userID uint, page, size int) ([]LedgerItem, int64, error) {
if page < 1 {
page = 1
}
if size < 1 || size > 50 {
size = 20
}
var total int64
q := s.db.Model(&model.PointLedger{}).Where("user_id = ?", userID)
if err := q.Count(&total).Error; err != nil {
return nil, 0, err
}
var rows []model.PointLedger
if err := q.Order("id DESC").Offset((page - 1) * size).Limit(size).Find(&rows).Error; err != nil {
return nil, 0, err
}
out := make([]LedgerItem, 0, len(rows))
for _, r := range rows {
out = append(out, LedgerItem{
ID: r.ID, Delta: r.Delta, Balance: r.Balance, Reason: r.Reason,
RefType: r.RefType, RefID: r.RefID, Note: r.Note, CreatedAt: r.CreatedAt,
})
}
return out, total, nil
}
// PointsEconomyStats 全站积分与类型帖概览(管理端)
type PointsEconomyStats struct {
TotalBalance int64 `json:"total_balance"` // 用户余额合计
LedgerCount int64 `json:"ledger_count"` // 流水条数
CheckinToday int64 `json:"checkin_today"` // 今日签到人数
OpenBounties int64 `json:"open_bounties"` // 未结算悬赏帖
EscrowedPoints int64 `json:"escrowed_points"` // 托管中悬赏积分(估算)
PostsByType map[string]int64 `json:"posts_by_type"` // 各类型帖数量
RecentLedger []LedgerItem `json:"recent_ledger"` // 最近全局流水(脱敏 note)
}
// AdminEconomyStats 管理端经济看板
func (s *PointsService) AdminEconomyStats() (*PointsEconomyStats, error) {
st := &PointsEconomyStats{PostsByType: map[string]int64{}}
if err := s.db.Model(&model.User{}).Where("deleted_at IS NULL").
Select("COALESCE(SUM(points),0)").Scan(&st.TotalBalance).Error; err != nil {
return nil, err
}
if err := s.db.Model(&model.PointLedger{}).Count(&st.LedgerCount).Error; err != nil {
return nil, err
}
today := time.Now().Truncate(24 * time.Hour)
// 用日期字符串更稳妥(与 checkin 一致用 date)
if err := s.db.Model(&model.Checkin{}).
Where("checkin_date::date = CURRENT_DATE").
Count(&st.CheckinToday).Error; err != nil {
return nil, err
}
_ = today
type row struct {
PostType string
Cnt int64
}
var rows []row
if err := s.db.Model(&model.Post{}).
Select("post_type, count(*) as cnt").
Where("deleted_at IS NULL").
Group("post_type").Scan(&rows).Error; err != nil {
return nil, err
}
for _, r := range rows {
pt := model.NormalizePostType(r.PostType)
st.PostsByType[pt] += r.Cnt
}
// 未结算悬赏:type_meta 含 escrowed true 且未 accepted/refunded —— 用简易扫描估算
var bountyPosts []model.Post
if err := s.db.Select("id, type_meta").
Where("post_type = ? AND deleted_at IS NULL", model.PostTypeBounty).
Find(&bountyPosts).Error; err != nil {
return nil, err
}
for _, p := range bountyPosts {
m, err := parseBountyMeta(p.TypeMeta)
if err != nil {
continue
}
if m.Escrowed && m.AcceptedCommentID == 0 && !m.Refunded && !m.Expired {
st.OpenBounties++
st.EscrowedPoints += int64(m.Points)
}
}
var recent []model.PointLedger
if err := s.db.Order("id DESC").Limit(15).Find(&recent).Error; err != nil {
return nil, err
}
st.RecentLedger = make([]LedgerItem, 0, len(recent))
for _, r := range recent {
st.RecentLedger = append(st.RecentLedger, LedgerItem{
ID: r.ID, Delta: r.Delta, Balance: r.Balance, Reason: r.Reason,
RefType: r.RefType, RefID: r.RefID, Note: r.Note, CreatedAt: r.CreatedAt,
})
}
return st, nil
}