From 9e46ce0b0266722fbb9256d3d3eab7379c69d0ce Mon Sep 17 00:00:00 2001 From: zhx Date: Fri, 3 Jul 2026 11:34:28 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B8=81=E7=A7=8D=E6=B1=87=E7=8E=87=E6=94=B9?= =?UTF-8?q?=E5=8F=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/internal/modules/payment/handler.go | 7 ++++++- .../internal/modules/payment/handler_test.go | 19 ++++++++++++++----- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/server/admin/internal/modules/payment/handler.go b/server/admin/internal/modules/payment/handler.go index 909d4cd7..4ba2f5c5 100644 --- a/server/admin/internal/modules/payment/handler.go +++ b/server/admin/internal/modules/payment/handler.go @@ -688,8 +688,13 @@ func applyThirdPartyPaymentRateMarkup(rateText string, markupPercent float64) (s if err != nil || rawRate <= 0 || !validThirdPartyPaymentRateMarkup(markupPercent) { return "", false } - // 汇率源返回的是实时 USD->本币原值;写库前统一叠加运营设置的上浮比例,再按 1 位小数向上取,保证 H5 下单看到的支付汇率不低于实时汇率。 + // 汇率源返回的是实时 USD->本币原值;写库前先叠加运营上浮比例,再按金额量级向上取整,保证 H5 下单汇率不低于实时汇率。 markedRate := rawRate * (1 + markupPercent/100) + if markedRate > 50 { + // 高汇率币种(例如 IDR)展示和支付金额都不应出现小数;减去极小量只抵消 float 乘法毛刺,真实 18510.1 仍会进到 18511。 + return strconv.FormatFloat(math.Ceil(markedRate-1e-9), 'f', 0, 64), true + } + // 低汇率币种仍保留原有“最多 1 位小数且向上取”的策略,避免 SAR/BHD 这类币种因强行整数化放大支付金额。 roundedRate := math.Ceil(markedRate*10-1e-9) / 10 return trimOneDecimalRate(strconv.FormatFloat(roundedRate, 'f', 1, 64)), true } diff --git a/server/admin/internal/modules/payment/handler_test.go b/server/admin/internal/modules/payment/handler_test.go index 0cac3d3e..1c15a579 100644 --- a/server/admin/internal/modules/payment/handler_test.go +++ b/server/admin/internal/modules/payment/handler_test.go @@ -482,7 +482,7 @@ func TestSyncThirdPartyPaymentMethodsDefaultsToV5Pay(t *testing.T) { } } -func TestSyncThirdPartyPaymentRatesAppliesMarkupAndCeilsToOneDecimal(t *testing.T) { +func TestSyncThirdPartyPaymentRatesAppliesMarkupAndCeilRules(t *testing.T) { wallet := &mockPaymentWallet{thirdPartyChannelsResp: &walletv1.ListThirdPartyPaymentChannelsResponse{ Channels: []*walletv1.ThirdPartyPaymentChannel{{ AppCode: "lalu", @@ -500,12 +500,13 @@ func TestSyncThirdPartyPaymentRatesAppliesMarkupAndCeilsToOneDecimal(t *testing. Status: "active", Methods: []*walletv1.ThirdPartyPaymentMethod{ {MethodId: 2310, CurrencyCode: "SAR", Status: "active"}, + {MethodId: 2311, CurrencyCode: "IDR", Status: "active"}, }, }}, }} handler := New(wallet, nil, nil, nil, nil) handler.exchangeRates = fakeExchangeRateClient{result: exchangeRateResult{ - Rates: map[string]string{"SAR": "3.75000000", "BHD": "0.37600000"}, + Rates: map[string]string{"SAR": "3.75000000", "BHD": "0.37600000", "IDR": "18510.10000000"}, SourceNames: []string{"open.er-api.com"}, }} router := newPaymentHandlerTestRouter(handler) @@ -521,7 +522,7 @@ func TestSyncThirdPartyPaymentRatesAppliesMarkupAndCeilsToOneDecimal(t *testing. if wallet.lastThirdPartyChannels == nil || !wallet.lastThirdPartyChannels.GetIncludeDisabledMethods() { t.Fatalf("sync must read active and disabled methods: %+v", wallet.lastThirdPartyChannels) } - if len(wallet.updateRateRequests) != 3 { + if len(wallet.updateRateRequests) != 4 { t.Fatalf("sync should update every method with a fetched currency rate: %+v", wallet.updateRateRequests) } got := map[int64]string{} @@ -531,18 +532,26 @@ func TestSyncThirdPartyPaymentRatesAppliesMarkupAndCeilsToOneDecimal(t *testing. } got[req.GetMethodId()] = req.GetUsdToCurrencyRate() } - if got[810] != "3.9" || got[811] != "0.4" || got[2310] != "3.9" { + if got[810] != "3.9" || got[811] != "0.4" || got[2310] != "3.9" || got[2311] != "19066" { t.Fatalf("synced rates mismatch: %+v", got) } var response adminPaymentTestResponse if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil { t.Fatalf("decode response failed: %v", err) } - if response.Code != 0 || response.Data["updatedCount"].(float64) != 3 { + if response.Code != 0 || response.Data["updatedCount"].(float64) != 4 { t.Fatalf("sync response mismatch: %+v", response) } } +func TestApplyThirdPartyPaymentRateMarkupCeilsHighRateToInteger(t *testing.T) { + rate, ok := applyThirdPartyPaymentRateMarkup("18510.10000000", 0) + + if !ok || rate != "18511" { + t.Fatalf("IDR high rate must ceil to integer: rate=%q ok=%v", rate, ok) + } +} + func TestSyncThirdPartyPaymentRatesRejectsInvalidMarkupPercent(t *testing.T) { wallet := &mockPaymentWallet{} router := newPaymentHandlerTestRouter(New(wallet, nil, nil, nil, nil))