package user import ( "context" "fmt" "math/rand" "time" "go.uber.org/zap" "bindbox-game/internal/repository/mysql/dao" "bindbox-game/internal/repository/mysql/model" ) // GrantRewardRequest 奖励发放请求 type GrantRewardRequest struct { ProductID int64 `json:"product_id" binding:"required"` // 商品ID Quantity int `json:"quantity" binding:"min=1"` // 发放数量 ActivityID *int64 `json:"activity_id,omitempty"` // 活动ID(可选) RewardID *int64 `json:"reward_id,omitempty"` // 奖励配置ID(可选) AddressID *int64 `json:"address_id,omitempty"` // 收货地址ID(可选,实物商品需要) Remark string `json:"remark,omitempty"` // 备注 PointsAmount int64 `json:"points_amount,omitempty"` // 消耗积分 SourceType *int32 `json:"source_type,omitempty"` // 订单来源(可选,默认3) ExtOrderID string `json:"ext_order_id,omitempty"` // 外部订单号(可选) } // GrantRewardResponse 奖励发放响应 type GrantRewardResponse struct { Success bool `json:"success"` OrderID int64 `json:"order_id"` InventoryIDs []int64 `json:"inventory_ids"` Message string `json:"message,omitempty"` } // GrantReward 给用户发放奖励(事务处理) func (s *service) GrantReward(ctx context.Context, userID int64, req GrantRewardRequest) (*GrantRewardResponse, error) { logger := zap.L().With(zap.Int64("user_id", userID), zap.Int64("product_id", req.ProductID)) var ( orderID int64 inventoryIDs []int64 ) logger.Info("开始发放奖励", zap.Any("request", req)) // 执行事务 err := s.writeDB.Transaction(func(tx *dao.Query) error { logger.Info("开始事务处理") var rewardSetting *model.ActivityRewardSettings var err error // 1. 检查奖励配置库存(如果提供了reward_id) if req.RewardID != nil { logger.Info("检查奖励配置", zap.Int64("reward_id", *req.RewardID)) rewardSetting, err = tx.ActivityRewardSettings.WithContext(ctx).Where( tx.ActivityRewardSettings.ID.Eq(*req.RewardID), ).First() if err != nil { logger.Error("查询奖励配置失败", zap.Error(err)) return fmt.Errorf("查询奖励配置失败: %w", err) } if rewardSetting == nil { return fmt.Errorf("奖励配置不存在") } if rewardSetting.Quantity < int64(req.Quantity) { return fmt.Errorf("奖励库存不足,剩余%d,需要%d", rewardSetting.Quantity, req.Quantity) } // 扣减奖励库存 _, err = tx.ActivityRewardSettings.WithContext(ctx).Where( tx.ActivityRewardSettings.ID.Eq(*req.RewardID), ).Update(tx.ActivityRewardSettings.Quantity, rewardSetting.Quantity-int64(req.Quantity)) if err != nil { logger.Error("扣减奖励库存失败", zap.Error(err)) return fmt.Errorf("扣减奖励库存失败: %w", err) } logger.Info("奖励库存扣减成功") } // 2. 生成订单号 orderNo := generateOrderNo() logger.Info("生成订单号", zap.String("order_no", orderNo)) // 3. 创建系统发放订单 now := time.Now() // 对于已支付的订单,设置取消时间为MySQL可接受的最小有效时间(1970-01-01) // 避免使用零时间导致MySQL的'0000-00-00'错误 minValidTime := time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC) order := &model.Orders{ OrderNo: orderNo, UserID: userID, SourceType: func() int32 { if req.SourceType != nil { return *req.SourceType } return 6 // 默认:系统发放/管理员 }(), Status: 2, // 已支付 TotalAmount: 0, DiscountAmount: 0, PointsAmount: req.PointsAmount, ActualAmount: 0, IsConsumed: 0, PaidAt: now, // 设置支付时间为当前时间 CancelledAt: minValidTime, // 设置取消时间为最小有效时间,避免MySQL错误 Remark: req.Remark, ExtOrderID: req.ExtOrderID, CreatedAt: now, UpdatedAt: now, } logger.Info("创建订单", zap.Any("order", order)) err = tx.Orders.WithContext(ctx).Create(order) if err != nil { logger.Error("创建订单失败", zap.Error(err)) return fmt.Errorf("创建订单失败: %w", err) } orderID = order.ID logger.Info("订单创建成功", zap.Int64("order_id", orderID)) // 4. 查询商品信息快照 product, err := tx.Products.WithContext(ctx).Where(tx.Products.ID.Eq(req.ProductID)).First() if err != nil { logger.Error("查询商品失败", zap.Error(err)) return fmt.Errorf("查询商品失败: %w", err) } // 检查商品库存是否充足 if product.Stock < int64(req.Quantity) { logger.Error("商品库存不足", zap.Int64("stock", product.Stock), zap.Int("need", req.Quantity)) return fmt.Errorf("商品库存不足,请联系客服处理") } // 扣减商品库存 _, err = tx.Products.WithContext(ctx).Where( tx.Products.ID.Eq(req.ProductID), ).Update(tx.Products.Stock, product.Stock-int64(req.Quantity)) if err != nil { logger.Error("扣减商品库存失败", zap.Error(err)) return fmt.Errorf("扣减商品库存失败: %w", err) } logger.Info("商品库存扣减成功", zap.Int64("product_id", req.ProductID), zap.Int("quantity", req.Quantity)) // 5. 创建订单项(按数量创建多个) for i := 0; i < req.Quantity; i++ { orderItem := &model.OrderItems{ OrderID: orderID, ProductID: req.ProductID, Title: product.Name, Quantity: 1, Price: 0, // 成交单价(分) TotalAmount: 0, // 行应付总额(分) ProductImages: product.ImagesJSON, Status: 1, // 行状态:1正常 } err := tx.OrderItems.WithContext(ctx).Create(orderItem) if err != nil { logger.Error("创建订单项失败", zap.Error(err)) return fmt.Errorf("创建订单项失败: %w", err) } // 6. 创建用户资产记录 inventory := &model.UserInventory{ UserID: userID, ProductID: req.ProductID, ValueCents: func() int64 { if rewardSetting != nil && rewardSetting.PriceSnapshotCents > 0 { return rewardSetting.PriceSnapshotCents } return product.Price }(), ValueSource: func() int32 { if rewardSetting != nil && rewardSetting.PriceSnapshotCents > 0 { return 1 } return 2 }(), ValueSnapshotAt: func() time.Time { if rewardSetting != nil && !rewardSetting.PriceSnapshotAt.IsZero() { return rewardSetting.PriceSnapshotAt } return time.Now() }(), OrderID: orderID, ActivityID: func() int64 { if req.ActivityID != nil { return *req.ActivityID } else { return 0 } }(), RewardID: func() int64 { if req.RewardID != nil { return *req.RewardID } else { return 0 } }(), Status: 1, // 持有状态 Remark: func() string { if req.Remark != "" { return req.Remark } return product.Name }(), } err = tx.UserInventory.WithContext(ctx).Create(inventory) if err != nil { logger.Error("创建用户资产失败", zap.Error(err)) return fmt.Errorf("创建用户资产失败: %w", err) } inventoryIDs = append(inventoryIDs, inventory.ID) // 7. 如果需要发货(提供了地址ID),创建发货记录 if req.AddressID != nil { // 设置发货和签收时间为MySQL可接受的最小有效时间(1970-01-01) // 避免使用零时间导致MySQL的'0000-00-00'错误 minValidTime := time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC) shippingRecord := &model.ShippingRecords{ UserID: userID, OrderID: orderID, OrderItemID: orderItem.ID, InventoryID: inventory.ID, ProductID: req.ProductID, AddressID: *req.AddressID, Quantity: 1, Status: 1, // 待发货 ShippedAt: minValidTime, // 设置发货时间为最小有效时间 ReceivedAt: minValidTime, // 设置签收时间为最小有效时间 CreatedAt: time.Now(), UpdatedAt: time.Now(), } err := tx.ShippingRecords.WithContext(ctx).Create(shippingRecord) if err != nil { logger.Error("创建发货记录失败", zap.Error(err)) return fmt.Errorf("创建发货记录失败: %w", err) } } } return nil }) if err != nil { return &GrantRewardResponse{ Success: false, Message: err.Error(), }, err } logger.Info("奖励发放成功", zap.Int64("order_id", orderID), zap.Int64s("inventory_ids", inventoryIDs), zap.Int("quantity", req.Quantity), ) return &GrantRewardResponse{ Success: true, OrderID: orderID, InventoryIDs: inventoryIDs, Message: fmt.Sprintf("成功发放%d个奖励", req.Quantity), }, nil } // generateOrderNo 生成订单号 func generateOrderNo() string { // 使用当前时间戳 + 随机数生成订单号 // 格式:RG + 年月日时分秒 + 6位随机数 r := rand.New(rand.NewSource(time.Now().UnixNano())) return fmt.Sprintf("RG%s%06d", time.Now().Format("20060102150405"), r.Intn(1000000), ) } // GrantRewardToOrderRequest 在现有订单上发放奖励的请求参数 type GrantRewardToOrderRequest struct { OrderID int64 `json:"order_id" binding:"required"` // 现有订单ID ProductID int64 `json:"product_id" binding:"required"` // 商品ID Quantity int `json:"quantity" binding:"min=1"` // 发放数量 ActivityID *int64 `json:"activity_id,omitempty"` // 活动ID(可选) RewardID *int64 `json:"reward_id,omitempty"` // 奖励配置ID(可选) Remark string `json:"remark,omitempty"` // 备注 } // GrantRewardToOrderResponse 在现有订单上发放奖励的响应 type GrantRewardToOrderResponse struct { Success bool `json:"success"` OrderID int64 `json:"order_id"` InventoryIDs []int64 `json:"inventory_ids"` Message string `json:"message,omitempty"` } // GrantRewardToOrder 在现有订单上发放奖励(不创建新订单) // 用于抽奖中奖场景:用户支付的抽奖订单中奖后,直接在该订单上添加中奖商品 func (s *service) GrantRewardToOrder(ctx context.Context, userID int64, req GrantRewardToOrderRequest) (*GrantRewardToOrderResponse, error) { logger := zap.L().With(zap.Int64("user_id", userID), zap.Int64("order_id", req.OrderID), zap.Int64("product_id", req.ProductID)) var inventoryIDs []int64 logger.Info("开始在现有订单上发放奖励", zap.Any("request", req)) // 执行事务 err := s.writeDB.Transaction(func(tx *dao.Query) error { logger.Info("开始事务处理") var rewardSetting *model.ActivityRewardSettings // 1. 验证订单存在且属于该用户 order, err := tx.Orders.WithContext(ctx).Where( tx.Orders.ID.Eq(req.OrderID), tx.Orders.UserID.Eq(userID), ).First() if err != nil || order == nil { logger.Error("订单不存在或不属于该用户", zap.Error(err)) return fmt.Errorf("订单不存在或不属于该用户") } // 【关键校验】验证订单状态必须为已支付(2),未支付的订单不能发奖 if order.Status != 2 { logger.Error("订单状态不正确,无法发奖", zap.Int32("status", order.Status)) return fmt.Errorf("订单未支付或状态异常(status=%d),无法发奖", order.Status) } // 2. 检查奖励配置库存(如果提供了reward_id) if req.RewardID != nil { logger.Info("检查奖励配置", zap.Int64("reward_id", *req.RewardID)) // 【使用乐观锁扣减库存】直接用 Quantity > 0 作为更新条件,避免竞态 result, err := tx.ActivityRewardSettings.WithContext(ctx).Where( tx.ActivityRewardSettings.ID.Eq(*req.RewardID), tx.ActivityRewardSettings.Quantity.Gt(0), // 乐观锁:只有库存>0才能扣减 ).UpdateSimple(tx.ActivityRewardSettings.Quantity.Add(-int64(req.Quantity))) if err != nil { logger.Error("扣减奖励库存失败", zap.Error(err)) return fmt.Errorf("扣减奖励库存失败: %w", err) } if result.RowsAffected == 0 { logger.Error("奖励库存不足或不存在") return fmt.Errorf("奖励库存不足或不存在") } rewardSetting, err = tx.ActivityRewardSettings.WithContext(ctx).Where( tx.ActivityRewardSettings.ID.Eq(*req.RewardID), ).First() if err != nil { logger.Error("查询奖励配置失败", zap.Error(err)) return fmt.Errorf("查询奖励配置失败: %w", err) } logger.Info("奖励库存扣减成功(乐观锁)") } // 3. 查询商品信息快照 product, err := tx.Products.WithContext(ctx).Where(tx.Products.ID.Eq(req.ProductID)).First() if err != nil { logger.Error("查询商品失败", zap.Error(err)) return fmt.Errorf("查询商品失败: %w", err) } // 4. 在现有订单上添加订单项和用户资产(按数量创建多个) for i := 0; i < req.Quantity; i++ { orderItem := &model.OrderItems{ OrderID: req.OrderID, ProductID: req.ProductID, Title: product.Name, Quantity: 1, Price: 0, // 中奖商品,无需额外付费 TotalAmount: 0, ProductImages: product.ImagesJSON, Status: 1, // 行状态:1正常 } err := tx.OrderItems.WithContext(ctx).Create(orderItem) if err != nil { logger.Error("创建订单项失败", zap.Error(err)) return fmt.Errorf("创建订单项失败: %w", err) } // 5. 创建用户资产记录(关联到原订单) inventory := &model.UserInventory{ UserID: userID, ProductID: req.ProductID, ValueCents: func() int64 { if rewardSetting != nil && rewardSetting.PriceSnapshotCents > 0 { return rewardSetting.PriceSnapshotCents } return product.Price }(), ValueSource: func() int32 { if rewardSetting != nil && rewardSetting.PriceSnapshotCents > 0 { return 1 } return 2 }(), ValueSnapshotAt: func() time.Time { if rewardSetting != nil && !rewardSetting.PriceSnapshotAt.IsZero() { return rewardSetting.PriceSnapshotAt } return time.Now() }(), OrderID: req.OrderID, // 关联到原抽奖订单 ActivityID: func() int64 { if req.ActivityID != nil { return *req.ActivityID } return 0 }(), RewardID: func() int64 { if req.RewardID != nil { return *req.RewardID } return 0 }(), Status: 1, // 持有状态 Remark: func() string { if req.Remark != "" { return req.Remark } return product.Name }(), } err = tx.UserInventory.WithContext(ctx).Create(inventory) if err != nil { logger.Error("创建用户资产失败", zap.Error(err)) return fmt.Errorf("创建用户资产失败: %w", err) } inventoryIDs = append(inventoryIDs, inventory.ID) } return nil }) if err != nil { return &GrantRewardToOrderResponse{ Success: false, Message: err.Error(), }, err } logger.Info("在现有订单上发放奖励成功", zap.Int64("order_id", req.OrderID), zap.Int64s("inventory_ids", inventoryIDs), zap.Int("quantity", req.Quantity), ) return &GrantRewardToOrderResponse{ Success: true, OrderID: req.OrderID, InventoryIDs: inventoryIDs, Message: fmt.Sprintf("成功发放%d个奖励到订单", req.Quantity), }, nil }