如何从 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。
备注:内容仅供参考。
