代码 支付金额单位踩坑记录:微信传分、Stripe 传 minor unit、支付宝传字符串元

2026-09-17 09:00:59

支付金额单位踩坑记录:微信传分、Stripe 传 minor unit、支付宝传字符串元

同一个订单,要在微信、支付宝、Stripe、USDT 四条通道上分别下单,如果金额单位口径没对齐,最常见的后果是金额差 100 倍——少收 99%,或者多收 99 倍。更麻烦的是,这种错误往往不在下单时暴露,而是等对账那天才发现账不平。

下面按通道把单位规则捋一遍,最后说工程侧怎么存、怎么查。

Stripe:amount 是货币最小单位,不是元

文档事实(https://docs.stripe.com/currencies):Stripe 所有 API 请求里的 amount 都使用该货币的最小单位(currency's minor unit),不带小数点。

默认两位小数的币种,收 10 USD 要传:

amount=1000, currency=usd

而同样是「10」这个面值,日元要传:

amount=10, currency=jpy

因为 JPY 属于 zero-decimal 币种,amount 和面值 1:1,不乘 100。文档给出的 zero-decimal 清单是:

BIF, CLP, DJF, GNF, JPY, KMF, KRW, MGA, PYG, RWF, UGX, VND, VUV, XAF, XOF, XPF

也就是说,接口不认「元」,只认最小单位;到底乘不乘 100,取决于币种。写死一个 /100*100 的工具函数,在遇到 JPY、KRW 时必然出错。

文档还列了几个特殊币种,这几个是最容易被统一换算逻辑坑到的:

  • ISK:已转为 zero-decimal,但向后兼容要求用两位小数表示,且小数位恒为 00。收 5 ISK 传 500,不能带小数。
  • HUF:充值时两位小数可以接受;但手动 payout 必须传能被 100 整除的整数。账户余额是 HUF 10.45 时,只能 payout HUF 10(传 1000),传 1045 不行。
  • TWD:同 HUF,payout 金额必须能被 100 整除。
  • UGX:向后兼容两位小数,小数位恒为 00,收 5 UGX 传 500;发票上出现小数会四舍五入到最近的 100 倍数,差额计入 customer balance。

最小收款金额按结算币种区分,文档举的例子包括 0.50 USD、0.30 GBP、50 JPY、50 KRW、4.00 HKD、10 MXN、175.00 HUF。

最大值方面,卡支付多数币种上限 12 位(999999999999 minor units),Amex 在多数币种是 9 位;非卡支付 IDR 12 位、COP 10 位、INR 9 位,其它币种 8 位(99999999,即 999,999.99)。日本 JCB / Diners / Discover 上限是 8 位,99,999,999 JPY

失败时的表现:金额传错单位一般不会在下单时报错,而是按你传的 minor unit 原样计入。把 10 USD 写成 amount=10,实收 0.10 USD;把 10 JPY 按两位小数写成 amount=1000,实收 1000 JPY。HUF/TWD 的 payout 传了不能被 100 整除的数,则会直接失败。

不适用场景:上面的 HUF、TWD 整除规则只针对 payout,不是 charge;UGX 的小数舍入只发生在发票上,不是所有 charge 都走这个逻辑。

微信支付 v3:amount.total 单位是分

文档事实:微信支付 v3 的 amount.total 单位是「分」,整数,金额字段类型为 int。退款接口里 amount.refundamount.total 同样是分。

所以一笔 10.00 元的订单:

"amount": { "total": 1000, "currency": "CNY" }

失败时的表现:如果把「元」的字符串直接塞进 total,或者写成 10.00 这种带小数的值,接口会在参数校验阶段就拒绝。这里反而比 Stripe「安全」一点——金额字段是整数类型,类型不对下单就失败,不会静默按错误金额成交。

适用边界:这条只覆盖 v3 的金额字段。老版本 v2 的 XML 接口字段名和约定不同,不要混用。

支付宝:total_amount 是「元」的字符串

文档事实:支付宝交易接口的 total_amount 单位是「元」,类型是字符串,两位小数,例如 "10.00"。接口要求字符串而不是数值。

total_amount="10.00"

这就和微信正好相反:微信给分(整数),支付宝给元(两位小数字符串)。两侧共用一套「金额换算」代码时,最容易在这里翻车。

失败时的表现:把 1000 分直接传给 total_amount,要么因格式校验失败,要么被当成 1000.00 元收走——1000 倍。反过来把 "10.00" 传给微信的 total,是参数错误。

经验判断(未实测):实践中更稳的做法是内部统一用最小单位整数运算,只在拼装各通道请求体的最后一步做一次显式转换,转换函数按通道分别命名,不要用一个通用的 toYuan() 到处调。

USDT TRC20:6 位小数,广播必须用最小单位整数

文档事实:TRC20 合约精度是 6 位小数,最小单位 1e-6,即 1 USDT = 1000000。广播交易时金额必须用最小单位整数。

转 1.5 USDT:

1500000

失败时的表现:传 1.5 这类浮点数通常会在构造交易时被拒绝,或者被截断成错误金额。和法币通道不同,链上交易一旦广播确认,几乎没有回滚余地。

不适用场景:上面是 TRC20 的 USDT 合约精度。别的链、别的代币精度不一样,USDT 在 ERC20 上也是 6 位,但不要把这条当成通用规则套到所有代币上。

工程侧:金额怎么存、怎么算

以下是经验判断(未实测),不是某家文档的规定:

  1. 数据库存整数最小单位(分 / 各币种最小单位),或者用 DECIMAL。绝不用 float / double。多币种系统里,整数列旁边必须带一个 currency 列,否则一个 1000 你无法判断是 10.00 元还是 1000 日元。
  2. JS / Node 里的 0.1 + 0.2 问题在金额场景是真实的。建议后端算、后端存,前端只做展示格式化,不要在前端做金额加减后再回传。
  3. 多币种先查 decimals。写一个 currencyDecimals 映射(0 / 2 / 6…),所有乘除都走它,不要散落硬编码的 *100。zero-decimal 币种在这个映射里必须是 0。
  4. 对账、退款、分账必须用同一单位口径。下单用分、退款用元、对账又按元比对,金额一定对不平。退款单位错会导致少退或多退,且退款往往已经出账,只能人工补。
  5. 日志里打印原始 amount 值和 currency。打印换算后的展示值没用,排查时你要看的是请求体里那个原始整数。

排查顺序

接口报参数错误时(比如 amount 不是整数、超出最小金额、负数),第一步先核对币种 decimals,确认这个币种是 0 位还是 2 位;第二步确认通道要求的是分、最小单位还是元字符串。

金额差正好 100 倍的,基本可以判定是分/元混用或者 zero-decimal 币种被多乘了一次 100。差 10 倍、1000 倍的,检查是不是在某一层做了两次乘除。

未实测部分:上文 Stripe 的特殊币种规则(ISK、HUF、TWD、UGX)以官方文档为准,未在真实账户逐一验证;最小金额和最大金额会随地区、支付方式变化,实际以自己账户的返回为准。微信、支付宝的字段类型和单位同样以各自最新版接口文档为准,接入前先拿小额订单跑通全链路,包括退款和对账。

推荐文章

程序员茄子在线接单