package user import ( "context" "fmt" "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"` // 消耗积分 } // 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("开始事务处理") // 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: 3, // 系统发放 Status: 2, // 已支付 TotalAmount: 0, DiscountAmount: 0, PointsAmount: req.PointsAmount, ActualAmount: 0, IsConsumed: 0, PaidAt: now, // 设置支付时间为当前时间 CancelledAt: minValidTime, // 设置取消时间为最小有效时间,避免MySQL错误 Remark: req.Remark, 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) } // 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, 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位随机数 return fmt.Sprintf("RG%s%d", time.Now().Format("20060102150405"), time.Now().UnixNano()%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("开始事务处理") // 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("奖励库存不足或不存在") } 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, 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 }