| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318 |
- 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)
- // 更新红包信息
- updates := map[string]interface{}{
- "grabbed_count": packet.GrabbedCount + 1,
- "grabbed_amount": packet.GrabbedAmount.Add(amount),
- "remain_count": packet.RemainCount - 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: packet.GrabbedCount + 1,
- GrabbedAt: time.Now(),
- }
- if err := tx.Create(record).Error; err != nil {
- return fmt.Errorf("创建记录失败: %w", err)
- }
- // 增加用户余额(从冻结转为可用),userId=0 时跳过(Telegram用户未绑定平台账号)
- if userId > 0 {
- if err := s.ActionAsset(tx, userId, packet.Symbol, amount, amount.Neg()); err != nil {
- return fmt.Errorf("增加余额失败: %w", err)
- }
- }
- // 如果抢完了,更新红包状态
- if packet.RemainCount == 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
- }
|