惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
H
Help Net Security
博客园_首页
P
Privacy International News Feed
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
Tenable Blog
Latest news
Latest news
D
Darknet – Hacking Tools, Hacker News & Cyber Security
爱范儿
爱范儿
Cyberwarzone
Cyberwarzone
P
Palo Alto Networks Blog
月光博客
月光博客
有赞技术团队
有赞技术团队
Know Your Adversary
Know Your Adversary
博客园 - 叶小钗
P
Proofpoint News Feed
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
S
Schneier on Security
A
About on SuperTechFans
F
Full Disclosure
The Cloudflare Blog
T
The Exploit Database - CXSecurity.com
S
SegmentFault 最新的问题
Apple Machine Learning Research
Apple Machine Learning Research
Microsoft Security Blog
Microsoft Security Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
N
News and Events Feed by Topic
www.infosecurity-magazine.com
www.infosecurity-magazine.com
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Webroot Blog
Webroot Blog
量子位
大猫的无限游戏
大猫的无限游戏
小众软件
小众软件
WordPress大学
WordPress大学
Last Week in AI
Last Week in AI
美团技术团队
Help Net Security
Help Net Security
Microsoft Azure Blog
Microsoft Azure Blog
GbyAI
GbyAI
云风的 BLOG
云风的 BLOG
Y
Y Combinator Blog
博客园 - 司徒正美
N
Netflix TechBlog - Medium
S
Security @ Cisco Blogs
B
Blog
P
Privacy & Cybersecurity Law Blog
Security Archives - TechRepublic
Security Archives - TechRepublic
Hacker News - Newest:
Hacker News - Newest: "LLM"
V
Visual Studio Blog
NISL@THU
NISL@THU

博客园 - warm3snow

Loop Engineering — 从"写 prompt"到"设计循环",AI Agent 的下一次进化 Hermes 多角色实战:我做了一个智能投研系统 Claude Code 为什么放弃 RAG:用 grep 干代码搜索的真实架构 Agent Eval 最佳实践:从 Benchmark 到生产监控的完整落地指南 如何打造一支 AI 原生团队:5-7 人小团队的四步搭建指南 一份来自 Karpathy 的 AI 编程 skill 用 Backtrader 拆解量化交易:一周内你会真正理解的七件事 AI 写代码比你快 10 倍,你还剩什么?——读 mattpocock/skills 多方协作平台的领域模型:为什么必须是"联盟、租户、项目、任务"四层 从龙虾到爱马仕:一个老虾农的"叛逃"实录 读懂加密市场(五):进阶之路 读懂加密市场(四):交易者的内功 读懂加密市场(三):合约、DEX 与工具 读懂加密市场(二):建立你的认知框架 读懂加密市场(一):加密市场的游戏规则 读懂加密市场:系列总览 一张图看懂巴菲特 48 年投资帝国:知识图谱效果全展示 用 AI 构建巴菲特投资知识图谱:从 48 封公开信到可交互的投资智慧网络 「同事.skill」爆火:当 AI 学会"炼化"你的同事 在线支付系列(四):PayPal——一位海外买家的安全支付之旅 在线支付系列(三):Stripe & 信用卡——一件跨境商品的卡支付之旅 在线支付系列(二):支付宝 & 微信支付——一杯咖啡的扫码之旅 在线支付系列(一):一笔订单触发的支付之旅 Harness Engineering 最佳实践:从概念到落地的完整操作手册 Claude Code 源码泄露全复盘:51.2 万行代码裸奔,Anthropic 在同一个坑里摔了两次 数字货币交易所系列(四):DEX 深度解析——从 AMM 到链上订单簿,去中心化交易的技术与未来 数字货币交易所系列(三):安全、风控与监管——从 Mt.Gox 到 FTX,CEX 的信任是怎么建立(和崩塌)的? 数字货币交易所系列(二):CEX 核心技术架构——撮合引擎、钱包系统与充提链路的深度拆解
在线支付系列(五):统一支付网关架构设计
warm3snow · 2026-04-04 · via 博客园 - warm3snow

