diff --git a/services/wallet-service/internal/service/wallet/external_recharge_order.go b/services/wallet-service/internal/service/wallet/external_recharge_order.go index 3a2ea089..440057c6 100644 --- a/services/wallet-service/internal/service/wallet/external_recharge_order.go +++ b/services/wallet-service/internal/service/wallet/external_recharge_order.go @@ -55,7 +55,7 @@ func (s *Service) createMifaPayRechargeOrder(ctx context.Context, command ledger if s.mifaPay == nil { return ledger.ExternalRechargeOrder{}, xerr.New(xerr.Unavailable, "mifapay client is not configured") } - method, err := s.repository.GetThirdPartyPaymentMethod(ctx, command.AppCode, command.PaymentMethodID) + method, err := s.sharedPaymentMethod(ctx, command.AppCode, ledger.PaymentProviderMifaPay, command.PaymentMethodID) if err != nil { return ledger.ExternalRechargeOrder{}, err } @@ -106,7 +106,7 @@ func (s *Service) createV5PayRechargeOrder(ctx context.Context, command ledger.C if s.v5Pay == nil { return ledger.ExternalRechargeOrder{}, xerr.New(xerr.Unavailable, "v5pay client is not configured") } - method, err := s.repository.GetThirdPartyPaymentMethod(ctx, command.AppCode, command.PaymentMethodID) + method, err := s.sharedPaymentMethod(ctx, command.AppCode, ledger.PaymentProviderV5Pay, command.PaymentMethodID) if err != nil { return ledger.ExternalRechargeOrder{}, err } diff --git a/services/wallet-service/internal/service/wallet/external_recharge_payment_methods.go b/services/wallet-service/internal/service/wallet/external_recharge_payment_methods.go index dee1b2ae..4697b81f 100644 --- a/services/wallet-service/internal/service/wallet/external_recharge_payment_methods.go +++ b/services/wallet-service/internal/service/wallet/external_recharge_payment_methods.go @@ -8,6 +8,20 @@ import ( "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 { @@ -20,8 +34,49 @@ func (s *Service) ListThirdPartyPaymentChannels(ctx context.Context, query ledge 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) - return s.repository.ListThirdPartyPaymentChannels(ctx, query) + 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) { diff --git a/services/wallet-service/internal/service/wallet/external_recharge_receipt.go b/services/wallet-service/internal/service/wallet/external_recharge_receipt.go index c6193c44..ada294b5 100644 --- a/services/wallet-service/internal/service/wallet/external_recharge_receipt.go +++ b/services/wallet-service/internal/service/wallet/external_recharge_receipt.go @@ -282,7 +282,7 @@ func (s *Service) v5PayReceiptQueryCandidates(ctx context.Context, command ledge if s.repository == nil { return candidates, nil } - channels, err := s.repository.ListThirdPartyPaymentChannels(ctx, ledger.ListThirdPartyPaymentChannelsQuery{ + channels, err := s.ListThirdPartyPaymentChannels(ctx, ledger.ListThirdPartyPaymentChannelsQuery{ AppCode: command.AppCode, ProviderCode: ledger.PaymentProviderV5Pay, Status: ledger.ThirdPartyPaymentStatusActive, diff --git a/services/wallet-service/internal/service/wallet/external_recharge_receipt_test.go b/services/wallet-service/internal/service/wallet/external_recharge_receipt_test.go index 59f663db..a3a7b72a 100644 --- a/services/wallet-service/internal/service/wallet/external_recharge_receipt_test.go +++ b/services/wallet-service/internal/service/wallet/external_recharge_receipt_test.go @@ -427,6 +427,45 @@ func TestVerifyCoinSellerRechargeReceiptV5PayWithoutCountryCurrencyUsesEnabledCa } } +func TestVerifyCoinSellerRechargeReceiptV5PayUsesLaluCountryCurrencyConfig(t *testing.T) { + repository := &fakeReceiptRepository{channels: []ledger.ThirdPartyPaymentChannel{{ + AppCode: "lalu", + ProviderCode: ledger.PaymentProviderV5Pay, + Status: ledger.ThirdPartyPaymentStatusActive, + Methods: []ledger.ThirdPartyPaymentMethod{{ + ProviderCode: ledger.PaymentProviderV5Pay, + CountryCode: "TR", + CurrencyCode: "TRY", + Status: ledger.ThirdPartyPaymentStatusActive, + }}, + }}} + v5Pay := &fakeV5PayClient{queryResp: walletservice.V5PayQueryOrderResponse{ + OrderID: "receipt-v5-yumi-shared-config", + ProviderOrderID: "tx_receipt_v5_yumi_shared_config", + Status: "2", + Amount: "48.00", + Currency: "TRY", + ProductType: "BANK_TRANSFER", + RawJSON: `{"status":"2"}`, + }} + svc := walletservice.New(repository) + svc.SetV5PayClient(v5Pay) + receipt, err := svc.VerifyCoinSellerRechargeReceipt(context.Background(), ledger.VerifyCoinSellerRechargeReceiptCommand{ + AppCode: "yumi", + ProviderCode: ledger.PaymentProviderV5Pay, + ExternalOrderNo: "receipt-v5-yumi-shared-config", + }) + if err != nil { + t.Fatalf("VerifyCoinSellerRechargeReceipt v5pay shared config failed: %v", err) + } + if !receipt.Verified || repository.query.AppCode != "lalu" { + t.Fatalf("V5Pay receipt must query Lalu country/currency config, receipt=%+v query=%+v", receipt, repository.query) + } + if len(v5Pay.queryReqs) != 1 || v5Pay.queryReqs[0].CountryCode != "TR" || v5Pay.queryReqs[0].CurrencyCode != "TRY" { + t.Fatalf("V5Pay receipt must use the Lalu candidate, got %+v", v5Pay.queryReqs) + } +} + func TestVerifyCoinSellerRechargeReceiptV5PayWithoutCountryCurrencyNotFound(t *testing.T) { repository := &fakeReceiptRepository{channels: []ledger.ThirdPartyPaymentChannel{{ AppCode: "lalu", diff --git a/services/wallet-service/internal/service/wallet/external_recharge_temporary.go b/services/wallet-service/internal/service/wallet/external_recharge_temporary.go index 85155dac..4385d0fe 100644 --- a/services/wallet-service/internal/service/wallet/external_recharge_temporary.go +++ b/services/wallet-service/internal/service/wallet/external_recharge_temporary.go @@ -35,7 +35,7 @@ func (s *Service) createTemporaryMifaPayRechargeOrder(ctx context.Context, comma if s.mifaPay == nil { return ledger.ExternalRechargeOrder{}, xerr.New(xerr.Unavailable, "mifapay client is not configured") } - method, err := s.repository.GetThirdPartyPaymentMethod(ctx, command.AppCode, command.PaymentMethodID) + method, err := s.sharedPaymentMethod(ctx, command.AppCode, ledger.PaymentProviderMifaPay, command.PaymentMethodID) if err != nil { return ledger.ExternalRechargeOrder{}, err } @@ -89,7 +89,7 @@ func (s *Service) createTemporaryV5PayRechargeOrder(ctx context.Context, command if s.v5Pay == nil { return ledger.ExternalRechargeOrder{}, xerr.New(xerr.Unavailable, "v5pay client is not configured") } - method, err := s.repository.GetThirdPartyPaymentMethod(ctx, command.AppCode, command.PaymentMethodID) + method, err := s.sharedPaymentMethod(ctx, command.AppCode, ledger.PaymentProviderV5Pay, command.PaymentMethodID) if err != nil { return ledger.ExternalRechargeOrder{}, err }