去年冬天有个做知识付费的哥们找到我,说他把收款从易支付换成了微信支付商户直连,理由是"少一层中间商,费率能省零点几个点"。结果上线第三天凌晨,他给我发了一长串截图——订单显示支付成功,钱也确实扣了,但他自己的后台里订单状态还是"待支付",用户付完钱拿不到课程,退款工单堆了二十几个。
他以为是自己回调地址写错了,查了半天;又以为是签名算法不对,对着文档逐字核对;最后发现是回调请求来了三次,第一次处理成功了,第二次第三次因为没做幂等,把已经发货的订单又改回了"待支付"。
这件事让我想明白一个道理:直连支付省下来的那点费率,很快就会被你补上的工程成本吃光——不是写代码的成本,是半夜爬起来查问题的成本。今天这篇就把直连支付从头到尾讲透,尤其是签名、回调、对账这三件看起来简单、实际上最容易翻车的事。
直连支付"直"在哪,先把这个词说清楚
网上讲直连支付,十个有九个在背名词:直连就是商户直接对接持牌机构,间连就是通过聚合平台转一道。这话没错,但没说到点上。
直连和间连的区别,本质不在"中间有没有人",而在资金和信息的控制权在谁手里。间连模式下,钱先进聚合平台的账户,平台再结算给你,回调也是平台先收到、再转给你;直连模式下,钱直接进你在持牌机构开的商户号,回调也直接打到你的服务器。一个是你站在别人身后,一个是你自己站在台前。
这个区别带来一个很多人忽略的后果:间连的时候,签名、回调重试、对账这些脏活累活是平台替你干的,你只管收钱发货;一旦直连,这些活全落到你自己头上。你不是省掉了一个中间商,而是把中间商干的活自己揽了过来。这才是理解直连支付的关键。
你以为省的是费率,其实省的是"托管"
直连的费率确实低一点。微信支付商户号的标准费率在 0.6% 上下,支付宝差不多也是这个区间,而聚合平台一般要在这个基础上再加自己的抽成,落到你头上可能就是 0.8%、1% 甚至更高。对交易流水大的商户,这零点几个点一年下来是笔真金白银。
但这笔账只算了一半。直连省下的费率,对应的代价是你要自己扛三件事:
第一,签名安全。你的商户密钥、API 证书、私钥文件,泄露了就是灾难。间连时代你只有一个平台的登录密码,直连时代你要管一堆密钥文件,哪个没放对位置都可能出事。
第二,回调的可靠性。间连平台会帮你处理回调重试、失败补偿;直连了,回调丢了就是丢了,没人帮你补,你得自己判断订单到底成没成。
第三,对账。间连平台每天给你一张汇总表;直连了,你要自己去持牌机构后台拉对账单,跟你自己的订单系统一笔一笔对。
这三件事,没有一件能靠"抄一段代码"解决。下面我按接入的顺序,一件一件说清楚,重点讲那些文档里不写、只有真做过才知道的坑。
第一步:商户号和证书,别急着写代码
很多人一上来就急着写代码调接口,结果卡在资质这一步。直连微信支付,个体工商户或者企业营业执照是硬门槛,个人身份证现在是开不了商户号的。支付宝的当面付、电脑网站支付,同样要营业执照。
资质过了之后,你会拿到几样东西,这里最容易搞混:
商户号(mchid)是一串数字,微信支付里通常以 190 或 160 开头。
API 密钥(APIv2 时代的 key)是一串 32 位字符,你自己设置的,签名要用。
API 证书(APIv3 时代)包含证书序列号、私钥文件、证书文件,一共三个东西,缺一个都签不了名。
很多人栽在这里:以为证书文件下载下来就是一个文件,其实微信 v3 要的是"证书序列号 + 私钥 pem + 证书 pem"三件套,私钥文件还不能用微信后台直接下载的那个,得用工具生成 CSR 再换。我第一次做的时候,光把这三个文件搞清楚就耗了大半天。
还有一个冷门细节:证书会过期。微信支付 API 证书有效期一般是一年,到期前 30 天后台会提示。如果你没做轮换,证书过期的那一刻,所有正在进行的支付请求会瞬间全部签名失败。所以证书轮换这件事,要在到期前就排上日程,不能等它过期了才手忙脚乱。
第二步:签名,最容易翻车的地方
签名是直连支付的第一道坎,也是最容易让人怀疑人生的地方——明明算法就是文档写的那个,怎么签名就是通不过?
签名的逻辑本身不复杂:把你请求里的参数按一定规则拼成一串,加上你的密钥,算一个哈希值,随请求一起发过去,对方用同样的规则算一遍,对上了就说明请求没被篡改。微信 v2 用 MD5,v3 用 RSA 签名,支付宝用的是 RSA2。关于 MD5 和 RSA 到底该用哪个、有什么区别,我单独写过一篇支付回调sign签名:MD5和RSA到底有什么区别、该用哪个 - 彩虹易支付,这里不展开,重点说几个文档里一笔带过、实际上坑死人的细节。
第一个坑:参数排序不是"字典序"那么简单。微信 v2 的签名规则是,把除了 sign 字段之外的所有参数,按字段名的 ASCII 码从小到大排序,拼成 key=value 用 & 连接,最后再拼上 key 密钥算 MD5。这里有两个容易错的地方:一是空值字段不参与签名,但它在请求里还得保留;二是排序的是"字段名",不是"字段值"。有些人把值也拿来排序,签名自然对不上。
第二个坑:时间戳偏移。签名请求里带一个 timestamp 字段,对方会校验你的时间戳和它服务器时间的差值,超过一定范围(微信是几分钟)直接拒绝。如果你的服务器时间慢了或者快了,签名算得再对也是 401。这个坑最阴险的地方在于,日志里只显示"签名错误",不告诉你其实是时间不对,你会对着签名算法查一晚上,最后发现是 ntp 服务没开。
第三个坑:密钥泄露的位置。私钥文件、API 密钥这些,绝对不能放在 web 可访问的目录下。我见过不止一个人把私钥写在 config.php 里、放在网站根目录,被爬虫扫到,一晚上被人用他的商户号生成了几十笔订单。密钥文件放 web 目录之外,权限设成 600,这是直连支付的第一条安全红线。
签名这块我建议自己写一个独立的小函数,别依赖网上复制来的代码片段,因为那些片段往往省略了空值处理,遇到带可选参数的请求就崩。下面这段是微信 v2 风格的签名写法,核心就一句话:空值字段不参与签名,但排序时按字段名来。
import hashlib
def build_sign(params: dict, api_key: str) -> str:
# 去掉 sign 字段和空值,按字段名 ASCII 升序排序
items = sorted(
(k, v) for k, v in params.items()
if k != "sign" and v != "" and v is not None
)
# 拼成 key=value&key=value 串,末尾拼接密钥
raw = "&".join(f"{k}={v}" for k, v in items)
raw += f"&key={api_key}"
return hashlib.md5(raw.encode("utf-8")).hexdigest().upper()
这个函数里最容易写错的就是那行 if 判断:有人图省事把空值也拼进去,有人把 sign 字段也算进签名里,还有人排序时把"值"也拿来排。三个错任何一个,签名必挂。
第三步:回调,幂等才是真正的难点
如果说签名是"能不能调通"的问题,回调就是"能不能稳定跑"的问题。回调的坑,十个有九个出在幂等上。
先搞清楚回调的机制:用户付完钱,微信或支付宝会往你预留的 notify_url 发一个 POST 请求,告诉你这笔订单成功了。但请注意,这个回调不是只来一次。如果你没有在规定时间内正确返回成功应答,对方会按一定间隔重试,微信是每隔一段时间重试,最多重试若干次。也就是说,同一笔订单的回调,可能来三五次。
如果你的回调处理逻辑是"收到成功通知 → 发货 → 改订单状态",没有做幂等,那第二次、第三次回调过来,就会重复发货,或者更糟——像开头那个哥们一样,把已经成功的订单又改回待支付。
正确的做法是:以交易单号(微信的 transaction_id、支付宝的 trade_no)作为唯一键,在数据库里建唯一索引。回调进来先查这个单号处理过没有,处理过就直接返回成功,跳过后续逻辑。这是幂等的兜底,靠代码判断不靠谱,一定要靠数据库唯一约束兜底。
还有一个容易漏的点:验签要在业务处理之前。回调请求来了,先别急着发货,先用你的密钥重新算一遍签名,比对通过再往下走。因为回调地址是公开的,任何人都可能往你的 notify_url 发请求,如果只认 trade_status 不验签,别人伪造一个"支付成功"的通知就能白嫖你的商品。
验签通过之后,还要判断订单状态。微信的 trade_state 字段,只有 SUCCESS 才代表真正支付成功,NOTPAY、USERPAYING、PAYERROR 这些都不能发货。很多人看到回调来了就发货,结果用户扣款了但银行端还在处理中,状态是 USERPAYING,你就提前发了货。
回调处理逻辑要尽量简单:验签 → 查重 → 改状态 → 返回成功字符串。发短信、发邮件、推消息这些异步的事,扔到队列里慢慢做,别在回调里同步做,否则回调处理超时,对方以为你失败了又重试,雪上加霜。
第四步:对账,四象限拆账定位到具体哪笔
对账是直连支付里最容易被轻视、也最磨人的一环。很多人以为对账就是"我这边成功订单数 = 微信那边成功订单数",对不上就抓瞎。
真正有用的对账方法,是把账拆成四个象限,一张表就能看明白:
| 我方订单 | 机构账单 | 说明 | 处理动作 |
|---|---|---|---|
| 成功 | 成功 | 两边一致,正常 | 无需处理 |
| 成功 | 失败 | 发货了但钱没收到,最危险 | 排查是否回调伪造或状态判断错 |
| 失败 | 成功 | 收了钱没发货,反馈重灾区 | 尽快补发或退款 |
| 失败 | 失败 | 两边一致,正常 | 无需处理 |
这张表的核心逻辑是:我方"成功"和机构"成功"要严格对齐。对不上的那两格(我方成功对方失败、我方失败对方成功)才是真正要查的,其余两格都是正常的。
对账的时候,不要拿总数对总数,要拿明细对明细。把两边的流水按单号 join 起来,找出只在一方出现的单子,再按上面四个象限分类。这样你就能定位到具体是哪一笔有问题,而不是面对一个"差了三毛二"的总账差额无从下手。
还有一个实操细节:持牌机构的对账单不是实时的。微信支付的账单一般是 T+1 才完整,当天的对账可能有延迟,所以别指望当天就能对平。对账要按天跑,用前一天的完整账单来对,今天的先放着。
对账脚本我建议做成每天定时跑一次,把对不上的单子自动标出来,而不是等月底财务结算的时候才发现有一堆差额。差额拖得越久越难查,因为原始流水可能已经被覆盖或者归档了。
直连到底值不值,看你的量
写到这,你可能已经发现,直连支付不是"更便宜"这么简单,它是一个完整的工程活。那么直连到底值不值得上?我的判断标准其实就一条:看你的交易量和团队。
如果你的月流水很大,一年省下的费率足够养一个专门维护支付的人,那直连是划算的,甚至直连多个渠道做冗余都值得。如果你是个个人开发者,一个月流水几千块,自己还要写业务、做运营、熬夜修 bug,那直连省下的那点费率,很快就会被"半夜爬起来查回调"这件事消耗掉——这时候走聚合平台反而更省心。
关于个人开发者到底该选直连还是易支付,我在个人开发者做知识付费:直连微信支付和易支付到底怎么选? - 彩虹易支付里算过一笔更细的账,从费率和维护成本两个维度对比过,纠结的话可以去看看。
回到开头那个哥们。他最后把回调加了幂等,用交易单号做了唯一索引,问题解决了,但那一周的退款工单和客服压力,早就把他直连省下来的那点费率吃了个精光。他后来跟我说了一句话,我觉得挺适合当这篇的结尾:直连支付省的是钱,花的是心,账要算全了再决定。