单看一个 commit 是"feat: 加个微信支付"。 单看下一个 commit 是"feat: 加个 webhook relay"。 串起来看,是中国市场 SaaS 才有的一种架构变形——ICP 备案约束直接重塑了系统拓扑。
Day 1:补齐第三个支付渠道
派宝早就接了 Stripe + 支付宝。微信支付一直缺,直到这周才补上:
- 创建订单:返回支付地址,前端渲染成二维码
- 订单查询:用户在微信里付钱时,前端轮询订单状态
- 支付回调:验签 + AES-256-GCM 解密 + 升级租户
设计上的一个小决定:wechat_sign.py 用纯 stdlib 实现,139 行。
不是图省事。微信官方 Python SDK 拖一长串依赖(cryptography、requests、urllib3 老版本),上线就要重打镜像。stdlib 实现意味着:
- 容器层不变
- portal 那边 TypeScript 可以做镜像实现(
static-ip-portal/lib/payment/wechat-sign.ts),两端签名逻辑可逐行对照审计 - 升级到 APIv4 时只需要改这一个文件,不用换 SDK 版本
升级路径也复用了 Alipay 那套:PLAN_PRICE_FEN_CN 和 PLAN_QUOTAS 两个常量被两个 router 共享——保证微信付费的租户和支付宝付费的租户拿到完全一致的配额。
到这里都还是工程上的"add another channel"故事,没什么稀奇。
Day 2:发现 ICP 备案把所有兄弟项目堵死了
微信支付商户后台让我们填 notify_url——这是支付成功后微信回调的目标地址。
填了派宝的回调地址(派宝.com 域名下)——✅ 通过。
填到下一个项目 Portal 的回调地址(ip.digitalforce.cc 域名下)——❌ 拒绝。
理由:该域名未经 ICP 备案。
中国内地的事实:支付宝 + 微信支付的 notify_url 必须指向已 ICP 备案的域名。我们 4 个 CN 业务项目里,只有"派宝.com"(punycode: xn--pbt132b.com)走完了备案:
| 项目 | 域名 | ICP 状态 | 能直接收 notify_url |
|---|---|---|---|
| 派宝 | 派宝.com | ✅ 已备案 | ✅ |
| Portal | ip.digitalforce.cc | ❌ digitalforce.cc 未备案 | ❌ |
| AutoGlobal | autoglobal.ai | ❌ .ai 不可备案 | ❌ |
| Realnator | realnator.com | ❌ 未备案 | ❌ |
| ClawOps | clawops.digitalforce.cc | ❌ 同上 | ❌ |
正常解法是给每个项目单独备案——周期 20-40 工作日 × 4,加上各种公司材料反复来回。我们等不起。
所以做了另一个选择:让派宝成为整个家族的统一支付 webhook 入口。
转发架构
┌─────────────────────┐ ┌───────────────────────┐
│ Alipay / WeChat Pay │ ────notify_url────────► │ 派宝.com (派宝) │
│ (商户后台填的) │ (备案域名) │ 支付转发入口 │
└─────────────────────┘ └──────────┬────────────┘
│ 透明转发原始
│ POST body + headers
┌───────────────┼────────────────┐
▼ ▼ ▼
ip.digitalforce autoglobal.ai realnator.com
/webhook/... /webhook/... /webhook/...
(Portal 自己验签 + 处理)
新增两个转发入口:一个转发支付宝 form-encoded 回调,一个转发微信 APIv3 加密回调。
路由表用环境变量覆盖:PAYMENT_RELAY_{PROJECT}_{GATEWAY}——例如 PAYMENT_RELAY_PORTAL_WECHAT 指向 ip.digitalforce.cc 的 webhook 地址。
关键安全设计
很容易把转发器写成"先解密 + 验签,再转发已验证的数据给下游"。这是错的。我们的设计:
| 原则 | 为什么 |
|---|---|
| 派宝不预验签 | 私钥/AES-GCM key 留在每个项目里,不让派宝看见。派宝只搬运字节。 |
| 派宝透传原始 POST body | 微信 APIv3 的签名是 timestamp + nonce + body 的 SHA256-RSA。改一个字符整个签名废掉。 |
| 派宝立即返回成功 | 支付宝 5s、微信 10s 内必须收到 success token,否则触发重试风暴。派宝即返即转。 |
| 下游自己验签 | 收到转发的 alipay-* / wechatpay-* 头之后,目标项目用自己的私钥重新验签。Trust 边界没下移。 |
| fire-and-forget 转发 | 转发失败不影响派宝给网关回 success。网关侧的指数退避重试覆盖瞬时网络故障。 |
| strip hop-by-hop headers | Connection/Keep-Alive/Transfer-Encoding 不应该跨 hop 透传,但 Content-Type 和签名头要保留。 |
这个设计让派宝在合规边界上只承担"备案域名"这一项责任,密钥管理和支付逻辑还是各项目自治。
一个隐性的副作用:观测性收敛
意料之外的好处:现在 4 个项目的支付回调全部经过派宝同一个 access log。
[wechat-relay] target=portal forward_ok=true upstream_status=200 60ms
[alipay-relay] target=realnator forward_ok=true upstream_status=200 42ms
[wechat-relay] target=autoglobal forward_ok=false upstream_status=503 timeout=5s
一个 grafana panel 看穿所有项目的支付健康度。以前要登 4 台服务器分别 tail -f。
一个我们没解决的开放问题
下游项目验签失败时,回滚机制还没设计好。当前的"派宝立即对网关回 success + 转发失败被吞掉"模式意味着:如果 Portal 验签失败(比如配置过期的 platform cert),用户的钱已经付了,但 Portal 的订单状态不会变。
正在做的改进方向:
- 派宝侧 AuditLog 记每次转发结果
- 下游验签失败必须主动 webhook 回到派宝的
/relay/dlq异常队列 - dlq 触发运营告警
这块还在草案阶段,等下个迭代落实。
结论:合规约束是隐藏的架构师
这个故事的全部价值不在"我们写了一个转发器"——而在:
某个看似无关的政策约束("必须 ICP 备案才能填 notify_url")会向上传导,重塑你的系统拓扑。
如果你的 SaaS 要进中国市场,提前问自己:
- 你的网关回调域名能不能备案?
- 你的多产品矩阵里有没有可以备案的"母舰域名"?
- 你的转发器有没有正确处理签名透传?
跨过这个坑的成本可能是两天,也可能是两个月。