Files
junhong_cmp_fiber/internal/service/order/service.go
huang 5bb0ff0ddf
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m54s
fix: 修复代理钱包订单创建逻辑,拆分后台/H5端下单方法并归档变更
- 拆分订单创建为 CreateAdminOrder(后台一步支付)和 CreateH5Order(H5 两步支付)
- 新增 CreateAdminOrderRequest DTO,后台仅允许 wallet/offline 支付方式
- 同步 delta specs 到主规格(order-payment 更新 + admin-order-creation 新增)
- 归档 fix-agent-wallet-order-creation 变更
- 新增 implement-order-expiration 变更提案
2026-02-28 16:31:31 +08:00

2072 lines
69 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 order
import (
"context"
"fmt"
"sort"
"strconv"
"strings"
"time"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
packagepkg "github.com/break/junhong_cmp_fiber/internal/service/package"
"github.com/break/junhong_cmp_fiber/internal/service/purchase_validation"
"github.com/break/junhong_cmp_fiber/internal/store"
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/break/junhong_cmp_fiber/pkg/middleware"
"github.com/break/junhong_cmp_fiber/pkg/queue"
"github.com/break/junhong_cmp_fiber/pkg/wechat"
"github.com/bytedance/sonic"
"github.com/redis/go-redis/v9"
"go.uber.org/zap"
"gorm.io/gorm"
)
type Service struct {
db *gorm.DB
redis *redis.Client
orderStore *postgres.OrderStore
orderItemStore *postgres.OrderItemStore
agentWalletStore *postgres.AgentWalletStore
cardWalletStore *postgres.CardWalletStore
purchaseValidationService *purchase_validation.Service
shopPackageAllocationStore *postgres.ShopPackageAllocationStore
shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore
iotCardStore *postgres.IotCardStore
deviceStore *postgres.DeviceStore
packageSeriesStore *postgres.PackageSeriesStore
packageUsageStore *postgres.PackageUsageStore
packageStore *postgres.PackageStore
wechatPayment wechat.PaymentServiceInterface
queueClient *queue.Client
logger *zap.Logger
}
func New(
db *gorm.DB,
redisClient *redis.Client,
orderStore *postgres.OrderStore,
orderItemStore *postgres.OrderItemStore,
agentWalletStore *postgres.AgentWalletStore,
cardWalletStore *postgres.CardWalletStore,
purchaseValidationService *purchase_validation.Service,
shopPackageAllocationStore *postgres.ShopPackageAllocationStore,
shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore,
iotCardStore *postgres.IotCardStore,
deviceStore *postgres.DeviceStore,
packageSeriesStore *postgres.PackageSeriesStore,
packageUsageStore *postgres.PackageUsageStore,
packageStore *postgres.PackageStore,
wechatPayment wechat.PaymentServiceInterface,
queueClient *queue.Client,
logger *zap.Logger,
) *Service {
return &Service{
db: db,
redis: redisClient,
orderStore: orderStore,
orderItemStore: orderItemStore,
agentWalletStore: agentWalletStore,
cardWalletStore: cardWalletStore,
purchaseValidationService: purchaseValidationService,
shopPackageAllocationStore: shopPackageAllocationStore,
shopSeriesAllocationStore: shopSeriesAllocationStore,
iotCardStore: iotCardStore,
deviceStore: deviceStore,
packageSeriesStore: packageSeriesStore,
packageUsageStore: packageUsageStore,
packageStore: packageStore,
wechatPayment: wechatPayment,
queueClient: queueClient,
logger: logger,
}
}
// CreateLegacy 创建订单(已废弃)
// Deprecated: 使用 CreateAdminOrder 或 CreateH5Order 替代。保留用于回滚。
func (s *Service) CreateLegacy(ctx context.Context, req *dto.CreateOrderRequest, buyerType string, buyerID uint) (*dto.OrderResponse, error) {
var validationResult *purchase_validation.PurchaseValidationResult
var err error
if req.OrderType == model.OrderTypeSingleCard {
if req.IotCardID == nil {
return nil, errors.New(errors.CodeInvalidParam, "单卡购买必须指定IoT卡ID")
}
validationResult, err = s.purchaseValidationService.ValidateCardPurchase(ctx, *req.IotCardID, req.PackageIDs)
} else if req.OrderType == model.OrderTypeDevice {
if req.DeviceID == nil {
return nil, errors.New(errors.CodeInvalidParam, "设备购买必须指定设备ID")
}
validationResult, err = s.purchaseValidationService.ValidateDevicePurchase(ctx, *req.DeviceID, req.PackageIDs)
} else {
return nil, errors.New(errors.CodeInvalidParam, "无效的订单类型")
}
if err != nil {
return nil, err
}
// 下单阶段校验混买限制:禁止同一订单同时包含正式套餐和加油包
if err := validatePackageTypeMixFromPackages(validationResult.Packages); err != nil {
return nil, err
}
// 幂等性检查:防止同一买家对同一载体短时间内重复下单
carrierType, carrierID := resolveCarrierInfo(req)
existingOrderID, err := s.checkOrderIdempotency(ctx, buyerType, buyerID, req.OrderType, carrierType, carrierID, req.PackageIDs)
if err != nil {
return nil, err
}
if existingOrderID > 0 {
return s.Get(ctx, existingOrderID)
}
// 获取到分布式锁后,确保无论成功还是失败都释放
lockKey := constants.RedisOrderCreateLockKey(carrierType, carrierID)
defer s.redis.Del(ctx, lockKey)
forceRechargeCheck := s.checkForceRechargeRequirement(ctx, validationResult)
if forceRechargeCheck.NeedForceRecharge && validationResult.TotalPrice < forceRechargeCheck.ForceRechargeAmount {
return nil, errors.New(errors.CodeForceRechargeRequired, "首次购买需满足最低充值要求")
}
userID := middleware.GetUserIDFromContext(ctx)
// 提取资源所属店铺ID
var resourceShopID *uint
var seriesID *uint
if validationResult.Card != nil {
resourceShopID = validationResult.Card.ShopID
seriesID = validationResult.Card.SeriesID
} else if validationResult.Device != nil {
resourceShopID = validationResult.Device.ShopID
seriesID = validationResult.Device.SeriesID
}
// 初始化订单字段
orderBuyerType := buyerType
orderBuyerID := buyerID
totalAmount := validationResult.TotalPrice
paymentMethod := req.PaymentMethod
paymentStatus := model.PaymentStatusPending
var paidAt *time.Time
now := time.Now()
isPurchaseOnBehalf := false
var operatorID *uint
operatorType := ""
var actualPaidAmount *int64
purchaseRole := ""
var sellerShopID *uint = resourceShopID
var sellerCostPrice int64
// 场景判断offline平台代购、wallet代理钱包支付、其他待支付
if req.PaymentMethod == model.PaymentMethodOffline {
// ==== 场景 1平台代购offline====
purchaseBuyerID, buyerCostPrice, purchasePaidAt, err := s.resolvePurchaseOnBehalfInfo(ctx, validationResult)
if err != nil {
return nil, err
}
orderBuyerType = model.BuyerTypeAgent
orderBuyerID = purchaseBuyerID
totalAmount = buyerCostPrice
paymentMethod = model.PaymentMethodOffline
paymentStatus = model.PaymentStatusPaid
paidAt = purchasePaidAt
isPurchaseOnBehalf = true
sellerCostPrice = buyerCostPrice
// 设置操作者信息(平台代购)
operatorID = nil
operatorType = constants.OwnerTypePlatform
purchaseRole = model.PurchaseRolePurchasedByPlatform
actualPaidAmount = nil
} else if req.PaymentMethod == model.PaymentMethodWallet {
// ==== 场景 2代理钱包支付wallet====
// 只有代理账号可以使用钱包支付
if buyerType != model.BuyerTypeAgent {
return nil, errors.New(errors.CodeInvalidParam, "只有代理账号可以使用钱包支付")
}
operatorShopID := buyerID
// 判断资源是否属于操作者
if resourceShopID == nil {
return nil, errors.New(errors.CodeInternalError, "资源店铺ID为空")
}
// 获取第一个套餐ID用于查询成本价
if len(validationResult.Packages) == 0 {
return nil, errors.New(errors.CodeInternalError, "套餐列表为空")
}
firstPackageID := validationResult.Packages[0].ID
if *resourceShopID == operatorShopID {
// ==== 子场景 2.1:代理自购 ====
costPrice, err := s.getCostPrice(ctx, operatorShopID, firstPackageID)
if err != nil {
return nil, err
}
orderBuyerType = model.BuyerTypeAgent
orderBuyerID = operatorShopID
totalAmount = costPrice
paymentMethod = model.PaymentMethodWallet
paymentStatus = model.PaymentStatusPaid
paidAt = &now
isPurchaseOnBehalf = false
operatorID = &operatorShopID
operatorType = "agent"
actualPaidAmountVal := costPrice
actualPaidAmount = &actualPaidAmountVal
purchaseRole = model.PurchaseRoleSelfPurchase
sellerCostPrice = costPrice
} else {
// ==== 子场景 2.2:代理代购(给下级购买)====
// 获取买家成本价
buyerCostPrice, err := s.getCostPrice(ctx, *resourceShopID, firstPackageID)
if err != nil {
return nil, err
}
// 获取操作者成本价
operatorCostPrice, err := s.getCostPrice(ctx, operatorShopID, firstPackageID)
if err != nil {
return nil, err
}
orderBuyerType = model.BuyerTypeAgent
orderBuyerID = *resourceShopID
totalAmount = buyerCostPrice
paymentMethod = model.PaymentMethodWallet
paymentStatus = model.PaymentStatusPaid
paidAt = &now
isPurchaseOnBehalf = true
operatorID = &operatorShopID
operatorType = "agent"
actualPaidAmount = &operatorCostPrice
purchaseRole = model.PurchaseRolePurchaseForSubordinate
sellerCostPrice = buyerCostPrice
}
}
order := &model.Order{
BaseModel: model.BaseModel{
Creator: userID,
Updater: userID,
},
OrderNo: s.orderStore.GenerateOrderNo(),
OrderType: req.OrderType,
BuyerType: orderBuyerType,
BuyerID: orderBuyerID,
IotCardID: req.IotCardID,
DeviceID: req.DeviceID,
TotalAmount: totalAmount,
PaymentMethod: paymentMethod,
PaymentStatus: paymentStatus,
PaidAt: paidAt,
CommissionStatus: model.CommissionStatusPending,
CommissionConfigVersion: 0,
SeriesID: seriesID,
SellerShopID: sellerShopID,
SellerCostPrice: sellerCostPrice,
IsPurchaseOnBehalf: isPurchaseOnBehalf,
OperatorID: operatorID,
OperatorType: operatorType,
ActualPaidAmount: actualPaidAmount,
PurchaseRole: purchaseRole,
}
items := s.buildOrderItems(userID, validationResult.Packages)
idempotencyKey := buildOrderIdempotencyKey(buyerType, buyerID, req.OrderType, carrierType, carrierID, req.PackageIDs)
// 根据支付方式选择创建订单的方式
if req.PaymentMethod == model.PaymentMethodOffline {
// 平台代购:创建订单并立即激活套餐
if err := s.createOrderWithActivation(ctx, order, items); err != nil {
return nil, err
}
s.enqueueCommissionCalculation(ctx, order.ID)
s.markOrderCreated(ctx, idempotencyKey, order.ID)
return s.buildOrderResponse(order, items), nil
} else if req.PaymentMethod == model.PaymentMethodWallet {
// 钱包支付:创建订单、扣款、激活套餐(在事务中完成)
if operatorID == nil {
return nil, errors.New(errors.CodeInternalError, "钱包支付场景下 operatorID 不能为空")
}
operatorShopID := *operatorID
buyerShopID := orderBuyerID
if err := s.createOrderWithWalletPayment(ctx, order, items, operatorShopID, buyerShopID); err != nil {
return nil, err
}
s.markOrderCreated(ctx, idempotencyKey, order.ID)
return s.buildOrderResponse(order, items), nil
} else {
// 其他支付方式:创建待支付订单
if err := s.orderStore.Create(ctx, order, items); err != nil {
return nil, err
}
s.markOrderCreated(ctx, idempotencyKey, order.ID)
return s.buildOrderResponse(order, items), nil
}
}
// CreateAdminOrder 后台订单创建(仅支持 wallet/offline立即扣款或激活
// 与 CreateH5Order 的核心区别后台订单不创建待支付状态wallet 立即扣款offline 立即激活
// POST /api/admin/orders
func (s *Service) CreateAdminOrder(ctx context.Context, req *dto.CreateAdminOrderRequest, buyerType string, buyerID uint) (*dto.OrderResponse, error) {
var validationResult *purchase_validation.PurchaseValidationResult
var err error
if req.OrderType == model.OrderTypeSingleCard {
if req.IotCardID == nil {
return nil, errors.New(errors.CodeInvalidParam, "单卡购买必须指定IoT卡ID")
}
validationResult, err = s.purchaseValidationService.ValidateCardPurchase(ctx, *req.IotCardID, req.PackageIDs)
} else if req.OrderType == model.OrderTypeDevice {
if req.DeviceID == nil {
return nil, errors.New(errors.CodeInvalidParam, "设备购买必须指定设备ID")
}
validationResult, err = s.purchaseValidationService.ValidateDevicePurchase(ctx, *req.DeviceID, req.PackageIDs)
} else {
return nil, errors.New(errors.CodeInvalidParam, "无效的订单类型")
}
if err != nil {
return nil, err
}
// 下单阶段校验混买限制:禁止同一订单同时包含正式套餐和加油包
if err := validatePackageTypeMixFromPackages(validationResult.Packages); err != nil {
return nil, err
}
// 幂等性检查:防止同一买家对同一载体短时间内重复下单
carrierType, carrierID := resolveAdminCarrierInfo(req)
existingOrderID, err := s.checkOrderIdempotency(ctx, buyerType, buyerID, req.OrderType, carrierType, carrierID, req.PackageIDs)
if err != nil {
return nil, err
}
if existingOrderID > 0 {
return s.Get(ctx, existingOrderID)
}
// 获取到分布式锁后,确保无论成功还是失败都释放
lockKey := constants.RedisOrderCreateLockKey(carrierType, carrierID)
defer s.redis.Del(ctx, lockKey)
forceRechargeCheck := s.checkForceRechargeRequirement(ctx, validationResult)
if forceRechargeCheck.NeedForceRecharge && validationResult.TotalPrice < forceRechargeCheck.ForceRechargeAmount {
return nil, errors.New(errors.CodeForceRechargeRequired, "首次购买需满足最低充值要求")
}
userID := middleware.GetUserIDFromContext(ctx)
// 提取资源所属店铺ID
var resourceShopID *uint
var seriesID *uint
if validationResult.Card != nil {
resourceShopID = validationResult.Card.ShopID
seriesID = validationResult.Card.SeriesID
} else if validationResult.Device != nil {
resourceShopID = validationResult.Device.ShopID
seriesID = validationResult.Device.SeriesID
}
// 初始化订单字段
orderBuyerType := buyerType
orderBuyerID := buyerID
totalAmount := validationResult.TotalPrice
paymentMethod := req.PaymentMethod
paymentStatus := model.PaymentStatusPaid
var paidAt *time.Time
now := time.Now()
isPurchaseOnBehalf := false
var operatorID *uint
operatorType := ""
var actualPaidAmount *int64
purchaseRole := ""
var sellerShopID *uint = resourceShopID
var sellerCostPrice int64
// 根据支付方式分别处理
if req.PaymentMethod == model.PaymentMethodOffline {
// ==== 场景 1平台代购offline====
purchaseBuyerID, buyerCostPrice, purchasePaidAt, err := s.resolvePurchaseOnBehalfInfo(ctx, validationResult)
if err != nil {
return nil, err
}
orderBuyerType = model.BuyerTypeAgent
orderBuyerID = purchaseBuyerID
totalAmount = buyerCostPrice
paymentMethod = model.PaymentMethodOffline
paymentStatus = model.PaymentStatusPaid
paidAt = purchasePaidAt
isPurchaseOnBehalf = true
sellerCostPrice = buyerCostPrice
// 设置操作者信息(平台代购)
operatorID = nil
operatorType = constants.OwnerTypePlatform
purchaseRole = model.PurchaseRolePurchasedByPlatform
actualPaidAmount = nil
} else if req.PaymentMethod == model.PaymentMethodWallet {
// ==== 场景 2代理钱包支付wallet====
// 只有代理账号可以使用钱包支付
if buyerType != model.BuyerTypeAgent {
return nil, errors.New(errors.CodeInvalidParam, "只有代理账号可以使用钱包支付")
}
operatorShopID := buyerID
// 判断资源是否属于操作者
if resourceShopID == nil {
return nil, errors.New(errors.CodeInternalError, "资源店铺ID为空")
}
// 获取第一个套餐ID用于查询成本价
if len(validationResult.Packages) == 0 {
return nil, errors.New(errors.CodeInternalError, "套餐列表为空")
}
firstPackageID := validationResult.Packages[0].ID
if *resourceShopID == operatorShopID {
// ==== 子场景 2.1:代理自购 ====
costPrice, err := s.getCostPrice(ctx, operatorShopID, firstPackageID)
if err != nil {
return nil, err
}
orderBuyerType = model.BuyerTypeAgent
orderBuyerID = operatorShopID
totalAmount = costPrice
paymentMethod = model.PaymentMethodWallet
paymentStatus = model.PaymentStatusPaid
paidAt = &now
isPurchaseOnBehalf = false
operatorID = &operatorShopID
operatorType = "agent"
actualPaidAmountVal := costPrice
actualPaidAmount = &actualPaidAmountVal
purchaseRole = model.PurchaseRoleSelfPurchase
sellerCostPrice = costPrice
} else {
// ==== 子场景 2.2:代理代购(给下级购买)====
// 获取买家成本价
buyerCostPrice, err := s.getCostPrice(ctx, *resourceShopID, firstPackageID)
if err != nil {
return nil, err
}
// 获取操作者成本价
operatorCostPrice, err := s.getCostPrice(ctx, operatorShopID, firstPackageID)
if err != nil {
return nil, err
}
orderBuyerType = model.BuyerTypeAgent
orderBuyerID = *resourceShopID
totalAmount = buyerCostPrice
paymentMethod = model.PaymentMethodWallet
paymentStatus = model.PaymentStatusPaid
paidAt = &now
isPurchaseOnBehalf = true
operatorID = &operatorShopID
operatorType = "agent"
actualPaidAmount = &operatorCostPrice
purchaseRole = model.PurchaseRolePurchaseForSubordinate
sellerCostPrice = buyerCostPrice
}
} else {
// 兜底检查后台不支持其他支付方式DTO 验证已拒绝,此为防御性编程)
return nil, errors.New(errors.CodeInvalidParam, "后台仅支持钱包支付或线下支付")
}
order := &model.Order{
BaseModel: model.BaseModel{
Creator: userID,
Updater: userID,
},
OrderNo: s.orderStore.GenerateOrderNo(),
OrderType: req.OrderType,
BuyerType: orderBuyerType,
BuyerID: orderBuyerID,
IotCardID: req.IotCardID,
DeviceID: req.DeviceID,
TotalAmount: totalAmount,
PaymentMethod: paymentMethod,
PaymentStatus: paymentStatus,
PaidAt: paidAt,
CommissionStatus: model.CommissionStatusPending,
CommissionConfigVersion: 0,
SeriesID: seriesID,
SellerShopID: sellerShopID,
SellerCostPrice: sellerCostPrice,
IsPurchaseOnBehalf: isPurchaseOnBehalf,
OperatorID: operatorID,
OperatorType: operatorType,
ActualPaidAmount: actualPaidAmount,
PurchaseRole: purchaseRole,
}
items := s.buildOrderItems(userID, validationResult.Packages)
idempotencyKey := buildOrderIdempotencyKey(buyerType, buyerID, req.OrderType, carrierType, carrierID, req.PackageIDs)
// 根据支付方式选择创建订单的方式
if req.PaymentMethod == model.PaymentMethodOffline {
// 平台代购:创建订单并立即激活套餐
if err := s.createOrderWithActivation(ctx, order, items); err != nil {
return nil, err
}
s.enqueueCommissionCalculation(ctx, order.ID)
s.markOrderCreated(ctx, idempotencyKey, order.ID)
return s.buildOrderResponse(order, items), nil
} else if req.PaymentMethod == model.PaymentMethodWallet {
// 钱包支付:创建订单、扣款、激活套餐(在事务中完成)
if operatorID == nil {
return nil, errors.New(errors.CodeInternalError, "钱包支付场景下 operatorID 不能为空")
}
operatorShopID := *operatorID
buyerShopID := orderBuyerID
if err := s.createOrderWithWalletPayment(ctx, order, items, operatorShopID, buyerShopID); err != nil {
return nil, err
}
s.markOrderCreated(ctx, idempotencyKey, order.ID)
return s.buildOrderResponse(order, items), nil
} else {
// 不应该到这里DTO 验证已拒绝其他支付方式)
return nil, errors.New(errors.CodeInvalidParam, "后台仅支持钱包支付或线下支付")
}
}
// CreateH5Order H5 端订单创建(支持 wallet/wechat/alipay支持待支付状态
// 保留原 Create() 方法的完整逻辑H5 端行为不变
// POST /api/h5/orders
func (s *Service) CreateH5Order(ctx context.Context, req *dto.CreateOrderRequest, buyerType string, buyerID uint) (*dto.OrderResponse, error) {
var validationResult *purchase_validation.PurchaseValidationResult
var err error
if req.OrderType == model.OrderTypeSingleCard {
if req.IotCardID == nil {
return nil, errors.New(errors.CodeInvalidParam, "单卡购买必须指定IoT卡ID")
}
validationResult, err = s.purchaseValidationService.ValidateCardPurchase(ctx, *req.IotCardID, req.PackageIDs)
} else if req.OrderType == model.OrderTypeDevice {
if req.DeviceID == nil {
return nil, errors.New(errors.CodeInvalidParam, "设备购买必须指定设备ID")
}
validationResult, err = s.purchaseValidationService.ValidateDevicePurchase(ctx, *req.DeviceID, req.PackageIDs)
} else {
return nil, errors.New(errors.CodeInvalidParam, "无效的订单类型")
}
if err != nil {
return nil, err
}
// 下单阶段校验混买限制:禁止同一订单同时包含正式套餐和加油包
if err := validatePackageTypeMixFromPackages(validationResult.Packages); err != nil {
return nil, err
}
// 幂等性检查:防止同一买家对同一载体短时间内重复下单
carrierType, carrierID := resolveCarrierInfo(req)
existingOrderID, err := s.checkOrderIdempotency(ctx, buyerType, buyerID, req.OrderType, carrierType, carrierID, req.PackageIDs)
if err != nil {
return nil, err
}
if existingOrderID > 0 {
return s.Get(ctx, existingOrderID)
}
// 获取到分布式锁后,确保无论成功还是失败都释放
lockKey := constants.RedisOrderCreateLockKey(carrierType, carrierID)
defer s.redis.Del(ctx, lockKey)
forceRechargeCheck := s.checkForceRechargeRequirement(ctx, validationResult)
if forceRechargeCheck.NeedForceRecharge && validationResult.TotalPrice < forceRechargeCheck.ForceRechargeAmount {
return nil, errors.New(errors.CodeForceRechargeRequired, "首次购买需满足最低充值要求")
}
userID := middleware.GetUserIDFromContext(ctx)
// 提取资源所属店铺ID
var resourceShopID *uint
var seriesID *uint
if validationResult.Card != nil {
resourceShopID = validationResult.Card.ShopID
seriesID = validationResult.Card.SeriesID
} else if validationResult.Device != nil {
resourceShopID = validationResult.Device.ShopID
seriesID = validationResult.Device.SeriesID
}
// 初始化订单字段
orderBuyerType := buyerType
orderBuyerID := buyerID
totalAmount := validationResult.TotalPrice
paymentMethod := req.PaymentMethod
paymentStatus := model.PaymentStatusPending
var paidAt *time.Time
now := time.Now()
isPurchaseOnBehalf := false
var operatorID *uint
operatorType := ""
var actualPaidAmount *int64
purchaseRole := ""
var sellerShopID *uint = resourceShopID
var sellerCostPrice int64
// 场景判断offline平台代购、wallet代理钱包支付、其他待支付
if req.PaymentMethod == model.PaymentMethodOffline {
// ==== 场景 1平台代购offline====
purchaseBuyerID, buyerCostPrice, purchasePaidAt, err := s.resolvePurchaseOnBehalfInfo(ctx, validationResult)
if err != nil {
return nil, err
}
orderBuyerType = model.BuyerTypeAgent
orderBuyerID = purchaseBuyerID
totalAmount = buyerCostPrice
paymentMethod = model.PaymentMethodOffline
paymentStatus = model.PaymentStatusPaid
paidAt = purchasePaidAt
isPurchaseOnBehalf = true
sellerCostPrice = buyerCostPrice
// 设置操作者信息(平台代购)
operatorID = nil
operatorType = constants.OwnerTypePlatform
purchaseRole = model.PurchaseRolePurchasedByPlatform
actualPaidAmount = nil
} else if req.PaymentMethod == model.PaymentMethodWallet {
// ==== 场景 2代理钱包支付wallet====
// 只有代理账号可以使用钱包支付
if buyerType != model.BuyerTypeAgent {
return nil, errors.New(errors.CodeInvalidParam, "只有代理账号可以使用钱包支付")
}
operatorShopID := buyerID
// 判断资源是否属于操作者
if resourceShopID == nil {
return nil, errors.New(errors.CodeInternalError, "资源店铺ID为空")
}
// 获取第一个套餐ID用于查询成本价
if len(validationResult.Packages) == 0 {
return nil, errors.New(errors.CodeInternalError, "套餐列表为空")
}
firstPackageID := validationResult.Packages[0].ID
if *resourceShopID == operatorShopID {
// ==== 子场景 2.1:代理自购 ====
costPrice, err := s.getCostPrice(ctx, operatorShopID, firstPackageID)
if err != nil {
return nil, err
}
orderBuyerType = model.BuyerTypeAgent
orderBuyerID = operatorShopID
totalAmount = costPrice
paymentMethod = model.PaymentMethodWallet
paymentStatus = model.PaymentStatusPaid
paidAt = &now
isPurchaseOnBehalf = false
operatorID = &operatorShopID
operatorType = "agent"
actualPaidAmountVal := costPrice
actualPaidAmount = &actualPaidAmountVal
purchaseRole = model.PurchaseRoleSelfPurchase
sellerCostPrice = costPrice
} else {
// ==== 子场景 2.2:代理代购(给下级购买)====
// 获取买家成本价
buyerCostPrice, err := s.getCostPrice(ctx, *resourceShopID, firstPackageID)
if err != nil {
return nil, err
}
// 获取操作者成本价
operatorCostPrice, err := s.getCostPrice(ctx, operatorShopID, firstPackageID)
if err != nil {
return nil, err
}
orderBuyerType = model.BuyerTypeAgent
orderBuyerID = *resourceShopID
totalAmount = buyerCostPrice
paymentMethod = model.PaymentMethodWallet
paymentStatus = model.PaymentStatusPaid
paidAt = &now
isPurchaseOnBehalf = true
operatorID = &operatorShopID
operatorType = "agent"
actualPaidAmount = &operatorCostPrice
purchaseRole = model.PurchaseRolePurchaseForSubordinate
sellerCostPrice = buyerCostPrice
}
}
order := &model.Order{
BaseModel: model.BaseModel{
Creator: userID,
Updater: userID,
},
OrderNo: s.orderStore.GenerateOrderNo(),
OrderType: req.OrderType,
BuyerType: orderBuyerType,
BuyerID: orderBuyerID,
IotCardID: req.IotCardID,
DeviceID: req.DeviceID,
TotalAmount: totalAmount,
PaymentMethod: paymentMethod,
PaymentStatus: paymentStatus,
PaidAt: paidAt,
CommissionStatus: model.CommissionStatusPending,
CommissionConfigVersion: 0,
SeriesID: seriesID,
SellerShopID: sellerShopID,
SellerCostPrice: sellerCostPrice,
IsPurchaseOnBehalf: isPurchaseOnBehalf,
OperatorID: operatorID,
OperatorType: operatorType,
ActualPaidAmount: actualPaidAmount,
PurchaseRole: purchaseRole,
}
items := s.buildOrderItems(userID, validationResult.Packages)
idempotencyKey := buildOrderIdempotencyKey(buyerType, buyerID, req.OrderType, carrierType, carrierID, req.PackageIDs)
// 根据支付方式选择创建订单的方式
if req.PaymentMethod == model.PaymentMethodOffline {
// 平台代购:创建订单并立即激活套餐
if err := s.createOrderWithActivation(ctx, order, items); err != nil {
return nil, err
}
s.enqueueCommissionCalculation(ctx, order.ID)
s.markOrderCreated(ctx, idempotencyKey, order.ID)
return s.buildOrderResponse(order, items), nil
} else if req.PaymentMethod == model.PaymentMethodWallet {
// 钱包支付:创建订单、扣款、激活套餐(在事务中完成)
if operatorID == nil {
return nil, errors.New(errors.CodeInternalError, "钱包支付场景下 operatorID 不能为空")
}
operatorShopID := *operatorID
buyerShopID := orderBuyerID
if err := s.createOrderWithWalletPayment(ctx, order, items, operatorShopID, buyerShopID); err != nil {
return nil, err
}
s.markOrderCreated(ctx, idempotencyKey, order.ID)
return s.buildOrderResponse(order, items), nil
} else {
// 其他支付方式创建待支付订单H5 端支持 wechat/alipay
if err := s.orderStore.Create(ctx, order, items); err != nil {
return nil, err
}
s.markOrderCreated(ctx, idempotencyKey, order.ID)
return s.buildOrderResponse(order, items), nil
}
}
func (s *Service) resolvePurchaseOnBehalfInfo(ctx context.Context, result *purchase_validation.PurchaseValidationResult) (uint, int64, *time.Time, error) {
var resourceShopID *uint
var seriesID *uint
if result.Card != nil {
resourceShopID = result.Card.ShopID
seriesID = result.Card.SeriesID
} else if result.Device != nil {
resourceShopID = result.Device.ShopID
seriesID = result.Device.SeriesID
}
if resourceShopID == nil || *resourceShopID == 0 {
return 0, 0, nil, errors.New(errors.CodeInvalidParam, "资源未分配给代理商,无法代购")
}
if seriesID == nil || *seriesID == 0 {
return 0, 0, nil, errors.New(errors.CodeInvalidParam, "资源未关联套餐系列")
}
if len(result.Packages) == 0 {
return 0, 0, nil, errors.New(errors.CodeInvalidParam, "订单中没有套餐")
}
firstPackageID := result.Packages[0].ID
buyerAllocation, err := s.shopPackageAllocationStore.GetByShopAndPackage(ctx, *resourceShopID, firstPackageID)
if err != nil {
return 0, 0, nil, errors.New(errors.CodeInvalidParam, "买家没有该套餐的分配配置")
}
now := time.Now()
return *resourceShopID, buyerAllocation.CostPrice, &now, nil
}
func (s *Service) buildOrderItems(operatorID uint, packages []*model.Package) []*model.OrderItem {
items := make([]*model.OrderItem, 0, len(packages))
for _, pkg := range packages {
item := &model.OrderItem{
BaseModel: model.BaseModel{
Creator: operatorID,
Updater: operatorID,
},
PackageID: pkg.ID,
PackageName: pkg.PackageName,
Quantity: 1,
UnitPrice: pkg.SuggestedRetailPrice,
Amount: pkg.SuggestedRetailPrice,
}
items = append(items, item)
}
return items
}
// getCostPrice 查询店铺对套餐的成本价
// shopID: 店铺ID
// packageID: 套餐ID
// 返回成本价(分),如果查询失败返回错误
func (s *Service) getCostPrice(ctx context.Context, shopID uint, packageID uint) (int64, error) {
allocation, err := s.shopPackageAllocationStore.GetByShopAndPackage(ctx, shopID, packageID)
if err != nil {
if err == gorm.ErrRecordNotFound {
return 0, errors.New(errors.CodeInvalidParam, "店铺没有该套餐的分配配置")
}
return 0, errors.Wrap(errors.CodeDatabaseError, err, "查询套餐成本价失败")
}
return allocation.CostPrice, nil
}
// createWalletTransaction 创建钱包流水记录
// ctx: 上下文
// tx: 事务对象
// walletID: 钱包ID
// orderID: 订单ID
// amount: 扣款金额(正数)
// purchaseRole: 订单角色
// relatedShopID: 关联店铺ID代购场景填充下级店铺ID
func (s *Service) createWalletTransaction(ctx context.Context, tx *gorm.DB, walletID uint, orderID uint, amount int64, purchaseRole string, relatedShopID *uint) error {
var subtype *string
remark := "购买套餐"
// 根据订单角色确定交易子类型和备注
switch purchaseRole {
case model.PurchaseRoleSelfPurchase:
subtypeVal := constants.WalletTransactionSubtypeSelfPurchase
subtype = &subtypeVal
case model.PurchaseRolePurchaseForSubordinate:
subtypeVal := constants.WalletTransactionSubtypePurchaseForSubordinate
subtype = &subtypeVal
// 查询下级店铺名称,填充到备注
if relatedShopID != nil {
var shop model.Shop
if err := tx.Where("id = ?", *relatedShopID).First(&shop).Error; err == nil {
remark = fmt.Sprintf("为下级代理【%s】购买套餐", shop.ShopName)
} else {
remark = "为下级代理购买套餐"
}
}
}
userID := middleware.GetUserIDFromContext(ctx)
// 创建钱包流水记录
transaction := &model.AgentWalletTransaction{
AgentWalletID: walletID,
ShopID: 0, // 将在下面从钱包记录获取
UserID: userID,
TransactionType: constants.AgentTransactionTypeDeduct,
TransactionSubtype: subtype,
Amount: -amount, // 扣款为负数
BalanceBefore: 0, // 将在下面填充
BalanceAfter: 0, // 将在下面填充
Status: constants.TransactionStatusSuccess,
ReferenceType: strPtr(constants.ReferenceTypeOrder),
ReferenceID: &orderID,
RelatedShopID: relatedShopID,
Remark: &remark,
Creator: userID,
ShopIDTag: 0, // 将在下面填充
EnterpriseIDTag: nil,
}
// 查询钱包记录,获取 shop_id 和余额信息
var wallet model.AgentWallet
if err := tx.Where("id = ?", walletID).First(&wallet).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "查询钱包信息失败")
}
transaction.ShopID = wallet.ShopID
transaction.ShopIDTag = wallet.ShopIDTag
transaction.EnterpriseIDTag = wallet.EnterpriseIDTag
transaction.BalanceBefore = wallet.Balance
transaction.BalanceAfter = wallet.Balance - amount
if err := tx.Create(transaction).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "创建钱包流水失败")
}
return nil
}
// strPtr 字符串指针辅助函数
func strPtr(s string) *string {
return &s
}
// createOrderWithWalletPayment 使用钱包支付创建订单并完成支付
// 包含余额检查、扣款、创建流水、激活套餐等操作,在事务中执行
// ctx: 上下文
// order: 订单对象
// items: 订单明细列表
// operatorShopID: 操作者店铺ID扣款的店铺
// buyerShopID: 买家店铺ID代购场景下级店铺ID
func (s *Service) createOrderWithWalletPayment(ctx context.Context, order *model.Order, items []*model.OrderItem, operatorShopID uint, buyerShopID uint) error {
if order.ActualPaidAmount == nil {
return errors.New(errors.CodeInternalError, "实际支付金额不能为空")
}
actualAmount := *order.ActualPaidAmount
// 1. 事务外:检查钱包余额(快速失败)
wallet, err := s.agentWalletStore.GetMainWallet(ctx, operatorShopID)
if err != nil {
if err == gorm.ErrRecordNotFound {
return errors.New(errors.CodeWalletNotFound, "钱包不存在")
}
return errors.Wrap(errors.CodeDatabaseError, err, "查询钱包失败")
}
if wallet.Balance < actualAmount {
return errors.New(errors.CodeInsufficientBalance, "余额不足")
}
// 2. 事务内:创建订单 + 扣款 + 创建流水 + 激活套餐
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
// 2.1 创建订单
if err := tx.Create(order).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "创建订单失败")
}
// 2.2 创建订单明细
for _, item := range items {
item.OrderID = order.ID
}
if err := tx.CreateInBatches(items, 100).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "创建订单明细失败")
}
// 2.3 扣减钱包余额(乐观锁)
result := tx.Model(&model.AgentWallet{}).
Where("id = ? AND balance >= ? AND version = ?", wallet.ID, actualAmount, wallet.Version).
Updates(map[string]any{
"balance": gorm.Expr("balance - ?", actualAmount),
"version": gorm.Expr("version + 1"),
})
if result.Error != nil {
return errors.Wrap(errors.CodeDatabaseError, result.Error, "扣减钱包余额失败")
}
if result.RowsAffected == 0 {
return errors.New(errors.CodeInsufficientBalance, "余额不足或并发冲突")
}
// 2.4 创建钱包流水
var relatedShopID *uint
if order.PurchaseRole == model.PurchaseRolePurchaseForSubordinate {
relatedShopID = &buyerShopID
}
if err := s.createWalletTransaction(ctx, tx, wallet.ID, order.ID, actualAmount, order.PurchaseRole, relatedShopID); err != nil {
return err
}
// 2.5 激活套餐
if err := s.activatePackage(ctx, tx, order); err != nil {
return err
}
return nil
})
if err != nil {
return err
}
// 3. 事务外:佣金计算(异步)
// 只有平台代购才入队佣金计算operator_id == nil
if order.OperatorID == nil {
s.enqueueCommissionCalculation(ctx, order.ID)
}
return nil
}
func (s *Service) createOrderWithActivation(ctx context.Context, order *model.Order, items []*model.OrderItem) error {
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Create(order).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "创建代购订单失败")
}
for _, item := range items {
item.OrderID = order.ID
}
if err := tx.CreateInBatches(items, 100).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "创建订单明细失败")
}
return s.activatePackage(ctx, tx, order)
})
}
func (s *Service) Get(ctx context.Context, id uint) (*dto.OrderResponse, error) {
order, items, err := s.orderStore.GetByIDWithItems(ctx, id)
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeNotFound, "订单不存在")
}
return nil, err
}
return s.buildOrderResponse(order, items), nil
}
func (s *Service) List(ctx context.Context, req *dto.OrderListRequest, buyerType string, buyerID uint) (*dto.OrderListResponse, error) {
page := req.Page
pageSize := req.PageSize
if page == 0 {
page = 1
}
if pageSize == 0 {
pageSize = constants.DefaultPageSize
}
opts := &store.QueryOptions{
Page: page,
PageSize: pageSize,
}
filters := map[string]any{
"buyer_type": buyerType,
"buyer_id": buyerID,
}
if req.PaymentStatus != nil {
filters["payment_status"] = *req.PaymentStatus
}
if req.OrderType != "" {
filters["order_type"] = req.OrderType
}
if req.OrderNo != "" {
filters["order_no"] = req.OrderNo
}
if req.PurchaseRole != "" {
filters["purchase_role"] = req.PurchaseRole
}
if req.StartTime != nil {
filters["start_time"] = req.StartTime
}
if req.EndTime != nil {
filters["end_time"] = req.EndTime
}
orders, total, err := s.orderStore.List(ctx, opts, filters)
if err != nil {
return nil, err
}
var orderIDs []uint
for _, o := range orders {
orderIDs = append(orderIDs, o.ID)
}
itemsMap := make(map[uint][]*model.OrderItem)
if len(orderIDs) > 0 {
allItems, err := s.orderItemStore.ListByOrderIDs(ctx, orderIDs)
if err != nil {
return nil, err
}
for _, item := range allItems {
itemsMap[item.OrderID] = append(itemsMap[item.OrderID], item)
}
}
var list []*dto.OrderResponse
for _, o := range orders {
list = append(list, s.buildOrderResponse(o, itemsMap[o.ID]))
}
totalPages := int(total) / pageSize
if int(total)%pageSize > 0 {
totalPages++
}
return &dto.OrderListResponse{
List: list,
Total: total,
Page: page,
PageSize: pageSize,
TotalPages: totalPages,
}, nil
}
func (s *Service) Cancel(ctx context.Context, id uint, buyerType string, buyerID uint) error {
order, err := s.orderStore.GetByID(ctx, id)
if err != nil {
if err == gorm.ErrRecordNotFound {
return errors.New(errors.CodeNotFound, "订单不存在")
}
return err
}
if order.BuyerType != buyerType || order.BuyerID != buyerID {
return errors.New(errors.CodeForbidden, "无权操作此订单")
}
if order.PaymentStatus != model.PaymentStatusPending {
return errors.New(errors.CodeInvalidStatus, "只能取消待支付的订单")
}
return s.orderStore.UpdatePaymentStatus(ctx, id, model.PaymentStatusCancelled, nil)
}
func (s *Service) WalletPay(ctx context.Context, orderID uint, buyerType string, buyerID uint) error {
order, err := s.orderStore.GetByID(ctx, orderID)
if err != nil {
if err == gorm.ErrRecordNotFound {
return errors.New(errors.CodeNotFound, "订单不存在")
}
return err
}
if order.BuyerType != buyerType || order.BuyerID != buyerID {
return errors.New(errors.CodeForbidden, "无权操作此订单")
}
if order.IsPurchaseOnBehalf {
return errors.New(errors.CodeInvalidStatus, "代购订单无需支付")
}
var resourceType string
var resourceID uint
if buyerType == model.BuyerTypePersonal {
if order.OrderType == model.OrderTypeSingleCard && order.IotCardID != nil {
resourceType = "iot_card"
resourceID = *order.IotCardID
} else if order.OrderType == model.OrderTypeDevice && order.DeviceID != nil {
resourceType = "device"
resourceID = *order.DeviceID
} else {
return errors.New(errors.CodeInvalidParam, "无法确定钱包归属")
}
} else if buyerType == model.BuyerTypeAgent {
resourceType = "shop"
resourceID = buyerID
} else {
return errors.New(errors.CodeInvalidParam, "不支持的买家类型")
}
// 根据资源类型选择对应的钱包系统
now := time.Now()
if resourceType == "shop" {
// 代理钱包系统(店铺)
wallet, err := s.agentWalletStore.GetMainWallet(ctx, resourceID)
if err != nil {
if err == gorm.ErrRecordNotFound {
return errors.New(errors.CodeWalletNotFound, "钱包不存在")
}
return err
}
if wallet.Balance < order.TotalAmount {
return errors.New(errors.CodeInsufficientBalance, "余额不足")
}
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
result := tx.Model(&model.Order{}).
Where("id = ? AND payment_status = ?", orderID, model.PaymentStatusPending).
Updates(map[string]any{
"payment_status": model.PaymentStatusPaid,
"payment_method": model.PaymentMethodWallet,
"paid_at": now,
})
if result.Error != nil {
return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新订单支付状态失败")
}
if result.RowsAffected == 0 {
var currentOrder model.Order
if err := tx.First(&currentOrder, orderID).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "查询订单失败")
}
switch currentOrder.PaymentStatus {
case model.PaymentStatusPaid:
return nil
case model.PaymentStatusCancelled:
return errors.New(errors.CodeInvalidStatus, "订单已取消,无法支付")
case model.PaymentStatusRefunded:
return errors.New(errors.CodeInvalidStatus, "订单已退款,无法支付")
default:
return errors.New(errors.CodeInvalidStatus, "订单状态异常")
}
}
walletResult := tx.Model(&model.AgentWallet{}).
Where("id = ? AND balance >= ? AND version = ?", wallet.ID, order.TotalAmount, wallet.Version).
Updates(map[string]any{
"balance": gorm.Expr("balance - ?", order.TotalAmount),
"version": gorm.Expr("version + 1"),
})
if walletResult.Error != nil {
return errors.Wrap(errors.CodeDatabaseError, walletResult.Error, "扣减钱包余额失败")
}
if walletResult.RowsAffected == 0 {
return errors.New(errors.CodeInsufficientBalance, "余额不足或并发冲突")
}
return s.activatePackage(ctx, tx, order)
})
} else {
// 卡钱包系统iot_card 或 device
wallet, err := s.cardWalletStore.GetByResourceTypeAndID(ctx, resourceType, resourceID)
if err != nil {
if err == gorm.ErrRecordNotFound {
return errors.New(errors.CodeWalletNotFound, "钱包不存在")
}
return err
}
if wallet.Balance < order.TotalAmount {
return errors.New(errors.CodeInsufficientBalance, "余额不足")
}
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
result := tx.Model(&model.Order{}).
Where("id = ? AND payment_status = ?", orderID, model.PaymentStatusPending).
Updates(map[string]any{
"payment_status": model.PaymentStatusPaid,
"payment_method": model.PaymentMethodWallet,
"paid_at": now,
})
if result.Error != nil {
return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新订单支付状态失败")
}
if result.RowsAffected == 0 {
var currentOrder model.Order
if err := tx.First(&currentOrder, orderID).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "查询订单失败")
}
switch currentOrder.PaymentStatus {
case model.PaymentStatusPaid:
return nil
case model.PaymentStatusCancelled:
return errors.New(errors.CodeInvalidStatus, "订单已取消,无法支付")
case model.PaymentStatusRefunded:
return errors.New(errors.CodeInvalidStatus, "订单已退款,无法支付")
default:
return errors.New(errors.CodeInvalidStatus, "订单状态异常")
}
}
walletResult := tx.Model(&model.CardWallet{}).
Where("id = ? AND balance >= ? AND version = ?", wallet.ID, order.TotalAmount, wallet.Version).
Updates(map[string]any{
"balance": gorm.Expr("balance - ?", order.TotalAmount),
"version": gorm.Expr("version + 1"),
})
if walletResult.Error != nil {
return errors.Wrap(errors.CodeDatabaseError, walletResult.Error, "扣减钱包余额失败")
}
if walletResult.RowsAffected == 0 {
return errors.New(errors.CodeInsufficientBalance, "余额不足或并发冲突")
}
return s.activatePackage(ctx, tx, order)
})
}
if err != nil {
return err
}
s.enqueueCommissionCalculation(ctx, orderID)
return nil
}
func (s *Service) HandlePaymentCallback(ctx context.Context, orderNo string, paymentMethod string) error {
order, err := s.orderStore.GetByOrderNo(ctx, orderNo)
if err != nil {
if err == gorm.ErrRecordNotFound {
return errors.New(errors.CodeNotFound, "订单不存在")
}
return err
}
now := time.Now()
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
result := tx.Model(&model.Order{}).
Where("id = ? AND payment_status = ?", order.ID, model.PaymentStatusPending).
Updates(map[string]any{
"payment_status": model.PaymentStatusPaid,
"payment_method": paymentMethod,
"paid_at": now,
})
if result.Error != nil {
return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新订单支付状态失败")
}
if result.RowsAffected == 0 {
var currentOrder model.Order
if err := tx.First(&currentOrder, order.ID).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "查询订单失败")
}
switch currentOrder.PaymentStatus {
case model.PaymentStatusPaid:
return nil
case model.PaymentStatusCancelled:
return errors.New(errors.CodeInvalidStatus, "订单已取消,无法支付")
case model.PaymentStatusRefunded:
return errors.New(errors.CodeInvalidStatus, "订单已退款,无法支付")
default:
return errors.New(errors.CodeInvalidStatus, "订单状态异常")
}
}
return s.activatePackage(ctx, tx, order)
})
if err != nil {
return err
}
s.enqueueCommissionCalculation(ctx, order.ID)
return nil
}
func (s *Service) activatePackage(ctx context.Context, tx *gorm.DB, order *model.Order) error {
var items []*model.OrderItem
if err := tx.Where("order_id = ?", order.ID).Find(&items).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "查询订单明细失败")
}
// 任务 8.1: 检查混买限制 - 禁止同订单混买正式套餐和加油包
if err := s.validatePackageTypeMix(tx, items); err != nil {
return err
}
// 确定载体类型和ID
carrierType := "iot_card"
var carrierID uint
if order.OrderType == model.OrderTypeSingleCard && order.IotCardID != nil {
carrierID = *order.IotCardID
} else if order.OrderType == model.OrderTypeDevice && order.DeviceID != nil {
carrierType = "device"
carrierID = *order.DeviceID
} else {
return errors.New(errors.CodeInvalidParam, "无效的订单类型或缺少载体ID")
}
now := time.Now()
for _, item := range items {
// 检查是否已存在使用记录
var existingUsage model.PackageUsage
err := tx.Where("order_id = ? AND package_id = ?", order.ID, item.PackageID).
First(&existingUsage).Error
if err == nil {
s.logger.Warn("套餐使用记录已存在,跳过创建",
zap.Uint("order_id", order.ID),
zap.Uint("package_id", item.PackageID))
continue
}
if err != gorm.ErrRecordNotFound {
return errors.Wrap(errors.CodeDatabaseError, err, "检查套餐使用记录失败")
}
// 查询套餐信息
var pkg model.Package
if err := tx.First(&pkg, item.PackageID).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "查询套餐信息失败")
}
// 根据套餐类型分别处理
if pkg.PackageType == "formal" {
// 主套餐处理逻辑(任务 8.2-8.4
if err := s.activateMainPackage(ctx, tx, order, &pkg, carrierType, carrierID, now); err != nil {
return err
}
} else if pkg.PackageType == "addon" {
// 加油包处理逻辑(任务 8.5-8.7
if err := s.activateAddonPackage(ctx, tx, order, &pkg, carrierType, carrierID, now); err != nil {
return err
}
}
}
return nil
}
// validatePackageTypeMix 任务 8.1: 检查混买限制
func (s *Service) validatePackageTypeMix(tx *gorm.DB, items []*model.OrderItem) error {
hasFormal := false
hasAddon := false
for _, item := range items {
var pkg model.Package
if err := tx.First(&pkg, item.PackageID).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "查询套餐信息失败")
}
if pkg.PackageType == constants.PackageTypeFormal {
hasFormal = true
} else if pkg.PackageType == constants.PackageTypeAddon {
hasAddon = true
}
if hasFormal && hasAddon {
return errors.New(errors.CodeInvalidParam, "不允许在同一订单中同时购买正式套餐和加油包")
}
}
return nil
}
// validatePackageTypeMixFromPackages 基于已加载的套餐列表检查混买限制(无需 DB 查询)
func validatePackageTypeMixFromPackages(packages []*model.Package) error {
hasFormal := false
hasAddon := false
for _, pkg := range packages {
if pkg.PackageType == constants.PackageTypeFormal {
hasFormal = true
} else if pkg.PackageType == constants.PackageTypeAddon {
hasAddon = true
}
if hasFormal && hasAddon {
return errors.New(errors.CodeInvalidParam, "不允许在同一订单中同时购买正式套餐和加油包")
}
}
return nil
}
// resolveCarrierInfo 从请求中提取载体类型和ID
func resolveCarrierInfo(req *dto.CreateOrderRequest) (carrierType string, carrierID uint) {
if req.OrderType == model.OrderTypeSingleCard && req.IotCardID != nil {
return "iot_card", *req.IotCardID
}
if req.OrderType == model.OrderTypeDevice && req.DeviceID != nil {
return "device", *req.DeviceID
}
return "", 0
}
// resolveAdminCarrierInfo 从后台订单请求中提取载体类型和ID
func resolveAdminCarrierInfo(req *dto.CreateAdminOrderRequest) (carrierType string, carrierID uint) {
if req.OrderType == model.OrderTypeSingleCard && req.IotCardID != nil {
return "iot_card", *req.IotCardID
}
if req.OrderType == model.OrderTypeDevice && req.DeviceID != nil {
return "device", *req.DeviceID
}
return "", 0
}
// buildOrderIdempotencyKey 生成订单创建的幂等性业务键
// 格式: {buyer_type}:{buyer_id}:{order_type}:{carrier_type}:{carrier_id}:{sorted_package_ids}
func buildOrderIdempotencyKey(buyerType string, buyerID uint, orderType string, carrierType string, carrierID uint, packageIDs []uint) string {
sortedIDs := make([]uint, len(packageIDs))
copy(sortedIDs, packageIDs)
sort.Slice(sortedIDs, func(i, j int) bool { return sortedIDs[i] < sortedIDs[j] })
idStrs := make([]string, len(sortedIDs))
for i, id := range sortedIDs {
idStrs[i] = strconv.FormatUint(uint64(id), 10)
}
return fmt.Sprintf("%s:%d:%s:%s:%d:%s",
buyerType, buyerID, orderType, carrierType, carrierID, strings.Join(idStrs, ","))
}
const (
orderIdempotencyTTL = 3 * time.Minute
orderCreateLockTTL = 10 * time.Second
)
// checkOrderIdempotency 检查订单是否已创建Redis SETNX + 分布式锁)
// 返回已存在的 orderID>0 表示重复请求),或 0 表示可以创续创建
func (s *Service) checkOrderIdempotency(ctx context.Context, buyerType string, buyerID uint, orderType string, carrierType string, carrierID uint, packageIDs []uint) (uint, error) {
idempotencyKey := buildOrderIdempotencyKey(buyerType, buyerID, orderType, carrierType, carrierID, packageIDs)
redisKey := constants.RedisOrderIdempotencyKey(idempotencyKey)
// 第 1 层Redis 快速检测,如果 key 已存在说明已创建或正在创建
val, err := s.redis.Get(ctx, redisKey).Result()
if err == nil && val != "" {
orderID, _ := strconv.ParseUint(val, 10, 64)
if orderID > 0 {
s.logger.Info("订单幂等性命中,返回已有订单",
zap.Uint("order_id", uint(orderID)),
zap.String("idempotency_key", idempotencyKey))
return uint(orderID), nil
}
}
// 第 2 层:分布式锁,防止并发创建
lockKey := constants.RedisOrderCreateLockKey(carrierType, carrierID)
locked, err := s.redis.SetNX(ctx, lockKey, time.Now().String(), orderCreateLockTTL).Result()
if err != nil {
s.logger.Warn("获取订单创建分布式锁失败,继续执行",
zap.Error(err),
zap.String("lock_key", lockKey))
return 0, nil
}
if !locked {
return 0, errors.New(errors.CodeTooManyRequests, "订单正在创建中,请勿重复提交")
}
// 第 3 层:加锁后二次检测,防止锁等待期间已被处理
val, err = s.redis.Get(ctx, redisKey).Result()
if err == nil && val != "" {
orderID, _ := strconv.ParseUint(val, 10, 64)
if orderID > 0 {
s.logger.Info("订单幂等性二次检测命中",
zap.Uint("order_id", uint(orderID)),
zap.String("idempotency_key", idempotencyKey))
return uint(orderID), nil
}
}
return 0, nil
}
// markOrderCreated 订单创建成功后标记 Redis 并释放分布式锁
func (s *Service) markOrderCreated(ctx context.Context, idempotencyKey string, orderID uint) {
redisKey := constants.RedisOrderIdempotencyKey(idempotencyKey)
if err := s.redis.Set(ctx, redisKey, strconv.FormatUint(uint64(orderID), 10), orderIdempotencyTTL).Err(); err != nil {
s.logger.Warn("设置订单幂等性标记失败",
zap.Error(err),
zap.Uint("order_id", orderID))
}
}
// activateMainPackage 任务 8.2-8.4: 主套餐激活逻辑
func (s *Service) activateMainPackage(ctx context.Context, tx *gorm.DB, order *model.Order, pkg *model.Package, carrierType string, carrierID uint, now time.Time) error {
// 检查是否有生效中主套餐
var activeMainPackage model.PackageUsage
err := tx.Where("status = ?", constants.PackageUsageStatusActive).
Where("master_usage_id IS NULL").
Where(carrierType+"_id = ?", carrierID).
Order("priority ASC").
First(&activeMainPackage).Error
hasActiveMain := err == nil
var status int
var priority int
var activatedAt time.Time
var expiresAt time.Time
var nextResetAt *time.Time
var pendingRealnameActivation bool
if hasActiveMain {
// 任务 8.3: 有生效中主套餐,新套餐排队
status = constants.PackageUsageStatusPending
// 查询当前最大优先级
var maxPriority int
tx.Model(&model.PackageUsage{}).
Where(carrierType+"_id = ?", carrierID).
Select("COALESCE(MAX(priority), 0)").
Scan(&maxPriority)
priority = maxPriority + 1
// 排队套餐暂不设置激活时间和过期时间(由激活任务处理)
} else {
// 任务 8.4: 无生效中主套餐,立即激活
status = constants.PackageUsageStatusActive
priority = 1
activatedAt = now
// 使用工具函数计算过期时间
expiresAt = packagepkg.CalculateExpiryTime(pkg.CalendarType, activatedAt, pkg.DurationMonths, pkg.DurationDays)
// 计算下次重置时间(基于套餐周期类型)
nextResetAt = packagepkg.CalculateNextResetTime(pkg.DataResetCycle, pkg.CalendarType, now, activatedAt)
}
// 任务 8.9: 后台囤货场景
if pkg.EnableRealnameActivation {
// 需要实名后才能激活
status = constants.PackageUsageStatusPending
pendingRealnameActivation = true
}
// 创建套餐使用记录
usage := &model.PackageUsage{
BaseModel: model.BaseModel{
Creator: order.Creator,
Updater: order.Creator,
},
OrderID: order.ID,
PackageID: pkg.ID,
UsageType: order.OrderType,
DataLimitMB: pkg.RealDataMB,
Status: status,
Priority: priority,
DataResetCycle: pkg.DataResetCycle,
PendingRealnameActivation: pendingRealnameActivation,
}
if carrierType == "iot_card" {
usage.IotCardID = carrierID
} else {
usage.DeviceID = carrierID
}
if status == constants.PackageUsageStatusActive {
usage.ActivatedAt = activatedAt
usage.ExpiresAt = expiresAt
usage.NextResetAt = nextResetAt
}
// 创建套餐使用记录(两步处理零值问题)
if err := tx.Omit("status", "pending_realname_activation").Create(usage).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "创建主套餐使用记录失败")
}
// 明确更新零值字段
if err := tx.Model(usage).Updates(map[string]interface{}{
"status": usage.Status,
"pending_realname_activation": usage.PendingRealnameActivation,
}).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "更新主套餐状态失败")
}
return nil
}
// activateAddonPackage 任务 8.5-8.7: 加油包激活逻辑
func (s *Service) activateAddonPackage(ctx context.Context, tx *gorm.DB, order *model.Order, pkg *model.Package, carrierType string, carrierID uint, now time.Time) error {
// 任务 8.5-8.6: 检查是否有主套餐status IN (0,1)
var mainPackage model.PackageUsage
err := tx.Where("status IN ?", []int{constants.PackageUsageStatusPending, constants.PackageUsageStatusActive}).
Where("master_usage_id IS NULL").
Where(carrierType+"_id = ?", carrierID).
Order("priority ASC").
First(&mainPackage).Error
if err == gorm.ErrRecordNotFound {
return errors.New(errors.CodeInvalidParam, "必须有主套餐才能购买加油包")
}
if err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "查询主套餐失败")
}
// 任务 8.7: 创建加油包,绑定到主套餐
// 查询当前最大优先级(加油包优先级低于主套餐)
var maxPriority int
tx.Model(&model.PackageUsage{}).
Where(carrierType+"_id = ?", carrierID).
Select("COALESCE(MAX(priority), 0)").
Scan(&maxPriority)
priority := maxPriority + 1
// 加油包立即生效
status := constants.PackageUsageStatusActive
activatedAt := now
// 计算过期时间(根据 has_independent_expiry
var expiresAt time.Time
// 注意has_independent_expiry 字段在 Package 模型中,暂时使用默认行为
// 默认加油包跟随主套餐过期
expiresAt = mainPackage.ExpiresAt
usage := &model.PackageUsage{
BaseModel: model.BaseModel{
Creator: order.Creator,
Updater: order.Creator,
},
OrderID: order.ID,
PackageID: pkg.ID,
UsageType: order.OrderType,
DataLimitMB: pkg.RealDataMB,
Status: status,
Priority: priority,
MasterUsageID: &mainPackage.ID,
ActivatedAt: activatedAt,
ExpiresAt: expiresAt,
DataResetCycle: pkg.DataResetCycle,
}
if carrierType == "iot_card" {
usage.IotCardID = carrierID
} else {
usage.DeviceID = carrierID
}
// 创建加油包使用记录(加油包 status=1不需要处理零值
if err := tx.Create(usage).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "创建加油包使用记录失败")
}
return nil
}
func (s *Service) enqueueCommissionCalculation(ctx context.Context, orderID uint) {
if s.queueClient == nil {
s.logger.Warn("队列客户端未初始化,跳过佣金计算任务入队", zap.Uint("order_id", orderID))
return
}
payload := map[string]interface{}{
"order_id": orderID,
}
payloadBytes, err := sonic.Marshal(payload)
if err != nil {
s.logger.Error("佣金计算任务载荷序列化失败",
zap.Uint("order_id", orderID),
zap.Error(err))
return
}
if err := s.queueClient.EnqueueTask(ctx, constants.TaskTypeCommission, payloadBytes); err != nil {
s.logger.Error("佣金计算任务入队失败",
zap.Uint("order_id", orderID),
zap.Error(err),
zap.String("task_type", constants.TaskTypeCommission))
return
}
s.logger.Info("佣金计算任务已入队",
zap.Uint("order_id", orderID),
zap.String("task_type", constants.TaskTypeCommission))
}
func (s *Service) buildOrderResponse(order *model.Order, items []*model.OrderItem) *dto.OrderResponse {
var itemResponses []*dto.OrderItemResponse
for _, item := range items {
itemResponses = append(itemResponses, &dto.OrderItemResponse{
ID: item.ID,
PackageID: item.PackageID,
PackageName: item.PackageName,
Quantity: item.Quantity,
UnitPrice: item.UnitPrice,
Amount: item.Amount,
})
}
statusText := ""
switch order.PaymentStatus {
case model.PaymentStatusPending:
statusText = "待支付"
case model.PaymentStatusPaid:
statusText = "已支付"
case model.PaymentStatusCancelled:
statusText = "已取消"
case model.PaymentStatusRefunded:
statusText = "已退款"
}
// 查询操作者名称
operatorName := ""
if order.OperatorType == "agent" && order.OperatorID != nil {
var shop model.Shop
if err := s.db.Where("id = ?", *order.OperatorID).First(&shop).Error; err == nil {
operatorName = shop.ShopName
}
}
// 生成派生字段
isPurchasedByParent := order.PurchaseRole == model.PurchaseRolePurchasedByParent
purchaseRemark := ""
switch order.PurchaseRole {
case model.PurchaseRolePurchasedByParent:
if operatorName != "" {
purchaseRemark = fmt.Sprintf("由上级代理【%s】购买", operatorName)
} else {
purchaseRemark = "由上级代理购买"
}
case model.PurchaseRolePurchasedByPlatform:
purchaseRemark = "由平台代购"
case model.PurchaseRolePurchaseForSubordinate:
if operatorName != "" {
purchaseRemark = fmt.Sprintf("由【%s】为下级购买", operatorName)
}
}
return &dto.OrderResponse{
ID: order.ID,
OrderNo: order.OrderNo,
OrderType: order.OrderType,
BuyerType: order.BuyerType,
BuyerID: order.BuyerID,
IotCardID: order.IotCardID,
DeviceID: order.DeviceID,
TotalAmount: order.TotalAmount,
PaymentMethod: order.PaymentMethod,
PaymentStatus: order.PaymentStatus,
PaymentStatusText: statusText,
PaidAt: order.PaidAt,
IsPurchaseOnBehalf: order.IsPurchaseOnBehalf,
CommissionStatus: order.CommissionStatus,
CommissionConfigVersion: order.CommissionConfigVersion,
// 操作者信息
OperatorID: order.OperatorID,
OperatorType: order.OperatorType,
OperatorName: operatorName,
ActualPaidAmount: order.ActualPaidAmount,
// 订单角色
PurchaseRole: order.PurchaseRole,
IsPurchasedByParent: isPurchasedByParent,
PurchaseRemark: purchaseRemark,
Items: itemResponses,
CreatedAt: order.CreatedAt,
UpdatedAt: order.UpdatedAt,
}
}
// WechatPayJSAPI 发起微信 JSAPI 支付
func (s *Service) WechatPayJSAPI(ctx context.Context, orderID uint, openID string, buyerType string, buyerID uint) (*dto.WechatPayJSAPIResponse, error) {
if s.wechatPayment == nil {
s.logger.Error("微信支付服务未配置")
return nil, errors.New(errors.CodeWechatPayFailed, "微信支付服务未配置")
}
order, err := s.orderStore.GetByID(ctx, orderID)
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeNotFound, "订单不存在")
}
return nil, errors.Wrap(errors.CodeInternalError, err, "查询订单失败")
}
if order.BuyerType != buyerType || order.BuyerID != buyerID {
return nil, errors.New(errors.CodeForbidden, "无权操作此订单")
}
if order.PaymentStatus != model.PaymentStatusPending {
return nil, errors.New(errors.CodeInvalidStatus, "订单状态不允许支付")
}
items, err := s.orderItemStore.ListByOrderIDs(ctx, []uint{orderID})
if err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "查询订单明细失败")
}
description := "套餐购买"
if len(items) > 0 {
description = items[0].PackageName
}
result, err := s.wechatPayment.CreateJSAPIOrder(ctx, order.OrderNo, description, openID, int(order.TotalAmount))
if err != nil {
s.logger.Error("创建 JSAPI 支付失败",
zap.Uint("order_id", orderID),
zap.String("order_no", order.OrderNo),
zap.Error(err),
)
return nil, err
}
s.logger.Info("创建 JSAPI 支付成功",
zap.Uint("order_id", orderID),
zap.String("order_no", order.OrderNo),
zap.String("prepay_id", result.PrepayID),
)
payConfig, _ := result.PayConfig.(map[string]interface{})
return &dto.WechatPayJSAPIResponse{
PrepayID: result.PrepayID,
PayConfig: payConfig,
}, nil
}
// WechatPayH5 发起微信 H5 支付
func (s *Service) WechatPayH5(ctx context.Context, orderID uint, sceneInfo *dto.WechatH5SceneInfo, buyerType string, buyerID uint) (*dto.WechatPayH5Response, error) {
if s.wechatPayment == nil {
s.logger.Error("微信支付服务未配置")
return nil, errors.New(errors.CodeWechatPayFailed, "微信支付服务未配置")
}
order, err := s.orderStore.GetByID(ctx, orderID)
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeNotFound, "订单不存在")
}
return nil, errors.Wrap(errors.CodeInternalError, err, "查询订单失败")
}
if order.BuyerType != buyerType || order.BuyerID != buyerID {
return nil, errors.New(errors.CodeForbidden, "无权操作此订单")
}
if order.PaymentStatus != model.PaymentStatusPending {
return nil, errors.New(errors.CodeInvalidStatus, "订单状态不允许支付")
}
items, err := s.orderItemStore.ListByOrderIDs(ctx, []uint{orderID})
if err != nil {
return nil, errors.Wrap(errors.CodeInternalError, err, "查询订单明细失败")
}
description := "套餐购买"
if len(items) > 0 {
description = items[0].PackageName
}
h5SceneInfo := &wechat.H5SceneInfo{
PayerClientIP: sceneInfo.PayerClientIP,
H5Type: sceneInfo.H5Info.Type,
}
result, err := s.wechatPayment.CreateH5Order(ctx, order.OrderNo, description, int(order.TotalAmount), h5SceneInfo)
if err != nil {
s.logger.Error("创建 H5 支付失败",
zap.Uint("order_id", orderID),
zap.String("order_no", order.OrderNo),
zap.Error(err),
)
return nil, err
}
s.logger.Info("创建 H5 支付成功",
zap.Uint("order_id", orderID),
zap.String("order_no", order.OrderNo),
zap.String("h5_url", result.H5URL),
)
return &dto.WechatPayH5Response{
H5URL: result.H5URL,
}, nil
}
type ForceRechargeRequirement struct {
NeedForceRecharge bool
ForceRechargeAmount int64
TriggerType string
}
// checkForceRechargeRequirement 检查强充要求
// 从 PackageSeries 获取一次性佣金配置,使用 per-series 追踪判断是否需要强充
func (s *Service) checkForceRechargeRequirement(ctx context.Context, result *purchase_validation.PurchaseValidationResult) *ForceRechargeRequirement {
defaultResult := &ForceRechargeRequirement{NeedForceRecharge: false}
// 1. 获取 seriesID
var seriesID *uint
var firstCommissionPaid bool
if result.Card != nil {
seriesID = result.Card.SeriesID
if seriesID != nil {
firstCommissionPaid = result.Card.IsFirstRechargeTriggeredBySeries(*seriesID)
}
} else if result.Device != nil {
seriesID = result.Device.SeriesID
if seriesID != nil {
firstCommissionPaid = result.Device.IsFirstRechargeTriggeredBySeries(*seriesID)
}
}
if seriesID == nil {
return defaultResult
}
// 2. 从 PackageSeries 获取一次性佣金配置
series, err := s.packageSeriesStore.GetByID(ctx, *seriesID)
if err != nil {
s.logger.Warn("查询套餐系列失败", zap.Uint("series_id", *seriesID), zap.Error(err))
return defaultResult
}
config, err := series.GetOneTimeCommissionConfig()
if err != nil || config == nil || !config.Enable {
return defaultResult
}
// 3. 如果该系列的一次性佣金已发放,无需强充
if firstCommissionPaid {
return defaultResult
}
// 4. 根据触发类型判断是否需要强充
if config.TriggerType == model.OneTimeCommissionTriggerFirstRecharge {
return &ForceRechargeRequirement{
NeedForceRecharge: true,
ForceRechargeAmount: config.Threshold,
TriggerType: model.OneTimeCommissionTriggerFirstRecharge,
}
}
// 5. 累计充值模式,检查是否启用强充
if config.EnableForceRecharge {
forceAmount := config.ForceAmount
if forceAmount == 0 {
forceAmount = config.Threshold
}
return &ForceRechargeRequirement{
NeedForceRecharge: true,
ForceRechargeAmount: forceAmount,
TriggerType: model.OneTimeCommissionTriggerAccumulatedRecharge,
}
}
return defaultResult
}
func (s *Service) GetPurchaseCheck(ctx context.Context, req *dto.PurchaseCheckRequest) (*dto.PurchaseCheckResponse, error) {
var validationResult *purchase_validation.PurchaseValidationResult
var err error
if req.OrderType == model.OrderTypeSingleCard {
validationResult, err = s.purchaseValidationService.ValidateCardPurchase(ctx, req.ResourceID, req.PackageIDs)
} else if req.OrderType == model.OrderTypeDevice {
validationResult, err = s.purchaseValidationService.ValidateDevicePurchase(ctx, req.ResourceID, req.PackageIDs)
} else {
return nil, errors.New(errors.CodeInvalidParam, "无效的订单类型")
}
if err != nil {
return nil, err
}
forceRechargeCheck := s.checkForceRechargeRequirement(ctx, validationResult)
response := &dto.PurchaseCheckResponse{
TotalPackageAmount: validationResult.TotalPrice,
NeedForceRecharge: forceRechargeCheck.NeedForceRecharge,
ForceRechargeAmount: forceRechargeCheck.ForceRechargeAmount,
ActualPayment: validationResult.TotalPrice,
WalletCredit: validationResult.TotalPrice,
}
if forceRechargeCheck.NeedForceRecharge {
if validationResult.TotalPrice < forceRechargeCheck.ForceRechargeAmount {
response.ActualPayment = forceRechargeCheck.ForceRechargeAmount
response.WalletCredit = forceRechargeCheck.ForceRechargeAmount
response.Message = "首次购买需满足最低充值要求"
}
}
return response, nil
}