Some checks failed
Build docker and publish / linux (1.24.5) (push) Failing after 15s
386 lines
12 KiB
Go
386 lines
12 KiB
Go
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
|
||
}
|