Files
sub2api/backend/internal/payment/currency.go
T
李建琦 6d655c9903
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
2026-08-21 18:30:13 +08:00

119 lines
3.8 KiB
Go

package payment
import (
"fmt"
"strings"
"github.com/shopspring/decimal"
)
const DefaultPaymentCurrency = "CNY"
type paymentCurrencyAmountUnit struct {
apiMinorUnit int
maxFractionDigits int
}
var (
zeroDecimalAmountUnit = paymentCurrencyAmountUnit{apiMinorUnit: 0, maxFractionDigits: 0}
twoDecimalAmountUnit = paymentCurrencyAmountUnit{apiMinorUnit: 2, maxFractionDigits: 2}
threeDecimalAmountUnit = paymentCurrencyAmountUnit{apiMinorUnit: 3, maxFractionDigits: 3}
stripeLegacyZeroAmount = paymentCurrencyAmountUnit{apiMinorUnit: 2, maxFractionDigits: 0}
)
var paymentCurrencyAmountUnits = map[string]paymentCurrencyAmountUnit{
"BIF": zeroDecimalAmountUnit,
"CLP": zeroDecimalAmountUnit,
"DJF": zeroDecimalAmountUnit,
"GNF": zeroDecimalAmountUnit,
"JPY": zeroDecimalAmountUnit,
"KMF": zeroDecimalAmountUnit,
"KRW": zeroDecimalAmountUnit,
"MGA": zeroDecimalAmountUnit,
"PYG": zeroDecimalAmountUnit,
"RWF": zeroDecimalAmountUnit,
"VND": zeroDecimalAmountUnit,
"VUV": zeroDecimalAmountUnit,
"XAF": zeroDecimalAmountUnit,
"XOF": zeroDecimalAmountUnit,
"XPF": zeroDecimalAmountUnit,
"ISK": stripeLegacyZeroAmount,
"UGX": stripeLegacyZeroAmount,
"BHD": threeDecimalAmountUnit,
"IQD": threeDecimalAmountUnit,
"JOD": threeDecimalAmountUnit,
"KWD": threeDecimalAmountUnit,
"LYD": threeDecimalAmountUnit,
"OMR": threeDecimalAmountUnit,
"TND": threeDecimalAmountUnit,
}
func NormalizePaymentCurrency(raw string) (string, error) {
currency := strings.ToUpper(strings.TrimSpace(raw))
if currency == "" {
return DefaultPaymentCurrency, nil
}
if len(currency) != 3 {
return "", fmt.Errorf("payment currency must be a 3-letter ISO currency code")
}
for _, ch := range currency {
if ch < 'A' || ch > 'Z' {
return "", fmt.Errorf("payment currency must be a 3-letter ISO currency code")
}
}
return currency, nil
}
func CurrencyMinorUnit(currency string) int {
return paymentCurrencyAmountUnitFor(currency).apiMinorUnit
}
// CurrencyMaxFractionDigits 返回支付金额允许展示和输入的小数位数。
func CurrencyMaxFractionDigits(currency string) int {
return paymentCurrencyAmountUnitFor(currency).maxFractionDigits
}
// FormatAmountForCurrency 按币种允许的小数位格式化支付金额。
func FormatAmountForCurrency(amount float64, currency string) string {
return decimal.NewFromFloat(amount).StringFixed(int32(CurrencyMaxFractionDigits(currency)))
}
func paymentCurrencyAmountUnitFor(currency string) paymentCurrencyAmountUnit {
normalized, err := NormalizePaymentCurrency(currency)
if err != nil {
return twoDecimalAmountUnit
}
if amountUnit, ok := paymentCurrencyAmountUnits[normalized]; ok {
return amountUnit
}
return twoDecimalAmountUnit
}
func AmountToMinorUnit(amountStr, currency string) (int64, error) {
d, err := decimal.NewFromString(strings.TrimSpace(amountStr))
if err != nil {
return 0, fmt.Errorf("invalid amount: %s", amountStr)
}
normalizedCurrency, err := NormalizePaymentCurrency(currency)
if err != nil {
return 0, err
}
amountUnit := paymentCurrencyAmountUnitFor(normalizedCurrency)
precisionFactor := decimal.New(1, int32(amountUnit.maxFractionDigits))
scaledForPrecision := d.Mul(precisionFactor)
if !scaledForPrecision.Equal(scaledForPrecision.Truncate(0)) {
if amountUnit.maxFractionDigits == 0 {
return 0, fmt.Errorf("payment amount for %s must be a whole number", normalizedCurrency)
}
return 0, fmt.Errorf("payment amount for %s must not have more than %d decimal places", normalizedCurrency, amountUnit.maxFractionDigits)
}
factor := decimal.New(1, int32(amountUnit.apiMinorUnit))
minorAmount := d.Mul(factor)
return minorAmount.IntPart(), nil
}
func MinorUnitToAmount(value int64, currency string) float64 {
factor := decimal.New(1, int32(CurrencyMinorUnit(currency)))
return decimal.NewFromInt(value).Div(factor).InexactFloat64()
}