redpacket_service.go 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  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. // 先保存原始值(GORM Updates 会回写到 packet 对象,导致后续判断用到更新后的值)
  144. origGrabbedCount := packet.GrabbedCount
  145. origRemainCount := packet.RemainCount
  146. // 更新红包信息
  147. updates := map[string]interface{}{
  148. "grabbed_count": origGrabbedCount + 1,
  149. "grabbed_amount": packet.GrabbedAmount.Add(amount),
  150. "remain_count": origRemainCount - 1,
  151. "remain_amount": packet.RemainAmount.Sub(amount),
  152. }
  153. if err := tx.Model(&packet).Updates(updates).Error; err != nil {
  154. return fmt.Errorf("更新红包失败: %w", err)
  155. }
  156. // 创建抢红包记录
  157. record = &entity.TgRedPacketRecord{
  158. PacketId: packet.Id,
  159. PacketNo: packet.PacketNo,
  160. UserId: userId,
  161. TelegramId: telegramId,
  162. TelegramUsername: telegramUsername,
  163. Amount: amount,
  164. Sequence: int(origGrabbedCount) + 1,
  165. GrabbedAt: time.Now(),
  166. }
  167. if err := tx.Create(record).Error; err != nil {
  168. return fmt.Errorf("创建记录失败: %w", err)
  169. }
  170. // 增加用户余额,userId=0 时跳过(Telegram用户未绑定平台账号)
  171. if userId > 0 {
  172. // 确保用户有资产记录(不存在则创建)
  173. var assetCount int64
  174. tx.Model(&entity.Asset{}).Where("user_id = ? AND symbol = ?", userId, packet.Symbol).Count(&assetCount)
  175. if assetCount == 0 {
  176. newAsset := &entity.Asset{
  177. UserId: userId,
  178. Symbol: packet.Symbol,
  179. Balance: decimal.Zero,
  180. Frozen: decimal.Zero,
  181. }
  182. if err := tx.Create(newAsset).Error; err != nil {
  183. return fmt.Errorf("创建资产记录失败: %w", err)
  184. }
  185. }
  186. if err := s.ActionAsset(tx, userId, packet.Symbol, amount, decimal.Zero); err != nil {
  187. return fmt.Errorf("增加余额失败: %w", err)
  188. }
  189. }
  190. // 如果抢完了,更新红包状态(使用原始值判断,因为 GORM Updates 已回写 packet)
  191. if origRemainCount == 1 {
  192. now := time.Now()
  193. if err := tx.Model(&packet).Updates(map[string]interface{}{
  194. "status": 2, // 已抢完
  195. "completed_at": &now,
  196. }).Error; err != nil {
  197. return fmt.Errorf("更新完成状态失败: %w", err)
  198. }
  199. // 异步计算手气最佳
  200. go s.CalculateBestLuck(packet.Id)
  201. }
  202. return nil
  203. })
  204. if err != nil {
  205. return nil, err
  206. }
  207. return record, nil
  208. }
  209. // CalculateGrabAmount 计算抢红包金额
  210. func (s *RedPacketService) CalculateGrabAmount(packet *entity.TgRedPacket) decimal.Decimal {
  211. // 最后一个红包:剩余所有金额
  212. if packet.RemainCount == 1 {
  213. return packet.RemainAmount
  214. }
  215. // 普通红包:平均分配
  216. if packet.PacketType == 1 {
  217. return packet.RemainAmount.Div(decimal.NewFromInt(int64(packet.RemainCount)))
  218. }
  219. // 手气红包:二倍均值算法
  220. if packet.PacketType == 2 {
  221. // 平均值
  222. avg := packet.RemainAmount.Div(decimal.NewFromInt(int64(packet.RemainCount)))
  223. // 最大值 = 平均值 * 2
  224. maxAmount := avg.Mul(decimal.NewFromInt(2))
  225. // 最小值:0.01
  226. minAmount := decimal.NewFromFloat(0.01)
  227. // 随机金额:minAmount ~ maxAmount
  228. randomAmount := s.randomDecimal(minAmount, maxAmount)
  229. // 确保不超过剩余金额
  230. if randomAmount.GreaterThan(packet.RemainAmount) {
  231. return packet.RemainAmount
  232. }
  233. return randomAmount
  234. }
  235. // 默认平均分配
  236. return packet.RemainAmount.Div(decimal.NewFromInt(int64(packet.RemainCount)))
  237. }
  238. // randomDecimal 生成随机金额
  239. func (s *RedPacketService) randomDecimal(min, max decimal.Decimal) decimal.Decimal {
  240. // 计算范围
  241. diff := max.Sub(min)
  242. // 生成随机数 0-1
  243. randomBig, _ := rand.Int(rand.Reader, big.NewInt(10000))
  244. randomFloat := decimal.NewFromBigInt(randomBig, -4) // 0.0000 - 0.9999
  245. // 计算随机金额
  246. amount := min.Add(diff.Mul(randomFloat))
  247. // 保留2位小数
  248. return amount.Round(2)
  249. }
  250. // CalculateBestLuck 计算手气最佳
  251. func (s *RedPacketService) CalculateBestLuck(packetId int64) {
  252. // 查询所有记录
  253. var records []entity.TgRedPacketRecord
  254. s.DB().Where("packet_id = ?", packetId).Order("amount DESC").Find(&records)
  255. if len(records) == 0 {
  256. return
  257. }
  258. // 标记手气最佳(金额最大的)
  259. bestRecord := records[0]
  260. s.DB().Model(&entity.TgRedPacketRecord{}).
  261. Where("id = ?", bestRecord.Id).
  262. Update("is_best", 1)
  263. }
  264. // GetRedPacketDetail 获取红包详情
  265. func (s *RedPacketService) GetRedPacketDetail(packetNo string) (*entity.TgRedPacket, []entity.TgRedPacketRecord, error) {
  266. var packet entity.TgRedPacket
  267. if err := s.DB().Where("packet_no = ?", packetNo).First(&packet).Error; err != nil {
  268. return nil, nil, err
  269. }
  270. var records []entity.TgRedPacketRecord
  271. s.DB().Where("packet_id = ?", packet.Id).Order("grabbed_at ASC").Find(&records)
  272. return &packet, records, nil
  273. }
  274. // GetUserRedPacketRecords 获取用户的红包记录
  275. func (s *RedPacketService) GetUserRedPacketRecords(userId int64, page, pageSize int) ([]entity.TgRedPacketRecord, int64, error) {
  276. var records []entity.TgRedPacketRecord
  277. var total int64
  278. db := s.DB().Model(&entity.TgRedPacketRecord{}).Where("user_id = ?", userId)
  279. db.Count(&total)
  280. offset := (page - 1) * pageSize
  281. if err := db.Order("grabbed_at DESC").Offset(offset).Limit(pageSize).Find(&records).Error; err != nil {
  282. return nil, 0, err
  283. }
  284. return records, total, nil
  285. }