package services import ( "app/commons/core/redisclient" "app/commons/model/entity" "crypto/rand" "encoding/hex" "errors" "fmt" "github.com/shopspring/decimal" "gorm.io/gorm" "math/big" "time" ) // RedPacketService 红包服务 type RedPacketService struct { CommonService } // GeneratePacketNo 生成红包编号 func (s *RedPacketService) GeneratePacketNo() string { b := make([]byte, 16) rand.Read(b) return fmt.Sprintf("RP%d%s", time.Now().Unix(), hex.EncodeToString(b)[:16]) } // CreateRedPacket 创建红包 func (s *RedPacketService) CreateRedPacket( userId int64, groupId string, packetType int, totalAmount decimal.Decimal, totalCount int, symbol string, blessingWords string, ) (*entity.TgRedPacket, error) { // 参数校验 if totalAmount.LessThanOrEqual(decimal.Zero) { return nil, errors.New("红包金额必须大于0") } if totalCount <= 0 || totalCount > 100 { return nil, errors.New("红包个数必须在1-100之间") } if packetType != 1 && packetType != 2 { return nil, errors.New("红包类型错误") } // 管理员发红包(userId=0)跳过资产检查和扣款 isAdmin := userId == 0 if !isAdmin { // 检查用户资产 userAsset, err := s.GetAssetBySymbol(userId, symbol) if err != nil { return nil, errors.New("获取资产失败") } if userAsset.Balance.LessThan(totalAmount) { return nil, errors.New("余额不足") } } // 开始事务 var packet *entity.TgRedPacket err := s.DB().Transaction(func(tx *gorm.DB) error { // 扣除用户余额(转为冻结),管理员跳过 if !isAdmin { if err := s.ActionAsset(tx, userId, symbol, totalAmount.Neg(), totalAmount); err != nil { return fmt.Errorf("扣除余额失败: %w", err) } } // 创建红包记录 packet = &entity.TgRedPacket{ PacketNo: s.GeneratePacketNo(), UserId: userId, GroupId: groupId, PacketType: packetType, TotalAmount: totalAmount, TotalCount: totalCount, GrabbedCount: 0, GrabbedAmount: decimal.Zero, RemainCount: totalCount, RemainAmount: totalAmount, Symbol: symbol, BlessingWords: blessingWords, Status: 1, // 进行中 ExpireAt: time.Now().Add(24 * time.Hour), } if err := tx.Create(packet).Error; err != nil { return fmt.Errorf("创建红包失败: %w", err) } return nil }) if err != nil { return nil, err } return packet, nil } // GrabRedPacket 抢红包 func (s *RedPacketService) GrabRedPacket( packetNo string, userId int64, telegramId int64, telegramUsername string, ) (*entity.TgRedPacketRecord, error) { // 分布式锁(防止并发抢同一个红包) lockKey := fmt.Sprintf("grab_redpacket:%s", packetNo) lock, err := redisclient.LockWithTime(lockKey, 5) if err != nil { return nil, errors.New("系统繁忙,请稍后重试") } defer redisclient.UnlockSafe(lock) // 获取红包信息 var packet entity.TgRedPacket if err := s.DB().Where("packet_no = ?", packetNo).First(&packet).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errors.New("红包不存在") } return nil, err } // 检查红包状态 if packet.Status != 1 { return nil, errors.New("红包已结束") } if time.Now().After(packet.ExpireAt) { return nil, errors.New("红包已过期") } if packet.RemainCount <= 0 { return nil, errors.New("红包已抢完") } // 检查是否已经抢过(优先用telegramId判断,兼容未绑定平台账号的用户) var count int64 if telegramId > 0 { s.DB().Model(&entity.TgRedPacketRecord{}). Where("packet_id = ? AND telegram_id = ?", packet.Id, telegramId). Count(&count) } else { s.DB().Model(&entity.TgRedPacketRecord{}). Where("packet_id = ? AND user_id = ?", packet.Id, userId). Count(&count) } if count > 0 { return nil, errors.New("您已经抢过这个红包了") } // 开始事务 var record *entity.TgRedPacketRecord err = s.DB().Transaction(func(tx *gorm.DB) error { // 计算抢到的金额 amount := s.CalculateGrabAmount(&packet) // 先保存原始值(GORM Updates 会回写到 packet 对象,导致后续判断用到更新后的值) origGrabbedCount := packet.GrabbedCount origRemainCount := packet.RemainCount // 更新红包信息 updates := map[string]interface{}{ "grabbed_count": origGrabbedCount + 1, "grabbed_amount": packet.GrabbedAmount.Add(amount), "remain_count": origRemainCount - 1, "remain_amount": packet.RemainAmount.Sub(amount), } if err := tx.Model(&packet).Updates(updates).Error; err != nil { return fmt.Errorf("更新红包失败: %w", err) } // 创建抢红包记录 record = &entity.TgRedPacketRecord{ PacketId: packet.Id, PacketNo: packet.PacketNo, UserId: userId, TelegramId: telegramId, TelegramUsername: telegramUsername, Amount: amount, Sequence: int(origGrabbedCount) + 1, GrabbedAt: time.Now(), } if err := tx.Create(record).Error; err != nil { return fmt.Errorf("创建记录失败: %w", err) } // 增加用户余额,userId=0 时跳过(Telegram用户未绑定平台账号) if userId > 0 { // 确保用户有资产记录(不存在则创建) var assetCount int64 tx.Model(&entity.Asset{}).Where("user_id = ? AND symbol = ?", userId, packet.Symbol).Count(&assetCount) if assetCount == 0 { newAsset := &entity.Asset{ UserId: userId, Symbol: packet.Symbol, Balance: decimal.Zero, Frozen: decimal.Zero, } if err := tx.Create(newAsset).Error; err != nil { return fmt.Errorf("创建资产记录失败: %w", err) } } if err := s.ActionAsset(tx, userId, packet.Symbol, amount, decimal.Zero); err != nil { return fmt.Errorf("增加余额失败: %w", err) } } // 如果抢完了,更新红包状态(使用原始值判断,因为 GORM Updates 已回写 packet) if origRemainCount == 1 { now := time.Now() if err := tx.Model(&packet).Updates(map[string]interface{}{ "status": 2, // 已抢完 "completed_at": &now, }).Error; err != nil { return fmt.Errorf("更新完成状态失败: %w", err) } // 异步计算手气最佳 go s.CalculateBestLuck(packet.Id) } return nil }) if err != nil { return nil, err } return record, nil } // CalculateGrabAmount 计算抢红包金额 func (s *RedPacketService) CalculateGrabAmount(packet *entity.TgRedPacket) decimal.Decimal { // 最后一个红包:剩余所有金额 if packet.RemainCount == 1 { return packet.RemainAmount } // 普通红包:平均分配 if packet.PacketType == 1 { return packet.RemainAmount.Div(decimal.NewFromInt(int64(packet.RemainCount))) } // 手气红包:二倍均值算法 if packet.PacketType == 2 { // 平均值 avg := packet.RemainAmount.Div(decimal.NewFromInt(int64(packet.RemainCount))) // 最大值 = 平均值 * 2 maxAmount := avg.Mul(decimal.NewFromInt(2)) // 最小值:0.01 minAmount := decimal.NewFromFloat(0.01) // 随机金额:minAmount ~ maxAmount randomAmount := s.randomDecimal(minAmount, maxAmount) // 确保不超过剩余金额 if randomAmount.GreaterThan(packet.RemainAmount) { return packet.RemainAmount } return randomAmount } // 默认平均分配 return packet.RemainAmount.Div(decimal.NewFromInt(int64(packet.RemainCount))) } // randomDecimal 生成随机金额 func (s *RedPacketService) randomDecimal(min, max decimal.Decimal) decimal.Decimal { // 计算范围 diff := max.Sub(min) // 生成随机数 0-1 randomBig, _ := rand.Int(rand.Reader, big.NewInt(10000)) randomFloat := decimal.NewFromBigInt(randomBig, -4) // 0.0000 - 0.9999 // 计算随机金额 amount := min.Add(diff.Mul(randomFloat)) // 保留2位小数 return amount.Round(2) } // CalculateBestLuck 计算手气最佳 func (s *RedPacketService) CalculateBestLuck(packetId int64) { // 查询所有记录 var records []entity.TgRedPacketRecord s.DB().Where("packet_id = ?", packetId).Order("amount DESC").Find(&records) if len(records) == 0 { return } // 标记手气最佳(金额最大的) bestRecord := records[0] s.DB().Model(&entity.TgRedPacketRecord{}). Where("id = ?", bestRecord.Id). Update("is_best", 1) } // GetRedPacketDetail 获取红包详情 func (s *RedPacketService) GetRedPacketDetail(packetNo string) (*entity.TgRedPacket, []entity.TgRedPacketRecord, error) { var packet entity.TgRedPacket if err := s.DB().Where("packet_no = ?", packetNo).First(&packet).Error; err != nil { return nil, nil, err } var records []entity.TgRedPacketRecord s.DB().Where("packet_id = ?", packet.Id).Order("grabbed_at ASC").Find(&records) return &packet, records, nil } // GetUserRedPacketRecords 获取用户的红包记录 func (s *RedPacketService) GetUserRedPacketRecords(userId int64, page, pageSize int) ([]entity.TgRedPacketRecord, int64, error) { var records []entity.TgRedPacketRecord var total int64 db := s.DB().Model(&entity.TgRedPacketRecord{}).Where("user_id = ?", userId) db.Count(&total) offset := (page - 1) * pageSize if err := db.Order("grabbed_at DESC").Offset(offset).Limit(pageSize).Find(&records).Error; err != nil { return nil, 0, err } return records, total, nil }