如何在 Ruby on Rails 中使用 UBA U-Collect payment gateway API?
结论
在 Ruby on Rails 中接入 UBA U-Collect 时,可以把支付流程封装成独立服务。Rails 后端负责创建唯一订单并生成网关请求,前端跳转到 U-Collect 支付页面。支付完成后,Rails 接收回调,再通过 U-Collect 的查询或验证接口确认最终状态。
U-Collect 的接口地址、字段名、签名算法和认证方式,可能因商户接入版本、国家或银行配置而异。下面的代码给出了一套可直接用于 Rails 项目的结构,但 URL、参数名和签名逻辑必须替换为 UBA 商户文档中的实际配置。不能只根据浏览器返回的结果判断支付是否成功。
接入前需要准备什么
先向 UBA 或负责 U-Collect 接入的技术团队申请以下资料:
- 商户编号,例如
merchant_id - API Key、Secret Key 或其他签名凭据
- 测试环境和生产环境地址
- 支付请求字段说明
- 签名生成与验签规则
- 支付结果通知地址的配置方式
- 订单查询或交易验证接口
- 测试卡号及测试交易规则
凭据应保存在 Rails Credentials 或环境变量中,不要写入控制器、JavaScript,也不要提交到 Git 仓库。
例如:
export UCOLLECT_MERCHANT_ID="your-merchant-id"
export UCOLLECT_SECRET_KEY="your-secret-key"
export UCOLLECT_PAYMENT_URL="https://gateway.example/payment"
export UCOLLECT_VERIFY_URL="https://gateway.example/verify"
这里的域名只是占位示例,并非 U-Collect 的真实接口地址。
建立支付订单
先在本地保存订单,再向支付网关发起请求。金额应从服务端订单中读取,不要直接使用浏览器传来的值,因为用户可以修改请求参数。
bin/rails generate model Payment \
reference:string:uniq \
amount:decimal \
currency:string \
status:string \
gateway_reference:string \
raw_response:json
bin/rails db:migrate
可以在模型中加入基本约束:
# app/models/payment.rb
class Payment < ApplicationRecord
validates :reference, presence: true, uniqueness: true
validates :amount, numericality: { greater_than: 0 }
validates :currency, presence: true
enum :status, {
pending: "pending",
paid: "paid",
failed: "failed"
}, default: :pending
end
货币金额最好使用 decimal 保存,不要使用浮点数。
封装 U-Collect 客户端
不要把所有支付网关逻辑都放进控制器。可以创建一个服务对象,统一处理请求、认证和交易验证。
下面的示例使用 Ruby 标准库,不依赖额外的 HTTP gem:
# app/services/u_collect/client.rb
require "net/http"
require "json"
require "openssl"
module UCollect
class Client
class Error < StandardError; end
def initialize
@merchant_id = ENV.fetch("UCOLLECT_MERCHANT_ID")
@secret_key = ENV.fetch("UCOLLECT_SECRET_KEY")
@payment_url = ENV.fetch("UCOLLECT_PAYMENT_URL")
@verify_url = ENV.fetch("UCOLLECT_VERIFY_URL")
end
def payment_parameters(payment:, callback_url:)
payload = {
merchant_id: @merchant_id,
reference: payment.reference,
amount: format("%.2f", payment.amount),
currency: payment.currency,
callback_url: callback_url
}
payload.merge(signature: generate_signature(payload))
end
def verify(reference:)
uri = URI(@verify_url)
payload = {
merchant_id: @merchant_id,
reference: reference
}
payload[:signature] = generate_signature(payload)
request = Net::HTTP::Post.new(uri)
request["Content-Type"] = "application/json"
request.body = JSON.generate(payload)
response = Net::HTTP.start(
uri.hostname,
uri.port,
use_ssl: uri.scheme == "https",
open_timeout: 5,
read_timeout: 15
) do |http|
http.request(request)
end
unless response.is_a?(Net::HTTPSuccess)
raise Error, "U-Collect verification failed with HTTP #{response.code}"
end
JSON.parse(response.body)
rescue JSON::ParserError
raise Error, "U-Collect returned an invalid JSON response"
end
private
def generate_signature(payload)
canonical_value = payload
.sort_by { |key, _value| key.to_s }
.map { |key, value| "#{key}=#{value}" }
.join("&")
OpenSSL::HMAC.hexdigest(
"SHA256",
@secret_key,
canonical_value
)
end
end
end
示例中的 HMAC-SHA256 和排序规则只用于展示代码结构。实际项目必须严格采用 UBA 文档规定的算法,包括字段顺序、大小写、字符编码和签名输出格式。任何一个细节不一致,都可能导致网关拒绝请求。
如果 U-Collect 使用 API Token、Basic Authentication、证书或加密报文,需要相应调整认证代码。
创建支付请求
先添加路由:
# config/routes.rb
Rails.application.routes.draw do
resources :payments, only: [:create] do
collection do
post :callback
end
end
end
控制器从服务端订单中读取金额,并创建支付记录:
# app/controllers/payments_controller.rb
class PaymentsController < ApplicationController
skip_forgery_protection only: :callback
def create
order = current_user.orders.find(params[:order_id])
payment = Payment.create!(
reference: SecureRandom.uuid,
amount: order.total_amount,
currency: "NGN",
status: "pending"
)
client = UCollect::Client.new
@gateway_url = ENV.fetch("UCOLLECT_PAYMENT_URL")
@gateway_fields = client.payment_parameters(
payment: payment,
callback_url: callback_payments_url
)
render :redirect_to_gateway
end
def callback
payment = Payment.find_by!(reference: params[:reference])
result = UCollect::Client.new.verify(reference: payment.reference)
if verified_success?(result, payment)
payment.update!(
status: "paid",
gateway_reference: result["gateway_reference"],
raw_response: result
)
else
payment.update!(
status: "failed",
raw_response: result
)
end
redirect_to payment_result_path(reference: payment.reference)
rescue UCollect::Client::Error => e
Rails.logger.error("U-Collect callback verification error: #{e.message}")
head :bad_gateway
end
private
def verified_success?(result, payment)
result["status"] == "SUCCESS" &&
result["reference"] == payment.reference &&
BigDecimal(result["amount"].to_s) == payment.amount &&
result["currency"] == payment.currency
end
end
SUCCESS、gateway_reference 等都是示例字段名,需要根据实际响应结构调整。
跳转到支付页面
如果 U-Collect 要求浏览器通过表单 POST 跳转,可以生成一个自动提交的表单:
<!-- app/views/payments/redirect_to_gateway.html.erb -->
<p>正在跳转到安全支付页面……</p>
<%= form_with url: @gateway_url,
method: :post,
local: true,
id: "u-collect-form" do |form| %>
<% @gateway_fields.each do |name, value| %>
<%= hidden_field_tag name, value %>
<% end %>
<%= form.submit "继续支付" %>
<% end %>
<script>
document.getElementById("u-collect-form").submit();
</script>
如果商户文档要求服务器调用 API,并从响应中获取支付 URL,就应由后端发出请求,读取网关返回的 URL,再通过 redirect_to 跳转。
正确处理支付回调
支付回调通常有两种形式:
- 浏览器返回地址:用户支付后被重定向回网站。
- 服务器通知地址:U-Collect 服务器主动通知 Rails。
这两类回调不能混在一起处理。用户可能在支付后关闭页面,浏览器参数也可能遭到篡改,因此支付状态必须以签名验证结果或服务器端交易查询结果为准。
处理回调时,至少要核对以下内容:
- 商户编号是否正确
- 本地订单号是否存在
- 网关状态是否表示成功
- 支付金额是否与本地订单一致
- 币种是否一致
- 网关交易号是否重复
- 回调签名是否有效
回调处理还必须保证幂等。同一个成功通知可能发送多次,业务逻辑不能因此重复发货或重复增加余额。例如:
payment.with_lock do
return if payment.paid?
payment.update!(
status: "paid",
gateway_reference: result["gateway_reference"],
raw_response: result
)
# 在确认状态首次变为 paid 后,再执行发货或记账逻辑。
end
如果发货流程较复杂,可以在事务提交后交给后台任务处理。
安全与上线注意事项
- 只通过 HTTPS 接收请求和回调。
- 不要在日志中记录 Secret Key、完整卡号、CVV 或未经处理的敏感报文。
- 不要让信用卡信息经过自己的 Rails 服务器,除非已经满足相关 PCI DSS 要求。
- 回调路由可以跳过 Rails CSRF 检查,但必须验证网关签名或调用交易查询接口。
- 为 HTTP 请求设置连接超时和读取超时。
- 记录网关交易号和原始响应,方便对账,但要先移除敏感字段。
- 测试重复回调、金额不一致、无效签名、超时、取消支付和查询接口不可用等情况。
- 测试环境与生产环境应使用不同的地址和凭据。
- 上线前确认 UBA 是否要求固定出口 IP、回调 IP 白名单、客户端证书或特定 TLS 配置。
按照本地建单、生成签名、跳转支付、接收回调、服务端验证和幂等更新的顺序组织代码,就能把 U-Collect 较为清晰地接入 Rails。请求最终能否成功,取决于 UBA 提供给商户的接口文档和凭据。在没有这些资料的情况下,不要猜测真实端点、字段名称或签名规则。
备注:内容仅供参考。
