From a481724140c98c5449aa61e1036017d98f58ab65 Mon Sep 17 00:00:00 2001 From: erio Date: Tue, 7 Apr 2026 00:55:13 +0800 Subject: [PATCH] fix(payment): add missing help_image_url/help_text fields + fix config merge on update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add help_image_url and help_text to PaymentConfig, UpdateRequest, read/write logic - Add these fields to frontend paymentPayload save and load - Fix provider config update: merge new config with existing (preserves sensitive fields that frontend skips as ••••••••, prevents data loss on edit) --- backend/cmd/server/VERSION | 2 +- .../service/payment_config_service.go | 66 ++++++++++++++++--- frontend/src/views/admin/SettingsView.vue | 4 ++ 3 files changed, 61 insertions(+), 11 deletions(-) diff --git a/backend/cmd/server/VERSION b/backend/cmd/server/VERSION index 786c342ca1..632bd4d685 100644 --- a/backend/cmd/server/VERSION +++ b/backend/cmd/server/VERSION @@ -1 +1 @@ -0.1.108.39 +0.1.108.40 diff --git a/backend/internal/service/payment_config_service.go b/backend/internal/service/payment_config_service.go index efb6d8439d..eab19e0452 100644 --- a/backend/internal/service/payment_config_service.go +++ b/backend/internal/service/payment_config_service.go @@ -27,6 +27,8 @@ const ( SettingBalancePayDisabled = "BALANCE_PAYMENT_DISABLED" SettingProductNamePrefix = "PRODUCT_NAME_PREFIX" SettingProductNameSuffix = "PRODUCT_NAME_SUFFIX" + SettingHelpImageURL = "PAYMENT_HELP_IMAGE_URL" + SettingHelpText = "PAYMENT_HELP_TEXT" SettingCancelRateLimitOn = "CANCEL_RATE_LIMIT_ENABLED" SettingCancelRateLimitMax = "CANCEL_RATE_LIMIT_MAX" SettingCancelWindowSize = "CANCEL_RATE_LIMIT_WINDOW" @@ -55,6 +57,8 @@ type PaymentConfig struct { LoadBalanceStrategy string `json:"load_balance_strategy"` ProductNamePrefix string `json:"product_name_prefix"` ProductNameSuffix string `json:"product_name_suffix"` + HelpImageURL string `json:"help_image_url"` + HelpText string `json:"help_text"` } // UpdatePaymentConfigRequest contains fields to update payment configuration. @@ -70,6 +74,8 @@ type UpdatePaymentConfigRequest struct { LoadBalanceStrategy *string `json:"load_balance_strategy"` ProductNamePrefix *string `json:"product_name_prefix"` ProductNameSuffix *string `json:"product_name_suffix"` + HelpImageURL *string `json:"help_image_url"` + HelpText *string `json:"help_text"` } // MethodLimits holds per-payment-type limits. @@ -158,6 +164,7 @@ func (s *PaymentConfigService) GetPaymentConfig(ctx context.Context) (*PaymentCo SettingDailyRechargeLimit, SettingOrderTimeoutMinutes, SettingMaxPendingOrders, SettingEnabledPaymentTypes, SettingBalancePayDisabled, SettingLoadBalanceStrategy, SettingProductNamePrefix, SettingProductNameSuffix, + SettingHelpImageURL, SettingHelpText, } vals, err := s.settingRepo.GetMultiple(ctx, keys) if err != nil { @@ -178,6 +185,8 @@ func (s *PaymentConfigService) parsePaymentConfig(vals map[string]string) *Payme LoadBalanceStrategy: vals[SettingLoadBalanceStrategy], ProductNamePrefix: vals[SettingProductNamePrefix], ProductNameSuffix: vals[SettingProductNameSuffix], + HelpImageURL: vals[SettingHelpImageURL], + HelpText: vals[SettingHelpText], } if cfg.LoadBalanceStrategy == "" { cfg.LoadBalanceStrategy = "round-robin" @@ -232,6 +241,12 @@ func (s *PaymentConfigService) UpdatePaymentConfig(ctx context.Context, req Upda if req.ProductNameSuffix != nil { m[SettingProductNameSuffix] = *req.ProductNameSuffix } + if req.HelpImageURL != nil { + m[SettingHelpImageURL] = *req.HelpImageURL + } + if req.HelpText != nil { + m[SettingHelpText] = *req.HelpText + } if len(m) == 0 { return nil } @@ -280,15 +295,8 @@ func (s *PaymentConfigService) ListProviderInstancesWithConfig(ctx context.Conte } func (s *PaymentConfigService) decryptAndMaskConfig(encrypted string) map[string]string { - if encrypted == "" { - return nil - } - decrypted, err := payment.Decrypt(encrypted, s.encryptionKey) - if err != nil { - return nil - } - var raw map[string]string - if err := json.Unmarshal([]byte(decrypted), &raw); err != nil { + raw := s.decryptConfig(encrypted) + if raw == nil { return nil } masked := make(map[string]string, len(raw)) @@ -401,7 +409,12 @@ func (s *PaymentConfigService) UpdateProviderInstance(ctx context.Context, id in u.SetName(*req.Name) } if req.Config != nil { - enc, err := s.encryptConfig(req.Config) + // Merge with existing config: preserve sensitive fields that were not re-submitted + merged, err := s.mergeConfig(ctx, id, req.Config) + if err != nil { + return nil, err + } + enc, err := s.encryptConfig(merged) if err != nil { return nil, err } @@ -425,6 +438,39 @@ func (s *PaymentConfigService) UpdateProviderInstance(ctx context.Context, id in return u.Save(ctx) } +// mergeConfig merges new config with existing config, preserving sensitive +// fields that were not re-submitted (frontend skips masked ••••••••). +func (s *PaymentConfigService) mergeConfig(ctx context.Context, id int64, newConfig map[string]string) (map[string]string, error) { + inst, err := s.entClient.PaymentProviderInstance.Get(ctx, id) + if err != nil { + return nil, fmt.Errorf("load existing provider: %w", err) + } + existing := s.decryptConfig(inst.Config) + if existing == nil { + return newConfig, nil + } + // Start from existing, overwrite with new non-empty values + for k, v := range newConfig { + existing[k] = v + } + return existing, nil +} + +func (s *PaymentConfigService) decryptConfig(encrypted string) map[string]string { + if encrypted == "" { + return nil + } + decrypted, err := payment.Decrypt(encrypted, s.encryptionKey) + if err != nil { + return nil + } + var raw map[string]string + if err := json.Unmarshal([]byte(decrypted), &raw); err != nil { + return nil + } + return raw +} + func (s *PaymentConfigService) DeleteProviderInstance(ctx context.Context, id int64) error { count, err := s.countPendingOrders(ctx, id) if err != nil { diff --git a/frontend/src/views/admin/SettingsView.vue b/frontend/src/views/admin/SettingsView.vue index 6faf82e82c..7a1f701ec0 100644 --- a/frontend/src/views/admin/SettingsView.vue +++ b/frontend/src/views/admin/SettingsView.vue @@ -2385,6 +2385,8 @@ async function loadSettings() { form.payment_product_name_prefix = paymentConfig.product_name_prefix || '' form.payment_product_name_suffix = paymentConfig.product_name_suffix || '' form.payment_load_balance_strategy = paymentConfig.load_balance_strategy || 'round-robin' + form.payment_help_image_url = paymentConfig.help_image_url || '' + form.payment_help_text = paymentConfig.help_text || '' } catch (error: any) { loadFailed.value = true appStore.showError( @@ -2532,6 +2534,8 @@ async function saveSettings() { product_name_prefix: form.payment_product_name_prefix, product_name_suffix: form.payment_product_name_suffix, load_balance_strategy: form.payment_load_balance_strategy, + help_image_url: form.payment_help_image_url, + help_text: form.payment_help_text, } const [updated] = await Promise.all([