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 var expiresAt *time.Time // 待支付订单设置过期时间,立即支付的订单为 nil // 场景判断: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, ExpiresAt: expiresAt, } 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) // 待支付订单设置过期时间,超过 30 分钟未支付则自动取消 expireTime := now.Add(constants.OrderExpireTimeout) order.ExpiresAt = &expireTime 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.cancelOrder(ctx, order) } // CancelExpiredOrders 批量取消已超时的待支付订单 // 返回已取消的订单数量和错误 func (s *Service) CancelExpiredOrders(ctx context.Context) (int, error) { startTime := time.Now() orders, err := s.orderStore.FindExpiredOrders(ctx, constants.OrderExpireBatchSize) if err != nil { return 0, errors.Wrap(errors.CodeDatabaseError, err, "查询超时订单失败") } if len(orders) == 0 { return 0, nil } cancelledCount := 0 for _, order := range orders { if err := s.cancelOrder(ctx, order); err != nil { s.logger.Error("自动取消超时订单失败", zap.Uint("order_id", order.ID), zap.String("order_no", order.OrderNo), zap.Error(err), ) continue } cancelledCount++ } s.logger.Info("批量取消超时订单完成", zap.Int("total", len(orders)), zap.Int("cancelled", cancelledCount), zap.Int("failed", len(orders)-cancelledCount), zap.Duration("duration", time.Since(startTime)), ) return cancelledCount, nil } // cancelOrder 内部取消订单逻辑(共用于手动取消和自动超时取消) // 在事务中执行:更新订单状态为已取消、清除过期时间、解冻钱包余额(如有) func (s *Service) cancelOrder(ctx context.Context, order *model.Order) error { return 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.PaymentStatusCancelled, "expires_at": nil, }) if result.Error != nil { return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新订单状态失败") } if result.RowsAffected == 0 { // 订单已被处理(幂等),直接返回 return nil } // 检查是否需要解冻钱包余额(混合支付场景) // 当前系统中钱包支付订单是立即支付的,不会进入待支付状态 // 此处为预留逻辑,支持未来混合支付场景的钱包解冻 if order.PaymentMethod == model.PaymentMethodWallet { if err := s.unfreezeWalletForCancel(ctx, tx, order); err != nil { s.logger.Error("取消订单时解冻钱包失败", zap.Uint("order_id", order.ID), zap.Error(err), ) return err } } return nil }) } // unfreezeWalletForCancel 取消订单时解冻钱包余额 // 根据买家类型和订单金额确定解冻金额和目标钱包 func (s *Service) unfreezeWalletForCancel(ctx context.Context, tx *gorm.DB, order *model.Order) error { if order.BuyerType == model.BuyerTypeAgent { // 代理商钱包(店铺钱包) wallet, err := s.agentWalletStore.GetMainWallet(ctx, order.BuyerID) if err != nil { return errors.Wrap(errors.CodeWalletNotFound, err, "查询代理钱包失败") } return s.agentWalletStore.UnfreezeBalanceWithTx(ctx, tx, wallet.ID, order.TotalAmount) } else if order.BuyerType == model.BuyerTypePersonal { // 个人客户钱包(卡/设备钱包) var resourceType string var resourceID uint 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.CodeInternalError, "无法确定钱包归属") } wallet, err := s.cardWalletStore.GetByResourceTypeAndID(ctx, resourceType, resourceID) if err != nil { return errors.Wrap(errors.CodeWalletNotFound, err, "查询卡钱包失败") } // 卡钱包解冻:直接减少冻结余额 result := tx.Model(&model.CardWallet{}). Where("id = ? AND frozen_balance >= ?", wallet.ID, order.TotalAmount). Updates(map[string]any{ "frozen_balance": gorm.Expr("frozen_balance - ?", order.TotalAmount), }) if result.Error != nil { return result.Error } if result.RowsAffected == 0 { return errors.New(errors.CodeInsufficientBalance, "冻结余额不足,无法解冻") } return nil } return 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, "expires_at": nil, // 支付成功,清除过期时间 }) if result.Error != nil { return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新订单支付状态失败") } if result.RowsAffected == 0 { var currentOrder model.Order if err := tx.First(¤tOrder, 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, "expires_at": nil, // 支付成功,清除过期时间 }) if result.Error != nil { return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新订单支付状态失败") } if result.RowsAffected == 0 { var currentOrder model.Order if err := tx.First(¤tOrder, 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, "expires_at": nil, // 支付成功,清除过期时间 }) if result.Error != nil { return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新订单支付状态失败") } if result.RowsAffected == 0 { var currentOrder model.Order if err := tx.First(¤tOrder, 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, // 订单超时信息 ExpiresAt: order.ExpiresAt, IsExpired: order.ExpiresAt != nil && order.PaymentStatus == model.PaymentStatusPending && time.Now().After(*order.ExpiresAt), } } // 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 }