fix(payment): 保存系统设置时不再清空可见支付方式配置

UpdatePaymentConfig 无条件写入全部设置键,未传的指针字段经
derefStr(nil) / formatBoolOrEmpty(nil) 转成空串后仍会落库。

setting_handler_update.go 构造 UpdatePaymentConfigRequest 时从不填充
VisibleMethod* 四个字段,而 UpdateSettingsWithAuthSourceDefaultsOmitting
先写入正确值、UpdatePaymentConfig 紧接着以空串覆盖,导致管理员每次保存
系统设置都会静默重置支付宝/微信的可见支付方式路由。

改为仅写入调用方显式提供的字段,符合 PATCH 语义。EnabledTypes 传
空切片仍可显式清空。
This commit is contained in:
dawn
2026-07-31 14:21:36 +08:00
parent f9d2791693
commit 3deb2f17d8
2 changed files with 160 additions and 30 deletions
@@ -343,38 +343,87 @@ func (s *PaymentConfigService) UpdatePaymentConfig(ctx context.Context, req Upda
return infraerrors.BadRequest("INVALID_RECHARGE_FEE_RATE", "recharge fee rate allows at most 2 decimal places")
}
}
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),
SettingBalanceRechargeMult: formatPositiveFloat(req.BalanceRechargeMultiplier),
SettingSubscriptionUSDToCNYRate: formatPositiveFloatExact(req.SubscriptionUSDToCNYRate),
SettingRechargeFeeRate: formatNonNegativeFloat(req.RechargeFeeRate),
SettingLoadBalanceStrategy: derefStr(req.LoadBalanceStrategy),
SettingProductNamePrefix: derefStr(req.ProductNamePrefix),
SettingProductNameSuffix: derefStr(req.ProductNameSuffix),
SettingHelpImageURL: derefStr(req.HelpImageURL),
SettingHelpText: derefStr(req.HelpText),
SettingCancelRateLimitOn: formatBoolOrEmpty(req.CancelRateLimitEnabled),
SettingCancelRateLimitMax: formatPositiveInt(req.CancelRateLimitMax),
SettingCancelWindowSize: formatPositiveInt(req.CancelRateLimitWindow),
SettingCancelWindowUnit: derefStr(req.CancelRateLimitUnit),
SettingCancelWindowMode: derefStr(req.CancelRateLimitMode),
SettingAlipayForceQRCode: formatBoolOrEmpty(req.AlipayForceQRCode),
SettingAlipayMobilePrecreateDeepLink: formatBoolOrEmpty(req.AlipayMobilePrecreateDeepLink),
SettingPaymentVisibleMethodAlipaySource: derefStr(req.VisibleMethodAlipaySource),
SettingPaymentVisibleMethodWxpaySource: derefStr(req.VisibleMethodWxpaySource),
SettingPaymentVisibleMethodAlipayEnabled: formatBoolOrEmpty(req.VisibleMethodAlipayEnabled),
SettingPaymentVisibleMethodWxpayEnabled: formatBoolOrEmpty(req.VisibleMethodWxpayEnabled),
m := make(map[string]string)
if req.Enabled != nil {
m[SettingPaymentEnabled] = formatBoolOrEmpty(req.Enabled)
}
if req.MinAmount != nil {
m[SettingMinRechargeAmount] = formatPositiveFloat(req.MinAmount)
}
if req.MaxAmount != nil {
m[SettingMaxRechargeAmount] = formatPositiveFloat(req.MaxAmount)
}
if req.DailyLimit != nil {
m[SettingDailyRechargeLimit] = formatPositiveFloat(req.DailyLimit)
}
if req.OrderTimeoutMin != nil {
m[SettingOrderTimeoutMinutes] = formatPositiveInt(req.OrderTimeoutMin)
}
if req.MaxPendingOrders != nil {
m[SettingMaxPendingOrders] = formatPositiveInt(req.MaxPendingOrders)
}
if req.EnabledTypes != nil {
m[SettingEnabledPaymentTypes] = strings.Join(req.EnabledTypes, ",")
} else {
m[SettingEnabledPaymentTypes] = ""
}
if req.BalanceDisabled != nil {
m[SettingBalancePayDisabled] = formatBoolOrEmpty(req.BalanceDisabled)
}
if req.BalanceRechargeMultiplier != nil {
m[SettingBalanceRechargeMult] = formatPositiveFloat(req.BalanceRechargeMultiplier)
}
if req.SubscriptionUSDToCNYRate != nil {
m[SettingSubscriptionUSDToCNYRate] = formatPositiveFloatExact(req.SubscriptionUSDToCNYRate)
}
if req.RechargeFeeRate != nil {
m[SettingRechargeFeeRate] = formatNonNegativeFloat(req.RechargeFeeRate)
}
if req.LoadBalanceStrategy != nil {
m[SettingLoadBalanceStrategy] = derefStr(req.LoadBalanceStrategy)
}
if req.ProductNamePrefix != nil {
m[SettingProductNamePrefix] = derefStr(req.ProductNamePrefix)
}
if req.ProductNameSuffix != nil {
m[SettingProductNameSuffix] = derefStr(req.ProductNameSuffix)
}
if req.HelpImageURL != nil {
m[SettingHelpImageURL] = derefStr(req.HelpImageURL)
}
if req.HelpText != nil {
m[SettingHelpText] = derefStr(req.HelpText)
}
if req.CancelRateLimitEnabled != nil {
m[SettingCancelRateLimitOn] = formatBoolOrEmpty(req.CancelRateLimitEnabled)
}
if req.CancelRateLimitMax != nil {
m[SettingCancelRateLimitMax] = formatPositiveInt(req.CancelRateLimitMax)
}
if req.CancelRateLimitWindow != nil {
m[SettingCancelWindowSize] = formatPositiveInt(req.CancelRateLimitWindow)
}
if req.CancelRateLimitUnit != nil {
m[SettingCancelWindowUnit] = derefStr(req.CancelRateLimitUnit)
}
if req.CancelRateLimitMode != nil {
m[SettingCancelWindowMode] = derefStr(req.CancelRateLimitMode)
}
if req.AlipayForceQRCode != nil {
m[SettingAlipayForceQRCode] = formatBoolOrEmpty(req.AlipayForceQRCode)
}
if req.AlipayMobilePrecreateDeepLink != nil {
m[SettingAlipayMobilePrecreateDeepLink] = formatBoolOrEmpty(req.AlipayMobilePrecreateDeepLink)
}
if req.VisibleMethodAlipaySource != nil {
m[SettingPaymentVisibleMethodAlipaySource] = derefStr(req.VisibleMethodAlipaySource)
}
if req.VisibleMethodWxpaySource != nil {
m[SettingPaymentVisibleMethodWxpaySource] = derefStr(req.VisibleMethodWxpaySource)
}
if req.VisibleMethodAlipayEnabled != nil {
m[SettingPaymentVisibleMethodAlipayEnabled] = formatBoolOrEmpty(req.VisibleMethodAlipayEnabled)
}
if req.VisibleMethodWxpayEnabled != nil {
m[SettingPaymentVisibleMethodWxpayEnabled] = formatBoolOrEmpty(req.VisibleMethodWxpayEnabled)
}
return s.settingRepo.SetMultiple(ctx, m)
}
@@ -441,7 +441,12 @@ func (s *paymentConfigSettingRepoStub) GetAll(context.Context) (map[string]strin
func (s *paymentConfigSettingRepoStub) Delete(context.Context, string) error { return nil }
func TestUpdatePaymentConfig_PersistsVisibleMethodRouting(t *testing.T) {
repo := &paymentConfigSettingRepoStub{values: map[string]string{}}
repo := &paymentConfigSettingRepoStub{values: map[string]string{
SettingPaymentVisibleMethodAlipayEnabled: "false",
SettingPaymentVisibleMethodAlipaySource: VisibleMethodSourceOfficialAlipay,
SettingPaymentVisibleMethodWxpayEnabled: "true",
SettingPaymentVisibleMethodWxpaySource: VisibleMethodSourceEasyPayWechat,
}}
svc := &PaymentConfigService{settingRepo: repo}
alipayEnabled := true
@@ -470,6 +475,82 @@ func TestUpdatePaymentConfig_PersistsVisibleMethodRouting(t *testing.T) {
}
}
func TestUpdatePaymentConfig_OmittedVisibleMethodRoutingIsPreserved(t *testing.T) {
wantVisibleMethods := map[string]string{
SettingPaymentVisibleMethodAlipayEnabled: "true",
SettingPaymentVisibleMethodAlipaySource: VisibleMethodSourceEasyPayAlipay,
SettingPaymentVisibleMethodWxpayEnabled: "false",
SettingPaymentVisibleMethodWxpaySource: VisibleMethodSourceOfficialWechat,
}
initial := make(map[string]string, len(wantVisibleMethods))
for key, value := range wantVisibleMethods {
initial[key] = value
}
repo := &paymentConfigSettingRepoStub{values: initial}
svc := &PaymentConfigService{settingRepo: repo}
enabled := true
err := svc.UpdatePaymentConfig(context.Background(), UpdatePaymentConfigRequest{Enabled: &enabled})
if err != nil {
t.Fatalf("UpdatePaymentConfig returned error: %v", err)
}
visibleMethodKeys := []string{
SettingPaymentVisibleMethodAlipayEnabled,
SettingPaymentVisibleMethodAlipaySource,
SettingPaymentVisibleMethodWxpayEnabled,
SettingPaymentVisibleMethodWxpaySource,
}
for _, key := range visibleMethodKeys {
if _, ok := repo.updates[key]; ok {
t.Fatalf("omitted visible method setting %q was written", key)
}
if repo.values[key] != wantVisibleMethods[key] {
t.Fatalf("visible method setting %q = %q, want preserved value %q", key, repo.values[key], wantVisibleMethods[key])
}
}
if repo.updates[SettingPaymentEnabled] != "true" {
t.Fatalf("payment enabled update = %q, want true", repo.updates[SettingPaymentEnabled])
}
}
func TestUpdatePaymentConfig_PersistsExplicitEmptyAndFalseValues(t *testing.T) {
repo := &paymentConfigSettingRepoStub{values: map[string]string{
SettingEnabledPaymentTypes: "alipay,wxpay",
SettingBalancePayDisabled: "true",
SettingProductNamePrefix: "existing",
}}
svc := &PaymentConfigService{settingRepo: repo}
falseValue := false
emptyString := ""
err := svc.UpdatePaymentConfig(context.Background(), UpdatePaymentConfigRequest{
EnabledTypes: []string{},
BalanceDisabled: &falseValue,
ProductNamePrefix: &emptyString,
})
if err != nil {
t.Fatalf("UpdatePaymentConfig returned error: %v", err)
}
want := map[string]string{
SettingEnabledPaymentTypes: "",
SettingBalancePayDisabled: "false",
SettingProductNamePrefix: "",
}
if len(repo.updates) != len(want) {
t.Fatalf("updates = %v, want exactly %v", repo.updates, want)
}
for key, value := range want {
if repo.updates[key] != value {
t.Fatalf("update %q = %q, want %q", key, repo.updates[key], value)
}
if repo.values[key] != value {
t.Fatalf("stored %q = %q, want %q", key, repo.values[key], value)
}
}
}
func paymentConfigStrPtr(value string) *string {
return &value
}