Medusa 移动支付从零接入实战:支付宝、微信支付一次讲透的避坑指南
你负责的跨境电商小程序定在月底上线,老板甩来一句"加上支付宝和微信支付,越快越好"。你打开 Medusa 项目,盯着 packages/modules/payment/ 目录一时不知从何下手——这很正常,移动支付接入的难点从来不在"调通 API",而在于签名、回调、退款、对账这一整套链路里,藏着太多让人半夜惊醒的暗坑。这篇文章不打算罗列概念,而是顺着一条真实的接入路线,把 Medusa 里跑通支付宝、微信支付的全过程拆给你看。读完你会发现:先搞懂架构,再写代码,一切都会顺很多。
动手之前,先搞懂 Medusa 把钱放在哪
Medusa 的支付系统不是一堆零散代码,而是一个边界清晰的模块。核心逻辑集中在 packages/modules/payment/:services/payment-module.ts 负责支付会话、收款单、捕获、退款这些业务编排,services/payment-provider.ts 则是所有第三方支付商的"接线板"。
真正关键的是抽象出来的支付商接口。从 payment-provider.ts 能看到,Medusa 对每个接入方只要求实现一套固定的动作:创建会话、更新会话、授权、查询状态、捕获、取消、退款,外加一个处理异步回调的 webhook 入口。这意味着不管你是接 Stripe、支付宝还是微信,插件只要补齐这套动作,上层业务一行都不用改。
记住这个思路:Medusa 不关心"谁在收钱",只关心"每个收钱的人有没有把这套动作做完"。
不用从零开始:把 Stripe 当教科书抄
Medusa 官方内置的 Stripe 支付商位于 packages/modules/providers/payment-stripe/,它就是你写支付宝、微信插件的最佳参照物。目录结构很有讲究:
core/stripe-base.ts:一个继承自AbstractPaymentProvider的抽象基类,把创建支付单、金额换算、签名校验等公共逻辑全部收拢在这里;services/stripe-provider.ts:真正的插件类,继承基类并声明自己的标识符,几十行代码就能注册成一个可用支付商;utils/get-smallest-unit.ts:处理"元转分"这类金额单位换算的小工具,写支付宝时同样用得上。
模仿这套结构,你的支付宝插件可以这样组织:
// 示意:自定义支付商的骨架
class AlipayProviderService extends AlipayBase {
static identifier = "pp_alipay" // 注册标识
// 继承基类,覆写支付相关的动作方法
// initiatePayment(): 创建预下单,返回唤起支付的参数
// getPaymentStatus(): 主动查询订单状态
// getWebhookActionAndData(): 解析支付宝异步通知
}
// 启动时校验配置是否齐全,缺 appId / 私钥就直接报错
static validateOptions(options) {
// 缺配置就 throw,避免带着残缺配置上线
}
支付商文件只需放在项目的 src/providers/ 下并正确注册,Medusa 的模块加载器会自动发现并把它注入容器。这就是整套架构最妙的地方:新增一种支付方式,等于新增一个实现固定接口的类,而不是改动订单、购物车这些既有代码。
三步打通支付宝支付链路
第一步:预下单与签名
用户在结算页选择支付宝后,Medusa 会调用你插件的创建会话方法。这一步你只需要做两件事:一是用商户私钥对订单参数做 RSA2 签名(金额、订单号、商品描述都要参与),二是把签名后的请求发给支付宝获取交易串。签名是新手最容易翻车的地方——参数排序必须严格按支付宝约定的字典序,多一个空格、少一个字段,签名校验就直接失败。
第二步:等待异步通知
用户付完钱,支付宝会往你配置的回调地址推一条异步通知。这里是整套流程的高危区:
- 验签先行:用支付宝公钥对通知内容做验签,验不过的请求直接丢弃,别碰任何业务逻辑;
- 幂等处理:支付宝的通知可能重复推送,收到
trade_status为已支付的通知时,要先查这笔订单是否已处理过; - 应答格式:处理成功后必须原样返回
success字样,否则支付宝会按失败重试,重试次数一多还会把你划进风险名单。
Medusa 的 webhook 入口会自动把原始请求体交给你插件里的解析方法,你要做的就是在验签通过后,把支付状态同步给 Medusa 的支付模块,让它推进订单状态。
第三步:配置注册
在项目的 medusa-config 里把插件挂上,并按环境注入密钥:
// medusa-config.js 片段
modules: [{
resolve: "./src/providers/alipay", // 本地支付商路径
options: {
appId: process.env.ALIPAY_APP_ID, // 应用ID
privateKey: process.env.ALIPAY_PRIVATE_KEY, // 商户私钥,别硬编码
alipayPublicKey: process.env.ALIPAY_PUBLIC_KEY // 支付宝公钥
}
}]
密钥务必走环境变量,明文写在配置文件里的后果,不用我多说。
微信支付:一个商户号,四种场景
微信支付比支付宝复杂的地方在于场景分化:小程序里用 JSAPI、App 内用 APP 支付、手机浏览器用 H5、PC 网页用 Native。四种场景共用一套签名与回调机制,差别主要在调起参数上。
- 小程序 / JSAPI:需要前端传
openid,用 code 换 openid 这一步建议放在服务端做,别把appsecret暴露给前端; - APP 支付:客户端拿到预支付参数后唤起微信,服务端只负责下单和验签;
- H5 支付:注意微信会校验
Referer域名白名单,测试时最容易在这里卡壳。
插件设计上,可以在基类里实现下单、验签、退款这些公共逻辑,四个场景各写一个子类复用,对应关系一目了然。
回调验证这样写最稳
微信的回调消息体本身经过加密,需要先用 API 密钥做 AES 解密,再验签、解析 transaction_id,最后核对金额与商户订单号。这里有个很容易被忽略的细节:回调里的金额要和本地订单做一致性比对,只查"订单号存在"是不够的——攻击者完全可以伪造一笔小金额通知来试探你的校验逻辑。
一个稳妥的写法是:验签 → 解密 → 比对金额与状态 → 幂等落库 → 返回成功应答,五步缺一不可。Medusa 侧,你会把最终结果映射为支付模块的会话状态,由它驱动订单流转,而不是在回调里直接改订单表。
退款与对账:别等出事了才想起它们
支付平台的对账单(支付宝日账单、微信账单文件)建议每天拉取一次,和本地支付记录做逐笔比对。Medusa 的支付模块本身有完整的支付记录、捕获、退款数据模型(见 packages/modules/payment/src/models/ 下的 payment、refund、capture),对账时可以据此做脚本化核对,而不是靠人工翻后台。
退款接口要走支付平台提供的退款 API,处理"全额退"和"部分退"两种场景,退款结果通常也是异步返回,记得监听退款回调。
从沙箱到生产的一次完整演练
支付宝的沙箱环境和微信的模拟支付工具都能让你在真实流程里走通下单、支付、回调、退款全链路,唯一要注意的是沙箱的 AppID 和密钥与生产完全不同,务必分开配置。Medusa 本身就支持按环境区分配置,把两套密钥分别放进 .env.dev 和 .env.prod 就能安全隔离。
演练清单按这个顺序走一遍:
- 沙箱里完成一笔完整支付,确认订单状态正确流转
- 模拟回调重复推送,验证幂等处理不产生重复订单
- 用错误金额/伪造签名触发回调,确认被拒之门外
- 发起全额退与部分退,核对后台退款记录
- 拉取一天的对账单,跑通自动比对脚本
生产环境上线时,把支付相关的错误日志接到监控告警里,设置支付失败率阈值。支付平台通常不保证回调的即时性,主动查询接口(如支付宝的订单查询)可以作为兜底——建议加一个定时任务,对长时间未回调的订单做主动对账。
上线前必查清单
- 所有密钥走环境变量,杜绝明文入库
- 回调地址强制 HTTPS,签名与验签逻辑单元测试覆盖
- 金额单位换算统一为最小单位(分),避免浮点误差
- 幂等键设计到位,回调、退款均可重放安全
- 准备好回滚方案:出问题时优先降级支付方式而非全站下线
写在最后
回头看,支付宝和微信支付的接入难点并不在 API 本身,而在于你对 Medusa 模块化边界的理解,以及签名、回调、幂等这些"看不见的功夫"。把 packages/modules/payment/ 的结构吃透,再照着 payment-stripe 这套官方模板套一遍,你会发现接入过程其实相当顺滑。
下一步建议你做的:先把仓库 https://gitcode.com/GitHub_Trending/me/medusa clone 下来,跑通 Stripe 的本地演示,再动手写自己的支付宝插件——从"抄作业"开始的接入,永远比从零硬啃快。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考





