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
This commit is contained in:
erio
2026-04-07 01:11:20 +08:00
parent 6147e4c0ed
commit ac00a0aeb3
3 changed files with 54 additions and 46 deletions
+1 -1
View File
@@ -1 +1 @@
0.1.108.42
0.1.108.43
@@ -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) {
+10 -6
View File
@@ -25,7 +25,7 @@
<AmountInput
v-model="amount"
:amounts="[10, 50, 100, 200, 500, 1000]"
:min="config?.min_amount ?? 1"
:min="minAmount"
:max="maxAmount"
/>
<p v-if="amountError" class="mt-2 text-xs text-amber-600 dark:text-amber-300">{{ amountError }}</p>
@@ -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<PaymentMethodOption[]>(() =>
@@ -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) {