PAYATHON 2026

Rails 7 生产环境 GET 请求缺少模板且无法重定向

支付小周

结论

问题不在于 production 环境无法处理 GET 请求,而在于 paye action 执行后没有完成响应。

当前代码只会在以下条件成立时重定向:

@payment_response['status'] == 1

如果支付接口返回失败、status 是字符串 "1",或者第二次请求因重复使用同一个 txnid 被拒绝,代码就会跳过 redirect_to,直接运行到方法末尾。Rails 随后执行默认渲染,尝试查找 orders/paye.html.erb,于是出现:

OrdersController#paye is missing a template for request formats: text/html

对于“第一次成功、第二次失败”的情况,应重点检查这一行:

"txnid" => "#{@order.id}"

同一个订单每次都使用相同的交易编号。支付网关通常要求每次发起支付时使用唯一的 txnid,具体规则仍应以 Easebuzz 当前接口文档和实际响应为准。

为什么 Rails 会查找模板

Controller action 必须通过以下任一方式结束请求:

  • redirect_to
  • render
  • head
  • 发送文件或流式响应

当前代码只有成功分支:

if @payment_response['status'] == 1
  redirect_to(...)
end

这里没有失败分支。只要条件不成立,Rails 就会进行隐式渲染,相当于尝试:

render :paye

由于项目中没有 app/views/orders/paye.html.erb,Rails 因此报告模板缺失。

这条错误说明 action 已经通过路由进入并运行到了末尾,并不表示 Easebuzz::Payment.initiate 没有执行。

先记录支付接口的真实响应

不要在 Passenger 部署中依赖 puts。它会把内容写入进程的标准输出,而这些输出可能由 Apache、Passenger 或 systemd 接管,不一定会进入 Rails 的 production 日志。

应改用 Rails logger:

Rails.logger.info(
  "Easebuzz initiation result: status=#{@payment_response['status'].inspect}"
)

诊断期间也可以记录过滤后的响应:

Rails.logger.info(
  "Easebuzz initiation response: #{@payment_response.except('data').inspect}"
)

不要将密钥、支付令牌、完整手机号或其他敏感数据写入日志。data 很可能包含不适合长期保存的支付信息。

另外,redirect_to 不会立即终止 Ruby 方法。最好显式使用 return,让控制流更清楚:

return redirect_to(...)

补全成功和失败分支

可以先将 action 调整为下面的结构:

def paye
  txnid = "#{@order.id}-#{SecureRandom.uuid}"

  payment_response = Easebuzz::Payment.initiate(
    {
      "txnid" => txnid,
      "amount" => amount.to_f,
      "firstname" => current_user.name,
      "email" => current_user.email,
      "phone" => current_user.phone_number,
      "productinfo" => "Payment for Order #{@order.id}",
      "surl" => successE_order_url(@order, protocol: "https"),
      "furl" => failedTransaction_order_url(@order, protocol: "https")
    }
  )

  Rails.logger.info(
    "Easebuzz initiation: order_id=#{@order.id} " \
    "txnid=#{txnid} status=#{payment_response['status'].inspect}"
  )

  if payment_response["status"].to_s == "1" && payment_response["data"].present?
    return redirect_to(
      "https://pay.easebuzz.in/pay/#{payment_response['data']}",
      allow_other_host: true,
      status: :see_other
    )
  end

  Rails.logger.error(
    "Easebuzz initiation failed: order_id=#{@order.id} " \
    "response=#{payment_response.except('data').inspect}"
  )

  redirect_to order_path(@order),
              alert: "无法发起支付,请稍后重试。"
rescue StandardError => e
  Rails.logger.error(
    "Easebuzz initiation exception: order_id=#{@order&.id} " \
    "#{e.class}: #{e.message}"
  )

  redirect_to order_path(@order),
              alert: "支付服务暂时不可用,请稍后重试。"
end

