redpacket_service.go 8.4 KB

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