package wallet import ( "context" "hyapp/pkg/appcode" "hyapp/pkg/xerr" "hyapp/services/wallet-service/internal/domain/ledger" "strings" ) // sharedPaymentConfigAppCode 是三方收单方式的唯一国家/币种配置 owner。 // Yumi、Aslan 等 App 可以有自己的商品和用户区域,但不能复制一套 V5Pay/MiFaPay // 国家、币种或汇率配置,否则人工收款与 H5 下单会因配置漂移得到不同的支付结果。 const sharedPaymentConfigAppCode = appcode.Default func usesSharedPaymentConfig(providerCode string) bool { switch ledger.NormalizePaymentProvider(providerCode) { case ledger.PaymentProviderMifaPay, ledger.PaymentProviderV5Pay: return true default: return false } } // ListThirdPartyPaymentChannels 返回后台三方支付管理页需要的渠道、国家和方式树。 func (s *Service) ListThirdPartyPaymentChannels(ctx context.Context, query ledger.ListThirdPartyPaymentChannelsQuery) ([]ledger.ThirdPartyPaymentChannel, error) { if s.repository == nil { return nil, xerr.New(xerr.Unavailable, "wallet repository is not configured") } query.AppCode = appcode.Normalize(query.AppCode) if strings.TrimSpace(query.Status) != "" { query.Status = ledger.NormalizeThirdPartyPaymentStatus(query.Status) if query.Status == "" { return nil, xerr.New(xerr.InvalidArgument, "status is invalid") } } // 指定渠道时,V5Pay/MiFaPay 的国家和币种统一由 Lalu 维护。读取端始终切到 // 该配置 owner,避免某个 App 没有复制支付方式时,临时链接和人工查单退化为无候选可用。 if usesSharedPaymentConfig(query.ProviderCode) { query.AppCode = sharedPaymentConfigAppCode ctx = appcode.WithContext(ctx, query.AppCode) return s.repository.ListThirdPartyPaymentChannels(ctx, query) } ctx = appcode.WithContext(ctx, query.AppCode) channels, err := s.repository.ListThirdPartyPaymentChannels(ctx, query) if err != nil || query.AppCode == sharedPaymentConfigAppCode || query.ProviderCode != "" { return channels, err } // 未指定渠道的临时收款页面仍要同时展示两个共享渠道。保留当前 App 的非共享渠道, // 但移除其历史副本,确保 UI 不会把同一国家/币种配置展示为两套来源。 filtered := make([]ledger.ThirdPartyPaymentChannel, 0, len(channels)+2) for _, channel := range channels { if !usesSharedPaymentConfig(channel.ProviderCode) { filtered = append(filtered, channel) } } for _, providerCode := range []string{ledger.PaymentProviderMifaPay, ledger.PaymentProviderV5Pay} { sharedQuery := query sharedQuery.AppCode = sharedPaymentConfigAppCode sharedQuery.ProviderCode = providerCode sharedCtx := appcode.WithContext(ctx, sharedPaymentConfigAppCode) sharedChannels, err := s.repository.ListThirdPartyPaymentChannels(sharedCtx, sharedQuery) if err != nil { return nil, err } filtered = append(filtered, sharedChannels...) } return filtered, nil } // sharedPaymentMethod 返回支付配置 owner 的方式。订单本身仍按 command.AppCode 落库, // 因此只共享国家/币种、产品类型和汇率,不会把 Yumi/Aslan 的账务事实写入 Lalu。 func (s *Service) sharedPaymentMethod(ctx context.Context, appCode string, providerCode string, methodID int64) (ledger.ThirdPartyPaymentMethod, error) { configAppCode := appcode.Normalize(appCode) if usesSharedPaymentConfig(providerCode) { configAppCode = sharedPaymentConfigAppCode } return s.repository.GetThirdPartyPaymentMethod(ctx, configAppCode, methodID) } func (s *Service) SetThirdPartyPaymentMethodStatus(ctx context.Context, command ledger.ThirdPartyPaymentMethodStatusCommand) (ledger.ThirdPartyPaymentMethod, error) { if command.MethodID <= 0 || command.OperatorUserID <= 0 { return ledger.ThirdPartyPaymentMethod{}, xerr.New(xerr.InvalidArgument, "payment method and operator are required") } if s.repository == nil { return ledger.ThirdPartyPaymentMethod{}, xerr.New(xerr.Unavailable, "wallet repository is not configured") } command.AppCode = appcode.Normalize(command.AppCode) ctx = appcode.WithContext(ctx, command.AppCode) return s.repository.SetThirdPartyPaymentMethodStatus(ctx, command) } func (s *Service) UpdateThirdPartyPaymentRate(ctx context.Context, command ledger.ThirdPartyPaymentRateCommand) (ledger.ThirdPartyPaymentMethod, error) { command.AppCode = appcode.Normalize(command.AppCode) command.USDToCurrencyRate = strings.TrimSpace(command.USDToCurrencyRate) if command.MethodID <= 0 || command.OperatorUserID <= 0 || !validNonNegativeDecimal(command.USDToCurrencyRate) { return ledger.ThirdPartyPaymentMethod{}, xerr.New(xerr.InvalidArgument, "payment rate command is invalid") } if s.repository == nil { return ledger.ThirdPartyPaymentMethod{}, xerr.New(xerr.Unavailable, "wallet repository is not configured") } ctx = appcode.WithContext(ctx, command.AppCode) return s.repository.UpdateThirdPartyPaymentRate(ctx, command) } // ListH5RechargeOptions 只返回目标用户当前可买商品和已配置汇率的支付方式。 func (s *Service) ListH5RechargeOptions(ctx context.Context, query ledger.H5RechargeOptionsQuery) (ledger.H5RechargeOptions, error) { query.AppCode = appcode.Normalize(query.AppCode) query.AudienceType = ledger.NormalizeRechargeAudienceType(query.AudienceType) query.TargetCountryCode = strings.ToUpper(strings.TrimSpace(query.TargetCountryCode)) if query.TargetUserID <= 0 || query.TargetRegionID <= 0 || query.AudienceType == "" { return ledger.H5RechargeOptions{}, xerr.New(xerr.InvalidArgument, "h5 recharge options query is incomplete") } if s.repository == nil { return ledger.H5RechargeOptions{}, xerr.New(xerr.Unavailable, "wallet repository is not configured") } ctx = appcode.WithContext(ctx, query.AppCode) options, err := s.repository.ListH5RechargeOptions(ctx, query) if err != nil { return ledger.H5RechargeOptions{}, err } if s.mifaPay == nil && s.v5Pay == nil { options.PaymentMethods = nil } else { filtered := options.PaymentMethods[:0] for _, method := range options.PaymentMethods { if method.ProviderCode == ledger.PaymentProviderMifaPay && s.mifaPay != nil { filtered = append(filtered, method) } if method.ProviderCode == ledger.PaymentProviderV5Pay && s.v5Pay != nil { filtered = append(filtered, method) } } options.PaymentMethods = s.filterV5PayRuntimeProductTypes(ctx, filtered) } usdtTRC20Address := s.externalRecharge.usdtTRC20AddressForApp(query.AppCode) options.USDTTRC20Enabled = s.externalRecharge.USDTTRC20Enabled && usdtTRC20Address != "" options.USDTTRC20Address = usdtTRC20Address return options, nil } func (s *Service) filterV5PayRuntimeProductTypes(ctx context.Context, methods []ledger.ThirdPartyPaymentMethod) []ledger.ThirdPartyPaymentMethod { if s.v5Pay == nil || len(methods) == 0 { return methods } type productKey struct { country string currency string } allowedByKey := map[productKey]map[string]bool{} filtered := methods[:0] for _, method := range methods { if method.ProviderCode != ledger.PaymentProviderV5Pay { filtered = append(filtered, method) continue } key := productKey{country: strings.ToUpper(strings.TrimSpace(method.CountryCode)), currency: strings.ToUpper(strings.TrimSpace(method.CurrencyCode))} allowed, ok := allowedByKey[key] if !ok { allowed = map[string]bool{} products, err := s.v5Pay.ListProductTypes(ctx, V5PayProductTypesRequest{CountryCode: key.country, CurrencyCode: key.currency}) if err == nil { for _, product := range products.PayinList { if product.SupportCashierMode == 0 { continue } if product.Currency != "" && !strings.EqualFold(product.Currency, key.currency) { continue } if productType := strings.ToUpper(strings.TrimSpace(product.ProductType)); productType != "" { allowed[productType] = true } } } allowedByKey[key] = allowed } if allowed[strings.ToUpper(strings.TrimSpace(method.PayType))] { filtered = append(filtered, method) } } return filtered }