feat: add configurable payment amount precision

- add system.amount_precision setting with 2-6 validation
This commit is contained in:
line-6000
2026-05-11 17:11:43 +08:00
parent 1c805ed061
commit 8d2ddbd045
178 changed files with 532 additions and 273 deletions
+4 -3
View File
@@ -86,7 +86,7 @@ func createOkPayDepositOrder(uniqueID string, amount float64, coin string, retur
form := map[string]string{
"unique_id": uniqueID,
"name": uniqueID,
"amount": fmt.Sprintf("%.2f", amount),
"amount": fmt.Sprintf("%.*f", data.GetAmountPrecision(), amount),
"coin": strings.ToUpper(strings.TrimSpace(coin)),
"callback_url": callbackURL,
"return_url": returnURL,
@@ -254,8 +254,9 @@ func HandleOkPayNotify(form map[string]string, rawFormData string) error {
if err != nil {
return fmt.Errorf("invalid okpay amount: %w", err)
}
if fmt.Sprintf("%.2f", notifyAmount) != fmt.Sprintf("%.2f", order.ActualAmount) {
return fmt.Errorf("okpay amount mismatch: got=%.2f want=%.2f", notifyAmount, order.ActualAmount)
precision := data.GetAmountPrecision()
if fmt.Sprintf("%.*f", precision, notifyAmount) != fmt.Sprintf("%.*f", precision, order.ActualAmount) {
return fmt.Errorf("okpay amount mismatch: got=%.*f want=%.*f", precision, notifyAmount, precision, order.ActualAmount)
}
err = OrderProcessing(&request.OrderProcessingRequest{
+9 -8
View File
@@ -24,7 +24,6 @@ import (
const (
CnyMinimumPaymentAmount = 0.01
UsdtMinimumPaymentAmount = 0.01
UsdtAmountPerIncrement = 0.01
IncrementalMaximumNumber = 100
)
@@ -61,7 +60,8 @@ func CreateTransaction(req *request.CreateTransactionRequest, apiKey *mdb.ApiKey
token := strings.ToUpper(strings.TrimSpace(req.Token))
currency := strings.ToUpper(strings.TrimSpace(req.Currency))
network := strings.ToLower(strings.TrimSpace(req.Network))
payAmount := math.MustParsePrecFloat64(req.Amount, 2)
amountPrecision := data.GetAmountPrecision()
payAmount := math.MustParsePrecFloat64(req.Amount, amountPrecision)
rate := config.GetRateForCoin(strings.ToLower(token), strings.ToLower(currency))
if rate <= 0 {
return nil, constant.RateAmountErr
@@ -96,7 +96,7 @@ func CreateTransaction(req *request.CreateTransactionRequest, apiKey *mdb.ApiKey
}
tradeID := GenerateCode()
amount := math.MustParsePrecFloat64(decimalTokenAmount.InexactFloat64(), 2)
amount := math.MustParsePrecFloat64(decimalTokenAmount.InexactFloat64(), amountPrecision)
availableAddress, availableAmount, err := ReserveAvailableWalletAndAmount(tradeID, network, token, amount, walletAddress)
if err != nil {
return nil, err
@@ -109,7 +109,7 @@ func CreateTransaction(req *request.CreateTransactionRequest, apiKey *mdb.ApiKey
order := &mdb.Orders{
TradeId: tradeID,
OrderId: req.OrderId,
Amount: req.Amount,
Amount: payAmount,
Currency: currency,
ActualAmount: availableAmount,
ReceiveAddress: availableAddress,
@@ -280,6 +280,7 @@ func OrderProcessing(req *request.OrderProcessingRequest) error {
func ReserveAvailableWalletAndAmount(tradeID string, network string, token string, amount float64, walletAddress []mdb.WalletAddress) (string, float64, error) {
availableAddress := ""
availableAmount := amount
amountPrecision := data.GetAmountPrecision()
tryLockWalletFunc := func(targetAmount float64) (string, error) {
for _, address := range walletAddress {
@@ -303,8 +304,8 @@ func ReserveAvailableWalletAndAmount(tradeID string, network string, token strin
}
if address == "" {
decimalOldAmount := decimal.NewFromFloat(availableAmount)
decimalIncr := decimal.NewFromFloat(UsdtAmountPerIncrement)
availableAmount = decimalOldAmount.Add(decimalIncr).InexactFloat64()
decimalIncr := decimal.New(1, int32(-amountPrecision))
availableAmount = math.MustParsePrecFloat64(decimalOldAmount.Add(decimalIncr).InexactFloat64(), amountPrecision)
continue
}
availableAddress = address
@@ -414,7 +415,7 @@ func SwitchNetwork(req *request.SwitchNetworkRequest) (*response.CheckoutCounter
}
subTradeID := GenerateCode()
amount := math.MustParsePrecFloat64(decimalTokenAmount.InexactFloat64(), 2)
amount := math.MustParsePrecFloat64(decimalTokenAmount.InexactFloat64(), data.GetAmountPrecision())
availableAddress, availableAmount, err := ReserveAvailableWalletAndAmount(subTradeID, network, token, amount, walletAddress)
if err != nil {
return nil, err
@@ -531,7 +532,7 @@ func switchToOkPay(parent *mdb.Orders, token string) (*response.CheckoutCounterR
}
subTradeID := GenerateCode()
amount := math.MustParsePrecFloat64(decimalTokenAmount.InexactFloat64(), 2)
amount := math.MustParsePrecFloat64(decimalTokenAmount.InexactFloat64(), data.GetAmountPrecision())
returnURL := strings.TrimSpace(parent.RedirectUrl)
if returnURL == "" {
returnURL = data.GetOkPayReturnURL()
+53
View File
@@ -97,6 +97,59 @@ func TestCreateTransactionAssignsIncrementedAmountsAndLocks(t *testing.T) {
}
}
func TestCreateTransactionUsesConfiguredAmountPrecision(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
if err := data.SetSetting(mdb.SettingGroupSystem, mdb.SettingKeyAmountPrecision, "4", mdb.SettingTypeInt); err != nil {
t.Fatalf("set amount precision: %v", err)
}
if _, err := data.AddWalletAddress("wallet_precision_1"); err != nil {
t.Fatalf("add wallet: %v", err)
}
resp1, err := CreateTransaction(newCreateTransactionRequest("order_precision_1", 1), nil)
if err != nil {
t.Fatalf("create first transaction: %v", err)
}
resp2, err := CreateTransaction(newCreateTransactionRequest("order_precision_2", 1), nil)
if err != nil {
t.Fatalf("create second transaction: %v", err)
}
if got := fmt.Sprintf("%.4f", resp1.ActualAmount); got != "1.0000" {
t.Fatalf("first actual amount = %s, want 1.0000", got)
}
if got := fmt.Sprintf("%.4f", resp2.ActualAmount); got != "1.0001" {
t.Fatalf("second actual amount = %s, want 1.0001", got)
}
}
func TestCreateTransactionStoresNormalizedMerchantAmount(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
if _, err := data.AddWalletAddress("wallet_normalized_1"); err != nil {
t.Fatalf("add wallet: %v", err)
}
resp, err := CreateTransaction(newCreateTransactionRequest("order_normalized_1", 100.129), nil)
if err != nil {
t.Fatalf("create transaction: %v", err)
}
if got := fmt.Sprintf("%.2f", resp.Amount); got != "100.13" {
t.Fatalf("response amount = %s, want 100.13", got)
}
order, err := data.GetOrderInfoByTradeId(resp.TradeId)
if err != nil {
t.Fatalf("load order: %v", err)
}
if got := fmt.Sprintf("%.2f", order.Amount); got != "100.13" {
t.Fatalf("stored amount = %s, want 100.13", got)
}
}
func TestCreateTransactionUsesRateAPIWhenForcedSettingIsNotPositive(t *testing.T) {
cleanup := testutil.SetupTestDatabases(t)
defer cleanup()
+2 -3
View File
@@ -471,7 +471,7 @@ const (
Token2022ProgramID = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
)
// ADJustAmount 将链上原始金额转为可读金额(除以 10^decimals,保留 2 位小数
// ADJustAmount 将链上原始金额转为可读金额(除以 10^decimals,保留到系统支持的最大匹配精度
func ADJustAmount(amount uint64, decimals int) float64 {
if amount == 0 {
return 0
@@ -480,8 +480,7 @@ func ADJustAmount(amount uint64, decimals int) float64 {
// 10^decimals
decimalDivisor := decimal.New(1, int32(decimals))
adjustedAmount := decimalAmount.Div(decimalDivisor)
// Round to 2 decimal places
return math.MustParsePrecFloat64(adjustedAmount.InexactFloat64(), 2)
return math.MustParsePrecFloat64(adjustedAmount.InexactFloat64(), data.MaxAmountPrecision)
}
func MatchUsdtAtaAddress(address string, ataTo string) bool {
+2 -2
View File
@@ -236,7 +236,7 @@ func TestAdjustAmount(t *testing.T) {
name: "USDT amount (6 decimals)",
amount: 123456789,
decimals: 6,
want: 123.46,
want: 123.456789,
},
{
name: "USDC amount (6 decimals)",
@@ -260,7 +260,7 @@ func TestAdjustAmount(t *testing.T) {
name: "Small amount",
amount: 1,
decimals: 6,
want: 0.0,
want: 0.000001,
},
}
+7 -5
View File
@@ -54,7 +54,7 @@ func TryProcessTronTRC20Transfer(token mdb.ChainToken, toAddr string, rawValue *
}
decimalQuant := decimal.NewFromBigInt(rawValue, 0)
amount := math.MustParsePrecFloat64(decimalQuant.Div(decimal.New(1, int32(decimals))).InexactFloat64(), 2)
amount := math.MustParsePrecFloat64(decimalQuant.Div(decimal.New(1, int32(decimals))).InexactFloat64(), data.MaxAmountPrecision)
if amount <= 0 {
return
}
@@ -126,7 +126,7 @@ func TryProcessTronTRXTransfer(toAddr string, rawSun int64, txHash string, block
}
decimalQuant := decimal.NewFromInt(rawSun)
amount := math.MustParsePrecFloat64(decimalQuant.Div(decimal.NewFromInt(1_000_000)).InexactFloat64(), 2)
amount := math.MustParsePrecFloat64(decimalQuant.Div(decimal.NewFromInt(1_000_000)).InexactFloat64(), data.MaxAmountPrecision)
if amount <= 0 {
return
}
@@ -222,7 +222,7 @@ func TryProcessEvmERC20Transfer(chainNetwork string, contract common.Address, to
pow := decimal.New(1, int32(decimals))
decimalQuant := decimal.NewFromBigInt(rawValue, 0)
amount := math.MustParsePrecFloat64(decimalQuant.Div(pow).InexactFloat64(), 2)
amount := math.MustParsePrecFloat64(decimalQuant.Div(pow).InexactFloat64(), data.MaxAmountPrecision)
if amount <= 0 {
log.Sugar.Warnf("[%s-%s][%s] skip non-positive amount %.2f", net, tokenSym, walletAddr, amount)
return
@@ -293,11 +293,13 @@ func sendPaymentNotification(order *mdb.Orders) {
}
}
precision := data.GetAmountPrecision()
amountFormat := fmt.Sprintf("%%.%df", precision)
msg := fmt.Sprintf(
"🎉 <b>收款成功通知</b>\n\n"+
"💰 <b>金额信息</b>\n"+
"├ 订单金额:<code>%.2f %s</code>\n"+
"└ 实际到账:<code>%.2f %s</code>\n\n"+
"├ 订单金额:<code>"+amountFormat+" %s</code>\n"+
"└ 实际到账:<code>"+amountFormat+" %s</code>\n\n"+
"📋 <b>订单信息</b>\n"+
"├ 交易号:<code>%s</code>\n"+
"├ 订单号:<code>%s</code>\n"+