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/packageprice" "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/fuiou" "github.com/break/junhong_cmp_fiber/pkg/middleware" "github.com/break/junhong_cmp_fiber/pkg/payment" "github.com/break/junhong_cmp_fiber/pkg/queue" "github.com/break/junhong_cmp_fiber/pkg/wechat" "github.com/redis/go-redis/v9" "go.uber.org/zap" "gorm.io/gorm" "gorm.io/gorm/clause" ) // WechatConfigServiceInterface 支付配置服务接口 // 用于查询当前生效的支付配置 type WechatConfigServiceInterface interface { GetActiveConfig(ctx context.Context) (*model.WechatConfig, error) } type Service struct { db *gorm.DB redis *redis.Client orderStore *postgres.OrderStore orderItemStore *postgres.OrderItemStore agentWalletStore *postgres.AgentWalletStore assetWalletStore *postgres.AssetWalletStore paymentStore *postgres.PaymentStore 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 wechatConfigService WechatConfigServiceInterface wechatPayment wechat.PaymentServiceInterface paymentLoader payment.PaymentConfigLoader queueClient *queue.Client logger *zap.Logger resumeCallback packagepkg.ResumeCallback assetIdentifierStore *postgres.AssetIdentifierStore personalCustomerStore *postgres.PersonalCustomerStore personalCustomerPhoneStore *postgres.PersonalCustomerPhoneStore } func New( db *gorm.DB, redisClient *redis.Client, orderStore *postgres.OrderStore, orderItemStore *postgres.OrderItemStore, agentWalletStore *postgres.AgentWalletStore, assetWalletStore *postgres.AssetWalletStore, paymentStore *postgres.PaymentStore, 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, wechatConfigService WechatConfigServiceInterface, wechatPayment wechat.PaymentServiceInterface, paymentLoader payment.PaymentConfigLoader, queueClient *queue.Client, logger *zap.Logger, assetIdentifierStore *postgres.AssetIdentifierStore, personalCustomerStore *postgres.PersonalCustomerStore, personalCustomerPhoneStore *postgres.PersonalCustomerPhoneStore, ) *Service { return &Service{ db: db, redis: redisClient, orderStore: orderStore, orderItemStore: orderItemStore, agentWalletStore: agentWalletStore, assetWalletStore: assetWalletStore, paymentStore: paymentStore, purchaseValidationService: purchaseValidationService, shopPackageAllocationStore: shopPackageAllocationStore, shopSeriesAllocationStore: shopSeriesAllocationStore, iotCardStore: iotCardStore, deviceStore: deviceStore, packageSeriesStore: packageSeriesStore, packageUsageStore: packageUsageStore, packageStore: packageStore, wechatConfigService: wechatConfigService, wechatPayment: wechatPayment, paymentLoader: paymentLoader, queueClient: queueClient, logger: logger, assetIdentifierStore: assetIdentifierStore, personalCustomerStore: personalCustomerStore, personalCustomerPhoneStore: personalCustomerPhoneStore, } } // SetResumeCallback 设置复机回调,支付成功激活套餐后触发自动复机 func (s *Service) SetResumeCallback(callback packagepkg.ResumeCallback) { s.resumeCallback = callback } // 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) { resolvedCard, resolvedDevice, resolveErr := s.resolveAssetByIdentifier(ctx, req.Identifier) if resolveErr != nil { return nil, resolveErr } var orderType string var iotCardID *uint var deviceID *uint var resourceShopID *uint if resolvedCard != nil { orderType = model.OrderTypeSingleCard iotCardID = &resolvedCard.ID resourceShopID = resolvedCard.ShopID } else { orderType = model.OrderTypeDevice deviceID = &resolvedDevice.ID resourceShopID = resolvedDevice.ShopID } var validationResult *purchase_validation.PurchaseValidationResult var err error operatorUserType := middleware.GetUserTypeFromContext(ctx) validationCtx := ctx if req.PaymentMethod == model.PaymentMethodOffline && (operatorUserType == constants.UserTypeSuperAdmin || operatorUserType == constants.UserTypePlatform) { validationCtx = purchase_validation.WithAllowGiftPackages(ctx) } if req.PaymentMethod == model.PaymentMethodOffline { if resolvedCard != nil { validationResult, err = s.purchaseValidationService.ValidateAdminOfflineCardPurchase(validationCtx, resolvedCard.ID, req.PackageIDs) } else { validationResult, err = s.purchaseValidationService.ValidateAdminOfflineDevicePurchase(validationCtx, resolvedDevice.ID, req.PackageIDs) } } else { if resolvedCard != nil { validationResult, err = s.purchaseValidationService.ValidateCardPurchase(validationCtx, resolvedCard.ID, req.PackageIDs) } else { validationResult, err = s.purchaseValidationService.ValidateDevicePurchase(validationCtx, resolvedDevice.ID, req.PackageIDs) } } if err != nil { return nil, rewriteAdminAgentPackageOffShelfError(err, resourceShopID, req.PaymentMethod != model.PaymentMethodOffline) } if err := validatePackageTypeMixFromPackages(validationResult.Packages); err != nil { return nil, err } containsGift := packageprice.ContainsGiftPackage(validationResult.Packages) if containsGift { if operatorUserType != constants.UserTypeSuperAdmin && operatorUserType != constants.UserTypePlatform { return nil, errors.New(errors.CodeForbidden, "仅平台侧可发放赠送套餐") } if req.PaymentMethod != model.PaymentMethodOffline { return nil, errors.New(errors.CodeInvalidParam, "赠送套餐仅支持后台线下发放") } if err := packageprice.ValidateGiftAdminOrder(validationResult.Packages); err != nil { return nil, err } } if req.PaymentMethod == model.PaymentMethodOffline && len(req.PaymentVoucherKey) == 0 { return nil, errors.New(errors.CodeInvalidParam, "线下支付必须上传支付凭证") } carrierType, carrierID := resolveAdminCarrierInfoFromVars(orderType, iotCardID, deviceID) existingOrderID, err := s.checkOrderIdempotency(ctx, buyerType, buyerID, 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) // offline 订单不检查强充:平台直接操作,不涉及消费者支付门槛,不产生一次性佣金触发条件 if req.PaymentMethod != model.PaymentMethodOffline { forceRechargeCheck := s.checkForceRechargeRequirement(ctx, validationResult) if forceRechargeCheck.NeedForceRecharge && validationResult.TotalPrice < forceRechargeCheck.ForceRechargeAmount { return nil, errors.New(errors.CodeForceRechargeRequired, "首次购买需满足最低充值要求") } } userID := middleware.GetUserIDFromContext(ctx) operatorAccountID, operatorAccountType, operatorAccountName := s.buildAccountOperatorSnapshot(ctx, userID, operatorUserType) // 提取资源所属店铺ID var seriesID *uint if validationResult.Card != nil { seriesID = validationResult.Card.SeriesID } else if validationResult.Device != nil { seriesID = validationResult.Device.SeriesID } // 先按零售价构建明细价格快照,后续在代购/成本价场景覆盖 // offline 场景先使用平台零售价,避免在平台代购场景下提前依赖代理分配记录 sellerShopIDValue := uint(0) if req.PaymentMethod != model.PaymentMethodOffline && resourceShopID != nil { sellerShopIDValue = *resourceShopID } itemUnitPriceMap, retailTotalAmount, err := s.buildRetailPriceMap(ctx, validationResult.Packages, sellerShopIDValue) if err != nil { return nil, rewriteAdminAgentPackageOffShelfError(err, resourceShopID, sellerShopIDValue > 0) } // 初始化订单字段 orderBuyerType := buyerType orderBuyerID := buyerID totalAmount := retailTotalAmount 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 var itemCostPriceMap map[uint]int64 // 各套餐成本单价映射,用于 OrderItem.CostPrice 字段 // 根据支付方式分别处理 if req.PaymentMethod == model.PaymentMethodOffline { // ==== 场景 1:offline(线下支付)==== if containsGift { orderBuyerType = model.BuyerTypePersonal orderBuyerID = 0 paymentMethod = model.PaymentMethodOffline paymentStatus = model.PaymentStatusPaid paidAt = &now isPurchaseOnBehalf = false sellerShopID = nil sellerCostPrice = 0 operatorID = nil operatorType = constants.OwnerTypePlatform actualPaidAmountVal := int64(0) actualPaidAmount = &actualPaidAmountVal purchaseRole = model.PurchaseRoleSelfPurchase } else { isPlatformOwned := resourceShopID == nil || *resourceShopID == 0 if isPlatformOwned { // ==== 子场景 1.1:平台自营(资源未分配给代理商)==== // 平台直接销售给终端用户,无代理商参与,使用零售价,无需查询分配配置 orderBuyerType = model.BuyerTypePersonal orderBuyerID = 0 paymentMethod = model.PaymentMethodOffline paymentStatus = model.PaymentStatusPaid paidAt = &now isPurchaseOnBehalf = false sellerCostPrice = 0 operatorID = nil operatorType = constants.OwnerTypePlatform purchaseRole = model.PurchaseRoleSelfPurchase actualPaidAmount = nil } else { // ==== 子场景 1.2:平台代购(资源属于代理商,平台代为购买)==== purchaseBuyerID, buyerCostPriceMap, buyerTotalCost, purchasePaidAt, err := s.resolvePurchaseOnBehalfInfo(ctx, validationResult) if err != nil { return nil, err } orderBuyerType = model.BuyerTypeAgent orderBuyerID = purchaseBuyerID itemCostPriceMap = buyerCostPriceMap paymentMethod = model.PaymentMethodOffline paymentStatus = model.PaymentStatusPaid paidAt = purchasePaidAt isPurchaseOnBehalf = true sellerCostPrice = buyerTotalCost // 设置操作者信息(平台代购) operatorID = nil operatorType = constants.OwnerTypePlatform purchaseRole = model.PurchaseRolePurchasedByPlatform actualPaidAmount = &buyerTotalCost } } } else if req.PaymentMethod == model.PaymentMethodWallet { // ==== 场景 2:代理钱包支付(wallet)==== if resourceShopID == nil || *resourceShopID == 0 { return nil, errors.New(errors.CodeInvalidParam, "资源未分配给代理商,无法使用代理钱包支付") } if buyerType == model.BuyerTypeAgent { operatorShopID := buyerID if *resourceShopID == operatorShopID { // ==== 子场景 2.1:代理自购 ==== operatorCostPriceMap, operatorTotalCost, err := s.buildCostPriceMap(ctx, validationResult.Packages, operatorShopID) if err != nil { return nil, err } orderBuyerType = model.BuyerTypeAgent orderBuyerID = operatorShopID itemCostPriceMap = operatorCostPriceMap paymentMethod = model.PaymentMethodWallet paymentStatus = model.PaymentStatusPaid paidAt = &now isPurchaseOnBehalf = false operatorID = &operatorShopID operatorType = "agent" actualPaidAmountVal := operatorTotalCost actualPaidAmount = &actualPaidAmountVal purchaseRole = model.PurchaseRoleSelfPurchase sellerCostPrice = operatorTotalCost } else { // ==== 子场景 2.2:代理代购(给下级购买)==== // 获取买家成本价(买家的价格映射用于 CostPrice 快照,总价不再使用) buyerCostPriceMap, _, err := s.buildCostPriceMap(ctx, validationResult.Packages, *resourceShopID) if err != nil { return nil, err } // 获取操作者成本价(操作方实际扣款金额和佣金基准) _, operatorTotalCost, err := s.buildCostPriceMap(ctx, validationResult.Packages, operatorShopID) if err != nil { return nil, err } orderBuyerType = model.BuyerTypeAgent orderBuyerID = *resourceShopID itemCostPriceMap = buyerCostPriceMap paymentMethod = model.PaymentMethodWallet paymentStatus = model.PaymentStatusPaid paidAt = &now isPurchaseOnBehalf = true operatorID = &operatorShopID operatorType = "agent" actualPaidAmountVal := operatorTotalCost actualPaidAmount = &actualPaidAmountVal purchaseRole = model.PurchaseRolePurchaseForSubordinate // 代理代购下级时,佣金差额基准应是操作方(代理自己)的成本价,而非下级的成本价 sellerCostPrice = operatorTotalCost } } else { // ==== 子场景 2.3:平台/超管代扣目标代理钱包 ==== targetShopID := *resourceShopID targetCostPriceMap, targetTotalCost, err := s.buildCostPriceMap(ctx, validationResult.Packages, targetShopID) if err != nil { return nil, err } orderBuyerType = model.BuyerTypeAgent orderBuyerID = targetShopID itemCostPriceMap = targetCostPriceMap paymentMethod = model.PaymentMethodWallet paymentStatus = model.PaymentStatusPaid paidAt = &now isPurchaseOnBehalf = false operatorID = nil operatorType = constants.OwnerTypePlatform actualPaidAmountVal := targetTotalCost actualPaidAmount = &actualPaidAmountVal purchaseRole = model.PurchaseRoleSelfPurchase sellerCostPrice = targetTotalCost } } else { // 兜底检查:后台不支持其他支付方式(DTO 验证已拒绝,此为防御性编程) return nil, errors.New(errors.CodeInvalidParam, "后台仅支持钱包支付或线下支付") } if paymentStatus == model.PaymentStatusPaid && actualPaidAmount == nil { actualPaidAmountSnapshot := totalAmount actualPaidAmount = &actualPaidAmountSnapshot } // 查询当前生效的支付配置 var paymentConfigID *uint if req.PaymentMethod == model.PaymentMethodWechat || req.PaymentMethod == model.PaymentMethodAlipay { activeConfig, err := s.wechatConfigService.GetActiveConfig(ctx) if err != nil { s.logger.Warn("查询生效支付配置失败", zap.Error(err)) } if activeConfig != nil { paymentConfigID = &activeConfig.ID } } // 从资产获取当前 generation(用于订单快照) var assetGeneration int if validationResult.Card != nil { assetGeneration = validationResult.Card.Generation } else if validationResult.Device != nil { assetGeneration = validationResult.Device.Generation } if assetGeneration == 0 { assetGeneration = 1 } order := &model.Order{ BaseModel: model.BaseModel{ Creator: userID, Updater: userID, }, OrderNo: s.orderStore.GenerateOrderNo(), OrderType: orderType, Source: constants.OrderSourceAdmin, Generation: assetGeneration, BuyerType: orderBuyerType, BuyerID: orderBuyerID, IotCardID: iotCardID, DeviceID: deviceID, AssetIdentifier: req.Identifier, TotalAmount: totalAmount, PaymentMethod: paymentMethod, PaymentStatus: paymentStatus, PaidAt: paidAt, CommissionStatus: model.CommissionStatusPending, CommissionConfigVersion: 0, SeriesID: seriesID, SellerShopID: sellerShopID, SellerCostPrice: sellerCostPrice, IsPurchaseOnBehalf: isPurchaseOnBehalf, OperatorAccountID: operatorAccountID, OperatorAccountType: operatorAccountType, OperatorAccountName: operatorAccountName, OperatorID: operatorID, OperatorType: operatorType, ActualPaidAmount: actualPaidAmount, PurchaseRole: purchaseRole, PaymentConfigID: paymentConfigID, } // 线下支付订单写入支付凭证 file_key 列表 if req.PaymentMethod == model.PaymentMethodOffline { order.PaymentVoucherKey = model.StringJSONBArray(req.PaymentVoucherKey) } if orderBuyerType == model.BuyerTypePersonal { order.BuyerPhone, order.BuyerNickname = s.fetchBuyerSnapshot(ctx, orderBuyerID) } items := s.buildOrderItems(userID, validationResult.Packages, itemUnitPriceMap, itemCostPriceMap) idempotencyKey := buildOrderIdempotencyKey(buyerType, buyerID, orderType, carrierType, carrierID, req.PackageIDs) // 根据支付方式选择创建订单的方式 if req.PaymentMethod == model.PaymentMethodOffline { // 平台代购:创建订单并立即激活套餐 if err := s.createOrderWithActivation(ctx, order, items); err != nil { return nil, err } if !containsGift { s.enqueueCommissionCalculation(ctx, order.ID) } s.markOrderCreated(ctx, idempotencyKey, order.ID) return s.buildOrderResponse(ctx, order, items), nil } else if req.PaymentMethod == model.PaymentMethodWallet { // 钱包支付:创建订单、扣款、激活套餐(在事务中完成) operatorShopID := orderBuyerID if operatorID != nil { 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(ctx, order, items), nil } else { // 不应该到这里(DTO 验证已拒绝其他支付方式) return nil, errors.New(errors.CodeInvalidParam, "后台仅支持钱包支付或线下支付") } } func rewriteAdminAgentPackageOffShelfError(err error, resourceShopID *uint, shouldRewrite bool) error { if err == nil || !shouldRewrite || resourceShopID == nil || *resourceShopID == 0 { return err } appErr, ok := err.(*errors.AppError) if !ok { return err } if appErr.Code == errors.CodeInvalidParam && appErr.Message == "套餐已下架" { return errors.New(errors.CodeInvalidParam, "该代理已经下架该套餐,让代理上架该套餐后购买") } return err } // 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) operatorUserType := middleware.GetUserTypeFromContext(ctx) operatorAccountID, operatorAccountType, operatorAccountName := s.buildAccountOperatorSnapshot(ctx, userID, operatorUserType) // 提取资源所属店铺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 } // 先按零售价构建明细价格快照,后续在代购/成本价场景覆盖 sellerShopIDValue := uint(0) if resourceShopID != nil { sellerShopIDValue = *resourceShopID } itemUnitPriceMap, retailTotalAmount, err := s.buildRetailPriceMap(ctx, validationResult.Packages, sellerShopIDValue) if err != nil { return nil, err } // 初始化订单字段 orderBuyerType := buyerType orderBuyerID := buyerID totalAmount := retailTotalAmount 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 var itemCostPriceMap map[uint]int64 // 各套餐成本单价映射,用于 OrderItem.CostPrice 字段 // 场景判断:offline(平台代购)、wallet(代理钱包支付)、其他(待支付) if req.PaymentMethod == model.PaymentMethodOffline { // ==== 场景 1:平台代购(offline)==== purchaseBuyerID, buyerCostPriceMap, buyerTotalCost, purchasePaidAt, err := s.resolvePurchaseOnBehalfInfo(ctx, validationResult) if err != nil { return nil, err } orderBuyerType = model.BuyerTypeAgent orderBuyerID = purchaseBuyerID itemCostPriceMap = buyerCostPriceMap paymentMethod = model.PaymentMethodOffline paymentStatus = model.PaymentStatusPaid paidAt = purchasePaidAt isPurchaseOnBehalf = true sellerCostPrice = buyerTotalCost // 设置操作者信息(平台代购) operatorID = nil operatorType = constants.OwnerTypePlatform purchaseRole = model.PurchaseRolePurchasedByPlatform actualPaidAmount = &buyerTotalCost } 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为空") } if *resourceShopID == operatorShopID { // ==== 子场景 2.1:代理自购 ==== operatorCostPriceMap, operatorTotalCost, err := s.buildCostPriceMap(ctx, validationResult.Packages, operatorShopID) if err != nil { return nil, err } orderBuyerType = model.BuyerTypeAgent orderBuyerID = operatorShopID itemCostPriceMap = operatorCostPriceMap paymentMethod = model.PaymentMethodWallet paymentStatus = model.PaymentStatusPaid paidAt = &now isPurchaseOnBehalf = false operatorID = &operatorShopID operatorType = "agent" actualPaidAmountVal := operatorTotalCost actualPaidAmount = &actualPaidAmountVal purchaseRole = model.PurchaseRoleSelfPurchase sellerCostPrice = operatorTotalCost } else { // ==== 子场景 2.2:代理代购(给下级购买)==== // 获取买家成本价(价格映射用于 CostPrice 快照,总价不再使用) buyerCostPriceMap, _, err := s.buildCostPriceMap(ctx, validationResult.Packages, *resourceShopID) if err != nil { return nil, err } // 获取操作者成本价(操作方实际扣款金额和佣金基准) _, operatorTotalCost, err := s.buildCostPriceMap(ctx, validationResult.Packages, operatorShopID) if err != nil { return nil, err } orderBuyerType = model.BuyerTypeAgent orderBuyerID = *resourceShopID itemCostPriceMap = buyerCostPriceMap paymentMethod = model.PaymentMethodWallet paymentStatus = model.PaymentStatusPaid paidAt = &now isPurchaseOnBehalf = true operatorID = &operatorShopID operatorType = "agent" actualPaidAmountVal := operatorTotalCost actualPaidAmount = &actualPaidAmountVal purchaseRole = model.PurchaseRolePurchaseForSubordinate // 代理代购下级时,佣金差额基准应是操作方(代理自己)的成本价,而非下级的成本价 sellerCostPrice = operatorTotalCost } } if paymentStatus == model.PaymentStatusPaid && actualPaidAmount == nil { actualPaidAmountSnapshot := totalAmount actualPaidAmount = &actualPaidAmountSnapshot } // 查询当前生效的支付配置 var h5PaymentConfigID *uint if req.PaymentMethod == model.PaymentMethodWechat || req.PaymentMethod == model.PaymentMethodAlipay { activeConfig, err := s.wechatConfigService.GetActiveConfig(ctx) if err != nil { s.logger.Warn("查询生效支付配置失败", zap.Error(err)) } if activeConfig != nil { h5PaymentConfigID = &activeConfig.ID } } 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, AssetIdentifier: buildOrderAssetIdentifier(validationResult), TotalAmount: totalAmount, PaymentMethod: paymentMethod, PaymentStatus: paymentStatus, PaidAt: paidAt, CommissionStatus: model.CommissionStatusPending, CommissionConfigVersion: 0, SeriesID: seriesID, SellerShopID: sellerShopID, SellerCostPrice: sellerCostPrice, IsPurchaseOnBehalf: isPurchaseOnBehalf, OperatorAccountID: operatorAccountID, OperatorAccountType: operatorAccountType, OperatorAccountName: operatorAccountName, OperatorID: operatorID, OperatorType: operatorType, ActualPaidAmount: actualPaidAmount, PurchaseRole: purchaseRole, ExpiresAt: expiresAt, PaymentConfigID: h5PaymentConfigID, } items := s.buildOrderItems(userID, validationResult.Packages, itemUnitPriceMap, itemCostPriceMap) 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(ctx, 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(ctx, 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(ctx, order, items), nil } } func (s *Service) resolvePurchaseOnBehalfInfo(ctx context.Context, result *purchase_validation.PurchaseValidationResult) (uint, map[uint]int64, 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, nil, 0, nil, errors.New(errors.CodeInvalidParam, "资源未分配给代理商,无法代购") } if seriesID == nil || *seriesID == 0 { return 0, nil, 0, nil, errors.New(errors.CodeInvalidParam, "资源未关联套餐系列") } if len(result.Packages) == 0 { return 0, nil, 0, nil, errors.New(errors.CodeInvalidParam, "订单中没有套餐") } buyerCostPriceMap, buyerTotalCost, err := s.buildCostPriceMap(ctx, result.Packages, *resourceShopID) if err != nil { return 0, nil, 0, nil, err } now := time.Now() return *resourceShopID, buyerCostPriceMap, buyerTotalCost, &now, nil } // buildOrderItems 构建订单明细列表 // retailPriceMap: 零售单价映射(UnitPrice),costPriceMap: 成本单价映射(CostPrice,无成本价时传 nil) func (s *Service) buildOrderItems(operatorID uint, packages []*model.Package, retailPriceMap map[uint]int64, costPriceMap map[uint]int64) []*model.OrderItem { items := make([]*model.OrderItem, 0, len(packages)) for _, pkg := range packages { if pkg == nil { continue } unitPrice := packageprice.PackageEffectiveRetailPrice(pkg) if price, ok := retailPriceMap[pkg.ID]; ok { unitPrice = price } var costPrice int64 if costPriceMap != nil { costPrice = costPriceMap[pkg.ID] } item := &model.OrderItem{ BaseModel: model.BaseModel{ Creator: operatorID, Updater: operatorID, }, PackageID: pkg.ID, PackageName: pkg.PackageName, PackageType: &pkg.PackageType, Quantity: 1, UnitPrice: unitPrice, CostPrice: costPrice, Amount: unitPrice, PackagePriceConfigStatus: pkg.PriceConfigStatus, PackageIsGift: pkg.IsGift, } items = append(items, item) } return items } // buildRetailPriceMap 构建订单明细零售价映射和总价(分) // sellerShopID > 0 时取代理分配零售价,=0 时取套餐建议零售价 func (s *Service) buildRetailPriceMap(ctx context.Context, packages []*model.Package, sellerShopID uint) (map[uint]int64, int64, error) { priceMap := make(map[uint]int64, len(packages)) var total int64 for _, pkg := range packages { if pkg == nil { continue } price, err := s.purchaseValidationService.GetPurchasePrice(ctx, pkg, sellerShopID) if err != nil { return nil, 0, err } priceMap[pkg.ID] = price total += price } return priceMap, total, nil } // buildCostPriceMap 构建订单明细成本价映射和总价(分) func (s *Service) buildCostPriceMap(ctx context.Context, packages []*model.Package, shopID uint) (map[uint]int64, int64, error) { priceMap := make(map[uint]int64, len(packages)) var total int64 for _, pkg := range packages { if pkg == nil { continue } costPrice, err := s.getCostPrice(ctx, shopID, pkg.ID) if err != nil { return nil, 0, err } priceMap[pkg.ID] = costPrice total += costPrice } return priceMap, total, nil } func (s *Service) fetchBuyerSnapshot(ctx context.Context, customerID uint) (phone, nickname *string) { if customerID == 0 { return nil, nil } customer, err := s.personalCustomerStore.GetByID(ctx, customerID) if err != nil { s.logger.Warn("fetchBuyerSnapshot: 查询个人客户失败", zap.Uint("customer_id", customerID), zap.Error(err)) } else if customer != nil && customer.Nickname != "" { nickname = &customer.Nickname } phoneRecord, err := s.personalCustomerPhoneStore.GetPrimaryPhone(ctx, customerID) if err != nil { s.logger.Warn("fetchBuyerSnapshot: 查询买家手机号失败", zap.Uint("customer_id", customerID), zap.Error(err)) } else if phoneRecord != nil && phoneRecord.Phone != "" { phone = &phoneRecord.Phone } return phone, nickname } func (s *Service) buildAccountOperatorSnapshot(ctx context.Context, userID uint, userType int) (*uint, string, string) { if userID == 0 { return nil, "", "" } var account model.Account if err := s.db.WithContext(ctx).Where("id = ?", userID).First(&account).Error; err != nil { s.logger.Warn("buildAccountOperatorSnapshot: 查询账号失败", zap.Uint("user_id", userID), zap.Error(err)) id := userID return &id, orderOperatorAccountTypeFromUserType(userType), "" } id := account.ID return &id, orderOperatorAccountTypeFromUserType(account.UserType), account.Username } func (s *Service) buildPersonalCustomerOperatorSnapshot(ctx context.Context, customerID uint, nickname *string) (*uint, string, string) { if customerID == 0 { return nil, "", "" } name := "" if nickname != nil { name = strings.TrimSpace(*nickname) } if name == "" && s.personalCustomerStore != nil { customer, err := s.personalCustomerStore.GetByID(ctx, customerID) if err != nil { s.logger.Warn("buildPersonalCustomerOperatorSnapshot: 查询个人客户失败", zap.Uint("customer_id", customerID), zap.Error(err)) } else if customer != nil { name = strings.TrimSpace(customer.Nickname) } } id := customerID return &id, model.OperatorAccountTypePersonalCustomer, name } func orderOperatorAccountTypeFromUserType(userType int) string { switch userType { case constants.UserTypeSuperAdmin, constants.UserTypePlatform: return model.OperatorAccountTypePlatform case constants.UserTypeAgent: return model.OperatorAccountTypeAgent case constants.UserTypeEnterprise: return model.OperatorAccountTypeEnterprise default: return "" } } // 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: 事务对象 // wallet: 扣款前的钱包快照 // order: 订单快照 // amount: 扣款金额(正数) // purchaseRole: 订单角色 // relatedShopID: 关联店铺ID(代购场景填充下级店铺ID) func (s *Service) createWalletTransaction(ctx context.Context, tx *gorm.DB, wallet *model.AgentWallet, order *model.Order, amount int64, purchaseRole string, relatedShopID *uint) error { var subtype *string remark := "购买套餐" if purchaseRole == "" { purchaseRole = model.PurchaseRoleSelfPurchase } // 根据订单角色确定交易子类型和备注 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) assetType, assetID, assetIdentifier := buildAgentWalletTransactionAssetSnapshot(order) // 创建钱包流水记录 transaction := &model.AgentWalletTransaction{ AgentWalletID: wallet.ID, ShopID: wallet.ShopID, UserID: userID, TransactionType: constants.AgentTransactionTypeDeduct, TransactionSubtype: subtype, Amount: -amount, // 扣款为负数 BalanceBefore: wallet.Balance, BalanceAfter: wallet.Balance - amount, Status: constants.TransactionStatusSuccess, ReferenceType: strPtr(constants.ReferenceTypeOrder), ReferenceID: &order.ID, RelatedShopID: relatedShopID, AssetType: assetType, AssetID: assetID, AssetIdentifier: assetIdentifier, Remark: &remark, Creator: userID, ShopIDTag: wallet.ShopIDTag, EnterpriseIDTag: wallet.EnterpriseIDTag, } if err := tx.Create(transaction).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "创建钱包流水失败") } return nil } // buildAgentWalletTransactionAssetSnapshot 从订单中生成代理钱包扣款流水的资产快照。 func buildAgentWalletTransactionAssetSnapshot(order *model.Order) (string, uint, string) { if order == nil { return "", 0, "" } switch order.OrderType { case model.OrderTypeSingleCard: if order.IotCardID == nil { return constants.AssetTypeIotCard, 0, order.AssetIdentifier } return constants.AssetTypeIotCard, *order.IotCardID, order.AssetIdentifier case model.OrderTypeDevice: if order.DeviceID == nil { return constants.AssetTypeDevice, 0, order.AssetIdentifier } return constants.AssetTypeDevice, *order.DeviceID, order.AssetIdentifier default: return "", 0, order.AssetIdentifier } } // buildOrderAssetIdentifier 生成订单资产标识快照。 // 后台按标识下单时保留请求标识;按ID下单时用卡ICCID或设备IMEI/虚拟号兜底。 func buildOrderAssetIdentifier(result *purchase_validation.PurchaseValidationResult) string { if result == nil { return "" } if result.Card != nil { return result.Card.ICCID } if result.Device != nil { return firstNonEmpty(result.Device.IMEI, result.Device.VirtualNo) } return "" } func firstNonEmpty(values ...string) string { for _, value := range values { if value != "" { return value } } return "" } // 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 // 事务内:加锁读取钱包 + 余额检查 + 创建订单 + 扣款 + 创建流水 + 激活套餐 // 使用 FOR UPDATE 悲观锁替代乐观锁,避免并发请求因 version 过期导致误报余额不足 err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { // 1. 加锁读取钱包,并发请求排队等待,不会冲突 var wallet model.AgentWallet if err := tx.Set("gorm:query_option", "FOR UPDATE"). Where("shop_id = ? AND wallet_type = ?", operatorShopID, constants.AgentWalletTypeMain). First(&wallet).Error; err != nil { if err == gorm.ErrRecordNotFound { return errors.New(errors.CodeWalletNotFound, "钱包不存在") } return errors.Wrap(errors.CodeDatabaseError, err, "查询钱包失败") } // 2. 余额检查(加锁后读到的是最新余额,结果准确) if wallet.Balance < actualAmount { return errors.New(errors.CodeInsufficientBalance, "余额不足") } // 3. 创建订单 if err := tx.Create(order).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "创建订单失败") } // 4. 创建订单明细 for _, item := range items { item.OrderID = order.ID } if err := tx.CreateInBatches(items, 100).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "创建订单明细失败") } // 5. 扣减钱包余额(FOR UPDATE 已保证串行,无需 version 条件) if err := tx.Model(&wallet).Updates(map[string]any{ "balance": gorm.Expr("balance - ?", actualAmount), "version": gorm.Expr("version + 1"), }).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "扣减钱包余额失败") } // 6. 创建钱包流水 var relatedShopID *uint if order.PurchaseRole == model.PurchaseRolePurchaseForSubordinate { relatedShopID = &buyerShopID } if err := s.createWalletTransaction(ctx, tx, &wallet, order, actualAmount, order.PurchaseRole, relatedShopID); err != nil { return err } // 7. 激活套餐 if err := s.activatePackage(ctx, tx, order); err != nil { return err } return nil }) if err != nil { return err } // 3. 事务外:所有已支付且适用差价佣金的订单都进入佣金计算 if order.CommissionStatus == model.CommissionStatusPending { 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(ctx, 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{} // 仅代理用户需要限定买家范围;平台/超管不加限制,可查看所有订单 if buyerType != "" { filters["buyer_type"] = buyerType } if buyerID != 0 { filters["buyer_id"] = buyerID } if req.PaymentStatus != nil { filters["payment_status"] = *req.PaymentStatus } if req.PaymentMethod != "" { filters["payment_method"] = req.PaymentMethod } 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.SellerShopID != nil { filters["seller_shop_id"] = *req.SellerShopID } if req.StartTime != nil { filters["start_time"] = req.StartTime } if req.EndTime != nil { filters["end_time"] = req.EndTime } if req.Identifier != "" { resolvedIotCardID, resolvedDeviceID, err := s.resolveOrderListAssetIdentifier(ctx, req.Identifier) if err != nil { return nil, err } // 保留快照标识匹配,并叠加资产 ID 匹配(由 Store 层统一按 OR 组合) filters["identifier"] = req.Identifier if resolvedIotCardID != nil { filters["iot_card_id"] = *resolvedIotCardID } if resolvedDeviceID != nil { filters["device_id"] = *resolvedDeviceID } } if req.BuyerPhone != "" { filters["buyer_phone"] = req.BuyerPhone } 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(ctx, o, itemsMap[o.ID])) } return &dto.OrderListResponse{ List: list, Total: total, Page: page, PageSize: pageSize, }, nil } // resolveOrderListAssetIdentifier 将订单列表的统一资产标识解析为卡或设备 ID // 支持 ICCID、VirtualNo、IMEI、SN、MSISDN,与资产解析接口的识别口径保持一致。 func (s *Service) resolveOrderListAssetIdentifier(ctx context.Context, identifier string) (*uint, *uint, error) { if identifier == "" { return nil, nil, nil } if s.assetIdentifierStore != nil { regRecord, regErr := s.assetIdentifierStore.FindByIdentifier(ctx, identifier) if regErr != nil { return nil, nil, errors.Wrap(errors.CodeDatabaseError, regErr, "查询资产标识符失败") } if regRecord != nil { switch regRecord.AssetType { case model.AssetTypeIotCard: card, cardErr := s.iotCardStore.GetByID(ctx, regRecord.AssetID) if cardErr == nil && card != nil { cardID := card.ID return &cardID, nil, nil } if cardErr != nil && cardErr != gorm.ErrRecordNotFound { return nil, nil, errors.Wrap(errors.CodeDatabaseError, cardErr, "查询物联网卡失败") } case model.AssetTypeDevice: device, deviceErr := s.deviceStore.GetByID(ctx, regRecord.AssetID) if deviceErr == nil && device != nil { deviceID := device.ID return nil, &deviceID, nil } if deviceErr != nil && deviceErr != gorm.ErrRecordNotFound { return nil, nil, errors.Wrap(errors.CodeDatabaseError, deviceErr, "查询设备失败") } } } } device, deviceErr := s.deviceStore.GetByIdentifier(ctx, identifier) if deviceErr == nil && device != nil { deviceID := device.ID return nil, &deviceID, nil } if deviceErr != nil && deviceErr != gorm.ErrRecordNotFound { return nil, nil, errors.Wrap(errors.CodeDatabaseError, deviceErr, "查询设备失败") } var card model.IotCard query := s.db.WithContext(ctx).Model(&model.IotCard{}) query = middleware.ApplyShopFilter(ctx, query) conditions := []string{"virtual_no = ?", "msisdn = ?", "iccid = ?"} args := []any{identifier, identifier, identifier} if len(identifier) == 19 { conditions = append(conditions, "iccid_19 = ?") args = append(args, identifier) } else if len(identifier) == 20 { conditions = append(conditions, "iccid_20 = ?") args = append(args, identifier) } if err := query.Where(strings.Join(conditions, " OR "), args...).First(&card).Error; err == nil { cardID := card.ID return &cardID, nil, nil } else if err != gorm.ErrRecordNotFound { return nil, nil, errors.Wrap(errors.CodeDatabaseError, err, "查询物联网卡失败") } return nil, nil, 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.assetWalletStore.GetByResourceTypeAndID(ctx, resourceType, resourceID) if err != nil { return errors.Wrap(errors.CodeWalletNotFound, err, "查询资产钱包失败") } // 资产钱包解冻:直接减少冻结余额 result := tx.Model(&model.AssetWallet{}). 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) createWalletPaymentRecord(tx *gorm.DB, order *model.Order, paymentMethod string) error { payment := &model.Payment{ PaymentNo: order.OrderNo, OrderID: order.ID, OrderType: model.PaymentOrderTypePackage, PaymentMethod: paymentMethod, Amount: order.TotalAmount, Status: model.PaymentRecordStatusPaid, } return tx.Create(payment).Error } 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, "代购订单无需支付") } existingPayment, _ := s.paymentStore.GetByOrderIDAndStatus(ctx, orderID, model.PaymentRecordStatusPaid) if existingPayment != nil { return nil } 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() actualPaidAmount := order.TotalAmount shouldEnqueueCommission := false 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, // 支付成功,清除过期时间 "actual_paid_amount": actualPaidAmount, }) 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, "订单状态异常") } } actualPaidAmountSnapshot := actualPaidAmount order.ActualPaidAmount = &actualPaidAmountSnapshot shouldEnqueueCommission = true 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, "余额不足或并发冲突") } if err := s.createWalletTransaction(ctx, tx, wallet, order, order.TotalAmount, order.PurchaseRole, nil); err != nil { return err } if err := s.createWalletPaymentRecord(tx, order, model.PaymentByWallet); err != nil { return err } return s.activatePackage(ctx, tx, order) }) } else { // 资产钱包系统(iot_card 或 device) wallet, err := s.assetWalletStore.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, // 支付成功,清除过期时间 "actual_paid_amount": actualPaidAmount, }) 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, "订单状态异常") } } actualPaidAmountSnapshot := actualPaidAmount order.ActualPaidAmount = &actualPaidAmountSnapshot // 扣款前记录余额快照,用于写入流水 balanceBefore := wallet.Balance walletResult := tx.Model(&model.AssetWallet{}). 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, "余额不足或并发冲突") } // 扣款成功后补写扣款流水,填补流水表中扣款记录缺失的问题 deductTx := &model.AssetWalletTransaction{ AssetWalletID: wallet.ID, ResourceType: resourceType, ResourceID: resourceID, UserID: buyerID, TransactionType: "deduct", Amount: -order.TotalAmount, BalanceBefore: balanceBefore, BalanceAfter: balanceBefore - order.TotalAmount, Status: 1, ReferenceType: strPtr("order"), ReferenceNo: &order.OrderNo, Remark: strPtr("钱包支付套餐"), ShopIDTag: wallet.ShopIDTag, EnterpriseIDTag: wallet.EnterpriseIDTag, } if err := tx.Create(deductTx).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "创建扣款流水失败") } if err := s.createWalletPaymentRecord(tx, order, model.PaymentByWallet); err != nil { return err } return s.activatePackage(ctx, tx, order) }) } if err != nil { return err } if shouldEnqueueCommission && order.CommissionStatus == model.CommissionStatusPending { s.enqueueCommissionCalculation(ctx, orderID) } return nil } func (s *Service) HandlePaymentCallback(ctx context.Context, orderNo string, paymentMethod string, actualPaidAmount int64) 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() shouldResumeAfterPayment := false shouldEnqueueCommission := false 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, // 支付成功,清除过期时间 "actual_paid_amount": actualPaidAmount, }) 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, "订单状态异常") } } // 回调更新成功后,同步内存对象,确保激活套餐时写入 paid_amount 快照 actualPaidAmountSnapshot := actualPaidAmount order.ActualPaidAmount = &actualPaidAmountSnapshot shouldResumeAfterPayment = true shouldEnqueueCommission = true return s.activatePackage(ctx, tx, order) }) if err != nil { return err } if shouldResumeAfterPayment { s.tryResumeAfterPayment(ctx, order) } if shouldEnqueueCommission && order.CommissionStatus == model.CommissionStatusPending { s.enqueueCommissionCalculation(ctx, order.ID) } return nil } // HandlePaymentRecordCallback 通过支付单处理套餐订单第三方支付回调。 func (s *Service) HandlePaymentRecordCallback(ctx context.Context, paymentNo string, paymentMethod string, thirdPartyTradeNo string, actualPaidAmount int64) error { payment, err := s.paymentStore.GetByPaymentNo(ctx, paymentNo) if err != nil { if err == gorm.ErrRecordNotFound { return errors.New(errors.CodeNotFound, "支付记录不存在") } return errors.Wrap(errors.CodeDatabaseError, err, "查询支付记录失败") } if payment.OrderType != model.PaymentOrderTypePackage { return errors.New(errors.CodeInvalidParam, "支付记录类型不匹配") } order, err := s.orderStore.GetByID(ctx, payment.OrderID) if err != nil { if err == gorm.ErrRecordNotFound { return errors.New(errors.CodeNotFound, "订单不存在") } return errors.Wrap(errors.CodeDatabaseError, err, "查询订单失败") } now := time.Now() shouldResumeAfterPayment := false shouldEnqueueCommission := false err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { paymentUpdates := map[string]any{ "status": model.PaymentRecordStatusPaid, "paid_at": now, } if thirdPartyTradeNo != "" { paymentUpdates["third_party_trade_no"] = thirdPartyTradeNo } paymentResult := tx.Model(&model.Payment{}). Where("id = ? AND status = ?", payment.ID, model.PaymentRecordStatusPending). Updates(paymentUpdates) if paymentResult.Error != nil { return errors.Wrap(errors.CodeDatabaseError, paymentResult.Error, "更新支付记录失败") } if paymentResult.RowsAffected == 0 { var currentPayment model.Payment if err := tx.First(¤tPayment, payment.ID).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "查询支付记录失败") } if currentPayment.Status == model.PaymentRecordStatusPaid { return nil } return errors.New(errors.CodeInvalidStatus, "支付记录状态不允许处理") } 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, "actual_paid_amount": actualPaidAmount, }) 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, "订单状态异常") } } actualPaidAmountSnapshot := actualPaidAmount order.ActualPaidAmount = &actualPaidAmountSnapshot shouldResumeAfterPayment = true shouldEnqueueCommission = true return s.activatePackage(ctx, tx, order) }) if err != nil { return err } if shouldResumeAfterPayment { s.tryResumeAfterPayment(ctx, order) } if shouldEnqueueCommission && order.CommissionStatus == model.CommissionStatusPending { s.enqueueCommissionCalculation(ctx, order.ID) } return nil } // tryResumeAfterPayment 支付成功后检查是否应触发自动复机 // 仅当本次支付直接激活了主套餐(status=1,非排队)时才调用复机,避免虚流量已超但套餐排队时错误复机 func (s *Service) tryResumeAfterPayment(ctx context.Context, order *model.Order) { if s.resumeCallback == nil { return } var resumeCarrierType string var resumeCarrierID uint if order.OrderType == model.OrderTypeSingleCard && order.IotCardID != nil { resumeCarrierType = "iot_card" resumeCarrierID = *order.IotCardID } else if order.OrderType == model.OrderTypeDevice && order.DeviceID != nil { resumeCarrierType = "device" resumeCarrierID = *order.DeviceID } if resumeCarrierID == 0 { return } var activatedCount int64 s.db.WithContext(ctx).Model(&model.PackageUsage{}). Where("order_id = ? AND status = ? AND master_usage_id IS NULL", order.ID, constants.PackageUsageStatusActive). Count(&activatedCount) if activatedCount == 0 { return } go func(ct string, cid uint) { if err := s.resumeCallback.ResumeCardIfStopped(context.Background(), ct, cid); err != nil { s.logger.Error("支付后自动复机失败", zap.String("carrier_type", ct), zap.Uint("carrier_id", cid), zap.Uint("order_id", order.ID), zap.Error(err)) } }(resumeCarrierType, resumeCarrierID) } 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 } func (s *Service) lockPackageCarrier(ctx context.Context, tx *gorm.DB, carrierType string, carrierID uint) error { switch carrierType { case constants.AssetWalletResourceTypeIotCard, "card": var card model.IotCard if err := tx.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).First(&card, carrierID).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "锁定卡失败") } case constants.AssetWalletResourceTypeDevice: var device model.Device if err := tx.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).First(&device, carrierID).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "锁定设备失败") } default: return errors.New(errors.CodeInvalidParam, "无效的套餐载体类型") } 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 } // resolveAdminCarrierInfoFromVars 从已解析的订单类型和资产ID中提取载体类型和ID func resolveAdminCarrierInfoFromVars(orderType string, iotCardID *uint, deviceID *uint) (carrierType string, carrierID uint) { if orderType == model.OrderTypeSingleCard && iotCardID != nil { return "iot_card", *iotCardID } if orderType == model.OrderTypeDevice && deviceID != nil { return "device", *deviceID } return "", 0 } // resolveAssetByIdentifier 通过标识符(ICCID 或 VirtualNo)解析资产 // 优先查注册表,注册表命中则直接返回对应卡或设备 func (s *Service) resolveAssetByIdentifier(ctx context.Context, identifier string) (*model.IotCard, *model.Device, error) { if s.assetIdentifierStore != nil { regRecord, regErr := s.assetIdentifierStore.FindByIdentifier(ctx, identifier) if regErr == nil && regRecord != nil { switch regRecord.AssetType { case model.AssetTypeIotCard: card, cardErr := s.iotCardStore.GetByID(ctx, regRecord.AssetID) if cardErr == nil && card != nil { return card, nil, nil } case model.AssetTypeDevice: device, devErr := s.deviceStore.GetByID(ctx, regRecord.AssetID) if devErr == nil && device != nil { return nil, device, nil } } } } if card, cardErr := s.iotCardStore.GetByIdentifier(ctx, identifier); cardErr == nil && card != nil { return card, nil, nil } if device, deviceErr := s.deviceStore.GetByIdentifier(ctx, identifier); deviceErr == nil && device != nil { return nil, device, nil } return nil, nil, errors.New(errors.CodeNotFound, "未找到对应资产,请使用 ICCID 或虚拟号") } // 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 { if err := s.lockPackageCarrier(ctx, tx, carrierType, carrierID); err != nil { return err } hasCurrentMain, err := packagepkg.HasCurrentMainPackageForQueue(tx.WithContext(ctx), carrierType, carrierID, now) if err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "查询主套餐失败") } var status int var priority int var activatedAt time.Time var expiresAt time.Time var nextResetAt *time.Time var pendingRealnameActivation bool if hasCurrentMain { // 当前仍有在用主套餐时,新主套餐进入待生效队列,后续由过期/退款等流程接续激活。 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 { 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) // REALNAME-02: 后台囤货场景三维决策(卡类型 + 购买路径 + expiry_base + 实名状态) // 查询卡类型(行业卡永远直接激活,不等实名) // 同时查询载体当前实名状态:已实名则跳过 pending,直接激活 var cardCategory string var currentlyRealnamed bool if carrierType == "iot_card" { var card model.IotCard if err := tx.Select("card_category", "real_name_status").First(&card, carrierID).Error; err == nil { cardCategory = card.CardCategory currentlyRealnamed = card.RealNameStatus == constants.RealNameStatusVerified } // err != nil 时 currentlyRealnamed 保守默认 false,不阻断购买流程 } else if carrierType == "device" { // 子查询:统计该设备绑定的已实名卡数量(GORM 自动注入 deleted_at IS NULL) var count int64 subQuery := tx.Model(&model.DeviceSimBinding{}). Select("iot_card_id"). Where("device_id = ? AND bind_status = 1", carrierID) if err := tx.Model(&model.IotCard{}). Where("id IN (?) AND real_name_status = 1", subQuery). Count(&count).Error; err == nil { currentlyRealnamed = count > 0 } // err != nil 时保守默认 false,不阻断购买流程 } // 判断是否 C 端购买(C 端购买前已做实名前置检查,直接激活) isCEndPurchase := order.BuyerType == model.BuyerTypePersonal if cardCategory != "industry" && !isCEndPurchase { // 后台囤货路径:按 expiry_base 决定是否等实名 if pkg.ExpiryBase != "from_purchase" { if currentlyRealnamed { s.logger.Info("购买时载体已实名,直接激活套餐", zap.String("carrier_type", carrierType), zap.Uint("carrier_id", carrierID)) } else { // from_activation(默认):未实名,等实名后激活 status = constants.PackageUsageStatusPending pendingRealnameActivation = true activatedAt = time.Time{} expiresAt = time.Time{} nextResetAt = nil } } // from_purchase:立即激活,status 已为 Active,保持不变 } // 行业卡或 C 端购买:直接激活,status 已为 Active,保持不变 } // 创建套餐使用记录 virtualTotalMBSnapshot, displayGainRatioSnapshot, enableVirtualDataSnapshot := model.BuildPackageUsageSnapshotValues(pkg) retailAmount := order.TotalAmount usage := &model.PackageUsage{ BaseModel: model.BaseModel{ Creator: order.Creator, Updater: order.Creator, }, OrderID: order.ID, OrderNo: order.OrderNo, PackageID: pkg.ID, PackageName: pkg.PackageName, UsageType: order.OrderType, DataLimitMB: pkg.RealDataMB, VirtualTotalMBSnapshot: virtualTotalMBSnapshot, DisplayGainRatioSnapshot: displayGainRatioSnapshot, EnableVirtualDataSnapshot: enableVirtualDataSnapshot, Status: status, Priority: priority, DataResetCycle: pkg.DataResetCycle, PendingRealnameActivation: pendingRealnameActivation, PaidAmount: order.ActualPaidAmount, RetailAmount: &retailAmount, PackagePriceConfigStatus: pkg.PriceConfigStatus, PackageIsGift: pkg.IsGift, } 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: 检查是否有可挂靠主套餐(待生效、未过期的生效中或已用完) mainPackage, err := packagepkg.FindAttachableMainPackageForAddon(tx, carrierType, carrierID, now) 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 模型中,暂时使用默认行为 // 默认加油包跟随主套餐过期 if mainPackage.ExpiresAt != nil { expiresAt = *mainPackage.ExpiresAt } else { expiresAt = now } virtualTotalMBSnapshot, displayGainRatioSnapshot, enableVirtualDataSnapshot := model.BuildPackageUsageSnapshotValues(pkg) addonRetailAmount := order.TotalAmount usage := &model.PackageUsage{ BaseModel: model.BaseModel{ Creator: order.Creator, Updater: order.Creator, }, OrderID: order.ID, OrderNo: order.OrderNo, PackageID: pkg.ID, PackageName: pkg.PackageName, UsageType: order.OrderType, DataLimitMB: pkg.RealDataMB, VirtualTotalMBSnapshot: virtualTotalMBSnapshot, DisplayGainRatioSnapshot: displayGainRatioSnapshot, EnableVirtualDataSnapshot: enableVirtualDataSnapshot, Status: status, Priority: priority, MasterUsageID: &mainPackage.ID, ActivatedAt: &activatedAt, ExpiresAt: &expiresAt, DataResetCycle: pkg.DataResetCycle, PaidAmount: order.ActualPaidAmount, RetailAmount: &addonRetailAmount, PackagePriceConfigStatus: pkg.PriceConfigStatus, PackageIsGift: pkg.IsGift, } 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 } // 直接传 map,由 EnqueueTask 内部统一序列化一次(传 []byte 会导致 sonic.Marshal 二次 base64 编码) if err := s.queueClient.EnqueueTask(ctx, constants.TaskTypeCommission, map[string]any{"order_id": orderID}); 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(ctx context.Context, 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, PackageType: item.PackageType, 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 = "已退款" } operatorID, operatorType, operatorName := s.resolveOrderOperatorView(ctx, order) sellerShopName := s.resolveSellerShopName(ctx, order.SellerShopID) // 生成派生字段 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) } } assetType := "" if order.OrderType == model.OrderTypeSingleCard { assetType = "card" } else if order.OrderType == model.OrderTypeDevice { assetType = "device" } actualPaidAmount := order.ActualPaidAmount if actualPaidAmount == nil && order.PaymentStatus == model.PaymentStatusPaid { actualPaidAmountSnapshot := order.TotalAmount actualPaidAmount = &actualPaidAmountSnapshot } commissionResult := s.resolveOrderCommissionResult(ctx, order) return &dto.OrderResponse{ ID: order.ID, OrderNo: order.OrderNo, OrderType: order.OrderType, BuyerType: order.BuyerType, BuyerID: order.BuyerID, BuyerPhone: order.BuyerPhone, BuyerNickname: order.BuyerNickname, IotCardID: order.IotCardID, DeviceID: order.DeviceID, TotalAmount: order.TotalAmount, PaymentMethod: order.PaymentMethod, PaymentStatus: order.PaymentStatus, PaymentStatusText: statusText, PaidAt: order.PaidAt, PaymentVoucherKey: []string(order.PaymentVoucherKey), IsPurchaseOnBehalf: order.IsPurchaseOnBehalf, CommissionStatus: order.CommissionStatus, CommissionStatusName: constants.GetOrderCommissionStatusName(order.CommissionStatus), CommissionResult: commissionResult, CommissionResultName: constants.GetOrderCommissionResultName(commissionResult), CommissionConfigVersion: order.CommissionConfigVersion, SellerShopID: order.SellerShopID, SellerShopName: sellerShopName, OperatorID: operatorID, OperatorType: operatorType, OperatorName: operatorName, ActualPaidAmount: 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), AssetIdentifier: order.AssetIdentifier, AssetType: assetType, } } func (s *Service) resolveOrderOperatorView(ctx context.Context, order *model.Order) (*uint, string, string) { if order.OperatorAccountID != nil || order.OperatorAccountType != "" || order.OperatorAccountName != "" { return order.OperatorAccountID, order.OperatorAccountType, order.OperatorAccountName } if order.Source == constants.OrderSourceClient && order.BuyerType == model.BuyerTypePersonal { operatorID, operatorType, operatorName := s.buildPersonalCustomerOperatorSnapshot(ctx, order.BuyerID, order.BuyerNickname) if operatorID != nil || operatorType != "" || operatorName != "" { return operatorID, operatorType, operatorName } } if order.Creator > 0 { operatorID, operatorType, operatorName := s.buildAccountOperatorSnapshot(ctx, order.Creator, 0) if operatorID != nil || operatorType != "" || operatorName != "" { return operatorID, operatorType, operatorName } } if order.OperatorType == model.OperatorAccountTypeAgent && order.OperatorID != nil { return nil, model.OperatorAccountTypeAgent, s.resolveLegacyOperatorShopName(ctx, *order.OperatorID) } if order.OperatorType == model.OperatorAccountTypePlatform { return nil, model.OperatorAccountTypePlatform, "平台" } return nil, "", "" } func (s *Service) resolveLegacyOperatorShopName(ctx context.Context, shopID uint) string { if shopID == 0 { return "" } var shop model.Shop if err := s.db.WithContext(ctx).Where("id = ?", shopID).First(&shop).Error; err != nil { return "" } return shop.ShopName } func (s *Service) resolveSellerShopName(ctx context.Context, sellerShopID *uint) string { if sellerShopID == nil || *sellerShopID == 0 { return "" } var shop model.Shop if err := s.db.WithContext(ctx).Where("id = ?", *sellerShopID).First(&shop).Error; err != nil { return "" } return shop.ShopName } func (s *Service) resolveOrderCommissionResult(ctx context.Context, order *model.Order) int { if order.CommissionResult != model.CommissionResultUnknown { return order.CommissionResult } if order.CommissionStatus == model.CommissionStatusPending { return model.CommissionResultUnknown } var pendingReviewCount int64 if err := s.db.WithContext(ctx).Model(&model.CommissionRecord{}). Where("order_id = ? AND status = ?", order.ID, constants.CommissionStatusPendingReview). Count(&pendingReviewCount).Error; err == nil && pendingReviewCount > 0 { return model.CommissionResultChainBroken } var releasedCount int64 if err := s.db.WithContext(ctx).Model(&model.CommissionRecord{}). Where("order_id = ? AND status = ? AND amount > 0", order.ID, constants.CommissionStatusReleased). Count(&releasedCount).Error; err == nil && releasedCount > 0 { return model.CommissionResultHasAmount } if order.CommissionStatus == model.CommissionStatusCompleted { return model.CommissionResultNoAmount } return model.CommissionResultUnknown } // WechatPayJSAPI 发起微信 JSAPI 支付 // 通过 order.PaymentConfigID 动态加载支付配置,支持多商户场景 func (s *Service) WechatPayJSAPI(ctx context.Context, orderID uint, openID string, buyerType string, buyerID uint) (*dto.WechatPayJSAPIResponse, error) { 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, "订单状态不允许支付") } // 按 payment_config_id 动态加载支付配置 if order.PaymentConfigID == nil { s.logger.Error("订单缺少支付配置", zap.Uint("order_id", orderID)) return nil, errors.New(errors.CodeWechatConfigNotFound, "订单缺少支付配置") } paymentSvc, err := s.paymentLoader.LoadConfig(ctx, *order.PaymentConfigID) if err != nil { return nil, err } 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 := paymentSvc.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 支付 // 通过 order.PaymentConfigID 动态加载支付配置,支持多商户场景 func (s *Service) WechatPayH5(ctx context.Context, orderID uint, sceneInfo *dto.WechatH5SceneInfo, buyerType string, buyerID uint) (*dto.WechatPayH5Response, error) { 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, "订单状态不允许支付") } // 按 payment_config_id 动态加载支付配置 if order.PaymentConfigID == nil { s.logger.Error("订单缺少支付配置", zap.Uint("order_id", orderID)) return nil, errors.New(errors.CodeWechatConfigNotFound, "订单缺少支付配置") } paymentSvc, err := s.paymentLoader.LoadConfig(ctx, *order.PaymentConfigID) if err != nil { return nil, err } 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 := paymentSvc.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 和卖家店铺 ID var seriesID *uint var firstCommissionPaid bool var sellerShopID uint if result.Card != nil { seriesID = result.Card.SeriesID if seriesID != nil { firstCommissionPaid = result.Card.IsFirstRechargeTriggeredBySeries(*seriesID) } if result.Card.ShopID != nil { sellerShopID = *result.Card.ShopID } } else if result.Device != nil { seriesID = result.Device.SeriesID if seriesID != nil { firstCommissionPaid = result.Device.IsFirstRechargeTriggeredBySeries(*seriesID) } if result.Device.ShopID != nil { sellerShopID = *result.Device.ShopID } } 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, } } // 6. 平台未设强充,查询卖家代理的 ShopSeriesAllocation,判断代理是否自设强充 // 仅在累计充值模式且平台未启用时,代理强充配置才生效 if sellerShopID > 0 { agentAllocation, allocErr := s.shopSeriesAllocationStore.GetByShopAndSeries(ctx, sellerShopID, *seriesID) if allocErr == nil && agentAllocation.EnableForceRecharge { agentForceAmount := agentAllocation.ForceRechargeAmount if agentForceAmount == 0 { agentForceAmount = config.Threshold } return &ForceRechargeRequirement{ NeedForceRecharge: true, ForceRechargeAmount: agentForceAmount, 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 } func (s *Service) FuiouPayJSAPI(ctx context.Context, orderID uint, openID string, buyerType string, buyerID uint) (*dto.FuiouPayJSAPIResponse, error) { return s.fuiouPreCreate(ctx, orderID, openID, buyerType, buyerID, "JSAPI", false) } func (s *Service) FuiouPayMiniApp(ctx context.Context, orderID uint, openID string, buyerType string, buyerID uint) (*dto.FuiouPayJSAPIResponse, error) { return s.fuiouPreCreate(ctx, orderID, openID, buyerType, buyerID, "LETPAY", true) } func (s *Service) fuiouPreCreate( ctx context.Context, orderID uint, openID string, buyerType string, buyerID uint, tradeType string, useMiniappAppID bool, ) (*dto.FuiouPayJSAPIResponse, error) { if strings.TrimSpace(openID) == "" { return nil, errors.New(errors.CodeInvalidParam, "openID 不能为空") } 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, "订单状态不允许支付") } activeConfig, err := s.wechatConfigService.GetActiveConfig(ctx) if err != nil { return nil, errors.Wrap(errors.CodeFuiouPayFailed, err, "查询生效支付配置失败") } if activeConfig == nil || activeConfig.ProviderType != model.ProviderTypeFuiou { return nil, errors.New(errors.CodeFuiouPayFailed, "未找到生效的富友支付配置") } subAppID := activeConfig.OaAppID if useMiniappAppID { subAppID = activeConfig.MiniappAppID } if strings.TrimSpace(activeConfig.FyInsCd) == "" || strings.TrimSpace(activeConfig.FyMchntCd) == "" || strings.TrimSpace(activeConfig.FyTermID) == "" || strings.TrimSpace(activeConfig.FyPrivateKey) == "" || strings.TrimSpace(activeConfig.FyPublicKey) == "" || strings.TrimSpace(activeConfig.FyAPIURL) == "" || strings.TrimSpace(activeConfig.FyNotifyURL) == "" || strings.TrimSpace(subAppID) == "" { return nil, errors.New(errors.CodeFuiouPayFailed, "富友支付配置不完整") } client, err := fuiou.NewClient( activeConfig.FyInsCd, activeConfig.FyMchntCd, activeConfig.FyTermID, activeConfig.FyAPIURL, activeConfig.FyNotifyURL, activeConfig.FyPrivateKey, activeConfig.FyPublicKey, s.logger, ) if err != nil { return nil, errors.Wrap(errors.CodeFuiouPayFailed, err, "创建富友支付客户端失败") } 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 } termIP := "127.0.0.1" if ip := middleware.GetIPFromContext(ctx); ip != nil && strings.TrimSpace(*ip) != "" { termIP = *ip } resp, err := client.WxPreCreate( order.OrderNo, strconv.FormatInt(order.TotalAmount, 10), description, termIP, tradeType, subAppID, openID, ) if err != nil { s.logger.Error("富友预下单失败", zap.Uint("order_id", orderID), zap.String("order_no", order.OrderNo), zap.String("trade_type", tradeType), zap.Error(err), ) return nil, errors.Wrap(errors.CodeFuiouPayFailed, err, "富友预下单失败") } s.logger.Info("富友预下单成功", zap.Uint("order_id", orderID), zap.String("order_no", order.OrderNo), zap.String("trade_type", tradeType), ) return &dto.FuiouPayJSAPIResponse{ AppID: resp.SdkAppid, TimeStamp: resp.SdkTimestamp, NonceStr: resp.SdkNoncestr, Package: resp.SdkPackage, SignType: resp.SdkSigntype, PaySign: resp.SdkPaysign, }, nil }