package user import ( "context" "errors" "time" "bindbox-game/internal/repository/mysql/dao" "bindbox-game/internal/repository/mysql/model" "gorm.io/gorm/clause" ) // ConsumePointsFor 扣减用户积分(通用) // 功能:按有效期优先扣减用户积分,并记录积分流水到 `user_points_ledger` // 参数: // - ctx:上下文 // - userID:用户ID // - points:扣减积分数量 // - refTable:业务关联表名(例如 products、system_coupons) // - refID:业务关联ID字符串(例如 product_id、coupon_id) // - remark:备注信息 // - action:流水动作标识(例如 redeem_product、redeem_coupon) // // 返回:ledgerID(积分流水ID),error(不足或写入失败时) func (s *service) ConsumePointsFor(ctx context.Context, userID int64, points int64, refTable string, refID string, remark string, action string) (int64, error) { if points <= 0 { return 0, nil } var ledgerID int64 err := s.writeDB.Transaction(func(tx *dao.Query) error { rows, err := tx.UserPoints.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).Where(tx.UserPoints.UserID.Eq(userID)).Order(tx.UserPoints.ValidEnd.Asc()).Find() if err != nil { return err } remain := points now := time.Now() for _, r := range rows { if !r.ValidEnd.IsZero() && r.ValidEnd.Before(now) { continue } if remain <= 0 { break } use := r.Points if use > remain { use = remain } _, err := tx.UserPoints.WithContext(ctx).Where(tx.UserPoints.ID.Eq(r.ID)).Updates(map[string]any{"points": r.Points - use}) if err != nil { return err } remain -= use } if remain > 0 { return errors.New("insufficient_points") } led := &model.UserPointsLedger{UserID: userID, Action: action, Points: -points, RefTable: refTable, RefID: refID, Remark: remark} if err := tx.UserPointsLedger.WithContext(ctx).Create(led); err != nil { return err } ledgerID = led.ID return nil }) return ledgerID, err } // ConsumePointsForRefund 扣减用户积分(退款回收场景,支持扣完为止) // 返回:ledgerID(流水ID), actualConsumed(实际扣除数量), error func (s *service) ConsumePointsForRefund(ctx context.Context, userID int64, points int64, refTable string, refID string, remark string) (int64, int64, error) { if points <= 0 { return 0, 0, nil } var ledgerID int64 var actualConsumed int64 err := s.writeDB.Transaction(func(tx *dao.Query) error { rows, err := tx.UserPoints.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).Where(tx.UserPoints.UserID.Eq(userID)).Order(tx.UserPoints.ValidEnd.Asc()).Find() if err != nil { return err } remain := points now := time.Now() for _, r := range rows { if !r.ValidEnd.IsZero() && r.ValidEnd.Before(now) { continue } if remain <= 0 { break } use := r.Points if use > remain { use = remain } _, err := tx.UserPoints.WithContext(ctx).Where(tx.UserPoints.ID.Eq(r.ID)).Updates(map[string]any{"points": r.Points - use}) if err != nil { return err } remain -= use } actualConsumed = points - remain if actualConsumed > 0 { led := &model.UserPointsLedger{ UserID: userID, Action: "refund_reclaim", Points: -actualConsumed, RefTable: refTable, RefID: refID, Remark: remark, } if err := tx.UserPointsLedger.WithContext(ctx).Create(led); err != nil { return err } ledgerID = led.ID } return nil }) return ledgerID, actualConsumed, err }