这段代码做了以下调整:

  1. 每次支付尝试都生成唯一的 txnid。
  2. 同时兼容接口返回整数 1 和字符串 "1"。
  3. 成功后显式执行 return redirect_to。
  4. 失败时同样执行 redirect_to,避免触发隐式模板渲染。
  5. 使用 Rails.logger 记录执行结果。
  6. 回调地址通过 URL helper 生成,减少手工拼接出错的可能。

如果业务要求将支付重试关联到订单,建议单独建立支付记录,保存 txnid、订单、支付状态和网关响应之间的关系,而不是只使用 order.id。

检查 status 的实际类型

开发和生产环境可能使用不同的网关、SDK 配置或接口响应,因此不能假设 status 一定是整数。

例如:

1 == 1
# true

"1" == 1
# false

原条件可能在一个环境中成立,在另一个环境中却不成立:

if @payment_response["status"] == 1

诊断时可以记录字段的值和类型:

Rails.logger.info(
  "Easebuzz status value=#{@payment_response['status'].inspect}, " \
  "class=#{@payment_response['status'].class}"
)

确认 Easebuzz 的响应格式后,再决定是否使用:

payment_response["status"].to_s == "1"

重定向前也不应只检查状态字段,还需要确认 data 存在:

payment_response["status"].to_s == "1" &&
  payment_response["data"].present?

支付发起操作应使用 POST

发起支付会创建交易或改变业务状态,不适合使用 GET。GET 请求可能被浏览器预加载、缓存、爬虫访问,也可能因用户刷新页面而重复发起。

路由可以改为:

resources :orders do
  member do
    post :paye
    post :successE
    post :failedTransaction
  end
end

页面上使用提交按钮:

<%= button_to "立即支付",
              paye_order_path(order),
              method: :post,
              data: { turbo: false } %>

Easebuzz 的成功和失败回调究竟应配置为 GET 还是 POST,取决于网关实际使用的回调方式。如果网关通过 POST 回调,就必须提供 POST 路由;如果浏览器通过 GET 返回,则应配置 GET 路由。最终应以真实回调请求和 Easebuzz 文档为准。

将 paye 改为 POST 并不能自动解决模板错误。只要 action 运行到末尾时没有执行 render、redirect_to 或 head,Rails 仍会查找模板。

检查 production 回调地址

生产环境应优先使用 HTTPS:

"surl" => successE_order_url(@order, protocol: "https"),
"furl" => failedTransaction_order_url(@order, protocol: "https")

也可以在 production 配置中设置默认主机:

# config/environments/production.rb
Rails.application.routes.default_url_options = {
  host: "example.in",
  protocol: "https"
}

之后直接使用:

successE_order_url(@order)
failedTransaction_order_url(@order)

还需要确认:

  • 回调路由确实存在。
  • action 名称与路由完全一致。
  • 域名没有使用示例中的括号。
  • HTTPS 证书有效,且公网可以访问回调地址。
  • production 使用了正确的 Easebuzz key、salt 和生产接口。
  • Web 服务器没有阻止相应的 HTTP 方法。

Ruby 方法名区分大小写,建议统一使用小写 snake_case,例如:

def paye
end

def success_e
end

def failed_transaction
end

对应路由也应保持一致,避免混用 payE、paye、successE 等名称。

最可能的完整故障链

结合“第一次成功、第二次开始报模板缺失”这一现象,最可能的过程是:

  1. 第一次请求使用 order.id 作为 txnid,支付网关接受了请求。
  2. 接口返回成功状态,代码执行 redirect_to。
  3. 第二次请求仍然使用相同的 txnid。
  4. 网关返回失败状态,或者返回了不同格式的状态。
  5. if @payment_response['status'] == 1 条件不成立。
  6. action 没有失败分支,继续运行到末尾。
  7. Rails 尝试渲染 orders/paye.html.erb。
  8. 模板不存在,因此报错。

需要同时修正唯一交易编号、响应类型判断、失败分支和日志记录方式。添加一个空的 paye.html.erb 只会掩盖真正的支付失败,并不能解决问题。

备注:内容仅供参考。