在线支付系列(五):统一支付网关架构设计

这是在线支付系列的最后一篇。前四篇里,我们分别搞定了支付宝、微信支付、Stripe 和 PayPal。如果你真的一篇一篇跟着做了下来,你的代码库里大概率已经出现了这样的场景——

一、噩梦的开始

小陈是一家跨境电商的后端工程师。过去三个月,他依次接入了支付宝、微信支付、Stripe 和 PayPal,每接一个都花了一两周。他觉得自己挺厉害的——四种支付全搞定了。

直到有一天早上,他打开了工作群:

产品经理:支付宝回调好像有问题,昨晚有几笔订单没到账
运营:微信退款怎么还没处理?
老板:PayPal 那边有个争议需要处理,你看看
财务:这个月的对账差了 3 笔,帮忙查查是哪个渠道的

小陈盯着屏幕发呆。四套支付渠道,四种签名机制,四种回调格式,四套退款逻辑,四份对账文件。每次改一个公共逻辑(比如加个日志字段),他得改四个地方。每次排查问题,他得在四套代码之间来回跳。

他突然意识到:接入四个渠道不是终点,而是噩梦的开始。

这正是「统一支付网关」要解决的问题。


二、思考:到底哪些东西可以统一?

在动手之前,小陈做了一张表,把四个渠道的差异摊开来看:

