PAYATHON 2026

如何从 Ruby on Rails 发送 XML POST 请求

支付阿杰

结论

Rails 无需使用专门的 XML 请求组件,Ruby 标准库 Net::HTTP 就能完成请求。先生成完整的 XML,通过 URI.encode_www_form 编码为 xmldata 参数,再向 HTTPS 地址发送 POST 请求。

如果接口明确要求把 xmldata 放在 URL 查询字符串中,可以这样写:

require "net/http"
require "uri"

xml_data = <<~XML
  <?xml version="1.0" encoding="UTF-8"?>
  <posnetRequest>
    <mid>YOUR_MERCHANT_ID</mid>
    <tid>YOUR_TERMINAL_ID</tid>
    <sale>
      <!-- 根据支付接口文档填写字段 -->
    </sale>
  </posnetRequest>
XML

uri = URI("https://setmpos.ykb.com/PosnetWebService/XML")
uri.query = URI.encode_www_form(xmldata: xml_data)

request = Net::HTTP::Post.new(uri)
request["Accept"] = "application/xml"

http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.open_timeout = 10
http.read_timeout = 30

response = http.request(request)

puts response.code
puts response.body

URI.encode_www_form 会正确编码 <、>、换行和空格,因此不要手动拼接或转义 XML。

在 Rails 中封装为服务对象

支付请求一般不应直接写在 Controller 中,可以将它封装成独立的客户端类:

# app/services/posnet_client.rb
require "net/http"
require "uri"

class PosnetClient
  ENDPOINT = "https://setmpos.ykb.com/PosnetWebService/XML"

  def send_request(xml_data)
    uri = URI(ENDPOINT)
    uri.query = URI.encode_www_form(xmldata: xml_data)

    request = Net::HTTP::Post.new(uri)
    request["Accept"] = "application/xml"

    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = true
    http.open_timeout = 10
    http.read_timeout = 30

    response = http.request(request)

    unless response.is_a?(Net::HTTPSuccess)
      raise "Payment request failed: HTTP #{response.code}"
    end

    response.body
  rescue Net::OpenTimeout, Net::ReadTimeout => e
    raise "Payment service timeout: #{e.message}"
  end
end

调用方式如下:

response_xml = PosnetClient.new.send_request(xml_data)

解析响应 XML 时,可以使用 Rails 项目中常见的 Nokogiri:

document = Nokogiri::XML(response_xml) do |config|
  config.strict.nonet
end

approved = document.at_xpath("//approved")&.text
response_code = document.at_xpath("//respCode")&.text

具体的 XPath 路径要根据支付服务实际返回的 XML 结构调整。

如果参数应放在 POST 请求体中

有些旧接口的示例 URL 虽然带有 xmldata,实际却要求把参数放入 application/x-www-form-urlencoded 格式的请求体。遇到这种情况,不要设置 uri.query,应改为:

uri = URI("https://setmpos.ykb.com/PosnetWebService/XML")

request = Net::HTTP::Post.new(uri)
request["Accept"] = "application/xml"
request.set_form_data("xmldata" => xml_data)

http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.open_timeout = 10
http.read_timeout = 30

response = http.request(request)

set_form_data 会自动设置:

Content-Type: application/x-www-form-urlencoded

应该使用查询字符串还是 POST 请求体,要以支付服务提供方的接口文档或有效请求示例为准。

如果接口要求原始 XML 请求体

另一类 XML 接口要求直接把 XML 写入请求体:

uri = URI("https://example.com/payment")

request = Net::HTTP::Post.new(uri)
request["Content-Type"] = "application/xml; charset=UTF-8"
request["Accept"] = "application/xml"
request.body = xml_data

这和传递 xmldata 参数是两种不同的方式,不能混用。

注意事项

  • 不要在源码中硬编码商户密钥、卡号、有效期或安全码。
  • 不要将完整的请求 URL、XML 或支付响应直接写入日志。查询字符串中的 xmldata 很容易被 Web 服务器、代理或监控系统记录。
  • 如果接口允许,将敏感数据放在 HTTPS 请求体中通常比放在 URL 中更合适,但最终仍须遵守支付接口协议。
  • 不要设置 VERIFY_NONE 来关闭 TLS 证书校验。
  • 支付请求超时不代表交易失败。再次提交前,应先调用交易查询接口确认状态,以免重复扣款。
  • XML 字段应通过 XML 构建器生成或正确转义,特别是动态值中包含 &、<、> 时。
  • 卡数据的处理、传输和日志策略需要符合适用的 PCI DSS 要求。生产环境应优先采用支付机构提供的令牌化方案或官方 SDK。

备注:内容仅供参考。