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 }