Clojure Web 开发中如何选择支付网关处理库?
结论
Clojure 生态目前没有公认且长期维护的“Active Merchant 等价物”,也没有一个成熟方案能统一覆盖多家支付网关。实际项目中更适合这样处理:
- 优先使用支付服务商提供的托管收银台或支付组件。
- 在 Clojure 中直接调用服务商的官方 Java SDK。
- 如果没有合适的 Java SDK,再接入其 HTTPS API。
- 增加一层轻量的 Clojure 适配代码,将支付服务商与业务逻辑隔离开。
Clojure 运行在 JVM 上,通常可以直接复用现有的 Java SDK。Compojure、Ring 等框架负责 HTTP 路由和请求处理,不会限制支付 SDK 的选择。
推荐的集成方式
支付流程通常包括三个环节:
- 服务端创建支付会话或支付订单。
- 浏览器跳转到服务商托管的支付页面,或加载服务商提供的安全支付组件。
- 服务端接收并验证 webhook,再根据通知更新订单状态。
信用卡号、CVV 等原始卡片数据应尽量避免经过自己的 Ring 应用。使用托管收银台、Hosted Fields 或令牌化组件,可以降低安全风险,缩小 PCI DSS 的合规范围。
选择支付服务商时,需要确认以下事项:
- 是否支持目标国家、币种和结算账户。
- 是否有官方 Java SDK。
- 是否提供托管收银台或令牌化组件。
- webhook 是否包含可验证的签名。
- 是否支持幂等请求、退款和支付状态查询。
- SDK 和 API 文档是否仍在维护。
服务商可能调整支持范围和接口名称,接入时应以其最新官方文档为准。
用 Clojure 封装支付网关
不要在各个路由处理函数中直接调用第三方 SDK。可以先定义一个简单协议:
(ns shop.payment.gateway)
(defprotocol PaymentGateway
(create-payment! [gateway payment])
(fetch-payment [gateway provider-payment-id])
(refund-payment! [gateway refund])
(verify-webhook [gateway raw-body headers]))
支付对象只保存业务需要的信息:
(def payment-request
{:order-id "order-20260901-001"
:amount-minor 1999
:currency "CNY"
:return-url "https://shop.example.com/payment/return"
:idempotency-key "order-20260901-001:create"})
amount-minor 表示最小货币单位。在人民币场景中,1999 通常表示 19.99 元。金额不要使用浮点数保存,以免浮点计算产生舍入误差。
适配层负责把统一的数据结构转换成服务商要求的 Java 对象或 HTTP 请求:
(ns shop.payment.example
(:require [shop.payment.gateway :as gateway]))
(defrecord ExampleGateway [client webhook-secret]
gateway/PaymentGateway
(create-payment! [_ payment]
;; 在这里调用支付服务商的官方 Java SDK。
;; 不同 SDK 的类名、构造方式和参数会变化,
;; 应按照当前官方文档实现。
(throw (UnsupportedOperationException.
"Implement using the selected provider SDK")))
(fetch-payment [_ provider-payment-id]
(throw (UnsupportedOperationException.
"Implement using the selected provider SDK")))
(refund-payment! [_ refund]
(throw (UnsupportedOperationException.
"Implement using the selected provider SDK")))
(verify-webhook [_ raw-body headers]
;; 必须按照支付服务商规定的算法验证签名。
;; 不要仅凭请求中的 payment status 更新订单。
(throw (UnsupportedOperationException.
"Implement using the provider webhook verifier"))))
有了这层适配,订单系统只需要依赖 PaymentGateway。以后更换支付服务商时,不必重写整个结账流程。
在 Ring 中创建支付
Ring handler 可以校验订单、调用支付网关,然后将客户端引导到支付页面:
(ns shop.handlers.checkout
(:require [shop.payment.gateway :as gateway]
[ring.util.response :as response]))
(defn create-checkout-handler
[payment-gateway order-repository]
(fn [request]
(let [order-id (get-in request [:path-params :order-id])
order (get-order order-repository order-id)]
(cond
(nil? order)
(-> (response/response "Order not found")
(response/status 404))
(not= :pending (:status order))
(-> (response/response "Order cannot be paid")
(response/status 409))
:else
(let [payment
(gateway/create-payment!
payment-gateway
{:order-id (:id order)
:amount-minor (:total-minor order)
:currency (:currency order)
:return-url "https://shop.example.com/payment/return"
:idempotency-key (str (:id order) ":create")})]
(response/redirect (:checkout-url payment)))))))
这里的 get-order 只是示意函数。实际实现还需要通过数据库事务或其他并发控制机制确认订单金额和状态,不能直接采用浏览器提交的金额。
正确处理 webhook
支付完成页面只能说明用户返回了商店,不能用来判断款项是否到账。最终支付状态应以经过签名验证的 webhook 为准,必要时还要主动向支付服务商查询。
(ns shop.handlers.webhook
(:require [shop.payment.gateway :as gateway]
[ring.util.response :as response]))
(defn webhook-handler
[payment-gateway order-repository]
(fn [request]
(let [raw-body (:raw-body request)
result (gateway/verify-webhook
payment-gateway
raw-body
(:headers request))]
(if-not (:valid? result)
(-> (response/response "Invalid signature")
(response/status 400))
(do
(case (:event-type result)
:payment-succeeded
(mark-order-paid!
order-repository
{:order-id (:order-id result)
:provider-payment-id (:payment-id result)
:event-id (:event-id result)})
:payment-failed
(mark-payment-failed!
order-repository
{:order-id (:order-id result)
:event-id (:event-id result)})
nil)
(response/response "OK"))))))
验签通常要使用未经 JSON 解析和字符转换的原始请求体。如果中间件提前读取并重新构造了 body,验签可能失败。应根据所用 Ring adapter 和中间件的配置,明确保留原始字节。
mark-order-paid! 也必须支持幂等处理。支付服务商可能多次发送同一个事件,应用应保存唯一的 event-id 或支付流水号,避免重复发货或重复记账。
如果只能直接调用 HTTP API
如果服务商没有 Java SDK,可以使用成熟的 HTTP 客户端和 JSON 库调用其 REST API。相关调用仍应放在网关适配层中:
(defn provider-request
[{:keys [http-client api-key]} method url body idempotency-key]
(http-client
{:method method
:url url
:headers {"Authorization" (str "Bearer " api-key)
"Content-Type" "application/json"
"Idempotency-Key" idempotency-key}
:body body
:connect-timeout 5000
:request-timeout 10000}))
这段代码只展示整体结构。http-client 的参数格式取决于实际选用的 Clojure HTTP 库。接入时应按照服务商文档处理状态码、超时、重试和错误响应,不能把所有非成功响应都视为支付失败。
需要分别处理这些情况:
- 请求被明确拒绝。
- 请求超时,支付结果未知。
- 服务商返回临时错误。
- 本地请求已经成功,但响应在网络传输中丢失。
如果请求结果未知,不要立即创建另一笔支付。可以使用相同的幂等键重试,或者先根据订单号查询原请求的状态。
安全与实现注意事项
- API 密钥应保存在环境变量或密钥管理系统中,不能提交到代码仓库。
- 不要记录卡号、CVV、完整支付令牌、Authorization header 或 webhook secret。
- 金额和币种必须来自服务端的订单数据,不能采用客户端传入的参数。
- 创建支付和退款等操作都应使用稳定且唯一的幂等键。
- webhook 必须验签,同时校验金额、币种、订单号和支付账户。
- 数据库应分别保存本地订单号和支付服务商流水号。
- 不要让 HTTP 请求线程无限等待支付结果。
- 只有明确可以安全重试的操作才能自动重试,并且要设置退避策略和重试次数上限。
- 日志应保留请求关联 ID、订单号和服务商事件 ID,以便排查问题,同时过滤敏感信息。
- 测试应使用服务商提供的 sandbox 或 test mode,不能使用生产密钥进行开发测试。
没有现成的 Clojure 版 Active Merchant,并不会妨碍支付功能的实现。通过 JVM 互操作复用官方 Java SDK,再使用 PaymentGateway 之类的协议增加一层封装,通常比依赖一个覆盖多家网关但维护状态不明的第三方库更可靠。
备注:内容仅供参考。
