bindbox-game/internal/service/user/reward_grant.go
邹方成 1ab39d2f5a
Some checks failed
Build docker and publish / linux (1.24.5) (push) Failing after 25s
refactor: 重构项目结构并重命名模块
feat(admin): 新增工会管理功能
feat(activity): 添加活动管理相关服务
feat(user): 实现用户道具卡和积分管理
feat(guild): 新增工会成员管理功能

fix: 修复数据库连接配置
fix: 修正jwtoken导入路径
fix: 解决端口冲突问题

style: 统一代码格式和注释风格
style: 更新项目常量命名

docs: 添加项目框架和开发规范文档
docs: 更新接口文档注释

chore: 移除无用代码和文件
chore: 更新Makefile和配置文件
chore: 清理日志文件

test: 添加道具卡测试脚本
2025-11-14 21:10:00 +08:00

228 lines
7.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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"` // 备注
}
// 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: 0,
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: 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,
)
}