redpacket_service.go 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. package services
  2. import (
  3. "app/commons/core/redisclient"
  4. "app/commons/model/entity"
  5. "crypto/rand"
  6. "encoding/hex"
  7. "errors"
  8. "fmt"
  9. "github.com/shopspring/decimal"
  10. "gorm.io/gorm"
  11. "math/big"
  12. "time"
  13. )
  14. // RedPacketService 红包服务
  15. type RedPacketService struct {
  16. CommonService
  17. }
  18. // GeneratePacketNo 生成红包编号
  19. func (s *RedPacketService) GeneratePacketNo() string {
  20. b := make([]byte, 16)
  21. rand.Read(b)
  22. return fmt.Sprintf("RP%d%s", time.Now().Unix(), hex.EncodeToString(b)[:16])
  23. }
  24. // CreateRedPacket 创建红包
  25. func (s *RedPacketService) CreateRedPacket(
  26. userId int64,
  27. groupId string,
  28. packetType int,
  29. totalAmount decimal.Decimal,
  30. totalCount int,
  31. symbol string,
  32. blessingWords string,
  33. ) (*entity.TgRedPacket, error) {
  34. // 参数校验
  35. if totalAmount.LessThanOrEqual(decimal.Zero) {
  36. return nil, errors.New("红包金额必须大于0")
  37. }
  38. if totalCount <= 0 || totalCount > 100 {
  39. return nil, errors.New("红包个数必须在1-100之间")
  40. }
  41. if packetType != 1 && packetType != 2 {
  42. return nil, errors.New("红包类型错误")
  43. }
  44. // 检查用户资产
  45. userAsset, err := s.GetAssetBySymbol(userId, symbol)
  46. if err != nil {
  47. return nil, errors.New("获取资产失败")
  48. }
  49. if userAsset.Balance.LessThan(totalAmount) {
  50. return nil, errors.New("余额不足")
  51. }
  52. // 开始事务
  53. var packet *entity.TgRedPacket
  54. err = s.DB().Transaction(func(tx *gorm.DB) error {
  55. // 扣除用户余额(转为冻结)
  56. if err := s.ActionAsset(tx, userId, symbol, totalAmount.Neg(), totalAmount); err != nil {
  57. return fmt.Errorf("扣除余额失败: %w", err)
  58. }
  59. // 创建红包记录
  60. packet = &entity.TgRedPacket{
  61. PacketNo: s.GeneratePacketNo(),
  62. UserId: userId,
  63. GroupId: groupId,
  64. PacketType: packetType,
  65. TotalAmount: totalAmount,
  66. TotalCount: totalCount,
  67. GrabbedCount: 0,
  68. GrabbedAmount: decimal.Zero,
  69. RemainCount: totalCount,
  70. RemainAmount: totalAmount,
  71. Symbol: symbol,
  72. BlessingWords: blessingWords,
  73. Status: 1, // 进行中
  74. ExpireAt: time.Now().Add(24 * time.Hour),
  75. }
  76. if err := tx.Create(packet).Error; err != nil {
  77. return fmt.Errorf("创建红包失败: %w", err)
  78. }
  79. return nil
  80. })
  81. if err != nil {
  82. return nil, err
  83. }
  84. return packet, nil
  85. }
  86. // GrabRedPacket 抢红包
  87. func (s *RedPacketService) GrabRedPacket(
  88. packetNo string,
  89. userId int64,
  90. telegramId int64,
  91. telegramUsername string,
  92. ) (*entity.TgRedPacketRecord, error) {
  93. // 分布式锁(防止并发抢同一个红包)
  94. lockKey := fmt.Sprintf("grab_redpacket:%s", packetNo)
  95. lock, err := redisclient.LockWithTime(lockKey, 5)
  96. if err != nil {
  97. return nil, errors.New("系统繁忙,请稍后重试")
  98. }
  99. defer redisclient.UnlockSafe(lock)
  100. // 获取红包信息
  101. var packet entity.TgRedPacket
  102. if err := s.DB().Where("packet_no = ?", packetNo).First(&packet).Error; err != nil {
  103. if errors.Is(err, gorm.ErrRecordNotFound) {
  104. return nil, errors.New("红包不存在")
  105. }
  106. return nil, err
  107. }
  108. // 检查红包状态
  109. if packet.Status != 1 {
  110. return nil, errors.New("红包已结束")
  111. }
  112. if time.Now().After(packet.ExpireAt) {
  113. return nil, errors.New("红包已过期")
  114. }
  115. if packet.RemainCount <= 0 {
  116. return nil, errors.New("红包已抢完")
  117. }
  118. // 检查是否已经抢过
  119. var count int64
  120. s.DB().Model(&entity.TgRedPacketRecord{}).
  121. Where("packet_id = ? AND user_id = ?", packet.Id, userId).
  122. Count(&count)
  123. if count > 0 {
  124. return nil, errors.New("您已经抢过这个红包了")
  125. }
  126. // 开始事务
  127. var record *entity.TgRedPacketRecord
  128. err = s.DB().Transaction(func(tx *gorm.DB) error {
  129. // 计算抢到的金额
  130. amount := s.CalculateGrabAmount(&packet)
  131. // 更新红包信息
  132. updates := map[string]interface{}{
  133. "grabbed_count": packet.GrabbedCount + 1,
  134. "grabbed_amount": packet.GrabbedAmount.Add(amount),
  135. "remain_count": packet.RemainCount - 1,
  136. "remain_amount": packet.RemainAmount.Sub(amount),
  137. }
  138. if err := tx.Model(&packet).Updates(updates).Error; err != nil {
  139. return fmt.Errorf("更新红包失败: %w", err)
  140. }
  141. // 创建抢红包记录
  142. record = &entity.TgRedPacketRecord{
  143. PacketId: packet.Id,
  144. PacketNo: packet.PacketNo,
  145. UserId: userId,
  146. TelegramId: telegramId,
  147. TelegramUsername: telegramUsername,
  148. Amount: amount,
  149. Sequence: packet.GrabbedCount + 1,
  150. GrabbedAt: time.Now(),
  151. }
  152. if err := tx.Create(record).Error; err != nil {
  153. return fmt.Errorf("创建记录失败: %w", err)
  154. }
  155. // 增加用户余额(从冻结转为可用)
  156. if err := s.ActionAsset(tx, userId, packet.Symbol, amount, amount.Neg()); err != nil {
  157. return fmt.Errorf("增加余额失败: %w", err)
  158. }
  159. // 如果抢完了,更新红包状态
  160. if packet.RemainCount == 1 {
  161. now := time.Now()
  162. if err := tx.Model(&packet).Updates(map[string]interface{}{
  163. "status": 2, // 已抢完
  164. "completed_at": &now,
  165. }).Error; err != nil {
  166. return fmt.Errorf("更新完成状态失败: %w", err)
  167. }
  168. // 异步计算手气最佳
  169. go s.CalculateBestLuck(packet.Id)
  170. }
  171. return nil
  172. })
  173. if err != nil {
  174. return nil, err
  175. }
  176. return record, nil
  177. }
  178. // CalculateGrabAmount 计算抢红包金额
  179. func (s *RedPacketService) CalculateGrabAmount(packet *entity.TgRedPacket) decimal.Decimal {
  180. // 最后一个红包:剩余所有金额
  181. if packet.RemainCount == 1 {
  182. return packet.RemainAmount
  183. }
  184. // 普通红包:平均分配
  185. if packet.PacketType == 1 {
  186. return packet.RemainAmount.Div(decimal.NewFromInt(int64(packet.RemainCount)))
  187. }
  188. // 手气红包:二倍均值算法
  189. if packet.PacketType == 2 {
  190. // 平均值
  191. avg := packet.RemainAmount.Div(decimal.NewFromInt(int64(packet.RemainCount)))
  192. // 最大值 = 平均值 * 2
  193. maxAmount := avg.Mul(decimal.NewFromInt(2))
  194. // 最小值:0.01
  195. minAmount := decimal.NewFromFloat(0.01)
  196. // 随机金额:minAmount ~ maxAmount
  197. randomAmount := s.randomDecimal(minAmount, maxAmount)
  198. // 确保不超过剩余金额
  199. if randomAmount.GreaterThan(packet.RemainAmount) {
  200. return packet.RemainAmount
  201. }
  202. return randomAmount
  203. }
  204. // 默认平均分配
  205. return packet.RemainAmount.Div(decimal.NewFromInt(int64(packet.RemainCount)))
  206. }
  207. // randomDecimal 生成随机金额
  208. func (s *RedPacketService) randomDecimal(min, max decimal.Decimal) decimal.Decimal {
  209. // 计算范围
  210. diff := max.Sub(min)
  211. // 生成随机数 0-1
  212. randomBig, _ := rand.Int(rand.Reader, big.NewInt(10000))
  213. randomFloat := decimal.NewFromBigInt(randomBig, -4) // 0.0000 - 0.9999
  214. // 计算随机金额
  215. amount := min.Add(diff.Mul(randomFloat))
  216. // 保留2位小数
  217. return amount.Round(2)
  218. }
  219. // CalculateBestLuck 计算手气最佳
  220. func (s *RedPacketService) CalculateBestLuck(packetId int64) {
  221. // 查询所有记录
  222. var records []entity.TgRedPacketRecord
  223. s.DB().Where("packet_id = ?", packetId).Order("amount DESC").Find(&records)
  224. if len(records) == 0 {
  225. return
  226. }
  227. // 标记手气最佳(金额最大的)
  228. bestRecord := records[0]
  229. s.DB().Model(&entity.TgRedPacketRecord{}).
  230. Where("id = ?", bestRecord.Id).
  231. Update("is_best", 1)
  232. }
  233. // GetRedPacketDetail 获取红包详情
  234. func (s *RedPacketService) GetRedPacketDetail(packetNo string) (*entity.TgRedPacket, []entity.TgRedPacketRecord, error) {
  235. var packet entity.TgRedPacket
  236. if err := s.DB().Where("packet_no = ?", packetNo).First(&packet).Error; err != nil {
  237. return nil, nil, err
  238. }
  239. var records []entity.TgRedPacketRecord
  240. s.DB().Where("packet_id = ?", packet.Id).Order("grabbed_at ASC").Find(&records)
  241. return &packet, records, nil
  242. }
  243. // GetUserRedPacketRecords 获取用户的红包记录
  244. func (s *RedPacketService) GetUserRedPacketRecords(userId int64, page, pageSize int) ([]entity.TgRedPacketRecord, int64, error) {
  245. var records []entity.TgRedPacketRecord
  246. var total int64
  247. db := s.DB().Model(&entity.TgRedPacketRecord{}).Where("user_id = ?", userId)
  248. db.Count(&total)
  249. offset := (page - 1) * pageSize
  250. if err := db.Order("grabbed_at DESC").Offset(offset).Limit(pageSize).Find(&records).Error; err != nil {
  251. return nil, 0, err
  252. }
  253. return records, total, nil
  254. }