package luckygift import ( "chatapp3-golang/internal/model" "chatapp3-golang/internal/utils" "context" "encoding/json" "strings" "time" "gorm.io/gorm" "gorm.io/gorm/clause" ) // ensureRequestRecord 创建或复用抽奖主记录,保证流程可恢复。 func (s *LuckyGiftService) ensureRequestRecord( ctx context.Context, req LuckyGiftDrawRequest, requestJSON string, walletEventID string, ) (*model.LuckyGiftDrawRequestRecord, error) { record, err := s.findRequestRecord(ctx, req.BusinessID) if err != nil { return nil, err } now := time.Now() if record == nil { id, err := utils.NextID() if err != nil { return nil, err } record = &model.LuckyGiftDrawRequestRecord{ ID: id, BusinessID: req.BusinessID, ConsumeAssetRecordID: req.ConsumeAssetRecordID, SysOrigin: req.SysOrigin, StandardID: req.StandardID, RoomID: req.RoomID, SendUserID: req.SendUserID, GiftID: req.GiftID, GiftPrice: req.GiftPrice, GiftNum: req.GiftNum, GiftIsFree: req.GiftIsFree, WalletEventID: walletEventID, Status: luckyGiftDrawStatusPending, RequestJSON: requestJSON, CreateTime: now, UpdateTime: now, } if err := s.repo.DB.WithContext(ctx).Create(record).Error; err != nil { return nil, err } return record, nil } updates := map[string]any{ "consume_asset_record_id": req.ConsumeAssetRecordID, "sys_origin": req.SysOrigin, "standard_id": req.StandardID, "room_id": req.RoomID, "send_user_id": req.SendUserID, "gift_id": req.GiftID, "gift_price": req.GiftPrice, "gift_num": req.GiftNum, "gift_is_free": req.GiftIsFree, "wallet_event_id": walletEventID, "request_json": requestJSON, "update_time": now, } if err := s.updateRequestRecord(ctx, req.BusinessID, updates); err != nil { return nil, err } record.ConsumeAssetRecordID = req.ConsumeAssetRecordID record.SysOrigin = req.SysOrigin record.StandardID = req.StandardID record.RoomID = req.RoomID record.SendUserID = req.SendUserID record.GiftID = req.GiftID record.GiftPrice = req.GiftPrice record.GiftNum = req.GiftNum record.GiftIsFree = req.GiftIsFree record.WalletEventID = walletEventID record.RequestJSON = requestJSON record.UpdateTime = now return record, nil } // findRequestRecord 按业务 ID 读取抽奖主记录。 func (s *LuckyGiftService) findRequestRecord(ctx context.Context, businessID string) (*model.LuckyGiftDrawRequestRecord, error) { var record model.LuckyGiftDrawRequestRecord err := s.repo.DB.WithContext(ctx).Where("business_id = ?", businessID).First(&record).Error if err == nil { return &record, nil } if err != nil && err != gorm.ErrRecordNotFound { return nil, err } return nil, nil } // loadSuccessfulResponse 读取已经成功完成的抽奖响应,用于幂等返回。 func (s *LuckyGiftService) loadSuccessfulResponse(ctx context.Context, businessID string) (*LuckyGiftDrawResponse, error) { record, err := s.findRequestRecord(ctx, businessID) if err != nil || record == nil { return nil, err } if record.Status != luckyGiftDrawStatusSuccess || strings.TrimSpace(record.ResponseJSON) == "" { return nil, nil } var resp LuckyGiftDrawResponse if err := json.Unmarshal([]byte(record.ResponseJSON), &resp); err != nil { return nil, err } return &resp, nil } // updateRequestRecord 局部更新主记录状态或响应信息。 func (s *LuckyGiftService) updateRequestRecord(ctx context.Context, businessID string, updates map[string]any) error { return s.repo.DB.WithContext(ctx). Model(&model.LuckyGiftDrawRequestRecord{}). Where("business_id = ?", businessID). Updates(updates).Error } // markRequestFailed 把主记录更新成失败态,并保存错误和三方回包。 func (s *LuckyGiftService) markRequestFailed( ctx context.Context, businessID string, providerCode int, rawResponse string, message string, ) error { updates := map[string]any{ "status": luckyGiftDrawStatusFailed, "error_message": truncateLuckyGiftError(message), "update_time": time.Now(), } if providerCode != 0 { updates["provider_code"] = providerCode } if strings.TrimSpace(rawResponse) != "" { updates["provider_response_json"] = rawResponse } return s.updateRequestRecord(ctx, businessID, updates) } // truncateLuckyGiftError 截断错误信息,避免超出字段长度。 func truncateLuckyGiftError(message string) string { message = strings.TrimSpace(message) if len(message) <= 1024 { return message } return message[:1024] } // upsertOrderRecords 批量写入抽奖子单结果,保证同一 businessId 可以重放。 func (s *LuckyGiftService) upsertOrderRecords(ctx context.Context, businessID string, results []LuckyGiftDrawResult) error { if len(results) == 0 { return nil } now := time.Now() rows := make([]model.LuckyGiftDrawOrderRecord, 0, len(results)) for index, item := range results { id, err := utils.NextID() if err != nil { return err } rows = append(rows, model.LuckyGiftDrawOrderRecord{ ID: id, BusinessID: businessID, OrderSeed: item.ID, SortNo: index + 1, AcceptUserID: item.AcceptUserID, ProviderOrderID: item.OrderID, IsWin: item.IsWin, RewardNum: item.RewardNum, CreateTime: now, UpdateTime: now, }) } return s.repo.DB.WithContext(ctx).Clauses(clause.OnConflict{ Columns: []clause.Column{ {Name: "business_id"}, {Name: "order_seed"}, }, DoUpdates: clause.AssignmentColumns([]string{ "sort_no", "accept_user_id", "provider_order_id", "is_win", "reward_num", "update_time", }), }).Create(&rows).Error } // lockTTL 返回抽奖分布式锁过期时间。 func (s *LuckyGiftService) lockTTL() time.Duration { timeout := s.httpClient.Timeout if timeout <= 0 { timeout = 8 * time.Second } if timeout < 10*time.Second { timeout = 10 * time.Second } return timeout + 5*time.Second }