PAYATHON 2026

如何在 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 提供给商户的接口文档和凭据。在没有这些资料的情况下,不要猜测真实端点、字段名称或签名规则。

备注:内容仅供参考。