From ac00a0aeb3fb2100b6be80e029a67db9fcf7df4b Mon Sep 17 00:00:00 2001 From: erio Date: Tue, 7 Apr 2026 01:11:20 +0800 Subject: [PATCH] fix(payment): full-replace config update + fix min/max defaults - Payment config update is now full-replace (not patch): all fields sent every time - 0 values for min/max/daily = clear (use default: min=1, max=unlimited) - Payment page: provider-level limits override global, proper fallback chain - Fix quick amounts disappearing when global min/max is empty --- backend/cmd/server/VERSION | 2 +- .../service/payment_config_service.go | 82 ++++++++++--------- frontend/src/views/user/PaymentView.vue | 16 ++-- 3 files changed, 54 insertions(+), 46 deletions(-) diff --git a/backend/cmd/server/VERSION b/backend/cmd/server/VERSION index 20f0101a2d..e794306a69 100644 --- a/backend/cmd/server/VERSION +++ b/backend/cmd/server/VERSION @@ -1 +1 @@ -0.1.108.42 +0.1.108.43 diff --git a/backend/internal/service/payment_config_service.go b/backend/internal/service/payment_config_service.go index f33505895e..5a5de735d0 100644 --- a/backend/internal/service/payment_config_service.go +++ b/backend/internal/service/payment_config_service.go @@ -207,52 +207,56 @@ func (s *PaymentConfigService) parsePaymentConfig(vals map[string]string) *Payme // nil-check before serialisation — this is inherent to patch-style update patterns // and cannot be meaningfully decomposed without introducing unnecessary abstraction. func (s *PaymentConfigService) UpdatePaymentConfig(ctx context.Context, req UpdatePaymentConfigRequest) error { - m := make(map[string]string) - if req.Enabled != nil { - m[SettingPaymentEnabled] = strconv.FormatBool(*req.Enabled) - } - if req.MinAmount != nil { - m[SettingMinRechargeAmount] = strconv.FormatFloat(*req.MinAmount, 'f', 2, 64) - } - if req.MaxAmount != nil { - m[SettingMaxRechargeAmount] = strconv.FormatFloat(*req.MaxAmount, 'f', 2, 64) - } - if req.DailyLimit != nil { - m[SettingDailyRechargeLimit] = strconv.FormatFloat(*req.DailyLimit, 'f', 2, 64) - } - if req.OrderTimeoutMin != nil { - m[SettingOrderTimeoutMinutes] = strconv.Itoa(*req.OrderTimeoutMin) - } - if req.MaxPendingOrders != nil { - m[SettingMaxPendingOrders] = strconv.Itoa(*req.MaxPendingOrders) + m := map[string]string{ + SettingPaymentEnabled: formatBoolOrEmpty(req.Enabled), + SettingMinRechargeAmount: formatPositiveFloat(req.MinAmount), + SettingMaxRechargeAmount: formatPositiveFloat(req.MaxAmount), + SettingDailyRechargeLimit: formatPositiveFloat(req.DailyLimit), + SettingOrderTimeoutMinutes: formatPositiveInt(req.OrderTimeoutMin), + SettingMaxPendingOrders: formatPositiveInt(req.MaxPendingOrders), + SettingBalancePayDisabled: formatBoolOrEmpty(req.BalanceDisabled), + SettingLoadBalanceStrategy: derefStr(req.LoadBalanceStrategy), + SettingProductNamePrefix: derefStr(req.ProductNamePrefix), + SettingProductNameSuffix: derefStr(req.ProductNameSuffix), + SettingHelpImageURL: derefStr(req.HelpImageURL), + SettingHelpText: derefStr(req.HelpText), } if req.EnabledTypes != nil { m[SettingEnabledPaymentTypes] = strings.Join(req.EnabledTypes, ",") - } - if req.BalanceDisabled != nil { - m[SettingBalancePayDisabled] = strconv.FormatBool(*req.BalanceDisabled) - } - if req.LoadBalanceStrategy != nil { - m[SettingLoadBalanceStrategy] = *req.LoadBalanceStrategy - } - if req.ProductNamePrefix != nil { - m[SettingProductNamePrefix] = *req.ProductNamePrefix - } - 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 + } else { + m[SettingEnabledPaymentTypes] = "" } return s.settingRepo.SetMultiple(ctx, m) } +func formatBoolOrEmpty(v *bool) string { + if v == nil { + return "" + } + return strconv.FormatBool(*v) +} + +func formatPositiveFloat(v *float64) string { + if v == nil || *v <= 0 { + return "" // empty → parsePaymentConfig uses default + } + return strconv.FormatFloat(*v, 'f', 2, 64) +} + +func formatPositiveInt(v *int) string { + if v == nil || *v <= 0 { + return "" + } + return strconv.Itoa(*v) +} + +func derefStr(v *string) string { + if v == nil { + return "" + } + return *v +} + // --- Provider Instance CRUD --- func (s *PaymentConfigService) ListProviderInstances(ctx context.Context) ([]*dbent.PaymentProviderInstance, error) { diff --git a/frontend/src/views/user/PaymentView.vue b/frontend/src/views/user/PaymentView.vue index 59ade1063a..35d502b8a5 100644 --- a/frontend/src/views/user/PaymentView.vue +++ b/frontend/src/views/user/PaymentView.vue @@ -25,7 +25,7 @@

{{ amountError }}

@@ -151,9 +151,15 @@ const tabs = computed(() => { }) const enabledMethods = computed(() => config.value?.enabled_payment_types ?? []) +const minAmount = computed(() => { + const limit = methodLimits.value[selectedMethod.value] + if (limit?.single_min && limit.single_min > 0) return limit.single_min + return config.value?.min_amount && config.value.min_amount > 0 ? config.value.min_amount : 1 +}) const maxAmount = computed(() => { const limit = methodLimits.value[selectedMethod.value] - return limit?.single_max && limit.single_max > 0 ? limit.single_max : (config.value?.max_amount ?? 10000) + if (limit?.single_max && limit.single_max > 0) return limit.single_max + return config.value?.max_amount && config.value.max_amount > 0 ? config.value.max_amount : 99999999 }) const methodOptions = computed(() => @@ -178,16 +184,14 @@ const totalAmount = computed(() => const amountError = computed(() => { if (validAmount.value <= 0) return '' - const min = config.value?.min_amount ?? 1 - if (validAmount.value < min) return t('payment.amountTooLow', { min }) + if (validAmount.value < minAmount.value) return t('payment.amountTooLow', { min: minAmount.value }) if (validAmount.value > maxAmount.value) return t('payment.amountTooHigh', { max: maxAmount.value }) return '' }) const canSubmit = computed(() => { - const min = config.value?.min_amount ?? 1 const limitInfo = methodLimits.value[selectedMethod.value] - return validAmount.value >= min && validAmount.value <= maxAmount.value && limitInfo?.available !== false + return validAmount.value >= minAmount.value && validAmount.value <= maxAmount.value && limitInfo?.available !== false }) function openSubscribeDialog(plan: SubscriptionPlan) {