package refund import ( "context" "strings" "gorm.io/gorm" merchantpayment "github.com/break/junhong_cmp_fiber/internal/application/merchantpayment" refundchannelapp "github.com/break/junhong_cmp_fiber/internal/application/refundchannel" "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/pkg/errors" ) // SetPaymentMerchantRuntime 注入冻结商户的当前凭证版本加载器。 func (s *Service) SetPaymentMerchantRuntime(runtime *merchantpayment.RuntimeLoader) { s.paymentMerchantRuntime = runtime } // preparePaymentRefundCredentials 在企微通过事务内装载并校验原路退款所需凭证。 // // 钱包支付和线下支付没有收款商户凭证,直接放行。merchant_id 为空仅表示留存期内的历史 // 线上支付,仍按 payment_config_id 读取兼容配置;绝不能按当前商户池推断历史商户。 func (s *Service) preparePaymentRefundCredentials(ctx context.Context, tx *gorm.DB, orderID uint) error { var payment model.Payment err := tx.WithContext(ctx). Where("order_id = ? AND order_type = ? AND status = ?", orderID, model.PaymentOrderTypePackage, model.PaymentRecordStatusPaid). Order("id DESC"). First(&payment).Error if err != nil { if err == gorm.ErrRecordNotFound { return nil } return errors.Wrap(errors.CodeDatabaseError, err, "查询退款关联支付单失败") } // 钱包支付和线下支付没有收款商户凭证,不被本检查阻断。 if payment.PaymentMethod == model.PaymentByWallet || payment.PaymentMethod == model.PaymentByOffline { return nil } if payment.MerchantID != nil { if s.paymentMerchantRuntime == nil { return errors.New(errors.CodeServiceUnavailable, "商户凭证加载能力未配置") } merchant, loadErr := s.paymentMerchantRuntime.LoadMerchant(ctx, *payment.MerchantID) if loadErr != nil { return loadErr } return validateFrozenMerchantRefundCredentials(merchant) } if payment.PaymentConfigID == nil { return errors.New(errors.CodeNoPaymentConfig, "历史支付单缺少支付配置") } var legacy model.WechatConfig if err := tx.WithContext(ctx).Unscoped().First(&legacy, *payment.PaymentConfigID).Error; err != nil { if err == gorm.ErrRecordNotFound { return errors.New(errors.CodeNoPaymentConfig, "历史支付配置不可用") } return errors.Wrap(errors.CodeDatabaseError, err, "读取历史支付配置失败") } return nil } // validateFrozenMerchantRefundCredentials 判断冻结商户是否具备原路退款能力。 // // 能力只由服务商类型与该服务商类型退款所需凭证的完整性决定,不提供人工开关: // - 微信直连 v3:具备 v3 密钥、证书、私钥与证书序列号时可调用 v3 退款接口; // - 微信 v2:其退款接口需要 API 客户端证书,而商户凭证键集合不含该证书,故判定不可用; // - 富友:凭证键集合已含退款与退款查询所需参数,能力可用; // - 支付宝:具备 AppID、应用私钥与支付宝公钥时可签名退款请求。 func validateFrozenMerchantRefundCredentials(merchant *model.PaymentMerchant) error { config, err := merchantRefundConfig(merchant) if err != nil { return err } if reason := channelRefundCredentialIssue(config); reason != "" { return errors.New(errors.CodeNoPaymentConfig, reason) } return nil } // merchantRefundConfig 把冻结商户凭证适配为渠道配置,供退款能力判定与渠道调用使用。 func merchantRefundConfig(merchant *model.PaymentMerchant) (*model.WechatConfig, error) { if merchant == nil { return nil, errors.New(errors.CodeNoPaymentConfig, "支付商户不存在") } return merchantpayment.MerchantConfig(merchant, nil) } // loadLegacyPaymentConfig 按历史支付配置标识读取兼容配置。 func loadLegacyPaymentConfig(ctx context.Context, db *gorm.DB, configID uint) (*model.WechatConfig, error) { if db == nil || configID == 0 { return nil, errors.New(errors.CodeNoPaymentConfig, "历史支付配置不可用") } var legacy model.WechatConfig if err := db.WithContext(ctx).Unscoped().First(&legacy, configID).Error; err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeNoPaymentConfig, "历史支付配置不可用") } return nil, errors.Wrap(errors.CodeDatabaseError, err, "读取历史支付配置失败") } return &legacy, nil } // channelRefundCredentialIssue 返回该商户凭证不满足原路退款要求的原因;凭证完整时返回空串。 // // 判定规则只有一处来源:refundchannel.RefundCredentialIssue。申请阶段的方式预检与执行阶段的 // 凭证校验必须完全一致,否则会出现「申请时可选原路、执行时凭证不完整」的不一致。 func channelRefundCredentialIssue(config *model.WechatConfig) string { if config == nil { return "商户支付凭证不可用" } return refundchannelapp.RefundCredentialIssue(config.ProviderType, config) } func blank(values ...string) bool { for _, value := range values { if strings.TrimSpace(value) == "" { return true } } return false }