本文是【金融科技工程】系列第 08 篇。上一篇《卡组织收单链路》讲了银联、Visa、Mastercard 与 ISO 8583/20022 的报文世界;在中国,真正跑在用户手机里的 C 端钱包是支付宝与微信支付。本文聚焦”接入”本身:从商户进件、签名体系、支付场景,到预授权、分账、退款、对账、异步通知、沙箱与灰度,最后落到服务商模式下最容易踩的二清红线。
一、读者画像与本文边界
本文预设读者画像是中小型平台或 SaaS 的后端工程师 / 架构师:公司已经(或即将)提供 C 端收款能力,需要把支付宝和微信支付接入到自己的交易系统里。如果你是:
- 一家电商、外卖、票务、SaaS 工具的后端,要做”微信小程序下单 + 支付宝 App 支付”的收银台;
- 一家物业、停车、连锁零售的聚合收银,要在多门店、多主体下分账、对账;
- 一家共享充电宝、共享汽车、酒店集团,要玩预授权冻结押金;
- 一家 ISV / 支付服务商,要替一堆小微商户开户、代理进件、代收代付;
那么这篇文章为你写的。
本文不覆盖的内容:
- 支付宝 / 微信支付的 C 端风控、反洗钱、支付密码体系——见第 20、21 篇;
- 跨境支付(Alipay+、WeChat Pay HK、境外收单)——见第 13 篇;
- 聚合支付网关的路由、限流、补单——见第 09 篇;本文只讲单通道接入的细节。
本文用到的术语与缩写在首次出现时给出中英对照,例如:开放平台(Open Platform)、服务商(Independent Software Vendor,ISV)、特约商户(Sub-Merchant)、商户号(Merchant ID,mch_id)、应用标识(AppID)、预授权(Pre-Authorization)、分账(Profit Sharing)、对账单(Reconciliation Bill)。
二、两条接入路径:直连商户 vs 服务商模式
“接入支付宝 / 微信支付”听起来只有一种做法,其实有两条完全不同的路径,后续的签约主体、资金流向、合规边界都不同。
2.1 直连商户(Direct Merchant)
直连商户路径:
- 商户(营业执照持有人)直接在支付宝开放平台 / 微信支付商户平台注册;
- 签约主体是商户自己,结算银行卡也是商户对公账户;
- 每一笔交易的资金流是:用户钱包 → 支付宝/微信 → 商户对公账户(T+1 结算);
- 手续费从商户自己的签约费率里扣(典型 0.38%–0.6%,行业不同)。
这条路径适合自营业务:一家公司,一套营业执照,流水归自己。工程上最省事:一套密钥、一套 AppID、一套回调域名。
2.2 服务商模式(ISV / Partner Mode)
服务商模式路径:
- 你(平台方)先成为服务商(支付宝叫 ISV,微信叫服务商 / Partner),拿到服务商 AppID 和服务商商户号(sp_appid、sp_mchid);
- 服务商帮下面的特约商户(Sub-Merchant)代开户 / 进件:提交营业执照、法人身份证、门店照片、对公账户;
- 每个特约商户拿到自己的 sub_mchid(微信)或 pid(支付宝);
- 交易发起时带上服务商身份 + 特约商户身份,资金直接从钱包清算到特约商户对公账户(这是关键!),服务商只拿分润。
服务商模式适合:
- SaaS 收银:比如一个连锁餐饮 SaaS,背后几千家小商户;
- 平台类业务:美团、饿了么、滴滴这种;
- 聚合工具:收钱吧、哆啦宝、二维火等。
2.3 两条路的合规关键差异:二清
二清(Second Clearing)是中国人民银行禁止的:没有支付牌照的机构,不得从事资金归集后再向商户结算的活动。简单翻译:钱不能先进你(平台)的账户,再由你转给商户——除非你持牌。
这就决定了:
| 模式 | 钱走哪里 | 是否二清 | 代表玩法 |
|---|---|---|---|
| 直连 | 钱包 → 商户对公 | 否 | 自营电商 |
| 合规服务商 | 钱包 → 特约商户对公(平台收分润) | 否 | 美团、滴滴、收钱吧 |
| 违规”大商户” | 钱包 → 平台账户 → 再打给商户 | 是,违规 | 被监管点名的聚合支付 |
| 平台持牌 | 钱包 → 平台备付金 → 商户 | 否(因为有牌照) | 支付宝、微信自身、拉卡拉 |
所以工程上,服务商模式最核心的设计是:分账接收方、清算账户一定要是特约商户自己的对公账户,平台不碰资金本金,只分润。第 11 篇讲清结算时会再展开。
三、支付宝开放平台:应用、签名、能力
3.1 应用(Application)与 AppID
登录 Alipay Open Platform(open.alipay.com),创建一个”网页/移动应用”,获得 AppID(16 位数字)。所有后续调用都要带 AppID——它标识你这套业务,而非商户。
一个 AppID 可以签约多种能力(Capability):
- 当面付(面对面扫码)
- App 支付
- 电脑网站支付
- 手机网站支付
- 预授权
- 资金分账
- 花呗分期
每个能力必须单独签约,在”产品中心”申请开通,一般需要营业执照、行业资质。
3.2 RSA2 签名
支付宝 Open API 全部使用非对称签名:商户生成 RSA 2048 密钥对,把公钥上传到开放平台,平台下发”支付宝公钥”(平台自己的公钥)。
签名算法是 SHA256WithRSA(也叫 RSA2,老的
RSA1 用 SHA1,已经不允许新商户使用)。
请求签名流程:
- 收集所有业务参数 + 公共参数(app_id、method、charset、sign_type=RSA2、timestamp、version、biz_content 等);
- 按参数名字典序拼
k1=v1&k2=v2; - 用商户私钥
SHA256WithRSA签名,Base64 编码,作为sign参数; - 作为 form-urlencoded POST 到
https://openapi.alipay.com/gateway.do。
响应验签:
- 响应体是 JSON,形如
{"alipay_trade_create_response": {...}, "sign": "..."}; - 验签对象是
alipay_trade_create_response对应的 JSON 字符串原文(注意不是重新序列化,必须原始字节),用支付宝公钥验; - 这里有个经典坑:Java 官方 SDK 老版本会重新序列化,导致字段顺序变化,验签失败。一定要截取原文。
3.3 应用证书模式
2020 年后支付宝推应用证书 +
支付宝根证书模式(代替公钥模式),为了兼容”一个商户绑多个应用”,证书文件包含证书序列号(SN),签名时带上
app_cert_sn 和 alipay_root_cert_sn
两个参数。新接入必须走证书模式。
3.4 商户进件(服务商模式)
服务商模式下,你用间连商户进件
API(alipay.open.agent.create、alipay.open.agent.facetoface.sign
等)提交材料:
- 营业执照、法人身份证正反面、门店照片、收银台照片;
- 结算银行账户(对公 or 小微商户可以对私);
- 经营类目(MCC)、日峰值交易额、客单价。
提交后进入人工审核(1–3 工作日),通过后拿到
PID(Partner ID),后续交易带上
sys_service_provider_id(你的 ISV PID)+ 商户
PID,资金结算到商户。
3.5 常用 API 一览
| API | 用途 |
|---|---|
alipay.trade.create |
统一收单下单(扫码、JSAPI 场景) |
alipay.trade.precreate |
预下单生成二维码(C 扫 B) |
alipay.trade.pay |
条码支付(B 扫 C) |
alipay.trade.app.pay |
App 支付(原生 SDK 调起) |
alipay.trade.wap.pay |
H5 支付 |
alipay.trade.page.pay |
PC 网站支付 |
alipay.trade.query |
查询订单状态 |
alipay.trade.refund |
退款 |
alipay.trade.close |
关闭订单 |
alipay.fund.auth.order.freeze |
预授权冻结 |
alipay.fund.auth.order.unfreeze |
预授权解冻 |
alipay.trade.order.settle |
分账 |
alipay.data.dataservice.bill.downloadurl.query |
对账单下载 |
四、微信支付:V2、V3、mch_id、AppID 的交叉
微信支付这边是另一种复杂度:历史上存在 V2(pay.weixin.qq.com 的老 API)和 V3(api.mch.weixin.qq.com 的新 API)两套 API 并存,且支付场景与 AppID 类型绑死。
4.1 商户号(mch_id)与 AppID 的组合
微信支付的账户关系是一个让新接入者头痛的知识点:
- 商户号 mch_id:在 pay.weixin.qq.com 申请,10 位数字,绑定对公银行账户。
- AppID:在 mp.weixin.qq.com(公众号)、mp.weixin.qq.com(小程序)、open.weixin.qq.com(开放平台 / 移动应用 / 网站应用)申请,18 位字符串。
一个 mch_id 可以关联多个 AppID(在商户平台”产品中心 - AppID 账号管理”里绑定),但每种支付场景要求 AppID 类型固定:
| 场景 | AppID 类型 | 发起方 |
|---|---|---|
| JSAPI 支付(公众号 H5) | 公众号 AppID | 微信浏览器内 H5 |
| JSAPI 支付(小程序) | 小程序 AppID | 小程序 wx.requestPayment |
| App 支付 | 开放平台移动应用 AppID | 原生 App SDK |
| H5 支付(外部浏览器) | 公众号 AppID | 微信外的 H5 |
| Native 支付 | 公众号或开放平台 AppID | PC 扫码 |
| 付款码支付(B 扫 C) | 任意(以 mch_id 为主) | 收银机扫用户付款码 |
openid 是用户在某个 AppID 下的唯一标识;JSAPI、小程序下单必须传 openid,所以下单前必须先完成授权登录拿到 openid。
4.2 V2 vs V3
- V2:XML body,MD5 或 HMAC-SHA256 签名(基于 APIv2 密钥),证书挂在客户端做双向 TLS(退款、下载对账单要用)。
- V3:JSON body,基于商户 API 私钥 + 微信支付平台证书的 RSA-SHA256 签名,敏感字段(银行卡号、身份证号)要用平台证书的公钥加密传输。
V3 是官方主推,V2 仍可用但已进入维护期,新功能只上 V3(例如商家转账、合单支付、分账都基本 V3 优先)。实际工程中很多老系统仍是 V2,迁移 V3 是一个独立项目。
4.3 V3 签名构造
V3 请求签名算法:
signature = RSA-SHA256(
HTTP 方法 + "\n" +
URL 路径(含 query) + "\n" +
时间戳 + "\n" +
随机串 + "\n" +
请求体 + "\n",
商户私钥
)
HTTP 头:
Authorization: WECHATPAY2-SHA256-RSA2048 \
mchid="1900000001", \
nonce_str="593BEC0C930BF1AFEB40B4A08C8FB242", \
signature="...base64...", \
timestamp="1554208460", \
serial_no="商户证书序列号"
响应验签用微信支付平台证书(注意:不是微信证书中心的
HTTPS 证书,是从 /v3/certificates 接口下载、用
APIv3 密钥 AES-GCM
解密后拿到的证书)。平台证书会轮换,SDK
要能自动拉新(每天一次)并在多张证书间按
Wechatpay-Serial 头选。
4.4 APIv3 密钥与平台证书
- APIv3 密钥:32
字节字符串,商户平台后台自己设置。用途:解密回调通知和
/v3/certificates响应里的敏感字段(AES-256-GCM)。 - 商户 API 私钥:商户自己生成 RSA 2048
私钥,CSR 上传商户平台拿到”商户 API 证书”,里面的序列号即
serial_no。 - 微信支付平台证书:微信下发,用于验证响应签名和加密上行敏感字段。
四把密钥一把不能少:漏了 APIv3 密钥,回调通知里的用户信息就解不开;漏了平台证书,回调签名就没法验。
4.5 常用 V3 API
| API | 路径 |
|---|---|
| JSAPI 下单 | POST /v3/pay/transactions/jsapi |
| Native 下单 | POST /v3/pay/transactions/native |
| App 下单 | POST /v3/pay/transactions/app |
| H5 下单 | POST /v3/pay/transactions/h5 |
| 查询订单 | GET /v3/pay/transactions/id/{transaction_id} |
| 关闭订单 | POST /v3/pay/transactions/out-trade-no/{out_trade_no}/close |
| 退款申请 | POST /v3/refund/domestic/refunds |
| 退款查询 | GET /v3/refund/domestic/refunds/{out_refund_no} |
| 申请交易账单 | GET /v3/bill/tradebill |
| 申请资金账单 | GET /v3/bill/fundflowbill |
| 分账请求 | POST /v3/profitsharing/orders |
| 分账回退 | POST /v3/profitsharing/return-orders |
| 解冻剩余资金 | POST /v3/profitsharing/orders/unfreeze |
五、支付场景:扫码、JSAPI、App、H5、小程序、刷脸
5.1 C 扫 B vs B 扫 C
中国线下支付的术语容易绕:
- C 扫 B(User-presented → Merchant-scanned? 反了,重看):用户(Consumer)用钱包扫商户贴的码;叫”主扫”。对应 Native / Precreate,用户主动发起。
- B 扫
C:商户(Business)用扫码枪扫用户钱包里的付款码(18
位动态条码);叫”被扫”或”付款码”。对应
micropay/pay。
两者资金都是用户付给商户,差别是发起设备。便利店、餐厅多用 B 扫 C(快,不需要用户操作),停车场、自助售货多用 C 扫 B(设备不用装扫码头)。
5.2 JSAPI(公众号 / 小程序内支付)
在微信内部浏览器或小程序里调起:
- 后端调
POST /v3/pay/transactions/jsapi传openid+ 金额 +out_trade_no,拿到prepay_id; - 后端再对
prepay_id第二次签名,返回给前端{appId, timeStamp, nonceStr, package: "prepay_id=xxx", signType: "RSA", paySign}; - 前端
wx.requestPayment(params)(小程序)或WeixinJSBridge.invoke('getBrandWCPayRequest', params)(公众号)。
支付宝对应的是生活号 / 小程序
JSAPI,流程相似但用
alipay.trade.create + 前端
my.tradePay。
5.3 App 支付
原生 App 集成微信/支付宝 SDK。后端下单拿到支付参数,透传给 App SDK,SDK 调起钱包 App,完成后回到商户 App,并不以回调结果为准——必须以异步通知 / 主动查单为准。
5.4 H5 支付
在非微信、非支付宝的外部浏览器里支付,后端拿到一个跳转 URL,浏览器 302 到钱包唤起支付。H5 支付有Referer 防刷要求:商户域名必须在商户平台配置白名单,不然报 “mweb_url invalid”。
5.5 刷脸支付:蜻蜓、青蛙
支付宝”蜻蜓”、微信”青蛙”是刷脸支付硬件。工程上是
SDK(Android / 自研
Linux)跑在设备里,调用人脸识别云端,识别成功后用
face_id
替代付款码走正常支付。对接方主要是收银系统
ISV,本文不展开。
六、预授权:冻结、解冻、扣款
预授权是酒店、租车、共享充电宝等行业的刚需:用户下单时冻结一笔押金(不真实扣款),使用完毕后根据实际金额扣款或解冻。
6.1 支付宝预授权
两段式 API:
alipay.fund.auth.order.freeze // 冻结
alipay.fund.auth.order.unfreeze // 解冻(全部 or 部分)
alipay.trade.pay // 以冻结单 auth_no 转支付(从预授权转完成)
业务流程:
- 用户下单 → 调
alipay.fund.auth.order.freeze冻结 500 元,拿到auth_no; - 用户使用充电宝 6 小时 → 计费 18 元;
- 商户调
alipay.trade.pay传auth_no+ amount=18,从冻结额度里扣 18 元,剩余 482 元自动解冻; - 或者整单退单,调
alipay.fund.auth.order.unfreeze解冻 500 元。
支付宝的冻结最长180 天。
6.2 微信”担保交易”与预授权
微信支付没有通用的”预授权”对外开放(历史上做过,后来在部分行业定向开放),更多场景是走”担保交易”或订单确认收货模式。共享租赁行业一般通过”免密支付 / 分期免押”替代——用户授权一个免密协议,归还后再扣款。
如果你做酒店/租车,支付宝有现成预授权,微信要找行业方案(微信”租借服务”平台 + 芝麻信用免押的组合变种)。
6.3 状态机
stateDiagram-v2
[*] --> 冻结中: freeze 成功
冻结中 --> 部分扣款: pay(auth_no, 部分)
冻结中 --> 全额解冻: unfreeze(全部)
冻结中 --> 全额扣款: pay(auth_no, 全部)
部分扣款 --> 剩余解冻: 自动
部分扣款 --> 追加扣款: pay(auth_no, 再扣)
全额解冻 --> [*]
全额扣款 --> [*]
剩余解冻 --> [*]
工程提醒:冻结额度和可扣款次数在不同版本 API 下规则不同(早期只允许扣一次,现在支持多次扣款直到额度用完)。
七、分账:合单、接收方、T+1 与解冻
平台业务最常见需求:用户付 100 元,平台分 20 元,商户得 80 元,快递员得 5 元,怎么做?答案:分账(Profit Sharing),而不是自己”先收后付”(二清)。
7.1 分账接收方绑定
不管支付宝还是微信支付,分账前都要:
- 开通分账产品:服务商或商户申请分账能力;
- 添加分账接收方:接收方必须是同一家支付机构下的个人钱包或商户号;微信
V3 用
POST /v3/profitsharing/receivers/add,支付宝用alipay.trade.royalty.relation.bind。 - 接收方可以是个人(需要用户授权,支付宝点同意、微信关注某公众号)或商户。
7.2 分账时机
- 订单支付成功后 → 订单默认处于”分账冻结”状态,最长可冻结到180 天(微信)/ 180 天(支付宝);
- 下单时在订单中声明”需分账”(
profit_sharing=true); - 支付成功后,调分账 API 把钱按比例分给接收方;
- 剩余未分的钱调解冻接口,退回商户余额。
7.3 微信分账 V3 示例请求体
{
"transaction_id": "4208450740201411110007820472",
"out_order_no": "P20260422001",
"receivers": [
{
"type": "MERCHANT_ID",
"account": "190001735",
"amount": 2000,
"description": "平台服务费"
},
{
"type": "PERSONAL_OPENID",
"account": "86693952",
"amount": 500,
"description": "配送员分润"
}
],
"unfreeze_unsplit": true
}unfreeze_unsplit=true
表示分完剩下的直接解冻到原商户。否则需要再调一次
orders/unfreeze。
7.4 分账回退
分账完成后的回退(Return)只能来自已分账的接收方,且只能退回到原支付商户。回退限额 = 已分金额;超过就报错。退款和回退是两码事:
- 退款:全部资金退用户钱包;退款会自动按比例从分账接收方追回(如果接收方余额不足,退款会失败)。
- 回退:只是接收方把钱退回商户,不退用户。
7.5 合单支付
“合单”指一次支付金额分属多个子单,每个子单可以走不同商户。微信提供
combine
合单支付(POST /v3/combine-transactions/jsapi
等),典型场景是商城一单买 A、B
两家店的商品。合单后分账各自独立。
八、退款:全额、部分、跨年、挂起
8.1 退款基本规则
- 退款不收手续费(但原订单手续费不退还);
- 退款按原路返回:用户怎么付的就怎么退(储蓄卡 → 储蓄卡,信用卡 → 信用卡,零钱 → 零钱);
- 商户侧必须账户余额 ≥ 退款金额,否则退款挂起;
- 每笔订单可以多次部分退款,累计不得超过原金额。
8.2 跨年退款
支付宝、微信支付对”跨年退款”有特殊规则:超过一年的订单退款成功率会下降,部分场景(银行侧已销账)需要走线下退款(客服介入)。工程上对老订单退款要加重试 + 工单兜底。
8.3 退款异常与挂起
退款可能进入异常状态:
| 状态 | 原因 | 处理 |
|---|---|---|
| PROCESSING | 正在处理 | 10 分钟后查单 |
| SUCCESS | 成功 | 入账完成 |
| CLOSED | 关闭 | 重新发起 |
| ABNORMAL | 异常(银行退回失败等) | 商户平台人工处理,一般退到”不可用余额” |
异常退款通常能在商户平台操作”退到余额”或”线下打款”,代码里一定要有兜底:每天扫描异常退款,推人工工单。
8.4 退款与分账的交互
前文提过:退款时会按比例追回分账。流程是:
- 商户发起退款;
- 微信/支付宝检查分账接收方余额;
- 按已分账金额 / 订单金额 的比例从接收方余额扣回;
- 任何接收方余额不足 → 整笔退款失败,进入 ABNORMAL;
- 余额充足 → 退款成功,接收方账单里出现”分账回退”记录。
这就是为什么平台分到的钱不能立刻提现——必须保留至少 180 天余额以应对退款。
九、异步通知工程:签名、防重放、幂等、补单
支付结果以异步通知为准(不是前端的”支付成功”页面,也不是 SDK 回调)。工程上处理异步通知的四个核心点:签名校验、防重放、幂等、补单。
9.1 签名校验
- 支付宝:把 POST 来的 form-urlencoded 参数(剔除
sign、sign_type)按字典序拼起来,用支付宝公钥验sign。 - 微信 V3:通知是 JSON body,HTTP 头带
Wechatpay-Signature、Wechatpay-Serial、Wechatpay-Timestamp、Wechatpay-Nonce,按timestamp\nnonce\nbody\n组合,用对应序列号的平台证书公钥验签。
验签失败一律丢弃,千万不要返回 200——不然攻击者会尝试伪造。
9.2 防重放
通知可能被中间人捕获后重放:
- 校验
timestamp与当前时间差 ≤ 5 分钟(微信要求); - 校验
nonce_str/out_trade_no是否已处理过(用 Redis SET NX,TTL 10 分钟); - 对回调数据
transaction_id+trade_state做幂等键。
9.3 幂等
异步通知至少会收到一次,可能多次(超时重推)。处理逻辑:
BEGIN;
SELECT * FROM payment WHERE out_trade_no = ? FOR UPDATE;
IF status IN ('PAID', 'REFUNDED') THEN
COMMIT; RETURN 200; // 幂等忽略
END IF;
IF status = 'PENDING' THEN
UPDATE payment SET status='PAID', paid_at=now(),
channel_trade_no=? WHERE out_trade_no=? AND status='PENDING';
// 写账务分录(见第 3 篇复式记账)
END IF;
COMMIT;
RETURN 200 "SUCCESS";
锁的是
out_trade_no,不是业务订单号,因为同一业务订单可能因关单后重下导致多个
out_trade_no。
9.4 通知重试策略
两家钱包的重试间隔都是指数退避:
- 支付宝:
4m, 10m, 10m, 1h, 2h, 6h, 15h,最多 8 次,总 ~25 小时; - 微信:
15s, 15s, 30s, 3m, 10m, 20m, 30m, 30m, 30m, 60m, 3h, 3h, 3h, 6h, 6h,最多 15 次,总 ~24 小时。
收到通知后必须返回约定的成功响应(支付宝返回文本
success,微信 V3 返回
{"code":"SUCCESS"} 并 200
状态码),任何非约定返回都会被当作失败并重推。
9.5 补单:主动查单
通知可能永久丢失(商户回调服务器宕机超过 25 小时)。不能依赖通知,必须有主动查单兜底:
- 创建订单 30 秒后起,定时调
trade.query//v3/pay/transactions/out-trade-no/{no}; - 指数退避:30s、60s、2m、5m、10m、30m……最长查 24 小时;
- 查到成功 → 走和通知相同的处理逻辑(同一幂等键,自然去重);
- 24 小时仍未成功 →
关单(
trade.close)并退给用户。
十、对账单:业务账单、资金账单、三方对账
10.1 两类账单
两家钱包都提供两类账单:
| 账单类型 | 内容 | 用途 |
|---|---|---|
| 交易/业务账单 | 当天所有订单(含成功、已退款、已关闭),每笔一行 | 对商户自己的订单表 |
| 资金账单 | 当天所有资金变动(收款、退款、提现、手续费),每笔一行 | 对商户对公账户流水 |
交易账单的金额 ≠ 资金账单的金额——因为有手续费、退款跨天、分账等差异。正确对账方式是:
- 先用交易账单对商户自己的订单表(确认哪些交易成功);
- 再用资金账单对银行对公账户流水(确认钱真的到了);
- 两张账单交叉(资金账单上每笔收款的
trade_no应能在交易账单找到对应订单)。
这叫三方对账(Three-way Reconciliation):订单表 ↔︎ 通道账单 ↔︎ 银行流水,第 23 篇会深入。
10.2 下载接口
- 支付宝:
alipay.data.dataservice.bill.downloadurl.query,返回一个30 分钟有效期的临时 URL,下载的是CSV 压缩包(.csv.zip); - 微信
V3:
GET /v3/bill/tradebill?bill_date=2026-04-21,返回 download_url + hash;再 GET 下载,得到 gzip 的 CSV;敏感字段(卡号 4 后缀)平台已脱敏。
10.3 入库与比对
典型 ETL 流水线:
01:00 下载 T-1 交易账单(支付宝、微信各一份)
01:10 解析 CSV → 插入 bill_raw 表
01:20 转换 → bill_normalized 表(统一 schema)
01:30 与 platform.orders 表 LEFT JOIN 跑 SQL 差异
01:40 差异入 diff_pending 表 + 邮件/告警
08:30 人工清差
常见差异类型:
- 长款:账单有,订单表没有 → 可能被刷单 / 接收异步通知失败;
- 短款:订单表成功,账单没有 → 可能实际未到账,或账单延迟;
- 金额不一致:多为退款跨日导致的汇总差;
- 手续费差:费率计算差 1 分,来自四舍五入规则不同。
10.4 账单下载时机
- 支付宝账单 T+1 早上 09:00 后可下;
- 微信账单 T+1 早上 09:00 后可下;
- 月账单月初 3 日后可下。
不要在 09:00 前重试——只会拿到”账单未生成”。
十一、沙箱与灰度
11.1 沙箱
- 支付宝沙箱
openapi.alipaydev.com:有独立的 AppID、测试买家账号、测试金额;能力有限,不能测分账、预授权某些组合; - 微信支付沙箱:历史上有专门的沙箱环境(
api.mch.weixin.qq.com/sandboxnew/),V3 基本没有全功能沙箱,最好用真实商户小额金额(最低 0.01 元)灰度。
沙箱的限制决定了你必须做真实小额灰度:
- 上线前内部员工账号各刷一笔 0.01 元,完成一笔 0.01 元退款;
- 分账场景:各接收方各分 0.01 元并回退;
- 预授权:冻结 0.02 元扣 0.01 元解冻 0.01 元。
11.2 灰度
新通道、新版本上线用按用户的灰度:hash(user_id) % 100 < k,k
从 1 → 5 → 20 → 50 → 100。同时监控:
- 支付成功率(分子:支付成功,分母:下单);
- 异步通知到达率;
- 退款成功率;
- 账单差异率。
任何指标下降立即回滚。
十二、工程坑点清单
项目上线后最常见的踩坑,大致分这些类:
12.1 证书类
- 证书过期:支付宝应用证书有效期 2–5 年,微信商户证书 5 年;到期没续签整个通道挂。加 30 天预警到 IM 群。
- 回调 HTTPS
证书链不完整:浏览器能打开不代表微信支付服务端能验通(微信服务端用的
root store 更严格)。用
openssl s_client -connect your.domain:443 -showcerts验证证书链,缺中间证书的加上。 - 平台证书轮换:微信 V3 平台证书每 12
个月轮换,客户端要同时保留旧新两张,按
Wechatpay-Serial选。
12.2 标识类
- out_trade_no 冲突:同一 out_trade_no 重下会报”重复订单”或返回老订单。规则:一旦支付失败要重下,必须换新 out_trade_no,老的走关单。
- AppID 与 mch_id 未绑定:报错
APPID_MCHID_NOT_MATCH;去商户平台绑定并公众号那边确认关联。 - openid 跨 AppID 不通:同一用户在公众号和小程序下 openid 不同,支付下单要用同一 AppID 对应的 openid。
12.3 时序类
- 异步通知乱序:通知服务器处理慢时,退款通知可能早于支付通知到达。用状态机 + FOR UPDATE 防错乱。
- 查单时机过早:下单后立即查单可能返回 ORDERNOTEXIST。等至少 5 秒。
12.4 合规类
- 服务商代收代付变二清:这是最严重的红线。若平台账户临时沉淀过商户资金(哪怕一小时),都可能被认定二清。正确做法:分账直接分到特约商户;平台自己的分润走独立分账接收方。
- 备付金:支付机构客户备付金 100% 集中存管在人行,服务商不持牌就不存在备付金概念;有牌照的(拉卡拉、易宝)要按央行规则。
12.5 体验类
- 关单太早:用户点支付后 SDK 唤起钱包还没付,商户端关单了;用户付钱后发现订单不存在,变成”异常充值”需要原路退。建议至少给 5–10 分钟关单窗口。
- 支付中状态可见性:一定要给用户一个”支付中”的中间态,不要让用户点两次导致重复下单。
十三、时序图:用户扫码 → 小程序 → 微信 API → 商户后台 → 回调
下面这张图是小程序下单 + JSAPI 支付的完整时序,服务商模式下最常见的组合。
sequenceDiagram
autonumber
participant U as 用户
participant MP as 小程序
participant BE as 商户后端
participant WX as 微信支付 V3
participant SUB as 特约商户账户
participant PF as 平台账户
U->>MP: 点击"立即支付"
MP->>BE: POST /order/create {goods, amount}
BE->>BE: 生成 out_trade_no, 写订单表 (PENDING)
BE->>WX: POST /v3/pay/transactions/jsapi<br/>sp_mchid, sub_mchid, openid, amount, notify_url
WX-->>BE: {prepay_id}
BE->>BE: 二次签名生成 paySign
BE-->>MP: {appId, timeStamp, nonceStr, package, paySign}
MP->>U: wx.requestPayment 调起收银台
U->>WX: 输入密码/指纹确认
WX->>SUB: 清算 (T+1),扣手续费
WX->>PF: 平台分润暂冻结
WX-->>MP: 支付成功
MP->>BE: 轮询/通知前端跳转
par 异步通知
WX->>BE: POST notify_url (V3 签名)
BE->>BE: 验签 → 幂等 → 更新订单 PAID → 写账务分录
BE-->>WX: {"code":"SUCCESS"}
and 主动查单兜底
BE->>WX: GET /v3/pay/transactions/out-trade-no/{no}
WX-->>BE: {trade_state: SUCCESS}
end
BE->>WX: POST /v3/profitsharing/orders<br/>分账给 PF (服务费) + 其他接收方
WX->>PF: 划拨分账金额
WX-->>BE: {order_id, state: PROCESSING}
WX->>BE: 分账回调 (SUCCESS)
BE->>BE: 写分账账务
十四、代码示例:Go 实现 JSAPI 下单 + V3 签名验签 + 分账
下面给一个最小可跑的 Go
片段(去掉错误处理细节),展示三个关键动作:V3 签名、JSAPI
下单、分账请求。生产代码建议直接用
github.com/wechatpay-apiv3/wechatpay-go(官方库)。
package wechatpay
import (
"bytes"
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"fmt"
"io"
"net/http"
"strconv"
"time"
)
type Client struct {
MchID string // 服务商 mch_id
SerialNo string // 商户证书序列号
PrivateKey *rsa.PrivateKey // 商户 API 私钥
APIv3Key []byte // APIv3 密钥(32 bytes)
PlatformCerts map[string]*rsa.PublicKey // serial_no -> platform cert pubkey
}
// buildAuthorization 构造 V3 请求签名头
func (c *Client) buildAuthorization(method, urlPath string, body []byte) (string, error) {
ts := strconv.FormatInt(time.Now().Unix(), 10)
nonce := randomString(32)
signStr := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n",
method, urlPath, ts, nonce, string(body))
h := sha256.New()
h.Write([]byte(signStr))
sig, err := rsa.SignPKCS1v15(rand.Reader, c.PrivateKey, crypto.SHA256, h.Sum(nil))
if err != nil {
return "", err
}
return fmt.Sprintf(`WECHATPAY2-SHA256-RSA2048 mchid="%s",nonce_str="%s",`+
`timestamp="%s",serial_no="%s",signature="%s"`,
c.MchID, nonce, ts, c.SerialNo, base64.StdEncoding.EncodeToString(sig)), nil
}
type JSAPIRequest struct {
SPAppID string `json:"sp_appid"`
SPMchID string `json:"sp_mchid"`
SubMchID string `json:"sub_mchid"`
Description string `json:"description"`
OutTradeNo string `json:"out_trade_no"`
NotifyURL string `json:"notify_url"`
SettleInfo struct {
ProfitSharing bool `json:"profit_sharing"`
} `json:"settle_info"`
Amount struct {
Total int `json:"total"`
Currency string `json:"currency"`
} `json:"amount"`
Payer struct {
SPOpenID string `json:"sp_openid,omitempty"`
SubOpenID string `json:"sub_openid,omitempty"`
} `json:"payer"`
}
type JSAPIResponse struct {
PrepayID string `json:"prepay_id"`
}
// JSAPIOrder 下 JSAPI 单(服务商模式)
func (c *Client) JSAPIOrder(req *JSAPIRequest) (*JSAPIResponse, error) {
const urlPath = "/v3/pay/partner/transactions/jsapi"
body, _ := json.Marshal(req)
auth, err := c.buildAuthorization("POST", urlPath, body)
if err != nil {
return nil, err
}
httpReq, _ := http.NewRequest("POST",
"https://api.mch.weixin.qq.com"+urlPath, bytes.NewReader(body))
httpReq.Header.Set("Authorization", auth)
httpReq.Header.Set("Accept", "application/json")
httpReq.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(httpReq)
if err != nil {
return nil, err
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
if resp.StatusCode != 200 {
return nil, fmt.Errorf("jsapi order failed: %s %s", resp.Status, respBody)
}
// 响应验签(略,见 VerifyResponse)
if err := c.VerifyResponse(resp.Header, respBody); err != nil {
return nil, fmt.Errorf("response verify: %w", err)
}
var out JSAPIResponse
_ = json.Unmarshal(respBody, &out)
return &out, nil
}
// VerifyResponse 验证 V3 响应签名
func (c *Client) VerifyResponse(h http.Header, body []byte) error {
ts := h.Get("Wechatpay-Timestamp")
nonce := h.Get("Wechatpay-Nonce")
serial := h.Get("Wechatpay-Serial")
sigB64 := h.Get("Wechatpay-Signature")
pub, ok := c.PlatformCerts[serial]
if !ok {
return fmt.Errorf("unknown platform cert serial: %s", serial)
}
signStr := fmt.Sprintf("%s\n%s\n%s\n", ts, nonce, string(body))
hash := sha256.Sum256([]byte(signStr))
sig, err := base64.StdEncoding.DecodeString(sigB64)
if err != nil {
return err
}
return rsa.VerifyPKCS1v15(pub, crypto.SHA256, hash[:], sig)
}
// BuildPaySign 生成前端调起的 paySign
func (c *Client) BuildPaySign(appID, prepayID string) (map[string]string, error) {
ts := strconv.FormatInt(time.Now().Unix(), 10)
nonce := randomString(32)
pkg := "prepay_id=" + prepayID
signStr := fmt.Sprintf("%s\n%s\n%s\n%s\n", appID, ts, nonce, pkg)
h := sha256.Sum256([]byte(signStr))
sig, err := rsa.SignPKCS1v15(rand.Reader, c.PrivateKey, crypto.SHA256, h[:])
if err != nil {
return nil, err
}
return map[string]string{
"appId": appID,
"timeStamp": ts,
"nonceStr": nonce,
"package": pkg,
"signType": "RSA",
"paySign": base64.StdEncoding.EncodeToString(sig),
}, nil
}
// ---------------- 分账 ----------------
type Receiver struct {
Type string `json:"type"` // MERCHANT_ID | PERSONAL_OPENID | PERSONAL_SUB_OPENID
Account string `json:"account"`
Amount int `json:"amount"` // 分
Description string `json:"description"`
}
type ProfitSharingRequest struct {
AppID string `json:"appid"`
SubMchID string `json:"sub_mchid"`
TransactionID string `json:"transaction_id"`
OutOrderNo string `json:"out_order_no"`
Receivers []Receiver `json:"receivers"`
UnfreezeUnsplit bool `json:"unfreeze_unsplit"`
}
func (c *Client) ProfitSharing(req *ProfitSharingRequest) ([]byte, error) {
const urlPath = "/v3/profitsharing/orders"
body, _ := json.Marshal(req)
auth, err := c.buildAuthorization("POST", urlPath, body)
if err != nil {
return nil, err
}
httpReq, _ := http.NewRequest("POST",
"https://api.mch.weixin.qq.com"+urlPath, bytes.NewReader(body))
httpReq.Header.Set("Authorization", auth)
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Accept", "application/json")
resp, err := http.DefaultClient.Do(httpReq)
if err != nil {
return nil, err
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("profit sharing failed: %s %s", resp.Status, respBody)
}
if err := c.VerifyResponse(resp.Header, respBody); err != nil {
return nil, err
}
return respBody, nil
}
// ---------------- 工具 ----------------
func LoadPrivateKeyPEM(pemBytes []byte) (*rsa.PrivateKey, error) {
block, _ := pem.Decode(pemBytes)
if block == nil {
return nil, fmt.Errorf("invalid PEM")
}
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return nil, err
}
return key.(*rsa.PrivateKey), nil
}
func randomString(n int) string {
b := make([]byte, n/2)
rand.Read(b)
return fmt.Sprintf("%x", b)
}调用方:
client := &Client{
MchID: "1900001109",
SerialNo: "34345340239483293...",
PrivateKey: priv,
APIv3Key: []byte("your-32-byte-apiv3-key-xxxxxxxxx"),
PlatformCerts: map[string]*rsa.PublicKey{...},
}
resp, err := client.JSAPIOrder(&JSAPIRequest{
SPAppID: "wx8888...",
SPMchID: "1900001109",
SubMchID: "1900000109", // 特约商户
Description: "订单 #P20260422001",
OutTradeNo: "P20260422001",
NotifyURL: "https://pay.example.com/wxpay/notify",
Amount: struct{Total int "json:\"total\""; Currency string "json:\"currency\""}{
Total: 10000, Currency: "CNY"},
Payer: struct{SPOpenID string "json:\"sp_openid,omitempty\""; SubOpenID string "json:\"sub_openid,omitempty\""}{
SubOpenID: "oLw..."},
})
if err != nil { log.Fatal(err) }
paySign, _ := client.BuildPaySign("wxSubAppID...", resp.PrepayID)
// paySign 返回给小程序前端支付宝的 SDK 使用类似,官方
github.com/smartwalle/alipay/v3
社区库最常用,签名 / 验签 / 证书模式都已封装。
十四 bis、订单与账务数据模型示例
接入两家钱包后,商户侧至少要维护四张核心表:支付单、退款单、分账单、异步通知原始日志。下面给一套 Postgres / MySQL 风格的最小建表,供参考(生产环境会再拆冷热表、按日分区)。
-- 支付单(统一通道抽象)
CREATE TABLE payment (
id BIGSERIAL PRIMARY KEY,
out_trade_no VARCHAR(64) NOT NULL UNIQUE, -- 商户订单号,幂等键
biz_order_id VARCHAR(64) NOT NULL, -- 业务订单号(可多对一)
channel VARCHAR(16) NOT NULL, -- alipay | wechatpay
scene VARCHAR(16) NOT NULL, -- jsapi | app | native | h5 | micropay
sp_mchid VARCHAR(32), -- 服务商商户号(可空)
sub_mchid VARCHAR(32) NOT NULL, -- 特约商户号 / PID
appid VARCHAR(32) NOT NULL,
amount BIGINT NOT NULL, -- 分
currency CHAR(3) NOT NULL DEFAULT 'CNY',
status VARCHAR(16) NOT NULL, -- PENDING | PAID | CLOSED | REFUNDED
channel_trade_no VARCHAR(64), -- 通道流水号(transaction_id / trade_no)
openid VARCHAR(64),
paid_at TIMESTAMPTZ,
closed_at TIMESTAMPTZ,
profit_sharing BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_payment_biz ON payment (biz_order_id);
CREATE INDEX idx_payment_status_created ON payment (status, created_at);
-- 退款单
CREATE TABLE refund (
id BIGSERIAL PRIMARY KEY,
out_refund_no VARCHAR(64) NOT NULL UNIQUE,
out_trade_no VARCHAR(64) NOT NULL REFERENCES payment(out_trade_no),
amount BIGINT NOT NULL,
status VARCHAR(16) NOT NULL, -- PENDING | SUCCESS | ABNORMAL | CLOSED
channel_refund_no VARCHAR(64),
reason VARCHAR(255),
refunded_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_refund_status_created ON refund (status, created_at);
-- 分账单(一次 profitsharing 对应多条 receivers)
CREATE TABLE profit_sharing (
id BIGSERIAL PRIMARY KEY,
out_order_no VARCHAR(64) NOT NULL UNIQUE,
out_trade_no VARCHAR(64) NOT NULL REFERENCES payment(out_trade_no),
status VARCHAR(16) NOT NULL, -- PROCESSING | FINISHED | CLOSED
unfreeze_unsplit BOOLEAN NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE profit_sharing_receiver (
id BIGSERIAL PRIMARY KEY,
ps_id BIGINT NOT NULL REFERENCES profit_sharing(id),
receiver_type VARCHAR(24) NOT NULL, -- MERCHANT_ID | PERSONAL_OPENID
receiver_account VARCHAR(64) NOT NULL,
amount BIGINT NOT NULL,
status VARCHAR(16) NOT NULL, -- PENDING | SUCCESS | CLOSED | RETURNED
description VARCHAR(128),
finished_at TIMESTAMPTZ
);
-- 异步通知原始日志(验签后原文存档,便于纠纷时回溯)
CREATE TABLE channel_notify_log (
id BIGSERIAL PRIMARY KEY,
channel VARCHAR(16) NOT NULL,
event_type VARCHAR(32) NOT NULL, -- TRANSACTION.SUCCESS | REFUND.SUCCESS | PROFITSHARING.SUCCESS
out_trade_no VARCHAR(64),
nonce VARCHAR(64),
received_at TIMESTAMPTZ NOT NULL DEFAULT now(),
raw_headers JSONB,
raw_body TEXT,
verify_ok BOOLEAN NOT NULL,
processed BOOLEAN NOT NULL DEFAULT FALSE,
processed_at TIMESTAMPTZ
);
CREATE INDEX idx_notify_trade ON channel_notify_log (out_trade_no);
CREATE UNIQUE INDEX uniq_notify_nonce ON channel_notify_log (channel, nonce);几个设计要点值得单拎出来讲:
out_trade_no是支付单的物理主键(UNIQUE),因为它同时是我们发出去给通道的幂等键,也是通道回传给我们的关联键。允许同一biz_order_id对应多个 payment(关单重下)。- 退款
out_refund_no不是越小越好:微信/支付宝对退款单号有 6–64 位限制,建议长度拉满以支持组合编码(如R{timestamp}{payment_id}),不要用自增。 - 分账的冻结属性在 payment 表冗余一份
profit_sharing字段,方便跑批扫描”未分账到期订单”。 - 通知日志用
(channel, nonce)唯一索引,天然防重放;raw_body原文存档用于日后纠纷回溯——千万别删,监管检查和客诉调证都要用。 - 字段规模上,10 亿级订单量时 payment 表按
created_at月分区,冷表转 OSS + Hive,详见第 04 篇。
14 bis.2 账务分录与支付的对应
结合第 03 篇复式记账,一笔微信 JSAPI 收款(服务商模式,交易额 100 元,手续费 0.38 元,平台分润 10 元)的记账应拆为四组分录:
T:用户支付 100 元
Dr 特约商户-待结算 100.00
Cr 用户应付-订单 100.00
T:通道扣手续费 0.38 元(支付宝/微信从待结算里扣,T+1 到账时就是 99.62)
Dr 费用-通道手续费 0.38
Cr 特约商户-待结算 0.38
T+N(分账时):平台分润 10 元从待结算转到平台应收
Dr 特约商户-待结算 10.00
Cr 平台应收-服务费 10.00
T+1(结算):剩余 89.62 结算到商户对公
Dr 商户对公-银行存款 89.62
Cr 特约商户-待结算 89.62
退款时所有分录逆向记账,分账接收方按比例追回。账务分录和支付单的关联是通过
payment.id + entry.business_id
完成的,第 03 篇有详解。
十五、真实案例与公开事件
国内:收钱吧、哆啦宝(2015–2018)的”大商户”整改。早期聚合支付平台普遍用”大商户模式”——平台自己作为一个大商户在银行收款,再按订单结算给下面真实商户。2017 年央行 281 号文《关于规范支付创新业务的通知》明确禁止无证机构”以聚合支付之名行无证经营收单业务之实”。随后一批聚合支付商被调整为合规服务商模式(钱直接到商户),或与持牌机构合作,历史存量清退历时数年。这是服务商模式二清红线最典型的公开事件。
国际:Stripe Connect 的 Platform / Connected
Account 模式。Stripe 做 SaaS 平台 / Marketplace
收款有三种 Connect
账户:Standard、Express、Custom。Standard
和国内服务商模式几乎对应:Connected Account 是 Stripe
直接签约的商户,平台只是代建账户和发起支付;Custom
则把全部 UI 自定义给平台,资金仍是 Stripe → Connected
Account。Stripe 通过 application_fee_amount
实现平台分润,等价于微信/支付宝的分账,但 Stripe
原生支持全球,国内两家钱包的分账目前主要限于境内商户。
2023 某酒店集团押金事故(公开新闻):预授权到期自动解冻,但商户结账时忘了先扣款就解冻,结果用户退房后账上余额不足,几千个订单转为线下催收。教训:预授权到期前必须要有主动结算任务,失败有告警。
十六、选型与落地清单
16.1 选型:直连 vs 服务商
- 直连:你只有一家公司、一个主体、一套流水 → 走直连。
- 服务商:你做 SaaS、平台、多商户 → 必走服务商。不要用直连的一个商户号”帮”多家公司收钱,那就是二清。
16.2 选型:V2 vs V3
- 新接入一律 V3,除非某个能力只有 V2(越来越少);
- 老系统迁移 V3 按场景分批:先迁统一下单、通知;再迁退款、分账;最后迁对账。
16.3 落地清单(Go live 前必过)
十七、参考资料
- 支付宝开放平台文档:
https://opendocs.alipay.com/open/00fjfh - 支付宝接口文档目录:
https://opendocs.alipay.com/apis - 微信支付 API v3
文档:
https://pay.weixin.qq.com/wiki/doc/apiv3/index.shtml - 微信支付签名规范(V3):
https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_0.shtml - 微信支付 Go
官方库:
https://github.com/wechatpay-apiv3/wechatpay-go - 中国人民银行《关于规范支付创新业务的通知》(银办发〔2017〕281 号)
- 中国人民银行《非银行支付机构客户备付金存管办法》(2021 年)
- Stripe Connect
文档:
https://stripe.com/docs/connect - 收钱吧、乐刷等聚合支付公开整改信息(公开新闻汇总)
上一篇:《卡组织收单链路:银联/Visa/Mastercard、ISO 8583、ISO 20022 迁移》
下一篇:《支付网关设计:路由、限流、补单、异步通知、签名与防重放》
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【金融科技工程】支付系统全景:收单、发卡、聚合、跨境、B2B、C2C
把模糊的"支付"一词拆成可工程化的系统分类——四方参与者、收单发卡双边、卡组织与钱包通道、中国与国际的费率结构、实时支付浪潮、以及一张完整的资金流图。
【金融科技工程】对账系统工程:账单、总账、资金对账、差错处理
从金融本质、分层架构到文件格式、匹配算法、差错处理与调账流程,系统讲解对账系统的工程落地,包含 Python/Go 示例、Mermaid 状态机与大型平台十亿级对账方案。
【金融科技工程】金融科技工程全景:从支付到交易所的系统分类与读图
金融科技(FinTech)不是普通后端加一张账户表。钱的原子性、监管的硬边界、一个小数点的代价,把这个领域推进到工程强度最高的那一档。本文是【金融科技工程】25 篇的总目录与阅读地图:先交代为什么它比一般业务系统更难,再给出对账体、支付体、交易体、风控合规体四维分类,把后续 24 篇挂到骨架上,最后给出一份绿地项目的落地顺序建议。
【金融科技工程】钱的建模:金额精度、币种、会计单位、多语言金额
在代码里正确地表示"一笔钱"远比看起来难。本文系统梳理金额的数值建模(浮点、定点、Decimal、最小单位)、币种标准(ISO 4217)、本地化显示、汇率换算与数据库存储,并给出 Go、Python、Java、Rust 的工程化示例。