维度 支付宝 微信支付 Stripe PayPal
金额单位 元(字符串 "99.99" 分(整数 9999 分(整数 9999 元(字符串 "99.99"
签名方式 RSA2(SHA-256) HMAC-SHA256 HMAC-SHA256 调 API 验签
回调格式 form 表单 JSON + XML JSON JSON
回调响应 返回 "success" 返回 {"code":"SUCCESS"} 返回 HTTP 200 返回 HTTP 200
退款接口 同步返回结果 异步回调通知 同步返回结果 同步返回结果
认证方式 应用私钥签名 商户证书 + API Key Secret Key OAuth 2.0

差异这么大,还能统一吗?

小陈画了一张图后发现:差异在细节,但流程是相通的。

每笔支付不管走哪个渠道,本质上都是这几步:

创建订单 → 调用渠道下单 → 等待用户支付 → 接收回调 → 更新状态 → 通知业务
                                                          ↓
                                              (可选)退款 → 对账

所以策略很清楚了:流程统一,差异下沉。


三、三层架构:让混乱变有序

小陈参考了几个开源方案(Jeepay、PayPal Braintree SDK、Stripe Connect 的设计),画出了这样的分层:

┌─────────────────────────────────────────────────────┐
│                    接入层(API Gateway)               │
│  统一 RESTful API / 参数校验 / 鉴权 / 限流           │
└────────────────────────┬────────────────────────────┘
                         │
┌────────────────────────▼────────────────────────────┐
│                    核心层(Payment Core)              │
│  ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐      │
│  │订单   │ │路由   │ │幂等   │ │状态机 │ │通知   │      │
│  │管理   │ │引擎   │ │控制   │ │引擎   │ │中心   │      │
│  └──────┘ └──────┘ └──────┘ └──────┘ └──────┘      │
│  ┌──────┐ ┌──────┐                                  │
│  │对账   │ │风控   │                                  │
│  │引擎   │ │引擎   │                                  │
│  └──────┘ └──────┘                                  │
└────────────────────────┬────────────────────────────┘
                         │
┌────────────────────────▼────────────────────────────┐
│                    渠道层(Channel Adapters)          │
│  ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐       │
│  │支付宝   │ │微信支付 │ │Stripe  │ │PayPal  │       │
│  │Adapter │ │Adapter │ │Adapter │ │Adapter │       │
│  └────────┘ └────────┘ └────────┘ └────────┘       │
└─────────────────────────────────────────────────────┘

三层各自的职责:

  • 接入层:对外暴露统一 API,业务系统只跟这一层打交道
  • 核心层:所有渠道共享的公共逻辑——订单管理、状态流转、幂等控制、对账
  • 渠道层:每个支付渠道的独有逻辑——签名方式、参数格式、回调解析

关键原则:业务系统永远不直接调渠道 API。哪天要换渠道,只需要改渠道层,上面两层不动。


四、统一 API:让业务系统只学一套接口

以前小陈的业务系统里散落着各种渠道特有的调用:

# 以前——到处都是渠道特有代码
if channel == "alipay":
    result = alipay_client.trade_precreate(out_trade_no=order_id, ...)
elif channel == "wechat":
    result = wechat_client.native_pay(out_trade_no=order_id, ...)
elif channel == "stripe":
    result = stripe.PaymentIntent.create(amount=amount, ...)
elif channel == "paypal":
    result = paypal_create_order(amount=amount, ...)

现在,业务系统只需要这样调:

4.1 统一下单

POST /api/v1/payments

{
    "order_id": "ORD_20260403001",     # 商户订单号(幂等键)
    "amount": 9999,                     # 金额,统一用最小单位(分)
    "currency": "CNY",                  # 币种
    "channel": "wechat_native",         # 支付渠道
    "subject": "Premium Plan",          # 商品描述
    "notify_url": "https://...",        # 回调地址(可选,有默认值)
    "extra": {                          # 渠道特有参数(可选)
        "openid": "oUpF8..."           # 比如微信 JSAPI 需要 openid
    }
}

响应也是统一的:

{
    "payment_id": "PAY_xxx",
    "channel_order_id": "wx_xxx",
    "status": "PENDING",
    "credential": {
        "qr_code": "weixin://..."
    }
}

credential 字段是前端拉起支付需要的凭证——微信是二维码链接,Stripe 是 client_secret,PayPal 是 approve_url。前端根据渠道类型解析即可。

4.2 统一查询、退款和关单

# 查询
GET /api/v1/payments/{payment_id}

# 退款
POST /api/v1/refunds
{
    "payment_id": "PAY_xxx",
    "amount": 5000,              # 退 50 元(分)
    "reason": "用户申请退款"
}

# 关单(超时未支付时主动关闭)
POST /api/v1/payments/{payment_id}/cancel

注意一个关键设计:金额统一用"分"。支付宝和 PayPal 用"元",微信和 Stripe 用"分",网关内部统一用分存储,在渠道适配器里做转换。这消除了最常见的金额计算 bug。


五、渠道适配器:策略模式让差异各归各位

这是整个网关最精妙的部分。小陈用了经典的「策略模式」——定义一个统一的适配器接口,每个渠道各自实现。

5.1 适配器基类

from abc import ABC, abstractmethod
from dataclasses import dataclass
from enum import Enum

class PaymentStatus(Enum):
    PENDING   = "PENDING"
    PAID      = "PAID"
    FAILED    = "FAILED"
    CLOSED    = "CLOSED"
    REFUNDING = "REFUNDING"
    REFUNDED  = "REFUNDED"

@dataclass
class PaymentRequest:
    order_id: str
    amount: int          # 统一用分
    currency: str
    subject: str
    notify_url: str
    extra: dict = None

@dataclass
class PaymentResponse:
    channel_order_id: str
    status: PaymentStatus
    credential: dict     # 前端拉起支付的凭证

class PaymentAdapter(ABC):
    """支付渠道适配器基类——所有渠道必须实现这四个方法"""

    @abstractmethod
    async def create_payment(self, req: PaymentRequest) -> PaymentResponse:
        """创建支付"""

    @abstractmethod
    async def query_payment(self, channel_order_id: str) -> PaymentStatus:
        """查询支付状态"""

    @abstractmethod
    async def refund(self, channel_order_id: str,
                     amount: int, reason: str) -> dict:
        """退款"""

    @abstractmethod
    async def verify_notify(self, headers: dict,
                            body: bytes) -> dict:
        """验证并解析回调通知"""

四个方法,四个渠道,形成了一个 4×4 的矩阵。每个格子里是各渠道的独有逻辑,但格子外面的世界看到的都是同一个接口

5.2 支付宝适配器

class AlipayAdapter(PaymentAdapter):
    def __init__(self, client):
        self.client = client  # 支付宝 SDK 客户端

    async def create_payment(self, req: PaymentRequest) -> PaymentResponse:
        biz_content = {
            "out_trade_no": req.order_id,
            "total_amount": str(req.amount / 100),  # 分 → 元(支付宝要元)
            "subject": req.subject,
        }
        result = self.client.api_alipay_trade_precreate(
            biz_content=biz_content,
            notify_url=req.notify_url,
        )
        return PaymentResponse(
            channel_order_id=result.get("trade_no", ""),
            status=PaymentStatus.PENDING,
            credential={"qr_code": result["qr_code"]},
        )

    async def verify_notify(self, headers, body) -> dict:
        params = parse_form(body)  # 支付宝回调是 form 格式
        if not self.client.verify(params, params.pop("sign")):
            raise ValueError("签名验证失败")
        return {
            "order_id": params["out_trade_no"],
            "channel_order_id": params["trade_no"],
            "amount": int(float(params["total_amount"]) * 100),  # 元 → 分
            "status": PaymentStatus.PAID,
        }

    def success_response(self):
        return "success"  # 支付宝要求返回纯文本 "success"

5.3 Stripe 适配器

class StripeAdapter(PaymentAdapter):
    async def create_payment(self, req: PaymentRequest) -> PaymentResponse:
        intent = stripe.PaymentIntent.create(
            amount=req.amount,           # Stripe 也用分,无需转换
            currency=req.currency.lower(),
            metadata={"order_id": req.order_id},
        )
        return PaymentResponse(
            channel_order_id=intent.id,
            status=PaymentStatus.PENDING,
            credential={"client_secret": intent.client_secret},
        )

    async def verify_notify(self, headers, body) -> dict:
        sig = headers.get("stripe-signature")
        event = stripe.Webhook.construct_event(body, sig, WEBHOOK_SECRET)
        intent = event["data"]["object"]
        return {
            "order_id": intent["metadata"]["order_id"],
            "channel_order_id": intent["id"],
            "amount": intent["amount"],  # 已经是分
            "status": PaymentStatus.PAID
                if event["type"] == "payment_intent.succeeded"
                else PaymentStatus.FAILED,
        }

    def success_response(self):
        return {"status": "ok"}  # Stripe 返回 200 即可

微信支付和 PayPal 的适配器同理,各自处理自己的签名和格式差异。关键是:核心层完全不需要知道这些差异

5.4 路由注册

class PaymentRouter:
    """支付渠道路由器"""

    def __init__(self):
        self._adapters: dict[str, PaymentAdapter] = {}

    def register(self, channel: str, adapter: PaymentAdapter):
        self._adapters[channel] = adapter

    def get_adapter(self, channel: str) -> PaymentAdapter:
        adapter = self._adapters.get(channel)
        if not adapter:
            raise ValueError(f"不支持的支付渠道: {channel}")
        return adapter

# 启动时注册所有渠道
router = PaymentRouter()
router.register("alipay_native",  AlipayAdapter(alipay_client))
router.register("wechat_native",  WechatAdapter(wechat_client))
router.register("stripe_card",    StripeAdapter())
router.register("paypal",         PayPalAdapter(paypal_config))

将来要加新渠道(比如 Apple Pay、Google Pay),只需要:

  1. 写一个新的 Adapter 实现四个方法
  2. router.register("apple_pay", ApplePayAdapter(...))

核心层和接入层的代码 一行都不用改


六、订单状态机:让状态流转有章可循

支付订单的状态不是随意变化的。小陈见过最离谱的 bug 是:一笔已经退款成功的订单,因为一个延迟到达的支付回调,又被标记成了"已支付"。

为了杜绝这种情况,需要一个严格的状态机:

                        创建订单
                          │
                          ▼
                    ┌───────────┐
                    │  PENDING   │ ─── 超时 ──→ CLOSED
                    └─────┬─────┘
                          │
                    支付成功/失败
                    ┌─────┴─────┐
                    ▼           ▼
              ┌─────────┐ ┌─────────┐
              │  PAID    │ │ FAILED  │
              └────┬────┘ └─────────┘
                   │
              申请退款
                   ▼
              ┌──────────┐
              │ REFUNDING │
              └────┬─────┘
                   │
              退款成功/失败
              ┌────┴─────┐
              ▼          ▼
        ┌──────────┐ ┌────────┐
        │ REFUNDED  │ │  PAID  │ (退款失败,回到 PAID)
        └──────────┘ └────────┘

代码实现:

VALID_TRANSITIONS = {
    PaymentStatus.PENDING:   [PaymentStatus.PAID,
                              PaymentStatus.FAILED,
                              PaymentStatus.CLOSED],
    PaymentStatus.PAID:      [PaymentStatus.REFUNDING],
    PaymentStatus.REFUNDING: [PaymentStatus.REFUNDED,
                              PaymentStatus.PAID],       # 退款失败回到 PAID
    PaymentStatus.FAILED:    [],                          # 终态
    PaymentStatus.CLOSED:    [],                          # 终态
    PaymentStatus.REFUNDED:  [],                          # 终态
}

def transition(current: PaymentStatus, target: PaymentStatus):
    if target not in VALID_TRANSITIONS.get(current, []):
        raise ValueError(
            f"非法状态流转: {current.value} → {target.value}"
        )
    return target

有了这个状态机,前面说的"延迟回调覆盖退款"的 bug 就不可能发生了——REFUNDED → PAID 不在合法转换列表里,直接抛异常。


七、幂等性:同一件事只做一次

支付系统里最危险的事情不是"支付失败",而是"支付了两次"。用户点了两次按钮、回调重复发了三次、网络超时重试——任何一种情况都可能导致重复扣款。

小陈的方案是 分布式锁 + 幂等键

import redis

r = redis.Redis()

async def create_payment_idempotent(order_id: str, channel: str,
                                     amount: int):
    # 幂等键 = 订单号 + 渠道 + 金额
    idem_key = f"pay:idem:{order_id}:{channel}:{amount}"

    # 1. 尝试获取分布式锁(防并发重复请求)
    lock = r.lock(f"lock:{idem_key}", timeout=10)
    if not lock.acquire(blocking_timeout=3):
        raise Exception("请求处理中,请稍候")

    try:
        # 2. 检查是否已有支付记录
        existing = await get_payment_by_order(order_id)
        if existing:
            return existing  # 直接返回已有结果,不重复创建

        # 3. 首次请求,真正创建支付
        result = await do_create_payment(order_id, channel, amount)
        return result
    finally:
        lock.release()

同样的思路也应用在回调处理上:

@app.post("/api/v1/notify/{channel}")
async def unified_notify(channel: str, request: Request):
    adapter = router.get_adapter(channel)
    headers = dict(request.headers)
    body = await request.body()

    # 1. 让适配器验签 + 解析(每个渠道的签名逻辑不同,但输出格式统一)
    result = await adapter.verify_notify(headers, body)

    # 2. 幂等检查:已支付的订单不重复处理
    payment = await get_payment(result["order_id"])
    if payment.status == PaymentStatus.PAID:
        return adapter.success_response()  # 直接返回成功

    # 3. 状态机校验 + 更新
    transition(payment.status, result["status"])
    await update_payment(payment.id, result)

    # 4. 通知业务系统(异步,不阻塞回调响应)
    await notify_merchant(payment)

    return adapter.success_response()

这段代码把「验签」「幂等」「状态机」「通知」串成了一条清晰的管道。不管是哪个渠道的回调进来,走的都是这一条路。


八、补偿机制:为"不确定性"兜底

支付系统有一个残酷的现实:你永远不能假设网络是可靠的。回调可能丢失,API 可能超时,数据库可能短暂不可用。所以需要多层补偿:

8.1 定时轮询

async def poll_pending_orders():
    """每 30 秒检查一次 PENDING 超过 5 分钟的订单"""
    pending_orders = await get_orders_by_status(
        status=PaymentStatus.PENDING,
        older_than_minutes=5
    )
    for order in pending_orders:
        adapter = router.get_adapter(order.channel)
        real_status = await adapter.query_payment(order.channel_order_id)

        if real_status != order.status:
            transition(order.status, real_status)
            await update_payment(order.id, {"status": real_status})
            await notify_merchant(order)

这是"双保险"策略:即使回调丢了,轮询也能补上。支付行业的潜规则是——不信任任何单一通知机制

8.2 通知重试

当网关需要通知业务系统时,也可能失败。小陈用了指数退避重试:

async def notify_merchant_with_retry(payment, max_retries=8):
    """通知业务系统,失败时指数退避重试"""
    for attempt in range(max_retries):
        try:
            resp = await http_post(payment.notify_url, payment.to_dict())
            if resp.status_code == 200:
                return  # 通知成功
        except Exception as e:
            pass

        # 指数退避:1s → 2s → 4s → 8s → 16s → 32s → 64s → 128s
        await asyncio.sleep(2 ** attempt)

    # 所有重试都失败,标记待人工处理
    await mark_notify_failed(payment.id)

8.3 日终对账

每日凌晨 2:00 ──→ 下载各渠道账单文件
                     │
                     ▼
              逐笔与本地订单比对
              ┌──────┴──────┐
              ▼             ▼
         金额/状态一致   发现差异
              │             │
              ▼             ▼
           标记对平     记录差异明细
                        ├── 本地有渠道无 → 可能是测试单或关单
                        ├── 渠道有本地无 → 严重!需补录
                        └── 金额不一致   → 严重!需人工核查

对账是支付系统的最后一道防线。 前面的幂等、状态机、回调处理做得再好,也不能保证 100% 正确。对账就是那个每天帮你"查缺补漏"的守门员。


九、完整的下单流程:走一遍

让我们以一笔微信支付为例,走完整个统一网关的流程:

用户在收银台选择"微信支付" → 点击"确认支付"
    │
    ▼
① 业务系统调用统一 API
   POST /api/v1/payments
   { "order_id": "ORD001", "amount": 9999, "channel": "wechat_native" }
    │
    ▼
② 接入层:参数校验、鉴权、限流 ✓
    │
    ▼
③ 核心层:
   → 幂等检查(这个订单号下过单吗?没有,继续)
   → 创建网关订单(状态:PENDING)
   → 路由引擎(channel = "wechat_native" → WechatAdapter)
    │
    ▼
④ 渠道层(WechatAdapter):
   → 拼装微信 Native 下单参数
   → RSA-SHA256 签名
   → 调用微信 API,获取二维码链接
    │
    ▼
⑤ 返回给业务系统:
   { "payment_id": "PAY_xxx", "credential": {"qr_code": "weixin://..."} }
    │
    ▼
⑥ 前端展示二维码,用户扫码支付
    │
    ▼
⑦ 微信服务器发送回调到统一回调入口:
   POST /api/v1/notify/wechat_native
    │
    ▼
⑧ 核心层:
   → WechatAdapter.verify_notify() 验签 + 解析
   → 幂等检查(已支付?没有,继续)
   → 状态机:PENDING → PAID ✓
   → 更新订单状态
   → 异步通知业务系统
   → 返回 {"code": "SUCCESS"} 给微信
    │
    ▼
⑨ 业务系统收到通知 → 发货 / 开通服务

整个过程中,业务系统只和接入层打交道,完全不知道底层是微信支付还是 Stripe。如果哪天要把微信支付换成另一个渠道,业务系统的代码 一行都不用改


十、什么时候该建统一网关?

小陈的经验总结:

你处于什么阶段?
    │
    ├─── MVP / 初创期(1 个渠道)
    │       └──→ 直接用渠道 SDK,别过度设计
    │            投入:1~2 天
    │
    ├─── 成长期(2~3 个渠道)
    │       └──→ 简单统一层 + 直连渠道
    │            开始抽象公共逻辑,但不用做得太重
    │            投入:1~2 周
    │
    └─── 规模化(4+ 渠道 / 多业务线)
            └──→ 自建统一支付网关(本文方案)
                 三层架构 + 状态机 + 幂等 + 对账
                 投入:1~2 月

如果你的团队人手有限,也可以考虑现有的开源或商业方案:

方案 语言 特点 适合
自建(本文方案) 任意 完全可控,按需定制 中大型企业,支付是核心业务
Jeepay Java 开源聚合支付,支持支付宝/微信 国内中小型
PayPal Braintree 多语言 SDK 国际化,聚合卡支付 + PayPal 纯出海业务
Ping++ 多语言 SDK 国内老牌聚合支付 SaaS 想快速接入不想自建
Stripe Connect 多语言 SDK 平台型支付(分账) 多边市场、平台经济

十一、踩坑清单:小陈的血泪经验

经历了三个月的实战,小陈总结了这些教训,希望后来人少走弯路:

1. 签名验证失败

  • 原因:参数排序错误 / 编码问题 / 密钥不匹配
  • 教训:用官方 SDK,不要自己拼签名串。 小陈在微信支付上自己拼签名串,调了两天才发现是 URL encode 的规则不一样

2. 回调收不到

  • 原因:回调地址不是公网 HTTPS / 处理超过 5 秒超时了
  • 教训:回调逻辑要轻量化——收到就存库,重活放异步队列。用 ngrok 等内网穿透工具在开发阶段调试

3. 金额精度问题

  • 原因:支付宝用元、微信用分,来回转换时浮点数精度丢失
  • 教训:内部一律用整数分存储。 $99.99 → 9999,展示时再除以 100

4. 重复支付

  • 原因:用户连点两次,或者重试逻辑没做幂等
  • 教训:订单号 + 数据库唯一约束 + 分布式锁,三保险

5. 退款的坑比支付还多

  • 原因:有的渠道同步返回结果,有的异步通知;退款金额不能超过原始金额;部分退款后再退款的余额计算
  • 教训:退款状态要独立管理(REFUNDING → REFUNDED),不要和支付状态混在一起

6. 证书/密钥过期

  • 原因:微信支付 API 证书、支付宝应用公钥证书都有有效期
  • 教训:做证书有效期监控 + 自动告警,不要等到线上报错才发现

十二、全系列回顾

如果你是第一次看到这篇文章,建议从第 1 篇开始读。整个系列的阅读路线:

篇目 标题 你会了解到
第 1 篇 一笔订单的支付之旅:在线支付全景概览 支付行业全貌、四方模型、各渠道特点、如何选型
第 2 篇 一杯咖啡的扫码之旅:支付宝 & 微信支付 扫码支付原理、签名机制、回调处理、完整对接代码
第 3 篇 一件跨境商品的卡支付之旅:Stripe & 信用卡 Payment Intents、3DS 验证、PCI DSS 合规、前后端代码
第 4 篇 一位海外买家的安全支付之旅:PayPal OAuth 2.0、Smart Buttons、争议保护、Webhook
第 5 篇 当四条河流汇入一片海:统一支付网关(本文) 三层架构、策略模式、状态机、幂等、对账补偿

前瞻:当 AI Agent 开始代替人类做消费决策时,支付体系将迎来又一次范式变革——从"人操作支付"到"Agent 自主支付"。关于 AI Agent 支付的深度分析,可以阅读本系列的姊妹篇:当 AI Agent 接管你的钱包x402 协议深度解析


参考来源


欢迎关注公众号 coft,获取更多深度技术文章。在线支付系列到此完结,如果这个系列对你有帮助,欢迎转发分享。